[
  {
    "path": ".clang-format",
    "content": "---\nAlignAfterOpenBracket: AlwaysBreak\nAllowShortFunctionsOnASingleLine: None\nAlwaysBreakAfterReturnType: None\nBasedOnStyle: Webkit\nBreakBeforeBinaryOperators: NonAssignment\nBreakAdjacentStringLiterals: true\nAlwaysBreakBeforeMultilineStrings: true\nColumnLimit: 80\nIndentPPDirectives: BeforeHash\nNamespaceIndentation: None\nPenaltyReturnTypeOnItsOwnLine: 1000\nPointerAlignment: Right\nSortIncludes: false\nInsertBraces: true\nSpaceInEmptyBraces: Never\n"
  },
  {
    "path": ".dockerignore",
    "content": "/data\n/test\n/build\n"
  },
  {
    "path": ".editorconfig",
    "content": "[*]\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\nend_of_line = lf\n\n[*.py]\nindent_size = 4\nmax_line_length = 79\ntrim_trailing_whitespace = true\n\n[*.lua]\nindent_size = 2\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @LostArtefacts/dev @LostArtefacts/qa\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Report a reproducible problem in TRX\ntitle: \"[Bug] \"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please open one issue per bug.\n        Before submitting, read the bug reporting guide:\n        https://github.com/LostArtefacts/TRX/blob/develop/BUG_REPORTING.md\n\n  - type: dropdown\n    id: game_type\n    attributes:\n      label: Game type\n      description: What does this affect?\n      options:\n        - Original game\n        - Custom level\n        - Not sure\n    validations:\n      required: true\n\n  - type: dropdown\n    id: game\n    attributes:\n      label: Game\n      options:\n        - All games\n        - TR1\n        - TR2\n        - TR3\n        - Other\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: |\n        If your report is about TR3, please note that current support is limited to Lara's Home, India, and South Pacific.\n        Reports for other TR3 areas are still out of scope while that work is in progress, so those issues are not being accepted yet.\n\n  - type: input\n    id: trx_version\n    attributes:\n      label: TRX version\n      description: Use the exact version. It can be found in the main menu, or .exe properties.\n      placeholder: 1.2.3\n    validations:\n      required: true\n\n  - type: input\n    id: os\n    attributes:\n      label: Operating system\n      placeholder: Windows 11\n    validations:\n      required: true\n\n  - type: input\n    id: gpu\n    attributes:\n      label: GPU\n      placeholder: NVIDIA GeForce RTX 4070\n    validations:\n      required: false\n\n  - type: textarea\n    id: repro_steps\n    attributes:\n      label: Describe the bug in plain terms, and write the shortest steps that reproduce it.\n      placeholder: |\n        Lara falls through the bridge after loading a save near the switch room.\n        1. Load the attached save.\n        2. Walk onto the bridge.\n        3. Pull the switch.\n        4. Observe Lara falling through the floor.\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected_result\n    attributes:\n      label: What did you expect instead?\n      placeholder: Lara should stay on the bridge and the switch should work normally.\n    validations:\n      required: true\n\n  - type: textarea\n    id: attachments\n    attributes:\n      label: Attachments\n      description: Attach `TRX.log`, saves, screenshots, videos, and custom level files here if they help reproduce the issue.\n      placeholder: Drag and drop `TRX.log`, save files, screenshots, videos, or custom level files here.\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: confirmations\n    attributes:\n      label: Before submitting\n      options:\n        - label: I included enough detail to reproduce this.\n          required: true\n        - label: I attached `TRX.log` and any saves, media, or custom level files needed to reproduce this.\n          required: true\n        - label: This issue covers one bug.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Bug reporting guidelines\n    url: https://github.com/LostArtefacts/TRX/blob/develop/BUG_REPORTING.md\n    about: Read this before opening a bug report, especially for logs, savegames, and custom levels.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest an improvement, enhancement, or new idea\ntitle: \"[Feature] \"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Feature requests can be lightweight.\n        A clear problem statement is more useful than a fully detailed spec.\n\n  - type: textarea\n    id: idea\n    attributes:\n      label: What would you like to see?\n      description: Describe the idea in plain terms.\n      placeholder: I'd like photo mode to remember the last-used HUD visibility setting.\n    validations:\n      required: true\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: What problem would this solve?\n      description: Explain the pain point, limitation, or workflow this would improve.\n      placeholder: Re-enabling the same setting every time is repetitive when taking several screenshots in a row.\n    validations:\n      required: false\n\n  - type: textarea\n    id: proposed_behavior\n    attributes:\n      label: Extra context\n      description: Add mockups, examples, references, screenshots, videos, related issues, etc.\n      placeholder: Add any extra context here that helps.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/actions/prepare_macos_tooling/action.yml",
    "content": "name: Cache TRX MacOS Dependencies\n\ninputs:\n  FFMPEG_INSTALL_TMP_ARM64:\n    required: false\n    default: /opt/local/install_arm64\n  FFMPEG_INSTALL_TMP_X86_64:\n    required: false\n    default: /opt/local/install_x86_64\n  CACHE_SRC_DIR:\n    required: false\n    default: /opt/local\n  CACHE_DIR:\n    required: true\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Select latest stable Xcode\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      uses: maxim-lobanov/setup-xcode@v1\n      with:\n        xcode-version: latest-stable\n\n    - name: Install and update MacPorts\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: |\n        wget -O ${{ github.workspace }}/macports.pkg https://github.com/macports/macports-base/releases/download/v2.9.2/MacPorts-2.9.2-14-Sonoma.pkg\n        sudo installer -pkg ${{ github.workspace }}/macports.pkg -target /\n\n    - name: Install build and deployment tools\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: |\n        # Install Python first to avoid multiple Python in the dep tree later on.\n        sudo port -N install python313 py313-pip\n        sudo port select --set python python313\n        sudo port select --set python3 python313\n        sudo port select --set pip pip313\n        sudo port select --set pip3 pip313\n\n        # Install Clang to get better C23 support.\n        sudo port -N install clang-16\n        sudo port select --set clang mp-clang-16\n\n        # Install the rest.\n        sudo port -N install create-dmg meson ninja pkgconfig\n        sudo pip3 install pyjson5\n\n    - name: \"Build dependencies: Compression libraries (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: |\n        sudo port -N install zlib +universal\n        sudo port -N install bzip2 +universal\n        sudo port -N install xz +universal\n\n    - name: \"Build dependency: pcre2 (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: sudo port -N install pcre2 +universal\n\n    - name: \"Build dependency: GLEW (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: sudo port -N install glew +universal\n\n    - name: \"Build dependency: libsdl2 (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: sudo port -N install libsdl2 +universal\n\n    - name: \"Build dependency: lua (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: sudo port -N install lua +universal\n\n    - name: \"Build dependency: uthash (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: sudo port -N install uthash +universal\n\n    - name: \"Build dependency: ffmpeg (universal)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: |\n        # Install to separate staging paths for all architectures in\n        # preparation for fusing universal libraries in a follow-up step.\n        cd \"$RUNNER_TEMP\"\n        git clone https://github.com/FFmpeg/FFmpeg ffmpeg-arm64\n        cd ffmpeg-arm64\n        git checkout 066432ebcf\n\n        # Common FFmpeg configure options\n        FFMPEG_CONFIG_OPTIONS=\" \\\n          --enable-shared \\\n          --disable-static \\\n          $(cat $GITHUB_WORKSPACE/tools/ffmpeg_flags.txt)\"\n\n        # Configure for arm64.\n        ./configure \\\n          --arch=arm64 \\\n          --prefix=${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }} \\\n          --cc='clang' \\\n          $FFMPEG_CONFIG_OPTIONS\n\n        # Build and install.\n        make -j$(sysctl -n hw.ncpu)\n        sudo make install\n\n        cd \"$RUNNER_TEMP\"\n        git clone https://github.com/FFmpeg/FFmpeg ffmpeg-x86-64\n        cd ffmpeg-x86-64\n        git checkout 066432ebcf\n        # Configure for x86-64.\n        ./configure \\\n          --arch=x86_64 \\\n          --enable-cross-compile \\\n          --prefix=${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }} \\\n          --cc='clang -arch x86_64' \\\n          $FFMPEG_CONFIG_OPTIONS\n\n        # Build and install.\n        make -j$(sysctl -n hw.ncpu)\n        sudo make install\n\n    - name: \"Build dependency: ffmpeg (fuse universal libraries)\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: |\n        # Libs\n        FFMPEG_LIBS=(\n          \"libavcodec\"\n          \"libavdevice\"\n          \"libavfilter\"\n          \"libavformat\"\n          \"libavutil\"\n          \"libswresample\"\n          \"libswscale\"\n        )\n\n        # Recreate include tree in MacPorts install prefix.\n        sudo rsync -arvL ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/include/ ${{ inputs.CACHE_SRC_DIR }}/include/\n\n        # Recreate library symlinks in MacPorts install prefix.\n        sudo find ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/ -type l -exec cp -P '{}' ${{ inputs.CACHE_SRC_DIR }}/lib/ ';'\n\n        # Fuse platform-specific binaries into a universal binary.\n        for LIB in ${FFMPEG_LIBS[@]}; do\n            RESOLVED_LIB=$(ls -l ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/${LIB}* \\\n              | grep -v '^l' \\\n              | awk -F'/' '{print $NF}')\n\n            sudo lipo -create \\\n              ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/$RESOLVED_LIB \\\n              ${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }}/lib/$RESOLVED_LIB \\\n              -output ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB\n\n            sudo ln -s -f \\\n              ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB \\\n              ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/$RESOLVED_LIB\n            sudo ln -s -f \\\n              ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB \\\n              ${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }}/lib/$RESOLVED_LIB\n        done\n\n        # Update and install pkgconfig files.\n        for file in \"${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/pkgconfig\"/*.pc; do\n          sudo sed -i '' \"s:${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}:${{ inputs.CACHE_SRC_DIR }}:g\" \"$file\"\n        done\n        sudo mv ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/pkgconfig/* ${{ inputs.CACHE_SRC_DIR }}/lib/pkgconfig/\n\n    - name: \"Prepare dependencies for caching\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      shell: bash\n      run: |\n        # Remove MacPorts leftover build and download files\n        sudo rm -rf /opt/local/var/macports/build/*\n        sudo rm -rf /opt/local/var/macports/distfiles/*\n        sudo rm -rf /opt/local/var/macports/packages/*\n\n        # Delete broken symlinks\n        sudo find ${{ inputs.CACHE_SRC_DIR }} -type l ! -exec test -e {} \\; -exec rm {} \\;\n\n        # Trying to cache the source directory directly leads to permission errors,\n        # so copy it to an intermediate temporary directory.\n        sudo rsync -arvq ${{ inputs.CACHE_SRC_DIR }}/ ${{ inputs.CACHE_DIR }}\n\n    - name: \"Save dependencies to cache\"\n      if: steps.restore-cache.outputs.cache-hit != 'true'\n      uses: actions/cache/save@v4\n      with:\n        key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }}\n        path: |\n          ${{ inputs.CACHE_DIR }}\n"
  },
  {
    "path": ".github/docker/lint.Dockerfile",
    "content": "FROM python:3.12-slim-bookworm\n\nENV DEBIAN_FRONTEND=noninteractive\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    ca-certificates \\\n    curl \\\n    git \\\n    gnupg \\\n    xz-utils \\\n    && rm -rf /var/lib/apt/lists/*\n\nRUN curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /usr/share/keyrings/llvm-archive-keyring.gpg\nRUN echo 'deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg] https://apt.llvm.org/bookworm llvm-toolchain-bookworm-22 main' > /etc/apt/sources.list.d/llvm.list\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    clang-format-22 \\\n    && apt-get purge -y --auto-remove gnupg xz-utils \\\n    && rm -rf /var/lib/apt/lists/*\n\nRUN ln -s /usr/bin/clang-format-22 /usr/local/bin/clang-format\nRUN just_version='1.40.0' && \\\n    curl -fsSL \"https://github.com/casey/just/releases/download/${just_version}/just-${just_version}-x86_64-unknown-linux-musl.tar.gz\" \\\n    | tar -xz -C /usr/local/bin just && \\\n    chmod +x /usr/local/bin/just\nRUN python3 -m pip install --no-cache-dir \\\n    prek \\\n    pyjson5\n\nENV PATH=\"/usr/local/bin:${PATH}\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "#### Checklist\n\n- [ ] I have read the [coding conventions](https://github.com/LostArtefacts/TRX/blob/develop/docs/CONTRIBUTING.md#coding-conventions)\n- [ ] I have added a changelog entry about what my pull request accomplishes, or it is an internal change\n- [ ] I have added a readme entry about my new feature or OG bug fix, or it is a different change\n\n#### Description\n\n...\n"
  },
  {
    "path": ".github/workflows/build_docker.yml",
    "content": "name: Build Docker toolchain\n\non:\n  - workflow_dispatch\n\njobs:\n  publish_docker_image:\n    name: Build Docker toolchain\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - platform: win\n          - platform: linux\n    steps:\n      - name: Login to Docker Hub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n\n      - name: Install dependencies\n        uses: taiki-e/install-action@just\n\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          submodules: 'true'\n\n      - name: Build Docker image (${{ matrix.platform }})\n        run: |\n          just image-${{ matrix.platform }}\n          just push-image-${{ matrix.platform }}\n"
  },
  {
    "path": ".github/workflows/build_lint_image.yml",
    "content": "name: Build lint image\n\non:\n  push:\n    branches:\n      - main\n      - lint\n    paths:\n      - .github/docker/lint.Dockerfile\n      - .github/workflows/build_lint_image.yml\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  build:\n    name: Build and push lint image\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Normalize repository owner\n        id: vars\n        run: echo \"owner_lc=${GITHUB_REPOSITORY_OWNER,,}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Build and push lint image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: .github/docker/lint.Dockerfile\n          push: true\n          tags: |\n            ghcr.io/${{ steps.vars.outputs.owner_lc }}/trx-lint:latest\n            ghcr.io/${{ steps.vars.outputs.owner_lc }}/trx-lint:${{ github.sha }}\n"
  },
  {
    "path": ".github/workflows/comment_build.yml",
    "content": "name: Post build links to pull request\non:\n  workflow_run:\n    workflows: ['Create a test build']\n    types: [completed]\n\npermissions:\n  actions: write\n  contents: write\n  pull-requests: write\njobs:\n  pr_comment:\n    if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@v7\n        with:\n          # This snippet is public-domain, combined from\n          # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml\n          # https://github.com/AKSW/submission.d2r2.aksw.org/blob/main/.github/workflows/pr-comment.yml\n          script: |\n            // Function Definitions\n\n            class NoMatchingPRError extends Error {\n              constructor(message) {\n                super(message);\n                this.name = \"NoMatchingPRError\";\n              }\n            }\n\n            /**\n             * Fetch PR details for a given commit SHA.\n             * @returns {Object} PR details containing prNumber, prRef, prRepoId.\n             * @throws {Error} If no matching PR is found.\n             */\n            async function fetchPRDetails() {\n              const iterator = github.paginate.iterator(github.rest.pulls.list, {\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n              });\n              for await (const { data } of iterator) {\n                for (const pull of data) {\n                  if (pull.head.sha === '${{github.event.workflow_run.head_sha}}') {\n                    return {\n                      prNumber: pull.number,\n                      prRef: pull.head.ref,\n                      prRepoId: pull.head.repo.id\n                    };\n                  }\n                }\n              }\n              throw new NoMatchingPRError(\"No matching PR found for the commit SHA\");\n            }\n\n            /**\n             * Fetch all artifacts for a given workflow run.\n             * @returns {Object} All artifacts data.\n             * @throws {Error} If no artifacts are found.\n             */\n            async function fetchAllArtifacts() {\n              const artifactsResponse = await github.rest.actions.listWorkflowRunArtifacts({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                run_id: context.payload.workflow_run.id,\n              });\n              if (!(artifactsResponse.data && artifactsResponse.data.artifacts && artifactsResponse.data.artifacts.length)) {\n                throw new Error(\"No artifacts found for the workflow run\");\n              }\n              return artifactsResponse.data.artifacts;\n            }\n\n            /**\n             * Create or update a comment on the PR.\n             * @param {number} prNumber - The PR number.\n             * @param {string} purpose - The purpose of the comment.\n             * @param {string} body - The comment body.\n             * @throws {Error} If the comment creation or update fails.\n             */\n            async function upsertComment(prNumber, purpose, body) {\n              const { data: comments } = await github.rest.issues.listComments({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: prNumber,\n              });\n              const marker = `<!-- bot: ${purpose} -->`;\n              body = marker + \"\\n\" + body;\n\n              const existing = comments.filter(c => c.body.includes(marker));\n              if (existing.length > 0) {\n                const last = existing[existing.length - 1];\n                core.info(`Updating comment ${last.id}`);\n                await github.rest.issues.updateComment({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  body: body,\n                  comment_id: last.id,\n                });\n              } else {\n                core.info(`Creating a comment in PR #${prNumber}`);\n                await github.rest.issues.createComment({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  body: body,\n                  issue_number: prNumber,\n                });\n              }\n            }\n\n            /**\n             * Handle and log errors with detailed context and exit the process.\n             * @param {Error} error - The error object.\n             * @param {string} description - Description of the context where the error occurred.\n             */\n            function handleError(error, description, prNumber) {\n              let exitCode = 1;\n              let log = core.error;\n              if (error instanceof NoMatchingPRError) {\n                exitCode = 0;\n                log = core.warning;\n              }\n              log(`Failed to ${description}`);\n              log(`Message: ${error.message}`);\n              log(`Stack Trace: ${error.stack || 'No stack trace available'}`);\n              if (prNumber) {\n                log(`PR Number: ${prNumber}`);\n              }\n              log(`PRs: https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/pulls`);\n              log(`SHA: ${{github.event.workflow_run.head_sha}}`);\n              process.exit(exitCode);\n            }\n\n            // Main Code Execution\n\n            let prNumber, prRef, prRepoId;\n\n            // Fetch PR details\n            try {\n              ({ prNumber, prRef, prRepoId } = await fetchPRDetails());\n              core.info(`Found PR: #${prNumber}, Ref: ${prRef}, Repo ID: ${prRepoId}`);\n            } catch (error) {\n              handleError(error, 'fetch PR details', undefined);\n            }\n\n            // Fetch all artifacts\n            let allArtifacts;\n            try {\n              allArtifacts = await fetchAllArtifacts();\n              core.info(`Artifacts fetched successfully`);\n            } catch (error) {\n              handleError(error, 'fetch artifacts', prNumber);\n            }\n\n            // Construct the comment body\n            let body = 'Download the built assets for this pull request:\\n' +\n              allArtifacts\n                .filter(item => item.name !== \"assets\")\n                .sort((a, b) => a.name.localeCompare(b.name))\n                .map(item => `* [${item.name}.zip](https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/artifacts/${item.id}.zip)`)\n                .join('\\n');\n\n            // Upsert the comment on the PR\n            try {\n              await upsertComment(prNumber, \"nightly-link\", body);\n              core.info(\"Comment created/updated successfully\");\n            } catch (error) {\n              handleError(error, 'create/update comment', prNumber);\n            }\n"
  },
  {
    "path": ".github/workflows/job_build.yml",
    "content": "name: Build TRX and the installer\n\non:\n  workflow_call:\n    inputs:\n      platform:\n        type: string\n        description: \"Platform to build for\"\n        required: true\n      target:\n        type: string\n        description: \"Target to build for\"\n        required: true\n      zip:\n        type: boolean\n        description: \"Pack the artifacts into zip\"\n        required: true\n\njobs:\n  build:\n    name: Build release assets\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Install dependencies\n        uses: taiki-e/install-action@just\n\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          submodules: 'true'\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - id: vars\n        name: Prepare variables\n        run: |\n          echo \"version=$(just output-current-version)\" >> $GITHUB_OUTPUT\n          echo \"tr1_dir=build/artifacts/tr1/\" >> $GITHUB_OUTPUT\n          echo \"tr2_dir=build/artifacts/tr2/\" >> $GITHUB_OUTPUT\n          echo \"tr3_dir=build/artifacts/tr3/\" >> $GITHUB_OUTPUT\n          echo \"trx_dir=build/artifacts/trx/\" >> $GITHUB_OUTPUT\n          echo \"tr1_asset=$(just output-package-name --game 1 --platform ${{ inputs.platform }})\" >> $GITHUB_OUTPUT\n          echo \"tr2_asset=$(just output-package-name --game 2 --platform ${{ inputs.platform }})\" >> $GITHUB_OUTPUT\n          echo \"tr3_asset=$(just output-package-name --game 3 --platform ${{ inputs.platform }})\" >> $GITHUB_OUTPUT\n          echo \"trx_asset=$(just output-package-name --platform ${{ inputs.platform }})\" >> $GITHUB_OUTPUT\n\n      - name: Download large assets\n        if: ${{ inputs.target == 'release' }}\n        run: |\n          just download-assets 1\n          just download-assets 2\n          just download-assets 3\n          just download-assets --combined\n\n      - name: Restore ccache\n        if: ${{ inputs.platform == 'linux' || inputs.platform == 'win' }}\n        uses: actions/cache@v4\n        with:\n          path: .cache/ccache\n          key: ccache-v1-${{ runner.os }}-${{ inputs.platform }}-${{ inputs.target }}-${{ hashFiles('justfile', 'tools/shared/docker/game-linux/Dockerfile', 'tools/shared/docker/game-win/Dockerfile', 'tools/shared/docker/game-win/meson_linux_mingw32.txt') }}\n          restore-keys: |\n            ccache-v1-${{ runner.os }}-${{ inputs.platform }}-${{ inputs.target }}-\n            ccache-v1-${{ runner.os }}-${{ inputs.platform }}-\n\n      - name: Package asset (${{ inputs.platform }})\n        env:\n          CCACHE_DIR: /app/.cache/ccache/${{ inputs.platform }}-${{ inputs.target }}\n          CCACHE_BASEDIR: /app\n          CCACHE_COMPILERCHECK: content\n          CCACHE_MAXSIZE: 1G\n        run: |\n          if [ \"${{ inputs.platform }}\" = \"linux\" ] || [ \"${{ inputs.platform }}\" = \"win\" ]; then\n            mkdir -p \".cache/ccache/${{ inputs.platform }}-${{ inputs.target }}\"\n            if [ \"${{ inputs.zip }}\" = \"1\" ] || [ \"${{ inputs.zip }}\" = \"true\" ]; then\n              just trx-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"build/artifacts/\"\n            else\n              just trx-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"build/artifacts/\" --no-zip\n            fi\n          else\n            if [ \"${{ inputs.zip }}\" = \"1\" ] || [ \"${{ inputs.zip }}\" = \"true\" ]; then\n              just tr1-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"${{ steps.vars.outputs.tr1_dir }}\"\n              just tr2-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"${{ steps.vars.outputs.tr2_dir }}\"\n              just trx-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"${{ steps.vars.outputs.trx_dir }}\"\n            else\n              just tr1-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"${{ steps.vars.outputs.tr1_dir }}\" --no-zip\n              just tr2-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"${{ steps.vars.outputs.tr2_dir }}\" --no-zip\n              just trx-package-${{ inputs.platform }} \"${{ inputs.target }}\" -o \"${{ steps.vars.outputs.trx_dir }}\" --no-zip\n            fi\n          fi\n\n      - name: Upload artifacts (tr1)\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ steps.vars.outputs.tr1_asset }}\n          path: ${{ steps.vars.outputs.tr1_dir }}\n          compression-level: 0\n\n      - name: Upload artifacts (tr2)\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ steps.vars.outputs.tr2_asset }}\n          path: ${{ steps.vars.outputs.tr2_dir }}\n          compression-level: 0\n\n      - name: Upload artifacts (tr3)\n        if: ${{ inputs.platform != 'win-installer' }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ steps.vars.outputs.tr3_asset }}\n          path: ${{ steps.vars.outputs.tr3_dir }}\n          compression-level: 0\n\n      - name: Upload artifacts (combined)\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ steps.vars.outputs.trx_asset }}\n          path: ${{ steps.vars.outputs.trx_dir }}\n          compression-level: 0\n"
  },
  {
    "path": ".github/workflows/job_build_macos.yml",
    "content": "name: Build TRX and the installer (macOS)\n\non:\n  workflow_call:\n    inputs:\n      target:\n        type: string\n        description: \"Target to build for\"\n        required: true\n      let_mac_fail:\n        type: boolean\n        description: \"Do not require Mac builds to pass\"\n        required: false\n        default: false\n\nenv:\n  FFMPEG_INSTALL_FINAL: /opt/local\n  FFMPEG_INSTALL_TMP_UNIVERSAL: /tmp/install_universal\n  FFMPEG_INSTALL_TMP_ARM64: /tmp/install_arm64\n  FFMPEG_INSTALL_TMP_X86_64: /tmp/install_x86_64\n  CACHE_TMP_DIR: /tmp/opt_local/\n  CACHE_DST_DIR: /opt/local/\n  C_INCLUDE_PATH: /opt/local/include/uthash/:/opt/local/include/\n\njobs:\n  build:\n    strategy:\n      matrix:\n        game:\n          - { version: 1 }\n          - { version: 2 }\n          - { version: 3 }\n\n    name: Build release assets\n    runs-on: macos-14\n    continue-on-error: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }}\n    steps:\n      - name: Set up signing certificate\n        env:\n          MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }}\n          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}\n          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}\n        run: |\n          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12\n          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db\n          echo -n \"$MACOS_CERTIFICATE\" | base64 --decode -o $CERTIFICATE_PATH\n          security create-keychain -p \"$MACOS_KEYCHAIN_PWD\" $KEYCHAIN_PATH\n          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH\n          security unlock-keychain -p \"$MACOS_KEYCHAIN_PWD\" $KEYCHAIN_PATH\n          security import $CERTIFICATE_PATH -P \"$MACOS_KEYCHAIN_PWD\" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH -T /usr/bin/codesign\n          security list-keychain -d user -s $KEYCHAIN_PATH\n          security set-key-partition-list -S \"apple-tool:,apple:,codesign:\" -s -k $MACOS_KEYCHAIN_PWD $KEYCHAIN_PATH\n\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          submodules: 'true'\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - id: vars\n        name: Prepare variables\n        run: |\n          echo \"version=$(tools/get_version)\" >> $GITHUB_OUTPUT\n          echo \"tag=tr${{matrix.game.version}}\" >> $GITHUB_OUTPUT\n          echo \"name=TR${{matrix.game.version}}X\" >> $GITHUB_OUTPUT\n          echo \"asset_name=$(tools/output_package_name --game ${{matrix.game.version}} --platform mac)\" >> $GITHUB_OUTPUT\n\n      - name: Extend PATH for MacPorts\n        run: |\n          echo -e \"/opt/local/bin\" >> $GITHUB_PATH\n          echo -e \"/opt/local/sbin\" >> $GITHUB_PATH\n\n      - name: \"Try restore dependencies from cache\"\n        id: restore-cache\n        uses: actions/cache/restore@v4\n        with:\n          key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }}\n          path: |\n            /tmp/opt_local/\n      - name: \"Build MacOS dependencies\"\n        if: steps.restore-cache.outputs.cache-hit != 'true'\n        uses: ./.github/actions/prepare_macos_tooling\n        with:\n          CACHE_DIR: /tmp/opt_local/\n      - name: \"Prepare cached dependencies for use\"\n        if: steps.restore-cache.outputs.cache-hit == 'true'\n        shell: bash\n        run: |\n          sudo rsync -arvq /tmp/opt_local/ /opt/local/\n          sudo dscl . -create /Groups/macports\n          sudo dscl . -create /Groups/macports RealName \"MacPorts\"\n          sudo dscl . -create /Groups/macports PrimaryGroupID 501\n          sudo dscl . -create /Groups/macports GeneratedUID 172D097F-351A-4579-BBD1-430D99BC4ABF\n          sudo dscl . -append /Groups/macports GroupMembership macports\n\n          sudo dscl . -create /Users/macports\n          sudo dscl . -create /Users/macports RealName \"MacPorts\"\n          sudo dscl . -create /Users/macports UniqueID 502\n          sudo dscl . -create /Users/macports PrimaryGroupID 501\n          sudo dscl . -create /Users/macports UserShell /usr/bin/false\n          sudo dscl . -create /Users/macports NFSHomeDirectory /opt/local/var/macports/home\n          sudo mkdir -p /opt/local/var/macports/home\n          sudo chown -R macports:macports /opt/local/var/macports\n\n      - name: Setup CA\n        run: |\n          sudo port -N install apple-pki-bundle curl-ca-bundle\n\n      - name: Download large assets\n        #if: ${{ inputs.target == 'release' }}\n        run: tools/download_assets ${{ matrix.game.version }}\n\n      - name: Build arm64 and create app bundle\n        env:\n          CC: clang\n        run: |\n          BUILD_DIR=build-arm64\n          BUILD_OPTIONS=\"src --prefix=/tmp/${{ steps.vars.outputs.name }}.app --bindir=Contents/MacOS --buildtype ${{ inputs.target }}\"\n          meson setup $BUILD_DIR $BUILD_OPTIONS\n          meson install -C $BUILD_DIR --tags \"${{ steps.vars.outputs.tag }},common\"\n\n      - name: Build x86-64\n        env:\n          CC: clang\n        run: |\n          BUILD_DIR=build-x86-64\n          BUILD_OPTIONS=\"src --prefix=/tmp/${{ steps.vars.outputs.name }}.app --bindir=Contents/MacOS --cross-file tools/shared/mac/x86-64_cross_file.txt --buildtype ${{ inputs.target }}\"\n          meson setup $BUILD_DIR $BUILD_OPTIONS\n          meson compile -C $BUILD_DIR\n\n      - name: Fuse universal executable\n        run: |\n          BUNDLE_EXEC_DIR=/tmp/${{ steps.vars.outputs.name }}.app/Contents/MacOS\n\n          # Fuse executable and move it into the app bundle.\n          lipo -create build-x86-64/TRX $BUNDLE_EXEC_DIR/TRX -output $BUNDLE_EXEC_DIR/TRX_universal\n          mv $BUNDLE_EXEC_DIR/TRX_universal $BUNDLE_EXEC_DIR/TRX\n\n          # Update dynamic library links in the fused executable.\n          ./tools/shared/mac/bundle_dylibs -a ${{ steps.vars.outputs.name }} --links-only\n\n      - name: Sign app bundle\n        run: |\n          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db\n          IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'\"' '{print $2}')\n          xattr -cr /tmp/${{ steps.vars.outputs.name }}.app\n          /usr/bin/codesign --force --deep --options runtime -s \"${IDENTITY}\" --keychain $KEYCHAIN_PATH -v /tmp/${{ steps.vars.outputs.name }}.app\n\n      - name: Create, sign and notarize disk image\n        env:\n          MACOS_APPLEID: ${{ secrets.MACOS_APPLEID }}\n          MACOS_APP_PWD: ${{ secrets.MACOS_APP_PWD }}\n          MACOS_TEAMID: ${{ secrets.MACOS_TEAMID }}\n        run: |\n          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db\n          IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'\"' '{print $2}')\n          DMG_NAME=\"${{ steps.vars.outputs.asset_name }}.dmg\"\n          tools/shared/mac/create_installer -a ${{ steps.vars.outputs.name }} -i \"data/${{ steps.vars.outputs.tag }}/mac/icon.icns\" -d \"${DMG_NAME}\"\n          xattr -cr \"${DMG_NAME}\"\n          /usr/bin/codesign --force --options runtime -s \"${IDENTITY}\" --keychain $KEYCHAIN_PATH -v \"${DMG_NAME}\"\n          xcrun notarytool submit --wait --apple-id \"$MACOS_APPLEID\" --password \"$MACOS_APP_PWD\" --team-id \"$MACOS_TEAMID\" \"${DMG_NAME}\"\n          xcrun stapler staple -v \"${DMG_NAME}\"\n\n      - name: Upload signed+notarized installer image\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ steps.vars.outputs.asset_name }}\n          path: |\n            *.dmg\n          compression-level: 0 # .dmg is already compressed.\n"
  },
  {
    "path": ".github/workflows/job_release.yml",
    "content": "name: Create a new release\n\non:\n  workflow_call:\n    inputs:\n      draft:\n        type: boolean\n        description: \"Draft\"\n        required: true\n        default: false\n      prerelease:\n        type: boolean\n        description: \"Prerelease\"\n        required: true\n        default: false\n      tag_name:\n        type: string\n        description: \"Tag name\"\n        required: false\n        default: github.ref_name\n\n\njobs:\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: \"Install dependencies\"\n        uses: taiki-e/install-action@just\n\n      - name: \"Checkout code\"\n        uses: actions/checkout@v4\n        with:\n          submodules: 'true'\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: \"Prepare release data\"\n        id: prepare_release_data\n        run: |\n          if [ \"${{ inputs.prerelease }}\" = \"1\" ] || [ \"${{ inputs.prerelease }}\" = \"true\" ]; then\n            echo \"release_name=Development snapshot\" >> $GITHUB_OUTPUT\n            echo \"changelog<<EOF\" >> $GITHUB_OUTPUT\n            just output-current-changelog >> $GITHUB_OUTPUT\n            echo \"EOF\" >> $GITHUB_OUTPUT\n          else\n            echo -n \"release_name=\" >> $GITHUB_OUTPUT\n            just output-release-name >> $GITHUB_OUTPUT\n            echo \"changelog<<EOF\" >> $GITHUB_OUTPUT\n            just output-current-changelog --stable >> $GITHUB_OUTPUT\n            echo \"EOF\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: \"Download built assets\"\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts/\n          merge-multiple: true\n\n      - name: \"Get information on the latest pre-release\"\n        if: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }}\n        id: last_release\n        uses: InsonusK/get-latest-release@v1.0.1\n        with:\n          myToken: ${{ github.token }}\n          exclude_types: \"draft|release\"\n\n      - name: 'Mark the pre-release as latest'\n        if: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }}\n        uses: EndBug/latest-tag@latest\n\n      - name: \"Delete old pre-release assets\"\n        if: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }}\n        uses: mknejp/delete-release-assets@v1\n        continue-on-error: true\n        with:\n          token: ${{ github.token }}\n          tag: ${{ steps.last_release.outputs.tag_name }}\n          assets: \"*.*\"\n\n      - name: \"Publish a release\"\n        uses: softprops/action-gh-release@v2.2.2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          tag_name: ${{ inputs.tag_name }}\n          name: ${{ steps.prepare_release_data.outputs.release_name }}\n          body: ${{ steps.prepare_release_data.outputs.changelog }}\n          draft: ${{ inputs.draft == true || inputs.draft == 'true' }}\n          prerelease: ${{ inputs.prerelease == true || inputs.prerelease == 'true' }}\n          fail_on_unmatched_files: true\n          files: |\n            artifacts/*\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Run code linters\n\non:\n  - push\n  - pull_request\n\njobs:\n  lint:\n    name: Run code linters\n    runs-on: ubuntu-latest\n    env:\n      PREK_HOME: ${{ github.workspace }}/.cache/prek\n    permissions:\n      contents: read\n      packages: read\n    container:\n      image: ghcr.io/lostartefacts/trx-lint:latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n          fetch-tags: false\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Configure git safe directory\n        working-directory: ${{ github.workspace }}\n        run: git config --global --add safe.directory \"${GITHUB_WORKSPACE}\"\n\n      - name: Ensure prek cache dir exists\n        run: mkdir -p \"${PREK_HOME}\"\n\n      - name: Restore prek cache\n        uses: actions/cache@v4\n        with:\n          path: .cache/prek\n          key: prek-v1-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}\n          restore-keys: |\n            prek-v1-${{ runner.os }}-\n\n      - name: Check formatted code differences\n        working-directory: ${{ github.workspace }}\n        run: |\n          set +e\n          just lint-format\n          lint_format_status=$?\n          set -e\n          if ! git diff --quiet; then\n            echo 'Formatting diffs detected in:'\n            git diff --exit-code || (\n              clang-format --version\n              echo 'Please run `just lint` and commit the changes.'\n              exit 1\n            )\n          fi\n          if [ \"${lint_format_status}\" -ne 0 ]; then\n            clang-format --version\n            echo 'just lint-format failed.'\n            exit \"${lint_format_status}\"\n          fi\n\n      - name: Check imports\n        working-directory: ${{ github.workspace }}\n        run: |\n          git add -u\n          just lint-imports\n          git diff --exit-code || (\n            include-what-you-use --version\n            echo 'Please run `just lint` and commit the changes.'\n            exit 1\n          )\n"
  },
  {
    "path": ".github/workflows/pr_builds.yml",
    "content": "name: Create a test build\n\npermissions:\n  contents: write\n\non:\n  pull_request:\n  push:\n    branches:\n      - '!stable'\n      - '!develop'\n\njobs:\n  package_linux:\n    name: Linux\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: linux\n      target: debug\n      zip: false\n    secrets: inherit\n\n  package_win:\n    name: Windows\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: win\n      target: debug\n      zip: false\n    secrets: inherit\n\n  # package_mac:\n  #   name: Mac\n  #   if: vars.MACOS_ENABLE == 'true'\n  #   uses: ./.github/workflows/job_build_macos.yml\n  #   with:\n  #     target: debug\n  #     let_mac_fail: true\n  #   secrets: inherit\n"
  },
  {
    "path": ".github/workflows/prerelease.yml",
    "content": "name: Publish a pre-release\n\npermissions:\n  contents: write\n\non:\n  push:\n    branches:\n    - develop\n\njobs:\n  package_linux:\n    name: Build Linux\n    if: vars.PRERELEASE_ENABLE == 'true'\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: linux\n      target: debug\n      zip: true\n    secrets: inherit\n\n  package_win:\n    name: Build Windows\n    if: vars.PRERELEASE_ENABLE == 'true'\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: win\n      target: debug\n      zip: true\n    secrets: inherit\n\n  # package_win_installer:\n  #   name: Build Windows installer\n  #   if: vars.PRERELEASE_ENABLE == 'true'\n  #   uses: ./.github/workflows/job_build.yml\n  #   with:\n  #     platform: win-installer\n  #     target: debug\n  #     zip: false\n  #   secrets: inherit\n\n  package_mac:\n    name: Build Mac\n    if: |\n      vars.PRERELEASE_ENABLE == 'true' &&\n      vars.MACOS_ENABLE == 'true'\n    uses: ./.github/workflows/job_build_macos.yml\n    with:\n      target: debug\n      let_mac_fail: true\n    secrets: inherit\n\n  publish_prerelease:\n    if: always() && (vars.PRERELEASE_ENABLE == 'true')\n    name: Create a prerelease\n    needs:\n      - package_linux\n      - package_mac\n      - package_win\n      # - package_win_installer\n    with:\n      draft: false\n      prerelease: true\n      tag_name: 'latest'\n    uses: ./.github/workflows/job_release.yml\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Publish a release\n\npermissions:\n  contents: write\n\non:\n  push:\n    branch: stable\n    tags:\n      - \"trx-*\"\n\n  workflow_dispatch:\n    inputs:\n      draft:\n        description: \"Draft\"\n        required: true\n        type: boolean\n        default: false\n      prerelease:\n        description: \"Prerelease\"\n        required: true\n        type: boolean\n        default: false\n      tag_name:\n        description: \"Tag name\"\n        required: false\n        type: string\n        default: github.ref_name\n\njobs:\n  package_linux:\n    name: Build Linux\n    if: vars.RELEASE_ENABLE == 'true'\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: linux\n      target: release\n      zip: true\n    secrets: inherit\n\n  package_win:\n    name: Build Windows\n    if: vars.RELEASE_ENABLE == 'true'\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: win\n      target: release\n      zip: true\n    secrets: inherit\n\n  package_win_installer:\n    name: Build Windows installer\n    if: vars.RELEASE_ENABLE == 'true'\n    uses: ./.github/workflows/job_build.yml\n    with:\n      platform: win-installer\n      target: release\n      zip: false\n    secrets: inherit\n\n  package_mac:\n    name: Build Mac\n    if: |\n      vars.RELEASE_ENABLE == 'true' &&\n      vars.MACOS_ENABLE == 'true'\n    uses: ./.github/workflows/job_build_macos.yml\n    with:\n      target: release\n      let_mac_fail: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }}\n    secrets: inherit\n\n  publish_release:\n    if: always() && (vars.RELEASE_ENABLE == 'true')\n    name: Create a GitHub release\n    needs:\n      - package_linux\n      - package_win\n      - package_win_installer\n      - package_mac\n    with:\n      draft: ${{ inputs.draft || false }}\n      prerelease: ${{ inputs.draft || false }}\n      tag_name: ${{ inputs.tag_name || github.ref_name }}\n    uses: ./.github/workflows/job_release.yml\n"
  },
  {
    "path": ".gitignore",
    "content": "*.cache.json\nTR1X.dll\nTR1X.exe\nTR1X.log\nTR2X.dll\nTR2X.exe\nTR2X.log\n\n# Docker builds garbage\n/build\n/.secrets\n/.local\n/workflow\n/TR1X*.zip\n/TR1X*.exe\n/TR2X*.zip\n/TR2X*.exe\n__pycache__/\n\n# VS garbage\nv15/\nv16/\n*.suo\n*.o\n*.obj\n*.pdb\n*.lib\n*.exp\nDebug/\nRelease/\n*.user\n*.ipch\n.vs/\n*.vcxproj\n*.filters\n.dotnet/\n\n# MacOS garbage\n.DS_Store\n\n# libtrx artefacts\n**/subprojects/packagecache/\n**/subprojects/dwarfstack-*/\ndata/tr1/ship/data/images/\ndata/tr2/ship/data/images/\ndata/tr2/ship/data/level1.tr2\ndata/tr2/ship/data/level2.tr2\ndata/tr2/ship/data/level3.tr2\ndata/tr2/ship/data/level4.tr2\ndata/tr2/ship/data/level5.tr2\ndata/tr2/ship/data/main_gm.sfx\ndata/tr2/ship/data/title_gm.tr2\ndata/tr2/ship/music/\ndata/tr3/ship/data/images/\n\ntools/installer/TR1X_Installer/Resources/release.zip\ntools/installer/TR2X_Installer/Resources/release.zip\ntools/installer/TRX_Installer/Resources/release.zip\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"libtrx\"]\n\tpath = subprojects/libtrx\n\turl = https://github.com/LostArtefacts/libtrx.git\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: local\n  hooks:\n  - id: clang-format\n    name: clang-format\n    entry: clang-format\n    args: [\"-style=file\", \"-i\"]\n    language: system\n    files: \\.[ch](pp)?$\n\n  - id: additional-lint\n    name: Run additional linters\n    entry: tools/additional_lint\n    language: python\n    stages: [pre-commit]\n    additional_dependencies:\n      - pyjson5\n      - jsonschema\n\n  - id: additional-lint -a\n    name: Run additional linters (repo-wide)\n    entry: tools/additional_lint -a\n    language: python\n    stages: [pre-commit]\n    pass_filenames: false\n    additional_dependencies:\n      - pyjson5\n      - jsonschema\n\n  - id: imports\n    name: imports\n    entry: tools/sort_imports\n    language: system\n    files: \\.[ch](pp)?$\n\n  - id: update-game-strings\n    name: Update game strings to match source code\n    entry: tools/update_game_strings\n    language: python\n    stages: [pre-commit]\n    pass_filenames: false\n    additional_dependencies:\n      - pyjson5\n\n- repo: https://github.com/JohnnyMorganz/StyLua\n  rev: v2.3.0\n  hooks:\n    - id: stylua-github\n"
  },
  {
    "path": "BUG_REPORTING.md",
    "content": "# TRX Bug Reporting Guide\n\nThanks for taking the time to report an issue.\n\nGood bug reports help us reproduce problems quickly and spend more time fixing\nthem instead of guessing. The goal is simple: give us enough information to\nsee the same bug you saw.\n\n## 1. Where to report bugs\n\n- Please report bugs on GitHub issues.\n- If you cannot create a GitHub account, use `#trx-bugs` on Discord.\n- For Discord users, please **do not** report bugs in general chat.\n  They are much harder to track there and usually miss important details.\n\n## 2. One issue per report\n\nPlease open one ticket per bug.\n\nKeeping reports separate makes it easier to reproduce, discuss, fix, and close\neach issue cleanly.\n\n## 3. What every report should include\n\nPlease include:\n\n- Clear step-by-step reproduction steps\n- Exact TRX version\n- Operating system\n- Logs, especially `TRX.log`\n- GPU details if you think it might be relevant\n\nHelpful extras:\n\n- Save files\n- Screenshots\n- Videos\n\nExtra requirement for custom levels:\n\n- Include the level files\n\nIf a custom level bug does not include the level data, we usually cannot debug\nit properly.\n\n## 4. How to write reproduction steps\n\nTry to describe the shortest reliable path to the issue.\n\nGood examples:\n\n- > 1. Load Escape from the Base\n    > 2. Reach room 66\n    > 3. pull the lever\n    > 4. game crashes.\n- > Play level X → do Y → push block disappears.\n\nLess helpful example:\n\n- `The game crashes sometimes when playing Bartoli's Bughouse.`\n\nThat kind of report tells us something is wrong, but not how to reproduce it,\nand thus we can't do anything about it.\n\n## 5. Why this matters\n\nActionable reports are the fastest route to a fix.\n\nIf a report does not include reproduction steps, version details, or the files\nneeded to reproduce the issue, we may not be able to investigate it further and\non a bad day may close it altogether.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# TRX Community Guide and Code of Conduct\n\nTRX is a volunteer, maintainer-led open source project. We're happy to have you here.\n\nOur number one enemy is exhaustion. If discussion drains maintainers more than\nit helps the project, everyone loses. So we keep communication clean,\nconstructive, and low-friction. There's no room for prolonged negativity,\nentitlement, or argument-as-a-hobby.\n\n## 1. The vibe we want\n\n- Curious, practical, kind\n- Clear reports, clear proposals\n- Disagreement expressed as \"here's a better idea\"\n- Solutions over speeches\n\nYou don't need to be an expert, but you do need to be respectful and actionable.\n\n## 2. How to participate in a way that gets results\n\nWhen you want change, write it as a proposal.  \nGood patterns:\n\n- \"It would be great if [specific change], because [reason]\".\n- \"Current behavior: X. Expected behavior: Y. Evidence: Z.\"\n- \"Comparison: Original game does [behavior]. TRX does [behavior].\"\n- \"Minimal reproduction case: [shortest possible steps to trigger issue].\"\n- \"After reading [documentation page/section], [specific information] remains unclear.\"\n- \"Technical observation: [system/component] appears to ignore [condition/input].\"\n- \"Tested on: [version], [platform], [configuration]. Issue: [description].\"\n\nIf you can't describe the change clearly, it's not ready to be requested.\n\nContributions are evaluated, not adopted automatically. Submitting research,\nproposals, or patches does not guarantee they will be merged in their current\nform, or at all. Maintainers may modify, defer, or decline contributions to\nkeep the project aligned with its goals and plans.\n\n## 3. What we will not engage with\n\nThese are exhaustion-generators. They will be moderated quickly.\n\n- Drive-by bug reports (no steps, no version, no logs, “crashes sometimes”); reports outside bug channels\n- Complaints without a concrete proposal\n- Catastrophizing (\"this ruins the game\", \"this breaks everything\") instead of specifics\n- Re-litigating decisions after maintainers say it's decided\n- \"Truth voice\" posting: presenting personal preference as objective fact\n- Purity tests and vision wars (\"real TR is X\", \"this feels like it drifts from the original vision\", etc.)\n- Mislabeling TRX as “just a mod” or otherwise misrepresenting what the project is (TRX is a standalone, reverse engineered, build-from-source engine project).\n- Pressure tactics: guilt, demands, timelines, \"you must\", \"you owe\", \"everyone agrees\", \"I think it is very important\"\n\n## 4. Governance\n\nTRX is maintainer-led. Maintainers have final say on:\n\n- Project vision and scope\n- Features and defaults\n- UI/UX and discoverability\n- What gets accepted, declined, closed, or locked\n\nYou're welcome to suggest. Maintainers decide. If a decision is made, continuing to argue it is not \"discussion\", it's drain.\n\n## 5. Moderation\n\nTo protect the project, maintainers may:\n\n- Ask for a proposal format\n- Close issues that aren't actionable\n- Lock threads that turn circular or hostile\n- Remove disruptive participants from project spaces\n\nWe don't want to do this. We will do this.\n\n## 6. Basic respect\n\nInstant hard no:\n\n- Harassment or personal attacks\n- Discrimination\n- Threats or intimidation\n\n## 7. If you disagree\n\nTotally fine. Choose a productive path:\n\n- Propose an alternative clearly\n- Adjust settings to your taste\n- Contribute a patch\n- Fork the project\n- Go play the OGs, Remasters, or something else altogether\n\nWhat's not fine is trying to force agreement through volume, repetition, or hostility.\n\n## Closing\n\n> Push hard enough against the current, and even the river will stop trying to pull you along.\n\nWhen you consistently create friction rather than progress, you will lose your seat at the table.\nWe're building something we care about, and we want the community (and ourselves) to feel good to be in.\n"
  },
  {
    "path": "COPYING.md",
    "content": "GNU GENERAL PUBLIC LICENSE\n==========================\nVersion 3, 29 June 2007\n==========================\n\n> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>  \n  Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\n# Preamble\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n# TERMS AND CONDITIONS\n\n## 0. Definitions.\n\n  _\"This License\"_ refers to version 3 of the GNU General Public License.\n\n  _\"Copyright\"_ also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  _\"The Program\"_ refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as _\"you\"_.  _\"Licensees\"_ and\n\"recipients\" may be individuals or organizations.\n\n  To _\"modify\"_ a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a _\"modified version\"_ of the\nearlier work or a work _\"based on\"_ the earlier work.\n\n  A _\"covered work\"_ means either the unmodified Program or a work based\non the Program.\n\n  To _\"propagate\"_ a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To _\"convey\"_ a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n## 1. Source Code.\n\n  The _\"source code\"_ for a work means the preferred form of the work\nfor making modifications to it. _\"Object code\"_ means any non-source\nform of a work.\n\n  A _\"Standard Interface\"_ means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The _\"System Libraries\"_ of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The _\"Corresponding Source\"_ for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n## 2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n## 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n## 4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n## 5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n## 6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A _\"User Product\"_ is either (1) a _\"consumer product\"_, which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  _\"Installation Information\"_ for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n## 7. Additional Terms.\n\n  _\"Additional permissions\"_ are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n## 8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n## 9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n## 10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An _\"entity transaction\"_ is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n## 11. Patents.\n\n  A _\"contributor\"_ is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's _\"essential patent claims\"_ are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n## 12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n## 13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n## 14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n## 15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n## 16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n## 17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n# END OF TERMS AND CONDITIONS\n--------------------------------------------------------------------------\n\n\n# How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type 'show c' for details.\n\n  The hypothetical commands _'show w'_ and _'show c'_ should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<h1>TRX – Tomb Raider I & II: Community Edition</h1>\n\n<p align=\"center\">\n  <a href=\"https://lostartefacts.dev/\">\n    <img src=\"data/trx/icon.png\" style=\"width: 128px;\" alt=\"TRX logo\"/>\n  </a>\n</p>\n\n<a href=\"https://github.com/LostArtefacts/TRX/releases?q=prerelease%3Afalse&expanded=true\">\n    <img src=\"data/download_trx.svg\"/>\n</a>\n</div>\n\n<hr/>\n\nWelcome to **TRX** – an open-source reimplementation of **Tomb Raider 1**, **Tomb Raider 2** and **Tomb Raider 3**. TRX aims to enhance these classic games through decompilation and the implementation of open-source alternatives to proprietary components. TRX is a single engine capable of running TR1, TR2, and custom levels respecting each of the distinct, classic engines' mechanics.\n\n## Showcase\n<table>\n    <tr>\n        <th>\n            Restored braid in TR1\n            <img src=\"docs/showcase/braid.jpg\"/>\n        </th>\n        <th>\n            Enemy health bar and UI scaling\n            <img src=\"docs/showcase/enemy_health_bar_and_scaling.jpg\"/>\n        </th>\n    </tr>\n    <tr>\n        <th>\n            Photo mode\n            <img src=\"docs/showcase/photo_mode.webp\"/>\n        </th>\n        <th>\n            3D pickups\n            <img src=\"docs/showcase/3d_pickups.jpg\"/>\n        </th>\n    </tr>\n    <tr>\n        <th>\n            Skybox support\n            <img src=\"docs/showcase/skybox.jpg\"/>\n        </th>\n        <th>\n            Updated moveset including sprint\n            <img src=\"docs/showcase/sprint.webp\"/>\n        </th>\n    </tr>\n    <tr>\n        <th>\n            Customizable draw distance\n            <img src=\"docs/showcase/draw_distance.webp\"/>\n        </th>\n        <th>\n            Developer console\n            <img src=\"docs/showcase/console.webp\"/>\n        </th>\n    </tr>\n    <tr>\n        <th>\n            Detailed level stats\n            <img src=\"docs/showcase/compass_stats.jpg\"/>\n        </th>\n        <th>\n            PS1 UI and expanded options\n            <img src=\"docs/showcase/ps1_ui_and_options.webp\"/>\n        </th>\n    </tr>\n</table>\n\n## Download\nDownload the latest TRX release for TR1-TR3:\n\n<a href=\"https://github.com/LostArtefacts/TRX/releases?q=prerelease%3Afalse&expanded=true\">\n    <img src=\"data/download_trx.svg\"/>\n</a>\n\n### Installation instructions\n* [Tomb Raider 1](docs/tr1/INSTALLING.md).\n* [Tomb Raider 2](docs/tr2/INSTALLING.md).\n* [Tomb Raider 3](docs/tr3/INSTALLING.md).\n* [Combined directory tree](docs/trx/INSTALLING.md).\n\n### Changelog\n\nFor the changelog for all of the games (TRX uses a unified engine capable of\nrunning all 3 games), please refer to [this document](docs/CHANGELOG.md).\n\n\n## Q&A\n\n1. **Are all three games fully playable from beginning to end?**\n\n    TR1 and TR2 – yes, by all means! If you encounter a bug, please file a\n    ticket.\n\n    TR3 is still in the works, though, and the team is hard at work to make\n    this happen!\n\n2. **Can we get HD textures? What about other visual updates?**\n\n    Regarding HD texture packs, that feature is not currently planned.\n\n    As for other visual updates, being able to introduce reflections and\n    skyboxes shows that quite literally the sky is the limit. TRX is constantly\n    getting new rendering improvements and texture fixes. But great stuff\n    takes time.\n\n4. **Can I play this on Mac, Linux, Android...?**\n\n    Currently supported platforms include Windows, Linux and macOS.\n\n5. **Can I play this with a controller?**\n\n    TRX supports a wide variety of controllers out of the box with no\n    additional software required. The keyboard or controller controls\n    can be fully customized in the Controls menu with multiple layouts.\n    Multi-key combo shortcuts (up to 3 keys) and two binding slots per\n    action are also supported.\n\n6. **What about TR3 support?**\n\n    TR3 work is well underway! Thanks to *TOMB3* serving as the backbone\n    for many of its core systems, tons of native systems are already in\n    place – new triangle geometry logic, new rendering effects, and\n    more. Still early, but we're moving _fast_.\n\n## Website\nThe Lost Artefacts team is a small, passionate community of developers and\ncreators with a deep love for the classic Tomb Raider titles. Our team builds\nand maintains freeware fan projects that celebrate Lara Croft's iconic\nadventures. Visit the website by clicking the logo below for more information\non TRX and its documentation as well as other Tomb Raider projects.\n\n<p align=\"left\">\n  <a href=\"https://lostartefacts.dev/\">\n    <img src=\"data/lostartefacts.png\" style=\"width: 128px;\" alt=\"TRX logo\"/>\n  </a>\n</p>\n\n## Credits\n\n- Endless GitHub contributors.\n- TR1 title screen image by Kidd Bowyer. HD assets by goblan and posix.\n- TR2 HD images by Arsunt.\n"
  },
  {
    "path": "data/common/glyphs/mapping.txt",
    "content": "# This file is used by the tooling in the tools/glyphs/ directory and is\n# crucial for the game's graphical text handling, serving two roles:\n#\n# 1. Hardcoding Unicode to sprite mapping\n#    It generates C macros that map Unicode code points and escaped sequences\n#    to O_ALPHABET's sprite indices, specify glyph dimensions, and instruct how\n#    to compose compound characters - all getting hardcoded into the executable.\n# 2. Guidance for font.bin creation\n#    It directs the injector tool in creating the font.bin file that contains\n#    O_ALPHABET sprite bitmaps, along with additional positional information.\n#\n# Important notes:\n# - Some sprite indices are fixed. This is for compatibility with the original\n#   game to retains original text format even if font.bin goes missing.\n\ninclude \"mapping_basic_latin.txt\"\ninclude \"mapping_icons.txt\"\ninclude \"mapping_combining_diactrics.txt\"\ninclude \"mapping_latin-1_supplement.txt\"\ninclude \"mapping_latin_extended-a.txt\"\ninclude \"mapping_latin_extended-b.txt\"\ninclude \"mapping_greek_and_coptic.txt\"\ninclude \"mapping_cyrillic.txt\"\ninclude \"mapping_latin_extended_additional.txt\"\ninclude \"mapping_misc.txt\"\ninclude \"mapping_keyboard.txt\"\ninclude \"mapping_controller.txt\"\ninclude \"mapping_small.txt\"\n"
  },
  {
    "path": "data/common/glyphs/mapping_basic_latin.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Basic Latin\" (U+0000 to U+007F)\n# --------------------------------------------------\n# ASCII a-z\nU+0061:a 0 T manual_sprite(\"glyphs.png\", 218, 41,  11, 18, index=26)\nU+0062:b 0 T manual_sprite(\"glyphs.png\", 229, 41,  11, 18, index=27)\nU+0063:c 0 T manual_sprite(\"glyphs.png\", 240, 41,  11, 18, index=28)\nU+0064:d 0 T manual_sprite(\"glyphs.png\", 0,   59,  11, 18, index=29)\nU+0065:e 0 T manual_sprite(\"glyphs.png\", 11,  59,  11, 18, index=30)\nU+0066:f 0 T manual_sprite(\"glyphs.png\", 22,  59,  11, 18, index=31)\nU+0067:g 0 T manual_sprite(\"glyphs.png\", 33,  59,  11, 18, index=32)\nU+0068:h 0 T manual_sprite(\"glyphs.png\", 44,  59,  11, 18, index=33)\nU+0069:i 0 T manual_sprite(\"glyphs.png\", 55,  59,  7,  18, index=34)\nU+006A:j 0 T manual_sprite(\"glyphs.png\", 62,  59,  11, 18, index=35)\nU+006B:k 0 T manual_sprite(\"glyphs.png\", 73,  59,  12, 18, index=36)\nU+006C:l 0 T manual_sprite(\"glyphs.png\", 85,  59,  7,  18, index=37)\nU+006D:m 0 T manual_sprite(\"glyphs.png\", 92,  59,  14, 18, index=38)\nU+006E:n 0 T manual_sprite(\"glyphs.png\", 106, 59,  12, 18, index=39)\nU+006F:o 0 T manual_sprite(\"glyphs.png\", 118, 59,  11, 18, index=40)\nU+0070:p 0 T manual_sprite(\"glyphs.png\", 129, 59,  11, 18, index=41)\nU+0071:q 0 T manual_sprite(\"glyphs.png\", 140, 59,  11, 18, index=42)\nU+0072:r 0 T manual_sprite(\"glyphs.png\", 151, 59,  10, 18, index=43)\nU+0073:s 0 T manual_sprite(\"glyphs.png\", 161, 59,  11, 18, index=44)\nU+0074:t 0 T manual_sprite(\"glyphs.png\", 172, 59,  11, 18, index=45)\nU+0075:u 0 T manual_sprite(\"glyphs.png\", 183, 59,  11, 18, index=46)\nU+0076:v 0 T manual_sprite(\"glyphs.png\", 194, 59,  11, 18, index=47)\nU+0077:w 0 T manual_sprite(\"glyphs.png\", 205, 59,  13, 18, index=48)\nU+0078:x 0 T manual_sprite(\"glyphs.png\", 218, 59,  11, 18, index=49)\nU+0079:y 0 T manual_sprite(\"glyphs.png\", 229, 59,  11, 18, index=50)\nU+007A:z 0 T manual_sprite(\"glyphs.png\", 240, 59,  11, 18, index=51)\n\n# ASCII A-Z\nU+0041:A 0 T manual_sprite(\"glyphs.png\", 65,  23,  17, 18, index=0) expand(-1)\nU+0042:B 0 T manual_sprite(\"glyphs.png\", 82,  23,  13, 18, index=1)\nU+0043:C 0 T manual_sprite(\"glyphs.png\", 95,  23,  13, 18, index=2)\nU+0044:D 0 T manual_sprite(\"glyphs.png\", 108, 23,  13, 18, index=3)\nU+0045:E 0 T manual_sprite(\"glyphs.png\", 121, 23,  13, 18, index=4)\nU+0046:F 0 T manual_sprite(\"glyphs.png\", 134, 23,  13, 18, index=5)\nU+0047:G 0 T manual_sprite(\"glyphs.png\", 147, 23,  13, 18, index=6)\nU+0048:H 0 T manual_sprite(\"glyphs.png\", 160, 23,  15, 18, index=7)\nU+0049:I 0 T manual_sprite(\"glyphs.png\", 175, 23,  10, 18, index=8)\nU+004A:J 0 T manual_sprite(\"glyphs.png\", 185, 23,  13, 18, index=9)\nU+004B:K 0 T manual_sprite(\"glyphs.png\", 198, 23,  14, 18, index=10)\nU+004C:L 0 T manual_sprite(\"glyphs.png\", 212, 23,  13, 18, index=11)\nU+004D:M 0 T manual_sprite(\"glyphs.png\", 225, 23,  15, 18, index=12)\nU+004E:N 0 T manual_sprite(\"glyphs.png\", 240, 23,  15, 18, index=13)\nU+004F:O 0 T manual_sprite(\"glyphs.png\", 0,   41,  14, 18, index=14)\nU+0050:P 0 T manual_sprite(\"glyphs.png\", 14,  41,  13, 18, index=15)\nU+0051:Q 0 T manual_sprite(\"glyphs.png\", 27,  41,  14, 18, index=16)\nU+0052:R 0 T manual_sprite(\"glyphs.png\", 41,  41,  14, 18, index=17)\nU+0053:S 0 T manual_sprite(\"glyphs.png\", 55,  41,  13, 18, index=18)\nU+0054:T 0 T manual_sprite(\"glyphs.png\", 68,  41,  14, 18, index=19)\nU+0055:U 0 T manual_sprite(\"glyphs.png\", 82,  41,  15, 18, index=20)\nU+0056:V 0 T manual_sprite(\"glyphs.png\", 97,  41,  15, 18, index=21)\nU+0057:W 0 T manual_sprite(\"glyphs.png\", 112, 41,  15, 18, index=22)\nU+0058:X 0 T manual_sprite(\"glyphs.png\", 127, 41,  14, 18, index=23)\nU+0059:Y 0 T manual_sprite(\"glyphs.png\", 141, 41,  14, 18, index=24)\nU+005A:Z 0 T manual_sprite(\"glyphs.png\", 155, 41,  13, 18, index=25)\n\n# Digits 0-9\nU+0030:0 0 T manual_sprite(\"glyphs.png\", 135, 5,  14, 18, index=52)\nU+0031:1 0 T manual_sprite(\"glyphs.png\", 149, 5,  11, 18, index=53)\nU+0032:2 0 T manual_sprite(\"glyphs.png\", 160, 5,  12, 18, index=54)\nU+0033:3 0 T manual_sprite(\"glyphs.png\", 172, 5,  12, 18, index=55)\nU+0034:4 0 T manual_sprite(\"glyphs.png\", 184, 5,  12, 18, index=56)\nU+0035:5 0 T manual_sprite(\"glyphs.png\", 196, 5,  12, 18, index=57)\nU+0036:6 0 T manual_sprite(\"glyphs.png\", 208, 5,  12, 18, index=58)\nU+0037:7 0 T manual_sprite(\"glyphs.png\", 220, 5,  12, 18, index=59)\nU+0038:8 0 T manual_sprite(\"glyphs.png\", 232, 5,  12, 18, index=60)\nU+0039:9 0 T manual_sprite(\"glyphs.png\", 244, 5,  12, 18, index=61)\n\n# Basic Punctuation\nU+0021:! 0 T manual_sprite(\"glyphs.png\", 0,   5,  6,  18, index=64)\nU+0022:\" 0 T manual_sprite(\"glyphs.png\", 6,   5,  9,  18)\nU+0023:# 0 T manual_sprite(\"glyphs.png\", 16,  5,  14, 18, index=78)\nU+0024:$ 0 T manual_sprite(\"glyphs.png\", 30,  5,  11, 18)\nU+0025:% 0 T manual_sprite(\"glyphs.png\", 41,  5,  13, 18)\nU+0026:& 0 T manual_sprite(\"glyphs.png\", 54,  5,  11, 18)\nU+0027:' 0 T manual_sprite(\"glyphs.png\", 65,  5,  6,  18, index=79)\nU+0028:( 0 T manual_sprite(\"glyphs.png\", 71,  5,  7,  18) expand(w=-1)\nU+0029:) 0 T manual_sprite(\"glyphs.png\", 78,  5,  7,  18) translate(x=1) expand(w=1)\nU+002A:* 0 T manual_sprite(\"glyphs.png\", 85,  5,  9,  18)\nU+002B:+ 0 T manual_sprite(\"glyphs.png\", 94,  5,  11, 18, index=72)\nU+002C:, 0 T manual_sprite(\"glyphs.png\", 105, 5,  6,  18, index=63)\nU+002D:- 0 T manual_sprite(\"glyphs.png\", 111, 5,  9,  18, index=71)\nU+002E:. 0 T manual_sprite(\"glyphs.png\", 120, 5,  6,  18, index=62)\nU+002F:/ 0 T manual_sprite(\"glyphs.png\", 126, 5,  9,  18, index=68)\nU+003A:: 0 T manual_sprite(\"glyphs.png\", 0,   23,  6,  18, index=73)\nU+003B:; 0 T manual_sprite(\"glyphs.png\", 6,   23,  6,  18)\nU+003C:< 0 T manual_sprite(\"glyphs.png\", 13,  23,  9,  18)\nU+003D:= 0 T manual_sprite(\"glyphs.png\", 22,  23,  9,  18)\nU+003E:> 0 T manual_sprite(\"glyphs.png\", 31,  23,  9,  18)\nU+003F:? 0 T manual_sprite(\"glyphs.png\", 41,  23,  12, 18, index=65)\nU+0040:@ 0 T manual_sprite(\"glyphs.png\", 53,  23,  12, 18)\nU+005B:[ 0 T manual_sprite(\"glyphs.png\", 168, 41,  8,  18, index=66)\nU+005C:\\ 0 T manual_sprite(\"glyphs.png\", 176, 41,  9,  18, index=76)\nU+005D:] 0 T manual_sprite(\"glyphs.png\", 185, 41,  8,  18, index=75)\nU+005E:^ 0 T manual_sprite(\"glyphs.png\", 193, 41,  9,  18)\nU+005F:_ 0 T manual_sprite(\"glyphs.png\", 202, 41,  9,  18)\nU+0060:` 0 T manual_sprite(\"glyphs.png\", 211, 41,  7,  18)\nU+007B:{ 0 T manual_sprite(\"glyphs.png\", 0,   77,  8,  18)\nU+007C:| 0 T manual_sprite(\"glyphs.png\", 8,   77,  6,  18)\nU+007D:} 0 T manual_sprite(\"glyphs.png\", 14,  77,  8,  18)\nU+007E:~ 0 T manual_sprite(\"glyphs.png\", 22,  77,  10, 18)\n"
  },
  {
    "path": "data/common/glyphs/mapping_combining_diactrics.txt",
    "content": "# --------------------------------------------------\n# Combining diactrics\n# --------------------------------------------------\n\"\\\\{grave accent}\"        0 c manual_sprite(\"glyphs.png\", 0,   0,  7,  5, index=77)\n\"\\\\{acute accent}\"        0 c manual_sprite(\"glyphs.png\", 7,   0,  7,  5, index=70)\n\"\\\\{circumflex accent}\"   0 c manual_sprite(\"glyphs.png\", 32,  0,  9,  5, index=69)\n\"\\\\{circumflex}\"          0 T link(\"\\\\{circumflex accent}\")\n\"\\\\{macron}\"              0 c manual_sprite(\"glyphs.png\", 23,  0,  9,  5)\n\"\\\\{breve}\"               0 c manual_sprite(\"glyphs.png\", 50,  0,  8,  5)\n\"\\\\{dot above}\"           0 c manual_sprite(\"glyphs.png\", 58,  0,  6,  5)\n\"\\\\{umlaut}\"              0 c manual_sprite(\"glyphs.png\", 14,  0,  9,  5, index=67)\n\"\\\\{caron}\"               0 c manual_sprite(\"glyphs.png\", 41,  0,  9,  5)\n\"\\\\{ring above}\"          0 c manual_sprite(\"glyphs.png\", 64,  0,  7,  5)\n\"\\\\{tilde}\"               0 c manual_sprite(\"glyphs.png\", 71,  0,  10, 5)\n\"\\\\{double acute accent}\" 0 c manual_sprite(\"glyphs.png\", 81,  0,  9,  5)\n\"\\\\{acute umlaut}\"        0 c manual_sprite(\"glyphs.png\", 90,  0,  11,  5)\n"
  },
  {
    "path": "data/common/glyphs/mapping_controller.txt",
    "content": "# --------------------------------------------------\n# Controller button icons\n# --------------------------------------------------\n\"\\\\{controller rstick}\"             0 I manual_sprite(\"controller.png\", 0,   0,   16, 16) translate(y=1)\n\"\\\\{controller rstick up}\"          0 I manual_sprite(\"controller.png\", 16,  0,   16, 16) translate(y=1)\n\"\\\\{controller rstick right}\"       0 I manual_sprite(\"controller.png\", 32,  0,   16, 16) translate(y=1)\n\"\\\\{controller rstick down}\"        0 I manual_sprite(\"controller.png\", 48,  0,   16, 16) translate(y=1)\n\"\\\\{controller rstick left}\"        0 I manual_sprite(\"controller.png\", 64,  0,   16, 16) translate(y=1)\n\"\\\\{controller lstick}\"             0 I manual_sprite(\"controller.png\", 0,   16,  16, 16) translate(y=1)\n\"\\\\{controller lstick up}\"          0 I manual_sprite(\"controller.png\", 16,  16,  16, 16) translate(y=1)\n\"\\\\{controller lstick right}\"       0 I manual_sprite(\"controller.png\", 32,  16,  16, 16) translate(y=1)\n\"\\\\{controller lstick down}\"        0 I manual_sprite(\"controller.png\", 48,  16,  16, 16) translate(y=1)\n\"\\\\{controller lstick left}\"        0 I manual_sprite(\"controller.png\", 64,  16,  16, 16) translate(y=1)\n\"\\\\{controller dpad up}\"            0 I manual_sprite(\"controller.png\", 0,   32,  15, 15) translate(y=1)\n\"\\\\{controller dpad right}\"         0 I manual_sprite(\"controller.png\", 16,  32,  15, 15) translate(y=1)\n\"\\\\{controller dpad down}\"          0 I manual_sprite(\"controller.png\", 32,  32,  15, 15) translate(y=1)\n\"\\\\{controller dpad left}\"          0 I manual_sprite(\"controller.png\", 48,  32,  15, 15) translate(y=1)\n\"\\\\{controller button l1}\"          0 I manual_sprite(\"controller.png\", 80,  0,   15, 15) translate(y=4)\n\"\\\\{controller button r1}\"          0 I manual_sprite(\"controller.png\", 96,  0,   15, 15) translate(y=4)\n\"\\\\{controller button l2}\"          0 I manual_sprite(\"controller.png\", 80,  16,  15, 15) translate(y=4)\n\"\\\\{controller button r2}\"          0 I manual_sprite(\"controller.png\", 96,  16,  15, 15) translate(y=4)\n\"\\\\{controller bumper left}\"        0 I manual_sprite(\"controller.png\", 80,  32,  16, 15) translate(y=4)\n\"\\\\{controller bumper right}\"       0 I manual_sprite(\"controller.png\", 96,  32,  16, 15) translate(y=4)\n\"\\\\{controller button zl}\"          0 I manual_sprite(\"controller.png\", 80,  48,  15, 15) translate(y=4)\n\"\\\\{controller button zr}\"          0 I manual_sprite(\"controller.png\", 96,  48,  15, 15) translate(y=4)\n\"\\\\{controller trigger left}\"       0 I manual_sprite(\"controller.png\", 80,  64,  15, 15) translate(y=1, x=2)\n\"\\\\{controller trigger right}\"      0 I manual_sprite(\"controller.png\", 96,  64,  15, 15) translate(y=2, x=2)\n\"\\\\{controller button a}\"           0 I manual_sprite(\"controller.png\", 0,   48,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button b}\"           0 I manual_sprite(\"controller.png\", 16,  48,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button x}\"           0 I manual_sprite(\"controller.png\", 32,  48,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button y}\"           0 I manual_sprite(\"controller.png\", 48,  48,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button xbox}\"        0 I manual_sprite(\"controller.png\", 64,  48,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button triangle}\"    0 I manual_sprite(\"controller.png\", 0,   64,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button square}\"      0 I manual_sprite(\"controller.png\", 16,  64,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button cross}\"       0 I manual_sprite(\"controller.png\", 32,  64,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button circle}\"      0 I manual_sprite(\"controller.png\", 48,  64,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button ps}\"          0 I manual_sprite(\"controller.png\", 64,  64,  15, 15) translate(y=2, x=1)\n\"\\\\{controller button capture}\"     0 I manual_sprite(\"controller.png\", 0,   80,  64, 15) translate(y=2)\n\"\\\\{controller button touchpad}\"    0 I manual_sprite(\"controller.png\", 0,   96,  64, 15) translate(y=2)\n\"\\\\{controller button paddle 1}\"    0 I manual_sprite(\"controller.png\", 0,   112, 64, 15) translate(y=2)\n\"\\\\{controller button paddle 2}\"    0 I manual_sprite(\"controller.png\", 0,   128, 64, 15) translate(y=2)\n\"\\\\{controller button paddle 3}\"    0 I manual_sprite(\"controller.png\", 0,   144, 64, 15) translate(y=2)\n\"\\\\{controller button paddle 4}\"    0 I manual_sprite(\"controller.png\", 0,   160, 64, 15) translate(y=2)\n\"\\\\{controller button share}\"       0 I manual_sprite(\"controller.png\", 64,  80,  48, 15) translate(y=2)\n\"\\\\{controller button back}\"        0 I manual_sprite(\"controller.png\", 64,  96,  48, 15) translate(y=2)\n\"\\\\{controller button start}\"       0 I manual_sprite(\"controller.png\", 64,  112, 48, 15) translate(y=2)\n\"\\\\{controller button mic}\"         0 I manual_sprite(\"controller.png\", 64,  128, 32, 15) translate(y=2)\n\"\\\\{controller button home}\"        0 I manual_sprite(\"controller.png\", 64,  144, 48, 15) translate(y=2)\n\"\\\\{controller button options}\"     0 I manual_sprite(\"controller.png\", 64,  160, 48, 15) translate(y=2)\n"
  },
  {
    "path": "data/common/glyphs/mapping_cyrillic.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Cyrillic\" (U+0400 to U+04FF)\n# --------------------------------------------------\nU+0400:Ѐ 0 C combine(U+0415:Е, \"\\\\{grave accent}\")\nU+0401:Ё 0 C combine(U+0415:Е, \"\\\\{umlaut}\")\nU+0402:Ђ 0 T manual_sprite(\"glyphs.png\", 73,  167, 15, 18)\nU+0403:Ѓ 0 C combine(U+0413:Г, \"\\\\{acute accent}\")\nU+0404:Є 0 T manual_sprite(\"glyphs.png\", 88,  167, 13, 18)\nU+0405:Ѕ 0 T link(\"S\")\nU+0406:І 0 T link(\"I\")\nU+0407:Ї 0 C combine(U+0406:І, \"\\\\{umlaut}\")\nU+0408:Ј 0 T link(\"J\")\nU+0409:Љ 0 T manual_sprite(\"glyphs.png\", 101, 167, 18, 18)\nU+040A:Њ 0 T manual_sprite(\"glyphs.png\", 119, 167, 18, 18)\nU+040B:Ћ 0 T manual_sprite(\"glyphs.png\", 137, 167, 16, 18)\nU+040C:Ќ 0 C combine(U+041A:К, \"\\\\{acute accent}\")\nU+040D:Ѝ 0 C combine(U+0418:И, \"\\\\{grave accent}\")\nU+040E:Ў 0 C combine(U+0423:У, \"\\\\{breve}\")\nU+040F:Џ 0 T manual_sprite(\"glyphs.png\", 153, 167, 14, 18)\nU+0410:А 0 T link(\"A\")\nU+0411:Б 0 T manual_sprite(\"glyphs.png\", 167, 167, 13, 18)\nU+0412:В 0 T link(\"B\")\nU+0413:Г 0 T link(U+0393:Γ)\nU+0414:Д 0 T manual_sprite(\"glyphs.png\", 180, 167, 16, 18)\nU+0415:Е 0 T link(\"E\")\nU+0416:Ж 0 T manual_sprite(\"glyphs.png\", 196, 167, 18, 18)\nU+0417:З 0 T manual_sprite(\"glyphs.png\", 214, 167, 12, 18)\nU+0418:И 0 T manual_sprite(\"glyphs.png\", 226, 167, 15, 18)\nU+0419:Й 0 C combine(U+0418:И, \"\\\\{breve}\")\nU+041A:К 0 T link(\"K\")\nU+041B:Л 0 T manual_sprite(\"glyphs.png\", 241, 167, 15, 18)\nU+041C:М 0 T link(\"M\")\nU+041D:Н 0 T link(\"H\")\nU+041E:О 0 T link(\"O\")\nU+041F:П 0 T link(U+03A0:Π)\nU+0420:Р 0 T link(\"P\")\nU+0421:С 0 T link(\"C\")\nU+0422:Т 0 T link(\"T\")\nU+0423:У 0 T manual_sprite(\"glyphs.png\", 0,   185, 14, 18)\nU+0424:Ф 0 T link(U+03A6:Φ)\nU+0425:Х 0 T link(\"X\")\nU+0426:Ц 0 T manual_sprite(\"glyphs.png\", 14,  185, 15, 18)\nU+0427:Ч 0 T manual_sprite(\"glyphs.png\", 29,  185, 15, 18)\nU+0428:Ш 0 T manual_sprite(\"glyphs.png\", 44,  185, 18, 18)\nU+0429:Щ 0 T manual_sprite(\"glyphs.png\", 62,  185, 18, 18)\nU+042A:Ъ 0 T manual_sprite(\"glyphs.png\", 80,  185, 15, 18)\nU+042B:Ы 0 T manual_sprite(\"glyphs.png\", 95,  185, 18, 18)\nU+042C:Ь 0 T manual_sprite(\"glyphs.png\", 113, 185, 13, 18)\nU+042D:Э 0 T manual_sprite(\"glyphs.png\", 126, 185, 13, 18)\nU+042E:Ю 0 T manual_sprite(\"glyphs.png\", 139, 185, 18, 18)\nU+042F:Я 0 T manual_sprite(\"glyphs.png\", 157, 185, 13, 18)\nU+0430:а 0 T link(\"a\")\nU+0431:б 0 T manual_sprite(\"glyphs.png\", 170, 185, 11, 18)\nU+0432:в 0 T manual_sprite(\"glyphs.png\", 181, 185, 11, 18)\nU+0433:г 0 T manual_sprite(\"glyphs.png\", 192, 185, 10, 18)\nU+0434:д 0 T manual_sprite(\"glyphs.png\", 202, 185, 12, 18)\nU+0435:е 0 T link(\"e\")\nU+0436:ж 0 T manual_sprite(\"glyphs.png\", 214, 185, 17, 18)\nU+0437:з 0 T manual_sprite(\"glyphs.png\", 232, 185, 11, 18)\nU+0438:и 0 T manual_sprite(\"glyphs.png\", 243, 185, 12, 18)\nU+0439:й 0 C combine(U+0438:и, \"\\\\{breve}\")\nU+043A:к 0 T link(U+0138:ĸ)\nU+043B:л 0 T manual_sprite(\"glyphs.png\", 0,   203, 11, 18)\nU+043C:м 0 T manual_sprite(\"glyphs.png\", 11,  203, 13, 18)\nU+043D:н 0 T manual_sprite(\"glyphs.png\", 24,  203, 11, 18)\nU+043E:о 0 T link(\"o\")\nU+043F:п 0 T manual_sprite(\"glyphs.png\", 35,  203, 11, 18)\nU+0440:р 0 T link(\"p\")\nU+0441:с 0 T link(\"c\")\nU+0442:т 0 T manual_sprite(\"glyphs.png\", 46,  203, 10, 18)\nU+0443:у 0 T link(\"y\")\nU+0444:ф 0 T manual_sprite(\"glyphs.png\", 56,  203, 14, 18)\nU+0445:х 0 T link(\"x\")\nU+0446:ц 0 T manual_sprite(\"glyphs.png\", 70,  203, 12, 18)\nU+0447:ч 0 T manual_sprite(\"glyphs.png\", 82,  203, 11, 18)\nU+0448:ш 0 T manual_sprite(\"glyphs.png\", 93,  203, 14, 18)\nU+0449:щ 0 T manual_sprite(\"glyphs.png\", 107, 203, 15, 18)\nU+044A:ъ 0 T manual_sprite(\"glyphs.png\", 122, 203, 13, 18)\nU+044B:ы 0 T manual_sprite(\"glyphs.png\", 135, 203, 14, 18)\nU+044C:ь 0 T manual_sprite(\"glyphs.png\", 149, 203, 11, 18)\nU+044D:э 0 T manual_sprite(\"glyphs.png\", 160, 203, 11, 18)\nU+044E:ю 0 T manual_sprite(\"glyphs.png\", 171, 203, 14, 18)\nU+044F:я 0 T manual_sprite(\"glyphs.png\", 185, 203, 11, 18)\nU+0450:ѐ 0 C combine(\"e\", \"\\\\{grave accent}\")\nU+0451:ё 0 C combine(\"e\", \"\\\\{umlaut}\")\nU+0452:ђ 0 T manual_sprite(\"glyphs.png\", 196, 203, 12, 18)\nU+0453:ѓ 0 C combine(U+0433:г, \"\\\\{acute accent}\")\nU+0454:є 0 T manual_sprite(\"glyphs.png\", 208, 203, 11, 18)\nU+0455:ѕ 0 T link(\"s\")\nU+0456:і 0 T link(\"i\")\nU+0457:ї 0 C combine(U+0131:ı, \"\\\\{umlaut}\")\nU+0458:ј 0 T link(\"j\")\nU+0459:љ 0 T manual_sprite(\"glyphs.png\", 219, 203, 16, 18)\nU+045A:њ 0 T manual_sprite(\"glyphs.png\", 235, 203, 16, 18)\nU+045B:ћ 0 T link(U+0127:ħ)\nU+045C:ќ 0 C combine(U+043A:к, \"\\\\{acute accent}\")\nU+045D:ѝ 0 C combine(U+0438:и, \"\\\\{grave accent}\")\nU+045E:ў 0 C combine(U+0443:у, \"\\\\{breve}\")\nU+045F:џ 0 T manual_sprite(\"glyphs.png\", 0,   221, 11, 18)\nU+0490:Ґ 0 T manual_sprite(\"glyphs.png\", 11,  221, 13, 18)\nU+0491:ґ 0 T manual_sprite(\"glyphs.png\", 24,  221, 10, 18)\n"
  },
  {
    "path": "data/common/glyphs/mapping_greek_and_coptic.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Greek and Coptic\" (U+0370 to U+03FF)\n# --------------------------------------------------\nU+0393:Γ 0 T manual_sprite(\"glyphs.png\", 194, 131, 13, 18)\nU+0394:Δ 0 T manual_sprite(\"glyphs.png\", 207, 131, 15, 18)\nU+0395:Ε 0 T link(\"E\")\nU+0396:Ζ 0 T link(\"Z\")\nU+0397:Η 0 T link(\"H\")\nU+0398:Θ 0 T manual_sprite(\"glyphs.png\", 222, 131, 14, 18)\nU+0399:Ι 0 T link(\"I\")\nU+039A:Κ 0 T link(\"K\")\nU+039B:Λ 0 T manual_sprite(\"glyphs.png\", 236, 131, 17, 18)\nU+039C:Μ 0 T link(\"M\")\nU+039D:Ν 0 T link(\"N\")\nU+039E:Ξ 0 T manual_sprite(\"glyphs.png\", 0,   149, 14, 18)\nU+039F:Ο 0 T link(\"O\")\nU+03A0:Π 0 T manual_sprite(\"glyphs.png\", 14,  149, 15, 18)\nU+03A1:Ρ 0 T link(\"P\")\nU+03A3:Σ 0 T manual_sprite(\"glyphs.png\", 29,  149, 13, 18)\nU+03A4:Τ 0 T link(\"T\")\nU+03A5:Υ 0 T link(\"Y\")\nU+03A6:Φ 0 T manual_sprite(\"glyphs.png\", 42,  149, 16, 18)\nU+03A7:Χ 0 T link(\"X\")\nU+03A8:Ψ 0 T manual_sprite(\"glyphs.png\", 58,  149, 18, 18)\nU+03A9:Ω 0 T manual_sprite(\"glyphs.png\", 76,  149, 14, 18)\nU+03B1:α 0 T manual_sprite(\"glyphs.png\", 90,  149, 12, 18)\nU+03B2:β 0 T manual_sprite(\"glyphs.png\", 102, 149, 11, 18)\nU+03B3:γ 0 T manual_sprite(\"glyphs.png\", 113, 149, 12, 18)\nU+03B4:δ 0 T manual_sprite(\"glyphs.png\", 125, 149, 11, 18)\nU+03B5:ε 0 T manual_sprite(\"glyphs.png\", 136, 149, 11, 18)\nU+03B6:ζ 0 T manual_sprite(\"glyphs.png\", 147, 149, 11, 18)\nU+03B7:η 0 T manual_sprite(\"glyphs.png\", 158, 149, 11, 18)\nU+03B8:θ 0 T manual_sprite(\"glyphs.png\", 169, 149, 11, 18)\nU+03B9:ι 0 T link(U+0131:ı)\nU+03BA:κ 0 T link(U+0138:ĸ)\nU+03BB:λ 0 T manual_sprite(\"glyphs.png\", 180, 149, 12, 18)\nU+03BC:μ 0 T link(U+00B5:µ)\nU+03BD:ν 0 T link(\"v\")\nU+03BE:ξ 0 T manual_sprite(\"glyphs.png\", 192, 149, 11, 18)\nU+03BF:ο 0 T link(\"o\")\nU+03C0:π 0 T manual_sprite(\"glyphs.png\", 203, 149, 13, 18)\nU+03C1:ρ 0 T manual_sprite(\"glyphs.png\", 216, 149, 11, 18)\nU+03C2:ς 0 T manual_sprite(\"glyphs.png\", 227, 149, 11, 18)\nU+03C3:σ 0 T manual_sprite(\"glyphs.png\", 238, 149, 12, 18)\nU+03C4:τ 0 T manual_sprite(\"glyphs.png\", 0,   167, 10, 18)\nU+03C5:υ 0 T manual_sprite(\"glyphs.png\", 10,  167, 11, 18)\nU+03C6:φ 0 T manual_sprite(\"glyphs.png\", 21,  167, 14, 18)\nU+03C7:χ 0 T manual_sprite(\"glyphs.png\", 35,  167, 11, 18)\nU+03C8:ψ 0 T manual_sprite(\"glyphs.png\", 46,  167, 14, 18)\nU+03C9:ω 0 T manual_sprite(\"glyphs.png\", 60,  167, 13, 18)\n\nU+0386:Ά 0 C combine(U+0391:Α, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+0388:Έ 0 C combine(U+0395:Ε, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+0389:Ή 0 C combine(U+0397:Η, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+038A:Ί 0 C combine(U+0399:Ι, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+038C:Ό 0 C combine(U+039F:Ο, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+038E:Ύ 0 C combine(U+03A5:Υ, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+038F:Ώ 0 C combine(U+03A9:Ω, \"\\\\{acute accent}\", offset_x=-4, offset_y=2)\nU+0390:ΐ 0 C combine(U+03B9:ι, \"\\\\{acute umlaut}\")\nU+0391:Α 0 T link(\"A\")\nU+0392:Β 0 T link(\"B\")\nU+03AA:Ϊ 0 C combine(U+0399:Ι, \"\\\\{umlaut}\")\nU+03AB:Ϋ 0 C combine(U+03A5:Υ, \"\\\\{umlaut}\")\nU+03AC:ά 0 C combine(U+03B1:α, \"\\\\{acute accent}\")\nU+03AD:έ 0 C combine(U+03B5:ε, \"\\\\{acute accent}\")\nU+03AE:ή 0 C combine(U+03B7:η, \"\\\\{acute accent}\")\nU+03AF:ί 0 C combine(U+03B9:ι, \"\\\\{acute accent}\")\nU+03B0:ΰ 0 C combine(U+03C5:υ, \"\\\\{acute umlaut}\")\nU+03CA:ϊ 0 C combine(U+03B9:ι, \"\\\\{umlaut}\")\nU+03CB:ϋ 0 C combine(U+03C5:υ, \"\\\\{umlaut}\")\nU+03CC:ό 0 C combine(U+03BF:ο, \"\\\\{acute accent}\")\nU+03CD:ύ 0 C combine(U+03C5:υ, \"\\\\{acute accent}\")\nU+03CE:ώ 0 C combine(U+03C9:ω, \"\\\\{acute accent}\")\n"
  },
  {
    "path": "data/common/glyphs/mapping_icons.txt",
    "content": "# --------------------------------------------------\n# Icons\n# --------------------------------------------------\n\"\\\\{button down}\"     0 I grid_sprite(\"buttons.png\", 0, 1, index=106)\n\"\\\\{button up}\"       0 I grid_sprite(\"buttons.png\", 1, 1, index=107)\n\"\\\\{button left}\"     0 I grid_sprite(\"buttons.png\", 2, 1, index=108)\n\"\\\\{button right}\"    0 I grid_sprite(\"buttons.png\", 3, 1, index=109)\n\"\\\\{button triangle}\" 0 I grid_sprite(\"buttons.png\", 0, 2, index=93)\n\"\\\\{button circle}\"   0 I grid_sprite(\"buttons.png\", 1, 2, index=94)\n\"\\\\{button x}\"        0 I grid_sprite(\"buttons.png\", 2, 2, index=95)\n\"\\\\{button square}\"   0 I grid_sprite(\"buttons.png\", 3, 2, index=96)\n\"\\\\{button empty}\"    0 I grid_sprite(\"buttons.png\", 0, 3, index=92)\n\"\\\\{button l1}\"       0 I grid_sprite(\"buttons.png\", 1, 3, index=97)\n\"\\\\{button r1}\"       0 I grid_sprite(\"buttons.png\", 2, 3, index=98)\n\"\\\\{button l2}\"       0 I grid_sprite(\"buttons.png\", 3, 3, index=99)\n\"\\\\{button r2}\"       0 I grid_sprite(\"buttons.png\", 4, 3, index=100)\n\"\\\\{icon sound}\"      0 I grid_sprite(\"buttons.png\", 8, 3, index=101)\n\"\\\\{icon music}\"      0 I grid_sprite(\"buttons.png\", 9, 3, index=102)\n\"\\\\{ammo shotgun}\"    0 I grid_sprite(\"buttons.png\", 7, 1, index=103)\n\"\\\\{ammo magnums}\"    0 I grid_sprite(\"buttons.png\", 8, 1, index=104)\n\"\\\\{ammo uzis}\"       0 I grid_sprite(\"buttons.png\", 9, 1, index=105)\n\"\\\\{arrow up}\"        0 I grid_sprite(\"buttons.png\", 8, 2, index=80)\n\"\\\\{arrow down}\"      0 I grid_sprite(\"buttons.png\", 9, 2, index=81) translate(y=-2)\n\"\\\\{review}\"          0 R grid_sprite(\"buttons.png\", 7, 2)\n"
  },
  {
    "path": "data/common/glyphs/mapping_keyboard.txt",
    "content": "# --------------------------------------------------\n# Keyboard button icons\n# --------------------------------------------------\n\"\\\\{keyboard backspace}\"            0 I manual_sprite(\"keyboard.png\", 0, 0, 60, 14) translate(y=2)\n\"\\\\{keyboard scroll_lock}\"          0 I manual_sprite(\"keyboard.png\", 60, 0, 60, 14) translate(y=2)\n\"\\\\{keyboard return}\"               0 I manual_sprite(\"keyboard.png\", 120, 0, 50, 14) translate(y=2)\n\"\\\\{keyboard caps_lock}\"            0 I manual_sprite(\"keyboard.png\", 170, 0, 50, 14) translate(y=2)\n\"\\\\{keyboard print_screen}\"         0 I manual_sprite(\"keyboard.png\", 220, 0, 50, 14) translate(y=2)\n\"\\\\{keyboard insert}\"               0 I manual_sprite(\"keyboard.png\", 270, 0, 50, 14) translate(y=2)\n\"\\\\{keyboard num_lock}\"             0 I manual_sprite(\"keyboard.png\", 320, 0, 50, 14) translate(y=2)\n\"\\\\{keyboard l_ctrl}\"               0 I manual_sprite(\"keyboard.png\", 370, 0, 32, 14) translate(y=2)\n\"\\\\{keyboard r_ctrl}\"               0 I manual_sprite(\"keyboard.png\", 402, 0, 32, 14) translate(y=2)\n\"\\\\{keyboard r_shift}\"              0 I manual_sprite(\"keyboard.png\", 434, 0, 32, 14) translate(y=2)\n\"\\\\{keyboard l_shift}\"              0 I manual_sprite(\"keyboard.png\", 466, 0, 32, 14) translate(y=2)\n\"\\\\{keyboard r_alt}\"                0 I manual_sprite(\"keyboard.png\", 0, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard l_alt}\"                0 I manual_sprite(\"keyboard.png\", 32, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard l_win}\"                0 I manual_sprite(\"keyboard.png\", 64, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard r_win}\"                0 I manual_sprite(\"keyboard.png\", 96, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard escape}\"               0 I manual_sprite(\"keyboard.png\", 128, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard tab}\"                  0 I manual_sprite(\"keyboard.png\", 160, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard space}\"                0 I manual_sprite(\"keyboard.png\", 192, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard pause}\"                0 I manual_sprite(\"keyboard.png\", 224, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard home}\"                 0 I manual_sprite(\"keyboard.png\", 256, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard page_up}\"              0 I manual_sprite(\"keyboard.png\", 288, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard delete}\"               0 I manual_sprite(\"keyboard.png\", 320, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard end}\"                  0 I manual_sprite(\"keyboard.png\", 352, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard page_down}\"            0 I manual_sprite(\"keyboard.png\", 384, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard f10}\"                  0 I manual_sprite(\"keyboard.png\", 416, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard f11}\"                  0 I manual_sprite(\"keyboard.png\", 448, 14, 32, 14) translate(y=2)\n\"\\\\{keyboard f12}\"                  0 I manual_sprite(\"keyboard.png\", 0, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f13}\"                  0 I manual_sprite(\"keyboard.png\", 32, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f14}\"                  0 I manual_sprite(\"keyboard.png\", 64, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f15}\"                  0 I manual_sprite(\"keyboard.png\", 96, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f16}\"                  0 I manual_sprite(\"keyboard.png\", 128, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f17}\"                  0 I manual_sprite(\"keyboard.png\", 160, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f18}\"                  0 I manual_sprite(\"keyboard.png\", 192, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f19}\"                  0 I manual_sprite(\"keyboard.png\", 224, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f20}\"                  0 I manual_sprite(\"keyboard.png\", 256, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f21}\"                  0 I manual_sprite(\"keyboard.png\", 288, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f22}\"                  0 I manual_sprite(\"keyboard.png\", 320, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f23}\"                  0 I manual_sprite(\"keyboard.png\", 352, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard f24}\"                  0 I manual_sprite(\"keyboard.png\", 384, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard num_0}\"                0 I manual_sprite(\"keyboard.png\", 416, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard num_1}\"                0 I manual_sprite(\"keyboard.png\", 448, 28, 32, 14) translate(y=2)\n\"\\\\{keyboard num_2}\"                0 I manual_sprite(\"keyboard.png\", 0, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_3}\"                0 I manual_sprite(\"keyboard.png\", 32, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_4}\"                0 I manual_sprite(\"keyboard.png\", 64, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_5}\"                0 I manual_sprite(\"keyboard.png\", 96, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_6}\"                0 I manual_sprite(\"keyboard.png\", 128, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_7}\"                0 I manual_sprite(\"keyboard.png\", 160, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_8}\"                0 I manual_sprite(\"keyboard.png\", 192, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_9}\"                0 I manual_sprite(\"keyboard.png\", 224, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_period}\"           0 I manual_sprite(\"keyboard.png\", 256, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_divide}\"           0 I manual_sprite(\"keyboard.png\", 288, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_multiply}\"         0 I manual_sprite(\"keyboard.png\", 320, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_minus}\"            0 I manual_sprite(\"keyboard.png\", 352, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_plus}\"             0 I manual_sprite(\"keyboard.png\", 384, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_equals}\"           0 I manual_sprite(\"keyboard.png\", 416, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_comma}\"            0 I manual_sprite(\"keyboard.png\", 448, 42, 32, 14) translate(y=2)\n\"\\\\{keyboard num_enter}\"            0 I manual_sprite(\"keyboard.png\", 0, 56, 32, 14) translate(y=2)\n\"\\\\{keyboard unknown}\"              0 I manual_sprite(\"keyboard.png\", 32, 56, 32, 14) translate(y=2)\n\"\\\\{keyboard f1}\"                   0 I manual_sprite(\"keyboard.png\", 64, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f2}\"                   0 I manual_sprite(\"keyboard.png\", 85, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f3}\"                   0 I manual_sprite(\"keyboard.png\", 106, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f4}\"                   0 I manual_sprite(\"keyboard.png\", 127, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f5}\"                   0 I manual_sprite(\"keyboard.png\", 148, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f6}\"                   0 I manual_sprite(\"keyboard.png\", 169, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f7}\"                   0 I manual_sprite(\"keyboard.png\", 190, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f8}\"                   0 I manual_sprite(\"keyboard.png\", 211, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard f9}\"                   0 I manual_sprite(\"keyboard.png\", 232, 56, 21, 14) translate(y=2)\n\"\\\\{keyboard left}\"                 0 I manual_sprite(\"keyboard.png\", 480, 14, 15, 14) translate(y=2)\n\"\\\\{keyboard up}\"                   0 I manual_sprite(\"keyboard.png\", 480, 28, 15, 14) translate(y=2)\n\"\\\\{keyboard right}\"                0 I manual_sprite(\"keyboard.png\", 480, 42, 15, 14) translate(y=2)\n\"\\\\{keyboard down}\"                 0 I manual_sprite(\"keyboard.png\", 253, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard a}\"                    0 I manual_sprite(\"keyboard.png\", 268, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard b}\"                    0 I manual_sprite(\"keyboard.png\", 283, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard c}\"                    0 I manual_sprite(\"keyboard.png\", 298, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard d}\"                    0 I manual_sprite(\"keyboard.png\", 313, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard e}\"                    0 I manual_sprite(\"keyboard.png\", 328, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard f}\"                    0 I manual_sprite(\"keyboard.png\", 343, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard g}\"                    0 I manual_sprite(\"keyboard.png\", 358, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard h}\"                    0 I manual_sprite(\"keyboard.png\", 373, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard i}\"                    0 I manual_sprite(\"keyboard.png\", 388, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard j}\"                    0 I manual_sprite(\"keyboard.png\", 403, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard k}\"                    0 I manual_sprite(\"keyboard.png\", 418, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard l}\"                    0 I manual_sprite(\"keyboard.png\", 433, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard m}\"                    0 I manual_sprite(\"keyboard.png\", 448, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard n}\"                    0 I manual_sprite(\"keyboard.png\", 463, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard o}\"                    0 I manual_sprite(\"keyboard.png\", 478, 56, 15, 14) translate(y=2)\n\"\\\\{keyboard p}\"                    0 I manual_sprite(\"keyboard.png\", 0, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard q}\"                    0 I manual_sprite(\"keyboard.png\", 15, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard r}\"                    0 I manual_sprite(\"keyboard.png\", 30, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard s}\"                    0 I manual_sprite(\"keyboard.png\", 45, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard t}\"                    0 I manual_sprite(\"keyboard.png\", 60, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard u}\"                    0 I manual_sprite(\"keyboard.png\", 75, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard v}\"                    0 I manual_sprite(\"keyboard.png\", 90, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard w}\"                    0 I manual_sprite(\"keyboard.png\", 105, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard x}\"                    0 I manual_sprite(\"keyboard.png\", 120, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard y}\"                    0 I manual_sprite(\"keyboard.png\", 135, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard z}\"                    0 I manual_sprite(\"keyboard.png\", 150, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 0}\"                    0 I manual_sprite(\"keyboard.png\", 165, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 1}\"                    0 I manual_sprite(\"keyboard.png\", 180, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 2}\"                    0 I manual_sprite(\"keyboard.png\", 195, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 3}\"                    0 I manual_sprite(\"keyboard.png\", 210, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 4}\"                    0 I manual_sprite(\"keyboard.png\", 225, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 5}\"                    0 I manual_sprite(\"keyboard.png\", 240, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 6}\"                    0 I manual_sprite(\"keyboard.png\", 255, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 7}\"                    0 I manual_sprite(\"keyboard.png\", 270, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 8}\"                    0 I manual_sprite(\"keyboard.png\", 285, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard 9}\"                    0 I manual_sprite(\"keyboard.png\", 300, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard minus}\"                0 I manual_sprite(\"keyboard.png\", 315, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard equals}\"               0 I manual_sprite(\"keyboard.png\", 330, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard left_square_bracket}\"  0 I manual_sprite(\"keyboard.png\", 345, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard right_square_bracket}\" 0 I manual_sprite(\"keyboard.png\", 360, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard backslash}\"            0 I manual_sprite(\"keyboard.png\", 375, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard hash}\"                 0 I manual_sprite(\"keyboard.png\", 390, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard semicolon}\"            0 I manual_sprite(\"keyboard.png\", 405, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard apostrophe}\"           0 I manual_sprite(\"keyboard.png\", 420, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard backtick}\"             0 I manual_sprite(\"keyboard.png\", 435, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard comma}\"                0 I manual_sprite(\"keyboard.png\", 450, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard period}\"               0 I manual_sprite(\"keyboard.png\", 465, 70, 15, 14) translate(y=2)\n\"\\\\{keyboard slash}\"                0 I manual_sprite(\"keyboard.png\", 480, 70, 15, 14) translate(y=2)\n"
  },
  {
    "path": "data/common/glyphs/mapping_latin-1_supplement.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin-1 Supplement\" (U+0080 to U+00FF)\n# --------------------------------------------------\nU+00A1:¡ 0 T manual_sprite(\"glyphs.png\", 32,  77,  6,  18)\nU+00A2:¢ 0 T manual_sprite(\"glyphs.png\", 38,  77,  11, 18)\nU+00A3:£ 0 T manual_sprite(\"glyphs.png\", 49,  77,  13, 18)\nU+00A4:¤ 0 T manual_sprite(\"glyphs.png\", 62,  77,  12, 18)\nU+00A5:¥ 0 T manual_sprite(\"glyphs.png\", 74,  77,  14, 18)\nU+00A6:¦ 0 T manual_sprite(\"glyphs.png\", 88,  77,  6,  18)\nU+00A7:§ 0 T manual_sprite(\"glyphs.png\", 94,  77,  11, 18)\nU+00A9:© 0 T manual_sprite(\"glyphs.png\", 105, 77,  15, 18)\nU+00AA:ª 0 T manual_sprite(\"glyphs.png\", 121, 77,  7,  18)\nU+00AB:« 0 T manual_sprite(\"glyphs.png\", 128, 77,  9,  18)\nU+00AC:¬ 0 T manual_sprite(\"glyphs.png\", 137, 77,  11, 18)\nU+00AE:® 0 T manual_sprite(\"glyphs.png\", 148, 77,  15, 18)\nU+00B0:° 0 T manual_sprite(\"glyphs.png\", 163, 77,  8,  18)\nU+00B1:± 0 T manual_sprite(\"glyphs.png\", 171, 77,  11, 18)\nU+00B2:² 0 T manual_sprite(\"glyphs.png\", 20,  95,  7,  9)\nU+00B3:³ 0 T manual_sprite(\"glyphs.png\", 13,  104, 7,  9)\nU+00B5:µ 0 T manual_sprite(\"glyphs.png\", 182, 77,  11, 18)\nU+00B6:¶ 0 T manual_sprite(\"glyphs.png\", 193, 77,  15, 18)\nU+00B7:· 0 T manual_sprite(\"glyphs.png\", 208, 77,  6,  18)\nU+00B9:¹ 0 T manual_sprite(\"glyphs.png\", 13,  95,  7,  9)\nU+00BA:º 0 T manual_sprite(\"glyphs.png\", 214, 77,  7,  18)\nU+00BB:» 0 T manual_sprite(\"glyphs.png\", 221, 77,  9,  18)\nU+00BC:¼ 0 T manual_sprite(\"glyphs.png\", 230, 77,  13, 18)\nU+00BD:½ 0 T manual_sprite(\"glyphs.png\", 243, 77,  13, 18)\nU+00BE:¾ 0 T manual_sprite(\"glyphs.png\", 0,   95,  13, 18)\nU+00BF:¿ 0 T manual_sprite(\"glyphs.png\", 27,  95,  12, 18)\nU+00C0:À 0 C combine(\"A\", \"\\\\{grave accent}\")\nU+00C1:Á 0 C combine(\"A\", \"\\\\{acute accent}\")\nU+00C2:Â 0 C combine(\"A\", \"\\\\{circumflex}\")\nU+00C3:Ã 0 C combine(\"A\", \"\\\\{tilde}\")\nU+00C4:Ä 0 C combine(\"A\", \"\\\\{umlaut}\")\nU+00C5:Å 0 C combine(\"A\", \"\\\\{ring above}\")\nU+00C6:Æ 0 T manual_sprite(\"glyphs.png\", 39,  95,  18, 18)\nU+00C7:Ç 0 T manual_sprite(\"glyphs.png\", 57,  95,  13, 18)\nU+00C8:È 0 C combine(\"E\", \"\\\\{grave accent}\")\nU+00C9:É 0 C combine(\"E\", \"\\\\{acute accent}\")\nU+00CA:Ê 0 C combine(\"E\", \"\\\\{circumflex}\")\nU+00CB:Ë 0 C combine(\"E\", \"\\\\{umlaut}\")\nU+00CC:Ì 0 C combine(\"I\", \"\\\\{grave accent}\")\nU+00CD:Í 0 C combine(\"I\", \"\\\\{acute accent}\")\nU+00CE:Î 0 C combine(\"I\", \"\\\\{circumflex}\")\nU+00CF:Ï 0 C combine(\"I\", \"\\\\{umlaut}\")\nU+00D0:Ð 0 T manual_sprite(\"glyphs.png\", 70,  95,  13, 18)\nU+00D1:Ñ 0 C combine(\"N\", \"\\\\{tilde}\")\nU+00D2:Ò 0 C combine(\"O\", \"\\\\{grave accent}\")\nU+00D3:Ó 0 C combine(\"O\", \"\\\\{acute accent}\")\nU+00D4:Ô 0 C combine(\"O\", \"\\\\{circumflex}\")\nU+00D5:Õ 0 C combine(\"O\", \"\\\\{tilde}\")\nU+00D6:Ö 0 C combine(\"O\", \"\\\\{umlaut}\")\nU+00D7:× 0 T manual_sprite(\"glyphs.png\", 83,  95,  10, 18)\nU+00D8:Ø 0 T manual_sprite(\"glyphs.png\", 93,  95,  14, 18)\nU+00D9:Ù 0 C combine(\"U\", \"\\\\{grave accent}\")\nU+00DA:Ú 0 C combine(\"U\", \"\\\\{acute accent}\")\nU+00DB:Û 0 C combine(\"U\", \"\\\\{circumflex}\")\nU+00DC:Ü 0 C combine(\"U\", \"\\\\{umlaut}\")\nU+00DD:Ý 0 C combine(\"Y\", \"\\\\{acute accent}\")\nU+00DE:Þ 0 T manual_sprite(\"glyphs.png\", 107, 95,  13, 18)\nU+00DF:ß 0 T manual_sprite(\"glyphs.png\", 120, 95,  11, 18, index=74)\nU+00E0:à 0 C combine(\"a\", \"\\\\{grave accent}\")\nU+00E1:á 0 C combine(\"a\", \"\\\\{acute accent}\")\nU+00E2:â 0 C combine(\"a\", \"\\\\{circumflex}\")\nU+00E3:ã 0 C combine(\"a\", \"\\\\{tilde}\")\nU+00E4:ä 0 C combine(\"a\", \"\\\\{umlaut}\")\nU+00E5:å 0 C combine(\"a\", \"\\\\{ring above}\")\nU+00E6:æ 0 T manual_sprite(\"glyphs.png\", 131, 95,  16, 18)\nU+00E7:ç 0 T manual_sprite(\"glyphs.png\", 147, 95,  11, 18)\nU+00E8:è 0 C combine(\"e\", \"\\\\{grave accent}\")\nU+00E9:é 0 C combine(\"e\", \"\\\\{acute accent}\")\nU+00EA:ê 0 C combine(\"e\", \"\\\\{circumflex}\")\nU+00EB:ë 0 C combine(\"e\", \"\\\\{umlaut}\")\nU+00EC:ì 0 C combine(U+0131:ı, \"\\\\{grave accent}\")\nU+00ED:í 0 C combine(U+0131:ı, \"\\\\{acute accent}\")\nU+00EE:î 0 C combine(U+0131:ı, \"\\\\{circumflex}\")\nU+00EF:ï 0 C combine(U+0131:ı, \"\\\\{umlaut}\")\nU+00F0:ð 0 T manual_sprite(\"glyphs.png\", 158, 95,  11, 18)\nU+00F1:ñ 0 C combine(\"n\", \"\\\\{tilde}\")\nU+00F2:ò 0 C combine(\"o\", \"\\\\{grave accent}\")\nU+00F3:ó 0 C combine(\"o\", \"\\\\{acute accent}\")\nU+00F4:ô 0 C combine(\"o\", \"\\\\{circumflex}\")\nU+00F5:õ 0 C combine(\"o\", \"\\\\{tilde}\")\nU+00F6:ö 0 C combine(\"o\", \"\\\\{umlaut}\")\nU+00F7:÷ 0 T manual_sprite(\"glyphs.png\", 169, 95,  10, 18)\nU+00F8:ø 0 T manual_sprite(\"glyphs.png\", 179, 95,  13, 18)\nU+00F9:ù 0 C combine(\"u\", \"\\\\{grave accent}\")\nU+00FA:ú 0 C combine(\"u\", \"\\\\{acute accent}\")\nU+00FB:û 0 C combine(\"u\", \"\\\\{circumflex}\")\nU+00FC:ü 0 C combine(\"u\", \"\\\\{umlaut}\")\nU+00FD:ý 0 C combine(\"y\", \"\\\\{acute accent}\")\nU+00FE:þ 0 T manual_sprite(\"glyphs.png\", 192, 95,  11, 18)\nU+00FF:ÿ 0 C combine(\"y\", \"\\\\{umlaut}\")\n"
  },
  {
    "path": "data/common/glyphs/mapping_latin_extended-a.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin Extended-A\" (U+0100 to U+017F)\n# --------------------------------------------------\nU+0100:Ā 0 C combine(\"A\", \"\\\\{macron}\")\nU+0101:ā 0 C combine(\"a\", \"\\\\{macron}\")\nU+0102:Ă 0 C combine(\"A\", \"\\\\{breve}\")\nU+0103:ă 0 C combine(\"a\", \"\\\\{breve}\")\nU+0104:Ą 0 T manual_sprite(\"glyphs.png\", 203, 95,  17, 18)\nU+0105:ą 0 T manual_sprite(\"glyphs.png\", 220, 95,  11, 18)\nU+0106:Ć 0 C combine(\"C\", \"\\\\{acute accent}\")\nU+0107:ć 0 C combine(\"c\", \"\\\\{acute accent}\")\nU+0108:Ĉ 0 C combine(\"C\", \"\\\\{circumflex}\")\nU+0109:ĉ 0 C combine(\"c\", \"\\\\{circumflex}\")\nU+010A:Ċ 0 C combine(\"C\", \"\\\\{dot above}\")\nU+010B:ċ 0 C combine(\"c\", \"\\\\{dot above}\")\nU+010C:Č 0 C combine(\"C\", \"\\\\{caron}\")\nU+010D:č 0 C combine(\"c\", \"\\\\{caron}\")\nU+010E:Ď 0 C combine(\"D\", \"\\\\{caron}\")\nU+010F:ď 0 C combine(\"d\", \"\\\\{caron}\")\nU+0110:Đ 0 T link(U+00D0:Ð)\nU+0111:đ 0 T manual_sprite(\"glyphs.png\", 231, 95,  12, 18)\nU+0112:Ē 0 C combine(\"E\", \"\\\\{macron}\")\nU+0113:ē 0 C combine(\"e\", \"\\\\{macron}\")\nU+0114:Ĕ 0 C combine(\"E\", \"\\\\{breve}\")\nU+0115:ĕ 0 C combine(\"e\", \"\\\\{breve}\")\nU+0116:Ė 0 C combine(\"E\", \"\\\\{dot above}\")\nU+0117:ė 0 C combine(\"e\", \"\\\\{dot above}\")\nU+0118:Ę 0 T manual_sprite(\"glyphs.png\", 243, 95,  13, 18)\nU+0119:ę 0 T manual_sprite(\"glyphs.png\", 0,   113, 11, 18)\nU+011A:Ě 0 C combine(\"E\", \"\\\\{caron}\")\nU+011B:ě 0 C combine(\"e\", \"\\\\{caron}\")\nU+011C:Ĝ 0 C combine(\"G\", \"\\\\{circumflex}\")\nU+011D:ĝ 0 C combine(\"g\", \"\\\\{circumflex}\")\nU+011E:Ğ 0 C combine(\"G\", \"\\\\{breve}\")\nU+011F:ğ 0 C combine(\"g\", \"\\\\{breve}\")\nU+0120:Ġ 0 C combine(\"G\", \"\\\\{dot above}\")\nU+0121:ġ 0 C combine(\"g\", \"\\\\{dot above}\")\nU+0122:Ģ 0 T manual_sprite(\"glyphs.png\", 11,  113, 13, 18)\nU+0123:ģ 0 T manual_sprite(\"glyphs.png\", 24,  113, 11, 18)\nU+0124:Ĥ 0 C combine(\"H\", \"\\\\{circumflex}\")\nU+0125:ĥ 0 C combine(\"h\", \"\\\\{circumflex}\")\nU+0126:Ħ 0 T manual_sprite(\"glyphs.png\", 35,  113, 15, 18)\nU+0127:ħ 0 T manual_sprite(\"glyphs.png\", 50,  113, 12, 18)\nU+0128:Ĩ 0 C combine(\"I\", \"\\\\{tilde}\")\nU+0129:ĩ 0 C combine(\"i\", \"\\\\{tilde}\")\nU+012A:Ī 0 C combine(\"I\", \"\\\\{macron}\")\nU+012B:ī 0 C combine(\"i\", \"\\\\{macron}\")\nU+012C:Ĭ 0 C combine(\"I\", \"\\\\{breve}\")\nU+012D:ĭ 0 C combine(\"i\", \"\\\\{breve}\")\nU+012E:Į 0 T manual_sprite(\"glyphs.png\", 62,  113, 10, 18)\nU+012F:į 0 T manual_sprite(\"glyphs.png\", 72,  113, 7,  18)\nU+0130:İ 0 C combine(\"I\", \"\\\\{dot above}\")\nU+0131:ı 0 T manual_sprite(\"glyphs.png\", 79,  113, 7,  18)\nU+0134:Ĵ 0 C combine(\"J\", \"\\\\{circumflex}\")\nU+0135:ĵ 0 C combine(\"j\", \"\\\\{circumflex}\")\nU+0136:Ķ 0 T manual_sprite(\"glyphs.png\", 86,  113, 14, 18)\nU+0137:ķ 0 T manual_sprite(\"glyphs.png\", 100, 113, 12, 18)\nU+0138:ĸ 0 T manual_sprite(\"glyphs.png\", 112, 113, 11, 18)\nU+0139:Ĺ 0 C combine(\"L\", \"\\\\{acute accent}\")\nU+013A:ĺ 0 C combine(\"l\", \"\\\\{acute accent}\")\nU+013B:Ļ 0 T manual_sprite(\"glyphs.png\", 123, 113, 13, 18)\nU+013C:ļ 0 T manual_sprite(\"glyphs.png\", 136, 113, 7,  18)\nU+013D:Ľ 0 C combine(\"L\", \"\\\\{caron}\")\nU+013E:ľ 0 C combine(\"l\", \"\\\\{caron}\")\nU+013F:Ŀ 0 C combine(\"L\", U+00B7:·, align=\"middle\", offset_x=3, offset_y=-1)\nU+0140:ŀ 0 C combine(\"l\", U+00B7:·, align=\"middle\", offset_x=3, offset_y=-1)\nU+0141:Ł 0 T manual_sprite(\"glyphs.png\", 143, 113, 14, 18)\nU+0142:ł 0 T manual_sprite(\"glyphs.png\", 157, 113, 10, 18)\nU+0143:Ń 0 C combine(\"N\", \"\\\\{acute accent}\")\nU+0144:ń 0 C combine(\"n\", \"\\\\{acute accent}\")\nU+0145:Ņ 0 T manual_sprite(\"glyphs.png\", 167, 113, 15, 18)\nU+0146:ņ 0 T manual_sprite(\"glyphs.png\", 182, 113, 12, 18)\nU+0147:Ň 0 C combine(\"N\", \"\\\\{caron}\")\nU+0148:ň 0 C combine(\"n\", \"\\\\{caron}\")\nU+0149:ŉ 0 C combine(\"n\", \"\\\\{acute accent}\", offset_x=-2)\nU+014A:Ŋ 0 T manual_sprite(\"glyphs.png\", 194, 113, 15, 18)\nU+014B:ŋ 0 T manual_sprite(\"glyphs.png\", 209, 113, 12, 18)\nU+014C:Ō 0 C combine(\"O\", \"\\\\{macron}\")\nU+014D:ō 0 C combine(\"o\", \"\\\\{macron}\")\nU+014E:Ŏ 0 C combine(\"O\", \"\\\\{breve}\")\nU+014F:ŏ 0 C combine(\"o\", \"\\\\{breve}\")\nU+0150:Ő 0 C combine(\"O\", \"\\\\{double acute accent}\")\nU+0151:ő 0 C combine(\"o\", \"\\\\{double acute accent}\")\nU+0152:Œ 0 T manual_sprite(\"glyphs.png\", 221, 113, 18, 18)\nU+0153:œ 0 T manual_sprite(\"glyphs.png\", 239, 113, 16, 18)\nU+0154:Ŕ 0 C combine(\"R\", \"\\\\{acute accent}\")\nU+0155:ŕ 0 C combine(\"r\", \"\\\\{acute accent}\")\nU+0156:Ŗ 0 T manual_sprite(\"glyphs.png\", 0,   131, 14, 18)\nU+0157:ŗ 0 T manual_sprite(\"glyphs.png\", 14,  131, 10, 18)\nU+0158:Ř 0 C combine(\"R\", \"\\\\{caron}\")\nU+0159:ř 0 C combine(\"r\", \"\\\\{caron}\")\nU+015A:Ś 0 C combine(\"S\", \"\\\\{acute accent}\")\nU+015B:ś 0 C combine(\"s\", \"\\\\{acute accent}\")\nU+015C:Ŝ 0 C combine(\"S\", \"\\\\{circumflex}\")\nU+015D:ŝ 0 C combine(\"s\", \"\\\\{circumflex}\")\nU+015E:Ş 0 T manual_sprite(\"glyphs.png\", 24,  131, 13, 18)\nU+015F:ş 0 T manual_sprite(\"glyphs.png\", 37,  131, 11, 18)\nU+0160:Š 0 C combine(\"S\", \"\\\\{caron}\")\nU+0161:š 0 C combine(\"s\", \"\\\\{caron}\")\nU+0162:Ţ 0 T manual_sprite(\"glyphs.png\", 48,  131, 14, 18)\nU+0163:ţ 0 T manual_sprite(\"glyphs.png\", 62,  131, 10, 18)\nU+0164:Ť 0 C combine(\"T\", \"\\\\{caron}\")\nU+0165:ť 0 C combine(\"t\", \"\\\\{caron}\")\nU+0166:Ŧ 0 T manual_sprite(\"glyphs.png\", 72,  131, 14, 18)\nU+0167:ŧ 0 T manual_sprite(\"glyphs.png\", 86,  131, 11, 18)\nU+0168:Ũ 0 C combine(\"U\", \"\\\\{tilde}\")\nU+0169:ũ 0 C combine(\"u\", \"\\\\{tilde}\")\nU+016A:Ū 0 C combine(\"U\", \"\\\\{macron}\")\nU+016B:ū 0 C combine(\"u\", \"\\\\{macron}\")\nU+016C:Ŭ 0 C combine(\"U\", \"\\\\{breve}\")\nU+016D:ŭ 0 C combine(\"u\", \"\\\\{breve}\")\nU+016E:Ů 0 C combine(\"U\", \"\\\\{ring above}\")\nU+016F:ů 0 C combine(\"u\", \"\\\\{ring above}\")\nU+0170:Ű 0 C combine(\"U\", \"\\\\{double acute accent}\")\nU+0171:ű 0 C combine(\"u\", \"\\\\{double acute accent}\")\nU+0172:Ų 0 T manual_sprite(\"glyphs.png\", 97,  131, 15, 18)\nU+0173:ų 0 T manual_sprite(\"glyphs.png\", 112, 131, 11, 18)\nU+0174:Ŵ 0 C combine(\"W\", \"\\\\{circumflex}\")\nU+0175:ŵ 0 C combine(\"w\", \"\\\\{circumflex}\")\nU+0176:Ŷ 0 C combine(\"Y\", \"\\\\{circumflex}\")\nU+0177:ŷ 0 C combine(\"y\", \"\\\\{circumflex}\")\nU+0178:Ÿ 0 C combine(\"Y\", \"\\\\{umlaut}\")\nU+0179:Ź 0 C combine(\"Z\", \"\\\\{acute accent}\")\nU+017A:ź 0 C combine(\"z\", \"\\\\{acute accent}\")\nU+017B:Ż 0 C combine(\"Z\", \"\\\\{dot above}\")\nU+017C:ż 0 C combine(\"z\", \"\\\\{dot above}\")\nU+017D:Ž 0 C combine(\"Z\", \"\\\\{caron}\")\nU+017E:ž 0 C combine(\"z\", \"\\\\{caron}\")\n"
  },
  {
    "path": "data/common/glyphs/mapping_latin_extended-b.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin Extended-B\" (U+0180 to U+024F)\n# --------------------------------------------------\nU+0192:ƒ 0 T manual_sprite(\"glyphs.png\", 123, 131, 11, 18)\nU+01CD:Ǎ 0 C combine(\"A\", \"\\\\{caron}\")\nU+01CE:ǎ 0 C combine(\"a\", \"\\\\{caron}\")\nU+01CF:Ǐ 0 C combine(\"I\", \"\\\\{caron}\")\nU+01D0:ǐ 0 C combine(\"i\", \"\\\\{caron}\")\nU+01D1:Ǒ 0 C combine(\"O\", \"\\\\{caron}\")\nU+01D2:ǒ 0 C combine(\"o\", \"\\\\{caron}\")\nU+01D3:Ǔ 0 C combine(\"U\", \"\\\\{caron}\")\nU+01D4:ǔ 0 C combine(\"u\", \"\\\\{caron}\")\nU+01E6:Ǧ 0 C combine(\"G\", \"\\\\{caron}\")\nU+01E7:ǧ 0 C combine(\"g\", \"\\\\{caron}\")\nU+01E8:Ǩ 0 C combine(\"K\", \"\\\\{caron}\")\nU+01E9:ǩ 0 C combine(\"k\", \"\\\\{caron}\")\nU+01F0:ǰ 0 C combine(\"j\", \"\\\\{caron}\")\nU+01F4:Ǵ 0 C combine(\"G\", \"\\\\{acute accent}\")\nU+01F5:ǵ 0 C combine(\"g\", \"\\\\{acute accent}\")\nU+01F8:Ǹ 0 C combine(\"N\", \"\\\\{grave accent}\")\nU+01F9:ǹ 0 C combine(\"n\", \"\\\\{grave accent}\")\nU+021E:Ȟ 0 C combine(\"H\", \"\\\\{caron}\")\nU+021F:ȟ 0 C combine(\"h\", \"\\\\{caron}\")\nU+0226:Ȧ 0 C combine(\"A\", \"\\\\{dot above}\")\nU+0227:ȧ 0 C combine(\"a\", \"\\\\{dot above}\")\nU+022E:Ȯ 0 C combine(\"O\", \"\\\\{dot above}\")\nU+022F:ȯ 0 C combine(\"o\", \"\\\\{dot above}\")\nU+0232:Ȳ 0 C combine(\"Y\", \"\\\\{macron}\")\nU+0233:ȳ 0 C combine(\"y\", \"\\\\{macron}\")\nU+0218:Ș 0 T manual_sprite(\"glyphs.png\", 134, 131, 13, 18)\nU+0219:ș 0 T manual_sprite(\"glyphs.png\", 147, 131, 11, 18)\nU+021A:Ț 0 T manual_sprite(\"glyphs.png\", 158, 131, 14, 18)\nU+021B:ț 0 T manual_sprite(\"glyphs.png\", 172, 131, 11, 18)\n"
  },
  {
    "path": "data/common/glyphs/mapping_latin_extended_additional.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin Extended Additional\" (U+1E00 to U+1EFF)\n# --------------------------------------------------\nU+1E02:Ḃ 0 C combine(\"B\", \"\\\\{dot above}\")\nU+1E03:ḃ 0 C combine(\"b\", \"\\\\{dot above}\")\nU+1E0A:Ḋ 0 C combine(\"D\", \"\\\\{dot above}\")\nU+1E0B:ḋ 0 C combine(\"d\", \"\\\\{dot above}\")\nU+1E1E:Ḟ 0 C combine(\"F\", \"\\\\{dot above}\")\nU+1E1F:ḟ 0 C combine(\"f\", \"\\\\{dot above}\")\nU+1E20:Ḡ 0 C combine(\"G\", \"\\\\{macron}\")\nU+1E21:ḡ 0 C combine(\"g\", \"\\\\{macron}\")\nU+1E22:Ḣ 0 C combine(\"H\", \"\\\\{dot above}\")\nU+1E23:ḣ 0 C combine(\"h\", \"\\\\{dot above}\")\nU+1E26:Ḧ 0 C combine(\"H\", \"\\\\{umlaut}\")\nU+1E27:ḧ 0 C combine(\"h\", \"\\\\{umlaut}\")\nU+1E30:Ḱ 0 C combine(\"K\", \"\\\\{acute accent}\")\nU+1E31:ḱ 0 C combine(\"k\", \"\\\\{acute accent}\")\nU+1E3E:Ḿ 0 C combine(\"M\", \"\\\\{acute accent}\")\nU+1E3F:ḿ 0 C combine(\"m\", \"\\\\{acute accent}\")\nU+1E40:Ṁ 0 C combine(\"M\", \"\\\\{dot above}\")\nU+1E41:ṁ 0 C combine(\"m\", \"\\\\{dot above}\")\nU+1E44:Ṅ 0 C combine(\"N\", \"\\\\{dot above}\")\nU+1E45:ṅ 0 C combine(\"n\", \"\\\\{dot above}\")\nU+1E54:Ṕ 0 C combine(\"P\", \"\\\\{acute accent}\")\nU+1E55:ṕ 0 C combine(\"p\", \"\\\\{acute accent}\")\nU+1E56:Ṗ 0 C combine(\"P\", \"\\\\{dot above}\")\nU+1E57:ṗ 0 C combine(\"p\", \"\\\\{dot above}\")\nU+1E58:Ṙ 0 C combine(\"R\", \"\\\\{dot above}\")\nU+1E59:ṙ 0 C combine(\"r\", \"\\\\{dot above}\")\nU+1E60:Ṡ 0 C combine(\"S\", \"\\\\{dot above}\")\nU+1E61:ṡ 0 C combine(\"s\", \"\\\\{dot above}\")\nU+1E6A:Ṫ 0 C combine(\"T\", \"\\\\{dot above}\")\nU+1E6B:ṫ 0 C combine(\"t\", \"\\\\{dot above}\")\nU+1E7C:Ṽ 0 C combine(\"V\", \"\\\\{tilde}\")\nU+1E7D:ṽ 0 C combine(\"v\", \"\\\\{tilde}\")\nU+1E80:Ẁ 0 C combine(\"W\", \"\\\\{grave accent}\")\nU+1E81:ẁ 0 C combine(\"w\", \"\\\\{grave accent}\")\nU+1E82:Ẃ 0 C combine(\"W\", \"\\\\{acute accent}\")\nU+1E83:ẃ 0 C combine(\"w\", \"\\\\{acute accent}\")\nU+1E84:Ẅ 0 C combine(\"W\", \"\\\\{umlaut}\")\nU+1E85:ẅ 0 C combine(\"w\", \"\\\\{umlaut}\")\nU+1E86:Ẇ 0 C combine(\"W\", \"\\\\{dot above}\")\nU+1E87:ẇ 0 C combine(\"w\", \"\\\\{dot above}\")\nU+1E8A:Ẋ 0 C combine(\"X\", \"\\\\{dot above}\")\nU+1E8B:ẋ 0 C combine(\"x\", \"\\\\{dot above}\")\nU+1E8C:Ẍ 0 C combine(\"X\", \"\\\\{umlaut}\")\nU+1E8D:ẍ 0 C combine(\"x\", \"\\\\{umlaut}\")\nU+1E8E:Ẏ 0 C combine(\"Y\", \"\\\\{dot above}\")\nU+1E8F:ẏ 0 C combine(\"y\", \"\\\\{dot above}\")\nU+1E90:Ẑ 0 C combine(\"Z\", \"\\\\{circumflex}\")\nU+1E91:ẑ 0 C combine(\"z\", \"\\\\{circumflex}\")\nU+1E97:ẗ 0 C combine(\"t\", \"\\\\{umlaut}\")\nU+1E98:ẘ 0 C combine(\"w\", \"\\\\{ring above}\")\nU+1E99:ẙ 0 C combine(\"y\", \"\\\\{ring above}\")\nU+1EBC:Ẽ 0 C combine(\"E\", \"\\\\{tilde}\")\nU+1EBD:ẽ 0 C combine(\"e\", \"\\\\{tilde}\")\nU+1EF2:Ỳ 0 C combine(\"Y\", \"\\\\{grave accent}\")\nU+1EF3:ỳ 0 C combine(\"y\", \"\\\\{grave accent}\")\nU+1EF8:Ỹ 0 C combine(\"Y\", \"\\\\{tilde}\")\nU+1EF9:ỹ 0 C combine(\"y\", \"\\\\{tilde}\")\n"
  },
  {
    "path": "data/common/glyphs/mapping_misc.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"General Punctuation\" (U+2000 to U+206F)\n# --------------------------------------------------\nU+2013:– 0 T manual_sprite(\"glyphs.png\", 35, 221, 11, 18)\nU+2014:— 0 T manual_sprite(\"glyphs.png\", 46, 221, 18, 18)\nU+2018:‘ 0 T manual_sprite(\"glyphs.png\", 64, 221, 6, 9)\nU+2019:’ 0 T manual_sprite(\"glyphs.png\", 64, 230, 6, 9)\nU+201C:“ 0 T manual_sprite(\"glyphs.png\", 70, 221, 9, 9)\nU+201D:” 0 T manual_sprite(\"glyphs.png\", 70, 230, 9, 9)\nU+2020:† 0 T manual_sprite(\"glyphs.png\", 80, 221, 10, 18)\nU+2021:‡ 0 T manual_sprite(\"glyphs.png\", 90, 221, 10, 18)\nU+2022:• 0 T manual_sprite(\"glyphs.png\", 100, 221, 8, 18)\nU+2026:… 0 T manual_sprite(\"glyphs.png\", 108, 221, 12, 18)\nU+2030:‰ 0 T manual_sprite(\"glyphs.png\", 120, 221, 16, 18)\nU+2039:‹ 0 T manual_sprite(\"glyphs.png\", 137, 221, 7, 18)\nU+203A:› 0 T manual_sprite(\"glyphs.png\", 144, 221, 7, 18)\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Superscripts and Subscripts\" (U+2070 to U+209F)\n# --------------------------------------------------\nU+2074:⁴ 0 T manual_sprite(\"glyphs.png\", 20,  104, 7,  9)\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Currency Symbols\" (U+20A0 to U+20CF)\n# --------------------------------------------------\nU+20AC:€ 0 T manual_sprite(\"glyphs.png\", 170, 221, 14, 18)\nU+20AF:₯ 0 T manual_sprite(\"glyphs.png\", 151, 221, 19, 18)\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Letterlike Symbols\" (U+2100 to U+214F)\n# --------------------------------------------------\nU+2116:№ 0 T manual_sprite(\"glyphs.png\", 184, 221, 18, 18)\nU+2122:™ 0 T manual_sprite(\"glyphs.png\", 203, 221, 18, 18)\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Alphabetic Presentation Forms\" (U+FB00 to U+FB4F)\n# --------------------------------------------------\nU+FB01:ﬁ 0 T manual_sprite(\"glyphs.png\", 221, 221, 13, 18)\nU+FB02:ﬂ 0 T manual_sprite(\"glyphs.png\", 234, 221, 13, 18)\n"
  },
  {
    "path": "data/common/glyphs/mapping_small.txt",
    "content": "# --------------------------------------------------\n# Small text\n# --------------------------------------------------\nU+0030:0 1 T grid_sprite(\"buttons.png\", 0, 0)\nU+0031:1 1 T grid_sprite(\"buttons.png\", 1, 0)\nU+0032:2 1 T grid_sprite(\"buttons.png\", 2, 0)\nU+0033:3 1 T grid_sprite(\"buttons.png\", 3, 0)\nU+0034:4 1 T grid_sprite(\"buttons.png\", 4, 0)\nU+0035:5 1 T grid_sprite(\"buttons.png\", 5, 0)\nU+0036:6 1 T grid_sprite(\"buttons.png\", 6, 0)\nU+0037:7 1 T grid_sprite(\"buttons.png\", 7, 0)\nU+0038:8 1 T grid_sprite(\"buttons.png\", 8, 0)\nU+0039:9 1 T grid_sprite(\"buttons.png\", 9, 0)\nU+002D:- 1 T grid_sprite(\"buttons.png\", 4, 1)\nU+002C:, 1 T grid_sprite(\"buttons.png\", 5, 1)\nU+00B0:° 1 T grid_sprite(\"buttons.png\", 6, 1)\n"
  },
  {
    "path": "data/scripting/assault_stats.lua",
    "content": "local raw = trxc.assault_stats\n\ntrx.assault_stats = {\n  add_record = raw.record,\n  remove_record = raw.remove,\n  list_records = raw.list,\n}\n"
  },
  {
    "path": "data/scripting/camera.lua",
    "content": "local raw = trxc.camera\n\nlocal getters = {\n  pos = raw.get_pos,\n  room_num = raw.get_room,\n  room = function()\n    local room_num = raw.get_room()\n    return room_num and trx.rooms[room_num] or nil\n  end,\n  target_pos = raw.get_target_pos,\n  target_room_num = raw.get_target_room,\n}\n\nlocal camera = {\n  shake = raw.shake,\n  reset = raw.reset,\n}\n\nsetmetatable(camera, {\n  __index = function(self, key)\n    local getter = getters[key]\n    return getter and getter() or nil\n  end,\n  __newindex = function(self, key, value)\n    error(\"Cannot set field '\" .. key .. \"' on trx.camera\", 2)\n  end,\n})\n\ntrx.camera = camera\n"
  },
  {
    "path": "data/scripting/catalog.lua",
    "content": "local raw = trxc.catalog\n\nlocal catalog = {\n  objects = raw.objects,\n  flip_effects = raw.flip_effects,\n  lara_states = raw.lara_states,\n  lara_anims = raw.lara_anims,\n  music = raw.music,\n  samples = raw.samples,\n  weapons = raw.weapons,\n}\n\ntrx.catalog = catalog\n"
  },
  {
    "path": "data/scripting/config.lua",
    "content": "local raw = trxc.config\n\nlocal config = {}\n\nfunction config.get(key)\n  return raw.get(key)\nend\n\nfunction config.set(key, value)\n  return raw.set(key, value)\nend\n\nfunction config.list()\n  return raw.list()\nend\n\ntrx.config = config\n"
  },
  {
    "path": "data/scripting/console.lua",
    "content": "local raw = trxc.console\nlocal LogLevel = trxc.log.LogLevel\n\nlocal log = { LogLevel = LogLevel }\nfunction log.generic(level, ...)\n  raw.log(level, ...)\nend\nfunction log.info(...)\n  raw.log(LogLevel.INFO, ...)\nend\nfunction log.warn(...)\n  raw.log(LogLevel.WARNING, ...)\nend\nfunction log.warning(...)\n  raw.log(LogLevel.WARNING, ...)\nend\nfunction log.error(...)\n  raw.log(LogLevel.ERROR, ...)\nend\nfunction log.debug(...)\n  raw.log(LogLevel.DEBUG, ...)\nend\n\nlocal console = {\n  log = log,\n}\nsetmetatable(console.log, {\n  __call = function(_, ...)\n    return console.log.info(...)\n  end,\n})\n\nfunction console.clear()\n  return raw.clear()\nend\n\nfunction console.eval(cmd, opts)\n  return raw.eval(cmd, opts)\nend\n\ntrx.console = console\n"
  },
  {
    "path": "data/scripting/creatures.lua",
    "content": "local raw = trxc.creatures\n\nlocal creatures = {\n  add_ally = raw.add_ally,\n  add_ally_target = raw.add_ally_target,\n}\n\nlocal getters = {\n  hostile_allies = raw.are_allies_hostile,\n}\n\nlocal setters = {\n  hostile_allies = raw.set_allies_hostile,\n}\n\nlocal creatures_mt = {\n  __index = function(self, key)\n    local getter = getters[key]\n    return getter and getter() or nil\n  end,\n  __newindex = function(self, key, value)\n    local setter = setters[key]\n    if setter then\n      setter(value)\n      return\n    end\n    error(\"Cannot set field '\" .. key .. \"' on trx.creatures\", 2)\n  end,\n}\n\nsetmetatable(creatures, creatures_mt)\ntrx.creatures = creatures\n"
  },
  {
    "path": "data/scripting/events.lua",
    "content": "local raw = trxc.events\nlocal types = trxc.events.EventType\n\nlocal Event = {}\nfunction Event.__call(self, callback)\n  local et = self._type\n  return raw.attach(et, callback)\nend\n\nlocal function to_event_name(name)\n  if string.sub(name, 1, 7) == \"BEFORE_\" then\n    return string.lower(name)\n  elseif string.sub(name, 1, 6) == \"AFTER_\" then\n    return string.lower(name)\n  end\n  return \"on_\" .. string.lower(name)\nend\n\nlocal events = { EventType = types }\nfor name, et in pairs(types) do\n  local proxy = { _type = et }\n  setmetatable(proxy, Event)\n  events[to_event_name(name)] = proxy\nend\n\nfunction events.detach(id)\n  raw.detach(id)\nend\n\ntrx.events = events\n"
  },
  {
    "path": "data/scripting/game.lua",
    "content": "local raw = trxc.game\n\nlocal function make_level(table_type, i)\n  return {\n    num = raw.get_level_num(table_type, i),\n    name = raw.get_level_name(table_type, i),\n    path = raw.get_level_path(table_type, i),\n    type = raw.get_level_type(table_type, i),\n  }\nend\n\nlocal function make_levels(table_type)\n  local count = raw.count_levels(table_type)\n  local levels = {}\n  for i = 1, count do\n    levels[i] = make_level(table_type, i)\n  end\n  return levels\nend\n\nlocal table_map = {\n  levels = raw.LevelTable.MAIN,\n  demos = raw.LevelTable.DEMOS,\n  cutscenes = raw.LevelTable.CUTSCENES,\n}\n\n-- settings system\nlocal Settings = (function()\n  local function config_entry(path)\n    return {\n      get = function()\n        return trx.config.get(path)\n      end,\n      set = function(value)\n        trx.config.set(path, value)\n      end,\n    }\n  end\n\n  local registry = {\n    lockout_option_ring = config_entry(\"flow.lockout_option_ring\"),\n    load_save_disabled = config_entry(\"flow.load_save_disabled\"),\n    play_any_level = config_entry(\"flow.play_any_level\"),\n    demo_delay = config_entry(\"flow.demo_delay\"),\n    cheat_keys = config_entry(\"flow.cheat_keys\"),\n  }\n\n  return setmetatable({}, {\n    __index = function(_, key)\n      local r = registry[key]\n      return r and r.get() or nil\n    end,\n    __newindex = function(_, key, value)\n      local r = registry[key]\n      if not r then\n        error(\"Cannot set field '\" .. key .. \"' on Settings\")\n      end\n      r.set(value)\n    end,\n  })\nend)()\n\nlocal dynamic_getters = {\n  current_level = function()\n    return make_level(raw.get_current_level_table(), raw.get_current_level_idx())\n  end,\n  version = raw.get_version,\n  trx_version = raw.get_trx_version,\n}\n\ntrx.game = setmetatable({\n  LevelTable = raw.LevelTable,\n  LevelType = raw.LevelType,\n  settings = Settings,\n  play_level = raw.play_level,\n  play_cutscene = raw.play_cutscene,\n  play_demo = raw.play_demo,\n}, {\n  __index = function(self, key)\n    local table_type = table_map[key]\n    if table_type then\n      local t = make_levels(table_type)\n      rawset(self, key, t)\n      return t\n    end\n\n    local getter = dynamic_getters[key]\n    return getter and getter() or nil\n  end,\n  __newindex = function(self, key, value)\n    error(\"Cannot set field '\" .. key .. \"' on trx.game\")\n  end,\n})\n"
  },
  {
    "path": "data/scripting/items.lua",
    "content": "local raw = trxc.items\n\n-- Item proxy metatable\nlocal getters = {\n  pos = raw.get_pos,\n  rot = raw.get_rot,\n  anim = raw.get_anim,\n  frame = raw.get_frame,\n  room_num = raw.get_room,\n  room = function(idx)\n    return trx.rooms[raw.get_room(idx)]\n  end,\n  status = raw.get_status,\n  flags = raw.get_flags,\n  timer = raw.get_timer,\n  object_id = raw.get_object_id,\n  hit_points = raw.get_hit_points,\n  max_hit_points = raw.get_max_hit_points,\n  name = raw.get_name,\n}\n\nlocal setters = {\n  pos = raw.set_pos,\n  rot = raw.set_rot,\n  anim = raw.set_anim,\n  frame = raw.set_frame,\n  hit_points = raw.set_hit_points,\n  max_hit_points = raw.set_max_hit_points,\n  name = raw.set_name,\n  object_id = raw.set_object_id,\n}\n\nlocal Item = {}\n\nItem.__index = function(self, key)\n  local getter = getters[key]\n  return getter and getter(self.idx) or nil\nend\n\nItem.__newindex = function(self, key, value)\n  local setter = setters[key]\n  if setter then\n    setter(self.idx, value)\n    return\n  end\n  error(\"Cannot set field '\" .. key .. \"' on trx.items.Item\")\nend\n\n-- items metatable - functions\nlocal fn = {}\n\nfunction fn.get(arg)\n  local idx = raw.get(arg)\n  if not idx then\n    return nil\n  end\n  return setmetatable({ idx = idx }, Item)\nend\n\nlocal find_query_keys = {\n  object_id = true,\n  room_num = true,\n}\n\nlocal function validate_find_query(query, fn_name)\n  if type(query) ~= \"table\" then\n    error(\"trx.items.\" .. fn_name .. \" query must be a table\", 2)\n  end\n\n  for key, _ in pairs(query) do\n    if not find_query_keys[key] then\n      trx.log.warn(\"trx.items.\" .. fn_name .. \": unknown property '\" .. tostring(key) .. \"'\")\n    end\n  end\nend\n\nlocal function is_matching(item, query)\n  if query.object_id ~= nil and item.object_id ~= query.object_id then\n    return false\n  end\n  if query.room_num ~= nil and item.room_num ~= query.room_num then\n    return false\n  end\n  return true\nend\n\nlocal function find_items(query, first_only)\n  local matches = first_only and nil or {}\n  local count = raw.count()\n  for i = 1, count do\n    local item = setmetatable({ idx = i }, Item)\n    if is_matching(item, query) then\n      if first_only then\n        return item\n      end\n      table.insert(matches, item)\n    end\n  end\n  return first_only and nil or matches\nend\n\nfunction fn.find(query)\n  if query == nil then\n    return {}\n  end\n  validate_find_query(query, \"find\")\n  return find_items(query, false)\nend\n\nfunction fn.first(query)\n  if query == nil then\n    return nil\n  end\n  validate_find_query(query, \"first\")\n  return find_items(query, true)\nend\n\n-- items metatable - metamethods\ntrx.items = setmetatable({}, {\n  Item = Item,\n  __len = function()\n    return raw.count()\n  end,\n  __index = function(_, key)\n    if key == \"fn\" then\n      return fn\n    elseif key == \"find\" then\n      return fn.find\n    elseif key == \"first\" then\n      return fn.first\n    elseif type(key) == \"number\" or type(key) == \"string\" then\n      return fn.get(key)\n    end\n    return nil\n  end,\n})\n"
  },
  {
    "path": "data/scripting/lara.lua",
    "content": "local raw = trxc.lara\n\nlocal lara = {\n  set_extra_equipment = raw.set_extra_equipment,\n  clear_equipment = raw.clear_equipment,\n  mesh = raw.mesh,\n  extra_mesh = raw.extra_mesh,\n}\n\n-- Item proxy metatable\nlocal getters = {\n  exposure_bar = raw.get_exposure_bar,\n  air_bar = raw.get_air_bar,\n  outfit = raw.get_outfit,\n  holsters_visible = raw.are_holsters_visible,\n  has_pistol_weapon = raw.has_pistol_weapon,\n  equipped_gun = raw.get_equipped_gun,\n  extra_anim = raw.get_extra_anim,\n  item = function()\n    return trx.items[raw.get_item()]\n  end,\n  target = function()\n    local target = raw.get_target()\n    if target == nil then\n      return nil\n    end\n    return trx.items[target]\n  end,\n}\n\nlocal setters = {\n  exposure_bar = raw.set_exposure_bar,\n  air_bar = raw.set_air_bar,\n  outfit = raw.set_outfit,\n  holsters_visible = raw.set_holsters_visible,\n}\n\nlocal lara_mt = {\n  __index = function(self, key)\n    local getter = getters[key]\n    return getter and getter() or nil\n  end,\n  __newindex = function(self, key, value)\n    local setter = setters[key]\n    if setter then\n      setter(value)\n      return\n    end\n    error(\"Cannot set field '\" .. key .. \"' on trx.lara\", 2)\n  end,\n}\n\nsetmetatable(lara, lara_mt)\ntrx.lara = lara\n"
  },
  {
    "path": "data/scripting/log.lua",
    "content": "local raw = trxc.log\nlocal LogLevel = trxc.log.LogLevel\n\nlocal log = { LogLevel = LogLevel }\nfunction log.generic(level, ...)\n  raw.log(level, ...)\nend\nfunction log.info(...)\n  raw.log(LogLevel.INFO, ...)\nend\nfunction log.warn(...)\n  raw.log(LogLevel.WARNING, ...)\nend\nfunction log.warning(...)\n  raw.log(LogLevel.WARNING, ...)\nend\nfunction log.error(...)\n  raw.log(LogLevel.ERROR, ...)\nend\nfunction log.debug(...)\n  raw.log(LogLevel.DEBUG, ...)\nend\n\ntrx.log = log\n"
  },
  {
    "path": "data/scripting/music.lua",
    "content": "local raw = trxc.music\n\nlocal music = {\n  PlayMode = raw.PlayMode,\n}\n\nfunction music.get_track()\n  return raw.get_track()\nend\n\nfunction music.play(id, opts)\n  opts = opts or {}\n  mode = opts.mode or trx.music.PlayMode.ONCE\n  raw.play(id, mode)\nend\n\nfunction music.pause()\n  raw.pause()\nend\n\nfunction music.unpause()\n  raw.unpause()\nend\n\nfunction music.stop()\n  raw.stop()\nend\n\nmusic.play_track = music.play\n\ntrx.music = music\n"
  },
  {
    "path": "data/scripting/objects.lua",
    "content": "local raw = trxc.objects\n\nlocal objects = {\n  swap_mesh = raw.swap_mesh,\n}\n\ntrx.objects = objects\n"
  },
  {
    "path": "data/scripting/rooms.lua",
    "content": "local raw = trxc.rooms\n\n-- Room proxy metatable\nlocal getters = {\n  num = function(idx)\n    return idx\n  end,\n  underwater = raw.get_underwater,\n  wind = raw.get_wind,\n  flip_status = raw.get_flip_status,\n  flipped_room = function(self, key)\n    local flipped_room = raw.get_flipped_room(self)\n    if flipped_room == nil then\n      return nil\n    end\n    return trx.rooms[flipped_room]\n  end,\n  bounds = raw.get_bounds,\n  internal_bounds = function(self, key)\n    local bounds = raw.get_bounds(self)\n    return {\n      min_x = bounds.min_x + 1024,\n      min_y = bounds.min_y,\n      min_z = bounds.min_z + 1024,\n      max_x = bounds.max_x - 1024,\n      max_y = bounds.max_y,\n      max_z = bounds.max_z - 1024,\n    }\n  end,\n}\n\nlocal setters = {\n  underwater = raw.set_underwater,\n  wind = raw.set_wind,\n}\n\nlocal Room = {}\n\nRoom.__index = function(self, key)\n  local getter = getters[key]\n  return getter and getter(self.idx) or nil\nend\nRoom.__newindex = function(self, key, value)\n  local setter = setters[key]\n  if setter then\n    setter(self.idx, value)\n    return\n  end\n  error(\"Cannot set field '\" .. key .. \"' on trx.items.Room\")\nend\n\n-- rooms metatable - functions\nlocal fn = {\n  FlipStatus = raw.FlipStatus,\n  Room = Room,\n  flip = raw.flip,\n  flip_effect = raw.flip_effect,\n}\n\nfunction fn.get(arg)\n  local idx = raw.get(arg)\n  if not idx then\n    return nil\n  end\n  return setmetatable({ idx = idx }, Room)\nend\n\ntrx.rooms = setmetatable({}, {\n  __len = function()\n    return raw.count()\n  end,\n  __index = function(_, key)\n    if key == \"fn\" then\n      return fn\n    elseif key == \"flip\" or key == \"flip_effect\" then\n      return fn[key]\n    elseif type(key) == \"number\" or type(key) == \"string\" then\n      return fn.get(key)\n    end\n    return nil\n  end,\n})\n"
  },
  {
    "path": "data/scripting/sound.lua",
    "content": "local raw = trxc.sound\n\nlocal sound = {}\n\nfunction sound.is_available(id)\n  return raw.is_available(id)\nend\n\nfunction sound.play(id, opts)\n  raw.play(id, opts)\nend\n\nfunction sound.stop(id)\n  raw.stop(id)\nend\n\nfunction sound.stop_all()\n  raw.stop_all()\nend\n\ntrx.sound = sound\n"
  },
  {
    "path": "data/tomb-11.bdf",
    "content": "STARTFONT 2.1\nFONT -nerdypepper-tomb-medium-r-normal--11-80-100-100-C-50-ISO10646-1\nSIZE 11 75 75\nFONTBOUNDINGBOX 7 10 0 -2\nSTARTPROPERTIES 34\nFOUNDRY \"nerdypepper\"\nFAMILY_NAME \"tomb\"\nWEIGHT_NAME \"medium\"\nSLANT \"r\"\nSETWIDTH_NAME \"normal\"\nADD_STYLE_NAME \"\"\nPIXEL_SIZE 11\nPOINT_SIZE 80\nRESOLUTION_X 100\nRESOLUTION_Y 100\nSPACING \"C\"\nAVERAGE_WIDTH 50\nCHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\nFONTNAME_REGISTRY \"\"\nCHARSET_COLLECTIONS \"ISO8859-2 ISO8859-9 ISO8859-4 ISO10646-1\"\nFONT_NAME \"tomb\"\nFACE_NAME \"tomb\"\nCOPYRIGHT \"Copyright (c) 2025 LostArtefacts\"\nFONT_VERSION \"1.0.0\"\nFONT_ASCENT 9\nFONT_DESCENT 2\nUNDERLINE_POSITION -1\nUNDERLINE_THICKNESS 1\nX_HEIGHT 4\nCAP_HEIGHT 4\nRAW_ASCENT 818\nRAW_DESCENT 181\nNORM_SPACE 5\nRELATIVE_WEIGHT 50\nRELATIVE_SETWIDTH 50\nFIGURE_WIDTH 5\nAVG_LOWERCASE_WIDTH 50\nAVG_UPPERCASE_WIDTH 50\nENDPROPERTIES\nCHARS 101\nSTARTCHAR space\nENCODING 32\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 1 4 0\nBITMAP\n00\nENDCHAR\nSTARTCHAR exclam\nENCODING 33\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 7 2 0\nBITMAP\n80\n80\n80\n80\n80\n00\n80\nENDCHAR\nSTARTCHAR quotedbl\nENCODING 34\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 3 3 1 4\nBITMAP\nA0\nA0\nA0\nENDCHAR\nSTARTCHAR numbersign\nENCODING 35\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n50\n50\nF8\n50\nF8\n50\n50\nENDCHAR\nSTARTCHAR dollar\nENCODING 36\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 9 0 -1\nBITMAP\n20\n70\nA8\nA0\n70\n28\nA8\n70\n20\nENDCHAR\nSTARTCHAR percent\nENCODING 37\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nC8\nC8\n10\n20\n40\n98\n98\nENDCHAR\nSTARTCHAR ampersand\nENCODING 38\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 7 0 0\nBITMAP\n60\n80\n90\n78\n90\n90\n68\nENDCHAR\nSTARTCHAR quotesingle\nENCODING 39\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 3 2 4\nBITMAP\n80\n80\n80\nENDCHAR\nSTARTCHAR parenleft\nENCODING 40\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 8 0 -1\nBITMAP\n30\n40\n80\n80\n80\n80\n40\n30\nENDCHAR\nSTARTCHAR parenright\nENCODING 41\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 8 0 -1\nBITMAP\nC0\n20\n10\n10\n10\n10\n20\nC0\nENDCHAR\nSTARTCHAR asterisk\nENCODING 42\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 5 0 0\nBITMAP\n50\n20\nF8\n20\n50\nENDCHAR\nSTARTCHAR plus\nENCODING 43\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 5 0 0\nBITMAP\n20\n20\nF8\n20\n20\nENDCHAR\nSTARTCHAR comma\nENCODING 44\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 2 3 1 -1\nBITMAP\n40\n40\n80\nENDCHAR\nSTARTCHAR hyphen\nENCODING 45\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 1 0 2\nBITMAP\nF0\nENDCHAR\nSTARTCHAR period\nENCODING 46\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 1 2 0\nBITMAP\n80\nENDCHAR\nSTARTCHAR slash\nENCODING 47\nSWIDTH 727 0\nDWIDTH 8 0\nBBX 7 7 0 0\nBITMAP\n02\n04\n08\n10\n20\n40\n80\nENDCHAR\nSTARTCHAR zero\nENCODING 48\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n98\nA8\nC8\n88\n70\nENDCHAR\nSTARTCHAR one\nENCODING 49\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n20\n60\nA0\n20\n20\n20\nF8\nENDCHAR\nSTARTCHAR two\nENCODING 50\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n08\n10\n20\n40\nF8\nENDCHAR\nSTARTCHAR three\nENCODING 51\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n08\n10\n08\n88\n70\nENDCHAR\nSTARTCHAR four\nENCODING 52\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n80\n90\n90\nF8\n10\n10\n10\nENDCHAR\nSTARTCHAR five\nENCODING 53\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF8\n80\n80\nF0\n08\n08\nF0\nENDCHAR\nSTARTCHAR six\nENCODING 54\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n80\nF0\n88\n88\n70\nENDCHAR\nSTARTCHAR seven\nENCODING 55\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 7 0 0\nBITMAP\nF8\n08\n08\n10\n10\n20\n20\nENDCHAR\nSTARTCHAR eight\nENCODING 56\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n88\n70\n88\n88\n70\nENDCHAR\nSTARTCHAR nine\nENCODING 57\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n88\n78\n08\n88\n70\nENDCHAR\nSTARTCHAR colon\nENCODING 58\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 4 2 1\nBITMAP\n80\n00\n00\n80\nENDCHAR\nSTARTCHAR semicolon\nENCODING 59\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 2 6 1 -1\nBITMAP\n40\n00\n00\n40\n40\n80\nENDCHAR\nSTARTCHAR less\nENCODING 60\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 3 5 0 0\nBITMAP\n20\n40\n80\n40\n20\nENDCHAR\nSTARTCHAR equal\nENCODING 61\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 3 0 1\nBITMAP\nF0\n00\nF0\nENDCHAR\nSTARTCHAR greater\nENCODING 62\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 3 5 1 0\nBITMAP\n80\n40\n20\n40\n80\nENDCHAR\nSTARTCHAR question\nENCODING 63\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n60\n90\n10\n20\n40\n00\n40\nENDCHAR\nSTARTCHAR at\nENCODING 64\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 7 0 0\nBITMAP\n70\n88\nB8\nA8\nB8\n80\n70\nENDCHAR\nSTARTCHAR A\nENCODING 65\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n20\n50\n88\nF8\n88\n88\n88\nENDCHAR\nSTARTCHAR B\nENCODING 66\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF0\n88\n88\nF0\n88\n88\nF0\nENDCHAR\nSTARTCHAR C\nENCODING 67\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n80\n80\n80\n88\n70\nENDCHAR\nSTARTCHAR D\nENCODING 68\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF0\n88\n88\n88\n88\n88\nF0\nENDCHAR\nSTARTCHAR E\nENCODING 69\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF8\n80\n80\nF0\n80\n80\nF8\nENDCHAR\nSTARTCHAR F\nENCODING 70\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF8\n80\n80\nF0\n80\n80\n80\nENDCHAR\nSTARTCHAR G\nENCODING 71\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n80\n98\n88\n88\n70\nENDCHAR\nSTARTCHAR H\nENCODING 72\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n88\n88\nF8\n88\n88\n88\nENDCHAR\nSTARTCHAR I\nENCODING 73\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 3 7 1 0\nBITMAP\nE0\n40\n40\n40\n40\n40\nE0\nENDCHAR\nSTARTCHAR J\nENCODING 74\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 4 7 0 0\nBITMAP\nF0\n10\n10\n10\n10\n10\nE0\nENDCHAR\nSTARTCHAR K\nENCODING 75\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n90\nA0\nC0\nA0\n90\n88\nENDCHAR\nSTARTCHAR L\nENCODING 76\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n80\n80\n80\n80\n80\n80\nF8\nENDCHAR\nSTARTCHAR M\nENCODING 77\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\nD8\nA8\n88\n88\n88\n88\nENDCHAR\nSTARTCHAR N\nENCODING 78\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\nC8\nA8\n98\n88\n88\n88\nENDCHAR\nSTARTCHAR O\nENCODING 79\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n88\n88\n88\n88\n70\nENDCHAR\nSTARTCHAR P\nENCODING 80\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF0\n88\n88\nF0\n80\n80\n80\nENDCHAR\nSTARTCHAR Q\nENCODING 81\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 6 8 0 -1\nBITMAP\n70\n88\n88\n88\n88\n98\n78\n04\nENDCHAR\nSTARTCHAR R\nENCODING 82\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF0\n88\n88\nF0\n88\n88\n88\nENDCHAR\nSTARTCHAR S\nENCODING 83\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n70\n88\n80\n70\n08\n88\n70\nENDCHAR\nSTARTCHAR T\nENCODING 84\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF8\n20\n20\n20\n20\n20\n20\nENDCHAR\nSTARTCHAR U\nENCODING 85\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n88\n88\n88\n88\n88\n70\nENDCHAR\nSTARTCHAR V\nENCODING 86\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n88\n88\n88\n88\n50\n20\nENDCHAR\nSTARTCHAR W\nENCODING 87\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n88\n88\n88\nA8\nD8\n88\nENDCHAR\nSTARTCHAR X\nENCODING 88\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n88\n50\n20\n50\n88\n88\nENDCHAR\nSTARTCHAR Y\nENCODING 89\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\n88\n88\n88\n50\n20\n20\n20\nENDCHAR\nSTARTCHAR Z\nENCODING 90\nSWIDTH 544 0\nDWIDTH 6 0\nBBX 5 7 0 0\nBITMAP\nF8\n08\n10\n20\n40\n80\nF8\nENDCHAR\nSTARTCHAR bracketleft\nENCODING 91\nSWIDTH 363 0\nDWIDTH 4 0\nBBX 2 7 1 0\nBITMAP\nC0\n80\n80\n80\n80\n80\nC0\nENDCHAR\nSTARTCHAR backslash\nENCODING 92\nSWIDTH 727 0\nDWIDTH 8 0\nBBX 7 7 0 0\nBITMAP\n80\n40\n20\n10\n08\n04\n02\nENDCHAR\nSTARTCHAR bracketright\nENCODING 93\nSWIDTH 363 0\nDWIDTH 4 0\nBBX 2 7 1 0\nBITMAP\nC0\n40\n40\n40\n40\n40\nC0\nENDCHAR\nSTARTCHAR asciicircum\nENCODING 94\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 5 3 0 4\nBITMAP\n20\n50\n88\nENDCHAR\nSTARTCHAR underscore\nENCODING 95\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 1 0 0\nBITMAP\nF0\nENDCHAR\nSTARTCHAR grave\nENCODING 96\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 3 3 1 4\nBITMAP\n80\n40\n20\nENDCHAR\nSTARTCHAR a\nENCODING 97\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n70\n90\n90\nB0\n50\nENDCHAR\nSTARTCHAR b\nENCODING 98\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n80\n80\nE0\n90\n90\n90\nE0\nENDCHAR\nSTARTCHAR c\nENCODING 99\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n60\n90\n80\n90\n60\nENDCHAR\nSTARTCHAR d\nENCODING 100\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n10\n10\n70\n90\n90\n90\n70\nENDCHAR\nSTARTCHAR e\nENCODING 101\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n60\n90\nF0\n80\n70\nENDCHAR\nSTARTCHAR f\nENCODING 102\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n30\n40\n40\nE0\n40\n40\n40\nENDCHAR\nSTARTCHAR g\nENCODING 103\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 -2\nBITMAP\n70\n90\n90\n90\n70\n10\n60\nENDCHAR\nSTARTCHAR h\nENCODING 104\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n80\n80\nE0\n90\n90\n90\n90\nENDCHAR\nSTARTCHAR i\nENCODING 105\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n40\n00\nC0\n40\n40\n40\n30\nENDCHAR\nSTARTCHAR j\nENCODING 106\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 3 9 0 -2\nBITMAP\n20\n00\n60\n20\n20\n20\n20\n20\nC0\nENDCHAR\nSTARTCHAR k\nENCODING 107\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n80\n80\n90\nA0\nC0\nA0\n90\nENDCHAR\nSTARTCHAR l\nENCODING 108\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\nC0\n40\n40\n40\n40\n40\n30\nENDCHAR\nSTARTCHAR m\nENCODING 109\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n90\nF0\n90\n90\n90\nENDCHAR\nSTARTCHAR n\nENCODING 110\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\nE0\n90\n90\n90\n90\nENDCHAR\nSTARTCHAR o\nENCODING 111\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n60\n90\n90\n90\n60\nENDCHAR\nSTARTCHAR p\nENCODING 112\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 -2\nBITMAP\nE0\n90\n90\n90\nE0\n80\n80\nENDCHAR\nSTARTCHAR q\nENCODING 113\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 -2\nBITMAP\n70\n90\n90\n90\n70\n10\n10\nENDCHAR\nSTARTCHAR r\nENCODING 114\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\nE0\n90\n80\n80\n80\nENDCHAR\nSTARTCHAR s\nENCODING 115\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n70\n80\n60\n10\nE0\nENDCHAR\nSTARTCHAR t\nENCODING 116\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 0\nBITMAP\n40\n40\nF0\n40\n40\n40\n30\nENDCHAR\nSTARTCHAR u\nENCODING 117\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n90\n90\n90\n90\n70\nENDCHAR\nSTARTCHAR v\nENCODING 118\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n90\n90\n90\n60\n60\nENDCHAR\nSTARTCHAR w\nENCODING 119\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n90\n90\n90\nF0\n90\nENDCHAR\nSTARTCHAR x\nENCODING 120\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\n90\n90\n60\n90\n90\nENDCHAR\nSTARTCHAR y\nENCODING 121\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 7 0 -2\nBITMAP\n90\n90\n90\n90\n70\n10\n60\nENDCHAR\nSTARTCHAR z\nENCODING 122\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 4 5 0 0\nBITMAP\nF0\n20\n40\n80\nF0\nENDCHAR\nSTARTCHAR braceleft\nENCODING 123\nSWIDTH 363 0\nDWIDTH 4 0\nBBX 3 7 0 0\nBITMAP\n20\n40\n40\nC0\n40\n40\n20\nENDCHAR\nSTARTCHAR bar\nENCODING 124\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 9 2 -1\nBITMAP\n80\n80\n80\n80\n80\n80\n80\n80\n80\nENDCHAR\nSTARTCHAR braceright\nENCODING 125\nSWIDTH 363 0\nDWIDTH 4 0\nBBX 3 7 1 0\nBITMAP\n80\n40\n40\n20\n40\n40\n80\nENDCHAR\nSTARTCHAR asciitilde\nENCODING 126\nSWIDTH 727 0\nDWIDTH 8 0\nBBX 7 3 0 1\nBITMAP\n60\n92\n0C\nENDCHAR\nSTARTCHAR uni00A0\nENCODING 160\nSWIDTH 454 0\nDWIDTH 5 0\nBBX 1 1 0 0\nBITMAP\n00\nENDCHAR\nSTARTCHAR arrowleft\nENCODING 8592\nSWIDTH 636 0\nDWIDTH 7 0\nBBX 6 5 0 1\nBITMAP\n20\n60\nFC\n60\n20\nENDCHAR\nSTARTCHAR arrowup\nENCODING 8593\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 6 0 0\nBITMAP\n20\n70\nF8\n20\n20\n20\nENDCHAR\nSTARTCHAR arrowright\nENCODING 8594\nSWIDTH 636 0\nDWIDTH 7 0\nBBX 6 5 0 1\nBITMAP\n10\n18\nFC\n18\n10\nENDCHAR\nSTARTCHAR arrowdown\nENCODING 8595\nSWIDTH 545 0\nDWIDTH 6 0\nBBX 5 5 0 1\nBITMAP\n20\n20\nF8\n70\n20\nENDCHAR\nSTARTCHAR carriagereturn\nENCODING 8629\nSWIDTH 636 0\nDWIDTH 7 0\nBBX 6 6 0 0\nBITMAP\n04\n24\n64\nFC\n60\n20\nENDCHAR\nENDFONT\n"
  },
  {
    "path": "data/tr1/mac/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CFBundleExecutable</key>\n    <string>TRX</string>\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n    <key>CFBundleIdentifier</key>\n    <string>com.lostartefacts.tr1x</string>\n    <key>CFBundleName</key>\n    <string>TR1X</string>\n    <key>CFBundleIconFile</key>\n    <string>icon</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "data/tr2/mac/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CFBundleExecutable</key>\n    <string>TRX</string>\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n    <key>CFBundleIdentifier</key>\n    <string>com.lostartefacts.tr2x</string>\n    <key>CFBundleName</key>\n    <string>TR2X</string>\n    <key>CFBundleIconFile</key>\n    <string>icon</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_basic_latin.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Basic Latin\" (U+0000 to U+007F)\n# --------------------------------------------------\n# ASCII a-z\nU+0061:a 0 T render(\"Timesbd.ttf\", index=26)\nU+0062:b 0 T render(\"Timesbd.ttf\", index=27)\nU+0063:c 0 T render(\"Timesbd.ttf\", index=28)\nU+0064:d 0 T render(\"Timesbd.ttf\", index=29)\nU+0065:e 0 T render(\"Timesbd.ttf\", index=30)\nU+0066:f 0 T render(\"Timesbd.ttf\", index=31)\nU+0067:g 0 T render(\"Timesbd.ttf\", index=32)\nU+0068:h 0 T render(\"Timesbd.ttf\", index=33)\nU+0069:i 0 T render(\"Timesbd.ttf\", index=34)\nU+006A:j 0 T render(\"Timesbd.ttf\", index=35)\nU+006B:k 0 T render(\"Timesbd.ttf\", index=36)\nU+006C:l 0 T render(\"Timesbd.ttf\", index=37)\nU+006D:m 0 T render(\"Timesbd.ttf\", index=38)\nU+006E:n 0 T render(\"Timesbd.ttf\", index=39)\nU+006F:o 0 T render(\"Timesbd.ttf\", index=40)\nU+0070:p 0 T render(\"Timesbd.ttf\", index=41)\nU+0071:q 0 T render(\"Timesbd.ttf\", index=42)\nU+0072:r 0 T render(\"Timesbd.ttf\", index=43)\nU+0073:s 0 T render(\"Timesbd.ttf\", index=44)\nU+0074:t 0 T render(\"Timesbd.ttf\", index=45)\nU+0075:u 0 T render(\"Timesbd.ttf\", index=46)\nU+0076:v 0 T render(\"Timesbd.ttf\", index=47)\nU+0077:w 0 T render(\"Timesbd.ttf\", index=48)\nU+0078:x 0 T render(\"Timesbd.ttf\", index=49)\nU+0079:y 0 T render(\"Timesbd.ttf\", index=50)\nU+007A:z 0 T render(\"Timesbd.ttf\", index=51)\n\n# ASCII A-Z\nU+0041:A 0 T render(\"Timesbd.ttf\", index=0)\nU+0042:B 0 T render(\"Timesbd.ttf\", index=1)\nU+0043:C 0 T render(\"Timesbd.ttf\", index=2)\nU+0044:D 0 T render(\"Timesbd.ttf\", index=3)\nU+0045:E 0 T render(\"Timesbd.ttf\", index=4)\nU+0046:F 0 T render(\"Timesbd.ttf\", index=5)\nU+0047:G 0 T render(\"Timesbd.ttf\", index=6)\nU+0048:H 0 T render(\"Timesbd.ttf\", index=7)\nU+0049:I 0 T render(\"Timesbd.ttf\", index=8)\nU+004A:J 0 T render(\"Timesbd.ttf\", index=9)\nU+004B:K 0 T render(\"Timesbd.ttf\", index=10)\nU+004C:L 0 T render(\"Timesbd.ttf\", index=11)\nU+004D:M 0 T render(\"Timesbd.ttf\", index=12)\nU+004E:N 0 T render(\"Timesbd.ttf\", index=13)\nU+004F:O 0 T render(\"Timesbd.ttf\", index=14)\nU+0050:P 0 T render(\"Timesbd.ttf\", index=15)\nU+0051:Q 0 T render(\"Timesbd.ttf\", index=16)\nU+0052:R 0 T render(\"Timesbd.ttf\", index=17)\nU+0053:S 0 T render(\"Timesbd.ttf\", index=18)\nU+0054:T 0 T render(\"Timesbd.ttf\", index=19)\nU+0055:U 0 T render(\"Timesbd.ttf\", index=20)\nU+0056:V 0 T render(\"Timesbd.ttf\", index=21)\nU+0057:W 0 T render(\"Timesbd.ttf\", index=22)\nU+0058:X 0 T render(\"Timesbd.ttf\", index=23)\nU+0059:Y 0 T render(\"Timesbd.ttf\", index=24)\nU+005A:Z 0 T render(\"Timesbd.ttf\", index=25)\n\n# Digits 0-9\nU+0030:0 0 T render(\"Timesbd.ttf\", index=52)\nU+0031:1 0 T render(\"Timesbd.ttf\", index=53)\nU+0032:2 0 T render(\"Timesbd.ttf\", index=54)\nU+0033:3 0 T render(\"Timesbd.ttf\", index=55)\nU+0034:4 0 T render(\"Timesbd.ttf\", index=56)\nU+0035:5 0 T render(\"Timesbd.ttf\", index=57)\nU+0036:6 0 T render(\"Timesbd.ttf\", index=58)\nU+0037:7 0 T render(\"Timesbd.ttf\", index=59)\nU+0038:8 0 T render(\"Timesbd.ttf\", index=60)\nU+0039:9 0 T render(\"Timesbd.ttf\", index=61)\n\n# Basic Punctuation\nU+0021:! 0 T render(\"Timesbd.ttf\", index=64)\nU+0022:\" 0 T render(\"Timesbd.ttf\")\nU+0023:# 0 T render(\"Timesbd.ttf\", index=78)\nU+0024:$ 0 T render(\"Timesbd.ttf\")\nU+0025:% 0 T render(\"Timesbd.ttf\")\nU+0026:& 0 T render(\"Timesbd.ttf\")\nU+0027:' 0 T render(\"Timesbd.ttf\", index=79)\nU+0028:( 0 T render(\"Timesbd.ttf\")\nU+0029:) 0 T render(\"Timesbd.ttf\")\nU+002A:* 0 T render(\"Timesbd.ttf\")\nU+002B:+ 0 T render(\"Timesbd.ttf\", index=72)\nU+002C:, 0 T render(\"Timesbd.ttf\", index=63)\nU+002D:- 0 T render(\"Timesbd.ttf\", index=71, offset_y=-2)\nU+002E:. 0 T render(\"Timesbd.ttf\", index=62)\nU+002F:/ 0 T render(\"Timesbd.ttf\", index=68)\nU+003A:: 0 T render(\"Timesbd.ttf\", index=73)\nU+003B:; 0 T render(\"Timesbd.ttf\")\nU+003C:< 0 T render(\"Timesbd.ttf\")\nU+003D:= 0 T render(\"Timesbd.ttf\")\nU+003E:> 0 T render(\"Timesbd.ttf\")\nU+003F:? 0 T render(\"Timesbd.ttf\", index=65)\nU+0040:@ 0 T render(\"Timesbd.ttf\")\nU+005B:[ 0 T render(\"Timesbd.ttf\", index=66)\nU+005C:\\ 0 T render(\"Timesbd.ttf\", index=76)\nU+005D:] 0 T render(\"Timesbd.ttf\", index=75)\nU+005E:^ 0 T render(\"Timesbd.ttf\")\nU+005F:_ 0 T render(\"Timesbd.ttf\")\nU+0060:` 0 T render(\"Timesbd.ttf\")\nU+007B:{ 0 T render(\"Timesbd.ttf\")\nU+007C:| 0 T render(\"Timesbd.ttf\")\nU+007D:} 0 T render(\"Timesbd.ttf\")\nU+007E:~ 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_combining_diactrics.txt",
    "content": "# --------------------------------------------------\n# Combining diactrics\n# --------------------------------------------------\n\"\\\\{grave accent}\"        0 T render(\"Timesbd.ttf\", index=77)\n\"\\\\{acute accent}\"        0 T render(\"Timesbd.ttf\", index=70)\n\"\\\\{circumflex accent}\"   0 T render(\"Timesbd.ttf\", index=69)\n\"\\\\{circumflex}\"          0 T link(\"\\\\{circumflex accent}\")\n\"\\\\{macron}\"              0 T render(\"Timesbd.ttf\")\n\"\\\\{breve}\"               0 T render(\"Timesbd.ttf\")\n\"\\\\{dot above}\"           0 T render(\"Timesbd.ttf\")\n\"\\\\{umlaut}\"              0 T render(\"Timesbd.ttf\", index=67)\n\"\\\\{caron}\"               0 T render(\"Timesbd.ttf\")\n\"\\\\{ring above}\"          0 T render(\"Timesbd.ttf\")\n\"\\\\{tilde}\"               0 T render(\"Timesbd.ttf\")\n\"\\\\{double acute accent}\" 0 T render(\"Timesbd.ttf\")\n\"\\\\{acute umlaut}\"        0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_cyrillic.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Cyrillic\" (U+0400 to U+04FF)\n# --------------------------------------------------\nU+0400:Ѐ 0 T render(\"Timesbd.ttf\")\nU+0401:Ё 0 T render(\"Timesbd.ttf\")\nU+0402:Ђ 0 T render(\"Timesbd.ttf\")\nU+0403:Ѓ 0 T render(\"Timesbd.ttf\")\nU+0404:Є 0 T render(\"Timesbd.ttf\")\nU+0405:Ѕ 0 T link(\"S\")\nU+0406:І 0 T link(\"I\")\nU+0407:Ї 0 T render(\"Timesbd.ttf\")\nU+0408:Ј 0 T link(\"J\")\nU+0409:Љ 0 T render(\"Timesbd.ttf\")\nU+040A:Њ 0 T render(\"Timesbd.ttf\")\nU+040B:Ћ 0 T render(\"Timesbd.ttf\")\nU+040C:Ќ 0 T render(\"Timesbd.ttf\")\nU+040D:Ѝ 0 T render(\"Timesbd.ttf\")\nU+040E:Ў 0 T render(\"Timesbd.ttf\")\nU+040F:Џ 0 T render(\"Timesbd.ttf\")\nU+0410:А 0 T link(\"A\")\nU+0411:Б 0 T render(\"Timesbd.ttf\")\nU+0412:В 0 T link(\"B\")\nU+0413:Г 0 T link(U+0393:Γ)\nU+0414:Д 0 T render(\"Timesbd.ttf\")\nU+0415:Е 0 T link(\"E\")\nU+0416:Ж 0 T render(\"Timesbd.ttf\")\nU+0417:З 0 T render(\"Timesbd.ttf\")\nU+0418:И 0 T render(\"Timesbd.ttf\")\nU+0419:Й 0 T render(\"Timesbd.ttf\")\nU+041A:К 0 T link(\"K\")\nU+041B:Л 0 T render(\"Timesbd.ttf\")\nU+041C:М 0 T link(\"M\")\nU+041D:Н 0 T link(\"H\")\nU+041E:О 0 T link(\"O\")\nU+041F:П 0 T link(U+03A0:Π)\nU+0420:Р 0 T link(\"P\")\nU+0421:С 0 T link(\"C\")\nU+0422:Т 0 T link(\"T\")\nU+0423:У 0 T render(\"Timesbd.ttf\")\nU+0424:Ф 0 T link(U+03A6:Φ)\nU+0425:Х 0 T link(\"X\")\nU+0426:Ц 0 T render(\"Timesbd.ttf\")\nU+0427:Ч 0 T render(\"Timesbd.ttf\")\nU+0428:Ш 0 T render(\"Timesbd.ttf\")\nU+0429:Щ 0 T render(\"Timesbd.ttf\")\nU+042A:Ъ 0 T render(\"Timesbd.ttf\")\nU+042B:Ы 0 T render(\"Timesbd.ttf\")\nU+042C:Ь 0 T render(\"Timesbd.ttf\")\nU+042D:Э 0 T render(\"Timesbd.ttf\")\nU+042E:Ю 0 T render(\"Timesbd.ttf\")\nU+042F:Я 0 T render(\"Timesbd.ttf\")\nU+0430:а 0 T link(\"a\")\nU+0431:б 0 T render(\"Timesbd.ttf\")\nU+0432:в 0 T render(\"Timesbd.ttf\")\nU+0433:г 0 T render(\"Timesbd.ttf\")\nU+0434:д 0 T render(\"Timesbd.ttf\")\nU+0435:е 0 T link(\"e\")\nU+0436:ж 0 T render(\"Timesbd.ttf\")\nU+0437:з 0 T render(\"Timesbd.ttf\")\nU+0438:и 0 T render(\"Timesbd.ttf\")\nU+0439:й 0 T render(\"Timesbd.ttf\")\nU+043A:к 0 T link(U+0138:ĸ)\nU+043B:л 0 T render(\"Timesbd.ttf\")\nU+043C:м 0 T render(\"Timesbd.ttf\")\nU+043D:н 0 T render(\"Timesbd.ttf\")\nU+043E:о 0 T link(\"o\")\nU+043F:п 0 T render(\"Timesbd.ttf\")\nU+0440:р 0 T link(\"p\")\nU+0441:с 0 T link(\"c\")\nU+0442:т 0 T render(\"Timesbd.ttf\")\nU+0443:у 0 T link(\"y\")\nU+0444:ф 0 T render(\"Timesbd.ttf\")\nU+0445:х 0 T link(\"x\")\nU+0446:ц 0 T render(\"Timesbd.ttf\")\nU+0447:ч 0 T render(\"Timesbd.ttf\")\nU+0448:ш 0 T render(\"Timesbd.ttf\")\nU+0449:щ 0 T render(\"Timesbd.ttf\")\nU+044A:ъ 0 T render(\"Timesbd.ttf\")\nU+044B:ы 0 T render(\"Timesbd.ttf\")\nU+044C:ь 0 T render(\"Timesbd.ttf\")\nU+044D:э 0 T render(\"Timesbd.ttf\")\nU+044E:ю 0 T render(\"Timesbd.ttf\")\nU+044F:я 0 T render(\"Timesbd.ttf\")\nU+0450:ѐ 0 T render(\"Timesbd.ttf\")\nU+0451:ё 0 T render(\"Timesbd.ttf\")\nU+0452:ђ 0 T render(\"Timesbd.ttf\")\nU+0453:ѓ 0 T render(\"Timesbd.ttf\")\nU+0454:є 0 T render(\"Timesbd.ttf\")\nU+0455:ѕ 0 T link(\"s\")\nU+0456:і 0 T link(\"i\")\nU+0457:ї 0 T render(\"Timesbd.ttf\")\nU+0458:ј 0 T link(\"j\")\nU+0459:љ 0 T render(\"Timesbd.ttf\")\nU+045A:њ 0 T render(\"Timesbd.ttf\")\nU+045B:ћ 0 T link(U+0127:ħ)\nU+045C:ќ 0 T render(\"Timesbd.ttf\")\nU+045D:ѝ 0 T render(\"Timesbd.ttf\")\nU+045E:ў 0 T render(\"Timesbd.ttf\")\nU+045F:џ 0 T render(\"Timesbd.ttf\")\nU+0490:Ґ 0 T render(\"Timesbd.ttf\")\nU+0491:ґ 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_greek_and_coptic.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Greek and Coptic\" (U+0370 to U+03FF)\n# --------------------------------------------------\nU+0393:Γ 0 T render(\"Timesbd.ttf\")\nU+0394:Δ 0 T render(\"Timesbd.ttf\")\nU+0395:Ε 0 T link(\"E\")\nU+0396:Ζ 0 T link(\"Z\")\nU+0397:Η 0 T link(\"H\")\nU+0398:Θ 0 T render(\"Timesbd.ttf\")\nU+0399:Ι 0 T link(\"I\")\nU+039A:Κ 0 T link(\"K\")\nU+039B:Λ 0 T render(\"Timesbd.ttf\")\nU+039C:Μ 0 T link(\"M\")\nU+039D:Ν 0 T link(\"N\")\nU+039E:Ξ 0 T render(\"Timesbd.ttf\")\nU+039F:Ο 0 T link(\"O\")\nU+03A0:Π 0 T render(\"Timesbd.ttf\")\nU+03A1:Ρ 0 T link(\"P\")\nU+03A3:Σ 0 T render(\"Timesbd.ttf\")\nU+03A4:Τ 0 T link(\"T\")\nU+03A5:Υ 0 T link(\"Y\")\nU+03A6:Φ 0 T render(\"Timesbd.ttf\")\nU+03A7:Χ 0 T link(\"X\")\nU+03A8:Ψ 0 T render(\"Timesbd.ttf\")\nU+03A9:Ω 0 T render(\"Timesbd.ttf\")\nU+03B1:α 0 T render(\"Timesbd.ttf\")\nU+03B2:β 0 T render(\"Timesbd.ttf\")\nU+03B3:γ 0 T render(\"Timesbd.ttf\")\nU+03B4:δ 0 T render(\"Timesbd.ttf\")\nU+03B5:ε 0 T render(\"Timesbd.ttf\")\nU+03B6:ζ 0 T render(\"Timesbd.ttf\")\nU+03B7:η 0 T render(\"Timesbd.ttf\")\nU+03B8:θ 0 T render(\"Timesbd.ttf\")\nU+03B9:ι 0 T link(U+0131:ı)\nU+03BA:κ 0 T link(U+0138:ĸ)\nU+03BB:λ 0 T render(\"Timesbd.ttf\")\nU+03BC:μ 0 T link(U+00B5:µ)\nU+03BD:ν 0 T link(\"v\")\nU+03BE:ξ 0 T render(\"Timesbd.ttf\")\nU+03BF:ο 0 T link(\"o\")\nU+03C0:π 0 T render(\"Timesbd.ttf\")\nU+03C1:ρ 0 T render(\"Timesbd.ttf\")\nU+03C2:ς 0 T render(\"Timesbd.ttf\")\nU+03C3:σ 0 T render(\"Timesbd.ttf\")\nU+03C4:τ 0 T render(\"Timesbd.ttf\")\nU+03C5:υ 0 T render(\"Timesbd.ttf\")\nU+03C6:φ 0 T render(\"Timesbd.ttf\")\nU+03C7:χ 0 T render(\"Timesbd.ttf\")\nU+03C8:ψ 0 T render(\"Timesbd.ttf\")\nU+03C9:ω 0 T render(\"Timesbd.ttf\")\n\nU+0386:Ά 0 T render(\"Timesbd.ttf\")\nU+0388:Έ 0 T render(\"Timesbd.ttf\")\nU+0389:Ή 0 T render(\"Timesbd.ttf\")\nU+038A:Ί 0 T render(\"Timesbd.ttf\")\nU+038C:Ό 0 T render(\"Timesbd.ttf\")\nU+038E:Ύ 0 T render(\"Timesbd.ttf\")\nU+038F:Ώ 0 T render(\"Timesbd.ttf\")\nU+0390:ΐ 0 T render(\"Timesbd.ttf\")\nU+0391:Α 0 T link(\"A\")\nU+0392:Β 0 T link(\"B\")\nU+03AA:Ϊ 0 T render(\"Timesbd.ttf\")\nU+03AB:Ϋ 0 T render(\"Timesbd.ttf\")\nU+03AC:ά 0 T render(\"Timesbd.ttf\")\nU+03AD:έ 0 T render(\"Timesbd.ttf\")\nU+03AE:ή 0 T render(\"Timesbd.ttf\")\nU+03AF:ί 0 T render(\"Timesbd.ttf\")\nU+03B0:ΰ 0 T render(\"Timesbd.ttf\")\nU+03CA:ϊ 0 T render(\"Timesbd.ttf\")\nU+03CB:ϋ 0 T render(\"Timesbd.ttf\")\nU+03CC:ό 0 T render(\"Timesbd.ttf\")\nU+03CD:ύ 0 T render(\"Timesbd.ttf\")\nU+03CE:ώ 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_latin-1_supplement.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin-1 Supplement\" (U+0080 to U+00FF)\n# --------------------------------------------------\nU+00A1:¡ 0 T render(\"Timesbd.ttf\")\nU+00A2:¢ 0 T render(\"Timesbd.ttf\")\nU+00A3:£ 0 T render(\"Timesbd.ttf\")\nU+00A4:¤ 0 T render(\"Timesbd.ttf\")\nU+00A5:¥ 0 T render(\"Timesbd.ttf\")\nU+00A6:¦ 0 T render(\"Timesbd.ttf\")\nU+00A7:§ 0 T render(\"Timesbd.ttf\")\nU+00A9:© 0 T render(\"Timesbd.ttf\")\nU+00AA:ª 0 T render(\"Timesbd.ttf\")\nU+00AB:« 0 T render(\"Timesbd.ttf\")\nU+00AC:¬ 0 T render(\"Timesbd.ttf\")\nU+00AE:® 0 T render(\"Timesbd.ttf\")\nU+00B0:° 0 T render(\"Timesbd.ttf\")\nU+00B1:± 0 T render(\"Timesbd.ttf\")\nU+00B2:² 0 T render(\"Timesbd.ttf\")\nU+00B3:³ 0 T render(\"Timesbd.ttf\")\nU+00B5:µ 0 T render(\"Timesbd.ttf\")\nU+00B6:¶ 0 T render(\"Timesbd.ttf\")\nU+00B7:· 0 T render(\"Timesbd.ttf\")\nU+00B9:¹ 0 T render(\"Timesbd.ttf\")\nU+00BA:º 0 T render(\"Timesbd.ttf\")\nU+00BB:» 0 T render(\"Timesbd.ttf\")\nU+00BC:¼ 0 T render(\"Timesbd.ttf\")\nU+00BD:½ 0 T render(\"Timesbd.ttf\")\nU+00BE:¾ 0 T render(\"Timesbd.ttf\")\nU+00BF:¿ 0 T render(\"Timesbd.ttf\")\nU+00C0:À 0 T render(\"Timesbd.ttf\")\nU+00C1:Á 0 T render(\"Timesbd.ttf\")\nU+00C2:Â 0 T render(\"Timesbd.ttf\")\nU+00C3:Ã 0 T render(\"Timesbd.ttf\")\nU+00C4:Ä 0 T render(\"Timesbd.ttf\")\nU+00C5:Å 0 T render(\"Timesbd.ttf\")\nU+00C6:Æ 0 T render(\"Timesbd.ttf\")\nU+00C7:Ç 0 T render(\"Timesbd.ttf\")\nU+00C8:È 0 T render(\"Timesbd.ttf\")\nU+00C9:É 0 T render(\"Timesbd.ttf\")\nU+00CA:Ê 0 T render(\"Timesbd.ttf\")\nU+00CB:Ë 0 T render(\"Timesbd.ttf\")\nU+00CC:Ì 0 T render(\"Timesbd.ttf\")\nU+00CD:Í 0 T render(\"Timesbd.ttf\")\nU+00CE:Î 0 T render(\"Timesbd.ttf\")\nU+00CF:Ï 0 T render(\"Timesbd.ttf\")\nU+00D0:Ð 0 T render(\"Timesbd.ttf\")\nU+00D1:Ñ 0 T render(\"Timesbd.ttf\")\nU+00D2:Ò 0 T render(\"Timesbd.ttf\")\nU+00D3:Ó 0 T render(\"Timesbd.ttf\")\nU+00D4:Ô 0 T render(\"Timesbd.ttf\")\nU+00D5:Õ 0 T render(\"Timesbd.ttf\")\nU+00D6:Ö 0 T render(\"Timesbd.ttf\")\nU+00D7:× 0 T render(\"Timesbd.ttf\")\nU+00D8:Ø 0 T render(\"Timesbd.ttf\")\nU+00D9:Ù 0 T render(\"Timesbd.ttf\")\nU+00DA:Ú 0 T render(\"Timesbd.ttf\")\nU+00DB:Û 0 T render(\"Timesbd.ttf\")\nU+00DC:Ü 0 T render(\"Timesbd.ttf\")\nU+00DD:Ý 0 T render(\"Timesbd.ttf\")\nU+00DE:Þ 0 T render(\"Timesbd.ttf\")\nU+00DF:ß 0 T render(\"Timesbd.ttf\", index=74)\nU+00E0:à 0 T render(\"Timesbd.ttf\")\nU+00E1:á 0 T render(\"Timesbd.ttf\")\nU+00E2:â 0 T render(\"Timesbd.ttf\")\nU+00E3:ã 0 T render(\"Timesbd.ttf\")\nU+00E4:ä 0 T render(\"Timesbd.ttf\")\nU+00E5:å 0 T render(\"Timesbd.ttf\")\nU+00E6:æ 0 T render(\"Timesbd.ttf\")\nU+00E7:ç 0 T render(\"Timesbd.ttf\")\nU+00E8:è 0 T render(\"Timesbd.ttf\")\nU+00E9:é 0 T render(\"Timesbd.ttf\")\nU+00EA:ê 0 T render(\"Timesbd.ttf\")\nU+00EB:ë 0 T render(\"Timesbd.ttf\")\nU+00EC:ì 0 T render(\"Timesbd.ttf\")\nU+00ED:í 0 T render(\"Timesbd.ttf\")\nU+00EE:î 0 T render(\"Timesbd.ttf\")\nU+00EF:ï 0 T render(\"Timesbd.ttf\")\nU+00F0:ð 0 T render(\"Timesbd.ttf\")\nU+00F1:ñ 0 T render(\"Timesbd.ttf\")\nU+00F2:ò 0 T render(\"Timesbd.ttf\")\nU+00F3:ó 0 T render(\"Timesbd.ttf\")\nU+00F4:ô 0 T render(\"Timesbd.ttf\")\nU+00F5:õ 0 T render(\"Timesbd.ttf\")\nU+00F6:ö 0 T render(\"Timesbd.ttf\")\nU+00F7:÷ 0 T render(\"Timesbd.ttf\")\nU+00F8:ø 0 T render(\"Timesbd.ttf\")\nU+00F9:ù 0 T render(\"Timesbd.ttf\")\nU+00FA:ú 0 T render(\"Timesbd.ttf\")\nU+00FB:û 0 T render(\"Timesbd.ttf\")\nU+00FC:ü 0 T render(\"Timesbd.ttf\")\nU+00FD:ý 0 T render(\"Timesbd.ttf\")\nU+00FE:þ 0 T render(\"Timesbd.ttf\")\nU+00FF:ÿ 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_latin_extended-a.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin Extended-A\" (U+0100 to U+017F)\n# --------------------------------------------------\nU+0100:Ā 0 T render(\"Timesbd.ttf\")\nU+0101:ā 0 T render(\"Timesbd.ttf\")\nU+0102:Ă 0 T render(\"Timesbd.ttf\")\nU+0103:ă 0 T render(\"Timesbd.ttf\")\nU+0104:Ą 0 T render(\"Timesbd.ttf\")\nU+0105:ą 0 T render(\"Timesbd.ttf\")\nU+0106:Ć 0 T render(\"Timesbd.ttf\")\nU+0107:ć 0 T render(\"Timesbd.ttf\")\nU+0108:Ĉ 0 T render(\"Timesbd.ttf\")\nU+0109:ĉ 0 T render(\"Timesbd.ttf\")\nU+010A:Ċ 0 T render(\"Timesbd.ttf\")\nU+010B:ċ 0 T render(\"Timesbd.ttf\")\nU+010C:Č 0 T render(\"Timesbd.ttf\")\nU+010D:č 0 T render(\"Timesbd.ttf\")\nU+010E:Ď 0 T render(\"Timesbd.ttf\")\nU+010F:ď 0 T render(\"Timesbd.ttf\")\nU+0110:Đ 0 T link(U+00D0:Ð)\nU+0111:đ 0 T render(\"Timesbd.ttf\")\nU+0112:Ē 0 T render(\"Timesbd.ttf\")\nU+0113:ē 0 T render(\"Timesbd.ttf\")\nU+0114:Ĕ 0 T render(\"Timesbd.ttf\")\nU+0115:ĕ 0 T render(\"Timesbd.ttf\")\nU+0116:Ė 0 T render(\"Timesbd.ttf\")\nU+0117:ė 0 T render(\"Timesbd.ttf\")\nU+0118:Ę 0 T render(\"Timesbd.ttf\")\nU+0119:ę 0 T render(\"Timesbd.ttf\")\nU+011A:Ě 0 T render(\"Timesbd.ttf\")\nU+011B:ě 0 T render(\"Timesbd.ttf\")\nU+011C:Ĝ 0 T render(\"Timesbd.ttf\")\nU+011D:ĝ 0 T render(\"Timesbd.ttf\")\nU+011E:Ğ 0 T render(\"Timesbd.ttf\")\nU+011F:ğ 0 T render(\"Timesbd.ttf\")\nU+0120:Ġ 0 T render(\"Timesbd.ttf\")\nU+0121:ġ 0 T render(\"Timesbd.ttf\")\nU+0122:Ģ 0 T render(\"Timesbd.ttf\")\nU+0123:ģ 0 T render(\"Timesbd.ttf\")\nU+0124:Ĥ 0 T render(\"Timesbd.ttf\")\nU+0125:ĥ 0 T render(\"Timesbd.ttf\")\nU+0126:Ħ 0 T render(\"Timesbd.ttf\")\nU+0127:ħ 0 T render(\"Timesbd.ttf\")\nU+0128:Ĩ 0 T render(\"Timesbd.ttf\")\nU+0129:ĩ 0 T render(\"Timesbd.ttf\")\nU+012A:Ī 0 T render(\"Timesbd.ttf\")\nU+012B:ī 0 T render(\"Timesbd.ttf\")\nU+012C:Ĭ 0 T render(\"Timesbd.ttf\")\nU+012D:ĭ 0 T render(\"Timesbd.ttf\")\nU+012E:Į 0 T render(\"Timesbd.ttf\")\nU+012F:į 0 T render(\"Timesbd.ttf\")\nU+0130:İ 0 T render(\"Timesbd.ttf\")\nU+0131:ı 0 T render(\"Timesbd.ttf\")\nU+0134:Ĵ 0 T render(\"Timesbd.ttf\")\nU+0135:ĵ 0 T render(\"Timesbd.ttf\")\nU+0136:Ķ 0 T render(\"Timesbd.ttf\")\nU+0137:ķ 0 T render(\"Timesbd.ttf\")\nU+0138:ĸ 0 T render(\"Timesbd.ttf\")\nU+0139:Ĺ 0 T render(\"Timesbd.ttf\")\nU+013A:ĺ 0 T render(\"Timesbd.ttf\")\nU+013B:Ļ 0 T render(\"Timesbd.ttf\")\nU+013C:ļ 0 T render(\"Timesbd.ttf\")\nU+013D:Ľ 0 T render(\"Timesbd.ttf\")\nU+013E:ľ 0 T render(\"Timesbd.ttf\")\nU+013F:Ŀ 0 T render(\"Timesbd.ttf\")\nU+0140:ŀ 0 T render(\"Timesbd.ttf\")\nU+0141:Ł 0 T render(\"Timesbd.ttf\")\nU+0142:ł 0 T render(\"Timesbd.ttf\")\nU+0143:Ń 0 T render(\"Timesbd.ttf\")\nU+0144:ń 0 T render(\"Timesbd.ttf\")\nU+0145:Ņ 0 T render(\"Timesbd.ttf\")\nU+0146:ņ 0 T render(\"Timesbd.ttf\")\nU+0147:Ň 0 T render(\"Timesbd.ttf\")\nU+0148:ň 0 T render(\"Timesbd.ttf\")\nU+0149:ŉ 0 T render(\"Timesbd.ttf\")\nU+014A:Ŋ 0 T render(\"Timesbd.ttf\")\nU+014B:ŋ 0 T render(\"Timesbd.ttf\")\nU+014C:Ō 0 T render(\"Timesbd.ttf\")\nU+014D:ō 0 T render(\"Timesbd.ttf\")\nU+014E:Ŏ 0 T render(\"Timesbd.ttf\")\nU+014F:ŏ 0 T render(\"Timesbd.ttf\")\nU+0150:Ő 0 T render(\"Timesbd.ttf\")\nU+0151:ő 0 T render(\"Timesbd.ttf\")\nU+0152:Œ 0 T render(\"Timesbd.ttf\")\nU+0153:œ 0 T render(\"Timesbd.ttf\")\nU+0154:Ŕ 0 T render(\"Timesbd.ttf\")\nU+0155:ŕ 0 T render(\"Timesbd.ttf\")\nU+0156:Ŗ 0 T render(\"Timesbd.ttf\")\nU+0157:ŗ 0 T render(\"Timesbd.ttf\")\nU+0158:Ř 0 T render(\"Timesbd.ttf\")\nU+0159:ř 0 T render(\"Timesbd.ttf\")\nU+015A:Ś 0 T render(\"Timesbd.ttf\")\nU+015B:ś 0 T render(\"Timesbd.ttf\")\nU+015C:Ŝ 0 T render(\"Timesbd.ttf\")\nU+015D:ŝ 0 T render(\"Timesbd.ttf\")\nU+015E:Ş 0 T render(\"Timesbd.ttf\")\nU+015F:ş 0 T render(\"Timesbd.ttf\")\nU+0160:Š 0 T render(\"Timesbd.ttf\")\nU+0161:š 0 T render(\"Timesbd.ttf\")\nU+0162:Ţ 0 T render(\"Timesbd.ttf\")\nU+0163:ţ 0 T render(\"Timesbd.ttf\")\nU+0164:Ť 0 T render(\"Timesbd.ttf\")\nU+0165:ť 0 T render(\"Timesbd.ttf\")\nU+0166:Ŧ 0 T render(\"Timesbd.ttf\")\nU+0167:ŧ 0 T render(\"Timesbd.ttf\")\nU+0168:Ũ 0 T render(\"Timesbd.ttf\")\nU+0169:ũ 0 T render(\"Timesbd.ttf\")\nU+016A:Ū 0 T render(\"Timesbd.ttf\")\nU+016B:ū 0 T render(\"Timesbd.ttf\")\nU+016C:Ŭ 0 T render(\"Timesbd.ttf\")\nU+016D:ŭ 0 T render(\"Timesbd.ttf\")\nU+016E:Ů 0 T render(\"Timesbd.ttf\")\nU+016F:ů 0 T render(\"Timesbd.ttf\")\nU+0170:Ű 0 T render(\"Timesbd.ttf\")\nU+0171:ű 0 T render(\"Timesbd.ttf\")\nU+0172:Ų 0 T render(\"Timesbd.ttf\")\nU+0173:ų 0 T render(\"Timesbd.ttf\")\nU+0174:Ŵ 0 T render(\"Timesbd.ttf\")\nU+0175:ŵ 0 T render(\"Timesbd.ttf\")\nU+0176:Ŷ 0 T render(\"Timesbd.ttf\")\nU+0177:ŷ 0 T render(\"Timesbd.ttf\")\nU+0178:Ÿ 0 T render(\"Timesbd.ttf\")\nU+0179:Ź 0 T render(\"Timesbd.ttf\")\nU+017A:ź 0 T render(\"Timesbd.ttf\")\nU+017B:Ż 0 T render(\"Timesbd.ttf\")\nU+017C:ż 0 T render(\"Timesbd.ttf\")\nU+017D:Ž 0 T render(\"Timesbd.ttf\")\nU+017E:ž 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_latin_extended-b.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin Extended-B\" (U+0180 to U+024F)\n# --------------------------------------------------\nU+0192:ƒ 0 T render(\"Timesbd.ttf\")\nU+01CD:Ǎ 0 T render(\"Timesbd.ttf\")\nU+01CE:ǎ 0 T render(\"Timesbd.ttf\")\nU+01CF:Ǐ 0 T render(\"Timesbd.ttf\")\nU+01D0:ǐ 0 T render(\"Timesbd.ttf\")\nU+01D1:Ǒ 0 T render(\"Timesbd.ttf\")\nU+01D2:ǒ 0 T render(\"Timesbd.ttf\")\nU+01D3:Ǔ 0 T render(\"Timesbd.ttf\")\nU+01D4:ǔ 0 T render(\"Timesbd.ttf\")\nU+01E6:Ǧ 0 T render(\"Timesbd.ttf\")\nU+01E7:ǧ 0 T render(\"Timesbd.ttf\")\nU+01E8:Ǩ 0 T render(\"Timesbd.ttf\")\nU+01E9:ǩ 0 T render(\"Timesbd.ttf\")\nU+01F0:ǰ 0 T render(\"Timesbd.ttf\")\nU+01F4:Ǵ 0 T render(\"Timesbd.ttf\")\nU+01F5:ǵ 0 T render(\"Timesbd.ttf\")\nU+01F8:Ǹ 0 T render(\"Timesbd.ttf\")\nU+01F9:ǹ 0 T render(\"Timesbd.ttf\")\nU+021E:Ȟ 0 T render(\"Timesbd.ttf\")\nU+021F:ȟ 0 T render(\"Timesbd.ttf\")\nU+0226:Ȧ 0 T render(\"Timesbd.ttf\")\nU+0227:ȧ 0 T render(\"Timesbd.ttf\")\nU+022E:Ȯ 0 T render(\"Timesbd.ttf\")\nU+022F:ȯ 0 T render(\"Timesbd.ttf\")\nU+0232:Ȳ 0 T render(\"Timesbd.ttf\")\nU+0233:ȳ 0 T render(\"Timesbd.ttf\")\nU+0218:Ș 0 T render(\"Timesbd.ttf\")\nU+0219:ș 0 T render(\"Timesbd.ttf\")\nU+021A:Ț 0 T render(\"Timesbd.ttf\")\nU+021B:ț 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_latin_extended_additional.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"Latin Extended Additional\" (U+1E00 to U+1EFF)\n# --------------------------------------------------\nU+1E02:Ḃ 0 T render(\"Timesbd.ttf\")\nU+1E03:ḃ 0 T render(\"Timesbd.ttf\")\nU+1E0A:Ḋ 0 T render(\"Timesbd.ttf\")\nU+1E0B:ḋ 0 T render(\"Timesbd.ttf\")\nU+1E1E:Ḟ 0 T render(\"Timesbd.ttf\")\nU+1E1F:ḟ 0 T render(\"Timesbd.ttf\")\nU+1E20:Ḡ 0 T render(\"Timesbd.ttf\")\nU+1E21:ḡ 0 T render(\"Timesbd.ttf\")\nU+1E22:Ḣ 0 T render(\"Timesbd.ttf\")\nU+1E23:ḣ 0 T render(\"Timesbd.ttf\")\nU+1E26:Ḧ 0 T render(\"Timesbd.ttf\")\nU+1E27:ḧ 0 T render(\"Timesbd.ttf\")\nU+1E30:Ḱ 0 T render(\"Timesbd.ttf\")\nU+1E31:ḱ 0 T render(\"Timesbd.ttf\")\nU+1E3E:Ḿ 0 T render(\"Timesbd.ttf\")\nU+1E3F:ḿ 0 T render(\"Timesbd.ttf\")\nU+1E40:Ṁ 0 T render(\"Timesbd.ttf\")\nU+1E41:ṁ 0 T render(\"Timesbd.ttf\")\nU+1E44:Ṅ 0 T render(\"Timesbd.ttf\")\nU+1E45:ṅ 0 T render(\"Timesbd.ttf\")\nU+1E54:Ṕ 0 T render(\"Timesbd.ttf\")\nU+1E55:ṕ 0 T render(\"Timesbd.ttf\")\nU+1E56:Ṗ 0 T render(\"Timesbd.ttf\")\nU+1E57:ṗ 0 T render(\"Timesbd.ttf\")\nU+1E58:Ṙ 0 T render(\"Timesbd.ttf\")\nU+1E59:ṙ 0 T render(\"Timesbd.ttf\")\nU+1E60:Ṡ 0 T render(\"Timesbd.ttf\")\nU+1E61:ṡ 0 T render(\"Timesbd.ttf\")\nU+1E6A:Ṫ 0 T render(\"Timesbd.ttf\")\nU+1E6B:ṫ 0 T render(\"Timesbd.ttf\")\nU+1E7C:Ṽ 0 T render(\"Timesbd.ttf\")\nU+1E7D:ṽ 0 T render(\"Timesbd.ttf\")\nU+1E80:Ẁ 0 T render(\"Timesbd.ttf\")\nU+1E81:ẁ 0 T render(\"Timesbd.ttf\")\nU+1E82:Ẃ 0 T render(\"Timesbd.ttf\")\nU+1E83:ẃ 0 T render(\"Timesbd.ttf\")\nU+1E84:Ẅ 0 T render(\"Timesbd.ttf\")\nU+1E85:ẅ 0 T render(\"Timesbd.ttf\")\nU+1E86:Ẇ 0 T render(\"Timesbd.ttf\")\nU+1E87:ẇ 0 T render(\"Timesbd.ttf\")\nU+1E8A:Ẋ 0 T render(\"Timesbd.ttf\")\nU+1E8B:ẋ 0 T render(\"Timesbd.ttf\")\nU+1E8C:Ẍ 0 T render(\"Timesbd.ttf\")\nU+1E8D:ẍ 0 T render(\"Timesbd.ttf\")\nU+1E8E:Ẏ 0 T render(\"Timesbd.ttf\")\nU+1E8F:ẏ 0 T render(\"Timesbd.ttf\")\nU+1E90:Ẑ 0 T render(\"Timesbd.ttf\")\nU+1E91:ẑ 0 T render(\"Timesbd.ttf\")\nU+1E97:ẗ 0 T render(\"Timesbd.ttf\")\nU+1E98:ẘ 0 T render(\"Timesbd.ttf\")\nU+1E99:ẙ 0 T render(\"Timesbd.ttf\")\nU+1EBC:Ẽ 0 T render(\"Timesbd.ttf\")\nU+1EBD:ẽ 0 T render(\"Timesbd.ttf\")\nU+1EF2:Ỳ 0 T render(\"Timesbd.ttf\")\nU+1EF3:ỳ 0 T render(\"Timesbd.ttf\")\nU+1EF8:Ỹ 0 T render(\"Timesbd.ttf\")\nU+1EF9:ỹ 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_misc.txt",
    "content": "# --------------------------------------------------\n# Unicode Block \"General Punctuation\" (U+2000 to U+206F)\n# --------------------------------------------------\nU+2013:– 0 T render(\"Timesbd.ttf\")\nU+2014:— 0 T render(\"Timesbd.ttf\")\nU+2018:‘ 0 T render(\"Timesbd.ttf\")\nU+2019:’ 0 T render(\"Timesbd.ttf\")\nU+201C:“ 0 T render(\"Timesbd.ttf\")\nU+201D:” 0 T render(\"Timesbd.ttf\")\nU+2020:† 0 T render(\"Timesbd.ttf\")\nU+2021:‡ 0 T render(\"Timesbd.ttf\")\nU+2022:• 0 T render(\"Timesbd.ttf\")\nU+2026:… 0 T render(\"Timesbd.ttf\")\nU+2030:‰ 0 T render(\"Timesbd.ttf\")\nU+2039:‹ 0 T render(\"Timesbd.ttf\")\nU+203A:› 0 T render(\"Timesbd.ttf\")\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Superscripts and Subscripts\" (U+2070 to U+209F)\n# --------------------------------------------------\nU+2074:⁴ 0 T render(\"Timesbd.ttf\")\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Currency Symbols\" (U+20A0 to U+20CF)\n# --------------------------------------------------\nU+20AC:€ 0 T render(\"Timesbd.ttf\")\nU+20AF:₯ 0 T render(\"Timesbd.ttf\")\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Letterlike Symbols\" (U+2100 to U+214F)\n# --------------------------------------------------\nU+2116:№ 0 T render(\"Timesbd.ttf\")\nU+2122:™ 0 T render(\"Timesbd.ttf\")\n\n\n\n# --------------------------------------------------\n# Unicode Block \"Alphabetic Presentation Forms\" (U+FB00 to U+FB4F)\n# --------------------------------------------------\nU+FB01:ﬁ 0 T render(\"Timesbd.ttf\")\nU+FB02:ﬂ 0 T render(\"Timesbd.ttf\")\n"
  },
  {
    "path": "data/tr3/glyphs/mapping_small.txt",
    "content": "# --------------------------------------------------\n# Small text\n# --------------------------------------------------\nU+0030:0 1 T render(\"Times.ttf\")\nU+0031:1 1 T render(\"Times.ttf\")\nU+0032:2 1 T render(\"Times.ttf\")\nU+0033:3 1 T render(\"Times.ttf\")\nU+0034:4 1 T render(\"Times.ttf\")\nU+0035:5 1 T render(\"Times.ttf\")\nU+0036:6 1 T render(\"Times.ttf\")\nU+0037:7 1 T render(\"Times.ttf\")\nU+0038:8 1 T render(\"Times.ttf\")\nU+0039:9 1 T render(\"Times.ttf\")\nU+002D:- 1 T render(\"Times.ttf\")\nU+002C:, 1 T render(\"Times.ttf\")\nU+00B0:° 1 T render(\"Times.ttf\")\n"
  },
  {
    "path": "data/tr3/mac/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CFBundleExecutable</key>\n    <string>TRX</string>\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n    <key>CFBundleIdentifier</key>\n    <string>com.lostartefacts.tr3x</string>\n    <key>CFBundleName</key>\n    <string>TR3X</string>\n    <key>CFBundleIconFile</key>\n    <string>icon</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "data/trx/icon.rc",
    "content": "id ICON \"{icon_path}\"\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"Deutsch\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Untersuchen\",\n            \"hide_dialog\": \"Dialog ausblenden\",\n            \"reset_defaults\": \"Auf Standard zurücksetzen\",\n            \"rotate\": \"Drehen\",\n            \"unbind\": \"Freigeben\",\n            \"use_item\": \"Benutzen\",\n        },\n        \"config_presets\": {\n            \"applied\": \"\\\\{review}Voreinstellung angewendet.\",\n            \"confirm_description\": \"\\\\{review}Die folgenden Einstellungen werden geändert:\",\n            \"confirm_restart_note\": \"\\\\{review}Hinweis: Einige Einstellungen erfordern möglicherweise einen Neustart des Spiels, um wirksam zu werden.\",\n            \"empty\": \"\\\\{review}Keine Voreinstellungen gefunden.\",\n            \"no_changes\": \"\\\\{review}Keine Änderungen anzuwenden.\",\n            \"title_fmt\": \"\\\\{review}Voreinstellung %s anwenden?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"\\\\{review}Bereich 1\",\n            \"area_2\": \"\\\\{review}Bereich 2\",\n            \"area_3\": \"\\\\{review}Bereich 3\",\n            \"area_4\": \"\\\\{review}Bereich 4\",\n            \"area_5\": \"\\\\{review}Bereich 5\",\n            \"area_6\": \"\\\\{review}Bereich 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"\\\\{review}Abenteuer\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"GAME OVER\",\n            \"heading_inventory\": \"INVENTAR\",\n            \"heading_items\": \"ITEMS\",\n            \"heading_option\": \"OPTION\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Demo Modus\",\n            \"direction_keys_controller\": \"Steuerkreuz\",\n            \"direction_keys_keyboard\": \"Pfeile\",\n            \"empty_slot_fmt\": \"- LEERER SLOT -\",\n            \"exit\": \"Beenden\",\n            \"hold_fmt\": \"Gedrückt halten %s\",\n            \"off\": \"Aus\",\n            \"on\": \"Ein\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Mehrdeutige Eingabe: %s und %s\",\n            \"ambiguous_input_3\": \"Mehrdeutige Eingabe: %s, %s, ...\",\n            \"bilinear_filter_off\": \"Bilinear-Filter: aus\",\n            \"bilinear_filter_on\": \"Bilinear-Filter: ein\",\n            \"command_bad_invocation\": \"Unzulässiger Aufruf: %s\",\n            \"command_bool\": \"ein, aus\",\n            \"command_decimal\": \"[Dezimal]\",\n            \"command_integer\": \"[Integer]\",\n            \"command_percent\": \"[Integer]\",\n            \"command_unavailable\": \"Dieses Kommando ist momentan nicht ausführbar\",\n            \"command_valid_values\": \"Zulässige Werte: %s\",\n            \"complete_level\": \"Level abgeschlossen!\",\n            \"config_option_get\": \"%s ist momentan eingestellt auf %s\",\n            \"config_option_set\": \"%s geändert zu %s\",\n            \"config_option_unknown_option\": \"Unbekannte Option: %s\",\n            \"current_health_get\": \"Laras aktuelle Lebensenergie: %d\",\n            \"current_health_set\": \"Laras Lebensenergie eingestellt auf %d\",\n            \"door_close\": \"Sesam, schließe dich!\",\n            \"door_open\": \"Sesam, öffne dich!\",\n            \"door_open_fail\": \"Keine Türen in Laras Nähe\",\n            \"flipmap_fail_already_off\": \"Flipmap ist bereits AUS\",\n            \"flipmap_fail_already_on\": \"Flipmap ist bereits AN\",\n            \"flipmap_off\": \"Flipmap \",\n            \"flipmap_on\": \"Flipmap eingeschaltet\",\n            \"fly_mode_off\": \"Flug-Modus ausgeschaltet\",\n            \"fly_mode_on\": \"Flug-Modus eingeschaltet\",\n            \"fps_counter_off\": \"FPS-Counter ausgeschaltet\",\n            \"fps_counter_on\": \"FPS-Counter eingeschaltet\",\n            \"give_item\": \"Es wurde %s zu Laras Inventar hinzugefügt.\",\n            \"give_item_all_guns\": \"Geladen und entsichert - Lara ist bis zu ihren Zähnen bewaffnet!\",\n            \"give_item_all_keys\": \"Überraschung! Jedes Schlüsselitem, dass Lara benötigt, ist jetzt in ihrem Inventar.\",\n            \"give_item_cheat\": \"Laras Rucksack ist schwerer geworden!\",\n            \"heal_already_full_hp\": \"Laras Gesundheit ist schon vollständig gefüllt.\",\n            \"heal_success\": \"Laras Gesundheit ist wieder vollständig wiederhergestellt.\",\n            \"invalid_cutscene\": \"Unzulässige Zwischensequenz\",\n            \"invalid_demo\": \"Unzulässige Demo\",\n            \"invalid_item\": \"Unbekanntes Item: %s\",\n            \"invalid_level\": \"Unzulässiger Level\",\n            \"invalid_object\": \"Unzulässiges Objekt\",\n            \"invalid_room\": \"Unzulässiger Raum: %d. Zulässige Räume sind 0-%d\",\n            \"invalid_sample\": \"Unzulässiger Sound: %d\",\n            \"kill\": \"Bye-bye!\",\n            \"kill_all\": \"Poof! %d Feinde verschwunden!\",\n            \"kill_all_fail\": \"Oh oh, es sind keine Feinde mehr zum Töten da...\",\n            \"kill_fail\": \"Kein Feind in der Nähe...\",\n            \"lighting_contrast_fmt\": \"Beleuchtungs-Kontrast: %s\",\n            \"load_game\": \"Spiel laden von folgendem Speicher-Slot %d\",\n            \"load_game_fail_invalid_slot\": \"Unzulässiger Speicher-Slot %d\",\n            \"load_game_fail_unavailable_slot\": \"Speicher-Slot %d ist nicht verfügbar\",\n            \"object_not_found\": \"Objekt nicht gefunden\",\n            \"play_cutscene\": \"Lade Zwischensequenz %d\",\n            \"play_demo\": \"Lade Demo %d\",\n            \"play_level\": \"Lade %s\",\n            \"pos_lara_missing\": \"Lara nicht vorhanden\",\n            \"pos_lara_pos_fmt\": \"Raum: %d\\nPosition: %.3f, %.3f, %.3f\\nRotation: %.3f, %.3f, %.3f\",\n            \"pos_level_fmt\": \"Level %d\",\n            \"pos_level_fmt_cutscene\": \"Zwischensequenz %d\",\n            \"pos_level_fmt_demo\": \"Demo %d\",\n            \"quick_load\": \"\\\\{review}Schnell geladener Slot %d\",\n            \"quick_load_fail_no_bound_slot\": \"\\\\{review}Kein Speicherplatz ist derzeit zugewiesen\",\n            \"quick_load_fail_unavailable_bound_slot\": \"\\\\{review}Der zugewiesene Speicherplatz ist nicht verfügbar\",\n            \"quick_save\": \"\\\\{review}Schnell gespeichert\",\n            \"quick_save_fail_no_slots\": \"\\\\{review}Keine Schnellspeicherplätze konfiguriert\",\n            \"save_game\": \"Spiel gespeichert auf Speicher-Slot %d\",\n            \"save_game_fail_invalid_slot\": \"Unzulässiger Speicher-Slot %d\",\n            \"sound_available_samples\": \"Verfügbare Sounds: %s\",\n            \"sound_playing_sample\": \"Spiele Sound: %d\",\n            \"speed_get\": \"Momentane Geschwindigkeit: %d\",\n            \"speed_set\": \"Geschwindigkeit eingestellt auf %d\",\n            \"strings_failed\": \"Neu Laden der Sprachdateien ist fehlgeschlagen\",\n            \"strings_reloaded\": \"Sprachdateien neu geladen\",\n            \"textures_off\": \"\\\\{review}Texturen: aus\",\n            \"textures_on\": \"\\\\{review}Texturen: an\",\n            \"trapezoid_filter_off\": \"Deaktiviert Trapezoid-Filter\",\n            \"trapezoid_filter_on\": \"Aktiviert Trapezoid-Filter\",\n            \"ui_off\": \"UI deakiviert\",\n            \"ui_on\": \"UI aktiviert\",\n            \"unknown_command\": \"Unbekanntes Kommando: %s\",\n            \"upscaling_factor\": \"Uscaling-Faktor: x%d\",\n            \"wireframe_mode_off\": \"Gitterrahmen-Modus: aus\",\n            \"wireframe_mode_on\": \"Gitterrahmen-Modus: ein\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"Animation: \",\n            \"debug_animation_state\": \"\\\\{review}Status: \",\n            \"debug_camera_pos\": \"\\\\{review}Kamerastandort: \",\n            \"debug_camera_target\": \"\\\\{review}Kameraziel: \",\n            \"debug_immune\": \"Unverwundbarkeit ein\",\n            \"debug_position\": \"Position: \",\n            \"debug_rotation\": \"Rotation: \",\n            \"debug_speed\": \"Geschwindigkeit: \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"\\\\{review}Löschen\",\n            \"delete_save_confirm\": \"\\\\{review}Diesen Speicherstand löschen?\",\n            \"delete_save_failed\": \"\\\\{review}Der ausgewählte Speicherstand konnte nicht gelöscht werden.\",\n            \"delete_save_no\": \"\\\\{review}Nein\",\n            \"delete_save_yes\": \"\\\\{review}Ja\",\n            \"exit_game\": \"Spiel beenden\",\n            \"exit_to_title\": \"Zurück zum Hauptmenü\",\n            \"load_game\": \"Spiel laden\",\n            \"mode_new_game\": \"Neues Spiel\",\n            \"mode_new_game_jp\": \"Japanisch NS\",\n            \"mode_new_game_jp_plus\": \"Japanisch NS+\",\n            \"mode_new_game_plus\": \"Neues Spiel+\",\n            \"new_game\": \"Neues Spiel\",\n            \"play_previous_levels\": \"\\\\{review}Vorherige Level spielen\",\n            \"restart_level\": \"Level Neu starten\",\n            \"save_game\": \"Spiel speichern\",\n            \"save_slot_unsupported\": \"\\\\{review}Dieser Speicherstand unterstützt diese Funktion nicht.\",\n            \"select_level\": \"Level auswählen\",\n            \"select_mod\": \"\\\\{review}Spiel auswählen\",\n            \"select_mode\": \"Modus Auswählen\",\n            \"select_save\": \"\\\\{review}Speicher auswählen\",\n            \"story_so_far\": \"Bisherige Geschichte...\",\n            \"switch_mod\": \"\\\\{review}Spiel wechseln\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"Sind sie sicher?\",\n            \"continue\": \"Fortsetzen\",\n            \"exit_to_title\": \"Wirklich zum Hauptmenü zurückkehren?\",\n            \"no\": \"Nein\",\n            \"paused\": \"Pause\",\n            \"quit\": \"Beenden\",\n            \"yes\": \"Ja\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"Frame Vorwärts\",\n            \"camera_move_prompt\": \"Kamera bewegen\",\n            \"camera_reset_prompt\": \"Kamera zurücksetzen\",\n            \"camera_roll_prompt\": \"Rolle Kamera\",\n            \"camera_rotate_90_prompt\": \"Rotiere um 90 Grad\",\n            \"camera_rotate_prompt\": \"Rotiere Kamera\",\n            \"change_lara_pose\": \"Pose ändern\",\n            \"fov_prompt\": \"FOV einstellen\",\n            \"lara_move_prompt\": \"\\\\{review}Bewege Lara\",\n            \"lara_reset_prompt\": \"\\\\{review}Setze Lara zurück\",\n            \"lara_roll_prompt\": \"\\\\{review}Rolle Lara\",\n            \"lara_rotate_90_prompt\": \"\\\\{review}Drehe Lara um 90°\",\n            \"lara_rotate_prompt\": \"\\\\{review}Drehe Lara\",\n            \"snap_prompt\": \"Bild aufnehmen\",\n            \"title_camera_pos\": \"Foto-Modus\",\n            \"title_lara_pos\": \"\\\\{review}Lara bewegen\",\n            \"toggle_help\": \"Hilfe Ein/Aus\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"\\\\{review}Die Einstellungen sind für dieses Level-Set deaktiviert.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Wert bearbeiten\",\n                \"frozen_option_disclaimer\": \"Diese Einstellung ist vom Ersteller des Levels erzwungen und kann nicht geändert werden.\",\n                \"hue\": \"Farbton\",\n                \"lightness\": \"Helligkeit\",\n                \"restore_default\": \"Standard wiederherstellen\",\n                \"toggle_help\": \"Hilfe Ein/Aus\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Kontroller\",\n                    \"keyboard\": \"Tastatur\",\n                },\n                \"customize\": \"Steuerung anpassen\",\n                \"layout\": {\n                    \"custom_1\": \"Eigene Steuerung 1\",\n                    \"custom_2\": \"Eigene Steuerung 2\",\n                    \"custom_3\": \"Eigene Steuerung 3\",\n                    \"default\": \"Standardtasten\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Bewegung\",\n                    \"items\": \"Items\",\n                    \"misc\": \"Sonstiges\",\n                    \"system\": \"System\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Steuerung\",\n                    \"fixes\": \"Fixes\",\n                    \"general\": \"Allgemeines\",\n                    \"mods\": \"Mods\",\n                    \"presets\": \"\\\\{review}Voreinstellungen\",\n                },\n                \"title\": \"Gameplay-Optionen\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"\\\\{review}Leisten\",\n                    \"rendering\": \"\\\\{review}Rendern\",\n                    \"stats\": \"\\\\{review}Statistiken\",\n                    \"ui\": \"\\\\{review}Benutzeroberfläche\",\n                    \"visuals\": \"\\\\{review}Darstellung\",\n                },\n                \"title\": \"Grafik Optionen\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"\\\\{review}Verschiedenes\",\n                    \"volume\": \"\\\\{review}Lautstärke\",\n                },\n                \"title\": \"Sound-Optionen\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Munition Treffer/Verbraucht\",\n            \"ammo_hits\": \"Treffer\",\n            \"ammo_used\": \"Benötigte Munition\",\n            \"assault_best_time_fmt\": \"\\\\{review}%s\",\n            \"assault_finish\": \"Zeit\",\n            \"assault_no_times_set\": \"Keine Zeiten gesetzt\",\n            \"assault_other_times_fmt\": \"\\\\{review}%s\",\n            \"assault_title\": \"Bestzeiten\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Bonusstatistiken\",\n            \"crystals\": \"\\\\{review}Kristalle\",\n            \"deaths\": \"Tode\",\n            \"detail_fmt\": \"%d von %d\",\n            \"distance_travelled\": \"Zurückgelegte Distanz\",\n            \"final_statistics\": \"Finale Statistiken\",\n            \"gym_assault_course\": \"\\\\{review}Angriffskurs\",\n            \"gym_racetrack_course\": \"\\\\{review}Rennstrecke\",\n            \"kills\": \"Besiegte Gegner\",\n            \"level\": \"Level\",\n            \"medipacks_used\": \"Benötigte Medi-Packs\",\n            \"none\": \"None\",\n            \"pickups\": \"Pickups\",\n            \"secrets\": \"Gefundene Geheimnisse\",\n            \"time_taken\": \"Benötigte Zeit\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Aktiviert und deaktiviert Laras Zopf.\",\n            },\n            \"cheats\": {\n                \"help\": \"Schaltet In-Game Cheats ein oder aus.\",\n            },\n            \"clear\": {\n                \"help\": \"\\\\{review}Löscht sichtbare Konsolenprotokolle.\",\n            },\n            \"debug\": {\n                \"help\": \"Aktiviert und deaktiviert visuele Debuginformationen.\",\n            },\n            \"drain\": {\n                \"help\": \"Entfernt das gesamte Wasser aus dem Raum, in dem sie sich im Moment befinden.\",\n            },\n            \"end_level\": {\n                \"help\": \"Beendet das aktuelle Level.\",\n            },\n            \"exit\": {\n                \"help\": \"Beendet das Spiel.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Aktiviert und deaktiviert die Flip-Map.\",\n            },\n            \"flood\": {\n                \"help\": \"Überflutet den Raum, in dem sie sich im Moment befinden, mit Wasser.\",\n            },\n            \"fly\": {\n                \"help\": \"Aktiviert und deaktiviert den Flug-Modus-Cheat.\",\n            },\n            \"fps\": {\n                \"help\": \"Erhöht oder senkt den FPS-Wert.\",\n            },\n            \"give\": {\n                \"help\": \"Fügt ein angegebenes Item zu Laras Inventar hinzu.\",\n                \"invalid_secret\": \"Unzulässiges Geheimnis: #%d (zulässige Geheimnisse: %s)\",\n                \"secret_given\": \"Geheimnis hinzugefügt %s\",\n                \"secret_list\": \"Gesammelte Geheimnisse: %d von %d (%s)\",\n                \"secret_none\": \"Gesammelte Geheimnisse: %d von %d\",\n                \"secret_taken\": \"Geheimnis enfernt %s\",\n            },\n            \"give_secret\": {\n                \"help\": \"Listet Laras Geheimnisse auf oder nimmt/gibt ein nach Nummern sortiertes Geheimnis.\",\n            },\n            \"heal\": {\n                \"help\": \"Heilt Lara zurück auf volle Lebensenergie.\",\n            },\n            \"help\": {\n                \"help\": \"Zeigt Hilfe für alle Kommandos oder detaillierte Hilfe für ein bestimmtes.\",\n                \"list\": \"Verfügbare Kommandos:\",\n            },\n            \"hp\": {\n                \"help\": \"Stellt Laras Lebensenergie auf den bestimmten Wert ein.\",\n            },\n            \"immune\": {\n                \"help\": \"Aktiviet oder deaktiviert Unverwundbarkeit. (Lara kann unter bestimmten Umständen immernoch sterben.)\",\n                \"off\": \"Lara ist nun verwundbar\",\n                \"on\": \"Lara ist nun unverwundbar\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"\\\\{review}Schaltet unendliches Sprinten um.\",\n                \"off\": \"\\\\{review}Lara kann nicht mehr ewig sprinten\",\n                \"on\": \"\\\\{review}Lara kann jetzt ewig sprinten\",\n            },\n            \"kill\": {\n                \"help\": \"Tötet in der Nähe befindliche Feinde.\",\n            },\n            \"lighting\": {\n                \"help\": \"Aktiviet oder deaktiviert Beleuchtungs-Sytem.\",\n            },\n            \"load\": {\n                \"help\": \"\\\\{review}Lädt das Spiel aus dem angegebenen Speicherplatz oder von einem Schnell-Speicher.\",\n            },\n            \"lua\": {\n                \"help\": \"\\\\{review}Führt den angegebenen Lua-Code-String aus.\",\n                \"runtime_error\": \"\\\\{review}Lua-Laufzeitfehler: %s\",\n                \"syntax_error\": \"\\\\{review}Lua-Syntaxfehler: %s\",\n            },\n            \"mod\": {\n                \"help\": \"\\\\{review}Wechselt zum angegebenen Mod und startet das Spiel neu.\",\n            },\n            \"music\": {\n                \"help\": \"Spielt Musik-Titel mit dazugehöriger ID ab.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Spielt Zwischensequenz mit dazugehöriger Nummer ab.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Spielt eine Demo mit der angegebenen Nummer ab.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Startet das Gym-Level.\",\n            },\n            \"play_level\": {\n                \"help\": \"Startet einen Level mit dem angegebenen Namen oder der dazugehörigen Nummer.\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Unzulässiger Musik Titel\",\n                \"stopped\": \"\\\\{review}Musik gestoppt\",\n                \"track\": \"Spiele Musiktitel %d\",\n            },\n            \"pos\": {\n                \"help\": \"Zeigt Laras Position an.\",\n            },\n            \"save\": {\n                \"help\": \"\\\\{review}Speichert das Spiel im angegebenen Speicherplatz oder im nächsten Schnell-Speicherplatz.\",\n            },\n            \"screenshot\": {\n                \"help\": \"\\\\{review}Speichert einen Screenshot auf der Festplatte, mit optionalem Pfad.\",\n            },\n            \"set\": {\n                \"help\": \"Aktualisiert oder zeigt die angegebene Konfigurations-Einstellung an.\",\n            },\n            \"sfx\": {\n                \"help\": \"Spielt einen Sound-Effekt mit der angegebenen ID ab.\",\n            },\n            \"spawn\": {\n                \"fail\": \"\\\\{review}Erzeugen des angeforderten Objekts fehlgeschlagen\",\n                \"success\": \"\\\\{review}Angefordertes Objekt in der Nähe von Lara erzeugt\",\n            },\n            \"speed\": {\n                \"help\": \"Ändert die Geschwindigkeit des Spiels.\",\n            },\n            \"strings\": {\n                \"help\": \"Lädt erneut die aktuellen Sprachdateien von der Festplatte.\",\n            },\n            \"teleport\": {\n                \"item\": \"Teleportiert zu Objekt: %d\",\n                \"item_fail\": \"Fehlgeschlagene Teleportation zu Objekt: %d\",\n                \"object\": \"Teleportiert zu Objekt: %s\",\n                \"object_fail\": \"Fehlgeschlagene Teleportation zu Objekt: %s\",\n                \"pos\": \"Teleportiere zu Position: %.3f %.3f %.3f\",\n                \"pos_fail\": \"Fehlgeschlagene Teleportation zu Position: %.3f %.3f %.3f\",\n                \"room\": \"Teleportiere zu Raum: %d\",\n                \"room_fail\": \"Teleportation zu Raum: %d\",\n            },\n            \"textures\": {\n                \"help\": \"\\\\{review}Schaltet die Texturen ein oder aus.\",\n            },\n            \"title\": {\n                \"help\": \"Bringt sie zurück zum Titel-Bildschirm.\",\n            },\n            \"tp\": {\n                \"help\": \"Teleportiert Lara zu der angegebenen Position oder Raum-Nummer.\",\n            },\n            \"trigger\": {\n                \"help\": \"\\\\{review}Löst ein Element durch ID, Elementname oder Objektnamen aus oder setzt es zurück.\",\n                \"invalid_item\": \"\\\\{review}Ungültiges Element: %s\",\n                \"no_match\": \"\\\\{review}Unbekanntes Ziel: %s\",\n                \"not_found\": \"\\\\{review}Keine passenden Elemente gefunden für: %s\",\n                \"triggered\": \"\\\\{review}Ausgelöstes Element(e): %s\",\n                \"untriggered\": \"\\\\{review}Nicht ausgelöstes Element(e): %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Vertical Sync. Ein/Aus\",\n            },\n            \"weather\": {\n                \"help\": \"\\\\{review}Ändert den aktuellen Wettertyp.\",\n                \"invalid\": \"\\\\{review}Ungültiges Wetter: %s (gültig: %s)\",\n                \"set\": \"\\\\{review}Wetter auf %s eingestellt\",\n            },\n            \"winston\": {\n                \"dead\": \"Der Butler ist tot. Du Monster!\",\n                \"spawn_failed\": \"Herbeirufen von Winston fehlgeschlagen\",\n                \"spawned\": \"Winston herbeigerufen, nahe Lara\",\n                \"teleported\": \"Winston herbeigerufen, nahe Lara\",\n            },\n            \"wireframe\": {\n                \"help\": \"Aktiviert und deaktiviert den Gitterrahmen-Render.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"\\\\{review}TR1 PC\",\n            \"tr1_ps1\": \"\\\\{review}TR1 PS1\",\n            \"tr2_pc\": \"\\\\{review}TR2 PC\",\n            \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n            \"tr3_pc\": \"\\\\{review}TR3 PC\",\n            \"tr3_ps1\": \"\\\\{review}TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"\\\\{review}TR2 PC\",\n                \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n                \"tr3_pc\": \"\\\\{review}TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"\\\\{review}Standard\",\n                \"golden_sophia\": \"\\\\{review}Goldene Sophia\",\n                \"sophia\": \"\\\\{review}Sophia\",\n                \"tr1_bacon_lara\": \"\\\\{review}Bacon-Lara\",\n                \"tr1_classic\": \"\\\\{review}TR1 Klassisch\",\n                \"tr1_combo\": \"\\\\{review}TR1 Kombiniert\",\n                \"tr1_golden_bacon_lara\": \"\\\\{review}Goldene Bacon-Lara\",\n                \"tr1_golden_lara\": \"\\\\{review}TR1 Goldene Lara\",\n                \"tr1_gym\": \"\\\\{review}TR1 Trainingsanzug\",\n                \"tr1_mauled\": \"\\\\{review}TR1 Verletzt\",\n                \"tr1_ngage\": \"\\\\{review}TR1 N-Gage\",\n                \"tr23_golden_lara\": \"\\\\{review}TR2/3 Goldene Lara\",\n                \"tr2_bomber_jacket\": \"\\\\{review}Bomberjacke\",\n                \"tr2_classic\": \"\\\\{review}TR2 Klassisch\",\n                \"tr2_diving_suit\": \"\\\\{review}Tauchanzug 1\",\n                \"tr2_diving_suit_alpha\": \"\\\\{review}Tauchanzug 2\",\n                \"tr2_gym\": \"\\\\{review}TR2 Trainingsanzug\",\n                \"tr2_robe\": \"\\\\{review}Robe\",\n                \"tr2_vegas\": \"\\\\{review}Las Vegas\",\n                \"tr3_antarctica\": \"\\\\{review}Antarktis\",\n                \"tr3_catsuit\": \"\\\\{review}Catsuit\",\n                \"tr3_classic\": \"\\\\{review}TR3 Klassisch\",\n                \"tr3_gym\": \"\\\\{review}TR3 Trainingsanzug\",\n                \"tr3_nevada\": \"\\\\{review}Nevada\",\n                \"tr3_south_pacific\": \"\\\\{review}Südpazifik\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"\\\\{review}Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"\\\\{review}Tomb Raider I Demo\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"\\\\{review}Unvollendete Angelegenheiten\",\n            },\n            \"tr2\": {\n                \"title\": \"\\\\{review}Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"\\\\{review}Die Goldene Maske\",\n            },\n            \"tr3\": {\n                \"title\": \"\\\\{review}Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"\\\\{review}Das Verlorene Artefakt\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"\\\\{review}Individuell\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"\\\\{review}Geteilt\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"\\\\{review}Auto\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"\\\\{review}Schwarz\",\n            \"BK_IMAGE\": \"\\\\{review}Bild\",\n            \"BK_MONOCHROME\": \"\\\\{review}Monochrom\",\n            \"BK_MONOCHROME_COOL\": \"\\\\{review}Monochrom (kühl)\",\n            \"BK_MONOCHROME_WARM\": \"\\\\{review}Monochrom (warm)\",\n            \"BK_NONE\": \"\\\\{review}Transparent\",\n            \"BK_PATTERN_STATIC\": \"\\\\{review}Statisch\",\n            \"BK_PATTERN_WAVE\": \"\\\\{review}Welle\",\n            \"BK_TRANSPARENT_DARK\": \"\\\\{review}Sehr dunkel\",\n            \"BK_TRANSPARENT_MEDIUM\": \"\\\\{review}Dunkel\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"Immer\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"Nur Boss-Gegner\",\n            \"BAR_SHOW_MODE_NEVER\": \"Nie\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"\\\\{review}Keine\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"\\\\{review}Perspektive\",\n            \"BILLBOARD_LOCK_ROLL\": \"\\\\{review}Roll\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"\\\\{review}Roll & Neigung\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"\\\\{review}Deaktiviert\",\n            \"BLOOD_EFFECTS_PINK\": \"\\\\{review}Pink\",\n            \"BLOOD_EFFECTS_RED\": \"\\\\{review}Rot\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"\\\\{review}TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"\\\\{review}Standard\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"\\\\{review}Nie\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"\\\\{review}Untergetaucht\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"\\\\{review}Controller\",\n            \"INPUT_BACKEND_KEYBOARD\": \"\\\\{review}Tastatur\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Aktion\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Kamera zurück\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Kamera runter\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Kamera Vorwärts\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Kamera Links\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"Kamera zurücksetzen\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Kamera Rechts\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Kamera Hoch\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"\\\\{review}Outfit wechseln\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Ziel wechseln\",\n            \"INPUT_ROLE_CROUCH\": \"\\\\{review}Ducken\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"Beleuchtungs-Kontrast durchschalten\",\n            \"INPUT_ROLE_DOWN\": \"Zurück\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Waffe ziehen\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Entwickler-Konsole\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"Automatik-Pistolen ausrüsten\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"\\\\{review}Desert Eagle ausrüsten\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"Granatenwerfer ausrüsten\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"Harpune ausrüsen\",\n            \"INPUT_ROLE_EQUIP_M16\": \"M16-ausrüsten\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"Magnums ausrüsten\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"\\\\{review}MP5 ausrüsten\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"Pistolen ausrüsten\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"\\\\{review}Raketenwerfer ausrüsten\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"Schrotflinte ausrüsten\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"Uzis ausrüsten\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Flug-Cheat\",\n            \"INPUT_ROLE_FPS\": \"FPS anzeigen\",\n            \"INPUT_ROLE_INVENTORY\": \"Inventar\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Item Cheat\",\n            \"INPUT_ROLE_JUMP\": \"Springen\",\n            \"INPUT_ROLE_LEFT\": \"Links\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Level überspringen\",\n            \"INPUT_ROLE_LOAD\": \"\\\\{review}Laden\",\n            \"INPUT_ROLE_LOOK\": \"Umsehen\",\n            \"INPUT_ROLE_PAUSE\": \"Pause\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"\\\\{review}Schnellladen\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"\\\\{review}Schnellspeichern\",\n            \"INPUT_ROLE_RIGHT\": \"Rechts\",\n            \"INPUT_ROLE_ROLL\": \"Rolle\",\n            \"INPUT_ROLE_SAVE\": \"\\\\{review}Speichern\",\n            \"INPUT_ROLE_SCREENSHOT\": \"Bildschirmfoto\",\n            \"INPUT_ROLE_SLOW\": \"Gehen\",\n            \"INPUT_ROLE_SPRINT\": \"\\\\{review}Sprinten\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Seit-Schritt Links\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Seit-Schritt Rechts\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"Wechsel zwischen Rahmen-Größen\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"Wechsel zwischen Upscaling-Faktoren\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"Bilinear-Filter Ein/Aus\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"Vollbild Ein/Aus\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Foto-Modus Ein/Aus\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"\\\\{review}Texturen umschalten\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"Trapezoid-Filter Ein/Aus\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"UI Ein/Aus\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"Drahtgitter Ein/Aus\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Turbo Geschwindigkeit\",\n            \"INPUT_ROLE_UP\": \"Laufen\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Großes Medi-Pack\",\n            \"INPUT_ROLE_USE_FLARE\": \"Fackel\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Kleines Medi-Pack\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"Deaktiviert\",\n            \"JUMP_LOCK_LEGACY\": \"Original\",\n            \"JUMP_LOCK_TUNED\": \"Getuned\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"Hoch\",\n            \"LIGHTING_CONTRAST_LOW\": \"Niedrig\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Mittel\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"\\\\{review}Immer\",\n            \"LOADING_SCREENS_DISABLED\": \"\\\\{review}Deaktiviert\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"\\\\{review}Neue Spiele\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"Erweitert\",\n            \"LOOK_MODE_RESTRICTED\": \"Beschränkt\",\n            \"LOOK_MODE_UNRESTRICTED\": \"Unbeschränkt\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"Immer\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"Nie\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Nicht-Umgebung\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"\\\\{review}Mehrfachfeger\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"\\\\{review}Einzelfeger\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"Ziehen oder wegstecken\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"Nur ziehen\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"\\\\{review}Kreis\",\n            \"SHADOW_TYPE_OCTAGON\": \"\\\\{review}Achteck\",\n            \"SHADOW_TYPE_SPRITE\": \"\\\\{review}Sprite\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"\\\\{review}Einfach\",\n            \"STATS_STYLE_BORDERED\": \"\\\\{review}Mit Rahmen\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"\\\\{review}Aus\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"\\\\{review}Undurchsichtig\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"\\\\{review}Transparent\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Volles Lock-On\",\n            \"TARGET_LOCK_MODE_NONE\": \"Kein Lock-On\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Halbes Lock-On\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Bilinear\",\n            \"TEXTURE_FILTER_POINT\": \"Aus\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"Unten Mitte\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"Unten links\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"Unten rechts\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"Oben Mitte\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"Oben links\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"Oben rechts\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"Gefixt\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"\\\\{review}Umgebungslautstärke\",\n            \"description\": \"\\\\{review}Passt die Umgebungslautstärke an.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"\\\\{review}Zwischensequenz-Lautstärke\",\n            \"description\": \"\\\\{review}Passt die Lautstärke der Zwischensequenzen im Spiel an.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Mikrofon nah an Lara\",\n            \"description\": \"Setzt das Mikrofon auf Laras Position. Wenn deaktiviert, wird das Mikrofon auf der Position der Kamera gesetzt.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"\\\\{review}Musik im Inventar abspielen\",\n            \"description\": \"\\\\{review}Lässt Spielgeräusche, Umgebungsgeräusche und Musik im Inventarbildschirm weiterlaufen.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Hauptmenü-Musik\",\n            \"description\": \"Spielt Musik im Hauptmenü ab.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Gepitchte Sounds\",\n            \"description\": \"Erlaubt die zufällige Wiedergabe von Sound-Effekten, leicht gepitched, um die Spiel-Sounds zu variieren.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"\\\\{review}PS1 SFX-Ersetzungen\",\n            \"description\": \"\\\\{review}Aktiviert bestimmte Soundeffekt-Ersetzungen mit PS1-Äquivalenten.\\n\\n- Uzi-Feuer (nur TR1)\\n- Laras Barfußgeräusche (nur TR2)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"Tauch-Animations Sounds\",\n            \"description\": \"Erlaubt Kontrolle über das Abspielen von bestimmten Animationen und Souneffekten - für Objekte, wie zum Beispiel Türen und Falltüren - wenn die Kamera unter Wasser ist.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Fix: Falsch abspielender Sound\",\n            \"description\": \"Verhindert, dass der Geheimnis-Ton inkorrekterweise abgespielt wird, wenn man den Goldenen Schlüssel im Grab von Tihocan benutzt.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Überlagernde Geheimnis-Musik\",\n            \"description\": \"Fixt dass der Ton vom Geheimnis-Einsammeln den momentan spielenden Musik-Titel unterbricht.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Überlagernde Feind-Sprache\",\n            \"description\": \"Fixt dass Feinde beim Sprechen den momentan spielenden Musiktitel unterbrechen.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"\\\\{review}FMV-Lautstärke\",\n            \"description\": \"Passt die Lautstärke der Videosequenzen an.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"\\\\{review}Umgebungslautstärke (Inventar)\",\n            \"description\": \"\\\\{review}Passt die Umgebungslautstärke im Inventarbildschirm an.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"\\\\{review}Musiklautstärke (Inventar)\",\n            \"description\": \"\\\\{review}Passt die Musiklautstärke im Inventarbildschirm an.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Fix: Trigger für Musik, die nur einmal spielen soll\",\n            \"description\": \"Lädt vorher getriggerte Musik, so dass Musik, die nur einmal spielen soll, nicht ein weiteres Mal abgespielt wird.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{review}\\\\{icon music} Hauptlautstärke\",\n            \"description\": \"\\\\{review}Passt die gesamte Spiel-Lautstärke an. Die restlichen Einstellungen sind relativ zu dieser Lautstärke.\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Musik beim Laden wiederherstellen\",\n            \"description\": \"Lädt den Musiktitel der vor dem Speichern des Spiels abgespielt wurde.\\n\\n- Nie: keine Musik-Titel beim laden wiederherstellen.\\n- Nicht-Umgebung: stellt nur Nicht-Umgebungs-Musiktitel beim laden wieder her.\\n- Immer: stellt jede Art von Musiktitel beim Laden wieder her.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"\\\\{review}Musiklautstärke\",\n            \"description\": \"\\\\{review}Passt die Musiklautstärke an.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"Ton stummschalten bei Fokusverlust\",\n            \"description\": \"Schaltet alle Musik und Soundeffekte stumm wenn das Fenster des Spiels nicht fokusiert ist.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Sound-Lautstärke\",\n            \"description\": \"Soundeffekt-Lautstärke einstellen.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"\\\\{review}Umgebungslautstärke (unter Wasser)\",\n            \"description\": \"\\\\{review}Passt die Umgebungslautstärke unter Wasser an.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"\\\\{review}Musiklautstärke (unter Wasser)\",\n            \"description\": \"\\\\{review}Passt die Musiklautstärke unter Wasser an.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"\\\\{review}Unendliche Fackelzeit\",\n            \"description\": \"\\\\{review}Verhindert, dass die tragbaren Fackeln jemals erlöschen. Geworfene Fackeln erlöschen jedoch weiterhin wie gewohnt.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"\\\\{review}Endloses Sprinten\",\n            \"description\": \"\\\\{review}Verhindert, dass Lara beim Sprinten jemals müde wird. Hindernisse bringen sie jedoch weiterhin zum Stillstand.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"\\\\{review}Verbündeten-Feindseligkeitspolitik\",\n            \"description\": \"\\\\{review}Steuert, wie freundliche Einheiten reagieren, wenn sie Schaden nehmen.\\n\\n- Individuell: Jeder Verbündete ändert seine Feindseligkeit eigenständig (TR3-Stil).\\n- Geteilt: Alle Verbündeten werden gemeinsam feindlich (TR2-Mönch-Stil).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Kamera-Geschwindigkeit\",\n            \"description\": \"Ändert, wie schnell sich die Kamera bewegt.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Ändere Pierre Spawn-Modus\",\n            \"description\": \"Erstellt einen gerade erst ausgelösten (flüchtenden) Pierre und ersetzt einen bereits existierenden (flüchtenden) Pierre.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"\\\\{review}Kreaturen-Ertrinkverhalten\",\n            \"description\": \"\\\\{review}Steuert, wie sich Landkreaturen in Wasserräumen verhalten.\\n\\n- Nie: Landkreaturen ertrinken niemals (TR1-Stil).\\n- Standard: Landkreaturen ertrinken in Wasser mit einer Tiefe von 2 Klicks oder mehr (TR2/3-Stil).\\n- Untergetaucht: Landkreaturen ertrinken nur, wenn sie vollständig untergetaucht sind.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"\\\\{review}Entferne zusätzliche Waffen\",\n            \"description\": \"Entfernt alle Waffen- und Munitions-Pickups aus dem Spiel, außer Pistolen (für Pistolen-Only-Challenge-Läufe).\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Bleibender Schaden\",\n            \"description\": \"Verhindert, dass Lara beim Starten eines neuen Levels geheilt wird (für \\\"No Heal Challenge\\\" Runs).\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Entferne Medi-Packs\",\n            \"description\": \"Entfernt alle Medi-Packs aus dem Spiel (für \\\"No Meds Challenge\\\" Runs).\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Enferne die Kollision des toten T-Rex\",\n            \"description\": \"Enfernt jegliche Kollision vom T-Rex sobald er stirbt. Dies ist sehr hilfreich, wenn die Leiche eines T-Rex den Ausgang blockiert.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"Erlaube das ziehlen auf freundliche Einheiten\",\n            \"description\": \"Erlaubt Lara, auf freundliche Einheiten zu ziehlen, wie zum Beispiel Mönche. Wenn diese Option deaktiviert ist, sind freundliche Einheiten immun gegen Laras Munition.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Vorausgewählte Schlüssel-Items\",\n            \"description\": \"Wenn Lara ein Schlüsselloch oder einen Rätsel-Slot verwendet und sie das dazu passende Item in ihrem Inventar hat, wird dieses automatisch ausgewählt.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"\\\\{review}Stolpern an rückwärtiger Schräge\",\n            \"description\": \"\\\\{review}Lässt Lara stolpern, wenn sie rückwärts springt und sich hinter ihr eine Schräge befindet (TR3). Wenn deaktiviert, kommt Lara abrupt an der Schräge zum Stehen (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"\\\\{review}Leichensack-Auslöser\",\n            \"description\": \"\\\\{review}Ermöglicht das Entfernen getöteter Gegner, wenn Lara in bestimmten Levels bestimmte Auslöser überquert. Wenn deaktiviert, werden tote Gegner immer angezeigt.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"\\\\{review}Felskamerawackeln aktivieren\",\n            \"description\": \"\\\\{review}Wenn aktiviert, wackelt die Kamera, sobald sich ein Fels bewegt.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"\\\\{review}Sprunggranaten\",\n            \"description\": \"\\\\{review}Aktiviert das Granatenverhalten im Stil von TR3: Sie prallen von Wänden und Hängen ab und erzeugen einen größeren Explosionsradius, jedoch auf Kosten einer reduzierten Geschwindigkeit.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Cheats\",\n            \"description\": \"Aktiviert verschiedene Cheats:\\n\\n- L: Beende das Level sofort.\\n- I: eine Erhöhung der Anzahl der Munition und Medipacks; und alle plotrelevanten Items des aktuellen Levels.\\n- O: Aktiviere Flug-Cheat (in der Luft schwimmen).\\n  - GEHEN-Taste: beende Flug-Modus.\\n  - WAFFE ZIEHEN-Taste: öffne die nächstgelegene Tür (funktioniert an ein paar Stellen nicht).\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"\\\\{review}Skriptsequenzen\",\n            \"description\": \"\\\\{review}Aktiviert Skriptsequenzen zu Beginn bestimmter Level, sofern vorhanden.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Levelstatistiken im Kompass\",\n            \"description\": \"Aktiviert die Anzeige von Levelstatistiken wenn der Kompass ausgewählt wird.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Konsole\",\n            \"description\": \"Aktiviert die Entwickler-Konsole.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"\\\\{review}Kontrollierter Fall\",\n            \"description\": \"\\\\{review}Ermöglicht Lara, sich in der Luft zu drehen und den Vorsprung zu greifen, den sie gerade verlassen hat, wenn die Aktionstaste beim Fallen gehalten wird.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"\\\\{review}Crawl-Absprung\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, aus Kriechgängen herauszuspringen.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"\\\\{review}Krabbelneigung\",\n            \"description\": \"\\\\{review}Richtet Laras Rotation beim Krabbeln an der Bodengeometrie aus.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"\\\\{review}Kriechen\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, sich zu ducken und zu kriechen.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"Credits\",\n            \"description\": \"Ermöglicht die Anzeige der Credits nachdem man das Spiel durchgespielt hat. Beeinflusst nicht die Anzeige der Finalen Statistik.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"\\\\{review}Hockrolle\",\n            \"description\": \"\\\\{review}Erlaubt Lara, aus der Hocke mit der Sprinttaste eine Vorwärtsrolle zu machen.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Zwischensequenzen\",\n            \"description\": \"Aktiviert das Abspielen von Zwischensequenzen.\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Demo-Modus\",\n            \"description\": \"Aktiviert die Anzeige von Demos im Hauptmenü.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Zufällige Startwinkel für Gegner\",\n            \"description\": \"Verwendet einen zusätzlichen zufälligen Startwinkel für ein paar Gegner, sobald sie initialisiert werden.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Speichere Effekte\",\n            \"description\": \"Erweitert Spielstände so das Grafikeffekte, Wasserfall-Nebel, Flammenerzeuger und mehr gespeichert werden, anstatt beim Laden zu verschwinden.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"FMVs\",\n            \"description\": \"Aktiviert das Abspielen von FMVs.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Spielmodi-Auswahl\",\n            \"description\": \"Erlaubt die Auswahl der 'Neues Spiel Plus'-Optionen im 'Neues Spiel' Ausweis-Menü.\\n\\n- Neues Spiel+: schaltet alle Waffen mit unendlich Munition frei; Feinde haben verdoppelte Lebenserenergie.\\n- Japanisch NS: Waffen verursachen doppelten Schaden und Fackel-Pickups enhalten 8 statt 6 Fackeln.\\n- Japanisch NS+: eine Kombination aus Neus Spiel+ und Japanisch NS.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"\\\\{review}Leerlauf-Kamera\",\n            \"description\": \"\\\\{review}Passt die Kamera an, sodass sie während der Posenanimation auf Lara gerichtet ist. Durch Drücken von Schauen wird die Kamera zurückgesetzt.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Invertiertes Umsehen\",\n            \"description\": \"Invertiert die Y-Axe von Laras Sichtsteuerung.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Item-Untersuchung\",\n            \"description\": \"Für benutzergenerierte Level - erlaubt die Anzeige von Item-Beschreibungen im Inventar, wenn der Ersteller des Levels die nötigen Daten zur Verfügung gestellt hat.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Sprungdrehungen\",\n            \"description\": \"Aktiviert TR2+-artige Sprungdrehungen und Purzelbäume, das heißt: Drücke Rolle während des Sprungs und Kopfsprung-Animationen.\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"\\\\{review}Tödliche Schiebeblöcke aktivieren\",\n            \"description\": \"Aktiviert, dass Lara sofort stirbt, wenn ein Schiebeblock von oben auf Lara herab fällt. Andernfalls wird Lara oben auf dem Block hängen bleiben und überleben.\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Schmales Springen\",\n            \"description\": \"Erlaubt Lara, sich weiter voran oder rückwärts zu tasten, wenn man eine der Richtungstasten gedrückt hält, während sie einen neutralen Sprung ausführt.\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"\\\\{review}Vorsprung-Sprünge\",\n            \"description\": \"\\\\{review}Ermöglicht Lara, nach oben oder rückwärts zu springen, während sie an einem Vorsprung hängt, sofern sich eine feste Fläche vor ihr befindet, um sich abzustoßen.\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Rechtshinweise\",\n            \"description\": \"Aktiviert die Rechtshinweise und die Core Design FMV beim Starten des Spiels.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"\\\\{review}Manuelle Kamera\",\n            \"description\": \"\\\\{review}Aktiviert die Kameratasten (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}), die zur Steuerung der Kamera im Fotomodus verwendet werden, um auch die Ingame-Kamera zu drehen.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"\\\\{review}Neutrale Drehung\",\n            \"description\": \"\\\\{review}Ermöglicht Lara eine Drehung in der Luft bei einem Sprung aus dem Stand. Drücke gleichzeitig die Sprung- und Rollentaste, während du stillstehst.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Gegenstände hervorheben\",\n            \"description\": \"Aktiviert einen periodischen Funkel-Effekt in der Nähe von Items, um ihre Gegenwart sichtbarer zu machen.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"\\\\{review}Vorherige Level spielen\",\n            \"description\": \"\\\\{review}Aktiviert die Funktionen \\\"Vorherige Level spielen\\\" und \\\"Bisherige Geschichte...\\\" im Auswahlbildschirm für Neues Spiel.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"\\\\{review}Reaktionsschnelles Kriechen\",\n            \"description\": \"\\\\{review}Aktiviert Verbesserungen gegenüber der ursprünglichen Kriechmechanik.\\n\\n- Ermöglicht es, nach dem Anhalten schneller wieder mit dem Kriechen zu beginnen.\\n- Ermöglicht den Übergang vom Laufen/Sprinten zum Kriechen, ohne zuvor anzuhalten.\\n- Ermöglicht den Übergang vom Kriechen zur Kriechrolle (falls aktiviert), ohne sich zuvor manuell zu ducken.\\n- Ermöglicht das Drehen im geduckten Zustand.\\n- Stellt Laras Kriech-Aufhebanimation wieder her (Fackeln ausgenommen).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"\\\\{review}Reaktives Sprinten\",\n            \"description\": \"\\\\{review}Aktiviert einen reaktiveren Sprintzustand für Lara.\\n\\n- ermöglicht Sprinten, sobald Lara Energie hat, statt nur bei voller Ausdauer.\\n- erlaubt Sprinten auf Treppen, ohne von Laras normaler Laufanimation unterbrochen zu werden.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Speicherkristalle\",\n            \"description\": \"Beschränkt Speichern auf den Anfang des Levels und Speicherkristalle. Level haben eine limitierte Anzahl von Speicherkristallen. Die Speicherkristalle funktionieren nur einmal, so wie auf der PS1. Eine Änderung dieser Option erfordert einen Neustart.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"\\\\{review}Rutsch-Lauf-Übergang\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, sofort loszulaufen, sobald sie nach dem Vorwärtsrutschen auf einer Schräge den Boden erreicht. Halte die Vorwärts-Taste gedrückt, um dies zu aktivieren.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"\\\\{review}Langsames Schwingen an einer Kante\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, langsam zu schwingen, wenn sie sich an einer sehr dünnen Kante festhält (TR3-Stil). Ist die Option deaktiviert, schwingt Lara nur kurz und kehrt dann in eine ruhende Hängeposition zurück (TR1/2-Stil).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"Sanftes Wand-Abprallen\",\n            \"description\": \"Erlaubt Lara, sich schneller zu fangen, nachdem sie gegen eine Wand gestoßen ist und eine der Richtungstasten zusammen mit der Vorwärtstaste gedrückt gehalten wird..\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"\\\\{review}Weiche Netz-Kollision\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, sich sanft gegen statische Meshes zu bewegen – ähnlich wie in TR4+ – anstatt abrupt anzuhalten.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"\\\\{review}Sprinten\",\n            \"description\": \"\\\\{review}Ermöglicht Lara zu sprinten, ähnlich wie in TR3+.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"Stufen-Roll-Boost\",\n            \"description\": \"Erlaubt Lara von einer ein-Click hohen Stufe (standard) geboostet zu werden, wenn nahe der Stufen-Kante Rollen gedrückt wird.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Schwingen unterbrechen\",\n            \"description\": \"Erlaubt, Laras Kanten-Schwinganimation abzubrechen, indem man kurz loslässt und sich schnell wieder festhält.\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Zielwechsel\",\n            \"description\": \"Ermöglicht TR4+ artiges wechseln zwischen den Zielen, während man mit den Waffen auf sie zielt. Drücke die Zielwechsel-Taste während des ziehlens, um das anvisierte Ziel zu wechseln.\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Timer zählt im Inventar\",\n            \"description\": \"Lässt den In-Game-Timer weiterlaufen, selbst wenn während des Spiels das Inventar geöffnet ist.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"\\\\{review}Ducken umschalten\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, nach einmaligem Drücken der Ducken-Taste geduckt zu bleiben. Drücke Ducken erneut, um wieder aufzustehen.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"\\\\{review}Sprint umschalten\",\n            \"description\": \"\\\\{review}Ermöglicht es Lara, nach einmaligem Drücken der Sprint-Taste weiterzulaufen. Drücke erneut Sprint, um das Sprinten zu beenden.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Finale Statistik-Anzeige\",\n            \"description\": \"Aktiviert die Erstellung einer Gesamtstatistik vom ganzen Spiel, die nach dem Abspielen der Credits angezeigt wird.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Reaktionsschnelleres Springen\",\n            \"description\": \"\\\\{review}Erlaubt Lara, während des Rennens jederzeit zu springen.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Reaktionsschneller Schwimm-Abbruch\",\n            \"description\": \"Erlaubt Lara, unter Wasser schneller stoppen zu können, wenn die Springen-Taste losgelassen wird.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Flüssiges Schwimmverhalten\",\n            \"description\": \"Verpasst Laras Drehgeschwindigkeit unter Wasser eine Beschleunigungskurve für ein runderes Steuerungsverhalten, ähnlich wie in TR2+.\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Rolle unter Wasser\",\n            \"description\": \"Erlaubt Lara, eine Rolle unter Wasser durchzuführen, ähnlich wie in TR2+.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Durchwaten\",\n            \"description\": \"\\\\{review}Erlaubt Lara, flaches Wasser zu durchwaten, anstatt an der Wasseroberfläche stecken zu bleiben.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Animierte Interaktionen\",\n            \"description\": \" Lässt Lara zu Pickups und Schaltern hingehen, anstatt sie zu ihnen zu teleportieren.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Fix: Alligator-AI\",\n            \"description\": \"Fixt, dass die Alligatoren keinen Schaden machen, wenn Lara sich im Wasser befindet und sich nicht bewegt.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Fix: Bären-AI\",\n            \"description\": \"Fixt, dass die aufrecht stehend ausgeführten Prankenhiebe von Bären Lara nicht mehr verfehlen.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Fix: Brückenkollision\",\n            \"description\": \"Fixt, dass Lara nicht in der Lage ist, sich an einigen Teilen von Brücken festzuhalten und unsichtbare Wände an den Kanten. Fixt auch Kollisions-Fehler mit Zugbrücken, Falltüren, und Brücken, wenn sie übereinander gestapelt sind, über Schrägen, und nahe dem Boden.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Fix: Zerbrechliche Böden-Stürze\",\n            \"description\": \"Fixt, dass Seit-Schritte und Vorwärtslaufen auf zerbrechlichen Platten dazu führen, dass Lara sofort herab zur darunter liegenden Platte fällt.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Fix: Fackel-Wurf-Priorität\",\n            \"description\": \"Fixt, dass Lara das Wegwerfen einer aufgebrauchten Fackel priorisiert, während sie sich in der Luft befindet. Dies kann manchmal dazu führen, dass sie nicht in der Lage ist, sich an Kanten festzuhalten.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Fix: Floor-Data-Fehler\",\n            \"description\": \"Fixt originale Fehler mit Boden Daten/Triggern.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Fix: gratis Fackel-Glitch\",\n            \"description\": \"Fixt die Möglichkeit, eine gratis Fackel zu erschaffen, wenn man während des Aufhebens eines Gegenstandes die Fackel-Taste drückt.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Fix: Item-Vervielfältigungs-Glitch\",\n            \"description\": \"Fixt die Möglichkeit, Schlüsselgegenstände im Inventar mehrfach nutzen zu können.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"Fix: Pickup-Fehler\",\n            \"description\": \"\\\\{review}Behebt ein Problem, bei dem Lara manchmal in Wände driftet, wenn sie unter Wasser Gegenstände einsammelt, sowie beim Einsammeln von Gegenständen über Wasser unter steil geneigten Decken.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"\\\\{review}Fix: M16/MP5-Genauigkeit\",\n            \"description\": \"\\\\{review}Fixt die Genauigkeit des M16/MP5, während Lara rennt.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"\\\\{review}Behebe die Priorität beim Aufheben von Affen\",\n            \"description\": \"\\\\{review}Angegriffene Affen werden das Zurückschlagen priorisieren, bevor sie Medi-Packs und Schlüssel aufsammeln.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"\\\\{review}Blasrohr-Zielen fixen\",\n            \"description\": \"\\\\{review}Behebt ein Problem, bei dem der Blasrohrschütze Lara manchmal nicht korrekt mit Pfeilen anvisieren kann.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Fix: QWOP-Glitch\",\n            \"description\": \"Fixt, dass Lara auf kleinen Stufen springt, was manchmal in einer seltsamen Lauf-Animation resultiert, bekannt als QWOP-Zustand.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Fix: Stufen-Glitch\",\n            \"description\": \"Fixt, dass Lara manchmal in Wände, die an Treppen angrenzen, gedrückt wird, wenn sie auf eine bestimmte Weise an ihnen entlangläuft.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"Fix: Wand-Treffer beim Waten\",\n            \"description\": \"Fixt, dass Lara beim waten nicht reagiert, wenn sie eine Wand trifft.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Fix: Gehen-Laufen-Sprung\",\n            \"description\": \"Fixt, dass Lara manchmal nicht sofort springen kann, direkt nachdem sie von ihrer Gehen-Animation zur Laufen-Animation wechselt.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"\\\\{review}Behebt Wandgeometrie\",\n            \"description\": \"\\\\{review}Behebt Fälle in der OG-Levelgeometrie, bei denen Neigungen innerhalb von Wänden zu ungenauen Höhenberechnungen führen können.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"Wasser Ausstieg\",\n            \"description\": \"Fixt dass Lara direkt in einen angrenzenden, trockenen Raum oder in einen trockenen Raum unterhalb gelangen kann. Zusätzlich verhindert es, dass Lara aus dem Wasser heraus auf nicht begehbare Schrägen klettern kann.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"Harpunenrückstoß\",\n            \"description\": \"Legt fest, wie oft Lara die Harpune nachladen muss, basierend auf ihrer momentanen Munitionsanzahl. Zum Beispiel, wenn es auf 3 festgelegt ist, muss sie nach jedem dritten Schuss nachladen. Auf 0 eingestellt, deaktiviert das Nachladen vollständig.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"\\\\{review}Leerlauf-Zeit\",\n            \"description\": \"\\\\{review}Ermöglicht Lara, nach der angegebenen Anzahl an Sekunden Inaktivität eine Posenanimation zu starten. Auf 0 setzen, um zu deaktivieren.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"Sprung-Block Modus\",\n            \"description\": \"Erlaubt einzustellen, wie früh es Lara erlaubt ist zu springen, nachdem sie begonnen hat zu laufen.\\n\\n- Original: entspricht dem originalen TR2 Timing.\\n- Getuned: springen ist 2 Frames früher möglich.\\n- Deaktiviert: springen ist sofort nachdem die Laufen-Animation begonnen hat möglich.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Ladebildschirme\",\n            \"description\": \"\\\\{review}Steuert Ladebildschirme vor dem Laden eines Levels.\\n\\n- Deaktiviert: Ladebildschirme nie anzeigen.\\n- Immer: Ladebildschirme anzeigen.\\n- Neue Spiele: Ladebildschirme beim Laden eines Spielstands überspringen.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"Umsehen Modus\",\n            \"description\": \"Erlaubt einzustellen wann Lara in der Lage ist sich umzusehen.\\n\\n- Beschränkt: Umsehen ist nur möglich wenn Lara still steht, und nie wenn man unter Wasser ist.\\n- Erweitert: Umsehen ist während der meisten Animationen möglich, ausser ein paar Ausnahmen wie das Schieben eines Blockes.\\n- Unbeschränkt: Umsehen ist jederzeit während der normalen Steuerung von Lara möglich.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"\\\\{review}Anzahl der Schnellspeicherplätze\",\n            \"description\": \"\\\\{review}Ändert die Anzahl der verfügbaren Schnellspeicherplätze.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Anzahl der Speicher-Slots\",\n            \"description\": \"Ändert die Anzahl der verfügbaren Speicher-Slots.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"\\\\{review}Pause bei Fokusverlust\",\n            \"description\": \"\\\\{review}Stoppt den Spielfortschritt, wenn das Spiel-Fenster den Fokus verliert.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"\\\\{review}Flächenschaden durch Projektile\",\n            \"description\": \"\\\\{review}Steuert, wie der Wirkungsbereich für Raketenwerfer und Granatwerfer sich ausbreitet.\\n\\n- Einzelfeger: Verhalten von TR1 & TR2.\\n- Mehrfachfeger: Verhalten von TR3.\\n\\nDie Mehrfachfeger-Option führt oft zu doppeltem Schaden bei einzelnen Gegnern.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Waffen zwischen den Leveln merken\",\n            \"description\": \"Bringt Lara dazu, sich zu erinnern, welche Waffe sie als Letztes im vorherigen Level verwendet hat, wenn ein neues Level beginnt. Bei Deaktivierung wird Lara auf geholsterte Pistolen zurückgreifen.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"PS1 Feinde wiederherstellen\",\n            \"description\": \"\\\\{review}Fügt die Mumie hinzu, die in der PlayStation-Version von City of Khamoon, Raum 25, erscheint.\\nDas Ändern dieser Option erfordert einen Neustart des Spiels.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Laras Lebensenergie beim Starten eines Levels\",\n            \"description\": \"Legt die Höhe des Gesundheitswertes von Lara für den Start eines jeden Levels fest.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Modis für automatische Anvisierung\",\n            \"description\": \"Ändert das automatische Anvisierungsverhalten von Laras Waffen gegenüber Zielen.\\n\\n- Volles Lock-On: Das Ziel immer bleibt anvisiert, selbst wenn sich der Feind außer Sicht ist oder stirbt (OG TR1-3).\\n- Halbes Lock-On: Das Ziel bleibt anvisiert wenn der Feind sich außer Sicht bewegt, löst aber die Anvisierung sobald der Gegner stibt.\\n- Kein Lock-On: Löse die Anvisierung sobald der Feind sich außer Sicht bewegt oder stibt (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"Wand-Glitch Ein/Aus\",\n            \"description\": \"Erlaubt, das Wand-Glitchverhalten von TR1 zu nutzen und umgekehrt; gleichermaßen erlaubt es, alle Arten von Wand-Glitches zu fixen.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"\\\\{review}Pufferung (F-Tasten)\",\n            \"description\": \"\\\\{review}Aktiviert F-Tasten (1-Frame) Pufferung, um eine präzise Steuerung von Laras Bewegung zu ermöglichen. Diese Funktion existiert ursprünglich nur im TombATI-Port (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"\\\\{review}Pufferung (Inventar)\",\n            \"description\": \"\\\\{review}Aktiviert die Pufferung des Inventars (2 Frames), um eine präzise Steuerung von Laras Bewegung zu ermöglichen.\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Reaktionsfähiger Ausweis\",\n            \"description\": \"Deaktiviert, dass man die Umblättern-Animation des Ausweises abwarten muss, sie können nun so schnell umblättern, wie sie wollen.\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Erweiterte Seit-Schritte\",\n            \"description\": \"Aktiviert Seitschritte im Stil von TR3+, e. g. Shift + Richtungspfeile. Deaktiviert die Seitschritt-Tasten.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"Schnell-Zieh-Tasten\",\n            \"description\": \"Bestimmt das Verhalten der Schnell-Zieh-Tasten.\\n\\n- Nur ziehen: eine der Tasten zu drücken sorgt dafür, dass Lara die zugewiesene Waffe ausrüstet.\\n- Ziehen oder wegstecken: das selbe wie bei 'Nur Ziehen', zusätzlich wird Lara die zugewiesene Waffe, die sie gerade benutzt, wegstecken.\",\n        },\n        \"language\": {\n            \"title\": \"Sprache\",\n            \"description\": \"Ändert die Sprache des UI-Textes.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Anisotropischer Filter\",\n            \"description\": \"Erwiterte Textur-Filterung auf Entfernung.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Seitenverhältnis\",\n            \"description\": \"Erzwingt bestimmte Seitenverhältnisse mit Letterbox.\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"Rahmen\",\n            \"description\": \"Fügt schwarze Rahmen um das Spiel-Fenster herum hinzu.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Trapezoid-Filter\",\n            \"description\": \"Korrigiert das Rendern von Vierecken.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"VSync\",\n            \"description\": \"Schaltet V-Sync an oder aus.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"FPS\",\n            \"description\": \"Stellt Spiel-Frames pro second ein.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Beleuchtungs-Kontrast\",\n            \"description\": \"Erhöht den Kontrast für dynamische Lichtquellen, wie zum Beispiel Fackeln und Mündungsfeuer.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Bildschirmfoto-Format\",\n            \"description\": \"Dateiformat des Bildschirmfotos.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"\\\\{review}Sprite-Sperrmodus\",\n            \"description\": \"\\\\{review}Steuert, welche Achsen beim Anzeigen von Sprites auf dem Bildschirm gesperrt werden.\\n\\n- Keine: Sprites normal anzeigen.\\n- Roll: Rollachse sperren – nur im Fotomodus nützlich.\\n- Roll & Neigung: Sicherstellen, dass die Sprites aufrecht stehen und nicht auf dem Boden liegen, wenn man sie von oben betrachtet.\\n- Perspektive: Roll- und Neigungsachsen sperren und zusätzlich die Sprites leicht zur Bildschirmmitte hin drehen.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Textur-Filter\",\n            \"description\": \"Wechselt zwischen weichgezeichneten und pixeligen Objekt-Texturen.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"UI-Filter\",\n            \"description\": \"Schaltet zwischen weichgezeichneten und pixeligen UI-Texturen hin und her.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"Upsaling-Faktor\",\n            \"description\": \"Skaliert das Spiel um einen festgelegten Wert hoch, erhält dabei die pixelige Optik.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"Upscaling-Filter\",\n            \"description\": \"Schaltet zwischen weichgezeichneter oder pixeliger Optik für das gesamte Bild.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Sauerstoffleistenfarbe\",\n            \"description\": \"Farbe der Sauerstoffleiste.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Sauerstoffleistenfarbe\",\n            \"description\": \"Farbe der Sauerstoffleiste.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Position der Sauerstoffleiste\",\n            \"description\": \"Position, an der die Sauerstoffleiste angezeigt wird.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"\\\\{review}Munitionszähler-Position\",\n            \"description\": \"\\\\{review}Position, an der der Munitionszähler angezeigt wird.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"\\\\{review}Aussehen der Balken\",\n            \"description\": \"\\\\{review}Steuert das visuelle Erscheinungsbild der UI-Leisten.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Leisten-Größe\",\n            \"description\": \"Ändert die Größe der Gesundheitsleisten, Sauerstoffleisten und Gesundheitsleisten von Feinden.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"\\\\{review}Leisten blinken\",\n            \"description\": \"\\\\{review}Lässt Laras Gesundheits- und Sauerstoffanzeigen blinken, wenn eine der Ressourcen knapp wird.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Leisten-Farbverläufe\",\n            \"description\": \"Sorgt bei Gesundheitsleisten, Sauerstoffleisten und Gesundheitsleisten von Feinden für einen fließenden Farbübergang.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Scroll-Schleife\",\n            \"description\": \"Ermöglicht die Richtungsnavigation in Menüs in einer Schleife.\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Leisten-Farbe der Feinde\",\n            \"description\": \"Gesundheitsleisten-Farbe der Feinde\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Freundliche Einheiten-Leisten Farbe\",\n            \"description\": \"Farbe für die Gusundheitsleisten freundlich gesinnter Einheiten. Wird auf der gleiche Position angezeigt wie die Gesundheitsleisten der Feinde.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Freundliche Einheiten-Leisten Farbe\",\n            \"description\": \"Farbe für die Gusundheitsleisten freundlich gesinnter Einheiten. Wird auf der gleiche Position angezeigt wie die Gesundheitsleisten der Feinde.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Leisten-Farbe der Feinde\",\n            \"description\": \"Gesundheitsleisten-Farbe der Feinde\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Position der Gesundheitsleiste der Feinde\",\n            \"description\": \"Die Position, an der die feindliche Gesundheitsleiste angezeigt wird.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Gesundheitleiste der Feinde\",\n            \"description\": \"Aktiviert die Anzeige einer Gesundheitleiste für aktive Feinde.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"\\\\{review}Farbe der Ausdauerleiste\",\n            \"description\": \"\\\\{review}Farbe der Kaltwasser-Ausdauerleiste.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"\\\\{review}Farbe der Ausdauerleiste\",\n            \"description\": \"\\\\{review}Farbe der Kaltwasser-Ausdauerleiste.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"\\\\{review}Position der Ausdauerleiste\",\n            \"description\": \"\\\\{review}Position, an der die Kaltwasser-Ausdauerleiste angezeigt wird.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Gesundheitsleisten-Farbe\",\n            \"description\": \"Farbe der Gesundheitsleiste.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Gesundheitsleisten-Farbe\",\n            \"description\": \"Farbe der Gesundheitsleiste.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Position der Gesundheitsleiste\",\n            \"description\": \"Position, an der die Gesundheitsleiste angezeigt wird.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"\\\\{review}Gift Gesundheitsbalken Farbe\",\n            \"description\": \"\\\\{review}Farbe des Gesundheitsbalkens, wenn Lara vergiftet ist.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"\\\\{review}Gift Gesundheitsbalken Farbe\",\n            \"description\": \"\\\\{review}Farbe des Gesundheitsbalkens, wenn Lara vergiftet ist.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"\\\\{review}Inventar-Hintergrund\",\n            \"description\": \"\\\\{review}Ändert die Art und Weise, wie der Hintergrund für den Inventarring angezeigt wird.\\n\\n- Dunkel: TR1 (PC).\\n- Sehr dunkel: TR1 (PS1).\\n- Statisch: TR2 (PC).\\n- Welle: TR2 (PS1).\\n- Monochrom: TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"\\\\{review}Inventar-Ausblend-Effekte\",\n            \"description\": \"\\\\{review}Feinabstimmung, ob die Ausblend-Effekte im Inventarring des Spiels aktiviert oder deaktiviert sind. Die Option Ausblend-Effekte muss aktiviert sein, damit dies funktioniert.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Menü-Stil\",\n            \"description\": \"Ändert, wie Menüs dargestellt werden.\\n\\n - PC: UI-Stil gleicht der PC-Version.\\n - PS1: UI-Stil gleicht der PS1-Version.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"\\\\{review}Pause-Hintergrund\",\n            \"description\": \"\\\\{review}Ändert die Art und Weise, wie der Hintergrund für den Pausenbildschirm angezeigt wird.\\n\\n- Dunkel: TR1 (PC).\\n- Sehr dunkel: TR1 (PS1).\\n- Statisch: TR2 (PC).\\n- Welle: TR2 (PS1).\\n- Monochrom: TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"\\\\{review}Pause-Ausblend-Effekte\",\n            \"description\": \"\\\\{review}Feinabstimmung, ob die Ausblend-Effekte im Pausenbildschirm aktiviert oder deaktiviert sind. Die Option Ausblend-Effekte muss aktiviert sein, damit dies funktioniert.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"Pickup-Skala\",\n            \"description\": \"\\\\{review}Ändert die Größe der im UI animierten Gegenstände, wenn Lara etwas aufnimmt.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"\\\\{review}Leisten anzeigen\",\n            \"description\": \"\\\\{review}Deaktiviert alle Ingame-Anzeigen und verdeckt Informationen über Laras Gesundheit und andere Ressourcen (für Herausforderungsdurchläufe).\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Pickup-Anzeige\",\n            \"description\": \"Zeigt Gegenstände unten rechts an, wenn Lara etwas aufnimmt.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"\\\\{review}Titel Versionsanzeige\",\n            \"description\": \"\\\\{review}Zeigt die TRX Versionszeichenfolge im Inventarring des Titels an.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"\\\\{review}Farbe der Sprintleiste\",\n            \"description\": \"\\\\{review}Farbe der Sprintleiste\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"\\\\{review}Farbe der Sprintleiste\",\n            \"description\": \"\\\\{review}Farbe der Sprintleiste\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"\\\\{review}Position der Sprintanleiste\",\n            \"description\": \"\\\\{review}Position, an der die Sprinleiste angezeigt wird.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"\\\\{review}Munition Treffer/Verbraucht\",\n            \"description\": \"\\\\{review}Zeigt die Munitionszeile in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"\\\\{review}Kristalle\",\n            \"description\": \"\\\\{review}Zeigt die Kristallreihe in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"\\\\{review}Tode\",\n            \"description\": \"\\\\{review}Zeigt Laras Tode in den Kompassstatistiken und in den Levelstatistiken an. Die Todesanzahl wird im aktuell geladenen Speicherstand aktualisiert, sobald Lara stirbt.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"\\\\{review}Zurückgelegte Distanz\",\n            \"description\": \"\\\\{review}Zeigt die Zeile der zurückgelegten Distanz in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"\\\\{review}Kills\",\n            \"description\": \"\\\\{review}Zeigt die Kills-Zeile in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"\\\\{review}Levelzähler\",\n            \"description\": \"\\\\{review}Zeigt die aktuelle Levelnummer oben in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"\\\\{review}Verwendete Gesundheitssets\",\n            \"description\": \"\\\\{review}Zeigt die Zeile der verwendeten Gesundheitssets in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"\\\\{review}Aufnahmen\",\n            \"description\": \"\\\\{review}Zeigt die Aufnahmen-Zeile in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"\\\\{review}Gefundene Geheimnisse\",\n            \"description\": \"\\\\{review}Zeigt die Zeile der gefundenen Geheimnisse in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"\\\\{review}Benötigte Zeit\",\n            \"description\": \"\\\\{review}Zeigt die Zeile der benötigten Zeit in den Levelstatistiken an.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"\\\\{review}Summen anzeigen\",\n            \"description\": \"\\\\{review}Zeigt Gesamtwerte neben den Statistiken an, wenn zutreffend. Geheimnisse bleiben von dieser Einstellung unberührt.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"\\\\{review}Stil der Statistiken\",\n            \"description\": \"\\\\{review}Steuert, wie der Statistikdialog angezeigt wird.\\n\\n- Einfach: zeigt das einfachere, rahmenlose Layout.\\n- Mit Rahmen: zeigt das Layout mit Rahmen.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"\\\\{review}Statistik-Hintergrund\",\n            \"description\": \"\\\\{review}Ändert die Art und Weise, wie der Hintergrund für die Endlevel-Statistiken angezeigt wird.\\n\\n- Dunkel: TR1 (PC).\\n- Sehr dunkel: TR1 (PS1).\\n- Statisch: TR2 (PC).\\n- Welle: TR2 (PS1).\\n- Monochrom: TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"\\\\{review}Statistik-Ausblend-Effekte\",\n            \"description\": \"\\\\{review}Feinabstimmung, ob die Ausblend-Effekte im Statistikbildschirm am Ende des Levels aktiviert oder deaktiviert sind. Die Option Ausblend-Effekte muss aktiviert sein, damit dies funktioniert.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Textgröße\",\n            \"description\": \"Ändert die Größe des UI-Textes.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"\\\\{review}Bluteffekte\",\n            \"description\": \"\\\\{review}Steuert die Farben der Blutspritzer.\\n\\n- Deaktiviert: Es werden keine Blutspritzer angezeigt.\\n- Pink: Standard in den deutschen PC-Versionen von TR3.\\n- Rot: Standard in allen anderen Einzelhandelsversionen.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Kamera-Modus\",\n            \"description\": \"Passt das Kameraverhalten während Aktionen, wie dem Benutzen von Schlüsseln, ausegführt werden.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"3D-Pickups\",\n            \"description\": \"Aktiviert die Darstellung von 3D-Modellen für Pickups anstatt der üblichen Sprites.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Laras Zopf\",\n            \"description\": \"Aktiviert Laras Zopf.\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Brise\",\n            \"description\": \"Aktiviert eine Brise, die Laras Zopf in den richtigen Räumen zum Wehen bringt.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Abblenden beim Beenden des Spiels\",\n            \"description\": \"Aktiviert die Abblend-Effekte wenn man das Spiel vollständig beendet.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Überganseffekte\",\n            \"description\": \"Aktiviere Abblenden bei Übergängen, zum Beispiel bei Credits-, Grafikeinstellungs-, Inventar- und Pause-Bildübergängen.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"Feuer-Beleuchtung\",\n            \"description\": \"Ermöglicht die generierung von dynamischen Lichteffekten neben aktiven Flammen.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"\\\\{review}Fußspuren\",\n            \"description\": \"\\\\{review}Ermöglicht das Anzeigen von Laras Fußspuren auf bestimmten Oberflächen in unterstützten Levels.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"\\\\{review}Gleitende Kameras\",\n            \"description\": \"\\\\{review}Ermöglicht bei festen Kameras, die Lara ansehen, eine Gleitbewegung mit einer sanften Geschwindigkeitskurve. Wenn deaktiviert, wechseln diese Kameras sofort zur Ansicht auf Lara.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Dynamisches Licht für Waffen\",\n            \"description\": \"Aktiviert die verwendung von dynamischer Beleuchtung für Schüsse und Explosionen.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"PS1-Kristall-Farbton\",\n            \"description\": \"Der Speicherkristall bekommt einen lila Farbton, ähnlich wie bei der PS1-Variante.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Reflektionen\",\n            \"description\": \"Aktiviert Reflektionen auf bestimmten Objekten.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"\\\\{review}Reaktionsfähige Mesh-Tönung\",\n            \"description\": \"\\\\{review}Ermöglicht es, Laras einzelne Meshes mit einer Wasserfärbung darzustellen, wenn sie sich selbst unter Wasser befinden (TR3-Stil). Andernfalls werden, wenn Lara im Wasser ist, alle ihre Meshes mit der Tönung dargestellt (TR1/2-Stil).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Schrotflinten-Mündungsfeuer\",\n            \"description\": \"Zeigt Mündungsfeuer an, wenn man mit der Schrotflinte schießt, wie bei anderen Waffen.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Skyboxes\",\n            \"description\": \"Aktiviert die Skybox in unterstützten Leveln.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"\\\\{review}Wetter\",\n            \"description\": \"\\\\{review}Aktiviert die Darstellung von Wettereffekten in unterstützten Levels.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Fix: Sprite-Animationen\",\n            \"description\": \"\\\\{review}Behebt die ursprünglichen Unterwasser-Pflanzen-Sprites, sodass sie in Wasserbereichen richtig animiert werden.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Fix: Item-Rotations-Fehler\",\n            \"description\": \"Fixt originale Fehler mit einigen falsch rotierenden Pickups, wenn man die 3D-Pickups-Option verwendet.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Fix: Textur-Fehler\",\n            \"description\": \"Fixt originale Fehler mit fehlenden oder inkorrekten Texturen/Meshes.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"\\\\{review}Nebel Farbe\",\n            \"description\": \"\\\\{review}Farbe des Nebels.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Ende des Nebels\",\n            \"description\": \"Stellt die Distanz in Kacheln ein, in der der Nebel alles völlig verhüllt.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Beginn des Nebels\",\n            \"description\": \"Stellt die Distanz in Kacheln ein, in der der Nebel zu erscheinen beginnt.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"\\\\{review}Nebel Transparenz\",\n            \"description\": \"Bestimmt ob die Überlagerung entfernter Geometrie mit 100% transparenten Flächen aktiviert werden soll.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"\\\\{review}Sichtfeld\",\n            \"description\": \"\\\\{review}Blickwinkel in Grad. Größere Werte erweitern das Sichtfeld, kleinere verengen es.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Helligkeit\",\n            \"description\": \"Ändert die Helligkeit des Spiels.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"\\\\{review}Gamma\",\n            \"description\": \"\\\\{review}Passt die Gammakurve an. Höhere Werte bedeuten hellere Beleuchtung. Der Wert 2,5 entspricht den Standardfarben.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"\\\\{review}Laras Outfit\",\n            \"description\": \"\\\\{review}Ändert Laras Aussehen. Bei Auswahl von Standard werden die regulären Outfit-Wechsel zwischen den Levels beibehalten, andernfalls bleibt das gewählte Outfit aktiv, bis es manuell geändert wird.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"\\\\{review}Schattenform\",\n            \"description\": \"\\\\{review}Wählt aus, wie die Schatten von Entitäten dargestellt werden.\\n\\n- Achteck: alte TR1- und TR2-Schatten\\n- Kreis: runde Schatten\\n- Sprite: TR3 texturbasierte Schatten\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"\\\\{review}Laras Sonnenbrille\",\n            \"description\": \"\\\\{review}Ändert den Stil von Laras Sonnenbrille. Hinweis: Die Gläser sind reflektierend, wenn die entsprechende Option aktiviert ist.\\n\\n- Aus: Lara trägt keine Sonnenbrille.\\n- Undurchsichtig: Laras Sonnenbrille hat undurchsichtige Gläser.\\n- Transparent: Laras Sonnenbrille hat halbtransparente Gläser.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"UI-Helligkeit\",\n            \"description\": \"Ändert die Helligkeit der Benutzeroberfläche.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Wasser-Farbe\",\n            \"description\": \"Farbe des Wassers.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"Alarm\",\n        },\n        \"alligator\": {\n            \"name\": \"Alligator\",\n        },\n        \"alphabet\": {\n            \"name\": \"\\\\{review}Standard Schriftart\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"\\\\{review}Kleine Schriftart\",\n        },\n        \"amber_light\": {\n            \"name\": \"\\\\{review}Bernsteinfarbenes Licht\",\n        },\n        \"animating_1\": {\n            \"name\": \"\\\\{review}Objekt 1 wird animiert\",\n        },\n        \"animating_10\": {\n            \"name\": \"\\\\{review}Objekt 10 wird animiert\",\n        },\n        \"animating_2\": {\n            \"name\": \"\\\\{review}Objekt 2 wird animiert\",\n        },\n        \"animating_3\": {\n            \"name\": \"\\\\{review}Objekt 3 wird animiert\",\n        },\n        \"animating_4\": {\n            \"name\": \"\\\\{review}Objekt 4 wird animiert\",\n        },\n        \"animating_5\": {\n            \"name\": \"\\\\{review}Objekt 5 wird animiert\",\n        },\n        \"animating_6\": {\n            \"name\": \"\\\\{review}Objekt 6 wird animiert\",\n        },\n        \"animating_7\": {\n            \"name\": \"\\\\{review}Objekt 7 wird animiert\",\n        },\n        \"animating_8\": {\n            \"name\": \"\\\\{review}Objekt 8 wird animiert\",\n        },\n        \"animating_9\": {\n            \"name\": \"\\\\{review}Objekt 9 wird animiert\",\n        },\n        \"ape\": {\n            \"name\": \"Affe\",\n        },\n        \"area_51_rocket\": {\n            \"name\": \"\\\\{review}Area 51 Rakete\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"\\\\{review}Area 51 Raketenstart\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"\\\\{review}Area 51 Raketenunterstützung\",\n        },\n        \"assault_digits\": {\n            \"name\": \"Assault Digits\",\n        },\n        \"assault_target\": {\n            \"name\": \"\\\\{review}Angriffsziele\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"\\\\{review}Boden-Atlanter\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"\\\\{review}Atlanter (Schießend)\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"\\\\{review}Geflügelter Atlanter\",\n        },\n        \"autos\": {\n            \"name\": \"Automatik\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"Automatik-Munition\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Schinken Lara\",\n        },\n        \"baldy\": {\n            \"name\": \"Glatzkopf\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"Söldner 1\",\n                \"Masked Goon 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"Söldner 2\",\n                \"Maskierter Schläger 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"Söldner 3\",\n                \"Maskierter Schläger 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"Barrakuda\",\n        },\n        \"bartoli\": {\n            \"name\": \"Marco Bartoli\",\n        },\n        \"bat\": {\n            \"name\": \"Fledermaus\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"\\\\{review}Fledermaus-Emitter\",\n        },\n        \"beacon_light\": {\n            \"name\": \"\\\\{review}Leuchtfeuer\",\n        },\n        \"bear\": {\n            \"name\": \"Bär\",\n        },\n        \"bell\": {\n            \"name\": \"Bell\",\n        },\n        \"big_bowl\": {\n            \"name\": \"Lava-Schüssel\",\n        },\n        \"big_eel\": {\n            \"name\": \"Großer Aal\",\n        },\n        \"big_pod\": {\n            \"name\": \"Big Pod\",\n        },\n        \"big_spider\": {\n            \"name\": \"Riesen-Spider\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"Vogel-Monster\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"Dripping Water\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"Singing Birds\",\n        },\n        \"blade\": {\n            \"name\": \"Wall-mounted Blade\",\n        },\n        \"blood\": {\n            \"name\": \"\\\\{review}Blut\",\n        },\n        \"blood_pink\": {\n            \"name\": \"\\\\{review}Blut (zensiert)\",\n        },\n        \"blue_light\": {\n            \"name\": \"\\\\{review}Blaues Licht\",\n        },\n        \"boat\": {\n            \"name\": \"Boot\",\n        },\n        \"boat_bits\": {\n            \"name\": \"Boots-Einzelteile\",\n        },\n        \"body_part\": {\n            \"name\": \"Körperteil\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"Brücke Flach\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"Brücke Tilt 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"Brücke Tilt 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"Blase 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Blase 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"Blasenerzeuger\",\n        },\n        \"camera_target\": {\n            \"name\": \"Kamera-Ziel\",\n        },\n        \"carcass\": {\n            \"name\": \"\\\\{review}Kadaver\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"Spiky Ceiling\",\n        },\n        \"centaur\": {\n            \"name\": \"Zentaur\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Statue\",\n        },\n        \"civilian\": {\n            \"name\": \"\\\\{review}Zivilist\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"\\\\{review}Klauenmutant\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"Bartoli Hideout clock\",\n        },\n        \"cog_1\": {\n            \"name\": \"Zahnrad 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Zahnrad 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Zahnrad 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"Kampf Ende\",\n        },\n        \"compass\": {\n            \"name\": \"Statistiken\",\n        },\n        \"compy\": {\n            \"name\": \"\\\\{review}Compsognathus\",\n        },\n        \"controls\": {\n            \"name\": \"Steuerung\",\n        },\n        \"copter\": {\n            \"name\": \"Helikopter\",\n        },\n        \"cowboy\": {\n            \"name\": \"Cowboy\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"\\\\{review}Kriechender Mutant\",\n        },\n        \"crocodile\": {\n            \"name\": \"Krokodil\",\n        },\n        \"crow\": {\n            \"name\": \"Krähe\",\n        },\n        \"cult_1\": {\n            \"name\": \"Maskierter Schläger 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"Maskierter Schläger 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"Maskierter Schläger 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"Messerwerfer\",\n        },\n        \"cult_3\": {\n            \"name\": \"Schrotflinten Schläger\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"Schrotflinten Dusch Animation\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Schwert des Damokles\",\n        },\n        \"dart\": {\n            \"name\": \"Pfeil\",\n        },\n        \"dart_effect\": {\n            \"name\": \"Pfeil-Effekt\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Pfeilerzeuger\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"\\\\{review}Desert Eagle\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"\\\\{review}Magazine der Desert Eagle\",\n        },\n        \"detonator_box\": {\n            \"name\": \"Zünder\",\n        },\n        \"ding_dong\": {\n            \"name\": \"Türklingel\",\n        },\n        \"dino_mutant\": {\n            \"name\": \"Dino Mutant\",\n        },\n        \"disc\": {\n            \"name\": \"Scheibe\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"Scheiben-Erzeuger\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"\\\\{review}Einweg-Animierobjekt 9\",\n        },\n        \"diver\": {\n            \"name\": \"Taucher\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"Hund\",\n                \"Dobermann\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Tür 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Tür 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Tür 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Tür 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Tür 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Tür 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Tür 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Tür 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"Drachen Hinterseite\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"Platzhalter\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"Drachen-Knochen Vorn\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"Drachen-Knochen Hinten\",\n        },\n        \"dragon_front\": {\n            \"name\": \"Drache Vorderseite\",\n        },\n        \"drawbridge\": {\n            \"name\": \"Zugbrücke\",\n        },\n        \"dust\": {\n            \"name\": \"Staub\",\n        },\n        \"dying_monk\": {\n            \"name\": \"sterbender Mönch\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"\\\\{review}Sterbender Mutant\",\n        },\n        \"eagle\": {\n            \"name\": \"Adler\",\n        },\n        \"earthquake\": {\n            \"name\": \"Erdbeben\",\n        },\n        \"eel\": {\n            \"name\": \"Aal\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"\\\\{review}Elektrischer Reiniger\",\n        },\n        \"electric_fence\": {\n            \"name\": \"\\\\{review}Elektrischer Zaun\",\n        },\n        \"electrical_light\": {\n            \"name\": \"\\\\{review}Elektrisches Licht\",\n        },\n        \"ember\": {\n            \"name\": \"Glut\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"Gluterzeuger\",\n        },\n        \"explosion_1\": {\n            \"name\": \"Explosion 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Explosion 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": [\n                \"Fallender Block 1\",\n                \"Zerbrechbarer Boden 1\",\n                \"Zerbrechbare Kacheln 1\",\n            ]\n        },\n        \"falling_block_2\": {\n            \"name\": [\n                \"Fallender Block 2\",\n                \"Zerbrechbarer Boden 2\",\n                \"Zerbrechbare Kacheln 2\",\n            ]\n        },\n        \"falling_block_3\": {\n            \"name\": [\n                \"Fallender Block 3\",\n                \"Zerbrechbarer Boden 3\",\n                \"Zerbrechbare Kacheln 3\",\n                \"Lose Bretter\",\n            ]\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"Falling Ceiling 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Fallende Decke 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"\\\\{review}Feuerkopf\",\n        },\n        \"fish_mutant\": {\n            \"name\": \"Fisch Mutant\",\n        },\n        \"flame\": {\n            \"name\": [\n                \"Flamme\",\n                \"Feuer\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"Flamenerzeuger\",\n                \"Feuererzeuger\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": \"\\\\{review}Flammenwerfer (Groß)\",\n        },\n        \"flame_emitter_jet\": {\n            \"name\": \"\\\\{review}Flammenwerfer (Strahl)\",\n        },\n        \"flame_emitter_side\": {\n            \"name\": \"\\\\{review}Flammenwerfer (Seite)\",\n        },\n        \"flame_emitter_small\": {\n            \"name\": \"\\\\{review}Flammenwerfer (Klein)\",\n        },\n        \"flare\": {\n            \"name\": \"Fackel\",\n        },\n        \"flare_fire\": {\n            \"name\": \"Fackel-Funken\",\n        },\n        \"flares_box\": {\n            \"name\": \"Flares Box\",\n        },\n        \"flickering_light\": {\n            \"name\": \"\\\\{review}Flackerndes Licht\",\n        },\n        \"fuse_box\": {\n            \"name\": \"\\\\{review}Sicherungskasten\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"Gray disk\",\n        },\n        \"gamma\": {\n            \"name\": \"\\\\{review}Gamma\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"\\\\{review}Gasemitter (Grün)\",\n        },\n        \"general\": {\n            \"name\": \"Mini U-Boot\",\n        },\n        \"globe\": {\n            \"name\": \"\\\\{review}Globus\",\n        },\n        \"glow\": {\n            \"name\": \"Glühen\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"Karten-Glühen\",\n        },\n        \"gondola\": {\n            \"name\": \"Gondel\",\n        },\n        \"gong\": {\n            \"name\": \"Gong\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"Gong Schlegel\",\n        },\n        \"graphics\": {\n            \"name\": \"Grafikeinstellungen\",\n        },\n        \"green_light\": {\n            \"name\": \"\\\\{review}Grünes Licht\",\n        },\n        \"grenade\": {\n            \"name\": \"Granate\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"Granatenwerfer\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"Granaten\",\n        },\n        \"gun_flash\": {\n            \"name\": \"Waffen Blitz\",\n        },\n        \"gun_shell\": {\n            \"name\": \"\\\\{review}Gewehrpatrone\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"Harpunen Pfeil\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"Harpune\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"Pfeile\",\n        },\n        \"hook\": {\n            \"name\": \"Hook\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"Extra Feuer\",\n        },\n        \"huskie\": {\n            \"name\": \"\\\\{review}Hund\",\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"\\\\{review}Hybridmutant\",\n        },\n        \"icicle\": {\n            \"name\": \"Icicles\",\n        },\n        \"inv_background\": {\n            \"name\": \"Menü Hintergrund\",\n        },\n        \"jelly\": {\n            \"name\": \"Qualle\",\n        },\n        \"kayak\": {\n            \"name\": \"\\\\{review}Kajak\",\n        },\n        \"key_1\": {\n            \"name\": \"Key 1\",\n        },\n        \"key_2\": {\n            \"name\": \"Key 2\",\n        },\n        \"key_3\": {\n            \"name\": \"Key 3\",\n        },\n        \"key_4\": {\n            \"name\": \"Key 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"Keyhole 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"Keyhole 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"Keyhole 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"Keyhole 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"\\\\{review}Alle töten (ausgelöst)\",\n        },\n        \"killer_statue\": {\n            \"name\": \"Statue with Sword\",\n        },\n        \"lara\": {\n            \"name\": \"Lara\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"Alarmglocke\",\n        },\n        \"lara_autos\": {\n            \"name\": \"\\\\{review}Animation automatischer Pistolen\",\n        },\n        \"lara_boat\": {\n            \"name\": \"Boots-Animation\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"\\\\{review}Animation der Desert Eagle\",\n        },\n        \"lara_extra\": {\n            \"name\": \"Laras Extra Animation\",\n        },\n        \"lara_flare\": {\n            \"name\": \"Fackel-Animation\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"Grantenwerfer-Animation\",\n        },\n        \"lara_hair\": {\n            \"name\": \"Laras Zopf\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"Harpunen-Animation\",\n        },\n        \"lara_m16\": {\n            \"name\": \"M16-Animation\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"Magnum-Animation\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"\\\\{review}MP5-Animation\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"Pistolen-Animation\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"\\\\{review}Animation des Raketenwerfers\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"Schrotflinten-Animation\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"Schneeemobil-Animation\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"Uzi-Animation\",\n        },\n        \"larson\": {\n            \"name\": \"Larson\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Lava-Keil\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Bleibarren\",\n        },\n        \"lift\": {\n            \"name\": \"Aufzug\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Blitzerzeuger\",\n        },\n        \"lion\": {\n            \"name\": \"Löwe\",\n        },\n        \"lioness\": {\n            \"name\": [\n                \"Löwin\",\n                \"Löwe\",\n            ]\n        },\n        \"lizard\": {\n            \"name\": \"\\\\{review}Echse\",\n        },\n        \"m16\": {\n            \"name\": \"M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"M16-Munition\",\n        },\n        \"m16_flash\": {\n            \"name\": \"M16 Blitz\",\n        },\n        \"magnums\": {\n            \"name\": \"Magnums\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Magnum Munition\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"Mesh Swap 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"Mesh Swap 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"\\\\{review}Mesh Swap 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Hand des Midas\",\n        },\n        \"mine\": {\n            \"name\": \"Wassermine\",\n        },\n        \"mine_cart\": {\n            \"name\": \"\\\\{review}Grubenwagen\",\n        },\n        \"mini_copter\": {\n            \"name\": \"Helikopter 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"\\\\{review}Geschoss (Atlantische Bombe)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"\\\\{review}Geschoss (Atlantischer Splitter)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"\\\\{review}Geschoss (Flamme)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"\\\\{review}Geschoss (Harpune)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"\\\\{review}Geschoss (Messer)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"\\\\{review}Geschoss (Gift)\",\n        },\n        \"monk_1\": {\n            \"name\": \"Mönch 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"Mönch 2\",\n        },\n        \"monkey\": {\n            \"name\": \"\\\\{review}Affe\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"\\\\{review}Montiertes Geschütz\",\n        },\n        \"mouse\": {\n            \"name\": \"Ratte\",\n        },\n        \"movable_block_1\": {\n            \"name\": [\n                \"Drückbarer Block 1\",\n                \"Bewegbarer Block 1\",\n            ]\n        },\n        \"movable_block_2\": {\n            \"name\": [\n                \"Drückbarer Block 2\",\n                \"Bewegbarer Block 2\",\n            ]\n        },\n        \"movable_block_3\": {\n            \"name\": [\n                \"Drückbarer Block 3\",\n                \"Bewegbarer Block 3\",\n            ]\n        },\n        \"movable_block_4\": {\n            \"name\": [\n                \"Drückbarer Block 4\",\n                \"Bewegbarer Block 4\",\n            ]\n        },\n        \"moving_bar\": {\n            \"name\": \"Moving Bar\",\n        },\n        \"mp5\": {\n            \"name\": \"\\\\{review}MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"\\\\{review}MP5-Munition\",\n        },\n        \"mp_1\": {\n            \"name\": \"\\\\{review}MP 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"\\\\{review}MP 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Mumie\",\n        },\n        \"natla\": {\n            \"name\": \"Natla\",\n        },\n        \"natla_gun\": {\n            \"name\": \"\\\\{review}Natlas Waffe\",\n        },\n        \"on_off_light\": {\n            \"name\": \"\\\\{review}An/Aus Licht\",\n        },\n        \"orca\": {\n            \"name\": \"\\\\{review}Orca\",\n        },\n        \"passport\": {\n            \"name\": \"Spiel\",\n        },\n        \"patrol_dog\": {\n            \"name\": \"\\\\{review}Hund\",\n        },\n        \"pda\": {\n            \"name\": \"Gameplay\",\n        },\n        \"pendulum_1\": {\n            \"name\": \"Sandsack\",\n        },\n        \"pendulum_2\": {\n            \"name\": \"Swinging Box\",\n        },\n        \"photo\": {\n            \"name\": \"Laras Haus\",\n        },\n        \"pickup_1\": {\n            \"name\": \"Pickup Item 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"Pickup Item 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Pickup-Hilfe\",\n        },\n        \"pierre\": {\n            \"name\": \"Pierre\",\n        },\n        \"pirahnas\": {\n            \"name\": \"\\\\{review}Piranhas\",\n        },\n        \"pistols\": {\n            \"name\": \"Pistolen\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"Pistolen Munition\",\n        },\n        \"player_1\": {\n            \"name\": \"Zwischensequenz-Schauspieler 1\",\n        },\n        \"player_10\": {\n            \"name\": \"Zwischensequenz-Schauspieler 10\",\n        },\n        \"player_2\": {\n            \"name\": \"Zwischensequenz-Schauspieler 2\",\n        },\n        \"player_3\": {\n            \"name\": \"Zwischensequenz-Schauspieler 3\",\n        },\n        \"player_4\": {\n            \"name\": \"Zwischensequenz-Schauspieler 4\",\n        },\n        \"player_5\": {\n            \"name\": \"Zwischensequenz-Schauspieler 5\",\n        },\n        \"player_6\": {\n            \"name\": \"Zwischensequenz-Schauspieler 6\",\n        },\n        \"player_7\": {\n            \"name\": \"Zwischensequenz-Schauspieler 7\",\n        },\n        \"player_8\": {\n            \"name\": \"Zwischensequenz-Schauspieler 8\",\n        },\n        \"player_9\": {\n            \"name\": \"Zwischensequenz-Schauspieler 9\",\n        },\n        \"pods\": {\n            \"name\": \"Pod\",\n        },\n        \"poison_dart\": {\n            \"name\": \"\\\\{review}Giftpfeil\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"\\\\{review}Giftpfeilwerfer\",\n        },\n        \"portacabin\": {\n            \"name\": \"Portable Cabin\",\n        },\n        \"power_saw\": {\n            \"name\": \"Power Saw\",\n        },\n        \"prisoner\": {\n            \"name\": \"\\\\{review}Gefangener\",\n        },\n        \"propeller_1\": {\n            \"name\": \"Airplane Propeller\",\n        },\n        \"propeller_2\": {\n            \"name\": \"Underwater Propeller\",\n        },\n        \"propeller_3\": {\n            \"name\": \"Air Fan\",\n        },\n        \"pulse_light\": {\n            \"name\": \"\\\\{review}Pulsierendes Licht\",\n        },\n        \"puma\": {\n            \"name\": \"Puma\",\n        },\n        \"punk_1\": {\n            \"name\": \"\\\\{review}Punk 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"\\\\{review}Punk 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"Puzzle Item 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"Puzzle Item 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"Puzzle Item 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"Puzzle Item 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"Puzzle Hole 1 (Done)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"Puzzle Hole 2 (Done)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"Puzzle Hole 3 (Done)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"Puzzle Hole 4 (Done)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"Puzzle Hole 1 (Empty)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"Puzzle Hole 2 (Empty)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"Puzzle Hole 3 (Empty)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"Puzzle Hole 4 (Empty)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"\\\\{review}Quad\",\n        },\n        \"quest_1\": {\n            \"name\": \"\\\\{review}Quest-Gegenstand 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"\\\\{review}Quest-Gegenstand 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"\\\\{review}Quest-Gegenstand 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"\\\\{review}Quest-Gegenstand 4\",\n        },\n        \"raptor\": {\n            \"name\": \"Raptor\",\n        },\n        \"raptor_emitter\": {\n            \"name\": \"\\\\{review}Raptor-Emitter\",\n        },\n        \"rat\": {\n            \"name\": [\n                \"Ratte\",\n                \"Landratte\",\n            ]\n        },\n        \"red_light\": {\n            \"name\": \"\\\\{review}Rotes Licht\",\n        },\n        \"rib\": {\n            \"name\": \"\\\\{review}RIB\",\n        },\n        \"ricochet\": {\n            \"name\": \"Querschläger\",\n        },\n        \"rocket\": {\n            \"name\": \"\\\\{review}Rakete\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"\\\\{review}Raketenwerfer\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"\\\\{review}Raketen\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": [\n                \"Fels 1\",\n                \"Rollender Ball\",\n            ]\n        },\n        \"rolling_ball_2\": {\n            \"name\": [\n                \"Boulder 2\",\n                \"Rolling Ball 2\",\n            ]\n        },\n        \"rolling_ball_3\": {\n            \"name\": [\n                \"Boulder 3\",\n                \"Rolling Ball 3\",\n            ]\n        },\n        \"rolling_ball_4\": {\n            \"name\": [\n                \"Boulder 4\",\n                \"Rolling Ball 4\",\n            ]\n        },\n        \"rotating_laser\": {\n            \"name\": \"\\\\{review}Rotierender Laser\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"\\\\{review}RX Arbeiter 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"\\\\{review}RX Arbeiter 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": \"\\\\{review}RX Arbeiter 3\",\n        },\n        \"save_crystal\": {\n            \"name\": \"Speicherkristall\",\n        },\n        \"scion\": {\n            \"name\": \"Scion\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Scion-Halter\",\n        },\n        \"secret_1\": {\n            \"name\": \"Geheimnis 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"Geheimnis 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"Geheimnis 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"\\\\{review}Sicherheitsmann\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"\\\\{review}Sicherheitslaser (Alarm)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"\\\\{review}Sicherheitslaser (Tödlich)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"\\\\{review}Sicherheitslaser (Killer)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"\\\\{review}Roboter-Wachgeschütz\",\n        },\n        \"shadow\": {\n            \"name\": \"\\\\{review}Schatten\",\n        },\n        \"shark\": {\n            \"name\": \"Hai\",\n        },\n        \"shiva\": {\n            \"name\": \"\\\\{review}Shiva\",\n        },\n        \"shotgun\": {\n            \"name\": \"Schrotflinte\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"Schrot-Munition\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"\\\\{review}Schrotpatrone\",\n        },\n        \"skate_kid\": {\n            \"name\": \"Skate Kid\",\n        },\n        \"skateboard\": {\n            \"name\": \"Skateboard\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"Schwarzes Schneemobil\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"Fahrer schwarzes Schneemobil\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"Rotes Schneemobil\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"Snowmobile Track\",\n        },\n        \"skybox\": {\n            \"name\": \"Skybox\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Sliding Pillar\",\n        },\n        \"smashable_1\": {\n            \"name\": \"Breakable Window 1\",\n        },\n        \"smashable_2\": {\n            \"name\": \"Breakable Window 2\",\n        },\n        \"smashable_3\": {\n            \"name\": \"Breakable Window 3\",\n        },\n        \"smashable_4\": {\n            \"name\": \"Breakable Window 4\",\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"\\\\{review}Rauchgenerator (Schwarz)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"\\\\{review}Rauchgenerator (Weiß)\",\n        },\n        \"snake\": {\n            \"name\": \"\\\\{review}Schlange\",\n        },\n        \"snow_sprite\": {\n            \"name\": \"Snowmobile Wake\",\n        },\n        \"sophia\": {\n            \"name\": \"\\\\{review}Sophia\",\n        },\n        \"sound\": {\n            \"name\": \"Sound\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"Dragon Explosion 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"Dragon Explosion 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"Dragon Explosion 3\",\n        },\n        \"spider\": {\n            \"name\": \"Spinne\",\n        },\n        \"spike_wall\": {\n            \"name\": \"Spike Wall\",\n        },\n        \"spikes\": {\n            \"name\": \"Stacheln\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"Spinning Blade\",\n        },\n        \"splash_1\": {\n            \"name\": \"Wasserkräuselungen 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Water-Kräuseln 2\",\n        },\n        \"springboard\": {\n            \"name\": \"Springboard\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"\\\\{review}Dampfgenerator\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"\\\\{review}Südsee-Söldner\",\n        },\n        \"stopwatch\": {\n            \"name\": \"\\\\{review}Statistiken\",\n        },\n        \"strobe_light\": {\n            \"name\": \"\\\\{review}Stroboskoplicht\",\n        },\n        \"swat_1\": {\n            \"name\": \"\\\\{review}SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"\\\\{review}SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"\\\\{review}SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"\\\\{review}Schwingende Axt\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"Airlock Switch\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"Button\",\n                \"Push Button\",\n                \"Switch\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"Lever\",\n                \"Switch\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"Small Switch\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"Underwater Lever\",\n                \"Underwater Switch\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": \"\\\\{review}Radswitch\",\n        },\n        \"teeth_trap\": {\n            \"name\": \"Zahn-Falle\",\n        },\n        \"text_box\": {\n            \"name\": \"UI Frame\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Thors Hammer Griff\",\n        },\n        \"thors_head\": {\n            \"name\": \"Thors Hammer\",\n        },\n        \"tiger\": {\n            \"name\": \"Tiger\",\n        },\n        \"tony\": {\n            \"name\": \"\\\\{review}Tony\",\n        },\n        \"torso\": {\n            \"name\": [\n                \"Torso\",\n                \"Adam\",\n                \"Riesenmutant\",\n            ]\n        },\n        \"train\": {\n            \"name\": \"\\\\{review}Zug\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Falltür 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Falltür 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Falltür 3\",\n        },\n        \"trex\": {\n            \"name\": \"T-Rex\",\n        },\n        \"trex_alpha\": {\n            \"name\": \"\\\\{review}T-Rex-Alpha\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"\\\\{review}Stammesaxtkämpfer\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"\\\\{review}Stammesanführer\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"\\\\{review}Stammes-Blasrohrbenutzer\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"\\\\{review}Tropischer Fisch\",\n        },\n        \"twinkle\": {\n            \"name\": \"Funken\",\n        },\n        \"upv\": {\n            \"name\": \"\\\\{review}Mini-U-Boot\",\n        },\n        \"uzis\": {\n            \"name\": \"Uzis\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"Uzi-Munition\",\n        },\n        \"vole\": {\n            \"name\": [\n                \"Wühlmaus\",\n                \"Wasserratte\",\n            ]\n        },\n        \"vulture\": {\n            \"name\": \"\\\\{review}Geier\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"\\\\{review}Wespenmutant\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"\\\\{review}Wespenmutanten-Emitter\",\n        },\n        \"water_sprite\": {\n            \"name\": \"Boat Wake\",\n        },\n        \"waterfall\": {\n            \"name\": \"Wasserfall Nebel\",\n        },\n        \"white_light\": {\n            \"name\": \"\\\\{review}Weißes Licht\",\n        },\n        \"willard\": {\n            \"name\": \"\\\\{review}Willard\",\n        },\n        \"winston\": {\n            \"name\": \"Winston\",\n        },\n        \"winston_army\": {\n            \"name\": \"\\\\{review}Winston (Armee)\",\n        },\n        \"wolf\": {\n            \"name\": \"Wolf\",\n        },\n        \"worker_1\": {\n            \"name\": \"Schläger Schütze 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"Schläger Schütze 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"Schlagstock schwingender Schläger 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"Schlagstock schwingender Schläger 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"Typ mit Flammenwerfer\",\n        },\n        \"xian_knight\": {\n            \"name\": \"Xian Ritter\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"Xian Ritter Statue\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"Xian Speerträger\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"Xian Speerträger Statue\",\n        },\n        \"yeti\": {\n            \"name\": \"Yeti\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"Zipline Handle\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-en-gb.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"extends\": \"en\",\n    \"language_name\": \"English (British)\",\n    \"general\": {\n        \"settings\": {\n            \"controls\": {\n                \"customize\": \"Customise Controls\",\n            }\n        },\n        \"stats\": {\n            \"distance_travelled\": \"Distance Travelled\",\n        }\n    },\n    \"dynamic\": {\n        \"mods\": {\n            \"tr3-la\": {\n                \"title\": \"The Lost Artefact\",\n            }\n        }\n    },\n    \"enums\": {\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"Bottom centre\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"Top centre\",\n        }\n    },\n    \"settings\": {\n        \"gameplay.enable_bouncy_grenades\": {\n            \"description\": \"Enables TR3-style grenade behaviour: they ricochet off walls and slopes and produce a larger blast radius, but at the expense of reduced velocity.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Randomise enemy start angle\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"description\": \"Allows Lara's ledge-swinging animation to be cancelled by letting go and quickly grabbing again.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"description\": \"Controls how the area-of-effect for Rocket Launcher and Grenade Launcher propagates.\\n\\n- Single-sweep: TR1 & TR2 behaviour.\\n- Multi-sweep: TR3 behaviour.\\n\\nThe multi-sweep option often ends up doing double damage to individual enemies.\",\n        },\n        \"gameplay.target_mode\": {\n            \"description\": \"Changes the behaviour of how weapons lock onto targets.\\n\\n- Full lock: always keep target lock even if the enemy moves out of sight or dies (OG TR1-3).\\n- Semi lock: keep target lock if the enemy moves out of sight but lose target lock if the enemy dies.\\n- No lock: lose target lock if the enemy goes out of sight or dies (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"description\": \"Allows using TR1 wall glitch behaviour in TR2 and vice-versa; equally allows fixing all types of wall glitch.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"description\": \"Controls the behaviour of the quick gun equip keys.\\n\\n- Draw only: pressing a key will cause Lara to equip the assigned gun.\\n- Draw or holster: same as above, plus Lara will undraw the assigned gun if she's currently carrying it.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Airbar colour\",\n            \"description\": \"Colour of the airbar.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Airbar colour\",\n            \"description\": \"Colour of the airbar.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"description\": \"Makes the UI bars use smooth colour transitions.\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Enemy bar colour\",\n            \"description\": \"Colour of the enemy healthbar.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Ally bar colour\",\n            \"description\": \"Colour of the allies healthbar. Shown in the location of the enemy healthbars.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Ally bar colour\",\n            \"description\": \"Colour of the allies healthbar. Shown in the location of the enemy healthbars.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Enemy bar colour\",\n            \"description\": \"Colour of the enemy healthbar.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"Exposure bar colour\",\n            \"description\": \"Colour of the cold water exposure bar.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"Exposure bar colour\",\n            \"description\": \"Colour of the cold water exposure bar.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Healthbar colour\",\n            \"description\": \"Colour of the healthbar.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Healthbar colour\",\n            \"description\": \"Colour of the healthbar.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"Poison healthbar colour\",\n            \"description\": \"Colour of the healthbar when Lara is poisoned.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"Poison healthbar colour\",\n            \"description\": \"Colour of the healthbar when Lara is poisoned.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"Sprintbar colour\",\n            \"description\": \"Colour of the sprintbar.\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"Sprintbar colour\",\n            \"description\": \"Colour of the sprintbar.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"Distance travelled\",\n            \"description\": \"Shows the distance travelled row in the level statistics.\",\n        },\n        \"visuals.blood_effects\": {\n            \"description\": \"Controls blood spark colours.\\n\\n- Disabled: no blood sparks are shown.\\n- Pink: the default in German PC releases of TR3.\\n- Red: the default in all other retail releases.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"Fog colour\",\n            \"description\": \"Colour of the fog.\",\n        },\n        \"visuals.gamma\": {\n            \"description\": \"Adjusts the gamma curve. Higher values mean brighter lighting. The value of 2.5 means default colours.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Water colour\",\n            \"description\": \"Colour of the water.\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"Français\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Examiner\",\n            \"hide_dialog\": \"Masquer le dialogue\",\n            \"reset_defaults\": \"Réinitialiser tout\",\n            \"rotate\": \"Tourner\",\n            \"unbind\": \"Dissocier\",\n            \"use_item\": \"Utiliser\",\n        },\n        \"config_presets\": {\n            \"applied\": \"\\\\{review}Préréglage appliqué.\",\n            \"confirm_description\": \"\\\\{review}Les paramètres suivants seront modifiés :\",\n            \"confirm_restart_note\": \"\\\\{review}Remarque : certains paramètres peuvent nécessiter un redémarrage du jeu pour prendre effet.\",\n            \"empty\": \"\\\\{review}Aucun préréglage trouvé.\",\n            \"no_changes\": \"\\\\{review}Aucun changement à appliquer.\",\n            \"title_fmt\": \"\\\\{review}Appliquer le préréglage %s ?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"\\\\{review}Zone 1\",\n            \"area_2\": \"\\\\{review}Zone 2\",\n            \"area_3\": \"\\\\{review}Zone 3\",\n            \"area_4\": \"\\\\{review}Zone 4\",\n            \"area_5\": \"\\\\{review}Zone 5\",\n            \"area_6\": \"\\\\{review}Zone 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"\\\\{review}Aventure\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"GAME OVER\",\n            \"heading_inventory\": \"INVENTAIRE\",\n            \"heading_items\": \"OBJETS\",\n            \"heading_option\": \"OPTION\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Mode Démo\",\n            \"direction_keys_controller\": \"Croix directionnelle\",\n            \"direction_keys_keyboard\": \"Flèches\",\n            \"empty_slot_fmt\": \"- EMPLACEMENT VIDE %d -\",\n            \"exit\": \"Sortie\",\n            \"hold_fmt\": \"Maintenir %s\",\n            \"off\": \"Désactivé\",\n            \"on\": \"Activé\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Entrée ambiguë : %s et %s\",\n            \"ambiguous_input_3\": \"Entrée ambiguë : %s, %s, ...\",\n            \"bilinear_filter_off\": \"\\\\{review}Filtre bilinéaire : désactivé\",\n            \"bilinear_filter_on\": \"\\\\{review}Filtre bilinéaire : activé\",\n            \"command_bad_invocation\": \"Invocation invalide : %s\",\n            \"command_bool\": \"activé, désactivé\",\n            \"command_decimal\": \"[décimal]\",\n            \"command_integer\": \"[entier]\",\n            \"command_percent\": \"[entier]\",\n            \"command_unavailable\": \"Cette commande n'est pas disponible pour l'instant\",\n            \"command_valid_values\": \"Valeurs acceptées : %s\",\n            \"complete_level\": \"Niveau terminé !\",\n            \"config_option_get\": \"%s est actuellement réglé sur %s\",\n            \"config_option_set\": \"%s réglé sur %s\",\n            \"config_option_unknown_option\": \"Option inconnue : %s\",\n            \"current_health_get\": \"Santé actuelle de Lara : %d\",\n            \"current_health_set\": \"Santé de Lara réglée sur %d\",\n            \"door_close\": \"Sésame, ferme-la !\",\n            \"door_open\": \"Sésame, ouvre-toi !\",\n            \"door_open_fail\": \"Aucune porte à proximité de Lara\",\n            \"flipmap_fail_already_off\": \"L'état secondaire des salles est déjà DÉSACTIVÉ\",\n            \"flipmap_fail_already_on\": \"L'état secondaire des salles est déjà ACTIVÉ\",\n            \"flipmap_off\": \"État secondaire des salles DÉSACTIVÉ\",\n            \"flipmap_on\": \"État secondaire des salles ACTIVÉ\",\n            \"fly_mode_off\": \"Mode vol désactivé\",\n            \"fly_mode_on\": \"Mode vol activé\",\n            \"fps_counter_off\": \"Compteur de FPS désactivé\",\n            \"fps_counter_on\": \"Compteur de FPS activé\",\n            \"give_item\": \"%s ajouté à l'inventaire de Lara\",\n            \"give_item_all_guns\": \"A vos marques, prêts... feu ! Lara est armée jusqu'aux dents.\",\n            \"give_item_all_keys\": \"Surprise ! Chaque clé dont Lara pourrait avoir besoin est maintenant dans son sac à dos.\",\n            \"give_item_cheat\": \"Le sac à dos de Lara est soudainement beaucoup plus lourd !\",\n            \"heal_already_full_hp\": \"Lara est déjà en pleine forme\",\n            \"heal_success\": \"Lara est maintenant en pleine forme\",\n            \"invalid_cutscene\": \"Cinématique invalide\",\n            \"invalid_demo\": \"Démo invalide\",\n            \"invalid_item\": \"Objet inconnu : %s\",\n            \"invalid_level\": \"Niveau invalide\",\n            \"invalid_object\": \"Objet invalide\",\n            \"invalid_room\": \"Salle invalide : %d. Les salles valides sont 0-%d\",\n            \"invalid_sample\": \"Effet sonore invalide : %d\",\n            \"kill\": \"Bye-bye !\",\n            \"kill_all\": \"Paf ! %d ennemis se sont volatisés !\",\n            \"kill_all_fail\": \"Oh-oh, il n'y a plus un seul ennemi à tuer...\",\n            \"kill_fail\": \"Aucun ennemi à proximité...\",\n            \"lighting_contrast_fmt\": \"\\\\{review}Contraste de l'éclairage : %s\",\n            \"load_game\": \"Partie chargée depuis l'emplacement de sauvegarde %d\",\n            \"load_game_fail_invalid_slot\": \"Emplacement de sauvegarde %d invalide\",\n            \"load_game_fail_unavailable_slot\": \"L'emplacement de sauvegarde %d n'est pas disponible\",\n            \"object_not_found\": \"Objet non trouvé\",\n            \"play_cutscene\": \"Chargement de la cinématique %d\",\n            \"play_demo\": \"Chargement de la démo %d\",\n            \"play_level\": \"Chargement de %s\",\n            \"pos_lara_missing\": \"Lara n'est pas présente\",\n            \"pos_lara_pos_fmt\": \"Salle : %d\\nPosition : %.3f, %.3f, %.3f\\nRotation : %.3f, %.3f, %.3f\",\n            \"pos_level_fmt\": \"Niveau %d\",\n            \"pos_level_fmt_cutscene\": \"Cinématique %d\",\n            \"pos_level_fmt_demo\": \"Démo %d\",\n            \"quick_load\": \"\\\\{review}Emplacement de chargement rapide %d\",\n            \"quick_load_fail_no_bound_slot\": \"\\\\{review}Aucun emplacement de sauvegarde n'est actuellement assigné\",\n            \"quick_load_fail_unavailable_bound_slot\": \"\\\\{review}L'emplacement de sauvegarde assigné n'est pas disponible\",\n            \"quick_save\": \"\\\\{review}Sauvegarde rapide\",\n            \"quick_save_fail_no_slots\": \"\\\\{review}Aucun emplacement de sauvegarde rapide n'est configuré\",\n            \"save_game\": \"Partie sauvegardée dans l'emplacement de sauvegarde %d\",\n            \"save_game_fail_invalid_slot\": \"Emplacement de sauvegarde %d invalide\",\n            \"sound_available_samples\": \"Effets sonores disponibles : %s\",\n            \"sound_playing_sample\": \"Lecture de l'effet sonore %d\",\n            \"speed_get\": \"Vitesse actuelle : %d\",\n            \"speed_set\": \"Vitesse réglée sur %d\",\n            \"strings_failed\": \"\\\\{review}Échec du rechargement des fichiers de langue\",\n            \"strings_reloaded\": \"\\\\{review}Fichiers de langue rechargés\",\n            \"textures_off\": \"\\\\{review}Textures : désactivées\",\n            \"textures_on\": \"\\\\{review}Textures : activées\",\n            \"trapezoid_filter_off\": \"Filtre trapézoïdal désactivé\",\n            \"trapezoid_filter_on\": \"Filtre trapézoïdal activé\",\n            \"ui_off\": \"Interface désactivée\",\n            \"ui_on\": \"Interface activée\",\n            \"unknown_command\": \"Commande inconnue : %s\",\n            \"upscaling_factor\": \"\\\\{review}Facteur de mise à l'échelle : x%d\",\n            \"wireframe_mode_off\": \"\\\\{review}Mode fil de fer : désactivé\",\n            \"wireframe_mode_on\": \"\\\\{review}Mode fil de fer : activé\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"\\\\{review}Animation : \",\n            \"debug_animation_state\": \"\\\\{review}État : \",\n            \"debug_camera_pos\": \"\\\\{review}Origine de la caméra : \",\n            \"debug_camera_target\": \"\\\\{review}Cible de la caméra : \",\n            \"debug_immune\": \"\\\\{review}Invulnérabilité activée\",\n            \"debug_position\": \"\\\\{review}Position : \",\n            \"debug_rotation\": \"\\\\{review}Rotation : \",\n            \"debug_speed\": \"\\\\{review}Vitesse : \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"\\\\{review}Supprimer\",\n            \"delete_save_confirm\": \"\\\\{review}Supprimer cette sauvegarde ?\",\n            \"delete_save_failed\": \"\\\\{review}Échec de la suppression de la sauvegarde choisie.\",\n            \"delete_save_no\": \"\\\\{review}Non\",\n            \"delete_save_yes\": \"\\\\{review}Oui\",\n            \"exit_game\": \"Quitter le jeu\",\n            \"exit_to_title\": \"Retourner à l'écran titre\",\n            \"load_game\": \"Charger une partie\",\n            \"mode_new_game\": \"Nouvelle partie\",\n            \"mode_new_game_jp\": \"Version japonaise\",\n            \"mode_new_game_jp_plus\": \"Version japonaise+\",\n            \"mode_new_game_plus\": \"Nouvelle partie+\",\n            \"new_game\": \"Nouvelle partie\",\n            \"play_previous_levels\": \"\\\\{review}Jouer les niveaux précédents\",\n            \"restart_level\": \"Recommencer le niveau\",\n            \"save_game\": \"Sauvegarder la partie\",\n            \"save_slot_unsupported\": \"\\\\{review}Cette sauvegarde ne prend pas en charge cette fonctionnalité.\",\n            \"select_level\": \"Sélectionner le niveau\",\n            \"select_mod\": \"\\\\{review}Sélectionner le jeu\",\n            \"select_mode\": \"Sélectionner le mode\",\n            \"select_save\": \"\\\\{review}Sélectionner Enregistrer\",\n            \"story_so_far\": \"L'histoire jusqu'à maintenant...\",\n            \"switch_mod\": \"\\\\{review}Changer de jeu\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"Êtes-vous sûr ?\",\n            \"continue\": \"Continuer\",\n            \"exit_to_title\": \"Retourner à l'écran titre ?\",\n            \"no\": \"Non\",\n            \"paused\": \"Pause\",\n            \"quit\": \"Quitter\",\n            \"yes\": \"Oui\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"\\\\{review}Avancer le cadre\",\n            \"camera_move_prompt\": \"Déplacer la caméra\",\n            \"camera_reset_prompt\": \"Réinitialiser la caméra\",\n            \"camera_roll_prompt\": \"Incliner la caméra\",\n            \"camera_rotate_90_prompt\": \"Pivoter de 90 degrés\",\n            \"camera_rotate_prompt\": \"Pivoter la caméra\",\n            \"change_lara_pose\": \"\\\\{review}Changer de pose\",\n            \"fov_prompt\": \"Ajuster le FOV\",\n            \"lara_move_prompt\": \"\\\\{review}Déplacer Lara\",\n            \"lara_reset_prompt\": \"\\\\{review}Réinitialiser Lara\",\n            \"lara_roll_prompt\": \"\\\\{review}Rouler Lara\",\n            \"lara_rotate_90_prompt\": \"\\\\{review}Faire pivoter Lara de 90°\",\n            \"lara_rotate_prompt\": \"\\\\{review}Faire pivoter Lara\",\n            \"snap_prompt\": \"Prendre une photo\",\n            \"title_camera_pos\": \"Mode photo\",\n            \"title_lara_pos\": \"\\\\{review}Déplacer Lara\",\n            \"toggle_help\": \"Basculer l'aide\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"\\\\{review}Les paramètres sont désactivés pour cet ensemble de niveaux.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Modifier la valeur\",\n                \"frozen_option_disclaimer\": \"Ce paramètre est imposé par le créateur du niveau et ne peut pas être modifié.\",\n                \"hue\": \"Teinte\",\n                \"lightness\": \"Luminosité\",\n                \"restore_default\": \"Restaurer\",\n                \"toggle_help\": \"Active l'aide\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Manette\",\n                    \"keyboard\": \"Clavier\",\n                },\n                \"customize\": \"Personnaliser les contrôles\",\n                \"layout\": {\n                    \"custom_1\": \"Contrôles personnalisés 1\",\n                    \"custom_2\": \"Contrôles personnalisés 2\",\n                    \"custom_3\": \"Contrôles personnalisés 3\",\n                    \"default\": \"Touches par défaut\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Mouvement\",\n                    \"items\": \"Objets\",\n                    \"misc\": \"Divers\",\n                    \"system\": \"Système\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Contrôles\",\n                    \"fixes\": \"Correctifs\",\n                    \"general\": \"Général\",\n                    \"mods\": \"Mods\",\n                    \"presets\": \"\\\\{review}Préréglages\",\n                },\n                \"title\": \"Options de jeu\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"\\\\{review}Barres\",\n                    \"rendering\": \"Rendu\",\n                    \"stats\": \"\\\\{review}Statistiques\",\n                    \"ui\": \"UI\",\n                    \"visuals\": \"Visuels\",\n                },\n                \"title\": \"Options graphiques\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"\\\\{review}Divers\",\n                    \"volume\": \"\\\\{review}Volume\",\n                },\n                \"title\": \"Options sonores\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Tirs Réussis/Effectués\",\n            \"ammo_hits\": \"\\\\{review}Coups\",\n            \"ammo_used\": \"\\\\{review}Munitions utilisées\",\n            \"assault_best_time_fmt\": \"\\\\{review}%s\",\n            \"assault_finish\": \"\\\\{review}Fin\",\n            \"assault_no_times_set\": \"\\\\{review}Aucun temps défini\",\n            \"assault_other_times_fmt\": \"\\\\{review}%s\",\n            \"assault_title\": \"\\\\{review}MEILLEURS TEMPS\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Statistiques bonus\",\n            \"crystals\": \"\\\\{review}Cristaux\",\n            \"deaths\": \"Morts\",\n            \"detail_fmt\": \"%d sur %d\",\n            \"distance_travelled\": \"Distance parcourue\",\n            \"final_statistics\": \"Statistiques finales\",\n            \"gym_assault_course\": \"\\\\{review}Parcours d'assaut\",\n            \"gym_racetrack_course\": \"\\\\{review}Circuit de course\",\n            \"kills\": \"Ennemis Tués\",\n            \"level\": \"\\\\{review}Niveau\",\n            \"medipacks_used\": \"Trousses de soin utilisées\",\n            \"none\": \"\\\\{review}Aucun\",\n            \"pickups\": \"Objets ramassés\",\n            \"secrets\": \"Secrets\",\n            \"time_taken\": \"Temps écoulé\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Active la tresse de Lara.\",\n            },\n            \"cheats\": {\n                \"help\": \"Active ou désactive les codes de triche.\",\n            },\n            \"clear\": {\n                \"help\": \"\\\\{review}Efface les journaux de la console visibles.\",\n            },\n            \"debug\": {\n                \"help\": \"Affiche les informations de débogage visuel.\",\n            },\n            \"drain\": {\n                \"help\": \"Retire toute l'eau de la salle actuelle.\",\n            },\n            \"end_level\": {\n                \"help\": \"Met fin au niveau actuel.\",\n            },\n            \"exit\": {\n                \"help\": \"Quitte le jeu.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Bascule les salles à leur état secondaire.\",\n            },\n            \"flood\": {\n                \"help\": \"Remplit la salle actuelle d'eau.\",\n            },\n            \"fly\": {\n                \"help\": \"Active ou désactive le code pour voler.\",\n            },\n            \"fps\": {\n                \"help\": \"Change le nombre de FPS.\",\n            },\n            \"give\": {\n                \"help\": \"Ajoute un élément à l'inventaire de Lara.\",\n                \"invalid_secret\": \"\\\\{review}Secret invalide : %s (secrets valides : %s)\",\n                \"secret_given\": \"\\\\{review}Secret ajouté %s\",\n                \"secret_list\": \"\\\\{review}Secrets collectés : %d sur %d (%s)\",\n                \"secret_none\": \"\\\\{review}Secrets collectés : %d sur %d\",\n                \"secret_taken\": \"\\\\{review}Secret retiré %s\",\n            },\n            \"give_secret\": {\n                \"help\": \"\\\\{review}Liste les secrets de Lara, ou prend/donne un secret par numéro.\",\n            },\n            \"heal\": {\n                \"help\": \"Redonne toute sa santé à Lara.\",\n            },\n            \"help\": {\n                \"help\": \"Affiche l'aide générale ou l'aide détaillée pour une commande.\",\n                \"list\": \"Commandes disponibles :\",\n            },\n            \"hp\": {\n                \"help\": \"Fixe la santé de Lara à la valeur spécifiée.\",\n            },\n            \"immune\": {\n                \"help\": \"\\\\{review}Active l'invulnérabilité. (Lara peut toujours être tuée dans certaines circonstances.)\",\n                \"off\": \"\\\\{review}Lara est maintenant vulnérable\",\n                \"on\": \"\\\\{review}Lara est maintenant imperméable aux dégâts\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"\\\\{review}Active ou désactive le sprint infini.\",\n                \"off\": \"\\\\{review}Lara ne peut plus sprinter indéfiniment\",\n                \"on\": \"\\\\{review}Lara peut maintenant sprinter indéfiniment\",\n            },\n            \"kill\": {\n                \"help\": \"Tue les ennemis à proximité.\",\n            },\n            \"lighting\": {\n                \"help\": \"\\\\{review}Active le système d'éclairage.\",\n            },\n            \"load\": {\n                \"help\": \"\\\\{review}Charge la partie depuis l'emplacement de sauvegarde donné ou depuis une sauvegarde rapide.\",\n            },\n            \"lua\": {\n                \"help\": \"\\\\{review}Exécute la chaîne de code Lua donnée.\",\n                \"runtime_error\": \"\\\\{review}Erreur d'exécution Lua : %s\",\n                \"syntax_error\": \"\\\\{review}Erreur de syntaxe Lua : %s\",\n            },\n            \"mod\": {\n                \"help\": \"\\\\{review}Bascule vers le mod spécifié et redémarre le jeu.\",\n            },\n            \"music\": {\n                \"help\": \"Lance la piste musicale pour l'identifiant donné.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Lance la cinématique correspondant au numéro donné.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Lance la démo correspondant au numéro donné.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Lance le niveau de gym.\",\n            },\n            \"play_level\": {\n                \"help\": \"Lance un niveau correspondant au nom ou au numéro donné.\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Piste musicale invalide\",\n                \"stopped\": \"\\\\{review}Musique arrêtée\",\n                \"track\": \"Lecture de la piste musicale %d\",\n            },\n            \"pos\": {\n                \"help\": \"Montre la position de Lara.\",\n            },\n            \"save\": {\n                \"help\": \"\\\\{review}Sauvegarde la partie dans l'emplacement de sauvegarde donné ou dans le prochain emplacement de sauvegarde rapide.\",\n            },\n            \"screenshot\": {\n                \"help\": \"\\\\{review}Enregistre une capture d'écran sur le disque, avec un chemin optionnel.\",\n            },\n            \"set\": {\n                \"help\": \"Affiche ou met à jour le paramètre de configuration donné.\",\n            },\n            \"sfx\": {\n                \"help\": \"Joue un effet sonore avec l'identifiant donné.\",\n            },\n            \"spawn\": {\n                \"fail\": \"\\\\{review}Échec de l'apparition de l'objet demandé\",\n                \"success\": \"\\\\{review}Objet demandé apparu près de Lara\",\n            },\n            \"speed\": {\n                \"help\": \"Change la vitesse du jeu.\",\n            },\n            \"strings\": {\n                \"help\": \"\\\\{review}Recharge les fichiers de langue actuels depuis le disque.\",\n            },\n            \"teleport\": {\n                \"item\": \"Téléporté à l'objet : %d\",\n                \"item_fail\": \"Échec du téléport vers l'objet : %d\",\n                \"object\": \"Téléporté à l'objet : %s\",\n                \"object_fail\": \"Échec du téléport vers l'objet : %s\",\n                \"pos\": \"Téléporté à la position : %.3f %.3f %.3f\",\n                \"pos_fail\": \"Échec du téléport vers la position : %.3f %.3f %.3f\",\n                \"room\": \"Téléporté à la salle : %d\",\n                \"room_fail\": \"Échec du téléport vers la salle : %d\",\n            },\n            \"textures\": {\n                \"help\": \"\\\\{review}Active ou désactive les textures.\",\n            },\n            \"title\": {\n                \"help\": \"Retourne à l'écran titre.\",\n            },\n            \"tp\": {\n                \"help\": \"Téléporte Lara à une position ou un numéro de salle donné.\",\n            },\n            \"trigger\": {\n                \"help\": \"\\\\{review}Déclenche ou annule le déclenchement d'un élément par id, nom de l'élément ou nom de l'objet.\",\n                \"invalid_item\": \"\\\\{review}Élément invalide : %s\",\n                \"no_match\": \"\\\\{review}Cible inconnue : %s\",\n                \"not_found\": \"\\\\{review}Aucun élément correspondant trouvé pour : %s\",\n                \"triggered\": \"\\\\{review}Élément(s) déclenché(s) : %s\",\n                \"untriggered\": \"\\\\{review}Élément(s) non déclenché(s) : %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Active/désactive la synchronisation verticale.\",\n            },\n            \"weather\": {\n                \"help\": \"\\\\{review}Change le type de météo actuel.\",\n                \"invalid\": \"\\\\{review}Météo invalide : %s (valide : %s)\",\n                \"set\": \"\\\\{review}Météo réglée sur %s\",\n            },\n            \"winston\": {\n                \"dead\": \"\\\\{review}Votre majordome est mort. Monstre !\",\n                \"spawn_failed\": \"\\\\{review}Échec d'invoquer Winston\",\n                \"spawned\": \"\\\\{review}Fait apparaître Winston près de Lara\",\n                \"teleported\": \"\\\\{review}Téléporté Winston près de Lara\",\n            },\n            \"wireframe\": {\n                \"help\": \"Active/désactive le rendu en fil de fer.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"\\\\{review}TR1 PC\",\n            \"tr1_ps1\": \"\\\\{review}TR1 PS1\",\n            \"tr2_pc\": \"\\\\{review}TR2 PC\",\n            \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n            \"tr3_pc\": \"\\\\{review}TR3 PC\",\n            \"tr3_ps1\": \"\\\\{review}TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"\\\\{review}TR2 PC\",\n                \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n                \"tr3_pc\": \"\\\\{review}TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"\\\\{review}Par défaut\",\n                \"golden_sophia\": \"\\\\{review}Sophia Dorée\",\n                \"sophia\": \"\\\\{review}Sophia\",\n                \"tr1_bacon_lara\": \"\\\\{review}Lara Bacon\",\n                \"tr1_classic\": \"\\\\{review}TR1 Classique\",\n                \"tr1_combo\": \"\\\\{review}TR1 Combinée\",\n                \"tr1_golden_bacon_lara\": \"\\\\{review}Lara Bacon Dorée\",\n                \"tr1_golden_lara\": \"\\\\{review}TR1 Lara Dorée\",\n                \"tr1_gym\": \"\\\\{review}TR1 Entraînement\",\n                \"tr1_mauled\": \"\\\\{review}TR1 Mutilée\",\n                \"tr1_ngage\": \"\\\\{review}TR1 N-Gage\",\n                \"tr23_golden_lara\": \"\\\\{review}TR2/3 Lara Dorée\",\n                \"tr2_bomber_jacket\": \"\\\\{review}Blouson aviateur\",\n                \"tr2_classic\": \"\\\\{review}TR2 Classique\",\n                \"tr2_diving_suit\": \"\\\\{review}Combinaison de plongée 1\",\n                \"tr2_diving_suit_alpha\": \"\\\\{review}Combinaison de plongée 2\",\n                \"tr2_gym\": \"\\\\{review}TR2 Entraînement\",\n                \"tr2_robe\": \"\\\\{review}Robe\",\n                \"tr2_vegas\": \"\\\\{review}Las Vegas\",\n                \"tr3_antarctica\": \"\\\\{review}Antarctique\",\n                \"tr3_catsuit\": \"\\\\{review}Combinaison moulante\",\n                \"tr3_classic\": \"\\\\{review}TR3 Classique\",\n                \"tr3_gym\": \"\\\\{review}TR3 Entraînement\",\n                \"tr3_nevada\": \"\\\\{review}Nevada\",\n                \"tr3_south_pacific\": \"\\\\{review}Pacifique Sud\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"\\\\{review}Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"\\\\{review}Démo Tomb Raider I\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"\\\\{review}Affaires Inachevées\",\n            },\n            \"tr2\": {\n                \"title\": \"\\\\{review}Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"\\\\{review}Le Masque d'Or\",\n            },\n            \"tr3\": {\n                \"title\": \"\\\\{review}Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"\\\\{review}L'Artéfact Perdu\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"\\\\{review}Individuel\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"\\\\{review}Partagé\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"Automatique\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"\\\\{review}Noir\",\n            \"BK_IMAGE\": \"\\\\{review}Image\",\n            \"BK_MONOCHROME\": \"\\\\{review}Monochrome\",\n            \"BK_MONOCHROME_COOL\": \"\\\\{review}Monochrome (froid)\",\n            \"BK_MONOCHROME_WARM\": \"\\\\{review}Monochrome (chaud)\",\n            \"BK_NONE\": \"\\\\{review}Transparent\",\n            \"BK_PATTERN_STATIC\": \"\\\\{review}Statique\",\n            \"BK_PATTERN_WAVE\": \"\\\\{review}Vague\",\n            \"BK_TRANSPARENT_DARK\": \"\\\\{review}Très sombre\",\n            \"BK_TRANSPARENT_MEDIUM\": \"\\\\{review}Sombre\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"Toujours\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"Boss uniquement\",\n            \"BAR_SHOW_MODE_NEVER\": \"Jamais\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"\\\\{review}Aucun\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"\\\\{review}Perspective\",\n            \"BILLBOARD_LOCK_ROLL\": \"\\\\{review}Roulis\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"\\\\{review}Roulis et tangage\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"\\\\{review}Désactivé\",\n            \"BLOOD_EFFECTS_PINK\": \"\\\\{review}Rose\",\n            \"BLOOD_EFFECTS_RED\": \"\\\\{review}Rouge\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"\\\\{review}TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"\\\\{review}Par défaut\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"\\\\{review}Jamais\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"\\\\{review}Submergé\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"\\\\{review}Manette\",\n            \"INPUT_BACKEND_KEYBOARD\": \"\\\\{review}Clavier\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Action\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Reculer la caméra\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Abaisser la caméra\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Avancer la caméra\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Caméra vers la gauche\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"\\\\{review}Réinitialisation de la caméra\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Caméra vers la droite\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Réhausser la caméra\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"\\\\{review}Changer de tenue\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Changer de cible\",\n            \"INPUT_ROLE_CROUCH\": \"\\\\{review}S'accroupir\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"\\\\{review}Contraste de l'éclairage en cycle\",\n            \"INPUT_ROLE_DOWN\": \"Retour\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Dégainer\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Console de développement\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"\\\\{review}Équiper les pistolets automatiques\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"\\\\{review}Équiper le Desert Eagle\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"\\\\{review}Équiper le lance-grenades\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"\\\\{review}Équiper le harpon\",\n            \"INPUT_ROLE_EQUIP_M16\": \"\\\\{review}Équiper le M16\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"S'équiper des magnums\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"\\\\{review}Équiper le MP5\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"S'équiper des pistolets\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"\\\\{review}Équiper le lance-roquettes\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"S'équiper du fusil à pompe\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"S'équiper des Uzi\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Triche de vol\",\n            \"INPUT_ROLE_FPS\": \"Afficher les FPS\",\n            \"INPUT_ROLE_INVENTORY\": \"Inventaire\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Triche d'objet\",\n            \"INPUT_ROLE_JUMP\": \"Sauter\",\n            \"INPUT_ROLE_LEFT\": \"Gauche\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Passer le niveau\",\n            \"INPUT_ROLE_LOAD\": \"\\\\{review}Charger\",\n            \"INPUT_ROLE_LOOK\": \"Regarder\",\n            \"INPUT_ROLE_PAUSE\": \"Pause\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"\\\\{review}Chargement rapide\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"\\\\{review}Sauvegarde rapide\",\n            \"INPUT_ROLE_RIGHT\": \"Droite\",\n            \"INPUT_ROLE_ROLL\": \"Roulade\",\n            \"INPUT_ROLE_SAVE\": \"\\\\{review}Sauvegarder\",\n            \"INPUT_ROLE_SCREENSHOT\": \"\\\\{review}Capture d'écran\",\n            \"INPUT_ROLE_SLOW\": \"Marcher\",\n            \"INPUT_ROLE_SPRINT\": \"\\\\{review}Sprint\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Pas à gauche\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Pas à droite\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"\\\\{review}Changer la taille des bordures\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"\\\\{review}Changer le facteur de mise à l'échelle\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"\\\\{review}Basculer le filtre bilinéaire\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"\\\\{review}Basculer en plein écran\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Basculer le mode photo\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"\\\\{review}Basculer les textures\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"\\\\{review}Basculer le filtre trapézoïdal\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"Basculer l'interface utilisateur\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"\\\\{review}Basculer le fil de fer\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Vitesse turbo\",\n            \"INPUT_ROLE_UP\": \"Courir\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Grand trousse de soins\",\n            \"INPUT_ROLE_USE_FLARE\": \"\\\\{review}Flare\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Petite trousse de soins\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"\\\\{review}Désactivé\",\n            \"JUMP_LOCK_LEGACY\": \"\\\\{review}Hérité\",\n            \"JUMP_LOCK_TUNED\": \"\\\\{review}Ajusté\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"Élevé\",\n            \"LIGHTING_CONTRAST_LOW\": \"Bas\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Moyen\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"\\\\{review}Toujours\",\n            \"LOADING_SCREENS_DISABLED\": \"\\\\{review}Désactivé\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"\\\\{review}Nouvelles parties\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"\\\\{review}Amélioré\",\n            \"LOOK_MODE_RESTRICTED\": \"\\\\{review}Restreint\",\n            \"LOOK_MODE_UNRESTRICTED\": \"\\\\{review}Illimité\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"Tout\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"Aucune\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Pistes musicales\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"\\\\{review}Balayage multiple\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"\\\\{review}Balayage unique\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"\\\\{review}Équiper & ranger\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"\\\\{review}Tirer seulement\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"\\\\{review}Cercle\",\n            \"SHADOW_TYPE_OCTAGON\": \"\\\\{review}Octogone\",\n            \"SHADOW_TYPE_SPRITE\": \"\\\\{review}Sprite\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"\\\\{review}Simple\",\n            \"STATS_STYLE_BORDERED\": \"\\\\{review}Encadré\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"\\\\{review}Désactivé\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"\\\\{review}Opaque\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"\\\\{review}Transparent\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Complet\",\n            \"TARGET_LOCK_MODE_NONE\": \"Aucun\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Hybride\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Bilinéraire\",\n            \"TEXTURE_FILTER_POINT\": \"\\\\{review}Point\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"En bas centré\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"En bas à gauche\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"En bas à droite\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"En haut centré\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"En haut à gauche\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"En haut à droit\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"\\\\{review}Réparé\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"\\\\{review}Volume ambiant\",\n            \"description\": \"\\\\{review}Ajuste le volume ambiant.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"\\\\{review}Volume des cinématiques\",\n            \"description\": \"\\\\{review}Ajuste le volume des cinématiques en jeu.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Micro sur Lara\",\n            \"description\": \"Place le micro au niveau de Lara. Si cette option est désactivée, le micro sera placé au niveau de la caméra.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"\\\\{review}Jouer de la musique dans l'inventaire\",\n            \"description\": \"\\\\{review}Permet aux sons du jeu, à l'ambiance et à la musique de continuer à jouer dans l'écran d'inventaire.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Musique du menu principal\",\n            \"description\": \"Active ou non la musique dans le menu principal.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Hauteurs aléatoires des sons\",\n            \"description\": \"Modifie aléatoirement et légèrement la hauteur des effets sonores, afin de leur donner un peu de variation.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"\\\\{review}PS1 remplacements SFX\",\n            \"description\": \"\\\\{review}Active des remplacements spécifiques d'effets sonores en utilisant les équivalents PS1.\\n\\n- Tir d'Uzi (TR1 uniquement)\\n- Sons de pieds nus de Lara (TR2 uniquement)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"\\\\{review}SFX anim. sous l'eau\",\n            \"description\": \"\\\\{review}Permet de contrôler la lecture de certains effets sonores d'animation — pour des objets comme des portes ou des trappes — lorsque la caméra est sous l'eau.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Corriger le son de chaîne des blocs\",\n            \"description\": \"Corrige le fait que le son du secret se joue par erreur lors de l'utilisation de la Clé en Or dans la Tombe de Tihocan.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Superposer le son du secret\",\n            \"description\": \"Corrige le fait que la musique est coupée à la découverte d'un secret.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Superposer les voix des ennemis\",\n            \"description\": \"Corrige le fait que la musique est coupée lorsque les ennemis se mettent à parler.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"\\\\{review}Volume des FMV\",\n            \"description\": \"\\\\{review}Ajuste le volume des films.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"\\\\{review}Volume ambiant (inventaire)\",\n            \"description\": \"\\\\{review}Ajuste le volume ambiant dans l'écran d'inventaire.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"\\\\{review}Volume de la musique (inventaire)\",\n            \"description\": \"\\\\{review}Ajuste le volume de la musique dans l'écran d'inventaire.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Mémoriser les musiques jouées\",\n            \"description\": \"Mémorise les musiques qui ont déjà été jouées, pour ne pas les répéter après un rechargement de partie.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{review}\\\\{icon music} Volume maître\",\n            \"description\": \"\\\\{review}Ajuste le volume de tous les sons du jeu. Les autres réglages sont relatifs à ce volume.\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Reprendre la musique au chargement\",\n            \"description\": \"Restaure la lecture des pistes audio qui étaient en cours de lecture lors de la sauvegarde.\\n- Aucune : ne restaure jamais la lecture au chargement.\\n- Pistes musicales : reprend uniquement les pistes musicales (hors ambiance) au chargement.\\n- Tout : restaure tout type de piste audio au chargement.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"\\\\{review}Volume de la musique\",\n            \"description\": \"\\\\{review}Ajuste le volume de la musique.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"\\\\{review}Couper le son lorsque la fenêtre perd le focus\",\n            \"description\": \"\\\\{review}Coupe toute la musique et les effets sonores lorsque la fenêtre du jeu n'est pas au premier plan.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Volume du son\",\n            \"description\": \"Ajuste le volume des effets sonores.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"\\\\{review}Volume ambiant (sous l'eau)\",\n            \"description\": \"\\\\{review}Ajuste le volume ambiant sous l'eau.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"\\\\{review}Volume de la musique (sous l'eau)\",\n            \"description\": \"\\\\{review}Ajuste le volume de la musique sous l'eau.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"\\\\{review}Temps de torche sans fin\",\n            \"description\": \"\\\\{review}Empêche les torches portatives de s'éteindre. Les torches lancées s'éteindront toujours normalement.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"\\\\{review}Sprint infini\",\n            \"description\": \"\\\\{review}Empêche Lara de se fatiguer lorsqu'elle sprinte. Les obstacles l'arrêteront toujours.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"\\\\{review}Politique d'hostilité des alliés\",\n            \"description\": \"\\\\{review}Contrôle la réaction des unités amies lorsqu'elles subissent des dégâts.\\n\\n- Individuel : chaque allié change d'hostilité de manière indépendante (style TR3).\\n- Partagé : tous les alliés deviennent hostiles ensemble (style moine TR2).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Vitesse de la caméra\",\n            \"description\": \"Modifie la vitesse à laquelle se déplace la caméra manuelle.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Modifier le comportement d'apparition de Pierre\",\n            \"description\": \"Fait en sorte que l'apparition d'un nouveau Pierre (qui s'échappe) remplace l'éventuel Pierre (qui s'échappe) déjà existant.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"\\\\{review}Comportement de noyade des créatures\",\n            \"description\": \"\\\\{review}Contrôle le comportement des créatures terrestres dans les zones d’eau.\\n\\n- Jamais: les créatures terrestres ne se noient jamais (style TR1).\\n- Par défaut: les créatures terrestres se noient dans une eau d’une profondeur de 2 clics ou plus (style TR2/3).\\n- Submergé: les créatures terrestres se noient uniquement lorsqu’elles sont entièrement immergées.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"\\\\{review}Supprimer les armes supplémentaires\",\n            \"description\": \"\\\\{review}Supprime toutes les armes et munitions ramassables dans le jeu sauf les pistolets (pour les défis uniquement avec pistolets).\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Garder les dégâts\",\n            \"description\": \"Empêche que Lara ne guérisse automatiquement entre chaque niveau (pour les No Heal challenges).\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Supprimer les trousses de soin\",\n            \"description\": \"Supprime toutes les trousses de soin de tous les niveaux (pour les No Meds challenges).\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Supprimer la colision du le cadavre du T-Rex\",\n            \"description\": \"Supprime toutes les collisions avec le cadavre T-Rex. Cela aide lorsque son cadavre bloque un passage.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"\\\\{review}Autoriser le ciblage des alliés\",\n            \"description\": \"\\\\{review}Permet à Lara de cibler des alliés, tels que des moines. Si désactivé, les alliés seront immunisés contre les munitions de Lara.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Présélection des bonnes clés\",\n            \"description\": \"Lorsque Lara appuie sur Action en face d'une serreure ou un emplacement de puzzle et qu'elle a l'objet correspondant dans son inventaire, cet objet sera présélectionné.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"\\\\{review}Trébuchement en pente arrière\",\n            \"description\": \"\\\\{review}Fait trébucher Lara si elle saute en arrière et qu’il y a une pente derrière elle (TR3). Si désactivé, Lara s’arrête net contre la pente (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"\\\\{review}Déclencheurs de sacs mortuaires\",\n            \"description\": \"\\\\{review}Permet la suppression des ennemis tués lorsque Lara traverse certains déclencheurs dans certains niveaux. Si désactivé, les ennemis morts seront toujours affichés.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"\\\\{review}Activer le tremblement des rochers\",\n            \"description\": \"\\\\{review}Si activé, la caméra tremblera lorsqu’un rocher est en mouvement.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"\\\\{review}Grenades rebondissantes\",\n            \"description\": \"\\\\{review}Active le comportement des grenades à la TR3 : elles ricochent sur les murs et les pentes et produisent un rayon d'explosion plus large, mais au prix d'une vitesse réduite.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Triche\",\n            \"description\": \"Active plusieurs fonctions de triche :\\n- L: termine immédiatement le niveau.\\n- I: donne toutes les armes à Lara, le maximum en munitions et trousses de soin, et tous les éléments utiles au niveau en cours.\\n- O: Fait voler Lara (nager dans les airs).\\n  - Touche MARCHER : Revenir au sol.\\n  - Touche ESPACE : Ouvre les portes fermées (peut ne pas fonctionner sur certaines).\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"\\\\{review}Séquences scénarisées\",\n            \"description\": \"\\\\{review}Active les séquences scénarisées au début de certains niveaux.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Statistiques dans la boussole\",\n            \"description\": \"Affiche les statistiques du niveau lorsque la boussole est sélectionnée.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Console\",\n            \"description\": \"Active la console de développement.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"\\\\{review}Chutes contrôlées\",\n            \"description\": \"\\\\{review}Permet à Lara de se retourner en l'air et d’attraper la corniche qu'elle vient de quitter, si la touche d'action est maintenue pendant la chute.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"\\\\{review}Saut en sortie de conduit\",\n            \"description\": \"\\\\{review}Permet à Lara de sauter hors des passages étroits.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"\\\\{review}Inclinaison en rampant\",\n            \"description\": \"\\\\{review}Aligne la rotation de Lara sur la géométrie du sol lorsqu'elle rampe.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"\\\\{review}Ramper\",\n            \"description\": \"\\\\{review}Permet à Lara de s’accroupir et de ramper.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"\\\\{review}Écrans de crédits\",\n            \"description\": \"\\\\{review}Active les écrans de crédits affichés après avoir terminé le jeu. N'influence pas l'écran des statistiques finales.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"\\\\{review}Roulade accroupie\",\n            \"description\": \"\\\\{review}Permet à Lara d'effectuer une roulade avant en position accroupie en appuyant sur sprint.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Activer les cutscenes\",\n            \"description\": \"Active les cutscenes en jeu.\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Activer le mode démo\",\n            \"description\": \"Active le mode démo dans le menu principal.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"\\\\{review}Randomiser l'angle de départ des ennemis\",\n            \"description\": \"\\\\{review}Applique un angle aléatoire supplémentaire à certains ennemis lorsqu'ils sont initialisés.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Sauvegarde les effets\",\n            \"description\": \"Améliore les sauvegardes afin que les effets graphiques, la brume des cascades, les émetteurs de flammes, etc. soient sauvegardés au lieu de disparaître au chargement.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"Activer les FMV\",\n            \"description\": \"Active les FMV en jeu.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Autres modes de jeu\",\n            \"description\": \"\\\\{review}Permet de sélectionner de nouveaux modes de jeu dans le menu nouvelle partie du passeport au menu principal.\\n- Nouvelle partie+ : Déverrouille toutes les armes avec munitions infinies ; les ennemis ont le double de points de vie.\\n- Version Japonaise : Les armes font le double de dégâts.\\n- Version Japonaise+ : Combine le mode Version Japonaise et le mode Nouvelle partie+.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"\\\\{review}Caméra pose\",\n            \"description\": \"\\\\{review}Ajuste la caméra pour qu'elle soit orientée vers Lara pendant sa animation de pose. Appuyer sur regarder pour réinitialiser la caméra.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Inversion verticale du mode regarder\",\n            \"description\": \"Inverse l'axe Y lorsque Lara regarde autour d'elle.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Description des objets\",\n            \"description\": \"Pour les niveaux personnalisés - permet d'afficher les descriptions des objets dans l'inventaire lorsque le créateur du niveau a fourni ces informations.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Sauts périlleux\",\n            \"description\": \"\\\\{review}Activer les demi-tours en plein saut et les cabrioles.\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"\\\\{review}Activer les blocs-poussoirs mortels\",\n            \"description\": \"\\\\{review}Si activé, lorsqu'un bloc-poussoir tombe de l'air et atterrit sur Lara, il la tuera immédiatement. Sinon, Lara se retrouvera coincée sur le dessus du bloc et survivra.\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Contrôle du saut sur place\",\n            \"description\": \"Permet à Lara d'avancer ou de reculer pendant les sauts sur place lorsque la touche correspondante est enfoncée, comme possible depuis TR2.\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"\\\\{review}Sauts depuis une corniche\",\n            \"description\": \"\\\\{review}Permet à Lara de sauter vers le haut ou en arrière lorsqu'elle est suspendue à une corniche, à condition qu'il y ait une surface solide devant elle pour prendre appui.\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Afficher le contenu juridique\",\n            \"description\": \"Affiche l'écran juridique et la FMV Core Design au début du jeu.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"\\\\{review}Caméra manuelle\",\n            \"description\": \"\\\\{review}Active les touches de la caméra (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}) utilisées pour contrôler la caméra du mode photo, pour également faire pivoter la caméra en jeu.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"\\\\{review}Pirouettes sur place\",\n            \"description\": \"\\\\{review}Permet à Lara d'effectuer une rotation en l'air lors d'un saut sur place. Appuyez simultanément sur les touches de saut et de roulade en étant immobile.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Aides objets à ramasser\",\n            \"description\": \"Active un scintillement intermittent à proximité des objets à ramasser pour les mettre en valeur.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"\\\\{review}Jouer les niveaux précédents\",\n            \"description\": \"\\\\{review}Active les fonctionnalités \\\"Jouer aux niveaux précédents\\\" et \\\"Histoire jusqu'à présent...\\\" dans l'écran de sélection Nouveau Jeu.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"\\\\{review}Ramper réactif\",\n            \"description\": \"\\\\{review}Active des améliorations par rapport aux mécaniques de ramper d’origine.\\n\\n- Permet de reprendre le rampement plus rapidement après un arrêt.\\n- Permet de passer de la course/sprint au rampement sans devoir s’arrêter d’abord.\\n- Permet de passer du rampement à la roulade accroupie (si activée) sans devoir s’accroupir manuellement.\\n- Permet de se tourner en position accroupie.\\n- Restaure l’animation de ramassage en rampant de Lara (hors torches).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"\\\\{review}Sprint réactif\",\n            \"description\": \"\\\\{review}Active un sprint plus réactif pour Lara.\\n\\n- permet de sprinter dès que Lara a de l'énergie, plutôt que d'attendre que son endurance soit pleine.\\n- permet de sprinter dans les escaliers sans être interrompue par l'animation de course normale.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Cristaux de sauvegarde\",\n            \"description\": \"Limite les sauvegardes aux débuts de niveau et aux cristaux de sauvegarde. Les niveaux ont un nombre limité de cristaux, à usage unique, comme sur PS1. Modifier cette option nécessite de recommencer le niveau.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"\\\\{review}Glissade-courir\",\n            \"description\": \"\\\\{review}Permet à Lara de commencer à courir immédiatement lorsqu'elle touche le sol après avoir glissé en avant sur une pente. Maintenez l'entrée avant pour activer.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"\\\\{review}Balancement lent sur corniche\",\n            \"description\": \"\\\\{review}Permet à Lara de se balancer lentement lorsqu’elle s’est accrochée à une corniche très fine (style TR3). Si désactivé, Lara se balancera brièvement avant de revenir à une position suspendue immobile (style TR1/2).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"\\\\{review}Déviation murale fluide\",\n            \"description\": \"\\\\{review}Permet à Lara de se remettre plus rapidement après avoir heurté un mur et qu'une touche de direction est maintenue avec l'avant.\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"\\\\{review}Collision de maillage souple\",\n            \"description\": \"\\\\{review}Permet à Lara de se déplacer en douceur contre les maillages statiques – comme dans TR4+ – plutôt que de s'arrêter brutalement.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"\\\\{review}Sprint\",\n            \"description\": \"\\\\{review}Permet à Lara de sprinter, comme dans TR3 et les suivants.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"\\\\{review}Boost de roulade de marche\",\n            \"description\": \"\\\\{review}Permet à Lara d'être propulsée d'un pas haut à un clic si la roulade est pressée près du bord.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Annulation du balancement\",\n            \"description\": \"Permet d'annuler l'animation de balancement à un rebord de Lara en relâchant puis en se raccrochant aussitôt, comme possible depuis TR2.\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Changement de cible\",\n            \"description\": \"Rend possible le changement de cible comme possible depuis TR4. Appuyez sur la touche Regarder tout en visant pour changer de cible.\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Comptabiliser du temps de jeu dans l'inventaire\",\n            \"description\": \"Fait avancer le compteur du jeu même quand l'inventaire est ouvert.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"\\\\{review}Basculer l'accroupissement\",\n            \"description\": \"\\\\{review}Permet à Lara de rester accroupie après avoir appuyé une fois sur la touche d'accroupissement. Appuyez de nouveau sur accroupissement pour vous relever.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"\\\\{review}Basculer le sprint\",\n            \"description\": \"\\\\{review}Permet à Lara de continuer à sprinter après avoir appuyé une fois sur la touche de sprint. Appuyez de nouveau sur sprint pour arrêter de sprinter.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Écran des statistiques finales\",\n            \"description\": \"Active un écran de statistiques finales du jeu qui apparait à la fin des crédits.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Saut réactif\",\n            \"description\": \"\\\\{review}Permet à Lara de sauter à tout moment pendant qu'elle court.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Arrêt de nage réactif\",\n            \"description\": \"Permet à Lara de s'arrêter de manière plus réactive sous l'eau lorsque la touche de nage est relâchée.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Nage fluide\",\n            \"description\": \"\\\\{review}Applique une accélération progressive aux rotations de Lara sous l'eau pour un mouvement plus naturel, comme c'est fait depuis TR2. Désactiver cette option donnera à Lara une vitesse de rotation plus rapide, comme dans TR1 d'origine.\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Roulade sous-marine\",\n            \"description\": \"\\\\{review}Permet à Lara de faire une roulade pour se retourner sous l'eau.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Patauger\",\n            \"description\": \"\\\\{review}Permet à Lara de patauger dans les eaux peu profondes, plutôt que de rester bloquée à la surface.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Animées les placements\",\n            \"description\": \"Fait marcher Lara vers les objets et interrupteurs à proximité, au lieu de se téléporter vers eux.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Corriger l'IA des aligators\",\n            \"description\": \"Corrige le fait que les alligators n'infligent aucun dégât si Lara reste immobile dans l'eau.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Corriger l'IA des ours\",\n            \"description\": \"Corrige l'attaque par coup de patte des ours pour qu'ils ne ratent pas Lara.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Corriger la colision des ponts\",\n            \"description\": \"Corrige l'impossibilité pour Lara de s'accrocher à des certaines parties de ponts et les murs invisibles à leurs bordures. Corrige également les problèmes de colision pour les ponts-levis, les trappes et les ponts lorsqu'ils sont les uns au-dessus des autres, au-dessus de pentes, ou près du sol.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Corriger les chutes sur sols fragiles\",\n            \"description\": \"Corrige le fait que les pas de côté et la marche arrière sur des sols fragiles font que Lara se téléporte immédiatement au niveau inférieur.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Corriger la priorité du jet des torches\",\n            \"description\": \"Corrige le fait que Lara priorise le jet des torches usagées en plein vol, ce qui peut l'empêcher d'attraper les rebords.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Corriger les bugs 'floor data'\",\n            \"description\": \"Corrige les bugs originaux liés aux informations des secteurs.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Corrige le glitch de torche gratuite\",\n            \"description\": \"Corrige la possibilité de faire apparaître une torche gratuite en appuyant sur la touche Torche pendant que Lara ramasse un objet.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Corriger le glitch de duplication d'objet\",\n            \"description\": \"Corrige le fait de pouvoir dupliquer l'utilisation des clés dans l'inventaire.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"\\\\{review}Bug de ramassage corrigé\",\n            \"description\": \"\\\\{review}Corrige un problème où Lara dérivait parfois dans les murs en ramassant des objets sous l’eau, ainsi qu’en ramassant des objets au-dessus de l’eau sous des plafonds fortement inclinés.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"\\\\{review}Corriger la précision du M16/MP5\",\n            \"description\": \"\\\\{review}Corrige la précision du M16/MP5 pendant que Lara court.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"\\\\{review}Corriger la priorité de ramassage des singes\",\n            \"description\": \"\\\\{review}Les singes attaqués privilégieront la riposte plutôt que de collecter les trousses de soins et les clés.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"\\\\{review}Corriger la visée du pipeman\",\n            \"description\": \"\\\\{review}Corrige le problème où le pipeman ne parvient parfois pas à viser correctement Lara avec ses fléchettes.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Corriger le glitch QWOP\",\n            \"description\": \"Corrige le fait que sauter sur des petites marches entraîne parfois Lara à se bloquer dans une animation de course étrange, connu sous le nom du glitch QWOP.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Corriger le glitch des petites marches\",\n            \"description\": \"Corrige le fait que Lara est parfois aspirée dans des murs adjacents à des petites marches lorsqu'elle monte dessus d'une manière spécifique.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"\\\\{review}Corriger le coup de mur en marchant\",\n            \"description\": \"\\\\{review}Corrige le fait que Lara ne réagisse pas lorsqu'elle heurte un mur en marchant.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Corriger le saut après marche\",\n            \"description\": \"Corrige le fait que Lara ne peut parfois pas sauter tout de suite après être passée de son animation de marche à celle de course.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"\\\\{review}Corriger la géométrie des murs\",\n            \"description\": \"\\\\{review}Corrige les cas dans la géométrie des niveaux OG où des inclinaisons à l'intérieur des murs peuvent entraîner des calculs de hauteur inexacts.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"\\\\{review}Corriger la sortie de l'eau\",\n            \"description\": \"\\\\{review}Corrige le fait que Lara puisse passer directement d'une pièce d'eau à une pièce sèche adjacente, ou à une pièce sèche en dessous. De plus, cela empêchera Lara de pouvoir sortir de l'eau sur des pentes non praticables.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"\\\\{review}Recul du harpon\",\n            \"description\": \"\\\\{review}Définit la fréquence à laquelle Lara doit recharger le harpon, en fonction de son nombre de munitions actuel. Par exemple, si réglé sur 3, elle devra recharger après chaque troisième tir. Réglez sur 0 pour désactiver complètement le rechargement.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"\\\\{review}Délai inactivité\",\n            \"description\": \"\\\\{review}Permet à Lara d'entrer dans une animation de pose après le nombre de secondes d'inactivité défini. Réglez sur 0 pour désactiver.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"\\\\{review}Verrou saut\",\n            \"description\": \"\\\\{review}Pour un saut réactif, permet de contrôler à quel moment après le début de la course Lara est autorisée à sauter.\\n\\n- Hérité : correspond au timing original de TR2.\\n- Ajusté : le saut est possible 2 images plus tôt que dans TR2 d'origine.\\n- Désactivé : le saut est possible immédiatement après l'animation de départ en course.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Activer les écrans de chargement\",\n            \"description\": \"\\\\{review}Contrôle les écrans de chargement avant le chargement des niveaux.\\n\\n- Désactivé : n'affiche jamais les écrans de chargement.\\n- Toujours : affiche les écrans de chargement.\\n- Nouvelles parties : n'affiche pas les écrans de chargement lors du chargement d'une sauvegarde.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"\\\\{review}Mode regard\",\n            \"description\": \"\\\\{review}Contrôle quand Lara peut utiliser la vue.\\n\\n- Restreint : le regard est autorisé uniquement à l’arrêt, jamais sous l’eau.\\n- Amélioré : le regard est autorisé pendant la plupart des animations, sauf certaines comme pousser un bloc.\\n- Illimité : le regard est autorisé à tout moment lorsque Lara est contrôlable.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"\\\\{review}Nombre d'emplacements de sauvegarde rapide\",\n            \"description\": \"\\\\{review}Modifie le nombre d'emplacements de sauvegarde rapide disponibles.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Nombre d'emplacements de sauvegarde\",\n            \"description\": \"Change le nombre d'emplacements de sauvegarde disponibles.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"\\\\{review}Pause lorsque la fenêtre perd le focus\",\n            \"description\": \"\\\\{review}Interrompt la progression du jeu lorsque la fenêtre du jeu perd le focus.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"\\\\{review}Dégâts de zone de projectile\",\n            \"description\": \"\\\\{review}Contrôle la propagation de la zone d'effet pour le Lance-roquettes et le Lance-grenades.\\n\\n- Balayage unique : comportement de TR1 & TR2.\\n- Balayage multiple : comportement de TR3.\\n\\nL'option de balayage multiple entraîne souvent des dégâts doublés sur les ennemis individuels.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Mémoriser les armes entre les niveaux\",\n            \"description\": \"\\\\{review}Mémoriser la dernière arme utilisée par Lara dans le niveau précédent au démarrage du suivant. Sans cette option, Lara reviendra aux pistolets rangés dans leurs holsters.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"Restaure les ennemis PS1\",\n            \"description\": \"\\\\{review}Ajoute la momie qui apparaît dans la version PlayStation de City of Khamoon, salle 25.\\nModifier cette option nécessitera de redémarrer le jeu.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Points de vie de Lara\",\n            \"description\": \"Définit les points de vie de Lara à chaque lancement de niveau.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Mode de verrouillage\",\n            \"description\": \"Modifie le comportement de verrouillage des cibles.\\n- Complet : Verrouille la cible même si elle sort du champ de vision ou meurt (TR1-3).\\n- Hybride : Verrouille la cible si elle sort du champ de vision, mais déverrouille si elle meurt.\\n- Aucun : Déverrouille la cible si elle sort du champ de vision ou meurt (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"\\\\{review}Mode de glitch de mur\",\n            \"description\": \"\\\\{review}Permet d'utiliser le comportement de glitch de mur de TR1 dans TR2 et vice-versa ; permet également de corriger tous les types de glitch de mur.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"\\\\{review}Mise en mémoire tampon (touches F)\",\n            \"description\": \"\\\\{review}Active la mise en mémoire tampon de la touche F (1 image) pour obtenir un contrôle précis des mouvements de Lara. Cette fonction existe à l'origine uniquement dans le port TombATI (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"\\\\{review}Mise en mémoire tampon (inventaire)\",\n            \"description\": \"\\\\{review}Active la mise en mémoire tampon de l'inventaire (2 images) pour un contrôle précis des mouvements de Lara.\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Passeport plus réactif\",\n            \"description\": \"Evite de bloquer complètement les contrôles pendant que les pages tournent : à la place, repousse simplement leur effet.\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Pas de côtés améliorés\",\n            \"description\": \"Permet d'effectuer des pas de côtés comme à partir de TR3 (exemple : Maj + touches directionelles). Les touches dédiées restent fonctionnelles.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"\\\\{review}Touches de l'arme rapide\",\n            \"description\": \"\\\\{review}Contrôle le comportement des touches d'équipement de l'arme rapide.\\n\\n- Tirer seulement : appuyer sur une touche fera en sorte que Lara équipe l'arme assignée.\\n- Tirer ou ranger : identique à ce qui précède, de plus Lara rangera l'arme assignée si elle la porte actuellement.\",\n        },\n        \"language\": {\n            \"title\": \"Langue\",\n            \"description\": \"Change la langue du texte de l'interface utilisateur.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Filtre anisotropique\",\n            \"description\": \"Améliore le filtrage des textures au loin.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Mode d'aspect\",\n            \"description\": \"Force un rapport d'aspect du jeu avec des bandes noires.\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"\\\\{review}Bordures\",\n            \"description\": \"\\\\{review}Ajoute des bordures noires autour de la fenêtre du jeu.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Filtre trapézoïdal\",\n            \"description\": \"Corrige le rendu des quadrilatères.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"VSync\",\n            \"description\": \"Active ou désactive la synchronistaion verticale.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"FPS\",\n            \"description\": \"Définit le nombre d'images par seconde du jeu.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Éclairage contrasté\",\n            \"description\": \"Augmente le contraste pour les sources de lumière dynamiques telles que les torches et les flashs des armes.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Format des captures d'écran\",\n            \"description\": \"Format de fichier pour les captures d'écran.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"\\\\{review}Mode verrouillage des sprites\",\n            \"description\": \"\\\\{review}Contrôle les axes à verrouiller lors de l'affichage des sprites à l'écran.\\n\\n- Aucun : afficher les sprites normalement.\\n- Roulis : verrouiller l'axe de roulis – utile uniquement en mode photo.\\n- Roulis et tangage : garantir que les sprites restent debout et ne reposent pas au sol lorsqu'on les regarde d'en haut.\\n- Perspective : verrouiller les axes de roulis et de tangage et, en plus, faire pivoter légèrement les sprites vers le centre de l'écran.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Filtre des texture\",\n            \"description\": \"\\\\{review}Alterne entre des textures ingame lisses et pixelisées.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"\\\\{review}Filtre UI\",\n            \"description\": \"\\\\{review}Alterne entre des textures UI lisses et pixelisées.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"\\\\{review}Facteur de mise à l'échelle\",\n            \"description\": \"Augmente la taille du jeu selon un multiplicateur, en maintenant l'aspect pixellisé.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"\\\\{review}Filtre de mise à l'échelle\",\n            \"description\": \"Applique une apparence lisse ou pixelisée pour tout l'écran.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Couleur de la barre d'air\",\n            \"description\": \"Couleur de la barre d'air.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Couleur de la barre d'air\",\n            \"description\": \"Couleur de la barre d'air.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Emplacement de la barre d'air\",\n            \"description\": \"Emplacement de la barre d'air.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"\\\\{review}Emplacement du compteur de munitions\",\n            \"description\": \"\\\\{review}Emplacement où le compteur de munitions est affiché.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"\\\\{review}Apparence des barres\",\n            \"description\": \"\\\\{review}Contrôle l'apparence visuelle des barres de l'interface utilisateur.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Échelle des barres\",\n            \"description\": \"Change la taille des barres de vie, d'air et des ennemis.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"\\\\{review}Barres clignotantes\",\n            \"description\": \"\\\\{review}Fait clignoter les barres de santé et d'oxygène de Lara lorsqu'elle est à court de l'une ou l'autre ressource.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Barres fluides\",\n            \"description\": \"Permet à la barre de santé et à la barre d'air d'utiliser des transitions de couleurs fluides.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Boucler au défilement\",\n            \"description\": \"Automatiquement retourner en haut après avoir atteint le bas d'un menu (et vice-versa).\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Couleur de la barre des ennemis\",\n            \"description\": \"Couleur de la barre de santé des ennemis.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"\\\\{review}Couleur de la barre des alliés\",\n            \"description\": \"\\\\{review}Couleur de la barre de vie des alliés. Affichée à l'emplacement des barres de vie des ennemis.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"\\\\{review}Couleur de la barre des alliés\",\n            \"description\": \"\\\\{review}Couleur de la barre de vie des alliés. Affichée à l'emplacement des barres de vie des ennemis.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Couleur de la barre des ennemis\",\n            \"description\": \"Couleur de la barre de santé des ennemis.\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Emplacement de la barre des ennemis\",\n            \"description\": \"Emplacement de la barre de santé des ennemis.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Comportement de la barre des ennemis\",\n            \"description\": \"Affiche une barre de santé pour l'ennemi ciblé.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"\\\\{review}Couleur de la barre d'exposition\",\n            \"description\": \"\\\\{review}Couleur de la barre d'exposition à l'eau froide.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"\\\\{review}Couleur de la barre d'exposition\",\n            \"description\": \"\\\\{review}Couleur de la barre d'exposition à l'eau froide.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"\\\\{review}Emplacement de la barre d'exposition\",\n            \"description\": \"\\\\{review}Emplacement où la barre d'exposition à l'eau froide est affichée.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Couleur de la barre de santé\",\n            \"description\": \"Couleur de la barre de santé.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Couleur de la barre de santé\",\n            \"description\": \"Couleur de la barre de santé.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Emplacement barre de santé\",\n            \"description\": \"Emplacement de la barre de santé\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"\\\\{review}Couleur de la barre de santé empoisonnée\",\n            \"description\": \"\\\\{review}Couleur de la barre de santé lorsque Lara est empoisonnée.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"\\\\{review}Couleur de la barre de santé empoisonnée\",\n            \"description\": \"\\\\{review}Couleur de la barre de santé lorsque Lara est empoisonnée.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"\\\\{review}Arrière-plan de l'inventaire\",\n            \"description\": \"\\\\{review}Modifie la façon dont l'arrière-plan de l'anneau d'inventaire est affiché.\\n\\n- Sombre : TR1 (PC).\\n- Très sombre : TR1 (PS1).\\n- Statique : TR2 (PC).\\n- Ondulé : TR2 (PS1).\\n- Monochrome : TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"\\\\{review}Effets de fondu de l'inventaire\",\n            \"description\": \"\\\\{review}Affine les effets de fondu à activer ou désactiver dans l'anneau d'inventaire en jeu. Nécessite que l'option Effets de fondu soit activée pour fonctionner.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Style des menus\",\n            \"description\": \"Changer la manière dont les menus sont affichés.\\n - PC : Interface de la version PC.\\n - PS1 : Interface de la version PlayStation.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"\\\\{review}Pause en arrière-plan\",\n            \"description\": \"\\\\{review}Modifie la façon dont l'arrière-plan de l'écran de pause est affiché.\\n\\n- Sombre : TR1 (PC).\\n- Très sombre : TR1 (PS1).\\n- Statique : TR2 (PC).\\n- Ondulé : TR2 (PS1).\\n- Monochrome : TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"\\\\{review}Effets de fondu de la pause\",\n            \"description\": \"\\\\{review}Affine les effets de fondu à activer ou désactiver dans l'écran de pause. Nécessite que l'option Effets de fondu soit activée pour fonctionner.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"\\\\{review}Échelle de ramassage\",\n            \"description\": \"\\\\{review}Modifie la taille des objets animés dans l'interface utilisateur lorsque Lara ramasse quelque chose.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"\\\\{review}Afficher les barres\",\n            \"description\": \"\\\\{review}Désactive toutes les barres en jeu, masquant les informations sur la santé de Lara et d'autres ressources (pour les défis).\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Affichage des objets ramassés\",\n            \"description\": \"Affiche les objets en bas à droite lorsque Lara ramasse quelque chose.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"\\\\{review}Texte de la version du titre\",\n            \"description\": \"\\\\{review}Affiche la chaîne de version TRX dans l'anneau d'inventaire du titre.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"\\\\{review}Couleur de la barre de sprint\",\n            \"description\": \"\\\\{review}Couleur de la barre de sprint.\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"\\\\{review}Couleur de la barre de sprint\",\n            \"description\": \"\\\\{review}Couleur de la barre de sprint.\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"\\\\{review}Emplacement de la barre de sprint\",\n            \"description\": \"\\\\{review}Emplacement où la barre de sprint est affichée.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"\\\\{review}Munitions touchées/utilisées\",\n            \"description\": \"\\\\{review}Affiche la ligne des munitions dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"\\\\{review}Cristaux\",\n            \"description\": \"\\\\{review}Affiche la rangée des cristaux dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"\\\\{review}Décès\",\n            \"description\": \"\\\\{review}Affiche les décès de Lara dans les statistiques de la boussole et dans les statistiques du niveau. Le nombre de décès est mis à jour dans la sauvegarde actuellement chargée dès que Lara meurt.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"\\\\{review}Distance parcourue\",\n            \"description\": \"\\\\{review}Affiche la ligne de la distance parcourue dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"\\\\{review}Éliminations\",\n            \"description\": \"\\\\{review}Affiche la ligne des éliminations dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"\\\\{review}Compteur de niveau\",\n            \"description\": \"\\\\{review}Affiche le numéro du niveau actuel en haut des statistiques du niveau.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"\\\\{review}Trousse de soins utilisées\",\n            \"description\": \"\\\\{review}Affiche la ligne des trousses de soins utilisées dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"\\\\{review}Objets ramassés\",\n            \"description\": \"\\\\{review}Affiche la ligne des objets ramassés dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"\\\\{review}Secrets trouvés\",\n            \"description\": \"\\\\{review}Affiche la ligne des secrets trouvés dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"\\\\{review}Temps écoulé\",\n            \"description\": \"\\\\{review}Affiche la ligne du temps écoulé dans les statistiques du niveau.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"\\\\{review}Afficher les totaux\",\n            \"description\": \"\\\\{review}Affiche les totaux à côté des statistiques lorsque cela est applicable. Les secrets ne sont pas affectés par ce paramètre.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"\\\\{review}Style des statistiques\",\n            \"description\": \"\\\\{review}Contrôle la façon dont la fenêtre des statistiques est affichée.\\n\\n- Simple : affiche la mise en page plus simple sans cadre.\\n- Encadré : affiche la mise en page encadrée.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"\\\\{review}Arrière-plan des statistiques\",\n            \"description\": \"\\\\{review}Modifie la façon dont l'arrière-plan des statistiques de fin de niveau est affiché.\\n\\n- Sombre : TR1 (PC).\\n- Très sombre : TR1 (PS1).\\n- Statique : TR2 (PC).\\n- Ondulé : TR2 (PS1).\\n- Monochrome : TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"\\\\{review}Effets de fondu des statistiques\",\n            \"description\": \"\\\\{review}Affine les effets de fondu à activer ou désactiver dans l'écran des statistiques de fin de niveau. Nécessite que l'option Effets de fondu soit activée pour fonctionner.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Échelle de texte\",\n            \"description\": \"Change la taille du texte de l'interface utilisateur.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"\\\\{review}Effets de sang\",\n            \"description\": \"\\\\{review}Contrôle les couleurs des étincelles de sang.\\n\\n- Désactivé : aucune étincelle de sang n'est affichée.\\n- Rose : la valeur par défaut dans les versions PC allemandes de TR3.\\n- Rouge : la valeur par défaut dans toutes les autres versions commerciales.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Mode caméra\",\n            \"description\": \"Ajuste le comportement de la caméra lors d'actions comme l'utilisation de clés.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"Objets à ramasser 3D\",\n            \"description\": \"Affiche les objets à ramasser en 3D au lieu de les afficher en sprites.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Tresse de Lara\",\n            \"description\": \"Active la tresse de Lara.\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Vent\",\n            \"description\": \"Active l'effet de vent sur la tresse de Lara dans les salles appropriées.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Fondu de sortie\",\n            \"description\": \"Active les effets de fondu lors de la sortie du jeu vers le bureau.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Effets de fondu\",\n            \"description\": \"Active les transitions en fondu, par exemple entre les images de crédit ou pour les transitions de l'inventaire et de l'écran de pause.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"\\\\{review}Allumage de feu\",\n            \"description\": \"\\\\{review}Active l'éclairage dynamique généré à proximité des flammes actives.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"\\\\{review}Empreintes de pas\",\n            \"description\": \"\\\\{review}Permet l'affichage des empreintes de Lara sur certaines surfaces dans les niveaux pris en charge.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"\\\\{review}Caméras en glissement\",\n            \"description\": \"\\\\{review}Active un effet de glissement sur les caméras fixes qui regardent Lara en adoptant une courbe de vitesse fluide. Si désactivé, ces caméras changeront immédiatement la vue pour regarder Lara.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Éclairage des armes\",\n            \"description\": \"\\\\{review}Permet de générer un éclairage dynamique pour les coups de feu et les explosions.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"Teinte des cristaux PS1\",\n            \"description\": \"Les cristaux de sauvegarde seront colorés avec une teinte violette, plus similaires à ceux de la PS1.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Reflets\",\n            \"description\": \"Active les reflets sur certains objets.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"\\\\{review}Teinte réactive des meshes\",\n            \"description\": \"\\\\{review}Permet d’afficher les meshes individuelles de Lara avec une teinte aquatique lorsqu’elles se trouvent elles-mêmes sous l’eau (style TR3). Sinon, si Lara est dans l’eau, toutes ses meshes seront affichées avec la teinte (style TR1/2).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Flash du fusil\",\n            \"description\": \"Génère un flash lorsque Lara tire avec le fusil à pompe, comme pour les autres armes.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Ciel\",\n            \"description\": \"Active le ciel dans les niveaux pris en charge.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"\\\\{review}Météo\",\n            \"description\": \"\\\\{review}Active le rendu des effets météorologiques dans les niveaux pris en charge.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Corriger les sprites animées\",\n            \"description\": \"\\\\{review}Corrige les sprites originaux des plantes sous-marines pour qu'ils s'animent correctement dans les zones aquatiques.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Corriger l'orientation des objets à ramasser\",\n            \"description\": \"Corrige les problèmes d'orientation de certains objets, lorsque l'option des objets à ramasser en 3D est utilisée.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Corriger les problèmes de texture\",\n            \"description\": \"Corrige des problèmes de textures/morceaux d'objets manquants ou incorrects.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"\\\\{review}Couleur du brouillard\",\n            \"description\": \"\\\\{review}Couleur du brouillard.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Fin de brouillard\",\n            \"description\": \"Définit la distance en secteurs où le brouillard rend tout complètement obscurci.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Début de brouillard\",\n            \"description\": \"Définit la distance en secteurs où le brouillard commence à apparaître.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"\\\\{review}Transparence du brouillard\",\n            \"description\": \"\\\\{review}Permet d'activer le mélange de la géométrie lointaine avec des faces 100% transparentes.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"\\\\{review}Champ de vision\",\n            \"description\": \"\\\\{review}Angle de vue en degrés. Des valeurs plus grandes élargissent le champ de vision, des valeurs plus petites le rétrécissent.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Luminosité\",\n            \"description\": \"Modifie la luminosité du jeu.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"\\\\{review}Gamma\",\n            \"description\": \"\\\\{review}Ajuste la courbe gamma. Des valeurs plus élevées signifient un éclairage plus lumineux. La valeur de 2,5 correspond aux couleurs par défaut.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"\\\\{review}Tenue de Lara\",\n            \"description\": \"\\\\{review}Modifie l’apparence de Lara. Le choix Par défaut respecte les changements de tenue normaux entre les niveaux ; sinon, la tenue sélectionnée restera active jusqu’à modification manuelle.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"\\\\{review}Forme des ombres\",\n            \"description\": \"\\\\{review}Sélectionne la manière dont les ombres des entités sont rendues.\\n\\n- Octogone : ombres des anciens TR1 et TR2\\n- Cercle : ombres rondes\\n- Sprite : ombres basées sur des textures TR3\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"\\\\{review}Lunettes de soleil de Lara\",\n            \"description\": \"\\\\{review}Modifie le style des lunettes de soleil de Lara. Remarque : les verres seront réfléchissants si l’option correspondante est activée.\\n\\n- Désactivé : Lara ne portera pas de lunettes de soleil.\\n- Opaque : Les lunettes de soleil de Lara auront des verres opaques.\\n- Transparent : Les lunettes de soleil de Lara auront des verres semi-transparents.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"Luminosité de l'interface\",\n            \"description\": \"Modifie la luminosité de l'interface.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Couleur de l'eau\",\n            \"description\": \"\\\\{review}Couleur de l'eau.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"\\\\{review}Alarme\",\n        },\n        \"alligator\": {\n            \"name\": \"Alligator\",\n        },\n        \"alphabet\": {\n            \"name\": \"\\\\{review}Police par défaut\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"\\\\{review}Petite police\",\n        },\n        \"amber_light\": {\n            \"name\": \"\\\\{review}Lumière Ambre\",\n        },\n        \"animating_1\": {\n            \"name\": \"\\\\{review}Animation de l'objet 1\",\n        },\n        \"animating_10\": {\n            \"name\": \"\\\\{review}Animation de l'objet 10\",\n        },\n        \"animating_2\": {\n            \"name\": \"\\\\{review}Animation de l'objet 2\",\n        },\n        \"animating_3\": {\n            \"name\": \"\\\\{review}Animation de l'objet 3\",\n        },\n        \"animating_4\": {\n            \"name\": \"\\\\{review}Animation de l'objet 4\",\n        },\n        \"animating_5\": {\n            \"name\": \"\\\\{review}Animation de l'objet 5\",\n        },\n        \"animating_6\": {\n            \"name\": \"\\\\{review}Animation de l'objet 6\",\n        },\n        \"animating_7\": {\n            \"name\": \"\\\\{review}Animation de l'objet 7\",\n        },\n        \"animating_8\": {\n            \"name\": \"\\\\{review}Animation de l'objet 8\",\n        },\n        \"animating_9\": {\n            \"name\": \"\\\\{review}Animation de l'objet 9\",\n        },\n        \"ape\": {\n            \"name\": \"Singe\",\n        },\n        \"area_51_rocket\": {\n            \"name\": \"\\\\{review}Fusée Zone 51\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"\\\\{review}Explosion de la Fusée Zone 51\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"\\\\{review}Support de la Fusée Zone 51\",\n        },\n        \"assault_digits\": {\n            \"name\": \"\\\\{review}Chiffres d'assaut\",\n        },\n        \"assault_target\": {\n            \"name\": \"\\\\{review}Cible d'assaut\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"\\\\{review}Atlante Terrestre\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"\\\\{review}Atlante (Tirant)\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"\\\\{review}Atlante Ailé\",\n        },\n        \"autos\": {\n            \"name\": \"\\\\{review}Pistolets automatiques\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"\\\\{review}Chargeurs de pistolet automatique\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Bacon-Lara\",\n        },\n        \"baldy\": {\n            \"name\": \"Le Grand Chauve\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"\\\\{review}Mercenaire 1\",\n                \"\\\\{review}Goon masqué 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"\\\\{review}Mercenaire 2\",\n                \"\\\\{review}Goon masqué 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"\\\\{review}Mercenaire 3\",\n                \"\\\\{review}Goon masqué 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"\\\\{review}Barracuda\",\n        },\n        \"bartoli\": {\n            \"name\": \"\\\\{review}Marco Bartoli\",\n        },\n        \"bat\": {\n            \"name\": \"Chauve-souris\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de chauve-souris\",\n        },\n        \"beacon_light\": {\n            \"name\": \"\\\\{review}Lumière Balise\",\n        },\n        \"bear\": {\n            \"name\": \"Ours\",\n        },\n        \"bell\": {\n            \"name\": \"\\\\{review}Cloche\",\n        },\n        \"big_bowl\": {\n            \"name\": \"\\\\{review}Bol de Lave\",\n        },\n        \"big_eel\": {\n            \"name\": \"\\\\{review}Grande anguille\",\n        },\n        \"big_pod\": {\n            \"name\": \"Gros gousse\",\n        },\n        \"big_spider\": {\n            \"name\": \"\\\\{review}Géante araignée\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"\\\\{review}Monstre oiseau\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"\\\\{review}Eau qui goutte\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"\\\\{review}Oiseaux chantants\",\n        },\n        \"blade\": {\n            \"name\": \"\\\\{review}Lame Murale\",\n        },\n        \"blood\": {\n            \"name\": \"\\\\{review}Sang\",\n        },\n        \"blood_pink\": {\n            \"name\": \"\\\\{review}Sang (censuré)\",\n        },\n        \"blue_light\": {\n            \"name\": \"\\\\{review}Lumière Bleue\",\n        },\n        \"boat\": {\n            \"name\": \"\\\\{review}Bateau\",\n        },\n        \"boat_bits\": {\n            \"name\": \"\\\\{review}Morceaux de bateau\",\n        },\n        \"body_part\": {\n            \"name\": \"\\\\{review}Membre\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"\\\\{review}Pont plat\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"\\\\{review}Pont incliné 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"\\\\{review}Pont incliné 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"\\\\{review}Bulle 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Bulle 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de bulles\",\n        },\n        \"camera_target\": {\n            \"name\": \"\\\\{review}Cible de caméra\",\n        },\n        \"carcass\": {\n            \"name\": \"\\\\{review}Carcasse\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"\\\\{review}Plafond Épineux\",\n        },\n        \"centaur\": {\n            \"name\": \"Centaure\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Statue\",\n        },\n        \"civilian\": {\n            \"name\": \"\\\\{review}Civil\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"\\\\{review}Mutant Griffu\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"\\\\{review}Horloge de la cachette de Bartoli\",\n        },\n        \"cog_1\": {\n            \"name\": \"Roue dentée 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Roue dentée 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Roue dentée 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"\\\\{review}Fin de combat\",\n        },\n        \"compass\": {\n            \"name\": \"\\\\{review}Statistiques\",\n        },\n        \"compy\": {\n            \"name\": \"\\\\{review}Compsognathus\",\n        },\n        \"controls\": {\n            \"name\": \"\\\\{review}Contrôles\",\n        },\n        \"copter\": {\n            \"name\": \"\\\\{review}Hélicoptère\",\n        },\n        \"cowboy\": {\n            \"name\": \"Le Cowboy\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"\\\\{review}Mutant Rampant\",\n        },\n        \"crocodile\": {\n            \"name\": \"Crocodile\",\n        },\n        \"crow\": {\n            \"name\": \"\\\\{review}Corbeau\",\n        },\n        \"cult_1\": {\n            \"name\": \"\\\\{review}Goon masqué 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"\\\\{review}Goon masqué 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"\\\\{review}Goon masqué 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"\\\\{review}Lanceur de couteaux\",\n        },\n        \"cult_3\": {\n            \"name\": \"\\\\{review}Goon au fusil\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"\\\\{review}Animation de douche de fusil à pompe\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Épée de Damoclès\",\n        },\n        \"dart\": {\n            \"name\": \"Flèche\",\n        },\n        \"dart_effect\": {\n            \"name\": \"\\\\{review}Effet de fléchette\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Émetteur de flèches\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"\\\\{review}Desert Eagle\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"\\\\{review}Chargeurs du Desert Eagle\",\n        },\n        \"detonator_box\": {\n            \"name\": \"\\\\{review}Boîte de détonateur\",\n        },\n        \"ding_dong\": {\n            \"name\": \"\\\\{review}Sonnette\",\n        },\n        \"dino_mutant\": {\n            \"name\": \"Dino-mutant\",\n        },\n        \"disc\": {\n            \"name\": \"\\\\{review}Disque\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de Disque\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"\\\\{review}Objet Animé Jetable 9\",\n        },\n        \"diver\": {\n            \"name\": \"\\\\{review}Plongeur\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"\\\\{review}Chien\",\n                \"\\\\{review}Doberman\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Porte 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Porte 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Porte 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Porte 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Porte 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Porte 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Porte 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Porte 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"\\\\{review}Dragon arrière\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"\\\\{review}Espace réservé\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"\\\\{review}Os de dragon avant\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"\\\\{review}Os de dragon arrière\",\n        },\n        \"dragon_front\": {\n            \"name\": \"\\\\{review}Dragon avant\",\n        },\n        \"drawbridge\": {\n            \"name\": \"\\\\{review}Pont-levis\",\n        },\n        \"dust\": {\n            \"name\": \"Poussière\",\n        },\n        \"dying_monk\": {\n            \"name\": \"\\\\{review}Moine mourant\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"\\\\{review}Mutant Mourant\",\n        },\n        \"eagle\": {\n            \"name\": \"\\\\{review}Aigle\",\n        },\n        \"earthquake\": {\n            \"name\": \"\\\\{review}Tremblement de terre\",\n        },\n        \"eel\": {\n            \"name\": \"\\\\{review}Anguille\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"\\\\{review}Nettoyeur électrique\",\n        },\n        \"electric_fence\": {\n            \"name\": \"\\\\{review}Clôture électrique\",\n        },\n        \"electrical_light\": {\n            \"name\": \"\\\\{review}Lumière électrique\",\n        },\n        \"ember\": {\n            \"name\": \"\\\\{review}Braise\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de braise\",\n        },\n        \"explosion_1\": {\n            \"name\": \"\\\\{review}Explosion 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Explosion 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": [\n                \"\\\\{review}Bloc tombant 1\",\n                \"\\\\{review}Sol rétractable 1\",\n                \"\\\\{review}Tuiles rétractables 1\",\n            ]\n        },\n        \"falling_block_2\": {\n            \"name\": [\n                \"\\\\{review}Bloc tombant 2\",\n                \"\\\\{review}Sol rétractable 2\",\n                \"\\\\{review}Tuiles rétractables 2\",\n            ]\n        },\n        \"falling_block_3\": {\n            \"name\": [\n                \"\\\\{review}Bloc tombant 3\",\n                \"\\\\{review}Sol rétractable 3\",\n                \"\\\\{review}Tuiles rétractables 3\",\n                \"\\\\{review}Planches lâches\",\n            ]\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"\\\\{review}Plafond Tombant 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Plafond tombant 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"\\\\{review}Tête de Feu\",\n        },\n        \"fish_mutant\": {\n            \"name\": \"Poisson-mutant\",\n        },\n        \"flame\": {\n            \"name\": [\n                \"\\\\{review}Flamme\",\n                \"\\\\{review}Feu\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"\\\\{review}Émetteur de flamme\",\n                \"\\\\{review}Émetteur de feu\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": \"\\\\{review}Émetteur de flammes (Grand)\",\n        },\n        \"flame_emitter_jet\": {\n            \"name\": \"\\\\{review}Émetteur de flammes (Jet)\",\n        },\n        \"flame_emitter_side\": {\n            \"name\": \"\\\\{review}Émetteur de flammes (Latéral)\",\n        },\n        \"flame_emitter_small\": {\n            \"name\": \"\\\\{review}Émetteur de flammes (Petit)\",\n        },\n        \"flare\": {\n            \"name\": \"\\\\{review}Fusée éclairante\",\n        },\n        \"flare_fire\": {\n            \"name\": \"\\\\{review}Étincelles de fusée éclairante\",\n        },\n        \"flares_box\": {\n            \"name\": \"\\\\{review}Boîte de fusées éclairantes\",\n        },\n        \"flickering_light\": {\n            \"name\": \"\\\\{review}Lumière Vacillante\",\n        },\n        \"fuse_box\": {\n            \"name\": \"\\\\{review}Boîte à fusibles\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"\\\\{review}Disque gris\",\n        },\n        \"gamma\": {\n            \"name\": \"\\\\{review}Gamma\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"\\\\{review}Émetteur de Gaz (Vert)\",\n        },\n        \"general\": {\n            \"name\": \"\\\\{review}Minisous-marin\",\n        },\n        \"globe\": {\n            \"name\": \"\\\\{review}Globe\",\n        },\n        \"glow\": {\n            \"name\": \"\\\\{review}Lueur\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"\\\\{review}Lueur de la carte\",\n        },\n        \"gondola\": {\n            \"name\": \"\\\\{review}Gondole\",\n        },\n        \"gong\": {\n            \"name\": \"\\\\{review}Gong\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"\\\\{review}Bâton de gong\",\n        },\n        \"graphics\": {\n            \"name\": \"\\\\{review}Graphismes\",\n        },\n        \"green_light\": {\n            \"name\": \"\\\\{review}Lumière Verte\",\n        },\n        \"grenade\": {\n            \"name\": \"\\\\{review}Grenade\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"\\\\{review}Lance-grenades\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"\\\\{review}Grenades\",\n        },\n        \"gun_flash\": {\n            \"name\": \"\\\\{review}Éclair de pistolet\",\n        },\n        \"gun_shell\": {\n            \"name\": \"\\\\{review}Cartouche de pistolet\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"\\\\{review}Bolt de harpon\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"\\\\{review}Fusil harpon\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"\\\\{review}Harpons\",\n        },\n        \"hook\": {\n            \"name\": \"\\\\{review}Crochet\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"\\\\{review}Feu supplémentaire\",\n        },\n        \"huskie\": {\n            \"name\": \"\\\\{review}Chien\",\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"\\\\{review}Mutant Hybride\",\n        },\n        \"icicle\": {\n            \"name\": \"\\\\{review}Glaçons\",\n        },\n        \"inv_background\": {\n            \"name\": \"\\\\{review}Fond de menu\",\n        },\n        \"jelly\": {\n            \"name\": \"\\\\{review}Méduse\",\n        },\n        \"kayak\": {\n            \"name\": \"\\\\{review}Kayak\",\n        },\n        \"key_1\": {\n            \"name\": \"\\\\{review}Clé 1\",\n        },\n        \"key_2\": {\n            \"name\": \"\\\\{review}Clé 2\",\n        },\n        \"key_3\": {\n            \"name\": \"\\\\{review}Clé 3\",\n        },\n        \"key_4\": {\n            \"name\": \"\\\\{review}Clé 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"\\\\{review}Trou de clé 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"\\\\{review}Trou de clé 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"\\\\{review}Trou de clé 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"\\\\{review}Trou de clé 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"\\\\{review}Élimination totale déclenchée\",\n        },\n        \"killer_statue\": {\n            \"name\": \"\\\\{review}Statue avec Épée\",\n        },\n        \"lara\": {\n            \"name\": \"\\\\{review}Lara\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"\\\\{review}Cloche d'alarme\",\n        },\n        \"lara_autos\": {\n            \"name\": \"\\\\{review}Animation des pistolets automatiques\",\n        },\n        \"lara_boat\": {\n            \"name\": \"\\\\{review}Animation du bateau\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"\\\\{review}Animation du Desert Eagle\",\n        },\n        \"lara_extra\": {\n            \"name\": \"\\\\{review}Animation supplémentaire de Lara\",\n        },\n        \"lara_flare\": {\n            \"name\": \"\\\\{review}Animation de la torche\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"\\\\{review}Animation du lance-grenades\",\n        },\n        \"lara_hair\": {\n            \"name\": \"\\\\{review}Tresse de Lara\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"\\\\{review}Animation du harpon\",\n        },\n        \"lara_m16\": {\n            \"name\": \"\\\\{review}Animation du M16\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"\\\\{review}Animation des magnums\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"\\\\{review}Animation du MP5\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"\\\\{review}Animation des pistolets\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"\\\\{review}Animation du lance-roquettes\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"\\\\{review}Animation du fusil à pompe\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"\\\\{review}Animation de la motoneige\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"\\\\{review}Animation des uzis\",\n        },\n        \"larson\": {\n            \"name\": \"Larson\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Coulée de lave\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Barre de plomb\",\n        },\n        \"lift\": {\n            \"name\": \"\\\\{review}Ascenseur\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Émetteur de foudre\",\n        },\n        \"lion\": {\n            \"name\": \"Lion\",\n        },\n        \"lioness\": {\n            \"name\": \"Lionne\",\n        },\n        \"lizard\": {\n            \"name\": \"\\\\{review}Lézard\",\n        },\n        \"m16\": {\n            \"name\": \"\\\\{review}M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"\\\\{review}Chargeurs de M16\",\n        },\n        \"m16_flash\": {\n            \"name\": \"\\\\{review}Éclair de M16\",\n        },\n        \"magnums\": {\n            \"name\": \"Magnums\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Chargeurs pour Magnums\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"\\\\{review}Échange de Maille 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"\\\\{review}Échange de Maille 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"\\\\{review}Échange de Maille 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Main de Midas\",\n        },\n        \"mine\": {\n            \"name\": \"\\\\{review}Mine aquatique\",\n        },\n        \"mine_cart\": {\n            \"name\": \"\\\\{review}Wagonnet de Mine\",\n        },\n        \"mini_copter\": {\n            \"name\": \"\\\\{review}Hélicoptère 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"\\\\{review}Missile (bombe atlante)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"\\\\{review}Missile (éclat atlante)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"\\\\{review}Missile (flamme)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"\\\\{review}Missile (harpon)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"\\\\{review}Missile (couteau)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"\\\\{review}Missile (poison)\",\n        },\n        \"monk_1\": {\n            \"name\": \"\\\\{review}Moine 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"\\\\{review}Moine 2\",\n        },\n        \"monkey\": {\n            \"name\": \"\\\\{review}Singe\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"\\\\{review}Mitrailleuse Fixe\",\n        },\n        \"mouse\": {\n            \"name\": \"\\\\{review}Rat\",\n        },\n        \"movable_block_1\": {\n            \"name\": [\n                \"\\\\{review}Bloc Poussé 1\",\n                \"\\\\{review}Bloc Mobile 1\",\n            ]\n        },\n        \"movable_block_2\": {\n            \"name\": [\n                \"\\\\{review}Bloc Poussé 2\",\n                \"\\\\{review}Bloc Mobile 2\",\n            ]\n        },\n        \"movable_block_3\": {\n            \"name\": [\n                \"\\\\{review}Bloc Poussé 3\",\n                \"\\\\{review}Bloc Mobile 3\",\n            ]\n        },\n        \"movable_block_4\": {\n            \"name\": [\n                \"\\\\{review}Bloc Poussé 4\",\n                \"\\\\{review}Bloc Mobile 4\",\n            ]\n        },\n        \"moving_bar\": {\n            \"name\": \"Barre mobile\",\n        },\n        \"mp5\": {\n            \"name\": \"\\\\{review}MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"\\\\{review}Chargeurs de MP5\",\n        },\n        \"mp_1\": {\n            \"name\": \"\\\\{review}MP 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"\\\\{review}MP 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Momie\",\n        },\n        \"natla\": {\n            \"name\": \"Natla\",\n        },\n        \"natla_gun\": {\n            \"name\": \"\\\\{review}Arme de Natla\",\n        },\n        \"on_off_light\": {\n            \"name\": \"\\\\{review}Lumière Marche/Arrêt\",\n        },\n        \"orca\": {\n            \"name\": \"\\\\{review}Orque\",\n        },\n        \"passport\": {\n            \"name\": \"\\\\{review}Jeu\",\n        },\n        \"patrol_dog\": {\n            \"name\": \"\\\\{review}Chien\",\n        },\n        \"pda\": {\n            \"name\": \"\\\\{review}Gameplay\",\n        },\n        \"pendulum_1\": {\n            \"name\": \"\\\\{review}Sac de sable\",\n        },\n        \"pendulum_2\": {\n            \"name\": \"\\\\{review}Boîte Oscillante\",\n        },\n        \"photo\": {\n            \"name\": \"\\\\{review}Maison de Lara\",\n        },\n        \"pickup_1\": {\n            \"name\": \"\\\\{review}Objet à ramasser 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"\\\\{review}Objet à ramasser 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Indice au ramassage\",\n        },\n        \"pierre\": {\n            \"name\": \"Pierre\",\n        },\n        \"pirahnas\": {\n            \"name\": \"\\\\{review}Piranhas\",\n        },\n        \"pistols\": {\n            \"name\": \"\\\\{review}Pistolets\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"\\\\{review}Chargeurs de pistolet\",\n        },\n        \"player_1\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 1\",\n        },\n        \"player_10\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 10\",\n        },\n        \"player_2\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 2\",\n        },\n        \"player_3\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 3\",\n        },\n        \"player_4\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 4\",\n        },\n        \"player_5\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 5\",\n        },\n        \"player_6\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 6\",\n        },\n        \"player_7\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 7\",\n        },\n        \"player_8\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 8\",\n        },\n        \"player_9\": {\n            \"name\": \"\\\\{review}Acteur de cinématique 9\",\n        },\n        \"pods\": {\n            \"name\": \"Gousse\",\n        },\n        \"poison_dart\": {\n            \"name\": \"\\\\{review}Dard empoisonné\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"\\\\{review}Lanceur de dards empoisonnés\",\n        },\n        \"portacabin\": {\n            \"name\": \"Préfabriqué portable\",\n        },\n        \"power_saw\": {\n            \"name\": \"\\\\{review}Scie Électrique\",\n        },\n        \"prisoner\": {\n            \"name\": \"\\\\{review}Prisonnier\",\n        },\n        \"propeller_1\": {\n            \"name\": \"\\\\{review}Hélice d'Avion\",\n        },\n        \"propeller_2\": {\n            \"name\": \"\\\\{review}Hélice Sous-marine\",\n        },\n        \"propeller_3\": {\n            \"name\": \"\\\\{review}Ventilateur\",\n        },\n        \"pulse_light\": {\n            \"name\": \"\\\\{review}Lumière Pulsée\",\n        },\n        \"puma\": {\n            \"name\": \"Puma\",\n        },\n        \"punk_1\": {\n            \"name\": \"\\\\{review}Punk 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"\\\\{review}Punk 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"\\\\{review}Objet de puzzle 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"\\\\{review}Objet de puzzle 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"\\\\{review}Objet de puzzle 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"\\\\{review}Objet de puzzle 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"\\\\{review}Trou de puzzle 1 (Fait)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"\\\\{review}Trou de puzzle 2 (Fait)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"\\\\{review}Trou de puzzle 3 (Fait)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"\\\\{review}Trou de puzzle 4 (Fait)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"\\\\{review}Trou de puzzle 1 (Vide)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"\\\\{review}Trou de puzzle 2 (Vide)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"\\\\{review}Trou de puzzle 3 (Vide)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"\\\\{review}Trou de puzzle 4 (Vide)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"\\\\{review}Quad\",\n        },\n        \"quest_1\": {\n            \"name\": \"\\\\{review}Objet de quête 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"\\\\{review}Objet de quête 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"\\\\{review}Objet de quête 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"\\\\{review}Objet de quête 4\",\n        },\n        \"raptor\": {\n            \"name\": \"Raptor\",\n        },\n        \"raptor_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de Raptor\",\n        },\n        \"rat\": {\n            \"name\": \"Rat\",\n        },\n        \"red_light\": {\n            \"name\": \"\\\\{review}Lumière Rouge\",\n        },\n        \"rib\": {\n            \"name\": \"\\\\{review}RIB\",\n        },\n        \"ricochet\": {\n            \"name\": \"\\\\{review}Ricochet\",\n        },\n        \"rocket\": {\n            \"name\": \"\\\\{review}Roquette\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"\\\\{review}Lance-roquettes\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"\\\\{review}Roquettes\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": [\n                \"\\\\{review}Rocher 1\",\n                \"\\\\{review}Boule Roulante\",\n            ]\n        },\n        \"rolling_ball_2\": {\n            \"name\": [\n                \"\\\\{review}Rocher 2\",\n                \"\\\\{review}Boule Roulante 2\",\n            ]\n        },\n        \"rolling_ball_3\": {\n            \"name\": [\n                \"\\\\{review}Rocher 3\",\n                \"\\\\{review}Boule Roulante 3\",\n            ]\n        },\n        \"rolling_ball_4\": {\n            \"name\": [\n                \"\\\\{review}Rocher 4\",\n                \"\\\\{review}Boule Roulante 4\",\n            ]\n        },\n        \"rotating_laser\": {\n            \"name\": \"\\\\{review}Laser rotatif\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"\\\\{review}RX Ouvrier 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"\\\\{review}RX Ouvrier 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": \"\\\\{review}RX Ouvrier 3\",\n        },\n        \"save_crystal\": {\n            \"name\": \"Cristal de Sauvegarde\",\n        },\n        \"scion\": {\n            \"name\": \"Scion\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Emplacement du Scion\",\n        },\n        \"secret_1\": {\n            \"name\": \"\\\\{review}Secret 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"\\\\{review}Secret 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"\\\\{review}Secret 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"\\\\{review}Agent de Sécurité\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"\\\\{review}Laser de sécurité (Alarme)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"\\\\{review}Laser de sécurité (Mortel)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"\\\\{review}Laser de sécurité (Tueur)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"\\\\{review}Tourelle robotisée\",\n        },\n        \"shadow\": {\n            \"name\": \"\\\\{review}Ombre\",\n        },\n        \"shark\": {\n            \"name\": \"\\\\{review}Requin\",\n        },\n        \"shiva\": {\n            \"name\": \"\\\\{review}Shiva\",\n        },\n        \"shotgun\": {\n            \"name\": \"\\\\{review}Fusil à pompe\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"\\\\{review}Cartouches de fusil à pompe\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"\\\\{review}Cartouche de fusil à pompe\",\n        },\n        \"skate_kid\": {\n            \"name\": \"Le Skateur\",\n        },\n        \"skateboard\": {\n            \"name\": \"Skateboard\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"\\\\{review}Motoneige noire\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"\\\\{review}Conducteur de motoneige noire\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"\\\\{review}Motoneige rouge\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"\\\\{review}Trace de Motoneige\",\n        },\n        \"skybox\": {\n            \"name\": \"\\\\{review}Ciel\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Pilier coulissant\",\n        },\n        \"smashable_1\": {\n            \"name\": \"\\\\{review}Fenêtre Brisable 1\",\n        },\n        \"smashable_2\": {\n            \"name\": \"\\\\{review}Fenêtre Brisable 2\",\n        },\n        \"smashable_3\": {\n            \"name\": \"\\\\{review}Fenêtre Brisable 3\",\n        },\n        \"smashable_4\": {\n            \"name\": \"\\\\{review}Fenêtre Brisable 4\",\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"\\\\{review}Émetteur de fumée (noir)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"\\\\{review}Émetteur de fumée (blanc)\",\n        },\n        \"snake\": {\n            \"name\": \"\\\\{review}Serpent\",\n        },\n        \"snow_sprite\": {\n            \"name\": \"\\\\{review}Sillage de Motoneige\",\n        },\n        \"sophia\": {\n            \"name\": \"\\\\{review}Sophia\",\n        },\n        \"sound\": {\n            \"name\": \"\\\\{review}Son\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"\\\\{review}Explosion de dragon 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"\\\\{review}Explosion de dragon 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"\\\\{review}Explosion de dragon 3\",\n        },\n        \"spider\": {\n            \"name\": \"\\\\{review}Araignée\",\n        },\n        \"spike_wall\": {\n            \"name\": \"\\\\{review}Mur de Pics\",\n        },\n        \"spikes\": {\n            \"name\": \"\\\\{review}Pics\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"\\\\{review}Lame Tournante\",\n        },\n        \"splash_1\": {\n            \"name\": \"\\\\{review}Ondulations de l'eau 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Éclaboussures 2\",\n        },\n        \"springboard\": {\n            \"name\": \"\\\\{review}Tremplin\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de vapeur\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"\\\\{review}Mercenaire du Pacifique Sud\",\n        },\n        \"stopwatch\": {\n            \"name\": \"\\\\{review}Statistiques\",\n        },\n        \"strobe_light\": {\n            \"name\": \"\\\\{review}Lumière Stroboscopique\",\n        },\n        \"swat_1\": {\n            \"name\": \"\\\\{review}SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"\\\\{review}SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"\\\\{review}SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"\\\\{review}Hache Oscillante\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"\\\\{review}Interrupteur de Sas\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"\\\\{review}Bouton\",\n                \"\\\\{review}Bouton poussoir\",\n                \"\\\\{review}Interrupteur\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"\\\\{review}Levier\",\n                \"\\\\{review}Interrupteur\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"\\\\{review}Petit Interrupteur\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"\\\\{review}Levier sous-marin\",\n                \"\\\\{review}Interrupteur sous-marin\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": \"\\\\{review}Interrupteur à Roue\",\n        },\n        \"teeth_trap\": {\n            \"name\": \"\\\\{review}Piège à Dents\",\n        },\n        \"text_box\": {\n            \"name\": \"\\\\{review}Cadre UI\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Poignée du marteau de Thor\",\n        },\n        \"thors_head\": {\n            \"name\": \"Marteau de Thor\",\n        },\n        \"tiger\": {\n            \"name\": \"\\\\{review}Tigre\",\n        },\n        \"tony\": {\n            \"name\": \"\\\\{review}Tony\",\n        },\n        \"torso\": {\n            \"name\": \"Torso\",\n        },\n        \"train\": {\n            \"name\": \"\\\\{review}Train\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Trappe 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Trappe 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Trappe 3\",\n        },\n        \"trex\": {\n            \"name\": \"\\\\{review}T-Rex\",\n        },\n        \"trex_alpha\": {\n            \"name\": \"\\\\{review}T-Rex Alpha\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"\\\\{review}Hacheur de la tribu\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"\\\\{review}Chef de tribu\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"\\\\{review}Utilisateur de sarbacane de la tribu\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"\\\\{review}Poisson tropical\",\n        },\n        \"twinkle\": {\n            \"name\": \"\\\\{review}Paillettes\",\n        },\n        \"upv\": {\n            \"name\": \"\\\\{review}Mini-sous-marin\",\n        },\n        \"uzis\": {\n            \"name\": \"\\\\{review}Uzis\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"\\\\{review}Chargeurs d'Uzi\",\n        },\n        \"vole\": {\n            \"name\": \"Campagnol\",\n        },\n        \"vulture\": {\n            \"name\": \"\\\\{review}Vautour\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"\\\\{review}Mutant Guêpe\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"\\\\{review}Émetteur de Mutant Guêpe\",\n        },\n        \"water_sprite\": {\n            \"name\": \"\\\\{review}Sillage de Bateau\",\n        },\n        \"waterfall\": {\n            \"name\": \"\\\\{review}Brume de cascade\",\n        },\n        \"white_light\": {\n            \"name\": \"\\\\{review}Lumière Blanche\",\n        },\n        \"willard\": {\n            \"name\": \"\\\\{review}Willard\",\n        },\n        \"winston\": {\n            \"name\": \"\\\\{review}Winston\",\n        },\n        \"winston_army\": {\n            \"name\": \"\\\\{review}Winston (armée)\",\n        },\n        \"wolf\": {\n            \"name\": \"Loup\",\n        },\n        \"worker_1\": {\n            \"name\": \"\\\\{review}Goon tireur 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"\\\\{review}Goon tireur 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"\\\\{review}Goon avec bâton 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"\\\\{review}Goon avec bâton 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"\\\\{review}Goon lance-flammes\",\n        },\n        \"xian_knight\": {\n            \"name\": \"\\\\{review}Chevalier Xian\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"\\\\{review}Statue de chevalier Xian\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"\\\\{review}Lancier Xian\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"\\\\{review}Statue de lancier Xian\",\n        },\n        \"yeti\": {\n            \"name\": \"\\\\{review}Yéti\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"\\\\{review}Poignée de Tyrolienne\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"Gàidhlig\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Sgrùd\",\n            \"hide_dialog\": \"Falaich an còmhradh\",\n            \"reset_defaults\": \"Ath-shuidhich\",\n            \"rotate\": \"Tionndaidh\",\n            \"unbind\": \"Falamhaich\",\n            \"use_item\": \"Cleachd\",\n        },\n        \"config_presets\": {\n            \"applied\": \"\\\\{review}Ro-shealladh air a chur an sàs.\",\n            \"confirm_description\": \"\\\\{review}Bidh na suidheachaidhean a leanas air an atharrachadh:\",\n            \"confirm_restart_note\": \"\\\\{review}Nota: dh’fhaodadh gum feum cuid de shuidheachaidhean ath-thòiseachadh a’ gheama airson buaidh fhaighinn.\",\n            \"empty\": \"\\\\{review}Cha deach ro-shealladh sam bith a lorg.\",\n            \"no_changes\": \"\\\\{review}Chan eil atharrachaidhean ri chur an gnìomh.\",\n            \"title_fmt\": \"\\\\{review}Cuir an ro-shealladh %s an sàs?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"Raon 1\",\n            \"area_2\": \"Raon 2\",\n            \"area_3\": \"Raon 3\",\n            \"area_4\": \"Raon 4\",\n            \"area_5\": \"Raon 5\",\n            \"area_6\": \"Raon 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"Cuairt-dànachd\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"GEAMA DEIREANNACH\",\n            \"heading_inventory\": \"CLÀR-SEILBHE\",\n            \"heading_items\": \"NITHEAN\",\n            \"heading_option\": \"ROGHAINN\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Modh Taisbeanaidh\",\n            \"direction_keys_controller\": \"D-pad\",\n            \"direction_keys_keyboard\": \"Saigheadan\",\n            \"empty_slot_fmt\": \"- SLOT FALAMH -\",\n            \"exit\": \"Dùin\",\n            \"hold_fmt\": \"Cùm %s\",\n            \"off\": \"Dheth\",\n            \"on\": \"Air\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Cur-a-steach neo-shoilleir: %s agus %s\",\n            \"ambiguous_input_3\": \"Cur-a-steach neo-shoilleir: %s, %s, ...\",\n            \"bilinear_filter_off\": \"Criathradh teacsdaire: dheth\",\n            \"bilinear_filter_on\": \"Criathradh teacsdaire: air\",\n            \"command_bad_invocation\": \"Iarrtas mì-dhligheach: %s\",\n            \"command_bool\": \"air, dheth\",\n            \"command_decimal\": \"[deicheach]\",\n            \"command_integer\": \"[àireamh slàn]\",\n            \"command_percent\": \"[àireamh slàn]\",\n            \"command_unavailable\": \"Chan eil an àithne seo ri làimh an-dràsta\",\n            \"command_valid_values\": \"Luachan dligheach: %s\",\n            \"complete_level\": \"Ìre deiseil!\",\n            \"config_option_get\": \"Tha %s air a rèiteachadh gu %s\",\n            \"config_option_set\": \"%s air atharrachadh gu %s\",\n            \"config_option_unknown_option\": \"Roghainn neo-aithnichte: %s\",\n            \"current_health_get\": \"Slàinte làithreach Lara: %d\",\n            \"current_health_set\": \"Slàinte Lara air a shuidheachadh gu %d\",\n            \"door_close\": \"Dùin Sesame!\",\n            \"door_open\": \"Fosgail Sesame!\",\n            \"door_open_fail\": \"Chan eil doras sam bith faisg air Lara\",\n            \"flipmap_fail_already_off\": \"Tha am mapa-flip mar-thà DHETH\",\n            \"flipmap_fail_already_on\": \"Tha am mapa-flip mar-thà AIR\",\n            \"flipmap_off\": \"Mapa-flip air a chur DHETH\",\n            \"flipmap_on\": \"Mapa-flip air a chur AIR\",\n            \"fly_mode_off\": \"Modh itealaich air a chur dheth\",\n            \"fly_mode_on\": \"Modh itealaich air a chur air\",\n            \"fps_counter_off\": \"Cuntair FPS dheth\",\n            \"fps_counter_on\": \"Cuntair FPS air\",\n            \"give_item\": \"Chaidh %s air a chur ri clàr-seilbhe Lara\",\n            \"give_item_all_guns\": \"Glasaich is losgaidh – tha Lara armaichte gu fiadhaich!\",\n            \"give_item_all_keys\": \"Ionndrainn! Tha a h-uile iuchair aig Lara a-nis.\",\n            \"give_item_cheat\": \"Tha baga-droma Lara mòran nas truime a-nis!\",\n            \"heal_already_full_hp\": \"Tha slàinte làn aig Lara mu thràth\",\n            \"heal_success\": \"Chaidh slàinte Lara a lìonadh gu lèir\",\n            \"invalid_cutscene\": \"Sealladh-film mì-dhligheach\",\n            \"invalid_demo\": \"Taisbeanadh mì-dhligheach\",\n            \"invalid_item\": \"Nì neo-aithnichte: %s\",\n            \"invalid_level\": \"Ìre mì-dhligheach\",\n            \"invalid_object\": \"Oibseact mì-dhligheach\",\n            \"invalid_room\": \"Seòmar mì-dhligheach: %d. Seòmraichean dligheach: 0 gu %d\",\n            \"invalid_sample\": \"Fuaim mì-dhligheach: %d\",\n            \"kill\": \"Beannachd leat!\",\n            \"kill_all\": \"Puf! Chaidh %d nàimhdean air falbh!\",\n            \"kill_all_fail\": \"Och, chan eil nàimhdean air fhàgail airson marbhadh...\",\n            \"kill_fail\": \"Chan eil nàmhaid sam bith faisg air làimh...\",\n            \"lighting_contrast_fmt\": \"Coimeas Solais: %s\",\n            \"load_game\": \"Geama air a luchdadh bho slot %d\",\n            \"load_game_fail_invalid_slot\": \"Slot mì-dhligheach: %d\",\n            \"load_game_fail_unavailable_slot\": \"Chan eil slot sàbhalaidh %d ri làimh\",\n            \"object_not_found\": \"Cha deach an t-oibseact a lorg\",\n            \"play_cutscene\": \"A' luchdachadh sealladh-film %d\",\n            \"play_demo\": \"A' luchdachadh taisbeanaidh %d\",\n            \"play_level\": \"A' luchdachadh %s\",\n            \"pos_lara_missing\": \"Chan eil Lara an làthair\",\n            \"pos_lara_pos_fmt\": \"Seomair: %d\\nSuidheachadh: %.3f, %.3f, %.3f\\nCuairteachadh: %.3f, %.3f, %.3f\",\n            \"pos_level_fmt\": \"Ìre %d\",\n            \"pos_level_fmt_cutscene\": \"Sealladh-film %d\",\n            \"pos_level_fmt_demo\": \"Taisbeanadh %d\",\n            \"quick_load\": \"Slot luchdaichte gu luath %d\",\n            \"quick_load_fail_no_bound_slot\": \"Chan eil slot sàbhalaidh sam bith ceangailte an-dràsta\",\n            \"quick_load_fail_unavailable_bound_slot\": \"Chan eil an slot sàbhalaidh ceangailte rim faotainn\",\n            \"quick_save\": \"Sàbhailte gu luath\",\n            \"quick_save_fail_no_slots\": \"Chan eil slotan sàbhalaidh luath sam bith air an rèiteachadh\",\n            \"save_game\": \"Geama air a shàbhaladh gu slot %d\",\n            \"save_game_fail_invalid_slot\": \"Slot sàbhalaidh mì-dhligheach: %d\",\n            \"sound_available_samples\": \"Fuaimean ri làimh: %s\",\n            \"sound_playing_sample\": \"A' cluich fuaim %d\",\n            \"speed_get\": \"Astar làithreach: %d\",\n            \"speed_set\": \"Astar air a shuidheachadh air %d\",\n            \"strings_failed\": \"Dh'fhàillig ath-luchdadh nam faidhlichean cànain\",\n            \"strings_reloaded\": \"Chaidh na faidhlichean cànain ath-luchdachadh\",\n            \"textures_off\": \"Teacsaichean: dheth\",\n            \"textures_on\": \"Teacsaichean: air\",\n            \"trapezoid_filter_off\": \"Criathradh trapeasoid dheth\",\n            \"trapezoid_filter_on\": \"Criathradh trapeasoid air\",\n            \"ui_off\": \"Eadar-aghaidh dheth\",\n            \"ui_on\": \"Eadar-aghaidh air\",\n            \"unknown_command\": \"Àithne neo-aithnichte: %s\",\n            \"upscaling_factor\": \"Factar sgèileachaidh: x%d\",\n            \"wireframe_mode_off\": \"Modh frèam-uèir: dheth\",\n            \"wireframe_mode_on\": \"Modh frèam-uèir: air\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"Beothachadh: \",\n            \"debug_animation_state\": \"Stàit: \",\n            \"debug_camera_pos\": \"Tùs a' chamara: \",\n            \"debug_camera_target\": \"Targaid a' chamara: \",\n            \"debug_immune\": \"Neo-bhàsmhorachd air\",\n            \"debug_position\": \"Suidheachadh: \",\n            \"debug_rotation\": \"Cuairteachadh: \",\n            \"debug_speed\": \"Luas: \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"\\\\{review}Sguab às\",\n            \"delete_save_confirm\": \"\\\\{review}A bheil thu airson an t-sàbhail seo a sguabadh às?\",\n            \"delete_save_failed\": \"\\\\{review}Dh'fhàillig an sguabadh às air an t-sàbhail a chaidh a thaghadh.\",\n            \"delete_save_no\": \"\\\\{review}Chan eil\",\n            \"delete_save_yes\": \"\\\\{review}Tha\",\n            \"exit_game\": \"Dùin an Geama\",\n            \"exit_to_title\": \"Till gu Tiotal\",\n            \"load_game\": \"Luchdaich Geama\",\n            \"mode_new_game\": \"Geama Ùr\",\n            \"mode_new_game_jp\": \"GÙ Iapanach\",\n            \"mode_new_game_jp_plus\": \"GÙ+ Iapanach\",\n            \"mode_new_game_plus\": \"Geama Ùr+\",\n            \"new_game\": \"Geama Ùr\",\n            \"play_previous_levels\": \"Cluich ìrean roimhe\",\n            \"restart_level\": \"Ath-thòisich an Ìre\",\n            \"save_game\": \"Sàbhail Geama\",\n            \"save_slot_unsupported\": \"Cha bheir an sàbhaladh seo taic don fheart seo.\",\n            \"select_level\": \"Tagh Ìre\",\n            \"select_mod\": \"\\\\{review}Tagh Geama\",\n            \"select_mode\": \"Tagh Modh\",\n            \"select_save\": \"Tagh Sàbhail\",\n            \"story_so_far\": \"An sgeul gu ruige seo...\",\n            \"switch_mod\": \"\\\\{review}Atharraich Geama\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"A bheil thu cinnteach?\",\n            \"continue\": \"Lean air adhart\",\n            \"exit_to_title\": \"Theid dhan tiotal?\",\n            \"no\": \"Chan eil\",\n            \"paused\": \"Air a stad\",\n            \"quit\": \"Dùin\",\n            \"yes\": \"Tha\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"Frèam air adhart\",\n            \"camera_move_prompt\": \"Gluais an camara\",\n            \"camera_reset_prompt\": \"Ath-shuidhich an camara\",\n            \"camera_roll_prompt\": \"Rolla an camara\",\n            \"camera_rotate_90_prompt\": \"Cuairtich 90 ceum\",\n            \"camera_rotate_prompt\": \"Cuairtich an camara\",\n            \"change_lara_pose\": \"Atharraich seasamh\",\n            \"fov_prompt\": \"Atharraich FOV\",\n            \"lara_move_prompt\": \"Gluais Lara\",\n            \"lara_reset_prompt\": \"Ath-shuidhich Lara\",\n            \"lara_roll_prompt\": \"Rolla Lara\",\n            \"lara_rotate_90_prompt\": \"Tionndaidh Lara 90°\",\n            \"lara_rotate_prompt\": \"Tionndaidh Lara\",\n            \"snap_prompt\": \"Tog dealbh\",\n            \"title_camera_pos\": \"Modh Dealbh\",\n            \"title_lara_pos\": \"A' gluasad Lara\",\n            \"toggle_help\": \"Tionndaidh cuideachadh\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"Tha na roghainnean ciorramach airson nan ìrean seo.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Deasaich luach\",\n                \"frozen_option_disclaimer\": \"Tha an suidheachadh seo air a chur an gnìomh le ùghdar nan ìrean agus chan urrainnear ga atharrachadh.\",\n                \"hue\": \"Tuar\",\n                \"lightness\": \"Soilleireachd\",\n                \"restore_default\": \"Ath-shuidhich\",\n                \"toggle_help\": \"Tionndaidh cuideachadh\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Rianadair\",\n                    \"keyboard\": \"Meur-chlàr\",\n                },\n                \"customize\": \"Gnàthaich na Smachdan\",\n                \"layout\": {\n                    \"custom_1\": \"Iuchraichean Cleachdaiche 1\",\n                    \"custom_2\": \"Iuchraichean Cleachdaiche 2\",\n                    \"custom_3\": \"Iuchraichean Cleachdaiche 3\",\n                    \"default\": \"Iuchraichean Bunaiteach\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Gluasad\",\n                    \"items\": \"Nithean\",\n                    \"misc\": \"Measgaichte\",\n                    \"system\": \"Siostam\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Smachdan\",\n                    \"fixes\": \"Càradh\",\n                    \"general\": \"Cumanta\",\n                    \"mods\": \"Mods\",\n                    \"presets\": \"\\\\{review}Ro-shealladh\",\n                },\n                \"title\": \"Roghainnean Cluiche\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"Bàr\",\n                    \"rendering\": \"Reandaradh\",\n                    \"stats\": \"\\\\{review}Staitistig\",\n                    \"ui\": \"Eadar-aghaidh\",\n                    \"visuals\": \"Lèirsinneachd\",\n                },\n                \"title\": \"Roghainnean Grafaigeach\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"Iomlan\",\n                    \"volume\": \"Tomhas\",\n                },\n                \"title\": \"Roghainnean Fuaime\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Buaidhean / Losgadh\",\n            \"ammo_hits\": \"Buaidhean\",\n            \"ammo_used\": \"Losgadh\",\n            \"assault_best_time_fmt\": \"%s\",\n            \"assault_finish\": \"Crìochnaich\",\n            \"assault_no_times_set\": \"Gun Àmanan Fhathast\",\n            \"assault_other_times_fmt\": \"%s\",\n            \"assault_title\": \"NA H-ÀMANAN AS FHEÀRR\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Staitistig a Bharrachd\",\n            \"crystals\": \"\\\\{review}Criostalan\",\n            \"deaths\": \"Bàsan\",\n            \"detail_fmt\": \"%d de %d\",\n            \"distance_travelled\": \"Astar air a Shiubhal\",\n            \"final_statistics\": \"Staitistig Dheireannach\",\n            \"gym_assault_course\": \"Cùrsa Ionnsaigh\",\n            \"gym_racetrack_course\": \"Cùrsa Rèis\",\n            \"kills\": \"Marbhain\",\n            \"level\": \"Ìre\",\n            \"medipacks_used\": \"Pasganan Cleachdte\",\n            \"none\": \"Gin sam bith\",\n            \"pickups\": \"Togailtean\",\n            \"secrets\": \"Dìomhaireachdan\",\n            \"time_taken\": \"Ùine air a Ghabhail\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Tionndaidheas feaman-fuilt Lara.\",\n            },\n            \"cheats\": {\n                \"help\": \"Tionndaidheas cleasan a' gheama.\",\n            },\n            \"clear\": {\n                \"help\": \"Glanas logaichean consol follaiseach.\",\n            },\n            \"debug\": {\n                \"help\": \"Tionndaidheas fiosrachadh deasbug.\",\n            },\n            \"drain\": {\n                \"help\": \"Drèaneas an t-seomair làithreach.\",\n            },\n            \"end_level\": {\n                \"help\": \"Crìochnaicheas an ìre làithreach.\",\n            },\n            \"exit\": {\n                \"help\": \"Dùinidh an gèama.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Tionndaidheas am mapa-flip.\",\n            },\n            \"flood\": {\n                \"help\": \"Lìonas an t-seomair làithreach le uisge.\",\n            },\n            \"fly\": {\n                \"help\": \"Tionndaidheas an cleas itealach.\",\n            },\n            \"fps\": {\n                \"help\": \"Atharraicheas an suidheachadh FPS.\",\n            },\n            \"give\": {\n                \"help\": \"Cuireas nì sònraichte anns clàr-seilbhe Lara.\",\n                \"invalid_secret\": \"Dìomhaireachd mì-dhligheach: %s (dìomhaireachdan dligheach: %s)\",\n                \"secret_given\": \"Chaidh dìomhaireachd %s air a chur ris\",\n                \"secret_list\": \"Dìomhaireachdan air an cruinneachadh: %d de %d (%s)\",\n                \"secret_none\": \"Dìomhaireachdan air an cruinneachadh: %d de %d\",\n                \"secret_taken\": \"Chaidh dìomhaireachd %s air a thoirt air falbh\",\n            },\n            \"give_secret\": {\n                \"help\": \"Seall liosta de dhìomhaireachdan Lara, neo cuir/thoir air falbh fear a rèir àireamh.\",\n            },\n            \"heal\": {\n                \"help\": \"Leighis Lara gu slàinte làn a-rithist.\",\n            },\n            \"help\": {\n                \"help\": \"Seallas cuideachadh airson gach àithne neo cuideachadh mionaideach airson tè shonraichte.\",\n                \"list\": \"Òrdughan rim faighinn:\",\n            },\n            \"hp\": {\n                \"help\": \"Suidhicheas slàinte Lara gu àireamh ainmichte.\",\n            },\n            \"immune\": {\n                \"help\": \"Tionndaidh neo-bhàsmhorachd. (Faodaidh Lara fhathast bàsachadh ann an cuid de shuidheachaidhean.)\",\n                \"off\": \"Tha Lara so-leònte a-nis\",\n                \"on\": \"Tha Lara do-ruigsinneach do mhilleadh a-nis\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"Cuir às do ruith-luath gun chrìoch.\",\n                \"off\": \"Chan urrainn do Lara ruith gu luath gun chrìoch tuilleadh\",\n                \"on\": \"'S urrainn do Lara ruith gu luath gun chrìoch a-nis\",\n            },\n            \"kill\": {\n                \"help\": \"Cuir às do nàimhdean a tha faisg air làimh.\",\n            },\n            \"lighting\": {\n                \"help\": \"Tionndaidh siostam solais\",\n            },\n            \"load\": {\n                \"help\": \"Luchdaich geama bhon t-slot sàbhalaidh ainmichte neo slot luath.\",\n            },\n            \"lua\": {\n                \"help\": \"Ruith an t-sreang còd Lua a chaidh a thoirt seachad.\",\n                \"runtime_error\": \"Mearachd a' ruith Lua: %s\",\n                \"syntax_error\": \"Mearachd sìntacs Lua: %s\",\n            },\n            \"mod\": {\n                \"help\": \"\\\\{review}A' gluasad gu am mod sònraichte agus a' tòiseachadh a' gheama a-rithist.\",\n            },\n            \"music\": {\n                \"help\": \"Cluich slighe-ciùil leis an ID ainmichte.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Cluich sealladh-film leis an àireamh ainmichte.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Cluich taisbeanadh leis an àireamh ainmichte.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Cluich ìre an t-Seòmair-eacarsaich.\",\n            },\n            \"play_level\": {\n                \"help\": \"Cluich ìre leis an ainm neo àireamh ainmichte.\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Slighe-ciùil mì-dhligheach\",\n                \"stopped\": \"Sguir an ceòl\",\n                \"track\": \"A' cluich ciùil %d\",\n            },\n            \"pos\": {\n                \"help\": \"Seall suidheachadh Lara.\",\n            },\n            \"save\": {\n                \"help\": \"Sàbhail geama dhan t-slot ainmichte neo dhan ath shlot luath.\",\n            },\n            \"screenshot\": {\n                \"help\": \"Sàbhail glacadh-sgrìn gu diosc, le slighe fhaidhle roghainneil.\",\n            },\n            \"set\": {\n                \"help\": \"Seall neo ùraich roghainn rèiteachaidh ainmichte.\",\n            },\n            \"sfx\": {\n                \"help\": \"Cluich buaidh-fuaime leis an ID ainmichte.\",\n            },\n            \"spawn\": {\n                \"fail\": \"Cha deach an nì air iarraidh a chruthachadh\",\n                \"success\": \"Chaidh an nì air iarraidh a chruthachadh faisg air Lara\",\n            },\n            \"speed\": {\n                \"help\": \"Atharraich astar a' gheama.\",\n            },\n            \"strings\": {\n                \"help\": \"Ath-luchdaich faidhlichean cànain làithreach on diosg.\",\n            },\n            \"teleport\": {\n                \"item\": \"Air a ghluasad gu nì: %d\",\n                \"item_fail\": \"Dh'fhàillig gluasad gu nì: %d\",\n                \"object\": \"Air a ghluasad gu oibseact: %s\",\n                \"object_fail\": \"Dh'fhàillig gluasad gu oibseact: %s\",\n                \"pos\": \"Air a ghluasad gu suidheachadh: %.3f %.3f %.3f\",\n                \"pos_fail\": \"Dh'fhàillig gluasad gu suidheachadh: %.3f %.3f %.3f\",\n                \"room\": \"Air a ghluasad gu seòmar: %d\",\n                \"room_fail\": \"Dh'fhàillig gluasad gu seòmar: %d\",\n            },\n            \"textures\": {\n                \"help\": \"Cuir air neo dheth an gnìomh teacsaichean.\",\n            },\n            \"title\": {\n                \"help\": \"Till gu scrion a' thiotail.\",\n            },\n            \"tp\": {\n                \"help\": \"Gluais Lara gu suidheachadh neo àireamh rùm ainmichte.\",\n            },\n            \"trigger\": {\n                \"help\": \"Nì nìomhaichte neo gun nìomhaichte le id, ainm nì, neo ainm nì.\",\n                \"invalid_item\": \"Nì ceàrr: %s\",\n                \"no_match\": \"Targaid neo-aithnichte: %s\",\n                \"not_found\": \"Cha deach nithean freagarrach a lorg airson: %s\",\n                \"triggered\": \"Nìomhaichte nithean: %s\",\n                \"untriggered\": \"Gun nìomhaichte nithean: %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Tionndaidheas sioncronachadh-inghearach air neo dheth.\",\n            },\n            \"weather\": {\n                \"help\": \"Atharraichidh seo an aimsir.\",\n                \"invalid\": \"Aimsir neo-dhligheach: %s (dligheach: %s)\",\n                \"set\": \"Suidhich an aimsir gu %s\",\n            },\n            \"winston\": {\n                \"dead\": \"Tha am butlar agad marbh. 'S tu nad fuamhaire!\",\n                \"spawn_failed\": \"Cha do shoirbhich le Winston a ghairm\",\n                \"spawned\": \"Thàinig Winston faisg air Lara\",\n                \"teleported\": \"Thàinig Winston faisg air Lara\",\n            },\n            \"wireframe\": {\n                \"help\": \"Tionndaidh reandaradh frèam-uèir air neo dheth.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"\\\\{review}TR1 PC\",\n            \"tr1_ps1\": \"\\\\{review}TR1 PS1\",\n            \"tr2_pc\": \"\\\\{review}TR2 PC\",\n            \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n            \"tr3_pc\": \"\\\\{review}TR3 PC\",\n            \"tr3_ps1\": \"\\\\{review}TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"TR2 PC\",\n                \"tr2_ps1\": \"TR2 PS1\",\n                \"tr3_pc\": \"TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"Bunaiteach\",\n                \"golden_sophia\": \"Sophia Òir\",\n                \"sophia\": \"Sophia\",\n                \"tr1_bacon_lara\": \"Lara Bacon\",\n                \"tr1_classic\": \"TR1 Clasaigeach\",\n                \"tr1_combo\": \"TR1 Cothlamadh\",\n                \"tr1_golden_bacon_lara\": \"Lara Bacon Òir\",\n                \"tr1_golden_lara\": \"TR1 Lara Òir\",\n                \"tr1_gym\": \"TR1 Trèanadh\",\n                \"tr1_mauled\": \"TR1 Air a Leòn\",\n                \"tr1_ngage\": \"TR1 N-Gage\",\n                \"tr23_golden_lara\": \"TR2/3 Lara Òir\",\n                \"tr2_bomber_jacket\": \"Seacaid Bomber\",\n                \"tr2_classic\": \"TR2 Clasaigeach\",\n                \"tr2_diving_suit\": \"Deise Dàibhidh 1\",\n                \"tr2_diving_suit_alpha\": \"Deise Dàibhidh 2\",\n                \"tr2_gym\": \"TR2 Trèanadh\",\n                \"tr2_robe\": \"Ròba\",\n                \"tr2_vegas\": \"Las Vegas\",\n                \"tr3_antarctica\": \"An Antartaig\",\n                \"tr3_catsuit\": \"Lunnainn\",\n                \"tr3_classic\": \"TR3 Clasaigeach\",\n                \"tr3_gym\": \"TR3 Trèanadh\",\n                \"tr3_nevada\": \"Nevada\",\n                \"tr3_south_pacific\": \"A' Chuain Sèimh a Deas\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"\\\\{review}Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"\\\\{review}Tomb Raider I Deuchainn\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"\\\\{review}Gnothach Neo-chrìochnaichte\",\n            },\n            \"tr2\": {\n                \"title\": \"\\\\{review}Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"\\\\{review}An Masg Òir\",\n            },\n            \"tr3\": {\n                \"title\": \"\\\\{review}Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"\\\\{review}An T-Seann Earraicht\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"Fa leth\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"Air a cho-roinn\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"Gin sam bith\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"\\\\{review}Dubh\",\n            \"BK_IMAGE\": \"\\\\{review}Ìomhaigh\",\n            \"BK_MONOCHROME\": \"Aon-dhathach\",\n            \"BK_MONOCHROME_COOL\": \"\\\\{review}Monochrome (fuar)\",\n            \"BK_MONOCHROME_WARM\": \"\\\\{review}Monochrome (blàth)\",\n            \"BK_NONE\": \"\\\\{review}Soilleir\",\n            \"BK_PATTERN_STATIC\": \"Statach\",\n            \"BK_PATTERN_WAVE\": \"Crathadh\",\n            \"BK_TRANSPARENT_DARK\": \"Fìor dhorcha\",\n            \"BK_TRANSPARENT_MEDIUM\": \"Dorcha\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"An-còmhnaidh\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"A-mhàin ri boss\",\n            \"BAR_SHOW_MODE_NEVER\": \"Na seall idir\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"Gin sam bith\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"Sealladh\",\n            \"BILLBOARD_LOCK_ROLL\": \"Rothladh\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"Rothladh is claonadh\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"\\\\{review}Ciorramh\",\n            \"BLOOD_EFFECTS_PINK\": \"\\\\{review}Pinc\",\n            \"BLOOD_EFFECTS_RED\": \"\\\\{review}Dearg\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"Bunaiteach\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"A-riamh\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"Fo uisge\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"\\\\{review}Smachdair\",\n            \"INPUT_BACKEND_KEYBOARD\": \"\\\\{review}Meur-chlàr\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Gnìomh\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Camara air Ais\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Camara Sios\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Camara air Adhart\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Camara gu Chlì\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"Ath-shuidhich an camara\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Camara gu Dheas\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Camara Suas\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"Atharraich aodach\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Atharraich an Targaid\",\n            \"INPUT_ROLE_CROUCH\": \"Crùb\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"Cuairt Coimeas Solais\",\n            \"INPUT_ROLE_DOWN\": \"Air Ais\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Uidheamaich\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Consol Leasachaidh\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"Uidheamaich Piostalan Fèin-obrachail\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"Uidheamaich an Desert Eagle\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"Uidheamaich Lannsair Grenèad\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"Uidheamaich Harpùn\",\n            \"INPUT_ROLE_EQUIP_M16\": \"Uidheamaich M16\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"Uidheamaich Magnuman\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"Uidheamaich MP5\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"Uidheamaich Piostalan\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"Uidheamaich Lannsair Rocaid\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"Uidheamaich Gunna-sgaoil\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"Uidheamaich Uzis\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Cleas Itealaich\",\n            \"INPUT_ROLE_FPS\": \"Seall FPS\",\n            \"INPUT_ROLE_INVENTORY\": \"Clàr-seilbhe\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Cleas Nì\",\n            \"INPUT_ROLE_JUMP\": \"Leum\",\n            \"INPUT_ROLE_LEFT\": \"Gu Chlì\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Cleas Ìre\",\n            \"INPUT_ROLE_LOAD\": \"Luchdaich\",\n            \"INPUT_ROLE_LOOK\": \"Coimhead\",\n            \"INPUT_ROLE_PAUSE\": \"Stad\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"Luchdaich gu luath\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"Sàbhail gu luath\",\n            \"INPUT_ROLE_RIGHT\": \"Gu Dheas\",\n            \"INPUT_ROLE_ROLL\": \"Rolla\",\n            \"INPUT_ROLE_SAVE\": \"Sàbhail\",\n            \"INPUT_ROLE_SCREENSHOT\": \"Glacadh-sgrìn\",\n            \"INPUT_ROLE_SLOW\": \"Coisich\",\n            \"INPUT_ROLE_SPRINT\": \"Ruith gu Luath\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Ceum air Chlì\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Ceum air Dheas\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"Atharraich Meud nan Iomallan\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"Atharraich Factar Sgèileachaidh\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"Tionndaidh Criathradh Teacsdaire\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"Tionndaidh Modh-làn-sgrìn\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Tionndaidh Modh Dealbh\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"Cuir dheth Teacsaichean\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"Tionndaidh Criathradh Trapeasoid\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"Tionndaidh an Eadar-aghaidh\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"Tionndaidh air frèam-uèir\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Luas Turbo\",\n            \"INPUT_ROLE_UP\": \"Ruith\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Pasgan Mòr\",\n            \"INPUT_ROLE_USE_FLARE\": \"Cleachd Lòchran\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Pasgan Beag\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"Dheth\",\n            \"JUMP_LOCK_LEGACY\": \"Dualchasach\",\n            \"JUMP_LOCK_TUNED\": \"Fìnealta\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"Àrd\",\n            \"LIGHTING_CONTRAST_LOW\": \"Ìosal\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Meadhanach\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"An-còmhnaidh\",\n            \"LOADING_SCREENS_DISABLED\": \"Ciorramach\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"Geamannan ùra\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"Leasaichte\",\n            \"LOOK_MODE_RESTRICTED\": \"Cuibhrichte\",\n            \"LOOK_MODE_UNRESTRICTED\": \"Neo-chuibhrichte\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"An-còmhnaidh\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"A-riamh\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Neo-àrainneachd\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"Ioma-sgèile\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"Ioma-singilte\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"Tarraing/falach\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"Tarraing a-mhàin\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"Cearcall\",\n            \"SHADOW_TYPE_OCTAGON\": \"Ochd-cheàrnach\",\n            \"SHADOW_TYPE_SPRITE\": \"Spraide\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"\\\\{review}Furasta\",\n            \"STATS_STYLE_BORDERED\": \"\\\\{review}Leacanach\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"Dheth\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"Neo-shoilleir\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"Follaiseach\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Glas làn\",\n            \"TARGET_LOCK_MODE_NONE\": \"Gun ghlas\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Leth-ghlas\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Da-lìnear\",\n            \"TEXTURE_FILTER_POINT\": \"Dheth\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"Bonn na meadhan\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"Bonn-clì\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"Bonn-deas\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"Bàrr na meadhan\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"Bàrr-clì\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"Bàrr-deas\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"Air a chàradh\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"Tomhas àrainneachd\",\n            \"description\": \"Cuiridh seo tomhas àrainneachd an àite.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"Tomhas nan seallaidhean\",\n            \"description\": \"Cuiridh seo tomhas nan seallaidhean sa gheama.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Micreofon ri taobh Lara\",\n            \"description\": \"Cuiridh seo am micreofon aig suidheachadh Lara. Ma thèid seo air a chur dheth, bidh e aig suidheachadh a' chamara.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"\\\\{review}Cluich ceòl anns an stòras\",\n            \"description\": \"\\\\{review}Leig le fuaimean a’ gheama, fuaimean àrainneachd agus ceòl a bhith a’ cluich fhathast air an sgrion clàr-stòraidh.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Ceòl sa phrìomh chlàr\",\n            \"description\": \"Cluichidh ceòl anns a' phrìomh chlàr.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Fuaimean atharraichte\",\n            \"description\": \"Cuiridh seo cead air buaidhean fuaim le beagan caochlaidhean àirde-fuaim gus iomadachd a thoirt do dh'fhuaimean a' gheama.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"Cleachd fuaimean a' PhS1\",\n            \"description\": \"Cleachd fuaimean sònraichte bhon PhS1 an àite an fheaghainn PhC.\\n\\n - Fuaim Uzi (TR1 a-mhàin)\\n- Fuaim casrùisgte Lara (TR2 a-mhàin)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"SFX beòthalachd fon uisge\",\n            \"description\": \"Leigidh seo leat smachd a chumail air buaidhean-fuaim beòthalachd shònraichte - airson nithean mar dorsan is trapaichean - nuair a tha a' chamara fon uisge.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Càraich fuaim bloca slabhraidh\",\n            \"description\": \"Cuiridh seo casg air fuaim dìomhaireachd a chluich gu ceàrr nuair a thèid an iuchair òir a chleachdadh ann an Uaigh Tihocan.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Ceòl dìomhaireachd ioma-fhilleadh\",\n            \"description\": \"Cuiridh seo ceartachadh air far a bheil ceòl dìomhaireachd a' stad an slighe-ciùil a tha a' cluich.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Labhairt nàmhaid ioma-fhilleadh\",\n            \"description\": \"Cuiridh seo ceartachadh air far a bheil nàimhdean a' stad ciùil gnìomhach nuair a bhruidhneas iad.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"Tomhas FMV\",\n            \"description\": \"Atharraichidh tomhas nam filmichean.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"Tomhas fuaim àrainneachd (stòradh)\",\n            \"description\": \"Atharraichidh tomhas àrainneachd air scrion stòrais.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"Tomhas ciùil (stòradh)\",\n            \"description\": \"Atharraichidh tomhas ciùil air scrion stòrais.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Càraich ciùil aon-ùine\",\n            \"description\": \"Cuiridh seo an luchdachadh air ais de chriomagan ciùil aon-ùine gus nach cluich iad a-rithist gu mearachdach.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{icon music} Tomhas prìomh\",\n            \"description\": \"Atharraichidh tomhas fuaim a' gheama gu lèir. Tha na tomhasan eile co-cheangailte ris an fhearr seo\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Aisig ceòl aig luchdadh\",\n            \"description\": \"Cuiridh seo an slighe-ciùil a bha a' cluich nuair a chaidh an geama a shàbhaladh air ais an sàs aig àm luchdaidh.\\n\\n- A-riamh: na bi ag ath-chluich ciùil idir.\\n- Neo-àrainneachd: ath-chluich ceòl neo-àrainneachdail a-mhàin.\\n- An-còmhnaidh: ath-chluich ceòl sam bith.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"Tomhas ciùil\",\n            \"description\": \"Atharraichidh tomhas ciùil.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"Balbhaich fuaim gun fòcas\",\n            \"description\": \"Balbhaichidh seo a h-uile phìos ciùil is buaidh-fuaim nuair nach eil uinneag a' gheama ann am fòcas.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Meud fuaime\",\n            \"description\": \"Cuiridh seo atharrachadh air meud nam buaidhean fuaime.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"Tomhas àrainneachd (fo uisge)\",\n            \"description\": \"Atharraichidh tomhas àrainneachd fo uisge.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"Tomhas ciùil (fo uisge)\",\n            \"description\": \"Atharraichidh tomhas ciùil fo uisge.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"Ùine lòchran gun chrìoch\",\n            \"description\": \"Cùimidh seo lòchran os laimhe a' deàrrsadh gu bràth. Theid lòchran air an tilgeil a-mach mar as àbhaist.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"Ruith gun chrìoch\",\n            \"description\": \"Cùimidh seo Lara a' ruith gu bràth fhad 's a tha am putan freagairteach air a phutadh.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"Poileasaidh nàimhdeas aonaid\",\n            \"description\": \"Atharraichidh seo an doigh anns a bhios cairdean a' freagairt nuair a thèid an dochann.\\n\\n- Fa leth: bidh an caraid - agus an caraid seo a-mhàin - a-nis na nàmhaid (stoidhle TR3).\\n- Air a cho-roinn: bidh gach caraid a-nis na nàmhaid (stoidhle TR2).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Luas a' chamara\",\n            \"description\": \"Atharraicheas dè cho luath 'sa ghluaiseas an camera.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Atharraich modh nochdaidh Pierre\",\n            \"description\": \"Leigidh seo le Pierre ùr a thigeas am bàrr a chuir an àite Pierre eile a tha fhathast beò.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"Poileasaidh bàthadh chreutairean\",\n            \"description\": \"Bheir buaidh air an doigh anns a bhios creutairean tìreil gan giùlan fhèin ann an seòmraichean uisge.\\n\\n- A-riamh: cha bhàsaichidh creutairean tìreil gu bràth (stoidhle TR1).\\n- Bunaiteach: bàsaichidh creutairean tìreil ann an uisge 2 chlic no nas doimhne (stoidhle TR2/3).\\n- Fo uisge: bàsaichidh creutairean tìreil a-mhàin nuair a tha iad gu tur fon uisge.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"Thoir air falbh armachd a bharrachd\",\n            \"description\": \"Thoir air falbh gach armachd agus ammo bhon gheama ach Pistols (airson ruith dùbhlain Pistols a-mhàin).\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Droch staid leantainneach\",\n            \"description\": \"Cuir stad air Lara bho bhith ag ath-shlànachadh nuair a thòisicheas i ìre ùr (airson ruith gun slànachadh).\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Thoir air falbh pasganan slàinte\",\n            \"description\": \"Thèid na pasganan slàinte uile a thoirt air falbh às a' gheama (airson ruith 'Gun Leigheas').\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Cuir às ri buaidh T-Rex marbh\",\n            \"description\": \"Cuiridh seo às ri buaidh-bhualaidh corp T-Rex às deidh bàs, gus casg a chur air bacadh slighe a-mach.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"Targaidich air càirdean\",\n            \"description\": \"Leigidh seo le Lara a targaidich air càirdean - manaich, 's a leithid. Mura tèid seo a chur an comas, bidh iad dìonach air peilearan Lara.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Roghainn ro-làimh air iuchraichean\",\n            \"description\": \"Ma bhriogas Lara air gnìomh aig toll iuchrach neo àite tòimhseachain, taghaidh i an nì ceart gu fèin-obrachail.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"Tuiseal air leathad air chùl\",\n            \"description\": \"Bheir seo air Lara tuisleadh ma leumas i air ais agus nuair a tha leathad air a cùlaibh (TR3). Ma tha e à comas, stadaidh Lara gu cruaidh an aghaidh an leathaid (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"Pocannan-cuirp\",\n            \"description\": \"Cuirish seo an comas a chuir às ri nàimhdean a chaidh a mharbhadh nuair a thèid Lara thairis air brosnachaidhean sònraichte ann an cuid de dh'ìrean. Ma tha e à comas, bidh nàimhdean marbh air an taisbeanadh an-còmhnaidh.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"Crathadh ulpagan\",\n            \"description\": \"Crathaidh an camara nuair a bhios ulpagan a' gluasad.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"Grenadan breabadh\",\n            \"description\": \"Cleachd grenadan ann an stoidhle TR3: bualaidh iad air ballachan 's claisean, agus nì iad raon spreadhaidh nas motha, ach aig cosgais astar nas ìsle.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Cleasan\",\n            \"description\": \"Cuiridh seo diofar chleasan an comas:\\n\\n- L: crìochnaich an ìre sa bhad.\\n- I: bheir do Lara na h-armachd uile, pailteas lòin, pasganan slàinte agus stuthan cuilbheart na h-ìre làithreach.\\n- O: cuir snàmh tro èadhar an comas.\\n  - Coisich: stad a' snàmh tro èadhar.\\n  - Uidheamaich: fosgail an doras as fhaisge (chan obraichidh seo an-còmhnaidh).\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"Seallaidhean sgriobtaichte\",\n            \"description\": \"Cuiridh seo an doigh seallaidhean sgriobtaichte aig toiseach cuid de dh'ìrean far a bheil iad air an sònrachadh.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Staitistig ìre anns a' chombaist\",\n            \"description\": \"Seallaidh seo staitistig na h-ìre nuair a thaghas tu a' chombaist.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Co-chomhairle leasachaidh\",\n            \"description\": \"Cuiridh seo an consol leasachaidh an comas.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"Tuit fo smachd\",\n            \"description\": \"Leigidh seo le Lara tionndadh san adhar agus grèim a ghabhail air an oir a dh'fhàg i, fhad 's a tha am putan gnìomh air a chumail sios tron tuiteam.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"Leum às snàg\",\n            \"description\": \"Leigidh le Lara leum a-mach à àiteachan snàgaidh.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"Claonadh snàgail\",\n            \"description\": \"Nì seo co-thaobhadh air cuairteachadh Lara a-reir geoimeatraidh an làir nuair a tha i a' snàgail.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"Snàgail\",\n            \"description\": \"Leigidh seo le Lara cromadh sìos agus snàgail.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"Sgrìoban creideis\",\n            \"description\": \"Cuiridh seo sgrìoban creideis an comas às dèidh crìochnachadh a' gheama. Cha bheireas seo buaidh air an sgrion staitistig mu dheireadh.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"Rolla crom\",\n            \"description\": \"Leigidh seo le Lara rolla air adhart a dhèanamh fhad 's a tha i crom le putan sprint.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Seallaidhean-film\",\n            \"description\": \"Cuiridh seo cluich seallaidhean-film an comas.\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Modh taisbeanaidh\",\n            \"description\": \"Leigidh le seo taisbeanadhan a chluich bhon phrìomh chlàr.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Ceàrn tùsail nàimhdean\",\n            \"description\": \"Cuiridh ceàrn thuilleadh air thuaiream ri cuid de na nàimhdean nuair a thòisicheas iad.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Buaidhean sàbhalaidh\",\n            \"description\": \"Leasaichidh sàbhaladh geama gus am bi buaidhean grafaigeach, ceò easan, sgaoiladairean lasraidh, agus tuilleadh, air an sàbhaladh an àite a dhol à sealladh nuair a luchdaichear iad.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"Bhideothan\",\n            \"description\": \"Cuiridh seo cluich bhideothan an comas.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Taghadh modh geama\",\n            \"description\": \"Leigeas le roghainnean Geama Ùr+ a bhith rim faotainn sa phasport aig toiseach a' gheama.\\n\\n- Geama Ùr+: fuasgladh nan armachd air fad le peilearan gun chrìoch; bidh HP dùbailte aig na nàimhdean.\\n- GÙ Iapanach: bidh na h-armachd ag adhbhrachadh milleadh dùbailte, agus bheir togail lòchrain 8 seach 6.\\n- GÙ+ Iapanach: measgachadh de GÙ+ is GÙ Iapanach.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"Camara seasaimh\",\n            \"description\": \"Gluaisidh an camara gus aghaidh a thoirt air Lara rè a beòthalachd seasaimh. Put seall gus an camara a ath-shuidheachadh.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Sealladh ais-iompaichte\",\n            \"description\": \"Tionndaidhidh an smachd Y-axis nuair a choimheadas Lara.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Sgrùdadh nithean\",\n            \"description\": \"Airson ìrean gnàthaichte — leigidh seo le tuairisgeulan nì a nochdadh san t-seilbh ma bheir ùghdar na h-ìre dàta freagarrach.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Snìomh leum\",\n            \"description\": \"Cuiridh seo snìomh is leth-chasach TR2+ an comas — m.e. brùth rolla rè leum neo dàibheadh.\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"Cleachd blocaichean putadh marbhach\",\n            \"description\": \"Ma tha seo comasach, nuair a thuiteas bloc putadh bhon adhar air Lara, cuiridh e i gu bàs i. Mur nach eil, theid Lara troimh a' bhloc.\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Leum le claonadh\",\n            \"description\": \"Leigidh Lara sleamhnachadh beagan air adhart/air ais le leum neodrach ma bhithear a' cumail iuchair stiùiridh.\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"Leum bho oir\",\n            \"description\": \"Leigidh seo le Lara leum suas neo air ais nuair a tha i a' crochadh air oir, fhad 's a tha uachdar cruaidh os a comhair airson putadh às.\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Sgrionaichean laghail\",\n            \"description\": \"Seallaidh seo scrion laghail agus bhideo Core Design aig toiseach a' gheama.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"Camara làimhe\",\n            \"description\": \"Cuir comas air putanan a' chamara (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}) a thathas a' cleachdadh gus smachd a chumail air camara Modh Dealbh, gus cuideachd camara a' gheama a chuairteachadh.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"Snìomh neodrach\",\n            \"description\": \"Leigidh seo le Lara tionndadh san adhar fhad 's a tha i a' leum gu neodrach. Brùth air na putanan leum is rola còmhla ri chèile nuair a tha i na seasamh.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Taic togail\",\n            \"description\": \"Cuiridh seo buaidh dealrach goirid faisg air nithean togail gus an dèanamh nas fhasa a dh'fhaicinn.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"Cluich ìrean roimhe\",\n            \"description\": \"Cuir comas air na feartan \\\"Cluich ìrean roimhe\\\" agus \\\"Sgeulachd gu ruige seo...\\\" anns an sgrion taghaidh Geama Ùr.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"Snàgadh freagairteach\",\n            \"description\": \"Cuiridh seo air dòigh leasachaidhean air meacanaig an t-snàgaidh thùsail.\\n\\n- Leigidh le snàgadh ath-thòiseachadh nas luaithe às dèidh stad.\\n- Leigidh le gluasad bho ruith/spìonadh gu snàgadh gun stad an toiseach.\\n- Leigidh le gluasad bho shnàgadh gu rola crom (ma tha e air a chur an comas) gun cromadh le làimh an toiseach.\\n- Leigidh le Lara tionndadh fhad 's a tha i crom.\\n- Leigidh le beòthachadh-togail sonraichte a chleachdadh fhad 's a tha Lara a' snàgadh (ach a-mhàin lòchran).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"Ruith luath freagairteach\",\n            \"description\": \"Cuiridh seo staid ruith luath nas freagairtiche an gnìomh do Lara.\\n\\n- leigidh le Lara ruith gu luath cho luath 's a tha neart aice, seach a bhith a' feitheamh gus am bi stamina làn aice.\\n- leigidh le Lara ruith gu luath suas staidhre gun a bhith a stad le beòthalachd ruith àbhaisteach.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Criostalan sàbhalaidh\",\n            \"description\": \"Cuiridh seo cuingealachadh air sàbhaladh gu toiseach ìrean is criostalan sàbhalaidh aon-chleachdadh, coltach ris a' PhS1. Feumar ath-thòiseachadh an ìre airson seo obrachadh.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"Sleamhnaich-gus-ruith\",\n            \"description\": \"Leigidh seo le Lara ruith sa bhad nuair a ruigeas i air uachdar còmhnard às dèidh a bhith a' sleamhnachadh air leac leathann. Cum am putan air adhart sios gus a ghnìomhachadh.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"Luascadh slaodach\",\n            \"description\": \"Leigidh seo le Lara luascadh gu slaodach nuair a tha i air grèim fhaighinn air oir glè thana (stoidhle TR3). Ma tha seo à comas, luaisgidh Lara greiseag ghoirid mus till i gu suidheachadh crochte aig fois (stoidhle TR1/2).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"Ath-bheòthachadh bhalla rèidh\",\n            \"description\": \"Cuidichidh Lara le tilleadh nas luaithe às dèidh buille ri balla ma thèid stiùireadh is 'air adhart' a chumail.\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"Bualadh mogal bog\",\n            \"description\": \"Leigidh le Lara gluasad gu rèidh air aghaidh mogalan statach – coltach ri TR4+ – seach stad chruaidh a dhèanamh.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"Ruith gu luath\",\n            \"description\": \"Leigidh seo le Lara ruith gu luath, coltach ri TR3+.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"Brùthadh le rolla bho cheum\",\n            \"description\": \"Leigidh Lara leum bhon cheum aon-bhriog ma bhriogar rolla faisg air an oir.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Sguir siùdadh\",\n            \"description\": \"Leigidh seo le Lara leigeil às nuair a tha i a' siùdadh, agus grèim a ghlacadh sa bhad airson gluasad luath.\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Atharraich targaid\",\n            \"description\": \"Cuiridh seo an comas atharrachadh targaid TR4+ fhad 's a tha Lara ag amas.\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Gleoc sa chlàr-seilbhe\",\n            \"description\": \"Leigidh seo leis a' ghleoc a leantainn air adhart fiù 's nuair a tha an clàr-seilbhe fosgailte.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"\\\\{review}Cuir air adhart cromadh\",\n            \"description\": \"\\\\{review}Leigidh seo le Lara fuireach crom às dèidh a bhith a' brùthadh air a' phutan cromadh aon turas. Brùth air a' phutan cromadh a-rithist gus seasamh suas.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"\\\\{review}Cuir air adhart ruith gu luath\",\n            \"description\": \"\\\\{review}Leigidh seo le Lara ruith gu luath às dèidh a bhith a' brùthadh air a' phutan ruith gu luath aon turas. Brùth air a' phutan ruith gu luath a-rithist gus stad a chur air ruith gu luath.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Sgrion staitistig deireannach\",\n            \"description\": \"Cuiridh seo sgrion staitistig iomlan air adhart às dèidh nan creideasan.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Leum freagairteach\",\n            \"description\": \"Leigidh le Lara leum aig àm sam bith nuair a tha i a' ruith.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Stad snàmh freagairteach\",\n            \"description\": \"Leigidh Lara stad gu sgiobalta fon uisge nuair a thèid am putan snàmh a leigeil às.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Snàmh rèidh\",\n            \"description\": \"Cuiridh seo lùb luathaidh air tionndadh Lara fon uisge airson gluasad nas rèidh, mar ann an TR2 bho thùs.\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Rolla fon uisge\",\n            \"description\": \"Leigidh le Lara rolla a dhèanamh fon uisge.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Coiseachd tro uisge\",\n            \"description\": \"Leigidh le Lara coiseachd tro uisge eu-dhomhain seach a bhith air a stad aig uachdar na h-aibhne.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Eadar-obrachadh beòthail\",\n            \"description\": \"Nì Lara coiseachd gu togailtean is suidsichean nuair a tha i faisg orra, an àite leum dìreach thuca.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Càraich AI ailigeutair\",\n            \"description\": \"Càradh far nach dèan ailigeutairean milleadh ma dh'fhanas Lara fhathast anns an uisge.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Càraich AI mathain\",\n            \"description\": \"Càraichidh seo ionnsaigh a' mhathain gus nach caill e Lara.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Càraich buaidh dhrochaidean\",\n            \"description\": \"Càradh far nach urrainn do Lara grèim fhaighinn air cuid de dhrochaidean, agus bugan le ballachan neo-fhaicsinneach. Càraidh seo cuideachd duilgheadasan le drochaidean, dorsan-trapa is làr-lùbte nuair a tha iad air an cur thairis air chèile neo ri talamh claon.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Càraich tuiteam air leacan bristeach\",\n            \"description\": \"Càradh far am bi Lara a' tuiteam gu sgiobalta tro leacan briste nuair a dhèanas i ceum-taoibh neo coiseachd air ais orra.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Càraich prìomhachas tilgeadh lòchran\",\n            \"description\": \"Càradh far am bi Lara a' feuchainn ri lòchran a thilgeil ann am meadhan leum, a' cur casg air grèim air iomallan.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Càraich duilgheadasan dàta-làir\",\n            \"description\": \"Càradh bugan tùsail le dàta-làir - trigearan 's a leithid.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Càraich glitch lòchran an-asgaidh\",\n            \"description\": \"Càradh far am faighear lòchran gun chosgais le bhith a' brùthadh am putan fhad 's a thogas Lara rud sam bith an àrd.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Càraich glitch dùblachaidh nì\",\n            \"description\": \"Càradh far an gabh nithean prìomhach a chleachdadh dà thuras neo barrachd san t-seilbh.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"Càraich cath-thogail\",\n            \"description\": \"Càradh far am bi Lara uaireannan a' gluasad a-steach gu ballachan nuair a tha i a' togail rudeigin fon uisge, agus os cionn uisge fo mhullaichean cas leathann.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"Càraich cruinneas M16/MP5\",\n            \"description\": \"Càradh cruinneas M16/MP5 fhad 's a tha Lara a' ruith.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"Cuir prìomhachas air togail muncaidh ceart\",\n            \"description\": \"Bidh muncaidhean air an ionnsaigh a' cur prìomhachas air freagairt air ais seach a bhith a' cruinneachadh pacaidean Medi agus iuchraichean.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"Càradh amas an fhir-phìoba\",\n            \"description\": \"Càraichidh seo duilgheadas far nach urrainn don fhear-phìoba uaireannan saighdean a chuimseachadh air Lara gu ceart.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Càraich glitch QWOP\",\n            \"description\": \"Càradh glitch - ris an canar QWOP - far am bi Lara a' leum air ceumannan beaga agus a' toiseachadh gluasad gu neònach tron làr.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Càraich glitch ceumannan\",\n            \"description\": \"Càradh far am bi Lara uaireannan air a putadh tro bhalla ri taobh ceuman nuair a ruitheas i suas orra.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"Càraich buille tro uisge eu-dhomhain\",\n            \"description\": \"Cuiridh seo ceartachadh air freagairt Lara nuair a bhuail i balla fhad 's a tha i a' coiseachd tro uisge eu-dhomhain.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Càraich leum às dèidh ruith\",\n            \"description\": \"Cuiridh seo ceartachadh air cùis far nach urrainn do Lara leum dìreach às dèidh gluasad bho choiseachd gu ruith.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"Càradh geoimeatraidh bhallachan\",\n            \"description\": \"Càraichidh seo cùisean ann an geoimeatraidh ìrean OG far am faod claonaidhean taobh a-staigh bhallachan leantainn gu àireamhachadh àirde mearachdach.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"Càraich fàgail à seòmar uisge\",\n            \"description\": \"Cuiridh seo casg air Lara a bhith comasach air a dhol gu seòmraichean tioram fodha neo ri taobh an seomair làithreach. Cuiridh seo cuideachd casg oirre a sreap air sleamhnagan nach gabh seasamh orra.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"Ath-luchdadh harpùn\",\n            \"description\": \"Suidhichidh seo dè cho tric 's a dh'fheumas Lara an harpùn ath-luchdadh, stèidhichte air an lòin a th'aice. M.e. 3 = ath-luchdaich às dèidh a chleachdadh trì tursan. 0 = na bi ag ath-luchdachadh a-riamh.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"Dàil seasaimh\",\n            \"description\": \"Leigidh seo le Lara beòthalachd shònraichte a thòiseachadh às dèidh dhi dad a dhèanamh fad an àireamh de dhiogan suidhichte. Suidhich gu 0 gus a chur à comas.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"Modh glasadh leum\",\n            \"description\": \"Airson leum freagairteach, leigidh seo leat smachd a chumail air cuin a bhios Lara comasach air leum às dèidh tòiseachadh ruith.\\n\\n- Dualchasach: a rèir àm TR2 tùsail.\\n- Fìnealta: tha leum comasach 2 fhrèam nas tràithe.\\n- Dheth: tha leum comasach sa bhad às dèidh an beòthalachd tòiseachaidh.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Sgrionaichean luchdachaidh\",\n            \"description\": \"Smachdaichidh seo na sgrionaichean luchdachaidh mus luchdaich ìrean.\\n\\n- Ciorramach: na seall sgrionaichean luchdachaidh idir.\\n- An-còmhnaidh: seall sgrionaichean luchdachaidh.\\n- Geamannan ùra: leig seachad sgrionaichean luchdachaidh nuair a luchdaichear sàbhaladh.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"Modh seallaidh\",\n            \"description\": \"Leigidh seo leat smachd a chumail air cuin as urrainn do Lara sealladh a chleachdadh.\\n\\n- Cuibhrichte: chan eil sealladh comasach ach nuair a tha Lara na stad, agus a-riamh fon uisge.\\n- Leasaichte: tha sealladh comasach rè a' mhòr-chuid de bheòthalachd, ach chan ann rè gluasadan mar putadh bloca.\\n- Neo-chuibhrichte: tha sealladh comasach aig àm sam bith fo smachd Lara àbhaisteach.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"Àireamh nan slotan sàbhalaidh luath\",\n            \"description\": \"Atharraichidh seo an àireamh de shlotan sàbhalaidh luath a tha rim faotainn.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Àireamh de shloinn sàbhalaidh\",\n            \"description\": \"Atharraichidh seo an àireamh de shloinnean sàbhalaidh a tha rim faotainn.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"\\\\{review}Cuir air fois nuair a chaill thu fòcas\",\n            \"description\": \"\\\\{review}Bidh e a’ stad geama bho bhith a’ dol air adhart nuair a bhios uinneag a’ gheama a’ call fòcas.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"Dàil sgìre pròiseactail\",\n            \"description\": \"Smachd air mar a sgaoileas an raon-buaidh airson an Lannsair Grenèad agus Lannsair Rocaid.\\n\\n- Ioma-singilte: mar TR2.\\n- Ioma-sgèile: mar TR3.\\n\\nBidh an roghainn Ioma-sgèile gu tric a' leantainn gu dùbailteachadh a' mhillidh air nàimhdean fa leth.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Cuimhnich armachd eadar ìrean\",\n            \"description\": \"Cuiridh seo air Lara cuimhne dè an armachd a bh'aice mu dheireadh nuair a thòisicheas i ìre ùr. Mur tèid seo a chomasachadh, tillidh i gu piostalan nan dìollaid.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"Aisig nàimhdean PS1\",\n            \"description\": \"Cuiridh seo spaoileadan ann an City of Khamoon (PS1), seòmar 25. Feumaidh ath-thòiseachadh a' gheama gu lèir gus obraicheas seo.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Slàinte tòiseachaidh Lara\",\n            \"description\": \"Suidhichidh seo luach slàinte Lara aig toiseach gach ìre.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Modh glas armachd\",\n            \"description\": \"Cuiridh seo atharrachadh air mar a bhios armachd a' glasadh air targaidean:\\n\\n- Glas làn: cùm glasadh fiù 's ma thèideas an nàmhaid a-mach à sealladh (TR1-3).\\n- Leth-ghlas: cùm glasadh nuair a tha an nàmhaid a-mach à sealladh, ach caill e ma bhàsaicheas e.\\n- Gun ghlas: caill glasadh ma thèideas an nàmhaid a-mach à sealladh neo nuair a bhàsaicheas e (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"Modh glitch bhalla\",\n            \"description\": \"Cuiridh seo modh glitch bhalla TR1 ann an TR2 agus a chaochladh an comas. Leigidh seo cuideachd le càradh iomlan air na glitchan uile.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"Bùthadh (putanan F)\",\n            \"description\": \"Leigeas leis nam putanan F smachd mionaideach (1-frèam) a thoirt air gluasad Lara. Tha am feart seo gu tùsail ri fhaighinn a-mhàin anns a' phort TombATI (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"Bùthadh (clàr-stòraidh)\",\n            \"description\": \"Leigeas leis am putan clàr-stòraidh smachd mionaideach (2-fhraim) a thoirt air gluasad Lara.\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Pasport freagairteach\",\n            \"description\": \"Cuiridh seo às do bhacadh cuir a-steach le brath ro-luath air tionndadh duilleagan a' phasport; leigidh e leutha bhith air an clàradh an àite a bhith air an stad.\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Ceumannan-taoibh leasaichte\",\n            \"description\": \"Cuiridh seo ceumannan-taoibh stoidhle TR3+ an comas, m.e. coisich + saigheadan. Bidh na putanan sònraichte fhathast ag obair.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"Iuchraichean luatha armachd\",\n            \"description\": \"Smachd air mar a dh'èireas na h-iuchraichean uidheamachaidh armachd luatha.\\n\\n- Tarraing a-mhàin: bheir iuchair teth air Lara an arm ainmichte a tharraing.\\n- Tarraing/falach: mar roimhe, ach bheir seo cuideachd air Lara an arm a chur air falach ma tha i ga ghiùlan.\",\n        },\n        \"language\": {\n            \"title\": \"Cànan\",\n            \"description\": \"Atharraicheas cànan an eadar-aghaidh.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Criathradh anisotropaigeach\",\n            \"description\": \"Leasaichidh seo criathradh inneach aig astaran fada.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Modh co-mheas sgrion\",\n            \"description\": \"Èigneachadh cuid de cho-mheudan le bàr-dubh (bogsa-litreach).\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"Iomallan\",\n            \"description\": \"Cuiridh seo iomallan dubha timcheall air uinneag a' gheama.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Criathradh trapeasoid\",\n            \"description\": \"Cuiridh seo ceartachadh air trapeasoidean (ceàrnan neo-chothromach) airson reandaradh nas fhearr.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"V-Sionc\",\n            \"description\": \"Tionndaidheas sioncronachadh-inghearach air neo dheth.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"FPS\",\n            \"description\": \"Suidhichidh an àireamh de frèamaichean a chluicheas gach diog 's a' gheama.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Iomsgaradh solais\",\n            \"description\": \"Cuiridh seo atharrachadh air iomsgaradh solais fiùghantach, airson m.e. lasraichean neo losgadh armachd.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Fòrmat glacadh-sgrìn\",\n            \"description\": \"Fòrmat fhaidhle airson glacadh-sgrìn.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"Glasadh sprìde\",\n            \"description\": \"Bheir seo buaidh air na h-aisealean a tha glaiste nuair a thathar a' coimhead air sprìdean air an sgrion.\\n\\n- Gin sam bith: seall air na sprìdean gu àbhaisteach.\\n- Rothladh: glasaich an aiseal rothlaidh – gu feumail a-mhàin ann am modh dealbh.\\n- Rothladh is claonadh: bidh na sprìdean nan seasamh dìreach agus cha bhith iad nan laighe air an talamh nuair a thathar gan choimhead bho shuas.\\n- Sealladh: glasaich na h-aisealean rothlaidh agus claonadh, 's a bharrachd air sin, rothlaich na sprìdean beagan a dh'ionnsaigh meadhan an sgrion.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Criathradh inneach\",\n            \"description\": \"Cuiridh seo atharrachadh eadar inneach rèidh agus piogsaileach.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"Criathradh EA\",\n            \"description\": \"Tionndaidh eadar inneach an eadar-aghaidh rèidh neo piogsaileach.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"Factar sgèileachaidh\",\n            \"description\": \"Àrdaichidh seo an geama le factar stèidhichte, aig an aon àm a' cumail coltas piogsaileach.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"Criathradh sgèileachaidh\",\n            \"description\": \"Tionndaidh eadar coltas rèidh neo piogsaileach airson na sgrìn air fad.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Dath bàr-èadhair\",\n            \"description\": \"Dath a' bhàr-èadhair.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Dath bàr-èadhair\",\n            \"description\": \"Dath a' bhàr-èadhair.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Suidheachadh bàr-èadhair\",\n            \"description\": \"Far a nochdas am bàr-èadhair air an sgrion.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"\\\\{review}Àite cunntair ammo\",\n            \"description\": \"\\\\{review}Àite far am bi an cunntair ammo air a thaisbeanadh.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"Coltas nam bàraichean\",\n            \"description\": \"Atharraichidh seo coltas nam bàraichean EA.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Meud bàr\",\n            \"description\": \"Atharraicheas meud nam bàraichean.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"Bàraichean deàrrsail\",\n            \"description\": \"Cuiridh seo an àite bàraichean slàinte agus ocsaidean Lara deàrrsail nuair a tha i a' ruith ìosal air na goireasan seo.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Bàraichean rèidh\",\n            \"description\": \"Cuiridh seo dath le gluasad rèidh air na bàraichean slàinte, èadhair is nàmhaid.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Sgrolaich timcheall\",\n            \"description\": \"Leigidh seòladh stiùiridh ann an clàran-bìdh lùbadh timcheall.\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Dath bàr-nàmhaid\",\n            \"description\": \"Dath bàr-shlàinte an nàmhaid.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Dath bàr nan càirdean\",\n            \"description\": \"Dath bàr-shlàinte nan càirdean. Seallaidh seo ann an àite bàraichean-shlàinte nan naimhdean.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Dath bàr nan càirdean\",\n            \"description\": \"Dath bàr-shlàinte nan càirdean. Seallaidh seo ann an àite bàraichean-shlàinte nan naimhdean.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Dath bàr-nàmhaid\",\n            \"description\": \"Dath bàr-shlàinte an nàmhaid.\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Suidheachadh bàr-nàmhaid\",\n            \"description\": \"Far a nochdas bàr-shlàinte an nàmhaid air an sgrion.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Modh bàr-nàmhaid\",\n            \"description\": \"Cuiridh seo bàr-shlàinte an nàmhaid gnìomhaich an comas.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"Dath bàr-fuachd\",\n            \"description\": \"Dath a' bhàir ann an uisge fuair.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"Dath bàr-fuachd\",\n            \"description\": \"Dath a' bhàir ann an uisge fuair.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"Suidheachadh bàr-fuachd\",\n            \"description\": \"Far a nochdas am bàr-fuachd air an sgrion.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Dath bàr-shlàinte\",\n            \"description\": \"Dath a' bhàr-shlàinte.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Dath bàr-shlàinte\",\n            \"description\": \"Dath a' bhàr-shlàinte.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Suidheachadh bàr-shlàinte\",\n            \"description\": \"Far a nochdas am bàr-shlàinte air an sgrion.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"Dath bàr-shlàinte puinnseanta\",\n            \"description\": \"Dath a' bhàr-shlàinte nuair a tha Lara air a puinnseanachadh.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"Dath bàr-shlàinte puinnseanta\",\n            \"description\": \"Dath a' bhàr-shlàinte nuair a tha Lara air a puinnseanachadh.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"Dealbh-cùil sa chlàr-seilbhe\",\n            \"description\": \"Atharraichidh an doigh anns a bhitheas an dealbh-cùil air a shealltainn sa chlàr-seilbhe.\\n\\n- Dorcha: TR1 (PC).\\n- Fìor dhorcha: TR1 (PS1).\\n- Statach: TR2 (PC).\\n- Crathadh: TR2 (PS1).\\n- Aon-dhathach: TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"Buaidhean falamh clàr-innealan\",\n            \"description\": \"A' rèiteachadh gu mionaideach na buaidhean falamh airson an cleachdadh neo an cur dheth anns a' chruinneachadh clàr-innealan sa gheama. Feumaidh an roghainn Buaidhean Falamh a bhith air a ghnìomhachadh gus seo obrachadh.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Stoidhle clàir\",\n            \"description\": \"Cuiridh seo stoidhle a' chlàir an comas:\\n\\n- PC: stoidhle eadar-aghaidh PC.\\n- PS1: stoidhle eadar-aghaidh a' PhS1.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"Dealbh-cùil sgrion-stad\",\n            \"description\": \"Atharraichidh an doigh anns a bhitheas an dealbh-cùil air a shealltainn san sgrion-stad.\\n\\n- Dorcha: TR1 (PC).\\n- Fìor dhorcha: TR1 (PS1).\\n- Statach: TR2 (PC).\\n- Crathadh: TR2 (PS1).\\n- Aon-dhathach: TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"Buaidhean falamh stad\",\n            \"description\": \"A' rèiteachadh gu mionaideach na buaidhean falamh airson an cleachdadh neo an cur dheth air scrion stad. Feumaidh an roghainn Buaidhean Falamh a bhith air a ghnìomhachadh airson seo obrachadh.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"Meud togailtean\",\n            \"description\": \"Atharraicheas meud nan togailtean beòthail san EA nuair a thogas Lara an àird iad.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"Cleachd bàraichean\",\n            \"description\": \"Cuiridh seo dheth gach bàr sa gheama, a' falach fiosrachadh mu shlàinte Lara agus goireasan eile (airson ruith dhùbhlanach).\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Taisbeanadh thogailtean\",\n            \"description\": \"Seallaidh seo togailtean san oisean deas-ìosal nuair a thogas Lara rudeigin.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"\\\\{review}Teacsa teacsa dreach\",\n            \"description\": \"\\\\{review}Seallas an sreang teacsa dreach TRX ann an fàinne clàr-stòrais na tiotal.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"Dath bàr ruith luath\",\n            \"description\": \"Dath a' bhàr ruith luath.\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"Dath bàr ruith luath\",\n            \"description\": \"Dath a' bhàr ruith luath.\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"Suidheachadh bàr ruith luath\",\n            \"description\": \"Far a nochdas am bàr ruith luath air an sgrion.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"\\\\{review}Bualadh ammo / cleachdadh\",\n            \"description\": \"\\\\{review}Seall an loidhne ammo anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"\\\\{review}Criostalan\",\n            \"description\": \"\\\\{review}Tha e a’ sealltainn an rìgh criostalan ann an staitistig an ìre.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"\\\\{review}Bàs\",\n            \"description\": \"\\\\{review}Seall bàsan Lara anns na staitistig compais agus anns na staitistig ìre. Tha cunntas bàis air ùrachadh anns an sàbhalaidh a tha air a luchdachadh an-dràsta cho luath ‘s a bhàsaich Lara.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"\\\\{review}Astar air a siubhal\",\n            \"description\": \"\\\\{review}Seall an loidhne astar air a siubhal anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"\\\\{review}Murtan\",\n            \"description\": \"\\\\{review}Seall an loidhne murtan anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"\\\\{review}Àireamh ìre\",\n            \"description\": \"\\\\{review}Seall àireamh an ìre làithreach aig mullach nan staitistig ìre.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"\\\\{review}Pacaidean slàinte air an cleachdadh\",\n            \"description\": \"\\\\{review}Seall an loidhne pacaidean slàinte air an cleachdadh anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"\\\\{review}Togalaichean\",\n            \"description\": \"\\\\{review}Seall an loidhne togalaichean anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"\\\\{review}Dìomhair air an lorg\",\n            \"description\": \"\\\\{review}Seall an loidhne dìomhair air an lorg anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"\\\\{review}Ùine a ghabh\",\n            \"description\": \"\\\\{review}Seall an loidhne ùine a ghabh anns na staitistig ìre.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"\\\\{review}Seall na suimean\",\n            \"description\": \"\\\\{review}Sealltainn iomlanan ri taobh staitistig nuair a tha sin iomchaidh. Chan eil dìomhaireachdan air an toirt buaidh leis an suidheachadh seo.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"\\\\{review}Stoidhle staitistig\",\n            \"description\": \"\\\\{review}Smachd air mar a thèid an còmhradh staitistig a thaisbeanadh.\\n\\n- Furasta: seall an cruth nas sìmplidh gun fhrèam.\\n- Leacanach: seall an cruth le bogsaichean.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"Dealbh-cùil staitistig\",\n            \"description\": \"Atharraichidh an doigh anns a bhitheas an dealbh-cùil air a shealltainn san sgrion-staitistig.\\n\\n- Dorcha: TR1 (PC).\\n- Fìor dhorcha: TR1 (PS1).\\n- Statach: TR2 (PC).\\n- Crathadh: TR2 (PS1).\\n- Aon-dhathach: TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"Buaidhean falamh staitistig\",\n            \"description\": \"A' rèiteachadh gu mionaideach na buaidhean falamh airson an cleachdadh neo an cur dheth air scrion staitistig deireadh ìre. Feumaidh an roghainn Buaidhean Falamh a bhith air a thaghadh gus seo obrachadh.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Meud teacsa\",\n            \"description\": \"Atharraicheas meud an teacsa.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"\\\\{review}Buaidhean fuil\",\n            \"description\": \"\\\\{review}Smachd air dathan spàirn fola.\\n\\n- Ciorramh: chan eilear a’ sealltainn spàirn fola sam bith.\\n- Pinc: an àbhaist ann an sgaoilidhean PC Gearmailteach de TR3.\\n- Dearg: an àbhaist anns a h-uile sgaoileadh reic eile.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Modh camara\",\n            \"description\": \"Atharraichidh seo an doigh anns a bhios a' chamara ag obair, m.e. aig àm ghnìomhan shonraichte, mar a bhith a' cleachdadh iuchraichean.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"Togail 3D\",\n            \"description\": \"Cuiridh seo modailean 3D an àite spraidean airson nithean togail.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Feaman-fuilt Lara\",\n            \"description\": \"Cuiridh seo feaman-fuilt Lara an comas.\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Gaoth\",\n            \"description\": \"Cuiridh seo buaidh na gaoithe air feaman-fuilt Lara ann an seòmraichean freagarrach.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Dorchachadh aig fàgail\",\n            \"description\": \"Cuiridh seo dorchachadh an comas nuair a dh'fhàgas tu an geama gu deasg.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Buaidhean dorchachaidh\",\n            \"description\": \"Cuiridh seo dorchachadh eadar gluasadan air adhart — m.e. eadar grafaigean creideis neo clàr-seilbhe.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"Lasadh teine\",\n            \"description\": \"Cuiridh seo comas air solas fiùghantach a bhith air a chruthachadh ri taobh lasraichean gnìomhach.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"Clachan-coise\",\n            \"description\": \"Leigeas le clachan-coise Lara a nochdadh air uachdaran sònraichte ann an ìrean taicichte.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"Camarathan sleamhainn\",\n            \"description\": \"Cuir an comas gluasad sleamhainn air camarathan stèidhichte a bhios a' coimhead air Lara le lùb astair rèidh a chleachdadh. Ma tha e à comas, atharraichidh na camarathan seo an sealladh sa bhad.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Solais armachd\",\n            \"description\": \"Cuiridh seo solais beòthail ri losgadh agus spreadhadh.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"Dath criostail PS1\",\n            \"description\": \"Bidh na criostalan sàbhalaidh purpaidh mar a tha iad anns a' PhS1.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Meòrachadh\",\n            \"description\": \"Cuiridh seo meòrachadh air nithean sònraichte an comas.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"Dathadh freagairteach lìonra\",\n            \"description\": \"Leigidh seo le lìonra Lara fa leth a bhith air an sealltainn le dath uisge ma tha iad fhèin fo uisge (stoidhle TR3). Air neo, ma tha Lara san uisge, thèid gach lìonra aice a shealltainn leis an dath (stoidhle TR1/2).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Lasadh gunna-sgaoil\",\n            \"description\": \"Cuiridh seo lasair an sàs nuair a losgais an gunna-sgaoil, coltach ri armachd eile.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Bogsa-nan-Speuran\",\n            \"description\": \"Cuiridh seo bogsa-nan-speuran an comas ann an ìrean far a bheil e freagarrach.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"Aimsir\",\n            \"description\": \"Cuir air neo dheth an aimsir anns na h-ìrean anns a bheil i an àite.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Càraich beòthachaidhean spraidean\",\n            \"description\": \"Ceartachaidh nan sprìodan lusan fo uisge tùsail gus an gluasad gu ceart ann an raointean uisge.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Càraich cuairteachadh nithean\",\n            \"description\": \"Càradh bugan far a bheil cuid de thogailtean air an tionndadh gu ceàrr le togailtean 3D air a chomasachadh.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Càraich duilgheadasan inneach\",\n            \"description\": \"Càradh bugan tùsail le inneach neo modalan a tha ceàrr neo a dhìth.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"Dath ceò\",\n            \"description\": \"Dath a' cheòth.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Deireadh ceò\",\n            \"description\": \"Suidhichidh seo an astar (ann an leacan) bhon chamara far a dhùnas ceò air a h-uile càil.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Tòiseachadh ceò\",\n            \"description\": \"Suidhichidh seo an astar (ann an leacan) bhon chamara far a thoisicheas ceò.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"Follaiseachd a' cheòth\",\n            \"description\": \"Leigidh seo le sealladh geoimeatraidh a tha fada air falbh bhon a' chamara a bhith follaiseachd gu h-iomlan.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"Raonadh seallaidh\",\n            \"description\": \"Ceàrn seallaidh ann an ceumannan. Bidh luachan nas motha a' leudachadh raon seallaidh, agus bidh luachan nas lugha ga chuingealachadh.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Soilleireachd\",\n            \"description\": \"Cuiridh seo atharrachadh air soilleireachd a' gheama.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"Gamma\",\n            \"description\": \"Cuir buaidh air dorchadas a' gheama. Tha luachan nas àirde a' ciallachadh solas nas soilleire. Tha luach 2.5 a' ciallachadh dathan àbhaisteach.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"Aodach Lara\",\n            \"description\": \"Atharraicheas seo coltas Lara. Ma thèid Bunaiteach a thaghadh, thèid atharrachaidhean aodaich àbhaisteach eadar ìrean a chumail; airson dad sam bith eile, mairidh an t-aodach a chaidh a thaghadh gus an tèid atharrachadh le làimh.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"Cruth nan faileasan\",\n            \"description\": \"Tagh mar a thèid faileasan nithean air an sealltainn.\\n\\n- Ochd-cheàrnach: faileas seann TR1 agus TR2\\n- Cearcall: faileas cruinn\\n- Spraide: faileas stèidhichte air teacsa TR3\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"Speuclairean-grèine Lara\",\n            \"description\": \"Atharraichidh seo stoidhle speuclairean-grèine Lara. Nòta: bidh na lionsan meòrachail ma tha an roghainn iomchaidh air a chur an comas.\\n\\n- Dheth: Cha bhi speuclairean-grèine air Lara.\\n- Neo-shoilleir: Bidh lionsan neo-shoilleir air speuclairean-grèine Lara.\\n- Follaiseach: Bidh lionsan leth-fhoilleiseach air speuclairean-grèine Lara.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"Soilleireachd EA\",\n            \"description\": \"Cuiridh seo atharrachadh air soilleireachd na h-eadar-aghaidh.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Dath an uisge\",\n            \"description\": \"Dath an uisge.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"Inneal-rabhaidh\",\n        },\n        \"alligator\": {\n            \"name\": \"Ailigeutair\",\n        },\n        \"alphabet\": {\n            \"name\": \"Clò àbhaisteach\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"Clò beag\",\n        },\n        \"amber_light\": {\n            \"name\": \"Solas Òmar\",\n        },\n        \"animating_1\": {\n            \"name\": \"Beòthaladh 1\",\n        },\n        \"animating_10\": {\n            \"name\": \"Beòthaladh 10\",\n        },\n        \"animating_2\": {\n            \"name\": \"Beòthaladh 2\",\n        },\n        \"animating_3\": {\n            \"name\": \"Beòthaladh 3\",\n        },\n        \"animating_4\": {\n            \"name\": \"Beòthaladh 4\",\n        },\n        \"animating_5\": {\n            \"name\": \"Beòthaladh 5\",\n        },\n        \"animating_6\": {\n            \"name\": \"Beòthaladh 6\",\n        },\n        \"animating_7\": {\n            \"name\": \"Beòthaladh 7\",\n        },\n        \"animating_8\": {\n            \"name\": \"Beòthaladh 8\",\n        },\n        \"animating_9\": {\n            \"name\": \"Beòthaladh 9\",\n        },\n        \"ape\": {\n            \"name\": \"Apa\",\n        },\n        \"area_51_rocket\": {\n            \"name\": \"\\\\{review}Ròcaid Sgìre 51\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"\\\\{review}Spreadh Ròcaid Sgìre 51\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"\\\\{review}Taic Ròcaid Sgìre 51\",\n        },\n        \"assault_digits\": {\n            \"name\": \"Àireamhan an t-Seòmair-eacarsaich\",\n        },\n        \"assault_target\": {\n            \"name\": \"Targaid Ionnsaigh\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"Atlantach Talmhainn\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"Atlantach le Gunna\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"Atlantach Sgiathach\",\n        },\n        \"autos\": {\n            \"name\": \"Piostalan Fèin-obrachail\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"Lòin Gunna Fèin-obrachail\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Lara Bèicein\",\n        },\n        \"baldy\": {\n            \"name\": \"Baldaidh\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"Saighdear-duais 1\",\n                \"Ruspal le Masg 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"Saighdear-duais 2\",\n                \"Ruspal le Masg 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"Saighdear-duais 3\",\n                \"Ruspal le Masg 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"Barracuda\",\n        },\n        \"bartoli\": {\n            \"name\": \"Marco Bartoli\",\n        },\n        \"bat\": {\n            \"name\": \"Ialtag\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"Sgaoiladair Bàta\",\n        },\n        \"beacon_light\": {\n            \"name\": \"Solas Beura\",\n        },\n        \"bear\": {\n            \"name\": \"Mathan\",\n        },\n        \"bell\": {\n            \"name\": \"Clag\",\n        },\n        \"big_bowl\": {\n            \"name\": \"Bobhla Teine-teinteach\",\n        },\n        \"big_eel\": {\n            \"name\": \"Easgann Mhòr\",\n        },\n        \"big_pod\": {\n            \"name\": \"Spàlag Mhòr\",\n        },\n        \"big_spider\": {\n            \"name\": \"Damhan-allaidh Fuamhaireil\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"Eun-uilebheisteach\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"Sileadair Uisge\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"Eòin a' Seinn\",\n        },\n        \"blade\": {\n            \"name\": \"Iarann air Balla\",\n        },\n        \"blood\": {\n            \"name\": \"\\\\{review}Fuil\",\n        },\n        \"blood_pink\": {\n            \"name\": \"\\\\{review}Fuil (air a sgaradh)\",\n        },\n        \"blue_light\": {\n            \"name\": \"Solas Gorm\",\n        },\n        \"boat\": {\n            \"name\": \"Bàta\",\n        },\n        \"boat_bits\": {\n            \"name\": \"Pìosan a' Bhàta\",\n        },\n        \"body_part\": {\n            \"name\": \"Pàirt Corp\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"Drochaid Rèidh\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"Drochaid Claonadh 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"Drochaid Claonadh 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"Builgean 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Builgean 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"Sgaoiladair Builgean\",\n        },\n        \"camera_target\": {\n            \"name\": \"Targaid Camara\",\n        },\n        \"carcass\": {\n            \"name\": \"Closach\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"Mullach-gheugan\",\n        },\n        \"centaur\": {\n            \"name\": \"Ceud-damh\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Ìomhaigh\",\n        },\n        \"civilian\": {\n            \"name\": \"Fear-sìobhalta\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"Mùtant Spòganach\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"Gleoc Bartoli\",\n        },\n        \"cog_1\": {\n            \"name\": \"Inneal Gèaraichean 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Inneal Gèaraichean 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Inneal Gèaraichean 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"Crioch-shabaid\",\n        },\n        \"compass\": {\n            \"name\": \"Staitistig\",\n        },\n        \"compy\": {\n            \"name\": \"Compsognathus\",\n        },\n        \"controls\": {\n            \"name\": \"Smachdan\",\n        },\n        \"copter\": {\n            \"name\": \"Heileacoptair\",\n        },\n        \"cowboy\": {\n            \"name\": \"Gille-cruidh\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"Mùtant Snàigeach\",\n        },\n        \"crocodile\": {\n            \"name\": [\n                \"Crocadal\",\n                \"Crogall\",\n            ]\n        },\n        \"crow\": {\n            \"name\": \"Feannag\",\n        },\n        \"cult_1\": {\n            \"name\": \"Ruspal le Masg 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"Ruspal le Masg 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"Ruspal le Masg 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"Fear-sgian\",\n        },\n        \"cult_3\": {\n            \"name\": \"Ruspal le Gunna-sgaoil\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"Beòthalachd Gunna-sgaoil Fhrais\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Claidheamh Damocles\",\n        },\n        \"dart\": {\n            \"name\": \"Dàrt\",\n        },\n        \"dart_effect\": {\n            \"name\": \"Buaidh Dàrt\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Sgaoiladair Dàrt\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"Desert Eagle\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"Lòin an Desert Eagle\",\n        },\n        \"detonator_box\": {\n            \"name\": \"Spreadhaichear\",\n        },\n        \"ding_dong\": {\n            \"name\": \"Clag an Dorais\",\n        },\n        \"dino_mutant\": {\n            \"name\": \"Dìneasair-mùthaidh\",\n        },\n        \"disc\": {\n            \"name\": \"Sgian-cruinnte\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"Sgaoiladair Sgian-cruinnte\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"Beòthaladh 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"Beòthaladh 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"Beòthaladh 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"Beòthaladh 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"Beòthaladh 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"Beòthaladh 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"Beòthaladh 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"Beòthaladh 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"Beòthaladh 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"Beòthaladh 9\",\n        },\n        \"diver\": {\n            \"name\": \"Sgùba-dhaibhear\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"Cù\",\n                \"Doberman\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Doras 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Doras 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Doras 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Doras 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Doras 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Doras 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Doras 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Doras 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"Cùl an Dràgon\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"Neach-àite\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"Cnàmhan Aghaidh an Dràgon\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"Cnàmhan Cùl an Dràgon\",\n        },\n        \"dragon_front\": {\n            \"name\": \"Aghaidh an Dràgon\",\n        },\n        \"drawbridge\": {\n            \"name\": \"Drochaid-tarraing\",\n        },\n        \"dust\": {\n            \"name\": \"Duslach\",\n        },\n        \"dying_monk\": {\n            \"name\": \"Manach a' Bàsachadh\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"Mùtant a' Bàsachadh\",\n        },\n        \"eagle\": {\n            \"name\": \"Iolaire\",\n        },\n        \"earthquake\": {\n            \"name\": \"Crith-thalmhainn\",\n        },\n        \"eel\": {\n            \"name\": \"Easgann\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"Inneal-glanaidh\",\n        },\n        \"electric_fence\": {\n            \"name\": \"Feansa Dealain\",\n        },\n        \"electrical_light\": {\n            \"name\": \"Solais Dealain\",\n        },\n        \"ember\": {\n            \"name\": \"Èibhleag\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"Sgaoiladair Èibhleag\",\n        },\n        \"explosion_1\": {\n            \"name\": \"Spreadhadh 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Spreadhadh 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": [\n                \"Bloc Tuiteamach 1\",\n                \"Làr Bristeach 1\",\n                \"Leacan Bristeach 1\",\n            ]\n        },\n        \"falling_block_2\": {\n            \"name\": [\n                \"Bloc Tuiteamach 2\",\n                \"Làr Bristeach 2\",\n                \"Leacan Bristeach 2\",\n            ]\n        },\n        \"falling_block_3\": {\n            \"name\": [\n                \"Bloc Tuiteamach 3\",\n                \"Làr Bristeach 3\",\n                \"Leacan Bristeach 3\",\n                \"Bùird Cugalach\",\n            ]\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"Mullach Tuiteamach 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Mullach Tuiteamach 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"Ceann Teine\",\n        },\n        \"fish_mutant\": {\n            \"name\": \"Iasg-mùthaidh\",\n        },\n        \"flame\": {\n            \"name\": [\n                \"Lasair\",\n                \"Teine\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"Sgaoiladair Lasair\",\n                \"Sgaoiladair Teine\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": \"Sgaoileadair Teine (Mòr)\",\n        },\n        \"flame_emitter_jet\": {\n            \"name\": \"Sgaoileadair Teine (Jet)\",\n        },\n        \"flame_emitter_side\": {\n            \"name\": \"Sgaoileadair Teine (Gu Taobh)\",\n        },\n        \"flame_emitter_small\": {\n            \"name\": \"Sgaoileadair Teine (Beag)\",\n        },\n        \"flare\": {\n            \"name\": \"Lòchran\",\n        },\n        \"flare_fire\": {\n            \"name\": \"Teinntreach Lòchran\",\n        },\n        \"flares_box\": {\n            \"name\": \"Bogsa Lòchrain\",\n        },\n        \"flickering_light\": {\n            \"name\": \"Solas Boillsgeach\",\n        },\n        \"fuse_box\": {\n            \"name\": \"\\\\{review}Bogsa Fuse\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"Cearcall Glas\",\n        },\n        \"gamma\": {\n            \"name\": \"Gamma\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"Sgaoileadair Ghasa (Uaine)\",\n        },\n        \"general\": {\n            \"name\": \"Bàt-aigeil Beag\",\n        },\n        \"globe\": {\n            \"name\": \"Cruinne\",\n        },\n        \"glow\": {\n            \"name\": \"Gàir\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"Gàir Mapa\",\n        },\n        \"gondola\": {\n            \"name\": \"Carbad-crochte\",\n        },\n        \"gong\": {\n            \"name\": \"Gong\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"Bata a' Ghong\",\n        },\n        \"graphics\": {\n            \"name\": \"Grafaigean\",\n        },\n        \"green_light\": {\n            \"name\": \"Solas Uaine\",\n        },\n        \"grenade\": {\n            \"name\": \"Grenèad\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"Lannsair Grenèad\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"Grenèadan\",\n        },\n        \"gun_flash\": {\n            \"name\": \"Lasadh Ghunna\",\n        },\n        \"gun_shell\": {\n            \"name\": \"Sgèith Gheir\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"Harpùn\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"Harpùn\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"Harpùnaichean\",\n        },\n        \"hook\": {\n            \"name\": \"Dubhan\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"Leaghan Teth\",\n        },\n        \"huskie\": {\n            \"name\": \"\\\\{review}Cù\",\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"Mùtant Hibrid\",\n        },\n        \"icicle\": {\n            \"name\": \"Bioranan-deighe\",\n        },\n        \"inv_background\": {\n            \"name\": \"Inneach a' Chlàr-seilbhe\",\n        },\n        \"jelly\": {\n            \"name\": \"Muir-tèachd\",\n        },\n        \"kayak\": {\n            \"name\": \"Càbaig\",\n        },\n        \"key_1\": {\n            \"name\": \"Iuchair 1\",\n        },\n        \"key_2\": {\n            \"name\": \"Iuchair 2\",\n        },\n        \"key_3\": {\n            \"name\": \"Iuchair 3\",\n        },\n        \"key_4\": {\n            \"name\": \"Iuchair 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"Toll Iuchrach 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"Toll Iuchrach 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"Toll Iuchrach 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"Toll Iuchrach 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"Cur às do gach rud\",\n        },\n        \"killer_statue\": {\n            \"name\": \"Ìomhaigh le Claidheamh\",\n        },\n        \"lara\": {\n            \"name\": \"Lara\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"Rabhadair-mhèirleach\",\n        },\n        \"lara_autos\": {\n            \"name\": \"Beòthalachd Piostalan Fèin-obrachail\",\n        },\n        \"lara_boat\": {\n            \"name\": \"Beòthalachd Bàta\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"Beòthalachd an Desert Eagle\",\n        },\n        \"lara_extra\": {\n            \"name\": \"Beòthalachd Eile Lara\",\n        },\n        \"lara_flare\": {\n            \"name\": \"Beòthalachd Lòchran\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"Beòthalachd Lannsair Grenèad\",\n        },\n        \"lara_hair\": {\n            \"name\": \"Feaman-fuilt Lara\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"Beòthalachd Harpùn\",\n        },\n        \"lara_m16\": {\n            \"name\": \"Beòthalachd M16\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"Beòthalachd Magnuman\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"Beòthalachd MP5\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"Beòthalachd Piostalan\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"Beòthalachd Lannsair Rocaid\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"Beòthalachd Gunna-sgaoil\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"Beòthalachd Cairt-shneachda\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"Beòthalachd Uzis\",\n        },\n        \"larson\": {\n            \"name\": \"Larson\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Cliathaich Teine-teinteach\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Bàr Luaidhe\",\n        },\n        \"lift\": {\n            \"name\": \"Lioft\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Sgaoiladair Dealanaich\",\n        },\n        \"lion\": {\n            \"name\": \"Leòmhann\",\n        },\n        \"lioness\": {\n            \"name\": [\n                \"Leòmhann-Boireann\",\n                \"Leòmhann\",\n            ]\n        },\n        \"lizard\": {\n            \"name\": \"Laghairt\",\n        },\n        \"m16\": {\n            \"name\": \"M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"Lòin M16\",\n        },\n        \"m16_flash\": {\n            \"name\": \"Lasadh M16\",\n        },\n        \"magnums\": {\n            \"name\": \"Magnuman\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Lòin Magnum\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"Suaip Lìonra 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"Suaip Lìonra 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"Suaip Lìonra 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Làmh Mìdas\",\n        },\n        \"mine\": {\n            \"name\": \"Mèinn Uisgeach\",\n        },\n        \"mine_cart\": {\n            \"name\": \"Cairt-mhèinne\",\n        },\n        \"mini_copter\": {\n            \"name\": \"Heileacoptair 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"Missil (Boma Atlanteach)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"Missil (Slinneag Atlanteach)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"Missil (Teine)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"Missil (Harpùn)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"Missil (Sgian)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"Missil (Puinnsean)\",\n        },\n        \"monk_1\": {\n            \"name\": \"Manach 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"Manach 2\",\n        },\n        \"monkey\": {\n            \"name\": \"Muncaidh\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"Gunna Stèidhichte\",\n        },\n        \"mouse\": {\n            \"name\": \"Radan\",\n        },\n        \"movable_block_1\": {\n            \"name\": [\n                \"Bloc Putaidh 1\",\n                \"Bloc Gluasadach 1\",\n            ]\n        },\n        \"movable_block_2\": {\n            \"name\": [\n                \"Bloc Putaidh 2\",\n                \"Bloc Gluasadach 2\",\n            ]\n        },\n        \"movable_block_3\": {\n            \"name\": [\n                \"Bloc Putaidh 3\",\n                \"Bloc Gluasadach 3\",\n            ]\n        },\n        \"movable_block_4\": {\n            \"name\": [\n                \"Bloc Putaidh 4\",\n                \"Bloc Gluasadach 4\",\n            ]\n        },\n        \"moving_bar\": {\n            \"name\": \"Bàr Gluasadach\",\n        },\n        \"mp5\": {\n            \"name\": \"MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"Lòin MP5\",\n        },\n        \"mp_1\": {\n            \"name\": \"MP 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"MP 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Spaoileadan\",\n        },\n        \"natla\": {\n            \"name\": \"Natla\",\n        },\n        \"natla_gun\": {\n            \"name\": \"Gunna Natla\",\n        },\n        \"on_off_light\": {\n            \"name\": \"Solas Air/Dheth\",\n        },\n        \"orca\": {\n            \"name\": \"\\\\{review}Orca\",\n        },\n        \"passport\": {\n            \"name\": \"Geama\",\n        },\n        \"patrol_dog\": {\n            \"name\": \"\\\\{review}Cù\",\n        },\n        \"pda\": {\n            \"name\": \"Cluiche\",\n        },\n        \"pendulum_1\": {\n            \"name\": \"Poca-gainmhich\",\n        },\n        \"pendulum_2\": {\n            \"name\": \"Bocsa\",\n        },\n        \"photo\": {\n            \"name\": \"Dachaigh Lara\",\n        },\n        \"pickup_1\": {\n            \"name\": \"Nì Togail 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"Nì Togail 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Cobhair Togail\",\n        },\n        \"pierre\": {\n            \"name\": \"Pierre\",\n        },\n        \"pirahnas\": {\n            \"name\": \"Pirahnan\",\n        },\n        \"pistols\": {\n            \"name\": \"Piostalan\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"Lòin Phiostail\",\n        },\n        \"player_1\": {\n            \"name\": \"Actair 1\",\n        },\n        \"player_10\": {\n            \"name\": \"Actair 10\",\n        },\n        \"player_2\": {\n            \"name\": \"Actair 2\",\n        },\n        \"player_3\": {\n            \"name\": \"Actair 3\",\n        },\n        \"player_4\": {\n            \"name\": \"Actair 4\",\n        },\n        \"player_5\": {\n            \"name\": \"Actair 5\",\n        },\n        \"player_6\": {\n            \"name\": \"Actair 6\",\n        },\n        \"player_7\": {\n            \"name\": \"Actair 7\",\n        },\n        \"player_8\": {\n            \"name\": \"Actair 8\",\n        },\n        \"player_9\": {\n            \"name\": \"Actair 9\",\n        },\n        \"pods\": {\n            \"name\": \"Spàlagan\",\n        },\n        \"poison_dart\": {\n            \"name\": \"Dàrt Phuinnseanta\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"Sgaoiladair Dàrt Phuinnseanta\",\n        },\n        \"portacabin\": {\n            \"name\": \"Caban So-ghiùlain\",\n        },\n        \"power_saw\": {\n            \"name\": \"Sàbh-dealain\",\n        },\n        \"prisoner\": {\n            \"name\": \"Prìosanach\",\n        },\n        \"propeller_1\": {\n            \"name\": \"Proipeilear-plèana\",\n        },\n        \"propeller_2\": {\n            \"name\": \"Proipeilear fo Uisge\",\n        },\n        \"propeller_3\": {\n            \"name\": \"Gaothran\",\n        },\n        \"pulse_light\": {\n            \"name\": \"Solas Cuisle\",\n        },\n        \"puma\": {\n            \"name\": \"Pùma\",\n        },\n        \"punk_1\": {\n            \"name\": \"Punc 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"Punc 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"Nì Tòimhseachain 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"Nì Tòimhseachain 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"Nì Tòimhseachain 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"Nì Tòimhseachain 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"Toll Tòimhseachain 1 (Deiseil)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"Toll Tòimhseachain 2 (Deiseil)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"Toll Tòimhseachain 3 (Deiseil)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"Toll Tòimhseachain 4 (Deiseil)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"Toll Tòimhseachain 1 (Falamh)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"Toll Tòimhseachain 2 (Falamh)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"Toll Tòimhseachain 3 (Falamh)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"Toll Tòimhseachain 4 (Falamh)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"Quad-baidhsagal\",\n        },\n        \"quest_1\": {\n            \"name\": \"Nì Tòir 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"Nì Tòir 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"Nì Tòir 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"Nì Tòir 4\",\n        },\n        \"raptor\": {\n            \"name\": [\n                \"Raptor\",\n                \"Dìneasair-beag\",\n            ]\n        },\n        \"raptor_emitter\": {\n            \"name\": \"Sgaoiladair Raptor\",\n        },\n        \"rat\": {\n            \"name\": [\n                \"Radan\",\n                \"Radan-tìre\",\n            ]\n        },\n        \"red_light\": {\n            \"name\": \"Solas Dearg\",\n        },\n        \"rib\": {\n            \"name\": \"RIB\",\n        },\n        \"ricochet\": {\n            \"name\": \"Leabaidh-loisgte\",\n        },\n        \"rocket\": {\n            \"name\": \"Rocaid\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"Lannsair Rocaid\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"Rocaidean\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": [\n                \"Ulbhag 1\",\n                \"Ball-rollaidh 1\",\n            ]\n        },\n        \"rolling_ball_2\": {\n            \"name\": [\n                \"Ulbhag 2\",\n                \"Ball-rollaidh 2\",\n            ]\n        },\n        \"rolling_ball_3\": {\n            \"name\": [\n                \"Ulbhag 3\",\n                \"Ball-rollaidh 3\",\n            ]\n        },\n        \"rolling_ball_4\": {\n            \"name\": [\n                \"Ulbhag 4\",\n                \"Ball-rollaidh 4\",\n            ]\n        },\n        \"rotating_laser\": {\n            \"name\": \"\\\\{review}Laser rothlach\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"Neach-obrach RX 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"Neach-obrach RX 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": \"\\\\{review}Neach-obrach RX 3\",\n        },\n        \"save_crystal\": {\n            \"name\": \"Criostal Geama Sàbhalaidh\",\n        },\n        \"scion\": {\n            \"name\": \"Scion\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Greimiche Scion\",\n        },\n        \"secret_1\": {\n            \"name\": \"Dìomhair 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"Dìomhair 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"Dìomhair 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"Geàrd Tèarainteachd\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"\\\\{review}Laser Tèarainteachd (Rabhadh)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"\\\\{review}Laser Tèarainteachd (Marbhtach)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"\\\\{review}Laser Tèarainteachd (Mharbhadh)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"\\\\{review}Gun Sentry Robot\",\n        },\n        \"shadow\": {\n            \"name\": \"Faileas\",\n        },\n        \"shark\": {\n            \"name\": \"Cearban\",\n        },\n        \"shiva\": {\n            \"name\": \"Sìabha\",\n        },\n        \"shotgun\": {\n            \"name\": \"Gunna-sgaoil\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"Sligean Gunna-sgaoil\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"Sgèith Gunna-sgaoil\",\n        },\n        \"skate_kid\": {\n            \"name\": \"Balach Speile-bhòrd\",\n        },\n        \"skateboard\": {\n            \"name\": \"Speile-bhòrd\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"Cairt-shneachda Dhubh\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"Draibhear Cairt-shneachda Dhubh\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"Cairt-shneachda Dhearg\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"Crios Cairt-shneachda\",\n        },\n        \"skybox\": {\n            \"name\": \"Bogsa-na-Speuran\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Colbh Sleamhnachaidh\",\n        },\n        \"smashable_1\": {\n            \"name\": \"Uinneag Bhristeach 1\",\n        },\n        \"smashable_2\": {\n            \"name\": \"Uinneag Bhristeach 2\",\n        },\n        \"smashable_3\": {\n            \"name\": \"Uinneag Bhristeach 3\",\n        },\n        \"smashable_4\": {\n            \"name\": \"Uinneag Bhristeach 4\",\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"Sgaoileadair Smùid (Dubh)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"Sgaoileadair Smùid (Geal)\",\n        },\n        \"snake\": {\n            \"name\": \"Nathair\",\n        },\n        \"snow_sprite\": {\n            \"name\": \"Rotal Cairt-shneachda\",\n        },\n        \"sophia\": {\n            \"name\": \"\\\\{review}Sophia\",\n        },\n        \"sound\": {\n            \"name\": \"Fuaim\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"Spreadhadh Dràgon 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"Spreadhadh Dràgon 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"Spreadhadh Dràgon 3\",\n        },\n        \"spider\": {\n            \"name\": \"Damhan-allaidh\",\n        },\n        \"spike_wall\": {\n            \"name\": \"Balla-gheugan\",\n        },\n        \"spikes\": {\n            \"name\": \"Geugan\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"Iarann Rothachadh\",\n        },\n        \"splash_1\": {\n            \"name\": \"Riplean Uisge 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Riplean Uisge 2\",\n        },\n        \"springboard\": {\n            \"name\": \"Bòrd-flosgaidh\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"Sgaoileadair Steimhe\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"Saighdear a' Chuain Shèimh a Deas\",\n        },\n        \"stopwatch\": {\n            \"name\": \"Staitistig\",\n        },\n        \"strobe_light\": {\n            \"name\": \"Solas Sròb\",\n        },\n        \"swat_1\": {\n            \"name\": \"SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"Tuagh\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"Suids Stopadh-adhair\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"Putan\",\n                \"Suids\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"Luaidhe\",\n                \"Suids\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"Suids Beag\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"Luaidhe fon-Uisge\",\n                \"Suids fon-Uisge\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": \"Suids Cuidhle\",\n        },\n        \"teeth_trap\": {\n            \"name\": [\n                \"Ribe Fiaclan\",\n                \"Doras 'Clang-Clang'\",\n            ]\n        },\n        \"text_box\": {\n            \"name\": \"Frèam Eadar-aghaidh\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Làimhseachadh Òrd Thor\",\n        },\n        \"thors_head\": {\n            \"name\": \"Òrd Thor\",\n        },\n        \"tiger\": {\n            \"name\": \"Tìgear\",\n        },\n        \"tony\": {\n            \"name\": \"Tònaidh\",\n        },\n        \"torso\": {\n            \"name\": [\n                \"Torsachan\",\n                \"Adhamh\",\n                \"Mùthaidh Mòr\",\n            ]\n        },\n        \"train\": {\n            \"name\": \"Trèan\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Làr-lùbte 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Làr-lùbte 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Làr-lùbte 3\",\n        },\n        \"trex\": {\n            \"name\": [\n                \"T-Rex\",\n                \"Dìneasair-mòr\",\n            ]\n        },\n        \"trex_alpha\": {\n            \"name\": \"T-Rex Priomh\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"Fear-treubh le Tuagh\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"Ceannard an Treubh\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"Fear-treubh le Dàrt\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"Iasg Tropaigeach\",\n        },\n        \"twinkle\": {\n            \"name\": \"Priobag\",\n        },\n        \"upv\": {\n            \"name\": \"Carbad fon uisge\",\n        },\n        \"uzis\": {\n            \"name\": \"Uzis\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"Lòin Uzi\",\n        },\n        \"vole\": {\n            \"name\": [\n                \"Luch-uisge\",\n                \"Radan-uisge\",\n            ]\n        },\n        \"vulture\": {\n            \"name\": \"Fitheach\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"Mùtant Speach\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"Sgaoiladair Mùtant Speach\",\n        },\n        \"water_sprite\": {\n            \"name\": \"Rotal Bàta\",\n        },\n        \"waterfall\": {\n            \"name\": \"Ceò Eas\",\n        },\n        \"white_light\": {\n            \"name\": \"Solas Geal\",\n        },\n        \"willard\": {\n            \"name\": \"\\\\{review}Willard\",\n        },\n        \"winston\": {\n            \"name\": \"Winston\",\n        },\n        \"winston_army\": {\n            \"name\": \"Winston (armachd)\",\n        },\n        \"wolf\": {\n            \"name\": \"Madadh-allaidh\",\n        },\n        \"worker_1\": {\n            \"name\": \"Ruspal-gunna 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"Ruspal-gunna 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"Ruspal-bata 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"Ruspal-bata 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"Ruspal Teine-tilgidh\",\n        },\n        \"xian_knight\": {\n            \"name\": \"Ridir Xian\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"Ìomhaigh Ridir Xian\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"Sleaghadair Xian\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"Ìomhaigh Sleaghadair Xian\",\n        },\n        \"yeti\": {\n            \"name\": \"Iètaidh\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"Greim Càball-slaodaidh\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"Italiano\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Esamina\",\n            \"hide_dialog\": \"Nascondi dialogo\",\n            \"reset_defaults\": \"Ripristina\",\n            \"rotate\": \"Ruota\",\n            \"unbind\": \"Dissocia\",\n            \"use_item\": \"Usa\",\n        },\n        \"config_presets\": {\n            \"applied\": \"Profilo applicato.\",\n            \"confirm_description\": \"Verranno modificate le seguenti impostazioni:\",\n            \"confirm_restart_note\": \"Nota: per avere effetto, alcune impostazioni potrebbero richiedere il riavvio del gioco.\",\n            \"empty\": \"Nessun profilo trovato.\",\n            \"no_changes\": \"Nessuna modifica da applicare.\",\n            \"title_fmt\": \"Applicare il profilo %s?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"Area 1\",\n            \"area_2\": \"Area 2\",\n            \"area_3\": \"Area 3\",\n            \"area_4\": \"Area 4\",\n            \"area_5\": \"Area 5\",\n            \"area_6\": \"Area 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"Avventura\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"GAME OVER\",\n            \"heading_inventory\": \"INVENTARIO\",\n            \"heading_items\": \"OGGETTI\",\n            \"heading_option\": \"OPZIONI\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Modalità Demo\",\n            \"direction_keys_controller\": \"Croce direzionale\",\n            \"direction_keys_keyboard\": \"Frecce\",\n            \"empty_slot_fmt\": \"- SLOT VUOTO -\",\n            \"exit\": \"Esci\",\n            \"hold_fmt\": \"Premere %s\",\n            \"off\": \"Spento\",\n            \"on\": \"Acceso\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Immissione ambigua: %s e %s\",\n            \"ambiguous_input_3\": \"Immissione ambigua: %s, %s, ...\",\n            \"bilinear_filter_off\": \"Filtro bilineare: disattivato\",\n            \"bilinear_filter_on\": \"Filtro bilineare: attivato\",\n            \"command_bad_invocation\": \"Invocazione non valida: %s\",\n            \"command_bool\": \"acceso, spento\",\n            \"command_decimal\": \"[decimale]\",\n            \"command_integer\": \"[intero]\",\n            \"command_percent\": \"[intero]\",\n            \"command_unavailable\": \"Questo comando non è attualmente disponibile\",\n            \"command_valid_values\": \"Valori validi: %s\",\n            \"complete_level\": \"Livello completato!\",\n            \"config_option_get\": \"%s è attualmente impostato su %s\",\n            \"config_option_set\": \"%s è stato modificato in %s\",\n            \"config_option_unknown_option\": \"Opzione sconosciuta: %s\",\n            \"current_health_get\": \"Salute attuale di Lara: %d\",\n            \"current_health_set\": \"Salute di Lara impostata su %d\",\n            \"door_close\": \"Chiuditi Sesamo!\",\n            \"door_open\": \"Apriti Sesamo!\",\n            \"door_open_fail\": \"Nessuna porta nelle vicinanze di Lara\",\n            \"flipmap_fail_already_off\": \"Mappa capovolta già disattivata\",\n            \"flipmap_fail_already_on\": \"Mappa capovolta già attivata\",\n            \"flipmap_off\": \"Mappa capovolta disattivata\",\n            \"flipmap_on\": \"Mappa capovolta attivata\",\n            \"fly_mode_off\": \"Modalità volo disabilitata\",\n            \"fly_mode_on\": \"Modalità volo abilitata\",\n            \"fps_counter_off\": \"Contatore FPS disabilitato\",\n            \"fps_counter_on\": \"Contatore FPS abilitato\",\n            \"give_item\": \"Aggiunto %s all'inventario di Lara\",\n            \"give_item_all_guns\": \"Preparati - Lara è armata fino ai denti!\",\n            \"give_item_all_keys\": \"Sorpresa! Ogni oggetto chiave di cui Lara ha bisogno è ora nel suo zaino.\",\n            \"give_item_cheat\": \"Lo zaino di Lara è diventato molto più pesante!\",\n            \"heal_already_full_hp\": \"Lara è già in piena salute\",\n            \"heal_success\": \"Lara è tornata in piena salute\",\n            \"invalid_cutscene\": \"Scena di intermezzo non valida\",\n            \"invalid_demo\": \"Demo non valida\",\n            \"invalid_item\": \"Oggetto sconosciuto: %s\",\n            \"invalid_level\": \"Livello non valido\",\n            \"invalid_object\": \"Oggetto non valido\",\n            \"invalid_room\": \"Stanza non valida: %d. Le stanze valide sono 0-%d\",\n            \"invalid_sample\": \"Suono non valido: %d\",\n            \"kill\": \"Ciao!\",\n            \"kill_all\": \"Puff! %d nemici scomparsi!\",\n            \"kill_all_fail\": \"Oh-oh, non ci sono più nemici da uccidere...\",\n            \"kill_fail\": \"Nessun nemico nelle vicinanze...\",\n            \"lighting_contrast_fmt\": \"Contrasto: %s\",\n            \"load_game\": \"Partita caricata dallo slot di salvataggio %d\",\n            \"load_game_fail_invalid_slot\": \"Slot di salvataggio %d non valido\",\n            \"load_game_fail_unavailable_slot\": \"Lo slot di salvataggio %d non è disponibile\",\n            \"object_not_found\": \"Oggetto non trovato\",\n            \"play_cutscene\": \"Caricamento scena di intermezzo %d\",\n            \"play_demo\": \"Caricamento demo %d\",\n            \"play_level\": \"Caricamento %s\",\n            \"pos_lara_missing\": \"Lara non presente\",\n            \"pos_lara_pos_fmt\": \"Stanza: %d\\nPosizione: %.3f, %.3f, %.3f\\nRotazione: %.3f, %.3f, %.3f\",\n            \"pos_level_fmt\": \"Livello %d\",\n            \"pos_level_fmt_cutscene\": \"Intermezzo %d\",\n            \"pos_level_fmt_demo\": \"Demo %d\",\n            \"quick_load\": \"Caricamento rapido dallo slot %d\",\n            \"quick_load_fail_no_bound_slot\": \"Nessuno slot di salvataggio attualmente assegnato\",\n            \"quick_load_fail_unavailable_bound_slot\": \"Lo slot di salvataggio assegnato non è disponibile\",\n            \"quick_save\": \"Salvataggio rapido\",\n            \"quick_save_fail_no_slots\": \"Nessuno slot di salvataggio rapido configurato\",\n            \"save_game\": \"Partita salvata nello slot di salvataggio %d\",\n            \"save_game_fail_invalid_slot\": \"Slot di salvataggio %d non valido\",\n            \"sound_available_samples\": \"Suoni disponibili: %s\",\n            \"sound_playing_sample\": \"Riproduzione del suono %d\",\n            \"speed_get\": \"Velocità attuale: %d\",\n            \"speed_set\": \"Velocità impostata su %d\",\n            \"strings_failed\": \"Impossibile ricaricare i file di lingua\",\n            \"strings_reloaded\": \"File di lingua ricaricati\",\n            \"textures_off\": \"Texture: disattivate\",\n            \"textures_on\": \"Texture: attivate\",\n            \"trapezoid_filter_off\": \"Filtro trapezoidale: disattivato\",\n            \"trapezoid_filter_on\": \"Filtro trapezoidale: attivato\",\n            \"ui_off\": \"Interfaccia utente: disattivata\",\n            \"ui_on\": \"Interfaccia utente: attivata\",\n            \"unknown_command\": \"Comando sconosciuto: %s\",\n            \"upscaling_factor\": \"Fattore di scala: x%d\",\n            \"wireframe_mode_off\": \"Modalità reticolo: disattivato\",\n            \"wireframe_mode_on\": \"Modalità reticolo: attivato\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"Animazione: \",\n            \"debug_animation_state\": \"Stato: \",\n            \"debug_camera_pos\": \"Origine telecamera: \",\n            \"debug_camera_target\": \"Obiettivo telecamera: \",\n            \"debug_immune\": \"Invulnerabilità attivata\",\n            \"debug_position\": \"Posizione: \",\n            \"debug_rotation\": \"Rotazione: \",\n            \"debug_speed\": \"Velocità: \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"\\\\{review}Elimina\",\n            \"delete_save_confirm\": \"\\\\{review}Eliminare questo salvataggio?\",\n            \"delete_save_failed\": \"\\\\{review}Impossibile eliminare il salvataggio selezionato.\",\n            \"delete_save_no\": \"\\\\{review}No\",\n            \"delete_save_yes\": \"\\\\{review}Sì\",\n            \"exit_game\": \"Esci dal Gioco\",\n            \"exit_to_title\": \"Torna al Menu\",\n            \"load_game\": \"Carica Partita\",\n            \"mode_new_game\": \"Nuova Partita\",\n            \"mode_new_game_jp\": \"NP Giapponese\",\n            \"mode_new_game_jp_plus\": \"NP Giapponese+\",\n            \"mode_new_game_plus\": \"Nuova Partita+\",\n            \"new_game\": \"Nuova Partita\",\n            \"play_previous_levels\": \"Gioca livelli precedenti\",\n            \"restart_level\": \"Ricomincia Livello\",\n            \"save_game\": \"Salva Partita\",\n            \"save_slot_unsupported\": \"Questo salvataggio non supporta questa funzione.\",\n            \"select_level\": \"Seleziona Livello\",\n            \"select_mod\": \"Seleziona Gioco\",\n            \"select_mode\": \"Seleziona Modalità\",\n            \"select_save\": \"Seleziona Salvataggio\",\n            \"story_so_far\": \"Un po' di storia...\",\n            \"switch_mod\": \"Cambia Gioco\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"Sei sicuro?\",\n            \"continue\": \"Continua\",\n            \"exit_to_title\": \"Vuoi tornare al menu?\",\n            \"no\": \"No\",\n            \"paused\": \"Pausa\",\n            \"quit\": \"Esci\",\n            \"yes\": \"Sì\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"Avanza fotogramma\",\n            \"camera_move_prompt\": \"Sposta telecamera\",\n            \"camera_reset_prompt\": \"Ripristina telecamera\",\n            \"camera_roll_prompt\": \"Inclina telecamera\",\n            \"camera_rotate_90_prompt\": \"Ruota di 90 gradi\",\n            \"camera_rotate_prompt\": \"Ruota telecamera\",\n            \"change_lara_pose\": \"Cambia posa\",\n            \"fov_prompt\": \"Regola campo visivo\",\n            \"lara_move_prompt\": \"Sposta Lara\",\n            \"lara_reset_prompt\": \"Ripristina Lara\",\n            \"lara_roll_prompt\": \"Inclina Lara\",\n            \"lara_rotate_90_prompt\": \"Ruota Lara di 90°\",\n            \"lara_rotate_prompt\": \"Ruota Lara\",\n            \"snap_prompt\": \"Scatta una foto\",\n            \"title_camera_pos\": \"Modalità Foto\",\n            \"title_lara_pos\": \"Spostamento Lara\",\n            \"toggle_help\": \"Visualizza guida\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"Le impostazioni sono disabilitate per questo gruppo di livelli.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Modifica valore\",\n                \"frozen_option_disclaimer\": \"Questa impostazione è applicata dal generatore di livelli e non può essere modificata.\",\n                \"hue\": \"Tonalità\",\n                \"lightness\": \"Luminosità\",\n                \"restore_default\": \"Ripristina predefiniti\",\n                \"toggle_help\": \"Visualizza guida\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Controller\",\n                    \"keyboard\": \"Tastiera\",\n                },\n                \"customize\": \"Personalizza comandi\",\n                \"layout\": {\n                    \"custom_1\": \"Tasti utente 1\",\n                    \"custom_2\": \"Tasti utente 2\",\n                    \"custom_3\": \"Tasti utente 3\",\n                    \"default\": \"Tasti predefiniti\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Movimento\",\n                    \"items\": \"Oggetti\",\n                    \"misc\": \"Vari\",\n                    \"system\": \"Sistema\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Comandi\",\n                    \"fixes\": \"Correzioni\",\n                    \"general\": \"Generale\",\n                    \"mods\": \"Modifiche\",\n                    \"presets\": \"Profili\",\n                },\n                \"title\": \"Opzioni di gioco\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"Barre\",\n                    \"rendering\": \"Schermo\",\n                    \"stats\": \"Statistiche\",\n                    \"ui\": \"Interfaccia\",\n                    \"visuals\": \"Grafica\",\n                },\n                \"title\": \"Opzioni grafiche\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"Varie\",\n                    \"volume\": \"Volume\",\n                },\n                \"title\": \"Opzioni audio\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Munizioni a Segno/Usate\",\n            \"ammo_hits\": \"Colpi a Segno\",\n            \"ammo_used\": \"Munizioni Usate\",\n            \"assault_best_time_fmt\": \"%s\",\n            \"assault_finish\": \"Fine\",\n            \"assault_no_times_set\": \"Nessun tempo impostato\",\n            \"assault_other_times_fmt\": \"%s\",\n            \"assault_title\": \"MIGLIORI TEMPI\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Statistiche Bonus\",\n            \"crystals\": \"Cristalli\",\n            \"deaths\": \"Morti\",\n            \"detail_fmt\": \"%d di %d\",\n            \"distance_travelled\": \"Distanza Percorsa\",\n            \"final_statistics\": \"Statistiche Finali\",\n            \"gym_assault_course\": \"Corso d'Addestramento\",\n            \"gym_racetrack_course\": \"Corso di Guida\",\n            \"kills\": \"Uccisioni\",\n            \"level\": \"Livello\",\n            \"medipacks_used\": \"Kit Medici Usati\",\n            \"none\": \"Nessuno\",\n            \"pickups\": \"Oggetti\",\n            \"secrets\": \"Segreti Scoperti\",\n            \"time_taken\": \"Tempo Impiegato\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Attiva o disattiva la treccia di Lara.\",\n            },\n            \"cheats\": {\n                \"help\": \"Attiva o disattiva i trucchi di gioco.\",\n            },\n            \"clear\": {\n                \"help\": \"Cancella la cronologia dei messaggi della console.\",\n            },\n            \"debug\": {\n                \"help\": \"Attiva o disattiva le informazioni di debug.\",\n            },\n            \"drain\": {\n                \"help\": \"Drena la stanza corrente, rimuovendo l'acqua.\",\n            },\n            \"end_level\": {\n                \"help\": \"Termina il livello attuale.\",\n            },\n            \"exit\": {\n                \"help\": \"Termina il gioco.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Attiva o disattiva mappa capovolta.\",\n            },\n            \"flood\": {\n                \"help\": \"Immerge la stanza corrente nell'acqua.\",\n            },\n            \"fly\": {\n                \"help\": \"Attiva o disattiva il trucco Volo.\",\n            },\n            \"fps\": {\n                \"help\": \"Modifica il valore degli FPS.\",\n            },\n            \"give\": {\n                \"help\": \"Aggiunge un determinato oggetto all'inventario di Lara.\",\n                \"invalid_secret\": \"Segreto non valido: %s (segreti validi: %s)\",\n                \"secret_given\": \"Segreto %s aggiunto\",\n                \"secret_list\": \"Segreti raccolti: %d di %d (%s)\",\n                \"secret_none\": \"Segreti raccolti: %d di %d\",\n                \"secret_taken\": \"Segreto %s rimosso\",\n            },\n            \"give_secret\": {\n                \"help\": \"Mostra i segreti ottenuti da Lara, oppure aggiunge/rimuove un segreto in base al numero specificato.\",\n            },\n            \"heal\": {\n                \"help\": \"Riporta Lara in piena salute.\",\n            },\n            \"help\": {\n                \"help\": \"Mostra la guida per tutti i comandi o la guida dettagliata per uno solo.\",\n                \"list\": \"Comandi disponibili:\",\n            },\n            \"hp\": {\n                \"help\": \"Imposta la salute di Lara al valore specificato.\",\n            },\n            \"immune\": {\n                \"help\": \"Attiva o disattiva l'invulnerabilità. (In alcune circostanze, Lara può essere comunque uccisa.)\",\n                \"off\": \"Lara è ora vulnerabile\",\n                \"on\": \"Lara è ora immune ai danni\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"Attiva o disattiva lo scatto infinito.\",\n                \"off\": \"Lara non può più scattare all'infinito\",\n                \"on\": \"Lara può ora scattare all'infinito\",\n            },\n            \"kill\": {\n                \"help\": \"Uccide i nemici vicini.\",\n            },\n            \"lighting\": {\n                \"help\": \"Attiva o disattiva il sistema di illuminazione.\",\n            },\n            \"load\": {\n                \"help\": \"Carica la partita dallo slot di salvataggio indicato o da un salvataggio rapido.\",\n            },\n            \"lua\": {\n                \"help\": \"Esegue la stringa di codice Lua specificata.\",\n                \"runtime_error\": \"Errore di esecuzione Lua: %s\",\n                \"syntax_error\": \"Errore di sintassi Lua: %s\",\n            },\n            \"mod\": {\n                \"help\": \"Passa alla mod specificata e riavvia il gioco.\",\n            },\n            \"music\": {\n                \"help\": \"Riproduce la traccia musicale con l'ID specificato.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Riproduce la scena di intermezzo con il numero specificato.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Riproduce la demo con il numero specificato.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Gioca al livello di allenamento.\",\n            },\n            \"play_level\": {\n                \"help\": \"Gioca un livello con il nome o il numero specificato.\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Traccia musicale non valida\",\n                \"stopped\": \"Musica interrotta\",\n                \"track\": \"Riproduzione traccia musicale %d\",\n            },\n            \"pos\": {\n                \"help\": \"Mostra la posizione di Lara.\",\n            },\n            \"save\": {\n                \"help\": \"Salva la partita nello slot di salvataggio indicato o nello slot di salvataggio rapido successivo.\",\n            },\n            \"screenshot\": {\n                \"help\": \"Salva un'istantanea dello schermo su disco in un percorso facoltativo.\",\n            },\n            \"set\": {\n                \"help\": \"Visualizza o aggiorna l'impostazione di configurazione specificata.\",\n            },\n            \"sfx\": {\n                \"help\": \"Riproduce un effetto sonoro con l'ID specificato.\",\n            },\n            \"spawn\": {\n                \"fail\": \"Impossibile generare l'oggetto richiesto\",\n                \"success\": \"Oggetto richiesto generato vicino a Lara\",\n            },\n            \"speed\": {\n                \"help\": \"Modifica la velocità di gioco.\",\n            },\n            \"strings\": {\n                \"help\": \"Ricarica i file di lingua correnti dal disco.\",\n            },\n            \"teleport\": {\n                \"item\": \"Teletrasportato all'oggetto: %d\",\n                \"item_fail\": \"Impossibile teletrasportarsi all'oggetto: %d\",\n                \"object\": \"Teletrasportato all'oggetto: %s\",\n                \"object_fail\": \"Impossibile teletrasportarsi all'oggetto: %s\",\n                \"pos\": \"Teletrasportato in posizione: %.3f %.3f %.3f\",\n                \"pos_fail\": \"Impossibile teletrasportarsi in posizione: %.3f %.3f %.3f\",\n                \"room\": \"Teletrasportato nella stanza: %d\",\n                \"room_fail\": \"Impossibile teletrasportarsi nella stanza: %d\",\n            },\n            \"textures\": {\n                \"help\": \"Attiva o disattiva le texture.\",\n            },\n            \"title\": {\n                \"help\": \"Ritorna al menu principale.\",\n            },\n            \"tp\": {\n                \"help\": \"Teletrasporta Lara in una posizione o in un numero di stanza specifici.\",\n            },\n            \"trigger\": {\n                \"help\": \"Attiva o disattiva un oggetto tramite ID o nome.\",\n                \"invalid_item\": \"Oggetto non valido: %s\",\n                \"no_match\": \"Obiettivo sconosciuto: %s\",\n                \"not_found\": \"Nessun oggetto corrispondente trovato per: %s\",\n                \"triggered\": \"Oggetto(i) attivato(i): %s\",\n                \"untriggered\": \"Oggetto(i) non attivato(i): %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Attiva o disattiva la sincronizzazione verticale.\",\n            },\n            \"weather\": {\n                \"help\": \"Modifica il tempo meteorologico attuale.\",\n                \"invalid\": \"Tempo meteorologico non valido: %s (valore valido: %s)\",\n                \"set\": \"Tempo meteorologico impostato su %s\",\n            },\n            \"winston\": {\n                \"dead\": \"Il tuo maggiordomo è morto. Sei un mostro!\",\n                \"spawn_failed\": \"Impossibile convocare Winston\",\n                \"spawned\": \"Winston è stato convocato vicino a Lara\",\n                \"teleported\": \"Winston è stato teletrasportato vicino a Lara\",\n            },\n            \"wireframe\": {\n                \"help\": \"Attiva o disattiva la renderizzazione in modalità reticolo.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"TR1 PC\",\n            \"tr1_ps1\": \"TR1 PS1\",\n            \"tr2_pc\": \"TR2 PC\",\n            \"tr2_ps1\": \"TR2 PS1\",\n            \"tr3_pc\": \"TR3 PC\",\n            \"tr3_ps1\": \"TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"TR2 PC\",\n                \"tr2_ps1\": \"TR2 PS1\",\n                \"tr3_pc\": \"TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"Predefinito\",\n                \"golden_sophia\": \"Sophia Dorata\",\n                \"sophia\": \"Sophia\",\n                \"tr1_bacon_lara\": \"Sosia di Lara\",\n                \"tr1_classic\": \"Classico TR1\",\n                \"tr1_combo\": \"Combinato TR1\",\n                \"tr1_golden_bacon_lara\": \"Sosia di Lara Dorata\",\n                \"tr1_golden_lara\": \"Lara Dorata TR1\",\n                \"tr1_gym\": \"Allenamento TR1 \",\n                \"tr1_mauled\": \"Lara ferita TR1\",\n                \"tr1_ngage\": \"TR1 N-Gage\",\n                \"tr23_golden_lara\": \"Lara Dorata TR2/3\",\n                \"tr2_bomber_jacket\": \"Giubbotto pesante\",\n                \"tr2_classic\": \"Classico TR2\",\n                \"tr2_diving_suit\": \"Muta subacquea 1\",\n                \"tr2_diving_suit_alpha\": \"Muta subacquea 2\",\n                \"tr2_gym\": \"Allenamento TR2\",\n                \"tr2_robe\": \"Vestaglia\",\n                \"tr2_vegas\": \"Las Vegas\",\n                \"tr3_antarctica\": \"Antartide\",\n                \"tr3_catsuit\": \"Tuta aderente\",\n                \"tr3_classic\": \"Classico TR3\",\n                \"tr3_gym\": \"Allenamento TR3\",\n                \"tr3_nevada\": \"Nevada\",\n                \"tr3_south_pacific\": \"Sud Pacifico\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"Demo di Tomb Raider I\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"Conti in Sospeso\",\n            },\n            \"tr2\": {\n                \"title\": \"Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"La Maschera Dorata\",\n            },\n            \"tr3\": {\n                \"title\": \"Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"L'Artefatto Perduto\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"Individuale\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"Condiviso\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"Qualsiasi\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"Nero\",\n            \"BK_IMAGE\": \"Immagine\",\n            \"BK_MONOCHROME\": \"Monocromatico\",\n            \"BK_MONOCHROME_COOL\": \"Monocromatico (freddo)\",\n            \"BK_MONOCHROME_WARM\": \"Monocromatico (caldo)\",\n            \"BK_NONE\": \"Trasparente\",\n            \"BK_PATTERN_STATIC\": \"Statico\",\n            \"BK_PATTERN_WAVE\": \"Ondeggiante\",\n            \"BK_TRANSPARENT_DARK\": \"Molto scuro\",\n            \"BK_TRANSPARENT_MEDIUM\": \"Scuro\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"Sempre\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"Solo per i Boss\",\n            \"BAR_SHOW_MODE_NEVER\": \"Mai\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"Nessuno\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"Prospettiva\",\n            \"BILLBOARD_LOCK_ROLL\": \"Rotazione\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"Rotaz. e inclinaz.\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"\\\\{review}Disabilitato\",\n            \"BLOOD_EFFECTS_PINK\": \"\\\\{review}Rosa\",\n            \"BLOOD_EFFECTS_RED\": \"\\\\{review}Rosso\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"\\\\{review}Predefinito\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"\\\\{review}Mai\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"\\\\{review}Sommerso\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"Controller\",\n            \"INPUT_BACKEND_KEYBOARD\": \"Tastiera\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Azione\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Telecamera Posteriore\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Telecamera in Basso\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Telecamera Anteriore\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Telecamera Sinistra\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"Ripristina Telecamera\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Telecamera Destra\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Telecamera in Alto\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"Cambia Costume\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Cambia Bersaglio\",\n            \"INPUT_ROLE_CROUCH\": \"Accovacciati\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"Livello Contrasto\",\n            \"INPUT_ROLE_DOWN\": \"Indietro\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Estrai Arma\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Apri Console\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"Equipaggia Pistole Automatiche\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"Equipaggia Desert Eagle\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"Equipaggia Lanciagranate\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"Equipaggia Fucile Subacqueo\",\n            \"INPUT_ROLE_EQUIP_M16\": \"Equipaggia M16\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"Equipaggia Magnum\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"Equipaggia MP5\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"Equipaggia Pistole\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"Equipaggia Lanciarazzi\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"Equipaggia Fucile a Pompa\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"Equipaggia Uzi\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Trucco Volo\",\n            \"INPUT_ROLE_FPS\": \"Mostra FPS\",\n            \"INPUT_ROLE_INVENTORY\": \"Inventario\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Trucco Oggetto\",\n            \"INPUT_ROLE_JUMP\": \"Salta\",\n            \"INPUT_ROLE_LEFT\": \"Sinistra\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Salta Livello\",\n            \"INPUT_ROLE_LOAD\": \"Carica Partita\",\n            \"INPUT_ROLE_LOOK\": \"Guarda\",\n            \"INPUT_ROLE_PAUSE\": \"Pausa\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"Caricamento Rapido\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"Salvataggio Rapido\",\n            \"INPUT_ROLE_RIGHT\": \"Destra\",\n            \"INPUT_ROLE_ROLL\": \"Capriola\",\n            \"INPUT_ROLE_SAVE\": \"Salva Partita\",\n            \"INPUT_ROLE_SCREENSHOT\": \"Cattura Schermo\",\n            \"INPUT_ROLE_SLOW\": \"Cammina\",\n            \"INPUT_ROLE_SPRINT\": \"Scatto\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Passo a Sinistra\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Passo a Destra\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"Modifica Dimensione Bordi\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"Modifica Fattore di Scala\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"Attiva/Disattiva Filtro Bilineare\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"Attiva/Disattiva Schermo Intero\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Attiva/Disattiva Modalità Foto\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"Attiva/Disattiva Texture\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"Attiva/Disattiva Filtro Trapezoidale\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"Attiva/Disattiva Interfaccia\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"Attiva/Disattiva Modalità Reticolo\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Velocità Turbo\",\n            \"INPUT_ROLE_UP\": \"Corri\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Kit Medico Grande\",\n            \"INPUT_ROLE_USE_FLARE\": \"Razzo di Segnalazione\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Kit Medico Piccolo\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"Disattivato\",\n            \"JUMP_LOCK_LEGACY\": \"Originale\",\n            \"JUMP_LOCK_TUNED\": \"Modificato\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"Alto\",\n            \"LIGHTING_CONTRAST_LOW\": \"Basso\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Medio\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"Sempre\",\n            \"LOADING_SCREENS_DISABLED\": \"Disattivato\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"Nuovo livello\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"Avanzato\",\n            \"LOOK_MODE_RESTRICTED\": \"Limitato\",\n            \"LOOK_MODE_UNRESTRICTED\": \"Illimitato\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"Sempre\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"Mai\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Non ambientale\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"Diffusione multipla\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"Diffusione singola\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"Equipaggia o riponi\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"Solo equipaggia\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"Cerchio\",\n            \"SHADOW_TYPE_OCTAGON\": \"Ottagono\",\n            \"SHADOW_TYPE_SPRITE\": \"Sprite\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"Senza bordi\",\n            \"STATS_STYLE_BORDERED\": \"Con bordi\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"Disattivato\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"Opaco\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"Trasparente\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Completo\",\n            \"TARGET_LOCK_MODE_NONE\": \"Nessuno\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Parziale\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Bilineare\",\n            \"TEXTURE_FILTER_POINT\": \"Nitido\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"In basso al centro\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"In basso a sinistra\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"In basso a destra\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"In alto al centro\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"In alto a sinistra\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"In alto a destra\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"Corretto\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"Volume ambientale\",\n            \"description\": \"Regola il volume dei suoni ambientali.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"Volume scene di intermezzo\",\n            \"description\": \"Regola il volume delle scene di intermezzo presenti nel gioco.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Microfono vicino a Lara\",\n            \"description\": \"Imposta il microfono in modo che sia collocato nella posizione di Lara. Se disabilitato, il microfono sarà collocato nella posizione della telecamera.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"Musica nell'inventario\",\n            \"description\": \"Permette di riprodurre i suoni di gioco, gli effetti ambientali e la musica nella schermata dell'inventario.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Musica menu principale\",\n            \"description\": \"Riproduce la musica nel menu principale.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Tonalità suoni\",\n            \"description\": \"Per una maggiore varietà, consente di modificare in modo leggero e casuale la tonalità degli effetti sonori.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"Effetti sonori PS1\",\n            \"description\": \"Sostituisce alcuni effetti sonori con quelli della versione PS1.\\n\\n- Suono dello sparo delle Uzi (solo TR1)\\n- Suono dei passi a piedi nudi di Lara (solo TR2)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"Effetti sonori sott'acqua\",\n            \"description\": \"Permette di gestire la riproduzione di specifici effetti sonori legati ad animazioni, per oggetti quali porte e botole, quando la telecamera è sott'acqua.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Correggi suono dei segreti\",\n            \"description\": \"Impedisce la riproduzione errata del suono dei segreti quando si utilizza la chiave d'oro nella Tomba di Tihocan.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Musica segreti in polifonia\",\n            \"description\": \"Risolve il problema per cui il suono riprodotto dalla scoperta di un segreto interrompe la traccia musicale attiva.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Dialoghi nemici in polifonia\",\n            \"description\": \"Risolve il problema per cui i nemici, quando parlano, interrompono la traccia musicale attiva.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"Volume FMV\",\n            \"description\": \"Regola il volume dei filmati.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"Volume ambientale (inventario)\",\n            \"description\": \"Regola il volume dei suoni ambientali nella schermata dell'inventario.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"Volume musica (inventario)\",\n            \"description\": \"Regola il volume della musica nella schermata dell'inventario.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Ricorda musica riprodotta\",\n            \"description\": \"Contrassegna le tracce musicali riprodotte nel file di salvataggio, in modo che le tracce legate ad eventi non vengano rieseguite dopo il caricamento della partita.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{icon music} Volume principale\",\n            \"description\": \"Regola il volume di gioco. Le altre impostazioni sono relative a questo volume.\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Ripristina traccia\",\n            \"description\": \"Carica la traccia musicale che era in riproduzione al momento del salvataggio della partita.\\n\\n- Mai: non ripristina le tracce musicali al caricamento.\\n- Non ambientale: ripristina le tracce musicali ma non i suoni ambientali al caricamento.\\n- Sempre: ripristina qualsiasi tipo di traccia al caricamento.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"Volume musica\",\n            \"description\": \"Regola il volume della musica.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"Disattiva se in secondo piano\",\n            \"description\": \"Disattiva tutta la musica e gli effetti sonori quando la finestra del gioco non è in primo piano.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Volume effetti\",\n            \"description\": \"Regola il volume degli effetti sonori.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"Volume ambientale (sott'acqua)\",\n            \"description\": \"Regola il volume dei suoni ambientali quando si è sott'acqua.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"Volume musica (sott'acqua)\",\n            \"description\": \"Regola il volume della musica quando si è sott'acqua.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"Razzi inesauribili\",\n            \"description\": \"Impedisce ai razzi di segnalazione di spegnersi. I razzi lanciati continueranno a spegnersi normalmente.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"Corsa infinita\",\n            \"description\": \"Impedisce a Lara di stancarsi durante uno scatto. Gli ostacoli la faranno comunque fermare.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"Modalità ostilità alleati\",\n            \"description\": \"Controlla il modo in cui le unità alleate reagiscono quando subiscono danni.\\n\\n- Individuale: ogni alleato passa in modalità ostile in modo autonomo (come in TR3).\\n- Condiviso: tutti gli alleati diventano ostili insieme (come i monaci in TR2).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Velocità telecamera\",\n            \"description\": \"Modifica la velocità di movimento della telecamera manuale.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Modifica generazione Pierre\",\n            \"description\": \"Fa in modo che, alla sua generazione, un Pierre (in fuga) sostituisca un altro Pierre (in fuga) già esistente.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"\\\\{review}Comportamento di annegamento delle creature\",\n            \"description\": \"\\\\{review}Controlla come le creature terrestri si comportano nelle stanze d’acqua.\\n\\n- Mai: le creature terrestri non annegano mai (stile TR1).\\n- Predefinito: le creature terrestri annegano in acqua profonda 2 clic o più (stile TR2/3).\\n- Sommerso: le creature terrestri annegano solo quando sono completamente sommerse.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"Rimuovi armi extra\",\n            \"description\": \"Rimuove tutte le armi e le relative munizioni dal gioco tranne le pistole (per le sfide con solo pistole).\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Danni persistenti\",\n            \"description\": \"Lara non guarisce quando inizia un nuovo livello (per le sfide).\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Rimuovi i kit medici\",\n            \"description\": \"Rimuove tutti i kit medici dal gioco (per le sfide).\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Rimuovi collisione T-Rex morto\",\n            \"description\": \"Rimuove tutte le collisioni con il T-Rex dopo la sua dipartita. Utile quando il corpo del T-Rex blocca il passaggio.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"Consenti fuoco amico\",\n            \"description\": \"Permette a Lara di colpire gli alleati, come i monaci. Se disabilitato, gli alleati saranno immuni ai proiettili di Lara.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Preselezione oggetti chiave\",\n            \"description\": \"Quando Lara preme azione contro un buco della serratura o un altro ricettacolo e ha l'oggetto corrispondente nell'inventario, quell'oggetto verrà preselezionato.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"Inciampo su pendenza\",\n            \"description\": \"Fa in modo che Lara inciampi se effettua un salto all'indietro e, alle sue spalle, c'è una pendenza (TR3). Se disabilitato, Lara si fermerà bruscamente contro la pendenza (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"\\\\{review}Trigger dei sacchi per cadaveri\",\n            \"description\": \"\\\\{review}Consente la rimozione dei nemici uccisi quando Lara attraversa specifici trigger in determinati livelli. Se disabilitato, i nemici morti verranno sempre visualizzati.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"Vibrazione massi\",\n            \"description\": \"Se abilitata, la telecamera trema quando un masso è in movimento.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"Collisione granate\",\n            \"description\": \"Abilita il comportamento delle granate in stile TR3: rimbalzano sui muri e sulle pendenze e producono un raggio d'esplosione più ampio, a scapito di una velocità ridotta.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Trucchi\",\n            \"description\": \"Abilita vari trucchi:\\n\\n- L: termina immediatamente il livello.\\n- I: dà a Lara tutte le armi; maggior quantità di munizioni e kit medici; tutti gli oggetti necessari per superare il livello.\\n- O: abilita il trucco DOZY (nuotare a mezz'aria).\\n  - tasto CAMMINA: disabilita il trucco DOZY.\\n  - tasto ESTRAI ARMA: apre la porta più vicina (non funziona in certi casi).\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"Scene iniziali\",\n            \"description\": \"Abilita le scene iniziali nei livelli in cui sono previste.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Statistiche livello bussola\",\n            \"description\": \"Abilita la visualizzazione delle statistiche del livello quando è selezionata la bussola.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Console\",\n            \"description\": \"Abilita la console per sviluppatori.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"Cadute controllate\",\n            \"description\": \"Permette a Lara di girare a mezz'aria e afferrare la sporgenza della superficie da cui è appena scivolata, se durante la caduta viene premuto il tasto Azione.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"Salto uscita cunicolo\",\n            \"description\": \"Permette a Lara di saltare fuori dagli spazi angusti.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"\\\\{review}Inclinazione in strisciamento\",\n            \"description\": \"\\\\{review}Allinea la rotazione di Lara alla geometria del pavimento durante lo strisciamento.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"Strisciamento\",\n            \"description\": \"Permette a Lara di accovacciarsi e strisciare.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"Titoli di coda\",\n            \"description\": \"Abilita i titoli di coda mostrati dopo aver completato il gioco. Non influisce sulla schermata finale delle statistiche.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"Capriola da accovacciata\",\n            \"description\": \"Permette a Lara di eseguire una capriola in avanti mentre è accovacciata premendo il tasto Scatto.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Scene di intermezzo\",\n            \"description\": \"Abilita la riproduzione delle scene di intermezzo.\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Modalità demo\",\n            \"description\": \"Abilita la riproduzione delle demo nel menu principale.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Rotazione nemici casuale\",\n            \"description\": \"Applica una rotazione casuale aggiuntiva ad alcuni nemici quando vengono inizializzati.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Salva gli effetti\",\n            \"description\": \"Migliora il sistema di salvataggio in modo che gli effetti grafici, la foschia delle cascate, gli emettitori di fiamme e altro vengano salvati invece di scomparire dopo il caricamento della partita.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"FMV\",\n            \"description\": \"Abilita la riproduzione dei filmati.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Selezione modalità di gioco\",\n            \"description\": \"Consente di selezionare le modalità di gioco aggiuntive dal menu passaporto.\\n\\n- Nuova Partita+: sblocca tutte le armi con munizioni infinite; i nemici hanno punti vita doppi.\\n- NP giapponese: le armi causano il doppio dei danni; le scatole di razzi contengono 8 razzi di segnalazione anziché 6.\\n- NP giapponese+: combinazione di Nuova Partita+ e NP giapponese.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"Telecamera posa inattiva\",\n            \"description\": \"Regola la telecamera in modo che sia rivolta verso Lara mentre viene riprodotta la sua animazione di posa inattiva. Premi Guarda per reimpostare la telecamera.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Visuale invertita\",\n            \"description\": \"Inverte i controlli dell'asse Y quando Lara si guarda intorno.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Esame degli oggetti\",\n            \"description\": \"Per livelli personalizzati: consente di visualizzare le descrizioni degli oggetti nell'inventario quando fornite dall'autore del livello.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Capriole a mezz’aria\",\n            \"description\": \"Abilita i salti mortali e le capriole a mezz'aria, ovvero premendo il tasto Capriola durante le animazioni del salto e del tuffo ad angelo.\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"Blocchi mobili letali\",\n            \"description\": \"Se abilitato, quando un blocco mobile cade e atterra su Lara, lei morirà all'istante. Se disabilitato, Lara si sposterà sopra il blocco e rimarrà illesa.\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Salti inclinati\",\n            \"description\": \"Permette a Lara di spostarsi leggermente in avanti o indietro, tenendo premuto il relativo tasto, quando esegue salti sul posto.\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"Salti da sporgenza\",\n            \"description\": \"Permette a Lara di saltare verso l'alto o all'indietro mentre è appesa a una sporgenza, a patto che ci sia una superficie solida sotto i suoi piedi con cui darsi uno slancio\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Contenuto legale\",\n            \"description\": \"Abilita la schermata di introduzione e il video Core Design all'avvio del gioco.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"Telecamera manuale\",\n            \"description\": \"Usa i tasti della telecamera (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}) utilizzati nella Modalità Foto, anche per ruotare la telecamera di gioco.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"Rotazione da fermo\",\n            \"description\": \"Permette a Lara di ruotare in aria mentre esegue un salto da fermo. Mentre sei fermo, premi contemporaneamente i tasti Salta e Capriola.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Segnalazione oggetti\",\n            \"description\": \"Abilita un effetto scintillante intermittente vicino agli oggetti da raccogliere per evidenziarne la presenza.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"Gioca livelli precedenti\",\n            \"description\": \"Abilita le opzioni \\\"Gioca livelli precedenti\\\" e \\\"Un po' di storia...\\\" nella schermata di selezione Nuova Partita.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"Strisciamento reattivo\",\n            \"description\": \"Abilita miglioramenti rispetto alle meccaniche originali.\\n\\n- Consente di riprendere a strisciare più rapidamente dopo essersi fermati.\\n- Consente di passare dalla corsa/scatto allo strisciare senza prima fermarsi.\\n- Consente di passare dallo strisciare alla capriola da accovacciata (se abilitata) senza doversi prima accovacciare manualmente.\\n- Consente di girarsi mentre si è accovacciati.\\n- Ripristina l’animazione di Lara di raccolta degli oggetti mentre lei striscia (esclusi i razzi).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"Scatto migliorato\",\n            \"description\": \"Abilita per Lara una modalità di scatto migliorata.\\n\\n- consente di effettuare uno scatto non appena Lara ha energia, invece di dover aspettare che la barra della resistenza sia piena.\\n- consente di effettuare uno scatto sulle scale senza essere interrotti dall’animazione di corsa normale.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Cristalli di salvataggio\",\n            \"description\": \"Permette di salvare all'inizio di ogni livello o utilizzando i cristalli. I livelli hanno un numero limitato di cristalli di salvataggio che possono essere utilizzati una sola volta, come nella versione PS1. La modifica di questa opzione richiederà il riavvio del livello.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"Corri dopo scivolata\",\n            \"description\": \"Permette a Lara di iniziare a correre immediatamente quando tocca terra dopo essere scivolata in avanti da una pendenza. Per farlo, tieni premuto il tasto Corri.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"Oscillazione su sporgenza\",\n            \"description\": \"Permette a Lara di oscillare lentamente quando è aggrappata ad una sporgenza molto sottile (come in TR3). Se disabilitato, Lara oscillerà solo nel momento in cui si aggrappa alla sporgenza (come in TR1/2).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"Deflessione rapida da muro\",\n            \"description\": \"Permette a Lara di riprendersi più velocemente dopo aver urtato un muro e un tasto direzionale viene premuto insieme con il tasto Corri.\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"Collisione delicata con mesh\",\n            \"description\": \"Permette a Lara di muoversi dolcemente contro le maglie poligonali statiche – simile a TR4+ – anziché fermarsi bruscamente.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"Scatto\",\n            \"description\": \"Permette a Lara di effettuare uno scatto, come in TR3+.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"Spinta su gradino\",\n            \"description\": \"Fa in modo che Lara sia spinta giù da un gradino se si preme il tasto Capriola vicino al bordo.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Annulla oscillazione\",\n            \"description\": \"Consente di annullare l'animazione di oscillazione di Lara dalle sporgenze, lasciandole andare e afferrandole di nuovo rapidamente\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Cambio bersaglio\",\n            \"description\": \"Abilita il cambio di bersaglio in stile TR4+ mentre si punta l'arma. Premi il tasto Cambia Bersaglio mentre miri per cambiare bersaglio.\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Cronometro nell'inventario\",\n            \"description\": \"Fa in modo che il tempo di gioco scorra anche nel menu dell'inventario.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"Attiva/Disattiva accovacciati\",\n            \"description\": \"Permette a Lara di rimanere accovacciata dopo aver premuto una volta il tasto \\\"Accovacciati\\\". Premi di nuovo il tasto per rialzarti.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"Attiva/Disattiva scatto\",\n            \"description\": \"Permette a Lara di continuare a correre dopo aver premuto una volta il tasto \\\"Scatto\\\". Premi di nuovo il tasto per interrompere lo scatto.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Schermata statistiche finali\",\n            \"description\": \"Abilita la schermata delle statistiche totali di gioco che viene visualizzata dopo i titoli di coda.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Salti reattivi\",\n            \"description\": \"Permette a Lara di saltare in qualsiasi momento mentre corre.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Annullamento reattivo nuoto\",\n            \"description\": \"Permette a Lara di fermarsi in modo più istantaneo sott'acqua quando viene rilasciato il tasto per nuotare.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Nuoto naturale\",\n            \"description\": \"Fornisce alla velocità di virata subacquea di Lara una curva di accelerazione per movimenti più fluidi, come in TR2+. Disabilitando questa opzione, Lara avrà una velocità di virata più rapida, come in TR1.\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Capriola sott'acqua\",\n            \"description\": \"Permette a Lara di eseguire capriole mentre è sott'acqua.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Guadare\",\n            \"description\": \"Permette a Lara di guadare acque poco profonde, anziché rimanere bloccata sulla superficie dell'acqua.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Interazioni animate\",\n            \"description\": \"Fa in modo che Lara si avvicini agli oggetti, alle leve e agli interruttori quando questi sono nelle vicinanze, invece di teletrasportarsi su di loro.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Correggi IA degli alligatori\",\n            \"description\": \"Risolve il problema degli alligatori che non infliggono danni se Lara rimane ferma nell'acqua.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Correggi IA degli orsi\",\n            \"description\": \"Risolve il problema per cui gli attacchi con la zampa degli orsi non colpiscono Lara.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Correggi collisione ponti\",\n            \"description\": \"Risolve il problema per cui Lara non è in grado di afferrare parti di alcuni ponti e muri invisibili ai bordi. Corregge anche i problemi di collisione con ponti levatoi, botole e ponti quando sono impilati l'uno sull'altro, su pendii o vicino al suolo.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Correggi cadute da pavimenti\",\n            \"description\": \"Risolve il problema per cui i passi laterali e il camminare all'indietro su pavimenti fragili fanno sì che Lara scenda immediatamente sul pavimento sottostante.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Correggi priorità lancio razzi\",\n            \"description\": \"Risolve il problema per cui Lara dà priorità al lancio di un razzo di segnalazione esaurito mentre è a mezz'aria, il che può impedirle di afferrare le sporgenze.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Correggi dati livelli\",\n            \"description\": \"Risolve i problemi dei livelli originali relativi a dati/eventi.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Correggi errore dei razzi\",\n            \"description\": \"Risolve il problema in cui, se si preme il tasto di estrazione di un razzo di segnalazione mentre si raccoglie un qualsiasi oggetto, ne verrà creato uno dal nulla.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Correggi duplicazione oggetti\",\n            \"description\": \"Risolve il problema per cui è possibile duplicare l'utilizzo degli oggetti chiave nell'inventario.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"Correggi errore di raccolta\",\n            \"description\": \"Risolve i problemi per cui Lara a volte scivola contro i muri quando raccoglie oggetti sott’acqua, e per cui si incastra sotto i soffitti fortemente inclinati quando raccoglie gli oggetti sopra l'acqua.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"Correggi precisione M16/MP5\",\n            \"description\": \"Corregge la precisione della mira dell'M16/MP5 mentre Lara corre.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"Correggi priorità scimmie\",\n            \"description\": \"Le scimmie sotto attacco daranno priorità alla rappresaglia piuttosto che alla raccolta di Kit Medici e Chiavi.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"Correggi mira indigeno\",\n            \"description\": \"Risolve il problema per cui a volte l'indigeno con la cerbottana non riesce a mirare correttamente verso Lara.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Correggi errore QWOP\",\n            \"description\": \"Risolve il problema per cui a volte Lara, saltando da piccoli gradini, provoca una strana animazione di corsa nota come stato QWOP.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Correggi errore gradini\",\n            \"description\": \"Risolve il problema per cui a volte Lara viene spinta contro i muri adiacenti ai gradini quando corre su di loro in un certo modo.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"Correggi impatto muro\",\n            \"description\": \"Risolve il problema per cui Lara non reagisce quando urta un muro mentre guada.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Correggi cammina-corsa-salto\",\n            \"description\": \"Risolve il problema per cui a volte Lara non riesce a saltare immediatamente dopo essere passata dall'animazione di camminata a quella di corsa.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"\\\\{review}Corregge la geometria dei muri\",\n            \"description\": \"\\\\{review}Corregge i casi nella geometria dei livelli OG in cui inclinazioni all'interno dei muri possono portare a calcoli di altezza imprecisi.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"Correggi uscita dall'acqua\",\n            \"description\": \"Risolve il problema che permette a Lara di passare direttamente da una stanza con acqua ad una senza acqua adiacente o sottostante. Inoltre, questo impedirà a Lara di uscire dall'acqua su pendii non calpestabili.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"Ricarica fucile subacqueo\",\n            \"description\": \"Imposta la frequenza con cui Lara deve ricaricare il fucile subacqueo, in base al numero attuale di munizioni. Ad esempio, se impostato a 3, dovrà ricaricare dopo ogni terzo colpo. Impostare a 0 per disabilitare completamente la ricarica.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"Tempo di inattività\",\n            \"description\": \"Permette a Lara di riprodurre la sua animazione di posa dopo il numero di secondi d'inattività specificato. Impostalo a 0 per disabilitarlo.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"Modalità blocco salto\",\n            \"description\": \"Per salti più reattivi, consente di modificare quanto tempo deve attendere Lara per poter saltare dopo aver iniziato a correre.\\n\\n- Originale: corrisponde alla durata di tempo in TR2.\\n- Modificato: è possibile effettuare il salto 2 fotogrammi prima.\\n- Disattivato: è possibile effettuare il salto subito dopo l'inizio della corsa.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Schermate di caricamento\",\n            \"description\": \"Controlla il comportamento delle schermate di caricamento dei livelli.\\n\\n- Disattivato: non mostra mai le schermate di caricamento.\\n- Sempre: mostra le schermate di caricamento.\\n- Nuovo livello: non mostra le schermate di caricamento quando si carica un salvataggio.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"Modalità osservazione\",\n            \"description\": \"Permette di gestire in quali situazioni Lara può guardarsi attorno (tasto 'Guarda').\\n\\n- Limitato: è consentito esaminare l'ambiente solo quando Lara è ferma e mai quando è sott'acqua.\\n- Avanzato: è consentito esaminare l'ambiente durante la maggior parte delle situazioni, tranne in alcune come quando si spingono i blocchi.\\n- Illimitato: è sempre consentito esaminare l'ambiente durante il normale controllo di Lara.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"Slot di salvataggio rapido\",\n            \"description\": \"Modifica il numero di slot di salvataggio rapido disponibili.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Slot di salvataggio\",\n            \"description\": \"Modifica il numero di slot di salvataggio disponibili.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"\\\\{review}Pausa quando si perde il focus\",\n            \"description\": \"\\\\{review}Interrompe il progresso del gioco quando la finestra del gioco perde il focus.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"Area danni proiettile\",\n            \"description\": \"Controlla la propagazione dei danni del Lanciarazzi e del Lanciagranate.\\n\\n- Diffusione singola: comportamento di TR1 e TR2.\\n- Diffusione multipla: comportamento di TR3.\\n\\nL'opzione \\\"Diffusione multipla\\\" spesso causa danni doppi ai singoli nemici.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Ricorda armi tra livelli\",\n            \"description\": \"Fa sì che Lara ricordi quale arma ha usato per ultima nel livello precedente quando inizia un nuovo livello. Se disabilitato, Lara tornerà ad avere nella fondina le sue pistole.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"Ripristina nemici PS1\",\n            \"description\": \"Aggiunge la mummia che appare nella versione PlayStation del livello 'Città di Khamoon', stanza 25.\\nLa modifica di questa opzione richiederà il riavvio della partita.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Salute iniziale di Lara\",\n            \"description\": \"Imposta il valore di salute di Lara all'inizio di ogni livello.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Modalità aggancio bersaglio\",\n            \"description\": \"Modifica il comportamento di aggancio dei bersagli.\\n\\n- Completo: mantieni sempre il bersaglio agganciato anche se il nemico esce dal campo visivo o muore (TR1-3).\\n- Parziale: mantieni il bersaglio agganciato anche se il nemico esce dal campo visivo, ma lo perdi se muore.\\n- Nessuno: perdi il bersaglio agganciato se il nemico esce dal campo visivo o muore (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"Modalità errore muro\",\n            \"description\": \"Consente di utilizzare il comportamento dell'errore del muro di TR1 in TR2 e viceversa; consente inoltre di correggere tutti i tipi di errore del muro.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"Buffering (tasti funzione)\",\n            \"description\": \"Abilita il buffering dei tasti funzione (1 fotogramma) per ottenere un controllo preciso dei movimenti di Lara. Questa funzione esisteva originariamente solo nella versione TombATI (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"Buffering (inventario)\",\n            \"description\": \"Abilita il buffering dell'inventario (2 fotogrammi) per ottenere un controllo preciso dei movimenti di Lara.\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Passaporto reattivo\",\n            \"description\": \"Fa in modo che sfogliare le pagine del passaporto sia più istantaneo, tenendo conto dei tasti premuti dell'utente\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Passi laterali migliorati\",\n            \"description\": \"Abilita i passi laterali in stile TR3+, ad es. MAIUSC+frecce direzionali. I pulsanti dedicati per i passi laterali continueranno a funzionare.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"Equipaggiamento rapido armi\",\n            \"description\": \"Controlla il comportamento dei tasti per l'equipaggiamento rapido delle armi.\\n\\n- Solo equipaggia: premendo il tasto, Lara equipaggerà l'arma assegnata.\\n- Equipaggia o riponi: premendo il tasto, Lara equipaggerà l'arma assegnata. In aggiunta, con lo stesso tasto, la riporrà se la sta attualmente tenendo in mano.\",\n        },\n        \"language\": {\n            \"title\": \"Lingua\",\n            \"description\": \"Cambia la lingua dell'interfaccia utente.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Filtro anisotropico\",\n            \"description\": \"Migliora il filtraggio delle texture a distanza.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Rapporto d'aspetto\",\n            \"description\": \"Forza determinati rapporti d'aspetto del gioco con barre nere ai bordi.\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"Bordi\",\n            \"description\": \"Aggiunge dei bordi neri attorno alla finestra del gioco.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Filtro trapezoidale\",\n            \"description\": \"Corregge la renderizzazione dei quadrilateri.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"Sincronizzazione verticale\",\n            \"description\": \"Attiva o disattiva la sincronizzazione verticale.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"FPS\",\n            \"description\": \"Imposta i fotogrammi al secondo.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Contrasto\",\n            \"description\": \"Aumenta il contrasto per fonti di luce dinamiche come razzi di segnalazione e lampi di armi da fuoco.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Formato cattura schermo\",\n            \"description\": \"Formato del file da utilizzare per le istantanee dello schermo.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"Modalità blocco sprite\",\n            \"description\": \"Controlla quali assi bloccare durante la visualizzazione degli sprite sullo schermo.\\n\\n- Nessuno: mostra gli sprite normalmente.\\n- Rotazione: blocca l'asse di rotazione – utile solo in modalità foto.\\n- Rotazione e inclinazione: fa in modo che gli sprite rimangano in posizione verticale e non si inclinino verso il suolo quando li si guarda dall'alto.\\n- Prospettiva: blocca gli assi di rotazione e inclinazione e, inoltre, ruota leggermente gli sprite verso il centro dello schermo.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Filtro texture\",\n            \"description\": \"Passa dalle texture di gioco filtrate a quelle pixelate e viceversa.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"Filtro interfaccia\",\n            \"description\": \"Passa dall'interfaccia di gioco filtrata a quella pixelata e viceversa.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"Fattore di scala\",\n            \"description\": \"Ridimensiona l'immagine di gioco in base a un fattore di scala definito, mantenendo un aspetto pixelato.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"Filtro ridimensionamento\",\n            \"description\": \"Alterna l'aspetto smussato o pixelato per l'intero schermo.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Colore barra ossigeno\",\n            \"description\": \"Colore utilizzato per la barra dell'ossigeno.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Colore barra ossigeno\",\n            \"description\": \"Colore utilizzato per la barra dell'ossigeno.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Posizione barra ossigeno\",\n            \"description\": \"Posizione in cui viene visualizzata la barra dell'ossigeno.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"Posizione indicatore munizioni\",\n            \"description\": \"Posizione in cui viene visualizzato l'indicatore delle munizioni.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"Aspetto barre\",\n            \"description\": \"Controlla l'aspetto visivo delle barre dell'interfaccia utente.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Dimensione barre\",\n            \"description\": \"Modifica le dimensioni delle barre dell'interfaccia utente.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"Barre lampeggianti\",\n            \"description\": \"Fa in modo che le barre della salute e dell'ossigeno di Lara lampeggino quando stanno per esaurirsi.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Barre sfumate\",\n            \"description\": \"Fa in modo che la barra della salute e la barra dell'ossigeno utilizzino transizioni di colore uniformi.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Scorrimento continuo\",\n            \"description\": \"Consente la navigazione direzionale nei menu senza soluzione di continuità.\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Colore barra nemici\",\n            \"description\": \"Colore utilizzato per la barra della salute dei nemici.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Colore barra alleati\",\n            \"description\": \"Colore utilizzato per la barra della salute degli alleati. La barra viene mostrata nella stessa posizione della barra della salute dei nemici.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Colore barra alleati\",\n            \"description\": \"Colore utilizzato per la barra della salute degli alleati. La barra viene mostrata nella stessa posizione della barra della salute dei nemici.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Colore barra nemici\",\n            \"description\": \"Colore utilizzato per la barra della salute dei nemici.\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Posizione barra nemici\",\n            \"description\": \"Posizione in cui viene visualizzata la barra della salute dei nemici.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Modalità barra nemici\",\n            \"description\": \"Abilita la visualizzazione della barra della salute per il nemico ingaggiato.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"Colore barra esposizione\",\n            \"description\": \"Colore utilizzato per la barra di esposizione all'acqua fredda.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"Colore barra esposizione\",\n            \"description\": \"Colore utilizzato per la barra di esposizione all'acqua fredda.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"Posizione barra esposizione\",\n            \"description\": \"Posizione in cui viene visualizzata la barra di esposizione all'acqua fredda.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Colore barra salute\",\n            \"description\": \"Colore utilizzato per la barra della salute.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Colore barra salute\",\n            \"description\": \"Colore utilizzato per la barra della salute.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Posizione barra salute\",\n            \"description\": \"Posizione in cui viene visualizzata la barra della salute.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"Colore barra veleno\",\n            \"description\": \"Colore utilizzato per la barra della salute quando Lara è avvelenata.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"Colore barra veleno\",\n            \"description\": \"Colore utilizzato per la barra della salute quando Lara è avvelenata.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"Sfondo inventario\",\n            \"description\": \"Modifica il modo in cui viene visualizzato lo sfondo dell'inventario.\\n\\n- Scuro: TR1 (PC).\\n- Molto scuro: TR1 (PS1).\\n- Statico: TR2 (PC).\\n- Ondeggiante: TR2 (PS1).\\n- Monocromatico: TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"Dissolvenza inventario\",\n            \"description\": \"Abilita o disabilita l'effetto di dissolvenza nell'inventario in gioco. Per funzionare, è necessario che l'opzione \\\"Effetti di dissolvenza\\\" sia abilitata.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Stile menu\",\n            \"description\": \"Modifica la modalità di visualizzazione dei menu.\\n\\n - PC: lo stile dell'interfaccia utente corrisponde alla versione PC.\\n - PS1: lo stile dell'interfaccia utente corrisponde alla versione PS1.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"Sfondo di pausa\",\n            \"description\": \"Modifica il modo in cui viene visualizzato lo sfondo della schermata di pausa.\\n\\n- Scuro: TR1 (PC).\\n- Molto scuro: TR1 (PS1).\\n- Statico: TR2 (PC).\\n- Ondeggiante: TR2 (PS1).\\n- Monocromatico: TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"Dissolvenza in pausa\",\n            \"description\": \"Abilita o disabilita l'effetto di dissolvenza nella schermata di pausa. Per funzionare, è necessario che l'opzione \\\"Effetti di dissolvenza\\\" sia abilitata.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"Scala oggetti raccolti\",\n            \"description\": \"Modifica la dimensione degli oggetti animati visualizzati nell'interfaccia utente quando Lara raccoglie qualcosa.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"Mostra barre\",\n            \"description\": \"Disabilita tutte le barre di gioco, nascondendo le informazioni sulla salute di Lara e altre risorse (per le sfide).\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Notifiche raccolta\",\n            \"description\": \"Mostra gli oggetti nell'angolo in basso a destra quando Lara raccoglie qualcosa.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"Mostra versione\",\n            \"description\": \"Mostra la versione di TRX nel menu principale.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"Colore barra resistenza\",\n            \"description\": \"Colore della barra della resistenza visualizzata durante uno scatto.\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"Colore barra resistenza\",\n            \"description\": \"Colore della barra della resistenza visualizzata durante uno scatto.\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"Posizione barra resistenza\",\n            \"description\": \"Posizione in cui viene visualizzata la barra della resistenza.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"Munizioni usate\",\n            \"description\": \"Mostra le informazioni sulle munizioni usate e dei colpi a segno nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"Cristalli\",\n            \"description\": \"Mostra le informazioni sui cristalli usati o raccolti nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"Morti\",\n            \"description\": \"Mostra il conteggio delle morti di Lara nella bussola e nelle statistiche del livello. Il conteggio viene aggiornato nel salvataggio attualmente caricato non appena Lara muore.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"Distanza percorsa\",\n            \"description\": \"Mostra le informazioni sulla distanza percorsa nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"Uccisioni\",\n            \"description\": \"Mostra le informazioni sulle uccisioni nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"Indicatore livello\",\n            \"description\": \"Mostra il numero del livello attuale nella parte superiore delle statistiche del livello.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"Kit medici usati\",\n            \"description\": \"Mostra le informazioni sui kit medici usati nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"Oggetti raccolti\",\n            \"description\": \"Mostra le informazioni sugli oggetti raccolti nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"Segreti scoperti\",\n            \"description\": \"Mostra le informazioni sui segreti scoperti nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"Tempo impiegato\",\n            \"description\": \"Mostra il tempo impiegato nelle statistiche del livello.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"Mostra totali\",\n            \"description\": \"Mostra i totali accanto alle statistiche, ove applicabile. I segreti non sono influenzati da questa impostazione.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"Stile statistiche\",\n            \"description\": \"Controlla come viene visualizzata la finestra delle statistiche.\\n\\n- Senza bordi: mostra le informazioni come un semplice elenco, senza bordi.\\n- Con bordi: mostra le informazioni all'interno di una finestra.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"Sfondo statistiche\",\n            \"description\": \"Modifica il modo in cui viene visualizzato lo sfondo delle statistiche di fine livello.\\n\\n- Scuro: TR1 (PC).\\n- Molto scuro: TR1 (PS1).\\n- Statico: TR2 (PC).\\n- Ondeggiante: TR2 (PS1).\\n- Monocromatico: TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"Dissolvenza statistiche\",\n            \"description\": \"Abilita o disabilita l'effetto di dissolvenza nella schermata delle statistiche di fine livello. Per funzionare, è necessario che l'opzione \\\"Effetti di dissolvenza\\\" sia abilitata.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Scala testo\",\n            \"description\": \"Modifica la dimensione del testo dell'interfaccia utente.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"\\\\{review}Effetti sangue\",\n            \"description\": \"\\\\{review}Controlla i colori delle scintille di sangue.\\n\\n- Disabilitato: nessuna scintilla di sangue viene mostrata.\\n- Rosa: il default nelle versioni tedesche per PC di TR3.\\n- Rosso: il default in tutte le altre versioni retail.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Modalità telecamera\",\n            \"description\": \"Regola il comportamento della telecamera durante azioni come l'uso degli oggetti chiave.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"Oggetti 3D\",\n            \"description\": \"Sostituisce gli oggetti recuperabili 2D con i rispettivi modelli 3D.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Treccia di Lara\",\n            \"description\": \"Abilita la treccia di Lara.\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Brezza\",\n            \"description\": \"Abilita l'effetto brezza sulla treccia di Lara nelle stanze appropriate.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Dissolvenza in uscita\",\n            \"description\": \"Abilita l'effetto di dissolvenza quando si esce dal gioco.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Effetti di dissolvenza\",\n            \"description\": \"Abilita le transizioni in dissolvenza, ad esempio tra le immagini dei titoli di coda o tra le schermate di inventario e pausa.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"Illuminazione fuoco\",\n            \"description\": \"Abilita l'illuminazione dinamica accanto alle sorgenti di fuoco attive.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"Impronte\",\n            \"description\": \"Abilita, nei livelli supportati, la visualizzazione delle impronte di Lara su alcune superfici.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"Inquadratura fluida\",\n            \"description\": \"Abilita lo spostamento fluido dell'inquadratura delle telecamere fisse che osservano Lara. Se disabilitato, tali telecamere sposteranno immediatamente l'inquadratura su Lara.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Illuminazione spari\",\n            \"description\": \"Abilita l'illuminazione dinamica per spari ed esplosioni.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"Tinta cristalli PS1\",\n            \"description\": \"I cristalli di salvataggio saranno di color viola, più simili a quelli della versione PS1.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Riflessi\",\n            \"description\": \"Abilita i riflessi su alcuni oggetti.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"Colorazione accurata mesh\",\n            \"description\": \"Consente di renderizzare con la tonalità dell'acqua solo le parti del corpo di Lara che sono immerse (come in TR3). Se disabilitato, quando Lara è in acqua, tutto il suo corpo verra renderizzato con la tonalità dell'acqua (come in TR1/2).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Lampo sparo fucile\",\n            \"description\": \"Mostra il lampo dello sparo quando Lara fa fuoco con il fucile a pompa, come per le altre armi.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Cieli\",\n            \"description\": \"Abilita il cielo nei livelli supportati.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"Tempo meteorologico\",\n            \"description\": \"Abilita, nei livelli supportati, la visualizzazione degli effetti atmosferici.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Correggi animazioni sprite\",\n            \"description\": \"Corregge gli sprite originali delle piante idrofite in modo che si animino correttamente nelle aree acquatiche.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Correggi rotazione oggetti\",\n            \"description\": \"Risolve i problemi relativi all'orientamento errato di alcuni oggetti quando viene utilizzata l'opzione Oggetti 3D nei livelli originali.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Correggi problemi texture\",\n            \"description\": \"Risolve i problemi riguardanti texture o maglie poligonali mancanti o errate nei livelli originali.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"Colore nebbia\",\n            \"description\": \"Colore della nebbia.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Fine nebbia\",\n            \"description\": \"Imposta la distanza nelle piastrelle in cui la nebbia oscura completamente ogni cosa.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Inizio nebbia\",\n            \"description\": \"Imposta la distanza nelle piastrelle in cui inizia ad apparire la nebbia.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"Trasparenza nebbia\",\n            \"description\": \"Abilita la fusione di geometrie distanti con superfici completamente trasparenti.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"Campo visivo\",\n            \"description\": \"Angolo di visione in gradi. Valori più grandi ampliano il campo visivo, valori più piccoli lo restringono.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Luminosità\",\n            \"description\": \"Modifica la luminosità del gioco.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"Gamma\",\n            \"description\": \"Regola la curva gamma. Valori più alti restituiscono una luminosità maggiore. Il valore 2,5 equivale al livello dei colori predefinito.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"Costume di Lara\",\n            \"description\": \"Modifica l’aspetto di Lara. Se si seleziona \\\"Predefinito\\\", verranno rispettati i normali cambi di costume tra i livelli; in caso contrario, il costume scelto resterà attivo finché non verrà modificato manualmente.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"Forma delle ombre\",\n            \"description\": \"Seleziona la modalità di renderizzazione delle ombre dei personaggi.\\n\\n- Ottagono: ombre in stile TR1 e TR2\\n- Cerchio: ombre rotonde\\n- Sprite: ombre basate su texture come in TR3\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"Occhiali di Lara\",\n            \"description\": \"Modifica lo stile degli occhiali da sole di Lara. Nota: le lenti saranno riflettenti se l’opzione corrispondente è abilitata.\\n\\n- Disattivato: Lara non indosserà gli occhiali da sole.\\n- Opaco: Gli occhiali da sole di Lara avranno lenti opache.\\n- Trasparente: Gli occhiali da sole di Lara avranno lenti semi-trasparenti.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"Luminosità interfaccia\",\n            \"description\": \"Modifica la luminosità dell'interfaccia.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Colore acqua\",\n            \"description\": \"Colore dell'acqua.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"Allarme\",\n        },\n        \"alligator\": {\n            \"name\": \"Alligatore\",\n        },\n        \"alphabet\": {\n            \"name\": \"Carattere Predefinito\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"Carattere Piccolo\",\n        },\n        \"amber_light\": {\n            \"name\": \"Luce Ambra\",\n        },\n        \"animating_1\": {\n            \"name\": \"Oggetto Animato 1\",\n        },\n        \"animating_10\": {\n            \"name\": \"\\\\{review}Oggetto Animato 10\",\n        },\n        \"animating_2\": {\n            \"name\": \"Oggetto Animato 2\",\n        },\n        \"animating_3\": {\n            \"name\": \"Oggetto Animato 3\",\n        },\n        \"animating_4\": {\n            \"name\": \"Oggetto Animato 4\",\n        },\n        \"animating_5\": {\n            \"name\": \"Oggetto Animato 5\",\n        },\n        \"animating_6\": {\n            \"name\": \"Oggetto Animato 6\",\n        },\n        \"animating_7\": {\n            \"name\": \"\\\\{review}Oggetto Animato 7\",\n        },\n        \"animating_8\": {\n            \"name\": \"\\\\{review}Oggetto Animato 8\",\n        },\n        \"animating_9\": {\n            \"name\": \"\\\\{review}Oggetto Animato 9\",\n        },\n        \"ape\": {\n            \"name\": \"Scimmia\",\n        },\n        \"area_51_rocket\": {\n            \"name\": \"Razzo Area 51\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"Esplosione Razzo Area 51\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"Supporto Razzo Area 51\",\n        },\n        \"assault_digits\": {\n            \"name\": \"Cifre Corso d'Addestramento\",\n        },\n        \"assault_target\": {\n            \"name\": \"Obiettivo Corso di Addestramento\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"\\\\{review}Atlantideo Alato\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"\\\\{review}Atlantideo (Che Spara)\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"\\\\{review}Atlantideo Terrestre\",\n        },\n        \"autos\": {\n            \"name\": \"Pistole Automatiche\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"Caricatori per Pistole Automatiche\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Sosia di Lara\",\n        },\n        \"baldy\": {\n            \"name\": \"Il Calvo\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"Mercenario 1\",\n                \"Scagnozzo Mascherato 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"Mercenario 2\",\n                \"Scagnozzo Mascherato 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"Mercenario 3\",\n                \"Scagnozzo Mascherato 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"Barracuda\",\n        },\n        \"bartoli\": {\n            \"name\": \"Marco Bartoli\",\n        },\n        \"bat\": {\n            \"name\": \"Pipistrello\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"Emettitore di Pipistrelli\",\n        },\n        \"beacon_light\": {\n            \"name\": \"Luce Segnaletica\",\n        },\n        \"bear\": {\n            \"name\": \"Orso\",\n        },\n        \"bell\": {\n            \"name\": \"Campana\",\n        },\n        \"big_bowl\": {\n            \"name\": \"Ciotola di Lava\",\n        },\n        \"big_eel\": {\n            \"name\": \"Anguilla Grande\",\n        },\n        \"big_pod\": {\n            \"name\": \"Guscio Grande\",\n        },\n        \"big_spider\": {\n            \"name\": \"Ragno Gigante\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"Uccello Guardiano\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"Gocce d'Acqua\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"Uccelli Canterini\",\n        },\n        \"blade\": {\n            \"name\": \"Lama a Parete\",\n        },\n        \"blood\": {\n            \"name\": \"\\\\{review}Sangue\",\n        },\n        \"blood_pink\": {\n            \"name\": \"\\\\{review}Sangue (censurato)\",\n        },\n        \"blue_light\": {\n            \"name\": \"Luce Blu\",\n        },\n        \"boat\": {\n            \"name\": \"Barca\",\n        },\n        \"boat_bits\": {\n            \"name\": \"Pezzi di Barca\",\n        },\n        \"body_part\": {\n            \"name\": \"Parte del Corpo\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"Ponte Piano\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"Ponte Inclinato 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"Ponte Inclinato 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"Bolla 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Bolla 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"Emettitore di Bolle\",\n        },\n        \"camera_target\": {\n            \"name\": \"Obiettivo Telecamera\",\n        },\n        \"carcass\": {\n            \"name\": \"Carcassa\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"Soffitto con Spuntoni\",\n        },\n        \"centaur\": {\n            \"name\": \"Centauro\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Statua Centauro\",\n        },\n        \"civilian\": {\n            \"name\": \"Civile\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"\\\\{review}Mutante Artigliato\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"Orologio Covo di Bartoli\",\n        },\n        \"cog_1\": {\n            \"name\": \"Ingranaggio 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Ingranaggio 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Ingranaggio 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"Fine Combattimento\",\n        },\n        \"compass\": {\n            \"name\": \"Bussola\",\n        },\n        \"compy\": {\n            \"name\": \"Compsognathus\",\n        },\n        \"controls\": {\n            \"name\": \"Comandi\",\n        },\n        \"copter\": {\n            \"name\": \"Elicottero\",\n        },\n        \"cowboy\": {\n            \"name\": \"Il Cowboy\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"\\\\{review}Mutante Strisciante\",\n        },\n        \"crocodile\": {\n            \"name\": \"Coccodrillo\",\n        },\n        \"crow\": {\n            \"name\": \"Corvo\",\n        },\n        \"cult_1\": {\n            \"name\": \"Scagnozzo Mascherato 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"Scagnozzo Mascherato 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"Scagnozzo Mascherato 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"Lanciatore di Coltelli\",\n        },\n        \"cult_3\": {\n            \"name\": \"Scagnozzo con Fucile\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"Animazione Fucile nella Doccia\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Spada di Damocle\",\n        },\n        \"dart\": {\n            \"name\": \"Dardo\",\n        },\n        \"dart_effect\": {\n            \"name\": \"Effetto Dardo\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Emettitore di Dardi\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"Desert Eagle\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"Caricatori per Desert Eagle\",\n        },\n        \"detonator_box\": {\n            \"name\": \"Detonatore\",\n        },\n        \"ding_dong\": {\n            \"name\": \"Campanello\",\n        },\n        \"dino_mutant\": {\n            \"name\": \"Dinosauro Mutante\",\n        },\n        \"disc\": {\n            \"name\": \"Disco\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"Emettitore di Dischi\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"\\\\{review}Animazione Usa e Getta 9\",\n        },\n        \"diver\": {\n            \"name\": \"Subacqueo\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"Cane\",\n                \"Dobermann\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Porta 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Porta 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Porta 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Porta 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Porta 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Porta 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Porta 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Porta 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"Retro Drago\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"Segnaposto\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"Fronte Ossa del Drago\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"Retro Ossa del Drago\",\n        },\n        \"dragon_front\": {\n            \"name\": \"Fronte Drago\",\n        },\n        \"drawbridge\": {\n            \"name\": \"Ponte Levatoio\",\n        },\n        \"dust\": {\n            \"name\": \"Polvere\",\n        },\n        \"dying_monk\": {\n            \"name\": \"Monaco Morente\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"\\\\{review}Mutante Morente\",\n        },\n        \"eagle\": {\n            \"name\": \"Aquila\",\n        },\n        \"earthquake\": {\n            \"name\": \"Terremoto\",\n        },\n        \"eel\": {\n            \"name\": \"Anguilla\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"Pulitore Elettrico\",\n        },\n        \"electric_fence\": {\n            \"name\": \"Recinzione Elettrica\",\n        },\n        \"electrical_light\": {\n            \"name\": \"Luce Elettrica\",\n        },\n        \"ember\": {\n            \"name\": \"Brace\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"Emettitore di Brace\",\n        },\n        \"explosion_1\": {\n            \"name\": \"Esplosione 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Esplosione 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": [\n                \"Blocco Cedevole 1\",\n                \"Pavimento Fragile 1\",\n                \"Piastrelle Fragili 1\",\n            ]\n        },\n        \"falling_block_2\": {\n            \"name\": [\n                \"Blocco Cedevole 2\",\n                \"Pavimento Fragile 2\",\n                \"Piastrelle Fragili 2\",\n            ]\n        },\n        \"falling_block_3\": {\n            \"name\": [\n                \"Blocco Cedevole 3\",\n                \"Pavimento Fragile 3\",\n                \"Piastrelle Fragili 3\",\n                \"Tavole Allentate\",\n            ]\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"Soffitto Cedevole 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Soffitto Cedevole 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"\\\\{review}Testa di Fuoco\",\n        },\n        \"fish_mutant\": {\n            \"name\": \"Pesce Mutante\",\n        },\n        \"flame\": {\n            \"name\": [\n                \"Fiamma\",\n                \"Fuoco\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"Emettitore di Fiamme\",\n                \"Emettitore di Fuoco\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": [\n                \"Emettitore di Fiamme (Grande)\",\n                \"Emettitore di Fuoco (Grande)\",\n            ]\n        },\n        \"flame_emitter_jet\": {\n            \"name\": [\n                \"Emettitore di Fiamme (Getto)\",\n                \"Emettitore di Fuoco (Getto)\",\n            ]\n        },\n        \"flame_emitter_side\": {\n            \"name\": [\n                \"Emettitore di Fiamme (Laterale)\",\n                \"Emettitore di Fuoco (Laterale)\",\n            ]\n        },\n        \"flame_emitter_small\": {\n            \"name\": [\n                \"Emettitore di Fiamme (Piccolo)\",\n                \"Emettitore di Fuoco (Piccolo)\",\n            ]\n        },\n        \"flare\": {\n            \"name\": \"Razzo di Segnalazione\",\n        },\n        \"flare_fire\": {\n            \"name\": \"Scintille Fiamma\",\n        },\n        \"flares_box\": {\n            \"name\": \"Scatola Razzi\",\n        },\n        \"flickering_light\": {\n            \"name\": \"\\\\{review}Luce Tremolante\",\n        },\n        \"fuse_box\": {\n            \"name\": \"Scatola Fusibili\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"Disco Grigio\",\n        },\n        \"gamma\": {\n            \"name\": \"Gamma\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"Emettitore di Gas (Verde)\",\n        },\n        \"general\": {\n            \"name\": \"Minisommergibile\",\n        },\n        \"globe\": {\n            \"name\": \"Globo\",\n        },\n        \"glow\": {\n            \"name\": \"Bagliore\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"Bagliore Mappa\",\n        },\n        \"gondola\": {\n            \"name\": \"Gondola\",\n        },\n        \"gong\": {\n            \"name\": \"Gong\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"Martello del Gong\",\n        },\n        \"graphics\": {\n            \"name\": \"Grafica\",\n        },\n        \"green_light\": {\n            \"name\": \"Luce Verde\",\n        },\n        \"grenade\": {\n            \"name\": \"Granata\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"Lanciagranate\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"Granate\",\n        },\n        \"gun_flash\": {\n            \"name\": \"Lampo Pistola\",\n        },\n        \"gun_shell\": {\n            \"name\": \"Bossolo di Proiettile\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"Dardo Arpione\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"Fucile Subacqueo\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"Arpioni\",\n        },\n        \"hook\": {\n            \"name\": \"Gancio\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"Fuoco Extra\",\n        },\n        \"huskie\": {\n            \"name\": [\n                \"Cane\",\n                \"Cane da Pattuglia\",\n                \"Husky\",\n            ]\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"\\\\{review}Mutante Ibrido\",\n        },\n        \"icicle\": {\n            \"name\": \"Ghiaccioli\",\n        },\n        \"inv_background\": {\n            \"name\": \"Sfondo Menu\",\n        },\n        \"jelly\": {\n            \"name\": \"Medusa\",\n        },\n        \"kayak\": {\n            \"name\": \"Kayak\",\n        },\n        \"key_1\": {\n            \"name\": \"Chiave 1\",\n        },\n        \"key_2\": {\n            \"name\": \"Chiave 2\",\n        },\n        \"key_3\": {\n            \"name\": \"Chiave 3\",\n        },\n        \"key_4\": {\n            \"name\": \"Chiave 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"Serratura 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"Serratura 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"Serratura 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"Serratura 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"Eliminazione totale attivata\",\n        },\n        \"killer_statue\": {\n            \"name\": \"Statua con Spada\",\n        },\n        \"lara\": {\n            \"name\": \"Lara\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"Campanello d'Allarme\",\n        },\n        \"lara_autos\": {\n            \"name\": \"Animazione Pistole Automatiche\",\n        },\n        \"lara_boat\": {\n            \"name\": \"Animazione Barca\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"Animazione Desert Eagle\",\n        },\n        \"lara_extra\": {\n            \"name\": \"Animazione Aggiuntiva di Lara\",\n        },\n        \"lara_flare\": {\n            \"name\": \"Animazione Razzo di Segnalazione\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"Animazione Lanciagranate\",\n        },\n        \"lara_hair\": {\n            \"name\": \"Treccia di Lara\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"Animazione Fucile Subacqueo\",\n        },\n        \"lara_m16\": {\n            \"name\": \"Animazione M16\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"Animazione Magnum\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"Animazione MP5\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"Animazione Pistole\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"Animazione Lanciarazzi\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"Animazione Fucile a Pompa\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"Animazione Motoslitta\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"Animazione Uzi\",\n        },\n        \"large_medipack\": {\n            \"name\": \"Kit Medico Grande\",\n        },\n        \"larson\": {\n            \"name\": \"Larson\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Cascata di Lava\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Barra di Piombo\",\n        },\n        \"lift\": {\n            \"name\": \"Ascensore\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Emettitore di Fulmini\",\n        },\n        \"lion\": {\n            \"name\": \"Leone\",\n        },\n        \"lioness\": {\n            \"name\": [\n                \"Leonessa\",\n                \"Leone\",\n            ]\n        },\n        \"lizard\": {\n            \"name\": \"Lucertola\",\n        },\n        \"m16\": {\n            \"name\": \"M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"Caricatori per M16\",\n        },\n        \"m16_flash\": {\n            \"name\": \"Lampo M16\",\n        },\n        \"magnums\": {\n            \"name\": \"Magnum\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Caricatori per Magnum\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"Cambio Costume 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"Cambio Costume 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"Cambio Costume 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Mano di Mida\",\n        },\n        \"mine\": {\n            \"name\": \"Miniera Acquatica\",\n        },\n        \"mine_cart\": {\n            \"name\": \"\\\\{review}Vagoncino da miniera\",\n        },\n        \"mini_copter\": {\n            \"name\": \"Elicottero 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"Missile (Bomba atlantidea)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"Missile (Scheggia atlantidea)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"Missile (Fiamma)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"Missile (Arpione)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"Missile (Coltello)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"Missile (Veleno)\",\n        },\n        \"monk_1\": {\n            \"name\": \"Monaco 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"Monaco 2\",\n        },\n        \"monkey\": {\n            \"name\": \"Scimmia\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"Mitragliatrice Fissa\",\n        },\n        \"mouse\": {\n            \"name\": \"Ratto\",\n        },\n        \"movable_block_1\": {\n            \"name\": [\n                \"Blocco Spostabile 1\",\n                \"Blocco Movibile 1\",\n            ]\n        },\n        \"movable_block_2\": {\n            \"name\": [\n                \"Blocco Spostabile 2\",\n                \"Blocco Movibile 2\",\n            ]\n        },\n        \"movable_block_3\": {\n            \"name\": [\n                \"Blocco Spostabile 3\",\n                \"Blocco Movibile 3\",\n            ]\n        },\n        \"movable_block_4\": {\n            \"name\": [\n                \"Blocco Spostabile 4\",\n                \"Blocco Movibile 4\",\n            ]\n        },\n        \"moving_bar\": {\n            \"name\": \"Barra Mobile\",\n        },\n        \"mp5\": {\n            \"name\": \"MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"Caricatori per MP5\",\n        },\n        \"mp_1\": {\n            \"name\": \"MP 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"MP 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Mummia\",\n        },\n        \"natla\": {\n            \"name\": \"Natla\",\n        },\n        \"natla_gun\": {\n            \"name\": \"Arma di Natla\",\n        },\n        \"on_off_light\": {\n            \"name\": \"Luce Accesa/Spenta\",\n        },\n        \"orca\": {\n            \"name\": \"Orca\",\n        },\n        \"passport\": {\n            \"name\": \"Partita\",\n        },\n        \"patrol_dog\": {\n            \"name\": [\n                \"Cane\",\n                \"Cane da Pattuglia\",\n            ]\n        },\n        \"pda\": {\n            \"name\": \"Opzioni di Gioco\",\n        },\n        \"pendulum_1\": {\n            \"name\": [\n                \"Pendolo\",\n                \"Sacco di Sabbia\",\n                \"Scatola Oscillante\",\n            ]\n        },\n        \"pendulum_2\": {\n            \"name\": [\n                \"Pendolo\",\n                \"Sacco di Sabbia\",\n                \"Scatola Oscillante\",\n            ]\n        },\n        \"photo\": {\n            \"name\": \"Casa di Lara\",\n        },\n        \"pickup_1\": {\n            \"name\": \"Oggetto Recuperabile 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"Oggetto Recuperabile 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Segnalazione Oggetti\",\n        },\n        \"pierre\": {\n            \"name\": \"Pierre\",\n        },\n        \"pirahnas\": {\n            \"name\": \"Piranha\",\n        },\n        \"pistols\": {\n            \"name\": \"Pistole\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"Caricatori per Pistole\",\n        },\n        \"player_1\": {\n            \"name\": \"Attore Intermezzo 1\",\n        },\n        \"player_10\": {\n            \"name\": \"Attore Intermezzo 10\",\n        },\n        \"player_2\": {\n            \"name\": \"Attore Intermezzo 2\",\n        },\n        \"player_3\": {\n            \"name\": \"Attore Intermezzo 3\",\n        },\n        \"player_4\": {\n            \"name\": \"Attore Intermezzo 4\",\n        },\n        \"player_5\": {\n            \"name\": \"Attore Intermezzo 5\",\n        },\n        \"player_6\": {\n            \"name\": \"Attore Intermezzo 6\",\n        },\n        \"player_7\": {\n            \"name\": \"Attore Intermezzo 7\",\n        },\n        \"player_8\": {\n            \"name\": \"Attore Intermezzo 8\",\n        },\n        \"player_9\": {\n            \"name\": \"Attore Intermezzo 9\",\n        },\n        \"pods\": {\n            \"name\": \"Guscio\",\n        },\n        \"poison_dart\": {\n            \"name\": \"Dardo Avvelenato\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"Emettitore di Dardi Avvelenati\",\n        },\n        \"portacabin\": {\n            \"name\": \"Cabina Portatile\",\n        },\n        \"power_saw\": {\n            \"name\": \"Sega Elettrica\",\n        },\n        \"prisoner\": {\n            \"name\": \"Prigioniero\",\n        },\n        \"propeller_1\": {\n            \"name\": \"Elica Aeroplano\",\n        },\n        \"propeller_2\": {\n            \"name\": \"Elica Sott'acqua\",\n        },\n        \"propeller_3\": {\n            \"name\": \"Ventilatore d'Aria\",\n        },\n        \"pulse_light\": {\n            \"name\": \"Luce a Impulsi\",\n        },\n        \"puma\": {\n            \"name\": \"Puma\",\n        },\n        \"punk_1\": {\n            \"name\": \"Punk 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"Punk 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"Oggetto Enigma 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"Oggetto Enigma 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"Oggetto Enigma 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"Oggetto Enigma 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"Serratura Enigma 1 (Usato)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"Serratura Enigma 2 (Usato)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"Serratura Enigma 3 (Usato)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"Serratura Enigma 4 (Usato)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"Serratura Enigma 1 (Vuoto)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"Serratura Enigma 2 (Vuoto)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"Serratura Enigma 3 (Vuoto)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"Serratura Enigma 4 (Vuoto)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"Quad\",\n        },\n        \"quest_1\": {\n            \"name\": \"Oggetto Missione 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"Oggetto Missione 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"Oggetto Missione 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"Oggetto Missione 4\",\n        },\n        \"raptor\": {\n            \"name\": \"Velociraptor\",\n        },\n        \"raptor_emitter\": {\n            \"name\": \"Emettitore di Velociraptor\",\n        },\n        \"rat\": {\n            \"name\": [\n                \"Ratto\",\n                \"Ratto Terrestre\",\n            ]\n        },\n        \"red_light\": {\n            \"name\": \"Luce Rossa\",\n        },\n        \"rib\": {\n            \"name\": \"\\\\{review}RIB\",\n        },\n        \"ricochet\": {\n            \"name\": \"Rimbalzo\",\n        },\n        \"rocket\": {\n            \"name\": \"Razzo\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"Lanciarazzi\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"Razzi\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": [\n                \"Masso 1\",\n                \"Masso Rotolante 1\",\n            ]\n        },\n        \"rolling_ball_2\": {\n            \"name\": [\n                \"Masso 2\",\n                \"Masso Rotolante 2\",\n            ]\n        },\n        \"rolling_ball_3\": {\n            \"name\": [\n                \"Masso 3\",\n                \"Masso Rotolante 3\",\n            ]\n        },\n        \"rolling_ball_4\": {\n            \"name\": [\n                \"Masso 4\",\n                \"Masso Rotolante 4\",\n            ]\n        },\n        \"rotating_laser\": {\n            \"name\": \"Laser rotante\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"\\\\{review}RX Operaio 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"\\\\{review}RX Operaio 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": \"\\\\{review}RX Operaio 3\",\n        },\n        \"save_crystal\": {\n            \"name\": \"Cristallo di Salvataggio\",\n        },\n        \"scion\": {\n            \"name\": \"Scion\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Supporto Scion\",\n        },\n        \"secret_1\": {\n            \"name\": \"Segreto 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"Segreto 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"Segreto 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"Guardia di Sicurezza\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"Laser di Sicurezza (Allarme)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"Laser di Sicurezza (Lesivo)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"Laser di Sicurezza (Letale)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"Torretta di Guardia\",\n        },\n        \"shadow\": {\n            \"name\": \"Ombra\",\n        },\n        \"shark\": {\n            \"name\": \"Squalo\",\n        },\n        \"shiva\": {\n            \"name\": \"Shiva\",\n        },\n        \"shotgun\": {\n            \"name\": \"Fucile a Pompa\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"Cartucce per Fucile a Pompa\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"Bossolo di Cartuccia\",\n        },\n        \"skate_kid\": {\n            \"name\": \"Skate Kid\",\n        },\n        \"skateboard\": {\n            \"name\": \"Skateboard\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"Motoslitta Nera\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"Pilota Motoslitta Nera\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"Motoslitta Rossa\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"Tracciato Motoslitta\",\n        },\n        \"skybox\": {\n            \"name\": \"Cielo\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Pilastro Scorrevole\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kit Medico Piccolo\",\n        },\n        \"smashable_1\": {\n            \"name\": \"Finestra Frangibile 1\",\n        },\n        \"smashable_2\": {\n            \"name\": \"Finestra Frangibile 2\",\n        },\n        \"smashable_3\": {\n            \"name\": \"Finestra Frangibile 3\",\n        },\n        \"smashable_4\": {\n            \"name\": \"Finestra Frangibile 4\",\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"Emettitore di Fumo (Nero)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"Emettitore di Fumo (Bianco)\",\n        },\n        \"snake\": {\n            \"name\": \"Serpente\",\n        },\n        \"snow_sprite\": {\n            \"name\": \"Scia Motoslitta\",\n        },\n        \"sophia\": {\n            \"name\": \"Sophia\",\n        },\n        \"sound\": {\n            \"name\": \"Suono\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"Esplosione Drago 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"Esplosione Drago 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"Esplosione Drago 3\",\n        },\n        \"spider\": {\n            \"name\": \"Ragno\",\n        },\n        \"spike_wall\": {\n            \"name\": \"Muro con Spuntoni\",\n        },\n        \"spikes\": {\n            \"name\": \"Spuntoni\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"Lama Rotante\",\n        },\n        \"splash_1\": {\n            \"name\": \"Increspatura Acqua 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Increspatura Acqua 2\",\n        },\n        \"springboard\": {\n            \"name\": \"Trampolino\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"Emettitore di Vapore\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"Mercenario del Sud Pacifico\",\n        },\n        \"stopwatch\": {\n            \"name\": \"Statistiche\",\n        },\n        \"strobe_light\": {\n            \"name\": \"Luce Stroboscopica\",\n        },\n        \"swat_1\": {\n            \"name\": \"SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"Ascia Oscillante\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"Interruttore Camera Stagna\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"Pulsante\",\n                \"Pulsante a Pressione\",\n                \"Interruttore\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"Leva\",\n                \"Interruttore\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"Interruttore Piccolo\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"Leva Subacquea\",\n                \"Interruttore Subacqueo\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": [\n                \"Interruttore Rotante\",\n                \"Interruttore a Puleggia\",\n                \"Interruttore a Valvola\",\n            ]\n        },\n        \"teeth_trap\": {\n            \"name\": [\n                \"Trappola con Spuntoni\",\n                \"Porta che sbatte\",\n            ]\n        },\n        \"text_box\": {\n            \"name\": \"Casella Interfaccia\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Manico del Martello di Thor\",\n        },\n        \"thors_head\": {\n            \"name\": \"Martello di Thor\",\n        },\n        \"tiger\": {\n            \"name\": \"Tigre\",\n        },\n        \"tony\": {\n            \"name\": \"Tony\",\n        },\n        \"torso\": {\n            \"name\": [\n                \"Torso\",\n                \"Adam\",\n                \"Mutante Gigante\",\n            ]\n        },\n        \"train\": {\n            \"name\": \"Treno\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Botola 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Botola 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Botola 3\",\n        },\n        \"trex\": {\n            \"name\": \"T-Rex\",\n        },\n        \"trex_alpha\": {\n            \"name\": \"T-Rex Alpha\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"Indigeno con Ascia\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"Capo Tribù\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"Indigeno con Cerbottana\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"Pesci Tropicali\",\n        },\n        \"twinkle\": {\n            \"name\": \"Scintille\",\n        },\n        \"upv\": {\n            \"name\": \"Mini Sottomarino\",\n        },\n        \"uzis\": {\n            \"name\": \"Uzi\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"Caricatori per Uzi\",\n        },\n        \"vole\": {\n            \"name\": [\n                \"Arvicola\",\n                \"Ratto Acquatico\",\n            ]\n        },\n        \"vulture\": {\n            \"name\": \"Avvoltoio\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"\\\\{review}Mutante Vespa\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"\\\\{review}Emettitore di Mutante Vespa\",\n        },\n        \"water_sprite\": {\n            \"name\": \"Scia Barca\",\n        },\n        \"waterfall\": {\n            \"name\": \"Foschia Cascata\",\n        },\n        \"white_light\": {\n            \"name\": \"Luce Bianca\",\n        },\n        \"willard\": {\n            \"name\": \"\\\\{review}Willard\",\n        },\n        \"winston\": {\n            \"name\": \"Winston\",\n        },\n        \"winston_army\": {\n            \"name\": \"Winston (in uniforme)\",\n        },\n        \"wolf\": {\n            \"name\": \"Lupo\",\n        },\n        \"worker_1\": {\n            \"name\": \"Scagnozzo Pistolero 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"Scagnozzo Pistolero 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"Scagnozzo con Bastone 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"Scagnozzo con Bastone 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"Scagnozzo con Lanciafiamme\",\n        },\n        \"xian_knight\": {\n            \"name\": \"Cavaliere di Xian\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"Statua Cavaliere di Xian\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"Lanciere di Xian\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"Statua Lanciere di Xian\",\n        },\n        \"yeti\": {\n            \"name\": \"Yeti\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"Maniglia Teleferica\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"Polski\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Zbadaj\",\n            \"hide_dialog\": \"Ukryj dialog\",\n            \"reset_defaults\": \"Zresetuj układ\",\n            \"rotate\": \"Obróć\",\n            \"unbind\": \"Usuń przypisanie\",\n            \"use_item\": \"Użyj\",\n        },\n        \"config_presets\": {\n            \"applied\": \"\\\\{review}Ustawienie wstępne zastosowane.\",\n            \"confirm_description\": \"\\\\{review}Następujące ustawienia zostaną zmienione:\",\n            \"confirm_restart_note\": \"\\\\{review}Uwaga: niektóre ustawienia mogą wymagać ponownego uruchomienia gry, aby zmiany weszły w życie.\",\n            \"empty\": \"\\\\{review}Nie znaleziono presetów.\",\n            \"no_changes\": \"\\\\{review}Brak zmian do zastosowania.\",\n            \"title_fmt\": \"\\\\{review}Zastosować ustawienie wstępne %s?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"Obszar 1\",\n            \"area_2\": \"Obszar 2\",\n            \"area_3\": \"Obszar 3\",\n            \"area_4\": \"Obszar 4\",\n            \"area_5\": \"Obszar 5\",\n            \"area_6\": \"Obszar 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"Przygoda\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"KONIEC GRY\",\n            \"heading_inventory\": \"EKWIPUNEK\",\n            \"heading_items\": \"PRZEDMIOTY\",\n            \"heading_option\": \"OPCJE\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Tryb demo\",\n            \"direction_keys_controller\": \"D-Pad\",\n            \"direction_keys_keyboard\": \"Strzałki\",\n            \"empty_slot_fmt\": \"- PUSTY SLOT -\",\n            \"exit\": \"Wyjście\",\n            \"hold_fmt\": \"%s\",\n            \"off\": \"Wyłączone\",\n            \"on\": \"Włączone\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Niejednoznaczne wywołanie: %s i %s\",\n            \"ambiguous_input_3\": \"Niejednoznaczne wywołanie: %s, %s, ...\",\n            \"bilinear_filter_off\": \"Filtr tekstur: punktowy\",\n            \"bilinear_filter_on\": \"Filtr tekstur: dwuliniowy\",\n            \"command_bad_invocation\": \"Nieprawidłowe wywołanie: %s\",\n            \"command_bool\": \"1, 0\",\n            \"command_decimal\": \"[liczba ułamkowa]\",\n            \"command_integer\": \"[liczba całkowita]\",\n            \"command_percent\": \"[procent]\",\n            \"command_unavailable\": \"Ta komenda nie jest obecnie dostępna\",\n            \"command_valid_values\": \"Dopuszczalne wartości: %s\",\n            \"complete_level\": \"Poziom ukończony!\",\n            \"config_option_get\": \"%s jest obecnie ustawione na %s\",\n            \"config_option_set\": \"Zmieniono %s na %s\",\n            \"config_option_unknown_option\": \"Nieznana opcja: %s\",\n            \"current_health_get\": \"Aktualne zdrowie Lary: %d\",\n            \"current_health_set\": \"Zdrowie Lary ustawione na %d\",\n            \"door_close\": \"Sezamie, zamknij się!\",\n            \"door_open\": \"Sezamie, otwórz się!\",\n            \"door_open_fail\": \"Brak drzwi w pobliżu Lary\",\n            \"flipmap_fail_already_off\": \"Flipmapa jest już wyłączona\",\n            \"flipmap_fail_already_on\": \"Flipmapa jest już włączona\",\n            \"flipmap_off\": \"Flipmapa wyłączona\",\n            \"flipmap_on\": \"Flipmapa włączona\",\n            \"fly_mode_off\": \"Tryb latania wyłączony\",\n            \"fly_mode_on\": \"Tryb latania włączony\",\n            \"fps_counter_off\": \"Licznik klatek na sekundę wyłączony\",\n            \"fps_counter_on\": \"Licznik klatek na sekundę włączony\",\n            \"give_item\": \"Dodano %s do ekwipunku Lary\",\n            \"give_item_all_guns\": \"Lara uzbrojona po zęby. Drżyj, świecie!\",\n            \"give_item_all_keys\": \"Niespodzianka! Wszystkie kluczowe przedmioty zmaterializowały się w plecaku Lary.\",\n            \"give_item_cheat\": \"Plecak Lary stał się znacznie cięższy!\",\n            \"heal_already_full_hp\": \"Lara jest już w pełni zdrowa\",\n            \"heal_success\": \"Lara uleczona do pełni zdrowia\",\n            \"invalid_cutscene\": \"Nieprawidłowy przerywnik\",\n            \"invalid_demo\": \"Nieprawidłowe demo\",\n            \"invalid_item\": \"Nieznany przedmiot: %s\",\n            \"invalid_level\": \"Nieznany poziom\",\n            \"invalid_object\": \"Nieznany obiekt\",\n            \"invalid_room\": \"Nieprawidłowe pomieszczenie: %d. Prawidłowe pomieszczenia to 0-%d\",\n            \"invalid_sample\": \"Nieprawidłowy dźwięk: %d\",\n            \"kill\": \"I po krzyku!\",\n            \"kill_all\": \"Sayonara!\",\n            \"kill_all_fail\": \"Uch, już nic nie zostało do zabicia...\",\n            \"kill_fail\": \"Brak wrogów w pobliżu\",\n            \"lighting_contrast_fmt\": \"Kontrast oświetlenia: %s\",\n            \"load_game\": \"Wczytano grę ze slotu %d\",\n            \"load_game_fail_invalid_slot\": \"Nieprawidłowy slot zapisu %d\",\n            \"load_game_fail_unavailable_slot\": \"Slot zapisu %d jest niedostępny\",\n            \"object_not_found\": \"Nie odnaleziono tego obiektu\",\n            \"play_cutscene\": \"Wczytano przerywnik %d\",\n            \"play_demo\": \"Wczytano demo %d\",\n            \"play_level\": \"Wczytano %s\",\n            \"pos_lara_missing\": \"Nie znaleziono Lary\",\n            \"pos_lara_pos_fmt\": \"Pomieszczenie: %d\\nPozycja: %.3f, %.3f, %.3f\\nRotacja: %.3f, %.3f, %.3f\",\n            \"pos_level_fmt\": \"Poziom %d\",\n            \"pos_level_fmt_cutscene\": \"Przerywnik %d\",\n            \"pos_level_fmt_demo\": \"Demo %d\",\n            \"quick_load\": \"Wczytano szybki zapis %d\",\n            \"quick_load_fail_no_bound_slot\": \"Obecnie nie przypisano żadnego zapisu\",\n            \"quick_load_fail_unavailable_bound_slot\": \"Szybkie wczytywanie nie jest dostępne\",\n            \"quick_save\": \"Szybki zapis\",\n            \"quick_save_fail_no_slots\": \"Nie skonfigurowano slotów szybkiego zapisu\",\n            \"save_game\": \"Zapisano grę na slocie zapisu %d\",\n            \"save_game_fail_invalid_slot\": \"Nieprawidłowy slot zapisu %d\",\n            \"sound_available_samples\": \"Dostępne dźwięki: %s\",\n            \"sound_playing_sample\": \"Odtwarzanie dźwięku %d\",\n            \"speed_get\": \"Aktualna prędkość: %d\",\n            \"speed_set\": \"Prędkość ustawiona na %d\",\n            \"strings_failed\": \"Nie udało się przeładować plików językowych\",\n            \"strings_reloaded\": \"Pliki językowe zostały przeładowane\",\n            \"textures_off\": \"Tekstury: wyłączone\",\n            \"textures_on\": \"Tekstury: włączone\",\n            \"trapezoid_filter_off\": \"Filtr trapezoidalny wyłączony\",\n            \"trapezoid_filter_on\": \"Filtr trapezoidalny włączony\",\n            \"ui_off\": \"Interfejs użytkownika wyłączony\",\n            \"ui_on\": \"Interfejs użytkownika włączony\",\n            \"unknown_command\": \"Nieznana komenda: %s\",\n            \"upscaling_factor\": \"Współczynnik skalowania: x%d\",\n            \"wireframe_mode_off\": \"Widok siatki: wyłączony\",\n            \"wireframe_mode_on\": \"Widok siatki: włączony\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"Animacja: \",\n            \"debug_animation_state\": \"Stan: \",\n            \"debug_camera_pos\": \"Pozycja kamery: \",\n            \"debug_camera_target\": \"Cel kamery: \",\n            \"debug_immune\": \"Nieśmiertelność\",\n            \"debug_position\": \"Pozycja: \",\n            \"debug_rotation\": \"Rotacja: \",\n            \"debug_speed\": \"Prędkość: \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"\\\\{review}Usuń\",\n            \"delete_save_confirm\": \"\\\\{review}Usunąć ten zapis?\",\n            \"delete_save_failed\": \"\\\\{review}Nie udało się usunąć wybranego zapisu.\",\n            \"delete_save_no\": \"\\\\{review}Nie\",\n            \"delete_save_yes\": \"\\\\{review}Tak\",\n            \"exit_game\": \"Wyjdź z gry\",\n            \"exit_to_title\": \"Menu główne\",\n            \"load_game\": \"Wczytaj grę\",\n            \"mode_new_game\": \"Nowa gra\",\n            \"mode_new_game_jp\": \"Japońska wersja\",\n            \"mode_new_game_jp_plus\": \"Japońska wersja, NG+\",\n            \"mode_new_game_plus\": \"Nowa gra+\",\n            \"new_game\": \"Nowa gra\",\n            \"play_previous_levels\": \"Zagraj w poprzednie poziomy\",\n            \"restart_level\": \"Restartuj poziom\",\n            \"save_game\": \"Zapisz grę\",\n            \"save_slot_unsupported\": \"Ten zapis nie obsługuje tej funkcji.\",\n            \"select_level\": \"Wybierz poziom\",\n            \"select_mod\": \"\\\\{review}Wybierz grę\",\n            \"select_mode\": \"Wybierz tryb\",\n            \"select_save\": \"Wybierz zapis\",\n            \"story_so_far\": \"Co się wydarzyło do tej pory\",\n            \"switch_mod\": \"\\\\{review}Zmień grę\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"Czy na pewno?\",\n            \"continue\": \"Nie, kontynuuj grę\",\n            \"exit_to_title\": \"Wyjść do menu głównego?\",\n            \"no\": \"Nie\",\n            \"paused\": \"Pauza\",\n            \"quit\": \"Tak, wyjdź\",\n            \"yes\": \"Tak\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"Pomiń klatkę\",\n            \"camera_move_prompt\": \"Przesuń kamerę\",\n            \"camera_reset_prompt\": \"Zresetuj kamerę\",\n            \"camera_roll_prompt\": \"Obróć kamerę w pionie\",\n            \"camera_rotate_90_prompt\": \"Obróć kamerę o 90°\",\n            \"camera_rotate_prompt\": \"Obróć kamerę w poziomie\",\n            \"change_lara_pose\": \"Zmień pozę\",\n            \"fov_prompt\": \"Dostosuj kąt widzenia\",\n            \"lara_move_prompt\": \"Przesuń Larę\",\n            \"lara_reset_prompt\": \"Zresetuj Larę\",\n            \"lara_roll_prompt\": \"Obróć Larę w pionie\",\n            \"lara_rotate_90_prompt\": \"Obróć Larę o 90°\",\n            \"lara_rotate_prompt\": \"Obróć Larę w poziomie\",\n            \"snap_prompt\": \"Zrób zdjęcie\",\n            \"title_camera_pos\": \"Tryb zdjęć\",\n            \"title_lara_pos\": \"Przesuń Larę\",\n            \"toggle_help\": \"Pokaż/ukryj pomoc\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"Ustawienia są wyłączone dla tego zestawu poziomów.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Edytuj wartość\",\n                \"frozen_option_disclaimer\": \"To ustawienie jest wymuszane przez twórcę poziomu i nie może być zmienione.\",\n                \"hue\": \"Odcień\",\n                \"lightness\": \"Jasność\",\n                \"restore_default\": \"Przywróć domyślne\",\n                \"toggle_help\": \"Pokaż pomoc\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Kontroler\",\n                    \"keyboard\": \"Klawiatura\",\n                },\n                \"customize\": \"Dostosuj sterowanie\",\n                \"layout\": {\n                    \"custom_1\": \"Ustawienia użytkownika 1\",\n                    \"custom_2\": \"Ustawienia użytkownika 2\",\n                    \"custom_3\": \"Ustawienia użytkownika 3\",\n                    \"default\": \"Domyślne ustawienia\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Ruch\",\n                    \"items\": \"Przedmioty\",\n                    \"misc\": \"Różne\",\n                    \"system\": \"System\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Sterowanie\",\n                    \"fixes\": \"Poprawki\",\n                    \"general\": \"Ogólne\",\n                    \"mods\": \"Mody\",\n                    \"presets\": \"\\\\{review}Presety\",\n                },\n                \"title\": \"Ustawienia gry\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"Paski\",\n                    \"rendering\": \"Obraz\",\n                    \"stats\": \"\\\\{review}Statystyki\",\n                    \"ui\": \"Interfejs\",\n                    \"visuals\": \"Wygląd\",\n                },\n                \"title\": \"Ustawienia grafiki\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"Różne\",\n                    \"volume\": \"Głośność\",\n                },\n                \"title\": \"Ustawienia dźwięku\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Amunicja/trafienia\",\n            \"ammo_hits\": \"Trafienia\",\n            \"ammo_used\": \"Zużyta amunicja\",\n            \"assault_best_time_fmt\": \"%s\",\n            \"assault_finish\": \"Finisz\",\n            \"assault_no_times_set\": \"Brak czasów\",\n            \"assault_other_times_fmt\": \"%s\",\n            \"assault_title\": \"NAJLEPSZE CZASY\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Statystyki bonusowe\",\n            \"crystals\": \"\\\\{review}Kryształy\",\n            \"deaths\": \"Śmierci\",\n            \"detail_fmt\": \"%d z %d\",\n            \"distance_travelled\": \"Przebyta odległość\",\n            \"final_statistics\": \"Statystyki końcowe\",\n            \"gym_assault_course\": \"Tor Przeszkód\",\n            \"gym_racetrack_course\": \"Tor Wyścigowy\",\n            \"kills\": \"Zabójstwa\",\n            \"level\": \"Poziom\",\n            \"medipacks_used\": \"Zużyte apteczki\",\n            \"none\": \"Brak\",\n            \"pickups\": \"Przedmioty\",\n            \"secrets\": \"Odnalezione sekrety\",\n            \"time_taken\": \"Spędzony czas\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Włącza lub wyłącza warkocz Lary.\",\n            },\n            \"cheats\": {\n                \"help\": \"Włącza lub wyłącza cheaty.\",\n            },\n            \"clear\": {\n                \"help\": \"Czyści logi konsoli.\",\n            },\n            \"debug\": {\n                \"help\": \"Włącza lub wyłącza różne informacje diagnostyczne.\",\n            },\n            \"drain\": {\n                \"help\": \"Osusza aktualne pomieszczenie usuwając z niego wodę.\",\n            },\n            \"end_level\": {\n                \"help\": \"Kończy bieżący poziom.\",\n            },\n            \"exit\": {\n                \"help\": \"Zamyka grę.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Przełącza flipmapę.\",\n            },\n            \"flood\": {\n                \"help\": \"Zalewa aktualne pomieszczenie wodą.\",\n            },\n            \"fly\": {\n                \"help\": \"Włącza lub wyłącza tryb latania.\",\n            },\n            \"fps\": {\n                \"help\": \"Zmienia liczbę klatek na sekundę.\",\n            },\n            \"give\": {\n                \"help\": \"Dodaje dany przedmiot do ekwipunku Lary.\",\n                \"invalid_secret\": \"Nieprawidłowy sekret: %s (prawidłowe sekrety: %s)\",\n                \"secret_given\": \"Dodano sekret %s\",\n                \"secret_list\": \"Zebrane sekrety: %d z %d (%s)\",\n                \"secret_none\": \"Zebrane sekrety: %d z %d\",\n                \"secret_taken\": \"Usunięto sekret %s\",\n            },\n            \"give_secret\": {\n                \"help\": \"Wyświetla sekrety Lary lub zabiera/daje sekret według numeru.\",\n            },\n            \"heal\": {\n                \"help\": \"Leczy Larę do pełni zdrowia.\",\n            },\n            \"help\": {\n                \"help\": \"Pokazuje listę komend lub opis konkretnej komendy.\",\n                \"list\": \"Dostępne komendy:\",\n            },\n            \"hp\": {\n                \"help\": \"Ustawia zdrowie Lary na określoną wartość.\",\n            },\n            \"immune\": {\n                \"help\": \"Przełącza nieśmiertelność. (Lara może być nadal zabita w niektórych okolicznościach.)\",\n                \"off\": \"Lara jest z powrotem wrażliwa na obrażenia\",\n                \"on\": \"Lara jest teraz odporna na obrażenia\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"Przełącza nieskończony sprint.\",\n                \"off\": \"Lara nie może już biegać w nieskończoność\",\n                \"on\": \"Lara może teraz biegać w nieskończoność\",\n            },\n            \"kill\": {\n                \"help\": \"Zabija pobliskich przeciwników.\",\n            },\n            \"lighting\": {\n                \"help\": \"Włącza lub wyłącza system oświetlenia.\",\n            },\n            \"load\": {\n                \"help\": \"Ładuje grę z danego slotu zapisu lub szybkiego zapisu.\",\n            },\n            \"lua\": {\n                \"help\": \"Wykonuje podany kod Lua.\",\n                \"runtime_error\": \"Błąd wykonania: %s\",\n                \"syntax_error\": \"Błąd składni: %s\",\n            },\n            \"mod\": {\n                \"help\": \"\\\\{review}Przełącza na określony mod i ponownie uruchamia grę.\",\n            },\n            \"music\": {\n                \"help\": \"Odtwarza utwór muzyczny o podanym identyfikatorze.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Wczytuje przerywnik o podanym numerze.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Wczytuje demo o podanym numerze.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Wczytuje poziom z domem Lary.\",\n            },\n            \"play_level\": {\n                \"help\": \"Wczytuje poziom o podanej nazwie lub numerze.\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Nieprawidłowa ścieżka muzyczka\",\n                \"stopped\": \"Muzyka zatrzymana\",\n                \"track\": \"Odtwarzanie utworu muzycznego %d\",\n            },\n            \"pos\": {\n                \"help\": \"Pokazuje pozycję Lary.\",\n            },\n            \"save\": {\n                \"help\": \"Zapisuje grę do danego slotu zapisu, lub do pierwszego dostępnego slotu szybkiego zapisu.\",\n            },\n            \"screenshot\": {\n                \"help\": \"Zapisuje zrzut ekranu na dysku w opcjonalnej lokalizacji.\",\n            },\n            \"set\": {\n                \"help\": \"Wyświetla lub aktualizuje dane ustawienie.\",\n            },\n            \"sfx\": {\n                \"help\": \"Odtwarza efekt dźwiękowy o podanym numerze.\",\n            },\n            \"spawn\": {\n                \"fail\": \"Nie udało się przywołać żądanego obiektu\",\n                \"success\": \"Żądany obiekt przywołany w pobliżu Lary\",\n            },\n            \"speed\": {\n                \"help\": \"Zmienia prędkość gry.\",\n            },\n            \"strings\": {\n                \"help\": \"Przeładowuje bieżące pliki językowe z dysku.\",\n            },\n            \"teleport\": {\n                \"item\": \"Teleportowano do obiektu: %d\",\n                \"item_fail\": \"Nie udało się teleportować do obiektu: %d\",\n                \"object\": \"Teleportowano do obiektu: %s\",\n                \"object_fail\": \"Nie znaleziono obiektu: %s\",\n                \"pos\": \"Teleportowano do pozycji: %.3f %.3f %.3f\",\n                \"pos_fail\": \"Nie udało się teleportować do pozycji: %.3f %.3f %.3f\",\n                \"room\": \"Teleportowano do pomieszczenia: %d\",\n                \"room_fail\": \"Nie udało się teleportować do pomieszczenia: %d\",\n            },\n            \"textures\": {\n                \"help\": \"Włącza lub wyłącza rysowanie tekstur.\",\n            },\n            \"title\": {\n                \"help\": \"Wraca do menu głównego.\",\n            },\n            \"tp\": {\n                \"help\": \"Teleportuje Larę do podanej pozycji lub numeru pomieszczenia.\",\n            },\n            \"trigger\": {\n                \"help\": \"Aktywuje lub dezaktywuje element według id, nazwy elementu lub nazwy obiektu.\",\n                \"invalid_item\": \"Nieprawidłowy cel: %s\",\n                \"no_match\": \"Nieznany cel: %s\",\n                \"not_found\": \"Nieznany cel: %s\",\n                \"triggered\": \"Cel aktywowany: %s\",\n                \"untriggered\": \"Cel dezaktywowany: %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Włącza lub wyłącza synchronizację pionową.\",\n            },\n            \"weather\": {\n                \"help\": \"Zmienia aktualną pogodę.\",\n                \"invalid\": \"Nieprawidłowa wartość: %s (prawidłowe: %s)\",\n                \"set\": \"Pogoda ustawiona na %s\",\n            },\n            \"winston\": {\n                \"dead\": \"Twój lokaj jest martwy. Ty potworze!\",\n                \"spawn_failed\": \"Nie udało się przywołać Winstona\",\n                \"spawned\": \"Przywołano Winstona w pobliżu Lary\",\n                \"teleported\": \"Przywołano Winstona w pobliżu Lary\",\n            },\n            \"wireframe\": {\n                \"help\": \"Włącza lub wyłącza rysowanie w widoku siatki.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"\\\\{review}TR1 PC\",\n            \"tr1_ps1\": \"\\\\{review}TR1 PS1\",\n            \"tr2_pc\": \"\\\\{review}TR2 PC\",\n            \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n            \"tr3_pc\": \"\\\\{review}TR3 PC\",\n            \"tr3_ps1\": \"\\\\{review}TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"TR2 PC\",\n                \"tr2_ps1\": \"TR2 PS1\",\n                \"tr3_pc\": \"TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"Domyślny\",\n                \"golden_sophia\": \"\\\\{review}Złota Sophia\",\n                \"sophia\": \"\\\\{review}Sophia\",\n                \"tr1_bacon_lara\": \"TR1 klon Lary\",\n                \"tr1_classic\": \"TR1 klasyczny\",\n                \"tr1_combo\": \"TR1 alternatywny\",\n                \"tr1_golden_bacon_lara\": \"TR1 złoty klon Lary\",\n                \"tr1_golden_lara\": \"TR1 złota Lara\",\n                \"tr1_gym\": \"TR1 dres\",\n                \"tr1_mauled\": \"TR1 sponiewierany\",\n                \"tr1_ngage\": \"\\\\{review}TR1 N-Gage\",\n                \"tr23_golden_lara\": \"TR2/3 złota Lara\",\n                \"tr2_bomber_jacket\": \"TR2 kurtka pilotka\",\n                \"tr2_classic\": \"TR2 klasyczny\",\n                \"tr2_diving_suit\": \"TR2 pianka do pływania 1\",\n                \"tr2_diving_suit_alpha\": \"\\\\{review}TR2 pianka do pływania 2\",\n                \"tr2_gym\": \"TR2 dres\",\n                \"tr2_robe\": \"TR2 szlafrok\",\n                \"tr2_vegas\": \"TR2 Las Vegas\",\n                \"tr3_antarctica\": \"TR3 Antarktyda\",\n                \"tr3_catsuit\": \"TR3 kombinezon\",\n                \"tr3_classic\": \"TR3 klasyczny\",\n                \"tr3_gym\": \"TR3 dres\",\n                \"tr3_nevada\": \"TR3 Nevada\",\n                \"tr3_south_pacific\": \"TR3 Południowy Pacyfik\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"\\\\{review}Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"\\\\{review}Tomb Raider I Demo\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"\\\\{review}Niedokończona sprawa\",\n            },\n            \"tr2\": {\n                \"title\": \"\\\\{review}Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"\\\\{review}Złota Maska\",\n            },\n            \"tr3\": {\n                \"title\": \"\\\\{review}Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"\\\\{review}Zaginiony Artefakt\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"Indywidualne\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"Współdzielone\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"Dowolne\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"Czarne\",\n            \"BK_IMAGE\": \"\\\\{review}Obraz\",\n            \"BK_MONOCHROME\": \"Czarno-białe\",\n            \"BK_MONOCHROME_COOL\": \"Czarno-białe (zimne)\",\n            \"BK_MONOCHROME_WARM\": \"Czarno-białe (ciepłe)\",\n            \"BK_NONE\": \"Przezroczyste\",\n            \"BK_PATTERN_STATIC\": \"Stałe\",\n            \"BK_PATTERN_WAVE\": \"Falujące\",\n            \"BK_TRANSPARENT_DARK\": \"Bardzo ciemne\",\n            \"BK_TRANSPARENT_MEDIUM\": \"Ciemne\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"Zawsze\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"Tylko bossowie\",\n            \"BAR_SHOW_MODE_NEVER\": \"Wyłączony\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"Brak\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"Perspektywa\",\n            \"BILLBOARD_LOCK_ROLL\": \"Obrót\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"Obrót i pochylenie\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"\\\\{review}Wyłączone\",\n            \"BLOOD_EFFECTS_PINK\": \"\\\\{review}Różowy\",\n            \"BLOOD_EFFECTS_RED\": \"\\\\{review}Czerwony\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"\\\\{review}Domyślne\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"\\\\{review}Nigdy\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"\\\\{review}Zanurzone\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"\\\\{review}Kontroler\",\n            \"INPUT_BACKEND_KEYBOARD\": \"\\\\{review}Klawiatura\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Interakcja\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Kamera do tyłu\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Kamera w dół\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Kamera do przodu\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Kamera w lewo\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"Reset kamery\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Kamera w prawo\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Kamera w górę\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"Zmień strój\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Zmień cel\",\n            \"INPUT_ROLE_CROUCH\": \"Kucnij\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"Zmiana kontrastu oświetlenia\",\n            \"INPUT_ROLE_DOWN\": \"Cofanie\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Wyciągnij broń\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Otwórz konsolę\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"Wyposaż pistolety automatyczne\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"Wyposaż Desert Eagle\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"Wyposaż wyrzutnię granatów\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"Wyposaż wyrzutnię harpunów\",\n            \"INPUT_ROLE_EQUIP_M16\": \"Wyposaż M16\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"Wyposaż magnumy\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"Wyposaż MP5\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"Wyposaż pistolety\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"Wyposaż wyrzutnię rakiet\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"Wyposaż strzelbę\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"Wyposaż uzi\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Włącz cheat na latanie\",\n            \"INPUT_ROLE_FPS\": \"Licznik klatek na sekundę\",\n            \"INPUT_ROLE_INVENTORY\": \"Ekwipunek\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Daj wszystkie przedmioty\",\n            \"INPUT_ROLE_JUMP\": \"Skakanie\",\n            \"INPUT_ROLE_LEFT\": \"Lewo\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Pomiń aktualny poziom\",\n            \"INPUT_ROLE_LOAD\": \"Wczytanie gry\",\n            \"INPUT_ROLE_LOOK\": \"Rozglądanie\",\n            \"INPUT_ROLE_PAUSE\": \"Pauza\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"Szybkie wczytanie\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"Szybki zapis\",\n            \"INPUT_ROLE_RIGHT\": \"Prawo\",\n            \"INPUT_ROLE_ROLL\": \"Przewrót\",\n            \"INPUT_ROLE_SAVE\": \"Zapis gry\",\n            \"INPUT_ROLE_SCREENSHOT\": \"Zrzut ekranu\",\n            \"INPUT_ROLE_SLOW\": \"Chodzenie\",\n            \"INPUT_ROLE_SPRINT\": \"Sprint\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Krok w lewo\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Krok w prawo\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"Zmień rozmiar ramki\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"Zmień współczynnik skalowania\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"Przełącz filtr tekstur\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"Włącz/wyłącz tryb pełnoekranowy\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Włącz tryb zdjęć\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"Włącz/wyłącz tekstury\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"Włącz/wyłącz filtr trapezowy\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"Ukryj lub pokaż UI\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"Włącz/wyłącz widok siatki\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Zwiększ lub zmniejsz prędkość\",\n            \"INPUT_ROLE_UP\": \"Bieg do przodu\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Duża apteczka\",\n            \"INPUT_ROLE_USE_FLARE\": \"Flara\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Mała apteczka\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"Wyłączony\",\n            \"JUMP_LOCK_LEGACY\": \"Klasyczny\",\n            \"JUMP_LOCK_TUNED\": \"Dostrojony\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"Wysoki\",\n            \"LIGHTING_CONTRAST_LOW\": \"Niski\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Średni\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"Zawsze\",\n            \"LOADING_SCREENS_DISABLED\": \"Wył.\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"Tylko nowa gra\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"Rozszerzony\",\n            \"LOOK_MODE_RESTRICTED\": \"Ograniczony\",\n            \"LOOK_MODE_UNRESTRICTED\": \"Nieograniczony\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"Zawsze\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"Nigdy\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Tylko muzyka\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"Wielokrotny\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"Pojedynczy\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"Wyposaż\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"Wyposaż lub schowaj\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"Elipsa\",\n            \"SHADOW_TYPE_OCTAGON\": \"Ośmiokąt\",\n            \"SHADOW_TYPE_SPRITE\": \"Sprite\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"\\\\{review}Prosty\",\n            \"STATS_STYLE_BORDERED\": \"\\\\{review}Z obramowaniem\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"\\\\{review}Wyłączone\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"\\\\{review}Nieprzezroczyste\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"\\\\{review}Przezroczyste\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Całkowity\",\n            \"TARGET_LOCK_MODE_NONE\": \"Brak\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Częściowy\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Dwuliniowy\",\n            \"TEXTURE_FILTER_POINT\": \"Punktowy\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"Dół na środku\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"Dół po lewej\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"Dół po prawej\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"Góra na środku\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"Góra po lewej\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"Góra po prawej\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"Naprawione\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"Głośność otoczenia\",\n            \"description\": \"Reguluje głośność otoczenia.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"Głośność przerywników\",\n            \"description\": \"Reguluje głośność przerywników filmowych w grze.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Mikrofon przy Larze\",\n            \"description\": \"Ustawia mikrofon na pozycji Lary. Domyślnie mikrofon jest na pozycji kamery. Wpływa to głównie na głośność efektów – czy przy kamerach daleko od Lary będą one cicho, czy głośno.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"\\\\{review}Odtwarzaj muzykę w ekwipunku\",\n            \"description\": \"\\\\{review}Pozwala na odtwarzanie dźwięków gry, dźwięków otoczenia i muzyki podczas przeglądania ekwipunku.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Muzyka w menu głównym\",\n            \"description\": \"Odtwarza muzykę w menu głównym.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Modulacja wysokości dźwięków\",\n            \"description\": \"Pozwala na losowe, nieznaczne zmiany tonacji efektów dźwiękowych dla urozmaicenia rozgrywki.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"Efekty dźwiękowe z PS1\",\n            \"description\": \"Zamienia określone efekty dźwiękowe na odpowiedniki z PS1.\\n\\n- Pociski z Uzi (tylko TR1)\\n- Odgłosy poruszania się boso (tylko TR2)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"Efekty dźwiękowe pod wodą\",\n            \"description\": \"Włącza odtwarzanie określonych efektów dźwiękowych animacji – dla obiektów takich jak drzwi i włazy – gdy kamera jest pod wodą.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Napraw dźwięku blokady łańcucha\",\n            \"description\": \"Zapobiega nieprawidłowemu odtwarzaniu dźwięku sekretu po użyciu złotego klucza w Grobowcu Tihocana.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Warstwowe dźwięki sekretów\",\n            \"description\": \"Sprawia, że podnoszenie sekretów nie zatrzymuje aktualnie odtwarzanej muzyki.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Warstwowe wypowiedzi wrogów\",\n            \"description\": \"Sprawia, że wypowiedzi wrogów nie zatrzymuje aktualnie odtwarzanej muzyki.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"Głośność filmów\",\n            \"description\": \"Reguluje głośność filmów.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"Głośność otoczenia (ekwipunek)\",\n            \"description\": \"Reguluje głośność otoczenia na ekranie ekwipunku.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"Głośność muzyki (ekwipunek)\",\n            \"description\": \"Reguluje głośność muzyki na ekranie ekwipunku.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Napraw wyzwalacze muzyki\",\n            \"description\": \"Ładuje wcześniej wyzwolone utwory muzyczne, tak aby jednorazowe ścieżki nie były odtwarzane ponownie.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{icon music} Głośność\",\n            \"description\": \"Reguluje głośność wszystkich dźwięków w grze. Pozostałe ustawienia są względem tej głośności.\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Przywróć muzykę po wczytaniu\",\n            \"description\": \"Ładuje ścieżkę dźwiękową graną podczas zapisu gry.\\n\\n- Nigdy: restartuje dźwięki otoczenia od 0.\\n- Tylko muzyka: przywraca utwory muzyczne.\\n- Zawsze: przywraca także dźwięki otoczenia od momentu, w którym gracz dokonał zapisu.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"Głośność muzyki\",\n            \"description\": \"Reguluje głośność muzyki.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"Wycisz dźwięk po zminimalizowaniu\",\n            \"description\": \"Wycisza muzykę i efekty dźwiękowe, kiedy okno gry jest nieaktywne.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Głośność dźwięku\",\n            \"description\": \"Dostosowuje głośność efektów dźwiękowych.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"Głośność otoczenia (pod wodą)\",\n            \"description\": \"Reguluje głośność otoczenia pod wodą.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"Głośność muzyki (pod wodą)\",\n            \"description\": \"Reguluje głośność muzyki pod wodą.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"Nielimitowany czas flar\",\n            \"description\": \"Zapobiega wygasaniu flar. Rzucone flary nadal gasną normalnie.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"Nielimitowany sprint\",\n            \"description\": \"Lara nie męczy się podczas biegu, lecz przeszkody wciąż ją zatrzymują.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"Wrogość sojuszników\",\n            \"description\": \"Kontroluje zachowanie jednostek kiedy zostaną zranione przez gracza.\\n\\n- Indywidualne: każdy sojusznik zmienia wrogość samodzielnie (styl TR3).\\n- Współdzielone: wszyscy sojusznicy stają się wrodzy jednocześnie (styl mnichów z TR2).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Prędkość kamery\",\n            \"description\": \"Zmienia prędkość, z jaką porusza się manualna kamera.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Zmień tryb pojawiania się Pierre'a\",\n            \"description\": \"Włączenie tej opcji sprawi, że podczas przechodzenia przez określone miejsca Pierre pojawi się bliżej. Jeśli opcja jest wyłączona, nic się nie zmieni. Dotyczy tylko sytuacji, gdzie nie Lara nie przegoniła Pierre'a.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"\\\\{review}Zasady tonięcia stworzeń\",\n            \"description\": \"\\\\{review}Określa zachowanie stworzeń lądowych w pomieszczeniach z wodą.\\n\\n- Nigdy: stworzenia lądowe nigdy nie toną (styl TR1).\\n- Domyślne: stworzenia lądowe toną w wodzie o głębokości 2 klików lub większej (styl TR2/3).\\n- Zanurzone: stworzenia lądowe toną tylko wtedy, gdy są całkowicie zanurzone.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"Wyłącz dodatkowe bronie\",\n            \"description\": \"Usuwa wszystkie bronie i amunicję z gry z wyjątkiem pistoletów.\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Trwałe obrażenia\",\n            \"description\": \"Wyłącza leczenie Lary, gdy gracz zaczyna nowy poziom.\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Wyłącz apteczki\",\n            \"description\": \"Usuwa wszystkie apteczki z gry.\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Usuń kolizję z ubitym T-Rexem\",\n            \"description\": \"Usuwa wszelką kolizję z tyranozaurem po jego śmierci, co pomaga w sytuacjach, gdy jego cielsko uniemożliwia przejście.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"Pozwól na celowanie w sojuszników\",\n            \"description\": \"Pozwala Larze celować w sojuszników, np. mnichów. Jeśli opcja jest wyłączona, sojusznicy będą całkowicie odporni na amunicję Lary.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Preselekcja przedmiotów\",\n            \"description\": \"Kiedy Lara spróbuje wejść w interakcję z zamkiem lub slotem na przedmiot i ma odpowiednią rzecz w plecaku, zostanie ona automatycznie wybrana.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"Potknięcia na spadach do tyłu\",\n            \"description\": \"Powoduje, że Lara potyka się, gdy się cofa, a za nią znajduje się pochylnia (TR3). Przy wyłączonej opcji, Lara zatrzymuje się gwałtownie na pochylniach (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"\\\\{review}Wyzwalacze worków na ciała\",\n            \"description\": \"\\\\{review}Umożliwia usuwanie zabitych przeciwników, gdy Lara przekracza określone wyzwalacze na niektórych poziomach. Jeśli wyłączone, martwi przeciwnicy będą zawsze wyświetlani.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"Wstrząsy kamery od głazów\",\n            \"description\": \"Włączenie opcji spowoduje wstrząsy kamery, kiedy w pobliżu Lary toczą się głazy.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"Odbicia granatów\",\n            \"description\": \"Włącza zachowanie granatów w stylu TR3 – odbijają się od ścian i pochyłości i zyskują większy promień wybuchu kosztem mniejszej prędkości.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Cheaty\",\n            \"description\": \"Włącza różne cheaty:\\n\\n- L: natychmiast kończy poziom.\\n- I: daje Larze wszystkie bronie; zwiększa ilość amunicji i apteczek; daje wszystkie przedmioty z bieżącego poziomu.\\n- O: włącza latanie.\\n  - Klawisz chodzenia: wyjście z trybu latani.\\n  - Klawisz broni: otwiera najbliższe drzwi.\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"\\\\{review}Sekwencje skryptowe\",\n            \"description\": \"\\\\{review}Włącza sekwencje skryptowe na początku niektórych poziomów, w których są dostępne.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Statystyki w kompasie\",\n            \"description\": \"Włącza wyświetlanie statystyk aktualnego poziomu przy wybraniu kompasu.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Konsola\",\n            \"description\": \"Włącza konsolę umożliwiającą wpisywanie różnych komend.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"Bezpieczne krawędzie\",\n            \"description\": \"Pozwala Larze obrócić się w powietrzu i złapać krawędź z której gracz właśnie się ześliznął, jeśli trzymay jest przycisk akcji.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"Skok z wąskich przestrzeni\",\n            \"description\": \"Umożliwia Larze wyskoczenie przy krawędziach w wąskich przestrzeniach, kiedy porusza się na czworaka.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"\\\\{review}Nachylenie podczas czołgania\",\n            \"description\": \"\\\\{review}Dopasowuje rotację Lary do geometrii podłoża podczas czołgania.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"Kucanie\",\n            \"description\": \"Pozwala Larze kucać i chodzić na czwroaka.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"Napisy końcowe\",\n            \"description\": \"Włącza ekrany z napisami końcowymi wyświetlane po ukończeniu gry. Ustawienie nie ma wpływu na ekran końcowych statystyk.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"Przewrót z kucania\",\n            \"description\": \"Pozwala Larze wykonać przewrót w przód z kucania po naciśnięciu klawisza sprintu.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Przerywniki\",\n            \"description\": \"Włącza odtwarzanie przerywników (cutscenek).\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Tryb demo\",\n            \"description\": \"Włącza odtwarzanie krótkich demonstracyjnych sekwencji w menu głównym.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Losuj rotację przeciwników\",\n            \"description\": \"Zastosowuje dodatkowy losowy kąt do niektórych przeciwników podczas ładowania poziomu.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Zapis efektów\",\n            \"description\": \"Zapisuje efekty graficzne, mgłę wodospadu, emitery płomieni itp., dzięki czemu nie znikają już po wczytaniu.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"Filmy\",\n            \"description\": \"Włącza odtwarzanie filmów.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Wybór trybu gry\",\n            \"description\": \"Pozwala na wybór opcji trybu przy rozpoczynaniu nowej gry.\\n\\n- Nowa Gra+: odblokowuje wszystkie bronie z nieskończoną amunicją; przeciwnicy mają podwójne zdrowie.\\n- Japońska NG: bronie zadają podwójne obrażenia; znajdźki z flarami zawierają 8 flar zamiast 6.\\n- Japońska NG+: połączenie Nowej Gry+ i Japońskiej NG.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"Kamera bezczynności\",\n            \"description\": \"Dostosowuje kamerę tak, aby była skierowana na Larę podczas animacji bezczynności. Przycisk patrzenia resetuje kamerę.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Odwrócenie osi Y patrzenia\",\n            \"description\": \"Odwraca sterowanie rozglądania się w osi Y.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Badanie przedmiotów\",\n            \"description\": \"Pozwala na wyświetlanie opisów przedmiotów w ekwipunku w niestandardowych poziomach, gdzie autor poziomu dostarczył odpowiednie dane.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Przewroty w powietrzu\",\n            \"description\": \"Włącza możliwość przewrotu podczas skakania.\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"Zabójcze bloki\",\n            \"description\": \"Kiedy ta opcja jest włączona, spadające z wysokości bloki natychmiast zabijają Larę. W przeciwnym razie Lara stanie na powierzchni bloku i przeżyje (tak jak w oryginalnych grach).\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Dalekie skoki w miejscu\",\n            \"description\": \"Pozwala Larze na przesuwanie się do przodu lub do tyłu dalej podczas wykonywania skoków w miejscu z wciśniętym odpowiednim klawiszem (tak jak w TR2+).\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"Skoki z krawędzi\",\n            \"description\": \"Pozwala Larze skoczyć w górę lub do tyłu, gdy zwisa z krawędzi, o ile przed nią znajduje się solidna powierzchnia, od której może się odepchnąć.\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Informacje prawne\",\n            \"description\": \"Włącza ekran z informacjami prawnymi oraz filmik z logiem Core Design na początku gry.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"Manualna kamera\",\n            \"description\": \"Umożliwia klawiszom kamery obracanie jej także podczas gry, nie tylko w trybie zdjęć.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"Salta w miejscu\",\n            \"description\": \"Pozwala Larze obracać się w powietrzu podczas neutralnego skoku. Aby wykonać salto w miejscu, należy przytrzymać jednocześnie przyciski skoku i przewrotu.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Pomoce do znajdziek\",\n            \"description\": \"Włącza okazjonalne migotanie w pobliżu przedmiotów do podniesienia w celu zwiększenia ich widoczności.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"Zagraj w poprzednie poziomy\",\n            \"description\": \"Włącza funkcje \\\"Zagraj w poprzednie poziomy\\\" oraz \\\"Co się wydarzyło do tej pory\\\" na ekranie wyboru Nowej Gry.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"Responsywne kucanie\",\n            \"description\": \"Włącza ulepszenia względem oryginalnej mechaniki kucania i czołgania.\\n\\n- Umożliwia szybsze wznowienie czołgania po zatrzymaniu.\\n- Umożliwia przejście z biegu/sprintu do kucnięcia bez konieczności wcześniejszego zatrzymania się.\\n- Umożliwia przejście z czołgania do przewrotu w przysiadzie (jeśli jest włączony) bez konieczności przejścia w kucnięcie.\\n- Umożliwia obracanie się podczas kucania.\\n- Przywraca animację podnoszenia przedmiotów podczas czołgania Lary (z wyłączeniem flar).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"Responsywny sprint\",\n            \"description\": \"Włącza bardziej responsywny tryb sprintu dla Lary.\\n\\n- Pozwala sprintować, gdy Lara ma energię, a nie tylko przy pełnej wytrzymałości.\\n- Umożliwia sprint po schodach bez przerywania zwykłą animacją biegu.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Kryształy zapisu\",\n            \"description\": \"Ogranicza zapisywanie do początku poziomów oraz kryształów zapisu umieszczonych w konkretnych lokacjach. Poziomy mają ograniczone, jednorazowe kryształy zapisu jak w wersji PS1. Zmiana tej opcji wymaga ponownego uruchomienia poziomu.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"Bieg po zjechaniu\",\n            \"description\": \"Umożliwia Larze natychmiastowe wznowienie biegu po zjechaniu w dół stoku. Aby aktywować, należy przytrzymać przycisk ruchu do przodu.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"Powolne kołysanie na krawędzi\",\n            \"description\": \"Umożliwia Larze powolne kołysanie się po uchwyceniu bardzo cienkiej krawędzi (styl TR3). Jeśli wyłączone, Lara zakołysze się tylko krótko, po czym wróci do nieruchomej pozycji wiszącej (styl TR1/2).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"Gładkie odbicia od ściany\",\n            \"description\": \"Pozwala Larze szybciej się odbić po uderzeniu w ścianę, gdy jednocześnie trzymany jest klawisz kierunku oraz do przodu.\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"Miękka kolizja ze statycznymi obiektami\",\n            \"description\": \"Pozwala Larze płynnie przesuwać się wzdłuż statycznych obiektów, jak w TR4+, zamiast nagle się zatrzymywać.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"Sprint\",\n            \"description\": \"Pozwala Larze sprintować, podobnie jak w TR3 i nowszych grach.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"Przewroty na schodkach\",\n            \"description\": \"Pozwala Larze uzyskać dodatkowy skok z małych schodków, jeśli spróbuje wykonać przewrót blisko krawędzi.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Łapanie krawędzi po puszczeniu\",\n            \"description\": \"Pozwala na anulowanie animacji huśtania Lary na krawędzi poprzez jej puszczenie i szybkie złapanie ponownie (tak jak w TR2+).\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Zmiana celu\",\n            \"description\": \"Włącza zmianę celu podczas celowania bronią (tak jak w TR4+).\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Upływ czasu w ekwipunku\",\n            \"description\": \"Sprawia, że zegar w grze działa nawet podczas wyświetlania menu ekwipunku.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"\\\\{review}Przełącz kucanie\",\n            \"description\": \"\\\\{review}Pozwala Larze pozostać w kucki po jednokrotnym naciśnięciu przycisku kucania. Naciśnij kucanie ponownie, aby wstać.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"\\\\{review}Przełącz sprint\",\n            \"description\": \"\\\\{review}Pozwala Larze biec sprintem po jednokrotnym naciśnięciu przycisku sprintu. Naciśnij sprint ponownie, aby przestać sprintować.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Statystyki końcowe\",\n            \"description\": \"Włącza ekran finalnych statystyk z całej gry, odtwarzany po napisach końcowych.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Responsywne skakanie\",\n            \"description\": \"Pozwala Larze skakać w dowolnym momencie podczas biegu.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Mniejszy dryf pod wodą\",\n            \"description\": \"Pozwala Larze na bardziej responsywne zatrzymanie się pod wodą po zwolnieniu klawisza pływania.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Zwinniejsze pływanie\",\n            \"description\": \"Powoduje, że Lara przyspiesza płynniej, ale wolniej podczas obracania pod wodą (tak jak w TR2+). Wyłączenie tej opcji zapewni Larze szybszą prędkość skrętu (tak jak w TR1).\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Przewroty pod wodą\",\n            \"description\": \"Pozwala Larze na przewrót pod wodą.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Brodzenie\",\n            \"description\": \"Pozwala Larze na brodzenie w płytkiej wodzie. Wyłączenie powoduje utknięcie na powierzchni wody.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Animowane interakcje\",\n            \"description\": \"Sprawia, że Lara chodzi do przedmiotów i przełączników, gdy są w pobliżu, zamiast się do nich natychmiastowo przesuwać.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Napraw AI aligatorów\",\n            \"description\": \"Naprawia błąd w którym aligatory nie zadają obrażeń, jeśli Lara pozostaje nieruchomo w wodzie.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Napraw AI niedźwiedzi\",\n            \"description\": \"Naprawia atak niedźwiedzia, aby nie chybiał Lary.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Napraw kolizję mostów\",\n            \"description\": \"Naprawia problem, w którym Lara nie może chwycić części niektórych mostów oraz krawędzi. Naprawia również problemy z kolizją mostów zwodzonych, pułapek i zwykłych mostów, gdy są nałożone na siebie, są przy zboczach lub blisko ziemi.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Napraw glitch z łamliwą podłogą\",\n            \"description\": \"Naprawia problem, w którym chodzenie w bok lub do tyłu na łamliwej podłodze powoduje natychmiastowy teleport Lary na podłogę poniżej.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Napraw priorytet rzucania flar\",\n            \"description\": \"Rozwiązuje problem, w którym Lara wyrzuca zużytą flarę w powietrze y nie może po tym złapać krawędzi.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Napraw triggery\",\n            \"description\": \"Naprawia błędy oryginalnej gry zw. z triggerami / wyzwalaczami.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Napraw darmową flarę\",\n            \"description\": \"Naprawia możliwość pojawienia się darmowej flary, jeśli gracz naciśnie przycisk flary podczas zbierania jakiegoś przedmiotu.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Napraw klonowanie przedmiotów\",\n            \"description\": \"Naprawia możliwość klonowania przedmiotów w górnym pierścieniu ekwipunku.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"Napraw podnoszenie przedmiotów\",\n            \"description\": \"Naprawia problem, w wyniku którego gra umieszczała Larę wewnątrz ścian podczas zbierania przedmiotów pod wodą lub pod stromymi sufitami.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"Napraw celność M16/MP5\",\n            \"description\": \"Naprawia celność M16/MP5 podczas biegania.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"Priorytet ataku u małp\",\n            \"description\": \"Zaatakowane małpy skupią się na odwecie zamiast podnosić apteczki i klucze.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"Popraw celowanie tubylców\",\n            \"description\": \"Naprawia błąd, przez który tubylcy z dmuchawkami czasami nie potrafili dobrze wycelować w Larę.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Napraw glitch QWOP\",\n            \"description\": \"Naprawia rzadki problem pojawiający się, kiedy Lara zeskakuje z małych schodków, co skutkuje dziwną animacją biegu znaną jako stan QWOP.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Napraw glitch ze schodami\",\n            \"description\": \"Naprawia problem, w którym Lara czasami jest popychana w ściany przylegające do małych stopni, kiedy biegnie po nich w określony sposób.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"Zderzenia w wodzie po pas\",\n            \"description\": \"Naprawia problem, w którym Lara nie reaguje na uderzenie w ścianę podczas brodzenia w wodzie po pas.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Napraw skakanie podczas biegu\",\n            \"description\": \"Naprawia problem, w którym Lara czasami nie może skoczyć natychmiast po przejściu z animacji chodzenia do biegu.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"\\\\{review}Naprawa geometrii ścian\",\n            \"description\": \"\\\\{review}Naprawia przypadki w geometrii poziomów OG, w których nachylenia wewnątrz ścian mogą prowadzić do niedokładnych obliczeń wysokości.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"Napraw wyjście z wody\",\n            \"description\": \"Naprawia problem, w którym Lara może przejść bezpośrednio z zatopionego pomieszczenia do sąsiadującego suchego pomieszczenia. Dodatkowo, opcja zapobiega Larze wchodzeniu z wody na nachylone powierzchnie, na których nie można stać.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"Odrzut harpuna\",\n            \"description\": \"Ustawia, jak często Lara musi przeładowywać harpun w zależności od aktualnej ilości amunicji. Np. przy ustawieniu wartości na 3 Lara będzie musiała przeładować po każdym trzecim strzale. Ustawienie na 0 całkowicie wyłącza przeładowywanie.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"Czas bezczynności\",\n            \"description\": \"Pozwala Larze przyjąć pozę po określonej liczbie sekund bezczynności. Wartość 0 wyłącza tę funkcję.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"Tryb blokady skoku\",\n            \"description\": \"Kontroluje, jak szybko Lara może skoczyć po rozpoczęciu biegu. Poszczególne tryby:\\n\\n- Klasyczny: odpowiada TR2.\\n- Dostrojony: skok jest możliwy o 2 klatki wcześniej niż w TR2.\\n- Wyłączony: skok jest możliwy natychmiast po animacji startu biegu.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Ekrany ładowania\",\n            \"description\": \"Kontroluje sposób pokazywania ekranów ładowania wyświetlanych przed rozgrywką.\\n\\n- Wyłączone: całkowicie wyłącza ekrany ładowania.\\n- Zawsze: pokazuje ekrany ładowania zawsze przed zaczęciem gry.\\n- Tylko nowa gra: pomija ekrany ładowania podczas wczytywania zapisanej gry.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"Tryb rozglądania\",\n            \"description\": \"Określa, kiedy Lara może się rozglądać.\\n\\n- Ograniczony: rozglądanie tylko w bezruchu, nigdy pod wodą.\\n- Rozszerzony: rozglądanie dozwolone podczas większości animacji, z wyjątkiem np. pchania bloku.\\n- Nieograniczony: rozglądanie zawsze dozwolone przy normalnym sterowaniu Larą.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"Liczba slotów szybkiego zapisu\",\n            \"description\": \"Zmienia liczbę dostępnych slotów szybkiego zapisu.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Liczba slotów zapisu\",\n            \"description\": \"Zmienia liczbę dostępnych slotów na zapisy.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"\\\\{review}Pauza po utracie fokusu\",\n            \"description\": \"\\\\{review}Zatrzymuje postęp rozgrywki, gdy okno gry traci fokus.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"Przebieg eksplozji pocisków\",\n            \"description\": \"Kontroluje metodę liczenia zasięgu eksplozji pocisków z wyrzutni rakiet i granatników.\\n\\n- Pojedynczy przebieg: zachowanie z TR1 i TR2.\\n- Wielokrotny przebieg: zachowanie z TR3.\\n\\nWielokrotny przebieg często powoduje podwójne obrażenia dla pojedynczych celów.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Zapamiętuj broń między poziomami\",\n            \"description\": \"Włączenie tej opcji sprawi, że Lara zapamięta wybór broni z poprzedniego poziomu. W przeciwnym wypadku Lara zawsze będzie wracać do pistoletów, domyślnie schowanych w kaburach.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"Przywróć przeciwników z PS1\",\n            \"description\": \"Dodaje mumię, która pojawia się w wersji na PlayStation w City of Khamoon, pokój 25.\\nZmiana tej opcji do poprawnego działania wymaga ponownego uruchomienia gry.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Startowe zdrowie Lary\",\n            \"description\": \"Ustala wartość zdrowia, z którą Lara zaczyna każdy poziom.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Tryb blokady broni\",\n            \"description\": \"Zmienia tryb, w jaki bronie blokują się na celach.\\n\\n- Całkowity: zawsze utrzymuje blokadę celu, nawet jeśli przeciwnik wyjdzie z pola widzenia lub zginie (tak jak w TR1-3).\\n- Częściowy: utrzymuje blokadę celu, jeśli przeciwnik wyjdzie z pola widzenia, ale traci ją, kiedy przeciwnik zginie.\\n- Brak: traci blokadę celu, kiedy przeciwnik wyjdzie z pola widzenia lub zginie (tak jak w TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"Tryb glitchy ścian\",\n            \"description\": \"Pozwala na używanie glitcha ściany z TR1 w TR2 - i odwrotnie, umożliwia również naprawienie wszystkich typów tego błędu.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"Buforowanie (klawisze funkcyjne)\",\n            \"description\": \"Włącza buforowanie klawisza F w celu precyzyjnej kontroli ruchu Lary (1 klatka). Ta funkcja pierwotnie istniała tylko w porcie TombATI (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"Buforowanie (ekwipunek)\",\n            \"description\": \"Włącza buforowanie ekranu ekwipunku, aby uzyskać precyzyjną kontrolę nad ruchem Lary (2 klatki).\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Responsywny paszport\",\n            \"description\": \"Wyłącza blokowanie klawiszy podczas przewracania stron paszportu, zamiast tego kolejkując pożądaną nawigację.\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Ulepszone kroki w bok\",\n            \"description\": \"Umożliwia chodzenie w bok w stylu TR3+, tj. shift+strzałki kierunkowe. Naturalnie, przyciski dedykowane do kroków w bok nadal będą działać.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"Szybkie klawisze broni\",\n            \"description\": \"Kontroluje zachowanie klawiszy do szybkiego wyposażania broni.\\n\\n- Wyposaż: naciśnięcie klawisza spowoduje, że Lara wyposaży daną broń.\\n- Wyposaż lub schowaj: to samo co powyżej; dodatkowo Lara schowa daną broń, jeśli ma ją aktualnie wyposażoną.\",\n        },\n        \"language\": {\n            \"title\": \"Język\",\n            \"description\": \"Zmienia język tekstu interfejsu.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Filtr anizotropowy\",\n            \"description\": \"Zwiększa filtrowanie tekstur w dużej odległości od kamery.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Proporcje ekranu\",\n            \"description\": \"Wymusza określone proporcje obrazu w grze przez dodanie czarnych pasków po bokach (letterboxing).\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"Ramka\",\n            \"description\": \"Dodaje czarną ramkę wokół okna gry.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Filtr trapezoidalny\",\n            \"description\": \"Poprawia rysowanie czworokątów, rozciągając teksturę między wszystkimi wierzchołkami zamiast dzielić je na dwa trójkąty.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"Synchronizacja pionowa\",\n            \"description\": \"Włącza lub wyłącza synchronizację pionową.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"Klatki na sekundę\",\n            \"description\": \"Ustawia liczbę klatek na sekundę w grze.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Kontrast oświetlenia\",\n            \"description\": \"Zwiększa kontrast dla dynamicznych źródeł światła, takich jak flary i rozbłyski broni.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Format zrzutów ekranu\",\n            \"description\": \"Format pliku użyty do zapisu zrzutów ekranu.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"Tryb blokady sprite'ów\",\n            \"description\": \"Blokuje dane osie podczas wyświetlania sprite'ów na ekranie.\\n\\n- Brak: wyświetlaj sprite'y normalnie.\\n- Obrót: blokuje oś obrotu – przydatne tylko w trybie fotograficznym.\\n- Obrót i pochylenie: zapewnia, że sprite'y stoją pionowo i nie kładą się na ziemi przy patrzeniu z góry.\\n- Perspektywa: blokuje osie obrotu i pochylenia oraz dodatkowo lekko obraca sprite'y w kierunku środka ekranu.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Filtr tekstur\",\n            \"description\": \"Przełącza między gładkimi a pikselowymi teksturami w grze.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"Filtr UI\",\n            \"description\": \"Przełącza między gładką a pikselową grafiką interfejsu.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"Współczynnik skalowania\",\n            \"description\": \"Skaluje grę o ustalony współczynnik, zachowując pikselowy wygląd.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"Filtr skalowania\",\n            \"description\": \"Przełącza gładki lub pikselowy wygląd dla całego ekranu.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Kolor paska tlenu\",\n            \"description\": \"Kolor paska tlenu.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Kolor paska tlenu\",\n            \"description\": \"Kolor paska tlenu.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Lokalizacja paska tlenu\",\n            \"description\": \"Miejsce, w którym wyświetlany jest pasek tlenu.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"\\\\{review}Lokalizacja licznika amunicji\",\n            \"description\": \"\\\\{review}Miejsce, w którym wyświetlany jest licznik amunicji.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"Wygląd pasków\",\n            \"description\": \"Steruje wyglądem pasków życia i tlenu.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Skala pasków\",\n            \"description\": \"Zmienia rozmiar pasków zdrowia i tlenu.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"Migające paski\",\n            \"description\": \"Sprawia, że paski zdrowia i tlenu Lary zaczynają migać, kiedy dany zasób jest na wyczerpaniu.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Wygładzanie pasków\",\n            \"description\": \"Sprawia, że paski życia i tlenu używają gładkich przejść kolorów.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Cykliczne przewijanie\",\n            \"description\": \"Zawijanie poza ostatni element spowoduje powrót na początek listy i na odwrót.\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Kolor paska zdrowia wrogów\",\n            \"description\": \"Kolor paska zdrowia przeciwników.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Kolor paska sojuszników\",\n            \"description\": \"Kolor paska zdrowia sojuszników. Wyświetlany w miejscu pasków zdrowia wrogów.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Kolor paska sojuszników\",\n            \"description\": \"Kolor paska zdrowia sojuszników. Wyświetlany w miejscu pasków zdrowia wrogów.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Kolor paska zdrowia wrogów\",\n            \"description\": \"Kolor paska zdrowia przeciwników.\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Lokalizacja paska wrogów\",\n            \"description\": \"Miejsce, w którym wyświetlany jest pasek zdrowia przeciwników.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Tryb paska wroga\",\n            \"description\": \"Zmienia sposób wyświetlania paska zdrowia dla aktywnego przeciwnika.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"Kolor paska wychłodzenia\",\n            \"description\": \"Kolor paska wychłodzenia podczas przebywania w zimnej wodzie.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"Kolor paska wychłodzenia\",\n            \"description\": \"Kolor paska wychłodzenia podczas przebywania w zimnej wodzie.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"Położenie paska wychłodzenia\",\n            \"description\": \"Miejsce, w którym wyświetlany jest pasek wychłodzenia.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Kolor paska zdrowia\",\n            \"description\": \"Kolor paska zdrowia Lary.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Kolor paska zdrowia\",\n            \"description\": \"Kolor paska zdrowia Lary.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Lokalizacja paska zdrowia\",\n            \"description\": \"Miejsce, w którym wyświetlany jest pasek zdrowia Lary.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"Kolor paska zdrowia przy zatruciu\",\n            \"description\": \"Kolor paska zdrowia, gdy Lara jest zatruta.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"Kolor paska zdrowia przy zatruciu\",\n            \"description\": \"Kolor paska zdrowia, gdy Lara jest zatruta.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"Tło ekwipunku\",\n            \"description\": \"Zmienia sposób wyświetlania tła dla pierścienia ekwipunku.\\n\\n- Ciemne: TR1 (PC).\\n- Bardzo ciemne: TR1 (PS1).\\n- Stałe: TR2 (PC).\\n- Falujące: TR2 (PS1).\\n- Czarno-białe: TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"Efekty przejścia w ekwipunku\",\n            \"description\": \"Włącza lub wyłącza efekty przejścia w pierścieniu ekwipunku. Wymaga opcji ogólnych efektów przejścia do działania.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Styl menu\",\n            \"description\": \"Zmienia sposób rysowania menu.\\n\\n - PC: styl interfejsu odpowiada wersji na PC.\\n - PS1: styl interfejsu odpowiada wersji na PlayStation 1.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"Tło pauzy\",\n            \"description\": \"Zmienia sposób wyświetlania tła dla ekranu pauzy.\\n\\n- Ciemne: TR1 (PC).\\n- Bardzo ciemne: TR1 (PS1).\\n- Stałe: TR2 (PC).\\n- Falujące: TR2 (PS1).\\n- Czarno-białe: TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"Efekty przejścia podczas pauzy\",\n            \"description\": \"Włącza lub wyłącza efekty przejścia na ekranie pauzy. Wymaga opcji ogólnych efektów przejścia do działania.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"Skala przedmiotów\",\n            \"description\": \"Zmienia rozmiar przedmiotów animowanych w rogu ekranu, kiedy Lara coś podnosi.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"Pokaż paski\",\n            \"description\": \"Wyłącza wszystkie paski w grze, ukrywając informacje o zdrowiu Lary i innych zasobach.\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Powiadomienia o podniesieniu\",\n            \"description\": \"Wyświetla przedmioty w prawym dolnym rogu, gdy Lara coś podnosi.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"\\\\{review}Tekst wersji tytułu\",\n            \"description\": \"\\\\{review}Wyświetla ciąg wersji TRX w pierścieniu ekwipunku tytułu.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"Kolor paska sprintu\",\n            \"description\": \"Kolor paska sprintu\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"Kolor paska sprintu\",\n            \"description\": \"Kolor paska sprintu\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"Pozycja paska sprintu\",\n            \"description\": \"Lokalizacja, w której wyświetlany jest pasek sprintu.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"\\\\{review}Trafienia/amunicja użyta\",\n            \"description\": \"\\\\{review}Pokazuje wiersz amunicji w statystykach poziomu.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"\\\\{review}Kryształy\",\n            \"description\": \"\\\\{review}Pokazuje wiersz kryształów w statystykach poziomu.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"\\\\{review}Śmierci\",\n            \"description\": \"\\\\{review}Pokazuje śmierci Lary w statystykach kompasu i statystykach poziomu. Liczba śmierci jest aktualizowana w aktualnie załadowanym zapisie zaraz po śmierci Lary.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"\\\\{review}Przebyta odległość\",\n            \"description\": \"\\\\{review}Pokazuje wiersz przebytej odległości w statystykach poziomu.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"\\\\{review}Zabójstwa\",\n            \"description\": \"\\\\{review}Pokazuje wiersz zabójstw w statystykach poziomu.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"\\\\{review}Licznik poziomu\",\n            \"description\": \"\\\\{review}Pokazuje aktualny numer poziomu na górze statystyk poziomu.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"\\\\{review}Użyte apteczki\",\n            \"description\": \"\\\\{review}Pokazuje wiersz użytych apteczek w statystykach poziomu.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"\\\\{review}Zebrane przedmioty\",\n            \"description\": \"\\\\{review}Pokazuje wiersz zebranych przedmiotów w statystykach poziomu.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"\\\\{review}Odnalezione sekrety\",\n            \"description\": \"\\\\{review}Pokazuje wiersz odnalezionych sekretów w statystykach poziomu.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"\\\\{review}Czas\",\n            \"description\": \"\\\\{review}Pokazuje wiersz czasu w statystykach poziomu.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"\\\\{review}Pokaż sumy\",\n            \"description\": \"\\\\{review}Pokazuje sumy obok statystyk, gdy jest to możliwe. Sekrety pozostają niezmienione przez to ustawienie.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"\\\\{review}Styl statystyk\",\n            \"description\": \"\\\\{review}Kontroluje sposób wyświetlania okna statystyk.\\n\\n- Prosty: pokazuje prostszy układ bez ramki.\\n- Z obramowaniem: pokazuje układ z ramką.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"Tło statystyk\",\n            \"description\": \"Zmienia sposób wyświetlania tła dla statystyk na końcu poziomu.\\n\\n- Ciemne: TR1 (PC).\\n- Bardzo ciemne: TR1 (PS1).\\n- Stałe: TR2 (PC).\\n- Falujące: TR2 (PS1).\\n- Czarno-białe: TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"Efekty przejścia w statystykach\",\n            \"description\": \"Włącza lub wyłącza efekty przejścia na ekranach statystyk. Wymaga opcji ogólnych efektów przejścia do działania.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Skala tekstu\",\n            \"description\": \"Zmienia rozmiar tekstu interfejsu.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"\\\\{review}Efekty krwi\",\n            \"description\": \"\\\\{review}Kontroluje kolory iskier krwi.\\n\\n- Wyłączone: nie pokazuje iskier krwi.\\n- Różowy: domyślny w niemieckich wersjach PC TR3.\\n- Czerwony: domyślny we wszystkich innych wersjach detalicznych.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Tryb kamery\",\n            \"description\": \"Zmienia zachowanie kamery podczas niektórych akcji, np. kiedy Lara używa kluczy lub dźwigni.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"Znajdźki 3D\",\n            \"description\": \"Rysuje przedmioty do podniesienia jako trójwymiarowe modele zamiast płaskich grafik.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Warkocz Lary\",\n            \"description\": \"Włącza warkocz Lary.\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Wiatr\",\n            \"description\": \"Włącza efekt wiatru na warkoczu Lary w miejscach, które to obsługują.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Przejścia przy wyjściu z gry\",\n            \"description\": \"Włącza efekt płynnego przejścia przy wychodzeniu z gry do pulpitu.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Efekty przejścia\",\n            \"description\": \"Włącza efekty płynnych przejść, np. między grafikami napisów końcowych, lub przy włączaniu ekranu ekwipunku i ekranu pauzy.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"Światło dookoła ogni\",\n            \"description\": \"Włącza generowanie dynamicznego oświetlenia obok aktywnych płomieni.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"Ślady stóp\",\n            \"description\": \"Włącza wyświetlanie śladów stóp Lary na niektórych powierzchniach w obsługiwanych poziomach.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"Płynne kamery\",\n            \"description\": \"Włącza efekt płynnych przejść dla kamer skierowanych na Larę. Wyłączenie tej opcji spowoduje natychmiastową zmianę widoku na Larę.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Efekty świetlne broni\",\n            \"description\": \"Włącza dynamiczne oświetlenie przy strzelaniu z broni i eksplozjach.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"Fioletowe kryształy\",\n            \"description\": \"Kryształy zapisu będą rysowane z fioletowym odcieniem, bardziej podobnym do odcienia z PS1.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Odbicia\",\n            \"description\": \"Włącza efekt lustrzanych odbić na niektórych obiektach.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"Reaktywne barwienie wody\",\n            \"description\": \"Nadaje częściom ciała Lary wodne zabarwienie tylko wtedy, gdy znajdują się pod wodą (styl TR3). Przy wyłączonej opcji, zabarwienie jest nadawane całemu ciału Lary (styl TR1/2).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Rozbłyski ze strzelby\",\n            \"description\": \"Rysuje płomienie podczas strzelania ze strzelby, jak w przypadku innych broni.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Niebo\",\n            \"description\": \"Włącza niebo w obsługiwanych poziomach.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"Pogoda\",\n            \"description\": \"Włącza efekty pogodowe w obsługiwanych poziomach.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Napraw animacje sprite'ów\",\n            \"description\": \"Przywraca glonom efekt animacji w wodnych poziomach.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Napraw rotacje przedmiotów\",\n            \"description\": \"Naprawia problemy z oryginalnej gry zw. z nieprawidłowo obróconymi przedmiotami podczas korzystania z opcji rysowania znajdziek jako obiekty 3D.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Napraw tekstury\",\n            \"description\": \"Naprawia błędy oryginalnej gry zw. z brakującymi lub nieprawidłowymi teksturami.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"Kolor mgły\",\n            \"description\": \"Kolor mgły.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Koniec mgły\",\n            \"description\": \"Ustala odległość w sektorach, w której gra przestaje rysować poziom.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Początek mgły\",\n            \"description\": \"Ustala odległość w sektorach, w której gra zaczyna ograniczać widoczność.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"Przezroczystość mgły\",\n            \"description\": \"Obiekty oddalone od kamery będą się stawać coraz bardziej przezroczyste.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"Kąt widzenia\",\n            \"description\": \"Kąt widzenia w stopniach. Większe wartości poszerzają pole widzenia, mniejsze je zmniejszają.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Jasność\",\n            \"description\": \"Zmienia jasność gry.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"Gamma\",\n            \"description\": \"Reguluje krzywą gamma. Wyższe wartości oznaczają jaśniejsze oświetlenie. Wartość 2,5 wyłącza modulację.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"Strój Lary\",\n            \"description\": \"Zmienia strój Lary. Opcja domyślna zachowuje standardowe zmiany między poziomami; w przeciwnym razie wybrany strój pozostaje aktywny, dopóki nie zostanie zmieniony ręcznie.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"Kształt cieni\",\n            \"description\": \"Wybiera sposób rysowania cieni obiektów.\\n\\n- Ośmiokąt: stare cienie z TR1 i TR2\\n- Elipsa: zaokrąglone cienie\\n- Sprite: cienie oparte na teksturach z TR3\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"Okulary przeciwsłoneczne\",\n            \"description\": \"\\\\{review}Zmienia styl okularów przeciwsłonecznych Lary. Uwaga: soczewki będą refleksyjne, jeśli odpowiednia opcja jest włączona.\\n\\n- Wyłączone: Lara nie będzie nosić okularów przeciwsłonecznych.\\n- Nieprzezroczyste: Okulary przeciwsłoneczne Lary będą miały nieprzezroczyste soczewki.\\n- Przezroczyste: Okulary przeciwsłoneczne Lary będą miały półprzezroczyste soczewki.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"Jasność interfejsu\",\n            \"description\": \"Zmienia jasność interfejsu.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Kolor wody\",\n            \"description\": \"Kolor wody.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"Alarm\",\n        },\n        \"alligator\": {\n            \"name\": \"Aligator\",\n        },\n        \"alphabet\": {\n            \"name\": \"Domyślna czcionka\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"Mała czcionka\",\n        },\n        \"amber_light\": {\n            \"name\": \"Bursztynowe światło\",\n        },\n        \"animating_1\": {\n            \"name\": \"Animowany Obiekt 1\",\n        },\n        \"animating_10\": {\n            \"name\": \"\\\\{review}Animowany Obiekt 10\",\n        },\n        \"animating_2\": {\n            \"name\": \"Animowany Obiekt 2\",\n        },\n        \"animating_3\": {\n            \"name\": \"Animowany Obiekt 3\",\n        },\n        \"animating_4\": {\n            \"name\": \"Animowany Obiekt 4\",\n        },\n        \"animating_5\": {\n            \"name\": \"Animowany Obiekt 5\",\n        },\n        \"animating_6\": {\n            \"name\": \"Animowany Obiekt 6\",\n        },\n        \"animating_7\": {\n            \"name\": \"\\\\{review}Animowany Obiekt 7\",\n        },\n        \"animating_8\": {\n            \"name\": \"\\\\{review}Animowany Obiekt 8\",\n        },\n        \"animating_9\": {\n            \"name\": \"\\\\{review}Animowany Obiekt 9\",\n        },\n        \"ape\": {\n            \"name\": [\n                \"Małpa\",\n                \"Goryl\",\n            ]\n        },\n        \"area_51_rocket\": {\n            \"name\": \"\\\\{review}Rakieta Strefa 51\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"\\\\{review}Wystrzał Rakiety Strefa 51\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"\\\\{review}Wsparcie Rakiety Strefa 51\",\n        },\n        \"assault_digits\": {\n            \"name\": \"Cyferki\",\n        },\n        \"assault_target\": {\n            \"name\": \"Tarcza\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"\\\\{review}Atlantydczyk Naziemny\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"\\\\{review}Atlantydczyk (Strzelający)\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"\\\\{review}Skrzydlaty Atlantydczyk\",\n        },\n        \"autos\": {\n            \"name\": \"Pistolety automatyczne\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"Amunicja do pistoletów automatycznych\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Klon Lary\",\n        },\n        \"baldy\": {\n            \"name\": \"Łysy\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"Najemnik 1\",\n                \"Zamaskowany oprych 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"Najemnik 2\",\n                \"Zamaskowany oprych 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"Najemnik 3\",\n                \"Zamaskowany oprych 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"Barakuda\",\n        },\n        \"bartoli\": {\n            \"name\": \"Marco Bartoli\",\n        },\n        \"bat\": {\n            \"name\": \"Nietoperz\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"Emiter nietoperzy\",\n        },\n        \"beacon_light\": {\n            \"name\": \"Światło sygnalizacyjne\",\n        },\n        \"bear\": {\n            \"name\": \"Niedźwiedź\",\n        },\n        \"bell\": {\n            \"name\": \"Dzwon\",\n        },\n        \"big_bowl\": {\n            \"name\": \"Miska z lawą\",\n        },\n        \"big_eel\": {\n            \"name\": \"Duży węgorz\",\n        },\n        \"big_pod\": {\n            \"name\": \"Duży kokon\",\n        },\n        \"big_spider\": {\n            \"name\": \"Gigantyczny pająk\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"Ptak-potwór\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"Kapiąca woda\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"Śpiewające ptaki\",\n        },\n        \"blade\": {\n            \"name\": \"Ostrze na ścianie\",\n        },\n        \"blood\": {\n            \"name\": \"\\\\{review}Krew\",\n        },\n        \"blood_pink\": {\n            \"name\": \"\\\\{review}Krew (ocenzurowana)\",\n        },\n        \"blue_light\": {\n            \"name\": \"Niebieskie światło\",\n        },\n        \"boat\": {\n            \"name\": \"Łódź\",\n        },\n        \"boat_bits\": {\n            \"name\": \"Elementy łodzi\",\n        },\n        \"body_part\": {\n            \"name\": \"Część ciała\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"Most płaski\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"Most pochylony 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"Most pochylony 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"Bańka 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Bąbelki 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"Emiter baniek\",\n        },\n        \"camera_target\": {\n            \"name\": \"Obiekt kamery\",\n        },\n        \"carcass\": {\n            \"name\": \"Padlina\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"Sufit z kolcami\",\n        },\n        \"centaur\": {\n            \"name\": \"Centaur\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Posąg\",\n        },\n        \"civilian\": {\n            \"name\": \"\\\\{review}Cywil\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"\\\\{review}Szponiasty Mutant\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"Zegar kryjówki Bartoliego\",\n        },\n        \"cog_1\": {\n            \"name\": \"Zębatka 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Zębatka 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Zębatka 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"Koniec walki\",\n        },\n        \"compass\": {\n            \"name\": \"Rozgrywka\",\n        },\n        \"compy\": {\n            \"name\": \"Mały dinozaur\",\n        },\n        \"controls\": {\n            \"name\": \"Sterowanie\",\n        },\n        \"copter\": {\n            \"name\": \"Helikopter\",\n        },\n        \"cowboy\": {\n            \"name\": \"Kowboj\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"\\\\{review}Pełzający Mutant\",\n        },\n        \"crocodile\": {\n            \"name\": \"Krokodyl\",\n        },\n        \"crow\": {\n            \"name\": \"Kruk\",\n        },\n        \"cult_1\": {\n            \"name\": \"Zamaskowany zbir 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"Zamaskowany zbir 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"Zamaskowany zbir 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"Nożownik\",\n        },\n        \"cult_3\": {\n            \"name\": \"Zbir z strzelbą\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"Animacja końcowa\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Miecz Damoklesa\",\n        },\n        \"dart\": {\n            \"name\": \"Strzała\",\n        },\n        \"dart_effect\": {\n            \"name\": \"Efekt strzały\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Wyzwalacz strzał\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"Desert Eagle\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"Amunicja do Desert Eagle\",\n        },\n        \"detonator_box\": {\n            \"name\": \"Skrzynka detonatora\",\n        },\n        \"ding_dong\": {\n            \"name\": \"Dzwonek do drzwi\",\n        },\n        \"dino_mutant\": {\n            \"name\": [\n                \"Dino mutant\",\n                \"Zmutowany dinozaur\",\n            ]\n        },\n        \"disc\": {\n            \"name\": \"Dysk\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"Emiter dysków\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"\\\\{review}Jednorazowa Animacja 9\",\n        },\n        \"diver\": {\n            \"name\": \"Nurek\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"Pies\",\n                \"Doberman\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Drzwi 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Drzwi 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Drzwi 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Drzwi 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Drzwi 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Drzwi 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Drzwi 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Drzwi 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"Tył smoka\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"Brak opisu\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"Przednie kości smoka\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"Tylne kości smoka\",\n        },\n        \"dragon_front\": {\n            \"name\": \"Przód smoka\",\n        },\n        \"drawbridge\": {\n            \"name\": \"Most zwodzony\",\n        },\n        \"dust\": {\n            \"name\": \"Kurz\",\n        },\n        \"dying_monk\": {\n            \"name\": \"Umierający mnich\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"\\\\{review}Umierający Mutant\",\n        },\n        \"eagle\": {\n            \"name\": \"Orzeł\",\n        },\n        \"earthquake\": {\n            \"name\": \"Trzęsienie ziemi\",\n        },\n        \"eel\": {\n            \"name\": \"Węgorz\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"\\\\{review}Elektryczny czyściciel\",\n        },\n        \"electric_fence\": {\n            \"name\": \"Ogrodzenie elektryczne\",\n        },\n        \"electrical_light\": {\n            \"name\": \"Światło elektryczne\",\n        },\n        \"ember\": {\n            \"name\": \"Żar\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"Emiter żaru\",\n        },\n        \"explosion_1\": {\n            \"name\": \"Eksplozja 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Eksplozja 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": \"Spadający blok 1\",\n        },\n        \"falling_block_2\": {\n            \"name\": \"Spadający blok 2\",\n        },\n        \"falling_block_3\": {\n            \"name\": \"Spadający blok 3\",\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"Spadający sufit 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Spadający sufit 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"\\\\{review}Ognista Głowa\",\n        },\n        \"fish_mutant\": {\n            \"name\": [\n                \"Ryba mutant\",\n                \"Zmutowana ryba\",\n            ]\n        },\n        \"flame\": {\n            \"name\": [\n                \"Płomień\",\n                \"Ogień\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"Emiter płomieni\",\n                \"Emiter ognia\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": \"Emiter płomieni (duży)\",\n        },\n        \"flame_emitter_jet\": {\n            \"name\": \"Emiter płomieni (odrzut)\",\n        },\n        \"flame_emitter_side\": {\n            \"name\": \"Emiter płomieni (boczny)\",\n        },\n        \"flame_emitter_small\": {\n            \"name\": \"Emiter płomieni (mały)\",\n        },\n        \"flare\": {\n            \"name\": \"Flara\",\n        },\n        \"flare_fire\": {\n            \"name\": \"Iskry flary\",\n        },\n        \"flares_box\": {\n            \"name\": \"Paczka flar\",\n        },\n        \"flickering_light\": {\n            \"name\": \"\\\\{review}Migoczące Światło\",\n        },\n        \"fuse_box\": {\n            \"name\": \"\\\\{review}Skrzynka bezpieczników\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"Szary dysk\",\n        },\n        \"gamma\": {\n            \"name\": \"Gamma\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"\\\\{review}Emiter Gazu (Zielony)\",\n        },\n        \"general\": {\n            \"name\": \"Miniłódź podwodna\",\n        },\n        \"globe\": {\n            \"name\": \"Globus\",\n        },\n        \"glow\": {\n            \"name\": \"Poświata\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"Poświata mapy\",\n        },\n        \"gondola\": {\n            \"name\": \"Gondola\",\n        },\n        \"gong\": {\n            \"name\": \"Gong\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"Pałka do gongu\",\n        },\n        \"graphics\": {\n            \"name\": \"Grafika\",\n        },\n        \"green_light\": {\n            \"name\": \"Zielone światło\",\n        },\n        \"grenade\": {\n            \"name\": \"Granat\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"Wyrzutnia granatów\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"Granaty\",\n        },\n        \"gun_flash\": {\n            \"name\": \"Rozbłysk broni\",\n        },\n        \"gun_shell\": {\n            \"name\": \"Łuska do pistoletu\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"Harpun\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"Wyrzutnia harpunów\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"Harpuny\",\n        },\n        \"hook\": {\n            \"name\": \"Hak\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"Gorąca ciecz\",\n        },\n        \"huskie\": {\n            \"name\": [\n                \"Pies\",\n                \"Huskie\",\n            ]\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"\\\\{review}Hybrydowy Mutant\",\n        },\n        \"icicle\": {\n            \"name\": \"Sopel lodu\",\n        },\n        \"inv_background\": {\n            \"name\": \"Tło menu\",\n        },\n        \"jelly\": {\n            \"name\": \"Meduza\",\n        },\n        \"kayak\": {\n            \"name\": \"Kajak\",\n        },\n        \"key_1\": {\n            \"name\": \"Klucz 1\",\n        },\n        \"key_2\": {\n            \"name\": \"Klucz 2\",\n        },\n        \"key_3\": {\n            \"name\": \"Klucz 3\",\n        },\n        \"key_4\": {\n            \"name\": \"Klucz 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"Zamek 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"Zamek 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"Zamek 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"Zamek 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"Eliminacja wszystkich wyzwolonych obiektów\",\n        },\n        \"killer_statue\": {\n            \"name\": \"Posąg z mieczem\",\n        },\n        \"lara\": {\n            \"name\": \"Lara\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"Alarm\",\n        },\n        \"lara_autos\": {\n            \"name\": \"Animacja pistoletów automatycznych\",\n        },\n        \"lara_boat\": {\n            \"name\": \"Animacja łodzi\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"Animacja Desert Eagle\",\n        },\n        \"lara_extra\": {\n            \"name\": \"Dodatkowa animacja Lary\",\n        },\n        \"lara_flare\": {\n            \"name\": \"Animacja flary\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"Animacja wyrzutni granatów\",\n        },\n        \"lara_hair\": {\n            \"name\": \"Warkocz Lary\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"Animacja wyrzutni harpunów\",\n        },\n        \"lara_m16\": {\n            \"name\": \"Animacja M16\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"Animacja magnumów\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"Animacja MP5\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"Animacja pistoletów\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"Animacja wyrzutni rakiet\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"Animacja strzelby\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"Animacja skutera śnieżnego\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"Animacja uzi\",\n        },\n        \"larson\": {\n            \"name\": \"Larson\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Lawa\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Sztaba ołowiu\",\n        },\n        \"lift\": {\n            \"name\": \"Winda\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Wyzwalacz piorunów\",\n        },\n        \"lion\": {\n            \"name\": \"Lew\",\n        },\n        \"lioness\": {\n            \"name\": [\n                \"Lwica\",\n                \"Lew\",\n            ]\n        },\n        \"lizard\": {\n            \"name\": \"Jaszczur\",\n        },\n        \"m16\": {\n            \"name\": \"M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"Magazynki do M16\",\n        },\n        \"m16_flash\": {\n            \"name\": \"Rozbłysk M16\",\n        },\n        \"magnums\": {\n            \"name\": \"Magnumy\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Amunicja do magnumów\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"Zamiana obiektu 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"Zamiana obiektu 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"Zamiana obiektu 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Ręka Midasa\",\n        },\n        \"mine\": {\n            \"name\": \"Mina wodna\",\n        },\n        \"mine_cart\": {\n            \"name\": \"\\\\{review}Wózek Kopalniany\",\n        },\n        \"mini_copter\": {\n            \"name\": \"Helikopter 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"Pocisk (Atlantydzka bomba)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"Pocisk (Atlantydzki odłamek)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"Pocisk (Płomień)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"Pocisk (Harpun)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"Pocisk (Nóż)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"Pocisk (Trucizna)\",\n        },\n        \"monk_1\": {\n            \"name\": \"Mnich 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"Mnich 2\",\n        },\n        \"monkey\": {\n            \"name\": \"Małpa\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"Działo przeciwpancerne\",\n        },\n        \"mouse\": {\n            \"name\": \"Szczur\",\n        },\n        \"movable_block_1\": {\n            \"name\": \"Blok do pchania 1\",\n        },\n        \"movable_block_2\": {\n            \"name\": \"Blok do pchania 2\",\n        },\n        \"movable_block_3\": {\n            \"name\": \"Blok do pchania 3\",\n        },\n        \"movable_block_4\": {\n            \"name\": \"Blok do pchania 4\",\n        },\n        \"moving_bar\": {\n            \"name\": \"Ruchomy pręt\",\n        },\n        \"mp5\": {\n            \"name\": \"MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"Amunicja do MP5\",\n        },\n        \"mp_1\": {\n            \"name\": \"\\\\{review}MP 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"\\\\{review}MP 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Mumia\",\n        },\n        \"natla\": {\n            \"name\": \"Natla\",\n        },\n        \"natla_gun\": {\n            \"name\": \"Broń Natli\",\n        },\n        \"on_off_light\": {\n            \"name\": \"Światło włącz/wyłącz\",\n        },\n        \"orca\": {\n            \"name\": \"\\\\{review}Orka\",\n        },\n        \"passport\": {\n            \"name\": \"Gra\",\n        },\n        \"patrol_dog\": {\n            \"name\": [\n                \"Pies\",\n                \"Pies patrolowy\",\n            ]\n        },\n        \"pda\": {\n            \"name\": \"Rozgrywka\",\n        },\n        \"pendulum_1\": {\n            \"name\": \"Worek z piaskiem\",\n        },\n        \"pendulum_2\": {\n            \"name\": \"Huśtająca się skrzynia\",\n        },\n        \"photo\": {\n            \"name\": \"Dom Lary\",\n        },\n        \"pickup_1\": {\n            \"name\": \"Znajdźka 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"Znajdźka 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Pomoc do znajdziek\",\n        },\n        \"pierre\": {\n            \"name\": \"Pierre\",\n        },\n        \"pirahnas\": {\n            \"name\": \"Piranie\",\n        },\n        \"pistols\": {\n            \"name\": \"Pistolety\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"Magazynki do pistoletu\",\n        },\n        \"player_1\": {\n            \"name\": \"Aktor 1\",\n        },\n        \"player_10\": {\n            \"name\": \"Aktor 10\",\n        },\n        \"player_2\": {\n            \"name\": \"Aktor 2\",\n        },\n        \"player_3\": {\n            \"name\": \"Aktor 3\",\n        },\n        \"player_4\": {\n            \"name\": \"Aktor 4\",\n        },\n        \"player_5\": {\n            \"name\": \"Aktor 5\",\n        },\n        \"player_6\": {\n            \"name\": \"Aktor 6\",\n        },\n        \"player_7\": {\n            \"name\": \"Aktor 7\",\n        },\n        \"player_8\": {\n            \"name\": \"Aktor 8\",\n        },\n        \"player_9\": {\n            \"name\": \"Aktor 9\",\n        },\n        \"pods\": {\n            \"name\": \"Kokon\",\n        },\n        \"poison_dart\": {\n            \"name\": \"Zatruta strzałka\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"Wyrzutnia zatrutych strzałek\",\n        },\n        \"portacabin\": {\n            \"name\": \"Kabina\",\n        },\n        \"power_saw\": {\n            \"name\": \"Piła tarczowa\",\n        },\n        \"prisoner\": {\n            \"name\": \"\\\\{review}Więzień\",\n        },\n        \"propeller_1\": {\n            \"name\": \"Śmigło samolotu\",\n        },\n        \"propeller_2\": {\n            \"name\": \"Podwodne śmigło\",\n        },\n        \"propeller_3\": {\n            \"name\": \"Wentylator powietrza\",\n        },\n        \"pulse_light\": {\n            \"name\": \"Pulsujące światło\",\n        },\n        \"puma\": {\n            \"name\": \"Puma\",\n        },\n        \"punk_1\": {\n            \"name\": \"\\\\{review}Punk 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"\\\\{review}Punk 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"Przedmiot 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"Przedmiot 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"Przedmiot 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"Przedmiot 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"Miejsce na przedmiot 1 (ukończone)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"Miejsce na przedmiot 2 (ukończone)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"Miejsce na przedmiot 3 (ukończone)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"Miejsce na przedmiot 4 (ukończone)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"Miejsce na przedmiot 1 (puste)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"Miejsce na przedmiot 2 (puste)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"Miejsce na przedmiot 3 (puste)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"Miejsce na przedmiot 4 (puste)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"Czterokołowiec\",\n        },\n        \"quest_1\": {\n            \"name\": \"Przedmiot Misji 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"Przedmiot Misji 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"Przedmiot Misji 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"Przedmiot Misji 4\",\n        },\n        \"raptor\": {\n            \"name\": [\n                \"Raptor\",\n                \"Welociraptor\",\n            ]\n        },\n        \"raptor_emitter\": {\n            \"name\": \"Emiter raptorów\",\n        },\n        \"rat\": {\n            \"name\": [\n                \"Szczur\",\n                \"Szczur lądowy\",\n            ]\n        },\n        \"red_light\": {\n            \"name\": \"Czerwone światło\",\n        },\n        \"rib\": {\n            \"name\": \"\\\\{review}RIB\",\n        },\n        \"ricochet\": {\n            \"name\": \"Odbicie\",\n        },\n        \"rocket\": {\n            \"name\": \"Rakieta\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"Wyrzutnia rakiet\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"Rakiety\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": \"Głaz 1\",\n        },\n        \"rolling_ball_2\": {\n            \"name\": \"Głaz 2\",\n        },\n        \"rolling_ball_3\": {\n            \"name\": \"Głaz 3\",\n        },\n        \"rolling_ball_4\": {\n            \"name\": \"Głaz 4\",\n        },\n        \"rotating_laser\": {\n            \"name\": \"\\\\{review}Obracające się lasery\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"\\\\{review}RX Pracownik 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"\\\\{review}RX Pracownik 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": \"\\\\{review}RX Pracownik 3\",\n        },\n        \"save_crystal\": {\n            \"name\": \"Kryształ do zapisu\",\n        },\n        \"scion\": {\n            \"name\": \"Scion\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Piedestał na Scion\",\n        },\n        \"secret_1\": {\n            \"name\": \"Sekret 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"Sekret 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"Sekret 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"\\\\{review}Ochroniarz\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"\\\\{review}Laser zabezpieczający (Alarm)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"\\\\{review}Laser zabezpieczający (Śmiertelny)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"\\\\{review}Laser zabezpieczający (Zabójczy)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"Działko automatyczne\",\n        },\n        \"shadow\": {\n            \"name\": \"Cień\",\n        },\n        \"shark\": {\n            \"name\": \"Rekin\",\n        },\n        \"shiva\": {\n            \"name\": \"Shiva\",\n        },\n        \"shotgun\": {\n            \"name\": \"Strzelba\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"Naboje do strzelby\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"Łuska do strzelby\",\n        },\n        \"skate_kid\": {\n            \"name\": [\n                \"Kid\",\n                \"Dzieciak na deskorolce\",\n            ]\n        },\n        \"skateboard\": {\n            \"name\": \"Deskorolka\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"Czarny skuter śnieżny\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"Kierowca czarnego skutera śnieżnego\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"Czerwony skuter śnieżny\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"Tor skutera śnieżnego\",\n        },\n        \"skybox\": {\n            \"name\": \"Niebo\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Przesuwalny filar\",\n        },\n        \"smashable_1\": {\n            \"name\": \"Okno 1\",\n        },\n        \"smashable_2\": {\n            \"name\": \"Okno 2\",\n        },\n        \"smashable_3\": {\n            \"name\": \"Okno 3\",\n        },\n        \"smashable_4\": {\n            \"name\": \"Okno 4\",\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"Emiter dymu (czarny)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"Emiter dymu (biały)\",\n        },\n        \"snake\": {\n            \"name\": \"Wąż\",\n        },\n        \"snow_sprite\": {\n            \"name\": \"Ślad skutera śnieżnego\",\n        },\n        \"sophia\": {\n            \"name\": \"\\\\{review}Sophia\",\n        },\n        \"sound\": {\n            \"name\": \"Dźwięk\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"Eksplozja smoka 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"Eksplozja smoka 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"Eksplozja smoka 3\",\n        },\n        \"spider\": {\n            \"name\": \"Pająk\",\n        },\n        \"spike_wall\": {\n            \"name\": \"Ściana z kolcami\",\n        },\n        \"spikes\": {\n            \"name\": \"Kolce\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"Toczące się ostrze\",\n        },\n        \"splash_1\": {\n            \"name\": \"Fale wodne 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Fale wody 2\",\n        },\n        \"springboard\": {\n            \"name\": \"Trampolina\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"Emiter pary\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"Najemnik\",\n        },\n        \"stopwatch\": {\n            \"name\": \"Statystyki\",\n        },\n        \"strobe_light\": {\n            \"name\": \"Światło stroboskopowe\",\n        },\n        \"swat_1\": {\n            \"name\": \"\\\\{review}SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"\\\\{review}SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"\\\\{review}SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"Siekiera\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"Pokretło\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"Przycisk\",\n                \"Przełącznik\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"Dźwignia\",\n                \"Wajcha\",\n                \"Przełącznik\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"Mały przełącznik\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"Podwodna dźwignia\",\n                \"Podwodna wajcha\",\n                \"Podwodny przełącznik\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": \"Koło do obracania\",\n        },\n        \"teeth_trap\": {\n            \"name\": \"Zatrzaskujące się drzwi\",\n        },\n        \"text_box\": {\n            \"name\": \"Ramka interfejsu\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Uchwyt Młota Thora\",\n        },\n        \"thors_head\": {\n            \"name\": \"Młot Thora\",\n        },\n        \"tiger\": {\n            \"name\": \"Tygrys\",\n        },\n        \"tony\": {\n            \"name\": \"Tony\",\n        },\n        \"torso\": {\n            \"name\": \"Gigantyczny mutant\",\n        },\n        \"train\": {\n            \"name\": \"\\\\{review}Pociąg\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Zapadnia 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Zapadnia 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Zapadnia 3\",\n        },\n        \"trex\": {\n            \"name\": [\n                \"T-Rex\",\n                \"Tyranozaur\",\n            ]\n        },\n        \"trex_alpha\": {\n            \"name\": \"T-Rex alfa\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"Tubylec (topornik)\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"Puna\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"Tubylec (dmuchawka)\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"Rybki\",\n        },\n        \"twinkle\": {\n            \"name\": \"Iskierki\",\n        },\n        \"upv\": {\n            \"name\": \"\\\\{review}Miniłódź podwodna\",\n        },\n        \"uzis\": {\n            \"name\": \"Uzi\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"Magazynki do uzi\",\n        },\n        \"vole\": {\n            \"name\": [\n                \"Nornik\",\n                \"Szczur wodny\",\n            ]\n        },\n        \"vulture\": {\n            \"name\": \"Sęp\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"\\\\{review}Osi Mutant\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"\\\\{review}Emiter Osiego Mutanta\",\n        },\n        \"water_sprite\": {\n            \"name\": \"Ślad łodzi\",\n        },\n        \"waterfall\": {\n            \"name\": \"Mgiełka wodospadu\",\n        },\n        \"white_light\": {\n            \"name\": \"Białe światło\",\n        },\n        \"willard\": {\n            \"name\": \"\\\\{review}Willard\",\n        },\n        \"winston\": {\n            \"name\": \"Winston\",\n        },\n        \"winston_army\": {\n            \"name\": \"Winston (umundurowany)\",\n        },\n        \"wolf\": {\n            \"name\": \"Wilk\",\n        },\n        \"worker_1\": {\n            \"name\": \"Oprych z bronią 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"Oprych z bronią 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"Oprych z kijem 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"Oprych z kijem 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"Oprych z miotaczem ognia\",\n        },\n        \"xian_knight\": {\n            \"name\": \"Rycerz Xian\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"Posąg rycerza Xian\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"Włócznik Xian\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"Posąg włócznika Xian\",\n        },\n        \"yeti\": {\n            \"name\": \"Yeti\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"Uchwyt do zjazdu na linie\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings-ru.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"Русский\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Осмотреть\",\n            \"hide_dialog\": \"Скрыть диалог\",\n            \"reset_defaults\": \"Сброс\",\n            \"rotate\": \"Повернуть\",\n            \"unbind\": \"Очистить\",\n            \"use_item\": \"Использовать\",\n        },\n        \"config_presets\": {\n            \"applied\": \"\\\\{review}Предустановка применена.\",\n            \"confirm_description\": \"\\\\{review}Следующие настройки будут изменены:\",\n            \"confirm_restart_note\": \"\\\\{review}Примечание: для вступления изменений в силу может потребоваться перезапуск игры.\",\n            \"empty\": \"\\\\{review}Пресеты не найдены.\",\n            \"no_changes\": \"\\\\{review}Нет изменений для применения.\",\n            \"title_fmt\": \"\\\\{review}Применить предустановку %s?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"\\\\{review}Зона 1\",\n            \"area_2\": \"\\\\{review}Зона 2\",\n            \"area_3\": \"\\\\{review}Зона 3\",\n            \"area_4\": \"\\\\{review}Зона 4\",\n            \"area_5\": \"\\\\{review}Зона 5\",\n            \"area_6\": \"\\\\{review}Зона 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"\\\\{review}Приключение\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"Конец игры\",\n            \"heading_inventory\": \"Инвентарь\",\n            \"heading_items\": \"Предметы\",\n            \"heading_option\": \"Настройки\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Демо режим\",\n            \"direction_keys_controller\": \"Крестовина\",\n            \"direction_keys_keyboard\": \"Стрелки\",\n            \"empty_slot_fmt\": \"- ПУСТОЙ СЛОТ %d -\",\n            \"exit\": \"Выход\",\n            \"hold_fmt\": \"удерж. %s\",\n            \"off\": \"Выкл\",\n            \"on\": \"Вкл\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Неоднозначный ввод: %s и %s\",\n            \"ambiguous_input_3\": \"Неоднозначный ввод: %s, %s, ...\",\n            \"bilinear_filter_off\": \"\\\\{review}Билинейная фильтрация: выкл\",\n            \"bilinear_filter_on\": \"\\\\{review}Билинейная фильтрация: вкл\",\n            \"command_bad_invocation\": \"Недопустимый вызов: %s\",\n            \"command_bool\": \"вкл, выкл\",\n            \"command_decimal\": \"[десятичн.]\",\n            \"command_integer\": \"[цел.]\",\n            \"command_percent\": \"[цел.]\",\n            \"command_unavailable\": \"Команда недоступна\",\n            \"command_valid_values\": \"Корректные значения: %s\",\n            \"complete_level\": \"Уровень завершён!\",\n            \"config_option_get\": \"%s установлен на %s\",\n            \"config_option_set\": \"%s изменён на %s\",\n            \"config_option_unknown_option\": \"Неизвестная опция: %s\",\n            \"current_health_get\": \"Текущее здоровье Лары: %d\",\n            \"current_health_set\": \"Здоровье Лары установлено на %d\",\n            \"door_close\": \"Сезам, закройся!\",\n            \"door_open\": \"Сезам, откройся!\",\n            \"door_open_fail\": \"Рядом с Ларой нет дверей\",\n            \"flipmap_fail_already_off\": \"Перевёрнутая карта уже выключена\",\n            \"flipmap_fail_already_on\": \"Перевёрнутая карта уже включена\",\n            \"flipmap_off\": \"Перевёрнутая карта выключена\",\n            \"flipmap_on\": \"Перевёрнутая карта включена\",\n            \"fly_mode_off\": \"Режим полёта выключен\",\n            \"fly_mode_on\": \"Режим полёта включен\",\n            \"fps_counter_off\": \"Счётчик FPS выключен\",\n            \"fps_counter_on\": \"Счётчик FPS включен\",\n            \"give_item\": \"%s добавлен в инвентарь Лары\",\n            \"give_item_all_guns\": \"Оружие к бою - Лара вооружена до зубов!\",\n            \"give_item_all_keys\": \"Сюрприз! Все необходимые Ларе предметы теперь в её рюкзаке.\",\n            \"give_item_cheat\": \"Рюкзак Лары только что стал намного тяжелее!\",\n            \"heal_already_full_hp\": \"Лара уже полностью здорова\",\n            \"heal_success\": \"Лара исцелена!\",\n            \"invalid_cutscene\": \"Невалидная заставка\",\n            \"invalid_demo\": \"С этим демо что-то не так...\",\n            \"invalid_item\": \"Неизвестный предмет: %s\",\n            \"invalid_level\": \"Невалидный уровень\",\n            \"invalid_object\": \"Невалидный объект\",\n            \"invalid_room\": \"Невалидная комната: %d. Валидные комнаты 0-%d\",\n            \"invalid_sample\": \"Невалидный звук: %d\",\n            \"kill\": \"Пока-пока!\",\n            \"kill_all\": \"Бах! Все враги убиты: %d!\",\n            \"kill_all_fail\": \"Ого! Врагов больше не осталось...\",\n            \"kill_fail\": \"Поблизости нет врагов...\",\n            \"lighting_contrast_fmt\": \"\\\\{review}Контраст освещения: %s\",\n            \"load_game\": \"Игра загружена из слота %d\",\n            \"load_game_fail_invalid_slot\": \"Неверный слот сохранения %d\",\n            \"load_game_fail_unavailable_slot\": \"Слот сохранения %d недоступен\",\n            \"object_not_found\": \"Объект не найден\",\n            \"play_cutscene\": \"Загрузка заставки %d\",\n            \"play_demo\": \"Загрузка демо %d\",\n            \"play_level\": \"Загрузка %s\",\n            \"pos_lara_missing\": \"Лара отсутствует\",\n            \"pos_lara_pos_fmt\": \"Комната: %d\\nПозиция: %.3f, %.3f, %.3f\\nВращение: %.3f,%.3f,%.3f\",\n            \"pos_level_fmt\": \"Уровень %d\",\n            \"pos_level_fmt_cutscene\": \"Заставка %d\",\n            \"pos_level_fmt_demo\": \"Демо %d\",\n            \"quick_load\": \"\\\\{review}Быстро загруженный слот %d\",\n            \"quick_load_fail_no_bound_slot\": \"\\\\{review}В данный момент не привязан ни один слот сохранения\",\n            \"quick_load_fail_unavailable_bound_slot\": \"\\\\{review}Привязанный слот сохранения недоступен\",\n            \"quick_save\": \"\\\\{review}Быстро сохранено\",\n            \"quick_save_fail_no_slots\": \"\\\\{review}Слоты быстрого сохранения не настроены\",\n            \"save_game\": \"Игра сохранена в слот %d\",\n            \"save_game_fail_invalid_slot\": \"Неверный слот сохранения %d\",\n            \"sound_available_samples\": \"Доступные звуки : %s\",\n            \"sound_playing_sample\": \"Воспроизведение звука %d\",\n            \"speed_get\": \"Текущая скорость: %d\",\n            \"speed_set\": \"Скорость установлена на %d\",\n            \"strings_failed\": \"Ошибка перезагрузки языковых файлов\",\n            \"strings_reloaded\": \"Языковые файлы перезагружены\",\n            \"textures_off\": \"\\\\{review}Текстуры: выключены\",\n            \"textures_on\": \"\\\\{review}Текстуры: включены\",\n            \"trapezoid_filter_off\": \"Четырёхугольная интерполяция выключена\",\n            \"trapezoid_filter_on\": \"Четырёхугольная интерполяция включена\",\n            \"ui_off\": \"Интерфейс выключен\",\n            \"ui_on\": \"Интерфейс включен\",\n            \"unknown_command\": \"Неизвестная команда: %s\",\n            \"upscaling_factor\": \"Коэффициент масштабирования: x%d\",\n            \"wireframe_mode_off\": \"\\\\{review}Режим каркаса: выкл\",\n            \"wireframe_mode_on\": \"\\\\{review}Режим каркаса: вкл\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"Анимация: \",\n            \"debug_animation_state\": \"\\\\{review}Состояние: \",\n            \"debug_camera_pos\": \"\\\\{review}Положение камеры: \",\n            \"debug_camera_target\": \"\\\\{review}Цель камеры: \",\n            \"debug_immune\": \"Неуязвимость вкл.\",\n            \"debug_position\": \"Позиция: \",\n            \"debug_rotation\": \"Поворот: \",\n            \"debug_speed\": \"Скорость: \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"\\\\{review}Удалить\",\n            \"delete_save_confirm\": \"\\\\{review}Удалить это сохранение?\",\n            \"delete_save_failed\": \"\\\\{review}Не удалось удалить выбранное сохранение.\",\n            \"delete_save_no\": \"\\\\{review}Нет\",\n            \"delete_save_yes\": \"\\\\{review}Да\",\n            \"exit_game\": \"Выход\",\n            \"exit_to_title\": \"Выйти в меню\",\n            \"load_game\": \"Загрузить игру\",\n            \"mode_new_game\": \"Новая игра\",\n            \"mode_new_game_jp\": \"Японская новая игра\",\n            \"mode_new_game_jp_plus\": \"Японская новая игра+\",\n            \"mode_new_game_plus\": \"Новая игра+\",\n            \"new_game\": \"Новая игра\",\n            \"play_previous_levels\": \"\\\\{review}Играть в предыдущие уровни\",\n            \"restart_level\": \"Перезапустить уровень\",\n            \"save_game\": \"Сохранить игру\",\n            \"save_slot_unsupported\": \"\\\\{review}Это сохранение не поддерживает эту функцию.\",\n            \"select_level\": \"Выбор уровня\",\n            \"select_mod\": \"\\\\{review}Выбрать игру\",\n            \"select_mode\": \"Выбор режима\",\n            \"select_save\": \"\\\\{review}Выбрать сохранение\",\n            \"story_so_far\": \"История до сих пор...\",\n            \"switch_mod\": \"\\\\{review}Сменить игру\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"Вы уверены?\",\n            \"continue\": \"Остаться\",\n            \"exit_to_title\": \"Выйти в меню?\",\n            \"no\": \"Нет\",\n            \"paused\": \"Пауза\",\n            \"quit\": \"Выйти в меню\",\n            \"yes\": \"Да\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"Дополнительный кадр\",\n            \"camera_move_prompt\": \"Двигать камеру\",\n            \"camera_reset_prompt\": \"Сбросить камеру\",\n            \"camera_roll_prompt\": \"Вращать камеру\",\n            \"camera_rotate_90_prompt\": \"Поворот на 90°\",\n            \"camera_rotate_prompt\": \"Повернуть камеру\",\n            \"change_lara_pose\": \"Изменить позу Лары\",\n            \"fov_prompt\": \"Изменить угол обзора\",\n            \"lara_move_prompt\": \"Двигать Лару\",\n            \"lara_reset_prompt\": \"Сбросить Лару\",\n            \"lara_roll_prompt\": \"Вращать Лару\",\n            \"lara_rotate_90_prompt\": \"Поворот Лары на 90°\",\n            \"lara_rotate_prompt\": \"Повернуть Лару\",\n            \"snap_prompt\": \"Сделать скриншот\",\n            \"title_camera_pos\": \"Фоторежим\",\n            \"title_lara_pos\": \"Положение Лары\",\n            \"toggle_help\": \"Вкл/выкл помощь\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"\\\\{review}Настройки отключены для этого набора уровней.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Изменить значение\",\n                \"frozen_option_disclaimer\": \"Этот параметр принудительно установлен редактором уровней и не может быть изменён.\",\n                \"hue\": \"Оттенок\",\n                \"lightness\": \"Светлота\",\n                \"restore_default\": \"По умолчанию\",\n                \"toggle_help\": \"Показать помощь\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Контроллер\",\n                    \"keyboard\": \"Клавиатура\",\n                },\n                \"customize\": \"Настроить управление\",\n                \"layout\": {\n                    \"custom_1\": \"Пользовательская схема 1\",\n                    \"custom_2\": \"Пользовательская схема 2\",\n                    \"custom_3\": \"Пользовательская схема 3\",\n                    \"default\": \"По умолчанию\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Движение\",\n                    \"items\": \"Предметы\",\n                    \"misc\": \"Разное\",\n                    \"system\": \"Система\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Управление\",\n                    \"fixes\": \"Исправления\",\n                    \"general\": \"Основные\",\n                    \"mods\": \"Модификации\",\n                    \"presets\": \"\\\\{review}Пресеты\",\n                },\n                \"title\": \"Игровой процесс\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"\\\\{review}Брусья\",\n                    \"rendering\": \"Графика\",\n                    \"stats\": \"\\\\{review}Статистика\",\n                    \"ui\": \"Интерфейс\",\n                    \"visuals\": \"Визуальные эффекты\",\n                },\n                \"title\": \"Настройки изображения\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"\\\\{review}Разное\",\n                    \"volume\": \"\\\\{review}Громкость\",\n                },\n                \"title\": \"Настройки звука\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Попадания/выстрелы\",\n            \"ammo_hits\": \"\\\\{review}Попадания\",\n            \"ammo_used\": \"\\\\{review}Использовано боеприпасов\",\n            \"assault_best_time_fmt\": \"\\\\{review}%s\",\n            \"assault_finish\": \"\\\\{review}Завершить\",\n            \"assault_no_times_set\": \"\\\\{review}Время не установлено\",\n            \"assault_other_times_fmt\": \"\\\\{review}%s\",\n            \"assault_title\": \"\\\\{review}ЛУЧШИЕ ВРЕМЕНА\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Бонусная статистика\",\n            \"crystals\": \"\\\\{review}Кристаллы\",\n            \"deaths\": \"Смертей\",\n            \"detail_fmt\": \"%d из %d\",\n            \"distance_travelled\": \"Пройденное расстояние\",\n            \"final_statistics\": \"Финальная статистика\",\n            \"gym_assault_course\": \"\\\\{review}Полоса препятствий\",\n            \"gym_racetrack_course\": \"\\\\{review}Гоночная трасса\",\n            \"kills\": \"Убито\",\n            \"level\": \"Уровень\",\n            \"medipacks_used\": \"Использовано аптечек\",\n            \"none\": \"\\\\{review}Нет\",\n            \"pickups\": \"Предметы\",\n            \"secrets\": \"Секреты\",\n            \"time_taken\": \"Время\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Переключает косу Лары.\",\n            },\n            \"cheats\": {\n                \"help\": \"Вкл/выкл внутриигровые читы.\",\n            },\n            \"clear\": {\n                \"help\": \"\\\\{review}Очищает видимые журналы консоли.\",\n            },\n            \"debug\": {\n                \"help\": \"Переключает визуальную отладочную информацию.\",\n            },\n            \"drain\": {\n                \"help\": \"Осушает текущую комнату, удаляет воду.\",\n            },\n            \"end_level\": {\n                \"help\": \"Завершает текущий уровень.\",\n            },\n            \"exit\": {\n                \"help\": \"Выходит из игры.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Переключает переворачивание карты.\",\n            },\n            \"flood\": {\n                \"help\": \"Затапливает текущую комнату водой.\",\n            },\n            \"fly\": {\n                \"help\": \"Переключает режим полета.\",\n            },\n            \"fps\": {\n                \"help\": \"Изменяет значение FPS.\",\n            },\n            \"give\": {\n                \"help\": \"Добавляет указанный предмет в инвентарь Лары.\",\n                \"invalid_secret\": \"Невалидный секрет: %s (валидные секреты: %s)\",\n                \"secret_given\": \"Добавлен секрет %s\",\n                \"secret_list\": \"Собрано секретов: %d из %d (%s)\",\n                \"secret_none\": \"Собрано секретов: %d из %d\",\n                \"secret_taken\": \"Удалён секрет %s\",\n            },\n            \"give_secret\": {\n                \"help\": \"Перечисляет секреты Лары, или добавляет/удаляет секрет по номеру.\",\n            },\n            \"heal\": {\n                \"help\": \"Восстанавливает здоровье Лары.\",\n            },\n            \"help\": {\n                \"help\": \"Показывает справку по всем командам или подробную справку по одной.\",\n                \"list\": \"Доступные команды:\",\n            },\n            \"hp\": {\n                \"help\": \"Устанавливает здоровье Лары на указанное значение.\",\n            },\n            \"immune\": {\n                \"help\": \"Переключает неуязвимость (в некоторых ситуациях Лара всё ещё может умереть).\",\n                \"off\": \"Лара теперь уязвима\",\n                \"on\": \"Теперь Лара неуязвима для повреждений\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"\\\\{review}Переключает бесконечный бег.\",\n                \"off\": \"\\\\{review}Лара больше не может бегать вечно\",\n                \"on\": \"\\\\{review}Лара теперь может бегать вечно\",\n            },\n            \"kill\": {\n                \"help\": \"Убивает ближайших врагов.\",\n            },\n            \"lighting\": {\n                \"help\": \"Переключает освещение.\",\n            },\n            \"load\": {\n                \"help\": \"\\\\{review}Загружает игру из указанного слота сохранения или из быстрого сохранения.\",\n            },\n            \"lua\": {\n                \"help\": \"Выполняет заданную строку кода Lua.\",\n                \"runtime_error\": \"Ошибка выполнения Lua: %s\",\n                \"syntax_error\": \"Ошибка синтаксиса Lua: %s\",\n            },\n            \"mod\": {\n                \"help\": \"\\\\{review}Переключается на указанный мод и перезапускает игру.\",\n            },\n            \"music\": {\n                \"help\": \"Воспроизводит музыкальную дорожку с указанным идентификатором.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Воспроизводит кат-сцену с указанным номером.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Воспроизводит демо с указанным номером.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Воспроизводит уровень Тренажёрный зал\",\n            },\n            \"play_level\": {\n                \"help\": \"Воспроизводит уровень с указанным именем или номером\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Невалидная музыка\",\n                \"stopped\": \"\\\\{review}Музыка остановлена\",\n                \"track\": \"Воспроизведение музыки %d\",\n            },\n            \"pos\": {\n                \"help\": \"Показывает позицию Лары\",\n            },\n            \"save\": {\n                \"help\": \"\\\\{review}Сохраняет игру в указанный слот сохранения или в следующий слот быстрого сохранения.\",\n            },\n            \"screenshot\": {\n                \"help\": \"\\\\{review}Сохраняет скриншот на диск с возможным указанием пути.\",\n            },\n            \"set\": {\n                \"help\": \"Отображает или обновляет указанный параметр конфигурации\",\n            },\n            \"sfx\": {\n                \"help\": \"Воспроизводит звуковой эффект с указанным идентификатором\",\n            },\n            \"spawn\": {\n                \"fail\": \"\\\\{review}Не удалось создать запрошенный объект\",\n                \"success\": \"\\\\{review}Запрошенный объект появился рядом с Ларой\",\n            },\n            \"speed\": {\n                \"help\": \"Изменяет скорость игры\",\n            },\n            \"strings\": {\n                \"help\": \"Перезагружает текущие языковые файлы с диска.\",\n            },\n            \"teleport\": {\n                \"item\": \"Телепортировано к объекту: %d\",\n                \"item_fail\": \"Невозможно телепортировать к объекту: %d\",\n                \"object\": \"Телепортировано к объекту: %s\",\n                \"object_fail\": \"Невозможно телепортировать к объекту: %s\",\n                \"pos\": \"Телепортировано в позицию: %.3f %.3f %.3f\",\n                \"pos_fail\": \"Невозможно телепортировать в позицию: %.3f %.3f %.3f\",\n                \"room\": \"Телепортировано в комнату: %d\",\n                \"room_fail\": \"Невозможно телепортировать в комнату: %d\",\n            },\n            \"textures\": {\n                \"help\": \"\\\\{review}Переключает отображение текстур.\",\n            },\n            \"title\": {\n                \"help\": \"Возвращает на титульный экран\",\n            },\n            \"tp\": {\n                \"help\": \"Телепортирует Лару в указанную позицию или номер комнаты.\",\n            },\n            \"trigger\": {\n                \"help\": \"\\\\{review}Активирует или деактивирует предмет по идентификатору, имени предмета или имени объекта.\",\n                \"invalid_item\": \"\\\\{review}Недопустимый предмет: %s\",\n                \"no_match\": \"\\\\{review}Неизвестная цель: %s\",\n                \"not_found\": \"\\\\{review}Совпадающие предметы не найдены для: %s\",\n                \"triggered\": \"\\\\{review}Активированный предмет(ы): %s\",\n                \"untriggered\": \"\\\\{review}Неактивированный предмет(ы): %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Переключает вертикальную синхронизацию.\",\n            },\n            \"weather\": {\n                \"help\": \"\\\\{review}Изменяет текущий тип погоды.\",\n                \"invalid\": \"\\\\{review}Неверная погода: %s (допустимо: %s)\",\n                \"set\": \"\\\\{review}Погода установлена на %s\",\n            },\n            \"winston\": {\n                \"dead\": \"\\\\{review}Ваш дворецкий мёртв. Ты монстр!\",\n                \"spawn_failed\": \"\\\\{review}Не удалось вызвать Уинстона\",\n                \"spawned\": \"\\\\{review}Вызван Уинстон рядом с Ларой\",\n                \"teleported\": \"\\\\{review}Вызван Уинстон рядом с Ларой\",\n            },\n            \"wireframe\": {\n                \"help\": \"Переключает рендеринг каркаса.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"\\\\{review}TR1 PC\",\n            \"tr1_ps1\": \"\\\\{review}TR1 PS1\",\n            \"tr2_pc\": \"\\\\{review}TR2 PC\",\n            \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n            \"tr3_pc\": \"\\\\{review}TR3 PC\",\n            \"tr3_ps1\": \"\\\\{review}TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"TR2 PC\",\n                \"tr2_ps1\": \"\\\\{review}TR2 PS1\",\n                \"tr3_pc\": \"TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"\\\\{review}По умолчанию\",\n                \"golden_sophia\": \"\\\\{review}Золотая София\",\n                \"sophia\": \"\\\\{review}София\",\n                \"tr1_bacon_lara\": \"\\\\{review}Лара Бекон\",\n                \"tr1_classic\": \"\\\\{review}TR1 Классический\",\n                \"tr1_combo\": \"\\\\{review}TR1 Комбинированный\",\n                \"tr1_golden_bacon_lara\": \"\\\\{review}Золотая Лара Бекон\",\n                \"tr1_golden_lara\": \"\\\\{review}TR1 Золотая Лара\",\n                \"tr1_gym\": \"\\\\{review}TR1 Тренировочный\",\n                \"tr1_mauled\": \"\\\\{review}TR1 Искалеченная\",\n                \"tr1_ngage\": \"\\\\{review}ТР1 Эн-Гейдж\",\n                \"tr23_golden_lara\": \"\\\\{review}TR2/3 Золотая Лара\",\n                \"tr2_bomber_jacket\": \"\\\\{review}Куртка-бомбер\",\n                \"tr2_classic\": \"\\\\{review}TR2 Классический\",\n                \"tr2_diving_suit\": \"\\\\{review}Водолазный костюм 1\",\n                \"tr2_diving_suit_alpha\": \"\\\\{review}Водолазный костюм 2\",\n                \"tr2_gym\": \"\\\\{review}TR2 Тренировочный\",\n                \"tr2_robe\": \"\\\\{review}Платье\",\n                \"tr2_vegas\": \"\\\\{review}Лас-Вегас\",\n                \"tr3_antarctica\": \"\\\\{review}Антарктида\",\n                \"tr3_catsuit\": \"\\\\{review}Кэтсьют\",\n                \"tr3_classic\": \"\\\\{review}TR3 Классический\",\n                \"tr3_gym\": \"\\\\{review}TR3 Тренировочный\",\n                \"tr3_nevada\": \"\\\\{review}Невада\",\n                \"tr3_south_pacific\": \"\\\\{review}Южный Тихий океан\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"\\\\{review}Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"\\\\{review}Демо Tomb Raider I\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"\\\\{review}Незавершённое дело\",\n            },\n            \"tr2\": {\n                \"title\": \"\\\\{review}Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"\\\\{review}Золотая маска\",\n            },\n            \"tr3\": {\n                \"title\": \"\\\\{review}Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"\\\\{review}Потерянный артефакт\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"\\\\{review}Индивидуально\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"\\\\{review}Общее\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"АВТО\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"\\\\{review}Черный\",\n            \"BK_IMAGE\": \"\\\\{review}Изображение\",\n            \"BK_MONOCHROME\": \"\\\\{review}Монохром\",\n            \"BK_MONOCHROME_COOL\": \"\\\\{review}Монохромный (холодный)\",\n            \"BK_MONOCHROME_WARM\": \"\\\\{review}Монохромный (теплый)\",\n            \"BK_NONE\": \"\\\\{review}Прозрачный\",\n            \"BK_PATTERN_STATIC\": \"\\\\{review}Статический\",\n            \"BK_PATTERN_WAVE\": \"\\\\{review}Волна\",\n            \"BK_TRANSPARENT_DARK\": \"\\\\{review}Очень темно\",\n            \"BK_TRANSPARENT_MEDIUM\": \"\\\\{review}Темно\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"Всегда\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"Только боссы\",\n            \"BAR_SHOW_MODE_NEVER\": \"Никогда\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"\\\\{review}Нет\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"\\\\{review}Перспектива\",\n            \"BILLBOARD_LOCK_ROLL\": \"\\\\{review}Крен\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"\\\\{review}Крен и тангаж\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"\\\\{review}Отключено\",\n            \"BLOOD_EFFECTS_PINK\": \"\\\\{review}Розовый\",\n            \"BLOOD_EFFECTS_RED\": \"\\\\{review}Красный\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"\\\\{review}TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"\\\\{review}По умолчанию\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"\\\\{review}Никогда\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"\\\\{review}Погружены\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"\\\\{review}Контроллер\",\n            \"INPUT_BACKEND_KEYBOARD\": \"\\\\{review}Клавиатура\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Действие\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Камера назад\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Камера вниз\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Камера вперёд\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Камера влево\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"\\\\{review}Сброс камеры\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Камера вправо\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Камера вверх\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"\\\\{review}Сменить костюм\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Изменить цель\",\n            \"INPUT_ROLE_CROUCH\": \"\\\\{review}Присесть\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"\\\\{review}Цикл контраста освещения\",\n            \"INPUT_ROLE_DOWN\": \"Назад\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Оружие\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Консоль разработчика\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"Оснастить автоматические пистолеты\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"\\\\{review}Оснастить Дезерт Игл\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"\\\\{review}Оснастить гранатомет\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"\\\\{review}Оснастить гарпуном\",\n            \"INPUT_ROLE_EQUIP_M16\": \"\\\\{review}Оснастить M16\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"Магнумы\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"\\\\{review}Оснастить MP5\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"Пистолеты\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"\\\\{review}Экипировать ракетную установку\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"Дробовик\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"Узи\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Чит Полёт\",\n            \"INPUT_ROLE_FPS\": \"Показать FPS\",\n            \"INPUT_ROLE_INVENTORY\": \"Инвентарь\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Чит Предметы\",\n            \"INPUT_ROLE_JUMP\": \"Прыжок\",\n            \"INPUT_ROLE_LEFT\": \"Влево\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Чит Пропуск уровня\",\n            \"INPUT_ROLE_LOAD\": \"\\\\{review}Загрузить\",\n            \"INPUT_ROLE_LOOK\": \"Обзор\",\n            \"INPUT_ROLE_PAUSE\": \"Пауза\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"\\\\{review}Быстрая загрузка\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"\\\\{review}Быстрое сохранение\",\n            \"INPUT_ROLE_RIGHT\": \"Вправо\",\n            \"INPUT_ROLE_ROLL\": \"Кувырок\",\n            \"INPUT_ROLE_SAVE\": \"\\\\{review}Сохранить\",\n            \"INPUT_ROLE_SCREENSHOT\": \"\\\\{review}Скриншот\",\n            \"INPUT_ROLE_SLOW\": \"Ходьба\",\n            \"INPUT_ROLE_SPRINT\": \"\\\\{review}Бег\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Шаг влево\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Шаг вправо\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"Изменить размер рамки\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"Изменить коэффициент масштабирования\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"\\\\{review}Переключить билинейную фильтрацию\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"\\\\{review}Переключить полноэкранный режим\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Фоторежим\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"\\\\{review}Переключить текстуры\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"Вкл/выкл четырёхугольную интерполяцию\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"Вкл/выкл интерфейс\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"\\\\{review}Переключить каркасный режим\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Турбо скорость\",\n            \"INPUT_ROLE_UP\": \"Вперёд\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Большая аптечка\",\n            \"INPUT_ROLE_USE_FLARE\": \"\\\\{review}Вспышка\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Малая аптечка\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"Отключена\",\n            \"JUMP_LOCK_LEGACY\": \"Оригинал\",\n            \"JUMP_LOCK_TUNED\": \"Оптимально\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"Высокая\",\n            \"LIGHTING_CONTRAST_LOW\": \"Низкая\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Средняя\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"\\\\{review}Всегда\",\n            \"LOADING_SCREENS_DISABLED\": \"\\\\{review}Отключено\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"\\\\{review}Новые игры\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"Расширенный\",\n            \"LOOK_MODE_RESTRICTED\": \"Ограниченный\",\n            \"LOOK_MODE_UNRESTRICTED\": \"Неограниченный\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"Всегда\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"Никогда\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Кроме фона\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"\\\\{review}Множественное сканирование\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"\\\\{review}Однократное сканирование\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"Достать или убрать\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"Только достать\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"\\\\{review}Круг\",\n            \"SHADOW_TYPE_OCTAGON\": \"\\\\{review}Восьмиугольник\",\n            \"SHADOW_TYPE_SPRITE\": \"\\\\{review}Спрайт\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"\\\\{review}Простой\",\n            \"STATS_STYLE_BORDERED\": \"\\\\{review}С рамкой\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"\\\\{review}Выкл\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"\\\\{review}Непрозрачный\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"\\\\{review}Прозрачный\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Полный захват\",\n            \"TARGET_LOCK_MODE_NONE\": \"Без захвата\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Полузахват\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Билинейная\",\n            \"TEXTURE_FILTER_POINT\": \"Точечная выборка\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"Внизу по центру\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"Внизу слева\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"Внизу справа\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"Вверху по центру\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"Вверху слева\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"Вверху справа\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"Исправлен\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"\\\\{review}Громкость окружения\",\n            \"description\": \"\\\\{review}Регулирует громкость окружения.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"\\\\{review}Громкость катсцен\",\n            \"description\": \"\\\\{review}Регулирует громкость катсцен в игре.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Микрофон рядом с Ларой\",\n            \"description\": \"Устанавливает микрофон в положение Лары. Если эта функция отключена, микрофон будет в положении камеры.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"\\\\{review}Воспроизвести музыку в инвентаре\",\n            \"description\": \"\\\\{review}Позволяет звукам игры, атмосфере и музыке продолжать играть на экране инвентаря.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Музыка в главном меню\",\n            \"description\": \"Воспроизводить музыку в главном меню.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Разнообразные звуки\",\n            \"description\": \"Случайным образом слегка изменять тон звуковых эффектов, чтобы разнообразить звучание игры.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"\\\\{review}Замены звуковых эффектов PS1\",\n            \"description\": \"\\\\{review}Включает определённые замены звуковых эффектов, используя эквиваленты с PS1.\\n\\n- Выстрелы из Узи (только TR1)\\n- Звуки босых ног Лары (только TR2)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"Подводные звуковые эффекты\",\n            \"description\": \"Позволяет управлять воспроизведением определенных анимационных звуковых эффектов (для таких объектов, как двери и люки), когда камера находится под водой.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Исправить звук золотого ключа\",\n            \"description\": \"Предотвращает неправильное воспроизведение секретного звука при использовании золотого ключа в Гробнице Тихокана.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Исправить звук секрета\",\n            \"description\": \"Исправляет ситуацию, когда звук нахождения секрета прерывает активную музыкальную дорожку.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Исправить звук разговора\",\n            \"description\": \"Исправляет ситуацию, когда звук разговора врагов прерывает активную музыкальную дорожку.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"\\\\{review}Громкость FMV\",\n            \"description\": \"\\\\{review}Регулирует громкость видеороликов.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"\\\\{review}Громкость окружения (инвентарь)\",\n            \"description\": \"\\\\{review}Регулирует громкость окружения в инвентаре.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"\\\\{review}Громкость музыки (инвентарь)\",\n            \"description\": \"\\\\{review}Регулирует громкость музыки в инвентаре.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Исправить музыкальные триггеры\",\n            \"description\": \"Загружает ранее запущенную одноразовую музыку, поэтому дорожки одноразовой музыки не воспроизводятся повторно.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{review}\\\\{icon music} Главная громкость\",\n            \"description\": \"\\\\{review}Регулирует общую громкость в игре. Остальные настройки зависят от этой громкости.\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Восстанавливать музыку\",\n            \"description\": \"Загружать музыкальную дорожку, которая воспроизводилась во время сохранения игры.\\n\\n- Никогда: не восстанавливать музыкальные дорожки при загрузке.\\n- Кроме фона: восстанавливать только дорожки, не связанные с фоновой музыкой.\\n- Всегда: восстанавливать любые музыкальные дорожки.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"\\\\{review}Громкость музыки\",\n            \"description\": \"\\\\{review}Регулирует громкость музыки.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"Отключить звук при потере фокуса\",\n            \"description\": \"Отключать всю музыку и звуковые эффекты, когда окно игры не активно.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Громкость звуков\",\n            \"description\": \"Громкость звуковых эффектов.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"\\\\{review}Громкость окружения (под водой)\",\n            \"description\": \"\\\\{review}Регулирует громкость окружения под водой.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"\\\\{review}Громкость музыки (под водой)\",\n            \"description\": \"\\\\{review}Регулирует громкость музыки под водой.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"\\\\{review}Бесконечное время горения факелов\",\n            \"description\": \"\\\\{review}Предотвращает затухание ручных факелов. Брошенные факелы по-прежнему будут гаснуть как обычно.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"\\\\{review}Бесконечный спринт\",\n            \"description\": \"\\\\{review}Предотвращает усталость Лары при беге. Препятствия по-прежнему остановят её.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"\\\\{review}Политика враждебности союзников\",\n            \"description\": \"\\\\{review}Управляет реакцией дружественных юнитов при получении урона.\\n\\n- Индивидуально: каждый союзник меняет враждебность самостоятельно (стиль TR3).\\n- Общее: все союзники становятся враждебными вместе (стиль монаха из TR2).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Скорость камеры\",\n            \"description\": \"Изменяет скорость камеры в ручном режиме.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Изменить режим возрождения Пьера\",\n            \"description\": \"Заставляет только что возродившегося (убегающего) Пьера заменить уже существующего (убегающего) Пьера.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"\\\\{review}Поведение утопления существ\",\n            \"description\": \"\\\\{review}Определяет поведение наземных существ в водных помещениях.\\n\\n- Никогда: наземные существа никогда не тонут (стиль TR1).\\n- По умолчанию: наземные существа тонут в воде глубиной 2 клика и более (стиль TR2/3).\\n- Погружены: наземные существа тонут только при полном погружении.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"\\\\{review}Удалить лишнее оружие\",\n            \"description\": \"\\\\{review}Удаляет все оружие и боеприпасы из игры, кроме пистолетов (для испытаний только с пистолетами).\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Постоянный урон\",\n            \"description\": \"Не исцелять Лару в начале нового уровня (для прохождений No Heal).\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Убрать аптечки\",\n            \"description\": \"Убрать из игры все аптечки (для прохождений No Meds).\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Убрать коллизии с тираннозавром\",\n            \"description\": \"Устраняет все столкновения с тираннозавром после его смерти. Это помогает, когда тело тираннозавра блокирует проход.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"Разрешить нацеливание на союзников\",\n            \"description\": \"Позволяет Ларе нацеливаться на союзников, таких как монахи. Если этот параметр отключен, союзники будут неуязвимы для боеприпасов Лары.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Автоматический выбор предмета\",\n            \"description\": \"Когда Лара нажимает кнопку действия возле замочной скважины или головоломки, и у неё в инвентаре есть соответствующий предмет, этот предмет будет предварительно выбран.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"\\\\{review}Спотыкание на склоне сзади\",\n            \"description\": \"\\\\{review}Заставляет Лару споткнуться, если она отпрыгивает назад и позади находится склон (TR3). Если отключено, Лара резко останавливается, упираясь в склон (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"\\\\{review}Триггеры мешков для тел\",\n            \"description\": \"\\\\{review}Включает удаление убитых врагов, когда Лара пересекает определённые триггеры в некоторых уровнях. Если отключено, мёртвые враги всегда будут отображаться.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"\\\\{review}Включить тряску при движении валуна\",\n            \"description\": \"\\\\{review}Если включено, камера будет трястись, когда валун находится в движении.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"\\\\{review}Прыгающие гранаты\",\n            \"description\": \"\\\\{review}Включает поведение гранат в стиле TR3: они отскакивают от стен и склонов и создают большую зону взрыва, но за счет уменьшенной скорости.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Читы\",\n            \"description\": \"Включает различные читы:\\n\\n- L: немедленно завершить уровень.\\n- I: дать Ларе всё оружие, запас боеприпасов и аптечек, а также все сюжетные предметы для текущего уровня.\\n- O: включить режим полёта (плавание в воздухе).\\n  - Клавиша ХОДЬБА: выйти из режима полёта.\\n  - Клавиша ОРУЖИЕ: открыть ближайшую дверь (не работает в некоторых местах).\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"\\\\{review}Скриптовые сцены\",\n            \"description\": \"\\\\{review}Включает скриптовые сцены в начале некоторых уровней, где они предусмотрены.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Статистика уровня в компасе\",\n            \"description\": \"Отображать статистику уровня при выборе компаса.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Консоль\",\n            \"description\": \"Включает консоль разработчика.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"Контролируемое падение\",\n            \"description\": \"Позволяет Ларе развернуться в воздухе и ухватиться за выступ, с которого она только что сошла, если удерживать клавишу действия во время падения.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"\\\\{review}Прыжок при выходе из лазов\",\n            \"description\": \"\\\\{review}Позволяет Ларе выпрыгивать из лазов.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"\\\\{review}Наклон при ползании\",\n            \"description\": \"\\\\{review}Выравнивает поворот Лары по геометрии пола при ползании.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"\\\\{review}Ползание\",\n            \"description\": \"\\\\{review}Позволяет Ларе приседать и ползать.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"Включить титры\",\n            \"description\": \"Отображать экраны с титрами после завершения игры. Не влияет на финальный экран статистики.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"\\\\{review}Перекат из приседа\",\n            \"description\": \"\\\\{review}Позволяет Ларе делать перекат вперед из приседа по нажатию спринта.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Заставки\",\n            \"description\": \"Воспроизводить внутриигровые заставки.\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Демо режим\",\n            \"description\": \"Показывать демонстрации в главном меню.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Разнообразить угол наклона врагов\",\n            \"description\": \"Применяет дополнительный случайный угол наклона к некоторым врагам при их инициализации.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Сохранить эффекты\",\n            \"description\": \"Улучшает сохранения игры, благодаря чему графические эффекты, туман от водопада, излучатели пламени и многое другое сохраняется, а не исчезает при загрузке.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"Видеоролики\",\n            \"description\": \"Воспроизводить видеоролики.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Выбор режима игры\",\n            \"description\": \"Позволяет выбирать дополнительные режимы игры в меню паспорта новой игры.\\n\\n- Новая игра+: разблокирует всё оружие с бесконечным боезапасом; у врагов вдвое больше здоровья.\\n- Японская новая игра: оружие наносит двойной урон и сигнальных ракет 8, а не 6.\\n- Японская новая игра+: комбинация Новой игры+ и Японской новой игры.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"\\\\{review}Камера позы\",\n            \"description\": \"\\\\{review}Настраивает камеру так, чтобы она была направлена на Лару во время анимации позы. Нажмите кнопку «осмотреться», чтобы сбросить камеру.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Инвертировать обзор\",\n            \"description\": \"Инвертировать вертикальную ось во время обзора.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Описание предметов\",\n            \"description\": \"Для пользовательских уровней - позволяет отображать описания предметов в инвентаре, если автор уровня предоставил эти данные.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Акробатика\",\n            \"description\": \"Позволяет делать прыжки с поворотами и сальто (нажмите кнопку КУВЫРОК во время прыжка и ныряния).\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"\\\\{review}Включить смертельные толкаемые блоки\",\n            \"description\": \"\\\\{review}Если включено, когда толкаемый блок падает с воздуха и приземляется на Лару, он убьет ее мгновенно. В противном случае Лара зацепится за верх блока и выживет.\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Компактные прыжки\",\n            \"description\": \"Позволяет Ларе продвигаться вперед или назад дальше при выполнении нейтральных прыжков с нажатой соответствующей клавишей ввода.\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"\\\\{review}Прыжки с выступа\",\n            \"description\": \"\\\\{review}Позволяет Ларе прыгнуть вверх или назад, когда она висит на уступе, если перед ней есть твёрдая поверхность, от которой можно оттолкнуться.\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Ролики правообладателей\",\n            \"description\": \"Показывать логотипы и ролики правообладателей при запуске игры.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"\\\\{review}Ручное управление камерой\",\n            \"description\": \"\\\\{review}Включает клавиши управления камерой (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}), используемые для управления камерой в режиме фотографии, для также вращения игровой камеры.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"Нейтральные перевороты\",\n            \"description\": \"Позволяет Ларе переворачиваться в воздухе, выполняя нейтральный прыжок. Нажмите клавиши ПРЫЖОК и КУВЫРОК одновременно, не двигаясь с места.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Помощь при подборе\",\n            \"description\": \"Включить эффект прерывистого мерцания возле подбираемых предметов, чтобы выделить их присутствие.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"\\\\{review}Играть в предыдущие уровни\",\n            \"description\": \"\\\\{review}Включает функции \\\"Играть предыдущие уровни\\\" и \\\"История на данный момент...\\\" в меню выбора Новая игра.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"\\\\{review}Отзывчивое ползание\",\n            \"description\": \"\\\\{review}Включает улучшения по сравнению с оригинальной механикой ползания.\\n\\n- Позволяет быстрее возобновить ползание после остановки.\\n- Позволяет переходить из бега/спринта в ползание без предварительной остановки.\\n- Позволяет переходить из ползания в перекат в приседе (если включён) без необходимости сначала вручную приседать.\\n- Позволяет поворачиваться в положении приседа.\\n- Восстанавливает анимацию подбора предметов Ларой в положении ползания (кроме фальшфейеров).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"\\\\{review}Реактивный бег\",\n            \"description\": \"\\\\{review}Включает более отзывчивый режим бега для Лары.\\n\\n- позволяет начинать бег, когда у Лары есть энергия, а не только при полном запасе выносливости.\\n- позволяет бежать по лестнице без прерывания обычной анимацией бега.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Кристаллы сохранения\",\n            \"description\": \"Ограничить сохранения только началом уровней и кристаллами сохранения. Количество кристаллов сохранения на уровнях ограничено, они одноразовые, как и в версии для PS1. Изменение этой настройки потребует перезапуска уровня.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"Бег после скольжения\",\n            \"description\": \"Позволяет Ларе начать бежать сразу после того, как она достигнет земли после скольжения вперёд по склону. Удерживайте кнопку ВПЕРЁД, чтобы активировать.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"\\\\{review}Медленное раскачивание на уступе\",\n            \"description\": \"\\\\{review}Позволяет Ларе медленно раскачиваться, ухватившись за очень тонкий уступ (стиль TR3). Если отключено, Лара слегка качнётся и затем перейдёт в неподвижное висячее положение (стиль TR1/2).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"Мягкое столкновение со стеной\",\n            \"description\": \"Позволяет Ларе быстрее восстанавливаться после удара о стену, и клавиша направления удерживается вместе с клавишей Вперёд.\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"\\\\{review}Мягкое столкновение с сеткой\",\n            \"description\": \"\\\\{review}Позволяет Ларе плавно двигаться вдоль статических мешей – как в TR4+ – вместо того чтобы резко останавливаться.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"\\\\{review}Бег\",\n            \"description\": \"\\\\{review}Позволяет Ларе бегать на спринте, как в TR3 и новее.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"Ускорение подъёма\",\n            \"description\": \"Позволяет Ларе одним щелчком подняться выше, если перекат нажат у края.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Отмена раскачивания\",\n            \"description\": \"Позволяет отменить анимацию раскачивания Лары на выступе, если отпустить и быстро схватиться за него снова.\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Изменить цель\",\n            \"description\": \"Позволяет менять цель в стиле TR4+. Нажмите кнопку «Изменить цель» во время прицеливания, чтобы сменить цель.\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Таймер в инвентаре\",\n            \"description\": \"Заставляет внутриигровой таймер работать даже во время отображения инвентаря в игре.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"\\\\{review}Переключение приседания\",\n            \"description\": \"\\\\{review}Позволяет Ларе оставаться присевшей после однократного нажатия кнопки приседания. Нажмите кнопку приседания снова, чтобы встать.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"\\\\{review}Переключение бега\",\n            \"description\": \"\\\\{review}Позволяет Ларе продолжать бегать после однократного нажатия кнопки бега. Нажмите кнопку бега снова, чтобы остановиться.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Экран финальной статистики\",\n            \"description\": \"Включает экран общей статистики игры, который воспроизводится после титров.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Отзывчивые прыжки\",\n            \"description\": \"\\\\{review}Позволяет Ларе прыгать в любой точке во время бега.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Отзывчивая отмена плавания\",\n            \"description\": \"Позволяет Lara быстрее останавливаться под водой при отпускании кнопки плавания.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Плавное плавание\",\n            \"description\": \"Добавляет кривую ускорения скорости поворота Лары под водой для более плавного движения, как в оригинальной версии TR2+. Отключение этой опции увеличит скорость поворота Лары под водой, как в оригинальной версии TR1.\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Переворот под водой\",\n            \"description\": \"Позволяет Ларе переворачиваться под водой.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Идти вброд\",\n            \"description\": \"\\\\{review}Позволяет Ларе преодолевать неглубокую воду, а не застревать на поверхности воды.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Анимированные взаимодействия\",\n            \"description\": \"Лара подходит к предметам и переключателям, когда они находятся поблизости, вместо телепортации к ним.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Исправить поведение аллигаторов\",\n            \"description\": \"Исправляет ошибку, из-за которой аллигаторы не наносили урона, если Лара оставалась неподвижной в воде.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Исправить атаку медведя\",\n            \"description\": \"Исправляет медвежью атаку, чтобы она не промахивалась по Ларе.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Исправить коллизии мостов\",\n            \"description\": \"Исправляет ошибки, из-за которых Лара не могла ухватиться за части некоторых мостов и невидимые стены на краю. Также исправляет проблемы столкновений с разводными мостами, люками и мостами, расположенными друг над другом, на склонах и у земли.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Исправить хрупкий пол\",\n            \"description\": \"Исправляет ошибку, когда при шагании в стороны или назад по разрушаемым плиткам, Лара немедленно спускалась на плитку под ней.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Исправить приоритет сигнальной ракеты\",\n            \"description\": \"Исправляет ошибку, из-за которой Лара отдавала приоритет броску использованной сигнальной ракеты, находясь в воздухе, из-за чего она не могла хвататься за уступы.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Исправить данные пола\",\n            \"description\": \"Исправляет проблемы с данными/триггерами пола.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Исправить бесплатную ракету\",\n            \"description\": \"Исправляет возможность создания бесплатной сигнальной ракеты при нажатии на кнопку запуска ракеты во время подбора любого предмета.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Исправить дублирование предметов\",\n            \"description\": \"Исправляет возможность дублирования использования ключевых предметов в инвентаре.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"\\\\{review}Исправлен баг подбора\",\n            \"description\": \"\\\\{review}Исправляет ошибку, из-за которой Лара иногда проскальзывала в стены при сборе предметов под водой, а также при сборе предметов над водой под крутыми наклонными потолками.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"\\\\{review}Исправить точность M16/MP5\",\n            \"description\": \"\\\\{review}Исправлять точность стрельбы из М16/MP5 во время бега Лары.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"\\\\{review}Исправить приоритет подбора обезьян\",\n            \"description\": \"\\\\{review}Атакованные обезьяны будут отдавать приоритет ответной атаке, а не сбору аптечек и ключей.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"\\\\{review}Прицел духовой трубки\",\n            \"description\": \"\\\\{review}Исправляет проблему, из-за которой стрелок с духовой трубкой иногда не может правильно прицелиться в Лару.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Исправить QWOP-состояние\",\n            \"description\": \"Исправляет странную анимацию бега, известную как QWOP-состояние, которое иногда возникает, когда Лара приземляется на пол во время прыжка.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Исправить глюк ступеней\",\n            \"description\": \"Исправляет ошибку, из-за которой Лара иногда вдавливалась в стены рядом со ступенями, если она бежала по ним определенным образом.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"Исправить переход вброд\",\n            \"description\": \"Исправлять ошибку, из-за которой Лара не реагировала на удары о стену во время перехода вброд.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Исправить прыжок после ходьбы-бега\",\n            \"description\": \"Исправлять ошибку, из-за которой Лара иногда не могла прыгнуть сразу после перехода от анимации ходьбы к анимации бега.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"\\\\{review}Исправление геометрии стен\",\n            \"description\": \"\\\\{review}Исправляет случаи в геометрии уровней OG, где наклоны внутри стен могут приводить к неточным расчётам высоты.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"Исправить выход из воды\",\n            \"description\": \"Исправлять ошибку, из-за которой Лара могла напрямую перейти из водной комнаты в соседнюю сухую комнату или в сухую комнату ниже. Кроме того, это не позволит Ларе выбраться из воды на неустойчивые склоны.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"Частота перезарядки гарпуна\",\n            \"description\": \"Устанавливает частоту перезарядки гарпуна Ларой, исходя из текущего количества патронов. Например, если установлено значение 3, Ларе придётся перезаряжать гарпунное ружьё после каждого третьего выстрела. Установите значение 0, чтобы полностью отключить перезарядку.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"\\\\{review}Таймер ожидания\",\n            \"description\": \"\\\\{review}Позволяет Ларе перейти в анимацию позы после заданного количества секунд бездействия. Установите 0, чтобы отключить.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"Блокировка прыжков\",\n            \"description\": \"Для отзывчивых прыжков: позволяет контролировать, через какое время после начала бега Лара может прыгать.\\n\\n- Оригинал: соответствует оригинальному времени TR2.\\n- Оптимально: прыжок возможен на 2 кадра раньше.\\n- Отключена: прыжок возможен сразу после анимации начала бега.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Загрузочные экраны\",\n            \"description\": \"\\\\{review}Управляет экранами загрузки перед загрузкой уровней.\\n\\n- Отключено: никогда не показывать экраны загрузки.\\n- Всегда: показывать экраны загрузки.\\n- Новые игры: пропускать экраны загрузки при загрузке сохранения.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"Режим обзора\",\n            \"description\": \"Позволяет контролировать, когда Лара может использовать обзор.\\n\\n- Ограниченный: обзор разрешён только когда Лара неподвижна и никогда под водой.\\n- Расширенный: обзор разрешён во время большинства анимаций, за исключением таких, как толкание блока.\\n- Неограниченный: взгляд разрешён в любое время во время обычного управления Ларой.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"\\\\{review}Количество слотов быстрого сохранения\",\n            \"description\": \"\\\\{review}Изменяет количество доступных слотов быстрого сохранения.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Количество слотов сохранения\",\n            \"description\": \"Изменяет количество доступных слотов сохранения.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"\\\\{review}Пауза при потере фокуса\",\n            \"description\": \"\\\\{review}Останавливает игровой процесс, когда окно игры теряет фокус.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"\\\\{review}Урон по области от снарядов\",\n            \"description\": \"\\\\{review}Управляет тем, как распространяется область действия для Ракетной установки и Гранатомёта.\\n\\n- Однократное сканирование: поведение TR1 и TR2.\\n- Множественное сканирование: поведение TR3.\\n\\nОпция множественного сканирования часто приводит к двойному урону по отдельным врагам.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Помнить оружие между уровнями\",\n            \"description\": \"\\\\{review}При переходе на новый уровень Лара запоминает, каким оружием она пользовалась в последний раз на предыдущем уровне. Если отключить, Лара вернётся к пистолетам в кобуре.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"Восстановить врагов PS1\",\n            \"description\": \"\\\\{review}Добавляет мумию, которая появляется в версии для PlayStation в городе Хамун, комната 25.\\nДля применения изменений потребуется перезапустить игру.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Начальное здоровье Лары\",\n            \"description\": \"Устанавливает значение здоровья Лары в начале каждого уровня.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Режим захвата цели\",\n            \"description\": \"Изменяет поведение захвата цели оружием.\\n\\n- Полный захват: всегда удерживать захват цели, даже если враг уходит из поля зрения или умирает (TR1-3).\\n- Полузахват: удерживать захват цели, если враг уходит из поля зрения, но терять захват, если враг умирает.\\n- Без захвата: терять захват цели, если враг уходит из поля зрения или умирает (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"Режим глюка стены\",\n            \"description\": \"Позволяет использовать поведение глюка стены TR1 в TR2 и наоборот; Исправлен: исправлять все типы глюков стены.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"\\\\{review}Буферизация (F-клавиши)\",\n            \"description\": \"\\\\{review}Включает буферизацию клавиши F (1 кадр) для точного управления движением Лары. Эта функция изначально присутствует только в порте TombATI (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"\\\\{review}Буферизация (инвентарь)\",\n            \"description\": \"\\\\{review}Включает буферизацию инвентаря (2 кадра) для точного управления движением Лары.\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Отзывчивый паспорт\",\n            \"description\": \"Отключает блокировку ввода данных пользователем при перелистывании страниц паспорта, вместо этого планирует перелистывание страниц.\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Улучшенные шаги\",\n            \"description\": \"Позволяет делать шаги в сторону в стиле TR3+, например, Shift+стрелки. Отдельные кнопки для шагов в сторону по-прежнему будут работать.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"Быстрые клавиши оружия\",\n            \"description\": \"Управляет поведением клавиш быстрого выбора оружия.\\n\\n- Только достать: нажатие клавиши заставит Лару вытащить назначенное оружие.\\n- Достать или убрать: Лара достаёт назначенное оружие, либо, если оно уже есть у неё в руках, убирает его.\",\n        },\n        \"language\": {\n            \"title\": \"Язык\",\n            \"description\": \"Изменить язык интерфейса.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Анизотропный фильтр\",\n            \"description\": \"Улучшает фильтрацию текстур на расстоянии.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Соотношение сторон\",\n            \"description\": \"Принудительно устанавливает пропорции игры с помощью Letterbox.\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"Рамка\",\n            \"description\": \"Добавляет черные рамки вокруг игрового окна.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Четырёхугольная интерполяция\",\n            \"description\": \"Исправляет отображение четырёхугольников.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"Вертикальная синхронизация\",\n            \"description\": \"Включает или выключает вертикальную синхронизацию.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"FPS\",\n            \"description\": \"Устанавливает количество кадров в секунду в игре.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Контрастность динамического освещения\",\n            \"description\": \"Увеличивает контрастность динамических источников света, таких как вспышки и выстрелы.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Формат снимка экрана\",\n            \"description\": \"Формат снимка экрана.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"\\\\{review}Режим блокировки спрайтов\",\n            \"description\": \"\\\\{review}Управляет осями, которые блокируются при отображении спрайтов на экране.\\n\\n- Нет: отображать спрайты в обычном виде.\\n- Крен: блокировать ось крена – полезно только в режиме фото.\\n- Крен и тангаж: обеспечивать, чтобы спрайты стояли вертикально и не лежали на земле при взгляде сверху.\\n- Перспектива: блокировать оси крена и тангажа, а также слегка поворачивать спрайты к центру экрана.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Фильтрация текстур\",\n            \"description\": \"Переключает между сглаженными и пикселизировнными игровыми текстурами.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"Фильтрация интерфейса\",\n            \"description\": \"Переключает между сглаженными и пикселизированными текстурами элементов интерфейса.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"Коэффициент масштабирования\",\n            \"description\": \"Увеличивает масштаб игры на заданный коэффициент, сохраняя пикселизированный вид.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"Фильтр масштабирования\",\n            \"description\": \"Переключает сглаженный или пиксельный вид для всего экрана.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Цвет шкалы воздуха\",\n            \"description\": \"Цвет шкалы воздуха.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Цвет шкалы воздуха\",\n            \"description\": \"Цвет шкалы воздуха.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Положение шкалы воздуха\",\n            \"description\": \"Место, где отображается шкала воздуха.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"\\\\{review}Расположение счетчика боеприпасов\",\n            \"description\": \"\\\\{review}Место, где отображается счетчик боеприпасов.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"\\\\{review}Внешний вид панелей\",\n            \"description\": \"\\\\{review}Управляет визуальным отображением панелей интерфейса.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Размер шкалы\",\n            \"description\": \"Изменяет размер шкал здоровья, воздуха и врагов.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"\\\\{review}Мигающие полосы\",\n            \"description\": \"\\\\{review}Заставляет полосы здоровья и кислорода Лары мигать, когда один из ресурсов на исходе.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Сглаженные шкалы\",\n            \"description\": \"Использовать плавные цветовые переходы на шкалах здоровья, воздуха и врагов.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Закольцевать список\",\n            \"description\": \"Переходить к началу списка при достижении последнего пункта\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Цвет шкалы здоровья врагов\",\n            \"description\": \"Цвет шкалы здоровья противников.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Цвет шкалы здоровья союзников\",\n            \"description\": \"Цвет шкалы здоровья союзников. Отображается на месте шкалы здоровья врагов.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Цвет шкалы здоровья союзников\",\n            \"description\": \"Цвет шкалы здоровья союзников. Отображается на месте шкалы здоровья врагов.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Цвет шкалы здоровья врагов\",\n            \"description\": \"Цвет шкалы здоровья противников.\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Положение шкалы здоровья врагов\",\n            \"description\": \"Место, где отображается шкала здоровья противника.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Шкала здоровья врагов\",\n            \"description\": \"Отображать индикатор здоровья для активного противника.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"\\\\{review}Цвет шкалы воздействия\",\n            \"description\": \"\\\\{review}Цвет шкалы воздействия холодной воды.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"\\\\{review}Цвет шкалы воздействия\",\n            \"description\": \"\\\\{review}Цвет шкалы воздействия холодной воды.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"\\\\{review}Расположение шкалы воздействия\",\n            \"description\": \"\\\\{review}Место, где отображается шкала воздействия холодной воды.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Цвет шкалы здоровья\",\n            \"description\": \"Цвет шкалы здоровья.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Цвет шкалы здоровья\",\n            \"description\": \"Цвет шкалы здоровья.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Положение шкалы здоровья\",\n            \"description\": \"Место, где отображается шкала здоровья Лары.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"\\\\{review}Цвет полосы здоровья при отравлении\",\n            \"description\": \"\\\\{review}Цвет полосы здоровья, когда Лара отравлена.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"\\\\{review}Цвет полосы здоровья при отравлении\",\n            \"description\": \"\\\\{review}Цвет полосы здоровья, когда Лара отравлена.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"\\\\{review}Фон инвентаря\",\n            \"description\": \"\\\\{review}Изменяет способ отображения фона для кольца инвентаря.\\n\\n- Темный: TR1 (PC).\\n- Очень темный: TR1 (PS1).\\n- Статичный: TR2 (PC).\\n- Волна: TR2 (PS1).\\n- Монохромный: TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"\\\\{review}Эффекты затухания инвентаря\",\n            \"description\": \"\\\\{review}Тонкая настройка включения или отключения эффектов затухания в кольце инвентаря в игре. Для работы требуется включенная опция Эффекты затухания.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Стиль меню\",\n            \"description\": \"Изменияет стиль меню.\\n\\n - PC: стиль меню соответствует версии для ПК.\\n - PS1: стиль меню соответствует версии для PlayStation.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"\\\\{review}Пауза фона\",\n            \"description\": \"\\\\{review}Изменяет способ отображения фона для экрана паузы.\\n\\n- Темный: TR1 (PC).\\n- Очень темный: TR1 (PS1).\\n- Статичный: TR2 (PC).\\n- Волна: TR2 (PS1).\\n- Монохромный: TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"\\\\{review}Эффекты затухания паузы\",\n            \"description\": \"\\\\{review}Тонкая настройка включения или отключения эффектов затухания на экране паузы. Для работы требуется включенная опция Эффекты затухания.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"\\\\{review}Масштаб подбора\",\n            \"description\": \"\\\\{review}Изменяет размер предметов, анимированных в интерфейсе, когда Лара что-то подбирает.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"\\\\{review}Показать полосы\",\n            \"description\": \"\\\\{review}Отключает все игровые полосы, скрывая информацию о здоровье Лары и других ресурсах (для сложных прохождений).\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Отображение подбора\",\n            \"description\": \"Показывает предметы в правом нижнем углу, когда Лара что-то подбирает.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"\\\\{review}Текст версии заголовка\",\n            \"description\": \"\\\\{review}Отображает строку версии TRX в кольце инвентаря заголовка.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"\\\\{review}Цвет индикатора бега\",\n            \"description\": \"\\\\{review}Цвет полоски спринта.\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"\\\\{review}Цвет индикатора бега\",\n            \"description\": \"\\\\{review}Цвет полоски спринта.\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"\\\\{review}Расположение индикатора бега\",\n            \"description\": \"\\\\{review}Расположение, где отображается полоска спринта.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"\\\\{review}Попадания/использовано боеприпасов\",\n            \"description\": \"\\\\{review}Показывает строку боеприпасов в статистике уровня.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"\\\\{review}Кристаллы\",\n            \"description\": \"\\\\{review}Показывает строку с кристаллами в статистике уровня.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"\\\\{review}Смерти\",\n            \"description\": \"\\\\{review}Показывает количество смертей Лары в статистике компаса и в статистике уровня. Количество смертей обновляется в текущем сохранении сразу после смерти Лары.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"\\\\{review}Пройденное расстояние\",\n            \"description\": \"\\\\{review}Показывает строку пройденного расстояния в статистике уровня.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"\\\\{review}Убийства\",\n            \"description\": \"\\\\{review}Показывает строку убийств в статистике уровня.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"\\\\{review}Счетчик уровней\",\n            \"description\": \"\\\\{review}Показывает номер текущего уровня в верхней части статистики уровня.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"\\\\{review}Использованные аптечки\",\n            \"description\": \"\\\\{review}Показывает строку использованных аптечек в статистике уровня.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"\\\\{review}Подобранные предметы\",\n            \"description\": \"\\\\{review}Показывает строку подобранных предметов в статистике уровня.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"\\\\{review}Найденные секреты\",\n            \"description\": \"\\\\{review}Показывает строку найденных секретов в статистике уровня.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"\\\\{review}Затраченное время\",\n            \"description\": \"\\\\{review}Показывает строку затраченного времени в статистике уровня.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"\\\\{review}Показывать итоги\",\n            \"description\": \"\\\\{review}Показывает итоги рядом со статистикой, когда это применимо. Секреты не затрагиваются этой настройкой.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"\\\\{review}Стиль статистики\",\n            \"description\": \"\\\\{review}Управляет отображением диалога статистики.\\n\\n- Простой: показывает упрощенный макет без рамок.\\n- С рамкой: показывает макет в рамке.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"\\\\{review}Фон статистики\",\n            \"description\": \"\\\\{review}Изменяет способ отображения фона для статистики в конце уровня.\\n\\n- Темный: TR1 (PC).\\n- Очень темный: TR1 (PS1).\\n- Статичный: TR2 (PC).\\n- Волна: TR2 (PS1).\\n- Монохромный: TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"\\\\{review}Эффекты затухания статистики\",\n            \"description\": \"\\\\{review}Тонкая настройка включения или отключения эффектов затухания на экране статистики в конце уровня. Для работы требуется включенная опция Эффекты затухания.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Размер текста\",\n            \"description\": \"Изменяет размер текста интерфейса.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"\\\\{review}Эффекты крови\",\n            \"description\": \"\\\\{review}Управляет цветами искр крови.\\n\\n- Отключено: искры крови не отображаются.\\n- Розовый: стандартный цвет в немецких версиях TR3 для ПК.\\n- Красный: стандартный цвет во всех остальных розничных версиях.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Режим камеры\",\n            \"description\": \"Регулирует поведение камеры во время таких действий, как нажатие клавиш.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"3D-предметы\",\n            \"description\": \"Отображать 3D-модели вместо спрайтов для подбираемых предметов.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Коса Лары\",\n            \"description\": \"Показывать косу Лары\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Эффект ветра\",\n            \"description\": \"Добавляет эффект ветра на косе Лары в соответствующих комнатах.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Затухание при выходе\",\n            \"description\": \"Включает эффекты затухания при выходе из игры на рабочий стол.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Плавные переходы\",\n            \"description\": \"Добавляет плавные переходы, например, между экранами инвентаря и паузы, или экранами титров.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"Свет от огня\",\n            \"description\": \"Создаёт динамическое освещение рядом с активным пламенем.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"\\\\{review}Следы\",\n            \"description\": \"\\\\{review}Включает отображение следов Лары на определённых поверхностях в поддерживаемых уровнях.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"\\\\{review}Плавные камеры\",\n            \"description\": \"\\\\{review}Включает эффект плавного перемещения для фиксированных камер, направленных на Лару, с использованием сглаженной кривой скорости. Если отключено, такие камеры будут мгновенно переключаться на вид, направленный на Лару.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Свет от выстрелов\",\n            \"description\": \"\\\\{review}Включает динамическое освещение для выстрелов и взрывов.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"Оттенок кристалла PS1\",\n            \"description\": \"Кристаллы сохранения будут отображаться с фиолетовым оттенком, больше похожим на кристаллы PS1.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Отражения\",\n            \"description\": \"Показывать отражения на определенных объектах.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"\\\\{review}Адаптивная тонировка мешей\",\n            \"description\": \"\\\\{review}Позволяет отрисовывать отдельные меши Лары с водной тонировкой, если они сами находятся под водой (стиль TR3). В противном случае, если Лара находится в воде, все её меши будут отрисовываться с тонировкой (стиль TR1/2).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Вспышки из дробовика\",\n            \"description\": \"Показывать вспышки при выстреле из дробовика, как и из другого оружия.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Скайбоксы\",\n            \"description\": \"Показывать текстуры неба и горизонта на поддерживаемых уровнях.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"\\\\{review}Погода\",\n            \"description\": \"\\\\{review}Включает отображение погодных эффектов в поддерживаемых уровнях.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Исправить анимацию спрайтов\",\n            \"description\": \"\\\\{review}Исправляет оригинальные спрайты подводных растений, чтобы они правильно анимировались в водных зонах.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Исправить поворот предметов\",\n            \"description\": \"Исправляет проблемы с некоторыми неправильно повёрнутыми предметами при использовании опции 3D-предметов.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Исправить ошибки текстур\",\n            \"description\": \"Исправляет проблемы с отсутствующими или неверными текстурами/сетками.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"\\\\{review}Цвет тумана\",\n            \"description\": \"\\\\{review}Цвет тумана.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Конец тумана\",\n            \"description\": \"Расстояние (в тайлах), на котором туман полностью скрывает обзор.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Начало тумана\",\n            \"description\": \"Расстояние (в тайлах), на котором начинает появляться туман.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"\\\\{review}Прозрачность тумана\",\n            \"description\": \"\\\\{review}Включить смешивание удаленной геометрии с полностью прозрачными поверхностями.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"\\\\{review}Поле зрения\",\n            \"description\": \"\\\\{review}Угол обзора в градусах. Большие значения расширяют поле зрения, меньшие — сужают его.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Яркость\",\n            \"description\": \"Изменяет яркость игры.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"\\\\{review}Гамма\",\n            \"description\": \"\\\\{review}Регулирует гамма-кривую. Более высокие значения означают более яркое освещение. Значение 2.5 соответствует цветам по умолчанию.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"\\\\{review}Костюм Лары\",\n            \"description\": \"\\\\{review}Изменяет внешний вид Лары. При выборе По умолчанию сохраняются обычные смены костюма между уровнями; в противном случае выбранный костюм будет использоваться до ручного изменения.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"\\\\{review}Форма теней\",\n            \"description\": \"\\\\{review}Выбирает способ отображения теней объектов.\\n\\n- Восьмиугольник: старые тени из TR1 и TR2\\n- Круг: круглые тени\\n- Спрайт: тени на основе текстур из TR3\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"\\\\{review}Солнцезащитные очки Лары\",\n            \"description\": \"\\\\{review}Изменяет стиль солнцезащитных очков Лары. Примечание: линзы будут отражающими, если соответствующая опция включена.\\n\\n- Выкл: Лара не будет носить солнцезащитные очки.\\n- Непрозрачный: У солнцезащитных очков Лары будут непрозрачные линзы.\\n- Прозрачный: У солнцезащитных очков Лары будут полупрозрачные линзы.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"Яркость интерфейса\",\n            \"description\": \"Изменяет яркость интерфейса.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Цвет воды\",\n            \"description\": \"Цвет воды.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"\\\\{review}Тревога\",\n        },\n        \"alligator\": {\n            \"name\": \"Аллигатор\",\n        },\n        \"alphabet\": {\n            \"name\": \"\\\\{review}Шрифт по умолчанию\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"\\\\{review}Маленький шрифт\",\n        },\n        \"amber_light\": {\n            \"name\": \"\\\\{review}Янтарный свет\",\n        },\n        \"animating_1\": {\n            \"name\": \"\\\\{review}Анимация объекта 1\",\n        },\n        \"animating_10\": {\n            \"name\": \"\\\\{review}Анимация объекта 10\",\n        },\n        \"animating_2\": {\n            \"name\": \"\\\\{review}Анимация объекта 2\",\n        },\n        \"animating_3\": {\n            \"name\": \"\\\\{review}Анимация объекта 3\",\n        },\n        \"animating_4\": {\n            \"name\": \"\\\\{review}Анимация объекта 4\",\n        },\n        \"animating_5\": {\n            \"name\": \"\\\\{review}Анимация объекта 5\",\n        },\n        \"animating_6\": {\n            \"name\": \"\\\\{review}Анимация объекта 6\",\n        },\n        \"animating_7\": {\n            \"name\": \"\\\\{review}Анимация объекта 7\",\n        },\n        \"animating_8\": {\n            \"name\": \"\\\\{review}Анимация объекта 8\",\n        },\n        \"animating_9\": {\n            \"name\": \"\\\\{review}Анимация объекта 9\",\n        },\n        \"ape\": {\n            \"name\": \"Обезьяна\",\n        },\n        \"area_51_rocket\": {\n            \"name\": \"\\\\{review}Ракета Зоны 51\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"\\\\{review}Взрыв ракеты Зоны 51\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"\\\\{review}Поддержка ракеты Зоны 51\",\n        },\n        \"assault_digits\": {\n            \"name\": \"\\\\{review}Цифры штурма\",\n        },\n        \"assault_target\": {\n            \"name\": \"\\\\{review}Цель атаки\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"\\\\{review}Наземный Атлант\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"\\\\{review}Атлант (Стреляющий)\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"\\\\{review}Крылатый Атлант\",\n        },\n        \"autos\": {\n            \"name\": \"\\\\{review}Автоматические пистолеты\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"\\\\{review}Обоймы для автоматических пистолетов\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Двойник Лары\",\n        },\n        \"baldy\": {\n            \"name\": \"Лысый дядька\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"\\\\{review}Наёмник 1\",\n                \"\\\\{review}Маскированный головорез 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"\\\\{review}Наёмник 2\",\n                \"\\\\{review}Маскированный головорез 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"\\\\{review}Наёмник 3\",\n                \"\\\\{review}Маскированный головорез 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"\\\\{review}Барракуда\",\n        },\n        \"bartoli\": {\n            \"name\": \"\\\\{review}Марко Бартоли\",\n        },\n        \"bat\": {\n            \"name\": \"Летучая мышь\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"\\\\{review}Излучатель летучих мышей\",\n        },\n        \"beacon_light\": {\n            \"name\": \"\\\\{review}Маячковый свет\",\n        },\n        \"bear\": {\n            \"name\": \"Медведь\",\n        },\n        \"bell\": {\n            \"name\": \"\\\\{review}Колокол\",\n        },\n        \"big_bowl\": {\n            \"name\": \"\\\\{review}Чаша с лавой\",\n        },\n        \"big_eel\": {\n            \"name\": \"\\\\{review}Большой угорь\",\n        },\n        \"big_pod\": {\n            \"name\": \"Большое гнездо\",\n        },\n        \"big_spider\": {\n            \"name\": \"\\\\{review}Гигантский паук\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"\\\\{review}Птице-монстр\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"\\\\{review}Капающая вода\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"\\\\{review}Поющие птицы\",\n        },\n        \"blade\": {\n            \"name\": \"\\\\{review}Клинок на стене\",\n        },\n        \"blood\": {\n            \"name\": \"\\\\{review}Кровь\",\n        },\n        \"blood_pink\": {\n            \"name\": \"\\\\{review}Кровь (цензурировано)\",\n        },\n        \"blue_light\": {\n            \"name\": \"\\\\{review}Синий свет\",\n        },\n        \"boat\": {\n            \"name\": \"Лодка\",\n        },\n        \"boat_bits\": {\n            \"name\": \"\\\\{review}Обломки лодки\",\n        },\n        \"body_part\": {\n            \"name\": \"Часть тела\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"Плоский мост\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"Наклонный мост 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"Наклонный мост 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"Пузыри 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Пузыри 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"Источник пузырей\",\n        },\n        \"camera_target\": {\n            \"name\": \"Цель камеры\",\n        },\n        \"carcass\": {\n            \"name\": \"\\\\{review}Туша\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"\\\\{review}Острый потолок\",\n        },\n        \"centaur\": {\n            \"name\": \"Кентавр\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Статуя\",\n        },\n        \"civilian\": {\n            \"name\": \"\\\\{review}Гражданский\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"\\\\{review}Когтистый Мутант\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"\\\\{review}Часы убежища Бартоли\",\n        },\n        \"cog_1\": {\n            \"name\": \"Шестерёнка 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Шестерёнка 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Шестерёнка 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"\\\\{review}Конец боя\",\n        },\n        \"compass\": {\n            \"name\": \"Компас\",\n        },\n        \"compy\": {\n            \"name\": \"\\\\{review}Компсогнат\",\n        },\n        \"controls\": {\n            \"name\": \"Управление\",\n        },\n        \"copter\": {\n            \"name\": \"\\\\{review}Вертолет\",\n        },\n        \"cowboy\": {\n            \"name\": \"Ковбой\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"\\\\{review}Ползающий Мутант\",\n        },\n        \"crocodile\": {\n            \"name\": \"Крокодил\",\n        },\n        \"crow\": {\n            \"name\": \"\\\\{review}Ворон\",\n        },\n        \"cult_1\": {\n            \"name\": \"\\\\{review}Бандит в маске 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"\\\\{review}Бандит в маске 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"\\\\{review}Бандит в маске 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"\\\\{review}Метатель ножей\",\n        },\n        \"cult_3\": {\n            \"name\": \"\\\\{review}Бандит с дробовиком\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"\\\\{review}Анимация дробовика\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Дамоклов меч\",\n        },\n        \"dart\": {\n            \"name\": \"Дротик\",\n        },\n        \"dart_effect\": {\n            \"name\": \"Эффект дротика\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Дротикомёт\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"\\\\{review}Дезерт Игл\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"\\\\{review}Магазины Дезерт Игл\",\n        },\n        \"detonator_box\": {\n            \"name\": \"\\\\{review}Коробка детонатора\",\n        },\n        \"ding_dong\": {\n            \"name\": \"\\\\{review}Дверной звонок\",\n        },\n        \"dino_mutant\": {\n            \"name\": \"Динозавр-мутант\",\n        },\n        \"disc\": {\n            \"name\": \"\\\\{review}Диск\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"\\\\{review}Излучатель дисков\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"\\\\{review}Одноразовый Аниматор 9\",\n        },\n        \"diver\": {\n            \"name\": \"\\\\{review}Водолаз\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"\\\\{review}Собака\",\n                \"\\\\{review}Доберман\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Дверь 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Дверь 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Дверь 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Дверь 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Дверь 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Дверь 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Дверь 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Дверь 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"\\\\{review}Дракон сзади\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"\\\\{review}Заполнитель\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"\\\\{review}Передние кости дракона\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"\\\\{review}Задние кости дракона\",\n        },\n        \"dragon_front\": {\n            \"name\": \"\\\\{review}Дракон спереди\",\n        },\n        \"drawbridge\": {\n            \"name\": \"Подъемный мост\",\n        },\n        \"dust\": {\n            \"name\": \"Пыль\",\n        },\n        \"dying_monk\": {\n            \"name\": \"\\\\{review}Умирающий монах\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"\\\\{review}Умирающий Мутант\",\n        },\n        \"eagle\": {\n            \"name\": \"\\\\{review}Орел\",\n        },\n        \"earthquake\": {\n            \"name\": \"Землетрясение\",\n        },\n        \"eel\": {\n            \"name\": \"\\\\{review}Угорь\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"\\\\{review}Электрический очиститель\",\n        },\n        \"electric_fence\": {\n            \"name\": \"\\\\{review}Электрический забор\",\n        },\n        \"electrical_light\": {\n            \"name\": \"\\\\{review}Электрический свет\",\n        },\n        \"ember\": {\n            \"name\": \"Уголь\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"Источник углей\",\n        },\n        \"explosion_1\": {\n            \"name\": \"Взрыв 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Взрыв 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": [\n                \"Падающий блок\",\n                \"Разрушающийся пол\",\n                \"Разрушающиеся плиты\",\n            ]\n        },\n        \"falling_block_2\": {\n            \"name\": [\n                \"\\\\{review}Падающий блок 2\",\n                \"\\\\{review}Обрушивающийся пол 2\",\n                \"\\\\{review}Обрушивающиеся плитки 2\",\n            ]\n        },\n        \"falling_block_3\": {\n            \"name\": [\n                \"\\\\{review}Падающий блок 3\",\n                \"\\\\{review}Обрушивающийся пол 3\",\n                \"\\\\{review}Обрушивающиеся плитки 3\",\n            ]\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"Падающий потолок 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Падающий потолок 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"\\\\{review}Огненная Голова\",\n        },\n        \"fish_mutant\": {\n            \"name\": \"Рыба-мутант\",\n        },\n        \"flame\": {\n            \"name\": [\n                \"Пламя\",\n                \"Огонь\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"Источник пламени\",\n                \"Источник огня\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": \"\\\\{review}Генератор пламени (Большой)\",\n        },\n        \"flame_emitter_jet\": {\n            \"name\": \"\\\\{review}Генератор пламени (Струйный)\",\n        },\n        \"flame_emitter_side\": {\n            \"name\": \"\\\\{review}Генератор пламени (Боковой)\",\n        },\n        \"flame_emitter_small\": {\n            \"name\": \"\\\\{review}Генератор пламени (Малый)\",\n        },\n        \"flare\": {\n            \"name\": \"\\\\{review}Вспышка\",\n        },\n        \"flare_fire\": {\n            \"name\": \"\\\\{review}Огонь фальшфейера\",\n        },\n        \"flares_box\": {\n            \"name\": \"\\\\{review}Коробка вспышки\",\n        },\n        \"flickering_light\": {\n            \"name\": \"\\\\{review}Мерцающий Свет\",\n        },\n        \"fuse_box\": {\n            \"name\": \"\\\\{review}Коробка предохранителей\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"\\\\{review}Серый диск\",\n        },\n        \"gamma\": {\n            \"name\": \"Гамма\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"\\\\{review}Излучатель Газа (Зелёный)\",\n        },\n        \"general\": {\n            \"name\": \"\\\\{review}Миниподводная лодка\",\n        },\n        \"globe\": {\n            \"name\": \"\\\\{review}Глобус\",\n        },\n        \"glow\": {\n            \"name\": \"Свечение\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"\\\\{review}Сияние карты\",\n        },\n        \"gondola\": {\n            \"name\": \"\\\\{review}Гондола\",\n        },\n        \"gong\": {\n            \"name\": \"\\\\{review}Гонг\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"\\\\{review}Палка для гонга\",\n        },\n        \"graphics\": {\n            \"name\": \"Изображение\",\n        },\n        \"green_light\": {\n            \"name\": \"\\\\{review}Зеленый свет\",\n        },\n        \"grenade\": {\n            \"name\": \"Граната\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"\\\\{review}Гранатомёт\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"\\\\{review}Гранаты\",\n        },\n        \"gun_flash\": {\n            \"name\": \"Вспышка выстрела\",\n        },\n        \"gun_shell\": {\n            \"name\": \"\\\\{review}Патрон для пистолета\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"\\\\{review}Гарпунный болт\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"\\\\{review}Гарпунное ружьё\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"\\\\{review}Гарпуны\",\n        },\n        \"hook\": {\n            \"name\": \"\\\\{review}Крюк\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"\\\\{review}Дополнительный огонь\",\n        },\n        \"huskie\": {\n            \"name\": \"\\\\{review}Собака\",\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"\\\\{review}Гибридный Мутант\",\n        },\n        \"icicle\": {\n            \"name\": \"\\\\{review}Сосульки\",\n        },\n        \"inv_background\": {\n            \"name\": \"\\\\{review}Фон меню\",\n        },\n        \"jelly\": {\n            \"name\": \"\\\\{review}Медуза\",\n        },\n        \"kayak\": {\n            \"name\": \"\\\\{review}Каяк\",\n        },\n        \"key_1\": {\n            \"name\": \"Ключ 1\",\n        },\n        \"key_2\": {\n            \"name\": \"Ключ 2\",\n        },\n        \"key_3\": {\n            \"name\": \"Ключ 3\",\n        },\n        \"key_4\": {\n            \"name\": \"Ключ 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"Замочная скважина 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"Замочная скважина 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"Замочная скважина 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"Замочная скважина 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"\\\\{review}Полное уничтожение (активировано)\",\n        },\n        \"killer_statue\": {\n            \"name\": \"\\\\{review}Статуя с мечом\",\n        },\n        \"lara\": {\n            \"name\": \"Лара\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"\\\\{review}Звонок тревоги\",\n        },\n        \"lara_autos\": {\n            \"name\": \"\\\\{review}Анимация автоматических пистолетов\",\n        },\n        \"lara_boat\": {\n            \"name\": \"\\\\{review}Анимация лодки\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"\\\\{review}Анимация Дезерт Игл\",\n        },\n        \"lara_extra\": {\n            \"name\": \"Дополнительная анимация Лары\",\n        },\n        \"lara_flare\": {\n            \"name\": \"\\\\{review}Анимация фальшфейера\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"\\\\{review}Анимация гранатомёта\",\n        },\n        \"lara_hair\": {\n            \"name\": \"Коса Лары\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"\\\\{review}Анимация гарпуна\",\n        },\n        \"lara_m16\": {\n            \"name\": \"\\\\{review}Анимация M16\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"Анимация магнумов\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"\\\\{review}Анимация MP5\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"Анимация пистолетов\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"\\\\{review}Анимация ракетной установки\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"Анимация дробовика\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"\\\\{review}Анимация снегохода\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"Анимация узи\",\n        },\n        \"larson\": {\n            \"name\": \"Ларсон\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Лава\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Свинцовый слиток\",\n        },\n        \"lift\": {\n            \"name\": \"\\\\{review}Лифт\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Излучатель молний\",\n        },\n        \"lion\": {\n            \"name\": \"Лев\",\n        },\n        \"lioness\": {\n            \"name\": [\n                \"Львица\",\n                \"Лев\",\n            ]\n        },\n        \"lizard\": {\n            \"name\": \"\\\\{review}Ящерица\",\n        },\n        \"m16\": {\n            \"name\": \"\\\\{review}M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"\\\\{review}Магазины для M16\",\n        },\n        \"m16_flash\": {\n            \"name\": \"\\\\{review}Вспышка M16\",\n        },\n        \"magnums\": {\n            \"name\": \"Магнумы\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Обоймы для магнума\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"\\\\{review}Замена модели 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"\\\\{review}Замена модели 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"\\\\{review}Замена модели 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Рука Мидаса\",\n        },\n        \"mine\": {\n            \"name\": \"\\\\{review}Водная мина\",\n        },\n        \"mine_cart\": {\n            \"name\": \"\\\\{review}Шахтная Вагонетка\",\n        },\n        \"mini_copter\": {\n            \"name\": \"\\\\{review}Вертолет 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"\\\\{review}Снаряд (Атлантическая бомба)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"\\\\{review}Снаряд (Атлантический осколок)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"\\\\{review}Снаряд (Пламя)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"\\\\{review}Снаряд (Гарпун)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"\\\\{review}Снаряд (Нож)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"\\\\{review}Снаряд (Яд)\",\n        },\n        \"monk_1\": {\n            \"name\": \"\\\\{review}Монах 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"\\\\{review}Монах 2\",\n        },\n        \"monkey\": {\n            \"name\": \"\\\\{review}Обезьяна\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"\\\\{review}Установленный пулемёт\",\n        },\n        \"mouse\": {\n            \"name\": \"\\\\{review}Крыса\",\n        },\n        \"movable_block_1\": {\n            \"name\": [\n                \"Нажимной блок 1\",\n                \"Передвижной блок 1\",\n            ]\n        },\n        \"movable_block_2\": {\n            \"name\": [\n                \"Нажимной блок 2\",\n                \"Передвижной блок 2\",\n            ]\n        },\n        \"movable_block_3\": {\n            \"name\": [\n                \"Нажимной блок 3\",\n                \"Передвижной блок 3\",\n            ]\n        },\n        \"movable_block_4\": {\n            \"name\": [\n                \"Нажимной блок 4\",\n                \"Передвижной блок 4\",\n            ]\n        },\n        \"moving_bar\": {\n            \"name\": \"Движущаяся планка\",\n        },\n        \"mp5\": {\n            \"name\": \"\\\\{review}MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"\\\\{review}Магазины для MP5\",\n        },\n        \"mp_1\": {\n            \"name\": \"\\\\{review}МП 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"\\\\{review}МП 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Мумия\",\n        },\n        \"natla\": {\n            \"name\": \"Натла\",\n        },\n        \"natla_gun\": {\n            \"name\": \"\\\\{review}Пушка Натлы\",\n        },\n        \"on_off_light\": {\n            \"name\": \"\\\\{review}Включение/выключение света\",\n        },\n        \"orca\": {\n            \"name\": \"\\\\{review}Косатка\",\n        },\n        \"passport\": {\n            \"name\": \"Игра\",\n        },\n        \"patrol_dog\": {\n            \"name\": \"\\\\{review}Собака\",\n        },\n        \"pda\": {\n            \"name\": \"Игровой процесс\",\n        },\n        \"pendulum_1\": {\n            \"name\": \"Маятник\",\n        },\n        \"pendulum_2\": {\n            \"name\": [\n                \"\\\\{review}Маятник\",\n                \"\\\\{review}Песчаный ящик\",\n                \"\\\\{review}Качающийся топор\",\n                \"\\\\{review}Качающийся ящик\",\n            ]\n        },\n        \"photo\": {\n            \"name\": \"Дом Лары\",\n        },\n        \"pickup_1\": {\n            \"name\": \"Предмет 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"Предмет 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Первая помощь\",\n        },\n        \"pierre\": {\n            \"name\": \"Пьер\",\n        },\n        \"pirahnas\": {\n            \"name\": \"\\\\{review}Пираньи\",\n        },\n        \"pistols\": {\n            \"name\": \"Пистолеты\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"\\\\{review}Магазины для пистолета\",\n        },\n        \"player_1\": {\n            \"name\": \"Актёр заставки 1\",\n        },\n        \"player_10\": {\n            \"name\": \"\\\\{review}Актер катсцены 10\",\n        },\n        \"player_2\": {\n            \"name\": \"Актёр заставки 2\",\n        },\n        \"player_3\": {\n            \"name\": \"Актёр заставки 3\",\n        },\n        \"player_4\": {\n            \"name\": \"Актёр заставки 4\",\n        },\n        \"player_5\": {\n            \"name\": \"\\\\{review}Актер катсцены 5\",\n        },\n        \"player_6\": {\n            \"name\": \"\\\\{review}Актер катсцены 6\",\n        },\n        \"player_7\": {\n            \"name\": \"\\\\{review}Актер катсцены 7\",\n        },\n        \"player_8\": {\n            \"name\": \"\\\\{review}Актер катсцены 8\",\n        },\n        \"player_9\": {\n            \"name\": \"\\\\{review}Актер катсцены 9\",\n        },\n        \"pods\": {\n            \"name\": \"Гнездо\",\n        },\n        \"poison_dart\": {\n            \"name\": \"\\\\{review}Отравленный дротик\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"\\\\{review}Метатель отравленных дротиков\",\n        },\n        \"portacabin\": {\n            \"name\": \"Кабина\",\n        },\n        \"power_saw\": {\n            \"name\": \"\\\\{review}Электропила\",\n        },\n        \"prisoner\": {\n            \"name\": \"\\\\{review}Заключённый\",\n        },\n        \"propeller_1\": {\n            \"name\": \"\\\\{review}Винт самолёта\",\n        },\n        \"propeller_2\": {\n            \"name\": \"\\\\{review}Подводный винт\",\n        },\n        \"propeller_3\": {\n            \"name\": \"\\\\{review}Воздушный вентилятор\",\n        },\n        \"pulse_light\": {\n            \"name\": \"\\\\{review}Импульсный свет\",\n        },\n        \"puma\": {\n            \"name\": \"Пума\",\n        },\n        \"punk_1\": {\n            \"name\": \"\\\\{review}Панк 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"\\\\{review}Панк 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"Элемент головоломки 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"Элемент головоломки 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"Элемент головоломки 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"Элемент головоломки 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"Головоломка 1 (готово)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"Головоломка 2 (готово)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"Головоломка 3 (готово)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"Головоломка 4 (готово)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"Головоломка 1 (пусто)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"Головоломка 2 (пусто)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"Головоломка 3 (пусто)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"Головоломка 4 (пусто)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"\\\\{review}Квадроцикл\",\n        },\n        \"quest_1\": {\n            \"name\": \"\\\\{review}Предмет задания 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"\\\\{review}Предмет задания 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"\\\\{review}Предмет задания 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"\\\\{review}Предмет задания 4\",\n        },\n        \"raptor\": {\n            \"name\": \"Раптор\",\n        },\n        \"raptor_emitter\": {\n            \"name\": \"\\\\{review}Излучатель Раптора\",\n        },\n        \"rat\": {\n            \"name\": [\n                \"Крыса\",\n                \"Крыса на земле\",\n            ]\n        },\n        \"red_light\": {\n            \"name\": \"\\\\{review}Красный свет\",\n        },\n        \"rib\": {\n            \"name\": \"\\\\{review}RIB\",\n        },\n        \"ricochet\": {\n            \"name\": \"Рикошет\",\n        },\n        \"rocket\": {\n            \"name\": \"\\\\{review}Ракета\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"\\\\{review}Ракетная установка\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"\\\\{review}Ракеты\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": [\n                \"Валун\",\n                \"Катящийся камень\",\n            ]\n        },\n        \"rolling_ball_2\": {\n            \"name\": [\n                \"\\\\{review}Камень 2\",\n                \"\\\\{review}Катающийся шар 2\",\n            ]\n        },\n        \"rolling_ball_3\": {\n            \"name\": [\n                \"\\\\{review}Камень 3\",\n                \"\\\\{review}Катающийся шар 3\",\n            ]\n        },\n        \"rolling_ball_4\": {\n            \"name\": [\n                \"\\\\{review}Камень 4\",\n                \"\\\\{review}Катающийся шар 4\",\n            ]\n        },\n        \"rotating_laser\": {\n            \"name\": \"\\\\{review}Вращающийся лазер\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"\\\\{review}RX Рабочий 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"\\\\{review}RX Рабочий 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": \"\\\\{review}RX Рабочий 3\",\n        },\n        \"save_crystal\": {\n            \"name\": \"Кристалл сохранения\",\n        },\n        \"scion\": {\n            \"name\": \"Наследие\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Постамент Наследия\",\n        },\n        \"secret_1\": {\n            \"name\": \"\\\\{review}Секрет 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"\\\\{review}Секрет 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"\\\\{review}Секрет 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"\\\\{review}Охранник\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"\\\\{review}Охранный лазер (Тревога)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"\\\\{review}Охранный лазер (Смертельный)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"\\\\{review}Охранный лазер (Убийственный)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"\\\\{review}Робот-страж\",\n        },\n        \"shadow\": {\n            \"name\": \"\\\\{review}Тень\",\n        },\n        \"shark\": {\n            \"name\": \"\\\\{review}Акула\",\n        },\n        \"shiva\": {\n            \"name\": \"\\\\{review}Шива\",\n        },\n        \"shotgun\": {\n            \"name\": \"Дробовик\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"Патроны для дробовика\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"\\\\{review}Патрон для дробовика\",\n        },\n        \"skate_kid\": {\n            \"name\": \"Скейтер\",\n        },\n        \"skateboard\": {\n            \"name\": \"Скейтборд\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"\\\\{review}Чёрный снегоход\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"\\\\{review}Водитель чёрного снегохода\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"\\\\{review}Красный снегоход\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"\\\\{review}Гусеница снегохода\",\n        },\n        \"skybox\": {\n            \"name\": \"Скайбокс\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Скользящая колонна\",\n        },\n        \"smashable_1\": {\n            \"name\": \"\\\\{review}Разбиваемое окно 1\",\n        },\n        \"smashable_2\": {\n            \"name\": \"\\\\{review}Разбиваемое окно 2\",\n        },\n        \"smashable_3\": {\n            \"name\": \"\\\\{review}Разбиваемое окно 3\",\n        },\n        \"smashable_4\": {\n            \"name\": \"\\\\{review}Разбиваемое окно 4\",\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"\\\\{review}Дымовой излучатель (черный)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"\\\\{review}Дымовой излучатель (белый)\",\n        },\n        \"snake\": {\n            \"name\": \"\\\\{review}Змей\",\n        },\n        \"snow_sprite\": {\n            \"name\": \"\\\\{review}След снегохода\",\n        },\n        \"sophia\": {\n            \"name\": \"\\\\{review}София\",\n        },\n        \"sound\": {\n            \"name\": \"Звук\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"\\\\{review}Взрыв дракона 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"\\\\{review}Взрыв дракона 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"\\\\{review}Взрыв дракона 3\",\n        },\n        \"spider\": {\n            \"name\": \"\\\\{review}Паук\",\n        },\n        \"spike_wall\": {\n            \"name\": \"\\\\{review}Стена с шипами\",\n        },\n        \"spikes\": {\n            \"name\": \"Шипы\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"\\\\{review}Вращающийся клинок\",\n        },\n        \"splash_1\": {\n            \"name\": \"Всплеск 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Всплеск 2\",\n        },\n        \"springboard\": {\n            \"name\": \"\\\\{review}Прыжковая доска\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"\\\\{review}Паровой излучатель\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"\\\\{review}Наёмник из южной части Тихого океана\",\n        },\n        \"stopwatch\": {\n            \"name\": \"\\\\{review}Статистика\",\n        },\n        \"strobe_light\": {\n            \"name\": \"\\\\{review}Стробоскопический свет\",\n        },\n        \"swat_1\": {\n            \"name\": \"\\\\{review}SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"\\\\{review}SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"\\\\{review}SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"\\\\{review}Pаскачивающийся топор\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"\\\\{review}Переключатель воздушного шлюза\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"\\\\{review}Кнопка\",\n                \"\\\\{review}Нажимная кнопка\",\n                \"\\\\{review}Выключатель\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"Рычаг\",\n                \"Переключатель\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"\\\\{review}Маленький выключатель\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"Подводный рычаг\",\n                \"Подводный переключатель\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": \"\\\\{review}Колёсный переключатель\",\n        },\n        \"teeth_trap\": {\n            \"name\": [\n                \"Зубная ловушка\",\n                \"Клацающая дверь\",\n            ]\n        },\n        \"text_box\": {\n            \"name\": \"\\\\{review}Рамка интерфейса\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Рукоять молота Тора\",\n        },\n        \"thors_head\": {\n            \"name\": \"Молот Тора\",\n        },\n        \"tiger\": {\n            \"name\": \"\\\\{review}Тигр\",\n        },\n        \"tony\": {\n            \"name\": \"\\\\{review}Тони\",\n        },\n        \"torso\": {\n            \"name\": [\n                \"Торсо\",\n                \"Адам\",\n                \"Безногий мутант\",\n            ]\n        },\n        \"train\": {\n            \"name\": \"\\\\{review}Поезд\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Люк 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Люк 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Люк 3\",\n        },\n        \"trex\": {\n            \"name\": \"Тираннозавр\",\n        },\n        \"trex_alpha\": {\n            \"name\": \"\\\\{review}Ти-Рекс Альфа\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"\\\\{review}Топорщик племени\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"\\\\{review}Вождь племени\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"\\\\{review}Пользователь духовой трубки племени\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"\\\\{review}Тропическая рыба\",\n        },\n        \"twinkle\": {\n            \"name\": \"Искры\",\n        },\n        \"upv\": {\n            \"name\": \"\\\\{review}Минисуб\",\n        },\n        \"uzis\": {\n            \"name\": \"Узи\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"Обоймы для узи\",\n        },\n        \"vole\": {\n            \"name\": [\n                \"Плавающая крыса\",\n                \"Крыса в воде\",\n            ]\n        },\n        \"vulture\": {\n            \"name\": \"\\\\{review}Стервятник\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"\\\\{review}Осинный Мутант\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"\\\\{review}Излучатель Осиного Мутанта\",\n        },\n        \"water_sprite\": {\n            \"name\": \"\\\\{review}След лодки\",\n        },\n        \"waterfall\": {\n            \"name\": \"Водопадный туман\",\n        },\n        \"white_light\": {\n            \"name\": \"\\\\{review}Белый свет\",\n        },\n        \"willard\": {\n            \"name\": \"\\\\{review}Уиллард\",\n        },\n        \"winston\": {\n            \"name\": \"\\\\{review}Уинстон\",\n        },\n        \"winston_army\": {\n            \"name\": \"\\\\{review}Уинстон (армия)\",\n        },\n        \"wolf\": {\n            \"name\": \"Волк\",\n        },\n        \"worker_1\": {\n            \"name\": \"\\\\{review}Бандит с пистолетом 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"\\\\{review}Бандит с пистолетом 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"\\\\{review}Бандит с палкой 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"\\\\{review}Бандит с палкой 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"\\\\{review}Бандит с огнемётом\",\n        },\n        \"xian_knight\": {\n            \"name\": \"\\\\{review}Ксианский рыцарь\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"\\\\{review}Статуя ксианского рыцаря\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"\\\\{review}Ксианский копейщик\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"\\\\{review}Статуя ксианского копейщика\",\n        },\n        \"yeti\": {\n            \"name\": \"\\\\{review}Йети\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"\\\\{review}Рукоятка зиплайна\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/base_strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"language_name\": \"English\",\n    \"general\": {\n        \"actions\": {\n            \"examine_item\": \"Examine\",\n            \"hide_dialog\": \"Hide dialog\",\n            \"reset_defaults\": \"Reset All\",\n            \"rotate\": \"Rotate\",\n            \"unbind\": \"Unbind\",\n            \"use_item\": \"Use\",\n        },\n        \"config_presets\": {\n            \"applied\": \"Preset applied.\",\n            \"confirm_description\": \"The following settings will be changed:\",\n            \"confirm_restart_note\": \"Note: some settings may require a game restart to take effect.\",\n            \"empty\": \"No presets found.\",\n            \"no_changes\": \"No changes to apply.\",\n            \"title_fmt\": \"Apply preset %s?\",\n        },\n        \"globe_select\": {\n            \"area_1\": \"Area 1\",\n            \"area_2\": \"Area 2\",\n            \"area_3\": \"Area 3\",\n            \"area_4\": \"Area 4\",\n            \"area_5\": \"Area 5\",\n            \"area_6\": \"Area 6\",\n        },\n        \"inventory_ring\": {\n            \"heading_adventure\": \"Adventure\",\n            \"heading_fmt\": \"%s\",\n            \"heading_game_over\": \"GAME OVER\",\n            \"heading_inventory\": \"INVENTORY\",\n            \"heading_items\": \"ITEMS\",\n            \"heading_option\": \"OPTION\",\n            \"item_count_fmt\": \"\\\\{small}%s\",\n            \"object_name_fmt\": \"%s\",\n        },\n        \"misc\": {\n            \"demo_mode\": \"Demo Mode\",\n            \"direction_keys_controller\": \"D-Pad\",\n            \"direction_keys_keyboard\": \"Arrows\",\n            \"empty_slot_fmt\": \"- EMPTY SLOT -\",\n            \"exit\": \"Exit\",\n            \"hold_fmt\": \"Hold %s\",\n            \"off\": \"Off\",\n            \"on\": \"On\",\n            \"pagination_nav\": \"%d / %d\",\n        },\n        \"osd\": {\n            \"ambiguous_input_2\": \"Ambiguous input: %s and %s\",\n            \"ambiguous_input_3\": \"Ambiguous input: %s, %s, ...\",\n            \"bilinear_filter_off\": \"Bilinear filter: off\",\n            \"bilinear_filter_on\": \"Bilinear filter: on\",\n            \"command_bad_invocation\": \"Invalid invocation: %s\",\n            \"command_bool\": \"on, off\",\n            \"command_decimal\": \"[decimal]\",\n            \"command_integer\": \"[integer]\",\n            \"command_percent\": \"[integer]\",\n            \"command_unavailable\": \"This command is not currently available\",\n            \"command_valid_values\": \"Valid values: %s\",\n            \"complete_level\": \"Level complete!\",\n            \"config_option_get\": \"%s is currently set to %s\",\n            \"config_option_set\": \"%s changed to %s\",\n            \"config_option_unknown_option\": \"Unknown option: %s\",\n            \"current_health_get\": \"Current Lara's health: %d\",\n            \"current_health_set\": \"Lara's health set to %d\",\n            \"door_close\": \"Close Sesame!\",\n            \"door_open\": \"Open Sesame!\",\n            \"door_open_fail\": \"No doors in Lara's proximity\",\n            \"flipmap_fail_already_off\": \"Flipmap is already OFF\",\n            \"flipmap_fail_already_on\": \"Flipmap is already ON\",\n            \"flipmap_off\": \"Flipmap set to OFF\",\n            \"flipmap_on\": \"Flipmap set to ON\",\n            \"fly_mode_off\": \"Fly mode disabled\",\n            \"fly_mode_on\": \"Fly mode enabled\",\n            \"fps_counter_off\": \"FPS counter: off\",\n            \"fps_counter_on\": \"FPS counter: on\",\n            \"give_item\": \"Added %s to Lara's inventory\",\n            \"give_item_all_guns\": \"Lock'n'load - Lara's armed to the teeth!\",\n            \"give_item_all_keys\": \"Surprise! Every key item Lara needs is now in her backpack.\",\n            \"give_item_cheat\": \"Lara's backpack just got way heavier!\",\n            \"heal_already_full_hp\": \"Lara's already at full health\",\n            \"heal_success\": \"Healed Lara back to full health\",\n            \"invalid_cutscene\": \"Invalid cutscene\",\n            \"invalid_demo\": \"Invalid demo\",\n            \"invalid_item\": \"Unknown item: %s\",\n            \"invalid_level\": \"Invalid level\",\n            \"invalid_object\": \"Invalid object\",\n            \"invalid_room\": \"Invalid room: %d. Valid rooms are 0-%d\",\n            \"invalid_sample\": \"Invalid sound: %d\",\n            \"kill\": \"Bye-bye!\",\n            \"kill_all\": \"Poof! %d enemies gone!\",\n            \"kill_all_fail\": \"Uh-oh, there are no enemies left to kill...\",\n            \"kill_fail\": \"No enemy nearby...\",\n            \"lighting_contrast_fmt\": \"Lighting Contrast: %s\",\n            \"load_game\": \"Loaded game from save slot %d\",\n            \"load_game_fail_invalid_slot\": \"Invalid save slot %d\",\n            \"load_game_fail_unavailable_slot\": \"Save slot %d is not available\",\n            \"object_not_found\": \"Object not found\",\n            \"play_cutscene\": \"Loading cutscene %d\",\n            \"play_demo\": \"Loading demo %d\",\n            \"play_level\": \"Loading %s\",\n            \"pos_lara_missing\": \"Lara not present\",\n            \"pos_lara_pos_fmt\": \"Room: %d\\nPosition: %.3f, %.3f, %.3f\\nRotation: %.3f, %.3f, %.3f\",\n            \"pos_level_fmt\": \"Level %d\",\n            \"pos_level_fmt_cutscene\": \"Cutscene %d\",\n            \"pos_level_fmt_demo\": \"Demo %d\",\n            \"quick_load\": \"Quick-loaded slot %d\",\n            \"quick_load_fail_no_bound_slot\": \"No save slot is currently bound\",\n            \"quick_load_fail_unavailable_bound_slot\": \"The bound save slot is not available\",\n            \"quick_save\": \"Quick-saved\",\n            \"quick_save_fail_no_slots\": \"No quick save slots are configured\",\n            \"save_game\": \"Saved game to save slot %d\",\n            \"save_game_fail_invalid_slot\": \"Invalid save slot %d\",\n            \"sound_available_samples\": \"Available sounds: %s\",\n            \"sound_playing_sample\": \"Playing sound %d\",\n            \"speed_get\": \"Current speed: %d\",\n            \"speed_set\": \"Speed set to %d\",\n            \"strings_failed\": \"Failed to reload the language files\",\n            \"strings_reloaded\": \"Language files reloaded\",\n            \"textures_off\": \"Textures: off\",\n            \"textures_on\": \"Textures: on\",\n            \"trapezoid_filter_off\": \"Trapezoid filter: off\",\n            \"trapezoid_filter_on\": \"Trapezoid filter: on\",\n            \"ui_off\": \"UI disabled\",\n            \"ui_on\": \"UI enabled\",\n            \"unknown_command\": \"Unknown command: %s\",\n            \"upscaling_factor\": \"Upscaling Factor: x%d\",\n            \"wireframe_mode_off\": \"Wireframe mode: off\",\n            \"wireframe_mode_on\": \"Wireframe mode: on\",\n        },\n        \"overlay\": {\n            \"debug_animation\": \"Animation: \",\n            \"debug_animation_state\": \"State: \",\n            \"debug_camera_pos\": \"Camera origin: \",\n            \"debug_camera_target\": \"Camera target: \",\n            \"debug_immune\": \"Invulnerability on\",\n            \"debug_position\": \"Position: \",\n            \"debug_rotation\": \"Rotation: \",\n            \"debug_speed\": \"Speed: \",\n            \"item_count_fmt_pc\": \"\\\\{small}%s\",\n            \"item_count_fmt_ps1\": \"\\\\{small}%s\",\n        },\n        \"passport\": {\n            \"delete_save\": \"Delete\",\n            \"delete_save_confirm\": \"Delete this save?\",\n            \"delete_save_failed\": \"Failed to delete the chosen save.\",\n            \"delete_save_no\": \"No\",\n            \"delete_save_yes\": \"Yes\",\n            \"exit_game\": \"Exit Game\",\n            \"exit_to_title\": \"Exit to Title\",\n            \"load_game\": \"Load Game\",\n            \"mode_new_game\": \"New Game\",\n            \"mode_new_game_jp\": \"Japanese NG\",\n            \"mode_new_game_jp_plus\": \"Japanese NG+\",\n            \"mode_new_game_plus\": \"New Game+\",\n            \"new_game\": \"New Game\",\n            \"play_previous_levels\": \"Play previous levels\",\n            \"restart_level\": \"Restart Level\",\n            \"save_game\": \"Save Game\",\n            \"save_slot_unsupported\": \"This save does not support this feature.\",\n            \"select_level\": \"Select Level\",\n            \"select_mod\": \"Select Game\",\n            \"select_mode\": \"Select Mode\",\n            \"select_save\": \"Select Save\",\n            \"story_so_far\": \"Story so far...\",\n            \"switch_mod\": \"Switch Game\",\n        },\n        \"pause\": {\n            \"are_you_sure\": \"Are you sure?\",\n            \"continue\": \"Continue\",\n            \"exit_to_title\": \"Exit to title?\",\n            \"no\": \"No\",\n            \"paused\": \"Paused\",\n            \"quit\": \"Quit\",\n            \"yes\": \"Yes\",\n        },\n        \"photo_mode\": {\n            \"advance_frame\": \"Advance frame\",\n            \"camera_move_prompt\": \"Move camera\",\n            \"camera_reset_prompt\": \"Reset camera\",\n            \"camera_roll_prompt\": \"Roll camera\",\n            \"camera_rotate_90_prompt\": \"Rotate camera 90°\",\n            \"camera_rotate_prompt\": \"Rotate camera\",\n            \"change_lara_pose\": \"Change Lara's pose\",\n            \"fov_prompt\": \"Adjust FOV\",\n            \"lara_move_prompt\": \"Move Lara\",\n            \"lara_reset_prompt\": \"Reset Lara\",\n            \"lara_roll_prompt\": \"Roll Lara\",\n            \"lara_rotate_90_prompt\": \"Rotate Lara 90°\",\n            \"lara_rotate_prompt\": \"Rotate Lara\",\n            \"snap_prompt\": \"Take picture\",\n            \"title_camera_pos\": \"Photo Mode\",\n            \"title_lara_pos\": \"Moving Lara\",\n            \"toggle_help\": \"Toggle help\",\n        },\n        \"settings\": {\n            \"common\": {\n                \"all_hidden_disclaimer\": \"Settings are disabled for this level set.\",\n                \"chroma\": \"Chroma\",\n                \"edit_value\": \"Edit value\",\n                \"frozen_option_disclaimer\": \"This setting is enforced by the level builder and cannot be changed.\",\n                \"hue\": \"Hue\",\n                \"lightness\": \"Lightness\",\n                \"restore_default\": \"Restore default\",\n                \"toggle_help\": \"Toggle help\",\n            },\n            \"controls\": {\n                \"backend\": {\n                    \"controller\": \"Controller\",\n                    \"keyboard\": \"Keyboard\",\n                },\n                \"customize\": \"Customize Controls\",\n                \"layout\": {\n                    \"custom_1\": \"User Keys 1\",\n                    \"custom_2\": \"User Keys 2\",\n                    \"custom_3\": \"User Keys 3\",\n                    \"default\": \"Default Keys\",\n                },\n                \"tabs\": {\n                    \"basics\": \"Movement\",\n                    \"items\": \"Items\",\n                    \"misc\": \"Misc\",\n                    \"system\": \"System\",\n                }\n            },\n            \"gameplay\": {\n                \"tabs\": {\n                    \"controls\": \"Controls\",\n                    \"fixes\": \"Fixes\",\n                    \"general\": \"General\",\n                    \"mods\": \"Mods\",\n                    \"presets\": \"Presets\",\n                },\n                \"title\": \"Gameplay Options\",\n            },\n            \"graphic_settings\": {\n                \"tabs\": {\n                    \"bars\": \"Bars\",\n                    \"rendering\": \"Rendering\",\n                    \"stats\": \"Stats\",\n                    \"ui\": \"UI\",\n                    \"visuals\": \"Visuals\",\n                },\n                \"title\": \"Graphic Options\",\n            },\n            \"sound\": {\n                \"tabs\": {\n                    \"misc\": \"Misc\",\n                    \"volume\": \"Volume\",\n                },\n                \"title\": \"Sound Options\",\n            }\n        },\n        \"stats\": {\n            \"ammo\": \"Ammo Hits/Used\",\n            \"ammo_hits\": \"Hits\",\n            \"ammo_used\": \"Ammo Used\",\n            \"assault_best_time_fmt\": \"%s\",\n            \"assault_finish\": \"Finish\",\n            \"assault_no_times_set\": \"No Times Set\",\n            \"assault_other_times_fmt\": \"%s\",\n            \"assault_title\": \"BEST TIMES\",\n            \"basic_fmt\": \"%d\",\n            \"bonus_statistics\": \"Bonus Statistics\",\n            \"crystals\": \"Crystals\",\n            \"deaths\": \"Deaths\",\n            \"detail_fmt\": \"%d of %d\",\n            \"distance_travelled\": \"Distance Traveled\",\n            \"final_statistics\": \"Final Statistics\",\n            \"gym_assault_course\": \"Assault Course\",\n            \"gym_racetrack_course\": \"Race Track Course\",\n            \"kills\": \"Kills\",\n            \"level\": \"Level\",\n            \"medipacks_used\": \"Health Packs Used\",\n            \"none\": \"None\",\n            \"pickups\": \"Pickups\",\n            \"secrets\": \"Secrets Found\",\n            \"time_taken\": \"Time Taken\",\n        }\n    },\n    \"console\": {\n        \"cmd\": {\n            \"braid\": {\n                \"help\": \"Toggles Lara's braid.\",\n            },\n            \"cheats\": {\n                \"help\": \"Toggles in-game cheats on or off.\",\n            },\n            \"clear\": {\n                \"help\": \"Clears visible console logs.\",\n            },\n            \"debug\": {\n                \"help\": \"Toggles visual debug information.\",\n            },\n            \"drain\": {\n                \"help\": \"Dries the current room, removing the water.\",\n            },\n            \"end_level\": {\n                \"help\": \"Ends the current level.\",\n            },\n            \"exit\": {\n                \"help\": \"Exits the game.\",\n            },\n            \"flipmap\": {\n                \"help\": \"Toggles the flip map.\",\n            },\n            \"flood\": {\n                \"help\": \"Submerges the current room with water.\",\n            },\n            \"fly\": {\n                \"help\": \"Toggles the fly-mode cheat.\",\n            },\n            \"fps\": {\n                \"help\": \"Changes the FPS value.\",\n            },\n            \"give\": {\n                \"help\": \"Adds a given item to Lara's inventory.\",\n                \"invalid_secret\": \"Invalid secret: %s (valid secrets: %s)\",\n                \"secret_given\": \"Added secret %s\",\n                \"secret_list\": \"Secrets collected: %d of %d (%s)\",\n                \"secret_none\": \"Secrets collected: %d of %d\",\n                \"secret_taken\": \"Removed secret %s\",\n            },\n            \"give_secret\": {\n                \"help\": \"Lists Lara's secrets, or takes/gives a secret by number.\",\n            },\n            \"heal\": {\n                \"help\": \"Heals Lara back to full health.\",\n            },\n            \"help\": {\n                \"help\": \"Shows help for all commands or detailed help for one.\",\n                \"list\": \"Available commands:\",\n            },\n            \"hp\": {\n                \"help\": \"Sets Lara's health to the specified value.\",\n            },\n            \"immune\": {\n                \"help\": \"Toggles invulnerability. (Lara can still be killed in some circumstances.)\",\n                \"off\": \"Lara is now vulnerable\",\n                \"on\": \"Lara is now impervious to damage\",\n            },\n            \"inf_sprint\": {\n                \"help\": \"Toggles infinite sprint.\",\n                \"off\": \"Lara can no longer sprint forever\",\n                \"on\": \"Lara can now sprint forever\",\n            },\n            \"kill\": {\n                \"help\": \"Kills nearby enemies.\",\n            },\n            \"lighting\": {\n                \"help\": \"Toggles lighting system.\",\n            },\n            \"load\": {\n                \"help\": \"Loads game from the given save slot or from a quick save.\",\n            },\n            \"lua\": {\n                \"help\": \"Executes the given Lua code string.\",\n                \"runtime_error\": \"Lua runtime error: %s\",\n                \"syntax_error\": \"Lua syntax error: %s\",\n            },\n            \"mod\": {\n                \"help\": \"Switches to the specified mod and restarts the game.\",\n            },\n            \"music\": {\n                \"help\": \"Plays a music track with the given id.\",\n            },\n            \"play_cutscene\": {\n                \"help\": \"Plays a cutscene with the given number.\",\n            },\n            \"play_demo\": {\n                \"help\": \"Plays a demo with the given number.\",\n            },\n            \"play_gym\": {\n                \"help\": \"Plays the Gym level.\",\n            },\n            \"play_level\": {\n                \"help\": \"Plays a level with the given name or number.\",\n            },\n            \"play_music\": {\n                \"invalid_track\": \"Invalid music track\",\n                \"stopped\": \"Music stopped\",\n                \"track\": \"Playing music track %d\",\n            },\n            \"pos\": {\n                \"help\": \"Shows Lara's position.\",\n            },\n            \"save\": {\n                \"help\": \"Saves game to the given save slot or to the next quick save slot.\",\n            },\n            \"screenshot\": {\n                \"help\": \"Saves a screenshot to disk at optional location.\",\n            },\n            \"set\": {\n                \"help\": \"Displays or updates the given configuration setting.\",\n            },\n            \"sfx\": {\n                \"help\": \"Plays a sound effect with the given id.\",\n            },\n            \"spawn\": {\n                \"fail\": \"Failed to spawn requested object\",\n                \"success\": \"Requested object spawned near Lara\",\n            },\n            \"speed\": {\n                \"help\": \"Changes the game's speed.\",\n            },\n            \"strings\": {\n                \"help\": \"Reloads the current language files from disk.\",\n            },\n            \"teleport\": {\n                \"item\": \"Teleported to item: %d\",\n                \"item_fail\": \"Failed to teleport to item: %d\",\n                \"object\": \"Teleported to object: %s\",\n                \"object_fail\": \"Failed to teleport to object: %s\",\n                \"pos\": \"Teleported to position: %.3f %.3f %.3f\",\n                \"pos_fail\": \"Failed to teleport to position: %.3f %.3f %.3f\",\n                \"room\": \"Teleported to room: %d\",\n                \"room_fail\": \"Failed to teleport to room: %d\",\n            },\n            \"textures\": {\n                \"help\": \"Toggles textures.\",\n            },\n            \"title\": {\n                \"help\": \"Returns to the title screen.\",\n            },\n            \"tp\": {\n                \"help\": \"Teleports Lara to a given position or room number.\",\n            },\n            \"trigger\": {\n                \"help\": \"Triggers or untriggers an item by id, item name, or object name.\",\n                \"invalid_item\": \"Invalid item: %s\",\n                \"no_match\": \"Unknown target: %s\",\n                \"not_found\": \"No matching items found for: %s\",\n                \"triggered\": \"Triggered item(s): %s\",\n                \"untriggered\": \"Untriggered item(s): %s\",\n            },\n            \"vsync\": {\n                \"help\": \"Toggles vertical sync.\",\n            },\n            \"weather\": {\n                \"help\": \"Changes the current weather type.\",\n                \"invalid\": \"Invalid weather: %s (valid: %s)\",\n                \"set\": \"Weather set to %s\",\n            },\n            \"winston\": {\n                \"dead\": \"Your butler is dead. You monster!\",\n                \"spawn_failed\": \"Failed to summon Winston\",\n                \"spawned\": \"Summoned Winston near Lara\",\n                \"teleported\": \"Summoned Winston near Lara\",\n            },\n            \"wireframe\": {\n                \"help\": \"Toggles wireframe rendering.\",\n            }\n        }\n    },\n    \"dynamic\": {\n        \"config_presets\": {\n            \"tr1_pc\": \"TR1 PC\",\n            \"tr1_ps1\": \"TR1 PS1\",\n            \"tr2_pc\": \"TR2 PC\",\n            \"tr2_ps1\": \"TR2 PS1\",\n            \"tr3_pc\": \"TR3 PC\",\n            \"tr3_ps1\": \"TR3 PS1\",\n        },\n        \"enums\": {\n            \"bar_look\": {\n                \"tr1_pc\": \"TR1 PC\",\n                \"tr2_pc\": \"TR2 PC\",\n                \"tr2_ps1\": \"TR2 PS1\",\n                \"tr3_pc\": \"TR3 PC\",\n                \"tr3_ps1\": \"TR3 PS1\",\n            },\n            \"lara_outfit\": {\n                \"default\": \"Default\",\n                \"golden_sophia\": \"Golden Sophia\",\n                \"sophia\": \"Sophia\",\n                \"tr1_bacon_lara\": \"Bacon Lara\",\n                \"tr1_classic\": \"TR1 Classic\",\n                \"tr1_combo\": \"TR1 Combo\",\n                \"tr1_golden_bacon_lara\": \"Golden Bacon Lara\",\n                \"tr1_golden_lara\": \"TR1 Golden Lara\",\n                \"tr1_gym\": \"TR1 Gym\",\n                \"tr1_mauled\": \"TR1 Mauled\",\n                \"tr1_ngage\": \"TR1 N-Gage\",\n                \"tr23_golden_lara\": \"TR2/3 Golden Lara\",\n                \"tr2_bomber_jacket\": \"Bomber Jacket\",\n                \"tr2_classic\": \"TR2 Classic\",\n                \"tr2_diving_suit\": \"Diving Suit 1\",\n                \"tr2_diving_suit_alpha\": \"Diving Suit 2\",\n                \"tr2_gym\": \"TR2 Gym\",\n                \"tr2_robe\": \"Robe\",\n                \"tr2_vegas\": \"Vegas\",\n                \"tr3_antarctica\": \"Antarctica\",\n                \"tr3_catsuit\": \"Catsuit\",\n                \"tr3_classic\": \"TR3 Classic\",\n                \"tr3_gym\": \"TR3 Gym\",\n                \"tr3_nevada\": \"Nevada\",\n                \"tr3_south_pacific\": \"South Pacific\",\n            }\n        },\n        \"mods\": {\n            \"tr1\": {\n                \"title\": \"Tomb Raider I\",\n            },\n            \"tr1-demo-pc\": {\n                \"title\": \"Tomb Raider I Demo\",\n            },\n            \"tr1-ub\": {\n                \"title\": \"Unfinished Business\",\n            },\n            \"tr2\": {\n                \"title\": \"Tomb Raider II\",\n            },\n            \"tr2-gm\": {\n                \"title\": \"The Golden Mask\",\n            },\n            \"tr3\": {\n                \"title\": \"Tomb Raider III\",\n            },\n            \"tr3-la\": {\n                \"title\": \"The Lost Artifact\",\n            }\n        }\n    },\n    \"enums\": {\n        \"ALLY_HOSTILITY_POLICY\": {\n            \"ALLY_HOSTILITY_POLICY_INDIVIDUAL\": \"Individual\",\n            \"ALLY_HOSTILITY_POLICY_SHARED\": \"Shared\",\n        },\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_16_10\": \"16:10\",\n            \"ASPECT_MODE_16_9\": \"16:9\",\n            \"ASPECT_MODE_4_3\": \"4:3\",\n            \"ASPECT_MODE_ANY\": \"Any\",\n        },\n        \"BACKGROUND_TYPE\": {\n            \"BK_BLACK\": \"Black\",\n            \"BK_IMAGE\": \"Image\",\n            \"BK_MONOCHROME\": \"Monochrome\",\n            \"BK_MONOCHROME_COOL\": \"Monochrome (cool)\",\n            \"BK_MONOCHROME_WARM\": \"Monochrome (warm)\",\n            \"BK_NONE\": \"Transparent\",\n            \"BK_PATTERN_STATIC\": \"Static\",\n            \"BK_PATTERN_WAVE\": \"Wave\",\n            \"BK_TRANSPARENT_DARK\": \"Very dark\",\n            \"BK_TRANSPARENT_MEDIUM\": \"Dark\",\n        },\n        \"BAR_SHOW_MODE\": {\n            \"BAR_SHOW_MODE_ALWAYS\": \"Always\",\n            \"BAR_SHOW_MODE_BOSS_ONLY\": \"Boss only\",\n            \"BAR_SHOW_MODE_NEVER\": \"Never\",\n        },\n        \"BILLBOARD_LOCK_MODE\": {\n            \"BILLBOARD_LOCK_NONE\": \"None\",\n            \"BILLBOARD_LOCK_PERSPECTIVE\": \"Perspective\",\n            \"BILLBOARD_LOCK_ROLL\": \"Roll\",\n            \"BILLBOARD_LOCK_ROLL_PITCH\": \"Roll & pitch\",\n        },\n        \"BLOOD_EFFECTS\": {\n            \"BLOOD_EFFECTS_DISABLED\": \"Disabled\",\n            \"BLOOD_EFFECTS_PINK\": \"Pink\",\n            \"BLOOD_EFFECTS_RED\": \"Red\",\n        },\n        \"CAMERA_MODE\": {\n            \"CAMERA_MODE_TR1\": \"TR1\",\n            \"CAMERA_MODE_TR2\": \"TR2\",\n            \"CAMERA_MODE_TR3\": \"TR3\",\n        },\n        \"CREATURE_DROWN_POLICY\": {\n            \"CREATURE_DROWN_POLICY_DEFAULT\": \"Default\",\n            \"CREATURE_DROWN_POLICY_NEVER\": \"Never\",\n            \"CREATURE_DROWN_POLICY_SUBMERGED\": \"Submerged\",\n        },\n        \"INPUT_BACKEND\": {\n            \"INPUT_BACKEND_CONTROLLER\": \"Controller\",\n            \"INPUT_BACKEND_KEYBOARD\": \"Keyboard\",\n        },\n        \"INPUT_ROLE\": {\n            \"INPUT_ROLE_ACTION\": \"Action\",\n            \"INPUT_ROLE_CAMERA_BACK\": \"Camera Back\",\n            \"INPUT_ROLE_CAMERA_DOWN\": \"Camera Down\",\n            \"INPUT_ROLE_CAMERA_FORWARD\": \"Camera Forward\",\n            \"INPUT_ROLE_CAMERA_LEFT\": \"Camera Left\",\n            \"INPUT_ROLE_CAMERA_RESET\": \"Camera Reset\",\n            \"INPUT_ROLE_CAMERA_RIGHT\": \"Camera Right\",\n            \"INPUT_ROLE_CAMERA_UP\": \"Camera Up\",\n            \"INPUT_ROLE_CHANGE_OUTFIT\": \"Change Outfit\",\n            \"INPUT_ROLE_CHANGE_TARGET\": \"Change Target\",\n            \"INPUT_ROLE_CROUCH\": \"Crouch\",\n            \"INPUT_ROLE_CYCLE_LIGHTING_CONTRAST\": \"Cycle Lighting Contrast\",\n            \"INPUT_ROLE_DOWN\": \"Back\",\n            \"INPUT_ROLE_DRAW_WEAPON\": \"Draw Weapon\",\n            \"INPUT_ROLE_ENTER_CONSOLE\": \"Dev Console\",\n            \"INPUT_ROLE_EQUIP_AUTOS\": \"Equip Automatic Pistols\",\n            \"INPUT_ROLE_EQUIP_DESERT_EAGLE\": \"Equip Desert Eagle\",\n            \"INPUT_ROLE_EQUIP_GRENADE_LAUNCHER\": \"Equip Grenade Launcher\",\n            \"INPUT_ROLE_EQUIP_HARPOON\": \"Equip Harpoon Gun\",\n            \"INPUT_ROLE_EQUIP_M16\": \"Equip M16\",\n            \"INPUT_ROLE_EQUIP_MAGNUMS\": \"Equip Magnums\",\n            \"INPUT_ROLE_EQUIP_MP5\": \"Equip MP5\",\n            \"INPUT_ROLE_EQUIP_PISTOLS\": \"Equip Pistols\",\n            \"INPUT_ROLE_EQUIP_ROCKET_LAUNCHER\": \"Equip Rocket Launcher\",\n            \"INPUT_ROLE_EQUIP_SHOTGUN\": \"Equip Shotgun\",\n            \"INPUT_ROLE_EQUIP_UZIS\": \"Equip Uzis\",\n            \"INPUT_ROLE_FLY_CHEAT\": \"Fly Cheat\",\n            \"INPUT_ROLE_FPS\": \"Show FPS\",\n            \"INPUT_ROLE_INVENTORY\": \"Inventory\",\n            \"INPUT_ROLE_ITEM_CHEAT\": \"Item Cheat\",\n            \"INPUT_ROLE_JUMP\": \"Jump\",\n            \"INPUT_ROLE_LEFT\": \"Left\",\n            \"INPUT_ROLE_LEVEL_SKIP_CHEAT\": \"Level Skip\",\n            \"INPUT_ROLE_LOAD\": \"Load\",\n            \"INPUT_ROLE_LOOK\": \"Look\",\n            \"INPUT_ROLE_PAUSE\": \"Pause\",\n            \"INPUT_ROLE_QUICK_LOAD\": \"Quick Load\",\n            \"INPUT_ROLE_QUICK_SAVE\": \"Quick Save\",\n            \"INPUT_ROLE_RIGHT\": \"Right\",\n            \"INPUT_ROLE_ROLL\": \"Roll\",\n            \"INPUT_ROLE_SAVE\": \"Save\",\n            \"INPUT_ROLE_SCREENSHOT\": \"Screenshot\",\n            \"INPUT_ROLE_SLOW\": \"Walk\",\n            \"INPUT_ROLE_SPRINT\": \"Sprint\",\n            \"INPUT_ROLE_STEP_LEFT\": \"Step Left\",\n            \"INPUT_ROLE_STEP_RIGHT\": \"Step Right\",\n            \"INPUT_ROLE_SWITCH_BORDERS\": \"Switch Borders Size\",\n            \"INPUT_ROLE_SWITCH_UPSCALING\": \"Switch Upscaling Factor\",\n            \"INPUT_ROLE_TOGGLE_BILINEAR_FILTER\": \"Toggle Bilinear Filter\",\n            \"INPUT_ROLE_TOGGLE_FULLSCREEN\": \"Toggle Fullscreen\",\n            \"INPUT_ROLE_TOGGLE_PHOTO_MODE\": \"Toggle Photo Mode\",\n            \"INPUT_ROLE_TOGGLE_TEXTURES\": \"Toggle Textures\",\n            \"INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER\": \"Toggle Trapezoid Filter\",\n            \"INPUT_ROLE_TOGGLE_UI\": \"Toggle UI\",\n            \"INPUT_ROLE_TOGGLE_WIREFRAME\": \"Toggle Wireframe\",\n            \"INPUT_ROLE_TURBO_CHEAT\": \"Turbo Speed\",\n            \"INPUT_ROLE_UP\": \"Run\",\n            \"INPUT_ROLE_USE_BIG_MEDI\": \"Large Medi\",\n            \"INPUT_ROLE_USE_FLARE\": \"Flare\",\n            \"INPUT_ROLE_USE_SMALL_MEDI\": \"Small Medi\",\n        },\n        \"JUMP_LOCK_MODE\": {\n            \"JUMP_LOCK_DISABLED\": \"Disabled\",\n            \"JUMP_LOCK_LEGACY\": \"Legacy\",\n            \"JUMP_LOCK_TUNED\": \"Tuned\",\n        },\n        \"LIGHTING_CONTRAST\": {\n            \"LIGHTING_CONTRAST_HIGH\": \"High\",\n            \"LIGHTING_CONTRAST_LOW\": \"Low\",\n            \"LIGHTING_CONTRAST_MEDIUM\": \"Medium\",\n        },\n        \"LOADING_SCREENS_MODE\": {\n            \"LOADING_SCREENS_ALWAYS\": \"Always\",\n            \"LOADING_SCREENS_DISABLED\": \"Disabled\",\n            \"LOADING_SCREENS_NEW_GAMES\": \"New games\",\n        },\n        \"LOOK_MODE\": {\n            \"LOOK_MODE_ENHANCED\": \"Enhanced\",\n            \"LOOK_MODE_RESTRICTED\": \"Restricted\",\n            \"LOOK_MODE_UNRESTRICTED\": \"Unrestricted\",\n        },\n        \"MUSIC_LOAD_CONDITION\": {\n            \"MUSIC_LOAD_CONDITION_ALWAYS\": \"Always\",\n            \"MUSIC_LOAD_CONDITION_NEVER\": \"Never\",\n            \"MUSIC_LOAD_CONDITION_NON_AMBIENT\": \"Non-ambient\",\n        },\n        \"PROJECTILE_AREA_DAMAGE\": {\n            \"PROJECTILE_AREA_DAMAGE_MULTI_SWEEP\": \"Multi-sweep\",\n            \"PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP\": \"Single-sweep\",\n        },\n        \"QUICK_GUNS_MODE\": {\n            \"QUICK_GUNS_MODE_DRAW_AND_HOLSTER\": \"Draw or holster\",\n            \"QUICK_GUNS_MODE_DRAW_ONLY\": \"Draw only\",\n        },\n        \"SCREENSHOT_FORMAT\": {\n            \"SCREENSHOT_FORMAT_JPEG\": \"JPG\",\n            \"SCREENSHOT_FORMAT_PNG\": \"PNG\",\n        },\n        \"SHADOW_TYPE\": {\n            \"SHADOW_TYPE_CIRCLE\": \"Circle\",\n            \"SHADOW_TYPE_OCTAGON\": \"Octagon\",\n            \"SHADOW_TYPE_SPRITE\": \"Sprite\",\n        },\n        \"STATS_STYLE\": {\n            \"STATS_STYLE_BARE\": \"Bare\",\n            \"STATS_STYLE_BORDERED\": \"Bordered\",\n        },\n        \"SUNGLASSES_MODE\": {\n            \"SUNGLASSES_MODE_OFF\": \"Off\",\n            \"SUNGLASSES_MODE_OPAQUE\": \"Opaque\",\n            \"SUNGLASSES_MODE_TRANSPARENT\": \"Transparent\",\n        },\n        \"TARGET_LOCK_MODE\": {\n            \"TARGET_LOCK_MODE_FULL\": \"Full lock\",\n            \"TARGET_LOCK_MODE_NONE\": \"No lock\",\n            \"TARGET_LOCK_MODE_SEMI\": \"Semi lock\",\n        },\n        \"TEXTURE_FILTER\": {\n            \"TEXTURE_FILTER_BILINEAR\": \"Bilinear\",\n            \"TEXTURE_FILTER_POINT\": \"Point\",\n        },\n        \"UI_ELEMENT_LOCATION\": {\n            \"UI_ELEMENT_LOCATION_BOTTOM_CENTER\": \"Bottom center\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_LEFT\": \"Bottom left\",\n            \"UI_ELEMENT_LOCATION_BOTTOM_RIGHT\": \"Bottom right\",\n            \"UI_ELEMENT_LOCATION_TOP_CENTER\": \"Top center\",\n            \"UI_ELEMENT_LOCATION_TOP_LEFT\": \"Top left\",\n            \"UI_ELEMENT_LOCATION_TOP_RIGHT\": \"Top right\",\n        },\n        \"UI_STYLE\": {\n            \"UI_STYLE_PC\": \"PC\",\n            \"UI_STYLE_PS1\": \"PS1\",\n        },\n        \"WALL_GLITCH_MODE\": {\n            \"WALL_GLITCH_FIXED\": \"Fixed\",\n            \"WALL_GLITCH_TR1\": \"TR1\",\n            \"WALL_GLITCH_TR2\": \"TR2\",\n        }\n    },\n    \"settings\": {\n        \"audio.ambient_volume\": {\n            \"title\": \"Ambient volume\",\n            \"description\": \"Adjusts ambient volume.\",\n        },\n        \"audio.cutscene_volume\": {\n            \"title\": \"Cutscene volume\",\n            \"description\": \"Adjusts the ingame cutscenes volume.\",\n        },\n        \"audio.enable_lara_mic\": {\n            \"title\": \"Microphone near Lara\",\n            \"description\": \"Set the microphone to be at Lara's position. If disabled, the microphone will be at the camera's position.\",\n        },\n        \"audio.enable_music_in_inventory\": {\n            \"title\": \"Play music in inventory\",\n            \"description\": \"Lets game sounds, ambient and music continue playing in the inventory screen.\",\n        },\n        \"audio.enable_music_in_menu\": {\n            \"title\": \"Main menu music\",\n            \"description\": \"Plays music in the main menu.\",\n        },\n        \"audio.enable_pitched_sounds\": {\n            \"title\": \"Pitched sounds\",\n            \"description\": \"Allows sound effects to be randomly, slightly pitched to vary the game sounds.\",\n        },\n        \"audio.enable_ps1_sfx\": {\n            \"title\": \"PS1 SFX replacements\",\n            \"description\": \"Enables specific sound effect replacements using PS1 equivalents.\\n\\n- Uzi fire (TR1 only)\\n- Lara barefoot sounds (TR2 only)\",\n        },\n        \"audio.enable_underwater_anim_sfx\": {\n            \"title\": \"Underwater animation SFX\",\n            \"description\": \"Allows control over playing specific animation sound effects - for objects such as doors and trapdoors - when the camera is underwater.\",\n        },\n        \"audio.fix_chainblock_secret_sound\": {\n            \"title\": \"Fix chain block sound\",\n            \"description\": \"Prevents the secret sound from incorrectly playing when using the golden key in Tomb of Tihocan.\",\n        },\n        \"audio.fix_secrets_killing_music\": {\n            \"title\": \"Layered secret music\",\n            \"description\": \"Fixes the sound of collecting a secret stopping the active music track.\",\n        },\n        \"audio.fix_speeches_killing_music\": {\n            \"title\": \"Layered enemy speech\",\n            \"description\": \"Fixes enemies stopping the active music track when they speak.\",\n        },\n        \"audio.fmv_volume\": {\n            \"title\": \"FMV volume\",\n            \"description\": \"Adjusts the movies volume.\",\n        },\n        \"audio.inventory_ambient_volume\": {\n            \"title\": \"Ambient volume (inventory)\",\n            \"description\": \"Adjusts ambient volume in inventory screen.\",\n        },\n        \"audio.inventory_music_volume\": {\n            \"title\": \"Music volume (inventory)\",\n            \"description\": \"Adjusts music volume in inventory screen.\",\n        },\n        \"audio.load_music_triggers\": {\n            \"title\": \"Fix one-shot music triggers\",\n            \"description\": \"Loads previously triggered, one shot music so one shot music tracks do not replay.\",\n        },\n        \"audio.master_volume\": {\n            \"title\": \"\\\\{icon music} Master volume\",\n            \"description\": \"Adjusts all ingame volume. The rest of the settings are relative to this volume.\",\n        },\n        \"audio.music_load_condition\": {\n            \"title\": \"Restore music on load\",\n            \"description\": \"Loads the music track that was playing when the game was saved.\\n\\n- Never: do not restore music tracks on load.\\n- Non-ambient: restore only non-ambient music tracks on load.\\n- Always: restore any kind of music track on load.\",\n        },\n        \"audio.music_volume\": {\n            \"title\": \"Music volume\",\n            \"description\": \"Adjusts music volume.\",\n        },\n        \"audio.mute_out_of_focus\": {\n            \"title\": \"Mute audio when focus lost\",\n            \"description\": \"Mutes all music and sound effects when the game window is not focused.\",\n        },\n        \"audio.sound_volume\": {\n            \"title\": \"\\\\{icon sound} Sound volume\",\n            \"description\": \"Adjusts sound effects volume.\",\n        },\n        \"audio.underwater_ambient_volume\": {\n            \"title\": \"Ambient volume (underwater)\",\n            \"description\": \"Adjusts ambient volume when underwater.\",\n        },\n        \"audio.underwater_music_volume\": {\n            \"title\": \"Music volume (underwater)\",\n            \"description\": \"Adjusts music volume when underwater.\",\n        },\n        \"debug.enable_endless_flare_time\": {\n            \"title\": \"Endless flare time\",\n            \"description\": \"Prevents the handheld flares from ever going out. Thrown flares will still go out as normal.\",\n        },\n        \"debug.enable_endless_sprint\": {\n            \"title\": \"Endless sprint\",\n            \"description\": \"Prevents Lara from ever getting tired when sprinting. Obstacles will still bring her to a stop.\",\n        },\n        \"gameplay.ally_hostility_policy\": {\n            \"title\": \"Ally hostility policy\",\n            \"description\": \"Controls how friendly units react when taking damage.\\n\\n- Individual: each ally changes hostility on their own (TR3 style).\\n- Shared: all allies become hostile together (TR2 monk style).\",\n        },\n        \"gameplay.camera_speed\": {\n            \"title\": \"Camera speed\",\n            \"description\": \"Changes how fast the manual camera moves.\",\n        },\n        \"gameplay.change_pierre_spawn\": {\n            \"title\": \"Change Pierre spawn mode\",\n            \"description\": \"Makes a freshly triggered (runaway) Pierre replace an already existing (runaway) Pierre.\",\n        },\n        \"gameplay.creature_drown_policy\": {\n            \"title\": \"Creature drown policy\",\n            \"description\": \"Controls how land creatures behave in water rooms.\\n\\n- Never: land creatures will never drown (TR1 style).\\n- Default: land creatures will drown in 2-click or deeper water (TR2/3 style).\\n- Submerged: land creatures will drown only when fully submerged.\",\n        },\n        \"gameplay.disable_extra_guns\": {\n            \"title\": \"Remove extra guns\",\n            \"description\": \"Removes all weapon and ammo pickups from the game except Pistols (for Pistols Only challenge runs).\",\n        },\n        \"gameplay.disable_healing_between_levels\": {\n            \"title\": \"Persistent damage\",\n            \"description\": \"Stops Lara from healing when starting a new level (for no Heal challenge runs).\",\n        },\n        \"gameplay.disable_medpacks\": {\n            \"title\": \"Remove medipacks\",\n            \"description\": \"Removes all medipacks from the game (for No Meds challenge runs).\",\n        },\n        \"gameplay.disable_trex_collision\": {\n            \"title\": \"Remove dead T-Rex collision\",\n            \"description\": \"Removes all collision with T-Rex upon death. This helps when the T-Rex's body blocks the passage out.\",\n        },\n        \"gameplay.enable_ally_targeting\": {\n            \"title\": \"Allow targeting allies\",\n            \"description\": \"Allows Lara to target allies, such as monks. If disabled, allies will be immune to Lara's ammunition.\",\n        },\n        \"gameplay.enable_auto_item_selection\": {\n            \"title\": \"Key item pre-selection\",\n            \"description\": \"When Lara presses action against a keyhole or puzzle slot, and she has the corresponding item in the inventory, that item will be pre-selected.\",\n        },\n        \"gameplay.enable_back_slope_stumble\": {\n            \"title\": \"Backwards slope stumble\",\n            \"description\": \"Makes Lara perform a stumble if she hops back and there is a slope behind her (TR3). If disabled, Lara will come to a hard stop against the slope (TR1/2).\",\n        },\n        \"gameplay.enable_body_bags\": {\n            \"title\": \"Body bag triggers\",\n            \"description\": \"Enables removal of killed enemies when Lara crosses specific triggers in certain levels. If disabled, dead enemies will always be drawn.\",\n        },\n        \"gameplay.enable_boulder_shake\": {\n            \"title\": \"Enable boulder shake\",\n            \"description\": \"If enabled, the camera will shake when a boulder is in motion.\",\n        },\n        \"gameplay.enable_bouncy_grenades\": {\n            \"title\": \"Bouncy grenades\",\n            \"description\": \"Enables TR3-style grenade behavior: they ricochet off walls and slopes and produce a larger blast radius, but at the expense of reduced velocity.\",\n        },\n        \"gameplay.enable_cheats\": {\n            \"title\": \"Cheats\",\n            \"description\": \"Enables various cheats:\\n\\n- L: immediately end the level.\\n- I: give Lara all weapons; a boost of ammo and medipacks; and all plot items for the current level.\\n- O: enable fly cheat (swimming midair).\\n  - WALK key: exit fly mode.\\n  - GUN key: open the closest door (doesn't work in some places).\",\n        },\n        \"gameplay.enable_cinematics\": {\n            \"title\": \"Cinematics\",\n            \"description\": \"Enables cinematics at the beginning of certain levels that have them defined.\",\n        },\n        \"gameplay.enable_compass_stats\": {\n            \"title\": \"Level statistics in compass\",\n            \"description\": \"Enables showing level statistics when the compass is selected.\",\n        },\n        \"gameplay.enable_console\": {\n            \"title\": \"Console\",\n            \"description\": \"Enables the developer console.\",\n        },\n        \"gameplay.enable_controlled_drops\": {\n            \"title\": \"Controlled drops\",\n            \"description\": \"Allows Lara to turn mid-air and grab the ledge she just stepped off, if the action input is held while falling.\",\n        },\n        \"gameplay.enable_crawl_jump\": {\n            \"title\": \"Crawl exit jump\",\n            \"description\": \"Allows Lara to jump out of crawlspaces.\",\n        },\n        \"gameplay.enable_crawl_tilt\": {\n            \"title\": \"Crawl tilt\",\n            \"description\": \"Aligns Lara's rotation to the floor geometry when crawling.\",\n        },\n        \"gameplay.enable_crawling\": {\n            \"title\": \"Crawling\",\n            \"description\": \"Allows Lara to crouch and crawl.\",\n        },\n        \"gameplay.enable_credits\": {\n            \"title\": \"Credit screens\",\n            \"description\": \"Enables credits screens shown after completing the game. Does not influence the final statistics screen.\",\n        },\n        \"gameplay.enable_crouch_roll\": {\n            \"title\": \"Crouch roll\",\n            \"description\": \"Allows Lara to do a forward roll while crouched by pressing sprint.\",\n        },\n        \"gameplay.enable_cutscenes\": {\n            \"title\": \"Cutscenes\",\n            \"description\": \"Enables cutscenes playing.\",\n        },\n        \"gameplay.enable_demo\": {\n            \"title\": \"Demo mode\",\n            \"description\": \"Enables demos showing in the main menu.\",\n        },\n        \"gameplay.enable_enemy_rotation\": {\n            \"title\": \"Randomize enemy start angle\",\n            \"description\": \"Applies an additional random angle to some enemies when they are initialised.\",\n        },\n        \"gameplay.enable_enhanced_saves\": {\n            \"title\": \"Save effects\",\n            \"description\": \"Enhances savegames so that graphic effects, waterfall mist, flame emitters, and more are saved instead of disappearing on load.\",\n        },\n        \"gameplay.enable_fmv\": {\n            \"title\": \"FMVs\",\n            \"description\": \"Enables FMVs playing.\",\n        },\n        \"gameplay.enable_game_modes\": {\n            \"title\": \"Game mode selection\",\n            \"description\": \"Allows new game plus options to be selected from the new game passport menu.\\n\\n- New Game+: unlocks all weapons with infinite ammo; enemies have double the HP.\\n- Japanese NG: weapons do double damage and flare pickups contain 8 rather than 6.\\n- Japanese NG+: combination of New Game+ and Japanese NG.\",\n        },\n        \"gameplay.enable_idle_pose_camera\": {\n            \"title\": \"Idle pose camera\",\n            \"description\": \"Adjusts the camera to face Lara during her idle pose animation. Pressing look will reset the camera.\",\n        },\n        \"gameplay.enable_inverted_look\": {\n            \"title\": \"Inverted look\",\n            \"description\": \"Inverts the Y axis controls when Lara looks.\",\n        },\n        \"gameplay.enable_item_examining\": {\n            \"title\": \"Item examination\",\n            \"description\": \"For custom levels - allows item descriptions to be displayed in the inventory where the level author has provided suitable data.\",\n        },\n        \"gameplay.enable_jump_twists\": {\n            \"title\": \"Jump twists\",\n            \"description\": \"Enables jump twists and somersaults i.e. press roll during jump and swan dive animations.\",\n        },\n        \"gameplay.enable_killer_pushblocks\": {\n            \"title\": \"Enable killer pushblocks\",\n            \"description\": \"If enabled, when a pushblock falls from the air and lands on Lara, it will kill her outright. Otherwise, Lara will clip on top of the block and survive.\",\n        },\n        \"gameplay.enable_lean_jumping\": {\n            \"title\": \"Lean jumping\",\n            \"description\": \"Allows Lara to creep forwards or backwards further when performing neutral jumps with the relevant input key pressed.\",\n        },\n        \"gameplay.enable_ledge_jumps\": {\n            \"title\": \"Ledge jumps\",\n            \"description\": \"Allows Lara to jump upwards or backwards when hanging from a ledge, provided she has a solid surface in front of her to push against.\",\n        },\n        \"gameplay.enable_legal\": {\n            \"title\": \"Legal screens\",\n            \"description\": \"Enables legal screen and Core Design FMV at the game start.\",\n        },\n        \"gameplay.enable_manual_camera\": {\n            \"title\": \"Manual camera\",\n            \"description\": \"Enables the camera keys (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}) used to control Photo Mode camera, to also rotate the ingame camera.\",\n        },\n        \"gameplay.enable_neutral_twists\": {\n            \"title\": \"Neutral twists\",\n            \"description\": \"Allows Lara to twist in the air while performing a neutral jump. Press jump and roll inputs together while stationary.\",\n        },\n        \"gameplay.enable_pickup_aids\": {\n            \"title\": \"Pickup aids\",\n            \"description\": \"Enables an intermittent twinkling effect near pickup items to highlight their presence.\",\n        },\n        \"gameplay.enable_play_previous_levels\": {\n            \"title\": \"Play previous levels\",\n            \"description\": \"Enables the \\\"Play previous levels\\\" and \\\"Story so far...\\\" features in the New Game selection screen.\",\n        },\n        \"gameplay.enable_responsive_crawl\": {\n            \"title\": \"Responsive crawling\",\n            \"description\": \"Enables enhancements over original crawling mechanics.\\n\\n- Allows resuming crawling more quickly after coming to a stop.\\n- Allows transitioning from run/sprint to crawl without first coming to a stop.\\n- Allows transitioning from crawl to crouch-roll (if enabled) without manually crouching first.\\n- Allows turning while crouched.\\n- Restores Lara's crawl pickup animation (excluding flares).\",\n        },\n        \"gameplay.enable_responsive_sprint\": {\n            \"title\": \"Responsive sprinting\",\n            \"description\": \"Enables a more responsive sprinting state for Lara.\\n\\n- allows sprinting whenever Lara has energy, rather than only when her stamina is full.\\n- allows sprinting up stairs rather than being interrupted by Lara's regular run animation.\",\n        },\n        \"gameplay.enable_save_crystals\": {\n            \"title\": \"Save crystals\",\n            \"description\": \"Limits saving to the beginning of levels and save crystals. Levels have limited, single use save crystals like the PS1 version. Changing this option will require restarting the level.\",\n        },\n        \"gameplay.enable_slide_to_run\": {\n            \"title\": \"Slide-to-run\",\n            \"description\": \"Allows Lara to start running immediately when she reaches ground after sliding forwards on a slope. Hold the forward input to activate.\",\n        },\n        \"gameplay.enable_slow_ledge_swing\": {\n            \"title\": \"Slow ledge swing\",\n            \"description\": \"Allows Lara to swing slowly when she has grabbed a very thin ledge (TR3 style). If disabled, Lara will swing briefly before coming to a resting hanging position (TR1/2 style).\",\n        },\n        \"gameplay.enable_smooth_wall_deflect\": {\n            \"title\": \"Smooth wall deflection\",\n            \"description\": \"Allows Lara to recover more quickly after hitting a wall and a direction key is held together with forward.\",\n        },\n        \"gameplay.enable_soft_statics\": {\n            \"title\": \"Soft static collision\",\n            \"description\": \"Allows Lara to move smoothly against static meshes - similar to TR4+ - rather than coming to a hard stop.\",\n        },\n        \"gameplay.enable_sprint\": {\n            \"title\": \"Sprinting\",\n            \"description\": \"Allows Lara to sprint, similar to TR3+.\",\n        },\n        \"gameplay.enable_step_roll_boost\": {\n            \"title\": \"Step roll boost\",\n            \"description\": \"Allows Lara to be boosted off a one-click high step if roll is pressed near the edge.\",\n        },\n        \"gameplay.enable_swing_cancel\": {\n            \"title\": \"Swing cancels\",\n            \"description\": \"Allows Lara's ledge-swinging animation to be canceled by letting go and quickly grabbing again.\",\n        },\n        \"gameplay.enable_target_change\": {\n            \"title\": \"Target change\",\n            \"description\": \"Enables TR4+ target changing while aiming weapons. Press the Change Target button while aiming to change targets.\",\n        },\n        \"gameplay.enable_timer_in_inventory\": {\n            \"title\": \"Timer counts in inventory\",\n            \"description\": \"Makes the in-game timer work even while the game is showing the inventory.\",\n        },\n        \"gameplay.enable_toggle_crouch\": {\n            \"title\": \"Toggle crouch\",\n            \"description\": \"Allows Lara to stay crouched after pressing the crouch input once. Press crouch again to stand up.\",\n        },\n        \"gameplay.enable_toggle_sprint\": {\n            \"title\": \"Toggle sprint\",\n            \"description\": \"Allows Lara to keep sprinting after pressing the sprint input once. Press sprint again to stop sprinting.\",\n        },\n        \"gameplay.enable_total_stats\": {\n            \"title\": \"Final statistics screen\",\n            \"description\": \"Enables a total game statistics screen that plays after the credits.\",\n        },\n        \"gameplay.enable_tr2_jumping\": {\n            \"title\": \"Responsive jumping\",\n            \"description\": \"Allows Lara to jump at any point while running.\",\n        },\n        \"gameplay.enable_tr2_swim_cancel\": {\n            \"title\": \"Responsive swim cancel\",\n            \"description\": \"Allows Lara to stop more responsively underwater when the swim key is released.\",\n        },\n        \"gameplay.enable_tr2_swimming\": {\n            \"title\": \"Smooth swimming\",\n            \"description\": \"Gives Lara's underwater turn rate an acceleration curve for smoother movement, as per TR2+ originally. Disabling this option will give Lara a snappier turn rate, per original TR1.\",\n        },\n        \"gameplay.enable_uw_roll\": {\n            \"title\": \"Underwater roll\",\n            \"description\": \"Allows Lara to roll while underwater.\",\n        },\n        \"gameplay.enable_wading\": {\n            \"title\": \"Wading\",\n            \"description\": \"Allows Lara to wade through shallow water, rather than becoming stuck on the water surface.\",\n        },\n        \"gameplay.enable_walk_to_items\": {\n            \"title\": \"Animated interactions\",\n            \"description\": \"Makes Lara walk to pickups and switches when nearby instead of teleporting to them.\",\n        },\n        \"gameplay.fix_alligator_ai\": {\n            \"title\": \"Fix alligator AI\",\n            \"description\": \"Fixes alligators dealing no damage if Lara remains still in the water.\",\n        },\n        \"gameplay.fix_bear_ai\": {\n            \"title\": \"Fix bear AI\",\n            \"description\": \"Fixes the bear pat attack so it does not miss Lara.\",\n        },\n        \"gameplay.fix_bridge_collision\": {\n            \"title\": \"Fix bridge collision\",\n            \"description\": \"Fixes Lara not being able to grab parts of some bridges and invisible walls at the edge. Also fixes collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground.\",\n        },\n        \"gameplay.fix_descending_glitch\": {\n            \"title\": \"Fix breakable floor falls\",\n            \"description\": \"Fixes sidestepping and walking backwards on breakable tiles causing Lara to immediately descend to the tile underneath.\",\n        },\n        \"gameplay.fix_flare_throw_priority\": {\n            \"title\": \"Fix flare throw priority\",\n            \"description\": \"Fixes Lara prioritising throwing a spent flare while in mid-air, which can lead to being unable to grab ledges.\",\n        },\n        \"gameplay.fix_floor_data_issues\": {\n            \"title\": \"Fix floor data issues\",\n            \"description\": \"Fixes original issues with floor data/triggers.\",\n        },\n        \"gameplay.fix_free_flare_glitch\": {\n            \"title\": \"Fix free flare glitch\",\n            \"description\": \"Fixes the ability to spawn a free flare when pressing the flare input while picking up any item.\",\n        },\n        \"gameplay.fix_item_duplication_glitch\": {\n            \"title\": \"Fix item duplication glitch\",\n            \"description\": \"Fixes the ability to duplicate usage of key items in the inventory.\",\n        },\n        \"gameplay.fix_lara_pickup_embed\": {\n            \"title\": \"Fix pickup embed glitch\",\n            \"description\": \"Fixes Lara sometimes drifting into walls when collecting underwater items, and fixes Lara embedding under steeply sloped ceilings when picking up an item above water.\",\n        },\n        \"gameplay.fix_m16_accuracy\": {\n            \"title\": \"Fix M16/MP5 accuracy\",\n            \"description\": \"Fixes the accuracy of the M16/MP5 while Lara is running.\",\n        },\n        \"gameplay.fix_monkey_pickup_priority\": {\n            \"title\": \"Fix monkey pickup priority\",\n            \"description\": \"Attacked monkeys will prioritize retaliating over collecting Medi packs and Keys.\",\n        },\n        \"gameplay.fix_pipeman_aim\": {\n            \"title\": \"Fix pipeman aim\",\n            \"description\": \"Fixes the pipeman sometimes not being able to aim darts at Lara correctly.\",\n        },\n        \"gameplay.fix_qwop_glitch\": {\n            \"title\": \"Fix QWOP glitch\",\n            \"description\": \"Fixes Lara jumping on small steps sometimes resulting in a weird running animation, known as a QWOP state.\",\n        },\n        \"gameplay.fix_step_glitch\": {\n            \"title\": \"Fix step glitch\",\n            \"description\": \"Fixes Lara sometimes being pushed into walls adjacent to steps when running up them in a specific way.\",\n        },\n        \"gameplay.fix_wade_wall_hit\": {\n            \"title\": \"Fix wading wall hit\",\n            \"description\": \"Fixes Lara not responding to hitting a wall while wading.\",\n        },\n        \"gameplay.fix_walk_run_jump\": {\n            \"title\": \"Fix walk run jump\",\n            \"description\": \"Fixes Lara at times not being able to jump immediately after going from her walking to running animation.\",\n        },\n        \"gameplay.fix_wall_geometry\": {\n            \"title\": \"Fix wall geometry\",\n            \"description\": \"Fixes cases in OG level geometry where tilts inside walls can lead to inaccurate height calculations.\",\n        },\n        \"gameplay.fix_water_exit\": {\n            \"title\": \"Fix water exit\",\n            \"description\": \"Fixes Lara being able to go directly from a water room to an adjacent dry room, or to a dry room below. Additionally, this will prevent Lara from being able to climb out of water onto non-standable slopes.\",\n        },\n        \"gameplay.harpoon_recoil\": {\n            \"title\": \"Harpoon recoil\",\n            \"description\": \"Sets how often Lara must reload the harpoon gun, based on her current ammo count. For example, if set to 3, she'll need to reload after every third shot. Set to 0 to disable reloading entirely.\",\n        },\n        \"gameplay.idle_pose_timeout\": {\n            \"title\": \"Idle pose timeout\",\n            \"description\": \"Allows Lara to enter a pose animation when she has been idle for the specified number of seconds. Set to 0 to disable.\",\n        },\n        \"gameplay.jump_lock_mode\": {\n            \"title\": \"Jump lock mode\",\n            \"description\": \"For responsive jumping, allows controlling how soon after starting to run that Lara is permitted to jump.\\n\\n- Legacy: matches original TR2 timing.\\n- Tuned: jumping is possible 2 frames earlier.\\n- Disabled: jumping is possible immediately after the start-to-run animation.\",\n        },\n        \"gameplay.loading_screens\": {\n            \"title\": \"Loading screens\",\n            \"description\": \"Controls loading screens before level loads.\\n\\n- Disabled: never show loading screens.\\n- Always: show loading screens.\\n- New games: skip showing loading screens when loading a save.\",\n        },\n        \"gameplay.look_mode\": {\n            \"title\": \"Look mode\",\n            \"description\": \"Allows controlling when Lara is able to use look.\\n\\n- Restricted: look is only permitted when Lara is stationary, and never when underwater.\\n- Enhanced: look is permitted during most animations, aside from ones such as pushing a block.\\n- Unrestricted: look is permitted at any time during normal Lara control.\",\n        },\n        \"gameplay.maximum_quick_save_slots\": {\n            \"title\": \"Number of quick save slots\",\n            \"description\": \"Changes the number of available quick save slots.\",\n        },\n        \"gameplay.maximum_save_slots\": {\n            \"title\": \"Number of save slots\",\n            \"description\": \"Changes the number of available save slots.\",\n        },\n        \"gameplay.pause_on_focus_lost\": {\n            \"title\": \"Pause when focus lost\",\n            \"description\": \"Stops gameplay from advancing when the game window loses focus.\",\n        },\n        \"gameplay.projectile_area_damage\": {\n            \"title\": \"Projectile area damage\",\n            \"description\": \"Controls how the area-of-effect for Rocket Launcher and Grenade Launcher propagates.\\n\\n- Single-sweep: TR1 & TR2 behavior.\\n- Multi-sweep: TR3 behavior.\\n\\nThe multi-sweep option often ends up doing double damage to individual enemies.\",\n        },\n        \"gameplay.remember_gun_status\": {\n            \"title\": \"Remember guns between levels\",\n            \"description\": \"Makes Lara remember which gun she was using last in the previous level when starting a new level. If disabled, Lara will revert to holstered pistols.\",\n        },\n        \"gameplay.restore_ps1_enemies\": {\n            \"title\": \"Restore PS1 enemies\",\n            \"description\": \"Adds the mummy that appears in the PlayStation version of City of Khamoon, room 25.\\nChanging this option will require restarting the game.\",\n        },\n        \"gameplay.start_lara_hitpoints\": {\n            \"title\": \"Lara's starting health\",\n            \"description\": \"Sets Lara's health value for the beginning of each level.\",\n        },\n        \"gameplay.target_mode\": {\n            \"title\": \"Weapon lock mode\",\n            \"description\": \"Changes the behavior of how weapons lock onto targets.\\n\\n- Full lock: always keep target lock even if the enemy moves out of sight or dies (OG TR1-3).\\n- Semi lock: keep target lock if the enemy moves out of sight but lose target lock if the enemy dies.\\n- No lock: lose target lock if the enemy goes out of sight or dies (TR4+).\",\n        },\n        \"gameplay.wall_glitch_mode\": {\n            \"title\": \"Wall glitch mode\",\n            \"description\": \"Allows using TR1 wall glitch behavior in TR2 and vice-versa; equally allows fixing all types of wall glitch.\",\n        },\n        \"input.enable_buffering_func_keys\": {\n            \"title\": \"Buffering (F-keys)\",\n            \"description\": \"Enables F-key (1-frame) buffering to achieve precise control of Lara's movement. This function originally only exists in the TombATI port (TR1).\",\n        },\n        \"input.enable_buffering_inventory\": {\n            \"title\": \"Buffering (inventory)\",\n            \"description\": \"Enables inventory (2-frame) buffering to achieve precise control of Lara's movement.\",\n        },\n        \"input.enable_responsive_passport\": {\n            \"title\": \"Responsive passport\",\n            \"description\": \"Disables blocking user input when passport flips pages, scheduling the page flips instead.\",\n        },\n        \"input.enable_tr3_sidesteps\": {\n            \"title\": \"Enhanced sidesteps\",\n            \"description\": \"Enables TR3+ style sidesteps, e.g. shift+directional arrows. Dedicated sidestep buttons will still work.\",\n        },\n        \"input.quick_guns_mode\": {\n            \"title\": \"Quick gun keys\",\n            \"description\": \"Controls the behavior of the quick gun equip keys.\\n\\n- Draw only: pressing a key will cause Lara to equip the assigned gun.\\n- Draw or holster: same as above, plus Lara will undraw the assigned gun if she's currently carrying it.\",\n        },\n        \"language\": {\n            \"title\": \"Language\",\n            \"description\": \"Changes the language of the UI text.\",\n        },\n        \"rendering.anisotropy_filter\": {\n            \"title\": \"Anisotropy filter\",\n            \"description\": \"Enhances texture filtering at distances.\",\n        },\n        \"rendering.aspect_mode\": {\n            \"title\": \"Aspect mode\",\n            \"description\": \"Forces certain game aspect ratios with letterbox.\",\n        },\n        \"rendering.borders\": {\n            \"title\": \"Borders\",\n            \"description\": \"Adds black borders around the game window.\",\n        },\n        \"rendering.enable_trapezoid_filter\": {\n            \"title\": \"Trapezoid filter\",\n            \"description\": \"Corrects rendering of quadrilaterals.\",\n        },\n        \"rendering.enable_vsync\": {\n            \"title\": \"VSync\",\n            \"description\": \"Turns V-Sync on or off.\",\n        },\n        \"rendering.fps\": {\n            \"title\": \"FPS\",\n            \"description\": \"Sets game frames per second.\",\n        },\n        \"rendering.lighting_contrast\": {\n            \"title\": \"Lighting contrast\",\n            \"description\": \"Boosts contrast for dynamic light sources such as flares and gun flashes.\",\n        },\n        \"rendering.screenshot_format\": {\n            \"title\": \"Screenshot format\",\n            \"description\": \"Screenshot file format.\",\n        },\n        \"rendering.sprite_lock_mode\": {\n            \"title\": \"Sprite lock mode\",\n            \"description\": \"Controls which axes to lock when showing sprites on the screen.\\n\\n- None: show the sprites normally.\\n- Roll: lock the roll axis – useful only in photo mode.\\n- Roll & pitch: ensure the sprites stand upright and do not lie on the ground when looking at them from above.\\n- Perspective: lock roll and pitch axes and addititonally, rotate the sprites slightly towards the center of the screen.\",\n        },\n        \"rendering.texture_filter\": {\n            \"title\": \"Texture filter\",\n            \"description\": \"Switches between smooth and pixel ingame textures.\",\n        },\n        \"rendering.ui_filter\": {\n            \"title\": \"UI filter\",\n            \"description\": \"Switches between smooth and pixel UI textures.\",\n        },\n        \"rendering.upscaling_factor\": {\n            \"title\": \"Upscaling factor\",\n            \"description\": \"Upscales game by a set factor, maintaining pixellated look.\",\n        },\n        \"rendering.upscaling_filter\": {\n            \"title\": \"Upscaling filter\",\n            \"description\": \"Switches smooth or pixel look for the whole screen.\",\n        },\n        \"ui.airbar_color\": {\n            \"title\": \"Airbar color\",\n            \"description\": \"Color of the airbar.\",\n        },\n        \"ui.airbar_color_ps1\": {\n            \"title\": \"Airbar color\",\n            \"description\": \"Color of the airbar.\",\n        },\n        \"ui.airbar_location\": {\n            \"title\": \"Airbar location\",\n            \"description\": \"Location where the airbar is displayed.\",\n        },\n        \"ui.ammo_counter_location\": {\n            \"title\": \"Ammo counter location\",\n            \"description\": \"Location where the ammo counter is displayed.\",\n        },\n        \"ui.bar_look\": {\n            \"title\": \"Bars appearance\",\n            \"description\": \"Controls the visual appearance of the UI bars.\",\n        },\n        \"ui.bar_scale\": {\n            \"title\": \"Bars scale\",\n            \"description\": \"Changes size of UI bars.\",\n        },\n        \"ui.enable_bar_flashing\": {\n            \"title\": \"Flash bars\",\n            \"description\": \"Makes Lara's health and oxygen bars blink when she's running low on either resource.\",\n        },\n        \"ui.enable_smooth_bars\": {\n            \"title\": \"Smooth bars\",\n            \"description\": \"Makes the UI bars use smooth color transitions.\",\n        },\n        \"ui.enable_wraparound\": {\n            \"title\": \"Scroll wrap\",\n            \"description\": \"Lets directional navigation in menus loop around.\",\n        },\n        \"ui.enemy_healthbar_color\": {\n            \"title\": \"Enemy bar color\",\n            \"description\": \"Color of the enemy healthbar.\",\n        },\n        \"ui.enemy_healthbar_color_allies\": {\n            \"title\": \"Ally bar color\",\n            \"description\": \"Color of the allies healthbar. Shown in the location of the enemy healthbars.\",\n        },\n        \"ui.enemy_healthbar_color_allies_ps1\": {\n            \"title\": \"Ally bar color\",\n            \"description\": \"Color of the allies healthbar. Shown in the location of the enemy healthbars.\",\n        },\n        \"ui.enemy_healthbar_color_ps1\": {\n            \"title\": \"Enemy bar color\",\n            \"description\": \"Color of the enemy healthbar.\",\n        },\n        \"ui.enemy_healthbar_location\": {\n            \"title\": \"Enemy bar location\",\n            \"description\": \"Location where the enemy healthbar is displayed.\",\n        },\n        \"ui.enemy_healthbar_show_mode\": {\n            \"title\": \"Enemy bar mode\",\n            \"description\": \"Enables showing a healthbar for the active enemy.\",\n        },\n        \"ui.exposurebar_color\": {\n            \"title\": \"Exposure bar color\",\n            \"description\": \"Color of the cold water exposure bar.\",\n        },\n        \"ui.exposurebar_color_ps1\": {\n            \"title\": \"Exposure bar color\",\n            \"description\": \"Color of the cold water exposure bar.\",\n        },\n        \"ui.exposurebar_location\": {\n            \"title\": \"Exposure bar location\",\n            \"description\": \"Location where the cold water exposure bar is displayed.\",\n        },\n        \"ui.healthbar_color\": {\n            \"title\": \"Healthbar color\",\n            \"description\": \"Color of the healthbar.\",\n        },\n        \"ui.healthbar_color_ps1\": {\n            \"title\": \"Healthbar color\",\n            \"description\": \"Color of the healthbar.\",\n        },\n        \"ui.healthbar_location\": {\n            \"title\": \"Healthbar location\",\n            \"description\": \"Location where the healthbar is displayed.\",\n        },\n        \"ui.healthbar_poison_color\": {\n            \"title\": \"Poison healthbar color\",\n            \"description\": \"Color of the healthbar when Lara is poisoned.\",\n        },\n        \"ui.healthbar_poison_color_ps1\": {\n            \"title\": \"Poison healthbar color\",\n            \"description\": \"Color of the healthbar when Lara is poisoned.\",\n        },\n        \"ui.inventory_background_style\": {\n            \"title\": \"Inventory background\",\n            \"description\": \"Changes the way the background for the inventory ring is displayed.\\n\\n- Dark: TR1 (PC).\\n- Very dark: TR1 (PS1).\\n- Static: TR2 (PC).\\n- Wave: TR2 (PS1).\\n- Monochrome: TR3.\",\n        },\n        \"ui.inventory_fade_effects\": {\n            \"title\": \"Inventory fade effects\",\n            \"description\": \"Fine-tunes the fade effects to be enabled or disabled in the in-game inventory ring. Needs the Fade Effects option to be enabled to work.\",\n        },\n        \"ui.menu_style\": {\n            \"title\": \"Menu style\",\n            \"description\": \"Changes how menus are displayed.\\n\\n - PC: UI style matches the PC version.\\n - PS1: UI style matches the PS1 version.\",\n        },\n        \"ui.pause_background_style\": {\n            \"title\": \"Pause background\",\n            \"description\": \"Changes the way the background for the pause screen is displayed.\\n\\n- Dark: TR1 (PC).\\n- Very dark: TR1 (PS1).\\n- Static: TR2 (PC).\\n- Wave: TR2 (PS1).\\n- Monochrome: TR3.\",\n        },\n        \"ui.pause_fade_effects\": {\n            \"title\": \"Pause fade effects\",\n            \"description\": \"Fine-tunes the fade effects to be enabled or disabled in the pause screen. Needs the Fade Effects option to be enabled to work.\",\n        },\n        \"ui.pickup_scale\": {\n            \"title\": \"Pickup scale\",\n            \"description\": \"Changes size of items animated in the UI when Lara picks something up.\",\n        },\n        \"ui.show_bars\": {\n            \"title\": \"Show bars\",\n            \"description\": \"Disables all ingame bars, obscuring information on Lara's health and other resources (for challenge runs).\",\n        },\n        \"ui.show_pickups_overlay\": {\n            \"title\": \"Pickups overlay\",\n            \"description\": \"Shows items in the bottom-right corner when Lara picks something up.\",\n        },\n        \"ui.show_title_version\": {\n            \"title\": \"Title version text\",\n            \"description\": \"Shows the TRX version string in the title inventory ring.\",\n        },\n        \"ui.sprintbar_color\": {\n            \"title\": \"Sprintbar color\",\n            \"description\": \"Color of the sprintbar.\",\n        },\n        \"ui.sprintbar_color_ps1\": {\n            \"title\": \"Sprintbar color\",\n            \"description\": \"Color of the sprintbar.\",\n        },\n        \"ui.sprintbar_location\": {\n            \"title\": \"Sprintbar location\",\n            \"description\": \"Location where the sprintbar is displayed.\",\n        },\n        \"ui.stats.show_ammo\": {\n            \"title\": \"Ammo hits/used\",\n            \"description\": \"Shows the ammo row in the level statistics.\",\n        },\n        \"ui.stats.show_crystals\": {\n            \"title\": \"Crystals\",\n            \"description\": \"Shows the crystals row in the level statistics.\",\n        },\n        \"ui.stats.show_deaths\": {\n            \"title\": \"Deaths\",\n            \"description\": \"Shows Lara's deaths in the compass statistics and in the level statistics. Death count is updated in the currently loaded save as soon as Lara dies.\",\n        },\n        \"ui.stats.show_distance_travelled\": {\n            \"title\": \"Distance traveled\",\n            \"description\": \"Shows the distance traveled row in the level statistics.\",\n        },\n        \"ui.stats.show_kills\": {\n            \"title\": \"Kills\",\n            \"description\": \"Shows the kills row in the level statistics.\",\n        },\n        \"ui.stats.show_level_header\": {\n            \"title\": \"Level counter\",\n            \"description\": \"Shows the current level number at the top of the level statistics.\",\n        },\n        \"ui.stats.show_medipacks_used\": {\n            \"title\": \"Health packs used\",\n            \"description\": \"Shows the health packs used row in the level statistics.\",\n        },\n        \"ui.stats.show_pickups\": {\n            \"title\": \"Pickups\",\n            \"description\": \"Shows the pickups row in the level statistics.\",\n        },\n        \"ui.stats.show_secrets\": {\n            \"title\": \"Secrets found\",\n            \"description\": \"Shows the secrets found row in the level statistics.\",\n        },\n        \"ui.stats.show_time_taken\": {\n            \"title\": \"Time taken\",\n            \"description\": \"Shows the time taken row in the level statistics.\",\n        },\n        \"ui.stats.show_totals\": {\n            \"title\": \"Show totals\",\n            \"description\": \"Shows totals next to stats when applicable. Secrets remain unaffected by this setting.\",\n        },\n        \"ui.stats.style\": {\n            \"title\": \"Statistics style\",\n            \"description\": \"Controls how the statistics dialog is displayed.\\n\\n- Bare: shows the simpler unframed layout.\\n- Bordered: shows the boxed layout.\",\n        },\n        \"ui.stats_background_style\": {\n            \"title\": \"Stats background\",\n            \"description\": \"Changes the way the background for the end of level stats is displayed.\\n\\n- Dark: TR1 (PC).\\n- Very dark: TR1 (PS1).\\n- Static: TR2 (PC).\\n- Wave: TR2 (PS1).\\n- Monochrome: TR3.\",\n        },\n        \"ui.stats_fade_effects\": {\n            \"title\": \"Stats fade effects\",\n            \"description\": \"Fine-tunes the fade effects to be enabled or disabled in the end of the level statistics screen. Needs the Fade Effects option to be enabled to work.\",\n        },\n        \"ui.text_scale\": {\n            \"title\": \"Text scale\",\n            \"description\": \"Changes the size of UI text.\",\n        },\n        \"visuals.blood_effects\": {\n            \"title\": \"Blood effects\",\n            \"description\": \"Controls blood spark colors.\\n\\n- Disabled: no blood sparks are shown.\\n- Pink: the default in German PC releases of TR3.\\n- Red: the default in all other retail releases.\",\n        },\n        \"visuals.camera_mode\": {\n            \"title\": \"Camera mode\",\n            \"description\": \"Adjusts how camera behaves during actions like using keys.\",\n        },\n        \"visuals.enable_3d_pickups\": {\n            \"title\": \"3D pickups\",\n            \"description\": \"Enables 3D models to be rendered in place of the sprites for pickup items.\",\n        },\n        \"visuals.enable_braid\": {\n            \"title\": \"Lara's braid\",\n            \"description\": \"Enables Lara's braid.\",\n        },\n        \"visuals.enable_breeze\": {\n            \"title\": \"Breeze\",\n            \"description\": \"Enables the breeze effect on Lara's braid in appropriate rooms.\",\n        },\n        \"visuals.enable_exit_fade_effects\": {\n            \"title\": \"Fade on game exit\",\n            \"description\": \"Enables the fade effects when exiting the game to desktop.\",\n        },\n        \"visuals.enable_fade_effects\": {\n            \"title\": \"Fade effects\",\n            \"description\": \"Enable fade transitions, for example between credit graphics or for inventory and pause screen transitions.\",\n        },\n        \"visuals.enable_fire_lighting\": {\n            \"title\": \"Fire lighting\",\n            \"description\": \"Enables dynamic lighting to be generated beside active flames.\",\n        },\n        \"visuals.enable_footprints\": {\n            \"title\": \"Footprints\",\n            \"description\": \"Enables rendering of Lara's footprints on certain surfaces in supported levels.\",\n        },\n        \"visuals.enable_glide_cameras\": {\n            \"title\": \"Glide cameras\",\n            \"description\": \"Enables a glide action on fixed cameras that look at Lara by adopting a smooth speed curve. If disabled, such cameras will change the view to look at Lara immediately.\",\n        },\n        \"visuals.enable_gun_lighting\": {\n            \"title\": \"Gun lighting\",\n            \"description\": \"Enables dynamic lighting to be generated for gunshots and explosions.\",\n        },\n        \"visuals.enable_ps1_crystals\": {\n            \"title\": \"PS1 crystal tint\",\n            \"description\": \"Save crystals will be drawn with a purple tint, more similar to the PS1 type.\",\n        },\n        \"visuals.enable_reflections\": {\n            \"title\": \"Reflections\",\n            \"description\": \"Enables reflections on certain objects.\",\n        },\n        \"visuals.enable_responsive_mesh_tint\": {\n            \"title\": \"Responsive mesh tint\",\n            \"description\": \"Enables Lara's individual meshes to be drawn with a water tint if they are themselves located underwater (TR3 style). Otherwise, if Lara is in water, each of her meshes will be drawn with the tint (TR1/2 style).\",\n        },\n        \"visuals.enable_shotgun_flash\": {\n            \"title\": \"Shotgun flash\",\n            \"description\": \"Draws flames when firing the shotgun, like for other guns.\",\n        },\n        \"visuals.enable_skybox\": {\n            \"title\": \"Skyboxes\",\n            \"description\": \"Enables the skybox in supported levels.\",\n        },\n        \"visuals.enable_weather\": {\n            \"title\": \"Weather\",\n            \"description\": \"Enables rendering of weather effects in supported levels.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"title\": \"Fix sprite animations\",\n            \"description\": \"Fixes original underwater plant sprites so they animate properly in water areas.\",\n        },\n        \"visuals.fix_item_rots\": {\n            \"title\": \"Fix item rotation issues\",\n            \"description\": \"Fixes original issues with some incorrectly rotated pickups when using the 3D pickups option.\",\n        },\n        \"visuals.fix_texture_issues\": {\n            \"title\": \"Fix texture issues\",\n            \"description\": \"Fixes original issues with missing or incorrect textures/meshes.\",\n        },\n        \"visuals.fog_color\": {\n            \"title\": \"Fog color\",\n            \"description\": \"Color of the fog.\",\n        },\n        \"visuals.fog_end\": {\n            \"title\": \"Fog end\",\n            \"description\": \"Sets distance in tiles where fog makes everything fully obscured.\",\n        },\n        \"visuals.fog_start\": {\n            \"title\": \"Fog start\",\n            \"description\": \"Sets distance in tiles where fog begins to appear.\",\n        },\n        \"visuals.fog_transparency\": {\n            \"title\": \"Fog transparency\",\n            \"description\": \"Whether to enable blending distant geometry into 100% transparent faces.\",\n        },\n        \"visuals.fov\": {\n            \"title\": \"Field of view\",\n            \"description\": \"Viewing angle in degrees. Larger values widen the field of view, smaller ones narrow it.\",\n        },\n        \"visuals.game_brightness\": {\n            \"title\": \"Game brightness\",\n            \"description\": \"Changes game brightness.\",\n        },\n        \"visuals.gamma\": {\n            \"title\": \"Gamma\",\n            \"description\": \"Adjusts the gamma curve. Higher values mean brighter lighting. The value of 2.5 means default colors.\",\n        },\n        \"visuals.lara_outfit\": {\n            \"title\": \"Lara's outfit\",\n            \"description\": \"Changes Lara's appearance. Choosing Default will respect any regular outfit changes between levels, otherwise the chosen value will persist until changed manually.\",\n        },\n        \"visuals.shadow_type\": {\n            \"title\": \"Shadows shape\",\n            \"description\": \"Selects how entity shadows are rendered.\\n\\n- Octagon: old TR1 and TR2 shadows\\n- Circle: round shadows\\n- Sprite: TR3 texture-based shadows\",\n        },\n        \"visuals.sunglasses_mode\": {\n            \"title\": \"Lara's sunglasses\",\n            \"description\": \"Changes the style of Lara's sunglasses. Note that lenses will be reflective if the relevant option is enabled.\\n\\n- Off: Lara will not wear sunglasses.\\n- Opaque: Lara's sunglasses will have opaque lenses.\\n- Transparent: Lara's sunglasses will have semi-transparent lenses.\",\n        },\n        \"visuals.ui_brightness\": {\n            \"title\": \"UI brightness\",\n            \"description\": \"Changes UI brightness.\",\n        },\n        \"visuals.water_color\": {\n            \"title\": \"Water color\",\n            \"description\": \"Color of the water.\",\n        }\n    },\n    \"objects\": {\n        \"alarm_sound\": {\n            \"name\": \"Alarm\",\n        },\n        \"alligator\": {\n            \"name\": \"Alligator\",\n        },\n        \"alphabet\": {\n            \"name\": \"Default font\",\n        },\n        \"alphabet_small\": {\n            \"name\": \"Small font\",\n        },\n        \"amber_light\": {\n            \"name\": \"Amber Light\",\n        },\n        \"animating_1\": {\n            \"name\": \"Animating Object 1\",\n        },\n        \"animating_10\": {\n            \"name\": \"Animating Object 10\",\n        },\n        \"animating_2\": {\n            \"name\": \"Animating Object 2\",\n        },\n        \"animating_3\": {\n            \"name\": \"Animating Object 3\",\n        },\n        \"animating_4\": {\n            \"name\": \"Animating Object 4\",\n        },\n        \"animating_5\": {\n            \"name\": \"Animating Object 5\",\n        },\n        \"animating_6\": {\n            \"name\": \"Animating Object 6\",\n        },\n        \"animating_7\": {\n            \"name\": \"Animating Object 7\",\n        },\n        \"animating_8\": {\n            \"name\": \"Animating Object 8\",\n        },\n        \"animating_9\": {\n            \"name\": \"Animating Object 9\",\n        },\n        \"ape\": {\n            \"name\": \"Ape\",\n        },\n        \"area_51_rocket\": {\n            \"name\": \"Area 51 Rocket\",\n        },\n        \"area_51_rocket_blast\": {\n            \"name\": \"Area 51 Rocket Blast\",\n        },\n        \"area_51_rocket_support\": {\n            \"name\": \"Area 51 Rocket Support\",\n        },\n        \"assault_digits\": {\n            \"name\": \"Assault Digits\",\n        },\n        \"assault_target\": {\n            \"name\": \"Assault Target\",\n        },\n        \"atlantean_ground\": {\n            \"name\": \"Ground Atlantean\",\n        },\n        \"atlantean_shooter\": {\n            \"name\": \"Shooting Atlantean\",\n        },\n        \"atlantean_winged\": {\n            \"name\": \"Winged Atlantean\",\n        },\n        \"autos\": {\n            \"name\": \"Automatic Pistols\",\n        },\n        \"autos_ammo\": {\n            \"name\": \"Automatic Pistol Clips\",\n        },\n        \"bacon_lara\": {\n            \"name\": \"Bacon Lara\",\n        },\n        \"baldy\": {\n            \"name\": \"Baldy\",\n        },\n        \"bandit_1\": {\n            \"name\": [\n                \"Mercenary 1\",\n                \"Masked Goon 1\",\n            ]\n        },\n        \"bandit_2\": {\n            \"name\": [\n                \"Mercenary 2\",\n                \"Masked Goon 2\",\n            ]\n        },\n        \"bandit_2b\": {\n            \"name\": [\n                \"Mercenary 3\",\n                \"Masked Goon 3\",\n            ]\n        },\n        \"barracuda\": {\n            \"name\": \"Barracuda\",\n        },\n        \"bartoli\": {\n            \"name\": \"Marco Bartoli\",\n        },\n        \"bat\": {\n            \"name\": \"Bat\",\n        },\n        \"bat_emitter\": {\n            \"name\": \"Bat Emitter\",\n        },\n        \"beacon_light\": {\n            \"name\": \"Beacon Light\",\n        },\n        \"bear\": {\n            \"name\": \"Bear\",\n        },\n        \"bell\": {\n            \"name\": \"Bell\",\n        },\n        \"big_bowl\": {\n            \"name\": \"Lava Bowl\",\n        },\n        \"big_eel\": {\n            \"name\": \"Big Eel\",\n        },\n        \"big_pod\": {\n            \"name\": \"Big Pod\",\n        },\n        \"big_spider\": {\n            \"name\": \"Giant Spider\",\n        },\n        \"bird_guardian\": {\n            \"name\": \"Bird Monster\",\n        },\n        \"bird_tweeter_1\": {\n            \"name\": \"Dripping Water\",\n        },\n        \"bird_tweeter_2\": {\n            \"name\": \"Singing Birds\",\n        },\n        \"blade\": {\n            \"name\": \"Wall-mounted Blade\",\n        },\n        \"blood\": {\n            \"name\": \"Blood\",\n        },\n        \"blood_pink\": {\n            \"name\": \"Blood (censored)\",\n        },\n        \"blue_light\": {\n            \"name\": \"Blue Light\",\n        },\n        \"boat\": {\n            \"name\": \"Boat\",\n        },\n        \"boat_bits\": {\n            \"name\": \"Boat Bits\",\n        },\n        \"body_part\": {\n            \"name\": \"Body Part\",\n        },\n        \"bridge_flat\": {\n            \"name\": \"Bridge Flat\",\n        },\n        \"bridge_tilt_1\": {\n            \"name\": \"Bridge Tilt 1\",\n        },\n        \"bridge_tilt_2\": {\n            \"name\": \"Bridge Tilt 2\",\n        },\n        \"bubble_1\": {\n            \"name\": \"Bubble 1\",\n        },\n        \"bubble_2\": {\n            \"name\": \"Bubble 2\",\n        },\n        \"bubble_emitter\": {\n            \"name\": \"Bubble Emitter\",\n        },\n        \"camera_target\": {\n            \"name\": \"Camera Target\",\n        },\n        \"carcass\": {\n            \"name\": \"Carcass\",\n        },\n        \"ceiling_spikes\": {\n            \"name\": \"Spiky Ceiling\",\n        },\n        \"centaur\": {\n            \"name\": \"Centaur\",\n        },\n        \"centaur_statue\": {\n            \"name\": \"Centaur Statue\",\n        },\n        \"civilian\": {\n            \"name\": \"Civilian\",\n        },\n        \"claw_mutant\": {\n            \"name\": \"Claw Mutant\",\n        },\n        \"clock_chimes\": {\n            \"name\": \"Bartoli Hideout clock\",\n        },\n        \"cog_1\": {\n            \"name\": \"Cog 1\",\n        },\n        \"cog_2\": {\n            \"name\": \"Cog 2\",\n        },\n        \"cog_3\": {\n            \"name\": \"Cog 3\",\n        },\n        \"combat_end\": {\n            \"name\": \"Combat End\",\n        },\n        \"compass\": {\n            \"name\": \"Compass\",\n        },\n        \"compy\": {\n            \"name\": \"Compsognathus\",\n        },\n        \"controls\": {\n            \"name\": \"Controls\",\n        },\n        \"copter\": {\n            \"name\": \"Helicopter\",\n        },\n        \"cowboy\": {\n            \"name\": \"Cowboy\",\n        },\n        \"crawler_mutant\": {\n            \"name\": \"Crawler Mutant\",\n        },\n        \"crocodile\": {\n            \"name\": \"Crocodile\",\n        },\n        \"crow\": {\n            \"name\": \"Crow\",\n        },\n        \"cult_1\": {\n            \"name\": \"Masked Goon 1\",\n        },\n        \"cult_1a\": {\n            \"name\": \"Masked Goon 2\",\n        },\n        \"cult_1b\": {\n            \"name\": \"Masked Goon 3\",\n        },\n        \"cult_2\": {\n            \"name\": \"Knife Thrower\",\n        },\n        \"cult_3\": {\n            \"name\": \"Shotgun Goon\",\n        },\n        \"cut_shotgun\": {\n            \"name\": \"Shotgun Shower Animation\",\n        },\n        \"damocles_sword\": {\n            \"name\": \"Damocles Sword\",\n        },\n        \"dart\": {\n            \"name\": \"Dart\",\n        },\n        \"dart_effect\": {\n            \"name\": \"Dart Effect\",\n        },\n        \"dart_emitter\": {\n            \"name\": \"Dart Emitter\",\n        },\n        \"desert_eagle\": {\n            \"name\": \"Desert Eagle\",\n        },\n        \"desert_eagle_ammo\": {\n            \"name\": \"Desert Eagle Clips\",\n        },\n        \"detonator_box\": {\n            \"name\": \"Detonator Box\",\n        },\n        \"ding_dong\": {\n            \"name\": \"Doorbell\",\n        },\n        \"dino_mutant\": {\n            \"name\": \"Dino Mutant\",\n        },\n        \"disc\": {\n            \"name\": \"Disc\",\n        },\n        \"disc_emitter\": {\n            \"name\": \"Disc Emitter\",\n        },\n        \"disposable_animating_1\": {\n            \"name\": \"Disposable Animating Object 1\",\n        },\n        \"disposable_animating_10\": {\n            \"name\": \"Disposable Animating Object 10\",\n        },\n        \"disposable_animating_2\": {\n            \"name\": \"Disposable Animating Object 2\",\n        },\n        \"disposable_animating_3\": {\n            \"name\": \"Disposable Animating Object 3\",\n        },\n        \"disposable_animating_4\": {\n            \"name\": \"Disposable Animating Object 4\",\n        },\n        \"disposable_animating_5\": {\n            \"name\": \"Disposable Animating Object 5\",\n        },\n        \"disposable_animating_6\": {\n            \"name\": \"Disposable Animating Object 6\",\n        },\n        \"disposable_animating_7\": {\n            \"name\": \"Disposable Animating Object 7\",\n        },\n        \"disposable_animating_8\": {\n            \"name\": \"Disposable Animating Object 8\",\n        },\n        \"disposable_animating_9\": {\n            \"name\": \"Disposable Animating Object 9\",\n        },\n        \"diver\": {\n            \"name\": \"Scuba Diver\",\n        },\n        \"dog\": {\n            \"name\": [\n                \"Dog\",\n                \"Doberman\",\n            ]\n        },\n        \"door_1\": {\n            \"name\": \"Door 1\",\n        },\n        \"door_2\": {\n            \"name\": \"Door 2\",\n        },\n        \"door_3\": {\n            \"name\": \"Door 3\",\n        },\n        \"door_4\": {\n            \"name\": \"Door 4\",\n        },\n        \"door_5\": {\n            \"name\": \"Door 5\",\n        },\n        \"door_6\": {\n            \"name\": \"Door 6\",\n        },\n        \"door_7\": {\n            \"name\": \"Door 7\",\n        },\n        \"door_8\": {\n            \"name\": \"Door 8\",\n        },\n        \"dragon_back\": {\n            \"name\": \"Dragon Back\",\n        },\n        \"dragon_bones_1\": {\n            \"name\": \"Placeholder\",\n        },\n        \"dragon_bones_2\": {\n            \"name\": \"Dragon Bones Front\",\n        },\n        \"dragon_bones_3\": {\n            \"name\": \"Dragon Bones Back\",\n        },\n        \"dragon_front\": {\n            \"name\": \"Dragon Front\",\n        },\n        \"drawbridge\": {\n            \"name\": \"Drawbridge\",\n        },\n        \"dust\": {\n            \"name\": \"Dust\",\n        },\n        \"dying_monk\": {\n            \"name\": \"Dying monk\",\n        },\n        \"dying_mutant\": {\n            \"name\": \"Dying Mutant\",\n        },\n        \"eagle\": {\n            \"name\": \"Eagle\",\n        },\n        \"earthquake\": {\n            \"name\": \"Earthquake\",\n        },\n        \"eel\": {\n            \"name\": \"Eel\",\n        },\n        \"electric_cleaner\": {\n            \"name\": \"Electric Cleaner\",\n        },\n        \"electric_fence\": {\n            \"name\": \"Electric Fence\",\n        },\n        \"electrical_light\": {\n            \"name\": \"Electrical Light\",\n        },\n        \"ember\": {\n            \"name\": \"Ember\",\n        },\n        \"ember_emitter\": {\n            \"name\": \"Ember Emitter\",\n        },\n        \"explosion_1\": {\n            \"name\": \"Explosion 1\",\n        },\n        \"explosion_2\": {\n            \"name\": \"Explosion 2\",\n        },\n        \"falling_block_1\": {\n            \"name\": [\n                \"Falling Block 1\",\n                \"Collapsible Floor 1\",\n                \"Collapsible Tiles 1\",\n            ]\n        },\n        \"falling_block_2\": {\n            \"name\": [\n                \"Falling Block 2\",\n                \"Collapsible Floor 2\",\n                \"Collapsible Tiles 2\",\n            ]\n        },\n        \"falling_block_3\": {\n            \"name\": [\n                \"Falling Block 3\",\n                \"Collapsible Floor 3\",\n                \"Collapsible Tiles 3\",\n            ]\n        },\n        \"falling_ceiling_1\": {\n            \"name\": \"Falling Ceiling 1\",\n        },\n        \"falling_ceiling_2\": {\n            \"name\": \"Falling Ceiling 2\",\n        },\n        \"fire_head\": {\n            \"name\": \"Fire Head\",\n        },\n        \"fish_mutant\": {\n            \"name\": \"Mutant Fish\",\n        },\n        \"flame\": {\n            \"name\": [\n                \"Flame\",\n                \"Fire\",\n            ]\n        },\n        \"flame_emitter\": {\n            \"name\": [\n                \"Flame Emitter\",\n                \"Fire Emitter\",\n            ]\n        },\n        \"flame_emitter_big\": {\n            \"name\": [\n                \"Flame Emitter (Big)\",\n                \"Fire Emitter (Big)\",\n            ]\n        },\n        \"flame_emitter_jet\": {\n            \"name\": [\n                \"Flame Emitter (Jet)\",\n                \"Fire Emitter (Jet)\",\n            ]\n        },\n        \"flame_emitter_side\": {\n            \"name\": [\n                \"Flame Emitter (Side)\",\n                \"Fire Emitter (Side)\",\n            ]\n        },\n        \"flame_emitter_small\": {\n            \"name\": [\n                \"Flame Emitter (Small)\",\n                \"Fire Emitter (Small)\",\n            ]\n        },\n        \"flare\": {\n            \"name\": \"Flare\",\n        },\n        \"flare_fire\": {\n            \"name\": \"Flare sparks\",\n        },\n        \"flares_box\": {\n            \"name\": \"Flares Box\",\n        },\n        \"flickering_light\": {\n            \"name\": \"Flickering Light\",\n        },\n        \"fuse_box\": {\n            \"name\": \"Fuse Box\",\n        },\n        \"fx_reserved\": {\n            \"name\": \"Gray disk\",\n        },\n        \"gamma\": {\n            \"name\": \"Gamma\",\n        },\n        \"gas_emitter_green\": {\n            \"name\": \"Gas Emitter (Green)\",\n        },\n        \"general\": {\n            \"name\": \"Minisub\",\n        },\n        \"globe\": {\n            \"name\": \"Globe\",\n        },\n        \"glow\": {\n            \"name\": \"Glow\",\n        },\n        \"glow_reserved\": {\n            \"name\": \"Map Glow\",\n        },\n        \"gondola\": {\n            \"name\": \"Gondola\",\n        },\n        \"gong\": {\n            \"name\": \"Gong\",\n        },\n        \"gong_bonger\": {\n            \"name\": \"Gong Stick\",\n        },\n        \"graphics\": {\n            \"name\": \"Graphics\",\n        },\n        \"green_light\": {\n            \"name\": \"Green Light\",\n        },\n        \"grenade\": {\n            \"name\": \"Grenade\",\n        },\n        \"grenade_launcher\": {\n            \"name\": \"Grenade Launcher\",\n        },\n        \"grenade_launcher_ammo\": {\n            \"name\": \"Grenades\",\n        },\n        \"gun_flash\": {\n            \"name\": \"Gun Flash\",\n        },\n        \"gun_shell\": {\n            \"name\": \"Gun Shell\",\n        },\n        \"harpoon_bolt\": {\n            \"name\": \"Harpoon Bolt\",\n        },\n        \"harpoon_gun\": {\n            \"name\": \"Harpoon Gun\",\n        },\n        \"harpoon_gun_ammo\": {\n            \"name\": \"Harpoons\",\n        },\n        \"hook\": {\n            \"name\": \"Hook\",\n        },\n        \"hot_liquid\": {\n            \"name\": \"Extra Fire\",\n        },\n        \"huskie\": {\n            \"name\": [\n                \"Dog\",\n                \"Patrol Dog\",\n                \"Huskie\",\n            ]\n        },\n        \"hybrid_mutant\": {\n            \"name\": \"Hybrid Mutant\",\n        },\n        \"icicle\": {\n            \"name\": \"Icicles\",\n        },\n        \"inv_background\": {\n            \"name\": \"Menu Background\",\n        },\n        \"jelly\": {\n            \"name\": \"Jellyfish\",\n        },\n        \"kayak\": {\n            \"name\": \"Kayak\",\n        },\n        \"key_1\": {\n            \"name\": \"Key 1\",\n        },\n        \"key_2\": {\n            \"name\": \"Key 2\",\n        },\n        \"key_3\": {\n            \"name\": \"Key 3\",\n        },\n        \"key_4\": {\n            \"name\": \"Key 4\",\n        },\n        \"key_hole_1\": {\n            \"name\": \"Keyhole 1\",\n        },\n        \"key_hole_2\": {\n            \"name\": \"Keyhole 2\",\n        },\n        \"key_hole_3\": {\n            \"name\": \"Keyhole 3\",\n        },\n        \"key_hole_4\": {\n            \"name\": \"Keyhole 4\",\n        },\n        \"kill_all_triggered\": {\n            \"name\": \"Kill All Triggered\",\n        },\n        \"killer_statue\": {\n            \"name\": \"Statue with Sword\",\n        },\n        \"lara\": {\n            \"name\": \"Lara\",\n        },\n        \"lara_alarm\": {\n            \"name\": \"Alarm Bell\",\n        },\n        \"lara_autos\": {\n            \"name\": \"Automatic Pistols Animation\",\n        },\n        \"lara_boat\": {\n            \"name\": \"Boat Animation\",\n        },\n        \"lara_desert_eagle\": {\n            \"name\": \"Desert Eagle Animation\",\n        },\n        \"lara_extra\": {\n            \"name\": \"Lara's Extra Animation\",\n        },\n        \"lara_flare\": {\n            \"name\": \"Flare Animation\",\n        },\n        \"lara_grenade\": {\n            \"name\": \"Grenade Launcher Animation\",\n        },\n        \"lara_hair\": {\n            \"name\": \"Lara's Braid\",\n        },\n        \"lara_harpoon\": {\n            \"name\": \"Harpoon Animation\",\n        },\n        \"lara_m16\": {\n            \"name\": \"M16 Animation\",\n        },\n        \"lara_magnums\": {\n            \"name\": \"Magnums Animation\",\n        },\n        \"lara_mp5\": {\n            \"name\": \"MP5 Animation\",\n        },\n        \"lara_pistols\": {\n            \"name\": \"Pistols Animation\",\n        },\n        \"lara_rocket\": {\n            \"name\": \"Rocket Launcher Animation\",\n        },\n        \"lara_shotgun\": {\n            \"name\": \"Shotgun Animation\",\n        },\n        \"lara_skidoo\": {\n            \"name\": \"Snowmobile Animation\",\n        },\n        \"lara_uzis\": {\n            \"name\": \"Uzis Animation\",\n        },\n        \"large_medipack\": {\n            \"name\": \"Large Medipack\",\n        },\n        \"larson\": {\n            \"name\": \"Larson\",\n        },\n        \"lava_wedge\": {\n            \"name\": \"Lava Wedge\",\n        },\n        \"lead_bar\": {\n            \"name\": \"Lead Bar\",\n        },\n        \"lift\": {\n            \"name\": \"Lift\",\n        },\n        \"lightning_emitter\": {\n            \"name\": \"Lightning Emitter\",\n        },\n        \"lion\": {\n            \"name\": \"Lion\",\n        },\n        \"lioness\": {\n            \"name\": [\n                \"Lioness\",\n                \"Lion\",\n            ]\n        },\n        \"lizard\": {\n            \"name\": \"Lizard\",\n        },\n        \"m16\": {\n            \"name\": \"M16\",\n        },\n        \"m16_ammo\": {\n            \"name\": \"M16 Clips\",\n        },\n        \"m16_flash\": {\n            \"name\": \"M16 Flash\",\n        },\n        \"magnums\": {\n            \"name\": \"Magnums\",\n        },\n        \"magnums_ammo\": {\n            \"name\": \"Magnum Clips\",\n        },\n        \"mesh_swap_1\": {\n            \"name\": \"Mesh Swap 1\",\n        },\n        \"mesh_swap_2\": {\n            \"name\": \"Mesh Swap 2\",\n        },\n        \"mesh_swap_3\": {\n            \"name\": \"Mesh Swap 3\",\n        },\n        \"midas_touch\": {\n            \"name\": \"Midas Hand\",\n        },\n        \"mine\": {\n            \"name\": \"Aquatic Mine\",\n        },\n        \"mine_cart\": {\n            \"name\": \"Mine Cart\",\n        },\n        \"mini_copter\": {\n            \"name\": \"Helicopter 2\",\n        },\n        \"missile_atlantean_bomb\": {\n            \"name\": \"Missile (Atlantean Bomb)\",\n        },\n        \"missile_atlantean_shard\": {\n            \"name\": \"Missile (Atlantean Shard)\",\n        },\n        \"missile_flame\": {\n            \"name\": \"Missile (Flame)\",\n        },\n        \"missile_harpoon\": {\n            \"name\": \"Missile (Harpoon)\",\n        },\n        \"missile_knife\": {\n            \"name\": \"Missile (Knife)\",\n        },\n        \"missile_poison\": {\n            \"name\": \"Missile (Poison)\",\n        },\n        \"monk_1\": {\n            \"name\": \"Monk 1\",\n        },\n        \"monk_2\": {\n            \"name\": \"Monk 2\",\n        },\n        \"monkey\": {\n            \"name\": \"Monkey\",\n        },\n        \"mounted_gun\": {\n            \"name\": \"Mounted Gun\",\n        },\n        \"mouse\": {\n            \"name\": \"Rat\",\n        },\n        \"movable_block_1\": {\n            \"name\": [\n                \"Push Block 1\",\n                \"Movable Block 1\",\n            ]\n        },\n        \"movable_block_2\": {\n            \"name\": [\n                \"Push Block 2\",\n                \"Movable Block 2\",\n            ]\n        },\n        \"movable_block_3\": {\n            \"name\": [\n                \"Push Block 3\",\n                \"Movable Block 3\",\n            ]\n        },\n        \"movable_block_4\": {\n            \"name\": [\n                \"Push Block 4\",\n                \"Movable Block 4\",\n            ]\n        },\n        \"moving_bar\": {\n            \"name\": \"Moving Bar\",\n        },\n        \"mp5\": {\n            \"name\": \"MP5\",\n        },\n        \"mp5_ammo\": {\n            \"name\": \"MP5 Clips\",\n        },\n        \"mp_1\": {\n            \"name\": \"MP 1\",\n        },\n        \"mp_2\": {\n            \"name\": \"MP 2\",\n        },\n        \"mummy\": {\n            \"name\": \"Mummy\",\n        },\n        \"natla\": {\n            \"name\": \"Natla\",\n        },\n        \"natla_gun\": {\n            \"name\": \"Natla's Gun\",\n        },\n        \"on_off_light\": {\n            \"name\": \"On/Off Light\",\n        },\n        \"orca\": {\n            \"name\": \"Orca\",\n        },\n        \"passport\": {\n            \"name\": \"Game\",\n        },\n        \"patrol_dog\": {\n            \"name\": [\n                \"Dog\",\n                \"Patrol Dog\",\n            ]\n        },\n        \"pda\": {\n            \"name\": \"Gameplay\",\n        },\n        \"pendulum_1\": {\n            \"name\": [\n                \"Pendulum\",\n                \"Sandbag\",\n                \"Swinging box\",\n            ]\n        },\n        \"pendulum_2\": {\n            \"name\": [\n                \"Pendulum\",\n                \"Sandbag\",\n                \"Swinging box\",\n            ]\n        },\n        \"photo\": {\n            \"name\": \"Lara's Home\",\n        },\n        \"pickup_1\": {\n            \"name\": \"Pickup Item 1\",\n        },\n        \"pickup_2\": {\n            \"name\": \"Pickup Item 2\",\n        },\n        \"pickup_aid\": {\n            \"name\": \"Pickup Aid\",\n        },\n        \"pierre\": {\n            \"name\": \"Pierre\",\n        },\n        \"pirahnas\": {\n            \"name\": \"Pirahnas\",\n        },\n        \"pistols\": {\n            \"name\": \"Pistols\",\n        },\n        \"pistols_ammo\": {\n            \"name\": \"Pistol Clips\",\n        },\n        \"player_1\": {\n            \"name\": \"Cutscene Actor 1\",\n        },\n        \"player_10\": {\n            \"name\": \"Cutscene Actor 10\",\n        },\n        \"player_2\": {\n            \"name\": \"Cutscene Actor 2\",\n        },\n        \"player_3\": {\n            \"name\": \"Cutscene Actor 3\",\n        },\n        \"player_4\": {\n            \"name\": \"Cutscene Actor 4\",\n        },\n        \"player_5\": {\n            \"name\": \"Cutscene Actor 5\",\n        },\n        \"player_6\": {\n            \"name\": \"Cutscene Actor 6\",\n        },\n        \"player_7\": {\n            \"name\": \"Cutscene Actor 7\",\n        },\n        \"player_8\": {\n            \"name\": \"Cutscene Actor 8\",\n        },\n        \"player_9\": {\n            \"name\": \"Cutscene Actor 9\",\n        },\n        \"pods\": {\n            \"name\": \"Pod\",\n        },\n        \"poison_dart\": {\n            \"name\": \"Poison Dart\",\n        },\n        \"poison_dart_emitter\": {\n            \"name\": \"Poison Dart Emitter\",\n        },\n        \"portacabin\": {\n            \"name\": \"Portable Cabin\",\n        },\n        \"power_saw\": {\n            \"name\": \"Power Saw\",\n        },\n        \"prisoner\": {\n            \"name\": \"Prisoner\",\n        },\n        \"propeller_1\": {\n            \"name\": \"Airplane Propeller\",\n        },\n        \"propeller_2\": {\n            \"name\": \"Underwater Propeller\",\n        },\n        \"propeller_3\": {\n            \"name\": \"Air Fan\",\n        },\n        \"pulse_light\": {\n            \"name\": \"Pulse Light\",\n        },\n        \"puma\": {\n            \"name\": \"Puma\",\n        },\n        \"punk_1\": {\n            \"name\": \"Punk 1\",\n        },\n        \"punk_2\": {\n            \"name\": \"Punk 2\",\n        },\n        \"puzzle_1\": {\n            \"name\": \"Puzzle Item 1\",\n        },\n        \"puzzle_2\": {\n            \"name\": \"Puzzle Item 2\",\n        },\n        \"puzzle_3\": {\n            \"name\": \"Puzzle Item 3\",\n        },\n        \"puzzle_4\": {\n            \"name\": \"Puzzle Item 4\",\n        },\n        \"puzzle_done_1\": {\n            \"name\": \"Puzzle Hole 1 (Done)\",\n        },\n        \"puzzle_done_2\": {\n            \"name\": \"Puzzle Hole 2 (Done)\",\n        },\n        \"puzzle_done_3\": {\n            \"name\": \"Puzzle Hole 3 (Done)\",\n        },\n        \"puzzle_done_4\": {\n            \"name\": \"Puzzle Hole 4 (Done)\",\n        },\n        \"puzzle_hole_1\": {\n            \"name\": \"Puzzle Hole 1 (Empty)\",\n        },\n        \"puzzle_hole_2\": {\n            \"name\": \"Puzzle Hole 2 (Empty)\",\n        },\n        \"puzzle_hole_3\": {\n            \"name\": \"Puzzle Hole 3 (Empty)\",\n        },\n        \"puzzle_hole_4\": {\n            \"name\": \"Puzzle Hole 4 (Empty)\",\n        },\n        \"quad_bike\": {\n            \"name\": \"Quad Bike\",\n        },\n        \"quest_1\": {\n            \"name\": \"Quest Item 1\",\n        },\n        \"quest_2\": {\n            \"name\": \"Quest Item 2\",\n        },\n        \"quest_3\": {\n            \"name\": \"Quest Item 3\",\n        },\n        \"quest_4\": {\n            \"name\": \"Quest Item 4\",\n        },\n        \"raptor\": {\n            \"name\": \"Raptor\",\n        },\n        \"raptor_emitter\": {\n            \"name\": \"Raptor Emitter\",\n        },\n        \"rat\": {\n            \"name\": [\n                \"Rat\",\n                \"Land Rat\",\n            ]\n        },\n        \"red_light\": {\n            \"name\": \"Red Light\",\n        },\n        \"rib\": {\n            \"name\": [\n                \"Rigid Inflatable Boat\",\n                \"RIB\",\n            ]\n        },\n        \"ricochet\": {\n            \"name\": \"Ricochet\",\n        },\n        \"rocket\": {\n            \"name\": \"Rocket\",\n        },\n        \"rocket_launcher\": {\n            \"name\": \"Rocket Launcher\",\n        },\n        \"rocket_launcher_ammo\": {\n            \"name\": \"Rockets\",\n        },\n        \"rolling_ball_1\": {\n            \"name\": [\n                \"Boulder 1\",\n                \"Rolling Ball 1\",\n            ]\n        },\n        \"rolling_ball_2\": {\n            \"name\": [\n                \"Boulder 2\",\n                \"Rolling Ball 2\",\n            ]\n        },\n        \"rolling_ball_3\": {\n            \"name\": [\n                \"Boulder 3\",\n                \"Rolling Ball 3\",\n            ]\n        },\n        \"rolling_ball_4\": {\n            \"name\": [\n                \"Boulder 4\",\n                \"Rolling Ball 4\",\n            ]\n        },\n        \"rotating_laser\": {\n            \"name\": \"Rotating Laser\",\n        },\n        \"rx_worker_1\": {\n            \"name\": \"RX Worker 1\",\n        },\n        \"rx_worker_2\": {\n            \"name\": \"RX Worker 2\",\n        },\n        \"rx_worker_3\": {\n            \"name\": [\n                \"RX Worker 3\",\n                \"Flamethrower\",\n            ]\n        },\n        \"save_crystal\": {\n            \"name\": \"Savegame Crystal\",\n        },\n        \"scion\": {\n            \"name\": \"Scion\",\n        },\n        \"scion_holder\": {\n            \"name\": \"Scion Holder\",\n        },\n        \"secret_1\": {\n            \"name\": \"Secret 1\",\n        },\n        \"secret_2\": {\n            \"name\": \"Secret 2\",\n        },\n        \"secret_3\": {\n            \"name\": \"Secret 3\",\n        },\n        \"security_guard\": {\n            \"name\": \"Security Guard\",\n        },\n        \"security_laser_alarm\": {\n            \"name\": \"Security Laser (Alarm)\",\n        },\n        \"security_laser_deadly\": {\n            \"name\": \"Security Laser (Deadly)\",\n        },\n        \"security_laser_killer\": {\n            \"name\": \"Security Laser (Killer)\",\n        },\n        \"sentry_gun\": {\n            \"name\": \"Sentry Gun\",\n        },\n        \"shadow\": {\n            \"name\": \"Shadow\",\n        },\n        \"shark\": {\n            \"name\": \"Shark\",\n        },\n        \"shiva\": {\n            \"name\": \"Shiva\",\n        },\n        \"shotgun\": {\n            \"name\": \"Shotgun\",\n        },\n        \"shotgun_ammo\": {\n            \"name\": \"Shotgun Shells\",\n        },\n        \"shotgun_shell\": {\n            \"name\": \"Shotgun Shell\",\n        },\n        \"skate_kid\": {\n            \"name\": \"Skate Kid\",\n        },\n        \"skateboard\": {\n            \"name\": \"Skateboard\",\n        },\n        \"skidoo_armed\": {\n            \"name\": \"Black Snowmobile\",\n        },\n        \"skidoo_driver\": {\n            \"name\": \"Black Snowmobile Driver\",\n        },\n        \"skidoo_fast\": {\n            \"name\": \"Red Snowmobile\",\n        },\n        \"skidoo_track\": {\n            \"name\": \"Snowmobile Track\",\n        },\n        \"skybox\": {\n            \"name\": \"Skybox\",\n        },\n        \"sliding_pillar\": {\n            \"name\": \"Sliding Pillar\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Small Medipack\",\n        },\n        \"smashable_1\": {\n            \"name\": [\n                \"Smashable 1\",\n                \"Breakable Window 1\",\n            ]\n        },\n        \"smashable_2\": {\n            \"name\": [\n                \"Smashable 2\",\n                \"Breakable Window 2\",\n            ]\n        },\n        \"smashable_3\": {\n            \"name\": [\n                \"Smashable 3\",\n                \"Breakable Window 3\",\n            ]\n        },\n        \"smashable_4\": {\n            \"name\": [\n                \"Smashable 4\",\n                \"Breakable Window 4\",\n            ]\n        },\n        \"smoke_emitter_black\": {\n            \"name\": \"Smoke Emitter (Black)\",\n        },\n        \"smoke_emitter_white\": {\n            \"name\": \"Smoke Emitter (White)\",\n        },\n        \"snake\": {\n            \"name\": [\n                \"Snake\",\n                \"Cobra\",\n            ]\n        },\n        \"snow_sprite\": {\n            \"name\": \"Snowmobile Wake\",\n        },\n        \"sophia\": {\n            \"name\": \"Sophia\",\n        },\n        \"sound\": {\n            \"name\": \"Sound\",\n        },\n        \"sphere_of_doom_1\": {\n            \"name\": \"Dragon Explosion 1\",\n        },\n        \"sphere_of_doom_2\": {\n            \"name\": \"Dragon Explosion 2\",\n        },\n        \"sphere_of_doom_3\": {\n            \"name\": \"Dragon Explosion 3\",\n        },\n        \"spider\": {\n            \"name\": \"Spider\",\n        },\n        \"spike_wall\": {\n            \"name\": \"Spike Wall\",\n        },\n        \"spikes\": {\n            \"name\": \"Spikes\",\n        },\n        \"spinning_blade\": {\n            \"name\": \"Spinning Blade\",\n        },\n        \"splash_1\": {\n            \"name\": \"Water Ripples 1\",\n        },\n        \"splash_2\": {\n            \"name\": \"Water Ripples 2\",\n        },\n        \"springboard\": {\n            \"name\": \"Springboard\",\n        },\n        \"steam_emitter\": {\n            \"name\": \"Steam Emitter\",\n        },\n        \"sthpac_mercenary\": {\n            \"name\": \"South Pacific Mercenary\",\n        },\n        \"stopwatch\": {\n            \"name\": \"Statistics\",\n        },\n        \"strobe_light\": {\n            \"name\": \"Strobe Light\",\n        },\n        \"swat_1\": {\n            \"name\": \"SWAT 1\",\n        },\n        \"swat_2\": {\n            \"name\": \"SWAT 2\",\n        },\n        \"swat_3\": {\n            \"name\": \"SWAT 3\",\n        },\n        \"swinging_axe\": {\n            \"name\": \"Swinging Axe\",\n        },\n        \"switch_type_airlock\": {\n            \"name\": \"Airlock Switch\",\n        },\n        \"switch_type_button\": {\n            \"name\": [\n                \"Button\",\n                \"Push Button\",\n                \"Switch\",\n            ]\n        },\n        \"switch_type_normal\": {\n            \"name\": [\n                \"Lever\",\n                \"Switch\",\n            ]\n        },\n        \"switch_type_small\": {\n            \"name\": \"Small Switch\",\n        },\n        \"switch_type_uw\": {\n            \"name\": [\n                \"Underwater Lever\",\n                \"Underwater Switch\",\n            ]\n        },\n        \"switch_type_wheel\": {\n            \"name\": [\n                \"Wheel Switch\",\n                \"Pulley Switch\",\n                \"Valve Switch\",\n            ]\n        },\n        \"teeth_trap\": {\n            \"name\": [\n                \"Teeth Trap\",\n                \"Clang-clang Door\",\n            ]\n        },\n        \"text_box\": {\n            \"name\": \"UI Frame\",\n        },\n        \"thors_handle\": {\n            \"name\": \"Thor's Hammer Handle\",\n        },\n        \"thors_head\": {\n            \"name\": \"Thor's Hammer\",\n        },\n        \"tiger\": {\n            \"name\": \"Tiger\",\n        },\n        \"tony\": {\n            \"name\": \"Tony\",\n        },\n        \"torso\": {\n            \"name\": [\n                \"Torso\",\n                \"Adam\",\n                \"Giant Mutant\",\n            ]\n        },\n        \"train\": {\n            \"name\": \"Train\",\n        },\n        \"trapdoor_1\": {\n            \"name\": \"Trapdoor 1\",\n        },\n        \"trapdoor_2\": {\n            \"name\": \"Trapdoor 2\",\n        },\n        \"trapdoor_3\": {\n            \"name\": \"Trapdoor 3\",\n        },\n        \"trex\": {\n            \"name\": \"T-Rex\",\n        },\n        \"trex_alpha\": {\n            \"name\": \"T-Rex Alpha\",\n        },\n        \"tribe_axeman\": {\n            \"name\": \"Tribe Axeman\",\n        },\n        \"tribe_boss\": {\n            \"name\": \"Tribe Boss\",\n        },\n        \"tribe_pipeman\": {\n            \"name\": \"Tribe Blowpipe User\",\n        },\n        \"tropical_fish\": {\n            \"name\": \"Tropical Fish\",\n        },\n        \"twinkle\": {\n            \"name\": \"Sparkles\",\n        },\n        \"upv\": {\n            \"name\": [\n                \"UPV\",\n                \"Minisub\",\n            ]\n        },\n        \"uzis\": {\n            \"name\": \"Uzis\",\n        },\n        \"uzis_ammo\": {\n            \"name\": \"Uzi Clips\",\n        },\n        \"vole\": {\n            \"name\": [\n                \"Vole\",\n                \"Water Rat\",\n            ]\n        },\n        \"vulture\": {\n            \"name\": \"Vulture\",\n        },\n        \"wasp_mutant\": {\n            \"name\": \"Wasp Mutant\",\n        },\n        \"wasp_mutant_emitter\": {\n            \"name\": \"Wasp Mutant Emitter\",\n        },\n        \"water_sprite\": {\n            \"name\": \"Boat Wake\",\n        },\n        \"waterfall\": {\n            \"name\": \"Waterfall Mist\",\n        },\n        \"white_light\": {\n            \"name\": \"White Light\",\n        },\n        \"willard\": {\n            \"name\": \"Willard\",\n        },\n        \"winston\": {\n            \"name\": \"Winston\",\n        },\n        \"winston_army\": {\n            \"name\": \"Winston (army)\",\n        },\n        \"wolf\": {\n            \"name\": \"Wolf\",\n        },\n        \"worker_1\": {\n            \"name\": \"Gunman Goon 1\",\n        },\n        \"worker_2\": {\n            \"name\": \"Gunman Goon 2\",\n        },\n        \"worker_3\": {\n            \"name\": \"Stick Wielding Goon 1\",\n        },\n        \"worker_4\": {\n            \"name\": \"Stick Wielding Goon 2\",\n        },\n        \"worker_5\": {\n            \"name\": \"Flamethrower Goon\",\n        },\n        \"xian_knight\": {\n            \"name\": \"Xian Knight\",\n        },\n        \"xian_knight_statue\": {\n            \"name\": \"Xian Knight Statue\",\n        },\n        \"xian_spearman\": {\n            \"name\": \"Xian Spearman\",\n        },\n        \"xian_spearman_statue\": {\n            \"name\": \"Xian Spearman Statue\",\n        },\n        \"yeti\": {\n            \"name\": \"Yeti\",\n        },\n        \"zipline_handle\": {\n            \"name\": \"Zipline Handle\",\n        }\n    }\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/outfits.json5",
    "content": "{\n    \"outfits\": {\n        // TR1 Gym\n        \"tr1_gym\" : {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_gym\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_1\",\n            \"gun_map\": 0,\n            \"footstep_sample_id\": \"SFX_LARA_BAREFOOT\",\n            \"combat_face_offset\": 1,\n            \"braid\": {\n                \"mode\": \"BRAID_MODE_TR1_HEAD_ONLY\",\n                \"mesh_offset\": 10,\n                \"gold_offset\": 16,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": 20,\n                    \"z\": -45,\n                },\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_lara\",\n            },\n        },\n\n        // TR1 Classic\n        \"tr1_classic\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_classic\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_2\",\n            \"gun_map\": 0,\n            \"combat_face_offset\": 1,\n            \"braid\": {\n                \"mode\": \"BRAID_MODE_TR1_FULL\",\n                \"mesh_offset\": 10,\n                \"gold_offset\": 16,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": 20,\n                    \"z\": -45,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 1,\n                \"thigh_l\": 2,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_TREX_KILL\": \"tr1_mauled\",\n                \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_lara\",\n            },\n        },\n\n        // TR1 Mauled\n        \"tr1_mauled\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_mauled\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_3\",\n            \"gun_map\": 0,\n            \"combat_face_offset\": 1,\n            \"braid\": {\n                \"mode\": \"BRAID_MODE_TR1_MAULED\",\n                \"mesh_offset\": 10,\n                \"gold_offset\": 16,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": 20,\n                    \"z\": -45,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 3,\n                \"thigh_l\": 4,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_lara\",\n            },\n        },\n\n        // TR1 Combo\n        \"tr1_combo\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_combo\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_4\",\n            \"gun_map\": 0,\n            \"combat_face_offset\": 1,\n            \"braid\": {\n                \"mode\": \"BRAID_MODE_TR1_HEAD_ONLY\",\n                \"mesh_offset\": 10,\n                \"gold_offset\": 16,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": 20,\n                    \"z\": -45,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 1,\n                \"thigh_l\": 2,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_lara\",\n            },\n        },\n\n        // TR1 N-Gage\n        \"tr1_ngage\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_ngage\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_24\",\n            \"gun_map\": 0,\n            \"combat_face_offset\": 1,\n            \"braid\": {\n                \"mode\": \"BRAID_MODE_TR1_HEAD_ONLY\",\n                \"mesh_offset\": 10,\n                \"gold_offset\": 16,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": 20,\n                    \"z\": -45,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 25,\n                \"thigh_l\": 26,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_lara\",\n            },\n        },\n\n        // TR1 Golden Lara\n        \"tr1_golden_lara\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_golden_lara\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_5\",\n            \"gun_map\": 1,\n            \"is_reflective\": true,\n            \"combat_face_offset\": -1,\n            \"braid\": {\n                \"mode\": \"BRAID_MODE_TR1_GOLD\",\n                \"mesh_offset\": 10,\n                \"gold_offset\": 16,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": 20,\n                    \"z\": -45,\n                },\n            },\n        },\n\n        // Bacon Lara\n        \"tr1_bacon_lara\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_bacon_lara\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_6\",\n            \"gun_map\": 0,\n            \"footstep_sample_id\": \"SFX_LARA_BAREFOOT\",\n            \"combat_face_offset\": -1,\n            \"supports_sunglasses\": false,\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_bacon_lara\",\n            },\n        },\n\n        // Golden Bacon Lara\n        \"tr1_golden_bacon_lara\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr1_golden_bacon_lara\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_7\",\n            \"gun_map\": 1,\n            \"is_reflective\": true,\n            \"supports_sunglasses\": false,\n            \"combat_face_offset\": -1,\n        },\n\n        // TR2 Gym\n        \"tr2_gym\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_gym\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_8\",\n            \"gun_map\": 2,\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 5,\n                \"thigh_l\": 6,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // TR2 Classic\n        \"tr2_classic\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_classic\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_9\",\n            \"gun_map\": 2,\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 7,\n                \"thigh_l\": 8,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Diving Suit\n        \"tr2_diving_suit\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_diving_suit\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_10\",\n            \"gun_map\": 2,\n            \"footstep_sample_id\": \"SFX_LARA_BAREFOOT\",\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Diving Suit Alpha\n        \"tr2_diving_suit_alpha\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_diving_suit_alpha\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_23\",\n            \"gun_map\": 2,\n            \"footstep_sample_id\": \"SFX_LARA_BAREFOOT\",\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Bomber Jacket\n        \"tr2_bomber_jacket\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_bomber_jacket\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_11\",\n            \"gun_map\": 2,\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 7,\n                \"thigh_l\": 8,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Robe\n        \"tr2_robe\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_robe\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_12\",\n            \"gun_map\": 2,\n            \"footstep_sample_id\": \"SFX_LARA_BAREFOOT\",\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 9,\n                \"thigh_l\": 10,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Vegas\n        \"tr2_vegas\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr2_vegas\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_13\",\n            \"gun_map\": 2,\n            \"combat_face_offset\": 2,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 11,\n                \"thigh_l\": 12,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // TR2/3 Golden Lara\n        \"tr23_golden_lara\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr23_golden_lara\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_14\",\n            \"gun_map\": 1,\n            \"is_reflective\": true,\n            \"combat_face_offset\": -1,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n        },\n\n        // TR3 Gym\n        \"tr3_gym\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr3_gym\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_15\",\n            \"gun_map\": 4,\n            \"combat_face_offset\": 3,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 13,\n                \"thigh_l\": 14,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // TR3 Classic\n        \"tr3_classic\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr3_classic\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_16\",\n            \"gun_map\": 3,\n            \"combat_face_offset\": 3,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 15,\n                \"thigh_l\": 16,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // South Pacific\n        \"tr3_south_pacific\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr3_south_pacific\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_17\",\n            \"gun_map\": 3,\n            \"combat_face_offset\": 3,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 17,\n                \"thigh_l\": 18,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Catsuit\n        \"tr3_catsuit\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr3_catsuit\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_18\",\n            \"gun_map\": 3,\n            \"combat_face_offset\": 3,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 19,\n                \"thigh_l\": 20,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Nevada\n        \"tr3_nevada\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr3_nevada\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_19\",\n            \"gun_map\": 4,\n            \"combat_face_offset\": 3,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 21,\n                \"thigh_l\": 22,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Antarctica\n        \"tr3_antarctica\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/tr3_antarctica\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_20\",\n            \"gun_map\": 4,\n            \"combat_face_offset\": 3,\n            \"braid\": {\n                \"mesh_offset\": 22,\n                \"gold_offset\": 28,\n                \"hair_pos\": {\n                    \"x\": 0,\n                    \"y\": -23,\n                    \"z\": -55,\n                },\n            },\n            \"no_holster_offsets\": {\n                \"thigh_r\": 23,\n                \"thigh_l\": 24,\n            },\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"tr23_golden_lara\",\n            },\n        },\n\n        // Sophia Leigh\n        \"sophia\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/sophia\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_21\",\n            \"gun_map\": 3,\n            \"combat_face_offset\": -1,\n            \"extra_outfits\": {\n                \"LS_EXTRA_MIDAS_KILL\": \"golden_sophia\",\n            },\n        },\n\n        // Golden Sophia Leigh\n        \"golden_sophia\": {\n            \"name_gs\": \"dynamic/enums/lara_outfit/golden_sophia\",\n            \"mesh_object\": \"O_LARA_SKIN_SWAP_22\",\n            \"gun_map\": 1,\n            \"is_selectable\": false,\n            \"is_reflective\": true,\n            \"combat_face_offset\": -1,\n        },\n    },\n\n    \"extra_meshes\": {\n        \"EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD\": 7,\n        \"EXTRA_MESH_TR1_BRAID_COMBAT_HEAD\": 9,\n        \"EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO\": 4,\n        \"EXTRA_MESH_TR1_BRAID_MAULED_TORSO\": 5,\n        \"EXTRA_MESH_TR1_BRAID_GOLD_HEAD\": 8,\n        \"EXTRA_MESH_TR1_BRAID_GOLD_TORSO\": 6,\n        \"EXTRA_MESH_DAGGER_HAND\": 35,\n        \"EXTRA_MESH_DAGGER_HIPS\": 34,\n        \"EXTRA_MESH_OAR\": 36,\n        \"EXTRA_MESH_SPANNER\": 37,\n        \"EXTRA_MESH_DRINK_CAN\": 38,\n        \"EXTRA_MESH_GLASSES_OPAQUE\": 39,\n        \"EXTRA_MESH_GLASSES_TRANSPARENT\": 40,\n    },\n\n    \"gun_maps\": [\n        // 0. TR1 style\n        {\n            \"LGT_UNARMED\": {\n                \"thigh_r\": 1,\n                \"thigh_l\": 2,\n            },\n            \"LGT_PISTOLS\": {\n                \"hand_r\": 56,\n                \"hand_l\": 57,\n                \"thigh_r\": 3,\n                \"thigh_l\": 4,\n            },\n            \"LGT_MAGNUMS\": {\n                \"hand_r\": 58,\n                \"hand_l\": 59,\n                \"thigh_r\": 5,\n                \"thigh_l\": 6,\n            },\n            \"LGT_AUTOS\": {\n                \"hand_r\": 60,\n                \"hand_l\": 61,\n                \"thigh_r\": 7,\n                \"thigh_l\": 8,\n            },\n            \"LGT_DESERT_EAGLE\": {\n                \"hand_r\": 62,\n                \"thigh_r\": 9,\n                \"thigh_l\": 2,\n            },\n            \"LGT_UZIS\": {\n                \"hand_r\": 63,\n                \"hand_l\": 64,\n                \"thigh_r\": 10,\n                \"thigh_l\": 11,\n            },\n            \"LGT_SHOTGUN\": {\n                \"hand_r\": 65,\n                \"torso\": 72,\n            },\n            \"LGT_FLARE\": {\n                \"hand_l\": 66,\n            },\n            \"LGT_M16\": {\n                \"hand_r\": 67,\n                \"torso\": 73,\n            },\n            \"LGT_MP5\": {\n                \"hand_r\": 68,\n                \"torso\": 74,\n            },\n            \"LGT_GRENADE\": {\n                \"hand_r\": 69,\n                \"torso\": 75,\n            },\n            \"LGT_HARPOON\": {\n                \"hand_r\": 70,\n                \"torso\": 76,\n            },\n            \"LGT_ROCKET\": {\n                \"hand_r\": 71,\n                \"torso\": 77,\n            },\n        },\n\n        // 1. Gold style\n        {\n            \"LGT_UNARMED\": {\n                \"thigh_r\": 12,\n                \"thigh_l\": 13,\n            },\n            \"LGT_PISTOLS\": {\n                \"hand_r\": 56,\n                \"hand_l\": 57,\n                \"thigh_r\": 14,\n                \"thigh_l\": 15,\n            },\n            \"LGT_MAGNUMS\": {\n                \"hand_r\": 58,\n                \"hand_l\": 59,\n                \"thigh_r\": 16,\n                \"thigh_l\": 17,\n            },\n            \"LGT_AUTOS\": {\n                \"hand_r\": 60,\n                \"hand_l\": 61,\n                \"thigh_r\": 18,\n                \"thigh_l\": 19,\n            },\n            \"LGT_DESERT_EAGLE\": {\n                \"hand_r\": 62,\n                \"thigh_r\": 20,\n                \"thigh_l\": 13,\n            },\n            \"LGT_UZIS\": {\n                \"hand_r\": 63,\n                \"hand_l\": 64,\n                \"thigh_r\": 21,\n                \"thigh_l\": 22,\n            },\n            \"LGT_SHOTGUN\": {\n                \"hand_r\": 65,\n                \"torso\": 72,\n            },\n            \"LGT_FLARE\": {\n                \"hand_l\": 66,\n            },\n            \"LGT_M16\": {\n                \"hand_r\": 67,\n                \"torso\": 73,\n            },\n            \"LGT_MP5\": {\n                \"hand_r\": 68,\n                \"torso\": 74,\n            },\n            \"LGT_GRENADE\": {\n                \"hand_r\": 69,\n                \"torso\": 75,\n            },\n            \"LGT_HARPOON\": {\n                \"hand_r\": 70,\n                \"torso\": 76,\n            },\n            \"LGT_ROCKET\": {\n                \"hand_r\": 71,\n                \"torso\": 77,\n            },\n        },\n\n        // 2. TR2 style\n        {\n            \"LGT_UNARMED\": {\n                \"thigh_r\": 23,\n                \"thigh_l\": 24,\n            },\n            \"LGT_PISTOLS\": {\n                \"hand_r\": 56,\n                \"hand_l\": 57,\n                \"thigh_r\": 25,\n                \"thigh_l\": 26,\n            },\n            \"LGT_MAGNUMS\": {\n                \"hand_r\": 58,\n                \"hand_l\": 59,\n                \"thigh_r\": 27,\n                \"thigh_l\": 28,\n            },\n            \"LGT_AUTOS\": {\n                \"hand_r\": 60,\n                \"hand_l\": 61,\n                \"thigh_r\": 29,\n                \"thigh_l\": 30,\n            },\n            \"LGT_DESERT_EAGLE\": {\n                \"hand_r\": 62,\n                \"thigh_r\": 31,\n                \"thigh_l\": 24,\n            },\n            \"LGT_UZIS\": {\n                \"hand_r\": 63,\n                \"hand_l\": 64,\n                \"thigh_r\": 32,\n                \"thigh_l\": 33,\n            },\n            \"LGT_SHOTGUN\": {\n                \"hand_r\": 65,\n                \"torso\": 72,\n            },\n            \"LGT_FLARE\": {\n                \"hand_l\": 66,\n            },\n            \"LGT_M16\": {\n                \"hand_r\": 67,\n                \"torso\": 73,\n            },\n            \"LGT_MP5\": {\n                \"hand_r\": 68,\n                \"torso\": 74,\n            },\n            \"LGT_GRENADE\": {\n                \"hand_r\": 69,\n                \"torso\": 75,\n            },\n            \"LGT_HARPOON\": {\n                \"hand_r\": 70,\n                \"torso\": 76,\n            },\n            \"LGT_ROCKET\": {\n                \"hand_r\": 71,\n                \"torso\": 77,\n            },\n        },\n\n        // 3. TR3 style\n        {\n            \"LGT_UNARMED\": {\n                \"thigh_r\": 34,\n                \"thigh_l\": 35,\n            },\n            \"LGT_PISTOLS\": {\n                \"hand_r\": 56,\n                \"hand_l\": 57,\n                \"thigh_r\": 36,\n                \"thigh_l\": 37,\n            },\n            \"LGT_MAGNUMS\": {\n                \"hand_r\": 58,\n                \"hand_l\": 59,\n                \"thigh_r\": 38,\n                \"thigh_l\": 39,\n            },\n            \"LGT_AUTOS\": {\n                \"hand_r\": 60,\n                \"hand_l\": 61,\n                \"thigh_r\": 40,\n                \"thigh_l\": 41,\n            },\n            \"LGT_DESERT_EAGLE\": {\n                \"hand_r\": 62,\n                \"thigh_r\": 42,\n                \"thigh_l\": 35,\n            },\n            \"LGT_UZIS\": {\n                \"hand_r\": 63,\n                \"hand_l\": 64,\n                \"thigh_r\": 43,\n                \"thigh_l\": 44,\n            },\n            \"LGT_SHOTGUN\": {\n                \"hand_r\": 65,\n                \"torso\": 72,\n            },\n            \"LGT_FLARE\": {\n                \"hand_l\": 66,\n            },\n            \"LGT_M16\": {\n                \"hand_r\": 67,\n                \"torso\": 73,\n            },\n            \"LGT_MP5\": {\n                \"hand_r\": 68,\n                \"torso\": 74,\n            },\n            \"LGT_GRENADE\": {\n                \"hand_r\": 69,\n                \"torso\": 75,\n            },\n            \"LGT_HARPOON\": {\n                \"hand_r\": 70,\n                \"torso\": 76,\n            },\n            \"LGT_ROCKET\": {\n                \"hand_r\": 71,\n                \"torso\": 77,\n            },\n        },\n\n        // 4. TR3 alt style\n        {\n            \"LGT_UNARMED\": {\n                \"thigh_r\": 45,\n                \"thigh_l\": 46,\n            },\n            \"LGT_PISTOLS\": {\n                \"hand_r\": 56,\n                \"hand_l\": 57,\n                \"thigh_r\": 47,\n                \"thigh_l\": 48,\n            },\n            \"LGT_MAGNUMS\": {\n                \"hand_r\": 58,\n                \"hand_l\": 59,\n                \"thigh_r\": 49,\n                \"thigh_l\": 50,\n            },\n            \"LGT_AUTOS\": {\n                \"hand_r\": 60,\n                \"hand_l\": 61,\n                \"thigh_r\": 51,\n                \"thigh_l\": 52,\n            },\n            \"LGT_DESERT_EAGLE\": {\n                \"hand_r\": 62,\n                \"thigh_r\": 53,\n                \"thigh_l\": 46,\n            },\n            \"LGT_UZIS\": {\n                \"hand_r\": 63,\n                \"hand_l\": 64,\n                \"thigh_r\": 54,\n                \"thigh_l\": 55,\n            },\n            \"LGT_SHOTGUN\": {\n                \"hand_r\": 65,\n                \"torso\": 72,\n            },\n            \"LGT_FLARE\": {\n                \"hand_l\": 66,\n            },\n            \"LGT_M16\": {\n                \"hand_r\": 67,\n                \"torso\": 73,\n            },\n            \"LGT_MP5\": {\n                \"hand_r\": 68,\n                \"torso\": 74,\n            },\n            \"LGT_GRENADE\": {\n                \"hand_r\": 69,\n                \"torso\": 75,\n            },\n            \"LGT_HARPOON\": {\n                \"hand_r\": 70,\n                \"torso\": 76,\n            },\n            \"LGT_ROCKET\": {\n                \"hand_r\": 71,\n                \"torso\": 77,\n            },\n        },\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/poses.json5",
    "content": "[\n    {\"offset\": [0,   -442, 0],    \"rots\": [[-2624,  4288,   9984],   [7808,  1088,   0],      [-15168, 0,      0],      [-2688,  1984,  -3648], [-960,  2048,   -960],   [-3328,  0,      0],      [-3072, 0,     2240],  [1856,  3648,  -1920], [6528,   -14080, -24640], [3328,  0,      0],      [-1856, 6144,   512],    [1664,  -8640,  6080],   [6464,  0,      640],    [576,   -2112,  2688],   [5056,  5440,   -1664]]},\n    {\"offset\": [0,   -565, -200], \"rots\": [[1920,   1024,   -512],   [15040, 0,      -1536],  [-15488, 0,      1216],   [-1856,  384,   256],   [14272, -32768, -29312], [-14016, -3200,  6976],   [-1152, 0,     0],     [-2688, -832,  1408],  [1024,   -16192, -8512],  [15872, -1024,  -704],   [7680,  1664,   768],    [3264,  12032,  6336],   [15360, -17472, -23616], [5248,  4032,   1664],   [1472,  192,    2112]]},\n    {\"offset\": [-8,  -439, -7],   \"rots\": [[-640,   64,     192],    [2112,  -2304,  1920],   [-3136,  -64,    0],      [1728,   448,   -2240], [2688,  2048,   -2688],  [-3456,  0,      0],      [1664,  -64,   2368],  [0,     -64,   -256],  [15616,  32704,  32704],  [0,     0,      -320],   [-1472, 0,      -384],   [15616, 32704,  32704],  [0,     0,      448],    [-1600, -64,    64],     [64,    0,      0]]},\n    {\"offset\": [-8,  -448, -7],   \"rots\": [[768,    -320,   -256],   [-2752, -2176,  1600],   [0,      0,      0],      [1728,   0,     -1152], [1024,  1216,   -2112],  [-2112,  0,      0],      [0,     0,     1856],  [-1088, 3456,  576],   [7104,   11520,  -2560],  [11712, -32768, -32768], [2112,  960,    -1792],  [-1536, -8320,  3776],   [4352,  0,      0],      [448,   2432,   2560],   [-1536, 1728,   640]]},\n    {\"offset\": [1,   -442, -4],   \"rots\": [[-3008,  -1792,  -17408], [-2048, -384,   -768],   [-4224,  0,      0],      [-9216,  0,     0],     [2752,  0,      2176],   [-6592,  0,      0],      [-8128, 0,     0],     [-1984, 2496,  3264],  [15936,  0,      0],      [0,     3904,   0],      [0,     0,      0],      [15232, 0,      0],      [0,     6528,   0],      [0,     0,      0],      [-3008, -1280,  5760]]},\n    {\"offset\": [-8,  -450, -7],   \"rots\": [[640,    832,    1216],   [-2048, -576,   -448],   [448,    0,      0],      [960,    -1472, -512],  [1664,  -448,   -2624],  [-2752,  0,      0],      [704,   0,     1024],  [-64,   -384,  -896],  [-9536,  -10432, 1664],   [15168, -32768, -32768], [832,   -5120,  -10496], [-8960, 10624,  -1664],  [15680, -32768, -32448], [2944,  5184,   12032],  [-2688, 1024,   1216]]},\n    {\"offset\": [0,   -422, 0],    \"rots\": [[-640,   384,    -64],    [5056,  -4352,  3392],   [-2368,  0,      0],      [-2176,  768,   -2624], [512,   128,    -3648],  [-5440,  0,      0],      [4928,  -448,  4352],  [-3008, -6080, -2112], [-64,    2560,   -7808],  [6784,  -256,   -448],   [-2112, 2752,   -128],   [1152,  -1600,  2816],   [15808, 18816,  18816],  [-1088, 768,    4096],   [-6016, 7936,   2944]]},\n    {\"offset\": [0,   -435, 50],   \"rots\": [[1152,   0,      1472],   [1920,  384,    -1856],  [-4800,  384,    -256],   [1792,   704,   384],   [4736,  3328,   -1984],  [-11136, -32704, 32064],  [-192,  192,   512],   [2944,  -1920, -2176], [12160,  -4224,  -1600],  [-64,   1088,   -64],    [-640,  -192,   -1088],  [1536,  12992,  10880],  [-256,  1664,   768],    [-960,  -13760, 1792],   [-5120, -1152,  -704]]},\n    {\"offset\": [0,   -140, 0],    \"rots\": [[576,    0,      -1984],  [9984,  2496,   5888],   [-6400,  32704,  32704],  [-11776, 0,     0],     [11072, -26304, 31488],  [-7104,  -32768, 32704],  [2496,  0,     0],     [-8064, -3264, 7680],  [1792,   -3456,  -10304], [11776, 256,    -512],   [32000, 30976,  31744],  [0,     8960,   2624],   [10944, 896,    3392],   [3200,  0,      3264],   [9344,  4416,   -192]]},\n    {\"offset\": [0,   -90,  0],    \"rots\": [[-2368,  1536,   18496],  [8000,  -7616,  -7744],  [-14016, -32768, 32704],  [-6080,  2304,  -2688], [7424,  -5248,  -4864],  [-14912, 0,      0],      [1664,  -64,   2368],  [-6080, 2432,  -4416], [5824,   -3712,  -320],   [15936, -1536,  -1856],  [0,     0,      -1024],  [7104,  1344,   0],      [10624, 0,      -7936],  [0,     0,      10496],  [-1408, 0,      4864]]},\n    {\"offset\": [0,   -275, -100], \"rots\": [[-2816,  0,      320],    [10048, 576,    0],      [-12992, 32704,  32704],  [12480,  -1472, -2176], [15168, -128,   -2944],  [-11840, 32704,  -32768], [8512,  -192,  -1536], [-3776, 0,     -64],   [11648,  -12224, -27456], [12992, -960,   -1024],  [448,   -768,   7488],   [12800, 28608,  -30272], [15040, 28672,  29632],  [-1728, 8256,   -13248], [5632,  2048,   832]]},\n    {\"offset\": [0,   -401, 0],    \"rots\": [[-4224,  2176,   64],     [1152,  384,    -512],   [-14272, -32768, 32704],  [-3648,  832,   1344],  [12352, 512,    -128],   [-9728,  0,      0],      [3200,  512,   960],   [-2560, 5120,  -448],  [3648,   14272,  -5184],  [-320,  -1152,  2368],   [-6976, -14208, -1728],  [2304,  7552,   12096],  [14656, 29632,  29568],  [5568,  1088,   -2112],  [2176,  -4096,  1024]]},\n    {\"offset\": [0,   -394, 0],    \"rots\": [[-3456,  0,      -704],   [6656,  3968,   8640],   [-8704,  64,     -256],   [-1088,  320,   -2816], [9216,  -896,   -1344],  [-11456, 0,      0],      [4800,  2752,  2368],  [-4096, 512,   -128],  [192,    15424,  23296],  [-320,  -1152,  896],    [-3008, -16064, 384],    [9088,  28928,  30848],  [896,   1600,   192],    [-512,  -2944,  -256],   [4096,  -896,   0]]},\n    {\"offset\": [0,   -51,  0],    \"rots\": [[4992,   960,    192],    [11712, 0,      0],      [-1088,  0,      0],      [-1472,  0,     0],     [13952, 28608,  27328],  [-13632, 1280,   576],    [-1152, 0,     0],     [-5248, -3456, -1536], [5952,   448,    -3264],  [7808,  -3776,  -3328],  [320,   -4480,  1152],   [384,   -4224,  4480],   [0,     0,      0],      [1216,  -1408,  12480],  [-832,  -6208,  -1536]]},\n    {\"offset\": [0,   -435, 0],    \"rots\": [[-3968,  17216,  1152],   [9920,  -1792,  -448],   [-7424,  0,      0],      [-448,   384,   -1152], [2176,  3712,   -2176],  [-2176,  0,      0],      [2944,  0,     2688],  [-704,  -4096, -2048], [6656,   -5888,  -5760],  [8768,  -32768, 29248],  [2304,  -2944,  -1344],  [-64,   -10688, 3712],   [7616,  0,      0],      [768,   3328,   -1408],  [-1280, -9024,  -3456]]},\n    {\"offset\": [0,   -460, 0],    \"rots\": [[-1344,  -2624,  -1792],  [1408,  -640,   1088],   [0,      0,      0],      [0,      0,     0],     [6848,  -2176,  1344],   [-9920,  1280,   -1024],  [-4160, 0,     1664],  [2496,  256,   1856],  [1984,   6272,   -7104],  [2816,  0,      -320],   [0,     0,      768],    [-1152, -2368,  5184],   [3648,  0,      0],      [192,   -3520,  -320],   [-4672, 1664,   640]]},\n    {\"offset\": [0,   -456, 0],    \"rots\": [[448,    -3008,  -576],   [3072,  -1408,  448],    [-8000,  0,      0],      [0,      0,     0],     [-1024, 2624,   640],    [0,      0,      0],      [0,     0,     0],     [0,     0,     1408],  [512,    7168,   -4608],  [3328,  0,      0],      [0,     0,      0],      [-960,  -1728,  3136],   [4288,  0,      0],      [0,     0,      0],      [-3584, 2688,   -1152]]},\n    {\"offset\": [0,   -450, 0],    \"rots\": [[-128,   -1408,  -576],   [-1024, -64,    768],    [-1408,  0,      0],      [1984,   0,     0],     [2496,  3328,   2176],   [0,      0,      0],      [-4480, 1920,  -832],  [512,   2432,  1600],  [8704,   5248,   -5696],  [11776, -32768, 31744],  [1088,  -64,    -384],   [-2432, -6080,  3584],   [5888,  0,      0],      [-1664, 320,    -1984],  [-2752, 4992,   896]]},\n    {\"offset\": [0,   -460, 0],    \"rots\": [[1600,   -3584,  1600],   [7296,  -1024,  -768],   [-16128, 0,      0],      [-8768,  3136,  -1536], [-1472, 0,      128],    [0,      0,      0],      [-576,  2048,  -1280], [128,   1600,  -192],  [2688,   -14400, -5696],  [14400, -32768, 30912],  [6272,  448,    -4928],  [-5376, -4672,  1600],   [10880, 7744,   4992],   [5440,  3520,   1728],   [-2880, 896,    -2624]]},\n    {\"offset\": [0,   -430, 0],    \"rots\": [[-2688,  1088,   768],    [7616,  0,      -704],   [-6272,  0,      0],      [3008,   0,     0],     [-1600, 2176,   -320],   [-5248,  0,      0],      [-3264, -960,  1920],  [-2048, -2048, -320],  [4608,   2944,   -2240],  [11648, -32768, -32768], [-2944, 2048,   -4160],  [-5056, 0,      0],      [13312, -32768, -32768], [2560,  0,      0],      [2560,  4096,   1408]]},\n    {\"offset\": [0,   -565, 0],    \"rots\": [[3328,   1984,   -6400],  [10176, -13376, -7168],  [-10368, -32768, -28608], [-3520,  4032,  -4288], [1280,  -320,   -5696],  [0,      0,      0],      [-64,   0,     2240],  [-7488, -1536, 2880],  [7872,   -1920,  -9088],  [704,   0,      0],      [0,     0,      0],      [-3008, 9664,   7680],   [15936, 0,      0],      [0,     0,      0],      [-6528, 5184,   6912]]},\n    {\"offset\": [0,   -449, 0],    \"rots\": [[1792,   2304,   -512],   [0,     -2816,  2944],   [-1152,  0,      0],      [64,     640,   -1472], [-4096, 192,    -2176],  [-384,   0,      0],      [1408,  0,     1600],  [-576,  1536,  1216],  [-5376,  6528,   -2304],  [4928,  5248,   2560],   [-896,  3008,   -1600],  [14976, 0,      2624],   [1088,  0,      0],      [-2048, 0,      0],      [-3520, -2560,  1536]]},\n    {\"offset\": [0,   -453, 0],    \"rots\": [[-896,   28672,  832],    [-768,  -3136,  1152],   [-768,   0,      0],      [448,    0,     -1408], [1088,  -320,   -3200],  [-960,   0,      0],      [0,     0,     2560],  [1216,  -3840, -2112], [-1856,  -4992,  -320],   [7168,  0,      0],      [-2304, -4160,  -7616],  [5568,  -6976,  4928],   [1728,  -22208, -24128], [1408,  9024,   256],    [-3072, -11520, -704]]},\n    {\"offset\": [0,   -454, 0],    \"rots\": [[448,    -17024, -320],   [4288,  -1536,  -320],   [-13056, 3200,   -3072],  [-2560,  1280,  0],     [-1152, 0,      960],    [0,      0,      0],      [0,     0,     0],     [1024,  832,   1536],  [3264,   -1344,  -704],   [8832,  -32768, -32768], [1856,  0,      0],      [448,   12224,  4736],   [15872, 0,      -3328],  [512,   0,      -7936],  [-2944, 10624,  -384]]},\n    {\"offset\": [0,   -452, 0],    \"rots\": [[-3008,  -768,   -192],   [1984,  832,    -256],   [320,    0,      0],      [640,    -1152, -64],   [5056,  3008,   2752],   [0,      0,      0],      [-3392, 0,     -960],  [-1088, 896,   1024],  [-4544,  -8000,  4032],   [7168,  -3328,  -2176],  [3520,  -192,   6016],   [9600,  3328,   3584],   [6464,  -16448, -22464], [4608,  -5504,  11328],  [1984,  -128,   -1408]]},\n    {\"offset\": [0,   -51,  0],    \"rots\": [[6080,   -512,   -640],   [14848, -32768, 30848],  [-12608, 0,      0],      [-2112,  0,     0],     [12032, 30400,  29312],  [-14144, -32768, -31552], [-512,  -3328, 192],   [-7872, 1344,  576],   [8064,   -9600,  -8384],  [8256,  -3712,  -2688],  [576,   -896,   3456],   [5312,  15104,  12864],  [11136, 3008,   1920],   [960,   1984,   -4736],  [2560,  -1024,  -2368]]},\n    {\"offset\": [0,   -565, 0],    \"rots\": [[-2176,  -640,   -8512],  [-2560, -4544,  1856],   [-5760,  0,      384],    [-3136,  0,     0],     [4160,  1216,   -640],   [-12032, 0,      0],      [-4288, 0,     0],     [-2496, 1280,  3392],  [-1344,  3520,   -8192],  [11008, 0,      0],      [6528,  0,      0],      [10560, 12032,  30464],  [9408,  0,      0],      [4032,  0,      -4160],  [4480,  -4736,  768]]},\n    {\"offset\": [0,   -51,  0],    \"rots\": [[-14016, 0,      0],      [-1216, -3392,  2688],   [-15680, -2624,  896],    [-1984,  0,     0],     [-1792, 4544,   -2496],  [-12544, 0,      0],      [0,     0,     0],     [1792,  -1856, -768],  [16256,  0,      0],      [9216,  -32768, -28800], [0,     0,      12224],  [13440, 1472,   1408],   [6080,  16640,  14656],  [0,     4928,   2944],   [7744,  -2624,  -3136]]},\n    {\"offset\": [0,   -232, 0],    \"rots\": [[192,    1280,   1600],   [14784, -32768, -32768], [-14016, -32768, -32768], [-64,    -576,  -1728], [768,   2048,   -3584],  [-13632, -32768, -32768], [-3328, -128,  2112],  [-3072, 2240,  448],   [1664,   5568,   -4160],  [8192,  0,      0],      [0,     0,      0],      [16320, -32768, -25344], [4224,  0,      0],      [-2240, 3008,   -384],   [192,   -4992,  -2880]]},\n    {\"offset\": [0,   -385, 0],    \"rots\": [[3968,   -32768, -32768], [11136, 0,      0],      [1728,   0,      0],      [-11008, 0,     0],     [-6528, 0,      0],      [256,    0,      0],      [-9920, 0,     0],     [6912,  0,     0],     [1664,   -28480, -31872], [3264,  -2432,  -2432],  [960,   -8128,  -10688], [1472,  22528,  30976],  [0,     2432,   0],      [0,     3648,   17280],  [9920,  0,      0]]},\n    {\"offset\": [0,   -385, 0],    \"rots\": [[-4288,  29056,  -28544], [7872,  5312,   1536],   [0,      0,      0],      [-9216,  0,     0],     [-7488, 6208,   -3200],  [0,      0,      0],      [-9344, 0,     0],     [3520,  2496,  -256],  [-2560,  1088,   -4224],  [1600,  6272,   0],      [-6720, 6976,   -4608],  [5952,  -30080, -27200], [0,     9920,   0],      [0,     10368,  16000],  [1664,  -13760, -11648]]},\n    {\"offset\": [0,   -452, 0],    \"rots\": [[-1472,  0,      0],      [3328,  0,      -704],   [0,      0,      0],      [-1728,  -1600, 1024],  [0,     0,      1024],   [-3136,  0,      0],      [-128,  2944,  448],   [0,     0,     0],     [1984,   -3840,  -3776],  [6976,  -32768, -32768], [-3008, 1472,   10752],  [-832,  2880,   768],    [7936,  5120,   2240],   [0,     8448,   -2304],  [1728,  -320,   768]]},\n    {\"offset\": [0,   -193, 0],    \"rots\": [[3456,   0,      0],      [12480, 0,      8128],   [-6272,  -32768, -32768], [7872,   576,   832],   [12288, 0,      -8000],  [-5760,  -32768, -32768], [8192,  -4736, -3456], [-9088, -1792, 2304],  [-3136,  -6976,  -9856],  [9728,  -4416,  -3584],  [640,   -6528,  -5952],  [-2304, 8512,   7424],   [8704,  4736,   3200],   [2112,  5824,   10368],  [4160,  4160,   960]]},\n    {\"offset\": [0,   -450, 0],    \"rots\": [[-2176,  -896,   -256],   [-192,  -1920,  -1152],  [-64,    0,      0],      [1408,   -640,  1280],  [3904,  -832,   640],    [384,    0,      0],      [-1728, 3264,  -576],  [192,   0,     0],     [-3648,  -9856,  4608],   [6016,  -2688,  -192],   [1728,  -2944,  5184],   [-3904, 6976,   -3968],  [5952,  6848,   2624],   [0,     3328,   -2432],  [4928,  2624,   1536]]},\n    {\"offset\": [0,   -450, 0],    \"rots\": [[704,    0,      0],      [-832,  1088,   1536],   [-1152,  0,      128],    [576,    -2496, -1856], [-1088, -1216,  -1472],  [-1216,  0,      -64],    [1024,  3264,  1792],  [-448,  0,     0],     [14016,  -768,   -14784], [-320,  0,      64],     [256,   -1088,  -448],   [1792,  17408,  12608],  [13248, 1920,   2432],   [1216,  -9984,  -704],   [-4096, 9152,   -2112]]},\n    {\"offset\": [0,   -420, 0],    \"rots\": [[2560,   9920,   448],    [768,   -1536,  3520],   [-5312,  64,     64],     [768,    -2240, -3584], [-192,  960,    -4032],  [-6592,  -64,    64],     [2624,  1856,  4736],  [-2240, -1216, 128],   [-512,   -5440,  -1408],  [24320, 3392,   1536],   [768,   6016,   4992],   [256,   -1536,  2368],   [22720, -1984,  -1280],  [2560,  -4160,  -5632],  [-1472, -8896,  512]]},\n    {\"offset\": [0,   -479, 0],    \"rots\": [[1088,   -13696, -9728],  [3072,  -5312,  8832],   [64,     0,      192],    [704,    1216,  320],   [4160,  -1536,  -9408],  [128,    0,      -128],   [-7296, 3328,  512],   [-1152, 1152,  832],   [-1856,  2304,   -5440],  [20224, 5888,   4032],   [768,   6016,   4992],   [-704,  12608,  8384],   [14592, 30784,  31488],  [2560,  -4160,  -5632],  [-4736, 6400,   -1536]]},\n    {\"offset\": [0,   -420, 0],    \"rots\": [[1920,   11136,  -832],   [1728,  -1472,  5312],   [-5440,  64,     64],     [320,    -2944, -4032], [1408,  448,    -2112],  [-7424,  -64,    64],     [2752,  1216,  3968],  [-2240, -1024, 64],    [6848,   -8832,  -6336],  [22336, 3968,   2240],   [768,   6016,   4992],   [9856,  704,    8512],   [23616, -1856,  -1152],  [2560,  -4160,  -5632],  [-704,  -7680,  512]]},\n    {\"offset\": [0,   -420, 0],    \"rots\": [[-192,   -8576,  -960],   [-128,  3072,   2944],   [-6592,  64,     0],      [6528,   -1088, -3328], [5696,  384,    -2560],  [-5312,  0,      0],      [-2368, 128,   1600],  [-704,  320,   384],   [-640,   960,    -4736],  [5120,  -192,   -768],   [-2880, 0,      576],    [-2752, -384,   5440],   [5888,  192,    448],    [-512,  -1664,  -256],   [-192,  6848,   832]]},\n    {\"offset\": [0,   -450, 0],    \"rots\": [[448,    0,      0],      [-320,  1088,   1408],   [-1536,  0,      128],    [704,    -320,  -1856], [-384,  -192,   -1728],  [-1472,  0,      -64],    [1024,  2944,  2496],  [-448,  0,     0],     [5952,   -15616, -10368], [20928, 4672,   3072],   [-3264, -4480,  -5376],  [7424,  5312,   5952],   [23168, -1920,  -960],   [2432,  -2240,  3392],   [-832,  8192,   -192]]},\n    {\"offset\": [0,   -70,  0],    \"rots\": [[-5440,  -10432, 16000],  [3520,  -4608,  -2240],  [-16832, -5568,  5696],   [-3200,  704,   -4608], [5952,  -4480,  -64],    [-23744, 512,    -640],   [4864,  320,   -1856], [-2304, 256,   -2880], [1024,   -1280,  -26496], [18176, 10240,  7040],   [2688,  -1728,  -960],   [12928, 11136,  6144],   [10944, 832,    1920],   [-384,  7808,   6400],   [10944, 384,    -10560]]},\n    {\"offset\": [0,   -150, 0],    \"rots\": [[2560,   448,    1088],   [15360, -1280,  1792],   [-28224, -192,   512],    [9856,   1536,  960],   [7232,  -4736,  -3584],  [-28928, 256,    -576],   [-960,  -3456, 1920],  [-8576, -1216, 704],   [0,      -5248,  -4608],  [11008, -1536,  -2496],  [-1344, -10944, -896],   [320,   5568,   10944],  [18496, -4352,  -3648],  [-1856, 5568,   -1536],  [1408,  128,    -1088]]},\n    {\"offset\": [0,   -170, 0],    \"rots\": [[768,    192,    256],    [7936,  3584,   2944],   [-26816, -256,   576],    [-3072,  1216,  -1344], [6144,  -5632,  -4288],  [-26240, 384,    -576],   [-1600, 320,   1856],  [-1600, 0,     0],     [12480,  -13376, -21120], [25664, 3200,   1152],   [-2880, 0,      -3456],  [19136, -16512, -10496], [24832, -1600,  -704],   [64,    64,     2560],   [-5248, 0,      -128]]},\n    {\"offset\": [0,   -332, 0],    \"rots\": [[256,    0,      256],    [5760,  384,    8768],   [-8704,  128,    -64],    [320,    640,   -6784], [7680,  -7808,  -3712],  [-16128, -12544, 12480],  [7552,  576,   2560],  [-3392, -1600, -1344], [-3648,  -4160,  -9920],  [21824, 1664,   768],    [5120,  -448,   3712],   [-2176, -512,   8704],   [-1728, -128,   -64],    [1216,  -4544,  -3904],  [384,   10560,  1792]]},\n    {\"offset\": [0,   -460, 0],    \"rots\": [[576,    0,      0],      [64,    832,    1280],   [-1600,  0,      128],    [-192,   -1024, -1344], [512,   -1280,  -768],   [-4032,  0,      -64],    [2880,  768,   1024],  [-512,  -2048, 192],   [-1408,  -3392,  -2688],  [4416,  -128,   -640],   [-256,  -64,    2816],   [-3264, -4672,  3648],   [5376,  192,    384],    [-1792, 4992,   -2176],  [-1088, -5760,  576]]},\n    {\"offset\": [0,   -442, 0],    \"rots\": [[6400,   -32768, -32768], [-4800, -704,   384],    [-12416, 448,    -384],   [-9472,  -1536, 1792],  [-4672, 960,    -832],   [-12608, -448,   512],    [-8000, 2112,  -2944], [2112,  0,     64],    [-31872, 12544,  -2688],  [-320,  0,      64],     [-1664, -5056,  -5184],  [0,     17728,  -29824], [-320,  0,      -64],    [-64,   -64,    4672],   [10752, 128,    192]]},\n    {\"offset\": [0,   -442, 0],    \"rots\": [[960,    0,      0],      [1536,  384,    832],    [-14656, 1280,   -1216],  [-3520,  1472,  -1984], [1216,  256,    768],    [-5696,  0,      0],      [3520,  -128,  -832],  [-2112, 0,     -64],   [-640,   -16896, -3840],  [18816, -20096, -22400], [-320,  6976,   2368],   [704,   15040,  1984],   [3712,  -7936,  256],    [2688,  0,      0],      [-5440, 320,    -128]]},\n    {\"offset\": [62,  -434, -7],   \"rots\": [[-640,   64,     -896],   [1472,  -2752,  4864],   [0,      0,      0],      [-192,   896,   -4224], [3136,  2944,   384],    [-5824,  0,      0],      [3328,  -320,  320],   [960,   -832,  1664],  [-256,   -14912, -11712], [12928, -31872, -31808], [0,     0,      9536],   [1664,  14336,  8128],   [13056, 31168,  31104],  [0,     0,      4096],   [-2688, 0,      -1664]]},\n    {\"offset\": [-8,  -431, -44],  \"rots\": [[320,    0,      -1216],  [6848,  1088,   3392],   [-7936,  0,      0],      [-2304,  1280,  -2304], [3520,  -2944,  256],    [-7168,  0,      0],      [2560,  64,    1792],  [4480,  -128,  1920],  [4032,   -8384,  -7168],  [2432,  -128,   -384],   [0,     0,      2176],   [6592,  9664,   7936],   [2816,  128,    512],    [0,     0,      -1792],  [-6720, 0,      0]]},\n    {\"offset\": [57,  -388, -7],   \"rots\": [[-640,   64,     192],    [4608,  256,    4736],   [-8704,  0,      0],      [4224,   -384,  -4544], [6656,  384,    -1408],  [-12352, 0,      0],      [6464,  1024,  1664],  [896,   -128,  -3200], [-2816,  3904,   -8000],  [-1216, 0,      -384],   [0,     0,      -3008],  [-8384, -1216,  3968],   [2496,  64,     512],    [0,     0,      4096],   [-1408, 0,      3968]]},\n    {\"offset\": [2,   -442, -7],   \"rots\": [[-704,   3584,   -64],    [3776,  -2048,  -512],   [-448,   0,      0],      [-4608,  -1664, 384],   [128,   768,    256],    [-2048,  0,      0],      [1984,  -2880, -832],  [704,   -4480, -384],  [-1472,  704,    -16320], [2112,  0,      0],      [0,     0,      896],    [-2496, -704,   16384],  [1472,  0,      0],      [0,     0,      -1088],  [-3648, 832,    64]]},\n    {\"offset\": [-11, -377, -83],  \"rots\": [[2368,   -192,   -1152],  [3520,  704,    3648],   [-15872, -32768, 32704],  [3712,   64,    -2752], [6592,  -320,   640],    [-4992,  0,      0],      [-6080, -960,  320],   [1792,  -576,  1920],  [4800,   -1920,  -9216],  [8640,  -384,   -512],   [0,     0,      64],     [6400,  4864,   10112],  [8832,  512,    704],    [0,     0,      576],    [-5888, 0,      -192]]},\n]\n"
  },
  {
    "path": "data/trx/ship/cfg/presets/tr1-pc.json5",
    "content": "{\n    \"name_gs\": \"dynamic/config_presets/tr1_pc\",\n    \"config\": {\n        \"ui.menu_style\": \"pc\",\n        \"ui.enable_smooth_bars\": true,\n        \"ui.bar_look\": \"tr1_pc\",\n        \"ui.ammo_counter_location\": \"top-right\",\n        \"audio.enable_ps1_sfx\": false,\n        \"gameplay.restore_ps1_enemies\": false,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/presets/tr1-ps1.json5",
    "content": "{\n    \"name_gs\": \"dynamic/config_presets/tr1_ps1\",\n    \"config\": {\n        \"ui.menu_style\": \"ps1\",\n        \"ui.enable_smooth_bars\": false,\n        \"ui.bar_look\": \"tr2_ps1\",\n        \"ui.ammo_counter_location\": \"top-right\",\n        \"audio.enable_ps1_sfx\": true,\n        \"gameplay.restore_ps1_enemies\": true,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/presets/tr2-pc.json5",
    "content": "{\n    \"name_gs\": \"dynamic/config_presets/tr2_pc\",\n    \"config\": {\n        \"ui.menu_style\": \"pc\",\n        \"ui.enable_smooth_bars\": false,\n        \"ui.bar_look\": \"tr2_pc\",\n        \"ui.ammo_counter_location\": \"top-right\",\n        \"audio.enable_ps1_sfx\": false,\n        \"gameplay.restore_ps1_enemies\": false,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/presets/tr2-ps1.json5",
    "content": "{\n    \"name_gs\": \"dynamic/config_presets/tr2_ps1\",\n    \"config\": {\n        \"ui.menu_style\": \"ps1\",\n        \"ui.enable_smooth_bars\": false,\n        \"ui.bar_look\": \"tr2_ps1\",\n        \"ui.ammo_counter_location\": \"top-right\",\n        \"audio.enable_ps1_sfx\": true,\n        \"gameplay.restore_ps1_enemies\": true,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/presets/tr3-pc.json5",
    "content": "{\n    \"name_gs\": \"dynamic/config_presets/tr3_pc\",\n    \"config\": {\n        \"ui.menu_style\": \"pc\",\n        \"ui.enable_smooth_bars\": false,\n        \"ui.bar_look\": \"tr3_pc\",\n        \"ui.ammo_counter_location\": \"top-right\",\n        \"audio.enable_ps1_sfx\": false,\n        \"gameplay.restore_ps1_enemies\": false,\n        \"gameplay.enable_save_crystals\": false,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/presets/tr3-ps1.json5",
    "content": "{\n    \"name_gs\": \"dynamic/config_presets/tr3_ps1\",\n    \"config\": {\n        \"ui.menu_style\": \"ps1\",\n        \"ui.enable_smooth_bars\": false,\n        \"ui.bar_look\": \"tr3_ps1\",\n        \"ui.ammo_counter_location\": \"bottom-right\",\n        \"audio.enable_ps1_sfx\": true,\n        \"gameplay.restore_ps1_enemies\": true,\n        \"gameplay.enable_save_crystals\": true,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/2d.glsl",
    "content": "#include \"common.glsl\"\n\n#define EFFECT_NONE 0\n#define EFFECT_VIGNETTE 1\n#define EFFECT_WAVE 2\n\n#define WAVE_SPEED_SHORT -3.92\n#define WAVE_SPEED_LONG -2.81\n#define WAVE_TILE_PHASE_SHORT vec2(67.5, 73.0)\n#define WAVE_TILE_PHASE_LONG vec2(33.94, 28.31)\n#define WAVE_LIGHT_DELTA 0.125\n#define WAVE_Y_TILES 6\n#define WAVE_ORBIT_RADIUS 0.2\n#define WAVE_FPS_DRIFT 25 / 30\n\nuniform int uEffect;\nuniform float uOpacity;\nuniform float uBrightnessScale;\nuniform int uFitMode;      // 0=stretch,1=letterbox,2=crop,3=smart\nuniform float uSrcAspect;  // src_w/src_h\n\n#ifdef VERTEX\n\nlayout(location = 0) in vec2 inPosition;\nlayout(location = 1) in vec2 inTexCoords;\n\nout vec2 vertCoords;\nout float vertLight;\nout vec2 vertMappedUv;\nout vec4 vertContentRect; // x0,y0,x1,y1 in normalized screen coords\n\nvoid main() {\n    if ((uEffect & EFFECT_WAVE) != 0) {\n        float edgeOffset = (1.0 / WAVE_Y_TILES) * 2.0;\n        vec2 baseNDC = ((inPosition.xy * (2.0 + 2.0 * edgeOffset)) - (1.0 + edgeOffset)) * vec2(1.0, -1.0);\n\n        vec2 aspectCorrection = vec2(uViewportSize.y / uViewportSize.x, 1);\n        vec2 repeat = float(WAVE_Y_TILES) / aspectCorrection;\n        float shortPhase = dot(inPosition, repeat * WAVE_TILE_PHASE_SHORT);\n        float longPhase = dot(inPosition, repeat * WAVE_TILE_PHASE_LONG);\n        float shortAng = radians((uTime * WAVE_FPS_DRIFT)  * WAVE_SPEED_SHORT + shortPhase);\n        float longAng = radians((uTime * WAVE_FPS_DRIFT) * WAVE_SPEED_LONG + longPhase);\n\n        float viewportSizeNDC = (1 + edgeOffset * 2);\n        vec2 tileSize = viewportSizeNDC / repeat;\n        vec2 vertexOffset = vec2(cos(shortAng), sin(shortAng)) * tileSize * WAVE_ORBIT_RADIUS;\n        vertLight = 0.5 + (sin(shortAng) + sin(longAng)) * WAVE_LIGHT_DELTA;\n\n        gl_Position  = vec4(baseNDC + vertexOffset, 0.0, 1.0);\n    } else {\n        vec2 baseNDC = inPosition * vec2(2.0, -2.0) + vec2(-1.0, 1.0);\n        gl_Position = vec4(baseNDC, 0.0, 1.0);\n    }\n\n    vertCoords    = inPosition;\n\n    int mode = uFitMode;\n    float dstAspect = uViewportSize.x / uViewportSize.y;\n    float srcAspect = uSrcAspect;\n\n    if (mode == 3) {\n        float arDiff =\n            (srcAspect > dstAspect ? srcAspect / dstAspect : dstAspect / srcAspect)\n            - 1.0;\n        if (arDiff <= 0.1) {\n            mode = 0;\n        } else if (srcAspect <= dstAspect) {\n            mode = 1;\n        } else {\n            mode = 2;\n        }\n    }\n\n    float x0 = 0.0;\n    float y0 = 0.0;\n    float x1 = 1.0;\n    float y1 = 1.0;\n\n    vec2 uv = inTexCoords;\n    if (mode == 1) {\n        // Letterbox: compute content rect and map UVs within it.\n        if (srcAspect > dstAspect) {\n            float h = dstAspect / srcAspect;\n            y0 = (1.0 - h) * 0.5;\n            y1 = y0 + h;\n        } else {\n            float w = srcAspect / dstAspect;\n            x0 = (1.0 - w) * 0.5;\n            x1 = x0 + w;\n        }\n\n        uv = (vertCoords - vec2(x0, y0)) / vec2(x1 - x0, y1 - y0);\n    } else if (mode == 2) {\n        // Crop: keep full screen coverage, but zoom the UVs.\n        if (srcAspect < dstAspect) {\n            float h = dstAspect / srcAspect;\n            float visible = 1.0 / h;\n            float v0 = (1.0 - visible) * 0.5;\n            uv.y = v0 + uv.y * visible;\n        } else {\n            float w = srcAspect / dstAspect;\n            float visible = 1.0 / w;\n            float u0 = (1.0 - visible) * 0.5;\n            uv.x = u0 + uv.x * visible;\n        }\n    }\n\n    vertMappedUv = uv;\n    vertContentRect = vec4(x0, y0, x1, y1);\n}\n\n#elif defined(FRAGMENT)\n\nuniform sampler2D uTexMain;\nuniform vec4 uTexSize;\n\nin vec2 vertCoords;\nin float vertLight;\nin vec2 vertMappedUv;\nin vec4 vertContentRect;\nout vec4 outColor;\n\nvoid main(void) {\n    if (vertCoords.x < vertContentRect.x || vertCoords.x > vertContentRect.z\n        || vertCoords.y < vertContentRect.y || vertCoords.y > vertContentRect.w) {\n        // Outside the content rect: force opaque black so nothing bleeds.\n        outColor = vec4(0.0, 0.0, 0.0, 1.0);\n        return;\n    }\n\n    vec2 uv = clampTexAtlas(vertMappedUv, uTexSize);\n    outColor = texture(uTexMain, uv);\n\n    if ((uEffect & EFFECT_WAVE) != 0) {\n        outColor.rgb *= vertLight;\n    } else if ((uEffect & EFFECT_VIGNETTE) != 0) {\n        float x_dist = vertCoords.x - 0.5;\n        float y_dist = vertCoords.y - 0.5;\n        float lightV = 256.0 - sqrt(x_dist * x_dist + y_dist * y_dist) * 300.0;\n        lightV = clamp(lightV, 0.0, 255.0) / 255.0;\n        outColor *= vec4(lightV, lightV, lightV, 1.0);\n    }\n\n    if (uDesaturation > 0.0) {\n        float luma = dot(outColor.rgb, vec3(0.299, 0.587, 0.114));\n        outColor.rgb = mix(outColor.rgb, vec3(luma), clamp(uDesaturation, 0.0, 1.0));\n    }\n\n    outColor.rgb *= uUIBrightnessMultiplier * uBrightnessScale;\n\n    outColor.a *= clamp(uOpacity, 0.0, 1.0);\n    // Output premultiplied alpha so callers can use (ONE, ONE_MINUS_SRC_ALPHA).\n    outColor.rgb *= outColor.a;\n}\n\n#endif\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/billboard.glsl",
    "content": "#define BILLBOARD_LOCK_NONE        0\n#define BILLBOARD_LOCK_ROLL        1\n#define BILLBOARD_LOCK_ROLL_PITCH  2\n#define BILLBOARD_LOCK_PERSPECTIVE 3\n\nvec4 offsetBillboard(vec3 pos, vec2 disp, mat4 view, mat4 model, mat4 proj, int mode)\n{\n    vec3 right, up;\n    if (mode == BILLBOARD_LOCK_NONE) {\n        right = normalize(vec3(view[0][0], view[1][0], view[2][0]));\n        up    = normalize(vec3(view[0][1], view[1][1], view[2][1]));\n    } else {\n        // Base forward for all locked modes\n        vec3 forward = -normalize(vec3(view[0][2], view[1][2], view[2][2]));\n        const vec3 worldUp = vec3(0,1,0);\n\n        if (mode != BILLBOARD_LOCK_ROLL) {\n            // Kill pitch if requested by any cylindrical/perspective mode\n            forward = normalize(vec3(forward.x, 0.0, forward.z));\n        }\n\n        if (mode == BILLBOARD_LOCK_PERSPECTIVE) {\n            vec4 clip     = proj * view * model * vec4(pos,1);\n            float ndcX    = clip.x / clip.w;\n            vec3 yawRight = normalize(cross(forward, worldUp));\n            float inv     = inversesqrt(1.0 + ndcX * ndcX);\n            forward       = normalize(inv * forward - (ndcX * inv) * yawRight);\n        }\n\n        right = normalize(cross(forward, worldUp));\n        up    = normalize(cross(right, forward));\n    }\n\n    vec4 wp = model * vec4(pos,1);\n    wp.xyz += disp.x * right + disp.y * up;\n    return view * wp;\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/common.glsl",
    "content": "#define PI 3.1415926538\n\n#define WALL_L      1024\n#define WIBBLE_SIZE 32\n#define MAX_WIBBLE  2\n\n#define SHADE_NEUTRAL  0x1000\n#define SHADE_MAX      0x1FFF\n#define SHADE_CAUSTICS 0x300\n\n#define VERT_NO_WIBBLE         0x0001u\n#define VERT_FLAT_SHADED       0x0002u\n#define VERT_REFLECTIVE        0x0004u\n#define VERT_NO_LIGHTING       0x0008u\n#define VERT_BILLBOARD         0x0010u\n#define VERT_ABS_SPRITE        0x0020u\n#define VERT_NO_ALPHA_DISCARD  0x0040u\n#define VERT_USE_DYNAMIC_LIGHT 0x0080u\n#define VERT_USE_OBJECT_LIGHT  0x0100u\n#define VERT_USE_OWN_LIGHT     0x0200u\n#define VERT_MOVE              0x0400u\n#define VERT_GLOW              0x0800u\n\n#define LIGHTING_CONTRAST_LOW    0\n#define LIGHTING_CONTRAST_MEDIUM 1\n#define LIGHTING_CONTRAST_HIGH   2\n\nlayout(std140) uniform Globals {\n    vec4 uGlobalTint;\n    vec4 uFogColor;\n    vec2 uFogDistance; // x = fog start, y = fog end\n    vec2 uViewportSize;\n    float uTime;\n    float uTimeInGame;\n    float uBrightnessMultiplier;\n    float uUIBrightnessMultiplier;\n    float uGamma;\n    float uDesaturation;\n    float uSunsetDuration;\n    float uMinShade;\n    int uBillboardLockMode;\n    int uLightingEnabled; // bool\n    int uTrapezoidFilterEnabled; // bool\n    int uReflectionsEnabled; // bool\n    int uTexturesEnabled; // bool\n    int uTRVersion;\n};\n\nlayout(std140) uniform Matrices {\n    mat4 uMatProj;\n    mat4 uMatView;\n};\n\nvec2 clampTexAtlas(vec2 uv, vec4 atlasSize)\n{\n    float epsilon = 0.5 / 256.0;\n    return clamp(uv, atlasSize.xy + epsilon, atlasSize.zw - epsilon);\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/fbo.glsl",
    "content": "#ifdef VERTEX\n\nlayout(location = 0) in vec2 inPosition;\n\nout vec2 vertTexCoords;\n\nvoid main(void) {\n    vertTexCoords = inPosition;\n    gl_Position = vec4(vertTexCoords * vec2(2.0, 2.0) + vec2(-1.0, -1.0), 0.0, 1.0);\n}\n\n#elif defined(FRAGMENT)\n\nuniform sampler2D uTex0;\n\nin vec2 vertTexCoords;\nout vec4 outColor;\n\nvoid main(void) {\n    outColor = texture(uTex0, vertTexCoords);\n}\n\n#endif\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/lights.glsl",
    "content": "#define MAX_LIGHTS 32\n\n#define RLM_NORMAL  0\n#define RLM_FLICKER 1\n#define RLM_GLOW    2\n#define RLM_SUNSET  3\n\nuniform int uWaterEffect;\nuniform vec3 uWaterEffectParams; // x=choppy amp, y=shimmer amp, z=abs intensity\n\nstruct Light {\n    vec4 pos;\n    vec4 color;\n    float shade;\n    float falloff;\n    float kind;\n    float _pad0;\n};\n\nlayout(std140) uniform Lights {\n    int uNumLights;\n    int uRoomLightMode;\n    Light uLights[MAX_LIGHTS];\n};\n\nlayout(std140) uniform LightSource {\n    float uLightAdder;\n    float uLightDivider;\n    vec4 uLightVectorSource;\n    vec4 uTR3Ambient;\n    vec4 uTR3LightDirView[3];\n    vec4 uTR3LightColor[3];\n};\n\nfloat ogPhaseTurns(vec3 worldPos, int scheme)\n{\n    // bucket like OG: xyz * (1/64, 1/64, 1/128)\n    ivec3 q = ivec3(floor(vec3(worldPos.x / 64.0,\n                               worldPos.y / 64.0,\n                               worldPos.z / 128.0)));\n\n    // cheap hash -> 0..1\n    float n = fract(sin(\n        dot(vec3(q), vec3(12.9898, 78.233, 37.719)) + float(scheme) * 19.19\n    ) * 43758.5453);\n\n    // OG-ish: random is multiples of 4, then &63 => 16 lanes\n    float lane = floor(n * 16.0);  // 0..15\n    float offTurns = lane / 16.0;  // 0,1/16,...15/16\n\n    // time base is uTimeInGame with period 64\n    float tTurns = fract(uTimeInGame / 64.0);\n    return tTurns + offTurns;\n}\n\nfloat effectChoppy(vec3 worldPos)\n{\n    int scheme = clamp(uWaterEffect - 2, 0, 21);\n    float angle = fract(ogPhaseTurns(worldPos, scheme)) * 2 * PI;\n    return -sin(angle) * uWaterEffectParams.x / 2.0;\n}\n\nfloat effectShimmer(vec3 worldPos)\n{\n    int scheme = clamp(uWaterEffect - 2, 0, 21);\n    float angle = fract(ogPhaseTurns(worldPos, scheme)) * 2 * PI;\n    return sin(angle) * uWaterEffectParams.y * 8.0;\n}\n\nfloat effectAbs()\n{\n    return uWaterEffectParams.z * 8.0;\n}\n\nint lightFlicker(float t) {\n    float h = fract(sin(t * 593.123) * 43758.5453);\n    return int(h * 32.0);\n}\n\nint lightGlow(float time) {\n    float phase = mod(time, 32.0) / 32.0;\n    float s = sin(phase * 2 * PI);\n    float normalized = (s + 1.0) * 0.5;\n    return int(normalized * 31.0);\n}\n\nint lightSunset(float time) {\n    float sunsetProgress = clamp(time / max(1, uSunsetDuration), 0.0, 1.0);\n    return int(sunsetProgress * 31.0);\n}\n\nint calcRoomShadeIndex(int mode, float time)\n{\n    if (mode == RLM_FLICKER) {\n        return lightFlicker(time);\n    }\n    if (mode == RLM_GLOW) {\n        return lightGlow(time);\n    }\n    if (mode == RLM_SUNSET) {\n        return lightSunset(time);\n    }\n    return 0;\n}\n\nfloat lightRoom(\n    int lightMode, float time, float vertexPhase)\n{\n    int i = calcRoomShadeIndex(lightMode, time);\n    float j = float(int(vertexPhase) & 31);\n\n    const float MAX_UNIT = 512.0;\n    return (j - 16.0) * float(i) * MAX_UNIT / 31.0;\n}\n\nfloat lightWaterCaustics(float shade, vec3 vtxPos)\n{\n    float time = mod(float(uTimeInGame), float(WIBBLE_SIZE));\n    // just a random offset based on the source vertex\n    float caustic = fract(sin(dot(vtxPos.xyz, vec3(12.9898, 78.233, 37.719))) * 43758.5453);\n    caustic = (caustic * 1023.0) - 511.0;\n    float angle = radians(360.0 * mod((time + caustic) / float(WIBBLE_SIZE), 1.0));\n    return clamp(shade + sin(angle) * float(SHADE_CAUSTICS), 0.0, float(SHADE_MAX));\n}\n\nvec3 safeNormalize(vec3 v)\n{\n    float len2 = dot(v, v);\n    if (len2 <= 0.0) {\n        return vec3(0.0);\n    }\n    return v * inversesqrt(len2);\n}\n\nfloat lightObjects(vec3 rawNormal, vec4 vertexPos)\n{\n    float lightAdder = uLightAdder;\n    if (uLightDivider != 0) {\n        vec3 L = mat3(transpose(uMatView * uMatModel)) * uLightVectorSource.xyz / uLightDivider;\n        lightAdder += dot(L, rawNormal.xyz / (1 << 14)) / 4;\n        lightAdder = clamp(lightAdder, 0, SHADE_MAX);\n    }\n    return lightAdder;\n}\n\nvec3 lightObjectsTR3(vec3 rawNormal)\n{\n    vec3 N = safeNormalize(mat3(uMatView * uMatModel) * (rawNormal.xyz / float(1 << 14)));\n\n    vec3 L0 = uTR3LightDirView[0].xyz;\n    vec3 L1 = uTR3LightDirView[1].xyz;\n    vec3 L2 = uTR3LightDirView[2].xyz;\n\n    float d0 = max(dot(N, L0), 0.0);\n    float d1 = max(dot(N, L1), 0.0);\n    float d2 = max(dot(N, L2), 0.0);\n\n    vec3 rgb = uTR3Ambient.rgb\n        + uTR3LightColor[0].rgb * d0\n        + uTR3LightColor[1].rgb * d1\n        + uTR3LightColor[2].rgb * d2;\n    return clamp(rgb, 0.0, 1.0);\n}\n\nvec3 lightOwnTR3(float shade)\n{\n    float shade8 = floor((SHADE_MAX - shade) / 32.0); // (0x1FFF - shade) >> 5\n    shade8 = (shade8 <= 0.0) ? 255.0 : shade8;\n    return clamp(uTR3Ambient.rgb * (shade8 / 255.0), 0.0, 1.0);\n}\n\nfloat lightDynamicTR12Lum(float baseLight, vec4 vertexPos)\n{\n    float lightAdder = baseLight;\n    for (int i = 0; i < uNumLights; i++) {\n        if (uLights[i].kind != 0.0) {\n            continue;\n        }\n        vec3 dist = uLights[i].pos.xyz - vertexPos.xyz;\n        float radius = exp2(uLights[i].falloff);\n        float distSq = dot(dist, dist);\n        if (distSq > radius * radius) {\n            continue;\n        }\n\n        float maxShade = exp2(uLights[i].shade);\n        float distTerm = distSq / exp2(2 * uLights[i].falloff - uLights[i].shade);\n        float shade = maxShade - distTerm;\n        lightAdder -= shade;\n    }\n    return max(lightAdder, 0);\n}\n\nvec3 lightDynamicTR12RGB(vec4 vertexPos)\n{\n    vec3 add = vec3(0.0);\n    for (int i = 0; i < uNumLights; i++) {\n        if (uLights[i].kind == 0.0) {\n            continue;\n        }\n\n        float radius = uLights[i].falloff * 0.5;\n        vec3 dist = uLights[i].pos.xyz - vertexPos.xyz;\n        float distSq = dot(dist, dist);\n        float radiusSq = radius * radius;\n        if (distSq > radiusSq) {\n            continue;\n        }\n\n        float d = sqrt(distSq);\n        float factor = (radius - d) / max(radius, 1.0);\n        add += factor * uLights[i].color.rgb;\n    }\n    return add;\n}\n\nvec3 lightDynamicTR3(vec4 vertexPos)\n{\n    vec3 add = vec3(0.0);\n    for (int i = 0; i < uNumLights; i++) {\n        float radius = uLights[i].falloff * 0.5; // falloff_raw >> 1\n        vec3 dist = uLights[i].pos.xyz - vertexPos.xyz;\n        float distSq = dot(dist, dist);\n        float radiusSq = radius * radius;\n        if (distSq > radiusSq) {\n            continue;\n        }\n\n        float d = sqrt(distSq);\n        float factor = (radius - d) / max(radius, 1.0);\n        add += factor * uLights[i].color.rgb;\n    }\n    return add;\n}\n\nfloat getDynamicLightContrastMul()\n{\n    // `uMinShade` is configured via the \"lighting contrast\" option.\n    // For TR1/TR2 it clamps the minimum shade; in TR3 the lighting is additive,\n    // so we remap it to a multiplier:\n    // LOW: uMinShade = SHADE_NEUTRAL -> 1.0\n    // MED: uMinShade = SHADE_HIGH    -> 1.5\n    // HIGH:uMinShade = 0             -> 2.0\n    return clamp(2.0 - (uMinShade / float(SHADE_NEUTRAL)), 1.0, 2.0);\n}\n\nfloat lightLumTR12(float shade, uint flags, vec3 normal, vec4 pos, float phase)\n{\n    if ((flags & VERT_USE_OWN_LIGHT) != 0u) {\n        shade = uLightAdder + shade;\n    } else if ((flags & VERT_USE_OBJECT_LIGHT) != 0u) {\n        shade = lightObjects(normal, pos);\n    } else {\n        if ((flags & VERT_USE_DYNAMIC_LIGHT) != 0u) {\n            shade = lightDynamicTR12Lum(shade, pos);\n            shade += lightRoom(uRoomLightMode, uTimeInGame, phase);\n        }\n        shade = clamp(shade, 0, SHADE_MAX);\n    }\n\n    if (uWaterEffect == 1) {\n        shade = lightWaterCaustics(shade, pos.xyz);\n    }\n\n    return shade;\n}\n\nstruct LightingResult {\n    float shade; // used only for TR1-2\n    vec3 add; // TR3: additive light (dynamic + post effects)\n    vec3 mul; // TR3: multiplicative light (object/own)\n};\n\nLightingResult light(\n    float shade, uint flags, vec3 normal, vec4 pos,\n    float vertexPhase)\n{\n    LightingResult result;\n    result.shade = SHADE_NEUTRAL;\n    result.add = vec3(0.0);\n    result.mul = vec3(1.0);\n\n    if (uLightingEnabled == 0) {\n        return result;\n    }\n    if ((flags & VERT_NO_LIGHTING) != 0u) {\n        return result;\n    }\n\n#if TR_VERSION >= 3\n    if ((flags & VERT_USE_DYNAMIC_LIGHT) != 0u) {\n        result.add += lightDynamicTR3(pos) * getDynamicLightContrastMul();\n    }\n\n    if ((flags & VERT_USE_OBJECT_LIGHT) != 0u) {\n        result.mul *= lightObjectsTR3(normal);\n    } else if ((flags & VERT_USE_OWN_LIGHT) != 0u) {\n        result.mul *= lightOwnTR3(shade);\n    }\n\n    float add = 0.0;\n    if ((flags & VERT_MOVE) != 0u) {\n        add += effectChoppy(pos.xyz) / 256.0;\n    }\n    if ((flags & VERT_GLOW) != 0u) {\n        add += effectShimmer(pos.xyz) / 256.0;\n        add += effectAbs() / 256.0;\n    }\n    result.add += vec3(add);\n\n    result.shade = SHADE_NEUTRAL;\n#else\n    result.shade = lightLumTR12(shade, flags, normal, pos, vertexPhase);\n    if ((flags & VERT_USE_DYNAMIC_LIGHT) != 0u) {\n        result.add += lightDynamicTR12RGB(pos) * getDynamicLightContrastMul();\n    }\n#endif\n\n    return result;\n}\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/meshes.glsl",
    "content": "#include \"common.glsl\"\n\n#ifdef VERTEX\n\nuniform mat4 uMatModel;\nuniform bool uWibbleEffect;\n\n#include \"billboard.glsl\"\n#include \"lights.glsl\"\n\nlayout(location = 0) in vec4 inPosition;\nlayout(location = 1) in vec4 inNormal;\nlayout(location = 2) in vec3 inUVW;\nlayout(location = 3) in vec4 inTextureSize;\nlayout(location = 4) in vec2 inTrapezoidRatios;\nlayout(location = 5) in uint inFlags;\nlayout(location = 6) in vec4 inColor;\nlayout(location = 7) in float inShade;\n\nout vec4 gEyePos;\nout vec3 gNormal;\nflat out uint gFlags;\nflat out int gTexLayer;\nout vec2 gTexUV;\nflat out vec4 gAtlasSize;\nout vec2 gTrapezoidRatios;\nout float gShade;\nout vec4 gColor;\n\nvec3 gammaCurve(vec3 rgb, float gamma_exp)\n{\n    return pow(clamp(rgb, 0.0, 1.0), vec3(gamma_exp));\n}\n\nvec3 waterWibble(vec4 worldPosition, vec4 screenPosition)\n{\n    vec3 ndc = screenPosition.xyz / screenPosition.w;\n    vec2 pixelPos = (ndc.xy * 0.5 + 0.5) * uViewportSize;\n#if TR_VERSION == 3\n    float phases = (uTimeInGame * 0.5 + length(worldPosition.xyz)) * (2.0 * PI / WIBBLE_SIZE);\n    float scale = length(uViewportSize) / length(vec2(640.0, 480.0));\n    float adjustedWibble = scale;\n    pixelPos.y += sin(phases) * adjustedWibble;\n#else\n    float phases = (uTimeInGame + length(worldPosition.xyz)) * (2.0 * PI / WIBBLE_SIZE);\n    pixelPos.x += sin(phases) * MAX_WIBBLE;\n    pixelPos.y += cos(phases) * MAX_WIBBLE;\n#endif\n    // reverse transform\n    ndc.xy = (pixelPos / uViewportSize - 0.5) * 2.0;\n    return ndc * screenPosition.w;\n}\n\nvoid main(void) {\n    vec4 worldPos = uMatModel * vec4(inPosition.xyz, 1.0);\n\n    if ((inFlags & VERT_MOVE) != 0u) {\n        float waterMul = (uWaterEffect != 0) ? 1.0 : 0.0;\n        worldPos.y += effectChoppy(worldPos.xyz) * waterMul;\n    }\n\n    if ((inFlags & (VERT_ABS_SPRITE | VERT_BILLBOARD)) != 0u) {\n        int lockMode = (inFlags & VERT_ABS_SPRITE) != 0u ? BILLBOARD_LOCK_NONE : uBillboardLockMode;\n        gEyePos = offsetBillboard(inPosition.xyz, inNormal.xy, uMatView, uMatModel, uMatProj, lockMode);\n    } else {\n        gEyePos = uMatView * worldPos;\n    }\n\n    gNormal = inNormal.xyz;\n    gl_Position = uMatProj * gEyePos;\n    gl_Position.z += inPosition.w;\n\n    // Apply water wibble effect only to non-sprite vertices\n    if (uWibbleEffect && (inFlags & (VERT_NO_WIBBLE | VERT_BILLBOARD)) == 0u) {\n        gl_Position.xyz = waterWibble(worldPos, gl_Position);\n    }\n\n    gFlags = inFlags;\n    gAtlasSize = inTextureSize;\n    gTexLayer = (uTexturesEnabled != 0) && (gFlags & VERT_FLAT_SHADED) == 0u ? int(inUVW.z) : -1;\n    gTrapezoidRatios = inTrapezoidRatios;\n    gTexUV = inUVW.xy;\n    if (uTrapezoidFilterEnabled != 0) {\n        gTexUV *= inTrapezoidRatios;\n    }\n\n    // The vertex diffuse is lit first and then modulated by the texture (or by\n    // the flat polygon's palette color). Keep the lighting component separate\n    // from the base color so gamma is applied in the right place.\n    LightingResult lr =\n        light(inShade, gFlags, inNormal.xyz, worldPos, inNormal.w);\n    gShade = lr.shade;\n\n    float gamma_exp = 1.0 / ((uGamma / 10.0) * 4.0);\n\n#if TR_VERSION >= 3\n    vec3 lightIn;\n    vec3 modulate;\n    if ((gFlags & VERT_FLAT_SHADED) == 0u) {\n        if (uLightingEnabled == 0) {\n            lightIn = vec3(1);\n        } else {\n            lightIn = inColor.rgb;\n        }\n        modulate = vec3(1);\n    } else {\n        lightIn = vec3(1);\n        modulate = inColor.rgb;\n    }\n\n    // Combine lighting in linear-ish space first: (base + add) * mul\n    vec3 lit = clamp(lightIn + lr.add, 0.0, 1.0);\n    lit *= lr.mul;\n    lit = gammaCurve(lit, gamma_exp);\n\n    // Apply flat shading AFTER modulation\n    gColor = vec4(lit * modulate, inColor.a);\n#else\n    float shade_mul = 1.0;\n    if ((gFlags & VERT_NO_LIGHTING) == 0u) {\n        shade_mul = (2.0 - (max(gShade, uMinShade) / SHADE_NEUTRAL));\n    }\n\n    // `shade_mul` is roughly in [0..2]. Remap to [0..1], apply the gamma\n    // curve, and restore the range. Use sqrt() to limit the effect scope,\n    // since we're applying it to the shade (TR1-2) rather than RGB (TR3).\n    vec3 mul = gammaCurve(vec3(shade_mul * 0.5), sqrt(gamma_exp)) * 2.0;\n\n    gColor = inColor;\n    if ((gFlags & VERT_FLAT_SHADED) == 0u) {\n        gColor.rgb = gammaCurve(gColor.rgb, gamma_exp);\n    }\n    gColor.rgb *= mul;\n    // Preserve the >1.0 lighting range until after texturing so TR1/TR2\n    // high contrast can still brighten textured geometry.\n    gColor.rgb += lr.add;\n#endif\n}\n\n#elif defined(FRAGMENT)\n\nuniform sampler2DArray uTexAtlas;\nuniform sampler2D uTexEnvMap;\nuniform vec3 uTint;\nuniform bool uDiscardAlpha;\n\nin vec4 gEyePos;\nin vec3 gNormal;\nflat in uint gFlags;\nflat in int gTexLayer;\nin vec2 gTexUV;\nflat in vec4 gAtlasSize;\nin float gShade;\nin vec4 gColor;\nin vec2 gTrapezoidRatios;\nout vec4 outColor;\n\nvec4 applyFog(vec4 color, float dist)\n{\n    float fogFactor = clamp(\n        (dist - uFogDistance.x) / (uFogDistance.y - uFogDistance.x), 0.0, 1.0);\n    return mix(color, uFogColor, fogFactor);\n}\n\nvoid main(void) {\n    vec4 texColor = gColor;\n\n    // Texturing and base color\n    if (gTexLayer >= 0) {\n        vec3 texCoords = vec3(gTexUV.x, gTexUV.y, gTexLayer);\n        if (uTrapezoidFilterEnabled != 0) {\n            texCoords.xy /= gTrapezoidRatios;\n        }\n        texCoords.xy = clampTexAtlas(texCoords.xy, gAtlasSize);\n        texColor *= texture(uTexAtlas, texCoords);\n    } else {\n        texColor.rgb *= texColor.a;\n    }\n\n    // Alpha discard - chroma keying || transparent pixels in the opaque pass\n    if (texColor.a <= 0.0\n        || (uDiscardAlpha && texColor.a < 0.99\n            && (gFlags & VERT_NO_ALPHA_DISCARD) == 0u)) {\n        discard;\n    }\n\n    // Reflections\n    if ((gFlags & VERT_REFLECTIVE) != 0u && uReflectionsEnabled != 0) {\n        vec2 env_uv = (normalize(gNormal) * 0.5 + 0.5).xy;\n        env_uv.y = 1.0 - env_uv.y;\n        texColor *= texture(uTexEnvMap, env_uv) * 2;\n    }\n\n    // Fog\n    if ((gFlags & VERT_NO_LIGHTING) == 0u && uLightingEnabled != 0) {\n        texColor = applyFog(texColor, length(gEyePos.xyz));\n    }\n\n    texColor.rgb *= uBrightnessMultiplier;\n    texColor.rgb *= uTint;\n\n    // Optional desaturation (0 = original, 1 = monochrome).\n    if (uDesaturation > 0.0) {\n        const vec3 luma = vec3(0.2126, 0.7152, 0.0722);\n        float y = dot(texColor.rgb, luma) * 0.5;\n        texColor.rgb = mix(texColor.rgb, vec3(y), clamp(uDesaturation, 0.0, 1.0));\n    }\n\n    texColor *= uGlobalTint;\n\n    outColor = texColor;\n}\n\n#endif\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/meshes_tr12.glsl",
    "content": "#define TR_VERSION 2\n#include \"meshes.glsl\"\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/meshes_tr3.glsl",
    "content": "#define TR_VERSION 3\n#include \"meshes.glsl\"\n"
  },
  {
    "path": "data/trx/ship/cfg/shaders/ui.glsl",
    "content": "#include \"common.glsl\"\n\n#ifdef VERTEX\n\nlayout(location = 0) in vec4 inPosition;\nlayout(location = 1) in vec3 inUVW;\nlayout(location = 2) in vec4 inTextureSize;\nlayout(location = 3) in uint inFlags;\nlayout(location = 4) in vec4 inColor;\n\nout vec3 gNormal;\nflat out uint gFlags;\nflat out int gTexLayer;\nout vec2 gTexUV;\nflat out vec4 gAtlasSize;\nout vec4 gColor;\n\nvoid main(void) {\n    gl_Position = uMatProj * uMatView * vec4(inPosition.xyz, 1.0);\n    gFlags = inFlags;\n    gAtlasSize = inTextureSize;\n    gTexUV = inUVW.xy;\n    gTexLayer = int(inUVW.z);\n    gColor = inColor;\n}\n\n#elif defined(FRAGMENT)\n\nuniform sampler2DArray uTexAtlas;\n\nflat in uint gFlags;\nflat in int gTexLayer;\nin vec2 gTexUV;\nflat in vec4 gAtlasSize;\nin vec4 gColor;\nout vec4 outColor;\n\nvoid main(void) {\n    vec4 texColor = gColor;\n\n    if ((gFlags & VERT_FLAT_SHADED) == 0u && gTexLayer >= 0) {\n        vec3 texCoords = vec3(gTexUV.x, gTexUV.y, gTexLayer);\n        texCoords.xy = clampTexAtlas(texCoords.xy, gAtlasSize);\n        texColor *= texture(uTexAtlas, texCoords);\n        if (texColor.a <= 0.0) {\n            discard;\n        }\n    } else {\n        texColor.rgb *= texColor.a;\n    }\n\n    texColor.rgb *= uUIBrightnessMultiplier;\n    outColor = texColor;\n}\n\n#endif\n"
  },
  {
    "path": "data/trx/ship/cfg/ui.json5",
    "content": "{\n  \"bars\": {\n    \"tr1_pc\": {\n      \"name_gs\": \"dynamic/enums/bar_look/tr1_pc\",\n      \"scale\": 1,\n      \"style\": \"pc\",\n      \"border_light\": \"#353535\",\n      \"border_dark\": \"#353535\",\n      \"colors\": {\n        \"red\":    [\"#A0281C\", \"#B82C20\", \"#A0281C\", \"#7C2020\", \"#541420\"],\n        \"blue\":   [\"#3d717b\", \"#65929a\", \"#3d717b\", \"#1f5d6b\", \"#004a5b\"],\n        \"grey\":   [\"#586458\", \"#748474\", \"#586458\", \"#4C504C\", \"#303030\"],\n        \"brown\":  [\"#7c5e25\", \"#a1823c\", \"#7c5e25\", \"#644613\", \"#4c2e02\"],\n        \"silver\": [\"#969696\", \"#E6E6E6\", \"#C8C8C8\", \"#8C8C8C\", \"#646464\"],\n        \"teal\":   [\"#14be6e\", \"#1ee682\", \"#14be6e\", \"#0f964b\", \"#0a6e28\"],\n        \"yellow\": [\"#b9b723\", \"#d6d629\", \"#b9b723\", \"#9b981e\", \"#7e7218\"],\n        \"cyan\":   [\"#20b3bc\", \"#25d1da\", \"#20b3bc\", \"#1b949e\", \"#166f80\"],\n        \"pink\":   [\"#DC8CAA\", \"#FF96C8\", \"#D282A0\", \"#A56478\", \"#783C46\"],\n        \"purple\": [\"#562484\", \"#682d9f\", \"#562484\", \"#492070\", \"#3c195c\"],\n        \"green\":  [\"#239117\", \"#33b020\", \"#239117\", \"#217210\", \"#23540b\"],\n        \"iron\":   [\"#475e76\", \"#5a748a\", \"#475e76\", \"#374e60\", \"#29414b\"],\n        \"orange\": [\"#a86015\", \"#c66b1e\", \"#a86015\", \"#88440f\", \"#6a260a\"]\n      }\n    },\n    \"tr2_pc\": {\n      \"name_gs\": \"dynamic/enums/bar_look/tr2_pc\",\n      \"scale\": 0.75,\n      \"style\": \"pc\",\n      \"border_light\": \"#FFFFFF\",\n      \"border_dark\": \"#404040\",\n      \"colors\": {\n        \"red\":    [\"#e40c10\", \"#e86c04\", \"#e40c10\", \"#e40c10\", \"#e40c10\"],\n        \"blue\":   [\"#1c10f4\", \"#fcfcfc\", \"#1c10f4\", \"#1c10f4\", \"#1c10f4\"],\n        \"grey\":   [\"#4C504C\", \"#A0A0A0\", \"#4C504C\", \"#4C504C\", \"#4C504C\"],\n        \"brown\":  [\"#80481c\", \"#a88044\", \"#80481c\", \"#80481c\", \"#80481c\"],\n        \"silver\": [\"#969696\", \"#E6E6E6\", \"#969696\", \"#969696\", \"#969696\"],\n        \"teal\":   [\"#00a05d\", \"#1ee62f\", \"#00a05d\", \"#00a05d\", \"#00a05d\"],\n        \"yellow\": [\"#b5a000\", \"#ffff1f\", \"#b5a000\", \"#b5a000\", \"#b5a000\"],\n        \"cyan\":   [\"#00d6dc\", \"#00fbff\", \"#00b1b9\", \"#00b1b9\", \"#00b1b9\"],\n        \"pink\":   [\"#db649c\", \"#ecaab0\", \"#db649c\", \"#db649c\", \"#db649c\"],\n        \"purple\": [\"#461E6B\", \"#8040ff\", \"#461E6B\", \"#461E6B\", \"#461E6B\"],\n        \"green\":  [\"#37aa0b\", \"#08e713\", \"#37aa0b\", \"#37aa0b\", \"#37aa0b\"],\n        \"iron\":   [\"#607088\", \"#fcfcfc\", \"#607088\", \"#607088\", \"#607088\"],\n        \"orange\": [\"#cc580b\", \"#cf8f04\", \"#cc580b\", \"#cc580b\", \"#cc580b\"]\n      }\n    },\n    \"tr3_pc\": {\n      \"name_gs\": \"dynamic/enums/bar_look/tr3_pc\",\n      \"scale\": 0.75,\n      \"style\": \"pc\",\n      \"border_light\": \"#FFFFFF\",\n      \"border_dark\": \"#404040\",\n      \"colors\": {\n        \"red\":    [\"#4F0000\", \"#FF0000\", \"#7F0000\", \"#7F0000\", \"#4F0000\"],\n        \"blue\":   [\"#00004F\", \"#0000FF\", \"#00007F\", \"#00007F\", \"#00004F\"],\n        \"grey\":   [\"#4F4F4F\", \"#7F7F7F\", \"#5F5F5F\", \"#5F5F5F\", \"#4F4F4F\"],\n        \"brown\":  [\"#3f2710\", \"#cb7d34\", \"#653e1a\", \"#653e1a\", \"#3f2710\"],\n        \"silver\": [\"#4F4F4F\", \"#BFBFBF\", \"#7F7F7F\", \"#7F7F7F\", \"#4F4F4F\"],\n        \"teal\":   [\"#004F2F\", \"#00FF7F\", \"#007F4F\", \"#007F4F\", \"#004F2F\"],\n        \"yellow\": [\"#4F4F00\", \"#FFFF00\", \"#7F7F00\", \"#7F7F00\", \"#4F4F00\"],\n        \"cyan\":   [\"#004F4F\", \"#00FFFF\", \"#007F7F\", \"#007F7F\", \"#004F4F\"],\n        \"pink\":   [\"#4F003F\", \"#FF7FBF\", \"#7F3F5F\", \"#7F3F5F\", \"#4F003F\"],\n        \"purple\": [\"#2F004F\", \"#7F00FF\", \"#4F007F\", \"#4F007F\", \"#2F004F\"],\n        \"green\":  [\"#004F00\", \"#00FF00\", \"#007F00\", \"#007F00\", \"#004F00\"],\n        \"iron\":   [\"#3d5164\", \"#93a9bd\", \"#4f6981\", \"#4f6981\", \"#3d5164\"],\n        \"orange\": [\"#783600\", \"#ff8929\", \"#a84c00\", \"#a84c00\", \"#783600\"]\n      }\n    },\n    \"tr2_ps1\": {\n      \"name_gs\": \"dynamic/enums/bar_look/tr2_ps1\",\n      \"scale\": 1,\n      \"style\": \"ps1\",\n      \"border_tl\": \"#508282\",\n      \"border_tr\": \"#9F9F9F\",\n      \"border_bl\": \"#294141\",\n      \"border_br\": \"#4F4F4F\",\n      \"colors\": {\n        \"red-green\":       [[\"#B60100\", \"#EA0100\", \"#B60100\", \"#870000\", \"#6D0000\"], [\"#01B900\", \"#03FA00\", \"#01B900\", \"#018800\", \"#015A00\"]],\n        \"dark-red-purple\": [[\"#400000\", \"#4C0000\", \"#580000\", \"#640000\", \"#7C0000\"], [\"#400080\", \"#4C0099\", \"#5800B2\", \"#6400CB\", \"#7C00FD\"]],\n        \"teal-green\":      [[\"#00717A\", \"#009298\", \"#00717A\", \"#005D6A\", \"#004A5A\"], [\"#007101\", \"#009201\", \"#007101\", \"#005D01\", \"#004A00\"]],\n        \"red-yellow\":      [[\"#C00100\", \"#F00100\", \"#C00100\", \"#900000\", \"#600000\"], [\"#C0B900\", \"#F0E800\", \"#C0B900\", \"#908A00\", \"#605A00\"]],\n        \"dark-blue-red\":   [[\"#000170\", \"#090091\", \"#000170\", \"#000053\", \"#00003E\"], [\"#C00100\", \"#F00100\", \"#C00100\", \"#900000\", \"#600000\"]],\n        \"orange-red\":      [[\"#B64C00\", \"#ED6400\", \"#B64C00\", \"#843700\", \"#6D2D00\"], [\"#C00100\", \"#F00100\", \"#C00100\", \"#900000\", \"#600000\"]],\n        \"yellow-green\":    [[\"#B78900\", \"#EEB300\", \"#B78900\", \"#846300\", \"#6D5200\"], [\"#01B900\", \"#03FA00\", \"#01B900\", \"#018800\", \"#015A00\"]],\n        \"purple-teal\":     [[\"#2F0030\", \"#5F0060\", \"#7E007F\", \"#5F0060\", \"#2F0030\"], [\"#002E30\", \"#015D60\", \"#017C7F\", \"#015D60\", \"#002E30\"]]\n      }\n    },\n    \"tr3_ps1\": {\n      \"name_gs\": \"dynamic/enums/bar_look/tr3_ps1\",\n      \"scale\": 1,\n      \"style\": \"ps1\",\n      \"border_tl\": \"#508282\",\n      \"border_tr\": \"#9F9F9F\",\n      \"border_bl\": \"#294141\",\n      \"border_br\": \"#4F4F4F\",\n      \"colors\": {\n        \"red-green\":       [[\"#3f0000\", \"#5f0000\", \"#7f0000\", \"#5f0000\", \"#3f0000\"], [\"#007e00\", \"#00bc00\", \"#00fb00\", \"#00bc00\", \"#007e00\"]],\n        \"dark-red-purple\": [[\"#400000\", \"#4C0000\", \"#580000\", \"#640000\", \"#7C0000\"], [\"#400080\", \"#4C0099\", \"#5800B2\", \"#6400CB\", \"#7C00FD\"]],\n        \"teal-green\":      [[\"#00717a\", \"#009299\", \"#00717a\", \"#005d6a\", \"#004a5a\"], [\"#007100\", \"#009202\", \"#007100\", \"#005d00\", \"#004a00\"]],\n        \"red-yellow\":      [[\"#900000\", \"#c00000\", \"#f00000\", \"#c00000\", \"#900000\"], [\"#908a00\", \"#c0b900\", \"#f0e800\", \"#c0b900\", \"#908a00\"]],\n        \"dark-blue-red\":   [[\"#00005f\", \"#00007f\", \"#00009f\", \"#00007f\", \"#00005f\"], [\"#5c0002\", \"#7c0002\", \"#9a0004\", \"#7c0002\", \"#5c0002\"]],\n        \"orange-red\":      [[\"#B64C00\", \"#ED6400\", \"#B64C00\", \"#843700\", \"#6D2D00\"], [\"#C00100\", \"#F00100\", \"#C00100\", \"#900000\", \"#600000\"]],\n        \"yellow-green\":    [[\"#B78900\", \"#EEB300\", \"#B78900\", \"#846300\", \"#6D5200\"], [\"#01B900\", \"#03FA00\", \"#01B900\", \"#018800\", \"#015A00\"]],\n        \"purple-teal\":     [[\"#2f0030\", \"#5f0060\", \"#7e007f\", \"#4f0050\", \"#1f0020\"], [\"#002e30\", \"#005e60\", \"#007d7f\", \"#004e50\", \"#001e20\"]]\n      }\n    }\n  },\n\n  \"ui\": {\n    // Menu appearance colors per TR version × UI style (pc/ps1).\n    // Selected at runtime by g_TRVersion and g_Config.ui.menu_style.\n    \"tr1\": {\n      \"pc\": {\n        // TS_BACKGROUND gradient: top color, bottom color\n        \"background\":       [\"#00000080\", \"#00000080\"],\n        // TS_BACKGROUND_HEAVY gradient: top color, bottom color\n        \"background_heavy\": [\"#000000E0\", \"#000000E0\"],\n        \"outline_light\":    \"#E8C070FF\",\n        \"outline_dark\":     \"#8C7038FF\"\n      },\n      \"ps1\": {\n        \"background_edge\":          \"#00000080\",\n        \"background_center\":        \"#00004080\",\n        \"background_heavy_edge\":    \"#000000E0\",\n        \"background_heavy_center\":  \"#000000E0\",\n        \"heading_edge\":             \"#00000080\",\n        \"heading_center\":           \"#80381080\",\n        \"requested_edge\":           \"#00000080\",\n        \"requested_center\":         \"#8038DC80\",\n        \"requested_outline_ch\":     \"#C8C8C8FF\",\n        \"requested_outline_cv\":     \"#C8C8C8FF\",\n        \"requested_outline_edge\":   \"#282828FF\",\n        \"outline_tl\":               \"#606060FF\",\n        \"outline_tr\":               \"#202020FF\",\n        \"outline_bl\":               \"#404040FF\",\n        \"outline_br\":               \"#000000FF\",\n        \"heading_outline\":          \"#000000FF\",\n      }\n    },\n    \"tr2\": {\n      \"pc\": {\n        // Note: TR2 uses O_TEXT_BOX for the outlines.\n        \"background\":       [\"#00000080\", \"#00000080\"],\n        \"background_heavy\": [\"#000000E0\", \"#000000E0\"],\n        \"outline_light\":    \"#FFFFFFFF\",\n        \"outline_dark\":     \"#404040FF\"\n      },\n      \"ps1\": {\n        \"background_edge\":          \"#00200080\",\n        \"background_center\":        \"#00600080\",\n        \"background_heavy_edge\":    \"#000000E0\",\n        \"background_heavy_center\":  \"#002000E0\",\n        \"heading_edge\":             \"#00000080\",\n        \"heading_center\":           \"#10803880\",\n        \"requested_edge\":           \"#00000080\",\n        \"requested_center\":         \"#38F08080\",\n        \"requested_outline_ch\":     \"#FFFFFFFF\",\n        \"requested_outline_cv\":     \"#38F080FF\",\n        \"requested_outline_edge\":   \"#000000FF\",\n        \"outline_tl\":               \"#606060FF\",\n        \"outline_tr\":               \"#202020FF\",\n        \"outline_bl\":               \"#404040FF\",\n        \"outline_br\":               \"#000000FF\",\n        \"heading_outline\":          \"#000000FF\",\n      }\n    },\n    \"tr3\": {\n      \"pc\": {\n        \"background\":       [\"#003FFF50\", \"#003F1F50\"],\n        \"background_heavy\": [\"#001020E0\", \"#000000E0\"],\n        \"outline_light\":    \"#4080C0FF\",\n        \"outline_dark\":     \"#001040FF\"\n      },\n      \"ps1\": {\n        \"background_edge\":          \"#30200080\",\n        \"background_center\":        \"#00600080\",\n        \"background_heavy_edge\":    \"#000000E0\",\n        \"background_heavy_center\":  \"#002000E0\",\n        \"heading_edge\":             \"#00000080\",\n        \"heading_center\":           \"#10803880\",\n        \"requested_edge\":           \"#00000080\",\n        \"requested_center\":         \"#38F08080\",\n        \"requested_outline_ch\":     \"#FFFFFFFF\",\n        \"requested_outline_cv\":     \"#38F080FF\",\n        \"requested_outline_edge\":   \"#000000FF\",\n        \"outline_tl\":               \"#606060FF\",\n        \"outline_tr\":               \"#202020FF\",\n        \"outline_bl\":               \"#404040FF\",\n        \"outline_br\":               \"#000000FF\",\n        \"heading_outline\":          \"#000000FF\",\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/catalog_item_actions.csv",
    "content": "0, ITEM_ACTION_TURN_180\n1, ITEM_ACTION_FLOOR_SHAKE\n2, ITEM_ACTION_LARA_NORMAL\n3, ITEM_ACTION_BUBBLES\n4, ITEM_ACTION_FINISH_LEVEL\n5, ITEM_ACTION_EARTHQUAKE\n6, ITEM_ACTION_FLOOD\n7, ITEM_ACTION_RAISING_BLOCK\n8, ITEM_ACTION_STAIRS_TO_SLOPE\n9, ITEM_ACTION_DROP_SAND\n10, ITEM_ACTION_POWER_UP\n11, ITEM_ACTION_EXPLOSION\n12, ITEM_ACTION_LARA_HANDS_FREE\n13, ITEM_ACTION_FLIP_MAP\n14, ITEM_ACTION_LARA_DRAW_RIGHT_GUN\n15, ITEM_ACTION_CHAIN_BLOCK\n16, ITEM_ACTION_FLICKER\n17, ITEM_ACTION_INVISIBILITY_ON\n18, ITEM_ACTION_SHADOW_ON\n19, ITEM_ACTION_SHADOW_OFF\n62, ITEM_ACTION_TURN_90\n"
  },
  {
    "path": "data/trx/ship/games/tr1/catalog_lara_anims.csv",
    "content": "0, LA_RUN\n1, LA_WALK_FORWARD\n2, LA_WALK_STOP_RIGHT\n3, LA_WALK_STOP_LEFT\n4, LA_WALK_TO_RUN_RIGHT\n5, LA_WALK_TO_RUN_LEFT\n6, LA_RUN_START\n7, LA_RUN_TO_WALK_RIGHT\n8, LA_RUN_TO_STAND_LEFT\n9, LA_RUN_TO_WALK_LEFT\n10, LA_RUN_TO_STAND_RIGHT\n11, LA_STAND_STILL\n12, LA_TURN_RIGHT_SLOW\n13, LA_TURN_LEFT_SLOW\n14, LA_JUMP_FORWARD_LAND_START_UNUSED\n15, LA_JUMP_FORWARD_LAND_END_UNUSED\n16, LA_RUN_JUMP_RIGHT_START\n17, LA_RUN_JUMP_RIGHT_CONTINUE\n18, LA_RUN_JUMP_LEFT_START\n19, LA_RUN_JUMP_LEFT_CONTINUE\n20, LA_WALK_FORWARD_START\n21, LA_WALK_FORWARD_START_CONTINUE\n22, LA_JUMP_FORWARD_TO_FREEFALL\n23, LA_FREEFALL\n24, LA_FREEFALL_LAND\n25, LA_FREEFALL_LAND_DEATH\n26, LA_STAND_TO_JUMP_UP\n27, LA_STAND_TO_JUMP_UP_CONTINUE\n28, LA_JUMP_UP\n29, LA_JUMP_UP_TO_HANG_UNUSED\n30, LA_JUMP_UP_TO_FREEFALL\n31, LA_JUMP_UP_LAND\n32, LA_SMASH_JUMP\n33, LA_SMASH_JUMP_CONTINUE\n34, LA_FALL_START\n35, LA_FALL\n36, LA_FALL_TO_FREEFALL\n37, LA_HANG_TO_FREEFALL\n38, LA_WALK_BACK_END_RIGHT\n39, LA_WALK_BACK_END_LEFT\n40, LA_WALK_BACK\n41, LA_WALK_BACK_START\n42, LA_CLIMB_3CLICK\n43, LA_CLIMB_3CLICK_END_TO_RUN\n44, LA_TURN_RIGHT\n45, LA_JUMP_FORWARD_TO_FREEFALL_2\n46, LA_REACH_TO_FREEFALL\n47, LA_ROLL_ALTERNATE\n48, LA_ROLL_END_ALTERNATE\n49, LA_JUMP_FORWARD_END_TO_FREEFALL\n50, LA_CLIMB_2CLICK\n51, LA_CLIMB_2CLICK_END\n52, LA_CLIMB_2CLICK_END_TO_RUN\n53, LA_WALL_SMASH_LEFT\n54, LA_WALL_SMASH_RIGHT\n55, LA_RUN_UP_STEP_RIGHT\n56, LA_RUN_UP_STEP_LEFT\n57, LA_WALK_UP_STEP_RIGHT\n58, LA_WALK_UP_STEP_LEFT\n59, LA_WALK_DOWN_LEFT\n60, LA_WALK_DOWN_RIGHT\n61, LA_WALK_DOWN_BACK_LEFT\n62, LA_WALK_DOWN_BACK_RIGHT\n63, LA_WALL_SWITCH_DOWN\n64, LA_WALL_SWITCH_UP\n65, LA_SIDE_STEP_LEFT\n66, LA_SIDE_STEP_LEFT_END\n67, LA_SIDE_STEP_RIGHT\n68, LA_SIDE_STEP_RIGHT_END\n69, LA_ROTATE_LEFT\n70, LA_SLIDE_FORWARD\n71, LA_SLIDE_FORWARD_END\n72, LA_SLIDE_FORWARD_STOP\n73, LA_STAND_TO_JUMP\n74, LA_JUMP_BACK_START\n75, LA_JUMP_BACK\n76, LA_JUMP_FORWARD_START\n77, LA_JUMP_FORWARD\n78, LA_JUMP_LEFT_START\n79, LA_JUMP_LEFT\n80, LA_JUMP_RIGHT_START\n81, LA_JUMP_RIGHT\n82, LA_LAND\n83, LA_JUMP_BACK_TO_FREEFALL\n84, LA_JUMP_LEFT_TO_FREEFALL\n85, LA_JUMP_RIGHT_TO_FREEFALL\n86, LA_UNDERWATER_SWIM_FORWARD\n87, LA_UNDERWATER_SWIM_FORWARD_DRIFT\n88, LA_SMALL_JUMP_BACK_START\n89, LA_SMALL_JUMP_BACK\n90, LA_SMALL_JUMP_BACK_END\n91, LA_JUMP_UP_START\n92, LA_LAND_TO_RUN\n93, LA_FALL_BACK\n94, LA_JUMP_FORWARD_TO_REACH\n95, LA_REACH\n96, LA_REACH_TO_HANG\n97, LA_CLIMB_ON\n98, LA_REACH_TO_FREEFALL_2\n99, LA_FALL_CROUCHING_LANDING\n100, LA_JUMP_FORWARD_TO_REACH_LATE\n101, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE\n102, LA_CLIMB_ON_END\n103, LA_STAND_IDLE\n104, LA_SLIDE_BACKWARD_START\n105, LA_SLIDE_BACKWARD\n106, LA_SLIDE_BACKWARD_END\n107, LA_UNDERWATER_SWIM_TO_IDLE\n108, LA_UNDERWATER_IDLE\n109, LA_UNDERWARER_IDLE_TO_SWIM\n110, LA_ONWATER_IDLE\n111, LA_ONWATER_TO_STAND_HIGH\n112, LA_FREEFALL_TO_UNDERWATER\n113, LA_ONWATER_DIVE_ALTERNATE\n114, LA_UNDERWATER_TO_ONWATER\n115, LA_ONWATER_SWIM_FORWARD_DIVE\n116, LA_ONWATER_SWIM_FORWARD\n117, LA_ONWATER_SWIM_FORWARD_TO_IDLE\n118, LA_ONWATER_IDLE_TO_SWIM_FORWARD\n119, LA_ONWATER_DIVE\n120, LA_PUSHABLE_GRAB\n121, LA_PUSHABLE_RELEASE\n122, LA_PUSHABLE_PULL\n123, LA_PUSHABLE_PUSH\n124, LA_UNDERWATER_DEATH\n125, LA_HIT_FRONT\n126, LA_HIT_BACK\n127, LA_HIT_LEFT\n128, LA_HIT_RIGHT\n129, LA_UNDERWATER_SWITCH\n130, LA_UNDERWATER_PICKUP\n131, LA_USE_KEY\n132, LA_ONWATER_DEATH\n133, LA_RUN_DEATH\n134, LA_USE_PUZZLE\n135, LA_PICKUP\n136, LA_SHIMMY_LEFT\n137, LA_SHIMMY_RIGHT\n138, LA_STAND_DEATH\n139, LA_BOULDER_DEATH\n140, LA_ONWATER_IDLE_TO_SWIM_BACK\n141, LA_ONWATER_SWIM_BACK\n142, LA_ONWATER_SWIM_BACK_TO_IDLE\n143, LA_ONWATER_SWIM_LEFT\n144, LA_ONWATER_SWIM_RIGHT\n145, LA_DEATH_JUMP\n146, LA_ROLL_START\n147, LA_ROLL_CONTINUE\n148, LA_ROLL_END\n149, LA_SPIKE_DEATH\n150, LA_SWING_IN_FAST\n151, LA_SWANDIVE_ROLL\n152, LA_SWANDIVE_TO_UNDERWATER\n153, LA_FREEFALL_SWANDIVE\n154, LA_FREEFALL_SWANDIVE_TO_UNDERWATER\n155, LA_SWANDIVE_DEATH\n156, LA_SWANDIVE_LEFT\n157, LA_SWANDIVE_RIGHT\n158, LA_SWANDIVE_START\n159, LA_CLIMB_ON_HANDSTAND\n160, LA_RUN_JUMP_ROLL_START\n161, LA_SOMERSAULT\n162, LA_RUN_JUMP_ROLL_END\n163, LA_JUMP_FORWARD_ROLL_START\n164, LA_JUMP_FORWARD_ROLL_END\n165, LA_JUMP_BACK_ROLL_START\n166, LA_JUMP_BACK_ROLL_END\n167, LA_UNDERWATER_ROLL_START\n168, LA_UNDERWATER_ROLL_END\n169, LA_ONWATER_TO_STAND_MEDIUM\n170, LA_WADE\n171, LA_RUN_TO_WADE_LEFT\n172, LA_RUN_TO_WADE_RIGHT\n173, LA_WADE_TO_RUN_LEFT\n174, LA_WADE_TO_RUN_RIGHT\n175, LA_WADE_TO_STAND_RIGHT\n176, LA_WADE_TO_STAND_LEFT\n177, LA_STAND_TO_WADE\n178, LA_ONWATER_TO_WADE\n179, LA_ONWATER_TO_WADE_LOW\n180, LA_UNDERWATER_TO_STAND\n181, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE\n182, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL\n183, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM\n184, LA_SLIDE_FORWARD_TO_RUN\n185, LA_JUMP_NEUTRAL_ROLL\n186, LA_CONTROLLED_DROP\n187, LA_CONTROLLED_DROP_CONTINUE\n188, LA_HANG_TO_JUMP_UP\n189, LA_HANG_TO_JUMP_UP_CONTINUE\n190, LA_HANG_TO_JUMP_BACK\n191, LA_HANG_TO_JUMP_BACK_CONTINUE\n192, LA_SPRINT\n193, LA_RUN_TO_SPRINT_LEFT\n194, LA_RUN_TO_SPRINT_RIGHT\n195, LA_SPRINT_SLIDE_STAND_LEFT\n196, LA_SPRINT_SLIDE_STAND_RIGHT\n197, LA_SPRINT_TO_ROLL_LEFT\n198, LA_SPRINT_ROLL_LEFT_TO_RUN\n199, LA_SPRINT_TO_ROLL_RIGHT\n200, LA_SPRINT_ROLL_RIGHT_TO_RUN\n201, LA_SPRINT_TO_RUN_LEFT\n202, LA_SPRINT_TO_RUN_RIGHT\n203, LA_POSE_RIGHT_START\n204, LA_POSE_RIGHT_CONTINUE\n205, LA_POSE_RIGHT_END\n206, LA_POSE_LEFT_START\n207, LA_POSE_LEFT_CONTINUE\n208, LA_POSE_LEFT_END\n209, LA_STAND_TO_LADDER\n210, LA_LADDER_UP\n211, LA_LADDER_UP_STOP_RIGHT\n212, LA_LADDER_UP_STOP_LEFT\n213, LA_LADDER_IDLE\n214, LA_LADDER_UP_START\n215, LA_LADDER_DOWN_STOP_LEFT\n216, LA_LADDER_DOWN_STOP_RIGHT\n217, LA_LADDER_DOWN\n218, LA_LADDER_DOWN_START\n219, LA_LADDER_RIGHT\n220, LA_LADDER_LEFT\n221, LA_LADDER_HANG\n222, LA_LADDER_HANG_TO_IDLE\n223, LA_LADDER_CLIMB_ON\n224, LA_LADDER_BACKFLIP_START\n225, LA_LADDER_BACKFLIP_CONTINUE\n226, LA_LADDER_UP_HANGING\n227, LA_LADDER_DOWN_HANGING\n228, LA_LADDER_TO_HANG_DOWN\n229, LA_LADDER_TO_HANG_RIGHT\n230, LA_LADDER_TO_HANG_LEFT\n231, LA_UNKNOWN\n232, LA_ONWATER_TO_WADE_SHALLOW_UNUSED\n233, LA_FLARE_THROW\n234, LA_SWITCH_SMALL_DOWN\n235, LA_SWITCH_SMALL_UP\n236, LA_BUTTON_PUSH\n237, LA_FLARE_PICKUP\n238, LA_UNDERWATER_FLARE_PICKUP\n239, LA_KICK\n240, LA_ZIPLINE_GRAB\n241, LA_ZIPLINE_RIDE\n242, LA_ZIPLINE_FALL\n243, LA_STAND_TO_CROUCH\n244, LA_STAND_TO_CROUCH_END\n245, LA_STAND_TO_CROUCH_ABORT_UNUSED\n246, LA_RUN_TO_CROUCH_LEFT_START\n247, LA_RUN_TO_CROUCH_LEFT_END\n248, LA_RUN_TO_CROUCH_RIGHT_START\n249, LA_RUN_TO_CROUCH_RIGHT_END\n250, LA_SPRINT_TO_CROUCH_LEFT\n251, LA_SPRINT_TO_CROUCH_RIGHT\n252, LA_HANG_TO_CROUCH_START\n253, LA_HANG_TO_CROUCH_END\n254, LA_CROUCH_IDLE\n255, LA_CROUCH_TO_STAND\n256, LA_CROUCH_PICKUP\n257, LA_CROUCH_PICKUP_FLARE\n258, LA_CROUCH_HIT_FRONT\n259, LA_CROUCH_HIT_BACK\n260, LA_CROUCH_HIT_RIGHT\n261, LA_CROUCH_HIT_LEFT\n262, LA_CROUCH_ROLL_FORWARD_START\n263, LA_CROUCH_ROLL_FORWARD_CONTINUE\n264, LA_CROUCH_ROLL_FORWARD_END\n265, LA_CROUCH_ROLL_FORWARD_START_ALTERNATE_UNUSED\n266, LA_CROUCH_TO_CRAWL_START\n267, LA_CROUCH_TO_CRAWL_CONTINUE\n268, LA_CROUCH_TO_CRAWL_END\n269, LA_CRAWL_IDLE\n270, LA_CRAWL_TO_CROUCH_START\n271, LA_CRAWL_TO_CROUCH_CONTINUE\n272, LA_CRAWL_TO_CROUCH_END_UNUSED\n273, LA_CRAWL_IDLE_TO_FORWARD\n274, LA_CRAWL_FORWARD\n275, LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT\n276, LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT\n277, LA_CRAWL_FORWARD_TO_IDLE_START_LEFT\n278, LA_CRAWL_FORWARD_TO_IDLE_END_LEFT\n279, LA_CRAWL_TURN_LEFT\n280, LA_CRAWL_TURN_RIGHT\n281, LA_CRAWL_IDLE_TO_BACKWARD\n282, LA_CRAWL_BACKWARD\n283, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START\n284, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END\n285, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START\n286, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END\n287, LA_CRAWL_TURN_LEFT_EARLY_END\n288, LA_CRAWL_TURN_RIGHT_EARLY_END\n289, LA_CRAWL_TO_HANG_START\n290, LA_CRAWL_TO_HANG_CONTINUE\n291, LA_CRAWL_TO_HANG_END\n292, LA_CRAWL_PICKUP\n293, LA_CRAWL_HIT_FRONT_UNUSED\n294, LA_CRAWL_HIT_BACK_UNUSED\n295, LA_CRAWL_HIT_RIGHT_UNUSED\n296, LA_CRAWL_HIT_LEFT_UNUSED\n297, LA_CRAWL_DEATH\n298, LA_CRAWL_JUMP_DOWN\n299, LA_CROUCH_TURN_LEFT\n300, LA_CROUCH_TURN_RIGHT\n301, LA_JUMP_FORWARD_START_TO_GRAB_EARLY\n302, LA_JUMP_FORWARD_START_TO_GRAB_LATE\n303, LA_RUN_TO_GRAB_RIGHT\n304, LA_RUN_TO_GRAB_LEFT\n305, LA_SWING_IN_SLOW\n306, LA_MONKEY_IDLE\n307, LA_MONKEY_FALL\n308, LA_MONKEY_GRAB\n309, LA_MONKEY_FORWARD\n310, LA_MONKEY_STOP_LEFT\n311, LA_MONKEY_STOP_RIGHT\n312, LA_MONKEY_IDLE_TO_FORWARD_LEFT\n313, LA_MONKEY_IDLE_TO_FORWARD_RIGHT\n314, LA_MONKEY_SHIMMY_LEFT\n315, LA_MONKEY_SHIMMY_LEFT_END\n316, LA_MONKEY_SHIMMY_RIGHT\n317, LA_MONKEY_SHIMMY_RIGHT_END\n318, LA_MONKEY_TURN_AROUND\n319, LA_MONKEY_TURN_LEFT\n320, LA_MONKEY_TURN_RIGHT\n321, LA_MONKEY_TURN_LEFT_EARLY_END\n322, LA_MONKEY_TURN_LEFT_LATE_END\n323, LA_MONKEY_TURN_RIGHT_EARLY_END\n324, LA_MONKEY_TURN_RIGHT_LATE_END\n325, LA_SPRINT_SLIDE_STAND_RIGHT_END_ALTERNATE_UNUSED\n326, LA_SPRINT_SLIDE_STAND_LEFT_END_ALTERNATE_UNUSED\n327, LA_SPRINT_TO_ROLL_LEFT_BETA_UNUSED\n328, LA_SPRINT_TO_ROLL_ALTERNATE_START_UNUSED\n329, LA_SPRINT_TO_ROLL_ALTERNATE_CONTINUE_UNUSED\n330, LA_SPRINT_TO_ROLL_ALTERNATE_END_UNUSED\n331, LA_LADDER_TO_CROUCH_START\n332, LA_LADDER_TO_CROUCH_END\n"
  },
  {
    "path": "data/trx/ship/games/tr1/catalog_lara_states.csv",
    "content": "0, LS_WALK\n1, LS_RUN\n2, LS_STOP\n3, LS_JUMP_FORWARD\n4, LS_POSE\n5, LS_FAST_BACK\n6, LS_TURN_RIGHT\n7, LS_TURN_LEFT\n8, LS_DEATH\n9, LS_FAST_FALL\n10, LS_HANG\n11, LS_REACH\n12, LS_SPLAT\n13, LS_TREAD\n14, LS_LAND\n15, LS_COMPRESS\n16, LS_WALK_BACK\n17, LS_SWIM\n18, LS_GLIDE\n19, LS_PULL_UP\n20, LS_FAST_TURN\n21, LS_STEP_RIGHT\n22, LS_STEP_LEFT\n23, LS_ROLL_CONT\n24, LS_SLIDE\n25, LS_JUMP_BACK\n26, LS_JUMP_RIGHT\n27, LS_JUMP_LEFT\n28, LS_JUMP_UP\n29, LS_FALL_BACK\n30, LS_SHIMMY_LEFT\n31, LS_SHIMMY_RIGHT\n32, LS_SLIDE_BACK\n33, LS_SURF_TREAD\n34, LS_SURF_SWIM\n35, LS_DIVE\n36, LS_PUSH_BLOCK\n37, LS_PULL_BLOCK\n38, LS_PP_READY\n39, LS_PICKUP\n40, LS_SWITCH_ON\n41, LS_SWITCH_OFF\n42, LS_USE_KEY\n43, LS_USE_PUZZLE\n44, LS_UW_DEATH\n45, LS_ROLL\n46, LS_SPECIAL\n47, LS_SURF_BACK\n48, LS_SURF_LEFT\n49, LS_SURF_RIGHT\n50, LS_USE_MIDAS\n51, LS_DIE_MIDAS\n52, LS_SWAN_DIVE\n53, LS_FAST_DIVE\n54, LS_GYMNAST\n55, LS_WATER_OUT\n56, LS_CONTROLLED\n57, LS_TWIST\n58, LS_WATER_ROLL\n59, LS_WADE\n60, LS_RESPONSIVE\n61, LS_NEUTRAL_ROLL\n62, LS_SPRINT\n63, LS_SPRINT_ROLL\n64, LS_POSE_START\n65, LS_POSE_END\n66, LS_POSE_LEFT\n67, LS_POSE_RIGHT\n68, LS_CLIMB_STANCE\n69, LS_CLIMBING\n70, LS_CLIMB_LEFT\n71, LS_CLIMB_END\n72, LS_CLIMB_RIGHT\n73, LS_CLIMB_DOWN\n74, LS_LARA_TEST1\n75, LS_LARA_TEST2\n76, LS_LARA_TEST3\n77, LS_FLARE_PICKUP\n78, LS_KICK\n79, LS_ZIPLINE\n80, LS_CROUCH_IDLE\n81, LS_CROUCH_ROLL\n82, LS_CRAWL_IDLE\n83, LS_CRAWL_FORWARD\n84, LS_CRAWL_TURN_LEFT\n85, LS_CRAWL_TURN_RIGHT\n86, LS_CRAWL_BACK\n87, LS_CLIMB_TO_CRAWL\n88, LS_CRAWL_TO_CLIMB\n89, LS_CRAWL_JUMP_DOWN\n90, LS_CROUCH_TURN_LEFT\n91, LS_CROUCH_TURN_RIGHT\n92, LS_MONKEY_IDLE\n93, LS_MONKEY_FORWARD\n94, LS_MONKEY_LEFT\n95, LS_MONKEY_RIGHT\n96, LS_MONKEY_ROLL\n97, LS_MONKEY_TURN_LEFT\n98, LS_MONKEY_TURN_RIGHT\n"
  },
  {
    "path": "data/trx/ship/games/tr1/catalog_music.csv",
    "content": "0,  MX_UNUSED_0\n1,  MX_UNUSED_1\n5,  MX_UNUSED_2\n13, MX_SECRET\n28, MX_TR1_GYM_HINT_03\n29, MX_TR1_GYM_HINT_04\n37, MX_TR1_GYM_HINT_12\n39, MX_TR1_GYM_HINT_14\n40, MX_TR1_GYM_HINT_15\n41, MX_TR1_GYM_HINT_16\n42, MX_TR1_GYM_HINT_17\n43, MX_TR1_GYM_HINT_18\n49, MX_TR1_GYM_HINT_24\n50, MX_TR1_GYM_HINT_25\n51, MX_BALDY_SPEECH\n52, MX_COWBOY_SPEECH\n53, MX_LARSON_SPEECH\n54, MX_NATLA_SPEECH\n55, MX_PIERRE_SPEECH\n56, MX_SKATEKID_SPEECH\n"
  },
  {
    "path": "data/trx/ship/games/tr1/catalog_objects.csv",
    "content": "0,   O_LARA\n1,   O_LARA_PISTOLS\n2,   O_LARA_SHOTGUN\n3,   O_LARA_MAGNUMS\n4,   O_LARA_UZIS\n5,   O_LARA_EXTRA\n6,   O_BACON_LARA\n7,   O_WOLF\n8,   O_BEAR\n9,   O_BAT\n10,  O_CROCODILE\n11,  O_ALLIGATOR\n12,  O_LION\n13,  O_LIONESS\n14,  O_PUMA\n15,  O_APE\n16,  O_RAT\n17,  O_VOLE\n18,  O_TREX\n19,  O_RAPTOR\n20,  O_ATLANTEAN_WINGED\n21,  O_ATLANTEAN_SHOOTER\n22,  O_ATLANTEAN_GROUND\n23,  O_CENTAUR\n24,  O_MUMMY\n25,  O_DINO_WARRIOR\n26,  O_FISH\n27,  O_LARSON\n28,  O_PIERRE\n29,  O_SKATEBOARD\n30,  O_SKATEKID\n31,  O_COWBOY\n32,  O_BALDY\n33,  O_NATLA\n34,  O_TORSO\n35,  O_FALLING_BLOCK_1\n36,  O_SWINGING_AXE\n37,  O_SPIKES\n38,  O_ROLLING_BALL_1\n39,  O_DART\n40,  O_DART_EMITTER\n41,  O_DRAWBRIDGE\n42,  O_TEETH_TRAP\n43,  O_DAMOCLES_SWORD\n44,  O_THORS_HANDLE\n45,  O_THORS_HEAD\n46,  O_LIGHTNING_EMITTER\n47,  O_MOVING_BAR\n48,  O_MOVABLE_BLOCK_1\n49,  O_MOVABLE_BLOCK_2\n50,  O_MOVABLE_BLOCK_3\n51,  O_MOVABLE_BLOCK_4\n52,  O_SLIDING_PILLAR\n53,  O_FALLING_CEILING_1\n54,  O_FALLING_CEILING_2\n55,  O_SWITCH_TYPE_NORMAL\n56,  O_SWITCH_TYPE_UW\n57,  O_DOOR_TYPE_1\n58,  O_DOOR_TYPE_2\n59,  O_DOOR_TYPE_3\n60,  O_DOOR_TYPE_4\n61,  O_DOOR_TYPE_5\n62,  O_DOOR_TYPE_6\n63,  O_DOOR_TYPE_7\n64,  O_DOOR_TYPE_8\n65,  O_TRAPDOOR_TYPE_1\n66,  O_TRAPDOOR_TYPE_2\n67,  O_TRAPDOOR_TYPE_3\n68,  O_BRIDGE_FLAT\n69,  O_BRIDGE_TILT_1\n70,  O_BRIDGE_TILT_2\n71,  O_PASSPORT_OPTION\n72,  O_COMPASS_OPTION\n73,  O_PHOTO_OPTION\n74,  O_COG_1\n75,  O_COG_2\n76,  O_COG_3\n77,  O_PLAYER_1\n78,  O_PLAYER_2\n79,  O_PLAYER_3\n80,  O_PLAYER_4\n81,  O_PASSPORT_CLOSED\n82,  O_PDA_OPTION\n83,  O_SAVE_CRYSTAL_ITEM\n84,  O_PISTOL_ITEM\n85,  O_SHOTGUN_ITEM\n86,  O_MAGNUM_ITEM\n87,  O_UZI_ITEM\n88,  O_PISTOL_AMMO_ITEM\n89,  O_SHOTGUN_AMMO_ITEM\n90,  O_MAGNUM_AMMO_ITEM\n91,  O_UZI_AMMO_ITEM\n92,  O_EXPLOSIVE_ITEM\n93,  O_SMALL_MEDIPACK_ITEM\n94,  O_LARGE_MEDIPACK_ITEM\n95,  O_DETAIL_OPTION\n96,  O_SOUND_OPTION\n97,  O_CONTROL_OPTION\n98,  O_GAMMA_OPTION\n99,  O_PISTOL_OPTION\n100, O_SHOTGUN_OPTION\n101, O_MAGNUM_OPTION\n102, O_UZI_OPTION\n103, O_PISTOL_AMMO_OPTION\n104, O_SHOTGUN_AMMO_OPTION\n105, O_MAGNUM_AMMO_OPTION\n106, O_UZI_AMMO_OPTION\n107, O_EXPLOSIVE_OPTION\n108, O_SMALL_MEDIPACK_OPTION\n109, O_LARGE_MEDIPACK_OPTION\n110, O_PUZZLE_ITEM_1\n111, O_PUZZLE_ITEM_2\n112, O_PUZZLE_ITEM_3\n113, O_PUZZLE_ITEM_4\n114, O_PUZZLE_OPTION_1\n115, O_PUZZLE_OPTION_2\n116, O_PUZZLE_OPTION_3\n117, O_PUZZLE_OPTION_4\n118, O_PUZZLE_HOLE_1\n119, O_PUZZLE_HOLE_2\n120, O_PUZZLE_HOLE_3\n121, O_PUZZLE_HOLE_4\n122, O_PUZZLE_DONE_1\n123, O_PUZZLE_DONE_2\n124, O_PUZZLE_DONE_3\n125, O_PUZZLE_DONE_4\n126, O_LEADBAR_ITEM\n127, O_LEADBAR_OPTION\n128, O_MIDAS_TOUCH\n129, O_KEY_ITEM_1\n130, O_KEY_ITEM_2\n131, O_KEY_ITEM_3\n132, O_KEY_ITEM_4\n133, O_KEY_OPTION_1\n134, O_KEY_OPTION_2\n135, O_KEY_OPTION_3\n136, O_KEY_OPTION_4\n137, O_KEY_HOLE_1\n138, O_KEY_HOLE_2\n139, O_KEY_HOLE_3\n140, O_KEY_HOLE_4\n141, O_PICKUP_ITEM_1\n142, O_PICKUP_ITEM_2\n143, O_SCION_ITEM_1\n144, O_SCION_ITEM_2\n145, O_SCION_ITEM_3\n146, O_SCION_ITEM_4\n147, O_SCION_HOLDER\n148, O_PICKUP_OPTION_1\n149, O_PICKUP_OPTION_2\n150, O_SCION_OPTION\n151, O_EXPLOSION_1\n152, O_EXPLOSION_2\n153, O_SPLASH_1\n154, O_SPLASH_2\n155, O_BUBBLE_1\n156, O_BUBBLE_2\n157, O_BUBBLE_EMITTER\n158, O_BLOOD\n159, O_BLOOD_PINK\n160, O_DART_EFFECT\n161, O_CENTAUR_STATUE\n162, O_PORTACABIN\n163, O_PODS\n164, O_RICOCHET\n165, O_TWINKLE\n166, O_GUN_FLASH\n167, O_DUST\n168, O_BODY_PART\n169, O_CAMERA_TARGET\n170, O_WATERFALL\n171, O_NATLA_GUN\n172, O_MISSILE_ATLANTEAN_SHARD\n173, O_MISSILE_ATLANTEAN_BOMB\n176, O_EMBER\n177, O_EMBER_EMITTER\n178, O_FLAME\n179, O_FLAME_EMITTER\n180, O_LAVA_WEDGE\n181, O_BIG_POD\n182, O_MOTOR_BOAT\n183, O_EARTHQUAKE\n184, O_SKYBOX\n185, O_PICKUP_AID\n186, O_GLOW\n187, O_FLAREBOX_ITEM\n188, O_FLAREBOX_OPTION\n189, O_LARA_HAIR\n190, O_ALPHABET\n191, O_WINSTON\n192, O_LARA_FLARE\n193, O_FLARE_ITEM\n194, O_FLARE_FIRE\n# Slots 195-200 moved for Lara skins: available for re-use\n201, O_LARA_M16\n202, O_LARA_GRENADE_GUN\n203, O_LARA_HARPOON_GUN\n204, O_M16_OPTION\n205, O_GRENADE_GUN_OPTION\n206, O_HARPOON_OPTION\n207, O_M16_AMMO_OPTION\n208, O_GRENADE_AMMO_OPTION\n209, O_HARPOON_AMMO_OPTION\n210, O_M16_FLASH\n211, O_GRENADE\n212, O_HARPOON_BOLT\n213, O_LARA_AUTOS\n214, O_AUTOS_OPTION\n215, O_AUTOS_AMMO_OPTION\n216, O_LARA_DESERT_EAGLE\n217, O_DESERT_EAGLE_OPTION\n218, O_DESERT_EAGLE_AMMO_OPTION\n219, O_LARA_MP5,\n220, O_MP5_OPTION\n221, O_MP5_AMMO_OPTION\n222, O_LARA_ROCKET_GUN\n223, O_ROCKET_GUN_OPTION\n224, O_ROCKET_AMMO_OPTION\n225, O_ROCKET\n241, O_M16_ITEM\n242, O_GRENADE_GUN_ITEM\n243, O_HARPOON_ITEM\n244, O_M16_AMMO_ITEM\n245, O_GRENADE_AMMO_ITEM\n246, O_HARPOON_AMMO_ITEM\n247, O_ALPHABET_SMALL\n248, O_AUTOS_ITEM\n249, O_AUTOS_AMMO_ITEM\n250, O_SNOWFLAKE\n251, O_DESERT_EAGLE_ITEM\n252, O_DESERT_EAGLE_AMMO_ITEM\n253, O_MP5_ITEM\n254, O_MP5_AMMO_ITEM\n255, O_ROCKET_GUN_ITEM\n256, O_ROCKET_AMMO_ITEM\n257, O_SHADOW\n258, O_LARA_SKIN_SWAP_1\n259, O_LARA_SKIN_SWAP_2\n260, O_LARA_SKIN_SWAP_3\n261, O_LARA_SKIN_SWAP_4\n262, O_LARA_SKIN_SWAP_5\n263, O_LARA_SKIN_SWAP_6\n264, O_LARA_SKIN_SWAP_7\n265, O_LARA_SKIN_SWAP_8\n266, O_LARA_SKIN_SWAP_9\n267, O_LARA_SKIN_SWAP_10\n268, O_LARA_SKIN_SWAP_11\n269, O_LARA_SKIN_SWAP_12\n270, O_LARA_SKIN_SWAP_13\n271, O_LARA_SKIN_SWAP_14\n272, O_LARA_SKIN_SWAP_15\n273, O_LARA_SKIN_SWAP_16\n274, O_LARA_SKIN_SWAP_17\n275, O_LARA_SKIN_SWAP_18\n276, O_LARA_SKIN_SWAP_19\n277, O_LARA_SKIN_SWAP_20\n278, O_LARA_SKIN_SWAP_21\n279, O_LARA_SKIN_SWAP_22\n280, O_LARA_SKIN_SWAP_23\n281, O_LARA_SKIN_SWAP_24\n282, O_LARA_SKIN_SWAP_25\n283, O_LARA_SKIN_SWAP_26\n284, O_LARA_SKIN_SWAP_27\n285, O_LARA_SKIN_SWAP_28\n286, O_LARA_SKIN_SWAP_29\n287, O_LARA_SKIN_SWAP_30\n288, O_LARA_SKIN_SWAP_31\n289, O_LARA_SKIN_SWAP_32\n290, O_LARA_SKIN_SWAP_EXTRA\n291, O_LARA_SKIN_SWAP_GUNS\n292, O_LARA_SKIN_SWAP_LEGS\n"
  },
  {
    "path": "data/trx/ship/games/tr1/catalog_samples.csv",
    "content": "0,   SFX_LARA_FOOTSTEP\n2,   SFX_LARA_NO\n6,   SFX_LARA_DRAW\n7,   SFX_LARA_HOLSTER\n8,   SFX_LARA_PISTOLS\n9,   SFX_LARA_RELOAD\n10,  SFX_LARA_RICOCHET\n12,  SFX_BEAR_FEET\n14,  SFX_BEAR_SNARL\n16,  SFX_BEAR_HURT\n20,  SFX_WOLF_HURT\n26,  SFX_LARA_CLIMB_3\n27,  SFX_LARA_BODYSL\n30,  SFX_LARA_FALL\n31,  SFX_LARA_INJURY\n33,  SFX_LARA_SPLASH\n36,  SFX_LARA_BREATH\n37,  SFX_LARA_BUBBLES\n39,  SFX_LARA_KEY\n41,  SFX_LARA_GENERAL_DEATH\n43,  SFX_LARA_UZI_FIRE\n44,  SFX_LARA_MAGNUMS\n45,  SFX_LARA_SHOTGUN\n48,  SFX_LARA_EMPTY\n50,  SFX_LARA_BULLETHIT\n53,  SFX_LARA_FALL_DEATH\n60,  SFX_UNDERWATER\n70,  SFX_PUSHBLOCK_LAND\n70,  SFX_EARTHQUAKE_2\n79,  SFX_WATERFALL_LOOP\n81,  SFX_WATERFALL_BIG\n81,  SFX_FLOOD\n85,  SFX_LION_HURT\n95,  SFX_RAT_CHIRP\n98,  SFX_THUNDER\n99,  SFX_EXPLOSION_2\n103, SFX_DAMOCLES_SWORD\n104, SFX_EXPLOSION_1\n108, SFX_MENU_ROTATE\n109, SFX_MENU_LARA_HOME\n110, SFX_MENU_GAMEBOY\n111, SFX_MENU_CHOOSE\n111, SFX_MENU_SPININ\n112, SFX_MENU_SPINOUT\n113, SFX_MENU_COMPASS\n114, SFX_MENU_GUNS\n115, SFX_MENU_PASSPORT\n116, SFX_MENU_MEDI\n117, SFX_RAISINGBLOCK_FX\n118, SFX_SAND_FX\n119, SFX_STAIRS_2_SLOPE_FX\n123, SFX_ATLANTEAN_NEEDLE\n132, SFX_SKATEBOARD_HIT\n142, SFX_TORSO_HIT\n147, SFX_ROLLING_BALL_1_ROLL\n147, SFX_EARTHQUAKE_1\n149, SFX_LAVA_FOUNTAIN\n150, SFX_LOOP_FOR_SMALL_FIRES\n151, SFX_DART\n155, SFX_POWERUP_FX\n161, SFX_TRAPDOOR_OPEN\n170, SFX_EXPLOSION_FX\n171, SFX_ATLANTEAN_DEATH\n172, SFX_CHAINBLOCK_FX\n173, SFX_SECRET\n199, SFX_BALDY_SPEECH\n200, SFX_COWBOY_SPEECH\n201, SFX_LARSON_SPEECH\n202, SFX_NATLA_SPEECH\n203, SFX_PIERRE_SPEECH\n204, SFX_SKATEKID_SPEECH\n257, SFX_LARA_FLARE_IGNITE\n258, SFX_LARA_FLARE_BURN\n259, SFX_M16_FIRE\n260, SFX_M16_STOP\n268, SFX_LARA_AUTOS\n269, SFX_LARA_DESERT_EAGLE\n270, SFX_MP5_FIRE\n271, SFX_ROCKET_FIRE\n272, SFX_EXPLOSION_3\n273, SFX_LARA_BAREFOOT\n# 274 used in animations for Lara knee shuffle\n344, SFX_WINSTON_GRUNT_1\n345, SFX_WINSTON_GRUNT_2\n346, SFX_WINSTON_GRUNT_3\n347, SFX_WINSTON_CUPS\n"
  },
  {
    "path": "data/trx/ship/games/tr1/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 1,\n    \"name\": \"Tomb Raider I\",\n\n    \"main_menu_picture\": \"title.webp\",\n    \"savegame_file_fmt\": \"save_tr1_%02d.dat\",\n\n    \"injections\": [\n        \"braid.bin\",\n        \"bubbles.bin\",\n        \"gun_glow.bin\",\n        \"lara_animations.bin\",\n        \"lara_guns.bin\",\n        \"crystal.bin\",\n        \"uzi_sfx.bin\",\n        \"explosion.bin\",\n        \"font.bin\",\n        \"pickup_aid.bin\",\n        \"sprite_alignment.bin\",\n        \"pda_model.bin\",\n        \"winston_model.bin\",\n        \"lara_extra.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n    ],\n\n    \"enable_tr2_item_drops\": false,\n    \"convert_dropped_guns\": false,\n\n    \"title\": {\n        \"path\": \"title.phd\",\n        \"music_track\": 2,\n        \"inherit_injections\": false,\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"eidos.webp\", \"legal\": true, \"display_time\": 1, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 1},\n            {\"type\": \"play_fmv\", \"fmv_id\": 2},\n            {\"type\": \"exit_to_title\"},\n        ],\n        \"injections\": [\n            \"pda_model.bin\",\n            \"font.bin\",\n            \"title_textures.bin\",\n            \"misc_sprites.bin\",\n        ],\n    },\n\n    \"levels\": [\n        // Level 0: Lara's Home\n        {\n            \"path\": \"gym.phd\",\n            \"script\": \"gym.lua\",\n            \"type\": \"gym\",\n            \"music_track\": 0,\n            \"inherit_injections\": false,\n            \"lara_outfit\": \"tr1_gym\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 3},\n                {\"type\": \"loading_screen\", \"path\": \"gym.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"exit_to_title\"},\n            ],\n            \"injections\": [\n                \"lara_gym_guns.bin\",\n                \"braid.bin\",\n                \"bubbles.bin\",\n                \"gun_glow.bin\",\n                \"gym_textures.bin\",\n                \"lara_animations.bin\",\n                \"uzi_sfx.bin\",\n                \"explosion.bin\",\n                \"pda_model.bin\",\n                \"font.bin\",\n                \"winston_model.bin\",\n                \"misc_sprites.bin\",\n                \"lara_feet_sfx.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n\n        // Level 1: Caves\n        {\n            \"path\": \"level1.phd\",\n            \"music_track\": 57,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 4},\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"caves_fd.bin\",\n                \"caves_itemrots.bin\",\n                \"caves_textures.bin\",\n            ],\n        },\n\n        // Level 2: City of Vilcabamba\n        {\n            \"path\": \"level2.phd\",\n            \"music_track\": 57,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"vilcabamba_door_sfx.bin\",\n                \"vilcabamba_itemrots.bin\",\n                \"vilcabamba_textures.bin\",\n            ],\n        },\n\n        // Level 3: Lost Valley\n        {\n            \"path\": \"level3a.phd\",\n            \"music_track\": 57,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"valley_fd.bin\",\n                \"valley_itemrots.bin\",\n                \"valley_skybox.bin\",\n                \"valley_textures.bin\",\n            ],\n        },\n\n        // Level 4: Tomb of Qualopec\n        {\n            \"path\": \"level3b.phd\",\n            \"music_track\": 57,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 0},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"qualopec_door_sfx.bin\",\n                \"qualopec_fd.bin\",\n                \"qualopec_itemrots.bin\",\n                \"qualopec_textures.bin\",\n            ],\n        },\n\n        // Level 5: St. Francis' Folly\n        {\n            \"path\": \"level4.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 5},\n                {\"type\": \"loading_screen\", \"path\": \"greece.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"door59_frames.bin\",\n                \"folly_fd.bin\",\n                \"folly_itemrots.bin\",\n                \"folly_pickup_meshes.bin\",\n                \"folly_textures.bin\",\n            ],\n        },\n\n        // Level 6: Colosseum\n        {\n            \"path\": \"level5.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"greece.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"colosseum_fd.bin\",\n                \"colosseum_itemrots.bin\",\n                \"colosseum_skybox.bin\",\n                \"colosseum_textures.bin\",\n                \"door58_frames.bin\",\n            ],\n        },\n\n        // Level 7: Palace Midas\n        {\n            \"path\": \"level6.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"greece.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"midas_itemrots.bin\",\n                \"midas_textures.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // Level 8: The Cistern\n        {\n            \"path\": \"level7a.phd\",\n            \"music_track\": 58,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"greece.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"cistern_fd.bin\",\n                \"cistern_itemrots.bin\",\n                \"cistern_plants.bin\",\n                \"cistern_skybox.bin\",\n                \"cistern_textures.bin\",\n            ],\n        },\n\n        // Level 9: Tomb of Tihocan\n        {\n            \"path\": \"level7b.phd\",\n            \"music_track\": 58,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"greece.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 1},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"cistern_plants.bin\",\n                \"tihocan_skybox.bin\",\n                \"door60_frames.bin\",\n                \"tihocan_fd.bin\",\n                \"tihocan_itemrots.bin\",\n                \"tihocan_textures.bin\",\n            ],\n            \"item_drops\": [\n                {\"enemy_num\": 82, \"object_ids\": [86, 144, 129]},\n            ],\n        },\n\n        // Level 10: City of Khamoon\n        {\n            \"path\": \"level8a.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 6},\n                {\"type\": \"loading_screen\", \"path\": \"egypt.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"khamoon_fd.bin\",\n                \"khamoon_itemrots.bin\",\n                \"khamoon_meshfixes.bin\",\n                \"khamoon_mummy.bin\",\n                \"khamoon_textures.bin\",\n                \"panther_sfx.bin\",\n            ],\n        },\n\n        // Level 11: Obelisk of Khamoon\n        {\n            \"path\": \"level8b.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"egypt.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"obelisk_fd.bin\",\n                \"obelisk_itemrots.bin\",\n                \"obelisk_meshfixes.bin\",\n                \"obelisk_skybox.bin\",\n                \"obelisk_textures.bin\",\n                \"panther_sfx.bin\",\n            ],\n        },\n\n        // Level 12: Sanctuary of the Scion\n        {\n            \"path\": \"level8c.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"egypt.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"door59_frames.bin\",\n                \"door59_sfx.bin\",\n                \"sanctuary_fd.bin\",\n                \"sanctuary_itemrots.bin\",\n                \"sanctuary_scion.bin\",\n                \"sanctuary_textures.bin\",\n            ],\n        },\n\n        // Level 13: Natla's Mines\n        {\n            \"path\": \"level10a.phd\",\n            \"music_track\": 58,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 7},\n                {\"type\": \"loading_screen\", \"path\": \"atlantis.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"remove_weapons\"},\n                {\"type\": \"remove_scions\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 2},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"mines_cameras.bin\",\n                \"mines_door_sfx.bin\",\n                \"mines_fd.bin\",\n                \"mines_itemrots.bin\",\n                \"mines_meshfixes.bin\",\n                \"mines_pushblocks.bin\",\n                \"mines_textures.bin\",\n                \"skate_kid_sfx.bin\",\n            ],\n            \"item_drops\": [\n                {\"enemy_num\": 17, \"object_ids\": [86]},\n                {\"enemy_num\": 50, \"object_ids\": [87]},\n                {\"enemy_num\": 75, \"object_ids\": [85]},\n            ],\n        },\n\n        // Level 14: Atlantis\n        {\n            \"path\": \"level10b.phd\",\n            \"music_track\": 60,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 8},\n                {\"type\": \"loading_screen\", \"path\": \"atlantis.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"pistols\", \"quantity\": 1},\n                {\"type\": \"setup_bacon_lara\", \"anchor_room\": 10},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_fmv\", \"fmv_id\": 9},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 3},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"atlantis_door_sfx.bin\",\n                \"atlantis_fd.bin\",\n                \"atlantis_itemrots.bin\",\n                \"atlantis_textures.bin\",\n            ],\n            \"unobtainable_pickups\": 3,\n        },\n\n        // Level 15: The Great Pyramid\n        {\n            \"path\": \"level10c.phd\",\n            \"music_track\": 60,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"atlantis.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_fmv\", \"fmv_id\": 10},\n                {\"type\": \"play_music\", \"music_track\": 19},\n                {\"type\": \"level_complete\"},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"end.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_1.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_2.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_3.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"total_stats\", \"background_path\": \"install.webp\"},\n            ],\n            \"injections\": [\n                \"atlantis_door_sfx.bin\",\n                \"pyramid_fd.bin\",\n                \"pyramid_itemrots.bin\",\n                \"pyramid_textures.bin\",\n                \"scion_collision.bin\",\n            ],\n        },\n\n        // Level 16-20: Legacy savegame placeholders\n        {\"type\": \"dummy\"},\n        {\"type\": \"dummy\"},\n        {\"type\": \"dummy\"},\n        {\"type\": \"dummy\"},\n        {\"type\": \"dummy\"},\n\n        // Level 21: Current Position\n        // This level is necessary to read TombATI's save files!\n        // OG has a special level called LV_CURRENT to handle save/load logic.\n        // TRX does away without this hack. However, the existing save games\n        // expect the level count to match, otherwise the game will crash.\n        // Hence this dummy level.\n        {\n            \"path\": \"current.phd\",\n            \"type\": \"current\",\n            \"music_track\": 0,\n            \"inherit_injections\": false,\n            \"sequence\": [\n                {\"type\": \"exit_to_title\"},\n            ],\n        },\n    ],\n\n    \"demos\": [\n        // Demo 1: City of Vilcabamba\n        {\n            \"path\": \"level2.phd\",\n            \"music_track\": 57,\n            \"lara_outfit\": \"tr1_classic\",\n            \"injections\": [\n                \"vilcabamba_door_sfx.bin\",\n                \"vilcabamba_itemrots.bin\",\n                \"vilcabamba_textures.bin\",\n            ],\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n\n        // Demo 2: Lost Valley\n        {\n            \"path\": \"level3a.phd\",\n            \"music_track\": 57,\n            \"lara_outfit\": \"tr1_classic\",\n            \"injections\": [\n                \"valley_itemrots.bin\",\n                \"valley_skybox.bin\",\n                \"valley_textures.bin\",\n            ],\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n\n    ],\n\n    \"cutscenes\": [\n        // Cutscene 1\n        {\n            \"path\": \"cut1.phd\",\n            \"music_track\": 23,\n            \"lara_outfit\": \"tr1_classic\",\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut1_setup.bin\",\n                \"braid.bin\",\n                \"photo.bin\",\n                \"pda_model.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n                \"misc_sprites.bin\",\n            ],\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n        },\n\n        // Cutscene 2\n        {\n            \"path\": \"cut2.phd\",\n            \"music_track\": 25,\n            \"lara_outfit\": \"tr1_classic\",\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut2_setup.bin\",\n                \"braid.bin\",\n                \"photo.bin\",\n                \"pda_model.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n                \"misc_sprites.bin\",\n            ],\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n        },\n\n        // Cutscene 3\n        {\n            \"path\": \"cut3.phd\",\n            \"music_track\": 24,\n            \"lara_outfit\": \"tr1_classic\",\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut3_setup.bin\",\n                \"cut3_textures.bin\",\n                \"photo.bin\",\n                \"pda_model.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n                \"misc_sprites.bin\",\n            ],\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n        },\n\n        // Cutscene 4\n        {\n            \"path\": \"cut4.phd\",\n            \"music_track\": 22,\n            \"lara_outfit\": \"tr1_classic\",\n            \"fog_start\": 12.0,\n            \"fog_end\": 18.0,\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut4_setup.bin\",\n                \"braid.bin\",\n                \"cut4_textures.bin\",\n                \"photo.bin\",\n                \"pda_model.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n                \"misc_sprites.bin\",\n            ],\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n        },\n    ],\n\n    // FMVs\n    \"fmvs\": [\n        {\"path\": \"core.avi\", \"legal\": true},\n        {\"path\": \"escape.avi\", \"legal\": true},\n        {\"path\": \"cafe.avi\"},\n        {\"path\": \"mansion.avi\"},\n        {\"path\": \"snow.avi\"},\n        {\"path\": \"lift.avi\"},\n        {\"path\": \"vision.avi\"},\n        {\"path\": \"canyon.avi\"},\n        {\"path\": \"pyramid.avi\"},\n        {\"path\": \"prison.avi\"},\n        {\"path\": \"end.avi\"},\n    ],\n\n    \"hidden_config\": [\n        \"enable_item_examining\", // TR1 has no special item descriptions\n        \"healthbar_poison_color\",\n        \"healthbar_poison_color_ps1\",\n        \"enemy_healthbar_color_allies\",\n        \"enemy_healthbar_color_allies_ps1\",\n        \"exposurebar_color\",\n        \"exposurebar_color_ps1\",\n        \"exposurebar_location\",\n        \"exposurebar_show_mode\",\n        \"enable_ally_targeting\",\n        \"enable_weather\",\n        \"enable_footprints\",\n        \"ally_hostility_policy\",\n        \"fix_monkey_pickup_priority\",\n        \"fix_pipeman_aim\",\n        \"enable_cinematics\",\n        \"enable_body_bags\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/inv_ring.json5",
    "content": "[\n    {\n        \"object_id\": \"O_SMALL_MEDIPACK_OPTION\",\n        \"frames_total\": 26,\n        \"open_frame\": 25,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4032,\n        \"x_rot_sel\": -7296,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 216,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 14,\n    },\n\n    {\n        \"object_id\": \"O_LARGE_MEDIPACK_OPTION\",\n        \"frames_total\": 20,\n        \"open_frame\": 19,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3616,\n        \"x_rot_sel\": -8160,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 13,\n    },\n\n    {\n        \"object_id\": \"O_FLAREBOX_OPTION\",\n        \"frames_total\": 31,\n        \"open_frame\": 30,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"y_rot_sel\": -8192,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 12,\n    },\n\n    {\n        \"object_id\": \"O_PISTOL_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 1,\n    },\n\n    {\n        \"object_id\": \"O_SHOTGUN_OPTION\",\n        \"frames_total\": 13,\n        \"open_frame\": 12,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"y_rot_sel\": -8192,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 2,\n    },\n\n    {\n        \"object_id\": \"O_MAGNUM_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 3,\n    },\n\n    {\n        \"object_id\": \"O_AUTOS_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 4,\n    },\n\n    {\n        \"object_id\": \"O_DESERT_EAGLE_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 5,\n    },\n\n    {\n        \"object_id\": \"O_UZI_OPTION\",\n        \"frames_total\": 13,\n        \"open_frame\": 12,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 6,\n    },\n\n    {\n        \"object_id\": \"O_HARPOON_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -736,\n        \"y_rot_sel\": -19456,\n        \"y_trans_sel\": 58,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 11,\n    },\n\n    {\n        \"object_id\": \"O_M16_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": -18432,\n        \"y_trans_sel\": 84,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 7,\n    },\n\n    {\n        \"object_id\": \"O_MP5_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": -18432,\n        \"y_trans_sel\": 84,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 8,\n    },\n\n    {\n        \"object_id\": \"O_ROCKET_GUN_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": 14336,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 9,\n    },\n\n    {\n        \"object_id\": \"O_GRENADE_GUN_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": 14336,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_PISTOL_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 1,\n    },\n\n    {\n        \"object_id\": \"O_SHOTGUN_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 2,\n    },\n\n    {\n        \"object_id\": \"O_MAGNUM_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 3,\n    },\n\n    {\n        \"object_id\": \"O_AUTOS_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 4,\n    },\n\n    {\n        \"object_id\": \"O_DESERT_EAGLE_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 5,\n    },\n\n    {\n        \"object_id\": \"O_UZI_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 6,\n    },\n\n    {\n        \"object_id\": \"O_HARPOON_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 11,\n    },\n\n    {\n        \"object_id\": \"O_M16_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 7,\n    },\n\n    {\n        \"object_id\": \"O_MP5_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 8,\n    },\n\n    {\n        \"object_id\": \"O_ROCKET_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 9,\n    },\n\n    {\n        \"object_id\": \"O_GRENADE_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_SCION_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 109,\n    },\n\n    {\n        \"object_id\": \"O_LEADBAR_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3616,\n        \"x_rot_sel\": -8160,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 100,\n    },\n\n    {\n        \"object_id\": \"O_PICKUP_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 111,\n    },\n\n    {\n        \"object_id\": \"O_PICKUP_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 110,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 108,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 107,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 106,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 105,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 101,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 102,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 103,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 104,\n    },\n\n    {\n        \"object_id\": \"O_STOPWATCH_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4352,\n        \"x_rot_sel\": -1536,\n        \"y_trans_sel\": -170,\n        \"z_trans_sel\": 320,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n    },\n\n    {\n        \"object_id\": \"O_COMPASS_OPTION\",\n        \"frames_total\": 25,\n        \"open_frame\": 10,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4352,\n        \"x_rot_sel\": -8192,\n        \"z_trans_sel\": 456,\n        \"meshes_sel\": 0b00000101,\n        \"meshes_drawn\": 0b00000101,\n    },\n\n    {\n        \"object_id\": \"O_PASSPORT_OPTION\",\n        \"frames_total\": 30,\n        \"open_frame\": 14,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"x_rot_sel\": -4320,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": 0b00010011,\n        \"meshes_drawn\": 0b00010011,\n        \"inv_pos\": 200,\n    },\n\n    {\n        \"object_id\": \"O_DETAIL_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4224,\n        \"x_rot_sel\": -6720,\n        \"z_trans_sel\": 424,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 201,\n    },\n\n    {\n        \"object_id\": \"O_SOUND_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4832,\n        \"x_rot_sel\": -2336,\n        \"z_trans_sel\": 368,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 202,\n    },\n\n    {\n        \"object_id\": \"O_CONTROL_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 5504,\n        \"x_rot_sel\": 1536,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 203,\n    },\n\n    {\n        \"object_id\": \"O_PHOTO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"x_rot_sel\": -4320,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 205,\n    },\n\n    {\n        \"object_id\": \"O_PDA_OPTION\",\n        \"frames_total\": 39,\n        \"open_frame\": 19,\n        \"anim_direction\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": 0b00000011,\n        \"meshes_drawn\": 0b00000011,\n        \"inv_pos\": 204,\n    },\n]\n"
  },
  {
    "path": "data/trx/ship/games/tr1/scripts/gym.lua",
    "content": "trx.events.on_game_start(function(level, is_save)\n  trx.lara.holsters_visible = trx.lara.has_pistol_weapon -- TODO: remove in TRX 1.5.\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- Leerer SLOT %d -\",\n        },\n        \"stats\": {\n            \"secrets\": \"\\\\{review}Geheimnisse\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Aktiviert die Verwendung von dynamischer Beleuchtung für Schüsse und Explosionen., ähnlich wie in TR2+.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Fixt originale Fehler in Die Zisterne und Grab des Tihocan, bei denen Pflanzen-Sprites in Wassergebieten nicht animiert sind.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Großes Medi-Pack\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kleines Medi-Pack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Zwischensequenz 1\",\n        },\n        {\n            \"title\": \"Zwischensequenz 2\",\n        },\n        {\n            \"title\": \"Zwischensequenz 3\",\n        },\n        {\n            \"title\": \"Zwischensequenz 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Laras Haus\",\n        },\n        {\n            \"title\": \"Die Kavernen\",\n        },\n        {\n            \"title\": \" Die Stadt Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silberner Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Goldener Götze\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das verlorene Tal\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Zahnrad\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Grab des Qualopec\",\n        },\n        {\n            \"title\": \"St. Francis' Folly\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel des Neptun\",\n                },\n                \"key_2\": {\n                    \"name\": \"Schlüssel des Atlas\",\n                },\n                \"key_3\": {\n                    \"name\": \"Schlüssel des Damokles\",\n                },\n                \"key_4\": {\n                    \"name\": \"Schlüssel des Thor\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kolosseum\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Palast des Midas\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Goldbarren\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die Zisterne\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Goldener Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Silberner Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Grab des Tihocan\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Goldener Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die Stadt Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Saphir Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Obelisk von Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Saphir Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Auge des Horus\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Skarabäus\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Siegel des Anubis\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ankh\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Heiligtum des Scion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Goldener Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ankh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Skarabäus\",\n                }\n            }\n        },\n        {\n            \"title\": \"Natlas Katakomben\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Sicherung\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Schlüssel der Pyramide\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantis\",\n        },\n        {\n            \"title\": \"Die Große Pyramide\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Titel\",\n        },\n        {\n            \"title\": \"Aktuelle Position\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Die Stadt Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silberner Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Goldener Götze\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das verlorene Tal\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Zahnrad\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-en-gb.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"extends\": \"en\",\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- EMPLACEMENT VIDE -\",\n        },\n        \"stats\": {\n            \"secrets\": \"\\\\{review}Secrets\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Génère un éclairage dynamique pour les coups de feu et les explosions, comme apparu depuis TR2.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Corrige le fait que les sprites des végétations ne s'animent pas dans les zones aquatiques de La Citerne et de la Tombe de Tihocan.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Grande trousse de soins\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Petite trousse de soins\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Cinématique 1\",\n        },\n        {\n            \"title\": \"Cinématique 2\",\n        },\n        {\n            \"title\": \"Cinématique 3\",\n        },\n        {\n            \"title\": \"Cinématique 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Demeure de Lara\",\n        },\n        {\n            \"title\": \"Cavernes\",\n        },\n        {\n            \"title\": \"Cité de Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé en Argent\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Idole d'Or\",\n                }\n            }\n        },\n        {\n            \"title\": \"La Vallée Perdue\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Roue dentée\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tombe de Qualopec\",\n        },\n        {\n            \"title\": \"Monument St Francis\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé de Neptune\",\n                },\n                \"key_2\": {\n                    \"name\": \"Clé d'Atlas\",\n                },\n                \"key_3\": {\n                    \"name\": \"Clé de Damoclès\",\n                },\n                \"key_4\": {\n                    \"name\": \"Clé de Thor\",\n                }\n            }\n        },\n        {\n            \"title\": \"Colosseum\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé Rouillée\",\n                }\n            }\n        },\n        {\n            \"title\": \"Palais de Midas\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Lingot d'Or\",\n                }\n            }\n        },\n        {\n            \"title\": \"La Citerne\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé en Or\",\n                },\n                \"key_2\": {\n                    \"name\": \"Clé en Argent\",\n                },\n                \"key_3\": {\n                    \"name\": \"Clé Rouillée\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tombe de Tihocan\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé en Or\",\n                },\n                \"key_2\": {\n                    \"name\": \"Clé Rouillée\",\n                },\n                \"key_3\": {\n                    \"name\": \"Clé Rouillée\",\n                }\n            }\n        },\n        {\n            \"title\": \"Cité de Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé de Saphir\",\n                }\n            }\n        },\n        {\n            \"title\": \"Obélisque de Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé de Saphir\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Œil d'Horus\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scarabée\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Sceau d'Anubis\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ankh\",\n                }\n            }\n        },\n        {\n            \"title\": \"Sanctuaire du Scion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé en Or\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ankh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scarabée\",\n                }\n            }\n        },\n        {\n            \"title\": \"Mines de Natla\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Fusible\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Clé de la Pyramide\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantide\",\n        },\n        {\n            \"title\": \"La Grande Pyramide\",\n        },\n        {\n            \"title\": \"Réservé\",\n        },\n        {\n            \"title\": \"Réservé\",\n        },\n        {\n            \"title\": \"Réservé\",\n        },\n        {\n            \"title\": \"Réservé\",\n        },\n        {\n            \"title\": \"Titre\",\n        },\n        {\n            \"title\": \"Position Actuelle\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Cité de Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé en Argent\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Idole d'Or\",\n                }\n            }\n        },\n        {\n            \"title\": \"Vallée perdue\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Roue dentée\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- SLOT FALAMH %d -\",\n        },\n        \"stats\": {\n            \"secrets\": \"\\\\{review}Dùnadh\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Cuiridh seo solais fiùghantach ri losgadh armachd is spreadhaidhean, coltach ri TR2+.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Càraichidh bugan san t-Siasarn is Uaigh Tihocan far nach gluais planntrais anns an uisge.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Pasgan Mòr Slàinte\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Pasgan Beag Slàinte\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Sealladh-film 1\",\n        },\n        {\n            \"title\": \"Sealladh-film 2\",\n        },\n        {\n            \"title\": \"Sealladh-film 3\",\n        },\n        {\n            \"title\": \"Sealladh-film 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Dachaigh Lara\",\n        },\n        {\n            \"title\": \"Uaimhean\",\n        },\n        {\n            \"title\": \"Baile Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Airgid\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ìomhaigh Òir\",\n                }\n            }\n        },\n        {\n            \"title\": \"Gleann Caillte\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Gèar Inneil\",\n                }\n            }\n        },\n        {\n            \"title\": \"Uaigh Qualopec\",\n        },\n        {\n            \"title\": \"Gòraiche Naoimh Frang\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Neaptain\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Atlas\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair Damocles\",\n                },\n                \"key_4\": {\n                    \"name\": \"Iuchair Thor\",\n                }\n            }\n        },\n        {\n            \"title\": \"Colasaem\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lùchairt Mìdas\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Bàr Òir\",\n                }\n            }\n        },\n        {\n            \"title\": \"An t-Siasarn\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Òir\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Airgid\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                }\n            }\n        },\n        {\n            \"title\": \"Uaigh Tihocan\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Òir\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                }\n            }\n        },\n        {\n            \"title\": \"Baile Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Saoirbheir\",\n                }\n            }\n        },\n        {\n            \"title\": \"Obelisk Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Saoirbheir\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Sùil Horus\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Sgarab\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Ròn Anubis\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ankh\",\n                }\n            }\n        },\n        {\n            \"title\": \"Naomh-chomhnaidh an Scion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Òir\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ankh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Sgarab\",\n                }\n            }\n        },\n        {\n            \"title\": \"Mèinnean Natla\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Tadhail\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Iuchair Pioramaid\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantais\",\n        },\n        {\n            \"title\": \"Am Pioramaid Mhòr\",\n        },\n        {\n            \"title\": \"Neach-àite\",\n        },\n        {\n            \"title\": \"Neach-àite\",\n        },\n        {\n            \"title\": \"Neach-àite\",\n        },\n        {\n            \"title\": \"Neach-àite\",\n        },\n        {\n            \"title\": \"Tiotal\",\n        },\n        {\n            \"title\": \"Ionad Làithreach\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Baile Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Airgid\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ìomhaigh Òir\",\n                }\n            }\n        },\n        {\n            \"title\": \"Gleann Caillte\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Fiaclan Inneil\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- SLOT VUOTO %d -\",\n        },\n        \"stats\": {\n            \"secrets\": \"Segreti\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Abilita l'illuminazione dinamica per spari ed esplosioni, simile a TR2+.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Risolve i problemi preesistenti nei livelli \\\"Cisterna\\\" e \\\"Tomba di Tihocan\\\" per cui le animazioni degli sprite delle piante nelle aree acquatiche non funzionano.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Kit Medico Grande\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kit Medico Piccolo\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Intermezzo 1\",\n        },\n        {\n            \"title\": \"Intermezzo 2\",\n        },\n        {\n            \"title\": \"Intermezzo 3\",\n        },\n        {\n            \"title\": \"Intermezzo 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Casa di Lara\",\n        },\n        {\n            \"title\": \"Caverne\",\n        },\n        {\n            \"title\": \"Città di Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Argento\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Idolo d'Oro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Valle Perduta\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Ruota Dentata\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tomba di Qualopec\",\n        },\n        {\n            \"title\": \"Rovine di St. Francis\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave di Nettuno\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave di Atlante\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave di Damocle\",\n                },\n                \"key_4\": {\n                    \"name\": \"Chiave di Thor\",\n                }\n            }\n        },\n        {\n            \"title\": \"Colosseo\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave Arrugginita\",\n                }\n            }\n        },\n        {\n            \"title\": \"Palazzo di Mida\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Lingotto d'Oro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Cisterna\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Oro\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave d'Argento\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave Arrugginita\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tomba di Tihocan\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Oro\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave Arrugginita\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave Arrugginita\",\n                }\n            }\n        },\n        {\n            \"title\": \"Città di Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave di Zaffiro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Obelisco di Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave di Zaffiro\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Occhio di Horus\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scarabeo\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Sigillo di Anubi\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ankh\",\n                }\n            }\n        },\n        {\n            \"title\": \"Santuario dello Scion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Oro\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ankh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scarabeo\",\n                }\n            }\n        },\n        {\n            \"title\": \"Miniere di Natla\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Fusibile\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Chiave della Piramide\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantide\",\n        },\n        {\n            \"title\": \"La Grande Piramide\",\n        },\n        {\n            \"title\": \"Vuoto\",\n        },\n        {\n            \"title\": \"Vuoto\",\n        },\n        {\n            \"title\": \"Vuoto\",\n        },\n        {\n            \"title\": \"Vuoto\",\n        },\n        {\n            \"title\": \"Titoli\",\n        },\n        {\n            \"title\": \"Posizione Attuale\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Città di Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Argento\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Idolo d'Oro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Valle Perduta\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Ruota Dentata\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- PUSTY SLOT %d -\",\n        },\n        \"stats\": {\n            \"secrets\": \"\\\\{review}Sekrety\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Włącza dynamiczne oświetlenie przy strzelaniu z broni i eksplozjach, podobnie jak w TR2+.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Naprawia błędy oryginalnej gry w Rezerwuarze i Grobowcu Tihocana, gdzie glony w wodnych obszarach nie są animowane.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Duża apteczka\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Mała apteczka\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Przerywnik 1\",\n        },\n        {\n            \"title\": \"Przerywnik 2\",\n        },\n        {\n            \"title\": \"Przerywnik 3\",\n        },\n        {\n            \"title\": \"Przerywnik 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Dom Lary\",\n        },\n        {\n            \"title\": \"Jaskinie\",\n        },\n        {\n            \"title\": \"Miasto Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Srebrny klucz\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Złoty posążek\",\n                }\n            }\n        },\n        {\n            \"title\": \"Zaginiona Dolina\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Zębatka\",\n                }\n            }\n        },\n        {\n            \"title\": \"Grobowiec Qualopeca\",\n        },\n        {\n            \"title\": \"Kompleks św. Franciszka\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz Neptuna\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz Atlasa\",\n                },\n                \"key_3\": {\n                    \"name\": \"Klucz Damoklesa\",\n                },\n                \"key_4\": {\n                    \"name\": \"Klucz Thora\",\n                }\n            }\n        },\n        {\n            \"title\": \"Koloseum\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Zardzewiały klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pałac Midasa\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Sztabka złota\",\n                }\n            }\n        },\n        {\n            \"title\": \"Rezerwuar\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Złoty klucz\",\n                },\n                \"key_2\": {\n                    \"name\": \"Srebrny klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Zardzewiały klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Grobowiec Tihocana\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Złoty klucz\",\n                },\n                \"key_2\": {\n                    \"name\": \"Zardzewiały klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Zardzewiały klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Miasto Khamoona\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Szafirowy klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Obelisk Khamoona\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Szafirowy klucz\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Oko Horusa\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Skarabeusz\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Pieczęć Anubisa\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ankh\",\n                }\n            }\n        },\n        {\n            \"title\": \"Sanktuarium Scion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Złoty klucz\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ankh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Skarabeusz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kopalnie Natli\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Bezpiecznik\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Klucz do piramidy\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantis\",\n        },\n        {\n            \"title\": \"Wielka Piramida\",\n        },\n        {\n            \"title\": \"-\",\n        },\n        {\n            \"title\": \"-\",\n        },\n        {\n            \"title\": \"-\",\n        },\n        {\n            \"title\": \"-\",\n        },\n        {\n            \"title\": \"Menu główne\",\n        },\n        {\n            \"title\": \"Aktualna pozycja\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Miasto Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Srebrny klucz\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Złoty posążek\",\n                }\n            }\n        },\n        {\n            \"title\": \"Zaginiona Dolina\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Zębatka\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings-ru.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- ПУСТОЙ СЛОТ %d -\",\n        },\n        \"stats\": {\n            \"secrets\": \"\\\\{review}Секреты\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Позволяет генерировать динамическое освещение для выстрелов и взрывов, аналогично TR2+.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Исправляет изначальные проблемы в Цистерне и Гробнице Тихокана, из-за которых спрайты растений в водных зонах не анимировались.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Большая аптечка\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Маленькая аптечка\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Cutscene 1\",\n        },\n        {\n            \"title\": \"Cutscene 2\",\n        },\n        {\n            \"title\": \"Cutscene 3\",\n        },\n        {\n            \"title\": \"Cutscene 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Дом Лары\",\n        },\n        {\n            \"title\": \"Пещеры\",\n        },\n        {\n            \"title\": \"Город Вилкабамба\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Серебряный ключ\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Золотой идол\",\n                }\n            }\n        },\n        {\n            \"title\": \"Затерянная долина\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Шестерёнка\",\n                }\n            }\n        },\n        {\n            \"title\": \"Гробница Куалопека\",\n        },\n        {\n            \"title\": \"Монастырь св. Франциска\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ключ Нептуна\",\n                },\n                \"key_2\": {\n                    \"name\": \"Ключ Атласа\",\n                },\n                \"key_3\": {\n                    \"name\": \"Ключ Дамокла\",\n                },\n                \"key_4\": {\n                    \"name\": \"Ключ Тора\",\n                }\n            }\n        },\n        {\n            \"title\": \"Колизей\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ржавый ключ\",\n                }\n            }\n        },\n        {\n            \"title\": \"Дворец Мидаса\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Золотой слиток\",\n                }\n            }\n        },\n        {\n            \"title\": \"Цистерна\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Золотой ключ\",\n                },\n                \"key_2\": {\n                    \"name\": \"Серебряный ключ\",\n                },\n                \"key_3\": {\n                    \"name\": \"Ржавый ключ\",\n                }\n            }\n        },\n        {\n            \"title\": \"Гробница Тихокана\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Золотой ключ\",\n                },\n                \"key_2\": {\n                    \"name\": \"Ржавый ключ\",\n                },\n                \"key_3\": {\n                    \"name\": \"Ржавый ключ\",\n                }\n            }\n        },\n        {\n            \"title\": \"Город Хамун\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Сапфировый ключ\",\n                }\n            }\n        },\n        {\n            \"title\": \"Обелиск Хамун\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Сапфировый ключ\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Глаз Гора\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Скарабей\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Печать Анубиса\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Анк\",\n                }\n            }\n        },\n        {\n            \"title\": \"Святилище Наследия\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Золотой ключ\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Анк\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Скарабей\",\n                }\n            }\n        },\n        {\n            \"title\": \"Раскопки Натлы\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Предохранитель\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Ключ к пирамиде\",\n                }\n            }\n        },\n        {\n            \"title\": \"Атлантида\",\n        },\n        {\n            \"title\": \"Великая пирамида\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Title\",\n        },\n        {\n            \"title\": \"Current Position\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Город Вилкабамба\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Серебряный ключ\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Золотой идол\",\n                }\n            }\n        },\n        {\n            \"title\": \"Затерянная долина\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Шестерёнка\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"misc\": {\n            \"empty_slot_fmt\": \"- EMPTY SLOT %d -\",\n        },\n        \"stats\": {\n            \"secrets\": \"Secrets\",\n        }\n    },\n    \"settings\": {\n        \"visuals.enable_gun_lighting\": {\n            \"description\": \"Enables dynamic lighting to be generated for gunshots and explosions, similar to TR2+.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Fixes original issues in The Cistern and Tomb of Tihocan where plant sprites in water areas do not animate.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Large Medi Pack\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Small Medi Pack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Cutscene 1\",\n        },\n        {\n            \"title\": \"Cutscene 2\",\n        },\n        {\n            \"title\": \"Cutscene 3\",\n        },\n        {\n            \"title\": \"Cutscene 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Lara's Home\",\n        },\n        {\n            \"title\": \"Caves\",\n        },\n        {\n            \"title\": \"City of Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silver Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Gold Idol\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lost Valley\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Machine Cog\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tomb of Qualopec\",\n        },\n        {\n            \"title\": \"St. Francis' Folly\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Neptune Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Atlas Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Damocles Key\",\n                },\n                \"key_4\": {\n                    \"name\": \"Thor Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Colosseum\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Rusty Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Palace Midas\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Gold Bar\",\n                }\n            }\n        },\n        {\n            \"title\": \"The Cistern\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Gold Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Silver Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Rusty Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tomb of Tihocan\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Gold Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rusty Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Rusty Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"City of Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Sapphire Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Obelisk of Khamoon\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Sapphire Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Eye of Horus\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scarab\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Seal of Anubis\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ankh\",\n                }\n            }\n        },\n        {\n            \"title\": \"Sanctuary of the Scion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Gold Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ankh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scarab\",\n                }\n            }\n        },\n        {\n            \"title\": \"Natla's Mines\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Fuse\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Pyramid Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantis\",\n        },\n        {\n            \"title\": \"The Great Pyramid\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Dummy\",\n        },\n        {\n            \"title\": \"Title\",\n        },\n        {\n            \"title\": \"Current Position\",\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"City of Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silver Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Gold Idol\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lost Valley\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Machine Cog\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1/weapons.json5",
    "content": "{\n    \"LGT_UNARMED\": {\n        \"sample_num\": \"SFX_LARA_NO\",\n    },\n\n    \"LGT_FLARE\": {\n        \"ammo\": {\n            \"initial_qty\": 6,\n            \"pickup_qty\": 6,\n            \"pickup_qty_alt\": 8,\n        },\n        \"flash_shade\": 2048,\n        \"flash_pos\": {\n            \"x\": 11,\n            \"y\": 32,\n            \"z\": 80,\n        },\n        \"flash_pos_alt\": {\n            \"x\": -6,\n            \"y\": 6,\n            \"z\": 80,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"is_available\": false,\n    },\n\n    \"LGT_PISTOLS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 1,\n        \"ammo\": {\n            \"initial_qty\": 32,\n            \"pickup_qty\": 32,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 5120,\n        \"flash_pos\": {\n            \"y\": 155,\n            \"z\": 50,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_PISTOLS\",\n    },\n\n    \"LGT_MAGNUMS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 2,\n        \"ammo\": {\n            \"initial_qty\": 50,\n            \"pickup_qty\": 50,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\n            \"y\": 155,\n            \"z\": 55,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_MAGNUMS\",\n    },\n\n    \"LGT_AUTOS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 2,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\n            \"y\": 215,\n            \"z\": 65,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_AUTOS\",\n        \"is_available\": false,\n    },\n\n    \"LGT_DESERT_EAGLE\": {\n        \"type\": \"WEAPON_TYPE_SINGLE_PISTOL\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-10, +10, -80, +80],\n        \"right_angles\": [0, 0, 0, 0],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 4,\n        \"gun_height\": 650,\n        \"damage\": 21,\n        \"ammo\": {\n            \"initial_qty\": 10,\n            \"pickup_qty\": 10,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 16,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\n            \"y\": 215,\n            \"z\": 65,\n        },\n        \"sample_num\": \"SFX_LARA_DESERT_EAGLE\",\n        \"is_available\": false,\n    },\n\n    \"LGT_UZIS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 1,\n        \"ammo\": {\n            \"initial_qty\": 100,\n            \"pickup_qty\": 100,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 3,\n        \"flash_time\": 2,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 180,\n            \"z\": 55,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_UZI_FIRE\",\n    },\n\n    \"LGT_SHOTGUN\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 0,\n        \"gun_height\": 500,\n        \"damage\": 4,\n        \"ammo\": {\n            \"initial_qty\": 2,\n            \"pickup_qty\": 2,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 285,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_SHOTGUN\",\n    },\n\n    \"LGT_M16\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 4,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 12.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"sample_num\": \"\",\n        \"flash_pos\": {\n            \"y\": 400,\n            \"z\": 99,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {\"z\": -65},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"is_available\": false,\n    },\n\n    \"LGT_MP5\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 60,\n            \"pickup_qty\": 60,\n        },\n        \"target_dist\": 12.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 16,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 332,\n            \"z\": 96,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {\"z\": -65},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"\",\n        \"is_available\": false,\n    },\n\n    \"LGT_ROCKET\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 30,\n        \"ammo\": {\n            \"initial_qty\": 1,\n            \"pickup_qty\": 1,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 12,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n        \"is_available\": false,\n    },\n\n    \"LGT_GRENADE\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 30,\n        \"ammo\": {\n            \"initial_qty\": 2,\n            \"pickup_qty\": 2,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 0,\n        \"draw_frame\": 13,\n        \"undraw_frame\": 14,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n        \"is_available\": false,\n    },\n\n    \"LGT_HARPOON\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -65, +65],\n        \"left_angles\": [-80, +80, -75, +75],\n        \"right_angles\": [-80, +80, -75, +75],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 4,\n        \"ammo\": {\n            \"initial_qty\": 3,\n            \"pickup_qty\": 3,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n        \"is_available\": false,\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 1,\n    \"extends\": \"tr1\",\n    \"name\": \"TR1 PC Demo\",\n\n    // path to the main menu background image\n    \"main_menu_picture\": \"title.webp\",\n\n    // path to the savegame file\n    \"savegame_file_fmt\": \"save_demo_pc_%02d.dat\",\n\n    \"injections\": [\n        \"braid.bin\",\n        \"bubbles.bin\",\n        \"gun_glow.bin\",\n        \"lara_animations.bin\",\n        \"lara_guns.bin\",\n        \"uzi_sfx.bin\",\n        \"explosion.bin\",\n        \"pickup_aid.bin\",\n        \"sprite_alignment.bin\",\n        \"pda_model.bin\",\n        \"font.bin\",\n        \"winston_model.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n    ],\n\n    \"enable_tr2_item_drops\": false,\n    \"convert_dropped_guns\": false,\n\n    \"title\": {\n        \"path\": [\n            \"data_demo_pc/title.phd\",\n            \"title.phd\",\n        ],\n        \"music_track\": 0,\n        \"inherit_injections\": false,\n        \"sequence\": [\n            {\"type\": \"exit_to_title\"},\n        ],\n        \"injections\": [\n            \"pda_model.bin\",\n            \"font.bin\",\n            \"title_textures.bin\",\n            \"misc_sprites.bin\",\n        ],\n    },\n\n    \"levels\": [\n        // Level 2: City of Vilcabamba\n        {\n            \"path\": [\n                \"data_demo_pc/level2.phd\",\n                \"level2.phd\",\n            ],\n            \"music_track\": 0,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"peru.webp\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n    ],\n\n    \"hidden_config\": [\n        \"enable_item_examining\",\n        \"healthbar_poison_color\",\n        \"healthbar_poison_color_ps1\",\n        \"enemy_healthbar_color_allies\",\n        \"enemy_healthbar_color_allies_ps1\",\n        \"exposurebar_color\",\n        \"exposurebar_color_ps1\",\n        \"exposurebar_location\",\n        \"exposurebar_show_mode\",\n        \"enable_ally_targeting\",\n        \"enable_weather\",\n        \"enable_footprints\",\n        \"ally_hostility_policy\",\n        \"fix_monkey_pickup_priority\",\n        \"fix_pipeman_aim\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Die Stadt Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silberner Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Goldener Götze\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"\\\\{review}Ville de Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé en argent\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Idole en or\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Baile Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Airgid\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ìomhaigh Òir\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Città di Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Argento\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Idolo d'Oro\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Miasto Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Srebrny klucz\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Złoty posążek\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings-ru.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Город Вилкабамба\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Серебряный ключ\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Золотой идол\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-demo-pc/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"City of Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silver Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Gold Idol\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/gameflow.json5",
    "content": "{\n    // This file is used to enable the -l argument support.\n\n    \"engine\": 1,\n    \"extends\": \"tr1\",\n    \"name\": \"TR1 Direct Level\",\n\n    \"main_menu_picture\": \"title.webp\",\n    \"savegame_file_fmt\": \"save_tmp_%02d.dat\",\n\n    \"injections\": [\n        \"braid.bin\",\n        \"bubbles.bin\",\n        \"gun_glow.bin\",\n        \"lara_animations.bin\",\n        \"lara_extra.bin\",\n        \"lara_guns.bin\",\n        \"uzi_sfx.bin\",\n        \"explosion.bin\",\n        \"pickup_aid.bin\",\n        \"sprite_alignment.bin\",\n        \"pda_model.bin\",\n        \"font.bin\",\n        \"winston_model.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n    ],\n\n    \"enable_tr2_item_drops\": false,\n    \"convert_dropped_guns\": false,\n\n    \"levels\": [\n        {\n            \"path\": \"%direct_level%\",\n            \"music_track\": 0,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [],\n        },\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Testlevel\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"\\\\{review}Niveau de test\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Ìre Phròbhail\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Livello di Prova\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Poziom testowy\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings-ru.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Тестовый уровень\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-level/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Test Level\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 1,\n    \"extends\": \"tr1\",\n    \"name\": \"Unfinished Business\",\n\n    \"main_menu_picture\": \"title_ub.webp\",\n    \"savegame_file_fmt\": \"save_trub_%02d.dat\",\n\n    \"injections\": [\n        \"braid.bin\",\n        \"bubbles.bin\",\n        \"gun_glow.bin\",\n        \"lara_animations.bin\",\n        \"lara_guns.bin\",\n        \"uzi_sfx.bin\",\n        \"explosion.bin\",\n        \"pickup_aid.bin\",\n        \"sprite_alignment.bin\",\n        \"pda_model.bin\",\n        \"font.bin\",\n        \"winston_model.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n        \"crystal.bin\",\n    ],\n\n    \"enable_tr2_item_drops\": false,\n    \"convert_dropped_guns\": false,\n\n    \"enforced_config\": {\n        \"fix_water_exit\": false,\n    },\n\n    \"title\": {\n        \"path\": \"title.phd\",\n        \"music_track\": 2,\n        \"inherit_injections\": false,\n        \"injections\": [\n            \"pda_model.bin\",\n            \"font.bin\",\n            \"title_textures.bin\",\n            \"misc_sprites.bin\",\n        ],\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"eidos.webp\", \"legal\": true, \"display_time\": 1, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 1},\n            {\"type\": \"exit_to_title\"},\n        ],\n    },\n\n    \"levels\": [\n        // Level 1: Return to Egypt\n        {\n            \"path\": \"egypt.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"ub_loading1.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"egypt_cameras.bin\",\n                \"egypt_fd.bin\",\n                \"egypt_itemrots.bin\",\n                \"egypt_meshfixes.bin\",\n                \"egypt_textures.bin\",\n                \"panther_sfx.bin\",\n                \"egypt_crystals.bin\",\n            ],\n            \"unobtainable_kills\": 1,\n        },\n\n        // Level 2: Temple of the Cat\n        {\n            \"path\": \"cat.phd\",\n            \"music_track\": 59,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"ub_loading1.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"cat_cameras.bin\",\n                \"cat_fd.bin\",\n                \"cat_itemrots.bin\",\n                \"cat_meshfixes.bin\",\n                \"cat_textures.bin\",\n                \"panther_sfx.bin\",\n                \"cat_crystals.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // Level 3: Atlantean Stronghold\n        {\n            \"path\": \"end.phd\",\n            \"music_track\": 60,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"ub_loading2.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"door61_sfx.bin\",\n                \"stronghold_fd.bin\",\n                \"stronghold_itemrots.bin\",\n                \"stronghold_textures.bin\",\n                \"stronghold_crystals.bin\",\n            ],\n            \"unobtainable_kills\": 1,\n        },\n\n        // Level 4: The Hive\n        {\n            \"path\": \"end2.phd\",\n            \"music_track\": 60,\n            \"lara_outfit\": \"tr1_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"ub_loading2.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_music\", \"music_track\": 19},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"end.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_ub.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_1.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_2.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credits_3.webp\", \"display_time\": 7.5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"total_stats\", \"background_path\": \"install.webp\"},\n                {\"type\": \"exit_to_title\"},\n            ],\n            \"injections\": [\n                \"hive_fd.bin\",\n                \"hive_itemrots.bin\",\n                \"hive_textures.bin\",\n                \"hive_crystals.bin\",\n            ],\n        },\n\n        {\"type\": \"dummy\"},\n\n        // Level 6: Current Position\n        // This level is necessary to read TombATI's save files.\n        {\n            \"path\": \"current.phd\",\n            \"type\": \"current\",\n            \"music_track\": 0,\n            \"inherit_injections\": false,\n            \"sequence\": [{\"type\": \"exit_to_title\"}],\n        },\n    ],\n\n    \"fmvs\": [\n        {\"path\": \"core.avi\", \"legal\": true},\n        {\"path\": \"escape.avi\", \"legal\": true},\n    ],\n\n    \"hidden_config\": [\n        \"enable_cutscenes\",       // UB has no cutscenes\n        \"enable_demo\",            // UB has no demos\n        \"enable_item_examining\",  // UB has no special item descriptions\n        \"healthbar_poison_color\",\n        \"healthbar_poison_color_ps1\",\n        \"enemy_healthbar_color_allies\",\n        \"enemy_healthbar_color_allies_ps1\",\n        \"exposurebar_color\",\n        \"exposurebar_color_ps1\",\n        \"exposurebar_location\",\n        \"exposurebar_show_mode\",\n        \"fix_water_exit\",\n        \"change_pierre_spawn\",\n        \"fix_bear_ai\",\n        \"fix_monkey_pickup_priority\",\n        \"restore_ps1_enemies\",\n        \"disable_trex_collision\",\n        \"fix_tihocan_secret_sound\",\n        \"fix_animated_sprites\",\n        \"enable_ally_targeting\",\n        \"enable_weather\",\n        \"enable_footprints\",\n        \"ally_hostility_policy\",\n        \"fix_pipeman_aim\",\n        \"enable_cinematics\",\n        \"enable_body_bags\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Rückkehr nach Ägypten\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Goldener Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Tempel der Katze\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Antiker Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die Festung von Atlantis\",\n        },\n        {\n            \"title\": \"Das Nest\",\n        },\n        {\n            \"title\": \"Titel\",\n        },\n        {\n            \"title\": \"Aktuelle Position\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Retour en Égypte\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé en or\",\n                }\n            }\n        },\n        {\n            \"title\": \"Le Temple du Chat\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Clé ornée\",\n                }\n            }\n        },\n        {\n            \"title\": \"La Forteresse Atlante\",\n        },\n        {\n            \"title\": \"La Ruche\",\n        },\n        {\n            \"title\": \"Titre\",\n        },\n        {\n            \"title\": \"Position actuelle\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"A' tilleadh dhan Eiphit\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Òir\",\n                }\n            }\n        },\n        {\n            \"title\": \"Teampall a' Chait\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Grinn\",\n                }\n            }\n        },\n        {\n            \"title\": \"An Daingneach Atlantais\",\n        },\n        {\n            \"title\": \"An Nead\",\n        },\n        {\n            \"title\": \"Tiotal\",\n        },\n        {\n            \"title\": \"Suidheachadh Làithreach\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Ritorno in Egitto\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Oro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tempio del Gatto\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave Decorata\",\n                }\n            }\n        },\n        {\n            \"title\": \"Fortezza Atlantidea\",\n        },\n        {\n            \"title\": \"L'Alveare\",\n        },\n        {\n            \"title\": \"Titoli\",\n        },\n        {\n            \"title\": \"Posizione Attuale\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Powrót do Egiptu\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Złoty klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Świątynia Kota\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ozdobny klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Twierdza Atlantydów\",\n        },\n        {\n            \"title\": \"Rój\",\n        },\n        {\n            \"title\": \"Menu główne\",\n        },\n        {\n            \"title\": \"Aktualna pozycja\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings-ru.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Возвращение в Египет\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Золотой ключ\",\n                }\n            }\n        },\n        {\n            \"title\": \"Храм Кошки\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Богато украшенный ключ\",\n                }\n            }\n        },\n        {\n            \"title\": \"Крепость атлантов\",\n        },\n        {\n            \"title\": \"Улей\",\n        },\n        {\n            \"title\": \"Оглавление\",\n        },\n        {\n            \"title\": \"Текущая позиция\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr1-ub/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Return to Egypt\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Gold Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Temple of the Cat\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ornate Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Atlantean Stronghold\",\n        },\n        {\n            \"title\": \"The Hive\",\n        },\n        {\n            \"title\": \"Title\",\n        },\n        {\n            \"title\": \"Current Position\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/catalog_item_actions.csv",
    "content": "0, ITEM_ACTION_TURN_180\n1, ITEM_ACTION_FLOOR_SHAKE\n2, ITEM_ACTION_LARA_NORMAL\n3, ITEM_ACTION_BUBBLES\n4, ITEM_ACTION_FINISH_LEVEL\n5, ITEM_ACTION_FLOOD\n6, ITEM_ACTION_CHANDELIER\n7, ITEM_ACTION_RUBBLE\n8, ITEM_ACTION_PISTON\n9, ITEM_ACTION_CURTAIN\n10, ITEM_ACTION_SET_CHANGE\n11, ITEM_ACTION_EXPLOSION\n12, ITEM_ACTION_LARA_HANDS_FREE\n13, ITEM_ACTION_FLIP_MAP\n14, ITEM_ACTION_LARA_DRAW_RIGHT_GUN\n15, ITEM_ACTION_LARA_DRAW_LEFT_GUN\n18, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1\n19, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2\n20, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3\n21, ITEM_ACTION_INVISIBILITY_ON\n22, ITEM_ACTION_INVISIBILITY_OFF\n23, ITEM_ACTION_DYNAMIC_LIGHT_ON\n24, ITEM_ACTION_DYNAMIC_LIGHT_OFF\n25, ITEM_ACTION_STATUE\n26, ITEM_ACTION_RESET_HAIR\n27, ITEM_ACTION_BOILER\n28, ITEM_ACTION_ASSAULT_RESET\n29, ITEM_ACTION_ASSAULT_STOP\n30, ITEM_ACTION_ASSAULT_START\n31, ITEM_ACTION_ASSAULT_FINISHED\n32, ITEM_ACTION_SHADOW_ON\n33, ITEM_ACTION_SHADOW_OFF\n62, ITEM_ACTION_TURN_90\n"
  },
  {
    "path": "data/trx/ship/games/tr2/catalog_lara_anims.csv",
    "content": "0, LA_RUN\n1, LA_WALK_FORWARD\n2, LA_WALK_STOP_RIGHT\n3, LA_WALK_STOP_LEFT\n4, LA_WALK_TO_RUN_RIGHT\n5, LA_WALK_TO_RUN_LEFT\n6, LA_RUN_START\n7, LA_RUN_TO_WALK_RIGHT\n8, LA_RUN_TO_STAND_LEFT\n9, LA_RUN_TO_WALK_LEFT\n10, LA_RUN_TO_STAND_RIGHT\n11, LA_STAND_STILL\n12, LA_TURN_RIGHT_SLOW\n13, LA_TURN_LEFT_SLOW\n14, LA_JUMP_FORWARD_LAND_START_UNUSED\n15, LA_JUMP_FORWARD_LAND_END_UNUSED\n16, LA_RUN_JUMP_RIGHT_START\n17, LA_RUN_JUMP_RIGHT_CONTINUE\n18, LA_RUN_JUMP_LEFT_START\n19, LA_RUN_JUMP_LEFT_CONTINUE\n20, LA_WALK_FORWARD_START\n21, LA_WALK_FORWARD_START_CONTINUE\n22, LA_JUMP_FORWARD_TO_FREEFALL\n23, LA_FREEFALL\n24, LA_FREEFALL_LAND\n25, LA_FREEFALL_LAND_DEATH\n26, LA_STAND_TO_JUMP_UP\n27, LA_STAND_TO_JUMP_UP_CONTINUE\n28, LA_JUMP_UP\n29, LA_JUMP_UP_TO_HANG_UNUSED\n30, LA_JUMP_UP_TO_FREEFALL\n31, LA_JUMP_UP_LAND\n32, LA_SMASH_JUMP\n33, LA_SMASH_JUMP_CONTINUE\n34, LA_FALL_START\n35, LA_FALL\n36, LA_FALL_TO_FREEFALL\n37, LA_HANG_TO_FREEFALL\n38, LA_WALK_BACK_END_RIGHT\n39, LA_WALK_BACK_END_LEFT\n40, LA_WALK_BACK\n41, LA_WALK_BACK_START\n42, LA_CLIMB_3CLICK\n43, LA_CLIMB_3CLICK_END_TO_RUN\n44, LA_TURN_RIGHT\n45, LA_JUMP_FORWARD_TO_FREEFALL_2\n46, LA_REACH_TO_FREEFALL\n47, LA_ROLL_ALTERNATE\n48, LA_ROLL_END_ALTERNATE\n49, LA_JUMP_FORWARD_END_TO_FREEFALL\n50, LA_CLIMB_2CLICK\n51, LA_CLIMB_2CLICK_END\n52, LA_CLIMB_2CLICK_END_TO_RUN\n53, LA_WALL_SMASH_LEFT\n54, LA_WALL_SMASH_RIGHT\n55, LA_RUN_UP_STEP_RIGHT\n56, LA_RUN_UP_STEP_LEFT\n57, LA_WALK_UP_STEP_RIGHT\n58, LA_WALK_UP_STEP_LEFT\n59, LA_WALK_DOWN_LEFT\n60, LA_WALK_DOWN_RIGHT\n61, LA_WALK_DOWN_BACK_LEFT\n62, LA_WALK_DOWN_BACK_RIGHT\n63, LA_WALL_SWITCH_DOWN\n64, LA_WALL_SWITCH_UP\n65, LA_SIDE_STEP_LEFT\n66, LA_SIDE_STEP_LEFT_END\n67, LA_SIDE_STEP_RIGHT\n68, LA_SIDE_STEP_RIGHT_END\n69, LA_ROTATE_LEFT\n70, LA_SLIDE_FORWARD\n71, LA_SLIDE_FORWARD_END\n72, LA_SLIDE_FORWARD_STOP\n73, LA_STAND_TO_JUMP\n74, LA_JUMP_BACK_START\n75, LA_JUMP_BACK\n76, LA_JUMP_FORWARD_START\n77, LA_JUMP_FORWARD\n78, LA_JUMP_LEFT_START\n79, LA_JUMP_LEFT\n80, LA_JUMP_RIGHT_START\n81, LA_JUMP_RIGHT\n82, LA_LAND\n83, LA_JUMP_BACK_TO_FREEFALL\n84, LA_JUMP_LEFT_TO_FREEFALL\n85, LA_JUMP_RIGHT_TO_FREEFALL\n86, LA_UNDERWATER_SWIM_FORWARD\n87, LA_UNDERWATER_SWIM_FORWARD_DRIFT\n88, LA_SMALL_JUMP_BACK_START\n89, LA_SMALL_JUMP_BACK\n90, LA_SMALL_JUMP_BACK_END\n91, LA_JUMP_UP_START\n92, LA_LAND_TO_RUN\n93, LA_FALL_BACK\n94, LA_JUMP_FORWARD_TO_REACH\n95, LA_REACH\n96, LA_REACH_TO_HANG\n97, LA_CLIMB_ON\n98, LA_REACH_TO_FREEFALL_2\n99, LA_FALL_CROUCHING_LANDING\n100, LA_JUMP_FORWARD_TO_REACH_LATE\n101, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE\n102, LA_CLIMB_ON_END\n103, LA_STAND_IDLE\n104, LA_SLIDE_BACKWARD_START\n105, LA_SLIDE_BACKWARD\n106, LA_SLIDE_BACKWARD_END\n107, LA_UNDERWATER_SWIM_TO_IDLE\n108, LA_UNDERWATER_IDLE\n109, LA_UNDERWARER_IDLE_TO_SWIM\n110, LA_ONWATER_IDLE\n111, LA_ONWATER_TO_STAND_HIGH\n112, LA_FREEFALL_TO_UNDERWATER\n113, LA_ONWATER_DIVE_ALTERNATE\n114, LA_UNDERWATER_TO_ONWATER\n115, LA_ONWATER_SWIM_FORWARD_DIVE\n116, LA_ONWATER_SWIM_FORWARD\n117, LA_ONWATER_SWIM_FORWARD_TO_IDLE\n118, LA_ONWATER_IDLE_TO_SWIM_FORWARD\n119, LA_ONWATER_DIVE\n120, LA_PUSHABLE_GRAB\n121, LA_PUSHABLE_RELEASE\n122, LA_PUSHABLE_PULL\n123, LA_PUSHABLE_PUSH\n124, LA_UNDERWATER_DEATH\n125, LA_HIT_FRONT\n126, LA_HIT_BACK\n127, LA_HIT_LEFT\n128, LA_HIT_RIGHT\n129, LA_UNDERWATER_SWITCH\n130, LA_UNDERWATER_PICKUP\n131, LA_USE_KEY\n132, LA_ONWATER_DEATH\n133, LA_RUN_DEATH\n134, LA_USE_PUZZLE\n135, LA_PICKUP\n136, LA_SHIMMY_LEFT\n137, LA_SHIMMY_RIGHT\n138, LA_STAND_DEATH\n139, LA_BOULDER_DEATH\n140, LA_ONWATER_IDLE_TO_SWIM_BACK\n141, LA_ONWATER_SWIM_BACK\n142, LA_ONWATER_SWIM_BACK_TO_IDLE\n143, LA_ONWATER_SWIM_LEFT\n144, LA_ONWATER_SWIM_RIGHT\n145, LA_DEATH_JUMP\n146, LA_ROLL_START\n147, LA_ROLL_CONTINUE\n148, LA_ROLL_END\n149, LA_SPIKE_DEATH\n150, LA_SWING_IN_FAST\n151, LA_SWANDIVE_ROLL\n152, LA_SWANDIVE_TO_UNDERWATER\n153, LA_FREEFALL_SWANDIVE\n154, LA_FREEFALL_SWANDIVE_TO_UNDERWATER\n155, LA_SWANDIVE_DEATH\n156, LA_SWANDIVE_LEFT\n157, LA_SWANDIVE_RIGHT\n158, LA_SWANDIVE_START\n159, LA_CLIMB_ON_HANDSTAND\n207, LA_RUN_JUMP_ROLL_START\n208, LA_SOMERSAULT\n209, LA_RUN_JUMP_ROLL_END\n210, LA_JUMP_FORWARD_ROLL_START\n211, LA_JUMP_FORWARD_ROLL_END\n212, LA_JUMP_BACK_ROLL_START\n213, LA_JUMP_BACK_ROLL_END\n203, LA_UNDERWATER_ROLL_START\n205, LA_UNDERWATER_ROLL_END\n191, LA_ONWATER_TO_STAND_MEDIUM\n177, LA_WADE\n178, LA_RUN_TO_WADE_LEFT\n179, LA_RUN_TO_WADE_RIGHT\n180, LA_WADE_TO_RUN_LEFT\n181, LA_WADE_TO_RUN_RIGHT\n184, LA_WADE_TO_STAND_RIGHT\n185, LA_WADE_TO_STAND_LEFT\n186, LA_STAND_TO_WADE\n190, LA_ONWATER_TO_WADE\n193, LA_ONWATER_TO_WADE_LOW\n192, LA_UNDERWATER_TO_STAND\n198, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE\n199, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL\n200, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM\n218, LA_SLIDE_FORWARD_TO_RUN\n219, LA_JUMP_NEUTRAL_ROLL\n220, LA_CONTROLLED_DROP\n221, LA_CONTROLLED_DROP_CONTINUE\n222, LA_HANG_TO_JUMP_UP\n223, LA_HANG_TO_JUMP_UP_CONTINUE\n224, LA_HANG_TO_JUMP_BACK\n225, LA_HANG_TO_JUMP_BACK_CONTINUE\n226, LA_SPRINT\n227, LA_RUN_TO_SPRINT_LEFT\n228, LA_RUN_TO_SPRINT_RIGHT\n229, LA_SPRINT_SLIDE_STAND_LEFT\n230, LA_SPRINT_SLIDE_STAND_RIGHT\n231, LA_SPRINT_TO_ROLL_LEFT\n232, LA_SPRINT_ROLL_LEFT_TO_RUN\n233, LA_SPRINT_TO_ROLL_RIGHT\n234, LA_SPRINT_ROLL_RIGHT_TO_RUN\n235, LA_SPRINT_TO_RUN_LEFT\n236, LA_SPRINT_TO_RUN_RIGHT\n237, LA_POSE_RIGHT_START\n238, LA_POSE_RIGHT_CONTINUE\n239, LA_POSE_RIGHT_END\n240, LA_POSE_LEFT_START\n241, LA_POSE_LEFT_CONTINUE\n242, LA_POSE_LEFT_END\n160, LA_STAND_TO_LADDER\n161, LA_LADDER_UP\n162, LA_LADDER_UP_STOP_RIGHT\n163, LA_LADDER_UP_STOP_LEFT\n164, LA_LADDER_IDLE\n165, LA_LADDER_UP_START\n166, LA_LADDER_DOWN_STOP_LEFT\n167, LA_LADDER_DOWN_STOP_RIGHT\n168, LA_LADDER_DOWN\n169, LA_LADDER_DOWN_START\n170, LA_LADDER_RIGHT\n171, LA_LADDER_LEFT\n172, LA_LADDER_HANG\n173, LA_LADDER_HANG_TO_IDLE\n174, LA_LADDER_CLIMB_ON\n182, LA_LADDER_BACKFLIP_START\n183, LA_LADDER_BACKFLIP_CONTINUE\n187, LA_LADDER_UP_HANGING\n188, LA_LADDER_DOWN_HANGING\n194, LA_LADDER_TO_HANG_DOWN\n201, LA_LADDER_TO_HANG_RIGHT\n202, LA_LADDER_TO_HANG_LEFT\n175, LA_UNKNOWN\n176, LA_ONWATER_TO_WADE_SHALLOW_UNUSED\n189, LA_FLARE_THROW\n195, LA_SWITCH_SMALL_DOWN\n196, LA_SWITCH_SMALL_UP\n197, LA_BUTTON_PUSH\n204, LA_FLARE_PICKUP\n206, LA_UNDERWATER_FLARE_PICKUP\n214, LA_KICK\n215, LA_ZIPLINE_GRAB\n216, LA_ZIPLINE_RIDE\n217, LA_ZIPLINE_FALL\n243, LA_STAND_TO_CROUCH\n244, LA_STAND_TO_CROUCH_END\n245, LA_STAND_TO_CROUCH_ABORT_UNUSED\n246, LA_RUN_TO_CROUCH_LEFT_START\n247, LA_RUN_TO_CROUCH_LEFT_END\n248, LA_RUN_TO_CROUCH_RIGHT_START\n249, LA_RUN_TO_CROUCH_RIGHT_END\n250, LA_SPRINT_TO_CROUCH_LEFT\n251, LA_SPRINT_TO_CROUCH_RIGHT\n252, LA_HANG_TO_CROUCH_START\n253, LA_HANG_TO_CROUCH_END\n254, LA_CROUCH_IDLE\n255, LA_CROUCH_TO_STAND\n256, LA_CROUCH_PICKUP\n257, LA_CROUCH_PICKUP_FLARE\n258, LA_CROUCH_HIT_FRONT\n259, LA_CROUCH_HIT_BACK\n260, LA_CROUCH_HIT_RIGHT\n261, LA_CROUCH_HIT_LEFT\n262, LA_CROUCH_ROLL_FORWARD_START\n263, LA_CROUCH_ROLL_FORWARD_CONTINUE\n264, LA_CROUCH_ROLL_FORWARD_END\n265, LA_CROUCH_ROLL_FORWARD_START_ALTERNATE_UNUSED\n266, LA_CROUCH_TO_CRAWL_START\n267, LA_CROUCH_TO_CRAWL_CONTINUE\n268, LA_CROUCH_TO_CRAWL_END\n269, LA_CRAWL_IDLE\n270, LA_CRAWL_TO_CROUCH_START\n271, LA_CRAWL_TO_CROUCH_CONTINUE\n272, LA_CRAWL_TO_CROUCH_END_UNUSED\n273, LA_CRAWL_IDLE_TO_FORWARD\n274, LA_CRAWL_FORWARD\n275, LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT\n276, LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT\n277, LA_CRAWL_FORWARD_TO_IDLE_START_LEFT\n278, LA_CRAWL_FORWARD_TO_IDLE_END_LEFT\n279, LA_CRAWL_TURN_LEFT\n280, LA_CRAWL_TURN_RIGHT\n281, LA_CRAWL_IDLE_TO_BACKWARD\n282, LA_CRAWL_BACKWARD\n283, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START\n284, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END\n285, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START\n286, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END\n287, LA_CRAWL_TURN_LEFT_EARLY_END\n288, LA_CRAWL_TURN_RIGHT_EARLY_END\n289, LA_CRAWL_TO_HANG_START\n290, LA_CRAWL_TO_HANG_CONTINUE\n291, LA_CRAWL_TO_HANG_END\n292, LA_CRAWL_PICKUP\n293, LA_CRAWL_HIT_FRONT_UNUSED\n294, LA_CRAWL_HIT_BACK_UNUSED\n295, LA_CRAWL_HIT_RIGHT_UNUSED\n296, LA_CRAWL_HIT_LEFT_UNUSED\n297, LA_CRAWL_DEATH\n298, LA_CRAWL_JUMP_DOWN\n299, LA_CROUCH_TURN_LEFT\n300, LA_CROUCH_TURN_RIGHT\n301, LA_JUMP_FORWARD_START_TO_GRAB_EARLY\n302, LA_JUMP_FORWARD_START_TO_GRAB_LATE\n303, LA_RUN_TO_GRAB_RIGHT\n304, LA_RUN_TO_GRAB_LEFT\n305, LA_SWING_IN_SLOW\n306, LA_MONKEY_IDLE\n307, LA_MONKEY_FALL\n308, LA_MONKEY_GRAB\n309, LA_MONKEY_FORWARD\n310, LA_MONKEY_STOP_LEFT\n311, LA_MONKEY_STOP_RIGHT\n312, LA_MONKEY_IDLE_TO_FORWARD_LEFT\n313, LA_MONKEY_IDLE_TO_FORWARD_RIGHT\n314, LA_MONKEY_SHIMMY_LEFT\n315, LA_MONKEY_SHIMMY_LEFT_END\n316, LA_MONKEY_SHIMMY_RIGHT\n317, LA_MONKEY_SHIMMY_RIGHT_END\n318, LA_MONKEY_TURN_AROUND\n319, LA_MONKEY_TURN_LEFT\n320, LA_MONKEY_TURN_RIGHT\n321, LA_MONKEY_TURN_LEFT_EARLY_END\n322, LA_MONKEY_TURN_LEFT_LATE_END\n323, LA_MONKEY_TURN_RIGHT_EARLY_END\n324, LA_MONKEY_TURN_RIGHT_LATE_END\n325, LA_SPRINT_SLIDE_STAND_RIGHT_END_ALTERNATE_UNUSED\n326, LA_SPRINT_SLIDE_STAND_LEFT_END_ALTERNATE_UNUSED\n327, LA_SPRINT_TO_ROLL_LEFT_BETA_UNUSED\n328, LA_SPRINT_TO_ROLL_ALTERNATE_START_UNUSED\n329, LA_SPRINT_TO_ROLL_ALTERNATE_CONTINUE_UNUSED\n330, LA_SPRINT_TO_ROLL_ALTERNATE_END_UNUSED\n331, LA_LADDER_TO_CROUCH_START\n332, LA_LADDER_TO_CROUCH_END\n"
  },
  {
    "path": "data/trx/ship/games/tr2/catalog_lara_states.csv",
    "content": "0, LS_WALK\n1, LS_RUN\n2, LS_STOP\n3, LS_JUMP_FORWARD\n4, LS_POSE\n5, LS_FAST_BACK\n6, LS_TURN_RIGHT\n7, LS_TURN_LEFT\n8, LS_DEATH\n9, LS_FAST_FALL\n10, LS_HANG\n11, LS_REACH\n12, LS_SPLAT\n13, LS_TREAD\n14, LS_LAND\n15, LS_COMPRESS\n16, LS_WALK_BACK\n17, LS_SWIM\n18, LS_GLIDE\n19, LS_PULL_UP\n20, LS_FAST_TURN\n21, LS_STEP_RIGHT\n22, LS_STEP_LEFT\n23, LS_ROLL_CONT\n24, LS_SLIDE\n25, LS_JUMP_BACK\n26, LS_JUMP_RIGHT\n27, LS_JUMP_LEFT\n28, LS_JUMP_UP\n29, LS_FALL_BACK\n30, LS_SHIMMY_LEFT\n31, LS_SHIMMY_RIGHT\n32, LS_SLIDE_BACK\n33, LS_SURF_TREAD\n34, LS_SURF_SWIM\n35, LS_DIVE\n36, LS_PUSH_BLOCK\n37, LS_PULL_BLOCK\n38, LS_PP_READY\n39, LS_PICKUP\n40, LS_SWITCH_ON\n41, LS_SWITCH_OFF\n42, LS_USE_KEY\n43, LS_USE_PUZZLE\n44, LS_UW_DEATH\n45, LS_ROLL\n46, LS_SPECIAL\n47, LS_SURF_BACK\n48, LS_SURF_LEFT\n49, LS_SURF_RIGHT\n50, LS_USE_MIDAS\n51, LS_DIE_MIDAS\n52, LS_SWAN_DIVE\n53, LS_FAST_DIVE\n54, LS_GYMNAST\n55, LS_WATER_OUT\n79, LS_CONTROLLED\n68, LS_TWIST\n66, LS_WATER_ROLL\n65, LS_WADE\n71, LS_RESPONSIVE\n72, LS_NEUTRAL_ROLL\n73, LS_SPRINT\n74, LS_SPRINT_ROLL\n75, LS_POSE_START\n76, LS_POSE_END\n77, LS_POSE_LEFT\n78, LS_POSE_RIGHT\n56, LS_CLIMB_STANCE\n57, LS_CLIMBING\n58, LS_CLIMB_LEFT\n59, LS_CLIMB_END\n60, LS_CLIMB_RIGHT\n61, LS_CLIMB_DOWN\n62, LS_LARA_TEST1\n63, LS_LARA_TEST2\n64, LS_LARA_TEST3\n67, LS_FLARE_PICKUP\n69, LS_KICK\n70, LS_ZIPLINE\n80, LS_CROUCH_IDLE\n81, LS_CROUCH_ROLL\n82, LS_CRAWL_IDLE\n83, LS_CRAWL_FORWARD\n84, LS_CRAWL_TURN_LEFT\n85, LS_CRAWL_TURN_RIGHT\n86, LS_CRAWL_BACK\n87, LS_CLIMB_TO_CRAWL\n88, LS_CRAWL_TO_CLIMB\n89, LS_CRAWL_JUMP_DOWN\n90, LS_CROUCH_TURN_LEFT\n91, LS_CROUCH_TURN_RIGHT\n92, LS_MONKEY_IDLE\n93, LS_MONKEY_FORWARD\n94, LS_MONKEY_LEFT\n95, LS_MONKEY_RIGHT\n96, LS_MONKEY_ROLL\n97, LS_MONKEY_TURN_LEFT\n98, LS_MONKEY_TURN_RIGHT\n"
  },
  {
    "path": "data/trx/ship/games/tr2/catalog_music.csv",
    "content": "43, MX_SECRET\n18, MX_TR2_GYM_HINT_14\n19, MX_TR2_GYM_HINT_15\n20, MX_TR2_GYM_HINT_16\n21, MX_TR2_GYM_HINT_17\n22, MX_TR2_GYM_HINT_18\n24, MX_DAGGER_PULL\n23, MX_CUTSCENE_BATH\n57, MX_REVEAL_1\n59, MX_REVEAL_2\n48, MX_SKIDOO_THEME\n49, MX_BATTLE_THEME\n"
  },
  {
    "path": "data/trx/ship/games/tr2/catalog_objects.csv",
    "content": "0,   O_LARA\n1,   O_LARA_PISTOLS\n2,   O_LARA_HAIR\n3,   O_LARA_SHOTGUN\n4,   O_LARA_AUTOS\n5,   O_LARA_UZIS\n6,   O_LARA_M16\n7,   O_LARA_GRENADE_GUN\n8,   O_LARA_HARPOON_GUN\n9,   O_LARA_FLARE\n10,  O_LARA_SKIDOO\n11,  O_LARA_BOAT\n12,  O_LARA_EXTRA\n13,  O_SKIDOO_FAST\n14,  O_BOAT\n15,  O_DOG\n16,  O_CULT_1\n17,  O_CULT_1A\n18,  O_CULT_1B\n19,  O_CULT_2\n20,  O_CULT_3\n21,  O_MOUSE\n22,  O_DRAGON_FRONT\n23,  O_DRAGON_BACK\n24,  O_GONDOLA\n25,  O_SHARK\n26,  O_EEL\n27,  O_BIG_EEL\n28,  O_BARRACUDA\n29,  O_DIVER\n30,  O_WORKER_1\n31,  O_WORKER_2\n32,  O_WORKER_3\n33,  O_WORKER_4\n34,  O_WORKER_5\n35,  O_JELLY\n36,  O_SPIDER\n37,  O_BIG_SPIDER\n38,  O_CROW\n39,  O_TIGER\n40,  O_BARTOLI\n41,  O_XIAN_SPEARMAN\n42,  O_XIAN_SPEARMAN_STATUE\n43,  O_XIAN_KNIGHT\n44,  O_XIAN_KNIGHT_STATUE\n45,  O_YETI\n46,  O_BIRD_GUARDIAN\n47,  O_EAGLE\n48,  O_BANDIT_1\n49,  O_BANDIT_2\n50,  O_BANDIT_2B\n51,  O_SKIDOO_ARMED\n52,  O_SKIDOO_DRIVER\n53,  O_MONK_1\n54,  O_MONK_2\n55,  O_FALLING_BLOCK_1\n56,  O_FALLING_BLOCK_2\n57,  O_FALLING_BLOCK_3\n58,  O_PENDULUM_1\n59,  O_SPIKES\n60,  O_ROLLING_BALL_1\n61,  O_DISC\n62,  O_DISC_EMITTER\n63,  O_DRAWBRIDGE\n64,  O_TEETH_TRAP\n65,  O_LIFT\n66,  O_GENERAL\n67,  O_MOVABLE_BLOCK_1\n68,  O_MOVABLE_BLOCK_2\n69,  O_MOVABLE_BLOCK_3\n70,  O_MOVABLE_BLOCK_4\n71,  O_BIG_BOWL\n72,  O_SMASH_OBJECT_1\n73,  O_SMASH_OBJECT_2\n74,  O_SMASH_OBJECT_3\n75,  O_SMASH_OBJECT_4\n76,  O_PROPELLER_1\n77,  O_POWER_SAW\n78,  O_HOOK\n79,  O_FALLING_CEILING_1\n80,  O_SPINNING_BLADE\n81,  O_BLADE\n82,  O_KILLER_STATUE\n83,  O_ROLLING_BALL_2\n84,  O_ICICLE\n85,  O_SPIKE_WALL\n86,  O_SPRINGBOARD\n87,  O_CEILING_SPIKES\n88,  O_BELL\n89,  O_WATER_SPRITE\n90,  O_SNOW_SPRITE\n91,  O_SKIDOO_TRACK\n92,  O_SWITCH_TYPE_AIRLOCK\n93,  O_SWITCH_TYPE_SMALL\n94,  O_PROPELLER_2\n95,  O_PROPELLER_3\n96,  O_PENDULUM_2\n97,  O_MESH_SWAP_1\n98,  O_MESH_SWAP_2\n99,  O_MESH_SWAP_3\n100, O_TEXT_BOX\n101, O_ROLLING_BALL_3\n102, O_ZIPLINE_HANDLE\n103, O_SWITCH_TYPE_BUTTON\n104, O_SWITCH_TYPE_NORMAL\n105, O_SWITCH_TYPE_UW\n106, O_DOOR_TYPE_1\n107, O_DOOR_TYPE_2\n108, O_DOOR_TYPE_3\n109, O_DOOR_TYPE_4\n110, O_DOOR_TYPE_5\n111, O_DOOR_TYPE_6\n112, O_DOOR_TYPE_7\n113, O_DOOR_TYPE_8\n114, O_TRAPDOOR_TYPE_1\n115, O_TRAPDOOR_TYPE_2\n116, O_TRAPDOOR_TYPE_3\n117, O_BRIDGE_FLAT\n118, O_BRIDGE_TILT_1\n119, O_BRIDGE_TILT_2\n120, O_PASSPORT_OPTION\n121, O_STOPWATCH_OPTION\n122, O_PHOTO_OPTION\n123, O_PLAYER_1\n124, O_PLAYER_2\n125, O_PLAYER_3\n126, O_PLAYER_4\n127, O_PLAYER_5\n128, O_PLAYER_6\n129, O_PLAYER_7\n130, O_PLAYER_8\n131, O_PLAYER_9\n132, O_PLAYER_10\n133, O_PASSPORT_CLOSED\n134, O_PDA_OPTION\n135, O_PISTOL_ITEM\n136, O_SHOTGUN_ITEM\n137, O_AUTOS_ITEM\n138, O_UZI_ITEM\n139, O_HARPOON_ITEM\n140, O_M16_ITEM\n141, O_GRENADE_GUN_ITEM\n142, O_PISTOL_AMMO_ITEM\n143, O_SHOTGUN_AMMO_ITEM\n144, O_AUTOS_AMMO_ITEM\n145, O_UZI_AMMO_ITEM\n146, O_HARPOON_AMMO_ITEM\n147, O_M16_AMMO_ITEM\n148, O_GRENADE_AMMO_ITEM\n149, O_SMALL_MEDIPACK_ITEM\n150, O_LARGE_MEDIPACK_ITEM\n151, O_FLAREBOX_ITEM\n152, O_FLARE_ITEM\n153, O_DETAIL_OPTION\n154, O_SOUND_OPTION\n155, O_CONTROL_OPTION\n156, O_GAMMA_OPTION\n157, O_PISTOL_OPTION\n158, O_SHOTGUN_OPTION\n159, O_AUTOS_OPTION\n160, O_UZI_OPTION\n161, O_HARPOON_OPTION\n162, O_M16_OPTION\n163, O_GRENADE_GUN_OPTION\n164, O_PISTOL_AMMO_OPTION\n165, O_SHOTGUN_AMMO_OPTION\n166, O_AUTOS_AMMO_OPTION\n167, O_UZI_AMMO_OPTION\n168, O_HARPOON_AMMO_OPTION\n169, O_M16_AMMO_OPTION\n170, O_GRENADE_AMMO_OPTION\n171, O_SMALL_MEDIPACK_OPTION\n172, O_LARGE_MEDIPACK_OPTION\n173, O_FLAREBOX_OPTION\n174, O_PUZZLE_ITEM_1\n175, O_PUZZLE_ITEM_2\n176, O_PUZZLE_ITEM_3\n177, O_PUZZLE_ITEM_4\n178, O_PUZZLE_OPTION_1\n179, O_PUZZLE_OPTION_2\n180, O_PUZZLE_OPTION_3\n181, O_PUZZLE_OPTION_4\n182, O_PUZZLE_HOLE_1\n183, O_PUZZLE_HOLE_2\n184, O_PUZZLE_HOLE_3\n185, O_PUZZLE_HOLE_4\n186, O_PUZZLE_DONE_1\n187, O_PUZZLE_DONE_2\n188, O_PUZZLE_DONE_3\n189, O_PUZZLE_DONE_4\n190, O_SECRET_1\n191, O_SECRET_2\n192, O_SECRET_3\n193, O_KEY_ITEM_1\n194, O_KEY_ITEM_2\n195, O_KEY_ITEM_3\n196, O_KEY_ITEM_4\n197, O_KEY_OPTION_1\n198, O_KEY_OPTION_2\n199, O_KEY_OPTION_3\n200, O_KEY_OPTION_4\n201, O_KEY_HOLE_1\n202, O_KEY_HOLE_2\n203, O_KEY_HOLE_3\n204, O_KEY_HOLE_4\n205, O_PICKUP_ITEM_1\n206, O_PICKUP_ITEM_2\n207, O_PICKUP_OPTION_1\n208, O_PICKUP_OPTION_2\n209, O_SPHERE_OF_DOOM_1\n210, O_SPHERE_OF_DOOM_2\n211, O_SPHERE_OF_DOOM_3\n212, O_ALARM_SOUND\n213, O_BIRD_TWEETER_1\n214, O_TREX\n215, O_BIRD_TWEETER_2\n216, O_CLOCK_CHIMES\n217, O_DRAGON_BONES_1\n218, O_DRAGON_BONES_2\n219, O_DRAGON_BONES_3\n220, O_HOT_LIQUID\n221, O_BOAT_BITS\n222, O_MINE\n223, O_INV_BACKGROUND\n224, O_FX_RESERVED\n225, O_GONG_BONGER\n226, O_GONG\n227, O_DETONATOR_BOX\n228, O_COPTER\n229, O_EXPLOSION_1\n230, O_SPLASH_1\n231, O_BUBBLE_1\n232, O_BUBBLE_EMITTER\n233, O_BLOOD\n234, O_DART_EFFECT\n235, O_FLARE_FIRE\n236, O_GLOW\n237, O_GLOW_RESERVED\n238, O_RICOCHET\n239, O_TWINKLE\n240, O_GUN_FLASH\n241, O_M16_FLASH\n242, O_BODY_PART\n243, O_CAMERA_TARGET\n244, O_WATERFALL\n245, O_MISSILE_HARPOON\n246, O_MISSILE_FLAME\n247, O_MISSILE_KNIFE\n248, O_GRENADE\n249, O_HARPOON_BOLT\n250, O_EMBER\n251, O_EMBER_EMITTER\n252, O_FLAME\n253, O_FLAME_EMITTER\n254, O_SKYBOX\n255, O_ALPHABET\n256, O_DYING_MONK\n257, O_DING_DONG\n258, O_LARA_ALARM\n259, O_MINI_COPTER\n260, O_WINSTON\n261, O_ASSAULT_DIGITS\n262, O_COMBAT_END\n263, O_CUT_SHOTGUN\n264, O_EARTHQUAKE\n265, O_BEAR\n266, O_WOLF\n267, O_MONK_3\n268, O_PICKUP_AID\n269, O_SAVE_CRYSTAL_ITEM\n# Slots 270-274 moved for Lara skins: available for re-use\n275, O_SECRET_1_OPTION\n276, O_SECRET_2_OPTION\n277, O_SECRET_3_OPTION\n278, O_ALPHABET_SMALL\n279, O_LARA_MAGNUMS\n280, O_MAGNUM_OPTION\n281, O_MAGNUM_AMMO_OPTION\n282, O_MAGNUM_ITEM\n283, O_MAGNUM_AMMO_ITEM\n284, O_SNOWFLAKE\n285, O_LARA_DESERT_EAGLE\n286, O_DESERT_EAGLE_OPTION\n287, O_DESERT_EAGLE_AMMO_OPTION\n288, O_DESERT_EAGLE_ITEM\n289, O_DESERT_EAGLE_AMMO_ITEM\n290, O_LARA_MP5,\n291, O_MP5_OPTION\n292, O_MP5_AMMO_OPTION\n293, O_MP5_ITEM\n294, O_MP5_AMMO_ITEM\n295, O_LARA_ROCKET_GUN\n296, O_ROCKET_GUN_OPTION\n297, O_ROCKET_AMMO_OPTION\n298, O_ROCKET\n299, O_ROCKET_GUN_ITEM\n300, O_ROCKET_AMMO_ITEM\n301, O_SHADOW\n302, O_LARA_SKIN_SWAP_1\n303, O_LARA_SKIN_SWAP_2\n304, O_LARA_SKIN_SWAP_3\n305, O_LARA_SKIN_SWAP_4\n306, O_LARA_SKIN_SWAP_5\n307, O_LARA_SKIN_SWAP_6\n308, O_LARA_SKIN_SWAP_7\n309, O_LARA_SKIN_SWAP_8\n310, O_LARA_SKIN_SWAP_9\n311, O_LARA_SKIN_SWAP_10\n312, O_LARA_SKIN_SWAP_11\n313, O_LARA_SKIN_SWAP_12\n314, O_LARA_SKIN_SWAP_13\n315, O_LARA_SKIN_SWAP_14\n316, O_LARA_SKIN_SWAP_15\n317, O_LARA_SKIN_SWAP_16\n318, O_LARA_SKIN_SWAP_17\n319, O_LARA_SKIN_SWAP_18\n320, O_LARA_SKIN_SWAP_19\n321, O_LARA_SKIN_SWAP_20\n322, O_LARA_SKIN_SWAP_21\n323, O_LARA_SKIN_SWAP_22\n324, O_LARA_SKIN_SWAP_23\n325, O_LARA_SKIN_SWAP_24\n326, O_LARA_SKIN_SWAP_25\n327, O_LARA_SKIN_SWAP_26\n328, O_LARA_SKIN_SWAP_27\n329, O_LARA_SKIN_SWAP_28\n330, O_LARA_SKIN_SWAP_29\n331, O_LARA_SKIN_SWAP_30\n332, O_LARA_SKIN_SWAP_31\n333, O_LARA_SKIN_SWAP_32\n334, O_LARA_SKIN_SWAP_EXTRA\n335, O_LARA_SKIN_SWAP_GUNS\n336, O_LARA_SKIN_SWAP_LEGS\n337, O_BLOOD_PINK\n"
  },
  {
    "path": "data/trx/ship/games/tr2/catalog_samples.csv",
    "content": "0,   SFX_LARA_FOOTSTEP\n2,   SFX_LARA_NO\n6,   SFX_LARA_DRAW\n7,   SFX_LARA_HOLSTER\n8,   SFX_LARA_PISTOLS\n9,   SFX_LARA_RELOAD\n10,  SFX_LARA_RICOCHET\n11,  SFX_LARA_FLARE_IGNITE\n12,  SFX_LARA_FLARE_BURN\n21,  SFX_LARA_AUTOS\n24,  SFX_MASSIVE_CRASH\n27,  SFX_LARA_BODYSL\n30,  SFX_LARA_FALL\n31,  SFX_LARA_INJURY\n36,  SFX_LARA_BREATH\n37,  SFX_LARA_BUBBLES\n39,  SFX_LARA_KEY\n41,  SFX_LARA_GENERAL_DEATH\n43,  SFX_LARA_UZI_FIRE\n45,  SFX_LARA_SHOTGUN\n48,  SFX_CLICK\n50,  SFX_LARA_BULLETHIT\n53,  SFX_LARA_FALL_DEATH\n58,  SFX_GLASS_BREAK\n60,  SFX_UNDERWATER\n71,  SFX_ENEMY_HIT_1\n72,  SFX_ENEMY_HIT_2\n78,  SFX_M16_FIRE\n79,  SFX_WATERFALL_LOOP\n79,  SFX_FLOOD\n104, SFX_M16_STOP\n105, SFX_EXPLOSION_1\n105, SFX_EXPLOSION_3\n108, SFX_MENU_ROTATE\n109, SFX_MENU_LARA_HOME\n111, SFX_MENU_CHOOSE\n111, SFX_MENU_SPININ\n112, SFX_MENU_SPINOUT\n113, SFX_MENU_STOPWATCH\n114, SFX_MENU_GUNS\n115, SFX_MENU_PASSPORT\n116, SFX_MENU_MEDI\n147, SFX_ROLLING_BALL_1_ROLL\n150, SFX_LOOP_FOR_SMALL_FIRES\n153, SFX_SKIDOO_IDLE\n155, SFX_SKIDOO_MOVING\n190, SFX_PULLEY_CRANE\n191, SFX_CURTAIN\n195, SFX_BOAT_IDLE\n197, SFX_BOAT_MOVING\n201, SFX_CLATTER_1\n202, SFX_CLATTER_2\n203, SFX_CLATTER_3\n204, SFX_SPIKE_WALL\n205, SFX_LARA_FLESH_WOUND\n206, SFX_SAW_REVVING\n207, SFX_SAW_STOP\n208, SFX_DOOR_CHIME\n213, SFX_AIRPLANE_IDLE\n215, SFX_UNDERWATER_FAN_ON\n217, SFX_SMALL_FAN_ON\n222, SFX_ROLLING_BALL_2_ROLL\n223, SFX_ROLLING_BALL_2_STOP\n227, SFX_ROLLING_BALL_3_ROLL\n228, SFX_ROLLING_BALL_3_STOP\n231, SFX_ROLLING_BLADE\n245, SFX_MONK_CRUNCH\n254, SFX_DISC\n258, SFX_PROJECTILE_HIT\n278, SFX_CHAIN_PULLEY\n280, SFX_ZIPLINE_GO\n281, SFX_ZIPLINE_STOP\n284, SFX_BOWL_POUR\n285, SFX_WATERFALL_2\n297, SFX_HELICOPTER_LOOP\n298, SFX_DRAGON_FEET\n298, SFX_EARTHQUAKE_1\n305, SFX_DRAGON_FIRE\n312, SFX_WARRIOR_HOVER\n316, SFX_BIRDS_CHIRP\n317, SFX_CRUNCH_1\n318, SFX_CRUNCH_2\n325, SFX_PUSHBLOCK_LAND\n325, SFX_EARTHQUAKE_2\n329, SFX_DRIPS_REVERB\n330, SFX_STAGE_BACKDROP\n331, SFX_STONE_DOOR_SLIDE\n332, SFX_PLATFORM_ALARM\n334, SFX_DOORBELL\n335, SFX_BURGLAR_ALARM\n336, SFX_BOAT_ENGINE\n337, SFX_BOAT_INTO_WATER\n338, SFX_BOILER\n341, SFX_MARCO_BARTOLLI_TRANSFORM\n344, SFX_WINSTON_GRUNT_1\n345, SFX_WINSTON_GRUNT_2\n346, SFX_WINSTON_GRUNT_3\n347, SFX_WINSTON_CUPS\n348, SFX_BRITTLE_GROUND_BREAK\n349, SFX_SPIDER_EXPLODE\n370, SFX_LARA_MAGNUMS\n371, SFX_LARA_DESERT_EAGLE\n372, SFX_MP5_FIRE\n373, SFX_EXPLOSION_2\n374, SFX_ROCKET_FIRE\n375, SFX_LARA_BAREFOOT\n# 376 used in animations for Lara knee shuffle\n"
  },
  {
    "path": "data/trx/ship/games/tr2/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 2,\n    \"name\": \"Tomb Raider II\",\n\n    \"main_menu_picture\": \"title_eu.webp\",\n    \"savegame_file_fmt\": \"save_tr2_%02d.dat\",\n\n    \"demo_version\": false,\n    \"enable_tr2_item_drops\": true,\n    \"convert_dropped_guns\": true,\n\n    \"title\": {\n        \"path\": \"title.tr2\",\n        \"music_track\": 60,\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"legal_eu.webp\", \"legal\": true},\n            {\"type\": \"play_fmv\", \"fmv_id\": 0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 1},\n            {\"type\": \"exit_to_title\"},\n        ],\n        \"inherit_injections\": false,\n        \"injections\": [\n            \"font.bin\",\n            \"inv_background.bin\",\n            \"pda_model.bin\",\n            \"title_textures.bin\",\n            \"misc_sprites.bin\",\n        ]\n    },\n\n    \"sfx_path\": \"main.sfx\",\n    \"injections\": [\n        \"font.bin\",\n        \"lara_animations.bin\",\n        \"pda_model.bin\",\n        \"pickup_aid.bin\",\n        \"crystal.bin\",\n        \"winston_model.bin\",\n        \"lara_extra.bin\",\n        \"lara_rifle_sfx.bin\",\n        \"secret_models_og.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n    ],\n\n    \"levels\": [\n        // 0. Lara's Home\n        {\n            \"type\": \"gym\",\n            \"path\": \"assault.tr2\",\n            \"script\": \"assault.lua\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_gym\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"mansion.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"font.bin\",\n                \"gym_fd.bin\",\n                \"gym_sfx.bin\",\n                \"gym_music_tracks.bin\",\n                \"gym_textures.bin\",\n                \"lara_gym_guns.bin\",\n                \"lara_animations.bin\",\n                \"pda_model.bin\",\n                \"pickup_aid.bin\",\n                \"misc_sprites.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n\n        // 1. The Great Wall\n        {\n            \"path\": \"wall.tr2\",\n            \"music_track\": 29,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 2},\n                {\"type\": \"loading_screen\", \"path\": \"china.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"shotgun\"},\n                {\"type\": \"give_item\", \"object_id\": \"small_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"large_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"flare\", \"quantity\": 2},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 2},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"small_medipack\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 0},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"door106_sfx.bin\",\n                \"lara_guns.bin\",\n                \"wall_cameras.bin\",\n                \"wall_itemrots.bin\",\n                \"wall_music_tracks.bin\",\n                \"wall_textures.bin\",\n                \"wall_crystals.bin\",\n            ],\n        },\n\n        // 2. Venice\n        {\n            \"path\": \"boat.tr2\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"venice.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"autos_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"boat_bits.bin\",\n                \"common_pickup_meshes.bin\",\n                \"lara_guns.bin\",\n                \"venice_fd.bin\",\n                \"venice_itemrots.bin\",\n                \"venice_music_tracks.bin\",\n                \"venice_textures.bin\",\n                \"venice_crystals.bin\",\n            ],\n        },\n\n        // 3. Bartoli's Hideout\n        {\n            \"path\": \"venice.tr2\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"venice.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"enable_sunset\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"shotgun_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"bartoli_music_tracks.bin\",\n                \"bartoli_secret_fd.bin\",\n                \"bartoli_textures.bin\",\n                \"common_pickup_meshes.bin\",\n                \"door108_sfx.bin\",\n                \"lara_guns.bin\",\n                \"detonator_lights.bin\",\n                \"bartoli_crystals.bin\",\n            ],\n        },\n\n        // 4. Opera House\n        {\n            \"path\": \"opera.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"venice.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 1},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"door108_sfx.bin\",\n                \"door111_sfx.bin\",\n                \"lara_guns.bin\",\n                \"loose_boards_sfx.bin\",\n                \"opera_fd.bin\",\n                \"opera_textures.bin\",\n                \"opera_itemrots.bin\",\n                \"opera_music_tracks.bin\",\n                \"opera_sfx.bin\",\n                \"opera_crystals.bin\",\n            ],\n            \"unobtainable_kills\": 1,\n        },\n\n        // 5. Offshore Rig\n        {\n            \"path\": \"rig.tr2\",\n            \"music_track\": 54,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 3},\n                {\"type\": \"loading_screen\", \"path\": \"rig.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"set_lara_start_anim\", \"anim\": 8},\n                {\"type\": \"remove_weapons\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis_ammo\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"door108_sfx.bin\",\n                \"lara_guns.bin\",\n                \"rig_itemrots.bin\",\n                \"rig_music_tracks.bin\",\n                \"rig_pickup_meshes.bin\",\n                \"rig_textures.bin\",\n                \"scuba_sfx.bin\",\n                \"rig_crystals.bin\",\n            ],\n        },\n\n        // 6. Diving Area\n        {\n            \"path\": \"platform.tr2\",\n            \"music_track\": 54,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"rig.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis_ammo\", \"quantity\": 4},\n                {\"type\": \"give_item\", \"object_id\": \"pistols\", \"quantity\": 1},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 2},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"diving_cameras.bin\",\n                \"diving_itemrots.bin\",\n                \"diving_music_tracks.bin\",\n                \"diving_pickup_meshes.bin\",\n                \"diving_sfx.bin\",\n                \"diving_textures.bin\",\n                \"door108_sfx.bin\",\n                \"lara_guns.bin\",\n                \"scuba_sfx.bin\",\n                \"diving_crystals.bin\",\n            ],\n        },\n\n        // 7. 40 Fathoms\n        {\n            \"path\": \"unwater.tr2\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr2_diving_suit\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 4},\n                {\"type\": \"loading_screen\", \"path\": \"titan.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"harpoon_gun_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"fathoms_goon_sfx.bin\",\n                \"fathoms_itemrots.bin\",\n                \"fathoms_music_tracks.bin\",\n                \"fathoms_secret_fd.bin\",\n                \"fathoms_plants.bin\",\n                \"fathoms_textures.bin\",\n                \"lara_guns.bin\",\n                \"scuba_sfx.bin\",\n                \"fathoms_crystals.bin\",\n            ],\n        },\n\n        // 8. Wreck of the Maria Doria\n        {\n            \"path\": \"keel.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_diving_suit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"titan.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"scuba_sfx.bin\",\n                \"wreck_cameras.bin\",\n                \"wreck_fd.bin\",\n                \"wreck_goon_sfx.bin\",\n                \"wreck_itemrots.bin\",\n                \"wreck_music_tracks.bin\",\n                \"wreck_pickup_meshes.bin\",\n                \"wreck_plants.bin\",\n                \"wreck_secret_fd.bin\",\n                \"wreck_textures.bin\",\n                \"wreck_crystals.bin\",\n            ],\n            \"unobtainable_kills\": 1,\n        },\n\n        // 9. Living Quarters\n        {\n            \"path\": \"living.tr2\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr2_diving_suit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"titan.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"m16_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"living_deck_goon_sfx.bin\",\n                \"living_fd.bin\",\n                \"living_itemrots.bin\",\n                \"living_music_tracks.bin\",\n                \"living_pickup_meshes.bin\",\n                \"living_secret_fd.bin\",\n                \"living_sfx.bin\",\n                \"living_textures.bin\",\n                \"seaweed_collision.bin\",\n                \"scuba_sfx.bin\",\n                \"living_crystals.bin\",\n            ],\n        },\n\n        // 10. The Deck\n        {\n            \"path\": \"deck.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_diving_suit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"titan.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"breakable_tile_sfx.bin\",\n                \"deck_cameras.bin\",\n                \"deck_fd.bin\",\n                \"deck_itemrots.bin\",\n                \"deck_music_tracks.bin\",\n                \"deck_pickup_meshes.bin\",\n                \"deck_plants.bin\",\n                \"deck_secret_fd.bin\",\n                \"deck_textures.bin\",\n                \"door110_sfx.bin\",\n                \"lara_guns.bin\",\n                \"living_deck_goon_sfx.bin\",\n                \"scuba_sfx.bin\",\n                \"deck_crystals.bin\",\n            ],\n        },\n\n        // 11. Tibetan Foothills\n        {\n            \"path\": \"skidoo.tr2\",\n            \"music_track\": 29,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"weather_type\": \"snow\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 5},\n                {\"type\": \"loading_screen\", \"path\": \"tibet.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"puzzle_4\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"lara_guns.bin\",\n                \"tibet_fd.bin\",\n                \"tibet_itemrots.bin\",\n                \"tibet_music_tracks.bin\",\n                \"tibet_textures.bin\",\n                \"tibet_crystals.bin\",\n            ],\n        },\n\n        // 12. Barkhang Monastery\n        {\n            \"path\": \"monastry.tr2\",\n            \"script\": \"monastry.lua\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"tibet.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"puzzle_4\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"m16_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"barkhang_cameras.bin\",\n                \"barkhang_fd.bin\",\n                \"barkhang_itemrots.bin\",\n                \"barkhang_music_tracks.bin\",\n                \"barkhang_pickup_meshes.bin\",\n                \"barkhang_textures.bin\",\n                \"lara_guns.bin\",\n                \"barkhang_crystals.bin\",\n            ],\n            \"unobtainable_pickups\": 2,\n            \"unobtainable_kills\": 1,\n        },\n\n        // 13. Catacombs of the Talion\n        {\n            \"path\": \"catacomb.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"tibet.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 2},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"m16_ammo\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"breakable_tile_sfx.bin\",\n                \"catacombs_fd.bin\",\n                \"catacombs_itemrots.bin\",\n                \"catacombs_music_tracks.bin\",\n                \"catacombs_textures.bin\",\n                \"common_pickup_meshes.bin\",\n                \"door108_sfx.bin\",\n                \"lara_guns.bin\",\n                \"catacombs_crystals.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // 14. Ice Palace\n        {\n            \"path\": \"icecave.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"tibet.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"door108_sfx.bin\",\n                \"guardian_death_commands.bin\",\n                \"lara_guns.bin\",\n                \"palace_fd.bin\",\n                \"palace_itemrots.bin\",\n                \"palace_music_tracks.bin\",\n                \"palace_secret_fd.bin\",\n                \"palace_textures.bin\",\n                \"portcullis_sfx.bin\",\n                \"palace_crystals.bin\",\n            ],\n        },\n\n        // 15. Temple of Xian\n        {\n            \"path\": \"emprtomb.tr2\",\n            \"music_track\": 55,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 6},\n                {\"type\": \"loading_screen\", \"path\": \"china.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis_ammo\", \"quantity\": 8},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 3},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"door108_sfx.bin\",\n                \"lara_guns.bin\",\n                \"portcullis_sfx.bin\",\n                \"xian_fd.bin\",\n                \"xian_itemrots.bin\",\n                \"xian_music_tracks.bin\",\n                \"xian_pickup_meshes.bin\",\n                \"xian_sfx.bin\",\n                \"xian_textures.bin\",\n                \"xian_crystals.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // 16. Floating Islands\n        {\n            \"path\": \"floating.tr2\",\n            \"script\": \"floating.lua\",\n            \"music_track\": 55,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"china.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"disable_floor\", \"height\": 9728},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 8},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"floating_fd.bin\",\n                \"floating_itemrots.bin\",\n                \"floating_music_tracks.bin\",\n                \"floating_pickup_meshes.bin\",\n                \"floating_textures.bin\",\n                \"floating_crystals.bin\",\n            ],\n        },\n\n        // 17. The Dragon's Lair\n        {\n            \"path\": \"xian.tr2\",\n            \"music_track\": 55,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"china.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_fmv\", \"fmv_id\": 7},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"dagger_sprite.bin\",\n                \"lair_bartolipos.bin\",\n                \"lair_music_tracks.bin\",\n                \"lair_textures.bin\",\n                \"lara_guns.bin\",\n                \"portcullis_sfx.bin\",\n                \"lair_crystals.bin\",\n            ],\n        },\n\n        // 18. Home Sweet Home\n        {\n            \"path\": \"house.tr2\",\n            \"script\": \"house.lua\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_robe\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"mansion.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"key_1\"},\n                {\"type\": \"set_lara_start_anim\", \"anim\": 15},\n                {\"type\": \"remove_weapons\"},\n                {\"type\": \"remove_ammo\"},\n                {\"type\": \"remove_flares\"},\n                {\"type\": \"remove_medipacks\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 48},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit01.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit02.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit03.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit04.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit05.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit06.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit07.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit08.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"total_stats\", \"background_path\": \"end.webp\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"explosion.bin\",\n                \"house_sfx.bin\",\n                \"house_itemrots.bin\",\n                \"house_music_tracks.bin\",\n                \"house_shower_frames.bin\",\n                \"house_textures.bin\",\n                \"lara_house_guns.bin\",\n            ],\n        },\n    ],\n\n    \"demos\": [\n        // Demo 1: Venice\n        {\n            \"path\": \"boat.tr2\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"venice.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"autos_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"venice_fd.bin\",\n                \"venice_itemrots.bin\",\n                \"venice_music_tracks.bin\",\n                \"venice_textures.bin\",\n                \"venice_crystals.bin\",\n            ],\n        },\n\n        // Demo 2: Wreck of the Maria Doria\n        {\n            \"path\": \"keel.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_diving_suit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"titan.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"grenade_launcher_ammo\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"scuba_sfx.bin\",\n                \"wreck_fd.bin\",\n                \"wreck_goon_sfx.bin\",\n                \"wreck_itemrots.bin\",\n                \"wreck_music_tracks.bin\",\n                \"wreck_pickup_meshes.bin\",\n                \"wreck_plants.bin\",\n                \"wreck_textures.bin\",\n                \"wreck_crystals.bin\",\n            ],\n        },\n\n        // Demo 3: Tibetan Foothills\n        {\n            \"path\": \"skidoo.tr2\",\n            \"music_track\": 29,\n            \"weather_type\": \"snow\",\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"tibet.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"puzzle_4\"},\n                {\"type\": \"add_secret_reward\", \"object_id\": \"uzis_ammo\", \"quantity\": 4},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes.bin\",\n                \"tibet_fd.bin\",\n                \"tibet_itemrots.bin\",\n                \"tibet_music_tracks.bin\",\n                \"tibet_textures.bin\",\n                \"tibet_crystals.bin\",\n            ],\n        },\n    ],\n\n    \"cutscenes\": [\n        {\n            \"path\": \"cut1.tr2\",\n            \"music_track\": 2,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"font.bin\",\n                \"photo.bin\",\n                \"misc_sprites.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        {\n            \"path\": \"cut2.tr2\",\n            \"music_track\": 3,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [{\"type\": \"loop_game\"}],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut2_setup.bin\",\n                \"cut2_textures.bin\",\n                \"font.bin\",\n                \"photo.bin\",\n                \"misc_sprites.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        {\n            \"path\": \"cut3.tr2\",\n            \"script\": \"cut3.lua\",\n            \"music_track\": 4,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [{\"type\": \"loop_game\"}],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut3_setup.bin\",\n                \"cut3_textures.bin\",\n                \"font.bin\",\n                \"photo.bin\",\n                \"misc_sprites.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        {\n            \"path\": \"cut4.tr2\",\n            \"music_track\": 26,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut4_setup.bin\",\n                \"cut4_textures.bin\",\n                \"font.bin\",\n                \"photo.bin\",\n                \"misc_sprites.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n    ],\n\n    \"fmvs\": [\n        {\"path\": \"LOGO.RPL\", \"legal\": true},\n        {\"path\": \"ANCIENT.RPL\"},\n        {\"path\": \"MODERN.RPL\"},\n        {\"path\": \"LANDING.RPL\"},\n        {\"path\": \"MS.RPL\"},\n        {\"path\": \"CRASH.RPL\"},\n        {\"path\": \"JEEP.RPL\"},\n        {\"path\": \"END.RPL\"},\n    ],\n\n    \"hidden_config\": [\n        \"enable_item_examining\", // TR2 has no special item descriptions\n        \"disable_trex_collision\", // TR2 always disables corpse collision\n        \"fix_alligator_ai\", // TR2 has no alligators\n        \"fix_bear_ai\", // TR2 has no bears\n        \"fix_monkey_pickup_priority\",\n        \"healthbar_poison_color\",\n        \"healthbar_poison_color_ps1\",\n        \"exposurebar_color\",\n        \"exposurebar_color_ps1\",\n        \"exposurebar_location\",\n        \"exposurebar_show_mode\",\n        \"change_pierre_spawn\",\n        \"fix_chainblock_secret_sound\",\n        \"enable_compass_stats\",\n        \"enable_wading\",\n        \"restore_ps1_enemies\",\n        \"fix_speeches_killing_music\",\n        \"enable_footprints\",\n        \"fix_pipeman_aim\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/inv_ring.json5",
    "content": "[\n    {\n        \"object_id\": \"O_SMALL_MEDIPACK_OPTION\",\n        \"frames_total\": 26,\n        \"open_frame\": 25,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4032,\n        \"x_rot_sel\": -7296,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 216,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 14,\n    },\n\n    {\n        \"object_id\": \"O_LARGE_MEDIPACK_OPTION\",\n        \"frames_total\": 20,\n        \"open_frame\": 19,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3616,\n        \"x_rot_sel\": -8160,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 13,\n    },\n\n    {\n        \"object_id\": \"O_FLAREBOX_OPTION\",\n        \"frames_total\": 31,\n        \"open_frame\": 30,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"y_rot_sel\": -8192,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 12,\n    },\n\n    {\n        \"object_id\": \"O_PISTOL_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 2848,\n        \"y_rot_sel\": -32768,\n        \"y_trans_sel\": 38,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 1,\n    },\n\n    {\n        \"object_id\": \"O_SHOTGUN_OPTION\",\n        \"frames_total\": 13,\n        \"open_frame\": 12,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 5120,\n        \"y_rot_sel\": 30720,\n        \"z_trans_sel\": 228,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 2,\n    },\n\n    {\n        \"object_id\": \"O_MAGNUM_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 3,\n    },\n\n    {\n        \"object_id\": \"O_AUTOS_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 4,\n    },\n\n    {\n        \"object_id\": \"O_DESERT_EAGLE_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 5,\n    },\n\n    {\n        \"object_id\": \"O_UZI_OPTION\",\n        \"frames_total\": 13,\n        \"open_frame\": 12,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 2336,\n        \"y_rot_sel\": -32768,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 322,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 6,\n    },\n\n    {\n        \"object_id\": \"O_HARPOON_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -736,\n        \"y_rot_sel\": -19456,\n        \"y_trans_sel\": 58,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 11,\n    },\n\n    {\n        \"object_id\": \"O_M16_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": -18432,\n        \"y_trans_sel\": 84,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 7,\n    },\n\n    {\n        \"object_id\": \"O_MP5_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": -18432,\n        \"y_trans_sel\": 84,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 8,\n    },\n\n    {\n        \"object_id\": \"O_ROCKET_GUN_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": 14336,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 9,\n    },\n\n    {\n        \"object_id\": \"O_GRENADE_GUN_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": 14336,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_PISTOL_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 1,\n    },\n\n    {\n        \"object_id\": \"O_SHOTGUN_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 2,\n    },\n\n    {\n        \"object_id\": \"O_MAGNUM_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 3,\n    },\n\n    {\n        \"object_id\": \"O_AUTOS_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 4,\n    },\n\n    {\n        \"object_id\": \"O_DESERT_EAGLE_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 5,\n    },\n\n    {\n        \"object_id\": \"O_UZI_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 6,\n    },\n\n    {\n        \"object_id\": \"O_HARPOON_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_M16_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 7,\n    },\n\n    {\n        \"object_id\": \"O_MP5_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 8,\n    },\n\n    {\n        \"object_id\": \"O_ROCKET_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 9,\n    },\n\n    {\n        \"object_id\": \"O_GRENADE_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_SCION_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 109,\n    },\n\n    {\n        \"object_id\": \"O_LEADBAR_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3616,\n        \"x_rot_sel\": -8160,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 100,\n    },\n\n    {\n        \"object_id\": \"O_PICKUP_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 111,\n    },\n\n    {\n        \"object_id\": \"O_PICKUP_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 110,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 108,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 107,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 106,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 105,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 101,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 102,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 103,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 104,\n    },\n\n    {\n        \"object_id\": \"O_STOPWATCH_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"y_trans_sel\": -135,\n        \"z_trans_sel\": 320,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n    },\n\n    {\n        \"object_id\": \"O_COMPASS_OPTION\",\n        \"frames_total\": 25,\n        \"open_frame\": 10,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4352,\n        \"x_rot_sel\": -8192,\n        \"z_trans_sel\": 456,\n        \"meshes_sel\": 0b00000101,\n        \"meshes_drawn\": 0b00000101,\n    },\n\n    {\n        \"object_id\": \"O_PASSPORT_OPTION\",\n        \"frames_total\": 30,\n        \"open_frame\": 14,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"x_rot_sel\": -4320,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": 0b00010011,\n        \"meshes_drawn\": 0b00010011,\n        \"inv_pos\": 200,\n    },\n\n    {\n        \"object_id\": \"O_DETAIL_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4224,\n        \"x_rot_sel\": -7232,\n        \"y_trans_sel\": 16,\n        \"z_trans_sel\": 444,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 201,\n    },\n\n    {\n        \"object_id\": \"O_SOUND_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4832,\n        \"x_rot_sel\": -5408,\n        \"y_rot_sel\": -3072,\n        \"y_trans_sel\": -2,\n        \"z_trans_sel\": 350,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 202,\n    },\n\n    {\n        \"object_id\": \"O_CONTROL_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 5504,\n        \"x_rot_sel\": -2560,\n        \"x_rot_nosel\": 5632,\n        \"y_rot_sel\": 13312,\n        \"y_trans_sel\": 46,\n        \"z_trans_sel\": 508,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 203,\n    },\n\n    {\n        \"object_id\": \"O_PHOTO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"x_rot_sel\": -4320,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 205,\n    },\n\n    {\n        \"object_id\": \"O_PDA_OPTION\",\n        \"frames_total\": 39,\n        \"open_frame\": 19,\n        \"anim_direction\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": 0b00000011,\n        \"meshes_drawn\": 0b00000011,\n        \"inv_pos\": 204,\n    },\n]\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/assault.lua",
    "content": "trx.events.on_game_start(function(level, is_save)\n  trx.lara.holsters_visible = trx.lara.has_pistol_weapon -- TODO: remove in TRX 1.5.\n  if is_save then\n    return\n  end\n  local records = trx.assault_stats.list_records()\n  if #records > 1 then\n    trx.music.play(22)\n  else\n    trx.music.play(5)\n  end\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/cut3.lua",
    "content": "local suit_change_anim = 7\nlocal outfit_changed = false\n\ntrx.events.after_control(function()\n  local lara_item = trx.lara.item\n  if lara_item.anim >= suit_change_anim and not outfit_changed then\n    trx.lara.outfit = \"tr2_diving_suit\"\n    outfit_changed = true\n  end\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/floating.lua",
    "content": "trx.events.after_level_file(function(level)\n  trx.objects.swap_mesh(trx.catalog.objects.secret_2_option, trx.catalog.objects.secret_3_option, 0, 0)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/house.lua",
    "content": "trx.events.on_game_start(function(level, is_save)\n  trx.lara.holsters_visible = trx.lara.has_pistol_weapon\n  if trx.lara.extra_anim == -1 then -- TODO: remove in TRX 1.5.\n    trx.lara.set_extra_equipment(trx.lara.mesh.hips, trx.lara.extra_mesh.dagger_hips)\n  end\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/level1.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.monk_1)\n  trx.creatures.add_ally_target(trx.catalog.objects.bandit_2)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/level3.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.monk_2)\n  trx.creatures.add_ally_target(trx.catalog.objects.bandit_1)\n  trx.creatures.add_ally_target(trx.catalog.objects.bandit_2)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/level4.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.monk_2)\n  trx.creatures.add_ally_target(trx.catalog.objects.bandit_2)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/scripts/monastry.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.monk_1)\n  trx.creatures.add_ally(trx.catalog.objects.monk_2)\n  trx.creatures.add_ally_target(trx.catalog.objects.bandit_1)\n  trx.creatures.add_ally_target(trx.catalog.objects.bandit_2)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"settings\": {\n        \"gameplay.fix_bear_ai\": {\n            \"description\": \"Fixt, dass die aufrecht stehend ausgeführten Prankenhiebe von Bären Lara, im Addon 'Die Goldene Maske', nicht mehr verfehlen.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"\\\\{review}Fixt originale Fehler in 40 Faden, Das Wrack der Maria Doria und An Deck, bei denen Pflanzen-Sprites in Wassergebieten nicht animiert sind.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Großes Medi-Pack\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kleines Medi-Pack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Cutscene 1\",\n        },\n        {\n            \"title\": \"Cutscene 2\",\n        },\n        {\n            \"title\": \"Cutscene 3\",\n        },\n        {\n            \"title\": \"Cutscene 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Laras Haus\",\n        },\n        {\n            \"title\": \"Die Große Mauer\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Wachstuben-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Venedig\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Bootshaus-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Stählernder Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Eiserner Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Bartolis Versteck\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Bibliothek-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Sprengschlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Opernhaus\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ornament-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Relais\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Schaltkteis\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Bohrturm\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Rote Karte\",\n                },\n                \"key_2\": {\n                    \"name\": \"Gelbe Karte\",\n                },\n                \"key_3\": {\n                    \"name\": \"Grüne Karte\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die Tiefe\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Rote Karte\",\n                },\n                \"key_4\": {\n                    \"name\": \"Blaue Karte\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Chip\",\n                }\n            }\n        },\n        {\n            \"title\": \"40 Faden\",\n        },\n        {\n            \"title\": \"Das Wrack der Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Waschraumschlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Kabinen-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Unterbrecher\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die Quatiere\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Theater-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"An Deck\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Stern-Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Lager-Schlüssel\",\n                },\n                \"key_4\": {\n                    \"name\": \"Kabinen-Schlüssel\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das tibetanische Hochland\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Schneeleopard\",\n                },\n                \"key_1\": {\n                    \"name\": \"Zugbrücken-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Hüttenschlüssel\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Kloster von Barkhang\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Kammer-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Falltür-Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Dachboden-Schlüssel\",\n                },\n                \"key_4\": {\n                    \"name\": \"Hallen-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Gebets-Rolle\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Edelstein\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die Katakomben des Talion\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Schneeleopard\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Gong-Hammer\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tibetanische Maske\",\n                }\n            }\n        },\n        {\n            \"title\": \"Eispalast\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Weißer Tiger\",\n                },\n                \"key_2\": {\n                    \"name\": \"Gong-Hammer\",\n                },\n                \"pickup_2\": {\n                    \"name\": \"Talion\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tibetanische Maske\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Tempel von Xian\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Goldener Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Silberner Schlüssel\",\n                },\n                \"key_4\": {\n                    \"name\": \"Kammer-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Siegel des Drachen\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die schwimmenden Inseln\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mystische Plakette\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Mystische Plakette\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Hort des Drachen\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mystische Plakette\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Dolch von Xian\",\n                }\n            }\n        },\n        {\n            \"title\": \"Zuhause\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Waffenschrank-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Dolch von Xian\",\n                }\n            }\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Venedig\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Bootshaus-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Stählerner Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Eiserner Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Wrack der Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Waschraum-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rostiger Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Kabinen-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Unterbrecher\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das tibetanische Hochland\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Schneeleopard\",\n                },\n                \"key_1\": {\n                    \"name\": \"Zugbrücken-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Hüttenschlüssel\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraph\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings-en-gb.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"extends\": \"en\",\n    \"levels\": [\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Theatre Key\",\n                }\n            }\n        },\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {}\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"settings\": {\n        \"gameplay.fix_bear_ai\": {\n            \"description\": \"\\\\{review}Corrige l'attaque de patte d'ours dans The Golden Mask pour qu'elle ne manque pas Lara.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"\\\\{review}Corrige le fait que les sprites des végétations ne s'animent pas dans les zones aquatiques du 40 brasses, Épave du Maria Doria et Le Pont.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"\\\\{review}Grand medipack\",\n        },\n        \"small_medipack\": {\n            \"name\": \"\\\\{review}Petit medipack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"\\\\{review}Scène cinématique 1\",\n        },\n        {\n            \"title\": \"\\\\{review}Scène cinématique 2\",\n        },\n        {\n            \"title\": \"\\\\{review}Scène cinématique 3\",\n        },\n        {\n            \"title\": \"\\\\{review}Scène cinématique 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"\\\\{review}Demeure de Lara\",\n        },\n        {\n            \"title\": \"\\\\{review}La Grande Muraille\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de la Garde\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé Rouillée\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Venise\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé du Hangar à Bateaux\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé en Acier\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé en Fer\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}La cache de Bartoli\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de la Bibliothèque\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé de Détonateur\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}L’Opéra\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé Ornate\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Boîte de Relais\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"\\\\{review}Carte de Circuit\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}La plate-forme offshore\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Carte d'Accès Rouge\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Carte d'Accès Jaune\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Carte d'Accès Verte\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Zone de plongée\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Carte d'Accès Rouge\",\n                },\n                \"key_4\": {\n                    \"name\": \"\\\\{review}Carte d'Accès Bleue\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Puce de Machine\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}40 brasses\",\n        },\n        {\n            \"title\": \"\\\\{review}Épave du Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de la Salle de Repos\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé Rouillée\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé de Cabine\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Disjoncteur\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Quartiers d'habitation\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de Théâtre\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé Rouillée\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Le Pont\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé de Poupe\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé de Stockage\",\n                },\n                \"key_4\": {\n                    \"name\": \"\\\\{review}Clé de Cabine\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"\\\\{review}Le Séraphin\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Les Contreforts tibétains\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"\\\\{review}Léopard des Neiges\",\n                },\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé du Pont-Levis\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé de la Hutte\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"\\\\{review}Le Séraphin\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Le Monastère de Barkhang\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de Chambre Forte\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé de Trappe\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé des Toits\",\n                },\n                \"key_4\": {\n                    \"name\": \"\\\\{review}Clé du Hall Principal\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Moulins à Prière\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"\\\\{review}Gemmes\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"\\\\{review}Le Séraphin\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Les Catacombes du Talion\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"\\\\{review}Léopard des neiges\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"\\\\{review}Marteau de gong\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Masque tibétain\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Le Palais de glace\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"\\\\{review}Tigre blanc\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Marteau de gong\",\n                },\n                \"pickup_2\": {\n                    \"name\": \"\\\\{review}Talion\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Masque tibétain\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Le Temple de Xian\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé en or\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé en argent\",\n                },\n                \"key_4\": {\n                    \"name\": \"\\\\{review}Clé de la chambre principale\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Le sceau du dragon\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Les Îles flottantes\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Plaque mystique\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"\\\\{review}Plaque mystique\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}L’Antre du dragon\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Plaque mystique\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"\\\\{review}Dague de Xian\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Home Sweet Home\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé du placard à armes\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Dague de Xian\",\n                }\n            }\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"\\\\{review}Venise\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé du bateau\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé en acier\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé en fer\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}É épave de la Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de la salle de repos\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé rouillée\",\n                },\n                \"key_3\": {\n                    \"name\": \"\\\\{review}Clé de la cabane\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Disjoncteur\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Contreforts tibétains\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"\\\\{review}Léopard des neiges\",\n                },\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé du pont-levis\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé de la hutte\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"\\\\{review}Le séraphin\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"settings\": {\n        \"gameplay.fix_bear_ai\": {\n            \"description\": \"Càraichidh ionnsaigh a' mhathain anns a' Mhasg Òir gus nach caill e Lara.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Càraichidh bugan ann an 40 Famhair, Long-bhriste na Maria Doria agus An Deic far nach gluais planntrais anns an uisge.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Pasgan Mòr Slàinte\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Pasgan Beag Slàinte\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Sealladh-film 1\",\n        },\n        {\n            \"title\": \"Sealladh-film 2\",\n        },\n        {\n            \"title\": \"Sealladh-film 3\",\n        },\n        {\n            \"title\": \"Sealladh-film 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Dachaigh Lara\",\n        },\n        {\n            \"title\": \"Am Balla Mòr\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Gheàrd-taighe\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                }\n            }\n        },\n        {\n            \"title\": \"Venise\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Taigh-bàta\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Stàilinn\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair Iarainn\",\n                }\n            }\n        },\n        {\n            \"title\": \"Taigh-falaich Bartoli\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Leabharlainn\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair an Spreadhaichear\",\n                }\n            }\n        },\n        {\n            \"title\": \"Taigh na h-Opera\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair Grinn\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Bogsa-iomlaid\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Bòrd-chuairt\",\n                }\n            }\n        },\n        {\n            \"title\": \"Inneal-ola Farraige\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Cairt-pasa Dearg\",\n                },\n                \"key_2\": {\n                    \"name\": \"Cairt-pasa Buidhe\",\n                },\n                \"key_3\": {\n                    \"name\": \"Cairt-pasa Uaine\",\n                }\n            }\n        },\n        {\n            \"title\": \"Roinn Dàibheadh\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Cairt-pasa Dearg\",\n                },\n                \"key_4\": {\n                    \"name\": \"Cairt-pasa Gorm\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Sgealb\",\n                }\n            }\n        },\n        {\n            \"title\": \"40 Famhair\",\n        },\n        {\n            \"title\": \"Long-bhriste na Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an t-Seòmair Fois\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair a' Chaibine\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Briseadair-cuairt\",\n                }\n            }\n        },\n        {\n            \"title\": \"Na Còmhnaidhean\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Taigh-cluiche\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                }\n            }\n        },\n        {\n            \"title\": \"An Deic\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Iuchair an Stìùir\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair an Stòrais\",\n                },\n                \"key_4\": {\n                    \"name\": \"Iuchair a' Chaibine\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"An t-Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Bràighean Tìbeata\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lìopaird-shneachda\",\n                },\n                \"key_1\": {\n                    \"name\": \"Iuchair an Drochaid-tarraing\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair na Bothain\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"An t-Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Manachainn Barkhang\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an t-Seòmair Làidir\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair an Làr-lùbte\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair nam Mullaichean\",\n                },\n                \"key_4\": {\n                    \"name\": \"Iuchair na Prìomh Thalla\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Cuibhlichean-ùrnaigh\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Clachan-luaidh\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"An t-Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Uaimhean an Talion\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lìopaird-shneachda\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Òrd a' Ghong\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Masg Tìbeata\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lùchairt Deighe\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Tìgear Geal\",\n                },\n                \"key_2\": {\n                    \"name\": \"Òrd a' Ghong\",\n                },\n                \"pickup_2\": {\n                    \"name\": \"An Talion\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Masg Tìbeata\",\n                }\n            }\n        },\n        {\n            \"title\": \"Teampall Xian\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Iuchair Òir\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair Airgid\",\n                },\n                \"key_4\": {\n                    \"name\": \"Iuchair na Prìomh Sheòmair\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ròn an Dràgon\",\n                }\n            }\n        },\n        {\n            \"title\": \"Eileanan Fleòdraidh\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Plaic Dhìomhair\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Plaic Dhìomhair\",\n                }\n            }\n        },\n        {\n            \"title\": \"Uaimh an Dràgon\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Plaic Dhìomhair\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Biodag Xian\",\n                }\n            }\n        },\n        {\n            \"title\": \"Dachaigh, mo Ghràdh\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Clòsaid-armachd\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Biodag Xian\",\n                }\n            }\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Venise\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Taigh-bàta\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Stàilinn\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair Iarainn\",\n                }\n            }\n        },\n        {\n            \"title\": \"Long-bhriste na Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an t-Seòmair Fois\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Meirgeach\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iuchair a' Chaibine\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Briseadair-cuairt\",\n                }\n            }\n        },\n        {\n            \"title\": \"Bràighean Tìbeata\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lìopaird-shneachda\",\n                },\n                \"key_1\": {\n                    \"name\": \"Iuchair an Drochaid-tarraing\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair na Bothain\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"An t-Seraph\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"settings\": {\n        \"gameplay.fix_bear_ai\": {\n            \"description\": \"Risolve il problema nel pacchetto di espansione La Maschera Dorata per cui gli attacchi con la zampa degli orsi non colpiscono Lara.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Risolve i problemi preesistenti nei livelli \\\"In Profondità\\\", \\\"Relitto della Maria Doria\\\" e \\\"Sul Ponte\\\" per cui le animazioni degli sprite delle piante nelle aree acquatiche non funzionano.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Kit Medico Grande\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kit Medico Piccolo\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Intermezzo 1\",\n        },\n        {\n            \"title\": \"Intermezzo 2\",\n        },\n        {\n            \"title\": \"Intermezzo 3\",\n        },\n        {\n            \"title\": \"Intermezzo 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Casa di Lara\",\n        },\n        {\n            \"title\": \"La Grande Muraglia\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Guardiola\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave Arrugginita\",\n                }\n            }\n        },\n        {\n            \"title\": \"Venezia\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Rimessa\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave d'Acciaio\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave di Ferro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Covo di Bartoli\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Biblioteca\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave del Detonatore\",\n                }\n            }\n        },\n        {\n            \"title\": \"Teatro dell'Opera\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave Cesellata\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Scatola Fusibili\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Circuito d'Avvio\",\n                }\n            }\n        },\n        {\n            \"title\": \"Piattaforma Offshore\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Tessera Rossa\",\n                },\n                \"key_2\": {\n                    \"name\": \"Tessera Gialla\",\n                },\n                \"key_3\": {\n                    \"name\": \"Tessera Verde\",\n                }\n            }\n        },\n        {\n            \"title\": \"Area di immersione\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Tessera Rossa\",\n                },\n                \"key_4\": {\n                    \"name\": \"Tessera Blu\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Chip\",\n                }\n            }\n        },\n        {\n            \"title\": \"In Profondità\",\n        },\n        {\n            \"title\": \"Relitto della Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Guardaroba\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave Arrugginita\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave della Cabina\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Interruttore di Circuito\",\n                }\n            }\n        },\n        {\n            \"title\": \"Alloggi\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Teatro\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave Arrugginita\",\n                }\n            }\n        },\n        {\n            \"title\": \"Sul Ponte\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Chiave di Poppa\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave del Ripostiglio\",\n                },\n                \"key_4\": {\n                    \"name\": \"Chiave della Cabina\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Il Serafo\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pendici Tibetane\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Leopardo delle Nevi\",\n                },\n                \"key_1\": {\n                    \"name\": \"Chiave del Ponte\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave della Capanna\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Il Serafo\",\n                }\n            }\n        },\n        {\n            \"title\": \"Monastero di Barkhang\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Caveau\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave della Botola\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave del Solaio\",\n                },\n                \"key_4\": {\n                    \"name\": \"Chiave dell’Atrio\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ruote della Preghiera\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Gemme\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Il Serafo\",\n                }\n            }\n        },\n        {\n            \"title\": \"Catacombe del Talion\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Leopardo delle Nevi\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Martello del Gong\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maschera Tibetana\",\n                }\n            }\n        },\n        {\n            \"title\": \"Palazzo di Ghiaccio\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Tigre Bianca\",\n                },\n                \"key_2\": {\n                    \"name\": \"Martello del Gong\",\n                },\n                \"pickup_2\": {\n                    \"name\": \"Talion\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maschera Tibetana\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tempio dello Xian\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Chiave d'Oro\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave d'Argento\",\n                },\n                \"key_4\": {\n                    \"name\": \"Chiave Camera Principale\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Sigillo del Drago\",\n                }\n            }\n        },\n        {\n            \"title\": \"Isole Galleggianti\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Piastra Mistica\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Piastra Mistica\",\n                }\n            }\n        },\n        {\n            \"title\": \"La Tana del Drago\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Piastra Mistica\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Pugnale di Xian\",\n                }\n            }\n        },\n        {\n            \"title\": \"Casa Dolce Casa\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave Arsenale\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Pugnale di Xian\",\n                }\n            }\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Venezia\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Rimessa\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave d'Acciaio\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave di Ferro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Relitto della Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Guardaroba\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave Arrugginita\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave della Cabina\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Interruttore di Circuito\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pendici Tibetane\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Leopardo delle Nevi\",\n                },\n                \"key_1\": {\n                    \"name\": \"Chiave del Ponte\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave della Capanna\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Il Serafo\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"settings\": {\n        \"gameplay.fix_bear_ai\": {\n            \"description\": \"Naprawia atak niedźwiedzia w dodatku Złota Maska, aby nie chybiał Lary.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Przywraca brakującą animację glonów w podwodnych poziomach.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Duża apteczka\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Mała apteczka\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Przerywnik 1\",\n        },\n        {\n            \"title\": \"Przerywnik 2\",\n        },\n        {\n            \"title\": \"Przerywnik 3\",\n        },\n        {\n            \"title\": \"Przerywnik 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Dom Lary\",\n        },\n        {\n            \"title\": \"Wielki Mur\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do strażnicy\",\n                },\n                \"key_2\": {\n                    \"name\": \"Zardzewiały klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Wenecja\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do hangaru łodzi\",\n                },\n                \"key_2\": {\n                    \"name\": \"Stalowy klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Żelazny klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kryjówka Bartoliego\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do biblioteki\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz do detonatora\",\n                }\n            }\n        },\n        {\n            \"title\": \"Opera\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ozdobny klucz\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Skrzynka przekaźnikowa\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Układ elektroniczny\",\n                }\n            }\n        },\n        {\n            \"title\": \"Platforma wiertnicza\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Czerwona karta dostępu\",\n                },\n                \"key_2\": {\n                    \"name\": \"Żółta karta dostępu\",\n                },\n                \"key_3\": {\n                    \"name\": \"Zielona karta dostępu\",\n                }\n            }\n        },\n        {\n            \"title\": \"Strefa nurkowania\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Czerwona karta dostępu\",\n                },\n                \"key_4\": {\n                    \"name\": \"Niebieska karta dostępu\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Chip maszynowy\",\n                }\n            }\n        },\n        {\n            \"title\": \"40 sążni\",\n        },\n        {\n            \"title\": \"Wrak statku Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do łazienki\",\n                },\n                \"key_2\": {\n                    \"name\": \"Zardzewiały klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Klucz do kajuty\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Wyłącznik obwodu\",\n                }\n            }\n        },\n        {\n            \"title\": \"Strefa pasażerska\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do teatru\",\n                },\n                \"key_2\": {\n                    \"name\": \"Zardzewiały klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pokład\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Klucz do zamka rufowego\",\n                },\n                \"key_3\": {\n                    \"name\": \"Klucz do magazynu\",\n                },\n                \"key_4\": {\n                    \"name\": \"Klucz do kajuty\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraf\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pogórze Tybetu\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lampart śnieżny\",\n                },\n                \"key_1\": {\n                    \"name\": \"Klucz do zwodzonego mostu\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz do chaty\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraf\",\n                }\n            }\n        },\n        {\n            \"title\": \"Klasztor Barkhang\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do skarbca\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz do zapadni\",\n                },\n                \"key_3\": {\n                    \"name\": \"Klucz do wyjścia na dach\",\n                },\n                \"key_4\": {\n                    \"name\": \"Klucz do głównej sali\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Młynek modlitewny\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Kamień szlachetny\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraf\",\n                }\n            }\n        },\n        {\n            \"title\": \"Katakumby Talionu\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Śnieżny lampart\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Pałka do gongu\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tybetańska maska\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pałac lodowy\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Biały tygrys\",\n                },\n                \"key_2\": {\n                    \"name\": \"Pałka do gongu\",\n                },\n                \"pickup_2\": {\n                    \"name\": \"Talion\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tybetańska maska\",\n                }\n            }\n        },\n        {\n            \"title\": \"Świątynia Xian\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Złoty klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Srebrny klucz\",\n                },\n                \"key_4\": {\n                    \"name\": \"Klucz do głównej komnaty\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Smocza pieczęć\",\n                }\n            }\n        },\n        {\n            \"title\": \"Podniebne wyspy\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mistyczna tabliczka\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Mistyczna tabliczka\",\n                }\n            }\n        },\n        {\n            \"title\": \"Legowisko smoka\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mistyczna tabliczka\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Sztylet Xian\",\n                }\n            }\n        },\n        {\n            \"title\": \"Dom, słodki dom\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do szafki na broń\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Sztylet Xian\",\n                }\n            }\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Wenecja\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do hangaru łodzi\",\n                },\n                \"key_2\": {\n                    \"name\": \"Stalowy klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Żelazny klucz\",\n                }\n            }\n        },\n        {\n            \"title\": \"Wrak statku Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do łazienki\",\n                },\n                \"key_2\": {\n                    \"name\": \"Zardzewiały klucz\",\n                },\n                \"key_3\": {\n                    \"name\": \"Klucz do kajuty\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Wyłącznik obwodu\",\n                }\n            }\n        },\n        {\n            \"title\": \"Pogórze Tybetu\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lampart śnieżny\",\n                },\n                \"key_1\": {\n                    \"name\": \"Klucz do zwodzonego mostu\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz do chaty\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Seraf\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"settings\": {\n        \"gameplay.fix_bear_ai\": {\n            \"description\": \"Fixes the bear pat attack in The Golden Mask so it does not miss Lara.\",\n        },\n        \"visuals.fix_animated_sprites\": {\n            \"description\": \"Fixes original issues in 40 Fathoms, Wreck of the Maria Doria and The Deck where plant sprites in water areas do not animate.\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Large Medipack\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Small Medipack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Cutscene 1\",\n        },\n        {\n            \"title\": \"Cutscene 2\",\n        },\n        {\n            \"title\": \"Cutscene 3\",\n        },\n        {\n            \"title\": \"Cutscene 4\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Lara's Home\",\n        },\n        {\n            \"title\": \"The Great Wall\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Guardhouse Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rusty Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Venice\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Boathouse Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Steel Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iron Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Bartoli's Hideout\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Library Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Detonator Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Opera House\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Ornate Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Relay Box\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Circuit Board\",\n                }\n            }\n        },\n        {\n            \"title\": \"Offshore Rig\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Red Pass Card\",\n                },\n                \"key_2\": {\n                    \"name\": \"Yellow Pass Card\",\n                },\n                \"key_3\": {\n                    \"name\": \"Green Pass Card\",\n                }\n            }\n        },\n        {\n            \"title\": \"Diving Area\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Red Pass Card\",\n                },\n                \"key_4\": {\n                    \"name\": \"Blue Pass Card\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Machine Chip\",\n                }\n            }\n        },\n        {\n            \"title\": \"40 Fathoms\",\n        },\n        {\n            \"title\": \"Wreck of the Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Rest Room Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rusty Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Cabin Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Circuit Breaker\",\n                }\n            }\n        },\n        {\n            \"title\": \"Living Quarters\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Theater Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rusty Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"The Deck\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Stern Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Storage Key\",\n                },\n                \"key_4\": {\n                    \"name\": \"Cabin Key\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"The Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tibetan Foothills\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Snow Leopard\",\n                },\n                \"key_1\": {\n                    \"name\": \"Drawbridge Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Hut Key\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"The Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Barkhang Monastery\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Strongroom Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Trapdoor Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Rooftops Key\",\n                },\n                \"key_4\": {\n                    \"name\": \"Main Hall Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Prayer Wheels\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Gemstones\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"The Seraph\",\n                }\n            }\n        },\n        {\n            \"title\": \"Catacombs of the Talion\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Snow Leopard\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Gong Hammer\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tibetan Mask\",\n                }\n            }\n        },\n        {\n            \"title\": \"Ice Palace\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"White Tiger\",\n                },\n                \"key_2\": {\n                    \"name\": \"Gong Hammer\",\n                },\n                \"pickup_2\": {\n                    \"name\": \"Talion\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tibetan Mask\",\n                }\n            }\n        },\n        {\n            \"title\": \"Temple of Xian\",\n            \"objects\": {\n                \"key_2\": {\n                    \"name\": \"Gold Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Silver Key\",\n                },\n                \"key_4\": {\n                    \"name\": \"Main Chamber Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"The Dragon Seal\",\n                }\n            }\n        },\n        {\n            \"title\": \"Floating Islands\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mystic Plaque\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Mystic Plaque\",\n                }\n            }\n        },\n        {\n            \"title\": \"The Dragon's Lair\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mystic Plaque\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Dagger of Xian\",\n                }\n            }\n        },\n        {\n            \"title\": \"Home Sweet Home\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Gun Cupboard Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Dagger of Xian\",\n                }\n            }\n        }\n    ],\n    \"demos\": [\n        {\n            \"title\": \"Venice\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Boathouse Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Steel Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Iron Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Wreck of the Maria Doria\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Rest Room Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rusty Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Cabin Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Circuit Breaker\",\n                }\n            }\n        },\n        {\n            \"title\": \"Tibetan Foothills\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Snow Leopard\",\n                },\n                \"key_1\": {\n                    \"name\": \"Drawbridge Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Hut Key\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"The Seraph\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2/weapons.json5",
    "content": "{\n    \"LGT_UNARMED\": {\n        \"sample_num\": \"SFX_LARA_NO\",\n    },\n\n    \"LGT_FLARE\": {\n        \"ammo\": {\n            \"initial_qty\": 6,\n            \"pickup_qty\": 6,\n            \"pickup_qty_alt\": 8,\n        },\n        \"flash_shade\": 2048,\n        \"flash_pos\": {\n            \"x\": 11,\n            \"y\": 32,\n            \"z\": 80,\n        },\n        \"flash_pos_alt\": {\n            \"x\": -6,\n            \"y\": 6,\n            \"z\": 80,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n    },\n\n    \"LGT_PISTOLS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 1,\n        \"ammo\": {\n            \"initial_qty\": 32,\n            \"pickup_qty\": 32,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 5120,\n        \"flash_pos\": {\n            \"y\": 185,\n            \"z\": 40,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_PISTOLS\",\n    },\n\n    \"LGT_MAGNUMS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 2,\n        \"ammo\": {\n            \"initial_qty\": 50,\n            \"pickup_qty\": 50,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\n            \"y\": 155,\n            \"z\": 55,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_MAGNUMS\",\n        \"is_available\": false,\n    },\n\n    \"LGT_AUTOS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 2,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\n            \"y\": 215,\n            \"z\": 65,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_AUTOS\",\n    },\n\n    \"LGT_DESERT_EAGLE\": {\n        \"type\": \"WEAPON_TYPE_SINGLE_PISTOL\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-10, +10, -80, +80],\n        \"right_angles\": [0, 0, 0, 0],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 4,\n        \"gun_height\": 650,\n        \"damage\": 21,\n        \"ammo\": {\n            \"initial_qty\": 10,\n            \"pickup_qty\": 10,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 16,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\n            \"y\": 215,\n            \"z\": 65,\n        },\n        \"sample_num\": \"SFX_LARA_DESERT_EAGLE\",\n        \"is_available\": false,\n    },\n\n    \"LGT_UZIS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 1,\n        \"ammo\": {\n            \"initial_qty\": 80,\n            \"pickup_qty\": 80,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 3,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 200,\n            \"z\": 50,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_UZI_FIRE\",\n    },\n\n    \"LGT_SHOTGUN\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 0,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 2,\n            \"pickup_qty\": 2,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 285,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_SHOTGUN\",\n    },\n\n    \"LGT_M16\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 4,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 12.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 400,\n            \"z\": 99,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {\"z\": -65},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_MP5\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 60,\n            \"pickup_qty\": 60,\n        },\n        \"target_dist\": 12.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 16,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\n            \"y\": 332,\n            \"z\": 96,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {\"z\": -65},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"\",\n        \"is_available\": false,\n    },\n\n    \"LGT_ROCKET\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 30,\n        \"ammo\": {\n            \"initial_qty\": 1,\n            \"pickup_qty\": 1,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 12,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n        \"is_available\": false,\n    },\n\n    \"LGT_GRENADE\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 30,\n        \"ammo\": {\n            \"initial_qty\": 2,\n            \"pickup_qty\": 2,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 0,\n        \"draw_frame\": 13,\n        \"undraw_frame\": 14,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_HARPOON\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -65, +65],\n        \"left_angles\": [-80, +80, -75, +75],\n        \"right_angles\": [-80, +80, -75, +75],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 4,\n        \"ammo\": {\n            \"initial_qty\": 3,\n            \"pickup_qty\": 3,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_SKIDOO\": {\n        \"type\": \"WEAPON_TYPE_MOUNTED\",\n        \"lock_angles\": [-30, 30, -55, 55],\n        \"left_angles\": [-30, 30, -55, 55],\n        \"right_angles\": [-30, 30, -55, 55],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 400,\n        \"damage\": 3,\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"flash_shade\": 5120,\n        \"flash_pos\": {\n            \"y\": 185,\n            \"z\": 40,\n        },\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_UZI_FIRE\",\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 2,\n    \"extends\": \"tr2\",\n    \"name\": \"The Golden Mask\",\n\n    \"main_menu_picture\": \"title_eu_gm.webp\",\n    \"savegame_file_fmt\": \"save_trgm_%02d.dat\",\n\n    \"demo_version\": false,\n    \"enable_tr2_item_drops\": true,\n    \"convert_dropped_guns\": true,\n\n    \"title\": {\n        \"path\": [\"title_gm.tr2\", \"title.tr2\"],\n        \"music_track\": 60,\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"legal_eu_gm.webp\", \"legal\": true},\n            {\"type\": \"exit_to_title\"},\n        ],\n        \"inherit_injections\": false,\n        \"injections\": [\n            \"font.bin\",\n            \"inv_background.bin\",\n            \"pda_model.bin\",\n            \"title_textures.bin\",\n            \"misc_sprites.bin\",\n        ]\n    },\n\n    \"sfx_path\": [\"main_gm.sfx\", \"main.sfx\"],\n    \"injections\": [\n        \"font.bin\",\n        \"lara_animations.bin\",\n        \"pda_model.bin\",\n        \"pickup_aid.bin\",\n        \"lara_extra.bin\",\n        \"lara_rifle_sfx.bin\",\n        \"secret_models_gm.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n        \"crystal.bin\",\n    ],\n\n    \"levels\": [\n        // 0. Legacy savegame placeholder\n        {\"type\": \"dummy\"},\n\n        // 1. The Cold War\n        {\n            \"path\": \"level1.tr2\",\n            \"script\": \"level1.lua\",\n            \"music_track\": 29,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"gm_level1.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"shotgun\"},\n                {\"type\": \"give_item\", \"object_id\": \"small_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"large_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"flare\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"coldwar_fd.bin\",\n                \"coldwar_itemrots.bin\",\n                \"coldwar_music_tracks.bin\",\n                \"coldwar_objects.bin\",\n                \"coldwar_textures.bin\",\n                \"common_pickup_meshes_gm.bin\",\n                \"lara_guns.bin\",\n                \"shark_sfx.bin\",\n                \"winston_model.bin\",\n                \"coldwar_crystals.bin\",\n            ],\n        },\n\n        // 2. Fool's Gold\n        {\n            \"path\": \"level2.tr2\",\n            \"music_track\": 54,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"gm_level2.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"door107_sfx.bin\",\n                \"door108_sfx.bin\",\n                \"fools_itemrots.bin\",\n                \"fools_music_tracks.bin\",\n                \"fools_pickup_meshes.bin\",\n                \"fools_textures.bin\",\n                \"lara_guns.bin\",\n                \"winston_model.bin\",\n                \"fools_crystals.bin\",\n            ],\n        },\n\n        // 3. Furnace of the Gods\n        {\n            \"path\": \"level3.tr2\",\n            \"script\": \"level3.lua\",\n            \"music_track\": 55,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"gm_level3.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"furnace_itemrots.bin\",\n                \"lara_guns.bin\",\n                \"furnace_music_tracks.bin\",\n                \"furnace_objects.bin\",\n                \"furnace_pickup_meshes.bin\",\n                \"furnace_textures.bin\",\n                \"winston_model.bin\",\n                \"furnace_crystals.bin\",\n            ],\n            \"unobtainable_ally_kills\": 2,\n        },\n\n        // 4. Kingdom\n        {\n            \"path\": \"level4.tr2\",\n            \"script\": \"level4.lua\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr2_bomber_jacket\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"gm_level4.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"puzzle_1\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 48},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit00_gm.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit01.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit02.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit03.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit04.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit05.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit06.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit07_gm.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit08.webp\", \"display_time\": 15, \"fade_in_time\": 0.5, \"fade_out_time\": 0.5},\n                {\"type\": \"total_stats\", \"background_path\": \"end.webp\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes_gm.bin\",\n                \"door108_sfx.bin\",\n                \"guardian_death_commands.bin\",\n                \"lara_guns.bin\",\n                \"kingdom_cameras.bin\",\n                \"kingdom_itemrots.bin\",\n                \"kingdom_music_tracks.bin\",\n                \"kingdom_textures.bin\",\n                \"winston_model.bin\",\n                \"kingdom_crystals.bin\",\n            ],\n        },\n\n        // 5. Nightmare in Vegas\n        {\n            \"path\": \"level5.tr2\",\n            \"type\": \"bonus\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr2_vegas\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"gm_level5.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"remove_weapons\"},\n                {\"type\": \"remove_ammo\"},\n                {\"type\": \"remove_flares\"},\n                {\"type\": \"remove_medipacks\"},\n                {\"type\": \"give_item\", \"object_id\": \"pistols\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 37},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"common_pickup_meshes_gm.bin\",\n                \"door108_sfx.bin\",\n                \"guardian_death_commands.bin\",\n                \"lara_vegas_guns.bin\",\n                \"vegas_fd.bin\",\n                \"vegas_itemrots.bin\",\n                \"vegas_music_tracks.bin\",\n                \"vegas_textures.bin\",\n                \"vegas_crystals.bin\",\n            ],\n        },\n    ],\n\n    \"demos\": [\n    ],\n\n    \"cutscenes\": [\n    ],\n\n    \"fmvs\": [\n    ],\n\n    \"hidden_config\": [\n        \"enable_cutscenes\",       // TR2G has no cutscenes\n        \"enable_demo\",            // TR2G has no demos\n        \"enable_fmv\",             // TR2G has no FMVs\n        \"enable_item_examining\",  // TR2G has no special item descriptions\n        \"fix_alligator_ai\",       // TR2G has no alligators\n        \"fix_animated_sprites\",\n        \"healthbar_poison_color\",\n        \"healthbar_poison_color_ps1\",\n        \"exposurebar_color\",\n        \"exposurebar_color_ps1\",\n        \"exposurebar_location\",\n        \"exposurebar_show_mode\",\n        \"change_pierre_spawn\",\n        \"fix_chainblock_secret_sound\",\n        \"enable_compass_stats\",\n        \"enable_wading\",\n        \"restore_ps1_enemies\",\n        \"fix_speeches_killing_music\",\n        \"enable_weather\",\n        \"enable_footprints\",\n        \"fix_monkey_pickup_priority\",\n        \"fix_pipeman_aim\",\n        \"enable_cinematics\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Laras Haus\",\n        },\n        {\n            \"title\": \"Der kalte Krieg\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Schneeleopard\",\n                },\n                \"key_1\": {\n                    \"name\": \"Wachraum-Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Schacht'B' Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Gold des Narren\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüsselkarte 1\",\n                },\n                \"key_4\": {\n                    \"name\": \"Schlüsselkarte 2\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Platine\",\n                }\n            }\n        },\n        {\n            \"title\": \"Hochofen der Götter\",\n            \"objects\": {\n                \"big_spider\": {\n                    \"name\": \"Eisbär\",\n                },\n                \"spider\": {\n                    \"name\": \"Wolf\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maske des Tornarsuk\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Goldklumpen\",\n                }\n            }\n        },\n        {\n            \"title\": \"Königreich\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Schneeleopard\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maske des Tornarsuk\",\n                }\n            }\n        },\n        {\n            \"title\": \"Alptraum in Vegas\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Hotelschlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Fahrstulteil\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Türstromkreis\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"\\\\{review}Demeure de Lara\",\n        },\n        {\n            \"title\": \"\\\\{review}Guerre froide\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"\\\\{review}Léopard des Neiges\",\n                },\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de la Salle des Gardiens\",\n                },\n                \"key_2\": {\n                    \"name\": \"\\\\{review}Clé de l'Ascenseur 'B'\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Désillusion\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé de Carte 1\",\n                },\n                \"key_4\": {\n                    \"name\": \"\\\\{review}Clé de Carte 2\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Carte de Circuit\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Le Fournaise des dieux\",\n            \"objects\": {\n                \"big_spider\": {\n                    \"name\": \"\\\\{review}Ours Polaire\",\n                },\n                \"spider\": {\n                    \"name\": \"\\\\{review}Loup\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Masque de Tornarsuk\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"\\\\{review}Pépite d'Or\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Royaume\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"\\\\{review}Léopard des Neiges\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Masque de Tornarsuk\",\n                }\n            }\n        },\n        {\n            \"title\": \"\\\\{review}Cauchemar à Las Vegas\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"\\\\{review}Clé d'Hôtel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"\\\\{review}Jonction de l'Ascenseur\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"\\\\{review}Circuit de Porte\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Dachaigh Lara\",\n        },\n        {\n            \"title\": \"An Cogadh Fuar\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lìopaird-shneachda\",\n                },\n                \"key_1\": {\n                    \"name\": \"Iuchair an t-Seòmair-ghàrda\",\n                },\n                \"key_2\": {\n                    \"name\": \"Iuchair Toll 'B'\",\n                }\n            }\n        },\n        {\n            \"title\": \"Òr nan Amadan\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Cairt-iuchrach 1\",\n                },\n                \"key_4\": {\n                    \"name\": \"Cairt-iuchrach 2\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Bòrd-chuairt\",\n                }\n            }\n        },\n        {\n            \"title\": \"Àmhainn nan Diathan\",\n            \"objects\": {\n                \"big_spider\": {\n                    \"name\": \"Mathan-pòla\",\n                },\n                \"spider\": {\n                    \"name\": \"Madadh-allaidh\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Masg Thornarsuk\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Bleideag Òir\",\n                }\n            }\n        },\n        {\n            \"title\": \"Rìoghachd\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lìopaird-shneachda\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Masg Thornarsuk\",\n                }\n            }\n        },\n        {\n            \"title\": \"Cuthag ann am Vegas\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Iuchair an Taigh-òsta\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ceangal an Lioft\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Cuairt na Dorsan\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Casa di Lara\",\n        },\n        {\n            \"title\": \"La Guerra Fredda\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Leopardo delle Nevi\",\n                },\n                \"key_1\": {\n                    \"name\": \"Chiave della Guardiola\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave del Tunnel \\\"B\\\"\",\n                }\n            }\n        },\n        {\n            \"title\": \"L'Oro degli Sciocchi\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave Magnetica 1\",\n                },\n                \"key_4\": {\n                    \"name\": \"Chiave Magnetica 2\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Circuito d'Avvio\",\n                }\n            }\n        },\n        {\n            \"title\": \"Crogiolo degli Dèi\",\n            \"objects\": {\n                \"big_spider\": {\n                    \"name\": \"Orso Polare\",\n                },\n                \"spider\": {\n                    \"name\": \"Lupo\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maschera di Tornarsuk\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Pepita d'Oro\",\n                }\n            }\n        },\n        {\n            \"title\": \"Il Regno\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Leopardo delle Nevi\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maschera di Tornarsuk\",\n                }\n            }\n        },\n        {\n            \"title\": \"Incubo a Las Vegas\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave dell'Hotel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Giunto per Ascensore\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Circuito della Porta\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Dom Lary\",\n        },\n        {\n            \"title\": \"Zimna Wojna\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lampart śnieżny\",\n                },\n                \"key_1\": {\n                    \"name\": \"Klucz do strażnicy\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz do szybu 'B'\",\n                }\n            }\n        },\n        {\n            \"title\": \"Złoto głupców\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Karta dostępu 1\",\n                },\n                \"key_4\": {\n                    \"name\": \"Karta dostępu 2\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Układ elektroniczny\",\n                }\n            }\n        },\n        {\n            \"title\": \"Piec bogów\",\n            \"objects\": {\n                \"big_spider\": {\n                    \"name\": \"Niedźwiedź polarny\",\n                },\n                \"spider\": {\n                    \"name\": \"Wilk\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maska Tornarsuka\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Złoty samorodek\",\n                }\n            }\n        },\n        {\n            \"title\": \"Królestwo\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Lampart śnieżny\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maska Tornarsuka\",\n                }\n            }\n        },\n        {\n            \"title\": \"Koszmar w Vegas\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do hotelu\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Węzeł windy\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Obwód drzwi\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-gm/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Lara's Home\",\n        },\n        {\n            \"title\": \"The Cold War\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Snow Leopard\",\n                },\n                \"key_1\": {\n                    \"name\": \"Guardroom Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Shaft 'B' Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Fool's Gold\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"CardKey 1\",\n                },\n                \"key_4\": {\n                    \"name\": \"CardKey 2\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Circuit Board\",\n                }\n            }\n        },\n        {\n            \"title\": \"Furnace of the Gods\",\n            \"objects\": {\n                \"big_spider\": {\n                    \"name\": \"Polar Bear\",\n                },\n                \"spider\": {\n                    \"name\": \"Wolf\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Mask of Tornarsuk\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Gold Nugget\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kingdom\",\n            \"objects\": {\n                \"tiger\": {\n                    \"name\": \"Snow Leopard\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Mask of Tornarsuk\",\n                }\n            }\n        },\n        {\n            \"title\": \"Nightmare in Vegas\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Hotel Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Elevator Junction\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Door Circuit\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/gameflow.json5",
    "content": "{\n    // This file is used to enable the -l argument support.\n\n    \"engine\": 2,\n    \"extends\": \"tr2\",\n    \"name\": \"TR2 Direct Level\",\n\n    \"main_menu_picture\": \"title_eu.webp\",\n    \"savegame_file_fmt\": \"save_tr2_custom_%02d.dat\",\n\n    \"demo_version\": false,\n    \"enable_tr2_item_drops\": true,\n    \"convert_dropped_guns\": true,\n\n    \"sfx_path\": \"main.sfx\",\n    \"injections\": [\n        \"font.bin\",\n        \"lara_animations.bin\",\n        \"lara_guns.bin\",\n        \"pda_model.bin\",\n        \"pickup_aid.bin\",\n        \"winston_model.bin\",\n        \"crystal.bin\",\n        \"lara_extra.bin\",\n        \"lara_rifle_sfx.bin\",\n        \"secret_models_og.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n    ],\n\n    \"levels\": [\n        {\n            \"path\": \"%direct_level%\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr2_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n            ],\n        },\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Testlevel\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/strings-fr.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"\\\\{review}Niveau de test\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/strings-gd.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Ìre Phròbhail\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Livello di Prova\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Poziom testowy\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr2-level/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Test Level\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3/catalog_item_actions.csv",
    "content": "0, ITEM_ACTION_TURN_180\n1, ITEM_ACTION_FLOOR_SHAKE\n2, ITEM_ACTION_LARA_NORMAL\n3, ITEM_ACTION_BUBBLES\n4, ITEM_ACTION_FINISH_LEVEL\n5, ITEM_ACTION_FLOOD\n6, ITEM_ACTION_CHANDELIER\n7, ITEM_ACTION_RUBBLE\n8, ITEM_ACTION_PISTON\n9, ITEM_ACTION_CURTAIN\n10, ITEM_ACTION_SET_CHANGE\n11, ITEM_ACTION_EXPLOSION\n12, ITEM_ACTION_LARA_HANDS_FREE\n13, ITEM_ACTION_FLIP_MAP\n14, ITEM_ACTION_LARA_DRAW_RIGHT_GUN\n15, ITEM_ACTION_LARA_DRAW_LEFT_GUN\n16, ITEM_ACTION_LARA_SHOOT_RIGHT_GUN\n17, ITEM_ACTION_LARA_SHOOT_LEFT_GUN\n18, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1\n19, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2\n20, ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3\n21, ITEM_ACTION_INVISIBILITY_ON\n22, ITEM_ACTION_INVISIBILITY_OFF\n23, ITEM_ACTION_DYNAMIC_LIGHT_ON\n24, ITEM_ACTION_DYNAMIC_LIGHT_OFF\n25, ITEM_ACTION_STATUE\n26, ITEM_ACTION_RESET_HAIR\n27, ITEM_ACTION_BOILER\n28, ITEM_ACTION_ASSAULT_RESET\n29, ITEM_ACTION_ASSAULT_STOP\n30, ITEM_ACTION_ASSAULT_START\n31, ITEM_ACTION_ASSAULT_FINISHED\n32, ITEM_ACTION_FOOTPRINT\n33, ITEM_ACTION_ASSAULT_PENALTY_8\n34, ITEM_ACTION_RACETRACK_START\n35, ITEM_ACTION_RACETRACK_RESET\n36, ITEM_ACTION_RACETRACK_FINISHED\n37, ITEM_ACTION_ASSAULT_PENALTY_30\n38, ITEM_ACTION_GYM_HINT_1\n39, ITEM_ACTION_GYM_HINT_2\n40, ITEM_ACTION_GYM_HINT_3\n41, ITEM_ACTION_GYM_HINT_4\n42, ITEM_ACTION_GYM_HINT_5\n43, ITEM_ACTION_GYM_HINT_6\n44, ITEM_ACTION_GYM_HINT_7\n45, ITEM_ACTION_GYM_HINT_8\n46, ITEM_ACTION_GYM_HINT_9\n47, ITEM_ACTION_GYM_HINT_10\n48, ITEM_ACTION_GYM_HINT_11\n49, ITEM_ACTION_GYM_HINT_12\n50, ITEM_ACTION_GYM_HINT_13\n51, ITEM_ACTION_GYM_HINT_14\n52, ITEM_ACTION_GYM_HINT_15\n53, ITEM_ACTION_GYM_HINT_16\n54, ITEM_ACTION_GYM_HINT_17\n55, ITEM_ACTION_GYM_HINT_18\n56, ITEM_ACTION_GYM_HINT_19\n57, ITEM_ACTION_GYM_HINT_RESET\n58, ITEM_ACTION_CAMERA_SHAKE\n59, ITEM_ACTION_LOWERING_BLOCK\n60, ITEM_ACTION_SHADOW_ON\n61, ITEM_ACTION_SHADOW_OFF\n62, ITEM_ACTION_TURN_90\n"
  },
  {
    "path": "data/trx/ship/games/tr3/catalog_lara_anims.csv",
    "content": "0, LA_RUN\n1, LA_WALK_FORWARD\n2, LA_WALK_STOP_RIGHT\n3, LA_WALK_STOP_LEFT\n4, LA_WALK_TO_RUN_RIGHT\n5, LA_WALK_TO_RUN_LEFT\n6, LA_RUN_START\n7, LA_RUN_TO_WALK_RIGHT\n8, LA_RUN_TO_STAND_LEFT\n9, LA_RUN_TO_WALK_LEFT\n10, LA_RUN_TO_STAND_RIGHT\n11, LA_STAND_STILL\n12, LA_TURN_RIGHT_SLOW\n13, LA_TURN_LEFT_SLOW\n14, LA_JUMP_FORWARD_LAND_START_UNUSED\n15, LA_JUMP_FORWARD_LAND_END_UNUSED\n16, LA_RUN_JUMP_RIGHT_START\n17, LA_RUN_JUMP_RIGHT_CONTINUE\n18, LA_RUN_JUMP_LEFT_START\n19, LA_RUN_JUMP_LEFT_CONTINUE\n20, LA_WALK_FORWARD_START\n21, LA_WALK_FORWARD_START_CONTINUE\n22, LA_JUMP_FORWARD_TO_FREEFALL\n23, LA_FREEFALL\n24, LA_FREEFALL_LAND\n25, LA_FREEFALL_LAND_DEATH\n26, LA_STAND_TO_JUMP_UP\n27, LA_STAND_TO_JUMP_UP_CONTINUE\n28, LA_JUMP_UP\n29, LA_JUMP_UP_TO_HANG_UNUSED\n30, LA_JUMP_UP_TO_FREEFALL\n31, LA_JUMP_UP_LAND\n32, LA_SMASH_JUMP\n33, LA_SMASH_JUMP_CONTINUE\n34, LA_FALL_START\n35, LA_FALL\n36, LA_FALL_TO_FREEFALL\n37, LA_HANG_TO_FREEFALL\n38, LA_WALK_BACK_END_RIGHT\n39, LA_WALK_BACK_END_LEFT\n40, LA_WALK_BACK\n41, LA_WALK_BACK_START\n42, LA_CLIMB_3CLICK\n43, LA_CLIMB_3CLICK_END_TO_RUN\n44, LA_TURN_RIGHT\n45, LA_JUMP_FORWARD_TO_FREEFALL_2\n46, LA_REACH_TO_FREEFALL\n47, LA_ROLL_ALTERNATE\n48, LA_ROLL_END_ALTERNATE\n49, LA_JUMP_FORWARD_END_TO_FREEFALL\n50, LA_CLIMB_2CLICK\n51, LA_CLIMB_2CLICK_END\n52, LA_CLIMB_2CLICK_END_TO_RUN\n53, LA_WALL_SMASH_LEFT\n54, LA_WALL_SMASH_RIGHT\n55, LA_RUN_UP_STEP_RIGHT\n56, LA_RUN_UP_STEP_LEFT\n57, LA_WALK_UP_STEP_RIGHT\n58, LA_WALK_UP_STEP_LEFT\n59, LA_WALK_DOWN_LEFT\n60, LA_WALK_DOWN_RIGHT\n61, LA_WALK_DOWN_BACK_LEFT\n62, LA_WALK_DOWN_BACK_RIGHT\n63, LA_WALL_SWITCH_DOWN\n64, LA_WALL_SWITCH_UP\n65, LA_SIDE_STEP_LEFT\n66, LA_SIDE_STEP_LEFT_END\n67, LA_SIDE_STEP_RIGHT\n68, LA_SIDE_STEP_RIGHT_END\n69, LA_ROTATE_LEFT\n70, LA_SLIDE_FORWARD\n71, LA_SLIDE_FORWARD_END\n72, LA_SLIDE_FORWARD_STOP\n73, LA_STAND_TO_JUMP\n74, LA_JUMP_BACK_START\n75, LA_JUMP_BACK\n76, LA_JUMP_FORWARD_START\n77, LA_JUMP_FORWARD\n78, LA_JUMP_LEFT_START\n79, LA_JUMP_LEFT\n80, LA_JUMP_RIGHT_START\n81, LA_JUMP_RIGHT\n82, LA_LAND\n83, LA_JUMP_BACK_TO_FREEFALL\n84, LA_JUMP_LEFT_TO_FREEFALL\n85, LA_JUMP_RIGHT_TO_FREEFALL\n86, LA_UNDERWATER_SWIM_FORWARD\n87, LA_UNDERWATER_SWIM_FORWARD_DRIFT\n88, LA_SMALL_JUMP_BACK_START\n89, LA_SMALL_JUMP_BACK\n90, LA_SMALL_JUMP_BACK_END\n91, LA_JUMP_UP_START\n92, LA_LAND_TO_RUN\n93, LA_FALL_BACK\n94, LA_JUMP_FORWARD_TO_REACH\n95, LA_REACH\n96, LA_REACH_TO_HANG\n97, LA_CLIMB_ON\n98, LA_REACH_TO_FREEFALL_2\n99, LA_FALL_CROUCHING_LANDING\n100, LA_JUMP_FORWARD_TO_REACH_LATE\n101, LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE\n102, LA_CLIMB_ON_END\n103, LA_STAND_IDLE\n104, LA_SLIDE_BACKWARD_START\n105, LA_SLIDE_BACKWARD\n106, LA_SLIDE_BACKWARD_END\n107, LA_UNDERWATER_SWIM_TO_IDLE\n108, LA_UNDERWATER_IDLE\n109, LA_UNDERWARER_IDLE_TO_SWIM\n110, LA_ONWATER_IDLE\n111, LA_ONWATER_TO_STAND_HIGH\n112, LA_FREEFALL_TO_UNDERWATER\n113, LA_ONWATER_DIVE_ALTERNATE\n114, LA_UNDERWATER_TO_ONWATER\n115, LA_ONWATER_SWIM_FORWARD_DIVE\n116, LA_ONWATER_SWIM_FORWARD\n117, LA_ONWATER_SWIM_FORWARD_TO_IDLE\n118, LA_ONWATER_IDLE_TO_SWIM_FORWARD\n119, LA_ONWATER_DIVE\n120, LA_PUSHABLE_GRAB\n121, LA_PUSHABLE_RELEASE\n122, LA_PUSHABLE_PULL\n123, LA_PUSHABLE_PUSH\n124, LA_UNDERWATER_DEATH\n125, LA_HIT_FRONT\n126, LA_HIT_BACK\n127, LA_HIT_LEFT\n128, LA_HIT_RIGHT\n129, LA_UNDERWATER_SWITCH\n130, LA_UNDERWATER_PICKUP\n131, LA_USE_KEY\n132, LA_ONWATER_DEATH\n133, LA_RUN_DEATH\n134, LA_USE_PUZZLE\n135, LA_PICKUP\n136, LA_SHIMMY_LEFT\n137, LA_SHIMMY_RIGHT\n138, LA_STAND_DEATH\n139, LA_BOULDER_DEATH\n140, LA_ONWATER_IDLE_TO_SWIM_BACK\n141, LA_ONWATER_SWIM_BACK\n142, LA_ONWATER_SWIM_BACK_TO_IDLE\n143, LA_ONWATER_SWIM_LEFT\n144, LA_ONWATER_SWIM_RIGHT\n145, LA_DEATH_JUMP\n146, LA_ROLL_START\n147, LA_ROLL_CONTINUE\n148, LA_ROLL_END\n149, LA_SPIKE_DEATH\n150, LA_SWING_IN_SLOW\n151, LA_SWANDIVE_ROLL\n152, LA_SWANDIVE_TO_UNDERWATER\n153, LA_FREEFALL_SWANDIVE\n154, LA_FREEFALL_SWANDIVE_TO_UNDERWATER\n155, LA_SWANDIVE_DEATH\n156, LA_SWANDIVE_LEFT\n157, LA_SWANDIVE_RIGHT\n158, LA_SWANDIVE_START\n159, LA_CLIMB_ON_HANDSTAND\n160, LA_STAND_TO_LADDER\n161, LA_LADDER_UP\n162, LA_LADDER_UP_STOP_RIGHT\n163, LA_LADDER_UP_STOP_LEFT\n164, LA_LADDER_IDLE\n165, LA_LADDER_UP_START\n166, LA_LADDER_DOWN_STOP_LEFT\n167, LA_LADDER_DOWN_STOP_RIGHT\n168, LA_LADDER_DOWN\n169, LA_LADDER_DOWN_START\n170, LA_LADDER_RIGHT\n171, LA_LADDER_LEFT\n172, LA_LADDER_HANG\n173, LA_LADDER_HANG_TO_IDLE\n174, LA_LADDER_CLIMB_ON\n175, LA_UNKNOWN\n176, LA_ONWATER_TO_WADE_SHALLOW_UNUSED\n177, LA_WADE\n178, LA_RUN_TO_WADE_LEFT\n179, LA_RUN_TO_WADE_RIGHT\n180, LA_WADE_TO_RUN_LEFT\n181, LA_WADE_TO_RUN_RIGHT\n182, LA_LADDER_BACKFLIP_START\n183, LA_LADDER_BACKFLIP_CONTINUE\n184, LA_WADE_TO_STAND_RIGHT\n185, LA_WADE_TO_STAND_LEFT\n186, LA_STAND_TO_WADE\n187, LA_LADDER_UP_HANGING\n188, LA_LADDER_DOWN_HANGING\n189, LA_FLARE_THROW\n190, LA_ONWATER_TO_WADE\n191, LA_ONWATER_TO_STAND_MEDIUM\n192, LA_UNDERWATER_TO_STAND\n193, LA_ONWATER_TO_WADE_LOW\n194, LA_LADDER_TO_HANG_DOWN\n195, LA_SWITCH_SMALL_DOWN\n196, LA_SWITCH_SMALL_UP\n197, LA_BUTTON_PUSH\n198, LA_UNDERWATER_SWIM_TO_STILL_HUDDLE\n199, LA_UNDERWATER_SWIM_TO_STILL_SPRAWL\n200, LA_UNDERWATER_SWIM_TO_STILL_MEDIUM\n201, LA_LADDER_TO_HANG_RIGHT\n202, LA_LADDER_TO_HANG_LEFT\n203, LA_UNDERWATER_ROLL_START\n204, LA_FLARE_PICKUP\n205, LA_UNDERWATER_ROLL_END\n206, LA_UNDERWATER_FLARE_PICKUP\n207, LA_RUN_JUMP_ROLL_START\n208, LA_SOMERSAULT\n209, LA_RUN_JUMP_ROLL_END\n210, LA_JUMP_FORWARD_ROLL_START\n211, LA_JUMP_FORWARD_ROLL_END\n212, LA_JUMP_BACK_ROLL_START\n213, LA_JUMP_BACK_ROLL_END\n214, LA_ZIPLINE_GRAB\n215, LA_ZIPLINE_RIDE\n216, LA_ZIPLINE_FALL\n217, LA_STAND_TO_CROUCH\n218, LA_CROUCH_ROLL_FORWARD_START\n219, LA_CROUCH_ROLL_FORWARD_CONTINUE\n220, LA_CROUCH_ROLL_FORWARD_END\n221, LA_CROUCH_TO_STAND\n222, LA_CROUCH_IDLE\n223, LA_SPRINT\n224, LA_RUN_TO_SPRINT_LEFT\n225, LA_RUN_TO_SPRINT_RIGHT\n226, LA_SPRINT_SLIDE_STAND_RIGHT\n227, LA_SPRINT_SLIDE_STAND_RIGHT_END_ALTERNATE_UNUSED\n228, LA_SPRINT_SLIDE_STAND_LEFT\n229, LA_SPRINT_SLIDE_STAND_LEFT_END_ALTERNATE_UNUSED\n230, LA_SPRINT_TO_ROLL_LEFT\n231, LA_SPRINT_TO_ROLL_LEFT_BETA_UNUSED\n232, LA_SPRINT_ROLL_LEFT_TO_RUN\n233, LA_MONKEY_GRAB\n234, LA_MONKEY_IDLE\n235, LA_MONKEY_FALL\n236, LA_MONKEY_FORWARD\n237, LA_MONKEY_STOP_LEFT\n238, LA_MONKEY_STOP_RIGHT\n239, LA_MONKEY_IDLE_TO_FORWARD_LEFT\n240, LA_SPRINT_TO_ROLL_ALTERNATE_START_UNUSED\n241, LA_SPRINT_TO_ROLL_ALTERNATE_CONTINUE_UNUSED\n242, LA_SPRINT_TO_ROLL_ALTERNATE_END_UNUSED\n243, LA_SPRINT_TO_RUN_LEFT\n244, LA_SPRINT_TO_RUN_RIGHT\n245, LA_STAND_TO_CROUCH_END\n246, LA_SLIDE_FORWARD_TO_RUN\n247, LA_CROUCH_ROLL_FORWARD_START_ALTERNATE_UNUSED\n248, LA_JUMP_FORWARD_START_TO_GRAB_EARLY\n249, LA_JUMP_FORWARD_START_TO_GRAB_LATE\n250, LA_RUN_TO_GRAB_RIGHT\n251, LA_RUN_TO_GRAB_LEFT\n252, LA_MONKEY_IDLE_TO_FORWARD_RIGHT\n253, LA_MONKEY_SHIMMY_LEFT\n254, LA_MONKEY_SHIMMY_LEFT_END\n255, LA_MONKEY_SHIMMY_RIGHT\n256, LA_MONKEY_SHIMMY_RIGHT_END\n257, LA_MONKEY_TURN_AROUND\n258, LA_CROUCH_TO_CRAWL_START\n259, LA_CRAWL_TO_CROUCH_START\n260, LA_CRAWL_FORWARD\n261, LA_CRAWL_IDLE_TO_FORWARD\n262, LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT\n263, LA_CRAWL_IDLE\n264, LA_CROUCH_TO_CRAWL_END\n265, LA_CRAWL_TO_CROUCH_END_UNUSED\n266, LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT\n267, LA_CRAWL_FORWARD_TO_IDLE_START_LEFT\n268, LA_CRAWL_FORWARD_TO_IDLE_END_LEFT\n269, LA_CRAWL_TURN_LEFT\n270, LA_CRAWL_TURN_RIGHT\n271, LA_MONKEY_TURN_LEFT\n272, LA_MONKEY_TURN_RIGHT\n273, LA_CROUCH_TO_CRAWL_CONTINUE\n274, LA_CRAWL_TO_CROUCH_CONTINUE\n275, LA_CRAWL_IDLE_TO_BACKWARD\n276, LA_CRAWL_BACKWARD\n277, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START\n278, LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END\n279, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START\n280, LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END\n281, LA_CRAWL_TURN_LEFT_EARLY_END\n282, LA_CRAWL_TURN_RIGHT_EARLY_END\n283, LA_MONKEY_TURN_LEFT_EARLY_END\n284, LA_MONKEY_TURN_LEFT_LATE_END\n285, LA_MONKEY_TURN_RIGHT_EARLY_END\n286, LA_MONKEY_TURN_RIGHT_LATE_END\n287, LA_HANG_TO_CROUCH_START\n288, LA_HANG_TO_CROUCH_END\n289, LA_CRAWL_TO_HANG_START\n290, LA_CRAWL_TO_HANG_CONTINUE\n291, LA_CROUCH_PICKUP\n292, LA_CRAWL_PICKUP\n293, LA_CROUCH_HIT_FRONT\n294, LA_CROUCH_HIT_BACK\n295, LA_CROUCH_HIT_RIGHT\n296, LA_CROUCH_HIT_LEFT\n297, LA_CRAWL_HIT_FRONT_UNUSED\n298, LA_CRAWL_HIT_BACK_UNUSED\n299, LA_CRAWL_HIT_RIGHT_UNUSED\n300, LA_CRAWL_HIT_LEFT_UNUSED\n301, LA_CRAWL_DEATH\n302, LA_CRAWL_TO_HANG_END\n303, LA_STAND_TO_CROUCH_ABORT_UNUSED\n304, LA_RUN_TO_CROUCH_LEFT_START\n305, LA_RUN_TO_CROUCH_RIGHT_START\n306, LA_RUN_TO_CROUCH_LEFT_END\n307, LA_RUN_TO_CROUCH_RIGHT_END\n308, LA_SPRINT_TO_ROLL_RIGHT\n309, LA_SPRINT_ROLL_RIGHT_TO_RUN\n310, LA_SPRINT_TO_CROUCH_LEFT\n311, LA_SPRINT_TO_CROUCH_RIGHT\n312, LA_CROUCH_PICKUP_FLARE\n313, LA_KICK\n314, LA_JUMP_NEUTRAL_ROLL\n315, LA_CONTROLLED_DROP\n316, LA_CONTROLLED_DROP_CONTINUE\n317, LA_HANG_TO_JUMP_UP\n318, LA_HANG_TO_JUMP_UP_CONTINUE\n319, LA_HANG_TO_JUMP_BACK\n320, LA_HANG_TO_JUMP_BACK_CONTINUE\n321, LA_POSE_RIGHT_START\n322, LA_POSE_RIGHT_CONTINUE\n323, LA_POSE_RIGHT_END\n324, LA_POSE_LEFT_START\n325, LA_POSE_LEFT_CONTINUE\n326, LA_POSE_LEFT_END\n327, LA_CRAWL_JUMP_DOWN\n328, LA_CROUCH_TURN_LEFT\n329, LA_CROUCH_TURN_RIGHT\n330, LA_SWING_IN_FAST\n331, LA_LADDER_TO_CROUCH_START\n332, LA_LADDER_TO_CROUCH_END\n"
  },
  {
    "path": "data/trx/ship/games/tr3/catalog_lara_states.csv",
    "content": "0, LS_WALK\n1, LS_RUN\n2, LS_STOP\n3, LS_JUMP_FORWARD\n4, LS_POSE\n5, LS_FAST_BACK\n6, LS_TURN_RIGHT\n7, LS_TURN_LEFT\n8, LS_DEATH\n9, LS_FAST_FALL\n10, LS_HANG\n11, LS_REACH\n12, LS_SPLAT\n13, LS_TREAD\n14, LS_LAND\n15, LS_COMPRESS\n16, LS_WALK_BACK\n17, LS_SWIM\n18, LS_GLIDE\n19, LS_PULL_UP\n20, LS_FAST_TURN\n21, LS_STEP_RIGHT\n22, LS_STEP_LEFT\n23, LS_ROLL_CONT\n24, LS_SLIDE\n25, LS_JUMP_BACK\n26, LS_JUMP_RIGHT\n27, LS_JUMP_LEFT\n28, LS_JUMP_UP\n29, LS_FALL_BACK\n30, LS_SHIMMY_LEFT\n31, LS_SHIMMY_RIGHT\n32, LS_SLIDE_BACK\n33, LS_SURF_TREAD\n34, LS_SURF_SWIM\n35, LS_DIVE\n36, LS_PUSH_BLOCK\n37, LS_PULL_BLOCK\n38, LS_PP_READY\n39, LS_PICKUP\n40, LS_SWITCH_ON\n41, LS_SWITCH_OFF\n42, LS_USE_KEY\n43, LS_USE_PUZZLE\n44, LS_UW_DEATH\n45, LS_ROLL\n46, LS_SPECIAL\n47, LS_SURF_BACK\n48, LS_SURF_LEFT\n49, LS_SURF_RIGHT\n50, LS_USE_MIDAS\n51, LS_DIE_MIDAS\n52, LS_SWAN_DIVE\n53, LS_FAST_DIVE\n54, LS_GYMNAST\n55, LS_WATER_OUT\n56, LS_CLIMB_STANCE\n57, LS_CLIMBING\n58, LS_CLIMB_LEFT\n59, LS_CLIMB_END\n60, LS_CLIMB_RIGHT\n61, LS_CLIMB_DOWN\n62, LS_LARA_TEST1\n63, LS_LARA_TEST2\n64, LS_LARA_TEST3\n65, LS_WADE\n66, LS_WATER_ROLL\n67, LS_FLARE_PICKUP\n68, LS_TWIST\n69, LS_KICK\n70, LS_ZIPLINE\n71, LS_CROUCH_IDLE\n72, LS_CROUCH_ROLL\n73, LS_SPRINT\n74, LS_SPRINT_ROLL\n75, LS_MONKEY_IDLE\n76, LS_MONKEY_FORWARD\n77, LS_MONKEY_LEFT\n78, LS_MONKEY_RIGHT\n79, LS_MONKEY_ROLL\n80, LS_CRAWL_IDLE\n81, LS_CRAWL_FORWARD\n82, LS_MONKEY_TURN_LEFT\n83, LS_MONKEY_TURN_RIGHT\n84, LS_CRAWL_TURN_LEFT\n85, LS_CRAWL_TURN_RIGHT\n86, LS_CRAWL_BACK\n87, LS_CLIMB_TO_CRAWL\n88, LS_CRAWL_TO_CLIMB\n89, LS_CONTROLLED\n90, LS_RESPONSIVE\n91, LS_NEUTRAL_ROLL\n92, LS_POSE_START\n93, LS_POSE_END\n94, LS_POSE_LEFT\n95, LS_POSE_RIGHT\n96, LS_CRAWL_JUMP_DOWN\n97, LS_CROUCH_TURN_LEFT\n98, LS_CROUCH_TURN_RIGHT\n"
  },
  {
    "path": "data/trx/ship/games/tr3/catalog_music.csv",
    "content": "12, MX_RIB_THEME\n12, MX_MINE_CART_THEME\n82, MX_TR3_GYM_EXERCISE_07\n83, MX_TR3_GYM_EXERCISE_14\n86, MX_TR3_GYM_EXERCISE_04\n89, MX_TR3_GYM_EXERCISE_16\n90, MX_TR3_GYM_EXERCISE_19\n95, MX_TR3_GYM_HINT_FAST_TIME\n96, MX_TR3_GYM_EXERCISE_17\n98, MX_TR3_GYM_EXERCISE_11\n107, MX_TR3_GYM_EXERCISE_02\n108, MX_TR3_GYM_EXERCISE_01\n109, MX_TR3_GYM_EXERCISE_15\n110, MX_TR3_GYM_EXERCISE_06\n112, MX_TR3_GYM_EXERCISE_18\n113, MX_TR3_GYM_EXERCISE_08\n114, MX_TR3_GYM_EXERCISE_09\n115, MX_TR3_GYM_EXERCISE_03\n116, MX_TR3_GYM_EXERCISE_12\n117, MX_TR3_GYM_EXERCISE_13\n118, MX_TR3_GYM_EXERCISE_05\n119, MX_TR3_GYM_EXERCISE_10\n122, MX_SECRET\n"
  },
  {
    "path": "data/trx/ship/games/tr3/catalog_objects.csv",
    "content": "0,   O_LARA\n1,   O_LARA_PISTOLS\n2,   O_LARA_HAIR\n3,   O_LARA_SHOTGUN\n4,   O_LARA_DESERT_EAGLE\n5,   O_LARA_UZIS\n6,   O_LARA_MP5\n7,   O_LARA_ROCKET_GUN\n8,   O_LARA_GRENADE_GUN\n9,   O_LARA_HARPOON_GUN\n10,  O_LARA_FLARE\n11,  O_LARA_VEHICLE_ANIM\n12,  O_LARA_VEHICLE_EXTRA\n13,  O_LARA_EXTRA\n14,  O_KAYAK\n15,  O_RIB\n16,  O_QUAD_BIKE\n17,  O_MINE_CART\n18,  O_MOUNTED_GUN\n19,  O_UPV\n20,  O_TRIBE_AXEMAN\n21,  O_TRIBE_PIPEMAN\n22,  O_PATROL_DOG\n23,  O_MOUSE\n24,  O_KILL_ALL_TRIGGERED\n25,  O_ORCA\n26,  O_DIVER\n27,  O_CROW\n28,  O_TIGER\n29,  O_VULTURE\n30,  O_ASSAULT_TARGET\n31,  O_DYING_MUTANT\n32,  O_ALLIGATOR\n34,  O_COMPY\n35,  O_LIZARD\n36,  O_TRIBE_BOSS\n37,  O_STHPAC_MERCENARY\n38,  O_CARCASS\n39,  O_RX_WORKER_1\n40,  O_RX_WORKER_2\n41,  O_HUSKIE\n42,  O_CRAWLER_MUTANT\n44,  O_WASP_MUTANT\n45,  O_CLAW_MUTANT\n46,  O_HYBRID_MUTANT\n47,  O_WASP_MUTANT_EMITTER\n48,  O_RAPTOR_EMITTER\n49,  O_WILLARD\n50,  O_RX_WORKER_3\n51,  O_SWAT_1\n52,  O_SWAT_2\n53,  O_PUNK_1\n54,  O_PUNK_2\n55,  O_WATER_BLOKE\n56,  O_SECURITY_GUARD\n57,  O_SOPHIA\n58,  O_ELECTRIC_CLEANER\n59,  O_FLOATING_CORPSE\n60,  O_MP_1\n61,  O_MP_2\n62,  O_PRISONER\n63,  O_SWAT_3\n64,  O_SENTRY_GUN\n65,  O_CIVILIAN\n66,  O_SECURITY_LASER_ALARM\n67,  O_SECURITY_LASER_DEADLY\n68,  O_SECURITY_LASER_KILLER\n69,  O_COBRA\n70,  O_SHIVA\n71,  O_MONKEY\n72,  O_BEAR_TRAP\n73,  O_TONY\n74,  O_AI_GUARD\n75,  O_AI_AMBUSH\n76,  O_AI_PATROL_1\n77,  O_AI_MODIFY\n78,  O_AI_FOLLOW\n79,  O_AI_PATROL_2\n80,  O_AI_X1\n81,  O_AI_X2\n82,  O_AI_X3\n83,  O_FALLING_BLOCK_1\n84,  O_FALLING_BLOCK_2\n85,  O_FALLING_BLOCK_3\n86,  O_PENDULUM_1\n87,  O_SPIKES\n88,  O_ROLLING_BALL_1\n89,  O_ROLLING_BALL_4\n90,  O_POISON_DART\n91,  O_POISON_DART_EMITTER\n92,  O_HOMING_DART_EMITTER\n93,  O_DRAWBRIDGE\n94,  O_TEETH_TRAP\n95,  O_LIFT\n96,  O_MOVING_BAR\n97,  O_MOVABLE_BLOCK_1\n98,  O_MOVABLE_BLOCK_2\n99,  O_MOVABLE_BLOCK_3\n100, O_MOVABLE_BLOCK_4\n101, O_SMASH_OBJECT_4\n102, O_SMASH_OBJECT_3\n103, O_SMASH_OBJECT_2\n104, O_SMASH_OBJECT_1\n105, O_POWER_SAW\n106, O_HOOK\n107, O_FALLING_CEILING_1\n108, O_SPINNING_BLADE\n109, O_CIRCULAR_BLADE\n110, O_TRAIN\n111, O_BLADE\n112, O_ROLLING_BALL_2\n113, O_ICICLE\n114, O_SPIKE_WALL\n115, O_SPRINGBOARD\n116, O_CEILING_SPIKES\n117, O_SWITCH_TYPE_WHEEL\n118, O_SWITCH_TYPE_SMALL\n119, O_PROPELLER_2\n120, O_PROPELLER_3\n121, O_PENDULUM_2\n122, O_MESH_SWAP_1\n123, O_MESH_SWAP_2\n124, O_MESH_SWAP_3\n125, O_TEXT_BOX\n126, O_ROLLING_BALL_3\n127, O_ZIPLINE_HANDLE\n128, O_SWITCH_TYPE_BUTTON\n129, O_SWITCH_TYPE_NORMAL\n130, O_SWITCH_TYPE_UW\n131, O_DOOR_TYPE_1\n132, O_DOOR_TYPE_2\n133, O_DOOR_TYPE_3\n134, O_DOOR_TYPE_4\n135, O_DOOR_TYPE_5\n136, O_DOOR_TYPE_6\n137, O_DOOR_TYPE_7\n138, O_DOOR_TYPE_8\n139, O_TRAPDOOR_TYPE_1\n140, O_TRAPDOOR_TYPE_2\n141, O_TRAPDOOR_TYPE_3\n142, O_BRIDGE_FLAT\n143, O_BRIDGE_TILT_1\n144, O_BRIDGE_TILT_2\n145, O_PASSPORT_OPTION\n146, O_STOPWATCH_OPTION\n147, O_PHOTO_OPTION\n148, O_PLAYER_1\n149, O_PLAYER_2\n150, O_PLAYER_3\n151, O_PLAYER_4\n152, O_PLAYER_5\n153, O_PLAYER_6\n154, O_PLAYER_7\n155, O_PLAYER_8\n156, O_PLAYER_9\n157, O_PLAYER_10\n158, O_PASSPORT_CLOSED\n159, O_PDA_OPTION\n160, O_PISTOL_ITEM\n161, O_SHOTGUN_ITEM\n162, O_DESERT_EAGLE_ITEM\n163, O_UZI_ITEM\n164, O_HARPOON_ITEM\n165, O_MP5_ITEM\n166, O_ROCKET_GUN_ITEM\n167, O_GRENADE_GUN_ITEM\n168, O_PISTOL_AMMO_ITEM\n169, O_SHOTGUN_AMMO_ITEM\n170, O_DESERT_EAGLE_AMMO_ITEM\n171, O_UZI_AMMO_ITEM\n172, O_HARPOON_AMMO_ITEM\n173, O_MP5_AMMO_ITEM\n174, O_ROCKET_AMMO_ITEM\n175, O_GRENADE_AMMO_ITEM\n176, O_SMALL_MEDIPACK_ITEM\n177, O_LARGE_MEDIPACK_ITEM\n178, O_FLAREBOX_ITEM\n179, O_FLARE_ITEM\n180, O_SAVE_CRYSTAL_ITEM\n181, O_DETAIL_OPTION\n182, O_SOUND_OPTION\n183, O_CONTROL_OPTION\n184, O_GLOBE_SELECT_OPTION\n185, O_PISTOL_OPTION\n186, O_SHOTGUN_OPTION\n187, O_DESERT_EAGLE_OPTION\n188, O_UZI_OPTION\n189, O_HARPOON_OPTION\n190, O_MP5_OPTION\n191, O_ROCKET_GUN_OPTION\n192, O_GRENADE_GUN_OPTION\n193, O_PISTOL_AMMO_OPTION\n194, O_SHOTGUN_AMMO_OPTION\n195, O_DESERT_EAGLE_AMMO_OPTION\n196, O_UZI_AMMO_OPTION\n197, O_HARPOON_AMMO_OPTION\n198, O_MP5_AMMO_OPTION\n199, O_ROCKET_AMMO_OPTION\n200, O_GRENADE_AMMO_OPTION\n201, O_SMALL_MEDIPACK_OPTION\n202, O_LARGE_MEDIPACK_OPTION\n203, O_FLAREBOX_OPTION\n204, O_SAVE_CRYSTAL_OPTION\n205, O_PUZZLE_ITEM_1\n206, O_PUZZLE_ITEM_2\n207, O_PUZZLE_ITEM_3\n208, O_PUZZLE_ITEM_4\n209, O_PUZZLE_OPTION_1\n210, O_PUZZLE_OPTION_2\n211, O_PUZZLE_OPTION_3\n212, O_PUZZLE_OPTION_4\n213, O_PUZZLE_HOLE_1\n214, O_PUZZLE_HOLE_2\n215, O_PUZZLE_HOLE_3\n216, O_PUZZLE_HOLE_4\n217, O_PUZZLE_DONE_1\n218, O_PUZZLE_DONE_2\n219, O_PUZZLE_DONE_3\n220, O_PUZZLE_DONE_4\n221, O_SECRET_1\n222, O_SECRET_2\n223, O_SECRET_3\n224, O_KEY_ITEM_1\n225, O_KEY_ITEM_2\n226, O_KEY_ITEM_3\n227, O_KEY_ITEM_4\n228, O_KEY_OPTION_1\n229, O_KEY_OPTION_2\n230, O_KEY_OPTION_3\n231, O_KEY_OPTION_4\n232, O_KEY_HOLE_1\n233, O_KEY_HOLE_2\n234, O_KEY_HOLE_3\n235, O_KEY_HOLE_4\n236, O_PICKUP_ITEM_1\n237, O_PICKUP_ITEM_2\n238, O_PICKUP_OPTION_1\n239, O_PICKUP_OPTION_2\n240, O_QUEST_ITEM_1\n241, O_QUEST_ITEM_2\n242, O_QUEST_ITEM_3\n243, O_QUEST_ITEM_4\n244, O_QUEST_OPTION_1\n245, O_QUEST_OPTION_2\n246, O_QUEST_OPTION_3\n247, O_QUEST_OPTION_4\n248, O_PICKUP_DISPLAY_PISTOLS\n249, O_PICKUP_DISPLAY_SHOTGUN\n250, O_PICKUP_DISPLAY_DESERTEAGLE\n251, O_PICKUP_DISPLAY_UZIS\n252, O_PICKUP_DISPLAY_HARPOON\n253, O_PICKUP_DISPLAY_HANDK\n254, O_PICKUP_DISPLAY_ROCKET_LAUNCHER\n255, O_PICKUP_DISPLAY_GRENADE_LAUNCHER\n256, O_PICKUP_DISPLAY_PISTOLS_AMMO\n257, O_PICKUP_DISPLAY_SHOTGUN_AMMO\n258, O_PICKUP_DISPLAY_DESERTEAGLE_AMMO\n259, O_PICKUP_DISPLAY_UZIS_AMMO\n260, O_PICKUP_DISPLAY_HARPOON_AMMO\n261, O_PICKUP_DISPLAY_HANDK_AMMO\n262, O_PICKUP_DISPLAY_ROCKET_LAUNCHER_AMMO\n263, O_PICKUP_DISPLAY_GRENADE_LAUNCHER_AMMO\n264, O_PICKUP_DISPLAY_SMALL_MEDIPACK\n265, O_PICKUP_DISPLAY_LARGE_MEDIPACK\n266, O_PICKUP_DISPLAY_FLAREBOX\n267, O_PICKUP_DISPLAY_SAVEGAME_CCRYSTAL\n268, O_PICKUP_DISPLAY_PUZZLE_1\n269, O_PICKUP_DISPLAY_PUZZLE_2\n270, O_PICKUP_DISPLAY_PUZZLE_3\n271, O_PICKUP_DISPLAY_PUZZLE_4\n272, O_PICKUP_DISPLAY_KEY_1\n273, O_PICKUP_DISPLAY_KEY_2\n274, O_PICKUP_DISPLAY_KEY_3\n275, O_PICKUP_DISPLAY_KEY_4\n276, O_PICKUP_DISPLAY_ICON_1\n277, O_PICKUP_DISPLAY_ICON_2\n278, O_PICKUP_DISPLAY_ICON_3\n279, O_PICKUP_DISPLAY_ICON_4\n280, O_PICKUP_DISPLAY_PICKUP_1\n281, O_PICKUP_DISPLAY_PICKUP_2\n282, O_FIRE_HEAD\n283, O_TONY_FIRE_BALL\n284, O_SPHERE_OF_DOOM_3\n285, O_ALARM_SOUND\n286, O_WATER_DRIP\n287, O_TREX_ALPHA\n288, O_RAPTOR\n289, O_BIRD_TWEETER\n290, O_CLOCK_CHIMES\n291, O_ROTATING_LASER\n292, O_ELECTRIC_FENCE\n293, O_HOT_LIQUID\n294, O_SHADOW\n295, O_DETONATOR_BOX\n296, O_EXPLOSION_1\n297, O_BUBBLE_1\n298, O_BUBBLE_2\n299, O_GLOW\n300, O_GUN_FLASH\n301, O_M16_FLASH\n302, O_DESERT_EAGLE_FLASH\n303, O_BODY_PART\n304, O_CAMERA_TARGET\n305, O_WATERFALL\n306, O_MISSILE_HARPOON\n307, O_DRAGON_FIRE\n308, O_KNIFE\n309, O_ROCKET\n310, O_HARPOON_BOLT\n311, O_GRENADE\n312, O_AREA_51_ROCKET\n313, O_AREA_51_ROCKET_BLAST\n314, O_AREA_51_ROCKET_SUPPORT\n315, O_LARA_SKIN\n316, O_LAVA\n317, O_LAVA_EMITTER\n318, O_STROBE_LIGHT\n319, O_ELECTRICAL_LIGHT\n320, O_ON_OFF_LIGHT\n321, O_PULSE_LIGHT\n322, O_BEACON_LIGHT\n323, O_EXTRA_LIGHT_UNUSED\n324, O_RED_LIGHT\n325, O_GREEN_LIGHT\n326, O_BLUE_LIGHT\n327, O_AMBER_LIGHT\n328, O_WHITE_LIGHT\n329, O_FLAME\n330, O_FLAME_EMITTER_BIG\n331, O_FLAME_EMITTER_SMALL\n332, O_FLAME_EMITTER_JET\n333, O_FLAME_EMITTER_SIDE\n334, O_SMOKE_EMITTER_WHITE\n335, O_SMOKE_EMITTER_BLACK\n336, O_STEAM_EMITTER\n337, O_GAS_EMITTER_GREEN\n338, O_PIRAHNAS\n339, O_TROPICAL_FISH\n340, O_PIRAHNA_GFX\n341, O_TROPICAL_FISH_GFX\n342, O_BAT_GFX\n343, O_TRIBEBOSS_GFX\n344, O_SPIDER_GFX\n345, O_TUMBLEWEED\n346, O_LEAVES\n347, O_BAT_EMITTER\n348, O_BIRD_EMITTER\n349, O_ANIMATING_1\n350, O_ANIMATING_2\n351, O_ANIMATING_3\n352, O_ANIMATING_4\n353, O_ANIMATING_5\n354, O_ANIMATING_6\n355, O_SKYBOX\n356, O_ALPHABET\n357, O_DING_DONG\n358, O_LARA_ALARM\n359, O_MINI_COPTER\n360, O_WINSTON\n361, O_WINSTON_ARMY\n362, O_ASSAULT_DIGITS\n363, O_FINAL_LEVEL\n364, O_CUT_SHOTGUN\n365, O_EARTHQUAKE\n366, O_GUN_SHELL\n367, O_SHOTGUN_SHELL\n368, O_CLAW_MUTANT_PLASMA_BALL\n369, O_EXTRA_FX_2\n370, O_DISPOSABLE_ANIMATING_1\n371, O_SOPHIA_LASER_BOLT\n372, O_SOPHIA_PLASMA_BALL\n373, O_FUSE_BOX\n374, O_EXTRA_FX_7\n375, O_EXTRA_FX_8\n376, O_ALPHABET_SMALL\n377, O_SNOWFLAKE\n378, O_LARA_MAGNUMS\n379, O_MAGNUM_OPTION\n380, O_MAGNUM_AMMO_OPTION\n381, O_MAGNUM_ITEM\n382, O_MAGNUM_AMMO_ITEM\n383, O_LARA_AUTOS\n384, O_AUTOS_OPTION\n385, O_AUTOS_AMMO_OPTION\n386, O_AUTOS_ITEM\n387, O_AUTOS_AMMO_ITEM\n388, O_LARA_M16\n389, O_M16_OPTION\n390, O_M16_AMMO_OPTION\n391, O_M16_ITEM\n392, O_M16_AMMO_ITEM\n393, O_LARA_SKIN_SWAP_1\n394, O_LARA_SKIN_SWAP_2\n395, O_LARA_SKIN_SWAP_3\n396, O_LARA_SKIN_SWAP_4\n397, O_LARA_SKIN_SWAP_5\n398, O_LARA_SKIN_SWAP_6\n399, O_LARA_SKIN_SWAP_7\n400, O_LARA_SKIN_SWAP_8\n401, O_LARA_SKIN_SWAP_9\n402, O_LARA_SKIN_SWAP_10\n403, O_LARA_SKIN_SWAP_11\n404, O_LARA_SKIN_SWAP_12\n405, O_LARA_SKIN_SWAP_13\n406, O_LARA_SKIN_SWAP_14\n407, O_LARA_SKIN_SWAP_15\n408, O_LARA_SKIN_SWAP_16\n409, O_LARA_SKIN_SWAP_17\n410, O_LARA_SKIN_SWAP_18\n411, O_LARA_SKIN_SWAP_19\n412, O_LARA_SKIN_SWAP_20\n413, O_LARA_SKIN_SWAP_21\n414, O_LARA_SKIN_SWAP_22\n415, O_LARA_SKIN_SWAP_23\n416, O_LARA_SKIN_SWAP_24\n417, O_LARA_SKIN_SWAP_25\n418, O_LARA_SKIN_SWAP_26\n419, O_LARA_SKIN_SWAP_27\n420, O_LARA_SKIN_SWAP_28\n421, O_LARA_SKIN_SWAP_29\n422, O_LARA_SKIN_SWAP_30\n423, O_LARA_SKIN_SWAP_31\n424, O_LARA_SKIN_SWAP_32\n425, O_LARA_SKIN_SWAP_EXTRA\n426, O_LARA_SKIN_SWAP_GUNS\n427, O_LARA_SKIN_SWAP_LEGS\n428, O_PICKUP_AID\n"
  },
  {
    "path": "data/trx/ship/games/tr3/catalog_samples.csv",
    "content": "0,   SFX_LARA_FOOTSTEP\n2,   SFX_LARA_NO\n6,   SFX_LARA_DRAW\n7,   SFX_LARA_HOLSTER\n8,   SFX_LARA_PISTOLS\n9,   SFX_LARA_RELOAD\n10,  SFX_LARA_RICOCHET\n11,  SFX_LARA_FLARE_IGNITE\n12,  SFX_LARA_FLARE_BURN\n23,  SFX_UPV_HARPOON\n27,  SFX_LARA_BODYSL\n30,  SFX_LARA_FALL\n31,  SFX_LARA_INJURY\n33,  SFX_LARA_SPLASH\n34,  SFX_LARA_GET_OUT\n36,  SFX_LARA_BREATH\n37,  SFX_LARA_BUBBLES\n39,  SFX_LARA_KEY\n41,  SFX_LARA_GENERAL_DEATH\n43,  SFX_LARA_UZI_FIRE\n44,  SFX_LARA_UZI_STOP\n45,  SFX_LARA_SHOTGUN\n48,  SFX_CLICK\n49,  SFX_LARA_SHOTGUN_SHELL\n50,  SFX_LARA_BULLETHIT\n53,  SFX_LARA_FALL_DEATH\n56,  SFX_LARA_FLESH_WOUND\n60,  SFX_UNDERWATER\n69,  SFX_ICICLE\n70,  SFX_LARA_THUD\n70,  SFX_PUSHBLOCK_LAND\n72,  SFX_LONDON_SWAT_FIRE\n76,  SFX_BLAST_CIRCLE\n77,  SFX_ROCKET_FIRE\n78,  SFX_MP5_FIRE\n79,  SFX_WATERFALL_LOOP\n105, SFX_EXPLOSION_1\n106, SFX_EXPLOSION_2\n107, SFX_EARTHQUAKE_LOOP\n108, SFX_MENU_ROTATE\n109, SFX_MENU_CHOOSE\n109, SFX_MENU_LARA_HOME\n111, SFX_MENU_SPININ\n112, SFX_MENU_SPINOUT\n113, SFX_MENU_STOPWATCH\n114, SFX_MENU_GUNS\n115, SFX_MENU_PASSPORT\n116, SFX_MENU_MEDI\n119, SFX_TARGET_HITS\n120, SFX_TARGET_SMASH\n121, SFX_LARA_DESERT_EAGLE\n131, SFX_CLEANER_FUSEBOX\n137, SFX_AMERICAN_SWAT_FIRE\n147, SFX_SPIKE_WALL\n147, SFX_ROLLING_BALL_1_ROLL\n147, SFX_ROLLING_BALL_4_ROLL\n148, SFX_TRAIN_LOOP\n149, SFX_LOWERING_BLOCK\n150, SFX_LOOP_FOR_SMALL_FIRES\n153, SFX_QUAD_IDLE\n155, SFX_QUAD_MOVE\n157, SFX_BATS_1\n163, SFX_FLOOD\n191, SFX_CLEANER_LOOP\n195, SFX_RIB_IDLE\n197, SFX_RIB_MOVING\n202, SFX_QUAD_FRONT_IMPACT\n204, SFX_FLAME_THROWER_LOOP\n208, SFX_ALARM_1\n209, SFX_MINE_CART_TRACK_LOOP\n210, SFX_MINE_CART_PULLY_LOOP\n211, SFX_MINE_CART_CLUNK_START\n212, SFX_SAVE_CRYSTAL\n214, SFX_SHUTTERS_BREAK\n215, SFX_UNDERWATER_FAN_ON\n216, SFX_UNDERWATER_FAN_OFF\n219, SFX_MINE_CART_SREECH_BRAKE\n220, SFX_SPANNER_CLUNK\n247, SFX_BLOWPIPE_BLOW\n257, SFX_HUGE_ROCKET_LOOP\n258, SFX_SHIVA_SWORD_1\n259, SFX_SHIVA_SWORD_2\n280, SFX_ZIPLINE_GO\n281, SFX_ZIPLINE_STOP\n288, SFX_FOOTSTEPS_MUD\n289, SFX_FOOTSTEPS_ICE\n290, SFX_FOOTSTEPS_GRAVEL\n291, SFX_FOOTSTEPS_SAND_OR_GRASS\n292, SFX_FOOTSTEPS_WOOD\n293, SFX_FOOTSTEPS_SNOW\n294, SFX_FOOTSTEPS_METAL\n299, SFX_ENGLISH_HOY\n300, SFX_AMERICAN_HOY\n305, SFX_SECURITY_GUARD_FIRE\n318, SFX_MACAQUE_ROLL\n334, SFX_DOORBELL\n335, SFX_BURGLAR_ALARM\n336, SFX_BOAT_ENGINE\n346, SFX_UPV_LOOP\n347, SFX_UPV_START\n348, SFX_UPV_STOP\n352, SFX_SOPHIA_SUMMON\n353, SFX_SOPHIA_TAKE_HIT\n354, SFX_SOPHIA_SUMMON_NOT\n361, SFX_TRIBOSS_TAKE_HIT\n362, SFX_TRIBOSS_TURN_CHAIR\n370, SFX_LARA_MAGNUMS\n371, SFX_LARA_AUTOS\n372, SFX_M16_FIRE\n373, SFX_M16_STOP\n374, SFX_LARA_BAREFOOT\n"
  },
  {
    "path": "data/trx/ship/games/tr3/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 3,\n    \"name\": \"Tomb Raider III\",\n\n    \"main_menu_picture\": \"title_eu.webp\",\n    \"savegame_file_fmt\": \"save_tr3_%02d.dat\",\n\n    \"enable_tr2_item_drops\": true,\n    \"convert_dropped_guns\": true,\n\n    \"title\": {\n        \"path\": \"title.tr2\",\n        \"music_track\": 5,\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"legal_eu.webp\", \"legal\": true},\n            {\"type\": \"play_fmv\", \"fmv_id\": 0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 1},\n            {\"type\": \"exit_to_title\"},\n        ],\n    },\n\n    \"ambient_tracks\": [\n        26, 27, 28, 29, 30, 31, 32, 33, 34,\n        35, 36, 73, 74, 75, 76, 77, 78,\n    ],\n\n    \"sfx_path\": \"main.sfx\",\n    \"injections\": [\n        \"font.bin\",\n        \"lara_animations.bin\",\n        \"pda_model.bin\",\n        \"lara_extra.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n        \"pickup_aid.bin\",\n    ],\n\n    \"globe_select_entries\": [\n        {\"rot\": [-1536, -7936, 1536], \"start_level_ordinal\": 1,  \"completion_level_ordinal\": 4,  \"prereq_zones\": [], \"mesh_idx\": 2},\n        {\"rot\": [1024, -512, -256],   \"start_level_ordinal\": 5,  \"completion_level_ordinal\": 8,  \"prereq_zones\": [0], \"mesh_idx\": 5},\n        {\"rot\": [2560, 21248, -4096], \"start_level_ordinal\": 13, \"completion_level_ordinal\": 15, \"prereq_zones\": [0], \"mesh_idx\": 4},\n        {\"rot\": [-3328, 29440, 1024], \"start_level_ordinal\": -1, \"completion_level_ordinal\": -1, \"prereq_zones\": [], \"mesh_idx\": 3},\n        {\"rot\": [3072, -20992, 6400], \"start_level_ordinal\": 9,  \"completion_level_ordinal\": 12, \"prereq_zones\": [0], \"mesh_idx\": 1},\n        {\"rot\": [-5120, -15360, -18688], \"start_level_ordinal\": 16, \"completion_level_ordinal\": 19, \"prereq_zones\": [0, 1, 2, 4], \"mesh_idx\": 6},\n    ],\n\n    \"levels\": [\n        // 0. Lara's Home\n        {\n            \"type\": \"gym\",\n            \"path\": \"house.tr2\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr3_gym\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"house.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n            ],\n            \"injections\": [\n                \"gym_sky.bin\",\n                \"lara_gym_guns.bin\",\n            ],\n        },\n\n        // 1. Jungle\n        {\n            \"path\": \"jungle.tr2\",\n            \"script\": \"jungle.lua\",\n            \"music_track\": 34,\n            \"lara_outfit\": \"tr3_classic\",\n            \"weather_type\": \"rain\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"india.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"small_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"large_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"flare\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 0},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"india_sky.bin\",\n                \"lara_guns.bin\",\n            ],\n        },\n\n        // 2. Temple Ruins\n        {\n            \"path\": \"temple.tr2\",\n            \"music_track\": 34,\n            \"lara_outfit\": \"tr3_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"india.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 1},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"india_sky.bin\",\n                \"lara_guns.bin\",\n            ],\n        },\n\n        // 3. The River Ganges\n        {\n            \"path\": \"quadchas.tr2\",\n            \"music_track\": 34,\n            \"lara_outfit\": \"tr3_classic\",\n            \"weather_type\": \"rain\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"india.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"india_sky.bin\",\n                \"lara_guns.bin\",\n                \"ganges_door131_frames.bin\",\n            ],\n        },\n\n        // 4. Caves of Kaliya\n        {\n            \"path\": \"tonyboss.tr2\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr3_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"india.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_fmv\", \"fmv_id\": 2},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"globe_select\", \"image\": \"india.webp\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"globe_model.bin\",\n            ],\n        },\n\n        // 5. Coastal Village\n        {\n            \"path\": \"shore.tr2\",\n            \"music_track\": 32,\n            \"lara_outfit\": \"tr3_south_pacific\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"southpac.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 2},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"coastal_airlock.bin\",\n                \"coastal_sky.bin\",\n                \"coastal_animating_bounds.bin\",\n                \"lara_guns.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // 6. Crash Site\n        {\n            \"path\": \"crash.tr2\",\n            \"script\": \"crash.lua\",\n            \"music_track\": 33,\n            \"lara_outfit\": \"tr3_south_pacific\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"southpac.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"pickup_1\", \"quantity\": 1},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 3},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"crash_sky.bin\",\n                \"crash_pickup_meshes.bin\",\n                \"lara_guns.bin\",\n            ],\n        },\n\n        // 7. Madubu Gorge\n        {\n            \"path\": \"rapids.tr2\",\n            \"music_track\": 36,\n            \"death_tile\": \"rapids\",\n            \"water_particles\": true,\n            \"lara_outfit\": \"tr3_south_pacific\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"southpac.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"rapids_sky.bin\",\n            ],\n        },\n\n        // 8. Temple of Puna\n        {\n            \"path\": \"triboss.tr2\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr3_south_pacific\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"southpac.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"globe_select\", \"image\": \"southpac.webp\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"puna_pickup_meshes.bin\",\n                \"globe_model.bin\",\n            ],\n        },\n\n        // 9. Thames Wharf\n        {\n            \"path\": \"roofs.tr2\",\n            \"music_track\": 73,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"weather_type\": \"rain\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"london.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"disable_floor\", \"height\": 1792},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 4},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"menu_artefacts.bin\",\n            ],\n        },\n\n        // 10. Aldwych\n        {\n            \"path\": \"sewer.tr2\",\n            \"music_track\": 74,\n            \"water_particles\": true,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"london.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 5},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"aldwych_fd.bin\",\n                \"aldwych_pickup_meshes.bin\",\n                \"aldwych_textures.bin\",\n                \"menu_artefacts.bin\",\n            ],\n        },\n\n        // 11. Lud's Gate\n        {\n            \"path\": \"tower.tr2\",\n            \"script\": \"tower.lua\",\n            \"music_track\": 31,\n            \"water_particles\": true,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"london.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 6},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"luds_diver_animation.bin\",\n                \"luds_textures.bin\",\n                \"menu_artefacts.bin\",\n            ],\n        },\n\n        // 12. City\n        {\n            \"path\": \"office.tr2\",\n            \"music_track\": 78,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"weather_type\": \"rain\",\n            \"death_tile\": \"electric\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"london.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"disable_floor\", \"height\": 5120},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"globe_select\", \"image\": \"london.webp\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"globe_model.bin\",\n                \"city_textures.bin\",\n                \"menu_artefacts.bin\",\n            ],\n        },\n\n        // 13. Nevada Desert\n        {\n            \"path\": \"nevada.tr2\",\n            \"music_track\": 33,\n            \"death_tile\": \"electric\",\n            \"lara_outfit\": \"tr3_nevada\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"nevada.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 7},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"nevada_sky.bin\",\n                \"nevada_door132_frames.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // 14. High Security Compound\n        {\n            \"path\": \"compound.tr2\",\n            \"script\": \"compound.lua\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr3_nevada\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"nevada.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"set_lara_start_anim\", \"anim\": 20},\n                {\"type\": \"remove_weapons\"},\n                {\"type\": \"remove_ammo\"},\n                {\"type\": \"remove_medipacks\"},\n                {\"type\": \"remove_flares\"},\n                {\"type\": \"give_item\", \"object_id\": \"small_medipack\", \"quantity\": 1},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 8},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"area51_sky.bin\",\n                \"lara_guns.bin\",\n                \"compound_cine.bin\",\n                \"compound_animating_bounds.bin\",\n                \"compound_textures.bin\",\n            ],\n            \"unobtainable_kills\": 9,\n            \"unobtainable_ally_kills\": 8,\n        },\n\n        // 15. Area 51\n        {\n            \"path\": \"area51.tr2\",\n            \"script\": \"area51.lua\",\n            \"music_track\": 27,\n            \"death_tile\": \"electric\",\n            \"lara_outfit\": \"tr3_nevada\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"nevada.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"pistols\", \"quantity\": 1},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"globe_select\", \"image\": \"nevada.webp\"},\n            ],\n            \"injections\": [\n                \"area51_sky.bin\",\n                \"lara_guns.bin\",\n                \"globe_model.bin\",\n                \"area51_textures.bin\",\n            ],\n        },\n\n        // 16. Antarctica\n        {\n            \"path\": \"antarc.tr2\",\n            \"music_track\": 28,\n            \"lara_outfit\": \"tr3_antarctica\",\n            \"cold_water\": true,\n            \"weather_type\": \"snow\",\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_id\": 3},\n                {\"type\": \"loading_screen\", \"path\": \"antarc.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 9},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"antarc_airlock.bin\",\n                \"antarc_sky.bin\",\n                \"lara_guns.bin\",\n                \"antarc_door134_frames.bin\",\n                \"menu_artefacts.bin\",\n            ],\n        },\n\n        // 17. RX-Tech Mines\n        {\n            \"path\": \"mines.tr2\",\n            \"script\": \"mines.lua\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr3_antarctica\",\n            \"cold_water\": true,\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"antarc.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"remove_scions\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"drill_collision.bin\",\n                \"mines_textures.bin\",\n                \"flamethrower_sfx.bin\",\n            ],\n        },\n\n        // 18. Lost City of Tinnos\n        {\n            \"path\": \"city.tr2\",\n            \"music_track\": 26,\n            \"lara_outfit\": \"tr3_antarctica\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"antarc.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 10},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"tinnos_cameras.bin\",\n                \"tinnos_flames.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n        },\n\n        // 19. Meteorite Cavern\n        {\n            \"path\": \"chamber.tr2\",\n            \"music_track\": 26,\n            \"lara_outfit\": \"tr3_antarctica\",\n            \"weather_type\": \"snow\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"antarc.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_fmv\", \"fmv_id\": 4},\n                {\"type\": \"play_music\", \"music_track\": 121},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"theend2.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit01.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit02.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit03.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit04.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit05.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit06.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit07.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit08.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit09.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"total_stats\", \"background_path\": \"theend2.webp\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"cavern_sky.bin\",\n                \"lara_guns.bin\",\n                \"cavern_pickup_meshes.bin\",\n                \"cavern_door131_frames.bin\",\n                \"flamethrower_sfx.bin\",\n                \"menu_artefacts.bin\",\n            ],\n        },\n\n        // 20. All Hallows\n        {\n            \"type\": \"bonus\",\n            \"path\": \"stpaul.tr2\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"weather_type\": \"rain\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"london.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"disable_floor\", \"height\": 10000},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"stpaul_animating_bounds.bin\",\n                \"stpaul_textures.bin\",\n                \"menu_artefacts.bin\",\n            ],\n            \"unobtainable_pickups\": 9,\n        },\n    ],\n\n    \"cutscenes\": [\n        // Cutscene 1\n        {\n            \"path\": \"cut6.tr2\",\n            \"music_track\": 64,\n            \"lara_outfit\": \"tr3_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut6_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 2\n        {\n            \"path\": \"cut9.tr2\",\n            \"music_track\": 69,\n            \"lara_outfit\": \"tr3_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut9_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"india_sky.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 3\n        {\n            \"path\": \"cut1.tr2\",\n            \"music_track\": 68,\n            \"lara_outfit\": \"tr3_south_pacific\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut1_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"crash_sky.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 4\n        {\n            \"path\": \"cut4.tr2\",\n            \"music_track\": 65,\n            \"lara_outfit\": \"tr3_south_pacific\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut4_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 5\n        {\n            \"path\": \"cut2.tr2\",\n            \"music_track\": 67,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"weather_type\": \"rain\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut2_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"london_sky.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 6\n        {\n            \"path\": \"cut5.tr2\",\n            \"music_track\": 63,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut5_setup.bin\",\n                \"cut5_textures.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 7\n        {\n            \"path\": \"cut11.tr2\",\n            \"music_track\": 71,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut11_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"london_sky.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 8\n        {\n            \"path\": \"cut7.tr2\",\n            \"music_track\": 72,\n            \"lara_outfit\": \"tr3_nevada\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut7_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"nevada_sky.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 9\n        {\n            \"path\": \"cut8.tr2\",\n            \"script\": \"cut8.lua\",\n            \"music_track\": 70,\n            \"lara_outfit\": \"tr3_nevada\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut8_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 10\n        {\n            \"path\": \"cut3.tr2\",\n            \"music_track\": 62,\n            \"lara_outfit\": \"tr3_antarctica\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"antarc_sky.bin\",\n                \"cut3_setup.bin\",\n                \"cut3_shell.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n        // Cutscene 11\n        {\n            \"path\": \"cut12.tr2\",\n            \"music_track\": 66,\n            \"lara_outfit\": \"tr3_antarctica\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n            \"inherit_injections\": false,\n            \"injections\": [\n                \"cut12_setup.bin\",\n                \"misc_sprites.bin\",\n                \"font.bin\",\n                \"lara_outfits.bin\",\n            ],\n        },\n    ],\n\n    \"fmvs\": [\n        {\"path\": \"logo.rpl\", \"legal\": true},\n        {\"path\": \"intr_eng.rpl\"},\n        {\"path\": \"sail_eng.rpl\"},\n        {\"path\": \"crsh_eng.rpl\"},\n        {\"path\": \"endgame.rpl\"},\n    ],\n\n    \"hidden_config\": [\n        \"enable_3d_pickups\", // TR3 has no sprite pickups\n        \"enable_item_examining\",\n        \"disable_trex_collision\",\n        \"fix_bear_ai\",\n        \"enable_save_crystals\",\n        \"enable_ps1_crystals\",\n        \"fix_chainblock_secret_sound\",\n        \"enable_compass_stats\",\n        \"enable_crawling\",\n        \"enable_wading\",\n        \"restore_ps1_enemies\",\n        \"change_pierre_spawn\",\n        \"fix_speeches_killing_music\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3/inv_ring.json5",
    "content": "[\n    {\n        \"object_id\": \"O_SMALL_MEDIPACK_OPTION\",\n        \"frames_total\": 26,\n        \"open_frame\": 25,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4032,\n        \"x_rot_sel\": -7296,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 216,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 14,\n    },\n\n    {\n        \"object_id\": \"O_LARGE_MEDIPACK_OPTION\",\n        \"frames_total\": 20,\n        \"open_frame\": 19,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3616,\n        \"x_rot_sel\": -8160,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 13,\n    },\n\n    {\n        \"object_id\": \"O_FLAREBOX_OPTION\",\n        \"frames_total\": 31,\n        \"open_frame\": 30,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"y_rot_sel\": -8192,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 12,\n    },\n\n    {\n        \"object_id\": \"O_PISTOL_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 2848,\n        \"y_rot_sel\": -32768,\n        \"y_trans_sel\": 38,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 1,\n    },\n\n    {\n        \"object_id\": \"O_SHOTGUN_OPTION\",\n        \"frames_total\": 13,\n        \"open_frame\": 12,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 5120,\n        \"y_rot_sel\": 30720,\n        \"z_trans_sel\": 228,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 2,\n    },\n\n    {\n        \"object_id\": \"O_MAGNUM_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 3,\n    },\n\n    {\n        \"object_id\": \"O_AUTOS_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 4,\n    },\n\n    {\n        \"object_id\": \"O_DESERT_EAGLE_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 3360,\n        \"y_rot_sel\": -32768,\n        \"z_trans_sel\": 362,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 5,\n    },\n\n    {\n        \"object_id\": \"O_UZI_OPTION\",\n        \"frames_total\": 13,\n        \"open_frame\": 12,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": 2336,\n        \"y_rot_sel\": -32768,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 322,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 6,\n    },\n\n    {\n        \"object_id\": \"O_HARPOON_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -736,\n        \"y_rot_sel\": -19456,\n        \"y_trans_sel\": 58,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 11,\n    },\n\n    {\n        \"object_id\": \"O_M16_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": -18432,\n        \"y_trans_sel\": 84,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 7,\n    },\n\n    {\n        \"object_id\": \"O_MP5_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": -18432,\n        \"y_trans_sel\": 84,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 8,\n    },\n\n    {\n        \"object_id\": \"O_ROCKET_GUN_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": 14336,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 9,\n    },\n\n    {\n        \"object_id\": \"O_GRENADE_GUN_OPTION\",\n        \"frames_total\": 12,\n        \"open_frame\": 11,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -224,\n        \"y_rot_sel\": 14336,\n        \"y_trans_sel\": 56,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_PISTOL_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 1,\n    },\n\n    {\n        \"object_id\": \"O_SHOTGUN_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 2,\n    },\n\n    {\n        \"object_id\": \"O_MAGNUM_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 3,\n    },\n\n    {\n        \"object_id\": \"O_AUTOS_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 4,\n    },\n\n    {\n        \"object_id\": \"O_DESERT_EAGLE_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 5,\n    },\n\n    {\n        \"object_id\": \"O_UZI_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 6,\n    },\n\n    {\n        \"object_id\": \"O_HARPOON_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 11,\n    },\n\n    {\n        \"object_id\": \"O_M16_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 7,\n    },\n\n    {\n        \"object_id\": \"O_MP5_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 8,\n    },\n\n    {\n        \"object_id\": \"O_ROCKET_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 9,\n    },\n\n    {\n        \"object_id\": \"O_GRENADE_AMMO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"x_rot_sel\": -3808,\n        \"z_trans_sel\": 296,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 10,\n    },\n\n    {\n        \"object_id\": \"O_SCION_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 109,\n    },\n\n    {\n        \"object_id\": \"O_LEADBAR_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3616,\n        \"x_rot_sel\": -8160,\n        \"y_rot_sel\": -4096,\n        \"z_trans_sel\": 352,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 100,\n    },\n\n    {\n        \"object_id\": \"O_PICKUP_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 111,\n    },\n\n    {\n        \"object_id\": \"O_PICKUP_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 110,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 108,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 107,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 106,\n    },\n\n    {\n        \"object_id\": \"O_PUZZLE_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 105,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 101,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 102,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 103,\n    },\n\n    {\n        \"object_id\": \"O_KEY_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 104,\n    },\n\n    {\n        \"object_id\": \"O_STOPWATCH_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 3200,\n        \"y_trans_sel\": -135,\n        \"z_trans_sel\": 320,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n    },\n\n    {\n        \"object_id\": \"O_COMPASS_OPTION\",\n        \"frames_total\": 25,\n        \"open_frame\": 10,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4352,\n        \"x_rot_sel\": -8192,\n        \"z_trans_sel\": 456,\n        \"meshes_sel\": 0b00000101,\n        \"meshes_drawn\": 0b00000101,\n    },\n\n    {\n        \"object_id\": \"O_GLOBE_SELECT_OPTION\",\n        \"frames_total\": 160,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"y_trans_sel\": -176,\n        \"z_trans_sel\": 256,\n        \"which_meshes\": 0xFFFFFFF7,\n        \"drawn_meshes\": 0xFFFFFFF7,\n        \"inv_pos\": 300,\n    },\n\n    {\n        \"object_id\": \"O_PASSPORT_OPTION\",\n        \"frames_total\": 30,\n        \"open_frame\": 14,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"x_rot_sel\": -4320,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": 0b00010011,\n        \"meshes_drawn\": 0b00010011,\n        \"inv_pos\": 200,\n    },\n\n    {\n        \"object_id\": \"O_DETAIL_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4224,\n        \"x_rot_sel\": -7232,\n        \"y_trans_sel\": 16,\n        \"z_trans_sel\": 444,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 201,\n    },\n\n    {\n        \"object_id\": \"O_SOUND_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4832,\n        \"x_rot_sel\": -5408,\n        \"y_rot_sel\": -3072,\n        \"y_trans_sel\": -2,\n        \"z_trans_sel\": 350,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 202,\n    },\n\n    {\n        \"object_id\": \"O_CONTROL_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 5504,\n        \"x_rot_sel\": -2560,\n        \"x_rot_nosel\": 5632,\n        \"y_rot_sel\": 13312,\n        \"y_trans_sel\": 46,\n        \"z_trans_sel\": 508,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 203,\n    },\n\n    {\n        \"object_id\": \"O_PHOTO_OPTION\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"x_rot_sel\": -4320,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 205,\n    },\n\n    {\n        \"object_id\": \"O_PDA_OPTION\",\n        \"frames_total\": 39,\n        \"open_frame\": 19,\n        \"anim_direction\": 1,\n        \"x_rot_pt_sel\": 4640,\n        \"z_trans_sel\": 384,\n        \"meshes_sel\": 0b00000011,\n        \"meshes_drawn\": 0b00000011,\n        \"inv_pos\": 204,\n    },\n\n    {\n        \"object_id\": \"O_QUEST_OPTION_1\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 112,\n    },\n\n    {\n        \"object_id\": \"O_QUEST_OPTION_2\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 113,\n    },\n\n    {\n        \"object_id\": \"O_QUEST_OPTION_3\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 114,\n    },\n\n    {\n        \"object_id\": \"O_QUEST_OPTION_4\",\n        \"frames_total\": 1,\n        \"anim_direction\": 1,\n        \"anim_speed\": 1,\n        \"x_rot_pt_sel\": 7200,\n        \"x_rot_sel\": -4352,\n        \"z_trans_sel\": 256,\n        \"meshes_sel\": -1,\n        \"meshes_drawn\": -1,\n        \"inv_pos\": 115,\n    },\n]\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/area51.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.prisoner)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/compound.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.prisoner)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/crash.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.sthpac_mercenary)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/cut8.lua",
    "content": "local drink_frame_num = 737\nlocal equipment_set = false\n\ntrx.events.after_control(function()\n  local lara_item = trx.lara.item\n  if lara_item.frame >= drink_frame_num and not equipment_set then\n    trx.lara.set_extra_equipment(trx.lara.mesh.hand_r, trx.lara.extra_mesh.drink_can)\n    equipment_set = true\n  end\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/jungle.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.monkey)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/mines.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.rx_worker_3)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/tower.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.punk_1)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/scripts/zoo.lua",
    "content": "trx.events.before_level_file(function(level)\n  trx.creatures.add_ally(trx.catalog.objects.monkey)\nend)\n"
  },
  {
    "path": "data/trx/ship/games/tr3/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"globe_select\": {\n            \"area_1\": \"\\\\{review}Indien\",\n            \"area_2\": \"\\\\{review}Südseeinseln\",\n            \"area_3\": \"\\\\{review}Nevada-Wüste\",\n            \"area_4\": \"\\\\{review}\",\n            \"area_5\": \"\\\\{review}London\",\n            \"area_6\": \"\\\\{review}Antarktis\",\n        },\n        \"inventory_ring\": {\n            \"heading_fmt\": \"\\\\{color 5}%s\",\n            \"item_count_fmt\": \"\\\\{color 3}%s\",\n            \"object_name_fmt\": \"\\\\{color 5}%s\",\n        },\n        \"misc\": {\n            \"empty_slot_fmt\": \"- Leerer Slot -\",\n        },\n        \"overlay\": {\n            \"item_count_fmt_pc\": \"%s\",\n            \"item_count_fmt_ps1\": \"\\\\{color 3}%s\",\n        },\n        \"pause\": {\n            \"paused\": \"\\\\{color 5}Pause\",\n        },\n        \"stats\": {\n            \"assault_other_times_fmt\": \"\\\\{review}\\\\{color 2}%s\",\n            \"gym_assault_course\": \"\\\\{review}\\\\{color 5}Angriffskurs\",\n            \"gym_racetrack_course\": \"\\\\{review}\\\\{color 5}Rennstrecke\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Großes Medi-Pack\",\n        },\n        \"quest_1\": {\n            \"description\": \"\",\n            \"name\": \"\\\\{review}Infada-Stein\",\n        },\n        \"quest_2\": {\n            \"description\": \"\",\n            \"name\": \"\\\\{review}Element 115\",\n        },\n        \"quest_3\": {\n            \"description\": \"\",\n            \"name\": \"\\\\{review}Auge des Isis\",\n        },\n        \"quest_4\": {\n            \"description\": \"\",\n            \"name\": \"\\\\{review}Ora-Dolch\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kleines Medi-Pack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Zwischensequenz 1\",\n        },\n        {\n            \"title\": \"Zwischensequenz 2\",\n        },\n        {\n            \"title\": \"Zwischensequenz 3\",\n        },\n        {\n            \"title\": \"Zwischensequenz 4\",\n        },\n        {\n            \"title\": \"Zwischensequenz 5\",\n        },\n        {\n            \"title\": \"Zwischensequenz 6\",\n        },\n        {\n            \"title\": \"Zwischensequenz 7\",\n        },\n        {\n            \"title\": \"Zwischensequenz 8\",\n        },\n        {\n            \"title\": \"Zwischensequenz 9\",\n        },\n        {\n            \"title\": \"Zwischensequenz 10\",\n        },\n        {\n            \"title\": \"Zwischensequenz 11\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Laras Anwesen\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zur Rennstrecke\",\n                }\n            }\n        },\n        {\n            \"title\": \"Dschungel\",\n            \"objects\": {\n                \"key_4\": {\n                    \"name\": \"Indra-Schlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Templeruine\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel des Ganesha\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Säbel\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Säbel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Der Ganges\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zum Tor\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kaliya Höhlen\",\n        },\n        {\n            \"title\": \"Küstendorf\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel des Schmugglers\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Schlangenstein\",\n                }\n            }\n        },\n        {\n            \"title\": \"Absturzstelle\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Commander Bishop's Schlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Lt. Tuckerman's Schlüssel\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Sumpfkarte\",\n                    \"description\": \"\",\n                }\n            }\n        },\n        {\n            \"title\": \"Madubu Schlucht\",\n        },\n        {\n            \"title\": \"Punatempel\",\n        },\n        {\n            \"title\": \"Kai an der Themse\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zum Belüftungsraum\",\n                },\n                \"key_2\": {\n                    \"name\": \"Schlüssel zur Kathedrale\",\n                }\n            }\n        },\n        {\n            \"title\": \"Aldwych\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Wartungsraumschlüssel\",\n                },\n                \"key_2\": {\n                    \"name\": \"Solomon's Schlüssel\",\n                },\n                \"key_3\": {\n                    \"name\": \"Solomon's Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Alter Penny\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Ticket\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Steinmetzhammer\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Verzierter Stern\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lud's Gate\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zum Heizraum\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Balsamierungs Öl\",\n                }\n            }\n        },\n        {\n            \"title\": \"Innenstadt\",\n        },\n        {\n            \"title\": \"Wüste von Nevada\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Zugangskarte zum Generator\",\n                },\n                \"key_2\": {\n                    \"name\": \"Zündschlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"Hochsicherheitstrakt\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Codekarte Typ A\",\n                },\n                \"key_2\": {\n                    \"name\": \"Codekarte Typ B\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Blauer Sicherheitsstecker\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Gelber Sicherheitsstecker\",\n                }\n            }\n        },\n        {\n            \"title\": \"Area 51\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Abschusscode\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Zugangsstecker Turm\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"CD zur Codefreigabe\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"CD zur Codefreigabe\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Zugangsstecker Hangar\",\n                }\n            }\n        },\n        {\n            \"title\": \"Antarktis\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zur Hütte\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Brechstange\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Torschlüssel\",\n                }\n            }\n        },\n        {\n            \"title\": \"RX-Techs Bergwerk\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Brechstange\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Bleisäurebattarie\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Anlasser für die Winch\",\n                }\n            }\n        },\n        {\n            \"title\": \"Die vergessene Stadt Tinnos\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Uli-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Ozeanische Maske\",\n                }\n            }\n        },\n        {\n            \"title\": \"Höhle des Meteoriten\",\n        },\n        {\n            \"title\": \"All Hallows\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zum Gewölbe\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"globe_select\": {\n            \"area_1\": \"India\",\n            \"area_2\": \"Isole del Sud Pacifico\",\n            \"area_3\": \"Nevada\",\n            \"area_4\": \" \",\n            \"area_5\": \"Londra\",\n            \"area_6\": \"Antartide\",\n        },\n        \"inventory_ring\": {\n            \"heading_fmt\": \"\\\\{color 5}%s\",\n            \"item_count_fmt\": \"\\\\{color 3}%s\",\n            \"object_name_fmt\": \"\\\\{color 5}%s\",\n        },\n        \"misc\": {\n            \"empty_slot_fmt\": \"- Slot Vuoto -\",\n        },\n        \"overlay\": {\n            \"item_count_fmt_pc\": \"%s\",\n            \"item_count_fmt_ps1\": \"\\\\{color 3}%s\",\n        },\n        \"pause\": {\n            \"paused\": \"\\\\{color 5}Pausa\",\n        },\n        \"stats\": {\n            \"assault_other_times_fmt\": \"\\\\{color 2}%s\",\n            \"gym_assault_course\": \"\\\\{color 5}Corso d'Addestramento\",\n            \"gym_racetrack_course\": \"\\\\{color 5}Corso di Guida\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Kit Medico Grande\",\n        },\n        \"quest_1\": {\n            \"description\": \"\",\n            \"name\": \"Pietra Infada\",\n        },\n        \"quest_2\": {\n            \"description\": \"\",\n            \"name\": \"Elemento 115\",\n        },\n        \"quest_3\": {\n            \"description\": \"\",\n            \"name\": \"Occhio di Iside\",\n        },\n        \"quest_4\": {\n            \"description\": \"\",\n            \"name\": \"Pugnale Ora\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Kit Medico Piccolo\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Intermezzo 1\",\n        },\n        {\n            \"title\": \"Intermezzo 2\",\n        },\n        {\n            \"title\": \"Intermezzo 3\",\n        },\n        {\n            \"title\": \"Intermezzo 4\",\n        },\n        {\n            \"title\": \"Intermezzo 5\",\n        },\n        {\n            \"title\": \"Intermezzo 6\",\n        },\n        {\n            \"title\": \"Intermezzo 7\",\n        },\n        {\n            \"title\": \"Intermezzo 8\",\n        },\n        {\n            \"title\": \"Intermezzo 9\",\n        },\n        {\n            \"title\": \"Intermezzo 10\",\n        },\n        {\n            \"title\": \"Intermezzo 11\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Casa di Lara\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Circuito\",\n                }\n            }\n        },\n        {\n            \"title\": \"Giungla\",\n            \"objects\": {\n                \"key_4\": {\n                    \"name\": \"Chiave di Indra\",\n                }\n            }\n        },\n        {\n            \"title\": \"Rovine del Tempio\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave di Ganesha\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Scimitarra\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scimitarra\",\n                }\n            }\n        },\n        {\n            \"title\": \"Il Fiume Gange\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Cancello\",\n                }\n            }\n        },\n        {\n            \"title\": \"Caverne di Kaliya\",\n        },\n        {\n            \"title\": \"Villaggio Costiero\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Contrabbandiere\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Pietra del Serpente\",\n                }\n            }\n        },\n        {\n            \"title\": \"Luogo dello Schianto\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Comandante Bishop\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave del Tenente Tuckerman\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Mappa della Palude\",\n                    \"description\": \"\",\n                }\n            }\n        },\n        {\n            \"title\": \"Gola di Madubu\",\n        },\n        {\n            \"title\": \"Tempio di Puna\",\n        },\n        {\n            \"title\": \"Molo sul Tamigi\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Canna Fumaria\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave della Cattedrale\",\n                }\n            }\n        },\n        {\n            \"title\": \"Aldwych\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave di Manutenzione\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave di Salomone\",\n                },\n                \"key_3\": {\n                    \"name\": \"Chiave di Salomone\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Vecchio Penny\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Biglietto\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Martello Massonico\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Stella Ornata\",\n                }\n            }\n        },\n        {\n            \"title\": \"Porta di Lud\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Caldaia\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Fluido di Imbalsamazione\",\n                }\n            }\n        },\n        {\n            \"title\": \"Città di Londra\",\n        },\n        {\n            \"title\": \"Deserto del Nevada\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave d'Accesso al Generatore\",\n                },\n                \"key_2\": {\n                    \"name\": \"Chiave del Detonatore\",\n                }\n            }\n        },\n        {\n            \"title\": \"Reparto di Massima Sicurezza\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Scheda Elettronica di Tipo A\",\n                },\n                \"key_2\": {\n                    \"name\": \"Scheda Elettronica di Tipo B\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Chiave di Sicurezza Elettronica Blu\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Chiave di Sicurezza Elettronica Gialla\",\n                }\n            }\n        },\n        {\n            \"title\": \"Area 51\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Scheda Codice di Lancio\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Chiave di Accesso alla Torre\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Disco del Codice d'Accesso\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Disco del Codice d'Accesso\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Chiave di Accesso all'Hangar\",\n                }\n            }\n        },\n        {\n            \"title\": \"Antartide\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Capanna\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Piede di Porco\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Chiave di Controllo del Cancello\",\n                }\n            }\n        },\n        {\n            \"title\": \"Miniere della RX-Tech\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Piede di Porco\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Batteria al Piombo\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Avviamento dell'Argano\",\n                }\n            }\n        },\n        {\n            \"title\": \"Città Perduta di Tinnos\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave Uli\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maschera Oceaniana\",\n                }\n            }\n        },\n        {\n            \"title\": \"Caverna del Meteorite\",\n        },\n        {\n            \"title\": \"All Hallows\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave della Cripta\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"globe_select\": {\n            \"area_1\": \"Indie\",\n            \"area_2\": \"Wyspy Południowego Pacyfiku\",\n            \"area_3\": \"Pustynia Nevada\",\n            \"area_4\": \" \",\n            \"area_5\": \"Londyn\",\n            \"area_6\": \"\\\\{Antarktyda\",\n        },\n        \"inventory_ring\": {\n            \"heading_fmt\": \"\\\\{color 5}%s\",\n            \"item_count_fmt\": \"\\\\{color 3}%s\",\n            \"object_name_fmt\": \"\\\\{color 5}%s\",\n        },\n        \"misc\": {\n            \"empty_slot_fmt\": \"- Pusty Slot -\",\n        },\n        \"overlay\": {\n            \"item_count_fmt_pc\": \"%s\",\n            \"item_count_fmt_ps1\": \"\\\\{color 3}%s\",\n        },\n        \"pause\": {\n            \"paused\": \"\\\\{color 5}Pauza\",\n        },\n        \"stats\": {\n            \"assault_other_times_fmt\": \"\\\\{color 2}%s\",\n            \"gym_assault_course\": \"\\\\{color 5}Tor Przeszkód\",\n            \"gym_racetrack_course\": \"\\\\{color 5}Tor Wyścigowy\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Scena Przerywnikowa 1\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 2\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 3\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 4\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 5\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 6\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 7\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 8\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 9\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 10\",\n        },\n        {\n            \"title\": \"Scena Przerywnikowa 11\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Dom Lary\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do toru wyścigowego\",\n                }\n            }\n        },\n        {\n            \"title\": \"Dżungla\",\n            \"objects\": {\n                \"key_4\": {\n                    \"name\": \"Klucz Indry\",\n                }\n            }\n        },\n        {\n            \"title\": \"Ruiny Świątyni\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz Ganeszy\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Szabla\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Szabla\",\n                }\n            }\n        },\n        {\n            \"title\": \"Rzeka Ganges\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do bramy\",\n                }\n            }\n        },\n        {\n            \"title\": \"Jaskinie Kaliyi\",\n        },\n        {\n            \"title\": \"Wioska Nadbrzeżna\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz przemytnika\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Kamień węża\",\n                }\n            }\n        },\n        {\n            \"title\": \"Miejsce Katastrofy\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz komandora Bishopa\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz porucznika Tuckermana\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Mapa bagna\",\n                    \"description\": \"\",\n                }\n            }\n        },\n        {\n            \"title\": \"Wąwóz Madubu\",\n        },\n        {\n            \"title\": \"Świątynia Puny\",\n        },\n        {\n            \"title\": \"Nabrzeże Tamizy\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do kotłowni\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz do katedry\",\n                }\n            }\n        },\n        {\n            \"title\": \"Aldwych\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz konserwacyjny\",\n                },\n                \"key_2\": {\n                    \"name\": \"Klucz Salomona\",\n                },\n                \"key_3\": {\n                    \"name\": \"Klucz Salomona\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Stara moneta\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Bilet\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Masoński młotek\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ozdobna gwiazda\",\n                }\n            }\n        },\n        {\n            \"title\": \"Brama Lud'a\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do kotłowni\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Płyn balsamujący\",\n                }\n            }\n        },\n        {\n            \"title\": \"Miasto\",\n        },\n        {\n            \"title\": \"Pustynia Nevady\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Dostęp do generatora\",\n                },\n                \"key_2\": {\n                    \"name\": \"Przełącznik detonatora\",\n                }\n            }\n        },\n        {\n            \"title\": \"Ściśle Strzeżona Baza\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Karta dostępu typu A\",\n                },\n                \"key_2\": {\n                    \"name\": \"Karta dostępu typu B\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Niebieska przepustka bezpieczeństwa\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Żółta przepustka bezpieczeństwa\",\n                }\n            }\n        },\n        {\n            \"title\": \"Strefa 51\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Przepustka z kodem startowym\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Klucz dostępu do wieży\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Dysk z kodem dostępu\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Dysk z kodem dostępu\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Klucz dostępu do hangaru\",\n                }\n            }\n        },\n        {\n            \"title\": \"Antarktyda\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do chaty\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Łom\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Klucz do bramy\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kopalnie RX-Tech\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Łom\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Akumulator\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Rozrusznik wyciągarki\",\n                }\n            }\n        },\n        {\n            \"title\": \"Zaginione Miasto Tinnos\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz Uli\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Maska\",\n                }\n            }\n        },\n        {\n            \"title\": \"Jaskinia Meteorytowa\",\n        },\n        {\n            \"title\": \"Wszystkich Świętych\",\n            \"objects\": {\n                \"quest_1\": {\n                    \"name\": \"Kamień Infada\",\n                    \"description\": \"\",\n                },\n                \"quest_2\": {\n                    \"name\": \"Pierwiastek 115\",\n                    \"description\": \"\",\n                },\n                \"quest_3\": {\n                    \"name\": \"Oko Izydy\",\n                    \"description\": \"\",\n                },\n                \"quest_4\": {\n                    \"name\": \"Sztylet Ora\",\n                    \"description\": \"\",\n                },\n                \"key_1\": {\n                    \"name\": \"Klucz do skarbca\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"general\": {\n        \"globe_select\": {\n            \"area_1\": \"India\",\n            \"area_2\": \"South Pacific Islands\",\n            \"area_3\": \"Nevada\",\n            \"area_4\": \"\",\n            \"area_5\": \"London\",\n            \"area_6\": \"Antarctica\",\n        },\n        \"inventory_ring\": {\n            \"heading_fmt\": \"\\\\{color 5}%s\",\n            \"item_count_fmt\": \"\\\\{color 3}%s\",\n            \"object_name_fmt\": \"\\\\{color 5}%s\",\n        },\n        \"misc\": {\n            \"empty_slot_fmt\": \"- Empty Slot -\",\n        },\n        \"overlay\": {\n            \"item_count_fmt_pc\": \"%s\",\n            \"item_count_fmt_ps1\": \"\\\\{color 3}%s\",\n        },\n        \"pause\": {\n            \"paused\": \"\\\\{color 5}Paused\",\n        },\n        \"stats\": {\n            \"assault_other_times_fmt\": \"\\\\{color 2}%s\",\n            \"gym_assault_course\": \"\\\\{color 5}Assault Course\",\n            \"gym_racetrack_course\": \"\\\\{color 5}Race Track Course\",\n        }\n    },\n    \"objects\": {\n        \"large_medipack\": {\n            \"name\": \"Large Medi Pack\",\n        },\n        \"quest_1\": {\n            \"description\": \"\",\n            \"name\": \"Infada Stone\",\n        },\n        \"quest_2\": {\n            \"description\": \"\",\n            \"name\": \"Element 115\",\n        },\n        \"quest_3\": {\n            \"description\": \"\",\n            \"name\": \"Eye of Isis\",\n        },\n        \"quest_4\": {\n            \"description\": \"\",\n            \"name\": \"Ora Dagger\",\n        },\n        \"small_medipack\": {\n            \"name\": \"Small Medi Pack\",\n        }\n    },\n    \"cutscenes\": [\n        {\n            \"title\": \"Cutscene 1\",\n        },\n        {\n            \"title\": \"Cutscene 2\",\n        },\n        {\n            \"title\": \"Cutscene 3\",\n        },\n        {\n            \"title\": \"Cutscene 4\",\n        },\n        {\n            \"title\": \"Cutscene 5\",\n        },\n        {\n            \"title\": \"Cutscene 6\",\n        },\n        {\n            \"title\": \"Cutscene 7\",\n        },\n        {\n            \"title\": \"Cutscene 8\",\n        },\n        {\n            \"title\": \"Cutscene 9\",\n        },\n        {\n            \"title\": \"Cutscene 10\",\n        },\n        {\n            \"title\": \"Cutscene 11\",\n        }\n    ],\n    \"levels\": [\n        {\n            \"title\": \"Lara's Home\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Racetrack Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Jungle\",\n            \"objects\": {\n                \"key_4\": {\n                    \"name\": \"Indra Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Temple Ruins\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Key of Ganesha\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Scimitar\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Scimitar\",\n                }\n            }\n        },\n        {\n            \"title\": \"The River Ganges\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Gate Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Caves of Kaliya\",\n        },\n        {\n            \"title\": \"Coastal Village\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Smuggler's Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Serpent Stone\",\n                }\n            }\n        },\n        {\n            \"title\": \"Crash Site\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Commander Bishop's Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Lt. Tuckerman's Key\",\n                },\n                \"pickup_1\": {\n                    \"name\": \"Swamp Map\",\n                    \"description\": \"\",\n                }\n            }\n        },\n        {\n            \"title\": \"Madubu Gorge\",\n        },\n        {\n            \"title\": \"Temple of Puna\",\n        },\n        {\n            \"title\": \"Thames Wharf\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Flue Room Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Cathedral Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Aldwych\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Maintenance Key\",\n                },\n                \"key_2\": {\n                    \"name\": \"Solomon's Key\",\n                },\n                \"key_3\": {\n                    \"name\": \"Solomon's Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Old Penny\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Ticket\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Masonic Mallet\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Ornate Star\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lud's Gate\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Boiler Room Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Embalming Fluid\",\n                }\n            }\n        },\n        {\n            \"title\": \"City\",\n        },\n        {\n            \"title\": \"Nevada Desert\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Generator Access\",\n                },\n                \"key_2\": {\n                    \"name\": \"Detonator Switch\",\n                }\n            }\n        },\n        {\n            \"title\": \"High Security Compound\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Keycard Type A\",\n                },\n                \"key_2\": {\n                    \"name\": \"Keycard Type B\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Blue Security Pass\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Yellow Security Pass\",\n                }\n            }\n        },\n        {\n            \"title\": \"Area 51\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Launch Code Pass\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Tower Access Key\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Code Clearance Disk\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Code Clearance Disk\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Hanger Access Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Antarctica\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Hut Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Crowbar\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Gate Control Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"RX-Tech Mines\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Crowbar\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Lead Acid Battery\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Winch Starter\",\n                }\n            }\n        },\n        {\n            \"title\": \"Lost City of Tinnos\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Uli Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Oceanic Mask\",\n                }\n            }\n        },\n        {\n            \"title\": \"Meteorite Cavern\",\n        },\n        {\n            \"title\": \"All Hallows\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Vault Key\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3/weapons.json5",
    "content": "{\n    \"LGT_UNARMED\": {\n        \"sample_num\": \"SFX_LARA_NO\",\n    },\n\n    \"LGT_FLARE\": {\n        \"ammo\": {\n            \"initial_qty\": 6,\n            \"pickup_qty\": 6,\n            \"pickup_qty_alt\": 8,\n        },\n        \"flash_shade\": 2048,\n        \"flash_pos\": {\"x\": 11, \"y\": 32, \"z\": 80},\n        \"flash_pos_alt\": {\"x\": -6, \"y\": 6, \"z\": 80},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n    },\n\n    \"LGT_PISTOLS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 1,\n        \"ammo\": {\n            \"initial_qty\": 32,\n            \"pickup_qty\": 32,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 5120,\n        \"flash_pos\": {\"y\": 185, \"z\": 40},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": -16, \"y\": 128, \"z\": 40},\n        \"muzzle_pos_alt\": {\"x\": 16, \"y\": 128, \"z\": 40},\n        \"shell_pos\": {\"x\": 8, \"y\": 48, \"z\": 40},\n        \"shell_pos_alt\": {\"x\": -12, \"y\": 48, \"z\": 40},\n        \"smoke_count\": 28,\n        \"sample_num\": \"SFX_LARA_PISTOLS\",\n    },\n\n    \"LGT_MAGNUMS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 2,\n        \"ammo\": {\n            \"initial_qty\": 50,\n            \"pickup_qty\": 50,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\"y\": 155, \"z\": 55},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": -16, \"y\": 128, \"z\": 40},\n        \"muzzle_pos_alt\": {\"x\": 16, \"y\": 128, \"z\": 40},\n        \"shell_pos\": {\"x\": 8, \"y\": 48, \"z\": 40},\n        \"shell_pos_alt\": {\"x\": -12, \"y\": 48, \"z\": 40},\n        \"smoke_count\": 28,\n        \"sample_num\": \"SFX_LARA_MAGNUMS\",\n        \"is_available\": false,\n    },\n\n    \"LGT_AUTOS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 2,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\"y\": 215, \"z\": 65},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": -16, \"y\": 128, \"z\": 40},\n        \"muzzle_pos_alt\": {\"x\": 16, \"y\": 128, \"z\": 40},\n        \"shell_pos\": {\"x\": 8, \"y\": 48, \"z\": 40},\n        \"shell_pos_alt\": {\"x\": -12, \"y\": 48, \"z\": 40},\n        \"smoke_count\": 28,\n        \"sample_num\": \"SFX_LARA_AUTOS\",\n        \"is_available\": false,\n    },\n\n    \"LGT_DESERT_EAGLE\": {\n        \"type\": \"WEAPON_TYPE_SINGLE_PISTOL\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-10, +10, -80, +80],\n        \"right_angles\": [0, 0, 0, 0],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 4,\n        \"gun_height\": 650,\n        \"damage\": 21,\n        \"ammo\": {\n            \"initial_qty\": 10,\n            \"pickup_qty\": 10,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 16,\n        \"flash_time\": 3,\n        \"flash_shade\": 4096,\n        \"flash_pos\": {\"y\": 215, \"z\": 65},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": -32, \"y\": 160, \"z\": 56},\n        \"muzzle_pos_alt\": {\"x\": 16, \"y\": 160, \"z\": 56},\n        \"shell_pos\": {\"x\": 16, \"y\": 40, \"z\": 56},\n        \"shell_pos_alt\": {\"x\": -16, \"y\": 40, \"z\": 56},\n        \"smoke_count\": 28,\n        \"sample_num\": \"SFX_LARA_DESERT_EAGLE\",\n    },\n\n    \"LGT_UZIS\": {\n        \"type\": \"WEAPON_TYPE_DUAL_PISTOLS\",\n        \"lock_angles\": [-60, +60, -60, +60],\n        \"left_angles\": [-170, +60, -80, +80],\n        \"right_angles\": [-60, +170, -80, +80],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 650,\n        \"damage\": 1,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 3,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\"y\": 200, \"z\": 50},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": -16, \"y\": 140, \"z\": 48},\n        \"muzzle_pos_alt\": {\"x\": 8, \"y\": 140, \"z\": 48},\n        \"shell_pos\": {\"x\": 8, \"y\": 35, \"z\": 48},\n        \"shell_pos_alt\": {\"x\": -16, \"y\": 35, \"z\": 48},\n        \"smoke_count\": 28,\n        \"sample_num\": \"SFX_LARA_UZI_FIRE\",\n    },\n\n    \"LGT_SHOTGUN\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 0,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 2,\n            \"pickup_qty\": 2,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 9,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\"y\": 285},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": -16, \"y\": 228, \"z\": 96},\n        \"shell_pos\": {\"x\": 16, \"y\": 114, \"z\": 32},\n        \"smoke_count\": 32,\n        \"sample_num\": \"SFX_LARA_SHOTGUN\",\n    },\n\n    \"LGT_M16\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 4,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 40,\n            \"pickup_qty\": 40,\n        },\n        \"target_dist\": 12.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"sample_num\": \"\",\n        \"flash_pos\": {\"y\": 400, \"z\": 99},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {\"z\": -65},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": 0, \"y\": 228, \"z\": 96},\n        \"shell_pos\": {\"x\": 16, \"y\": 2, \"z\": 64},\n        \"smoke_count\": 24,\n        \"is_available\": false,\n    },\n\n    \"LGT_MP5\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 3,\n        \"ammo\": {\n            \"initial_qty\": 60,\n            \"pickup_qty\": 60,\n        },\n        \"target_dist\": 12.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 16,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 3,\n        \"flash_shade\": 2560,\n        \"flash_pos\": {\"y\": 332, \"z\": 96},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {\"z\": -65},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"muzzle_pos\": {\"x\": 0, \"y\": 228, \"z\": 96},\n        \"shell_pos\": {\"x\": 16, \"y\": 2, \"z\": 64},\n        \"smoke_count\": 24,\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_GRENADE\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 20,\n        \"ammo\": {\n            \"initial_qty\": 2,\n            \"pickup_qty\": 2,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 0,\n        \"draw_frame\": 13,\n        \"undraw_frame\": 14,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"muzzle_pos\": {\"x\": 0, \"y\": 180, \"z\": 80},\n        \"smoke_count\": 32,\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_ROCKET\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -55, +55],\n        \"left_angles\": [-80, +80, -65, +65],\n        \"right_angles\": [-80, +80, -65, +65],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 30,\n        \"ammo\": {\n            \"initial_qty\": 1,\n            \"pickup_qty\": 1,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 12,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"muzzle_pos\": {\"x\": 0, \"y\": 84, \"z\": 72},\n        \"smoke_count\": 32,\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_HARPOON\": {\n        \"type\": \"WEAPON_TYPE_RIFLE\",\n        \"lock_angles\": [-60, +60, -65, +65],\n        \"left_angles\": [-80, +80, -75, +75],\n        \"right_angles\": [-80, +80, -75, +75],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 500,\n        \"damage\": 6,\n        \"ammo\": {\n            \"initial_qty\": 3,\n            \"pickup_qty\": 3,\n        },\n        \"target_dist\": 8.0,\n        \"equip_anim_idx\": 1,\n        \"draw_frame\": 10,\n        \"undraw_frame\": 21,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"sample_num\": \"\",\n    },\n\n    \"LGT_SKIDOO\": {\n        \"type\": \"WEAPON_TYPE_MOUNTED\",\n        \"lock_angles\": [-30, 30, -55, 55],\n        \"left_angles\": [-30, 30, -55, 55],\n        \"right_angles\": [-30, 30, -55, 55],\n        \"aim_speed\": 10,\n        \"shot_accuracy\": 8,\n        \"gun_height\": 400,\n        \"damage\": 3,\n        \"target_dist\": 8.0,\n        \"recoil_frame\": 0,\n        \"flash_time\": 2,\n        \"flash_shade\": 5120,\n        \"flash_pos\": {\"y\": 185, \"z\": 40},\n        \"flash_color\": [0.75, 0.56, 0.0],\n        \"glow_pos\": {},\n        \"glow_color\": [1, 0.75, 0.125],\n        \"sample_num\": \"SFX_LARA_UZI_FIRE\",\n    },\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-la/gameflow.json5",
    "content": "{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"engine\": 3,\n    \"extends\": \"tr3\",\n    \"name\": \"The Lost Artifact\",\n\n    \"main_menu_picture\": \"title_eu_la.webp\",\n    \"savegame_file_fmt\": \"save_trla_%02d.dat\",\n\n    \"enable_tr2_item_drops\": true,\n    \"convert_dropped_guns\": true,\n\n    \"title\": {\n        \"path\": [\"title_la.tr2\", \"title.tr2\"],\n        \"music_track\": 5,\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"legal_eu_la.webp\", \"legal\": true},\n            {\"type\": \"play_fmv\", \"fmv_id\": 0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 1},\n            {\"type\": \"exit_to_title\"},\n        ],\n    },\n\n    \"ambient_tracks\": [\n        26, 27, 28, 29, 30, 31, 32, 33, 34,\n        35, 36, 73, 74, 75, 76, 77, 78,\n    ],\n\n    \"sfx_path\": [\"main_la.sfx\", \"main.sfx\"],\n    \"injections\": [\n        \"font.bin\",\n        \"lara_animations.bin\",\n        \"pda_model.bin\",\n        \"lara_extra.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n        \"pickup_aid.bin\",\n    ],\n\n    \"levels\": [\n        // 0. Legacy savegame placeholder\n        {\"type\": \"dummy\"},\n\n        // 1. Highland Fling\n        {\n            \"path\": \"scotland.tr2\",\n            \"music_track\": 36,\n            \"lara_outfit\": \"tr3_classic\",\n            \"weather_type\": \"rain\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"highland.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"give_item\", \"object_id\": \"small_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"large_medipack\"},\n                {\"type\": \"give_item\", \"object_id\": \"flare\", \"quantity\": 2},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"scotland_sky.bin\",\n            ],\n        },\n\n        // 2. Willard's Lair\n        {\n            \"path\": \"willsden.tr2\",\n            \"music_track\": 30,\n            \"lara_outfit\": \"tr3_classic\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"willard.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"scotland_sky.bin\",\n                \"willsden_heli.bin\",\n            ],\n            \"unobtainable_kills\": 1,\n        },\n\n        // 3. Shakespeare Cliff\n        {\n            \"path\": \"chunnel.tr2\",\n            \"music_track\": 74,\n            \"lara_outfit\": \"tr3_nevada\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"chunnel.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"cliff_door132_frames.bin\",\n            ],\n        },\n\n        // 4. Sleeping with the Fishes\n        {\n            \"path\": \"undersea.tr2\",\n            \"music_track\": 27,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"undersea.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"antarc_airlock.bin\",\n                \"lara_guns.bin\",\n                \"undersea_animating_bounds.bin\",\n                \"undersea_train.bin\",\n            ],\n        },\n\n        // 5. It's a Madhouse!\n        {\n            \"path\": \"zoo.tr2\",\n            \"script\": \"zoo.lua\",\n            \"music_track\": 34,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"zoo.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"remove_scions\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"zoo_train.bin\",\n            ],\n        },\n\n        // 6. Reunion\n        {\n            \"path\": \"slinc.tr2\",\n            \"music_track\": 26,\n            \"lara_outfit\": \"tr3_catsuit\",\n            \"sequence\": [\n                {\"type\": \"loading_screen\", \"path\": \"slinc.webp\", \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_music\", \"music_track\": 121},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"theend2_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit01_eu_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit02_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit03_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit04_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit05_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit06_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit07_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit08_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"display_picture\", \"credit\": true, \"path\": \"credit09_la.webp\", \"display_time\": 5, \"fade_in_time\": 1.0, \"fade_out_time\": 1.0},\n                {\"type\": \"total_stats\", \"background_path\": \"theend2_la.webp\"},\n                {\"type\": \"level_complete\"},\n            ],\n            \"injections\": [\n                \"lara_guns.bin\",\n                \"reunion_flames.bin\",\n            ],\n            \"unobtainable_pickups\": 1,\n            \"unobtainable_secrets\": 1,\n        },\n    ],\n\n    \"fmvs\": [\n        {\"path\": \"logo.rpl\", \"legal\": true},\n    ],\n\n    \"hidden_config\": [\n        \"enable_3d_pickups\", // TR3 has no sprite pickups\n        \"enable_item_examining\",\n        \"disable_trex_collision\",\n        \"fix_bear_ai\",\n        \"enable_save_crystals\",\n        \"enable_ps1_crystals\",\n        \"fix_chainblock_secret_sound\",\n        \"enable_compass_stats\",\n        \"enable_crawling\",\n        \"enable_wading\",\n        \"restore_ps1_enemies\",\n        \"change_pierre_spawn\",\n        \"fix_speeches_killing_music\",\n        \"fix_pipeman_aim\",\n        \"enable_cinematics\",\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-la/strings-de.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Laras Anwesen\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Schlüssel zur Rennstrecke\",\n                }\n            }\n        },\n        {\n            \"title\": \"Das Hochland\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Brechstange\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Distelstein\",\n                }\n            }\n        },\n        {\n            \"title\": \"Willards Unterschlupf\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Steinhaufen-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Brechstange\",\n                }\n            }\n        },\n        {\n            \"title\": \"Shakespeare-Klippe\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Bohrer-Aktivierungs-Karte\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Pumoen-Zugangs-Disk\",\n                }\n            }\n        },\n        {\n            \"title\": \"Mit den Fischen schlafen\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Blaue Stromkreis-Glühbirne\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Mutant-Probe\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Mutant-Probe\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Gelbe Stromkreis-Glühbirne\",\n                }\n            }\n        },\n        {\n            \"title\": \"Es ist ein Irrenhaus!\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Zoo-Schlüssel\",\n                },\n                \"key_4\": {\n                    \"name\": \"Vogelhaus-Schlüssel\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Hand von Rathmore\",\n                }\n            }\n        },\n        {\n            \"title\": \"Wiedervereinigung\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Hand von Rathmore\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-la/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Casa di Lara\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Circuito\",\n                }\n            }\n        },\n        {\n            \"title\": \"Balzo nell'Altopiano\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Piede di Porco\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Gemma del Cardo\",\n                }\n            }\n        },\n        {\n            \"title\": \"Nascondiglio di Willard\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave del Tumulo\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Piede di Porco\",\n                }\n            }\n        },\n        {\n            \"title\": \"Scogliera di Shakespeare\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Scheda di Attivazione della Trivella\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Disco di Accesso alla Pompa\",\n                }\n            }\n        },\n        {\n            \"title\": \"Dormire con i Pesci\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Lampadina\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Campione Mutante\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Campione Mutante\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Lampadina\",\n                }\n            }\n        },\n        {\n            \"title\": \"È un Manicomio!\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Chiave dello Zoo\",\n                },\n                \"key_4\": {\n                    \"name\": \"Chiave della Voliera\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Mano di Rathmore\",\n                }\n            }\n        },\n        {\n            \"title\": \"Riunione\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Mano di Rathmore\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-la/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"objects\": {\n        \"quest_1\": {\n            \"description\": \"\",\n            \"name\": \"Dłoń Rathmore'a\",\n        }\n    },\n    \"levels\": [\n        {\n            \"title\": \"Dom Lary\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do toru wyścigowego\",\n                }\n            }\n        },\n        {\n            \"title\": \"Wypad na wzgórza\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Łom\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Kamień Ostu\",\n                }\n            }\n        },\n        {\n            \"title\": \"Kryjówka Willarda\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do kurhanu\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Łom\",\n                }\n            }\n        },\n        {\n            \"title\": \"Klif Szekspira\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Karta uruchamiająca wiertło\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Dysk dostępu do pompy\",\n                }\n            }\n        },\n        {\n            \"title\": \"Na dnie z rybami\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Żarówka układu\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Próbka mutanta\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Próbka mutanta\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Żarówka układu\",\n                }\n            }\n        },\n        {\n            \"title\": \"Dom wariatów!\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Klucz do zoo\",\n                },\n                \"key_4\": {\n                    \"name\": \"Klucz do woliery\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Dłoń Rathmore'a\",\n                }\n            }\n        },\n        {\n            \"title\": \"Spotkanie po latach\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Dłoń Rathmore'a\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-la/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"objects\": {\n        \"orca\": {\n            \"name\": \"Dolphin\",\n        },\n        \"quest_1\": {\n            \"description\": \"\",\n            \"name\": \"Hand of Rathmore\",\n        }\n    },\n    \"levels\": [\n        {\n            \"title\": \"Lara's Home\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Racetrack Key\",\n                }\n            }\n        },\n        {\n            \"title\": \"Highland Fling\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Crowbar\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Thistle Stone\",\n                }\n            }\n        },\n        {\n            \"title\": \"Willard's Lair\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Cairn Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Crowbar\",\n                }\n            }\n        },\n        {\n            \"title\": \"Shakespeare Cliff\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Drill Activator Card\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Pump Access Disk\",\n                }\n            }\n        },\n        {\n            \"title\": \"Sleeping with the Fishes\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Circuit Bulb\",\n                },\n                \"puzzle_2\": {\n                    \"name\": \"Mutant Sample\",\n                },\n                \"puzzle_3\": {\n                    \"name\": \"Mutant Sample\",\n                },\n                \"puzzle_4\": {\n                    \"name\": \"Circuit Bulb\",\n                }\n            }\n        },\n        {\n            \"title\": \"It's a Madhouse!\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Zoo Key\",\n                },\n                \"key_4\": {\n                    \"name\": \"Aviary Key\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Hand of Rathmore\",\n                }\n            }\n        },\n        {\n            \"title\": \"Reunion\",\n            \"objects\": {\n                \"puzzle_1\": {\n                    \"name\": \"Hand of Rathmore\",\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-level/gameflow.json5",
    "content": "{\n    // This file is used to enable the -l argument support.\n\n    \"engine\": 3,\n    \"extends\": \"tr3\",\n    \"name\": \"TR3 Direct Level\",\n\n    \"main_menu_picture\": \"title_eu.webp\",\n    \"savegame_file_fmt\": \"save_tr3_custom_%02d.dat\",\n\n    \"demo_version\": false,\n    \"enable_tr2_item_drops\": true,\n    \"convert_dropped_guns\": true,\n\n    \"sfx_path\": \"main.sfx\",\n    \"injections\": [\n        \"font.bin\",\n        \"lara_animations.bin\",\n        \"pda_model.bin\",\n        \"lara_extra.bin\",\n        \"misc_sprites.bin\",\n        \"lara_outfits.bin\",\n        \"pickup_aid.bin\",\n    ],\n\n    \"levels\": [\n        {\n            \"path\": \"%direct_level%\",\n            \"music_track\": -1,\n            \"lara_outfit\": \"tr3_classic\",\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n            ],\n        },\n    ],\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-level/strings-it.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Livello di Prova\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-level/strings-pl.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Poziom testowy\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/ship/games/tr3-level/strings.json5",
    "content": "{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"levels\": [\n        {\n            \"title\": \"Test Level\",\n        }\n    ]\n}\n"
  },
  {
    "path": "data/trx/version.rc",
    "content": "1 VERSIONINFO\nFILEVERSION     0,0,0,0\nPRODUCTVERSION  0,0,0,0\nBEGIN\n  BLOCK \"StringFileInfo\"\n  BEGIN\n    BLOCK \"080904E4\"\n    BEGIN\n      VALUE \"CompanyName\", \"LostArtefacts\"\n      VALUE \"FileDescription\", \"Tomb Raider X: Community Edition\"\n      VALUE \"FileVersion\", \"TRX {version}\"\n      VALUE \"InternalName\", \"TRX\"\n      VALUE \"OriginalFilename\", \"TRX.exe\"\n      VALUE \"ProductName\", \"Tomb Raider X: Community Edition\"\n      VALUE \"ProductVersion\", \"TRX {version}\"\n    END\n  END\n  BLOCK \"VarFileInfo\"\n  BEGIN\n    VALUE \"Translation\", 0x809, 1252\n  END\nEND\n"
  },
  {
    "path": "docs/BUILDING.md",
    "content": "# Building TRX\n\n## Build workflow\n\nInitial build:\n\n- Compile the project.\n- Copy all executable files from `build/` to your game directory.\n- Copy the contents of `data/*/ship/` to your game directory.\n\nSubsequent builds:\n\n- Compile the project.\n- Copy all executable files from `build/` to your game directory.\n  We recommend making a script file to do this.\n\n## Compiling\n\n### Compiling on Linux\n\nFollow [BUILDING_ON_LINUX.md](BUILDING_ON_LINUX.md).\n\n### Compiling on Windows\n\nFollow [BUILDING_ON_WINDOWS.md](BUILDING_ON_WINDOWS.md).\n\n### Compiling on MacOS\n\nFollow [BUILDING_ON_MACOS.md](BUILDING_ON_MACOS.md).\n\n### Supported compilers\n\nPlease be advised that any build systems that are not the one we use for\nautomating releases (= mingw-w64) come at user's own risk. They might crash or\neven refuse to compile.\n"
  },
  {
    "path": "docs/BUILDING_ON_LINUX.md",
    "content": "## Building on Linux\n\nThis guide describes the officially supported Linux build workflow using\nDocker and [just](https://github.com/casey/just).\n\n## Installing dependencies\n\nInstall the following dependencies using your distribution's package manager:\n\n- `docker`\n- `ffmpeg`\n- `glew`\n- `just`\n- `meson`\n- `pkgconfig`\n- `python3`\n- `sdl2`\n- `uthash`\n\nDepending on your system, the Docker package may be named `docker`,\n`docker.io`, or similar.\n\n## Building TRX\n\n1. Download the shipped game assets for the game you want to build:\n\n    ```bash\n    ./tools/download_assets X\n    ```\n\n    Replace `X` with the TR version.\n\n2. Build the Linux target:\n\n    ```bash\n    just trx-build-linux target='debug'\n    ```\n\nThe built files will be placed in the `build/` directory.\n\n## Other build methods\n\nThe Docker workflow above is the recommended way to build TRX on Linux.\n\nIf you prefer a manual, non-Docker setup, you are welcome to try it, but it is\nnot part of the project's official build workflow.\n\nThe best starting point is to inspect the files in `tools/*/docker/` for the\nexternal dependencies and `meson.build` for the local files, then tailor your\nsystem to match the release build environment as closely as possible.\n\n## Running the game\n\nTo prepare the game directory:\n\n1. Copy the built files from `build/`.\n2. Copy the contents of `data/X/ship/`.\n3. Copy the original game files from your game installation.\n\nReplace `X` with the TR version you built.\n\nOnce the files are in place, run:\n\n```bash\n./TRX\n```\n"
  },
  {
    "path": "docs/BUILDING_ON_MACOS.md",
    "content": "## Building on macOS\n\nThis guide describes the native macOS build workflow using Meson.\n\n## Installing dependencies\n\nInstall either Homebrew or MacPorts, then install the required dependencies.\n\nHomebrew:\n\n```bash\nbrew install sdl2 glew ffmpeg@6 uthash pkgconfig meson python@3.14\n```\n\nMacPorts:\n\n```bash\nsudo port install sdl2 ffmpeg uthash pkgconfig glew meson python@3.14\n```\n\n## Building TRX\n\n1. Download the shipped game assets for the game you want to build:\n\n    ```bash\n    ./tools/download_assets X\n    ```\n\n    Replace `X` with the TR version.\n\n2. Configure the build:\n\n    Intel Macs:\n\n    ```bash\n    meson setup build src --prefix=/tmp/TRX.app --bindir=Contents/MacOS --buildtype release --cross-file tools/shared/mac/x86-64_cross_file.txt\n    ```\n\n    Apple Silicon Macs:\n\n    ```bash\n    meson setup build src --prefix=/tmp/TRX.app --bindir=Contents/MacOS --buildtype release\n    ```\n\n3. Build the project:\n\n    ```bash\n    meson compile -C build\n    ```\n\n## Other build methods\n\nThe native Meson workflow above is the recommended way to build TRX on macOS.\n\nIf you want to experiment with other approaches, such as Docker or a different\nnative setup, you are welcome to do so, but they are not part of the project's\nofficial build workflow.\n\n## Running the game\n\nTo prepare the game directory:\n\n1. Copy the built files from `build/`.\n2. Copy the contents of `data/X/ship/`.\n3. Copy the original game files from your game installation.\n\nReplace `X` with the TR version you built.\n\nOnce the files are in place, run:\n\n```bash\n./TRX\n```\n"
  },
  {
    "path": "docs/BUILDING_ON_WINDOWS.md",
    "content": "## Building on Windows\n\nThis guide describes the officially supported Windows build workflow using WSL.\n\n## Installing dependencies\n\nInstall WSL and Ubuntu first.\n\n1. Run PowerShell as Administrator.\n2. Enable the Windows Subsystem for Linux feature:\n\n    ```powershell\n    Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux\n    ```\n\n3. Restart the computer.\n4. Open the Microsoft Store.\n5. Install Ubuntu.\n\nOnce WSL is installed, continue by following the Linux build guide from within\nyour Ubuntu environment.\n\n## Building TRX\n\nAfter opening Ubuntu in WSL, follow the steps in\n[BUILDING_ON_LINUX.md](BUILDING_ON_LINUX.md).\n\n## Other build methods\n\nThe WSL workflow above is the recommended way to build TRX on Windows.\n\nIf you want to experiment with Visual Studio or other native Windows build\nmethods, you are welcome to do so, but they are not part of the project's\nofficial build workflow.\n\n## Running the game\n\nOnce the game directory is prepared, run TRX from the copied build output and\ngame files as described in [BUILDING_ON_LINUX.md](BUILDING_ON_LINUX.md).\n"
  },
  {
    "path": "docs/CHANGELOG.md",
    "content": "## [Unreleased](https://github.com/LostArtefacts/TRX/compare/trx-1.5...develop) - ××××-××-××\n- added the ability to do a forward roll without releasing sprint first (#5270)\n- added the ability for Lara to align herself with floor tilts when crawling (Gameplay Options → Controls → Crawl tilt) (#4945)\n- added the ability to turn off or censor blood effects (Graphic Options → Visuals → Blood effects)\n- added the ability to delete saves directly from the passport save and load screens (#5309)\n- added the ability to pause FMVs with the Pause input (#1754)\n- added an option to stop the game from advancing when the window loses focus (Gameplay Options → General → Pause when focus lost) (#3978)\n- added `O_DISPOSABLE_ANIMATING_1`...`O_DISPOSABLE_ANIMATING_10`, which will behave like regular animating objects but are removed from being drawn when deactivated\n- added an option to fix inaccurate wall geometry in original levels (Gameplay Options → Fixes → Fix wall geometry)\n- added an option to control how land creatures behave in water (Gameplay Options → General → Creature drown policy) (#5387)\n- changed the PS1 crystal tint option to take effect without having to reload the level\n- changed the `ITEM_ACTION_FLOOD` sound effect to play when underwater rather than only when above water\n- improved weapon setup so picking up a weapon can now give a different amount of ammo than picking up its matching ammo item (#5352)\n- improved `--level PATH` so it accepts relative paths and reports clearer startup errors when the level cannot be launched\n- improved savegame loading if item counts have changed between making the save and loading it\n- fixed max stats not refreshing after changing unobtainable pickups, kills, or secrets in the gameflow\n- fixed Lara rapidly switching animations when shimmying across the top of a ladder (#5295)\n- fixed Lara being able to crawl and crouch-roll too far into water from land\n- fixed incorrect transparent and yellow pixels on TR2 and TR3 outfit heads when bilinear filtering is enabled (#5300)\n- fixed rotating 3D pickup notifications following the UI filter setting instead of the in-game texture filter\n- fixed climbing issues on ladders that are against walls that (incorrectly) contain tilt data within them (#5304, regression from 1.1)\n- fixed Lara not transitioning immediately to run after vaulting two clicks when forward is held (#5305, regression from 1.3)\n- fixed settings list auto-scroll sometimes stopping after switching to a different mod (regression from 1.4)\n- fixed first-time keyboard keybindings conflicting after switching to a different mod (regression from 1.4)\n- fixed boulders stopping too soon on some slopes with low ceilings (#5337, regression from 1.2)\n- fixed persistent damage restoring Lara to full health after inter-level cutscenes (#5364, regression from 1.2)\n- fixed missing default object names for `O_ANIMATING_7`...`O_ANIMATING_10` and `O_FLICKERING_LIGHT` (regression from 1.4)\n- fixed empty centaur statues incorrectly referencing other level items when the centaur object is not loaded\n- fixed TR3 camera mode potentially behaving erratically when loading a level and the look input is held (regression from 1.1)\n\n**TR1**:\n- added savegame crystals to Unfinished Business (#1525)\n- fixed not being able to hear the flood/drain sound effect when using the lever in Tomb of Tihocan room 23\n\n**TR2**:\n- added savegame crystals to base levels and The Golden Mask\n- added an option to disable body bag triggers, so that killed enemies will always be visible\n\n**TR3**:\n- added Boat (RIB) control\n- added Mine Cart control\n- added Willard control\n- added RX-Tech Worker 1 control\n- added RX-Tech Worker 2 control\n- added RX-Tech Worker 3 control\n- added Crawler Mutant control\n- added Dying Mutant control\n- added Hybrid Mutant control\n- added Wasp Mutant control\n- added Wasp Mutant Emitter control\n- added Claw Mutant control\n- added Fire Head control\n- added Disposable Animating control (Tinnos light shaft)\n- added an option to disable body bag triggers, so that killed enemies will always be visible\n- changed Sophia's final height to follow the level setup instead of using a fixed value\n- restored the animated mine cart tracks in RX-Tech Mines\n- restored the missing flamethrower blast sound effect in RX-Tech Mines and Meteorite Cavern\n- removed Lara's Home from TR3:LA to stay compatible with the OG and other expansion packs\n- fixed letterboxing of images on 16:10 resolution\n- fixed Fire Lighting option having no effect\n- fixed missing conveyor belt animations in High Security Compound\n- fixed delayed lighting updates on Lara during movement, particularly noticeable on ladders (regression from 1.1)\n- fixed z-fighting in High Security Compound rooms 135/179\n- fixed transparent and magenta pixels on grating textures in High Security Compound and Area 51\n- fixed an incorrect window texture in High Security Compound room 105\n- fixed the satellite dish in High Security Compound room 44 being clipped out of view too soon (missing animation bounds) (#5297, regression from 1.1)\n- fixed It's a Madhouse! street lamps being too bright\n- fixed menu artefacts not appearing in consistent positions between levels\n- fixed TR3:LA playing TR3 intro FMV\n- fixed Coastal Village and Lost City of Tinnos having the wrong pickup count\n- fixed Willard's Lair having wrong kill count\n- fixed Reunion having wrong pickup and secret count\n- fixed being able to re-use switches that are intended to only be used once (#5328, regression from 1.1)\n- fixed capitalization of the \"Empty Slot\" text in passport\n- fixed the second boulder at the beginning of Reunion stopping too early (regression from 1.2)\n- fixed Willard increasing the kill count each time he collapses (OG bug)\n- fixed Wasp Emitters generating too many spawns if activated from non one-shot triggers and the player stands for too long on the trigger\n- fixed Lara being unable to pull up on specific ledges near walls that have invalid triangles within them (regression from 1.1)\n- fixed potential crashes when using grenades on enemies in levels that use the body bag feature (#5378, regression from 1.1)\n- fixed activated one-shot antitriggers not being remembered when loading a save (regression from 1.2)\n- fixed the Ora Dagger appearing too low in the inventory in Meteorite Cavern (regression from 1.2)\n- fixed quest item pickup counts incrementing if the item cheat is used and then save/load is repeatedly used (regression from 1.2)\n\n\n\n## [1.5](https://github.com/LostArtefacts/TRX/compare/trx-1.4.2...trx-1.5) - 2026-04-04\nShowcase: https://youtu.be/TTlajgcM9-8\n- added multi-key combo shortcuts (up to 3 keys) with two binding slots per action for both keyboard and controller\n- added remembering of the last played mod\n- added a new console command, `/tp enemy`, to cycle Lara through hostile creatures in the current level\n- added a new animation command, `ITEM_ACTION_TURN_90`, which will rotate the affected item 90°\n- added dynamic mod discovery from the games/ directory using new `extends` and `name` fields in gameflow.json5\n- added expanded statistics screen customization, including per-row toggles and a choice between bare and bordered layouts (Graphic Options → Stats)\n- added an option to show or hide the version text in the title inventory ring (#5235)\n- added optional save/heal crystal counts to level and final statistics (#5180)\n- added the ability for security lasers to activate heavy triggers when tripped by Lara (#5225)\n- added `trx.camera.reset()` to Lua, which will reposition the camera based on Lara's position\n- added an option to disable cinematics at the start of levels (Offshore Rig, Home Sweet Home and High Security Compound) (Gameplay → General → Cinematics) (#5284)\n- changed `--level` to no longer require `-e/--engine` to work\n- changed background images on title, inventory, and statistics screens to always use smooth bilinear filtering instead of the pixel-sharp look\n- improved rendering line segments (poison darts, rain drops, SWAT laser sights)\n- fixed recordings keeping unbound hotkeys active during playback\n- fixed being unable to change FOV after using photo mode without restarting the level (#5246, regression from TR1X 4.15)\n- fixed Lara getting stuck if using crouch-roll near very low ceilings (#5248)\n- fixed Bell in room 48 being shootable from room 55 again (#4949, regression from TRX 1.4 Sophia Reunion targeting fix)\n- fixed certain TR1 1.1 savegames refusing to load (#5252, regression from 1.2)\n- fixed the total kill count including allies if hostility policy is set to individual and Lara shoots a hostile enemy (#5255, regression from 1.2)\n- fixed quick-load remaining unavailable while Lara is in her death animation (#5264, regression from 1.3)\n- fixed destroying the Fuse Box to defeat Sophia in City and Reunion not counting as a kill in the level statistics\n- fixed potential freezing issues after moving an item to a different room via Lua\n- fixed crash when issuing `/mod tr1-ub` when playing late TR1 levels\n- fixed Lara being unable to draw weapons if animated interactions are enabled and she is hit by an enemy while moving towards a pickup (#5288, regression from TR1X 4.13)\n- fixed interaction issues with pushblocks when in shallow water (regression from 1.0)\n\n**TR2**:\n- changed the Detonator Box to no longer hard-code dynamic light output; refer to the migration guide for custom levels\n- fixed the total possible kill count in Furnace of the Gods being inaccurate if the monks are attacked (#5229)\n\n**TR3**:\n- added Security Laser (Alarm) control\n- added Security Laser (Damage) control\n- added Security Laser (Kill) control\n- added Rotating Laser control\n- added Sentry Gun control\n- added Civilian control\n- added Detonator Box control\n- added Prisoner control\n- added MP 1 control\n- added MP 2 control\n- added Orca control\n- added Area 51 Rocket control\n- added Hook control\n- added pickup aids (Graphic Options → Visuals → Pickup aids) (#5239)\n- added high-resolution 16:9 and 4:3 images for TR3:LA  \n    To download the new images ahead of a stable release, please see the [TRX data](https://github.com/LostArtefacts/TRX-data) repository.\n- restored the cutscene at the beginning of High Security Compound\n- changed Area 51 Rocket to no longer hardcode room 52 as the fire blast room; instead it checks for presence of an upwards portal pointing to the rocket room\n- changed enemies who are killed by lasers to be included in the stats\n- fixed Sophia's staff having a shadow in the City cutscene\n- fixed some doors having a bad rotation when closing, mostly visible when using the door cheat\n- fixed the Area 51 sliding doors being offset too far from the floor\n- fixed bad vertices in staircase static meshes in Aldwych and Lud's Gate, allowing for visible gaps in geometry (#5182)\n- fixed bad positioning of light static meshes in Aldwych that could result in Lara not grabbing certain ledges (#5181)\n- fixed several missing textures in Lud's Gate room 77\n- fixed Tony's fireballs flying the wrong way and piling up after loading a save (regression from 1.1)\n- fixed rockets exploding underwater being able to create a water splash in the wrong place (regression from 1.1)\n\n\n\n## [1.4.2](https://github.com/LostArtefacts/TRX/compare/trx-1.4.1...trx-1.4.2) - 2026-03-24\n- fixed 3D pickups and inventory ring view still affected by fog (regression from 1.4)\n\n**TR2**:\n- fixed the Detonator Box being difficult to activate when selecting the key manually from the inventory (#5215, regression from TR2X 1.3)\n- fixed the Detonator Box rotating if Lara interacts with it but then doesn't pick the key from the inventory (#5215, regression from TR2X 1.3)\n\n\n\n## [1.4.1](https://github.com/LostArtefacts/TRX/compare/trx-1.4...trx-1.4.1) - 2026-03-23\n- fixed Lara using the ladder-to-crouch animation in some rare cases despite there being headroom in front of her (regression from 1.2)\n- fixed toggle-sprint key failing to cancel sprint mid-run (#5174, regression from 1.4)\n- fixed toggle-duck key failing to keep Lara ducked in run-to-duck and sprint-to-duck paths (#5177, regression from 1.4)\n- fixed flipped state of \"Pause music in inventory\", changed to \"Enable music in inventory\" (regression from 1.4)\n- fixed final statistics in City of Khamoon counting the optional PS1 mummy when Restore PS1 enemies is disabled (#5188, regression from 1.1)\n- fixed bouncy grenades getting stuck in certain geometry (#5202, regression from 1.2)\n- fixed thrown flares getting stuck in certain geometry (#5202, regression from 1.4)\n\n**TR3**:\n- fixed door 34 in Thames Wharf closing permanently during the flipmap puzzle (#5170, regression from 1.1)\n- fixed Lara being unable to move after grabbing ladders in specific geometry (#5169, regression from 1.1)\n- fixed a missing camera shake effect in room 135 in Aldwych (#5183)\n- fixed a missing sound effect during the flip map in the Egyptian room in Lud's Gate (#5183)\n- fixed potential framerate drops during audio playback when no active sound effects are playing (regression from 1.3)\n- fixed some enemies automatically being hostile when triggered if other enemies have been killed (#5203, regression from 1.1)\n\n\n\n## [1.4](https://github.com/LostArtefacts/TRX/compare/trx-1.3.1...trx-1.4) - 2026-03-21\nShowcase: https://youtu.be/8SavYv2SawI\n- added an option to let Lara stay crouched without holding the button (Gameplay → Controls → Toggle crouch) (#5006)\n- added an option to let Lara keep sprinting without holding the button (Gameplay → Controls → Toggle sprint) (#5006)\n- added three additional outfits for Lara\n- added a new console command, `/mod {name}`, to switch between installed game/mod packs without relaunching\n- added a new option in the New Game dialog, \"Switch Game\", to switch between installed game/mod packs without relaunching\n- added experimental support for config presets (Gameplay Options → Presets)\n     Currently very basic presets available only – looking for help with improving them :)\n- added new backgrounds to Inventory Ring / Pause screen / Stats screen styles:\n    - Transparent: like TR2 PS1 Pause Screen\n    - Black: like the Remasters\n    - Monochrome (cool): like TR3 PS1 Inventory Screen\n    - Monochrome (warm): like TR3 PS1 Pause Screen\n- added support for TR4-style trigger-triggerers (named `O_TRIGGER_GATE` in TRX) for custom levels\n- added support for `.wma` music files for broader custom level compatibility\n- added support for `.ogv` and `.fmv` FMV extensions, with `.ogv` preferred over `.fmv` for remaster compatibility\n- added audio fallback for FMV files that lack an audio stream (e.g. remastered `.ogv`), probing alternative extensions for a companion audio track\n- added an option to move Ammo counter location (Graphic Options → UI → Ammo counter location) (#5076)\n- added simple level-load caching by introducing a `cache/` folder\n- added an option for Lara to wear semi-transparent sunglasses (Graphic Options → Lara's sunglasses)\n- added four additional general animating object slots, `O_ANIMATING_7` to `O_ANIMATING_10`\n- added `O_FLICKERING_LIGHT`, which is similar to `O_ELECTRICAL_LIGHT` but is permanently flickering\n- changed the reflections option to be available in all game modes (Graphic Options → Enable reflections)\n- changed the delay in performing a running jump by one frame less, when jump lock mode is set to disabled (Gameplay → Controls → Jump lock mode) (#3841)\n- improved level loading times by 15%\n- improved FMV audio to play through the game's existing audio mixer instead of opening a separate audio device\n- fixed High lighting contrast not attenuating brightness properly in TR1 and TR2 (regression from 1.1)\n- fixed the photo mode red frame not covering the full screen when using integer upscaling\n- fixed boulders that have moved vertically reactivating for a frame after loading a save (regression from 1.2)\n- fixed low fog distances affecting 3D pickups and inventory ring view\n- fixed cheat and weapon hotkeys affecting gameplay during demos (#5163, regression from 1.0)\n\n**TR1**:\n- added the ability to use flames on Pendulums, similar to TR3\n- changed skyboxes in TR1 to be drawn only if the appropriate room flag is set\n- changed the scion pickup in Sanctuary of the Scion to not be displayed on-screen briefly before the level ends (#3682)\n- fixed Scion taking damage before activation (regression from 1.0)\n\n**TR2**:\n- added the ability to use flames on Pendulums, similar to TR3\n- changed demos to show accurate gun meshes before Lara draws her pre-selected weapon (#3585)\n- fixed Bartoli appearing frozen towards the end of the Opera House cutscene\n- fixed pulling the Dagger of Xian from dragon's corpse not counting as a pickup (regression from 1.0)\n- fixed thrown flares falling through trapdoors and becoming stuck in the void if thrown underwater near the floor (#3708)\n- fixed flamethrowers and Dragon's breath doing weird animation when hitting floor (#5104, regression from 1.3)\n- fixed yetis dealing no damage during their charge attack (#5126, regression from TR2X 0.8)\n\n**TR3**:\n- added UPV control\n- added Train control\n- added Patrol Dog control\n- added Crow control\n- added Sophia control\n- added Fuse Box control\n- added Electric Cleaner control\n- added Punk control, including `O_PUNK_2`, which was unused in OG\n- added Security Guard control\n- added Propeller control\n- added SWAT control\n- added Diver control\n- added Pendulum control\n- added 60 FPS interpolation to:\n    - sparks\n    - weather effects\n    - water effects\n    - wake effects\n    - explosion rings\n    - bat emitters\n- changed Ammo Counter to appear in red when the Menu Style is set to PS1\n- changed Punks to have friendliness assignable through Lua, so removing the hard-coded behaviour in the Lud's Gate level sequence\n- changed Trains to no longer hard-code speed based on the level number and instead take it from their default animation\n- changed Pendulums that have flames to be setup by placing a flame emitter at the same position, rather than setting the item's timer via its trigger\n- changed Meteorite Artefacts to be exempted from drop tile centering\n- fixed a soft lock preventing Lara from picking up the artefact, when saving/loading during boss explosion sequence (regression from 1.2)\n- fixed the helicopter in Highland Fling briefly disappearing when crossing room portals (regression from 1.1)\n- fixed Lara dying from touching Trains that haven't yet been activated\n- fixed harpoons from Divers not spawning blood when they hit Lara\n- fixed `O_KILL_ALL_TRIGGERED` removing unused Save Crystals (#5035)\n- fixed TR1/TR2-only options showing up in TR3 gameplay settings (#5055)\n- fixed Lara stopping against one-click raised slopes when running instead of beginning to slide (#5038)\n- fixed rain not spawning in outside rooms in the Thames Wharf cutscene\n- added PSX-style underwater water particles to Madubu Gorge, Aldwych, and Lud's Gate\n- fixed the punk in the cutscene before Lud's Gate walking through a wall\n- fixed Lara appearing frozen at the beginning of the cutscene before City\n- fixed incorrect texturing on the fish in City\n- fixed the Eye of Isis not showing in the inventory in All Hallows\n- fixed too low volume in all FMVs (except logo which used a different codec)\n- fixed Lara by default being unable to climb out of water onto steep slopes (change manually in Gameplay → Fixes → Fix water exit)\n- fixed thrown flares falling through trapdoors (regression from 1.1)\n- fixed some level textures appearing slightly misaligned on room geometry\n- fixed potential AI behavioural differences in the South Pacific Mercenary (regression from 1.2)\n- fixed smoke from Lara's guns persisting between levels (regression from 1.1)\n- fixed sound effects potentially playing after completing a level and entering into the globe select screen (regression from 1.2)\n- fixed audio lag and framerate drops near the end of an audio track that's playing when no active sound effects are playing (#5167, regression from 1.3)\n\n\n\n## [1.3.1](https://github.com/LostArtefacts/TRX/compare/trx-1.3...trx-1.3.1) - 2026-03-11\n- fixed main.sfx resolution being enforced (regression from 1.3)\n- fixed the microphone entering underwater mode too eagerly when `Microphone near Lara` is enabled (#5057, #4888)\n- fixed save counters sometimes drifting after dying and reloading (#5054, regression from TR1X 4.9 / TRX 1.0)\n- fixed flare and gun flash being drawn with a water tint when in shallow water regardless of responsive tint option (#5072, regression from 1.2)\n- fixed fade transitions using the wrong picture size when upscaling or borders are enabled (#5081, regression)\n\n**TR2**:\n- fixed guns as secret rewards not being converted to the equivalent ammo if Lara already has the gun\n\n**TR3**:\n- fixed reverb affecting inventory ring sounds (#5056)\n- fixed Pause text color\n- fixed the secret sound not playing in some installations, whereby `cdaudio.wad` contains invalid track sizes (#5049)\n- fixed mounting a UPV causing Lara's braid to stand upright (OG)\n\n\n\n## [1.3](https://github.com/LostArtefacts/TRX/compare/trx-1.2.2...trx-1.3) - 2026-03-06\nShowcase: https://youtu.be/FgB9JgDM65E\n- added the ability to freely rotate examinable items\n- added a color editor dialog for fog and water colors in Graphic Options → Visuals\n- added an option for Lara to wear sunglasses (Graphic Options → Visuals → Sunglasses) (#4869)\n- added `O_SWITCH_TYPE_WHEEL`, which is similar to `O_SWITCH_TYPE_AIRLOCK` but can be used more than once\n- added `O_SMASH_OBJECT_3`, which can only be broken with triggers or the Crash Site gun\n- added `O_SMASH_OBJECT_4`, which behaves like `O_SMASH_OBJECT_1` but uses `SFX_SHUTTERS_BREAK`\n- added `O_TREX_ALPHA`, which can target raptors and be distracted by flares\n- added the ability to trigger dragons independently of Bartoli in custom levels (#5011)\n  - place an `O_DRAGON_BACK` item in the editor and trigger it normally\n  - the dragon will spawn immediately when triggered and will be one-phase, so no dagger needs to be pulled\n- added a new Lua item query helpers, `trx.items.find(query)` and `trx.items.first(query)`, with support for `object_id` and `room_num` filters\n- added a new Lua catalog, `trx.catalog.weapons`, for weapon identifiers\n- added a new Lua property, `trx.lara.equipped_gun`, to read Lara's currently equipped gun type\n- added a new Lua property, `trx.lara.target`, to read Lara's current locked target item\n- added a new Lua property, `trx.Item.flags`, to read current item flags (related to triggers)\n- added a new Lua property, `trx.Item.timer`, to read current item timer value (related to triggers)\n- added support for using more sound slots than originally possible in custom levels (#3898)\n- added support for actual quick saves with round-robin quicksave slot cycling. (#1897)  \n  Note: This feature is disabled by default and needs the player to manually bind new inputs.\n- added quick-save/load command aliases:\n  - `/save quick`, `/quicksave`, `/qs`\n  - `/load quick [slot]`, `/load q[slot]`, `/quickload [slot]`, `/ql [slot]`\n- added blood effects when enemies shoot any other creature (not just Lara)\n- added support for the globe-style level selection mechanic in the new game for level builders (#4920)\n- added an option to control how Lara swings on thin ledges (Gameplay → Controls → Slow ledge swing) (#3341)\n- added `/tp precise {x} {y} {z}` to teleport using raw world-space coordinates (no `/1024` scaling – matches TRView)\n- added the ability to use glide cameras when using TR3 camera mode\n- added an option to toggle glide cameras (Graphic Options → Visuals → Glide cameras)\n- changed PC and PS1 UI colors to no longer be hardcoded by moving it to `ui.json5` (#5003)\n- changed Fog start and Fog end to change by 10 by default, with Slow allowing 1-step precision (#5015)\n- changed `O_WINDOW_1` and `O_WINDOW_2` to `O_SMASH_OBJECT_1` and `O_SMASH_OBJECT_2` respectively\n- changed `O_MINI_COPTER` to no longer hardcode direction\n- changed Earthquake to support being reset\n- changed loading screens setting to use modes (`disabled`, `always`, `new-games`). Previously, they were hardcoded to not show for saves (#1290)\n- changed logs to no longer emit ANSI color characters when the game's output is piped to a file / process\n- changed the degenerate static mesh collision check to only apply when all axes have an empty size\n- improved error reporting for gameflow issues to now display full key paths for faulty nodes\n- fixed Lara teleporting after vaulting 2 or 3 clicks when there is a room below the target position that has no immediately adjoining portal (#4530)\n- fixed Lara attempting to jump up (using action) despite the ceiling above her making it impossible to grab any ledge (#3558)\n- fixed Lara not being able to grab ledges when under low ceilings (#4093)\n- fixed Lara sometimes falling when vaulting 2 or 3 clicks onto a ledge that has triangulation\n- fixed NG+ always forcing Lara's default equipped gun at level start even when \"remember guns between levels\" is enabled (#4711)\n- fixed not restoring Lara's back weapon mesh between levels when \"remember guns\" is enabled and a rifle-type weapon is equipped at level end\n- fixed a missing footstep sound when Lara starts to sprint\n- fixed Lara's flare undraw animation being skippable on specific late draw frames (#1593)\n- fixed UI bar scale option not updating the padding and borders (regression from 1.2)\n- fixed Blade stopping in the wrong position when anti-triggered (#4894)\n- fixed very distant Boulders causing camera shake (similar to the Tihocan crocodile targeting bug)\n- fixed drawing debug triggers using wrong orientation in some triangular geometry\n- fixed heavy triggers with no `TO_TARGET` / `TO_CAMERA` resetting cameras\n- fixed Lua `trx.catalog` only exposing `objects` and `flip_effects`; it now also exposes `lara_states`, `lara_anims`, `music`, and `samples`\n- fixed a freeze if firing a grenade very close to room portals (#4938, regression from 1.2)\n- fixed non-deterministic Inventory Ring control (transition speeds depended on v-sync / wall clock timing)\n- fixed game logic speeding up while the game was fading out after quitting\n- fixed Lara being able to shoot smashable objects located in unreachable overlapping rooms (#4949, regression from TR1X 4.14 / TR2X 1.4)\n- fixed touching Lava Wedges causing endless Flame effect spawns when the immunity cheat is on\n- fixed touching Lava tiles causing reduced Flame effect when the immunity cheat is on\n- fixed collision issues on bridges, trapdoors, breakable tiles and pushblocks if positioned over a triangle portal (regression from 1.0)\n- fixed Lara being able to sprint through swamps when responsive sprinting is enabled\n- fixed bar borders scaling poorly (off-by-1px errors, regression from 1.2)\n- fixed death counter not being preserved in saves after changing levels, causing stopwatch and final statistics to sometimes show 0 deaths\n\n**TR1**:\n- added an option to allow Lara to crouch and crawl (Gameplay → Controls → Crawling)\n- added support for monkey bars\n- changed Lara to be able to grab ealier when performing forward jumps, like TR3\n- fixed a very rare case of raptors using an incorrect death animation\n- fixed Lara unable to run around in random spots at the bottom of The Great Pyramid's starting pit\n\n**TR2**:\n- added an option to allow Lara to crouch and crawl (Gameplay → Controls → Crawling)\n- added support for monkey bars\n- changed Lara to be able to grab ealier when performing forward jumps, like TR3\n- fixed secret reward in Venice giving Magnums ammo instead of Automatic Pistol Clips (#4951, regression from 1.1)\n- fixed flickering switches and spike ceilings in Temple of Xian and Floating Islands (#4874)\n- fixed Airlock door handles not getting drawn from certain angles (#4886, regression from 1.0)\n- fixed loading screens showing before playing FMVs on most levels\n- fixed Lara not being able to move after exiting water, having used an underwater lever with the animated interactions setting enabled (#4912, regression from 1.0)\n- fixed Bell in room 48 being shootable from room 55 (#4949, regression from TR2X 1.4)\n- fixed \"Disable T-Rex Collision\" option missing from The Golden Mask (there are T-Rex enemies in Nightmare in Vegas)\n- removed the requirement to use `main.sfx` in custom levels (#3898)\n\n**TR3**:\n- added reverb support\n- added Kayak control\n- added Compsognathus control\n- added Mounted Gun control\n- added Tribe Axeman control\n- added Tribe Pipeman control\n- added Tribe Boss control\n- added Lizard control\n- added Crocodile control\n- added Carcass control (hanging Raptor)\n- added T-Rex control\n- added Raptor control\n- added Raptor Emitter control\n- added Bat Emitter control with save/load support\n- added South Pacific Mercenary control\n- added Smashable Wall control\n- added Smashable Shutters control\n- added a slide-to-sprint animation state change for Lara, similar to TR1 and TR2\n- added a new gameplay option to toggle Lara's crouch roll (Gameplay → Controls → Crouch roll)\n- added an option to allow Lara to jump out of crawlspaces (Gameplay → Controls → Crawl exit jump)\n- added crouching/crawling enhancements (Gameplay → Controls → Responsive crawling)\n  - added the ability to resume crawling more quickly after coming to a stop\n  - added transitions from run/sprint to crawl without first coming to a stop\n  - added a transition from crawl to crouch-roll without having to manually crouch first\n  - added the ability to turn while in the crouch idle state\n  - restored an unused pickup animation when in the crawling state, bypassing the crouch transition\n- added a transition from ladder to crawlspaces instead of first having to drop and re-grab the ladder (#4954)\n- fixed Uzis having wrong clips capacity (was 80, is now 40 – sorry!)\n- fixed Lara briefly switching from run back to wade when crossing from 2-click to 1-click water depth\n- fixed Lara unable to climb small ledges with low crawlspaces\n- fixed Lara using the thin-ledge swing hang animation instead of the normal hang in some 1-click ledge cases\n- fixed Lara being unable to transition from slow swing at the base of a ladder to being able to climb the ladder\n- fixed Lara's cutscene gun shots not rendering muzzle flashes, gun smoke and shell ejections (e.g., Tony cutscene)\n- fixed water ripples triggering z-fighting with 0-click ground surfaces\n- fixed footprints rendering with an excessive Y offset\n- fixed wheel switches only being usable once\n- fixed wheel switch triggers activating too early\n- fixed Kayak voiding and teleporting on large slopes\n- fixed Kayak wake effects sometimes clipping through complex geometry\n- fixed loading screens showing before playing FMVs in Antarctica\n- fixed end credits referencing non-existing image file\n- fixed Puna to no longer hardcode Lizard locations, and instead use relative offsets\n- fixed Puna's summoned Lizards counting towards total level kill count\n- fixed Tony briefly appearing for a single frame when loading a save after his death\n- fixed Lara sometimes getting stuck when crawling backwards off a tilted ledge (#4956)\n- fixed the Tribe Pipeman sometimes not being able to aim darts at Lara correctly (Gameplay → Fixes → Fix Pipeman aim)\n- fixed Lara's footprints sometimes spawning when standing on a bridge, trapdoor or pushblock\n- fixed Lara being unable to walk or sidestep at times when standing on a bridge that sits over a steep slope (regression from 1.1)\n- fixed Lara's left arm elevating when holding a flare and performing a crouch pickup (regression from 1.1)\n- removed the limitation of one Carcass instance per level working with Piranhas\n- removed the limitation of Piranhas only attacking Carcass instances if the level sequence matches Crash Site's\n- restored the ability for Lara to perform grab cancels, like TR1 and TR2\n- restored glide camera functionality\n\n\n\n## [1.2.2](https://github.com/LostArtefacts/TRX/compare/trx-1.2.1...trx-1.2.2) - 2026-02-13\n- fixed a potential `GL_OUT_OF_MEMORY` error that could occur after reloading levels many times (regression from <1.0)\n\n\n\n## [1.2.1](https://github.com/LostArtefacts/TRX/compare/trx-1.2...trx-1.2.1) - 2026-02-11\n- fixed title ring music inheriting the wrong audio volume (regression from 1.2)\n- fixed settings dialog changing size when cycling through non-scrollable tabs (regression from 1.2)\n- fixed Play Previous Level feature not restoring Lara's equipment correctly for pre-1.2 saves (regression from 1.2)\n- fixed Play Previous Level feature causing Lara to instantly die for pre-1.2 saves, when the Persistent Damage option is on (regression from 1.2)\n    Note: for those 1.0/1.1 saves, this feature will restore her health to full, as it was not stored correctly. 1.2 will continue to restore the correct HP value.\n- fixed TR2 delayed music triggers not working (regression from 1.1)\n- fixed TR3 using delayed music triggers (TR2-only feature)\n\n\n\n## [1.2](https://github.com/LostArtefacts/TRX/compare/trx-1.1...trx-1.2) - 2026-02-11\nShowcase: https://www.youtube.com/watch?v=jeq8rQONaic\n- added globe level selection mechanic\n- added Bubble Emitter control (#4629)\n- added dynamic light objects:\n    - added Red Light control\n    - added Green Light control\n    - added Blue Light control\n    - added Amber Light control\n    - added White Light control\n    - added Strobe Light control\n    - added Pulse Light control\n    - added Beacon Light control\n    - added On/Off Light control\n- added the ability in Lua to hook into control loop events during cutscenes\n- added an option to change Lara's outfit, with 20 variants included by default; custom levels can provide up to 32 outfits (Visuals → General → Lara's outfit) (#4383)\n- added an option to control UI brightness (Graphic Options → Rendering → UI brightness); renamed \"Brightness\" to \"Game brightness\"\n- added an option to allow Lara's underwater mesh tint to be more responsive based on position, as per TR3 (Visuals → General → Responsive mesh tint)\n- added the ability for custom levels to define Lara's braid position relative to her head (#110)\n- added the ability to disable manual camera (Gameplay → Controls → Manual camera)\n- added the ability to enable bouncy grenades (Gameplay → General → Enable bouncy grenades)\n- added the ability to toggle TR1/2 and TR3 projectile area damage – TR3 often deals double damage (Gameplay → Mods → Projectile Area Damage)\n- added the ability to hide pickup notifications in the bottom-right corner (Graphic Options → UI → Pickups overlay)\n- added a new Lua event, `trx.events.on_game_start`, which fires when the level finishes loading and the game is about to start\n- added a new Lua function, `trx.rooms.flip()`, to toggle the flip map (#4704)\n- added a new Lua function, `trx.rooms.flip_effect()`, to set the active flip effect with an optional timer (#4704)\n- added a new Lua catalog, `trx.catalog.flip_effects` for name-based flip effect catalog IDs\n- added a new Lua music play mode, `trx.music.PlayMode.OVERLAY` for playing on top of currently played track\n- added new Lua catalogs for Lara states, Lara anims, music, and samples\n- added a new Lua module, `trx.camera`, with camera getters and `trx.camera.shake()`\n- added a new Lua property, `trx.rooms.Room.num`\n- added support for cross-fades to the title screen\n- added visual previews of bar colors (Graphic Options → Bars)\n- added the ability to change PS1 bar colors\n- added shadow rendering to all cutscene actors\n- added endless sprint (available previously via the `/restless` command) to the UI settings (Gameplay options → Mods → Endless sprint)\n- added endless flare time cheat (Gameplay options → Mods → Endless flare time)\n- added `O_VULTURE` for custom levels\n- added `O_ROLLING_BALL_4` (giant Temple of Puna boulder) for custom levels\n- added an option to control whether or not moving boulders should shake the camera (Gameplay options → General → Enable boulder shake)\n- added an option to make Lara stumble if she hops backwards and there is a slope behind her (Gameplay options → Controls → Backwards slope stumble)\n- added `/trigger` and `/untrigger` console commands, with support for targeting by item ID, item name, or object name\n- added the ability to seek backwards through cutscenes with left button\n- added the ability to trigger collapsible tiles from heavy triggers, regardless of Lara's position (#4807)\n- added floor height change detection for boulders when stopped, so they will drop if the floor below them drops (#4808)\n- added splash effects to neutral twists and rolls (#4793)\n- improved rendering performance\n- improved the ability to seek through cutscenes to support even faster seeks (Slow = ±1 s, default = ±5 s, new: Draw = ±15 s)\n- improved `/tp` to accept `room`/`item` prefixes and `rN`/`iN` shortcuts\n- improved inventory ring active item highlight for smoother appearance\n- improved savegame file size by reducing it about 20–30%.\n- improved indentation for nested bullets in the UIs\n- changed `debug.debug_cuboids` option name from \"debug cuboids\" to \"debug bounding boxes\" (`/debug bounding-boxes` or `/set debug-bounding-boxes 1`)\n- changed `debug.enable_debug_pos` option to split into `enable_debug_pos` and `enable_debug_anim`\n- changed `debug.enable_invulnerability` option to only show the marker if the setting `enable_debug_status` is on (off by default) (#4631)\n- changed `audio.load_music_triggers` (Gameplay → Fixes → Fix one-shot music triggers) to be enabled by default\n- changed photo mode to no longer show \"Entering photo mode\" in the console\n- changed photo mode to always display a red frame around the game view when active (not visible in screenshots)\n- changed stats dialog to include allies in kill count if they turn hostile. This applies to all levels that follow, and the final stats screen.\n- changed rooms-to-draw tracking to no longer stop at the 100-room limit\n- changed boulders to stop if the ceiling height is lower than their height\n- changed all UI bar colors from hardcoded to configurable via `cfg/ui.json5`, enabling some customization for PS1 bars\n- changed `/debug [0|1]` command to no longer spam about settings that aren't changed\n- changed `/set` command to always use hyphens for enum option values, and accept both underscores and hyphens\n- changed Lua catalog keys to strip `O_` prefixes and use lowercase\n- changed Lua event callback names to be more consistent:\n    - `on_level_init` → `before_level_file`\n    - `on_level_start` → `after_level_file`\n    - `on_level_load` → `after_level_state`\n    - `on_control` → `before_control`\n    - `on_control_post` → `after_control`\n- changed turbo cheat to auto‑reset to normal speed if pushed past limit, making it easier for new players to recover from accidental changes\n- changed Blades to support being reset\n- changed the barefoot SFX option toggle in TR2 to no longer require reloading the level for changes to take effect\n- changed triggers that target pickup items to support antitriggers, switches and bitmasks\n- removed support for legacy (TombATI / TR2 GOG/Steam) and pre-1.0 (TR1X/TR2X) savegame files\n- fixed random face dropouts on levels with more than 32k textures\n- fixed a small hiccup when launching the game on certain GPUs\n- fixed inconsistent music volume in the statistics screens (#4499)\n- fixed shadows to support 60 FPS interpolation\n- fixed soft static mesh collision not working right with statics that appear in overlapping rooms\n- fixed drawing debug triggers using random tint near water sources\n- fixed drawing debug triggers glitching through triangular portals\n- fixed Lara being force-resurfaced near split-triangle water portals in certain spots\n- fixed custom levels that contain invalid room static mesh references not being able to load (#4770)\n- fixed the tip of Lara's braid using an invalid offset position on the first frame of a level (#4821)\n- fixed drawing shadows twice when item intersects a portal (#4640, regression from 1.0)\n- fixed drawing circle/octagon shadows in TR2/TR3 cutscenes using wrong positions\n- fixed being unable to use the manual camera in TR3 camera mode when Lara is idle (#4670, regression from 1.1)\n- fixed grenades not killing more than a single enemy\n- fixed running `/title` and similar commands leaving the \"Examine\" button briefly visible in the key items ring (old regression)\n- fixed running `/title` and similar commands when examining an item causing incorrect item rotation next time the ring opens (old regression)\n- fixed endless sprint cheat setting not retained between game relaunches\n- fixed Cobras not being counted in level kill count\n- fixed stats dialog retaining friendly status for allies that become enemy types in later levels, causing them to get excluded from kill count\n- fixed targeting hostile ex-allies not working if \"Enable ally targeting\" option is off\n- fixed `/play` and similar commands fading out instead of running instantly on stats/title screens\n- fixed `/play` and similar commands sometimes preserving cutscene camera tilt if invoked while a cutscene was paused\n- fixed Cheats description showing arrows in the indented bullets (#4753, regression from TRX 1.1)\n- fixed game freezing on exit on certain platforms when there are no active sound devices (SDL bug)\n- fixed Lara twitching when trying to step back onto death tiles\n- fixed Lara's look head rotation/tilt limits being hardcoded to the engine version rather than camera mode\n- fixed Lara rotating around an incorrect origin in photo mode during cutscenes\n- fixed pushblocks being able to fall into rooms below despite no portals being present (#4788, regression from TR1X 4.15/TR2X 1.5)\n- fixed one-shot triggers for hidden pickup items making the items permanently invisible (#4784)\n- fixed secret tracks played at low quality when \"fix secrets killing music\" option is on\n- fixed secret tracks not restored from the savegame when \"fix secrets killing music\" option is on\n- fixed slow-forward seeking through cutscenes (right+slow) not working (regression from 1.0)\n- fixed statics marked collidable but with zero‑size hitboxes causing phantom collisions\n- fixed Lara being displaced during the sprint-slide animation if she tried to pick up an item at the same time (#4843, regression from TR1X 4.14, TR2X 1.4)\n\n**TR1**:\n- added Unfinished Business loading screens (#1310, thanks to rockahub)\n- fixed save crystal reflections rendering upside down (regression from 4.14)\n- fixed underwater wobble effect acting twitchy with camera movement\n- fixed several texture issues on each of Lara's outfits and guns\n- fixed gun injections overwriting Lara's footstep SFX in all levels (#4733, regression from 1.1)\n- fixed pushblocks in Natla's Mines becoming unusable after loading a save made in earlier versions (#4735, regression from 1.1)\n- fixed low-quality texture palette on injected TR2/3 weapons and flares\n- fixed baddie speeches played at low quality when \"fix speeches killing music\" option is on\n- fixed baddie speeches not restored from the savegame when \"fix speeches killing music\" option is on\n\n**TR2**:\n- added \"Sound Options → Misc → Layered secret music\" option\n- added \"Gameplay → Fixes → Fix one-shot music triggers\" option\n- changed Assault Course stats to show scroll indicators (#3510)\n- changed statistics screen rows to be more compact\n- fixed wrong line played when finishing the Assault Course for the first time (#4667, regression from 1.1)\n- fixed underwater wobble effect acting twitchy with camera movement\n- fixed several texture issues on each of Lara's outfits and guns\n- fixed a deviation in water current behaviour that could result in Lara stopping too early (#4706, regression from TR2X 1.1)\n- fixed gun injections overwriting Lara's footstep SFX in underwater levels (#4733, regression from 1.1)\n- fixed exploding Armed Snowmobile not disappearing the vehicle (#4762)\n- fixed the polar bear in Furnace of the Gods twitching if killed when in its reared state (#4624)\n- fixed incorrect textures on the MP5 when equipped or on Lara's back\n\n**TR3**:\n- added \"Sound Options → Misc → Layered secret music\" option\n- added \"Gameplay → Fixes → Fix one-shot music triggers\" option\n- added new UI bar appearances, \"TR3 PC\" and \"TR3 PS1\" (Graphic Options → Bars → Bars appearance)\n- added new water currents\n- added new blood effects\n- added underwater blood spills\n- added poison mechanic\n- added heal crystals\n- added animated puzzle holes support\n- added new creature explosions effects\n- added meteorite artifacts support\n- added examine item feature for certain items\n- added Monkey control\n- added Shiva control\n- added Tony control\n- added Spikes animation in Coastal Village and Madubu Gorge\n- added Electric Fence control\n- added Aldwych Drill control (Spike Ceiling with timer=1 to descend faster)\n- added TR3 behavior patterns to Tiger control\n- added Kill All Triggered control\n- added Vulture control\n- added Boulder control\n- added Poison Dart control\n- added Earthquake control\n- added dynamic light objects:\n    - added Red Light control\n    - added Green Light control\n    - added Blue Light control\n    - added Amber Light control\n    - added White Light control\n    - added Strobe Light control\n    - added Pulse Light control\n    - added Beacon Light control\n    - added On/Off Light control\n- added Lara's backwards-hop stumble if there is a slope behind her\n- added \"Sound Options → Misc → Layered secret music\" option\n- improved look camera stability to reduce idle-breathing camera bobbing/roll\n- improved Monkeys to no longer hardcode hostility status based on Tiger presence\n- changed Assault Course stats to show scroll indicators (#3510)\n- changed statistics screen rows to be more compact\n- changed hostile Monkeys to share hostility status, like TR2 Barkhang monks (the original TR3 behavior can be restored in Gameplay → General → Ally hostility policy)\n- changed enemy drops to appear at the tile center, to conform with the OG\n- fixed several texture issues on each of Lara's outfits and guns\n- fixed actors jumping to their start frame at the end of cutscenes\n- fixed Flame in Cutscene 4 and 6 appearing static\n- fixed Swamp Map rotation\n- fixed seaweed disappearing too quickly in certain levels\n- fixed Hand of Rathmore not rotating in Sleeping with the Fishes\n- fixed Icicles not having sound\n- fixed Spike Walls not having sound\n- fixed colored exhaust smokes on Quad Bike for 1 frame\n- fixed Cobras and Rattlesnakes being immune to explosives in their sleeping state\n- fixed Quad Bikes not restoring their state from savegames properly\n- fixed exploding Assault Targets in Lara's Home counting as penalties\n- fixed surface and underwater effects simulation speed\n- fixed underwater wobble effect amplitude\n- fixed animated textures speed\n- fixed inconsistent Meteor Artifacts names\n- fixed wrong item selection sound in the inventory ring\n- fixed flame emitters not getting restored when loading from a save\n- fixed Lara holding onto ledges after dying if the Action key wasn't released\n- fixed Shiva death smoke effects getting misplaced if the player saves and reloads mid-battle\n- fixed Grenade, Rocket Launcher, and Harpoons damage\n- fixed being unable to antitrigger Poison Dart Emitters\n- fixed ally Lua API not working with most of the TR3 enemies supported so far\n- fixed one-shot antitriggers / antipads behavior\n- fixed Blades in Coastal Village not respecting antitrigger\n- fixed some Poison Darts disappearing 1 frame early\n- fixed running down an enemy with a Quad not counting as a kill\n- fixed killing Cobras with a manually-aimed projectile not counting as a kill\n- fixed smoke and spark rotation snapping at 180° instead of rotating smoothly\n- fixed Lara burning instead of getting electrocuted when touching the top of the electric fence\n- fixed driving over Winston with a Quad Bike or shooting him with the Harpoon Gun causing him to bleed\n- fixed driving over Assault Target with a Quad Bike or shooting it with the Harpoon Gun causing it to spawn blood\n- fixed skybox data in Scotland TR3:LA levels to show correct top and bottom colors\n\n\n\n## [1.1](https://github.com/LostArtefacts/TRX/compare/trx-1.0.3...trx-1.1) - 2026-01-17\nShowcase: https://www.youtube.com/watch?v=veVYyr--H1A\n- added a fade-in and fade-out effect to patterned inventory backgrounds\n- added the ability to use monochrome image for inventory and statistic screens backgrounds\n- added the ability to use very dark image for inventory and statistic screens backgrounds (#4469)\n- added the ability to change pause screen background\n- added the ability to control whether or not allies are hostile towards Lara via Lua (#3873)\n- added the ability to control via Lua which enemies are allies and which are ones that will fight with allies (#3873)\n- added the ability to control Lara's air timer via Lua (#4592)\n- added the ability to fine-tune the fade effects between the inventory ring, the pause screen, and the stats screen (Graphic Options → UI → Inventory/Pause/Stats fade effects)\n- added gamma control (TR3-style) to all games (Graphic Options → Rendering → Gamma)\n- added support for TR3 weather effects to all games (#3881)\n- added support for 3D secret objects, and provided defaults for OG levels in TR2 (#4380)\n- added catalog object IDs to Lua\n- added the ability to swap meshes in Lua\n- added support for locked cameras, similar to TR4+ (#2040)\n- added support to use `O_DINO_WARRIOR` and `O_FISH` as aliases for `O_TREX` and `O_BARRACUDA` respectively\n- added the ability to define gun types, flash shade and offset positions in `cfg/weapons.json5`\n- added the ability to define ammo pickup quantities per weapon in `cfg/weapons.json5` (#4518)\n- added a new input, that lets the player toggle in-game textures on/off, available by default under F8\n- added a new console command, `/textures`, that lets the player toggle in-game textures on/off\n- added a new console command, `/weather`, that lets the player control the weather\n- added a new console command, `/spawn`, that lets the builder spawn an entity of their choice to test things around\n- added Animating Item 1-6 control\n- added the option to use TR3 sprite-based shadows (Visuals → Shadows shape)\n- added an option for soft static mesh collision; this also allows for arbitrary mesh rotation in custom levels and retaining accurate collision (Gameplay → Controls → Soft mesh collision) (#3654)\n- added an option to use the TR3 camera (Visuals → Camera Mode)\n- improved a fade-in and fade-out effect on loading screens – they now smoothly transition to the game screen\n- improved fog behavior to be less dependent on camera rotation\n- changed the 3D pickups option to try the simplified 3D meshes first, if available, before falling back to inventory items\n- changed the 2D and 3D statics limit from 256 to unlimited\n- changed the lighting contrast key binding to F9\n- changed underwater statics to be affected by caustics, even if they don't get merged into level geometry (#4430)\n- changed Magnums and Automatic Pistols to be separate objects, so both can appear in the same level (#4475)\n- changed the M16 and MP5 to be separate objects, so both can appear in the same level\n- changed the swinging axe to be defined separately from other pendulums (use object `O_SWINGING_AXE` in catalogs)\n- changed the following trap types to support being reset (#3993)\n  - collapsible tiles\n  - Damocles swords\n  - ember emitters\n  - falling ceiling\n  - hooks\n  - icicles\n  - lava wedge\n  - pendulums\n  - pushblocks (via timed triggers only)\n  - spike ceilings\n- changed the fonts to no longer use hardcoded character widths\n- changed the fonts to use dedicated sprites for accented characters instead of composing them at runtime\n- changed the fonts to use dedicated sprites for similar-looking characters instead of using aliases\n- changed the reset keybindings bars appearance to be more visible\n- changed the default exposure bar PC color to blue 2\n- changed lua music `PlayMode` constant names\n- removed the `scripting/trx` directory – internal TRX LUA scripts now get embedded in the exe\n- fixed broken final statistic counters (#4432, regression from 1.0)\n- fixed undefined behavior (crashes and/or texture glitches) in levels with a lot of textures\n- fixed a crash if a pickup aid spawns against an item whose 3D model isn't present\n- fixed Bacon Lara not always being drawn perfectly in sync with Lara's animation (#4210)\n- fixed gondolas not being drawn with an underwater tint when they have sunk (#4428)\n- fixed the teleport-to-item command not succeeding if used in succession with the same type and an out of bounds item is encountered (#4468)\n- fixed skybox faces with transparent pixels always rendering in front of all other faces (#4351, regression from 1.0)\n- fixed unbound inputs not being saved between game launches (#4360, regression from TR1X 4.14/TR2X 1.4)\n- fixed Lara drawing a flare when the draw weapons input is pressed, and she already has an active flare but no weapons (#4361, regression from TR2X 1.4)\n- fixed wading splashes spawning when using the fly cheat (#4400, regression from 1.0)\n- fixed grenades not exploding floating water creatures (#4399, regression from TR2X 1.3)\n- fixed water enemies not getting tinted when dead and floating (#4407, regression from 1.0)\n- fixed Lara not colliding with mines/gondolas when underwater (#4424, regression from TR2X 1.3)\n- fixed flare box pickups containing only one flare if Lara has none in her inventory at that time (#4423, regression from 1.0)\n- fixed water enemies appearing untinted for a frame after dying and moving to the water surface (#4420, regression from TR2X 0.1)\n- fixed the interactive fly cheat breaking with animated interactions enabled (#4444, regression from TR1X 4.14)\n- fixed switch triggers using an incorrect state check, which could result in fixed camera behavior that deviated from OG (#4456, regression from 1.0)\n- fixed ambient music triggers to no longer kill active normal music tracks (#4463)\n- fixed game crashing when Lara passes through light sources in certain levels\n- fixed waterfall mist not brightening when holding a flare (#4486)\n- fixed resetting camera in the photo mode not clearing the underwater tint\n- fixed developer console text editing (backspace, moving the caret) doing weird things with Unicode characters\n- fixed Lara jumping if player holds the swim button when exiting the fly cheat (#4470)\n- fixed game refusing to load savegames made with the JP mode (#4558)\n\n**TR1**:\n- added the ability to change inventory and statistics background styles (pattern + wave are not implemented in TR1)\n- added Automatic Pistols, the Desert Eagle, the MP5, and the Rocket Launcher to the `/moreguns` console command\n- fixed Lara standing two clicks below `O_FALLING_BLOCK_3` items rather than directly on top (#4374)\n- fixed missing menu guns SFX in Lara's Home\n- fixed several OG texture issues in Caves (rooms 0, 1, 2, 6, 24, 30 and 32)\n- fixed Lara automatically being given TR2 weapons in NG+ when playing the OG levels (#4365, regression from 1.0)\n- fixed Lara's pistol holster meshes appearing in NG+ in place of her Uzi holster meshes (#4368, regression from 1.0)\n- fixed Lara's footstep sounds being very quiet when weapons are equipped (#4451, regression from 1.0)\n- fixed the grenade blast SFX not always playing in succession (#4628, regression from 1.0)\n\n**TR2**:\n- added unused gym voice line at level start if Lara has any logged assault course attempts (#2822)\n- added high-resolution 16:9 and 4:3 loading screens\n- added high-resolution 16:9 and 4:3 game end screen  \n    To download the new images ahead of a stable release, please see the [TRX data](https://github.com/LostArtefacts/TRX-data) repository.\n- added Magnums, the Desert Eagle, the MP5, and the Rocket Launcher to the `/moreguns` console command\n- changed Tibetan Foothills to have snow (you can disable this via Graphic Options → Visuals → Weather)\n- changed ember emitters to use the `SFX_LAVA_FOUNTAIN` sample (#4376)\n- fixed the scuba diver's death SFX not playing (#4386)\n- fixed a missing trigger for tiger 6 in Ice Palace (#4390)\n- fixed missing music triggers in Venice room 11 and Floating Islands room 80\n- fixed a missing death tile in Floating Islands room 91\n- fixed vertex lighting and stretched textures in Lara's Home room 28 and Home Sweet Home room 27\n- fixed z-fighting on fences in Barkhang Monastery and gondola poles in Venice\n- fixed missing oxygen tanks in Offshore Rig room 82\n- fixed the monk in the Diving Area cutscene not having a complete death animation\n- fixed demos not using loading screens\n- fixed reading room lights for custom TR2 levels (regression from 1.0)\n- fixed the switch in room 46 of Opera House randomly disappearing\n- fixed game crashing when Lara passes through light sources in levels compiled with dxtre3D\n- fixed Snowmobile music not getting resumed (#4519)\n- fixed Stopwatch position in the inventory ring (#2014)\n- fixed static lighting on broken ice/windows (#4506, regression from 1.0)\n\n**TR3**:\n\nA lot of our TR3 work builds on *TOMB3*, which Troye and ChocolateFan kindly let us dive into and expand on.\nTheir hard work gave us the perfect base to push TRX further, and made the climb a lot less vertical!\n\n- added support for monkey bar mechanics\n- added support for crawlspace mechanics\n- added RGB lighting system support\n- added flame effects\n- added swamp and water surfaces wave effect\n- added underwater caustics\n- added proper bubbles\n- added water splash and ripple effects\n- added waterfall mist effect\n- added per-mesh underwater tinting (Lara only)\n- added `cdaudio.wad` music playback support\n- added weather effects\n- added sprite-based shadows\n- added footprints\n- added surface-based step sounds\n- added cold breath effects\n- added gun shells\n- added gun projectiles\n- added gun smoke effects\n- added new ricochets\n- added flare lighting and sparks\n- added monochrome inventory backgrounds\n- added TR3 inventory ring lighting\n- added high-resolution 16:9 and 4:3 loading screens\n- added high-resolution 16:9 and 4:3 title and game end screens\n- added high-resolution 16:9 and 4:3 credit images\n    To download the new images ahead of a stable release, please see the [TRX data](https://github.com/LostArtefacts/TRX-data) repository.\n- added support for the serif font\n- added support for colored text\n- added Assault Course and Race Track course mechanics\n- added Quad Bike control\n- added Animating Item 1-6 control\n- added Electrical Light control\n- added Smoke Emitters control\n- added Steam Emitter control\n- added Flame Emitter 1-3 and Side Flame Emitter control\n- added Piranhas and Tropical Fish control\n- added Desert Eagle control\n- added MP5 control\n- added Rocket Launcher control\n- added Magnums, the Automatic Pistols, and the M16 to the `/moreguns` console command\n- added all weapons to Lara's Home (accessible with cheats or via the console only)\n- added Assault Course target control\n- added Assault Course penalty system\n- added an option to fix the MP5 accuracy while running\n- added TR3 camera control and look functionality\n- improved run-to-crawl transition\n- improved text colors of the Assault Course statistics and timers\n- improved Assault Course targets to spawn ricochets\n- changed The River Ganges, City and All Hallows to have rain\n- fixed sample reading to support correct pitch and volume\n- fixed pool edges shifting along with the water effect\n- fixed Lara's thigh being drawn when a flare is in Lara's hand or has been discarded\n- fixed gun flashes being drawn in white\n- fixed disabling lighting system not working\n- fixed skybox data to show correct top and bottom colors\n- fixed Assault Course timer remaining indefinitely on screen\n- fixed Quad Bike low visibility of exhaust smokes at high speeds\n- fixed Quad Bike wheels appearing to spin backwards at high speeds\n- fixed the skybox's blue lid for the Thames Wharf and City cutscenes\n- fixed fish schools to no longer swim at supersonic speeds if their triggers do not have timers set, or reuse the same timer\n- fixed Lara letting go of some ledges\n- fixed shadow sizes dependent on Lara's placement instead of their owner's\n\n## [1.0.3](https://github.com/LostArtefacts/TRX/compare/trx-1.0.2...trx-1.0.3) - 2025-11-27\n- fixed the conveyor belt fuse in Natla's Mines not appearing after using the nearby switch (#4349, regression from 1.0)\n\n## [1.0.2](https://github.com/LostArtefacts/TRX/compare/trx-1.0.1...trx-1.0.2) - 2025-11-26\n- fixed Lara being unable to interact with keyholes after picking up an item if animated interactions are enabled (#4342, regression from 1.0)\n\n## [1.0.1](https://github.com/LostArtefacts/TRX/compare/trx-1.0...trx-1.0.1) - 2025-11-25\n- changed default master volume to 80% in TR2 to match TR1 (#4337)\n- fixed 2D sprites not appearing in the UI (#4338, regression since 1.0)\n\n## [1.0](https://github.com/LostArtefacts/TRX/compare/76109a8855da99f3304ca4d9a3f5882dada2dd40...trx-1.0) - 2025-11-23\nShowcase: https://youtu.be/vVU9vbUXTXc\n**Common**:\n- added LUA scripting engine\n    Supports basic events, item interactions, teleporting and much more.\n    See [the documentation](../trx/lua) for details.\n- added a game flow option for cold water in custom levels, similar to TR3 (#4021)\n- added a splash effect when Lara jumps in wading depth water, similar to TR3+ (#3975)\n- added bounding box debugging (`/debug 1` or `/set debug-cuboids 1`)\n- added support for object, music, sound, flip effects, Lara state, and Lara animation slots overrides through CSV catalogs  \n    Lets builders link hardcoded logic to slots of their choice, allowing object sharing between games (for example, use TR1 bats in TR2).  \n    This feature is experimental — some objects may not behave correctly. Please report any bugs encountered! 🩷  \n    See [the documentation](../CATALOGS.md) for details.\n- added `enable_debug_camera` setting that shows camera position in realtime (reachable via `/debug` and `/set`)\n- added the ability to fast-forward through cutscenes with the right button (+5 s) or with slow+right (+1 s)\n- added support for dark theme on Windows\n- added support for triangular geometry\n- added support for additive blending in textures\n- added support for quicksand rooms\n- improved bilinear filtering for smoother edge blending when multiple objects overlap in depth\n- improved rendering of statics and items in overlapping rooms (#2005)\n- improved ricochets placement\n    - fixed dart and disc ricochets being placed mid-air (#4063)\n    - fixed ricochets not showing on slopes\n- improved bar setting UIs in various ways\n    - added two new options: \"Show bars\" on/off, and \"Flash bars\" on/off\n    - changed the bars options to be placed in its own tab\n    - changed the appearance labels to better align with expectations (#4025)\n    - removed the look modes for every bar (except enemy bars that retain the \"boss only\" setting)\n    - fixed health bar flicker on medi packs when cycling the inventory ring (#4211, regression from TR1X 4.14 / TR2X 1.4)\n- changed the `/debug` command to accept optional option name argument (for example: `/debug pos 1`)\n- changed dart emitters and disc emitters to have separate slots (so with catalogs, both can be used in the same level simultaneously)\n- changed the debug position UI to no longer be hidden in photo mode\n- changed the unrestricted look mode option to include Lara being able to look freely while shooting an enemy (#4090)\n- changed the `ambient_tracks` property to be only available on the root level\n- changed music triggers that match the level's default ambient track to automatically be treated as ambient if omitted from `ambient_tracks` (#4181)\n- changed the `-q`/`--quiet` argument to no longer silence warnings\n- changed the `Remember Guns between Levels` option to also apply to whether or not Lara starts with those guns equipped\n- changed the FOV formula to be consistent between games\n    - changed the FOV default increment from 10 to 5 (#4026)\n    - removed \"Vertical FOV\" option\n    - removed \"Use PS1 FOV\" option\n- fixed missing footstep sound effects when Lara climbs off a ladder and when she finishes a handstand (#4030)\n- fixed a crash in custom levels if a flip effect that expects to act on an item is used in a regular trigger (#4085)\n- fixed a crash if trying to kill an enemy by name but there is no naming definition for that object\n- fixed photo mode camera clipping through overlapping rooms (#1674)\n- fixed bogus warnings about resume info in logs when playing cutscenes and in the title level\n- fixed title bar size being too small on HiDPI screens on Windows platform (#2837)\n- fixed statics and items not getting rendered when all portals leading to them are offscreen (#2005)\n- fixed Lara's arms getting stuck in the M16 gun firing animation while she dies (#4130)\n- fixed Lara jittering in the QWOP state\n- fixed doors and trapdoors not interpolating when using the door cheat\n- fixed credit images and loading images showing black screen if the file is missing (#4325)\n- fixed caustics not affecting underwater plant sprites (#4317)\n\n**TR1**:\n- added a new easter egg command\n- added support for flares (for OG levels, use `/give flare`) (#4121)\n- added support for TR2 weapons (for OG levels, use `/give moreguns`)\n- added support for custom levels to use Lara's extra animations from TR2\n- added new hidden settings (available via LUA and the `/set` command):\n    - `flow.lockout_option_ring`\n    - `flow.load_save_disabled`\n    - `flow.play_any_level`\n    - `flow.cheat_keys`\n- added the ability for the sound system to use Lara's position instead of camera's position (#1438) (Sound Options → Misc → Microphone near Lara)\n- added an option to use TR2-style inventory ring backgrounds in custom levels (Graphic Options → UI → Inventory background) (#4264)\n- added an option to use TR2-style statistics dialog backgrounds in custom levels (Graphic Options → UI → Stats background) (#4264)\n- improved the positions of some 3D pickup items, such as the scion that Pierre drops\n- improved the quality of the PS1 Uzi SFX (#4024)\n- changed the following game flow options to become hidden settings (available via LUA and the `/set` command):\n    - `flow.demo_delay`\n    - `gameplay.enable_killer_pushblocks`\n- changed Select Level and Story So Far features placement to the New Game menu\n- changed the input buffering option to separately tackle F-keys and Inventory (Gameplay → Input → Buffering (F-keys), Gameplay → Input → Buffering (Inventory))\n- changed exploded meshes to trigger a splash effect when they hit water, similar to TR2\n- changed LOS algorithm to TR2+ implementation\n- changed save crystal collision to make them easier to activate\n- changed cutscene data (e.g. `cut1.phd`, as opposed to in-game cinematics) to match TR2 format, where Lara (as `O_LARA`) must be defined as an item in the level file\n- changed the Remove shotguns, Remove Uzis and Remove Magnums into a single \"Remove extra guns\" option\n- changed toggling Lara's braid in-game to swap out her head and torso meshes appropriately without the need to reload the level (#2399)\n- removed the `Enhanced shotgun targeting` option in favour of using the common weapon lock mode (Gameplay → Controls → Weapon lock mode)\n- fixed Lara being able to push blocks through toggle opacity 1 portals (#4129)\n- fixed Lara drifting during the Atlantis cutscene while the camera focuses in on Natla (#4153)\n- fixed Lara retaining her hit animation if nudged by an enemy at the same time as starting a special animation such as picking up a scion (#4212)\n- fixed Lara being drawn if the explosion cheat has been used and Bacon Lara is active (#4148)\n- fixed ambient music not playing in demo levels (#4046, regression from 4.13)\n- fixed caustics stopping after spending roughly 12 minutes in a level (#4109, regression from 4.10)\n- fixed legacy UB crashing the game (#4113, regression from 4.12)\n- fixed select level feature to also be available to games started with `/play`\n- fixed select level feature slot status not updated on save\n- fixed \"Story So Far...\" showing loading screens\n- fixed matrix stack overflow crash when moving through overlapping or dome portals (#2685)\n- fixed the gun-draw SFX playing when holstering the shotgun (#3755)\n- fixed Lara's braid remaining reflective if the fly cheat is used to resurrect her on the Midas hand\n- fixed invulnerability cheat not getting disabled during the demos (regression from 4.13)\n- fixed crash when loading OG saves made in City of Khamoon, while the \"Restore PS1 enemies\" option is on (#4217, regression from 2.16)\n- fixed the jump lock mode UI option remaining visible when responsive jumping is disabled (#4027, regression from 4.13)\n- fixed a slight misalignment in the PDA in its open state (#4247)\n- fixed Lara being unable to exit the water in (for example) room 41 of Return to Egypt (#4315, regression from 4.12)\n\n**TR2**:\n- added loading screens (Gameplay Options → General → Loading screens) (#1620)  \n    Tomb Raider II 3×2 upscales done by Arsunt.  \n    Tomb Raider II: The Golden Mask images done by Lito Perezito.\n- added Restart Level option when Lara dies (#1555)\n- added Play Previous Levels feature (available in the New Game screen)\n- added Story So Far… feature (available in the New Game screen)\n- added game mode selection to the Play Any Level feature\n- added support for custom levels to use Lara's extra animations from TR1\n- added an option to use TR1-style inventory ring backgrounds (Graphic Options → UI → Inventory background) (#3923)\n- added an option to use TR1-style statistics dialog backgrounds (Graphic Options → UI → Stats background) (#3923)\n- added extended statistics support (#2578)\n    - added pickup count and death count support in the stats screen (Graphic Options → UI → Statistics details)\n    - added max pickup, secret and kills support (Graphic Options → UI → Statistics details)\n    - added deaths counter support (Gameplay Options → General → Count Lara's death)\n    - added unobtainable secrets, pickups and kills stats support in the gameflow\n- added an option to disable final statistics (Gameplay options → General → Final statistics screen)\n- added an option to disable all medipacks (Gameplay options → Mods → Remove medipacks)\n- added an option to disable all guns except Pistols (Gameplay options → Mods → Remove extra guns)\n- added an option for pickup aids, which will show an intermittent twinkle when Lara is nearby pickup items (Graphic Options → Visuals → Pickup aids) (#4057)\n- added an option for animated interactions with pickups and switches (Gameplay → Controls → Animated interactions) (#4067)\n- added an option to change max savegame slot count (Gameplay → General → Number of save slots)\n- added an option to turn off Inventory input buffering (Gameplay → Input → Buffering (Inventory))\n- added an option to turn on TR1-style F-keys input buffering (Gameplay → Input → Buffering (F-keys))\n- added an option to draw Shotgun flashes (Graphic Options → Visuals → Shotgun flash)\n- added support for TR1-like secret triggers (#2047)\n- added support to disable wading, like TR1 (Gameplay → Controls → Wading) (hidden by default)\n- added support to disable responsive running jumps, like TR1 (Gameplay → Controls → Responsive jumping)\n- added support to disable responsive swim cancel, like TR1 (Gameplay → Controls → Responsive swim cancel)\n- added support for game-flow defined enemy item drops, similar to OG TR1 levels; regular level-defined drops will continue to work normally\n- improved the quality of the PS1 barefoot SFX (#4024)\n- changed the following game flow options to become hidden settings (available via LUA and the `/set` command):\n    - `flow.lockout_option_ring`\n    - `flow.load_save_disabled`\n    - `flow.play_any_level`\n    - `flow.demo_delay`\n    - `flow.cheat_keys`\n    - `gameplay.enable_killer_pushblocks`\n- changed the Pause key to no longer work when Lara's dead (similar to TR1)\n- changed sprites to respect the water tint if placed underwater\n- removed the following game flow options:\n    - `cmd_init`\n    - `cmd_title`\n    - `cmd_death_in_demo`\n    - `cmd_death_in_game`\n    - `cmd_demo_end`\n    - `cmd_demo_interrupt`\n    - `single_level`\n    - `is_demo_version`\n- fixed the new game modes dialog requiring the Action key to show up (TR2 dialogs don't need this.)\n- fixed Lara's pistols not being removed from holsters when she equips during a cutscene (#4136)\n- fixed potential softlocks in custom levels with enemies who have end-level flip effects but the player uses the grenade launcher to kill them (#4261)\n- fixed Lara not having holstered pistols after she changes costumes in the Diving Area cutscene (#4142)\n- fixed ambient music not playing in demo levels (#4046, regression from 1.3)\n- fixed twists not adhering to original game movement (#4078, regression from 1.4)\n- fixed legacy saves in Opera House and Vegas crashing the game (#4103, regression from 1.5)\n- fixed caustics stopping after spending roughly 12 minutes in a level (#4109, regression from 1.4)\n- fixed Lara being able to push blocks through toggle opacity 1 portals (#4129, regression from 1.5)\n- fixed pistols disappearing from Lara's holsters in the cutscene following The Great Wall (#4145, regression from 0.9)\n- fixed Lara's thigh meshes defaulting if entering the fly cheat while holding a flare and she doesn't currently have holstered weapons (#4143, regression from 1.3)\n- fixed wrong lighting of exploded body parts\n- fixed weird clipping when moving through overlapping or dome portals (#2685)\n- fixed Lara reloading the harpoon gun if she draws the weapon and does not have any harpoons (#4259)\n- fixed Lara holding on to the grenade launcher for too long when undrawing it (#3474)\n- fixed the holster SFX playing when drawing rifle type weapons (#3755)\n- fixed missing SFX in the harpoon drawing and undrawing animations (#3755)\n- fixed invulnerability cheat not getting disabled during the demos (regression from 1.3)\n- fixed disable targeting allies option not working (#4184, regression from 1.5)\n- fixed Lara losing forward momentum on springboards when the wall glitch mode option is set to `Fixed` (#4187, regression from 1.2)\n- fixed the M16 accuracy option not taking effect until restarting the game (#4227, regression from 0.3)\n- fixed incorrect keys object orientation in the inventory ring (#4239, regression from 0.3)\n- fixed underwater hum when Microphone near Lara option is on (#2188)\n- fixed 3D pickups not rendering if the associated sprite is not present in the level file (#4275, regression from 0.6)\n\n**TR3**:\n- added basic TR3 level loader (nothing is working yet!)\n"
  },
  {
    "path": "docs/CHANGE_SUBMISSION.md",
    "content": "# Submitting changes\n\n## Pull requests\n\nWe commit via pull requests rather than directly to the protected `develop`\nbranch. Each pull request undergoes a peer review and requires at least one\napproval from the development team before merging. We ensure that all\ndiscussions are resolved and aim to test changes prior to merging. When a code\nreview comment is minor and the author has addressed it, they should mark it as\nresolved. Otherwise, we leave discussions open to allow reviewers to respond.\nAfter addressing all change requests, it's considerate to re-request a review\nfrom the relevant parties.\n\n## Changelog\n\nWe maintain a changelog for each project in the `CHANGELOG.md` files, recording\nany changes except internal modifications or refactors. New features and\noriginal bug fixes should also be documented in the `README.md`. If a change\naffects game flow behavior, be sure to update the `GAME_FLOW/` accordingly.\nLikewise, changes to the console commands should update `COMMANDS.md`.\n\n## Commit scope\n\nWhen merging, we use rebasing for a clean commit history. For that reason,\neach significant change should have an isolated commit. It's okay to force-push\npull requests.\n\n## Commit messages\n\n**Bug fixes and feature implementations should include the phrase `Resolves\n#123`.** For player-facing changes without an existing ticket, a ticket needs\nto be created first.\n\nAnything else is just for consistency and general neatness. Our commit messages\naim to respect the 50/72 rule and have the following form:\n\n    module-prefix: description in an imperative mood (max 50 characters)\n\n    Longer description of what happens that can span multiple lines. Each\n    line should be maximally 72 characters long, with the exceptions of\n    code/log dumps.\n\nThe prefix should describe the module that the pull request touches the most.\nIn general this is the name of the `.c` or `.h` file with the most changes.\nNote that this includes the folder names which are separated with `/`. Avoid\nunderscores (`_`) in favor of dashes (`-`).\n\nThe description should be as concise as possible; any details should be given\nin the commit message body. Use simple, to the point words like `add`, `fix`,\n`remove`, `improve`.\n\nGood:\n\n```text\nui: improve resolution changing\n\nAdded the ability for the player to switch resolutions directly from\nthe game ui.\n\nResolves #123.\n```\n\nGreat:\n\n```text\nlog: fix varargs for Log_Message()\n\nOn Linux, the engine crashes when printing the log messages. This\nhappens because the current code re-uses the same va_list variable on\ntwo calls to vprintf() and vfprintf(). Actually, this is not allowed.\nFor using the same information on multiple formatting functions, it is\nneeded to create a copy of the primary va_list to a second one, by using\nva_copy(). After rewriting properly the Log_Message() function, the\nsegmentation fault is gone. Tested on both Linux and Windows builds.\n```\n\n> [!NOTE]\n> This has no ticket number, but it was an internal change improving support\n> for a platform unsupported at that time, which made it acceptable.\n\nBad:\n\n```text\nui: implemented the ability to switch resolutions from the ui\n```\n\n- the subject doesn't use imperative mood\n- the subject is too long\n- it's missing a ticket number\n\nBad:\n\n```text\ndart: added dart emitters to the savegame (#779)\n\ndart: added dart emitters to the savegame\n\nAdd function for checking legacy savegame save flags\nResolves #774.\n```\n\n- it duplicates the subject in the message body\n- the subject doesn't use imperative mood\n\nWhen using squash to merge, it is acceptable for GitHub to append the pull\nrequest number, but it's important to carefully review the body field, as it\noften includes unwanted content.\n"
  },
  {
    "path": "docs/CODING_GUIDELINES.md",
    "content": "# Coding guidelines\n\n## Top values\n\n- Compatibility with the original game's look and feel\n- Player choice whether to enable any impactful changes\n- Maintainability\n- Automation where possible\n- Documentation (git history and GitHub issues are great for this purpose)\n\n## Automatic code formatting\n\nThis project uses [pre-commit](https://pre-commit.com/) to make sure the code\nis formatted the right way. This tool has additional external dependencies:\n`clang-format` for automatic code formatting. To install pre-commit:\n\n```console\npython3 -m pip install --user pre-commit\npre-commit install\n```\n\nTo install required external dependencies on Ubuntu:\n\n```console\napt-get install -y clang-format-18\n```\n\nAfter this, each commit should trigger a hook to automatically format changes.\nTo manually initiate this process, run `just lint-format`. This excludes the\nslower checks that could affect productivity. For the full process, run `just\nlint`. If installing the above software isn't possible, the CI pipeline will\nindicate necessary changes in case of mistakes.\n\n## Coding conventions\n\n- Variables are `lower_snake_case`\n- Global variables are `g_PascalCase`\n- Module variables are `m_PascalCase` and static\n- Global function names are `Module_PascalCase`\n- Module functions are `M_PascalCase` and static\n- Macros are `UPPER_SNAKE_CASE`\n- Struct names are `UPPER_SNAKE_CASE`\n- Struct members are `lower_snake_case`\n- Enum names are `UPPER_SNAKE_CASE`\n- Enum members are `UPPER_SNAKE_CASE`\n\nIt's recommended to minimize the use of global variables. Instead, consider\ndeclaring them as `static` within the module they're used.\n\nOther things:\n\n- We use clang-format to automatically format the code.\n- We do not omit `{` and `}`.\n- We use K&R brace style.\n- We condense consecutive `if` expressions into one.\n\n    Recommended:\n\n    ```c\n    if (a && b) {\n    }\n    ```\n\n    Not recommended:\n\n    ```c\n    if (a) {\n        if (b) {\n        }\n    }\n    ```\n\n    When expressions become extraordinarily complex, consider refactoring them\n    into smaller conditions or functions.\n\n## Tooling\n\nInternal tools are typically coded in a reasonably recent version of Python,\nwhile avoiding the use of bash, shell, and similar languages.\n"
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "content": "# Development guidelines\n\nThis section collects the main documents for contributing to TRX.\n\n## Build and setup\n\n- Follow [BUILDING.md](BUILDING.md) for the build workflow and supported\n  compiler notes.\n\n## Working with the project\n\n- Follow [CODING_GUIDELINES.md](CODING_GUIDELINES.md) for project values,\n  automatic formatting, coding conventions, and tooling notes.\n- Follow [CHANGE_SUBMISSION.md](CHANGE_SUBMISSION.md) for pull requests,\n  changelog updates, commit scope, and commit messages.\n\n## Release process\n\n- Follow [RELEASING.md](RELEASING.md) for branching, releases, hotfixes, and\n  versioning.\n\n## Glossary\n\n- See [GLOSSARY.md](GLOSSARY.md) for common project terms and nicknames.\n"
  },
  {
    "path": "docs/GLOSSARY.md",
    "content": "# Glossary\n\n- OG: original game (for TR1 this is most often TombATI)\n- PS: the PlayStation version of the game\n- UK Box: a variant of Tomb Raider II released on physical discs in the UK\n- Multipatch: a variant of Tomb Raider II released on Steam\n- Vole: a rat that swims\n- Pod: a mutant egg (including the big egg)\n- Cabin: the room with the pistols from Natla's Mines\n- Statue: centaur statues from the entrance of Tihocan's Tomb\n- Bacon Lara: the doppelgänger Lara in the Atlantis level\n- Torso/Adam: the big boss mutant from The Great Pyramid level\n- Tomb1Main: the previous name of the TR1X project\n- TR1X: the previous name of this TRX project targeting TR1\n- TR2X: the previous name of this TRX project targeting TR2\n- T1M: short hand of Tomb1Main\n"
  },
  {
    "path": "docs/RELEASING.md",
    "content": "# Releasing TRX\n\n## Branching model\n\nWe have two branches: `develop` and `stable`. `develop` is where all changes\nabout to be published in the next release land. `stable` is the latest release.\n\n## Releasing a new version\n\nNew version releases are published automatically whenever a new tag is pushed\nto the `stable` branch with the help of GitHub actions. The general workflow is\nthis:\n\n```console\nRELEASE_VERSION=...\n\n# Switch to the stable branch.\ngit checkout stable\n\n# Merge `develop` into it.\ngit merge develop\n\n# Create a special commit `docs: release X.Y.Z` marking the release in the\n# relevant changelog file. Then tag it with `trx-X.Y.Z`.Y.Z`.\n# You can do that by hand, or run the command below:\ntools/release commit ${RELEASE_VERSION}\ntools/release tag ${RELEASE_VERSION}\n\n# Review the changelog content.\n\n# Switch back to develop.\ngit checkout develop\n\n# Merge stable using fast-forward.\ngit merge --ff stable\n\n# Review both branches and changes. If everything is okay, push to GitHub.\n# You can do this by hand: git push origin develop stable trx-X.Y.Z, or:\n# tools/release push ${RELEASE_VERSION}\n```\n\n## Hotfixes\n\nHotfix releases are a bit different as we try to not include non-bugfix changes\nin them. Here instead of merging `develop` to `stable` we cherry-pick relevant\nchanges, resolving conflicts along the way.\n\n## Versioning\n\nWe increase the major version for significant releases based on judgment,\ntypically defaulting to increasing the minor version. Hotfixes increase the\npatch version.\n"
  },
  {
    "path": "docs/SECRETS.md",
    "content": "# GitHub repository secrets\n\nIn the unfortunate event that the lead developers become unavailable for any\nreason, here is documentation detailing the third-party integrations we're\nusing, including some paid services. All integrations are automated through CI,\nand the access information is securely stored in GitHub secrets.\n\n### DockerHub\n\nWe utilize DockerHub for our Docker builds, maintaining distinct images for\nWindows and Linux platforms. (Mac builds are an exception and do not employ\nDocker.) Each image is equipped with the necessary tools to compile the game\nexecutable, thus eliminating the need for developers or CI environments to\ninstall additional tools on their machines. While these images can be built\nlocally, we host the pre-built Docker images on DockerHub for the convenience\nof new developers and CI processes that otherwise couldn't cache the images.\n\n**Variables**:\n\n- `DOCKERHUB_USERNAME`:  \n    The username to log to the DockerHub and under which the images are hosted.\n\n- `DOCKERHUB_PASSWORD`:  \n    The password to the account.\n\nWhenever we change the images for any reason (such as by introducing a new hard\ndependency), we have to run the Build Docker toolchain GitHub action by hand.\n\n### MacOS builds\n\nMacOS builds require a paid Apple Developer account.\n\n**Variables**:\n\n- `MACOS_APPLEID`:  \n    Apple developer account id / email address\n\n- `MACOS_APP_PWD`:  \n    Apple developer account password. It is recommended to use an app password,\n    that can be individually revoked.\n\n- `MACOS_TEAMID`:  \n    Every Apple developer account has an associated team ID. To see one:\n    1. Navigate to https://developer.apple.com/account.\n    2. Go to Membership details.\n    3. Examine the `Team ID` value.\n\n- `MACOS_KEYCHAIN_PWD`:  \n    This is used for a temporary keychain file made by the GitHub workflow - as\n    such, the exact value doesn't really matter.\n\n- `MACOS_CERTIFICATE`:  \n    A codesigning certificate generated from the Apple developer account. To generate it:\n\n    1. Navigate to https://developer.apple.com/account/resources/certificates/list.\n\n    2. Create a new Certificate:\n\n       1. Select Developer ID Application; continue to the next page.\n\n       2. Create a Certificate Signing Request and Private Key pair:\n\n           > openssl req -new -newkey rsa:2048 -nodes -keyout TR1X.key -out TR1X.csr -subj \"/emailAddress=your-mail@example.com, CN=TR1X\"\n\n       3. Upload the newly generated `TR1X.csr` file; continue to the next page.\n\n    3. Download the certificate and save it as `TR1X.cer`.\n\n    4. Convert the certificate to the PKCS12 format - run:\n\n       > openssl pkcs12 -export -out TR1X.pem -inkey TR1X.key -in TR1X.cer -name TR1X -legacy\n\n       This command will ask you for a password. It should be noted down.\n\n    5. Serialize the key in base-64 without spaces - run:\n\n       > base64 TR1X.pem|tr -d '\\n'\n       > base64 -i BUILD_CERTIFICATE.p12 | pbcopy (macos)\n\n       The result is to be put as the value of the `MACOS_CERTIFICATE` secret.\n\n- `MACOS_CERTIFICATE_PWD`:  \n    The password to the `MACOS_CERTIFICATE`.\n"
  },
  {
    "path": "docs/gameflow.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"TRX gameflow schema\",\n  \"$defs\": {\n    \"path\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"minItems\": 1\n        }\n      ]\n    }\n  },\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Human-readable display name for this mod, shown in the Switch Game menu.\"\n    },\n    \"engine\": {\n      \"type\": \"integer\",\n      \"enum\": [1, 2, 3],\n      \"description\": \"Engine version this mod runs on (1 = TR1, 2 = TR2, 3 = TR3).\"\n    },\n    \"extends\": {\n      \"type\": \"string\",\n      \"description\": \"Directory name of the base mod this mod extends. Used for asset fallback.\"\n    },\n    \"main_menu_picture\": {\n      \"type\": \"string\",\n      \"description\": \"Path to the main menu background image.\"\n    },\n    \"main_script\": {\n      \"type\": \"string\",\n      \"description\": \"Path to a global Lua script to execute after game initialization, before the first level loads.\"\n    },\n    \"savegame_file_fmt\": {\n      \"type\": \"string\",\n      \"description\": \"Path pattern to look for the savegame files.\"\n    },\n    \"demo_version\": {\n      \"type\": \"boolean\",\n      \"description\": \"Legacy demo version flag (scheduled for removal).\"\n    },\n    \"title\": {\n      \"$ref\": \"#/definitions/title\",\n      \"description\": \"Configuration for the title screen.\"\n    },\n    \"sfx_path\": {\n      \"$ref\": \"#/$defs/path\",\n      \"description\": \"Path to the sound effects (.sfx) file to use in the game.\"\n    },\n    \"injections\": {\n      \"type\": \"array\",\n      \"description\": \"Global data injection file paths applied to all levels unless overridden.\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"globe_select_entries\": {\n      \"type\": \"array\",\n      \"description\": \"Array of globe entries to define how level area selection works.\",\n      \"items\": {\n        \"$ref\": \"#/definitions/globe_select\"\n      }\n    },\n    \"levels\": {\n      \"type\": \"array\",\n      \"description\": \"Array of regular level definitions.\",\n      \"items\": {\n        \"$ref\": \"#/definitions/level\"\n      }\n    },\n    \"cutscenes\": {\n      \"type\": \"array\",\n      \"description\": \"Array of cutscene level definitions.\",\n      \"items\": {\n        \"$ref\": \"#/definitions/level\"\n      }\n    },\n    \"demos\": {\n      \"type\": \"array\",\n      \"description\": \"Array of demo level definitions.\",\n      \"items\": {\n        \"$ref\": \"#/definitions/level\"\n      }\n    },\n    \"fmvs\": {\n      \"type\": \"array\",\n      \"description\": \"Array of FMV entries.\",\n      \"items\": {\n        \"$ref\": \"#/definitions/fmv\"\n      }\n    },\n    \"convert_dropped_guns\": {\n      \"type\": \"boolean\",\n      \"description\": \"Forces guns dropped by enemies to be converted to the equivalent ammo if Lara already has the gun.\"\n    },\n    \"enable_tr2_item_drops\": {\n      \"type\": \"boolean\",\n      \"description\": \"Forces enemies who are placed in the same position as pickup items to carry those items and drop them when killed.\"\n    },\n    \"enforced_config\": {\n      \"type\": \"object\",\n      \"description\": \"Overrides for any regular game config settings.\",\n      \"additionalProperties\": true\n    },\n    \"hidden_config\": {\n      \"type\": \"array\",\n      \"description\": \"List of config settings to hide from the in-game settings dialogs.\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"fog_start\": {\n      \"type\": \"number\",\n      \"description\": \"The distance (in tiles) at which objects and the world start to fade into blackness.\"\n    },\n    \"fog_end\": {\n      \"type\": \"number\",\n      \"description\": \"The distance (in tiles) at which objects and the world are clipped away.\"\n    },\n    \"water_color\": {\n      \"description\": \"Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0 or `00` means completely black color.\",\n      \"oneOf\": [\n        {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"number\"\n          },\n          \"minItems\": 3,\n          \"maxItems\": 3\n        },\n        {\n          \"type\": \"string\"\n        }\n      ]\n    },\n    \"ambient_tracks\": {\n      \"type\": \"array\",\n      \"description\": \"List of music track IDs treated as ambient music (persists on save/load).\",\n      \"items\": {\n        \"type\": \"integer\"\n      }\n    }\n  },\n  \"required\": [\n    \"engine\",\n    \"main_menu_picture\",\n    \"savegame_file_fmt\",\n    \"levels\"\n  ],\n  \"additionalProperties\": false,\n  \"definitions\": {\n    \"command\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"action\": {\n          \"type\": \"string\"\n        },\n        \"param\": {\n          \"type\": [\n            \"integer\",\n            \"string\"\n          ]\n        }\n      },\n      \"required\": [\n        \"action\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"title\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"path\": {\n          \"$ref\": \"#/$defs/path\"\n        },\n        \"music_track\": {\n          \"type\": \"integer\"\n        },\n        \"inherit_injections\": {\n          \"type\": \"boolean\"\n        },\n        \"injections\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"sequence\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/sequence\"\n          }\n        }\n      },\n      \"required\": [\n        \"path\",\n        \"sequence\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"level\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"script\": {\n          \"type\": \"string\",\n          \"description\": \"Path to a Lua script executed after loading this level.\"\n        },\n        \"path\": {\n          \"$ref\": \"#/$defs/path\"\n        },\n        \"music_track\": {\n          \"type\": \"integer\"\n        },\n        \"lara_outfit\": {\n          \"type\": \"string\"\n        },\n        \"weather_type\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"rain\",\n            \"snow\"\n          ],\n          \"description\": \"Enables a per-level weather effect.\"\n        },\n        \"water_particles\": {\n          \"type\": \"boolean\",\n          \"description\": \"TR3 only. Enables PSX-style underwater water particles in this level.\"\n        },\n        \"cold_water\": {\n          \"type\": \"boolean\"\n        },\n        \"death_tile\": {\n          \"type\": \"string\",\n          \"description\": \"Defines death-tile behavior (for TR3 only).\",\n          \"enum\": [\n            \"lava\",\n            \"rapids\",\n            \"electric\"\n          ]\n        },\n        \"sequence\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/sequence\"\n          }\n        },\n        \"inherit_injections\": {\n          \"type\": \"boolean\"\n        },\n        \"injections\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"item_drops\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/item_drop\"\n          }\n        },\n        \"water_color\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"number\"\n          },\n          \"minItems\": 3,\n          \"maxItems\": 3\n        },\n        \"fog_start\": {\n          \"type\": \"number\"\n        },\n        \"fog_end\": {\n          \"type\": \"number\"\n        },\n        \"unobtainable_pickups\": {\n          \"type\": \"integer\"\n        },\n        \"unobtainable_kills\": {\n          \"type\": \"integer\"\n        },\n        \"unobtainable_ally_kills\": {\n          \"type\": \"integer\"\n        },\n        \"unobtainable_secrets\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"if\": {\n        \"properties\": {\n          \"type\": {\n            \"enum\": [\n              \"current\",\n              \"dummy\"\n            ]\n          }\n        },\n        \"required\": [\n          \"type\"\n        ]\n      },\n      \"else\": {\n        \"required\": [\n          \"path\",\n          \"sequence\",\n          \"lara_outfit\"\n        ]\n      },\n      \"additionalProperties\": false\n    },\n    \"item_drop\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"enemy_num\": {\n          \"type\": \"integer\"\n        },\n        \"object_ids\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"integer\"\n          }\n        }\n      },\n      \"required\": [\n        \"enemy_num\",\n        \"object_ids\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"globe_select\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"rot\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"integer\"\n          },\n          \"minItems\": 3,\n          \"maxItems\": 3\n        },\n        \"start_level_ordinal\": {\n          \"type\": \"integer\"\n        },\n        \"completion_level_ordinal\": {\n          \"type\": \"integer\"\n        },\n        \"prereq_zones\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"integer\"\n          }\n        },\n        \"mesh_idx\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"rot\",\n        \"start_level_ordinal\",\n        \"completion_level_ordinal\",\n        \"prereq_zones\",\n        \"mesh_idx\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"fmv\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"path\": {\n          \"$ref\": \"#/$defs/path\"\n        },\n        \"legal\": {\n          \"type\": \"boolean\"\n        }\n      },\n      \"required\": [\n        \"path\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"sequence\": {\n      \"oneOf\": [\n        {\n          \"$ref\": \"#/definitions/seq_loop_game\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_level_complete\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_exit_to_title\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_level_stats\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_total_stats\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_display_picture\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_loading_screen\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_play_fmv\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_give_item\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_add_secret_reward\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_play_music\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_remove_ammo\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_remove_weapons\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_remove_medipacks\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_remove_scions\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_remove_flares\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_play_cutscene\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_enable_sunset\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_set_lara_start_anim\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_setup_bacon_lara\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_disable_floor\"\n        },\n        {\n          \"$ref\": \"#/definitions/seq_globe_select\"\n        }\n      ]\n    },\n    \"seq_loop_game\": {\n      \"type\": \"object\",\n      \"description\": \"Plays the main game loop.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"loop_game\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_level_complete\": {\n      \"type\": \"object\",\n      \"description\": \"Ends the current level and plays the next one, if available.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"level_complete\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_exit_to_title\": {\n      \"type\": \"object\",\n      \"description\": \"Returns to the title level.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"exit_to_title\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_level_stats\": {\n      \"type\": \"object\",\n      \"description\": \"Displays the end of level statistics for the current level. In a Gym level, this fades the screen to black.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"level_stats\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_total_stats\": {\n      \"type\": \"object\",\n      \"description\": \"Displays the end of game statistics with the given picture file shown as a background.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"total_stats\"\n        },\n        \"background_path\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"background_path\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_display_picture\": {\n      \"type\": \"object\",\n      \"description\": \"Displays the specified picture for a fixed time. Files that are needed to function only with a specific aspect ratio can be placed in a directory adjacent to the main image, named according to the aspect ratio – for example, 4x3/title.png or 16x10/title.png. The game won't attempt to match these precisely; instead, it will select the file with the aspect ratio closest to the game's viewport. The main image designated by path is presumed to have a 16:9 aspect ratio for this purpose, and as such there's no need for 16x9-specific directory. This logic applies to all images.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"display_picture\"\n        },\n        \"path\": {\n          \"$ref\": \"#/$defs/path\"\n        },\n        \"display_time\": {\n          \"type\": \"number\"\n        },\n        \"fade_in_time\": {\n          \"type\": \"number\"\n        },\n        \"fade_out_time\": {\n          \"type\": \"number\"\n        },\n        \"credit\": {\n          \"type\": \"boolean\"\n        },\n        \"legal\": {\n          \"type\": \"boolean\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"path\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_loading_screen\": {\n      \"type\": \"object\",\n      \"description\": \"Shows a picture prior to loading a level. Functions identically to display_picture, except these pictures can be enabled/disabled by the user with the loading screen option in the config tool.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"loading_screen\"\n        },\n        \"path\": {\n          \"type\": \"string\"\n        },\n        \"display_time\": {\n          \"type\": \"number\"\n        },\n        \"fade_in_time\": {\n          \"type\": \"number\"\n        },\n        \"fade_out_time\": {\n          \"type\": \"number\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"path\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_play_cutscene\": {\n      \"type\": \"object\",\n      \"description\": \"Plays the specified cinematic level (from the cutscenes).\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"play_cutscene\"\n        },\n        \"cutscene_id\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"cutscene_id\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_play_fmv\": {\n      \"type\": \"object\",\n      \"description\": \"Plays the specified FMV. fmv_id must be a valid index into the fmvs root key.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"play_fmv\"\n        },\n        \"fmv_id\": {\n          \"type\": [\n            \"integer\",\n            \"string\"\n          ]\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"fmv_id\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_give_item\": {\n      \"type\": \"object\",\n      \"description\": \"Adds the specified item and quantity to Lara's inventory.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"give_item\"\n        },\n        \"object_id\": {\n          \"type\": [\n            \"integer\",\n            \"string\"\n          ]\n        },\n        \"quantity\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"object_id\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_add_secret_reward\": {\n      \"type\": \"object\",\n      \"description\": \"Adds the specified item to the current level's list of rewards for collecting all secrets.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"add_secret_reward\"\n        },\n        \"object_id\": {\n          \"type\": [\n            \"integer\",\n            \"string\"\n          ]\n        },\n        \"quantity\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"object_id\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_play_music\": {\n      \"type\": \"object\",\n      \"description\": \"Plays the given audio track.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"play_music\"\n        },\n        \"music_track\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"music_track\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_remove_ammo\": {\n      \"type\": \"object\",\n      \"description\": \"Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"remove_ammo\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_remove_weapons\": {\n      \"type\": \"object\",\n      \"description\": \"Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"remove_weapons\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_remove_medipacks\": {\n      \"type\": \"object\",\n      \"description\": \"Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"remove_medipacks\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_remove_scions\": {\n      \"type\": \"object\",\n      \"description\": \"Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"remove_scions\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_remove_flares\": {\n      \"type\": \"object\",\n      \"description\": \"Any combination of these sequences can be used to modify Lara's inventory at the start of a level. `remove_weapons` does not remove the ammo for those guns, and `remove_ammo` does not remove the guns. Item removal is performed first, followed by addition.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"remove_flares\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_enable_sunset\": {\n      \"type\": \"object\",\n      \"description\": \"Enables the sunset effect, like in Bartoli's Hideout. At present, this feature is hardcoded to gradually darken the game 40 minutes into playing a level.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"enable_sunset\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_set_lara_start_anim\": {\n      \"type\": \"object\",\n      \"description\": \"Applies the selected animation to Lara when the level begins. This is used, for example, in the Offshore Rig of Tomb Raider II.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"set_lara_start_anim\"\n        },\n        \"anim\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"anim\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_setup_bacon_lara\": {\n      \"type\": \"object\",\n      \"description\": \"Sets the room number in which Bacon Lara will be anchored to enable correct mirroring behaviour with Lara.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"setup_bacon_lara\"\n        },\n        \"anchor_room\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"anchor_room\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_disable_floor\": {\n      \"type\": \"object\",\n      \"description\": \"Configures a specific height (with 256 representing 1 click and 1024 representing 1 sector) to define an abyss that will invariably lead to Lara's death if she falls into it. Additionally, it employs special rendering to ensure it isn't treated as solid ground.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"disable_floor\"\n        },\n        \"height\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"height\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"seq_globe_select\": {\n      \"type\": \"object\",\n      \"description\": \"Displays the area selection globe using the specified background image.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"globe_select\"\n        },\n        \"image\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"image\"\n      ],\n      \"additionalProperties\": false\n    }\n  }\n}\n"
  },
  {
    "path": "docs/tr1/CHANGELOG.md",
    "content": "## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr1-4.15.1...develop) - ××××-××-××\nSee [/docs/CHANGELOG.md].\n\n## [4.15.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.15...tr1-4.15.1) - 2025-10-10\n- changed the examine dialog to be usable with non-puzzle items (#4009)\n- fixed a crash on game exit if specifying \"ambient_tracks\" in the game flow root (regression from 4.11)\n- fixed alternate ambient tracks being lost on reload in custom levels (#3997, regression from 4.14)\n- fixed Lara at times not being able to grab pushblocks despite being in the correct position to do so (#4005, regression from 0.9.1)\n- fixed Lara appearing flat for a frame during the neutral twist, controlled drop and ledge jump back animations (#4012, regression from 4.14)\n- fixed the pickup embed glitch when Lara is below a steeply sloped ceiling not being optional (Gameplay → Fixes → Fix pickup embed glitch) (#4020, regression from 4.10)\n\n## [4.15](https://github.com/LostArtefacts/TRX/compare/tr1-4.14.2...tr1-4.15) - 2025-10-04\nShowcase: https://youtu.be/BwZXWL0WULg\n- added an option to use TR2-style UI bars (Graphics → UI → Bars look)\n- added an option to use PS1-style UI bars (Graphics → UI → Bars look) (#1637)\n- added a new `/cls` / `/clear` console command to quickly clear console logs\n- added support for ladders (#3124)\n- improved PS1-style UI faithfulness\n- improved sound settings:\n    - added tabs (Volume and Misc)\n    - added a dedicated option to control master volume (Sound options → Volume → Master volume)\n    - added a dedicated option to control cutscenes volume (Sound options → Volume → Cutscenes volume) (#3490)\n    - added a dedicated option to control FMV volume (Sound options → Volume → FMV volume) (#3490)\n    - added a dedicated option to control general ambient volume (Sound options → Volume → Ambient volume) (#3707)\n    - improved volume settings to accept slow input for finer adjustments\n    - fixed changing sound volume not updating certain ambient sound sources while in the inventory ring (#3970)\n- changed OG glitch-related config options to be on/fixed by default (#3929)\n- changed the UI style to use the PS1 look by default (Graphics → UI → Menu style)\n- changed pickup aids to be enabled by default (Graphics → Visuals → Pickup aids)\n- changed responsive jumping to be enabled by default (Gameplay → Controls → Responsive jumping)\n- changed lean jumping to be enabled by default (Gameplay → Controls → Lean jumping)\n- changed smooth swimming to be enabled by default (Gameplay → Controls → Smooth swimming)\n- changed responsive swim cancel to be enabled by default (Gameplay → Controls → Responsive swim cancel)\n- changed idle pose timeout from 15 to 60 seconds by default (Gameplay → Controls → Idle pose timeout)\n- changed idle pose camera to be disabled by default (Gameplay → Controls → Idle pose camera)\n- changed PS1 uzi sound to be enabled by default (Sound → Misc → PS1 uzi sound)\n- changed max pickup scale to 200% (#3952)\n- fixed pickup scale being greyed out if the 3D pickups option is enabled (#3952)\n- fixed certain ambient sounds volume scaling wrong on non-100% volumes\n- fixed trapdoor type 3 (object #67) not functioning (#3895)\n- fixed gameplay settings UI displaying eagerly after the first use (#3583, regression from 4.13)\n- fixed changing FPS after advancing frames in photo mode causing the game to speed up (#3605, regression from 4.13)\n- fixed CPU spike during playing FMVs (#3908, regression from 4.6)\n- fixed `/play` command likely to skip opening FMVs when inventory buffering is enabled (#3910, regression from 3.0)\n- fixed `/pos` command crashing in cutscenes (#3944, regression from 4.10)\n- fixed loading a save made in the gym with the item cheat resulting in Lara's meshes appearing broken (#3917, regression from 4.7)\n- fixed resumed music tracks playing briefly track start upon savegame load (#3916)\n- fixed loading TombATI saves with shotgun equipped causing weird Lara's animation (#3920, regression from 4.12)\n- fixed numerous interactions with movable blocks, trapdoors, drawbridges, bridges, sliding pillars, and falling blocks for custom levels (#2758):\n    - added the ability for movable blocks to move on trapdoors, drawbridges, bridges, sliding pillars, and falling blocks\n    - added the ability for stacks of movable blocks to fall and land on trapdoors, drawbridges, bridges, sliding pillars, and falling blocks\n    - added the ability for stacks of movable blocks to fall when on opened trapdoors and drawbridges\n    - fixed various bugs with falling movable blocks\n- fixed pushblocks becoming unusable when on the same sector as a door that does not sit on a room portal (#3814)\n- fixed pushblocks that fall from a great height potentially causing a crash (#3969)\n- fixed recordings replaying commands twice (regression from 4.14)\n- fixed the fix for the sticky corner glitch not being optional - now linked to Gameplay → Fixes → Wall glitch mode (#3957, regression from 4.14)\n- fixed Lara retaining guns if drawn during wade to float transition (#3979, regression from 4.13)\n- fixed -s/--save argument no longer working with -l/--level (#3990, regression from 4.14)\n\n## [4.14.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.14.1...tr1-4.14.2) - 2025-09-07\n- fixed broken rendering in MacOS releases (#3880, regression from 4.14)\n- fixed images from MacOS releases (#3892, regression from 4.14)\n\n## [4.14.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.14...tr1-4.14.1) - 2025-08-30\n- fixed missing shader and configuration files from MacOS releases (#3870, regression from 4.14)\n- fixed zero byte at the end of config files (#3875, regression from 4.14)\n- fixed stacked sprites flickering (#3872, regression from 4.14)\n\n## [4.14](https://github.com/LostArtefacts/TRX/compare/tr1-4.13.2...tr1-4.14) - 2025-08-23\nShowcase: https://www.youtube.com/watch?v=iV8G9lhxVQ8\n\n>[!WARNING]\n>Attention level builders: this version introduces backwards incompatible changes to the file structure.\n>Please refer to the [migration guide](../trx/MIGRATING.md) to see how to update your levels.\n\n- added lighting contrast option (Graphic options → Rendering → Lighting contrast)\n- added new command switches:\n    - `--test-record` and `--test-replay` for automated playthroughs with (internal tool – the recording file format may be subject to changes)\n    - `--headless`: runs the game offscreen with no audio and at unlocked simulation speed\n    - -q`, `--quiet`: outputs only error messages to the terminal, with log files being written to normally\n- added new hotkeys: F7 for toggling the wireframe mode, F8 for cycling the lighting contrast\n- added ability to move Lara around in photo mode\n- added additional poses for photo mode\n- added an option to allow Lara to sprint (Gameplay → Controls → Sprinting) (#3711)\n- added an option to use Lara's slide-to-run animation from TR3+ (Gameplay → Controls → Slide-to-run) (#1089)\n- added an option to use Lara's neutral jump-twist from early TR1 betas (Gameplay → Controls → Neutral twists) (#1392)\n- added an option to allow Lara to turn around and grab a ledge she has just stepped off (Gameplay → Controls → Controlled drops) (#3621)\n- added an option to allow Lara to jump up or back when hanging from a ledge (Gameplay → Controls → Ledge jumps) (#3683)\n- added an option to have Lara pose after standing idle for a certain time (Gameplay → Controls → Idle pose timeout) (#3727)\n- added an option to keep sprites upright (Graphic options → Rendering → Sprites lock mode)\n- added an option to scale the 3D pickups in the UI (Graphic options → UI → Pickup scale)\n- added an option to control fog color (Graphic options → Visuals → Fog transparency and Fog color) (#712, #3618)\n- added Russian translation\n- added German translation\n- added skyboxes to The Cistern and Tomb of Tihocan (#2143)\n- added a new `/lua` console command (for now, [it cannot do much](../trx/lua/))\n- added a new `/restless` console command, which enables or disables infinite sprint\n- added debug room clip feature (`/debug 1`)\n- improved object loading error messages when an invalid object ID is detected\n- improved frames in Lara's jump-twist animations\n- improved lighting, projection and sizing of 3D pickups in the UI\n- improved PS1 menu style border offsets and frames to match PC style\n- improved drawing shadows in no-clip camera mode (they're no longer double-sided)\n- improved wireframe mode to show text and UI normally\n- improved bilinear filter edge blending (#587)\n- improved window resize performance in the title inventory ring\n- changed the brightness filter to also work on title inventory ring background\n- changed the brightness filter option to work in smaller increments (10% reduced to 5%); added support for slow increments by 1% (hold Walk key)\n- changed the text and bar scale option to work in smaller increments (10% reduced to 5%); added support for slow increments by 1% (hold Walk key)\n- changed the game flow and game strings file placement\n- changed the skybox option to allow toggling in-game without the need to reload the level\n- changed the texture page limit from 128 to unlimited (#3517)\n- changed the `/set` console command to report boolean values as `0` or `1`, language-agnostic\n- changed waterfall objects to always be drawn when active rather than only when Lara is within a 10 sector range (#3598)\n- changed `-l`/`--level` switch to accept the level number on top of the level path\n- changed settings dialogs to show a suitable message if a level builder has hidden all options within that dialog (#3637)\n- changed the fly cheat to allow Lara to interact with switches and pickups (#3665)\n- removed the option in Unfinished Business to fix animated sprites as it is irrelevant there\n- fixed glide camera behaviour and position in room 101 in Temple of the Cat (#3533)\n- fixed French translations containing Italian text in some cases (#3567)\n- fixed the camera remaining locked on moving lava if it touches Lara when she is immune (#3578)\n- fixed several issues with door data\n    - fixed missing door/trapdoor sound effects (#3408, #3374, #3617, #3619)\n    - fixed animation frames in doors in St. Francis' Folly, Tomb of Tihocan and Sanctuary of the Scion (#3661)\n    - fixed the cameras for doors 81 in Tomb of Tihocan and 1 in Sanctuary of the Scion only showing once (#3661)\n- fixed the passport having an invisible back page, noticeable when opening/closing it (#2051)\n- fixed z-fighting on the front of the passport (#3584)\n- fixed setting description dialog missing borders with PS1 UI style (#3714, regression from 4.12)\n- fixed being unable to activate waterfall objects with code bits (#3589)\n- fixed skippable triggers for waterfall objects in Lost Valley (#3593)\n- fixed incorrectly rotated 3D pickup items in several levels (#2147)\n- fixed incorrect room mesh structure in Vilcabamba room 41, causing disappearing polygons (#3613)\n- fixed missing textures on the statues in Vilcabamba and Tomb of Qualopec (#3629)\n- fixed missing textures in Atlantis rooms 7, 9, 13, 14, 95, 96 (#3657)\n- fixed missing double-sided textures in The Cistern rooms 9 and 12\n- fixed texture clipping in Atlantean Stronghold when looking into room 18, and missing textures in rooms 5, 6, 18 and 74 (#3668)\n- fixed the collision box on the tall statues in Tomb of Qualopec e.g. room 20 (#3629)\n- fixed the mesh structure on the cat statue in Egyptian levels to standardize its position (#3634)\n- fixed the collision box on some static meshes in Egypt to prevent the camera shaking when Lara walks by them (#762)\n- fixed incorrect transparent pixels on room textures in St. Francis' Folly and Temple of the Cat (#3659)\n- fixed the positions of big pods in Atlantean levels and cutscenes (#3670)\n- fixed several texture issues in Lara's Home, Vilcabamba, Lost Valley, St. Francis' Folly and Natla's Mines (#3860)\n- fixed a missing transition animation between Lara jumping forward and entering freefall (#3815)\n- fixed incorrect wet footstep sounds in some of Lara's climb-up animations (#3607, regression from 4.6)\n- fixed the `/kill` command potentially causing a crash if used in a level with pods that don't hatch creatures (#3628, regression from 4.12)\n- fixed Lara's animation not being restored correctly on load if a save was made during a special animation, such as using the Midas Hand (#3625, regression from 4.9)\n- fixed emitted darts moving in the wrong direction when reloading a save (#3677, regression from 2.11)\n- fixed backslash/grave key/less-than character on some keyboards shown as ???? – now it's shown as backslash (#3713)\n- fixed wireframe mode rendering as mostly white (#3649, regression from 4.13.2)\n- fixed wireframe mode not working in the inventory ring (#3651, regression from 4.10)\n- fixed the boulder in room 78 getting drawn in the overlapping room 74 in Tomb of Tihocan (#3761, regression from 4.10)\n- fixed shadow rendering\n    - fixed Y component not interpolated in 60 FPS (#1314)\n    - fixed shadows being rendered partially opaque near room portals (#879)\n    - fixed Bacon Lara shadow rendered transparent when she's standing on a trapdoor (#3666)\n- fixed potentially being able to reactivate an already used puzzle slot's trigger (#3849, regression from 4.13)\n- fixed being unable to cycle poses in photo mode if cheats were disabled (#3726, regression from 4.13)\n- fixed Lara exiting the fly cheat if the walk key is used during photo mode (#3753, regression from 4.13)\n- fixed being able to issue certain console commands that target Lara during loading screens (#3662, regression from 4.13)\n- fixed flame SFX being audible underwater (#3830, regression from 4.13)\n- fixed z-fighting of doors near walls\n- fixed Lara walking backwards off ledges into lava (#3745)\n- fixed room scheduling algorithm sometimes drawing overlapping rooms (#3774, regression from 4.1)\n- fixed exiting photo mode on a controller conflicting with the roll input (#3842, regression from 4.8)\n- fixed resuming non-ambient music tracks when loading a savegame (#3845, regression from 4.13)\n- fixed quick draw button not working until after Lara equipped guns by other methods with certain saves (#3844, regression from 4.13)\n\n## [4.13.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.13.1...tr1-4.13.2) - 2025-07-20\n- fixed savegame scanner only seeing all-lowercase file names (#3518, regression from 4.9)\n- fixed drawing UI text with bilinear filter and PS1 UI (#3548, regression from 4.13)\n- fixed dynamic fire light being generated despite the flame object not being present in the level (#3539, regression from 4.13)\n- fixed the first camera frame when starting or loading a level being inaccurate (#3537, regression from 4.12.3)\n\n## [4.13.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.13...tr1-4.13.1) - 2025-07-18\n- fixed Lara's first pose in photo mode at times being skipped (#3522, regression from 4.13)\n- fixed Lara's arms being drawn inaccurately when posing in photo mode with dual weapons equipped (#3520, regression from 4.13)\n- fixed Lara being unable to use key items at times with animated interactions enabled (#3524, regression from 4.13)\n\n## [4.13](https://github.com/LostArtefacts/TRX/compare/tr1-4.12.3...tr1-4.13) - 2025-07-14\nShowcase: https://youtu.be/YKI7u2QOolU\n- reworked screen resolutions\n    - removed \"screen resolution\" option\n    - removed \"window size\" rendering mode, enforcing the FBO rendering method (#3332)\n    - added aspect ratio mode (Graphic options → Rendering → Aspect mode)\n    - added window border option (Graphic options → Rendering → Borders)\n    - added integer upscaling option (Graphic options → Rendering → Upscaling factor)\n    - renamed \"FBO filter\" option to \"Upscaling filter\" (Graphic options → Rendering → Upscaling filter)\n    - greatly improved text and other UI rendering with upscaling turned on (#1944)\n    - changed screenshots to always produce images at desktop resolution\n- added French translation\n- added Gaelic translation\n- added Italian translation to the installer\n- added dedicated British English translation (#3212)\n- added the ability to advance individual frames to the photo mode\n- added the ability to skip end game credits (#3266)\n- added the ability to hide specific game settings (#3242)\n- added the ability to cycle UI tabs with sidestep keys (#3272)\n- added the ability to skip consecutive credit images by holding the action / escape keys\n- added the ability to cycle between a list of predefined Lara poses in the photo mode\n- added a `/lighting` console command to let the player turn lighting system on/off\n- added an `/immune` console command to make Lara impervious to damage\n- added support for underwater save crystals in custom levels (#3356)\n- added an option to have dynamic lights generated by flames (Graphic options → Visuals → Fire lighting) (#3336)\n- added an option to control responsive jumping lock behaviour (Gameplay settings → Controls → Jump lock mode) (#3389)\n- added an option to display level counter in the statistics dialog (Graphic options → UI → Level counter) (#1087)\n- added an option to control playing of certain animation sound effects such as doors when underwater (Sound options → Underwater animation SFX) (#3385)\n- added an option to allow the audio to play when the game is out of focus (Sound options → Mute audio when focus lost, #3333)\n- added an option to make the quick gun equip keys also holster the active gun (Gameplay settings → UI → Quick gun keys) (#828)\n- added an option to control texture filter for UI alone (Graphic options → Rendering → UI filter)\n- added the ability to use the dev console during FMVs\n- improved the `/tp` command to orient Lara towards keyholes and doors\n- improved handling of animation sound effects when in shallow water (#3385)\n- improved performance when resizing the window\n- improved error messages for game flow and string edit mistakes to include path of the problematic file\n- changed statistics details mode to be placed in the UI section\n- changed controls dialog to remember the player's preferred input method\n- changed UI to show icons relevant to the chosen input method\n- changed death timer skip to only trigger with Action and Inventory keys\n- changed the examine dialog to be close-able with Look button (#3225)\n- changed some settings to be hidden when they're only applicable to specific games or custom levels (#3242)\n- changed some settings to be dimmed when they're not taking effect due to other settings (#3166)\n- changed photo mode help dialog to show icons for inputs\n- changed settings to retain their active position until exiting to title or starting a new level (#3271)\n- changed the dev console to accept compound characters (#2938)\n- changed save crystal collision to be more lenient for custom levels (#3343)\n- changed the walk-run-jump bug fix for responsive jumping to be optional (Gameplay settings → Fixes → Fix walk run jump) (#3389)\n- changed the enhanced look option to allow choosing between original TR1, original TR2 or unrestricted modes (Gameplay settings → Controls → Look mode) (#3403)\n- changed `/secret give` and `/secret take` to give or take all valid secrets when no index is specified\n- removed config tool (we have ingame setting dialogs now)\n- removed the \"Enable numeric keys\" option (it was added when these keys were not changeable)\n- fixed several more OG texture issues (refer to README for details) (#3352)\n- fixed Lara not saying 'no' near receptacles if she doesn't carry any items (#3337, regression from 4.0)\n- fixed Lara not saying 'no' near complete receptacles (#3337, regression from 4.0)\n- fixed key items getting consumed at the start of the interaction with receptacles (#3399)\n- fixed certain commands (such as `/load` or `/play`) not working as expected while in the key use inventory screen (#3338)\n- fixed the camera resetting if Lara is looking and then draws her guns (OG behaviour retained when using restricted look mode) (#3406)\n- fixed game window getting misplaced in windowed mode between game relaunches on certain systems (#3418)\n- fixed Lara using the wrong hit animation under certain scenarios based on her hit angle (#3424)\n- fixed already playing samples not getting muted when the game window goes out of focus\n- fixed the `/play` command starting the level with wrong items sometimes (#3147, regression from 4.11)\n- fixed the `/tp` command breaking the photo mode\n- fixed the `/tp` command misbehaving when giving fractional coordinates\n- fixed Compass label in Gym not appearing when holding arrows during inventory spin-out (#3460)\n\n## [4.12.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.12.2...tr1-4.12.3) - 2025-06-24\n- fixed game crashing when the expected resources are missing (#3310, regression from 4.12.2)\n- fixed restore default pop-up requiring all 3 water color options to be adjusted instead of just one (#3314, regression from 4.12)\n- fixed pause screen rendered without background overlay if fade effects are disabled (#3316, regression from 4.11)\n- fixed `/pos` command crashing when the level title is not set (regression from 4.12)\n\n## [4.12.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.12.1...tr1-4.12.2) - 2025-06-22\n- fixed depth buffer problems when closing the inventory ring with fade effects disabled (#3267, regression from 4.8)\n- fixed Lara's braid not being reflective (on Midas' hand) (#3257, regression from 4.9)\n- fixed turbo cheat causing audio desync in cutscenes (#3263)\n- fixed support for non-linear secret flags in custom levels (#3262, regression from 4.12)\n- fixed movable blocks getting stuck in midair if the game is saved and loaded while they are falling (#3274)\n- fixed PS touchpad input missing an icon (#3288, regression from 4.12)\n- fixed inability to use unbind key / reset layout buttons with controllers (#3290, regression from 4.12)\n- fixed inventory ring consuming too many items under severe frame drop conditions (#3295, regression from 4.8)\n- fixed screenshots stripping accented characters (#3238)\n- fixed accented lowercase `i` characters retaining the superscript dot (#3298)\n- reverted the partial fix for wrong audio device reinitialization (#3251, regression from 4.12)\n\n## [4.12.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.12...tr1-4.12.1) - 2025-06-18\n- fixed certain secrets not registering (#3252, regression from 4.12)\n\n## [4.12](https://github.com/LostArtefacts/TRX/compare/tr1-4.11.2...tr1-4.12) - 2025-06-17\nShowcase: https://www.youtube.com/watch?v=IqjVuXTVI4A\n- added builtin support for ingame string translations\n    - changed duplicate game strings between TR1 and TR2 to be placed in a single file TRX_common_strings.json5\n    - added a new setting, `enable_review_markers`, which display which text requires review (only available via `/set`)\n    - added Italian translation\n    - added Polish translation\n    - added support for non-breaking spaces\n    - fixed game crashing when trying to word-wrap unknown characters\n- added UI for all config tool settings\n- added ingame help for all settings\n- added support for object name aliases; added aliases for dev commands\n- added an optional breeze effect for Lara's braid in appropriate outside rooms (#3090)\n- added keyboard and controller input icons to the controls settings dialog\n- added an option to adjust music and ambient volume while in inventory\n- added a `/secret` console command for easier debugging of secrets\n- added `enable_debug_pos` setting that shows Lara's position in realtime (reachable via `/debug`, fine-tuned `/set`)\n- added an option to control whether or not Lara responds to hitting a wall while wading (#3138)\n- added an option for smooth wall deflection when Lara comes to a stop at a wall, similar to TR2 (#3148)\n- added an option to fix the step glitch where Lara can be pushed into walls (#3148)\n- added an option to have Lara always roll off one-click steps rather than boosting forward (#3149)\n- added an option to toggle allowing Lara to exit from water horizontally, below, or climbing out onto non-standable slopes (#3154)\n- added an option to toggle random enemy initial angle adjustment (#3129)\n- improved the teleport cheat if used when Lara is in a special animation, such as grabbing the Scion\n- improved the dev console commands documentation\n- changed the maximum number of 2D static mesh slots (room sprites) from 50 to 256 (#3200)\n- changed the wall glitch config option to a selection of being fixed, using TR1 behaviour or TR2 behaviour (#3153)\n- changed sound and music volumes to be displayed as percentage instead of 0-10\n- changed the graphic settings dialog to use tabs\n- changed the setting dialogs to respect the UI wraparound setting\n- changed the `/tp` command to align Lara to switches and pickups\n- changed the `/set` command to accept `-`, which will restore the given setting to its default state\n- changed the music track slot limit from 64 to 1024 (#3101)\n- changed text kerning to a smaller value\n- changed the underwater music volume setting to separate ambient and music volume sliders\n- changed logs format to include timestamps\n- fixed a game crash in custom levels if centaur statues exploded without having centaur objects in the level file (#3155)\n- fixed being unable to re-purpose some gym music tracks in custom levels (#3164)\n- fixed Lara not catching fire after reloading a save made when she was on fire and enhanced saves are disabled (applies to new saves only) (#3157)\n- fixed 3D pickups misplacing or hiding UI elements with render mode set to window size and the game windowed (#3067, regression from 4.10)\n- fixed the `/play` command crashing when the game has only ATI saves (#3137, regression from 4.10)\n- fixed the `/play` command taking resume information from the highlighted slot (#3137, regression from 4.10)\n- fixed text glyphs having cut off right and bottom borders (regression from 4.7)\n- fixed unbind key option being available when it shouldn't (#3111, regression from 4.11)\n- fixed not saving screen resolution (regression from 4.11)\n- fixed vertical FOV option not working properly (#3120, regression from 4.10)\n- fixed Lara's position on a ledge after grabbing it extremely late (#3132, regression from 2.2.1)\n- fixed a rare crash when editing certain dev console history entries (#2913, regression from 4.10)\n- fixed a desync in the Vilcabamba demo if the wall glitch fix option was enabled (#3172, regression from 1.3)\n- fixed demos being affected if Lara's starting HP has been altered (#3180, regression from 2.6)\n- fixed Lara's health bar showing at the start of cutscenes (#3182, regression from 4.11)\n- fixed broken playback of mono music tracks (regression from 2.0)\n- fixed hot-plugging certain audio devices causing glitchy playback (partial fix; regression from 2.0)\n- fixed being unable to toggle fullscreen mode during FMV sequences (#3188, regression from 4.6)\n- fixed examine hint text lingering on screen when moving to a different item in the inventory (#3228, regression from 4.8)\n- fixed controls settings dialog missing trapezoid filter option (#3246, regression from 4.9)\n- fixed logging not outputting anything on Windows terminals\n\n## [4.11.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.11.1...tr1-4.11.2) - 2025-05-24\n- improved word wrapping algorithm in the dev console\n- changed examine item descriptions to remove extra blank lines\n- fixed examine item overlapping with other UI elements at large text scales\n- fixed a crash related to carried items if using saves made prior to 4.11 (#3052, regression from 4.11)\n\n## [4.11.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.11...tr1-4.11.1) - 2025-05-23\n- fixed \"Load Game\" bottom text arrows jumping when entering the load game dialog (regression from 4.11)\n- fixed missing arrows around focused navigation elements in the controls dialog (#3042, regression from 4.11)\n- fixed text outline being a bit laggy when rebinding the inputs in the controls dialog\n- fixed crashes in the save dialog on Linux (#3046, regression from 4.11)\n\n## [4.11](https://github.com/LostArtefacts/TRX/compare/tr1-4.10.2...tr1-4.11) - 2025-05-21\nShowcase: https://www.youtube.com/watch?v=JVtcZoNoeRM\n- added the ability to trigger a flip effect without having to also trigger the flip map, in line with TR2 (#2921)\n- added a /help command (#2917)\n- added an option to toggle between TR1 and TR2 camera modes (#2990)\n- added the ability to trigger different ambient tracks in custom levels, which will loop and be remembered between saves (#811)\n- changed the all items cheat to include the lead bar if present in the level (#3008)\n- changed the design of the controls dialog to use pages, making it match the new TR2X controls dialog\n- changed the pause screen to have a darker black overlay transparency (#2252)\n- fixed Lara's braid pointing straight down when swimming below sloped ceilings (#1600)\n- fixed enemy hitpoints being doubled in demo mode as a result of NG+ (#2904)\n- fixed an illegal reachable slope in Lost Valley room 58, which could lead to Lara becoming softlocked (#2900)\n- fixed some pickup sprites being too far embedded into the floor (#2903)\n- fixed vase room sprites in Return to Egypt and Temple of the Cat being embedded in the floor (#2095)\n- fixed the camera behaving erratically in rooms/sectors that have no pathfinding data (#2946)\n- fixed the game crashing when editing long dev console history entries (#2913, regression from 4.10)\n- fixed FPS counter turning off after a game relaunch (#2911)\n- fixed falling ceiling and Damocles Sword traps not falling through stacked rooms (#2924)\n- fixed health bar in top center position covering inventory text\n- fixed the save crystal animation skipping a frame in 60 FPS (#1528)\n- fixed Lara unable to equip pistols after getting the Shotgun wet while wading (#2994)\n- fixed select level dialog not reacting to the menu back key (#2918, regression from 4.9)\n- fixed carried items falling from flying enemies not animating in 60 FPS (#2954, regression from 4.0)\n- fixed items carried by the Qualopec mummy spawning early after save/load (#2956, regression from 4.6)\n- fixed potential memory corruption if `/kill all` is used with a Qualopec mummy that is carrying items (#2957, regression from 4.6)\n- fixed a crash when portal debugging is enabled in rooms that have no portals (#2968, regression from 4.8)\n- fixed rats/voles and crocodiles/alligators at times not assuming the correct death pose after reloading a save (#2960, regression from 0.12)\n- fixed incorrect camera shifts when some fixed cameras return to normal view (#2971, regression from 4.9)\n- fixed Lara not having weapons when playing a level with -l/--level (#2995, regression from 4.9)\n- fixed inventory ring items not being animated when the ring is rotating (#2964, regression from 4.9)\n- fixed a hole appearing in the floor in Natla's Mines room 84 after exploding the TNT box (#3007, regression from 4.9)\n- fixed button mashing causing quick save/load to misbehave on a specific passport animation frame (#3021, regression from 4.10)\n- fixed save level numbers being replaced incorrectly if Lara dies in a level and the last save was in the previous level (#3026, regression from 4.9)\n- fixed ambient music not looping correctly after reloading a save with the option to reload ambient timestamps enabled (#3032, regression from 4.6)\n\n## [4.10.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.10.1...tr1-4.10.2) - 2025-05-15\n- fixed animated textures not working the right way in flipped rooms (#2966, regression from 4.10)\n- fixed the final statistics always showing zero deaths regardless of the actual total (#2965, regression from 4.10)\n\n## [4.10.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.10...tr1-4.10.1) - 2025-04-30\n- fixed water caustics appearance (#2896, regression from 4.10)\n\n## [4.10](https://github.com/LostArtefacts/TRX/compare/tr1-4.9...tr1-4.10) - 2025-04-30\nShowcase: https://www.youtube.com/watch?v=qJPq9obD6Cc\n- added an ability to customize the fog distances (#634)\n- added an ability to customize the water color [see the reference](../WATER_COLORS.md) (#1532)  \n- added support for a hex water color notation (eg. `#80FFFF`) in the game flow file\n- added support for antitriggers, like TR2+ (#2580)\n- added support for aspect ratio-specific images (#1840)\n- added an option to wraparound when scrolling UI dialogs, such as save/load (#2834)\n- added aliases to CLI options (`-gold` becomes `-g/--gold`, `-demo_pc` becomes `--demo-pc`)\n- improved bubble appearance (#2672)\n- improved rendering performance\n- improved pause exit dialog - it can now be canceled with escape\n- improved the `/set` console command to display available options if given an unknown argument\n- added a `--help` CLI option (may not output anything on Windows machines – OS bug)\n- changed the `draw_distance_fade` and `draw_distance_max` to `fog_start` and `fog_end`\n- changed `Select Detail` dialog title to `Graphic Options`\n- changed the number of static mesh slots from 50 to 256 (#2734)\n- changed the \"enable EIDOS logo\" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to \"enable legal\" (#2741)\n- changed sprite pickups to respect the water tint if placed underwater (#2673)\n- changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833)\n- changed the sound dialog appearance (repositioned and added text labels)\n- changed The Unfinished Business strings to default to the OG strings file for the main tables (#2847)\n- changed the dev console to no longer add duplicate entries to the history\n- removed the pretty pixels options (it's now always enabled, #2258)\n- fixed the bilinear filter to not readjust the UVs (#2258)\n- fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8)\n- fixed anisotropy filter causing black lines on certain GPUs (#902)\n- fixed mesh faces not being drawn under some circumstances (#2452, #2438)\n- fixed objects disappearing too early around screen edges (#2005)\n- fixed the trapezoid filter being toggled if Alt-F4 (either left or right) is used to close the game (#2690)\n- fixed enemies in one-click high water appearing with a water tint, and not making any animation sounds (#2753)\n- fixed the scale of the four keys in St. Francis' Folly (#2652)\n- fixed the panther at times not making a sound when it dies, and restored Skate Kid's death SFX (#2647)\n- fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776)\n- fixed Lara becoming clamped if she picks up an item under a steeply sloped ceiling (#2879)\n- fixed a crash when 3D pickups are disabled and Lara crosses a trigger to look at a pickup item (#2711, regression from 4.8)\n- fixed trapezoid filter warping on faces close to the camera (#2629, regression from 4.9)\n- fixed Mac builds crashing upon start (regression from 4.9)\n- fixed sprites rendering black if no shade value is assigned in the level (#2701, regression from 4.9)\n- fixed being stuck on the Restart Level page if using save crystals and F5 is pressed when no saves are present (#2700, regression from 4.8.2)\n- fixed being stuck on the Exit to Title page if using save crystals and a new save is made when there were previously none, and then F5 is pressed (#2700, regression from 4.9)\n- fixed the sprite UVs to restore the right and bottom edge pixels (#2672, regression from 4.8)\n- fixed sprites missing the fog effect (regression from 4.9)\n- fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 4.9)\n- fixed wrong PS1-style title bar color for the end of the level stats dialog (regression from 4.9)\n- fixed Story So Far showing up even when there's nothing to play (#2611, regression from 2.10)\n- fixed Story So Far not playing the opening FMV, `cafe.rpl` (#2779, regression from 2.10)\n- fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 3.0)\n- fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0)\n- fixed clicks in audio sounds (#2846, regression from 2.0)\n- fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 4.9)\n- fixed game crashing if the music folder was not present (#2887, regression from 4.9)\n- fixed the camera jumping if going from a look at trigger to a fixed camera (#3033, regression from 4.8)\n- fixed game crashing on unknown sequencer events\n\n## [4.9](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.3...tr1-4.9) - 2025-03-31\nShowcase: https://www.youtube.com/watch?v=AYVpnsYQNno\n- added quadrilateral interpolation (#354)\n- added `/flood` and `/drain` console commands\n- added support for `-l`/`--level` argument to play a single level\n- added support for `-s`/`--save` argument to immediately start a saved game\n- added support for custom levels to use `disable_floor` in the gameflow, similar to TR2's Floating Islands (#2541)\n- added drawing of object mesh spheres to the `/debug` console command\n- added TR2+ stats if the full stat detail mode option is enabled (#2561):\n    - ammo hits / used\n    - health packs used\n    - distance travelled\n- added a TR2+ style bordered stat box to the end of level stats if the full stat detail mode option is enabled (#2658)\n- changed the Controls screen to hide the reset and unbind texts when changing a key (#2103)\n- changed injections to a new file format with a smaller footprint and improved applicability tests (#1967)\n- changed the `/pos` command to show `Demo` and `Cutscene` instead of `Level` when relevant\n- changed the `/pos` command to show demo and cutscene numbers starting at 1, in line with `/play`\n- changed the `/play` and `/pos` commands to always treat the gym level as the level 0 – even if it's not included\n- changed sprites to respect the water tint if placed underwater (#2093)\n- changed the optional `Deaths` stat to be placed last in the stats menu\n- fixed delays when scanning available save games (#2610, #1335, regression from <3.0)\n- fixed several instances of the camera going out of bounds (#1034)\n- fixed issues with stacked, floating and flipmap pushblocks in custom levels\n- fixed issues with fixed cameras in 60 FPS shifting before settling on their target (#1186)\n- fixed missiles from mutants/centaurs/Natla jittering in 60 FPS (#1314)\n- fixed the bear AI fix option being applied in the Vilcabamba demo (#2559, regression from 4.8)\n- fixed extremely large item quantities crashing the game (#2497, regression from 0.3)\n- fixed Lara's meshes not resetting after using the fly cheat (#2565, #2572, regressions from 4.8)\n- fixed the select level feature not giving Lara her items (#2617, regression from 4.8)\n- fixed guns appearing in Lara's hands if the draw input is held when unarmed and while picking up a gun item (#2577, regressions from 0.8/4.3)\n- fixed being able to play with Lara invisible after using the explosion cheat then the fly cheat (#2584, regression from 4.8)\n- fixed the `/pos` command not working in cutscenes\n- fixed the `/pos` command not showing demo and cutscene titles\n- fixed the embedded bats fix causing problems inside rooms with trapdoors (regression from 4.6)\n- fixed cutscene music looping (#2591, regression from 4.8)\n- fixed saves created before version 2.15 causing a crash on load (#2654, regression from 4.8)\n- fixed the console opening when remapping its key (#2641)\n- removed perspective filter toggle (it had no effect; repurposed to trapezoid interpolation toggle)\n- improved camera mode navigation:\n    - improved support for pivoting\n    - improved roll support\n    - expanded world bounding box by 5 tiles in each direction\n    - added support for 60 FPS\n\n## [4.8.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.2...tr1-4.8.3) - 2025-02-17\n- fixed some of Lara's speech in the gym not playing in response to player action (#2514, regression from 4.8)\n- fixed passport text disappearing too quickly (#2512, regression from 4.8.2)\n- fixed inability to navigate to Select Level menu (#2518, regression from 4.8.2)\n- fixed NG+ flag causing problems with loading non-NG+ savegames (#2515, regression from 2.8)\n\n## [4.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.1...tr1-4.8.2) - 2025-02-15\n- improved memory usage by shedding ca. 100-110 MB on average\n- changed default FPS value to 60 (#2501)\n- changed passport to be more responsive to player inputs (#1328)\n- fixed Story So Far not skipping over levels (#2506, regression from 4.8)\n- fixed resolving paths (especially to music files) on case-sensitive filesystems (#1934, #2504)\n\n## [4.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.8...tr1-4.8.1) - 2025-02-14\n- fixed loading non-Caves saves triggering a new save prompt when save crystals are enabled (#2498, regression from 4.8)\n\n## [4.8](https://github.com/LostArtefacts/TRX/compare/tr1-4.7.1...tr1-4.8) - 2025-02-14\nShowcase: https://www.youtube.com/watch?v=td2Qz3nbRVo\n>[!WARNING]\n>Attention level builders: this version introduces backwards incompatible changes to the game flow file.\n>Please refer to the following documents to see how to update your levels:\n>- [Migration guide](../trx/MIGRATING.md)\n>- [Game flow documentation](../trx/game_flow/)\n>- [Game strings documentation](../trx/GAME_STRINGS.md)\n\n- added the ability to hold left/right to move through menus more quickly (#2298)\n- added an option for pickup aids, which will show an intermittent twinkle when Lara is nearby pickup items (#2076)\n- added an optional demo number argument to the `/demo` command\n- added pause screen support to demos\n- added a fade-out effect when exiting the pause screen to the inventory\n- added exit fade-out effects (#2348)\n- added optional dynamic lighting for gun flashes and explosions, similar to TR2+ (#2357)\n- added a `/cut` (alias: `/cutscene`) console command for playing cutscenes\n- added a `/gym` (alias: `/home`) console command for playing Lara's Home\n- added a `/music` console command that plays a specific music track\n- added a `/debug` console command that shows all triggers and portals\n- added a console log when using the `/demo` command\n- improved pause screen compatibility with PS1 (#2248)\n- improved level loading times with respect to injection processing\n- improved wireframe mode appearance around screen edges\n- ⚠️ changed the game data to use a separate strings file for text information, removing it from the game flow file\n- ⚠️ changed the game flow file internal structure\n- changed the object texture limit from 2048 to unlimited (within game's overall memory cap)\n- changed the sprite texture limit from 512 to unlimited (within game's overall memory cap)\n- changed demo to be interrupted only by esc or action keys\n- changed the turbo cheat to also affect ingame timer (#2167)\n- changed the pause screen to wait before yielding control during fade out effect\n- changed the compass and final stats to use two columns, similar to TR2 (doesn't apply to end-of-level \"bare\" stats)\n- changed the fix for transparent eyes on wolves to use black instead of off-white (#2252)\n- changed the `/kill` command with no arguments to look for enemies within 5 tiles (#2297)\n- fixed blood spawning on Lara from gunshots using incorrect positioning data (#2253)\n- fixed ghost meshes appearing near statics in custom levels (#2310)\n- fixed photo mode switching to the wrong flipmap rooms at times (#2362)\n- fixed the teleporting command sometimes putting Lara in invalid flipmap rooms (#2370)\n- fixed teleporting to an item on a ledge sometimes pushing Lara to the room below (#2372)\n- fixed secret and enemy speech not playing if the sound effects are missing from the level file (#2458)\n- fixed being unable to load a level that contains no sound effect data (#2460)\n- fixed input controller remaps not being saved across game relaunches (#2422, regression from 4.6)\n- fixed the upside-down camera fix to no longer limit Lara's vision (#2276, regression from 4.2)\n- fixed being unable to load some old custom levels that contain certain (invalid) floor data (#2114, regression from 4.3)\n- fixed a desync in the Lost Valley demo if responsive swim cancellation was enabled (#2113, regression from 4.6)\n- fixed the game hanging when Lara is on fire and enters the fly cheat on the same frame as reaching water (#2116, regression from 0.8)\n- fixed Lara activating triggers one frame too early (#2208, regression from 4.3)\n- fixed wrong underwater caustics speed with the turbo cheat (#2231)\n- fixed 1-frame UI flicker on pause screen exit confirmation\n- fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames (#2413)\n- fixed missing ricochet sprites in the gym (#2462)\n- fixed being able to use keys and puzzle items in keyholes/slots that have already been used (#2256, regression from 4.0)\n- fixed textures animating during demo fade-outs (#2217, regression from 4.0)\n- fixed waterfall mist not animating during demo (#2218, regression from 3.0)\n- fixed sound option arrows disappearing with specific volumes chosen (#2295, regression from 2.7)\n- fixed wireframe mode discarding transparent pixels (#2315, regression from 4.2)\n- fixed sprite pickup not being paused in the pause/inventory screen (#2319, regression from 4.1)\n- fixed 3D pickups not being paused in the pause/inventory screen (#2319, regression from 2.16)\n- fixed incorrect sprite sequences potentially animating after visiting a level with valid animating sprites (#2309, regression from 4.0)\n- fixed `/kill all` command destroying Scion and causing a soft lock in The Great Pyramid (#2329, regression from 4.4)\n- fixed health bar continuing to show when the inventory ring rotates (#1991, regression from 4.0)\n- fixed header and arrows disappearing when the inventory ring rotates (#2352, regression from 4.4)\n- fixed Story So Far feature not playing opening FMVs from the current level (#2360, regression from 4.2)\n- fixed `/play` command crashing the game when used after loading a level (#2411, regression)\n- fixed various death counter problems (existing saves will have the count reset) (#2264/#2412, regression from 2.6)\n- fixed `/demo` command crashing the game if no demos are present (regression from 4.1)\n- fixed Lara not being able to jump or stop swimming if the related responsive config options are enabled, but enhanced animations are not present (#2397, regression from 4.6)\n- fixed Lara not being able to jump if responsive jumping is disabled via the console in-level in certain scenarios (#2444, regression from 4.6)\n- fixed Lara being unable to climb or use guns after using an underwater lever and then entering the wading state (#2416, regression from 4.6)\n- fixed Eidos logo briefly flashing prior to the initial fade-in effect (#1388, regression from 4.1)\n- fixed Lara's meshes being incorrectly swapped in various scenarios using the fly cheat (#2461, regression from 4.7)\n\n## [4.7.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.7...tr1-4.7.1) - 2024-12-21\n- changed the inventory examine UI to auto-hide if the item description is empty (#2097)\n- fixed falling pickup items not being drawn when they land in rare cases (#2088)\n- fixed unbinding keys not working for controllers (#2090, regression from 4.6)\n- fixed hiding game UI causing the reset progressbar UI element to not show (regression from 4.7)\n\n## [4.7](https://github.com/LostArtefacts/TRX/compare/tr1-4.6.1...tr1-4.7) - 2024-12-20\nShowcase: https://www.youtube.com/watch?v=ThXt0I2j_QI\n- added support for Wayland in binary Linux builds (#1927)\n- added support for Unicode in gameflow JSON (#386, #636, #1919 and #1928)\n    Expanding on the 4.6's added support for named sequences, we now support\n    most of the characters the following Unicode planes:\n\n    - Basic Latin\n    - Cyrillic\n    - Greek and Coptic\n    - Latin-1 Supplement\n    - Latin Extended A\n\n    The sprites were created by Arsunt originally posted in the TRF topic here:\n    https://www.tombraiderforums.com/showthread.php?p=8396039\n\n    This should be enough to let gameflow editors provide full localisation for\n    the following languages:\n\n    Basque, Belarusian, Bosnian, Bulgarian, Catalan, Croatian, Czech, Danish,\n    Dutch, English, Estonian, Faroese, Finnish, French, Galician, German,\n    Greek, Hungarian, Icelandic, Indonesian, Irish, Italian, Latvian,\n    Lithuanian, Macedonian, Malay, Maltese, Northern Sami, Norwegian, Polish,\n    Portuguese, Romanian, Russian, Serbian, Slovak, Slovenian, Spanish,\n    Swedish, Turkish and possibly more.\n\n    Importantly, Asian and Arabic languages remain unsupported at the moment.\n- added the ability for falling pushblocks to kill Lara outright if one lands directly on her (#2035)\n- fixed clock drift accumulating with time (#1935, regression from 4.0)\n- fixed a potential invisible wall issue in custom levels with non-portal doors and certain geometry (#1958, regression from 4.3)\n- fixed transparent eyes on the wolf and bat models in Peru (#1945)\n- fixed incorrect transparent pixels on some Egypt textures (#1975)\n- fixed arrows overlapping with passport text if strings longer than the defaults are used (#1971)\n- fixed objects close to the camera being clipped (#819, regression from TombATI)\n- fixed the drawbridge in Obelisk of Khamoon not being angled correctly when open, which was resulting in embedded artefacts (#2006)\n- fixed incorrect positions on static meshes in Obelisk of Khamoon, Return to Egypt and Temple of the Cat (#2006)\n- fixed incorrect picture strides on certain hardware (#1979)\n- fixed doors at times disappearing if Lara is close to portals and the door's room is no longer visible (#2005)\n- fixed camera positions in Return to Egypt and Temple of the Cat (#1317, regression from 4.1)\n- fixed being able to see the flipmap in Natla's Mines when moving the boat (#2019)\n- fixed an invisible wall in Temple of the Cat, due to a wrongly positioned door (#2021)\n- fixed the `enable_console` config option not being adhered to (#2063, regression from 4.5)\n- fixed random pixels in the injected explosion sprites (#1985, regression from 4.6)\n\n## [4.6.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.6...tr1-4.6.1) - 2024-11-25\n- added ability to disable saves completely by setting the save slot to 0 (#1954)\n- fixed invisible walls being present in front of some doors (#1948, regression from 4.6)\n- fixed big font scale causing text overlaps in the graphics options (#1946)\n- fixed missing FMVs causing the game to go silent (#1931, regression from 4.6)\n- fixed game crashing when toggling the bilinear filter in passport (#1942, regression from 4.5)\n- fixed game crashing when changing the save slot with `/set` when in passport (#1954, regression from 4.2)\n\n## [4.6](https://github.com/LostArtefacts/TRX/compare/tr1-4.5.1...tr1-4.6) - 2024-11-18\nShowcase: https://www.youtube.com/watch?v=raSzSAu7kLI\n- added support for wading, similar to TR2+ (#1537)\n- added the ability to pause during cutscenes (#1673)\n- added an option to enable responsive swim cancellation, similar to TR2+ (#1004)\n- added a special target, \"pickup\", to item-based console commands\n- added support for custom levels to enforce values for any config setting (#1846)\n- added support for key/puzzle/pickup descriptions, allowing players to examine said items in the inventory (#1821)\n- added an option to fix inventory item usage duplication (#1586)\n- added optional automatic key/puzzle inventory item pre-selection (#1884)\n- added a search feature to the config tool (#1889)\n- improved enemy item drops by supporting the TR2+ approach of having drops defined in level data (#1713)\n- improved Italian localization for the Config Tool\n- improved the injection approach for Lara's responsive jumping (#1823)\n- improved the exploding Lara input cheat to always use explosion sprites\n- changed OpenGL backend to use version 3.3, with fallback to 2.1 if initialization fails (#1738)\n- changed text backend to accept named sequences. Currently supported sequences (limited by the sprites available in OG):\n    - `\\{umlaut}`\n    - `\\{hat}`\n    - `\\{acute accent}`\n    - `\\{grave accent}`\n    - `\\{arrow up}`\n    - `\\{arrow down}`\n    - `\\{small digit 0}`\n    - `\\{small digit 1}`\n    - `\\{small digit 2}`\n    - `\\{small digit 3}`\n    - `\\{small digit 4}`\n    - `\\{small digit 5}`\n    - `\\{small digit 6}`\n    - `\\{small digit 7}`\n    - `\\{small digit 8}`\n    - `\\{small digit 9}`\n    - `\\{button empty}`\n    - `\\{button triangle}`\n    - `\\{button circle}`\n    - `\\{button x}`\n    - `\\{button square}`\n    - `\\{button l1}`\n    - `\\{button r1}`\n    - `\\{button l2}`\n    - `\\{button r2}`\n    - `\\{button down}`\n    - `\\{button up}`\n    - `\\{button left}`\n    - `\\{button right}`\n    - `\\{icon sound}`\n    - `\\{icon music}`\n    - `\\{ammo shotgun}`\n    - `\\{ammo magnums}`\n    - `\\{ammo uzis}`\n- changed the `/pos` command to include the level number and title\n- changed the `/tp` command to teleport to items in a round-robin fashion\n  The first call will teleport Lara to the object that's the closest to her; repeated calls will cycle through all matching objects in the object placement order.\n- changed the music timestamp loading behaviour and config option to support ambient tracks (#1769)\n- removed health cheat (we now have the `/hp` command)\n- removed background for the \"Reset\" and \"Unbind\" labels in the controls dialog\n- removed `force_game_modes` and `force_save_crystals` from the gameflow - see GAMEFLOW.md for details on how to enforce these settings (#1857)\n- fixed a crash relating to audio decoding (#1895)\n- fixed missing pushblock SFX in Natla's Mines (#1714)\n- fixed crash reports not working in certain circumstances (#1738)\n- fixed missing trapdoor triggers in City of Khamoon (#1744)\n- fixed being unable to rename the lead bar (#1774, regression from 4.5)\n- fixed the controls menu extending to the bottom of the screen with certain text scaling values (#1783, regression from 2.12)\n- fixed game stuck at remapping controller key if no controllers connected (#1788)\n- fixed being able to shoot the scion multiple times if save/load is used while it blows up (#1819)\n- fixed certain erroneous `/play` invocations resulting in duplicated error messages\n- fixed the `/play` console command resulting in Lara starting the target level without pistols (#1861, regression from 4.5)\n- fixed the demo mode text overlapping with the enemy health bar if the health bar is located in the bottom centered (#1446)\n- fixed mutant explosions sometimes heavily damaging Lara even if they missed (#1758, regression since 4.5)\n- fixed wrongly calculated trapdoor size that could affect custom levels (#1904)\n\n## [4.5.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.5...tr1-4.5.1) - 2024-10-14\n- fixed mac builds missing embedded resources (#1710, regression from 4.5)\n\n## [4.5](https://github.com/LostArtefacts/TRX/compare/tr1-4.4...tr1-4.5) - 2024-10-08\nShowcase: https://www.youtube.com/watch?v=eMnVYbB4QBc\n- added a photo mode feature (#1669)\n- added `/sfx` command\n- added `/nextlevel` alias to `/endlevel` console command\n- added `/quit` alias to `/exit` console command\n- added an option to toggle the in-game UI, such as healthbars and ammo text (#1656)\n- added the ability to cycle through console prompt history (#1571)\n- added Lara's exit-water-to-medium-height animation from TR2+ (#1538)\n- improved object name matching in console commands to work like TR2X\n- improved vertex movement when looking through water portals even more (#1493)\n- improved console commands targeting creatures and pickups (#1667)\n- changed the easter egg console command to pack more punch\n- changed `/set` console command to do fuzzy matching (LostArtefacts/libtrx#38)\n- removed dedicated camera reset button in favor of pressing the look button (#1658)\n- fixed console caret position off by a couple of pixels (regression from 3.0)\n- fixed holding a key when closing the console registering as a game input (regression from 3.0)\n- fixed ability to crash the game with extreme FOV values (regression from 0.9)\n- fixed double \"Fly mode enabled\" message when using `/fly` console command (regression from 4.0)\n- fixed crash in the `/set` console command (regression from 4.4)\n- fixed toggling fullscreen not always saving (regression from 4.4)\n- fixed altering fov with `/set` not being immediately respected (#1547)\n- fixed main menu music volume when exiting while underwater with certain music settings (#1540, regression from 4.4)\n- fixed `/kill` command unable to target a special object\n- fixed really fast typing in console sometimes losing the first input (regression from 4.4)\n- fixed Lara's head not matching the braid if in use when she is killed by the T-rex (#1549)\n- fixed `/endlevel` displaying a success message in the title screen\n- fixed Story So Far feature looping cutscenes forever (#1551, regression from 4.4)\n- fixed a rare crash related to the camera that could affect custom levels (#1671)\n- fixed a bug when saving and loading when picking up an item or using a switch with animated interactions enabled (#1546)\n- fixed a bug where Lara was stuck for a long time in an animated interactions if pushed (#1687)\n\n## [4.4](https://github.com/LostArtefacts/TRX/compare/tr1-4.3...tr1-4.4) - 2024-09-20\nShowcase: https://www.youtube.com/watch?v=3XOSl9WqH3A\n- added `/exit` command (#1462)\n- added reflections to Midas Hand death animation and savegame crystals (#154)\n- added an option to use PS1 tinted savegame crystals (#1506)\n- improved appearance of textures around edges when bilinear filter is off (#1483)\n  Since this removes the seams on pushblocks, this was made optional.\n- improved level load times (#1456, #1457)\n- improved logs module names readability\n- improved crash debug information on Windows\n- improved vertex movement when looking through water portals (#1493)\n- improved anisotropic filter rendering (#902, #1507)\n- improved skybox appearance (#1520)\n- fixed `/play`, `/load`, `/demo` and similar commands not working in stats, credits, cinematics and fmvs (#1477)\n- fixed console commands being able to interfere with demos, cutscenes and the title screen (#1489, regression from 3.0)\n- fixed reopening the compass not resetting its needle (#1472, regression from 4.0)\n- fixed holstering pistols hiding the gun meshes 1 frame too early (#1449, regression from 0.6)\n- fixed Lara's sliding animation sometimes being interrupted by a stumble (#1452, regression from 4.3)\n- fixed cameras with glide values sometimes moving in the wrong direction (#1451, regression from 4.3)\n- fixed `/give` console command giving duplicate items under some circumstances (#1463, regression from 3.0)\n- fixed `/give` console command confusing logging around mismatched items (#1463, regression from 3.0)\n- fixed `/give` console command unable to give Scion in Tomb of Qualopec and Sanctuary (regression from 3.0)\n- fixed `/flip` console command misreporting an already enabled flipmap as off (regression from 4.0)\n- fixed `/kill` console command not fully killing enemies (#1482, regression from 3.0)\n- fixed `/tp` console command not always picking the closest item (#1486, regression from 4.1)\n- fixed `/tp` console command reporting teleport fails as success (#1484, regression from 4.1)\n- fixed `/tp` console command allowing teleporting to consumed savegame crystals (#1518)\n- fixed `/hp` console command taking arbitrary integers\n- fixed `/set` console command crashing with unknown targets (regression from 4.2)\n- fixed `/set` console command not sanitizing numeric values (#1515)\n- fixed console commands causing improper ring shutdown with selected inventory item (#1460, regression from 3.0)\n- fixed console input immediately ending demo (#1480, regression from 4.1)\n- fixed a potential softlock when killing the Torso boss in Great Pyramid (#1236)\n- fixed Bacon Lara re-spawning after saving and loading (#1500, regression from 0.7)\n- fixed config JSON not sanitizing some numeric values (#1515)\n- fixed potential crashes in custom levels if hybrid creature objects are not present in the level (#1444)\n- changed the target change functionality from the look key to a new, standalone key (default `z` on keyboard, `left analog click` on controller) (#1503)\n- changed `/heal` console command to also extinguish Lara\n- changed `/tp` console command to look for the closest place to teleport to when targeting items (#1484)\n- changed `/set` console command output to always use fully-qualified option names\n- changed `/fps`, `/vsync`, `/wireframe`, `/braid` and `/cheats` console commands output to be in line with `/set` console command output\n- changed the door cheat to also target drawbridges\n\n## [4.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.2...tr1-4.3) - 2024-08-15\nShowcase: https://www.youtube.com/watch?v=kc2oo-pSMh0\n- added deadly water feature from TR2+ for custom levels (#1404)\n- added skybox support, with a default option provided for Lost Valley, Colosseum and Obelisk of Khamoon (#94)\n- added an option for Lara to use her underwater swimming physics from TR2+ (#1003)\n- added weapons to Lara's empty holsters on pickup (#1291)\n- added options to quiet or mute music while underwater (#528)\n- improved initial level load time by lazy-loading audio samples (LostArtefacts/TR2X#114)\n- changed the turbo cheat to no longer affect the gameplay time (#1420)\n- changed weapon pickup behavior when unarmed to set any weapon as the default weapon, not just pistols (#1443)\n- fixed adjacent Midas Touch objects potentially allowing gold bar duplication in custom levels (#1415)\n- fixed the excessive pitch and playback speed correction for music files with sampling rate other than 44100 Hz (#1417, regression from 2.0)\n- fixed the ingame timer being skewed upon inventory open (#1420, regression from 4.1)\n- fixed Lara able to reach triggers through closed doors (#1419, regression from 1.1.4)\n- fixed Lara voiding when loading the game on a closed door (#1419)\n- fixed underwater caustics not resumed smoothly when unpausing (#1423, regression from 3.2)\n- fixed collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground (#606)\n- fixed an issue with a missing Spanish config tool translation for the target mode (#1439)\n- fixed carrying over unexpected guns in holsters to the next level under rare scenarios (#1437, regression from 2.4)\n- fixed item cheats not updating Lara holster and backpack meshes (#1437)\n\n## [4.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.1.2...tr1-4.2) - 2024-07-14\nShowcase: https://www.youtube.com/watch?v=gV7oz0wEzWk\n- added creating minidump files on crashes\n- added new console commands:\n    - `/hp`\n    - `/hp [num]`\n    - `/heal`\n    - `/wireframe`\n    - `/set`\n- added unobtainable secrets stat support in the gameflow (#1379)\n- added a wireframe mode\n- changed console caret blinking rate (#1377)\n- changed the TR1X install source in the installer to suggest using the existing installation directory (#1350)\n- fixed config tool and installer missing icons (#1358, regression from 4.0)\n- fixed looking forward too far causing an upside down camera frame (#1338)\n- fixed the enemy bear behavior in demo mode (#1370, regression from 2.16)\n- fixed the FPS counter overlapping the healthbar in demo mode (#1369)\n- fixed the Scion being extremely difficult to shoot with the shotgun (#1381)\n- fixed lightning rendering z-buffer issues (#1385, regression from 1.4)\n- fixed possible game crashes if more than 16 savegame slots are set (#1374)\n- fixed savegame slots higher than 64 not working (#1395)\n- fixed a crash in custom levels if a room had more than 1500 vertices (#1398)\n- fixed a potential crash or silence with certain music files (#794, regression from 2.0)\n- fixed the console command to change FPS also starting demo mode (#1368)\n- fixed text blinking rate being different in 30 and 60 FPS (#1377)\n- fixed slow sound volume adjustment at 60 FPS when holding arrow keys (#1407)\n\n## [4.1.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.1.1...tr1-4.1.2) - 2024-04-28\n- fixed pictures display time (#1349, regression from 4.1)\n\n## [4.1.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.1...tr1-4.1.1) - 2024-04-27\n- fixed reading animated texture data in levels (#1346, regression from 4.1)\n\n## [4.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.0.3...tr1-4.1) - 2024-04-26\nShowcase: https://www.youtube.com/watch?v=ioo2P0FuFWU\n- added ability to show enemy healthbars only for bosses (#1300)\n- added ability to kill specific enemy types (#1313)\n- added ability to teleport to nearest specific object (#1312)\n- added `/load` and `/save` commands for even quicker savegame operations\n- added `/demo` command to quickly play the demo\n- added `/title` command to quickly exit to title\n- added `/vsync on` and `/vsync off` commands to toggle the VSync setting\n- added `/give all` variant of the item cheat\n- changed injection files to be placed in its own directory (#1306)\n- changed item cheat sound effects\n- changed the `/play` command to work immediately in the title screen\n- fixed turbo cheat speed setting not saved across game relaunches (#1320)\n- fixed turbo cheat behavior with the following game elements (#1341):\n    - animated textures animation rate (regression from 4.0.3)\n    - 3D pickups animation rate (regression from 4.0.3)\n    - healthbar flashing rate\n    - UI text flashing rate\n    - inventory stats timer\n    - underwater wibble effect rate\n    - loading screen and credit images display time\n    - title screen demo delay\n    - fade times\n- fixed camera vibrations when using the teleport command in 60 FPS (#1274)\n- fixed the camera being thrown through doors for one frame when looked at from fixed camera positions (#954)\n- fixed console not retaining changed user settings across game relaunches (#1318)\n- fixed passport inventory item not being animated in 60 FPS (#1314)\n- fixed object explosions not being animated in 60 FPS (#1314)\n- fixed lava emitters not being animated in 60 FPS (#1314)\n- fixed underwater bubbles not being animated in 60 FPS (#1314)\n- fixed compass needle being too fast in 60 FPS (#1316, regression from 4.0)\n- fixed black screen flickers that can occur in 60 FPS (#1295)\n- fixed a slight delay with the passport menu selector (#1334)\n- decreased initial flicker upon game launch (#1322)\n\n## [4.0.3](https://github.com/LostArtefacts/TRX/compare/tr1-4.0.2...tr1-4.0.3) - 2024-04-14\n- fixed flickering sprite pickups (#1298)\n\n## [4.0.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.0.1...tr1-4.0.2) - 2024-04-11\n- fixed Mac binaries not working on x86-64 (eg not Apple Silicon)\n- fixed building on Linux outside of the Docker toolchain (#1296, regression from 4.0)\n\n## [4.0.1](https://github.com/LostArtefacts/TRX/compare/tr1-4.0...tr1-4.0.1) - 2024-04-10\n- fixed trying to pick up a lead bar crashing the game (#1293, regression from 4.0)\n\n## [4.0](https://github.com/LostArtefacts/TRX/compare/tr1-3.1.1...tr1-4.0) - 2024-04-09\nShowcase: https://www.youtube.com/watch?v=-ED8HSHdHHQ&t=63s\n- added experimental support for 60 FPS, available from the in-game graphics menu\n- added ability to slow the game down using the turbo cheat (#1215)\n- added /speed command to control the turbo cheat (#1215)\n- added the option to change weapon targets by tapping the look key like in TR4+ (#1145)\n- added three targeting options: full lock always keeps target lock (OG), semi lock loses target lock if the enemy dies, and no lock loses target lock if the enemy goes out of sight or dies (TR4+) (#1146)\n- added an option to the installer to install from a CD drive (#1144)\n- added stack traces to logs for better crash debugging (#1165)\n- added an option to use PS1 loading screens (#358)\n- added high quality images for the Eidos, Unfinished Business title, Unfinished Business credit, and final statistics screens\n- added support for macOS builds (for both Apple Silicon and Intel)\n- added optional support for OpenGL 3.3 Core Profile\n- added Italian localization to the config tool\n- added the ability to move the look camera while targeting an enemy in combat (#1187)\n- added the ability to skip fade-out in stats screens\n- added support for animated room sprites in custom levels and an option to animate plant sprites in The Cistern and Tomb of Tihocan (#449)\n- added on-screen messages for certain actions (#1220)\n- changed stats no longer disappear during fade-out (#1211)\n- changed the way music timestamps are internally handled – resets music position in existing saves\n- changed vertex and fragment shaders into unified files that are runtime pre-processed for OpenGL versions 2.1 or 3.3\n- changed the `/kill` command to use Lara as a reference point, and kill all creatures that are within a single tile first (#1256)\n- changed the config not to save key mappings if they do not deviate from the current version's defaults (#1218)\n- changed the item cheat keybind to also work in Gym\n- changed the item cheat command to display a relevant message if Lara object is not loaded\n- fixed a missing translation for the Spanish config tool for the Eidos logo skip option (#1151)\n- fixed a flipmap issue in Natla's Mines that could make the cabin appear stacked and prevent normal gameplay (#1052)\n- fixed several texture issues across the majority of levels (#1231)\n- fixed broken gorilla animations (#1244, regression from 2.15.3)\n- fixed saving and loading the music timestamp when the load current music option is enabled and game sounds in inventory are disabled (#1237)\n- fixed the remember played music option always being enabled (#1249, regression from 2.16)\n- fixed the underwater SFX playing for one frame at the start of Palace Midas (#1251)\n- fixed an incorrect frame in Lara's underwater twist animation (OG bug in TR2 onwards) (#1242)\n- fixed Lara saying \"no\" when taking valid actions in front of a key item receptacle (#1268)\n- fixed Lara not saying \"no\" when using the Scion incorrectly (#1278)\n- fixed flickering in bats' death animations and rapid shooting if Lara continues to fire when they are killed (#992)\n- fixed an incorrect animation in the door used at the beginning of Colosseum (#1287)\n\n## [3.1.1](https://github.com/LostArtefacts/TRX/compare/tr1-3.1...tr1-3.1.1) - 2024-01-19\n- changed quick load to show empty passport instead of opening the save game menu when there are no saves (#1141)\n- fixed a game crash when the quick load passport is deselected (#1136, regression from 3.1)\n- fixed not being able to save in an empty slot using quick save if the load game menu was opened before (#1140, regression from 3.1)\n- fixed the passport briefly flashing inaccessible page text (#1137, regression from 3.1)\n\n## [3.1](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.5...tr1-3.1) - 2024-01-14\n- added the option to use \"shell(s)\" to give shotgun ammo in the developer console (#1096)\n- added the restart level option to the passport in save crystal mode (#1099)\n- added the ability to back out of menus with the circle and triangle buttons when using a gamepad (cross acts as confirm) (#1104)\n- changed `force_enable_save_crystals` to `force_save_crystals` for custom level authors to force enable or disable the save crystals setting (#1102)\n- changed `force_disable_game_modes` to `force_game_modes` for custom level authors to force enable or disable the game modes setting (#1102)\n- changed the Scion in The Great Pyramid from spawning blood when hit to a ricochet effect if texture fixes enabled (#1121)\n- changed the gamepad control menu's 'reset all buttons' bind to held R1 (was held triangle) (#1104)\n- changed the number of visible enemies from 8 to 32 (#1122)\n- fixed FMVs always playing at 100% volume – now they'll play at the game sound volume (#1110)\n- fixed bugs when trying to stack multiple movable blocks (#1079)\n- fixed Lara's meshes being swapped in the gym level when using the console to give guns (#1092)\n- fixed Midas's touch having unrestricted vertical range (#1094)\n- fixed flames not being drawn when Lara is on fire but leaves the room where she caught fire (#1106)\n- fixed being able to deselect the passport in quick save, quick load, save crystal, and death modes (#1108)\n- fixed inability to save in Unfinished Business in crystals mode as UB doesn't have crystals (#1102)\n- fixed items not being added to inventory if the sprite is missing from the level file (#1130)\n- fixed differences when looking at items from triggers that do not use fixed cameras when the enhanced look option is enabled (#1026)\n\n## [3.0.5](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.4...tr1-3.0.5) - 2023-12-13\n- fixed crash when pressing certain keys and the console is disabled (#1116, regression from 3.0)\n- fixed lightning bolts wrongly drawn (#1113, regression from 0.9)\n\n## [3.0.4](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.3...tr1-3.0.4) - 2023-12-08\n- fixed missiles damaging Lara when she is far beyond their damage range (#1090)\n- fixed pushblocks moving freely if Lara releases but tries to regrab during the release animation (#1101, regression from 3.0)\n\n## [3.0.3](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.2...tr1-3.0.3) - 2023-11-27\n- fixed underwater shadow effects rendering always in the same way rather than at random (#1081)\n\n## [3.0.2](https://github.com/LostArtefacts/TRX/compare/tr1-3.0.1...tr1-3.0.2) - 2023-11-11\n- fixed incorrect usage reference URLs in the gameflow files (#1073)\n- fixed random number generation becoming stuck after entering and leaving the inventory, which affected effects and SFX (#1070, #1074)\n\n## [3.0.1](https://github.com/LostArtefacts/TRX/compare/tr1-3.0...tr1-3.0.1) - 2023-11-10\n- fixed installer not detecting old Tomb1Main installations (#1071)\n\n## [3.0](https://github.com/LostArtefacts/TRX/compare/tr1-2.16...tr1-3.0) - 2023-11-09\nShowcase: https://www.youtube.com/watch?v=vqvOkZzHx6M\n- renamed the project from Tomb1Main to TR1X in an effort to establish our own unique identity, while respectfully disassociating from TR2Main.\n- added developer console (accessible with `/`, see [2-COMMANDS.md] for details)\n- added Linux builds and toolchain\n- added an option to allow Lara to roll while underwater, similar to TR2+ (#993)\n- added an option to turn off Eidos logo entirely through config (#1044)\n- added the bonus level type for custom levels that unlocks if all main game secrets are found (#645)\n- added detection for animation commands to play SFX on land, water or both (#999)\n- added support for customizable enemy item drops via the gameflow (#967)\n- added an option to enable F-key and inventory frame buffering (#591)\n- added a pickup overlay for the Midas gold bar when it changes from lead (#1010)\n- added an option to allow Lara to creep forwards or backwards further when performing neutral jumps, in line with TR2+ (#998)\n- added an option to the installer to choose between the original and fan-made Unfinished Business level sets (#1019)\n- fixed baddies dropping duplicate guns (only affects mods) (#1000)\n- fixed Lara never using the step back down right animation (#1014)\n- fixed dead crocodiles floating in drained rooms (#1031)\n- fixed 3d pickups sometimes triggering z-buffer issues (#1015)\n- fixed oversized passport in cinematic camera mode (eg when Lara steps on the Midas Hand) (#1009)\n- fixed braid being disabled by default unless the player runs the config tool first (#1043)\n- fixed various bugs with falling movable blocks (#723)\n- fixed the incorrect positioning of door 12 in Tomb of Tihocan (#1063)\n- fixed a potential softlock in The Cistern by restoring a missing trigger in room 56 (#1066)\n- improved frame scheduling to use less CPU (#985)\n- improved and expanded gameflow documentation (#1018)\n- rotated the Scion in Tomb of Qualopec to face the the main gate and Qualopec (#1007)\n\n## [2.16](https://github.com/LostArtefacts/TRX/compare/tr1-2.15.3...tr1-2.16) - 2023-09-20\n- added a new rendering mode called \"framebuffer\" that lets the game to run at lower resolutions (#114)  \n  (forces players to reset their bilinear filter setting)\n- added the current music track and timestamp to the savegame so they now persist on load (#419)\n- added the triggered music tracks to the savegame so one shot tracks don't replay on load (#371)\n- added forward/backward input detection in line with TR2+ for jump-twists (#931)\n- added an option to restore the mummy in City of Khamoon room 25, similar to the PS1 version (#886)\n- added a flag indicating if new game plus is unlocked to the player config which allows the player to select new game plus or not when making a new game (#966)\n- improved Spanish localization for the config tool\n- improved support for windowed mode (#896)\n- changed sprite-based pickups to 3D pickups when the 3D pickups option is enabled (#257)\n- changed the installer to always overwrite all essential files such as the gameflow and injections (#904)\n- changed the data injection system to warn when it detects invalid or missing files, rather than preventing levels from loading (#918)\n- changed the gameflow to detect and skip over legacy sequence types, rather than preventing the game from starting (#882)\n- moved the enable_game_modes option from the gameflow to the config tool and added a gameflow option to override (#962)\n- moved the enable_save_crystals option from the gameflow to the config tool (#962)\n- fixed Natla's gun moving while she is in her semi death state (#878)\n- fixed an error message from showing on exiting the game when the gym level is not present in the gameflow (#899)\n- fixed the bear pat attack so it does not miss Lara (#450)\n- fixed some incorrectly rotated pickups when using the 3D pickups option (#253)\n- fixed dead centaurs exploding again after saving and reloading (#924)\n- fixed the incorrect starting animation on centaurs that spawn from statues (#926, regression from 2.15)\n- fixed jump-twist animations at times being interrupted (#932, regression from 2.15.1)\n- fixed walk-run-jump at times breaking when TR2 jumping is enabled (OG bug in TR2+) (#934)\n- fixed Lara jumping late with TR2 jumping enabled, as compared to normal TR1 jumping when entering the run animation initially (#975)\n- fixed the reset and unbind progress bars in the controls menu for non-default bar scaling (#930)\n- fixed original data issues where music triggers are not set as one shot (#939)\n- fixed a missing enemy trigger in Tomb of Tihocan (#751)\n- fixed incorrect trapdoor triggers in City of Khamoon and a switch trigger in Obelisk of Khamoon (#942)\n- fixed the setup of two music triggers in St. Francis' Folly (#865)\n- fixed data portal issues in Atlantean Stronghold that could result in a crash (#227)\n- fixed the camera in Natla's Mines when pulling the lever in room 67 (#352)\n- fixed flame emitter saving and loading which caused rare crashing (#947)\n- fixed new game plus not working if enable_game_modes was set to false (#960, regression from 2.8)\n- fixed Alt-Enter triggering game actions (#979, regression from 2.15)\n- fixed Natla spinning in her semi-death and second phases when more than one is active in the level (#906)\n- fixed FPS counter, perspective filter and texture filter not always saved when changed from keyboard (#988)\n\n## [2.15.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.15.2...tr1-2.15.3) - 2023-08-15\n- fixed Lara stuttering when performing certain animations (#901, regression from 2.14)\n- fixed Lara not grabbing certain edges when the swing-cancel option is enabled (#911)\n\n## [2.15.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.15.1...tr1-2.15.2) - 2023-07-17\n- fixed Natla not leaving her semi-death state after Lara takes her down for the first time (#892, regression from 2.15.1)\n\n## [2.15.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.15...tr1-2.15.1) - 2023-07-14\n- fixed the ape not performing the vault animation when climbing (#880)\n- fixed holding down up or down to scroll the passport faster (#883, regression from 2.14)\n- fixed Lara becoming stuck in a T-pose on rare occasions after performing a jump tiwst (#889)\n\n## [2.15](https://github.com/LostArtefacts/TRX/compare/tr1-2.14...tr1-2.15) - 2023-06-08\n- added an option to enable TR2+ jump-twist and somersault animations (#88)\n- added the ability to unbind the sidestep left and sidestep right keys (#766)\n- added a cheat to explode Lara like in TR2 and TR3 (#793)\n- added an inverted look camera option (#700)\n- added a camera speed option for the manual camera (#815)\n- added an option to fix original texture issues (#826)\n- added menu specific controls meaning arrow keys, return, and escape now always function in menus (#814, regression from 2.12)\n- added forward/backward jumps while looking and looking up/down while hanging if enhanced look is enabled (#848)\n- added case insensitive directory and file detection (#845)\n- added controller detection during runtime (#850)\n- added an option to allow cancelling Lara's ledge-swinging animation (#856)\n- added an option to allow Lara to jump at any point while running, similar to TR2+ (#157)\n- added the ability to define the anchor room for Bacon Lara in the gameflow (#868)\n- changed screen resolution option to apply immediately (#114)\n- changed shaders to use GLSL 1.20 which should fix most issues with OpenGL 2.1 (#327, #685)\n- changed Bacon Lara to prevent movement after her death (#875)\n- fixed sounds stopping instead of pausing if game sounds in inventory are disabled (#717)\n- fixed skipping Eidos logo and end credits (#541)\n- fixed ceiling heights at times being miscalculated, resulting in camera issues and Lara being able to jump into the ceiling (#323)\n- fixed Lara not being able to jump off trapdoors or crumbling floors if the sidestep descent fix is enabled (#830)\n- fixed walk to pickups feature (#834, regression from 2.8)\n- fixed .mpeg FMVs not working (#844)\n- fixed the restart level passport text incorrectly showing new game in Lara's Home (#851)\n- fixed quick load creating an invalid save if used when no saves are present (#853)\n- fixed Lara entering body hit animations when not appropriate to do so (#857)\n- fixed SkateKid causing a game crash when too many enemies are active (#866)\n- fixed missiles damaging Lara when she is far beyond their damage range (#871)\n\n## [2.14](https://github.com/LostArtefacts/TRX/compare/tr1-2.13.2...tr1-2.14) - 2023-04-05\n- added Spanish localization to the config tool\n- added an option to launch Unfinished Business from the config tool (#739)\n- added dart emitters to the savegame (#774)\n- added the ability for level builders to stop all music via triggers (#785)\n- added an option to prevent enemy speeches stopping the current music track (#762)\n- improved the control of Lara's braid to result in smoother animation and to detect floor collision (#761)\n- increased the number of effects from 100 to 1000 (#623)\n- changed the health, air, and enemy bars to better match the PS1 version (#698)\n- removed the fix_pyramid_secret gameflow sequence (now handled by data injection) (#788)\n- fixed Larson's gun textures in Tomb of Qualopec to match the cutscene and Sanctuary of the Scion (#737)\n- fixed texture issues in the Cowboy, Kold and Skateboard Kid models (#744)\n- fixed the savegame requestor arrow's position with a large number of savegames and long level titles (#756)\n- fixed empty holsters when starting a level with the shotgun equipped (#749)\n- fixed a crash when taking a screenshot of an opening FMV (#445)\n- fixed the animation of Lara's left arm when the shotgun is equipped (#771)\n- fixed Lara's braid not turning to gold during the Midas touch animation (#769)\n- fixed the equipped weapon's ammo showing on the inventory screen (#777)\n- fixed the health, air, and enemy bars from being affected by the text scaling option (#698)\n- fixed music triggers with partial masks killing the ambient track (#763)\n- fixed the text and bar scaling from being able to be set below the max and min  (#698)\n- fixed a data issue in Colosseum, which prevented a bat from triggering (#750)\n- fixed lightning and gun flash continuing to animate in the inventory, pause and statistics screens (#767)\n- fixed the FPS, healthbar, and arrows from overlapping on the inventory screen (#787)\n\n## [2.13.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.13.1...tr1-2.13.2) - 2023-03-10\n- fixed depth buffer size causing rendering issues on some hardware (#748, regression from 2.13)\n- fixed a game crash when loading a save in which Lara had been struck by an exploding missile (#746)\n\n## [2.13.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.13...tr1-2.13.1) - 2023-03-03\n- added an option to use the PlayStation Uzi sound effects (#152)\n- fixed a few flip effect sounds not playing (#743, regression from 2.12.1)\n- fixed a game crash when exiting the game with a controller connected (#663)\n\n## [2.13](https://github.com/LostArtefacts/TRX/compare/tr1-2.12.1...tr1-2.13) - 2023-02-19\n- added the ability to inject data into levels, with Lara's braid being the initial focus (#27)\n- added support for .ogg, .mp3 and .wav formats for audio tracks (#688)\n- added the mummy to the level kill stats if Lara touches it and it falls (#701)\n- fixed save crystal collision pushing Lara through walls (#682)\n- fixed passport animation when deselecting the passport (#703)\n- fixed inconsistent wording in config tool health and air color options (#705)\n- fixed Scion 1 respawning on load (#707)\n- fixed dead water rats looking alive when a room's water is drained (#687, regression from 0.12.0)\n- fixed triggered flip effects not working if there are no sound devices (#583)\n- fixed the incorrect ceiling textures in Colosseum (#131)\n\n## [2.12.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.12...tr1-2.12.1) - 2023-01-16\n- fixed crash when using enhanced saves in levels with flame emitters (#693)\n- fixed the death counter from breaking old saves if enhanced saves are turned on (#699)\n\n## [2.12](https://github.com/LostArtefacts/TRX/compare/tr1-2.11...tr1-2.12) - 2022-12-23\n- added collision to save crystals (#654)\n- added additional custom control schemes (#636)\n- added the ability to unbind unessential keys (#657)\n- added the ability to reset control schemes to default (#657)\n- added customizable controller support (#659)\n- added French localization to the config tool (#664)\n- fixed small cracks in the UI borders for PS1-style menus (#643)\n- fixed Lara loading inside a movable block if she's on a stack near a room portal (#619)\n- fixed a game crash on shutdown if the action button is held down (#646)\n- fixed the compass and new game menus at high text scaling (#648)\n- fixed save crystals so they are single use (#654)\n- fixed demo mode if the do not heal on level finish option is used (#660)\n- removed the puzzle key sound effect when using save crystals (#654)\n- stopped the default controls from functioning when the user unbound them (#564)\n\n## [2.11](https://github.com/LostArtefacts/TRX/compare/tr1-2.10.3...tr1-2.11) - 2022-10-19\n- added a .NET-based configuration tool (#633)\n- added graphics effects, lava emitters, flame emitters, and waterfalls to the savegame so they now persist on load (#418)\n- added an option to turn off sound effect pitching (#625)\n- changed passport to highlight latest save at game start (#618)\n- fixed some sound effects playing in the inventory when disable_music_in_inventory is true (#486)\n- fixed underwater currents breaking in rare cases (#127)\n- fixed gameflow option remove_guns preventing weapon pickups in rare situations (#611)\n- fixed gameflow option remove_scions causing Lara to equip weapons even if she has none (#605)\n- added gameflow option remove_ammo to remove all shotgun, magnum and uzi ammo from the inventory on level start (#599)\n- added gameflow option remove_medipacks to remove all medi packs from the inventory on level start (#599)\n- improved the UI frame drawing, it will now look consistent across all resolutions and no longer have gaps between the lines\n- fixed bridge item in City of Khamoon being incorrectly raised (#627)\n- fixed Lara firing blanks indefinitely when she doesn't have pistols and is out of ammo on non-pistol weapons (#629) \n\n## [2.10.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.10.2...tr1-2.10.3) - 2022-09-15\n- fixed save crystal mode always saving in the first slot (#607, regression from 2.8)\n\n## [2.10.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.10.1...tr1-2.10.2) - 2022-08-03\n- fixed revert_to_pistols ignoring gameflow's remove_guns (#603)\n\n## [2.10.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.10...tr1-2.10.1) - 2022-07-27\n- fixed Lara being able to equip pistols in the gym level (#594)\n\n## [2.10](https://github.com/LostArtefacts/TRX/compare/tr1-2.9.1...tr1-2.10) - 2022-07-26\n- added a .NET-based installer\n- added the option to make Lara revert to pistols on new level start (#557)\n- added the PS1 style UI (#517)\n- added the \"Story So far...\" option in the select level menu to view cutscenes and FMVs (#201)\n\n## [2.9.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.9...tr1-2.9.1) - 2022-06-03\n- fixed crash on centaur hatch (#579, regression from 2.9)\n\n## [2.9](https://github.com/LostArtefacts/TRX/compare/tr1-2.8.2...tr1-2.9) - 2022-06-01\n- added generic SDL-based controller support (#278)\n- added the ability to make freshly triggered (runaway) Pierre replace an already existing (runaway) Pierre (#532)\n- added a fade out when completing Lara's Home (#383)\n- added the config option to change the number of save slots (#170)\n- changed default save slot count to 25 (#170)\n- removed DInput-based XBox controller support\n- fixed Tihocan chain block sound (#433)\n- fixed passport menu with high UI scaling (#546, regression from 2.7)\n- fixed passport menu border being off by one pixel (#547)\n- fixed the new game and save game passport options using the wrong closing animation (#542, regression from 2.7)\n- fixed bridges at floor level appearing under the floor (#523)\n- fixed Lara's outfit in Lara's Home when replaying the level (#571, regression from 2.7)\n- fixed crash when dying in the gym level with no saves (#576, regression from 2.8)\n- fixed exiting select level menu causing deaths in a new game incremented in that slot (#575, regression from 2.8)\n\n## [2.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.8.1...tr1-2.8.2) - 2022-05-20\n- fixed Lara not picking up items near the edges of room portals (#563, regression from 2.8)\n\n## [2.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.8...tr1-2.8.1) - 2022-05-05\n- fixed Pierre not resetting across levels (#538, regression from 2.7)\n- fixed pushables breaking with flipped rooms when loading a save (#536, regression from 2.8)\n\n## [2.8](https://github.com/LostArtefacts/TRX/compare/tr1-2.7...tr1-2.8) - 2022-05-04\n- added the option to pause sound in the inventory screen (#309)\n- added level selection to the load game menu (#197)\n- added the ability to pick up multiple items at once with walk to items enabled (#505)\n- added the ability to skip pictures during fade animation (#510)\n- added a cheat to increase the game speed (#135)\n- added a matrix stack overflow error check and message if GetRoomBounds runs infinitely (#506)\n- added ability to turn off trex collision (#437)\n- changed the savegame dialog to remember the user's requested slot number (#514)\n- changed the new game dialog to always fall back to new game\n- fixed ghost margins during fade animation on HiDPI screens (#438)\n- fixed music rolling over to the main menu if main menu music disabled (#490)\n- fixed Unfinished Business gameflow not using basic / detailed stats strings (#497, regression from 2.7)\n- fixed picking up multiple underwater pickups with walk to items enabled (#500)\n- fixed incorrect Lara health when restarting a level\n- fixed pushables breaking with flipped rooms when loading a save (#496, regression from 2.6)\n- fixed pictures displayed before starting a level causing a black screen (custom levels only)\n- fixed underwater caustics animating at 2x speed (#109)\n- fixed new game plus infinite ammo carrying over to a loaded game (#535, regression from 2.6)\n\n## [2.7](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.4...tr1-2.7) - 2022-03-16\n- added ability to automatically walk to pickups when nearby (#18)\n- added ability to automatically walk to switches when nearby (#222)\n- added ability to turn off detailed end of the level stats (#447)\n- added contextual arrows to passport navigation (#420)\n- added contextual arrows to sound option navigation (#459)\n- added contextual arrows to controls option navigation (#461)\n- added contextual arrows to graphics option navigation (#462)\n- added a final statistics screen (#385)\n- added music during the credits (#356)\n- added fade effects to displayed images (#476)\n- added unobtainable pickups and kills stats support in the gameflow (#470)\n- fixed exploded mutant pods sometimes appearing unhatched on reload (#423)\n- fixed sound effects playing rapidly in sound menu if input held down (#467)\n\n## [2.6.4](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.3...tr1-2.6.4) - 2022-02-20\n- fixed crash when loading a legacy save and saving on a new slot (#442, regression from 2.6)\n\n## [2.6.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.2...tr1-2.6.3) - 2022-02-18\n- fixed croc and rats breaking saves after a flipmap (#441, regression from 2.6)\n\n## [2.6.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.6.1...tr1-2.6.2) - 2022-02-17\n- fixed equipping gun after starting a demo (#440, regression from 2.6)\n\n## [2.6.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.6...tr1-2.6.1) - 2022-02-16\n- fixed equipping gun after starting the game (#439, regression from 2.6)\n\n## [2.6](https://github.com/LostArtefacts/TRX/compare/tr1-2.5...tr1-2.6) - 2022-02-16\n- added deaths counter (#388, requires new saves)\n- added total pickups and kills per level to the compass and end level stats screens (#362)\n- added new, more resilient savegame format (#277)\n- added ability to give Lara various items in the gameflow file\n- added restart level to passport menu on death (#48)\n- changed Lara's starting health to be configurable; useful for no damage runs (#365)\n- changed saves to be put in the saves/ directory (#87)\n- changed fade animations to block the main menu inventory ring like in PS1 (#379)\n- changed fade animations to be FPS-independent\n- changed fade animations to run faster in the main menu\n- changed compass text order to be consistent with level stats (#415)\n- fixed detail levels text flashing with any option change (#380)\n- fixed main menu demo playing even when the passport is open (#410, regression from 2.1)\n- fixed broken poses at the end of cinematics (#390)\n- fixed libavcodec-related memory leaks (#389)\n- fixed crash in custom levels that call `level_stats` after playing an FMV (#393, regression from 2.5)\n- fixed calling `level_stats` for different levels (#336, requires new saves)\n- fixed sounds playing after demo mode ends when game is minimized (#399)\n- fixed glitched floor in the Natla cutscene (#405)\n- fixed gun pickups disappearing in rare circumstances on save load (#406)\n- fixed equipping gun after loading a legacy save (#427, regression from 2.4)\n- fixed empty mutant shells in Unfinished Business spawning Lara's hips (#250)\n- fixed rare audio distance glitch (#421)\n- fixed Lara not getting her pistols in Atlantis if the player finishes Natla's Mines without picking up any gun (#424)\n- fixed broken dart ricochet effect (#429)\n\n## [2.5](https://github.com/LostArtefacts/TRX/compare/tr1-2.4...tr1-2.5) - 2022-01-31\n- added CHANGELOG.md\n- added ability to skip cinematics with the Action key\n- added fade animations (#363)\n- added a vsync option (#364)\n- fixed certain inputs skipping too many things (#359)\n- fixed a memory leak in the audio sampler (#369)\n\n\n## [2.4](https://github.com/LostArtefacts/TRX/compare/tr1-2.3...tr1-2.4) - 2022-01-19\n- added ability to skip FMVs with the action key (#334)\n- changed shaders to use GLSL version 1.30 (#327)\n- changed savegames to consume less space\n- fixed ingame overlay (bars and ammo) being sometimes shown in the menus\n- fixed menu backgrounds not being shown on certain platforms (#324)\n- fixed Lara reverting back to pistols when finishing a level with another gun (#338)\n- fixed lava wedge not setting Lara on fire (#353, regression from 2.2)\n- fixed fallback game strings not working (#335, regression from 2.3)\n- fixed high DPI window scaling on Windows (#280)\n- fixed not all sounds being muted when minimizing the game (#349)\n- fixed ability to push movable blocks through doors (#46)\n- fixed showing inventory ring up/down arrows when uncalled for (#337)\n- fixed Tomb1Main.log to be placed in the game directory rather than the current working directory\n- fixed a crash when exiting the game (regression from 2.3)\n- fixed a crash when shader compilation fails\n\n\n## [2.3](https://github.com/LostArtefacts/TRX/compare/tr1-2.2.1...tr1-2.3) - 2022-01-12\n- added ability to hold down forward/back to move through saves faster (#171)\n- changed screenshots to be saved in its own folder and with more meaningful names (#255)\n- fixed audible clicks near the end of samples (#281)\n- fixed secret chime not playing if the secret sound fix is disabled, and nothing plays between consecutive secret pickups (#310)\n- fixed ambient noises not pausing on pause screen (#316)\n- fixed underwater sound effect playing only once (#305)\n- fixed UZI sound stopping near big mutant explosions\n- fixed switching inventory rings briefly displaying black frames (#75)\n- fixed top offscreen load game selection (#273, #304)\n- fixed Lara voiding through static objects (#299)\n- fixed step left controller input not working (#302, regression from 2.0)\n- fixed memory leaks\n\n\n## [2.2.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.2...tr1-2.2.1) - 2022-01-05\n- fixed listing available resolutions (a regression from 2.2)\n- fixed Lara's airbar showing up when Lara's dead (a regression from 2.1)\n\n\n## [2.2](https://github.com/LostArtefacts/TRX/compare/tr1-2.1...tr1-2.2) - 2022-01-05\n- added ability to control anisotropy filter strength\n- changed the engine look for HD FMVs by default for Unfinished Business\n- removed tiny screen resolutions (might require setting the resolution again)\n- fixed Lara getting set on fire on trapdoors over lava\n- fixed letterbox in main menu showing garbage data on certain machines\n- fixed save crystals saving before gym level\n- fixed black lines appearing on walls and floors\n- fixed hang bug for stacked rooms\n\n\n## [2.1](https://github.com/LostArtefacts/TRX/compare/tr1-...tr1-2.1) - 2021-12-21\n- added ability to disable healthbar and airbar flashing\n- changed the engine look for HD FMVs by default\n- increased max active samples to 20 (should fix rare mute sounds issues)\n- fixed loading TombATI Atlantis saves\n- fixed shotgun shooting when target out of sight\n- fixed save selection being offscreen if the first savegame starts with high enough number\n- fixed alligators dealing no damage under certain circumstances\n- fixed grabbing bridges under certain circumstances\n- fixed crash if user presses a key during ring close animation\n\n## [2.0.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.0...tr1-2.0.1) - 2021-12-13\nAdded an icon to the .exe (thanks TRFan94!)\n\n\n## [2.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.4.0...tr1-2.0) - 2021-12-07\nShipped our own .exe! Tomb1Main is now fully open source and no longer needs injecting itself to the game. It also no longer depends on any of the TombATI .dll files. You can have both versions installed in the same folder.\n- added support for HD FMVs\n- added support for .png and .jpg pictures\n- added support for .png and .jpg screenshots\n- added fanmade 16:9 menu backgrounds\n- added wine support\n- added ability to run the game from any directory (its CWD no longer needs to point to the game's directory)\n- changed music player to SDL\n- changed sample player to SDL\n- changed FMV player to libavcodec and SDL\n- changed Eidos logo and initial FMVs to be stored in the gameflow file\n- changed Unfinished Business to no longer play cafe.rpl\n- changed the game no longer switches resolution back and forth in windowed mode\n- changed T1M no longer reads atiset.dat\n- improved shaders readability (chroma key is now stored in the texture alpha channel)\n- improved shader performance a bit when the bilinear filter is off\n- improved 3D rendering performance a bit (no more C++ exception handling)\n- fixed brightness not being saved\n- fixed game exiting with \"Fatal DirectInput error\" when losing focus early\n\n\n## [1.4.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.3.0...tr1-1.4.0) - 2021-11-16\n- added adjustable ingame brightness\n- added per-level fog settings\n- added control over fog density (in terms of tiles)\n- improved TR3 sidesteps\n- improved wording in readme\n- fixed lighting for 3D pickups\n- fixed a crash when drawing lightnings\n- fixed a crash when compiling the game on MSVC\n\n\n## [1.3.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.2.2...tr1-1.3.0) - 2021-11-06\n- added version in the bottom right corner\n- added movable camera on W,A,S,D\n- added Xbox One Controller support\n- added rounded shadows (instead of the default octagon)\n- added per-level customizable water color (with customizable blue component)\n- added rendering of pickups on the ground as 3D meshes\n- added the ability to change resolution in-game\n- added optional fixes for the following original game glitches:\n  - slope/wall bug (\"bonk to ascend\" bug)\n  - breakable tiles bug (\"sidestep to descend\" bug)\n  - qwop\n- changed maximum textures from 2048 to 8192\n- changed maximum texture pages from 32 to 128\n- changed default level skip cheat key from X to L\n- removed hard limit of 1024 rooms\n- fixed level skip working in inventory (it would apply only after closing the inventory)\n- fixed bats being positioned too high\n- fixed flashing conflicts when cheat buttons are disabled\n- fixed ability to rebind the pause button\n\n\n## [1.2.2](https://github.com/LostArtefacts/TRX/compare/tr1-1.2.1...tr1-1.2.2) - 2021-10-17\n- added ability to mute music in main menu\n- added pausing the music while in pause\n- added more screen resolutions\n- fixed demos playing oddly when the enhanced look option is enabled\n- fixed shadows rendering\n- fixed too big healthbar margins on low resolutions\n- fixed bilinear filter not working\n- fixed resolution width/height being ignored\n\n\n## [1.2.1](https://github.com/LostArtefacts/TRX/compare/tr1-1.2.0...tr1-1.2.1) - 2021-10-17\n- added resolution_width and resolution_height to the default settings\n- fixed screen resolution regression from 1.2.0\n\n\n## [1.2.0](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.5...tr1-1.2.0) - 2021-10-15\n- fixed a common crash on many machines\n\n\n## [1.1.5](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.4...tr1-1.1.5) - 2021-10-13\n- fixed a regression resulting in crashes from 1.1.4\n\n\n## [1.1.4](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.3...tr1-1.1.4) - 2021-10-13\n- fixed problem with the alt key on certain machines\n- fixed a rare crash on certain machines\n\n\n## [1.1.3](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.2...tr1-1.1.3) - 2021-03-30\n- changed smooth bars to be enabled by default\n- changed end of level freeze fix can no longer be disabled\n- changed creature distance fix can no longer be disabled\n- changed pistols + key triggers fix can no longer be disabled\n- changed illegal gun equip fix can no longer be disabled\n- changed FMV escape key fix can no longer be disabled\n- changed input to DirectInput\n- fixed switchin Control keys when shimmying causing Lara to drop\n- fixed some anomalies around FPS counter within ingame menus\n- fixed controls UI missing its borders\n\n\n## [1.1.2](https://github.com/LostArtefacts/TRX/compare/tr1-1.1.1...tr1-1.1.2) - 2021-03-30\n- fixed main menu demo mode not playing correctly (regression from 1.1.1)\n- fixed game speeding up on certain machines (regression from 1.1.1)\n\n\n## [1.1.1](https://github.com/LostArtefacts/TRX/compare/tr1-1.1...tr1-1.1.1) - 2021-03-29\n- added deactivating game when Alt-Tabbing\n- improved pink bar color\n- fixed sounds volume slider not working for ingame sounds\n\n\n## [1.1](https://github.com/LostArtefacts/TRX/compare/tr1-1.0...tr1-1.1) - 2021-03-28\n- added an alert messagebox whenever something bad (within the code's expectations) happens\n- added smooth bars (needs to be explicitly enabled in the settings)\n- finished porting the input and sound routines\n- fixed custom bar colors not working in certain levels\n- fixed RNG not being seeded (no practical consequences on the gameplay)\n\n\n## [1.0](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.3...tr1-1.0) - 2021-03-21\n- added pause screen\n- added -gold command line switch to run Unfinished Business\n\n\n## [0.13.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.2...tr1-0.13.3) - 2021-03-21\n- added crystals mode (can be enabled in the gameflow)\n- improved navigation through keyboard controls UI\n- fixed Unfinished Business gameflow not loading\n- fixed OG conflicting controls not flashing after relaunching the game\n- fixed drawing Lara's hair when she carries shotgun on her back\n- fixed loading custom layouts that conflict with default controls\n\n\n## [0.13.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.1...tr1-0.13.2) - 2021-03-19\n- fixed lighting issues (Lara being sometimes very brightly lighted)\n\n\n## [0.13.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.13.0...tr1-0.13.1) - 2021-03-19\n- changed demo_delay constant to be stored in the gameflow file\n- fixed regression in LoadSamples\n\n\n## [0.13.0](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.7...tr1-0.13.0) - 2021-03-19\n- added display_time parameter to display_picture (requires overwriting your gameflow file)\n- added user controllable UI and bar scaling\n- changed limit of max items (moveables in TRLE lingo) from 256 to 10240\n- fixed whacky navigation in controls dialog if cheats are enabled\n- fixed regression in LoadItems that crashes Atlantis\n- fixed skipping pictures displayed before starting the level with the escape key causing inventory to open\n\n\n## [0.12.7](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.6...tr1-0.12.7) - 2021-03-19\n- added ability to remap cheat keys (except obscure f11 debug key)\n- changed f10 level skip cheat key to 'x' (can be now changed); had to be done because the game does not let mapping to function keys\n- changed lots of variables to stay in T1M memory (may cause regressions)\n- changed runtime game config to be read and written to a new JSON configuration rather than atiset.cfg\n- changed files directory placement to a new directory, cfg/\n\n\n## [0.12.6](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.5...tr1-0.12.6) - 2021-03-18\n- fixed loading game in Natla's Mines causing Lara to lose her guns\n\n\n## [0.12.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.4...tr1-0.12.5) - 2021-03-17\n- fixed collected secrets resetting after using compass\n\n\n## [0.12.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.3...tr1-0.12.4) - 2021-03-17\n- added showing level stats in compass (can be disabled)\n- added ability to disable game mode selection in gameflow\n- added fallback gameflow strings (in case someone installs new T1M but forgets to't override the gameflow file)\n- added ability to exit level stats with escape\n- changed ingame timer to tick also in the inventory (can be disabled)\n- changed bar sizes and location to match TR2Main\n- fixed reading key configuration for keys that override defaults\n- fixed calculating creature distances (fixes Tihocan croc bug)\n\n\n## [0.12.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.2...tr1-0.12.3) - 2021-03-17\n- add Japanese mode (enemies are 2 times weaker)\n- improve skipping cutscenes\n- fix crash when FMVs are missing (this doesn't add support for HQ FMVs though)\n\n\n## [0.12.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.1...tr1-0.12.2) - 2021-03-14\n- changed settings to save after each change\n- fixed OG music stopping when playing the secrets chime (can be disabled)\n- fixed OG game not saving key layout choice (default vs. user keys)\n- fixed OG volume slider not working when starting muted\n- fixed OG holding action to skip credit pictures skipping them all at once\n- fixed OG holding escape to skip FMVs opening inventory\n\n\n## [0.12.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.12.0...tr1-0.12.1) - 2021-03-14\n- huge internal refactors\n- improved door open cheat\n- changed 4k scaling path to be always enabled (previously known as enable_enhanced_ui)\n- fixed killing music underwater\n- fixed main menu background for UB\n\n\n## [0.12.0](https://github.com/LostArtefacts/TRX/compare/tr1-0.11.1...tr1-0.12.0) - 2021-03-12\n- introduced gameflow sequencer (moves FMVs, cutscenes, level stats etc. logic to the gameflow JSON file); add ability to control number of levels\n- refactored gameflow\n- added ability to disable cinematic scenes\n- changed automatic calculation of secret count to be always enabled\n- fixed starting NG+ from gym not working\n- fixed cinematics resetting FOV\n\n\n## [0.11.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.11...tr1-0.11.1) - 2021-03-11\n- added ability to turn off main menu demos\n- added ability to turn off FMVs\n- added reporting JSON parsing errors in the logs\n- fixed reading config sometimes not working\n- fixed killing music in the inventory\n- fixed missing Demo Mode text\n- fixed showing Eidos logo for too short\n- fixed Lara wearing normal clothes in Gym\n\n\n## [0.11](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.5...tr1-0.11) - 2021-03-11\n- introduced gameflow file (moves all game strings to a gameflow JSON file, including level paths and names); level number, FMVs etc. are still hardcoded\n\n\n## [0.10.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.4...tr1-0.10.5) - 2021-03-10\n- added arrows to save/load dialogs\n- improved user keys settings dialog - you don't have to hold the key for exactly 1 frame anymore\n- made new game dialog smaller\n- fixed passport closing when exiting new game mode selection dialog\n\n\n## [0.10.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.3...tr1-0.10.4) - 2021-03-08\n- fixed load game screen\n\n\n## [0.10.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.2...tr1-0.10.3) - 2021-03-08\n- added NG/NG+ mode selection\n\n\n## [0.10.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.10.1...tr1-0.10.2) - 2021-03-07\n- fixed fly cheat resurrection with lava wedges\n\n\n## [0.10.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.10...tr1-0.10.1) - 2021-03-07\n- improved dealing with missing config\n- renamed config to .json5\n- fixed sound going off after playing a cinematic\n\n\n## [0.10](https://github.com/LostArtefacts/TRX/compare/tr1-0.9.2...tr1-0.10) - 2021-03-06\n- added support for opening closest doors\n\n\n## [0.9.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.9.1...tr1-0.9.2) - 2021-03-05\n- fixed messged up FMV sequence IDs\n- fixed crash when drawing lightnings near Scion\n\n\n## [0.9.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.9...tr1-0.9.1) - 2021-03-04\n- fixed bats flying near floor\n- fixed typo in Tomb1Main.json causing everything to be disabled\n\n\n## [0.9](https://github.com/LostArtefacts/TRX/compare/tr1-0.8.3...tr1-0.9) - 2021-03-03\n- added FOV support (overrides GLrage completely, but should be compatible with it)\n- added support for more than 3 pickups at once (for TRLE builders)\n- fixed smaller pickup sprites\n- fixed showing FPS in the main menu doing weird stuff to the inventory text after starting the game\n\n\n## [0.8.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.8.2...tr1-0.8.3) - 2021-02-28\n- improved TR3-like sidesteps\n- improved bar flashing modes\n- fixed Lara targeting enemies even after death\n- fixed version information missing from releases\n\n\n## [0.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.8.1...tr1-0.8.2) - 2021-02-28\n- fixed Lara drawing guns when loading OG saves\n\n\n## [0.8.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.8...tr1-0.8.1) - 2021-02-27\n- fixed AI sometimes having problems to find Lara\n- fixed shotgun firing sound after running out of ammo\n- fixed OG being able to get pistols by running out of ammo in other weapons, even without having them in the inventory\n\n\n## [0.8](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.6...tr1-0.8) - 2021-02-27\n- added optional TR3-like sidesteps\n- added \"never\" to healthbar display modes (so that you can run without ever knowing your health!)\n- added airbar display modes (so that you can swim without ever knowing your remaining oxygen)\n- added experimental braid, off by default (works only in Lost Valley due to other levels having no braid meshes)\n- added version information (it's in file properties)\n- changed turning fly cheat on above water no longer causes Lara to create bubbles for 1 frame\n- changed turning fly cheat on after stepping on Midas hand and getting eaten by T-Rex now resets Lara's appearance back to normal\n- change turning fly cheat on while burning now extinguishes Lara\n- increase the chance for the player to resurrect Lara with fly cheat after dying (up to 10 s, but it has to be the first keystroke they press)\n- fixed T1M bug - holding fly cheat and WALK resulting in hoisting Lara up\n- fixed T1M bug - added ability to draw last selected weapon with numkeys\n- fixed OG bug - keys and puzzles not triggering after drawing guns\n- fixed OG bug - having to draw guns via inventory after picking them up in Natla's Mines\n- fixed OG crash when Lara is on fire and walks too far away from where she caught fire\n\n\n## [0.7.6](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.5...tr1-0.7.6) - 2021-02-23\n- fixed Atlanteans behavior\n\n\n## [0.7.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.4...tr1-0.7.5) - 2021-02-22\n- fixed ammo text placement\n- fixed healthbar placement in the inventory\n\n\n## [0.7.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.3...tr1-0.7.4) - 2021-02-22\n- added support for user-configured bar colors\n- switched configuration format to use JSON5\n- moved comments to Tomb5Main.json\n- fixed bar placement\n\n\n## [0.7.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.2...tr1-0.7.3) - 2021-02-22\n- added support for user-configured bar locations\n- fixed pickups scaling\n\n\n## [0.7.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.7.1...tr1-0.7.2) - 2021-02-22\n- fixed ability to look around while Lara's dead\n- fixed UI scaling in controls dialog\n- fixed crash for some creatures\n\n\n## [0.7.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.7...tr1-0.7.1) - 2021-02-22\n- added inventory cheat\n- made fly cheat faster\n\n\n## [0.7](https://github.com/LostArtefacts/TRX/compare/tr1-0.6...tr1-0.7) - 2021-02-21\n- added fly cheat\n- fixed a crash when hit by a lightning (T1M regression)\n- fixed missing \"Demo Mode\" text (T1M regression)\n\n\n## [0.6](https://github.com/LostArtefacts/TRX/compare/tr1-0.5.1...tr1-0.6) - 2021-02-20\n- changed the code to count secrets automatically (useful for custom level builders)\n- fixed secret trigger in The Great Pyramid\n- fixed a crash when loading levels with more than 1024 textures\n- fixed drawing Lara (T1M regression)\n\n\n## [0.5.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.5...tr1-0.5.1) - 2021-02-20\n- added fire sprite to shotgun\n\n\n## [0.5](https://github.com/LostArtefacts/TRX/compare/tr1-0.4.1...tr1-0.5) - 2021-02-18\n- renamed the project from TR1Main to Tomb1Main on the request of Arsunt\n- improved documentation\n\n\n## [0.4.1](https://github.com/LostArtefacts/TRX/compare/tr1-...tr1-0.4.1) - 2021-02-15\n- added an option to always show the healthbar\n- fixed enemy healthbars in NG+\n- fixed no heal mode\n\n## [0.4](https://github.com/LostArtefacts/TRX/compare/tr1-0.3.1...tr1-0.4) - 2021-02-14\n- added UI scaling\n- added ability to look around underwater\n\n\n## [0.3.1](https://github.com/LostArtefacts/TRX/compare/tr1-...tr1-0.3.1) - 2021-02-13\n- improved the ability to look around while running\n\n## [0.3](https://github.com/LostArtefacts/TRX/compare/tr1-0.2.1...tr1-0.3) - 2021-02-13\n- added an option disable magnums\n- added an option disable uzis\n- added an option disable shotgun\n- added ability to look around while running\n- added support for using items with numeric keys\n- fixed an OG bug with the secret sound in Tomb of Tihocan\n\n\n## [0.2.1](https://github.com/LostArtefacts/TRX/compare/tr1-0.2...tr1-0.2.1) - 2021-02-11\n- changed the default configuration to enable enemy healthbars, red healthbar and end of the level freeze fix\n\n\n## [0.2](https://github.com/LostArtefacts/TRX/compare/tr1-0.1...tr1-0.2) - 2021-02-11\n- added enemy healthbars\n- added a red healthbar\n\n\n## [0.1](https://github.com/LostArtefacts/TRX/compare/...tr1-0.1) - 2021-02-10\n\nInitial version.\n"
  },
  {
    "path": "docs/tr1/INSTALLING.md",
    "content": "# Windows (installer)\n\n## Installing (simplified)\n\n1. Download the latest TRX installer for TR1 (e.g. `TRX-1.0-Windows_Installer-tr1.exe`).\n2. Mark the installer EXE as safe to run:\n    - Right-click on the `.exe`.\n    - Go to properties.\n    - Click \"Unblock\".\n3. Run the installer and proceed with the steps.\n\n> [!NOTE]\n> When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI‑based heuristics – they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because:\n>\n> - It isn't signed with a costly commercial certificate.\n> - It's a niche, community‑built project, so not widely recognized.\n> - It's a custom build, not from the Microsoft Store.\n>\n> Don't worry: TRX is open‑source, and you can inspect the code yourself on [GitHub](https://github.com/LostArtefacts/TRX/).\n\n# Windows / Linux\n\n## Installing (manual)\n\n1. Download the TRX zip file.\n2. Extract the zip file into a directory of your choice.  \n     Make sure you choose to overwrite existing directories and files.\n3. If installing for the first time – put your original game files into the target directory.\n\n    **Steam / GOG users**\n\n    1. Extract the original `GAME.BIN` / `GAME.GOG` file to your target directory.  \n        On Windows, this can be done with tools like UltraISO or UniExtract.  \n        On Linux, this can be done with `innoextract`, `bin2iso` and `7z`.\n    2. Get the music files – unfortunately, neither GOG nor Steam ship these assets.\n        - You can download the music files from the link below.  \n            https://lostartefacts.dev/aux/tr1x/music.zip  \n            (The legality of this approach is disputable.)\n        - Rip the assets yourself from a physical PlayStation/SegaSaturn disk.\n    3. Optionally, install the Unfinished Business expansion pack.\n        - Pre-packaged level files containing fan-made patch to include music triggers:  \n            https://lostartefacts.dev/aux/tr1x/trub-music.zip\n        - Pre-packaged original level files, which do not include music triggers:  \n            https://lostartefacts.dev/aux/tr1x/trub-vanilla.zip\n        - Original ISO - requires more involved manual extraction:  \n            https://archive.org/details/tomb-raider-i-unfinished-business-pc-eng-full-version_20201225\n\n    **TombATI users**\n\n    1. Copy the `data`, `fmv` and `music` directories.\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-win\">\n<pre><code>.\n├── cfg\n│   ├── presets\n│   │   ├── tr1-pc.json5\n│   │   ├── tr1-ps1.json5\n│   │   ├── tr2-pc.json5\n│   │   ├── tr2-ps1.json5\n│   │   ├── tr3-pc.json5\n│   │   └── tr3-ps1.json5\n│   ├── tr1\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-en-gb.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── tr1-demo-pc\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── tr1-level\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── tr1-ub\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── base_strings-de.json5\n│   ├── base_strings-en-gb.json5\n│   ├── base_strings-fr.json5\n│   ├── base_strings-gd.json5\n│   ├── base_strings-it.json5\n│   ├── base_strings-pl.json5\n│   ├── base_strings-ru.json5\n│   ├── base_strings.json5\n│   ├── catalog_item_actions.csv\n│   ├── catalog_lara_anims.csv\n│   ├── catalog_lara_states.csv\n│   ├── catalog_music.csv\n│   ├── catalog_objects.csv\n│   ├── catalog_samples.csv\n│   ├── inv_ring.json5\n│   ├── outfits.json5\n│   ├── poses.json5\n│   ├── TR1X.json5*\n│   ├── ui.json5\n│   └── weapons.json5\n├── data\n│   ├── images\n│   │   ├── atlantis.webp\n│   │   ├── credits_1.webp\n│   │   ├── credits_2.webp\n│   │   ├── credits_3.webp\n│   │   ├── credits_3_alt.webp\n│   │   ├── credits_ps1.webp\n│   │   ├── credits_ub.webp\n│   │   ├── egypt.webp\n│   │   ├── eidos.webp\n│   │   ├── end.webp\n│   │   ├── greece.webp\n│   │   ├── greece_saturn.webp\n│   │   ├── gym.webp\n│   │   ├── install.webp\n│   │   ├── peru.webp\n│   │   ├── title.webp\n│   │   ├── title_og_alt.webp\n│   │   ├── title_ub.webp\n│   │   ├── ub_loading1.webp\n│   │   └── ub_loading2.webp\n│   ├── injections\n│   │   ├── atlantis_door_sfx.bin\n│   │   ├── atlantis_fd.bin\n│   │   ├── atlantis_itemrots.bin\n│   │   ├── atlantis_textures.bin\n│   │   ├── braid.bin\n│   │   ├── bubbles.bin\n│   │   ├── cat_cameras.bin\n│   │   ├── cat_crystals.bin\n│   │   ├── cat_fd.bin\n│   │   ├── cat_itemrots.bin\n│   │   ├── cat_meshfixes.bin\n│   │   ├── cat_textures.bin\n│   │   ├── caves_fd.bin\n│   │   ├── caves_itemrots.bin\n│   │   ├── caves_textures.bin\n│   │   ├── cistern_fd.bin\n│   │   ├── cistern_itemrots.bin\n│   │   ├── cistern_plants.bin\n│   │   ├── cistern_skybox.bin\n│   │   ├── cistern_textures.bin\n│   │   ├── colosseum_fd.bin\n│   │   ├── colosseum_itemrots.bin\n│   │   ├── colosseum_skybox.bin\n│   │   ├── colosseum_textures.bin\n│   │   ├── crystal.bin\n│   │   ├── cut1_setup.bin\n│   │   ├── cut2_setup.bin\n│   │   ├── cut3_setup.bin\n│   │   ├── cut3_textures.bin\n│   │   ├── cut4_setup.bin\n│   │   ├── cut4_textures.bin\n│   │   ├── door58_frames.bin\n│   │   ├── door59_frames.bin\n│   │   ├── door59_sfx.bin\n│   │   ├── door60_frames.bin\n│   │   ├── door61_sfx.bin\n│   │   ├── egypt_cameras.bin\n│   │   ├── egypt_crystals.bin\n│   │   ├── egypt_fd.bin\n│   │   ├── egypt_itemrots.bin\n│   │   ├── egypt_meshfixes.bin\n│   │   ├── egypt_textures.bin\n│   │   ├── explosion.bin\n│   │   ├── folly_fd.bin\n│   │   ├── folly_itemrots.bin\n│   │   ├── folly_pickup_meshes.bin\n│   │   ├── folly_textures.bin\n│   │   ├── font.bin\n│   │   ├── gun_glow.bin\n│   │   ├── gym_textures.bin\n│   │   ├── hive_crystals.bin\n│   │   ├── hive_fd.bin\n│   │   ├── hive_itemrots.bin\n│   │   ├── hive_textures.bin\n│   │   ├── khamoon_fd.bin\n│   │   ├── khamoon_itemrots.bin\n│   │   ├── khamoon_meshfixes.bin\n│   │   ├── khamoon_mummy.bin\n│   │   ├── khamoon_textures.bin\n│   │   ├── lara_animations.bin\n│   │   ├── lara_extra.bin\n│   │   ├── lara_feet_sfx.bin\n│   │   ├── lara_flares.bin\n│   │   ├── lara_guns.bin\n│   │   ├── lara_gym_flares.bin\n│   │   ├── lara_gym_guns.bin\n│   │   ├── lara_outfits.bin\n│   │   ├── midas_itemrots.bin\n│   │   ├── midas_textures.bin\n│   │   ├── mines_cameras.bin\n│   │   ├── mines_door_sfx.bin\n│   │   ├── mines_fd.bin\n│   │   ├── mines_itemrots.bin\n│   │   ├── mines_meshfixes.bin\n│   │   ├── mines_pushblocks.bin\n│   │   ├── mines_textures.bin\n│   │   ├── misc_sprites.bin\n│   │   ├── obelisk_fd.bin\n│   │   ├── obelisk_itemrots.bin\n│   │   ├── obelisk_meshfixes.bin\n│   │   ├── obelisk_skybox.bin\n│   │   ├── obelisk_textures.bin\n│   │   ├── panther_sfx.bin\n│   │   ├── pda_model.bin\n│   │   ├── photo.bin\n│   │   ├── pickup_aid.bin\n│   │   ├── pyramid_fd.bin\n│   │   ├── pyramid_itemrots.bin\n│   │   ├── pyramid_textures.bin\n│   │   ├── qualopec_door_sfx.bin\n│   │   ├── qualopec_fd.bin\n│   │   ├── qualopec_itemrots.bin\n│   │   ├── qualopec_textures.bin\n│   │   ├── sanctuary_fd.bin\n│   │   ├── sanctuary_itemrots.bin\n│   │   ├── sanctuary_scion.bin\n│   │   ├── sanctuary_textures.bin\n│   │   ├── scion_collision.bin\n│   │   ├── skate_kid_sfx.bin\n│   │   ├── sprite_alignment.bin\n│   │   ├── stronghold_crystals.bin\n│   │   ├── stronghold_fd.bin\n│   │   ├── stronghold_itemrots.bin\n│   │   ├── stronghold_textures.bin\n│   │   ├── tihocan_fd.bin\n│   │   ├── tihocan_itemrots.bin\n│   │   ├── tihocan_skybox.bin\n│   │   ├── tihocan_textures.bin\n│   │   ├── title_textures.bin\n│   │   ├── uzi_sfx.bin\n│   │   ├── valley_fd.bin\n│   │   ├── valley_itemrots.bin\n│   │   ├── valley_skybox.bin\n│   │   ├── valley_textures.bin\n│   │   ├── vilcabamba_door_sfx.bin\n│   │   ├── vilcabamba_itemrots.bin\n│   │   ├── vilcabamba_textures.bin\n│   │   └── winston_model.bin\n│   ├── scripts\n│   │   └── gym.lua\n│   ├── cat.phd\n│   ├── cut1.phd\n│   ├── cut2.phd\n│   ├── cut3.phd\n│   ├── cut4.phd\n│   ├── egypt.phd\n│   ├── end2.phd\n│   ├── end.phd\n│   ├── gym.phd\n│   ├── level1.phd\n│   ├── level2.phd\n│   ├── level3a.phd\n│   ├── level3b.phd\n│   ├── level4.phd\n│   ├── level5.phd\n│   ├── level6.phd\n│   ├── level7a.phd\n│   ├── level7b.phd\n│   ├── level8a.phd\n│   ├── level8b.phd\n│   ├── level8c.phd\n│   ├── level10a.phd\n│   ├── level10b.phd\n│   ├── level10c.phd\n│   └── title.phd\n├── fmv\n│   ├── cafe.rpl\n│   ├── canyon.rpl\n│   ├── core.avi\n│   ├── end.rpl\n│   ├── escape.rpl\n│   ├── lift.rpl\n│   ├── mansion.rpl\n│   ├── prison.rpl\n│   ├── pyramid.rpl\n│   ├── snow.rpl\n│   └── vision.rpl\n├── music\n│   ├── track02.flac\n│   ├── track03.flac\n│   ├── track04.flac\n│   ├── track05.flac\n│   ├── track06.flac\n│   ├── track07.flac\n│   ├── track08.flac\n│   ├── track09.flac\n│   ├── track10.flac\n│   ├── track11.flac\n│   ├── track12.flac\n│   ├── track13.flac\n│   ├── track14.flac\n│   ├── track15.flac\n│   ├── track16.flac\n│   ├── track17.flac\n│   ├── track18.flac\n│   ├── track19.flac\n│   ├── track20.flac\n│   ├── track21.flac\n│   ├── track22.flac\n│   ├── track23.flac\n│   ├── track24.flac\n│   ├── track25.flac\n│   ├── track26.flac\n│   ├── track27.flac\n│   ├── track28.flac\n│   ├── track29.flac\n│   ├── track30.flac\n│   ├── track31.flac\n│   ├── track32.flac\n│   ├── track33.flac\n│   ├── track34.flac\n│   ├── track35.flac\n│   ├── track36.flac\n│   ├── track37.flac\n│   ├── track38.flac\n│   ├── track39.flac\n│   ├── track40.flac\n│   ├── track41.flac\n│   ├── track42.flac\n│   ├── track43.flac\n│   ├── track44.flac\n│   ├── track45.flac\n│   ├── track46.flac\n│   ├── track47.flac\n│   ├── track48.flac\n│   ├── track49.flac\n│   ├── track50.flac\n│   ├── track51.flac\n│   ├── track52.flac\n│   ├── track53.flac\n│   ├── track54.flac\n│   ├── track55.flac\n│   ├── track56.flac\n│   ├── track57.flac\n│   ├── track58.flac\n│   ├── track59.flac\n│   └── track60.flac\n├── shaders\n│   ├── 2d.glsl\n│   ├── billboard.glsl\n│   ├── common.glsl\n│   ├── fbo.glsl\n│   ├── lights.glsl\n│   ├── meshes.glsl\n│   ├── meshes_tr3.glsl\n│   ├── meshes_tr12.glsl\n│   └── ui.glsl\n└── TRX.exe</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n\n## Playing the game\n\n- To play the game, run `TRX.exe`.\n- To play the Unfinished Business expansion pack, run `TRX.exe --gold`.\n\n# macOS\n\n## Installing\n\n1. Download the latest TRX for TR1 installer image (e.g `TRX-0.1-Mac-tr1.dmg`). Mount the image and drag TR1X to the Applications folder.\n2. Run TR1X from the Applications folder. This will show you an error dialog about missing game data files. This is expected at this point, as you have not copied them in yet. However, it's important to run the app first to allow macOS to verify the app bundle's signature.\n3. Find TR1X in your Applications folder. Right-click it and click \"Show Package Contents\".\n4. Copy your Tomb Raider 1 game data files into `Contents/Resources`. (See the Windows / Linux instructions for retrieving game data from e.g. GOG.)\n\nIn case you see a popup \"TR1X is damaged\" when you run the game, run `xattr -cr /Applications/TR1X.app`.\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-mac\">\n<pre><code>.\n└── Contents\n    ├── Resources\n    │   ├── cfg\n    │   │   ├── presets\n    │   │   │   ├── tr1-pc.json5\n    │   │   │   ├── tr1-ps1.json5\n    │   │   │   ├── tr2-pc.json5\n    │   │   │   ├── tr2-ps1.json5\n    │   │   │   ├── tr3-pc.json5\n    │   │   │   └── tr3-ps1.json5\n    │   │   ├── tr1\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-en-gb.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   ├── strings-ru.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr1-demo-pc\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   ├── strings-ru.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr1-level\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   ├── strings-ru.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr1-ub\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   ├── strings-ru.json5\n    │   │   │   └── strings.json5\n    │   │   ├── base_strings-de.json5\n    │   │   ├── base_strings-en-gb.json5\n    │   │   ├── base_strings-fr.json5\n    │   │   ├── base_strings-gd.json5\n    │   │   ├── base_strings-it.json5\n    │   │   ├── base_strings-pl.json5\n    │   │   ├── base_strings-ru.json5\n    │   │   ├── base_strings.json5\n    │   │   ├── catalog_item_actions.csv\n    │   │   ├── catalog_lara_anims.csv\n    │   │   ├── catalog_lara_states.csv\n    │   │   ├── catalog_music.csv\n    │   │   ├── catalog_objects.csv\n    │   │   ├── catalog_samples.csv\n    │   │   ├── inv_ring.json5\n    │   │   ├── outfits.json5\n    │   │   ├── poses.json5\n    │   │   ├── ui.json5\n    │   │   └── weapons.json5\n    │   ├── data\n    │   │   ├── images\n    │   │   │   ├── atlantis.webp\n    │   │   │   ├── credits_1.webp\n    │   │   │   ├── credits_2.webp\n    │   │   │   ├── credits_3.webp\n    │   │   │   ├── credits_3_alt.webp\n    │   │   │   ├── credits_ps1.webp\n    │   │   │   ├── credits_ub.webp\n    │   │   │   ├── egypt.webp\n    │   │   │   ├── eidos.webp\n    │   │   │   ├── end.webp\n    │   │   │   ├── greece.webp\n    │   │   │   ├── greece_saturn.webp\n    │   │   │   ├── gym.webp\n    │   │   │   ├── install.webp\n    │   │   │   ├── peru.webp\n    │   │   │   ├── title.webp\n    │   │   │   ├── title_og_alt.webp\n    │   │   │   ├── title_ub.webp\n    │   │   │   ├── ub_loading1.webp\n    │   │   │   └── ub_loading2.webp\n    │   │   ├── injections\n    │   │   │   ├── atlantis_door_sfx.bin\n    │   │   │   ├── atlantis_fd.bin\n    │   │   │   ├── atlantis_itemrots.bin\n    │   │   │   ├── atlantis_textures.bin\n    │   │   │   ├── braid.bin\n    │   │   │   ├── bubbles.bin\n    │   │   │   ├── cat_cameras.bin\n    │   │   │   ├── cat_crystals.bin\n    │   │   │   ├── cat_fd.bin\n    │   │   │   ├── cat_itemrots.bin\n    │   │   │   ├── cat_meshfixes.bin\n    │   │   │   ├── cat_textures.bin\n    │   │   │   ├── caves_fd.bin\n    │   │   │   ├── caves_itemrots.bin\n    │   │   │   ├── caves_textures.bin\n    │   │   │   ├── cistern_fd.bin\n    │   │   │   ├── cistern_itemrots.bin\n    │   │   │   ├── cistern_plants.bin\n    │   │   │   ├── cistern_skybox.bin\n    │   │   │   ├── cistern_textures.bin\n    │   │   │   ├── colosseum_fd.bin\n    │   │   │   ├── colosseum_itemrots.bin\n    │   │   │   ├── colosseum_skybox.bin\n    │   │   │   ├── colosseum_textures.bin\n    │   │   │   ├── crystal.bin\n    │   │   │   ├── cut1_setup.bin\n    │   │   │   ├── cut2_setup.bin\n    │   │   │   ├── cut3_setup.bin\n    │   │   │   ├── cut3_textures.bin\n    │   │   │   ├── cut4_setup.bin\n    │   │   │   ├── cut4_textures.bin\n    │   │   │   ├── door58_frames.bin\n    │   │   │   ├── door59_frames.bin\n    │   │   │   ├── door59_sfx.bin\n    │   │   │   ├── door60_frames.bin\n    │   │   │   ├── door61_sfx.bin\n    │   │   │   ├── egypt_cameras.bin\n    │   │   │   ├── egypt_crystals.bin\n    │   │   │   ├── egypt_fd.bin\n    │   │   │   ├── egypt_itemrots.bin\n    │   │   │   ├── egypt_meshfixes.bin\n    │   │   │   ├── egypt_textures.bin\n    │   │   │   ├── explosion.bin\n    │   │   │   ├── folly_fd.bin\n    │   │   │   ├── folly_itemrots.bin\n    │   │   │   ├── folly_pickup_meshes.bin\n    │   │   │   ├── folly_textures.bin\n    │   │   │   ├── font.bin\n    │   │   │   ├── gun_glow.bin\n    │   │   │   ├── gym_textures.bin\n    │   │   │   ├── hive_crystals.bin\n    │   │   │   ├── hive_fd.bin\n    │   │   │   ├── hive_itemrots.bin\n    │   │   │   ├── hive_textures.bin\n    │   │   │   ├── khamoon_fd.bin\n    │   │   │   ├── khamoon_itemrots.bin\n    │   │   │   ├── khamoon_meshfixes.bin\n    │   │   │   ├── khamoon_mummy.bin\n    │   │   │   ├── khamoon_textures.bin\n    │   │   │   ├── lara_animations.bin\n    │   │   │   ├── lara_extra.bin\n    │   │   │   ├── lara_feet_sfx.bin\n    │   │   │   ├── lara_flares.bin\n    │   │   │   ├── lara_guns.bin\n    │   │   │   ├── lara_gym_flares.bin\n    │   │   │   ├── lara_gym_guns.bin\n    │   │   │   ├── lara_outfits.bin\n    │   │   │   ├── midas_itemrots.bin\n    │   │   │   ├── midas_textures.bin\n    │   │   │   ├── mines_cameras.bin\n    │   │   │   ├── mines_door_sfx.bin\n    │   │   │   ├── mines_fd.bin\n    │   │   │   ├── mines_itemrots.bin\n    │   │   │   ├── mines_meshfixes.bin\n    │   │   │   ├── mines_pushblocks.bin\n    │   │   │   ├── mines_textures.bin\n    │   │   │   ├── misc_sprites.bin\n    │   │   │   ├── obelisk_fd.bin\n    │   │   │   ├── obelisk_itemrots.bin\n    │   │   │   ├── obelisk_meshfixes.bin\n    │   │   │   ├── obelisk_skybox.bin\n    │   │   │   ├── obelisk_textures.bin\n    │   │   │   ├── panther_sfx.bin\n    │   │   │   ├── pda_model.bin\n    │   │   │   ├── photo.bin\n    │   │   │   ├── pickup_aid.bin\n    │   │   │   ├── pyramid_fd.bin\n    │   │   │   ├── pyramid_itemrots.bin\n    │   │   │   ├── pyramid_textures.bin\n    │   │   │   ├── qualopec_door_sfx.bin\n    │   │   │   ├── qualopec_fd.bin\n    │   │   │   ├── qualopec_itemrots.bin\n    │   │   │   ├── qualopec_textures.bin\n    │   │   │   ├── sanctuary_fd.bin\n    │   │   │   ├── sanctuary_itemrots.bin\n    │   │   │   ├── sanctuary_scion.bin\n    │   │   │   ├── sanctuary_textures.bin\n    │   │   │   ├── scion_collision.bin\n    │   │   │   ├── skate_kid_sfx.bin\n    │   │   │   ├── sprite_alignment.bin\n    │   │   │   ├── stronghold_crystals.bin\n    │   │   │   ├── stronghold_fd.bin\n    │   │   │   ├── stronghold_itemrots.bin\n    │   │   │   ├── stronghold_textures.bin\n    │   │   │   ├── tihocan_fd.bin\n    │   │   │   ├── tihocan_itemrots.bin\n    │   │   │   ├── tihocan_skybox.bin\n    │   │   │   ├── tihocan_textures.bin\n    │   │   │   ├── title_textures.bin\n    │   │   │   ├── uzi_sfx.bin\n    │   │   │   ├── valley_fd.bin\n    │   │   │   ├── valley_itemrots.bin\n    │   │   │   ├── valley_skybox.bin\n    │   │   │   ├── valley_textures.bin\n    │   │   │   ├── vilcabamba_door_sfx.bin\n    │   │   │   ├── vilcabamba_itemrots.bin\n    │   │   │   ├── vilcabamba_textures.bin\n    │   │   │   └── winston_model.bin\n    │   │   ├── scripts\n    │   │   │   └── gym.lua\n    │   │   ├── cat.phd\n    │   │   ├── cut1.phd\n    │   │   ├── cut2.phd\n    │   │   ├── cut3.phd\n    │   │   ├── cut4.phd\n    │   │   ├── egypt.phd\n    │   │   ├── end2.phd\n    │   │   ├── end.phd\n    │   │   ├── gym.phd\n    │   │   ├── level1.phd\n    │   │   ├── level2.phd\n    │   │   ├── level3a.phd\n    │   │   ├── level3b.phd\n    │   │   ├── level4.phd\n    │   │   ├── level5.phd\n    │   │   ├── level6.phd\n    │   │   ├── level7a.phd\n    │   │   ├── level7b.phd\n    │   │   ├── level8a.phd\n    │   │   ├── level8b.phd\n    │   │   ├── level8c.phd\n    │   │   ├── level10a.phd\n    │   │   ├── level10b.phd\n    │   │   ├── level10c.phd\n    │   │   └── title.phd\n    │   ├── fmv\n    │   │   ├── cafe.rpl\n    │   │   ├── canyon.rpl\n    │   │   ├── core.avi\n    │   │   ├── end.rpl\n    │   │   ├── escape.rpl\n    │   │   ├── lift.rpl\n    │   │   ├── mansion.rpl\n    │   │   ├── prison.rpl\n    │   │   ├── pyramid.rpl\n    │   │   ├── snow.rpl\n    │   │   └── vision.rpl\n    │   ├── music\n    │   │   ├── track02.flac\n    │   │   ├── track03.flac\n    │   │   ├── track04.flac\n    │   │   ├── track05.flac\n    │   │   ├── track06.flac\n    │   │   ├── track07.flac\n    │   │   ├── track08.flac\n    │   │   ├── track09.flac\n    │   │   ├── track10.flac\n    │   │   ├── track11.flac\n    │   │   ├── track12.flac\n    │   │   ├── track13.flac\n    │   │   ├── track14.flac\n    │   │   ├── track15.flac\n    │   │   ├── track16.flac\n    │   │   ├── track17.flac\n    │   │   ├── track18.flac\n    │   │   ├── track19.flac\n    │   │   ├── track20.flac\n    │   │   ├── track21.flac\n    │   │   ├── track22.flac\n    │   │   ├── track23.flac\n    │   │   ├── track24.flac\n    │   │   ├── track25.flac\n    │   │   ├── track26.flac\n    │   │   ├── track27.flac\n    │   │   ├── track28.flac\n    │   │   ├── track29.flac\n    │   │   ├── track30.flac\n    │   │   ├── track31.flac\n    │   │   ├── track32.flac\n    │   │   ├── track33.flac\n    │   │   ├── track34.flac\n    │   │   ├── track35.flac\n    │   │   ├── track36.flac\n    │   │   ├── track37.flac\n    │   │   ├── track38.flac\n    │   │   ├── track39.flac\n    │   │   ├── track40.flac\n    │   │   ├── track41.flac\n    │   │   ├── track42.flac\n    │   │   ├── track43.flac\n    │   │   ├── track44.flac\n    │   │   ├── track45.flac\n    │   │   ├── track46.flac\n    │   │   ├── track47.flac\n    │   │   ├── track48.flac\n    │   │   ├── track49.flac\n    │   │   ├── track50.flac\n    │   │   ├── track51.flac\n    │   │   ├── track52.flac\n    │   │   ├── track53.flac\n    │   │   ├── track54.flac\n    │   │   ├── track55.flac\n    │   │   ├── track56.flac\n    │   │   ├── track57.flac\n    │   │   ├── track58.flac\n    │   │   ├── track59.flac\n    │   │   └── track60.flac\n    │   ├── shaders\n    │   │   ├── 2d.glsl\n    │   │   ├── billboard.glsl\n    │   │   ├── common.glsl\n    │   │   ├── fbo.glsl\n    │   │   ├── lights.glsl\n    │   │   ├── meshes.glsl\n    │   │   ├── meshes_tr3.glsl\n    │   │   ├── meshes_tr12.glsl\n    │   │   └── ui.glsl\n    │   └── icon.icns\n    ├── _CodeSignature\n    ├── Frameworks\n    ├── info.plist\n    └── MacOS</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n"
  },
  {
    "path": "docs/tr2/CHANGELOG.md",
    "content": "## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.5.1...develop) - ××××-××-××\nSee [/docs/CHANGELOG.md].\n\n## [1.5.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.5...tr2-1.5.1) - 2025-10-10\n- changed the examine dialog to be usable with non-puzzle items (#4009)\n- fixed discs that spawn from emitters facing East starting too far from the emitters themselves (#4007)\n- fixed a crash on game exit if specifying \"ambient_tracks\" in the game flow root (regression from 1.1)\n- fixed alternate ambient tracks being lost on reload in custom levels (#3997, regression from 1.4)\n- fixed a crash if the game had certain objects (id=17, 18, 41, 43, 50) but was missing their reference objects (id=16, 16, 42, 44, 49, respectively)\n- fixed Lara at times not being able to grab pushblocks despite being in the correct position to do so (#4005, regression from 1.5)\n- fixed enemies being unable to smash windows (#4011, regression from 1.4)\n- fixed Lara appearing flat for a frame during the neutral twist, controlled drop and ledge jump back animations (#4012, regression from 1.4)\n\n## [1.5](https://github.com/LostArtefacts/TRX/compare/tr2-1.4.2...tr2-1.5) - 2025-10-04\nShowcase: https://youtu.be/ClkbvsENSvc\n- added an option to use Lara's barefoot sound effects in appropriate levels (Sound options → Barefoot SFX) (#2643)\n- added dev console gradient backdrop, similar to TR1X (#2150)\n- added an option to use smooth bars (Graphics → UI → Smooth bars, default off)\n- added an option to use TR1-style UI bars (Graphics → UI → Bars look)\n- added an option to use PS1-style UI bars (Graphics → UI → Bars look) (#1637)\n- added an option to use PS1-style UI backgrounds and frames (Graphics → UI → Menu style) (#1635)\n- added an option to use PS1-style carpet texture animation (Graphics → UI → Background style) (#1630)\n- added an option to change target lock modes (Gameplay → Controls → Weapon lock mode) (#3950)\n- added an option to cycle targets (Gameplay → Controls → Target change; Controls → Misc → Change Target) (#3951)\n- added a new `/cls` / `/clear` console command to quickly clear console logs\n- added an option to turn off ingame timer in the inventory ring (Gameplay → General → Timer counts in inventory) (#3931)\n- added an option to disable demos (Gameplay → General → Demo mode)\n- added an option to disable music in the title screen (Sound → Misc → Main menu music)\n- improved sound settings:\n    - added tabs (Volume and Misc)\n    - added a dedicated option to control master volume (Sound options → Volume → Master volume)\n    - added a dedicated option to control cutscenes volume (Sound options → Volume → Cutscenes volume) (#3490)\n    - added a dedicated option to control FMV volume (Sound options → Volume → FMV volume) (#3490)\n    - added a dedicated option to control general ambient volume (Sound options → Volume → Ambient volume) (#3707)\n    - added an option to turn off sound effect pitching (#625)\n    - improved volume settings to accept slow input for finer adjustments\n    - fixed changing sound volume not updating certain ambient sound sources while in the inventory ring (#3970)\n- changed OG glitch-related config options to be on/fixed by default (#3929)\n- changed the Use PSX FOV option name to Use PS1 FOV (Graphics → Visuals → Use PS1 FOV)\n- changed the UI style to use the PS1 look by default (Graphics → UI → Menu style)\n- changed the bar appearance to use the PS1 look by default (Graphics → UI → Bars look)\n- changed the inventory and stats screen to use the PS1 wave animation by default (Graphics → UI → Background style)\n- changed idle pose timeout from 15 to 60 seconds by default (Gameplay → Controls → Idle pose timeout)\n- changed idle pose camera to be disabled by default (Gameplay → Controls → Idle pose camera)\n- changed the game to launch in fullscreen mode by default (Alt-Enter to toggle)\n- changed max pickup scale to 200% (#3952)\n- fixed pickup scale being greyed out if the 3D pickups option is enabled (#3952)\n- fixed trapdoor type 3 (object #116) not functioning (#3895)\n- fixed camera stutter when shimmying on ladders to the left (#3904, regression from 1.3)\n- fixed gameplay settings UI displaying eagerly after the first use (#3583, regression from 1.3)\n- fixed changing FPS after advancing frames in photo mode causing the game to speed up (#3605, regression from 1.3)\n- fixed CPU spike during playing FMVs (regression from 0.6)\n- fixed `/play` command likely to skip opening FMVs (#3910, regression from 0.8)\n- fixed resumed music tracks playing briefly track start upon savegame load (#3916)\n- fixed highlight size in health and air bars\n- fixed a potential crash when loading a save where Lara is holding a flare (#3924, regression from 1.0)\n- fixed unrestricted look mode allowing cinematic cameras to be broken out of (#3926, regression from 1.4)\n- added the ability for falling movable blocks to kill Lara outright if one lands directly on her (#3784)\n- fixed numerous interactions with movable blocks, trapdoors, drawbridges, bridges, lifts, and falling blocks for custom levels (#2758):\n    - added the ability for movable blocks to move on trapdoors, drawbridges, bridges, lifts, and falling blocks\n    - added the ability for stacks of movable blocks to fall and land on trapdoors, drawbridges, bridges, lifts, and falling blocks\n    - added the ability for stacks of movable blocks to fall when on opened trapdoors and drawbridges\n    - added the ability for movable blocks to travel up and down lifts\n    - fixed various bugs with falling movable blocks\n- fixed Lara hang climbing up a movable block used as a ladder piece (#3828)\n- fixed pushblocks becoming unusable when on the same sector as a door that does not sit on a room portal (#3814)\n- fixed pushblocks that fall from a great height potentially causing a crash (#3969)\n- fixed a rare crash if the t-rex is killed with a grenade and many other enemies are active (#3938)\n- fixed dead skidoo drivers not registering with combat end after loading a save (#3966)\n- fixed recordings replaying commands twice (regression from 1.4)\n- fixed the fix for the sticky corner glitch not being optional - now linked to Gameplay → Fixes → Wall glitch mode (#3957, regression from 1.4)\n- fixed Lara shooting rifle-type weapons drawn during wade to float transition (#3986)\n- fixed Lara retaining guns if drawn during wade to float transition (#3979, regression from 1.3)\n- fixed Lara instantly holstering harpoon when drawing in 2+ click water (#3980, regression from 1.3)\n- fixed -s/--save argument no longer working with -l/--level (#3990, regression from 1.4)\n\n## [1.4.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.4.1...tr2-1.4.2) - 2025-09-07\n- fixed broken rendering in MacOS releases (#3880, regression from 1.4)\n- fixed images from MacOS releases (#3892, regression from 1.4)\n- fixed the combat end logic not completing properly if Lara is on a vehicle (#3885)\n- fixed dead water creatures not registering with combat end (#3887, regression from 1.3)\n\n## [1.4.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.4...tr2-1.4.1) - 2025-08-30\n- fixed missing shader and configuration files from MacOS releases (#3870, regression from 1.4)\n- fixed zero byte at the end of config files (#3875, regression from 1.4)\n- fixed stacked sprites flickering (#3872, regression from 1.4)\n\n## [1.4](https://github.com/LostArtefacts/TRX/compare/tr2-1.3.2...tr2-1.4) - 2025-08-23\nShowcase: https://www.youtube.com/watch?v=AAOP1VFX9Lw\n\n>[!WARNING]\n>Attention level builders: this version introduces backwards incompatible changes to the file structure.\n>Please refer to the [migration guide](../3-MIGRATING.md) to see how to update your levels.\n\n- reworked TR2 rendering\n    - added round shadows option (Graphic options → Visuals → Round shadows)\n    - added option to disable skyboxes (Graphic options → Visuals → Skyboxes)\n    - added brightness option (Graphic settings → Rendering → Brightness)\n    - added anisotropy option (Graphic settings → Rendering → Anisotropy filter)\n    - added vertical sync option (Graphic settings → Rendering → VSync)\n    - added an option to keep sprites upright (Graphic options → Rendering → Sprites lock mode)\n    - added debug portals feature (`/debug 1`)\n    - added debug room clip feature (`/debug 1`)\n    - added debug spheres feature (`/debug 1`)\n    - added debug triggers feature (`/debug 1`)\n    - added support for 60 fps in 3D UI pickups\n    - improved bilinear filter appearance - no more dark edges around objects\n    - improved bilinear filter texture adjustment - no more texture \"expansion\" (#2258)\n    - changed the F7 hotkey to be used as a wireframe toggle (previously available as Shift+F7)\n    - removed software rendering mode\n    - removed the z-buffer option, which is now always enabled\n    - removed undocumented linear and nearest texel adjustment options\n    - fixed trapezoid textures warping at the edge of the screen (#2629)\n    - fixed certain polygons disappearing in some objects (#3699)\n    - fixed z-fighting of doors near walls\n- added new command switches:\n    - `--test-record` and `--test-replay` for automated playthroughs with (internal tool – the recording file format may be subject to changes)\n    - `--headless`: runs the game offscreen with no audio and at unlocked simulation speed\n    - -q`, `--quiet`: outputs only error messages to the terminal, with log files being written to normally\n- added ability to move Lara around in photo mode (use sidestep keys to switch modes)\n- added additional poses for photo mode\n- added an option to allow Lara to sprint (Gameplay → Controls → Sprinting) (#3711)\n- added an option to use Lara's slide-to-run animation from TR3+ (Gameplay → Controls → Slide-to-run) (#1089)\n- added an option to use Lara's neutral jump-twist from early TR1 betas (Gameplay → Controls → Neutral twists) (#1392)\n- added an option to allow Lara to turn around and grab a ledge she has just stepped off (Gameplay → Controls → Controlled drops) (#3621)\n- added an option to allow Lara to jump up or back when hanging from a ledge (Gameplay → Controls → Ledge jumps) (#3683)\n- added an option to have Lara pose after standing idle for a certain time (Gameplay → Controls → Idle pose timeout) (#3727)\n- added an option to animate the algae in 40 Fathoms, Wreck of the Maria Doria and The Deck (Gameplay settings → Fixes → Fix sprite animations) (#3141)\n- added an option to scale the 3D pickups in the UI (Graphic options → UI → Pickup scale)\n- added an option to control fog color (Graphic options → Visuals → Fog transparency and Fog color) (#712, #3618)\n- added German translation\n- added a PS1 fade-out to final cutscene (#3521)\n- added a new `/vsync` console command to toggle the vsync option, like in TR1\n- added a new `/lua` console command (for now, [it cannot do much](../8-LUA.md))\n- added a new `/restless` console command, which enables or disables infinite sprint\n- improved frames in Lara's jump-twist animations\n- improved object loading error messages when an invalid object ID is detected\n- improved window resize performance in the title inventory ring\n- improved projectiles\n    - changed conventional weapons to smash all shatterable objects simultaneously instead of 1 for rifles and 2 for pistols (#3378, #3551)\n    - fixed collision detection on windows\n    - fixed harpoons/grenades having no effect on bells (#3379)\n    - fixed conventional weapons not spawning ricochets on bells (#3379)\n- changed the game flow and game strings file placement\n- changed the texture page limit from 128 to unlimited (#3517)\n- changed the `/set` console command to report boolean values as `0` or `1`, language-agnostic\n- changed waterfall objects to always be drawn when active rather than only when Lara is within a 10 sector range (#3598)\n- changed `-l`/`--level` switch to accept the level number on top of the level path\n- changed settings dialogs to show a suitable message if a level builder has hidden all options within that dialog (#3637)\n- changed the text and bar scale option to work in smaller increments (10% reduced to 5%); added support for slow increments by 1% (hold Walk key)\n- changed the fly cheat to allow Lara to interact with switches and pickups (#3665)\n- fixed audio in the shower cutscene in Home Sweet Home not being sync with the turbo cheat (#3541)\n- fixed projectiles sometimes not shattering breakable windows (#3378, #3551)\n- fixed flat/opaque window shards in Lara's Home and Home Sweet Home (#3512)\n- fixed several OG texture issues (#1834, #2082, #3140, #3187, #3372, #3516, #3629, #3634, #3657, #3659, #3791, #3829, #3860)\n- fixed the passport having an invisible back page, noticeable when opening/closing it (#2051)\n- fixed z-fighting on the front of the passport (#3584)\n- fixed window 23 in Venice potentially appearing broken after loading a savegame, despite being intact before saving (#3559)\n- fixed French translations containing Italian text in some cases (#3567)\n- fixed several missing, delayed and duplicated door sound effects (#3363, #3614. #3615, #3616, #3663)\n- fixed being unable to antitrigger waterfall objects (#3589)\n- fixed incorrect frames in Lara's underwater roll animation (#1589)\n- fixed mismatched animation frames between the airlock wheel and its corresponding door in offshore levels (#3644)\n- fixed incorrect airlock and sliding door object positions in offshore levels (#3644)\n- fixed incorrect door positions in Nightmare in Vegas, causing some to be visible through walls (#3836)\n- fixed incorrect push button object positions in all levels where it appears (#3596)\n- fixed incorrect portals in Catacombs of the Talion room 41 (#3664)\n- fixed being unable to hang off bridges in Barkhang Monastery and Temple of Xian (#3691)\n- fixed missing zipline reset triggers in Lara's Home (#3698)\n- fixed shadows Y component not interpolated in 60 FPS (#1314)\n- fixed a crash when the level file was missing\n- fixed Lara walking backwards off ledges into lava (#3745)\n- fixed backslash/grave key/less-than character on some keyboards shown as ???? – now it's shown as backslash (#3713)\n- fixed Lara being able to get on a skidoo while underwater and consequently dying (#3810)\n- fixed a missing transition animation between Lara jumping forward and entering freefall (#3815)\n- fixed potentially being able to reactivate an already used puzzle slot's trigger (#3849, regression from 1.3)\n- fixed persistent damage resetting Lara's HP after cutscenes (#3595, regression from 1.2)\n- fixed Lara not being able to look when look mode is set to unrestricted and she is using an airlock door (#3645, regression from 1.3)\n- fixed wireframe mode rendering as mostly white (#3649, regression from 1.3.2)\n- fixed being unable to cycle poses in photo mode if cheats were disabled (#3726, regression from 1.3)\n- fixed Lara exiting the fly cheat if the walk key is used during photo mode (#3753, regression from 1.3)\n- fixed triggered pickup items flickering in custom levels (#3623, regression from 0.10)\n- fixed Lara not throwing away a spent flare when swimming of flying (#3816, regression from 0.8)\n- fixed flame SFX being audible underwater (#3830, regression from 0.3)\n- fixed harpoon gun not working correctly in NG+ (#3837, regression from 1.3)\n- fixed exiting photo mode on a controller conflicting with the roll input (#3842, regression from 0.9)\n- fixed Lara being able to move away from a keyhole/puzzle slot after selecting the key item from the inventory (#3866, regression from 1.3)\n\n## [1.3.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.3.1...tr2-1.3.2) - 2025-07-20\n- fixed audio playback with CDAudio backend in cutscenes (#2593)\n- fixed sprites having thick borders depending on viewing angle (#3549, regression from 1.3)\n- fixed savegame scanner only seeing all-lowercase file names (#3518, regression from 1.0)\n- fixed dynamic fire light being generated despite the flame object not being present in the level (#3539, regression from 1.3)\n- fixed harpoons disappearing if used near inactive/invisible enemies (#3546, regression from 1.3)\n- fixed the first camera frame when starting or loading a level being inaccurate (#3537, regression from 1.2.2)\n\n## [1.3.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.3...tr2-1.3.1) - 2025-07-18\n- fixed Lara's first pose in photo mode at times being skipped (#3522, regression from 1.3)\n- fixed Lara's arms being drawn inaccurately when posing in photo mode with dual weapons equipped (#3520, regression from 1.3)\n- fixed Lara's hair at times becoming detached when posing in photo mode with the M16 equipped (#3525, regression from 1.3)\n\n## [1.3](https://github.com/LostArtefacts/TRX/compare/tr2-1.2.2...tr2-1.3) - 2025-07-14\nShowcase: https://youtu.be/C9Nf4j05u_w\n- reworked scaler/sizer options\n    - added an option to set the upscaling filter (Graphic settings → Rendering → Upscaling filter)\n    - changed the \"Sizer\" option name to \"Upscaling factor\" (Graphic settings → Rendering → Upscaling factor)\n    - changed the maximum upscaling factor from 4 to 8\n    - changed the \"Scaler\" option name to \"Borders\" (Graphic settings → Rendering → Borders)\n    - changed the border option to use nice square borders if the aspect mode is set to Any\n    - greatly improved text and other UI rendering with upscaling turned on (#1944)\n    - removed default bindings for the \"Sizer\" and the \"Scaler\" options (#2853)\n    - changed screenshots to always produce images at desktop resolution\n- added French translation\n- added Gaelic translation\n- added Italian translation to the installer\n- added dedicated British English translation (#3212)\n- added the ability to advance individual frames to the photo mode\n- added the ability to skip end game credits (#3266)\n- added the ability to hide specific game settings (#3242)\n- added the ability to cycle UI tabs with sidestep keys (#3272)\n- added the ability to change the health bar color for allies, defaulting to green (#3005)\n- added the ability to skip consecutive credit images by holding the action / escape keys\n- added the ability to cycle between a list of predefined Lara poses in the photo mode\n- added the ability to use the dev console during FMVs\n- added a new easter egg command\n- added a `/lighting` console command to let the player turn lighting system on/off\n- added an `/immune` console command to make Lara impervious to damage\n- added an option to have dynamic lights generated by flames (Graphic options → Visuals → Fire lighting) (#3336)\n- added missing weapons to Lara's Home, Home Sweet Home and Nightmare in Vegas (for the weapons cheat) (#3360)\n- added the ability in custom levels to use the bear, wolf and ice warrior monk from The Golden Mask in the same level as spiders and other monks\n- added an option to use TR1 snappy swim turn behaviour (Gameplay settings → Controls → Smooth swimming) (#3387)\n- added an option to disable underwater twist (Gameplay settings → Controls → Underwater roll) (#3388)\n- added an option to disable jump twist and swan-dive roll (Gameplay settings → Controls → Jump twists) (#3388)\n- added an option to control responsive jumping lock behaviour (Gameplay settings → Controls → Jump lock mode) (#3389)\n- added an option to display level counter in the statistics dialog (Graphic options → UI → Level counter) (#1087)\n- added an option to control playing of certain animation sound effects such as doors when underwater (Sound options → Underwater animation SFX) (#3385)\n- added an option to choose between original TR1, original TR2 or unrestricted look modes (Gameplay settings → Controls → Look mode) (#3403)\n- added an option to make the quick gun equip keys also holster the active gun (Gameplay settings → UI → Quick gun keys) (#828)\n- added an option to allow the audio to mute when the game is out of focus (Sound options → Mute audio when focus lost, #3333)\n- added an option to control texture filter for UI alone (Graphic options → Rendering → UI filter)\n- added a 16:10 aspect ratio to the Aspect mode option (Graphic options → Rendering → Aspect mode)\n- added an inverted look camera option (Gameplay settings → Controls → Inverted look) (#3403)\n- added missing end of level statistic screens to Home Sweet Home and Kingdom (#2682)\n- added an option to control whether or not Lara reverts to pistols when going from one level to another (Gameplay settings → General → Remember guns between levels) (#3455)\n- improved performance when resizing the window\n- improved support for >3 secret dragons in custom levels up to 16 dragons\n- improved the `/tp` command to orient Lara towards keyholes and doors\n- improved handling of animation sound effects when in shallow water (#3385)\n- improved error messages for game flow and string edit mistakes to include path of the problematic file\n- ⚠️ changed game flow logic for a level that follows one that removed Lara's guns e.g. Diving Area: re-adding pistols now needs to be done in the game flow file, similar to Atlantis in TR1\n- changed statistics details mode to be placed in the UI section\n- changed controls dialog to remember the player's preferred input method\n- changed UI to show icons relevant to the chosen input method\n- changed death timer skip to only trigger with Action and Inventory keys\n- changed the examine dialog to be close-able with Look button (#3225)\n- changed some settings to be hidden when they're only applicable to specific games or custom levels (#3242)\n- changed some settings to be dimmed when they're not taking effect due to other settings (#3166)\n- changed photo mode help dialog to show icons for inputs\n- changed settings to retain their active position until exiting to title or starting a new level (#3271)\n- changed the dev console to accept compound characters (#2938)\n- changed the item duplication glitch fix to be on by default\n- changed the Bartoli's Hideout sunset effect to also apply to skybox lighting (#1617)\n- changed `/secret give` and `/secret take` to give or take all valid secrets when no index is specified\n- removed config tool (we have ingame setting dialogs now)\n- removed the limit of 10 dynamic lights per frame (#3384)\n- removed the `gym_enabled` game flow property\n- fixed inventory screen carpet background texture stretched on non-4:3 aspect ratios (#2022)\n- fixed picked up guns not appearing in holsters / on Lara's back (#1588)\n- fixed room 134 in Opera House having wrong textures (#3142)\n- fixed room 136 in Opera House not having water (#3214)\n- fixed Lara not saying 'aha' when picking up the secret in Lara's Home (#3103)\n- fixed Lara not drawing weapons with quick draw hotkeys if that was her last equipped weapon (#828)\n- fixed Lara not drawing weapons other than pistols and Shotgun with draw key if she didn't have any weapons (#828)\n- fixed Lara using flares only once when holding the flare key (#2062, regression from 0.3)\n- fixed Lara defaulting to pistols when starting Diving Area, if the player has not collected them in Offshore Rig (#828)\n- fixed missing zipline sound in Home Sweet Home (#3102)\n- fixed flare count getting corrupt on save/load if Lara had more than 255 flares (#1592)\n- fixed title screen background not updating aspect ratio when moving fullscreen window between monitors (#2842)\n- fixed title screen background and credit images stretching when using very wide resolutions (#2001)\n- fixed certain commands (such as `/load` or `/play`) not working as expected while in the key use inventory screen (#3338)\n- fixed Lara able to schedule an interaction with a detonator when it's in use (#3349)\n- fixed Lara not saying 'no' near gong or detonator when applicable (#3337)\n- fixed Lara saying 'no' near receptacles after loading a game (#1603)\n- fixed Lara saying 'no' near receptacles when using guns, medikits or flares (#1601)\n- fixed Lara being able to permanently discard a key item if she gets pushed on the exact frame she interact with a receptacle (#3398)\n- fixed key items getting consumed at the start of the interaction with receptacles (#3399)\n- fixed the Bartoli's Hideout sunset effect being reset after reloading a save (#1617)\n- fixed the shotgun sound at the end of the shower cutscene in Home Sweet Home being cut off when the credits start (#1579)\n- fixed the camera being partially inside the wall at the end of the Home Sweet Home shower cutscene (#3370)\n- fixed the boat veering if Lara looks left or right when driving (#3409)\n- fixed Lara not equipping a weapon chosen from inventory if it is the last weapon used (#3457)\n- fixed Stopwatch label in Gym not appearing when holding arrows during inventory spin-out (#3460)\n- fixed incorrectly shaded sprites (#3476, regression from 1.0)\n- fixed being able to deselect the passport in the game over screen (#3381, regression from 1.0)\n- fixed Lara getting stuck in the fly cheat in rare circumstances (#3392, regression from 0.3)\n- fixed hostile snowmobiles only shooting one gun (#3478, regression from 0.8)\n- fixed support for >3 secret dragons in custom levels (#3415, regression from 1.2)\n- fixed level select picking one level ahead of the one chosen if the gym is disabled (#3446, regression from 1.0)\n- fixed Lara's holsters resetting at times to incorrect meshes when using the fly cheat (#3451, regression from 0.3)\n- fixed a possible soft lock when saving the game after killing the last boss in Home Sweet Home (#3470, regression from 1.2)\n- fixed the `/play` command starting the level with wrong items sometimes (#3147, regression from 1.1)\n- fixed the `/play` command starting Gym in The Golden Mask (this level is not working correctly with TR2G's main.sfx)\n- fixed the `/tp` command breaking the photo mode\n- fixed the `/tp` command misbehaving when giving fractional coordinates\n- fixed the `/play` command not stopping active music when used to play Venice (#3469, regression from 0.8)\n- fixed Lara being affected by the `/kill` command if monks have been angered (#3492, regression from 1.0)\n\n## [1.2.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.2.1...tr2-1.2.2) - 2025-06-24\n- fixed underwater hum not playing properly (#3305, regression from 0.10)\n- fixed game crashing when the expected resources are missing (#3310, regression from 1.2.1)\n- fixed restore default pop-up requiring all 3 water color options to be adjusted instead of just one (#3314, regression from 1.2)\n- fixed pause screen rendered without background overlay if fade effects are disabled (#3316, regression from 1.1)\n- fixed `/pos` command crashing when the level title is not set (regression from 1.2)\n\n## [1.2.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.2...tr2-1.2.1) - 2025-06-22\n- fixed some secrets in some levels incorrectly registering by standing on specific tiles (#3280, regression from 1.2)\n- fixed movable blocks getting stuck in midair if the game is saved and loaded while they are falling (#3274)\n- fixed PS touchpad input missing an icon (#3288, regression from 4.12)\n- fixed inability to use unbind key / reset layout buttons with controllers (#3290, regression from 1.2)\n- fixed inventory ring consuming too many items under severe frame drop conditions (#3295, regression from 1.0)\n- fixed screenshots stripping accented characters (#3238)\n- fixed accented lowercase `i` characters retaining the superscript dot (#3298)\n- reverted the partial fix for wrong audio device reinitialization (#3251, regression from 1.2)\n\n## [1.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.1...tr2-1.2) - 2025-06-17\nShowcase: https://www.youtube.com/watch?v=yG82_Lt6v9M\n- added builtin support for ingame string translations\n    - changed duplicate game strings between TR1 and TR2 to be placed in a single file TRX_common_strings.json5\n    - added a new setting, `enable_review_markers`, which display which text requires review (only available via `/set`)\n    - added Italian translation\n    - added Polish translation\n    - added support for non-breaking spaces\n    - fixed game crashing when trying to word-wrap unknown characters\n- added UI for all config tool settings\n- added ingame help for all settings\n- added the ability to use `.avi`, `.mkv`, `.mp4`, `.mpeg`, and `.webm` files for FMVs, as well as the default `.rpl` (#3190)\n- added support for showing key/puzzle/pickup item descriptions (examining) in the inventory (#1875)\n- added support for object name aliases; added aliases for dev commands\n- added a pickup overlay display when Lara pulls the dagger from the dragon (#1830)\n- added an option to disable Lara's braid (#3089)\n- added an option to disable the breeze effect on Lara's braid (#3090)\n- added keyboard and controller input icons to the controls settings dialog\n- added an option to continue playing music while in the inventory (#1702)\n- added an option to adjust music and ambient volume while in the inventory (#2870)\n- added a `/debug` console command\n- added a `/secret` console command for easier debugging of secrets\n- added `enable_debug_pos` setting that shows Lara's position in realtime (reachable via `/debug`)\n- added graphics effects to the savegame so they now persist on load (#2736)\n- added an option to control whether or not Lara responds to hitting a wall while wading (#3138)\n- added an option to fix the breakable floor descending glitch (#3152)\n- added an option to fix wall glitches, or to use TR1 wall glitch behaviour (#3153)\n- added an option to disable swing cancelling (#3150)\n- added an option to disable lean jumping (#3151)\n- added an option to disable smooth wall deflection when Lara comes to a stop at a wall, similar to TR1 (#3148)\n- added an option to have Lara boost forward when rolling off one-click steps, similar to TR1 (#3149)\n- added an option to toggle allowing Lara to exit from water horizontally, below, or climbing out onto non-standable slopes (#3154)\n- added an option to toggle random enemy initial angle adjustment (#3129)\n- added an option to prevent Lara targeting allies, either with weapons or the skidoo (#3012)\n- added an option to alter Lara's HP for the beginning of each level (#3179)\n- added an option to not restore Lara's HP at the beginning of each level (#3179)\n- added an option to configure how many shots Lara can take with the harpoon gun before reloading, including disabling reloading altogether (#3057)\n- improved word wrapping algorithm in the dev console\n- improved the dev console commands documentation\n- changed logs format to include timestamps\n- changed the music track slot limit from 64 to 1024 (#3101)\n- ⚠️ changed the music track behaviour to no longer shift track numbers (#3100)\n  - if playing original levels, make sure to update the game flow and injection files from this release\n  - if building levels, use track numbers that correspond to the file names; previously built levels will need to be manually adjusted\n- changed the maximum number of 2D static mesh slots (room sprites) from 50 to 256 (#3200)\n- changed sound and music volumes to be displayed as percentage instead of 0-10\n- changed the `/tp` command to align Lara to switches and pickups\n- changed the `/set` command to accept `-`, which will restore the given setting to its default state\n- changed the graphic settings dialog to use tabs\n- changed the setting dialogs to respect the UI wraparound setting\n- changed the combat end logic (used in Home Sweet Home) to allow using any regular enemy type aside from the boss\n- changed the rotation of some pickups in The Golden Mask to better suit the 3D pickups option (#1973)\n- changed text kerning to a smaller value\n- fixed a missing collapsible tile trigger in The Cold War room 82 (#3058)\n- fixed missing sound effects for collapsible tiles in Opera House, The Deck and Catacombs of the Talion (#2262, #2872, #3087)\n- fixed texture and visibility issues with the skyboxes in The Cold War and Kingdom (#3056)\n- fixed the same boss item always being selected in Home Sweet Home, regardless of Lara's proximity (#3062)\n- fixed transparent eyes on Lara's model in the gym and Home Sweet Home levels (#3072)\n- fixed transparent eyes on the wolf model in Furnace of the Gods (#3073)\n- fixed Lara getting stuck in her hit animation if she is hit while using an airlock door, the detonator or the gong (#3092)\n- fixed Lara behaving erratically if she is killed while hanging from a ledge (#3134)\n- fixed Lara's health bar showing in the Home Sweet Home shower cutscene (#1564)\n- fixed Lara dropping flares after certain special animations, such as pulling the dagger from the dragon (#3084, regression from 1.1)\n- fixed unbind key option being available when it shouldn't (#3111, regression from 1.1)\n- fixed the sizer option accepting values above 1 which made no sense (#3123, regression from 1.0)\n- fixed a rare crash when editing certain dev console history entries (#2913, regression from 1.0)\n- fixed Lara's health bar showing at the start of cutscenes (#3182, regression from 1.1)\n- fixed scaler/sizer options not working under some circumstances (#3240, regression from 0.7)\n- fixed broken playback of mono music tracks (regression from 0.2)\n- fixed hot-plugging certain audio devices causing glitchy playback (partial fix; regression from 0.2)\n- fixed stats dialog reserving too much space for extra secrets (#3237, regression from 1.0)\n- fixed logging not outputting anything on Windows terminals\n- fixed `/kill all` command softlocking the game in Home Sweet Home\n\n## [1.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.2...tr2-1.1) - 2025-05-23\nShowcase: https://www.youtube.com/watch?v=g5lrrDXDYKo\n- added a /help command (#2917)\n- added a flashing Demo Mode caption to demos (#1556)\n- added arrows to the passport text like in TR1X (#2926)\n- added aliases to CLI options (`-gold` becomes `-g/--gold`)\n- added a `--help` CLI option (may not output anything on Windows machines – OS bug)\n- added explosion sprites to Home Sweet Home (#1569)\n- added ability to reposition the health bar and the air bar (#1611)\n- added enemy health bars (#2909)\n- added an FPS counter (#2910)\n- added the ability to move the camera around with W,A,S,D (rebindable) (#2978)\n- added an option to toggle between TR1 and TR2 camera modes (#2990)\n- added the ability to reset active inputs layout\n- added the ability to unbind non-essential keys\n- added the ability to rebind more keys\n- added the ability to trigger different ambient tracks in custom levels, which will loop and be remembered between saves\n- improved word wrapping algorithm in the dev console\n- improved the `/set` console command to display available options if given an unknown argument\n- improved handling of items that are dropped by enemies (#2952)\n    - added the ability for any enemy type to drop items, excluding eels\n    - fixed items dropped by flying creatures not falling to the ground\n- changed the design of the controls dialog to use pages, making it better suited for small screens, larger text sizes, and more key bindings\n- changed on-screen messages (such as `Z-Buffer on` to use the dev console, like in TR1X)\n- changed the sound dialog appearance (repositioned, added text labels and arrows)\n- changed the installer to always allow downloading music files (#2891)\n- changed the dev console to no longer add duplicate entries to the history\n- changed the health bar and the air bar sizes to be slightly bigger\n- changed the pause screen to have a darker black overlay transparency (#2252)\n- removed the hard-coded inventory allocation on the first level by default, moving it instead to the game flow (#1867)\n- removed the hard-coded repositioning of Bartoli (pre-dragon) on initialise (#2950)\n- fixed Lara's braid pointing straight down when swimming below sloped ceilings (#1600)\n- fixed glide cameras using a default speed rather than maintaining the values set in the level file (#2962)\n- fixed Lara being killed if she enters the void in a level that uses the `disable_floor` sequence in the game flow (#2874, regression from 0.10)\n- fixed Lara unable to equip pistols after getting a rifle-type weapon wet while wading (#2994)\n- fixed flame emitter 23 in room 6 not being deactivated when the lever in room 1 is used (#2851)\n- fixed Lara snapping to face forwards if she has a slight angle and action is pressed after using an airlock door (#2215)\n- fixed Lara being able to equip guns and flares during in-game cutscenes (#2895)\n- fixed an illegal reachable slope in Barkhang Monastery room 96, which could lead to Lara becoming softlocked (#2900)\n- fixed the camera behaving erratically in rooms/sectors that have no pathfinding data (#2946)\n- fixed wall light mesh positions in Venice, Bartoli's Hideout and Barkhang Monastery (#2944)\n- fixed faulty zoning data in Ice Palace rooms 48/110 that could result in the yetis becoming stuck (#3000)\n- fixed a misplaced springboard trigger in Ice Palace room 104 (#3003)\n- fixed the game crashing on unknown sequencer events\n- fixed the game crashing when editing long dev console history entries (#2913, regression from 1.0)\n- fixed harpoon's ammo counter overlapping with the air bar (#2871)\n- fixed flames showing briefly when Lara enters water and a death tile is present\n- fixed being unable to load a save made in the first level if that level removes Lara's weapons but also has a shotgun pickup (#2934, regression from 0.9)\n- fixed misplaced effects such as bubbles and dragon fire in 60 FPS (#2873, #2881, regression from 0.10)\n- fixed incorrect camera shifts when some fixed cameras return to normal view (#2971, regression from 0.10)\n- fixed blood not spawning when Lara is run down by boulders/barrels (#2982, regression from 0.7)\n- fixed floors being lowered too much under pushable blocks that are killed in the same trigger that flips the map (#3007, regression from 0.9)\n- fixed inventory ring items not being animated when the ring is rotating (#2964, regression from 0.9)\n- fixed the camera jumping if going from a look at trigger to a fixed camera, such as in The Cold War room 36 (#3033, regression from 0.9)\n- fixed a crash in The Golden Mask if the bear is killed with the grenade launcher (#3037, regression from 1.0)\n- fixed passport faces partially invisible\n\n## [1.0.2](https://github.com/LostArtefacts/TRX/compare/tr2-1.0.1...tr2-1.0.2) - 2025-04-26\n- changed The Golden Mask strings to default to the OG strings file for the main tables (#2847)\n- fixed Lara voiding if she stops on a tile with a closing door, and the door isn't on a portal (#2848)\n- fixed guns carried by enemies not being converted to ammo if Lara has picked up the same gun elsewhere in the same level (#2856)\n- fixed button mashing causing quick save/load to misbehave on a specific passport animation frame  (#2863, regression from 1.0)\n- fixed guns carried by enemies not being converted to ammo if Lara starts the level with the gun and the game has later been reloaded (#2850, regression from 1.0)\n- fixed 1920x1080 screenshots in 16:9 aspect mode being saved as 1919x1080 (#2845, regression from 0.8)\n- fixed clicks in audio sounds (#2846, regression from 0.2)\n\n## [1.0.1](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...tr2-1.0.1) - 2025-04-24\n- added an option to wraparound when scrolling UI dialogs, such as save/load (#2834)\n- improved graphic settings dialog sizing (#2841)\n- changed save to take priority over load when both inputs are held on the same frame, in line with OG (#2833)\n- fixed the selected keyboard/controller layout not being saved (#2830, regression from 1.0)\n- fixed toggling the PSX FOV option not having an immediate effect (#2831, regression from 1.0)\n- fixed changing the aspect ratio not updating the current background image (#2832, regression from 1.0)\n\n## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23\nShowcase: https://www.youtube.com/watch?v=iUNUJda6QCU\n- added support for The Golden Mask (#1621)\n- added ability to turn off legal screen and FMVs (#2740)\n- added ability to turn off ingame cutscenes (#2127)\n- added HD images from TR2Main (with Arsunt's consent)\n- added sunglasses for graphic options (#1615)\n- added control over the fog distances for players and level builders (#1622)\n- added control over the water color for players and level builders [see the reference](/docs/WATER_COLORS.md) (#1619)\n- added an installer for Windows (#2681)\n- added the bonus level game flow type, which allows for levels to be unlocked if all main game secrets are found (#2668)\n- added the ability for custom levels to have up to two of each secret type per level (#2674)\n- added BSON savegame support, removing the limits imposed by the OG 8KB file size, so allowing for storing more data and offering improved feature support (legacy save files can still be read, similar to TR1) (#2662)\n- added NG+, Japanese, and Japanese NG+ game mode options to the New Game page in the passport (#2731)\n- added the ability for spike walls to be reset (antitriggered)\n- added the current music track and timestamp to the savegame so they now persist on load (#2579)\n- added waterfalls to the savegame so that they now persist on load (#2686)\n- added support for aspect ratio-specific images (#1840)\n- added a guard to ensure the game always starts on a visible screen even after unplugging displays (#2819)\n- improved performance when moving the window around\n- improved pause exit dialog - it can now be canceled with escape\n- changed savegame files to be stored in the `saves` directory (#2087)\n- changed the default fog distance to 22 tiles cutting off at 30 tiles to match TR1X (#1622)\n- changed the number of static mesh slots from 50 to 256 (#2734)\n- changed the maximum number of items (moveables) per level from 256 to 10240 (1024 remains the limit for triggered items) (#1794)\n- changed the maximum number of visible enemies from 5 to 32 (#1624)\n- changed the maximum number of effects (flames, embers, exploding parts etc) from 100 to 1000 (#1581)\n- changed default pitch of the save/load dialog ingame - it's now higher.\n- removed the need to specify in the game flow levels that have no secrets (secrets will be automatically counted) (#1582)\n- removed the hard-coded end-level behaviour of the bird guardian for custom levels (#1583)\n- removed the FPS and aspect mode options from the config tool (now available in-game in the graphics options)\n- fixed the inability to completely mute the sounds, even at sound volume 0 (#2722)\n- fixed the final two levels not allowing for secrets to be counted in the statistics (#1582)\n- fixed assault course best times not being retained between game relaunches (#1578)\n- fixed flares disappearing on the ground when the z buffer is enabled (#1595)\n- fixed Lara's holsters being empty if a game flow level removes all weapons but also re-adds the pistols (#2677)\n- fixed the console opening when remapping its key (#2641)\n- fixed the boat when it explodes after crossing mines, where Lara's hips would appear rather than exploded boat parts (#1605)\n- fixed Lara's hips appearing on Bartoli in the Temple of Xian cutscene (#2558)\n- fixed collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground (#2752)\n- fixed the lift to work in any cardinal direction in custom levels, not just South (#2100)\n- fixed the springboard not responding correctly when Lara drives across one on a skidoo (#1903)\n- fixed the drawbridge producing dynamic light when open (#2294)\n- fixed the scale of several pickup models in The Golden Mask (#2652)\n- fixed the shark in The Cold War not making any sounds when biting Lara (#2678)\n- fixed the bird monster not having a shadow (#2060)\n- fixed the in-game cinematic camera at times yielding invalid positions (and hence views) in custom levels (#2754)\n- fixed a softlock in Temple of Xian if the main chamber key is missed (#2042)\n- fixed a potential softlock in Floating Islands if returning towards the level start from the gold secret (#2590)\n- fixed a potential softlock in Nightmare in Vegas where the bird monster could remain inactive, or the flip map not set (#1851)\n- fixed invalid portals in The Deck between rooms 17 and 104, which could result in Lara seeing enemies in disconnected rooms (#2393)\n- fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776)\n- fixed the boat briefly having an underwater hue when Lara first climbs on (#2787)\n- fixed destroyed gondolas appearing embedded in the ground after loading a save (#1612)\n- fixed a crash in custom levels with large rooms (#2749)\n- fixed the viewport not always in sync with the window (#2820)\n- fixed inability to move the window to another screen (#2820)\n- fixed flares flipped to the right when thrown (regression from 0.10)\n- fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 0.10)\n- fixed sprites rendering black if no shade value is assigned in the level (#2701, regression from 0.8)\n- fixed some 3D pickup items rendering black in software mode (#2792, regression from 0.10)\n- fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 0.3)\n- fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 0.3)\n- fixed a crash if an image was missing\n- fixed a crash on level load if an animation has no frames (#2746, regression from 0.8)\n- fixed flares missing the flicker effect in 60 FPS (#2806, regression from 0.10)\n\n## [0.10](https://github.com/LostArtefacts/TRX/compare/tr2-0.9.2...tr2-0.10) - 2025-03-18\nShowcase: https://www.youtube.com/watch?v=s41hznpTJkY\n- added support for 60 FPS rendering\n- added support for more accented characters (#2356)\n- added quadrilateral interpolation (#354)\n- added a `/cheats` console command\n- added a `/wireframe` console command (#2500)\n- added a `/fps` console command\n- added `/flood` and `/drain` console commands\n- added support for `-l`/`--level` argument to play a single level\n- added support for `-s`/`--save` argument to immediately start a saved game\n- added the ability to specify per-level SFX files rather than enforcing the default (main.sfx) on all levels (#2615)\n- added the camera shutter sound to cutscenes for photo mode (#2280)\n- added Italian localization to the config tool\n- improved camera mode navigation:\n    - improved support for pivoting\n    - improved roll support\n    - expanded world bounding box by 5 tiles in each direction\n    - added support for 60 FPS\n- changed injections to a new file format with a smaller footprint, improved applicability tests and similar feature support as TR1 (#1967)\n- changed the `/pos` command to show `Demo` and `Cutscene` instead of `Level` when relevant\n- changed the `/pos` command to show demo and cutscene numbers starting at 1, in line with `/play`\n- changed the `/play` and `/pos` commands to always treat the gym level as the level 0 – even if it's not included\n- removed the hardcoded title screen image path, replacing it with a game flow file property instead\n- fixed smashed windows blocking enemy pathing after loading a save (#2535)\n- fixed several instances of the camera going out of bounds (#1034)\n- fixed Lara getting stuck in a T-pose after jumping/falling and then dying before reaching fast fall speed (#2575)\n- fixed missing enemy sound effects in the underwater levels (#2293)\n- fixed seaweed collision in Living Quarters preventing Lara from climbing out of the water in room 15 (#2197)\n- fixed the scale and rotation of several pickup models, such as the offshore key cards and Barkhang prayer wheels (#1832, #1894)\n- fixed a rare issue whereby Lara would be unable to move after disposing a flare (#2545, regression from 0.9)\n- fixed flare pickups only adding one flare to Lara's inventory rather than six (#2551, regression from 0.9)\n- fixed several issues with pushblocks (#2036/#2193)\n    - fixed an invisible wall above stacked pushblocks if near a ceiling portal\n    - fixed floor height issues with pushblocks poised to fall in various scenarios\n    - fixed being unable to stack multiple pushblocks over multiple rooms\n    - fixed falling pushblocks using the enemy grunt sound effect\n- fixed play any level causing the game to hang when no gym level is present (#2560, regression from 0.9)\n- fixed extremely large item quantities crashing the game (#2497, regression from 0.3)\n- fixed missing new game text in the passport when play any level is enabled (#2563, regression from 0.9)\n- fixed the play any level dialog not showing in the gym passport (#2564, regression from 0.9)\n- fixed losing the NG+ flag when loading a save that has it set (#2566, regression from 0.9.2)\n- fixed the ammo counter not showing in demos if NG+ is set (#2574, regression from 0.9)\n- fixed being able to play with Lara invisible after using the explosion cheat then the fly cheat (#2584, regression from 0.9)\n- fixed the `/pos` command not showing demo and cutscene titles\n- fixed the distance travelled stat displaying the wrong value when over 1000m (#2659)\n\n## [0.9.2](https://github.com/LostArtefacts/TRX/compare/tr2-0.9.1...tr2-0.9.2) - 2025-02-19\n- fixed secret rewards not handed out after loading a save (#2528, regression from 0.8)\n- fixed music not working on certain Linux setups (#2504, regression from 0.2)\n\n## [0.9.1](https://github.com/LostArtefacts/TRX/compare/tr2-0.9...tr2-0.9.1) - 2025-02-15\n- improved memory usage by shedding ca. 100-110 MB on average\n- changed passport to be more responsive to player inputs (#1328)\n- fixed resolving paths (especially to music files) on case-sensitive filesystems (#1934, #2504)\n- fixed loading a game crashing on Linux (#2508, regression from 0.9)\n\n## [0.9](https://github.com/LostArtefacts/TRX/compare/tr2-0.8...tr2-0.9) - 2025-02-14\nShowcase: https://www.youtube.com/watch?v=FrBSW35ZPKY\n- added Linux builds and toolchain (#1598)\n- added macOS builds (for both Apple Silicon and Intel) (#2226)\n- added pause dialog (#1638)\n- added a photo mode feature (#2277)\n- added fade-out effect to the demos\n- added the ability to hold left/right to move through menus more quickly (#2298)\n- added the ability to disable exit fade effects alone (#2348)\n- added a fade-out effect when completing Lara's Home\n- added support for animated sprites (#2401)\n- added a `/cut` (alias: `/cutscene`) console command for playing cutscenes\n- added a `/gym` (alias: `/home`) console command for playing Lara's Home\n- added a `/music` console command that plays a specific music track\n- added a console log when using the `/demo` command\n- improved rendering to achieve a slight performance boost in big rooms (#2325)\n- improved wireframe mode appearance around screen edges\n- changed the object texture limit from 2048 to unlimited (within game's overall memory cap) (#1795)\n- changed the sprite texture limit from 512 to unlimited (within game's overall memory cap) (#1795)\n- changed the texture page limit from 32 to 128 (#1796)\n- changed default input bindings to let the photo mode binding be compatible with TR1X:\n    | Key                           | Old binding | New binding  |\n    | ----------------------------- | ----------- | ------------ |\n    | Decrease resolution           | Shift+F1    | Shift+F11    |\n    | Increase resolution           | F1          | F11          |\n    | Decrease internal screen size | Shift+F2    | Shift+F10    |\n    | Increase internal screen size | F2          | F10          |\n    | Toggle photo mode             | ---         | F1           |\n    | Toggle photo mode UI          | ---         | H            |\n- changed the `/kill` command with no arguments to look for enemies within 5 tiles (#2297)\n- changed the game data to use a separate strings file for text information, removing it from the game flow file\n- changed dynamic lighting for gun flashes and explosions to be optional (#2357)\n- fixed scale of secret icons on level complete summary (#1631)\n- fixed showing inventory ring up/down arrows when uncalled for (#2225)\n- fixed Lara never stepping backwards off a step using her right foot (#1602)\n- fixed flawed frame number checks which prevented Lara's wall hit animation while wading\n- fixed blood spawning on Lara from gunshots using incorrect positioning data (#2253)\n- fixed ghost meshes appearing near statics in custom levels (#2310)\n- fixed potential memory corruption when reading a custom level with more than 512 sprite textures (#2338)\n- fixed the teleporting command sometimes putting Lara in invalid flipmap rooms (#2370)\n- fixed teleporting to an item on a ledge sometimes pushing Lara to the room below (#2372)\n- fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames (#2413)\n- fixed being unable to load a level that contains no sound effect data (#2460)\n- fixed issues with sound effects not playing or looping forever in some cases when many other effects are playing (#2494)\n- fixed Lara activating triggers one frame too early (#2205, regression from 0.7)\n- fixed savegame incompatibility with OG (#2271, regression from 0.8)\n- fixed stopwatch showing wrong UI in some circumstances (#2221, regression from 0.8)\n- fixed excessive braid movement when dead in windy rooms (#2265, regression from 0.8)\n- fixed item counter shown even for a single medipack (#2222, regression from 0.3)\n- fixed item counter always hidden in NG+, even for keys (#2223, regression from 0.3)\n- fixed the passport object not being selected when exiting to title (#2192, regression from 0.8)\n- fixed the upside-down camera fix to no longer limit Lara's vision (#2276, regression from 0.8)\n- fixed /kill command freezing the game under rare circumstances (#2297, regression from 0.3)\n- fixed wireframe mode discarding transparent pixels (#2315, regression from 0.7)\n- fixed sprite pickups not being paused in the pause/inventory screen (#2319, regression from 0.6)\n- fixed Skidoo snow wake effects at slow speeds (#2324, regression from 0.6)\n- fixed software renderer skybox occlusion issues (#2343, regression from 0.7)\n- fixed gunflare from bandits in Tibetan levels spawning too far from their guns (#2365, regression from 0.8)\n- fixed guns sometimes appearing in Lara's hands when entering the fly cheat while undrawing weapons (#2376, regression from 0.3)\n- fixed the `/play` console command not resetting Lara's inventory (#2267, regression from 0.3)\n- fixed flashing text when trying to exit passport while Lara is dead and an action is required (#2263)\n\n## [0.8](https://github.com/LostArtefacts/TRX/compare/tr2-0.8...tr2-0.8) - 2025-01-01\n- completed decompilation efforts – TR2X.dll is gone, Tomb2.exe no longer needed (#1694)\n- added the ability to set user-defined FOV (no UI for it yet) (#2177)\n- added the ability to turn FMVs off (#2110)\n- added an option to use PS1 contrast levels, available under F8 (#1646)\n- added an option to use TR3+ side steps (#2111)\n- added an option to allow disabling the developer console (#2063)\n- added an optional fix for the QWOP glitch (#2122)\n- added an optional fix for the step glitch, where Lara can be pushed into walls (#2124)\n- added an optional fix for drawing a free flare during the underwater pickup animation (#2123)\n- added an optional fix for Lara drifting into walls when collecting underwater items (#2096)\n- added an option to control how music is played while underwater (#1937)\n- added an optional demo number argument to the `/demo` command\n- added an option to set the bar scaling (no UI for it yet) (#1636)\n- added an option to set the text scaling (no UI for it yet) (#1636)\n- improved the animation of Lara's braid (#2094)\n- changed demo to be interrupted only by esc or action keys\n- changed the turbo cheat to also affect ingame timer (#2167)\n- fixed health bar and air bar scaling (#2149)\n- fixed text being stretched on non-4:3 aspect ratios (#2012)\n- fixed Lara prioritising throwing a spent flare while mid-air, so to avoid missing ledge grabs (#1989)\n- fixed Lara at times not being able to jump immediately after going from her walking to running animation (#1587)\n- fixed bubbles spawning from flares if Lara is in shallow water (#1590)\n- fixed flare sound effects not always playing when Lara is in shallow water (#1590)\n- fixed looking forward too far causing an upside down camera frame (#1594)\n- fixed music not playing if triggered while the game is muted, but the volume is then increased (#2170)\n- fixed game FOV being interpreted as horizontal (#2002)\n- fixed the inventory up arrow at times overlapping the health bar (#2180)\n- fixed software renderer not applying underwater tint (#2066, regression from 0.7)\n- fixed some enemies not looking at Lara (#2080, regression from 0.6)\n- fixed the camera getting stuck at the start of Home Sweet Home (#2129, regression from 0.7)\n- fixed assault course timer not paused in the inventory (#2153, regression from 0.6)\n- fixed Lara spawning air bubbles above water surfaces during the fly cheat (#2115, regression from 0.3)\n- fixed demos playing too eagerly (#2068, regression from 0.3)\n- fixed Lara sometimes being unable to use switches (#2184, regression from 0.6)\n- fixed Lara interacting with airlock switches in unexpected ways (#2186, regression from 0.6)\n- fixed input controller remaps not being saved across game relaunches (#2422, regression from 0.6)\n\n## [0.7.1](https://github.com/LostArtefacts/TRX/compare/tr2-0.7...tr2-0.7.1) - 2024-12-17\n- fixed a crash when selecting the sound option (#2057, regression from 0.6)\n\n## [0.7](https://github.com/LostArtefacts/TRX/compare/tr2-0.6...tr2-0.7) - 2024-12-16\n- switched to OpenGL rendering (#1844)\n    - improved support for non-4:3 aspect ratios (#1647)\n    - changed fullscreen behavior to use windowed desktop mode (#1643)\n    - added an option for 1-2-3-4× pixel upscaling (available under the F1/Shift-F1 key)\n    - added the ability to use the window border option at all times (available under the F2/Shift-F2 key)\n    - added the ability to toggle between the software/hardware renderer at runtime (available under the F12 key)\n    - added fade effects to the hardware renderer (#1623)\n    - added an informative text when toggling various rendering options at runtime (#1873)\n    - added a wireframe mode (available with `/set` console command and with Shift+F7)\n    - changed the software renderer to use the picture's palette for the background pictures\n    - changed the hardware renderer to always use 16-bit textures (#1558)\n    - fixed texture corruption after FMVs play (#1562)\n    - fixed black borders in windowed mode (#1645)\n    - fixed \"Failed to create device\" when toggling fullscreen (#1842)\n    - fixed distant rooms sometimes not appearing, causing the skybox to be visible when it shouldn't (#2000)\n    - fixed rendering problems on certain Intel GPUs (#1574)\n- replaced the Windows Registry configuration with .json files\n    - removed setup dialog support (using `Tomb2.exe -setup` will have no effect on TR2X)\n    - removed unused detail level option\n    - removed triple buffering option\n    - removed dither option\n- added support for custom levels to enforce values for any config setting (#1846)\n- added an option to fix inventory item usage duplication (#1586)\n- added optional automatic key/puzzle inventory item pre-selection (#1884)\n- added a search feature to the config tool (#1889)\n- added an option to fix rotation on some pickup items to better suit 3D pickup mode (#1613)\n- added background for the final game stats (#1584)\n- added the ability to turn fade effects on/off (#1623)\n- removed unused detail level option\n- fixed a crash when trying to draw too many rooms at once (#1998)\n- fixed Lara getting stuck in her hit animation if she is hit while mounting the boat or skidoo (#1606)\n- fixed pistols appearing in Lara's hands when entering the fly cheat during certain animations (#1874)\n- fixed wrongly calculated trapdoor size that could affect custom levels (#1904)\n- fixed one of the collapsible tiles in Opera House room 184 not triggering (#1902)\n- fixed being unable to use the drawbridge key in Tibetan Foothills after the flipmap (#1744)\n- fixed missing triggers and ladder in Catacombs of the Talion after the flipmap (#1960)\n- fixed incorrect music trigger types at the beginning of Catacombs of the Talion (#1962)\n- fixed missing death tiles in Temple of Xian room 91 (#1920)\n- fixed the detonator key and gong hammer not activating their target items when manually selected from the inventory (#1887)\n- fixed wrongly positioned doors in Ice Palace and Floating Islands, which caused invisible walls (#1963)\n- fixed picking up the Gong Hammer in Ice Palace sometimes not opening the nearby door (#1716)\n- fixed room 98 in Wreck of the Maria Doria not having water (#1939)\n- fixed a potential crash if Lara is on the skidoo in a room with many other adjoining rooms (#1987)\n- fixed a softlock in Home Sweet Home if the final cutscene is triggered while Lara is on water surface (#1701)\n- fixed Lara's left arm becoming stuck if a flare is drawn just before the final cutscene in Home Sweet Home (#1992)\n- fixed resizing game window on the stats dialog cloning the UI elements, eventually crashing the game (#1999)\n- fixed exiting the game with Alt+F4 not immediately working in cutscenes\n- fixed game freezing when starting demo/credits/inventory offscreen\n- fixed problems when trying to launch the game with High DPI mode enabled (#1845)\n- fixed clock drift accumulating with time, causing audio desync in cutscenes (#1935, regression from 0.6)\n- fixed controllers dialog missing background in the software renderer mode (#1978, regression from 0.6)\n- fixed a crash relating to audio decoding (#1895, regression from 0.2)\n- fixed depth problems when drawing certain rooms (#1853, regression from 0.6)\n- fixed being unable to go from surface swimming to underwater swimming without first stopping (#1863, regression from 0.6)\n- fixed Lara continuing to walk after being killed if in that animation (#1880, regression from 0.1)\n- fixed some music tracks looping while Lara remained on the same trigger tile (#1899, regression from 0.2)\n- fixed some music tracks not playing if they were the last played track and the level had no ambience (#1899, regression from 0.2)\n- fixed broken final stats screen in software rendering mode (#1915, regression from 0.6)\n- fixed screenshots not capturing level stats (#1925, regression from 0.6)\n- fixed screenshots sometimes crashing in the windowed mode (regression from 0.6)\n- fixed creatures being able to swim/fly above the ceiling up to one tile (#1936, regression from 0.1)\n- fixed the `/kill all` command reporting an incorrect count in some levels (#1995, regression from 0.3)\n\n## [0.6](https://github.com/LostArtefacts/TRX/compare/tr2-0.5...tr2-0.6) - 2024-11-06\n- added a fly cheat key (#1642)\n- added an items cheat key (#1641)\n- added a level skip cheat key (#1640)\n- added a turbo cheat (#1639)\n- added the ability to skip end credits with the action and escape keys (#1800)\n- added the ability to skip FMVs with the action key (#1650)\n- added the ability to hold forward/back to move through menus more quickly (#1644)\n- added optional rendering of pickups in the UI as 3D meshes (#1633)\n- added optional rendering of pickups on the ground as 3D meshes (#1634)\n- added a special target, \"pickup\", to item-based console commands\n- changed the inputs backend from DirectX to SDL (#1695)\n    - improved controller support to match TR1X\n    - changed the number of custom layouts to 3\n    - changed default key bindings according to the following table:\n        | Key                           | Old binding | New binding  | Reason\n        | ----------------------------- | ----------- | ------------ | -----\n        | Flare                         | Comma (,)   | Period (.)   | To maintain forward compatibility with TR3\n        | Screenshot                    | S           | Print Screen | To maintain compatibility with TR1X\n        | Toggle bilinear filter        | F8          | F3           | To maintain compatibility with TR1X\n        | Toggle perspective filter     | Shift+F8    | F4           | To maintain compatibility with TR1X\n        | Toggle z-buffer               | F7          | F7           | Likely to be permanently enabled in the future\n        | Toggle triple buffering       | Shift+F7    | **Removed**  | Obscure setting, will be either removed or available via the ingame UI at some point\n        | Toggle dither                 | F11         | **Removed**  | Obscure setting, will be either removed or available via the ingame UI at some point\n        | Toggle fullscreen             | F12         | Alt-Enter    | To maintain compatibility with TR1X\n        | Toggle rendering mode         | Shift+F12   | F12          | No more conflict to require Shift\n        | Decrease resolution           | F1          | Shift+F1     | F3 and F4 are already taken\n        | Increase resolution           | F2          | F1           | F3 and F4 are already taken\n        | Decrease internal screen size | F3          | Shift+F2     | F3 and F4 are already taken\n        | Increase internal screen size | F4          | F2           | F3 and F4 are already taken\n    - removed \"falling through\" to the default layout, with the exception of keyboard arrows (matching TR1X behavior)\n    - removed hardcoded Shift+F7 key binding for toggling triple buffering\n    - removed hardcoded `0` key binding for flares\n    - removed hardcoded cooldown of 15 frames for medipacks\n- changed text backend to accept named sequences (eg. \"\\{arrow up}\" and similar)\n- changed inventory to pause the music rather than muting it (#1707)\n- changed the `/pos` command to include the level number and title\n- changed the `/tp` command to teleport to items in a round-robin fashion\n  The first call will teleport Lara to the object that's the closest to her; repeated calls will cycle through all matching objects in the object placement order.\n- improved FMV mode appearance - removed black scanlines (#1729)\n- improved FMV mode behavior - stopped switching screen resolutions (#1729)\n- improved screenshots: now saved in the screenshots/ directory with level titles and timestamps as JPG or PNG, similar to TR1X (#1773)\n- improved switch object names\n    - Switch Type 1 renamed to \"Airlock Switch\"\n    - Switch Type 2 renamed to \"Small Switch\"\n    - Switch Type 3 renamed to \"Switch Button\"\n    - Switch Type 4 renamed to \"Lever/Switch\"\n    - Switch Type 5 renamed to \"Underwater Lever/Switch\"\n- fixed screenshots not working in windowed mode (#1766)\n- fixed screenshots key not getting debounced (#1773)\n- fixed `/give` not working with weapons (regression from 0.5)\n- fixed the camera being cut off after using the gong hammer in Ice Palace (#1580)\n- fixed the audio not being in sync when Lara strikes the gong in Ice Palace (#1725)\n- fixed door cheat not working with drawbridges (#1748)\n- fixed certain audio samples continuing to play after finishing the level (#1770, regression from 0.2)\n- fixed Lara's underwater hue being retained when re-entering a boat (#1596)\n- fixed Lara reloading the harpoon gun after every shot in NG+ (#1575)\n- fixed the dragon reviving itself after Lara removes the dagger in rare circumstances (#1572)\n- fixed grenades counting as double kills in the game statistics (#1560)\n- fixed the ammo counter being hidden while a demo plays in NG+ (#1559)\n- fixed the game crashing in large rooms with z-buffer disabled (#1761, regression from 0.2)\n- fixed the game hanging if exited during the level stats, credits, or final stats (#1585)\n- fixed the console not being drawn during credits (#1802)\n- fixed grenades launched at too slow speeds (#1760, regression from 0.3)\n- fixed the dragon counting as more than one kill if allowed to revive (#1771)\n- fixed a crash when firing grenades at Xian guards in statue form (#1561)\n- fixed harpoon bolts damaging inactive enemies (#1804)\n- fixed enemies that are run over by the skidoo not being counted in the statistics (#1772)\n- fixed sound settings resuming the music (#1707)\n- fixed being able to use hotkeys in the end-level statistics screen\n- fixed the inventory ring spinout animation sometimes running too fast (#1704, regression from 0.3)\n- fixed new saves not displaying the save count in the passport (#1591)\n- fixed certain erroneous `/play` invocations resulting in duplicated error messages\n\n## [0.5](https://github.com/LostArtefacts/TRX/compare/afaf12a...tr2-0.5) - 2024-10-08\n- added `/sfx` command\n- added `/nextlevel` alias to `/endlevel` console command\n- added `/quit` alias to `/exit` console command\n- added the ability to cycle through console prompt history (#1571)\n- improved vertex movement when looking through water portals (#1493)\n- improved console commands targeting creatures and pickups (#1667)\n- changed `/set` console command to do fuzzy matching (LostArtefacts/libtrx#38)\n- fixed crash in the `/set` console command (regression from 0.3)\n- fixed using console in cutscenes immediately exiting the game (regression from 0.3)\n- fixed Lara remaining tilted when teleporting off a vehicle while on a slope (LostArtefacts/TR2X#275, regression from 0.3)\n- fixed `/endlevel` displaying a success message in the title screen\n- fixed very loud music volume set by default (#1614)\n\n## [0.4]\nVersion 0.4 was skipped because of a major repository merge with TR1X into TRX.\n\n## [0.3](https://github.com/LostArtefacts/TR2X/compare/0.2...0.3) - 2024-09-20\n- added new console commands:\n    - `/endlevel`\n    - `/demo`\n    - `/title`\n    - `/play [level]`\n    - `/load [slot]`\n    - `/save [slot]`\n    - `/exit`\n    - `/fly`\n    - `/give`\n    - `/kill`\n    - `/flip`\n    - `/set`\n- added the ability to remap the console key (LostArtefacts/TR2X#163)\n- added `/tp` console command's ability to teleport to specific items\n- added `/fly` console command's ability to open nearest doors\n- added an option to fix M16 accuracy while running (LostArtefacts/TR2X#45)\n- added a .NET-based configuration tool (LostArtefacts/TR2X#197)\n- improved initial level load time by lazy-loading audio samples (LostArtefacts/TR2X#114)\n- improved crash debug information (LostArtefacts/TR2X#137)\n- improved the console caret sprite (LostArtefacts/TR2X#91)\n- changed the default flare key from `/` to `.` to avoid conflicts with the console (LostArtefacts/TR2X#163)\n- fixed numeric keys interfering with the demos (LostArtefacts/TR2X#172)\n- fixed explosions sometimes being drawn too dark (LostArtefacts/TR2X#187)\n- fixed killing the T-Rex with a grenade launcher crashing the game (LostArtefacts/TR2X#168)\n- fixed secret rewards not displaying shotgun ammo (LostArtefacts/TR2X#159)\n- fixed controls dialog remapping being too sensitive (LostArtefacts/TR2X#5)\n- fixed `/tp` console command during special animations in HSH and Offshore Rig (LostArtefacts/TR2X#178, regression from 0.2)\n- fixed `/hp` console command taking arbitrary integers\n- fixed console commands being able to interfere with demos, cutscenes and the title screen (LostArtefacts/TR2X#182, #179, regression from 0.2)\n- fixed console registering key inputs too eagerly (regression from 0.2)\n- fixed console not being drawn in cutscenes (LostArtefacts/TR2X#180, regression from 0.2)\n- fixed sounds not playing under certain circumstances (LostArtefacts/TR2X#113, regression from 0.2)\n- fixed the excessive pitch and playback speed correction for music files with sampling rate other than 44100 Hz (LostArtefacts/TR1X#1417, regression from 0.2)\n- fixed a crash potential with certain music files (regression from 0.2)\n- fixed enemy movement patterns in demo 1 and demo 3 (LostArtefacts/TR2X#98, regression from 0.1)\n- fixed underwater creatures dying (LostArtefacts/TR2X#98, regression from 0.1)\n- fixed a crash when spawning enemy drops (LostArtefacts/TR2X#125, regression from 0.1)\n- fixed how sprites are shaded (LostArtefacts/TR2X#134, regression from 0.1.1)\n- fixed enemies unable to climb (LostArtefacts/TR2X#138, regression from 0.1)\n- fixed items not being reset between level loads (LostArtefacts/TR2X#142, regression from 0.1)\n- fixed pulling the dagger from the dragon not activating triggers (LostArtefacts/TR2X#148, regression from 0.1)\n- fixed the music at the beginning of Offshore Rig not playing (LostArtefacts/TR2X#150, regression from 0.1)\n- fixed wade animation when moving from deep to shallow water (LostArtefacts/TR2X#231, regression from 0.1)\n- fixed the distorted skybox in room 5 of Barkhang Monastery (LostArtefacts/TR2X#196)\n\n## [0.2](https://github.com/LostArtefacts/TR2X/compare/0.1.1...0.2) - 2024-05-07\n- added dev console with the following commands:\n    - `/pos`\n    - `/tp [room_num]`\n    - `/tp [x] [y] [z]`\n    - `/hp`\n    - `/hp [num]`\n    - `/heal`\n- changed the music backend from WinMM to libtrx (SDL + libav)\n- changed the sound backend from DirectX to libtrx (SDL + libav)\n- fixed seams around underwater portals (LostArtefacts/TR2X#76, regression from 0.1)\n- fixed Lara's climb down camera angle (LostArtefacts/TR2X#78, regression from 0.1)\n- fixed healthbar and airbar flashing the wrong way when at low values (LostArtefacts/TR2X#82, regression from 0.1)\n\n## [0.1.1](https://github.com/LostArtefacts/TR2X/compare/0.1...0.1.1) - 2024-04-27\n- fixed Lara's shadow with z-buffer option on (LostArtefacts/TR2X#64, regression from 0.1)\n- fixed rare camera issues (LostArtefacts/TR2X#65, regression from 0.1)\n- fixed flat rectangle colors (LostArtefacts/TR2X#70, regression from 0.1)\n- fixed medipacks staying open after use in Lara's inventory (LostArtefacts/TR2X#69, regression from 0.1)\n- fixed pickup sprites UI drawn forever in Lara's Home (LostArtefacts/TR2X#68, regression from 0.1)\n\n## [0.1](https://github.com/rr-/TR2X/compare/...0.1) - 2024-04-26\n- added version string to the inventory\n- fixed CDAudio not playing on certain versions (uses PaulD patch)\n- fixed TGA screenshots crashing the game\n"
  },
  {
    "path": "docs/tr2/INSTALLING.md",
    "content": "# Windows (installer)\n\n## Installing (simplified)\n\n1. Download the latest TRX installer for TR2 (e.g. `TRX-1.0-Windows_Installer-tr2.exe`).\n2. Mark the installer EXE as safe to run:\n    - Right-click on the `.exe`.\n    - Go to properties.\n    - Click \"Unblock\".\n3. Run the installer and proceed with the steps.\n\n> [!NOTE]\n> When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI‑based heuristics – they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because:\n>\n> - It isn't signed with a costly commercial certificate.\n> - It's a niche, community‑built project, so not widely recognized.\n> - It's a custom build, not from the Microsoft Store.\n>\n> Don't worry: TRX is open‑source, and you can inspect the code yourself on [GitHub](https://github.com/LostArtefacts/TRX/).\n\n# Windows / Linux\n\n## Installing (manual)\n\n1. Download the TRX zip file.\n2. Extract the zip file into a directory of your choice.  \n     Make sure you choose to overwrite existing directories and files.\n3. If installing for the first time – put your original game files into the target directory.\n\n   Optionally, you can also install the Golden Mask expansion pack files. Extract the contents of the following zip into the target directory:  \n   https://lostartefacts.dev/aux/tr2x/trgm.zip\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-win\">\n<pre><code>.\n├── cfg\n│   ├── presets\n│   │   ├── tr1-pc.json5\n│   │   ├── tr1-ps1.json5\n│   │   ├── tr2-pc.json5\n│   │   ├── tr2-ps1.json5\n│   │   ├── tr3-pc.json5\n│   │   └── tr3-ps1.json5\n│   ├── tr2\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-en-gb.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── tr2-gm\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── tr2-level\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── base_strings-de.json5\n│   ├── base_strings-en-gb.json5\n│   ├── base_strings-fr.json5\n│   ├── base_strings-gd.json5\n│   ├── base_strings-it.json5\n│   ├── base_strings-pl.json5\n│   ├── base_strings-ru.json5\n│   ├── base_strings.json5\n│   ├── catalog_item_actions.csv\n│   ├── catalog_lara_anims.csv\n│   ├── catalog_lara_states.csv\n│   ├── catalog_music.csv\n│   ├── catalog_objects.csv\n│   ├── catalog_samples.csv\n│   ├── inv_ring.json5\n│   ├── outfits.json5\n│   ├── poses.json5\n│   ├── TR2X.json5*\n│   ├── ui.json5\n│   └── weapons.json5\n├── data\n│   ├── images\n│   │   ├── 3x2\n│   │   │   ├── china.webp\n│   │   │   ├── credit00_gm.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit07_gm.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── end.webp\n│   │   │   ├── gm_level1.webp\n│   │   │   ├── gm_level2.webp\n│   │   │   ├── gm_level3.webp\n│   │   │   ├── gm_level4.webp\n│   │   │   ├── gm_level5.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_eu_gm.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── legal_us_gm.webp\n│   │   │   ├── mansion.webp\n│   │   │   ├── rig.webp\n│   │   │   ├── tibet.webp\n│   │   │   ├── titan.webp\n│   │   │   ├── title_eu.webp\n│   │   │   ├── title_eu_gm.webp\n│   │   │   ├── title_us.webp\n│   │   │   ├── title_us_gm.webp\n│   │   │   └── venice.webp\n│   │   ├── 4x3\n│   │   │   ├── china.webp\n│   │   │   ├── credit00_gm.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit07_gm.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── end.webp\n│   │   │   ├── gm_level1.webp\n│   │   │   ├── gm_level2.webp\n│   │   │   ├── gm_level3.webp\n│   │   │   ├── gm_level4.webp\n│   │   │   ├── gm_level5.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_eu_gm.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── legal_us_gm.webp\n│   │   │   ├── mansion.webp\n│   │   │   ├── rig.webp\n│   │   │   ├── tibet.webp\n│   │   │   ├── titan.webp\n│   │   │   ├── title_eu.webp\n│   │   │   ├── title_eu_gm.webp\n│   │   │   ├── title_us.webp\n│   │   │   ├── title_us_gm.webp\n│   │   │   └── venice.webp\n│   │   ├── og\n│   │   │   ├── china.webp\n│   │   │   ├── credit00_gm.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit07_gm.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── end.webp\n│   │   │   ├── legal.webp\n│   │   │   ├── mansion.webp\n│   │   │   ├── rig.webp\n│   │   │   ├── tibet.webp\n│   │   │   ├── titan.webp\n│   │   │   ├── title_eu.webp\n│   │   │   ├── title_eu_gm.webp\n│   │   │   ├── title_us.webp\n│   │   │   ├── title_us_gm.webp\n│   │   │   └── venice.webp\n│   │   ├── china.webp\n│   │   ├── credit00_gm.webp\n│   │   ├── credit01.webp\n│   │   ├── credit02.webp\n│   │   ├── credit03.webp\n│   │   ├── credit04.webp\n│   │   ├── credit05.webp\n│   │   ├── credit06.webp\n│   │   ├── credit07.webp\n│   │   ├── credit07_gm.webp\n│   │   ├── credit08.webp\n│   │   ├── end.webp\n│   │   ├── gm_level1.webp\n│   │   ├── gm_level2.webp\n│   │   ├── gm_level3.webp\n│   │   ├── gm_level4.webp\n│   │   ├── gm_level5.webp\n│   │   ├── legal_eu.webp\n│   │   ├── legal_eu_gm.webp\n│   │   ├── legal_us.webp\n│   │   ├── legal_us_gm.webp\n│   │   ├── mansion.webp\n│   │   ├── rig.webp\n│   │   ├── tibet.webp\n│   │   ├── titan.webp\n│   │   ├── title_eu.webp\n│   │   ├── title_eu_gm.webp\n│   │   ├── title_us.webp\n│   │   ├── title_us_gm.webp\n│   │   └── venice.webp\n│   ├── injections\n│   │   ├── barkhang_cameras.bin\n│   │   ├── barkhang_crystals.bin\n│   │   ├── barkhang_fd.bin\n│   │   ├── barkhang_itemrots.bin\n│   │   ├── barkhang_music_tracks.bin\n│   │   ├── barkhang_pickup_meshes.bin\n│   │   ├── barkhang_textures.bin\n│   │   ├── bartoli_crystals.bin\n│   │   ├── bartoli_music_tracks.bin\n│   │   ├── bartoli_secret_fd.bin\n│   │   ├── bartoli_textures.bin\n│   │   ├── boat_bits.bin\n│   │   ├── breakable_tile_sfx.bin\n│   │   ├── catacombs_crystals.bin\n│   │   ├── catacombs_fd.bin\n│   │   ├── catacombs_itemrots.bin\n│   │   ├── catacombs_music_tracks.bin\n│   │   ├── catacombs_textures.bin\n│   │   ├── coldwar_crystals.bin\n│   │   ├── coldwar_fd.bin\n│   │   ├── coldwar_itemrots.bin\n│   │   ├── coldwar_music_tracks.bin\n│   │   ├── coldwar_objects.bin\n│   │   ├── coldwar_textures.bin\n│   │   ├── common_pickup_meshes.bin\n│   │   ├── common_pickup_meshes_gm.bin\n│   │   ├── crystal.bin\n│   │   ├── cut2_setup.bin\n│   │   ├── cut2_textures.bin\n│   │   ├── cut3_setup.bin\n│   │   ├── cut3_textures.bin\n│   │   ├── cut4_setup.bin\n│   │   ├── cut4_textures.bin\n│   │   ├── dagger_sprite.bin\n│   │   ├── deck_cameras.bin\n│   │   ├── deck_crystals.bin\n│   │   ├── deck_fd.bin\n│   │   ├── deck_itemrots.bin\n│   │   ├── deck_music_tracks.bin\n│   │   ├── deck_pickup_meshes.bin\n│   │   ├── deck_plants.bin\n│   │   ├── deck_secret_fd.bin\n│   │   ├── deck_textures.bin\n│   │   ├── detonator_lights.bin\n│   │   ├── diving_cameras.bin\n│   │   ├── diving_crystals.bin\n│   │   ├── diving_itemrots.bin\n│   │   ├── diving_music_tracks.bin\n│   │   ├── diving_pickup_meshes.bin\n│   │   ├── diving_sfx.bin\n│   │   ├── diving_textures.bin\n│   │   ├── door106_sfx.bin\n│   │   ├── door107_sfx.bin\n│   │   ├── door108_sfx.bin\n│   │   ├── door110_sfx.bin\n│   │   ├── door111_sfx.bin\n│   │   ├── explosion.bin\n│   │   ├── fathoms_crystals.bin\n│   │   ├── fathoms_goon_sfx.bin\n│   │   ├── fathoms_itemrots.bin\n│   │   ├── fathoms_music_tracks.bin\n│   │   ├── fathoms_plants.bin\n│   │   ├── fathoms_secret_fd.bin\n│   │   ├── fathoms_textures.bin\n│   │   ├── floating_crystals.bin\n│   │   ├── floating_fd.bin\n│   │   ├── floating_itemrots.bin\n│   │   ├── floating_music_tracks.bin\n│   │   ├── floating_pickup_meshes.bin\n│   │   ├── floating_textures.bin\n│   │   ├── font.bin\n│   │   ├── fools_crystals.bin\n│   │   ├── fools_itemrots.bin\n│   │   ├── fools_music_tracks.bin\n│   │   ├── fools_pickup_meshes.bin\n│   │   ├── fools_textures.bin\n│   │   ├── furnace_crystals.bin\n│   │   ├── furnace_itemrots.bin\n│   │   ├── furnace_music_tracks.bin\n│   │   ├── furnace_objects.bin\n│   │   ├── furnace_pickup_meshes.bin\n│   │   ├── furnace_textures.bin\n│   │   ├── guardian_death_commands.bin\n│   │   ├── gym_fd.bin\n│   │   ├── gym_music_tracks.bin\n│   │   ├── gym_sfx.bin\n│   │   ├── gym_textures.bin\n│   │   ├── house_itemrots.bin\n│   │   ├── house_music_tracks.bin\n│   │   ├── house_sfx.bin\n│   │   ├── house_shower_frames.bin\n│   │   ├── house_textures.bin\n│   │   ├── inv_background.bin\n│   │   ├── kingdom_cameras.bin\n│   │   ├── kingdom_crystals.bin\n│   │   ├── kingdom_itemrots.bin\n│   │   ├── kingdom_music_tracks.bin\n│   │   ├── kingdom_textures.bin\n│   │   ├── lair_bartolipos.bin\n│   │   ├── lair_crystals.bin\n│   │   ├── lair_music_tracks.bin\n│   │   ├── lair_textures.bin\n│   │   ├── lara_animations.bin\n│   │   ├── lara_extra.bin\n│   │   ├── lara_guns.bin\n│   │   ├── lara_gym_guns.bin\n│   │   ├── lara_house_guns.bin\n│   │   ├── lara_outfits.bin\n│   │   ├── lara_rifle_sfx.bin\n│   │   ├── lara_vegas_guns.bin\n│   │   ├── living_crystals.bin\n│   │   ├── living_deck_goon_sfx.bin\n│   │   ├── living_fd.bin\n│   │   ├── living_itemrots.bin\n│   │   ├── living_music_tracks.bin\n│   │   ├── living_pickup_meshes.bin\n│   │   ├── living_secret_fd.bin\n│   │   ├── living_sfx.bin\n│   │   ├── living_textures.bin\n│   │   ├── loose_boards_sfx.bin\n│   │   ├── misc_sprites.bin\n│   │   ├── opera_crystals.bin\n│   │   ├── opera_fd.bin\n│   │   ├── opera_itemrots.bin\n│   │   ├── opera_music_tracks.bin\n│   │   ├── opera_sfx.bin\n│   │   ├── opera_textures.bin\n│   │   ├── palace_crystals.bin\n│   │   ├── palace_fd.bin\n│   │   ├── palace_itemrots.bin\n│   │   ├── palace_music_tracks.bin\n│   │   ├── palace_secret_fd.bin\n│   │   ├── palace_textures.bin\n│   │   ├── pda_model.bin\n│   │   ├── photo.bin\n│   │   ├── pickup_aid.bin\n│   │   ├── portcullis_sfx.bin\n│   │   ├── rig_crystals.bin\n│   │   ├── rig_itemrots.bin\n│   │   ├── rig_music_tracks.bin\n│   │   ├── rig_pickup_meshes.bin\n│   │   ├── rig_textures.bin\n│   │   ├── scuba_sfx.bin\n│   │   ├── seaweed_collision.bin\n│   │   ├── secret_models_gm.bin\n│   │   ├── secret_models_og.bin\n│   │   ├── shark_sfx.bin\n│   │   ├── tibet_crystals.bin\n│   │   ├── tibet_fd.bin\n│   │   ├── tibet_itemrots.bin\n│   │   ├── tibet_music_tracks.bin\n│   │   ├── tibet_textures.bin\n│   │   ├── title_textures.bin\n│   │   ├── vegas_crystals.bin\n│   │   ├── vegas_fd.bin\n│   │   ├── vegas_itemrots.bin\n│   │   ├── vegas_music_tracks.bin\n│   │   ├── vegas_textures.bin\n│   │   ├── venice_crystals.bin\n│   │   ├── venice_fd.bin\n│   │   ├── venice_itemrots.bin\n│   │   ├── venice_music_tracks.bin\n│   │   ├── venice_textures.bin\n│   │   ├── wall_cameras.bin\n│   │   ├── wall_crystals.bin\n│   │   ├── wall_itemrots.bin\n│   │   ├── wall_music_tracks.bin\n│   │   ├── wall_textures.bin\n│   │   ├── winston_model.bin\n│   │   ├── wreck_cameras.bin\n│   │   ├── wreck_crystals.bin\n│   │   ├── wreck_fd.bin\n│   │   ├── wreck_goon_sfx.bin\n│   │   ├── wreck_itemrots.bin\n│   │   ├── wreck_music_tracks.bin\n│   │   ├── wreck_pickup_meshes.bin\n│   │   ├── wreck_plants.bin\n│   │   ├── wreck_secret_fd.bin\n│   │   ├── wreck_textures.bin\n│   │   ├── xian_crystals.bin\n│   │   ├── xian_fd.bin\n│   │   ├── xian_itemrots.bin\n│   │   ├── xian_music_tracks.bin\n│   │   ├── xian_pickup_meshes.bin\n│   │   ├── xian_sfx.bin\n│   │   └── xian_textures.bin\n│   ├── scripts\n│   │   ├── assault.lua\n│   │   ├── cut3.lua\n│   │   ├── floating.lua\n│   │   ├── house.lua\n│   │   ├── level1.lua\n│   │   ├── level3.lua\n│   │   ├── level4.lua\n│   │   └── monastry.lua\n│   ├── assault.tr2\n│   ├── boat.tr2\n│   ├── catacomb.tr2\n│   ├── cut1.tr2\n│   ├── cut2.tr2\n│   ├── cut3.tr2\n│   ├── cut4.tr2\n│   ├── deck.tr2\n│   ├── emprtomb.tr2\n│   ├── floating.tr2\n│   ├── house.tr2\n│   ├── icecave.tr2\n│   ├── keel.tr2\n│   ├── level1.tr2\n│   ├── level2.tr2\n│   ├── level3.tr2\n│   ├── level4.tr2\n│   ├── level5.tr2\n│   ├── living.tr2\n│   ├── main.sfx\n│   ├── main_gm.sfx\n│   ├── monastry.tr2\n│   ├── opera.tr2\n│   ├── platform.tr2\n│   ├── rig.tr2\n│   ├── skidoo.tr2\n│   ├── title.tr2\n│   ├── title_gm.tr2\n│   ├── unwater.tr2\n│   ├── venice.tr2\n│   ├── wall.tr2\n│   └── xian.tr2\n├── fmv\n│   ├── ancient.rpl\n│   ├── crash.rpl\n│   ├── end.rpl\n│   ├── jeep.rpl\n│   ├── landing.rpl\n│   ├── logo.rpl\n│   ├── modern.rpl\n│   └── ms.rpl\n├── music\n│   ├── 2.mp3\n│   ├── 3.mp3\n│   ├── 4.mp3\n│   ├── 5.mp3\n│   ├── 6.mp3\n│   ├── 7.mp3\n│   ├── 8.mp3\n│   ├── 9.mp3\n│   ├── 10.mp3\n│   ├── 11.mp3\n│   ├── 12.mp3\n│   ├── 13.mp3\n│   ├── 14.mp3\n│   ├── 15.mp3\n│   ├── 16.mp3\n│   ├── 17.mp3\n│   ├── 18.mp3\n│   ├── 19.mp3\n│   ├── 20.mp3\n│   ├── 21.mp3\n│   ├── 22.mp3\n│   ├── 23.mp3\n│   ├── 24.mp3\n│   ├── 25.mp3\n│   ├── 26.mp3\n│   ├── 27.mp3\n│   ├── 28.mp3\n│   ├── 29.mp3\n│   ├── 30.mp3\n│   ├── 31.mp3\n│   ├── 32.mp3\n│   ├── 33.mp3\n│   ├── 34.mp3\n│   ├── 35.mp3\n│   ├── 36.mp3\n│   ├── 37.mp3\n│   ├── 38.mp3\n│   ├── 39.mp3\n│   ├── 40.mp3\n│   ├── 41.mp3\n│   ├── 42.mp3\n│   ├── 43.mp3\n│   ├── 44.mp3\n│   ├── 45.mp3\n│   ├── 46.mp3\n│   ├── 47.mp3\n│   ├── 48.mp3\n│   ├── 49.mp3\n│   ├── 50.mp3\n│   ├── 51.mp3\n│   ├── 52.mp3\n│   ├── 53.mp3\n│   ├── 54.mp3\n│   ├── 55.mp3\n│   ├── 56.mp3\n│   ├── 57.mp3\n│   ├── 58.mp3\n│   ├── 59.mp3\n│   ├── 60.mp3\n│   └── 61.mp3\n├── shaders\n│   ├── 2d.glsl\n│   ├── billboard.glsl\n│   ├── common.glsl\n│   ├── fbo.glsl\n│   ├── lights.glsl\n│   ├── meshes.glsl\n│   ├── meshes_tr3.glsl\n│   ├── meshes_tr12.glsl\n│   └── ui.glsl\n└── TRX.exe</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n\n## Playing the game\n\n- To play the game, run `TRX.exe`.\n- To play the Golden Mask expansion pack, run `TRX.exe --gold`.\n\n# macOS\n\n## Installing\n\n1. Download the latest TRX for TR2 installer image (e.g `TRX-0.1-Mac-tr2.dmg`). Mount the image and drag TR2X to the Applications folder.\n2. Run TR2X from the Applications folder. This will show you an error dialog about missing game data files. This is expected at this point, as you have not copied them in yet. However, it's important to run the app first to allow macOS to verify the app bundle's signature.\n3. Find TR2X in your Applications folder. Right-click it and click \"Show Package Contents\".\n4. Copy your Tomb Raider 2 game data files into `Contents/Resources`. (See the Windows / Linux instructions for retrieving game data from e.g. GOG.)\n\nIn case you see a popup \"TR2X is damaged\" when you run the game, run `xattr -cr /Applications/TR2X.app`.\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-mac\">\n<pre><code>.\n└── Contents\n    ├── Resources\n    │   ├── cfg\n    │   │   ├── presets\n    │   │   │   ├── tr1-pc.json5\n    │   │   │   ├── tr1-ps1.json5\n    │   │   │   ├── tr2-pc.json5\n    │   │   │   ├── tr2-ps1.json5\n    │   │   │   ├── tr3-pc.json5\n    │   │   │   └── tr3-ps1.json5\n    │   │   ├── tr2\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-en-gb.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr2-gm\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr2-level\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-fr.json5\n    │   │   │   ├── strings-gd.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   └── strings.json5\n    │   │   ├── base_strings-de.json5\n    │   │   ├── base_strings-en-gb.json5\n    │   │   ├── base_strings-fr.json5\n    │   │   ├── base_strings-gd.json5\n    │   │   ├── base_strings-it.json5\n    │   │   ├── base_strings-pl.json5\n    │   │   ├── base_strings-ru.json5\n    │   │   ├── base_strings.json5\n    │   │   ├── catalog_item_actions.csv\n    │   │   ├── catalog_lara_anims.csv\n    │   │   ├── catalog_lara_states.csv\n    │   │   ├── catalog_music.csv\n    │   │   ├── catalog_objects.csv\n    │   │   ├── catalog_samples.csv\n    │   │   ├── inv_ring.json5\n    │   │   ├── outfits.json5\n    │   │   ├── poses.json5\n    │   │   ├── ui.json5\n    │   │   └── weapons.json5\n    │   ├── data\n    │   │   ├── images\n    │   │   │   ├── 3x2\n    │   │   │   │   ├── china.webp\n    │   │   │   │   ├── credit00_gm.webp\n    │   │   │   │   ├── credit01.webp\n    │   │   │   │   ├── credit02.webp\n    │   │   │   │   ├── credit03.webp\n    │   │   │   │   ├── credit04.webp\n    │   │   │   │   ├── credit05.webp\n    │   │   │   │   ├── credit06.webp\n    │   │   │   │   ├── credit07.webp\n    │   │   │   │   ├── credit07_gm.webp\n    │   │   │   │   ├── credit08.webp\n    │   │   │   │   ├── end.webp\n    │   │   │   │   ├── gm_level1.webp\n    │   │   │   │   ├── gm_level2.webp\n    │   │   │   │   ├── gm_level3.webp\n    │   │   │   │   ├── gm_level4.webp\n    │   │   │   │   ├── gm_level5.webp\n    │   │   │   │   ├── legal_eu.webp\n    │   │   │   │   ├── legal_eu_gm.webp\n    │   │   │   │   ├── legal_us.webp\n    │   │   │   │   ├── legal_us_gm.webp\n    │   │   │   │   ├── mansion.webp\n    │   │   │   │   ├── rig.webp\n    │   │   │   │   ├── tibet.webp\n    │   │   │   │   ├── titan.webp\n    │   │   │   │   ├── title_eu.webp\n    │   │   │   │   ├── title_eu_gm.webp\n    │   │   │   │   ├── title_us.webp\n    │   │   │   │   ├── title_us_gm.webp\n    │   │   │   │   └── venice.webp\n    │   │   │   ├── 4x3\n    │   │   │   │   ├── china.webp\n    │   │   │   │   ├── credit00_gm.webp\n    │   │   │   │   ├── credit01.webp\n    │   │   │   │   ├── credit02.webp\n    │   │   │   │   ├── credit03.webp\n    │   │   │   │   ├── credit04.webp\n    │   │   │   │   ├── credit05.webp\n    │   │   │   │   ├── credit06.webp\n    │   │   │   │   ├── credit07.webp\n    │   │   │   │   ├── credit07_gm.webp\n    │   │   │   │   ├── credit08.webp\n    │   │   │   │   ├── end.webp\n    │   │   │   │   ├── gm_level1.webp\n    │   │   │   │   ├── gm_level2.webp\n    │   │   │   │   ├── gm_level3.webp\n    │   │   │   │   ├── gm_level4.webp\n    │   │   │   │   ├── gm_level5.webp\n    │   │   │   │   ├── legal_eu.webp\n    │   │   │   │   ├── legal_eu_gm.webp\n    │   │   │   │   ├── legal_us.webp\n    │   │   │   │   ├── legal_us_gm.webp\n    │   │   │   │   ├── mansion.webp\n    │   │   │   │   ├── rig.webp\n    │   │   │   │   ├── tibet.webp\n    │   │   │   │   ├── titan.webp\n    │   │   │   │   ├── title_eu.webp\n    │   │   │   │   ├── title_eu_gm.webp\n    │   │   │   │   ├── title_us.webp\n    │   │   │   │   ├── title_us_gm.webp\n    │   │   │   │   └── venice.webp\n    │   │   │   ├── og\n    │   │   │   │   ├── china.webp\n    │   │   │   │   ├── credit00_gm.webp\n    │   │   │   │   ├── credit01.webp\n    │   │   │   │   ├── credit02.webp\n    │   │   │   │   ├── credit03.webp\n    │   │   │   │   ├── credit04.webp\n    │   │   │   │   ├── credit05.webp\n    │   │   │   │   ├── credit06.webp\n    │   │   │   │   ├── credit07.webp\n    │   │   │   │   ├── credit07_gm.webp\n    │   │   │   │   ├── credit08.webp\n    │   │   │   │   ├── end.webp\n    │   │   │   │   ├── legal.webp\n    │   │   │   │   ├── mansion.webp\n    │   │   │   │   ├── rig.webp\n    │   │   │   │   ├── tibet.webp\n    │   │   │   │   ├── titan.webp\n    │   │   │   │   ├── title_eu.webp\n    │   │   │   │   ├── title_eu_gm.webp\n    │   │   │   │   ├── title_us.webp\n    │   │   │   │   ├── title_us_gm.webp\n    │   │   │   │   └── venice.webp\n    │   │   │   ├── china.webp\n    │   │   │   ├── credit00_gm.webp\n    │   │   │   ├── credit01.webp\n    │   │   │   ├── credit02.webp\n    │   │   │   ├── credit03.webp\n    │   │   │   ├── credit04.webp\n    │   │   │   ├── credit05.webp\n    │   │   │   ├── credit06.webp\n    │   │   │   ├── credit07.webp\n    │   │   │   ├── credit07_gm.webp\n    │   │   │   ├── credit08.webp\n    │   │   │   ├── end.webp\n    │   │   │   ├── gm_level1.webp\n    │   │   │   ├── gm_level2.webp\n    │   │   │   ├── gm_level3.webp\n    │   │   │   ├── gm_level4.webp\n    │   │   │   ├── gm_level5.webp\n    │   │   │   ├── legal_eu.webp\n    │   │   │   ├── legal_eu_gm.webp\n    │   │   │   ├── legal_us.webp\n    │   │   │   ├── legal_us_gm.webp\n    │   │   │   ├── mansion.webp\n    │   │   │   ├── rig.webp\n    │   │   │   ├── tibet.webp\n    │   │   │   ├── titan.webp\n    │   │   │   ├── title_eu.webp\n    │   │   │   ├── title_eu_gm.webp\n    │   │   │   ├── title_us.webp\n    │   │   │   ├── title_us_gm.webp\n    │   │   │   └── venice.webp\n    │   │   ├── injections\n    │   │   │   ├── barkhang_cameras.bin\n    │   │   │   ├── barkhang_crystals.bin\n    │   │   │   ├── barkhang_fd.bin\n    │   │   │   ├── barkhang_itemrots.bin\n    │   │   │   ├── barkhang_music_tracks.bin\n    │   │   │   ├── barkhang_pickup_meshes.bin\n    │   │   │   ├── barkhang_textures.bin\n    │   │   │   ├── bartoli_crystals.bin\n    │   │   │   ├── bartoli_music_tracks.bin\n    │   │   │   ├── bartoli_secret_fd.bin\n    │   │   │   ├── bartoli_textures.bin\n    │   │   │   ├── boat_bits.bin\n    │   │   │   ├── breakable_tile_sfx.bin\n    │   │   │   ├── catacombs_crystals.bin\n    │   │   │   ├── catacombs_fd.bin\n    │   │   │   ├── catacombs_itemrots.bin\n    │   │   │   ├── catacombs_music_tracks.bin\n    │   │   │   ├── catacombs_textures.bin\n    │   │   │   ├── coldwar_crystals.bin\n    │   │   │   ├── coldwar_fd.bin\n    │   │   │   ├── coldwar_itemrots.bin\n    │   │   │   ├── coldwar_music_tracks.bin\n    │   │   │   ├── coldwar_objects.bin\n    │   │   │   ├── coldwar_textures.bin\n    │   │   │   ├── common_pickup_meshes.bin\n    │   │   │   ├── common_pickup_meshes_gm.bin\n    │   │   │   ├── crystal.bin\n    │   │   │   ├── cut2_setup.bin\n    │   │   │   ├── cut2_textures.bin\n    │   │   │   ├── cut3_setup.bin\n    │   │   │   ├── cut3_textures.bin\n    │   │   │   ├── cut4_setup.bin\n    │   │   │   ├── cut4_textures.bin\n    │   │   │   ├── dagger_sprite.bin\n    │   │   │   ├── deck_cameras.bin\n    │   │   │   ├── deck_crystals.bin\n    │   │   │   ├── deck_fd.bin\n    │   │   │   ├── deck_itemrots.bin\n    │   │   │   ├── deck_music_tracks.bin\n    │   │   │   ├── deck_pickup_meshes.bin\n    │   │   │   ├── deck_plants.bin\n    │   │   │   ├── deck_secret_fd.bin\n    │   │   │   ├── deck_textures.bin\n    │   │   │   ├── detonator_lights.bin\n    │   │   │   ├── diving_cameras.bin\n    │   │   │   ├── diving_crystals.bin\n    │   │   │   ├── diving_itemrots.bin\n    │   │   │   ├── diving_music_tracks.bin\n    │   │   │   ├── diving_pickup_meshes.bin\n    │   │   │   ├── diving_sfx.bin\n    │   │   │   ├── diving_textures.bin\n    │   │   │   ├── door106_sfx.bin\n    │   │   │   ├── door107_sfx.bin\n    │   │   │   ├── door108_sfx.bin\n    │   │   │   ├── door110_sfx.bin\n    │   │   │   ├── door111_sfx.bin\n    │   │   │   ├── explosion.bin\n    │   │   │   ├── fathoms_crystals.bin\n    │   │   │   ├── fathoms_goon_sfx.bin\n    │   │   │   ├── fathoms_itemrots.bin\n    │   │   │   ├── fathoms_music_tracks.bin\n    │   │   │   ├── fathoms_plants.bin\n    │   │   │   ├── fathoms_secret_fd.bin\n    │   │   │   ├── fathoms_textures.bin\n    │   │   │   ├── floating_crystals.bin\n    │   │   │   ├── floating_fd.bin\n    │   │   │   ├── floating_itemrots.bin\n    │   │   │   ├── floating_music_tracks.bin\n    │   │   │   ├── floating_pickup_meshes.bin\n    │   │   │   ├── floating_textures.bin\n    │   │   │   ├── font.bin\n    │   │   │   ├── fools_crystals.bin\n    │   │   │   ├── fools_itemrots.bin\n    │   │   │   ├── fools_music_tracks.bin\n    │   │   │   ├── fools_pickup_meshes.bin\n    │   │   │   ├── fools_textures.bin\n    │   │   │   ├── furnace_crystals.bin\n    │   │   │   ├── furnace_itemrots.bin\n    │   │   │   ├── furnace_music_tracks.bin\n    │   │   │   ├── furnace_objects.bin\n    │   │   │   ├── furnace_pickup_meshes.bin\n    │   │   │   ├── furnace_textures.bin\n    │   │   │   ├── guardian_death_commands.bin\n    │   │   │   ├── gym_fd.bin\n    │   │   │   ├── gym_music_tracks.bin\n    │   │   │   ├── gym_sfx.bin\n    │   │   │   ├── gym_textures.bin\n    │   │   │   ├── house_itemrots.bin\n    │   │   │   ├── house_music_tracks.bin\n    │   │   │   ├── house_sfx.bin\n    │   │   │   ├── house_shower_frames.bin\n    │   │   │   ├── house_textures.bin\n    │   │   │   ├── inv_background.bin\n    │   │   │   ├── kingdom_cameras.bin\n    │   │   │   ├── kingdom_crystals.bin\n    │   │   │   ├── kingdom_itemrots.bin\n    │   │   │   ├── kingdom_music_tracks.bin\n    │   │   │   ├── kingdom_textures.bin\n    │   │   │   ├── lair_bartolipos.bin\n    │   │   │   ├── lair_crystals.bin\n    │   │   │   ├── lair_music_tracks.bin\n    │   │   │   ├── lair_textures.bin\n    │   │   │   ├── lara_animations.bin\n    │   │   │   ├── lara_extra.bin\n    │   │   │   ├── lara_guns.bin\n    │   │   │   ├── lara_gym_guns.bin\n    │   │   │   ├── lara_house_guns.bin\n    │   │   │   ├── lara_outfits.bin\n    │   │   │   ├── lara_rifle_sfx.bin\n    │   │   │   ├── lara_vegas_guns.bin\n    │   │   │   ├── living_crystals.bin\n    │   │   │   ├── living_deck_goon_sfx.bin\n    │   │   │   ├── living_fd.bin\n    │   │   │   ├── living_itemrots.bin\n    │   │   │   ├── living_music_tracks.bin\n    │   │   │   ├── living_pickup_meshes.bin\n    │   │   │   ├── living_secret_fd.bin\n    │   │   │   ├── living_sfx.bin\n    │   │   │   ├── living_textures.bin\n    │   │   │   ├── loose_boards_sfx.bin\n    │   │   │   ├── misc_sprites.bin\n    │   │   │   ├── opera_crystals.bin\n    │   │   │   ├── opera_fd.bin\n    │   │   │   ├── opera_itemrots.bin\n    │   │   │   ├── opera_music_tracks.bin\n    │   │   │   ├── opera_sfx.bin\n    │   │   │   ├── opera_textures.bin\n    │   │   │   ├── palace_crystals.bin\n    │   │   │   ├── palace_fd.bin\n    │   │   │   ├── palace_itemrots.bin\n    │   │   │   ├── palace_music_tracks.bin\n    │   │   │   ├── palace_secret_fd.bin\n    │   │   │   ├── palace_textures.bin\n    │   │   │   ├── pda_model.bin\n    │   │   │   ├── photo.bin\n    │   │   │   ├── pickup_aid.bin\n    │   │   │   ├── portcullis_sfx.bin\n    │   │   │   ├── rig_crystals.bin\n    │   │   │   ├── rig_itemrots.bin\n    │   │   │   ├── rig_music_tracks.bin\n    │   │   │   ├── rig_pickup_meshes.bin\n    │   │   │   ├── rig_textures.bin\n    │   │   │   ├── scuba_sfx.bin\n    │   │   │   ├── seaweed_collision.bin\n    │   │   │   ├── secret_models_gm.bin\n    │   │   │   ├── secret_models_og.bin\n    │   │   │   ├── shark_sfx.bin\n    │   │   │   ├── tibet_crystals.bin\n    │   │   │   ├── tibet_fd.bin\n    │   │   │   ├── tibet_itemrots.bin\n    │   │   │   ├── tibet_music_tracks.bin\n    │   │   │   ├── tibet_textures.bin\n    │   │   │   ├── title_textures.bin\n    │   │   │   ├── vegas_crystals.bin\n    │   │   │   ├── vegas_fd.bin\n    │   │   │   ├── vegas_itemrots.bin\n    │   │   │   ├── vegas_music_tracks.bin\n    │   │   │   ├── vegas_textures.bin\n    │   │   │   ├── venice_crystals.bin\n    │   │   │   ├── venice_fd.bin\n    │   │   │   ├── venice_itemrots.bin\n    │   │   │   ├── venice_music_tracks.bin\n    │   │   │   ├── venice_textures.bin\n    │   │   │   ├── wall_cameras.bin\n    │   │   │   ├── wall_crystals.bin\n    │   │   │   ├── wall_itemrots.bin\n    │   │   │   ├── wall_music_tracks.bin\n    │   │   │   ├── wall_textures.bin\n    │   │   │   ├── winston_model.bin\n    │   │   │   ├── wreck_cameras.bin\n    │   │   │   ├── wreck_crystals.bin\n    │   │   │   ├── wreck_fd.bin\n    │   │   │   ├── wreck_goon_sfx.bin\n    │   │   │   ├── wreck_itemrots.bin\n    │   │   │   ├── wreck_music_tracks.bin\n    │   │   │   ├── wreck_pickup_meshes.bin\n    │   │   │   ├── wreck_plants.bin\n    │   │   │   ├── wreck_secret_fd.bin\n    │   │   │   ├── wreck_textures.bin\n    │   │   │   ├── xian_crystals.bin\n    │   │   │   ├── xian_fd.bin\n    │   │   │   ├── xian_itemrots.bin\n    │   │   │   ├── xian_music_tracks.bin\n    │   │   │   ├── xian_pickup_meshes.bin\n    │   │   │   ├── xian_sfx.bin\n    │   │   │   └── xian_textures.bin\n    │   │   ├── scripts\n    │   │   │   ├── assault.lua\n    │   │   │   ├── cut3.lua\n    │   │   │   ├── floating.lua\n    │   │   │   ├── house.lua\n    │   │   │   ├── level1.lua\n    │   │   │   ├── level3.lua\n    │   │   │   ├── level4.lua\n    │   │   │   └── monastry.lua\n    │   │   ├── assault.tr2\n    │   │   ├── boat.tr2\n    │   │   ├── catacomb.tr2\n    │   │   ├── cut1.tr2\n    │   │   ├── cut2.tr2\n    │   │   ├── cut3.tr2\n    │   │   ├── cut4.tr2\n    │   │   ├── deck.tr2\n    │   │   ├── emprtomb.tr2\n    │   │   ├── floating.tr2\n    │   │   ├── house.tr2\n    │   │   ├── icecave.tr2\n    │   │   ├── keel.tr2\n    │   │   ├── level1.tr2\n    │   │   ├── level2.tr2\n    │   │   ├── level3.tr2\n    │   │   ├── level4.tr2\n    │   │   ├── level5.tr2\n    │   │   ├── living.tr2\n    │   │   ├── main.sfx\n    │   │   ├── main_gm.sfx\n    │   │   ├── monastry.tr2\n    │   │   ├── opera.tr2\n    │   │   ├── platform.tr2\n    │   │   ├── rig.tr2\n    │   │   ├── skidoo.tr2\n    │   │   ├── title.tr2\n    │   │   ├── title_gm.tr2\n    │   │   ├── unwater.tr2\n    │   │   ├── venice.tr2\n    │   │   ├── wall.tr2\n    │   │   └── xian.tr2\n    │   ├── fmv\n    │   │   ├── ancient.rpl\n    │   │   ├── crash.rpl\n    │   │   ├── end.rpl\n    │   │   ├── jeep.rpl\n    │   │   ├── landing.rpl\n    │   │   ├── logo.rpl\n    │   │   ├── modern.rpl\n    │   │   └── ms.rpl\n    │   ├── music\n    │   │   ├── 2.mp3\n    │   │   ├── 3.mp3\n    │   │   ├── 4.mp3\n    │   │   ├── 5.mp3\n    │   │   ├── 6.mp3\n    │   │   ├── 7.mp3\n    │   │   ├── 8.mp3\n    │   │   ├── 9.mp3\n    │   │   ├── 10.mp3\n    │   │   ├── 11.mp3\n    │   │   ├── 12.mp3\n    │   │   ├── 13.mp3\n    │   │   ├── 14.mp3\n    │   │   ├── 15.mp3\n    │   │   ├── 16.mp3\n    │   │   ├── 17.mp3\n    │   │   ├── 18.mp3\n    │   │   ├── 19.mp3\n    │   │   ├── 20.mp3\n    │   │   ├── 21.mp3\n    │   │   ├── 22.mp3\n    │   │   ├── 23.mp3\n    │   │   ├── 24.mp3\n    │   │   ├── 25.mp3\n    │   │   ├── 26.mp3\n    │   │   ├── 27.mp3\n    │   │   ├── 28.mp3\n    │   │   ├── 29.mp3\n    │   │   ├── 30.mp3\n    │   │   ├── 31.mp3\n    │   │   ├── 32.mp3\n    │   │   ├── 33.mp3\n    │   │   ├── 34.mp3\n    │   │   ├── 35.mp3\n    │   │   ├── 36.mp3\n    │   │   ├── 37.mp3\n    │   │   ├── 38.mp3\n    │   │   ├── 39.mp3\n    │   │   ├── 40.mp3\n    │   │   ├── 41.mp3\n    │   │   ├── 42.mp3\n    │   │   ├── 43.mp3\n    │   │   ├── 44.mp3\n    │   │   ├── 45.mp3\n    │   │   ├── 46.mp3\n    │   │   ├── 47.mp3\n    │   │   ├── 48.mp3\n    │   │   ├── 49.mp3\n    │   │   ├── 50.mp3\n    │   │   ├── 51.mp3\n    │   │   ├── 52.mp3\n    │   │   ├── 53.mp3\n    │   │   ├── 54.mp3\n    │   │   ├── 55.mp3\n    │   │   ├── 56.mp3\n    │   │   ├── 57.mp3\n    │   │   ├── 58.mp3\n    │   │   ├── 59.mp3\n    │   │   ├── 60.mp3\n    │   │   └── 61.mp3\n    │   ├── shaders\n    │   │   ├── 2d.glsl\n    │   │   ├── billboard.glsl\n    │   │   ├── common.glsl\n    │   │   ├── fbo.glsl\n    │   │   ├── lights.glsl\n    │   │   ├── meshes.glsl\n    │   │   ├── meshes_tr3.glsl\n    │   │   ├── meshes_tr12.glsl\n    │   │   └── ui.glsl\n    │   └── icon.icns\n    ├── _CodeSignature\n    ├── Frameworks\n    ├── info.plist\n    └── MacOS</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n"
  },
  {
    "path": "docs/tr2/symbols.txt",
    "content": "# TYPES\ntypedef IDirect3DDevice2 *LPDIRECT3DDEVICE2;\ntypedef IDirect3DTexture2 *LPDIRECT3DTEXTURE2;\ntypedef IDirect3DViewport2 *LPDIRECT3DVIEWPORT2;\ntypedef IDirect3DMaterial2 *LPDIRECT3DMATERIAL2;\ntypedef DDSURFACEDESC DDSDESC, *LPDDSDESC;\ntypedef LPDIRECTDRAWSURFACE3 LPDDS;\ntypedef LPDIRECTDRAW3 LPDD;\ntypedef D3DTEXTUREHANDLE HWR_TEXTURE_HANDLE;\n\ntypedef struct __unaligned {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n} XYZ_32;\n\ntypedef struct __unaligned {\n    int16_t x;\n    int16_t y;\n    int16_t z;\n} XYZ_16;\n\ntypedef struct __unaligned {\n    int32_t _00;\n    int32_t _01;\n    int32_t _02;\n    int32_t _03;\n    int32_t _10;\n    int32_t _11;\n    int32_t _12;\n    int32_t _13;\n    int32_t _20;\n    int32_t _21;\n    int32_t _22;\n    int32_t _23;\n} MATRIX;\n\ntypedef enum {\n    VGA_NO_VGA    = 0,\n    VGA_256_COLOR = 1,\n    VGA_MODEX     = 2,\n    VGA_STANDARD  = 3,\n} VGA_MODE;\n\ntypedef struct __unaligned {\n    LPBITMAPINFO bmp_info;\n    void *bmp_data;\n    HPALETTE hPalette;\n    DWORD flags;\n} BITMAP_RESOURCE;\n\ntypedef struct __unaligned {\n    int32_t width;\n    int32_t height;\n    int32_t bpp;\n    VGA_MODE vga;\n} DISPLAY_MODE;\n\ntypedef struct __unaligned DISPLAY_MODE_NODE {\n    struct DISPLAY_MODE_NODE *next;\n    struct DISPLAY_MODE_NODE *previous;\n    DISPLAY_MODE body;\n} DISPLAY_MODE_NODE;\n\ntypedef struct __unaligned {\n    DISPLAY_MODE_NODE *head;\n    DISPLAY_MODE_NODE *tail;\n    DWORD count;\n} DISPLAY_MODE_LIST;\n\ntypedef struct __unaligned {\n    char *content;\n    bool is_valid;\n} STRING_FLAGGED;\n\ntypedef struct __unaligned {\n    LPGUID adapter_guid_ptr;\n    GUID adapter_guid;\n    STRING_FLAGGED driver_desc;\n    STRING_FLAGGED driver_name;\n    DDCAPS_DX5 driver_caps;\n    DDCAPS_DX5 hel_caps;\n    GUID device_guid;\n    D3DDEVICEDESC_V2 hw_device_desc;\n    DISPLAY_MODE_LIST hw_disp_mode_list;\n    DISPLAY_MODE_LIST sw_disp_mode_list;\n    DISPLAY_MODE vga_mode1;\n    DISPLAY_MODE vga_mode2;\n    uint32_t screen_width;\n    bool hw_render_supported;\n    bool sw_windowed_supported;\n    bool hw_windowed_supported;\n    bool is_vga_mode1_presented;\n    bool is_vga_mode2_presented;\n    bool perspective_correct_supported;\n    bool dither_supported;\n    bool zbuffer_supported;\n    bool linear_filter_supported;\n    bool shade_restricted;\n} DISPLAY_ADAPTER;\n\ntypedef struct __unaligned DISPLAY_ADAPTER_NODE {\n    struct DISPLAY_ADAPTER_NODE *next;\n    struct DISPLAY_ADAPTER_NODE *previous;\n    DISPLAY_ADAPTER body;\n} DISPLAY_ADAPTER_NODE;\n\ntypedef struct __unaligned {\n    DISPLAY_ADAPTER_NODE *head;\n    DISPLAY_ADAPTER_NODE *tail;\n    DWORD count;\n} DISPLAY_ADAPTER_LIST;\n\ntypedef struct __unaligned {\n    GUID *adapter_guid_ptr;\n    GUID adapter_guid;\n    STRING_FLAGGED description;\n    STRING_FLAGGED module;\n} SOUND_ADAPTER;\n\ntypedef struct __unaligned SOUND_ADAPTER_NODE {\n    struct SOUND_ADAPTER_NODE *next;\n    struct SOUND_ADAPTER_NODE *previous;\n    SOUND_ADAPTER body;\n} SOUND_ADAPTER_NODE;\n\ntypedef struct __unaligned {\n    SOUND_ADAPTER_NODE *head;\n    SOUND_ADAPTER_NODE *tail;\n    DWORD count;\n} SOUND_ADAPTER_LIST;\n\ntypedef struct __unaligned {\n    GUID *lpJoystickGuid;\n    GUID joystickGuid;\n    STRING_FLAGGED productName;\n    STRING_FLAGGED instanceName;\n} JOYSTICK;\n\ntypedef struct __unaligned JOYSTICK_NODE {\n    struct JOYSTICK_NODE *next;\n    struct JOYSTICK_NODE *previous;\n    JOYSTICK body;\n} JOYSTICK_NODE;\n\ntypedef struct __unaligned JOYSTICK_LIST {\n    struct JOYSTICK_LIST *head;\n    struct JOYSTICK_LIST *tail;\n    DWORD count;\n} JOYSTICK_LIST;\n\ntypedef enum {\n    RM_UNKNOWN  = 0,\n    RM_SOFTWARE = 1,\n    RM_HARDWARE = 2,\n} RENDER_MODE;\n\ntypedef enum {\n    AM_4_3  = 0,\n    AM_16_9 = 1,\n    AM_ANY  = 2,\n} ASPECT_MODE;\n\ntypedef enum {\n    TAM_DISABLED      = 0,\n    TAM_BILINEAR_ONLY = 1,\n    TAM_ALWAYS        = 2,\n} TEXEL_ADJUST_MODE;\n\ntypedef struct __unaligned {\n    DISPLAY_ADAPTER_NODE *preferred_display_adapter;\n    SOUND_ADAPTER_NODE *preferred_sound_adapter;\n    JOYSTICK_NODE *preferred_joystick;\n    const DISPLAY_MODE_NODE *video_mode;\n    RENDER_MODE render_mode;\n    int32_t window_width;\n    int32_t window_height;\n    ASPECT_MODE aspect_mode;\n    bool perspective_correct;\n    bool dither;\n    bool zbuffer;\n    bool bilinear_filtering;\n    bool triple_buffering; // TODO: remove this option\n    bool fullscreen;\n    bool sound_enabled;\n    bool lara_mic; // TODO: remove this option\n    bool joystick_enabled;\n    bool disable_16bit_textures;\n    bool dont_sort_primitives;\n    bool flip_broken;\n    TEXEL_ADJUST_MODE texel_adjust_mode;\n    int32_t nearest_adjustment;\n    int32_t linear_adjustment;\n} APP_SETTINGS;\n\ntypedef struct __unaligned {\n    LPDDS sys_mem_surface;\n    LPDDS vid_mem_surface;\n    LPDIRECTDRAWPALETTE palette;\n    LPDIRECT3DTEXTURE2 texture_3d;\n    HWR_TEXTURE_HANDLE tex_handle;\n    int32_t width;\n    int32_t height;\n    int32_t status;\n} TEXPAGE_DESC;\n\ntypedef struct __unaligned {\n    union {\n        uint8_t red;\n        uint8_t r;\n    };\n    union {\n        uint8_t green;\n        uint8_t g;\n    };\n    union {\n        uint8_t blue;\n        uint8_t b;\n    };\n} RGB_888;\n\ntypedef struct __unaligned {\n    union {\n        uint8_t red;\n        uint8_t r;\n    };\n    union {\n        uint8_t green;\n        uint8_t g;\n    };\n    union {\n        uint8_t blue;\n        uint8_t b;\n    };\n    union {\n        uint8_t alpha;\n        uint8_t a;\n    };\n} RGBA_8888;\n\ntypedef struct {\n    struct {\n        uint32_t r;\n        uint32_t g;\n        uint32_t b;\n        uint32_t a;\n    } mask, depth, offset;\n} COLOR_BIT_MASKS;\n\ntypedef struct __unaligned {\n    D3DCOLOR clr[4][4];\n} GOURAUD_FILL;\n\ntypedef struct __unaligned {\n    D3DCOLOR clr[9];\n} GOURAUD_OUTLINE;\n\ntypedef struct __unaligned {\n    uint8_t index[256];\n} DEPTHQ_ENTRY;\n\ntypedef struct __unaligned {\n    uint8_t index[32];\n} GOURAUD_ENTRY;\n\ntypedef struct __unaligned {\n    XYZ_32 pos;\n    XYZ_16 rot;\n} PHD_3DPOS;\n\ntypedef struct __unaligned {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n    int32_t r;\n} SPHERE;\n\ntypedef struct __unaligned {\n    union {\n        uint32_t all;\n        struct {\n            uint32_t active:      1;\n            uint32_t flash:       1;\n            uint32_t rotate_h:    1;\n            uint32_t rotate_v:    1;\n            uint32_t centre_h:    1;\n            uint32_t centre_v:    1;\n            uint32_t hide:        1;\n            uint32_t right:       1;\n            uint32_t bottom:      1;\n            uint32_t background:  1;\n            uint32_t outline:     1;\n            uint32_t multiline:   1;\n            uint32_t manual_draw: 1; // not present in the OG\n        };\n    } flags;\n    uint16_t text_flags;\n    uint16_t bgnd_flags;\n    uint16_t outl_flags;\n    XYZ_16 pos;\n    int16_t letter_spacing;\n    int16_t word_spacing;\n    struct {\n        int16_t rate;\n        int16_t count;\n    } flash;\n    int16_t bgnd_color;\n    const uint16_t *bgnd_gour;\n    int16_t outl_color;\n    const uint16_t *outl_gour;\n    struct {\n        int16_t x;\n        int16_t y;\n    } bgnd_size;\n    XYZ_16 bgnd_off;\n    struct {\n        int32_t h;\n        int32_t v;\n    } scale;\n    char *content;\n} TEXTSTRING;\n\ntypedef struct __unaligned {\n    float xv;\n    float yv;\n    float zv;\n    float rhw;\n    float xs;\n    float ys;\n    int16_t clip;\n    int16_t g;\n    int16_t u;\n    int16_t v;\n} PHD_VBUF;\n\ntypedef struct __unaligned {\n    uint16_t u;\n    uint16_t v;\n} PHD_UV;\n\ntypedef struct __unaligned {\n    uint16_t draw_type;\n    uint16_t tex_page;\n    PHD_UV uv[4];\n} PHD_TEXTURE;\n\ntypedef struct __unaligned {\n    uint16_t tex_page;\n    uint16_t offset;\n    uint16_t width;\n    uint16_t height;\n    int16_t x0;\n    int16_t y0;\n    int16_t x1;\n    int16_t y1;\n} PHD_SPRITE;\n\ntypedef enum {\n    SHAPE_SPRITE = 1,\n    SHAPE_LINE   = 2,\n    SHAPE_BOX    = 3,\n    SHAPE_FBOX   = 4,\n} SHAPE;\n\ntypedef enum {\n    SPRF_RGB       = 0x00FFFFFF,\n    SPRF_ABS       = 0x01000000,\n    SPRF_SEMITRANS = 0x02000000,\n    SPRF_SCALE     = 0x04000000,\n    SPRF_SHADE     = 0x08000000,\n} SPRITE_FLAG;\n\ntypedef struct __unaligned {\n    float xv;\n    float yv;\n    float zv;\n    float rhw;\n    float xs;\n    float ys;\n    float u;\n    float v;\n    float g;\n} POINT_INFO;\n\ntypedef struct __unaligned {\n    float x;\n    float y;\n    float rhw;\n    float u;\n    float v;\n    float g;\n} VERTEX_INFO;\n\ntypedef enum {\n    INPUT_ROLE_FORWARD   = 0,\n    INPUT_ROLE_BACK      = 1,\n    INPUT_ROLE_LEFT      = 2,\n    INPUT_ROLE_RIGHT     = 3,\n    INPUT_ROLE_STEP_L    = 4,\n    INPUT_ROLE_STEP_R    = 5,\n    INPUT_ROLE_SLOW      = 6,\n    INPUT_ROLE_JUMP      = 7,\n    INPUT_ROLE_ACTION    = 8,\n    INPUT_ROLE_DRAW      = 9,\n    INPUT_ROLE_USE_FLARE = 10,\n    INPUT_ROLE_LOOK      = 11,\n    INPUT_ROLE_ROLL      = 12,\n    INPUT_ROLE_OPTION    = 13,\n} INPUT_ROLE;\n\ntypedef struct __unaligned {\n    uint16_t no_selector : 1;\n    uint16_t ready : 1; // not present in the OG\n    uint16_t pad : 14;\n    uint16_t items_count;\n    uint16_t selected;\n    uint16_t visible_count;\n    uint16_t line_offset;\n    uint16_t line_old_offset;\n    uint16_t pix_width;\n    uint16_t line_height;\n    int16_t x_pos;\n    int16_t y_pos;\n    int16_t z_pos;\n    uint16_t item_string_len;\n    char *pitem_strings1;\n    char *pitem_strings2;\n    uint32_t *pitem_flags1;\n    uint32_t *pitem_flags2;\n    uint32_t heading_flags1;\n    uint32_t heading_flags2;\n    uint32_t background_flags;\n    uint32_t moreup_flags;\n    uint32_t moredown_flags;\n    uint32_t item_flags1[24]; // MAX_REQUESTER_ITEMS\n    uint32_t item_flags2[24]; // MAX_REQUESTER_ITEMS\n    TEXTSTRING *heading_text1;\n    TEXTSTRING *heading_text2;\n    TEXTSTRING *background_text;\n    TEXTSTRING *moreup_text;\n    TEXTSTRING *moredown_text;\n    TEXTSTRING *item_texts1[24]; // MAX_REQUESTER_ITEMS\n    TEXTSTRING *item_texts2[24]; // MAX_REQUESTER_ITEMS\n    char heading_string1[32];\n    char heading_string2[32];\n    uint32_t render_width;\n    uint32_t render_height;\n} REQUEST_INFO;\n\ntypedef enum {\n    POLY_GTMAP        = 0,\n    POLY_WGTMAP       = 1,\n    POLY_GTMAP_PERSP  = 2,\n    POLY_WGTMAP_PERSP = 3,\n    POLY_LINE         = 4,\n    POLY_FLAT         = 5,\n    POLY_GOURAUD      = 6,\n    POLY_TRANS        = 7,\n    POLY_SPRITE       = 8,\n    POLY_HWR_GTMAP    = 9,\n    POLY_HWR_WGTMAP   = 10,\n    POLY_HWR_GOURAUD  = 11,\n    POLY_HWR_LINE     = 12,\n    POLY_HWR_TRANS    = 13,\n} POLY_TYPE;\n\ntypedef struct __unaligned {\n    uint32_t best_time[10];\n    uint32_t best_finish[10];\n    uint32_t finish_count;\n} ASSAULT_STATS;\n\ntypedef struct __unaligned {\n    int32_t _0;\n    int32_t _1;\n} SORT_ITEM;\n\ntypedef enum {\n    ST_AVG_Z = 0,\n    ST_MAX_Z = 1,\n    ST_FAR_Z = 2,\n} SORT_TYPE;\n\ntypedef enum {\n    DRAW_OPAQUE    = 0,\n    DRAW_COLOR_KEY = 1,\n} DRAW_TYPE;\n\ntypedef struct __unaligned {\n    int32_t floor;\n    int32_t ceiling;\n    int32_t type;\n} COLL_SIDE;\n\ntypedef struct __unaligned {\n    COLL_SIDE side_mid;\n    COLL_SIDE side_front;\n    COLL_SIDE side_left;\n    COLL_SIDE side_right;\n    int32_t radius;\n    int32_t bad_pos;\n    int32_t bad_neg;\n    int32_t bad_ceiling;\n    XYZ_32 shift;\n    XYZ_32 old;\n    int16_t old_anim_state;\n    int16_t old_anim_num;\n    int16_t old_frame_num;\n    int16_t facing;\n    int16_t quadrant;\n    int16_t coll_type;\n    int16_t *trigger;\n    int8_t x_tilt;\n    int8_t z_tilt;\n    int8_t hit_by_baddie;\n    int8_t hit_static;\n    uint16_t slopes_are_walls:   1; // 0x01 1\n    uint16_t slopes_are_pits:    1; // 0x02 2\n    uint16_t lava_is_pit:        1; // 0x04 4\n    uint16_t enable_baddie_push: 1; // 0x08 8\n    uint16_t enable_spaz:        1; // 0x10 16\n    uint16_t hit_ceiling:        1; // 0x20 32\n    uint16_t pad:                10;\n} COLL_INFO;\n\ntypedef struct __unaligned {\n    int16_t min_x;\n    int16_t max_x;\n    int16_t min_y;\n    int16_t max_y;\n    int16_t min_z;\n    int16_t max_z;\n} BOUNDS_16;\n\ntypedef struct __unaligned {\n    int16_t mesh_idx;\n    uint16_t flags;\n    BOUNDS_16 draw_bounds;\n    BOUNDS_16 collision_bounds;\n} STATIC_INFO;\n\ntypedef struct __unaligned {\n    int32_t floor;\n    uint32_t touch_bits;\n    uint32_t mesh_bits;\n    int16_t object_id;\n    int16_t current_anim_state;\n    int16_t goal_anim_state;\n    int16_t required_anim_state;\n    int16_t anim_num;\n    int16_t frame_num;\n    int16_t room_num;\n    int16_t next_item;\n    int16_t next_active;\n    int16_t speed;\n    int16_t fall_speed;\n    int16_t hit_points;\n    int16_t box_num;\n    int16_t timer;\n    uint16_t flags;\n    int16_t shade_1;\n    int16_t shade_2;\n    int16_t carried_item;\n    void *data;\n    union {\n        struct {\n            XYZ_32 pos;\n            XYZ_16 rot;\n        };\n        PHD_3DPOS pos_full; // TODO: stick to pos and rot\n    };\n    uint16_t active:        1; // 0x0001\n    uint16_t status:        2; // 0x0002…0x0004\n    uint16_t gravity:       1; // 0x0008\n    uint16_t hit_status:    1; // 0x0010\n    uint16_t collidable:    1; // 0x0020\n    uint16_t looked_at:     1; // 0x0040\n    uint16_t dynamic_light: 1; // 0x0080\n    uint16_t killed:        1; // 0x0100\n    uint16_t pad:           7; // 0x0200…0x8000\n} ITEM;\n\ntypedef struct __unaligned {\n    uint32_t timer;\n    uint32_t shots;\n    uint32_t hits;\n    uint32_t distance;\n    uint16_t kills;\n    uint8_t secrets_flags;\n    uint8_t medipacks;\n} STATISTICS_INFO;\n\ntypedef struct __unaligned {\n    uint16_t pistol_ammo;\n    uint16_t magnum_ammo;\n    uint16_t uzi_ammo;\n    uint16_t shotgun_ammo;\n    uint16_t m16_ammo;\n    uint16_t grenade_ammo;\n    uint16_t harpoon_ammo;\n    uint8_t small_medipacks;\n    uint8_t large_medipacks;\n    uint8_t reserved1;\n    uint8_t flares;\n    uint8_t gun_status;\n    uint8_t gun_type;\n    uint16_t available:   1; // 0x01 1\n    uint16_t has_pistols: 1; // 0x02 2\n    uint16_t has_magnums: 1; // 0x04 4\n    uint16_t has_uzis:    1; // 0x08 8\n    uint16_t has_shotgun: 1; // 0x10 16\n    uint16_t has_m16:     1; // 0x20 32\n    uint16_t has_grenade: 1; // 0x40 64\n    uint16_t has_harpoon: 1; // 0x80 128\n    uint16_t pad : 8;\n    uint16_t reserved2;\n    STATISTICS_INFO statistics;\n} START_INFO;\n\ntypedef struct __unaligned {\n    START_INFO start[24];\n    STATISTICS_INFO statistics;\n    int16_t current_level;\n    bool bonus_flag;\n    uint8_t num_pickup[2];\n    uint8_t num_puzzle[4];\n    uint8_t num_key[4];\n    uint16_t reserved;\n    char buffer[6272]; // MAX_SG_BUFFER_SIZE\n} SAVEGAME_INFO;\n\ntypedef struct __unaligned {\n    uint16_t idx;\n    int16_t box;\n    uint8_t pit_room;\n    int8_t floor;\n    uint8_t sky_room;\n    int8_t ceiling;\n} SECTOR;\n\ntypedef struct __unaligned {\n    int16_t lock_angles[4];\n    int16_t left_angles[4];\n    int16_t right_angles[4];\n    int16_t aim_speed;\n    int16_t shot_accuracy;\n    int32_t gun_height;\n    int32_t damage;\n    int32_t target_dist;\n    int16_t recoil_frame;\n    int16_t flash_time;\n    int16_t sample_num;\n} WEAPON_INFO;\n\ntypedef struct __unaligned {\n    XYZ_32 pos;\n    XYZ_16 rot;\n    int16_t room_num;\n    int16_t object_id;\n    int16_t next_free;\n    int16_t next_active;\n    int16_t speed;\n    int16_t fall_speed;\n    int16_t frame_num;\n    int16_t counter;\n    int16_t shade;\n} EFFECT;\n\ntypedef struct __unaligned {\n    int16_t zone_num;\n    int16_t enemy_zone_num;\n    int32_t distance;\n    int32_t ahead;\n    int32_t bite;\n    int16_t angle;\n    int16_t enemy_facing;\n} AI_INFO;\n\ntypedef struct __unaligned {\n    int16_t exit_box;\n    uint16_t search_num;\n    int16_t next_expansion;\n    int16_t box_num;\n} BOX_NODE;\n\ntypedef struct __unaligned {\n    BOX_NODE *node;\n    int16_t head;\n    int16_t tail;\n    uint16_t search_num;\n    uint16_t block_mask;\n    int16_t step;\n    int16_t drop;\n    int16_t fly;\n    int16_t zone_count;\n    int16_t target_box;\n    int16_t required_box;\n    XYZ_32 target;\n} LOT_INFO;\n\ntypedef enum {\n    GFL_NO_LEVEL  = -1,\n    GFL_TITLE     = 0,\n    GFL_NORMAL    = 1,\n    GFL_SAVED     = 2,\n    GFL_DEMO      = 3,\n    GFL_CUTSCENE  = 4,\n    GFL_STORY     = 5,\n    GFL_QUIET     = 6,\n    GFL_MID_STORY = 7,\n} GAME_FLOW_LEVEL_TYPE;\n\ntypedef struct __unaligned {\n    int16_t timer;\n    int16_t sprite;\n} PICKUP_INFO;\n\ntypedef struct __unaligned {\n    int16_t shape;\n    XYZ_16 pos;\n    int32_t param1;\n    int32_t param2;\n    void *grdptr;\n    int16_t sprite_num;\n} INVENTORY_SPRITE;\n\ntypedef struct __unaligned {\n    char *string;\n    int16_t object_id;\n    int16_t frames_total;\n    int16_t current_frame;\n    int16_t goal_frame;\n    int16_t open_frame;\n    int16_t anim_direction;\n    int16_t anim_speed;\n    int16_t anim_count;\n    int16_t x_rot_pt_sel;\n    int16_t x_rot_pt;\n    int16_t x_rot_sel;\n    int16_t x_rot_nosel;\n    int16_t x_rot;\n    int16_t y_rot_sel;\n    int16_t y_rot;\n    int32_t y_trans_sel;\n    int32_t y_trans;\n    int32_t z_trans_sel;\n    int32_t z_trans;\n    uint32_t meshes_sel;\n    uint32_t meshes_drawn;\n    int16_t inv_pos;\n    INVENTORY_SPRITE **sprite_list;\n    int32_t reserved[4];\n} INVENTORY_ITEM;\n\ntypedef enum {\n    RNG_OPENING           = 0,\n    RNG_OPEN              = 1,\n    RNG_CLOSING           = 2,\n    RNG_MAIN2OPTION       = 3,\n    RNG_MAIN2KEYS         = 4,\n    RNG_KEYS2MAIN         = 5,\n    RNG_OPTION2MAIN       = 6,\n    RNG_SELECTING         = 7,\n    RNG_SELECTED          = 8,\n    RNG_DESELECTING       = 9,\n    RNG_DESELECT          = 10,\n    RNG_CLOSING_ITEM      = 11,\n    RNG_EXITING_INVENTORY = 12,\n    RNG_DONE              = 13,\n} RING_STATUS;\n\ntypedef struct __unaligned {\n    int16_t count;\n    int16_t status;\n    int16_t status_target;\n    int16_t radius_target;\n    int16_t radius_rate;\n    int16_t camera_y_target;\n    int16_t camera_y_rate;\n    int16_t camera_pitch_target;\n    int16_t camera_pitch_rate;\n    int16_t rotate_target;\n    int16_t rotate_rate;\n    int16_t item_pt_x_rot_target;\n    int16_t item_pt_x_rot_rate;\n    int16_t item_x_rot_target;\n    int16_t item_x_rot_rate;\n    int32_t item_y_trans_target;\n    int32_t item_y_trans_rate;\n    int32_t item_z_trans_target;\n    int32_t item_z_trans_rate;\n    int32_t misc;\n} IMOTION_INFO;\n\ntypedef enum {\n    PM_SPINE    = 1,\n    PM_FRONT    = 2,\n    PM_IN_FRONT = 4,\n    PM_PAGE_2   = 8,\n    PM_BACK     = 16,\n    PM_IN_BACK  = 32,\n    PM_PAGE_1   = 64,\n    PM_COMMON   = PM_SPINE | PM_BACK | PM_FRONT,\n} PASS_MESH;\n\ntypedef struct __unaligned {\n    INVENTORY_ITEM **list;\n    int16_t type;\n    int16_t radius;\n    int16_t camera_pitch;\n    int16_t rotating;\n    int16_t rot_count;\n    int16_t current_object;\n    int16_t target_object;\n    int16_t number_of_objects;\n    int16_t angle_adder;\n    int16_t rot_adder;\n    int16_t rot_adder_l;\n    int16_t rot_adder_r;\n    PHD_3DPOS ring_pos;\n    PHD_3DPOS camera;\n    XYZ_32 light;\n    IMOTION_INFO *imo;\n} RING_INFO;\n\ntypedef enum {\n    GFE_PICTURE          = 0,\n    GFE_LIST_START       = 1,\n    GFE_LIST_END         = 2,\n    GFE_PLAY_FMV         = 3,\n    GFE_START_LEVEL      = 4,\n    GFE_CUTSCENE         = 5,\n    GFE_LEVEL_COMPLETE   = 6,\n    GFE_DEMO_PLAY        = 7,\n    GFE_JUMP_TO_SEQ      = 8,\n    GFE_END_SEQ          = 9,\n    GFE_SET_TRACK        = 10,\n    GFE_SUNSET           = 11,\n    GFE_LOADING_PIC      = 12,\n    GFE_DEADLY_WATER     = 13,\n    GFE_REMOVE_WEAPONS   = 14,\n    GFE_GAME_COMPLETE    = 15,\n    GFE_CUT_ANGLE        = 16,\n    GFE_NO_FLOOR         = 17,\n    GFE_ADD_TO_INV       = 18,\n    GFE_START_ANIM       = 19,\n    GFE_NUM_SECRETS      = 20,\n    GFE_KILL_TO_COMPLETE = 21,\n    GFE_REMOVE_AMMO      = 22,\n} GF_EVENTS;\n\ntypedef enum {\n    MOOD_BORED  = 0,\n    MOOD_ATTACK = 1,\n    MOOD_ESCAPE = 2,\n    MOOD_STALK  = 3,\n} MOOD_TYPE;\n\ntypedef enum {\n    TARGET_NONE      = 0,\n    TARGET_PRIMARY   = 1,\n    TARGET_SECONDARY = 2,\n} TARGET_TYPE;\n\ntypedef struct __unaligned {\n    XYZ_32 pos;\n    int32_t mesh_num;\n} BITE;\n\ntypedef struct __unaligned {\n    int16_t *frame_ptr;\n    int16_t interpolation;\n    int16_t current_anim_state;\n    int32_t velocity;\n    int32_t acceleration;\n    int16_t frame_base;\n    int16_t frame_end;\n    int16_t jump_anim_num;\n    int16_t jump_frame_num;\n    int16_t num_changes;\n    int16_t change_idx;\n    int16_t num_commands;\n    int16_t command_idx;\n} ANIM;\n\ntypedef struct {\n    int16_t goal_anim_state;\n    int16_t num_ranges;\n    int16_t range_idx;\n} ANIM_CHANGE;\n\ntypedef struct {\n    int16_t start_frame;\n    int16_t end_frame;\n    int16_t link_anim_num;\n    int16_t link_frame_num;\n} ANIM_RANGE;\n\ntypedef struct __unaligned {\n    int16_t room;\n    XYZ_16 normal;\n    XYZ_16 vertex[4];\n} PORTAL;\n\ntypedef struct __unaligned {\n    int16_t count;\n    PORTAL portal[];\n} PORTALS;\n\ntypedef struct __unaligned {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n    int16_t intensity_1;\n    int16_t intensity_2;\n    int32_t falloff_1;\n    int32_t falloff_2;\n} LIGHT;\n\ntypedef struct __unaligned {\n    XYZ_16 pos;\n    struct __unaligned {\n        int16_t y;\n    } rot;\n    int16_t shade_1;\n    int16_t shade_2;\n    int16_t static_num;\n} MESH;\n\ntypedef enum {\n    RF_UNDERWATER  = 0x01,\n    RF_OUTSIDE     = 0x08,\n    RF_DYNAMIC_LIT = 0x10,\n    RF_NOT_INSIDE  = 0x20,\n    RF_INSIDE      = 0x40,\n} ROOM_FLAG;\n\ntypedef struct __unaligned {\n    SECTOR *sector;\n    SECTOR old_sector;\n    int16_t block;\n} DOORPOS_DATA;\n\ntypedef struct __unaligned {\n    DOORPOS_DATA d1;\n    DOORPOS_DATA d1flip;\n    DOORPOS_DATA d2;\n    DOORPOS_DATA d2flip;\n} DOOR_DATA;\n\ntypedef struct __unaligned {\n    int16_t *data;\n    PORTALS *portals;\n    SECTOR *sectors;\n    LIGHT *lights;\n    MESH *meshes;\n    XYZ_32 pos;\n    int32_t min_floor;\n    int32_t max_ceiling;\n    struct __unaligned {\n        int16_t z;\n        int16_t x;\n    } size;\n    int16_t ambient_1;\n    int16_t ambient_2;\n    int16_t light_mode;\n    int16_t num_lights;\n    int16_t num_meshes;\n    int16_t bound_left;\n    int16_t bound_right;\n    int16_t bound_top;\n    int16_t bound_bottom;\n    uint16_t bound_active;\n    int16_t test_left;\n    int16_t test_right;\n    int16_t test_top;\n    int16_t test_bottom;\n    int16_t item_num;\n    int16_t effect_num;\n    int16_t flipped_room;\n    uint16_t flags;\n} ROOM;\n\ntypedef struct __unaligned {\n    int16_t head_rotation;\n    int16_t neck_rotation;\n    int16_t maximum_turn;\n    int16_t flags;\n    int16_t item_num;\n    MOOD_TYPE mood;\n    LOT_INFO lot;\n    XYZ_32 target;\n    ITEM *enemy;\n} CREATURE;\n\ntypedef enum {\n    CAM_CHASE     = 0,\n    CAM_FIXED     = 1,\n    CAM_LOOK      = 2,\n    CAM_COMBAT    = 3,\n    CAM_CINEMATIC = 4,\n    CAM_HEAVY     = 5,\n} CAMERA_TYPE;\n\ntypedef struct __unaligned {\n    union {\n        XYZ_32 pos;\n        struct {\n            int32_t x;\n            int32_t y;\n            int32_t z;\n        };\n    };\n    int16_t room_num;\n    int16_t box_num;\n} GAME_VECTOR;\n\ntypedef struct __unaligned {\n    union {\n        struct __unaligned {\n            int32_t x;\n            int32_t y;\n            int32_t z;\n        };\n        XYZ_32 pos;\n    };\n    int16_t data;\n    int16_t flags;\n} OBJECT_VECTOR;\n\ntypedef struct __unaligned {\n    uint8_t left;\n    uint8_t right;\n    uint8_t top;\n    uint8_t bottom;\n    int16_t height;\n    int16_t overlap_index;\n} BOX_INFO;\n\ntypedef enum {\n    LV_GYM   = 0,\n    LV_FIRST = 1,\n} LEVEL_TYPE;\n\ntypedef enum {\n    RT_MAIN   = 0,\n    RT_OPTION = 1,\n    RT_KEYS   = 2,\n} RING_TYPE;\n\ntypedef enum {\n    INV_COLOR_BLACK      = 0,\n    INV_COLOR_GRAY       = 1,\n    INV_COLOR_WHITE      = 2,\n    INV_COLOR_RED        = 3,\n    INV_COLOR_ORANGE     = 4,\n    INV_COLOR_YELLOW     = 5,\n    INV_COLOR_DARK_GREEN = 12,\n    INV_COLOR_GREEN      = 13,\n    INV_COLOR_CYAN       = 14,\n    INV_COLOR_BLUE       = 15,\n    INV_COLOR_MAGENTA    = 16,\n    INV_COLOR_NUMBER_OF  = 17,\n} INV_COLOR;\n\ntypedef enum {\n    INV_GAME_MODE  = 0,\n    INV_TITLE_MODE = 1,\n    INV_KEYS_MODE  = 2,\n    INV_SAVE_MODE  = 3,\n    INV_LOAD_MODE  = 4,\n    INV_DEATH_MODE = 5,\n} INVENTORY_MODE;\n\ntypedef enum {\n    TRAP_SET      = 0,\n    TRAP_ACTIVATE = 1,\n    TRAP_WORKING  = 2,\n    TRAP_FINISHED = 3,\n} TRAP_ANIM;\n\ntypedef enum {\n    DOOR_STATE_CLOSED = 0,\n    DOOR_STATE_OPEN   = 1,\n} DOOR_STATE;\n\ntypedef enum {\n    GFD_START_GAME       = 0x0000,\n    GFD_START_SAVED_GAME = 0x0100,\n    GFD_START_CINE       = 0x0200,\n    GFD_START_FMV        = 0x0300,\n    GFD_START_DEMO       = 0x0400,\n    GFD_EXIT_TO_TITLE    = 0x0500,\n    GFD_LEVEL_COMPLETE   = 0x0600,\n    GFD_EXIT_GAME        = 0x0700,\n    GFD_EXIT_TO_OPTION   = 0x0800,\n    GFD_TITLE_DESELECT   = 0x0900,\n} GAME_FLOW_DIR;\n\ntypedef struct __unaligned {\n    int32_t first_option;\n    int32_t title_replace;\n    int32_t on_death_demo_mode;\n    int32_t on_death_in_game;\n    int32_t no_input_time;\n    int32_t on_demo_interrupt;\n    int32_t on_demo_end;\n    uint16_t reserved1[18];\n    uint16_t num_levels;\n    uint16_t num_pictures;\n    uint16_t num_titles;\n    uint16_t num_fmvs;\n    uint16_t num_cutscenes;\n    uint16_t num_demos;\n    uint16_t title_track;\n    int16_t single_level;\n    uint16_t reserved2[16];\n\n    uint16_t demo_version:              1; // 0x0001\n    uint16_t title_disabled:            1; // 0x0002\n    uint16_t cheat_mode_check_disabled: 1; // 0x0004\n    uint16_t no_input_timeout:          1; // 0x0008\n    uint16_t load_save_disabled:        1; // 0x0010\n    uint16_t screen_sizing_disabled:    1; // 0x0020\n    uint16_t lockout_option_ring:       1; // 0x0040\n    uint16_t dozy_cheat_enabled:        1; // 0x0080\n    uint16_t cyphered_strings:          1; // 0x0100\n    uint16_t gym_enabled:               1; // 0x0200\n    uint16_t play_any_level:            1; // 0x0400\n    uint16_t cheat_enable:              1; // 0x0800\n\n    uint16_t reserved3[3];\n    uint8_t cypher_code;\n    uint8_t language;\n    uint8_t secret_track;\n    uint8_t level_complete_track;\n    uint16_t reserved4[2];\n} GAME_FLOW;\n\ntypedef struct __unaligned {\n    int16_t mesh_count;\n    int16_t mesh_idx;\n    int32_t bone_idx;\n    int16_t *frame_base; // TODO: make me FRAME_INFO\n\n    void (*initialise_func)(int16_t item_num);\n    void (*control_func)(int16_t item_num);\n    void (*floor_height_func)(\n        const ITEM *item, int32_t x, int32_t y, int32_t z,\n        int32_t *out_height);\n    void (*ceiling_height_func)(\n        const ITEM *item, int32_t x, int32_t y, int32_t z,\n        int32_t *out_height);\n    void (*draw_func)(const ITEM *item);\n    void (*collision_func)(int16_t\n        item_num, ITEM *lara_item, COLL_INFO *coll);\n\n    int16_t anim_idx;\n    int16_t hit_points;\n    int16_t pivot_length;\n    int16_t radius;\n    int16_t shadow_size;\n\n    union {\n        uint16_t flags;\n        struct {\n            uint16_t loaded:           1; // 0x01 1\n            uint16_t intelligent:      1; // 0x02 2\n            uint16_t save_position:    1; // 0x04 4\n            uint16_t save_hitpoints:   1; // 0x08 8\n            uint16_t save_flags:       1; // 0x10 16\n            uint16_t save_anim:        1; // 0x20 32\n            uint16_t semi_transparent: 1; // 0x40 64\n            uint16_t water_creature:   1; // 0x80 128\n            uint16_t pad : 8;\n        };\n    };\n} OBJECT;\n\ntypedef struct __unaligned {\n    GAME_VECTOR pos;\n    GAME_VECTOR target;\n    CAMERA_TYPE type;\n    int32_t shift;\n    uint32_t flags;\n    int32_t fixed_camera;\n    int32_t num_frames;\n    int32_t bounce;\n    int32_t underwater;\n    int32_t target_distance;\n    int32_t target_square;\n    int16_t target_angle;\n    int16_t actual_angle;\n    int16_t target_elevation;\n    int16_t box;\n    int16_t num;\n    int16_t last;\n    int16_t timer;\n    int16_t speed;\n    ITEM *item;\n    ITEM *last_item;\n    OBJECT_VECTOR *fixed;\n    int32_t is_lara_mic; // TODO: remove this - now stored in g_Config\n    XYZ_32 mic_pos;\n} CAMERA_INFO;\n\ntypedef struct __unaligned {\n    int16_t *frame_base;\n    int16_t frame_num;\n    int16_t anim_num;\n    int16_t lock;\n    struct __unaligned {\n        int16_t y;\n        int16_t x;\n        int16_t z;\n    } rot; // TODO: XYZ_16\n    int16_t flash_gun;\n} LARA_ARM;\n\ntypedef struct __unaligned {\n    int32_t ammo;\n} AMMO_INFO;\n\ntypedef enum {\n    LWS_ABOVE_WATER = 0,\n    LWS_UNDERWATER = 1,\n    LWS_SURFACE = 2,\n    LWS_CHEAT = 3,\n    LWS_WADE = 4,\n} LARA_WATER_STATE;\n\ntypedef struct __unaligned {\n    int16_t item_num;\n    int16_t gun_status;\n    int16_t gun_type;\n    int16_t request_gun_type;\n    int16_t last_gun_type;\n    int16_t calc_fall_speed;\n    int16_t water_status;\n    int16_t climb_status;\n    int16_t pose_count;\n    int16_t hit_frame;\n    int16_t hit_direction;\n    int16_t air;\n    int16_t dive_count;\n    int16_t death_timer;\n    int16_t current_active;\n    int16_t spaz_effect_count;\n    int16_t flare_age;\n    int16_t skidoo;\n    int16_t weapon_item;\n    int16_t back_gun;\n    int16_t flare_frame;\n    union {\n        uint16_t flags;\n        struct ___unaligned {\n            uint16_t flare_control_left:  1; // 0x01 1\n            uint16_t flare_control_right: 1; // 0x02 2\n            uint16_t extra_anim:          1; // 0x04 4\n            uint16_t look:                1; // 0x08 8\n            uint16_t burn:                1; // 0x10 16\n            uint16_t pad:                 11;\n        };\n    };\n    int32_t water_surface_dist;\n    XYZ_32 last_pos;\n    EFFECT *spaz_effect;\n    uint32_t mesh_effects;\n    int16_t *mesh_ptrs[15];\n    ITEM *target;\n    int16_t target_angles[2];\n    int16_t turn_rate;\n    int16_t move_angle;\n    int16_t head_y_rot;\n    int16_t head_x_rot;\n    int16_t head_z_rot;\n    int16_t torso_y_rot;\n    int16_t torso_x_rot;\n    int16_t torso_z_rot;\n    LARA_ARM left_arm;\n    LARA_ARM right_arm;\n    AMMO_INFO pistol_ammo;\n    AMMO_INFO magnum_ammo;\n    AMMO_INFO uzi_ammo;\n    AMMO_INFO shotgun_ammo;\n    AMMO_INFO harpoon_ammo;\n    AMMO_INFO grenade_ammo;\n    AMMO_INFO m16_ammo;\n    CREATURE *creature;\n} LARA_INFO;\n\ntypedef enum {\n    SFX_LARA_FEET = 0,\n    SFX_LARA_CLIMB_2 = 1,\n    SFX_LARA_NO = 2,\n    SFX_LARA_SLIPPING = 3,\n    SFX_LARA_LAND = 4,\n    SFX_LARA_CLIMB_1 = 5,\n    SFX_LARA_DRAW = 6,\n    SFX_LARA_HOLSTER = 7,\n    SFX_LARA_FIRE = 8,\n    SFX_LARA_RELOAD = 9,\n    SFX_LARA_RICOCHET = 10,\n    SFX_LARA_FLARE_IGNITE = 11,\n    SFX_LARA_FLARE_BURN = 12,\n    SFX_LARA_HARPOON_FIRE = 15,\n    SFX_LARA_HARPOON_LOAD = 16,\n    SFX_LARA_WET_FEET = 17,\n    SFX_LARA_WADE = 18,\n    SFX_LARA_TREAD = 20,\n    SFX_LARA_FIRE_MAGNUMS = 21,\n    SFX_LARA_HARPOON_LOAD_WATER = 22,\n    SFX_LARA_HARPOON_FIRE_WATER = 23,\n    SFX_MASSIVE_CRASH = 24,\n    SFX_PUSH_SWITCH = 25,\n    SFX_LARA_CLIMB_3 = 26,\n    SFX_LARA_BODYSL = 27,\n    SFX_LARA_SHIMMY = 28,\n    SFX_LARA_JUMP = 29,\n    SFX_LARA_FALL = 30,\n    SFX_LARA_INJURY = 31,\n    SFX_LARA_ROLL = 32,\n    SFX_LARA_SPLASH = 33,\n    SFX_LARA_GETOUT = 34,\n    SFX_LARA_SWIM = 35,\n    SFX_LARA_BREATH = 36,\n    SFX_LARA_BUBBLES = 37,\n    SFX_LARA_SWITCH = 38,\n    SFX_LARA_KEY = 39,\n    SFX_LARA_OBJECT = 40,\n    SFX_LARA_GENERAL_DEATH = 41,\n    SFX_LARA_KNEES_DEATH = 42,\n    SFX_LARA_UZI_FIRE = 43,\n    SFX_LARA_UZI_STOP = 44,\n    SFX_LARA_SHOTGUN = 45,\n    SFX_LARA_BLOCK_PUSH_1 = 46,\n    SFX_LARA_BLOCK_PUSH_2 = 47,\n    SFX_CLICK = 48,\n    SFX_LARA_HIT = 49,\n    SFX_LARA_BULLETHIT = 50,\n    SFX_LARA_BLKPULL = 51,\n    SFX_LARA_FLOATING = 52,\n    SFX_LARA_FALLDETH = 53,\n    SFX_LARA_GRABHAND = 54,\n    SFX_LARA_GRABBODY = 55,\n    SFX_LARA_GRABFEET = 56,\n    SFX_LARA_SWITCHUP = 57,\n    SFX_GLASS_BREAK = 58,\n    SFX_WATER_LOOP = 59,\n    SFX_UNDERWATER = 60,\n    SFX_UNDERWATER_SWITCH = 61,\n    SFX_LARA_PICKUP = 62,\n    SFX_BLOCK_SOUND = 63,\n    SFX_DOOR = 64,\n    SFX_SWING = 65,\n    SFX_ROCK_FALL_CRUMBLE = 66,\n    SFX_ROCK_FALL_LAND = 67,\n    SFX_ROCK_FALL_SOLID = 68,\n    SFX_ENEMY_FEET = 69,\n    SFX_ENEMY_GRUNT = 70,\n    SFX_ENEMY_HIT_1 = 71,\n    SFX_ENEMY_HIT_2 = 72,\n    SFX_ENEMY_DEATH_1 = 73,\n    SFX_ENEMY_JUMP = 74,\n    SFX_ENEMY_CLIMBUP = 75,\n    SFX_ENEMY_CLIMBDOWN = 76,\n    SFX_WEAPON_CLATTER = 77,\n    SFX_M16_FIRE = 78,\n    SFX_WATERFALL_LOOP = 79,\n    SFX_SWORD_STATUE_DROP = 80,\n    SFX_SWORD_STATUE_LIFT = 81,\n    SFX_PORTCULLIS_UP = 82,\n    SFX_PORTCULLIS_DOWN = 83,\n    SFX_DOG_FEET_1 = 84,\n    SFX_BODY_SLAM = 85,\n    SFX_DOG_BARK_1 = 86,\n    SFX_DOG_FEET_2 = 87,\n    SFX_DOG_BARK_2 = 88,\n    SFX_DOG_DEATH = 89,\n    SFX_DOG_PANT = 90,\n    SFX_LEOPARD_FEET = 91,\n    SFX_LEOPARD_ROAR = 92,\n    SFX_LEOPARD_BITE = 93,\n    SFX_LEOPARD_STRIKE = 94,\n    SFX_LEOPARD_DEATH = 95,\n    SFX_LEOPARD_GROWL = 96,\n    SFX_RAT_ATTACK = 97,\n    SFX_RAT_DEATH = 98,\n    SFX_TIGER_ROAR = 99,\n    SFX_TIGER_BITE = 100,\n    SFX_TIGER_STRIKE = 101,\n    SFX_TIGER_DEATH = 102,\n    SFX_TIGER_GROWL = 103,\n    SFX_M16_STOP = 104,\n    SFX_EXPLOSION_1 = 105,\n    SFX_GROWL = 106,\n    SFX_SPIDER_JUMP = 107,\n    SFX_MENU_ROTATE = 108,\n    SFX_MENU_LARA_HOME = 109,\n    SFX_MENU_SPININ = 111,\n    SFX_MENU_SPINOUT = 112,\n    SFX_MENU_STOPWATCH = 113,\n    SFX_MENU_GUNS = 114,\n    SFX_MENU_PASSPORT = 115,\n    SFX_MENU_MEDI = 116,\n    SFX_ENEMY_HEELS = 117,\n    SFX_ENEMY_FIRE_SILENCER = 118,\n    SFX_ENEMY_AH_DYING = 119,\n    SFX_ENEMY_OOH_DYING = 120,\n    SFX_ENEMY_THUMP = 121,\n    SFX_SPIDER_MOVING = 122,\n    SFX_LARA_MINI_LOAD = 123,\n    SFX_LARA_MINI_LOCK = 124,\n    SFX_LARA_MINI_FIRE = 125,\n    SFX_SPIDER_BITE = 126,\n    SFX_SLAM_DOOR_SLIDE = 127,\n    SFX_SLAM_DOOR_CLOSE = 128,\n    SFX_EAGLE_SQUAWK = 129,\n    SFX_EAGLE_WING_FLAP = 130,\n    SFX_EAGLE_DEATH = 131,\n    SFX_CROW_CAW = 132,\n    SFX_CROW_WING_FLAP = 133,\n    SFX_CROW_DEATH = 134,\n    SFX_CROW_ATTACK = 135,\n    SFX_ENEMY_GUN_COCKING = 136,\n    SFX_ENEMY_FIRE_1 = 137,\n    SFX_ENEMY_FIRE_TWIRL = 138,\n    SFX_ENEMY_HOLSTER = 139,\n    SFX_ENEMY_BREATH_1 = 140,\n    SFX_ENEMY_CHUCKLE = 141,\n    SFX_MONK_POY = 142,\n    SFX_MONK_DEATH = 143,\n    SFX_LARA_SPIKE_DEATH = 145,\n    SFX_LARA_DEATH_3 = 146,\n    SFX_ROLLING_BALL = 147,\n    SFX_SANDBAG_SNAP = 148,\n    SFX_SANDBAG_HIT = 149,\n    SFX_LOOP_FOR_SMALL_FIRES = 150,\n    SFX_SKIDOO_START = 152,\n    SFX_SKIDOO_IDLE = 153,\n    SFX_SKIDOO_ACCELERATE = 154,\n    SFX_SKIDOO_MOVING = 155,\n    SFX_SKIDOO_STOP = 156,\n    SFX_ENEMY_FIRE_2 = 157,\n    SFX_ENEMY_DEATH_2 = 158,\n    SFX_ENEMY_BREATH_2 = 159,\n    SFX_STICK_TAP = 160,\n    SFX_TRAPDOOR_OPEN = 161,\n    SFX_TRAPDOOR_CLOSE = 162,\n    SFX_YETI_GROWL = 163,\n    SFX_YETI_CHEST_BEAT = 164,\n    SFX_YETI_THUMP = 165,\n    SFX_YETI_GRUNT_1 = 166,\n    SFX_YETI_SCREAM = 167,\n    SFX_YETI_DEATH = 168,\n    SFX_YETI_GROWL_1 = 169,\n    SFX_YETI_GROWL_2 = 170,\n    SFX_YETI_GRUNT_2 = 171,\n    SFX_YETI_GROWL_3 = 172,\n    SFX_YETI_FEET = 173,\n    SFX_ENEMY_HEAVY_BREATH = 174,\n    SFX_ENEMY_FLAMETHROWER_FIRE = 175,\n    SFX_ENEMY_FLAMETHROWER_SCRAPE = 176,\n    SFX_ENEMY_FLAMETHROWER_CLICK = 177,\n    SFX_ENEMY_FLAMETHROWER_DEATH = 178,\n    SFX_ENEMY_FLAMETHROWER_FALL = 179,\n    SFX_ENEMY_BELT_JINGLE = 180,\n    SFX_ENEMY_WRENCH = 181,\n    SFX_FOOTSTEP = 182,\n    SFX_FOOTSTEP_HIT = 183,\n    SFX_ENEMY_COCKING_SHOTGUN = 184,\n    SFX_SCUBA_DIVER_FLIPPER = 186,\n    SFX_SCUBA_DIVER_BREATH = 188,\n    SFX_PULLEY_CRANE = 190,\n    SFX_CURTAIN = 191,\n    SFX_SCUBA_DIVER_DEATH = 192,\n    SFX_SCUBA_DIVER_DIVING = 193,\n    SFX_BOAT_START = 194,\n    SFX_BOAT_IDLE = 195,\n    SFX_BOAT_ACCELERATE = 196,\n    SFX_BOAT_MOVING = 197,\n    SFX_BOAT_STOP = 198,\n    SFX_BOAT_SLOW_DOWN = 199,\n    SFX_BOAT_HIT = 200,\n    SFX_CLATTER_1 = 201,\n    SFX_CLATTER_2 = 202,\n    SFX_CLATTER_3 = 203,\n    SFX_DOOR_SLIDE = 204,\n    SFX_LARA_FLESH_WOUND = 205,\n    SFX_SAW_REVVING = 206,\n    SFX_SAW_STOP = 207,\n    SFX_DOOR_CHIME = 208,\n    SFX_CHAIN_CREAK_SNAP = 209,\n    SFX_SWINGING = 210,\n    SFX_BREAKING_1 = 211,\n    SFX_PULLEY_MOVE = 212,\n    SFX_AIRPLANE_IDLE = 213,\n    SFX_UNDERWATER_FAN_ON = 215,\n    SFX_SMALL_FAN_ON = 217,\n    SFX_SWINGING_BOX_BAG = 218,\n    SFX_JUMP_PAD_UP = 219,\n    SFX_JUMP_PAD_DOWN = 220,\n    SFX_BREAKING_2 = 221,\n    SFX_SNOWBALL_ROLL = 222,\n    SFX_SNOWBALL_STOP = 223,\n    SFX_ROLLING = 224,\n    SFX_ROLLING_STOP_1 = 225,\n    SFX_ROLLING_STOP_2 = 226,\n    SFX_ROLLING_2 = 227,\n    SFX_ROLLING_2_HIT = 228,\n    SFX_SIDE_BLADE_SWING = 229,\n    SFX_SIDE_BLADE_BACK = 230,\n    SFX_ROLLING_BLADE = 231,\n    SFX_ICILE_DETACH = 232,\n    SFX_ICICLE_HIT = 233,\n    SFX_ROTATING_HANDLE_LOOSE = 234,\n    SFX_ROTATING_HANDLE_TURN = 235,\n    SFX_ROTATING_HANDLE_OPEN = 236,\n    SFX_ROTATING_HANDLE_CREAK = 237,\n    SFX_MONK_FEET = 238,\n    SFX_MONK_SWORD_SWING_1 = 239,\n    SFX_MONK_SWORD_SWING_2 = 240,\n    SFX_MONK_SHOUT_1 = 241,\n    SFX_MONK_SHOUT_2 = 242,\n    SFX_MONK_SHOUT_3 = 243,\n    SFX_MONK_SHOUT_4 = 244,\n    SFX_MONK_CRUNCH = 245,\n    SFX_MONK_BREATH = 246,\n    SFX_SPLASH_SURFACE = 247,\n    SFX_WATERFALL_1 = 248,\n    SFX_ENEMY_FEET_SNOW = 249,\n    SFX_ENEMY_FIRE_3 = 250,\n    SFX_ENEMY_FIRE_SEMIAUTO = 251,\n    SFX_ENEMY_DEATH_3 = 252,\n    SFX_ENEMY_DEATH_4 = 253,\n    SFX_CIRCLE_BLADE = 254,\n    SFX_KNIFETHROWER_FEET = 255,\n    SFX_MONK_OYE = 256,\n    SFX_MONK_AWEH = 257,\n    SFX_CIRCLE_BLADE_HIT = 258,\n    SFX_KNIFETHROWER_WARRIOR_FEET = 259,\n    SFX_WARRIOR_BLADE_SWING_1 = 260,\n    SFX_WARRIOR_BLADE_SWING_2 = 261,\n    SFX_WARRIOR_GROWL = 262,\n    SFX_KNIFETHROWER_HICCUP = 263,\n    SFX_WARROPR_BURP = 264,\n    SFX_WARRIOR_GROWL_1 = 265,\n    SFX_WARRIOR_WAKE = 267,\n    SFX_WARRIOR_GROWL_2 = 268,\n    SFX_SMALL_SWITCH = 269,\n    SFX_CHAIN_PULLEY = 278,\n    SFX_ZIPLINE_GRAB = 279,\n    SFX_ZIPLINE_GO = 280,\n    SFX_ZIPLINE_STOP = 281,\n    SFX_BODY_SLUMP = 282,\n    SFX_BOWL_TIPPING = 283,\n    SFX_BOWL_POUR = 284,\n    SFX_WATERFALL_2 = 285,\n    SFX_ELEVATOR_OPEN = 286,\n    SFX_ELEVATOR_CLOSE = 287,\n    SFX_MINISUB_CLATTER_1 = 288,\n    SFX_MINISUB_CLATTER_2 = 289,\n    SFX_MINISUB_CLATTER_3 = 290,\n    SFX_BIRD_MONSTER_SCREAM = 291,\n    SFX_BIRD_MONSTER_GASP = 292,\n    SFX_BIRD_MONSTER_BREATH = 293,\n    SFX_BIRD_MONSTER_FEET = 294,\n    SFX_BIRD_MONSTER_DEATH = 295,\n    SFX_BIRD_MONSTER_SCRAPE = 296,\n    SFX_HELICOPTER_LOOP = 297,\n    SFX_DRAGON_FEET = 298,\n    SFX_DRAGON_GROWL_1 = 299,\n    SFX_DRAGON_GROWL_2 = 300,\n    SFX_DRAGON_FALL = 301,\n    SFX_DRAGON_BREATH = 302,\n    SFX_DRAGON_GROWL_3 = 303,\n    SFX_DRAGON_GRUNT = 304,\n    SFX_DRAGON_FIRE = 305,\n    SFX_DRAGON_LEG_LIFT = 306,\n    SFX_DRAGON_LEG_HIT = 307,\n    SFX_WARRIOR_BLADE_SWING_3 = 308,\n    SFX_WARRIOR_BLADE_SWING_FAST = 309,\n    SFX_WARRIOR_BREATH_ACTIVE = 311,\n    SFX_WARRIOR_HOVER = 312,\n    SFX_WARRIOR_LANDING = 313,\n    SFX_WARRIOR_SWORD_CLANK = 314,\n    SFX_WARRIOR_SWORD_SLICE = 315,\n    SFX_BIRDS_CHIRP = 316,\n    SFX_CRUNCH_1 = 317,\n    SFX_CRUNCH_2 = 318,\n    SFX_DOOR_CREAK = 319,\n    SFX_BREAKING_3 = 320,\n    SFX_BIG_SPIDER_SNARL = 321,\n    SFX_BIG_SPIDER_FEET = 322,\n    SFX_BIG_SPIDER_DEATH = 323,\n    SFX_T_REX_ROAR = 324,\n    SFX_T_REX_FEET = 325,\n    SFX_T_REX_GROWL_1 = 326,\n    SFX_T_REX_DEATH = 327,\n    SFX_DRIPS_REVERB = 329,\n    SFX_STAGE_BACKDROP = 330,\n    SFX_STONE_DOOR_SLIDE = 331,\n    SFX_PLATFORM_ALARM = 332,\n    SFX_TICK_TOCK = 333,\n    SFX_DOORBELL = 334,\n    SFX_BURGLAR_ALARM = 335,\n    SFX_BOAT_ENGINE = 336,\n    SFX_BOAT_INTO_WATER = 337,\n    SFX_UNKNOWN_1 = 338,\n    SFX_UNKNOWN_2 = 339,\n    SFX_UNKNOWN_3 = 340,\n    SFX_MARCO_BARTOLLI_TRANSFORM = 341,\n    SFX_WINSTON_SHUFFLE = 342,\n    SFX_WINSTON_FEET = 343,\n    SFX_WINSTON_GRUNT_1 = 344,\n    SFX_WINSTON_GRUNT_2 = 345,\n    SFX_WINSTON_GRUNT_3 = 346,\n    SFX_WINSTON_CUPS = 347,\n    SFX_BRITTLE_GROUND_BREAK = 348,\n    SFX_SPIDER_EXPLODE = 349,\n    SFX_SHARK_BITE = 350,\n    SFX_LAVA_BUBBLES = 351,\n    SFX_EXPLOSION_2 = 352,\n    SFX_BURGLARS = 353,\n    SFX_ZIPPER = 354,\n    SFX_NUMBER_OF = 370,\n} SOUND_EFFECT_ID;\n\ntypedef enum {\n    SPM_NORMAL     = 0,\n    SPM_UNDERWATER = 1,\n    SPM_ALWAYS     = 2,\n    SPM_PITCH      = 4,\n} SOUND_PLAY_MODE;\n\ntypedef enum {\n    CF_NORMAL        = 0,\n    CF_FOLLOW_CENTRE = 1,\n    CF_NO_CHUNKY     = 2,\n    CF_CHASE_OBJECT  = 3,\n} CAMERA_FLAGS;\n\ntypedef enum {\n    FBBOX_MIN_X = 0,\n    FBBOX_MAX_X = 1,\n    FBBOX_MIN_Y = 2,\n    FBBOX_MAX_Y = 3,\n    FBBOX_MIN_Z = 4,\n    FBBOX_MAX_Z = 5,\n    FBBOX_X     = 6,\n    FBBOX_Y     = 7,\n    FBBOX_Z     = 8,\n    FBBOX_ROT   = 9,\n} FRAME_BBOX_INFO;\n\ntypedef struct __unaligned {\n    union {\n        int32_t flags;\n        struct {\n            uint32_t matrix_pop:  1;\n            uint32_t matrix_push: 1;\n            uint32_t rot_x:       1;\n            uint32_t rot_y:       1;\n            uint32_t rot_z:       1;\n            uint32_t pad:         11;\n        };\n    };\n    XYZ_32 pos;\n} BONE;\n\ntypedef enum {\n    BF_MATRIX_POP  = 1,\n    BF_MATRIX_PUSH = 2,\n    BF_ROT_X       = 4,\n    BF_ROT_Y       = 8,\n    BF_ROT_Z       = 16,\n} BONE_FLAGS;\n\ntypedef struct __unaligned {\n    int16_t tx;\n    int16_t ty;\n    int16_t tz;\n    int16_t cx;\n    int16_t cy;\n    int16_t cz;\n    int16_t fov;\n    int16_t roll;\n} CINE_FRAME;\n\ntypedef enum {\n    IF_ONE_SHOT  = 0x0100,\n    IF_CODE_BITS = 0x3E00,\n    IF_REVERSE   = 0x4000,\n    IF_INVISIBLE = 0x0100,\n    IF_KILLED    = 0x8000,\n} ITEM_FLAG;\n\ntypedef enum {\n    IS_INACTIVE    = 0,\n    IS_ACTIVE      = 1,\n    IS_DEACTIVATED = 2,\n    IS_INVISIBLE   = 3,\n} ITEM_STATUS;\n\ntypedef struct __unaligned {\n    uint16_t key[14]; // INPUT_ROLE_NUMBER_OF\n} CONTROL_LAYOUT;\n\ntypedef enum {\n    IN_FORWARD     = 0x00000001,\n    IN_BACK        = 0x00000002,\n    IN_LEFT        = 0x00000004,\n    IN_RIGHT       = 0x00000008,\n    IN_JUMP        = 0x00000010,\n    IN_DRAW        = 0x00000020,\n    IN_ACTION      = 0x00000040,\n    IN_SLOW        = 0x00000080,\n    IN_OPTION      = 0x00000100,\n    IN_LOOK        = 0x00000200,\n    IN_STEP_LEFT   = 0x00000400,\n    IN_STEP_RIGHT  = 0x00000800,\n    IN_ROLL        = 0x00001000,\n    IN_PAUSE       = 0x00002000,\n    IN_RESERVED1   = 0x00004000,\n    IN_RESERVED2   = 0x00008000,\n    IN_DOZY_CHEAT  = 0x00010000,\n    IN_STUFF_CHEAT = 0x00020000,\n    IN_DEBUG_INFO  = 0x00040000,\n    IN_FLARE       = 0x00080000,\n    IN_SELECT      = 0x00100000,\n    IN_DESELECT    = 0x00200000,\n    IN_SAVE        = 0x00400000,\n    IN_LOAD        = 0x00800000,\n} INPUT_STATE;\n\ntypedef enum {\n    LA_RUN                                   = 0,\n    LA_WALK_FORWARD                          = 1,\n    LA_WALK_STOP_RIGHT                       = 2,\n    LA_WALK_STOP_LEFT                        = 3,\n    LA_WALK_TO_RUN_RIGHT                     = 4,\n    LA_WALK_TO_RUN_LEFT                      = 5,\n    LA_RUN_START                             = 6,\n    LA_RUN_TO_WALK_RIGHT                     = 7,\n    LA_RUN_TO_STAND_LEFT                     = 8,\n    LA_RUN_TO_WALK_LEFT                      = 9,\n    LA_RUN_TO_STAND_RIGHT                    = 10,\n    LA_STAND_STILL                           = 11,\n    LA_TURN_RIGHT_SLOW                       = 12,\n    LA_TURN_LEFT_SLOW                        = 13,\n    LA_JUMP_FORWARD_LAND_START               = 14,\n    LA_JUMP_FORWARD_LAND_END_UNUSED          = 15,\n    LA_RUN_JUMP_RIGHT_START                  = 16,\n    LA_RUN_JUMP_RIGHT_CONTINUE               = 17,\n    LA_RUN_JUMP_LEFT_START                   = 18,\n    LA_RUN_JUMP_LEFT_CONTINUE                = 19,\n    LA_WALK_FORWARD_START                    = 20,\n    LA_WALK_FORWARD_START_CONTINUE           = 21,\n    LA_JUMP_FORWARD_TO_FREEFALL              = 22,\n    LA_FREEFALL                              = 23,\n    LA_FREEFALL_LAND                         = 24,\n    LA_FREEFALL_LAND_DEATH                   = 25,\n    LA_STAND_TO_JUMP_UP                      = 26,\n    LA_STAND_TO_JUMP_UP_CONTINUE             = 27,\n    LA_JUMP_UP                               = 28,\n    LA_JUMP_UP_TO_HANG                       = 29,\n    LA_JUMP_UP_TO_FREEFALL                   = 30,\n    LA_JUMP_UP_LAND                          = 31,\n    LA_SMASH_JUMP                            = 32,\n    LA_SMASH_JUMP_CONTINUE                   = 33,\n    LA_FALL_START                            = 34,\n    LA_FALL                                  = 35,\n    LA_FALL_TO_FREEFALL                      = 36,\n    LA_HANG_TO_FREEFALL                      = 37,\n    LA_WALK_BACK_END_RIGHT                   = 38,\n    LA_WALK_BACK_END_LEFT                    = 39,\n    LA_WALK_BACK                             = 40,\n    LA_WALK_BACK_START                       = 41,\n    LA_CLIMB_3CLICK                          = 42,\n    LA_CLIMB_3CLICK_END_TO_RUN               = 43,\n    LA_TURN_RIGHT                            = 44,\n    LA_JUMP_FORWARD_TO_FREEFALL_2            = 45,\n    LA_REACH_TO_FREEFALL                     = 46,\n    LA_ROLL_ALTERNATE                        = 47,\n    LA_ROLL_END_ALTERNATE                    = 48,\n    LA_JUMP_FORWARD_END_TO_FREEFALL          = 49,\n    LA_CLIMB_2CLICK                          = 50,\n    LA_CLIMB_2CLICK_END                      = 51,\n    LA_CLIMB_2CLICK_END_TO_RUN               = 52,\n    LA_WALL_SMASH_LEFT                       = 53,\n    LA_WALL_SMASH_RIGHT                      = 54,\n    LA_RUN_UP_STEP_RIGHT                     = 55,\n    LA_RUN_UP_STEP_LEFT                      = 56,\n    LA_WALK_UP_STEP_RIGHT                    = 57,\n    LA_WALK_UP_STEP_LEFT                     = 58,\n    LA_WALK_DOWN_LEFT                        = 59,\n    LA_WALK_DOWN_RIGHT                       = 60,\n    LA_WALK_DOWN_BACK_LEFT                   = 61,\n    LA_WALK_DOWN_BACK_RIGHT                  = 62,\n    LA_WALL_SWITCH_DOWN                      = 63,\n    LA_WALL_SWITCH_UP                        = 64,\n    LA_SIDESTEP_LEFT                         = 65,\n    LA_SIDESTEP_LEFT_END                     = 66,\n    LA_SIDESTEP_RIGHT                        = 67,\n    LA_SIDESTEP_RIGHT_END                    = 68,\n    LA_ROTATE_LEFT                           = 69,\n    LA_SLIDE_FORWARD                         = 70,\n    LA_SLIDE_FORWARD_END                     = 71,\n    LA_SLIDE_FORWARD_STOP                    = 72,\n    LA_STAND_TO_JUMP                         = 73,\n    LA_JUMP_BACK_START                       = 74,\n    LA_JUMP_BACK                             = 75,\n    LA_JUMP_FORWARD_START                    = 76,\n    LA_JUMP_FORWARD                          = 77,\n    LA_JUMP_LEFT_START                       = 78,\n    LA_JUMP_LEFT                             = 79,\n    LA_JUMP_RIGHT_START                      = 80,\n    LA_JUMP_RIGHT                            = 81,\n    LA_LAND                                  = 82,\n    LA_JUMP_BACK_TO_FREEFALL                 = 83,\n    LA_JUMP_LEFT_TO_FREEFALL                 = 84,\n    LA_JUMP_RIGHT_TO_FREEFALL                = 85,\n    LA_UNDERWATER_SWIM_FORWARD               = 86,\n    LA_UNDERWATER_SWIM_FORWARD_DRIFT         = 87,\n    LA_SMALL_JUMP_BACK_START                 = 88,\n    LA_SMALL_JUMP_BACK                       = 89,\n    LA_SMALL_JUMP_BACK_END                   = 90,\n    LA_JUMP_UP_START                         = 91,\n    LA_LAND_TO_RUN                           = 92,\n    LA_FALL_BACK                             = 93,\n    LA_JUMP_FORWARD_TO_REACH                 = 94,\n    LA_REACH                                 = 95,\n    LA_REACH_TO_HANG                         = 96,\n    LA_CLIMB_ON                              = 97,\n    LA_REACH_TO_FREEFALL_2                   = 98,\n    LA_FALL_CROUCHING_LANDING                = 99,\n    LA_JUMP_FORWARD_TO_REACH_LATE            = 100,\n    LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE = 101,\n    LA_CLIMB_ON_END                          = 102,\n    LA_STAND_IDLE                            = 103,\n    LA_SLIDE_BACKWARD_START                  = 104,\n    LA_SLIDE_BACKWARD                        = 105,\n    LA_SLIDE_BACKWARD_END                    = 106,\n    LA_UNDERWATER_SWIM_TO_IDLE               = 107,\n    LA_UNDERWATER_IDLE                       = 108,\n    LA_UNDERWARER_IDLE_TO_SWIM               = 109,\n    LA_ONWATER_IDLE                          = 110,\n    LA_ONWATER_TO_STAND_HIGH                 = 111,\n    LA_FREEFALL_TO_UNDERWATER                = 112,\n    LA_ONWATER_DIVE_ALTERNATE                = 113,\n    LA_UNDERWATER_TO_ONWATER                 = 114,\n    LA_ONWATER_SWIM_FORWARD_DIVE             = 115,\n    LA_ONWATER_SWIM_FORWARD                  = 116,\n    LA_ONWATER_SWIM_FORWARD_TO_IDLE          = 117,\n    LA_ONWATER_IDLE_TO_SWIM_FORWARD          = 118,\n    LA_ONWATER_DIVE                          = 119,\n    LA_PUSHABLE_GRAB                         = 120,\n    LA_PUSHABLE_RELEASE                      = 121,\n    LA_PUSHABLE_PULL                         = 122,\n    LA_PUSHABLE_PUSH                         = 123,\n    LA_UNDERWATER_DEATH                      = 124,\n    LA_HIT_FRONT                             = 125,\n    LA_HIT_BACK                              = 126,\n    LA_HIT_LEFT                              = 127,\n    LA_HIT_RIGHT                             = 128,\n    LA_UNDERWATER_SWITCH                     = 129,\n    LA_UNDERWATER_PICKUP                     = 130,\n    LA_USE_KEY                               = 131,\n    LA_ONWATER_DEATH                         = 132,\n    LA_RUN_DEATH                             = 133,\n    LA_USE_PUZZLE                            = 134,\n    LA_PICKUP                                = 135,\n    LA_SHIMMY_LEFT                           = 136,\n    LA_SHIMMY_RIGHT                          = 137,\n    LA_STAND_DEATH                           = 138,\n    LA_BOULDER_DEATH                         = 139,\n    LA_ONWATER_IDLE_TO_SWIM_BACK             = 140,\n    LA_ONWATER_SWIM_BACK                     = 141,\n    LA_ONWATER_SWIM_BACK_TO_IDLE             = 142,\n    LA_ONWATER_SWIM_LEFT                     = 143,\n    LA_ONWATER_SWIM_RIGHT                    = 144,\n    LA_DEATH_JUMP                            = 145,\n    LA_ROLL_START                            = 146,\n    LA_ROLL_CONTINUE                         = 147,\n    LA_ROLL_END                              = 148,\n    LA_SPIKE_DEATH                           = 149,\n    LA_REACH_TO_THIN_LEDGE                   = 150,\n    LA_SWANDIVE_ROLL                         = 151,\n    LA_SWANDIVE_TO_UNDERWATER                = 152,\n    LA_FREEFALL_SWANDIVE                     = 153,\n    LA_FREEFALL_SWANDIVE_TO_UNDERWATER       = 154,\n    LA_SWANDIVE_DEATH                        = 155,\n    LA_SWANDIVE_LEFT                         = 156,\n    LA_SWANDIVE_RIGHT                        = 157,\n    LA_SWANDIVE_START                        = 158,\n    LA_CLIMB_ON_HANDSTAND                    = 159,\n    LA_STAND_TO_LADDER                       = 160,\n    LA_LADDER_UP                             = 161,\n    LA_LADDER_UP_STOP_RIGHT                  = 162,\n    LA_LADDER_UP_STOP_LEFT                   = 163,\n    LA_LADDER_IDLE                           = 164,\n    LA_LADDER_UP_START                       = 165,\n    LA_LADDER_DOWN_STOP_LEFT                 = 166,\n    LA_LADDER_DOWN_STOP_RIGHT                = 167,\n    LA_LADDER_DOWN                           = 168,\n    LA_LADDER_DOWN_START                     = 169,\n    LA_LADDER_RIGHT                          = 170,\n    LA_LADDER_LEFT                           = 171,\n    LA_LADDER_HANG                           = 172,\n    LA_LADDER_HANG_TO_IDLE                   = 173,\n    LA_LADDER_CLIMB_ON                       = 174,\n    LA_UNKNOWN                               = 175,\n    LA_ONWATER_TO_WADE_SHALLOW               = 176,\n    LA_WADE                                  = 177,\n    LA_RUN_TO_WADE_LEFT                      = 178,\n    LA_RUN_TO_WADE_RIGHT                     = 179,\n    LA_WADE_TO_RUN_LEFT                      = 180,\n    LA_WADE_TO_RUN_RIGHT                     = 181,\n    LA_LADDER_BACKFLIP_START                 = 182,\n    LA_LADDER_BACKFLIP_CONTINUE              = 183,\n    LA_WADE_TO_STAND_RIGHT                   = 184,\n    LA_WADE_TO_STAND_LEFT                    = 185,\n    LA_STAND_TO_WADE                         = 186,\n    LA_LADDER_UP_HANGING                     = 187,\n    LA_LADDER_DOWN_HANGING                   = 188,\n    LA_FLARE_THROW                           = 189,\n    LA_ONWATER_TO_WADE                       = 190,\n    LA_ONWATER_TO_STAND_MEDIUM               = 191,\n    LA_UNDERWATER_TO_STAND                   = 192,\n    LA_ONWATER_TO_WADE_LOW                   = 193,\n    LA_LADDER_TO_HANG_DOWN                   = 194,\n    LA_SWITCH_SMALL_DOWN                     = 195,\n    LA_SWITCH_SMALL_UP                       = 196,\n    LA_BUTTON_PUSH                           = 197,\n    LA_UNDERWATER_SWIM_TO_STILL_HUDDLE       = 198,\n    LA_UNDERWATER_SWIM_TO_STILL_SPRAWL       = 199,\n    LA_UNDERWATER_SWIM_TO_STILL_MEDIUM       = 200,\n    LA_LADDER_TO_HANG_RIGHT                  = 201,\n    LA_LADDER_TO_HANG_LEFT                   = 202,\n    LA_UNDERWATER_ROLL_START                 = 203,\n    LA_FLARE_PICKUP                          = 204,\n    LA_UNDERWATER_ROLL_END                   = 205,\n    LA_UNDERWATER_FLARE_PICKUP               = 206,\n    LA_RUN_JUMP_ROLL_START                   = 207,\n    LA_SOMERSAULT                            = 208,\n    LA_RUN_JUMP_ROLL_END                     = 209,\n    LA_JUMP_FORWARD_ROLL_START               = 210,\n    LA_JUMP_FORWARD_ROLL_END                 = 211,\n    LA_JUMP_BACK_ROLL_START                  = 212,\n    LA_JUMP_BACK_ROLL_END                    = 213,\n    LA_KICK                                  = 214,\n    LA_ZIPLINE_GRAB                          = 215,\n    LA_ZIPLINE_RIDE                          = 216,\n    LA_ZIPLINE_FALL                          = 217,\n} LARA_ANIMATION;\n\ntypedef enum {\n    LA_EXTRA_BREATH      = 0,\n    LA_EXTRA_PLUNGER     = 1,\n    LA_EXTRA_YETI_KILL   = 2,\n    LA_EXTRA_SHARK_KILL  = 3,\n    LA_EXTRA_AIRLOCK     = 4,\n    LA_EXTRA_GONG_BONG   = 5,\n    LA_EXTRA_TREX_KILL   = 6,\n    LA_EXTRA_PULL_DAGGER = 7,\n    LA_EXTRA_START_ANIM  = 8,\n    LA_EXTRA_START_HOUSE = 9,\n    LA_EXTRA_FINAL_ANIM  = 10,\n} LARA_EXTRA_ANIMATION;\n\ntypedef enum {\n    LS_WALK         = 0,\n    LS_RUN          = 1,\n    LS_STOP         = 2,\n    LS_JUMP_FORWARD = 3,\n    LS_POSE         = 4,\n    LS_FAST_BACK    = 5,\n    LS_TURN_RIGHT   = 6,\n    LS_TURN_LEFT    = 7,\n    LS_DEATH        = 8,\n    LS_FAST_FALL    = 9,\n    LS_HANG         = 10,\n    LS_REACH        = 11,\n    LS_SPLAT        = 12,\n    LS_TREAD        = 13,\n    LS_LAND         = 14,\n    LS_COMPRESS     = 15,\n    LS_BACK         = 16,\n    LS_SWIM         = 17,\n    LS_GLIDE        = 18,\n    LS_NULL         = 19,\n    LS_FAST_TURN    = 20,\n    LS_STEP_RIGHT   = 21,\n    LS_STEP_LEFT    = 22,\n    LS_HIT          = 23,\n    LS_SLIDE        = 24,\n    LS_JUMP_BACK    = 25,\n    LS_JUMP_RIGHT   = 26,\n    LS_JUMP_LEFT    = 27,\n    LS_JUMP_UP      = 28,\n    LS_FALL_BACK    = 29,\n    LS_HANG_LEFT    = 30,\n    LS_HANG_RIGHT   = 31,\n    LS_SLIDE_BACK   = 32,\n    LS_SURF_TREAD   = 33,\n    LS_SURF_SWIM    = 34,\n    LS_DIVE         = 35,\n    LS_PUSH_BLOCK   = 36,\n    LS_PULL_BLOCK   = 37,\n    LS_PP_READY     = 38,\n    LS_PICKUP       = 39,\n    LS_SWITCH_ON    = 40,\n    LS_SWITCH_OFF   = 41,\n    LS_USE_KEY      = 42,\n    LS_USE_PUZZLE   = 43,\n    LS_UW_DEATH     = 44,\n    LS_ROLL         = 45,\n    LS_SPECIAL      = 46,\n    LS_SURF_BACK    = 47,\n    LS_SURF_LEFT    = 48,\n    LS_SURF_RIGHT   = 49,\n    LS_USE_MIDAS    = 50,\n    LS_DIE_MIDAS    = 51,\n    LS_SWAN_DIVE    = 52,\n    LS_FAST_DIVE    = 53,\n    LS_GYMNAST      = 54,\n    LS_WATER_OUT    = 55,\n    LS_CLIMB_STANCE = 56,\n    LS_CLIMBING     = 57,\n    LS_CLIMB_LEFT   = 58,\n    LS_CLIMB_END    = 59,\n    LS_CLIMB_RIGHT  = 60,\n    LS_CLIMB_DOWN   = 61,\n    LS_LARA_TEST1   = 62,\n    LS_LARA_TEST2   = 63,\n    LS_LARA_TEST3   = 64,\n    LS_WADE         = 65,\n    LS_WATER_ROLL   = 66,\n    LS_FLARE_PICKUP = 67,\n    LS_TWIST        = 68,\n    LS_KICK         = 69,\n    LS_ZIPLINE      = 70,\n} LARA_STATE;\n\ntypedef enum {\n    LGS_ARMLESS = 0,\n    LGS_HANDS_BUSY = 1,\n    LGS_DRAW = 2,\n    LGS_UNDRAW = 3,\n    LGS_READY = 4,\n    LGS_SPECIAL = 5,\n} LARA_GUN_STATE;\n\ntypedef enum {\n    LGT_UNARMED = 0,\n    LGT_PISTOLS = 1,\n    LGT_MAGNUMS = 2,\n    LGT_UZIS    = 3,\n    LGT_SHOTGUN = 4,\n    LGT_M16     = 5,\n    LGT_GRENADE = 6,\n    LGT_HARPOON = 7,\n    LGT_FLARE   = 8,\n    LGT_SKIDOO  = 9,\n    NUM_WEAPONS = 10,\n} LARA_GUN_TYPE;\n\ntypedef enum {\n    LM_HIPS      = 0,\n    LM_THIGH_L   = 1,\n    LM_CALF_L    = 2,\n    LM_FOOT_L    = 3,\n    LM_THIGH_R   = 4,\n    LM_CALF_R    = 5,\n    LM_FOOT_R    = 6,\n    LM_TORSO     = 7,\n    LM_UARM_R    = 8,\n    LM_LARM_R    = 9,\n    LM_HAND_R    = 10,\n    LM_UARM_L    = 11,\n    LM_LARM_L    = 12,\n    LM_HAND_L    = 13,\n    LM_HEAD      = 14,\n    LM_NUMBER_OF = 15,\n} LARA_MESH;\n\ntypedef enum {\n    NO_OBJECT               = -1,\n    O_LARA                  = 0,\n    O_LARA_PISTOLS          = 1,\n    O_LARA_HAIR             = 2,\n    O_LARA_SHOTGUN          = 3,\n    O_LARA_MAGNUMS          = 4,\n    O_LARA_UZIS             = 5,\n    O_LARA_M16              = 6,\n    O_LARA_GRENADE          = 7,\n    O_LARA_HARPOON          = 8,\n    O_LARA_FLARE            = 9,\n    O_LARA_SKIDOO           = 10,\n    O_LARA_BOAT             = 11,\n    O_LARA_EXTRA            = 12,\n    O_SKIDOO_FAST           = 13,\n    O_BOAT                  = 14,\n    O_DOG                   = 15,\n    O_CULT_1                = 16,\n    O_CULT_1A               = 17,\n    O_CULT_1B               = 18,\n    O_CULT_2                = 19,\n    O_CULT_3                = 20,\n    O_MOUSE                 = 21,\n    O_DRAGON_FRONT          = 22,\n    O_DRAGON_BACK           = 23,\n    O_GONDOLA               = 24,\n    O_SHARK                 = 25,\n    O_EEL                   = 26,\n    O_BIG_EEL               = 27,\n    O_BARRACUDA             = 28,\n    O_DIVER                 = 29,\n    O_WORKER_1              = 30,\n    O_WORKER_2              = 31,\n    O_WORKER_3              = 32,\n    O_WORKER_4              = 33,\n    O_WORKER_5              = 34,\n    O_JELLY                 = 35,\n    O_SPIDER                = 36,\n    O_BIG_SPIDER            = 37,\n    O_CROW                  = 38,\n    O_TIGER                 = 39,\n    O_BARTOLI               = 40,\n    O_XIAN_SPEARMAN         = 41,\n    O_XIAN_SPEARMAN_STATUE  = 42,\n    O_XIAN_KNIGHT           = 43,\n    O_XIAN_KNIGHT_STATUE    = 44,\n    O_YETI                  = 45,\n    O_BIRD_GUARDIAN         = 46,\n    O_EAGLE                 = 47,\n    O_BANDIT_1              = 48,\n    O_BANDIT_2              = 49,\n    O_BANDIT_2B             = 50,\n    O_SKIDOO_ARMED          = 51,\n    O_SKIDOO_DRIVER         = 52,\n    O_MONK_1                = 53,\n    O_MONK_2                = 54,\n    O_FALLING_BLOCK_1       = 55,\n    O_FALLING_BLOCK_2       = 56,\n    O_FALLING_BLOCK_3       = 57,\n    O_PENDULUM_1            = 58,\n    O_SPIKES                = 59,\n    O_ROLLING_BALL_1        = 60,\n    O_DART                  = 61,\n    O_DART_EMITTER          = 62,\n    O_DRAWBRIDGE            = 63,\n    O_TEETH_TRAP            = 64,\n    O_LIFT                  = 65,\n    O_GENERAL               = 66,\n    O_MOVABLE_BLOCK_1       = 67,\n    O_MOVABLE_BLOCK_2       = 68,\n    O_MOVABLE_BLOCK_3       = 69,\n    O_MOVABLE_BLOCK_4       = 70,\n    O_BIG_BOWL              = 71,\n    O_WINDOW_1              = 72,\n    O_WINDOW_2              = 73,\n    O_WINDOW_3              = 74,\n    O_WINDOW_4              = 75,\n    O_PROPELLER_1           = 76,\n    O_POWER_SAW             = 77,\n    O_HOOK                  = 78,\n    O_FALLING_CEILING       = 79,\n    O_SPINNING_BLADE        = 80,\n    O_BLADE                 = 81,\n    O_KILLER_STATUE         = 82,\n    O_ROLLING_BALL_2        = 83,\n    O_ICICLE                = 84,\n    O_SPIKE_WALL            = 85,\n    O_SPRINGBOARD           = 86,\n    O_CEILING_SPIKES        = 87,\n    O_BELL                  = 88,\n    O_WATER_SPRITE          = 89,\n    O_SNOW_SPRITE           = 90,\n    O_SKIDOO_TRACK          = 91,\n    O_SWITCH_TYPE_AIRLOCK   = 92,\n    O_SWITCH_TYPE_SMALL     = 93,\n    O_PROPELLER_2           = 94,\n    O_PROPELLER_3           = 95,\n    O_PENDULUM_2            = 96,\n    O_MESH_SWAP_1           = 97,\n    O_MESH_SWAP_2           = 98,\n    O_LARA_SWAP             = 99,\n    O_TEXT_BOX              = 100,\n    O_ROLLING_BALL_3        = 101,\n    O_ZIPLINE_HANDLE        = 102,\n    O_SWITCH_TYPE_BUTTON    = 103,\n    O_SWITCH_TYPE_NORMAL    = 104,\n    O_SWITCH_TYPE_UW        = 105,\n    O_DOOR_TYPE_1           = 106,\n    O_DOOR_TYPE_2           = 107,\n    O_DOOR_TYPE_3           = 108,\n    O_DOOR_TYPE_4           = 109,\n    O_DOOR_TYPE_5           = 110,\n    O_DOOR_TYPE_6           = 111,\n    O_DOOR_TYPE_7           = 112,\n    O_DOOR_TYPE_8           = 113,\n    O_TRAPDOOR_TYPE_1       = 114,\n    O_TRAPDOOR_TYPE_2       = 115,\n    O_TRAPDOOR_TYPE_3       = 116,\n    O_BRIDGE_FLAT           = 117,\n    O_BRIDGE_TILT_1         = 118,\n    O_BRIDGE_TILT_2         = 119,\n    O_PASSPORT_OPTION       = 120,\n    O_COMPASS_OPTION        = 121,\n    O_PHOTO_OPTION          = 122,\n    O_PLAYER_1              = 123,\n    O_PLAYER_2              = 124,\n    O_PLAYER_3              = 125,\n    O_PLAYER_4              = 126,\n    O_PLAYER_5              = 127,\n    O_PLAYER_6              = 128,\n    O_PLAYER_7              = 129,\n    O_PLAYER_8              = 130,\n    O_PLAYER_9              = 131,\n    O_PLAYER_10             = 132,\n    O_PASSPORT_CLOSED       = 133,\n    O_COMPASS_ITEM          = 134,\n    O_PISTOL_ITEM           = 135,\n    O_SHOTGUN_ITEM          = 136,\n    O_MAGNUM_ITEM           = 137,\n    O_UZI_ITEM              = 138,\n    O_HARPOON_ITEM          = 139,\n    O_M16_ITEM              = 140,\n    O_GRENADE_ITEM          = 141,\n    O_PISTOL_AMMO_ITEM      = 142,\n    O_SHOTGUN_AMMO_ITEM     = 143,\n    O_MAGNUM_AMMO_ITEM      = 144,\n    O_UZI_AMMO_ITEM         = 145,\n    O_HARPOON_AMMO_ITEM     = 146,\n    O_M16_AMMO_ITEM         = 147,\n    O_GRENADE_AMMO_ITEM     = 148,\n    O_SMALL_MEDIPACK_ITEM   = 149,\n    O_LARGE_MEDIPACK_ITEM   = 150,\n    O_FLARES_ITEM           = 151,\n    O_FLARE_ITEM            = 152,\n    O_DETAIL_OPTION         = 153,\n    O_SOUND_OPTION          = 154,\n    O_CONTROL_OPTION        = 155,\n    O_GAMMA_OPTION          = 156,\n    O_PISTOL_OPTION         = 157,\n    O_SHOTGUN_OPTION        = 158,\n    O_MAGNUM_OPTION         = 159,\n    O_UZI_OPTION            = 160,\n    O_HARPOON_OPTION        = 161,\n    O_M16_OPTION            = 162,\n    O_GRENADE_OPTION        = 163,\n    O_PISTOL_AMMO_OPTION    = 164,\n    O_SHOTGUN_AMMO_OPTION   = 165,\n    O_MAGNUM_AMMO_OPTION    = 166,\n    O_UZI_AMMO_OPTION       = 167,\n    O_HARPOON_AMMO_OPTION   = 168,\n    O_M16_AMMO_OPTION       = 169,\n    O_GRENADE_AMMO_OPTION   = 170,\n    O_SMALL_MEDIPACK_OPTION = 171,\n    O_LARGE_MEDIPACK_OPTION = 172,\n    O_FLARES_OPTION         = 173,\n    O_PUZZLE_ITEM_1         = 174,\n    O_PUZZLE_ITEM_2         = 175,\n    O_PUZZLE_ITEM_3         = 176,\n    O_PUZZLE_ITEM_4         = 177,\n    O_PUZZLE_OPTION_1       = 178,\n    O_PUZZLE_OPTION_2       = 179,\n    O_PUZZLE_OPTION_3       = 180,\n    O_PUZZLE_OPTION_4       = 181,\n    O_PUZZLE_HOLE_1         = 182,\n    O_PUZZLE_HOLE_2         = 183,\n    O_PUZZLE_HOLE_3         = 184,\n    O_PUZZLE_HOLE_4         = 185,\n    O_PUZZLE_DONE_1         = 186,\n    O_PUZZLE_DONE_2         = 187,\n    O_PUZZLE_DONE_3         = 188,\n    O_PUZZLE_DONE_4         = 189,\n    O_SECRET_1              = 190,\n    O_SECRET_2              = 191,\n    O_SECRET_3              = 192,\n    O_KEY_ITEM_1            = 193,\n    O_KEY_ITEM_2            = 194,\n    O_KEY_ITEM_3            = 195,\n    O_KEY_ITEM_4            = 196,\n    O_KEY_OPTION_1          = 197,\n    O_KEY_OPTION_2          = 198,\n    O_KEY_OPTION_3          = 199,\n    O_KEY_OPTION_4          = 200,\n    O_KEY_HOLE_1            = 201,\n    O_KEY_HOLE_2            = 202,\n    O_KEY_HOLE_3            = 203,\n    O_KEY_HOLE_4            = 204,\n    O_PICKUP_ITEM_1         = 205,\n    O_PICKUP_ITEM_2         = 206,\n    O_PICKUP_OPTION_1       = 207,\n    O_PICKUP_OPTION_2       = 208,\n    O_SPHERE_OF_DOOM_1      = 209,\n    O_SPHERE_OF_DOOM_2      = 210,\n    O_SPHERE_OF_DOOM_3      = 211,\n    O_ALARM_SOUND           = 212,\n    O_BIRD_TWEETER_1        = 213,\n    O_DINO                  = 214,\n    O_BIRD_TWEETER_2        = 215,\n    O_CLOCK_CHIMES          = 216,\n    O_DRAGON_BONES_1        = 217,\n    O_DRAGON_BONES_2        = 218,\n    O_DRAGON_BONES_3        = 219,\n    O_HOT_LIQUID            = 220,\n    O_BOAT_BITS             = 221,\n    O_MINE                  = 222,\n    O_INV_BACKGROUND        = 223,\n    O_FX_RESERVED           = 224,\n    O_GONG_BONGER           = 225,\n    O_DETONATOR_1           = 226,\n    O_DETONATOR_2           = 227,\n    O_COPTER                = 228,\n    O_EXPLOSION             = 229,\n    O_SPLASH                = 230,\n    O_BUBBLE                = 231,\n    O_BUBBLE_EMITTER        = 232,\n    O_BLOOD                 = 233,\n    O_DART_EFFECT           = 234,\n    O_FLARE_FIRE            = 235,\n    O_GLOW                  = 236,\n    O_GLOW_RESERVED         = 237,\n    O_RICOCHET              = 238,\n    O_TWINKLE               = 239,\n    O_GUN_FLASH             = 240,\n    O_M16_FLASH             = 241,\n    O_BODY_PART             = 242,\n    O_CAMERA_TARGET         = 243,\n    O_WATERFALL             = 244,\n    O_MISSILE_HARPOON       = 245,\n    O_MISSILE_FLAME         = 246,\n    O_MISSILE_KNIFE         = 247,\n    O_GRENADE               = 248,\n    O_HARPOON_BOLT          = 249,\n    O_EMBER                 = 250,\n    O_EMBER_EMITTER         = 251,\n    O_FLAME                 = 252,\n    O_FLAME_EMITTER         = 253,\n    O_SKYBOX                = 254,\n    O_ALPHABET              = 255,\n    O_DYING_MONK            = 256,\n    O_DING_DONG             = 257,\n    O_LARA_ALARM            = 258,\n    O_MINI_COPTER           = 259,\n    O_WINSTON               = 260,\n    O_ASSAULT_DIGITS        = 261,\n    O_FINAL_LEVEL_COUNTER   = 262,\n    O_CUT_SHOTGUN           = 263,\n    O_EARTHQUAKE            = 264,\n    O_NUMBER_OF             = 265,\n} GAME_OBJECT_ID;\n\ntypedef enum {\n    MX_INACTIVE                = -1,\n    MX_UNUSED_0                = 0, // 2.mp3\n    MX_UNUSED_1                = 1, // 2.mp3\n    MX_CUTSCENE_THE_GREAT_WALL = 2, // 2.mp3\n    MX_UNUSED_2                = 3, // 2.mp3\n    MX_CUTSCENE_OPERA_HOUSE    = 4, // 3.mp3\n    MX_CUTSCENE_BROTHER_CHAN   = 5, // 4.mp3\n    MX_GYM_HINT_1              = 6, // 5.mp3\n    MX_GYM_HINT_2              = 7, // 6.mp3\n    MX_GYM_HINT_3              = 8, // 7.mp3\n    MX_GYM_HINT_4              = 9, // 8.mp3\n    MX_GYM_HINT_5              = 10, // 9.mp3\n    MX_GYM_HINT_6              = 11, // 10.mp3\n    MX_GYM_HINT_7              = 12, // 11.mp3\n    MX_GYM_HINT_8              = 13, // 12.mp3\n    MX_GYM_HINT_9              = 14, // 13.mp3\n    MX_GYM_HINT_10             = 15, // 14.mp3\n    MX_GYM_HINT_11             = 16, // 15.mp3\n    MX_GYM_HINT_12             = 17, // 16.mp3\n    MX_GYM_HINT_13             = 18, // 17.mp3\n    MX_GYM_HINT_14             = 19, // 18.mp3\n    MX_UNUSED_3                = 20, // 18.mp3\n    MX_UNUSED_4                = 21, // 18.mp3\n    MX_GYM_HINT_15             = 22, // 19.mp3\n    MX_GYM_HINT_16             = 23, // 20.mp3\n    MX_GYM_HINT_17             = 24, // 21.mp3\n    MX_GYM_HINT_18             = 25, // 22.mp3\n    MX_UNUSED_5                = 26, // 23.mp3\n    MX_CUTSCENE_BATH           = 27, // 23.mp3\n    MX_DAGGER_PULL             = 28, // 24.mp3\n    MX_GYM_HINT_20             = 29, // 25.mp3\n    MX_CUTSCENE_XIAN           = 30, // 26.mp3\n    MX_CAVES_AMBIENCE          = 31, // 27.mp3\n    MX_SEWERS_AMBIENCE         = 32, // 28.mp3\n    MX_WINDY_AMBIENCE          = 33, // 29.mp3\n    MX_HEARTBEAT_AMBIENCE      = 34, // 30.mp3\n    MX_SURPRISE_1              = 35, // 31.mp3\n    MX_SURPRISE_2              = 36, // 32.mp3\n    MX_SURPRISE_3              = 37, // 33.mp3\n    MX_OOH_AAH_1               = 38, // 34.mp3\n    MX_OOH_AAH_2               = 39, // 35.mp3\n    MX_VENICE_VIOLINS          = 40, // 36.mp3\n    MX_END_OF_LEVEL            = 41, // 37.mp3\n    MX_SPOOKY_1                = 42, // 38.mp3\n    MX_SPOOKY_2                = 43, // 39.mp3\n    MX_SPOOKY_3                = 44, // 40.mp3\n    MX_HARP_THEME              = 45, // 41.mp3\n    MX_MYSTERY_1               = 46, // 42.mp3\n    MX_SECRET                  = 47, // 43.mp3\n    MX_AMBUSH_1                = 48, // 44.mp3\n    MX_AMBUSH_2                = 49, // 45.mp3\n    MX_AMBUSH_3                = 50, // 46.mp3\n    MX_AMBUSH_4                = 51, // 47.mp3\n    MX_SKIDOO_THEME            = 52, // 48.mp3\n    MX_BATTLE_THEME            = 53, // 49.mp3\n    MX_MYSTERY_2               = 54, // 50.mp3\n    MX_MYSTERY_3               = 55, // 51.mp3\n    MX_MYSTERY_4               = 56, // 52.mp3\n    MX_MYSTERY_5               = 57, // 53.mp3\n    MX_RIG_AMBIENCE            = 58, // 54.mp3\n    MX_TOMB_AMBIENCE           = 59, // 55.mp3\n    MX_OOH_AAH_3               = 60, // 56.mp3\n    MX_REVEAL_1                = 61, // 57.mp3\n    MX_CUTSCENE_RIG            = 62, // 58.mp3\n    MX_REVEAL_2                = 63, // 59.mp3\n    MX_TITLE_THEME             = 64, // 60.mp3\n    MX_UNUSED_6                = 65, // 61.mp3\n} MUSIC_TRACK_ID;\n\ntypedef enum {\n    COLL_NONE      = 0x00,\n    COLL_FRONT     = 0x01,\n    COLL_LEFT      = 0x02,\n    COLL_RIGHT     = 0x04,\n    COLL_TOP       = 0x08,\n    COLL_TOP_FRONT = 0x10,\n    COLL_CLAMP     = 0x20,\n} COLL_TYPE;\n\ntypedef enum {\n    FT_FLOOR   = 0,\n    FT_DOOR    = 1,\n    FT_TILT    = 2,\n    FT_ROOF    = 3,\n    FT_TRIGGER = 4,\n    FT_LAVA    = 5,\n    FT_CLIMB   = 6,\n} FLOOR_TYPE;\n\ntypedef enum {\n    HT_WALL        = 0,\n    HT_SMALL_SLOPE = 1,\n    HT_BIG_SLOPE   = 2,\n} HEIGHT_TYPE;\n\ntypedef enum {\n    DIR_UNKNOWN = -1,\n    DIR_NORTH   = 0,\n    DIR_EAST    = 1,\n    DIR_SOUTH   = 2,\n    DIR_WEST    = 3,\n} DIRECTION;\n\ntypedef struct __unaligned {\n    uint16_t x;\n    uint16_t y;\n} XGEN_X;\n\ntypedef struct __unaligned {\n    int32_t x1;\n    int32_t x2;\n} XBUF_X;\n\ntypedef struct __unaligned {\n    int16_t x;\n    int16_t y;\n    int16_t g;\n} XGEN_XG;\n\ntypedef struct __unaligned {\n    int32_t x1;\n    int32_t g1;\n    int32_t x2;\n    int32_t g2;\n} XBUF_XG;\n\ntypedef struct __unaligned {\n    uint16_t x;\n    uint16_t y;\n    uint16_t g;\n    uint16_t u;\n    uint16_t v;\n} XGEN_XGUV;\n\ntypedef struct __unaligned {\n    int32_t x1;\n    int32_t g1;\n    int32_t u1;\n    int32_t v1;\n    int32_t x2;\n    int32_t g2;\n    int32_t u2;\n    int32_t v2;\n} XBUF_XGUV;\n\ntypedef struct __unaligned {\n    uint16_t x;\n    uint16_t y;\n    uint16_t g;\n    float rhw;\n    float u;\n    float v;\n} XGEN_XGUVP;\n\ntypedef struct __unaligned {\n    int32_t x1;\n    int32_t g1;\n    float u1;\n    float v1;\n    float rhw1;\n    int32_t x2;\n    int32_t g2;\n    float u2;\n    float v2;\n    float rhw2;\n} XBUF_XGUVP;\n\ntypedef struct __unaligned {\n    uint8_t manufacturer;\n    uint8_t version;\n    uint8_t rle;\n    uint8_t bpp;\n    uint16_t x_min;\n    uint16_t y_min;\n    uint16_t x_max;\n    uint16_t y_max;\n    uint16_t h_dpi;\n    uint16_t v_dpi;\n    RGB_888 palette[16];\n    uint8_t reserved;\n    uint8_t planes;\n    uint16_t bytes_per_line;\n    uint16_t pal_pnterpret;\n    uint16_t h_res;\n    uint16_t v_res;\n    uint8_t reserved_data[54];\n} PCX_HEADER;\n\ntypedef struct __unaligned {\n    uint8_t id_length;\n    uint8_t color_map_type;\n    uint8_t data_type_code;\n    uint16_t color_map_origin;\n    uint16_t color_map_length;\n    uint8_t color_map_depth;\n    uint16_t x_origin;\n    uint16_t y_origin;\n    uint16_t width;\n    uint16_t height;\n    uint8_t bpp;\n    uint8_t image_descriptor;\n} TGA_HEADER;\n\ntypedef struct __unaligned {\n    int16_t number;\n    int16_t volume;\n    int16_t randomness;\n    int16_t flags;\n} SAMPLE_INFO;\n\n/*\ntypedef struct __unaligned {\n    int32_t volume;\n    int32_t pan;\n    int32_t sample_num;\n    int32_t pitch;\n} SOUND_SLOT;\n*/\n\ntypedef enum {\n    SF_FLIP = 0x40,\n    SF_UNFLIP = 0x80,\n} SOUND_FLAG;\n\ntypedef enum {\n    GBUF_TEMP_ALLOC               = 0,\n    GBUF_TEXTURE_PAGES            = 1,\n    GBUF_MESH_POINTERS            = 2,\n    GBUF_MESHES                   = 3,\n    GBUF_ANIMS                    = 4,\n    GBUF_STRUCTS                  = 5,\n    GBUF_ANIM_RANGES              = 6,\n    GBUF_ANIM_COMMANDS            = 7,\n    GBUF_ANIM_BONES               = 8,\n    GBUF_ANIM_FRAMES              = 9,\n    GBUF_ROOM_TEXTURES            = 10,\n    GBUF_ROOMS                    = 11,\n    GBUF_ROOM_MESH                = 12,\n    GBUF_ROOM_PORTALS             = 13,\n    GBUF_ROOM_FLOOR               = 14,\n    GBUF_ROOM_LIGHTS              = 15,\n    GBUF_ROOM_STATIC_MESHES       = 16,\n    GBUF_FLOOR_DATA               = 17,\n    GBUF_ITEMS                    = 18,\n    GBUF_CAMERAS                  = 19,\n    GBUF_SOUND_FX                 = 20,\n    GBUF_BOXES                    = 21,\n    GBUF_OVERLAPS                 = 22,\n    GBUF_GROUND_ZONE              = 23,\n    GBUF_FLY_ZONE                 = 24,\n    GBUF_ANIMATING_TEXTURE_RANGES = 25,\n    GBUF_CINEMATIC_FRAMES         = 26,\n    GBUF_LOAD_DEMO_BUFFER         = 27,\n    GBUF_SAVE_DEMO_BUFFER         = 28,\n    GBUF_CINEMATIC_EFFECTS        = 29,\n    GBUF_MUMMY_HEAD_TURN          = 30,\n    GBUF_EXTRA_DOOR_STUFF         = 31,\n    GBUF_EFFECTS_ARRAY            = 32,\n    GBUF_CREATURE_DATA            = 33,\n    GBUF_CREATURE_LOT             = 34,\n    GBUF_SAMPLE_INFOS             = 35,\n    GBUF_SAMPLES                  = 36,\n    GBUF_SAMPLE_OFFSETS           = 37,\n    GBUF_ROLLING_BALL_STUFF       = 38,\n    GBUF_SKIDOO_STUFF             = 39,\n    GBUF_LOAD_PICTURE_BUFFER      = 40,\n    GBUF_FMV_BUFFERS              = 41,\n    GBUF_POLYGON_BUFFERS          = 42,\n    GBUF_ORDER_TABLES             = 43,\n    GBUF_CLUTS                    = 44,\n    GBUF_TEXTURE_INFOS            = 45,\n    GBUF_SPRITE_INFOS             = 46,\n    GBUF_NUM_MALLOC_TYPES         = 47,\n} GAME_BUFFER;\n\ntypedef enum {\n    CLRB_PRIMARY_BUFFER          = 0x0001,\n    CLRB_BACK_BUFFER             = 0x0002,\n    CLRB_THIRD_BUFFER            = 0x0004,\n    CLRB_Z_BUFFER                = 0x0008,\n    CLRB_RENDER_BUFFER           = 0x0010,\n    CLRB_PICTURE_BUFFER          = 0x0020,\n    CLRB_WINDOWED_PRIMARY_BUFFER = 0x0040,\n    CLRB_RESERVED                = 0x0080,\n    CLRB_PHDWINSIZE              = 0x0100,\n} CLEAR_BUFFER_FLAGS;\n\ntypedef enum {\n    AC_NULL          = 0,\n    AC_MOVE_ORIGIN   = 1,\n    AC_JUMP_VELOCITY = 2,\n    AC_ATTACK_READY  = 3,\n    AC_DEACTIVATE    = 4,\n    AC_SOUND_FX      = 5,\n    AC_EFFECT        = 6,\n} ANIM_COMMAND;\n\ntypedef enum {\n    ACE_ALL   = 0,\n    ACE_LAND  = 1,\n    ACE_WATER = 2,\n} ANIM_COMMAND_ENVIRONMENT;\n\ntypedef struct __unaligned {\n    DDPIXELFORMAT pixel_fmt;\n    COLOR_BIT_MASKS color_bit_masks;\n    DWORD bpp;\n} TEXTURE_FORMAT;\n\ntypedef struct __unaligned {\n    int32_t boat_turn;\n    int32_t left_fallspeed;\n    int32_t right_fallspeed;\n    int16_t tilt_angle;\n    int16_t extra_rotation;\n    int32_t water;\n    int32_t pitch;\n} BOAT_INFO;\n\ntypedef struct __unaligned {\n    int16_t track_mesh;\n    int32_t skidoo_turn;\n    int32_t left_fallspeed;\n    int32_t right_fallspeed;\n    int16_t momentum_angle;\n    int16_t extra_rotation;\n    int32_t pitch;\n} SKIDOO_INFO;\n\ntypedef struct __unaligned {\n    int32_t start_height;\n    int32_t wait_time;\n} LIFT_INFO;\n\ntypedef struct __unaligned {\n    struct {\n        XYZ_16 min;\n        XYZ_16 max;\n    } shift, rot;\n} OBJECT_BOUNDS;\n\ntypedef struct __unaligned {\n    int32_t xv;\n    int32_t yv;\n    int32_t zv;\n} PORTAL_VBUF;\n\ntypedef struct __unaligned {\n    BOUNDS_16 bounds;\n    XYZ_16 offset;\n    int16_t mesh_rots[];\n} FRAME_INFO;\n\ntypedef struct __unaligned {\n    XYZ_16 pos;\n    int16_t radius;\n    int16_t poly_count;\n    int16_t vertex_count;\n    XYZ_16 vertex[32];\n} SHADOW_INFO;\n\ntypedef struct __unaligned {\n    int32_t table[32]; // WIBBLE_SIZE\n} ROOM_LIGHT_TABLE;\n\ntypedef struct __unaligned {\n    XYZ_16 pos;\n    int16_t light_base;\n    uint8_t light_table_value;\n    uint8_t flags;\n    int16_t light_adder;\n} ROOM_VERTEX;\n\ntypedef struct __unaligned {\n    XYZ_32 pos;\n    XYZ_16 rot;\n} HAIR_SEGMENT;\n\ntypedef enum {\n    TO_OBJECT      = 0,\n    TO_CAMERA      = 1,\n    TO_SINK        = 2,\n    TO_FLIP_MAP    = 3,\n    TO_FLIP_ON     = 4,\n    TO_FLIP_OFF    = 5,\n    TO_TARGET      = 6,\n    TO_FINISH      = 7,\n    TO_CD          = 8,\n    TO_FLIP_EFFECT = 9,\n    TO_SECRET      = 10,\n    TO_BODY_BAG    = 11,\n} TRIGGER_OBJECT;\n\ntypedef enum {\n    TT_TRIGGER     = 0,\n    TT_PAD         = 1,\n    TT_SWITCH      = 2,\n    TT_KEY         = 3,\n    TT_PICKUP      = 4,\n    TT_HEAVY       = 5,\n    TT_ANTIPAD     = 6,\n    TT_COMBAT      = 7,\n    TT_DUMMY       = 8,\n    TT_ANTITRIGGER = 9,\n} TRIGGER_TYPE;\n\ntypedef enum {\n    GF_S_PC_DETAIL_LEVELS      = 0,\n    GF_S_PC_DEMO_MODE          = 1,\n    GF_S_PC_SOUND              = 2,\n    GF_S_PC_CONTROLS           = 3,\n    GF_S_PC_GAMMA              = 4,\n    GF_S_PC_SET_VOLUMES        = 5,\n    GF_S_PC_USER_KEYS          = 6,\n    GF_S_PC_SAVE_FILE_WARNING  = 7,\n    GF_S_PC_TRY_AGAIN_QUESTION = 8,\n    GF_S_PC_YES                = 9,\n    GF_S_PC_NO                 = 10,\n    GF_S_PC_SAVE_COMPLETE      = 11,\n    GF_S_PC_NO_SAVE_GAMES      = 12,\n    GF_S_PC_NONE_VALID         = 13,\n    GF_S_PC_SAVE_GAME_QUESTION = 14,\n    GF_S_PC_EMPTY_SLOT         = 15,\n    GF_S_PC_OFF                = 16,\n    GF_S_PC_ON                 = 17,\n    GF_S_PC_SETUP_SOUND_CARD   = 18,\n    GF_S_PC_DEFAULT_KEYS       = 19,\n    GF_S_PC_DOZY               = 20,\n    GF_S_PC_NUMBER_OF          = 41,\n} GF_PC_STRING;\n\ntypedef enum {\n    GF_S_GAME_HEADING_INVENTORY       = 0,\n    GF_S_GAME_HEADING_OPTION          = 1,\n    GF_S_GAME_HEADING_ITEMS           = 2,\n    GF_S_GAME_HEADING_GAME_OVER       = 3,\n    GF_S_GAME_PASSPORT_LOAD_GAME      = 4,\n    GF_S_GAME_PASSPORT_SAVE_GAME      = 5,\n    GF_S_GAME_PASSPORT_NEW_GAME       = 6,\n    GF_S_GAME_PASSPORT_RESTART_LEVEL  = 7,\n    GF_S_GAME_PASSPORT_EXIT_TO_TITLE  = 8,\n    GF_S_GAME_PASSPORT_EXIT_DEMO      = 9,\n    GF_S_GAME_PASSPORT_EXIT_GAME      = 10,\n    GF_S_GAME_PASSPORT_SELECT_LEVEL   = 11,\n    GF_S_GAME_PASSPORT_SAVE_POSITION  = 12,\n    GF_S_GAME_DETAIL_SELECT_DETAIL    = 13,\n    GF_S_GAME_DETAIL_HIGH             = 14,\n    GF_S_GAME_DETAIL_MEDIUM           = 15,\n    GF_S_GAME_DETAIL_LOW              = 16,\n    GF_S_GAME_KEYMAP_WALK             = 17,\n    GF_S_GAME_KEYMAP_ROLL             = 18,\n    GF_S_GAME_KEYMAP_RUN              = 19,\n    GF_S_GAME_KEYMAP_LEFT             = 20,\n    GF_S_GAME_KEYMAP_RIGHT            = 21,\n    GF_S_GAME_KEYMAP_BACK             = 22,\n    GF_S_GAME_KEYMAP_STEP_LEFT        = 23,\n    GF_S_GAME_KEYMAP_RESERVED_1       = 24,\n    GF_S_GAME_KEYMAP_STEP_RIGHT       = 25,\n    GF_S_GAME_KEYMAP_RESERVED_2       = 26,\n    GF_S_GAME_KEYMAP_LOOK             = 27,\n    GF_S_GAME_KEYMAP_JUMP             = 28,\n    GF_S_GAME_KEYMAP_ACTION           = 29,\n    GF_S_GAME_KEYMAP_DRAW_WEAPON      = 30,\n    GF_S_GAME_KEYMAP_RESERVED_3       = 31,\n    GF_S_GAME_KEYMAP_INVENTORY        = 32,\n    GF_S_GAME_KEYMAP_FLARE            = 33,\n    GF_S_GAME_KEYMAP_STEP             = 34,\n    GF_S_GAME_INV_ITEM_STATISTICS     = 35,\n    GF_S_GAME_INV_ITEM_PISTOLS        = 36,\n    GF_S_GAME_INV_ITEM_SHOTGUN        = 37,\n    GF_S_GAME_INV_ITEM_MAGNUMS        = 38,\n    GF_S_GAME_INV_ITEM_UZIS           = 39,\n    GF_S_GAME_INV_ITEM_HARPOON        = 40,\n    GF_S_GAME_INV_ITEM_M16            = 41,\n    GF_S_GAME_INV_ITEM_GRENADE        = 42,\n    GF_S_GAME_INV_ITEM_FLARE          = 43,\n    GF_S_GAME_INV_ITEM_PISTOL_AMMO    = 44,\n    GF_S_GAME_INV_ITEM_SHOTGUN_AMMO   = 45,\n    GF_S_GAME_INV_ITEM_MAGNUM_AMMO    = 46,\n    GF_S_GAME_INV_ITEM_UZI_AMMO       = 47,\n    GF_S_GAME_INV_ITEM_HARPOON_AMMO   = 48,\n    GF_S_GAME_INV_ITEM_M16_AMMO       = 49,\n    GF_S_GAME_INV_ITEM_GRENADE_AMMO   = 50,\n    GF_S_GAME_INV_ITEM_SMALL_MEDIPACK = 51,\n    GF_S_GAME_INV_ITEM_LARGE_MEDIPACK = 52,\n    GF_S_GAME_INV_ITEM_PICKUP         = 53,\n    GF_S_GAME_INV_ITEM_PUZZLE         = 54,\n    GF_S_GAME_INV_ITEM_KEY            = 55,\n    GF_S_GAME_INV_ITEM_GAME           = 56,\n    GF_S_GAME_INV_ITEM_LARA_HOME      = 57,\n    GF_S_GAME_MISC_LOADING            = 58,\n    GF_S_GAME_MISC_TIME_TAKEN         = 59,\n    GF_S_GAME_MISC_SECRETS_FOUND      = 60,\n    GF_S_GAME_MISC_LOCATION           = 61,\n    GF_S_GAME_MISC_KILLS              = 62,\n    GF_S_GAME_MISC_AMMO_USED          = 63,\n    GF_S_GAME_MISC_HITS               = 64,\n    GF_S_GAME_MISC_SAVES_PERFORMED    = 65,\n    GF_S_GAME_MISC_DISTANCE_TRAVELLED = 66,\n    GF_S_GAME_MISC_HEALTH_PACKS_USED  = 67,\n    GF_S_GAME_MISC_RELEASE_VERSION    = 68,\n    GF_S_GAME_MISC_NONE               = 69,\n    GF_S_GAME_MISC_FINISH             = 70,\n    GF_S_GAME_MISC_BEST_TIMES         = 71,\n    GF_S_GAME_MISC_NO_TIMES_SET       = 72,\n    GF_S_GAME_MISC_NA                 = 73,\n    GF_S_GAME_MISC_CURRENT_POSITION   = 74,\n    GF_S_GAME_MISC_FINAL_STATISTICS   = 75,\n    GF_S_GAME_MISC_OF                 = 76,\n    GF_S_GAME_MISC_STORY_SO_FAR       = 77,\n    GF_S_GAME_NUMBER_OF               = 89,\n} GF_GAME_STRING;\n\ntypedef enum {\n    GF_ADD_INV_PISTOLS      = 0,\n    GF_ADD_INV_SHOTGUN      = 1,\n    GF_ADD_INV_MAGNUMS      = 2,\n    GF_ADD_INV_UZIS         = 3,\n    GF_ADD_INV_HARPOON      = 4,\n    GF_ADD_INV_M16          = 5,\n    GF_ADD_INV_GRENADE      = 6,\n    GF_ADD_INV_PISTOL_AMMO  = 7,\n    GF_ADD_INV_SHOTGUN_AMMO = 8,\n    GF_ADD_INV_MAGNUM_AMMO  = 9,\n    GF_ADD_INV_UZI_AMMO     = 10,\n    GF_ADD_INV_HARPOON_AMMO = 11,\n    GF_ADD_INV_M16_AMMO     = 12,\n    GF_ADD_INV_GRENADE_AMMO = 13,\n    GF_ADD_INV_FLARES       = 14,\n    GF_ADD_INV_SMALL_MEDI   = 15,\n    GF_ADD_INV_LARGE_MEDI   = 16,\n    GF_ADD_INV_PICKUP_1     = 17,\n    GF_ADD_INV_PICKUP_2     = 18,\n    GF_ADD_INV_PUZZLE_1     = 19,\n    GF_ADD_INV_PUZZLE_2     = 20,\n    GF_ADD_INV_PUZZLE_3     = 21,\n    GF_ADD_INV_PUZZLE_4     = 22,\n    GF_ADD_INV_KEY_1        = 23,\n    GF_ADD_INV_KEY_2        = 24,\n    GF_ADD_INV_KEY_3        = 25,\n    GF_ADD_INV_KEY_4        = 26,\n    GF_ADD_INV_NUMBER_OF    = 27,\n} GF_ADD_INV;\n\ntypedef enum {\n    IT_NAME      = 0,\n    IT_QTY       = 1,\n    IT_NUMBER_OF = 2,\n} INV_TEXT;\n\ntypedef enum {\n    REQ_CENTER      = 0x00,\n    REQ_USE         = 0x01,\n    REQ_ALIGN_LEFT  = 0x02,\n    REQ_ALIGN_RIGHT = 0x04,\n    REQ_HEADING     = 0x08,\n    REQ_BEST_TIME   = 0x10,\n    REQ_NORMAL_TIME = 0x20,\n    REQ_NO_TIME     = 0x40,\n} REQUESTER_FLAGS;\n\n\n# FUNCTIONS\n# Offset    Size    Declaration\n\n# 3dsystem/3d_gen.c\n0x00401000  0x01D0  void __cdecl Matrix_GenerateW2V(PHD_3DPOS *viewpos);\n0x004011D0  0x0072  void __cdecl Matrix_LookAt(int32_t xsrc, int32_t ysrc, int32_t zsrc, int32_t xtar, int32_t ytar, int32_t ztar, int16_t roll);\n0x00401250  0x0078  void __cdecl Math_GetVectorAngles(int32_t x, int32_t y, int32_t z, int16_t *dest);\n0x004012D0  0x00AA  void __cdecl Matrix_RotX(int16_t rx);\n0x00401380  0x00A8  void __cdecl Matrix_RotY(int16_t ry);\n0x00401430  0x00A8  void __cdecl Matrix_RotZ(int16_t rz);\n0x004014E0  0x01DC  void __cdecl Matrix_RotYXZ(int16_t ry, int16_t rx, int16_t rz);\n0x004016C0  0x01E7  void __cdecl Matrix_RotYXZpack(uint32_t rpack);\n0x004018B0  0x00AB  bool __cdecl Matrix_TranslateRel(int32_t x, int32_t y, int32_t z);\n0x00401960  0x007A  void __cdecl Matrix_TranslateAbs(int32_t x, int32_t y, int32_t z);\n0x004019E0  0x00F3  void __cdecl Output_InsertPolygons(const int16_t *obj_ptr, int32_t clip);\n0x00401AE0  0x00EA  void __cdecl Output_InsertRoom(const int16_t *obj_ptr, int32_t is_outside);\n0x00401BD0  0x0032  const int16_t *__cdecl Output_CalcSkyboxLight(const int16_t *obj_ptr);\n0x00401C10  0x0134  void __cdecl Output_InsertSkybox(const int16_t *obj_ptr);\n0x00401D50  0x0001  void __cdecl Output_InsertInventoryBackground(const int16_t *obj_ptr);\n0x00401D60  0x01D5  const int16_t *__cdecl Output_CalcObjectVertices(const int16_t *obj_ptr);\n0x00401F40  0x016D  const int16_t *__cdecl Output_CalcVerticeLight(const int16_t *obj_ptr);\n0x004020B0  0x027D  const int16_t *__cdecl Output_CalcRoomVertices(const int16_t *obj_ptr, int32_t far_clip);\n0x00402330  0x00C7  void __cdecl Output_RotateLight(int16_t pitch, int16_t yaw);\n0x00402400  0x0039  void __cdecl Output_InitPolyList(void);\n0x00402430  0x0033  void __cdecl Output_SortPolyList(void);\n0x00402470  0x00C5  void __cdecl Output_QuickSort(int32_t left, int32_t right);\n0x00402540  0x0036  void __cdecl Output_PrintPolyList(uint8_t *surface_ptr);\n0x00402580  0x00A1  void __cdecl Output_AlterFOV(int16_t fov);\n0x00402690  0x0095  void __cdecl Output_SetNearZ(int32_t near_z);\n0x004026E0  0x006B  void __cdecl Output_SetFarZ(int32_t far_z);\n0x00402700  0x0266  void __cdecl Output_Init(int16_t x, int16_t y, int32_t width, int32_t height, int32_t near_z, int32_t far_z, int16_t view_angle, int32_t screen_width, int32_t screen_height);\n\n# 3dsystem/3d_out.c\n0x00402970  0x019F  void __cdecl Output_DrawPolyLine(const int16_t *obj_ptr);\n0x00402B10  0x0035  void __cdecl Output_DrawPolyFlat(const int16_t *obj_ptr);\n0x00402B50  0x0035  void __cdecl Output_DrawPolyTrans(const int16_t *obj_ptr);\n0x00402B90  0x0035  void __cdecl Output_DrawPolyGouraud(const int16_t *obj_ptr);\n0x00402BD0  0x003C  void __cdecl Output_DrawPolyGTMap(const int16_t *obj_ptr);\n0x00402C10  0x003C  void __cdecl Output_DrawPolyWGTMap(const int16_t *obj_ptr);\n0x00402C50  0x00D2  int32_t __cdecl Output_XGenX(const int16_t *obj_ptr);\n0x00402D30  0x0146  int32_t __cdecl Output_XGenXG(const int16_t *obj_ptr);\n0x00402E80  0x0219  int32_t __cdecl Output_XGenXGUV(const int16_t *obj_ptr);\n0x004030A0  0x0284  int32_t __cdecl Output_XGenXGUVPerspFP(const int16_t *obj_ptr);\n0x00403330  0x0FC6  void __cdecl Output_GTMapPersp32FP(int32_t y1, int32_t y2, uint8_t *tex_page);\n0x00404300  0x14C4  void __cdecl Output_WGTMapPersp32FP(int32_t y1, int32_t y2, uint8_t *tex_page);\n0x004057D0  0x0037  void __cdecl Output_DrawPolyGTMapPersp(const int16_t *obj_ptr);\n0x00405810  0x0037  void __cdecl Output_DrawPolyWGTMapPersp(const int16_t *obj_ptr);\n\n# 3dsystem/3dinsert.\n0x00405850  0x006C  int32_t __cdecl Output_VisibleZClip(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2);\n0x004058C0  0x0140  int32_t __cdecl Output_ZedClipper(int32_t vtx_count, POINT_INFO *pts, VERTEX_INFO *vtx);\n0x00405A00  0x0511  int32_t __cdecl Output_XYGUVClipper(int32_t vtx_count, VERTEX_INFO *vtx);\n0x00405F20  0x0A5C  const int16_t *__cdecl Output_InsertObjectGT4(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x00406980  0x0872  const int16_t *__cdecl Output_InsertObjectGT3(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x00407200  0x0422  int32_t __cdecl Output_XYGClipper(int32_t vtx_count, VERTEX_INFO *vtx);\n0x00407630  0x03D1  const int16_t *__cdecl Output_InsertObjectG4(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x00407A10  0x031B  const int16_t *__cdecl Output_InsertObjectG3(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x00407D30  0x02D0  int32_t __cdecl Output_XYClipper(int32_t vtx_count, VERTEX_INFO *vtx);\n0x00408000  0x04A4  void __cdecl Output_InsertTrans8(const PHD_VBUF *vbuf, int16_t shade);\n0x004084B0  0x00D3  void __cdecl Output_InsertTransQuad(int32_t x, int32_t y, int32_t width, int32_t height, int32_t z);\n0x00408590  0x00CB  void __cdecl Output_InsertFlatRect(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x00408660  0x00B5  void __cdecl Output_InsertLine(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x00408720  0x0642  void __cdecl Output_InsertGT3_ZBuffered(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_TEXTURE *texture, const PHD_UV *uv0, const PHD_UV *uv1, const PHD_UV *uv2);\n0x00408D70  0x0140  void __cdecl Output_DrawClippedPoly_Textured(int32_t vtx_count);\n0x00408EB0  0x0444  void __cdecl Output_InsertGT4_ZBuffered(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_VBUF *vtx3, const PHD_TEXTURE *texture);\n0x00409300  0x0091  const int16_t *__cdecl Output_InsertObjectGT4_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x004093A0  0x00AA  const int16_t *__cdecl Output_InsertObjectGT3_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x00409450  0x039C  const int16_t *__cdecl Output_InsertObjectG4_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x004097F0  0x00F7  void __cdecl Output_DrawPoly_Gouraud(int32_t vtx_count, int32_t red, int32_t green, int32_t blue);\n0x004098F0  0x02D3  const int16_t *__cdecl Output_InsertObjectG3_ZBuffered(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x00409BD0  0x01C9  void __cdecl Output_InsertFlatRect_ZBuffered(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x00409DA0  0x0133  void __cdecl Output_InsertLine_ZBuffered(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x00409EE0  0x0706  void __cdecl Output_InsertGT3_Sorted(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_TEXTURE *texture, const PHD_UV *uv0, const PHD_UV *uv1, const PHD_UV *uv2, SORT_TYPE sort_type);\n0x0040A5F0  0x01AC  void __cdecl Output_InsertClippedPoly_Textured(int32_t vtx_count, float z, int16_t poly_type, int16_t tex_page);\n0x0040A7A0  0x04D7  void __cdecl Output_InsertGT4_Sorted(const PHD_VBUF *vtx0, const PHD_VBUF *vtx1, const PHD_VBUF *vtx2, const PHD_VBUF *vtx3, const PHD_TEXTURE *texture, SORT_TYPE sort_type);\n0x0040AC80  0x008C  const int16_t *__cdecl Output_InsertObjectGT4_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x0040AD10  0x009F  const int16_t *__cdecl Output_InsertObjectGT3_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x0040ADB0  0x043B  const int16_t *__cdecl Output_InsertObjectG4_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x0040B1F0  0x0175  void __cdecl Output_InsertPoly_Gouraud(int32_t vtx_count, float z, int32_t red, int32_t green, int32_t blue, int16_t poly_type);\n0x0040B370  0x0343  const int16_t *__cdecl Output_InsertObjectG3_Sorted(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x0040B6C0  0x0347  void __cdecl Output_InsertSprite_Sorted(int32_t z, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t sprite_idx, int16_t shade);\n0x0040BA10  0x017F  void __cdecl Output_InsertFlatRect_Sorted(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x0040BB90  0x012B  void __cdecl Output_InsertLine_Sorted(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x0040BCC0  0x0195  void __cdecl Output_InsertTrans8_Sorted(const PHD_VBUF *vbuf, int16_t shade);\n0x0040BE60  0x013D  void __cdecl Output_InsertTransQuad_Sorted(int32_t x, int32_t y, int32_t width, int32_t height, int32_t z);\n0x0040BFA0  0x00A7  void __cdecl Output_InsertSprite(int32_t z, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t sprite_idx, int16_t shade);\n\n# 3dsystem/scalespr.\n0x0040C050  0x02C7  void __cdecl Output_DrawSprite(uint32_t flags, int32_t x, int32_t y, int32_t z, int16_t sprite_idx, int16_t shade, int16_t scale);\n0x0040C320  0x0085  void __cdecl Output_DrawPickup(int32_t sx, int32_t sy, int32_t scale, int16_t sprite_idx, int16_t shade);\n0x0040C3B0  0x0152  const int16_t *__cdecl Output_InsertRoomSprite(const int16_t *obj_ptr, int32_t vtx_count);\n0x0040C510  0x0096  void __cdecl Output_DrawScreenSprite2D(int32_t sx, int32_t sy, int32_t sz, int32_t scale_h, int32_t scale_v, int16_t sprite_idx, int16_t shade, uint16_t flags);\n0x0040C5B0  0x009D  void __cdecl Output_DrawScreenSprite(int32_t sx, int32_t sy, int32_t sz, int32_t scale_h, int32_t scale_v, int16_t sprite_idx, int16_t shade, uint16_t flags);\n0x0040C650  0x0223  void __cdecl Output_DrawScaledSpriteC(const int16_t *obj_ptr);\n\n# game/bird.c\n0x0040C880  0x0089  void __cdecl Bird_Initialise(int16_t item_num);\n0x0040C910  0x0200  void __cdecl Bird_Control(int16_t item_num);\n\n# game/boat.c\n0x0040CB30  0x003C  void __cdecl Boat_Initialise(int16_t item_num);\n0x0040CB70  0x0170  int32_t __cdecl Boat_CheckGetOn(int16_t item_num, COLL_INFO *coll);\n0x0040CCE0  0x015E  void __cdecl Boat_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x0040CE40  0x00F8  int32_t __cdecl Boat_TestWaterHeight(ITEM *item, int32_t z_off, int32_t x_off, XYZ_32 *pos);\n0x0040CF40  0x01C1  void __cdecl Boat_DoShift(int32_t boat_num);\n0x0040D110  0x0174  void __cdecl Boat_DoWakeEffect(ITEM *boat);\n0x0040D290  0x004B  int32_t __cdecl Boat_DoDynamics(int32_t height, int32_t fall_speed, int32_t *y);\n0x0040D2E0  0x04DD  int32_t __cdecl Boat_Dynamics(int16_t boat_num);\n0x0040D7C0  0x0187  int32_t __cdecl Boat_UserControl(ITEM *boat);\n0x0040D950  0x0169  void __cdecl Boat_Animation(ITEM *boat, int32_t collide);\n0x0040DAC0  0x062A  void __cdecl Boat_Control(int16_t item_num);\n0x0040E0F0  0x00B3  void __cdecl Gondola_Control(int16_t item_num);\n\n# game/box.c\n0x0040E1B0  0x002F  void __cdecl Creature_Initialise(int16_t item_num);\n0x0040E1E0  0x0047  int32_t __cdecl Creature_Activate(int16_t item_num);\n0x0040E230  0x0242  void __cdecl Creature_AIInfo(ITEM *item, AI_INFO *info);\n0x0040E490  0x01F3  int32_t __cdecl Box_SearchLOT(LOT_INFO *lot, int32_t expansion);\n0x0040E690  0x006F  int32_t __cdecl Box_UpdateLOT(LOT_INFO *lot, int32_t expansion);\n0x0040E700  0x0095  void __cdecl Box_TargetBox(LOT_INFO *lot, int16_t box_num);\n0x0040E7A0  0x00F2  int32_t __cdecl Box_StalkBox(const ITEM *item, const ITEM *enemy, int16_t box_num);\n0x0040E8A0  0x00A4  int32_t __cdecl Box_EscapeBox(const ITEM *item, const ITEM *enemy, int16_t box_num);\n0x0040E950  0x00A7  int32_t __cdecl Box_ValidBox(const ITEM *item, int16_t zone_num, int16_t box_num);\n0x0040EA00  0x043F  void __cdecl Creature_Mood(ITEM *item, AI_INFO *info, int32_t violent);\n0x0040EE70  0x0459  TARGET_TYPE __cdecl Box_CalculateTarget(XYZ_32 *target, ITEM *item, LOT_INFO *lot);\n0x0040F2D0  0x00F8  int32_t __cdecl Creature_CheckBaddieOverlap(int16_t item_num);\n0x0040F3D0  0x008B  int32_t __cdecl Box_BadFloor(int32_t x, int32_t y, int32_t z, int32_t box_height, int32_t next_height, int16_t room_num, LOT_INFO *lot);\n0x0040F460  0x00B8  void __cdecl Creature_Die(int16_t item_num, int32_t explode);\n0x0040F520  0x08CC  int32_t __cdecl Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt);\n0x0040FDF0  0x00D5  int16_t __cdecl Creature_Turn(ITEM *item, int16_t maximum_turn);\n0x0040FED0  0x0035  void __cdecl Creature_Tilt(ITEM *item, int16_t angle);\n0x0040FF10  0x0049  void __cdecl Creature_Head(ITEM *item, int16_t required);\n0x0040FF60  0x004E  void __cdecl Creature_Neck(ITEM *item, int16_t required);\n0x0040FFB0  0x00A8  void __cdecl Creature_Float(int16_t item_num);\n0x00410060  0x0050  void __cdecl Creature_Underwater(ITEM *item, int32_t depth);\n0x004100B0  0x005C  int16_t __cdecl Creature_Effect(ITEM *item, BITE *bite, int16_t (*__cdecl spawn)(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num));\n0x00410110  0x0131  int32_t __cdecl Creature_Vault(int16_t item_num, int16_t angle, int32_t vault, int32_t shift);\n0x00410250  0x016F  void __cdecl Creature_Kill(ITEM *item, int32_t kill_anim, int32_t kill_state, int32_t lara_kill_state);\n0x004103C0  0x01DB  void __cdecl Creature_GetBaddieTarget(int16_t item_num, int32_t goody);\n\n# game/camera.c\n0x004105A0  0x00B0  void __cdecl Camera_Initialise(void);\n0x00410650  0x0372  void __cdecl Camera_Move(const GAME_VECTOR *target, int32_t speed);\n0x004109D0  0x00D7  void __cdecl Camera_Clip(int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, int32_t target_h, int32_t left, int32_t top, int32_t right, int32_t bottom);\n0x00410AB0  0x0154  void __cdecl Camera_Shift(int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, int32_t target_h, int32_t left, int32_t top, int32_t right, int32_t bottom);\n0x00410C10  0x0050  const SECTOR *__cdecl Camera_GoodPosition(int32_t x, int32_t y, int32_t z, int16_t room_num);\n0x00410C60  0x0781  void __cdecl Camera_SmartShift(GAME_VECTOR *target, void (*__cdecl shift)(int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y, int32_t target_h, int32_t left, int32_t top, int32_t right, int32_t bottom));\n0x004113F0  0x00ED  void __cdecl Camera_Chase(const ITEM *item);\n0x004114E0  0x019E  int32_t __cdecl Camera_ShiftClamp(GAME_VECTOR *pos, int32_t clamp);\n0x00411680  0x018E  void __cdecl Camera_Combat(const ITEM *item);\n0x00411810  0x01E2  void __cdecl Camera_Look(const ITEM *item);\n0x00411A00  0x0099  void __cdecl Camera_Fixed(void);\n0x00411AA0  0x04A9  void __cdecl Camera_Update(void);\n\n# game/cinema.c\n0x00411F50  0x000A  void __cdecl Game_SetCutsceneTrack(int32_t track);\n0x00411F60  0x0112  int32_t __cdecl Game_Cutscene_Start(int32_t level_num);\n0x00412080  0x0093  void __cdecl Room_InitCinematic(void);\n0x00412120  0x016F  int32_t __cdecl Game_Cutscene_Control(int32_t nframes);\n0x00412290  0x0138  void __cdecl Camera_UpdateCutscene(void);\n0x004123D0  0x007F  int32_t __cdecl Room_FindByPos(int32_t x, int32_t y, int32_t z);\n0x00412450  0x00DC  void __cdecl CutscenePlayer_Control(int16_t item_num);\n0x00412530  0x0096  void __cdecl Lara_Control_Cutscene(int16_t item_num);\n0x004125D0  0x008F  void __cdecl CutscenePlayer1_Initialise(int16_t item_num);\n0x00412660  0x0033  void __cdecl CutscenePlayerGen_Initialise(int16_t item_num);\n0x004126A0  0x0245  void __cdecl Camera_LoadCutsceneFrame(void);\n\n# game/collide.c\n0x004128F0  0x067C  void __cdecl Collide_GetCollisionInfo(COLL_INFO *coll, int32_t xpos, int32_t ypos, int32_t zpos, int16_t room_num, int32_t obj_height);\n0x00412FB0  0x002F  int32_t __cdecl Room_FindGridShift(int32_t src, int32_t dst);\n0x00412FE0  0x03D2  int32_t __cdecl Collide_CollideStaticObjects(COLL_INFO *coll, int32_t x, int32_t y, int32_t z, int16_t room_num, int32_t height);\n0x004133D0  0x00C8  void __cdecl Room_GetNearbyRooms(int32_t x, int32_t y, int32_t z, int32_t r, int32_t h, int16_t room_num);\n0x004134A0  0x0055  void __cdecl Room_GetNewRoom(int32_t x, int32_t y, int32_t z, int16_t room_num);\n0x00413500  0x0037  void __cdecl Item_ShiftCol(ITEM *item, COLL_INFO *coll);\n0x00413540  0x005D  void __cdecl Item_UpdateRoom(ITEM *item, int32_t height);\n0x004135A0  0x0099  int16_t __cdecl Room_GetTiltType(const SECTOR *sector, int32_t x, int32_t y, int32_t z);\n0x00413640  0x0195  void __cdecl Lara_BaddieCollision(ITEM *lara_item, COLL_INFO *coll);\n0x004137E0  0x0079  void __cdecl Lara_TakeHit(ITEM *lara_item, COLL_INFO *coll);\n0x00413860  0x0078  void __cdecl Creature_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x004138E0  0x0055  void __cdecl Object_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00413940  0x0077  void __cdecl Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x004139C0  0x0067  void __cdecl Object_Collision_Trap(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00413A30  0x0306  void __cdecl Lara_Push(ITEM *item, ITEM *lara_item, COLL_INFO *coll, int32_t spaz_on, int32_t big_push);\n0x00413D40  0x00CB  int32_t __cdecl Item_TestBoundsCollide(const ITEM *src_item, const ITEM *dst_item, int32_t radius);\n0x00413E10  0x0137  int32_t __cdecl Item_TestPosition(int16_t *bounds, ITEM *src_item, ITEM *dst_item);\n0x00413F50  0x013B  void __cdecl Item_AlignPosition(XYZ_32 *vec, ITEM *src_item, ITEM *dst_item);\n0x00414090  0x0187  int32_t __cdecl Lara_MovePosition(XYZ_32 *vec, ITEM *item, ITEM *lara_item);\n0x00414220  0x016E  int32_t __cdecl Misc_Move3DPosTo3DPos(PHD_3DPOS *src_pos, const PHD_3DPOS *dest_pos, int32_t velocity, int16_t ang_add);\n\n# game/control.c\n0x00414390  0x0356  int32_t __cdecl Game_Control(int32_t nframes, int32_t demo_mode);\n0x004146F0  0x0338  void __cdecl Item_Animate(ITEM *item);\n0x00414A60  0x00AB  int32_t __cdecl Item_GetAnimChange(ITEM *item, const ANIM *anim);\n0x00414B10  0x005F  void __cdecl Item_Translate(ITEM *item, int32_t x, int32_t y, int32_t z);\n0x00414B70  0x0198  SECTOR *__cdecl Room_GetSector(int32_t x, int32_t y, int32_t z, int16_t *room_num);\n0x00414D10  0x0168  int32_t __cdecl Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num);\n0x00414E80  0x0265  int32_t __cdecl Room_GetHeight(const SECTOR *sector, int32_t x, int32_t y, int32_t z);\n0x00415100  0x00E7  void __cdecl Camera_RefreshFromTrigger(int16_t type, const int16_t *data);\n0x004151F0  0x0690  void __cdecl Room_TestTriggers(int16_t *data, int32_t heavy);\n0x004158D0  0x0055  int32_t __cdecl Item_IsTriggerActive(ITEM *item);\n0x00415930  0x023D  int32_t __cdecl Room_GetCeiling(const SECTOR *sector, int32_t x, int32_t y, int32_t z);\n0x00415B90  0x004E  int16_t __cdecl Room_GetDoor(const SECTOR *sector);\n0x00415BE0  0x00A0  int32_t __cdecl LOS_Check(const GAME_VECTOR *start, GAME_VECTOR *target);\n0x00415C80  0x02EB  int32_t __cdecl LOS_CheckZ(const GAME_VECTOR *start, GAME_VECTOR *target);\n0x00415F70  0x02EC  int32_t __cdecl LOS_CheckX(const GAME_VECTOR *start, GAME_VECTOR *target);\n0x00416260  0x00DA  int32_t __cdecl LOS_ClipTarget(const GAME_VECTOR *start, GAME_VECTOR *target, const SECTOR *sector);\n0x00416340  0x02FE  int32_t __cdecl LOS_CheckSmashable(const GAME_VECTOR *start, GAME_VECTOR *target);\n0x00416640  0x00B3  void __cdecl Room_FlipMap(void);\n0x00416700  0x0096  void __cdecl Room_RemoveFlipItems(ROOM *r);\n0x004167A0  0x005C  void __cdecl Room_AddFlipItems(ROOM *r);\n0x00416800  0x0024  void __cdecl Room_TriggerMusicTrack(int16_t value, int16_t flags, int16_t type);\n0x00416830  0x00DA  void __cdecl Room_TriggerMusicTrackImpl(int16_t value, int16_t flags, int16_t type);\n\n# game/demo.c\n0x00416910  0x0059  int32_t __cdecl Demo_Control(int32_t level_num);\n0x00416970  0x01B0  int32_t __cdecl Demo_Start(int32_t level_num);\n0x00416B20  0x00CD  void __cdecl Demo_LoadLaraPos(void);\n0x00416BF0  0x002D  void __cdecl Demo_GetInput(void);\n\n# game/diver.c\n0x00416C20  0x007A  int16_t __cdecl Diver_Harpoon(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num);\n0x00416CA0  0x0106  int32_t __cdecl Diver_GetWaterSurface(int32_t x, int32_t y, int32_t z, int16_t room_num);\n0x00416DB0  0x0389  void __cdecl Diver_Control(int16_t item_num);\n\n# game/dog.c\n0x00417160  0x0387  void __cdecl Dog_Control(int16_t item_num);\n0x00417510  0x027E  void __cdecl Tiger_Control(int16_t item_num);\n\n# game/dragon.c\n0x004177B0  0x017F  void __cdecl Twinkle_Control(int16_t effect_num);\n0x00417930  0x00D9  void __cdecl Effect_CreateBartoliLight(int16_t item_num);\n0x00417A10  0x00AB  int16_t __cdecl Effect_MissileFlame(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num);\n0x00417AC0  0x02ED  void __cdecl Dragon_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00417DB0  0x00D9  void __cdecl Dragon_Bones(int16_t item_num);\n0x00417E90  0x0519  void __cdecl Dragon_Control(int16_t back_num);\n0x004183E0  0x0114  void __cdecl Bartoli_Initialise(int16_t item_num);\n0x00418500  0x0193  void __cdecl Bartoli_Control(int16_t item_num);\n0x004186A0  0x0287  void __cdecl TRex_Control(int16_t item_num);\n\n# game/draw.c\n0x00418950  0x0037  int32_t __cdecl Game_DrawCinematic(void);\n0x00418990  0x0037  int32_t __cdecl Game_Draw(void);\n0x004189D0  0x02B0  void __cdecl Room_DrawAllRooms(int16_t current_room);\n0x00418C80  0x01C6  void __cdecl Room_GetBounds(void);\n0x00418E50  0x037F  void __cdecl Room_SetBounds(const int16_t *objptr, int32_t room_num, ROOM *parent);\n0x004191D0  0x03D2  void __cdecl Room_Clip(ROOM *r);\n0x004195B0  0x00B4  void __cdecl Room_DrawSingleRoomGeometry(int16_t room_num);\n0x00419670  0x0218  void __cdecl Room_DrawSingleRoomObjects(int16_t room_num);\n0x00419890  0x0147  void __cdecl Effect_Draw(int16_t effect_num);\n0x004199E0  0x0083  void __cdecl Object_DrawSpriteItem(const ITEM *item);\n0x00419A70  0x0378  void __cdecl Object_DrawAnimatingItem(const ITEM *item);\n0x00419DF0  0x0D02  void __cdecl Lara_Draw(const ITEM *item);\n0x0041AB20  0x0BC6  void __cdecl Lara_Draw_I(const ITEM *item, const FRAME_INFO *frame1, const FRAME_INFO *frame2, int32_t frac, int32_t rate);\n0x0041B710  0x0034  void __cdecl Matrix_InitInterpolate(int32_t frac, int32_t rate);\n0x0041B750  0x0022  void __cdecl Matrix_Pop_I(void);\n0x0041B780  0x0027  void __cdecl Matrix_Push_I(void);\n0x0041B7B0  0x0031  void __cdecl Matrix_RotY_I(int16_t ang);\n0x0041B7F0  0x0031  void __cdecl Matrix_RotX_I(int16_t ang);\n0x0041B830  0x0031  void __cdecl Matrix_RotZ_I(int16_t ang);\n0x0041B870  0x0041  void __cdecl Matrix_TranslateRel_I(int32_t x, int32_t y, int32_t z);\n0x0041B8C0  0x0047  void __cdecl Matrix_TranslateRel_ID(int32_t x, int32_t y, int32_t z, int32_t x2, int32_t y2, int32_t z2);\n0x0041B910  0x0041  void __cdecl Matrix_RotYXZ_I(int16_t y, int16_t x, int16_t z);\n0x0041B960  0x003D  void __cdecl Matrix_RotYXZsuperpack_I(const int16_t **pprot1, const int16_t **pprot2, int32_t skip);\n0x0041B9A0  0x00A1  void __cdecl Matrix_RotYXZsuperpack(const int16_t **pprot, int32_t skip);\n0x0041BA50  0x002A  void __cdecl Output_InsertPolygons_I(int16_t *ptr, int32_t clip);\n0x0041BA80  0x01A5  void __cdecl Matrix_Interpolate(void);\n0x0041BC30  0x00FC  void __cdecl Matrix_InterpolateArm(void);\n0x0041BD30  0x014B  void __cdecl Gun_DrawFlash(LARA_GUN_TYPE weapon_type, int32_t clip);\n0x0041BEA0  0x00E8  void __cdecl Output_CalculateObjectLighting(const ITEM *item, const BOUNDS_16 *bounds);\n0x0041BF90  0x0092  int32_t __cdecl Item_GetFrames(const ITEM *item, FRAME_INFO *frmptr[], int32_t *rate);\n0x0041C030  0x007C  BOUNDS_16 *__cdecl Item_GetBoundsAccurate(const ITEM *item);\n0x0041C0B0  0x0035  FRAME_INFO *__cdecl Item_GetBestFrame(const ITEM *item);\n0x0041C0F0  0x0048  void __cdecl Output_AddDynamicLight(int32_t x, int32_t y, int32_t z, int32_t intensity, int32_t falloff);\n\n# game/eel.c\n0x0041C140  0x019D  void __cdecl BigEel_Control(int16_t item_num);\n0x0041C2E0  0x01E1  void __cdecl Eel_Control(int16_t item_num);\n\n# game/effects.c\n0x0041C4D0  0x008C  int32_t __cdecl Lara_IsNearItem(PHD_3DPOS *pos, int32_t distance);\n0x0041C560  0x0068  void __cdecl Sound_UpdateEffects(void);\n0x0041C5D0  0x0059  int16_t __cdecl DoBloodSplat(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t direction, int16_t room_num);\n0x0041C630  0x00A4  void __cdecl DoLotsOfBlood(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t direction, int16_t room_num, int32_t num);\n0x0041C6E0  0x0082  void __cdecl Blood_Control(int16_t effect_num);\n0x0041C770  0x007F  void __cdecl Explosion_Control(int16_t effect_num);\n0x0041C7F0  0x0072  void __cdecl Ricochet(GAME_VECTOR *pos);\n0x0041C870  0x0030  void __cdecl Ricochet_Control(int16_t effect_num);\n0x0041C8A0  0x0064  void __cdecl CreateBubble(XYZ_32 *pos, int16_t room_num);\n0x0041C910  0x0078  void __cdecl FX_Bubbles(ITEM *item);\n0x0041C990  0x00F3  void __cdecl Bubble_Control(int16_t effect_num);\n0x0041CA90  0x00C2  void __cdecl Splash(ITEM *item);\n0x0041CB60  0x0071  void __cdecl Splash_Control(int16_t effect_num);\n0x0041CBE0  0x00AE  void __cdecl WaterSprite_Control(int16_t effect_num);\n0x0041CC90  0x008C  void __cdecl SnowSprite_Control(int16_t effect_num);\n0x0041CD20  0x00DE  void __cdecl HotLiquid_Control(int16_t effect_num);\n0x0041CE00  0x013D  void __cdecl Waterfall_Control(int16_t item_num);\n0x0041CF40  0x000B  void __cdecl FX_FinishLevel(ITEM *item);\n0x0041CF50  0x0016  void __cdecl FX_Turn180(ITEM *item);\n0x0041CF70  0x0096  void __cdecl FX_FloorShake(ITEM *item);\n0x0041D010  0x0040  void __cdecl FX_LaraNormal(ITEM *item);\n0x0041D050  0x001C  void __cdecl FX_Boiler(ITEM *item);\n0x0041D070  0x008F  void __cdecl FX_Flood(ITEM *item);\n0x0041D100  0x0023  void __cdecl FX_Rubble(ITEM *item);\n0x0041D130  0x002C  void __cdecl FX_Chandelier(ITEM *item);\n0x0041D160  0x0023  void __cdecl FX_Explosion(ITEM *item);\n0x0041D190  0x001C  void __cdecl FX_Piston(ITEM *item);\n0x0041D1B0  0x001C  void __cdecl FX_Curtain(ITEM *item);\n0x0041D1D0  0x001C  void __cdecl FX_Statue(ITEM *item);\n0x0041D1F0  0x001C  void __cdecl FX_SetChange(ITEM *item);\n0x0041D210  0x003F  void __cdecl DingDong_Control(int16_t item_num);\n0x0041D250  0x0037  void __cdecl LaraAlarm_Control(int16_t item_num);\n0x0041D290  0x0067  void __cdecl AlarmSound_Control(int16_t item_num);\n0x0041D300  0x005D  void __cdecl BirdTweeter_Control(int16_t item_num);\n0x0041D360  0x0059  void __cdecl DoChimeSound(const ITEM *item);\n0x0041D3C0  0x0068  void __cdecl ClockChimes_Control(int16_t item_num);\n0x0041D430  0x0128  void __cdecl SphereOfDoom_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x0041D560  0x00F0  void __cdecl SphereOfDoom_Control(int16_t item_num);\n0x0041D650  0x012D  void __cdecl SphereOfDoom_Draw(const ITEM *item);\n0x0041D780  0x000A  void __cdecl FX_LaraHandsFree(ITEM *item);\n0x0041D790  0x0005  void __cdecl FX_FlipMap(ITEM *item);\n0x0041D7A0  0x0043  void __cdecl FX_LaraDrawRightGun(ITEM *item);\n0x0041D7F0  0x0043  void __cdecl FX_LaraDrawLeftGun(ITEM *item);\n0x0041D840  0x0063  void __cdecl FX_SwapMeshesWithMeshSwap1(ITEM *item);\n0x0041D8B0  0x0063  void __cdecl FX_SwapMeshesWithMeshSwap2(ITEM *item);\n0x0041D920  0x009A  void __cdecl FX_SwapMeshesWithMeshSwap3(ITEM *item);\n0x0041D9C0  0x0009  void __cdecl FX_InvisibilityOn(ITEM *item);\n0x0041D9D0  0x0016  void __cdecl FX_InvisibilityOff(ITEM *item);\n0x0041D9F0  0x0009  void __cdecl FX_DynamicLightOn(ITEM *item);\n0x0041DA00  0x000B  void __cdecl FX_DynamicLightOff(ITEM *item);\n0x0041DA10  0x0005  void __cdecl FX_ResetHair(ITEM *item);\n0x0041DA20  0x0024  void __cdecl FX_AssaultStart(ITEM *item);\n0x0041DA50  0x001F  void __cdecl FX_AssaultStop(ITEM *item);\n0x0041DA70  0x0017  void __cdecl FX_AssaultReset(ITEM *item);\n0x0041DA90  0x00B2  void __cdecl FX_AssaultFinished(ITEM *item);\n\n# game/enemies.c\n0x0041DB50  0x0076  int16_t __cdecl Knife(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num);\n0x0041DBD0  0x040B  void __cdecl Cultist2_Control(int16_t item_num);\n0x0041E000  0x04A1  void __cdecl Monk_Control(int16_t item_num);\n0x0041E4D0  0x05BD  void __cdecl Worker3_Control(int16_t item_num);\n0x0041EAE0  0x03F7  void __cdecl XianWarrior_Draw(const ITEM *item);\n0x0041EEE0  0x00A8  void __cdecl XianSpearman_DoDamage(ITEM *item, CREATURE *xian, int32_t damage);\n0x0041EF90  0x0058  void __cdecl XianWarrior_Initialise(int16_t item_num);\n0x0041EFF0  0x0590  void __cdecl XianSpearman_Control(int16_t item_num);\n0x0041F5D0  0x0098  void __cdecl XianKnight_SparkleTrail(ITEM *item);\n0x0041F670  0x03BA  void __cdecl XianKnight_Control(int16_t item_num);\n\n# game/gameflow.c\n0x0041FA60  0x01E9  int32_t __cdecl GF_LoadScriptFile(const char *fname);\n0x0041FC50  0x001F  int32_t __cdecl GF_DoFrontendSequence(void);\n0x0041FC70  0x0066  int32_t __cdecl GF_DoLevelSequence(int32_t level, GAME_FLOW_LEVEL_TYPE type);\n0x0041FCE0  0x047C  int32_t __cdecl GF_InterpretSequence(int16_t *ptr, GAME_FLOW_LEVEL_TYPE type, int32_t seq_type);\n0x004201C0  0x0CD3  void __cdecl GF_ModifyInventory(int32_t level, int32_t type);\n\n# game/hair.c\n0x00420EA0  0x0074  void __cdecl Lara_Hair_Initialise(void);\n0x00420F20  0x09E5  void __cdecl Lara_Hair_Control(bool in_cutscene);\n0x00421920  0x0076  void __cdecl Lara_Hair_Draw(void);\n\n# game/health.c\n0x004219A0  0x002D  BOOL __cdecl Overlay_FlashCounter(void);\n0x004219D0  0x0145  void __cdecl Overlay_DrawAssaultTimer(void);\n0x00421B20  0x0045  void __cdecl Overlay_DrawGameInfo(bool pickup_state);\n0x00421B70  0x00AB  void __cdecl Overlay_DrawHealthBar(bool flash_state);\n0x00421C20  0x0097  void __cdecl Overlay_DrawAirBar(bool flash_state);\n0x00421CC0  0x0028  void __cdecl Overlay_MakeAmmoString(char *string);\n0x00421CF0  0x0132  void __cdecl Overlay_DrawAmmoInfo(void);\n0x00421E40  0x0015  void __cdecl Overlay_InitialisePickUpDisplay(void);\n0x00421E60  0x00FD  void __cdecl Overlay_DrawPickups(bool pickup_state);\n0x00421F60  0x006C  void __cdecl Overlay_AddDisplayPickup(GAME_OBJECT_ID object_id);\n0x00421FD0  0x007A  void __cdecl Overlay_DisplayModeInfo(char* string);\n0x00422050  0x002C  void __cdecl Overlay_DrawModeInfo(void);\n\n# game/inventory.c\n0x00422080  0x119E  int32_t __cdecl Inv_Display(int32_t inventory_mode);\n0x00423310  0x0156  void __cdecl Inv_Construct(void);\n0x00423470  0x0089  void __cdecl Inv_SelectMeshes(INVENTORY_ITEM *inv_item);\n0x00423500  0x0081  int32_t __cdecl Inv_AnimateInventoryItem(INVENTORY_ITEM *inv_item);\n0x00423590  0x041D  void __cdecl Inv_DrawInventoryItem(INVENTORY_ITEM *inv_item);\n0x004239C0  0x0019  int32_t __cdecl Input_GetDebounced(int32_t input);\n0x004239E0  0x0005  void __cdecl Inv_DoInventoryPicture(void);\n0x004239F0  0x0132  void __cdecl Inv_DoInventoryBackground(void);\n\n# game/invfunc.c\n0x00423B30  0x010A  void __cdecl Inv_InitColors(void);\n0x00423C40  0x0167  void __cdecl Inv_RingIsOpen(RING_INFO *ring);\n0x00423DB0  0x0081  void __cdecl Inv_RingIsNotOpen(RING_INFO *ring);\n0x00423E40  0x0369  void __cdecl Inv_RingNotActive(INVENTORY_ITEM *inv_item);\n0x004242B0  0x0032  void __cdecl Inv_RingActive(void);\n0x004242F0  0x06BE  int32_t __cdecl Inv_AddItem(GAME_OBJECT_ID object_id);\n0x00424B00  0x0129  void __cdecl Inv_InsertItem(INVENTORY_ITEM *inv_item);\n0x00424C30  0x0077  int32_t __cdecl Inv_RequestItem(GAME_OBJECT_ID object_id);\n0x00424CB0  0x001B  void __cdecl Inv_RemoveAllItems(void);\n0x00424CD0  0x0110  int32_t __cdecl Inv_RemoveItem(GAME_OBJECT_ID object_id);\n0x00424DE0  0x00C1  int32_t __cdecl Inv_GetItemOption(GAME_OBJECT_ID object_id);\n0x00424FD0  0x0024  void __cdecl Inv_RemoveInventoryText(void);\n0x00425000  0x010F  void __cdecl Inv_Ring_Init(RING_INFO *ring, int16_t type, INVENTORY_ITEM **list, int16_t qty, int16_t current, IMOTION_INFO *imo);\n0x00425110  0x0060  void __cdecl Inv_Ring_GetView(RING_INFO *ring, PHD_3DPOS *viewer);\n0x00425170  0x0040  void __cdecl Inv_Ring_Light(RING_INFO *ring);\n0x004251B0  0x002C  void __cdecl Inv_Ring_CalcAdders(RING_INFO *ring, int16_t rotation_duration);\n0x004251E0  0x013E  void __cdecl Inv_Ring_DoMotions(RING_INFO *ring);\n0x00425320  0x002F  void __cdecl Inv_Ring_RotateLeft(RING_INFO *ring);\n0x00425350  0x002F  void __cdecl Inv_Ring_RotateRight(RING_INFO *ring);\n0x00425380  0x0063  void __cdecl Inv_Ring_MotionInit(RING_INFO *ring, int16_t frames, int16_t status, int16_t status_target);\n0x004253F0  0x002C  void __cdecl Inv_Ring_MotionSetup(RING_INFO *ring, int16_t status, int16_t status_target, int16_t frames);\n0x00425420  0x0026  void __cdecl Inv_Ring_MotionRadius(RING_INFO *ring, int16_t target);\n0x00425450  0x0022  void __cdecl Inv_Ring_MotionRotation(RING_INFO *ring, int16_t rotation, int16_t target);\n0x00425480  0x0025  void __cdecl Inv_Ring_MotionCameraPos(RING_INFO *ring, int16_t target);\n0x004254B0  0x0020  void __cdecl Inv_Ring_MotionCameraPitch(RING_INFO *ring, int16_t target);\n0x004254D0  0x005D  void __cdecl Inv_Ring_MotionItemSelect(RING_INFO *ring, INVENTORY_ITEM *inv_item);\n0x00425530  0x0063  void __cdecl Inv_Ring_MotionItemDeselect(RING_INFO *ring, INVENTORY_ITEM *inv_item);\n\n# game/invtext.c\n0x004255A0  0x0082  void __cdecl Requester_Init(REQUEST_INFO *req);\n0x00425630  0x00A3  void __cdecl Requester_Shutdown(REQUEST_INFO *req);\n0x004256E0  0x001B  void __cdecl Requester_Item_CenterAlign(REQUEST_INFO *req, TEXTSTRING *txt);\n0x00425700  0x0054  void __cdecl Requester_Item_LeftAlign(REQUEST_INFO *req, TEXTSTRING *txt);\n0x00425760  0x0056  void __cdecl Requester_Item_RightAlign(REQUEST_INFO *req, TEXTSTRING *txt);\n0x004257C0  0x0866  int32_t __cdecl Requester_Display(REQUEST_INFO *req, int32_t des, int32_t backgrounds);\n0x00426030  0x00AA  void __cdecl Requester_SetHeading(REQUEST_INFO *req, char *text1, uint32_t flags1, char *text2, uint32_t flags2);\n0x004260E0  0x0013  void __cdecl Requester_RemoveAllItems(REQUEST_INFO *req);\n0x00426100  0x00C0  void __cdecl Requester_ChangeItem(REQUEST_INFO *req, int32_t item, const char *text1, uint32_t flags1, const char *text2, uint32_t flags2);\n0x004261C0  0x00AC  void __cdecl Requester_AddItem(REQUEST_INFO *req, const char *text1, uint32_t flags1, const char *text2, uint32_t flags2);\n0x00426270  0x0039  void __cdecl Requester_SetSize(REQUEST_INFO *req, int32_t maxlines, int32_t ypos);\n0x004262B0  0x0081  int32_t __cdecl AddAssaultTime(uint32_t time);\n0x00426340  0x01D6  void __cdecl ShowGymStatsText(char *time_str, int32_t type);\n0x00426520  0x0397  void __cdecl ShowStatsText(char *time_str, int32_t type);\n0x004268C0  0x0425  void __cdecl ShowEndStatsText(void);\n\n# game/items.c\n0x00426CF0  0x0052  void __cdecl Item_InitialiseArray(int32_t num_items);\n0x00426D50  0x011E  void __cdecl Item_Kill(int16_t item_num);\n0x00426E70  0x0039  int16_t __cdecl Item_Create(void);\n0x00426EB0  0x01B3  void __cdecl Item_Initialise(int16_t item_num);\n0x00427070  0x008A  void __cdecl Item_RemoveActive(int16_t item_num);\n0x00427100  0x006F  void __cdecl Item_RemoveDrawn(int16_t item_num);\n0x00427170  0x005A  void __cdecl Item_AddActive(int16_t item_num);\n0x004271D0  0x009C  void __cdecl Item_NewRoom(int16_t item_num, int16_t room_num);\n0x00427270  0x007C  int32_t __cdecl Item_GlobalReplace(GAME_OBJECT_ID src_object_id, GAME_OBJECT_ID dst_object_id);\n0x004272F0  0x0030  void __cdecl Effect_InitialiseArray(void);\n0x00427320  0x006C  int16_t __cdecl Effect_Create(int16_t room_num);\n0x00427390  0x00E3  void __cdecl Effect_Kill(int16_t effect_num);\n0x00427480  0x0093  void __cdecl Effect_NewRoom(int16_t effect_num, int16_t room_num);\n0x00427520  0x0058  void __cdecl Item_ClearKilled(void);\n\n# game/lara.c\n0x00427580  0x0195  void __cdecl Lara_HandleAboveWater(ITEM *item, COLL_INFO *coll);\n0x00427720  0x0066  void __cdecl Lara_LookUpDown(void);\n0x00427790  0x0072  void __cdecl Lara_LookLeftRight(void);\n0x00427810  0x0089  void __cdecl Lara_ResetLook(void);\n0x004278A0  0x008B  void __cdecl Lara_State_Walk(ITEM *item, COLL_INFO *coll);\n0x00427930  0x0143  void __cdecl Lara_State_Run(ITEM *item, COLL_INFO *coll);\n0x00427A80  0x0148  void __cdecl Lara_State_Stop(ITEM *item, COLL_INFO *coll);\n0x00427BD0  0x00D3  void __cdecl Lara_State_ForwardJump(ITEM *item, COLL_INFO *coll);\n0x00427CB0  0x0057  void __cdecl Lara_State_FastBack(ITEM *item, COLL_INFO *coll);\n0x00427D10  0x008A  void __cdecl Lara_State_TurnRight(ITEM *item, COLL_INFO *coll);\n0x00427DA0  0x0089  void __cdecl Lara_State_TurnLeft(ITEM *item, COLL_INFO *coll);\n0x00427E30  0x0014  void __cdecl Lara_State_Death(ITEM *item, COLL_INFO *coll);\n0x00427E50  0x0040  void __cdecl Lara_State_FastFall(ITEM *item, COLL_INFO *coll);\n0x00427E90  0x0058  void __cdecl Lara_State_Hang(ITEM *item, COLL_INFO *coll);\n0x00427EF0  0x001C  void __cdecl Lara_State_Reach(ITEM *item, COLL_INFO *coll);\n0x00427F10  0x000A  void __cdecl Lara_State_Splat(ITEM *item, COLL_INFO *coll);\n0x00427F20  0x010C  void __cdecl Lara_State_Compress(ITEM *item, COLL_INFO *coll);\n0x00428030  0x0084  void __cdecl Lara_State_Back(ITEM *item, COLL_INFO *coll);\n0x004280C0  0x000B  void __cdecl Lara_State_Null(ITEM *item, COLL_INFO *coll);\n0x004280D0  0x004B  void __cdecl Lara_State_FastTurn(ITEM *item, COLL_INFO *coll);\n0x00428120  0x007C  void __cdecl Lara_State_StepRight(ITEM *item, COLL_INFO *coll);\n0x004281A0  0x007C  void __cdecl Lara_State_StepLeft(ITEM *item, COLL_INFO *coll);\n0x00428220  0x002B  void __cdecl Lara_State_Slide(ITEM *item, COLL_INFO *coll);\n0x00428250  0x004A  void __cdecl Lara_State_BackJump(ITEM *item, COLL_INFO *coll);\n0x004282A0  0x0033  void __cdecl Lara_State_RightJump(ITEM *item, COLL_INFO *coll);\n0x004282E0  0x0033  void __cdecl Lara_State_LeftJump(ITEM *item, COLL_INFO *coll);\n0x00428320  0x0013  void __cdecl Lara_State_UpJump(ITEM *item, COLL_INFO *coll);\n0x00428340  0x002C  void __cdecl Lara_State_Fallback(ITEM *item, COLL_INFO *coll);\n0x00428370  0x0035  void __cdecl Lara_State_HangLeft(ITEM *item, COLL_INFO *coll);\n0x004283B0  0x0035  void __cdecl Lara_State_HangRight(ITEM *item, COLL_INFO *coll);\n0x004283F0  0x0018  void __cdecl Lara_State_SlideBack(ITEM *item, COLL_INFO *coll);\n0x00428410  0x0030  void __cdecl Lara_State_PushBlock(ITEM *item, COLL_INFO *coll);\n0x00428440  0x0027  void __cdecl Lara_State_PPReady(ITEM *item, COLL_INFO *coll);\n0x00428470  0x0030  void __cdecl Lara_State_Pickup(ITEM *item, COLL_INFO *coll);\n0x004284A0  0x0058  void __cdecl Lara_State_PickupFlare(ITEM *item, COLL_INFO *coll);\n0x00428500  0x0039  void __cdecl Lara_State_SwitchOn(ITEM *item, COLL_INFO *coll);\n0x00428540  0x0030  void __cdecl Lara_State_UseKey(ITEM *item, COLL_INFO *coll);\n0x00428570  0x001D  void __cdecl Lara_State_Special(ITEM *item, COLL_INFO *coll);\n0x00428590  0x002F  void __cdecl Lara_State_SwanDive(ITEM *item, COLL_INFO *coll);\n0x004285C0  0x0054  void __cdecl Lara_State_FastDive(ITEM *item, COLL_INFO *coll);\n0x00428620  0x0015  void __cdecl Lara_State_WaterOut(ITEM *item, COLL_INFO *coll);\n0x00428640  0x00CA  void __cdecl Lara_State_Wade(ITEM *item, COLL_INFO *coll);\n0x00428710  0x0096  void __cdecl Lara_State_Zipline(ITEM *item, COLL_INFO *coll);\n0x004287B0  0x004C  void __cdecl Lara_State_Extra_Breath(ITEM *item, COLL_INFO *coll);\n0x00428800  0x0047  void __cdecl Lara_State_Extra_YetiKill(ITEM *item, COLL_INFO *coll);\n0x00428850  0x0091  void __cdecl Lara_State_Extra_SharkKill(ITEM *item, COLL_INFO *coll);\n0x004288F0  0x0013  void __cdecl Lara_State_Extra_Airlock(ITEM *item, COLL_INFO *coll);\n0x00428910  0x001D  void __cdecl Lara_State_Extra_GongBong(ITEM *item, COLL_INFO *coll);\n0x00428930  0x0051  void __cdecl Lara_State_Extra_DinoKill(ITEM *item, COLL_INFO *coll);\n0x00428990  0x00BC  void __cdecl Lara_State_Extra_PullDagger(ITEM *item, COLL_INFO *coll);\n0x00428A50  0x004D  void __cdecl Lara_State_Extra_StartAnim(ITEM *item, COLL_INFO *coll);\n0x00428AA0  0x00A5  void __cdecl Lara_State_Extra_StartHouse(ITEM *item, COLL_INFO *coll);\n0x00428B50  0x00A3  void __cdecl Lara_State_Extra_FinalAnim(ITEM *item, COLL_INFO *coll);\n0x00428C00  0x0051  int32_t __cdecl Lara_Fallen(ITEM *item, COLL_INFO *coll);\n0x00428C60  0x009B  void __cdecl Lara_CollideStop(ITEM *item, COLL_INFO *coll);\n0x00428D20  0x0191  void __cdecl Lara_Col_Walk(ITEM *item, COLL_INFO *coll);\n0x00428EC0  0x0176  void __cdecl Lara_Col_Run(ITEM *item, COLL_INFO *coll);\n0x00429040  0x0081  void __cdecl Lara_Col_Stop(ITEM *item, COLL_INFO *coll);\n0x004290D0  0x00D7  void __cdecl Lara_Col_ForwardJump(ITEM *item, COLL_INFO *coll);\n0x004291B0  0x00B3  void __cdecl Lara_Col_FastBack(ITEM *item, COLL_INFO *coll);\n0x00429270  0x0095  void __cdecl Lara_Col_TurnRight(ITEM *item, COLL_INFO *coll);\n0x00429310  0x0013  void __cdecl Lara_Col_TurnLeft(ITEM *item, COLL_INFO *coll);\n0x00429330  0x0068  void __cdecl Lara_Col_Death(ITEM *item, COLL_INFO *coll);\n0x004293A0  0x0099  void __cdecl Lara_Col_FastFall(ITEM *item, COLL_INFO *coll);\n0x00429440  0x0127  void __cdecl Lara_Col_Hang(ITEM *item, COLL_INFO *coll);\n0x00429570  0x0090  void __cdecl Lara_Col_Reach(ITEM *item, COLL_INFO *coll);\n0x00429600  0x0059  void __cdecl Lara_Col_Splat(ITEM *item, COLL_INFO *coll);\n0x00429660  0x0013  void __cdecl Lara_Col_Land(ITEM *item, COLL_INFO *coll);\n0x00429680  0x0096  void __cdecl Lara_Col_Compress( ITEM *item, COLL_INFO *coll );\n0x00429720  0x00FB  void __cdecl Lara_Col_Back(ITEM *item, COLL_INFO *coll);\n0x00429820  0x00BE  void __cdecl Lara_Col_StepRight(ITEM *item, COLL_INFO *coll);\n0x004298E0  0x0013  void __cdecl Lara_Col_StepLeft(ITEM *item, COLL_INFO *coll);\n0x00429900  0x001E  void __cdecl Lara_Col_Slide(ITEM *item, COLL_INFO *coll);\n0x00429920  0x0023  void __cdecl Lara_Col_BackJump(ITEM *item, COLL_INFO *coll);\n0x00429950  0x0023  void __cdecl Lara_Col_RightJump(ITEM *item, COLL_INFO *coll);\n0x00429980  0x0023  void __cdecl Lara_Col_LeftJump(ITEM *item, COLL_INFO *coll);\n0x004299B0  0x011B  void __cdecl Lara_Col_UpJump(ITEM *item, COLL_INFO *coll);\n0x00429AD0  0x0083  void __cdecl Lara_Col_Fallback(ITEM *item, COLL_INFO *coll);\n0x00429B60  0x0033  void __cdecl Lara_Col_HangLeft(ITEM *item, COLL_INFO *coll);\n0x00429BA0  0x0033  void __cdecl Lara_Col_HangRight(ITEM *item, COLL_INFO *coll);\n0x00429BE0  0x0023  void __cdecl Lara_Col_SlideBack(ITEM *item, COLL_INFO *coll);\n0x00429C10  0x0013  void __cdecl Lara_Col_Null(ITEM *item, COLL_INFO *coll);\n0x00429C30  0x0081  void __cdecl Lara_Col_Roll(ITEM *item, COLL_INFO *coll);\n0x00429CC0  0x00B3  void __cdecl Lara_Col_Roll2(ITEM *item, COLL_INFO *coll);\n0x00429D80  0x0069  void __cdecl Lara_Col_SwanDive(ITEM *item, COLL_INFO *coll);\n0x00429DF0  0x0079  void __cdecl Lara_Col_FastDive(ITEM *item, COLL_INFO *coll);\n0x00429E70  0x0162  void __cdecl Lara_Col_Wade(ITEM *item, COLL_INFO *coll);\n0x00429FE0  0x0036  void __cdecl Lara_Col_Default(ITEM *item, COLL_INFO *coll);\n0x0042A020  0x0074  void __cdecl Lara_Col_Jumper(ITEM *item, COLL_INFO *coll);\n0x0042A0A0  0x0032  void __cdecl Lara_GetCollisionInfo(ITEM *item, COLL_INFO *coll);\n0x0042A0E0  0x00E2  void __cdecl Lara_SlideSlope(ITEM *item, COLL_INFO *coll);\n0x0042A1D0  0x0067  int32_t __cdecl Lara_HitCeiling(ITEM *item, COLL_INFO *coll);\n0x0042A240  0x007F  int32_t __cdecl Lara_DeflectEdge(ITEM *item, COLL_INFO *coll);\n0x0042A2C0  0x0136  void __cdecl Lara_DeflectEdgeJump(ITEM *item, COLL_INFO *coll);\n0x0042A440  0x00AB  void __cdecl Lara_SlideEdgeJump(ITEM *item, COLL_INFO *coll);\n0x0042A530  0x00E1  int32_t __cdecl Lara_TestWall(ITEM *item, int32_t front, int32_t right, int32_t down);\n0x0042A640  0x00F5  int32_t __cdecl Lara_TestHangOnClimbWall(ITEM *item, COLL_INFO *coll);\n0x0042A750  0x00BE  int32_t __cdecl Lara_TestClimbStance(ITEM *item, COLL_INFO *coll);\n0x0042A810  0x033E  void __cdecl Lara_HangTest(ITEM *item, COLL_INFO *coll);\n0x0042AB70  0x00AD  int32_t __cdecl Lara_TestEdgeCatch(ITEM *item, COLL_INFO *coll, int32_t *edge);\n0x0042AC20  0x016D  int32_t __cdecl Lara_TestHangJumpUp(ITEM *item, COLL_INFO *coll);\n0x0042AD90  0x019E  int32_t __cdecl Lara_TestHangJump(ITEM *item, COLL_INFO *coll);\n0x0042AF30  0x00B1  int32_t __cdecl Lara_TestHangSwingIn(ITEM *item, int16_t angle);\n0x0042AFF0  0x02E7  int32_t __cdecl Lara_TestVault(ITEM *item, COLL_INFO *coll);\n0x0042B2E0  0x0130  int32_t __cdecl Lara_TestSlide(ITEM *item, COLL_INFO *coll);\n0x0042B410  0x0075  int16_t __cdecl Lara_FloorFront(ITEM *item, int16_t ang, int32_t dist);\n0x0042B490  0x00BB  int32_t __cdecl Lara_LandedBad(ITEM *item, COLL_INFO *coll);\n0x0042B550  0x038F  void __cdecl Lara_GetJointAbsPosition(XYZ_32 *vec, int32_t joint);\n0x0042B8E0  0x031A  void __cdecl Lara_GetJointAbsPosition_I(ITEM *item, XYZ_32 *vec, int16_t *frame1, int16_t *frame2, int32_t frac, int32_t rate);\n\n# game/lara1gun.c\n0x0042BC00  0x0033  void __cdecl Gun_Rifle_DrawMeshes(LARA_GUN_TYPE weapon_type);\n0x0042BC40  0x002B  void __cdecl Gun_Rifle_UndrawMeshes(LARA_GUN_TYPE weapon_type);\n0x0042BC70  0x0070  void __cdecl Gun_Rifle_Ready(LARA_GUN_TYPE weapon_type);\n0x0042BCE0  0x00F5  void __cdecl Gun_Rifle_Control(LARA_GUN_TYPE weapon_type);\n0x0042BDE0  0x00F2  void __cdecl Gun_Rifle_FireShotgun(void);\n0x0042BEE0  0x007B  void __cdecl Gun_Rifle_FireM16(bool running);\n0x0042BF60  0x0187  void __cdecl Gun_Rifle_FireHarpoon(void);\n0x0042C0F0  0x0344  void __cdecl HarpoonBolt_Control(int16_t item_num);\n0x0042C440  0x00F0  void __cdecl Gun_Rifle_FireGrenade(void);\n0x0042C530  0x03FD  void __cdecl Grenade_Control(int16_t item_num);\n0x0042C930  0x0166  void __cdecl Gun_Rifle_Draw(LARA_GUN_TYPE weapon_type);\n0x0042CAA0  0x0104  void __cdecl Gun_Rifle_Undraw(LARA_GUN_TYPE weapon_type);\n0x0042CBB0  0x037E  void __cdecl Gun_Rifle_Animate(LARA_GUN_TYPE weapon_type);\n\n# game/lara2gun.c\n0x0042CF60  0x004F  void __cdecl Gun_Pistols_SetArmInfo(LARA_ARM *arm, int32_t frame);\n0x0042CFB0  0x007C  void __cdecl Gun_Pistols_Draw(LARA_GUN_TYPE weapon_type);\n0x0042D030  0x0225  void __cdecl Gun_Pistols_Undraw(LARA_GUN_TYPE weapon_type);\n0x0042D260  0x005C  void __cdecl Gun_Pistols_Ready(LARA_GUN_TYPE weapon_type);\n0x0042D2C0  0x004E  void __cdecl Gun_Pistols_DrawMeshes(LARA_GUN_TYPE weapon_type);\n0x0042D310  0x003A  void __cdecl Gun_Pistols_UndrawMeshLeft(LARA_GUN_TYPE weapon_type);\n0x0042D350  0x003A  void __cdecl Gun_Pistols_UndrawMeshRight(LARA_GUN_TYPE weapon_type);\n0x0042D390  0x018C  void __cdecl Gun_Pistols_Control(LARA_GUN_TYPE weapon_type);\n0x0042D520  0x0330  void __cdecl Gun_Pistols_Animate(LARA_GUN_TYPE weapon_type);\n\n# game/laraclimb.c\n0x0042D850  0x0035  void __cdecl Lara_State_ClimbLeft(ITEM *item, COLL_INFO *coll);\n0x0042D890  0x0035  void __cdecl Lara_State_ClimbRight(ITEM *item, COLL_INFO *coll);\n0x0042D8D0  0x0075  void __cdecl Lara_State_ClimbStance(ITEM *item, COLL_INFO *coll);\n0x0042D950  0x0014  void __cdecl Lara_State_Climbing(ITEM *item, COLL_INFO *coll);\n0x0042D970  0x001E  void __cdecl Lara_State_ClimbEnd(ITEM *item, COLL_INFO *coll);\n0x0042D990  0x0014  void __cdecl Lara_State_ClimbDown(ITEM *item, COLL_INFO *coll);\n0x0042D9B0  0x005D  void __cdecl Lara_Col_ClimbLeft(ITEM *item, COLL_INFO *coll);\n0x0042DA10  0x0059  void __cdecl Lara_Col_ClimbRight(ITEM *item, COLL_INFO *coll);\n0x0042DA70  0x020D  void __cdecl Lara_Col_ClimbStance(ITEM *item, COLL_INFO *coll);\n0x0042DC80  0x014D  void __cdecl Lara_Col_Climbing(ITEM *item, COLL_INFO *coll);\n0x0042DDD0  0x019C  void __cdecl Lara_Col_ClimbDown(ITEM *item, COLL_INFO *coll);\n0x0042DF70  0x00AA  int32_t __cdecl Lara_CheckForLetGo(ITEM *item, COLL_INFO *coll);\n0x0042E020  0x0263  int32_t __cdecl Lara_TestClimb(int32_t x, int32_t y, int32_t z, int32_t xfront, int32_t zfront, int32_t item_height, int16_t item_room, int32_t *shift);\n0x0042E290  0x00BC  int32_t __cdecl Lara_TestClimbPos(ITEM *item, int32_t front, int32_t right, int32_t origin, int32_t height, int32_t *shift);\n0x0042E360  0x00EF  void __cdecl Lara_DoClimbLeftRight(ITEM *item, COLL_INFO *coll, int32_t result, int32_t shift);\n0x0042E450  0x0235  int32_t __cdecl Lara_TestClimbUpPos(ITEM *item, int32_t front, int32_t right, int32_t *shift, int32_t *ledge);\n\n# game/larafire.c\n0x0042E6A0  0x04E8  void __cdecl Gun_Control(void);\n0x0042EC10  0x003B  int32_t __cdecl Gun_CheckForHoldingState(int32_t state);\n0x0042EC50  0x011C  void __cdecl Gun_InitialiseNewWeapon(void);\n0x0042ED90  0x0194  void __cdecl Gun_TargetInfo(const WEAPON_INFO *winfo);\n0x0042EF30  0x021C  void __cdecl Gun_GetNewTarget(WEAPON_INFO *winfo);\n0x0042F150  0x00AA  void __cdecl Gun_FindTargetPoint(const ITEM *item, GAME_VECTOR *target);\n0x0042F200  0x00C1  void __cdecl Gun_AimWeapon(WEAPON_INFO *winfo, LARA_ARM *arm);\n0x0042F2D0  0x0360  int32_t __cdecl Gun_FireWeapon(LARA_GUN_TYPE weapon_type, ITEM *target, const ITEM *src, const int16_t *angles);\n0x0042F640  0x0096  void __cdecl Gun_HitTarget(ITEM *item, GAME_VECTOR *hitpos, int32_t damage);\n0x0042F6E0  0x0051  void __cdecl Gun_SmashItem(int16_t item_num, LARA_GUN_TYPE weapon_type);\n0x0042F740  0x003B  GAME_OBJECT_ID Gun_GetWeaponAnim(const LARA_GUN_TYPE gun_type);\n\n# game/laraflare.c\n0x0042F7A0  0x009D  int32_t __cdecl Flare_DoLight(XYZ_32 *pos, int32_t flare_age);\n0x0042F840  0x00D3  void __cdecl Flare_DoInHand(int32_t flare_age);\n0x0042F920  0x00F8  void __cdecl Flare_DrawInAir(const ITEM *item);\n0x0042FA20  0x01D7  void __cdecl Flare_Create(int32_t thrown);\n0x0042FC00  0x004B  void __cdecl Flare_SetArm(int32_t frame);\n0x0042FC50  0x0169  void __cdecl Flare_Draw(void);\n0x0042FDC0  0x0221  void __cdecl Flare_Undraw(void);\n0x0042FFF0  0x0018  void __cdecl Flare_DrawMeshes(void);\n0x00430010  0x0018  void __cdecl Flare_UndrawMeshes(void);\n0x00430030  0x003E  void __cdecl Flare_Ready(void);\n0x00430070  0x026E  void __cdecl Flare_Control(int16_t item_num);\n\n# game/laramisc.c\n0x004302E0  0x0668  void __cdecl Lara_Control(int16_t item_num);\n0x00430970  0x02CD  void __cdecl Lara_Animate(ITEM *item);\n0x00430C70  0x013F  void __cdecl Lara_UseItem(GAME_OBJECT_ID object_id);\n0x00430E30  0x00BA  void __cdecl Lara_CheatGetStuff(void);\n0x00430EF0  0x001B  void __cdecl Lara_ControlExtra(int16_t item_num);\n0x00430F10  0x0021  void __cdecl Lara_InitialiseLoad(int16_t item_num);\n0x00430F40  0x02BD  void __cdecl Lara_Initialise(int32_t type);\n0x00431200  0x036C  void __cdecl Lara_InitialiseInventory(int32_t level_num);\n0x00431570  0x00FA  void __cdecl Lara_InitialiseMeshes(int32_t level_num);\n\n# game/larasurf.c\n0x00431670  0x0158  void __cdecl Lara_HandleSurface(ITEM *item, COLL_INFO *coll);\n0x004317D0  0x0070  void __cdecl Lara_State_SurfSwim(ITEM *item, COLL_INFO *coll);\n0x00431840  0x005E  void __cdecl Lara_State_SurfBack(ITEM *item, COLL_INFO *coll);\n0x004318A0  0x0060  void __cdecl Lara_State_SurfLeft(ITEM *item, COLL_INFO *coll);\n0x00431900  0x0060  void __cdecl Lara_State_SurfRight(ITEM *item, COLL_INFO *coll);\n0x00431960  0x00EB  void __cdecl Lara_State_SurfTread(ITEM *item, COLL_INFO *coll);\n0x00431A50  0x0032  void __cdecl Lara_Col_SurfSwim(ITEM *item, COLL_INFO *coll);\n0x00431A90  0x0023  void __cdecl Lara_Col_SurfBack(ITEM *item, COLL_INFO *coll);\n0x00431AC0  0x0023  void __cdecl Lara_Col_SurfLeft(ITEM *item, COLL_INFO *coll);\n0x00431AF0  0x0023  void __cdecl Lara_Col_SurfRight(ITEM *item, COLL_INFO *coll);\n0x00431B20  0x001E  void __cdecl Lara_Col_SurfTread(ITEM *item, COLL_INFO *coll);\n0x00431B40  0x00F3  void __cdecl Lara_SurfaceCollision(ITEM *item, COLL_INFO *coll);\n0x00431C40  0x00E7  int32_t __cdecl Lara_TestWaterStepOut(ITEM *item, COLL_INFO *coll);\n0x00431D30  0x021C  int32_t __cdecl Lara_TestWaterClimbOut(ITEM *item, COLL_INFO *coll);\n\n# game/laraswim.c\n0x00431F50  0x0223  void __cdecl Lara_HandleUnderwater(ITEM *item, COLL_INFO *coll);\n0x00432180  0x0086  void __cdecl Lara_SwimTurn(ITEM *item);\n0x00432210  0x006B  void __cdecl Lara_State_Swim(ITEM *item, COLL_INFO *coll);\n0x00432280  0x0076  void __cdecl Lara_State_Glide(ITEM *item, COLL_INFO *coll);\n0x00432300  0x0085  void __cdecl Lara_State_Tread(ITEM *item, COLL_INFO *coll);\n0x00432390  0x0014  void __cdecl Lara_State_Dive(ITEM *item, COLL_INFO *coll);\n0x004323B0  0x0053  void __cdecl Lara_State_UWDeath(ITEM *item, COLL_INFO *coll);\n0x00432410  0x000B  void __cdecl Lara_State_UWTwist(ITEM *item, COLL_INFO *coll);\n0x00432420  0x0013  void __cdecl Lara_Col_Swim(ITEM *item, COLL_INFO *coll);\n0x00432440  0x005B  void __cdecl Lara_Col_UWDeath(ITEM *item, COLL_INFO *coll);\n0x004324A0  0x0192  int32_t __cdecl Lara_GetWaterDepth(int32_t x, int32_t y, int32_t z, int16_t room_num);\n0x00432640  0x00CE  void __cdecl Lara_TestWaterDepth(ITEM *item, COLL_INFO *coll);\n0x00432710  0x015C  void __cdecl Lara_SwimCollision(ITEM *item, COLL_INFO *coll);\n0x00432870  0x01EC  void __cdecl Lara_WaterCurrent(COLL_INFO *coll);\n\n# game/lot.c\n0x00432A60  0x0053  void __cdecl LOT_InitialiseArray(void);\n0x00432AC0  0x004F  void __cdecl LOT_DisableBaddieAI(int16_t item_num);\n0x00432B10  0x01B0  bool __cdecl LOT_EnableBaddieAI(int16_t item_num, bool always);\n0x00432CC0  0x0106  void __cdecl LOT_InitialiseSlot(int16_t item_num, int32_t slot);\n0x00432ED0  0x00B8  void __cdecl LOT_CreateZone(ITEM *item);\n0x00432F90  0x0049  void __cdecl LOT_ClearLOT(LOT_INFO *lot);\n\n# game/missile.c\n0x00432FE0  0x02D0  void __cdecl Missile_Control(int16_t effect_num);\n0x004332B0  0x00A7  void __cdecl Missile_ShootAtLara(EFFECT *effect);\n0x00433360  0x0386  int32_t __cdecl Item_Explode(int16_t item_num, int32_t mesh_bits, int16_t damage);\n0x004336F0  0x0200  void __cdecl BodyPart_Control(int16_t effect_num);\n\n# game/moveblock.c\n0x004338F0  0x002C  void __cdecl MovableBlock_Initialise(int16_t item_num);\n0x00433920  0x0148  void __cdecl MovableBlock_Control(int16_t item_num);\n0x00433A70  0x0239  void __cdecl MovableBlock_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00433CD0  0x004E  int32_t __cdecl MovableBlock_TestDestination(ITEM *item, int32_t block_height);\n0x00433D20  0x0137  int32_t __cdecl MovableBlock_TestPush(ITEM *item, int32_t block_height, uint16_t quadrant);\n0x00433E70  0x0225  int32_t __cdecl MovableBlock_TestPull(ITEM *item, int32_t block_height, uint16_t quadrant);\n0x004340B0  0x00BB  void __cdecl Room_AlterFloorHeight(ITEM *item, int32_t height);\n0x00434170  0x0022  void __cdecl MovableBlock_Draw(const ITEM *item);\n0x004341A0  0x006B  void __cdecl Object_DrawUnclippedItem(const ITEM *item);\n\n# game/objects.c\n0x00434210  0x00DB  void __cdecl Earthquake_Control(int16_t item_num);\n0x004342F0  0x003C  void __cdecl FinalCutscene_Control(int16_t item_num);\n0x00434330  0x009D  void __cdecl InitialiseFinalLevel(void);\n0x00434400  0x020F  void __cdecl FinalLevelCounter_Control(int16_t item_num);\n0x00434610  0x00D9  void __cdecl MiniCopter_Control(int16_t item_num);\n0x004346F0  0x007C  void __cdecl DyingMonk_Initialise(int16_t item_num);\n0x00434770  0x0087  void __cdecl DyingMonk_Control(int16_t item_num);\n0x00434800  0x00BD  void __cdecl GongBonger_Control(int16_t item_num);\n0x004348C0  0x00BF  void __cdecl Zipline_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00434980  0x028F  void __cdecl Zipline_Control(int16_t item_num);\n0x00434C10  0x00E3  void __cdecl BigBowl_Control(int16_t item_num);\n0x00434D00  0x007E  void __cdecl Bell_Control(int16_t item_num);\n0x00434D80  0x0075  void __cdecl Window_Initialise(int16_t item_num);\n0x00434E00  0x00C4  void __cdecl Window_Smash(int16_t item_num);\n0x00434ED0  0x0096  void __cdecl Window_1_Control(int16_t item_num);\n0x00434F70  0x00DC  void __cdecl Window_2_Control(int16_t item_num);\n0x00435050  0x0042  void __cdecl Door_Shut(DOORPOS_DATA *d);\n0x004350A0  0x0032  void __cdecl Door_Open(DOORPOS_DATA *d);\n0x004350E0  0x03DC  void __cdecl Door_Initialise(int16_t item_num);\n0x004354C0  0x00C8  void __cdecl Door_Control(int16_t item_num);\n0x00435590  0x00B1  int32_t __cdecl Drawbridge_IsItemOnTop(const ITEM *item, int32_t x, int32_t y);\n0x00435650  0x0036  void __cdecl Drawbridge_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435690  0x003B  void __cdecl Drawbridge_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x004356D0  0x002C  void __cdecl Drawbridge_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00435700  0x0035  void __cdecl Lift_Initialise(int16_t item_num);\n0x00435740  0x00D4  void __cdecl Lift_Control(int16_t item_num);\n0x00435820  0x0179  void __cdecl Lift_FloorCeiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *floor, int32_t *ceiling);\n0x004359A0  0x0035  void __cdecl Lift_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x004359E0  0x0035  void __cdecl Lift_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435A20  0x0016  void __cdecl BridgeFlat_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435A40  0x001B  void __cdecl BridgeFlat_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435A60  0x003B  int32_t __cdecl Bridge_GetOffset(const ITEM *item, int32_t x, int32_t z);\n0x00435AA0  0x0030  void __cdecl BridgeTilt1_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435AD0  0x0035  void __cdecl BridgeTilt1_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435B10  0x002F  void __cdecl BridgeTilt2_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435B40  0x0034  void __cdecl BridgeTilt2_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00435B80  0x010C  void __cdecl Copter_Control(int16_t item_num);\n0x00435C90  0x00D2  void __cdecl General_Control(int16_t item_num);\n0x00435D70  0x008D  void __cdecl Detonator_Control(int16_t item_num);\n\n# game/people.c\n0x00435E00  0x0085  bool __cdecl Creature_CanTargetEnemy(const ITEM *item, const AI_INFO *info);\n0x00435E90  0x003B  void __cdecl Glow_Control(int16_t effect_num);\n0x00435ED0  0x004E  void __cdecl GunFlash_Control(int16_t effect_num);\n0x00435F20  0x0066  int16_t __cdecl Effect_GunShot(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num);\n0x00435F90  0x00B9  int16_t __cdecl Effect_GunHit(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num);\n0x00436050  0x00A7  int16_t __cdecl Effect_GunMiss(int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, int16_t room_num);\n0x00436100  0x01C4  int32_t __cdecl Creature_ShootAtLara(ITEM *item, AI_INFO *info, BITE *gun, int16_t extra_rotation, int32_t damage);\n0x004362D0  0x0043  void __cdecl Cultist1_Initialise(int16_t item_num);\n0x00436320  0x0401  void __cdecl Cultist1_Control(int16_t item_num);\n0x00436750  0x0050  void __cdecl Cultist3_Initialise(int16_t item_num);\n0x004367A0  0x053C  void __cdecl Cultist3_Control(int16_t item_num);\n0x00436D10  0x03CA  void __cdecl Worker1_Control(int16_t item_num);\n0x00437110  0x042A  void __cdecl Worker2_Control(int16_t item_num);\n0x00437570  0x030B  void __cdecl Bandit1_Control(int16_t item_num);\n0x004378B0  0x0408  void __cdecl Bandit2_Control(int16_t item_num);\n0x00437CF0  0x0172  void __cdecl Winston_Control(int16_t item_num);\n\n# game/pickup.c\n0x00437E70  0x0480  void __cdecl Pickup_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x004382F0  0x020A  void __cdecl Switch_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00438500  0x00FC  void __cdecl Switch_CollisionUW(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00438600  0x023B  void __cdecl Detonator_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00438840  0x0223  void __cdecl Keyhole_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00438A80  0x0294  void __cdecl PuzzleHole_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n0x00438D40  0x0039  void __cdecl Switch_Control(int16_t item_num);\n0x00438D80  0x00BD  int32_t __cdecl Switch_Trigger(int16_t item_num, int16_t timer);\n0x00438E40  0x003D  int32_t __cdecl Keyhole_Trigger(int16_t item_num);\n0x00438E80  0x0033  int32_t __cdecl Pickup_Trigger(int16_t item_num);\n0x00438EC0  0x0023  void __cdecl Secret2_Control(int16_t item_num);\n\n# game/rat.c\n0x00438EF0  0x01DC  void __cdecl Mouse_Control(int16_t item_num);\n\n# game/savegame.c\n0x004390E0  0x0062  void __cdecl InitialiseStartInfo(void);\n0x00439150  0x00DB  void __cdecl ModifyStartInfo(int32_t level_num);\n0x00439230  0x0201  void __cdecl CreateStartInfo(int32_t level_num);\n0x00439440  0x052B  void __cdecl CreateSaveGameInfo(void);\n0x00439970  0x085C  void __cdecl ExtractSaveGameInfo(void);\n0x0043A1D0  0x0015  void __cdecl ResetSG(void);\n0x0043A1F0  0x004C  void __cdecl WriteSG(const void *pointer, int32_t size);\n0x0043A240  0x0035  void __cdecl ReadSG(void *pointer, int32_t size);\n\n# game/setup.c\n0x0043A280  0x015F  int32_t __cdecl Level_Initialise(int32_t level_num, int32_t level_type);\n0x0043A3E0  0x0061  void __cdecl InitialiseGameFlags(void);\n0x0043A450  0x0027  void __cdecl InitialiseLevelFlags(void);\n0x0043A480  0x103B  void __cdecl Object_SetupBaddyObjects(void);\n0x0043B4C0  0x05FD  void __cdecl Object_SetupTrapObjects(void);\n0x0043BAC0  0x0C4C  void __cdecl Object_SetupGeneralObjects(void);\n0x0043C710  0x0068  void __cdecl Object_SetupAllObjects(void);\n0x0043C780  0x00CE  void __cdecl GetCarriedItems(void);\n\n# game/shark.c\n0x0043C850  0x0116  void __cdecl Jelly_Control(int16_t item_num);\n0x0043C970  0x021B  void __cdecl Barracuda_Control(int16_t item_num);\n0x0043CBA0  0x027C  void __cdecl Shark_Control(int16_t item_num);\n\n# game/skidoo.c\n0x0043CE30  0x0040  void __cdecl Skidoo_Initialise(int16_t item_num);\n0x0043CE70  0x00E1  int32_t __cdecl Skidoo_CheckGetOn(int16_t item_num, COLL_INFO *coll);\n0x0043CF60  0x00F8  void __cdecl Skidoo_Collision(int16_t item_num, ITEM *litem, COLL_INFO *coll);\n0x0043D060  0x01F9  void __cdecl Skidoo_BaddieCollision(const ITEM *skidoo);\n0x0043D260  0x00B2  int32_t __cdecl Skidoo_TestHeight(const ITEM *item, int32_t z_off, int32_t x_off, XYZ_32 *pos);\n0x0043D320  0x027C  int32_t __cdecl DoShift(ITEM *skidoo, XYZ_32 *pos, XYZ_32 *old);\n0x0043D5A0  0x0054  int32_t __cdecl DoDynamics(int32_t height, int32_t fall_speed, int32_t *y);\n0x0043D600  0x0090  int32_t __cdecl GetCollisionAnim(ITEM *skidoo, XYZ_32 *moved);\n0x0043D690  0x0140  void __cdecl Skidoo_DoSnowEffect(ITEM *skidoo);\n0x0043D7D0  0x049E  int32_t __cdecl Skidoo_Dynamics(ITEM *skidoo);\n0x0043DC70  0x01B6  int32_t __cdecl Skidoo_UserControl(ITEM *skidoo, int32_t height, int32_t *pitch);\n0x0043DE30  0x0106  int32_t __cdecl Skidoo_CheckGetOffOK(int32_t direction);\n0x0043DF40  0x02B9  void __cdecl Skidoo_Animation(ITEM *skidoo, int32_t collide, int32_t dead);\n0x0043E220  0x007C  void __cdecl Skidoo_Explode(const ITEM *skidoo);\n0x0043E2A0  0x0233  int32_t __cdecl Skidoo_CheckGetOff(void);\n0x0043E4E0  0x011B  void __cdecl Skidoo_Guns(void);\n0x0043E600  0x0440  int32_t __cdecl Skidoo_Control(void);\n0x0043EA60  0x02D5  void __cdecl Skidoo_Draw(const ITEM *item);\n0x0043ED40  0x007F  void __cdecl SkidooDriver_Initialise(int16_t item_num);\n0x0043EDD0  0x03E2  void __cdecl SkidooDriver_Control(int16_t rider_num);\n0x0043F1D0  0x0119  void __cdecl SkidooArmed_Push(const ITEM *item, ITEM *lara_item, int32_t radius);\n0x0043F2F0  0x0081  void __cdecl SkidooArmed_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n\n# game/sound.c\n0x0043F380  0x0031  int32_t __cdecl Music_GetRealTrack(int32_t track);\n0x0043F3C0  0x0484  void __cdecl Sound_Effect(int32_t sample_id, const XYZ_32 *pos, uint32_t flags);\n0x0043F860  0x005E  void __cdecl Sound_StopEffect(int32_t sample_id);\n0x0043F8C0  0x0086  void __cdecl Sound_EndScene(void);\n0x0043F950  0x0024  void __cdecl Sound_Shutdown(void);\n0x0043F980  0x002A  void __cdecl Sound_Init(void);\n\n# game/sphere.c\n0x0043F9B0  0x0128  int32_t __cdecl Collide_TestCollision(ITEM *item, const ITEM *lara_item);\n0x0043FAE0  0x02D8  int32_t __cdecl Collide_GetSpheres(const ITEM *item, SPHERE *spheres, bool world_space);\n0x0043FDC0  0x019A  void __cdecl Collide_GetJointAbsPosition(const ITEM *item, XYZ_32 *out_vec, int32_t joint);\n0x0043FF60  0x005D  void __cdecl TeethTrap_Bite(ITEM *item, const BITE *bite);\n\n# game/spider.c\n0x0043FFC0  0x00AC  void __cdecl Spider_Leap(int16_t item_num, int16_t angle);\n0x00440070  0x0206  void __cdecl Spider_Control(int16_t item_num);\n0x00440290  0x01A5  void __cdecl BigSpider_Control(int16_t item_num);\n\n# game/text.c\n0x00440450  0x002C  void __cdecl Text_Init(void);\n0x00440480  0x0105  TEXTSTRING *__cdecl Text_Create(int32_t x, int32_t y, int32_t z, const char *text);\n0x00440590  0x0037  void __cdecl Text_ChangeText(TEXTSTRING *string, const char *text);\n0x004405D0  0x0017  void __cdecl Text_SetScale(TEXTSTRING *string, int32_t scale_h, int32_t scale_v);\n0x004405F0  0x002B  void __cdecl Text_Flash(TEXTSTRING *string, int16_t enable, int16_t rate);\n0x00440620  0x008C  void __cdecl Text_AddBackground(TEXTSTRING *string, int16_t x_size, int16_t y_size, int16_t x_off, int16_t y_off, int16_t z_off, int16_t color, uint16_t *gour_ptr, uint16_t flags);\n0x004406B0  0x0010  void __cdecl Text_RemoveBackground(TEXTSTRING *string);\n0x004406C0  0x0029  void __cdecl Text_AddOutline(TEXTSTRING *string, int16_t enable, int16_t color, uint16_t *gour_ptr, uint16_t flags);\n0x004406F0  0x0010  void __cdecl Text_RemoveOutline(TEXTSTRING *string);\n0x00440700  0x001E  void __cdecl Text_CentreH(TEXTSTRING *string, int16_t enable);\n0x00440720  0x001E  void __cdecl Text_CentreV(TEXTSTRING *string, int16_t enable);\n0x00440740  0x001E  void __cdecl Text_AlignRight(TEXTSTRING *string, int16_t enable);\n0x00440760  0x001E  void __cdecl Text_AlignBottom(TEXTSTRING *string, int16_t enable);\n0x00440780  0x0107  int32_t __cdecl Text_GetWidth(TEXTSTRING *string);\n0x00440890  0x0025  int32_t __cdecl Text_Remove(TEXTSTRING *string);\n0x004408C0  0x0024  int16_t __cdecl Text_GetTextLength(const char *text);\n0x004408F0  0x0027  void __cdecl Text_Draw(void);\n0x00440920  0x0189  void __cdecl Text_DrawBorder(int32_t x, int32_t y, int32_t z, int32_t width, int32_t height);\n0x00440AB0  0x03D2  void __cdecl Text_DrawText(const TEXTSTRING *string);\n0x00440E90  0x0037  uint32_t __cdecl Text_GetScaleH(uint32_t value);\n0x00440ED0  0x0039  uint32_t __cdecl Text_GetScaleV(uint32_t value);\n\n# game/traps.c\n0x00440F10  0x01F4  void __cdecl Mine_Control(int16_t mine_num);\n0x00441110  0x0138  void __cdecl SpikeWall_Control(int16_t item_num);\n0x00441250  0x0115  void __cdecl SpikeCeiling_Control(int16_t item_num);\n0x00441370  0x0086  void __cdecl Hook_Control(int16_t item_num);\n0x00441400  0x0190  void __cdecl Propeller_Control(int16_t item_num);\n0x00441590  0x017B  void __cdecl SpinningBlade_Control(int16_t item_num);\n0x00441710  0x00FE  void __cdecl Icicle_Control(int16_t item_num);\n0x00441810  0x003C  void __cdecl Blade_Initialise(int16_t item_num);\n0x00441850  0x0091  void __cdecl Blade_Control(int16_t item_num);\n0x004418F0  0x0046  void __cdecl KillerStatue_Initialise(int16_t item_num);\n0x00441940  0x0109  void __cdecl KillerStatue_Control(int16_t item_num);\n0x00441A50  0x00DB  void __cdecl Springboard_Control(int16_t item_num);\n0x00441B30  0x003C  void __cdecl RollingBall_Initialise(int16_t item_num);\n0x00441B70  0x0347  void __cdecl RollingBall_Control(int16_t item_num);\n0x00441EC0  0x024A  void __cdecl RollingBall_Collision(int16_t item_num, ITEM *litem, COLL_INFO *coll);\n0x00442110  0x0155  void __cdecl Spikes_Collision(int16_t item_num, ITEM *litem, COLL_INFO *coll);\n0x00442270  0x004F  void __cdecl Trapdoor_Control(int16_t item_num);\n0x004422C0  0x003A  void __cdecl Trapdoor_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00442300  0x003F  void __cdecl Trapdoor_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00442340  0x00A3  int32_t __cdecl Trapdoor_IsItemOnTop(const ITEM *item, int32_t x, int32_t z);\n0x004423F0  0x010A  void __cdecl Pendulum_Control(int16_t item_num);\n0x00442500  0x0105  void __cdecl FallingBlock_Control(int16_t item_num);\n0x00442610  0x003E  void __cdecl FallingBlock_Floor(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x00442650  0x0044  void __cdecl FallingBlock_Ceiling(const ITEM *item, int32_t x, int32_t y, int32_t z, int32_t *out_height);\n0x004426A0  0x00BD  void __cdecl TeethTrap_Control(int16_t item_num);\n0x00442760  0x00E0  void __cdecl FallingCeiling_Control(int16_t item_num);\n0x00442840  0x013E  void __cdecl DartEmitter_Control(int16_t item_num);\n0x00442980  0x0155  void __cdecl Dart_Control(int16_t item_num);\n0x00442AE0  0x004B  void __cdecl DartEffect_Control(int16_t effect_num);\n0x00442B30  0x0090  void __cdecl FlameEmitter_Control(int16_t item_num);\n0x00442BC0  0x0164  void __cdecl Flame_Control(int16_t effect_num);\n0x00442D30  0x0049  void __cdecl Lara_CatchFire(void);\n0x00442D80  0x00E6  void __cdecl Lara_TouchLava(ITEM *item);\n0x00442E70  0x00C5  void __cdecl EmberEmitter_Control(int16_t item_num);\n0x00442F40  0x010B  void __cdecl Ember_Control(int16_t effect_num);\n\n# game/yeti.c\n0x00443050  0x02CA  void __cdecl BirdGuardian_Control(int16_t item_num);\n0x00443350  0x05ED  void __cdecl Yeti_Control(int16_t item_num);\n\n0x00443990  0x01B8  void __cdecl BGND_Make640x480(uint8_t *bitmap, RGB_888 *palette);\n0x00443B50  0x00B9  int32_t __cdecl BGND_AddTexture(int32_t tile_idx, BYTE *bitmap, int32_t pal_index, RGB_888 *bmp_pal);\n0x00443C10  0x0032  void __cdecl BGND_GetPageHandles(void);\n0x00443C50  0x005F  void __cdecl BGND_DrawInGameBlack(void);\n0x00443CB0  0x00DC  void __cdecl BGND_DrawQuad(float sx, float sy, float width, float height, D3DCOLOR color);\n0x00443D90  0x0220  void __cdecl BGND_DrawInGameBackground(void);\n0x00443FB0  0x0251  void __cdecl BGND_DrawTextureTile(int32_t sx, int32_t sy, int32_t width, int32_t height, HWR_TEXTURE_HANDLE tex_source, int32_t tu, int32_t tv, int32_t t_width, int32_t t_height, D3DCOLOR color0, D3DCOLOR color1, D3DCOLOR color2, D3DCOLOR color3);\n0x00444210  0x008B  D3DCOLOR __cdecl BGND_CenterLighting(int32_t x, int32_t y, int32_t width, int32_t height);\n0x004444C0  0x004D  void __cdecl BGND_Free(void);\n0x00444510  0x0030  bool __cdecl BGND_Init(void);\n0x00444540  0x003E  void __cdecl Enumerate3DDevices(DISPLAY_ADAPTER *adapter);\n0x00444570  0x001F  bool __cdecl D3DCreate(void);\n0x004445B0  0x00BD  HRESULT __stdcall Enum3DDevicesCallback(GUID *lpGuid, LPTSTR lpDeviceDescription, LPTSTR lpDeviceName, LPD3DDEVICEDESC lpD3DHWDeviceDesc, LPD3DDEVICEDESC lpD3DHELDeviceDesc, LPVOID lpContext);\n0x00444670  0x0037  bool __cdecl D3DIsSupported(LPD3DDEVICEDESC desc);\n0x004446B0  0x00B9  bool __cdecl D3DSetViewport(void);\n0x00444770  0x01B8  void __cdecl D3DDeviceCreate(LPDDS lpBackBuffer);\n0x00444930  0x006A  void __cdecl Direct3DRelease(void);\n0x00444980  0x0006  bool __cdecl Direct3DInit(void);\n0x00444990  0x0018  sub_444990\n0x004449A0  0x0012  sub_4449A0\n0x004449D0  0x00C6  sub_4449D0\n0x00444AA0  0x0018  sub_444AA0\n0x00444AB0  0x005F  sub_444AB0\n0x00444B20  0x008C  sub_444B20\n0x00444BB0  0x0005  sub_444BB0\n0x00444BC0  0x0001  sub_444BC0\n0x00444BD0  0x0054  bool __cdecl DDrawCreate(LPGUID lpGUID);\n0x00444C30  0x0033  void __cdecl DDrawRelease(void);\n0x00444C70  0x0073  void __cdecl GameWindowCalculateSizeFromClient(int32_t *width, int32_t *height);\n0x00444CF0  0x006A  void __cdecl GameWindowCalculateSizeFromClientByZero(int32_t *width, int32_t *height);\n0x00444D60  0x0041  void __cdecl WinVidSetMinWindowSize(int32_t width, int32_t height);\n0x00444DB0  0x0008  void __cdecl WinVidClearMinWindowSize(void);\n0x00444DC0  0x0041  void __cdecl WinVidSetMaxWindowSize(int32_t width, int32_t height);\n0x00444E10  0x0008  void __cdecl WinVidClearMaxWindowSize(void);\n0x00444E20  0x0048  int32_t __cdecl CalculateWindowWidth(int32_t width, int32_t height);\n0x00444E70  0x0028  int32_t __cdecl CalculateWindowHeight(int32_t width, int32_t height);\n0x00444EA0  0x0104  bool __cdecl WinVidGetMinMaxInfo(LPMINMAXINFO info);\n0x00444FB0  0x0011  HWND __cdecl WinVidFindGameWindow(void);\n0x00444FD0  0x00E2  bool __cdecl WinVidSpinMessageLoop(bool needWait);\n0x004450C0  0x0043  void __cdecl WinVidShowGameWindow(int32_t nCmdShow);\n0x00445110  0x003A  void __cdecl WinVidHideGameWindow(void);\n0x00445150  0x0035  void __cdecl WinVidSetGameWindowSize(int32_t width, int32_t height);\n0x00445190  0x00A7  bool __cdecl ShowDDrawGameWindow(bool active);\n0x00445240  0x0087  bool __cdecl HideDDrawGameWindow(void);\n0x004452D0  0x0044  HRESULT __cdecl DDrawSurfaceCreate(LPDDSDESC dsp, LPDDS *surface);\n0x00445320  0x0046  HRESULT __cdecl DDrawSurfaceRestoreLost(LPDDS surface1, LPDDS surface2, bool blank);\n0x00445370  0x004D  bool __cdecl WinVidClearBuffer(LPDDS surface, LPRECT rect, DWORD fill_color);\n0x004453C0  0x003D  HRESULT __cdecl WinVidBufferLock(LPDDS surface, LPDDSDESC desc, DWORD flags);\n0x00445400  0x0025  HRESULT __cdecl WinVidBufferUnlock(LPDDS surface, LPDDSDESC desc);\n0x00445430  0x0090  bool __cdecl WinVidCopyBitmapToBuffer(LPDDS surface, const BYTE *bitmap);\n0x004454C0  0x0046  DWORD __cdecl GetRenderBitDepth(DWORD dwRGBBitCount);\n0x00445550  0x0071  void __thiscall WinVidGetColorBitMasks(COLOR_BIT_MASKS *bm, LPDDPIXELFORMAT pixel_format);\n0x004455D0  0x0044  void __cdecl BitMaskGetNumberOfBits(uint32_t bit_mask, uint32_t *bit_depth, uint32_t *bit_offset);\n0x00445620  0x0061  DWORD __cdecl CalculateCompatibleColor(COLOR_BIT_MASKS *mask, int32_t red, int32_t green, int32_t blue, int32_t alpha);\n0x00445690  0x008C  bool __cdecl WinVidGetDisplayMode(DISPLAY_MODE *disp_mode);\n0x00445720  0x0088  bool __cdecl WinVidGoFullScreen(DISPLAY_MODE *disp_mode);\n0x004457B0  0x010B  bool __cdecl WinVidGoWindowed(int32_t width, int32_t height, DISPLAY_MODE *dispMode);\n0x004458C0  0x00D5  void __cdecl WinVidSetDisplayAdapter(DISPLAY_ADAPTER *disp_adapter);\n0x004459A0  0x0045  bool __thiscall CompareVideoModes(const DISPLAY_MODE *mode1, const DISPLAY_MODE *mode2);\n0x004459F0  0x0053  bool __cdecl WinVidGetDisplayModes(void);\n0x00445A50  0x03B1  HRESULT __stdcall EnumDisplayModesCallback(LPDDSDESC lpDDSurfaceDesc, LPVOID lpContext);\n0x00445E10  0x0040  bool __cdecl WinVidInit(void);\n0x00445E50  0x00AF  bool __cdecl WinVidGetDisplayAdapters(void);\n0x00445F00  0x0013  void __thiscall S_FlaggedString_Delete(STRING_FLAGGED *string);\n0x00445F20  0x001A  bool __cdecl EnumerateDisplayAdapters(DISPLAY_ADAPTER_LIST *displayAdapterList);\n0x00445F40  0x01BE  BOOL __stdcall EnumDisplayAdaptersCallback(GUID *lpGUID, LPTSTR lpDriverDescription, LPTSTR lpDriverName, LPVOID lpContext);\n0x00446100  0x0035  void __thiscall S_FlaggedString_InitAdapter(DISPLAY_ADAPTER *adapter);\n0x00446140  0x006A  bool __cdecl WinVidRegisterGameWindowClass(void);\n0x004461B0  0x049F  LRESULT __stdcall WinVidGameWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);\n0x004467C0  0x01C0  void __cdecl WinVidResizeGameWindow(HWND hWnd, int32_t edge, LPRECT rect);\n0x004469A0  0x00BC  bool __cdecl WinVidCheckGameWindowPalette(HWND hWnd);\n0x00446A60  0x00C6  bool __cdecl WinVidCreateGameWindow(void);\n0x00446B30  0x0022  void __cdecl WinVidFreeWindow(void);\n0x00446B60  0x004D  void __cdecl WinVidExitMessage(void);\n0x00446BB0  0x0048  DISPLAY_ADAPTER_NODE *__cdecl WinVidGetDisplayAdapter(GUID *lpGuid);\n0x00446C00  0x0374  void __cdecl WinVidStart(void);\n0x00446F80  0x0013  void __cdecl WinVidFinish(void);\n0x00446FA0  0x000D  void __thiscall DisplayModeListInit(DISPLAY_MODE_LIST *pList);\n0x00446FB0  0x0032  void __thiscall DisplayModeListDelete(DISPLAY_MODE_LIST *pList);\n0x00446FF0  0x0012  DISPLAY_MODE *__thiscall InsertDisplayMode(DISPLAY_MODE_LIST *modeList, DISPLAY_MODE_NODE *before);\n0x00447010  0x0048  DISPLAY_MODE *__thiscall InsertDisplayModeInListHead(DISPLAY_MODE_LIST *modeList);\n0x00447060  0x004A  DISPLAY_MODE *__thiscall InsertDisplayModeInListTail(DISPLAY_MODE_LIST *modeList);\n0x004470B0  0x0018  sub_4470B0\n0x004470C0  0x0012  sub_4470C0\n0x004470F0  0x0068  sub_4470F0\n0x00447160  0x0018  sub_447160\n0x00447170  0x0039  sub_447170\n0x004471C0  0x002F  sub_4471C0\n0x004471F0  0x0022  bool __cdecl DInputCreate(void);\n0x00447220  0x001A  void __cdecl DInputRelease(void);\n0x00447240  0x005A  void __cdecl WinInReadKeyboard(LPVOID lpInputData);\n0x004472A0  0x00F3  DWORD __cdecl WinInReadJoystick(int32_t *x, int32_t *y);\n0x004473A0  0x0005  sub_4473A0\n0x004473B0  0x007F  bool __cdecl WinInputInit(void);\n0x00447430  0x0024  bool __cdecl DInputEnumDevices(JOYSTICK_LIST *joystickList);\n0x00447460  0x00E8  BOOL __stdcall DInputEnumDevicesCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef);\n0x00447550  0x001F  void __thiscall S_FlaggedString_Create(STRING_FLAGGED *string, int32_t size);\n0x00447570  0x004E  JOYSTICK_NODE *__cdecl GetJoystick(GUID *lpGuid);\n0x004475C0  0x00C9  void __cdecl DInputKeyboardCreate(void);\n0x00447690  0x0029  void __cdecl DInputKeyboardRelease(void);\n0x004476C0  0x00E4  bool __cdecl DInputJoystickCreate(void);\n0x004477B0  0x002D  void __cdecl WinInStart(void);\n0x004477E0  0x000F  void __cdecl WinInFinish(void);\n0x004477F0  0x0017  void __cdecl WinInRunControlPanel(HWND hWnd);\n0x00447810  0x0062  void __cdecl IncreaseScreenSize(void);\n0x00447880  0x0062  void __cdecl DecreaseScreenSize(void);\n0x004478F0  0x009F  void __cdecl setup_screen_size(void);\n0x00447990  0x0034  void __cdecl TempVideoAdjust(int32_t hires, double sizer);\n0x004479D0  0x0039  void __cdecl TempVideoRemove(void);\n0x00447A10  0x0035  void __cdecl S_FadeInInventory(BOOL isFade);\n0x00447A50  0x0027  void __cdecl S_FadeOutInventory(BOOL isFade);\n0x00447A80  0x0018  sub_447A80\n0x00447A90  0x0012  sub_447A90\n0x00447AC0  0x0068  sub_447AC0\n0x00447B30  0x0018  sub_447B30\n0x00447B40  0x0039  sub_447B40\n0x00447B90  0x002F  sub_447B90\n0x00447BC0  0x0048  const SOUND_ADAPTER_NODE *__cdecl S_Audio_Sample_GetAdapter(GUID *guid);\n0x00447C10  0x002E  void __cdecl S_Audio_Sample_CloseAllTracks(void);\n0x00447C40  0x010E  bool __cdecl S_Audio_Sample_Load(int32_t sample_id, LPWAVEFORMATEX format, const void *data, int32_t data_size);\n0x00447D50  0x0045  bool __cdecl S_Audio_Sample_IsTrackPlaying(int32_t track_id);\n0x00447DA0  0x00E7  int32_t __cdecl S_Audio_Sample_Play(int32_t sample_id, int32_t volume, int32_t pitch, int32_t pan, int32_t flags);\n0x00447E90  0x0039  int32_t __cdecl S_Audio_Sample_GetFreeTrackIndex(void);\n0x00447ED0  0x002C  void __cdecl S_Audio_Sample_AdjustTrackVolumeAndPan(int32_t track_id, int32_t volume, int32_t pan);\n0x00447F00  0x0031  void __cdecl S_Audio_Sample_AdjustTrackPitch(int32_t track_id, int32_t pitch);\n0x00447F40  0x002F  void __cdecl S_Audio_Sample_CloseTrack(int32_t track_id);\n0x00447FA0  0x0005  sub_447FA0\n0x00447FB0  0x009C  bool __cdecl S_Audio_Sample_Init(void);\n0x00448050  0x001A  bool __cdecl S_Audio_Sample_DSoundEnumerate(SOUND_ADAPTER_LIST *adapter_list);\n0x00448070  0x00E2  BOOL __stdcall S_Audio_Sample_DSoundEnumCallback(LPGUID guid, LPCTSTR description, LPCTSTR module, LPVOID context);\n0x00448160  0x017C  void __cdecl S_Audio_Sample_Init2(HWND hwnd);\n0x004482E0  0x001C  bool __cdecl S_Audio_Sample_DSoundCreate(GUID *guid);\n0x00448300  0x00C4  bool __cdecl S_Audio_Sample_DSoundBufferTest(void);\n0x004483D0  0x002A  void __cdecl S_Audio_Sample_Shutdown(void);\n0x00448400  0x0006  bool __cdecl S_Audio_Sample_IsEnabled(void);\n0x00448410  0x0005  sub_448410\n0x00448420  0x0001  sub_448420\n0x00448430  0x013B  void __cdecl CreateScreenBuffers(void);\n0x00448570  0x0094  void __cdecl CreatePrimarySurface(void);\n0x00448610  0x0098  void __cdecl CreateBackBuffer(void);\n0x004486B0  0x009D  void __cdecl CreateClipper(void);\n0x00448750  0x00D3  void __cdecl CreateWindowPalette(void);\n0x00448830  0x00BC  void __cdecl CreateZBuffer(void);\n0x004488F0  0x002B  DWORD __cdecl GetZBufferDepth(void);\n0x00448920  0x00A1  void __cdecl CreateRenderBuffer(void);\n0x004489D0  0x0070  void __cdecl CreatePictureBuffer(void);\n0x00448A40  0x01A4  void __cdecl ClearBuffers(DWORD flags, DWORD fill_color);\n0x00448BF0  0x013C  void __cdecl RestoreLostBuffers(void);\n0x00448D30  0x00CF  void __cdecl UpdateFrame(bool need_run_message_loop, LPRECT rect);\n0x00448E00  0x003B  void __cdecl WaitPrimaryBufferFlip(void);\n0x00448E40  0x0003  bool __cdecl RenderInit(void);\n0x00448E50  0x03A5  void __cdecl RenderStart(bool is_reset);\n0x00449200  0x00E6  void __cdecl RenderFinish(bool need_to_clear_textures);\n0x004492F0  0x0204  bool __cdecl ApplySettings(APP_SETTINGS *new_settings);\n0x00449500  0x0105  void __cdecl FmvBackToGame(void);\n0x00449610  0x023A  void __cdecl GameApplySettings(APP_SETTINGS *new_settings);\n0x00449850  0x0067  void __cdecl UpdateGameResolution(void);\n0x004498C0  0x000C  LPCTSTR __cdecl DecodeErrorMessage(DWORD error_code);\n0x004498D0  0x0049  BOOL __cdecl ReadFileSync(HANDLE handle, LPVOID lpBuffer, DWORD nBytesToRead, LPDWORD lpnBytesRead, LPOVERLAPPED lpOverlapped);\n0x00449920  0x0188  BOOL __cdecl Level_LoadTexturePages(HANDLE handle);\n0x00449AB0  0x03A0  BOOL __cdecl Level_LoadRooms(HANDLE handle);\n0x00449E50  0x0097  void __cdecl AdjustTextureUVs(bool reset_uv_add);\n0x00449EF0  0x057E  BOOL __cdecl Level_LoadObjects(HANDLE handle);\n0x0044A470  0x0135  BOOL __cdecl Level_LoadSprites(HANDLE handle);\n0x0044A5B0  0x01D6  BOOL __cdecl Level_LoadItems(HANDLE handle);\n0x0044A790  0x0188  BOOL __cdecl Level_LoadDepthQ(HANDLE handle);\n0x0044A920  0x0071  BOOL __cdecl Level_LoadPalettes(HANDLE handle);\n0x0044A9A0  0x0060  BOOL __cdecl Level_LoadCameras(HANDLE handle);\n0x0044AA00  0x0060  BOOL __cdecl Level_LoadSoundEffects(HANDLE handle);\n0x0044AA60  0x0221  BOOL __cdecl Level_LoadBoxes(HANDLE handle);\n0x0044AC90  0x0055  BOOL __cdecl Level_LoadAnimatedTextures(HANDLE handle);\n0x0044ACF0  0x0079  BOOL __cdecl Level_LoadCinematic(HANDLE handle);\n0x0044AD70  0x008A  BOOL __cdecl Level_LoadDemo(HANDLE handle);\n0x0044AE00  0x009A  void __cdecl Level_LoadDemoExternal(LPCTSTR level_name);\n0x0044AEA0  0x0265  BOOL __cdecl Level_LoadSamples(HANDLE handle);\n0x0044B110  0x0036  void __cdecl ChangeFileNameExtension(char *file_name, const char *file_ext);\n0x0044B150  0x0026  LPCTSTR __cdecl GetFullPath(LPCTSTR file_name);\n0x0044B180  0x00E0  BOOL __cdecl SelectDrive(void);\n0x0044B260  0x024A  bool __cdecl Level_Load(const char *file_name, int32_t level_num);\n0x0044B4B0  0x0018  BOOL __cdecl S_LoadLevelFile(LPCTSTR file_name, int32_t level_num, GAME_FLOW_LEVEL_TYPE level_type);\n0x0044B4D0  0x002A  void __cdecl S_UnloadLevelFile(void);\n0x0044B500  0x0014  void __cdecl S_AdjustTexelCoordinates(void);\n0x0044B520  0x00C4  BOOL __cdecl S_ReloadLevelGraphics(BOOL reload_palettes, BOOL reload_tex_pages);\n0x0044B5F0  0x00C6  BOOL __cdecl GF_ReadStringTable(DWORD count, char **string_table, char **string_buf, LPDWORD buf_size, HANDLE handle);\n0x0044B6C0  0x06D1  BOOL __cdecl GF_LoadFromFile(const char *file_name);\n0x0044BDA0  0x006B  bool __cdecl PlayFMV(const char *file_name);\n0x0044BE10  0x02E0  void __cdecl WinPlayFMV(const char *file_name, bool is_playback);\n0x0044C0F0  0x0048  void __cdecl WinStopFMV(bool is_playback);\n0x0044C140  0x0088  bool __cdecl IntroFMV(const char *file_name1, const char *file_name2);\n0x0044C1D0  0x0023  uint16_t __cdecl S_FindColor(int32_t red, int32_t green, int32_t blue);\n0x0044C200  0x0035  void __cdecl S_DrawScreenLine(int32_t x, int32_t y, int32_t z, int32_t x_len, int32_t y_len, BYTE color_idx, D3DCOLOR *gour, uint16_t flags);\n0x0044C240  0x0116  void __cdecl S_DrawScreenBox(int32_t sx, int32_t sy, int32_t z, int32_t width, int32_t height, BYTE color_idx, const GOURAUD_OUTLINE *gour, uint16_t flags);\n0x0044C360  0x002E  void __cdecl S_DrawScreenFBox(int32_t sx, int32_t sy, int32_t z, int32_t width, int32_t height, BYTE color_idx, const GOURAUD_FILL *gour, uint16_t flags);\n0x0044C390  0x000F  void __cdecl S_FinishInventory(void);\n0x0044C3A0  0x0043  void __cdecl S_FadeToBlack(void);\n0x0044C3F0  0x0057  void __cdecl S_Wait(int32_t timeout, BOOL input_check);\n0x0044C450  0x000E  bool __cdecl S_PlayFMV(const char *file_name);\n0x0044C460  0x0013  bool __cdecl S_IntroFMV(const char *file_name1, const char *file_name2);\n0x0044C480  0x0144  int16_t __cdecl Game_Start(int32_t level_num, GAME_FLOW_LEVEL_TYPE level_type);\n0x0044C5D0  0x009A  int32_t __cdecl Game_Loop(bool demo_mode);\n0x0044C670  0x0006  int32_t __cdecl LevelCompleteSequence(void);\n0x0044C680  0x01C2  int32_t __cdecl LevelStats(int32_t level_num);\n0x0044C850  0x0113  int32_t __cdecl GameStats(int32_t level_num);\n0x0044C970  0x001E  int32_t __cdecl Random_GetControl(void);\n0x0044C990  0x000A  void __cdecl Random_SeedControl(int32_t seed);\n0x0044C9A0  0x001E  int32_t __cdecl Random_GetDraw(void);\n0x0044C9C0  0x000A  void __cdecl Random_SeedDraw(int32_t seed);\n0x0044C9D0  0x0044  void __cdecl GetValidLevelsList(REQUEST_INFO *req);\n0x0044CA20  0x004C  void __cdecl GetSavedGamesList(REQUEST_INFO *req);\n0x0044CA70  0x0233  void __cdecl DisplayCredits(void);\n0x0044CCB0  0x0165  BOOL __cdecl S_FrontEndCheck(void);\n0x0044CE20  0x0114  int32_t __cdecl S_SaveGame(const void *save_data, uint32_t save_size, int32_t slot_num);\n0x0044CF40  0x0096  int32_t __cdecl S_LoadGame(void *save_data, uint32_t save_size, int32_t slot_num);\n0x0044CFE0  0x0128  void __cdecl HWR_InitState(void);\n0x0044D110  0x0029  void __cdecl HWR_ResetTexSource(void);\n0x0044D140  0x002B  void __cdecl HWR_ResetColorKey(void);\n0x0044D170  0x0059  void __cdecl HWR_ResetZBuffer(void);\n0x0044D1D0  0x0024  void __cdecl HWR_TexSource(HWR_TEXTURE_HANDLE tex_source);\n0x0044D200  0x004A  void __cdecl HWR_EnableColorKey(bool state);\n0x0044D250  0x0082  void __cdecl HWR_EnableZBuffer(bool z_write_enable, bool z_enable);\n0x0044D2E0  0x0016  void __cdecl HWR_BeginScene(void);\n0x0044D310  0x016C  void __cdecl HWR_DrawPolyList(void);\n0x0044D490  0x008E  void __cdecl HWR_LoadTexturePages(int32_t pages_count, void *pages_buf, RGB_888 *palette);\n0x0044D520  0x004A  void __cdecl HWR_FreeTexturePages(void);\n0x0044D570  0x0035  void __cdecl HWR_GetPageHandles(void);\n0x0044D5B0  0x0019  bool __cdecl HWR_VertexBufferFull(void);\n0x0044D5E0  0x0022  bool __cdecl HWR_Init(void);\n0x0044D610  0x005C  BOOL __cdecl S_InitialiseSystem(void);\n0x0044D670  0x0011  void __cdecl GameBuf_Shutdown(void);\n0x0044D690  0x0021  void __cdecl GameBuf_Reset(void);\n0x0044D6C0  0x006C  void *__cdecl GameBuf_Alloc(size_t alloc_size, GAME_BUFFER buf_index);\n0x0044D740  0x0034  void __cdecl GameBuf_Free(size_t free_size);\n0x0044D780  0x00E8  void __cdecl Output_CalculateWibbleTable(void);\n0x0044D870  0x007F  void __cdecl Random_Seed(void);\n0x0044D8F0  0x0120  BOOL __cdecl S_Input_Key(KEYMAP keymap);\n0x0044DA10  0x0AC4  bool __cdecl Input_Update(void);\n0x0044E4E0  0x003C  int32_t __cdecl RenderErrorBox(int32_t error_code);\n0x0044E520  0x01D6  int32_t __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int32_t nShowCmd);\n0x0044E6F0  0x0001  sub_44E6F0\n0x0044E700  0x0064  int32_t __cdecl GameInit(bool skip_cd_init);\n0x0044E770  0x0055  void __cdecl Shell_Cleanup(void);\n0x0044E7A0  0x007C  int32_t __cdecl WinGameStart(void);\n0x0044E820  0x0039  void __cdecl Shell_Shutdown(void);\n0x0044E860  0x0017  sub_44E860\n0x0044E880  0x0010  sub_44E880\n0x0044E890  0x003C  void __cdecl Shell_ExitSystem(const char *message);\n0x0044E8E0  0x010F  void __cdecl ScreenshotPCX(void);\n0x0044E9F0  0x00AE  DWORD __cdecl CompPCX(BYTE *bitmap, DWORD width, DWORD height, RGB_888 *palette, BYTE **pcx_data);\n0x0044EAA0  0x00D2  DWORD __cdecl EncodeLinePCX(BYTE *src, DWORD width, BYTE *dst);\n0x0044EB80  0x003E  DWORD __cdecl EncodePutPCX(BYTE value, BYTE num, BYTE *buffer);\n0x0044EBC0  0x01F5  void __cdecl Screenshot(LPDDS screen);\n0x0044EDC0  0x007F  void __cdecl Option_DoInventory(INVENTORY_ITEM *item);\n0x0044EED0  0x0648  void __cdecl Option_Passport(INVENTORY_ITEM *item);\n0x0044F520  0x02DA  void __cdecl Option_Detail(INVENTORY_ITEM *item);\n0x0044F800  0x049D  void __cdecl Option_Sound(INVENTORY_ITEM *item);\n0x0044FCA0  0x00C0  void __cdecl Option_Compass(INVENTORY_ITEM *item);\n0x0044FD60  0x007E  void __cdecl Option_Controls_FlashConflicts(void);\n0x0044FDE0  0x0040  void __cdecl Option_Controls_DefaultConflict(void);\n0x0044FE20  0x06F4  void __cdecl Option_Controls(INVENTORY_ITEM *item);\n0x00450530  0x04D0  void __cdecl Option_Controls_ShowControls(void);\n0x00450A00  0x0096  void __cdecl Option_Controls_UpdateText(void);\n0x00450AA0  0x003B  void __cdecl S_RemoveCtrlText(void);\n0x00450AE0  0x0006  int32_t __cdecl GetRenderHeight(void);\n0x00450AF0  0x0006  int32_t __cdecl GetRenderWidth(void);\n0x00450B00  0x00E4  void __cdecl S_InitialisePolyList(BOOL clear_back_buffer);\n0x00450BF0  0x0036  DWORD __cdecl S_DumpScreen(void);\n0x00450C30  0x000B  void __cdecl S_ClearScreen(void);\n0x00450C40  0x0037  void __cdecl S_InitialiseScreen(GAME_FLOW_LEVEL_TYPE level_type);\n0x00450C80  0x0089  void __cdecl S_OutputPolyList(void);\n0x00450CC0  0x0270  int32_t __cdecl Output_GetObjectBounds(const BOUNDS_16 *bounds);\n0x00450F30  0x0046  void __cdecl S_InsertBackPolygon(int32_t x0, int32_t y0, int32_t x1, int32_t y1);\n0x00450F80  0x01F1  void __cdecl Output_InsertShadow(int16_t radius, const BOUNDS_16 *bounds, const ITEM *item);\n0x00451180  0x02F6  void __cdecl Output_CalculateLight(int32_t x, int32_t y, int32_t z, int16_t room_num);\n0x00451480  0x0031  void __cdecl Output_CalculateStaticLight(int16_t adder);\n0x004514C0  0x0124  void __cdecl Output_CalculateStaticMeshLight(int32_t x, int32_t y, int32_t z, int32_t shade_1, int32_t shade_2, ROOM *room);\n0x004515F0  0x0206  void __cdecl Output_LightRoom(ROOM *room);\n0x00451800  0x01CC  void __cdecl Output_DrawHealthBar(int32_t percent);\n0x004519D0  0x01F6  void __cdecl Output_DrawAirBar(int32_t percent);\n0x00451BD0  0x00C0  void __cdecl Output_DoAnimateTextures(int32_t ticks);\n0x00451C90  0x0051  void __cdecl Output_SetupBelowWater(bool underwater);\n0x00451CF0  0x0021  void __cdecl Output_SetupAboveWater(bool underwater);\n0x00451D20  0x00B1  void __cdecl Output_AnimateTextures(int32_t ticks);\n0x00451DE0  0x0105  void __cdecl S_DisplayPicture(const char *file_name, BOOL is_title);\n0x00451EF0  0x007E  void __cdecl S_SyncPictureBufferPalette(void);\n0x00451F70  0x001C  void __cdecl S_DontDisplayPicture(void);\n0x00451F80  0x000D  void __cdecl ScreenDump(void);\n0x00451F90  0x0010  void __cdecl ScreenPartialDump(void);\n0x00451FA0  0x01C9  void __cdecl FadeToPal(int32_t fade_value, RGB_888 *palette);\n0x00452170  0x0026  void __cdecl ScreenClear(bool is_phd_win_size);\n0x004521A0  0x00AB  void __cdecl S_CopyScreenToBuffer(void);\n0x00452250  0x0254  void __cdecl S_CopyBufferToScreen(void);\n0x004522A0  0x00FA  BOOL __cdecl DecompPCX(const uint8_t *pcx, size_t pcx_size, LPBYTE pic, RGB_888 *pal);\n0x004523A0  0x0005  sub_4523A0\n0x004523B0  0x0001  sub_4523B0\n0x004523C0  0x004E  bool __cdecl OpenGameRegistryKey(LPCTSTR key);\n0x00452410  0x0005  LONG __cdecl CloseGameRegistryKey(void);\n0x00452420  0x0262  bool __cdecl SE_WriteAppSettings(APP_SETTINGS *settings);\n0x00452690  0x0348  int32_t __cdecl SE_ReadAppSettings(APP_SETTINGS *settings);\n0x004529E0  0x00D7  bool __cdecl SE_GraphicsTestStart(void);\n0x00452AB0  0x0014  void __cdecl SE_GraphicsTestFinish(void);\n0x00452AD0  0x0003  int32_t __cdecl SE_GraphicsTestExecute(void);\n0x00452AE0  0x0057  int32_t __cdecl SE_GraphicsTest(void);\n0x00452B40  0x00C7  bool __cdecl SE_SoundTestStart(void);\n0x00452C00  0x0005  void __cdecl SE_SoundTestFinish(void);\n0x00452C10  0x003D  int32_t __cdecl SE_SoundTestExecute(void);\n0x00452C50  0x0057  int32_t __cdecl SE_SoundTest(void);\n0x00452CB0  0x003E  int32_t __stdcall SE_PropSheetCallback(HWND hwndDlg, UINT uMsg, LPARAM lParam);\n0x00452CF0  0x005D  LRESULT __stdcall SE_NewPropSheetWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x00452D50  0x02DE  bool __cdecl SE_ShowSetupDialog(HWND hParent, bool isDefault);\n0x00453030  0x0351  INT_PTR __stdcall SE_GraphicsDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x004533F0  0x01DC  void __cdecl SE_GraphicsDlgFullScreenModesUpdate(HWND hwndDlg);\n0x004535E0  0x0017  void __cdecl SE_GraphicsAdapterSet(HWND hwndDlg, DISPLAY_ADAPTER_NODE *adapter);\n0x00453600  0x0735  void __cdecl SE_GraphicsDlgUpdate(HWND hwndDlg);\n0x00453D40  0x017C  void __cdecl SE_GraphicsDlgInit(HWND hwndDlg);\n0x00453EC0  0x0149  INT_PTR __stdcall SE_SoundDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x00454050  0x000A  void __cdecl SE_SoundAdapterSet(HWND hwndDlg, SOUND_ADAPTER_NODE *adapter);\n0x00454060  0x011B  void __cdecl SE_SoundDlgUpdate(HWND hwndDlg);\n0x00454180  0x00BE  void __cdecl SE_SoundDlgInit(HWND hwndDlg);\n0x00454240  0x0106  INT_PTR __stdcall SE_ControlsDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x00454350  0x000A  void __cdecl SE_ControlsJoystickSet(HWND hwndDlg, JOYSTICK_NODE *joystick);\n0x00454360  0x0068  void __cdecl SE_ControlsDlgUpdate(HWND hwndDlg);\n0x004543D0  0x00BD  void __cdecl SE_ControlsDlgInit(HWND hwndDlg);\n0x00454490  0x008A  INT_PTR __stdcall SE_OptionsDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x00454520  0x0234  void __cdecl SE_OptionsDlgUpdate(HWND hwndDlg);\n0x00454760  0x004B  void __cdecl SE_OptionsStrCat(LPTSTR *dstString, bool isEnabled, bool *isNext, LPCTSTR srcString);\n0x004547B0  0x00DC  INT_PTR __stdcall SE_AdvancedDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x004548B0  0x0093  void __cdecl SE_AdvancedDlgUpdate(HWND hwndDlg);\n0x00454950  0x000E  void __cdecl SE_AdvancedDlgInit(HWND hwndDlg);\n0x00454960  0x0011  HWND __cdecl SE_FindSetupDialog(void);\n0x00454980  0x02D0  BOOL __cdecl Shell_Main(void);\n0x00454C50  0x0110  int16_t __cdecl TitleSequence(void);\n0x00454D60  0x032D  void __cdecl Lara_Cheat_CheckKeys(void);\n0x004550C0  0x007D  void __cdecl S_SaveSettings(void);\n0x00455140  0x00DB  void __cdecl S_LoadSettings(void);\n0x00455220  0x0046  int32_t __cdecl S_Audio_Sample_OutPlay(int32_t sample_id, uint16_t volume, int32_t pitch, int32_t pan);\n0x00455270  0x002A  int32_t __cdecl S_Audio_Sample_CalculateSampleVolume(int32_t volume);\n0x004552A0  0x0026  int32_t __cdecl S_Audio_Sample_CalculateSamplePan(int16_t pan);\n0x004552D0  0x0046  int32_t __cdecl S_Audio_Sample_OutPlayLooped(int32_t track_id, uint16_t volume, int32_t pitch, int32_t pan);\n0x00455320  0x0039  void __cdecl S_Audio_Sample_OutSetPanAndVolume(int32_t track_id, int32_t pan, uint16_t volume);\n0x00455360  0x001C  void __cdecl S_Audio_Sample_OutSetPitch(int32_t track_id, int32_t pitch);\n0x00455380  0x000A  void __cdecl Sound_SetMasterVolume(int32_t volume);\n0x00455390  0x0017  void __cdecl S_Audio_Sample_OutCloseTrack(int32_t track_id);\n0x004553B0  0x003C  void __cdecl S_Audio_Sample_OutCloseAllTracks(void);\n0x004553C0  0x001F  BOOL __cdecl S_Audio_Sample_OutIsTrackPlaying(int32_t track_id);\n0x004553E0  0x0077  bool __cdecl Music_Init(void);\n0x00455460  0x0051  void __cdecl Music_Shutdown(void);\n0x00455500  0x006F  void __cdecl Music_Play(int16_t track_id, bool is_looped);\n0x00455570  0x0039  void __cdecl Music_Stop(void);\n0x004555B0  0x0084  bool __cdecl Music_PlaySynced(int32_t track_id);\n0x00455640  0x0061  int32_t __cdecl Music_GetFrames(void);\n0x004556B0  0x0092  void __cdecl Music_SetVolume(int32_t volume);\n0x004557A0  0x0137  void __cdecl CopyBitmapPalette(RGB_888 *src_pal, BYTE *src_bitmap, int32_t bitmap_size, RGB_888 *dest_pal);\n0x004558E0  0x00C8  BYTE __cdecl FindNearestPaletteEntry(RGB_888 *palette, int32_t red, int32_t green, int32_t blue, bool ignore_sys_palette);\n0x004559B0  0x00AE  void __cdecl SyncSurfacePalettes(void *src_data, int32_t width, int32_t height, int32_t src_pitch, RGB_888 *src_palette, void *dst_data, int32_t dst_pitch, RGB_888 *dst_palette, bool preserve_sys_palette);\n0x00455A60  0x0087  int32_t __cdecl CreateTexturePalette(const RGB_888 *pal);\n0x00455AF0  0x001C  int32_t __cdecl GetFreePaletteIndex(void);\n0x00455B10  0x0023  void __cdecl FreePalette(int32_t palette_idx);\n0x00455B40  0x0012  void __cdecl SafeFreePalette(int32_t palette_idx);\n0x00455B90  0x006A  int32_t __cdecl CreateTexturePage(int32_t width, int32_t height, LPDIRECTDRAWPALETTE palette);\n0x00455C00  0x001C  int32_t __cdecl GetFreeTexturePageIndex(void);\n0x00455C20  0x0098  bool __cdecl CreateTexturePageSurface(TEXPAGE_DESC *desc);\n0x00455CC0  0x0174  bool __cdecl TexturePageInit(TEXPAGE_DESC *page);\n0x00455E40  0x0025  LPDIRECT3DTEXTURE2 __cdecl Create3DTexture(LPDDS surface);\n0x00455E70  0x0020  void __cdecl SafeFreeTexturePage(int32_t page_idx);\n0x00455E90  0x0032  void __cdecl FreeTexturePage(int32_t page_idx);\n0x00455ED0  0x003B  void __cdecl TexturePageReleaseVidMemSurface(TEXPAGE_DESC *page);\n0x00455F10  0x0026  void __cdecl FreeTexturePages(void);\n0x00455F40  0x00A2  bool __cdecl LoadTexturePage(int32_t page_idx, bool reset);\n0x00455FF0  0x0035  bool __cdecl ReloadTextures(bool reset);\n0x00456030  0x003E  HWR_TEXTURE_HANDLE __cdecl GetTexturePageHandle(int32_t page_idx);\n0x00456070  0x00F5  int32_t __cdecl AddTexturePage8(int32_t width, int32_t height, const uint8_t *page_buf, int32_t pal_idx);\n0x00456170  0x0196  int32_t __cdecl AddTexturePage16(int32_t width, int32_t height, const uint8_t *page_buf);\n0x00456310  0x011A  HRESULT __stdcall EnumTextureFormatsCallback(LPDDSDESC lpDdsd, LPVOID lpContext);\n0x00456430  0x0025  HRESULT __cdecl EnumerateTextureFormats(void);\n0x00456460  0x0030  void __cdecl CleanupTextures(void);\n0x00456470  0x001F  bool __cdecl InitTextures(void);\n0x00456490  0x0040  void __cdecl UpdateTicks(void);\n0x004564D0  0x0051  bool __cdecl TIME_Init(void);\n0x00456530  0x0058  DWORD __cdecl Sync(void);\n0x00456590  0x0036  LPVOID __cdecl UT_LoadResource(LPCTSTR lpName, LPCTSTR lpType);\n0x004565D0  0x0060  void __cdecl UT_InitAccurateTimer(void);\n0x00456630  0x004E  double __cdecl UT_Microseconds(void);\n0x00456680  0x006F  BOOL __cdecl UT_CenterWindow(HWND hWnd);\n0x004566F0  0x002C  LPTSTR __cdecl UT_FindArg(LPCTSTR str);\n0x00456720  0x0018  int32_t __cdecl UT_MessageBox(LPCTSTR lpText, HWND hWnd);\n0x00456740  0x0042  int32_t __cdecl UT_ErrorBox(UINT uID, HWND hWnd);\n0x00456790  0x0051  LPCTSTR __cdecl GuidBinaryToString(GUID *guid);\n0x004567F0  0x00AA  bool __cdecl GuidStringToBinary(LPCTSTR lpString, GUID *guid);\n0x004568A0  0x0030  BOOL __cdecl OpenRegistryKey(LPCTSTR lpSubKey);\n0x004568D0  0x000F  bool __cdecl IsNewRegistryKeyCreated(void);\n0x004568E0  0x000D  LONG __cdecl CloseRegistryKey(void);\n0x004568F0  0x001E  LONG __cdecl SetRegistryDwordValue(LPCTSTR lpValueName, DWORD value);\n0x00456910  0x002A  LONG __cdecl SetRegistryBoolValue(LPCTSTR lpValueName, bool value);\n0x00456940  0x0036  LONG __cdecl SetRegistryFloatValue(LPCTSTR lpValueName, double value);\n0x00456980  0x0037  LONG __cdecl SetRegistryBinaryValue(LPCTSTR lpValueName, LPBYTE value, DWORD valueSize);\n0x004569C0  0x004A  LONG __cdecl SetRegistryStringValue(LPCTSTR lpValueName, LPCTSTR value, int32_t length);\n0x00456A10  0x0013  LONG __cdecl DeleteRegistryValue(LPCTSTR lpValueName);\n0x00456A30  0x005E  bool __cdecl GetRegistryDwordValue(LPCTSTR lpValueName, DWORD *pValue, DWORD defaultValue);\n0x00456A90  0x0076  bool __cdecl GetRegistryBoolValue(LPCTSTR lpValueName, bool *pValue, bool defaultValue);\n0x00456B10  0x005C  bool __cdecl GetRegistryFloatValue(LPCTSTR lpValueName, double *value, double defaultValue);\n0x00456B70  0x0071  bool __cdecl GetRegistryBinaryValue(LPCTSTR lpValueName, LPBYTE value, DWORD valueSize, LPBYTE defaultValue);\n0x00456BF0  0x0095  bool __cdecl GetRegistryStringValue(LPCTSTR lpValueName, LPTSTR value, DWORD maxSize, LPCTSTR defaultValue);\n0x00456C90  0x0091  bool __cdecl GetRegistryGuidValue(LPCTSTR lpValueName, GUID *value, GUID *defaultValue);\n0x00456D30  0x0037  void __thiscall SE_ReleaseBitmapResource(BITMAP_RESOURCE *bmpRsrc);\n0x00456D70  0x00C4  void __thiscall SE_LoadBitmapResource(BITMAP_RESOURCE *bmpRsrc, LPCTSTR lpName);\n0x00456E40  0x0064  void __thiscall SE_DrawBitmap(BITMAP_RESOURCE *bmpRsrc, HDC hdc, int32_t x, int32_t y);\n0x00456EB0  0x001C  void __thiscall SE_UpdateBitmapPalette(BITMAP_RESOURCE *bmpRsrc, HWND hWnd, HWND hSender);\n0x00456ED0  0x0057  void __thiscall SE_ChangeBitmapPalette(BITMAP_RESOURCE *bmpRsrc, HWND hWnd);\n0x00456F30  0x0061  bool __cdecl SE_RegisterSetupWindowClass(void);\n0x00456FA0  0x023A  LRESULT __stdcall SE_SetupWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);\n0x004571E0  0x0026  void __cdecl SE_PassMessageToImage(HWND hWnd, UINT uMsg, WPARAM wParam);\n0x00457210  0x006E  void __cdecl UT_MemBlt(BYTE *dstBuf, DWORD dstX, DWORD dstY, DWORD width, DWORD height, DWORD dstPitch, BYTE *srcBuf, DWORD srcX, DWORD srcY, DWORD srcPitch);\n0x00457280  0x001E  void __cdecl Matrix_Push(void);\n0x0045729E  0x0033  void __cdecl Matrix_PushUnit(void);\n0x004572D4  0x0061  void __fastcall Output_FlatA(int32_t y0, int32_t y1, uint8_t color_idx); // actually, __watcall, which is esoteric and rarely supported\n0x00457335  0x013A  void __fastcall Output_TransA(int32_t y0, int32_t y1, uint8_t depth_q); // actually, __watcall, which is esoteric and rarely supported\n0x0045746F  0x0160  void __fastcall Output_GourA(int32_t y0, int32_t y1, uint8_t color_idx); // actually, __watcall, which is esoteric and rarely supported\n0x004575CF  0x02FD  void __fastcall Output_GTMapA(int32_t y0, int32_t y1, uint8_t *tex_page); // actually, __watcall, which is esoteric and rarely supported\n0x004578CC  0x0341  void __fastcall Output_WGTMapA(int32_t y0, int32_t y1, uint8_t *tex_page); // actually, __watcall, which is esoteric and rarely supported\n0x00457C10  0x0048  int32_t __fastcall Math_Atan(int32_t x, int32_t y);\n0x00457C58  0x0006  int32_t __fastcall Math_Cos(int16_t angle);\n0x00457C5E  0x001B  int32_t __fastcall Math_Sin(int16_t angle);\n0x00457C79  0x001A  int32_t __fastcall Math_SinImpl(int16_t angle);\n0x00457C93  0x002C  uint32_t  __fastcall Math_Sqrt(uint32_t n);\n\n0x00458D00  0x0006  int __cdecl Player_PlayFrame(LPVOID, LPVOID, LPVOID, DWORD, LPCRECT, DWORD, DWORD, DWORD);\n0x00458D06  0x0006  int __cdecl Movie_GetTotalFrames(LPVOID);\n0x00458D0C  0x0006  int __cdecl Movie_GetCurrentFrame(LPVOID);\n0x00458D12  0x0006  int __cdecl Player_StartTimer(LPVOID);\n0x00458D18  0x0006  int __cdecl Player_InitMoviePlayback(LPVOID, LPVOID, LPVOID);\n0x00458D1E  0x0006  int __cdecl Movie_SetSyncAdjust(LPVOID, LPVOID, DWORD);\n0x00458D24  0x0006  int __cdecl Player_InitSound(LPVOID, DWORD, DWORD, BOOL, DWORD, DWORD, DWORD, DWORD, DWORD);\n0x00458D2A  0x0006  int __cdecl Movie_GetSoundChannels(LPVOID);\n0x00458D30  0x0006  int __cdecl Movie_GetSoundRate(LPVOID);\n0x00458D36  0x0006  int __cdecl Movie_GetSoundPrecision(LPVOID);\n0x00458D3C  0x0006  int __cdecl Player_GetDSErrorCode(void);\n0x00458D42  0x0006  int __cdecl Player_InitSoundSystem(HWND);\n0x00458D48  0x0006  int __cdecl Player_BlankScreen(DWORD, DWORD, DWORD, DWORD);\n0x00458D4E  0x0006  int __cdecl Player_InitPlaybackMode(HWND, LPVOID, DWORD, DWORD);\n0x00458D54  0x0006  int __cdecl Player_InitVideo(LPVOID, LPVOID, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD);\n0x00458D5A  0x0006  int __cdecl Movie_GetXSize(LPVOID);\n0x00458D60  0x0006  int __cdecl Movie_GetYSize(LPVOID);\n0x00458D66  0x0006  int __cdecl Movie_GetFormat(LPVOID);\n0x00458D6C  0x0006  int __cdecl Player_InitMovie(LPVOID, DWORD, DWORD, LPCTSTR, DWORD);\n0x00458D72  0x0006  int __cdecl Player_PassInDirectDrawObject(LPDIRECTDRAW3);\n0x00458D78  0x0006  int __cdecl Player_ReturnPlaybackMode(BOOL);\n0x00458D7E  0x0006  int __cdecl Player_ShutDownSoundSystem(void);\n0x00458D84  0x0006  int __cdecl Player_ShutDownMovie(LPVOID);\n0x00458D8A  0x0006  int __cdecl Player_ShutDownVideo(LPVOID);\n0x00458D90  0x0006  int __cdecl Player_ShutDownSound(LPVOID);\n0x00458D96  0x0006  int __cdecl Player_StopTimer(LPVOID);\n\n\n# VARIABLES\n# Offset    Declaration\n\n0x00464060  uint32_t g_PerspectiveDistance = 0x3000000;\n0x00464068  void (*__cdecl g_PolyDrawRoutines[9])(const int16_t *obj_ptr);\n0x0046408C  float g_RhwFactor = 335544320.0f; // 10*2**25\n0x004640B0  int32_t g_CineTrackID = 1;\n0x004640B8  int32_t g_CineTickRate = 0x8000; // 0x8000 = PHD_ONE/TICKS_PER_FRAME\n0x004640BC  int16_t g_CD_TrackID = -1;\n0x004640C4  int32_t g_FlipEffect = -1;\n0x004641F0  uint32_t g_AssaultBestTime = -1;\n0x004641F8  void (*__cdecl g_EffectRoutines[32])(ITEM *item);\n0x00464310  int16_t g_CineTargetAngle = 0x4000; // PHD_90\n0x004644E0  int32_t g_OverlayStatus = 1;\n0x004654E0  int16_t g_Inv_MainObjectsCount = 8;\n0x00465604  int16_t g_Inv_OptionObjectsCount = 4;\n0x00465618  BOOL g_GymInvOpenEnabled = TRUE;\n0x00465A50  int16_t g_Inv_Chosen = -1;\n0x00465A54  INVENTORY_MODE g_Inv_Mode = INV_TITLE_MODE;\n0x00465A5C  int16_t g_OptionSoundVolume = 165; // NOTE: value should be 10\n0x00465A60  int16_t g_OptionMusicVolume = 255; // NOTE: should be 10\n0x00465AD4  int32_t g_JumpPermitted = 1;\n0x00465AD8  int16_t g_LaraOldSlideAngle = 1;\n0x00465CD0  void (*__cdecl g_LaraControlRoutines[71])(ITEM *item, COLL_INFO *coll);\n0x00465DF0  void (*__cdecl g_ExtraControlRoutines[11])(ITEM *item, COLL_INFO *coll);\n0x00465E20  void (*__cdecl g_LaraCollisionRoutines[71])(ITEM *item, COLL_INFO *coll);\n0x00466290  int8_t g_TextSpacing[80];\n0x004662E0  int8_t g_TextASCIIMap[];\n0x00466400  int32_t g_BGND_PaletteIndex = -1;\n0x00466480  double g_GameSizer = 1.0;\n0x00466488  double g_GameSizerCopy = 1.0;\n0x00466490  int32_t g_FadeValue = 0x100000;\n0x00466494  int32_t g_FadeLimit = 0x100000;\n0x00466498  int32_t g_FadeAdder = 0x8000;\n0x004664E8  const char *g_ErrorMessages[43];\n0x00466BB0  int32_t g_RandControl;\n0x00466BB4  int32_t g_RandDraw;\n0x00466F70  CONTROL_LAYOUT g_Layout[2];\n0x00467DD0  const int32_t g_AtanBaseTable[8];\n0x00467DF0  const int16_t g_AtanAngleTable[0x800];\n0x00468DF4  const int16_t g_SinTable[0x402];\n0x0046C300  int32_t g_MidSort = 0;\n0x0046C304  float g_ViewportAspectRatio = 0.0f;\n0x0046C308  int32_t g_XGenY1;\n0x0046C30C  int32_t g_XGenY2;\n0x0046C310  GOURAUD_ENTRY g_GouraudTable[256];\n0x0046E310  int32_t g_PhdWinTop;\n0x0046E318  PHD_SPRITE g_PhdSprites[512];\n0x00470318  int32_t g_LsAdder;\n0x0047031C  float g_FltWinBottom;\n0x00470320  float g_FltResZBuf;\n0x00470324  float g_FltResZ;\n0x00470328  void (*__cdecl g_Output_InsertTransQuad)(int32_t x, int32_t y, int32_t width, int32_t height, int32_t z);\n0x0047032C  int32_t g_PhdWinHeight;\n0x00470330  int32_t g_PhdWinCenterX;\n0x00470334  int32_t g_PhdWinCenterY;\n0x00470338  int16_t g_LsYaw;\n0x0047033C  void (*__cdecl g_Output_InsertTrans8)(const PHD_VBUF *vbuf, int16_t shade);\n0x00470340  float g_FltWinTop;\n0x00470348  SORT_ITEM g_SortBuffer[4000];\n0x00478048  float g_FltWinLeft;\n0x0047804C  int16_t g_PhdWinMinY;\n0x00478058  int32_t g_PhdFarZ;\n0x0047805C  float g_FltRhwOPersp;\n0x00478060  int32_t g_PhdWinBottom;\n0x00478064  int32_t g_PhdPersp;\n0x00478068  int32_t g_PhdWinLeft;\n0x0047806C  void (*__cdecl g_Output_InsertFlatRect)(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x00478070  int16_t g_Info3DBuffer[120000];\n0x004B29F0  int32_t g_PhdWinMaxX;\n0x004B29F4  int32_t g_PhdNearZ;\n0x004B29F8  float g_FltResZORhw;\n0x004B29FC  float g_FltFarZ;\n0x004B2A00  float g_FltWinCenterX;\n0x004B2A04  float g_FltWinCenterY;\n0x004B2A08  int32_t g_PhdScreenHeight;\n0x004B2A0C  uint8_t *g_PrintSurfacePtr;\n0x004B2A10  int16_t g_PhdWinMinX;\n0x004B2A14  float g_FltPerspONearZ;\n0x004B2A18  float g_FltRhwONearZ;\n0x004B2A1C  int32_t g_PhdWinMaxY;\n0x004B2A20  void (*__cdecl g_Output_InsertSprite)(int32_t z, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t sprite_idx, int16_t shade);\n0x004B2A24  float g_FltNearZ;\n0x004B2A28  MATRIX *g_MatrixPtr;\n0x004B2A2C  const int16_t *(*__cdecl g_Output_DrawObjectGT3)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x004B2A30  const int16_t *(*__cdecl g_Output_DrawObjectGT4)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x004B2A38  int32_t g_RandomTable[32];\n0x004B2AB8  float g_FltPersp;\n0x004B2AC0  MATRIX g_W2VMatrix;\n0x004B2AF0  int16_t *g_Info3DPtr;\n0x004B2AF4  int32_t g_PhdWinWidth;\n0x004B2AF8  void (*__cdecl g_Output_InsertLine)(int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t z, uint8_t color_idx);\n0x004B2B00  PHD_TEXTURE g_TextureInfo[0x800]; // MAX_TEXTURES\n0x004BCB00  int32_t g_PhdViewDistance;\n0x004BCB04  int16_t g_LsPitch;\n0x004BCB08  const int16_t *(*__cdecl g_Output_DrawObjectG4)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x004BCB10  int16_t g_ShadesTable[32];\n0x004BCB50  const int16_t *(*__cdecl g_Output_DrawObjectG3)(const int16_t *obj_ptr, int32_t num, SORT_TYPE sort_type);\n0x004BCB58  MATRIX g_MatrixStack[];\n0x004BD2D8  DEPTHQ_ENTRY g_DepthQTable[32];\n0x004BF3D8  int32_t g_PhdScreenWidth;\n0x004BF3DC  int32_t g_LsDivider;\n0x004BF3E0  PHD_VBUF g_PhdVBuf[1500];\n0x004CAF60  void *g_XBuffer;\n0x004D6AE0  uint8_t *g_TexturePageBuffer8[32]; // MAX_TEXTURE_PAGES\n0x004D6B60  float g_FltWinRight;\n0x004D6B68  XYZ_32 g_LsVectorView;\n0x004D6B78  float g_WibbleTable[32];\n0x004D6BF8  int32_t g_PhdWinRight;\n0x004D6BFC  int32_t g_SurfaceCount;\n0x004D6C00  SORT_ITEM *g_Sort3DPtr;\n0x004D6C0C  int32_t g_WibbleOffset;\n0x004D6C10  int32_t g_IsWibbleEffect;\n0x004D6C14  int32_t g_IsWaterEffect;\n0x004D6CD8  VERTEX_INFO g_VBuffer[20];\n0x004D6F78  int8_t g_IsShadeEffect;\n0x004D6F80  D3DTLVERTEX g_VBufferD3D[32];\n0x004D7380  PALETTEENTRY g_GamePalette16[256];\n0x004D7780  int32_t g_CineFrameCurrent;\n0x004D778C  int32_t g_IsChunkyCamera;\n0x004D7794  int32_t g_NoInputCounter;\n0x004D7798  BOOL g_IsResetFlag;\n0x004D779C  int32_t g_FlipTimer;\n0x004D77A0  int32_t g_LOSNumRooms = 0;\n0x004D77A4  BOOL g_StopInventory;\n0x004D77AC  BOOL g_IsDemoLevelType;\n0x004D77B0  BOOL g_IsDemoLoaded;\n0x004D77C0  int32_t g_BoundStart;\n0x004D77C4  int32_t g_BoundEnd;\n0x004D77E0  int32_t g_IsAssaultTimerDisplay;\n0x004D77E4  BOOL g_IsAssaultTimerActive;\n0x004D77E8  BOOL g_IsMonkAngry;\n0x004D791C  int32_t g_OldGameTimer;\n0x004D7920  BOOL g_FlashState;\n0x004D7924  int32_t g_FlashCounter;\n0x004D7928  int32_t g_OldHitPoints;\n0x004D792C  TEXTSTRING *g_AmmoTextInfo;\n0x004D7930  TEXTSTRING *g_DisplayModeTextInfo;\n0x004D7934  DWORD g_DisplayModeInfoTimer;\n0x004D7938  UINT16 g_Inv_MainCurrent;\n0x004D793C  UINT16 g_Inv_KeyObjectsCount;\n0x004D7940  UINT16 g_Inv_KeysCurrent;\n0x004D7944  UINT16 g_Inv_OptionCurrent;\n0x004D7954  TEXTSTRING* g_Inv_RingText;\n0x004D795C  TEXTSTRING* g_Inv_UpArrow1;\n0x004D7960  TEXTSTRING* g_Inv_UpArrow2;\n0x004D7964  TEXTSTRING* g_Inv_DownArrow1;\n0x004D7968  TEXTSTRING* g_Inv_DownArrow2;\n0x004D796C  uint32_t g_InputDB;\n0x004D7978  uint16_t g_Inv_IsActive;\n0x004D79A0  BOOL g_Inv_DemoMode;\n0x004D79B4  BOOL g_Inv_IsOptionsDelay;\n0x004D79B8  int32_t g_Inv_OptionsDelayCounter;\n0x004D79BC  uint16_t g_SoundOptionLine;\n0x004D79C0  REQUEST_INFO g_StatsRequester;\n0x004D7BD8  ASSAULT_STATS g_Assault;\n0x004D7C38  int32_t g_LevelItemCount;\n0x004D7C3C  int32_t g_HealthBarTimer;\n0x004D7C80  int32_t g_SoundTrackIds[128];\n0x004D7EBC  LPDIRECT3DDEVICE2 g_D3DDev;\n0x004D7EE4  bool g_IsGameWindowCreated;\n0x004D7EE8  bool g_IsGameWindowUpdating;\n0x004D7EEC  bool g_IsDDrawGameWindowShow;\n0x004D7EF0  int32_t g_MinWindowClientWidth;\n0x004D7ED0  int32_t g_MinWindowClientHeight;\n0x004D8388  int32_t g_MinWindowWidth;\n0x004D838C  int32_t g_MinWindowHeight;\n0x004D7EF4  bool g_IsGameWindowShow;\n0x004D7EF8  bool g_IsMinWindowSizeSet;\n0x004D7EFC  int32_t g_MaxWindowClientWidth;\n0x004D7F00  int32_t g_GameWindowWidth;\n0x004D7F04  bool g_IsMinMaxInfoSpecial;\n0x004D7F08  bool g_IsGameFullScreen;\n0x004D7F0C  bool g_IsGameWindowMaximized;\n0x004D7F10  HWND g_GameWindowHandle;\n0x004D7F14  int32_t g_GameWindowHeight;\n0x004D7F18  DISPLAY_ADAPTER_NODE* g_PrimaryDisplayAdapter;\n0x004D7F20  DISPLAY_ADAPTER g_CurrentDisplayAdapter;\n0x004D8338  uint32_t g_LockedBufferCount;\n0x004D833C  int32_t g_GameWindowPositionX;\n0x004D8340  int32_t g_GameWindowPositionY;\n0x004D8348  DISPLAY_ADAPTER_LIST g_DisplayAdapterList;\n0x004D8354  int32_t g_MaxWindowClientHeight;\n0x004D8358  bool g_IsMessageLoopClosed;\n0x004D835C  int32_t g_MaxWindowWidth;\n0x004D7EDC  int32_t g_MaxWindowHeight;\n0x004D8360  bool g_IsMaxWindowSizeSet;\n0x004D8364  uint32_t g_AppResultCode;\n0x004D8368  int32_t g_FullScreenWidth;\n0x004D836C  int32_t g_FullScreenHeight;\n0x004D8370  int32_t g_FullScreenBPP;\n0x004D8374  int32_t g_FullScreenVGA;\n0x004D8378  uint8_t g_IsGameToExit;\n0x004D8568  int32_t g_ScreenSizer;\n0x004D856C  int32_t g_IsVidSizeLock;\n0x004D8570  DWORD g_SampleFreqs[256];\n0x004D8970  SOUND_ADAPTER_LIST g_SoundAdapterList;\n0x004D8980  LPDIRECTSOUNDBUFFER g_SampleBuffers[256];\n0x004D8D80  uint8_t g_IsSoundEnabled;\n0x004D8D84  LPDIRECTSOUND g_DSound;\n0x004D8D88  int32_t g_ChannelSamples[32];\n0x004D8E08  LPDIRECTSOUNDBUFFER g_ChannelBuffers[32];\n0x004D8E8C  SOUND_ADAPTER g_CurrentSoundAdapter;\n0x004D8EAC  SOUND_ADAPTER_NODE *g_PrimarySoundAdapter;\n0x004D8EB0  LPDDS g_RenderBufferSurface;\n0x004D92B8  LPDDS g_ThirdBufferSurface;\n0x004D92BC  LPDDS g_PictureBufferSurface;\n0x004D92C0  LPDDS g_ZBufferSurface;\n0x004D92C8  LPDDS g_PrimaryBufferSurface;\n0x004D9338  int32_t g_GameVid_Width;\n0x004D933C  int32_t g_GameVid_Height;\n0x004D9340  int32_t g_GameVid_BPP;\n0x004D934C  int32_t g_UVAdd;\n0x004D9351  int8_t g_GameVid_IsWindowedVGA;\n0x004D9EAC  int32_t g_IsFMVPlaying;\n0x004D9EC0  int32_t g_CurrentLevel;\n0x004D9EC4  int32_t g_LevelComplete;\n0x004D9ED8  D3DTLVERTEX g_HWR_VertexBuffer[0x2000]; // MAX_VERTICES\n0x00519EE0  HWR_TEXTURE_HANDLE g_HWR_PageHandles[32];\n0x00519F60  D3DTLVERTEX *g_HWR_VertexPtr;\n0x0051A0CC  char *g_GameBuf_MemBase;\n0x0051A0D0  BOOL g_ConflictLayout[14]; // INPUT_ROLE_NUMBER_OF\n0x0051A108  uint8_t g_DIKeys[256];\n0x0051A208  int32_t g_Input;\n0x0051A20C  int8_t g_IsVidModeLock;\n0x0051A210  int32_t g_JoyKeys;\n0x0051A214  int32_t g_JoyXPos;\n0x0051A218  int32_t g_JoyYPos;\n0x0051A220  int32_t g_MediPackCooldown;\n0x0051A224  int8_t g_IsF3Pressed;\n0x0051A228  int8_t g_IsF4Pressed;\n0x0051A22C  int8_t g_IsF7Pressed;\n0x0051A230  int8_t g_IsF8Pressed;\n0x0051A234  int8_t g_IsF11Pressed;\n0x0051A238  HINSTANCE g_GameModule;\n0x0051A23C  char *g_CmdLine;\n0x0051A240  int32_t g_ScreenshotCounter;\n0x0051B918  RECT g_PhdWinRect;\n0x0051B928  int32_t g_HiRes;\n0x0051B930  RGB_888 g_GamePalette8[256];\n0x0051BCC0  APP_SETTINGS g_SavedAppSettings;\n0x0051BD20  char g_ErrorMessage[128];\n0x0051BDA8  int32_t g_MasterVolume;\n0x0051BDAC  MCIDEVICEID g_MciDeviceID;\n0x0051BDB0  int32_t g_CD_LoopTrack;\n0x0051C820  TEXTSTRING g_TextstringTable[64]; // MAX_TEXTSTRINGS\n0x0051D6A0  int16_t g_TextstringCount = 0;\n0x0051D6C0  char g_TextstringBuffers[64][64];\n0x0051E6C4  int32_t g_SoundIsActive;\n0x0051E9E0  SAVEGAME_INFO g_SaveGame;\n0x005206E0  LARA_INFO g_Lara;\n0x005207BC  ITEM *g_LaraItem;\n0x005207C0  EFFECT *g_Effects;\n0x005207C4  int16_t g_NextEffectFree;\n0x005207C6  int16_t g_NextItemFree;\n0x005207C8  int16_t g_NextItemActive;\n0x005207CA  int16_t g_NextEffectActive;\n0x005207CC  int16_t g_PrevItemActive;\n0x00521CA0  PICKUP_INFO g_Pickups[12];\n0x00521DE0  GAME_FLOW g_GameFlow;\n0x00521FDC  int32_t g_SoundEffectCount;\n0x00522000  OBJECT g_Objects[265];\n0x005252B0  int16_t **g_Meshes;\n0x005252C0  MATRIX g_IMMatrixStack[256];\n0x005258F0  int32_t g_IMFrac;\n0x005258F4  ANIM *g_Anims;\n0x00525BE8  int32_t *g_AnimBones;\n0x00526180  int32_t g_RoomCount;\n0x00526184  int32_t g_IMRate;\n0x00526188  MATRIX *g_IMMatrixPtr;\n0x0052618C  ROOM *g_Rooms;\n0x00526240  int32_t g_FlipStatus;\n0x00526288  int16_t *g_TriggerIndex;\n0x005262A0  int32_t g_LOSRooms[20];\n0x005262F0  ITEM *g_Items;\n0x005262F6  int16_t g_NumCineFrames;\n0x005262F8  CINE_FRAME *g_CineData = NULL;\n0x00526300  PHD_3DPOS g_CinePos;\n0x00526314  int16_t g_CineFrameIdx;\n0x00526320  CAMERA_INFO g_Camera;\n0x005263CC  BOX_INFO *g_Boxes;\n0x004D855C  LPDIRECTINPUT g_DInput;\n0x004D8560  LPDIRECTINPUTDEVICE IDID_SysKeyboard;\n0x0051BDA0  BOOL g_IsTitleLoaded;\n0x004D7980  int32_t g_Inv_ExtraData[8];\n0x004D8394  int32_t g_MessageLoopCounter;\n0x004D8384  bool g_IsGameWindowMinimized;\n0x004D8390  bool g_IsGameWindowActive;\n0x004D837C  int32_t g_GameWindowY;\n0x004D7EE0  LPDIRECTDRAW3 g_DDraw;\n0x004D8380  int32_t g_GameWindowX;\n0x00463150  GUID g_IID_IDirectDrawSurface3;\n0x00463170  GUID g_IID_IDirect3DTexture2;\n0x004640A0  BITE g_CrowBite;\n0x00464090  BITE g_BirdBite;\n0x005263C0  int16_t *g_FlyZone[2];\n0x005263A0  int16_t *g_GroundZone[][2];\n0x005263C8  uint16_t *g_Overlap;\n0x005206C0  CREATURE *g_BaddieSlots;\n0x00526312  int16_t g_CineLevelID;\n0x005252B8  int32_t g_DrawRoomsCount;\n0x00525B20  int16_t g_DrawRoomsArray[100];\n0x00525BEC  int32_t g_DynamicLightCount;\n0x004D7784  int32_t g_CineTickCount;\n0x004D7788  int32_t g_OriginalRoom;\n0x00465518  INVENTORY_ITEM *g_Inv_MainList[];\n0x00465608  INVENTORY_ITEM *g_Inv_OptionList[];\n0x004655A8  INVENTORY_ITEM *g_Inv_KeysList[];\n0x004644F8  int32_t g_Inv_NFrames;\n0x00525C00  STATIC_INFO g_StaticObjects[50]; // MAX_STATIC_OBJECTS\n0x00521FE0  OBJECT_VECTOR *g_SoundEffects;\n0x0051E6E0  int16_t g_SampleLUT[];\n0x0051E9C4  SAMPLE_INFO *g_SampleInfos;\n0x004D7C78  SOUND_SLOT g_SoundSlots[32];\n0x004D9328  RECT g_GameVid_Rect;\n0x004D9358  LPDDS g_BackBufferSurface;\n0x004D9350  bool g_GameVid_IsVga;\n0x004D9344  int32_t g_GameVid_BufWidth;\n0x004D9348  int32_t g_GameVid_BufHeight;\n0x004D8EB4  LPDIRECTDRAWCLIPPER g_DDrawClipper;\n0x004D8EB8  PALETTEENTRY g_WinVid_Palette[256];\n0x004D92C4  LPDIRECTDRAWPALETTE g_DDrawPalette;\n0x004D7EC4  LPDIRECT3DVIEWPORT2 g_D3DView;\n0x004D9355  bool g_NeedToReloadTextures;\n0x004D9352  bool g_GameVid_IsFullscreenVGA;\n0x004D9353  bool g_IsWindowedVGA;\n0x004D9354  bool g_Is16bitTextures;\n0x004D9318  RECT g_GameVid_BufRect;\n0x00466BE4  int16_t g_DumpX;\n0x00466BE6  int16_t g_DumpY;\n0x00466BE8  int16_t g_DumpWidth;\n0x00466BEA  int16_t g_DumpHeight;\n0x0051C1B8  TEXTURE_FORMAT g_TextureFormat;\n0x004D92E8  COLOR_BIT_MASKS g_ColorBitMasks;\n0x0051BC30  bool g_WinVidNeedToResetBuffers;\n0x004D7E88  bool g_BGND_PictureIsReady;\n0x004D7E90  int32_t g_BGND_TexturePageIndexes[5];\n0x004D7EA8  HWR_TEXTURE_HANDLE g_BGND_PageHandles[5];\n0x004D7EC0  LPDIRECT3D2 g_D3D;\n0x004D7EC8  LPDIRECT3DMATERIAL2 g_D3DMaterial;\n0x004D7ED4  LPDIRECTDRAW g_DDrawInterface;\n0x00466448  const char g_GameClassName[];\n0x00466468  const char g_GameWindowName[];\n0x004D7ED8  bool g_IsGameWindowChanging;\n0x00519F68  D3DRENDERSTATETYPE g_AlphaBlendEnabler;\n0x00519ED8  D3DTEXTUREHANDLE g_CurrentTexSource;\n0x00519F6C  bool g_ColorKeyState;\n0x0051C20C  bool g_TexturesAlphaChannel;\n0x00519F64  bool g_ZEnableState;\n0x00519F70  bool g_ZWriteEnableState;\n0x00466BDC  int32_t g_PaletteIndex;\n0x00519F78  int32_t g_HWR_TexturePageIndexes[32]; // MAX_TEXTURE_PAGES\n0x004D7790  int32_t g_HeightType;\n0x004D9D94  int16_t *g_FloorData;\n0x00525B08  int16_t *g_AnimCommands;\n0x0052617C  ANIM_CHANGE *g_AnimChanges;\n0x00525B04  ANIM_RANGE *g_AnimRanges;\n0x00526260  int32_t g_FlipMaps[10]; // MAX_FLIP_MAPS\n0x005252B4  int32_t g_Outside;\n0x00526198  int32_t g_OutsideRight;\n0x00526178  int32_t g_OutsideLeft;\n0x005261AC  int32_t g_OutsideTop;\n0x00525B00  int32_t g_OutsideBottom;\n0x00525900  int32_t g_BoundRooms[128]; // MAX_BOUND_ROOMS\n0x005258C0  PORTAL_VBUF g_DoorVBuf[4];\n0x00464180  int32_t g_BoxLines[12][2];\n0x00526190  BOOL g_CameraUnderwater;\n0x005263D0  int32_t g_BoxCount;\n0x004D7C50  int32_t g_SlotsUsed;\n0x004D9360  int32_t g_TexturePageCount;\n0x004D9D90  int16_t *g_MeshBase;\n0x004D9E98  int32_t g_TextureInfoCount;\n0x004D93F0  uint8_t g_LabTextureUVFlag[2048]; // MAX_TEXTURES\n0x005251B0  FRAME_INFO *g_AnimFrames;\n0x0051BC38  int32_t g_IsWet;\n0x0051B308  RGB_888 g_WaterPalette[256];\n0x004BF2D8  uint8_t g_DepthQIndex[256];\n0x004D7C74  int32_t g_NumCameras;\n0x0051B92C  int16_t *g_AnimTextureRanges;\n0x005262F4  int16_t g_CineLoaded;\n0x005261B0  uint32_t *g_DemoPtr;\n0x005261B4  int32_t g_DemoCount;\n0x0051E6C0  int32_t g_NumSampleInfos;\n0x004D9BF4  int32_t g_LevelFilePalettesOffset;\n0x004D9BF8  int32_t g_LevelFileTexPagesOffset;\n0x004D9E9C  int32_t g_LevelFileDepthQOffset;\n0x004D9D98  char g_LevelFileName[256];\n0x005261C0  uint16_t g_MusicTrackFlags[64];\n0x00465AE0  WEAPON_INFO g_Weapons[];\n0x005206A8  int16_t g_FinalBossActive;\n0x005206BA  int16_t g_FinalLevelCount;\n0x005206BC  int16_t g_FinalBossCount;\n0x005206B0  int16_t g_FinalBossItem[5];\n0x004D77B4  int32_t g_DemoLevel;\n0x004D77B8  int32_t g_DemoLevel2;\n0x00464A90  INVENTORY_ITEM g_Inv_Item_Stopwatch;\n0x00464AE0  INVENTORY_ITEM g_Inv_Item_Pistols;\n0x00464B30  INVENTORY_ITEM g_Inv_Item_Flare;\n0x00464B80  INVENTORY_ITEM g_Inv_Item_Shotgun;\n0x00464BD0  INVENTORY_ITEM g_Inv_Item_Magnums;\n0x00464C20  INVENTORY_ITEM g_Inv_Item_Uzis;\n0x00464C70  INVENTORY_ITEM g_Inv_Item_Harpoon;\n0x00464CC0  INVENTORY_ITEM g_Inv_Item_M16;\n0x00464D10  INVENTORY_ITEM g_Inv_Item_Grenade;\n0x00464D60  INVENTORY_ITEM g_Inv_Item_PistolAmmo;\n0x00464DB0  INVENTORY_ITEM g_Inv_Item_ShotgunAmmo;\n0x00464E00  INVENTORY_ITEM g_Inv_Item_MagnumAmmo;\n0x00464E50  INVENTORY_ITEM g_Inv_Item_UziAmmo;\n0x00464EA0  INVENTORY_ITEM g_Inv_Item_HarpoonAmmo;\n0x00464EF0  INVENTORY_ITEM g_Inv_Item_M16Ammo;\n0x00464F40  INVENTORY_ITEM g_Inv_Item_GrenadeAmmo;\n0x00464F90  INVENTORY_ITEM g_Inv_Item_SmallMedi;\n0x00464FE0  INVENTORY_ITEM g_Inv_Item_LargeMedi;\n0x00465030  INVENTORY_ITEM g_Inv_Item_Pickup1;\n0x00465080  INVENTORY_ITEM g_Inv_Item_Pickup2;\n0x004650D0  INVENTORY_ITEM g_Inv_Item_Puzzle1;\n0x00465120  INVENTORY_ITEM g_Inv_Item_Puzzle2;\n0x00465170  INVENTORY_ITEM g_Inv_Item_Puzzle3;\n0x004651C0  INVENTORY_ITEM g_Inv_Item_Puzzle4;\n0x00465210  INVENTORY_ITEM g_Inv_Item_Key1;\n0x00465260  INVENTORY_ITEM g_Inv_Item_Key2;\n0x004652B0  INVENTORY_ITEM g_Inv_Item_Key3;\n0x00465300  INVENTORY_ITEM g_Inv_Item_Key4;\n0x00465350  INVENTORY_ITEM g_Inv_Item_Passport;\n0x004653A0  INVENTORY_ITEM g_Inv_Item_Graphics;\n0x004653F0  INVENTORY_ITEM g_Inv_Item_Sound;\n0x00465440  INVENTORY_ITEM g_Inv_Item_Controls;\n0x00465490  INVENTORY_ITEM g_Inv_Item_Photo;\n0x00465620  REQUEST_INFO g_LoadGameRequester;\n0x00465838  REQUEST_INFO g_SaveGameRequester;\n0x004642E8  int16_t g_GF_NumSecrets = 3;\n0x004642F0  int16_t g_GF_MusicTracks[16];\n0x004D77EC  int32_t g_GF_ScriptVersion;\n0x004D77F0  int32_t g_GF_LaraStartAnim;\n0x004D77F4  int16_t g_GF_SunsetEnabled;\n0x004D77F8  int16_t g_GF_DeadlyWater;\n0x004D77FC  int16_t g_GF_NoFloor;\n0x004D7800  int16_t g_GF_RemoveWeapons;\n0x004D7804  int16_t g_GF_RemoveAmmo;\n0x004D7808  char g_GF_Kill2Complete;\n0x004D780C  int8_t g_GF_StartGame;\n0x004D7818  char g_GF_Description[256];\n0x004D9C00  int16_t g_GF_LevelOffsets[200];\n0x00521DC4  char **g_GF_CutsceneFileNames;\n0x00521E68  char *g_GF_FMVFilenamesBuf;\n0x00521E6C  char *g_GF_Key1StringsBuf;\n0x00521E70  int16_t *g_GF_FrontendSequence;\n0x00521E74  char **g_GF_Key2Strings;\n0x00521E78  char *g_GF_CutsceneFileNamesBuf;\n0x00521E7C  char *g_GF_Key4StringsBuf;\n0x00521E80  int16_t *g_GF_SequenceBuf;\n0x00521E84  char *g_GF_Key2StringsBuf;\n0x00521E8C  char *g_GF_PicFilenamesBuf;\n0x00521E90  char **g_GF_Key4Strings;\n0x00521DC0  char **g_GF_Puzzle1Strings;\n0x00521E98  char **g_GF_Puzzle2Strings;\n0x00521EC0  char **g_GF_Puzzle3Strings;\n0x00521E60  char **g_GF_Puzzle4Strings;\n0x00521E94  char **g_GF_Pickup1Strings;\n0x00521F44  char **g_GF_Pickup2Strings;\n0x00521EA8  char *g_GF_Puzzle1StringsBuf;\n0x00521F40  char *g_GF_Puzzle2StringsBuf;\n0x00521F98  char *g_GF_Puzzle3StringsBuf;\n0x00521F90  char *g_GF_Puzzle4StringsBuf;\n0x00521E64  char *g_GF_Pickup1StringsBuf;\n0x00521E88  char *g_GF_Pickup2StringsBuf;\n0x00521E9C  char *g_GF_LevelFileNamesBuf;\n0x00521EA0  char **g_GF_PicFilenames;\n0x00521EA4  char **g_GF_Key1Strings;\n0x00521EAC  char *g_GF_LevelNamesBuf;\n0x00521EB0  char **g_GF_GameStrings;\n0x00521EB4  char *g_GF_PCStringsBuf;\n0x00521EB8  char *g_GF_GameStringsBuf;\n0x00521EBC  char **g_GF_Key3Strings;\n0x00521EC4  char **g_GF_LevelNames;\n0x00521EE0  int16_t *g_GF_ScriptTable[24]; // MAX_LEVELS\n0x00521F48  char **g_GF_TitleFileNames;\n0x00521F4C  char *g_GF_TitleFileNamesBuf;\n0x00521F50  char **g_GF_PCStrings;\n0x00521F54  char **g_GF_LevelFileNames;\n0x00521F60  int16_t g_GF_ValidDemos[24]; // MAX_DEMO_FILES\n0x00521F94  char **g_GF_FMVFilenames;\n0x00521F9C  char *g_GF_Key3StringsBuf;\n0x00521FA0  char g_GF_SecretInvItems[27]; // GF_ADD_INV_NUMBER_OF\n0x00521FC0  char g_GF_Add2InvItems[27]; // GF_ADD_INV_NUMBER_OF\n0x004D9ECC  int32_t g_GameMode; // GAMEMODE\n0x004D7970  int32_t g_OldInputDB;\n0x004D7948  TEXTSTRING *g_Inv_ItemText[3]; // IT_NUMBER_OF\n0x004D7950  TEXTSTRING *g_Inv_LevelText;\n0x004D7958  TEXTSTRING *g_Inv_TagText;\n0x004D9EBC  int32_t g_SavedGames;\n0x0051A2CC  TEXTSTRING *g_PasswordText1;\n0x0051A2D0  int32_t g_PassportMode;\n0x0051A2D8  TEXTSTRING *g_DetailText[5];\n0x0051A2F0  TEXTSTRING *g_SoundText[4];\n0x0051A290  TEXTSTRING *m_ControlsTextA[14]; // INPUT_ROLE_NUMBER_OF\n0x0051A258  TEXTSTRING *g_ControlsTextB[14]; // INPUT_ROLE_NUMBER_OF\n0x0051A300  TEXTSTRING *g_ControlsText[2];\n0x004D7C30  int32_t m_ShowStatsTextMode;\n0x005207E0  char g_ValidLevelStrings1[];\n0x00521720  char g_ValidLevelStrings2[];\n0x004D7C34  int32_t m_ShowEndStatsTextMode;\n0x004D7C2C  int32_t m_ShowGymStatsTextMode;\n0x00520D00  uint32_t g_RequesterFlags1[24]; // MAX_REQUESTER_ITEMS\n0x00520CA0  uint32_t g_RequesterFlags2[24]; // MAX_REQUESTER_ITEMS\n0x00521C40  uint32_t g_SaveGameReqFlags1[24]; // MAX_REQUESTER_ITEMS\n0x00521BE0  uint32_t g_SaveGameReqFlags2[24]; // MAX_REQUESTER_ITEMS\n0x004D9EC8  int32_t g_SaveCounter;\n0x00466B80  int16_t g_SavedLevels[24]; // MAX_LEVELS\n0x004654E8  int16_t g_Inv_MainQtys[];\n0x00465578  int16_t g_Inv_KeysQtys[];\n0x0046773C  int32_t g_DetailLevel;\n0x0051A250  int32_t g_LayoutPage;\n0x0051A24C  int32_t g_KeySelector;\n0x0051A248  int32_t g_KeyCursor;\n0x00466FA8  const char *g_KeyNames[];\n0x00464500  const uint16_t g_Requester_BackgroundGour1[];\n0x00464520  const uint16_t g_Requester_BackgroundGour2[];\n0x00464538  const uint16_t g_Requester_MainGour1[];\n0x00464558  const uint16_t g_Requester_MainGour2[];\n0x00464590  const uint16_t g_Requester_SelectionGour2[];\n0x004645A8  const uint16_t g_Requester_UnselectionGour1[];\n0x005216E0  uint16_t g_InvColors[17]; // INV_COLOR_NUMBER_OF\n0x00464150  BITE g_DragonMouth;\n0x00466230  BITE g_SkidooLeftGun;\n0x00466240  BITE g_SkidooRightGun;\n0x00464130  BITE g_DogBite;\n0x00464140  BITE g_TigerBite;\n0x00465F40  int16_t g_MovableBlockBounds[];\n0x00465F58  int16_t g_ZiplineHandleBounds[];\n0x00465FF0  int16_t g_PickupBounds[];\n0x00466018  int16_t g_GongBounds[];\n0x00466030  int16_t g_PickupBoundsUW[];\n0x00466058  int16_t g_SwitchBounds[];\n0x004660A0  int16_t g_SwitchBoundsUW[];\n0x004660C8  int16_t g_KeyholeBounds[];\n0x004660F0  int16_t g_PuzzleHoleBounds[];\n0x00465F70  XYZ_32 g_ZiplineHandlePosition;\n0x00466008  XYZ_32 g_PickupPosition;\n0x00466048  XYZ_32 g_PickupPositionUW;\n0x00466070  XYZ_32 g_SmallSwitchPosition;\n0x00466080  XYZ_32 g_PushSwitchPosition;\n0x00466090  XYZ_32 g_AirlockPosition;\n0x004660B8  XYZ_32 g_SwitchUWPosition;\n0x004660E0  XYZ_32 g_KeyholePosition;\n0x00466108  XYZ_32 g_PuzzleHolePosition;\n0x004D7C58  XYZ_32 g_InteractPosition;\n0x004D7C68  XYZ_32 g_DetonatorPosition;\n0x004D9EB0  void *g_MovieContext;\n0x004D9EB4  void *g_FmvContext;\n0x004D9EB8  void *g_FmvSoundContext;\n0x0051A000  size_t g_GameBuf_MemCap;\n0x0051A004  char *g_GameBuf_MemPtr;\n0x0051A008  size_t g_GameBuf_MemUsed;\n0x0051A00C  size_t g_GameBuf_MemFree;\n0x0051B608  RGB_888 g_PicturePalette[256];\n0x004D7E7C  int32_t g_DetonateAllMines;\n0x005206A4  int32_t g_SavegameBufPos;\n0x0051E9C8  char *g_SavegameBufPtr;\n0x0051C210  LPDIRECTDRAWPALETTE g_TexturePalettes[16]; // MAX_PALETTES\n0x0051BDB8  TEXPAGE_DESC g_TexturePages[32]; // MAX_TEXTURE_PAGES\n0x00466280  BITE g_BigSpiderBite;\n0x0051C20D  uint8_t g_TexturesHaveCompatibleMasks;\n0x00467768  SHADOW_INFO g_ShadowInfo;\n0x004663C0  BITE g_YetiLBite;\n0x004663D0  BITE g_YetiRBite;\n0x004663E0  BITE g_BirdGuardianBite;\n0x0051BDA4  int32_t g_CheatMode;\n0x0051BD1C  bool g_CheatFlare;\n0x0051BD18  int16_t g_CheatAngle;\n0x0051BD10  int32_t g_CheatTurn;\n0x0051A308  ROOM_LIGHT_TABLE g_RoomLightTables[32]; // WIBBLE_SIZE\n0x005251C0  LIGHT g_DynamicLights[10]; // MAX_DYNAMIC_LIGHTS\n0x0051B908  int32_t g_RoomLightShades[4];\n0x00526194  int32_t g_SunsetTimer;\n0x00521CD0  int32_t g_IsFirstHair;\n0x00521CE0  XYZ_32 g_HairVelocity[7]; // HAIR_SEGMENTS + 1\n0x00521D40  HAIR_SEGMENT g_HairSegments[7]; // HAIR_SEGMENTS + 1\n0x004D7918  int32_t g_HairWind;\n0x004641E0  BITE g_BigEelBite;\n0x00466210  BITE g_BarracudaBite;\n0x00466220  BITE g_SharkBite;\n0x00466118  BITE g_MouseBite;\n0x00465F80  BITE g_Cultist1Gun;\n0x004642C8  BITE g_Cultist2LeftHand;\n0x004642D8  BITE g_Cultist2RightHand;\n0x00465F90  BITE g_Cultist3LeftGun;\n0x00465FA0  BITE g_Cultist3RightGun;\n0x00465FD0  BITE g_Bandit1Gun;\n0x00465FE0  BITE g_Bandit2Gun;\n0x00465FB0  BITE g_Worker1Gun;\n0x00465FC0  BITE g_Worker2Gun;\n0x00464288  BITE g_Worker3Hit;\n0x00464278  BITE g_MonkHit;\n0x004642A8  BITE g_XianSpearmanRightSpear;\n0x00464298  BITE g_XianSpearmanLeftSpear;\n0x004642B8  BITE g_XianKnightSword;\n0x00466360  BITE g_TeethTrapTeeth1A;\n0x00466370  BITE g_TeethTrapTeeth1B;\n0x00466380  BITE g_TeethTrapTeeth2A;\n0x00466390  BITE g_TeethTrapTeeth2B;\n0x004663A0  BITE g_TeethTrapTeeth3A;\n0x004663B0  BITE g_TeethTrapTeeth3B;\n"
  },
  {
    "path": "docs/tr3/INSTALLING.md",
    "content": "# Windows (installer)\n\n## Installing (simplified)\n\n**The TR3 installer is not yet ready, but we'll eventually provide it.**\n\n> [!NOTE]\n> When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI‑based heuristics – they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because:\n>\n> - It isn't signed with a costly commercial certificate.\n> - It's a niche, community‑built project, so not widely recognized.\n> - It's a custom build, not from the Microsoft Store.\n>\n> Don't worry: TRX is open‑source, and you can inspect the code yourself on [GitHub](https://github.com/LostArtefacts/TRX/).\n\n# Windows / Linux\n\n## Installing (manual)\n\n1. Download the TRX zip file.\n2. Extract the zip file into a directory of your choice.  \n     Make sure you choose to overwrite existing directories and files.\n3. If installing for the first time – put your original game files into the target directory.\n\n   Unfortunately, due to legal reasons, we cannot offer an easy packaging of The Lost Artifact expansion pack.\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-win\">\n<pre><code>.\n├── audio\n│   └── cdaudio.wad\n├── cfg\n│   ├── presets\n│   │   ├── tr1-pc.json5\n│   │   ├── tr1-ps1.json5\n│   │   ├── tr2-pc.json5\n│   │   ├── tr2-ps1.json5\n│   │   ├── tr3-pc.json5\n│   │   └── tr3-ps1.json5\n│   ├── tr3\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── tr3-la\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── tr3-level\n│   │   ├── gameflow.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── base_strings-de.json5\n│   ├── base_strings-en-gb.json5\n│   ├── base_strings-fr.json5\n│   ├── base_strings-gd.json5\n│   ├── base_strings-it.json5\n│   ├── base_strings-pl.json5\n│   ├── base_strings-ru.json5\n│   ├── base_strings.json5\n│   ├── catalog_item_actions.csv\n│   ├── catalog_lara_anims.csv\n│   ├── catalog_lara_states.csv\n│   ├── catalog_music.csv\n│   ├── catalog_objects.csv\n│   ├── catalog_samples.csv\n│   ├── inv_ring.json5\n│   ├── outfits.json5\n│   ├── poses.json5\n│   ├── TR3X.json5*\n│   ├── ui.json5\n│   └── weapons.json5\n├── cuts\n│   ├── cut1.tr2\n│   ├── cut2.tr2\n│   ├── cut3.tr2\n│   ├── cut4.tr2\n│   ├── cut5.tr2\n│   ├── cut6.tr2\n│   ├── cut7.tr2\n│   ├── cut8.tr2\n│   ├── cut9.tr2\n│   ├── cut11.tr2\n│   └── cut12.tr2\n├── data\n│   ├── images\n│   │   ├── 3x2\n│   │   │   ├── antarc.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── credit09.webp\n│   │   │   ├── house.webp\n│   │   │   ├── india.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── london.webp\n│   │   │   ├── nevada.webp\n│   │   │   ├── southpac.webp\n│   │   │   ├── theend2.webp\n│   │   │   ├── title_eu.webp\n│   │   │   └── title_us.webp\n│   │   ├── 4x3\n│   │   │   ├── antarc.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── credit09.webp\n│   │   │   ├── house.webp\n│   │   │   ├── india.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── london.webp\n│   │   │   ├── nevada.webp\n│   │   │   ├── southpac.webp\n│   │   │   ├── theend2.webp\n│   │   │   ├── title_eu.webp\n│   │   │   └── title_us.webp\n│   │   ├── og\n│   │   │   ├── antarc.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── credit09.webp\n│   │   │   ├── house.webp\n│   │   │   ├── india.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── london.webp\n│   │   │   ├── nevada.webp\n│   │   │   ├── nevadafff.webp\n│   │   │   ├── southpac.webp\n│   │   │   ├── theend2.webp\n│   │   │   ├── theend.webp\n│   │   │   ├── title_eu.webp\n│   │   │   └── title_us.webp\n│   │   ├── antarc.webp\n│   │   ├── credit01.webp\n│   │   ├── credit02.webp\n│   │   ├── credit03.webp\n│   │   ├── credit04.webp\n│   │   ├── credit05.webp\n│   │   ├── credit06.webp\n│   │   ├── credit07.webp\n│   │   ├── credit08.webp\n│   │   ├── credit09.webp\n│   │   ├── house.webp\n│   │   ├── india.webp\n│   │   ├── legal_eu.webp\n│   │   ├── legal_us.webp\n│   │   ├── london.webp\n│   │   ├── nevada.webp\n│   │   ├── southpac.webp\n│   │   ├── theend2.webp\n│   │   ├── title_eu.webp\n│   │   └── title_us.webp\n│   ├── injections\n│   │   ├── aldwych_fd.bin\n│   │   ├── aldwych_pickup_meshes.bin\n│   │   ├── aldwych_textures.bin\n│   │   ├── antarc_airlock.bin\n│   │   ├── antarc_door134_frames.bin\n│   │   ├── antarc_sky.bin\n│   │   ├── area51_sky.bin\n│   │   ├── area51_textures.bin\n│   │   ├── cavern_door131_frames.bin\n│   │   ├── cavern_pickup_meshes.bin\n│   │   ├── cavern_sky.bin\n│   │   ├── city_textures.bin\n│   │   ├── cliff_door132_frames.bin\n│   │   ├── coastal_airlock.bin\n│   │   ├── coastal_animating_bounds.bin\n│   │   ├── coastal_sky.bin\n│   │   ├── compound_animating_bounds.bin\n│   │   ├── compound_cine.bin\n│   │   ├── compound_textures.bin\n│   │   ├── crash_pickup_meshes.bin\n│   │   ├── crash_sky.bin\n│   │   ├── cut1_setup.bin\n│   │   ├── cut2_setup.bin\n│   │   ├── cut3_setup.bin\n│   │   ├── cut3_shell.bin\n│   │   ├── cut4_setup.bin\n│   │   ├── cut5_setup.bin\n│   │   ├── cut5_textures.bin\n│   │   ├── cut6_setup.bin\n│   │   ├── cut7_setup.bin\n│   │   ├── cut8_setup.bin\n│   │   ├── cut9_setup.bin\n│   │   ├── cut11_setup.bin\n│   │   ├── cut12_setup.bin\n│   │   ├── drill_collision.bin\n│   │   ├── flamethrower_sfx.bin\n│   │   ├── font.bin\n│   │   ├── ganges_door131_frames.bin\n│   │   ├── globe_model.bin\n│   │   ├── gym_sky.bin\n│   │   ├── india_sky.bin\n│   │   ├── lara_animations.bin\n│   │   ├── lara_extra.bin\n│   │   ├── lara_guns.bin\n│   │   ├── lara_gym_guns.bin\n│   │   ├── lara_outfits.bin\n│   │   ├── london_sky.bin\n│   │   ├── luds_diver_animation.bin\n│   │   ├── luds_textures.bin\n│   │   ├── menu_artefacts.bin\n│   │   ├── mines_textures.bin\n│   │   ├── misc_sprites.bin\n│   │   ├── nevada_door132_frames.bin\n│   │   ├── nevada_sky.bin\n│   │   ├── pda_model.bin\n│   │   ├── pickup_aid.bin\n│   │   ├── puna_pickup_meshes.bin\n│   │   ├── rapids_sky.bin\n│   │   ├── reunion_flames.bin\n│   │   ├── scotland_sky.bin\n│   │   ├── stpaul_animating_bounds.bin\n│   │   ├── stpaul_textures.bin\n│   │   ├── tinnos_cameras.bin\n│   │   ├── tinnos_flames.bin\n│   │   ├── undersea_animating_bounds.bin\n│   │   ├── undersea_train.bin\n│   │   ├── willsden_heli.bin\n│   │   └── zoo_train.bin\n│   ├── scripts\n│   │   ├── area51.lua\n│   │   ├── compound.lua\n│   │   ├── crash.lua\n│   │   ├── cut8.lua\n│   │   ├── jungle.lua\n│   │   ├── mines.lua\n│   │   ├── tower.lua\n│   │   └── zoo.lua\n│   ├── antarc.tr2\n│   ├── area51.tr2\n│   ├── chamber.tr2\n│   ├── chunnel.tr2\n│   ├── city.tr2\n│   ├── compound.tr2\n│   ├── crash.tr2\n│   ├── house.tr2\n│   ├── jungle.tr2\n│   ├── main.sfx\n│   ├── main_la.sfx\n│   ├── mines.tr2\n│   ├── nevada.tr2\n│   ├── office.tr2\n│   ├── quadchas.tr2\n│   ├── rapids.tr2\n│   ├── roofs.tr2\n│   ├── scotland.tr2\n│   ├── sewer.tr2\n│   ├── shore.tr2\n│   ├── slinc.tr2\n│   ├── stpaul.tr2\n│   ├── temple.tr2\n│   ├── title.tr2\n│   ├── title_la.tr2\n│   ├── tombpc.dat\n│   ├── tonyboss.tr2\n│   ├── tower.tr2\n│   ├── triboss.tr2\n│   ├── undersea.tr2\n│   ├── willsden.tr2\n│   └── zoo.tr2\n├── fmv\n│   ├── crsh_eng.rpl\n│   ├── endgame.rpl\n│   ├── intr_eng.rpl\n│   ├── logo.rpl\n│   └── sail_eng.rpl\n├── shaders\n│   ├── 2d.glsl\n│   ├── billboard.glsl\n│   ├── common.glsl\n│   ├── fbo.glsl\n│   ├── lights.glsl\n│   ├── meshes.glsl\n│   ├── meshes_tr3.glsl\n│   ├── meshes_tr12.glsl\n│   └── ui.glsl\n└── TRX.exe</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n\n## Playing the game\n\n- To play the game, run `TRX.exe`.\n- To play the Lost Artifact expansion pack, run `TRX.exe --gold`.\n\n# macOS\n\n## Installing\n\n1. Download the latest TRX for TR3 installer image (e.g `TRX-0.1-Mac-tr3.dmg`). Mount the image and drag TR3X to the Applications folder.\n2. Run TR3X from the Applications folder. This will show you an error dialog about missing game data files. This is expected at this point, as you have not copied them in yet. However, it's important to run the app first to allow macOS to verify the app bundle's signature.\n3. Find TR3X in your Applications folder. Right-click it and click \"Show Package Contents\".\n4. Copy your Tomb Raider 3 game data files into `Contents/Resources`. (See the Windows / Linux instructions for retrieving game data from e.g. GOG.)\n\nIn case you see a popup \"TR3X is damaged\" when you run the game, run `xattr -cr /Applications/TR3X.app`.\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-mac\">\n<pre><code>.\n└── Contents\n    ├── Resources\n    │   ├── audio\n    │   │   └── cdaudio.wad\n    │   ├── cfg\n    │   │   ├── presets\n    │   │   │   ├── tr1-pc.json5\n    │   │   │   ├── tr1-ps1.json5\n    │   │   │   ├── tr2-pc.json5\n    │   │   │   ├── tr2-ps1.json5\n    │   │   │   ├── tr3-pc.json5\n    │   │   │   └── tr3-ps1.json5\n    │   │   ├── tr3\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr3-la\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-de.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   └── strings.json5\n    │   │   ├── tr3-level\n    │   │   │   ├── gameflow.json5\n    │   │   │   ├── strings-it.json5\n    │   │   │   ├── strings-pl.json5\n    │   │   │   └── strings.json5\n    │   │   ├── base_strings-de.json5\n    │   │   ├── base_strings-en-gb.json5\n    │   │   ├── base_strings-fr.json5\n    │   │   ├── base_strings-gd.json5\n    │   │   ├── base_strings-it.json5\n    │   │   ├── base_strings-pl.json5\n    │   │   ├── base_strings-ru.json5\n    │   │   ├── base_strings.json5\n    │   │   ├── catalog_item_actions.csv\n    │   │   ├── catalog_lara_anims.csv\n    │   │   ├── catalog_lara_states.csv\n    │   │   ├── catalog_music.csv\n    │   │   ├── catalog_objects.csv\n    │   │   ├── catalog_samples.csv\n    │   │   ├── inv_ring.json5\n    │   │   ├── outfits.json5\n    │   │   ├── poses.json5\n    │   │   ├── ui.json5\n    │   │   └── weapons.json5\n    │   ├── cuts\n    │   │   ├── cut1.tr2\n    │   │   ├── cut2.tr2\n    │   │   ├── cut3.tr2\n    │   │   ├── cut4.tr2\n    │   │   ├── cut5.tr2\n    │   │   ├── cut6.tr2\n    │   │   ├── cut7.tr2\n    │   │   ├── cut8.tr2\n    │   │   ├── cut9.tr2\n    │   │   ├── cut11.tr2\n    │   │   └── cut12.tr2\n    │   ├── data\n    │   │   ├── images\n    │   │   │   ├── 3x2\n    │   │   │   │   ├── antarc.webp\n    │   │   │   │   ├── credit01.webp\n    │   │   │   │   ├── credit02.webp\n    │   │   │   │   ├── credit03.webp\n    │   │   │   │   ├── credit04.webp\n    │   │   │   │   ├── credit05.webp\n    │   │   │   │   ├── credit06.webp\n    │   │   │   │   ├── credit07.webp\n    │   │   │   │   ├── credit08.webp\n    │   │   │   │   ├── credit09.webp\n    │   │   │   │   ├── house.webp\n    │   │   │   │   ├── india.webp\n    │   │   │   │   ├── legal_eu.webp\n    │   │   │   │   ├── legal_us.webp\n    │   │   │   │   ├── london.webp\n    │   │   │   │   ├── nevada.webp\n    │   │   │   │   ├── southpac.webp\n    │   │   │   │   ├── theend2.webp\n    │   │   │   │   ├── title_eu.webp\n    │   │   │   │   └── title_us.webp\n    │   │   │   ├── 4x3\n    │   │   │   │   ├── antarc.webp\n    │   │   │   │   ├── credit01.webp\n    │   │   │   │   ├── credit02.webp\n    │   │   │   │   ├── credit03.webp\n    │   │   │   │   ├── credit04.webp\n    │   │   │   │   ├── credit05.webp\n    │   │   │   │   ├── credit06.webp\n    │   │   │   │   ├── credit07.webp\n    │   │   │   │   ├── credit08.webp\n    │   │   │   │   ├── credit09.webp\n    │   │   │   │   ├── house.webp\n    │   │   │   │   ├── india.webp\n    │   │   │   │   ├── legal_eu.webp\n    │   │   │   │   ├── legal_us.webp\n    │   │   │   │   ├── london.webp\n    │   │   │   │   ├── nevada.webp\n    │   │   │   │   ├── southpac.webp\n    │   │   │   │   ├── theend2.webp\n    │   │   │   │   ├── title_eu.webp\n    │   │   │   │   └── title_us.webp\n    │   │   │   ├── og\n    │   │   │   │   ├── antarc.webp\n    │   │   │   │   ├── credit01.webp\n    │   │   │   │   ├── credit02.webp\n    │   │   │   │   ├── credit03.webp\n    │   │   │   │   ├── credit04.webp\n    │   │   │   │   ├── credit05.webp\n    │   │   │   │   ├── credit06.webp\n    │   │   │   │   ├── credit07.webp\n    │   │   │   │   ├── credit08.webp\n    │   │   │   │   ├── credit09.webp\n    │   │   │   │   ├── house.webp\n    │   │   │   │   ├── india.webp\n    │   │   │   │   ├── legal_eu.webp\n    │   │   │   │   ├── legal_us.webp\n    │   │   │   │   ├── london.webp\n    │   │   │   │   ├── nevada.webp\n    │   │   │   │   ├── nevadafff.webp\n    │   │   │   │   ├── southpac.webp\n    │   │   │   │   ├── theend2.webp\n    │   │   │   │   ├── theend.webp\n    │   │   │   │   ├── title_eu.webp\n    │   │   │   │   └── title_us.webp\n    │   │   │   ├── antarc.webp\n    │   │   │   ├── credit01.webp\n    │   │   │   ├── credit02.webp\n    │   │   │   ├── credit03.webp\n    │   │   │   ├── credit04.webp\n    │   │   │   ├── credit05.webp\n    │   │   │   ├── credit06.webp\n    │   │   │   ├── credit07.webp\n    │   │   │   ├── credit08.webp\n    │   │   │   ├── credit09.webp\n    │   │   │   ├── house.webp\n    │   │   │   ├── india.webp\n    │   │   │   ├── legal_eu.webp\n    │   │   │   ├── legal_us.webp\n    │   │   │   ├── london.webp\n    │   │   │   ├── nevada.webp\n    │   │   │   ├── southpac.webp\n    │   │   │   ├── theend2.webp\n    │   │   │   ├── title_eu.webp\n    │   │   │   └── title_us.webp\n    │   │   ├── injections\n    │   │   │   ├── aldwych_fd.bin\n    │   │   │   ├── aldwych_pickup_meshes.bin\n    │   │   │   ├── aldwych_textures.bin\n    │   │   │   ├── antarc_airlock.bin\n    │   │   │   ├── antarc_door134_frames.bin\n    │   │   │   ├── antarc_sky.bin\n    │   │   │   ├── area51_sky.bin\n    │   │   │   ├── area51_textures.bin\n    │   │   │   ├── cavern_door131_frames.bin\n    │   │   │   ├── cavern_pickup_meshes.bin\n    │   │   │   ├── cavern_sky.bin\n    │   │   │   ├── city_textures.bin\n    │   │   │   ├── cliff_door132_frames.bin\n    │   │   │   ├── coastal_airlock.bin\n    │   │   │   ├── coastal_animating_bounds.bin\n    │   │   │   ├── coastal_sky.bin\n    │   │   │   ├── compound_animating_bounds.bin\n    │   │   │   ├── compound_cine.bin\n    │   │   │   ├── compound_textures.bin\n    │   │   │   ├── crash_pickup_meshes.bin\n    │   │   │   ├── crash_sky.bin\n    │   │   │   ├── cut1_setup.bin\n    │   │   │   ├── cut2_setup.bin\n    │   │   │   ├── cut3_setup.bin\n    │   │   │   ├── cut3_shell.bin\n    │   │   │   ├── cut4_setup.bin\n    │   │   │   ├── cut5_setup.bin\n    │   │   │   ├── cut5_textures.bin\n    │   │   │   ├── cut6_setup.bin\n    │   │   │   ├── cut7_setup.bin\n    │   │   │   ├── cut8_setup.bin\n    │   │   │   ├── cut9_setup.bin\n    │   │   │   ├── cut11_setup.bin\n    │   │   │   ├── cut12_setup.bin\n    │   │   │   ├── drill_collision.bin\n    │   │   │   ├── flamethrower_sfx.bin\n    │   │   │   ├── font.bin\n    │   │   │   ├── ganges_door131_frames.bin\n    │   │   │   ├── globe_model.bin\n    │   │   │   ├── gym_sky.bin\n    │   │   │   ├── india_sky.bin\n    │   │   │   ├── lara_animations.bin\n    │   │   │   ├── lara_extra.bin\n    │   │   │   ├── lara_guns.bin\n    │   │   │   ├── lara_gym_guns.bin\n    │   │   │   ├── lara_outfits.bin\n    │   │   │   ├── london_sky.bin\n    │   │   │   ├── luds_diver_animation.bin\n    │   │   │   ├── luds_textures.bin\n    │   │   │   ├── menu_artefacts.bin\n    │   │   │   ├── mines_textures.bin\n    │   │   │   ├── misc_sprites.bin\n    │   │   │   ├── nevada_door132_frames.bin\n    │   │   │   ├── nevada_sky.bin\n    │   │   │   ├── pda_model.bin\n    │   │   │   ├── pickup_aid.bin\n    │   │   │   ├── puna_pickup_meshes.bin\n    │   │   │   ├── rapids_sky.bin\n    │   │   │   ├── reunion_flames.bin\n    │   │   │   ├── scotland_sky.bin\n    │   │   │   ├── stpaul_animating_bounds.bin\n    │   │   │   ├── stpaul_textures.bin\n    │   │   │   ├── tinnos_cameras.bin\n    │   │   │   ├── tinnos_flames.bin\n    │   │   │   ├── undersea_animating_bounds.bin\n    │   │   │   ├── undersea_train.bin\n    │   │   │   ├── willsden_heli.bin\n    │   │   │   └── zoo_train.bin\n    │   │   ├── scripts\n    │   │   │   ├── area51.lua\n    │   │   │   ├── compound.lua\n    │   │   │   ├── crash.lua\n    │   │   │   ├── cut8.lua\n    │   │   │   ├── jungle.lua\n    │   │   │   ├── mines.lua\n    │   │   │   ├── tower.lua\n    │   │   │   └── zoo.lua\n    │   │   ├── antarc.tr2\n    │   │   ├── area51.tr2\n    │   │   ├── chamber.tr2\n    │   │   ├── chunnel.tr2\n    │   │   ├── city.tr2\n    │   │   ├── compound.tr2\n    │   │   ├── crash.tr2\n    │   │   ├── house.tr2\n    │   │   ├── jungle.tr2\n    │   │   ├── main.sfx\n    │   │   ├── main_la.sfx\n    │   │   ├── mines.tr2\n    │   │   ├── nevada.tr2\n    │   │   ├── office.tr2\n    │   │   ├── quadchas.tr2\n    │   │   ├── rapids.tr2\n    │   │   ├── roofs.tr2\n    │   │   ├── scotland.tr2\n    │   │   ├── sewer.tr2\n    │   │   ├── shore.tr2\n    │   │   ├── slinc.tr2\n    │   │   ├── stpaul.tr2\n    │   │   ├── temple.tr2\n    │   │   ├── title.tr2\n    │   │   ├── title_la.tr2\n    │   │   ├── tombpc.dat\n    │   │   ├── tonyboss.tr2\n    │   │   ├── tower.tr2\n    │   │   ├── triboss.tr2\n    │   │   ├── undersea.tr2\n    │   │   ├── willsden.tr2\n    │   │   └── zoo.tr2\n    │   ├── fmv\n    │   │   ├── crsh_eng.rpl\n    │   │   ├── endgame.rpl\n    │   │   ├── intr_eng.rpl\n    │   │   ├── logo.rpl\n    │   │   └── sail_eng.rpl\n    │   ├── shaders\n    │   │   ├── 2d.glsl\n    │   │   ├── billboard.glsl\n    │   │   ├── common.glsl\n    │   │   ├── fbo.glsl\n    │   │   ├── lights.glsl\n    │   │   ├── meshes.glsl\n    │   │   ├── meshes_tr3.glsl\n    │   │   ├── meshes_tr12.glsl\n    │   │   └── ui.glsl\n    │   └── icon.icns\n    ├── _CodeSignature\n    ├── Frameworks\n    ├── info.plist\n    └── MacOS</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n"
  },
  {
    "path": "docs/trx/CATALOGS.md",
    "content": "---\ntitle: Catalogs\norder: 7\n---\n\n# Catalogs\n\nTombEditor normally lets builders manage WADs and object slots through its\ncatalogs. The snag is that the game engine itself still references certain\nslots directly in code. That means if a builder repurposes one of those\nhardcoded slots for an animation command, they might get an ugly surprise when\nanother in‑game object tries to use that same slot behind the scenes.\n\nThat's where TRX catalogs come in.\n\nBefore TRX 1.0, all object IDs and music tracks were hardcoded for TR1 and TR2.\nNow, builders can freely re‑assign those IDs however they want. In the future,\nthe original lists will allow extensions by including objects from other games!\n\nUnder the hood, each entity is identified by its stable name string. The catalog\nmaps numeric slots to these name keys, so when the engine references an entity,\nit can grab the correct sample or resource tied to that slot.\n\nCatalogs are just comma‑separated value (CSV) files you can edit with any text\neditor, including Notepad or Excel. They live in the `cfg/` folder and must be\npresent for the game to function properly.\n\nTRX catalogs only include data that's directly referenced by the game's code.\nEntries used *only* in animation commands or other editor‑controlled behaviors\naren't included, since those can already be managed freely within the level\neditor.\n\n## Working example\n\nLet's say, as a builder, you wish to use the bird monster from TR2 in a TR1\nlevel. Here are the necessary steps to set this up.\n\n1. Make a note of the OG TR2 slot for the bird monster e.g. from WadTool or\ntrview. In this case, it's `46`.\n2. Look-up the object name for TR2. To do this, download a copy of TRX with TR2\nassets and open the `cfg/catalog_objects.csv` file. For slot 46, the name is\n`O_BIRD_GUARDIAN`.\n3. Choose a slot in your TR1 level that you wish to use for this object. You can\npick a new slot, or replace an existing one. Add the object to the slot in\nWadTool and keep a note of the number you chose.\n4. In your TR1 level folder, open `cfg/catalog_objects.csv`. Add an entry on a\nnew line with the slot number and the object name, ensuring you have a comma in\nbetween. For example, if you chose slot `247`, the line would be:\n<br/><br/>\n`247, O_BIRD_GUARDIAN`\n<br/><br/>\nIf you chose to replace an existing object, instead of adding a new line, locate\nthat slot in the CSV file and just replace the object name.\n\nThat's it! You can now place a bird monster in your level in TombEditor. You can\nalso proceed to check which sounds the bird monster uses in its animations in\nWadTool and set up appropriate ones for TR1.\n"
  },
  {
    "path": "docs/trx/COMMANDS.md",
    "content": "---\ntitle: Commands\norder: 2\n---\n\n# Commands\n\nTRX introduces a developer console, by default accessible with the <kbd>/</kbd> key.\nThis key can be rebound in the controls dialog to anything of your choice. Note that\nwhere <kbd>/</kbd> is used in command documentation, you should interpret that as\nwhichever key you have bound, and not include it as part of the command itself.\n\n## Gameplay commands\n\n- `/help`  \n  `/help {command}`  \n  Shows a list of the available commands or a detailed help message for the chosen one. Even Lara needs a lifeline!\n\n- `/pos`  \n  Reveals Lara's exact coordinates in the universe. Knowledge is power!\n\n- `/tp {room_number}`  \n  `/tp room {room_number}`  \n  `/tp r{room_number}`  \n  `/tp item {item_number}`  \n  `/tp i{item_number}`  \n  `/tp precise {x} {y} {z}`  \n  `/tp {x} {y} {z}`  \n  `/tp {object}`  \n  `/tp enemy`  \n  `/tp pickup`  \n  Instant travel! Teleports Lara to:\n  - a random spot within the specified room;\n  - an item's position by item number;\n  - the specified X,Y,Z coordinates in grid units;\n  - the specified world-space coordinates with `precise` (no `1024` scaling);\n  - the next pickup in round-robin order with `pickup`;\n  - the next hostile creature in round-robin order with `enemy`;\n  - the nearest object of a specific type.\n  - legacy: `/tp {room_number}` is still accepted.\n\n- `/hp`  \n  `/hp {health}`  \n  Displays or sets Lara's health. Tougher trials await!\n\n- `/heal`  \n  Tough day, Lara? Heals our girl back to full health.\n\n- `/give {item_name}`  \n  `/give {num} {item_name}`  \n  `/give all`  \n  `/give guns` or `/guns`  \n  `/give moreguns` or `/moreguns`  \n  `/give keys` or `/keys`  \n  Gives Lara an item. Try `/give guns` to arm her to the teeth, and `/give keys` to get her all important puzzle items. Ain't nobody got time for searching!\n\n- `/secret`  \n  `/secret take`  \n  `/secret take {num}`  \n  `/secret give`  \n  `/secret give {num}`  \n  Uncovers Lara's secret stash: list discovered secrets, pilfer one or all with `take`, or gift one or all back with `give`.\n\n- `/kill`  \n  `/kill all`  \n  `/kill {enemy_type}`  \n  Tired of all of those pesky creatures and goons trying to spoil your adventure? Instantly dispose of the nearest one, or kill them all at once.\n\n- `/fly`  \n  `/fly on`  \n  `/fly off`  \n  Turns on the fly cheat. Why even walk? Levitate like a legend.\n\n- `/immune`  \n  `/immune on`  \n  `/immune off`  \n  Turns on immunity, making Lara impervious to harm. Perfect for when you'd rather explore every nook than tiptoe past traps.\n\n- `/restless`  \n  `/restless on`  \n  `/restless off`  \n  Turns on infinite sprint. Lara's always been a speedster, but with this, even cheetahs are asking her for running tips!\n\n- `/teatime`  \n  Calls your loyal butler to whatever ends of the world you're exploring right now. Effective immediately.\n\n- `/spawn {object}`  \n  Spawn an object of your choice. Not guaranteed to behave, but good for testing and oddly therapeutic for goofing off.\n\n- `/trigger {item_num}`  \n  `/trigger {item_name}`  \n  `/trigger {object}`  \n  Force-triggers one or more items by their item ID, item name, or object name. Great for testing switches, traps, and scripted events.\n\n- `/untrigger {item_num}`  \n  `/untrigger {item_name}`  \n  `/untrigger {object}`  \n  Reverses `/trigger` for one or more items.\n\n## Configuration commands\n\n- `/set {option}`  \n  `/set {option} {value}`  \n  `/set {option} -`  \n  Retrieve or change specific configuration options, like a tech-savvy wizard.\n  - use `-` as `{value}` to restore the option to default.\n  - some options need a game or level re-launch to apply.\n  - option names use `-`, not `_`, because reasons.\n\n- `/cheats on`  \n  `/cheats off`  \n  Enables or disables the cheater's toolkit. But let's face it – you're reading _this_, so that ship has sailed.\n\n- `/braid on`  \n  `/braid off`  \n  Toggle Lara's braid like it's a fashion accessory. Hair today, gone tomorrow.\n\n- `/wireframe on`  \n  `/wireframe off`  \n  Enables or disables the wireframe mode. Enter the debugging realm!\n\n- `/lighting on`  \n  `/lighting off`  \n  Enables or disables the lighting system. Bask in dynamic shadows or embrace bright clarity!\n\n- `/textures on`  \n  `/textures off`  \n  Enables or disables texture rendering. Peek the exact polygons that power these pretty visuals!\n\n- `/debug on`  \n  `/debug off`  \n  `/debug {option} on`  \n  `/debug {option} off`  \n  Toggles debug mode, turning your screen into a glorious display of dev scribbles.\n  - floor triggers - enemy skips incoming!\n  - room portals - wait, there are _how_ many rooms?!\n  - room clip rectangles – the source of developers nightmares.\n  - object mesh spheres - see hitboxes in their natural habitat.\n  - Lara's position and animation details - nerdy stats, you've gotta love them.\n  - bounding boxes – to marvel at the collision code.\n\n- `/speed`  \n  `/speed {num}`  \n  Displays or sets the speed of the game. Because sometimes you want to moonwalk through mayhem.\n\n- `/vsync on`  \n  `/vsync off`  \n  Turns vertical sync on or off. For the smooth freaks among us.\n\n- `/fps`  \n  `/fps {num}`  \n  Displays or sets the game's frames per second. Higher FPS = smoother Lara.\n\n- `/weather off`  \n  `/weather snow`  \n  `/weather rain`  \n  Changes the current level weather. Your game, your forecast.\n\n## Environmental commands\n\n- `/flip`  \n  `/flip off`  \n  `/flip on`  \n  Switches the global flipmap on or off. Turn the reality around you on its head.\n\n- `/flood`  \n  `/drain`  \n  `/flood {room_num}`  \n  `/drain {room_num}`  \n  Floods or drains rooms at will. Act like you're Poseidon with a plumbing license, for when drowning is preferable to puzzles!\n\n- `/music {track_id}`  \n  Plays a music track by its ID. Perfect for setting the mood at will.\n\n- `/sfx`  \n  `/sfx {sound}`  \n  Plays a sound effect on demand. Because sometimes you just need Lara to grunt on cue.\n\n## Game flow commands\n\n- `/endlevel`  \n  `/nextlevel`  \n  Too cool to finish puzzles? Smash-cut to the ending! Lara doesn't have time for this nonsense.\n\n- `/level {num}`  \n  `/level {name}`  \n  `/play {num}`  \n  `/play {name}`  \n  Launches any level you like. Start with `/play 0` to warm up in the gym – or skip straight to the danger zone with `/play 1` onwards.\n\n- `/cut {num}`  \n  `/cutscene {num}`  \n  Plays a dramatic cinematic. Follow the lore!\n\n- `/gym`  \n  `/home`  \n  Sends Lara to her humble abode. Even tomb raiders can't skip leg day.\n\n- `/save {slot_num}`  \n  `/save quick`  \n  `/quicksave`  \n  `/qs`  \n  Save your progress to a normal slot, or do a rotating quick-save.\n\n- `/load {slot_num}`  \n  `/load quick [slot_num]`  \n  `/load q[slot_num]`  \n  `/quickload [slot_num]`  \n  `/ql [slot_num]`  \n  Time-travel to a previous normal save, or load a quick-save by sorted quick-save position (most recent is `1`).\n\n- `/demo`  \n  `/demo {num}`  \n  Starts a demo. No number? They'll just cycle.\n\n- `/title`  \n  Had enough? Let's return to the main menu.\n\n- `/mod {name}`  \n  Switches to another installed game/mod pack and reloads the game flow. Great for hopping between adventures without relaunching TRX.\n\n- `/exit`  \n  Closes the game. Ends the adventure. We're done here.\n\n## Miscellaneous commands\n\n- `/cls`  \n  `/clear`\n  Wipes the console logs, quickly erasing all traces of your cheat spree (or that ugly pile of debug misery).\n\n- `/strings`  \n  Reloads the current language files on the fly. Très utile for translators.\n\n- `/screenshot [path]`  \n  Commemorates Lara's antics by taking a picture and saving it to the optional path (relative to the game root directory).\n\n- `/lua {string}`\n  Type any LUA code to run it on the spot. Proceed with caution, or at least a sense of adventure!\n"
  },
  {
    "path": "docs/trx/COMMAND_LINE.md",
    "content": "---\ntitle: CLI options\norder: 1\n---\n\n# Command line options\n\nCurrently the following command line interface options are available:\n\n- `--mod <MOD_ID>`  \n  Runs a specific game or mod directly.\n  Available mods:\n  - `tr1` (Tomb Raider I)\n  - `tr1-ub` (Tomb Raider I: Unfinished Business)\n  - `tr1-demo-pc` (Tomb Raider I: PC Demo)\n  - `tr2` (Tomb Raider II)\n  - `tr2-gm` (Tomb Raider II: The Golden Mask)\n  - `tr3` (Tomb Raider III)\n  - `tr3-la` (Tomb Raider III: The Lost Artifact)\n\n  TRX remembers the last selected game or expansion when no explicit startup\n  option is given. Because of that, `TRX.exe` starts whichever game or\n  expansion was selected most recently. Use `TRX.exe --mod` to force the\n  matching expansion pack from a shortcut.\n\n- `-l <path|num>`, `--level <path|num>`  \n  Runs the game immediately launching it into the specified level. If `<path>`\n  is provided, runs the custom level located in the specified location. If the\n  path is relative, it is resolved from the current working directory first,\n  then from the game directory. Internally, this option uses\n  `tr*-level/gameflow.json5` as a template instructing it how to run the game.\n  If `<num>` is an integer, plays the level with the given number within the\n  main game flow (1-based).\n\n- `-s <num>`, `--save <num>`:  \n  Runs the game immediately loading a specific save slot. The first save starts\n  at `num=1`. This option can be combined with `-l`/`--level`.\n\n- `--test-record <PATH>`:  \n  Records gameplay events to an external text file.\n\n- `--test-replay <PATH>`, `--test-play <PATH>`:  \n  Replays gameplay events from an external text file.\n\n- `--headless`:  \n  Runs the game in command line only. Only available with `--test-replay`.\n\n- `--headless-fps <num>`:  \n  In headless mode, forces the simulation to run at a constant FPS. If omitted\n  or zero, uses the FPS from the configuration.\n\n- `--debug-render-performance`:  \n  Outputs diagnostic information related to GPU usage and throughput.\n\n- `-q`, `--quiet`  \n  Suppresses most of output to the standard output, keeping only errors.\n  The log file is written to normally.\n\n> [!TIP]\n> If you want `TRX.exe` to start the main game again after using an expansion\n> shortcut, switch back to the main game in the passport first, or launch it\n> with `--mod`/`--engine` from the shortcut as needed.\n\n> [!NOTE]\n> Gameplay capture is considered an internal testing tool, and may be\n> subject to breaking changes without warnings.\n\n# Legacy command line options\n\n- `-g`, `--gold` (legacy: `-gold`)  \n  Runs the Unfinished Business or the Golden Mask expansion pack, depending\n  on the last launched game. Please use `--mod` option instead.\n\n- `--demo-pc` (legacy: `-demo_pc`) (TR1 only)  \n  Runs the PC demo level. Please use `--mod` option instead.\n"
  },
  {
    "path": "docs/trx/ENEMY_DEFAULTS.md",
    "content": "---\ntitle: Enemy defaults\norder: 14\n---\n\n# Enemy defaults\n\n### Tomb Raider 1\n\n| Game ID | TRX ID              | Hit points |\n| ------- | ------------------- | ---------- |\n| `7`     | O_WOLF              | `6`        |\n| `8`     | O_BEAR              | `20`       |\n| `9`     | O_BAT               | `1`        |\n| `10`    | O_CROCODILE         | `20`       |\n| `11`    | O_ALLIGATOR         | `20`       |\n| `12`    | O_LION              | `30`       |\n| `13`    | O_LIONESS           | `25`       |\n| `14`    | O_PUMA              | `45`       |\n| `15`    | O_APE               | `22`       |\n| `16`    | O_RAT               | `5`        |\n| `17`    | O_VOLE              | `5`        |\n| `18`    | O_TREX              | `100`      |\n| `19`    | O_RAPTOR            | `20`       |\n| `20`    | O_ATLANTEAN_WINGED  | `50`       |\n| `21`    | O_ATLANTEAN_SHOOTER | `50`       |\n| `22`    | O_ATLANTEAN_GROUND  | `50`       |\n| `23`    | O_CENTAUR           | `120`      |\n| `24`    | O_MUMMY             | `18`       |\n| `25`    | O_DINO_WARRIOR      | `100`      |\n| `26`    | O_FISH              | `12`       |\n| `27`    | O_LARSON            | `50`       |\n| `28`    | O_PIERRE            | `70`       |\n| `30`    | O_SKATEKID          | `125`      |\n| `31`    | O_COWBOY            | `150`      |\n| `32`    | O_BALDY             | `200`      |\n| `33`    | O_NATLA             | `400`      |\n| `34`    | O_TORSO             | `500`      |\n| `145`   | O_SCION_ITEM_3      | `5`        |\n\n### Tomb Raider 2\n\n| Game ID | TRX ID          | Hit points |\n| ------- | --------------- | ---------- |\n| `15`    | O_DOG           | `10`       |\n| `16`    | O_CULT_1        | `25`       |\n| `17`    | O_CULT_1A       | `25`       |\n| `18`    | O_CULT_1B       | `25`       |\n| `19`    | O_CULT_2        | `60`       |\n| `20`    | O_CULT_3        | `150`      |\n| `21`    | O_MOUSE         | `4`        |\n| `22`    | O_DRAGON_FRONT  | `300`      |\n| `25`    | O_SHARK         | `30`       |\n| `26`    | O_EEL           | `5`        |\n| `27`    | O_BIG_EEL       | `20`       |\n| `28`    | O_BARRACUDA     | `12`       |\n| `29`    | O_DIVER         | `20`       |\n| `30`    | O_WORKER_1      | `25`       |\n| `31`    | O_WORKER_2      | `20`       |\n| `32`    | O_WORKER_3      | `27`       |\n| `33`    | O_WORKER_4      | `27`       |\n| `34`    | O_WORKER_5      | `20`       |\n| `35`    | O_JELLY         | `10`       |\n| `36`    | O_SPIDER        | `5`        |\n| `37`    | O_BIG_SPIDER    | `40`       |\n| `38`    | O_CROW          | `15`       |\n| `39`    | O_TIGER         | `20`       |\n| `41`    | O_XIAN_SPEARMAN | `100`      |\n| `43`    | O_XIAN_KNIGHT   | `80`       |\n| `45`    | O_YETI          | `30`       |\n| `46`    | O_BIRD_GUARDIAN | `200`      |\n| `47`    | O_EAGLE         | `20`       |\n| `48`    | O_BANDIT_1      | `45`       |\n| `49`    | O_BANDIT_2      | `50`       |\n| `50`    | O_BANDIT_2B     | `50`       |\n| `53`    | O_MONK_1        | `30`       |\n| `54`    | O_MONK_2        | `30`       |\n| `214`   | O_TREX          | `100`      |\n| `265`   | O_BEAR          | `30`       |\n| `266`   | O_WOLF          | `10`       |\n| `267`   | O_MONK_3        | `30`       |\n"
  },
  {
    "path": "docs/trx/GAME_STRINGS.md",
    "content": "---\ntitle: Game strings\norder: 4\n---\n\n# Game Strings\n\n## Overview\n\nThis document describes how to use and translate the TRX game strings. Game\nstrings let level builders customize built-in object and level names in custom\nlevels, and translators translate the entire game (including UI) into other\nlanguages. The configuration is structured in JSON5 format, which permits\ncomments and supports a layering mechanism for overrides.\n\n## Audiences\n\nThis document serves two main audiences:\n\n- **Level builders**: Customize object names, level titles, and UI strings for\n  custom levels and mod packs.\n- **Translators**: Translate the full game text (gameplay, UI, and menus) into\n  different languages.\n\n## Quick‑start example\n\n```json5\n{\n    // Override only the key_1 pickup in Level 0\n    \"levels\": [\n        {\n            \"title\": \"City of Vilcabamba\",\n            \"objects\": { \"key_1\": { \"name\": \"Gold Key\" } }\n        }\n    ]\n}\n```\n\n## Layering and Override Mechanism\n\nTRX supports multiple layers of strings files that are loaded in a specific\norder. Later layers override earlier ones, allowing expansion packs and custom\nmods to selectively override base text. The default load order is:\n\n1. `base_strings.json5` — Common defaults for all engines and languages.\n2. `tr1/strings.json5` — Base strings for the main game.\n3. `tr1-ub/strings.json5` — Overrides for the Unfinished Business expansion pack\n   (or other expansion packs/mods).\n\n   Depending on which mod or pack you run, the third layer may vary:\n   - `tr1-ub/strings.json5` for the Unfinished Business expansion pack\n   - `tr2-gm/strings.json5` for the Golden Mask TR2 expansion pack\n   - `tr1-demo-pc/strings.json5` for the PC TR1 demo\n   - `tr*-level/strings.json5` for the -l/--level command line switch\n\nFor example, if the same key exists in both `base_strings.json5` and\n`tr1/strings.json5`, the value `tr1/strings.json5` will take precedence.\n\nEach layer can also have a variant translated to other languages - see\n[this section](./4-GAME_STRINGS.md#translating-each-layer).\n\n## General structure\n\nThe document is organized as follows:\n\n```json5\n{\n    \"language_name\": \"English\",\n    \"levels\": [\n        {\n            \"title\": \"City of Vilcabamba\",\n            \"objects\": {\n                \"key_1\": {\n                    \"name\": \"Silver Key\",\n                    \"description\": \"This shows when the player examines key1 in the inventory.\",\n                },\n                \"puzzle_1\": {\n                    \"name\": \"Gold Idol\",\n                    \"description\": \"You can use \\n to make new lines and \\f to make new pages.\",\n                },\n                \"key_2\": {\n                    \"name\": \"Rusty Key\",\n                },\n                // etc\n            },\n            \"general\": {\n                \"stats\": {\n                    \"time_taken\": \"Time Taken\",\n                    // etc\n                },\n            },\n        },\n        // etc\n    ],\n    \"general\": {\n        \"stats\": {\n            \"time_taken\": \"Time Taken\",\n            // etc\n        },\n    },\n    \"console\": {\n        \"cmd\": {\n            \"help\": {\n                \"help\": \"Shows help for all commands or detailed help for one.\",\n            },\n        },\n    },\n    \"dynamic\": {\n        \"enums\": {\n            \"lara_outfit\": {\n                \"default\": \"Default\",\n                \"tr1_classic\": \"TR1 Classic\",\n                // etc\n            },\n        },\n    },\n    \"enums\": {\n        \"ASPECT_MODE\": {\n            \"ASPECT_MODE_ANY\": \"Any\",\n            // etc\n        },\n        // etc\n    },\n    \"settings\": {\n        \"visuals.lara_outfit\": {\n            \"title\": \"Lara's outfit\",\n            \"description\": \"Changes Lara's appearance.\",\n        },\n        // etc\n    },\n    \"objects\": {\n        \"lara\": {\"name\": \"Lara\"},\n        \"dog\": {\"names\": [\"Dog\", \"Doberman\"]},\n        // etc\n    }\n}\n```\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Required</th>\n    <th>Scope / Description</th>\n  </tr>\n\n  <tr valign=\"top\">\n    <td><code>extends</code></td>\n    <td>String</td>\n    <td>No</td>\n    <td>\n      Fallback to another language code for missing entries. For dialects (e.g., \"fr-ca\"),\n      specify <code>\"extends\": \"fr\"</code> to inherit missing layers from the parent language.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td><code>language_name</code></td>\n    <td>String</td>\n    <td>No (only in common file)</td>\n    <td>\n      The display name of the language (e.g., \"English\", \"Français\") shown in\n      the language selection UI. Should only be defined in the\n      <code>base_strings.json5</code> file.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>levels</code>\n    </td>\n    <td>Object&nbsp;array</td>\n    <td>No</td>\n    <td>\n      This is where overrides for individual level details are defined. If a\n      level doesn't override a string through its <code>objects</code> or\n      nested string sections such as <code>general</code> or\n      <code>console</code>, it'll be looked up in the global scope next.\n      If the global scope doesn't define it either, it'll default to an\n      internal default value shipped with the engine.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>title</code>\n    </td>\n    <td>String</td>\n    <td>Yes</td>\n    <td>\n      Level entry field (<code>levels[].title</code>).\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>objects</code>\n    </td>\n    <td>Object&nbsp;array</td>\n    <td>No</td>\n    <td>\n      Object-related strings.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>general</code>\n    </td>\n    <td>Nested object map</td>\n    <td>No</td>\n    <td>\n      General gameplay, UI, and menu strings in the form\n      <code>general/&lt;group&gt;/&lt;key&gt;</code>.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>console</code>\n    </td>\n    <td>Nested object map</td>\n    <td>No</td>\n    <td>\n      Developer-console labels, help text, and command messages in the form\n      <code>console/&lt;group&gt;/&lt;key&gt;</code>.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>settings</code>\n    </td>\n    <td>Nested object map</td>\n    <td>No</td>\n    <td>\n      Localized setting labels and descriptions, keyed by option path\n      (<code>&lt;option&gt;/title</code> and\n      <code>&lt;option&gt;/description</code>).\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>enums</code>\n    </td>\n    <td>Nested object map</td>\n    <td>No</td>\n    <td>\n      Static enum labels in the form\n      <code>enums/&lt;ENUM_TYPE&gt;/&lt;ENUM_VALUE&gt;</code>.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>dynamic</code>\n    </td>\n    <td>Nested object map</td>\n    <td>No</td>\n    <td>\n      Runtime dynamic-enum labels in the form\n      <code>dynamic/enums/&lt;domain&gt;/&lt;value&gt;</code>.\n    </td>\n  </tr>\n</table>\n\n### Object entry fields\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Required</th>\n    <th>Scope / Description</th>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>name</code>\n    </td>\n    <td>String&nbsp;/&nbsp;String&nbsp;array</td>\n    <td>No</td>\n    <td>\n      Object entry field (<code>objects.&lt;id&gt;.name</code>).\n      Allows renaming any object, including key items and pickups. Can be a\n      list of strings: inventory objects use the first name; additional names\n      can be used with commands like <code>/tp</code> and <code>/give</code>.\n    </td>\n  </tr>\n\n  <tr valign=\"top\">\n    <td>\n      <code>description</code>\n    </td>\n    <td>String</td>\n    <td>No</td>\n    <td>\n      Object entry field (<code>objects.&lt;id&gt;.description</code>).\n      Defines longer text for key and puzzle items. Use <code>\\n</code> for\n      new lines and <code>\\f</code> for page breaks. Empty strings suppress the\n      examine text UI.\n    </td>\n  </tr>\n</table>\n\n> [!NOTE]\n> The `extends` property now refers to another language code, not a file path.\n> When present in the common strings layer, the manager will first load the\n> parent language (and any further `extends` chains), then apply the current\n> file's overrides. Cyclic `extends` chains are detected and will emit a\n> warning rather than infinite recursion.\n\n## Common Object IDs and names\n\n| JSON key   | Object ID (TR1) | Object ID (TR2) | Object ID (TR3) |\n|------------|-----------------|-----------------|-----------------|\n| `key_1`    | 129 and 133     | 193 and 197     | 224 and 228     |\n| `key_2`    | 130 and 134     | 194 and 198     | 225 and 229     |\n| `key_3`    | 131 and 135     | 195 and 199     | 226 and 230     |\n| `key_4`    | 132 and 136     | 196 and 200     | 227 and 231     |\n| `pickup_1` | 141 and 148     | 205 and 207     | 236 and 238     |\n| `pickup_2` | 142 and 149     | 206 and 208     | 237 and 239     |\n| `puzzle_1` | 110 and 114     | 174 and 178     | 205 and 209     |\n| `puzzle_2` | 111 and 115     | 175 and 179     | 206 and 210     |\n| `puzzle_3` | 112 and 116     | 176 and 180     | 207 and 211     |\n| `puzzle_4` | 113 and 117     | 177 and 181     | 208 and 212     |\n| `quest_1`  | -               | -               | 240 and 244     |\n| `quest_2`  | -               | -               | 241 and 245     |\n| `quest_3`  | -               | -               | 242 and 246     |\n| `quest_4`  | -               | -               | 243 and 247     |\n| `secret_1` | -               | 190             | -               |\n| `secret_2` | -               | 191             | -               |\n| `secret_3` | -               | 192             | -               |\n\n> [!NOTE]\n> Nearly all pickup items exist in two forms, as early games differentiate\n> between a sprite displayed on the ground and a 3D object depicted in the\n> inventory ring. Secrets are a notable exception, as they never appear in the\n> inventory ring in the original game. For convenience, both forms are defined\n> using a single key.\n\n## Translation Workflow\n\n### Translating each layer <a name=\"translating-each-layer\"></a>\n\nTo provide localized translations, place language-specific overrides alongside\neach base file using the naming pattern `<basename>-<lang>.json5`. Translation\nfiles must live in the same directory as their base (e.g. `cfg/`). For example,\nto add French translations:\n\n```text\ncfg/base_strings-fr.json5          # common strings\ncfg/tr1/strings-fr.json5           # base game strings\ncfg/tr1-demo-pc/strings-fr.json5   # TR1 demo overrides\ncfg/tr1-level/strings-fr.json5     # custom-level pack overrides\ncfg/tr1-ub/strings-fr.json5        # Unfinished Business overrides\ncfg/tr2/strings-fr.json5           # base game strings\ncfg/tr2-gm/strings-fr.json5        # Golden Mask TR2 overrides\ncfg/tr2-level/strings-fr.json5     # custom-level pack overrides\n```\n\nWhen the game starts, TRX will detect these files and load them in place of the\ndefault layer for that language code (`fr` in this example). Omit any\ntranslation file for a layer you do not need; the game will fall back to the\nEnglish base for that layer by default.\n\n### Live reloading (`/strings`)\n\nTo apply changes to string files without restarting the game, use the `/strings`\nconsole command. It reloads all string layers for the current language (common,\nbase, and mod-specific), making it easy to test translation or custom overrides\non the fly.\n\nIn addition, languages can be switched at runtime without the need to use the\nUI with the `/set language <code>` console command (e.g. `/set language fr`).\n\nNew language files are only detected at the game launch. If you create a new\nlayer file, you'll need to relaunch the game to see the effects.\n\n### Review system\n\nThe development team uses AI-assisted tools to create initial translations.\nAutomated translations are tagged with a special marker `\\{review}` indicating\nthat the text needs human review. By default, review markers are hidden in-game.\nTo enable review mode and display markers, run:\n\n```\n/set review 1\n```\n\nTranslators should remove the `\\{review}` tags once the translation has been\nreviewed and finalized.\n\n### `language_name`\n\nOnly supported in the common strings file (`base_strings.json5`), the\n`language_name` property sets the display name of the language in the options\nmenu. For example:\n\n```json5\n{\n    \"language_name\": \"Français\"\n}\n```\n\n## Custom levels\n\n### General tips\n\n- **Zero-indexed levels**: Levels are zero-indexed and match the order in the\n  game flow file.\n- **Minimal overrides**: Only define the strings you need; the game will fall\n  back to built-in defaults for any missing entries. For example:\n  ```json5\n  {\n      \"levels\": [\n          {\n              \"title\": \"City of Vilcabamba\",\n              \"objects\": {\n                  \"key_1\": {\"name\": \"Gold Key\"}\n              }\n          }\n      ]\n  }\n  ```\n- **Renaming any object**: You can rename puzzle items, keys, enemies, or any\n  other in-game object. For example, TR2 object #&NoBreak;39 (tiger) can be\n  renamed to \"Snow Leopard\" in a winter-themed level.\n\n### Console and object IDs\n\nAll objects have names, even if they are not shown in the UI. Use these names\nwith console commands such as `/tp` or `/give`. For example, `/tp tiger`\nteleports Lara to object #&NoBreak;39. Console commands accept partial,\ncase-insensitive names, and will match unique substrings to objects (powered by\nfuzzy matching). In case the player uses languages other than English, the\ncommands also accept builtin English names (so even though wolf is called a\nwilk in Polish, on top of `/tp wilk` the players can still `/tp wolf`).\n\nFor a complete list of object IDs for a specific engine, refer to the game\nstrings files shipped with the relevant TRX builds.\n"
  },
  {
    "path": "docs/trx/INJECTIONS.md",
    "content": "---\ntitle: Injections (.bin files)\norder: 15\n---\n\n# Injections (`.bin` files)\n\nIn TRX, an *injection* is a binary patch file (`.bin`) that the engine applies\nto level data at load time. Injections are used to fix or extend base game data\nin a way that stays compatible with custom levels (unless you intentionally\nreplace the same data in your own WAD).\n\nMost builders only need injections for the \"default TRX assets\" (extra Lara\nanimations, extended fonts, PDA model, etc).\n\n## How injections are configured (gameflow)\n\nGameflow JSON supports injections in two places:\n\n- **Global injections**: applied to all levels.\n- **Per-level injections**: applied only for that level, optionally inheriting global injections.\n\nBy default, injections defined in the global gameflow are applied to every\nlevel. If a level defines its own injections, those are merged with the global\nset when the level loads.\n\nIndividual levels can set `inherit_injections` to `false`. In that case, global\ninjection files are not used. If such a level defines its own `injections`,\nonly those are applied; if it defines none, nothing is injected.\n\nRelevant keys (names may differ slightly per gameflow version):\n\n```json5\n{\n  // global\n  \"injections\": [\n    \"data/injections/lara_extra.bin\",\n    \"data/injections/font.bin\"\n  ],\n\n  \"levels\": [\n    {\n      \"path\": \"data/levels/MY_LEVEL.TR2\",\n      \"inherit_injections\": true,\n      \"injections\": [\n        \"data/injections/pda_model.bin\"\n      ]\n    }\n  ]\n}\n```\n\n> [!WARNING]\n> If you **import** the assets into your level WAD (see below), you should then\n> **remove** the corresponding `.bin` from gameflow to avoid\n> double-applying/replacing data!\n\n> [!NOTE]\n> If a level should **not** receive the global injections, set\n> `\"inherit_injections\": false` (or omit inheritance, depending on the\n> schema/version you're targeting).\n\n> [!NOTE]\n> The gameflow ignores referenced injection files that do not exist, but it's\n> best practice to remove references to keep gameflow clean.\n\n## Builder workflow: keep the `.bin`, or bake into your WAD\n\nYou can handle TRX default assets in two ways:\n\n1. **Keep using injections** (recommended for most cases):\n   ship the `.bin` files and reference them in gameflow.\n2. **Bake assets into your WAD**:\n   import the provided assets into your level's WAD, then remove the related\n   `.bin` references from gameflow.\n\n### Common steps for importing TRX assets into your WAD (WadTool)\n\n1. Open your level's WAD in **WadTool**.\n2. Open the extracted `.wad2` file for the applicable game as the **source level** in WadTool.\n3. Move the required assets from the source to the destination, replacing the existing ones.\n4. Follow any asset-specific notes in the table below.\n5. Update your gameflow to remove references to the asset's `.bin` file.\n\nTRX provides asset packs intended for WadTool import:\n\n- `https://lostartefacts.dev/pub/tr1-assets.zip`\n- `https://lostartefacts.dev/pub/tr2-assets.zip`\n\nThe zips also include Tomb Editor catalogs (`Moveables.xml` /\n`SpriteSequences.xml`) so TRX object names show up (and to enable cross-game\nplacements like TR2 guns in TR1 levels). See the README inside the zip for\ndetails.\n\n## Builder note on custom levels\n\nCustom levels should generally not rely on injections for correctness; instead,\nprovide data that is already correct and consistent.\n\nNote however that the injections that relate to Lara can work in custom levels\nthat do not modify Lara's default mesh structure or animations. These injection\nfiles are based on the original Lara model.\n\n## Default injection files (reference)\n\nThe rule of thumb for custom levels is that if an injection file name starts\nwith a level name, it is meant for specific original levels and should\ngenerally be removed from your custom gameflow unless you know what you're doing.\n\n### Core files useful for most builders\n\n| Injection file                        | Usage    | Purpose                                                                                                                                                                                                                                                                                                                                           |\n| ---                                   | ---      | ---                                                                                                                                                                                                                                                                                                                                               |\n| `lara_animations.bin`                 | TR1, TR2 | Lara animations/state/commands (jump-twist, somersault, underwater roll, wading, etc). If Lara's appearance is customised, move the source object to another slot and replace meshes manually. **TR1 only:** add `wet-feet.xml` to the sound catalogue (adds sound IDs 15 & 17) and provide the referenced wet-feet `.wav` samples (or your own). |\n| `lara_guns.bin` / `lara_gym_guns.bin` | TR1, TR2 | In TR1, replaces Lara's fixed shotgun-torso mesh with the TR2+ approach of an independent resting gun mesh. These files also contain Lara's guns from the other games, including flares. The gym file injects all of Lara's weapons and weapon animations in the gym level (for cheats only).                                                     |\n| `lara_extra.bin`                      | TR1, TR2 | Combined object containing extra animations shared between TR1, TR2 and TR3.                                                                                                                                                                                                                                                                      |\n| `lara_outfits.bin`                    | TR1, TR2 | Contains each of Lara's outfits to offer live skin swaps in-game.                                                                                                                                                                                                                                                                                 |\n| `pda_model.bin`                       | TR1, TR2 | The original PDA model with an opening animation. Used by the Gameplay options UI.                                                                                                                                                                                                                                                                |\n| `font.bin`                            | TR1, TR2 | Replacement font sprites to support more characters than OG.                                                                                                                                                                                                                                                                                      |\n| `secret_models_*.bin`                 | TR2      | 3D models for secret pickups in OG and Golden Mask.                                                                                                                                                                                                                                                                                               |\n| `braid.bin`                           | TR1      | Braid option: injects braid plus mesh swaps for Lara's head/backpack (incl. Midas variant).                                                                                                                                                                                                                                                       |\n| `bubbles.bin`                         | TR1      | Replacement sprites for Lara's underwater bubbles (OG sprites are cut off).                                                                                                                                                                                                                                                                       |\n| `pickup_aid.bin`                      | TR1, TR2 | Sprite sequence for pickup aids option; custom levels should define a suitable sprite sequence in slot 185.                                                                                                                                                                                                                                       |\n| `photo.bin`                           | TR1, TR2 | Camera shutter SFX for photo mode (needed only for cutscene levels).                                                                                                                                                                                                                                                                              |\n| `crystal.bin`                         | TR1, TR2 | Replacement savegame crystal model (PS1 style).                                                                                                                                                                                                                                                                                                   |\n| `scion_collision.bin`                 | TR1      | Increases collision radius on the targetable Scion so it can be shot with the shotgun.                                                                                                                                                                                                                                                            |\n| `guardian_death_commands.bin`         | TR2      | Bird guardian death anim command to end the level on the final frame (TRX removes the hard-coded behavior).                                                                                                                                                                                                                                       |\n| `mines_pushblocks.bin`                | TR1      | Restores missing scraping SFX for pushblock types 2/3/4 by injecting anim command data.                                                                                                                                                                                                                                                           |\n| `boat_bits.bin`                       | TR2      | Model for `O_BOAT_BITS` (221) used to show the boat exploding when it crosses mines.                                                                                                                                                                                                                                                              |\n| `explosion.bin`                       | TR1      | Explosion sprites for certain console commands.                                                                                                                                                                                                                                                                                                   |\n| `misc_sprites.bin`                    | TR1, TR2 | Various special-effect sprites such as snowflakes, shadow sprites, and the pink blood sequence used by `O_BLOOD_2`.                                                                                                                                                                                                                              |\n\n### Level-specific files \n\n| Injection file           | Usage    | Purpose                                                                                     |\n| ---                      | ---      | ---                                                                                         |\n| `*_cameras.bin`          | TR1, TR2 | Positional adjustments for cameras that can otherwise cause visual issues.                  |\n| `*_fd.bin`               | TR1, TR2 | Fixes for floor data issues in original levels.                                             |\n| `*_itemrots.bin`         | TR1, TR2 | Pickup item rotations for better visuals with 3D pickups.                                   |\n| `*_meshfixes.bin`        | TR1      | Miscellaneous mesh adjustments for objects (e.g., to avoid z-fighting).                     |\n| `*_music_tracks.bin`     | TR2      | Trigger adjustments to convert music track numbers to match file names (OG levels only).    |\n| `*_pickup_meshes.bin`    | TR1, TR2 | Pickup mesh edits (e.g., rescaling keys / specific pickups).                                |\n| `*_sfx.bin`              | TR1, TR2 | Various SFX fixes/additions.                                                                |\n| `*_skybox.bin`           | TR1      | Predefined skybox injected into specific levels, and specific rooms marked to use it.       |\n| `*_textures.bin`         | TR1, TR2 | Texture fixes in original levels (e.g., gaps, wrong colors).                                |\n| `cistern_plants.bin`     | TR1      | Disables animation on sprite ID 193 in The Cistern and Tomb of Tihocan.                     |\n| `detonator_lights.bin`   | TR2      | Adds animation commands to the Bartoli's Hideout detonator to control the dynamic lighting. |\n| `khamoon_mummy.bin`      | TR1      | Mummy in City of Khamoon room 25 (present on PS1, missing on PC).                           |\n| `seaweed_collision.bin`  | TR2      | Fixes seaweed in Living Quarters blocking Lara from exiting the water.                      |\n| `breakable_tile_sfx.bin` | TR2      | Adds missing breakable tiles (collapsing floor) sounds that are otherwise silent in the OG. |\n| `loose_boards_sfx.bin`   | TR2      | Adds missing breakable tiles (collapsing floor) sounds that are otherwise silent in the OG. |\n| `dagger_sprite.bin`      | TR2      | Adds a UI sprite for the Dagger of Xian when 3D pickups are disabled.                       |\n| `lara_feet_sfx.bin`      | TR1      | Resets Lara's footstep sound effects in the gym level to allow for contextual outfit SFX.   |\n"
  },
  {
    "path": "docs/trx/INSTALLING.md",
    "content": "# Windows (installer)\n\n## Installing (simplified)\n\n1. Download the latest combined TRX installer.\n2. Choose a destination directory for TRX itself.\n3. Select whichever game packs you want to add:\n   - `TR1`, `TR2`, and `TR3` from your original Steam, GOG, or disc installs.\n   - `TR1:UB`, `TR1 PC Demo`, and `TR2:GM` as direct downloads from Lost Artefacts.\n   - `TR3:LA` from your original disc install.\n4. Let the installer remap the original files into the combined `games/<game-id>/` hierarchy.\n\n> [!NOTE]\n> When downloading TRX, you might see a warning from Windows Defender, your browser, or another security tool. Modern antivirus systems use AI-based heuristics - they flag anything uncommon or unsigned as suspicious, even if it's perfectly safe. TRX can trigger these alerts because:\n>\n> - It isn't signed with an expensive commercial certificate.\n> - It’s a small, community-made project, not a big corporate release, meaning tools err on the side of caution.\n> - It comes straight from us, not the Microsoft Store.\n>\n> TRX is open-source, so if you're curious, you can peek at everything on [GitHub](https://github.com/LostArtefacts/TRX/).\n\n# Windows / Linux\n\n## Installing (manual)\n\n1. Download the combined TRX zip file (without the `tr1-3` suffix).\n2. Extract the zip file into a directory of your choice.\n   Make sure you choose to overwrite existing directories and files.\n3. Copy your original game files into the new combined hierarchy.\n\nThe new zip separates shared files from game data:\n\n- Shared TRX files stay in `cfg/`.\n- Game-specific files now live in `games/<game-id>/`.\n\nWhen using the combined zip, please do not copy your original files into top-level `data/`, `fmv/`, `music/`, `audio/`, or `cuts/` directories. Instead, place them in these target directories:\n\n- **Tomb Raider 1**\n  - `data/*.phd` → `games/tr1/levels/*.phd`\n  - `fmv/*` → `games/tr1/fmv/*`\n  - `music/*` → `games/tr1/music/*`\n- **Tomb Raider 1: Unfinished Business**\n  - `data/cat.phd` → `games/tr1-ub/levels/cat.phd`\n  - `data/egypt.phd` → `games/tr1-ub/levels/egypt.phd`\n  - `data/end.phd` → `games/tr1-ub/levels/end.phd`\n  - `data/end2.phd` → `games/tr1-ub/levels/end2.phd`\n- **Tomb Raider 2**\n  - `data/*.tr2` → `games/tr2/levels/*.tr2`\n  - `data/main.sfx` → `games/tr2/main.sfx`\n  - `fmv/*` → `games/tr2/fmv/*`\n  - `music/*.mp3` → `games/tr2/music/*.mp3`\n- **Tomb Raider 2: Golden Mask**\n  - `data/level1.tr2` → `games/tr2-gm/levels/level1.tr2`\n  - `data/level2.tr2` → `games/tr2-gm/levels/level2.tr2`\n  - `data/level3.tr2` → `games/tr2-gm/levels/level3.tr2`\n  - `data/level4.tr2` → `games/tr2-gm/levels/level4.tr2`\n  - `data/level5.tr2` → `games/tr2-gm/levels/level5.tr2`\n  - `data/title.tr2` → `games/tr2-gm/levels/title.tr2`\n  - `data/main.sfx` → `games/tr2-gm/main.sfx`\n- **Tomb Raider 3**\n  - `data/*.tr2` → `games/tr3/levels/*.tr2`\n  - `data/main.sfx` → `games/tr3/main.sfx`\n  - `audio/cdaudio.wad` → `games/tr3/audio/cdaudio.wad`\n  - `cuts/*.tr2` → `games/tr3/cuts/*.tr2`\n  - `fmv/*` → `games/tr3/fmv/*`\n- **Tomb Raider 3: The Lost Artifact**\n  - `data/chunnel.tr2` → `games/tr3-la/levels/chunnel.tr2`\n  - `data/scotland.tr2` → `games/tr3-la/levels/scotland.tr2`\n  - `data/slinc.tr2` → `games/tr3-la/levels/slinc.tr2`\n  - `data/undersea.tr2` → `games/tr3-la/levels/undersea.tr2`\n  - `data/willsden.tr2` → `games/tr3-la/levels/willsden.tr2`\n  - `data/zoo.tr2` → `games/tr3-la/levels/zoo.tr2`\n  - `data/title.tr2` → `games/tr3-la/levels/title.tr2`\n  - `data/main.sfx` → `games/tr3-la/main.sfx`\n\n## Verifying the installation\n\nIf you install everything correctly, your game directory should look more or less like this (click to expand):\n\n<details data-id=\"file-tree-win\">\n<pre><code>.\n├── cfg\n│   ├── presets\n│   │   ├── tr1-pc.json5\n│   │   ├── tr1-ps1.json5\n│   │   ├── tr2-pc.json5\n│   │   ├── tr2-ps1.json5\n│   │   ├── tr3-pc.json5\n│   │   └── tr3-ps1.json5\n│   ├── shaders\n│   │   ├── 2d.glsl\n│   │   ├── billboard.glsl\n│   │   ├── common.glsl\n│   │   ├── fbo.glsl\n│   │   ├── lights.glsl\n│   │   ├── meshes.glsl\n│   │   ├── meshes_tr3.glsl\n│   │   ├── meshes_tr12.glsl\n│   │   └── ui.glsl\n│   ├── base_strings-de.json5\n│   ├── base_strings-en-gb.json5\n│   ├── base_strings-fr.json5\n│   ├── base_strings-gd.json5\n│   ├── base_strings-it.json5\n│   ├── base_strings-pl.json5\n│   ├── base_strings-ru.json5\n│   ├── base_strings.json5\n│   ├── outfits.json5\n│   ├── poses.json5\n│   ├── shell.json5*\n│   ├── TR1X.json5*\n│   ├── TR2X.json5*\n│   ├── TR3X.json5*\n│   └── ui.json5\n├── games\n│   ├── tr1\n│   │   ├── fmv\n│   │   │   ├── cafe.rpl\n│   │   │   ├── canyon.rpl\n│   │   │   ├── core.avi\n│   │   │   ├── end.rpl\n│   │   │   ├── escape.rpl\n│   │   │   ├── lift.rpl\n│   │   │   ├── mansion.rpl\n│   │   │   ├── prison.rpl\n│   │   │   ├── pyramid.rpl\n│   │   │   ├── snow.rpl\n│   │   │   └── vision.rpl\n│   │   ├── images\n│   │   │   ├── atlantis.webp\n│   │   │   ├── credits_1.webp\n│   │   │   ├── credits_2.webp\n│   │   │   ├── credits_3.webp\n│   │   │   ├── credits_3_alt.webp\n│   │   │   ├── credits_ps1.webp\n│   │   │   ├── egypt.webp\n│   │   │   ├── eidos.webp\n│   │   │   ├── end.webp\n│   │   │   ├── greece.webp\n│   │   │   ├── greece_saturn.webp\n│   │   │   ├── gym.webp\n│   │   │   ├── install.webp\n│   │   │   ├── peru.webp\n│   │   │   ├── title.webp\n│   │   │   └── title_og_alt.webp\n│   │   ├── injections\n│   │   │   ├── atlantis_door_sfx.bin\n│   │   │   ├── atlantis_fd.bin\n│   │   │   ├── atlantis_itemrots.bin\n│   │   │   ├── atlantis_textures.bin\n│   │   │   ├── braid.bin\n│   │   │   ├── bubbles.bin\n│   │   │   ├── cat_cameras.bin\n│   │   │   ├── cat_crystals.bin\n│   │   │   ├── cat_fd.bin\n│   │   │   ├── cat_itemrots.bin\n│   │   │   ├── cat_meshfixes.bin\n│   │   │   ├── cat_textures.bin\n│   │   │   ├── caves_fd.bin\n│   │   │   ├── caves_itemrots.bin\n│   │   │   ├── caves_textures.bin\n│   │   │   ├── cistern_fd.bin\n│   │   │   ├── cistern_itemrots.bin\n│   │   │   ├── cistern_plants.bin\n│   │   │   ├── cistern_skybox.bin\n│   │   │   ├── cistern_textures.bin\n│   │   │   ├── colosseum_fd.bin\n│   │   │   ├── colosseum_itemrots.bin\n│   │   │   ├── colosseum_skybox.bin\n│   │   │   ├── colosseum_textures.bin\n│   │   │   ├── crystal.bin\n│   │   │   ├── cut1_setup.bin\n│   │   │   ├── cut2_setup.bin\n│   │   │   ├── cut3_setup.bin\n│   │   │   ├── cut3_textures.bin\n│   │   │   ├── cut4_setup.bin\n│   │   │   ├── cut4_textures.bin\n│   │   │   ├── door58_frames.bin\n│   │   │   ├── door59_frames.bin\n│   │   │   ├── door59_sfx.bin\n│   │   │   ├── door60_frames.bin\n│   │   │   ├── door61_sfx.bin\n│   │   │   ├── egypt_cameras.bin\n│   │   │   ├── egypt_crystals.bin\n│   │   │   ├── egypt_fd.bin\n│   │   │   ├── egypt_itemrots.bin\n│   │   │   ├── egypt_meshfixes.bin\n│   │   │   ├── egypt_textures.bin\n│   │   │   ├── explosion.bin\n│   │   │   ├── folly_fd.bin\n│   │   │   ├── folly_itemrots.bin\n│   │   │   ├── folly_pickup_meshes.bin\n│   │   │   ├── folly_textures.bin\n│   │   │   ├── font.bin\n│   │   │   ├── gun_glow.bin\n│   │   │   ├── gym_textures.bin\n│   │   │   ├── hive_crystals.bin\n│   │   │   ├── hive_fd.bin\n│   │   │   ├── hive_itemrots.bin\n│   │   │   ├── hive_textures.bin\n│   │   │   ├── khamoon_fd.bin\n│   │   │   ├── khamoon_itemrots.bin\n│   │   │   ├── khamoon_meshfixes.bin\n│   │   │   ├── khamoon_mummy.bin\n│   │   │   ├── khamoon_textures.bin\n│   │   │   ├── lara_animations.bin\n│   │   │   ├── lara_extra.bin\n│   │   │   ├── lara_feet_sfx.bin\n│   │   │   ├── lara_flares.bin\n│   │   │   ├── lara_guns.bin\n│   │   │   ├── lara_gym_flares.bin\n│   │   │   ├── lara_gym_guns.bin\n│   │   │   ├── lara_outfits.bin\n│   │   │   ├── midas_itemrots.bin\n│   │   │   ├── midas_textures.bin\n│   │   │   ├── mines_cameras.bin\n│   │   │   ├── mines_door_sfx.bin\n│   │   │   ├── mines_fd.bin\n│   │   │   ├── mines_itemrots.bin\n│   │   │   ├── mines_meshfixes.bin\n│   │   │   ├── mines_pushblocks.bin\n│   │   │   ├── mines_textures.bin\n│   │   │   ├── misc_sprites.bin\n│   │   │   ├── obelisk_fd.bin\n│   │   │   ├── obelisk_itemrots.bin\n│   │   │   ├── obelisk_meshfixes.bin\n│   │   │   ├── obelisk_skybox.bin\n│   │   │   ├── obelisk_textures.bin\n│   │   │   ├── panther_sfx.bin\n│   │   │   ├── pda_model.bin\n│   │   │   ├── photo.bin\n│   │   │   ├── pickup_aid.bin\n│   │   │   ├── pyramid_fd.bin\n│   │   │   ├── pyramid_itemrots.bin\n│   │   │   ├── pyramid_textures.bin\n│   │   │   ├── qualopec_door_sfx.bin\n│   │   │   ├── qualopec_fd.bin\n│   │   │   ├── qualopec_itemrots.bin\n│   │   │   ├── qualopec_textures.bin\n│   │   │   ├── sanctuary_fd.bin\n│   │   │   ├── sanctuary_itemrots.bin\n│   │   │   ├── sanctuary_scion.bin\n│   │   │   ├── sanctuary_textures.bin\n│   │   │   ├── scion_collision.bin\n│   │   │   ├── skate_kid_sfx.bin\n│   │   │   ├── sprite_alignment.bin\n│   │   │   ├── stronghold_crystals.bin\n│   │   │   ├── stronghold_fd.bin\n│   │   │   ├── stronghold_itemrots.bin\n│   │   │   ├── stronghold_textures.bin\n│   │   │   ├── tihocan_fd.bin\n│   │   │   ├── tihocan_itemrots.bin\n│   │   │   ├── tihocan_skybox.bin\n│   │   │   ├── tihocan_textures.bin\n│   │   │   ├── title_textures.bin\n│   │   │   ├── uzi_sfx.bin\n│   │   │   ├── valley_fd.bin\n│   │   │   ├── valley_itemrots.bin\n│   │   │   ├── valley_skybox.bin\n│   │   │   ├── valley_textures.bin\n│   │   │   ├── vilcabamba_door_sfx.bin\n│   │   │   ├── vilcabamba_itemrots.bin\n│   │   │   ├── vilcabamba_textures.bin\n│   │   │   └── winston_model.bin\n│   │   ├── levels\n│   │   │   ├── cut1.phd\n│   │   │   ├── cut2.phd\n│   │   │   ├── cut3.phd\n│   │   │   ├── cut4.phd\n│   │   │   ├── gym.phd\n│   │   │   ├── level1.phd\n│   │   │   ├── level2.phd\n│   │   │   ├── level3a.phd\n│   │   │   ├── level3b.phd\n│   │   │   ├── level4.phd\n│   │   │   ├── level5.phd\n│   │   │   ├── level6.phd\n│   │   │   ├── level7a.phd\n│   │   │   ├── level7b.phd\n│   │   │   ├── level8a.phd\n│   │   │   ├── level8b.phd\n│   │   │   ├── level8c.phd\n│   │   │   ├── level10a.phd\n│   │   │   ├── level10b.phd\n│   │   │   ├── level10c.phd\n│   │   │   └── title.phd\n│   │   ├── music\n│   │   │   ├── track02.flac\n│   │   │   ├── track03.flac\n│   │   │   ├── track04.flac\n│   │   │   ├── track05.flac\n│   │   │   ├── track06.flac\n│   │   │   ├── track07.flac\n│   │   │   ├── track08.flac\n│   │   │   ├── track09.flac\n│   │   │   ├── track10.flac\n│   │   │   ├── track11.flac\n│   │   │   ├── track12.flac\n│   │   │   ├── track13.flac\n│   │   │   ├── track14.flac\n│   │   │   ├── track15.flac\n│   │   │   ├── track16.flac\n│   │   │   ├── track17.flac\n│   │   │   ├── track18.flac\n│   │   │   ├── track19.flac\n│   │   │   ├── track20.flac\n│   │   │   ├── track21.flac\n│   │   │   ├── track22.flac\n│   │   │   ├── track23.flac\n│   │   │   ├── track24.flac\n│   │   │   ├── track25.flac\n│   │   │   ├── track26.flac\n│   │   │   ├── track27.flac\n│   │   │   ├── track28.flac\n│   │   │   ├── track29.flac\n│   │   │   ├── track30.flac\n│   │   │   ├── track31.flac\n│   │   │   ├── track32.flac\n│   │   │   ├── track33.flac\n│   │   │   ├── track34.flac\n│   │   │   ├── track35.flac\n│   │   │   ├── track36.flac\n│   │   │   ├── track37.flac\n│   │   │   ├── track38.flac\n│   │   │   ├── track39.flac\n│   │   │   ├── track40.flac\n│   │   │   ├── track41.flac\n│   │   │   ├── track42.flac\n│   │   │   ├── track43.flac\n│   │   │   ├── track44.flac\n│   │   │   ├── track45.flac\n│   │   │   ├── track46.flac\n│   │   │   ├── track47.flac\n│   │   │   ├── track48.flac\n│   │   │   ├── track49.flac\n│   │   │   ├── track50.flac\n│   │   │   ├── track51.flac\n│   │   │   ├── track52.flac\n│   │   │   ├── track53.flac\n│   │   │   ├── track54.flac\n│   │   │   ├── track55.flac\n│   │   │   ├── track56.flac\n│   │   │   ├── track57.flac\n│   │   │   ├── track58.flac\n│   │   │   ├── track59.flac\n│   │   │   └── track60.flac\n│   │   ├── scripts\n│   │   │   └── gym.lua\n│   │   ├── catalog_item_actions.csv\n│   │   ├── catalog_lara_anims.csv\n│   │   ├── catalog_lara_states.csv\n│   │   ├── catalog_music.csv\n│   │   ├── catalog_objects.csv\n│   │   ├── catalog_samples.csv\n│   │   ├── gameflow.json5\n│   │   ├── inv_ring.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-en-gb.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   ├── strings.json5\n│   │   └── weapons.json5\n│   ├── tr1-demo-pc\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── tr1-level\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── tr1-ub\n│   │   ├── images\n│   │   │   ├── credits_ub.webp\n│   │   │   ├── title_ub.webp\n│   │   │   ├── ub_loading1.webp\n│   │   │   └── ub_loading2.webp\n│   │   ├── levels\n│   │   │   ├── cat.phd\n│   │   │   ├── egypt.phd\n│   │   │   ├── end2.phd\n│   │   │   └── end.phd\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings-ru.json5\n│   │   └── strings.json5\n│   ├── tr2\n│   │   ├── fmv\n│   │   │   ├── ancient.rpl\n│   │   │   ├── crash.rpl\n│   │   │   ├── end.rpl\n│   │   │   ├── jeep.rpl\n│   │   │   ├── landing.rpl\n│   │   │   ├── logo.rpl\n│   │   │   ├── modern.rpl\n│   │   │   └── ms.rpl\n│   │   ├── images\n│   │   │   ├── 3x2\n│   │   │   │   ├── china.webp\n│   │   │   │   ├── credit01.webp\n│   │   │   │   ├── credit02.webp\n│   │   │   │   ├── credit03.webp\n│   │   │   │   ├── credit04.webp\n│   │   │   │   ├── credit05.webp\n│   │   │   │   ├── credit06.webp\n│   │   │   │   ├── credit07.webp\n│   │   │   │   ├── credit08.webp\n│   │   │   │   ├── end.webp\n│   │   │   │   ├── legal_eu.webp\n│   │   │   │   ├── legal_us.webp\n│   │   │   │   ├── mansion.webp\n│   │   │   │   ├── rig.webp\n│   │   │   │   ├── tibet.webp\n│   │   │   │   ├── titan.webp\n│   │   │   │   ├── title_eu.webp\n│   │   │   │   ├── title_us.webp\n│   │   │   │   └── venice.webp\n│   │   │   ├── 4x3\n│   │   │   │   ├── china.webp\n│   │   │   │   ├── credit01.webp\n│   │   │   │   ├── credit02.webp\n│   │   │   │   ├── credit03.webp\n│   │   │   │   ├── credit04.webp\n│   │   │   │   ├── credit05.webp\n│   │   │   │   ├── credit06.webp\n│   │   │   │   ├── credit07.webp\n│   │   │   │   ├── credit08.webp\n│   │   │   │   ├── end.webp\n│   │   │   │   ├── legal_eu.webp\n│   │   │   │   ├── legal_us.webp\n│   │   │   │   ├── mansion.webp\n│   │   │   │   ├── rig.webp\n│   │   │   │   ├── tibet.webp\n│   │   │   │   ├── titan.webp\n│   │   │   │   ├── title_eu.webp\n│   │   │   │   ├── title_us.webp\n│   │   │   │   └── venice.webp\n│   │   │   ├── og\n│   │   │   │   ├── china.webp\n│   │   │   │   ├── credit01.webp\n│   │   │   │   ├── credit02.webp\n│   │   │   │   ├── credit03.webp\n│   │   │   │   ├── credit04.webp\n│   │   │   │   ├── credit05.webp\n│   │   │   │   ├── credit06.webp\n│   │   │   │   ├── credit07.webp\n│   │   │   │   ├── credit08.webp\n│   │   │   │   ├── end.webp\n│   │   │   │   ├── legal.webp\n│   │   │   │   ├── mansion.webp\n│   │   │   │   ├── rig.webp\n│   │   │   │   ├── tibet.webp\n│   │   │   │   ├── titan.webp\n│   │   │   │   ├── title_eu.webp\n│   │   │   │   ├── title_us.webp\n│   │   │   │   └── venice.webp\n│   │   │   ├── china.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── end.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── mansion.webp\n│   │   │   ├── rig.webp\n│   │   │   ├── tibet.webp\n│   │   │   ├── titan.webp\n│   │   │   ├── title_eu.webp\n│   │   │   ├── title_us.webp\n│   │   │   └── venice.webp\n│   │   ├── injections\n│   │   │   ├── barkhang_cameras.bin\n│   │   │   ├── barkhang_crystals.bin\n│   │   │   ├── barkhang_fd.bin\n│   │   │   ├── barkhang_itemrots.bin\n│   │   │   ├── barkhang_music_tracks.bin\n│   │   │   ├── barkhang_pickup_meshes.bin\n│   │   │   ├── barkhang_textures.bin\n│   │   │   ├── bartoli_crystals.bin\n│   │   │   ├── bartoli_music_tracks.bin\n│   │   │   ├── bartoli_secret_fd.bin\n│   │   │   ├── bartoli_textures.bin\n│   │   │   ├── boat_bits.bin\n│   │   │   ├── breakable_tile_sfx.bin\n│   │   │   ├── catacombs_crystals.bin\n│   │   │   ├── catacombs_fd.bin\n│   │   │   ├── catacombs_itemrots.bin\n│   │   │   ├── catacombs_music_tracks.bin\n│   │   │   ├── catacombs_textures.bin\n│   │   │   ├── coldwar_crystals.bin\n│   │   │   ├── coldwar_fd.bin\n│   │   │   ├── coldwar_itemrots.bin\n│   │   │   ├── coldwar_music_tracks.bin\n│   │   │   ├── coldwar_objects.bin\n│   │   │   ├── coldwar_textures.bin\n│   │   │   ├── common_pickup_meshes.bin\n│   │   │   ├── common_pickup_meshes_gm.bin\n│   │   │   ├── crystal.bin\n│   │   │   ├── cut2_setup.bin\n│   │   │   ├── cut2_textures.bin\n│   │   │   ├── cut3_setup.bin\n│   │   │   ├── cut3_textures.bin\n│   │   │   ├── cut4_setup.bin\n│   │   │   ├── cut4_textures.bin\n│   │   │   ├── dagger_sprite.bin\n│   │   │   ├── deck_cameras.bin\n│   │   │   ├── deck_crystals.bin\n│   │   │   ├── deck_fd.bin\n│   │   │   ├── deck_itemrots.bin\n│   │   │   ├── deck_music_tracks.bin\n│   │   │   ├── deck_pickup_meshes.bin\n│   │   │   ├── deck_plants.bin\n│   │   │   ├── deck_secret_fd.bin\n│   │   │   ├── deck_textures.bin\n│   │   │   ├── detonator_lights.bin\n│   │   │   ├── diving_cameras.bin\n│   │   │   ├── diving_crystals.bin\n│   │   │   ├── diving_itemrots.bin\n│   │   │   ├── diving_music_tracks.bin\n│   │   │   ├── diving_pickup_meshes.bin\n│   │   │   ├── diving_sfx.bin\n│   │   │   ├── diving_textures.bin\n│   │   │   ├── door106_sfx.bin\n│   │   │   ├── door107_sfx.bin\n│   │   │   ├── door108_sfx.bin\n│   │   │   ├── door110_sfx.bin\n│   │   │   ├── door111_sfx.bin\n│   │   │   ├── explosion.bin\n│   │   │   ├── fathoms_crystals.bin\n│   │   │   ├── fathoms_goon_sfx.bin\n│   │   │   ├── fathoms_itemrots.bin\n│   │   │   ├── fathoms_music_tracks.bin\n│   │   │   ├── fathoms_plants.bin\n│   │   │   ├── fathoms_secret_fd.bin\n│   │   │   ├── fathoms_textures.bin\n│   │   │   ├── floating_crystals.bin\n│   │   │   ├── floating_fd.bin\n│   │   │   ├── floating_itemrots.bin\n│   │   │   ├── floating_music_tracks.bin\n│   │   │   ├── floating_pickup_meshes.bin\n│   │   │   ├── floating_textures.bin\n│   │   │   ├── font.bin\n│   │   │   ├── fools_crystals.bin\n│   │   │   ├── fools_itemrots.bin\n│   │   │   ├── fools_music_tracks.bin\n│   │   │   ├── fools_pickup_meshes.bin\n│   │   │   ├── fools_textures.bin\n│   │   │   ├── furnace_crystals.bin\n│   │   │   ├── furnace_itemrots.bin\n│   │   │   ├── furnace_music_tracks.bin\n│   │   │   ├── furnace_objects.bin\n│   │   │   ├── furnace_pickup_meshes.bin\n│   │   │   ├── furnace_textures.bin\n│   │   │   ├── guardian_death_commands.bin\n│   │   │   ├── gym_fd.bin\n│   │   │   ├── gym_music_tracks.bin\n│   │   │   ├── gym_sfx.bin\n│   │   │   ├── gym_textures.bin\n│   │   │   ├── house_itemrots.bin\n│   │   │   ├── house_music_tracks.bin\n│   │   │   ├── house_sfx.bin\n│   │   │   ├── house_shower_frames.bin\n│   │   │   ├── house_textures.bin\n│   │   │   ├── inv_background.bin\n│   │   │   ├── kingdom_cameras.bin\n│   │   │   ├── kingdom_crystals.bin\n│   │   │   ├── kingdom_itemrots.bin\n│   │   │   ├── kingdom_music_tracks.bin\n│   │   │   ├── kingdom_textures.bin\n│   │   │   ├── lair_bartolipos.bin\n│   │   │   ├── lair_crystals.bin\n│   │   │   ├── lair_music_tracks.bin\n│   │   │   ├── lair_textures.bin\n│   │   │   ├── lara_animations.bin\n│   │   │   ├── lara_extra.bin\n│   │   │   ├── lara_guns.bin\n│   │   │   ├── lara_gym_guns.bin\n│   │   │   ├── lara_house_guns.bin\n│   │   │   ├── lara_outfits.bin\n│   │   │   ├── lara_rifle_sfx.bin\n│   │   │   ├── lara_vegas_guns.bin\n│   │   │   ├── living_crystals.bin\n│   │   │   ├── living_deck_goon_sfx.bin\n│   │   │   ├── living_fd.bin\n│   │   │   ├── living_itemrots.bin\n│   │   │   ├── living_music_tracks.bin\n│   │   │   ├── living_pickup_meshes.bin\n│   │   │   ├── living_secret_fd.bin\n│   │   │   ├── living_sfx.bin\n│   │   │   ├── living_textures.bin\n│   │   │   ├── loose_boards_sfx.bin\n│   │   │   ├── misc_sprites.bin\n│   │   │   ├── opera_crystals.bin\n│   │   │   ├── opera_fd.bin\n│   │   │   ├── opera_itemrots.bin\n│   │   │   ├── opera_music_tracks.bin\n│   │   │   ├── opera_sfx.bin\n│   │   │   ├── opera_textures.bin\n│   │   │   ├── palace_crystals.bin\n│   │   │   ├── palace_fd.bin\n│   │   │   ├── palace_itemrots.bin\n│   │   │   ├── palace_music_tracks.bin\n│   │   │   ├── palace_secret_fd.bin\n│   │   │   ├── palace_textures.bin\n│   │   │   ├── pda_model.bin\n│   │   │   ├── photo.bin\n│   │   │   ├── pickup_aid.bin\n│   │   │   ├── portcullis_sfx.bin\n│   │   │   ├── rig_crystals.bin\n│   │   │   ├── rig_itemrots.bin\n│   │   │   ├── rig_music_tracks.bin\n│   │   │   ├── rig_pickup_meshes.bin\n│   │   │   ├── rig_textures.bin\n│   │   │   ├── scuba_sfx.bin\n│   │   │   ├── seaweed_collision.bin\n│   │   │   ├── secret_models_gm.bin\n│   │   │   ├── secret_models_og.bin\n│   │   │   ├── shark_sfx.bin\n│   │   │   ├── tibet_crystals.bin\n│   │   │   ├── tibet_fd.bin\n│   │   │   ├── tibet_itemrots.bin\n│   │   │   ├── tibet_music_tracks.bin\n│   │   │   ├── tibet_textures.bin\n│   │   │   ├── title_textures.bin\n│   │   │   ├── vegas_crystals.bin\n│   │   │   ├── vegas_fd.bin\n│   │   │   ├── vegas_itemrots.bin\n│   │   │   ├── vegas_music_tracks.bin\n│   │   │   ├── vegas_textures.bin\n│   │   │   ├── venice_crystals.bin\n│   │   │   ├── venice_fd.bin\n│   │   │   ├── venice_itemrots.bin\n│   │   │   ├── venice_music_tracks.bin\n│   │   │   ├── venice_textures.bin\n│   │   │   ├── wall_cameras.bin\n│   │   │   ├── wall_crystals.bin\n│   │   │   ├── wall_itemrots.bin\n│   │   │   ├── wall_music_tracks.bin\n│   │   │   ├── wall_textures.bin\n│   │   │   ├── winston_model.bin\n│   │   │   ├── wreck_cameras.bin\n│   │   │   ├── wreck_crystals.bin\n│   │   │   ├── wreck_fd.bin\n│   │   │   ├── wreck_goon_sfx.bin\n│   │   │   ├── wreck_itemrots.bin\n│   │   │   ├── wreck_music_tracks.bin\n│   │   │   ├── wreck_pickup_meshes.bin\n│   │   │   ├── wreck_plants.bin\n│   │   │   ├── wreck_secret_fd.bin\n│   │   │   ├── wreck_textures.bin\n│   │   │   ├── xian_crystals.bin\n│   │   │   ├── xian_fd.bin\n│   │   │   ├── xian_itemrots.bin\n│   │   │   ├── xian_music_tracks.bin\n│   │   │   ├── xian_pickup_meshes.bin\n│   │   │   ├── xian_sfx.bin\n│   │   │   └── xian_textures.bin\n│   │   ├── levels\n│   │   │   ├── assault.tr2\n│   │   │   ├── boat.tr2\n│   │   │   ├── catacomb.tr2\n│   │   │   ├── cut1.tr2\n│   │   │   ├── cut2.tr2\n│   │   │   ├── cut3.tr2\n│   │   │   ├── cut4.tr2\n│   │   │   ├── deck.tr2\n│   │   │   ├── emprtomb.tr2\n│   │   │   ├── floating.tr2\n│   │   │   ├── house.tr2\n│   │   │   ├── icecave.tr2\n│   │   │   ├── keel.tr2\n│   │   │   ├── living.tr2\n│   │   │   ├── monastry.tr2\n│   │   │   ├── opera.tr2\n│   │   │   ├── platform.tr2\n│   │   │   ├── rig.tr2\n│   │   │   ├── skidoo.tr2\n│   │   │   ├── title.tr2\n│   │   │   ├── unwater.tr2\n│   │   │   ├── venice.tr2\n│   │   │   ├── wall.tr2\n│   │   │   └── xian.tr2\n│   │   ├── music\n│   │   │   ├── 2.mp3\n│   │   │   ├── 3.mp3\n│   │   │   ├── 4.mp3\n│   │   │   ├── 5.mp3\n│   │   │   ├── 6.mp3\n│   │   │   ├── 7.mp3\n│   │   │   ├── 8.mp3\n│   │   │   ├── 9.mp3\n│   │   │   ├── 10.mp3\n│   │   │   ├── 11.mp3\n│   │   │   ├── 12.mp3\n│   │   │   ├── 13.mp3\n│   │   │   ├── 14.mp3\n│   │   │   ├── 15.mp3\n│   │   │   ├── 16.mp3\n│   │   │   ├── 17.mp3\n│   │   │   ├── 18.mp3\n│   │   │   ├── 19.mp3\n│   │   │   ├── 20.mp3\n│   │   │   ├── 21.mp3\n│   │   │   ├── 22.mp3\n│   │   │   ├── 23.mp3\n│   │   │   ├── 24.mp3\n│   │   │   ├── 25.mp3\n│   │   │   ├── 26.mp3\n│   │   │   ├── 27.mp3\n│   │   │   ├── 28.mp3\n│   │   │   ├── 29.mp3\n│   │   │   ├── 30.mp3\n│   │   │   ├── 31.mp3\n│   │   │   ├── 32.mp3\n│   │   │   ├── 33.mp3\n│   │   │   ├── 34.mp3\n│   │   │   ├── 35.mp3\n│   │   │   ├── 36.mp3\n│   │   │   ├── 37.mp3\n│   │   │   ├── 38.mp3\n│   │   │   ├── 39.mp3\n│   │   │   ├── 40.mp3\n│   │   │   ├── 41.mp3\n│   │   │   ├── 42.mp3\n│   │   │   ├── 43.mp3\n│   │   │   ├── 44.mp3\n│   │   │   ├── 45.mp3\n│   │   │   ├── 46.mp3\n│   │   │   ├── 47.mp3\n│   │   │   ├── 48.mp3\n│   │   │   ├── 49.mp3\n│   │   │   ├── 50.mp3\n│   │   │   ├── 51.mp3\n│   │   │   ├── 52.mp3\n│   │   │   ├── 53.mp3\n│   │   │   ├── 54.mp3\n│   │   │   ├── 55.mp3\n│   │   │   ├── 56.mp3\n│   │   │   ├── 57.mp3\n│   │   │   ├── 58.mp3\n│   │   │   ├── 59.mp3\n│   │   │   ├── 60.mp3\n│   │   │   └── 61.mp3\n│   │   ├── scripts\n│   │   │   ├── assault.lua\n│   │   │   ├── cut3.lua\n│   │   │   ├── floating.lua\n│   │   │   ├── house.lua\n│   │   │   ├── level1.lua\n│   │   │   ├── level3.lua\n│   │   │   ├── level4.lua\n│   │   │   └── monastry.lua\n│   │   ├── catalog_item_actions.csv\n│   │   ├── catalog_lara_anims.csv\n│   │   ├── catalog_lara_states.csv\n│   │   ├── catalog_music.csv\n│   │   ├── catalog_objects.csv\n│   │   ├── catalog_samples.csv\n│   │   ├── gameflow.json5\n│   │   ├── inv_ring.json5\n│   │   ├── main.sfx\n│   │   ├── strings-de.json5\n│   │   ├── strings-en-gb.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings.json5\n│   │   └── weapons.json5\n│   ├── tr2-gm\n│   │   ├── images\n│   │   │   ├── 3x2\n│   │   │   │   ├── credit00_gm.webp\n│   │   │   │   ├── credit07_gm.webp\n│   │   │   │   ├── gm_level1.webp\n│   │   │   │   ├── gm_level2.webp\n│   │   │   │   ├── gm_level3.webp\n│   │   │   │   ├── gm_level4.webp\n│   │   │   │   ├── gm_level5.webp\n│   │   │   │   ├── legal_eu_gm.webp\n│   │   │   │   ├── legal_us_gm.webp\n│   │   │   │   ├── title_eu_gm.webp\n│   │   │   │   └── title_us_gm.webp\n│   │   │   ├── 4x3\n│   │   │   │   ├── credit00_gm.webp\n│   │   │   │   ├── credit07_gm.webp\n│   │   │   │   ├── gm_level1.webp\n│   │   │   │   ├── gm_level2.webp\n│   │   │   │   ├── gm_level3.webp\n│   │   │   │   ├── gm_level4.webp\n│   │   │   │   ├── gm_level5.webp\n│   │   │   │   ├── legal_eu_gm.webp\n│   │   │   │   ├── legal_us_gm.webp\n│   │   │   │   ├── title_eu_gm.webp\n│   │   │   │   └── title_us_gm.webp\n│   │   │   ├── og\n│   │   │   │   ├── credit00_gm.webp\n│   │   │   │   ├── credit07_gm.webp\n│   │   │   │   ├── title_eu_gm.webp\n│   │   │   │   └── title_us_gm.webp\n│   │   │   ├── credit00_gm.webp\n│   │   │   ├── credit07_gm.webp\n│   │   │   ├── gm_level1.webp\n│   │   │   ├── gm_level2.webp\n│   │   │   ├── gm_level3.webp\n│   │   │   ├── gm_level4.webp\n│   │   │   ├── gm_level5.webp\n│   │   │   ├── legal_eu_gm.webp\n│   │   │   ├── legal_us_gm.webp\n│   │   │   ├── title_eu_gm.webp\n│   │   │   └── title_us_gm.webp\n│   │   ├── levels\n│   │   │   ├── level1.tr2\n│   │   │   ├── level2.tr2\n│   │   │   ├── level3.tr2\n│   │   │   ├── level4.tr2\n│   │   │   ├── level5.tr2\n│   │   │   └── title.tr2\n│   │   ├── gameflow.json5\n│   │   ├── main.sfx\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── tr2-level\n│   │   ├── gameflow.json5\n│   │   ├── strings-de.json5\n│   │   ├── strings-fr.json5\n│   │   ├── strings-gd.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   ├── tr3\n│   │   ├── audio\n│   │   │   └── cdaudio.wad\n│   │   ├── cuts\n│   │   │   ├── cut1.tr2\n│   │   │   ├── cut2.tr2\n│   │   │   ├── cut3.tr2\n│   │   │   ├── cut4.tr2\n│   │   │   ├── cut5.tr2\n│   │   │   ├── cut6.tr2\n│   │   │   ├── cut7.tr2\n│   │   │   ├── cut8.tr2\n│   │   │   ├── cut9.tr2\n│   │   │   ├── cut11.tr2\n│   │   │   └── cut12.tr2\n│   │   ├── fmv\n│   │   │   ├── crsh_eng.rpl\n│   │   │   ├── endgame.rpl\n│   │   │   ├── intr_eng.rpl\n│   │   │   ├── logo.rpl\n│   │   │   └── sail_eng.rpl\n│   │   ├── images\n│   │   │   ├── 3x2\n│   │   │   │   ├── antarc.webp\n│   │   │   │   ├── credit01.webp\n│   │   │   │   ├── credit02.webp\n│   │   │   │   ├── credit03.webp\n│   │   │   │   ├── credit04.webp\n│   │   │   │   ├── credit05.webp\n│   │   │   │   ├── credit06.webp\n│   │   │   │   ├── credit07.webp\n│   │   │   │   ├── credit08.webp\n│   │   │   │   ├── credit09.webp\n│   │   │   │   ├── house.webp\n│   │   │   │   ├── india.webp\n│   │   │   │   ├── legal_eu.webp\n│   │   │   │   ├── legal_us.webp\n│   │   │   │   ├── london.webp\n│   │   │   │   ├── nevada.webp\n│   │   │   │   ├── southpac.webp\n│   │   │   │   ├── theend2.webp\n│   │   │   │   ├── title_eu.webp\n│   │   │   │   └── title_us.webp\n│   │   │   ├── 4x3\n│   │   │   │   ├── antarc.webp\n│   │   │   │   ├── credit01.webp\n│   │   │   │   ├── credit02.webp\n│   │   │   │   ├── credit03.webp\n│   │   │   │   ├── credit04.webp\n│   │   │   │   ├── credit05.webp\n│   │   │   │   ├── credit06.webp\n│   │   │   │   ├── credit07.webp\n│   │   │   │   ├── credit08.webp\n│   │   │   │   ├── credit09.webp\n│   │   │   │   ├── house.webp\n│   │   │   │   ├── india.webp\n│   │   │   │   ├── legal_eu.webp\n│   │   │   │   ├── legal_us.webp\n│   │   │   │   ├── london.webp\n│   │   │   │   ├── nevada.webp\n│   │   │   │   ├── southpac.webp\n│   │   │   │   ├── theend2.webp\n│   │   │   │   ├── title_eu.webp\n│   │   │   │   └── title_us.webp\n│   │   │   ├── og\n│   │   │   │   ├── antarc.webp\n│   │   │   │   ├── credit01.webp\n│   │   │   │   ├── credit02.webp\n│   │   │   │   ├── credit03.webp\n│   │   │   │   ├── credit04.webp\n│   │   │   │   ├── credit05.webp\n│   │   │   │   ├── credit06.webp\n│   │   │   │   ├── credit07.webp\n│   │   │   │   ├── credit08.webp\n│   │   │   │   ├── credit09.webp\n│   │   │   │   ├── house.webp\n│   │   │   │   ├── india.webp\n│   │   │   │   ├── legal_eu.webp\n│   │   │   │   ├── legal_us.webp\n│   │   │   │   ├── london.webp\n│   │   │   │   ├── nevada.webp\n│   │   │   │   ├── nevadafff.webp\n│   │   │   │   ├── southpac.webp\n│   │   │   │   ├── theend2.webp\n│   │   │   │   ├── theend.webp\n│   │   │   │   ├── title_eu.webp\n│   │   │   │   └── title_us.webp\n│   │   │   ├── antarc.webp\n│   │   │   ├── credit01.webp\n│   │   │   ├── credit02.webp\n│   │   │   ├── credit03.webp\n│   │   │   ├── credit04.webp\n│   │   │   ├── credit05.webp\n│   │   │   ├── credit06.webp\n│   │   │   ├── credit07.webp\n│   │   │   ├── credit08.webp\n│   │   │   ├── credit09.webp\n│   │   │   ├── house.webp\n│   │   │   ├── india.webp\n│   │   │   ├── legal_eu.webp\n│   │   │   ├── legal_us.webp\n│   │   │   ├── london.webp\n│   │   │   ├── nevada.webp\n│   │   │   ├── southpac.webp\n│   │   │   ├── theend2.webp\n│   │   │   ├── title_eu.webp\n│   │   │   └── title_us.webp\n│   │   ├── injections\n│   │   │   ├── aldwych_fd.bin\n│   │   │   ├── aldwych_pickup_meshes.bin\n│   │   │   ├── aldwych_textures.bin\n│   │   │   ├── antarc_airlock.bin\n│   │   │   ├── antarc_door134_frames.bin\n│   │   │   ├── antarc_sky.bin\n│   │   │   ├── area51_sky.bin\n│   │   │   ├── area51_textures.bin\n│   │   │   ├── cavern_door131_frames.bin\n│   │   │   ├── cavern_pickup_meshes.bin\n│   │   │   ├── cavern_sky.bin\n│   │   │   ├── city_textures.bin\n│   │   │   ├── cliff_door132_frames.bin\n│   │   │   ├── coastal_airlock.bin\n│   │   │   ├── coastal_animating_bounds.bin\n│   │   │   ├── coastal_sky.bin\n│   │   │   ├── compound_animating_bounds.bin\n│   │   │   ├── compound_cine.bin\n│   │   │   ├── compound_textures.bin\n│   │   │   ├── crash_pickup_meshes.bin\n│   │   │   ├── crash_sky.bin\n│   │   │   ├── cut1_setup.bin\n│   │   │   ├── cut2_setup.bin\n│   │   │   ├── cut3_setup.bin\n│   │   │   ├── cut3_shell.bin\n│   │   │   ├── cut4_setup.bin\n│   │   │   ├── cut5_setup.bin\n│   │   │   ├── cut5_textures.bin\n│   │   │   ├── cut6_setup.bin\n│   │   │   ├── cut7_setup.bin\n│   │   │   ├── cut8_setup.bin\n│   │   │   ├── cut9_setup.bin\n│   │   │   ├── cut11_setup.bin\n│   │   │   ├── cut12_setup.bin\n│   │   │   ├── drill_collision.bin\n│   │   │   ├── flamethrower_sfx.bin\n│   │   │   ├── font.bin\n│   │   │   ├── ganges_door131_frames.bin\n│   │   │   ├── globe_model.bin\n│   │   │   ├── gym_sky.bin\n│   │   │   ├── india_sky.bin\n│   │   │   ├── lara_animations.bin\n│   │   │   ├── lara_extra.bin\n│   │   │   ├── lara_guns.bin\n│   │   │   ├── lara_gym_guns.bin\n│   │   │   ├── lara_outfits.bin\n│   │   │   ├── london_sky.bin\n│   │   │   ├── luds_diver_animation.bin\n│   │   │   ├── luds_textures.bin\n│   │   │   ├── menu_artefacts.bin\n│   │   │   ├── mines_textures.bin\n│   │   │   ├── misc_sprites.bin\n│   │   │   ├── nevada_door132_frames.bin\n│   │   │   ├── nevada_sky.bin\n│   │   │   ├── pda_model.bin\n│   │   │   ├── pickup_aid.bin\n│   │   │   ├── puna_pickup_meshes.bin\n│   │   │   ├── rapids_sky.bin\n│   │   │   ├── reunion_flames.bin\n│   │   │   ├── scotland_sky.bin\n│   │   │   ├── stpaul_animating_bounds.bin\n│   │   │   ├── stpaul_textures.bin\n│   │   │   ├── tinnos_cameras.bin\n│   │   │   ├── tinnos_flames.bin\n│   │   │   ├── undersea_animating_bounds.bin\n│   │   │   ├── undersea_train.bin\n│   │   │   ├── willsden_heli.bin\n│   │   │   └── zoo_train.bin\n│   │   ├── levels\n│   │   │   ├── antarc.tr2\n│   │   │   ├── area51.tr2\n│   │   │   ├── chamber.tr2\n│   │   │   ├── city.tr2\n│   │   │   ├── compound.tr2\n│   │   │   ├── crash.tr2\n│   │   │   ├── house.tr2\n│   │   │   ├── jungle.tr2\n│   │   │   ├── mines.tr2\n│   │   │   ├── nevada.tr2\n│   │   │   ├── office.tr2\n│   │   │   ├── quadchas.tr2\n│   │   │   ├── rapids.tr2\n│   │   │   ├── roofs.tr2\n│   │   │   ├── sewer.tr2\n│   │   │   ├── shore.tr2\n│   │   │   ├── stpaul.tr2\n│   │   │   ├── temple.tr2\n│   │   │   ├── title.tr2\n│   │   │   ├── tonyboss.tr2\n│   │   │   ├── tower.tr2\n│   │   │   └── triboss.tr2\n│   │   ├── scripts\n│   │   │   ├── area51.lua\n│   │   │   ├── compound.lua\n│   │   │   ├── crash.lua\n│   │   │   ├── cut8.lua\n│   │   │   ├── jungle.lua\n│   │   │   ├── mines.lua\n│   │   │   ├── tower.lua\n│   │   │   └── zoo.lua\n│   │   ├── catalog_item_actions.csv\n│   │   ├── catalog_lara_anims.csv\n│   │   ├── catalog_lara_states.csv\n│   │   ├── catalog_music.csv\n│   │   ├── catalog_objects.csv\n│   │   ├── catalog_samples.csv\n│   │   ├── gameflow.json5\n│   │   ├── inv_ring.json5\n│   │   ├── main.sfx\n│   │   ├── strings-de.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   ├── strings.json5\n│   │   ├── tombpc.dat\n│   │   └── weapons.json5\n│   ├── tr3-la\n│   │   ├── levels\n│   │   │   ├── chunnel.tr2\n│   │   │   ├── scotland.tr2\n│   │   │   ├── slinc.tr2\n│   │   │   ├── title.tr2\n│   │   │   ├── undersea.tr2\n│   │   │   ├── willsden.tr2\n│   │   │   └── zoo.tr2\n│   │   ├── gameflow.json5\n│   │   ├── main.sfx\n│   │   ├── strings-de.json5\n│   │   ├── strings-it.json5\n│   │   ├── strings-pl.json5\n│   │   └── strings.json5\n│   └── tr3-level\n│       ├── gameflow.json5\n│       ├── strings-it.json5\n│       ├── strings-pl.json5\n│       └── strings.json5\n└── TRX.exe</code></pre>\n</details>\n\n*\\* Will not be present until the game has been launched.*\n\n## Playing the game\n\n- To play the last selected game or expansion, run `TRX.exe`.\n- To launch a specific base game from a shortcut, use one of the following commands:\n    - `TRX.exe --mod tr1`\n    - `TRX.exe --mod tr2`\n    - `TRX.exe --mod tr3`\n- To launch a specific expansion pack from a shortcut, use one of the following commands:\n    - `TRX.exe --mod tr1-ub`\n    - `TRX.exe --mod tr2-gm`\n    - `TRX.exe --mod tr3-la`\n\n## Migrating from previous TRX installations\n\nIf TRX shows an error like `Mixed mod layout detected: found legacy mod data`,\nyour install contains a mix of the old layout and the new one. Older combined\nbuilds stored mod definitions under `cfg/<game-id>/`, with the matching game\nfiles in top-level folders such as `data/`, `fmv/`, `music/`, `audio/`, and\n`cuts/`. Current combined builds keep each game together under\n`games/<game-id>/`. That means each game now has its own directory, such as:\n\n- `games/tr1/`\n- `games/tr2/`\n- `games/tr3/`\n\nInside each of those folders you will find the game's own files, for example\n`levels/`, `fmv/`, `music/`, `audio/`, `cuts/`, and the mod's `gameflow.json5`. To fix the error:\n\n1. Keep the shared `cfg/` directory from the current combined TRX build.\n2. Remove any legacy per-game folders from `cfg/`, such as:\n   - `cfg/tr1/`,\n   - `cfg/tr1-demo-pc`,\n   - `cfg/tr1-level/`,\n   - `cfg/tr1-ub/`,\n   - `cfg/tr2/`,\n   - `cfg/tr2-gm/`,\n   - `cfg/tr2-level/`,\n   - `cfg/tr3/`,\n   - `cfg/tr3-la/`,\n   - `cfg/tr3-level/`.\n3. Move or re-copy your original game files into the matching\n   `games/<game-id>/` folders listed above.\n4. Do not keep the same game in both places at once. For example, if you use\n   `games/tr1/`, do not also keep `cfg/tr1/` and `data/`.\n\nIf you are updating from an older combined install, the simplest fix is usually\nto start from a fresh extract of the latest TRX zip and then copy your original\ngame files into `games/<game-id>/` again.\n\n\n\n# macOS\n\n## Installing\n\nmacOS packages still use the per-game hierarchy. The combined zip layout documented above does not apply to macOS yet.\n\nUse the existing guides instead:\n\n- [TR1 installing guide](../tr1/INSTALLING.md)\n- [TR2 installing guide](../tr2/INSTALLING.md)\n- [TR3 installing guide](../tr3/INSTALLING.md)\n"
  },
  {
    "path": "docs/trx/LEVELS.md",
    "content": "---\ntitle: Level reference\norder: 9\n---\n\n# Level reference\n\n## Tomb Raider I\n\n| #  | Title                  | File           | Pickups               | Kills                 | Secrets         | Save Crystals |\n| -- | ---------------------- | -------------- | --------------------- | --------------------- | --------------- | ------------- |\n| 0  | Lara's Home            | `gym.phd`      | -                     | -                     | -               | -             |\n| 1  | Caves                  | `level1.phd`   | 7                     | 14                    | 3               | 1             |\n| 2  | City of Vilcabamba     | `level2.phd`   | 13                    | 29                    | 3               | 2             |\n| 3  | Lost Valley            | `level3a.phd`  | 16                    | 13                    | 5               | 3             |\n| 4  | Tomb of Qualopec       | `level3b.phd`  | 8                     | 7 / 8<sup>[1]</sup>   | 3               | 3             |\n| 5  | St. Francis' Folly     | `level4.phd`   | 19                    | 23                    | 4               | 5             |\n| 6  | Colosseum              | `level5.phd`   | 14                    | 26 / 27<sup>[2]</sup> | 3               | 4             |\n| 7  | Palace Midas           | `level6.phd`   | 22 / 23<sup>[3]</sup> | 43                    | 3               | 8             |\n| 8  | The Cistern            | `level7a.phd`  | 28                    | 34                    | 3               | 5             |\n| 9  | Tomb of Tihocan        | `level7b.phd`  | 26                    | 17                    | 2               | 5             |\n| 10 | City of Khamoon        | `level8a.phd`  | 24                    | 14 / 15<sup>[4]</sup> | 3               | 4             |\n| 11 | Obelisk of Khamoon     | `level8b.phd`  | 38                    | 16                    | 3               | 7             |\n| 12 | Sanctuary of the Scion | `level8c.phd`  | 29                    | 15                    | 1               | 7             |\n| 13 | Natla's Mines          | `level10a.phd` | 30                    | 3                     | 3               | 7             |\n| 14 | Atlantis               | `level10b.phd` | 50                    | 32                    | 3               | 7             |\n| 15 | The Great Pyramid      | `level10c.phd` | 31                    | 6                     | 3<sup>[5]</sup> | 4             |\n| -  | Total                  | -              | 355                   | 294 / 295             | 45              | 72            |\n\n- <sup>[1]</sup>: extra mummy kill in the Tomb of Qualopec.\n- <sup>[2]</sup>: bat with a missing trigger in the OG.\n- <sup>[3]</sup>: unobtainable medipack in the Midas hub.\n- <sup>[4]</sup>: additional mummy in the PS1 version.\n- <sup>[5]</sup>: broken secret trigger in some versions of the game.\n\n\n## Tomb Raider I: Unfinished Business\n\n| #  | Title                | File        | Pickups | Kills | Secrets |\n| -- | -------------------- | ----------- | ------- | ----- | ------- |\n| 1  | Return to Egypt      | `egypt.phd` | 53      | 41    | 3       |\n| 2  | Temple of the Cat    | `cat.phd`   | 63      | 44    | 4       |\n| 3  | Atlantean Stronghold | `end.phd`   | 63      | 31    | 2       |\n| 4  | The Hive             | `end2.phd`  | 60      | 41    | 1       |\n| -  | Total                | -           | 239     | 157   | 10      |\n\n\n## Tomb Raider II\n\n| #  | Title                    | File           | Pickups               | Kills                 | Secrets |\n| -- | ------------------------ | -------------- | --------------------- | --------------------- | ------- |\n| 0  | Lara's Home              | `assault.tr2`  | 1                     | -                     | -       |\n| 1  | The Great Wall           | `wall.tr2`     | 14                    | 23                    | 3       |\n| 2  | Venice                   | `boat.tr2`     | 30                    | 24                    | 3       |\n| 3  | Bartoli's Hideout        | `venice.tr2`   | 28                    | 37                    | 3       |\n| 4  | Opera House              | `opera.tr2`    | 37                    | 46                    | 3       |\n| 5  | Offshore Rig             | `rig.tr2`      | 31                    | 20                    | 3       |\n| 6  | Diving Area              | `platform.tr2` | 39                    | 34                    | 3       |\n| 7  | 40 Fathoms               | `unwater.tr2`  | 14                    | 16                    | 3       |\n| 8  | Wreck of the Maria Doria | `keel.tr2`     | 41                    | 35 / 36<sup>[1]</sup> | 3       |\n| 9  | Living Quarters          | `living.tr2`   | 16                    | 21                    | 3       |\n| 10 | The Deck                 | `deck.tr2`     | 35                    | 30                    | 3       |\n| 11 | Tibetan Foothills        | `skidoo.tr2`   | 31                    | 33                    | 3       |\n| 12 | Barkhang Monastery       | `monastry.tr2` | 44                    | 30 / 52<sup>[2]</sup> | 3       |\n| 13 | Catacombs of the Talion  | `catacomb.tr2` | 39                    | 33                    | 3       |\n| 14 | Ice Palace               | `icecave.tr2`  | 33                    | 22                    | 3       |\n| 15 | Temple of Xian           | `emprtomb.tr2` | 39 / 40<sup>[3]</sup> | 37                    | 3       |\n| 16 | Floating Islands         | `floating.tr2` | 39                    | 26                    | 3       |\n| 17 | The Dragon's Lair        | `xian.tr2`     | 25                    | 11                    | -       |\n| 18 | Home Sweet Home          | `house.tr2`    | 45                    | 16                    | -       |\n| -  | Total                    | -              | 580                   | 494 / 516             | 48      |\n\n- <sup>[1]</sup>: shark in an unreachable room (possible to kill with extreme patience)\n- <sup>[2]</sup>: hostiles vs. hostiles and allies\n- <sup>[3]</sup>: unobtainable medipack above the waterfall lake\n\n\n## Tomb Raider II: The Golden Mask\n\n| #  | Title               | File         | Pickups               | Kills                 | Secrets   |\n| -- | ------------------- | ------------ | --------------------- | --------------------- | --------- |\n| 1  | The Cold War        | `level1.tr2` | 71                    | 39 / 44<sup>[1]</sup> | 3         |\n| 2  | Fool's Gold         | `level2.tr2` | 69                    | 62                    | 3         |\n| 3  | Furnace of the Gods | `level3.tr2` | 64                    | 38 / 41<sup>[1]</sup> | 3         |\n| 4  | Kingdom             | `level4.tr2` | 50 / 52<sup>[2]</sup> | 25 / 29<sup>[1]</sup> | 3         |\n| 5  | Nightmare in Vegas  | `level5.tr2` | 75                    | 23                    | 3         |\n| -  | Total               | -            | 329 / 331             | 187 / 199             | 15        |\n\n- <sup>[1]</sup>: hostiles vs. hostiles and allies\n- <sup>[2]</sup>: additional drops from allies\n"
  },
  {
    "path": "docs/trx/MIGRATING.md",
    "content": "---\ntitle: Migrating levels\norder: 3\n---\n\n# Migration guide for level builders\n\n## TRX\n\n### Version 1.5 to 1.6\n\n1. **TR1 and TR2 blood catalog names were renamed**:\n    In `cfg/catalog_objects.csv`, update old symbols to the new names:\n    - `O_BLOOD_1` → `O_BLOOD`\n\n    This also affects catalog-derived Lua names (`trx.catalog.objects`):\n    - `blood_1` → `blood`\n\n2. **Update weapon ammo quantities**:\n   In `weapons.json5`, the old `pickup_qty` and `pickup_qty_alt` fields have\n   been reorganized under a new nested `ammo` object. This lets weapon pickups\n   grant a different amount of ammo than their matching ammo pickups.\n\n   To match the previous setup:\n\n   1. Open `weapons.json5`.\n   2. For each weapon entry:\n      - Create a nested `ammo` object if it doesn't already exist.\n      - Move the value from `pickup_qty` into both `ammo.initial_qty` and\n        `ammo.pickup_qty` fields.\n   3. If the weapon had a `pickup_qty_alt` field (e.g. flares):\n      - Move that value into `ammo.pickup_qty_alt`.\n   4. Remove the old `pickup_qty` and `pickup_qty_alt` fields.\n\n3. **TR1 Atlantean catalog names were changed**:\n   In `cfg/catalog_objects.csv`, update old symbols to the new names:\n    - `O_WARRIOR_1` → `O_ATLANTEAN_WINGED`\n    - `O_WARRIOR_2` → `O_ATLANTEAN_SHOOTER`\n    - `O_WARRIOR_3` → `O_ATLANTEAN_GROUND`\n\n    This also affects catalog-derived Lua names (`trx.catalog.objects`):\n    - `warrior_1` → `atlantean_winged`\n    - `warrior_2` → `atlantean_shooter`\n    - `warrior_3` → `atlantean_ground`\n\n### Version 1.4 to 1.5\n\n1. **Update TR2 detonator box**\n   Dynamic light output when using the detonator is no longer hard-coded and now\n   uses animation commands. The updated OG asset is available to download \n   [here](INJECTIONS.md#builder-workflow-keep-the-codebincode-or-bake-into-your-wad).\n\n### Version 1.3 to 1.4\n\n1. **Update strings file structure**\n   The flat string section has been replaced with nested root sections.\n   Please see shipped string files or documentation for details.\n\n### Version 1.2 to 1.3\n\n1. **TR1 missile catalog names were renamed**:\n    In `cfg/catalog_objects.csv`, update old missile symbols to the new names:\n    - `O_MISSILE_1` → `O_NATLA_GUN`\n    - `O_MISSILE_2` → `O_MISSILE_ATLANTEAN_SHARD`\n    - `O_MISSILE_3` → `O_MISSILE_ATLANTEAN_BOMB`\n    - `O_MISSILE_4` and `O_MISSILE_5` are no longer used and should be removed.\n\n    This also affects catalog-derived Lua names (`trx.catalog.objects`):\n    - `missile_1` → `natla_gun`\n    - `missile_2` → `missile_atlantean_shard`\n    - `missile_3` → `missile_atlantean_bomb`\n\n2. **TR2 breakable window catalog names were renamed**:\n    In `cfg/catalog_objects.csv`, update old breakable windows to the new names:\n    - `O_WINDOW_1` → `O_SMASH_OBJECT_1`\n    - `O_WINDOW_2` → `O_SMASH_OBJECT_2`\n\n    This also affects catalog-derived Lua names (`trx.catalog.objects`):\n    - `window_1` → `smash_object_1`\n    - `window_2` → `smash_object_2`\n\n3. **Flooding flip effect sound ID was changed**:\n    In `cfg/catalog_samples.csv`, add an alias for `SFX_FLOOD`:\n    - TR1: `81, SFX_FLOOD`\n    - TR2: `79, SFX_FLOOD`\n\n### Version 1.1 to 1.2\n\n1. **Lara skin system**:  \n    Lara's outfit must now be defined using additional skin objects, along with\n    game-flow and JSON setup. Refer to [outfits documentation](OUTFITS.md).\n\n2. **Lua event name cleanup**:  \n    The following events got new names:\n    - `on_level_init` → `before_level_file`\n    - `on_level_start` → `after_level_file`\n    - `on_level_load` → `after_level_state`\n    - `on_control` → `before_control`\n    - `on_control_post` → `after_control`\n\n3. **Lua objects catalog name cleanup**:  \n    All keys in `trx.catalog.objects` had their `O_` prefix removed and were\n    converted to lowercase.  \n    Before: `trx.catalog.objects.O_BANDIT_1`  \n    After: `trx.catalog.objects.bandit_1`\n\n4. **Savegame file pattern rename**:  \n    Replace `savegame_fmt_bson` with `savegame_file_fmt` in game flow files.\n    The old `savegame_fmt_bson` key is still accepted but logs a warning and is\n    scheduled for removal in TRX 1.5.\n\n5. **Legacy savegame pattern removed**:  \n    Remove the `savegame_fmt_legacy` key from game flow files.\n\n### Version 1.0 to 1.1\n\n1. **Ally and ally target behavior moved to Lua**:  \n    Monks being allies and bandits being enemies who will target allies is no\n    longer hardcoded and instead must be defined in Lua. Refer to the game flow\n    and linked script files of the original levels for reference.\n\n## TR1X\n\n### Version 4.15 to TRX 1.0\n\n1. **Game flow options moved to the config module**:\n    Certain settings are no longer part of the game flow spec and instead\n    became hidden player settings. To change them, put them in the\n    `enforced_config` section. List of the affected settings:\n    - `demo_delay`\n    - `enable_killer_pushblocks`\n\n2. **Lara shotgun animation**: \n   Lara now uses the TR2+ approach of a separate shotgun mesh on her back. You\n   must use the `lara_guns.bin` injection or otherwise refer to \n   https://github.com/LostArtefacts/TRXInjectionTool/blob/main/docs/ASSETS.md\n\n3. **Lara extra animations**: \n   Lara now uses the TR2+ approach of having defined state changes for extra\n   animations (scion pickups, Midas touch etc). You must use the `lara_extra.bin`\n   injection or otherwise refer to \n   https://github.com/LostArtefacts/TRXInjectionTool/blob/main/docs/ASSETS.md\n\n### Version 4.13 to 4.14\n\n1. **Update file paths**  \n   - Move and rename the `cfg/TR1X_gameflow.json5` file to `cfg/tr1/gameflow.json5`.\n   - Move and rename the `cfg/TR1X_strings*.json5` files to `cfg/tr1/strings*.json5`.\n   - Move and rename the `cfg/TRX_common_strings*.json5` files to `cfg/base_strings*.json5`.\n   - Remove leftover `TR1X_strings_ub.json5`.\n\n    This is how the directory should look:\n    ```\n    .\n    └── cfg\n        ├── base_strings.json5\n        ├── base_strings-pl.json5 (in case you want to provide translation files)\n        ├── base_strings-….json5 (in case you want to provide translation files)\n        ├── tr1\n        │   ├── gameflow.json5\n        │   ├── strings.json5\n        │   ├── strings-pl.json5 (in case you want to provide translation files)\n        │   └── strings-….json5 (in case you want to provide translation files)\n        └── poses.json5\n    ```\n\n### Version 4.9 to 4.10\n\n1. **Update fog configuration**  \n    If you wish to force your fog settings on player:\n    - Rename `draw_distance_fade` to `fog_start`\n    - Rename `draw_distance_max` to `fog_end`\n\n    If you wish to give the player agency to change the fog:\n    - Remove `draw_distance_fade` and `draw_distance_max`\n\n### Version 4.7 to 4.8\n\n1. **Rename basic keys**  \n   - Replace `file` key with `path` for every level.\n   - Replace `music` key with `music_track` for every level.\n\n2. **Update level enumeration structure**:\n   - The `\"type\": \"title\"` property is no longer supported. Instead, the title\n     level needs to be placed in the top-level `\"title\"` key.\n   - The `\"type\": \"cutscene\"` property is no longer supported. Instead, the\n     cutscenes need to be placed in the top-level `\"cutscenes\"` array.\n   - All FMVs need to be placed in its own top-level `\"fmvs\"` array.\n\n3. **Update individual level sequences**  \n   - `start_game` should be removed.\n   - `exit_to_cine` should be removed.\n   - `exit_to_level` should be replaced with `level_complete`. No parameter needed.\n   - `display_picture` no longer takes a `picture_path` argument and instead just takes a `path`.\n   - `loading_screen` no longer takes a `picture_path` argument and instead just takes a `path`.\n   - `level_stats` no longer takes a `level_id` argument.\n   - `total_stats` no longer takes a `picture_path` argument and instead takes a `background_path`.\n   - `play_fmv` no longer takes a `fmv_path` argument and instead takes a `fmv_id`.\n   - `play_synced_audio` is renamed to `play_music` and takes a `music_track` argument rather than `audio_id`.\n\n4. **Update strings**  \n   The game strings are now placed in a separate file, `TR1X_strings.json5` in\n   preparation to eventually support internationalization. Elements such as\n   item titles or item names need to be configured entirely in the new file, so\n   all `\"strings\"` keys can be safely removed from the game flow. Refer to\n   [game strings documentation](4-GAME_STRINGS.md) for more details.\n\n\n\n## TR2X\n\n### Version 1.5 to TRX 1.0\n\n1. **Game flow options moved to the config module**:\n    Certain settings are no longer part of the game flow spec and instead\n    became hidden player settings. To change them, put them in the\n    `enforced_config` section. List of the affected settings:\n    - `lockout_option_ring`\n    - `load_save_disabled`\n    - `play_any_level`\n    - `demo_delay`\n    - `cheat_keys`\n    - `enable_killer_pushblocks`\n\n2. **Removed game flow settings**\n    The following game flow features were removed and are no longer available:\n    - `cmd_init`\n    - `cmd_title`\n    - `cmd_death_in_demo`\n    - `cmd_death_in_game`\n    - `cmd_demo_end`\n    - `cmd_demo_interrupt`\n    - `single_level`\n    - `is_demo_version`\n\n3. **Lara extra animations**: \n   Lara's extra animations have been combined with TR1. You must use the\n   `lara_extra.bin` injection or otherwise refer to \n   https://github.com/LostArtefacts/TRXInjectionTool/blob/main/docs/ASSETS.md\n\n4. **Secret track**:\n   The setting `secret_track` is no longer present – the engine will always\n   play `MX_SECRET` track. To change its slot, please refer to the\n   `catalog_music.csv` file.\n\n### Version 1.3 to 1.4\n\n1. **Update file paths**  \n   - Move and rename the `cfg/TR2X_gameflow.json5` file to `cfg/tr2/gameflow.json5`.\n   - Move and rename the `cfg/TR2X_strings*.json5` files to `cfg/tr2/strings*.json5`.\n   - Move and rename the `cfg/TRX_common_strings*.json5` files to `cfg/base_strings*.json5`.\n   - Remove leftover `TR2X_strings_ub.json5`.\n\n    This is how the directory should look:\n    ```\n    .\n    └── cfg\n        ├── base_strings.json5\n        ├── base_strings-pl.json5 (in case you want to provide translation files)\n        ├── base_strings-….json5 (in case you want to provide translation files)\n        ├── tr2\n        │   ├── gameflow.json5\n        │   ├── strings.json5\n        │   ├── strings-pl.json5 (in case you want to provide translation files)\n        │   └── strings-….json5 (in case you want to provide translation files)\n        └── poses.json5\n    ```\n\n### Version 1.2 to 1.3\n\n1. **Rename objects**\n    - Replace `\"detonator_1\"` with `\"gong\"`.\n    - Replace `\"detonator_2\"` with `\"detonator_box\"`.\n\n2. **Re-add pistols**  \n   Pistols are no longer added automatically to a level that follows one in\n   which Lara previously lost her weapons. A game flow entry to re-add pistols\n   will be required - refer to the Diving Area level in the default game flow.\n\n3. **Bears, wolves and ice warriors**  \n   If you wish to use the bear, wolf or ice warrior (monk with no shadow) from\n   The Golden Mask while still being able to use big spiders, small spiders and\n   other monks, use the following object slots.\n    - Bear: slot 265\n    - Wolf: slot 266\n    - Ice warrior: slot 267\n\n4. **Disabling gym**\n    The option `gym_enabled` is no longer available. If you need to remove the\n    access to Lara's Home, please either remove the relevant level from the\n    game flow (this may break existing saves), or change its type to `\"dummy\"`\n    to get it ignored (this will work with existing saves).\n\n### Version 1.0.2 to 1.1\n\n1. **Update first level inventory allocation**  \n   The first level no longer hard-codes the shotgun, flare and small/large medi\n   pack allocations. To continue to have Lara start with these items, refer to\n   the shipped game flow file's `Great Wall` sequences, specifically the\n   `give_item` entries.\n"
  },
  {
    "path": "docs/trx/MUSIC.md",
    "content": "---\ntitle: Music track IDs\norder: 10\n---\n\n# Music track IDs\n\n### Tomb Raider 1\n\n| ID   | Description |\n| ---  | --- |\n| `0`  | Null (antipads and antitriggers with this track ID will stop current track) |\n| `1`  | Unused |\n| `2`  | _Tomb Raider Theme_ |\n| `3`  | _Where the Depths Unfold_ (part 1) |\n| `4`  | _Tomb Raider Theme_ (alternative mix) |\n| `5`  | Unused |\n| `6`  | _Time to Run_ |\n| `7`  | _Friend Since Gone_ |\n| `8`  | _The T-Rex_ (part 1) |\n| `9`  | _A Long Way Down_ |\n| `10` | _Longing for Home_ |\n| `11` | _Spooky_ (part 1) |\n| `12` | _Keep your balance_ |\n| `13` | Secret sound |\n| `14` | _Spooky_ (part 3) |\n| `15` | _Where the Depths Unfold_ (part 2) |\n| `16` | _The T-Rex_ (part 2) |\n| `17` | _Where the Depths Unfold_ (part 3) |\n| `18` | _Where the Depths Unfold_ (part 4) |\n| `19` | _Tomb Raider Theme_ (alternative mix 2) |\n| `20` | _Time to Run_ (part 2) |\n| `21` | _Longing for Home_ (alternative mix) |\n| `22` | Natla's fall cutscene |\n| `23` | Larson cutscene |\n| `24` | Natla placing Scion cutscene |\n| `25` | Lara in Tomb of Tihocan cutscene |\n| `26` | Gym hint \"Welcome to my home. I'll take you on a guided tour.\" |\n| `27` | Gym hint \"Use the D-Pad to go to the music room.\" |\n| `28` | Gym hint \"OK. Let's do some tumbling. Press the Jump button.\" |\n| `29` | Gym hint \"Now press it again and quickly press one of the directions, and I'll jump that way.\" |\n| `30` | Gym hint \"Ah, the main hall. Sorry about the crates. I'm having some things put into storage, and the delivery people haven't been yet.\" |\n| `31` | Gym hint \"Run up to a crate, and while still pressing Forward, press Action, and I'll vault up onto it.\" |\n| `32` | Gym hint \"This used to be the ball room, but I've converted it into my own personal gym. What do you think? Well, let's do some exercises!\" |\n| `33` | Gym hint \"I don't actually run everywhere. When I want to be careful, I walk. Hold down the Walk button, and walk to the white line.\" |\n| `34` | Gym hint \"With the Walk button down, I won't fall off even if you try to make me. Go on, try it.\" |\n| `35` | Gym hint \"If you want to look around, press and hold the Look button. Then press in the direction where you want to look.\" |\n| `36` | Gym hint \"If a jump is too far for me, I can grab the ledge and save myself from a nasty fall. Walk to the edge with the white line until I won't go any further. Then press Jump immediately followed by Forward, and while I'm in the air, press and hold the Action button.\" |\n| `37` | Gym hint \"Press Forward, and I'll climb up.\" |\n| `38` | Gym hint \"If I do a running jump, I can make a jump like that, no problem.\" |\n| `39` | Gym hint \"Walk to the edge with a white line until I stop. Then let go of Walk, and tap Backwards to give me a run-up. Press Forward, and almost immediately press and hold the Jump button. I won't actually jump until the last minute.\" |\n| `40` | Gym hint \"Right, this is a really big one. So do a running jump exactly as before, except while I'm in the air, press and hold the Action button to make me grab the edge.\" |\n| `41` | Gym hint \"Nice!\" |\n| `42` | Gym hint \"Try to vault up here. Press Forward, and hold Action.\" |\n| `43` | Gym hint \"I can't climb up because the gap is too small. But press Right, and I'll shimmy sideways until there is room. Then press Forward.\" |\n| `44` | Gym hint \"Great! If there is a long jump and I don't want to hurt myself jumping off, I can let myself down carefully.\" |\n| `45` | Gym hint \"Tap Backwards, and I'll jump off Backwards. Immediately press and hold the Action button, and I'll grab the ledge on the way down.\" |\n| `46` | Gym hint \"Then let go.\" |\n| `47` | Gym hint \"Let's go for a swim!\" |\n| `48` | Gym hint \"The Jump button and the directions move me around underwater.\" |\n| `49` | Gym hint \"Oh, air! Just use Forward, and Left and Right, to manoeuvre around on the surface. Press Jump to dive down for another swim-about, or go to the edge and press Action to climb out.\" |\n| `50` | Gym hint \"Right. Now I'd better take off these wet clothes.\" |\n| `51` | Baldy's speech \"Say cheese!\" |\n| `52` | Cowboy's speech \"Ain't nothing personal.\" |\n| `53` | Larson's speech \"I still got a pain in my brain from ya – and it's telling me funny ideas now, like to shoot you to hell!\" |\n| `54` | Natla's speech \"You can't bump off me and my brood so easily, Lara!\" |\n| `55` | Pierre's speech \"A little late for the prize giving, no? Still, it is the taking part which counts.\" |\n| `56` | Skate kid's speech \"You firing at me? You firing at me, huh? Ain't nobody else here, so you must be firing at me!\" |\n| `57` | Caves ambience |\n| `58` | Cistern ambience |\n| `59` | Windy ambience |\n| `60` | Atlantis ambience |\n\n### Tomb Raider 2\n\n| ID   | Description |\n| ---  | --- |\n| `2`  | Cutscene (The Great Wall) |\n| `3`  | Cutscene (Opera House) |\n| `4`  | Cutscene (Brother Chan) |\n| `5`  | Gym hint \"Welcome back. After that grueling business last year I decided to build this assault course to hone my skills… and learn some new ones.\" |\n| `6`  | Gym hint \"No, that's not right. You need to press Jump and Forward together for me to clear the gap. Run back to the start and try again.\" |\n| `7`  | Gym hint \"For bigger gaps I need to do a running jump. Back to the start.\" |\n| `8`  | Gym hint \"To climb up, press Forward and hold down the Action button.\" |\n| `9`  | Gym hint \"To avoid falling off the ledges, you can use the Walk button. That way, I'm more careful and won't step over the edge.\" |\n| `10` | Gym hint \"To make that jump successfully, I need to walk back as far as possible from the edge, then take a running jump as before. Or you could walk to the edge, jump forward, and press Action to make me grab the far edge.\" |\n| `11` | Gym hint \"Okay, that was quite a tough one. You need to make a running jump, and then press Action for me to grab the edge. To get the run-up exactly right, walk to the edge, then tap Backwards to jump back. If you run forwards from there and press Jump, I'll make it just right.\" |\n| `12` | Gym hint \"I can climb up this wall. Just walk up to it and hold down the Action button. You can then use the direction buttons to climb up, down, and side-to-side.\" |\n| `13` | Gym hint \"This set of obstacles is easiest to traverse by using my sideways jumps. Just press Left or Right at the same time as Jump. I can jump backwards, too.\" |\n| `14` | Gym hint \"This is another climbing wall. You can tell by the footholds.\" |\n| `15` | Gym hint \"Oh, dear. Back to the start if I want to beat my best time.\" |\n| `16` | Gym hint \"To swim underwater, press the Jump button and the direction buttons to guide me.\" |\n| `17` | Gym hint \"On the surface, the direction buttons move me about, and the Jump button makes me dive underwater. At the edge of the pool, I can climb out by using the Action button.\" |\n| `18` | Gym hint \"Great, but nowhere near my best time.\" |\n| `19` | Gym hint \"Gosh! That was my best time yet!\" |\n| `20` | Gym hint \"Almost. Perhaps another try, and I might beat it.\" |\n| `21` | Gym hint \"Congratulations! You did it! But perhaps I could've been faster.\" |\n| `22` | Gym hint \"Hi there. Let's see if I can beat my best time.\" |\n| `23` | Cutscene (bathroom) |\n| `24` | Dagger pull |\n| `25` | Gym hint \"Feel free to explore the rest of the house and gardens.\" |\n| `26` | Cutscene (Temple of Xian) |\n| `27` | Caves ambience |\n| `28` | Sewers ambience |\n| `29` | Windy ambience |\n| `30` | Heartbeat ambience |\n| `31` | Surprise 1 |\n| `32` | Surprise 2 |\n| `33` | Surprise 3 |\n| `34` | Ooh-aah 1 |\n| `35` | Ooh-aah 2 |\n| `36` | Venice Violins |\n| `37` | End of level |\n| `41` | Harp theme |\n| `42` | Mystery 1 |\n| `44` | Ambush 1 |\n| `45` | Ambush 2 |\n| `46` | Ambush 3 |\n| `47` | Ambush 4 |\n| `48` | Skidoo theme |\n| `49` | Battle theme |\n| `50` | Mystery 2 |\n| `51` | Mystery 3 |\n| `52` | Mystery 4 |\n| `53` | Mystery 5 |\n| `54` | Rig ambience |\n| `55` | Tomb ambience |\n| `56` | Ooh-aah 3 |\n| `57` | Reveal 1 |\n| `58` | Cutscene (Offshore Rig) |\n| `59` | Reveal 2 |\n| `60` | Title theme |\n| `61` | Unused |\n"
  },
  {
    "path": "docs/trx/OUTFITS.md",
    "content": "---\ntitle: Lara's outfits\norder: 16\n---\n\n# Outfits\n\nIn TR1 and TR2 originally, Lara's meshes were taken from the `O_LARA` object,\nwith mesh swaps being performed as required at runtime using additional objects,\nsuch as `O_LARA_PISTOL`, `O_LARA_SHOTGUN`, `O_LARA_EXTRA` etc. TR3 moved to a\ndedicated `O_LARA_SKIN` object, but still depended on the additional gun and\nextra mesh swaps, and these remained tightly coupled with the level's outfit.\nFor example, when putting a shotgun in Lara's hand, the relevant mesh would\ninclude an entire copy of her hand, when in reality only the shotgun was\nrequired. This meant if customizing Lara's gloves, the builder would need to do\nso on several different objects.\n\nTRX uses a different skin system, both to allow outfit swaps in-game for players\nand to remove unnecessary mesh faces where applicable for a more streamlined\ndata setup. Custom level builders can define up to 32 outfits; following is a\nguide to the data and JSON configuration, and some scenario/workflow examples.\n\n## Data setup\n\nThe skin system uses the following objects. These are provided in the\n`lara_outfits.bin` injection, and are available to download as a separate WAD\n(see [injections](INJECTIONS.md)).\n\n#### `O_LARA_SKIN_SWAP_1`...`O_LARA_SKIN_SWAP_32`\nEach of these should contain a distinct Lara model, with the mesh count and bone\norder conforming to the standard for Lara. Bone offsets are used (e.g. consider\nBacon Lara's different structure); animations are not used.\n\n#### `O_LARA_SKIN_SWAP_EXTRA`\nThis object contains various additional meshes for Lara, such as altered torsos\nwhen the TR1 braid is in use, Lara's combat face, and meshes used in extra\nanimations, such as pulling the dagger in Dragon's Lair. It also contains both\nthe TR1 and TR2/3 braid.\n\n#### `O_LARA_SKIN_SWAP_GUNS`\nThis object contains holsters - both empty and equipped with the various guns - \nas well as the guns themselves when they are in Lara's hands or on her back.\n\n#### `O_LARA_SKIN_SWAP_LEGS`\nThis object contains copies of Lara's legs for each outfit, with holster strap\ntextures removed. This allows levels such as Home Sweet Home to swap out Lara's\nlegs when she has no holsters. It is not an essential object to include.\n\n## JSON setup\nThe file `cfg/outfits.json5` sets up the available outfits, and how they should\nbehave. The structure of this file is described below.\n\n#### Top-level overview\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>outfits</code></td>\n    <td>Object map</td>\n    <td>\n      The keys in this map define the available outfits, and these are the keys\n      that should be used in the game-flow. The outfit object is described\n      separately below.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>extra_meshes</code></td>\n    <td>Integer map</td>\n    <td>\n      This map defines mesh offsets in <code>O_LARA_SKIN_SWAP_EXTRA</code>,\n      which are required for various events/paths in the engine.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>gun_maps</code></td>\n    <td>Object array</td>\n    <td>\n      These maps dictate which meshes to use in\n      <code>O_LARA_SKIN_SWAP_GUNS</code> for a given outfit and gun combination.\n    </td>\n  </tr>\n</table>\n\n### Outfits\n\n<details>\n<summary>Show snippet</summary>\n\n```json\n\"tr1_classic\": {\n  \"name_gs\": \"dynamic/enums/lara_outfit/tr1_classic\",\n  \"mesh_object\": \"O_LARA_SKIN_SWAP_2\",\n  \"is_reflective\": false,\n  \"gun_map\": 0,\n  \"combat_face_offset\": 1,\n  \"supports_sunglasses\": true,\n  \"braid\": {\n    \"mode\": \"BRAID_MODE_TR1_FULL\",\n    \"mesh_offset\": 10,\n    \"gold_offset\": 16,\n    \"hair_pos\": {\n      \"x\": 0,\n      \"y\": 20,\n      \"z\": -45,\n    },\n  },\n  \"no_holster_offsets\": {\n    \"thigh_r\": 1,\n    \"thigh_l\": 2,\n  },\n  \"extra_outfits\": {\n    \"LS_EXTRA_TREX_KILL\": \"tr1_mauled\",\n    \"LS_EXTRA_MIDAS_KILL\": \"tr1_golden_lara\",\n  },\n},\n```\n</details>\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th colspan=\"2\">Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>name_gs</code></td>\n    <td>String</td>\n    <td colspan=\"2\">\n      The game string key used for localized UI labels for this outfit\n      (for example, <code>dynamic/enums/lara_outfit/tr1_classic</code>).\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>mesh_object</code></td>\n    <td>String</td>\n    <td colspan=\"2\">\n     Indicates which object contains the outfit's meshes and bones.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>is_reflective</code></td>\n    <td>Boolean</td>\n    <td colspan=\"2\">Indicates whether or not the outfit is reflective.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>gun_map</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      The index into the <code>gun_maps</code> array to use for this outfit.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>braid</code></td>\n    <td>Object</td>\n    <td colspan=\"2\">\n      The braid setup specific to this outfit. If omitted, no braid will be\n      shown. See the braids section below.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>combat_face_offset</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      The mesh offset in <code>O_LARA_SKIN_SWAP_EXTRA</code> for Lara's combat\n      face. <code>-1</code> implies no combat face swap. This mesh is used when\n      Lara is firing a weapon (traditionally, the <code>O_LARA_UZI</code> head\n      mesh was used).\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>supports_sunglasses</code></td>\n    <td>Boolean</td>\n    <td colspan=\"2\">\n      Defines whether or not sunglasses can be used with this outfit.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>no_holster_offsets</code></td>\n    <td>Integer map</td>\n    <td colspan=\"2\">\n      The mesh offsets in <code>O_LARA_SKIN_SWAP_LEGS</code> to use when Lara's\n      holsters aren't visible. Omitting this property infers no mesh swaps.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"3\"><code>extra_outfits</code></td>\n    <td rowspan=\"3\">String map</td>\n    <td colspan=\"2\">\n      Pointers to alternative outfits to use for specific game events. The two\n      supported events are as follows. If these are omitted, no swaps will occur\n      for the events.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>LS_EXTRA_TREX_KILL</code></td>\n    <td>When Lara is killed by the T-rex - instant full outfit swap.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>LS_EXTRA_MIDAS_KILL</code></td>\n    <td>When Lara steps on the Midas hand - progressive outfit swap.</td>\n  </tr>\n</table>\n\n### Braids\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th colspan=\"2\">Description</th>\n  </tr>\n  <tr valign=\"top\" >\n    <td rowspan=\"6\"><code>mode</code></td>\n    <td rowspan=\"6\">String</td>\n    <td colspan=\"2\">Indicates special handling when the braid is active.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>BRAID_MODE_NONE</code></td>\n    <td>\n      No special treatment (this mode is implied if <code>mode</code> is not\n      specified).\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>BRAID_MODE_TR1_HEAD_ONLY</code></td>\n    <td>\n      Replaces Lara's head with <code>EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD</code>\n      defined in the <code>O_LARA_SKIN_SWAP_EXTRA</code> object.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>BRAID_MODE_TR1_FULL</code></td>\n    <td>\n      As per <code>BRAID_MODE_TR1_HEAD_ONLY</code>, plus Lara's torso will be\n      replaced with <code>EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO</code>.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>BRAID_MODE_TR1_MAULED</code></td>\n    <td>\n      As per <code>BRAID_MODE_TR1_FULL</code>, but the torso swap mesh used here\n      is <code>EXTRA_MESH_TR1_BRAID_MAULED_TORSO</code>.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>BRAID_MODE_TR1_GOLD</code></td>\n    <td>\n      As per <code>BRAID_MODE_TR1_FULL</code>, but the swap meshes used here\n      are <code>EXTRA_MESH_TR1_BRAID_GOLD_HEAD</code> and\n      <code>EXTRA_MESH_TR1_BRAID_GOLD_TORSO</code>.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>mesh_offset</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      The starting offset in <code>O_LARA_SKIN_SWAP_EXTRA</code> for the regular\n      braid meshes and bones.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>gold_offset</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      The starting offset in <code>O_LARA_SKIN_SWAP_EXTRA</code> for the golden\n      braid meshes and bones.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>hair_pos</code></td>\n    <td>XYZ</td>\n    <td colspan=\"2\">\n      The position relative to Lara's head where the braid will be drawn.\n    </td>\n  </tr>\n</table>\n\n### Guns\n\n<details>\n<summary>Show snippet</summary>\n\n```json\n{\n  \"LGT_DESERT_EAGLE\": {\n    \"hand_r\": 62,\n    \"thigh_r\": 9,\n  },\n  \"LGT_UZIS\": {\n    \"hand_r\": 63,\n    \"hand_l\": 64,\n    \"thigh_r\": 10,\n    \"thigh_l\": 11,\n  },\n  \"LGT_SHOTGUN\": {\n    \"hand_r\": 65,\n    \"torso\": 72,\n  },\n}\n```\n</details>\n\nThe map keys must match known engine weapons. See [weapons](WEAPONS.md) and\n`cfg/weapons.json5` for reference. Any entry may omit hand, thigh or torso, and\nnote that specific gun types will only look for particular entries. For example,\ndefining a `thigh_l` property for `LGT_SHOTGUN` is meaningless and will be\nignored. Any missing fields imply that no mesh is drawn for that slot.\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>hand_r</code><br/>\n      <code>hand_l</code>\n    </td>\n    <td>Integer</td>\n    <td>\n      The mesh offset in <code>O_LARA_SKIN_SWAP_GUNS</code> for the gun to draw\n      in Lara's hands.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>thigh_r</code><br/>\n      <code>thigh_l</code>\n    </td>\n    <td>Integer</td>\n    <td>\n      The mesh offset in <code>O_LARA_SKIN_SWAP_GUNS</code> for the gun to draw\n      against Lara's thighs. This expects the holster to be part of the mesh.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>torso</code>\n    </td>\n    <td>Integer</td>\n    <td>\n      The mesh offset in <code>O_LARA_SKIN_SWAP_GUNS</code> for the gun to draw\n      on Lara's back.\n    </td>\n  </tr>\n</table>\n\n## Custom level use cases\n\n> I don't need to customize Lara's outfit, and I don't mind players freely\nswitching outfits.\n\nIn this scenario, simply ship your level with `lara_outfits.bin` and the default\n`cfg/outfits.json5`.\n\n***\n\n> I don't need to customize Lara's outfit, nor do I want the player to change\nit.\n\nIn this case, you can simply enforce the outfit option, and continue to ship the\nstandard files as above. Provided your game-flow level has an accurate\n`lara_outfit` entry, you can enforce as follows.\n\n```json\n\"enforced_config\": {\n  \"lara_outfit\": null,\n},\n```\n\nUsing null here means that if your second level uses a different outfit, that\nwill still be honoured.\n\n***\n\n> I don't need to customize Lara's outfits, but I want to restrict the ones that\ncan be selected by the player.\n\nShip the default injection file, but edit `cfg/outfits.json5` by removing the \nentries from the `outfits` section that you do not need.\n\n***\n\n> I want to customize Lara's outfits.\n\nDownload the provided TRX WAD to access the data included in the shipped\ninjection. Move each of the relevant objects to your WAD. You can then proceed\nto edit the meshes as required. Ensure that you remove the `lara_outfits.bin`\nfrom your game-flow, otherwise these will override the data in your level.\n\n> I want to customize Lara's braid.\n\nBraid meshes and bones are taken from the `O_LARA_SKIN_SWAP_EXTRA` object, so to\ncustomize it you will need to follow the same steps as for customizing outfits\ni.e. import the TRX WAD, edit the data and remove the injection.\n"
  },
  {
    "path": "docs/trx/SAMPLES.md",
    "content": "---\ntitle: Sample IDs\norder: 11\n---\n\n# Sample IDs\n\n### Tomb Raider 1\n\n| ID    | Description |\n| ---   | --- |\n| `0`   | SFX_LARA_FEET             |\n| `1`   | SFX_LARA_CLIMB_2          |\n| `2`   | SFX_LARA_NO               |\n| `3`   | SFX_LARA_SLIPPING         |\n| `4`   | SFX_LARA_LAND             |\n| `5`   | SFX_LARA_CLIMB_1          |\n| `6`   | SFX_LARA_DRAW             |\n| `7`   | SFX_LARA_HOLSTER          |\n| `8`   | SFX_LARA_PISTOLS          |\n| `9`   | SFX_LARA_RELOAD           |\n| `10`  | SFX_LARA_RICOCHET         |\n| `11`  | SFX_BEAR_GROWL            |\n| `12`  | SFX_BEAR_FEET             |\n| `13`  | SFX_BEAR_ATTACK           |\n| `14`  | SFX_BEAR_SNARL            |\n| `15`  | SFX_LARA_WET_FEET         |\n| `16`  | SFX_BEAR_HURT             |\n| `17`  | SFX_LARA_WADE             |\n| `18`  | SFX_BEAR_DEATH            |\n| `19`  | SFX_WOLF_JUMP             |\n| `20`  | SFX_WOLF_HURT             |\n| `22`  | SFX_WOLF_DEATH            |\n| `24`  | SFX_WOLF_HOWL             |\n| `25`  | SFX_WOLF_ATTACK           |\n| `26`  | SFX_LARA_CLIMB_3          |\n| `27`  | SFX_LARA_BODYSL           |\n| `28`  | SFX_LARA_SHIMMY_2         |\n| `29`  | SFX_LARA_JUMP             |\n| `30`  | SFX_LARA_FALL             |\n| `31`  | SFX_LARA_INJURY           |\n| `32`  | SFX_LARA_ROLL             |\n| `33`  | SFX_LARA_SPLASH           |\n| `34`  | SFX_LARA_GETOUT           |\n| `35`  | SFX_LARA_SWIM             |\n| `36`  | SFX_LARA_BREATH           |\n| `37`  | SFX_LARA_BUBBLES          |\n| `38`  | SFX_LARA_SWITCH           |\n| `39`  | SFX_LARA_KEY              |\n| `40`  | SFX_LARA_OBJECT           |\n| `41`  | SFX_LARA_GENERAL_DEATH    |\n| `42`  | SFX_LARA_KNEES_DEATH      |\n| `43`  | SFX_LARA_UZI_FIRE         |\n| `44`  | SFX_LARA_MAGNUMS          |\n| `45`  | SFX_LARA_SHOTGUN          |\n| `46`  | SFX_LARA_BLOCK_PUSH_1     |\n| `47`  | SFX_LARA_BLOCK_PUSH_2     |\n| `48`  | SFX_LARA_EMPTY            |\n| `50`  | SFX_LARA_BULLETHIT        |\n| `51`  | SFX_LARA_BLKPULL          |\n| `52`  | SFX_LARA_FLOATING         |\n| `53`  | SFX_LARA_FALLDETH         |\n| `54`  | SFX_LARA_GRABHAND         |\n| `55`  | SFX_LARA_GRABBODY         |\n| `56`  | SFX_LARA_GRABFEET         |\n| `57`  | SFX_LARA_SWITCHUP         |\n| `58`  | SFX_BAT_SQK               |\n| `59`  | SFX_BAT_FLAP              |\n| `60`  | SFX_UNDERWATER            |\n| `61`  | SFX_UNDERWATER_SWITCH     |\n| `63`  | SFX_BLOCK_SOUND           |\n| `64`  | SFX_DOOR                  |\n| `65`  | SFX_PENDULUM_BLADES       |\n| `66`  | SFX_ROCK_FALL_CRUMBLE     |\n| `67`  | SFX_ROCK_FALL_FALL        |\n| `68`  | SFX_ROCK_FALL_LAND        |\n| `69`  | SFX_T_REX_DEATH           |\n| `70`  | SFX_T_REX_STOMP           |\n| `70`  | SFX_PUSHBLOCK_LAND        |\n| `70`  | SFX_EARTHQUAKE_2          |\n| `71`  | SFX_T_REX_ROAR            |\n| `72`  | SFX_T_REX_ATTACK          |\n| `73`  | SFX_RAPTOR_ROAR           |\n| `74`  | SFX_RAPTOR_ATTACK         |\n| `75`  | SFX_RAPTOR_FEET           |\n| `76`  | SFX_MUMMY_GROWL           |\n| `77`  | SFX_LARSON_FIRE           |\n| `78`  | SFX_LARSON_RICOCHET       |\n| `79`  | SFX_WATERFALL_LOOP        |\n| `80`  | SFX_WATER_LOOP            |\n| `81`  | SFX_WATERFALL_BIG         |\n| `82`  | SFX_CHAINDOOR_UP          |\n| `83`  | SFX_CHAINDOOR_DOWN        |\n| `84`  | SFX_COGS                  |\n| `85`  | SFX_LION_HURT             |\n| `86`  | SFX_LION_ATTACK           |\n| `87`  | SFX_LION_ROAR             |\n| `88`  | SFX_LION_DEATH            |\n| `89`  | SFX_GORILLA_FEET          |\n| `90`  | SFX_GORILLA_PANT          |\n| `91`  | SFX_GORILLA_DEATH         |\n| `92`  | SFX_CROC_FEET             |\n| `93`  | SFX_CROC_ATTACK           |\n| `94`  | SFX_RAT_FEET              |\n| `95`  | SFX_RAT_CHIRP             |\n| `96`  | SFX_RAT_ATTACK            |\n| `97`  | SFX_RAT_DEATH             |\n| `98`  | SFX_THUNDER               |\n| `99`  | SFX_EXPLOSION_2           |\n| `100` | SFX_GORILLA_GRUNT         |\n| `101` | SFX_GORILLA_GRUNTS        |\n| `102` | SFX_CROC_DEATH            |\n| `103` | SFX_DAMOCLES_SWORD        |\n| `104` | SFX_EXPLOSION_1           |\n| `108` | SFX_MENU_ROTATE           |\n| `109` | SFX_MENU_LARA_HOME        |\n| `110` | SFX_MENU_GAMEBOY          |\n| `111` | SFX_MENU_SPININ           |\n| `112` | SFX_MENU_SPINOUT          |\n| `113` | SFX_MENU_COMPASS          |\n| `114` | SFX_MENU_GUNS             |\n| `115` | SFX_MENU_PASSPORT         |\n| `116` | SFX_MENU_MEDI             |\n| `117` | SFX_RAISINGBLOCK_FX       |\n| `118` | SFX_SAND_FX               |\n| `119` | SFX_STAIRS_2_SLOPE_FX     |\n| `120` | SFX_ATLANTEAN_WALK        |\n| `121` | SFX_ATLANTEAN_ATTACK      |\n| `122` | SFX_ATLANTEAN_JUMP_ATTACK |\n| `123` | SFX_ATLANTEAN_NEEDLE      |\n| `124` | SFX_ATLANTEAN_BALL        |\n| `125` | SFX_ATLANTEAN_WINGS       |\n| `126` | SFX_ATLANTEAN_RUN         |\n| `127` | SFX_SLAMDOOR_CLOSE        |\n| `128` | SFX_SLAMDOOR_OPEN         |\n| `129` | SFX_SKATEBOARD_MOVE       |\n| `130` | SFX_SKATEBOARD_STOP       |\n| `131` | SFX_SKATEBOARD_SHOOT      |\n| `132` | SFX_SKATEBOARD_HIT        |\n| `133` | SFX_SKATEBOARD_START      |\n| `134` | SFX_SKATEBOARD_DEATH      |\n| `135` | SFX_SKATEBOARD_HIT_GROUND |\n| `136` | SFX_TORSO_HIT_GROUND      |\n| `137` | SFX_TORSO_ATTACK_1        |\n| `138` | SFX_TORSO_ATTACK_2        |\n| `139` | SFX_TORSO_DEATH           |\n| `140` | SFX_TORSO_ARM_SWING       |\n| `141` | SFX_TORSO_MOVE            |\n| `142` | SFX_TORSO_HIT             |\n| `143` | SFX_CENTAUR_FEET          |\n| `144` | SFX_CENTAUR_ROAR          |\n| `145` | SFX_LARA_SPIKE_DEATH      |\n| `146` | SFX_LARA_DEATH_3          |\n| `147` | SFX_ROLLING_BALL_1_ROLL   |\n| `147` | SFX_EARTHQUAKE_1          |\n| `148` | SFX_LAVA_LOOP             |\n| `149` | SFX_LAVA_FOUNTAIN         |\n| `150` | SFX_LOOP_FOR_SMALL_FIRES  |\n| `151` | SFX_DART                  |\n| `152` | SFX_METAL_DOOR_CLOSE      |\n| `153` | SFX_METAL_DOOR_OPEN       |\n| `154` | SFX_ALTAR_LOOP            |\n| `155` | SFX_POWERUP_FX            |\n| `156` | SFX_COWBOY_DEATH          |\n| `157` | SFX_BLACK_GOON_DEATH      |\n| `158` | SFX_LARSON_DEATH          |\n| `159` | SFX_PIERRE_DEATH          |\n| `160` | SFX_NATLA_DEATH           |\n| `161` | SFX_TRAPDOOR_OPEN         |\n| `162` | SFX_TRAPDOOR_CLOSE        |\n| `163` | SFX_ATLANTEAN_EGG_LOOP    |\n| `164` | SFX_ATLANTEAN_EGG_HATCH   |\n| `165` | SFX_DRILL_ENGINE_START    |\n| `166` | SFX_DRILL_ENGINE_LOOP     |\n| `167` | SFX_CONVEYOR_BELT         |\n| `168` | SFX_HUT_LOWERED           |\n| `169` | SFX_HUT_HIT_GROUND        |\n| `170` | SFX_EXPLOSION_FX          |\n| `171` | SFX_ATLANTEAN_DEATH       |\n| `172` | SFX_CHAINBLOCK_FX         |\n| `173` | SFX_SECRET                |\n| `174` | SFX_GYM_HINT_01           |\n| `175` | SFX_GYM_HINT_02           |\n| `176` | SFX_GYM_HINT_03           |\n| `177` | SFX_GYM_HINT_04           |\n| `178` | SFX_GYM_HINT_05           |\n| `179` | SFX_GYM_HINT_06           |\n| `180` | SFX_GYM_HINT_07           |\n| `181` | SFX_GYM_HINT_08           |\n| `182` | SFX_GYM_HINT_09           |\n| `183` | SFX_GYM_HINT_10           |\n| `184` | SFX_GYM_HINT_11           |\n| `185` | SFX_GYM_HINT_12           |\n| `186` | SFX_GYM_HINT_13           |\n| `187` | SFX_GYM_HINT_14           |\n| `188` | SFX_GYM_HINT_15           |\n| `189` | SFX_GYM_HINT_16           |\n| `190` | SFX_GYM_HINT_17           |\n| `191` | SFX_GYM_HINT_18           |\n| `192` | SFX_GYM_HINT_19           |\n| `193` | SFX_GYM_HINT_20           |\n| `194` | SFX_GYM_HINT_21           |\n| `195` | SFX_GYM_HINT_22           |\n| `196` | SFX_GYM_HINT_23           |\n| `197` | SFX_GYM_HINT_24           |\n| `198` | SFX_GYM_HINT_25           |\n| `199` | SFX_BALDY_SPEECH          |\n| `200` | SFX_COWBOY_SPEECH         |\n| `201` | SFX_LARSON_SPEECH         |\n| `202` | SFX_NATLA_SPEECH          |\n| `203` | SFX_PIERRE_SPEECH         |\n| `204` | SFX_SKATEKID_SPEECH       |\n| `205` | SFX_LARA_SETUP            |\n\n### Tomb Raider 2\n\n| ID    | Description |\n| ---   | --- |\n| `0`   | SFX_LARA_FEET |\n| `1`   | SFX_LARA_CLIMB_2 |\n| `2`   | SFX_LARA_NO |\n| `3`   | SFX_LARA_SLIPPING |\n| `4`   | SFX_LARA_LAND |\n| `5`   | SFX_LARA_CLIMB_1 |\n| `6`   | SFX_LARA_DRAW |\n| `7`   | SFX_LARA_HOLSTER |\n| `8`   | SFX_LARA_PISTOLS |\n| `9`   | SFX_LARA_RELOAD |\n| `10`  | SFX_LARA_RICOCHET |\n| `11`  | SFX_LARA_FLARE_IGNITE |\n| `12`  | SFX_LARA_FLARE_BURN |\n| `15`  | SFX_LARA_HARPOON_FIRE |\n| `16`  | SFX_LARA_HARPOON_LOAD |\n| `17`  | SFX_LARA_WET_FEET |\n| `18`  | SFX_LARA_WADE |\n| `20`  | SFX_LARA_TREAD |\n| `21`  | SFX_LARA_AUTOS |\n| `22`  | SFX_LARA_HARPOON_LOAD_WATER |\n| `23`  | SFX_LARA_HARPOON_FIRE_WATER |\n| `24`  | SFX_MASSIVE_CRASH |\n| `25`  | SFX_PUSH_SWITCH |\n| `26`  | SFX_LARA_CLIMB_3 |\n| `27`  | SFX_LARA_BODYSL |\n| `28`  | SFX_LARA_SHIMMY |\n| `29`  | SFX_LARA_JUMP |\n| `30`  | SFX_LARA_FALL |\n| `31`  | SFX_LARA_INJURY |\n| `32`  | SFX_LARA_ROLL |\n| `33`  | SFX_LARA_SPLASH |\n| `34`  | SFX_LARA_GETOUT |\n| `35`  | SFX_LARA_SWIM |\n| `36`  | SFX_LARA_BREATH |\n| `37`  | SFX_LARA_BUBBLES |\n| `38`  | SFX_LARA_SWITCH |\n| `39`  | SFX_LARA_KEY |\n| `40`  | SFX_LARA_OBJECT |\n| `41`  | SFX_LARA_GENERAL_DEATH |\n| `42`  | SFX_LARA_KNEES_DEATH |\n| `43`  | SFX_LARA_UZI_FIRE |\n| `44`  | SFX_LARA_UZI_STOP |\n| `45`  | SFX_LARA_SHOTGUN |\n| `46`  | SFX_LARA_BLOCK_PUSH_1 |\n| `47`  | SFX_LARA_BLOCK_PUSH_2 |\n| `48`  | SFX_CLICK |\n| `49`  | SFX_LARA_HIT |\n| `50`  | SFX_LARA_BULLETHIT |\n| `51`  | SFX_LARA_BLKPULL |\n| `52`  | SFX_LARA_FLOATING |\n| `53`  | SFX_LARA_FALLDETH |\n| `54`  | SFX_LARA_GRABHAND |\n| `55`  | SFX_LARA_GRABBODY |\n| `56`  | SFX_LARA_GRABFEET |\n| `57`  | SFX_LARA_SWITCHUP |\n| `58`  | SFX_GLASS_BREAK |\n| `59`  | SFX_WATER_LOOP |\n| `60`  | SFX_UNDERWATER |\n| `61`  | SFX_UNDERWATER_SWITCH |\n| `62`  | SFX_LARA_PICKUP |\n| `63`  | SFX_BLOCK_SOUND |\n| `64`  | SFX_DOOR |\n| `65`  | SFX_SWING |\n| `66`  | SFX_ROCK_FALL_CRUMBLE |\n| `67`  | SFX_ROCK_FALL_LAND |\n| `68`  | SFX_ROCK_FALL_SOLID |\n| `69`  | SFX_ENEMY_FEET |\n| `70`  | SFX_ENEMY_GRUNT |\n| `71`  | SFX_ENEMY_HIT_1 |\n| `72`  | SFX_ENEMY_HIT_2 |\n| `73`  | SFX_ENEMY_DEATH_1 |\n| `74`  | SFX_ENEMY_JUMP |\n| `75`  | SFX_ENEMY_CLIMBUP |\n| `76`  | SFX_ENEMY_CLIMBDOWN |\n| `77`  | SFX_WEAPON_CLATTER |\n| `78`  | SFX_M16_FIRE |\n| `79`  | SFX_WATERFALL_LOOP |\n| `80`  | SFX_SWORD_STATUE_DROP |\n| `81`  | SFX_SWORD_STATUE_LIFT |\n| `82`  | SFX_PORTCULLIS_UP |\n| `83`  | SFX_PORTCULLIS_DOWN |\n| `84`  | SFX_DOG_FEET_1 |\n| `85`  | SFX_BODY_SLAM |\n| `86`  | SFX_DOG_BARK_1 |\n| `87`  | SFX_DOG_FEET_2 |\n| `88`  | SFX_DOG_BARK_2 |\n| `89`  | SFX_DOG_DEATH |\n| `90`  | SFX_DOG_PANT |\n| `91`  | SFX_LEOPARD_FEET |\n| `92`  | SFX_LEOPARD_ROAR |\n| `93`  | SFX_LEOPARD_BITE |\n| `94`  | SFX_LEOPARD_STRIKE |\n| `95`  | SFX_LEOPARD_DEATH |\n| `96`  | SFX_LEOPARD_GROWL |\n| `97`  | SFX_RAT_ATTACK |\n| `98`  | SFX_RAT_DEATH |\n| `99`  | SFX_TIGER_ROAR |\n| `100` | SFX_TIGER_BITE |\n| `101` | SFX_TIGER_STRIKE |\n| `102` | SFX_TIGER_DEATH |\n| `103` | SFX_TIGER_GROWL |\n| `104` | SFX_M16_STOP |\n| `105` | SFX_EXPLOSION_1 |\n| `106` | SFX_GROWL |\n| `107` | SFX_SPIDER_JUMP |\n| `108` | SFX_MENU_ROTATE |\n| `109` | SFX_MENU_LARA_HOME |\n| `111` | SFX_MENU_SPININ |\n| `112` | SFX_MENU_SPINOUT |\n| `113` | SFX_MENU_STOPWATCH |\n| `114` | SFX_MENU_GUNS |\n| `115` | SFX_MENU_PASSPORT |\n| `116` | SFX_MENU_MEDI |\n| `117` | SFX_ENEMY_HEELS |\n| `118` | SFX_ENEMY_FIRE_SILENCER |\n| `119` | SFX_ENEMY_AH_DYING |\n| `120` | SFX_ENEMY_OOH_DYING |\n| `121` | SFX_ENEMY_THUMP |\n| `122` | SFX_SPIDER_MOVING |\n| `123` | SFX_LARA_MINI_LOAD |\n| `124` | SFX_LARA_MINI_LOCK |\n| `125` | SFX_LARA_MINI_FIRE |\n| `126` | SFX_SPIDER_BITE |\n| `127` | SFX_SLAM_DOOR_SLIDE |\n| `128` | SFX_SLAM_DOOR_CLOSE |\n| `129` | SFX_EAGLE_SQUAWK |\n| `130` | SFX_EAGLE_WING_FLAP |\n| `131` | SFX_EAGLE_DEATH |\n| `132` | SFX_CROW_CAW |\n| `133` | SFX_CROW_WING_FLAP |\n| `134` | SFX_CROW_DEATH |\n| `135` | SFX_CROW_ATTACK |\n| `136` | SFX_ENEMY_GUN_COCKING |\n| `137` | SFX_ENEMY_FIRE_1 |\n| `138` | SFX_ENEMY_FIRE_TWIRL |\n| `139` | SFX_ENEMY_HOLSTER |\n| `140` | SFX_ENEMY_BREATH_1 |\n| `141` | SFX_ENEMY_CHUCKLE |\n| `142` | SFX_MONK_POY |\n| `143` | SFX_MONK_DEATH |\n| `145` | SFX_LARA_SPIKE_DEATH |\n| `146` | SFX_LARA_DEATH_3 |\n| `147` | SFX_ROLLING_BALL |\n| `148` | SFX_SANDBAG_SNAP |\n| `149` | SFX_SANDBAG_HIT |\n| `150` | SFX_LOOP_FOR_SMALL_FIRES |\n| `152` | SFX_SKIDOO_START |\n| `153` | SFX_SKIDOO_IDLE |\n| `154` | SFX_SKIDOO_ACCELERATE |\n| `155` | SFX_SKIDOO_MOVING |\n| `156` | SFX_SKIDOO_STOP |\n| `157` | SFX_ENEMY_FIRE_2 |\n| `158` | SFX_ENEMY_DEATH_2 |\n| `159` | SFX_ENEMY_BREATH_2 |\n| `160` | SFX_STICK_TAP |\n| `161` | SFX_TRAPDOOR_OPEN |\n| `162` | SFX_TRAPDOOR_CLOSE |\n| `163` | SFX_YETI_GROWL |\n| `164` | SFX_YETI_CHEST_BEAT |\n| `165` | SFX_YETI_THUMP |\n| `166` | SFX_YETI_GRUNT_1 |\n| `167` | SFX_YETI_SCREAM |\n| `168` | SFX_YETI_DEATH |\n| `169` | SFX_YETI_GROWL_1 |\n| `170` | SFX_YETI_GROWL_2 |\n| `171` | SFX_YETI_GRUNT_2 |\n| `172` | SFX_YETI_GROWL_3 |\n| `173` | SFX_YETI_FEET |\n| `174` | SFX_ENEMY_HEAVY_BREATH |\n| `175` | SFX_ENEMY_FLAMETHROWER_FIRE |\n| `176` | SFX_ENEMY_FLAMETHROWER_SCRAPE |\n| `177` | SFX_ENEMY_FLAMETHROWER_CLICK |\n| `178` | SFX_ENEMY_FLAMETHROWER_DEATH |\n| `179` | SFX_ENEMY_FLAMETHROWER_FALL |\n| `180` | SFX_ENEMY_BELT_JINGLE |\n| `181` | SFX_ENEMY_WRENCH |\n| `182` | SFX_FOOTSTEP |\n| `183` | SFX_FOOTSTEP_HIT |\n| `184` | SFX_ENEMY_COCKING_SHOTGUN |\n| `186` | SFX_SCUBA_DIVER_FLIPPER |\n| `188` | SFX_SCUBA_DIVER_BREATH |\n| `190` | SFX_PULLEY_CRANE |\n| `191` | SFX_CURTAIN |\n| `192` | SFX_SCUBA_DIVER_DEATH |\n| `193` | SFX_SCUBA_DIVER_DIVING |\n| `194` | SFX_BOAT_START |\n| `195` | SFX_BOAT_IDLE |\n| `196` | SFX_BOAT_ACCELERATE |\n| `197` | SFX_BOAT_MOVING |\n| `198` | SFX_BOAT_STOP |\n| `199` | SFX_BOAT_SLOW_DOWN |\n| `200` | SFX_BOAT_HIT |\n| `201` | SFX_CLATTER_1 |\n| `202` | SFX_CLATTER_2 |\n| `203` | SFX_CLATTER_3 |\n| `204` | SFX_SPIKE_WALL |\n| `205` | SFX_LARA_FLESH_WOUND |\n| `206` | SFX_SAW_REVVING |\n| `207` | SFX_SAW_STOP |\n| `208` | SFX_DOOR_CHIME |\n| `209` | SFX_CHAIN_CREAK_SNAP |\n| `210` | SFX_SWINGING |\n| `211` | SFX_BREAKING_1 |\n| `212` | SFX_PULLEY_MOVE |\n| `213` | SFX_AIRPLANE_IDLE |\n| `215` | SFX_UNDERWATER_FAN_ON |\n| `217` | SFX_SMALL_FAN_ON |\n| `218` | SFX_SWINGING_BOX_BAG |\n| `219` | SFX_JUMP_PAD_UP |\n| `220` | SFX_JUMP_PAD_DOWN |\n| `221` | SFX_BREAKING_2 |\n| `222` | SFX_ROLLING_BALL_2_ROLL |\n| `223` | SFX_ROLLING_BALL_2_STOP |\n| `224` | SFX_ROLLING |\n| `225` | SFX_ROLLING_STOP_1 |\n| `226` | SFX_ROLLING_STOP_2 |\n| `227` | SFX_ROLLING_BALL_3_ROLL |\n| `228` | SFX_ROLLING_BALL_3_STOP\n| `229` | SFX_SIDE_BLADE_SWING |\n| `230` | SFX_SIDE_BLADE_BACK |\n| `231` | SFX_ROLLING_BLADE |\n| `232` | SFX_ICILE_DETACH |\n| `233` | SFX_ICICLE_HIT |\n| `234` | SFX_ROTATING_HANDLE_LOOSE |\n| `235` | SFX_ROTATING_HANDLE_TURN |\n| `236` | SFX_ROTATING_HANDLE_OPEN |\n| `237` | SFX_ROTATING_HANDLE_CREAK |\n| `238` | SFX_MONK_FEET |\n| `239` | SFX_MONK_SWORD_SWING_1 |\n| `240` | SFX_MONK_SWORD_SWING_2 |\n| `241` | SFX_MONK_SHOUT_1 |\n| `242` | SFX_MONK_SHOUT_2 |\n| `243` | SFX_MONK_SHOUT_3 |\n| `244` | SFX_MONK_SHOUT_4 |\n| `245` | SFX_MONK_CRUNCH |\n| `246` | SFX_MONK_BREATH |\n| `247` | SFX_SPLASH_SURFACE |\n| `248` | SFX_WATERFALL_1 |\n| `249` | SFX_ENEMY_FEET_SNOW |\n| `250` | SFX_ENEMY_FIRE_3 |\n| `251` | SFX_ENEMY_FIRE_SEMIAUTO |\n| `252` | SFX_ENEMY_DEATH_3 |\n| `253` | SFX_ENEMY_DEATH_4 |\n| `254` | SFX_DISC |\n| `255` | SFX_KNIFETHROWER_FEET |\n| `256` | SFX_MONK_OYE |\n| `257` | SFX_MONK_AWEH |\n| `258` | SFX_PROJECTILE_HIT |\n| `259` | SFX_KNIFETHROWER_WARRIOR_FEET |\n| `260` | SFX_WARRIOR_BLADE_SWING_1 |\n| `261` | SFX_WARRIOR_BLADE_SWING_2 |\n| `262` | SFX_WARRIOR_GROWL |\n| `263` | SFX_KNIFETHROWER_HICCUP |\n| `264` | SFX_WARROPR_BURP |\n| `265` | SFX_WARRIOR_GROWL_1 |\n| `267` | SFX_WARRIOR_WAKE |\n| `268` | SFX_WARRIOR_GROWL_2 |\n| `269` | SFX_SMALL_SWITCH |\n| `278` | SFX_CHAIN_PULLEY |\n| `279` | SFX_ZIPLINE_GRAB |\n| `280` | SFX_ZIPLINE_GO |\n| `281` | SFX_ZIPLINE_STOP |\n| `282` | SFX_BODY_SLUMP |\n| `283` | SFX_BOWL_TIPPING |\n| `284` | SFX_BOWL_POUR |\n| `285` | SFX_WATERFALL_2 |\n| `286` | SFX_ELEVATOR_OPEN |\n| `287` | SFX_ELEVATOR_CLOSE |\n| `288` | SFX_MINISUB_CLATTER_1 |\n| `289` | SFX_MINISUB_CLATTER_2 |\n| `290` | SFX_MINISUB_CLATTER_3 |\n| `291` | SFX_BIRD_MONSTER_SCREAM |\n| `292` | SFX_BIRD_MONSTER_GASP |\n| `293` | SFX_BIRD_MONSTER_BREATH |\n| `294` | SFX_BIRD_MONSTER_FEET |\n| `295` | SFX_BIRD_MONSTER_DEATH |\n| `296` | SFX_BIRD_MONSTER_SCRAPE |\n| `297` | SFX_HELICOPTER_LOOP |\n| `298` | SFX_DRAGON_FEET |\n| `298` | SFX_EARTHQUAKE_1 |\n| `299` | SFX_DRAGON_GROWL_1 |\n| `300` | SFX_DRAGON_GROWL_2 |\n| `301` | SFX_DRAGON_FALL |\n| `302` | SFX_DRAGON_BREATH |\n| `303` | SFX_DRAGON_GROWL_3 |\n| `304` | SFX_DRAGON_GRUNT |\n| `305` | SFX_DRAGON_FIRE |\n| `306` | SFX_DRAGON_LEG_LIFT |\n| `307` | SFX_DRAGON_LEG_HIT |\n| `308` | SFX_WARRIOR_BLADE_SWING_3 |\n| `309` | SFX_WARRIOR_BLADE_SWING_FAST |\n| `311` | SFX_WARRIOR_BREATH_ACTIVE |\n| `312` | SFX_WARRIOR_HOVER |\n| `313` | SFX_WARRIOR_LANDING |\n| `314` | SFX_WARRIOR_SWORD_CLANK |\n| `315` | SFX_WARRIOR_SWORD_SLICE |\n| `316` | SFX_BIRDS_CHIRP |\n| `317` | SFX_CRUNCH_1 |\n| `318` | SFX_CRUNCH_2 |\n| `319` | SFX_DOOR_CREAK |\n| `320` | SFX_BREAKING_3 |\n| `321` | SFX_BIG_SPIDER_SNARL |\n| `322` | SFX_BIG_SPIDER_FEET |\n| `323` | SFX_BIG_SPIDER_DEATH |\n| `324` | SFX_T_REX_ROAR |\n| `325` | SFX_T_REX_STOMP |\n| `325` | SFX_PUSHBLOCK_LAND |\n| `325` | SFX_EARTHQUAKE_2 |\n| `326` | SFX_T_REX_GROWL_1 |\n| `327` | SFX_T_REX_DEATH |\n| `329` | SFX_DRIPS_REVERB |\n| `330` | SFX_STAGE_BACKDROP |\n| `331` | SFX_STONE_DOOR_SLIDE |\n| `332` | SFX_PLATFORM_ALARM |\n| `333` | SFX_TICK_TOCK |\n| `334` | SFX_DOORBELL |\n| `335` | SFX_BURGLAR_ALARM |\n| `336` | SFX_BOAT_ENGINE |\n| `337` | SFX_BOAT_INTO_WATER |\n| `338` | SFX_UNKNOWN_1 |\n| `339` | SFX_UNKNOWN_2 |\n| `340` | SFX_UNKNOWN_3 |\n| `341` | SFX_MARCO_BARTOLLI_TRANSFORM |\n| `342` | SFX_WINSTON_SHUFFLE |\n| `343` | SFX_WINSTON_FEET |\n| `344` | SFX_WINSTON_GRUNT_1 |\n| `345` | SFX_WINSTON_GRUNT_2 |\n| `346` | SFX_WINSTON_GRUNT_3 |\n| `347` | SFX_WINSTON_CUPS |\n| `348` | SFX_BRITTLE_GROUND_BREAK |\n| `349` | SFX_SPIDER_EXPLODE |\n| `350` | SFX_SHARK_BITE |\n| `351` | SFX_LAVA_BUBBLES |\n| `352` | SFX_EXPLOSION_2 |\n| `353` | SFX_BURGLARS |\n| `354` | SFX_ZIPPER |\n"
  },
  {
    "path": "docs/trx/SUPPORT.md",
    "content": "---\ntitle: Getting support\norder: 12\n---\n\n# Getting support\n\nFor bugs, crashes, and feature requests, please use [GitHub issues](https://github.com/LostArtefacts/TRX/issues).\n\nBefore opening a bug report, read the [bug reporting guide](https://github.com/LostArtefacts/TRX/blob/develop/BUG_REPORTING.md).\n"
  },
  {
    "path": "docs/trx/WATER_COLORS.md",
    "content": "---\ntitle: Water colors\norder: 8\n---\n\n# Water colors\n\n<h4 colspan=\"4\">TR1</h4>\n<table>\n<thead>\n    <tr>\n        <th>Platform</th>\n        <th>Color</th>\n        <th>Color&nbsp;(array)</th>\n        <th>Usage</th>\n    </tr>\n</thead>\n<tbody>\n    <tr>\n        <td rowspan=\"1\">DOS</td>\n        <td><img src=\"https://dummyimage.com/20x20/99b2ff/99b2ff.png\" width=\"20\" height=\"20\" alt=\"#99B2FF\" valign=\"middle\"/> <code>#99B2FF</code></td>\n        <td><code>[0.6, 0.698, 1]</code></td>\n        <td>original DOS version color</td>\n    </tr>\n    <tr>\n        <td rowspan=\"1\">PC</td>\n        <td><img src=\"https://dummyimage.com/20x20/72ffff/72ffff.png\" width=\"20\" height=\"20\" alt=\"#72FFFF\" valign=\"middle\"/> <code>#72FFFF</code></td>\n        <td><code>[0.447, 1, 1]</code></td>\n        <td>default TombATI color</td>\n    </tr>\n</tbody>\n</table>\n\n<h4 colspan=\"4\">TR2</h4>\n<table>\n<thead>\n    <tr>\n        <th>Platform</th>\n        <th>Color</th>\n        <th>Color&nbsp;(array)</th>\n        <th>Usage</th>\n    </tr>\n</thead>\n<tbody>\n    <tr>\n        <td rowspan=\"2\">PC</td>\n        <td><img src=\"https://dummyimage.com/20x20/80e0ff/80e0ff.png\" width=\"20\" height=\"20\" alt=\"#80E0FF\" valign=\"middle\"/> <code>#80E0FF</code></td>\n        <td><code>[0.502, 0.878, 1]</code></td>\n        <td>default PC hardware renderer color</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/aaaaff/aaaaff.png\" width=\"20\" height=\"20\" alt=\"#AAAAFF\" valign=\"middle\"/> <code>#AAAAFF</code></td>\n        <td><code>[0.667, 0.667, 1]</code></td>\n        <td>default PC software renderer color</td>\n    </tr>\n    <tr>\n        <td rowspan=\"19\">PS1</td>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Lara's Home</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/b2e5e5/b2e5e5.png\" width=\"20\" height=\"20\" alt=\"#B2E5E5\" valign=\"middle\"/> <code>#B2E5E5</code></td>\n        <td><code>[0.698, 0.898, 0.898]</code></td>\n        <td>The Great Wall</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Venice</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Bartoli's Hideout</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Opera House</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Offshore Rig</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Diving Area</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>40 Fathoms</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Wreck of the Maria Doria</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Living Quarters</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>The Deck</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/b2e5e5/b2e5e5.png\" width=\"20\" height=\"20\" alt=\"#B2E5E5\" valign=\"middle\"/> <code>#B2E5E5</code></td>\n        <td><code>[0.698, 0.898, 0.898]</code></td>\n        <td>Tibetan Foothills</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Barkhang Monastery</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Catacombs of the Talion</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Ice Palace</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff99/ccff99.png\" width=\"20\" height=\"20\" alt=\"#CCFF99\" valign=\"middle\"/> <code>#CCFF99</code></td>\n        <td><code>[0.8, 1, 0.6]</code></td>\n        <td>Temple of Xian</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccffcc/ccffcc.png\" width=\"20\" height=\"20\" alt=\"#CCFFCC\" valign=\"middle\"/> <code>#CCFFCC</code></td>\n        <td><code>[0.8, 1, 0.8]</code></td>\n        <td>Floating Islands</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccffcc/ccffcc.png\" width=\"20\" height=\"20\" alt=\"#CCFFCC\" valign=\"middle\"/> <code>#CCFFCC</code></td>\n        <td><code>[0.8, 1, 0.8]</code></td>\n        <td>Dragon's Lair</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Home Sweet Home</td>\n    </tr>\n</tbody>\n</table>\n\n<h4 colspan=\"4\">TR3</h4>\n<table>\n<thead>\n    <tr>\n        <th>Platform</th>\n        <th>Color</th>\n        <th>Color&nbsp;(array)</th>\n        <th>Usage</th>\n    </tr>\n</thead>\n<tbody>\n    <tr>\n        <td rowspan=\"1\">PC</td>\n        <td><img src=\"https://dummyimage.com/20x20/80e0ff/80e0ff.png\" width=\"20\" height=\"20\" alt=\"#80E0FF\" valign=\"middle\"/> <code>#80E0FF</code></td>\n        <td><code>[0.502, 0.878, 1]</code></td>\n        <td>default PC renderer color</td>\n    </tr>\n    <tr>\n        <td rowspan=\"21\">PS1</td>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Lara's Home</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Jungle</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Temple Ruins</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>The River Ganges</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Caves of Kaliya</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Coastal Village</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ffffff/ffffff.png\" width=\"20\" height=\"20\" alt=\"#FFFFFF\" valign=\"middle\"/> <code>#FFFFFF</code></td>\n        <td><code>[1, 1, 1]</code></td>\n        <td>Crash Site</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ffffff/ffffff.png\" width=\"20\" height=\"20\" alt=\"#FFFFFF\" valign=\"middle\"/> <code>#FFFFFF</code></td>\n        <td><code>[1, 1, 1]</code></td>\n        <td>Madubu Gorge</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80e0ff/80e0ff.png\" width=\"20\" height=\"20\" alt=\"#80E0FF\" valign=\"middle\"/> <code>#80E0FF</code></td>\n        <td><code>[0.502, 0.878, 1]</code></td>\n        <td>Temple of Puna (no water)</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ffffff/ffffff.png\" width=\"20\" height=\"20\" alt=\"#FFFFFF\" valign=\"middle\"/> <code>#FFFFFF</code></td>\n        <td><code>[1, 1, 1]</code></td>\n        <td>Thames Wharf</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Aldwych</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>Lud's Gate</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccff80/ccff80.png\" width=\"20\" height=\"20\" alt=\"#CCFF80\" valign=\"middle\"/> <code>#CCFF80</code></td>\n        <td><code>[0.8, 1, 0.502]</code></td>\n        <td>City</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ffffff/ffffff.png\" width=\"20\" height=\"20\" alt=\"#FFFFFF\" valign=\"middle\"/> <code>#FFFFFF</code></td>\n        <td><code>[1, 1, 1]</code></td>\n        <td>Nevada Desert</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ffffff/ffffff.png\" width=\"20\" height=\"20\" alt=\"#FFFFFF\" valign=\"middle\"/> <code>#FFFFFF</code></td>\n        <td><code>[1, 1, 1]</code></td>\n        <td>High Security Compound</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ffffff/ffffff.png\" width=\"20\" height=\"20\" alt=\"#FFFFFF\" valign=\"middle\"/> <code>#FFFFFF</code></td>\n        <td><code>[1, 1, 1]</code></td>\n        <td>Area 51</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80ffff/80ffff.png\" width=\"20\" height=\"20\" alt=\"#80FFFF\" valign=\"middle\"/> <code>#80FFFF</code></td>\n        <td><code>[0.502, 1, 1]</code></td>\n        <td>Antarctica</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/ccffcc/ccffcc.png\" width=\"20\" height=\"20\" alt=\"#CCFFCC\" valign=\"middle\"/> <code>#CCFFCC</code></td>\n        <td><code>[0.8, 1, 0.8]</code></td>\n        <td>RX-Tech Mines</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80e0ff/80e0ff.png\" width=\"20\" height=\"20\" alt=\"#80E0FF\" valign=\"middle\"/> <code>#80E0FF</code></td>\n        <td><code>[0.502, 0.878, 1]</code></td>\n        <td>Lost City of Tinnos</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/80e0ff/80e0ff.png\" width=\"20\" height=\"20\" alt=\"#80E0FF\" valign=\"middle\"/> <code>#80E0FF</code></td>\n        <td><code>[0.502, 0.878, 1]</code></td>\n        <td>Meteorite Cavern (no water)</td>\n    </tr>\n    <tr>\n        <td><img src=\"https://dummyimage.com/20x20/b2e6e6/b2e6e6.png\" width=\"20\" height=\"20\" alt=\"#B2E6E6\" valign=\"middle\"/> <code>#B2E6E6</code></td>\n        <td><code>[0.698, 0.902, 0.902]</code></td>\n        <td>All Hallows</td>\n    </tr>\n</tbody>\n</table>\n"
  },
  {
    "path": "docs/trx/WEAPONS.md",
    "content": "---\ntitle: Weapons\norder: 13\n---\n\n# Weapons\n\nLara has a fixed number of weapons as follows. \n\n- Pistols\n- Magnums / Automatic Pistols\n- Uzis\n- Shotgun\n- M16\n- Grenade Launcher\n- Harpoon Gun\n- Flare (not strictly a weapon, but treated similarly by the engine)\n- Black Skidoo\n\nThe file `cfg/weapons.json5` contains properties for these weapon types, each\ndescribed in the table below.\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>aim_speed</code></td>\n    <td>Integer</td>\n    <td>Determines how quickly Lara's arms rotate into position when aiming at a target.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>damage</code></td>\n    <td>Integer</td>\n    <td>The HP damage value to subtract from targets when struck by this weapon type. This value is doubled when playing either <code>Japanese</code> or <code>Japanese NG+</code> modes.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>draw_frame</code></td>\n    <td>Integer</td>\n    <td>For rifle type weapons, the relative frame number of the equip animation where the object mesh swap is performed e.g. removing the shotgun from Lara's back and putting it in her hand.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>equip_anim_idx</code></td>\n    <td>Integer</td>\n    <td>For rifle type weapons, the relative equip animation index of the associated object e.g. <code>O_LARA_SHOTGUN</code>.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>flash_pos</code> / <code>flash_pos_alt</code></td>\n    <td>XYZ</td>\n    <td>Specifies the offset position where the weapon flash object (<code>O_GUN_FLASH</code> / <code>O_M16_FLASH</code> / <code>O_FLARE_FIRE</code>) will be drawn. <code>flash_pos_alt</code> is used only for discarded flares.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>flash_shade</code></td>\n    <td>Integer</td>\n    <td>Specifies the shade applied when drawing the weapon flash object (<code>O_GUN_FLASH</code> / <code>O_M16_FLASH</code> / <code>O_FLARE_FIRE</code>).</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>flash_color</code></td>\n    <td>Float array (length 3)</td>\n    <td>Specifies the color applied when drawing the weapon flash object (<code>O_GUN_FLASH</code> / <code>O_M16_FLASH</code> / <code>O_FLARE_FIRE</code>), used in TR3 lighting system.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>glow_color</code></td>\n    <td>Float array (length 3)</td>\n    <td>Specifies the color applied when drawing the weapon glow object (<code>O_GLOW</code>), used in TR3 lighting system.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>glow_pos</code></td>\n    <td>XYZ</td>\n    <td>Specifies the additional offset to apply to the glow sprite position.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>flash_time</code></td>\n    <td>Integer</td>\n    <td>Determines the number of frames to show the weapon flash object (<code>O_GUN_FLASH</code> / <code>O_M16_FLASH</code>) after firing a weapon.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>muzzle_pos</code></td>\n    <td>XYZ</td>\n    <td>Specifies the additional offset to apply to the muzzle for smoke effects (right hand).</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>muzzle_pos_alt</code></td>\n    <td>XYZ</td>\n    <td>Specifies the additional offset to apply to the muzzle for smoke effects (left hand for dual pistols).</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>smoke_count</code></td>\n    <td>Integer</td>\n    <td>How many smoke effect instances to spawn upon shooting.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>shell_pos</code></td>\n    <td>XYZ</td>\n    <td>Specifies the additional offset to apply to the gun for shells (right hand).</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>shell_pos_alt</code></td>\n    <td>XYZ</td>\n    <td>Specifies the additional offset to apply to the gun for shells (left hand for dual pistols).</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>gun_height</code></td>\n    <td>Integer</td>\n    <td>Used to determine the start Y position when firing a weapon, and to determine if Lara is too far submerged in water to be able to use a weapon (other than the harpoon).</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>is_available</code></td>\n    <td>Boolean</td>\n    <td>Determines if a weapon can be given to Lara when using item cheats. Pickups for unavailable weapons/flares will still work normally.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>left_angles</code></td>\n    <td>Integer array (length 4)</td>\n    <td>These values determine if Lara has lost target on her left arm.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>lock_angles</code></td>\n    <td>Integer array (length 4)</td>\n    <td>These values are used to test if Lara is able to lock on to a target.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>ammo</code></td>\n    <td>Object</td>\n    <td>Configures how much ammo a weapon gives when acquired and when its matching ammo pickup is collected.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>ammo.initial_qty</code></td>\n    <td>Integer</td>\n    <td>The amount of ammo given when the weapon itself is collected.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>ammo.pickup_qty</code></td>\n    <td>Integer</td>\n    <td>The amount of ammo given when the equivalent ammo object is picked up.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>ammo.pickup_qty_alt</code></td>\n    <td>Integer</td>\n    <td>As per <code>ammo.pickup_qty</code>, but this applies exclusively to flares when playing Japanese NG.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>recoil_frame</code></td>\n    <td>Integer</td>\n    <td>For pistol type weapons, this value determines when Lara should snap back to the aiming frame after the weapon is fired i.e. Uzis have a lower value than Pistols for faster fire rate.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>right_angles</code></td>\n    <td>Integer array (length 4)</td>\n    <td>These values determine if Lara has lost target on her right arm.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>sample_num</code></td>\n    <td>String</td>\n    <td>The sound effect to play when the weapon is fired (see ./SAMPLES.md).</td>\n  </tr> \n  <tr valign=\"top\">\n    <td><code>shot_accuracy</code></td>\n    <td>Integer</td>\n    <td>Adds a random factor to angles used when firing a weapon. Higher values mean less accuracy.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>target_dist</code></td>\n    <td>Float</td>\n    <td>The maximum distance (in world sectors) that a target can be from Lara in order for her to lock on.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>type</code></td>\n    <td>String</td>\n    <td>\n      The category that determines how the gun is handled. Accepted values are as follows.\n      <ul>\n        <li><code>WEAPON_TYPE_DUAL_PISTOLS</code></li>\n        <li><code>WEAPON_TYPE_SINGLE_PISTOL</code></li>\n        <li><code>WEAPON_TYPE_RIFLE</code></li>\n        <li><code>WEAPON_TYPE_MOUNTED</code></li>\n      </ul>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>undraw_frame</code></td>\n    <td>Integer</td>\n    <td>For rifle type weapons, the relative frame number of the unequip animation where the object mesh swap is performed e.g. removing the shotgun from Lara's hand and putting it on her back.</td>\n  </tr>\n</table>\n"
  },
  {
    "path": "docs/trx/game_flow/COMMANDS.md",
    "content": "---\ntitle: Commands\norder: 3\n---\n\n## Game flow commands\n\nThe command allows you to modify the original game flow, but please note that\ndeviations from the original script may result in unexpected behavior. If you\nencounter any bugs, we encourage you to report your experience by opening an\nissue on GitHub. The overall structure is as follows:\n\n```json5\n{\n  \"command\": \"play_level\",\n  \"param\": 5,\n}\n```\n\nCurrently the following commands are available.\n\n<table>\n  <tr>\n    <th>Command</th>\n    <th>Description</th>\n    <th>Parameter</th>\n  </tr>\n  <tr>\n    <td><code>noop</code></td>\n    <td>Continue the flow as normal.</td>\n    <td>N/A</td>\n  </tr>\n  <tr>\n    <td><code>play_level</code></td>\n    <td>Play a specific level.</td>\n    <td>Level to play.</td>\n  </tr>\n  <tr>\n    <td><code>load_saved_game</code></td>\n    <td>Load a specific savegame.</td>\n    <td>Save slot number to use</td>\n  </tr>\n  <tr>\n    <td><code>play_cutscene</code></td>\n    <td>Play a specific cutscene.</td>\n    <td>Cutscene number to play</td>\n  </tr>\n  <tr>\n    <td><code>play_demo</code></td>\n    <td>Play a specific demo.</td>\n    <td>Demo number to play.</td>\n  </tr>\n  <tr>\n    <td><code>play_fmv</code></td>\n    <td>Play a specific movie.</td>\n    <td>Movie number to play.</td>\n  </tr>\n  <tr>\n    <td><code>exit_to_title</code></td>\n    <td>Return the game to the title screen.</td>\n    <td>N/A</td>\n  </tr>\n  <tr>\n    <td><code>level_complete</code></td>\n    <td>\n      End the current sequence inside level sequences, do nothing otherwise.\n    </td>\n    <td>N/A</td>\n  </tr>\n  <tr>\n    <td><code>exit_game</code></td>\n    <td>Exit the game to desktop.</td>\n    <td>N/A</td>\n  </tr>\n  <tr>\n    <td><code>select_level</code></td>\n    <td>Play a specific level (and reset inventory).</td>\n    <td>Level number to play.</td>\n  </tr>\n  <tr>\n    <td><code>restart_level</code>¹</td>\n    <td>Restart the currently played level.</td>\n    <td>N/A</td>\n  </tr>\n  <tr>\n    <td><code>story_so_far</code>¹</td>\n    <td>Play the movies and cutscenes up until the currently played level.</td>\n    <td>Save slot number to use</td>\n  </tr>\n</table>\n\n**¹** Tomb Raider 1 only.\n\nAdditional notes:\n- All numbers (levels, cutscenes, ...) start with 0.\n"
  },
  {
    "path": "docs/trx/game_flow/GLOBAL_PROPERTIES.md",
    "content": "---\ntitle: Global properties\norder: 0\n---\n\n## Global properties\nThe following properties are in the root of the game flow document and control\nvarious pieces of global behaviour. Currently, the majority of this section\nremains distinct for each game.\n\n### TR1\n\n#### Example structure\n\n<details>\n<summary>Show snippet</summary>\n\n```json5\n{\n    \"main_menu_picture\": \"data/titleh.png\",\n    \"savegame_file_fmt\": \"save_tr1_%02d.dat\",\n    \"water_color\": [0.45, 1.0, 1.0],\n    \"fog_transparency\": false,\n    \"fog_color\": [0.0, 0.0, 0.0],\n    \"fog_start\": 22.0,\n    \"fog_end\": 30.0,\n    \"ambient_tracks\": [57, 58, 59, 60],\n    \"injections\": [\n        \"data/global_injection1.bin\",\n        \"data/global_injection2.bin\",\n        // etc\n    ],\n    \"convert_dropped_guns\": false,\n    \"enforced_config\": {\n        \"enable_save_crystals\": false,\n    },\n    \"hidden_config\": [\n        \"enable_legal\",\n    ],\n    // Optional global Lua script file\n    \"main_script\": \"data/scripts/global.lua\",\n    \"levels\": [\n        {\n            \"path\": \"data/gym.phd\",\n            // etc\n        },\n    ],\n    \"cutscenes\": [\n        {\n            \"path\": \"data/cut1.phd\",\n            // etc\n        },\n    ],\n    \"demos\": [\n        {\n            \"path\": \"data/gym.phd\",\n            // etc\n        },\n    ],\n    \"fmvs\": [\n        {\"path\": \"data/snow.rpl\"},\n        // etc\n    },\n}\n```\n\n</details>\n\n#### Reference\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"cold-water\"></a>\n      <code>cold_water</code>\n    </td>\n    <td>Boolean</td>\n    <td>\n      Enables an exposure meter for Lara when she is in cold water.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"convert-dropped-guns\"></a>\n      <code>convert_dropped_guns</code>\n    </td>\n    <td>Boolean</td>\n    <td>\n      Forces guns dropped by enemies to be converted to the equivalent ammo\n      if Lara already has the gun. See\n      <a href=\"./levels/ITEM_DROPS.md\">Item drops</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"extends\"></a>\n      <code>extends</code>\n    </td>\n    <td>String</td>\n    <td>\n      Directory name of the base mod this mod extends. Used for asset fallback\n      and engine version resolution. Required for custom mods to appear in the\n      Switch Game menu.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"fog-transparency\"></a>\n      <code>fog_transparency</code>\n    </td>\n    <td>Boolean</td>\n    <td>\n      Enables blending distant geometry into skybox rather than a solid color.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"fog-color\"></a>\n      <code>fog_color</code>\n    </td>\n    <td>Float array or hex string</td>\n    <td>\n      Fog color (R, G, B) or `#RRGGBB`. OG uses `#000000`. Will have no effect\n      if `fog_transparency` is set to true.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"draw-distance-fade\"></a>\n      <code>fog_start</code>\n    </td>\n    <td>Double</td>\n    <td>\n      The distance (in tiles) at which objects and the world start to fade into\n      blackness.\n      <ul>\n        <li>The default value in OG TR1 is hardcoded to 12.</li>\n        <li>The default (disabled) value in TombATI is 72.</li>\n      </ul>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"draw-distance-max\"></a>\n      <code>fog_end</code>\n    </td>\n    <td>Double</td>\n    <td>\n      The distance (in tiles) at which objects and the world are clipped away.\n      <ul>\n        <li>The default value in OG TR1 is hardcoded to 20.</li>\n        <li>The default (disabled) value in TombATI is 80.</li>\n      </ul>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"enable-tr2-item-drops\"></a>\n      <code>enable_tr2_item_drops</code>\n    </td>\n    <td>Boolean</td>\n    <td>\n      Forces enemies who are placed in the same position as pickup items to\n      carry those items and drop them when killed (OG TR2+ behavior). See\n      <a href=\"./levels/ITEM_DROPS.md\">Item drops</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><a name=\"enforced-config\"></a>\n    <code>enforced_config</code></td>\n    <td>String-to-object map</td>\n    <td>\n      This allows <em>any</em> regular game config setting to be overriden. See\n      <a href=\"./USER_CONFIGURATION.md\">User configuration</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><a name=\"hidden-config\"></a>\n    <code>hidden_config</code></td>\n    <td>String array</td>\n    <td>\n      This allows <em>any</em> regular game config setting to be hidden from\n      the ingame settings dialogs. See <a href=\"./USER_CONFIGURATION.md\">User\n      configuration</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>injections</code></td>\n    <td>String array</td>\n    <td>\n      Global data injection file paths. Individual levels will inherit these\n      unless <code>inherit_injections</code> is set to <code>false</code> on\n      those levels. See <a href=\"../INJECTIONS.md\">Injections</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"globe-select-entries\"></a>\n      <code>globe_select_entries</code>\n    </td>\n    <td>Object array</td>\n    <td>\n      Defines up to 6 selectable destinations for the <code>globe_select</code>\n      sequence. Each entry is an object with the following keys:\n      <ul>\n        <li>\n          <code>rot</code> (integer array, length 3): target rotation\n          (<code>[x, y, z]</code>) in engine angle units.\n        </li>\n        <li>\n          <code>start_level_ordinal</code> (integer): ordinal number of the\n          first level for this destination within the main level table.\n        </li>\n        <li>\n          <code>completion_level_ordinal</code> (integer): ordinal number of a\n          level that, once completed, marks this destination as completed.\n        </li>\n        <li>\n          <code>prereq_zones</code> (integer array): a list of required\n          destination indices to unlock this area (e.g. <code>[0, 1, 2, 4]</code>).\n        </li>\n        <li>\n          <code>mesh_idx</code> (integer): globe mesh index used to represent\n          the destination (for rotation/selection and hiding unavailable meshes).\n        </li>\n      </ul>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>levels</code></td>\n    <td>Object array<strong>*</strong></td>\n    <td>\n      This is where the individual level details are defined - see\n      <a href=\"./levels/REGULAR_LEVELS.md\">Level properties</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>main_script</code></td>\n    <td>String</td>\n    <td>\n      Path to a global Lua script to execute after game initialization, before\n      the first level loads.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"name\"></a>\n      <code>name</code>\n    </td>\n    <td>String</td>\n    <td>\n      Human-readable display name for this mod, shown in the Switch Game menu.\n      If not set, the directory name is used as a fallback.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>main_menu_picture</code></td>\n    <td>String<strong>*</strong></td>\n    <td>Path to the main menu background image.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>savegame_file_fmt</code></td>\n    <td>String<strong>*</strong></td>\n    <td>Path pattern to look for the savegame files.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"water-color\"></a>\n      <code>water_color</code>\n    </td>\n    <td>Float array or hex string</td>\n    <td>\n      Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0\n      or `00` means completely black color.\n      See <a href=\"../7-WATER_COLORS.md\">this table</a> for reference values.</a>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"ambient-tracks\"></a>\n      <code>ambient_tracks</code>\n    </td>\n    <td>Integer array</td>\n    <td>\n      A list of music track IDs, which will be treated as ambient music. If\n      Lara crosses a trigger for any of these, it will become the current looped\n      track, and will persist on save/load.\n    </td>\n  </tr>\n</table>\n\n**\\*** Required property.\n\n### TR2\n\n#### Example structure\n\n<details>\n<summary>Show snippet</summary>\n\n```json5\n{\n    // NOTE: bad changes to this file may result in crashes.\n    // Lines starting with double slashes are comments and are ignored.\n\n    \"main_menu_picture\": \"data/images/title_eu.webp\",\n    \"savegame_file_fmt\": \"save_tr2_%02d.dat\",\n\n    \"demo_version\": false,\n\n    \"title\": {\n        \"path\": \"data/title.tr2\",\n        \"music_track\": 60,\n        \"sequence\": [\n            {\"type\": \"display_picture\", \"path\": \"data/images/legal_eu.webp\", \"legal\": true},\n            {\"type\": \"play_fmv\", \"fmv_id\": 0},\n            {\"type\": \"play_fmv\", \"fmv_id\": 1},\n            {\"type\": \"exit_to_title\"},\n        ],\n    },\n\n    \"sfx_path\": \"main.sfx\",\n    \"injections\": [\n        \"data/injections/pda_model.bin\",\n        \"data/injections/winston_model.bin\",\n        \"data/injections/font.bin\",\n    ],\n\n    \"levels\": [\n        {\n            \"path\": \"data/gym.phd\",\n            // etc\n        },\n    ],\n\n    \"cutscenes\": [\n        {\n            \"path\": \"data/cut1.phd\",\n            // etc\n        },\n    ],\n\n    \"demos\": [\n        {\n            \"path\": \"data/gym.phd\",\n            // etc\n        },\n    ],\n\n    \"fmvs\": [\n        {\"path\": \"data/snow.rpl\"},\n        // etc\n    ],\n\n    \"enforced_config\": {\n        enable_3d_pickups\": false,\n    },\n    \"hidden_config\": [\n        \"enable_save_crystals\",\n    ],\n}\n```\n\n</details>\n\n#### Reference\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>demo_version</code></td>\n    <td>Boolean</td>\n    <td>Legacy setting scheduled for removal at a later time.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>main_menu_picture</code></td>\n    <td>String<strong>*</strong></td>\n    <td>Path to the main menu background image.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>savegame_file_fmt</code></td>\n    <td>String<strong>*</strong></td>\n    <td>Path pattern to look for the savegame files.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>sfx_path</code></td>\n    <td>String</td>\n    <td>\n      The path to the sound effects (.sfx) file to use in the game.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>globe_select_entries</code></td>\n    <td>Object array</td>\n    <td>\n      Defines up to 6 selectable destinations for the <code>globe_select</code>\n      sequence. See\n      <a href=\"./GLOBAL_PROPERTIES.md#globe-select-entries\">TR1 section</a>\n      for the full schema.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"fog-transparency\"></a>\n      <code>fog_transparency</code>\n    </td>\n    <td>Boolean</td>\n    <td>\n      Enables blending distant geometry into skybox rather than a solid color.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"fog-color\"></a>\n      <code>fog_color</code>\n    </td>\n    <td>Float array or hex string</td>\n    <td>\n      Fog color (R, G, B) or `#RRGGBB`. OG uses `#000000`. Will have no effect\n      if `fog_transparency` is set to true.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"draw-distance-fade\"></a>\n      <code>fog_start</code>\n    </td>\n    <td>Double</td>\n    <td>\n      The distance (in tiles) at which objects and the world start to fade into\n      blackness. The default value in OG TR2 is hardcoded to 12.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"draw-distance-max\"></a>\n      <code>fog_end</code>\n    </td>\n    <td>Double</td>\n    <td>\n      The distance (in tiles) at which objects and the world are clipped away.\n      The default value in OG TR2 is hardcoded to 20.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>water_color</code>\n    </td>\n    <td>Float array or hex string</td>\n    <td>\n      Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0\n      or `00` means completely black color.\n      See <a href=\"../7-WATER_COLORS.md\">this table</a> for reference values.</a>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>ambient_tracks</code>\n    </td>\n    <td>Integer array</td>\n    <td>\n      A list of music track IDs, which will be treated as ambient music. If\n      Lara crosses a trigger for any of these, it will become the current looped\n      track, and will persist on save/load.\n    </td>\n  </tr>\n</table>\n"
  },
  {
    "path": "docs/trx/game_flow/README.md",
    "content": "---\ntitle: Game flow\norder: 5\n---\n\n# Game flow specification\n\nIn the original Tomb Raider 1, the game flow was completely hard-coded,\nincluding the levels, limiting the builders' flexibility. Tomb Raider 2\nimproved upon this by introducing a tombpc.dat binary file for game\nconfiguration, although it remained cryptic and required builders to use\ndedicated tooling. TRX has transitioned away from these earlier methods,\nchoosing to manage game flow with a JSON file that provides a unified\nstructure for both games. This document details the elements that can be\nmodified using this updated format.\n"
  },
  {
    "path": "docs/trx/game_flow/SEQUENCES.md",
    "content": "---\ntitle: Sequences\norder: 2\n---\n\n## Sequences\n\nThe following describes each available game flow sequence type and the required\nparameters for each. Note that while this table is displayed in alphabetical\norder, care must be taken to define sequences in the correct order. Refer to the\ndefault game flow for examples.\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Sequence</th>\n    <th>Parameter</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>loop_game</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n    <td>Plays the main game loop.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>level_complete</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n    <td>Ends the current level and plays the next one, if available.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>exit_to_title</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n    <td>Returns to the title level.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>level_stats</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n    <td>\n        Displays the end of level statistics for the current level. In a Gym\n        level, this fades the screen to black.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>total_stats</code></td>\n    <td><code>path</code></td>\n    <td>String</td>\n    <td>\n      Displays the end of game statistics with the given picture file shown as\n      a background.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"2\"><code>globe_select</code></td>\n    <td><code>globe_select_entries</code></td>\n    <td><code>Object</code></td>\n    <td>\n      Ends the current level and opens the globe destination selector.\n      Available destinations are configured by the global\n      <code><a href=\"./GLOBAL_PROPERTIES.md#globe-select-entries\">globe_select_entries</a></code>.\n      You can make the globe selectable at game start. To do this, let the\n      first level contain only the `<code>globe_select</code>` directive, and\n      have its first area link to level 2.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>image</code></td>\n    <td>String</td>\n    <td>\n      Optional path to the background image. If omitted, a plain black background is used.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"4\">\n      <code>display_picture</code>\n    </td>\n    <td><code>path</code></td>\n    <td>String</td>\n    <td>\n      Displays the specified picture for a fixed time.\n      Files that are needed to function only with a specific aspect ratio can\n      be placed in a directory adjacent to the main image, named according to\n      the aspect ratio – for example, 4x3/title.png or 16x10/title.png. The\n      game won't attempt to match these precisely; instead, it will select the\n      file with the aspect ratio closest to the game's viewport. The main image\n      designated by <code>path</code> is presumed to have a 16:9 aspect ratio\n      for this purpose, and as such there's no need for 16x9-specific\n      directory.<br/>\n      This logic applies to all images.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>display_time</code></td>\n    <td>Double</td>\n    <td> Number of seconds to display the picture for (default: 5). </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fade_in_time</code></td>\n    <td>Double</td>\n    <td>\n      Number of seconds to do the fade-in animation, if enabled (default: 1).\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fade_out_time</code></td>\n    <td>Double</td>\n    <td>\n      Number of seconds to do the fade-out animation, if enabled (default: 0.33).\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"4\"><code>loading_screen</code></td>\n    <td><code>path</code></td>\n    <td>String</td>\n    <td rowspan=\"4\">\n      Shows a picture prior to loading a level. Functions identically to\n      <code>display_picture</code>, except these pictures can be\n      enabled/disabled by the user with the loading screen option in the config\n      tool.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>display_time</code></td>\n    <td>Double</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fade_in_time</code></td>\n    <td>Double</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fade_out_time</code></td>\n    <td>Double</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>play_cutscene</code></td>\n    <td><code>cutscene_id</code></td>\n    <td>Integer</td>\n    <td>\n      Plays the specified cinematic level (from the <code>cutscenes</code>).\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>play_fmv</code></td>\n    <td><code>fmv_id</code></td>\n    <td>String</td>\n    <td>\n      Plays the specified FMV. <code>fmv_id</code> must be a valid index into\n      the <code>fmvs</code> root key.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"2\">\n      <a name=\"give-item\"></a>\n      <code>give_item</code>\n    </td>\n    <td><code>object_id</code></td>\n    <td>Integer / String</td>\n    <td rowspan=\"2\">\n      Adds the specified item and quantity to Lara's inventory.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>quantity</code></td>\n    <td>Integer</td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"2\"><code>add_secret_reward</code></td>\n    <td><code>object_id</code></td>\n    <td>Integer / String</td>\n    <td rowspan=\"2\">\n      Adds the specified item to the current level's list of rewards for\n      collecting all secrets. This applies when using the TR2 style of specific\n      secret item pickups as opposed to floor-data defined triggers only.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>quantity</code></td>\n    <td>Integer</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>play_music</code></td>\n    <td><code>music_track</code></td>\n    <td>Integer</td>\n    <td>Plays the given audio track.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>remove_ammo</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n    <td rowspan=\"5\">\n      Any combination of these sequences can be used to modify Lara's\n      inventory at the start of a level. There are a few simple points to note:\n      <ul>\n        <li>\n          <code>remove_weapons</code> does not remove the ammo for those guns,\n          and equally <code>remove_ammo</code> does not remove the guns. Each\n          works independently of the other.\n        </li>\n        <li>\n          These sequences can also work together with\n          <code><a href=\"./SEQUENCES.md#give-item\">give_item</a></code> - so, item removal is\n          performed first, followed by addition.\n        </li>\n      </ul>\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>remove_weapons</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>remove_medipacks</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>remove_flares</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>remove_scions</code></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>setup_bacon_lara</code></td>\n    <td><code>anchor_room</code></td>\n    <td>Integer</td>\n    <td>\n      Sets the room number in which Bacon Lara will be anchored to enable\n      correct mirroring behaviour with Lara.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>enable_sunset</code><strong>²</strong></td>\n    <td colspan=\"2\" align=\"center\">N/A</td>\n    <td>\n      Enables the sunset effect, like in Bartoli's Hideout. At present, this\n      feature is hardcoded to gradually darken the game 40 minutes into playing\n      a level.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>set_lara_start_anim</code></td>\n    <td><code>value</code></td>\n    <td>Integer</td>\n    <td>\n      Applies the selected animation to Lara when the level begins. This is\n      used, for example, in the Offshore Rig of Tomb Raider II.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>disable_floor</code></td>\n    <td><code>value</code></td>\n    <td>Integer</td>\n    <td>\n      Configures a specific height (with 256 representing 1 click and 1024\n      representing 1 sector) to define an abyss that will invariably lead to\n      Lara's death if she falls into it. Additionally, it employs special\n      rendering to ensure it isn't treated as solid ground. This is used, for\n      example, in the Floating Islands of Tomb Raider II.\n    </td>\n  </tr>\n</table>\n\n**²** Tomb Raider 2 only.\n"
  },
  {
    "path": "docs/trx/game_flow/USER_CONFIGURATION.md",
    "content": "---\ntitle: User configuration\norder: 4\n---\n\n## User Configuration\n\nTRX allows the players to configure the game to their taste. The ingame setting\ndialogs write to `cfg/TR1X.json5` and `cfg/TR2X.json5`. As a level builder, you\nmay however wish to enforce some settings to match how your level is designed.\n\nAs an example, let's say you do not wish to add save crystals to your level, and\nas a result you wish to prevent the player from enabling that option in the\nconfig tool. To achieve this, open `cfg/tr1/gameflow.json5` in a suitable text\neditor and add the following.\n\n```json5\n{\n  // …\n  \"enforced_config\": {\n    \"enable_save_crystals\": false,\n  }\n}\n```\n\nThis means that the game will enforce your chosen value for this particular\nconfig setting. If the player tries to edit the settings, the option to toggle\nsave crystals will be disabled.\n\nYou can add as many settings within the `enforced_config` section as needed.\nRefer to the key names within `cfg/TR1X.json5` and `cfg/TR2X.json5` for\nreference.\n\nNote that you do not need to ship a full configuration with your level, and\nindeed it is not recommended to do so if you have, for example, your own custom\nkeyboard or controller layouts defined.\n\nIf you do not have any requirement to enforce settings, you can omit the\n`enforced_config` section from your game flow altogether.\n\n### Hiding settings from the in-game UI\n\nIf you prefer to hide certain configuration options entirely (rather than\nmerely enforce their values), you can add a `hidden_config` section in your\ngame flow JSON. Any setting listed here will be omitted from the settings\ndialogs. For example:\n\n```json5\n{\n  // …\n  \"hidden_config\": [\n    \"enable_legal\",\n    \"enable_save_crystals\",\n  ]\n}\n```\n\nWhen all settings in a given tab are hidden, that tab will also be removed from\nthe UI.\n"
  },
  {
    "path": "docs/trx/game_flow/levels/BONUS_LEVELS.md",
    "content": "---\ntitle: Bonus levels\norder: 5\n---\n\n## Bonus levels\n\nThe game flow supports bonus levels, which are unlocked only when the player\ncollects all secrets in the game's normal levels. These bonus levels behave just\nlike normal levels, so you can include FMVs, cutscenes in-between and so on.\n\nStatistics are maintained separately, so normal end-game statistics are shown\nonce, and then separate bonus level statistics are shown on completion of those\nlevels.\n\nFollowing is a sample level configuration with three normal levels and two bonus\nlevels. After the end-game credits are played following level 3, if the player\nhas collected all secrets, they will then be taken to level 4. Otherwise, the\ngame will exit to title.\n\n<details>\n<summary>Show example setup</summary>\n\n```json5\n{\n    \"levels\": [\n        {\n            // gym level definition\n        },\n\n        {\n            \"path\": \"data/level1.phd\",\n            \"music_track\": 57,\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n\n        {\n            \"path\": \"data/level2.phd\",\n            \"music_track\": 57,\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n\n        {\n            \"path\": \"data/level3.phd\",\n            \"music_track\": 57,\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_music\", \"music_track\": 19},\n                {\"type\": \"display_picture\", \"path\": \"data/end.pcx\", \"display_time\": 7.5},\n                {\"type\": \"display_picture\", \"path\": \"data/cred1.pcx\", \"display_time\": 7.5},\n                {\"type\": \"display_picture\", \"path\": \"data/cred2.pcx\", \"display_time\": 7.5},\n                {\"type\": \"display_picture\", \"path\": \"data/cred3.pcx\", \"display_time\": 7.5},\n                {\"type\": \"total_stats\", \"background_path\": \"data/install.pcx\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n\n        {\n            \"path\": \"data/bonus1.phd\",\n            \"type\": \"bonus\",\n            \"music_track\": 57,\n            \"sequence\": [\n                {\"type\": \"play_fmv\", \"fmv_path\": \"snow.avi\"},\n                {\"type\": \"loop_game\"},\n                {\"type\": \"play_cutscene\", \"cutscene_id\": 0},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"level_complete\"},\n            ],\n        },\n\n        {\n            \"path\": \"data/bonus2.phd\",\n            \"type\": \"bonus\",\n            \"music_track\": 57,\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n                {\"type\": \"level_stats\"},\n                {\"type\": \"play_music\", \"music_track\": 14},\n                {\"type\": \"total_stats\", \"background_path\": \"data/install.pcx\"},\n                {\"type\": \"exit_to_title\"},\n            ],\n        },\n    ],\n\n    \"cutscenes\": [\n        {\n            \"path\": \"data/bonuscut1.phd\",\n            \"music_track\": 23,\n            \"sequence\": [\n                {\"type\": \"loop_game\"},\n            ],\n        },\n    ],\n}\n```\n</details>\n"
  },
  {
    "path": "docs/trx/game_flow/levels/CUTSCENE_PROPERTIES.md",
    "content": "---\ntitle: Cutscenes\norder: 1\n---\n\n## Cutscenes\n\nThe `cutscenes` section contains all the cinematic levels, used with the\n`play_cutscene` sequence. Its structure is identical to the regular levels.\n"
  },
  {
    "path": "docs/trx/game_flow/levels/DEMO_PROPERTIES.md",
    "content": "---\ntitle: Demos\norder: 4\n---\n\n## Demos\n\nThe `demos` section contains all the levels that can play a demo when the player\nleaves the main inventory screen idle for a while or by using the `/demo`\ncommand. For the demos to work, these levels need to have demo data built-in.\nAside from this requirement, this section works just like the regular levels.\n"
  },
  {
    "path": "docs/trx/game_flow/levels/FMV_PROPERTIES.md",
    "content": "---\ntitle: FMVs\norder: 2\n---\n\n## FMVs\n\nThe FMVs section of the document defines how to play video content. This is an\narray of objects and can be defined in any order. The flow is controlled using\nthe correct sequencing within each level itself.\n\nFollowing are each of the properties available within an FMV.\n\n```json5\n{\n    \"path\": \"data/example.avi\",\n}\n```\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th colspan=\"2\">Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>path</code>\n    </td>\n    <td>String<strong>*</strong></td>\n    <td colspan=\"2\">The path to the FMV's video file.</td>\n  </tr>\n</table>\n\n**\\*** Required property.\n"
  },
  {
    "path": "docs/trx/game_flow/levels/ITEM_DROPS.md",
    "content": "---\ntitle: Item drops\norder: 4\n---\n\n## Item drops\n\nIn the original Tomb Raider I, items dropped by enemies were hardcoded such\nthat only specific enemies could drop, and the items and quantities that they\ndropped were immutable. This is no longer the case, with the game flow providing\na mechanism to allow the _majority_ of enemy types to carry and drop items.\nNote that this also means by default that the original enemies who did drop\nitems will not do so unless the game flow has been configured as such.\n\nItem drops can be defined in two ways. If `enable_tr2_item_drops` is `true`,\nthen custom level builders can add items directly to the level file, setting\ntheir position to be the same as the enemies who should drop them.\n\nFor the original TR1 levels, `enable_tr2_item_drops` is `false`. Item drops are\ninstead defined in the `item_drops` section of a level's definition by creating\nobjects with the following parameter structure. You can define at most one entry\nper enemy, but that definition can have as many drop items as necessary (within\nthe engine's overall item limit).\n\n<details>\n<summary>Show example setup</summary>\n\n```json5\n{\n    \"path\": \"data/example.phd\",\n    \"music_track\": 57,\n    \"item_drops\": [\n        {\"enemy_num\": 17, \"object_ids\": [86]},\n        {\"enemy_num\": 50, \"object_ids\": [87]},\n        {\"enemy_num\": 12, \"object_ids\": [93, 93]},\n        {\"enemy_num\": 47, \"object_ids\": [111]},\n    ],\n    \"sequence\": [\n         {\"type\": \"loop_game\"},\n         {\"type\": \"level_stats\"},\n         {\"type\": \"level_complete\"},\n    ],\n},\n```\n\nThis translates as follows.\n- Enemy #17 will drop the magnums\n- Enemy #50 will drop the uzis\n- Enemy #12 will drop two small medipacks\n- Enemy #47 will drop puzzle 2\n</details>\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Parameter</th>\n    <th>Type</th>\n    <th>Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>enemy_num</code>\n    </td>\n    <td>Integer</td>\n    <td>The index of the enemy in the level's item list.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <code>object_ids</code>\n    </td>\n    <td>Integer / string array</td>\n    <td>\n      A list of item <em>types</em> to drop. These items will spawn dynamically\n      and do not need to be added to the level file. Duplicate IDs are permitted\n      in the same array.\n    </td>\n  </tr>\n</table>\n\nYou can also toggle `convert_dropped_guns` in\n[global properties](../GLOBAL_PROPERTIES.md#convert-dropped-guns). When `true`, if an enemy drops a gun\nthat Lara already has, it will be converted to the equivalent ammo. When\n`false`, the gun will always be dropped.\n\n### Enemy validity\n\nAll enemy types are permitted to carry and drop items. This includes regular\nenemies as well as TR1 Atlantean pods (objects 163, 181), TR1 centaur\nstatues (object 161), and TR2 statues (objects 42, 44). For pods, the items will be allocated to the creature\nwithin (obviously empty pods are excluded).\n\nItems dropped by flying or swimming creatures will fall to the ground (TR1 only).\n\nFor clarity, following is a list of all enemy type IDs which you can\nreference when building your game flow. The game flow will ignore drops for\nnon-enemy type objects, and a suitable warning message will be produced in the\nlog file.\n\n<table>\n  <tr><th colspan=\"2\">TR1</th><th colspan=\"2\">TR2</th></tr>\n  <tr valign=\"top\" align=\"left\"><th>Object ID <th>Name</th><th>Object ID <th>Name</th></tr>\n  <tr><td>7</td><td>Wolf</td><td>15</td><td>Dog</td></tr>\n  <tr><td>8</td><td>Bear</td><td>16</td><td>Masked Goon 1</td></tr>\n  <tr><td>9</td><td>Bat</td><td>17</td><td>Masked Goon 2</td></tr>\n  <tr><td>10</td><td>Crocodile</td><td>18</td><td>Masked Goon 3</td></tr>\n  <tr><td>11</td><td>Alligator</td><td>19</td><td>Knife Thrower</td></tr>\n  <tr><td>12</td><td>Lion</td><td>20</td><td>Shotgun Goon</td></tr>\n  <tr><td>13</td><td>Lioness</td><td>21</td><td>Rat</td></tr>\n  <tr><td>14</td><td>Puma</td><td>22</td><td>Dragon Front</td></tr>\n  <tr><td>15</td><td>Ape</td><td>25</td><td>Shark</td></tr>\n  <tr><td>16</td><td>Rat</td><td>26</td><td>Eel</td></tr>\n  <tr><td>17</td><td>Vole</td><td>27</td><td>Big Eel</td></tr>\n  <tr><td>18</td><td>T-rex</td><td>28</td><td>Barracuda</td></tr>\n  <tr><td>19</td><td>Raptor</td><td>29</td><td>Scuba Diver</td></tr>\n  <tr><td>20</td><td>Flying mutant</td><td>30</td><td>Gunman Goon 1</td></tr>\n  <tr><td>21</td><td>Grounded mutant (shooter)</td><td>31</td><td>Gunman Goon 2</td></tr>\n  <tr><td>22</td><td>Grounded mutant (non-shooter)</td><td>32</td><td>Stick Wielding Goon 1</td></tr>\n  <tr><td>23</td><td>Centaur</td><td>33</td><td>Stick Wielding Goon 2</td></tr>\n  <tr><td>24</td><td>Mummy (Tomb of Qualopec)</td><td>34</td><td>Flamethrower Goon</td></tr>\n  <tr><td>27</td><td>Larson</td><td>35</td><td>Jellyfish</td></tr>\n  <tr><td>28</td><td>Pierre (not runaway)</td><td>36</td><td>Spider</td></tr>\n  <tr><td>30</td><td>Skate kid</td><td>37</td><td>Giant Spider</td></tr>\n  <tr><td>31</td><td>Cowboy</td><td>38</td><td>Crow</td></tr>\n  <tr><td>32</td><td>Kold</td><td>39</td><td>Tiger</td></tr>\n  <tr><td>33</td><td>Natla (items drop after second phase)</td><td>40</td><td>Marco Bartoli</td></tr>\n  <tr><td>34</td><td>Torso</td><td>41</td><td>Xian Spearman</td></tr>\n  <tr><td colspan=\"2\" rowspan=\"11\"></td><td>42</td><td>Xian Spearman Statue</td></tr>\n  <tr><td>43</td><td>Xian Knight</td></tr>\n  <tr><td>44</td><td>Xian Knight</td></tr>\n  <tr><td>45</td><td>Yeti</td></tr>\n  <tr><td>46</td><td>Bird Monster</td></tr>\n  <tr><td>47</td><td>Eagle</td></tr>\n  <tr><td>48</td><td>Mercenary 1</td></tr>\n  <tr><td>49</td><td>Mercenary 2</td></tr>\n  <tr><td>50</td><td>Mercenary 3</td></tr>\n  <tr><td>52</td><td>Black Snowmobile Driver</td></tr>\n  <tr><td>214</td><td>T-Rex</td></tr>\n</table>\n\n### Item validity\n\nThe following object types are capable of being carried and dropped. The\ngame flow will ignore anything that is not in this list, and a suitable warning\nmessage will be produced in the log file.\n\n<table>\n  <tr><th colspan=\"2\">TR1</th><th colspan=\"2\">TR2</th></tr>\n  <tr valign=\"top\" align=\"left\"><th>Object ID</th><th>Name</th><th>Object ID</th><th>Name</th></tr>\n  <tr><td>84</td><td>Pistols</td><td>135</td><td>Pistols</td></tr>\n  <tr><td>85</td><td>Shotgun</td><td>136</td><td>Shotgun</td></tr>\n  <tr><td>86</td><td>Magnums</td><td>137</td><td>Automatic Pistols</td></tr>\n  <tr><td>87</td><td>Uzis</td><td>138</td><td>Uzis</td></tr>\n  <tr><td>89</td><td>Shotgun ammo</td><td>139</td><td>Harpoon Gun</td></tr>\n  <tr><td>90</td><td>Magnum ammo</td><td>140</td><td>M16</td></tr>\n  <tr><td>91</td><td>Uzi ammo</td><td>141</td><td>Grenade Launcher</td></tr>\n  <tr><td>93</td><td>Small medipack</td><td>142</td><td>Pistol Clips</td></tr>\n  <tr><td>94</td><td>Large medipack</td><td>143</td><td>Shotgun Shells</td></tr>\n  <tr><td>110</td><td>Puzzle1</td><td>144</td><td>Automatic Pistol Clips</td></tr>\n  <tr><td>111</td><td>Puzzle2</td><td>145</td><td>Uzi Clips</td></tr>\n  <tr><td>112</td><td>Puzzle3</td><td>146</td><td>Harpoons</td></tr>\n  <tr><td>113</td><td>Puzzle4</td><td>147</td><td>M16 Clips</td></tr>\n  <tr><td>126</td><td>Lead bar</td><td>148</td><td>Grenades</td></tr>\n  <tr><td>129</td><td>Key1</td><td>149</td><td>Small Medipack</td></tr>\n  <tr><td>130</td><td>Key2</td><td>150</td><td>Large Medipack</td></tr>\n  <tr><td>131</td><td>Key3</td><td>152</td><td>Flare</td></tr>\n  <tr><td>132</td><td>Key4</td><td>151</td><td>Flares Box</td></tr>\n  <tr><td>141</td><td>Pickup1</td><td>174</td><td>Puzzle Item 1</td></tr>\n  <tr><td>142</td><td>Pickup2</td><td>175</td><td>Puzzle Item 2</td></tr>\n  <tr><td>144</td><td>Scion (à la Pierre)</td><td>176</td><td>Puzzle Item 3</td></tr>\n  <tr><td rowspan=\"10\" colspan=\"2\"></td><td>177</td><td>Puzzle Item 4</td></tr>\n  <tr><td>193</td><td>Key 1</td></tr>\n  <tr><td>194</td><td>Key 2</td></tr>\n  <tr><td>195</td><td>Key 3</td></tr>\n  <tr><td>196</td><td>Key 4</td></tr>\n  <tr><td>205</td><td>Pickup Item 1</td></tr>\n  <tr><td>206</td><td>Pickup Item 2</td></tr>\n  <tr><td>190</td><td>Secret 1</td></tr>\n  <tr><td>191</td><td>Secret 2</td></tr>\n  <tr><td>192</td><td>Secret 3</td></tr>\n</table>\n"
  },
  {
    "path": "docs/trx/game_flow/levels/README.md",
    "content": "---\ntitle: Levels\norder: 1\n---\n"
  },
  {
    "path": "docs/trx/game_flow/levels/REGULAR_LEVELS.md",
    "content": "---\ntitle: Regular levels\norder: 0\n---\n\n## Regular levels\n\nThe `levels` section of the document defines how the game plays out. This is an\narray of objects and can be defined in any order. The flow is controlled using\nthe correct [sequencing](../SEQUENCES.md) within each level itself.\n\nFollowing are each of the properties available within a level.\n\n<details>\n<summary>Show snippet</summary>\n\n```json5\n{\n    \"path\": \"data/example.phd\",\n    // Optional level Lua script file\n    \"script\": \"data/scripts/level1.lua\",\n    \"music_track\": 57,\n    \"lara_outfit\": \"tr2_classic\",\n    \"weather_type\": \"rain\",\n    \"water_particles\": true,\n    \"death_tile\": \"rapids\",\n    \"water_color\": [0.7, 0.5, 0.85],\n    \"cold_water\": true,\n    \"fog_transparency\": false,\n    \"fog_color\": [0, 0, 0],\n    \"fog_start\": 34.0,\n    \"fog_end\": 50.0,\n    \"unobtainable_pickups\": 1,\n    \"unobtainable_kills\": 1,\n    \"inherit_injections\": false,\n    \"injections\": [\n        \"data/level_injection1.bin\",\n        \"data/level_injection2.bin\",\n    ],\n    \"item_drops\": [\n        {\"enemy_num\": 17, \"object_ids\": [86]},\n        {\"enemy_num\": 50, \"object_ids\": [87]},\n        // etc\n    ],\n    \"sequence\": [\n        {\"type\": \"play_fmv\", \"fmv_id\": 0},\n        // etc\n    ],\n},\n```\n</details>\n\n<table>\n  <tr valign=\"top\" align=\"left\">\n    <th>Property</th>\n    <th>Type</th>\n    <th colspan=\"2\">Description</th>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>path</code></td>\n    <td>String<strong>*</strong></td>\n    <td colspan=\"2\">The path to the level's data file.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>script</code></td>\n    <td>String</td>\n    <td colspan=\"2\">Path to a Lua script executed after loading this level.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td rowspan=\"7\">\n      <code>type</code>\n    </td>\n    <td rowspan=\"7\">String</td>\n    <td colspan=\"2\">\n      The level type, which must be one of the following values.\n      Defaults to normal level.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><strong>Type</strong></td>\n    <td><strong>Description</strong></td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>normal</code></td>\n    <td>A standard level.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>gym</code></td>\n    <td>\n      At most one of these can be defined. Accessed from the photo option\n      (object ID 73) on the title screen. If omitted, the photo option is not\n      displayed.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>bonus</code></td>\n    <td>\n      Only playable when all secrets are collected. See\n      <a href=\"./BONUS_LEVELS.md\">Bonus levels</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>current</code></td>\n    <td>\n      One level of this type is necessary to read TombATI's save files. OG has a\n      special level called <code>LV_CURRENT</code> to handle save/load logic.\n      TRX does away with this hack. However, the existing save games expect the\n      level count to match, otherwise the game will crash.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>dummy</code></td>\n    <td>A placeholder level necessary to read TombATI's save files.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>sequence</code></td>\n    <td>Object array<strong>*</strong></td>\n    <td colspan=\"2\">\n      Instructions to define how a level plays out. See\n      <a href=\"../SEQUENCES.md\">Sequences</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>music_track</code></td>\n    <td>Integer<strong>*</strong></td>\n    <td colspan=\"2\">The ambient music track ID.</td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>lara_outfit</code></td>\n    <td>string<strong>*</strong></td>\n    <td colspan=\"2\">\n      Defines the outfit to use for Lara, unless overridden by player choice.\n      See <a href=\"../../OUTFITS.md\">Outfits</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>weather_type</code></td>\n    <td>String</td>\n    <td colspan=\"2\">\n      TR3 only. Enables per-level weather.\n      Valid values: <code>rain</code>, <code>snow</code>.\n      Omit for none.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>water_particles</code></td>\n    <td>Boolean</td>\n    <td colspan=\"2\">\n      TR3 only. Enables PSX-style underwater water particles for this level.\n      These follow the weather effects toggle.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>death_tile</code></td>\n    <td>String</td>\n    <td colspan=\"2\">\n      TR3 only. Controls the per-level death tile behavior.\n      Valid values: <code>lava</code>, <code>rapids</code>, <code>electric</code>.\n      Omit for lava.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td>\n      <a name=\"cold-water\"></a>\n      <code>cold_water</code>\n    </td>\n    <td>Boolean</td>\n    <td colspan=\"2\">\n      Can be customized per level. See <a href=\"../GLOBAL_PROPERTIES.md#cold-water\">the global property</a>\n      for details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fog_transparency</code></td>\n    <td>Boolean</td>\n    <td colspan=\"2\">\n      Can be customized per level. See <a href=\"../GLOBAL_PROPERTIES.md#fog-transparency\">the global property</a>\n      for details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fog_color</code></td>\n    <td>Float array or hex string</td>\n    <td colspan=\"2\">\n      Can be customized per level. See <a href=\"../GLOBAL_PROPERTIES.md#fog-color\">the global property</a>\n      for details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fog_start</code></td>\n    <td>Double</td>\n    <td colspan=\"2\">\n      Can be customized per level. See <a href=\"../GLOBAL_PROPERTIES.md#draw-distance-fade\">the global property</a>\n      for details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>fog_end</code></td>\n    <td>Double</td>\n    <td colspan=\"2\">\n      Can be customized per level. See <a href=\"../GLOBAL_PROPERTIES.md#draw-distance-max\">the global property</a>\n      for details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>injections</code></td>\n    <td>String array</td>\n    <td colspan=\"2\">\n      Injection file paths. See <a href=\"../../INJECTIONS.md\">Injections</a> for full\n      details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>inherit_injections</code></td>\n    <td>Boolean</td>\n    <td colspan=\"2\">\n      A flag to indicate whether or not the level should use the globally\n      defined injections. See <a href=\"../../INJECTIONS.md\">Injections</a> for full\n      details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>item_drops</code></td>\n    <td>Object array</td>\n    <td colspan=\"2\">\n      Instructions to allocate items to enemies who will drop those items when\n      killed. See <a href=\"./ITEM_DROPS.md\">Item drops</a> for full details.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>sfx_path</code><strong>²</strong></td>\n    <td>String</td>\n    <td colspan=\"2\">\n      The path to the sound effects (.sfx) file to use in this level. If this\n      property is not defined, the default global file will be used.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>unobtainable_kills</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      A count of enemies that will be excluded from kill statistics.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>unobtainable_pickups</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      A count of items that will be excluded from pickup statistics.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>unobtainable_secrets</code></td>\n    <td>Integer</td>\n    <td colspan=\"2\">\n      A count of secrets that will be excluded from secret statistics. Useful for level demos.\n    </td>\n  </tr>\n  <tr valign=\"top\">\n    <td><code>water_color</code></td>\n    <td>Float array or hex string</td>\n    <td colspan=\"2\">\n      Can be customized per level. See <a href=\"../GLOBAL_PROPERTIES.md#water-color\">the global property</a> for\n      details.\n    </td>\n  </tr>\n</table>\n\n**\\*** Required property.  \n**²** Tomb Raider 2 only.\n"
  },
  {
    "path": "docs/trx/lua/GETTING_STARTED.md",
    "content": "---\ntitle: Getting started\norder: 0\n---\n\n## Getting started\n\nYou'll need:\n\n- A TR1 or TR2 TRX build from the latest develop branch that adds Lua scripting.\n- Familiarity with the game flow JSON format.\n\n### Quick steps\n\nAdd per-level scripts in a level object:\n\n```json5\n{\n    \"levels\": [\n        // …,\n        {\n            \"script\": \"data/scripts/level1.lua\",\n            // …,\n        },\n        // …\n    ],\n    // …\n}\n```\n\nCreate a file `data/scripts/level1.lua` folder in your project and put the\nfollowing content:\n\n```lua\ntrx.events.after_level_state(function(level)\n  trx.log.info(\"hello from level 1!\")\nend)\n```\n\nStart the game. In the logs, you should see the following:\n\n```\nINF | 2025-10-04 12:12:23.155 [data/scripts/level1.lua:2:?] hello from level 1!\n```\n\n---\n\nOptionally, you can also load a global script by adding a global property to\nyour game flow configuration:\n\n```json5\n{\n    // Optional global Lua script file\n    \"main_script\": \"data/scripts/global.lua\",\n}\n```\n\n### Interactive commands\n\nYou can also try out short lua commands in-game with the `/lua` console command:\n\n```text\n/lua\n```\n"
  },
  {
    "path": "docs/trx/lua/README.md",
    "content": "---\ntitle: LUA scripting\norder: 6\n---\n\n# Introduction\n\nThis module adds **Lua scripting support** for creating advanced in-level\nbehaviors – dynamic triggers, event systems, and experimental gameplay logic.\nIts goal is to give builders a powerful toolkit for designing interactions that\ngo beyond what the editor alone can express.\n\nThe Lua system is still under active development. It works today, but its\nutility is currently limited, and some APIs may behave unexpectedly in certain\nedge cases. These quirks are part of the framework's growing pains, and we're\nsteadily working to refine and stabilize it.\n\nBecause the system is evolving quickly, the exposed scripting APIs are not yet\nfinal. Function names, parameters, and available hooks may change as we\ncontinue to expand and improve the framework. All updates and breaking changes\nwill be documented in the changelog so you can keep your projects up to date.\n\nAlso, we'd *love* to hear what you're building! Tell us about the systems,\ninteractions, or bold ideas you'd like to explore with Lua scripting – your\nexperiments and feedback will directly shape the future of this module.\n"
  },
  {
    "path": "docs/trx/lua/examples/README.md",
    "content": "---\ntitle: Examples\norder: 1\n---\n\n## LUA script examples\n\n### Adjusting enemy HP\n\nThis example sets all bats to start with 20 hitpoints, and all wolves to start\nwith 30 hitpoints. Simply adjust the lookup table to fit your needs. Refer to\n[this section](../../ENEMY_DEFAULTS.md) as a reference for original values.\n\n```lua\n-- Lookup table mapping object IDs to HP\nlocal hp_lut = {\n  [trx.catalog.objects.wolf] = 30,\n  [trx.catalog.objects.bat] = 20,\n}\n\n-- Adjust HP of enemies when the level loads\ntrx.events.after_level_state(function(level)\n  for i = 1, #trx.items do\n    local item = trx.items[i]\n    local hp = hp_lut[item.object_id]\n    if hp ~= nil then\n      item.hit_points = hp\n      item.max_hit_points = hp\n    end\n  end\nend)\n```\n\n### Teleporting Lara upon picking up a medipack\n\nThis will teleport Lara back to the starting point in TR1 Caves. Resetting the \ncamera may be required in some cases.\n\n```lua\ntrx.events.on_pickup(function(pickup_item)\n  local lara = trx.lara.item\n  lara.pos = {\n    x = 73.5 * 1024,\n    y = 3 * 1024,\n    z = 3.5 * 1024,\n  }\n  trx.camera.reset()\nend)\n```\n\n### Running code every control loop\n\nThis will run the provided function once every logical frame, meaning the\nfunction will always run at 30 FPS regardless of the player's FPS settings.\n\n```lua\ntrx.events.before_control(function()\n  -- handle per-frame control logic\nend)\n```\n\n### Changing water color in concrete rooms\n\nThis will change the color to crimson red if Lara is in room 15, and\ndemonstrates how to throttle updates to only happen if Lara goes from one room\nto another.\n\n```lua\nlocal last_room = 0\n\ntrx.events.before_control(function()\n  local lara = trx.lara.item\n  if lara.room_num ~= last_room then\n    last_room = lara.room_num\n    if lara.room_num == 15 then\n      trx.config.set(\"visuals.water_color\", \"ff0000\")\n    else\n      trx.config.set(\"visuals.water_color\", \"0000ff\")\n    end\n  end\nend)\n```\n"
  },
  {
    "path": "docs/trx/lua/reference/ASSAULT_STATS.md",
    "content": "---\ntitle: Assault course stats\norder: 15\n---\n\n## Assault course module\n\nModule for controlling the gym assault course statistics.\n\n### Functions\n\n- [lua]`trx.assault_stats.add_record(time)`  \n    Adds a new record with the given time (in seconds). Increments internal attempt number.\n\n- [lua]`trx.assault_stats.remove_record(record_id)`  \n    Removes a record at the given position, with ids starting from 1.\n\n- [lua]`trx.assault_stats.list_records()`  \n    Returns a list of record times.  \n    Structure:  \n    - `time`: time in seconds.\n    - `attempt_num`: which attempt this was.\n"
  },
  {
    "path": "docs/trx/lua/reference/CAMERA.md",
    "content": "---\ntitle: Camera\norder: 16\n---\n\n## Camera module\n\nModule for inspecting the active camera state.\n\n### Properties\n\n- **`pos`**: current camera position as `{ x, y, z }`.\n- **`room_num`**: 1-based room number of the camera position, or `nil` if unknown.\n- **`room`**: [lua]`trx.rooms.Room` for the camera position, or `nil` if unknown.\n- **`target_pos`**: current camera target position as `{ x, y, z }`.\n- **`target_room_num`**: 1-based room number of the camera target, or `nil` if unknown.\n\n### Functions\n\n- [lua]`trx.camera.shake(intensity)`  \n  Sets camera shake intensity by updating the camera bounce value.  \n  Positive values shake the camera upward; negative values shake it downward.  \n  Example:\n  ```lua\n  trx.camera.shake(200)\n  ```\n\n- [lua]`trx.camera.reset()`  \n  Resets the camera based on Lara's current position.  \n"
  },
  {
    "path": "docs/trx/lua/reference/CATALOG.md",
    "content": "---\ntitle: Catalog\norder: 13\n---\n\n## Catalog module\n\nA convenience module for accessing TRX catalog IDs.\n\n### Structures\n\n- [lua]`trx.catalog.objects`   \n  This table contains each TRX object ID. Names match those defined in `catalog_objects.csv`, with the `O_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`if item.object_id == trx.catalog.objects.wolf then ...`\n\n- [lua]`trx.catalog.flip_effects`   \n  This table contains each TRX flip effect action ID. Names match those defined in `catalog_item_actions.csv`, with the `ITEM_ACTION_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`trx.rooms.flip_effect(trx.catalog.flip_effects.floor_shake, 10)`\n\n- [lua]`trx.catalog.lara_states`   \n  This table contains each TRX Lara state ID. Names match those defined in `catalog_lara_states.csv`, with the `LS_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`if lara.state == trx.catalog.lara_states.run then ...`\n\n- [lua]`trx.catalog.weapons`   \n  This table contains each TRX Lara gun type ID. Names match the keys from `weapons.json5`, with the `LGT_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`if trx.lara.equipped_gun == trx.catalog.weapons.desert_eagle then ...`\n\n- [lua]`trx.catalog.lara_anims`   \n  This table contains each TRX Lara animation ID. Names match those defined in `catalog_lara_anims.csv`, with the `LA_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`if anim == trx.catalog.lara_anims.jump_forward then ...`\n\n- [lua]`trx.catalog.music`   \n  This table contains each TRX music track ID. Names match those defined in `catalog_music.csv`, with the `MX_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`trx.music.play(trx.catalog.music.secret)`\n\n- [lua]`trx.catalog.samples`   \n  This table contains each TRX sample ID. Names match those defined in `catalog_samples.csv`, with the `SFX_` prefix stripped and lowercased.   \n  Examples:   \n  - [lua]`trx.sound.play(trx.catalog.samples.lara_no)`\n"
  },
  {
    "path": "docs/trx/lua/reference/CONFIG.md",
    "content": "---\ntitle: Config\norder: 8\n---\n\n## Config module\n\nModule for querying and modifying engine configuration settings.\n\n### Functions\n\n- [lua]`trx.config.get(key)`  \n   Retrieves the current value of the config option specified by `key`.  \n\n- [lua]`trx.config.set(key, value)`  \n    Sets the configuration option identified by `key` to `value`. Returns an\n    error if the option is unknown or the value has an invalid format. All\n    values are currently passed as strings, even for numeric or boolean\n    options. Color settings expect a 6-digit hexadecimal string.\n\n    Note that using this API overrides the player's settings. The old value\n    isn't stored anywhere – the new value immediately becomes the active\n    setting, as if changed directly by the player, and will be remembered\n    across game saves and relaunches globally. Because of this, it's best to\n    mark any options you plan to modify (like water or fog color, view\n    distances, etc.) as frozen or hidden in the gameflow settings.\n\n- [lua]`trx.config.list()`  \n   Returns a Lua table mapping each config key to its current value.\n\n   Example - to list all settings and their values in the log file:\n\n   ```lua\n   for opt, val in pairs(trx.config.list()) do\n     trx.log.info(opt .. \" = \" .. val)\n   end\n   ```\n"
  },
  {
    "path": "docs/trx/lua/reference/CONSOLE.md",
    "content": "---\ntitle: Console\norder: 5\n---\n\n## Console module\n\nModule for interacting with the developer console.\n\n### Functions\n\n- [lua]`trx.console.log(\"string1\", \"string2\", ...)`  \n  [lua]`trx.console.log.generic(level, ...)`  \n  [lua]`trx.console.log.info(...)`  \n  [lua]`trx.console.log.error(...)`  \n  [lua]`trx.console.log.warn(...)`  \n  [lua]`trx.console.log.warning(...)`  \n  [lua]`trx.console.log.debug(...)`  \n    Logs a line to the developer console with a specific level.\n\n- [lua]`trx.console.eval(\"string\"[, opts])`  \n   Evaluates a given string as a developer console command.\n\n   By default, output from commands is silenced and only appears in the\n   terminal and the log file. To see output from the commands normally, pass `{ verbose = true }` in `opts`.  \n\n   Example:\n   > ```lua\n   > trx.console.eval(\"play 1\", { verbose = true })\n   > ```\n   will play the first level and show an according message in the console log.\n\n- [lua]`trx.console.clear()`  \n    Clears the console.\n"
  },
  {
    "path": "docs/trx/lua/reference/CREATURE.md",
    "content": "---\ntitle: Creatures\norder: 12\n---\n\n## Creatures module\n\nModule for controlling certain creature behavior.\n\n### Functions\n\n- [lua]`trx.creatures.hostile_allies`  \n    Reads/writes the global flag to indicate if allies are hostile towards Lara.  \n    Examples:\n    - [lua]`trx.creatures.hostile_allies = true`  \n      All allies are now hostile\n\n- [lua]`trx.creatures.add_ally(obj_id)`  \n    Marks the given object as being an ally of Lara.  \n    Examples:\n    - [lua]`trx.creatures.add_ally(trx.catalog.objects.monk_1)`  \n      All items of type `O_MONK_1` are now an ally of Lara\n\n- [lua]`trx.creatures.add_ally_target(obj_id)`  \n    Marks the given object as being one who will fight with any allies of Lara.  \n    Examples:\n    - [lua]`trx.creatures.add_ally_target(trx.catalog.objects.bandit_1)`  \n      All items of type `O_BANDIT_1` will now target any allies of Lara.\n"
  },
  {
    "path": "docs/trx/lua/reference/EVENTS.md",
    "content": "---\ntitle: Events\norder: 2\n---\n\n## Events module\n\nLua scripts can listen for game events using the global `events` API.\n\n### API\n\n- [lua]`trx.events.before_level_file(callback)`\n- [lua]`trx.events.after_level_file(callback)`\n- [lua]`trx.events.after_level_state(callback)`\n- [lua]`trx.events.on_game_start(callback)`\n- [lua]`trx.events.on_pickup(callback)`  \n- [lua]`trx.events.before_control(callback)`  \n- [lua]`trx.events.after_control(callback)`  \n  Register a handler for a game event. Returns `listener_id`.\n- [lua]`trx.events.detach(listener_id)`  \n  Remove a previously registered event handler.\n\n### Events\n\n#### `before_level_file`\nHappens prior to loading the level file.\n\nArguments:\n- `level_num`\n\n#### `after_level_file`\nHappens after the level finishes loading, prior to loading information from a\nsavegame.\n\nArguments:\n- `level_num`\n\n#### `after_level_state`\nHappens after the level finishes loading, after loading information from a\nsavegame. If the game is started normally, this duplicates `after_level_file`.\n\nArguments:\n- `level_num`\n\n#### `on_game_start`\nHappens after the level finishes loading and the game is about to start.\nThe difference from `after_level_file` and `after_level_state` is that this waits for the fade-to-black / cross-fade effects to\nfinish, and is suitable to play sound effects and run game logic.\n\nArguments:\n- `level_num`\n- `is_save`\n\n#### `on_pickup`\nHappens just after Lara picks up an item.\n\nArguments:\n- `item_num`\n\n#### `before_control`\nHappens on every logical game frame, before executing main game logic.\n\nArguments: none  \n\n#### `after_control`\nHappens on every logical game frame, after executing main game logic.\n\nArguments: none  \n\n### Examples\n\n```lua\ntrx.events.before_level_file(function(level_num)\n  -- handle pre-file-load setup\nend)\n\ntrx.events.after_level_state(function(level_num)\n  -- handle post-savegame state restore\nend)\n\ntrx.events.on_pickup(function(item_num)\n  trx.console.log(trx.items[item_num].object_id)\nend)\n\nlocal control_handler = trx.events.before_control(function()\n  -- handle control loop event\nend)\n-- detach a handler\ntrx.events.detach(control_handler)\n```\n"
  },
  {
    "path": "docs/trx/lua/reference/GAME.md",
    "content": "---\ntitle: Game\norder: 11\n---\n\n## Game module\n\nModule for retrieving game version and level tables.\n\n### Structures\n\n- [lua]`trx.game.Level`\n\n    Represents a level entry.\n\n    Properties:\n\n    - **`num`**: Integer ordinal number of the level.\n    - **`name`**: String title of the level.\n    - **`path`**: String file path of the level data.\n    - **`type`**: Integer type identifier of the level.\n\n- [lua]`trx.game.Settings`\n\n    Represents global engine settings.\n\n    Properties:\n    - **`lockout_option_ring`**: Whether to disallow the player from using the option ring in-game.\n    - **`load_save_disabled`**: Whether to disable saving and loading the game.\n    - **`play_any_level`**: Whether to show a full list of all levels in place of the New Game passport page.\n    - **`demo_delay`**: The number of seconds to pass in the main menu before playing the demo.\n    - **`cheat_keys`**: Whether to enable original game cheats (the ones where Lara turns around three times).\n\n    Writable properties:\n    - `lockout_option_ring`\n    - `load_save_disabled`\n    - `play_any_level`\n    - `demo_delay`\n    - `cheat_keys`\n\n### Functions\n\n- [lua]`trx.game.version`  \n  Returns the current game version integer. This is guessed from the level data.\n- [lua]`trx.game.trx_version`  \n  Returns the current TRX version string.\n\n- [lua]`trx.game.current_level`  \n  Retrieves the [lua]`trx.game.Level` that's currently loaded or `nil`.\n\n- [lua]`#trx.game.settings`  \n  Accesses the global engine settings.\n\n- [lua]`#trx.game.levels`  \n  [lua]`#trx.game.demos`  \n  [lua]`#trx.game.cutscenes`  \n  Returns the number of levels of the specific type.\n\n- [lua]`trx.game.levels[num]`  \n  [lua]`trx.game.demos[num]`  \n  [lua]`trx.game.cutscenes[num]`  \n  Retrieves the [lua]`trx.game.Level` of the specific type at the given index,\n  or `nil` if out of range.\n\n- [lua]`trx.game.play_level(num)`  \n  Plays the specified level via game flow override or errors if invalid.\n  If the Gym level is available, it's the level 1.\n- [lua]`trx.game.play_cutscene(num)`  \n  Plays the specified cutscene via game flow override or errors if invalid.\n- [lua]`trx.game.play_demo(num)`  \n  Plays the specified demo via game flow override or errors if invalid.\n"
  },
  {
    "path": "docs/trx/lua/reference/ITEMS.md",
    "content": "---\ntitle: Items\norder: 4\n---\n\n## Items module\n\nModule for controlling all moveables behavior.\n\n### Structures\n\n- [lua]`trx.items.Item`\n\n    Represents an item, also known as a moveable.\n\n    Properties:\n    - **`pos`**: A table with fields `x`, `y`, `z` representing position.\n    - **`rot`**: A table with fields `x`, `y`, `z` representing rotation.\n    - **`anim`**: Current animation number (0-indexed).\n    - **`frame`**: Current frame number (0-indexed).\n    - **`room_num`**: room number.\n    - **`room`**: [`trx.rooms.Room`] object for the room containing this item.\n    - **`status`**: Integer representing the item's status.\n    - **`flags`**: Integer representing the item's trigger-related flags.\n    - **`timer`**: Integer representing the item's trigger-related timer value.\n    - **`hit_points`**: Integer representing the item's hit points.\n    - **`max_hit_points`**: Integer representing the item's hit points.\n    - **`object_id`**: Integer ID of the item's object type.\n    - **`name`**: String name of the item, or `nil` if none.\n\n    Writable properties:\n    - `pos` (updating this also updates `room` and `room_num`)\n    - `rot`\n    - `anim`\n    - `frame`\n    - `hit_points` (updating this also may increase `max_hit_points`)\n    - `max_hit_points`\n    - `name` (string identifier; setting duplicates raises an error)\n\n### Functions\n\n- [lua]`#trx.items`  \n  Returns the total number of allocated items.\n\n- [lua]`trx.items[num]`  \n  [lua]`trx.items[\"name\"]`  \n  Retrieves the [lua]`trx.items.Item` at the given 1-based index or with the given `name`, or `nil` if out of range/not found.  \n\n  Example:\n  ```lua\n  local item = trx.items[1]\n  item.name = \"lara\"\n  local lara = trx.items[\"lara\"]\n  ```\n\n- [lua]`trx.items.fn.get(arg)`  \n  Alias of `trx.items[arg]`.\n\n  Example:\n  ```lua\n  local item_hp = trx.items.fn.get(17).hit_points\n  local lara_hp = trx.items.fn.get(\"lara\").hit_points\n  ```\n\n- [lua]`trx.items.find(query)`  \n  Finds all items matching the query and returns a list of [lua]`trx.items.Item`.\n\n  Supported query fields:\n  - `object_id`\n  - `room_num`\n\n  Unknown query fields are ignored and logged as warnings.\n\n  Example:\n  ```lua\n  local wolves = trx.items.find({ object_id = trx.catalog.objects.wolf })\n  local wolves_in_room_7 = trx.items.find({\n    object_id = trx.catalog.objects.wolf,\n    room_num = 7,\n  })\n  ```\n\n- [lua]`trx.items.first(query)`  \n  Finds the first item matching the query and returns a [lua]`trx.items.Item`, or `nil` if none match.\n\n  Supported query fields:\n  - `object_id`\n  - `room_num`\n\n  Unknown query fields are ignored and logged as warnings.\n\n  Example:\n  ```lua\n  local first_natla = trx.items.first({\n    object_id = trx.catalog.objects.natla,\n  })\n  ```\n"
  },
  {
    "path": "docs/trx/lua/reference/LARA.md",
    "content": "---\ntitle: Lara\norder: 3\n---\n\n## Lara module\n\nModule for interacting with the Lara's object.\n\n### Functions\n\n- [lua]`trx.lara.item`  \n    Returns [lua]`trx.items.Item` associated with Lara, or [lua]`nil` if the\n    Lara object is not available.\n\n- [lua]`trx.lara.target`  \n    Read-only - returns Lara's current gun target as [lua]`trx.items.Item`,\n    or [lua]`nil` if no target is locked.\n\n- [lua]`trx.lara.exposure_bar`  \n    Reads/writes exposure timer (cold water bar). The maximum value is 600.\n    If the cold bar setting is disabled on the game flow level, the health must\n    be managed manually from LUA.\n\n- [lua]`trx.lara.air_bar`  \n    Reads/writes Lara's air timer. The maximum value is 1800.  \n      \n    Example:\n    ```lua\n    -- Infinite oxygen\n    trx.events.after_control(function()\n        trx.lara.air_bar = 1800\n    end)\n    ```\n\n- [lua]`trx.lara.outfit`  \n    Reads/writes Lara's outfit name string (for example [lua]`\"tr2_diving_suit\"`).\n    Outfit names are the keys defined in [md]`cfg/outfits.json5`. Outfits are\n    stored in saves, but writing this value does not change the global config\n    setting, so subsequent levels will adhere to regular outfit changes.\n\n- [lua]`trx.lara.set_extra_equipment(lara_mesh_id, extra_mesh_id)`  \n    Defines a specific extra mesh to be drawn at the same position as the given\n    Lara mesh.\n\n    The extra mesh must be present in the `O_LARA_SKIN_SWAP_EXTRA` object and \n    should be setup properly in the [outfits JSON](../../OUTFITS.md). Refer\n    to the constants further below.\n\n    Example:\n    ```lua\n    -- Put an oar in Lara's right hand\n    trx.lara.set_extra_equipment(trx.lara.mesh.hand_r, trx.lara.extra_mesh.oar)\n    ```\n\n- [lua]`trx.lara.clear_equipment(lara_mesh_id)`  \n    Removes any equipment on the given Lara mesh. This applies to guns and extra\n    meshes.\n\n- [lua]`trx.lara.holsters_visible`  \n    Hides or shows Lara's holsters. This is used in OG TR1/2 gym and Home Sweet\n    Home levels to maintain original outfit appearance. If Lara picks up a\n    holster type weapon, or the weapon cheat is used, or a gun is given via the\n    console, the holsters will automatically be made visible.\n\n- [lua]`trx.lara.has_pistol_weapon`  \n    Read-only - returns true if Lara has any pistol-type weapon currently in her\n    inventory.\n\n- [lua]`trx.lara.extra_anim`  \n    Read-only - if Lara is currently in an extra anim state, returns the\n    relative animation number of the `O_LARA_EXTRA` object.  \n    Otherwise, returns -1.\n\n- [lua]`trx.lara.equipped_gun`  \n    Read-only - returns Lara's currently equipped gun ID.\n    Compare values using [lua]`trx.catalog.weapons`.\n\n## Mesh constants\n\n- `trx.lara.mesh.hips`  \n    An index to Lara's hips mesh.\n- `trx.lara.mesh.thigh_l`  \n    An index to Lara's left thigh mesh.\n- `trx.lara.mesh.calf_l`  \n    An index to Lara's left calf mesh.\n- `trx.lara.mesh.foot_l`  \n    An index to Lara's left foot mesh.\n- `trx.lara.mesh.thigh_r`  \n    An index to Lara's right thigh mesh.\n- `trx.lara.mesh.calf_r`  \n    An index to Lara's right calf mesh.\n- `trx.lara.mesh.foot_r`  \n    An index to Lara's right foot mesh.\n- `trx.lara.mesh.torso`  \n    An index to Lara's torso mesh.\n- `trx.lara.mesh.uarm_r`  \n    An index to Lara's upper right arm mesh.\n- `trx.lara.mesh.larm_r`  \n    An index to Lara's lower right arm mesh.\n- `trx.lara.mesh.hand_r`  \n    An index to Lara's right hand mesh.\n- `trx.lara.mesh.uarm_l`  \n    An index to Lara's upper left arm mesh.\n- `trx.lara.mesh.larm_l`  \n    An index to Lara's lower left arm mesh.\n- `trx.lara.mesh.hand_l`  \n    An index to Lara's left hand mesh.\n- `trx.lara.mesh.head`  \n    An index to Lara's head mesh.\n\n## Extra mesh constants\n\n- `trx.lara.extra_mesh.dagger_hand`  \n    An index to the dagger mesh used when Lara retrieves it from the dragon and\n    when inspecting it at home.\n- `trx.lara.extra_mesh.dagger_hips`  \n    An index to the dagger mesh that sits on Lara's hips during Home Sweet Home.\n- `trx.lara.extra_mesh.oar`  \n    An index to the oar mesh used when Lara is in the kayak.\n- `trx.lara.extra_mesh.spanner`  \n    An index to the spanner mesh used when Lara is in the minecart.\n- `trx.lara.extra_mesh.drink_can`  \n    An index to the drink can mesh used in the High Security Compound cutscene.\n- `trx.lara.extra_mesh.glasses_opaque`  \n    An index to Lara's opaque sunglasses mesh.\n- `trx.lara.extra_mesh.glasses_transparent`  \n    An index to Lara's transparent sunglasses mesh.\n"
  },
  {
    "path": "docs/trx/lua/reference/LOGGING.md",
    "content": "---\ntitle: Logging\norder: 1\n---\n\n## Logging module\n\nLua scripts can log with contextual source info via the global `Log` module.\nEach call records the Lua script filename, function name, and line number. The\nresults are logged to the standard output in the console as well as the\n`TRX.log` file in the installation directory.\n\n### Functions\n\n- [lua]`trx.log.info(message)`  \n  Logs an information to the terminal output and the log file.\n\n- [lua]`trx.log.warn(message)`  \n  Logs a warning to the terminal output and the log file.\n\n- [lua]`trx.log.error(message)`  \n  Logs an error to the terminal output and the log file.\n\n- [lua]`trx.log.debug(message)`  \n  Logs a debug message to the terminal output and the log file.\n\n### Examples\n\n```lua\ntrx.log.info(\"hello from lua\")\n```\n"
  },
  {
    "path": "docs/trx/lua/reference/MISC.md",
    "content": "---\ntitle: Miscellaneous\norder: 9\n---\n\n## Miscellaneous functions\n\n- [lua]`assert(condition)`  \n    Logs a detailed error message to the console if something is not true.\n- [lua]`print(message)`  \n    Logs a basic string to the console without extra decorations like timestamp or the module name.\n"
  },
  {
    "path": "docs/trx/lua/reference/MUSIC.md",
    "content": "---\ntitle: Music\norder: 6\n---\n\n## Music module\n\n## Functions\n\n- [lua]`trx.music.get_track()`  \n    Returns current playing track ID, or `nil` if none.\n- [lua]`trx.music.play(id[, opts])`  \n    Plays specified track. `opts.mode` selects a play mode constant. Errors if the track ID or mode is invalid.  \n    Examples:\n    - [lua]`trx.music.play(1)`  \n      Plays track 1 once.\n    - [lua]`trx.music.play(2, { mode = trx.music.PlayMode.LOOP })`  \n      Plays track 2 as a looped track.\n- [lua]`trx.music.pause()`  \n    Pauses the music.\n- [lua]`trx.music.unpause()`  \n    Resumes paused music.\n- [lua]`trx.music.stop()`  \n    Stops all music.\n\n## Play mode constants\n\n- `trx.music.PlayMode.ONCE`  \n    Plays the track once; after it finishes, any active looped track resumes.\n- `trx.music.PlayMode.LOOP`  \n    Plays the track in looped mode continuously. This track becomes the ambient track.\n- `trx.music.PlayMode.NO_REPEAT`  \n    Plays the track once but prevents retriggering if it's already playing.\n- `trx.music.PlayMode.DELAY`  \n    Schedules the track for later playback without starting it immediately.\n- `trx.music.PlayMode.OVERLAY`  \n    Schedules the track on top of current music track.\n"
  },
  {
    "path": "docs/trx/lua/reference/OBJECTS.md",
    "content": "---\ntitle: Object\norder: 14\n---\n\n## Object module\n\nModule for controlling game objects.\n\n### Functions\n\n- [lua]`trx.objects.swap_mesh(obj1_id, obj2_id, mesh1_num, mesh2_num)`  \n    Swaps the given meshes of the given objects.  \n    Examples:\n    - [lua]`trx.objects.swap_mesh(trx.catalog.objects.pierre, trx.catalog.objects.larson, 8, 8)`  \n      Pierre now has Larson's head, and vice-versa\n\n- [lua]`trx.objects.swap_mesh(obj1_id, obj2_id)`  \n    Similar to above, but this will swap out all meshes rather than specific ones. This works best when both objects have the same mesh count; if one object has fewer meshes than the other, the minimum count will be used.  \n    Examples:\n    - [lua]`trx.objects.swap_mesh(trx.catalog.objects.pierre, trx.catalog.objects.larson)`  \n      Pierre and Larson's meshes are fully swapped\n    - [lua]`trx.objects.swap_mesh(trx.catalog.objects.pierre, trx.catalog.objects.warrior_1)`  \n      Pierre's 15 meshes are now of mutant type; the mutant's first 15 meshes are Pierre's, the rest are default.\n"
  },
  {
    "path": "docs/trx/lua/reference/README.md",
    "content": "---\ntitle: Modules\norder: 2\n---\n"
  },
  {
    "path": "docs/trx/lua/reference/ROOMS.md",
    "content": "---\ntitle: Rooms\norder: 10\n---\n\n## Rooms module\n\nModule for inspecting all rooms in the current level.\n\n### Structures\n\n- [lua]`trx.rooms.fn.FlipStatus`:\n    - `trx.rooms.fn.FlipStatus.NONE`  \n        This is a normal room.\n    - `trx.rooms.fn.FlipStatus.UNFLIPPED`  \n        This room is currently reachable by Lara.\n    - `trx.rooms.fn.FlipStatus.FLIPPED`  \n        This room is currently inactive and unreachable by Lara.\n\n- [lua]`trx.rooms.fn.Room`\n\n    Represents a room.\n\n    Properties:\n    - **`num`**: 1-based room number.\n    - **`underwater`**: Whether the room is underwater or not.\n    - **`wind`**: Whether the room has breeze enabled or not. (Requires the player to have breeze enabled in the game settings).\n    - **`bounds`**: a table with world-coordinate bounds of the room. The table contains:\n      - **`min_x`**: minimum x coordinate.\n      - **`min_y`**: minimum y coordinate.\n      - **`min_z`**: minimum z coordinate.\n      - **`max_x`**: maximum x coordinate.\n      - **`max_y`**: maximum y coordinate.\n      - **`max_z`**: maximum z coordinate.\n    - **`internal_bounds`**: similar to `bounds`, but excludes the outer sector.\n    - **`flip_status`**: current room flip status (see `trx.rooms.fn.FlipStatus`).\n    - **`flipped_room`**: linked flip room of this room.\n\n    Writable properties:\n    - `underwater`\n    - `wind`\n\n### Functions\n\n-- Uses Lua length operator on the rooms table:\n- [lua]`#trx.rooms`  \n  Returns the total number of rooms.\n\n- [lua]`trx.rooms[num]`  \n  Retrieves the [lua]`trx.rooms.fn.Room` at the given 1-based index, or `nil` if out of range.\n\n- [lua]`trx.rooms.fn.get(arg)`  \n  Alias of `trx.rooms[arg]`.\n\n- [lua]`trx.rooms.flip()`  \n  Flips the current room map.\n\n- [lua]`trx.rooms.flip_effect(effect_id, [timer])`  \n  Sets the flip effect id (0-based), and optionally the flip timer.  \n  Use `effect_id=-1` to disable the current effect.\n"
  },
  {
    "path": "docs/trx/lua/reference/SOUND.md",
    "content": "---\ntitle: Sound\norder: 7\n---\n\n## Sound module\n\n## Functions\n\n- [lua]`trx.sound.is_available(id)`  \n    Returns `true` if the specified sound sample is available.\n- [lua]`trx.sound.stop(id)`  \n    Stops the specified sound effect.\n- [lua]`trx.sound.play(id[, opts])`  \n    Plays specified sound effect. `opts.pos` may be a `{ x=, y=, z= }` table for position.  \n    Examples:\n    - [lua]`trx.sound.play(99)`  \n      Plays the sound 99 (in TR1, this is an explosion, in TR2 this is a tiger's roar) at full volume.\n    - [lua]`trx.sound.play(99, { pos = { x = 100, y = 200, z = 50 } })`  \n      Plays the same sound at world position (100,200,50), applying pan and volume accordingly.\n- [lua]`trx.sound.stop_all()`  \n    Stops all currently playing sound effects.\n"
  },
  {
    "path": "docs/trx/water_colors.yml",
    "content": "name: Water colors\norder: 8\n\nTR1 DOS:\n  - name: original DOS version color\n    color: \"#99B2FF\"\n\nTR1 PC:\n  - name: default TombATI color\n    color: \"#72FFFF\"\n\nTR2 PC:\n  - name: default PC hardware renderer color\n    color: \"#80E0FF\"\n  - name: default PC software renderer color\n    color: \"#AAAAFF\"\n\nTR2 PS1:\n  - name: Lara's Home\n    color: \"#80FFFF\"\n  - name: The Great Wall\n    color: \"#B2E5E5\"\n  - name: Venice\n    color: \"#CCFF80\"\n  - name: Bartoli's Hideout\n    color: \"#CCFF80\"\n  - name: Opera House\n    color: \"#CCFF80\"\n  - name: Offshore Rig\n    color: \"#80FFFF\"\n  - name: Diving Area\n    color: \"#80FFFF\"\n  - name: 40 Fathoms\n    color: \"#80FFFF\"\n  - name: Wreck of the Maria Doria\n    color: \"#80FFFF\"\n  - name: Living Quarters\n    color: \"#80FFFF\"\n  - name: The Deck\n    color: \"#80FFFF\"\n  - name: Tibetan Foothills\n    color: \"#B2E5E5\"\n  - name: Barkhang Monastery\n    color: \"#80FFFF\"\n  - name: Catacombs of the Talion\n    color: \"#80FFFF\"\n  - name: Ice Palace\n    color: \"#80FFFF\"\n  - name: Temple of Xian\n    color: \"#CCFF99\"\n  - name: Floating Islands\n    color: \"#CCFFCC\"\n  - name: Dragon's Lair\n    color: \"#CCFFCC\"\n  - name: Home Sweet Home\n    color: \"#80FFFF\"\n\nTR3 PC:\n  - name: default PC renderer color\n    color: \"#80E0FF\"\n\nTR3 PS1:\n  - name: Lara's Home\n    color: \"#CCFF80\"\n  - name: Jungle\n    color: \"#CCFF80\"\n  - name: Temple Ruins\n    color: \"#CCFF80\"\n  - name: The River Ganges\n    color: \"#CCFF80\"\n  - name: Caves of Kaliya\n    color: \"#CCFF80\"\n  - name: Coastal Village\n    color: \"#80FFFF\"\n  - name: Crash Site\n    color: \"#FFFFFF\"\n  - name: Madubu Gorge\n    color: \"#FFFFFF\"\n  - name: Temple of Puna (no water)\n    color: \"#80E0FF\"\n  - name: Thames Wharf\n    color: \"#FFFFFF\"\n  - name: Aldwych\n    color: \"#CCFF80\"\n  - name: Lud's Gate\n    color: \"#CCFF80\"\n  - name: City\n    color: \"#CCFF80\"\n  - name: Nevada Desert\n    color: \"#FFFFFF\"\n  - name: High Security Compound\n    color: \"#FFFFFF\"\n  - name: Area 51\n    color: \"#FFFFFF\"\n  - name: Antarctica\n    color: \"#80FFFF\"\n  - name: RX-Tech Mines\n    color: \"#CCFFCC\"\n  - name: Lost City of Tinnos\n    color: \"#80E0FF\"\n  - name: Meteorite Cavern (no water)\n    color: \"#80E0FF\"\n  - name: All Hallows\n    color: \"#B2E6E6\"\n"
  },
  {
    "path": "justfile",
    "content": "CWD := `pwd`\nHOST_USER_UID := `id -u`\nHOST_USER_GID := `id -g`\nDOCKER_IMAGE_VERSION := \"20260318.rev1\"\n\ndefault: (trx-build-win \"debug\")\n\n_docker_push tag:\n    docker push {{tag}}:{{DOCKER_IMAGE_VERSION}}\n\n_docker_build dockerfile tag force=\"0\":\n    #!/usr/bin/env sh\n    full_tag=\"{{tag}}:{{DOCKER_IMAGE_VERSION}}\"\n    if [ \"{{force}}\" = \"0\" ]; then\n        docker images --format '{''{.Repository}}:{''{.Tag}}' | grep '^'\"$full_tag\"'$' >/dev/null\n        if [ $? -eq 0 ]; then\n            echo \"Docker image $full_tag found\"\n            exit 0\n        fi\n        echo \"Docker image $full_tag not found, trying to download from DockerHub\"\n        if docker pull $full_tag; then\n            echo \"Docker image $full_tag downloaded from DockerHub\"\n            exit 0\n        fi\n        echo \"Docker image $full_tag not found, trying to build\"\n    fi\n\n    echo \"Building Docker image: {{dockerfile}} → $full_tag\"\n    docker build \\\n        . \\\n        -f {{dockerfile}} \\\n        -t $full_tag\n\n_docker_run tag *args:\n    #!/usr/bin/env sh\n    full_tag=\"{{tag}}:{{DOCKER_IMAGE_VERSION}}\"\n    echo \"Running docker image: $full_tag {{args}}\"\n    docker run \\\n        --rm \\\n        --user \\\n        {{HOST_USER_UID}}:{{HOST_USER_GID}} \\\n        -e CCACHE_DIR \\\n        -e CCACHE_BASEDIR \\\n        -e CCACHE_COMPILERCHECK \\\n        -e CCACHE_MAXSIZE \\\n        -v {{CWD}}:/app/ \\\n        $full_tag \\\n        {{args}}\n\nimage-win force=\"1\": (_docker_build \"tools/shared/docker/game-win/Dockerfile\" \"rrdash/trx-win\" force)\nimage-linux force=\"1\": (_docker_build \"tools/shared/docker/game-linux/Dockerfile\" \"rrdash/trx-linux\" force)\nimage-win-installer force=\"1\": (_docker_build \"tools/shared/docker/installer/Dockerfile\" \"rrdash/trx-installer\" force)\n\npush-image-linux: (image-linux \"0\") (_docker_push \"rrdash/trx-linux\")\npush-image-win: (image-win \"0\") (_docker_push \"rrdash/trx-win\")\n\nimport \"justfile.tr1\"\nimport \"justfile.tr2\"\nimport \"justfile.tr3\"\n\ndownload-assets tr_version='all':\n    tools/download_assets {{tr_version}}\n\noutput-release-name:\n    tools/output_release_name\n\noutput-current-version *args:\n    tools/get_version {{args}}\n\noutput-current-changelog *args:\n    tools/output_current_changelog {{args}}\n\noutput-package-name *args:\n    tools/output_package_name {{args}}\n\nclean:\n    -find build/ -type f -delete\n    -find tools/ -type f \\( -ipath '*/out/*' -or -ipath '*/bin/*' -or -ipath '*/obj/*' \\) -delete\n    -find . -mindepth 1 -empty -type d -delete\n\n[group('lint')]\nlint-imports:\n    tools/sort_imports\n\n[group('lint')]\nlint-format:\n    prek -a\n\n[group('lint')]\nlint: (lint-imports) (lint-format)\n\ntrx-build-linux target='debug': (image-linux \"0\") (_docker_run \"rrdash/trx-linux\" \"build\" \"--target\" target)\ntrx-build-win target='debug': (image-win \"0\") (_docker_run \"rrdash/trx-win\" \"build\" \"--target\" target)\n\ntrx-build-win-installer target='release' *args: \\\n    (trx-build-win target) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"-o\" \"tools/installer/TRX_Installer/Resources/release.zip\") \\\n    (image-win-installer \"0\") \\\n    (_docker_run \"rrdash/trx-installer\" \"trx\")\n\ntrx-package-linux target='debug' *args: (trx-build-linux target) (_docker_run \"rrdash/trx-linux\" \"package-all\" args)\ntrx-package-win target='debug' *args: (trx-build-win target) (_docker_run \"rrdash/trx-win\" \"package-all\" args)\ntrx-package-win-installer target='release' *args: \\\n    (trx-build-win-installer target args) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--platform\" \"win-installer\" args)\n"
  },
  {
    "path": "justfile.tr1",
    "content": "[group('tr1')]\ntr1-build-win-installer: (image-win-installer \"0\") (_docker_run \"rrdash/trx-installer\" \"1\")\n\n[group('tr1')]\ntr1-package-linux target='release' *args: (trx-build-linux target) (_docker_run \"rrdash/trx-linux\" \"package\" \"--tr-version\" \"1\" args)\n[group('tr1')]\ntr1-package-win target='release' *args: (trx-build-win target) (_docker_run \"rrdash/trx-win\" \"package\" \"--tr-version\" \"1\" args)\n[group('tr1')]\ntr1-package-win-installer target='release' *args: \\\n    (trx-build-win target) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--tr-version\" \"1\" \"-o\" \"tools/installer/TR1X_Installer/Resources/release.zip\") \\\n    (tr1-build-win-installer) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--platform\" \"win-installer\" \"--tr-version\" \"1\" args)\n"
  },
  {
    "path": "justfile.tr2",
    "content": "[group('tr2')]\ntr2-build-win-installer: (image-win-installer \"0\") (_docker_run \"rrdash/trx-installer\" \"2\")\n\n[group('tr2')]\ntr2-package-linux target='release' *args: (trx-build-linux target) (_docker_run \"rrdash/trx-linux\" \"package\" \"--tr-version\" \"2\" args)\n[group('tr2')]\ntr2-package-win target='release' *args: (trx-build-win target) (_docker_run \"rrdash/trx-win\" \"package\" \"--tr-version\" \"2\" args)\n[group('tr2')]\ntr2-package-win-installer target='release' *args: \\\n    (trx-build-win target) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--tr-version\" \"2\" \"-o\" \"tools/installer/TR2X_Installer/Resources/release.zip\") \\\n    (tr2-build-win-installer) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--platform\" \"win-installer\" \"--tr-version\" \"2\" args)\n"
  },
  {
    "path": "justfile.tr3",
    "content": "[group('tr3')]\ntr3-build-win-installer: (image-win-installer \"0\") (_docker_run \"rrdash/trx-installer\" \"3\")\n\n[group('tr3')]\ntr3-package-linux target='release' *args: (trx-build-linux target) (_docker_run \"rrdash/trx-linux\" \"package\" \"--tr-version\" \"3\" args)\n[group('tr3')]\ntr3-package-win target='release' *args: (trx-build-win target) (_docker_run \"rrdash/trx-win\" \"package\" \"--tr-version\" \"3\" args)\n[group('tr3')]\ntr3-package-win-installer target='release' *args: \\\n    (trx-build-win target) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--tr-version\" \"3\" \"-o\" \"tools/installer/TR3X_Installer/Resources/release.zip\") \\\n    (tr3-build-win-installer) \\\n    (_docker_run \"rrdash/trx-win\" \"package\" \"--platform\" \"win-installer\" \"--tr-version\" \"3\" args)\n"
  },
  {
    "path": "src/meson.build",
    "content": "project(\n  'TRX',\n  'c',\n  default_options: [\n    'c_std=c2x',\n    'warning_level=3',\n  ],\n  meson_version: '>=1.3.0',\n)\n\n\nfs = import('fs')\n\nstaticdeps = get_option('staticdeps')\n# Always dynamically link on macOS\nif host_machine.system() == 'darwin'\n  staticdeps = false\nendif\n\nc_compiler = meson.get_compiler('c')\ngit = find_program('git', required: true)\npython3 = find_program('python3', required: true)\n\ntrx_lua_embed = custom_target(\n  'trx_lua_embed',\n  input: [\n    '../data/scripting/assault_stats.lua',\n    '../data/scripting/camera.lua',\n    '../data/scripting/catalog.lua',\n    '../data/scripting/config.lua',\n    '../data/scripting/console.lua',\n    '../data/scripting/creatures.lua',\n    '../data/scripting/events.lua',\n    '../data/scripting/game.lua',\n    '../data/scripting/items.lua',\n    '../data/scripting/lara.lua',\n    '../data/scripting/log.lua',\n    '../data/scripting/music.lua',\n    '../data/scripting/objects.lua',\n    '../data/scripting/rooms.lua',\n    '../data/scripting/sound.lua',\n  ],\n  output: ['trx_embedded_lua.c'],\n  command: [\n    python3,\n    meson.project_source_root() + '/../tools/embed_trx_lua.py',\n    '--output',\n    '@OUTPUT0@',\n    '@INPUT@',\n  ],\n)\n\nprefix_map = fs.relative_to(meson.current_source_dir() / 'trx', meson.global_build_root())\ncommon_build_opts = [\n  '-Wshadow',\n  '-Wno-unused',\n  '-Wno-unused-parameter',\n  '-DMESON_BUILD',\n  '-DGLEW_NO_GLU',\n  '-DSDL_MAIN_HANDLED',\n  '-fms-extensions',\n  '-fno-omit-frame-pointer',\n  '-fmacro-prefix-map=@0@/='.format(prefix_map),\n  '-fdebug-prefix-map=@0@/=src/'.format(prefix_map),\n  '-DDWST_STATIC',\n  '-DPCRE2_STATIC',\n  '-DPCRE2_CODE_UNIT_WIDTH=8',\n  '-DDEBUG=' + (get_option('buildtype') == 'debug' ? '1' : '0'),\n]\n\n# Some warning names are compiler-specific (e.g. Clang-only) and will spam notes\n# on GCC. Only add the ones the current compiler actually supports.\ncompiler_specific_warning_opts = [\n  '-Wno-microsoft-anon-tag',\n  '-Wno-gnu-binary-literal',\n  '-Wno-gnu-empty-initializer',\n  '-Wno-gnu-zero-variadic-macro-arguments',\n]\n\nbuild_opts = (\n  common_build_opts + c_compiler.get_supported_arguments(compiler_specific_warning_opts)\n)\n\nadd_project_arguments(build_opts, language: 'c')\n\nnull_dep = dependency('', required: false)\ndep_avcodec = dependency('libavcodec', static: staticdeps)\ndep_avformat = dependency('libavformat', static: staticdeps)\ndep_avutil = dependency('libavutil', static: staticdeps)\ndep_sdl2 = dependency('SDL2', static: staticdeps)\ndep_glew = dependency('glew', static: staticdeps)\ndep_pcre2 = dependency('libpcre2-8', static: staticdeps)\ndep_backtrace = c_compiler.find_library('backtrace', static: true, required: false)\ndep_swscale = dependency('libswscale', static: staticdeps)\ndep_swresample = dependency('libswresample', static: staticdeps)\ndep_lua = dependency('lua', static: staticdeps)\nc_compiler.check_header('uthash.h', required: true)\ndep_mathlibrary = c_compiler.find_library('m', static: staticdeps, required: false)\n\ndep_zlib = null_dep\nif not staticdeps\n  dep_zlib = dependency('zlib', static: staticdeps)\nendif\n\nif host_machine.system() == 'windows'\n  dep_opengl = c_compiler.find_library('opengl32')\nelse\n  dep_opengl = dependency('GL')\nendif\n\ndependencies = [\n  dep_avcodec,\n  dep_avformat,\n  dep_avutil,\n  dep_backtrace,\n  dep_glew,\n  dep_lua,\n  dep_mathlibrary,\n  dep_opengl,\n  dep_pcre2,\n  dep_sdl2,\n  dep_swresample,\n  dep_swscale,\n  dep_zlib,\n]\n\ncommon_sources = [\n  'trx/av/audio.c',\n  'trx/av/audio_reverb.c',\n  'trx/av/audio_sample.c',\n  'trx/av/audio_stream.c',\n  'trx/av/image.c',\n  'trx/av/video.c',\n  'trx/config/common.c',\n  'trx/config/dynamic_enum.c',\n  'trx/config/enum.c',\n  'trx/config/file.c',\n  'trx/config/map.c',\n  'trx/config/presets.c',\n  'trx/config/priv.c',\n  'trx/config/vars.c',\n  'trx/core/benchmark.c',\n  'trx/core/bson/parse.c',\n  'trx/core/bson/write.c',\n  'trx/core/colors.c',\n  'trx/core/enum_map.c',\n  'trx/core/event_manager.c',\n  'trx/core/filesystem.c',\n  'trx/core/hash.c',\n  'trx/core/json/base.c',\n  'trx/core/json/parse.c',\n  'trx/core/json/util/file.c',\n  'trx/core/json/util/read_io.c',\n  'trx/core/json/util/write_io.c',\n  'trx/core/json/write.c',\n  'trx/core/log.c',\n  'trx/core/math/func.c',\n  'trx/core/math/geom.c',\n  'trx/core/math/trig.c',\n  'trx/core/math/util.c',\n  'trx/core/memory.c',\n  'trx/core/strings/case_funcs.c',\n  'trx/core/strings/common.c',\n  'trx/core/strings/fuzzy_match.c',\n  'trx/core/thread_pool.c',\n  'trx/core/vector.c',\n  'trx/core/virtual_file.c',\n  'trx/game/anims/commands.c',\n  'trx/game/anims/common.c',\n  'trx/game/anims/frames.c',\n  'trx/game/camera/box_camera.c',\n  'trx/game/camera/cinematic.c',\n  'trx/game/camera/common.c',\n  'trx/game/camera/environment.c',\n  'trx/game/camera/fixed.c',\n  'trx/game/camera/los_camera.c',\n  'trx/game/camera/photo_mode.c',\n  'trx/game/camera/vars.c',\n  'trx/game/catalog/manager.c',\n  'trx/game/clock/common.c',\n  'trx/game/clock/timer.c',\n  'trx/game/clock/turbo.c',\n  'trx/game/collision/common.c',\n  'trx/game/collision/los.c',\n  'trx/game/console/cmd/clear.c',\n  'trx/game/console/cmd/config.c',\n  'trx/game/console/cmd/debug.c',\n  'trx/game/console/cmd/die.c',\n  'trx/game/console/cmd/easy_config.c',\n  'trx/game/console/cmd/end_level.c',\n  'trx/game/console/cmd/exit_game.c',\n  'trx/game/console/cmd/exit_to_title.c',\n  'trx/game/console/cmd/flipmap.c',\n  'trx/game/console/cmd/flood.c',\n  'trx/game/console/cmd/fly.c',\n  'trx/game/console/cmd/give_item.c',\n  'trx/game/console/cmd/give_secret.c',\n  'trx/game/console/cmd/heal.c',\n  'trx/game/console/cmd/help.c',\n  'trx/game/console/cmd/immune.c',\n  'trx/game/console/cmd/inf_sprint.c',\n  'trx/game/console/cmd/kill.c',\n  'trx/game/console/cmd/load_game.c',\n  'trx/game/console/cmd/lua.c',\n  'trx/game/console/cmd/mod.c',\n  'trx/game/console/cmd/music.c',\n  'trx/game/console/cmd/play_cutscene.c',\n  'trx/game/console/cmd/play_demo.c',\n  'trx/game/console/cmd/play_gym.c',\n  'trx/game/console/cmd/play_level.c',\n  'trx/game/console/cmd/pos.c',\n  'trx/game/console/cmd/save_game.c',\n  'trx/game/console/cmd/screenshot.c',\n  'trx/game/console/cmd/set_health.c',\n  'trx/game/console/cmd/sfx.c',\n  'trx/game/console/cmd/spawn.c',\n  'trx/game/console/cmd/speed.c',\n  'trx/game/console/cmd/strings.c',\n  'trx/game/console/cmd/teleport.c',\n  'trx/game/console/cmd/test_text.c',\n  'trx/game/console/cmd/trigger.c',\n  'trx/game/console/cmd/weather.c',\n  'trx/game/console/cmd/winston.c',\n  'trx/game/console/common.c',\n  'trx/game/console/history.c',\n  'trx/game/console/registry.c',\n  'trx/game/creature/alert.c',\n  'trx/game/creature/behavior.c',\n  'trx/game/creature/common.c',\n  'trx/game/creature/shooting.c',\n  'trx/game/cutscene.c',\n  'trx/game/demo.c',\n  'trx/game/effects/draw.c',\n  'trx/game/effects/manager.c',\n  'trx/game/enum.c',\n  'trx/game/events.c',\n  'trx/game/fader.c',\n  'trx/game/fmv.c',\n  'trx/game/fx/common.c',\n  'trx/game/fx/explosion_ring.c',\n  'trx/game/fx/footprint.c',\n  'trx/game/fx/gun_flash.c',\n  'trx/game/fx/laser.c',\n  'trx/game/fx/wake.c',\n  'trx/game/fx/water.c',\n  'trx/game/fx/water_particles.c',\n  'trx/game/fx/weather.c',\n  'trx/game/game/control.c',\n  'trx/game/game/draw.c',\n  'trx/game/game/state.c',\n  'trx/game/game_buf.c',\n  'trx/game/game_flow/common.c',\n  'trx/game/game_flow/inventory.c',\n  'trx/game/game_flow/reader.c',\n  'trx/game/game_flow/sequencer.c',\n  'trx/game/game_flow/sequencer_events.c',\n  'trx/game/game_flow/sequencer_misc.c',\n  'trx/game/game_flow/util.c',\n  'trx/game/game_flow/vars.c',\n  'trx/game/game_strings/entries.c',\n  'trx/game/game_strings/manager.c',\n  'trx/game/game_strings/table/common.c',\n  'trx/game/game_strings/table/priv.c',\n  'trx/game/game_strings/table/reader.c',\n  'trx/game/gun/common.c',\n  'trx/game/gun/control.c',\n  'trx/game/gun/misc.c',\n  'trx/game/gun/pistols.c',\n  'trx/game/gun/rifle.c',\n  'trx/game/gun/smashing.c',\n  'trx/game/gun/smoke.c',\n  'trx/game/gun/vars.c',\n  'trx/game/gym.c',\n  'trx/game/inject/common.c',\n  'trx/game/inject/data/anims.c',\n  'trx/game/inject/data/camera.c',\n  'trx/game/inject/data/meshes.c',\n  'trx/game/inject/data/objects.c',\n  'trx/game/inject/data/sound.c',\n  'trx/game/inject/data/textures.c',\n  'trx/game/inject/editor.c',\n  'trx/game/inject/editors/anims.c',\n  'trx/game/inject/editors/floor_data.c',\n  'trx/game/inject/editors/items.c',\n  'trx/game/inject/editors/meshes.c',\n  'trx/game/inject/editors/objects.c',\n  'trx/game/inject/editors/rooms.c',\n  'trx/game/inject/editors/textures.c',\n  'trx/game/inject/testers/items.c',\n  'trx/game/inject/testers/rooms.c',\n  'trx/game/inject/utils.c',\n  'trx/game/input/backends/controller.c',\n  'trx/game/input/backends/internal.c',\n  'trx/game/input/backends/keyboard.c',\n  'trx/game/input/combo.c',\n  'trx/game/input/common.c',\n  'trx/game/input/update.c',\n  'trx/game/interpolation.c',\n  'trx/game/inventory.c',\n  'trx/game/inventory_ring/control.c',\n  'trx/game/inventory_ring/draw.c',\n  'trx/game/inventory_ring/priv.c',\n  'trx/game/inventory_ring/vars.c',\n  'trx/game/items/actions/common.c',\n  'trx/game/items/actions/effects.c',\n  'trx/game/items/actions/footprint.c',\n  'trx/game/items/actions/general.c',\n  'trx/game/items/actions/gym_tr3.c',\n  'trx/game/items/actions/ids.c',\n  'trx/game/items/actions/items.c',\n  'trx/game/items/actions/lara.c',\n  'trx/game/items/anim.c',\n  'trx/game/items/carrier.c',\n  'trx/game/items/col.c',\n  'trx/game/items/draw.c',\n  'trx/game/items/manager.c',\n  'trx/game/items/utils.c',\n  'trx/game/items/walkable.c',\n  'trx/game/lara/breath.c',\n  'trx/game/lara/cheat.c',\n  'trx/game/lara/cheat_keys.c',\n  'trx/game/lara/col.c',\n  'trx/game/lara/col/climb.c',\n  'trx/game/lara/col/crouch.c',\n  'trx/game/lara/col/jump.c',\n  'trx/game/lara/col/land.c',\n  'trx/game/lara/col/monkey.c',\n  'trx/game/lara/col/swim.c',\n  'trx/game/lara/common.c',\n  'trx/game/lara/control.c',\n  'trx/game/lara/draw.c',\n  'trx/game/lara/electric.c',\n  'trx/game/lara/flare.c',\n  'trx/game/lara/hair.c',\n  'trx/game/lara/look.c',\n  'trx/game/lara/mesh.c',\n  'trx/game/lara/misc.c',\n  'trx/game/lara/pose.c',\n  'trx/game/lara/skin/common.c',\n  'trx/game/lara/skin/storage.c',\n  'trx/game/lara/state.c',\n  'trx/game/lara/state/climb.c',\n  'trx/game/lara/state/crouch.c',\n  'trx/game/lara/state/extra.c',\n  'trx/game/lara/state/jump.c',\n  'trx/game/lara/state/land.c',\n  'trx/game/lara/state/monkey.c',\n  'trx/game/lara/state/swim.c',\n  'trx/game/lara/vehicle.c',\n  'trx/game/level/cache.c',\n  'trx/game/level/common.c',\n  'trx/game/level/context.c',\n  'trx/game/level/finalize/animations.c',\n  'trx/game/level/finalize/gameplay_objects.c',\n  'trx/game/level/finalize/render_assets.c',\n  'trx/game/level/finalize/rooms.c',\n  'trx/game/level/format/format_tr1.c',\n  'trx/game/level/format/format_tr2.c',\n  'trx/game/level/format/format_tr3.c',\n  'trx/game/level/format/pipeline.c',\n  'trx/game/level/pipeline.c',\n  'trx/game/level/sections/anims.c',\n  'trx/game/level/sections/audio.c',\n  'trx/game/level/sections/cinematics.c',\n  'trx/game/level/sections/meshes.c',\n  'trx/game/level/sections/objects.c',\n  'trx/game/level/sections/pathing.c',\n  'trx/game/level/sections/rooms.c',\n  'trx/game/level/sections/textures.c',\n  'trx/game/level/settings.c',\n  'trx/game/lua/assault_stats.c',\n  'trx/game/lua/camera.c',\n  'trx/game/lua/catalog.c',\n  'trx/game/lua/common.c',\n  'trx/game/lua/config.c',\n  'trx/game/lua/console.c',\n  'trx/game/lua/creatures.c',\n  'trx/game/lua/events.c',\n  'trx/game/lua/game.c',\n  'trx/game/lua/items.c',\n  'trx/game/lua/lara.c',\n  'trx/game/lua/log.c',\n  'trx/game/lua/music.c',\n  'trx/game/lua/objects.c',\n  'trx/game/lua/rooms.c',\n  'trx/game/lua/sound.c',\n  'trx/game/matrix.c',\n  'trx/game/music/backend_cdaudio.c',\n  'trx/game/music/backend_cdaudio_wad.c',\n  'trx/game/music/backend_files.c',\n  'trx/game/music/common.c',\n  'trx/game/music/ids.c',\n  'trx/game/objects/col.c',\n  'trx/game/objects/common.c',\n  'trx/game/objects/creatures/ape.c',\n  'trx/game/objects/creatures/atlantean.c',\n  'trx/game/objects/creatures/bacon_lara.c',\n  'trx/game/objects/creatures/baldy.c',\n  'trx/game/objects/creatures/bandit_1.c',\n  'trx/game/objects/creatures/bandit_2.c',\n  'trx/game/objects/creatures/barracuda.c',\n  'trx/game/objects/creatures/bartoli.c',\n  'trx/game/objects/creatures/bat.c',\n  'trx/game/objects/creatures/bear.c',\n  'trx/game/objects/creatures/big_eel.c',\n  'trx/game/objects/creatures/big_spider.c',\n  'trx/game/objects/creatures/bird.c',\n  'trx/game/objects/creatures/bird_guardian.c',\n  'trx/game/objects/creatures/centaur.c',\n  'trx/game/objects/creatures/centaur_statue.c',\n  'trx/game/objects/creatures/civilian.c',\n  'trx/game/objects/creatures/claw_mutant.c',\n  'trx/game/objects/creatures/claw_mutant_plasma_ball.c',\n  'trx/game/objects/creatures/cobra.c',\n  'trx/game/objects/creatures/compy.c',\n  'trx/game/objects/creatures/cowboy.c',\n  'trx/game/objects/creatures/crawler_mutant.c',\n  'trx/game/objects/creatures/crocodile.c',\n  'trx/game/objects/creatures/cultist_1.c',\n  'trx/game/objects/creatures/cultist_2.c',\n  'trx/game/objects/creatures/cultist_3.c',\n  'trx/game/objects/creatures/diver.c',\n  'trx/game/objects/creatures/dog.c',\n  'trx/game/objects/creatures/dragon.c',\n  'trx/game/objects/creatures/eel.c',\n  'trx/game/objects/creatures/hybrid_mutant.c',\n  'trx/game/objects/creatures/jelly.c',\n  'trx/game/objects/creatures/larson.c',\n  'trx/game/objects/creatures/lion.c',\n  'trx/game/objects/creatures/lizard.c',\n  'trx/game/objects/creatures/mercenary.c',\n  'trx/game/objects/creatures/monk.c',\n  'trx/game/objects/creatures/monkey.c',\n  'trx/game/objects/creatures/mouse.c',\n  'trx/game/objects/creatures/mp_1.c',\n  'trx/game/objects/creatures/mp_2.c',\n  'trx/game/objects/creatures/mummy.c',\n  'trx/game/objects/creatures/natla.c',\n  'trx/game/objects/creatures/natla_gun.c',\n  'trx/game/objects/creatures/orca.c',\n  'trx/game/objects/creatures/patrol_dog.c',\n  'trx/game/objects/creatures/pierre.c',\n  'trx/game/objects/creatures/pod.c',\n  'trx/game/objects/creatures/prisoner.c',\n  'trx/game/objects/creatures/punk.c',\n  'trx/game/objects/creatures/raptor.c',\n  'trx/game/objects/creatures/rat.c',\n  'trx/game/objects/creatures/rx_worker_1.c',\n  'trx/game/objects/creatures/rx_worker_2.c',\n  'trx/game/objects/creatures/rx_worker_3.c',\n  'trx/game/objects/creatures/security_guard.c',\n  'trx/game/objects/creatures/shark.c',\n  'trx/game/objects/creatures/shiva.c',\n  'trx/game/objects/creatures/skate_kid.c',\n  'trx/game/objects/creatures/skidoo_driver.c',\n  'trx/game/objects/creatures/sophia.c',\n  'trx/game/objects/creatures/sophia_laser_bolt.c',\n  'trx/game/objects/creatures/sophia_plasma_ball.c',\n  'trx/game/objects/creatures/spider.c',\n  'trx/game/objects/creatures/swat.c',\n  'trx/game/objects/creatures/tiger.c',\n  'trx/game/objects/creatures/tony.c',\n  'trx/game/objects/creatures/tony_fire_ball.c',\n  'trx/game/objects/creatures/torso.c',\n  'trx/game/objects/creatures/trex.c',\n  'trx/game/objects/creatures/trex_alpha.c',\n  'trx/game/objects/creatures/tribe_axeman.c',\n  'trx/game/objects/creatures/tribe_boss.c',\n  'trx/game/objects/creatures/tribe_pipeman.c',\n  'trx/game/objects/creatures/wasp_mutant.c',\n  'trx/game/objects/creatures/willard.c',\n  'trx/game/objects/creatures/willard_plasma_ball.c',\n  'trx/game/objects/creatures/winston.c',\n  'trx/game/objects/creatures/winston_army.c',\n  'trx/game/objects/creatures/wolf.c',\n  'trx/game/objects/creatures/worker_1.c',\n  'trx/game/objects/creatures/worker_2.c',\n  'trx/game/objects/creatures/worker_3.c',\n  'trx/game/objects/creatures/xian_common.c',\n  'trx/game/objects/creatures/xian_knight.c',\n  'trx/game/objects/creatures/xian_spearman.c',\n  'trx/game/objects/creatures/yeti.c',\n  'trx/game/objects/draw.c',\n  'trx/game/objects/effects/blood.c',\n  'trx/game/objects/effects/body_part.c',\n  'trx/game/objects/effects/bubble.c',\n  'trx/game/objects/effects/dart_effect.c',\n  'trx/game/objects/effects/ember.c',\n  'trx/game/objects/effects/explosion.c',\n  'trx/game/objects/effects/flame.c',\n  'trx/game/objects/effects/glow.c',\n  'trx/game/objects/effects/gun_flash.c',\n  'trx/game/objects/effects/gun_shell.c',\n  'trx/game/objects/effects/hot_liquid.c',\n  'trx/game/objects/effects/missile.c',\n  'trx/game/objects/effects/pickup_aid.c',\n  'trx/game/objects/effects/ricochet.c',\n  'trx/game/objects/effects/snow_sprite.c',\n  'trx/game/objects/effects/splash.c',\n  'trx/game/objects/effects/twinkle.c',\n  'trx/game/objects/effects/water_sprite.c',\n  'trx/game/objects/general/ai_node.c',\n  'trx/game/objects/general/alarm_sound.c',\n  'trx/game/objects/general/animating.c',\n  'trx/game/objects/general/area_51_rocket.c',\n  'trx/game/objects/general/assault_target.c',\n  'trx/game/objects/general/bat_emitter.c',\n  'trx/game/objects/general/bell.c',\n  'trx/game/objects/general/big_bowl.c',\n  'trx/game/objects/general/bird_tweeter.c',\n  'trx/game/objects/general/boat.c',\n  'trx/game/objects/general/bridge_common.c',\n  'trx/game/objects/general/bridge_flat.c',\n  'trx/game/objects/general/bridge_tilt1.c',\n  'trx/game/objects/general/bridge_tilt2.c',\n  'trx/game/objects/general/cabin.c',\n  'trx/game/objects/general/camera_target.c',\n  'trx/game/objects/general/carcass.c',\n  'trx/game/objects/general/clock_chimes.c',\n  'trx/game/objects/general/cog.c',\n  'trx/game/objects/general/combat_end.c',\n  'trx/game/objects/general/copter.c',\n  'trx/game/objects/general/cutscene_player.c',\n  'trx/game/objects/general/detonator_box.c',\n  'trx/game/objects/general/ding_dong.c',\n  'trx/game/objects/general/disposable_animating.c',\n  'trx/game/objects/general/door.c',\n  'trx/game/objects/general/drawbridge.c',\n  'trx/game/objects/general/dummy.c',\n  'trx/game/objects/general/earthquake.c',\n  'trx/game/objects/general/final_cutscene.c',\n  'trx/game/objects/general/flare_item.c',\n  'trx/game/objects/general/fuse_box.c',\n  'trx/game/objects/general/gas_emitter.c',\n  'trx/game/objects/general/general.c',\n  'trx/game/objects/general/gong.c',\n  'trx/game/objects/general/gong_bonger.c',\n  'trx/game/objects/general/grenade.c',\n  'trx/game/objects/general/harpoon_bolt.c',\n  'trx/game/objects/general/keyhole.c',\n  'trx/game/objects/general/kill_all_triggered.c',\n  'trx/game/objects/general/lara_alarm.c',\n  'trx/game/objects/general/lift.c',\n  'trx/game/objects/general/lights/beacon_light.c',\n  'trx/game/objects/general/lights/colored_light.c',\n  'trx/game/objects/general/lights/electrical_light.c',\n  'trx/game/objects/general/lights/on_off_light.c',\n  'trx/game/objects/general/lights/pulse_light.c',\n  'trx/game/objects/general/lights/strobe_light.c',\n  'trx/game/objects/general/mini_copter.c',\n  'trx/game/objects/general/moving_bar.c',\n  'trx/game/objects/general/pickup.c',\n  'trx/game/objects/general/puzzle_hole.c',\n  'trx/game/objects/general/rocket.c',\n  'trx/game/objects/general/save_crystal.c',\n  'trx/game/objects/general/scion1.c',\n  'trx/game/objects/general/scion3.c',\n  'trx/game/objects/general/scion4.c',\n  'trx/game/objects/general/scion_holder.c',\n  'trx/game/objects/general/shoal.c',\n  'trx/game/objects/general/smashable.c',\n  'trx/game/objects/general/smoke_emitter.c',\n  'trx/game/objects/general/sphere_of_doom.c',\n  'trx/game/objects/general/switch.c',\n  'trx/game/objects/general/trapdoor.c',\n  'trx/game/objects/general/trigger_gate.c',\n  'trx/game/objects/general/waterfall.c',\n  'trx/game/objects/general/zipline.c',\n  'trx/game/objects/names.c',\n  'trx/game/objects/setup.c',\n  'trx/game/objects/traps/blade.c',\n  'trx/game/objects/traps/bubble_emitter.c',\n  'trx/game/objects/traps/cleaner.c',\n  'trx/game/objects/traps/common.c',\n  'trx/game/objects/traps/damocles_sword.c',\n  'trx/game/objects/traps/dart.c',\n  'trx/game/objects/traps/dart_emitter.c',\n  'trx/game/objects/traps/dying_monk.c',\n  'trx/game/objects/traps/electric_fence.c',\n  'trx/game/objects/traps/ember_emitter.c',\n  'trx/game/objects/traps/falling_block.c',\n  'trx/game/objects/traps/falling_ceiling.c',\n  'trx/game/objects/traps/fire_head.c',\n  'trx/game/objects/traps/flame_emitter.c',\n  'trx/game/objects/traps/gondola.c',\n  'trx/game/objects/traps/hook.c',\n  'trx/game/objects/traps/icicle.c',\n  'trx/game/objects/traps/killer_statue.c',\n  'trx/game/objects/traps/lava_wedge.c',\n  'trx/game/objects/traps/lightning_emitter.c',\n  'trx/game/objects/traps/midas_touch.c',\n  'trx/game/objects/traps/mine.c',\n  'trx/game/objects/traps/movable_block.c',\n  'trx/game/objects/traps/pendulum.c',\n  'trx/game/objects/traps/power_saw.c',\n  'trx/game/objects/traps/propeller.c',\n  'trx/game/objects/traps/raptor_emitter.c',\n  'trx/game/objects/traps/rolling_ball.c',\n  'trx/game/objects/traps/rotating_laser.c',\n  'trx/game/objects/traps/security_laser.c',\n  'trx/game/objects/traps/sentry_gun.c',\n  'trx/game/objects/traps/sliding_pillar.c',\n  'trx/game/objects/traps/spike_ceiling.c',\n  'trx/game/objects/traps/spike_wall.c',\n  'trx/game/objects/traps/spikes.c',\n  'trx/game/objects/traps/spinning_blade.c',\n  'trx/game/objects/traps/springboard.c',\n  'trx/game/objects/traps/teeth_trap.c',\n  'trx/game/objects/traps/thors_hammer.c',\n  'trx/game/objects/traps/train.c',\n  'trx/game/objects/traps/wasp_emitter.c',\n  'trx/game/objects/vars.c',\n  'trx/game/objects/vehicles/boat.c',\n  'trx/game/objects/vehicles/common.c',\n  'trx/game/objects/vehicles/kayak.c',\n  'trx/game/objects/vehicles/mine_cart.c',\n  'trx/game/objects/vehicles/mounted_gun.c',\n  'trx/game/objects/vehicles/quad_bike.c',\n  'trx/game/objects/vehicles/rib.c',\n  'trx/game/objects/vehicles/skidoo_armed.c',\n  'trx/game/objects/vehicles/skidoo_common.c',\n  'trx/game/objects/vehicles/skidoo_fast.c',\n  'trx/game/objects/vehicles/upv.c',\n  'trx/game/option/common.c',\n  'trx/game/option/controls.c',\n  'trx/game/option/examine.c',\n  'trx/game/option/gameplay.c',\n  'trx/game/option/globe_select.c',\n  'trx/game/option/graphics.c',\n  'trx/game/option/passport.c',\n  'trx/game/option/sound.c',\n  'trx/game/option/stats.c',\n  'trx/game/output/bind.c',\n  'trx/game/output/common.c',\n  'trx/game/output/draw.c',\n  'trx/game/output/func.c',\n  'trx/game/output/lights.c',\n  'trx/game/output/mesh_batcher/batcher.c',\n  'trx/game/output/mesh_batcher/mesh.c',\n  'trx/game/output/mesh_batcher/mesh_builder.c',\n  'trx/game/output/quad.c',\n  'trx/game/output/scene_compositor.c',\n  'trx/game/output/shaders/generic.c',\n  'trx/game/output/shaders/mesh.c',\n  'trx/game/output/shaders/ui.c',\n  'trx/game/output/sources/lightnings.c',\n  'trx/game/output/sources/misc.c',\n  'trx/game/output/sources/objects.c',\n  'trx/game/output/sources/overlay.c',\n  'trx/game/output/sources/poly_fx.c',\n  'trx/game/output/sources/rooms.c',\n  'trx/game/output/sources/rooms_debug.c',\n  'trx/game/output/sources/shadows.c',\n  'trx/game/output/sources/sprites.c',\n  'trx/game/output/sources/ui.c',\n  'trx/game/output/state.c',\n  'trx/game/output/textures.c',\n  'trx/game/output/uniforms.c',\n  'trx/game/output/utils.c',\n  'trx/game/output/vars.c',\n  'trx/game/output/vertex_range.c',\n  'trx/game/overlay.c',\n  'trx/game/pathing/box.c',\n  'trx/game/pathing/lot.c',\n  'trx/game/phase/executor.c',\n  'trx/game/phase/phase_cutscene.c',\n  'trx/game/phase/phase_demo.c',\n  'trx/game/phase/phase_game.c',\n  'trx/game/phase/phase_globe_select.c',\n  'trx/game/phase/phase_inventory.c',\n  'trx/game/phase/phase_pause.c',\n  'trx/game/phase/phase_photo_mode.c',\n  'trx/game/phase/phase_picture.c',\n  'trx/game/phase/phase_stats.c',\n  'trx/game/photo_mode.c',\n  'trx/game/random.c',\n  'trx/game/replay/test_recorder.c',\n  'trx/game/replay/test_replay.c',\n  'trx/game/rooms/common.c',\n  'trx/game/rooms/draw.c',\n  'trx/game/rooms/floor_data.c',\n  'trx/game/rooms/geometry.c',\n  'trx/game/savegame/common.c',\n  'trx/game/savegame/file.c',\n  'trx/game/savegame/file_read.c',\n  'trx/game/savegame/file_write.c',\n  'trx/game/screenshot.c',\n  'trx/game/shell/args.c',\n  'trx/game/shell/common.c',\n  'trx/game/shell/config.c',\n  'trx/game/shell/events.c',\n  'trx/game/shell/flow.c',\n  'trx/game/shell/input.c',\n  'trx/game/shell/main.c',\n  'trx/game/shell/mod.c',\n  'trx/game/shell/paths.c',\n  'trx/game/shell/platform.c',\n  'trx/game/shell/session.c',\n  'trx/game/shell/state.c',\n  'trx/game/sound/common.c',\n  'trx/game/sound/ids.c',\n  'trx/game/sparks/manager.c',\n  'trx/game/sparks/spawners.c',\n  'trx/game/spawn.c',\n  'trx/game/stats/common.c',\n  'trx/game/stats/init.c',\n  'trx/game/stats/scan.c',\n  'trx/game/ui/common.c',\n  'trx/game/ui/dialogs/base_passport.c',\n  'trx/game/ui/dialogs/color_editor.c',\n  'trx/game/ui/dialogs/config_presets.c',\n  'trx/game/ui/dialogs/controls.c',\n  'trx/game/ui/dialogs/controls_backend.c',\n  'trx/game/ui/dialogs/controls_editor.c',\n  'trx/game/ui/dialogs/gameplay_settings.c',\n  'trx/game/ui/dialogs/graphic_settings.c',\n  'trx/game/ui/dialogs/new_game.c',\n  'trx/game/ui/dialogs/pause.c',\n  'trx/game/ui/dialogs/photo_mode.c',\n  'trx/game/ui/dialogs/play_any_level.c',\n  'trx/game/ui/dialogs/save_slot.c',\n  'trx/game/ui/dialogs/select_level.c',\n  'trx/game/ui/dialogs/setting_helpers/enums.c',\n  'trx/game/ui/dialogs/setting_helpers/handlers.c',\n  'trx/game/ui/dialogs/setting_helpers/handlers_language.c',\n  'trx/game/ui/dialogs/settings.c',\n  'trx/game/ui/dialogs/settings_editor.c',\n  'trx/game/ui/dialogs/settings_tabs.c',\n  'trx/game/ui/dialogs/sound_settings.c',\n  'trx/game/ui/dialogs/stats.c',\n  'trx/game/ui/dialogs/switch_mod.c',\n  'trx/game/ui/dialogs/text.c',\n  'trx/game/ui/draw.c',\n  'trx/game/ui/elements/ammo_label.c',\n  'trx/game/ui/elements/anchor.c',\n  'trx/game/ui/elements/bar.c',\n  'trx/game/ui/elements/bar_enemy_hp.c',\n  'trx/game/ui/elements/bar_lara_air.c',\n  'trx/game/ui/elements/bar_lara_exposure.c',\n  'trx/game/ui/elements/bar_lara_hp.c',\n  'trx/game/ui/elements/bar_lara_sprint.c',\n  'trx/game/ui/elements/button_label.c',\n  'trx/game/ui/elements/color_swatch.c',\n  'trx/game/ui/elements/flash.c',\n  'trx/game/ui/elements/fps_counter.c',\n  'trx/game/ui/elements/frame.c',\n  'trx/game/ui/elements/gradient_slider.c',\n  'trx/game/ui/elements/hide.c',\n  'trx/game/ui/elements/horizontal_line.c',\n  'trx/game/ui/elements/label.c',\n  'trx/game/ui/elements/modal.c',\n  'trx/game/ui/elements/offset.c',\n  'trx/game/ui/elements/pad.c',\n  'trx/game/ui/elements/progress_button.c',\n  'trx/game/ui/elements/prompt.c',\n  'trx/game/ui/elements/requester.c',\n  'trx/game/ui/elements/resize.c',\n  'trx/game/ui/elements/row_arrows.c',\n  'trx/game/ui/elements/scrollable_stack.c',\n  'trx/game/ui/elements/sleek_bar.c',\n  'trx/game/ui/elements/spacer.c',\n  'trx/game/ui/elements/span.c',\n  'trx/game/ui/elements/stack.c',\n  'trx/game/ui/elements/tab_switch.c',\n  'trx/game/ui/elements/window.c',\n  'trx/game/ui/events.c',\n  'trx/game/ui/helpers.c',\n  'trx/game/ui/hud/console.c',\n  'trx/game/ui/hud/console_logs.c',\n  'trx/game/ui/hud/overlay.c',\n  'trx/game/ui/scaler.c',\n  'trx/game/ui/scrollable.c',\n  'trx/game/ui/settings.c',\n  'trx/game/ui/text.c',\n  'trx/game/viewport.c',\n  'trx/gl/buffer.c',\n  'trx/gl/context.c',\n  'trx/gl/enum.c',\n  'trx/gl/fbo.c',\n  'trx/gl/program.c',\n  'trx/gl/renderer.c',\n  'trx/gl/sampler.c',\n  'trx/gl/screenshot.c',\n  'trx/gl/texture.c',\n  'trx/gl/track.c',\n  'trx/gl/utils.c',\n  'trx/gl/vertex_array.c',\n  'trx/version.c',\n]\n\n\nif dep_backtrace.found() and host_machine.system() == 'linux'\n  common_sources += ['trx/core/log_linux.c']\nelif host_machine.system() == 'windows'\n  common_sources += ['trx/core/log_windows.c']\n  dwarfstack = subproject('dwarfstack', default_options: ['warning_level=0'])\n  dep_dwarfstack = dwarfstack.get_variable('dep_dwarfstack')\n  dep_dbghelp = c_compiler.find_library('dbghelp')\n  dependencies += [dep_dbghelp, dep_dwarfstack]\nelse\n  common_sources += ['trx/core/log_unknown.c']\nendif\n\nlink_args = []\nif host_machine.system() == 'windows'\n  link_args += ['-static']\nendif\n\n\n\n#################################################################################\n# TRX\n#################################################################################\n\n# autogenerated files\ntrx_init = custom_target(\n  'trx_fake_init',\n  output: ['trx_init.c'],\n  command: [python3, meson.project_source_root() + '/../tools/generate_init', '-o', meson.current_build_dir() / '@OUTPUT0@'],\n  build_always_stale: true,\n)\ntrx_version_rc = custom_target(\n  'trx_fake_version',\n  output: ['trx_version.rc'],\n  command: [python3, meson.project_source_root() + '/../tools/generate_rcfile', '-o', '@OUTPUT0@'],\n  build_always_stale: true,\n)\ntrx_icon_rc = custom_target(\n  'trx_fake_icon',\n  output: ['trx_icon.rc'],\n  command: [python3, meson.project_source_root() + '/../tools/generate_rcfile', '-o', '@OUTPUT0@'],\n)\n\ntrx_resources = []\nif host_machine.system() == 'windows'\n  windows = import('windows')\n  trx_resources = [\n    windows.compile_resources(trx_version_rc),\n    windows.compile_resources(trx_icon_rc),\n  ]\nendif\n\ntrx_sources = common_sources + [trx_init, trx_resources, trx_lua_embed]\n\nexecutable(\n  'TRX',\n  trx_sources,\n  dependencies: dependencies,\n  link_args: link_args,\n  win_subsystem: 'windows',\n  install: true,\n  install_tag: 'common',\n  c_args: ['-I.'],\n)\n\nif host_machine.system() == 'darwin'\n  mac_install_tree = files('../tools/shared/mac/install_tree')\n\n  foreach game_id : ['tr1', 'tr2', 'tr3']\n    meson.add_install_script(mac_install_tree, '--source', '../data/@0@/ship/cfg'.format(game_id), '--dest', 'Contents/Resources/cfg', install_tag: game_id)\n    meson.add_install_script(mac_install_tree, '--source', '../data/@0@/ship/data'.format(game_id), '--dest', 'Contents/Resources/data', install_tag: game_id)\n    meson.add_install_script(mac_install_tree, '--source', '../data/common/ship/cfg', '--dest', 'Contents/Resources/cfg', install_tag: game_id)\n    meson.add_install_script(mac_install_tree, '--source', '../data/common/ship/shaders', '--dest', 'Contents/Resources/shaders', install_tag: game_id)\n    install_data('../data/@0@/mac/icon.icns'.format(game_id), install_dir: 'Contents/Resources', install_tag: game_id)\n    install_data('../data/@0@/mac/Info.plist'.format(game_id), install_dir: 'Contents', install_tag: game_id)\n  endforeach\n\n  meson.add_install_script('../tools/shared/mac/bundle_dylibs', '-a', 'TR1X', install_tag: 'tr1')\n  meson.add_install_script('../tools/shared/mac/bundle_dylibs', '-a', 'TR2X', install_tag: 'tr2')\n  meson.add_install_script('../tools/shared/mac/bundle_dylibs', '-a', 'TR3X', install_tag: 'tr3')\nendif\n"
  },
  {
    "path": "src/meson.options",
    "content": "option('staticdeps', type: 'boolean', value: true, description: 'Try to build against static dependencies. default: true')\n"
  },
  {
    "path": "src/subprojects/dwarfstack.wrap",
    "content": "[wrap-file]\ndirectory = dwarfstack-2.2\nsource_url = https://github.com/ssbssa/dwarfstack/archive/refs/tags/2.2.tar.gz\nsource_filename = dwarfstack-2.2.tar.gz\nsource_hash = 1fca1d12756941c4c932b50f9abe56a3756f012a1e93deef553e141a78cc2709\npatch_directory = dwarfstack\n\n[provide]\ndwarfstack = dwarfstack_dep\n"
  },
  {
    "path": "src/subprojects/packagefiles/dwarfstack/meson.build",
    "content": "project(\n  'dwarfstack',\n  'c',\n  default_options: [\n    'c_std=c2x',\n    'warning_level=2',\n  ],\n)\n\nc_compiler = meson.get_compiler('c')\nbuild_opts = [\n  '-Wno-unused',\n  '-DDWST_STATIC',\n  '-DNO_DBGHELP',\n  '-DLIBDWARF_STATIC',\n]\nadd_project_arguments(build_opts, language: 'c')\ndep_zlib = dependency('zlib')\n\nsources = [\n  'mgwhelp/dwarf_pe.c',\n  'src/dwst-exception-dialog.c',\n  'src/dwst-exception.c',\n  'src/dwst-process.c',\n  'src/dwst-location.c',\n  'src/dwst-file.c',\n  'libdwarf/dwarf_debugnames.c',\n  'libdwarf/dwarf_dsc.c',\n  'libdwarf/dwarf_alloc.c',\n  'libdwarf/dwarf_loclists.c',\n  'libdwarf/dwarf_macro5.c',\n  'libdwarf/dwarf_harmless.c',\n  'libdwarf/dwarf_locationop_read.c',\n  'libdwarf/dwarf_gnu_index.c',\n  'libdwarf/dwarf_rnglists.c',\n  'libdwarf/dwarf_error.c',\n  'libdwarf/dwarf_init_finish.c',\n  'libdwarf/dwarf_abbrev.c',\n  'libdwarf/dwarf_xu_index.c',\n  'libdwarf/dwarf_names.c',\n  'libdwarf/dwarf_str_offsets.c',\n  'libdwarf/dwarf_tsearchhash.c',\n  'libdwarf/dwarf_die_deliv.c',\n  'libdwarf/dwarf_frame.c',\n  'libdwarf/dwarf_query.c',\n  'libdwarf/dwarf_global.c',\n  'libdwarf/dwarf_loc.c',\n  'libdwarf/dwarf_tied.c',\n  'libdwarf/dwarf_util.c',\n  'libdwarf/dwarf_form.c',\n  'libdwarf/dwarf_groups.c',\n  'libdwarf/dwarf_frame2.c',\n  'libdwarf/dwarf_memcpy_swap.c',\n  'libdwarf/dwarf_leb.c',\n  'libdwarf/dwarf_debuglink.c',\n  'libdwarf/dwarf_string.c',\n  'libdwarf/dwarf_line.c',\n  'libdwarf/dwarf_fission_to_cu.c',\n  'libdwarf/dwarf_find_sigref.c',\n  'libdwarf/dwarf_ranges.c',\n]\n\ndependencies = [\n  dep_zlib,\n]\n\nlibdwarfstack = static_library(\n  'libdwarfstack',\n  sources,\n  dependencies: dependencies,\n  include_directories: [\n    'include/',\n    'mgwhelp/',\n    'src/',\n    'libdwarf/',\n  ]\n)\n\ndep_dwarfstack = declare_dependency(\n  link_whole: libdwarfstack,\n  include_directories: [\n    include_directories('include', is_system: true)\n  ]\n)\n"
  },
  {
    "path": "src/trx/av/audio.c",
    "content": "#include <trx/av/audio_internal.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_error.h>\n#include <SDL2/SDL_stdinc.h>\n#include <stdint.h>\n#include <string.h>\n\nSDL_AudioDeviceID g_AudioDeviceID = 0;\nstatic int32_t m_RefCount = 0;\nstatic size_t m_MixBufferCapacity = 0;\nstatic float *m_MixBuffer = nullptr;\nstatic Uint8 m_Silence = 0;\nstatic bool m_Muted = false;\nstatic bool m_CallbackSeen = false;\nstatic bool m_ShouldSkipSDLQuitAudio = false;\n\nstatic void M_MixerCallback(void *userdata, Uint8 *stream_data, int32_t len)\n{\n    m_CallbackSeen = true;\n    memset(m_MixBuffer, m_Silence, len);\n    Audio_Sample_Mix(m_MixBuffer, len);\n    Audio_Reverb_Process(m_MixBuffer, len);\n    Audio_Stream_Mix(m_MixBuffer, len);\n    if (m_Muted) {\n        memset(m_MixBuffer, m_Silence, len);\n    }\n    memcpy(stream_data, m_MixBuffer, len);\n}\n\nbool Audio_Init(void)\n{\n    m_RefCount++;\n    if (g_AudioDeviceID) {\n        // already initialized\n        return true;\n    }\n\n    m_CallbackSeen = false;\n    m_ShouldSkipSDLQuitAudio = false;\n    int32_t result = SDL_InitSubSystem(SDL_INIT_AUDIO);\n    if (result < 0) {\n        LOG_ERROR(\"Error while calling SDL_Init: 0x%lx\", result);\n        return false;\n    }\n\n    SDL_AudioSpec desired;\n    SDL_memset(&desired, 0, sizeof(desired));\n    desired.freq = AUDIO_WORKING_RATE;\n    desired.format = AUDIO_WORKING_FORMAT;\n    desired.channels = AUDIO_WORKING_CHANNELS;\n    desired.samples = AUDIO_SAMPLES;\n    desired.callback = M_MixerCallback;\n    desired.userdata = nullptr;\n\n    SDL_AudioSpec delivered;\n    g_AudioDeviceID = SDL_OpenAudioDevice(nullptr, 0, &desired, &delivered, 0);\n\n    if (!g_AudioDeviceID) {\n        LOG_ERROR(\"Failed to open audio device: %s\", SDL_GetError());\n        return false;\n    }\n\n    m_Silence = desired.silence;\n    m_MixBufferCapacity = desired.samples * desired.channels\n        * SDL_AUDIO_BITSIZE(desired.format) / 8;\n\n    m_MixBuffer = Memory_Alloc(m_MixBufferCapacity);\n\n    SDL_PauseAudioDevice(g_AudioDeviceID, 0);\n\n    Audio_Sample_Init();\n    Audio_Stream_Init();\n    Audio_Reverb_Init(AUDIO_WORKING_RATE, AUDIO_WORKING_CHANNELS);\n\n    return true;\n}\n\nbool Audio_Shutdown(void)\n{\n    m_RefCount--;\n    if (m_RefCount > 0) {\n        return false;\n    }\n\n    if (g_AudioDeviceID) {\n        SDL_PauseAudioDevice(g_AudioDeviceID, 1);\n        if (!m_CallbackSeen) {\n            m_ShouldSkipSDLQuitAudio = true;\n        } else {\n            SDL_CloseAudioDevice(g_AudioDeviceID);\n        }\n        g_AudioDeviceID = 0;\n    }\n    Memory_FreePointer(&m_MixBuffer);\n\n    Audio_Sample_Shutdown();\n    Audio_Stream_Shutdown();\n    Audio_Reverb_Shutdown();\n\n    if (!m_ShouldSkipSDLQuitAudio) {\n        SDL_QuitSubSystem(SDL_INIT_AUDIO);\n    }\n    return true;\n}\n\nbool Audio_ShouldSkipSDLQuitAudio(void)\n{\n    return m_ShouldSkipSDLQuitAudio;\n}\n\nvoid Audio_Mute(void)\n{\n    m_Muted = true;\n}\n\nvoid Audio_Unmute(void)\n{\n    m_Muted = false;\n}\n\nbool Audio_IsMuted(void)\n{\n    return m_Muted;\n}\n\nvoid Audio_LockDevice(void)\n{\n    if (g_AudioDeviceID == 0) {\n        return;\n    }\n    SDL_LockAudioDevice(g_AudioDeviceID);\n}\n\nvoid Audio_UnlockDevice(void)\n{\n    if (g_AudioDeviceID == 0) {\n        return;\n    }\n    SDL_UnlockAudioDevice(g_AudioDeviceID);\n}\n\nvoid Audio_SetReverbType(const uint8_t reverb_type)\n{\n    if (g_AudioDeviceID) {\n        SDL_LockAudioDevice(g_AudioDeviceID);\n    }\n    Audio_Reverb_SetType(reverb_type);\n    if (g_AudioDeviceID) {\n        SDL_UnlockAudioDevice(g_AudioDeviceID);\n    }\n}\n\nuint8_t Audio_GetReverbType(void)\n{\n    uint8_t reverb_type = 0;\n    if (g_AudioDeviceID) {\n        SDL_LockAudioDevice(g_AudioDeviceID);\n    }\n    reverb_type = Audio_Reverb_GetType();\n    if (g_AudioDeviceID) {\n        SDL_UnlockAudioDevice(g_AudioDeviceID);\n    }\n    return reverb_type;\n}\n\nint32_t Audio_GetAVChannelLayout(const int32_t channels)\n{\n    switch (channels) {\n        // clang-format off\n        case 1: return AV_CH_LAYOUT_MONO;\n        case 2: return AV_CH_LAYOUT_STEREO;\n        default: return AV_CH_LAYOUT_MONO;\n        // clang-format on\n    }\n}\n\nint32_t Audio_GetAVAudioFormat(const int32_t sample_fmt)\n{\n    switch (sample_fmt) {\n        // clang-format off\n        case AUDIO_U8: return AV_SAMPLE_FMT_U8;\n        case AUDIO_S16: return AV_SAMPLE_FMT_S16;\n        case AUDIO_S32: return AV_SAMPLE_FMT_S32;\n        case AUDIO_F32: return AV_SAMPLE_FMT_FLT;\n        default: return -1;\n        // clang-format on\n    }\n}\n\nint32_t Audio_GetSDLAudioFormat(const enum AVSampleFormat sample_fmt)\n{\n    // clang-format off\n    switch (sample_fmt) {\n        case AV_SAMPLE_FMT_U8: return AUDIO_U8;\n        case AV_SAMPLE_FMT_S16: return AUDIO_S16;\n        case AV_SAMPLE_FMT_S32: return AUDIO_S32;\n        case AV_SAMPLE_FMT_FLT: return AUDIO_F32;\n        default: return -1;\n    }\n    // clang-format on\n}\n"
  },
  {
    "path": "src/trx/av/audio.h",
    "content": "#pragma once\n\n#include <SDL2/SDL_audio.h>\n#include <libavutil/samplefmt.h>\n#include <stddef.h>\n#include <stdint.h>\n\n#define AUDIO_MAX_SAMPLES 1000\n#define AUDIO_MAX_ACTIVE_SAMPLES 50\n#define AUDIO_MAX_ACTIVE_STREAMS 10\n#define AUDIO_DRIFT_THRESHOLD 0.2\n#define AUDIO_NO_SOUND (-1)\n\nbool Audio_Init(void);\nbool Audio_Shutdown(void);\nbool Audio_ShouldSkipSDLQuitAudio(void);\n\nvoid Audio_Mute(void);\nvoid Audio_Unmute(void);\nbool Audio_IsMuted(void);\n\nbool Audio_Stream_Pause(int32_t sound_id);\nbool Audio_Stream_Unpause(int32_t sound_id);\nbool Audio_Stream_SetPaused(int32_t sound_id, bool is_paused);\nint32_t Audio_Stream_CreateFromFile(const char *path);\nint32_t Audio_Stream_CreateFromMemory(uint8_t *data, size_t size);\nbool Audio_Stream_Close(int32_t sound_id);\nbool Audio_Stream_IsLooped(int32_t sound_id);\nbool Audio_Stream_SetVolume(int32_t sound_id, float volume);\nbool Audio_Stream_SetIsLooped(int32_t sound_id, bool is_looped);\n\n// Sync the audio against specific timestamp (seek if the drift is too large).\nbool Audio_Stream_SyncTimestamp(int32_t sound_id, double timestamp);\n\nbool Audio_Stream_SetFinishCallback(\n    int32_t sound_id, void (*callback)(int32_t sound_id, void *user_data),\n    void *user_data);\ndouble Audio_Stream_GetTimestamp(int32_t sound_id);\ndouble Audio_Stream_GetDuration(int32_t sound_id);\nbool Audio_Stream_SeekTimestamp(int32_t sound_id, double timestamp);\nbool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp);\nbool Audio_Stream_SetStopTimestamp(int32_t sound_id, double timestamp);\n\nbool Audio_Sample_Load(int32_t sample_num, const char *content, size_t size);\nbool Audio_Sample_Unload(int32_t sample_id);\nbool Audio_Sample_UnloadAll(void);\n\nint32_t Audio_Sample_Play(\n    int32_t sample_id, int32_t volume, float pitch, int32_t pan,\n    bool is_looped);\nbool Audio_Sample_IsPlaying(int32_t sound_id);\nbool Audio_Sample_Pause(int32_t sound_id);\nbool Audio_Sample_PauseAll(void);\nbool Audio_Sample_Unpause(int32_t sound_id);\nbool Audio_Sample_UnpauseAll(void);\nbool Audio_Sample_Close(int32_t sound_id);\nbool Audio_Sample_CloseAll(void);\nbool Audio_Sample_SetPan(int32_t sound_id, int32_t pan);\nbool Audio_Sample_SetVolume(int32_t sound_id, int32_t volume);\nbool Audio_Sample_SetPitch(int32_t sound_id, float pan);\n\nvoid Audio_SetReverbType(uint8_t reverb_type);\nuint8_t Audio_GetReverbType(void);\n"
  },
  {
    "path": "src/trx/av/audio_internal.h",
    "content": "#pragma once\n\n#include <trx/av/audio.h>\n\n#include <SDL2/SDL_audio.h>\n#include <libavformat/avformat.h>\n\n#define AUDIO_WORKING_RATE 44100\n#define AUDIO_WORKING_FORMAT AUDIO_F32\n#define AUDIO_SAMPLES 500\n#define AUDIO_WORKING_CHANNELS 2\n\nextern SDL_AudioDeviceID g_AudioDeviceID;\n\nvoid Audio_LockDevice(void);\nvoid Audio_UnlockDevice(void);\n\nint32_t Audio_GetAVChannelLayout(int32_t sample_fmt);\nint32_t Audio_GetAVAudioFormat(int32_t sample_fmt);\nint32_t Audio_GetSDLAudioFormat(enum AVSampleFormat sample_fmt);\n\nvoid Audio_Sample_Init(void);\nvoid Audio_Sample_Shutdown(void);\nvoid Audio_Sample_Mix(float *dst_buffer, size_t len);\n\nvoid Audio_Stream_Init(void);\nvoid Audio_Stream_Shutdown(void);\nvoid Audio_Stream_Mix(float *dst_buffer, size_t len);\n\nvoid Audio_Reverb_Init(int32_t sample_rate, int32_t channels);\nvoid Audio_Reverb_Shutdown(void);\nvoid Audio_Reverb_Process(float *dst_buffer, size_t len);\nvoid Audio_Reverb_SetType(uint8_t reverb_type);\nuint8_t Audio_Reverb_GetType(void);\n"
  },
  {
    "path": "src/trx/av/audio_reverb.c",
    "content": "// Based on Wine/FAudio reverb DSP (FAudioFX_reverb.c). Adapted for TRX.\n// Original authors: Ethan Lee, Luigi Auriemma, and the MonoGame Team.\n// License: zlib (see wine/libs/faudio/src/FAudioFX_reverb.c).\n\n#include <trx/av/audio_internal.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n\n#include <math.h>\n#include <stdint.h>\n#include <string.h>\n\n#define M_PRESET_COUNT 3\n\n#define M_REVERB_DEFAULT_REAR_DELAY 5\n#define M_REVERB_DEFAULT_POSITION 6\n#define M_REVERB_DEFAULT_POSITION_MATRIX 27\n#define M_REVERB_DEFAULT_ROOM_SIZE 100.0f\n#define M_REVERB_DEFAULT_WET_DRY_MIX 100.0f\n#define M_REVERB_DEFAULT_ROOM_FILTER_FREQ 5000.0f\n#define M_REVERB_DEFAULT_ROOM_FILTER_MAIN 0.0f\n#define M_REVERB_DEFAULT_ROOM_FILTER_HF 0.0f\n#define M_REVERB_DEFAULT_REFLECTIONS_GAIN 0.0f\n#define M_REVERB_DEFAULT_REVERB_GAIN 0.0f\n#define M_REVERB_DEFAULT_DECAY_TIME 1.0f\n#define M_REVERB_DEFAULT_DENSITY 100.0f\n\n#define M_REVERB_MAX_REFLECTIONS_DELAY 300\n#define M_REVERB_MAX_REVERB_DELAY 85\n#define M_REVERB_MIN_DECAY_TIME 0.1f\n#define M_REVERB_DENORMAL_EPSILON 1e-15f\n\n#define M_REVERB_WET_GAIN 1.20f // TRX addition\n#define M_DELAY_MAX_MS 300\n\n#define M_REVERB_COUNT_COMB 8\n#define M_REVERB_COUNT_APF_IN 1\n#define M_REVERB_COUNT_APF_OUT 4\n\ntypedef struct {\n    float wet_dry_mix;\n    int32_t room;\n    int32_t room_hf;\n    float room_rolloff_factor;\n    float decay_time;\n    float decay_hf_ratio;\n    int32_t reflections;\n    float reflections_delay;\n    int32_t reverb;\n    float reverb_delay;\n    float diffusion;\n    float density;\n    float hf_reference;\n} M_I3DL2_PARAMETERS;\n\ntypedef struct {\n    float wet_dry_mix;\n    uint32_t reflections_delay;\n    uint8_t reverb_delay;\n    uint8_t rear_delay;\n    uint8_t position_left;\n    uint8_t position_right;\n    uint8_t position_matrix_left;\n    uint8_t position_matrix_right;\n    uint8_t early_diffusion;\n    uint8_t late_diffusion;\n    uint8_t low_eq_gain;\n    uint8_t low_eq_cutoff;\n    uint8_t high_eq_gain;\n    uint8_t high_eq_cutoff;\n    float room_filter_freq;\n    float room_filter_main;\n    float room_filter_hf;\n    float reflections_gain;\n    float reverb_gain;\n    float decay_time;\n    float density;\n    float room_size;\n} M_PARAMETERS;\n\ntypedef struct {\n    int32_t sample_rate;\n    uint32_t capacity;\n    uint32_t delay;\n    uint32_t read_idx;\n    uint32_t write_idx;\n    float *buffer;\n} M_DSP_DELAY;\n\ntypedef enum {\n    M_DSP_BI_QUAD_LOW_SHELVING,\n    M_DSP_BI_QUAD_HIGH_SHELVING,\n} M_DSP_BI_QUAD_TYPE;\n\ntypedef struct {\n    int32_t sample_rate;\n    float a0;\n    float a1;\n    float a2;\n    float b1;\n    float b2;\n    float c0;\n    float d0;\n    float delay0;\n    float delay1;\n} M_DSP_BI_QUAD;\n\ntypedef struct {\n    M_DSP_DELAY comb_delay;\n    M_DSP_BI_QUAD low_shelving;\n    M_DSP_BI_QUAD high_shelving;\n    float comb_feedback_gain;\n} M_DSP_COMB_SHELVING;\n\ntypedef struct {\n    M_DSP_DELAY apf_delay;\n    float apf_feedback_gain;\n} M_DSP_ALL_PASS;\n\ntypedef enum {\n    M_POSITION_LEFT = 1,\n    M_POSITION_RIGHT = 2,\n    M_POSITION_CENTER = 4,\n    M_POSITION_REAR = 8\n} M_CHANNEL_POSITION_FLAGS;\n\ntypedef struct {\n    M_DSP_DELAY reverb_delay;\n    M_DSP_COMB_SHELVING lpf_comb[M_REVERB_COUNT_COMB];\n    M_DSP_ALL_PASS apf_out[M_REVERB_COUNT_APF_OUT];\n    M_DSP_BI_QUAD room_high_shelf;\n    float early_gain;\n    float gain;\n} M_DSP_REVERB_CHANNEL;\n\ntypedef struct {\n    M_DSP_DELAY early_delay;\n    M_DSP_ALL_PASS apf_in[M_REVERB_COUNT_APF_IN];\n\n    int32_t in_channels;\n    int32_t out_channels;\n    int32_t reverb_channels;\n    M_DSP_REVERB_CHANNEL channel[2];\n\n    float early_gain;\n    float reverb_gain;\n    float room_gain;\n    float wet_ratio;\n    float dry_ratio;\n} M_DSP_REVERB;\n\nstatic const float m_CombDelays[M_REVERB_COUNT_COMB] = {\n    25.31f, 26.94f, 28.96f, 30.75f, 32.24f, 33.80f, 35.31f, 36.67f,\n};\n\nstatic const float m_ApfInDelays[M_REVERB_COUNT_APF_IN] = { 13.28f };\nstatic const float m_ApfOutDelays[M_REVERB_COUNT_APF_OUT] = {\n    5.10f,\n    12.61f,\n    10.0f,\n    7.73f,\n};\n\nstatic const M_I3DL2_PARAMETERS m_ReverbPresets[M_PRESET_COUNT] = {\n    // clang-format off\n    { 50.0f, -1000, -500, 0.0f, 2.31f, 0.64f, -711, 0.012f, -800, 0.017f, 100.0f, 100.0f, 5000.0f, }, // Small Room\n    { 50.0f, -1000, -500, 0.0f, 2.31f, 0.64f, -711, 0.012f, -300, 0.017f, 100.0f, 100.0f, 5000.0f, }, // Medium Room\n    { 50.0f, -1000, -500, 0.0f, 2.31f, 0.64f, -711, 0.012f, 200, 0.017f, 100.0f, 100.0f, 5000.0f, } // Large Room\n    // clang-format on\n};\n\nstatic bool m_IsInitialised = false;\nstatic uint8_t m_ReverbType = 0;\nstatic M_PARAMETERS m_ReverbTypes[M_PRESET_COUNT];\nstatic M_DSP_REVERB m_Reverb;\n\nstatic inline float M_DbGainToFactor(const float gain)\n{\n    return powf(10.0f, gain / 20.0f);\n}\n\nstatic inline uint32_t M_MsToSamples(\n    const float msec, const int32_t sample_rate)\n{\n    return (uint32_t)((sample_rate * msec) / 1000.0f);\n}\n\nstatic inline float M_Undenormalize(const float sample_in)\n{\n    return fabsf(sample_in) < M_REVERB_DENORMAL_EPSILON ? 0.0f : sample_in;\n}\n\nstatic inline void M_DspDelay_Initialize(\n    M_DSP_DELAY *const filter, const int32_t sample_rate, const float delay_ms)\n{\n    ASSERT(delay_ms >= 0.0f && delay_ms <= M_DELAY_MAX_MS);\n\n    filter->sample_rate = sample_rate;\n    filter->capacity = M_MsToSamples(M_DELAY_MAX_MS, sample_rate);\n    filter->delay = M_MsToSamples(delay_ms, sample_rate);\n    filter->read_idx = 0;\n    filter->write_idx = filter->delay;\n    filter->buffer = Memory_Alloc(filter->capacity * sizeof(float));\n}\n\nstatic inline void M_DspDelay_Change(\n    M_DSP_DELAY *const filter, const float delay_ms)\n{\n    ASSERT(delay_ms >= 0.0f && delay_ms <= M_DELAY_MAX_MS);\n\n    filter->delay = M_MsToSamples(delay_ms, filter->sample_rate);\n    filter->read_idx = (filter->write_idx - filter->delay + filter->capacity)\n        % filter->capacity;\n}\n\nstatic inline float M_DspDelay_Read(M_DSP_DELAY *const filter)\n{\n    ASSERT(filter->read_idx < filter->capacity);\n    const float delay_out = filter->buffer[filter->read_idx];\n    filter->read_idx = (filter->read_idx + 1) % filter->capacity;\n    return delay_out;\n}\n\nstatic inline void M_DspDelay_Write(\n    M_DSP_DELAY *const filter, const float sample)\n{\n    ASSERT(filter->write_idx < filter->capacity);\n    filter->buffer[filter->write_idx] = M_Undenormalize(sample);\n    filter->write_idx = (filter->write_idx + 1) % filter->capacity;\n}\n\nstatic inline float M_DspDelay_Process(\n    M_DSP_DELAY *const filter, const float sample)\n{\n    const float delay_out = M_DspDelay_Read(filter);\n    M_DspDelay_Write(filter, sample);\n    return delay_out;\n}\n\nstatic inline void M_DspDelay_Reset(M_DSP_DELAY *const filter)\n{\n    filter->read_idx = 0;\n    filter->write_idx = filter->delay;\n    memset(filter->buffer, 0, filter->capacity * sizeof(float));\n}\n\nstatic inline void M_DspDelay_Destroy(M_DSP_DELAY *const filter)\n{\n    Memory_Free(filter->buffer);\n    filter->buffer = nullptr;\n}\n\nstatic inline float M_DspComb_FeedbackFromRT60(\n    M_DSP_DELAY *const delay, const float rt60_ms)\n{\n    const float exponent = (-3.0f * (float)delay->delay * 1000.0f)\n        / ((float)delay->sample_rate * rt60_ms);\n    return powf(10.0f, exponent);\n}\n\nstatic inline void M_DspBiQuad_Change(\n    M_DSP_BI_QUAD *const filter, const M_DSP_BI_QUAD_TYPE type,\n    const float frequency, const float q, const float gain)\n{\n    const float theta_c = (2 * M_PI * frequency) / (float)filter->sample_rate;\n    const float mu = M_DbGainToFactor(gain);\n    const float beta = type == M_DSP_BI_QUAD_LOW_SHELVING ? 4.0f / (1.0f + mu)\n                                                          : (1.0f + mu) / 4.0f;\n    const float delta = beta * tanf(theta_c * 0.5f);\n    const float gamma = (1.0f - delta) / (1.0f + delta);\n\n    if (type == M_DSP_BI_QUAD_LOW_SHELVING) {\n        filter->a0 = (1.0f - gamma) * 0.5f;\n        filter->a1 = filter->a0;\n    } else {\n        filter->a0 = (1.0f + gamma) * 0.5f;\n        filter->a1 = -filter->a0;\n    }\n\n    filter->a2 = 0.0f;\n    filter->b1 = -gamma;\n    filter->b2 = 0.0f;\n    filter->c0 = mu - 1.0f;\n    filter->d0 = 1.0f;\n}\n\nstatic inline void M_DspBiQuad_Initialize(\n    M_DSP_BI_QUAD *const filter, const int32_t sample_rate,\n    const M_DSP_BI_QUAD_TYPE type, const float frequency, const float q,\n    const float gain)\n{\n    filter->sample_rate = sample_rate;\n    filter->delay0 = 0.0f;\n    filter->delay1 = 0.0f;\n    M_DspBiQuad_Change(filter, type, frequency, q, gain);\n}\n\nstatic inline float M_DspBiQuad_Process(\n    M_DSP_BI_QUAD *const filter, const float sample_in)\n{\n    const float result = (filter->a0 * sample_in) + filter->delay0;\n    const float delay0 =\n        (filter->a1 * sample_in) - (filter->b1 * result) + filter->delay1;\n    const float delay1 = (filter->a2 * sample_in) - (filter->b2 * result);\n    filter->delay0 = M_Undenormalize(delay0);\n    filter->delay1 = M_Undenormalize(delay1);\n\n    return M_Undenormalize((result * filter->c0) + (sample_in * filter->d0));\n}\n\nstatic inline void M_DspBiQuad_Reset(M_DSP_BI_QUAD *const filter)\n{\n    filter->delay0 = 0.0f;\n    filter->delay1 = 0.0f;\n}\n\nstatic inline void M_DspBiQuad_Destroy(M_DSP_BI_QUAD *const filter)\n{\n    (void)filter;\n}\n\nstatic inline void M_DspCombShelving_Initialize(\n    M_DSP_COMB_SHELVING *const filter, const int32_t sample_rate,\n    const float delay_ms, const float rt60_ms, const float low_frequency,\n    const float low_q, const float low_gain, const float high_frequency,\n    const float high_q, const float high_gain)\n{\n    M_DspDelay_Initialize(&filter->comb_delay, sample_rate, delay_ms);\n    filter->comb_feedback_gain =\n        M_DspComb_FeedbackFromRT60(&filter->comb_delay, rt60_ms);\n    M_DspBiQuad_Initialize(\n        &filter->low_shelving, sample_rate, M_DSP_BI_QUAD_LOW_SHELVING,\n        low_frequency, low_q, low_gain);\n    M_DspBiQuad_Initialize(\n        &filter->high_shelving, sample_rate, M_DSP_BI_QUAD_HIGH_SHELVING,\n        high_frequency, high_q, high_gain);\n}\n\nstatic inline float M_DspCombShelving_Process(\n    M_DSP_COMB_SHELVING *const filter, const float sample_in)\n{\n    const float delay_out = M_DspDelay_Read(&filter->comb_delay);\n\n    float feedback = M_DspBiQuad_Process(&filter->high_shelving, delay_out);\n    feedback = M_DspBiQuad_Process(&filter->low_shelving, feedback);\n\n    const float to_buf =\n        M_Undenormalize(sample_in + (filter->comb_feedback_gain * feedback));\n    M_DspDelay_Write(&filter->comb_delay, to_buf);\n\n    return delay_out;\n}\n\nstatic inline void M_DspCombShelving_Reset(M_DSP_COMB_SHELVING *const filter)\n{\n    M_DspDelay_Reset(&filter->comb_delay);\n    M_DspBiQuad_Reset(&filter->low_shelving);\n    M_DspBiQuad_Reset(&filter->high_shelving);\n}\n\nstatic inline void M_DspCombShelving_Destroy(M_DSP_COMB_SHELVING *const filter)\n{\n    M_DspDelay_Destroy(&filter->comb_delay);\n    M_DspBiQuad_Destroy(&filter->low_shelving);\n    M_DspBiQuad_Destroy(&filter->high_shelving);\n}\n\nstatic inline void M_DspAllPass_Initialize(\n    M_DSP_ALL_PASS *const filter, const int32_t sample_rate,\n    const float delay_ms, const float gain)\n{\n    M_DspDelay_Initialize(&filter->apf_delay, sample_rate, delay_ms);\n    filter->apf_feedback_gain = gain;\n}\n\nstatic inline void M_DspAllPass_Change(\n    M_DSP_ALL_PASS *const filter, const float delay_ms, const float gain)\n{\n    M_DspDelay_Change(&filter->apf_delay, delay_ms);\n    filter->apf_feedback_gain = gain;\n}\n\nstatic inline float M_DspAllPass_Process(\n    M_DSP_ALL_PASS *const filter, const float sample_in)\n{\n    const float delay_out = M_DspDelay_Read(&filter->apf_delay);\n    const float to_buf =\n        M_Undenormalize(sample_in + (filter->apf_feedback_gain * delay_out));\n    M_DspDelay_Write(&filter->apf_delay, to_buf);\n    return M_Undenormalize(delay_out - (filter->apf_feedback_gain * to_buf));\n}\n\nstatic inline void M_DspAllPass_Reset(M_DSP_ALL_PASS *const filter)\n{\n    M_DspDelay_Reset(&filter->apf_delay);\n}\n\nstatic inline void M_DspAllPass_Destroy(M_DSP_ALL_PASS *const filter)\n{\n    M_DspDelay_Destroy(&filter->apf_delay);\n}\n\nstatic inline M_CHANNEL_POSITION_FLAGS M_GetChannelPositionFlags(\n    const int32_t total_channels, const int32_t channel)\n{\n    switch (total_channels) {\n    case 1:\n        return M_POSITION_CENTER;\n    case 2:\n        return channel == 0 ? M_POSITION_LEFT : M_POSITION_RIGHT;\n    default:\n        break;\n    }\n\n    ASSERT(0 && \"Unsupported channel count\");\n    return M_POSITION_LEFT;\n}\n\nstatic inline float M_GetStereoSpreadDelayMS(\n    const int32_t total_channels, const int32_t channel)\n{\n    const M_CHANNEL_POSITION_FLAGS flags =\n        M_GetChannelPositionFlags(total_channels, channel);\n    return (flags & M_POSITION_RIGHT) != 0 ? 0.5216f : 0.0f;\n}\n\nstatic inline void M_DspReverb_Create(\n    M_DSP_REVERB *const reverb, const int32_t sample_rate,\n    const int32_t in_channels, const int32_t out_channels)\n{\n    ASSERT(in_channels == 1 || in_channels == 2);\n    ASSERT(out_channels == 1 || out_channels == 2);\n\n    memset(reverb, 0, sizeof(*reverb));\n    M_DspDelay_Initialize(&reverb->early_delay, sample_rate, 10.0f);\n\n    for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) {\n        M_DspAllPass_Initialize(\n            &reverb->apf_in[i], sample_rate, m_ApfInDelays[i], 0.5f);\n    }\n\n    reverb->reverb_channels = out_channels;\n    for (int32_t c = 0; c < reverb->reverb_channels; c++) {\n        M_DspDelay_Initialize(\n            &reverb->channel[c].reverb_delay, sample_rate, 10.0f);\n\n        for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) {\n            M_DspCombShelving_Initialize(\n                &reverb->channel[c].lpf_comb[i], sample_rate,\n                m_CombDelays[i]\n                    + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c),\n                500.0f, 500.0f, 0.0f, -6.0f, 5000.0f, 0.0f, -6.0f);\n        }\n\n        for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) {\n            M_DspAllPass_Initialize(\n                &reverb->channel[c].apf_out[i], sample_rate,\n                m_ApfOutDelays[i]\n                    + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c),\n                0.5f);\n        }\n\n        M_DspBiQuad_Initialize(\n            &reverb->channel[c].room_high_shelf, sample_rate,\n            M_DSP_BI_QUAD_HIGH_SHELVING, 5000.0f, 0.0f, -10.0f);\n        reverb->channel[c].gain = 1.0f;\n    }\n\n    reverb->early_gain = 1.0f;\n    reverb->reverb_gain = 1.0f;\n    reverb->dry_ratio = 0.0f;\n    reverb->wet_ratio = 1.0f;\n    reverb->in_channels = in_channels;\n    reverb->out_channels = out_channels;\n}\n\nstatic inline void M_DspReverb_Destroy(M_DSP_REVERB *const reverb)\n{\n    M_DspDelay_Destroy(&reverb->early_delay);\n\n    for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) {\n        M_DspAllPass_Destroy(&reverb->apf_in[i]);\n    }\n\n    for (int32_t c = 0; c < reverb->reverb_channels; c++) {\n        M_DspDelay_Destroy(&reverb->channel[c].reverb_delay);\n        for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) {\n            M_DspCombShelving_Destroy(&reverb->channel[c].lpf_comb[i]);\n        }\n        M_DspBiQuad_Destroy(&reverb->channel[c].room_high_shelf);\n        for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) {\n            M_DspAllPass_Destroy(&reverb->channel[c].apf_out[i]);\n        }\n    }\n}\n\nstatic inline void M_DspReverb_SetParameters(\n    M_DSP_REVERB *const reverb, const M_PARAMETERS *const params)\n{\n    const float early_diffusion =\n        0.6f - ((params->early_diffusion / 15.0f) * 0.2f);\n\n    M_DspDelay_Change(&reverb->early_delay, (float)params->reflections_delay);\n\n    for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) {\n        M_DspAllPass_Change(\n            &reverb->apf_in[i], m_ApfInDelays[i], early_diffusion);\n    }\n\n    for (int32_t c = 0; c < reverb->reverb_channels; c++) {\n        const M_CHANNEL_POSITION_FLAGS position =\n            M_GetChannelPositionFlags(reverb->reverb_channels, c);\n        const float channel_delay =\n            (position & M_POSITION_REAR) != 0 ? params->rear_delay : 0.0f;\n\n        M_DspDelay_Change(\n            &reverb->channel[c].reverb_delay,\n            (float)params->reverb_delay + channel_delay);\n\n        for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) {\n            M_DSP_COMB_SHELVING *const comb = &reverb->channel[c].lpf_comb[i];\n\n            M_DspDelay_Change(\n                &comb->comb_delay,\n                m_CombDelays[i]\n                    + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c));\n\n            comb->comb_feedback_gain = M_DspComb_FeedbackFromRT60(\n                &comb->comb_delay,\n                MAX(params->decay_time, M_REVERB_MIN_DECAY_TIME) * 1000.0f);\n\n            M_DspBiQuad_Change(\n                &comb->low_shelving, M_DSP_BI_QUAD_LOW_SHELVING,\n                50.0f + params->low_eq_cutoff * 50.0f, 0.0f,\n                params->low_eq_gain - 8.0f);\n            M_DspBiQuad_Change(\n                &comb->high_shelving, M_DSP_BI_QUAD_HIGH_SHELVING,\n                1000.0f + params->high_eq_cutoff * 500.0f, 0.0f,\n                params->high_eq_gain - 8.0f);\n        }\n    }\n\n    reverb->early_gain = M_DbGainToFactor(params->reflections_gain);\n    reverb->reverb_gain = M_DbGainToFactor(params->reverb_gain);\n    reverb->room_gain = M_DbGainToFactor(params->room_filter_main);\n\n    const float late_diffusion =\n        0.6f - ((params->late_diffusion / 15.0f) * 0.2f);\n    for (int32_t c = 0; c < reverb->reverb_channels; c++) {\n        const M_CHANNEL_POSITION_FLAGS position =\n            M_GetChannelPositionFlags(reverb->reverb_channels, c);\n        for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) {\n            M_DspAllPass_Change(\n                &reverb->channel[c].apf_out[i],\n                m_ApfOutDelays[i]\n                    + M_GetStereoSpreadDelayMS(reverb->reverb_channels, c),\n                late_diffusion);\n        }\n\n        M_DspBiQuad_Change(\n            &reverb->channel[c].room_high_shelf, M_DSP_BI_QUAD_HIGH_SHELVING,\n            params->room_filter_freq, 0.0f,\n            params->room_filter_main + params->room_filter_hf);\n\n        float gain = 0.0f;\n        if ((position & M_POSITION_LEFT) != 0) {\n            gain = params->position_matrix_left;\n        } else if ((position & M_POSITION_RIGHT) != 0) {\n            gain = params->position_matrix_right;\n        } else {\n            gain =\n                (params->position_matrix_left + params->position_matrix_right)\n                / 2.0f;\n        }\n        reverb->channel[c].gain = 1.5f - (gain / 27.0f) * 0.5f;\n\n        if ((position & M_POSITION_REAR) != 0) {\n            reverb->channel[c].gain *= 0.75f;\n        }\n\n        if ((position & M_POSITION_LEFT) != 0) {\n            gain = params->position_left;\n        } else if ((position & M_POSITION_RIGHT) != 0) {\n            gain = params->position_right;\n        } else {\n            gain = (params->position_left + params->position_right) / 2.0f;\n        }\n        reverb->channel[c].early_gain =\n            (1.2f - (gain / 6.0f) * 0.2f) * reverb->early_gain;\n    }\n\n    reverb->wet_ratio = params->wet_dry_mix / 100.0f;\n    reverb->dry_ratio = 1.0f - reverb->wet_ratio;\n}\n\nstatic inline void M_DspReverb_Reset(M_DSP_REVERB *const reverb)\n{\n    M_DspDelay_Reset(&reverb->early_delay);\n    for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) {\n        M_DspAllPass_Reset(&reverb->apf_in[i]);\n    }\n    for (int32_t c = 0; c < reverb->reverb_channels; c++) {\n        M_DspDelay_Reset(&reverb->channel[c].reverb_delay);\n        for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) {\n            M_DspCombShelving_Reset(&reverb->channel[c].lpf_comb[i]);\n        }\n        for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) {\n            M_DspAllPass_Reset(&reverb->channel[c].apf_out[i]);\n        }\n        M_DspBiQuad_Reset(&reverb->channel[c].room_high_shelf);\n    }\n}\n\nstatic inline float M_DspReverb_ProcessEarly(\n    M_DSP_REVERB *const reverb, const float sample_in)\n{\n    float delay_out = M_DspDelay_Process(&reverb->early_delay, sample_in);\n    for (int32_t i = 0; i < M_REVERB_COUNT_APF_IN; i++) {\n        delay_out = M_DspAllPass_Process(&reverb->apf_in[i], delay_out);\n    }\n    return delay_out;\n}\n\nstatic inline float M_DspReverb_ProcessChannel(\n    M_DSP_REVERB *const reverb, M_DSP_REVERB_CHANNEL *const channel,\n    const float sample_in)\n{\n    float sample_out = 0.0f;\n    float early_late = 0.0f;\n\n    const float revdelay =\n        M_DspDelay_Process(&channel->reverb_delay, sample_in);\n\n    for (int32_t i = 0; i < M_REVERB_COUNT_COMB; i++) {\n        sample_out +=\n            M_DspCombShelving_Process(&channel->lpf_comb[i], revdelay);\n    }\n    sample_out /= (float)M_REVERB_COUNT_COMB;\n\n    for (int32_t i = 0; i < M_REVERB_COUNT_APF_OUT; i++) {\n        sample_out = M_DspAllPass_Process(&channel->apf_out[i], sample_out);\n    }\n\n    early_late =\n        (sample_in * channel->early_gain) + (sample_out * reverb->reverb_gain);\n\n    sample_out = M_DspBiQuad_Process(\n        &channel->room_high_shelf, early_late * reverb->room_gain);\n\n    return sample_out * channel->gain;\n}\n\nstatic inline void M_DspReverb_Process_2_to_2(\n    M_DSP_REVERB *const reverb, float *samples, const size_t sample_count)\n{\n    float *const samples_end = samples + (sample_count * 2);\n    while (samples < samples_end) {\n        const float in = (samples[0] + samples[1]) * 0.5f;\n        const float early = M_DspReverb_ProcessEarly(reverb, in);\n\n        const float left =\n            (M_DspReverb_ProcessChannel(reverb, &reverb->channel[0], early)\n             * reverb->wet_ratio * M_REVERB_WET_GAIN)\n            + samples[0] * reverb->dry_ratio;\n        const float right =\n            (M_DspReverb_ProcessChannel(reverb, &reverb->channel[1], early)\n             * reverb->wet_ratio * M_REVERB_WET_GAIN)\n            + samples[1] * reverb->dry_ratio;\n\n        samples[0] = M_Undenormalize(left);\n        samples[1] = M_Undenormalize(right);\n        samples += 2;\n    }\n}\n\nstatic inline void M_DspReverb_Process_1_to_1(\n    M_DSP_REVERB *const reverb, float *samples, const size_t sample_count)\n{\n    float *const samples_end = samples + sample_count;\n    while (samples < samples_end) {\n        const float in = *samples;\n        const float early = M_DspReverb_ProcessEarly(reverb, in);\n        const float late =\n            M_DspReverb_ProcessChannel(reverb, &reverb->channel[0], early);\n        *samples = M_Undenormalize(\n            (late * reverb->wet_ratio) + (in * reverb->dry_ratio));\n        samples++;\n    }\n}\n\nstatic inline void M_ConvertI3DL2ToNative(\n    const M_I3DL2_PARAMETERS *const i3dl2, M_PARAMETERS *const native)\n{\n    native->rear_delay = M_REVERB_DEFAULT_REAR_DELAY;\n    native->position_left = M_REVERB_DEFAULT_POSITION;\n    native->position_right = M_REVERB_DEFAULT_POSITION;\n    native->position_matrix_left = M_REVERB_DEFAULT_POSITION_MATRIX;\n    native->position_matrix_right = M_REVERB_DEFAULT_POSITION_MATRIX;\n    native->room_size = M_REVERB_DEFAULT_ROOM_SIZE;\n    native->low_eq_cutoff = 4;\n    native->high_eq_cutoff = 6;\n\n    native->room_filter_main = (float)i3dl2->room / 100.0f;\n    native->room_filter_hf = (float)i3dl2->room_hf / 100.0f;\n\n    if (i3dl2->decay_hf_ratio >= 1.0f) {\n        int32_t index = (int32_t)(-4.0f * log10f(i3dl2->decay_hf_ratio));\n        if (index < -8) {\n            index = -8;\n        }\n        native->low_eq_gain = (uint8_t)(index < 0 ? index + 8 : 8);\n        native->high_eq_gain = 8;\n        native->decay_time = i3dl2->decay_time * i3dl2->decay_hf_ratio;\n    } else {\n        int32_t index = (int32_t)(4.0f * log10f(i3dl2->decay_hf_ratio));\n        if (index < -8) {\n            index = -8;\n        }\n        native->low_eq_gain = 8;\n        native->high_eq_gain = (uint8_t)(index < 0 ? index + 8 : 8);\n        native->decay_time = i3dl2->decay_time;\n    }\n\n    float reflections_delay = i3dl2->reflections_delay * 1000.0f;\n    if (reflections_delay >= M_REVERB_MAX_REFLECTIONS_DELAY) {\n        reflections_delay = (float)(M_REVERB_MAX_REFLECTIONS_DELAY - 1);\n    } else if (reflections_delay <= 1.0f) {\n        reflections_delay = 1.0f;\n    }\n    native->reflections_delay = (uint32_t)reflections_delay;\n\n    float reverb_delay = i3dl2->reverb_delay * 1000.0f;\n    if (reverb_delay >= M_REVERB_MAX_REVERB_DELAY) {\n        reverb_delay = (float)(M_REVERB_MAX_REVERB_DELAY - 1);\n    }\n    native->reverb_delay = (uint8_t)reverb_delay;\n\n    native->reflections_gain = i3dl2->reflections / 100.0f;\n    native->reverb_gain = i3dl2->reverb / 100.0f;\n    native->early_diffusion = (uint8_t)(15.0f * i3dl2->diffusion / 100.0f);\n    native->late_diffusion = native->early_diffusion;\n    native->density = i3dl2->density;\n    native->room_filter_freq = i3dl2->hf_reference;\n    native->wet_dry_mix = i3dl2->wet_dry_mix;\n}\n\nvoid Audio_Reverb_Init(const int32_t sample_rate, const int32_t channels)\n{\n    if (m_IsInitialised) {\n        return;\n    }\n\n    M_DspReverb_Create(&m_Reverb, sample_rate, channels, channels);\n    for (int32_t i = 0; i < M_PRESET_COUNT; i++) {\n        M_ConvertI3DL2ToNative(&m_ReverbPresets[i], &m_ReverbTypes[i]);\n    }\n    m_IsInitialised = true;\n}\n\nvoid Audio_Reverb_Shutdown(void)\n{\n    if (!m_IsInitialised) {\n        return;\n    }\n\n    M_DspReverb_Destroy(&m_Reverb);\n    m_IsInitialised = false;\n}\n\nvoid Audio_Reverb_SetType(uint8_t reverb_type)\n{\n    CLAMPG(reverb_type, M_PRESET_COUNT);\n\n    const bool type_changed = m_ReverbType != reverb_type;\n    m_ReverbType = reverb_type;\n    if (!m_IsInitialised) {\n        return;\n    }\n\n    if (type_changed) {\n        M_DspReverb_Reset(&m_Reverb);\n    }\n\n    if (m_ReverbType != 0) {\n        M_DspReverb_SetParameters(&m_Reverb, &m_ReverbTypes[m_ReverbType - 1]);\n    }\n}\n\nuint8_t Audio_Reverb_GetType(void)\n{\n    return m_ReverbType;\n}\n\nvoid Audio_Reverb_Process(float *const dst_buffer, const size_t len)\n{\n    if (!m_IsInitialised) {\n        return;\n    }\n\n    if (m_ReverbType == 0) {\n        return;\n    }\n\n    const size_t samples = len / sizeof(float) / (size_t)AUDIO_WORKING_CHANNELS;\n    if (samples == 0) {\n        return;\n    }\n\n    if (AUDIO_WORKING_CHANNELS == 1) {\n        M_DspReverb_Process_1_to_1(&m_Reverb, dst_buffer, samples);\n    } else if (AUDIO_WORKING_CHANNELS == 2) {\n        M_DspReverb_Process_2_to_2(&m_Reverb, dst_buffer, samples);\n    }\n}\n"
  },
  {
    "path": "src/trx/av/audio_sample.c",
    "content": "#include <trx/av/audio_internal.h>\n\n#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/version.h>\n\n#include <SDL2/SDL_audio.h>\n#include <errno.h>\n#include <libavcodec/avcodec.h>\n#include <libavcodec/codec.h>\n#include <libavcodec/packet.h>\n#include <libavformat/avformat.h>\n#include <libavformat/avio.h>\n#include <libavutil/avutil.h>\n#include <libavutil/error.h>\n#include <libavutil/frame.h>\n#include <libavutil/mem.h>\n#include <libavutil/samplefmt.h>\n#include <libswresample/swresample.h>\n#include <math.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n\ntypedef struct {\n    struct {\n        int32_t format;\n        AVChannelLayout ch_layout;\n        int32_t sample_rate;\n    } src, dst;\n    SwrContext *ctx;\n    size_t working_buffer_size;\n    uint8_t *working_buffer;\n} M_SWR_CONTEXT;\n\ntypedef struct {\n    char *original_data;\n    size_t original_size;\n    float *sample_data;\n    int32_t channels;\n    int32_t num_samples;\n} AUDIO_SAMPLE;\n\ntypedef struct {\n    bool is_used;\n    bool is_looped;\n    bool is_playing;\n    float volume_l; // sample gain multiplier\n    float volume_r; // sample gain multiplier\n\n    float pitch;\n    // `volume`/`pan` come from the game layer (src/trx/game/sound/common.c).\n    // Despite the historic \"decibel\" naming, these values are not base-10\n    // centi-dB. They are a log2-based gain domain that the OG engine used to\n    // feed directly to DirectSound (-10000..0 style range).\n    //\n    // In TRX we keep the game-side math pristine and interpret these as:\n    //   TR1/2: log2(gain) * 1000\n    //   TR3:   DirectSound-style centi-dB in [-10000..0]\n    //\n    // `M_DecibelToMultiplier()` is the corresponding inverse transform:\n    //   TR1/2: gain = 2^(value/1000)\n    //   TR3:   gain = 10^(value/2000)\n    //\n    // This makes combining contributions (volume + pan) an additive operation\n    // in the game's log domain, while still producing a linear multiplier for\n    // the mixer.\n    int32_t volume;\n    int32_t pan;\n\n    // pitch shift means the same samples can be reused twice, hence float\n    float current_sample;\n\n    AUDIO_SAMPLE *sample;\n} AUDIO_SAMPLE_SOUND;\n\ntypedef struct {\n    const uint8_t *data;\n    const uint8_t *ptr;\n    int32_t size;\n    int32_t remaining;\n} AUDIO_AV_BUFFER;\n\nstatic int32_t m_LoadedSamplesCount = 0;\nstatic AUDIO_SAMPLE m_LoadedSamples[AUDIO_MAX_SAMPLES] = {};\nstatic AUDIO_SAMPLE_SOUND m_Samples[AUDIO_MAX_ACTIVE_SAMPLES] = {};\n\nstatic double M_DecibelToMultiplier(double db_gain)\n{\n    if (g_TRVersion < 3) {\n        // Legacy scale\n        return pow(2.0, db_gain / 600.0);\n    } else {\n        // DirectSound-style centi-dB domain: gain = 10^(centi_dB/2000).\n        return pow(10.0, db_gain / 2000.0);\n    }\n}\n\nstatic bool M_RecalculateChannelVolumes(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {\n        return false;\n    }\n\n    AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];\n    sound->volume_l = M_DecibelToMultiplier(\n        sound->volume - (sound->pan > 0 ? sound->pan : 0));\n    sound->volume_r = M_DecibelToMultiplier(\n        sound->volume + (sound->pan < 0 ? sound->pan : 0));\n\n    return true;\n}\n\nstatic int32_t M_ReadAVBuffer(void *opaque, uint8_t *dst, int32_t dst_size)\n{\n    ASSERT(opaque != nullptr);\n    ASSERT(dst != nullptr);\n    AUDIO_AV_BUFFER *src = opaque;\n    int32_t read = dst_size >= src->remaining ? src->remaining : dst_size;\n    if (!read) {\n        return AVERROR_EOF;\n    }\n    memcpy(dst, src->ptr, read);\n    src->ptr += read;\n    src->remaining -= read;\n    return read;\n}\n\nstatic int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence)\n{\n    ASSERT(opaque != nullptr);\n    AUDIO_AV_BUFFER *src = opaque;\n    if (whence & AVSEEK_SIZE) {\n        return src->size;\n    }\n    switch (whence) {\n    case SEEK_SET:\n        if (src->size - offset < 0) {\n            return AVERROR_EOF;\n        }\n        src->ptr = src->data + offset;\n        src->remaining = src->size - offset;\n        break;\n    case SEEK_CUR:\n        if (src->remaining - offset < 0) {\n            return AVERROR_EOF;\n        }\n        src->ptr += offset;\n        src->remaining -= offset;\n        break;\n    case SEEK_END:\n        if (src->size + offset < 0) {\n            return AVERROR_EOF;\n        }\n        src->ptr = src->data - offset;\n        src->remaining = src->size + offset;\n        break;\n    }\n    return src->ptr - src->data;\n}\n\nstatic int32_t M_OutputAudioFrame(\n    M_SWR_CONTEXT *const swr, AVFrame *const frame)\n{\n    // Determine the maximum number of output samples this call can produce,\n    // based on the current delay already inside the resampler plus the new\n    // input. Using av_rescale_rnd() keeps everything in integer domain and\n    // avoids cumulative rounding errors.\n    const int64_t delay = swr_get_delay(swr->ctx, swr->src.sample_rate);\n    const int32_t out_samples = (int32_t)av_rescale_rnd(\n        delay + frame->nb_samples, swr->dst.sample_rate, swr->src.sample_rate,\n        AV_ROUND_UP);\n    if (out_samples <= 0) {\n        return 0; // nothing to do\n    }\n\n    uint8_t *out_buffer = nullptr;\n    if (av_samples_alloc(\n            &out_buffer, nullptr, swr->dst.ch_layout.nb_channels, out_samples,\n            swr->dst.format, 1)\n        < 0) {\n        return AVERROR(ENOMEM);\n    }\n\n    // Convert – we do *not* drain the resampler here.\n    const int32_t converted = swr_convert(\n        swr->ctx, &out_buffer, out_samples, (const uint8_t **)frame->data,\n        frame->nb_samples);\n\n    if (converted < 0) {\n        av_freep(&out_buffer);\n        return converted; // propagate error\n    }\n\n    if (converted > 0) {\n        const int32_t out_buffer_size = av_samples_get_buffer_size(\n            nullptr, swr->dst.ch_layout.nb_channels, converted, swr->dst.format,\n            1);\n        if (out_buffer_size > 0) {\n            swr->working_buffer = Memory_Realloc(\n                swr->working_buffer,\n                swr->working_buffer_size + out_buffer_size);\n            memcpy(\n                swr->working_buffer + swr->working_buffer_size, out_buffer,\n                out_buffer_size);\n            swr->working_buffer_size += out_buffer_size;\n        }\n    }\n\n    av_freep(&out_buffer);\n    return 0;\n}\n\nstatic int32_t M_DecodePacket(\n    AVCodecContext *const dec, const AVPacket *const pkt, AVFrame *frame,\n    M_SWR_CONTEXT *const swr)\n{\n    // Submit the packet to the decoder\n    int32_t ret = avcodec_send_packet(dec, pkt);\n    if (ret < 0) {\n        LOG_ERROR(\n            \"Error submitting a packet for decoding (%s)\\n\", av_err2str(ret));\n        return ret;\n    }\n\n    // Get all the available frames from the decoder\n    while (ret >= 0) {\n        ret = avcodec_receive_frame(dec, frame);\n        if (ret < 0) {\n            // those two return values are special and mean there is no output\n            // frame available, but there were no errors during decoding\n            if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) {\n                return 0;\n            }\n            LOG_ERROR(\n                \"Error receiving a frame for decoding (%s)\\n\", av_err2str(ret));\n            return ret;\n        }\n\n        ret = M_OutputAudioFrame(swr, frame);\n        av_frame_unref(frame);\n    }\n\n    return ret;\n}\n\nstatic bool M_ConvertRawData(\n    const uint8_t *const original_data, const int32_t original_size,\n    const int32_t dst_sample_rate, const int32_t dst_format,\n    const int32_t dst_channel_count, uint8_t **const out_sample_data,\n    size_t *const out_size, size_t *const out_sample_count)\n{\n    bool result = false;\n\n    struct {\n        size_t read_buffer_size;\n        AVIOContext *avio_context;\n        AVStream *stream;\n        AVFormatContext *format_ctx;\n        const AVCodec *codec;\n        AVCodecContext *codec_ctx;\n        AVPacket *packet;\n        AVFrame *frame;\n    } av = {\n        .read_buffer_size = 8192,\n        .avio_context = nullptr,\n        .stream = nullptr,\n        .format_ctx = nullptr,\n        .codec = nullptr,\n        .codec_ctx = nullptr,\n        .packet = nullptr,\n        .frame = nullptr,\n    };\n\n    M_SWR_CONTEXT swr = {};\n    int32_t error_code;\n\n    uint8_t *const read_buffer = av_malloc(av.read_buffer_size);\n    if (read_buffer == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    AUDIO_AV_BUFFER av_buf = {\n        .data = original_data,\n        .ptr = original_data,\n        .size = original_size,\n        .remaining = original_size,\n    };\n\n    av.avio_context = avio_alloc_context(\n        read_buffer, av.read_buffer_size, 0, &av_buf, M_ReadAVBuffer, nullptr,\n        M_SeekAVBuffer);\n\n    av.format_ctx = avformat_alloc_context();\n    av.format_ctx->pb = av.avio_context;\n    error_code = avformat_open_input(&av.format_ctx, \"mem:\", nullptr, nullptr);\n    if (error_code != 0) {\n        goto cleanup;\n    }\n\n    error_code = avformat_find_stream_info(av.format_ctx, nullptr);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    av.stream = nullptr;\n    for (uint32_t i = 0; i < av.format_ctx->nb_streams; i++) {\n        AVStream *current_stream = av.format_ctx->streams[i];\n        if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {\n            av.stream = current_stream;\n            break;\n        }\n    }\n    if (av.stream == nullptr) {\n        error_code = AVERROR_STREAM_NOT_FOUND;\n        goto cleanup;\n    }\n\n    av.codec = avcodec_find_decoder(av.stream->codecpar->codec_id);\n    if (av.codec == nullptr) {\n        error_code = AVERROR_DEMUXER_NOT_FOUND;\n        goto cleanup;\n    }\n\n    av.codec_ctx = avcodec_alloc_context3(av.codec);\n    if (av.codec_ctx == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    error_code =\n        avcodec_parameters_to_context(av.codec_ctx, av.stream->codecpar);\n    if (error_code) {\n        goto cleanup;\n    }\n\n    error_code = avcodec_open2(av.codec_ctx, av.codec, nullptr);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    av.packet = av_packet_alloc();\n    if (av.packet == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    av.frame = av_frame_alloc();\n    if (av.frame == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    swr.src.sample_rate = av.codec_ctx->sample_rate;\n    swr.src.ch_layout = av.codec_ctx->ch_layout;\n    swr.src.format = av.codec_ctx->sample_fmt;\n    swr.dst.sample_rate = AUDIO_WORKING_RATE;\n    av_channel_layout_default(&swr.dst.ch_layout, dst_channel_count);\n    swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT);\n    swr_alloc_set_opts2(\n        &swr.ctx, &swr.dst.ch_layout, swr.dst.format, swr.dst.sample_rate,\n        &swr.src.ch_layout, swr.src.format, swr.src.sample_rate, 0, 0);\n    if (swr.ctx == nullptr) {\n        av_packet_unref(av.packet);\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    error_code = swr_init(swr.ctx);\n    if (error_code != 0) {\n        av_packet_unref(av.packet);\n        goto cleanup;\n    }\n\n    while ((error_code = av_read_frame(av.format_ctx, av.packet)) >= 0) {\n        M_DecodePacket(av.codec_ctx, av.packet, av.frame, &swr);\n        av_packet_unref(av.packet);\n        if (error_code < 0) {\n            break;\n        }\n    }\n\n    if (av.codec_ctx != nullptr) {\n        M_DecodePacket(av.codec_ctx, nullptr, av.frame, &swr);\n    }\n\n    if (error_code == AVERROR_EOF) {\n        error_code = 0;\n    } else if (error_code < 0) {\n        goto cleanup;\n    }\n\n    if (out_size != nullptr) {\n        *out_size = swr.working_buffer_size;\n    }\n    if (out_sample_count != nullptr) {\n        *out_sample_count = (int32_t)swr.working_buffer_size\n            / av_get_bytes_per_sample(swr.dst.format)\n            / swr.dst.ch_layout.nb_channels;\n    }\n    if (out_sample_data != nullptr) {\n        *out_sample_data = swr.working_buffer;\n    } else {\n        Memory_FreePointer(&swr.working_buffer);\n    }\n    result = true;\n\ncleanup:\n    if (error_code != 0) {\n        LOG_ERROR(\"Error while decoding sample: %s\", av_err2str(error_code));\n    }\n\n    if (!result) {\n        if (out_size != nullptr) {\n            *out_size = 0;\n        }\n        if (out_sample_count != nullptr) {\n            *out_sample_count = 0;\n        }\n        if (out_sample_data != nullptr) {\n            *out_sample_data = nullptr;\n        }\n        Memory_FreePointer(&swr.working_buffer);\n    }\n\n    if (swr.ctx) {\n        swr_free(&swr.ctx);\n    }\n    if (av.frame) {\n        av_frame_free(&av.frame);\n    }\n    if (av.packet) {\n        av_packet_free(&av.packet);\n    }\n    av.codec = nullptr;\n    if (av.codec_ctx) {\n        avcodec_free_context(&av.codec_ctx);\n    }\n    if (av.format_ctx) {\n        avformat_close_input(&av.format_ctx);\n    }\n    if (av.avio_context) {\n        av_freep(&av.avio_context->buffer);\n        avio_context_free(&av.avio_context);\n    }\n    return result;\n}\n\nstatic bool M_ConvertSample(const int32_t sample_id)\n{\n    ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount);\n    AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];\n    if (sample->sample_data != nullptr) {\n        return true;\n    }\n\n    size_t num_samples;\n    BENCHMARK benchmark = Benchmark_Start();\n\n    const bool result = M_ConvertRawData(\n        (uint8_t *)sample->original_data, sample->original_size,\n        AUDIO_WORKING_RATE, Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT), 1,\n        (uint8_t **)&sample->sample_data, nullptr, &num_samples);\n\n    char buffer[80];\n    sprintf(buffer, \"sample %d decoded\", sample_id);\n    Benchmark_End(&benchmark, buffer);\n\n    sample->channels = 1;\n    sample->num_samples = num_samples;\n    return result;\n}\n\nstatic bool M_IsOriginalDataDefined(const int32_t sample_id)\n{\n    ASSERT(sample_id >= 0 && sample_id < m_LoadedSamplesCount);\n    const AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];\n    return sample->original_data != nullptr;\n}\n\nvoid Audio_Sample_Init(void)\n{\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;\n         sound_id++) {\n        AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];\n        sound->is_used = false;\n        sound->is_playing = false;\n        sound->volume = 0.0f;\n        sound->pitch = 1.0f;\n        sound->pan = 0.0f;\n        sound->current_sample = 0.0f;\n        sound->sample = nullptr;\n    }\n}\n\nvoid Audio_Sample_Shutdown(void)\n{\n    Audio_Sample_CloseAll();\n    Audio_Sample_UnloadAll();\n}\n\nbool Audio_Sample_Unload(const int32_t sample_id)\n{\n    if (sample_id < 0 || sample_id >= AUDIO_MAX_SAMPLES) {\n        LOG_ERROR(\"Maximum allowed samples: %d\", AUDIO_MAX_SAMPLES);\n        return false;\n    }\n\n    bool result = false;\n    AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];\n    if (sample->sample_data == nullptr) {\n        LOG_ERROR(\"Sample %d is already unloaded\", sample_id);\n        return false;\n    }\n    Memory_FreePointer(&sample->sample_data);\n    Memory_FreePointer(&sample->original_data);\n    m_LoadedSamplesCount--;\n    return true;\n}\n\nbool Audio_Sample_UnloadAll(void)\n{\n    m_LoadedSamplesCount = 0;\n    for (int32_t i = 0; i < AUDIO_MAX_SAMPLES; i++) {\n        AUDIO_SAMPLE *const sample = &m_LoadedSamples[i];\n        Memory_FreePointer(&sample->sample_data);\n        Memory_FreePointer(&sample->original_data);\n    }\n    return true;\n}\n\nbool Audio_Sample_Load(\n    const int32_t sample_id, const char *const data, const size_t size)\n{\n    if (data == nullptr || size == 0) {\n        LOG_ERROR(\"Missing sample data %d\", sample_id);\n        return false;\n    }\n\n    if (!g_AudioDeviceID) {\n        LOG_ERROR(\"Unitialized audio device\");\n        return false;\n    }\n\n    if (sample_id < 0 || sample_id >= AUDIO_MAX_SAMPLES) {\n        LOG_ERROR(\"Maximum allowed samples: %d\", AUDIO_MAX_SAMPLES);\n        return false;\n    }\n\n    AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];\n    if (sample->original_data != nullptr) {\n        LOG_ERROR(\n            \"Sample %d is already loaded (trying to overwrite with %d bytes)\",\n            sample_id, size);\n        return false;\n    }\n\n    sample->original_data = Memory_Alloc(size);\n    sample->original_size = size;\n    memcpy(sample->original_data, data, size);\n    m_LoadedSamplesCount++;\n    return true;\n}\n\nint32_t Audio_Sample_Play(\n    int32_t sample_id, int32_t volume, float pitch, int32_t pan, bool is_looped)\n{\n    if (!g_AudioDeviceID) {\n        LOG_ERROR(\"audio device is unavailable\");\n        return false;\n    }\n\n    if (sample_id < 0 || sample_id >= m_LoadedSamplesCount) {\n        LOG_DEBUG(\"Invalid sample id: %d\", sample_id);\n        return AUDIO_NO_SOUND;\n    }\n\n    if (!M_IsOriginalDataDefined(sample_id)) {\n        return AUDIO_NO_SOUND;\n    }\n\n    int32_t result = AUDIO_NO_SOUND;\n\n    Audio_LockDevice();\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;\n         sound_id++) {\n        AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];\n        if (sound->is_used) {\n            continue;\n        }\n\n        M_ConvertSample(sample_id);\n\n        sound->is_used = true;\n        sound->is_playing = true;\n        sound->volume = volume;\n        sound->pitch = pitch;\n        sound->pan = pan;\n        sound->is_looped = is_looped;\n        sound->current_sample = 0.0f;\n        sound->sample = &m_LoadedSamples[sample_id];\n\n        M_RecalculateChannelVolumes(sound_id);\n\n        result = sound_id;\n        break;\n    }\n    Audio_UnlockDevice();\n\n    if (result == AUDIO_NO_SOUND) {\n        LOG_ERROR(\"All sample buffers are used!\");\n    }\n\n    return result;\n}\n\nbool Audio_Sample_IsPlaying(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {\n        return false;\n    }\n\n    return m_Samples[sound_id].is_playing;\n}\n\nbool Audio_Sample_Pause(int32_t sound_id)\n{\n    if (!g_AudioDeviceID) {\n        return false;\n    }\n\n    if (m_Samples[sound_id].is_playing) {\n        Audio_LockDevice();\n        m_Samples[sound_id].is_playing = false;\n        Audio_UnlockDevice();\n    }\n\n    return true;\n}\n\nbool Audio_Sample_PauseAll(void)\n{\n    if (!g_AudioDeviceID) {\n        return false;\n    }\n\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;\n         sound_id++) {\n        if (m_Samples[sound_id].is_used) {\n            Audio_Sample_Pause(sound_id);\n        }\n    }\n\n    return true;\n}\n\nbool Audio_Sample_Unpause(int32_t sound_id)\n{\n    if (!g_AudioDeviceID) {\n        return false;\n    }\n\n    if (!m_Samples[sound_id].is_playing) {\n        Audio_LockDevice();\n        m_Samples[sound_id].is_playing = true;\n        Audio_UnlockDevice();\n    }\n\n    return true;\n}\n\nbool Audio_Sample_UnpauseAll(void)\n{\n    if (!g_AudioDeviceID) {\n        return false;\n    }\n\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;\n         sound_id++) {\n        if (m_Samples[sound_id].is_used) {\n            Audio_Sample_Unpause(sound_id);\n        }\n    }\n\n    return true;\n}\n\nbool Audio_Sample_Close(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {\n        return false;\n    }\n\n    Audio_LockDevice();\n    m_Samples[sound_id].is_used = false;\n    m_Samples[sound_id].is_playing = false;\n    Audio_UnlockDevice();\n\n    return true;\n}\n\nbool Audio_Sample_CloseAll(void)\n{\n    if (!g_AudioDeviceID) {\n        return false;\n    }\n\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;\n         sound_id++) {\n        if (m_Samples[sound_id].is_used) {\n            Audio_Sample_Close(sound_id);\n        }\n    }\n\n    return true;\n}\n\nbool Audio_Sample_SetPan(int32_t sound_id, int32_t pan)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {\n        return false;\n    }\n\n    Audio_LockDevice();\n    m_Samples[sound_id].pan = pan;\n    M_RecalculateChannelVolumes(sound_id);\n    Audio_UnlockDevice();\n\n    return true;\n}\n\nbool Audio_Sample_SetVolume(int32_t sound_id, int32_t volume)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {\n        return false;\n    }\n\n    Audio_LockDevice();\n    m_Samples[sound_id].volume = volume;\n    M_RecalculateChannelVolumes(sound_id);\n    Audio_UnlockDevice();\n\n    return true;\n}\n\nbool Audio_Sample_SetPitch(int32_t sound_id, float pitch)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {\n        return false;\n    }\n\n    Audio_LockDevice();\n    m_Samples[sound_id].pitch = pitch;\n    M_RecalculateChannelVolumes(sound_id);\n    Audio_UnlockDevice();\n\n    return true;\n}\n\nvoid Audio_Sample_Mix(float *dst_buffer, size_t len)\n{\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;\n         sound_id++) {\n        AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];\n        if (!sound->is_playing) {\n            continue;\n        }\n\n        int32_t samples_requested =\n            len / sizeof(AUDIO_WORKING_FORMAT) / AUDIO_WORKING_CHANNELS;\n        float src_sample_idx = sound->current_sample;\n        const float *src_buffer = sound->sample->sample_data;\n        float *dst_ptr = dst_buffer;\n\n        while ((dst_ptr - dst_buffer) / AUDIO_WORKING_CHANNELS\n               < samples_requested) {\n\n            // because we handle 3d sound ourselves, downmix to mono\n            float src_sample = 0.0f;\n            for (int32_t i = 0; i < sound->sample->channels; i++) {\n                src_sample += src_buffer\n                    [(int32_t)src_sample_idx * sound->sample->channels + i];\n            }\n            src_sample /= (float)sound->sample->channels;\n\n            *dst_ptr++ += src_sample * sound->volume_l;\n            *dst_ptr++ += src_sample * sound->volume_r;\n            src_sample_idx += sound->pitch;\n\n            if ((int32_t)src_sample_idx >= sound->sample->num_samples) {\n                if (sound->is_looped) {\n                    src_sample_idx = 0.0f;\n                } else {\n                    break;\n                }\n            }\n        }\n\n        sound->current_sample = src_sample_idx;\n        if (sound->current_sample >= sound->sample->num_samples\n            && !sound->is_looped) {\n            Audio_Sample_Close(sound_id);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/av/audio_stream.c",
    "content": "#include <trx/av/audio_internal.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n\n#include <SDL2/SDL_audio.h>\n#include <SDL2/SDL_error.h>\n#include <errno.h>\n#include <libavcodec/avcodec.h>\n#include <libavcodec/codec.h>\n#include <libavcodec/packet.h>\n#include <libavformat/avformat.h>\n#include <libavformat/avio.h>\n#include <libavutil/avutil.h>\n#include <libavutil/error.h>\n#include <libavutil/frame.h>\n#include <libavutil/mem.h>\n#include <libavutil/rational.h>\n#include <libavutil/samplefmt.h>\n#include <libswresample/swresample.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n\n#define READ_BUFFER_SIZE                                                       \\\n    (AUDIO_SAMPLES * AUDIO_WORKING_CHANNELS * sizeof(AUDIO_WORKING_FORMAT))\n\ntypedef enum {\n    M_STREAM_SRC_NONE,\n    M_STREAM_SRC_MEMORY,\n} M_STREAM_SOURCE_TYPE;\n\ntypedef struct {\n    uint8_t *data;\n    size_t size;\n    size_t pos;\n} M_MEM_SOURCE;\n\ntypedef struct {\n    bool is_used;\n    bool is_playing;\n    bool is_read_done;\n    bool is_looped;\n    float volume;\n    double duration;\n    double decode_timestamp;\n    int64_t played_samples;\n\n    double start_at;\n    double stop_at;\n\n    void (*finish_callback)(int32_t sound_id, void *user_data);\n    void *finish_callback_user_data;\n\n    M_STREAM_SOURCE_TYPE src_type;\n    void *src;\n    uint8_t *avio_ctx_buffer;\n    AVIOContext *avio_ctx;\n\n    struct {\n        AVStream *stream;\n        AVFormatContext *format_ctx;\n        const AVCodec *codec;\n        AVCodecContext *codec_ctx;\n        AVPacket *packet;\n        AVFrame *frame;\n    } av;\n\n    struct {\n        struct {\n            int32_t format;\n            AVChannelLayout ch_layout;\n            int32_t sample_rate;\n        } src, dst;\n        SwrContext *ctx;\n    } swr;\n\n    struct {\n        SDL_AudioStream *stream;\n    } sdl;\n} AUDIO_STREAM_SOUND;\n\nextern SDL_AudioDeviceID g_AudioDeviceID;\n\nstatic AUDIO_STREAM_SOUND m_Streams[AUDIO_MAX_ACTIVE_STREAMS] = {};\nstatic float m_MixBuffer[AUDIO_SAMPLES * AUDIO_WORKING_CHANNELS] = {};\nstatic size_t m_DecodeBufferCapacity = 0;\nstatic float *m_DecodeBuffer = nullptr;\n\nstatic int32_t M_MemoryRead(\n    void *const opaque, uint8_t *const buf, const int32_t buf_size)\n{\n    ASSERT(opaque != nullptr);\n    ASSERT(buf != nullptr);\n\n    if (buf_size <= 0) {\n        return 0;\n    }\n\n    M_MEM_SOURCE *const s = opaque;\n    if (s->pos >= s->size) {\n        return AVERROR_EOF;\n    }\n\n    size_t to_copy = s->size - s->pos;\n    if (to_copy > (size_t)buf_size) {\n        to_copy = (size_t)buf_size;\n    }\n\n    memcpy(buf, s->data + s->pos, to_copy);\n    s->pos += to_copy;\n    return (int32_t)to_copy;\n}\n\nstatic int64_t M_MemorySeek(\n    void *const opaque, const int64_t offset, const int32_t whence)\n{\n    ASSERT(opaque != nullptr);\n\n    M_MEM_SOURCE *const s = opaque;\n    if ((whence & AVSEEK_SIZE) != 0) {\n        return (int64_t)s->size;\n    }\n\n    const int32_t base_whence = whence & ~AVSEEK_FORCE;\n    int64_t base;\n    if (base_whence == SEEK_SET) {\n        base = 0;\n    } else if (base_whence == SEEK_CUR) {\n        base = (int64_t)s->pos;\n    } else if (base_whence == SEEK_END) {\n        base = (int64_t)s->size;\n    } else {\n        return AVERROR(EINVAL);\n    }\n\n    int64_t new_pos = base + offset;\n    if (new_pos < 0) {\n        new_pos = 0;\n    }\n    if (new_pos > (int64_t)s->size) {\n        new_pos = (int64_t)s->size;\n    }\n    s->pos = (size_t)new_pos;\n    return new_pos;\n}\n\nstatic void M_ResetPlaybackState(\n    AUDIO_STREAM_SOUND *const stream, const double relative_timestamp)\n{\n    ASSERT(stream != nullptr);\n\n    const double clamped = MAX(0.0, relative_timestamp);\n    stream->played_samples = (int64_t)(clamped * (double)AUDIO_WORKING_RATE);\n}\n\nstatic void M_DiscardSDLStreamData(AUDIO_STREAM_SOUND *const stream)\n{\n    ASSERT(stream != nullptr);\n\n    if (stream->sdl.stream != nullptr) {\n        while (SDL_AudioStreamAvailable(stream->sdl.stream) > 0) {\n            const int32_t bytes_gotten = SDL_AudioStreamGet(\n                stream->sdl.stream, m_MixBuffer, READ_BUFFER_SIZE);\n            if (bytes_gotten <= 0) {\n                break;\n            }\n        }\n    }\n}\n\nstatic void M_SeekToStart(AUDIO_STREAM_SOUND *stream)\n{\n    ASSERT(stream != nullptr);\n\n    stream->decode_timestamp = stream->start_at;\n    M_ResetPlaybackState(stream, 0.0);\n    int32_t error_code;\n    if (stream->start_at <= 0.0) {\n        // reset to start of file\n        avio_seek(stream->av.format_ctx->pb, 0, SEEK_SET);\n        error_code = avformat_seek_file(\n            stream->av.format_ctx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME);\n    } else {\n        // seek to specific timestamp\n        AVFormatContext *const fmt = stream->av.format_ctx;\n        if (fmt->pb != nullptr && (fmt->pb->seekable & AVIO_SEEKABLE_NORMAL)) {\n            const int64_t ts = (int64_t)(stream->start_at * AV_TIME_BASE);\n            error_code = avformat_seek_file(\n                fmt, stream->av.stream->index, INT64_MIN, ts, INT64_MAX,\n                AVSEEK_FLAG_BACKWARD);\n        } else {\n            // fallback to stream-based seek\n            const double time_base_sec = av_q2d(stream->av.stream->time_base);\n            error_code = av_seek_frame(\n                fmt, stream->av.stream->index,\n                (int64_t)(stream->start_at / time_base_sec), AVSEEK_FLAG_ANY);\n        }\n    }\n    if (error_code < 0) {\n        LOG_ERROR(\n            \"seek failed for timestamp %f: %s\", stream->decode_timestamp,\n            av_err2str(error_code));\n    } else {\n        avcodec_flush_buffers(stream->av.codec_ctx);\n        M_DiscardSDLStreamData(stream);\n        stream->is_read_done = false;\n    }\n}\n\nstatic bool M_DecodeFrame(AUDIO_STREAM_SOUND *stream)\n{\n    ASSERT(stream != nullptr);\n\n    if (stream->stop_at > 0.0 && stream->decode_timestamp >= stream->stop_at) {\n        if (stream->is_looped) {\n            M_SeekToStart(stream);\n            return M_DecodeFrame(stream);\n        } else {\n            return false;\n        }\n    }\n\n    // av_read_frame() overwrites the packet; always unref any previous content.\n    av_packet_unref(stream->av.packet);\n    int32_t error_code =\n        av_read_frame(stream->av.format_ctx, stream->av.packet);\n\n    if (error_code == AVERROR_EOF && stream->is_looped) {\n        M_SeekToStart(stream);\n        return M_DecodeFrame(stream);\n    }\n\n    if (error_code == AVERROR_EOF) {\n        return false;\n    }\n\n    if (error_code < 0) {\n        LOG_ERROR(\n            \"error while decoding audio stream: %d (%s)\", error_code,\n            av_err2str(error_code));\n        return false;\n    }\n\n    if (stream->av.packet->stream_index != stream->av.stream->index) {\n        return true;\n    }\n\n    error_code = avcodec_send_packet(stream->av.codec_ctx, stream->av.packet);\n    if (error_code < 0) {\n        av_packet_unref(stream->av.packet);\n        LOG_ERROR(\n            \"Got an error when decoding frame: %s\", av_err2str(error_code));\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_InitialiseFromFormatContext(\n    int32_t sound_id, AVFormatContext *const fmt_ctx)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    bool ret = false;\n    Audio_LockDevice();\n\n    AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];\n    int32_t error_code = 0;\n\n    stream->av.format_ctx = fmt_ctx;\n\n    error_code = avformat_find_stream_info(stream->av.format_ctx, nullptr);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    stream->av.stream = nullptr;\n    for (uint32_t i = 0; i < stream->av.format_ctx->nb_streams; i++) {\n        AVStream *current_stream = stream->av.format_ctx->streams[i];\n        if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {\n            stream->av.stream = current_stream;\n            break;\n        }\n    }\n    if (!stream->av.stream) {\n        error_code = AVERROR_STREAM_NOT_FOUND;\n        goto cleanup;\n    }\n\n    stream->av.codec =\n        avcodec_find_decoder(stream->av.stream->codecpar->codec_id);\n    if (!stream->av.codec) {\n        error_code = AVERROR_DEMUXER_NOT_FOUND;\n        goto cleanup;\n    }\n\n    stream->av.codec_ctx = avcodec_alloc_context3(stream->av.codec);\n    if (!stream->av.codec_ctx) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    error_code = avcodec_parameters_to_context(\n        stream->av.codec_ctx, stream->av.stream->codecpar);\n    if (error_code != 0) {\n        goto cleanup;\n    }\n\n    error_code = avcodec_open2(stream->av.codec_ctx, stream->av.codec, nullptr);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    stream->av.packet = av_packet_alloc();\n    if (!stream->av.packet) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    stream->av.frame = av_frame_alloc();\n    if (!stream->av.frame) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    M_DecodeFrame(stream);\n\n    const int32_t sdl_channels = stream->av.codec_ctx->ch_layout.nb_channels;\n\n    stream->is_read_done = false;\n    stream->is_used = true;\n    stream->is_playing = true;\n    stream->is_looped = false;\n    stream->volume = 1.0f;\n    stream->decode_timestamp = 0.0;\n    stream->played_samples = 0;\n    stream->finish_callback = nullptr;\n    stream->finish_callback_user_data = nullptr;\n    stream->duration =\n        (double)stream->av.format_ctx->duration / (double)AV_TIME_BASE;\n    stream->start_at = -1.0; // negative value means unset\n    stream->stop_at = -1.0; // negative value means unset\n\n    stream->sdl.stream = SDL_NewAudioStream(\n        AUDIO_WORKING_FORMAT, sdl_channels, AUDIO_WORKING_RATE,\n        AUDIO_WORKING_FORMAT, sdl_channels, AUDIO_WORKING_RATE);\n    if (!stream->sdl.stream) {\n        LOG_ERROR(\"Failed to create SDL stream: %s\", SDL_GetError());\n        goto cleanup;\n    }\n\n    ret = true;\n\ncleanup:\n    if (error_code != 0) {\n        LOG_ERROR(\n            \"Error while opening audio stream: %s\", av_err2str(error_code));\n    }\n\n    if (!ret) {\n        Audio_Stream_Close(sound_id);\n    }\n\n    Audio_UnlockDevice();\n    return ret;\n}\n\nstatic bool M_EnqueueFrame(AUDIO_STREAM_SOUND *stream)\n{\n    ASSERT(stream != nullptr);\n\n    int32_t error_code;\n\n    if (!stream->swr.ctx) {\n        stream->swr.src.sample_rate = stream->av.codec_ctx->sample_rate;\n        stream->swr.src.ch_layout = stream->av.codec_ctx->ch_layout;\n        stream->swr.src.format = stream->av.codec_ctx->sample_fmt;\n        stream->swr.dst.sample_rate = AUDIO_WORKING_RATE;\n        av_channel_layout_default(\n            &stream->swr.dst.ch_layout, AUDIO_WORKING_CHANNELS);\n        stream->swr.dst.format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT);\n        swr_alloc_set_opts2(\n            &stream->swr.ctx, &stream->swr.dst.ch_layout,\n            stream->swr.dst.format, stream->swr.dst.sample_rate,\n            &stream->swr.src.ch_layout, stream->swr.src.format,\n            stream->swr.src.sample_rate, 0, 0);\n        if (!stream->swr.ctx) {\n            av_packet_unref(stream->av.packet);\n            error_code = AVERROR(ENOMEM);\n            goto cleanup;\n        }\n\n        error_code = swr_init(stream->swr.ctx);\n        if (error_code != 0) {\n            av_packet_unref(stream->av.packet);\n            goto cleanup;\n        }\n    }\n\n    while (1) {\n        error_code =\n            avcodec_receive_frame(stream->av.codec_ctx, stream->av.frame);\n        if (error_code == AVERROR(EAGAIN)) {\n            av_frame_unref(stream->av.frame);\n            error_code = 0;\n            break;\n        }\n\n        if (error_code < 0) {\n            av_frame_unref(stream->av.frame);\n            break;\n        }\n\n        uint8_t *out_buffer = nullptr;\n        const int32_t out_samples =\n            swr_get_out_samples(stream->swr.ctx, stream->av.frame->nb_samples);\n        av_samples_alloc(\n            &out_buffer, nullptr, stream->swr.dst.ch_layout.nb_channels,\n            out_samples, stream->swr.dst.format, 1);\n        int32_t resampled_size = swr_convert(\n            stream->swr.ctx, &out_buffer, out_samples,\n            (const uint8_t **)stream->av.frame->data,\n            stream->av.frame->nb_samples);\n\n        size_t out_pos = 0;\n        while (resampled_size > 0) {\n            const size_t out_buffer_size = av_samples_get_buffer_size(\n                nullptr, stream->swr.dst.ch_layout.nb_channels, resampled_size,\n                stream->swr.dst.format, 1);\n\n            if (out_pos + out_buffer_size > m_DecodeBufferCapacity) {\n                m_DecodeBufferCapacity = out_pos + out_buffer_size;\n                m_DecodeBuffer =\n                    Memory_Realloc(m_DecodeBuffer, m_DecodeBufferCapacity);\n            }\n            if (m_DecodeBuffer != nullptr && out_buffer != nullptr) {\n                memcpy(\n                    (uint8_t *)m_DecodeBuffer + out_pos, out_buffer,\n                    out_buffer_size);\n            }\n            out_pos += out_buffer_size;\n\n            resampled_size = swr_convert(\n                stream->swr.ctx, &out_buffer, out_samples, nullptr, 0);\n        }\n\n        if (SDL_AudioStreamPut(stream->sdl.stream, m_DecodeBuffer, out_pos)) {\n            LOG_ERROR(\"Got an error when decoding frame: %s\", SDL_GetError());\n            av_frame_unref(stream->av.frame);\n            break;\n        }\n\n        ASSERT(stream->av.format_ctx != nullptr);\n        ASSERT(stream->av.codec_ctx != nullptr);\n        ASSERT(stream->av.stream != nullptr);\n        double time_base_sec = av_q2d(stream->av.stream->time_base);\n        stream->decode_timestamp =\n            stream->av.frame->best_effort_timestamp * time_base_sec;\n        av_freep(&out_buffer);\n        av_frame_unref(stream->av.frame);\n    }\n\n    av_packet_unref(stream->av.packet);\n\ncleanup:\n    if (error_code > 0) {\n        LOG_ERROR(\n            \"Got an error when decoding frame: %d, %s\", error_code,\n            av_err2str(error_code));\n    }\n\n    return true;\n}\n\nstatic bool M_InitialiseFromPath(int32_t sound_id, const char *file_path)\n{\n    ASSERT(file_path != nullptr);\n\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    int32_t error_code = 0;\n    AVFormatContext *fmt_ctx = nullptr;\n    error_code = avformat_open_input(&fmt_ctx, file_path, nullptr, nullptr);\n    if (error_code != 0) {\n        LOG_ERROR(\n            \"Error while opening audio %s: %s\", file_path,\n            av_err2str(error_code));\n        return false;\n    }\n\n    return M_InitialiseFromFormatContext(sound_id, fmt_ctx);\n}\n\nstatic void M_Clear(AUDIO_STREAM_SOUND *stream)\n{\n    ASSERT(stream != nullptr);\n\n    stream->is_used = false;\n    stream->is_playing = false;\n    stream->is_read_done = true;\n    stream->is_looped = false;\n    stream->volume = 0.0f;\n    stream->duration = 0.0;\n    stream->decode_timestamp = 0.0;\n    stream->played_samples = 0;\n    stream->sdl.stream = nullptr;\n    stream->finish_callback = nullptr;\n    stream->finish_callback_user_data = nullptr;\n\n    stream->src_type = M_STREAM_SRC_NONE;\n    stream->src = nullptr;\n    stream->avio_ctx_buffer = nullptr;\n    stream->avio_ctx = nullptr;\n}\n\nvoid Audio_Stream_Init(void)\n{\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;\n         sound_id++) {\n        M_Clear(&m_Streams[sound_id]);\n    }\n}\n\nvoid Audio_Stream_Shutdown(void)\n{\n    Memory_FreePointer(&m_DecodeBuffer);\n    m_DecodeBufferCapacity = 0;\n    if (!g_AudioDeviceID) {\n        return;\n    }\n\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;\n         sound_id++) {\n        if (m_Streams[sound_id].is_used) {\n            Audio_Stream_Close(sound_id);\n        }\n    }\n}\n\nbool Audio_Stream_SyncTimestamp(const int32_t sound_id, const double timestamp)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id];\n    double drift = Audio_Stream_GetTimestamp(sound_id) - timestamp;\n    if (drift < 0) {\n        drift = -drift;\n    }\n    if (drift >= AUDIO_DRIFT_THRESHOLD) {\n        LOG_DEBUG(\"Detected audio drift: %f s\", drift);\n        Audio_Stream_SeekTimestamp(sound_id, timestamp);\n        return true;\n    }\n    return false;\n}\n\nbool Audio_Stream_Pause(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id];\n    if (stream->is_playing) {\n        Audio_LockDevice();\n        stream->is_playing = false;\n        Audio_UnlockDevice();\n    }\n\n    return true;\n}\n\nbool Audio_Stream_Unpause(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id];\n    if (!stream->is_playing) {\n        Audio_LockDevice();\n        stream->is_playing = true;\n        Audio_UnlockDevice();\n    }\n\n    return true;\n}\n\nbool Audio_Stream_SetPaused(const int32_t sound_id, const bool is_paused)\n{\n    return is_paused ? Audio_Stream_Pause(sound_id)\n                     : Audio_Stream_Unpause(sound_id);\n}\n\nint32_t Audio_Stream_CreateFromFile(const char *file_path)\n{\n    if (!g_AudioDeviceID) {\n        return AUDIO_NO_SOUND;\n    }\n\n    ASSERT(file_path != nullptr);\n\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;\n         sound_id++) {\n        AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];\n        if (stream->is_used) {\n            continue;\n        }\n\n        if (!M_InitialiseFromPath(sound_id, file_path)) {\n            return AUDIO_NO_SOUND;\n        }\n\n        return sound_id;\n    }\n\n    return AUDIO_NO_SOUND;\n}\n\nint32_t Audio_Stream_CreateFromMemory(uint8_t *const data, const size_t size)\n{\n    if (!g_AudioDeviceID) {\n        return AUDIO_NO_SOUND;\n    }\n\n    ASSERT(data != nullptr);\n    ASSERT(size != 0);\n\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;\n         sound_id++) {\n        AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];\n        if (stream->is_used) {\n            continue;\n        }\n\n        M_MEM_SOURCE *const src = Memory_Alloc(sizeof(M_MEM_SOURCE));\n        *src = (M_MEM_SOURCE) {\n            .data = data,\n            .size = size,\n            .pos = 0,\n        };\n\n        stream->src_type = M_STREAM_SRC_MEMORY;\n        stream->src = src;\n\n        stream->avio_ctx_buffer = av_malloc(4096);\n        if (stream->avio_ctx_buffer == nullptr) {\n            Audio_Stream_Close(sound_id);\n            return AUDIO_NO_SOUND;\n        }\n\n        stream->avio_ctx = avio_alloc_context(\n            stream->avio_ctx_buffer, 4096, 0, src, M_MemoryRead, nullptr,\n            M_MemorySeek);\n        if (stream->avio_ctx == nullptr) {\n            Audio_Stream_Close(sound_id);\n            return AUDIO_NO_SOUND;\n        }\n\n        stream->av.format_ctx = avformat_alloc_context();\n        if (stream->av.format_ctx == nullptr) {\n            Audio_Stream_Close(sound_id);\n            return AUDIO_NO_SOUND;\n        }\n        stream->av.format_ctx->pb = stream->avio_ctx;\n        stream->av.format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO;\n\n        int32_t error_code = avformat_open_input(\n            &stream->av.format_ctx, nullptr, nullptr, nullptr);\n        if (error_code != 0) {\n            LOG_ERROR(\n                \"Error while opening audio memory stream: %s\",\n                av_err2str(error_code));\n            Audio_Stream_Close(sound_id);\n            return AUDIO_NO_SOUND;\n        }\n\n        if (!M_InitialiseFromFormatContext(sound_id, stream->av.format_ctx)) {\n            Audio_Stream_Close(sound_id);\n            return AUDIO_NO_SOUND;\n        }\n\n        return sound_id;\n    }\n\n    return AUDIO_NO_SOUND;\n}\n\nbool Audio_Stream_Close(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    Audio_LockDevice();\n\n    AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];\n\n    if (stream->av.codec_ctx) {\n        // XXX: potential libav bug - avcodec_close should free this info\n        if (stream->av.codec_ctx->extradata != nullptr) {\n            av_freep(&stream->av.codec_ctx->extradata);\n        }\n\n        avcodec_free_context(&stream->av.codec_ctx);\n        stream->av.codec_ctx = nullptr;\n    }\n\n    if (stream->av.format_ctx) {\n        avformat_close_input(&stream->av.format_ctx);\n        stream->av.format_ctx = nullptr;\n    }\n\n    if (stream->avio_ctx != nullptr) {\n        av_freep(&stream->avio_ctx->buffer);\n        avio_context_free(&stream->avio_ctx);\n        stream->avio_ctx = nullptr;\n    } else if (stream->avio_ctx_buffer != nullptr) {\n        av_freep(&stream->avio_ctx_buffer);\n    }\n    stream->avio_ctx_buffer = nullptr;\n\n    if (stream->src_type == M_STREAM_SRC_MEMORY && stream->src != nullptr) {\n        M_MEM_SOURCE *const src = stream->src;\n        Memory_FreePointer(&src->data);\n        Memory_FreePointer(&stream->src);\n    }\n\n    if (stream->swr.ctx) {\n        swr_free(&stream->swr.ctx);\n    }\n\n    if (stream->av.frame) {\n        av_frame_free(&stream->av.frame);\n        stream->av.frame = nullptr;\n    }\n\n    if (stream->av.packet) {\n        av_packet_free(&stream->av.packet);\n        stream->av.packet = nullptr;\n    }\n\n    stream->av.stream = nullptr;\n    stream->av.codec = nullptr;\n\n    if (stream->sdl.stream) {\n        SDL_FreeAudioStream(stream->sdl.stream);\n        stream->sdl.stream = nullptr;\n    }\n\n    void (*finish_callback)(int32_t, void *) = stream->finish_callback;\n    void *finish_callback_user_data = stream->finish_callback_user_data;\n\n    M_Clear(stream);\n\n    Audio_UnlockDevice();\n\n    if (finish_callback) {\n        finish_callback(sound_id, finish_callback_user_data);\n    }\n\n    return true;\n}\n\nbool Audio_Stream_SetVolume(int32_t sound_id, float volume)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    m_Streams[sound_id].volume = volume;\n\n    return true;\n}\n\nbool Audio_Stream_IsLooped(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    return m_Streams[sound_id].is_looped;\n}\n\nbool Audio_Stream_SetIsLooped(int32_t sound_id, bool is_looped)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    m_Streams[sound_id].is_looped = is_looped;\n\n    return true;\n}\n\nbool Audio_Stream_SetFinishCallback(\n    int32_t sound_id, void (*callback)(int32_t sound_id, void *user_data),\n    void *user_data)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    m_Streams[sound_id].finish_callback = callback;\n    m_Streams[sound_id].finish_callback_user_data = user_data;\n\n    return true;\n}\n\nvoid Audio_Stream_Mix(float *dst_buffer, size_t len)\n{\n    for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;\n         sound_id++) {\n        AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id];\n        if (!stream->is_playing) {\n            continue;\n        }\n\n        while ((SDL_AudioStreamAvailable(stream->sdl.stream) < (int32_t)len)\n               && !stream->is_read_done) {\n            if (M_DecodeFrame(stream)) {\n                M_EnqueueFrame(stream);\n            } else {\n                stream->is_read_done = true;\n            }\n        }\n\n        memset(m_MixBuffer, 0, READ_BUFFER_SIZE);\n        int32_t bytes_gotten = SDL_AudioStreamGet(\n            stream->sdl.stream, m_MixBuffer, READ_BUFFER_SIZE);\n        if (bytes_gotten < 0) {\n            LOG_ERROR(\"Error reading from sdl.stream: %s\", SDL_GetError());\n            stream->is_playing = false;\n            stream->is_used = false;\n            stream->is_read_done = true;\n        } else if (bytes_gotten == 0) {\n            // legit end of stream. looping is handled in\n            // M_DecodeFrame\n            stream->is_playing = false;\n            stream->is_used = false;\n            stream->is_read_done = true;\n        } else {\n            int32_t samples_gotten = bytes_gotten\n                / (AUDIO_WORKING_CHANNELS * sizeof(AUDIO_WORKING_FORMAT));\n            stream->played_samples += (int64_t)samples_gotten;\n\n            const float *src_ptr = &m_MixBuffer[0];\n            float *dst_ptr = dst_buffer;\n\n            for (int32_t s = 0; s < samples_gotten; s++) {\n                for (int32_t c = 0; c < AUDIO_WORKING_CHANNELS; c++) {\n                    *dst_ptr++ += *src_ptr++ * stream->volume;\n                }\n            }\n        }\n\n        if (!stream->is_used) {\n            Audio_Stream_Close(sound_id);\n        }\n    }\n}\n\ndouble Audio_Stream_GetTimestamp(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return -1.0;\n    }\n\n    double timestamp = -1.0;\n    AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];\n\n    if (stream->duration > 0.0) {\n        Audio_LockDevice();\n        timestamp = (double)stream->played_samples / (double)AUDIO_WORKING_RATE;\n        Audio_UnlockDevice();\n    }\n\n    return timestamp;\n}\n\ndouble Audio_Stream_GetDuration(int32_t sound_id)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return -1.0;\n    }\n\n    Audio_LockDevice();\n    AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];\n    double duration = stream->duration;\n    Audio_UnlockDevice();\n    return duration;\n}\n\nbool Audio_Stream_SeekTimestamp(const int32_t sound_id, const double timestamp)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    AUDIO_STREAM_SOUND *const stream = &m_Streams[sound_id];\n    if (!stream->is_used) {\n        return false;\n    }\n\n    Audio_LockDevice();\n    const double time_base_sec = av_q2d(stream->av.stream->time_base);\n    if (time_base_sec <= 0.0) {\n        LOG_ERROR(\n            \"Audio_Stream_SeekTimestamp: invalid time_base %f\", time_base_sec);\n        Audio_UnlockDevice();\n        return false;\n    }\n\n    const int32_t stream_index = stream->av.stream->index;\n    const int64_t seek_target =\n        (int64_t)((MAX(0.0f, stream->start_at) + timestamp) / time_base_sec);\n    const int32_t error_code = av_seek_frame(\n        stream->av.format_ctx, stream_index, seek_target, AVSEEK_FLAG_ANY);\n    if (error_code < 0) {\n        LOG_ERROR(\n            \"seek failed for timestamp %f: %s\", timestamp,\n            av_err2str(error_code));\n        Audio_UnlockDevice();\n        return false;\n    }\n\n    avcodec_flush_buffers(stream->av.codec_ctx);\n    if (stream->sdl.stream != nullptr) {\n        M_DiscardSDLStreamData(stream);\n    }\n\n    stream->decode_timestamp = timestamp + MAX(stream->start_at, 0.0f);\n    M_ResetPlaybackState(stream, timestamp);\n    stream->is_read_done = false;\n\n    Audio_UnlockDevice();\n    return true;\n}\n\nbool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    m_Streams[sound_id].start_at = timestamp;\n    return true;\n}\n\nbool Audio_Stream_SetStopTimestamp(int32_t sound_id, double timestamp)\n{\n    if (!g_AudioDeviceID || sound_id < 0\n        || sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {\n        return false;\n    }\n\n    m_Streams[sound_id].stop_at = timestamp;\n    return true;\n}\n"
  },
  {
    "path": "src/trx/av/image.c",
    "content": "#include <trx/av/image.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n\n#include <errno.h>\n#include <libavcodec/avcodec.h>\n#include <libavcodec/codec.h>\n#include <libavcodec/codec_id.h>\n#include <libavcodec/packet.h>\n#include <libavformat/avformat.h>\n#include <libavutil/avutil.h>\n#include <libavutil/error.h>\n#include <libavutil/frame.h>\n#include <libavutil/imgutils.h>\n#include <libavutil/mem.h>\n#include <libavutil/pixfmt.h>\n#include <libavutil/rational.h>\n#include <libswscale/swscale.h>\n#include <stdint.h>\n#include <string.h>\n\ntypedef struct {\n    struct {\n        int32_t x;\n        int32_t y;\n        int32_t width;\n        int32_t height;\n    } src, dst;\n} IMAGE_BLIT;\n\ntypedef struct {\n    int error_code;\n    AVFormatContext *format_ctx;\n    AVCodecContext *codec_ctx;\n    const AVCodec *codec;\n    AVFrame *frame;\n    AVPacket *packet;\n} IMAGE_READER_CONTEXT;\n\nstatic void M_Free(IMAGE_READER_CONTEXT *const ctx)\n{\n    if (ctx->packet != nullptr) {\n        av_packet_free(&ctx->packet);\n    }\n\n    if (ctx->frame != nullptr) {\n        av_frame_free(&ctx->frame);\n    }\n\n    if (ctx->codec_ctx != nullptr) {\n        avcodec_free_context(&ctx->codec_ctx);\n    }\n\n    if (ctx->format_ctx != nullptr) {\n        avformat_close_input(&ctx->format_ctx);\n    }\n}\n\nstatic bool M_Init(const char *const path, IMAGE_READER_CONTEXT *const ctx)\n{\n    ASSERT(ctx != nullptr);\n    ctx->format_ctx = nullptr;\n    ctx->codec = nullptr;\n    ctx->codec_ctx = nullptr;\n    ctx->frame = nullptr;\n    ctx->packet = nullptr;\n\n    int32_t error_code = 0;\n    error_code = avformat_open_input(&ctx->format_ctx, path, nullptr, nullptr);\n\n    if (error_code != 0) {\n        goto finish;\n    }\n\n#if 0\n    error_code = avformat_find_stream_info(format_ctx, nullptr);\n    if (error_code < 0) {\n        goto finish;\n    }\n#endif\n\n    AVStream *video_stream = nullptr;\n    for (unsigned int i = 0; i < ctx->format_ctx->nb_streams; i++) {\n        AVStream *current_stream = ctx->format_ctx->streams[i];\n        if (current_stream->codecpar == nullptr) {\n            continue;\n        }\n        if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {\n            video_stream = current_stream;\n            break;\n        }\n    }\n    if (video_stream == nullptr) {\n        error_code = AVERROR_STREAM_NOT_FOUND;\n        goto finish;\n    }\n\n    ctx->codec = avcodec_find_decoder(video_stream->codecpar->codec_id);\n    if (ctx->codec == nullptr) {\n        error_code = AVERROR_DEMUXER_NOT_FOUND;\n        goto finish;\n    }\n\n    ctx->codec_ctx = avcodec_alloc_context3(ctx->codec);\n    if (ctx->codec_ctx == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto finish;\n    }\n\n    error_code =\n        avcodec_parameters_to_context(ctx->codec_ctx, video_stream->codecpar);\n    if (error_code) {\n        goto finish;\n    }\n\n#if 0\n    ctx->codec_ctx->thread_count = 0;\n    if (ctx->codec->capabilities & AV_CODEC_CAP_FRAME_THREADS)\n        ctx->codec_ctx->thread_type = FF_THREAD_FRAME;\n    else if (ctx->codec->capabilities & AV_CODEC_CAP_SLICE_THREADS)\n        ctx->codec_ctx->thread_type = FF_THREAD_SLICE;\n    else\n        ctx->codec_ctx->thread_count = 1; //don't use multithreading\n#endif\n\n    error_code = avcodec_open2(ctx->codec_ctx, ctx->codec, nullptr);\n    if (error_code < 0) {\n        goto finish;\n    }\n\n    ctx->packet = av_packet_alloc();\n    error_code = av_read_frame(ctx->format_ctx, ctx->packet);\n    if (error_code < 0) {\n        goto finish;\n    }\n\n    error_code = avcodec_send_packet(ctx->codec_ctx, ctx->packet);\n    if (error_code < 0) {\n        goto finish;\n    }\n\n    ctx->frame = av_frame_alloc();\n    if (ctx->frame == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto finish;\n    }\n\n    error_code = avcodec_receive_frame(ctx->codec_ctx, ctx->frame);\n    if (error_code < 0) {\n        goto finish;\n    }\n    error_code = 0;\n\nfinish:\n    if (error_code != 0) {\n        LOG_ERROR(\n            \"Error while opening image %s: %s\", path, av_err2str(error_code));\n        M_Free(ctx);\n        return false;\n    }\n\n    return true;\n}\n\nstatic IMAGE_BLIT M_GetBlit(\n    const int32_t source_width, const int32_t source_height,\n    const int32_t target_width, const int32_t target_height,\n    IMAGE_FIT_MODE fit_mode)\n{\n    const float source_ratio = source_width / (float)source_height;\n    const float target_ratio = target_width / (float)target_height;\n\n    if (fit_mode == IMAGE_FIT_SMART) {\n        const float ar_diff =\n            (source_ratio > target_ratio ? source_ratio / target_ratio\n                                         : target_ratio / source_ratio)\n            - 1.0f;\n        if (ar_diff <= 0.1f) {\n            // if the difference between aspect ratios is under 10%, just\n            // stretch it\n            fit_mode = IMAGE_FIT_STRETCH;\n        } else if (source_ratio <= target_ratio) {\n            // if the viewport is too wide, center the image\n            fit_mode = IMAGE_FIT_LETTERBOX;\n        } else {\n            // if the image is too wide, crop the image\n            fit_mode = IMAGE_FIT_CROP;\n        }\n    }\n\n    IMAGE_BLIT blit;\n\n    switch (fit_mode) {\n    case IMAGE_FIT_STRETCH:\n        blit.src.width = source_width;\n        blit.src.height = source_height;\n        blit.src.x = 0;\n        blit.src.y = 0;\n\n        blit.dst.width = target_width;\n        blit.dst.height = target_height;\n        blit.dst.x = 0;\n        blit.dst.y = 0;\n        break;\n\n    case IMAGE_FIT_CROP:\n        blit.src.width = source_ratio < target_ratio\n            ? source_width\n            : source_height * target_ratio;\n        blit.src.height = source_ratio < target_ratio\n            ? source_width / target_ratio\n            : source_height;\n        blit.src.x = (source_width - blit.src.width) / 2;\n        blit.src.y = (source_height - blit.src.height) / 2;\n\n        blit.dst.width = target_width;\n        blit.dst.height = target_height;\n        blit.dst.x = 0;\n        blit.dst.y = 0;\n        break;\n\n    case IMAGE_FIT_LETTERBOX:\n        blit.src.width = source_width;\n        blit.src.height = source_height;\n        blit.src.x = 0;\n        blit.src.y = 0;\n\n        blit.dst.width = (source_ratio > target_ratio)\n            ? target_width\n            : target_height * source_ratio;\n        blit.dst.height = (source_ratio > target_ratio)\n            ? target_width / source_ratio\n            : target_height;\n        blit.dst.x = (target_width - blit.dst.width) / 2;\n        blit.dst.y = (target_height - blit.dst.height) / 2;\n        break;\n\n    default:\n        ASSERT_FAIL();\n        break;\n    }\n    return blit;\n}\n\nstatic IMAGE *M_ConstructImage(\n    IMAGE_READER_CONTEXT *const ctx, const int32_t target_width,\n    const int32_t target_height, IMAGE_FIT_MODE fit_mode)\n{\n    ASSERT(ctx != nullptr);\n    ASSERT(target_width > 0);\n    ASSERT(target_height > 0);\n\n    IMAGE_BLIT blit = M_GetBlit(\n        ctx->frame->width, ctx->frame->height, target_width, target_height,\n        fit_mode);\n\n    if (blit.src.y != 0 || blit.src.x != 0) {\n        ctx->frame->crop_top = blit.src.y;\n        ctx->frame->crop_left = blit.src.x;\n        av_frame_apply_cropping(ctx->frame, AV_FRAME_CROP_UNALIGNED);\n    }\n\n    struct SwsContext *const sws_ctx = sws_getContext(\n        blit.src.width, blit.src.height, ctx->frame->format, blit.dst.width,\n        blit.dst.height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr,\n        nullptr);\n    if (sws_ctx == nullptr) {\n        LOG_ERROR(\"Failed to get SWS context\");\n        return nullptr;\n    }\n\n    IMAGE *const target_image = Image_Create(target_width, target_height);\n\n    uint8_t *dst_planes[4] = { (uint8_t *)target_image->data\n                                   + (blit.dst.y * target_image->width\n                                      + blit.dst.x)\n                                       * sizeof(IMAGE_PIXEL),\n                               nullptr, nullptr, nullptr };\n    int dst_linesize[4] = { target_image->width * sizeof(IMAGE_PIXEL), 0, 0,\n                            0 };\n\n    sws_scale(\n        sws_ctx, (const uint8_t *const *)ctx->frame->data, ctx->frame->linesize,\n        0, blit.src.height, dst_planes, dst_linesize);\n\n    sws_freeContext(sws_ctx);\n    return target_image;\n}\n\nIMAGE *Image_Create(const int width, const int height)\n{\n    IMAGE *image = Memory_Alloc(sizeof(IMAGE));\n    image->width = width;\n    image->height = height;\n    image->data = Memory_Alloc(width * height * sizeof(IMAGE_PIXEL));\n    return image;\n}\n\nIMAGE *Image_CreateFromFile(const char *const path)\n{\n    ASSERT(path != nullptr);\n\n    IMAGE_READER_CONTEXT ctx;\n    if (!M_Init(path, &ctx)) {\n        return nullptr;\n    }\n\n    IMAGE *target_image = M_ConstructImage(\n        &ctx, ctx.frame->width, ctx.frame->height, IMAGE_FIT_STRETCH);\n\n    M_Free(&ctx);\n\n    return target_image;\n}\n\nIMAGE *Image_CreateFromFileInto(\n    const char *const path, const int32_t target_width,\n    const int32_t target_height, const IMAGE_FIT_MODE fit_mode)\n{\n    ASSERT(path != nullptr);\n\n    IMAGE_READER_CONTEXT ctx;\n    if (!M_Init(path, &ctx)) {\n        return nullptr;\n    }\n\n    IMAGE *target_image =\n        M_ConstructImage(&ctx, target_width, target_height, fit_mode);\n\n    M_Free(&ctx);\n\n    return target_image;\n}\n\nbool Image_SaveToFile(const IMAGE *const image, const char *const path)\n{\n    ASSERT(image != nullptr);\n    ASSERT(path != nullptr);\n\n    bool result = false;\n\n    int error_code = 0;\n    const AVCodec *codec = nullptr;\n    AVCodecContext *codec_ctx = nullptr;\n    AVFrame *frame = nullptr;\n    AVPacket *packet = nullptr;\n    struct SwsContext *sws_ctx = nullptr;\n    MYFILE *fp = nullptr;\n\n    enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_RGB24;\n    enum AVPixelFormat dst_pix_fmt;\n    enum AVCodecID codec_id;\n\n    if (strstr(path, \".jpg\")) {\n        dst_pix_fmt = AV_PIX_FMT_YUVJ420P;\n        codec_id = AV_CODEC_ID_MJPEG;\n    } else if (strstr(path, \".png\")) {\n        dst_pix_fmt = AV_PIX_FMT_RGB24;\n        codec_id = AV_CODEC_ID_PNG;\n    } else {\n        LOG_ERROR(\"Cannot determine image format based on path '%s'\", path);\n        goto cleanup;\n    }\n\n    File_EnsureParentDirectories(path);\n    fp = File_Open(path, FILE_OPEN_WRITE);\n    if (fp == nullptr) {\n        LOG_ERROR(\"Cannot create image file: %s\", path);\n        goto cleanup;\n    }\n\n    codec = avcodec_find_encoder(codec_id);\n    if (codec == nullptr) {\n        error_code = AVERROR_MUXER_NOT_FOUND;\n        goto cleanup;\n    }\n\n    codec_ctx = avcodec_alloc_context3(codec);\n    if (codec_ctx == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n\n    codec_ctx->bit_rate = 400000;\n    codec_ctx->width = image->width;\n    codec_ctx->height = image->height;\n    codec_ctx->time_base = (AVRational) { 1, 25 };\n    codec_ctx->pix_fmt = dst_pix_fmt;\n\n    if (codec_id == AV_CODEC_ID_MJPEG) {\n        // 9 JPEG quality\n        codec_ctx->flags |= AV_CODEC_FLAG_QSCALE;\n        codec_ctx->global_quality = FF_QP2LAMBDA * 9;\n    }\n\n    error_code = avcodec_open2(codec_ctx, codec, nullptr);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    frame = av_frame_alloc();\n    if (frame == nullptr) {\n        error_code = AVERROR(ENOMEM);\n        goto cleanup;\n    }\n    frame->format = codec_ctx->pix_fmt;\n    frame->width = codec_ctx->width;\n    frame->height = codec_ctx->height;\n    frame->pts = 0;\n\n    error_code = av_image_alloc(\n        frame->data, frame->linesize, codec_ctx->width, codec_ctx->height,\n        codec_ctx->pix_fmt, 32);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    packet = av_packet_alloc();\n\n    sws_ctx = sws_getContext(\n        image->width, image->height, src_pix_fmt, frame->width, frame->height,\n        dst_pix_fmt, SWS_BILINEAR, nullptr, nullptr, nullptr);\n\n    if (sws_ctx == nullptr) {\n        LOG_ERROR(\"Failed to get SWS context\");\n        error_code = AVERROR_EXTERNAL;\n        goto cleanup;\n    }\n\n    uint8_t *src_planes[4];\n    int src_linesize[4];\n    av_image_fill_arrays(\n        src_planes, src_linesize, (const uint8_t *)image->data, src_pix_fmt,\n        image->width, image->height, 1);\n\n    sws_scale(\n        sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,\n        image->height, frame->data, frame->linesize);\n\n    error_code = avcodec_send_frame(codec_ctx, frame);\n    if (error_code < 0) {\n        goto cleanup;\n    }\n\n    while (error_code >= 0) {\n        error_code = avcodec_receive_packet(codec_ctx, packet);\n        if (error_code == AVERROR(EAGAIN) || error_code == AVERROR_EOF) {\n            error_code = 0;\n            break;\n        }\n        if (error_code < 0) {\n            goto cleanup;\n        }\n\n        File_WriteData(fp, packet->data, packet->size);\n        av_packet_unref(packet);\n    }\n\ncleanup:\n    if (error_code) {\n        LOG_ERROR(\n            \"Error while saving image %s: %s\", path, av_err2str(error_code));\n    } else {\n        result = true;\n    }\n\n    if (fp) {\n        File_Close(fp);\n        fp = nullptr;\n    }\n\n    if (sws_ctx) {\n        sws_freeContext(sws_ctx);\n    }\n\n    if (packet) {\n        av_packet_free(&packet);\n    }\n\n    if (codec) {\n        avcodec_free_context(&codec_ctx);\n    }\n\n    if (frame) {\n        av_freep(&frame->data[0]);\n        av_frame_free(&frame);\n    }\n\n    return result;\n}\n\nIMAGE *Image_Scale(\n    const IMAGE *const source_image, size_t target_width, size_t target_height,\n    IMAGE_FIT_MODE fit_mode)\n{\n    ASSERT(source_image != nullptr);\n    ASSERT(source_image->data != nullptr);\n    ASSERT(target_width > 0);\n    ASSERT(target_height > 0);\n\n    IMAGE_BLIT blit = M_GetBlit(\n        source_image->width, source_image->height, target_width, target_height,\n        fit_mode);\n\n    struct SwsContext *const sws_ctx = sws_getContext(\n        blit.src.width, blit.src.height, AV_PIX_FMT_RGB24, blit.dst.width,\n        blit.dst.height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr,\n        nullptr);\n    if (sws_ctx == nullptr) {\n        LOG_ERROR(\"Failed to get SWS context\");\n        return nullptr;\n    }\n\n    IMAGE *const target_image = Image_Create(target_width, target_height);\n\n    uint8_t *src_planes[4] = { (uint8_t *)source_image->data\n                                   + (blit.src.y * source_image->width\n                                      + blit.src.x)\n                                       * sizeof(IMAGE_PIXEL),\n                               nullptr, nullptr, nullptr };\n    int src_linesize[4] = { source_image->width * sizeof(IMAGE_PIXEL), 0, 0,\n                            0 };\n\n    uint8_t *dst_planes[4] = { (uint8_t *)target_image->data\n                                   + (blit.dst.y * target_image->width\n                                      + blit.dst.x)\n                                       * sizeof(IMAGE_PIXEL),\n                               nullptr, nullptr, nullptr };\n    int dst_linesize[4] = { target_image->width * sizeof(IMAGE_PIXEL), 0, 0,\n                            0 };\n\n    sws_scale(\n        sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,\n        blit.src.height, (uint8_t *const *)dst_planes, dst_linesize);\n\n    sws_freeContext(sws_ctx);\n    return target_image;\n}\n\nvoid Image_Free(IMAGE *image)\n{\n    if (image) {\n        Memory_FreePointer(&image->data);\n    }\n    Memory_FreePointer(&image);\n}\n"
  },
  {
    "path": "src/trx/av/image.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef struct {\n    uint8_t r;\n    uint8_t g;\n    uint8_t b;\n} IMAGE_PIXEL;\n\ntypedef struct {\n    int32_t width;\n    int32_t height;\n    IMAGE_PIXEL *data;\n} IMAGE;\n\ntypedef enum {\n    IMAGE_FIT_STRETCH,\n    IMAGE_FIT_CROP,\n    IMAGE_FIT_LETTERBOX,\n    IMAGE_FIT_SMART,\n} IMAGE_FIT_MODE;\n\nIMAGE *Image_Create(int width, int height);\n\nIMAGE *Image_CreateFromFile(const char *path);\n\nIMAGE *Image_CreateFromFileInto(\n    const char *path, int32_t target_width, int32_t target_height,\n    IMAGE_FIT_MODE fit_mode);\n\nvoid Image_Free(IMAGE *image);\n\nbool Image_GetFileInfo(const char *path, int32_t *width, int32_t *height);\n\nbool Image_SaveToFile(const IMAGE *image, const char *path);\n\nIMAGE *Image_Scale(\n    const IMAGE *source_image, size_t target_width, size_t target_height,\n    IMAGE_FIT_MODE fit_mode);\n"
  },
  {
    "path": "src/trx/av/video.c",
    "content": "/*\n * Copyright (c) 2003 Fabrice Bellard\n *\n * This file is part of FFmpeg.\n *\n * FFmpeg is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 2.1 of the License, or (at your option) any later version.\n *\n * FFmpeg is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with FFmpeg; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n */\n\n#include <trx/av/video.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_audio.h>\n#include <SDL2/SDL_error.h>\n#include <SDL2/SDL_events.h>\n#include <SDL2/SDL_keycode.h>\n#include <SDL2/SDL_mutex.h>\n#include <SDL2/SDL_pixels.h>\n#include <SDL2/SDL_rect.h>\n#include <SDL2/SDL_render.h>\n#include <SDL2/SDL_stdinc.h>\n#include <SDL2/SDL_thread.h>\n#include <SDL2/SDL_video.h>\n#include <errno.h>\n#include <libavcodec/avcodec.h>\n#include <libavcodec/codec.h>\n#include <libavcodec/codec_id.h>\n#include <libavcodec/codec_par.h>\n#include <libavcodec/packet.h>\n#include <libavformat/avformat.h>\n#include <libavformat/avio.h>\n#include <libavutil/attributes.h>\n#include <libavutil/avutil.h>\n#include <libavutil/channel_layout.h>\n#include <libavutil/common.h>\n#include <libavutil/dict.h>\n#include <libavutil/error.h>\n#include <libavutil/fifo.h>\n#include <libavutil/frame.h>\n#include <libavutil/imgutils.h>\n#include <libavutil/macros.h>\n#include <libavutil/mathematics.h>\n#include <libavutil/mem.h>\n#include <libavutil/pixfmt.h>\n#include <libavutil/rational.h>\n#include <libavutil/samplefmt.h>\n#include <libavutil/time.h>\n#include <libswresample/swresample.h>\n#include <libswscale/swscale.h>\n#include <math.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n\n#define MAX_QUEUE_SIZE (15 * 1024 * 1024)\n#define MIN_FRAMES 25\n#define SDL_AUDIO_MIN_BUFFER_SIZE 512\n#define SDL_AUDIO_MAX_CALLBACKS_PER_SEC 30\n#define AV_SYNC_THRESHOLD_MIN 0.04\n#define AV_SYNC_THRESHOLD_MAX 0.1\n#define AV_SYNC_FRAMEDUP_THRESHOLD 0.1\n#define AV_NOSYNC_THRESHOLD 10.0\n#define SAMPLE_CORRECTION_PERCENT_MAX 10\n#define AUDIO_DIFF_AVG_NB 20\n#define REFRESH_RATE 0.01\n#define SAMPLE_ARRAY_SIZE (8 * 65536)\n#define VIDEO_PICTURE_QUEUE_SIZE 3\n#define SAMPLE_QUEUE_SIZE 9\n#define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE, VIDEO_PICTURE_QUEUE_SIZE)\n#define FF_QUIT_EVENT (SDL_USEREVENT + 2)\n\ntypedef struct {\n    AVPacket *pkt;\n    int serial;\n} M_PACKET_LIST;\n\ntypedef struct {\n    AVFifo *pkt_list;\n    int nb_packets;\n    int size;\n    int64_t duration;\n    bool abort_request;\n    int serial;\n    SDL_mutex *mutex;\n    SDL_cond *cond;\n} M_PACKET_QUEUE;\n\ntypedef struct {\n    int freq;\n    int channels;\n    AVChannelLayout ch_layout;\n    enum AVSampleFormat fmt;\n    int frame_size;\n    int bytes_per_sec;\n} M_AUDIO_PARAMS;\n\ntypedef struct {\n    double pts;\n    double pts_drift;\n    double last_updated;\n    double speed;\n    int serial;\n    bool paused;\n    int *queue_serial;\n} M_CLOCK;\n\ntypedef struct {\n    AVFrame *frame;\n    int serial;\n    double pts;\n    double duration;\n    int64_t pos;\n    int width;\n    int height;\n    int format;\n    AVRational sar;\n} M_FRAME;\n\ntypedef struct {\n    int64_t pkt_pos;\n} M_FRAME_DATA;\n\ntypedef struct {\n    M_FRAME queue[FRAME_QUEUE_SIZE];\n    int rindex;\n    int windex;\n    int size;\n    int max_size;\n    int keep_last;\n    int rindex_shown;\n    SDL_mutex *mutex;\n    SDL_cond *cond;\n    M_PACKET_QUEUE *pktq;\n} M_FRAME_QUEUE;\n\ntypedef enum {\n    AV_SYNC_AUDIO_MASTER,\n    AV_SYNC_VIDEO_MASTER,\n    AV_SYNC_EXTERNAL_CLOCK,\n} M_CLOCK_TYPE;\n\ntypedef struct {\n    AVPacket *pkt;\n    M_PACKET_QUEUE *queue;\n    AVCodecContext *avctx;\n    int pkt_serial;\n    int finished;\n    bool packet_pending;\n    SDL_cond *empty_queue_cond;\n    int64_t start_pts;\n    AVRational start_pts_tb;\n    int64_t next_pts;\n    AVRational next_pts_tb;\n    SDL_Thread *decoder_tid;\n} M_DECODER;\n\ntypedef struct {\n    SDL_Thread *read_tid;\n    AVInputFormat *iformat;\n    bool abort_request;\n    bool playback_finished;\n    bool force_refresh;\n    bool paused;\n    bool last_paused;\n    bool queue_attachments_req;\n    int read_pause_return;\n    AVFormatContext *ic;\n    double remaining_time;\n\n    M_CLOCK audclk;\n    M_CLOCK vidclk;\n    M_CLOCK extclk;\n\n    M_FRAME_QUEUE pictq;\n    M_FRAME_QUEUE sampq;\n\n    M_DECODER auddec;\n    M_DECODER viddec;\n\n    int audio_stream;\n\n    int av_sync_type;\n\n    double audio_clock;\n    int audio_clock_serial;\n    double audio_diff_cum;\n    double audio_diff_avg_coef;\n    double audio_diff_threshold;\n    int audio_diff_avg_count;\n    AVStream *audio_st;\n    M_PACKET_QUEUE audioq;\n    int audio_hw_buf_size;\n    uint8_t *audio_buf;\n    uint8_t *audio_buf1;\n    unsigned int audio_buf_size;\n    unsigned int audio_buf1_size;\n    int audio_buf_index;\n    int audio_write_buf_size;\n    int audio_volume;\n    M_AUDIO_PARAMS audio_src;\n    M_AUDIO_PARAMS audio_tgt;\n    struct SwrContext *swr_ctx;\n    int frame_drops_early;\n    int frame_drops_late;\n\n    // surface size at the size of the display buffer\n    int surface_width;\n    int surface_height;\n    // target surface coordinates, keeping the video A:R\n    int target_surface_x;\n    int target_surface_y;\n    int target_surface_width;\n    int target_surface_height;\n\n    double frame_timer;\n    double frame_last_returned_time;\n    int video_stream;\n    AVStream *video_st;\n    M_PACKET_QUEUE videoq;\n    double max_frame_duration; // maximum duration of a frame - above this, we\n                               // consider the jump a timestamp discontinuity\n    struct SwsContext *img_convert_ctx;\n    bool eof;\n\n    char *filename;\n    int display_width;\n    int display_height;\n\n    SDL_cond *continue_read_thread;\n\n    void *primary_surface;\n    enum AVPixelFormat primary_surface_pixel_format;\n    int32_t primary_surface_stride;\n\n    VIDEO_SURFACE_ALLOCATOR_FUNC surface_allocator_func;\n    void *surface_allocator_func_user_data;\n\n    void (*surface_deallocator_func)(void *surface, void *user_data);\n    void *surface_deallocator_func_user_data;\n\n    void (*surface_clear_func)(void *surface, void *user_data);\n    void *surface_clear_func_user_data;\n\n    void (*render_begin_func)(void *surface, void *user_data);\n    void *render_begin_func_user_data;\n\n    void (*render_end_func)(void *surface, void *user_data);\n    void *render_end_func_user_data;\n\n    void *(*surface_lock_func)(void *surface, void *user_data);\n    void *surface_lock_func_user_data;\n\n    void (*surface_unlock_func)(void *surface, void *user_data);\n    void *surface_unlock_func_user_data;\n\n    void (*surface_upload_func)(void *surface, void *user_data);\n    void *surface_upload_func_user_data;\n\n    bool audio_enabled;\n} M_STATE;\n\nstatic int64_t m_AudioCallbackTime;\nstatic SDL_AudioDeviceID m_AudioDevice = 0;\n\nstatic int M_PacketQueuePutPrivate(M_PACKET_QUEUE *q, AVPacket *pkt)\n{\n    M_PACKET_LIST pkt1;\n\n    if (q->abort_request) {\n        return -1;\n    }\n\n    pkt1.pkt = pkt;\n    pkt1.serial = q->serial;\n\n    int32_t ret = av_fifo_write(q->pkt_list, &pkt1, 1);\n    if (ret < 0) {\n        return ret;\n    }\n    q->nb_packets++;\n    q->size += pkt1.pkt->size + sizeof(pkt1);\n    q->duration += pkt1.pkt->duration;\n    SDL_CondSignal(q->cond);\n    return 0;\n}\n\nstatic int M_PacketQueuePut(M_PACKET_QUEUE *q, AVPacket *pkt)\n{\n    AVPacket *pkt1;\n    int ret;\n\n    pkt1 = av_packet_alloc();\n    if (!pkt1) {\n        av_packet_unref(pkt);\n        return -1;\n    }\n    av_packet_move_ref(pkt1, pkt);\n\n    SDL_LockMutex(q->mutex);\n    ret = M_PacketQueuePutPrivate(q, pkt1);\n    SDL_UnlockMutex(q->mutex);\n\n    if (ret < 0) {\n        av_packet_free(&pkt1);\n    }\n\n    return ret;\n}\n\nstatic int M_PacketQueuePutNullPacket(\n    M_PACKET_QUEUE *q, AVPacket *pkt, int stream_index)\n{\n    pkt->stream_index = stream_index;\n    return M_PacketQueuePut(q, pkt);\n}\n\nstatic int M_PacketQueueInit(M_PACKET_QUEUE *q)\n{\n    memset(q, 0, sizeof(M_PACKET_QUEUE));\n    q->pkt_list =\n        av_fifo_alloc2(1, sizeof(M_PACKET_LIST), AV_FIFO_FLAG_AUTO_GROW);\n    if (q->pkt_list == nullptr) {\n        return AVERROR(ENOMEM);\n    }\n\n    q->mutex = SDL_CreateMutex();\n    if (!q->mutex) {\n        LOG_ERROR(\"SDL_CreateMutex(): %s\", SDL_GetError());\n        return AVERROR(ENOMEM);\n    }\n\n    q->cond = SDL_CreateCond();\n    if (!q->cond) {\n        LOG_ERROR(\"SDL_CreateCond(): %s\", SDL_GetError());\n        return AVERROR(ENOMEM);\n    }\n\n    q->abort_request = true;\n    return 0;\n}\n\nstatic void M_PacketQueueFlush(M_PACKET_QUEUE *q)\n{\n    if (q == nullptr || q->mutex == nullptr || q->pkt_list == nullptr) {\n        return;\n    }\n\n    M_PACKET_LIST pkt1;\n\n    SDL_LockMutex(q->mutex);\n    while (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0) {\n        av_packet_free(&pkt1.pkt);\n    }\n    q->nb_packets = 0;\n    q->size = 0;\n    q->duration = 0;\n    q->serial++;\n    SDL_UnlockMutex(q->mutex);\n}\n\nstatic void M_PacketQueueDestroy(M_PACKET_QUEUE *q)\n{\n    if (q == nullptr) {\n        return;\n    }\n\n    M_PacketQueueFlush(q);\n    av_fifo_freep2(&q->pkt_list);\n\n    if (q->mutex != nullptr) {\n        SDL_DestroyMutex(q->mutex);\n        q->mutex = nullptr;\n    }\n    if (q->cond != nullptr) {\n        SDL_DestroyCond(q->cond);\n        q->cond = nullptr;\n    }\n}\n\nstatic void M_PacketQueueAbort(M_PACKET_QUEUE *q)\n{\n    SDL_LockMutex(q->mutex);\n    q->abort_request = true;\n    SDL_CondSignal(q->cond);\n    SDL_UnlockMutex(q->mutex);\n}\n\nstatic void M_PacketQueueStart(M_PACKET_QUEUE *q)\n{\n    SDL_LockMutex(q->mutex);\n    q->abort_request = false;\n    q->serial++;\n    SDL_UnlockMutex(q->mutex);\n}\n\nstatic int M_PacketQueueGet(\n    M_PACKET_QUEUE *q, AVPacket *pkt, int block, int *serial)\n{\n    M_PACKET_LIST pkt1;\n    int ret;\n\n    SDL_LockMutex(q->mutex);\n\n    while (1) {\n        if (q->abort_request) {\n            ret = -1;\n            break;\n        }\n\n        if (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0) {\n            q->nb_packets--;\n            q->size -= pkt1.pkt->size + sizeof(pkt1);\n            q->duration -= pkt1.pkt->duration;\n            av_packet_move_ref(pkt, pkt1.pkt);\n            if (serial) {\n                *serial = pkt1.serial;\n            }\n            av_packet_free(&pkt1.pkt);\n            ret = 1;\n            break;\n        } else if (!block) {\n            ret = 0;\n            break;\n        } else {\n            SDL_CondWait(q->cond, q->mutex);\n        }\n    }\n    SDL_UnlockMutex(q->mutex);\n    return ret;\n}\n\nstatic int M_DecoderInit(\n    M_DECODER *d, AVCodecContext *avctx, M_PACKET_QUEUE *queue,\n    SDL_cond *empty_queue_cond)\n{\n    memset(d, 0, sizeof(M_DECODER));\n    d->pkt = av_packet_alloc();\n    if (!d->pkt) {\n        return AVERROR(ENOMEM);\n    }\n    d->avctx = avctx;\n    d->queue = queue;\n    d->empty_queue_cond = empty_queue_cond;\n    d->start_pts = AV_NOPTS_VALUE;\n    d->pkt_serial = -1;\n    return 0;\n}\n\nstatic int M_DecoderDecodeFrame(M_DECODER *d, AVFrame *frame)\n{\n    int ret = AVERROR(EAGAIN);\n\n    while (1) {\n        if (d->queue->serial == d->pkt_serial) {\n            do {\n                if (d->queue->abort_request) {\n                    return -1;\n                }\n\n                switch (d->avctx->codec_type) {\n                case AVMEDIA_TYPE_VIDEO:\n                    ret = avcodec_receive_frame(d->avctx, frame);\n                    if (ret >= 0) {\n                        frame->pts = frame->best_effort_timestamp;\n                    }\n                    break;\n\n                case AVMEDIA_TYPE_AUDIO:\n                    ret = avcodec_receive_frame(d->avctx, frame);\n                    if (ret >= 0) {\n                        AVRational tb = (AVRational) { 1, frame->sample_rate };\n                        if (frame->pts != AV_NOPTS_VALUE) {\n                            frame->pts = av_rescale_q(\n                                frame->pts, d->avctx->pkt_timebase, tb);\n                        } else if (d->next_pts != AV_NOPTS_VALUE) {\n                            frame->pts =\n                                av_rescale_q(d->next_pts, d->next_pts_tb, tb);\n                        }\n                        if (frame->pts != AV_NOPTS_VALUE) {\n                            d->next_pts = frame->pts + frame->nb_samples;\n                            d->next_pts_tb = tb;\n                        }\n                    }\n                    break;\n\n                default:\n                    break;\n                }\n\n                if (ret == AVERROR_EOF) {\n                    d->finished = d->pkt_serial;\n                    avcodec_flush_buffers(d->avctx);\n                    return 0;\n                }\n                if (ret >= 0) {\n                    return 1;\n                }\n            } while (ret != AVERROR(EAGAIN));\n        }\n\n        while (1) {\n            if (d->queue->nb_packets == 0) {\n                SDL_CondSignal(d->empty_queue_cond);\n            }\n            if (d->packet_pending) {\n                d->packet_pending = false;\n            } else {\n                int old_serial = d->pkt_serial;\n                if (M_PacketQueueGet(d->queue, d->pkt, 1, &d->pkt_serial) < 0) {\n                    return -1;\n                }\n                if (old_serial != d->pkt_serial) {\n                    avcodec_flush_buffers(d->avctx);\n                    d->finished = 0;\n                    d->next_pts = d->start_pts;\n                    d->next_pts_tb = d->start_pts_tb;\n                }\n            }\n            if (d->queue->serial == d->pkt_serial) {\n                break;\n            }\n            av_packet_unref(d->pkt);\n        }\n\n        if (d->pkt->buf && !d->pkt->opaque_ref) {\n            d->pkt->opaque_ref = av_buffer_allocz(sizeof(M_FRAME_DATA));\n            if (!d->pkt->opaque_ref) {\n                return AVERROR(ENOMEM);\n            }\n            M_FRAME_DATA *fd = (M_FRAME_DATA *)d->pkt->opaque_ref->data;\n            fd->pkt_pos = d->pkt->pos;\n        }\n\n        if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {\n            LOG_ERROR(\n                \"Receive_frame and send_packet both returned EAGAIN, \"\n                \"which is an API violation.\");\n            d->packet_pending = true;\n        } else {\n            av_packet_unref(d->pkt);\n        }\n    }\n}\n\nstatic void M_DecoderShutdown(M_DECODER *d)\n{\n    av_packet_free(&d->pkt);\n    avcodec_free_context(&d->avctx);\n}\n\nstatic void M_FrameQueueUnrefItem(M_FRAME *vp)\n{\n    av_frame_unref(vp->frame);\n}\n\nstatic int M_FrameQueueInit(\n    M_FRAME_QUEUE *f, M_PACKET_QUEUE *pktq, int max_size, int keep_last)\n{\n    memset(f, 0, sizeof(M_FRAME_QUEUE));\n    if (!(f->mutex = SDL_CreateMutex())) {\n        LOG_ERROR(\"SDL_CreateMutex(): %s\", SDL_GetError());\n        return AVERROR(ENOMEM);\n    }\n    if (!(f->cond = SDL_CreateCond())) {\n        LOG_ERROR(\"SDL_CreateCond(): %s\", SDL_GetError());\n        return AVERROR(ENOMEM);\n    }\n    f->pktq = pktq;\n    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);\n    f->keep_last = !!keep_last;\n    for (int i = 0; i < f->max_size; i++) {\n        if (!(f->queue[i].frame = av_frame_alloc())) {\n            return AVERROR(ENOMEM);\n        }\n    }\n    return 0;\n}\n\nstatic void M_FrameQueueShutdown(M_FRAME_QUEUE *f)\n{\n    for (int i = 0; i < f->max_size; i++) {\n        M_FRAME *vp = &f->queue[i];\n        if (vp->frame != nullptr) {\n            M_FrameQueueUnrefItem(vp);\n            av_frame_free(&vp->frame);\n        }\n    }\n    if (f->mutex != nullptr) {\n        SDL_DestroyMutex(f->mutex);\n        f->mutex = nullptr;\n    }\n    if (f->cond != nullptr) {\n        SDL_DestroyCond(f->cond);\n        f->cond = nullptr;\n    }\n}\n\nstatic void M_FrameQueueSignal(M_FRAME_QUEUE *f)\n{\n    SDL_LockMutex(f->mutex);\n    SDL_CondSignal(f->cond);\n    SDL_UnlockMutex(f->mutex);\n}\n\nstatic M_FRAME *M_FrameQueuePeek(M_FRAME_QUEUE *f)\n{\n    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];\n}\n\nstatic M_FRAME *M_FrameQueuePeekNext(M_FRAME_QUEUE *f)\n{\n    return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];\n}\n\nstatic M_FRAME *M_FrameQueuePeekLast(M_FRAME_QUEUE *f)\n{\n    return &f->queue[f->rindex];\n}\n\nstatic M_FRAME *M_FrameQueuePeekWritable(M_FRAME_QUEUE *f)\n{\n    SDL_LockMutex(f->mutex);\n    while (f->size >= f->max_size && !f->pktq->abort_request) {\n        SDL_CondWait(f->cond, f->mutex);\n    }\n    SDL_UnlockMutex(f->mutex);\n\n    if (f->pktq->abort_request) {\n        return nullptr;\n    }\n\n    return &f->queue[f->windex];\n}\n\nstatic M_FRAME *M_FrameQueuePeekReadable(M_FRAME_QUEUE *f)\n{\n    SDL_LockMutex(f->mutex);\n    while (f->size - f->rindex_shown <= 0 && !f->pktq->abort_request) {\n        SDL_CondWait(f->cond, f->mutex);\n    }\n    SDL_UnlockMutex(f->mutex);\n\n    if (f->pktq->abort_request) {\n        return nullptr;\n    }\n\n    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];\n}\n\nstatic void M_FrameQueuePush(M_FRAME_QUEUE *f)\n{\n    if (++f->windex == f->max_size) {\n        f->windex = 0;\n    }\n    SDL_LockMutex(f->mutex);\n    f->size++;\n    SDL_CondSignal(f->cond);\n    SDL_UnlockMutex(f->mutex);\n}\n\nstatic void M_FrameQueueNext(M_FRAME_QUEUE *f)\n{\n    if (f->keep_last && !f->rindex_shown) {\n        f->rindex_shown = 1;\n        return;\n    }\n    M_FrameQueueUnrefItem(&f->queue[f->rindex]);\n    if (++f->rindex == f->max_size) {\n        f->rindex = 0;\n    }\n    SDL_LockMutex(f->mutex);\n    f->size--;\n    SDL_CondSignal(f->cond);\n    SDL_UnlockMutex(f->mutex);\n}\n\nstatic int M_FrameQueueNBRemaining(M_FRAME_QUEUE *f)\n{\n    return f->size - f->rindex_shown;\n}\n\nstatic void M_DecoderAbort(M_DECODER *d, M_FRAME_QUEUE *fq)\n{\n    M_PacketQueueAbort(d->queue);\n    M_FrameQueueSignal(fq);\n    SDL_WaitThread(d->decoder_tid, nullptr);\n    d->decoder_tid = nullptr;\n    M_PacketQueueFlush(d->queue);\n}\n\nstatic void M_ReallocPrimarySurface(\n    M_STATE *is, int surface_width, int surface_height, bool clear)\n{\n    is->surface_width = surface_width;\n    is->surface_height = surface_height;\n\n    if (is->primary_surface != nullptr) {\n        is->surface_deallocator_func(\n            is->primary_surface, is->surface_deallocator_func_user_data);\n        is->primary_surface = nullptr;\n    }\n\n    {\n        is->primary_surface = is->surface_allocator_func(\n            is->surface_width, is->surface_height,\n            is->surface_allocator_func_user_data);\n    }\n\n    if (clear) {\n        is->surface_clear_func(\n            is->primary_surface, is->surface_clear_func_user_data);\n    }\n}\n\nstatic void M_RecalcSurfaceTargetRect(\n    M_STATE *is, int32_t frame_width, int32_t frame_height)\n{\n    const float source_ratio = frame_width / (float)frame_height;\n    const float target_ratio = is->surface_width / (float)is->surface_height;\n\n    is->target_surface_width = source_ratio < target_ratio\n        ? is->surface_height * source_ratio\n        : is->surface_width;\n    is->target_surface_height = source_ratio < target_ratio\n        ? is->surface_height\n        : is->surface_width / source_ratio;\n    is->target_surface_x = (is->surface_width - is->target_surface_width) / 2;\n    is->target_surface_y = (is->surface_height - is->target_surface_height) / 2;\n}\n\nstatic int M_UploadTexture(M_STATE *is, AVFrame *frame)\n{\n    int ret = 0;\n\n    is->img_convert_ctx = sws_getCachedContext(\n        is->img_convert_ctx, frame->width, frame->height, frame->format,\n        is->target_surface_width, is->target_surface_height,\n        is->primary_surface_pixel_format, SWS_BILINEAR, nullptr, nullptr,\n        nullptr);\n\n    if (is->img_convert_ctx) {\n        is->render_begin_func(\n            is->primary_surface, is->render_begin_func_user_data);\n\n        void *pixels = is->surface_lock_func(\n            is->primary_surface, is->surface_lock_func_user_data);\n\n        if (pixels != nullptr) {\n            uint8_t *surf_planes[4] = { pixels, nullptr, nullptr, nullptr };\n            int surf_linesize[4] = {};\n            if (is->primary_surface_stride > 0) {\n                surf_linesize[0] = is->primary_surface_stride;\n            } else {\n                surf_linesize[0] = av_image_get_linesize(\n                    is->primary_surface_pixel_format, is->surface_width, 0);\n            }\n\n            surf_planes[0] += is->target_surface_y * surf_linesize[0];\n            surf_planes[0] += av_image_get_linesize(\n                is->primary_surface_pixel_format, is->target_surface_x, 0);\n\n            sws_scale(\n                is->img_convert_ctx, (const uint8_t *const *)frame->data,\n                frame->linesize, 0, frame->height, surf_planes, surf_linesize);\n\n            is->surface_unlock_func(\n                is->primary_surface, is->surface_unlock_func_user_data);\n            is->surface_upload_func(\n                is->primary_surface, is->surface_upload_func_user_data);\n        }\n\n        is->render_end_func(is->primary_surface, is->render_end_func_user_data);\n    } else {\n        LOG_ERROR(\"Cannot initialize the conversion context\");\n        ret = -1;\n    }\n\n    return ret;\n}\n\nstatic void M_VideoImageDisplay(M_STATE *is)\n{\n    M_FRAME *vp = M_FrameQueuePeekLast(&is->pictq);\n\n    M_RecalcSurfaceTargetRect(is, vp->frame->width, vp->frame->height);\n    M_UploadTexture(is, vp->frame);\n}\n\nstatic void M_StreamComponentClose(M_STATE *is, int stream_index)\n{\n    AVFormatContext *ic = is->ic;\n    AVCodecParameters *codecpar;\n\n    if (stream_index < 0 || stream_index >= (signed)ic->nb_streams) {\n        return;\n    }\n    codecpar = ic->streams[stream_index]->codecpar;\n\n    switch (codecpar->codec_type) {\n    case AVMEDIA_TYPE_AUDIO:\n        M_DecoderAbort(&is->auddec, &is->sampq);\n        M_DecoderShutdown(&is->auddec);\n        swr_free(&is->swr_ctx);\n        av_freep(&is->audio_buf1);\n        if (m_AudioDevice > 0) {\n            SDL_CloseAudioDevice(m_AudioDevice);\n            m_AudioDevice = 0;\n        }\n        is->audio_buf1_size = 0;\n        is->audio_buf = nullptr;\n\n        break;\n    case AVMEDIA_TYPE_VIDEO:\n        M_DecoderAbort(&is->viddec, &is->pictq);\n        M_DecoderShutdown(&is->viddec);\n        break;\n    default:\n        break;\n    }\n\n    ic->streams[stream_index]->discard = AVDISCARD_ALL;\n    switch (codecpar->codec_type) {\n    case AVMEDIA_TYPE_AUDIO:\n        is->audio_st = nullptr;\n        is->audio_stream = -1;\n        break;\n    case AVMEDIA_TYPE_VIDEO:\n        is->video_st = nullptr;\n        is->video_stream = -1;\n        break;\n    default:\n        break;\n    }\n}\n\nstatic void M_StreamClose(M_STATE *is)\n{\n    if (is == nullptr) {\n        return;\n    }\n\n    if (is->read_tid != nullptr) {\n        SDL_WaitThread(is->read_tid, nullptr);\n        is->read_tid = nullptr;\n    }\n\n    if (is->ic != nullptr && is->audio_stream >= 0) {\n        M_StreamComponentClose(is, is->audio_stream);\n    }\n    if (is->ic != nullptr && is->video_stream >= 0) {\n        M_StreamComponentClose(is, is->video_stream);\n    }\n\n    if (is->ic != nullptr) {\n        avformat_close_input(&is->ic);\n    }\n\n    M_PacketQueueDestroy(&is->videoq);\n    M_PacketQueueDestroy(&is->audioq);\n\n    if (is->pictq.mutex != nullptr || is->pictq.cond != nullptr) {\n        M_FrameQueueShutdown(&is->pictq);\n    }\n    if (is->sampq.mutex != nullptr || is->sampq.cond != nullptr) {\n        M_FrameQueueShutdown(&is->sampq);\n    }\n    if (is->continue_read_thread != nullptr) {\n        SDL_DestroyCond(is->continue_read_thread);\n        is->continue_read_thread = nullptr;\n    }\n    sws_freeContext(is->img_convert_ctx);\n    av_free(is->filename);\n    if (is->primary_surface) {\n        is->surface_deallocator_func(\n            is->primary_surface, is->surface_deallocator_func_user_data);\n    }\n    av_free(is);\n}\n\nstatic void M_VideoDisplay(M_STATE *is)\n{\n    if (is->video_st) {\n        M_VideoImageDisplay(is);\n    }\n}\n\nstatic double M_GetClock(M_CLOCK *c)\n{\n    if (*c->queue_serial != c->serial) {\n        return NAN;\n    }\n    if (c->paused) {\n        return c->pts;\n    } else {\n        double time = av_gettime_relative() / 1000000.0;\n        return c->pts_drift + time\n            - (time - c->last_updated) * (1.0 - c->speed);\n    }\n}\n\nstatic void M_SetClockAt(M_CLOCK *c, double pts, int serial, double time)\n{\n    c->pts = pts;\n    c->last_updated = time;\n    c->pts_drift = c->pts - time;\n    c->serial = serial;\n}\n\nstatic void M_SetClock(M_CLOCK *c, double pts, int serial)\n{\n    double time = av_gettime_relative() / 1000000.0;\n    M_SetClockAt(c, pts, serial, time);\n}\n\nstatic void M_InitClock(M_CLOCK *c, int *queue_serial)\n{\n    c->speed = 1.0;\n    c->paused = false;\n    c->queue_serial = queue_serial;\n    M_SetClock(c, NAN, -1);\n}\n\nstatic void M_SyncClockToSlave(M_CLOCK *const c, M_CLOCK *const slave)\n{\n    double clock = M_GetClock(c);\n    double slave_clock = M_GetClock(slave);\n    if (!isnan(slave_clock)\n        && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD)) {\n        M_SetClock(c, slave_clock, slave->serial);\n    }\n}\n\nstatic int M_GetMasterSyncType(M_STATE *is)\n{\n    if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) {\n        if (is->video_st) {\n            return AV_SYNC_VIDEO_MASTER;\n        } else {\n            return AV_SYNC_AUDIO_MASTER;\n        }\n    } else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {\n        if (is->audio_st) {\n            return AV_SYNC_AUDIO_MASTER;\n        } else {\n            return AV_SYNC_EXTERNAL_CLOCK;\n        }\n    } else {\n        return AV_SYNC_EXTERNAL_CLOCK;\n    }\n}\n\nstatic double M_GetMasterClock(M_STATE *is)\n{\n    switch (M_GetMasterSyncType(is)) {\n    case AV_SYNC_VIDEO_MASTER:\n        return M_GetClock(&is->vidclk);\n\n    case AV_SYNC_AUDIO_MASTER:\n        return M_GetClock(&is->audclk);\n\n    default:\n        return M_GetClock(&is->extclk);\n    }\n}\n\nstatic double M_ComputeTargetDelay(double delay, M_STATE *is)\n{\n    double sync_threshold, diff = 0;\n\n    if (M_GetMasterSyncType(is) != AV_SYNC_VIDEO_MASTER) {\n        diff = M_GetClock(&is->vidclk) - M_GetMasterClock(is);\n\n        sync_threshold =\n            FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));\n        if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {\n            if (diff <= -sync_threshold) {\n                delay = FFMAX(0, delay + diff);\n            } else if (\n                diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {\n                delay = delay + diff;\n            } else if (diff >= sync_threshold) {\n                delay = 2 * delay;\n            }\n        }\n    }\n\n    return delay;\n}\n\nstatic double M_VPDuration(M_STATE *is, M_FRAME *vp, M_FRAME *nextvp)\n{\n    if (vp->serial == nextvp->serial) {\n        double duration = nextvp->pts - vp->pts;\n        if (isnan(duration) || duration <= 0\n            || duration > is->max_frame_duration) {\n            return vp->duration;\n        } else {\n            return duration;\n        }\n    } else {\n        return 0.0;\n    }\n}\n\nstatic void M_UpdateVideoPTS(M_STATE *is, double pts, int64_t pos, int serial)\n{\n    M_SetClock(&is->vidclk, pts, serial);\n    M_SyncClockToSlave(&is->extclk, &is->vidclk);\n}\n\nstatic void M_VideoRefresh(void *opaque, double *remaining_time)\n{\n    M_STATE *is = opaque;\n    double time;\n\n    if (is->video_st) {\n    retry:\n        if (M_FrameQueueNBRemaining(&is->pictq) != 0) {\n            double last_duration, duration, delay;\n            M_FRAME *vp, *lastvp;\n\n            lastvp = M_FrameQueuePeekLast(&is->pictq);\n            vp = M_FrameQueuePeek(&is->pictq);\n\n            if (vp->serial != is->videoq.serial) {\n                M_FrameQueueNext(&is->pictq);\n                goto retry;\n            }\n\n            if (lastvp->serial != vp->serial) {\n                is->frame_timer = av_gettime_relative() / 1000000.0;\n            }\n\n            if (is->paused) {\n                goto display;\n            }\n\n            last_duration = M_VPDuration(is, lastvp, vp);\n            delay = M_ComputeTargetDelay(last_duration, is);\n\n            time = av_gettime_relative() / 1000000.0;\n            if (time < is->frame_timer + delay) {\n                *remaining_time =\n                    FFMIN(is->frame_timer + delay - time, *remaining_time);\n                goto display;\n            }\n\n            is->frame_timer += delay;\n            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) {\n                is->frame_timer = time;\n            }\n\n            SDL_LockMutex(is->pictq.mutex);\n            if (!isnan(vp->pts)) {\n                M_UpdateVideoPTS(is, vp->pts, vp->pos, vp->serial);\n            }\n            SDL_UnlockMutex(is->pictq.mutex);\n\n            if (M_FrameQueueNBRemaining(&is->pictq) > 1) {\n                M_FRAME *nextvp = M_FrameQueuePeekNext(&is->pictq);\n                duration = M_VPDuration(is, vp, nextvp);\n                if (M_GetMasterSyncType(is) != AV_SYNC_VIDEO_MASTER\n                    && time > is->frame_timer + duration) {\n                    is->frame_drops_late++;\n                    M_FrameQueueNext(&is->pictq);\n                    goto retry;\n                }\n            }\n\n            M_FrameQueueNext(&is->pictq);\n            is->force_refresh = true;\n        }\n\n    display:\n        if (is->force_refresh && is->pictq.rindex_shown) {\n            M_VideoDisplay(is);\n        }\n    }\n    is->force_refresh = false;\n}\n\nstatic int M_QueuePicture(\n    M_STATE *is, AVFrame *src_frame, double pts, double duration, int64_t pos,\n    int serial)\n{\n    M_FRAME *vp;\n\n    if (!(vp = M_FrameQueuePeekWritable(&is->pictq))) {\n        return -1;\n    }\n\n    vp->sar = src_frame->sample_aspect_ratio;\n\n    vp->width = src_frame->width;\n    vp->height = src_frame->height;\n    vp->format = src_frame->format;\n\n    vp->pts = pts;\n    vp->duration = duration;\n    vp->pos = pos;\n    vp->serial = serial;\n\n    av_frame_move_ref(vp->frame, src_frame);\n    M_FrameQueuePush(&is->pictq);\n    return 0;\n}\n\nstatic int M_GetVideoFrame(M_STATE *is, AVFrame *frame)\n{\n    int got_picture;\n\n    if ((got_picture = M_DecoderDecodeFrame(&is->viddec, frame)) < 0) {\n        return -1;\n    }\n\n    if (got_picture) {\n        double dpts = NAN;\n\n        if (frame->pts != AV_NOPTS_VALUE) {\n            dpts = av_q2d(is->video_st->time_base) * frame->pts;\n        }\n\n        frame->sample_aspect_ratio =\n            av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);\n\n        if (M_GetMasterSyncType(is) != AV_SYNC_VIDEO_MASTER) {\n            if (frame->pts != AV_NOPTS_VALUE) {\n                double diff = dpts - M_GetMasterClock(is);\n                if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD && diff < 0\n                    && is->viddec.pkt_serial == is->vidclk.serial\n                    && is->videoq.nb_packets) {\n                    is->frame_drops_early++;\n                    av_frame_unref(frame);\n                    got_picture = 0;\n                }\n            }\n        }\n    }\n\n    return got_picture;\n}\n\nstatic int M_AudioThread(void *arg)\n{\n    M_STATE *is = arg;\n    AVFrame *frame = av_frame_alloc();\n    M_FRAME *af;\n    int last_serial = -1;\n    int got_frame = 0;\n    AVRational tb;\n    int ret = 0;\n\n    if (frame == nullptr) {\n        return AVERROR(ENOMEM);\n    }\n\n    do {\n        got_frame = M_DecoderDecodeFrame(&is->auddec, frame);\n        if (got_frame < 0) {\n            goto the_end;\n        }\n\n        if (got_frame) {\n            tb = (AVRational) { 1, frame->sample_rate };\n\n            M_FRAME_DATA *fd = frame->opaque_ref\n                ? (M_FRAME_DATA *)frame->opaque_ref->data\n                : nullptr;\n            af = M_FrameQueuePeekWritable(&is->sampq);\n            if (af == nullptr) {\n                goto the_end;\n            }\n\n            af->pts =\n                (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);\n            af->pos = fd ? fd->pkt_pos : -1;\n            af->serial = is->auddec.pkt_serial;\n            af->duration =\n                av_q2d((AVRational) { frame->nb_samples, frame->sample_rate });\n\n            av_frame_move_ref(af->frame, frame);\n            M_FrameQueuePush(&is->sampq);\n\n            if (is->audioq.serial != is->auddec.pkt_serial) {\n                break;\n            }\n\n            if (ret == AVERROR_EOF) {\n                is->auddec.finished = is->auddec.pkt_serial;\n            }\n        }\n    } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);\n\nthe_end:\n    av_frame_free(&frame);\n    return ret;\n}\n\nstatic int M_DecoderStart(\n    M_DECODER *d, int (*fn)(void *), const char *thread_name, void *arg)\n{\n    M_PacketQueueStart(d->queue);\n    d->decoder_tid = SDL_CreateThread(fn, thread_name, arg);\n    if (!d->decoder_tid) {\n        LOG_ERROR(\"SDL_CreateThread(): %s\", SDL_GetError());\n        return AVERROR(ENOMEM);\n    }\n    return 0;\n}\n\nstatic int M_VideoThread(void *arg)\n{\n    M_STATE *is = arg;\n    AVFrame *frame = av_frame_alloc();\n    double pts;\n    double duration;\n    int ret;\n    AVRational tb = is->video_st->time_base;\n    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, nullptr);\n\n    if (!frame) {\n        return AVERROR(ENOMEM);\n    }\n\n    while (1) {\n        ret = M_GetVideoFrame(is, frame);\n        if (ret < 0) {\n            goto the_end;\n        }\n        if (!ret) {\n            continue;\n        }\n\n        is->frame_last_returned_time = av_gettime_relative() / 1000000.0;\n\n        M_FRAME_DATA *fd = frame->opaque_ref\n            ? (M_FRAME_DATA *)frame->opaque_ref->data\n            : nullptr;\n\n        duration =\n            (frame_rate.num && frame_rate.den\n                 ? av_q2d((AVRational) { frame_rate.den, frame_rate.num })\n                 : 0);\n        pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);\n        ret = M_QueuePicture(\n            is, frame, pts, duration, fd ? fd->pkt_pos : -1,\n            is->viddec.pkt_serial);\n        av_frame_unref(frame);\n        if (is->videoq.serial != is->viddec.pkt_serial) {\n            break;\n        }\n    }\nthe_end:\n    av_frame_free(&frame);\n    return 0;\n}\n\nstatic int M_SynchronizeAudio(M_STATE *is, int nb_samples)\n{\n    int wanted_nb_samples = nb_samples;\n\n    if (M_GetMasterSyncType(is) != AV_SYNC_AUDIO_MASTER) {\n        double diff, avg_diff;\n        int min_nb_samples, max_nb_samples;\n\n        diff = M_GetClock(&is->audclk) - M_GetMasterClock(is);\n\n        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {\n            is->audio_diff_cum =\n                diff + is->audio_diff_avg_coef * is->audio_diff_cum;\n            if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {\n                is->audio_diff_avg_count++;\n            } else {\n                avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);\n\n                if (fabs(avg_diff) >= is->audio_diff_threshold) {\n                    wanted_nb_samples =\n                        nb_samples + (int)(diff * is->audio_src.freq);\n                    min_nb_samples =\n                        ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX)\n                          / 100));\n                    max_nb_samples =\n                        ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX)\n                          / 100));\n                    wanted_nb_samples = av_clip(\n                        wanted_nb_samples, min_nb_samples, max_nb_samples);\n                }\n            }\n        } else {\n            is->audio_diff_avg_count = 0;\n            is->audio_diff_cum = 0;\n        }\n    }\n\n    return wanted_nb_samples;\n}\n\nstatic int M_AudioDecodeFrame(M_STATE *is)\n{\n\n    int data_size, resampled_data_size;\n    av_unused double audio_clock0;\n    int wanted_nb_samples;\n    M_FRAME *af;\n\n    if (is->paused) {\n        return -1;\n    }\n\n    do {\n#if defined(_WIN32)\n        while (M_FrameQueueNBRemaining(&is->sampq) == 0) {\n            if ((av_gettime_relative() - m_AudioCallbackTime) > 1000000LL\n                    * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2) {\n                return -1;\n            }\n            av_usleep(1000);\n        }\n#endif\n        if (!(af = M_FrameQueuePeekReadable(&is->sampq))) {\n            return -1;\n        }\n        M_FrameQueueNext(&is->sampq);\n    } while (af->serial != is->audioq.serial);\n\n    data_size = av_samples_get_buffer_size(\n        nullptr, af->frame->ch_layout.nb_channels, af->frame->nb_samples,\n        af->frame->format, 1);\n\n    wanted_nb_samples = M_SynchronizeAudio(is, af->frame->nb_samples);\n\n    if (af->frame->format != is->audio_src.fmt\n        || av_channel_layout_compare(\n            &af->frame->ch_layout, &is->audio_src.ch_layout)\n        || af->frame->sample_rate != is->audio_src.freq\n        || (wanted_nb_samples != af->frame->nb_samples && !is->swr_ctx)) {\n        int ret;\n        swr_free(&is->swr_ctx);\n        ret = swr_alloc_set_opts2(\n            &is->swr_ctx, &is->audio_tgt.ch_layout, is->audio_tgt.fmt,\n            is->audio_tgt.freq, &af->frame->ch_layout, af->frame->format,\n            af->frame->sample_rate, 0, nullptr);\n        if (ret < 0 || swr_init(is->swr_ctx) < 0) {\n            LOG_ERROR(\n                \"Cannot create sample rate converter for conversion of %d Hz \"\n                \"%s %d channels to %d Hz %s %d channels!\",\n                af->frame->sample_rate,\n                av_get_sample_fmt_name(af->frame->format),\n                af->frame->ch_layout.nb_channels, is->audio_tgt.freq,\n                av_get_sample_fmt_name(is->audio_tgt.fmt),\n                is->audio_tgt.ch_layout.nb_channels);\n            swr_free(&is->swr_ctx);\n            return -1;\n        }\n        if (av_channel_layout_copy(\n                &is->audio_src.ch_layout, &af->frame->ch_layout)\n            < 0) {\n            return -1;\n        }\n        is->audio_src.freq = af->frame->sample_rate;\n        is->audio_src.fmt = af->frame->format;\n    }\n\n    if (is->swr_ctx) {\n        const uint8_t **in = (const uint8_t **)af->frame->extended_data;\n        uint8_t **out = &is->audio_buf1;\n        int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq\n                / af->frame->sample_rate\n            + 256;\n        int out_size = av_samples_get_buffer_size(\n            nullptr, is->audio_tgt.ch_layout.nb_channels, out_count,\n            is->audio_tgt.fmt, 0);\n        int len2;\n        if (out_size < 0) {\n            LOG_ERROR(\"av_samples_get_buffer_size() failed\");\n            return -1;\n        }\n        if (wanted_nb_samples != af->frame->nb_samples) {\n            if (swr_set_compensation(\n                    is->swr_ctx,\n                    (wanted_nb_samples - af->frame->nb_samples)\n                        * is->audio_tgt.freq / af->frame->sample_rate,\n                    wanted_nb_samples * is->audio_tgt.freq\n                        / af->frame->sample_rate)\n                < 0) {\n                LOG_ERROR(\"swr_set_compensation() failed\");\n                return -1;\n            }\n        }\n        av_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size);\n        if (!is->audio_buf1) {\n            return AVERROR(ENOMEM);\n        }\n        len2 =\n            swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);\n        if (len2 < 0) {\n            LOG_ERROR(\"swr_convert() failed\");\n            return -1;\n        }\n        if (len2 == out_count) {\n            LOG_ERROR(\"audio buffer is probably too small\");\n            if (swr_init(is->swr_ctx) < 0) {\n                swr_free(&is->swr_ctx);\n            }\n        }\n        is->audio_buf = is->audio_buf1;\n        resampled_data_size = len2 * is->audio_tgt.ch_layout.nb_channels\n            * av_get_bytes_per_sample(is->audio_tgt.fmt);\n    } else {\n        is->audio_buf = af->frame->data[0];\n        resampled_data_size = data_size;\n    }\n\n    audio_clock0 = is->audio_clock;\n    if (!isnan(af->pts)) {\n        is->audio_clock =\n            af->pts + (double)af->frame->nb_samples / af->frame->sample_rate;\n    } else {\n        is->audio_clock = NAN;\n    }\n    is->audio_clock_serial = af->serial;\n    return resampled_data_size;\n}\n\nstatic void M_SDLAudioCallback(void *opaque, Uint8 *stream, int len)\n{\n    M_STATE *is = opaque;\n    int audio_size, len1;\n\n    m_AudioCallbackTime = av_gettime_relative();\n\n    while (len > 0) {\n        if (is->audio_buf_index >= (signed)is->audio_buf_size) {\n            audio_size = M_AudioDecodeFrame(is);\n            if (audio_size < 0) {\n                is->audio_buf = nullptr;\n                is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE\n                    / is->audio_tgt.frame_size * is->audio_tgt.frame_size;\n            } else {\n                is->audio_buf_size = audio_size;\n            }\n            is->audio_buf_index = 0;\n        }\n        len1 = is->audio_buf_size - is->audio_buf_index;\n        if (len1 > len) {\n            len1 = len;\n        }\n        if (is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME) {\n            memcpy(\n                stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);\n        } else {\n            memset(stream, 0, len1);\n            if (is->audio_buf) {\n                SDL_MixAudioFormat(\n                    stream, (uint8_t *)is->audio_buf + is->audio_buf_index,\n                    AUDIO_S16SYS, len1, is->audio_volume);\n            }\n        }\n        len -= len1;\n        stream += len1;\n        is->audio_buf_index += len1;\n    }\n    is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;\n    if (!isnan(is->audio_clock)) {\n        M_SetClockAt(\n            &is->audclk,\n            is->audio_clock\n                - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size)\n                    / is->audio_tgt.bytes_per_sec,\n            is->audio_clock_serial, m_AudioCallbackTime / 1000000.0);\n        M_SyncClockToSlave(&is->extclk, &is->audclk);\n    }\n}\n\nstatic int M_AudioOpen(\n    M_STATE *is, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate,\n    M_AUDIO_PARAMS *audio_hw_params)\n{\n    SDL_AudioSpec wanted_spec, spec;\n    const char *env;\n    static const int next_nb_channels[] = { 0, 0, 1, 6, 2, 6, 4, 6 };\n    static const int next_sample_rates[] = { 0, 44100, 48000, 96000, 192000 };\n    int next_sample_rate_idx = FF_ARRAY_ELEMS(next_sample_rates) - 1;\n    int wanted_nb_channels = wanted_channel_layout->nb_channels;\n\n    env = SDL_getenv(\"SDL_AUDIO_CHANNELS\");\n    if (env) {\n        wanted_nb_channels = atoi(env);\n        av_channel_layout_uninit(wanted_channel_layout);\n        av_channel_layout_default(wanted_channel_layout, wanted_nb_channels);\n    }\n    if (wanted_channel_layout->order != AV_CHANNEL_ORDER_NATIVE) {\n        av_channel_layout_uninit(wanted_channel_layout);\n        av_channel_layout_default(wanted_channel_layout, wanted_nb_channels);\n    }\n    wanted_nb_channels = wanted_channel_layout->nb_channels;\n    wanted_spec.channels = wanted_nb_channels;\n    wanted_spec.freq = wanted_sample_rate;\n    if (wanted_spec.freq <= 0 || wanted_spec.channels <= 0) {\n        LOG_ERROR(\"Invalid sample rate or channel count!\");\n        return -1;\n    }\n    while (next_sample_rate_idx\n           && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq) {\n        next_sample_rate_idx--;\n    }\n    wanted_spec.format = AUDIO_S16SYS;\n    wanted_spec.silence = 0;\n    wanted_spec.samples = FFMAX(\n        SDL_AUDIO_MIN_BUFFER_SIZE,\n        2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));\n    wanted_spec.callback = M_SDLAudioCallback;\n    wanted_spec.userdata = is;\n    while (\n        !(m_AudioDevice = SDL_OpenAudioDevice(\n              nullptr, 0, &wanted_spec, &spec,\n              SDL_AUDIO_ALLOW_FREQUENCY_CHANGE\n                  | SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) {\n        LOG_WARNING(\n            \"SDL_OpenAudio (%d channels, %d Hz): %s\", wanted_spec.channels,\n            wanted_spec.freq, SDL_GetError());\n        wanted_spec.channels = next_nb_channels[FFMIN(7, wanted_spec.channels)];\n        if (!wanted_spec.channels) {\n            wanted_spec.freq = next_sample_rates[next_sample_rate_idx--];\n            wanted_spec.channels = wanted_nb_channels;\n            if (!wanted_spec.freq) {\n                LOG_ERROR(\"No more combinations to try, audio open failed\");\n                return -1;\n            }\n        }\n        av_channel_layout_default(wanted_channel_layout, wanted_spec.channels);\n    }\n    if (spec.format != AUDIO_S16SYS) {\n        LOG_ERROR(\"SDL advised audio format %d is not supported!\", spec.format);\n        return -1;\n    }\n    if (spec.channels != wanted_spec.channels) {\n        av_channel_layout_uninit(wanted_channel_layout);\n        av_channel_layout_default(wanted_channel_layout, spec.channels);\n        if (wanted_channel_layout->order != AV_CHANNEL_ORDER_NATIVE) {\n            LOG_ERROR(\n                \"SDL advised channel count %d is not supported!\",\n                spec.channels);\n            return -1;\n        }\n    }\n\n    audio_hw_params->fmt = AV_SAMPLE_FMT_S16;\n    audio_hw_params->freq = spec.freq;\n    if (av_channel_layout_copy(\n            &audio_hw_params->ch_layout, wanted_channel_layout)\n        < 0) {\n        return -1;\n    }\n    audio_hw_params->frame_size = av_samples_get_buffer_size(\n        nullptr, audio_hw_params->ch_layout.nb_channels, 1,\n        audio_hw_params->fmt, 1);\n    audio_hw_params->bytes_per_sec = av_samples_get_buffer_size(\n        nullptr, audio_hw_params->ch_layout.nb_channels, audio_hw_params->freq,\n        audio_hw_params->fmt, 1);\n    if (audio_hw_params->bytes_per_sec <= 0\n        || audio_hw_params->frame_size <= 0) {\n        LOG_ERROR(\"av_samples_get_buffer_size failed\");\n        return -1;\n    }\n    return spec.size;\n}\n\nstatic int M_StreamComponentOpen(M_STATE *is, int stream_index)\n{\n    AVFormatContext *ic = is->ic;\n    AVCodecContext *avctx = nullptr;\n    const AVCodec *codec = nullptr;\n    const char *forced_codec_name = nullptr;\n    AVDictionary *opts = nullptr;\n    const AVDictionaryEntry *t = nullptr;\n    int sample_rate;\n    int nb_channels;\n    AVChannelLayout ch_layout = {};\n    bool has_ch_layout = false;\n    int ret = 0;\n\n    if (stream_index < 0 || stream_index >= (signed)ic->nb_streams) {\n        return -1;\n    }\n\n    avctx = avcodec_alloc_context3(nullptr);\n    if (!avctx) {\n        return AVERROR(ENOMEM);\n    }\n\n    ret = avcodec_parameters_to_context(\n        avctx, ic->streams[stream_index]->codecpar);\n    if (ret < 0) {\n        goto fail;\n    }\n    avctx->pkt_timebase = ic->streams[stream_index]->time_base;\n\n    codec = avcodec_find_decoder(avctx->codec_id);\n\n    if (!codec) {\n        LOG_ERROR(\n            \"No decoder could be found for codec %s\",\n            avcodec_get_name(avctx->codec_id));\n        ret = AVERROR(EINVAL);\n        goto fail;\n    }\n\n    avctx->codec_id = codec->id;\n    avctx->lowres = 0;\n\n    if ((ret = avcodec_open2(avctx, codec, nullptr)) < 0) {\n        goto fail;\n    }\n\n    is->eof = false;\n    ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;\n    switch (avctx->codec_type) {\n    case AVMEDIA_TYPE_AUDIO:\n        sample_rate = avctx->sample_rate;\n        ret = av_channel_layout_copy(&ch_layout, &avctx->ch_layout);\n        if (ret < 0) {\n            goto fail;\n        }\n        has_ch_layout = true;\n\n        if ((ret = M_AudioOpen(is, &ch_layout, sample_rate, &is->audio_tgt))\n            < 0) {\n            goto fail;\n        }\n        is->audio_hw_buf_size = ret;\n        is->audio_src = is->audio_tgt;\n        is->audio_buf_size = 0;\n        is->audio_buf_index = 0;\n\n        is->audio_diff_avg_coef = exp(log(0.01) / AUDIO_DIFF_AVG_NB);\n        is->audio_diff_avg_count = 0;\n        is->audio_diff_threshold =\n            (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;\n\n        is->audio_stream = stream_index;\n        is->audio_st = ic->streams[stream_index];\n\n        if ((ret = M_DecoderInit(\n                 &is->auddec, avctx, &is->audioq, is->continue_read_thread))\n            < 0) {\n            goto fail;\n        }\n        if (is->ic->iformat->flags & AVFMT_NOTIMESTAMPS) {\n            is->auddec.start_pts = is->audio_st->start_time;\n            is->auddec.start_pts_tb = is->audio_st->time_base;\n        }\n        if (M_DecoderStart(&is->auddec, M_AudioThread, \"audio_decoder\", is)\n            < 0) {\n            LOG_ERROR(\"Error starting audio decoder\");\n            goto fail;\n        }\n        SDL_PauseAudioDevice(m_AudioDevice, 0);\n        break;\n\n    case AVMEDIA_TYPE_VIDEO:\n        is->video_stream = stream_index;\n        is->video_st = ic->streams[stream_index];\n\n        if ((ret = M_DecoderInit(\n                 &is->viddec, avctx, &is->videoq, is->continue_read_thread))\n            < 0) {\n            goto fail;\n        }\n        is->queue_attachments_req = true;\n        if ((M_DecoderStart(&is->viddec, M_VideoThread, \"video_decoder\", is))\n            < 0) {\n            LOG_ERROR(\"Error starting video decoder\");\n            goto fail;\n        }\n        break;\n\n    default:\n        break;\n    }\n    goto out;\n\nfail:\n    avcodec_free_context(&avctx);\nout:\n    if (has_ch_layout) {\n        av_channel_layout_uninit(&ch_layout);\n    }\n\n    return ret;\n}\n\nstatic int M_DecodeInterruptCB(void *ctx)\n{\n    M_STATE *is = ctx;\n    return is->abort_request;\n}\n\nstatic int M_StreamHasEnoughPackets(\n    AVStream *st, int stream_id, M_PACKET_QUEUE *queue)\n{\n    return stream_id < 0 || queue->abort_request\n        || (st->disposition & AV_DISPOSITION_ATTACHED_PIC)\n        || (queue->nb_packets > MIN_FRAMES\n            && (!queue->duration\n                || av_q2d(st->time_base) * queue->duration > 1.0));\n}\n\nstatic int M_ReadThread(void *arg)\n{\n    M_STATE *is = arg;\n    AVFormatContext *ic = nullptr;\n    int err;\n    int ret;\n    int st_index[AVMEDIA_TYPE_NB];\n    AVPacket *pkt = nullptr;\n    SDL_mutex *wait_mutex = SDL_CreateMutex();\n    int64_t pkt_ts;\n\n    if (!wait_mutex) {\n        LOG_ERROR(\"SDL_CreateMutex(): %s\", SDL_GetError());\n        ret = AVERROR(ENOMEM);\n        goto fail;\n    }\n\n    memset(st_index, -1, sizeof(st_index));\n    is->eof = false;\n\n    pkt = av_packet_alloc();\n    if (!pkt) {\n        LOG_ERROR(\"Could not allocate packet.\");\n        ret = AVERROR(ENOMEM);\n        goto fail;\n    }\n    ic = avformat_alloc_context();\n    if (!ic) {\n        LOG_ERROR(\"Could not allocate context.\");\n        ret = AVERROR(ENOMEM);\n        goto fail;\n    }\n    ic->interrupt_callback.callback = M_DecodeInterruptCB;\n    ic->interrupt_callback.opaque = is;\n    err = avformat_open_input(&ic, is->filename, nullptr, nullptr);\n    if (err < 0) {\n        LOG_ERROR(\n            \"Error while opening file %s: %s\", is->filename, av_err2str(err));\n        ret = -1;\n        goto fail;\n    }\n\n    is->ic = ic;\n\n    avformat_find_stream_info(ic, nullptr);\n#if LIBAVFORMAT_VERSION_MAJOR < 59\n    av_format_inject_global_side_data(ic);\n#endif\n\n    if (ic->pb) {\n        ic->pb->eof_reached = 0;\n    }\n\n    is->max_frame_duration =\n        (ic->iformat->flags & AVFMT_TS_DISCONT) ? 10.0 : 3600.0;\n\n    for (int i = 0; i < (signed)ic->nb_streams; i++) {\n        AVStream *st = ic->streams[i];\n        enum AVMediaType type = st->codecpar->codec_type;\n        st->discard = AVDISCARD_ALL;\n    }\n\n    st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(\n        ic, AVMEDIA_TYPE_VIDEO, st_index[AVMEDIA_TYPE_VIDEO], -1, nullptr, 0);\n\n    st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream(\n        ic, AVMEDIA_TYPE_AUDIO, st_index[AVMEDIA_TYPE_AUDIO],\n        st_index[AVMEDIA_TYPE_VIDEO], nullptr, 0);\n\n    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {\n        AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];\n        AVCodecParameters *codecpar = st->codecpar;\n        AVRational sar = av_guess_sample_aspect_ratio(ic, st, nullptr);\n    }\n\n    if (is->audio_enabled && st_index[AVMEDIA_TYPE_AUDIO] >= 0) {\n        M_StreamComponentOpen(is, st_index[AVMEDIA_TYPE_AUDIO]);\n    }\n\n    ret = -1;\n    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {\n        ret = M_StreamComponentOpen(is, st_index[AVMEDIA_TYPE_VIDEO]);\n    }\n\n    if (is->video_stream < 0 && is->audio_stream < 0) {\n        LOG_ERROR(\"Failed to decode file\");\n        ret = -1;\n        goto fail;\n    }\n\n    while (1) {\n        if (is->abort_request) {\n            break;\n        }\n        if (is->paused != is->last_paused) {\n            is->last_paused = is->paused;\n            if (is->paused) {\n                is->read_pause_return = av_read_pause(ic);\n            } else {\n                av_read_play(ic);\n            }\n        }\n        if (is->queue_attachments_req) {\n            if (is->video_st\n                && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {\n                if ((ret = av_packet_ref(pkt, &is->video_st->attached_pic))\n                    < 0) {\n                    goto fail;\n                }\n                M_PacketQueuePut(&is->videoq, pkt);\n                M_PacketQueuePutNullPacket(&is->videoq, pkt, is->video_stream);\n            }\n            is->queue_attachments_req = false;\n        }\n\n        if (is->audioq.size + is->videoq.size > MAX_QUEUE_SIZE\n            || (M_StreamHasEnoughPackets(\n                    is->audio_st, is->audio_stream, &is->audioq)\n                && M_StreamHasEnoughPackets(\n                    is->video_st, is->video_stream, &is->videoq))) {\n            SDL_LockMutex(wait_mutex);\n            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);\n            SDL_UnlockMutex(wait_mutex);\n            continue;\n        }\n        if (!is->paused\n            && (!is->audio_st\n                || (is->auddec.finished == is->audioq.serial\n                    && M_FrameQueueNBRemaining(&is->sampq) == 0))\n            && (!is->video_st\n                || (is->viddec.finished == is->videoq.serial\n                    && M_FrameQueueNBRemaining(&is->pictq) == 0))) {\n            ret = AVERROR_EOF;\n            goto fail;\n        }\n        ret = av_read_frame(ic, pkt);\n        if (ret < 0) {\n            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {\n                if (is->video_stream >= 0) {\n                    M_PacketQueuePutNullPacket(\n                        &is->videoq, pkt, is->video_stream);\n                }\n                if (is->audio_stream >= 0) {\n                    M_PacketQueuePutNullPacket(\n                        &is->audioq, pkt, is->audio_stream);\n                }\n                is->eof = true;\n            }\n            if (ic->pb && ic->pb->error) {\n                goto fail;\n            }\n            SDL_LockMutex(wait_mutex);\n            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);\n            SDL_UnlockMutex(wait_mutex);\n            continue;\n        } else {\n            is->eof = false;\n        }\n        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;\n        if (pkt->stream_index == is->audio_stream) {\n            M_PacketQueuePut(&is->audioq, pkt);\n        } else if (\n            pkt->stream_index == is->video_stream\n            && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {\n            M_PacketQueuePut(&is->videoq, pkt);\n        } else {\n            av_packet_unref(pkt);\n        }\n    }\n\n    ret = 0;\nfail:\n    if (ic && !is->ic) {\n        avformat_close_input(&ic);\n    }\n\n    av_packet_free(&pkt);\n    is->playback_finished = true;\n    SDL_DestroyMutex(wait_mutex);\n    return 0;\n}\n\nstatic M_STATE *M_StreamOpen(const char *filename)\n{\n    M_STATE *const is = av_mallocz(sizeof(M_STATE));\n    if (is == nullptr) {\n        return nullptr;\n    }\n    is->video_stream = -1;\n    is->audio_stream = -1;\n\n    is->filename = av_strdup(filename);\n    if (is->filename == nullptr) {\n        goto fail;\n    }\n\n    is->iformat = nullptr;\n\n    if (M_FrameQueueInit(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1)\n        < 0) {\n        goto fail;\n    }\n    if (M_FrameQueueInit(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0) {\n        goto fail;\n    }\n\n    if (M_PacketQueueInit(&is->videoq) < 0) {\n        goto fail;\n    }\n    if (M_PacketQueueInit(&is->audioq) < 0) {\n        goto fail;\n    }\n\n    if (!(is->continue_read_thread = SDL_CreateCond())) {\n        LOG_ERROR(\"SDL_CreateCond(): %s\", SDL_GetError());\n        goto fail;\n    }\n\n    M_InitClock(&is->vidclk, &is->videoq.serial);\n    M_InitClock(&is->audclk, &is->audioq.serial);\n    M_InitClock(&is->extclk, &is->extclk.serial);\n    is->audio_clock_serial = -1;\n    is->audio_volume = SDL_MIX_MAXVOLUME;\n    is->av_sync_type = AV_SYNC_AUDIO_MASTER;\n    return is;\n\nfail:\n    M_StreamClose(is);\n    return nullptr;\n}\n\nVIDEO *Video_Open(const char *const file_path)\n{\n    if (file_path == nullptr || String_IsEmpty(file_path)) {\n        LOG_ERROR(\"Cannot open video: empty file path\");\n        return nullptr;\n    }\n\n    LOG_DEBUG(\"Playing video: %s\", file_path);\n    int flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;\n    if (SDL_Init(flags)) {\n        LOG_ERROR(\"Could not initialize SDL - %s\", SDL_GetError());\n        return nullptr;\n    }\n\n    SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);\n    SDL_EventState(SDL_USEREVENT, SDL_IGNORE);\n\n    VIDEO *const result = Memory_Alloc(sizeof(VIDEO));\n\n    result->priv = M_StreamOpen(file_path);\n    if (result->priv == nullptr) {\n        Memory_Free(result);\n        LOG_ERROR(\"Failed to initialize video!\");\n        return nullptr;\n    }\n\n    result->path = Memory_DupStr(file_path);\n    result->is_playing = true;\n    return result;\n}\n\nvoid Video_PumpEvents(VIDEO *video)\n{\n    M_STATE *const is = video->priv;\n\n    if (is->remaining_time > 0.0) {\n        av_usleep((int64_t)(is->remaining_time * 1000000.0));\n    }\n    is->remaining_time = REFRESH_RATE;\n    if (!is->paused || is->force_refresh) {\n        M_VideoRefresh(is, &is->remaining_time);\n    }\n\n    video->is_playing = !is->abort_request && !is->playback_finished;\n}\n\nvoid Video_SetAudioEnabled(VIDEO *const video, const bool enabled)\n{\n    M_STATE *const is = video->priv;\n    is->audio_enabled = enabled;\n}\n\nvoid Video_SetVolume(VIDEO *const video, const double volume)\n{\n    M_STATE *const is = video->priv;\n    is->audio_volume = volume * SDL_MIX_MAXVOLUME;\n}\n\nvoid Video_Start(VIDEO *const video)\n{\n    M_STATE *const is = video->priv;\n    is->remaining_time = 0.0;\n    is->read_tid = SDL_CreateThread(M_ReadThread, \"read_thread\", is);\n    if (is->read_tid == nullptr) {\n        LOG_ERROR(\"Error starting read thread: %s\", SDL_GetError());\n    }\n}\n\nvoid Video_Stop(VIDEO *const video)\n{\n    M_STATE *const is = video->priv;\n    is->abort_request = true;\n}\n\nvoid Video_Close(VIDEO *const video)\n{\n    M_STATE *const is = video->priv;\n    if (is) {\n        M_StreamClose(is);\n    }\n\n    LOG_DEBUG(\"Finished playing video: %s\", video->path);\n\n    Memory_Free((char *)video->path);\n    Memory_Free(video);\n}\n\nvoid Video_SetSurfaceSize(\n    VIDEO *const video, const int32_t surface_width,\n    const int32_t surface_height)\n{\n    M_STATE *const is = video->priv;\n    if (is->surface_width == surface_width\n        && is->surface_height == surface_height) {\n        return;\n    }\n\n    M_ReallocPrimarySurface(is, surface_width, surface_height, false);\n}\n\nvoid Video_SetSurfacePixelFormat(VIDEO *video, enum AVPixelFormat pixel_format)\n{\n    M_STATE *const is = video->priv;\n    if (is->primary_surface_pixel_format == pixel_format) {\n        return;\n    }\n\n    is->primary_surface_pixel_format = pixel_format;\n    M_ReallocPrimarySurface(is, is->surface_width, is->surface_height, false);\n}\n\nvoid Video_SetSurfaceStride(VIDEO *video, const int32_t stride)\n{\n    M_STATE *const is = video->priv;\n    if (is->primary_surface_stride == stride) {\n        return;\n    }\n\n    is->primary_surface_stride = stride;\n    M_ReallocPrimarySurface(is, is->surface_width, is->surface_height, false);\n}\n\nvoid Video_SetSurfaceAllocatorFunc(\n    VIDEO *const video, const VIDEO_SURFACE_ALLOCATOR_FUNC func,\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->surface_allocator_func = func;\n    is->surface_allocator_func_user_data = user_data;\n}\n\nvoid Video_SetSurfaceDeallocatorFunc(\n    VIDEO *const video, void (*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->surface_deallocator_func = func;\n    is->surface_deallocator_func_user_data = user_data;\n}\n\nvoid Video_SetSurfaceClearFunc(\n    VIDEO *const video, void (*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->surface_clear_func = func;\n    is->surface_clear_func_user_data = user_data;\n}\n\nvoid Video_SetSurfaceLockFunc(\n    VIDEO *const video, void *(*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->surface_lock_func = func;\n    is->surface_lock_func_user_data = user_data;\n}\n\nvoid Video_SetSurfaceUnlockFunc(\n    VIDEO *const video, void (*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->surface_unlock_func = func;\n    is->surface_unlock_func_user_data = user_data;\n}\n\nvoid Video_SetSurfaceUploadFunc(\n    VIDEO *const video, void (*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->surface_upload_func = func;\n    is->surface_upload_func_user_data = user_data;\n}\n\nvoid Video_SetRenderBeginFunc(\n    VIDEO *const video, void (*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->render_begin_func = func;\n    is->render_begin_func_user_data = user_data;\n}\n\nvoid Video_SetRenderEndFunc(\n    VIDEO *const video, void (*func)(void *surface, void *user_data),\n    void *const user_data)\n{\n    M_STATE *const is = video->priv;\n    is->render_end_func = func;\n    is->render_end_func_user_data = user_data;\n}\n\nvoid Video_SetExternalAudioClock(VIDEO *const video, const double timestamp)\n{\n    M_STATE *const is = video->priv;\n    M_SetClock(&is->extclk, timestamp, is->extclk.serial);\n}\n\nvoid Video_SetPaused(VIDEO *const video, const bool paused)\n{\n    M_STATE *const is = video->priv;\n    is->paused = paused;\n    if (!paused) {\n        is->force_refresh = true;\n    }\n}\n"
  },
  {
    "path": "src/trx/av/video.h",
    "content": "#pragma once\n\n#include <libavutil/pixfmt.h>\n#include <stdint.h>\n\ntypedef struct {\n    const char *path;\n    bool is_playing;\n    void *priv;\n} VIDEO;\n\ntypedef void *(*VIDEO_SURFACE_ALLOCATOR_FUNC)(\n    int32_t width, int32_t height, void *user_data);\n\nVIDEO *Video_Open(const char *path);\nvoid Video_SetAudioEnabled(VIDEO *video, bool enabled);\nvoid Video_SetVolume(VIDEO *video, double volume);\nvoid Video_SetSurfaceSize(VIDEO *video, int32_t width, int32_t height);\nvoid Video_SetSurfacePixelFormat(VIDEO *video, enum AVPixelFormat pixel_format);\nvoid Video_SetSurfaceStride(VIDEO *video, int32_t stride);\nvoid Video_SetSurfaceAllocatorFunc(\n    VIDEO *video, VIDEO_SURFACE_ALLOCATOR_FUNC func, void *user_data);\nvoid Video_SetSurfaceDeallocatorFunc(\n    VIDEO *video, void (*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetSurfaceClearFunc(\n    VIDEO *video, void (*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetSurfaceLockFunc(\n    VIDEO *video, void *(*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetSurfaceUnlockFunc(\n    VIDEO *video, void (*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetSurfaceUploadFunc(\n    VIDEO *video, void (*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetRenderBeginFunc(\n    VIDEO *video, void (*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetRenderEndFunc(\n    VIDEO *video, void (*func)(void *surface, void *user_data),\n    void *user_data);\nvoid Video_SetExternalAudioClock(VIDEO *video, double timestamp);\nvoid Video_SetPaused(VIDEO *video, bool paused);\nvoid Video_Start(VIDEO *video);\nvoid Video_Stop(VIDEO *video);\nvoid Video_PumpEvents(VIDEO *video);\nvoid Video_Close(VIDEO *video);\n"
  },
  {
    "path": "src/trx/config/common.c",
    "content": "#include <trx/config/common.h>\n\n#include <trx/config/dynamic_enum.h>\n#include <trx/config/file.h>\n#include <trx/config/priv.h>\n#include <trx/config/vars.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow/vars.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/shell.h>\n\n#include <stdio.h>\n#include <string.h>\n\n// In-memory list of pointers to config options enforced by the game flow.\nstatic VECTOR *m_EnforcedOptions = nullptr;\n// In-memory list of pointers to config options hidden by the game flow.\nstatic VECTOR *m_HiddenOptions = nullptr;\n\nstatic EVENT_MANAGER *m_EventManager = nullptr;\n\nstatic void M_FreeStringOptionValues(void)\n{\n    const CONFIG_OPTION *option = Config_GetOptionMap();\n    while (option != nullptr && option->target != nullptr) {\n        if (option->type == COT_STRING || option->type == COT_DYNAMIC_ENUM) {\n            Memory_Free(*(char **)option->target);\n        }\n        option++;\n    }\n}\n\n__attribute__((constructor)) static void M_Init(void)\n{\n    m_EventManager = EventManager_Create();\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    EventManager_Free(m_EventManager);\n    m_EventManager = nullptr;\n\n    M_FreeStringOptionValues();\n    Memory_FreePointer(&g_Config.default_path);\n    Memory_FreePointer(&g_Config.enforced_path);\n\n    if (m_EnforcedOptions != nullptr) {\n        Vector_Free(m_EnforcedOptions);\n        m_EnforcedOptions = nullptr;\n    }\n    if (m_HiddenOptions != nullptr) {\n        Vector_Free(m_HiddenOptions);\n        m_HiddenOptions = nullptr;\n    }\n}\n\nvoid Config_ApplyDefaultSettings(void)\n{\n    const CONFIG_OPTION *option = Config_GetOptionMap();\n    while (option->target != nullptr) {\n        Config_RestoreOptionDefault(option->target);\n        option++;\n    }\n}\n\nbool Config_Read(\n    const char *const default_path, const char *const enforced_path)\n{\n    // Always initialize the config, even if the file is missing, so that\n    // the game can interact with these properties.\n    Memory_FreePointer(&g_Config.default_path);\n    Memory_FreePointer(&g_Config.enforced_path);\n    g_Config.default_path = Memory_DupStr(default_path);\n    g_Config.enforced_path = Memory_DupStr(enforced_path);\n    g_Config.loaded = true;\n\n    LOG_DEBUG(\"Reading config\");\n    LOG_DEBUG(\"  default_path=%s\", g_Config.default_path);\n    LOG_DEBUG(\"  enforced_path=%s\", g_Config.enforced_path);\n    if (m_EnforcedOptions == nullptr) {\n        m_EnforcedOptions = Vector_Create(sizeof(void *));\n    } else {\n        Vector_ClearRealloc(m_EnforcedOptions);\n    }\n    if (m_HiddenOptions == nullptr) {\n        m_HiddenOptions = Vector_Create(sizeof(void *));\n    } else {\n        Vector_ClearRealloc(m_HiddenOptions);\n    }\n    const CONFIG_IO_ARGS args = {\n        .default_path = g_Config.default_path,\n        .enforced_path = g_Config.enforced_path,\n        .action = &Config_LoadFromJSON,\n        .enforced_targets = m_EnforcedOptions,\n        .hidden_targets = m_HiddenOptions,\n    };\n    const bool result = ConfigFile_Read(&args);\n    if (result) {\n        LOG_DEBUG(\"Config loaded\");\n    } else {\n        LOG_WARNING(\"Errors while loading config\");\n    }\n    Config_Sanitize();\n    g_SavedConfig = g_Config;\n    return result;\n}\n\nbool Config_Update(void)\n{\n    Config_Sanitize();\n    if (memcmp(&g_Config, &g_SavedConfig, sizeof(CONFIG)) == 0) {\n        return false;\n    }\n\n    if (m_EventManager != nullptr) {\n        const EVENT event = {\n            .name = \"change\",\n            .sender = nullptr,\n            .data = nullptr,\n        };\n        EventManager_Fire(m_EventManager, &event);\n    }\n    g_Config.dirty = false;\n    g_SavedConfig = g_Config;\n    return true;\n}\n\nbool Config_Write(void)\n{\n    ASSERT(g_Config.default_path != nullptr);\n    const CONFIG_IO_ARGS args = {\n        .default_path = g_Config.default_path,\n        .enforced_path = g_Config.enforced_path,\n        .action = &Config_DumpToJSON,\n    };\n    return ConfigFile_Write(&args);\n}\n\nint32_t Config_SubscribeChanges(\n    const EVENT_LISTENER listener, void *const user_data)\n{\n    ASSERT(m_EventManager != nullptr);\n    return EventManager_Subscribe(\n        m_EventManager, \"change\", nullptr, listener, user_data);\n}\n\nvoid Config_UnsubscribeChanges(const int32_t listener_id)\n{\n    ASSERT(m_EventManager != nullptr);\n    EventManager_Unsubscribe(m_EventManager, listener_id);\n}\n\nconst CONFIG_OPTION *Config_GetOption(const void *const target)\n{\n    const CONFIG_OPTION *option = Config_GetOptionMap();\n    if (option == nullptr) {\n        return nullptr;\n    }\n    while (option->target != nullptr) {\n        if (option->target == target) {\n            return option;\n        }\n        option++;\n    }\n    return nullptr;\n}\n\nbool Config_IsOptionEnforced(const void *const target)\n{\n    return m_EnforcedOptions != nullptr\n        && Vector_Contains(m_EnforcedOptions, &target);\n}\n\nbool Config_IsOptionHidden(const void *const target)\n{\n    return m_HiddenOptions != nullptr\n        && Vector_Contains(m_HiddenOptions, &target);\n}\n\nbool Config_IsOptionAtDefault(const void *const target)\n{\n    const CONFIG_OPTION *option = Config_GetOption(target);\n    if (target == nullptr) {\n        return true;\n    }\n    switch (option->type) {\n    case COT_BOOL:\n        return *(bool *)option->target == *(bool *)option->default_value;\n    case COT_INT32:\n        return *(int32_t *)option->target == *(int32_t *)option->default_value;\n    case COT_FLOAT:\n    case COT_FLOAT_PERCENT:\n        return *(float *)option->target == *(float *)option->default_value;\n    case COT_DOUBLE:\n        return *(double *)option->target == *(double *)option->default_value;\n    case COT_RGB888: {\n        const RGB_888 cur = *(RGB_888 *)option->target;\n        const RGB_888 def = *(RGB_888 *)option->default_value;\n        return cur.r == def.r && cur.g == def.g && cur.b == def.b;\n    }\n    case COT_ENUM:\n        return *(int32_t *)option->target == *(int32_t *)option->default_value;\n        break;\n    case COT_STRING:\n    case COT_DYNAMIC_ENUM: {\n        const char *const cur = *(char **)option->target;\n        const char *const def = (const char *)option->default_value;\n        if (cur == nullptr && def == nullptr) {\n            return true;\n        }\n        if (cur == nullptr || def == nullptr) {\n            return false;\n        }\n        return strcmp(cur, def) == 0;\n    }\n    }\n    return true;\n}\n\nbool Config_RestoreOptionDefault(const void *const target)\n{\n    const CONFIG_OPTION *option = Config_GetOption(target);\n    if (target == nullptr) {\n        return false;\n    }\n    switch (option->type) {\n    case COT_BOOL:\n        *(bool *)option->target = *(bool *)option->default_value;\n        return true;\n    case COT_INT32:\n        *(int32_t *)option->target = *(int32_t *)option->default_value;\n        return true;\n    case COT_FLOAT:\n    case COT_FLOAT_PERCENT:\n        *(float *)option->target = *(float *)option->default_value;\n        return true;\n    case COT_DOUBLE:\n        *(double *)option->target = *(double *)option->default_value;\n        return true;\n    case COT_RGB888:\n        *(RGB_888 *)option->target = *(RGB_888 *)option->default_value;\n        return true;\n    case COT_ENUM:\n        *(int32_t *)option->target = *(int32_t *)option->default_value;\n        return true;\n    case COT_STRING:\n    case COT_DYNAMIC_ENUM: {\n        char **const p = (char **)option->target;\n        const char *const def = (const char *)option->default_value;\n        char *const old = *p;\n        *p = def != nullptr ? Memory_DupStr(def) : nullptr;\n        // VERY IMPORTANT: free the memory AFTER we allocate, so that we force\n        // the pointer to get a different macro, so that change subscribers\n        // can see the string has changed by comparing just the pointers.\n        Memory_Free(old);\n        return true;\n    }\n    }\n    return false;\n}\n\nstatic bool M_ParseBool(const char *const value, bool *const result)\n{\n    if (String_Match(value, \"^(on|true|1)$\")) {\n        *result = true;\n        return true;\n    }\n    if (String_Match(value, \"^(off|false|0)$\")) {\n        *result = false;\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_ParseInt32(const char *const value, int32_t *const result)\n{\n    return sscanf(value, \"%d\", result) == 1;\n}\n\nstatic bool M_ParseFloat(const char *const value, float *const result)\n{\n    return sscanf(value, \"%f\", result) == 1;\n}\n\nstatic bool M_ParseDouble(const char *const value, double *const result)\n{\n    return sscanf(value, \"%lf\", result) == 1;\n}\n\nstatic bool M_ParseEnum(\n    const CONFIG_OPTION *const option, const char *const value,\n    const bool allow_numeric, int32_t *const result)\n{\n    const int32_t mapped = EnumMap_Get(option->param, value, -1);\n    if (mapped != -1) {\n        *result = mapped;\n        return true;\n    }\n    if (allow_numeric) {\n        return M_ParseInt32(value, result);\n    }\n    return false;\n}\n\nstatic bool M_ParseRGB888(const char *const value, RGB_888 *const result)\n{\n    return String_ParseRGB888(value, result);\n}\n\nstatic const char *M_FormatBool(const bool value)\n{\n    return String_FormatStatic(\"%d\", value);\n}\n\nstatic const char *M_FormatBoolHuman(const bool value)\n{\n    return value ? GS(\"general/misc/on\") : GS(\"general/misc/off\");\n}\n\nstatic const char *M_FormatInt32(const int32_t value)\n{\n    return String_FormatStatic(\"%d\", value);\n}\n\nstatic const char *M_FormatFloat(const float value)\n{\n    return String_FormatStatic(\"%.2f\", value);\n}\n\nstatic const char *M_FormatFloatPercent(const float value)\n{\n    return String_FormatStatic(\"%.0f%%\", value);\n}\n\nstatic const char *M_FormatDouble(const double value)\n{\n    return String_FormatStatic(\"%.2f\", value);\n}\n\nstatic const char *M_FormatEnumMachine(\n    const CONFIG_OPTION *const option, const int32_t value)\n{\n    return String_FormatStatic(\"%s\", EnumMap_ToString(option->param, value));\n}\n\nstatic const char *M_FormatEnumHuman(\n    const CONFIG_OPTION *const option, const int32_t value)\n{\n    const char *const localized = EnumMap_GetLabel(option->param, value);\n    ASSERT(localized != nullptr);\n    return localized;\n}\n\nstatic const char *M_FormatRGB888(const RGB_888 *const value)\n{\n    return String_FormatStatic(\n        \"%02hhx%02hhx%02hhx\", value->r, value->g, value->b);\n}\n\nstatic const char *M_FormatString(const char *const value)\n{\n    return String_FormatStatic(\"%s\", value != nullptr ? value : \"\");\n}\n\nconst char *Config_GetOptionValueAsString(\n    const CONFIG_OPTION *const option, const bool human_readable)\n{\n    if (option == nullptr) {\n        return nullptr;\n    }\n    switch (option->type) {\n    case COT_BOOL:\n        return human_readable ? M_FormatBoolHuman(*(bool *)option->target)\n                              : M_FormatBool(*(bool *)option->target);\n    case COT_INT32:\n        return M_FormatInt32(*(int32_t *)option->target);\n    case COT_FLOAT:\n        return M_FormatFloat(*(float *)option->target);\n    case COT_FLOAT_PERCENT:\n        return M_FormatFloatPercent((*(float *)option->target) * 100.0f);\n    case COT_DOUBLE:\n        return M_FormatDouble(*(double *)option->target);\n    case COT_ENUM:\n        return human_readable\n            ? M_FormatEnumHuman(option, *(int32_t *)option->target)\n            : M_FormatEnumMachine(option, *(int32_t *)option->target);\n    case COT_RGB888:\n        return M_FormatRGB888(option->target);\n    case COT_STRING:\n        return M_FormatString(*(char **)option->target);\n    case COT_DYNAMIC_ENUM: {\n        if (human_readable) {\n            const char *const value = *(char **)option->target;\n            const char *const label =\n                Config_DynamicEnum_GetLabelForValue(option, value);\n            if (label != nullptr) {\n                return label;\n            }\n        }\n        return M_FormatString(*(char **)option->target);\n    }\n    default:\n        return nullptr;\n    }\n}\n\nconst char *Config_GetOptionTitle(const CONFIG_OPTION *const opt)\n{\n    if (opt == nullptr || opt->name == nullptr) {\n        return nullptr;\n    }\n    return GameString_Get(String_FormatStatic(\"settings/%s/title\", opt->name));\n}\n\nconst char *Config_GetOptionDescription(const CONFIG_OPTION *const opt)\n{\n    if (opt == nullptr || opt->name == nullptr) {\n        return nullptr;\n    }\n    return GameString_Get(\n        String_FormatStatic(\"settings/%s/description\", opt->name));\n}\n\nchar *Config_NormalizeOptionValueString(\n    const CONFIG_OPTION *const option, const char *const value,\n    const bool human_readable)\n{\n    if (option == nullptr) {\n        return Memory_DupStr(value != nullptr ? value : \"\");\n    }\n\n    const char *const input = value != nullptr ? value : \"\";\n\n#define L_NORMALIZE_TYPED(type_, parse_expr_, format_expr_)                    \\\n    do {                                                                       \\\n        type_ parsed;                                                          \\\n        if (!(parse_expr_)) {                                                  \\\n            return Memory_DupStr(input);                                       \\\n        }                                                                      \\\n        return Memory_DupStr(format_expr_);                                    \\\n    } while (false)\n\n    switch (option->type) {\n    case COT_BOOL:\n        L_NORMALIZE_TYPED(\n            bool, M_ParseBool(input, &parsed),\n            human_readable ? M_FormatBoolHuman(parsed) : M_FormatBool(parsed));\n    case COT_INT32:\n        L_NORMALIZE_TYPED(\n            int32_t, M_ParseInt32(input, &parsed), M_FormatInt32(parsed));\n    case COT_FLOAT:\n        L_NORMALIZE_TYPED(\n            float, M_ParseFloat(input, &parsed), M_FormatFloat(parsed));\n    case COT_FLOAT_PERCENT:\n        L_NORMALIZE_TYPED(\n            float, M_ParseFloat(input, &parsed), M_FormatFloatPercent(parsed));\n    case COT_DOUBLE:\n        L_NORMALIZE_TYPED(\n            double, M_ParseDouble(input, &parsed), M_FormatDouble(parsed));\n    case COT_ENUM:\n        L_NORMALIZE_TYPED(\n            int32_t, M_ParseEnum(option, input, true, &parsed),\n            human_readable ? M_FormatEnumHuman(option, parsed)\n                           : M_FormatEnumMachine(option, parsed));\n    case COT_RGB888:\n        L_NORMALIZE_TYPED(\n            RGB_888, M_ParseRGB888(input, &parsed), M_FormatRGB888(&parsed));\n    case COT_STRING:\n        return Memory_DupStr(M_FormatString(input));\n    case COT_DYNAMIC_ENUM:\n        if (!Config_DynamicEnum_IsValidValue(option, input)) {\n            return Memory_DupStr(input);\n        }\n        if (human_readable) {\n            const char *const label =\n                Config_DynamicEnum_GetLabelForValue(option, input);\n            if (label != nullptr) {\n                return Memory_DupStr(label);\n            }\n        }\n        return Memory_DupStr(M_FormatString(input));\n    }\n#undef L_NORMALIZE_TYPED\n\n    return Memory_DupStr(input);\n}\n\nbool Config_SetOptionValueFromString(\n    const CONFIG_OPTION *const option, const char *const new_value)\n{\n    ASSERT(option != nullptr);\n    ASSERT(option->target != nullptr);\n    switch (option->type) {\n    case COT_BOOL: {\n        bool parsed;\n        if (M_ParseBool(new_value, &parsed)) {\n            *(bool *)option->target = parsed;\n            return true;\n        }\n        break;\n    }\n\n    case COT_INT32: {\n        int32_t parsed;\n        if (M_ParseInt32(new_value, &parsed)) {\n            *(int32_t *)option->target = parsed;\n            return true;\n        }\n        break;\n    }\n\n    case COT_FLOAT: {\n        float parsed;\n        if (M_ParseFloat(new_value, &parsed)) {\n            *(float *)option->target = parsed;\n            return true;\n        }\n        break;\n    }\n\n    case COT_FLOAT_PERCENT: {\n        float parsed;\n        if (M_ParseFloat(new_value, &parsed)) {\n            *(float *)option->target = parsed / 100.0f;\n            return true;\n        }\n        break;\n    }\n\n    case COT_DOUBLE: {\n        double parsed;\n        if (M_ParseDouble(new_value, &parsed)) {\n            *(double *)option->target = parsed;\n            return true;\n        }\n        break;\n    }\n\n    case COT_ENUM: {\n        int32_t parsed;\n        if (M_ParseEnum(option, new_value, false, &parsed)) {\n            *(int32_t *)option->target = parsed;\n            return true;\n        }\n        break;\n    }\n\n    case COT_RGB888: {\n        RGB_888 parsed;\n        if (M_ParseRGB888(new_value, &parsed)) {\n            *(RGB_888 *)option->target = parsed;\n            return true;\n        }\n        break;\n    }\n\n    case COT_STRING:\n    case COT_DYNAMIC_ENUM: {\n        if (option->type == COT_DYNAMIC_ENUM\n            && !Config_DynamicEnum_IsValidValue(option, new_value)) {\n            return false;\n        }\n        char **const p = (char **)option->target;\n        char *const old = *p;\n        *p = new_value != nullptr ? Memory_DupStr(new_value) : nullptr;\n        // VERY IMPORTANT: free the memory AFTER we allocate, so that we force\n        // the pointer to get a different macro, so that change subscribers\n        // can see the string has changed by comparing just the pointers.\n        Memory_Free(old);\n        return true;\n    }\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "src/trx/config/common.h",
    "content": "#pragma once\n\n#include <trx/config/option.h>\n#include <trx/core/event_manager.h>\n#include <trx/core/json.h>\n\n#include <stdint.h>\n\nvoid Config_ApplyDefaultSettings(void);\nbool Config_Read(const char *default_path, const char *enforced_path);\nbool Config_Write(void);\nbool Config_Update(void);\n\nconst CONFIG_OPTION *Config_GetOptionMap(void);\n\nint32_t Config_SubscribeChanges(EVENT_LISTENER listener, void *user_data);\nvoid Config_UnsubscribeChanges(int32_t listener_id);\n\n// Retrieves CONFIG_OPTION related to the target setting (a pointer into a\n// g_Config property).\nconst CONFIG_OPTION *Config_GetOption(const void *target);\n\n// Returns true if a given setting was enforced by the game flow file.\nbool Config_IsOptionEnforced(const void *target);\n\n// Returns true if a given setting should be hidden in settings dialogs.\nbool Config_IsOptionHidden(const void *target);\n\n// Returns whether the given setting's current value is the same as its default.\nbool Config_IsOptionAtDefault(const void *target);\n\n// Restores the given setting's default value.\nbool Config_RestoreOptionDefault(const void *target);\n\n// Get a flat string name of an option.\nconst char *Config_ResolveOptionName(const char *option_name);\n\n// Retrieve an option given a string path.\nconst CONFIG_OPTION *Config_GetOptionByPath(const char *path);\n\n// Returns translated title for a config option.\nconst char *Config_GetOptionTitle(const CONFIG_OPTION *opt);\n\n// Returns translated description for a config option.\nconst char *Config_GetOptionDescription(const CONFIG_OPTION *opt);\n\n// Formats the current value of a config option as a static string.\n// The string must not be freed and is short lived.\nconst char *Config_GetOptionValueAsString(\n    const CONFIG_OPTION *option, bool human_readable);\n\n// Normalizes an option value string using the same parser/formatter as config\n// runtime values.\n// Returns an allocated string that must be freed by the caller.\nchar *Config_NormalizeOptionValueString(\n    const CONFIG_OPTION *option, const char *value, bool human_readable);\n\n// Updates the given setting's value from string.\nbool Config_SetOptionValueFromString(\n    const CONFIG_OPTION *option, const char *new_value);\n"
  },
  {
    "path": "src/trx/config/const.h",
    "content": "#pragma once\n\n#define MAX_ASSAULT_TIMES 10\n#define CONFIG_MIN_BRIGHTNESS 0.1f\n#define CONFIG_MAX_BRIGHTNESS 2.0f\n#define CONFIG_MIN_GAMMA 1.0f\n#define CONFIG_MAX_GAMMA 10.0f\n"
  },
  {
    "path": "src/trx/config/dynamic_enum.c",
    "content": "#include <trx/config/dynamic_enum.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <string.h>\n\ntypedef struct {\n    char *value;\n    char *label;\n} M_DYNAMIC_ENUM_VALUE;\n\ntypedef struct M_DYNAMIC_ENUM_REGISTRY_ENTRY {\n    const void *target;\n    VECTOR *values;\n    struct M_DYNAMIC_ENUM_REGISTRY_ENTRY *next;\n} M_DYNAMIC_ENUM_REGISTRY_ENTRY;\n\nstatic M_DYNAMIC_ENUM_REGISTRY_ENTRY *m_Registry = nullptr;\n\nstatic bool M_IsDynamicEnum(const CONFIG_OPTION *const option)\n{\n    return option != nullptr && option->type == COT_DYNAMIC_ENUM;\n}\n\nstatic bool M_IsSameValue(const char *const left, const char *const right)\n{\n    if (left == nullptr && right == nullptr) {\n        return true;\n    }\n    if (left == nullptr || right == nullptr) {\n        return false;\n    }\n    return strcmp(left, right) == 0;\n}\n\nstatic M_DYNAMIC_ENUM_REGISTRY_ENTRY *M_GetRegistryEntry(\n    const CONFIG_OPTION *const option, const bool create)\n{\n    if (option == nullptr) {\n        return nullptr;\n    }\n\n    for (M_DYNAMIC_ENUM_REGISTRY_ENTRY *entry = m_Registry; entry != nullptr;\n         entry = entry->next) {\n        if (entry->target == option->target) {\n            return entry;\n        }\n    }\n\n    if (!create) {\n        return nullptr;\n    }\n\n    M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry = Memory_Alloc(sizeof(*entry));\n    entry->target = option->target;\n    entry->values = Vector_Create(sizeof(M_DYNAMIC_ENUM_VALUE));\n    entry->next = m_Registry;\n    m_Registry = entry;\n    return entry;\n}\n\nstatic void M_FreeValues(VECTOR **const values_ptr)\n{\n    if (values_ptr == nullptr || *values_ptr == nullptr) {\n        return;\n    }\n\n    VECTOR *const values = *values_ptr;\n    for (int32_t i = 0; i < values->count; i++) {\n        M_DYNAMIC_ENUM_VALUE *const dyn_value = Vector_Get(values, i);\n        Memory_FreePointer(&dyn_value->value);\n        Memory_FreePointer(&dyn_value->label);\n    }\n    Vector_Free(values);\n    *values_ptr = nullptr;\n}\n\nstatic int32_t M_FindValueIndex(\n    const CONFIG_OPTION *const option, const char *const value)\n{\n    const M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry =\n        M_GetRegistryEntry(option, false);\n    if (entry == nullptr || entry->values == nullptr) {\n        return -1;\n    }\n\n    for (int32_t i = 0; i < entry->values->count; i++) {\n        const M_DYNAMIC_ENUM_VALUE *const dyn_value =\n            Vector_Get(entry->values, i);\n        if (M_IsSameValue(dyn_value->value, value)) {\n            return i;\n        }\n    }\n\n    return -1;\n}\n\nstatic const M_DYNAMIC_ENUM_VALUE *M_GetValueEntry(\n    const CONFIG_OPTION *const option, const int32_t index)\n{\n    const M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry =\n        M_GetRegistryEntry(option, false);\n    if (entry == nullptr || entry->values == nullptr) {\n        return nullptr;\n    }\n    if (index < 0 || index >= entry->values->count) {\n        return nullptr;\n    }\n    return Vector_Get(entry->values, index);\n}\n\nstatic const char *M_GetDisplayLabel(\n    const M_DYNAMIC_ENUM_VALUE *const dyn_value)\n{\n    if (dyn_value == nullptr) {\n        return \"(null)\";\n    }\n    if (!String_IsEmpty(dyn_value->label)) {\n        const char *const resolved = GameString_Get(dyn_value->label);\n        if (!String_IsEmpty(resolved)) {\n            return resolved;\n        }\n    }\n    if (dyn_value->value != nullptr) {\n        return dyn_value->value;\n    }\n    return \"(null)\";\n}\n\nstatic void M_Shutdown(void)\n{\n    M_DYNAMIC_ENUM_REGISTRY_ENTRY *entry = m_Registry;\n    while (entry != nullptr) {\n        M_DYNAMIC_ENUM_REGISTRY_ENTRY *const next = entry->next;\n        M_FreeValues(&entry->values);\n        Memory_FreePointer(&entry);\n        entry = next;\n    }\n    m_Registry = nullptr;\n}\n\n__attribute__((destructor)) static void M_AtShutdown(void)\n{\n    M_Shutdown();\n}\n\nvoid Config_DynamicEnum_ResetValues(const CONFIG_OPTION *const option)\n{\n    if (!M_IsDynamicEnum(option)) {\n        return;\n    }\n\n    M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry =\n        M_GetRegistryEntry(option, true);\n    ASSERT(entry != nullptr);\n    M_FreeValues(&entry->values);\n    entry->values = Vector_Create(sizeof(M_DYNAMIC_ENUM_VALUE));\n}\n\nbool Config_DynamicEnum_AddValue(\n    const CONFIG_OPTION *const option, const char *const value,\n    const char *const label)\n{\n    if (!M_IsDynamicEnum(option)) {\n        return false;\n    }\n\n    M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry =\n        M_GetRegistryEntry(option, true);\n    ASSERT(entry != nullptr);\n    if (entry->values == nullptr) {\n        entry->values = Vector_Create(sizeof(M_DYNAMIC_ENUM_VALUE));\n    }\n\n    M_DYNAMIC_ENUM_VALUE dyn_value = {\n        .value = value != nullptr ? Memory_DupStr(value) : nullptr,\n        .label = label != nullptr ? Memory_DupStr(label) : nullptr,\n    };\n    Vector_Add(entry->values, &dyn_value);\n    return true;\n}\n\nbool Config_DynamicEnum_IsValidValue(\n    const CONFIG_OPTION *const option, const char *const value)\n{\n    return M_FindValueIndex(option, value) >= 0;\n}\n\nint32_t Config_DynamicEnum_GetValueCount(const CONFIG_OPTION *const option)\n{\n    const M_DYNAMIC_ENUM_REGISTRY_ENTRY *const entry =\n        M_GetRegistryEntry(option, false);\n    if (entry == nullptr || entry->values == nullptr) {\n        return 0;\n    }\n    return entry->values->count;\n}\n\nconst char *Config_DynamicEnum_GetValueAt(\n    const CONFIG_OPTION *const option, const int32_t index)\n{\n    const M_DYNAMIC_ENUM_VALUE *const dyn_value =\n        M_GetValueEntry(option, index);\n    if (dyn_value == nullptr) {\n        return nullptr;\n    }\n    return dyn_value->value;\n}\n\nconst char *Config_DynamicEnum_GetLabelAt(\n    const CONFIG_OPTION *const option, const int32_t index)\n{\n    const M_DYNAMIC_ENUM_VALUE *const dyn_value =\n        M_GetValueEntry(option, index);\n    return M_GetDisplayLabel(dyn_value);\n}\n\nconst char *Config_DynamicEnum_GetLabelForValue(\n    const CONFIG_OPTION *const option, const char *const value)\n{\n    const int32_t idx = M_FindValueIndex(option, value);\n    if (idx < 0) {\n        return value != nullptr ? value : \"(null)\";\n    }\n    return Config_DynamicEnum_GetLabelAt(option, idx);\n}\n\nbool Config_DynamicEnum_CanCycle(\n    const CONFIG_OPTION *const option, const char *const current,\n    const int32_t dir)\n{\n    if (!M_IsDynamicEnum(option) || dir == 0) {\n        return false;\n    }\n\n    const int32_t value_count = Config_DynamicEnum_GetValueCount(option);\n    if (value_count <= 0) {\n        return false;\n    }\n\n    const int32_t cur_idx = M_FindValueIndex(option, current);\n    if (cur_idx < 0) {\n        return true;\n    }\n\n    const int32_t step = dir < 0 ? -1 : 1;\n    const int32_t next_idx = cur_idx + step;\n    return next_idx >= 0 && next_idx < value_count;\n}\n\nconst char *Config_DynamicEnum_GetNext(\n    const CONFIG_OPTION *const option, const char *const current,\n    const int32_t dir)\n{\n    if (!M_IsDynamicEnum(option) || dir == 0) {\n        return nullptr;\n    }\n\n    const int32_t value_count = Config_DynamicEnum_GetValueCount(option);\n    if (value_count <= 0) {\n        return nullptr;\n    }\n\n    const int32_t cur_idx = M_FindValueIndex(option, current);\n    if (cur_idx < 0) {\n        return Config_DynamicEnum_GetValueAt(option, 0);\n    }\n\n    const int32_t step = dir < 0 ? -1 : 1;\n    const int32_t next_idx = cur_idx + step;\n    if (next_idx < 0 || next_idx >= value_count) {\n        return nullptr;\n    }\n\n    return Config_DynamicEnum_GetValueAt(option, next_idx);\n}\n"
  },
  {
    "path": "src/trx/config/dynamic_enum.h",
    "content": "#pragma once\n\n#include <trx/config/option.h>\n\n#include <stdint.h>\n\nvoid Config_DynamicEnum_ResetValues(const CONFIG_OPTION *option);\nbool Config_DynamicEnum_AddValue(\n    const CONFIG_OPTION *option, const char *value, const char *label);\nbool Config_DynamicEnum_IsValidValue(\n    const CONFIG_OPTION *option, const char *value);\nint32_t Config_DynamicEnum_GetValueCount(const CONFIG_OPTION *option);\nconst char *Config_DynamicEnum_GetValueAt(\n    const CONFIG_OPTION *option, int32_t index);\nconst char *Config_DynamicEnum_GetLabelAt(\n    const CONFIG_OPTION *option, int32_t index);\nconst char *Config_DynamicEnum_GetLabelForValue(\n    const CONFIG_OPTION *option, const char *value);\nbool Config_DynamicEnum_CanCycle(\n    const CONFIG_OPTION *option, const char *current, int32_t dir);\nconst char *Config_DynamicEnum_GetNext(\n    const CONFIG_OPTION *option, const char *current, int32_t dir);\n"
  },
  {
    "path": "src/trx/config/enum.c",
    "content": "#include <trx/config/enum.h>\n\n#include <trx/core/enum_map.h>\n\nstatic __attribute__((constructor)) void M_Init(void)\n{\n    ENUM_MAP(ASPECT_MODE, ASPECT_MODE_ANY, \"any\");\n    ENUM_MAP(ASPECT_MODE, ASPECT_MODE_4_3, \"4:3\");\n    ENUM_MAP(ASPECT_MODE, ASPECT_MODE_16_9, \"16:9\");\n    ENUM_MAP(ASPECT_MODE, ASPECT_MODE_16_10, \"16:10\");\n\n    ENUM_MAP(INPUT_BACKEND, INPUT_BACKEND_KEYBOARD, \"keyboard\");\n    ENUM_MAP(INPUT_BACKEND, INPUT_BACKEND_CONTROLLER, \"controller\");\n\n    ENUM_MAP(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_JPEG, \"jpg\");\n    ENUM_MAP(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_JPEG, \"jpeg\");\n    ENUM_MAP(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_PNG, \"png\");\n\n    ENUM_MAP(MUSIC_LOAD_CONDITION, MUSIC_LOAD_CONDITION_NEVER, \"never\");\n    ENUM_MAP(\n        MUSIC_LOAD_CONDITION, MUSIC_LOAD_CONDITION_NON_AMBIENT, \"non-ambient\");\n    ENUM_MAP(MUSIC_LOAD_CONDITION, MUSIC_LOAD_CONDITION_ALWAYS, \"always\");\n\n    ENUM_MAP(LOADING_SCREENS_MODE, LOADING_SCREENS_DISABLED, \"disabled\");\n    ENUM_MAP(LOADING_SCREENS_MODE, LOADING_SCREENS_ALWAYS, \"always\");\n    ENUM_MAP(LOADING_SCREENS_MODE, LOADING_SCREENS_NEW_GAMES, \"new-games\");\n\n    ENUM_MAP(BAR_SHOW_MODE, BAR_SHOW_MODE_NEVER, \"never\");\n    ENUM_MAP(BAR_SHOW_MODE, BAR_SHOW_MODE_ALWAYS, \"always\");\n    ENUM_MAP(BAR_SHOW_MODE, BAR_SHOW_MODE_BOSS_ONLY, \"boss-only\");\n\n    ENUM_MAP(UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_TOP_LEFT, \"top-left\");\n    ENUM_MAP(UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_TOP_CENTER, \"top-center\");\n    ENUM_MAP(UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_TOP_RIGHT, \"top-right\");\n    ENUM_MAP(\n        UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_BOTTOM_LEFT, \"bottom-left\");\n    ENUM_MAP(\n        UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_BOTTOM_CENTER,\n        \"bottom-center\");\n    ENUM_MAP(\n        UI_ELEMENT_LOCATION, UI_ELEMENT_LOCATION_BOTTOM_RIGHT, \"bottom-right\");\n\n    ENUM_MAP(UI_STYLE, UI_STYLE_PS1, \"ps1\");\n    ENUM_MAP(UI_STYLE, UI_STYLE_PC, \"pc\");\n\n    ENUM_MAP(BACKGROUND_TYPE, BK_NONE, \"none\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_TRANSPARENT_MEDIUM, \"transparent\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_TRANSPARENT_DARK, \"transparent-dark\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_BLACK, \"black\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_PATTERN_STATIC, \"pattern-static\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_PATTERN_WAVE, \"pattern-wave\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_IMAGE, \"image\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_MONOCHROME, \"monochrome\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_MONOCHROME_WARM, \"monochrome-warm\");\n    ENUM_MAP(BACKGROUND_TYPE, BK_MONOCHROME_COOL, \"monochrome-cool\");\n\n    ENUM_MAP(CAMERA_MODE, CAMERA_MODE_TR1, \"tr1\");\n    ENUM_MAP(CAMERA_MODE, CAMERA_MODE_TR2, \"tr2\");\n    ENUM_MAP(CAMERA_MODE, CAMERA_MODE_TR3, \"tr3\");\n\n    ENUM_MAP(WALL_GLITCH_MODE, WALL_GLITCH_FIXED, \"fixed\");\n    ENUM_MAP(WALL_GLITCH_MODE, WALL_GLITCH_TR1, \"tr1\");\n    ENUM_MAP(WALL_GLITCH_MODE, WALL_GLITCH_TR2, \"tr2\");\n\n    ENUM_MAP(JUMP_LOCK_MODE, JUMP_LOCK_LEGACY, \"legacy\");\n    ENUM_MAP(JUMP_LOCK_MODE, JUMP_LOCK_TUNED, \"tuned\");\n    ENUM_MAP(JUMP_LOCK_MODE, JUMP_LOCK_DISABLED, \"disabled\");\n\n    ENUM_MAP(LOOK_MODE, LOOK_MODE_RESTRICTED, \"restricted\");\n    ENUM_MAP(LOOK_MODE, LOOK_MODE_ENHANCED, \"enhanced\");\n    ENUM_MAP(LOOK_MODE, LOOK_MODE_UNRESTRICTED, \"unrestricted\");\n\n    ENUM_MAP(QUICK_GUNS_MODE, QUICK_GUNS_MODE_DRAW_ONLY, \"draw-only\");\n    ENUM_MAP(\n        QUICK_GUNS_MODE, QUICK_GUNS_MODE_DRAW_AND_HOLSTER, \"draw-and-holster\");\n\n    ENUM_MAP(LIGHTING_CONTRAST, LIGHTING_CONTRAST_LOW, \"low\");\n    ENUM_MAP(LIGHTING_CONTRAST, LIGHTING_CONTRAST_MEDIUM, \"medium\");\n    ENUM_MAP(LIGHTING_CONTRAST, LIGHTING_CONTRAST_HIGH, \"high\");\n\n    ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_NONE, \"none\");\n    ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_ROLL, \"roll\");\n    ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_ROLL_PITCH, \"roll-pitch\");\n    ENUM_MAP(BILLBOARD_LOCK_MODE, BILLBOARD_LOCK_PERSPECTIVE, \"perspective\");\n\n    ENUM_MAP(TARGET_LOCK_MODE, TARGET_LOCK_MODE_FULL, \"full-lock\");\n    ENUM_MAP(TARGET_LOCK_MODE, TARGET_LOCK_MODE_SEMI, \"semi-lock\");\n    ENUM_MAP(TARGET_LOCK_MODE, TARGET_LOCK_MODE_NONE, \"no-lock\");\n\n    ENUM_MAP(STATS_STYLE, STATS_STYLE_BARE, \"bare\");\n    ENUM_MAP(STATS_STYLE, STATS_STYLE_BORDERED, \"bordered\");\n\n    ENUM_MAP(SHADOW_TYPE, SHADOW_TYPE_OCTAGON, \"octagon\");\n    ENUM_MAP(SHADOW_TYPE, SHADOW_TYPE_CIRCLE, \"circle\");\n    ENUM_MAP(SHADOW_TYPE, SHADOW_TYPE_SPRITE, \"sprite\");\n\n    ENUM_MAP(BLOOD_EFFECTS, BLOOD_EFFECTS_DISABLED, \"disabled\");\n    ENUM_MAP(BLOOD_EFFECTS, BLOOD_EFFECTS_PINK, \"pink\");\n    ENUM_MAP(BLOOD_EFFECTS, BLOOD_EFFECTS_RED, \"red\");\n\n    ENUM_MAP(SUNGLASSES_MODE, SUNGLASSES_MODE_OFF, \"off\");\n    ENUM_MAP(SUNGLASSES_MODE, SUNGLASSES_MODE_OPAQUE, \"opaque\");\n    ENUM_MAP(SUNGLASSES_MODE, SUNGLASSES_MODE_TRANSPARENT, \"transparent\");\n\n    ENUM_MAP(ALLY_HOSTILITY_POLICY, ALLY_HOSTILITY_POLICY_SHARED, \"shared\");\n    ENUM_MAP(\n        ALLY_HOSTILITY_POLICY, ALLY_HOSTILITY_POLICY_INDIVIDUAL, \"individual\");\n\n    ENUM_MAP(CREATURE_DROWN_POLICY, CREATURE_DROWN_POLICY_NEVER, \"never\");\n    ENUM_MAP(CREATURE_DROWN_POLICY, CREATURE_DROWN_POLICY_DEFAULT, \"default\");\n    ENUM_MAP(\n        CREATURE_DROWN_POLICY, CREATURE_DROWN_POLICY_SUBMERGED, \"accurate\");\n\n    ENUM_MAP(\n        PROJECTILE_AREA_DAMAGE, PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP,\n        \"single-sweep\");\n    ENUM_MAP(\n        PROJECTILE_AREA_DAMAGE, PROJECTILE_AREA_DAMAGE_MULTI_SWEEP,\n        \"multi-sweep\");\n}\n"
  },
  {
    "path": "src/trx/config/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    ASPECT_MODE_4_3,\n    ASPECT_MODE_16_9,\n    ASPECT_MODE_16_10,\n    ASPECT_MODE_ANY,\n} ASPECT_MODE;\n\ntypedef enum {\n    INPUT_BACKEND_KEYBOARD,\n    INPUT_BACKEND_CONTROLLER,\n    INPUT_BACKEND_NUMBER_OF,\n} INPUT_BACKEND;\n\ntypedef enum {\n    INPUT_LAYOUT_DEFAULT,\n    INPUT_LAYOUT_CUSTOM_1,\n    INPUT_LAYOUT_CUSTOM_2,\n    INPUT_LAYOUT_CUSTOM_3,\n    INPUT_LAYOUT_NUMBER_OF,\n} INPUT_LAYOUT;\n\ntypedef enum {\n    SCREENSHOT_FORMAT_JPEG,\n    SCREENSHOT_FORMAT_PNG,\n} SCREENSHOT_FORMAT;\n\ntypedef enum {\n    BK_NONE,\n    BK_TRANSPARENT_MEDIUM,\n    BK_TRANSPARENT_DARK,\n    BK_BLACK,\n    BK_PATTERN_STATIC,\n    BK_PATTERN_WAVE,\n    BK_IMAGE,\n    BK_MONOCHROME,\n    BK_MONOCHROME_COOL,\n    BK_MONOCHROME_WARM,\n} BACKGROUND_TYPE;\n\ntypedef enum {\n    UI_ELEMENT_LOCATION_TOP_LEFT,\n    UI_ELEMENT_LOCATION_TOP_CENTER,\n    UI_ELEMENT_LOCATION_TOP_RIGHT,\n    UI_ELEMENT_LOCATION_BOTTOM_LEFT,\n    UI_ELEMENT_LOCATION_BOTTOM_CENTER,\n    UI_ELEMENT_LOCATION_BOTTOM_RIGHT,\n    UI_ELEMENT_LOCATION_CUSTOM,\n} UI_ELEMENT_LOCATION;\n\ntypedef enum {\n    BAR_SHOW_MODE_NEVER,\n    BAR_SHOW_MODE_ALWAYS,\n    BAR_SHOW_MODE_BOSS_ONLY,\n} BAR_SHOW_MODE;\n\ntypedef enum {\n    MUSIC_LOAD_CONDITION_NEVER,\n    MUSIC_LOAD_CONDITION_NON_AMBIENT,\n    MUSIC_LOAD_CONDITION_ALWAYS,\n} MUSIC_LOAD_CONDITION;\n\ntypedef enum {\n    LOADING_SCREENS_DISABLED,\n    LOADING_SCREENS_ALWAYS,\n    LOADING_SCREENS_NEW_GAMES,\n} LOADING_SCREENS_MODE;\n\ntypedef enum {\n    UI_STYLE_PS1,\n    UI_STYLE_PC,\n} UI_STYLE;\n\ntypedef enum {\n    CAMERA_MODE_TR1,\n    CAMERA_MODE_TR2,\n    CAMERA_MODE_TR3,\n    CAMERA_MODE_NUMBER_OF,\n} CAMERA_MODE;\n\ntypedef enum {\n    WALL_GLITCH_FIXED,\n    WALL_GLITCH_TR1,\n    WALL_GLITCH_TR2,\n} WALL_GLITCH_MODE;\n\ntypedef enum {\n    JUMP_LOCK_LEGACY,\n    JUMP_LOCK_TUNED,\n    JUMP_LOCK_DISABLED,\n    JUMP_LOCK_NUMBER_OF,\n} JUMP_LOCK_MODE;\n\ntypedef enum {\n    LOOK_MODE_RESTRICTED,\n    LOOK_MODE_ENHANCED,\n    LOOK_MODE_UNRESTRICTED,\n} LOOK_MODE;\n\ntypedef enum {\n    QUICK_GUNS_MODE_DRAW_ONLY,\n    QUICK_GUNS_MODE_DRAW_AND_HOLSTER,\n} QUICK_GUNS_MODE;\n\ntypedef enum {\n    LIGHTING_CONTRAST_LOW,\n    LIGHTING_CONTRAST_MEDIUM,\n    LIGHTING_CONTRAST_HIGH,\n    LIGHTING_CONTRAST_NUMBER_OF,\n} LIGHTING_CONTRAST;\n\ntypedef enum {\n    BILLBOARD_LOCK_NONE,\n    BILLBOARD_LOCK_ROLL,\n    BILLBOARD_LOCK_ROLL_PITCH,\n    BILLBOARD_LOCK_PERSPECTIVE,\n    BILLBOARD_LOCK_NUMBER_OF,\n} BILLBOARD_LOCK_MODE;\n\ntypedef enum {\n    TARGET_LOCK_MODE_FULL,\n    TARGET_LOCK_MODE_SEMI,\n    TARGET_LOCK_MODE_NONE,\n} TARGET_LOCK_MODE;\n\ntypedef enum {\n    STATS_STYLE_BARE,\n    STATS_STYLE_BORDERED,\n} STATS_STYLE;\n\ntypedef enum {\n    SHADOW_TYPE_OCTAGON,\n    SHADOW_TYPE_CIRCLE,\n    SHADOW_TYPE_SPRITE,\n    SHADOW_TYPE_NUMBER_OF,\n} SHADOW_TYPE;\n\ntypedef enum {\n    BLOOD_EFFECTS_DISABLED,\n    BLOOD_EFFECTS_PINK,\n    BLOOD_EFFECTS_RED,\n    BLOOD_EFFECTS_NUMBER_OF,\n} BLOOD_EFFECTS;\n\ntypedef enum {\n    ALLY_HOSTILITY_POLICY_INDIVIDUAL,\n    ALLY_HOSTILITY_POLICY_SHARED,\n} ALLY_HOSTILITY_POLICY;\n\ntypedef enum {\n    CREATURE_DROWN_POLICY_NEVER,\n    CREATURE_DROWN_POLICY_DEFAULT,\n    CREATURE_DROWN_POLICY_SUBMERGED,\n} CREATURE_DROWN_POLICY;\n\ntypedef enum {\n    PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP,\n    PROJECTILE_AREA_DAMAGE_MULTI_SWEEP,\n} PROJECTILE_AREA_DAMAGE;\n\ntypedef enum {\n    SUNGLASSES_MODE_OFF,\n    SUNGLASSES_MODE_OPAQUE,\n    SUNGLASSES_MODE_TRANSPARENT,\n} SUNGLASSES_MODE;\n"
  },
  {
    "path": "src/trx/config/file.c",
    "content": "#include <trx/config/file.h>\n\n#include <trx/config/common.h>\n#include <trx/core/colors.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/console/history.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_EMPTY_ROOT \"{}\"\n#define M_ENFORCED_KEY \"enforced_config\"\n#define M_HIDDEN_KEY \"hidden_config\"\n\nstatic bool M_ReadFromJSON(\n    const char *const default_path, const char *const enforced_path,\n    void (*load)(JSON_OBJECT *root_obj), VECTOR *const enforced_targets,\n    VECTOR *const hidden_targets)\n{\n    bool result = false;\n\n    JSON_VALUE *cfg_root = JSONFile_Read(default_path);\n    if (cfg_root != nullptr) {\n        result = true;\n    } else {\n        JSON_OBJECT *const cfg_root_obj = JSON_ObjectNew();\n        JSON_ObjectAppendInt(cfg_root_obj, \"config_version\", -1);\n        cfg_root = JSON_ValueFromObject(cfg_root_obj);\n    }\n    JSON_VALUE *const enf_root =\n        enforced_path != nullptr ? JSONFile_Read(enforced_path) : nullptr;\n\n    JSON_OBJECT *cfg_root_obj = JSON_ValueAsObject(cfg_root);\n    JSON_OBJECT *enf_root_obj = JSON_ValueAsObject(enf_root);\n\n    // Merge settings from the game flow file.\n    JSON_OBJECT *const enforced_config =\n        JSON_ObjectGetObject(enf_root_obj, M_ENFORCED_KEY);\n    if (enforced_config != nullptr && enforced_targets != nullptr) {\n        Vector_ClearRealloc(enforced_targets);\n        const JSON_OBJECT_ELEMENT *elem = enforced_config->start;\n        while (elem != nullptr) {\n            const char *const name = elem->name->string;\n            const CONFIG_OPTION *const opt = Config_GetOptionByPath(name);\n            if (opt != nullptr) {\n                Vector_Add(enforced_targets, &opt->target);\n            }\n            elem = elem->next;\n        }\n    }\n    if (enforced_config != nullptr) {\n        JSON_ObjectMerge(cfg_root_obj, enforced_config);\n    }\n\n    // Record hidden settings from the game flow file.\n    JSON_ARRAY *const hidden_config_arr =\n        JSON_ObjectGetArray(enf_root_obj, M_HIDDEN_KEY);\n    if (hidden_config_arr != nullptr && hidden_targets != nullptr) {\n        Vector_ClearRealloc(hidden_targets);\n        for (size_t i = 0; i < hidden_config_arr->length; i++) {\n            const char *const name =\n                JSON_ArrayGetString(hidden_config_arr, i, nullptr);\n            if (name == nullptr) {\n                LOG_WARNING(\n                    \"Expected element %d in \\\"%s\\\" to be a string\", i,\n                    M_HIDDEN_KEY);\n                continue;\n            }\n            const CONFIG_OPTION *const opt = Config_GetOptionByPath(name);\n            if (opt != nullptr) {\n                Vector_Add(hidden_targets, &opt->target);\n            }\n        }\n    }\n\n    load(cfg_root_obj);\n\n    if (cfg_root) {\n        JSON_ValueFree(cfg_root);\n    }\n    if (enf_root) {\n        JSON_ValueFree(enf_root);\n    }\n\n    return result;\n}\n\nstatic void M_PreserveEnforcedState(\n    JSON_OBJECT *const root_obj, JSON_VALUE *const old_root,\n    JSON_VALUE *const enf_root)\n{\n    if (old_root == nullptr || enf_root == nullptr) {\n        return;\n    }\n\n    JSON_OBJECT *old_root_obj = JSON_ValueAsObject(old_root);\n    JSON_OBJECT *enf_root_obj = JSON_ValueAsObject(enf_root);\n    JSON_OBJECT *enforced_obj =\n        JSON_ObjectGetObject(enf_root_obj, M_ENFORCED_KEY);\n    if (enforced_obj == nullptr) {\n        return;\n    }\n\n    // Restore the original values for any enforced settings, provided they were\n    // defined.\n    JSON_OBJECT_ELEMENT *elem = enforced_obj->start;\n    while (elem != nullptr) {\n        const char *const name = elem->name->string;\n        elem = elem->next;\n\n        JSON_ObjectEvictKey(root_obj, name);\n        if (!JSON_ObjectContainsKey(old_root_obj, name)) {\n            continue;\n        }\n\n        JSON_VALUE *const old_value = JSON_ObjectGetValue(old_root_obj, name);\n        JSON_ObjectAppend(root_obj, name, old_value);\n    }\n}\n\nbool ConfigFile_Read(const CONFIG_IO_ARGS *const args)\n{\n    ASSERT(args->default_path != nullptr);\n    return M_ReadFromJSON(\n        args->default_path, args->enforced_path, args->action,\n        args->enforced_targets, args->hidden_targets);\n}\n\nbool ConfigFile_Write(const CONFIG_IO_ARGS *const args)\n{\n    ASSERT(args->default_path != nullptr);\n    JSON_VALUE *const old_root = JSONFile_Read(args->default_path);\n    JSON_VALUE *const enf_root = args->enforced_path != nullptr\n        ? JSONFile_Read(args->enforced_path)\n        : nullptr;\n\n    JSON_OBJECT *const root_obj = JSON_ObjectNew();\n    args->action(root_obj);\n    M_PreserveEnforcedState(root_obj, old_root, enf_root);\n\n    JSON_VALUE *const new_root = JSON_ValueFromObject(root_obj);\n    const bool updated = JSONFile_Write(args->default_path, new_root);\n\n    JSON_ValueFree(new_root);\n    JSON_ValueFree(old_root);\n    JSON_ValueFree(enf_root);\n    return updated;\n}\n\nvoid ConfigFile_LoadOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options)\n{\n    const CONFIG_OPTION *opt = options;\n    while (opt->target != nullptr) {\n        switch (opt->type) {\n        case COT_BOOL:\n            *(bool *)opt->target = JSON_ObjectGetBool(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(bool *)opt->default_value);\n            break;\n\n        case COT_INT32: {\n            JSON_VALUE *const value = JSON_ObjectGetValue(\n                root_obj, Config_ResolveOptionName(opt->name));\n            bool success = false;\n            if (value != nullptr && value->type == JSON_TYPE_NUMBER) {\n                *(int32_t *)opt->target =\n                    JSON_ValueGetInt(value, *(int32_t *)opt->default_value);\n                success = true;\n            } else if (value != nullptr && value->type == JSON_TYPE_STRING) {\n                success = String_ParseInteger(\n                    JSON_ValueGetString(value, \"\"), (int32_t *)opt->target);\n            }\n            if (!success) {\n                *(int32_t *)opt->target = *(int32_t *)opt->default_value;\n            }\n            break;\n        }\n\n        case COT_FLOAT:\n        case COT_FLOAT_PERCENT:\n            *(float *)opt->target = JSON_ObjectGetDouble(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(float *)opt->default_value);\n            break;\n\n        case COT_DOUBLE:\n            *(double *)opt->target = JSON_ObjectGetDouble(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(double *)opt->default_value);\n            break;\n\n        case COT_ENUM:\n            *(int *)opt->target = ConfigFile_ReadEnum(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(int *)opt->default_value, opt->param);\n            break;\n\n        case COT_STRING:\n        case COT_DYNAMIC_ENUM: {\n            const char *const val = JSON_ObjectGetString(\n                root_obj, Config_ResolveOptionName(opt->name),\n                (const char *)opt->default_value);\n            char **const p = (char **)opt->target;\n            Memory_FreePointer(p);\n            if (val != nullptr) {\n                *p = Memory_DupStr(val);\n            } else if (opt->default_value != nullptr) {\n                *p = Memory_DupStr(opt->default_value);\n            } else {\n                *p = nullptr;\n            }\n            break;\n        }\n\n        case COT_RGB888: {\n            RGB_888 *const target = (RGB_888 *)opt->target;\n            JSON_VALUE *const value = JSON_ObjectGetValue(\n                root_obj, Config_ResolveOptionName(opt->name));\n            bool success = false;\n            if (value != nullptr && value->type == JSON_TYPE_NUMBER) {\n                const uint32_t rgb_value =\n                    JSON_ValueGetInt(value, JSON_INVALID_NUMBER);\n                ASSERT(rgb_value != JSON_INVALID_NUMBER);\n                target->r = (rgb_value >> 0) & 0xFF;\n                target->g = (rgb_value >> 8) & 0xFF;\n                target->b = (rgb_value >> 16) & 0xFF;\n                success = true;\n            } else if (value != nullptr && value->type == JSON_TYPE_STRING) {\n                const char *str_value =\n                    JSON_ValueGetString(value, JSON_INVALID_STRING);\n                ASSERT(str_value != JSON_INVALID_STRING);\n                success = String_ParseRGB888(str_value, target);\n            }\n            if (!success) {\n                *(RGB_888 *)opt->target = *(RGB_888 *)opt->default_value;\n            }\n            break;\n        }\n        }\n        opt++;\n    }\n}\n\nvoid ConfigFile_DumpOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options)\n{\n    const CONFIG_OPTION *opt = options;\n    while (opt->target != nullptr) {\n        switch (opt->type) {\n        case COT_BOOL:\n            JSON_ObjectAppendBool(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(bool *)opt->target);\n            break;\n\n        case COT_INT32:\n            JSON_ObjectAppendInt(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(int32_t *)opt->target);\n            break;\n\n        case COT_FLOAT:\n        case COT_FLOAT_PERCENT:\n            JSON_ObjectAppendDouble(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(float *)opt->target);\n            break;\n\n        case COT_DOUBLE:\n            JSON_ObjectAppendDouble(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(double *)opt->target);\n            break;\n\n        case COT_ENUM:\n            ConfigFile_WriteEnum(\n                root_obj, Config_ResolveOptionName(opt->name),\n                *(int *)opt->target, (const char *)opt->param);\n            break;\n\n        case COT_STRING:\n        case COT_DYNAMIC_ENUM:\n            if (*(char **)opt->target != nullptr) {\n                JSON_ObjectAppendString(\n                    root_obj, Config_ResolveOptionName(opt->name),\n                    *(char **)opt->target);\n            }\n            break;\n\n        case COT_RGB888: {\n            const RGB_888 *const color = (RGB_888 *)opt->target;\n            char tmp[10];\n            sprintf(tmp, \"#%02X%02X%02X\", color->r, color->g, color->b);\n            JSON_ObjectAppendString(\n                root_obj, Config_ResolveOptionName(opt->name), tmp);\n            break;\n        }\n        }\n        opt++;\n    }\n}\n\nint ConfigFile_ReadEnum(\n    JSON_OBJECT *const obj, const char *const name, const int default_value,\n    const char *const enum_name)\n{\n    const char *value_str = JSON_ObjectGetString(obj, name, nullptr);\n    if (value_str != nullptr) {\n        return EnumMap_Get(enum_name, value_str, default_value);\n    }\n    return default_value;\n}\n\nvoid ConfigFile_WriteEnum(\n    JSON_OBJECT *obj, const char *name, int value, const char *enum_name)\n{\n    JSON_ObjectAppendString(obj, name, EnumMap_ToString(enum_name, value));\n}\n\nbool ConfigFile_LoadGymTrackStats(\n    JSON_OBJECT *const root_obj, const char *const key_name,\n    GYM_TRACK_STATS *const stats)\n{\n    JSON_OBJECT *const stats_obj = JSON_ObjectGetObject(root_obj, key_name);\n    if (stats_obj == nullptr) {\n        return false;\n    }\n    JSON_ARRAY *const entries_arr = JSON_ObjectGetArray(stats_obj, \"entries\");\n    if (entries_arr != nullptr) {\n        for (size_t i = 0; i < entries_arr->length && i < MAX_ASSAULT_TIMES;\n             i++) {\n            JSON_OBJECT *const entry_obj = JSON_ArrayGetObject(entries_arr, i);\n            if (entry_obj != nullptr) {\n                stats->entries[i].time = JSON_ObjectGetInt(\n                    entry_obj, \"time\", stats->entries[i].time);\n                stats->entries[i].attempt_num = JSON_ObjectGetInt(\n                    entry_obj, \"attempt_num\", stats->entries[i].attempt_num);\n            }\n        }\n    }\n    stats->total_attempts =\n        JSON_ObjectGetInt(stats_obj, \"total_attempts\", stats->total_attempts);\n    return true;\n}\n\nbool ConfigFile_DumpGymTrackStats(\n    JSON_OBJECT *const root_obj, const char *const key_name,\n    const GYM_TRACK_STATS *const stats)\n{\n    JSON_OBJECT *const stats_obj = JSON_ObjectNew();\n    JSON_ARRAY *const entries_arr = JSON_ArrayNew();\n    for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) {\n        if (stats->entries[i].time == 0) {\n            break;\n        }\n        JSON_OBJECT *const entry_obj = JSON_ObjectNew();\n        JSON_ObjectAppendInt(entry_obj, \"time\", stats->entries[i].time);\n        JSON_ObjectAppendInt(\n            entry_obj, \"attempt_num\", stats->entries[i].attempt_num);\n        JSON_ArrayAppendObject(entries_arr, entry_obj);\n    }\n    JSON_ObjectAppendArray(stats_obj, \"entries\", entries_arr);\n    JSON_ObjectAppendInt(stats_obj, \"total_attempts\", stats->total_attempts);\n    JSON_ObjectAppendObject(root_obj, key_name, stats_obj);\n    return true;\n}\n"
  },
  {
    "path": "src/trx/config/file.h",
    "content": "#pragma once\n\n#include <trx/config/option.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/json.h>\n#include <trx/core/vector.h>\n#include <trx/game/gym.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    const char *default_path;\n    const char *enforced_path;\n    void (*action)(JSON_OBJECT *root_obj);\n    VECTOR *enforced_targets;\n    VECTOR *hidden_targets;\n} CONFIG_IO_ARGS;\n\nbool ConfigFile_Read(const CONFIG_IO_ARGS *control);\nbool ConfigFile_Write(const CONFIG_IO_ARGS *control);\n\nvoid ConfigFile_LoadOptions(\n    JSON_OBJECT *root_obj, const CONFIG_OPTION *options);\nvoid ConfigFile_DumpOptions(\n    JSON_OBJECT *root_obj, const CONFIG_OPTION *options);\n\nint ConfigFile_ReadEnum(\n    JSON_OBJECT *obj, const char *name, int default_value,\n    const char *enum_name);\nvoid ConfigFile_WriteEnum(\n    JSON_OBJECT *obj, const char *name, int value, const char *enum_name);\n\nbool ConfigFile_LoadGymTrackStats(\n    JSON_OBJECT *root_obj, const char *key_name,\n    GYM_TRACK_STATS *assault_stats);\nbool ConfigFile_DumpGymTrackStats(\n    JSON_OBJECT *root_obj, const char *key_name,\n    const GYM_TRACK_STATS *assault_stats);\n"
  },
  {
    "path": "src/trx/config/map.c",
    "content": "#include <trx/config/option.h>\n#include <trx/config/types.h>\n#include <trx/config/vars.h>\n#include <trx/core/colors.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/lara/const.h>\n#include <trx/version.h>\n\n#include <string.h>\n\n#define X_CFG_BOOL(target_, default_value_)                                    \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_BOOL,                                                        \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(bool) { default_value_ },                             \\\n      .param = nullptr },\n\n#define X_CFG_INT32(target_, default_value_)                                   \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_INT32,                                                       \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(int32_t) { default_value_ },                          \\\n      .param = nullptr },\n\n#define X_CFG_FLOAT(target_, default_value_)                                   \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_FLOAT,                                                       \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(float) { default_value_ },                            \\\n      .param = nullptr },\n\n#define X_CFG_FLOAT_PERCENT(target_, default_value_)                           \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_FLOAT_PERCENT,                                               \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(float) { default_value_ },                            \\\n      .param = nullptr },\n\n#define X_CFG_DOUBLE(target_, default_value_)                                  \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_DOUBLE,                                                      \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(double) { default_value_ },                           \\\n      .param = nullptr },\n\n#define X_CFG_ENUM(target_, default_value_, enum_map)                          \\\n    X_CFG_ENUM_EX(QUOTE(target_), target_, default_value_, enum_map)\n\n#define X_CFG_ENUM_EX(name_, target_, default_value_, enum_map)                \\\n    { .name = name_,                                                           \\\n      .type = COT_ENUM,                                                        \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(int32_t) { default_value_ },                          \\\n      .param = ENUM_MAP_NAME(enum_map) },\n\n#define X_CFG_RGB888(target_, default_r, default_g, default_b)                 \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_RGB888,                                                      \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = &(RGB_888) { default_r, default_g, default_b },         \\\n      .param = nullptr },\n\n#define X_CFG_STRING(target_, default_value_)                                  \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_STRING,                                                      \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = default_value_,                                         \\\n      .param = nullptr },\n\n#define X_CFG_STRING_EX(name_, target_, default_value_)                        \\\n    { .name = name_,                                                           \\\n      .type = COT_STRING,                                                      \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = default_value_,                                         \\\n      .param = nullptr },\n\n#define X_CFG_DYNAMIC_ENUM(target_, default_value_)                            \\\n    { .name = QUOTE(target_),                                                  \\\n      .type = COT_DYNAMIC_ENUM,                                                \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = default_value_,                                         \\\n      .param = nullptr },\n\n#define X_CFG_DYNAMIC_ENUM_EX(name_, target_, default_value_)                  \\\n    { .name = name_,                                                           \\\n      .type = COT_DYNAMIC_ENUM,                                                \\\n      .target = &g_Config.target_,                                             \\\n      .default_value = default_value_,                                         \\\n      .param = nullptr },\n\nstatic const CONFIG_OPTION *m_ConfigOptionMap[TR_VERSION_COUNT] = {\n    [0] =\n        (CONFIG_OPTION[]) {\n#include <trx/config/map_tr1.def>\n            {}, // sentinel\n        },\n    [1] =\n        (CONFIG_OPTION[]) {\n#include <trx/config/map_tr2.def>\n            {}, // sentinel\n        },\n    [2] =\n        (CONFIG_OPTION[]) {\n#include <trx/config/map_tr3.def>\n            {}, // sentinel\n        },\n};\n\n#undef X_CFG_BOOL\n#undef X_CFG_INT32\n#undef X_CFG_FLOAT\n#undef X_CFG_FLOAT_PERCENT\n#undef X_CFG_DOUBLE\n#undef X_CFG_ENUM\n#undef X_CFG_ENUM_EX\n#undef X_CFG_RGB888\n#undef X_CFG_STRING\n#undef X_CFG_STRING_EX\n#undef X_CFG_DYNAMIC_ENUM\n#undef X_CFG_DYNAMIC_ENUM_EX\n\nconst CONFIG_OPTION *Config_GetOptionMap(void)\n{\n    if (g_TRVersion < 1 || g_TRVersion > 3) {\n        return nullptr;\n    }\n    return m_ConfigOptionMap[g_TRVersion - 1];\n}\n\nconst char *Config_ResolveOptionName(const char *option_name)\n{\n    const char *dot = strrchr(option_name, '.');\n    if (dot) {\n        return dot + 1;\n    }\n    return option_name;\n}\n\nconst CONFIG_OPTION *Config_GetOptionByPath(const char *const path)\n{\n    if (path == nullptr) {\n        return nullptr;\n    }\n    for (const CONFIG_OPTION *opt = Config_GetOptionMap(); opt->name != nullptr;\n         opt++) {\n        if (strcmp(opt->name, path) == 0\n            || strcmp(Config_ResolveOptionName(opt->name), path) == 0) {\n            return opt;\n        }\n    }\n    return nullptr;\n}\n"
  },
  {
    "path": "src/trx/config/map.def",
    "content": "X_CFG_BOOL(audio.enable_lara_mic,                      false)\nX_CFG_BOOL(audio.enable_music_in_inventory,            true)\nX_CFG_BOOL(audio.enable_music_in_menu,                 true)\nX_CFG_BOOL(audio.enable_pitched_sounds,                true)\nX_CFG_BOOL(audio.enable_ps1_sfx,                       true)\nX_CFG_BOOL(audio.enable_underwater_anim_sfx,           true)\nX_CFG_BOOL(audio.fix_chainblock_secret_sound,          true)\nX_CFG_BOOL(audio.fix_secrets_killing_music,            true)\nX_CFG_BOOL(audio.load_music_triggers,                  true)\nX_CFG_BOOL(audio.mute_out_of_focus,                    true)\nX_CFG_BOOL(debug.enable_debug_anim,                    false)\nX_CFG_BOOL(debug.enable_debug_bounding_boxes,          false)\nX_CFG_BOOL(debug.enable_debug_camera,                  false)\nX_CFG_BOOL(debug.enable_debug_portals,                 false)\nX_CFG_BOOL(debug.enable_debug_pos,                     false)\nX_CFG_BOOL(debug.enable_debug_room_clip,               false)\nX_CFG_BOOL(debug.enable_debug_spheres,                 false)\nX_CFG_BOOL(debug.enable_debug_status,                  false)\nX_CFG_BOOL(debug.enable_debug_triggers,                false)\nX_CFG_BOOL(debug.enable_endless_flare_time,            false)\nX_CFG_BOOL(debug.enable_endless_sprint,                false)\nX_CFG_BOOL(debug.enable_invulnerability,               false)\nX_CFG_BOOL(debug.enable_review_markers,                false)\nX_CFG_BOOL(flow.cheat_keys,                            true)\nX_CFG_BOOL(flow.load_save_disabled,                    false)\nX_CFG_BOOL(flow.lockout_option_ring,                   false)\nX_CFG_BOOL(flow.play_any_level,                        false)\nX_CFG_BOOL(gameplay.change_pierre_spawn,               true)\nX_CFG_BOOL(gameplay.disable_extra_guns,                false)\nX_CFG_BOOL(gameplay.disable_healing_between_levels,    false)\nX_CFG_BOOL(gameplay.disable_medpacks,                  false)\nX_CFG_BOOL(gameplay.disable_trex_collision,            false)\nX_CFG_BOOL(gameplay.enable_ally_targeting,             true)\nX_CFG_BOOL(gameplay.enable_auto_item_selection,        true)\nX_CFG_BOOL(gameplay.enable_body_bags,                  false)\nX_CFG_BOOL(gameplay.enable_cheats,                     false)\nX_CFG_BOOL(gameplay.enable_cinematics,                 true)\nX_CFG_BOOL(gameplay.enable_compass_stats,              true)\nX_CFG_BOOL(gameplay.enable_console,                    true)\nX_CFG_BOOL(gameplay.enable_controlled_drops,           true)\nX_CFG_BOOL(gameplay.enable_crawl_jump,                 true)\nX_CFG_BOOL(gameplay.enable_crawl_tilt,                 true)\nX_CFG_BOOL(gameplay.enable_crawling,                   true)\nX_CFG_BOOL(gameplay.enable_credits,                    true)\nX_CFG_BOOL(gameplay.enable_crouch_roll,                true)\nX_CFG_BOOL(gameplay.enable_cutscenes,                  true)\nX_CFG_BOOL(gameplay.enable_demo,                       true)\nX_CFG_BOOL(gameplay.enable_enemy_rotation,             true)\nX_CFG_BOOL(gameplay.enable_enhanced_saves,             true)\nX_CFG_BOOL(gameplay.enable_fmv,                        true)\nX_CFG_BOOL(gameplay.enable_game_modes,                 true)\nX_CFG_BOOL(gameplay.enable_idle_pose_camera,           false)\nX_CFG_BOOL(gameplay.enable_inverted_look,              false)\nX_CFG_BOOL(gameplay.enable_item_examining,             true)\nX_CFG_BOOL(gameplay.enable_jump_twists,                true)\nX_CFG_BOOL(gameplay.enable_killer_pushblocks,          true)\nX_CFG_BOOL(gameplay.enable_lean_jumping,               true)\nX_CFG_BOOL(gameplay.enable_ledge_jumps,                true)\nX_CFG_BOOL(gameplay.enable_legal,                      true)\nX_CFG_BOOL(gameplay.enable_manual_camera,              false)\nX_CFG_BOOL(gameplay.enable_neutral_twists,             true)\nX_CFG_BOOL(gameplay.enable_pickup_aids,                true)\nX_CFG_BOOL(gameplay.enable_play_previous_levels,       true)\nX_CFG_BOOL(gameplay.enable_responsive_crawl,           true)\nX_CFG_BOOL(gameplay.enable_responsive_sprint,          true)\nX_CFG_BOOL(gameplay.enable_save_crystals,              false)\nX_CFG_BOOL(gameplay.enable_slide_to_run,               true)\nX_CFG_BOOL(gameplay.enable_smooth_wall_deflect,        true)\nX_CFG_BOOL(gameplay.enable_soft_statics,               false)\nX_CFG_BOOL(gameplay.enable_sprint,                     true)\nX_CFG_BOOL(gameplay.enable_swing_cancel,               true)\nX_CFG_BOOL(gameplay.enable_target_change,              true)\nX_CFG_BOOL(gameplay.enable_toggle_crouch,              false)\nX_CFG_BOOL(gameplay.enable_toggle_sprint,              false)\nX_CFG_BOOL(gameplay.enable_total_stats,                true)\nX_CFG_BOOL(gameplay.enable_tr2_jumping,                true)\nX_CFG_BOOL(gameplay.enable_tr2_swim_cancel,            true)\nX_CFG_BOOL(gameplay.enable_tr2_swimming,               true)\nX_CFG_BOOL(gameplay.enable_uw_roll,                    true)\nX_CFG_BOOL(gameplay.enable_wading,                     true)\nX_CFG_BOOL(gameplay.enable_walk_to_items,              false)\nX_CFG_BOOL(gameplay.fix_alligator_ai,                  true)\nX_CFG_BOOL(gameplay.fix_bear_ai,                       true)\nX_CFG_BOOL(gameplay.fix_bridge_collision,              true)\nX_CFG_BOOL(gameplay.fix_descending_glitch,             true)\nX_CFG_BOOL(gameplay.fix_flare_throw_priority,          true)\nX_CFG_BOOL(gameplay.fix_floor_data_issues,             true)\nX_CFG_BOOL(gameplay.fix_free_flare_glitch,             true)\nX_CFG_BOOL(gameplay.fix_item_duplication_glitch,       true)\nX_CFG_BOOL(gameplay.fix_lara_pickup_embed,             true)\nX_CFG_BOOL(gameplay.fix_m16_accuracy,                  true)\nX_CFG_BOOL(gameplay.fix_monkey_pickup_priority,        true)\nX_CFG_BOOL(gameplay.fix_pipeman_aim,                   true)\nX_CFG_BOOL(gameplay.fix_qwop_glitch,                   true)\nX_CFG_BOOL(gameplay.fix_step_glitch,                   true)\nX_CFG_BOOL(gameplay.fix_wade_wall_hit,                 true)\nX_CFG_BOOL(gameplay.fix_walk_run_jump,                 true)\nX_CFG_BOOL(gameplay.fix_wall_geometry,                 true)\nX_CFG_BOOL(gameplay.pause_on_focus_lost,               false)\nX_CFG_BOOL(gameplay.remember_gun_status,               true)\nX_CFG_BOOL(gameplay.restore_ps1_enemies,               false)\nX_CFG_BOOL(input.enable_buffering_func_keys,           false)\nX_CFG_BOOL(input.enable_buffering_inventory,           true)\nX_CFG_BOOL(input.enable_responsive_passport,           true)\nX_CFG_BOOL(input.enable_tr3_sidesteps,                 true)\nX_CFG_BOOL(profile.new_game_plus_unlock,               false)\nX_CFG_BOOL(rendering.enable_lighting,                  true)\nX_CFG_BOOL(rendering.enable_textures,                  true)\nX_CFG_BOOL(rendering.enable_trapezoid_filter,          true)\nX_CFG_BOOL(rendering.enable_vsync,                     true)\nX_CFG_BOOL(rendering.enable_wireframe,                 false)\nX_CFG_BOOL(ui.enable_bar_flashing,                     true)\nX_CFG_BOOL(ui.enable_fps_counter,                      false)\nX_CFG_BOOL(ui.enable_game_ui,                          true)\nX_CFG_BOOL(ui.enable_photo_mode_ui,                    true)\nX_CFG_BOOL(ui.enable_wraparound,                       true)\nX_CFG_BOOL(ui.inventory_fade_effects,                  true)\nX_CFG_BOOL(ui.pause_fade_effects,                      true)\nX_CFG_BOOL(ui.show_bars,                               true)\nX_CFG_BOOL(ui.show_pickups_overlay,                    true)\nX_CFG_BOOL(ui.show_title_version,                      true)\nX_CFG_BOOL(ui.stats.show_ammo,                         true)\nX_CFG_BOOL(ui.stats.show_crystals,                     false)\nX_CFG_BOOL(ui.stats.show_deaths,                       true)\nX_CFG_BOOL(ui.stats.show_distance_travelled,           true)\nX_CFG_BOOL(ui.stats.show_kills,                        true)\nX_CFG_BOOL(ui.stats.show_level_header,                 false)\nX_CFG_BOOL(ui.stats.show_medipacks_used,               true)\nX_CFG_BOOL(ui.stats.show_pickups,                      true)\nX_CFG_BOOL(ui.stats.show_secrets,                      true)\nX_CFG_BOOL(ui.stats.show_time_taken,                   true)\nX_CFG_BOOL(ui.stats.show_totals,                       true)\nX_CFG_BOOL(ui.stats_fade_effects,                      true)\nX_CFG_BOOL(visuals.enable_3d_pickups,                  true)\nX_CFG_BOOL(visuals.enable_braid,                       true)\nX_CFG_BOOL(visuals.enable_breeze,                      true)\nX_CFG_BOOL(visuals.enable_exit_fade_effects,           true)\nX_CFG_BOOL(visuals.enable_fade_effects,                true)\nX_CFG_BOOL(visuals.enable_fire_lighting,               true)\nX_CFG_BOOL(visuals.enable_footprints,                  true)\nX_CFG_BOOL(visuals.enable_glide_cameras,               true)\nX_CFG_BOOL(visuals.enable_gun_lighting,                true)\nX_CFG_BOOL(visuals.enable_ps1_crystals,                true)\nX_CFG_BOOL(visuals.enable_reflections,                 true)\nX_CFG_BOOL(visuals.enable_responsive_mesh_tint,        true)\nX_CFG_BOOL(visuals.enable_shotgun_flash,               true)\nX_CFG_BOOL(visuals.enable_skybox,                      true)\nX_CFG_BOOL(visuals.enable_weather,                     true)\nX_CFG_BOOL(visuals.fix_animated_sprites,               true)\nX_CFG_BOOL(visuals.fix_item_rots,                      true)\nX_CFG_BOOL(visuals.fix_texture_issues,                 true)\nX_CFG_BOOL(visuals.fog_transparency,                   false)\nX_CFG_BOOL(window.is_fullscreen,                       true)\nX_CFG_BOOL(window.is_maximized,                        true)\nX_CFG_DYNAMIC_ENUM(visuals.lara_outfit,                nullptr)\nX_CFG_DYNAMIC_ENUM_EX(\"ui.airbar_color\",               ui.lara_air_bar.color, \"blue\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.airbar_color_ps1\",           ui.lara_air_bar.color_ps1, \"teal-green\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.enemy_healthbar_color\",      ui.enemy_health_bar.color, \"grey\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.enemy_healthbar_color_allies\", ui.enemy_health_bar.color_allies, \"teal\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.enemy_healthbar_color_allies_ps1\", ui.enemy_health_bar.color_allies_ps1, \"yellow-green\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.enemy_healthbar_color_ps1\",  ui.enemy_health_bar.color_ps1, \"orange-red\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.exposurebar_color\",          ui.lara_exposure_bar.color, \"cyan\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.exposurebar_color_ps1\",      ui.lara_exposure_bar.color_ps1, \"dark-blue-red\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.healthbar_color\",            ui.lara_health_bar.color, \"red\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.healthbar_color_ps1\",        ui.lara_health_bar.color_ps1, \"red-green\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.healthbar_poison_color\",     ui.lara_health_bar.poison_color, \"yellow\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.healthbar_poison_color_ps1\", ui.lara_health_bar.poison_color_ps1, \"dark-red-purple\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.sprintbar_color\",            ui.lara_sprint_bar.color, \"green\")\nX_CFG_DYNAMIC_ENUM_EX(\"ui.sprintbar_color_ps1\",        ui.lara_sprint_bar.color_ps1, \"red-yellow\")\nX_CFG_ENUM(audio.music_load_condition,                 MUSIC_LOAD_CONDITION_NON_AMBIENT, MUSIC_LOAD_CONDITION)\nX_CFG_ENUM(gameplay.ally_hostility_policy,             ALLY_HOSTILITY_POLICY_SHARED, ALLY_HOSTILITY_POLICY)\nX_CFG_ENUM(gameplay.creature_drown_policy,             CREATURE_DROWN_POLICY_SUBMERGED, CREATURE_DROWN_POLICY)\nX_CFG_ENUM(gameplay.loading_screens,                   LOADING_SCREENS_DISABLED, LOADING_SCREENS_MODE)\nX_CFG_ENUM(gameplay.look_mode,                         LOOK_MODE_ENHANCED, LOOK_MODE)\nX_CFG_ENUM(gameplay.target_mode,                       TARGET_LOCK_MODE_FULL, TARGET_LOCK_MODE)\nX_CFG_ENUM(gameplay.wall_glitch_mode,                  WALL_GLITCH_FIXED, WALL_GLITCH_MODE)\nX_CFG_ENUM(input.backend,                              INPUT_BACKEND_KEYBOARD, INPUT_BACKEND)\nX_CFG_ENUM(input.quick_guns_mode,                      QUICK_GUNS_MODE_DRAW_AND_HOLSTER, QUICK_GUNS_MODE)\nX_CFG_ENUM(rendering.aspect_mode,                      ASPECT_MODE_ANY, ASPECT_MODE)\nX_CFG_ENUM(rendering.lighting_contrast,                LIGHTING_CONTRAST_MEDIUM, LIGHTING_CONTRAST)\nX_CFG_ENUM(rendering.screenshot_format,                SCREENSHOT_FORMAT_JPEG, SCREENSHOT_FORMAT)\nX_CFG_ENUM(rendering.sprite_lock_mode,                 BILLBOARD_LOCK_PERSPECTIVE, BILLBOARD_LOCK_MODE)\nX_CFG_ENUM(rendering.texture_filter,                   TEXTURE_FILTER_POINT, TEXTURE_FILTER)\nX_CFG_ENUM(rendering.ui_filter,                        TEXTURE_FILTER_POINT, TEXTURE_FILTER)\nX_CFG_ENUM(rendering.upscaling_filter,                 TEXTURE_FILTER_POINT, TEXTURE_FILTER)\nX_CFG_ENUM(ui.menu_style,                              UI_STYLE_PS1, UI_STYLE)\nX_CFG_ENUM(ui.stats.style,                             STATS_STYLE_BORDERED, STATS_STYLE)\nX_CFG_ENUM(visuals.blood_effects,                      BLOOD_EFFECTS_RED, BLOOD_EFFECTS)\nX_CFG_ENUM(visuals.sunglasses_mode,                    SUNGLASSES_MODE_OFF, SUNGLASSES_MODE)\nX_CFG_ENUM_EX(\"ui.airbar_location\",                    ui.lara_air_bar.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION)\nX_CFG_ENUM_EX(\"ui.ammo_counter_location\",              ui.ammo_counter.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION)\nX_CFG_ENUM_EX(\"ui.enemy_healthbar_location\",           ui.enemy_health_bar.location, UI_ELEMENT_LOCATION_BOTTOM_LEFT, UI_ELEMENT_LOCATION)\nX_CFG_ENUM_EX(\"ui.enemy_healthbar_show_mode\",          ui.enemy_health_bar.show_mode, BAR_SHOW_MODE_ALWAYS, BAR_SHOW_MODE)\nX_CFG_ENUM_EX(\"ui.exposurebar_location\",               ui.lara_exposure_bar.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION)\nX_CFG_ENUM_EX(\"ui.healthbar_location\",                 ui.lara_health_bar.location, UI_ELEMENT_LOCATION_TOP_LEFT, UI_ELEMENT_LOCATION)\nX_CFG_ENUM_EX(\"ui.sprintbar_location\",                 ui.lara_sprint_bar.location, UI_ELEMENT_LOCATION_TOP_RIGHT, UI_ELEMENT_LOCATION)\nX_CFG_FLOAT(rendering.anisotropy_filter,               16.0f)\nX_CFG_FLOAT(rendering.wireframe_width,                 2.5)\nX_CFG_FLOAT(visuals.game_brightness,                   1.0f)\nX_CFG_FLOAT(visuals.gamma,                             2.5f)\nX_CFG_FLOAT(visuals.ui_brightness,                     1.0f)\nX_CFG_FLOAT_PERCENT(audio.ambient_volume,              1.0f)\nX_CFG_FLOAT_PERCENT(audio.cutscene_volume,             1.0f)\nX_CFG_FLOAT_PERCENT(audio.fmv_volume,                  1.0f)\nX_CFG_FLOAT_PERCENT(audio.inventory_ambient_volume,    1.0f)\nX_CFG_FLOAT_PERCENT(audio.inventory_music_volume,      1.0f)\nX_CFG_FLOAT_PERCENT(audio.master_volume,               0.8f)\nX_CFG_FLOAT_PERCENT(audio.music_volume,                1.0f)\nX_CFG_FLOAT_PERCENT(audio.sound_volume,                1.0f)\nX_CFG_FLOAT_PERCENT(audio.underwater_ambient_volume,   1.0f)\nX_CFG_FLOAT_PERCENT(audio.underwater_music_volume,     1.0f)\nX_CFG_FLOAT_PERCENT(rendering.borders,                 0.0f)\nX_CFG_FLOAT_PERCENT(ui.bar_scale,                      1.0f)\nX_CFG_FLOAT_PERCENT(ui.pickup_scale,                   1.0f)\nX_CFG_FLOAT_PERCENT(ui.text_scale,                     1.0f)\nX_CFG_INT32(config_version,                            0)\nX_CFG_INT32(gameplay.camera_speed,                     5)\nX_CFG_INT32(gameplay.harpoon_recoil,                   4)\nX_CFG_INT32(gameplay.idle_pose_timeout,                60)\nX_CFG_INT32(gameplay.start_lara_hitpoints,             LARA_MAX_HITPOINTS)\nX_CFG_INT32(gameplay.turbo_speed,                      0)\nX_CFG_INT32(input.controller_layout,                   INPUT_LAYOUT_DEFAULT)\nX_CFG_INT32(input.keyboard_layout,                     INPUT_LAYOUT_DEFAULT)\nX_CFG_INT32(rendering.fps,                             60)\nX_CFG_INT32(rendering.upscaling_factor,                1)\nX_CFG_INT32(visuals.fog_end,                           30)\nX_CFG_INT32(visuals.fog_start,                         22)\nX_CFG_INT32(visuals.fov,                               80)\nX_CFG_INT32(window.fs_height,                          -1)\nX_CFG_INT32(window.fs_width,                           -1)\nX_CFG_INT32(window.height,                             720)\nX_CFG_INT32(window.width,                              1280)\nX_CFG_INT32(window.x,                                  -1)\nX_CFG_INT32(window.y,                                  -1)\nX_CFG_RGB888(visuals.fog_color,                        0, 0, 0)\nX_CFG_STRING(language,                                 \"en\")\n"
  },
  {
    "path": "src/trx/config/map_tr1.def",
    "content": "#include <trx/config/map.def>\n\nX_CFG_BOOL(audio.fix_speeches_killing_music,   true)\nX_CFG_BOOL(gameplay.enable_slow_ledge_swing,   false)\nX_CFG_ENUM(gameplay.jump_lock_mode,            JUMP_LOCK_TUNED, JUMP_LOCK_MODE)\nX_CFG_ENUM(visuals.camera_mode,                CAMERA_MODE_TR1, CAMERA_MODE)\nX_CFG_RGB888(visuals.water_color,              0x72, 0xFF, 0xFF)\nX_CFG_ENUM(ui.inventory_background_style,      BK_TRANSPARENT_MEDIUM, BACKGROUND_TYPE)\nX_CFG_ENUM(ui.stats_background_style,          BK_TRANSPARENT_MEDIUM, BACKGROUND_TYPE)\nX_CFG_ENUM(ui.pause_background_style,          BK_TRANSPARENT_DARK, BACKGROUND_TYPE)\nX_CFG_BOOL(gameplay.enable_boulder_shake,      false)\nX_CFG_BOOL(gameplay.enable_back_slope_stumble, false)\nX_CFG_BOOL(gameplay.enable_step_roll_boost,    true)\nX_CFG_BOOL(gameplay.enable_timer_in_inventory, false)\nX_CFG_BOOL(gameplay.fix_water_exit,            false)\nX_CFG_BOOL(ui.enable_smooth_bars,              true)\nX_CFG_DYNAMIC_ENUM(ui.bar_look,                \"tr1_pc\")\nX_CFG_FLOAT(flow.demo_delay,                   16.0f)\nX_CFG_INT32(gameplay.maximum_save_slots,       25)\nX_CFG_INT32(gameplay.maximum_quick_save_slots, 1)\nX_CFG_ENUM(visuals.shadow_type,                SHADOW_TYPE_CIRCLE, SHADOW_TYPE)\nX_CFG_BOOL(gameplay.enable_bouncy_grenades,    false)\nX_CFG_ENUM(gameplay.projectile_area_damage,    PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, PROJECTILE_AREA_DAMAGE)\n"
  },
  {
    "path": "src/trx/config/map_tr2.def",
    "content": "#include <trx/config/map.def>\n\nX_CFG_BOOL(audio.fix_speeches_killing_music,   false)\nX_CFG_BOOL(gameplay.enable_slow_ledge_swing,   false)\nX_CFG_ENUM(gameplay.jump_lock_mode,            JUMP_LOCK_LEGACY, JUMP_LOCK_MODE)\nX_CFG_ENUM(visuals.camera_mode,                CAMERA_MODE_TR2, CAMERA_MODE)\nX_CFG_RGB888(visuals.water_color,              0x80, 0xE0, 0xFF)\nX_CFG_ENUM(ui.inventory_background_style,      BK_PATTERN_WAVE, BACKGROUND_TYPE)\nX_CFG_ENUM(ui.stats_background_style,          BK_PATTERN_WAVE, BACKGROUND_TYPE)\nX_CFG_ENUM(ui.pause_background_style,          BK_TRANSPARENT_DARK, BACKGROUND_TYPE)\nX_CFG_BOOL(gameplay.enable_boulder_shake,      true)\nX_CFG_BOOL(gameplay.enable_back_slope_stumble, false)\nX_CFG_BOOL(gameplay.enable_step_roll_boost,    false)\nX_CFG_BOOL(gameplay.enable_timer_in_inventory, true)\nX_CFG_BOOL(gameplay.fix_water_exit,            true)\nX_CFG_BOOL(ui.enable_smooth_bars,              false)\nX_CFG_DYNAMIC_ENUM(ui.bar_look,                \"tr2_ps1\")\nX_CFG_FLOAT(flow.demo_delay,                   30.0f)\nX_CFG_INT32(gameplay.maximum_save_slots,       24)\nX_CFG_INT32(gameplay.maximum_quick_save_slots, 1)\nX_CFG_ENUM(visuals.shadow_type,                SHADOW_TYPE_CIRCLE, SHADOW_TYPE)\nX_CFG_BOOL(gameplay.enable_bouncy_grenades,    false)\nX_CFG_ENUM(gameplay.projectile_area_damage,    PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, PROJECTILE_AREA_DAMAGE)\n"
  },
  {
    "path": "src/trx/config/map_tr3.def",
    "content": "#include <trx/config/map.def>\n\nX_CFG_BOOL(audio.fix_speeches_killing_music,   false)\nX_CFG_BOOL(gameplay.enable_slow_ledge_swing,   true)\nX_CFG_ENUM(gameplay.jump_lock_mode,            JUMP_LOCK_LEGACY, JUMP_LOCK_MODE)\nX_CFG_ENUM(visuals.camera_mode,                CAMERA_MODE_TR3, CAMERA_MODE)\nX_CFG_RGB888(visuals.water_color,              0x80, 0xE0, 0xFF)\nX_CFG_ENUM(ui.inventory_background_style,      BK_MONOCHROME, BACKGROUND_TYPE)\nX_CFG_ENUM(ui.stats_background_style,          BK_MONOCHROME, BACKGROUND_TYPE)\nX_CFG_ENUM(ui.pause_background_style,          BK_TRANSPARENT_DARK, BACKGROUND_TYPE)\nX_CFG_BOOL(gameplay.enable_boulder_shake,      true)\nX_CFG_BOOL(gameplay.enable_back_slope_stumble, true)\nX_CFG_BOOL(gameplay.enable_step_roll_boost,    false)\nX_CFG_BOOL(gameplay.enable_timer_in_inventory, true)\nX_CFG_BOOL(gameplay.fix_water_exit,            false)\nX_CFG_BOOL(ui.enable_smooth_bars,              false)\nX_CFG_DYNAMIC_ENUM(ui.bar_look,                \"tr3_ps1\")\nX_CFG_FLOAT(flow.demo_delay,                   30.0f)\nX_CFG_INT32(gameplay.maximum_save_slots,       24)\nX_CFG_INT32(gameplay.maximum_quick_save_slots, 1)\nX_CFG_ENUM(visuals.shadow_type,                SHADOW_TYPE_SPRITE, SHADOW_TYPE)\nX_CFG_BOOL(gameplay.enable_bouncy_grenades,    true)\nX_CFG_ENUM(gameplay.projectile_area_damage,    PROJECTILE_AREA_DAMAGE_MULTI_SWEEP, PROJECTILE_AREA_DAMAGE)\n"
  },
  {
    "path": "src/trx/config/option.h",
    "content": "#pragma once\n\ntypedef enum {\n    COT_BOOL,\n    COT_INT32,\n    COT_FLOAT,\n    COT_FLOAT_PERCENT,\n    COT_DOUBLE,\n    COT_ENUM,\n    COT_RGB888,\n    COT_STRING,\n    COT_DYNAMIC_ENUM,\n} CONFIG_OPTION_TYPE;\n\ntypedef struct {\n    const char *name;\n    CONFIG_OPTION_TYPE type;\n    const void *target;\n    const void *default_value;\n    const void *param;\n} CONFIG_OPTION;\n"
  },
  {
    "path": "src/trx/config/presets.c",
    "content": "#include <trx/config/presets.h>\n\n#include <trx/config/common.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/json.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/game/shell/paths.h>\n\nstatic VECTOR *m_Presets = nullptr; // CONFIG_PRESET\n\nstatic void M_FreePreset(CONFIG_PRESET *const preset)\n{\n    Memory_FreePointer(&preset->name_gs);\n    for (int32_t i = 0; i < preset->setting_count; i++) {\n        Memory_FreePointer(&preset->keys[i]);\n        Memory_FreePointer(&preset->values[i]);\n    }\n    Memory_FreePointer(&preset->keys);\n    Memory_FreePointer(&preset->values);\n}\n\nstatic void M_FreeAllPresets(void)\n{\n    if (m_Presets != nullptr) {\n        for (int32_t i = 0; i < m_Presets->count; i++) {\n            M_FreePreset(Vector_Get(m_Presets, i));\n        }\n        Vector_Free(m_Presets);\n        m_Presets = nullptr;\n    }\n}\n\nstatic char *M_SerializeJSONValue(const JSON_VALUE *const val)\n{\n    if (val == nullptr) {\n        return Memory_DupStr(\"\");\n    }\n    const char *const str = JSON_ValueGetString(val, nullptr);\n    if (str != nullptr) {\n        return Memory_DupStr(str);\n    }\n    const int bool_val = JSON_ValueGetBool(val, JSON_INVALID_BOOL);\n    if (bool_val != JSON_INVALID_BOOL) {\n        return Memory_DupStr(bool_val ? \"true\" : \"false\");\n    }\n    const JSON_NUMBER *const num = JSON_ValueGetNumber(val);\n    if (num != nullptr && num->number != nullptr) {\n        return Memory_DupStr(num->number);\n    }\n    return Memory_DupStr(\"\");\n}\n\nstatic bool M_LoadPreset(const char *const path)\n{\n    JSON_VALUE *const root = JSONFile_ReadEx(path, false);\n    if (root == nullptr) {\n        LOG_WARNING(\"Failed to parse preset: %s\", path);\n        return false;\n    }\n\n    const JSON_OBJECT *const root_obj = JSON_ValueAsObject(root);\n    if (root_obj == nullptr) {\n        LOG_WARNING(\"Preset root is not an object: %s\", path);\n        JSON_ValueFree(root);\n        return false;\n    }\n\n    const char *const name_gs =\n        JSON_ObjectGetString(root_obj, \"name_gs\", nullptr);\n    if (name_gs == nullptr) {\n        LOG_WARNING(\"Preset missing name_gs: %s\", path);\n        JSON_ValueFree(root);\n        return false;\n    }\n\n    const JSON_OBJECT *const config_obj =\n        JSON_ObjectGetObject(root_obj, \"config\");\n    if (config_obj == nullptr) {\n        LOG_WARNING(\"Preset missing config object: %s\", path);\n        JSON_ValueFree(root);\n        return false;\n    }\n\n    // Count entries first\n    int32_t count = 0;\n    for (const JSON_OBJECT_ELEMENT *elem = config_obj->start; elem != nullptr;\n         elem = elem->next) {\n        count++;\n    }\n\n    CONFIG_PRESET preset = {\n        .name_gs = Memory_DupStr(name_gs),\n        .setting_count = count,\n        .keys = count > 0 ? Memory_Alloc(sizeof(char *) * count) : nullptr,\n        .values = count > 0 ? Memory_Alloc(sizeof(char *) * count) : nullptr,\n    };\n\n    int32_t i = 0;\n    for (const JSON_OBJECT_ELEMENT *elem = config_obj->start; elem != nullptr;\n         elem = elem->next, i++) {\n        preset.keys[i] = Memory_DupStr(elem->name->string);\n        char *const serialized = M_SerializeJSONValue(elem->value);\n        const CONFIG_OPTION *const opt = Config_GetOptionByPath(preset.keys[i]);\n        preset.values[i] =\n            Config_NormalizeOptionValueString(opt, serialized, false);\n        Memory_Free(serialized);\n    }\n\n    Vector_Add(m_Presets, &preset);\n    JSON_ValueFree(root);\n    return true;\n}\n\nstatic void __attribute__((destructor)) M_Shutdown(void)\n{\n    M_FreeAllPresets();\n}\n\nvoid Config_Presets_ScanFiles(void)\n{\n    M_FreeAllPresets();\n\n    m_Presets = Vector_Create(sizeof(CONFIG_PRESET));\n\n    char *presets_dir = TRXPath_ExpandVars(\"%mod_dir%/presets\");\n    if (presets_dir == nullptr || !File_DirExists(presets_dir)) {\n        Memory_FreePointer(&presets_dir);\n        presets_dir = TRXPath_ExpandVars(\"%base_mod_dir%/presets\");\n    }\n    if (presets_dir == nullptr || !File_DirExists(presets_dir)) {\n        Memory_FreePointer(&presets_dir);\n        presets_dir = TRXPath_ExpandVars(\"%config_dir%/presets\");\n    }\n    if (presets_dir == nullptr || !File_DirExists(presets_dir)) {\n        Memory_FreePointer(&presets_dir);\n        return;\n    }\n\n    void *const dir = File_OpenDirectory(presets_dir);\n    if (dir == nullptr) {\n        Memory_FreePointer(&presets_dir);\n        return;\n    }\n\n    const char *entry_name;\n    while ((entry_name = File_ReadDirectory(dir)) != nullptr) {\n        if (!String_EndsWith(entry_name, \".json5\")) {\n            continue;\n        }\n        const char *const full_path =\n            String_FormatStatic(\"%s/%s\", presets_dir, entry_name);\n        M_LoadPreset(full_path);\n    }\n    File_CloseDirectory(dir);\n    Memory_FreePointer(&presets_dir);\n\n    LOG_INFO(\"Loaded %d config preset(s)\", m_Presets->count);\n}\n\nint32_t Config_Presets_GetCount(void)\n{\n    return m_Presets != nullptr ? m_Presets->count : 0;\n}\n\nconst CONFIG_PRESET *Config_Presets_Get(const int32_t idx)\n{\n    if (m_Presets == nullptr || idx < 0 || idx >= m_Presets->count) {\n        return nullptr;\n    }\n    return Vector_Get(m_Presets, idx);\n}\n\nvoid Config_Presets_Apply(const int32_t idx)\n{\n    const CONFIG_PRESET *const preset = Config_Presets_Get(idx);\n    if (preset == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < preset->setting_count; i++) {\n        const CONFIG_OPTION *const opt =\n            Config_GetOptionByPath(preset->keys[i]);\n        if (opt == nullptr) {\n            LOG_WARNING(\"Preset: unknown config key '%s'\", preset->keys[i]);\n            continue;\n        }\n        Config_SetOptionValueFromString(opt, preset->values[i]);\n    }\n    Config_Update();\n    Config_Write();\n}\n"
  },
  {
    "path": "src/trx/config/presets.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\n// A config preset represents a named set of settings to apply all at once.\n// Presets are loaded from cfg/presets/*.json5 at startup.\n\ntypedef struct {\n    char *name_gs;\n    // Flat arrays of config key paths and their string values.\n    char **keys;\n    char **values;\n    int32_t setting_count;\n} CONFIG_PRESET;\n\n// Load all presets from the game's cfg/presets/ directory.\nvoid Config_Presets_ScanFiles(void);\n\n// Number of loaded presets.\nint32_t Config_Presets_GetCount(void);\n\n// Returns the preset at the given index, or nullptr if out of range.\nconst CONFIG_PRESET *Config_Presets_Get(int32_t idx);\n\n// Apply all settings from preset[idx] and write config to disk.\nvoid Config_Presets_Apply(int32_t idx);\n"
  },
  {
    "path": "src/trx/config/priv.c",
    "content": "#include <trx/config/priv.h>\n\n#include <trx/config/common.h>\n#include <trx/config/file.h>\n#include <trx/config/vars.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/clock.h>\n#include <trx/game/input.h>\n#include <trx/game/lara/const.h>\n#include <trx/version.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_CONFIG_VERSION_CURRENT 1\n\nstatic void M_LoadKeyboardLayout(\n    JSON_OBJECT *const parent_obj, const INPUT_LAYOUT layout)\n{\n    char layout_name[20];\n    sprintf(layout_name, \"layout_%d\", layout);\n    JSON_ARRAY *const arr = JSON_ObjectGetArray(parent_obj, layout_name);\n    if (arr == nullptr) {\n        return;\n    }\n\n    for (size_t i = 0; i < arr->length; i++) {\n        JSON_OBJECT *const bind_obj = JSON_ArrayGetObject(arr, i);\n        if (bind_obj == nullptr) {\n            // this can happen on TR1X <= 3.1.1, which is no longer supported\n            LOG_WARNING(\"unsupported keyboard layout config\");\n            continue;\n        }\n\n        Input_AssignFromJSONObject(INPUT_BACKEND_KEYBOARD, layout, bind_obj);\n    }\n}\n\nstatic void M_LoadControllerLayout(\n    JSON_OBJECT *const parent_obj, const INPUT_LAYOUT layout)\n{\n    char layout_name[20];\n    sprintf(layout_name, \"cntlr_layout_%d\", layout);\n    JSON_ARRAY *const arr = JSON_ObjectGetArray(parent_obj, layout_name);\n    if (arr == nullptr) {\n        return;\n    }\n\n    for (size_t i = 0; i < arr->length; i++) {\n        JSON_OBJECT *const bind_obj = JSON_ArrayGetObject(arr, i);\n        if (bind_obj == nullptr) {\n            // this can happen on TR1X <= 3.1.1, which is no longer supported\n            LOG_WARNING(\"unsupported controller layout config\");\n            continue;\n        }\n\n        Input_AssignFromJSONObject(INPUT_BACKEND_CONTROLLER, layout, bind_obj);\n    }\n}\n\nstatic void M_LoadLegacyInputConfig(JSON_OBJECT *const root_obj)\n{\n    for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1;\n         layout < INPUT_LAYOUT_NUMBER_OF; layout++) {\n        M_LoadKeyboardLayout(root_obj, layout);\n        M_LoadControllerLayout(root_obj, layout);\n    }\n}\n\nstatic void M_LoadInputLayout(\n    JSON_OBJECT *const parent_obj, const INPUT_BACKEND backend,\n    const INPUT_LAYOUT layout)\n{\n    char layout_name[20];\n    sprintf(layout_name, \"layout_%d\", layout);\n    JSON_ARRAY *const arr = JSON_ObjectGetArray(parent_obj, layout_name);\n    if (arr == nullptr) {\n        return;\n    }\n\n    for (size_t i = 0; i < arr->length; i++) {\n        JSON_OBJECT *const bind_obj = JSON_ArrayGetObject(arr, i);\n        ASSERT(bind_obj != nullptr);\n        Input_AssignFromJSONObject(backend, layout, bind_obj);\n    }\n}\n\nstatic void M_LoadInputConfig(JSON_OBJECT *const root_obj)\n{\n    JSON_OBJECT *const input_obj = JSON_ObjectGetObject(root_obj, \"input\");\n    if (input_obj == nullptr) {\n        return;\n    }\n\n    JSON_OBJECT *const keyboard_obj =\n        JSON_ObjectGetObject(input_obj, \"keyboard\");\n    JSON_OBJECT *const controller_obj =\n        JSON_ObjectGetObject(input_obj, \"controller\");\n    for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1;\n         layout < INPUT_LAYOUT_NUMBER_OF; layout++) {\n        if (keyboard_obj != nullptr) {\n            M_LoadInputLayout(keyboard_obj, INPUT_BACKEND_KEYBOARD, layout);\n        }\n        if (controller_obj != nullptr) {\n            M_LoadInputLayout(controller_obj, INPUT_BACKEND_CONTROLLER, layout);\n        }\n    }\n}\n\nstatic void M_DumpInputLayout(\n    JSON_OBJECT *const parent_obj, const INPUT_BACKEND backend,\n    const INPUT_LAYOUT layout)\n{\n    JSON_ARRAY *const arr = JSON_ArrayNew();\n\n    bool has_elements = false;\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n            JSON_OBJECT *const bind_obj = JSON_ObjectNew();\n            if (Input_AssignToJSONObject(\n                    backend, layout, bind_obj, role, slot)) {\n                has_elements = true;\n                JSON_ArrayAppendObject(arr, bind_obj);\n            } else {\n                JSON_ObjectFree(bind_obj);\n            }\n        }\n    }\n\n    if (has_elements) {\n        char layout_name[20];\n        sprintf(layout_name, \"layout_%d\", layout);\n        JSON_ObjectAppendArray(parent_obj, layout_name, arr);\n    } else {\n        JSON_ArrayFree(arr);\n    }\n}\n\nstatic void M_DumpInputConfig(JSON_OBJECT *const root_obj)\n{\n    JSON_OBJECT *const input_obj = JSON_ObjectNew();\n    JSON_OBJECT *const keyboard_obj = JSON_ObjectNew();\n    JSON_OBJECT *const controller_obj = JSON_ObjectNew();\n    JSON_ObjectAppendObject(root_obj, \"input\", input_obj);\n    JSON_ObjectAppendObject(input_obj, \"keyboard\", keyboard_obj);\n    JSON_ObjectAppendObject(input_obj, \"controller\", controller_obj);\n    for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1;\n         layout < INPUT_LAYOUT_NUMBER_OF; layout++) {\n        M_DumpInputLayout(keyboard_obj, INPUT_BACKEND_KEYBOARD, layout);\n        M_DumpInputLayout(controller_obj, INPUT_BACKEND_CONTROLLER, layout);\n    }\n}\n\nstatic void M_MigrateBarColorName(char **const value_ptr)\n{\n    const char *value = *value_ptr;\n    const char *new_value = nullptr;\n    if (value == nullptr) {\n        return;\n    }\n    if (String_Equivalent(value, \"gold\")) {\n        new_value = \"brown\";\n    } else if (String_Equivalent(value, \"green\")) {\n        new_value = \"teal\";\n    } else if (String_Equivalent(value, \"gold2\")) {\n        new_value = \"yellow\";\n    } else if (String_Equivalent(value, \"blue2\")) {\n        new_value = \"cyan\";\n    } else if (String_Equivalent(value, \"green2\")) {\n        new_value = \"green\";\n    } else if (String_Equivalent(value, \"gold-green\")) {\n        new_value = \"yellow-green\";\n    }\n    if (new_value != nullptr) {\n        Memory_FreePointer(value_ptr);\n        *value_ptr = Memory_DupStr(new_value);\n    }\n}\n\ntypedef enum {\n    M_LEGACY_STATS_DETAILS_MINIMAL,\n    M_LEGACY_STATS_DETAILS_DETAILED,\n    M_LEGACY_STATS_DETAILS_FULL,\n} M_LEGACY_STATS_DETAILS;\n\nstatic bool M_TryGetLegacyStatsDetails(\n    JSON_OBJECT *const parent_obj, M_LEGACY_STATS_DETAILS *const out_value)\n{\n    const JSON_VALUE *const value =\n        JSON_ObjectGetValue(parent_obj, \"stat_detail_mode\");\n    if (value == nullptr) {\n        return false;\n    }\n\n    if (value->type == JSON_TYPE_STRING) {\n        const char *const value_str = JSON_ValueGetString(value, nullptr);\n        if (String_Equivalent(value_str, \"minimal\")) {\n            *out_value = M_LEGACY_STATS_DETAILS_MINIMAL;\n            return true;\n        }\n        if (String_Equivalent(value_str, \"detailed\")) {\n            *out_value = M_LEGACY_STATS_DETAILS_DETAILED;\n            return true;\n        }\n        if (String_Equivalent(value_str, \"full\")) {\n            *out_value = M_LEGACY_STATS_DETAILS_FULL;\n            return true;\n        }\n        return false;\n    }\n\n    if (value->type == JSON_TYPE_NUMBER) {\n        switch (JSON_ValueGetInt(value, M_LEGACY_STATS_DETAILS_FULL)) {\n        case M_LEGACY_STATS_DETAILS_MINIMAL:\n            *out_value = M_LEGACY_STATS_DETAILS_MINIMAL;\n            return true;\n        case M_LEGACY_STATS_DETAILS_DETAILED:\n            *out_value = M_LEGACY_STATS_DETAILS_DETAILED;\n            return true;\n        case M_LEGACY_STATS_DETAILS_FULL:\n            *out_value = M_LEGACY_STATS_DETAILS_FULL;\n            return true;\n        default:\n            break;\n        }\n    }\n\n    return false;\n}\n\nstatic void M_MigrateLegacyStatsOptions(JSON_OBJECT *const parent_obj)\n{\n    M_LEGACY_STATS_DETAILS legacy_details = M_LEGACY_STATS_DETAILS_FULL;\n    const bool has_legacy_details =\n        M_TryGetLegacyStatsDetails(parent_obj, &legacy_details);\n    if (!has_legacy_details) {\n        return;\n    }\n\n    if (JSON_ObjectGetValue(parent_obj, \"style\") == nullptr) {\n        g_Config.ui.stats.style =\n            g_TRVersion == 1 && legacy_details != M_LEGACY_STATS_DETAILS_FULL\n            ? STATS_STYLE_BARE\n            : STATS_STYLE_BORDERED;\n    }\n\n    if (JSON_ObjectGetValue(parent_obj, \"show_totals\") == nullptr) {\n        g_Config.ui.stats.show_totals =\n            legacy_details != M_LEGACY_STATS_DETAILS_MINIMAL;\n    }\n\n    if (legacy_details == M_LEGACY_STATS_DETAILS_FULL) {\n        return;\n    }\n\n    if (JSON_ObjectGetValue(parent_obj, \"show_time_taken\") == nullptr) {\n        g_Config.ui.stats.show_time_taken = true;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_secrets\") == nullptr) {\n        g_Config.ui.stats.show_secrets = true;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_crystals\") == nullptr) {\n        g_Config.ui.stats.show_crystals = false;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_pickups\") == nullptr) {\n        g_Config.ui.stats.show_pickups = g_TRVersion == 1;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_kills\") == nullptr) {\n        g_Config.ui.stats.show_kills = true;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_ammo\") == nullptr) {\n        g_Config.ui.stats.show_ammo = g_TRVersion != 1;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_medipacks_used\") == nullptr) {\n        g_Config.ui.stats.show_medipacks_used = g_TRVersion != 1;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_distance_travelled\") == nullptr) {\n        g_Config.ui.stats.show_distance_travelled = g_TRVersion != 1;\n    }\n}\n\nstatic void M_LoadLegacyOptions(JSON_OBJECT *const parent_obj)\n{\n#define L_READ_BOOL(target, key)                                               \\\n    target = JSON_ObjectGetBool(parent_obj, key, target)\n#define L_READ_INT(target, key)                                                \\\n    target = JSON_ObjectGetInt(parent_obj, key, target)\n\n    // TR1X 2.16..4.5.1 load_current_music\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"load_current_music\");\n        if (JSON_ValueIsTrue(value)) {\n            g_Config.audio.music_load_condition =\n                MUSIC_LOAD_CONDITION_NON_AMBIENT;\n        } else if (JSON_ValueIsFalse(value)) {\n            g_Config.audio.music_load_condition = MUSIC_LOAD_CONDITION_NEVER;\n        }\n    }\n\n    // Legacy bool loading screens option.\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"enable_loading_screens\");\n        if (JSON_ValueIsTrue(value)) {\n            g_Config.gameplay.loading_screens = LOADING_SCREENS_ALWAYS;\n        } else if (JSON_ValueIsFalse(value)) {\n            g_Config.gameplay.loading_screens = LOADING_SCREENS_DISABLED;\n        }\n    }\n\n    // TR1X ..4.7\n    L_READ_BOOL(g_Config.window.is_fullscreen, \"enable_fullscreen\");\n    L_READ_BOOL(g_Config.window.is_maximized, \"enable_maximized\");\n    L_READ_BOOL(g_Config.gameplay.enable_walk_to_items, \"walk_to_items\");\n    L_READ_BOOL(\n        g_Config.gameplay.enable_inverted_look, \"enabled_inverted_look\");\n    L_READ_INT(g_Config.window.x, \"window_x\");\n    L_READ_INT(g_Config.window.y, \"window_y\");\n    L_READ_INT(g_Config.window.width, \"window_width\");\n    L_READ_INT(g_Config.window.height, \"window_height\");\n    L_READ_INT(g_Config.input.keyboard_layout, \"layout\");\n    L_READ_INT(g_Config.input.controller_layout, \"cntlr_layout\");\n\n    // TR1X ..4.9\n    L_READ_BOOL(g_Config.gameplay.enable_cutscenes, \"enable_cine\");\n    L_READ_BOOL(g_Config.gameplay.enable_legal, \"enable_eidos_logo\");\n\n    // TR1X ..4.11, TR2X ..1.1: 0…10 scale volumes to 0.0f…1.0f\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"sound_volume\");\n        const JSON_NUMBER *const num =\n            value != nullptr ? JSON_ValueGetNumber(value) : nullptr;\n        if (num != nullptr && strchr(num->number, '.') == nullptr) {\n            g_Config.audio.sound_volume = JSON_ValueGetInt(value, 0) / 10.0f;\n        }\n    }\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"music_volume\");\n        const JSON_NUMBER *const num =\n            value != nullptr ? JSON_ValueGetNumber(value) : nullptr;\n        if (value != nullptr && value->type == JSON_TYPE_NUMBER\n            && strchr(num->number, '.') == nullptr) {\n            g_Config.audio.music_volume = JSON_ValueGetInt(value, 0) / 10.0f;\n        }\n    }\n\n    // TR1X ..4.11: convert wall bug fix\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"fix_wall_jump_glitch\");\n        if (JSON_ValueIsTrue(value)) {\n            g_Config.gameplay.wall_glitch_mode = WALL_GLITCH_FIXED;\n        }\n    }\n\n    // TR1X ..4.13: convert enhanced look to enum type\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"enable_enhanced_look\");\n        if (JSON_ValueIsTrue(value)) {\n            g_Config.gameplay.look_mode = LOOK_MODE_UNRESTRICTED;\n        } else if (JSON_ValueIsFalse(value)) {\n            g_Config.gameplay.look_mode = LOOK_MODE_RESTRICTED;\n        }\n    }\n\n    // TR1X ..4.15, TR2X 1.5\n    if (JSON_ObjectGetValue(parent_obj, \"ambient_volume\") == nullptr) {\n        g_Config.audio.ambient_volume = g_Config.audio.music_volume;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"cutscene_volume\") == nullptr) {\n        g_Config.audio.cutscene_volume = g_Config.audio.music_volume;\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"fmv_volume\") == nullptr) {\n        g_Config.audio.fmv_volume = g_Config.audio.music_volume;\n    }\n\n    // TR2X 1.6\n    L_READ_BOOL(g_Config.audio.enable_ps1_sfx, \"enable_barefoot_sfx\");\n    // TR1X ..4.16\n    L_READ_BOOL(g_Config.audio.enable_ps1_sfx, \"enable_ps_uzi_sfx\");\n    L_READ_BOOL(\n        g_Config.audio.fix_chainblock_secret_sound, \"fix_tihocan_secret_sound\");\n    L_READ_BOOL(g_Config.input.enable_buffering_inventory, \"enable_buffering\");\n    L_READ_BOOL(g_Config.input.enable_buffering_func_keys, \"enable_buffering\");\n\n    // TR1X ..4.16, TR2X ..1.6\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"revert_to_pistols\");\n        if (JSON_ValueIsTrue(value)) {\n            g_Config.gameplay.remember_gun_status = false;\n        } else if (JSON_ValueIsFalse(value)) {\n            g_Config.gameplay.remember_gun_status = true;\n        }\n    }\n\n    // TR2X ..1.5.1\n    {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"fix_pickup_drift_glitch\");\n        if (JSON_ValueIsFalse(value)) {\n            g_Config.gameplay.fix_lara_pickup_embed = false;\n        }\n    }\n\n    // TRX ..1.2: split brightness into game and UI fields.\n    {\n        const JSON_VALUE *const old_value =\n            JSON_ObjectGetValue(parent_obj, \"brightness\");\n        if (old_value != nullptr) {\n            const float old_brightness = JSON_ValueGetDouble(\n                old_value, g_Config.visuals.game_brightness);\n            if (JSON_ObjectGetValue(parent_obj, \"game_brightness\") == nullptr) {\n                g_Config.visuals.game_brightness = old_brightness;\n            }\n            if (JSON_ObjectGetValue(parent_obj, \"ui_brightness\") == nullptr) {\n                g_Config.visuals.ui_brightness = old_brightness;\n            }\n        }\n    }\n\n    // TRX legacy: \"round shadows\" boolean to enum shadow type.\n    if (JSON_ObjectGetValue(parent_obj, \"shadow_type\") == nullptr) {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"enable_round_shadow\");\n        if (JSON_ValueIsTrue(value)) {\n            g_Config.visuals.shadow_type = SHADOW_TYPE_CIRCLE;\n        } else if (JSON_ValueIsFalse(value)) {\n            g_Config.visuals.shadow_type = SHADOW_TYPE_OCTAGON;\n        }\n    }\n\n    // Pre-1.2 UI bar look and color migration; one-shot via config version.\n    if (g_Config.config_version >= 0 && g_Config.config_version < 1) {\n        {\n            const char *const value =\n                JSON_ObjectGetString(parent_obj, \"bar_look\", \"\");\n            const char *new_value = nullptr;\n            if (String_Equivalent(value, \"tr1\")) {\n                new_value = \"tr1_pc\";\n            } else if (String_Equivalent(value, \"tr2\")) {\n                new_value = \"tr2_pc\";\n            } else if (String_Equivalent(value, \"tr3\")) {\n                new_value = \"tr3_pc\";\n            } else if (String_Equivalent(value, \"ps1\")) {\n                new_value = \"tr2_ps1\";\n            }\n            if (new_value != nullptr) {\n                Memory_FreePointer(&g_Config.ui.bar_look);\n                g_Config.ui.bar_look = Memory_DupStr(new_value);\n            }\n        }\n\n        M_MigrateBarColorName(&g_Config.ui.lara_health_bar.color);\n        M_MigrateBarColorName(&g_Config.ui.lara_health_bar.color_ps1);\n        M_MigrateBarColorName(&g_Config.ui.lara_health_bar.poison_color);\n        M_MigrateBarColorName(&g_Config.ui.lara_health_bar.poison_color_ps1);\n        M_MigrateBarColorName(&g_Config.ui.lara_air_bar.color);\n        M_MigrateBarColorName(&g_Config.ui.lara_air_bar.color_ps1);\n        M_MigrateBarColorName(&g_Config.ui.lara_sprint_bar.color);\n        M_MigrateBarColorName(&g_Config.ui.lara_sprint_bar.color_ps1);\n        M_MigrateBarColorName(&g_Config.ui.lara_exposure_bar.color);\n        M_MigrateBarColorName(&g_Config.ui.lara_exposure_bar.color_ps1);\n        M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color);\n        M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color_ps1);\n        M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color_allies);\n        M_MigrateBarColorName(&g_Config.ui.enemy_health_bar.color_allies_ps1);\n    }\n\n    // Pre 1.3 Fixed glide camera option was only visible to TR2 and defaulted\n    // to false in TR1. Now a regular toggle for all camera modes.\n    if (g_TRVersion == 2) {\n        L_READ_BOOL(g_Config.visuals.enable_glide_cameras, \"fix_glide_cameras\");\n    }\n\n    // TRX ..1.4: sunglasses on/off changed to mode.\n    if (JSON_ObjectGetValue(parent_obj, \"sunglasses_mode\") == nullptr) {\n        const JSON_VALUE *const value =\n            JSON_ObjectGetValue(parent_obj, \"enable_sunglasses\");\n        g_Config.visuals.sunglasses_mode = JSON_ValueIsTrue(value)\n            ? SUNGLASSES_MODE_OPAQUE\n            : SUNGLASSES_MODE_OFF;\n    }\n\n    if (JSON_ObjectGetValue(parent_obj, \"show_level_header\") == nullptr) {\n        L_READ_BOOL(\n            g_Config.ui.stats.show_level_header, \"enable_stats_level_header\");\n    }\n    if (JSON_ObjectGetValue(parent_obj, \"show_deaths\") == nullptr) {\n        L_READ_BOOL(g_Config.ui.stats.show_deaths, \"enable_deaths_counter\");\n    }\n    M_MigrateLegacyStatsOptions(parent_obj);\n\n    if (g_Config.config_version >= 0\n        && g_Config.config_version < M_CONFIG_VERSION_CURRENT) {\n        g_Config.config_version = M_CONFIG_VERSION_CURRENT;\n    }\n\n#undef L_READ_BOOL\n#undef L_READ_INT\n}\n\nvoid Config_LoadFromJSON(JSON_OBJECT *root_obj)\n{\n    ConfigFile_LoadOptions(root_obj, Config_GetOptionMap());\n    if (Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) {\n        ConfigFile_LoadGymTrackStats(\n            root_obj, \"assault_stats\", &g_Config.profile.assault_stats);\n    }\n    if (Gym_TrackManager_HasStats(GYM_TRACK_QUAD)) {\n        ConfigFile_LoadGymTrackStats(\n            root_obj, \"racetrack_stats\", &g_Config.profile.racetrack_stats);\n    }\n    M_LoadLegacyInputConfig(root_obj);\n    M_LoadInputConfig(root_obj);\n    M_LoadLegacyOptions(root_obj);\n}\n\nvoid Config_DumpToJSON(JSON_OBJECT *root_obj)\n{\n    ConfigFile_DumpOptions(root_obj, Config_GetOptionMap());\n    if (Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) {\n        ConfigFile_DumpGymTrackStats(\n            root_obj, \"assault_stats\", &g_Config.profile.assault_stats);\n    }\n    if (Gym_TrackManager_HasStats(GYM_TRACK_QUAD)) {\n        ConfigFile_DumpGymTrackStats(\n            root_obj, \"racetrack_stats\", &g_Config.profile.racetrack_stats);\n    }\n    M_DumpInputConfig(root_obj);\n}\n\nvoid Config_Sanitize(void)\n{\n    if (g_Config.rendering.aspect_mode != ASPECT_MODE_ANY\n        && g_Config.rendering.aspect_mode != ASPECT_MODE_16_9\n        && g_Config.rendering.aspect_mode != ASPECT_MODE_16_10) {\n        g_Config.rendering.aspect_mode = ASPECT_MODE_4_3;\n    }\n    CLAMP(g_Config.audio.master_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.sound_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.music_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.inventory_music_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.underwater_music_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.ambient_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.inventory_ambient_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.underwater_ambient_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.cutscene_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.audio.fmv_volume, 0.0f, 1.0f);\n    CLAMP(g_Config.input.keyboard_layout, 0, INPUT_LAYOUT_NUMBER_OF - 1);\n    CLAMP(g_Config.input.controller_layout, 0, INPUT_LAYOUT_NUMBER_OF - 1);\n    CLAMP(\n        g_Config.gameplay.turbo_speed, CLOCK_TURBO_SPEED_MIN,\n        CLOCK_TURBO_SPEED_MAX);\n    CLAMP(g_Config.gameplay.start_lara_hitpoints, 1, LARA_MAX_HITPOINTS);\n    CLAMP(g_Config.gameplay.camera_speed, 1, 10);\n    CLAMP(g_Config.gameplay.idle_pose_timeout, 0, 1200);\n    CLAMP(g_Config.rendering.wireframe_width, 1.0, 100.0);\n    CLAMP(g_Config.rendering.upscaling_factor, 1, 8);\n    CLAMP(g_Config.rendering.borders, 0.0, 0.45);\n    CLAMP(g_Config.ui.bar_scale, 0.5, 2.0);\n    CLAMP(g_Config.ui.text_scale, 0.5, 2.0);\n    CLAMP(g_Config.ui.pickup_scale, 0.5, 2.0);\n    CLAMP(g_Config.visuals.fog_start, 1, 100);\n    CLAMP(g_Config.visuals.fog_end, 1, 100);\n    CLAMP(g_Config.visuals.fov, 30, 150);\n    CLAMPL(g_Config.gameplay.maximum_save_slots, 0);\n    CLAMPL(g_Config.gameplay.maximum_quick_save_slots, 0);\n    CLAMP(g_Config.visuals.shadow_type, 0, SHADOW_TYPE_NUMBER_OF - 1);\n    CLAMP(g_Config.visuals.blood_effects, 0, BLOOD_EFFECTS_NUMBER_OF - 1);\n    CLAMP(g_Config.gameplay.loading_screens, 0, LOADING_SCREENS_NEW_GAMES);\n\n    if (g_Config.rendering.fps != 30 && g_Config.rendering.fps != 60) {\n        g_Config.rendering.fps = 30;\n    }\n\n    CLAMP(\n        g_Config.visuals.game_brightness, CONFIG_MIN_BRIGHTNESS,\n        CONFIG_MAX_BRIGHTNESS);\n    CLAMP(\n        g_Config.visuals.ui_brightness, CONFIG_MIN_BRIGHTNESS,\n        CONFIG_MAX_BRIGHTNESS);\n    CLAMP(g_Config.visuals.gamma, CONFIG_MIN_GAMMA, CONFIG_MAX_GAMMA);\n    CLAMPL(g_Config.rendering.anisotropy_filter, 1.0);\n}\n"
  },
  {
    "path": "src/trx/config/priv.h",
    "content": "#pragma once\n\n#include <trx/core/json.h>\n\nvoid Config_LoadFromJSON(JSON_OBJECT *root_obj);\nvoid Config_DumpToJSON(JSON_OBJECT *root_obj);\nvoid Config_Sanitize(void);\n"
  },
  {
    "path": "src/trx/config/types.h",
    "content": "#pragma once\n\n#include <trx/config/const.h>\n#include <trx/config/enum.h>\n#include <trx/core/colors.h>\n#include <trx/gl/enum.h>\n\ntypedef struct {\n    uint32_t time;\n    uint32_t attempt_num;\n} GYM_TRACK_ENTRY;\n\ntypedef struct {\n    GYM_TRACK_ENTRY entries[MAX_ASSAULT_TIMES];\n    uint32_t total_attempts;\n} GYM_TRACK_STATS;\n\ntypedef struct {\n    // This signifies whether the config was already read from disk.\n    bool loaded;\n\n    // This holds paths passed to Config_Read(), so that Config_Write() knows\n    // where to save the updates.\n    char *default_path;\n    char *enforced_path;\n\n    // This field is used to force trigger a change event for fields that are\n    // not stored in the CONFIG struct.\n    bool dirty;\n\n    // Start of user fields\n    int32_t config_version;\n    char *language;\n\n    struct {\n        bool new_game_plus_unlock;\n        GYM_TRACK_STATS assault_stats;\n        GYM_TRACK_STATS racetrack_stats;\n    } profile;\n\n    struct {\n        INPUT_BACKEND backend; // Not decisive - mostly for UI visuals\n        union {\n            struct {\n                int32_t keyboard_layout;\n                int32_t controller_layout;\n            };\n            int32_t layout[INPUT_BACKEND_NUMBER_OF];\n        };\n        bool enable_tr3_sidesteps;\n        bool enable_responsive_passport;\n        bool enable_buffering_func_keys;\n        bool enable_buffering_inventory;\n        QUICK_GUNS_MODE quick_guns_mode;\n    } input;\n\n    struct {\n        bool is_fullscreen;\n        bool is_maximized;\n        int32_t x;\n        int32_t y;\n        int32_t width;\n        int32_t height;\n        int32_t fs_width;\n        int32_t fs_height;\n    } window;\n\n    struct {\n        bool enable_fade_effects;\n        bool enable_exit_fade_effects;\n\n        int32_t fov;\n\n        CAMERA_MODE camera_mode;\n        bool enable_glide_cameras;\n        float game_brightness;\n        float ui_brightness;\n        float gamma;\n\n        bool enable_reflections;\n        bool enable_3d_pickups;\n        bool enable_braid;\n        bool enable_breeze;\n        bool enable_gun_lighting;\n        bool enable_fire_lighting;\n        bool enable_shotgun_flash;\n        bool enable_responsive_mesh_tint;\n        char *lara_outfit;\n        SUNGLASSES_MODE sunglasses_mode;\n        SHADOW_TYPE shadow_type;\n        BLOOD_EFFECTS blood_effects;\n        bool enable_skybox;\n        bool enable_weather;\n        bool enable_footprints;\n        bool enable_ps1_crystals;\n\n        bool fix_item_rots;\n        bool fix_animated_sprites;\n        bool fix_texture_issues;\n\n        RGB_888 water_color;\n        bool fog_transparency;\n        RGB_888 fog_color;\n        int32_t fog_start;\n        int32_t fog_end;\n    } visuals;\n\n    struct {\n        bool enable_game_ui;\n        bool enable_photo_mode_ui;\n        bool enable_wraparound;\n        bool enable_fps_counter;\n        bool show_pickups_overlay;\n        bool show_title_version;\n\n        float text_scale;\n        float bar_scale;\n        float pickup_scale;\n\n        UI_STYLE menu_style;\n        struct {\n            STATS_STYLE style;\n            bool show_totals;\n            bool show_level_header;\n            bool show_time_taken;\n            bool show_secrets;\n            bool show_crystals;\n            bool show_pickups;\n            bool show_kills;\n            bool show_ammo;\n            bool show_medipacks_used;\n            bool show_distance_travelled;\n            bool show_deaths;\n        } stats;\n\n        BACKGROUND_TYPE inventory_background_style;\n        BACKGROUND_TYPE stats_background_style;\n        BACKGROUND_TYPE pause_background_style;\n        bool inventory_fade_effects;\n        bool stats_fade_effects;\n        bool pause_fade_effects;\n\n        bool enable_smooth_bars;\n        char *bar_look;\n        bool show_bars;\n        bool enable_bar_flashing;\n        struct {\n            UI_ELEMENT_LOCATION location;\n            char *color;\n            char *color_ps1;\n            char *poison_color;\n            char *poison_color_ps1;\n        } lara_health_bar;\n        struct {\n            UI_ELEMENT_LOCATION location;\n            char *color;\n            char *color_ps1;\n        } lara_air_bar, lara_sprint_bar, lara_exposure_bar;\n        struct {\n            BAR_SHOW_MODE show_mode;\n            UI_ELEMENT_LOCATION location;\n            char *color;\n            char *color_ps1;\n            char *color_allies;\n            char *color_allies_ps1;\n        } enemy_health_bar;\n        struct {\n            UI_ELEMENT_LOCATION location;\n        } ammo_counter;\n    } ui;\n\n    struct {\n        float master_volume;\n        float sound_volume;\n        float music_volume;\n        float inventory_music_volume;\n        float underwater_music_volume;\n        float ambient_volume;\n        float inventory_ambient_volume;\n        float underwater_ambient_volume;\n        float cutscene_volume;\n        float fmv_volume;\n\n        bool fix_chainblock_secret_sound;\n        bool fix_secrets_killing_music;\n        bool fix_speeches_killing_music;\n        bool enable_lara_mic;\n        bool enable_music_in_menu;\n        bool enable_music_in_inventory;\n        bool enable_ps1_sfx;\n        bool enable_pitched_sounds;\n        bool load_music_triggers;\n        bool enable_underwater_anim_sfx;\n        bool mute_out_of_focus;\n\n        MUSIC_LOAD_CONDITION music_load_condition;\n    } audio;\n\n    struct {\n        bool disable_healing_between_levels;\n        bool disable_medpacks;\n        bool disable_extra_guns;\n        bool enable_pickup_aids;\n        bool enable_save_crystals;\n        bool enable_enhanced_saves;\n\n        bool enable_cheats;\n        bool enable_console;\n        bool enable_game_modes;\n        bool enable_play_previous_levels;\n        bool enable_fmv;\n        bool enable_legal;\n        bool enable_credits;\n        bool enable_cinematics;\n        bool enable_cutscenes;\n        bool enable_demo;\n        LOADING_SCREENS_MODE loading_screens;\n        bool enable_compass_stats;\n        bool enable_total_stats;\n        bool pause_on_focus_lost;\n\n        bool enable_jump_twists;\n        bool enable_uw_roll;\n        bool enable_crouch_roll;\n        bool enable_tr2_swimming;\n        bool enable_wading;\n        bool enable_tr2_swim_cancel;\n        bool enable_tr2_jumping;\n        bool enable_swing_cancel;\n        bool enable_smooth_wall_deflect;\n        bool enable_lean_jumping;\n        bool enable_step_roll_boost;\n        bool enable_slide_to_run;\n        bool enable_back_slope_stumble;\n        bool enable_neutral_twists;\n        bool enable_controlled_drops;\n        bool enable_ledge_jumps;\n        bool enable_crawling;\n        bool enable_responsive_crawl;\n        bool enable_crawl_jump;\n        bool enable_crawl_tilt;\n        bool enable_sprint;\n        bool enable_responsive_sprint;\n        bool enable_toggle_crouch;\n        bool enable_toggle_sprint;\n        bool enable_slow_ledge_swing;\n        int32_t idle_pose_timeout;\n        bool enable_idle_pose_camera;\n        bool enable_soft_statics;\n        bool enable_bouncy_grenades;\n\n        bool enable_auto_item_selection;\n        bool enable_manual_camera;\n        bool enable_item_examining;\n        bool enable_target_change;\n        bool enable_walk_to_items;\n        bool restore_ps1_enemies;\n        bool enable_ally_targeting;\n        bool enable_enemy_rotation;\n        bool enable_killer_pushblocks;\n        bool enable_boulder_shake;\n        bool enable_body_bags;\n        ALLY_HOSTILITY_POLICY ally_hostility_policy;\n        CREATURE_DROWN_POLICY creature_drown_policy;\n\n        bool enable_timer_in_inventory;\n        LOOK_MODE look_mode;\n        bool enable_inverted_look;\n        bool remember_gun_status;\n\n        int32_t turbo_speed;\n        int32_t camera_speed;\n        int32_t start_lara_hitpoints;\n        int32_t maximum_save_slots;\n        int32_t maximum_quick_save_slots;\n        int32_t harpoon_recoil;\n\n        JUMP_LOCK_MODE jump_lock_mode;\n        TARGET_LOCK_MODE target_mode;\n        bool fix_qwop_glitch;\n        bool fix_step_glitch;\n        bool fix_item_duplication_glitch;\n        bool fix_descending_glitch;\n        bool fix_lara_pickup_embed;\n        bool fix_water_exit;\n        WALL_GLITCH_MODE wall_glitch_mode;\n        bool fix_wall_geometry;\n        bool fix_alligator_ai;\n        bool disable_trex_collision;\n        bool change_pierre_spawn;\n        bool fix_m16_accuracy;\n        bool fix_flare_throw_priority;\n        bool fix_free_flare_glitch;\n        bool fix_walk_run_jump;\n        bool fix_wade_wall_hit;\n\n        bool fix_floor_data_issues;\n        bool fix_bridge_collision;\n        bool fix_bear_ai;\n        bool fix_monkey_pickup_priority;\n        bool fix_pipeman_aim;\n\n        PROJECTILE_AREA_DAMAGE projectile_area_damage;\n    } gameplay;\n\n    struct {\n        ASPECT_MODE aspect_mode;\n        int32_t fps;\n        bool enable_trapezoid_filter;\n        bool enable_lighting;\n        bool enable_textures;\n        TEXTURE_FILTER ui_filter;\n        TEXTURE_FILTER texture_filter;\n        TEXTURE_FILTER upscaling_filter;\n        bool enable_wireframe;\n        float wireframe_width;\n        bool enable_vsync;\n        float anisotropy_filter;\n        SCREENSHOT_FORMAT screenshot_format;\n        LIGHTING_CONTRAST lighting_contrast;\n        BILLBOARD_LOCK_MODE sprite_lock_mode;\n        int32_t upscaling_factor;\n        float borders;\n    } rendering;\n\n    struct {\n        bool enable_debug_triggers;\n        bool enable_debug_portals;\n        bool enable_debug_room_clip;\n        bool enable_debug_spheres;\n        bool enable_debug_bounding_boxes;\n        bool enable_debug_pos;\n        bool enable_debug_anim;\n        bool enable_debug_camera;\n        bool enable_debug_status;\n        bool enable_review_markers;\n        bool enable_invulnerability;\n        bool enable_endless_sprint;\n        bool enable_endless_flare_time;\n    } debug;\n\n    struct {\n        bool lockout_option_ring;\n        bool load_save_disabled;\n        bool play_any_level;\n        float demo_delay;\n        bool cheat_keys;\n    } flow;\n} CONFIG;\n"
  },
  {
    "path": "src/trx/config/vars.c",
    "content": "#include <trx/config/vars.h>\n\nCONFIG g_Config = {};\nCONFIG g_SavedConfig = {};\n"
  },
  {
    "path": "src/trx/config/vars.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n\nextern CONFIG g_Config;\nextern CONFIG g_SavedConfig;\n"
  },
  {
    "path": "src/trx/config.h",
    "content": "#pragma once\n\n#include <trx/config/common.h>\n#include <trx/config/dynamic_enum.h>\n#include <trx/config/vars.h>\n"
  },
  {
    "path": "src/trx/core/benchmark.c",
    "content": "#include <trx/core/benchmark.h>\n\n#include <trx/core/log.h>\n\n#include <SDL2/SDL_timer.h>\n\nstatic void M_Log(\n    BENCHMARK *const b, const char *file, int32_t line, const char *func,\n    Uint64 current, const char *message, const bool closing)\n{\n    const Uint64 freq = SDL_GetPerformanceFrequency();\n    const double elapsed_start =\n        (double)(current - b->start) * 1000.0 / (double)freq;\n    const double elapsed_last =\n        (double)(current - b->last) * 1000.0 / (double)freq;\n\n    if (closing) {\n        if (message == nullptr) {\n            Log_Message(\n                LOG_LEVEL_DEBUG, file, line, func, \"took %.02f ms\",\n                elapsed_start);\n        } else {\n            Log_Message(\n                LOG_LEVEL_DEBUG, file, line, func, \"%s: took %.02f ms\", message,\n                elapsed_start);\n        }\n    } else {\n        if (message == nullptr) {\n            Log_Message(\n                LOG_LEVEL_DEBUG, file, line, func, \"took %.02f ms (%.02f ms)\",\n                elapsed_start, elapsed_last);\n        } else {\n            Log_Message(\n                LOG_LEVEL_DEBUG, file, line, func,\n                \"%s: took %.02f ms (%.02f ms)\", message, elapsed_start,\n                elapsed_last);\n        }\n    }\n}\n\nBENCHMARK Benchmark_Start(void)\n{\n    const Uint64 perf = SDL_GetPerformanceCounter();\n    return (BENCHMARK) {\n        .start = perf,\n        .last = perf,\n    };\n}\n\nvoid Benchmark_Tick_Impl(\n    BENCHMARK *const b, const char *const file, const int32_t line,\n    const char *const func, const char *const message)\n{\n    const Uint64 current = SDL_GetPerformanceCounter();\n    M_Log(b, file, line, func, current, message, false);\n    b->last = current;\n}\n\nvoid Benchmark_End_Impl(\n    BENCHMARK *b, const char *const file, const int32_t line,\n    const char *const func, const char *const message)\n{\n    const Uint64 current = SDL_GetPerformanceCounter();\n    M_Log(b, file, line, func, current, message, true);\n}\n"
  },
  {
    "path": "src/trx/core/benchmark.h",
    "content": "#pragma once\n\n#include <SDL2/SDL_stdinc.h>\n\ntypedef struct {\n    Uint64 start;\n    Uint64 last;\n} BENCHMARK;\n\nBENCHMARK Benchmark_Start(void);\n\n#define Benchmark_End(b, ...)                                                  \\\n    Benchmark_End_Impl(b, __FILE__, __LINE__, __func__, __VA_ARGS__)\n\n#define Benchmark_Tick(b, ...)                                                 \\\n    Benchmark_Tick_Impl(b, __FILE__, __LINE__, __func__, __VA_ARGS__)\n\nvoid Benchmark_End_Impl(\n    BENCHMARK *b, const char *file, int32_t line, const char *func,\n    const char *message);\n\nvoid Benchmark_Tick_Impl(\n    BENCHMARK *b, const char *file, int32_t line, const char *func,\n    const char *message);\n"
  },
  {
    "path": "src/trx/core/bson/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    BSON_PARSE_ERROR_NONE = 0,\n    BSON_PARSE_ERROR_INVALID_VALUE,\n    BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER,\n    BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES,\n    BSON_PARSE_ERROR_UNKNOWN,\n} BSON_PARSE_ERROR;\n"
  },
  {
    "path": "src/trx/core/bson/parse.c",
    "content": "#include <trx/core/bson/parse.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n\ntypedef struct {\n    const char *src;\n    size_t size;\n    size_t offset;\n\n    char *data;\n    char *dom;\n    size_t dom_size;\n    size_t data_size;\n\n    size_t error;\n} M_STATE;\n\nstatic bool M_GetValueSize(M_STATE *state, uint8_t marker);\nstatic void M_HandleValue(M_STATE *state, JSON_VALUE *value, uint8_t marker);\n\nstatic int32_t M_ReadI32(const char *const src)\n{\n    int32_t value = 0;\n    memcpy(&value, src, sizeof(value));\n    return value;\n}\n\nstatic double M_ReadDouble(const char *const src)\n{\n    double value = 0.0;\n    memcpy(&value, src, sizeof(value));\n    return value;\n}\n\nstatic bool M_GetObjectKeySize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    while (state->src[state->offset]) {\n        state->data_size++;\n        state->offset++;\n    }\n    state->data_size++;\n    state->offset++;\n    return true;\n}\n\nstatic bool M_GetNullValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    return true;\n}\n\nstatic bool M_GetBoolValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    if (state->offset + sizeof(uint8_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n\n    switch (state->src[state->offset]) {\n    case 0x00:\n        break;\n    case 0x01:\n        break;\n    default:\n        state->error = BSON_PARSE_ERROR_INVALID_VALUE;\n        return false;\n    }\n\n    state->offset++;\n    return true;\n}\n\nstatic bool M_GetInt32ValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    if (state->offset + sizeof(int32_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    int32_t num = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    state->dom_size += sizeof(JSON_NUMBER);\n    state->data_size += snprintf(nullptr, 0, \"%d\", num) + 1;\n    return true;\n}\n\nstatic bool M_GetDoubleValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    if (state->offset + sizeof(double) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    double num = M_ReadDouble(&state->src[state->offset]);\n    state->offset += sizeof(double);\n\n    state->dom_size += sizeof(JSON_NUMBER);\n    state->data_size += snprintf(nullptr, 0, \"%f\", num) + 1;\n    return true;\n}\n\nstatic bool M_GetStringValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    if (state->offset + sizeof(int32_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    int32_t size = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n    if (state->offset + size > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    if (state->src[state->offset + size - 1] != '\\0') {\n        state->error = BSON_PARSE_ERROR_INVALID_VALUE;\n        return false;\n    }\n    state->offset += size;\n    state->dom_size += sizeof(JSON_STRING);\n    state->data_size += size;\n    return true;\n}\n\nstatic bool M_GetArrayElementWrappedSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n\n    if (state->offset + sizeof(uint8_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    uint8_t marker = state->src[state->offset];\n    state->offset++;\n\n    // BSON arrays always use keys\n    state->dom_size += sizeof(JSON_STRING);\n    if (!M_GetObjectKeySize(state)) {\n        return false;\n    }\n\n    state->dom_size += sizeof(JSON_VALUE);\n    return M_GetValueSize(state, marker);\n}\n\nstatic bool M_GetArraySize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n\n    const size_t start_offset = state->offset;\n    if (state->offset + sizeof(int32_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    const int size = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    while (state->offset < start_offset + size - 1) {\n        state->dom_size += sizeof(JSON_ARRAY_ELEMENT);\n        if (!M_GetArrayElementWrappedSize(state)) {\n            return false;\n        }\n    }\n\n    if (state->offset + sizeof(char) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    if (state->src[state->offset] != '\\0') {\n        state->error = BSON_PARSE_ERROR_INVALID_VALUE;\n        return false;\n    }\n    state->offset++;\n    return true;\n}\n\nstatic bool M_GetArrayValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    state->dom_size += sizeof(JSON_ARRAY);\n    return M_GetArraySize(state);\n}\n\nstatic bool M_GetObjectElementWrappedSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n\n    if (state->offset + sizeof(uint8_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    uint8_t marker = state->src[state->offset];\n    state->offset++;\n\n    state->dom_size += sizeof(JSON_STRING);\n    if (!M_GetObjectKeySize(state)) {\n        return false;\n    }\n\n    state->dom_size += sizeof(JSON_VALUE);\n    return M_GetValueSize(state, marker);\n}\n\nstatic bool M_GetObjectSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n\n    const size_t start_offset = state->offset;\n    if (state->offset + sizeof(int32_t) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    const int size = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    while (state->offset < start_offset + size - 1) {\n        state->dom_size += sizeof(JSON_OBJECT_ELEMENT);\n        if (!M_GetObjectElementWrappedSize(state)) {\n            return false;\n        }\n    }\n\n    if (state->offset + sizeof(char) > state->size) {\n        state->error = BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return false;\n    }\n    if (state->src[state->offset] != '\\0') {\n        state->error = BSON_PARSE_ERROR_INVALID_VALUE;\n        return false;\n    }\n    state->offset++;\n    return true;\n}\n\nstatic bool M_GetObjectValueSize(M_STATE *state)\n{\n    ASSERT(state != nullptr);\n    state->dom_size += sizeof(JSON_OBJECT);\n    return M_GetObjectSize(state);\n}\n\nstatic bool M_GetValueSize(M_STATE *state, uint8_t marker)\n{\n    ASSERT(state != nullptr);\n    switch (marker) {\n    case 0x01:\n        return M_GetDoubleValueSize(state);\n    case 0x02:\n        return M_GetStringValueSize(state);\n    case 0x03:\n        return M_GetObjectValueSize(state);\n    case 0x04:\n        return M_GetArrayValueSize(state);\n    case 0x0A:\n        return M_GetNullValueSize(state);\n    case 0x08:\n        return M_GetBoolValueSize(state);\n    case 0x10:\n        return M_GetInt32ValueSize(state);\n    default:\n        state->error = BSON_PARSE_ERROR_INVALID_VALUE;\n        return false;\n    }\n}\n\nstatic bool M_GetRootSize(M_STATE *state)\n{\n    // assume the root element to be an object\n    state->dom_size += sizeof(JSON_VALUE);\n    return M_GetObjectValueSize(state);\n}\n\nstatic void M_HandleObjectKey(M_STATE *state, JSON_STRING *string)\n{\n    ASSERT(state != nullptr);\n    ASSERT(string != nullptr);\n    size_t size = 0;\n    string->ref_count = 1;\n    string->string = state->data;\n    while (state->src[state->offset]) {\n        state->data[size++] = state->src[state->offset++];\n    }\n    string->string_size = size;\n    state->data[size++] = state->src[state->offset++];\n    state->data += size;\n}\n\nstatic void M_HandleNullValue(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n    value->type = JSON_TYPE_NULL;\n    value->payload = nullptr;\n}\n\nstatic void M_HandleBoolValue(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n    ASSERT(state->offset + sizeof(char) <= state->size);\n    switch (state->src[state->offset]) {\n    case 0x00:\n        value->type = JSON_TYPE_FALSE;\n        value->payload = nullptr;\n        break;\n    case 0x01:\n        value->type = JSON_TYPE_TRUE;\n        value->payload = nullptr;\n        break;\n    default:\n        ASSERT_FAIL();\n    }\n    state->offset++;\n}\n\nstatic void M_HandleInt32Value(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n\n    ASSERT(state->offset + sizeof(int32_t) <= state->size);\n    int32_t num = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    JSON_NUMBER *number = (JSON_NUMBER *)state->dom;\n    number->ref_count = 1;\n    state->dom += sizeof(JSON_NUMBER);\n\n    number->number = state->data;\n    sprintf(state->data, \"%d\", num);\n    number->number_size = strlen(number->number);\n    state->data += number->number_size + 1;\n\n    value->type = JSON_TYPE_NUMBER;\n    value->payload = number;\n}\n\nstatic void M_HandleDoubleValue(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n\n    ASSERT(state->offset + sizeof(double) <= state->size);\n    double num = M_ReadDouble(&state->src[state->offset]);\n    state->offset += sizeof(double);\n\n    JSON_NUMBER *number = (JSON_NUMBER *)state->dom;\n    number->ref_count = 1;\n    state->dom += sizeof(JSON_NUMBER);\n\n    number->number = state->data;\n    sprintf(state->data, \"%f\", num);\n    number->number_size = strlen(number->number);\n    state->data += number->number_size + 1;\n\n    // strip trailing zeroes after decimal point\n    if (strchr(number->number, '.')) {\n        while (number->number[number->number_size - 1] == '0'\n               && number->number_size > 1) {\n            number->number_size--;\n        }\n        number->number[number->number_size] = '\\0';\n    }\n\n    value->type = JSON_TYPE_NUMBER;\n    value->payload = number;\n}\n\nstatic void M_HandleStringValue(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n\n    ASSERT(state->offset + sizeof(int32_t) <= state->size);\n    int32_t size = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    JSON_STRING *string = (JSON_STRING *)state->dom;\n    string->ref_count = 1;\n    state->dom += sizeof(JSON_STRING);\n\n    memcpy(state->data, state->src + state->offset, size);\n    state->offset += size;\n\n    string->string = state->data;\n    string->string_size = size;\n    state->data += size;\n\n    value->type = JSON_TYPE_STRING;\n    value->payload = string;\n}\n\nstatic void M_HandleArrayElementWrapped(\n    M_STATE *state, JSON_ARRAY_ELEMENT *element)\n{\n    ASSERT(state != nullptr);\n    ASSERT(element != nullptr);\n\n    ASSERT(state->offset + sizeof(uint8_t) <= state->size);\n    uint8_t marker = state->src[state->offset];\n    state->offset++;\n\n    // BSON arrays always use keys\n    JSON_STRING *key = (JSON_STRING *)state->dom;\n    key->ref_count = 1;\n    state->dom += sizeof(JSON_STRING);\n    M_HandleObjectKey(state, key);\n\n    JSON_VALUE *value = (JSON_VALUE *)state->dom;\n    value->ref_count = 1;\n    state->dom += sizeof(JSON_VALUE);\n\n    element->value = value;\n\n    M_HandleValue(state, value, marker);\n}\n\nstatic void M_HandleArray(M_STATE *state, JSON_ARRAY *array)\n{\n    ASSERT(state != nullptr);\n    ASSERT(array != nullptr);\n\n    const size_t start_offset = state->offset;\n    ASSERT(state->offset + sizeof(int32_t) <= state->size);\n    const int size = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    JSON_ARRAY_ELEMENT *previous = nullptr;\n    int count = 0;\n    while (state->offset < start_offset + size - 1) {\n        JSON_ARRAY_ELEMENT *element = (JSON_ARRAY_ELEMENT *)state->dom;\n        element->ref_count = 1;\n        state->dom += sizeof(JSON_ARRAY_ELEMENT);\n        if (!previous) {\n            array->start = element;\n        } else {\n            previous->next = element;\n        }\n        previous = element;\n        M_HandleArrayElementWrapped(state, element);\n        count++;\n    }\n    if (previous) {\n        previous->next = nullptr;\n    }\n    if (!count) {\n        array->start = nullptr;\n    }\n    array->ref_count = 1;\n    array->length = count;\n    ASSERT(state->offset + sizeof(char) <= state->size);\n    ASSERT(state->src[state->offset] == '\\0');\n    state->offset++;\n}\n\nstatic void M_HandleArrayValue(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n\n    JSON_ARRAY *array = (JSON_ARRAY *)state->dom;\n    array->ref_count = 1;\n    state->dom += sizeof(JSON_ARRAY);\n\n    M_HandleArray(state, array);\n\n    value->type = JSON_TYPE_ARRAY;\n    value->payload = array;\n}\n\nstatic void M_HandleObjectElementWrapped(\n    M_STATE *state, JSON_OBJECT_ELEMENT *element)\n{\n    ASSERT(state != nullptr);\n    ASSERT(element != nullptr);\n\n    ASSERT(state->offset + sizeof(uint8_t) <= state->size);\n    uint8_t marker = state->src[state->offset];\n    state->offset++;\n\n    JSON_STRING *key = (JSON_STRING *)state->dom;\n    key->ref_count = 1;\n    state->dom += sizeof(JSON_STRING);\n    M_HandleObjectKey(state, key);\n\n    JSON_VALUE *value = (JSON_VALUE *)state->dom;\n    value->ref_count = 1;\n    state->dom += sizeof(JSON_VALUE);\n\n    element->name = key;\n    element->value = value;\n\n    M_HandleValue(state, value, marker);\n}\n\nstatic void M_HandleObject(M_STATE *state, JSON_OBJECT *object)\n{\n    ASSERT(state != nullptr);\n    ASSERT(object != nullptr);\n\n    const size_t start_offset = state->offset;\n    ASSERT(state->offset + sizeof(int32_t) <= state->size);\n    const int size = M_ReadI32(&state->src[state->offset]);\n    state->offset += sizeof(int32_t);\n\n    JSON_OBJECT_ELEMENT *previous = nullptr;\n    int count = 0;\n    while (state->offset < start_offset + size - 1) {\n        JSON_OBJECT_ELEMENT *element = (JSON_OBJECT_ELEMENT *)state->dom;\n        element->ref_count = 1;\n        state->dom += sizeof(JSON_OBJECT_ELEMENT);\n        if (!previous) {\n            object->start = element;\n        } else {\n            previous->next = element;\n        }\n        previous = element;\n        M_HandleObjectElementWrapped(state, element);\n        count++;\n    }\n    if (previous) {\n        previous->next = nullptr;\n    }\n    if (!count) {\n        object->start = nullptr;\n    }\n    object->ref_count = 1;\n    object->length = count;\n    ASSERT(state->offset + sizeof(char) <= state->size);\n    ASSERT(state->src[state->offset] == '\\0');\n    state->offset++;\n}\n\nstatic void M_HandleObjectValue(M_STATE *state, JSON_VALUE *value)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n\n    JSON_OBJECT *object = (JSON_OBJECT *)state->dom;\n    object->ref_count = 1;\n    state->dom += sizeof(JSON_OBJECT);\n\n    M_HandleObject(state, object);\n\n    value->type = JSON_TYPE_OBJECT;\n    value->payload = object;\n}\n\nstatic void M_HandleValue(M_STATE *state, JSON_VALUE *value, uint8_t marker)\n{\n    ASSERT(state != nullptr);\n    ASSERT(value != nullptr);\n    switch (marker) {\n    case 0x01:\n        M_HandleDoubleValue(state, value);\n        break;\n    case 0x02:\n        M_HandleStringValue(state, value);\n        break;\n    case 0x03:\n        M_HandleObjectValue(state, value);\n        break;\n    case 0x04:\n        M_HandleArrayValue(state, value);\n        break;\n    case 0x0A:\n        M_HandleNullValue(state, value);\n        break;\n    case 0x08:\n        M_HandleBoolValue(state, value);\n        break;\n    case 0x10:\n        M_HandleInt32Value(state, value);\n        break;\n    default:\n        ASSERT_FAIL();\n    }\n}\n\nJSON_VALUE *BSON_Parse(const char *src, size_t src_size)\n{\n    return BSON_ParseEx(src, src_size, nullptr);\n}\n\nJSON_VALUE *BSON_ParseEx(\n    const char *src, size_t src_size, BSON_PARSE_RESULT *result)\n{\n    M_STATE state;\n    void *allocation;\n    JSON_VALUE *value;\n    size_t total_size;\n\n    if (result) {\n        result->error = BSON_PARSE_ERROR_NONE;\n        result->error_offset = 0;\n    }\n\n    if (!src) {\n        return nullptr;\n    }\n\n    state.src = src;\n    state.size = src_size;\n    state.offset = 0;\n    state.error = BSON_PARSE_ERROR_NONE;\n    state.dom_size = 0;\n    state.data_size = 0;\n\n    if (M_GetRootSize(&state)) {\n        if (state.offset != state.size) {\n            state.error = BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES;\n        }\n    }\n\n    if (state.error != BSON_PARSE_ERROR_NONE) {\n        if (result) {\n            result->error = state.error;\n            result->error_offset = state.offset;\n        }\n        LOG_ERROR(\n            \"Error while reading BSON near offset %d: %s\", state.offset,\n            BSON_GetErrorDescription(state.error));\n        return nullptr;\n    }\n\n    total_size = state.dom_size + state.data_size;\n\n    allocation = Memory_Alloc(total_size);\n    state.offset = 0;\n    state.dom = (char *)allocation;\n    state.data = state.dom + state.dom_size;\n\n    // assume the root element to be an object\n    value = (JSON_VALUE *)state.dom;\n    value->ref_count = 0;\n    state.dom += sizeof(JSON_VALUE);\n    M_HandleObjectValue(&state, value);\n\n    ASSERT(state.dom == (char *)allocation + state.dom_size);\n    ASSERT(state.data == (char *)allocation + state.dom_size + state.data_size);\n\n    return value;\n}\n\nconst char *BSON_GetErrorDescription(BSON_PARSE_ERROR error)\n{\n    switch (error) {\n    case BSON_PARSE_ERROR_NONE:\n        return \"no error\";\n\n    case BSON_PARSE_ERROR_INVALID_VALUE:\n        return \"invalid value\";\n\n    case BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER:\n        return \"premature end of buffer\";\n\n    case BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES:\n        return \"unexpected trailing bytes\";\n\n    case BSON_PARSE_ERROR_UNKNOWN:\n    default:\n        return \"unknown\";\n    }\n}\n"
  },
  {
    "path": "src/trx/core/bson/parse.h",
    "content": "#pragma once\n\n#include <trx/core/bson/types.h>\n\n// Parse a BSON file, returning a pointer to the root of the JSON structure.\n// Returns nullptr if an error occurred (malformed BSON input, or malloc\n// failed).\nJSON_VALUE *BSON_Parse(const char *src, size_t src_size);\n\nJSON_VALUE *BSON_ParseEx(\n    const char *src, size_t src_size, BSON_PARSE_RESULT *result);\n\nconst char *BSON_GetErrorDescription(BSON_PARSE_ERROR error);\n"
  },
  {
    "path": "src/trx/core/bson/types.h",
    "content": "#pragma once\n\n#include <trx/core/bson/enum.h>\n#include <trx/core/json/base.h>\n\ntypedef struct {\n    BSON_PARSE_ERROR error;\n    size_t error_offset;\n} BSON_PARSE_RESULT;\n"
  },
  {
    "path": "src/trx/core/bson/write.c",
    "content": "#include <trx/core/bson/write.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n\n#include <float.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nstatic bool M_GetValueWrappedSize(\n    size_t *size, const char *key, const JSON_VALUE *value);\nstatic char *M_WriteValueWrapped(\n    char *data, const char *key, const JSON_VALUE *value);\n\nstatic bool M_GetMarkerSize(size_t *size, const char *key)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    *size += 1; // marker\n    *size += strlen(key); // key\n    *size += 1; // nullptr terminator\n    return true;\n}\n\nstatic bool M_GetNullWrappedSize(size_t *size, const char *key)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    return M_GetMarkerSize(size, key);\n}\n\nstatic bool M_GetBoolWrappedSize(size_t *size, const char *key)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    if (!M_GetMarkerSize(size, key)) {\n        return false;\n    }\n    *size += 1;\n    return true;\n}\n\nstatic bool M_GetInt32Size(size_t *size)\n{\n    ASSERT(size != nullptr);\n    *size += sizeof(int32_t);\n    return true;\n}\n\nstatic bool M_GetInt32WrappedSize(size_t *size, const char *key)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    if (!M_GetMarkerSize(size, key)) {\n        return false;\n    }\n    if (!M_GetInt32Size(size)) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_GetDoubleSize(size_t *size)\n{\n    ASSERT(size != nullptr);\n    *size += sizeof(double);\n    return true;\n}\n\nstatic bool M_GetDoubleWrappedSize(size_t *size, const char *key)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    if (!M_GetMarkerSize(size, key)) {\n        return false;\n    }\n    if (!M_GetDoubleSize(size)) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_GetNumberWrappedSize(\n    size_t *size, const char *key, const JSON_NUMBER *number)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n\n    char *str = number->number;\n    ASSERT(str != nullptr);\n\n    // hexadecimal numbers\n    if (number->number_size >= 2 && (str[1] == 'x' || str[1] == 'X')) {\n        return M_GetInt32WrappedSize(size, key);\n    }\n\n    // skip leading sign\n    if (str[0] == '+' || str[0] == '-') {\n        str += 1;\n    }\n    ASSERT(str[0] != '\\0');\n\n    if (!strcmp(str, \"Infinity\")) {\n        // BSON does not support Infinity.\n        return M_GetDoubleWrappedSize(size, key);\n    } else if (!strcmp(str, \"NaN\")) {\n        // BSON does not support NaN.\n        return M_GetInt32WrappedSize(size, key);\n    } else if (strchr(str, '.')) {\n        return M_GetDoubleWrappedSize(size, key);\n    } else {\n        return M_GetInt32WrappedSize(size, key);\n    }\n\n    return false;\n}\n\nstatic bool M_GetStringSize(size_t *size, const JSON_STRING *string)\n{\n    ASSERT(size != nullptr);\n    ASSERT(string != nullptr);\n    *size += sizeof(uint32_t); // size\n    *size += string->string_size; // string\n    *size += 1; // nullptr terminator\n    return true;\n}\n\nstatic bool M_GetStringWrappedSize(\n    size_t *size, const char *key, const JSON_STRING *string)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(string != nullptr);\n    if (!M_GetMarkerSize(size, key)) {\n        return false;\n    }\n    if (!M_GetStringSize(size, string)) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_GetArraySize(size_t *size, const JSON_ARRAY *array)\n{\n    ASSERT(size != nullptr);\n    ASSERT(array != nullptr);\n    char key[12];\n    int idx = 0;\n    *size += sizeof(int32_t); // object size\n    for (JSON_ARRAY_ELEMENT *element = array->start; element != nullptr;\n         element = element->next) {\n        sprintf(key, \"%d\", idx);\n        idx++;\n        if (!M_GetValueWrappedSize(size, key, element->value)) {\n            return false;\n        }\n    }\n    *size += 1; // nullptr terminator\n    return true;\n}\n\nstatic bool M_GetArrayWrappedSize(\n    size_t *size, const char *key, const JSON_ARRAY *array)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(array != nullptr);\n    if (!M_GetMarkerSize(size, key)) {\n        return false;\n    }\n    if (!M_GetArraySize(size, array)) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_GetObjectSize(size_t *size, const JSON_OBJECT *object)\n{\n    ASSERT(size != nullptr);\n    ASSERT(object != nullptr);\n    *size += sizeof(int32_t); // object size\n    for (JSON_OBJECT_ELEMENT *element = object->start; element != nullptr;\n         element = element->next) {\n        if (!M_GetValueWrappedSize(\n                size, element->name->string, element->value)) {\n            return false;\n        }\n    }\n    *size += 1; // nullptr terminator\n    return true;\n}\n\nstatic bool M_GetObjectWrappedSize(\n    size_t *size, const char *key, const JSON_OBJECT *object)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(object != nullptr);\n    if (!M_GetMarkerSize(size, key)) {\n        return false;\n    }\n    if (!M_GetObjectSize(size, object)) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_GetValueSize(size_t *size, const JSON_VALUE *value)\n{\n    ASSERT(size != nullptr);\n    ASSERT(value != nullptr);\n    switch (value->type) {\n    case JSON_TYPE_ARRAY:\n        return M_GetArraySize(size, (JSON_ARRAY *)value->payload);\n    case JSON_TYPE_OBJECT:\n        return M_GetObjectSize(size, (JSON_OBJECT *)value->payload);\n    default:\n        LOG_ERROR(\"Bad BSON root element: %d\", value->type);\n    }\n    return false;\n}\n\nstatic bool M_GetValueWrappedSize(\n    size_t *size, const char *key, const JSON_VALUE *value)\n{\n    ASSERT(size != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(value != nullptr);\n    switch (value->type) {\n    case JSON_TYPE_NULL:\n        return M_GetNullWrappedSize(size, key);\n    case JSON_TYPE_TRUE:\n        return M_GetBoolWrappedSize(size, key);\n    case JSON_TYPE_FALSE:\n        return M_GetBoolWrappedSize(size, key);\n    case JSON_TYPE_NUMBER:\n        return M_GetNumberWrappedSize(size, key, (JSON_NUMBER *)value->payload);\n    case JSON_TYPE_STRING:\n        return M_GetStringWrappedSize(size, key, (JSON_STRING *)value->payload);\n    case JSON_TYPE_ARRAY:\n        return M_GetArrayWrappedSize(size, key, (JSON_ARRAY *)value->payload);\n    case JSON_TYPE_OBJECT:\n        return M_GetObjectWrappedSize(size, key, (JSON_OBJECT *)value->payload);\n    default:\n        LOG_ERROR(\"Unknown JSON element: %d\", value->type);\n        return false;\n    }\n}\n\nstatic char *M_WriteMarker(char *data, const char *key, const uint8_t marker)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    *data++ = marker;\n    strcpy(data, key);\n    data += strlen(key);\n    *data++ = '\\0';\n    return data;\n}\n\nstatic char *M_WriteNullWrapped(char *data, const char *key)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    return M_WriteMarker(data, key, '\\x0A');\n}\n\nstatic char *M_WriteBoolWrapped(char *data, const char *key, bool value)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    data = M_WriteMarker(data, key, '\\x08');\n    *(int8_t *)data++ = (int8_t)value;\n    return data;\n}\n\nstatic char *M_WriteInt32(char *data, const int32_t value)\n{\n    ASSERT(data != nullptr);\n    memcpy(data, &value, sizeof(value));\n    data += sizeof(int32_t);\n    return data;\n}\n\nstatic char *M_WriteInt32Wrapped(\n    char *data, const char *key, const int32_t value)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    data = M_WriteMarker(data, key, '\\x10');\n    return M_WriteInt32(data, value);\n}\n\nstatic char *M_WriteDouble(char *data, const double value)\n{\n    ASSERT(data != nullptr);\n    memcpy(data, &value, sizeof(value));\n    data += sizeof(double);\n    return data;\n}\n\nstatic char *M_WriteDoubleWrapped(\n    char *data, const char *key, const double value)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    data = M_WriteMarker(data, key, '\\x01');\n    return M_WriteDouble(data, value);\n}\n\nstatic char *M_WriteNumberWrapped(\n    char *data, const char *key, const JSON_NUMBER *number)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(number != nullptr);\n    char *str = number->number;\n\n    // hexadecimal numbers\n    if (number->number_size >= 2 && (str[1] == 'x' || str[1] == 'X')) {\n        return M_WriteInt32Wrapped(\n            data, key, json_strtoumax(number->number, nullptr, 0));\n    }\n\n    // skip leading sign\n    if (str[0] == '+' || str[0] == '-') {\n        str++;\n    }\n    ASSERT(str[0] != '\\0');\n\n    if (!strcmp(str, \"Infinity\")) {\n        // BSON does not support Infinity.\n        return M_WriteDoubleWrapped(data, key, DBL_MAX);\n    } else if (!strcmp(str, \"NaN\")) {\n        // BSON does not support NaN.\n        return M_WriteInt32Wrapped(data, key, 0);\n    } else if (strchr(str, '.')) {\n        return M_WriteDoubleWrapped(data, key, atof(number->number));\n    } else {\n        return M_WriteInt32Wrapped(data, key, atoi(number->number));\n    }\n\n    return data;\n}\n\nstatic char *M_WriteString(char *data, const JSON_STRING *string)\n{\n    ASSERT(data != nullptr);\n    ASSERT(string != nullptr);\n    const uint32_t bson_string_size = string->string_size + 1;\n    memcpy(data, &bson_string_size, sizeof(bson_string_size));\n    data += sizeof(uint32_t);\n    memcpy(data, string->string, string->string_size);\n    data += string->string_size;\n    *data++ = '\\0';\n    return data;\n}\n\nstatic char *M_WriteStringWrapped(\n    char *data, const char *key, const JSON_STRING *string)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(string != nullptr);\n    data = M_WriteMarker(data, key, '\\x02');\n    data = M_WriteString(data, string);\n    return data;\n}\n\nstatic char *M_WriteArray(char *data, const JSON_ARRAY *array)\n{\n    ASSERT(data != nullptr);\n    ASSERT(array != nullptr);\n    char key[12];\n    int idx = 0;\n    char *old = data;\n    data += sizeof(int32_t);\n    for (JSON_ARRAY_ELEMENT *element = array->start; element != nullptr;\n         element = element->next) {\n        sprintf(key, \"%d\", idx);\n        idx++;\n        data = M_WriteValueWrapped(data, key, element->value);\n    }\n    *data++ = '\\0';\n    const int32_t object_size = data - old;\n    memcpy(old, &object_size, sizeof(object_size));\n    return data;\n}\n\nstatic char *M_WriteArrayWrapped(\n    char *data, const char *key, const JSON_ARRAY *array)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(array != nullptr);\n    data = M_WriteMarker(data, key, '\\x04');\n    data = M_WriteArray(data, array);\n    return data;\n}\n\nstatic char *M_WriteObject(char *data, const JSON_OBJECT *object)\n{\n    ASSERT(data != nullptr);\n    ASSERT(object != nullptr);\n    char *old = data;\n    data += sizeof(int32_t);\n    for (JSON_OBJECT_ELEMENT *element = object->start; element != nullptr;\n         element = element->next) {\n        data = M_WriteValueWrapped(data, element->name->string, element->value);\n    }\n    *data++ = '\\0';\n    const int32_t object_size = data - old;\n    memcpy(old, &object_size, sizeof(object_size));\n    return data;\n}\n\nstatic char *M_WriteObjectWrapped(\n    char *data, const char *key, const JSON_OBJECT *object)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(object != nullptr);\n    data = M_WriteMarker(data, key, '\\x03');\n    data = M_WriteObject(data, object);\n    return data;\n}\n\nstatic char *M_WriteValue(char *data, const JSON_VALUE *value)\n{\n    ASSERT(data != nullptr);\n    ASSERT(value != nullptr);\n    switch (value->type) {\n    case JSON_TYPE_ARRAY:\n        data = M_WriteArray(data, (JSON_ARRAY *)value->payload);\n        break;\n    case JSON_TYPE_OBJECT:\n        data = M_WriteObject(data, (JSON_OBJECT *)value->payload);\n        break;\n    default:\n        ASSERT_FAIL();\n    }\n    return data;\n}\n\nstatic char *M_WriteValueWrapped(\n    char *data, const char *key, const JSON_VALUE *value)\n{\n    ASSERT(data != nullptr);\n    ASSERT(key != nullptr);\n    ASSERT(value != nullptr);\n    switch (value->type) {\n    case JSON_TYPE_NULL:\n        return M_WriteNullWrapped(data, key);\n    case JSON_TYPE_TRUE:\n        return M_WriteBoolWrapped(data, key, true);\n    case JSON_TYPE_FALSE:\n        return M_WriteBoolWrapped(data, key, false);\n    case JSON_TYPE_NUMBER:\n        return M_WriteNumberWrapped(data, key, (JSON_NUMBER *)value->payload);\n    case JSON_TYPE_STRING:\n        return M_WriteStringWrapped(data, key, (JSON_STRING *)value->payload);\n    case JSON_TYPE_ARRAY:\n        return M_WriteArrayWrapped(data, key, (JSON_ARRAY *)value->payload);\n    case JSON_TYPE_OBJECT:\n        return M_WriteObjectWrapped(data, key, (JSON_OBJECT *)value->payload);\n    default:\n        return nullptr;\n    }\n}\n\nvoid *BSON_Write(const JSON_VALUE *value, size_t *out_size)\n{\n    ASSERT(value != nullptr);\n    *out_size = -1;\n    if (value == nullptr) {\n        return nullptr;\n    }\n\n    size_t size = 0;\n    if (!M_GetValueSize(&size, value)) {\n        return nullptr;\n    }\n\n    char *data = Memory_Alloc(size);\n    char *data_end = M_WriteValue(data, value);\n    ASSERT((size_t)(data_end - data) == size);\n\n    if (out_size != nullptr) {\n        *out_size = size;\n    }\n\n    return data;\n}\n"
  },
  {
    "path": "src/trx/core/bson/write.h",
    "content": "#pragma once\n\n#include <trx/core/bson/types.h>\n\n/* Write out a BSON binary string. Return 0 if an error occurred (malformed\n * JSON input, or malloc failed). The out_size parameter is optional. */\nvoid *BSON_Write(const JSON_VALUE *value, size_t *out_size);\n"
  },
  {
    "path": "src/trx/core/bson.h",
    "content": "#pragma once\n\n#include <trx/core/bson/enum.h>\n#include <trx/core/bson/parse.h>\n#include <trx/core/bson/types.h>\n#include <trx/core/bson/write.h>\n"
  },
  {
    "path": "src/trx/core/colors.c",
    "content": "#include <trx/core/colors.h>\n\n#include <trx/core/utils.h>\n\n#include <math.h>\n\n#ifndef M_PI\n    #define M_PI 3.14159265358979323846\n#endif\n\nRGBA_8888 Color_RGB888ToRGBA8888_Impl(const RGB_888 color)\n{\n    return Color_RGB888ToRGBA8888Ex_Impl(color, 255);\n}\n\nRGBA_8888 Color_RGB888ToRGBA8888Ex_Impl(\n    const RGB_888 color, const uint8_t alpha)\n{\n    return (RGBA_8888) { color.r, color.g, color.b, alpha };\n}\n\nRGBA_F Color_RGBFToRGBAF_Impl(const RGB_F color)\n{\n    return Color_RGBFToRGBAFEx_Impl(color, 1.0f);\n}\n\nRGBA_F Color_RGBFToRGBAFEx_Impl(const RGB_F color, const float alpha)\n{\n    return (RGBA_F) { color.r, color.g, color.b, alpha };\n}\n\nRGBA_8888 Color_ARGB1555ToRGBA8888(const uint16_t argb1555)\n{\n    // Extract 5-bit values for each ARGB component.\n    uint8_t a1 = (argb1555 >> 15) & 0x01;\n    uint8_t r5 = (argb1555 >> 10) & 0x1F;\n    uint8_t g5 = (argb1555 >> 5) & 0x1F;\n    uint8_t b5 = argb1555 & 0x1F;\n\n    // Expand 5-bit color components to 8-bit.\n    uint8_t a8 = a1 * 255; // 1-bit alpha (either 0 or 255)\n    uint8_t r8 = (r5 << 3) | (r5 >> 2);\n    uint8_t g8 = (g5 << 3) | (g5 >> 2);\n    uint8_t b8 = (b5 << 3) | (b5 >> 2);\n\n    return (RGBA_8888) {\n        .r = r8,\n        .g = g8,\n        .b = b8,\n        .a = a8,\n    };\n}\n\nRGB_F Color_MixRGBF_Impl(\n    const RGB_F color_1, const RGB_F color_2, const float ratio)\n{\n    return (RGB_F) {\n        .r = color_1.r + (color_2.r - color_1.r) * ratio,\n        .g = color_1.g + (color_2.g - color_1.g) * ratio,\n        .b = color_1.b + (color_2.b - color_1.b) * ratio,\n    };\n}\n\nRGBA_F Color_MixRGBAF_Impl(\n    const RGBA_F color_1, const RGBA_F color_2, const float ratio)\n{\n    return (RGBA_F) {\n        .r = color_1.r + (color_2.r - color_1.r) * ratio,\n        .g = color_1.g + (color_2.g - color_1.g) * ratio,\n        .b = color_1.b + (color_2.b - color_1.b) * ratio,\n        .a = color_1.a + (color_2.a - color_1.a) * ratio,\n    };\n}\n\nRGB_888 Color_MixRGB888_Impl(\n    const RGB_888 color_1, const RGB_888 color_2, const float ratio)\n{\n    return (RGB_888) {\n        .r = (uint8_t)(color_1.r + (color_2.r - color_1.r) * ratio),\n        .g = (uint8_t)(color_1.g + (color_2.g - color_1.g) * ratio),\n        .b = (uint8_t)(color_1.b + (color_2.b - color_1.b) * ratio),\n    };\n}\n\nRGBA_8888 Color_MixRGBA8888_Impl(\n    const RGBA_8888 color_1, const RGBA_8888 color_2, const float ratio)\n{\n    return (RGBA_8888) {\n        .r = (uint8_t)(color_1.r + (color_2.r - color_1.r) * ratio),\n        .g = (uint8_t)(color_1.g + (color_2.g - color_1.g) * ratio),\n        .b = (uint8_t)(color_1.b + (color_2.b - color_1.b) * ratio),\n        .a = (uint8_t)(color_1.a + (color_2.a - color_1.a) * ratio),\n    };\n}\n\nRGB_888 Color_HSLToRGB(const float h, const float s, const float l)\n{\n    float hue = h < 0.0f ? 0.0f : fmodf(h, 360.0f);\n    float sat = s;\n    float light = l;\n    CLAMP(hue, 0.0f, 360.0f);\n    CLAMP(sat, 0.0f, 1.0f);\n    CLAMP(light, 0.0f, 1.0f);\n\n    const float c = (1.0f - fabsf(2.0f * light - 1.0f)) * sat;\n    const float x = c * (1.0f - fabsf(fmodf(hue / 60.0f, 2.0f) - 1.0f));\n    const float m = light - c / 2.0f;\n\n    float rp = 0.0f;\n    float gp = 0.0f;\n    float bp = 0.0f;\n\n    if (hue < 60.0f) {\n        rp = c;\n        gp = x;\n    } else if (hue < 120.0f) {\n        rp = x;\n        gp = c;\n    } else if (hue < 180.0f) {\n        gp = c;\n        bp = x;\n    } else if (hue < 240.0f) {\n        gp = x;\n        bp = c;\n    } else if (hue < 300.0f) {\n        rp = x;\n        bp = c;\n    } else {\n        rp = c;\n        bp = x;\n    }\n\n    return (RGB_888) {\n        .r = (uint8_t)roundf((rp + m) * 255.0f),\n        .g = (uint8_t)roundf((gp + m) * 255.0f),\n        .b = (uint8_t)roundf((bp + m) * 255.0f),\n    };\n}\n\nvoid Color_RGBToHSL(\n    const RGB_888 color, float *const out_h, float *const out_s,\n    float *const out_l)\n{\n    const float r = color.r / 255.0f;\n    const float g = color.g / 255.0f;\n    const float b = color.b / 255.0f;\n    const float max_c = MAX(r, MAX(g, b));\n    const float min_c = MIN(r, MIN(g, b));\n    const float delta = max_c - min_c;\n    float light = (max_c + min_c) / 2.0f;\n\n    float hue = 0.0f;\n    float sat = 0.0f;\n    if (delta > 0.0f) {\n        if (max_c == r) {\n            hue = 60.0f * fmodf((g - b) / delta, 6.0f);\n        } else if (max_c == g) {\n            hue = 60.0f * (((b - r) / delta) + 2.0f);\n        } else {\n            hue = 60.0f * (((r - g) / delta) + 4.0f);\n        }\n        if (hue < 0.0f) {\n            hue += 360.0f;\n        }\n        sat = delta / (1.0f - fabsf(2.0f * light - 1.0f));\n    }\n\n    CLAMP(hue, 0.0f, 360.0f);\n    CLAMP(sat, 0.0f, 1.0f);\n    CLAMP(light, 0.0f, 1.0f);\n    *out_h = hue;\n    *out_s = sat;\n    *out_l = light;\n}\n\nstatic float M_SRGBToLinear(const float c)\n{\n    if (c <= 0.04045f) {\n        return c / 12.92f;\n    }\n    return powf((c + 0.055f) / 1.055f, 2.4f);\n}\n\nstatic float M_LinearToSRGB(const float c)\n{\n    if (c <= 0.0031308f) {\n        return c * 12.92f;\n    }\n    return 1.055f * powf(c, 1.0f / 2.4f) - 0.055f;\n}\n\nRGB_888 Color_OKLCHToRGB(const float l, const float c, const float h)\n{\n    float lightness = l;\n    float chroma = c;\n    float hue = h < 0.0f ? 0.0f : fmodf(h, 360.0f);\n    CLAMP(lightness, 0.0f, 1.0f);\n    CLAMP(chroma, 0.0f, 1.0f);\n    CLAMP(hue, 0.0f, 360.0f);\n\n    const float hue_rad = hue * M_PI / 180.0f;\n    const float a = chroma * cosf(hue_rad);\n    const float b = chroma * sinf(hue_rad);\n\n    const float l_ = lightness + 0.3963377774f * a + 0.2158037573f * b;\n    const float m_ = lightness - 0.1055613458f * a - 0.0638541728f * b;\n    const float s_ = lightness - 0.0894841775f * a - 1.2914855480f * b;\n\n    const float l_3 = l_ * l_ * l_;\n    const float m_3 = m_ * m_ * m_;\n    const float s_3 = s_ * s_ * s_;\n\n    float r_linear =\n        +4.0767416621f * l_3 - 3.3077115913f * m_3 + 0.2309699292f * s_3;\n    float g_linear =\n        -1.2684380046f * l_3 + 2.6097574011f * m_3 - 0.3413193965f * s_3;\n    float b_linear =\n        -0.0041960863f * l_3 - 0.7034186147f * m_3 + 1.7076147010f * s_3;\n    CLAMP(r_linear, 0.0f, 1.0f);\n    CLAMP(g_linear, 0.0f, 1.0f);\n    CLAMP(b_linear, 0.0f, 1.0f);\n\n    const float r_srgb = M_LinearToSRGB(r_linear);\n    const float g_srgb = M_LinearToSRGB(g_linear);\n    const float b_srgb = M_LinearToSRGB(b_linear);\n\n    return (RGB_888) {\n        .r = (uint8_t)roundf(r_srgb * 255.0f),\n        .g = (uint8_t)roundf(g_srgb * 255.0f),\n        .b = (uint8_t)roundf(b_srgb * 255.0f),\n    };\n}\n\nvoid Color_RGBToOKLCH(\n    const RGB_888 color, float *const out_l, float *const out_c,\n    float *const out_h)\n{\n    const float r = M_SRGBToLinear(color.r / 255.0f);\n    const float g = M_SRGBToLinear(color.g / 255.0f);\n    const float b = M_SRGBToLinear(color.b / 255.0f);\n\n    const float l =\n        cbrtf(0.4122214708f * r + 0.5363325363f * g + 0.0514459929f * b);\n    const float m =\n        cbrtf(0.2119034982f * r + 0.6806995451f * g + 0.1073969566f * b);\n    const float s =\n        cbrtf(0.0883024619f * r + 0.2817188376f * g + 0.6299787005f * b);\n\n    float lightness = 0.2104542553f * l + 0.7936177850f * m - 0.0040720468f * s;\n    const float a = 1.9779984951f * l - 2.4285922050f * m + 0.4505937099f * s;\n    const float b_comp =\n        0.0259040371f * l + 0.7827717662f * m - 0.8086757660f * s;\n\n    const float chroma = sqrtf(a * a + b_comp * b_comp);\n    float hue = atan2f(b_comp, a) * 180.0f / M_PI;\n    if (hue < 0.0f) {\n        hue += 360.0f;\n    }\n\n    CLAMP(lightness, 0.0f, 1.0f);\n    *out_l = lightness;\n    *out_c = chroma;\n    *out_h = hue;\n}\n"
  },
  {
    "path": "src/trx/core/colors.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct {\n    float r, g, b;\n} RGB_F;\n\ntypedef struct {\n    float r, g, b, a;\n} RGBA_F;\n\ntypedef struct {\n    uint8_t r, g, b;\n} RGB_888;\n\ntypedef struct {\n    uint8_t r, g, b, a;\n} RGBA_8888;\n\n#define COLOR_RGBA_8888_BLACK ((RGBA_8888) { 0x00, 0x00, 0x00, 0xFF })\n#define COLOR_RGBA_8888_WHITE ((RGBA_8888) { 0xFF, 0xFF, 0xFF, 0xFF })\n#define COLOR_RGB_888_BLACK ((RGB_888) { 0x00, 0x00, 0x00 })\n#define COLOR_RGB_888_WHITE ((RGB_888) { 0xFF, 0xFF, 0xFF })\n#define COLOR_RGB_F_WHITE ((RGB_F) { 1.0f, 1.0f, 1.0f })\n\nRGBA_8888 Color_RGB888ToRGBA8888_Impl(RGB_888 color);\nRGBA_8888 Color_RGB888ToRGBA8888Ex_Impl(RGB_888 color, uint8_t alpha);\nRGBA_F Color_RGBFToRGBAF_Impl(RGB_F color);\nRGBA_F Color_RGBFToRGBAFEx_Impl(RGB_F color, float alpha);\nRGBA_8888 Color_ARGB1555ToRGBA8888(uint16_t argb1555);\nRGB_F Color_MixRGBF_Impl(RGB_F color_1, RGB_F color_2, float ratio);\nRGBA_F Color_MixRGBAF_Impl(RGBA_F color_1, RGBA_F color_2, float ratio);\nRGB_888 Color_MixRGB888_Impl(RGB_888 color_1, RGB_888 color_2, float ratio);\nRGBA_8888 Color_MixRGBA8888_Impl(\n    RGBA_8888 color_1, RGBA_8888 color_2, float ratio);\n\n#define Color_RGBToRGBA(color)                                                 \\\n    _Generic(                                                                  \\\n        (color),                                                               \\\n        RGB_888: Color_RGB888ToRGBA8888_Impl,                                  \\\n        RGB_F: Color_RGBFToRGBAF_Impl)(color)\n\n#define Color_RGBToRGBAEx(color, alpha)                                        \\\n    _Generic(                                                                  \\\n        (color),                                                               \\\n        RGB_888: Color_RGB888ToRGBA8888Ex_Impl,                                \\\n        RGB_F: Color_RGBFToRGBAFEx_Impl)(color, alpha)\n\n#define Color_Mix(color_1, color_2, ratio)                                     \\\n    _Generic(                                                                  \\\n        (color_1),                                                             \\\n        RGB_F: Color_MixRGBF_Impl,                                             \\\n        RGBA_F: Color_MixRGBAF_Impl,                                           \\\n        RGB_888: Color_MixRGB888_Impl,                                         \\\n        RGBA_8888: Color_MixRGBA8888_Impl)(color_1, color_2, ratio)\n\nRGB_888 Color_HSLToRGB(float h, float s, float l);\nvoid Color_RGBToHSL(RGB_888 color, float *out_h, float *out_s, float *out_l);\nRGB_888 Color_OKLCHToRGB(float l, float c, float h);\nvoid Color_RGBToOKLCH(RGB_888 color, float *out_l, float *out_c, float *out_h);\n"
  },
  {
    "path": "src/trx/core/enum_map.c",
    "content": "#include <trx/core/enum_map.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <uthash.h>\n\ntypedef struct {\n    char *key;\n    int32_t value;\n    UT_hash_handle hh;\n} M_STR_TO_ID_ENTRY;\n\ntypedef struct {\n    char *key;\n    char *str_value;\n    UT_hash_handle hh;\n} M_ID_TO_STR_ENTRY;\n\nstatic M_STR_TO_ID_ENTRY *m_Str2IdMap = nullptr;\nstatic M_ID_TO_STR_ENTRY *m_Id2StrMap = nullptr;\nstatic M_ID_TO_STR_ENTRY *m_Id2NameMap = nullptr;\nstatic M_ID_TO_STR_ENTRY *m_Id2LabelKeyMap = nullptr;\n\nstatic void M_ClearStr2IdMap(M_STR_TO_ID_ENTRY **map)\n{\n    M_STR_TO_ID_ENTRY *current, *tmp;\n    HASH_ITER(hh, *map, current, tmp)\n    {\n        HASH_DEL(*map, current);\n        Memory_Free(current->key);\n        Memory_Free(current);\n    }\n}\n\nstatic void M_ClearId2StrMap(M_ID_TO_STR_ENTRY **map)\n{\n    M_ID_TO_STR_ENTRY *current, *tmp;\n    HASH_ITER(hh, *map, current, tmp)\n    {\n        HASH_DEL(*map, current);\n        Memory_Free(current->str_value);\n        Memory_Free(current->key);\n        Memory_Free(current);\n    }\n}\n\nstatic __attribute__((destructor)) void M_Shutdown(void)\n{\n    M_ClearStr2IdMap(&m_Str2IdMap);\n    M_ClearId2StrMap(&m_Id2StrMap);\n    M_ClearId2StrMap(&m_Id2NameMap);\n    M_ClearId2StrMap(&m_Id2LabelKeyMap);\n}\n\nstatic void M_DefineStr2Id(\n    M_STR_TO_ID_ENTRY **map, const char *const enum_type_name,\n    const int32_t enum_value, const char *const str_value)\n{\n    const char *const key =\n        String_FormatStatic(\"%s|%s\", enum_type_name, str_value);\n    M_STR_TO_ID_ENTRY *const entry = Memory_Alloc(sizeof(M_STR_TO_ID_ENTRY));\n    entry->key = Memory_DupStr(key);\n    entry->value = enum_value;\n    HASH_ADD_KEYPTR(hh, *map, entry->key, strlen(entry->key), entry);\n}\n\nstatic void M_DefineId2Str(\n    M_ID_TO_STR_ENTRY **map, const char *const enum_type_name,\n    const int32_t enum_value, const char *const str_value)\n{\n    const char *const key =\n        String_FormatStatic(\"%s|%d\", enum_type_name, enum_value);\n    M_ID_TO_STR_ENTRY *entry;\n    HASH_FIND_STR(*map, key, entry);\n    if (entry != nullptr) {\n        // The inverse lookup is already defined - do not override it.\n        // (This means that the first call to ENUM_MAP for a given enum value\n        // also determines what serializing it back to string will pick\n        // in the event there are multiple aliases).\n        return;\n    }\n\n    entry = Memory_Alloc(sizeof(M_ID_TO_STR_ENTRY));\n    entry->key = Memory_DupStr(key);\n    entry->str_value = Memory_DupStr(str_value);\n    HASH_ADD_KEYPTR(hh, *map, entry->key, strlen(entry->key), entry);\n}\n\nstatic int32_t M_Str2Id(\n    M_STR_TO_ID_ENTRY *const *map, const char *const enum_type_name,\n    const char *const str_value, int32_t default_value)\n{\n    const char *const key =\n        String_FormatStatic(\"%s|%s\", enum_type_name, str_value);\n    M_STR_TO_ID_ENTRY *entry;\n    HASH_FIND_STR(*map, key, entry);\n    return entry != nullptr ? entry->value : default_value;\n}\n\nstatic const char *M_Id2Str(\n    M_ID_TO_STR_ENTRY *const *map, const char *const enum_type_name,\n    const int32_t enum_value)\n{\n    const char *const key =\n        String_FormatStatic(\"%s|%d\", enum_type_name, enum_value);\n    M_ID_TO_STR_ENTRY *entry;\n    HASH_FIND_STR(*map, key, entry);\n    return entry != nullptr ? entry->str_value : nullptr;\n}\n\nvoid EnumMap_Define(\n    const char *const enum_type_name, const char *const enum_name,\n    const char *const label_key, const int32_t enum_value,\n    const char *const str_value)\n{\n    M_DefineStr2Id(&m_Str2IdMap, enum_type_name, enum_value, str_value);\n    M_DefineId2Str(&m_Id2StrMap, enum_type_name, enum_value, str_value);\n    M_DefineId2Str(&m_Id2NameMap, enum_type_name, enum_value, enum_name);\n    M_DefineId2Str(&m_Id2LabelKeyMap, enum_type_name, enum_value, label_key);\n}\n\nint32_t EnumMap_Get(\n    const char *const enum_type_name, const char *const str_value,\n    int32_t default_value)\n{\n    return M_Str2Id(&m_Str2IdMap, enum_type_name, str_value, default_value);\n}\n\nconst char *EnumMap_ToString(\n    const char *const enum_type_name, const int32_t enum_value)\n{\n    return M_Id2Str(&m_Id2StrMap, enum_type_name, enum_value);\n}\n\nconst char *EnumMap_GetName(\n    const char *const enum_type_name, const int32_t enum_value)\n{\n    return M_Id2Str(&m_Id2NameMap, enum_type_name, enum_value);\n}\n\nconst char *EnumMap_GetLabel(\n    const char *const enum_type_name, const int32_t enum_value)\n{\n    const char *const key =\n        M_Id2Str(&m_Id2LabelKeyMap, enum_type_name, enum_value);\n    if (key == nullptr) {\n        return nullptr;\n    }\n    return GameString_Get(key);\n}\n\nVECTOR *EnumMap_ListValues(const char *const enum_type_name)\n{\n    if (enum_type_name == nullptr) {\n        return nullptr;\n    }\n\n    // Compare the prefix to find the matching enum values.\n    const size_t prefix_len = strlen(enum_type_name) + 1;\n\n    VECTOR *const results = Vector_Create(sizeof(char *));\n    M_ID_TO_STR_ENTRY *entry;\n    M_ID_TO_STR_ENTRY *tmp;\n    HASH_ITER(hh, m_Id2StrMap, entry, tmp)\n    {\n        if (strncmp(entry->key, enum_type_name, prefix_len - 1) == 0\n            && entry->key[prefix_len - 1] == '|') {\n            Vector_Add(results, &entry->str_value);\n        }\n    }\n    return results;\n}\n"
  },
  {
    "path": "src/trx/core/enum_map.h",
    "content": "#include <trx/core/vector.h>\n\n#define ENUM_MAP(enum_type_name, enum_value, str_value)                        \\\n    EnumMap_Define(                                                            \\\n        ENUM_MAP_NAME(enum_type_name), #enum_value,                            \\\n        ENUM_MAP_LABEL_KEY(enum_type_name, enum_value), enum_value, str_value)\n\n#define ENUM_MAP_SELF(enum_type_name, enum_value)                              \\\n    ENUM_MAP(enum_type_name, enum_value, #enum_value)\n\n#define ENUM_MAP_GET(enum_type_name, str_value, default_value)                 \\\n    EnumMap_Get(ENUM_MAP_NAME(enum_type_name), str_value, default_value)\n\n#define ENUM_MAP_TO_STRING(enum_type_name, enum_value)                         \\\n    EnumMap_ToString(ENUM_MAP_NAME(enum_type_name), enum_value)\n\n#define ENUM_MAP_NAME(enum_type_name) #enum_type_name\n#define ENUM_MAP_LABEL_KEY(enum_type_name, enum_value)                         \\\n    \"enums/\" #enum_type_name \"/\" #enum_value\n\n// Associate an integer enum value, such as WEATHER_SNOW, with a string\n// representation such as \"snow\".\n// @param enum_type_name    Name of the enum type, such as \"WEATHER\".\n// @param enum_name         Name of the enum, such as \"WEATHER_SNOW\".\n// @param enum_value        Value of the enum, such as 1.\n// @param str_value         String representation of the enum, such as \"snow\".\nvoid EnumMap_Define(\n    const char *enum_type_name, const char *enum_name, const char *label_key,\n    int32_t enum_value, const char *str_value);\n\n// Retrieve an integer enum value from a string representation.\n// @param enum_type_name    Name of the enum type, such as \"WEATHER\".\n// @param str_value         String representation of the enum, such as \"snow\".\n// @param default_value     Value to return in case the mapping fails.\n// @return                  Value of the enum, such as 1.\nint32_t EnumMap_Get(\n    const char *enum_type_name, const char *str_value, int32_t default_value);\n\n// Retrieve an enum integer canonical name as a string.\n// @param enum_type_name    Name of the enum type, such as \"WEATHER\".\n// @param enum_value        Value of the enum, such as 1.\n// @return                  Name of the enum, such as \"WEATHER_SNOW\".\nconst char *EnumMap_GetName(const char *enum_type_name, int32_t enum_value);\n\n// Retrieve a string representation, such as \"snow\", based on an integer enum\n// value such as WEATHER_SNOW.\n// @param enum_type_name    Name of the enum type, such as \"WEATHER\".\n// @param enum_value        Value of the enum, such as 1.\n// @return                  String representation of the enum, such as \"snow\".\nconst char *EnumMap_ToString(const char *enum_type_name, int32_t enum_value);\n\n// Retrieve a localized label for the given enum value.\n// @param enum_type_name    Name of the enum type, such as \"WEATHER\".\n// @param enum_value        Value of the enum, such as 1.\n// @return                  Localized label or nullptr if missing.\nconst char *EnumMap_GetLabel(const char *enum_type_name, int32_t enum_value);\n\n// Returns a vector of valid string values for the given enum_type_name.\n//\n// The returned vector must be freed via Vector_Free(). The string pointers\n// within the vector are owned by the enum map and should not be freed by the\n// caller. Returns nullptr if the enum_type_name is not valid.\nVECTOR *EnumMap_ListValues(const char *enum_type_name);\n"
  },
  {
    "path": "src/trx/core/event_manager.c",
    "content": "#include <trx/core/event_manager.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n\n#include <stdlib.h>\n#include <string.h>\n\ntypedef struct {\n    int32_t listener_id;\n    const char *event_name;\n    const void *sender;\n    EVENT_LISTENER listener;\n    void *user_data;\n} M_LISTENER;\n\ntypedef struct EVENT_MANAGER {\n    VECTOR *listeners;\n    int32_t listener_id;\n} EVENT_MANAGER;\n\nEVENT_MANAGER *EventManager_Create(void)\n{\n    EVENT_MANAGER *manager = Memory_Alloc(sizeof(EVENT_MANAGER));\n    manager->listeners = Vector_Create(sizeof(M_LISTENER));\n    manager->listener_id = 0;\n    return manager;\n}\n\nvoid EventManager_Free(EVENT_MANAGER *const manager)\n{\n    if (manager == nullptr) {\n        return;\n    }\n    Vector_Free(manager->listeners);\n    Memory_Free(manager);\n}\n\nint32_t EventManager_Subscribe(\n    EVENT_MANAGER *const manager, const char *const event_name,\n    const void *const sender, const EVENT_LISTENER listener,\n    void *const user_data)\n{\n    M_LISTENER entry = {\n        .listener_id = manager->listener_id++,\n        .event_name = event_name,\n        .sender = sender,\n        .listener = listener,\n        .user_data = user_data,\n    };\n    Vector_Add(manager->listeners, &entry);\n    return entry.listener_id;\n}\n\nvoid EventManager_Unsubscribe(\n    EVENT_MANAGER *const manager, const int32_t listener_id)\n{\n    for (int32_t i = 0; i < manager->listeners->count; i++) {\n        M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i);\n        if (entry.listener_id == listener_id) {\n            Vector_RemoveAt(manager->listeners, i);\n            return;\n        }\n    }\n}\n\nvoid EventManager_Fire(EVENT_MANAGER *const manager, const EVENT *const event)\n{\n    for (int32_t i = 0; i < manager->listeners->count; i++) {\n        M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i);\n        if (strcmp(entry.event_name, event->name) == 0\n            && entry.sender == event->sender) {\n            entry.listener(event, entry.user_data);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/core/event_manager.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct {\n    const char *name;\n    const void *sender;\n    const void *data;\n} EVENT;\n\ntypedef void (*EVENT_LISTENER)(const EVENT *, void *user_data);\n\ntypedef struct EVENT_MANAGER EVENT_MANAGER;\n\nEVENT_MANAGER *EventManager_Create(void);\nvoid EventManager_Free(EVENT_MANAGER *manager);\n\nint32_t EventManager_Subscribe(\n    EVENT_MANAGER *manager, const char *event_name, const void *sender,\n    EVENT_LISTENER listener, void *user_data);\n\nvoid EventManager_Unsubscribe(EVENT_MANAGER *manager, int32_t listener_id);\n\nvoid EventManager_Fire(EVENT_MANAGER *manager, const EVENT *event);\n"
  },
  {
    "path": "src/trx/core/filesystem.c",
    "content": "#include <trx/core/filesystem.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n\n#include <dirent.h>\n#include <stdio.h>\n#include <string.h>\n\n#if defined(_WIN32)\n    #include <direct.h>\n    #include <io.h>\n    #include <sys/stat.h>\n    #define PATH_SEPARATOR \"\\\\\"\n#else\n    #include <sys/stat.h>\n    #include <unistd.h>\n    #define PATH_SEPARATOR \"/\"\n#endif\n\nstruct MYFILE {\n    FILE *fp;\n    const char *path;\n};\n\n#if defined(_WIN32)\n    #include <wchar.h>\n    #include <stdlib.h>\n    #include <string.h>\n    #include <windows.h>\n\nstatic wchar_t *M_UTF8ToWide(const char *const utf8_str)\n{\n    if (utf8_str == nullptr) {\n        return nullptr;\n    }\n    const size_t len = strlen(utf8_str);\n    const size_t wide_len =\n        MultiByteToWideChar(CP_UTF8, 0, utf8_str, len, nullptr, 0);\n    wchar_t *wide_str = Memory_Alloc((wide_len + 1) * sizeof(wchar_t));\n    MultiByteToWideChar(CP_UTF8, 0, utf8_str, len, wide_str, wide_len);\n    wide_str[wide_len] = L'\\0';\n    return wide_str;\n}\n\nstatic FILE *M_UTF8Fopen(const char *path, const char *mode)\n{\n    if (path == nullptr || mode == nullptr) {\n        return nullptr;\n    }\n    wchar_t *const wide_path = M_UTF8ToWide(path);\n    wchar_t *const wide_mode = M_UTF8ToWide(mode);\n    FILE *const file = _wfopen(wide_path, wide_mode);\n    Memory_Free(wide_path);\n    Memory_Free(wide_mode);\n    return file;\n}\n\n#else\nstatic FILE *M_UTF8Fopen(const char *path, const char *mode)\n{\n    if (path == nullptr || mode == nullptr) {\n        return nullptr;\n    }\n    return fopen(path, mode);\n}\n#endif\n\nstatic bool M_ExistsRaw(const char *path)\n{\n    if (path == nullptr) {\n        return false;\n    }\n    FILE *fp = M_UTF8Fopen(path, \"rb\");\n    if (fp) {\n        fclose(fp);\n        return true;\n    }\n    return false;\n}\n\nbool File_IsAbsolute(const char *path)\n{\n    return path && (path[0] == '/' || strstr(path, \":\\\\\"));\n}\n\nbool File_IsRelative(const char *path)\n{\n    return path && !File_IsAbsolute(path);\n}\n\nbool File_DirExists(const char *path)\n{\n    if (path == nullptr) {\n        return false;\n    }\n    DIR *dir = opendir(path);\n    if (dir != nullptr) {\n        closedir(dir);\n        return true;\n    }\n    return false;\n}\n\nbool File_Exists(const char *path)\n{\n    if (path == nullptr) {\n        return false;\n    }\n    return M_ExistsRaw(path);\n}\n\nchar *File_GetParentDirectory(const char *path)\n{\n    if (path == nullptr) {\n        return nullptr;\n    }\n\n    char *last_delim = MAX(strrchr(path, '/'), strrchr(path, '\\\\'));\n    if (last_delim != nullptr) {\n        return String_Format(\"%.*s\", last_delim - path, path);\n    }\n\n    return nullptr;\n}\n\nMYFILE *File_Open(const char *path, FILE_OPEN_MODE mode)\n{\n    MYFILE *file = Memory_Alloc(sizeof(MYFILE));\n    file->path = Memory_DupStr(path);\n    switch (mode) {\n    case FILE_OPEN_WRITE:\n        file->fp = M_UTF8Fopen(path, \"wb\");\n        break;\n    case FILE_OPEN_READ:\n        file->fp = M_UTF8Fopen(path, \"rb\");\n        break;\n    case FILE_OPEN_READ_WRITE:\n        file->fp = M_UTF8Fopen(path, \"r+b\");\n        break;\n    default:\n        file->fp = nullptr;\n        break;\n    }\n    if (file->fp == nullptr) {\n        Memory_FreePointer(&file->path);\n        Memory_FreePointer(&file);\n    }\n    return file;\n}\n\nbool File_ReadData(MYFILE *const file, void *const data, const size_t size)\n{\n    return fread(data, size, 1, file->fp) == 1;\n}\n\nbool File_ReadItems(\n    MYFILE *const file, void *data, const size_t count, const size_t item_size)\n{\n    return fread(data, item_size, count, file->fp) == count;\n}\n\nint8_t File_ReadS8(MYFILE *const file)\n{\n    int8_t result;\n    File_ReadData(file, &result, sizeof(result));\n    return result;\n}\n\nint16_t File_ReadS16(MYFILE *const file)\n{\n    int16_t result;\n    File_ReadData(file, &result, sizeof(result));\n    return result;\n}\n\nint32_t File_ReadS32(MYFILE *const file)\n{\n    int32_t result;\n    File_ReadData(file, &result, sizeof(result));\n    return result;\n}\n\nuint8_t File_ReadU8(MYFILE *const file)\n{\n    uint8_t result;\n    File_ReadData(file, &result, sizeof(result));\n    return result;\n}\n\nuint16_t File_ReadU16(MYFILE *const file)\n{\n    uint16_t result;\n    File_ReadData(file, &result, sizeof(result));\n    return result;\n}\n\nuint32_t File_ReadU32(MYFILE *const file)\n{\n    uint32_t result;\n    File_ReadData(file, &result, sizeof(result));\n    return result;\n}\n\nvoid File_WriteData(\n    MYFILE *const file, const void *const data, const size_t size)\n{\n    fwrite(data, size, 1, file->fp);\n}\n\nvoid File_WriteItems(\n    MYFILE *const file, const void *const data, const size_t count,\n    const size_t item_size)\n{\n    fwrite(data, item_size, count, file->fp);\n}\n\nvoid File_WriteS8(MYFILE *const file, const int8_t value)\n{\n    fwrite(&value, sizeof(value), 1, file->fp);\n}\n\nvoid File_WriteS16(MYFILE *const file, const int16_t value)\n{\n    fwrite(&value, sizeof(value), 1, file->fp);\n}\n\nvoid File_WriteS32(MYFILE *const file, const int32_t value)\n{\n    fwrite(&value, sizeof(value), 1, file->fp);\n}\n\nvoid File_WriteU8(MYFILE *const file, const uint8_t value)\n{\n    fwrite(&value, sizeof(value), 1, file->fp);\n}\n\nvoid File_WriteU16(MYFILE *const file, const uint16_t value)\n{\n    fwrite(&value, sizeof(value), 1, file->fp);\n}\n\nvoid File_WriteU32(MYFILE *const file, const uint32_t value)\n{\n    fwrite(&value, sizeof(value), 1, file->fp);\n}\n\nvoid File_WriteString(MYFILE *file, const char *fmt, ...)\n{\n    if (file == nullptr || file->fp == nullptr) {\n        return;\n    }\n    va_list args;\n    va_start(args, fmt);\n    const char *s = String_FormatStaticV(fmt, args);\n    va_end(args);\n    fputs(s, file->fp);\n}\n\nvoid File_Skip(MYFILE *file, size_t bytes)\n{\n    File_Seek(file, bytes, FILE_SEEK_CUR);\n}\n\nvoid File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode)\n{\n    switch (mode) {\n    case FILE_SEEK_SET:\n        fseek(file->fp, pos, SEEK_SET);\n        break;\n    case FILE_SEEK_CUR:\n        fseek(file->fp, pos, SEEK_CUR);\n        break;\n    case FILE_SEEK_END:\n        fseek(file->fp, pos, SEEK_END);\n        break;\n    }\n}\n\nsize_t File_Pos(MYFILE *file)\n{\n    return ftell(file->fp);\n}\n\nsize_t File_Size(MYFILE *file)\n{\n    size_t old = ftell(file->fp);\n    fseek(file->fp, 0, SEEK_END);\n    size_t size = ftell(file->fp);\n    fseek(file->fp, old, SEEK_SET);\n    return size;\n}\n\nconst char *File_GetPath(MYFILE *file)\n{\n    return file->path;\n}\n\nbool File_GetMeta(\n    const char *const path, uint64_t *const out_size, uint64_t *const out_mtime)\n{\n    MYFILE *const file = File_Open(path, FILE_OPEN_READ);\n    if (file == nullptr) {\n        return false;\n    }\n\n    if (out_size != nullptr) {\n        *out_size = (uint64_t)File_Size(file);\n    }\n\n    if (out_mtime != nullptr) {\n        uint64_t mtime = 0;\n#if defined(_WIN32)\n        struct _stat64 st;\n        if (_fstat64(_fileno(file->fp), &st) == 0) {\n            mtime = (uint64_t)st.st_mtime;\n        }\n#else\n        struct stat st;\n        if (fstat(fileno(file->fp), &st) == 0) {\n            mtime = (uint64_t)st.st_mtime;\n        }\n#endif\n        *out_mtime = mtime;\n    }\n\n    File_Close(file);\n    return true;\n}\n\nvoid File_Close(MYFILE *file)\n{\n    fclose(file->fp);\n    Memory_FreePointer(&file->path);\n    // free per-file line buffer\n    Memory_FreePointer(&file);\n}\n\nbool File_Load(const char *path, char **output_data, size_t *output_size)\n{\n    ASSERT(output_data != nullptr);\n\n    MYFILE *fp = File_Open(path, FILE_OPEN_READ);\n    if (!fp) {\n        LOG_ERROR(\"Can't open file %s\", path);\n        *output_data = nullptr;\n        return false;\n    }\n\n    size_t data_size = File_Size(fp);\n    char *data = Memory_Alloc(data_size + 1);\n    File_ReadData(fp, data, data_size);\n    if (File_Pos(fp) != data_size) {\n        *output_data = nullptr;\n        LOG_ERROR(\"Can't read file %s\", path);\n        Memory_FreePointer(&data);\n        File_Close(fp);\n        return false;\n    }\n    File_Close(fp);\n    data[data_size] = '\\0';\n\n    *output_data = data;\n    if (output_size != nullptr) {\n        *output_size = data_size;\n    }\n    return true;\n}\n\nvoid File_CreateDirectory(const char *path)\n{\n    if (path == nullptr) {\n        return;\n    }\n#if defined(_WIN32)\n    _mkdir(path);\n#else\n    mkdir(path, 0775);\n#endif\n}\n\nvoid File_EnsureParentDirectories(const char *path)\n{\n    ASSERT(path != nullptr);\n    char *parent = File_GetParentDirectory(path);\n    if (parent != nullptr) {\n        /* Only recurse/create if there is a distinct, non-empty parent */\n        if (parent[0] != '\\0' && strcmp(parent, path) != 0) {\n            if (!File_DirExists(parent)) {\n                File_EnsureParentDirectories(parent);\n                File_CreateDirectory(parent);\n            }\n        }\n        Memory_FreePointer(&parent);\n    }\n}\n\nvoid *File_OpenDirectory(const char *const path)\n{\n    ASSERT(path != nullptr);\n    return opendir(path);\n}\n\nconst char *File_ReadDirectory(void *const dir)\n{\n    DIR *path_dir = (DIR *)dir;\n    struct dirent *cur_file = readdir(dir);\n    if (cur_file == nullptr) {\n        return nullptr;\n    }\n    return cur_file->d_name;\n}\n\nvoid File_CloseDirectory(void *const dir)\n{\n    ASSERT(dir != nullptr);\n    closedir(dir);\n}\n"
  },
  {
    "path": "src/trx/core/filesystem.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\n// Low-level filesystem module.\n// Intentionally dumb wrappers over file/dir primitives. No path policy,\n// no token expansion, and no case-normalization logic belongs here.\n\ntypedef enum {\n    FILE_SEEK_SET,\n    FILE_SEEK_CUR,\n    FILE_SEEK_END,\n} FILE_SEEK_MODE;\n\ntypedef enum {\n    FILE_OPEN_READ,\n    FILE_OPEN_READ_WRITE,\n    FILE_OPEN_WRITE,\n} FILE_OPEN_MODE;\n\ntypedef struct MYFILE MYFILE;\n\n// ============================================================================\n// Path functions\n\n// Return true when path points to an existing directory.\nbool File_DirExists(const char *path);\n\n// Return true if path is absolute for current platform conventions.\nbool File_IsAbsolute(const char *path);\n\n// Return true if path is not absolute.\nbool File_IsRelative(const char *path);\n\n// Return true when path points to an existing file.\nbool File_Exists(const char *path);\n\n// Return parent directory component of path (owning string), or nullptr.\nchar *File_GetParentDirectory(const char *path);\n\n// ============================================================================\n// File handle functions\n\n// Open path with requested mode and wrap as MYFILE.\n// Returns nullptr on failure.\nMYFILE *File_Open(const char *path, FILE_OPEN_MODE mode);\n// Current byte position in file stream.\nsize_t File_Pos(MYFILE *file);\n\n// Total file size in bytes.\nsize_t File_Size(MYFILE *file);\n\n// Original path passed to File_Open.\nconst char *File_GetPath(MYFILE *file);\n\n// Get file size and modification time (seconds since epoch).\n// Returns false if the file cannot be opened/resolved.\nbool File_GetMeta(const char *path, uint64_t *out_size, uint64_t *out_mtime);\n\n// Skip forward by `bytes`.\nvoid File_Skip(MYFILE *file, size_t bytes);\n\n// Seek to position according to FILE_SEEK_MODE.\nvoid File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode);\n\n// Close and free MYFILE.\nvoid File_Close(MYFILE *file);\n\n// Load entire file into memory as a null-terminated buffer.\n// Caller owns `output_data` and must free it.\nbool File_Load(const char *path, char **output_data, size_t *output_size);\n\n// ============================================================================\n// Read helpers\n\n// Read exact byte count into `data`. Returns false on short read.\nbool File_ReadData(MYFILE *file, void *data, size_t size);\n// Read `count` items of `item_size` into `data`. Returns false on short read.\nbool File_ReadItems(MYFILE *file, void *data, size_t count, size_t item_size);\n// Typed scalar read helpers.\nint8_t File_ReadS8(MYFILE *file);\nint16_t File_ReadS16(MYFILE *file);\nint32_t File_ReadS32(MYFILE *file);\nuint8_t File_ReadU8(MYFILE *file);\nuint16_t File_ReadU16(MYFILE *file);\nuint32_t File_ReadU32(MYFILE *file);\n\n// ============================================================================\n// Write helpers\n\n// Raw/typed write helpers.\nvoid File_WriteData(MYFILE *file, const void *data, size_t size);\nvoid File_WriteItems(\n    MYFILE *file, const void *data, size_t count, size_t item_size);\nvoid File_WriteS8(MYFILE *file, int8_t value);\nvoid File_WriteS16(MYFILE *file, int16_t value);\nvoid File_WriteS32(MYFILE *file, int32_t value);\nvoid File_WriteU8(MYFILE *file, uint8_t value);\nvoid File_WriteU16(MYFILE *file, uint16_t value);\nvoid File_WriteU32(MYFILE *file, uint32_t value);\n\n// Write formatted string to file using a static-format buffer.\n// The formatted text is written via fputs; no trailing newline is added.\nvoid File_WriteString(MYFILE *file, const char *fmt, ...);\n\n// ============================================================================\n// Directory functions\n\n// Create one directory path component.\nvoid File_CreateDirectory(const char *path);\n// Recursively ensure all parent directories for `path` exist.\nvoid File_EnsureParentDirectories(const char *path);\n\n// Directory iteration API.\nvoid *File_OpenDirectory(const char *path);\nconst char *File_ReadDirectory(void *dir);\nvoid File_CloseDirectory(void *dir);\n"
  },
  {
    "path": "src/trx/core/hash.c",
    "content": "#include <trx/core/hash.h>\n\n#include <string.h>\n\n#define M_FNV_1A_PRIME 1099511628211ULL\n\nuint64_t Hash_FNV1a64_Init(void)\n{\n    return HASH_FNV1A64_BASE;\n}\n\nuint64_t Hash_FNV1a64_Update(\n    uint64_t hash, const void *const data, const size_t size)\n{\n    const uint8_t *cur = data;\n    for (size_t i = 0; i < size; i++) {\n        hash ^= cur[i];\n        hash *= M_FNV_1A_PRIME;\n    }\n    return hash;\n}\n\nuint64_t Hash_FNV1a64_UpdateU32(const uint64_t hash, const uint32_t value)\n{\n    return Hash_FNV1a64_Update(hash, &value, sizeof(value));\n}\n\nuint64_t Hash_FNV1a64_UpdateU64(const uint64_t hash, const uint64_t value)\n{\n    return Hash_FNV1a64_Update(hash, &value, sizeof(value));\n}\n\nuint64_t Hash_FNV1a64_UpdateString(uint64_t hash, const char *const value)\n{\n    if (value == nullptr) {\n        return Hash_FNV1a64_UpdateU32(hash, 0);\n    }\n    const uint32_t len = (uint32_t)strlen(value);\n    hash = Hash_FNV1a64_UpdateU32(hash, len);\n    return Hash_FNV1a64_Update(hash, value, len);\n}\n"
  },
  {
    "path": "src/trx/core/hash.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\n#define HASH_FNV1A64_BASE 14695981039346656037ULL\n\nuint64_t Hash_FNV1a64_Init(void);\nuint64_t Hash_FNV1a64_Update(uint64_t hash, const void *data, size_t size);\nuint64_t Hash_FNV1a64_UpdateU32(uint64_t hash, uint32_t value);\nuint64_t Hash_FNV1a64_UpdateU64(uint64_t hash, uint64_t value);\nuint64_t Hash_FNV1a64_UpdateString(uint64_t hash, const char *value);\n"
  },
  {
    "path": "src/trx/core/json/base.c",
    "content": "#include <trx/core/json/base.h>\n\n#include <trx/core/memory.h>\n\n#include <inttypes.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nstatic JSON_NUMBER *M_NumberNewInt(const int number)\n{\n    const size_t size = snprintf(nullptr, 0, \"%d\", number) + 1;\n    char *const buf = Memory_Alloc(size);\n    sprintf(buf, \"%d\", number);\n    JSON_NUMBER *const elem = Memory_Alloc(sizeof(JSON_NUMBER));\n    elem->number = buf;\n    elem->number_size = strlen(buf);\n    return elem;\n}\n\nstatic JSON_NUMBER *M_NumberNewInt64(const int64_t number)\n{\n    const size_t size = snprintf(nullptr, 0, \"%\" PRId64, number) + 1;\n    char *const buf = Memory_Alloc(size);\n    sprintf(buf, \"%\" PRId64, number);\n    JSON_NUMBER *const elem = Memory_Alloc(sizeof(JSON_NUMBER));\n    elem->number = buf;\n    elem->number_size = strlen(buf);\n    return elem;\n}\n\nstatic JSON_NUMBER *M_NumberNewDouble(const double number)\n{\n    const size_t size = snprintf(nullptr, 0, \"%f\", number) + 3;\n    char *const buf = Memory_Alloc(size);\n    sprintf(buf, \"%f\", number);\n\n    // Remove trailing zeros, keeping at least one digit after the decimal point\n    char *const dot = strchr(buf, '.');\n    if (dot == nullptr) {\n        strcat(buf, \".0\");\n    } else {\n        char *end = buf + strlen(buf) - 1;\n        while (end > dot && *end == '0') {\n            end--;\n        }\n        if (*end == '.') {\n            // All fractional digits removed => append a single 0 to get \"1.0\".\n            end[1] = '0';\n            end[2] = '\\0';\n        } else {\n            // Terminate string after the last non-zero digit to get \"1.123\".\n            end[1] = '\\0';\n        }\n    }\n\n    JSON_NUMBER *const elem = Memory_Alloc(sizeof(JSON_NUMBER));\n    elem->number = buf;\n    elem->number_size = strlen(buf);\n    return elem;\n}\n\nstatic void M_NumberFree(JSON_NUMBER *const num)\n{\n    if (num->ref_count == 0) {\n        Memory_Free(num->number);\n        Memory_Free(num);\n    }\n}\n\nstatic JSON_STRING *M_StringNew(const char *const string)\n{\n    JSON_STRING *const str = Memory_Alloc(sizeof(JSON_STRING));\n    str->string = Memory_DupStr(string);\n    str->string_size = strlen(string);\n    return str;\n}\n\nstatic void M_StringFree(JSON_STRING *const str)\n{\n    if (str->ref_count == 0) {\n        Memory_Free(str->string);\n        Memory_Free(str);\n    }\n}\n\nstatic JSON_VALUE *M_ValueFromNumber(JSON_NUMBER *const num)\n{\n    JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE));\n    value->type = JSON_TYPE_NUMBER;\n    value->payload = num;\n    return value;\n}\n\nstatic void M_ArrayElementFree(JSON_ARRAY_ELEMENT *const element)\n{\n    if (element->ref_count == 0) {\n        Memory_Free(element);\n    }\n}\n\nstatic void M_ObjectElementFree(JSON_OBJECT_ELEMENT *element)\n{\n    if (element->ref_count == 0) {\n        Memory_FreePointer(&element);\n    }\n}\n\nJSON_VALUE *JSON_ValueFromBool(const int b)\n{\n    JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE));\n    value->type = b ? JSON_TYPE_TRUE : JSON_TYPE_FALSE;\n    value->payload = nullptr;\n    return value;\n}\n\nJSON_VALUE *JSON_ValueFromInt(const int number)\n{\n    return M_ValueFromNumber(M_NumberNewInt(number));\n}\n\nJSON_VALUE *JSON_ValueFromInt64(const int64_t number)\n{\n    return M_ValueFromNumber(M_NumberNewInt64(number));\n}\n\nJSON_VALUE *JSON_ValueFromDouble(const double number)\n{\n    return M_ValueFromNumber(M_NumberNewDouble(number));\n}\n\nJSON_VALUE *JSON_ValueFromString(const char *const string)\n{\n    JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE));\n    value->type = JSON_TYPE_STRING;\n    value->payload = M_StringNew(string);\n    return value;\n}\n\nJSON_VALUE *JSON_ValueFromArray(JSON_ARRAY *const arr)\n{\n    JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE));\n    value->type = JSON_TYPE_ARRAY;\n    value->payload = arr;\n    return value;\n}\n\nJSON_VALUE *JSON_ValueFromObject(JSON_OBJECT *const obj)\n{\n    JSON_VALUE *const value = Memory_Alloc(sizeof(JSON_VALUE));\n    value->type = JSON_TYPE_OBJECT;\n    value->payload = obj;\n    return value;\n}\n\nvoid JSON_ValueFree(JSON_VALUE *const value)\n{\n    if (value == nullptr) {\n        return;\n    }\n\n    switch (value->type) {\n    case JSON_TYPE_NUMBER:\n        M_NumberFree((JSON_NUMBER *)value->payload);\n        break;\n    case JSON_TYPE_STRING:\n        M_StringFree((JSON_STRING *)value->payload);\n        break;\n    case JSON_TYPE_ARRAY:\n        JSON_ArrayFree((JSON_ARRAY *)value->payload);\n        break;\n    case JSON_TYPE_OBJECT:\n        JSON_ObjectFree((JSON_OBJECT *)value->payload);\n        break;\n    case JSON_TYPE_TRUE:\n    case JSON_TYPE_NULL:\n    case JSON_TYPE_FALSE:\n        break;\n    }\n\n    if (value->ref_count == 0) {\n        Memory_Free(value);\n    }\n}\n\nbool JSON_ValueIsNull(const JSON_VALUE *const value)\n{\n    return value != nullptr && value->type == JSON_TYPE_NULL;\n}\n\nbool JSON_ValueIsTrue(const JSON_VALUE *const value)\n{\n    return value != nullptr && value->type == JSON_TYPE_TRUE;\n}\n\nbool JSON_ValueIsFalse(const JSON_VALUE *const value)\n{\n    return value != nullptr && value->type == JSON_TYPE_FALSE;\n}\n\nint JSON_ValueGetBool(const JSON_VALUE *const value, const int d)\n{\n    if (JSON_ValueIsTrue(value)) {\n        return 1;\n    } else if (JSON_ValueIsFalse(value)) {\n        return 0;\n    }\n    return d;\n}\n\nconst JSON_NUMBER *JSON_ValueGetNumber(const JSON_VALUE *const value)\n{\n    if (value == nullptr || value->type != JSON_TYPE_NUMBER) {\n        return nullptr;\n    }\n    return (const JSON_NUMBER *)value->payload;\n}\n\nint JSON_ValueGetInt(const JSON_VALUE *const value, const int d)\n{\n    const JSON_NUMBER *const num = JSON_ValueGetNumber(value);\n    if (num == nullptr) {\n        return d;\n    }\n    const char *const s = num->number;\n    if (strncmp(s, \"0x\", 2) == 0 || strncmp(s, \"0X\", 2) == 0) {\n        return strtol(s, nullptr, 16);\n    }\n    if (strncmp(s, \"0b\", 2) == 0 || strncmp(s, \"0B\", 2) == 0) {\n        return strtol(s + 2, nullptr, 2);\n    }\n    return atoi(s);\n}\n\nint64_t JSON_ValueGetInt64(const JSON_VALUE *const value, const int64_t d)\n{\n    const JSON_NUMBER *const num = JSON_ValueGetNumber(value);\n    return num != nullptr ? strtoll(num->number, nullptr, 10) : d;\n}\n\ndouble JSON_ValueGetDouble(const JSON_VALUE *const value, const double d)\n{\n    const JSON_NUMBER *const num = JSON_ValueGetNumber(value);\n    return num != nullptr ? atof(num->number) : d;\n}\n\nconst char *JSON_ValueGetString(\n    const JSON_VALUE *const value, const char *const d)\n{\n    if (value == nullptr || value->type != JSON_TYPE_STRING) {\n        return nullptr;\n    }\n    const JSON_STRING *const string = value->payload;\n    return string != nullptr ? string->string : d;\n}\n\nJSON_ARRAY *JSON_ValueAsArray_Impl(const JSON_VALUE *const value)\n{\n    if (value == nullptr || value->type != JSON_TYPE_ARRAY) {\n        return nullptr;\n    }\n    return (JSON_ARRAY *)value->payload;\n}\n\nJSON_OBJECT *JSON_ValueAsObject_Impl(const JSON_VALUE *const value)\n{\n    if (value == nullptr || value->type != JSON_TYPE_OBJECT) {\n        return nullptr;\n    }\n    return (JSON_OBJECT *)value->payload;\n}\n\nJSON_ARRAY *JSON_ArrayNew(void)\n{\n    JSON_ARRAY *const arr = Memory_Alloc(sizeof(JSON_ARRAY));\n    arr->start = nullptr;\n    arr->length = 0;\n    return arr;\n}\n\nvoid JSON_ArrayFree(JSON_ARRAY *const arr)\n{\n    JSON_ARRAY_ELEMENT *elem = arr->start;\n    while (elem) {\n        JSON_ARRAY_ELEMENT *const next = elem->next;\n        JSON_ValueFree(elem->value);\n        M_ArrayElementFree(elem);\n        elem = next;\n    }\n    if (arr->ref_count == 0) {\n        Memory_Free(arr);\n    }\n}\n\nvoid JSON_ArrayAppend(JSON_ARRAY *const arr, JSON_VALUE *const value)\n{\n    JSON_ARRAY_ELEMENT *elem = Memory_Alloc(sizeof(JSON_ARRAY_ELEMENT));\n    elem->value = value;\n    elem->next = nullptr;\n    if (arr->start) {\n        JSON_ARRAY_ELEMENT *target = arr->start;\n        while (target->next) {\n            target = target->next;\n        }\n        target->next = elem;\n    } else {\n        arr->start = elem;\n    }\n    arr->length++;\n}\n\nvoid JSON_ArrayAppendBool(JSON_ARRAY *const arr, const int b)\n{\n    JSON_ArrayAppend(arr, JSON_ValueFromBool(b));\n}\n\nvoid JSON_ArrayAppendInt(JSON_ARRAY *const arr, const int number)\n{\n    JSON_ArrayAppend(arr, JSON_ValueFromInt(number));\n}\n\nvoid JSON_ArrayAppendDouble(JSON_ARRAY *const arr, const double number)\n{\n    JSON_ArrayAppend(arr, JSON_ValueFromDouble(number));\n}\n\nvoid JSON_ArrayAppendString(JSON_ARRAY *const arr, const char *string)\n{\n    JSON_ArrayAppend(arr, JSON_ValueFromString(string));\n}\n\nvoid JSON_ArrayAppendArray(JSON_ARRAY *const arr, JSON_ARRAY *const arr2)\n{\n    JSON_ArrayAppend(arr, JSON_ValueFromArray(arr2));\n}\n\nvoid JSON_ArrayAppendObject(JSON_ARRAY *const arr, JSON_OBJECT *const obj)\n{\n    JSON_ArrayAppend(arr, JSON_ValueFromObject(obj));\n}\n\nJSON_VALUE *JSON_ArrayGetValue(const JSON_ARRAY *const arr, const size_t idx)\n{\n    if (arr == nullptr || idx >= arr->length) {\n        return nullptr;\n    }\n    JSON_ARRAY_ELEMENT *elem = arr->start;\n    for (size_t i = 0; i < idx; i++) {\n        elem = elem->next;\n    }\n    return elem->value;\n}\n\nint JSON_ArrayGetBool(\n    const JSON_ARRAY *const arr, const size_t idx, const int d)\n{\n    const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx);\n    return JSON_ValueGetBool(value, d);\n}\n\nint JSON_ArrayGetInt(const JSON_ARRAY *const arr, const size_t idx, const int d)\n{\n    const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx);\n    return JSON_ValueGetInt(value, d);\n}\n\ndouble JSON_ArrayGetDouble(\n    const JSON_ARRAY *const arr, const size_t idx, const double d)\n{\n    const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx);\n    return JSON_ValueGetDouble(value, d);\n}\n\nconst char *JSON_ArrayGetString(\n    const JSON_ARRAY *const arr, const size_t idx, const char *const d)\n{\n    const JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx);\n    return JSON_ValueGetString(value, d);\n}\n\nJSON_ARRAY *JSON_ArrayGetArray_Impl(\n    const JSON_ARRAY *const arr, const size_t idx)\n{\n    JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx);\n    return JSON_ValueAsArray(value);\n}\n\nJSON_OBJECT *JSON_ArrayGetObject_Impl(\n    const JSON_ARRAY *const arr, const size_t idx)\n{\n    JSON_VALUE *const value = JSON_ArrayGetValue(arr, idx);\n    return JSON_ValueAsObject(value);\n}\n\nJSON_OBJECT *JSON_ObjectNew(void)\n{\n    JSON_OBJECT *const obj = Memory_Alloc(sizeof(JSON_OBJECT));\n    obj->start = nullptr;\n    obj->length = 0;\n    return obj;\n}\n\nvoid JSON_ObjectFree(JSON_OBJECT *const obj)\n{\n    JSON_OBJECT_ELEMENT *elem = obj->start;\n    while (elem) {\n        JSON_OBJECT_ELEMENT *next = elem->next;\n        M_StringFree(elem->name);\n        JSON_ValueFree(elem->value);\n        M_ObjectElementFree(elem);\n        elem = next;\n    }\n    if (obj->ref_count == 0) {\n        Memory_Free(obj);\n    }\n}\n\nvoid JSON_ObjectAppend(\n    JSON_OBJECT *const obj, const char *const key, JSON_VALUE *const value)\n{\n    JSON_OBJECT_ELEMENT *elem = Memory_Alloc(sizeof(JSON_OBJECT_ELEMENT));\n    elem->name = M_StringNew(key);\n    elem->value = value;\n    elem->next = nullptr;\n    if (obj->start) {\n        JSON_OBJECT_ELEMENT *target = obj->start;\n        while (target->next) {\n            target = target->next;\n        }\n        target->next = elem;\n    } else {\n        obj->start = elem;\n    }\n    obj->length++;\n}\n\nvoid JSON_ObjectAppendBool(\n    JSON_OBJECT *const obj, const char *const key, const int b)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromBool(b));\n}\n\nvoid JSON_ObjectAppendInt(\n    JSON_OBJECT *const obj, const char *const key, const int number)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromInt(number));\n}\n\nvoid JSON_ObjectAppendInt64(\n    JSON_OBJECT *const obj, const char *const key, const int64_t number)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromInt64(number));\n}\n\nvoid JSON_ObjectAppendDouble(\n    JSON_OBJECT *const obj, const char *const key, const double number)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromDouble(number));\n}\n\nvoid JSON_ObjectAppendString(\n    JSON_OBJECT *const obj, const char *const key, const char *string)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromString(string));\n}\n\nvoid JSON_ObjectAppendArray(\n    JSON_OBJECT *const obj, const char *const key, JSON_ARRAY *const arr)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromArray(arr));\n}\n\nvoid JSON_ObjectAppendObject(\n    JSON_OBJECT *const obj, const char *const key, JSON_OBJECT *const obj2)\n{\n    JSON_ObjectAppend(obj, key, JSON_ValueFromObject(obj2));\n}\n\nbool JSON_ObjectContainsKey(JSON_OBJECT *const obj, const char *const key)\n{\n    JSON_OBJECT_ELEMENT *elem = obj->start;\n    while (elem != nullptr) {\n        if (!strcmp(elem->name->string, key)) {\n            return true;\n        }\n\n        elem = elem->next;\n    }\n\n    return false;\n}\n\nvoid JSON_ObjectEvictKey(JSON_OBJECT *const obj, const char *const key)\n{\n    if (obj == nullptr) {\n        return;\n    }\n    JSON_OBJECT_ELEMENT *elem = obj->start;\n    JSON_OBJECT_ELEMENT *prev = nullptr;\n    while (elem) {\n        if (!strcmp(elem->name->string, key)) {\n            if (prev == nullptr) {\n                obj->start = elem->next;\n            } else {\n                prev->next = elem->next;\n            }\n            M_StringFree(elem->name);\n            JSON_ValueFree(elem->value);\n            M_ObjectElementFree(elem);\n            obj->length--;\n            return;\n        }\n        prev = elem;\n        elem = elem->next;\n    }\n}\n\nvoid JSON_ObjectMerge(JSON_OBJECT *const root, const JSON_OBJECT *const obj)\n{\n    JSON_OBJECT_ELEMENT *elem = obj->start;\n    while (elem != nullptr) {\n        JSON_ObjectEvictKey(root, elem->name->string);\n        JSON_ObjectAppend(root, elem->name->string, elem->value);\n        elem = elem->next;\n    }\n}\n\nJSON_VALUE *JSON_ObjectGetValue(\n    const JSON_OBJECT *const obj, const char *const key)\n{\n    if (obj == nullptr) {\n        return nullptr;\n    }\n    JSON_OBJECT_ELEMENT *elem = obj->start;\n    while (elem) {\n        if (!strcmp(elem->name->string, key)) {\n            return elem->value;\n        }\n        elem = elem->next;\n    }\n    return nullptr;\n}\n\nint JSON_ObjectGetBool(\n    const JSON_OBJECT *const obj, const char *const key, const int d)\n{\n    const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueGetBool(value, d);\n}\n\nint JSON_ObjectGetInt(\n    const JSON_OBJECT *const obj, const char *const key, const int d)\n{\n    const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueGetInt(value, d);\n}\n\nint64_t JSON_ObjectGetInt64(\n    const JSON_OBJECT *const obj, const char *const key, const int64_t d)\n{\n    const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueGetInt64(value, d);\n}\n\ndouble JSON_ObjectGetDouble(\n    const JSON_OBJECT *const obj, const char *const key, const double d)\n{\n    const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueGetDouble(value, d);\n}\n\nconst char *JSON_ObjectGetString(\n    const JSON_OBJECT *const obj, const char *const key, const char *const d)\n{\n    const JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueGetString(value, d);\n}\n\nJSON_ARRAY *JSON_ObjectGetArray_Impl(\n    const JSON_OBJECT *const obj, const char *const key)\n{\n    JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueAsArray(value);\n}\n\nJSON_OBJECT *JSON_ObjectGetObject_Impl(\n    const JSON_OBJECT *const obj, const char *const key)\n{\n    JSON_VALUE *const value = JSON_ObjectGetValue(obj, key);\n    return JSON_ValueAsObject(value);\n}\n"
  },
  {
    "path": "src/trx/core/json/base.h",
    "content": "#pragma once\n\n#include <trx/core/json/enum.h>\n#include <trx/core/json/types.h>\n\n// values\nJSON_VALUE *JSON_ValueFromBool(int b);\nJSON_VALUE *JSON_ValueFromInt(int number);\nJSON_VALUE *JSON_ValueFromInt64(int64_t number);\nJSON_VALUE *JSON_ValueFromDouble(double number);\nJSON_VALUE *JSON_ValueFromString(const char *string);\nJSON_VALUE *JSON_ValueFromArray(JSON_ARRAY *arr);\nJSON_VALUE *JSON_ValueFromObject(JSON_OBJECT *obj);\nvoid JSON_ValueFree(JSON_VALUE *value);\n\nbool JSON_ValueIsNull(const JSON_VALUE *value);\nbool JSON_ValueIsTrue(const JSON_VALUE *value);\nbool JSON_ValueIsFalse(const JSON_VALUE *value);\nint JSON_ValueGetBool(const JSON_VALUE *value, int d);\nint JSON_ValueGetInt(const JSON_VALUE *value, int d);\nint64_t JSON_ValueGetInt64(const JSON_VALUE *value, int64_t d);\ndouble JSON_ValueGetDouble(const JSON_VALUE *value, double d);\nconst JSON_NUMBER *JSON_ValueGetNumber(const JSON_VALUE *value);\nconst char *JSON_ValueGetString(const JSON_VALUE *value, const char *d);\n\nJSON_ARRAY *JSON_ValueAsArray_Impl(const JSON_VALUE *value);\n#define JSON_ValueAsArray(value)                                               \\\n    JSON_CONST_DISPATCH(                                                       \\\n        value, const JSON_ARRAY *, JSON_ValueAsArray_Impl(value))\n\nJSON_OBJECT *JSON_ValueAsObject_Impl(const JSON_VALUE *value);\n#define JSON_ValueAsObject(value)                                              \\\n    JSON_CONST_DISPATCH(                                                       \\\n        value, const JSON_OBJECT *, JSON_ValueAsObject_Impl(value))\n\n// arrays\nJSON_ARRAY *JSON_ArrayNew(void);\nvoid JSON_ArrayFree(JSON_ARRAY *arr);\n\nvoid JSON_ArrayAppend(JSON_ARRAY *arr, JSON_VALUE *value);\nvoid JSON_ArrayAppendBool(JSON_ARRAY *arr, int b);\nvoid JSON_ArrayAppendInt(JSON_ARRAY *arr, int number);\nvoid JSON_ArrayAppendDouble(JSON_ARRAY *arr, double number);\nvoid JSON_ArrayAppendString(JSON_ARRAY *arr, const char *string);\nvoid JSON_ArrayAppendArray(JSON_ARRAY *arr, JSON_ARRAY *arr2);\nvoid JSON_ArrayAppendObject(JSON_ARRAY *arr, JSON_OBJECT *obj);\n\nJSON_VALUE *JSON_ArrayGetValue(const JSON_ARRAY *arr, size_t idx);\nint JSON_ArrayGetBool(const JSON_ARRAY *arr, size_t idx, int d);\nint JSON_ArrayGetInt(const JSON_ARRAY *arr, size_t idx, int d);\ndouble JSON_ArrayGetDouble(const JSON_ARRAY *arr, size_t idx, double d);\nconst char *JSON_ArrayGetString(\n    const JSON_ARRAY *arr, size_t idx, const char *d);\n\nJSON_ARRAY *JSON_ArrayGetArray_Impl(const JSON_ARRAY *arr, size_t idx);\n#define JSON_ArrayGetArray(value, ...)                                         \\\n    JSON_CONST_DISPATCH(                                                       \\\n        value, const JSON_ARRAY *,                                             \\\n        JSON_ArrayGetArray_Impl(value, __VA_ARGS__))\n\nJSON_OBJECT *JSON_ArrayGetObject_Impl(const JSON_ARRAY *arr, size_t idx);\n#define JSON_ArrayGetObject(value, ...)                                        \\\n    JSON_CONST_DISPATCH(                                                       \\\n        value, const JSON_ARRAY *,                                             \\\n        JSON_ArrayGetObject_Impl(value, __VA_ARGS__))\n\n// objects\nJSON_OBJECT *JSON_ObjectNew(void);\nvoid JSON_ObjectFree(JSON_OBJECT *obj);\n\nvoid JSON_ObjectAppend(JSON_OBJECT *obj, const char *key, JSON_VALUE *value);\nvoid JSON_ObjectAppendBool(JSON_OBJECT *obj, const char *key, int b);\nvoid JSON_ObjectAppendInt(JSON_OBJECT *obj, const char *key, int number);\nvoid JSON_ObjectAppendInt64(JSON_OBJECT *obj, const char *key, int64_t number);\nvoid JSON_ObjectAppendDouble(JSON_OBJECT *obj, const char *key, double number);\nvoid JSON_ObjectAppendString(\n    JSON_OBJECT *obj, const char *key, const char *string);\nvoid JSON_ObjectAppendArray(JSON_OBJECT *obj, const char *key, JSON_ARRAY *arr);\nvoid JSON_ObjectAppendObject(\n    JSON_OBJECT *obj, const char *key, JSON_OBJECT *obj2);\n\nbool JSON_ObjectContainsKey(JSON_OBJECT *obj, const char *key);\nvoid JSON_ObjectEvictKey(JSON_OBJECT *obj, const char *key);\nvoid JSON_ObjectMerge(JSON_OBJECT *root, const JSON_OBJECT *obj);\n\nJSON_VALUE *JSON_ObjectGetValue(const JSON_OBJECT *obj, const char *key);\nint JSON_ObjectGetBool(const JSON_OBJECT *obj, const char *key, int d);\nint JSON_ObjectGetInt(const JSON_OBJECT *obj, const char *key, int d);\nint64_t JSON_ObjectGetInt64(const JSON_OBJECT *obj, const char *key, int64_t d);\ndouble JSON_ObjectGetDouble(const JSON_OBJECT *obj, const char *key, double d);\nconst char *JSON_ObjectGetString(\n    const JSON_OBJECT *obj, const char *key, const char *d);\n\nJSON_ARRAY *JSON_ObjectGetArray_Impl(const JSON_OBJECT *obj, const char *key);\n#define JSON_ObjectGetArray(value, ...)                                        \\\n    JSON_CONST_DISPATCH(                                                       \\\n        value, const JSON_ARRAY *,                                             \\\n        JSON_ObjectGetArray_Impl(value, __VA_ARGS__))\n\nJSON_OBJECT *JSON_ObjectGetObject_Impl(const JSON_OBJECT *obj, const char *key);\n#define JSON_ObjectGetObject(value, ...)                                       \\\n    JSON_CONST_DISPATCH(                                                       \\\n        value, const JSON_OBJECT *,                                            \\\n        JSON_ObjectGetObject_Impl(value, __VA_ARGS__))\n"
  },
  {
    "path": "src/trx/core/json/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    JSON_TYPE_STRING,\n    JSON_TYPE_NUMBER,\n    JSON_TYPE_OBJECT,\n    JSON_TYPE_ARRAY,\n    JSON_TYPE_TRUE,\n    JSON_TYPE_FALSE,\n    JSON_TYPE_NULL\n} JSON_TYPE;\n"
  },
  {
    "path": "src/trx/core/json/parse.c",
    "content": "#include <trx/core/json/parse.h>\n\n#include <trx/core/memory.h>\n\ntypedef struct {\n    const char *src;\n    size_t size;\n    size_t offset;\n    size_t flags_bitset;\n    char *data;\n    char *dom;\n    size_t dom_size;\n    size_t data_size;\n    size_t line_no;\n    size_t line_offset;\n    size_t error;\n} M_STATE;\n\nstatic int M_GetValueSize(M_STATE *state, int is_global_object);\nstatic void M_HandleValue(\n    M_STATE *state, int is_global_object, JSON_VALUE *value);\n\nstatic int M_HexDigit(const char c)\n{\n    if ('0' <= c && c <= '9') {\n        return c - '0';\n    }\n    if ('a' <= c && c <= 'f') {\n        return c - 'a' + 10;\n    }\n    if ('A' <= c && c <= 'F') {\n        return c - 'A' + 10;\n    }\n    return -1;\n}\n\nstatic int M_HexValue(\n    const char *c, const unsigned long size, unsigned long *result)\n{\n    const char *p;\n    int digit;\n\n    if (size > sizeof(unsigned long) * 2) {\n        return 0;\n    }\n\n    *result = 0;\n    for (p = c; (unsigned long)(p - c) < size; ++p) {\n        *result <<= 4;\n        digit = M_HexDigit(*p);\n        if (digit < 0 || digit > 15) {\n            return 0;\n        }\n        *result |= (unsigned char)digit;\n    }\n    return 1;\n}\n\nstatic int M_SkipWhitespace(M_STATE *state)\n{\n    size_t offset = state->offset;\n    const size_t size = state->size;\n    const char *const src = state->src;\n\n    /* the only valid whitespace according to ECMA-404 is ' ', '\\n', '\\r' and\n     * '\\t'. */\n    switch (src[offset]) {\n    default:\n        return 0;\n    case ' ':\n    case '\\r':\n    case '\\t':\n    case '\\n':\n        break;\n    }\n\n    do {\n        switch (src[offset]) {\n        default:\n            /* Update offset. */\n            state->offset = offset;\n            return 1;\n        case ' ':\n        case '\\r':\n        case '\\t':\n            break;\n        case '\\n':\n            state->line_no++;\n            state->line_offset = offset;\n            break;\n        }\n\n        offset++;\n    } while (offset < size);\n\n    /* Update offset. */\n    state->offset = offset;\n    return 1;\n}\n\nstatic int M_SkipCStyleComments(M_STATE *state)\n{\n    /* do we have a comment?. */\n    if ('/' == state->src[state->offset]) {\n        /* skip '/'. */\n        state->offset++;\n\n        if ('/' == state->src[state->offset]) {\n            /* we had a comment of the form //. */\n\n            /* skip second '/'. */\n            state->offset++;\n\n            while (state->offset < state->size) {\n                switch (state->src[state->offset]) {\n                default:\n                    /* skip the character in the comment. */\n                    state->offset++;\n                    break;\n                case '\\n':\n                    /* if we have a newline, our comment has ended! Skip the\n                     * newline. */\n                    state->offset++;\n\n                    /* we entered a newline, so move our line info forward. */\n                    state->line_no++;\n                    state->line_offset = state->offset;\n                    return 1;\n                }\n            }\n\n            /* we reached the end of the JSON file! */\n            return 1;\n        } else if ('*' == state->src[state->offset]) {\n            /* we had a comment in the C-style long form. */\n\n            /* skip '*'. */\n            state->offset++;\n\n            while (state->offset + 1 < state->size) {\n                if (('*' == state->src[state->offset])\n                    && ('/' == state->src[state->offset + 1])) {\n                    /* we reached the end of our comment! */\n                    state->offset += 2;\n                    return 1;\n                } else if ('\\n' == state->src[state->offset]) {\n                    /* we entered a newline, so move our line info forward. */\n                    state->line_no++;\n                    state->line_offset = state->offset;\n                }\n\n                /* skip character within comment. */\n                state->offset++;\n            }\n\n            /* Comment wasn't ended correctly which is a failure. */\n            return 1;\n        }\n    }\n\n    /* we didn't have any comment, which is ok too! */\n    return 0;\n}\n\nstatic int M_SkipAllSkippables(M_STATE *state)\n{\n    /* skip all whitespace and other skippables until there are none left. note\n     * that the previous version suffered from read past errors should. the\n     * stream end on M_SkipCStyleComments eg. '{\"a\" ' with comments flag.\n     */\n\n    int did_consume = 0;\n    const size_t size = state->size;\n\n    if (JSON_PARSE_FLAGS_ALLOW_C_STYLE_COMMENTS & state->flags_bitset) {\n        do {\n            if (state->offset == size) {\n                state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                return 1;\n            }\n\n            did_consume = M_SkipWhitespace(state);\n\n            /* This should really be checked on access, not in front of every\n             * call.\n             */\n            if (state->offset == size) {\n                state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                return 1;\n            }\n\n            did_consume |= M_SkipCStyleComments(state);\n        } while (0 != did_consume);\n    } else {\n        do {\n            if (state->offset == size) {\n                state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                return 1;\n            }\n\n            did_consume = M_SkipWhitespace(state);\n        } while (0 != did_consume);\n    }\n\n    if (state->offset == size) {\n        state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return 1;\n    }\n\n    return 0;\n}\n\nstatic int M_GetStringSize(M_STATE *state, size_t is_key)\n{\n    size_t offset = state->offset;\n    const size_t size = state->size;\n    size_t data_size = 0;\n    const char *const src = state->src;\n    const int is_single_quote = '\\'' == src[offset];\n    const char quote_to_use = is_single_quote ? '\\'' : '\"';\n    const size_t flags_bitset = state->flags_bitset;\n    unsigned long codepoint;\n    unsigned long high_surrogate = 0;\n\n    if ((JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) != 0\n        && is_key != 0) {\n        state->dom_size += sizeof(JSON_STRING_EX);\n    } else {\n        state->dom_size += sizeof(JSON_STRING);\n    }\n\n    if ('\"' != src[offset]) {\n        /* if we are allowed single quoted strings check for that too. */\n        if (!((JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS & flags_bitset)\n              && is_single_quote)) {\n            state->error = JSON_PARSE_ERROR_EXPECTED_OPENING_QUOTE;\n            state->offset = offset;\n            return 1;\n        }\n    }\n\n    /* skip leading '\"' or '\\''. */\n    offset++;\n\n    while ((offset < size) && (quote_to_use != src[offset])) {\n        /* add space for the character. */\n        data_size++;\n\n        switch (src[offset]) {\n        case '\\0':\n        case '\\t':\n            state->error = JSON_PARSE_ERROR_INVALID_STRING;\n            state->offset = offset;\n            return 1;\n        }\n\n        if ('\\\\' == src[offset]) {\n            /* skip reverse solidus character. */\n            offset++;\n\n            if (offset == size) {\n                state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                state->offset = offset;\n                return 1;\n            }\n\n            switch (src[offset]) {\n            default:\n                state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                state->offset = offset;\n                return 1;\n            case '\"':\n            case '\\\\':\n            case '/':\n            case 'b':\n            case 'f':\n            case 'n':\n            case 'r':\n            case 't':\n                /* all valid characters! */\n                offset++;\n                break;\n            case 'u':\n                if (!(offset + 5 < size)) {\n                    /* invalid escaped unicode sequence! */\n                    state->error =\n                        JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                    state->offset = offset;\n                    return 1;\n                }\n\n                codepoint = 0;\n                if (!M_HexValue(&src[offset + 1], 4, &codepoint)) {\n                    /* escaped unicode sequences must contain 4 hexadecimal\n                     * digits! */\n                    state->error =\n                        JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                    state->offset = offset;\n                    return 1;\n                }\n\n                /* Valid sequence!\n                 * see: https://en.wikipedia.org/wiki/UTF-8#Invalid_code_points.\n                 *      1       7       U + 0000        U + 007F 0xxxxxxx. 2 11\n                 * U + 0080        U + 07FF        110xxxxx 10xxxxxx. 3       16\n                 * U + 0800        U + FFFF        1110xxxx 10xxxxxx 10xxxxxx.\n                 *      4       21      U + 10000       U + 10FFFF      11110xxx\n                 * 10xxxxxx        10xxxxxx        10xxxxxx.\n                 * Note: the high and low surrogate halves used by UTF-16\n                 * (U+D800 through U+DFFF) and code points not encodable by\n                 * UTF-16 (those after U+10FFFF) are not legal Unicode values,\n                 * and their UTF-8 encoding must be treated as an invalid byte\n                 * sequence. */\n\n                if (high_surrogate != 0) {\n                    /* we previously read the high half of the \\uxxxx\\uxxxx\n                     * pair, so now we expect the low half. */\n                    if (codepoint >= 0xdc00\n                        && codepoint <= 0xdfff) { /* low surrogate range. */\n                        data_size += 3;\n                        high_surrogate = 0;\n                    } else {\n                        state->error =\n                            JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                        state->offset = offset;\n                        return 1;\n                    }\n                } else if (codepoint <= 0x7f) {\n                    data_size += 0;\n                } else if (codepoint <= 0x7ff) {\n                    data_size += 1;\n                } else if (\n                    codepoint >= 0xd800 && codepoint <= 0xdbff) { /* high\n                                                                     surrogate\n                                                                     range.\n                                                                   */\n                    /* The codepoint is the first half of a \"utf-16 surrogate\n                     * pair\". so we need the other half for it to be valid:\n                     * \\uHHHH\\uLLLL. */\n                    if (offset + 11 > size || '\\\\' != src[offset + 5]\n                        || 'u' != src[offset + 6]) {\n                        state->error =\n                            JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                        state->offset = offset;\n                        return 1;\n                    }\n                    high_surrogate = codepoint;\n                } else if (\n                    codepoint >= 0xd800 && codepoint <= 0xdfff) { /* low\n                                                                     surrogate\n                                                                     range.\n                                                                   */\n                    /* we did not read the other half before. */\n                    state->error =\n                        JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                    state->offset = offset;\n                    return 1;\n                } else {\n                    data_size += 2;\n                }\n                /* escaped codepoints after 0xffff are supported in json through\n                 * utf-16 surrogate pairs: \\uD83D\\uDD25 for U+1F525. */\n\n                offset += 5;\n                break;\n            }\n        } else if (('\\r' == src[offset]) || ('\\n' == src[offset])) {\n            if (!(JSON_PARSE_FLAGS_ALLOW_MULTI_LINE_STRINGS & flags_bitset)) {\n                /* invalid escaped unicode sequence! */\n                state->error = JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE;\n                state->offset = offset;\n                return 1;\n            }\n\n            offset++;\n        } else {\n            /* skip character (valid part of sequence). */\n            offset++;\n        }\n    }\n\n    /* If the offset is equal to the size, we had a non-terminated string! */\n    if (offset == size) {\n        state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        state->offset = offset - 1;\n        return 1;\n    }\n\n    /* skip trailing '\"' or '\\''. */\n    offset++;\n\n    /* add enough space to store the string. */\n    state->data_size += data_size;\n\n    /* one more byte for null terminator ending the string! */\n    state->data_size++;\n\n    /* update offset. */\n    state->offset = offset;\n\n    return 0;\n}\n\nstatic int M_IsValidUnquotedKeyChar(const char c)\n{\n    return (\n        ('0' <= c && c <= '9') || ('a' <= c && c <= 'z')\n        || ('A' <= c && c <= 'Z') || ('_' == c));\n}\n\nstatic int M_GetKeySize(M_STATE *state)\n{\n    const size_t flags_bitset = state->flags_bitset;\n\n    if (JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS & flags_bitset) {\n        size_t offset = state->offset;\n        const size_t size = state->size;\n        const char *const src = state->src;\n        size_t data_size = state->data_size;\n\n        /* if we are allowing unquoted keys, first grok for a quote... */\n        if ('\"' == src[offset]) {\n            /* ... if we got a comma, just parse the key as a string as normal.\n             */\n            return M_GetStringSize(state, 1);\n        } else if (\n            (JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS & flags_bitset)\n            && ('\\'' == src[offset])) {\n            /* ... if we got a comma, just parse the key as a string as normal.\n             */\n            return M_GetStringSize(state, 1);\n        } else {\n            while ((offset < size) && M_IsValidUnquotedKeyChar(src[offset])) {\n                offset++;\n                data_size++;\n            }\n\n            /* one more byte for null terminator ending the string! */\n            data_size++;\n\n            if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) {\n                state->dom_size += sizeof(JSON_STRING_EX);\n            } else {\n                state->dom_size += sizeof(JSON_STRING);\n            }\n\n            /* update offset. */\n            state->offset = offset;\n\n            /* update data_size. */\n            state->data_size = data_size;\n\n            return 0;\n        }\n    } else {\n        /* we are only allowed to have quoted keys, so just parse a string! */\n        return M_GetStringSize(state, 1);\n    }\n}\n\nstatic int M_GetObjectSize(M_STATE *state, int is_global_object)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    const char *const src = state->src;\n    const size_t size = state->size;\n    size_t elements = 0;\n    int allow_comma = 0;\n    int found_closing_brace = 0;\n\n    if (is_global_object) {\n        /* if we found an opening '{' of an object, we actually have a normal\n         * JSON object at the root of the DOM... */\n        if (!M_SkipAllSkippables(state) && '{' == state->src[state->offset]) {\n            /* . and we don't actually have a global object after all! */\n            is_global_object = 0;\n        }\n    }\n\n    if (!is_global_object) {\n        if ('{' != src[state->offset]) {\n            state->error = JSON_PARSE_ERROR_UNKNOWN;\n            return 1;\n        }\n\n        /* skip leading '{'. */\n        state->offset++;\n    }\n\n    state->dom_size += sizeof(JSON_OBJECT);\n\n    if ((state->offset == size) && !is_global_object) {\n        state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return 1;\n    }\n\n    do {\n        if (!is_global_object) {\n            if (M_SkipAllSkippables(state)) {\n                state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                return 1;\n            }\n\n            if ('}' == src[state->offset]) {\n                /* skip trailing '}'. */\n                state->offset++;\n\n                found_closing_brace = 1;\n\n                /* finished the object! */\n                break;\n            }\n        } else {\n            /* we don't require brackets, so that means the object ends when the\n             * input stream ends! */\n            if (M_SkipAllSkippables(state)) {\n                break;\n            }\n        }\n\n        /* if we parsed at least once element previously, grok for a comma. */\n        if (allow_comma) {\n            if (',' == src[state->offset]) {\n                /* skip comma. */\n                state->offset++;\n                allow_comma = 0;\n            } else if (JSON_PARSE_FLAGS_ALLOW_NO_COMMAS & flags_bitset) {\n                /* we don't require a comma, and we didn't find one, which is\n                 * ok! */\n                allow_comma = 0;\n            } else {\n                /* otherwise we are required to have a comma, and we found none.\n                 */\n                state->error =\n                    JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET;\n                return 1;\n            }\n\n            if (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA & flags_bitset) {\n                continue;\n            } else {\n                if (M_SkipAllSkippables(state)) {\n                    state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                    return 1;\n                }\n            }\n        }\n\n        if (M_GetKeySize(state)) {\n            /* key parsing failed! */\n            state->error = JSON_PARSE_ERROR_INVALID_STRING;\n            return 1;\n        }\n\n        if (M_SkipAllSkippables(state)) {\n            state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n            return 1;\n        }\n\n        if (JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT & flags_bitset) {\n            const char current = src[state->offset];\n            if ((':' != current) && ('=' != current)) {\n                state->error = JSON_PARSE_ERROR_EXPECTED_COLON;\n                return 1;\n            }\n        } else {\n            if (':' != src[state->offset]) {\n                state->error = JSON_PARSE_ERROR_EXPECTED_COLON;\n                return 1;\n            }\n        }\n\n        /* skip colon. */\n        state->offset++;\n\n        if (M_SkipAllSkippables(state)) {\n            state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n            return 1;\n        }\n\n        if (M_GetValueSize(state, /* is_global_object = */ 0)) {\n            /* value parsing failed! */\n            return 1;\n        }\n\n        /* successfully parsed a name/value pair! */\n        elements++;\n        allow_comma = 1;\n    } while (state->offset < size);\n\n    if ((state->offset == size) && !is_global_object && !found_closing_brace) {\n        state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n        return 1;\n    }\n\n    state->dom_size += sizeof(JSON_OBJECT_ELEMENT) * elements;\n\n    return 0;\n}\n\nstatic int M_GetArraySize(M_STATE *state)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    size_t elements = 0;\n    int allow_comma = 0;\n    const char *const src = state->src;\n    const size_t size = state->size;\n\n    if ('[' != src[state->offset]) {\n        /* expected array to begin with leading '['. */\n        state->error = JSON_PARSE_ERROR_UNKNOWN;\n        return 1;\n    }\n\n    /* skip leading '['. */\n    state->offset++;\n\n    state->dom_size += sizeof(JSON_ARRAY);\n\n    while (state->offset < size) {\n        if (M_SkipAllSkippables(state)) {\n            state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n            return 1;\n        }\n\n        if (']' == src[state->offset]) {\n            /* skip trailing ']'. */\n            state->offset++;\n\n            state->dom_size += sizeof(JSON_ARRAY_ELEMENT) * elements;\n\n            /* finished the object! */\n            return 0;\n        }\n\n        /* if we parsed at least once element previously, grok for a comma. */\n        if (allow_comma) {\n            if (',' == src[state->offset]) {\n                /* skip comma. */\n                state->offset++;\n                allow_comma = 0;\n            } else if (!(JSON_PARSE_FLAGS_ALLOW_NO_COMMAS & flags_bitset)) {\n                state->error =\n                    JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET;\n                return 1;\n            }\n\n            if (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA & flags_bitset) {\n                allow_comma = 0;\n                continue;\n            } else {\n                if (M_SkipAllSkippables(state)) {\n                    state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n                    return 1;\n                }\n            }\n        }\n\n        if (M_GetValueSize(state, /* is_global_object = */ 0)) {\n            /* value parsing failed! */\n            return 1;\n        }\n\n        /* successfully parsed an array element! */\n        elements++;\n        allow_comma = 1;\n    }\n\n    /* we consumed the entire input before finding the closing ']' of the array!\n     */\n    state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n    return 1;\n}\n\nstatic int M_GetNumberSize(M_STATE *state)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    size_t offset = state->offset;\n    const size_t size = state->size;\n    int had_leading_digits = 0;\n    const char *const src = state->src;\n\n    state->dom_size += sizeof(JSON_NUMBER);\n\n    if ((JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS & flags_bitset)\n        && (offset + 1 < size) && ('0' == src[offset])\n        && (('x' == src[offset + 1]) || ('X' == src[offset + 1]))) {\n        /* skip the leading 0x that identifies a hexadecimal number. */\n        offset += 2;\n\n        /* consume hexadecimal digits. */\n        while ((offset < size)\n               && (('0' <= src[offset] && src[offset] <= '9')\n                   || ('a' <= src[offset] && src[offset] <= 'f')\n                   || ('A' <= src[offset] && src[offset] <= 'F'))) {\n            offset++;\n        }\n    } else if (\n        (JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS & flags_bitset)\n        && (offset + 1 < size) && ('0' == src[offset])\n        && (('b' == src[offset + 1]) || ('B' == src[offset + 1]))) {\n        /* skip the leading 0b that identifies a binary number. */\n        offset += 2;\n\n        /* consume binary digits. */\n        while ((offset < size) && ('0' <= src[offset] && src[offset] <= '1')) {\n            offset++;\n        }\n    } else {\n        int found_sign = 0;\n        int inf_or_nan = 0;\n\n        if ((offset < size)\n            && (('-' == src[offset])\n                || ((JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN & flags_bitset)\n                    && ('+' == src[offset])))) {\n            /* skip valid leading '-' or '+'. */\n            offset++;\n\n            found_sign = 1;\n        }\n\n        if (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) {\n            const char inf[9] = \"Infinity\";\n            const size_t inf_strlen = sizeof(inf) - 1;\n            const char nan[4] = \"NaN\";\n            const size_t nan_strlen = sizeof(nan) - 1;\n\n            if (offset + inf_strlen < size) {\n                int found = 1;\n                size_t i;\n                for (i = 0; i < inf_strlen; i++) {\n                    if (inf[i] != src[offset + i]) {\n                        found = 0;\n                        break;\n                    }\n                }\n\n                if (found) {\n                    /* We found our special 'Infinity' keyword! */\n                    offset += inf_strlen;\n\n                    inf_or_nan = 1;\n                }\n            }\n\n            if (offset + nan_strlen < size) {\n                int found = 1;\n                size_t i;\n                for (i = 0; i < nan_strlen; i++) {\n                    if (nan[i] != src[offset + i]) {\n                        found = 0;\n                        break;\n                    }\n                }\n\n                if (found) {\n                    /* We found our special 'NaN' keyword! */\n                    offset += nan_strlen;\n\n                    inf_or_nan = 1;\n                }\n            }\n        }\n\n        if (found_sign && !inf_or_nan && (offset < size)\n            && !('0' <= src[offset] && src[offset] <= '9')) {\n            /* check if we are allowing leading '.'. */\n            if (!(JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT\n                  & flags_bitset)\n                || ('.' != src[offset])) {\n                /* a leading '-' must be immediately followed by any digit! */\n                state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n                state->offset = offset;\n                return 1;\n            }\n        }\n\n        if ((offset < size) && ('0' == src[offset])) {\n            /* skip valid '0'. */\n            offset++;\n\n            /* we need to record whether we had any leading digits for checks\n             * later.\n             */\n            had_leading_digits = 1;\n\n            if ((offset < size) && ('0' <= src[offset] && src[offset] <= '9')) {\n                /* a leading '0' must not be immediately followed by any digit!\n                 */\n                state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n                state->offset = offset;\n                return 1;\n            }\n        }\n\n        /* the main digits of our number next. */\n        while ((offset < size) && ('0' <= src[offset] && src[offset] <= '9')) {\n            offset++;\n\n            /* we need to record whether we had any leading digits for checks\n             * later.\n             */\n            had_leading_digits = 1;\n        }\n\n        if ((offset < size) && ('.' == src[offset])) {\n            offset++;\n\n            if (!('0' <= src[offset] && src[offset] <= '9')) {\n                if (!(JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT\n                      & flags_bitset)\n                    || !had_leading_digits) {\n                    /* a decimal point must be followed by at least one digit.\n                     */\n                    state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n                    state->offset = offset;\n                    return 1;\n                }\n            }\n\n            /* a decimal point can be followed by more digits of course! */\n            while ((offset < size)\n                   && ('0' <= src[offset] && src[offset] <= '9')) {\n                offset++;\n            }\n        }\n\n        if ((offset < size) && ('e' == src[offset] || 'E' == src[offset])) {\n            /* our number has an exponent! Skip 'e' or 'E'. */\n            offset++;\n\n            if ((offset < size) && ('-' == src[offset] || '+' == src[offset])) {\n                /* skip optional '-' or '+'. */\n                offset++;\n            }\n\n            if ((offset < size)\n                && !('0' <= src[offset] && src[offset] <= '9')) {\n                /* an exponent must have at least one digit! */\n                state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n                state->offset = offset;\n                return 1;\n            }\n\n            /* consume exponent digits. */\n            do {\n                offset++;\n            } while ((offset < size)\n                     && ('0' <= src[offset] && src[offset] <= '9'));\n        }\n    }\n\n    if (offset < size) {\n        switch (src[offset]) {\n        case ' ':\n        case '\\t':\n        case '\\r':\n        case '\\n':\n        case '}':\n        case ',':\n        case ']':\n            /* all of the above are ok. */\n            break;\n        case '=':\n            if (JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT & flags_bitset) {\n                break;\n            }\n\n            state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n            state->offset = offset;\n            return 1;\n        default:\n            state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n            state->offset = offset;\n            return 1;\n        }\n    }\n\n    state->data_size += offset - state->offset;\n\n    /* one more byte for null terminator ending the number string! */\n    state->data_size++;\n\n    /* update offset. */\n    state->offset = offset;\n\n    return 0;\n}\n\nstatic int M_GetValueSize(M_STATE *state, int is_global_object)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    const char *const src = state->src;\n    size_t offset;\n    const size_t size = state->size;\n\n    if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) {\n        state->dom_size += sizeof(JSON_VALUE_EX);\n    } else {\n        state->dom_size += sizeof(JSON_VALUE);\n    }\n\n    if (is_global_object) {\n        return M_GetObjectSize(state, /* is_global_object = */ 1);\n    } else {\n        if (M_SkipAllSkippables(state)) {\n            state->error = JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER;\n            return 1;\n        }\n\n        /* can cache offset now. */\n        offset = state->offset;\n\n        switch (src[offset]) {\n        case '\"':\n            return M_GetStringSize(state, 0);\n        case '\\'':\n            if (JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS & flags_bitset) {\n                return M_GetStringSize(state, 0);\n            } else {\n                /* invalid value! */\n                state->error = JSON_PARSE_ERROR_INVALID_VALUE;\n                return 1;\n            }\n        case '{':\n            return M_GetObjectSize(state, /* is_global_object = */ 0);\n        case '[':\n            return M_GetArraySize(state);\n        case '-':\n        case '0':\n        case '1':\n        case '2':\n        case '3':\n        case '4':\n        case '5':\n        case '6':\n        case '7':\n        case '8':\n        case '9':\n            return M_GetNumberSize(state);\n        case '+':\n            if (JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN & flags_bitset) {\n                return M_GetNumberSize(state);\n            } else {\n                /* invalid value! */\n                state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n                return 1;\n            }\n        case '.':\n            if (JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT\n                & flags_bitset) {\n                return M_GetNumberSize(state);\n            } else {\n                /* invalid value! */\n                state->error = JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT;\n                return 1;\n            }\n        default:\n            if ((offset + 4) <= size && 't' == src[offset + 0]\n                && 'r' == src[offset + 1] && 'u' == src[offset + 2]\n                && 'e' == src[offset + 3]) {\n                state->offset += 4;\n                return 0;\n            } else if (\n                (offset + 5) <= size && 'f' == src[offset + 0]\n                && 'a' == src[offset + 1] && 'l' == src[offset + 2]\n                && 's' == src[offset + 3] && 'e' == src[offset + 4]) {\n                state->offset += 5;\n                return 0;\n            } else if (\n                (offset + 4) <= size && 'n' == state->src[offset + 0]\n                && 'u' == state->src[offset + 1]\n                && 'l' == state->src[offset + 2]\n                && 'l' == state->src[offset + 3]) {\n                state->offset += 4;\n                return 0;\n            } else if (\n                (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset)\n                && (offset + 3) <= size && 'N' == src[offset + 0]\n                && 'a' == src[offset + 1] && 'N' == src[offset + 2]) {\n                return M_GetNumberSize(state);\n            } else if (\n                (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset)\n                && (offset + 8) <= size && 'I' == src[offset + 0]\n                && 'n' == src[offset + 1] && 'f' == src[offset + 2]\n                && 'i' == src[offset + 3] && 'n' == src[offset + 4]\n                && 'i' == src[offset + 5] && 't' == src[offset + 6]\n                && 'y' == src[offset + 7]) {\n                return M_GetNumberSize(state);\n            }\n\n            /* invalid value! */\n            state->error = JSON_PARSE_ERROR_INVALID_VALUE;\n            return 1;\n        }\n    }\n}\n\nstatic void M_HandleString(M_STATE *state, JSON_STRING *string)\n{\n    size_t offset = state->offset;\n    size_t bytes_written = 0;\n    const char *const src = state->src;\n    const char quote_to_use = '\\'' == src[offset] ? '\\'' : '\"';\n    char *data = state->data;\n    unsigned long high_surrogate = 0;\n    unsigned long codepoint;\n\n    string->ref_count = 1;\n    string->string = data;\n\n    /* skip leading '\"' or '\\''. */\n    offset++;\n\n    while (quote_to_use != src[offset]) {\n        if ('\\\\' == src[offset]) {\n            /* skip the reverse solidus. */\n            offset++;\n\n            switch (src[offset++]) {\n            default:\n                return; /* we cannot ever reach here. */\n            case 'u': {\n                codepoint = 0;\n                if (!M_HexValue(&src[offset], 4, &codepoint)) {\n                    return; /* this shouldn't happen as the value was already\n                             * validated.\n                             */\n                }\n\n                offset += 4;\n\n                if (codepoint <= 0x7fu) {\n                    data[bytes_written++] = (char)codepoint; /* 0xxxxxxx. */\n                } else if (codepoint <= 0x7ffu) {\n                    data[bytes_written++] =\n                        (char)(0xc0u | (codepoint >> 6)); /* 110xxxxx. */\n                    data[bytes_written++] =\n                        (char)(0x80u | (codepoint & 0x3fu)); /* 10xxxxxx. */\n                } else if (\n                    codepoint >= 0xd800 && codepoint <= 0xdbff) { /* high\n                                                                     surrogate.\n                                                                   */\n                    high_surrogate = codepoint;\n                    continue; /* we need the low half to form a complete\n                                 codepoint. */\n                } else if (\n                    codepoint >= 0xdc00 && codepoint <= 0xdfff) { /* low\n                                                                     surrogate.\n                                                                   */\n                    /* combine with the previously read half to obtain the\n                     * complete codepoint. */\n                    const unsigned long surrogate_offset =\n                        0x10000u - (0xD800u << 10) - 0xDC00u;\n                    codepoint =\n                        (high_surrogate << 10) + codepoint + surrogate_offset;\n                    high_surrogate = 0;\n                    data[bytes_written++] =\n                        (char)(0xF0u | (codepoint >> 18)); /* 11110xxx. */\n                    data[bytes_written++] =\n                        (char)(0x80u\n                               | ((codepoint >> 12) & 0x3fu)); /* 10xxxxxx.\n                                                                */\n                    data[bytes_written++] =\n                        (char)(0x80u | ((codepoint >> 6) & 0x3fu)); /* 10xxxxxx.\n                                                                     */\n                    data[bytes_written++] =\n                        (char)(0x80u | (codepoint & 0x3fu)); /* 10xxxxxx. */\n                } else {\n                    /* we assume the value was validated and thus is within the\n                     * valid range. */\n                    data[bytes_written++] =\n                        (char)(0xe0u | (codepoint >> 12)); /* 1110xxxx. */\n                    data[bytes_written++] =\n                        (char)(0x80u | ((codepoint >> 6) & 0x3fu)); /* 10xxxxxx.\n                                                                     */\n                    data[bytes_written++] =\n                        (char)(0x80u | (codepoint & 0x3fu)); /* 10xxxxxx. */\n                }\n            } break;\n            case '\"':\n                data[bytes_written++] = '\"';\n                break;\n            case '\\\\':\n                data[bytes_written++] = '\\\\';\n                break;\n            case '/':\n                data[bytes_written++] = '/';\n                break;\n            case 'b':\n                data[bytes_written++] = '\\b';\n                break;\n            case 'f':\n                data[bytes_written++] = '\\f';\n                break;\n            case 'n':\n                data[bytes_written++] = '\\n';\n                break;\n            case 'r':\n                data[bytes_written++] = '\\r';\n                break;\n            case 't':\n                data[bytes_written++] = '\\t';\n                break;\n            case '\\r':\n                data[bytes_written++] = '\\r';\n\n                /* check if we have a \"\\r\\n\" sequence. */\n                if ('\\n' == src[offset]) {\n                    data[bytes_written++] = '\\n';\n                    offset++;\n                }\n\n                break;\n            case '\\n':\n                data[bytes_written++] = '\\n';\n                break;\n            }\n        } else {\n            /* copy the character. */\n            data[bytes_written++] = src[offset++];\n        }\n    }\n\n    /* skip trailing '\"' or '\\''. */\n    offset++;\n\n    /* record the size of the string. */\n    string->string_size = bytes_written;\n\n    /* add null terminator to string. */\n    data[bytes_written++] = '\\0';\n\n    /* move data along. */\n    state->data += bytes_written;\n\n    /* update offset. */\n    state->offset = offset;\n}\n\nstatic void M_HandleKey(M_STATE *state, JSON_STRING *string)\n{\n    if (JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS & state->flags_bitset) {\n        const char *const src = state->src;\n        char *const data = state->data;\n        size_t offset = state->offset;\n\n        /* if we are allowing unquoted keys, check for quoted anyway... */\n        if (('\"' == src[offset]) || ('\\'' == src[offset])) {\n            /* ... if we got a quote, just parse the key as a string as normal.\n             */\n            M_HandleString(state, string);\n        } else {\n            size_t size = 0;\n\n            string->ref_count = 1;\n            string->string = state->data;\n\n            while (M_IsValidUnquotedKeyChar(src[offset])) {\n                data[size++] = src[offset++];\n            }\n\n            /* add null terminator to string. */\n            data[size] = '\\0';\n\n            /* record the size of the string. */\n            string->string_size = size++;\n\n            /* move data along. */\n            state->data += size;\n\n            /* update offset. */\n            state->offset = offset;\n        }\n    } else {\n        /* we are only allowed to have quoted keys, so just parse a string! */\n        M_HandleString(state, string);\n    }\n}\n\nstatic void M_HandleObject(\n    M_STATE *state, int is_global_object, JSON_OBJECT *object)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    const size_t size = state->size;\n    const char *const src = state->src;\n    size_t elements = 0;\n    int allow_comma = 0;\n    JSON_OBJECT_ELEMENT *previous = nullptr;\n\n    if (is_global_object) {\n        /* if we skipped some whitespace, and then found an opening '{' of an.\n         */\n        /* object, we actually have a normal JSON object at the root of the\n         * DOM...\n         */\n        if ('{' == src[state->offset]) {\n            /* . and we don't actually have a global object after all! */\n            is_global_object = 0;\n        }\n    }\n\n    if (!is_global_object) {\n        /* skip leading '{'. */\n        state->offset++;\n    }\n\n    M_SkipAllSkippables(state);\n\n    /* reset elements. */\n    elements = 0;\n\n    while (state->offset < size) {\n        JSON_OBJECT_ELEMENT *element = nullptr;\n        JSON_STRING *string = nullptr;\n        JSON_VALUE *value = nullptr;\n\n        if (!is_global_object) {\n            M_SkipAllSkippables(state);\n\n            if ('}' == src[state->offset]) {\n                /* skip trailing '}'. */\n                state->offset++;\n\n                /* finished the object! */\n                break;\n            }\n        } else {\n            if (M_SkipAllSkippables(state)) {\n                /* global object ends when the file ends! */\n                break;\n            }\n        }\n\n        /* if we parsed at least one element previously, grok for a comma. */\n        if (allow_comma) {\n            if (',' == src[state->offset]) {\n                /* skip comma. */\n                state->offset++;\n                allow_comma = 0;\n                continue;\n            }\n        }\n\n        element = (JSON_OBJECT_ELEMENT *)state->dom;\n        element->ref_count = 1;\n\n        state->dom += sizeof(JSON_OBJECT_ELEMENT);\n\n        if (nullptr == previous) {\n            /* this is our first element, so record it in our object. */\n            object->start = element;\n        } else {\n            previous->next = element;\n        }\n\n        previous = element;\n\n        if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) {\n            JSON_STRING_EX *string_ex = (JSON_STRING_EX *)state->dom;\n            state->dom += sizeof(JSON_STRING_EX);\n\n            string_ex->offset = state->offset;\n            string_ex->line_no = state->line_no;\n            string_ex->row_no = state->offset - state->line_offset;\n\n            string = &(string_ex->string);\n        } else {\n            string = (JSON_STRING *)state->dom;\n            state->dom += sizeof(JSON_STRING);\n        }\n\n        element->name = string;\n\n        M_HandleKey(state, string);\n\n        M_SkipAllSkippables(state);\n\n        /* skip colon or equals. */\n        state->offset++;\n\n        M_SkipAllSkippables(state);\n\n        if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & flags_bitset) {\n            JSON_VALUE_EX *value_ex = (JSON_VALUE_EX *)state->dom;\n            state->dom += sizeof(JSON_VALUE_EX);\n\n            value_ex->offset = state->offset;\n            value_ex->line_no = state->line_no;\n            value_ex->row_no = state->offset - state->line_offset;\n\n            value = &(value_ex->value);\n        } else {\n            value = (JSON_VALUE *)state->dom;\n            state->dom += sizeof(JSON_VALUE);\n        }\n\n        element->value = value;\n\n        M_HandleValue(state, /* is_global_object = */ 0, value);\n\n        /* successfully parsed a name/value pair! */\n        elements++;\n        allow_comma = 1;\n    }\n\n    /* if we had at least one element, end the linked list. */\n    if (previous) {\n        previous->next = nullptr;\n    }\n\n    if (elements == 0) {\n        object->start = nullptr;\n    }\n\n    object->ref_count = 1;\n    object->length = elements;\n}\n\nstatic void M_HandleArray(M_STATE *state, JSON_ARRAY *array)\n{\n    const char *const src = state->src;\n    const size_t size = state->size;\n    size_t elements = 0;\n    int allow_comma = 0;\n    JSON_ARRAY_ELEMENT *previous = nullptr;\n\n    /* skip leading '['. */\n    state->offset++;\n\n    M_SkipAllSkippables(state);\n\n    /* reset elements. */\n    elements = 0;\n\n    do {\n        JSON_ARRAY_ELEMENT *element = nullptr;\n        JSON_VALUE *value = nullptr;\n\n        M_SkipAllSkippables(state);\n\n        if (']' == src[state->offset]) {\n            /* skip trailing ']'. */\n            state->offset++;\n\n            /* finished the array! */\n            break;\n        }\n\n        /* if we parsed at least one element previously, grok for a comma. */\n        if (allow_comma) {\n            if (',' == src[state->offset]) {\n                /* skip comma. */\n                state->offset++;\n                allow_comma = 0;\n                continue;\n            }\n        }\n\n        element = (JSON_ARRAY_ELEMENT *)state->dom;\n        element->ref_count = 1;\n\n        state->dom += sizeof(JSON_ARRAY_ELEMENT);\n\n        if (nullptr == previous) {\n            /* this is our first element, so record it in our array. */\n            array->start = element;\n        } else {\n            previous->next = element;\n        }\n\n        previous = element;\n\n        if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & state->flags_bitset) {\n            JSON_VALUE_EX *value_ex = (JSON_VALUE_EX *)state->dom;\n            state->dom += sizeof(JSON_VALUE_EX);\n\n            value_ex->offset = state->offset;\n            value_ex->line_no = state->line_no;\n            value_ex->row_no = state->offset - state->line_offset;\n\n            value = &(value_ex->value);\n        } else {\n            value = (JSON_VALUE *)state->dom;\n            state->dom += sizeof(JSON_VALUE);\n        }\n\n        element->value = value;\n\n        M_HandleValue(state, /* is_global_object = */ 0, value);\n\n        /* successfully parsed an array element! */\n        elements++;\n        allow_comma = 1;\n    } while (state->offset < size);\n\n    /* end the linked list. */\n    if (previous) {\n        previous->next = nullptr;\n    }\n\n    if (elements == 0) {\n        array->start = nullptr;\n    }\n\n    array->ref_count = 1;\n    array->length = elements;\n}\n\nstatic void M_HandleNumber(M_STATE *state, JSON_NUMBER *number)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    size_t offset = state->offset;\n    const size_t size = state->size;\n    size_t bytes_written = 0;\n    const char *const src = state->src;\n    char *data = state->data;\n\n    number->ref_count = 1;\n    number->number = data;\n\n    if (JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS & flags_bitset) {\n        if (('0' == src[offset])\n            && (('x' == src[offset + 1]) || ('X' == src[offset + 1]))) {\n            /* consume hexadecimal digits. */\n            while ((offset < size)\n                   && (('0' <= src[offset] && src[offset] <= '9')\n                       || ('a' <= src[offset] && src[offset] <= 'f')\n                       || ('A' <= src[offset] && src[offset] <= 'F')\n                       || ('x' == src[offset]) || ('X' == src[offset]))) {\n                data[bytes_written++] = src[offset++];\n            }\n        }\n    }\n\n    if (JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS & flags_bitset) {\n        if (('0' == src[offset])\n            && (('b' == src[offset + 1]) || ('b' == src[offset + 1]))) {\n            /* consume binary digits. */\n            while ((offset < size)\n                   && (('0' <= src[offset] && src[offset] <= '1')\n                       || ('b' == src[offset]) || ('B' == src[offset]))) {\n                data[bytes_written++] = src[offset++];\n            }\n        }\n    }\n\n    while (offset < size) {\n        int end = 0;\n\n        switch (src[offset]) {\n        case '0':\n        case '1':\n        case '2':\n        case '3':\n        case '4':\n        case '5':\n        case '6':\n        case '7':\n        case '8':\n        case '9':\n        case '.':\n        case 'e':\n        case 'E':\n        case '+':\n        case '-':\n            data[bytes_written++] = src[offset++];\n            break;\n        default:\n            end = 1;\n            break;\n        }\n\n        if (0 != end) {\n            break;\n        }\n    }\n\n    if (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset) {\n        const size_t inf_strlen = 8; /* = strlen(\"Infinity\");. */\n        const size_t nan_strlen = 3; /* = strlen(\"NaN\");. */\n\n        if (offset + inf_strlen < size) {\n            if ('I' == src[offset]) {\n                size_t i;\n                /* We found our special 'Infinity' keyword! */\n                for (i = 0; i < inf_strlen; i++) {\n                    data[bytes_written++] = src[offset++];\n                }\n            }\n        }\n\n        if (offset + nan_strlen < size) {\n            if ('N' == src[offset]) {\n                size_t i;\n                /* We found our special 'NaN' keyword! */\n                for (i = 0; i < nan_strlen; i++) {\n                    data[bytes_written++] = src[offset++];\n                }\n            }\n        }\n    }\n\n    /* record the size of the number. */\n    number->number_size = bytes_written;\n    /* add null terminator to number string. */\n    data[bytes_written++] = '\\0';\n    /* move data along. */\n    state->data += bytes_written;\n    /* update offset. */\n    state->offset = offset;\n}\n\nstatic void M_HandleValue(\n    M_STATE *state, int is_global_object, JSON_VALUE *value)\n{\n    const size_t flags_bitset = state->flags_bitset;\n    const char *const src = state->src;\n    const size_t size = state->size;\n    size_t offset;\n\n    M_SkipAllSkippables(state);\n\n    /* cache offset now. */\n    offset = state->offset;\n\n    if (is_global_object) {\n        value->type = JSON_TYPE_OBJECT;\n        value->payload = state->dom;\n        state->dom += sizeof(JSON_OBJECT);\n        M_HandleObject(\n            state, /* is_global_object = */ 1, (JSON_OBJECT *)value->payload);\n    } else {\n        value->ref_count = 1;\n        switch (src[offset]) {\n        case '\"':\n        case '\\'':\n            value->type = JSON_TYPE_STRING;\n            value->payload = state->dom;\n            state->dom += sizeof(JSON_STRING);\n            M_HandleString(state, (JSON_STRING *)value->payload);\n            break;\n        case '{':\n            value->type = JSON_TYPE_OBJECT;\n            value->payload = state->dom;\n            state->dom += sizeof(JSON_OBJECT);\n            M_HandleObject(\n                state, /* is_global_object = */ 0,\n                (JSON_OBJECT *)value->payload);\n            break;\n        case '[':\n            value->type = JSON_TYPE_ARRAY;\n            value->payload = state->dom;\n            state->dom += sizeof(JSON_ARRAY);\n            M_HandleArray(state, (JSON_ARRAY *)value->payload);\n            break;\n        case '-':\n        case '+':\n        case '0':\n        case '1':\n        case '2':\n        case '3':\n        case '4':\n        case '5':\n        case '6':\n        case '7':\n        case '8':\n        case '9':\n        case '.':\n            value->type = JSON_TYPE_NUMBER;\n            value->payload = state->dom;\n            state->dom += sizeof(JSON_NUMBER);\n            M_HandleNumber(state, (JSON_NUMBER *)value->payload);\n            break;\n        default:\n            if ((offset + 4) <= size && 't' == src[offset + 0]\n                && 'r' == src[offset + 1] && 'u' == src[offset + 2]\n                && 'e' == src[offset + 3]) {\n                value->type = JSON_TYPE_TRUE;\n                value->payload = nullptr;\n                state->offset += 4;\n            } else if (\n                (offset + 5) <= size && 'f' == src[offset + 0]\n                && 'a' == src[offset + 1] && 'l' == src[offset + 2]\n                && 's' == src[offset + 3] && 'e' == src[offset + 4]) {\n                value->type = JSON_TYPE_FALSE;\n                value->payload = nullptr;\n                state->offset += 5;\n            } else if (\n                (offset + 4) <= size && 'n' == src[offset + 0]\n                && 'u' == src[offset + 1] && 'l' == src[offset + 2]\n                && 'l' == src[offset + 3]) {\n                value->type = JSON_TYPE_NULL;\n                value->payload = nullptr;\n                state->offset += 4;\n            } else if (\n                (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset)\n                && (offset + 3) <= size && 'N' == src[offset + 0]\n                && 'a' == src[offset + 1] && 'N' == src[offset + 2]) {\n                value->type = JSON_TYPE_NUMBER;\n                value->payload = state->dom;\n                state->dom += sizeof(JSON_NUMBER);\n                M_HandleNumber(state, (JSON_NUMBER *)value->payload);\n            } else if (\n                (JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN & flags_bitset)\n                && (offset + 8) <= size && 'I' == src[offset + 0]\n                && 'n' == src[offset + 1] && 'f' == src[offset + 2]\n                && 'i' == src[offset + 3] && 'n' == src[offset + 4]\n                && 'i' == src[offset + 5] && 't' == src[offset + 6]\n                && 'y' == src[offset + 7]) {\n                value->type = JSON_TYPE_NUMBER;\n                value->payload = state->dom;\n                state->dom += sizeof(JSON_NUMBER);\n                M_HandleNumber(state, (JSON_NUMBER *)value->payload);\n            }\n            break;\n        }\n    }\n}\n\nJSON_VALUE *JSON_Parse(const void *src, size_t src_size)\n{\n    return JSON_ParseEx(\n        src, src_size, JSON_PARSE_FLAGS_DEFAULT, nullptr, nullptr, nullptr);\n}\n\nJSON_VALUE *JSON_ParseEx(\n    const void *src, size_t src_size, size_t flags_bitset,\n    void *(*alloc_func_ptr)(void *user_data, size_t size), void *user_data,\n    JSON_PARSE_RESULT *result)\n{\n    M_STATE state;\n    void *allocation;\n    JSON_VALUE *value;\n    size_t total_size;\n    int input_error;\n\n    if (result) {\n        result->error = JSON_PARSE_ERROR_NONE;\n        result->error_offset = 0;\n        result->error_line_no = 0;\n        result->error_row_no = 0;\n    }\n\n    if (nullptr == src) {\n        /* invalid src pointer was null! */\n        return nullptr;\n    }\n\n    state.src = (const char *)src;\n    state.size = src_size;\n    state.offset = 0;\n    state.line_no = 1;\n    state.line_offset = 0;\n    state.error = JSON_PARSE_ERROR_NONE;\n    state.dom_size = 0;\n    state.data_size = 0;\n    state.flags_bitset = flags_bitset;\n\n    input_error = M_GetValueSize(\n        &state,\n        (int)(JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT & state.flags_bitset));\n\n    if (input_error == 0) {\n        M_SkipAllSkippables(&state);\n\n        if (state.offset != state.size) {\n            /* our parsing didn't have an error, but there are characters\n             * remaining in the input that weren't part of the JSON! */\n\n            state.error = JSON_PARSE_ERROR_UNEXPECTED_TRAILING_CHARACTERS;\n            input_error = 1;\n        }\n    }\n\n    if (input_error) {\n        /* parsing value's size failed (most likely an invalid JSON DOM!). */\n        if (result) {\n            result->error = state.error;\n            result->error_offset = state.offset;\n            result->error_line_no = state.line_no;\n            result->error_row_no = state.offset - state.line_offset;\n        }\n        return nullptr;\n    }\n\n    /* our total allocation is the combination of the dom and data sizes (we. */\n    /* first encode the structure of the JSON, and then the data referenced by.\n     */\n    /* the JSON values). */\n    total_size = state.dom_size + state.data_size;\n\n    if (nullptr == alloc_func_ptr) {\n        allocation = Memory_Alloc(total_size);\n    } else {\n        allocation = alloc_func_ptr(user_data, total_size);\n    }\n\n    if (nullptr == allocation) {\n        /* malloc failed! */\n        if (result) {\n            result->error = JSON_PARSE_ERROR_ALLOCATOR_FAILED;\n            result->error_offset = 0;\n            result->error_line_no = 0;\n            result->error_row_no = 0;\n        }\n\n        return nullptr;\n    }\n\n    /* reset offset so we can reuse it. */\n    state.offset = 0;\n\n    /* reset the line information so we can reuse it. */\n    state.line_no = 1;\n    state.line_offset = 0;\n\n    state.dom = (char *)allocation;\n    state.data = state.dom + state.dom_size;\n\n    if (JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION & state.flags_bitset) {\n        JSON_VALUE_EX *value_ex = (JSON_VALUE_EX *)state.dom;\n        state.dom += sizeof(JSON_VALUE_EX);\n\n        value_ex->offset = state.offset;\n        value_ex->line_no = state.line_no;\n        value_ex->row_no = state.offset - state.line_offset;\n\n        value = &(value_ex->value);\n    } else {\n        value = (JSON_VALUE *)state.dom;\n        state.dom += sizeof(JSON_VALUE);\n    }\n\n    M_HandleValue(\n        &state,\n        (int)(JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT & state.flags_bitset),\n        value);\n\n    ((JSON_VALUE *)allocation)->ref_count = 0;\n\n    return (JSON_VALUE *)allocation;\n}\n\nconst char *JSON_GetErrorDescription(JSON_PARSE_ERROR error)\n{\n    switch (error) {\n    case JSON_PARSE_ERROR_NONE:\n        return \"no error\";\n\n    case JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET:\n        return \"expected comma or closing bracket\";\n\n    case JSON_PARSE_ERROR_EXPECTED_COLON:\n        return \"expected colon\";\n\n    case JSON_PARSE_ERROR_EXPECTED_OPENING_QUOTE:\n        return \"expected opening quote\";\n\n    case JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE:\n        return \"invalid string escape sequence\";\n\n    case JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT:\n        return \"invalid number format\";\n\n    case JSON_PARSE_ERROR_INVALID_VALUE:\n        return \"invalid value\";\n\n    case JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER:\n        return \"premature end of buffer\";\n\n    case JSON_PARSE_ERROR_INVALID_STRING:\n        return \"allocator failed\";\n\n    case JSON_PARSE_ERROR_ALLOCATOR_FAILED:\n        return \"allocator failed\";\n\n    case JSON_PARSE_ERROR_UNEXPECTED_TRAILING_CHARACTERS:\n        return \"unexpected trailing characters\";\n\n    case JSON_PARSE_ERROR_UNKNOWN:\n    default:\n        return \"unknown\";\n    }\n}\n"
  },
  {
    "path": "src/trx/core/json/parse.h",
    "content": "#pragma once\n\n#include <trx/core/json/base.h>\n\ntypedef enum {\n    JSON_PARSE_ERROR_NONE = 0,\n    JSON_PARSE_ERROR_EXPECTED_COMMA_OR_CLOSING_BRACKET,\n    JSON_PARSE_ERROR_EXPECTED_COLON,\n    JSON_PARSE_ERROR_EXPECTED_OPENING_QUOTE,\n    JSON_PARSE_ERROR_INVALID_STRING_ESCAPE_SEQUENCE,\n    JSON_PARSE_ERROR_INVALID_NUMBER_FORMAT,\n    JSON_PARSE_ERROR_INVALID_VALUE,\n    JSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER,\n    JSON_PARSE_ERROR_INVALID_STRING,\n    JSON_PARSE_ERROR_ALLOCATOR_FAILED,\n    JSON_PARSE_ERROR_UNEXPECTED_TRAILING_CHARACTERS,\n    JSON_PARSE_ERROR_UNKNOWN\n} JSON_PARSE_ERROR;\n\ntypedef enum {\n    JSON_PARSE_FLAGS_DEFAULT = 0,\n\n    /* allow trailing commas in objects and arrays. For example, both [true,]\n       and\n       {\"a\" : null,} would be allowed with this option on. */\n    JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA = 0x1,\n\n    /* allow unquoted keys for objects. For example, {a : null} would be allowed\n       with this option on. */\n    JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS = 0x2,\n\n    /* allow a global unbracketed object. For example, a : null, b : true, c :\n       {} would be allowed with this option on. */\n    JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT = 0x4,\n\n    /* allow objects to use '=' instead of ':' between key/value pairs. For\n       example, a = null, b : true would be allowed with this option on. */\n    JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT = 0x8,\n\n    /* allow that objects don't have to have comma separators between key/value\n       pairs. */\n    JSON_PARSE_FLAGS_ALLOW_NO_COMMAS = 0x10,\n\n    /* allow c-style comments (either variants) to be ignored in the input JSON\n       file. */\n    JSON_PARSE_FLAGS_ALLOW_C_STYLE_COMMENTS = 0x20,\n\n    /* deprecated flag, unused. */\n    JSON_PARSE_FLAGS_DEPRECATED = 0x40,\n\n    /* record location information for each value. */\n    JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION = 0x80,\n\n    /* allow strings to be 'single quoted'. */\n    JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS = 0x100,\n\n    /* allow numbers to be binary. */\n    JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS = 0x4000,\n\n    /* allow numbers to be hexadecimal. */\n    JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS = 0x200,\n\n    /* allow numbers like +123 to be parsed. */\n    JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN = 0x400,\n\n    /* allow numbers like .0123 or 123. to be parsed. */\n    JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT = 0x800,\n\n    /* allow Infinity, -Infinity, NaN, -NaN. */\n    JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN = 0x1000,\n\n    /* allow multi line string values. */\n    JSON_PARSE_FLAGS_ALLOW_MULTI_LINE_STRINGS = 0x2000,\n\n    /* allow simplified JSON to be parsed. Simplified JSON is an enabling of a\n       set of other parsing options. */\n    JSON_PARSE_FLAGS_ALLOW_SIMPLIFIED_JSON =\n        (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA\n         | JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS\n         | JSON_PARSE_FLAGS_ALLOW_GLOBAL_OBJECT\n         | JSON_PARSE_FLAGS_ALLOW_EQUALS_IN_OBJECT\n         | JSON_PARSE_FLAGS_ALLOW_NO_COMMAS),\n\n    /* allow JSON5 to be parsed. JSON5 is an enabling of a set of other parsing\n       options. */\n    JSON_PARSE_FLAGS_ALLOW_JSON5 =\n        (JSON_PARSE_FLAGS_ALLOW_TRAILING_COMMA\n         | JSON_PARSE_FLAGS_ALLOW_UNQUOTED_KEYS\n         | JSON_PARSE_FLAGS_ALLOW_C_STYLE_COMMENTS\n         | JSON_PARSE_FLAGS_ALLOW_SINGLE_QUOTED_STRINGS\n         | JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS\n         | JSON_PARSE_FLAGS_ALLOW_BINARY_NUMBERS\n         | JSON_PARSE_FLAGS_ALLOW_LEADING_PLUS_SIGN\n         | JSON_PARSE_FLAGS_ALLOW_LEADING_OR_TRAILING_DECIMAL_POINT\n         | JSON_PARSE_FLAGS_ALLOW_INF_AND_NAN\n         | JSON_PARSE_FLAGS_ALLOW_MULTI_LINE_STRINGS)\n} JSON_PARSE_FLAGS;\n\n/* Parse a JSON text file, returning a pointer to the root of the JSON\n * structure. JSON_Parse performs 1 call to malloc for the entire encoding.\n * Returns 0 if an error occurred (malformed JSON input, or malloc failed). */\nJSON_VALUE *JSON_Parse(const void *src, size_t src_size);\n\n/* Parse a JSON text file, returning a pointer to the root of the JSON\n * structure. JSON_Parse performs 1 call to alloc_func_ptr for the entire\n * encoding. Returns 0 if an error occurred (malformed JSON input, or malloc\n * failed). If an error occurred, the result struct (if not nullptr) will\n * explain the type of error, and the location in the input it occurred. If\n * alloc_func_ptr is null then malloc is used. */\nJSON_VALUE *JSON_ParseEx(\n    const void *src, size_t src_size, size_t flags_bitset,\n    void *(*alloc_func_ptr)(void *, size_t), void *user_data,\n    JSON_PARSE_RESULT *result);\n\nconst char *JSON_GetErrorDescription(JSON_PARSE_ERROR error);\n"
  },
  {
    "path": "src/trx/core/json/types.h",
    "content": "#pragma once\n\n#define JSON_INVALID_BOOL -1\n#define JSON_INVALID_STRING nullptr\n#define JSON_INVALID_NUMBER 0x7FFFFFFF\n\n#include <inttypes.h>\n#include <stddef.h>\n#include <stdint.h>\n\n#define json_uintmax_t uintmax_t\n#define json_strtoumax strtoumax\n\n#define JSON_CONST_DISPATCH(arg, ctype, call)                                  \\\n    _Generic(0 ? (arg) : (void *)1, const void *: (ctype)call, default: call)\n\ntypedef struct {\n    void *payload;\n    size_t type;\n    size_t ref_count;\n} JSON_VALUE;\n\ntypedef struct {\n    char *string;\n    size_t string_size;\n    size_t ref_count;\n} JSON_STRING;\n\ntypedef struct {\n    JSON_STRING string;\n    size_t offset;\n    size_t line_no;\n    size_t row_no;\n} JSON_STRING_EX;\n\ntypedef struct {\n    char *number;\n    size_t number_size;\n    size_t ref_count;\n} JSON_NUMBER;\n\ntypedef struct JSON_OBJECT_ELEMENT {\n    JSON_STRING *name;\n    JSON_VALUE *value;\n    struct JSON_OBJECT_ELEMENT *next;\n    size_t ref_count;\n} JSON_OBJECT_ELEMENT;\n\ntypedef struct {\n    JSON_OBJECT_ELEMENT *start;\n    size_t length;\n    size_t ref_count;\n} JSON_OBJECT;\n\ntypedef struct JSON_ARRAY_ELEMENT {\n    JSON_VALUE *value;\n    struct JSON_ARRAY_ELEMENT *next;\n    size_t ref_count;\n} JSON_ARRAY_ELEMENT;\n\ntypedef struct {\n    JSON_ARRAY_ELEMENT *start;\n    size_t length;\n    size_t ref_count;\n} JSON_ARRAY;\n\ntypedef struct {\n    JSON_VALUE value;\n    size_t offset;\n    size_t line_no;\n    size_t row_no;\n} JSON_VALUE_EX;\n\ntypedef struct {\n    size_t error;\n    size_t error_offset;\n    size_t error_line_no;\n    size_t error_row_no;\n} JSON_PARSE_RESULT;\n"
  },
  {
    "path": "src/trx/core/json/util/file.c",
    "content": "#include <trx/core/json/util/file.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/json.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/shell.h>\n\n#include <string.h>\n\n#define M_PARSE_FLAGS                                                          \\\n    (JSON_PARSE_FLAGS_ALLOW_JSON5 | JSON_PARSE_FLAGS_ALLOW_LOCATION_INFORMATION)\n\nvoid JSONFile_ExitWithReadIOError(\n    const JSON_READ_IO *const io, const char *const fallback_message)\n{\n    const char *const error = JSON_ReadIO_GetError(io);\n    if (error != nullptr && error[0] != '\\0') {\n        char log_message[1024];\n        char dialog_message[1024];\n        JSON_ReadIO_FormatError(io, false, log_message, sizeof(log_message));\n        JSON_ReadIO_FormatError(\n            io, true, dialog_message, sizeof(dialog_message));\n        Shell_ExitSystemEx(log_message, dialog_message);\n    }\n    Shell_ExitSystem(fallback_message);\n}\n\nJSON_VALUE *JSONFile_Read(const char *path)\n{\n    return JSONFile_ReadEx(path, false);\n}\n\nJSON_VALUE *JSONFile_ReadEx(const char *path, const bool exit_on_error)\n{\n    char *file_data = nullptr;\n    if (!File_Load(path, &file_data, nullptr)) {\n        return nullptr;\n    }\n\n    JSON_PARSE_RESULT pr;\n    JSON_VALUE *const value = JSON_ParseEx(\n        file_data, strlen(file_data), M_PARSE_FLAGS, nullptr, nullptr, &pr);\n    if (value == nullptr) {\n        JSON_READ_IO *const io = JSON_ReadIO_Create(nullptr, 0, path);\n        JSON_ReadIO_SetErrorAt(\n            io, pr.error_line_no, pr.error_row_no, \"%s\",\n            JSON_GetErrorDescription(pr.error));\n        if (exit_on_error) {\n            JSONFile_ExitWithReadIOError(io, \"JSON parse error\");\n        } else {\n            char log_message[1024];\n            JSON_ReadIO_FormatError(\n                io, false, log_message, sizeof(log_message));\n            LOG_ERROR(\"%s\", log_message);\n        }\n        JSON_ReadIO_Destroy(io);\n    }\n    Memory_FreePointer(&file_data);\n    return value;\n}\n\nbool JSONFile_Write(const char *path, JSON_VALUE *const value)\n{\n    char *old_data = nullptr;\n    File_Load(path, &old_data, nullptr);\n\n    size_t out_len;\n    char *out_data = JSON_WritePretty(value, \"  \", \"\\n\", &out_len);\n\n    bool updated = false;\n    if (old_data == nullptr || strcmp(old_data, out_data) != 0) {\n        MYFILE *const fp = File_Open(path, FILE_OPEN_WRITE);\n        if (fp == nullptr) {\n            LOG_ERROR(\"unable to open '%s' for writing\", path);\n        } else {\n            LOG_DEBUG(\"saving JSON to %s\", path);\n            File_WriteData(fp, out_data, out_len - 1); // w/o \\0\n            File_Close(fp);\n            updated = true;\n        }\n    }\n\n    Memory_FreePointer(&old_data);\n    Memory_FreePointer(&out_data);\n    return updated;\n}\n"
  },
  {
    "path": "src/trx/core/json/util/file.h",
    "content": "#pragma once\n\n#include <trx/core/json.h>\n\ntypedef struct JSON_READ_IO JSON_READ_IO;\n\n// Read and parse a JSON5 file. Missing files will return nullptr.\n// @param path  Path to read.\n// @return      The root JSON_VALUE, or nullptr on I/O/parse failure. Caller\n//              must free the result with JSON_ValueFree().\nJSON_VALUE *JSONFile_Read(const char *path);\n\n// Like JSONFile_Read(), except optionally exits on parse error.\nJSON_VALUE *JSONFile_ReadEx(const char *path, bool exit_on_error);\n\n// Format and hard-exit with the JSON read error details.\nvoid JSONFile_ExitWithReadIOError(\n    const JSON_READ_IO *io, const char *fallback_message);\n\n// Write a JSON_VALUE to disk (pretty-printed), overwriting only if changed.\n// @param path  Path to read.\n// @param root  Value to write to the file.\n// @return      Returns true if the file was written; false on error or no-op.\nbool JSONFile_Write(const char *path, JSON_VALUE *root);\n"
  },
  {
    "path": "src/trx/core/json/util/read_io.c",
    "content": "#include <trx/core/json/util/read_io.h>\n\n#include <trx/core/json.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n\n#include <stdarg.h>\n#include <stdio.h>\n#include <string.h>\n\n#define M_MAX_STACK_SIZE 10\n\ntypedef struct JSON_READ_IO {\n    char source_path[256];\n    char path[256];\n    int32_t path_index_stack[M_MAX_STACK_SIZE];\n    int32_t path_top;\n    char error_msg[256];\n    char error_path[256];\n    char error_body[256];\n    int32_t error_line;\n    int32_t error_col;\n    JSON_VALUE *stack[M_MAX_STACK_SIZE];\n    JSON_VALUE *current;\n    size_t current_pos;\n    uint16_t version;\n} JSON_READ_IO;\n\nstatic void M_SetErrorV(\n    JSON_READ_IO *const io, const int32_t line, const int32_t col,\n    const bool has_explicit_location, const char *const fmt, va_list ap)\n{\n    char body[256];\n    vsnprintf(body, sizeof(body), fmt, ap);\n    int32_t final_line = line;\n    int32_t final_col = col;\n    if (!has_explicit_location) {\n        final_line = -1;\n        final_col = -1;\n        if (io != nullptr && io->source_path[0] != '\\0'\n            && io->current != nullptr) {\n            // File-backed JSON parses in this codebase are location-enabled.\n            // Avoid probing non-file trees (e.g. BSON) where value_ex layout\n            // is not guaranteed.\n            const JSON_VALUE_EX *const value_ex =\n                (const JSON_VALUE_EX *)io->current;\n            const size_t offset = value_ex->offset;\n            const size_t line_val = value_ex->line_no;\n            const size_t col_val = value_ex->row_no;\n\n            if (offset <= 16 * 1024 * 1024 && line_val > 0\n                && line_val <= 1024 * 1024 && col_val <= 1024 * 1024) {\n                final_line = (int32_t)line_val;\n                final_col = (int32_t)col_val;\n            }\n        }\n    }\n\n    if (io != nullptr) {\n        io->error_line = final_line;\n        io->error_col = final_col;\n        snprintf(io->error_body, sizeof(io->error_body), \"%s\", body);\n        if (io->path[0] != '\\0') {\n            snprintf(io->error_path, sizeof(io->error_path), \"%s\", io->path);\n        } else {\n            io->error_path[0] = '\\0';\n        }\n    }\n    if (io == nullptr) {\n        return;\n    }\n    if (io->source_path[0] != '\\0') {\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wformat-truncation\"\n        if (final_line >= 0 && final_col >= 0) {\n            if (io->path[0] != '\\0') {\n                snprintf(\n                    io->error_msg, sizeof(io->error_msg),\n                    \"Error parsing '%s' (line %d, col %d): %s - %s\",\n                    io->source_path, final_line, final_col, io->path, body);\n            } else {\n                snprintf(\n                    io->error_msg, sizeof(io->error_msg),\n                    \"Error parsing '%s' (line %d, col %d): %s\", io->source_path,\n                    final_line, final_col, body);\n            }\n        } else {\n            if (io->path[0] != '\\0') {\n                snprintf(\n                    io->error_msg, sizeof(io->error_msg),\n                    \"Error parsing '%s': %s - %s\", io->source_path, io->path,\n                    body);\n            } else {\n                snprintf(\n                    io->error_msg, sizeof(io->error_msg),\n                    \"Error parsing '%s': %s\", io->source_path, body);\n            }\n        }\n#pragma GCC diagnostic pop\n    } else {\n        if (final_line >= 0 && final_col >= 0) {\n            snprintf(\n                io->error_msg, sizeof(io->error_msg),\n                \"(line %d, col %d): %.200s\", final_line, final_col, body);\n        } else {\n            snprintf(io->error_msg, sizeof(io->error_msg), \"%.200s\", body);\n        }\n    }\n}\n\nstatic void M_SetError(JSON_READ_IO *const io, const char *fmt, ...)\n{\n    va_list ap;\n    va_start(ap, fmt);\n    M_SetErrorV(io, -1, -1, false, fmt, ap);\n    va_end(ap);\n}\n\nstatic void M_SetErrorAt(\n    JSON_READ_IO *const io, const int32_t line, const int32_t col,\n    const char *fmt, ...)\n{\n    va_list ap;\n    va_start(ap, fmt);\n    M_SetErrorV(io, line, col, true, fmt, ap);\n    va_end(ap);\n}\n\nstatic bool M_PushPathKey(JSON_READ_IO *const io, const char *const key)\n{\n    if (io->path_top + 1 >= M_MAX_STACK_SIZE) {\n        return false;\n    }\n    const size_t pos = strlen(io->path);\n    io->path_index_stack[io->path_top++] = pos;\n    if (pos != 0) {\n        strncat(io->path, \".\", sizeof(io->path) - strlen(io->path) - 1);\n    }\n    strncat(io->path, key, sizeof(io->path) - strlen(io->path) - 1);\n    return true;\n}\n\nstatic bool M_PushPathIndex(JSON_READ_IO *const io, const size_t idx)\n{\n    if (io->path_top + 1 >= M_MAX_STACK_SIZE) {\n        return false;\n    }\n    io->path_index_stack[io->path_top++] = strlen(io->path);\n    char tmp[32];\n    snprintf(tmp, sizeof(tmp), \"[%zu]\", idx);\n    strncat(io->path, tmp, sizeof(io->path) - strlen(io->path) - 1);\n    return true;\n}\n\nstatic void M_PopPath(JSON_READ_IO *const io)\n{\n    if (io->path_top <= 0) {\n        io->path[0] = '\\0';\n        io->path_top = 0;\n        return;\n    }\n    int pos = io->path_index_stack[--io->path_top];\n    io->path[pos] = '\\0';\n}\n\nstatic bool M_PushValue(JSON_READ_IO *const io, JSON_VALUE *const value)\n{\n    if (value == nullptr) {\n        M_SetError(io, \"pushing null value\");\n        return false;\n    }\n    if (io->current_pos + 1 >= M_MAX_STACK_SIZE) {\n        M_SetError(io, \"stack overflow\");\n        return false;\n    }\n    io->current_pos++;\n    io->stack[io->current_pos] = value;\n    io->current = io->stack[io->current_pos];\n    return true;\n}\n\nstatic bool M_ReadBoolCurrent(JSON_READ_IO *const io, bool *const target)\n{\n    if (JSON_ValueIsTrue(io->current)) {\n        *target = true;\n        return true;\n    } else if (JSON_ValueIsFalse(io->current)) {\n        *target = false;\n        return true;\n    } else {\n        M_SetError(io, \"not a bool\");\n        return false;\n    }\n}\n\n#define L_DEFINE_M_READ_NUM_CURRENT(type_, name, minv, maxv)                   \\\n    static bool M_ReadNumCurrent_##name(                                       \\\n        JSON_READ_IO *const io, void *const target)                            \\\n    {                                                                          \\\n        if (io->current->type != JSON_TYPE_NUMBER) {                           \\\n            M_SetError(io, \"not a number\");                                    \\\n            return false;                                                      \\\n        }                                                                      \\\n        const long long val = JSON_ValueGetInt(io->current, 0);                \\\n        if (val < (long long)(minv) || val > (long long)(maxv)) {              \\\n            M_SetError(io, \"value out of range: %lld\", val);                   \\\n            return false;                                                      \\\n        }                                                                      \\\n        const type_ parsed = (type_)val;                                       \\\n        memcpy(target, &parsed, sizeof(parsed));                               \\\n        return true;                                                           \\\n    }\nL_DEFINE_M_READ_NUM_CURRENT(int8_t, S8, INT8_MIN, INT8_MAX)\nL_DEFINE_M_READ_NUM_CURRENT(int16_t, S16, INT16_MIN, INT16_MAX)\nL_DEFINE_M_READ_NUM_CURRENT(int32_t, S32, INT32_MIN, INT32_MAX)\nL_DEFINE_M_READ_NUM_CURRENT(uint8_t, U8, 0, UINT8_MAX)\nL_DEFINE_M_READ_NUM_CURRENT(uint16_t, U16, 0, UINT16_MAX)\nL_DEFINE_M_READ_NUM_CURRENT(uint32_t, U32, 0, UINT32_MAX)\n#undef L_DEFINE_M_READ_NUM_CURRENT\n\nstatic bool M_ReadNumCurrent_Double(\n    JSON_READ_IO *const io, double *const target)\n{\n    if (io->current->type != JSON_TYPE_NUMBER) {\n        M_SetError(io, \"not a number\");\n        return false;\n    }\n    const double val = JSON_ValueGetDouble(io->current, -1.0);\n    memcpy(target, &val, sizeof(val));\n    return true;\n}\n\nstatic bool M_ReadNumCurrent_Float(JSON_READ_IO *const io, float *const target)\n{\n    if (io->current->type != JSON_TYPE_NUMBER) {\n        M_SetError(io, \"not a number\");\n        return false;\n    }\n    const double val = JSON_ValueGetDouble(io->current, -1.0);\n    const float parsed = (float)val;\n    memcpy(target, &parsed, sizeof(parsed));\n    return true;\n}\n\nstatic bool M_ReadStringCurrent(\n    JSON_READ_IO *const io, const char **const target)\n{\n    if (io->current->type != JSON_TYPE_STRING) {\n        M_SetError(io, \"not a string\");\n        return false;\n    }\n    *target = JSON_ValueGetString(io->current, nullptr);\n    return *target != nullptr;\n}\n\nbool JSON_ReadIO_ReadXYZ32Current(\n    JSON_READ_IO *const io, void *const target_void)\n{\n    XYZ_32 *const target = target_void;\n    JSON_ARRAY *const tuple = JSON_ValueAsArray(io->current);\n    if (tuple != nullptr) {\n        const int32_t tuple_len = tuple->length;\n        if (tuple_len != 3) {\n            M_SetError(io, \"XYZ tuple must have exactly 3 values\");\n            JSON_FAIL();\n        }\n        JSON_MUST(JSON_READ_A(io, 0, &target->x));\n        JSON_MUST(JSON_READ_A(io, 1, &target->y));\n        JSON_MUST(JSON_READ_A(io, 2, &target->z));\n    } else {\n        JSON_MUST(JSON_READ(io, \"x\", &target->x));\n        JSON_MUST(JSON_READ(io, \"y\", &target->y));\n        JSON_MUST(JSON_READ(io, \"z\", &target->z));\n    }\n    JSON_FINISH();\n}\n\nbool JSON_ReadIO_ReadXYZ16Current(\n    JSON_READ_IO *const io, void *const target_void)\n{\n    XYZ_32 tmp;\n    JSON_MUST(JSON_ReadIO_ReadXYZ32Current(io, &tmp));\n    if (tmp.x < INT16_MIN || tmp.x > INT16_MAX || tmp.y < INT16_MIN\n        || tmp.y > INT16_MAX || tmp.z < INT16_MIN || tmp.z > INT16_MAX) {\n        M_SetError(io, \"XYZ16 value out of range\");\n        JSON_FAIL();\n    }\n    XYZ_16 *const target = target_void;\n    target->x = tmp.x;\n    target->y = tmp.y;\n    target->z = tmp.z;\n    JSON_FINISH();\n}\n\nstatic bool M_ReadRGB888Current(JSON_READ_IO *const io, RGB_888 *const target)\n{\n    JSON_ARRAY *const tuple = JSON_ValueAsArray(io->current);\n    if (tuple != nullptr) {\n        const int32_t tuple_len = tuple->length;\n        if (tuple_len != 3) {\n            M_SetError(io, \"RGB array must have exactly 3 values\");\n            JSON_FAIL();\n        }\n\n        RGB_F color = { -1.0f, -1.0f, -1.0f };\n        JSON_MUST(JSON_READ_A(io, 0, &color.r));\n        JSON_MUST(JSON_READ_A(io, 1, &color.g));\n        JSON_MUST(JSON_READ_A(io, 2, &color.b));\n        if (color.r < 0.0f || color.g < 0.0f || color.b < 0.0f || color.r > 1.0f\n            || color.g > 1.0f || color.b > 1.0f) {\n            M_SetError(io, \"RGB array values must be in range 0.0..1.0\");\n            JSON_FAIL();\n        }\n\n        *target = (RGB_888) {\n            (uint8_t)(color.r * 255.0f),\n            (uint8_t)(color.g * 255.0f),\n            (uint8_t)(color.b * 255.0f),\n        };\n    } else {\n        const char *str = nullptr;\n        JSON_MUST(JSON_READ_CURRENT(io, &str));\n        if (!String_ParseRGB888(str, target)) {\n            M_SetError(io, \"invalid RGB color string\");\n            JSON_FAIL();\n        }\n    }\n\n    JSON_FINISH();\n}\n\nstatic bool M_ReadRGBA8888Current(\n    JSON_READ_IO *const io, RGBA_8888 *const target)\n{\n    const char *str = nullptr;\n    JSON_MUST(JSON_READ_CURRENT(io, &str));\n    if (!String_ParseRGBA8888(str, target)) {\n        M_SetError(io, \"invalid RGBA color string\");\n        JSON_FAIL();\n    }\n    JSON_FINISH();\n}\n\n#define L_DEFINE_JSON_READ_IO_TYPE(name, ctype, impl_func)                     \\\n    bool JSON_ReadIO_Read##name##Current(                                      \\\n        JSON_READ_IO *const io, void *const target)                            \\\n    {                                                                          \\\n        return impl_func(io, (ctype *)target);                                 \\\n    }\nL_DEFINE_JSON_READ_IO_TYPE(Bool, bool, M_ReadBoolCurrent)\nL_DEFINE_JSON_READ_IO_TYPE(S8, int8_t, M_ReadNumCurrent_S8)\nL_DEFINE_JSON_READ_IO_TYPE(U8, uint8_t, M_ReadNumCurrent_U8)\nL_DEFINE_JSON_READ_IO_TYPE(S16, int16_t, M_ReadNumCurrent_S16)\nL_DEFINE_JSON_READ_IO_TYPE(U16, uint16_t, M_ReadNumCurrent_U16)\nL_DEFINE_JSON_READ_IO_TYPE(S32, int32_t, M_ReadNumCurrent_S32)\nL_DEFINE_JSON_READ_IO_TYPE(U32, uint32_t, M_ReadNumCurrent_U32)\nL_DEFINE_JSON_READ_IO_TYPE(Float, float, M_ReadNumCurrent_Float)\nL_DEFINE_JSON_READ_IO_TYPE(Double, double, M_ReadNumCurrent_Double)\nL_DEFINE_JSON_READ_IO_TYPE(String, const char *, M_ReadStringCurrent)\nL_DEFINE_JSON_READ_IO_TYPE(RGB888, RGB_888, M_ReadRGB888Current)\nL_DEFINE_JSON_READ_IO_TYPE(RGBA8888, RGBA_8888, M_ReadRGBA8888Current)\n#undef L_DEFINE_JSON_READ_IO_TYPE\n\nconst char *JSON_ReadIO_GetError(const JSON_READ_IO *const io)\n{\n    return io->error_msg;\n}\n\nconst char *JSON_ReadIO_GetErrorPath(const JSON_READ_IO *const io)\n{\n    return io->error_path;\n}\n\nconst char *JSON_ReadIO_GetErrorBody(const JSON_READ_IO *const io)\n{\n    return io->error_body;\n}\n\nint32_t JSON_ReadIO_GetErrorLine(const JSON_READ_IO *const io)\n{\n    return io->error_line;\n}\n\nint32_t JSON_ReadIO_GetErrorCol(const JSON_READ_IO *const io)\n{\n    return io->error_col;\n}\n\nuint16_t JSON_ReadIO_GetVersion(const JSON_READ_IO *const io)\n{\n    return io->version;\n}\n\nvoid JSON_ReadIO_FormatError(\n    const JSON_READ_IO *const io, const bool multiline, char *const buffer,\n    const size_t buffer_size)\n{\n    const char *const body = JSON_ReadIO_GetErrorBody(io);\n    const char *const path = JSON_ReadIO_GetErrorPath(io);\n    const char *const source_path = io->source_path;\n    const int32_t line = JSON_ReadIO_GetErrorLine(io);\n    const int32_t col = JSON_ReadIO_GetErrorCol(io);\n    const char *const separator = multiline ? \"\\n\" : \" \";\n\n    if (buffer_size == 0) {\n        return;\n    }\n\n    if (source_path == nullptr || source_path[0] == '\\0') {\n        snprintf(buffer, buffer_size, \"%s\", JSON_ReadIO_GetError(io));\n        return;\n    }\n\n    if (line >= 0 && col >= 0) {\n        if (path != nullptr && path[0] != '\\0') {\n            snprintf(\n                buffer, buffer_size,\n                \"Error parsing '%s' (line %d, col %d):%s%s - %s\", source_path,\n                line, col, separator, path, body);\n        } else {\n            snprintf(\n                buffer, buffer_size,\n                \"Error parsing '%s' (line %d, col %d):%s%s\", source_path, line,\n                col, separator, body);\n        }\n    } else {\n        if (path != nullptr && path[0] != '\\0') {\n            snprintf(\n                buffer, buffer_size, \"Error parsing '%s':%s%s - %s\",\n                source_path, separator, path, body);\n        } else {\n            snprintf(\n                buffer, buffer_size, \"Error parsing '%s':%s%s\", source_path,\n                separator, body);\n        }\n    }\n}\n\nvoid JSON_ReadIO_SetError(JSON_READ_IO *const io, const char *fmt, ...)\n{\n    va_list ap;\n    va_start(ap, fmt);\n    char body[256];\n    vsnprintf(body, sizeof(body), fmt, ap);\n    va_end(ap);\n    M_SetError(io, \"%s\", body);\n}\n\nvoid JSON_ReadIO_SetErrorAt(\n    JSON_READ_IO *const io, const int32_t line, const int32_t col,\n    const char *fmt, ...)\n{\n    va_list ap;\n    va_start(ap, fmt);\n    char body[256];\n    vsnprintf(body, sizeof(body), fmt, ap);\n    va_end(ap);\n    M_SetErrorAt(io, line, col, \"%s\", body);\n}\n\nbool JSON_ReadIO_PushObject(JSON_READ_IO *const io, const char *const key)\n{\n    JSON_OBJECT *const current_obj = JSON_ValueAsObject(io->current);\n    if (current_obj == nullptr) {\n        M_SetError(io, \"not an object\");\n        return false;\n    }\n    JSON_VALUE *const child = JSON_ObjectGetValue(current_obj, key);\n    if (child == nullptr) {\n        M_SetError(io, \"key does not exist: %s\", key);\n        return false;\n    }\n    if (!M_PushPathKey(io, key)) {\n        M_SetError(io, \"path depth overflow\");\n        return false;\n    }\n    return M_PushValue(io, child);\n}\n\nbool JSON_ReadIO_PushArrayElem(JSON_READ_IO *const io, const size_t index)\n{\n    JSON_ARRAY *const current_arr = JSON_ValueAsArray(io->current);\n    if (current_arr == nullptr) {\n        M_SetError(io, \"not an array\");\n        return false;\n    }\n    JSON_VALUE *const child = JSON_ArrayGetValue(current_arr, index);\n    if (child == nullptr) {\n        M_SetError(io, \"invalid array index\");\n        return false;\n    }\n    if (!M_PushPathIndex(io, index)) {\n        M_SetError(io, \"path depth overflow\");\n        return false;\n    }\n    return M_PushValue(io, child);\n}\n\nbool JSON_ReadIO_Pop(JSON_READ_IO *const io)\n{\n    if (io->current_pos == 0) {\n        M_SetError(io, \"pop from empty stack\");\n        return false;\n    }\n    io->current_pos--;\n    io->current = io->stack[io->current_pos];\n    M_PopPath(io);\n    return true;\n}\n\nint32_t JSON_ReadIO_GetArrayLength(JSON_READ_IO *const io)\n{\n    JSON_ARRAY *const arr = JSON_ValueAsArray(io->current);\n    if (arr == nullptr) {\n        M_SetError(io, \"not an array\");\n        return -1;\n    }\n    return arr->length;\n}\n\nbool JSON_ReadIO_HasKey(JSON_READ_IO *const io, const char *const key)\n{\n    JSON_OBJECT *const obj = JSON_ValueAsObject(io->current);\n    if (obj == nullptr) {\n        return false;\n    }\n    return JSON_ObjectContainsKey(obj, key);\n}\n\nJSON_OBJECT *JSON_ReadIO_GetCurrentObject(JSON_READ_IO *const io)\n{\n    return JSON_ValueAsObject(io->current);\n}\n\nJSON_VALUE *JSON_ReadIO_GetCurrentValue(JSON_READ_IO *const io)\n{\n    return io->current;\n}\n\nJSON_READ_IO *JSON_ReadIO_Create(\n    JSON_VALUE *const root, const uint16_t version,\n    const char *const source_path)\n{\n    JSON_READ_IO *const io = Memory_Alloc(sizeof(*io));\n    if (source_path != nullptr) {\n        snprintf(io->source_path, sizeof(io->source_path), \"%s\", source_path);\n    } else {\n        io->source_path[0] = '\\0';\n    }\n    io->stack[0] = root;\n    io->current_pos = 0;\n    io->current = io->stack[0];\n    io->version = version;\n    return io;\n}\n\nvoid JSON_ReadIO_Destroy(JSON_READ_IO *const io)\n{\n    Memory_Free(io);\n}\n"
  },
  {
    "path": "src/trx/core/json/util/read_io.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/json.h>\n#include <trx/core/log.h>\n#include <trx/core/math/types.h>\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef struct JSON_READ_IO JSON_READ_IO;\n\ntypedef struct {\n    void *tmp;\n} JSON_READ_IO_DUMMY;\n\n#define JSON_READ_IO_TYPE_LIST_BASE(X)                                         \\\n    X(Bool, bool)                                                              \\\n    X(S8, int8_t)                                                              \\\n    X(U8, uint8_t)                                                             \\\n    X(S16, int16_t)                                                            \\\n    X(U16, uint16_t)                                                           \\\n    X(S32, int32_t)                                                            \\\n    X(U32, uint32_t)                                                           \\\n    X(Float, float)                                                            \\\n    X(Double, double)                                                          \\\n    X(XYZ16, XYZ_16)                                                           \\\n    X(XYZ32, XYZ_32)                                                           \\\n    X(RGB888, RGB_888)                                                         \\\n    X(RGBA8888, RGBA_8888)                                                     \\\n    X(String, const char *)\n#define JSON_READ_IO_TYPE_LIST JSON_READ_IO_TYPE_LIST_BASE\n\n#define JSON_READ_IO_TYPE_TO_CURRENT_FN(name, ctype)                           \\\n    ctype:                                                                     \\\n    JSON_ReadIO_Read##name##Current,\n\n// ============================================================================\n// Public APIs\n\nJSON_READ_IO *JSON_ReadIO_Create(\n    JSON_VALUE *root, uint16_t version, const char *source_path);\nvoid JSON_ReadIO_Destroy(JSON_READ_IO *io);\n\nconst char *JSON_ReadIO_GetError(const JSON_READ_IO *io);\nconst char *JSON_ReadIO_GetErrorPath(const JSON_READ_IO *io);\nconst char *JSON_ReadIO_GetErrorBody(const JSON_READ_IO *io);\nint32_t JSON_ReadIO_GetErrorLine(const JSON_READ_IO *io);\nint32_t JSON_ReadIO_GetErrorCol(const JSON_READ_IO *io);\nuint16_t JSON_ReadIO_GetVersion(const JSON_READ_IO *io);\nvoid JSON_ReadIO_FormatError(\n    const JSON_READ_IO *io, bool multiline, char *buffer, size_t buffer_size);\n\nvoid JSON_ReadIO_SetError(JSON_READ_IO *io, const char *fmt, ...);\nvoid JSON_ReadIO_SetErrorAt(\n    JSON_READ_IO *io, int32_t line, int32_t col, const char *fmt, ...);\nbool JSON_ReadIO_PushObject(JSON_READ_IO *io, const char *key);\nbool JSON_ReadIO_PushArrayElem(JSON_READ_IO *io, size_t index);\nbool JSON_ReadIO_Pop(JSON_READ_IO *io);\nint32_t JSON_ReadIO_GetArrayLength(JSON_READ_IO *io);\nbool JSON_ReadIO_HasKey(JSON_READ_IO *io, const char *key);\nJSON_OBJECT *JSON_ReadIO_GetCurrentObject(JSON_READ_IO *io);\nJSON_VALUE *JSON_ReadIO_GetCurrentValue(JSON_READ_IO *io);\n\n#define L_DECLARE_JSON_READ_IO_TYPE(name, ctype)                               \\\n    bool JSON_ReadIO_Read##name##Current(JSON_READ_IO *io, void *target);\nJSON_READ_IO_TYPE_LIST(L_DECLARE_JSON_READ_IO_TYPE)\n#undef L_DECLARE_JSON_READ_IO_TYPE\n\n#define JSON_PUSH(io, key) JSON_ReadIO_PushObject((io), (key))\n#define JSON_PUSH_INDEX(io, idx) JSON_ReadIO_PushArrayElem((io), (idx))\n#define JSON_POP(io) JSON_ReadIO_Pop((io))\n#define JSON_ARRAY_LEN(io) JSON_ReadIO_GetArrayLength((io))\n\n// Read the value into target_ptr from the current stack value.\n#define JSON_READ_CURRENT(io, target_ptr)                                      \\\n    _Generic(                                                                  \\\n        *(target_ptr),                                                         \\\n        JSON_READ_IO_TYPE_LIST(JSON_READ_IO_TYPE_TO_CURRENT_FN)                \\\n            JSON_READ_IO_DUMMY: JSON_ReadIO_ReadS32Current)(                   \\\n        (io), (target_ptr))\n\n// Push a key onto stack, read the value into target_ptr, and pop the value.\n// Fails if the key is missing, or reading failed.\n#define JSON_READ(io, key, target_ptr)                                         \\\n    (JSON_PUSH((io), (key))                                                    \\\n         ? (JSON_READ_CURRENT((io), (target_ptr)) ? JSON_POP((io))             \\\n                                                  : (JSON_POP((io)), false))   \\\n         : false)\n\n// Like JSON_READ(), except also supports default value to fall back to.\n#define JSON_READ_D(io, key, target_ptr, default_value)                        \\\n    ((*(target_ptr) = (default_value)), JSON_READ((io), (key), (target_ptr)))\n\n// Like JSON_READ(), except push an array index instead of an object key.\n#define JSON_READ_A(io, idx, target_ptr)                                       \\\n    (JSON_PUSH_INDEX((io), (idx))                                              \\\n         ? (JSON_READ_CURRENT((io), (target_ptr)) ? JSON_POP((io))             \\\n                                                  : (JSON_POP((io)), false))   \\\n         : false)\n\n// ============================================================================\n// Control flow macros.\n\n// Users of this API are expected to consume JSON_ReadIO_GetError() in the\n// outermost scope. Example usage:\n//\n// static bool M_CustomSectionReader(JSON_READ_IO *io)\n// {\n//     int32_t foo_value;\n//     JSON_MUST(JSON_READ(io, \"foo\", &foo_value);\n//     JSON_FINISH();\n// }\n//\n// static void M_OuterReader(void)\n// {\n//     JSON_READ_IO *io = JSON_ReadIO_Create(…);\n//     bool success = M_CustomSectionReader(io);\n//     JSON_ReadIO_Destroy(io);\n// }\n\n// If the expr fails, log an error, and carry on.\n#define JSON_SHOULD(expr)                                                      \\\n    ((expr) ? 1 : (LOG_WARNING(\"%s\", JSON_ReadIO_GetError(io)), 0))\n\n// Do nothing.\n#define JSON_OPTIONAL(expr) (expr)\n\n// If the expr fails, immediately go to the failure route.\n#define JSON_MUST(expr)                                                        \\\n    if (!(expr)) {                                                             \\\n        goto fail;                                                             \\\n    }\n\n// Immediately go to the failure route (with `goto fail`).\n#define JSON_FAIL() goto fail;\n\n// Declare the failure route: by default, it just does return a bool.\n// To be used at the end of the function.\n#define JSON_FINISH()                                                          \\\n    do {                                                                       \\\n    success:                                                                   \\\n        return true;                                                           \\\n    fail:                                                                      \\\n        return false;                                                          \\\n    } while (0);\n"
  },
  {
    "path": "src/trx/core/json/util/write_io.c",
    "content": "#include <trx/core/json/util/write_io.h>\n\n#include <trx/core/json.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n\n#define M_MAX_STACK_SIZE 10\n\ntypedef struct JSON_WRITE_IO {\n    JSON_VALUE *stack[M_MAX_STACK_SIZE];\n    JSON_VALUE *current;\n    size_t current_pos;\n} JSON_WRITE_IO;\n\nstatic void M_PushValue(JSON_WRITE_IO *const io, JSON_VALUE *const value)\n{\n    io->current_pos++;\n    io->stack[io->current_pos] = value;\n    io->current = value;\n}\n\nstatic void M_Pop(JSON_WRITE_IO *const io)\n{\n    io->current_pos--;\n    io->current = io->stack[io->current_pos];\n}\n\nvoid JSON_WriteIO_PushObject(JSON_WRITE_IO *const io)\n{\n    JSON_OBJECT *const child = JSON_ObjectNew();\n    M_PushValue(io, JSON_ValueFromObject(child));\n}\n\nvoid JSON_WriteIO_PushArray(JSON_WRITE_IO *const io)\n{\n    JSON_ARRAY *const child = JSON_ArrayNew();\n    M_PushValue(io, JSON_ValueFromArray(child));\n}\n\nvoid JSON_WriteIO_PopAndSet(JSON_WRITE_IO *const io, const char *const key)\n{\n    JSON_OBJECT *const parent =\n        JSON_ValueAsObject(io->stack[io->current_pos - 1]);\n    ASSERT(parent != nullptr);\n    JSON_ObjectAppend(parent, key, io->current);\n    M_Pop(io);\n}\n\nvoid JSON_WriteIO_DiscardCurrent(JSON_WRITE_IO *const io)\n{\n    JSON_ValueFree(io->current);\n    M_Pop(io);\n}\n\nvoid JSON_WriteIO_PopAndSetNZ(JSON_WRITE_IO *const io, const char *const key)\n{\n    const JSON_OBJECT *const obj = JSON_ValueAsObject(io->current);\n    if (obj != nullptr && obj->length == 0) {\n        JSON_WriteIO_DiscardCurrent(io);\n        return;\n    }\n\n    const JSON_ARRAY *const arr = JSON_ValueAsArray(io->current);\n    if (arr != nullptr && arr->length == 0) {\n        JSON_WriteIO_DiscardCurrent(io);\n        return;\n    }\n\n    JSON_WriteIO_PopAndSet(io, key);\n}\n\nvoid JSON_WriteIO_PopAndAppend(JSON_WRITE_IO *const io)\n{\n    JSON_ARRAY *const parent =\n        JSON_ValueAsArray(io->stack[io->current_pos - 1]);\n    ASSERT(parent != nullptr);\n    JSON_ArrayAppend(parent, io->current);\n    M_Pop(io);\n}\n\nJSON_OBJECT *JSON_WriteIO_GetCurrentObject(JSON_WRITE_IO *const io)\n{\n    return JSON_ValueAsObject(io->current);\n}\n\nvoid JSON_WriteIO_PushString(JSON_WRITE_IO *const io, const char *const value)\n{\n    M_PushValue(io, JSON_ValueFromString(value));\n}\n\nvoid JSON_WriteIO_PushBool(JSON_WRITE_IO *const io, const bool value)\n{\n    M_PushValue(io, JSON_ValueFromBool(value));\n}\n\nvoid JSON_WriteIO_PushInt(JSON_WRITE_IO *const io, const int32_t value)\n{\n    M_PushValue(io, JSON_ValueFromInt(value));\n}\n\nvoid JSON_WriteIO_PushDouble(JSON_WRITE_IO *const io, const double value)\n{\n    M_PushValue(io, JSON_ValueFromDouble(value));\n}\n\nvoid JSON_WriteIO_PushRGB888(JSON_WRITE_IO *const io, const RGB_888 value)\n{\n    JSON_WriteIO_PushArray(io);\n    JSON_WriteIO_PushDouble(io, (double)value.r / 255.0);\n    JSON_WriteIO_PopAndAppend(io);\n    JSON_WriteIO_PushDouble(io, (double)value.g / 255.0);\n    JSON_WriteIO_PopAndAppend(io);\n    JSON_WriteIO_PushDouble(io, (double)value.b / 255.0);\n    JSON_WriteIO_PopAndAppend(io);\n}\n\nvoid JSON_WriteIO_PushXYZ16(JSON_WRITE_IO *const io, const XYZ_16 value)\n{\n    JSON_WriteIO_PushObject(io);\n    JSON_WriteIO_PushInt(io, value.x);\n    JSON_WriteIO_PopAndSet(io, \"x\");\n    JSON_WriteIO_PushInt(io, value.y);\n    JSON_WriteIO_PopAndSet(io, \"y\");\n    JSON_WriteIO_PushInt(io, value.z);\n    JSON_WriteIO_PopAndSet(io, \"z\");\n}\n\nvoid JSON_WriteIO_PushXYZ32(JSON_WRITE_IO *const io, const XYZ_32 value)\n{\n    JSON_WriteIO_PushObject(io);\n    JSON_WriteIO_PushInt(io, value.x);\n    JSON_WriteIO_PopAndSet(io, \"x\");\n    JSON_WriteIO_PushInt(io, value.y);\n    JSON_WriteIO_PopAndSet(io, \"y\");\n    JSON_WriteIO_PushInt(io, value.z);\n    JSON_WriteIO_PopAndSet(io, \"z\");\n}\n\nJSON_WRITE_IO *JSON_WriteIO_Create(void)\n{\n    JSON_WRITE_IO *const io = Memory_Alloc(sizeof(*io));\n    JSON_OBJECT *const root_obj = JSON_ObjectNew();\n    io->stack[0] = JSON_ValueFromObject(root_obj);\n    io->current = io->stack[0];\n    return io;\n}\n\nvoid JSON_WriteIO_Destroy(JSON_WRITE_IO *const io)\n{\n    JSON_ValueFree(io->stack[0]);\n    Memory_Free(io);\n}\n\nJSON_VALUE *JSON_WriteIO_GetRoot(JSON_WRITE_IO *const io)\n{\n    ASSERT(io->stack[0] != nullptr);\n    return io->stack[0];\n}\n"
  },
  {
    "path": "src/trx/core/json/util/write_io.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/json.h>\n#include <trx/core/math/types.h>\n\n#include <stddef.h>\n#include <stdint.h>\n#include <string.h>\n\ntypedef struct JSON_WRITE_IO JSON_WRITE_IO;\n\ntypedef enum JSON_WRITE_TYPE {\n    JSON_WRITE_TYPE_BOOL = 0,\n    JSON_WRITE_TYPE_INT,\n    JSON_WRITE_TYPE_DOUBLE,\n    JSON_WRITE_TYPE_STRING,\n} JSON_WRITE_TYPE;\n\nJSON_WRITE_IO *JSON_WriteIO_Create(void);\nvoid JSON_WriteIO_Destroy(JSON_WRITE_IO *io);\n\nJSON_VALUE *JSON_WriteIO_GetRoot(JSON_WRITE_IO *io);\n\nvoid JSON_WriteIO_PushObject(JSON_WRITE_IO *io);\nvoid JSON_WriteIO_PushArray(JSON_WRITE_IO *io);\nvoid JSON_WriteIO_PopAndSet(JSON_WRITE_IO *io, const char *key);\nvoid JSON_WriteIO_PopAndSetNZ(JSON_WRITE_IO *io, const char *key);\nvoid JSON_WriteIO_PopAndAppend(JSON_WRITE_IO *io);\nvoid JSON_WriteIO_DiscardCurrent(JSON_WRITE_IO *io);\nJSON_OBJECT *JSON_WriteIO_GetCurrentObject(JSON_WRITE_IO *io);\n\nvoid JSON_WriteIO_PushBool(JSON_WRITE_IO *io, bool value);\nvoid JSON_WriteIO_PushInt(JSON_WRITE_IO *io, int32_t value);\nvoid JSON_WriteIO_PushDouble(JSON_WRITE_IO *io, double value);\nvoid JSON_WriteIO_PushRGB888(JSON_WRITE_IO *io, RGB_888 value);\nvoid JSON_WriteIO_PushXYZ16(JSON_WRITE_IO *io, XYZ_16 value);\nvoid JSON_WriteIO_PushXYZ32(JSON_WRITE_IO *io, XYZ_32 value);\nvoid JSON_WriteIO_PushString(JSON_WRITE_IO *io, const char *value);\n\n#define JSONW_PUSH_OBJECT(io) JSON_WriteIO_PushObject((io))\n#define JSONW_PUSH_ARRAY(io) JSON_WriteIO_PushArray((io))\n#define JSONW_POP_AND_SET(io, key) JSON_WriteIO_PopAndSet((io), (key))\n#define JSONW_POP_AND_SET_NZ(io, key) JSON_WriteIO_PopAndSetNZ((io), (key))\n#define JSONW_POP_AND_APPEND(io) JSON_WriteIO_PopAndAppend((io))\n\n#define JSONW_PUSH_VALUE(io, value)                                            \\\n    _Generic(                                                                  \\\n        (value),                                                               \\\n        bool: JSON_WriteIO_PushBool,                                           \\\n        int8_t: JSON_WriteIO_PushInt,                                          \\\n        uint8_t: JSON_WriteIO_PushInt,                                         \\\n        int16_t: JSON_WriteIO_PushInt,                                         \\\n        uint16_t: JSON_WriteIO_PushInt,                                        \\\n        int32_t: JSON_WriteIO_PushInt,                                         \\\n        uint32_t: JSON_WriteIO_PushInt,                                        \\\n        float: JSON_WriteIO_PushDouble,                                        \\\n        double: JSON_WriteIO_PushDouble,                                       \\\n        RGB_888: JSON_WriteIO_PushRGB888,                                      \\\n        XYZ_16: JSON_WriteIO_PushXYZ16,                                        \\\n        XYZ_32: JSON_WriteIO_PushXYZ32,                                        \\\n        const char *: JSON_WriteIO_PushString,                                 \\\n        char *: JSON_WriteIO_PushString)(io, value)\n\n#define JSONW_WRITE(io, key, value)                                            \\\n    do {                                                                       \\\n        JSONW_PUSH_VALUE((io), (value));                                       \\\n        JSONW_POP_AND_SET((io), (key));                                        \\\n    } while (0)\n\n#define JSONW_WRITE_NZ(io, key, value)                                         \\\n    do {                                                                       \\\n        typeof(value) _tmp = (value);                                          \\\n        unsigned char _zero[sizeof(_tmp)] = { 0 };                             \\\n        if (memcmp(&_tmp, _zero, sizeof(_tmp)) != 0) {                         \\\n            JSONW_WRITE(io, key, _tmp);                                        \\\n        }                                                                      \\\n    } while (0)\n"
  },
  {
    "path": "src/trx/core/json/write.c",
    "content": "#include <trx/core/json/write.h>\n\n#include <trx/core/memory.h>\n\nstatic int M_GetValueSize_Minified(const JSON_VALUE *value, size_t *size);\nstatic char *M_WriteValue_Minified(const JSON_VALUE *value, char *data);\n\nstatic int M_GetValueSize_Pretty(\n    const JSON_VALUE *value, size_t depth, size_t indent_size,\n    size_t newline_size, size_t *size);\nstatic char *M_WriteValue_Pretty(\n    const JSON_VALUE *value, size_t depth, const char *indent,\n    const char *newline, char *data);\n\nstatic int M_GetNumberSize(const JSON_NUMBER *number, size_t *size)\n{\n    json_uintmax_t parsed_number;\n    size_t i;\n\n    if (number->number_size >= 2) {\n        switch (number->number[1]) {\n        case 'x':\n        case 'X':\n            /* the number is a JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS\n             * hexadecimal so we have to do extra work to convert it to a\n             * non-hexadecimal for JSON output. */\n            parsed_number = json_strtoumax(number->number, nullptr, 0);\n\n            i = 0;\n\n            while (0 != parsed_number) {\n                parsed_number /= 10;\n                i++;\n            }\n\n            *size += i;\n            return 0;\n        }\n    }\n\n    /* check to see if the number has leading/trailing decimal point. */\n    i = 0;\n\n    /* skip any leading '+' or '-'. */\n    if ((i < number->number_size)\n        && (('+' == number->number[i]) || ('-' == number->number[i]))) {\n        i++;\n    }\n\n    /* check if we have infinity. */\n    if ((i < number->number_size) && ('I' == number->number[i])) {\n        const char *inf = \"Infinity\";\n        size_t k;\n\n        for (k = i; k < number->number_size; k++) {\n            const char c = *inf++;\n\n            /* Check if we found the Infinity string! */\n            if ('\\0' == c) {\n                break;\n            } else if (c != number->number[k]) {\n                break;\n            }\n        }\n\n        if ('\\0' == *inf) {\n            /* Inf becomes 1.7976931348623158e308 because JSON can't support it.\n             */\n            *size += 22;\n\n            /* if we had a leading '-' we need to record it in the JSON output.\n             */\n            if ('-' == number->number[0]) {\n                *size += 1;\n            }\n        }\n\n        return 0;\n    }\n\n    /* check if we have nan. */\n    if ((i < number->number_size) && ('N' == number->number[i])) {\n        const char *nan = \"NaN\";\n        size_t k;\n\n        for (k = i; k < number->number_size; k++) {\n            const char c = *nan++;\n\n            /* Check if we found the NaN string! */\n            if ('\\0' == c) {\n                break;\n            } else if (c != number->number[k]) {\n                break;\n            }\n        }\n\n        if ('\\0' == *nan) {\n            /* NaN becomes 1 because JSON can't support it. */\n            *size += 1;\n\n            return 0;\n        }\n    }\n\n    /* if we had a leading decimal point. */\n    if ((i < number->number_size) && ('.' == number->number[i])) {\n        /* 1 + because we had a leading decimal point. */\n        *size += 1;\n        goto cleanup;\n    }\n\n    for (; i < number->number_size; i++) {\n        const char c = number->number[i];\n        if (!('0' <= c && c <= '9')) {\n            break;\n        }\n    }\n\n    /* if we had a trailing decimal point. */\n    if ((i + 1 == number->number_size) && ('.' == number->number[i])) {\n        /* 1 + because we had a trailing decimal point. */\n        *size += 1;\n        goto cleanup;\n    }\n\ncleanup:\n    *size += number->number_size; /* the actual string of the number. */\n\n    /* if we had a leading '+' we don't record it in the JSON output. */\n    if ('+' == number->number[0]) {\n        *size -= 1;\n    }\n\n    return 0;\n}\n\nstatic int M_GetStringSize(const JSON_STRING *string, size_t *size)\n{\n    size_t i;\n    for (i = 0; i < string->string_size; i++) {\n        switch (string->string[i]) {\n        case '\"':\n        case '\\\\':\n        case '\\b':\n        case '\\f':\n        case '\\n':\n        case '\\r':\n        case '\\t':\n            *size += 2;\n            break;\n        default:\n            *size += 1;\n            break;\n        }\n    }\n\n    *size += 2; /* need to encode the surrounding '\"' characters. */\n\n    return 0;\n}\n\nstatic int M_GetArraySize_Minified(const JSON_ARRAY *array, size_t *size)\n{\n    JSON_ARRAY_ELEMENT *element;\n\n    *size += 2; /* '[' and ']'. */\n\n    if (1 < array->length) {\n        *size += array->length - 1; /* ','s seperate each element. */\n    }\n\n    for (element = array->start; nullptr != element; element = element->next) {\n        if (M_GetValueSize_Minified(element->value, size)) {\n            /* value was malformed! */\n            return 1;\n        }\n    }\n\n    return 0;\n}\n\nstatic int M_GetObjectSize_Minified(const JSON_OBJECT *object, size_t *size)\n{\n    JSON_OBJECT_ELEMENT *element;\n\n    *size += 2; /* '{' and '}'. */\n\n    *size += object->length; /* ':'s seperate each name/value pair. */\n\n    if (1 < object->length) {\n        *size += object->length - 1; /* ','s seperate each element. */\n    }\n\n    for (element = object->start; nullptr != element; element = element->next) {\n        if (M_GetStringSize(element->name, size)) {\n            /* string was malformed! */\n            return 1;\n        }\n\n        if (M_GetValueSize_Minified(element->value, size)) {\n            /* value was malformed! */\n            return 1;\n        }\n    }\n\n    return 0;\n}\n\nstatic int M_GetValueSize_Minified(const JSON_VALUE *value, size_t *size)\n{\n    switch (value->type) {\n    default:\n        /* unknown value type found! */\n        return 1;\n    case JSON_TYPE_NUMBER:\n        return M_GetNumberSize((JSON_NUMBER *)value->payload, size);\n    case JSON_TYPE_STRING:\n        return M_GetStringSize((JSON_STRING *)value->payload, size);\n    case JSON_TYPE_ARRAY:\n        return M_GetArraySize_Minified((JSON_ARRAY *)value->payload, size);\n    case JSON_TYPE_OBJECT:\n        return M_GetObjectSize_Minified((JSON_OBJECT *)value->payload, size);\n    case JSON_TYPE_TRUE:\n        *size += 4; /* the string \"true\". */\n        return 0;\n    case JSON_TYPE_FALSE:\n        *size += 5; /* the string \"false\". */\n        return 0;\n    case JSON_TYPE_NULL:\n        *size += 4; /* the string \"null\". */\n        return 0;\n    }\n}\n\nstatic char *M_WriteNumber(const JSON_NUMBER *number, char *data)\n{\n    json_uintmax_t parsed_number, backup;\n    size_t i;\n\n    if (number->number_size >= 2) {\n        switch (number->number[1]) {\n        case 'x':\n        case 'X':\n            /* The number is a JSON_PARSE_FLAGS_ALLOW_HEXADECIMAL_NUMBERS\n             * hexadecimal so we have to do extra work to convert it to a\n             * non-hexadecimal for JSON output. */\n            parsed_number = json_strtoumax(number->number, nullptr, 0);\n\n            /* We need a copy of parsed number twice, so take a backup of it. */\n            backup = parsed_number;\n\n            i = 0;\n\n            while (0 != parsed_number) {\n                parsed_number /= 10;\n                i++;\n            }\n\n            /* Restore parsed_number to its original value stored in the backup.\n             */\n            parsed_number = backup;\n\n            /* Now use backup to take a copy of i, or the length of the string.\n             */\n            backup = i;\n\n            do {\n                *(data + i - 1) = '0' + (char)(parsed_number % 10);\n                parsed_number /= 10;\n                i--;\n            } while (0 != parsed_number);\n\n            data += backup;\n\n            return data;\n        }\n    }\n\n    /* check to see if the number has leading/trailing decimal point. */\n    i = 0;\n\n    /* skip any leading '-'. */\n    if ((i < number->number_size)\n        && (('+' == number->number[i]) || ('-' == number->number[i]))) {\n        i++;\n    }\n\n    /* check if we have infinity. */\n    if ((i < number->number_size) && ('I' == number->number[i])) {\n        const char *inf = \"Infinity\";\n        size_t k;\n\n        for (k = i; k < number->number_size; k++) {\n            const char c = *inf++;\n\n            /* Check if we found the Infinity string! */\n            if ('\\0' == c) {\n                break;\n            } else if (c != number->number[k]) {\n                break;\n            }\n        }\n\n        if ('\\0' == *inf++) {\n            const char *dbl_max;\n\n            /* if we had a leading '-' we need to record it in the JSON output.\n             */\n            if ('-' == number->number[0]) {\n                *data++ = '-';\n            }\n\n            /* Inf becomes 1.7976931348623158e308 because JSON can't support it.\n             */\n            for (dbl_max = \"1.7976931348623158e308\"; '\\0' != *dbl_max;\n                 dbl_max++) {\n                *data++ = *dbl_max;\n            }\n\n            return data;\n        }\n    }\n\n    /* check if we have nan. */\n    if ((i < number->number_size) && ('N' == number->number[i])) {\n        const char *nan = \"NaN\";\n        size_t k;\n\n        for (k = i; k < number->number_size; k++) {\n            const char c = *nan++;\n\n            /* Check if we found the NaN string! */\n            if ('\\0' == c) {\n                break;\n            } else if (c != number->number[k]) {\n                break;\n            }\n        }\n\n        if ('\\0' == *nan++) {\n            /* NaN becomes 0 because JSON can't support it. */\n            *data++ = '0';\n            return data;\n        }\n    }\n\n    /* if we had a leading decimal point. */\n    if ((i < number->number_size) && ('.' == number->number[i])) {\n        i = 0;\n\n        /* skip any leading '+'. */\n        if ('+' == number->number[i]) {\n            i++;\n        }\n\n        /* output the leading '-' if we had one. */\n        if ('-' == number->number[i]) {\n            *data++ = '-';\n            i++;\n        }\n\n        /* insert a '0' to fix the leading decimal point for JSON output. */\n        *data++ = '0';\n\n        /* and output the rest of the number as normal. */\n        for (; i < number->number_size; i++) {\n            *data++ = number->number[i];\n        }\n\n        return data;\n    }\n\n    for (; i < number->number_size; i++) {\n        const char c = number->number[i];\n        if (!('0' <= c && c <= '9')) {\n            break;\n        }\n    }\n\n    /* if we had a trailing decimal point. */\n    if ((i + 1 == number->number_size) && ('.' == number->number[i])) {\n        i = 0;\n\n        /* skip any leading '+'. */\n        if ('+' == number->number[i]) {\n            i++;\n        }\n\n        /* output the leading '-' if we had one. */\n        if ('-' == number->number[i]) {\n            *data++ = '-';\n            i++;\n        }\n\n        /* and output the rest of the number as normal. */\n        for (; i < number->number_size; i++) {\n            *data++ = number->number[i];\n        }\n\n        /* insert a '0' to fix the trailing decimal point for JSON output. */\n        *data++ = '0';\n\n        return data;\n    }\n\n    i = 0;\n\n    /* skip any leading '+'. */\n    if ('+' == number->number[i]) {\n        i++;\n    }\n\n    for (; i < number->number_size; i++) {\n        *data++ = number->number[i];\n    }\n\n    return data;\n}\n\nstatic char *M_WriteString(const JSON_STRING *string, char *data)\n{\n    size_t i;\n\n    *data++ = '\"'; /* open the string. */\n\n    for (i = 0; i < string->string_size; i++) {\n        switch (string->string[i]) {\n        case '\"':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = '\"';\n            break;\n        case '\\\\':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = '\\\\';\n            break;\n        case '\\b':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = 'b';\n            break;\n        case '\\f':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = 'f';\n            break;\n        case '\\n':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = 'n';\n            break;\n        case '\\r':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = 'r';\n            break;\n        case '\\t':\n            *data++ = '\\\\'; /* escape the control character. */\n            *data++ = 't';\n            break;\n        default:\n            *data++ = string->string[i];\n            break;\n        }\n    }\n\n    *data++ = '\"'; /* close the string. */\n\n    return data;\n}\n\nstatic char *M_WriteArray_Minified(const JSON_ARRAY *array, char *data)\n{\n    JSON_ARRAY_ELEMENT *element = nullptr;\n\n    *data++ = '['; /* open the array. */\n\n    for (element = array->start; nullptr != element; element = element->next) {\n        if (element != array->start) {\n            *data++ = ','; /* ','s seperate each element. */\n        }\n\n        data = M_WriteValue_Minified(element->value, data);\n\n        if (nullptr == data) {\n            /* value was malformed! */\n            return nullptr;\n        }\n    }\n\n    *data++ = ']'; /* close the array. */\n\n    return data;\n}\n\nstatic char *M_WriteObject_Minified(const JSON_OBJECT *object, char *data)\n{\n    JSON_OBJECT_ELEMENT *element = nullptr;\n\n    *data++ = '{'; /* open the object. */\n\n    for (element = object->start; nullptr != element; element = element->next) {\n        if (element != object->start) {\n            *data++ = ','; /* ','s seperate each element. */\n        }\n\n        data = M_WriteString(element->name, data);\n\n        if (nullptr == data) {\n            /* string was malformed! */\n            return nullptr;\n        }\n\n        *data++ = ':'; /* ':'s seperate each name/value pair. */\n\n        data = M_WriteValue_Minified(element->value, data);\n\n        if (nullptr == data) {\n            /* value was malformed! */\n            return nullptr;\n        }\n    }\n\n    *data++ = '}'; /* close the object. */\n\n    return data;\n}\n\nstatic char *M_WriteValue_Minified(const JSON_VALUE *value, char *data)\n{\n    switch (value->type) {\n    default:\n        /* unknown value type found! */\n        return nullptr;\n    case JSON_TYPE_NUMBER:\n        return M_WriteNumber((JSON_NUMBER *)value->payload, data);\n    case JSON_TYPE_STRING:\n        return M_WriteString((JSON_STRING *)value->payload, data);\n    case JSON_TYPE_ARRAY:\n        return M_WriteArray_Minified((JSON_ARRAY *)value->payload, data);\n    case JSON_TYPE_OBJECT:\n        return M_WriteObject_Minified((JSON_OBJECT *)value->payload, data);\n    case JSON_TYPE_TRUE:\n        data[0] = 't';\n        data[1] = 'r';\n        data[2] = 'u';\n        data[3] = 'e';\n        return data + 4;\n    case JSON_TYPE_FALSE:\n        data[0] = 'f';\n        data[1] = 'a';\n        data[2] = 'l';\n        data[3] = 's';\n        data[4] = 'e';\n        return data + 5;\n    case JSON_TYPE_NULL:\n        data[0] = 'n';\n        data[1] = 'u';\n        data[2] = 'l';\n        data[3] = 'l';\n        return data + 4;\n    }\n}\n\nvoid *JSON_WriteMinified(const JSON_VALUE *value, size_t *out_size)\n{\n    size_t size = 0;\n    char *data = nullptr;\n    char *data_end = nullptr;\n\n    if (nullptr == value) {\n        return nullptr;\n    }\n\n    if (M_GetValueSize_Minified(value, &size)) {\n        /* value was malformed! */\n        return nullptr;\n    }\n\n    size += 1; /* for the '\\0' null terminating character. */\n\n    data = (char *)Memory_Alloc(size);\n\n    if (nullptr == data) {\n        /* malloc failed! */\n        return nullptr;\n    }\n\n    data_end = M_WriteValue_Minified(value, data);\n\n    if (nullptr == data_end) {\n        /* bad chi occurred! */\n        Memory_Free(data);\n        return nullptr;\n    }\n\n    /* null terminated the string. */\n    *data_end = '\\0';\n\n    if (nullptr != out_size) {\n        *out_size = size;\n    }\n\n    return data;\n}\n\nstatic int M_GetArraySize_Pretty(\n    const JSON_ARRAY *array, size_t depth, size_t indent_size,\n    size_t newline_size, size_t *size)\n{\n    JSON_ARRAY_ELEMENT *element;\n\n    *size += 1; /* '['. */\n\n    if (0 < array->length) {\n        /* if we have any elements we need to add a newline after our '['. */\n        *size += newline_size;\n\n        *size += array->length - 1; /* ','s seperate each element. */\n\n        for (element = array->start; nullptr != element;\n             element = element->next) {\n            /* each element gets an indent. */\n            *size += (depth + 1) * indent_size;\n\n            if (M_GetValueSize_Pretty(\n                    element->value, depth + 1, indent_size, newline_size,\n                    size)) {\n                /* value was malformed! */\n                return 1;\n            }\n\n            /* each element gets a newline too. */\n            *size += newline_size;\n        }\n\n        /* since we wrote out some elements, need to add a newline and\n         * indentation.\n         */\n        /* to the trailing ']'. */\n        *size += depth * indent_size;\n    }\n\n    *size += 1; /* ']'. */\n\n    return 0;\n}\n\nstatic int M_GetObjectSize_Pretty(\n    const JSON_OBJECT *object, size_t depth, size_t indent_size,\n    size_t newline_size, size_t *size)\n{\n    JSON_OBJECT_ELEMENT *element;\n\n    *size += 1; /* '{'. */\n\n    if (0 < object->length) {\n        *size += newline_size; /* need a newline next. */\n\n        *size += object->length - 1; /* ','s seperate each element. */\n\n        for (element = object->start; nullptr != element;\n             element = element->next) {\n            /* each element gets an indent and newline. */\n            *size += (depth + 1) * indent_size;\n            *size += newline_size;\n\n            if (M_GetStringSize(element->name, size)) {\n                /* string was malformed! */\n                return 1;\n            }\n\n            *size += 2; /* seperate each name/value pair with \": \". */\n\n            if (M_GetValueSize_Pretty(\n                    element->value, depth + 1, indent_size, newline_size,\n                    size)) {\n                /* value was malformed! */\n                return 1;\n            }\n        }\n\n        *size += depth * indent_size;\n    }\n\n    *size += 1; /* '}'. */\n\n    return 0;\n}\n\nstatic int M_GetValueSize_Pretty(\n    const JSON_VALUE *value, size_t depth, size_t indent_size,\n    size_t newline_size, size_t *size)\n{\n    switch (value->type) {\n    default:\n        /* unknown value type found! */\n        return 1;\n    case JSON_TYPE_NUMBER:\n        return M_GetNumberSize((JSON_NUMBER *)value->payload, size);\n    case JSON_TYPE_STRING:\n        return M_GetStringSize((JSON_STRING *)value->payload, size);\n    case JSON_TYPE_ARRAY:\n        return M_GetArraySize_Pretty(\n            (JSON_ARRAY *)value->payload, depth, indent_size, newline_size,\n            size);\n    case JSON_TYPE_OBJECT:\n        return M_GetObjectSize_Pretty(\n            (JSON_OBJECT *)value->payload, depth, indent_size, newline_size,\n            size);\n    case JSON_TYPE_TRUE:\n        *size += 4; /* the string \"true\". */\n        return 0;\n    case JSON_TYPE_FALSE:\n        *size += 5; /* the string \"false\". */\n        return 0;\n    case JSON_TYPE_NULL:\n        *size += 4; /* the string \"null\". */\n        return 0;\n    }\n}\n\nstatic char *M_WriteArray_Pretty(\n    const JSON_ARRAY *array, size_t depth, const char *indent,\n    const char *newline, char *data)\n{\n    size_t k, m;\n    JSON_ARRAY_ELEMENT *element;\n\n    *data++ = '['; /* open the array. */\n\n    if (0 < array->length) {\n        for (k = 0; '\\0' != newline[k]; k++) {\n            *data++ = newline[k];\n        }\n\n        for (element = array->start; nullptr != element;\n             element = element->next) {\n            if (element != array->start) {\n                *data++ = ','; /* ','s seperate each element. */\n\n                for (k = 0; '\\0' != newline[k]; k++) {\n                    *data++ = newline[k];\n                }\n            }\n\n            for (k = 0; k < depth + 1; k++) {\n                for (m = 0; '\\0' != indent[m]; m++) {\n                    *data++ = indent[m];\n                }\n            }\n\n            data = M_WriteValue_Pretty(\n                element->value, depth + 1, indent, newline, data);\n\n            if (nullptr == data) {\n                /* value was malformed! */\n                return nullptr;\n            }\n        }\n\n        for (k = 0; '\\0' != newline[k]; k++) {\n            *data++ = newline[k];\n        }\n\n        for (k = 0; k < depth; k++) {\n            for (m = 0; '\\0' != indent[m]; m++) {\n                *data++ = indent[m];\n            }\n        }\n    }\n\n    *data++ = ']'; /* close the array. */\n\n    return data;\n}\n\nstatic char *M_WriteObject_Pretty(\n    const JSON_OBJECT *object, size_t depth, const char *indent,\n    const char *newline, char *data)\n{\n    size_t k, m;\n    JSON_OBJECT_ELEMENT *element;\n\n    *data++ = '{'; /* open the object. */\n\n    if (0 < object->length) {\n        for (k = 0; '\\0' != newline[k]; k++) {\n            *data++ = newline[k];\n        }\n\n        for (element = object->start; nullptr != element;\n             element = element->next) {\n            if (element != object->start) {\n                *data++ = ','; /* ','s seperate each element. */\n\n                for (k = 0; '\\0' != newline[k]; k++) {\n                    *data++ = newline[k];\n                }\n            }\n\n            for (k = 0; k < depth + 1; k++) {\n                for (m = 0; '\\0' != indent[m]; m++) {\n                    *data++ = indent[m];\n                }\n            }\n\n            data = M_WriteString(element->name, data);\n\n            if (nullptr == data) {\n                /* string was malformed! */\n                return nullptr;\n            }\n\n            /* \": \"s seperate each name/value pair. */\n            *data++ = ':';\n            *data++ = ' ';\n\n            data = M_WriteValue_Pretty(\n                element->value, depth + 1, indent, newline, data);\n\n            if (nullptr == data) {\n                /* value was malformed! */\n                return nullptr;\n            }\n        }\n\n        for (k = 0; '\\0' != newline[k]; k++) {\n            *data++ = newline[k];\n        }\n\n        for (k = 0; k < depth; k++) {\n            for (m = 0; '\\0' != indent[m]; m++) {\n                *data++ = indent[m];\n            }\n        }\n    }\n\n    *data++ = '}'; /* close the object. */\n\n    return data;\n}\n\nstatic char *M_WriteValue_Pretty(\n    const JSON_VALUE *value, size_t depth, const char *indent,\n    const char *newline, char *data)\n{\n    switch (value->type) {\n    default:\n        /* unknown value type found! */\n        return nullptr;\n    case JSON_TYPE_NUMBER:\n        return M_WriteNumber((JSON_NUMBER *)value->payload, data);\n    case JSON_TYPE_STRING:\n        return M_WriteString((JSON_STRING *)value->payload, data);\n    case JSON_TYPE_ARRAY:\n        return M_WriteArray_Pretty(\n            (JSON_ARRAY *)value->payload, depth, indent, newline, data);\n    case JSON_TYPE_OBJECT:\n        return M_WriteObject_Pretty(\n            (JSON_OBJECT *)value->payload, depth, indent, newline, data);\n    case JSON_TYPE_TRUE:\n        data[0] = 't';\n        data[1] = 'r';\n        data[2] = 'u';\n        data[3] = 'e';\n        return data + 4;\n    case JSON_TYPE_FALSE:\n        data[0] = 'f';\n        data[1] = 'a';\n        data[2] = 'l';\n        data[3] = 's';\n        data[4] = 'e';\n        return data + 5;\n    case JSON_TYPE_NULL:\n        data[0] = 'n';\n        data[1] = 'u';\n        data[2] = 'l';\n        data[3] = 'l';\n        return data + 4;\n    }\n}\n\nvoid *JSON_WritePretty(\n    const JSON_VALUE *value, const char *indent, const char *newline,\n    size_t *out_size)\n{\n    size_t size = 0;\n    size_t indent_size = 0;\n    size_t newline_size = 0;\n    char *data = nullptr;\n    char *data_end = nullptr;\n\n    if (nullptr == value) {\n        return nullptr;\n    }\n\n    if (nullptr == indent) {\n        indent = \"  \"; /* default to two spaces. */\n    }\n\n    if (nullptr == newline) {\n        newline = \"\\n\"; /* default to linux newlines. */\n    }\n\n    while ('\\0' != indent[indent_size]) {\n        ++indent_size; /* skip non-null terminating characters. */\n    }\n\n    while ('\\0' != newline[newline_size]) {\n        ++newline_size; /* skip non-null terminating characters. */\n    }\n\n    if (M_GetValueSize_Pretty(value, 0, indent_size, newline_size, &size)) {\n        /* value was malformed! */\n        return nullptr;\n    }\n\n    size += 1; /* for the '\\0' null terminating character. */\n\n    data = (char *)Memory_Alloc(size);\n\n    if (nullptr == data) {\n        /* malloc failed! */\n        return nullptr;\n    }\n\n    data_end = M_WriteValue_Pretty(value, 0, indent, newline, data);\n\n    if (nullptr == data_end) {\n        /* bad chi occurred! */\n        Memory_Free(data);\n        return nullptr;\n    }\n\n    /* null terminated the string. */\n    *data_end = '\\0';\n\n    if (nullptr != out_size) {\n        *out_size = size;\n    }\n\n    return data;\n}\n"
  },
  {
    "path": "src/trx/core/json/write.h",
    "content": "#pragma once\n\n#include <trx/core/json/base.h>\n\n/* Write out a minified JSON utf-8 string. This string is an encoding of the\n * minimal string characters required to still encode the same data.\n * json_write_minified performs 1 call to malloc for the entire encoding. Return\n * 0 if an error occurred (malformed JSON input, or malloc failed). The out_size\n * parameter is optional as the utf-8 string is null terminated. */\nvoid *JSON_WriteMinified(const JSON_VALUE *value, size_t *out_size);\n\n/* Write out a pretty JSON utf-8 string. This string is encoded such that the\n * resultant JSON is pretty in that it is easily human readable. The indent and\n * newline parameters allow a user to specify what kind of indentation and\n * newline they want (two spaces / three spaces / tabs? \\r, \\n, \\r\\n ?). Both\n * indent and newline can be nullptr, indent defaults to two spaces (\"  \"), and\n * newline defaults to linux newlines ('\\n' as the newline character).\n * json_write_pretty performs 1 call to malloc for the entire encoding. Return 0\n * if an error occurred (malformed JSON input, or malloc failed). The out_size\n * parameter is optional as the utf-8 string is null terminated. */\nvoid *JSON_WritePretty(\n    const JSON_VALUE *value, const char *indent, const char *newline,\n    size_t *out_size);\n"
  },
  {
    "path": "src/trx/core/json.h",
    "content": "#pragma once\n\n#include <trx/core/json/base.h>\n#include <trx/core/json/enum.h>\n#include <trx/core/json/parse.h>\n#include <trx/core/json/types.h>\n#include <trx/core/json/write.h>\n"
  },
  {
    "path": "src/trx/core/log.c",
    "content": "#include <trx/core/log.h>\n\n#include <stdarg.h>\n#include <stdio.h>\n#include <sys/time.h>\n#include <time.h>\n\n#define M_FORMAT \"%s | %s [%s:%d:%s] \"\n\nstatic LOG_LEVEL m_LogLevel = LOG_LEVEL_MAX;\nstatic FILE *m_LogHandle = nullptr;\nstatic bool m_UseAnsiColors = true;\n\nstatic const char *const m_LogLevelColors[] = {\n    [LOG_LEVEL_INFO] = LOG_ANSI_COLOR_RESET,\n    [LOG_LEVEL_WARNING] = LOG_ANSI_COLOR_YELLOW,\n    [LOG_LEVEL_ERROR] = LOG_ANSI_COLOR_RED,\n    [LOG_LEVEL_DEBUG] = LOG_ANSI_COLOR_CYAN,\n};\nstatic const char *const m_LogLevelStrings[] = {\n    [LOG_LEVEL_INFO] = \"INF\",\n    [LOG_LEVEL_WARNING] = \"WRN\",\n    [LOG_LEVEL_ERROR] = \"ERR\",\n    [LOG_LEVEL_DEBUG] = \"DBG\",\n};\n\nvoid Log_Init(const char *path, const LOG_LEVEL min_level)\n{\n    if (m_LogHandle != nullptr) {\n        fclose(m_LogHandle);\n        m_LogHandle = nullptr;\n    }\n    m_LogLevel = min_level;\n    m_UseAnsiColors = Log_ShouldUseAnsiColors();\n    if (path != nullptr) {\n        m_LogHandle = fopen(path, \"w\");\n    }\n    Log_Init_Extra(path);\n}\n\nLOG_LEVEL Log_GetMinLevel(void)\n{\n    return m_LogLevel;\n}\n\nvoid Log_SetMinLevel(const LOG_LEVEL min_level)\n{\n    m_LogLevel = min_level;\n}\n\nvoid Log_Message(\n    const LOG_LEVEL level, const char *const file, const int line,\n    const char *const func, const char *const fmt, ...)\n{\n    va_list va;\n    va_start(va, fmt);\n\n    char timestamp_str[32];\n    struct timeval tv;\n    gettimeofday(&tv, nullptr);\n    struct tm *const tm_info = localtime(&tv.tv_sec);\n    const size_t timestamp_len = strftime(\n        timestamp_str, sizeof(timestamp_str), \"%Y-%m-%d %H:%M:%S\", tm_info);\n    snprintf(\n        timestamp_str + timestamp_len, sizeof(timestamp_str) - timestamp_len,\n        \".%03d\", (int)(tv.tv_usec / 1000));\n\n    const char *const log_str = m_LogLevelStrings[level];\n    const char *const log_color = m_LogLevelColors[level];\n\n    // print to log file\n    if (m_LogHandle != nullptr) {\n        va_list vb;\n\n        va_copy(vb, va);\n        fprintf(\n            m_LogHandle, M_FORMAT, log_str, timestamp_str, file, line, func);\n        vfprintf(m_LogHandle, fmt, vb);\n        fprintf(m_LogHandle, \"\\n\");\n        fflush(m_LogHandle);\n\n        va_end(vb);\n    }\n\n    // print to stdout\n    if (level >= m_LogLevel) {\n        if (m_UseAnsiColors) {\n            printf(\"%s\", log_color);\n        }\n        printf(M_FORMAT, log_str, timestamp_str, file, line, func);\n        vprintf(fmt, va);\n        if (m_UseAnsiColors) {\n            printf(\"%s\", LOG_ANSI_COLOR_RESET);\n        }\n        printf(\"\\n\");\n        fflush(stdout);\n    }\n\n    va_end(va);\n}\n\nvoid Log_Shutdown(void)\n{\n    Log_Shutdown_Extra();\n    if (m_LogHandle != nullptr) {\n        fclose(m_LogHandle);\n        m_LogHandle = nullptr;\n    }\n}\n"
  },
  {
    "path": "src/trx/core/log.h",
    "content": "#pragma once\n\ntypedef enum {\n    // from least important to most important\n    LOG_LEVEL_DEBUG,\n    LOG_LEVEL_INFO,\n    LOG_LEVEL_WARNING,\n    LOG_LEVEL_ERROR,\n    LOG_LEVEL_MAX = -1,\n} LOG_LEVEL;\n\n#define LOG_ANSI_COLOR_RED \"\\x1b[31m\"\n#define LOG_ANSI_COLOR_GREEN \"\\x1b[32m\"\n#define LOG_ANSI_COLOR_YELLOW \"\\x1b[33m\"\n#define LOG_ANSI_COLOR_CYAN \"\\x1b[36m\"\n#define LOG_ANSI_COLOR_RESET \"\\x1b[0m\"\n\n#define LOG_GENERIC(level, ...)                                                \\\n    Log_Message(level, __FILE__, __LINE__, __func__, __VA_ARGS__)\n#define LOG_INFO(...) LOG_GENERIC(LOG_LEVEL_INFO, __VA_ARGS__)\n#define LOG_WARNING(...) LOG_GENERIC(LOG_LEVEL_WARNING, __VA_ARGS__)\n#define LOG_ERROR(...) LOG_GENERIC(LOG_LEVEL_ERROR, __VA_ARGS__)\n#define LOG_DEBUG(...) LOG_GENERIC(LOG_LEVEL_DEBUG, __VA_ARGS__)\n#define LOG_TRACE(...)\n// disable by default\n\n#define LOG_VAR(var)                                                           \\\n    _Generic(                                                                  \\\n        (var),                                                                 \\\n        int: LOG_DEBUG(#var \": %d\", var),                                      \\\n        bool: LOG_DEBUG(#var \": %d\", var),                                     \\\n        int8_t: LOG_DEBUG(#var \": %d\", var),                                   \\\n        int16_t: LOG_DEBUG(#var \": %d\", var),                                  \\\n        uint8_t: LOG_DEBUG(#var \": %d\", var),                                  \\\n        uint16_t: LOG_DEBUG(#var \": %d\", var),                                 \\\n        uint32_t: LOG_DEBUG(#var \": %d\", var),                                 \\\n        float: LOG_DEBUG(#var \": %f\", var),                                    \\\n        double: LOG_DEBUG(#var \": %f\", var),                                   \\\n        char *: LOG_DEBUG(#var \": %s\", var),                                   \\\n        const char *: LOG_DEBUG(#var \": %s\", var),                             \\\n        default: LOG_DEBUG(#var \": %p\", var))\n\nvoid Log_Init(const char *path, LOG_LEVEL min_level);\nLOG_LEVEL Log_GetMinLevel(void);\nvoid Log_SetMinLevel(LOG_LEVEL min_level);\nvoid Log_Shutdown(void);\nvoid Log_Message(\n    LOG_LEVEL level, const char *file, int line, const char *func,\n    const char *fmt, ...);\n\n// platform-specific implementations\nbool Log_ShouldUseAnsiColors(void);\nvoid Log_Init_Extra(const char *path);\nvoid Log_Shutdown_Extra(void);\n"
  },
  {
    "path": "src/trx/core/log_linux.c",
    "content": "#include <trx/core/log.h>\n\n#include <backtrace-supported.h>\n#include <backtrace.h>\n#include <signal.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nstatic void M_ErrorCallback(void *data, const char *msg, int errnum)\n{\n    LOG_ERROR(\"%s\", msg);\n}\n\nstatic int M_StackTrace(\n    void *data, uintptr_t pc, const char *filename, int lineno,\n    const char *func_name)\n{\n    if (filename) {\n        LOG_ERROR(\n            \"0x%08X: %s (%s:%d)\", pc, func_name ? func_name : \"???\",\n            filename ? filename : \"???\", lineno);\n    } else {\n        LOG_ERROR(\"0x%08X: %s\", pc, func_name ? func_name : \"???\");\n    }\n    return 0;\n}\n\nstatic void M_SignalHandler(int sig)\n{\n    LOG_ERROR(\"== CRASH REPORT ==\");\n    LOG_ERROR(\"SIGNAL: %d\", sig);\n    LOG_ERROR(\"STACK TRACE:\");\n    struct backtrace_state *state = backtrace_create_state(\n        nullptr, BACKTRACE_SUPPORTS_THREADS, M_ErrorCallback, nullptr);\n    backtrace_full(state, 0, M_StackTrace, M_ErrorCallback, nullptr);\n    exit(EXIT_FAILURE);\n}\n\nvoid Log_Init_Extra(const char *path)\n{\n    signal(SIGSEGV, M_SignalHandler);\n    signal(SIGFPE, M_SignalHandler);\n    signal(SIGILL, M_SignalHandler);\n}\n\nvoid Log_Shutdown_Extra(void)\n{\n}\n\nbool Log_ShouldUseAnsiColors(void)\n{\n    return isatty(fileno(stdout));\n}\n"
  },
  {
    "path": "src/trx/core/log_unknown.c",
    "content": "#include <trx/core/log.h>\n\nbool Log_ShouldUseAnsiColors(void)\n{\n    return true;\n}\n\nvoid Log_Init_Extra(const char *path)\n{\n}\n\nvoid Log_Shutdown_Extra(void)\n{\n}\n"
  },
  {
    "path": "src/trx/core/log_windows.c",
    "content": "#include <trx/core/log.h>\n\n#include <trx/core/memory.h>\n\n#include <windows.h>\n#include <dbghelp.h>\n#include <tlhelp32.h>\n#include <dwarfstack.h>\n#include <io.h>\n#include <process.h>\n#include <signal.h>\n#include <stdio.h>\n#include <stdlib.h>\n\n#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING\n    #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004\n#endif\n\nstatic char *m_MiniDumpPath = nullptr;\n\nstatic char *M_GetMiniDumpPath(const char *const log_path)\n{\n    char *dot = strrchr(log_path, '.');\n    if (dot == nullptr) {\n        return nullptr;\n    }\n\n    const size_t index = dot - log_path;\n    const char *new_extension = \".dmp\";\n    const size_t new_len = index + strlen(new_extension) + 1;\n    char *minidump_path = Memory_Alloc(new_len);\n    strncpy(minidump_path, log_path, index);\n    strcat(minidump_path, new_extension);\n    return minidump_path;\n}\n\nstatic void M_StackTrace(\n    const uint64_t addr, const char *filename, const int line_no,\n    const char *const func_name, void *const context, const int column_no)\n{\n    int32_t *count = context;\n    void *ptr = (void *)(uintptr_t)addr;\n\n    switch (line_no) {\n    case DWST_BASE_ADDR:\n        LOG_INFO(\"--- 0x%p: %s\", ptr, filename);\n        break;\n\n    case DWST_NOT_FOUND:\n    case DWST_NO_DBG_SYM:\n    case DWST_NO_SRC_FILE:\n        LOG_INFO(\"%02d. 0x%p: %s\", *count, ptr, filename);\n        (*count)++;\n        break;\n\n    default:\n        if (ptr != nullptr) {\n            LOG_INFO(\n                \"%02d. 0x%p: (%s:%d:%d) %s\", *count, ptr, filename, line_no,\n                column_no, func_name);\n        } else {\n            LOG_INFO(\n                \"%02d. %*s (%s:%d:%d) %s\", *count, (int32_t)sizeof(void *) * 2,\n                \"\", filename, line_no, column_no, func_name);\n        }\n        (*count)++;\n        break;\n    }\n}\n\nstatic void M_CreateMiniDump(\n    EXCEPTION_POINTERS *const ex, const char *const path)\n{\n    HANDLE handle = CreateFile(\n        path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,\n        nullptr);\n    MINIDUMP_EXCEPTION_INFORMATION dump_info;\n    dump_info.ExceptionPointers = ex;\n    dump_info.ThreadId = GetCurrentThreadId();\n    dump_info.ClientPointers = TRUE;\n    MiniDumpWriteDump(\n        GetCurrentProcess(), GetCurrentProcessId(), handle, MiniDumpNormal,\n        &dump_info, nullptr, nullptr);\n    CloseHandle(handle);\n\n    LOG_INFO(\"Crash dump info put in %s\", path);\n}\n\nLONG WINAPI Log_CrashHandler(EXCEPTION_POINTERS *ex)\n{\n    LOG_ERROR(\"== CRASH REPORT ==\");\n    LOG_INFO(\"EXCEPTION CODE: %x\", ex->ExceptionRecord->ExceptionCode);\n    LOG_INFO(\"EXCEPTION ADDRESS: %x\", ex->ExceptionRecord->ExceptionAddress);\n    LOG_INFO(\"STACK TRACE:\");\n\n    int32_t count = 0;\n    dwstOfException(ex->ContextRecord, &M_StackTrace, &count);\n\n    M_CreateMiniDump(ex, m_MiniDumpPath);\n\n    return EXCEPTION_EXECUTE_HANDLER;\n}\n\nvoid Log_Init_Extra(const char *log_path)\n{\n    // let the game (a gui app) log output to a terminal\n    if (AttachConsole(ATTACH_PARENT_PROCESS)) {\n        FILE *fp;\n        freopen_s(&fp, \"CONOUT$\", \"w\", stdout);\n        freopen_s(&fp, \"CONOUT$\", \"w\", stderr);\n        freopen_s(&fp, \"CONIN$\", \"r\", stdin);\n    }\n\n    // enable ANSI escape codes processing\n    HANDLE h_out = GetStdHandle(STD_OUTPUT_HANDLE);\n    if (h_out != INVALID_HANDLE_VALUE) {\n        DWORD mode = 0;\n        if (GetConsoleMode(h_out, &mode)) {\n            SetConsoleMode(h_out, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);\n        }\n    }\n\n    if (log_path != nullptr) {\n        m_MiniDumpPath = M_GetMiniDumpPath(log_path);\n        SetUnhandledExceptionFilter(Log_CrashHandler);\n    }\n}\n\nvoid Log_Shutdown_Extra(void)\n{\n    Memory_FreePointer(&m_MiniDumpPath);\n}\n\nbool Log_ShouldUseAnsiColors(void)\n{\n    return _isatty(_fileno(stdout));\n}\n"
  },
  {
    "path": "src/trx/core/math/const.h",
    "content": "#pragma once\n\n#ifndef M_PI\n    #define M_PI 3.14159265358979323846\n#endif\n\n// clang-format off\n#define DEG_360 0x10000\n#define DEG_315 (DEG_180 + DEG_135) // = 0xE000\n#define DEG_270 (DEG_180 + DEG_90)  // = 0xC000\n#define DEG_225 (DEG_180 + DEG_45)  // = 0xA000\n#define DEG_180 ((DEG_360) / 2)     // = 0x8000\n#define DEG_135 ((DEG_45)  * 3)     // = 0x6000\n#define DEG_90  ((DEG_360) / 4)     // = 0x4000\n#define DEG_45  ((DEG_360) / 8)     // = 0x2000\n#define DEG_1   ((DEG_360) / 360)   // = 182\n// clang-format on\n\n#define W2V_SHIFT 14\n"
  },
  {
    "path": "src/trx/core/math/func.c",
    "content": "#include <trx/core/math/func.h>\n\n#include <trx/core/math/const.h>\n#include <trx/core/math/geom.h>\n#include <trx/core/utils.h>\n\nuint32_t Math_Sqrt(uint32_t n)\n{\n    uint32_t result = 0;\n    uint32_t base = 0x40000000;\n    do {\n        do {\n            uint32_t based_result = base + result;\n            result >>= 1;\n            if (based_result > n) {\n                break;\n            }\n            n -= based_result;\n            result |= base;\n\n            base >>= 2;\n        } while (base);\n\n        base >>= 2;\n    } while (base);\n\n    return result;\n}\n\nuint64_t Math_Sqrt64(uint64_t n)\n{\n    uint64_t result = 0;\n    uint64_t bit = 1ULL << 62;\n\n    while (bit > n) {\n        bit >>= 2;\n    }\n\n    while (bit != 0) {\n        if (n >= result + bit) {\n            n -= result + bit;\n            result = (result >> 1) + bit;\n        } else {\n            result >>= 1;\n        }\n        bit >>= 2;\n    }\n\n    return result;\n}\n\nvoid Math_GetVectorAngles(\n    const int32_t x, const int32_t y, const int32_t z, int16_t *const dest)\n{\n    dest[0] = XYZ_32_GetYaw((XYZ_32) { x, y, z });\n    dest[1] = XYZ_32_GetPitch((XYZ_32) { x, y, z });\n}\n\nint32_t Math_AngleInCone(int32_t angle1, int32_t angle2, int32_t cone)\n{\n    const int32_t diff = ((int)(angle1 - angle2 + DEG_180)) % DEG_360 - DEG_180;\n    return ABS(diff) < cone;\n}\n\nDIRECTION Math_GetDirection(const int16_t angle)\n{\n    return (uint16_t)(angle + DEG_45) / DEG_90;\n}\n\nDIRECTION Math_GetDirectionCone(const int16_t angle, const int16_t cone)\n{\n    if (angle >= -cone && angle <= cone) {\n        return DIR_NORTH;\n    } else if (angle >= DEG_90 - cone && angle <= DEG_90 + cone) {\n        return DIR_EAST;\n    } else if (angle >= DEG_180 - cone || angle <= -DEG_180 + cone) {\n        return DIR_SOUTH;\n    } else if (angle >= -DEG_90 - cone && angle <= -DEG_90 + cone) {\n        return DIR_WEST;\n    }\n    return DIR_UNKNOWN;\n}\n\nint16_t Math_DirectionToAngle(const DIRECTION dir)\n{\n    switch (dir) {\n    case DIR_NORTH:\n        return 0;\n    case DIR_EAST:\n        return DEG_90;\n    case DIR_SOUTH:\n        return -DEG_180;\n    case DIR_WEST:\n        return -DEG_90;\n    default:\n        return 0;\n    }\n}\n\nint32_t Math_AngleMean(int32_t angle1, int32_t angle2, double ratio)\n{\n    int32_t diff = angle2 - angle1;\n\n    if (diff > DEG_180) {\n        diff -= DEG_360;\n    } else if (diff < -DEG_180) {\n        diff += DEG_360;\n    }\n\n    int32_t result = angle1 + diff * ratio;\n\n    result %= DEG_360;\n    if (result < 0) {\n        result += DEG_360;\n    }\n\n    return result;\n}\n\nint32_t Math_FloorDiv(const int32_t x, const int32_t divisor)\n{\n    return (x >= 0) ? x / divisor : -((-x + divisor - 1) / divisor);\n}\n\nint32_t Math_GCD(int32_t a, int32_t b)\n{\n    while (b != 0) {\n        int32_t t = b;\n        b = a % b;\n        a = t;\n    }\n    return a;\n}\n"
  },
  {
    "path": "src/trx/core/math/func.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n\nuint32_t Math_Sqrt(uint32_t n);\nuint64_t Math_Sqrt64(uint64_t n);\n\nvoid Math_GetVectorAngles(int32_t x, int32_t y, int32_t z, int16_t *dest);\nint32_t Math_AngleInCone(int32_t angle1, int32_t angle2, int32_t cone);\nDIRECTION Math_GetDirection(int16_t angle);\nDIRECTION Math_GetDirectionCone(int16_t angle, int16_t cone);\nint16_t Math_DirectionToAngle(DIRECTION dir);\nint32_t Math_AngleMean(int32_t angle1, int32_t angle2, double ratio);\nint32_t Math_FloorDiv(int32_t x, int32_t divisor);\nint32_t Math_GCD(int32_t a, int32_t b);\n"
  },
  {
    "path": "src/trx/core/math/geom.c",
    "content": "#include <trx/core/math/geom.h>\n\n#include <trx/core/math/const.h>\n#include <trx/core/math/func.h>\n#include <trx/core/math/trig.h>\n#include <trx/core/utils.h>\n\n#include <math.h>\n\nint16_t XYZ_32_GetYaw(const XYZ_32 pos)\n{\n    return Math_Atan(pos.z, pos.x);\n}\n\nint16_t XYZ_32_GetYawDiff(const XYZ_32 pos1, const XYZ_32 pos2)\n{\n    return Math_Atan(pos2.z - pos1.z, pos2.x - pos1.x);\n}\n\nint16_t XYZ_32_GetPitch(XYZ_32 pos)\n{\n    // make sure SQUARE() doesn't get out of bounds\n    while ((int16_t)pos.x != pos.x || (int16_t)pos.y != pos.y\n           || (int16_t)pos.z != pos.z) {\n        pos.x >>= 1;\n        pos.y >>= 1;\n        pos.z >>= 1;\n    }\n    return Math_Atan(Math_Sqrt(SQUARE(pos.x) + SQUARE(pos.z)), -pos.y);\n}\n\nint32_t XYZ_32_GetDistance(const XYZ_32 pos1, const XYZ_32 pos2)\n{\n    int64_t x = (int64_t)pos1.x - pos2.x;\n    int64_t y = (int64_t)pos1.y - pos2.y;\n    int64_t z = (int64_t)pos1.z - pos2.z;\n\n    int32_t scale = 0;\n    while ((int32_t)x != x || (int32_t)y != y || (int32_t)z != z) {\n        scale++;\n        x >>= 1;\n        y >>= 1;\n        z >>= 1;\n    }\n\n    const uint64_t dist = Math_Sqrt64(\n        SQUARE((uint64_t)ABS(x)) + SQUARE((uint64_t)ABS(y))\n        + SQUARE((uint64_t)ABS(z)));\n    if (dist > ((uint64_t)INT32_MAX >> scale)) {\n        return INT32_MAX;\n    }\n    return (int32_t)(dist << scale);\n}\n\nbool XYZ_32_IsNearby(\n    const XYZ_32 pos1, const XYZ_32 pos2, const int32_t distance)\n{\n    const XYZ_32 delta = {\n        .x = pos1.x - pos2.x,\n        .y = pos1.y - pos2.y,\n        .z = pos1.z - pos2.z,\n    };\n    return delta.x > -distance && delta.x < distance && delta.y > -distance\n        && delta.y < distance && delta.z > -distance && delta.z < distance;\n}\n\nint32_t XYZ_32_GetLength(const XYZ_32 pos)\n{\n    return (int32_t)Math_Sqrt64(\n        SQUARE((uint64_t)ABS((int64_t)pos.x))\n        + SQUARE((uint64_t)ABS((int64_t)pos.y))\n        + SQUARE((uint64_t)ABS((int64_t)pos.z)));\n}\n\nint32_t XYZ_32_GetLength2(const XYZ_32 pos)\n{\n    const int64_t dist_64 = XYZ_32_GetLength2_64(pos);\n    return dist_64 > INT32_MAX ? INT32_MAX : (int32_t)dist_64;\n}\n\nint64_t XYZ_32_GetLength2_64(const XYZ_32 pos)\n{\n    return SQUARE((int64_t)pos.x) + SQUARE((int64_t)pos.y)\n        + SQUARE((int64_t)pos.z);\n}\n\nbool XYZ_32_AreEquivalent(const XYZ_32 pos1, const XYZ_32 pos2)\n{\n    return pos1.x == pos2.x && pos1.y == pos2.y && pos1.z == pos2.z;\n}\n\nbool XYZ_16_AreEquivalent(const XYZ_16 rot1, const XYZ_16 rot2)\n{\n    return rot1.x == rot2.x && rot1.y == rot2.y && rot1.z == rot2.z;\n}\n\nXYZ_32 XYZ_32_From16(const XYZ_16 src)\n{\n    return (XYZ_32) { src.x, src.y, src.z };\n}\n\nXYZ_32 XYZ_32_OffsetYaw(\n    const XYZ_32 src, const int16_t yaw, const int32_t distance)\n{\n    return (XYZ_32) {\n        .x = src.x + ((distance * Math_Sin(yaw)) >> W2V_SHIFT),\n        .y = src.y,\n        .z = src.z + ((distance * Math_Cos(yaw)) >> W2V_SHIFT),\n    };\n}\n\nXYZ_32 XYZ_32_FromYawPitch(\n    const int16_t yaw, const int16_t pitch, const int32_t distance)\n{\n    const int32_t cx = Math_Cos(pitch);\n    const int32_t sx = Math_Sin(pitch);\n    const int32_t cy = Math_Cos(yaw);\n    const int32_t sy = Math_Sin(yaw);\n\n    const int32_t horz = (distance * cx) >> W2V_SHIFT;\n    return (XYZ_32) {\n        .x = (int32_t)(((int64_t)horz * sy) >> W2V_SHIFT),\n        .y = -(int32_t)(((int64_t)distance * sx) >> W2V_SHIFT),\n        .z = (int32_t)(((int64_t)horz * cy) >> W2V_SHIFT),\n    };\n}\n\nint64_t XYZ_32_DotProduct_64(const XYZ_32 a, const XYZ_32 b)\n{\n    return (int64_t)a.x * b.x + (int64_t)a.y * b.y + (int64_t)a.z * b.z;\n}\n\nbool XYZ_32_ProjectPointOntoAxis(\n    const XYZ_32 origin, const XYZ_32 axis, const int64_t axis_len2,\n    XYZ_32 *const pos)\n{\n    // Finds the value `t` such that the point\n    //   origin + t * axis\n    // is the closest point on the line to the original *pos, and then writes\n    // that point back into *pos.\n    //\n    // Example:\n    // - origin = (0, 0, 0)\n    // - axis   = (1, 0, 0)  // line is the X axis\n    // - *pos   = (5, 2, -3)\n    // The closest point on the X axis is (5, 0, 0), so after the call:\n    // - *pos = (5, 0, 0)\n\n    if (axis_len2 == 0) {\n        return false;\n    }\n\n    const XYZ_32 offset = {\n        .x = pos->x - origin.x,\n        .y = pos->y - origin.y,\n        .z = pos->z - origin.z,\n    };\n\n    const int64_t t_num = XYZ_32_DotProduct_64(offset, axis);\n    *pos = (XYZ_32) {\n        .x = origin.x + (int32_t)((t_num * axis.x) / axis_len2),\n        .y = origin.y + (int32_t)((t_num * axis.y) / axis_len2),\n        .z = origin.z + (int32_t)((t_num * axis.z) / axis_len2),\n    };\n    return true;\n}\n\nfloat XYZ_F_DotProduct(const XYZ_F a, const XYZ_F b)\n{\n    return a.x * b.x + a.y * b.y + a.z * b.z;\n}\n\nfloat XYZ_F_Length2(const XYZ_F pos)\n{\n    return XYZ_F_DotProduct(pos, pos);\n}\n\nfloat XYZ_F_Length(const XYZ_F pos)\n{\n    return sqrtf(XYZ_F_Length2(pos));\n}\n\nXYZ_F XYZ_F_Subtract(const XYZ_F a, const XYZ_F b)\n{\n    return (XYZ_F) {\n        .x = a.x - b.x,\n        .y = a.y - b.y,\n        .z = a.z - b.z,\n    };\n}\n"
  },
  {
    "path": "src/trx/core/math/geom.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n\nXYZ_32 XYZ_32_From16(XYZ_16 src);\nint16_t XYZ_32_GetYaw(XYZ_32 pos);\nint16_t XYZ_32_GetYawDiff(XYZ_32 pos1, const XYZ_32 pos2);\nint16_t XYZ_32_GetPitch(XYZ_32 pos);\nint32_t XYZ_32_GetDistance(XYZ_32 pos1, XYZ_32 pos2);\n\n// Take length of a vector\nint32_t XYZ_32_GetLength(XYZ_32 pos);\n\n// Take squared length of a vector\nint32_t XYZ_32_GetLength2(XYZ_32 pos);\nint64_t XYZ_32_GetLength2_64(XYZ_32 pos);\n\nint64_t XYZ_32_DotProduct_64(XYZ_32 a, XYZ_32 b);\n\nbool XYZ_32_AreEquivalent(XYZ_32 pos1, XYZ_32 pos2);\nbool XYZ_32_IsNearby(XYZ_32 pos1, XYZ_32 pos2, int32_t distance);\n\nXYZ_32 XYZ_32_FromYawPitch(int16_t yaw, int16_t pitch, int32_t distance);\nXYZ_32 XYZ_32_OffsetYaw(XYZ_32 src, int16_t yaw, int32_t distance);\n\nbool XYZ_32_ProjectPointOntoAxis(\n    XYZ_32 origin, XYZ_32 axis, int64_t axis_len2, XYZ_32 *pos);\n\nbool XYZ_16_AreEquivalent(XYZ_16 rot1, XYZ_16 rot2);\n\nfloat XYZ_F_DotProduct(XYZ_F a, XYZ_F b);\nfloat XYZ_F_Length2(XYZ_F pos);\nfloat XYZ_F_Length(XYZ_F pos);\nXYZ_F XYZ_F_Subtract(XYZ_F a, XYZ_F b);\n"
  },
  {
    "path": "src/trx/core/math/trig.c",
    "content": "#include <trx/core/math/trig.h>\n\n#include <trx/core/math/const.h>\n\nstatic const int16_t m_SinTable[0x402] = {\n    0x0000, 0x0019, 0x0032, 0x004B, 0x0065, 0x007E, 0x0097, 0x00B0, 0x00C9,\n    0x00E2, 0x00FB, 0x0114, 0x012E, 0x0147, 0x0160, 0x0179, 0x0192, 0x01AB,\n    0x01C4, 0x01DD, 0x01F7, 0x0210, 0x0229, 0x0242, 0x025B, 0x0274, 0x028D,\n    0x02A6, 0x02C0, 0x02D9, 0x02F2, 0x030B, 0x0324, 0x033D, 0x0356, 0x036F,\n    0x0388, 0x03A1, 0x03BB, 0x03D4, 0x03ED, 0x0406, 0x041F, 0x0438, 0x0451,\n    0x046A, 0x0483, 0x049C, 0x04B5, 0x04CE, 0x04E7, 0x0500, 0x051A, 0x0533,\n    0x054C, 0x0565, 0x057E, 0x0597, 0x05B0, 0x05C9, 0x05E2, 0x05FB, 0x0614,\n    0x062D, 0x0646, 0x065F, 0x0678, 0x0691, 0x06AA, 0x06C3, 0x06DC, 0x06F5,\n    0x070E, 0x0727, 0x0740, 0x0759, 0x0772, 0x078B, 0x07A4, 0x07BD, 0x07D6,\n    0x07EF, 0x0807, 0x0820, 0x0839, 0x0852, 0x086B, 0x0884, 0x089D, 0x08B6,\n    0x08CF, 0x08E8, 0x0901, 0x0919, 0x0932, 0x094B, 0x0964, 0x097D, 0x0996,\n    0x09AF, 0x09C7, 0x09E0, 0x09F9, 0x0A12, 0x0A2B, 0x0A44, 0x0A5C, 0x0A75,\n    0x0A8E, 0x0AA7, 0x0AC0, 0x0AD8, 0x0AF1, 0x0B0A, 0x0B23, 0x0B3B, 0x0B54,\n    0x0B6D, 0x0B85, 0x0B9E, 0x0BB7, 0x0BD0, 0x0BE8, 0x0C01, 0x0C1A, 0x0C32,\n    0x0C4B, 0x0C64, 0x0C7C, 0x0C95, 0x0CAE, 0x0CC6, 0x0CDF, 0x0CF8, 0x0D10,\n    0x0D29, 0x0D41, 0x0D5A, 0x0D72, 0x0D8B, 0x0DA4, 0x0DBC, 0x0DD5, 0x0DED,\n    0x0E06, 0x0E1E, 0x0E37, 0x0E4F, 0x0E68, 0x0E80, 0x0E99, 0x0EB1, 0x0ECA,\n    0x0EE2, 0x0EFB, 0x0F13, 0x0F2B, 0x0F44, 0x0F5C, 0x0F75, 0x0F8D, 0x0FA5,\n    0x0FBE, 0x0FD6, 0x0FEE, 0x1007, 0x101F, 0x1037, 0x1050, 0x1068, 0x1080,\n    0x1099, 0x10B1, 0x10C9, 0x10E1, 0x10FA, 0x1112, 0x112A, 0x1142, 0x115A,\n    0x1173, 0x118B, 0x11A3, 0x11BB, 0x11D3, 0x11EB, 0x1204, 0x121C, 0x1234,\n    0x124C, 0x1264, 0x127C, 0x1294, 0x12AC, 0x12C4, 0x12DC, 0x12F4, 0x130C,\n    0x1324, 0x133C, 0x1354, 0x136C, 0x1384, 0x139C, 0x13B4, 0x13CC, 0x13E4,\n    0x13FB, 0x1413, 0x142B, 0x1443, 0x145B, 0x1473, 0x148B, 0x14A2, 0x14BA,\n    0x14D2, 0x14EA, 0x1501, 0x1519, 0x1531, 0x1549, 0x1560, 0x1578, 0x1590,\n    0x15A7, 0x15BF, 0x15D7, 0x15EE, 0x1606, 0x161D, 0x1635, 0x164C, 0x1664,\n    0x167C, 0x1693, 0x16AB, 0x16C2, 0x16DA, 0x16F1, 0x1709, 0x1720, 0x1737,\n    0x174F, 0x1766, 0x177E, 0x1795, 0x17AC, 0x17C4, 0x17DB, 0x17F2, 0x180A,\n    0x1821, 0x1838, 0x184F, 0x1867, 0x187E, 0x1895, 0x18AC, 0x18C3, 0x18DB,\n    0x18F2, 0x1909, 0x1920, 0x1937, 0x194E, 0x1965, 0x197C, 0x1993, 0x19AA,\n    0x19C1, 0x19D8, 0x19EF, 0x1A06, 0x1A1D, 0x1A34, 0x1A4B, 0x1A62, 0x1A79,\n    0x1A90, 0x1AA7, 0x1ABE, 0x1AD4, 0x1AEB, 0x1B02, 0x1B19, 0x1B30, 0x1B46,\n    0x1B5D, 0x1B74, 0x1B8A, 0x1BA1, 0x1BB8, 0x1BCE, 0x1BE5, 0x1BFC, 0x1C12,\n    0x1C29, 0x1C3F, 0x1C56, 0x1C6C, 0x1C83, 0x1C99, 0x1CB0, 0x1CC6, 0x1CDD,\n    0x1CF3, 0x1D0A, 0x1D20, 0x1D36, 0x1D4D, 0x1D63, 0x1D79, 0x1D90, 0x1DA6,\n    0x1DBC, 0x1DD3, 0x1DE9, 0x1DFF, 0x1E15, 0x1E2B, 0x1E42, 0x1E58, 0x1E6E,\n    0x1E84, 0x1E9A, 0x1EB0, 0x1EC6, 0x1EDC, 0x1EF2, 0x1F08, 0x1F1E, 0x1F34,\n    0x1F4A, 0x1F60, 0x1F76, 0x1F8C, 0x1FA2, 0x1FB7, 0x1FCD, 0x1FE3, 0x1FF9,\n    0x200F, 0x2024, 0x203A, 0x2050, 0x2065, 0x207B, 0x2091, 0x20A6, 0x20BC,\n    0x20D1, 0x20E7, 0x20FD, 0x2112, 0x2128, 0x213D, 0x2153, 0x2168, 0x217D,\n    0x2193, 0x21A8, 0x21BE, 0x21D3, 0x21E8, 0x21FE, 0x2213, 0x2228, 0x223D,\n    0x2253, 0x2268, 0x227D, 0x2292, 0x22A7, 0x22BC, 0x22D2, 0x22E7, 0x22FC,\n    0x2311, 0x2326, 0x233B, 0x2350, 0x2365, 0x237A, 0x238E, 0x23A3, 0x23B8,\n    0x23CD, 0x23E2, 0x23F7, 0x240B, 0x2420, 0x2435, 0x244A, 0x245E, 0x2473,\n    0x2488, 0x249C, 0x24B1, 0x24C5, 0x24DA, 0x24EF, 0x2503, 0x2518, 0x252C,\n    0x2541, 0x2555, 0x2569, 0x257E, 0x2592, 0x25A6, 0x25BB, 0x25CF, 0x25E3,\n    0x25F8, 0x260C, 0x2620, 0x2634, 0x2648, 0x265C, 0x2671, 0x2685, 0x2699,\n    0x26AD, 0x26C1, 0x26D5, 0x26E9, 0x26FD, 0x2711, 0x2724, 0x2738, 0x274C,\n    0x2760, 0x2774, 0x2788, 0x279B, 0x27AF, 0x27C3, 0x27D6, 0x27EA, 0x27FE,\n    0x2811, 0x2825, 0x2838, 0x284C, 0x2860, 0x2873, 0x2886, 0x289A, 0x28AD,\n    0x28C1, 0x28D4, 0x28E7, 0x28FB, 0x290E, 0x2921, 0x2935, 0x2948, 0x295B,\n    0x296E, 0x2981, 0x2994, 0x29A7, 0x29BB, 0x29CE, 0x29E1, 0x29F4, 0x2A07,\n    0x2A1A, 0x2A2C, 0x2A3F, 0x2A52, 0x2A65, 0x2A78, 0x2A8B, 0x2A9D, 0x2AB0,\n    0x2AC3, 0x2AD6, 0x2AE8, 0x2AFB, 0x2B0D, 0x2B20, 0x2B33, 0x2B45, 0x2B58,\n    0x2B6A, 0x2B7D, 0x2B8F, 0x2BA1, 0x2BB4, 0x2BC6, 0x2BD8, 0x2BEB, 0x2BFD,\n    0x2C0F, 0x2C21, 0x2C34, 0x2C46, 0x2C58, 0x2C6A, 0x2C7C, 0x2C8E, 0x2CA0,\n    0x2CB2, 0x2CC4, 0x2CD6, 0x2CE8, 0x2CFA, 0x2D0C, 0x2D1E, 0x2D2F, 0x2D41,\n    0x2D53, 0x2D65, 0x2D76, 0x2D88, 0x2D9A, 0x2DAB, 0x2DBD, 0x2DCF, 0x2DE0,\n    0x2DF2, 0x2E03, 0x2E15, 0x2E26, 0x2E37, 0x2E49, 0x2E5A, 0x2E6B, 0x2E7D,\n    0x2E8E, 0x2E9F, 0x2EB0, 0x2EC2, 0x2ED3, 0x2EE4, 0x2EF5, 0x2F06, 0x2F17,\n    0x2F28, 0x2F39, 0x2F4A, 0x2F5B, 0x2F6C, 0x2F7D, 0x2F8D, 0x2F9E, 0x2FAF,\n    0x2FC0, 0x2FD0, 0x2FE1, 0x2FF2, 0x3002, 0x3013, 0x3024, 0x3034, 0x3045,\n    0x3055, 0x3066, 0x3076, 0x3087, 0x3097, 0x30A7, 0x30B8, 0x30C8, 0x30D8,\n    0x30E8, 0x30F9, 0x3109, 0x3119, 0x3129, 0x3139, 0x3149, 0x3159, 0x3169,\n    0x3179, 0x3189, 0x3199, 0x31A9, 0x31B9, 0x31C8, 0x31D8, 0x31E8, 0x31F8,\n    0x3207, 0x3217, 0x3227, 0x3236, 0x3246, 0x3255, 0x3265, 0x3274, 0x3284,\n    0x3293, 0x32A3, 0x32B2, 0x32C1, 0x32D0, 0x32E0, 0x32EF, 0x32FE, 0x330D,\n    0x331D, 0x332C, 0x333B, 0x334A, 0x3359, 0x3368, 0x3377, 0x3386, 0x3395,\n    0x33A3, 0x33B2, 0x33C1, 0x33D0, 0x33DF, 0x33ED, 0x33FC, 0x340B, 0x3419,\n    0x3428, 0x3436, 0x3445, 0x3453, 0x3462, 0x3470, 0x347F, 0x348D, 0x349B,\n    0x34AA, 0x34B8, 0x34C6, 0x34D4, 0x34E2, 0x34F1, 0x34FF, 0x350D, 0x351B,\n    0x3529, 0x3537, 0x3545, 0x3553, 0x3561, 0x356E, 0x357C, 0x358A, 0x3598,\n    0x35A5, 0x35B3, 0x35C1, 0x35CE, 0x35DC, 0x35EA, 0x35F7, 0x3605, 0x3612,\n    0x3620, 0x362D, 0x363A, 0x3648, 0x3655, 0x3662, 0x366F, 0x367D, 0x368A,\n    0x3697, 0x36A4, 0x36B1, 0x36BE, 0x36CB, 0x36D8, 0x36E5, 0x36F2, 0x36FF,\n    0x370C, 0x3718, 0x3725, 0x3732, 0x373F, 0x374B, 0x3758, 0x3765, 0x3771,\n    0x377E, 0x378A, 0x3797, 0x37A3, 0x37B0, 0x37BC, 0x37C8, 0x37D5, 0x37E1,\n    0x37ED, 0x37F9, 0x3805, 0x3812, 0x381E, 0x382A, 0x3836, 0x3842, 0x384E,\n    0x385A, 0x3866, 0x3871, 0x387D, 0x3889, 0x3895, 0x38A1, 0x38AC, 0x38B8,\n    0x38C3, 0x38CF, 0x38DB, 0x38E6, 0x38F2, 0x38FD, 0x3909, 0x3914, 0x391F,\n    0x392B, 0x3936, 0x3941, 0x394C, 0x3958, 0x3963, 0x396E, 0x3979, 0x3984,\n    0x398F, 0x399A, 0x39A5, 0x39B0, 0x39BB, 0x39C5, 0x39D0, 0x39DB, 0x39E6,\n    0x39F0, 0x39FB, 0x3A06, 0x3A10, 0x3A1B, 0x3A25, 0x3A30, 0x3A3A, 0x3A45,\n    0x3A4F, 0x3A59, 0x3A64, 0x3A6E, 0x3A78, 0x3A82, 0x3A8D, 0x3A97, 0x3AA1,\n    0x3AAB, 0x3AB5, 0x3ABF, 0x3AC9, 0x3AD3, 0x3ADD, 0x3AE6, 0x3AF0, 0x3AFA,\n    0x3B04, 0x3B0E, 0x3B17, 0x3B21, 0x3B2A, 0x3B34, 0x3B3E, 0x3B47, 0x3B50,\n    0x3B5A, 0x3B63, 0x3B6D, 0x3B76, 0x3B7F, 0x3B88, 0x3B92, 0x3B9B, 0x3BA4,\n    0x3BAD, 0x3BB6, 0x3BBF, 0x3BC8, 0x3BD1, 0x3BDA, 0x3BE3, 0x3BEC, 0x3BF5,\n    0x3BFD, 0x3C06, 0x3C0F, 0x3C17, 0x3C20, 0x3C29, 0x3C31, 0x3C3A, 0x3C42,\n    0x3C4B, 0x3C53, 0x3C5B, 0x3C64, 0x3C6C, 0x3C74, 0x3C7D, 0x3C85, 0x3C8D,\n    0x3C95, 0x3C9D, 0x3CA5, 0x3CAD, 0x3CB5, 0x3CBD, 0x3CC5, 0x3CCD, 0x3CD5,\n    0x3CDD, 0x3CE4, 0x3CEC, 0x3CF4, 0x3CFB, 0x3D03, 0x3D0B, 0x3D12, 0x3D1A,\n    0x3D21, 0x3D28, 0x3D30, 0x3D37, 0x3D3F, 0x3D46, 0x3D4D, 0x3D54, 0x3D5B,\n    0x3D63, 0x3D6A, 0x3D71, 0x3D78, 0x3D7F, 0x3D86, 0x3D8D, 0x3D93, 0x3D9A,\n    0x3DA1, 0x3DA8, 0x3DAF, 0x3DB5, 0x3DBC, 0x3DC2, 0x3DC9, 0x3DD0, 0x3DD6,\n    0x3DDD, 0x3DE3, 0x3DE9, 0x3DF0, 0x3DF6, 0x3DFC, 0x3E03, 0x3E09, 0x3E0F,\n    0x3E15, 0x3E1B, 0x3E21, 0x3E27, 0x3E2D, 0x3E33, 0x3E39, 0x3E3F, 0x3E45,\n    0x3E4A, 0x3E50, 0x3E56, 0x3E5C, 0x3E61, 0x3E67, 0x3E6C, 0x3E72, 0x3E77,\n    0x3E7D, 0x3E82, 0x3E88, 0x3E8D, 0x3E92, 0x3E98, 0x3E9D, 0x3EA2, 0x3EA7,\n    0x3EAC, 0x3EB1, 0x3EB6, 0x3EBB, 0x3EC0, 0x3EC5, 0x3ECA, 0x3ECF, 0x3ED4,\n    0x3ED8, 0x3EDD, 0x3EE2, 0x3EE7, 0x3EEB, 0x3EF0, 0x3EF4, 0x3EF9, 0x3EFD,\n    0x3F02, 0x3F06, 0x3F0A, 0x3F0F, 0x3F13, 0x3F17, 0x3F1C, 0x3F20, 0x3F24,\n    0x3F28, 0x3F2C, 0x3F30, 0x3F34, 0x3F38, 0x3F3C, 0x3F40, 0x3F43, 0x3F47,\n    0x3F4B, 0x3F4F, 0x3F52, 0x3F56, 0x3F5A, 0x3F5D, 0x3F61, 0x3F64, 0x3F68,\n    0x3F6B, 0x3F6E, 0x3F72, 0x3F75, 0x3F78, 0x3F7B, 0x3F7F, 0x3F82, 0x3F85,\n    0x3F88, 0x3F8B, 0x3F8E, 0x3F91, 0x3F94, 0x3F97, 0x3F99, 0x3F9C, 0x3F9F,\n    0x3FA2, 0x3FA4, 0x3FA7, 0x3FAA, 0x3FAC, 0x3FAF, 0x3FB1, 0x3FB4, 0x3FB6,\n    0x3FB8, 0x3FBB, 0x3FBD, 0x3FBF, 0x3FC1, 0x3FC4, 0x3FC6, 0x3FC8, 0x3FCA,\n    0x3FCC, 0x3FCE, 0x3FD0, 0x3FD2, 0x3FD4, 0x3FD5, 0x3FD7, 0x3FD9, 0x3FDB,\n    0x3FDC, 0x3FDE, 0x3FE0, 0x3FE1, 0x3FE3, 0x3FE4, 0x3FE6, 0x3FE7, 0x3FE8,\n    0x3FEA, 0x3FEB, 0x3FEC, 0x3FED, 0x3FEF, 0x3FF0, 0x3FF1, 0x3FF2, 0x3FF3,\n    0x3FF4, 0x3FF5, 0x3FF6, 0x3FF7, 0x3FF7, 0x3FF8, 0x3FF9, 0x3FFA, 0x3FFA,\n    0x3FFB, 0x3FFC, 0x3FFC, 0x3FFD, 0x3FFD, 0x3FFE, 0x3FFE, 0x3FFE, 0x3FFF,\n    0x3FFF, 0x3FFF, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000, 0x4000,\n};\n\nstatic const int32_t m_AtanBaseTable[8] = {\n    0x0000, -0x4000, -0xFFFF, 0xC000, -0x8000, 0x4000, 0x8000, -0xC000,\n};\n\nstatic const int16_t m_AtanAngleTable[0x802] = {\n    0x0000, 0x0005, 0x000A, 0x000F, 0x0014, 0x0019, 0x001F, 0x0024, 0x0029,\n    0x002E, 0x0033, 0x0038, 0x003D, 0x0042, 0x0047, 0x004C, 0x0051, 0x0057,\n    0x005C, 0x0061, 0x0066, 0x006B, 0x0070, 0x0075, 0x007A, 0x007F, 0x0084,\n    0x008A, 0x008F, 0x0094, 0x0099, 0x009E, 0x00A3, 0x00A8, 0x00AD, 0x00B2,\n    0x00B7, 0x00BC, 0x00C2, 0x00C7, 0x00CC, 0x00D1, 0x00D6, 0x00DB, 0x00E0,\n    0x00E5, 0x00EA, 0x00EF, 0x00F4, 0x00FA, 0x00FF, 0x0104, 0x0109, 0x010E,\n    0x0113, 0x0118, 0x011D, 0x0122, 0x0127, 0x012C, 0x0131, 0x0137, 0x013C,\n    0x0141, 0x0146, 0x014B, 0x0150, 0x0155, 0x015A, 0x015F, 0x0164, 0x0169,\n    0x016F, 0x0174, 0x0179, 0x017E, 0x0183, 0x0188, 0x018D, 0x0192, 0x0197,\n    0x019C, 0x01A1, 0x01A6, 0x01AC, 0x01B1, 0x01B6, 0x01BB, 0x01C0, 0x01C5,\n    0x01CA, 0x01CF, 0x01D4, 0x01D9, 0x01DE, 0x01E3, 0x01E9, 0x01EE, 0x01F3,\n    0x01F8, 0x01FD, 0x0202, 0x0207, 0x020C, 0x0211, 0x0216, 0x021B, 0x0220,\n    0x0226, 0x022B, 0x0230, 0x0235, 0x023A, 0x023F, 0x0244, 0x0249, 0x024E,\n    0x0253, 0x0258, 0x025D, 0x0262, 0x0268, 0x026D, 0x0272, 0x0277, 0x027C,\n    0x0281, 0x0286, 0x028B, 0x0290, 0x0295, 0x029A, 0x029F, 0x02A4, 0x02A9,\n    0x02AF, 0x02B4, 0x02B9, 0x02BE, 0x02C3, 0x02C8, 0x02CD, 0x02D2, 0x02D7,\n    0x02DC, 0x02E1, 0x02E6, 0x02EB, 0x02F0, 0x02F6, 0x02FB, 0x0300, 0x0305,\n    0x030A, 0x030F, 0x0314, 0x0319, 0x031E, 0x0323, 0x0328, 0x032D, 0x0332,\n    0x0337, 0x033C, 0x0341, 0x0347, 0x034C, 0x0351, 0x0356, 0x035B, 0x0360,\n    0x0365, 0x036A, 0x036F, 0x0374, 0x0379, 0x037E, 0x0383, 0x0388, 0x038D,\n    0x0392, 0x0397, 0x039C, 0x03A2, 0x03A7, 0x03AC, 0x03B1, 0x03B6, 0x03BB,\n    0x03C0, 0x03C5, 0x03CA, 0x03CF, 0x03D4, 0x03D9, 0x03DE, 0x03E3, 0x03E8,\n    0x03ED, 0x03F2, 0x03F7, 0x03FC, 0x0401, 0x0407, 0x040C, 0x0411, 0x0416,\n    0x041B, 0x0420, 0x0425, 0x042A, 0x042F, 0x0434, 0x0439, 0x043E, 0x0443,\n    0x0448, 0x044D, 0x0452, 0x0457, 0x045C, 0x0461, 0x0466, 0x046B, 0x0470,\n    0x0475, 0x047A, 0x047F, 0x0484, 0x0489, 0x048E, 0x0494, 0x0499, 0x049E,\n    0x04A3, 0x04A8, 0x04AD, 0x04B2, 0x04B7, 0x04BC, 0x04C1, 0x04C6, 0x04CB,\n    0x04D0, 0x04D5, 0x04DA, 0x04DF, 0x04E4, 0x04E9, 0x04EE, 0x04F3, 0x04F8,\n    0x04FD, 0x0502, 0x0507, 0x050C, 0x0511, 0x0516, 0x051B, 0x0520, 0x0525,\n    0x052A, 0x052F, 0x0534, 0x0539, 0x053E, 0x0543, 0x0548, 0x054D, 0x0552,\n    0x0557, 0x055C, 0x0561, 0x0566, 0x056B, 0x0570, 0x0575, 0x057A, 0x057F,\n    0x0584, 0x0589, 0x058E, 0x0593, 0x0598, 0x059D, 0x05A2, 0x05A7, 0x05AC,\n    0x05B1, 0x05B6, 0x05BB, 0x05C0, 0x05C5, 0x05CA, 0x05CF, 0x05D4, 0x05D9,\n    0x05DE, 0x05E3, 0x05E8, 0x05ED, 0x05F2, 0x05F7, 0x05FC, 0x0601, 0x0606,\n    0x060B, 0x0610, 0x0615, 0x061A, 0x061F, 0x0624, 0x0629, 0x062E, 0x0633,\n    0x0638, 0x063D, 0x0642, 0x0647, 0x064C, 0x0651, 0x0656, 0x065B, 0x0660,\n    0x0665, 0x066A, 0x066E, 0x0673, 0x0678, 0x067D, 0x0682, 0x0687, 0x068C,\n    0x0691, 0x0696, 0x069B, 0x06A0, 0x06A5, 0x06AA, 0x06AF, 0x06B4, 0x06B9,\n    0x06BE, 0x06C3, 0x06C8, 0x06CD, 0x06D2, 0x06D7, 0x06DC, 0x06E1, 0x06E5,\n    0x06EA, 0x06EF, 0x06F4, 0x06F9, 0x06FE, 0x0703, 0x0708, 0x070D, 0x0712,\n    0x0717, 0x071C, 0x0721, 0x0726, 0x072B, 0x0730, 0x0735, 0x0739, 0x073E,\n    0x0743, 0x0748, 0x074D, 0x0752, 0x0757, 0x075C, 0x0761, 0x0766, 0x076B,\n    0x0770, 0x0775, 0x077A, 0x077E, 0x0783, 0x0788, 0x078D, 0x0792, 0x0797,\n    0x079C, 0x07A1, 0x07A6, 0x07AB, 0x07B0, 0x07B5, 0x07B9, 0x07BE, 0x07C3,\n    0x07C8, 0x07CD, 0x07D2, 0x07D7, 0x07DC, 0x07E1, 0x07E6, 0x07EB, 0x07EF,\n    0x07F4, 0x07F9, 0x07FE, 0x0803, 0x0808, 0x080D, 0x0812, 0x0817, 0x081C,\n    0x0820, 0x0825, 0x082A, 0x082F, 0x0834, 0x0839, 0x083E, 0x0843, 0x0848,\n    0x084C, 0x0851, 0x0856, 0x085B, 0x0860, 0x0865, 0x086A, 0x086F, 0x0873,\n    0x0878, 0x087D, 0x0882, 0x0887, 0x088C, 0x0891, 0x0896, 0x089A, 0x089F,\n    0x08A4, 0x08A9, 0x08AE, 0x08B3, 0x08B8, 0x08BD, 0x08C1, 0x08C6, 0x08CB,\n    0x08D0, 0x08D5, 0x08DA, 0x08DF, 0x08E3, 0x08E8, 0x08ED, 0x08F2, 0x08F7,\n    0x08FC, 0x0901, 0x0905, 0x090A, 0x090F, 0x0914, 0x0919, 0x091E, 0x0922,\n    0x0927, 0x092C, 0x0931, 0x0936, 0x093B, 0x093F, 0x0944, 0x0949, 0x094E,\n    0x0953, 0x0958, 0x095C, 0x0961, 0x0966, 0x096B, 0x0970, 0x0975, 0x0979,\n    0x097E, 0x0983, 0x0988, 0x098D, 0x0992, 0x0996, 0x099B, 0x09A0, 0x09A5,\n    0x09AA, 0x09AE, 0x09B3, 0x09B8, 0x09BD, 0x09C2, 0x09C6, 0x09CB, 0x09D0,\n    0x09D5, 0x09DA, 0x09DE, 0x09E3, 0x09E8, 0x09ED, 0x09F2, 0x09F6, 0x09FB,\n    0x0A00, 0x0A05, 0x0A0A, 0x0A0E, 0x0A13, 0x0A18, 0x0A1D, 0x0A22, 0x0A26,\n    0x0A2B, 0x0A30, 0x0A35, 0x0A39, 0x0A3E, 0x0A43, 0x0A48, 0x0A4D, 0x0A51,\n    0x0A56, 0x0A5B, 0x0A60, 0x0A64, 0x0A69, 0x0A6E, 0x0A73, 0x0A77, 0x0A7C,\n    0x0A81, 0x0A86, 0x0A8B, 0x0A8F, 0x0A94, 0x0A99, 0x0A9E, 0x0AA2, 0x0AA7,\n    0x0AAC, 0x0AB1, 0x0AB5, 0x0ABA, 0x0ABF, 0x0AC4, 0x0AC8, 0x0ACD, 0x0AD2,\n    0x0AD7, 0x0ADB, 0x0AE0, 0x0AE5, 0x0AE9, 0x0AEE, 0x0AF3, 0x0AF8, 0x0AFC,\n    0x0B01, 0x0B06, 0x0B0B, 0x0B0F, 0x0B14, 0x0B19, 0x0B1E, 0x0B22, 0x0B27,\n    0x0B2C, 0x0B30, 0x0B35, 0x0B3A, 0x0B3F, 0x0B43, 0x0B48, 0x0B4D, 0x0B51,\n    0x0B56, 0x0B5B, 0x0B60, 0x0B64, 0x0B69, 0x0B6E, 0x0B72, 0x0B77, 0x0B7C,\n    0x0B80, 0x0B85, 0x0B8A, 0x0B8F, 0x0B93, 0x0B98, 0x0B9D, 0x0BA1, 0x0BA6,\n    0x0BAB, 0x0BAF, 0x0BB4, 0x0BB9, 0x0BBD, 0x0BC2, 0x0BC7, 0x0BCB, 0x0BD0,\n    0x0BD5, 0x0BD9, 0x0BDE, 0x0BE3, 0x0BE7, 0x0BEC, 0x0BF1, 0x0BF5, 0x0BFA,\n    0x0BFF, 0x0C03, 0x0C08, 0x0C0D, 0x0C11, 0x0C16, 0x0C1B, 0x0C1F, 0x0C24,\n    0x0C29, 0x0C2D, 0x0C32, 0x0C37, 0x0C3B, 0x0C40, 0x0C45, 0x0C49, 0x0C4E,\n    0x0C53, 0x0C57, 0x0C5C, 0x0C60, 0x0C65, 0x0C6A, 0x0C6E, 0x0C73, 0x0C78,\n    0x0C7C, 0x0C81, 0x0C86, 0x0C8A, 0x0C8F, 0x0C93, 0x0C98, 0x0C9D, 0x0CA1,\n    0x0CA6, 0x0CAB, 0x0CAF, 0x0CB4, 0x0CB8, 0x0CBD, 0x0CC2, 0x0CC6, 0x0CCB,\n    0x0CCF, 0x0CD4, 0x0CD9, 0x0CDD, 0x0CE2, 0x0CE6, 0x0CEB, 0x0CF0, 0x0CF4,\n    0x0CF9, 0x0CFD, 0x0D02, 0x0D07, 0x0D0B, 0x0D10, 0x0D14, 0x0D19, 0x0D1E,\n    0x0D22, 0x0D27, 0x0D2B, 0x0D30, 0x0D34, 0x0D39, 0x0D3E, 0x0D42, 0x0D47,\n    0x0D4B, 0x0D50, 0x0D54, 0x0D59, 0x0D5E, 0x0D62, 0x0D67, 0x0D6B, 0x0D70,\n    0x0D74, 0x0D79, 0x0D7D, 0x0D82, 0x0D87, 0x0D8B, 0x0D90, 0x0D94, 0x0D99,\n    0x0D9D, 0x0DA2, 0x0DA6, 0x0DAB, 0x0DAF, 0x0DB4, 0x0DB9, 0x0DBD, 0x0DC2,\n    0x0DC6, 0x0DCB, 0x0DCF, 0x0DD4, 0x0DD8, 0x0DDD, 0x0DE1, 0x0DE6, 0x0DEA,\n    0x0DEF, 0x0DF3, 0x0DF8, 0x0DFC, 0x0E01, 0x0E05, 0x0E0A, 0x0E0F, 0x0E13,\n    0x0E18, 0x0E1C, 0x0E21, 0x0E25, 0x0E2A, 0x0E2E, 0x0E33, 0x0E37, 0x0E3C,\n    0x0E40, 0x0E45, 0x0E49, 0x0E4E, 0x0E52, 0x0E56, 0x0E5B, 0x0E5F, 0x0E64,\n    0x0E68, 0x0E6D, 0x0E71, 0x0E76, 0x0E7A, 0x0E7F, 0x0E83, 0x0E88, 0x0E8C,\n    0x0E91, 0x0E95, 0x0E9A, 0x0E9E, 0x0EA3, 0x0EA7, 0x0EAC, 0x0EB0, 0x0EB4,\n    0x0EB9, 0x0EBD, 0x0EC2, 0x0EC6, 0x0ECB, 0x0ECF, 0x0ED4, 0x0ED8, 0x0EDC,\n    0x0EE1, 0x0EE5, 0x0EEA, 0x0EEE, 0x0EF3, 0x0EF7, 0x0EFC, 0x0F00, 0x0F04,\n    0x0F09, 0x0F0D, 0x0F12, 0x0F16, 0x0F1B, 0x0F1F, 0x0F23, 0x0F28, 0x0F2C,\n    0x0F31, 0x0F35, 0x0F3A, 0x0F3E, 0x0F42, 0x0F47, 0x0F4B, 0x0F50, 0x0F54,\n    0x0F58, 0x0F5D, 0x0F61, 0x0F66, 0x0F6A, 0x0F6E, 0x0F73, 0x0F77, 0x0F7C,\n    0x0F80, 0x0F84, 0x0F89, 0x0F8D, 0x0F91, 0x0F96, 0x0F9A, 0x0F9F, 0x0FA3,\n    0x0FA7, 0x0FAC, 0x0FB0, 0x0FB5, 0x0FB9, 0x0FBD, 0x0FC2, 0x0FC6, 0x0FCA,\n    0x0FCF, 0x0FD3, 0x0FD7, 0x0FDC, 0x0FE0, 0x0FE5, 0x0FE9, 0x0FED, 0x0FF2,\n    0x0FF6, 0x0FFA, 0x0FFF, 0x1003, 0x1007, 0x100C, 0x1010, 0x1014, 0x1019,\n    0x101D, 0x1021, 0x1026, 0x102A, 0x102E, 0x1033, 0x1037, 0x103B, 0x1040,\n    0x1044, 0x1048, 0x104D, 0x1051, 0x1055, 0x105A, 0x105E, 0x1062, 0x1067,\n    0x106B, 0x106F, 0x1073, 0x1078, 0x107C, 0x1080, 0x1085, 0x1089, 0x108D,\n    0x1092, 0x1096, 0x109A, 0x109E, 0x10A3, 0x10A7, 0x10AB, 0x10B0, 0x10B4,\n    0x10B8, 0x10BC, 0x10C1, 0x10C5, 0x10C9, 0x10CE, 0x10D2, 0x10D6, 0x10DA,\n    0x10DF, 0x10E3, 0x10E7, 0x10EB, 0x10F0, 0x10F4, 0x10F8, 0x10FD, 0x1101,\n    0x1105, 0x1109, 0x110E, 0x1112, 0x1116, 0x111A, 0x111F, 0x1123, 0x1127,\n    0x112B, 0x1130, 0x1134, 0x1138, 0x113C, 0x1140, 0x1145, 0x1149, 0x114D,\n    0x1151, 0x1156, 0x115A, 0x115E, 0x1162, 0x1166, 0x116B, 0x116F, 0x1173,\n    0x1177, 0x117C, 0x1180, 0x1184, 0x1188, 0x118C, 0x1191, 0x1195, 0x1199,\n    0x119D, 0x11A1, 0x11A6, 0x11AA, 0x11AE, 0x11B2, 0x11B6, 0x11BB, 0x11BF,\n    0x11C3, 0x11C7, 0x11CB, 0x11CF, 0x11D4, 0x11D8, 0x11DC, 0x11E0, 0x11E4,\n    0x11E9, 0x11ED, 0x11F1, 0x11F5, 0x11F9, 0x11FD, 0x1202, 0x1206, 0x120A,\n    0x120E, 0x1212, 0x1216, 0x121A, 0x121F, 0x1223, 0x1227, 0x122B, 0x122F,\n    0x1233, 0x1237, 0x123C, 0x1240, 0x1244, 0x1248, 0x124C, 0x1250, 0x1254,\n    0x1259, 0x125D, 0x1261, 0x1265, 0x1269, 0x126D, 0x1271, 0x1275, 0x127A,\n    0x127E, 0x1282, 0x1286, 0x128A, 0x128E, 0x1292, 0x1296, 0x129A, 0x129F,\n    0x12A3, 0x12A7, 0x12AB, 0x12AF, 0x12B3, 0x12B7, 0x12BB, 0x12BF, 0x12C3,\n    0x12C7, 0x12CC, 0x12D0, 0x12D4, 0x12D8, 0x12DC, 0x12E0, 0x12E4, 0x12E8,\n    0x12EC, 0x12F0, 0x12F4, 0x12F8, 0x12FC, 0x1301, 0x1305, 0x1309, 0x130D,\n    0x1311, 0x1315, 0x1319, 0x131D, 0x1321, 0x1325, 0x1329, 0x132D, 0x1331,\n    0x1335, 0x1339, 0x133D, 0x1341, 0x1345, 0x1349, 0x134D, 0x1351, 0x1355,\n    0x135A, 0x135E, 0x1362, 0x1366, 0x136A, 0x136E, 0x1372, 0x1376, 0x137A,\n    0x137E, 0x1382, 0x1386, 0x138A, 0x138E, 0x1392, 0x1396, 0x139A, 0x139E,\n    0x13A2, 0x13A6, 0x13AA, 0x13AE, 0x13B2, 0x13B6, 0x13BA, 0x13BE, 0x13C2,\n    0x13C6, 0x13CA, 0x13CE, 0x13D2, 0x13D6, 0x13DA, 0x13DE, 0x13E2, 0x13E6,\n    0x13E9, 0x13ED, 0x13F1, 0x13F5, 0x13F9, 0x13FD, 0x1401, 0x1405, 0x1409,\n    0x140D, 0x1411, 0x1415, 0x1419, 0x141D, 0x1421, 0x1425, 0x1429, 0x142D,\n    0x1431, 0x1435, 0x1439, 0x143D, 0x1440, 0x1444, 0x1448, 0x144C, 0x1450,\n    0x1454, 0x1458, 0x145C, 0x1460, 0x1464, 0x1468, 0x146C, 0x1470, 0x1473,\n    0x1477, 0x147B, 0x147F, 0x1483, 0x1487, 0x148B, 0x148F, 0x1493, 0x1497,\n    0x149B, 0x149E, 0x14A2, 0x14A6, 0x14AA, 0x14AE, 0x14B2, 0x14B6, 0x14BA,\n    0x14BE, 0x14C1, 0x14C5, 0x14C9, 0x14CD, 0x14D1, 0x14D5, 0x14D9, 0x14DD,\n    0x14E0, 0x14E4, 0x14E8, 0x14EC, 0x14F0, 0x14F4, 0x14F8, 0x14FB, 0x14FF,\n    0x1503, 0x1507, 0x150B, 0x150F, 0x1513, 0x1516, 0x151A, 0x151E, 0x1522,\n    0x1526, 0x152A, 0x152D, 0x1531, 0x1535, 0x1539, 0x153D, 0x1541, 0x1544,\n    0x1548, 0x154C, 0x1550, 0x1554, 0x1558, 0x155B, 0x155F, 0x1563, 0x1567,\n    0x156B, 0x156E, 0x1572, 0x1576, 0x157A, 0x157E, 0x1581, 0x1585, 0x1589,\n    0x158D, 0x1591, 0x1594, 0x1598, 0x159C, 0x15A0, 0x15A4, 0x15A7, 0x15AB,\n    0x15AF, 0x15B3, 0x15B7, 0x15BA, 0x15BE, 0x15C2, 0x15C6, 0x15C9, 0x15CD,\n    0x15D1, 0x15D5, 0x15D8, 0x15DC, 0x15E0, 0x15E4, 0x15E8, 0x15EB, 0x15EF,\n    0x15F3, 0x15F7, 0x15FA, 0x15FE, 0x1602, 0x1606, 0x1609, 0x160D, 0x1611,\n    0x1614, 0x1618, 0x161C, 0x1620, 0x1623, 0x1627, 0x162B, 0x162F, 0x1632,\n    0x1636, 0x163A, 0x163E, 0x1641, 0x1645, 0x1649, 0x164C, 0x1650, 0x1654,\n    0x1658, 0x165B, 0x165F, 0x1663, 0x1666, 0x166A, 0x166E, 0x1671, 0x1675,\n    0x1679, 0x167D, 0x1680, 0x1684, 0x1688, 0x168B, 0x168F, 0x1693, 0x1696,\n    0x169A, 0x169E, 0x16A1, 0x16A5, 0x16A9, 0x16AC, 0x16B0, 0x16B4, 0x16B7,\n    0x16BB, 0x16BF, 0x16C2, 0x16C6, 0x16CA, 0x16CD, 0x16D1, 0x16D5, 0x16D8,\n    0x16DC, 0x16E0, 0x16E3, 0x16E7, 0x16EB, 0x16EE, 0x16F2, 0x16F6, 0x16F9,\n    0x16FD, 0x1700, 0x1704, 0x1708, 0x170B, 0x170F, 0x1713, 0x1716, 0x171A,\n    0x171D, 0x1721, 0x1725, 0x1728, 0x172C, 0x1730, 0x1733, 0x1737, 0x173A,\n    0x173E, 0x1742, 0x1745, 0x1749, 0x174C, 0x1750, 0x1754, 0x1757, 0x175B,\n    0x175E, 0x1762, 0x1766, 0x1769, 0x176D, 0x1770, 0x1774, 0x1778, 0x177B,\n    0x177F, 0x1782, 0x1786, 0x1789, 0x178D, 0x1791, 0x1794, 0x1798, 0x179B,\n    0x179F, 0x17A2, 0x17A6, 0x17AA, 0x17AD, 0x17B1, 0x17B4, 0x17B8, 0x17BB,\n    0x17BF, 0x17C2, 0x17C6, 0x17C9, 0x17CD, 0x17D1, 0x17D4, 0x17D8, 0x17DB,\n    0x17DF, 0x17E2, 0x17E6, 0x17E9, 0x17ED, 0x17F0, 0x17F4, 0x17F7, 0x17FB,\n    0x17FE, 0x1802, 0x1806, 0x1809, 0x180D, 0x1810, 0x1814, 0x1817, 0x181B,\n    0x181E, 0x1822, 0x1825, 0x1829, 0x182C, 0x1830, 0x1833, 0x1837, 0x183A,\n    0x183E, 0x1841, 0x1845, 0x1848, 0x184C, 0x184F, 0x1853, 0x1856, 0x185A,\n    0x185D, 0x1860, 0x1864, 0x1867, 0x186B, 0x186E, 0x1872, 0x1875, 0x1879,\n    0x187C, 0x1880, 0x1883, 0x1887, 0x188A, 0x188E, 0x1891, 0x1894, 0x1898,\n    0x189B, 0x189F, 0x18A2, 0x18A6, 0x18A9, 0x18AD, 0x18B0, 0x18B3, 0x18B7,\n    0x18BA, 0x18BE, 0x18C1, 0x18C5, 0x18C8, 0x18CC, 0x18CF, 0x18D2, 0x18D6,\n    0x18D9, 0x18DD, 0x18E0, 0x18E3, 0x18E7, 0x18EA, 0x18EE, 0x18F1, 0x18F5,\n    0x18F8, 0x18FB, 0x18FF, 0x1902, 0x1906, 0x1909, 0x190C, 0x1910, 0x1913,\n    0x1917, 0x191A, 0x191D, 0x1921, 0x1924, 0x1928, 0x192B, 0x192E, 0x1932,\n    0x1935, 0x1938, 0x193C, 0x193F, 0x1943, 0x1946, 0x1949, 0x194D, 0x1950,\n    0x1953, 0x1957, 0x195A, 0x195D, 0x1961, 0x1964, 0x1968, 0x196B, 0x196E,\n    0x1972, 0x1975, 0x1978, 0x197C, 0x197F, 0x1982, 0x1986, 0x1989, 0x198C,\n    0x1990, 0x1993, 0x1996, 0x199A, 0x199D, 0x19A0, 0x19A4, 0x19A7, 0x19AA,\n    0x19AE, 0x19B1, 0x19B4, 0x19B8, 0x19BB, 0x19BE, 0x19C2, 0x19C5, 0x19C8,\n    0x19CC, 0x19CF, 0x19D2, 0x19D5, 0x19D9, 0x19DC, 0x19DF, 0x19E3, 0x19E6,\n    0x19E9, 0x19ED, 0x19F0, 0x19F3, 0x19F6, 0x19FA, 0x19FD, 0x1A00, 0x1A04,\n    0x1A07, 0x1A0A, 0x1A0D, 0x1A11, 0x1A14, 0x1A17, 0x1A1B, 0x1A1E, 0x1A21,\n    0x1A24, 0x1A28, 0x1A2B, 0x1A2E, 0x1A31, 0x1A35, 0x1A38, 0x1A3B, 0x1A3E,\n    0x1A42, 0x1A45, 0x1A48, 0x1A4B, 0x1A4F, 0x1A52, 0x1A55, 0x1A58, 0x1A5C,\n    0x1A5F, 0x1A62, 0x1A65, 0x1A69, 0x1A6C, 0x1A6F, 0x1A72, 0x1A76, 0x1A79,\n    0x1A7C, 0x1A7F, 0x1A83, 0x1A86, 0x1A89, 0x1A8C, 0x1A8F, 0x1A93, 0x1A96,\n    0x1A99, 0x1A9C, 0x1A9F, 0x1AA3, 0x1AA6, 0x1AA9, 0x1AAC, 0x1AB0, 0x1AB3,\n    0x1AB6, 0x1AB9, 0x1ABC, 0x1AC0, 0x1AC3, 0x1AC6, 0x1AC9, 0x1ACC, 0x1ACF,\n    0x1AD3, 0x1AD6, 0x1AD9, 0x1ADC, 0x1ADF, 0x1AE3, 0x1AE6, 0x1AE9, 0x1AEC,\n    0x1AEF, 0x1AF2, 0x1AF6, 0x1AF9, 0x1AFC, 0x1AFF, 0x1B02, 0x1B05, 0x1B09,\n    0x1B0C, 0x1B0F, 0x1B12, 0x1B15, 0x1B18, 0x1B1C, 0x1B1F, 0x1B22, 0x1B25,\n    0x1B28, 0x1B2B, 0x1B2E, 0x1B32, 0x1B35, 0x1B38, 0x1B3B, 0x1B3E, 0x1B41,\n    0x1B44, 0x1B48, 0x1B4B, 0x1B4E, 0x1B51, 0x1B54, 0x1B57, 0x1B5A, 0x1B5D,\n    0x1B61, 0x1B64, 0x1B67, 0x1B6A, 0x1B6D, 0x1B70, 0x1B73, 0x1B76, 0x1B79,\n    0x1B7D, 0x1B80, 0x1B83, 0x1B86, 0x1B89, 0x1B8C, 0x1B8F, 0x1B92, 0x1B95,\n    0x1B98, 0x1B9C, 0x1B9F, 0x1BA2, 0x1BA5, 0x1BA8, 0x1BAB, 0x1BAE, 0x1BB1,\n    0x1BB4, 0x1BB7, 0x1BBA, 0x1BBD, 0x1BC1, 0x1BC4, 0x1BC7, 0x1BCA, 0x1BCD,\n    0x1BD0, 0x1BD3, 0x1BD6, 0x1BD9, 0x1BDC, 0x1BDF, 0x1BE2, 0x1BE5, 0x1BE8,\n    0x1BEB, 0x1BEE, 0x1BF2, 0x1BF5, 0x1BF8, 0x1BFB, 0x1BFE, 0x1C01, 0x1C04,\n    0x1C07, 0x1C0A, 0x1C0D, 0x1C10, 0x1C13, 0x1C16, 0x1C19, 0x1C1C, 0x1C1F,\n    0x1C22, 0x1C25, 0x1C28, 0x1C2B, 0x1C2E, 0x1C31, 0x1C34, 0x1C37, 0x1C3A,\n    0x1C3D, 0x1C40, 0x1C43, 0x1C46, 0x1C49, 0x1C4C, 0x1C4F, 0x1C52, 0x1C55,\n    0x1C58, 0x1C5B, 0x1C5E, 0x1C61, 0x1C64, 0x1C67, 0x1C6A, 0x1C6D, 0x1C70,\n    0x1C73, 0x1C76, 0x1C79, 0x1C7C, 0x1C7F, 0x1C82, 0x1C85, 0x1C88, 0x1C8B,\n    0x1C8E, 0x1C91, 0x1C94, 0x1C97, 0x1C9A, 0x1C9D, 0x1CA0, 0x1CA3, 0x1CA6,\n    0x1CA9, 0x1CAC, 0x1CAF, 0x1CB2, 0x1CB5, 0x1CB8, 0x1CBB, 0x1CBE, 0x1CC1,\n    0x1CC3, 0x1CC6, 0x1CC9, 0x1CCC, 0x1CCF, 0x1CD2, 0x1CD5, 0x1CD8, 0x1CDB,\n    0x1CDE, 0x1CE1, 0x1CE4, 0x1CE7, 0x1CEA, 0x1CED, 0x1CF0, 0x1CF3, 0x1CF5,\n    0x1CF8, 0x1CFB, 0x1CFE, 0x1D01, 0x1D04, 0x1D07, 0x1D0A, 0x1D0D, 0x1D10,\n    0x1D13, 0x1D16, 0x1D18, 0x1D1B, 0x1D1E, 0x1D21, 0x1D24, 0x1D27, 0x1D2A,\n    0x1D2D, 0x1D30, 0x1D33, 0x1D35, 0x1D38, 0x1D3B, 0x1D3E, 0x1D41, 0x1D44,\n    0x1D47, 0x1D4A, 0x1D4D, 0x1D4F, 0x1D52, 0x1D55, 0x1D58, 0x1D5B, 0x1D5E,\n    0x1D61, 0x1D64, 0x1D66, 0x1D69, 0x1D6C, 0x1D6F, 0x1D72, 0x1D75, 0x1D78,\n    0x1D7B, 0x1D7D, 0x1D80, 0x1D83, 0x1D86, 0x1D89, 0x1D8C, 0x1D8E, 0x1D91,\n    0x1D94, 0x1D97, 0x1D9A, 0x1D9D, 0x1DA0, 0x1DA2, 0x1DA5, 0x1DA8, 0x1DAB,\n    0x1DAE, 0x1DB1, 0x1DB3, 0x1DB6, 0x1DB9, 0x1DBC, 0x1DBF, 0x1DC2, 0x1DC4,\n    0x1DC7, 0x1DCA, 0x1DCD, 0x1DD0, 0x1DD3, 0x1DD5, 0x1DD8, 0x1DDB, 0x1DDE,\n    0x1DE1, 0x1DE3, 0x1DE6, 0x1DE9, 0x1DEC, 0x1DEF, 0x1DF1, 0x1DF4, 0x1DF7,\n    0x1DFA, 0x1DFD, 0x1DFF, 0x1E02, 0x1E05, 0x1E08, 0x1E0B, 0x1E0D, 0x1E10,\n    0x1E13, 0x1E16, 0x1E19, 0x1E1B, 0x1E1E, 0x1E21, 0x1E24, 0x1E26, 0x1E29,\n    0x1E2C, 0x1E2F, 0x1E32, 0x1E34, 0x1E37, 0x1E3A, 0x1E3D, 0x1E3F, 0x1E42,\n    0x1E45, 0x1E48, 0x1E4A, 0x1E4D, 0x1E50, 0x1E53, 0x1E55, 0x1E58, 0x1E5B,\n    0x1E5E, 0x1E60, 0x1E63, 0x1E66, 0x1E69, 0x1E6B, 0x1E6E, 0x1E71, 0x1E74,\n    0x1E76, 0x1E79, 0x1E7C, 0x1E7F, 0x1E81, 0x1E84, 0x1E87, 0x1E8A, 0x1E8C,\n    0x1E8F, 0x1E92, 0x1E94, 0x1E97, 0x1E9A, 0x1E9D, 0x1E9F, 0x1EA2, 0x1EA5,\n    0x1EA8, 0x1EAA, 0x1EAD, 0x1EB0, 0x1EB2, 0x1EB5, 0x1EB8, 0x1EBA, 0x1EBD,\n    0x1EC0, 0x1EC3, 0x1EC5, 0x1EC8, 0x1ECB, 0x1ECD, 0x1ED0, 0x1ED3, 0x1ED5,\n    0x1ED8, 0x1EDB, 0x1EDE, 0x1EE0, 0x1EE3, 0x1EE6, 0x1EE8, 0x1EEB, 0x1EEE,\n    0x1EF0, 0x1EF3, 0x1EF6, 0x1EF8, 0x1EFB, 0x1EFE, 0x1F00, 0x1F03, 0x1F06,\n    0x1F08, 0x1F0B, 0x1F0E, 0x1F10, 0x1F13, 0x1F16, 0x1F18, 0x1F1B, 0x1F1E,\n    0x1F20, 0x1F23, 0x1F26, 0x1F28, 0x1F2B, 0x1F2E, 0x1F30, 0x1F33, 0x1F36,\n    0x1F38, 0x1F3B, 0x1F3D, 0x1F40, 0x1F43, 0x1F45, 0x1F48, 0x1F4B, 0x1F4D,\n    0x1F50, 0x1F53, 0x1F55, 0x1F58, 0x1F5A, 0x1F5D, 0x1F60, 0x1F62, 0x1F65,\n    0x1F68, 0x1F6A, 0x1F6D, 0x1F6F, 0x1F72, 0x1F75, 0x1F77, 0x1F7A, 0x1F7C,\n    0x1F7F, 0x1F82, 0x1F84, 0x1F87, 0x1F8A, 0x1F8C, 0x1F8F, 0x1F91, 0x1F94,\n    0x1F97, 0x1F99, 0x1F9C, 0x1F9E, 0x1FA1, 0x1FA4, 0x1FA6, 0x1FA9, 0x1FAB,\n    0x1FAE, 0x1FB0, 0x1FB3, 0x1FB6, 0x1FB8, 0x1FBB, 0x1FBD, 0x1FC0, 0x1FC3,\n    0x1FC5, 0x1FC8, 0x1FCA, 0x1FCD, 0x1FCF, 0x1FD2, 0x1FD5, 0x1FD7, 0x1FDA,\n    0x1FDC, 0x1FDF, 0x1FE1, 0x1FE4, 0x1FE6, 0x1FE9, 0x1FEC, 0x1FEE, 0x1FF1,\n    0x1FF3, 0x1FF6, 0x1FF8, 0x1FFB, 0x1FFD, 0x2000, 0x2000,\n};\n\nint32_t Math_Cos(int32_t angle)\n{\n    return Math_Sin(angle + DEG_90);\n}\n\nint32_t Math_Sin(int32_t angle)\n{\n    uint16_t sector = (uint16_t)angle & (DEG_180 - 1);\n    if (sector > DEG_90) {\n        sector = DEG_180 - sector;\n    }\n    int16_t result = m_SinTable[sector >> 4];\n    if ((uint16_t)angle >= DEG_180) {\n        result = -result;\n    }\n    return result;\n}\n\nint32_t Math_Atan(int32_t x, int32_t y)\n{\n    if (x == 0 && y == 0) {\n        return 0;\n    }\n\n    int8_t base = 0;\n    if (x < 0) {\n        base |= 4;\n        x = -x;\n    }\n\n    if (y < 0) {\n        base |= 2;\n        y = -y;\n    }\n\n    if (y > x) {\n        base |= 1;\n        int32_t tmp = x;\n        x = y;\n        y = tmp;\n    }\n\n    int32_t result = m_AtanBaseTable[base] + m_AtanAngleTable[0x800 * y / x];\n    if (result < 0) {\n        result = -result;\n    }\n\n    return result;\n}\n"
  },
  {
    "path": "src/trx/core/math/trig.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nint32_t Math_Cos(int32_t angle);\nint32_t Math_Sin(int32_t angle);\nint32_t Math_Atan(int32_t x, int32_t y);\n"
  },
  {
    "path": "src/trx/core/math/types.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\n#pragma pack(push, 1)\n\ntypedef struct {\n    int32_t x;\n    int32_t z;\n} XZ_32;\n\ntypedef struct {\n    int16_t x;\n    int16_t z;\n} XZ_16;\n\ntypedef struct {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n} XYZ_32;\n\ntypedef struct {\n    int16_t x;\n    int16_t y;\n    int16_t z;\n} XYZ_16;\n\ntypedef struct {\n    bool x;\n    bool y;\n    bool z;\n} XYZ_BOOL;\n\ntypedef struct {\n    float x, y, z;\n} XYZ_F;\n\ntypedef struct {\n    float x, y, z, w;\n} XYZW_F;\n\ntypedef enum {\n    DIR_UNKNOWN = -1,\n    DIR_NORTH = 0,\n    DIR_EAST = 1,\n    DIR_SOUTH = 2,\n    DIR_WEST = 3,\n} DIRECTION;\n\ntypedef struct {\n    XYZ_16 min;\n    XYZ_16 max;\n} BOUNDS_16;\n\ntypedef struct {\n    XYZ_32 min;\n    XYZ_32 max;\n} BOUNDS_32;\n\n#pragma pack(pop)\n"
  },
  {
    "path": "src/trx/core/math/util.c",
    "content": "#include <trx/core/math/util.h>\n\nbool Bounds32_Intersect(const BOUNDS_32 *const a, const BOUNDS_32 *const b)\n{\n    return !(\n        a->min.x > b->max.x || a->max.x < b->min.x || a->min.y > b->max.y\n        || a->max.y < b->min.y || a->min.z > b->max.z || a->max.z < b->min.z);\n}\n"
  },
  {
    "path": "src/trx/core/math/util.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n\nbool Bounds32_Intersect(const BOUNDS_32 *a, const BOUNDS_32 *b);\n"
  },
  {
    "path": "src/trx/core/math.h",
    "content": "#pragma once\n\n#include <trx/core/math/const.h>\n#include <trx/core/math/func.h>\n#include <trx/core/math/geom.h>\n#include <trx/core/math/trig.h>\n#include <trx/core/math/types.h>\n#include <trx/core/math/util.h>\n"
  },
  {
    "path": "src/trx/core/memory.c",
    "content": "#include <trx/core/memory.h>\n\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n\n#include <stdlib.h>\n#include <string.h>\n\nstatic MEMORY_ARENA_CHUNK *M_ArenaAllocChunk(\n    MEMORY_ARENA_ALLOCATOR *const allocator, const size_t size)\n{\n    const size_t new_chunk_size = MAX(allocator->default_chunk_size, size);\n    MEMORY_ARENA_CHUNK *const new_chunk =\n        Memory_Alloc(sizeof(MEMORY_ARENA_CHUNK) + new_chunk_size);\n    new_chunk->memory = (char *)new_chunk + sizeof(MEMORY_ARENA_CHUNK);\n    new_chunk->size = new_chunk_size;\n    new_chunk->offset = 0;\n    new_chunk->next = nullptr;\n    return new_chunk;\n}\n\nsize_t Memory_Align(const size_t size)\n{\n    return (size + 7) & ~7;\n}\n\nvoid *Memory_Alloc(const size_t size)\n{\n    void *result = malloc(size);\n    ASSERT(result != nullptr);\n    memset(result, 0, size);\n    return result;\n}\n\nvoid *Memory_Realloc(void *const memory, const size_t size)\n{\n    void *result = realloc(memory, size);\n    ASSERT(result != nullptr);\n    return result;\n}\n\nvoid Memory_Free(void *const memory)\n{\n    if (memory != nullptr) {\n        free(memory);\n    }\n}\n\nvoid Memory_FreePointer(void *arg)\n{\n    ASSERT(arg != nullptr);\n    void *memory;\n    memcpy(&memory, arg, sizeof(void *));\n    memcpy(arg, &(void *) { nullptr }, sizeof(void *));\n    Memory_Free(memory);\n}\n\nvoid *Memory_Dup(const void *const buffer, const size_t size)\n{\n    ASSERT(buffer != nullptr);\n    char *memory = Memory_Alloc(size);\n    memcpy(memory, buffer, size);\n    return memory;\n}\n\nchar *Memory_DupStr(const char *const string)\n{\n    if (string == nullptr) {\n        return nullptr;\n    }\n    char *memory = Memory_Alloc(strlen(string) + 1);\n    strcpy(memory, string);\n    return memory;\n}\n\nvoid *Memory_ArenaAlloc(\n    MEMORY_ARENA_ALLOCATOR *const allocator, const size_t size)\n{\n    // Ensure a default chunk size is set.\n    if (allocator->default_chunk_size == 0) {\n        allocator->default_chunk_size = 1024 * 4; // default to 4K\n    }\n\n    // Find first chunk that has enough space.\n    MEMORY_ARENA_CHUNK *chunk = allocator->current_chunk;\n    while (chunk != nullptr && chunk->offset + size > chunk->size) {\n        chunk = chunk->next;\n    }\n\n    // If no chunk satisfies this criteria, append a new chunk.\n    if (chunk == nullptr) {\n        chunk = M_ArenaAllocChunk(allocator, size);\n        if (allocator->current_chunk != nullptr) {\n            chunk->next = allocator->current_chunk->next;\n            allocator->current_chunk->next = chunk;\n        }\n        allocator->current_chunk = chunk;\n        if (allocator->first_chunk == nullptr) {\n            allocator->first_chunk = chunk;\n        }\n    }\n\n    ASSERT(chunk != nullptr);\n\n    // Allocate from the current chunk.\n    void *const result = (char *)chunk->memory + chunk->offset;\n    chunk->offset += size;\n    return result;\n}\n\nvoid Memory_ArenaReset(MEMORY_ARENA_ALLOCATOR *const allocator)\n{\n    MEMORY_ARENA_CHUNK *chunk = allocator->first_chunk;\n    while (chunk != nullptr) {\n        memset(chunk->memory, 0, chunk->size);\n        chunk->offset = 0;\n        chunk = chunk->next;\n    }\n    allocator->current_chunk = allocator->first_chunk;\n}\n\nvoid Memory_ArenaFree(MEMORY_ARENA_ALLOCATOR *const allocator)\n{\n    MEMORY_ARENA_CHUNK *chunk = allocator->first_chunk;\n    while (chunk != nullptr) {\n        MEMORY_ARENA_CHUNK *const next = chunk->next;\n        Memory_Free(chunk);\n        chunk = next;\n    }\n    allocator->first_chunk = nullptr;\n    allocator->current_chunk = nullptr;\n}\n"
  },
  {
    "path": "src/trx/core/memory.h",
    "content": "#pragma once\n\n#include <stddef.h>\n\n// Basic memory utilities that exit the game in case the system runs out of\n// memory.\n\n// Arena allocator - a buffer that only grows, until it's reset. Doesn't\n// support freeing while in-use.\ntypedef struct MEMORY_ARENA_CHUNK {\n    void *memory;\n    size_t size;\n    size_t offset;\n    struct MEMORY_ARENA_CHUNK *next;\n} MEMORY_ARENA_CHUNK;\n\ntypedef struct {\n    MEMORY_ARENA_CHUNK *first_chunk;\n    MEMORY_ARENA_CHUNK *current_chunk;\n    size_t default_chunk_size;\n} MEMORY_ARENA_ALLOCATOR;\n\n// Align byte count to the platform-specific pointer size.\nsize_t Memory_Align(size_t size);\n\n// Allocate n bytes. In case the memory allocation fails, shows an error to the\n// user and exits the application. The allocated memory is filled with zeros.\nvoid *Memory_Alloc(size_t size);\n\n// Reallocate existing memory to n bytes, returning an address to the\n// reallocated memory. In case the memory allocation fails, shows an error to\n// the user and exits the application. All pointers to the old memory address\n// become invalid. Preserves the previous memory contents. If the memory is\n// nullptr, the function acts like Memory_Alloc.\nvoid *Memory_Realloc(void *memory, size_t size);\n\n// Frees the memory associated with a given address. If the memory is nullptr,\n// the function is a no-op.\nvoid Memory_Free(void *memory);\n\n// Frees the memory associated with a given pointer and sets it to nullptr. The\n// user is expected to pass a pointer of their variable like so:\n//\n// char *mem = Memory_Alloc(10);\n// Memory_FreePointer(&mem);\n// (mem is now nullptr)\n//\n// Giving a nullptr to this function is a fatal error. Passing mem directly is\n// also an error.\nvoid Memory_FreePointer(void *memory);\n\n// Duplicates a buffer. In case the memory allocation fails, shows an error to\n// the user and exits the application.\n// Giving a nullptr to this function is a fatal error.\nvoid *Memory_Dup(const void *buffer, size_t size);\n\n// Duplicates a string. In case the memory allocation fails, shows an error to\n// the user and exits the application. The string must be nullptr-terminated.\n// Giving a nullptr to this function returns nullptr.\nchar *Memory_DupStr(const char *string);\n\n// Allocate n bytes using the arena allocator. If there's insufficient memory,\n// grow the buffer using internal growth function. The allocated memory is\n// filled with zeros.\nvoid *Memory_ArenaAlloc(MEMORY_ARENA_ALLOCATOR *allocator, size_t size);\n\n// Resets the buffer used by the arena allocator, but does not free the memory.\n// allocator must not be a nullptr. Used to reset the buffer, but not suffer\n// from performance penalty associated with reallocating the actual memory.\nvoid Memory_ArenaReset(MEMORY_ARENA_ALLOCATOR *allocator);\n\n// Frees the entire buffer owned by the arena allocator. allocator must not be\n// nullptr.\nvoid Memory_ArenaFree(MEMORY_ARENA_ALLOCATOR *allocator);\n\n#define AUTO_FREE __attribute__((cleanup(Memory_FreePointer)))\n"
  },
  {
    "path": "src/trx/core/shell.h",
    "content": "#pragma once\n\nvoid Shell_ExitSystem(const char *message);\nvoid Shell_ExitSystemEx(const char *log_message, const char *dialog_message);\nvoid Shell_ExitSystemFmt(const char *fmt, ...);\n"
  },
  {
    "path": "src/trx/core/strings/case_funcs.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n\n#include <ctype.h>\n#include <string.h>\n\n// Mapping of lowercase to uppercase characters beyond ASCII.\nstatic struct {\n    const char *lower;\n    const char *upper;\n} m_CaseMap[] = {\n#define X_CASE_MAP(low, up) { low, up },\n#include <trx/core/strings/case_map.def>\n#undef X_CASE_MAP\n    { .lower = nullptr, .upper = nullptr },\n};\n\nsize_t String_GetCharByteSize(const char *const ptr)\n{\n    // Check for named escape sequence.\n    if (*ptr == '\\\\' && *(ptr + 1) == '{') {\n        const char *end = strchr(ptr + 2, '}');\n        if (end != nullptr) {\n            return end + 1 - ptr;\n        }\n        return 1;\n    }\n\n    // clang-format off\n    // UTF-8 sequence lengths\n    if ((*ptr & 0x80) == 0x00) { return 1; } // 1-byte sequence\n    if ((*ptr & 0xE0) == 0xC0) { return 2; } // 2-byte sequence\n    if ((*ptr & 0xF0) == 0xE0) { return 3; } // 3-byte sequence\n    if ((*ptr & 0xF8) == 0xF0) { return 4; } // 4-byte sequence\n    // clang-format on\n\n    // Fallback to 1\n    return 1;\n}\n\nchar *String_ToUpper(const char *const text)\n{\n    if (text == nullptr) {\n        return nullptr;\n    }\n\n    const size_t text_len = strlen(text);\n    char *const upper_text = Memory_Alloc(text_len + 1);\n    const char *src = text;\n    char *dest = upper_text;\n\n    while (*src != '\\0') {\n        bool mapped = false;\n        for (size_t i = 0; m_CaseMap[i].lower != nullptr; i++) {\n            const char *const lower = m_CaseMap[i].lower;\n            const char *const upper = m_CaseMap[i].upper;\n            const size_t lower_len = strlen(lower);\n            if (strncmp(src, lower, lower_len) == 0) {\n                const size_t upper_len = strlen(upper);\n                memcpy(dest, upper, upper_len);\n                dest += upper_len;\n                src += lower_len;\n                mapped = true;\n                break;\n            }\n        }\n        if (mapped) {\n            continue;\n        }\n\n        const size_t char_len = String_GetCharByteSize(src);\n        memcpy(dest, src, char_len);\n        dest += char_len;\n        src += char_len;\n    }\n\n    *dest = '\\0';\n    return upper_text;\n}\n\nchar *String_ToUpperPattern(const char *const pattern)\n{\n    char *const upper_pattern = Memory_DupStr(pattern);\n    char *q = upper_pattern;\n    while (*q != '\\0') {\n        if (*q == '%') {\n            q++;\n            while (*q != '\\0' && !strchr(\"diouxXfFeEgGaAcspn%\", *q)) {\n                q++;\n            }\n            if (*q != '\\0') {\n                q++;\n            }\n        } else {\n            *q = (char)toupper((unsigned char)*q);\n            q++;\n        }\n    }\n    return upper_pattern;\n}\n"
  },
  {
    "path": "src/trx/core/strings/case_map.def",
    "content": "// This file is autogenerated - do not edit.\n// See tools/glyphs/generate_case_map for details.\n\nX_CASE_MAP(\"a\", \"A\")\nX_CASE_MAP(\"b\", \"B\")\nX_CASE_MAP(\"c\", \"C\")\nX_CASE_MAP(\"d\", \"D\")\nX_CASE_MAP(\"e\", \"E\")\nX_CASE_MAP(\"f\", \"F\")\nX_CASE_MAP(\"g\", \"G\")\nX_CASE_MAP(\"h\", \"H\")\nX_CASE_MAP(\"i\", \"I\")\nX_CASE_MAP(\"j\", \"J\")\nX_CASE_MAP(\"k\", \"K\")\nX_CASE_MAP(\"l\", \"L\")\nX_CASE_MAP(\"m\", \"M\")\nX_CASE_MAP(\"n\", \"N\")\nX_CASE_MAP(\"o\", \"O\")\nX_CASE_MAP(\"p\", \"P\")\nX_CASE_MAP(\"q\", \"Q\")\nX_CASE_MAP(\"r\", \"R\")\nX_CASE_MAP(\"s\", \"S\")\nX_CASE_MAP(\"t\", \"T\")\nX_CASE_MAP(\"u\", \"U\")\nX_CASE_MAP(\"v\", \"V\")\nX_CASE_MAP(\"w\", \"W\")\nX_CASE_MAP(\"x\", \"X\")\nX_CASE_MAP(\"y\", \"Y\")\nX_CASE_MAP(\"z\", \"Z\")\nX_CASE_MAP(\"ª\", \"ª\")\nX_CASE_MAP(\"µ\", \"Μ\")\nX_CASE_MAP(\"º\", \"º\")\nX_CASE_MAP(\"à\", \"À\")\nX_CASE_MAP(\"á\", \"Á\")\nX_CASE_MAP(\"â\", \"Â\")\nX_CASE_MAP(\"ã\", \"Ã\")\nX_CASE_MAP(\"ä\", \"Ä\")\nX_CASE_MAP(\"å\", \"Å\")\nX_CASE_MAP(\"æ\", \"Æ\")\nX_CASE_MAP(\"ç\", \"Ç\")\nX_CASE_MAP(\"è\", \"È\")\nX_CASE_MAP(\"é\", \"É\")\nX_CASE_MAP(\"ê\", \"Ê\")\nX_CASE_MAP(\"ë\", \"Ë\")\nX_CASE_MAP(\"ì\", \"Ì\")\nX_CASE_MAP(\"í\", \"Í\")\nX_CASE_MAP(\"î\", \"Î\")\nX_CASE_MAP(\"ï\", \"Ï\")\nX_CASE_MAP(\"ð\", \"Ð\")\nX_CASE_MAP(\"ñ\", \"Ñ\")\nX_CASE_MAP(\"ò\", \"Ò\")\nX_CASE_MAP(\"ó\", \"Ó\")\nX_CASE_MAP(\"ô\", \"Ô\")\nX_CASE_MAP(\"õ\", \"Õ\")\nX_CASE_MAP(\"ö\", \"Ö\")\nX_CASE_MAP(\"ø\", \"Ø\")\nX_CASE_MAP(\"ù\", \"Ù\")\nX_CASE_MAP(\"ú\", \"Ú\")\nX_CASE_MAP(\"û\", \"Û\")\nX_CASE_MAP(\"ü\", \"Ü\")\nX_CASE_MAP(\"ý\", \"Ý\")\nX_CASE_MAP(\"þ\", \"Þ\")\nX_CASE_MAP(\"ÿ\", \"Ÿ\")\nX_CASE_MAP(\"ā\", \"Ā\")\nX_CASE_MAP(\"ă\", \"Ă\")\nX_CASE_MAP(\"ą\", \"Ą\")\nX_CASE_MAP(\"ć\", \"Ć\")\nX_CASE_MAP(\"ĉ\", \"Ĉ\")\nX_CASE_MAP(\"ċ\", \"Ċ\")\nX_CASE_MAP(\"č\", \"Č\")\nX_CASE_MAP(\"ď\", \"Ď\")\nX_CASE_MAP(\"đ\", \"Đ\")\nX_CASE_MAP(\"ē\", \"Ē\")\nX_CASE_MAP(\"ĕ\", \"Ĕ\")\nX_CASE_MAP(\"ė\", \"Ė\")\nX_CASE_MAP(\"ę\", \"Ę\")\nX_CASE_MAP(\"ě\", \"Ě\")\nX_CASE_MAP(\"ĝ\", \"Ĝ\")\nX_CASE_MAP(\"ğ\", \"Ğ\")\nX_CASE_MAP(\"ġ\", \"Ġ\")\nX_CASE_MAP(\"ģ\", \"Ģ\")\nX_CASE_MAP(\"ĥ\", \"Ĥ\")\nX_CASE_MAP(\"ħ\", \"Ħ\")\nX_CASE_MAP(\"ĩ\", \"Ĩ\")\nX_CASE_MAP(\"ī\", \"Ī\")\nX_CASE_MAP(\"ĭ\", \"Ĭ\")\nX_CASE_MAP(\"į\", \"Į\")\nX_CASE_MAP(\"ı\", \"I\")\nX_CASE_MAP(\"ĵ\", \"Ĵ\")\nX_CASE_MAP(\"ķ\", \"Ķ\")\nX_CASE_MAP(\"ĸ\", \"ĸ\")\nX_CASE_MAP(\"ĺ\", \"Ĺ\")\nX_CASE_MAP(\"ļ\", \"Ļ\")\nX_CASE_MAP(\"ľ\", \"Ľ\")\nX_CASE_MAP(\"ŀ\", \"Ŀ\")\nX_CASE_MAP(\"ł\", \"Ł\")\nX_CASE_MAP(\"ń\", \"Ń\")\nX_CASE_MAP(\"ņ\", \"Ņ\")\nX_CASE_MAP(\"ň\", \"Ň\")\nX_CASE_MAP(\"ŋ\", \"Ŋ\")\nX_CASE_MAP(\"ō\", \"Ō\")\nX_CASE_MAP(\"ŏ\", \"Ŏ\")\nX_CASE_MAP(\"ő\", \"Ő\")\nX_CASE_MAP(\"œ\", \"Œ\")\nX_CASE_MAP(\"ŕ\", \"Ŕ\")\nX_CASE_MAP(\"ŗ\", \"Ŗ\")\nX_CASE_MAP(\"ř\", \"Ř\")\nX_CASE_MAP(\"ś\", \"Ś\")\nX_CASE_MAP(\"ŝ\", \"Ŝ\")\nX_CASE_MAP(\"ş\", \"Ş\")\nX_CASE_MAP(\"š\", \"Š\")\nX_CASE_MAP(\"ţ\", \"Ţ\")\nX_CASE_MAP(\"ť\", \"Ť\")\nX_CASE_MAP(\"ŧ\", \"Ŧ\")\nX_CASE_MAP(\"ũ\", \"Ũ\")\nX_CASE_MAP(\"ū\", \"Ū\")\nX_CASE_MAP(\"ŭ\", \"Ŭ\")\nX_CASE_MAP(\"ů\", \"Ů\")\nX_CASE_MAP(\"ű\", \"Ű\")\nX_CASE_MAP(\"ų\", \"Ų\")\nX_CASE_MAP(\"ŵ\", \"Ŵ\")\nX_CASE_MAP(\"ŷ\", \"Ŷ\")\nX_CASE_MAP(\"ź\", \"Ź\")\nX_CASE_MAP(\"ż\", \"Ż\")\nX_CASE_MAP(\"ž\", \"Ž\")\nX_CASE_MAP(\"ǎ\", \"Ǎ\")\nX_CASE_MAP(\"ǐ\", \"Ǐ\")\nX_CASE_MAP(\"ǒ\", \"Ǒ\")\nX_CASE_MAP(\"ǔ\", \"Ǔ\")\nX_CASE_MAP(\"ǧ\", \"Ǧ\")\nX_CASE_MAP(\"ǩ\", \"Ǩ\")\nX_CASE_MAP(\"ǵ\", \"Ǵ\")\nX_CASE_MAP(\"ǹ\", \"Ǹ\")\nX_CASE_MAP(\"ș\", \"Ș\")\nX_CASE_MAP(\"ț\", \"Ț\")\nX_CASE_MAP(\"ȟ\", \"Ȟ\")\nX_CASE_MAP(\"ȧ\", \"Ȧ\")\nX_CASE_MAP(\"ȯ\", \"Ȯ\")\nX_CASE_MAP(\"ȳ\", \"Ȳ\")\nX_CASE_MAP(\"ά\", \"Ά\")\nX_CASE_MAP(\"έ\", \"Έ\")\nX_CASE_MAP(\"ή\", \"Ή\")\nX_CASE_MAP(\"ί\", \"Ί\")\nX_CASE_MAP(\"α\", \"Α\")\nX_CASE_MAP(\"β\", \"Β\")\nX_CASE_MAP(\"γ\", \"Γ\")\nX_CASE_MAP(\"δ\", \"Δ\")\nX_CASE_MAP(\"ε\", \"Ε\")\nX_CASE_MAP(\"ζ\", \"Ζ\")\nX_CASE_MAP(\"η\", \"Η\")\nX_CASE_MAP(\"θ\", \"Θ\")\nX_CASE_MAP(\"ι\", \"Ι\")\nX_CASE_MAP(\"κ\", \"Κ\")\nX_CASE_MAP(\"λ\", \"Λ\")\nX_CASE_MAP(\"μ\", \"Μ\")\nX_CASE_MAP(\"ν\", \"Ν\")\nX_CASE_MAP(\"ξ\", \"Ξ\")\nX_CASE_MAP(\"ο\", \"Ο\")\nX_CASE_MAP(\"π\", \"Π\")\nX_CASE_MAP(\"ρ\", \"Ρ\")\nX_CASE_MAP(\"ς\", \"Σ\")\nX_CASE_MAP(\"σ\", \"Σ\")\nX_CASE_MAP(\"τ\", \"Τ\")\nX_CASE_MAP(\"υ\", \"Υ\")\nX_CASE_MAP(\"φ\", \"Φ\")\nX_CASE_MAP(\"χ\", \"Χ\")\nX_CASE_MAP(\"ψ\", \"Ψ\")\nX_CASE_MAP(\"ω\", \"Ω\")\nX_CASE_MAP(\"ϊ\", \"Ϊ\")\nX_CASE_MAP(\"ϋ\", \"Ϋ\")\nX_CASE_MAP(\"ό\", \"Ό\")\nX_CASE_MAP(\"ύ\", \"Ύ\")\nX_CASE_MAP(\"ώ\", \"Ώ\")\nX_CASE_MAP(\"а\", \"А\")\nX_CASE_MAP(\"б\", \"Б\")\nX_CASE_MAP(\"в\", \"В\")\nX_CASE_MAP(\"г\", \"Г\")\nX_CASE_MAP(\"д\", \"Д\")\nX_CASE_MAP(\"е\", \"Е\")\nX_CASE_MAP(\"ж\", \"Ж\")\nX_CASE_MAP(\"з\", \"З\")\nX_CASE_MAP(\"и\", \"И\")\nX_CASE_MAP(\"й\", \"Й\")\nX_CASE_MAP(\"к\", \"К\")\nX_CASE_MAP(\"л\", \"Л\")\nX_CASE_MAP(\"м\", \"М\")\nX_CASE_MAP(\"н\", \"Н\")\nX_CASE_MAP(\"о\", \"О\")\nX_CASE_MAP(\"п\", \"П\")\nX_CASE_MAP(\"р\", \"Р\")\nX_CASE_MAP(\"с\", \"С\")\nX_CASE_MAP(\"т\", \"Т\")\nX_CASE_MAP(\"у\", \"У\")\nX_CASE_MAP(\"ф\", \"Ф\")\nX_CASE_MAP(\"х\", \"Х\")\nX_CASE_MAP(\"ц\", \"Ц\")\nX_CASE_MAP(\"ч\", \"Ч\")\nX_CASE_MAP(\"ш\", \"Ш\")\nX_CASE_MAP(\"щ\", \"Щ\")\nX_CASE_MAP(\"ъ\", \"Ъ\")\nX_CASE_MAP(\"ы\", \"Ы\")\nX_CASE_MAP(\"ь\", \"Ь\")\nX_CASE_MAP(\"э\", \"Э\")\nX_CASE_MAP(\"ю\", \"Ю\")\nX_CASE_MAP(\"я\", \"Я\")\nX_CASE_MAP(\"ѐ\", \"Ѐ\")\nX_CASE_MAP(\"ё\", \"Ё\")\nX_CASE_MAP(\"ђ\", \"Ђ\")\nX_CASE_MAP(\"ѓ\", \"Ѓ\")\nX_CASE_MAP(\"є\", \"Є\")\nX_CASE_MAP(\"ѕ\", \"Ѕ\")\nX_CASE_MAP(\"і\", \"І\")\nX_CASE_MAP(\"ї\", \"Ї\")\nX_CASE_MAP(\"ј\", \"Ј\")\nX_CASE_MAP(\"љ\", \"Љ\")\nX_CASE_MAP(\"њ\", \"Њ\")\nX_CASE_MAP(\"ћ\", \"Ћ\")\nX_CASE_MAP(\"ќ\", \"Ќ\")\nX_CASE_MAP(\"ѝ\", \"Ѝ\")\nX_CASE_MAP(\"ў\", \"Ў\")\nX_CASE_MAP(\"џ\", \"Џ\")\nX_CASE_MAP(\"ґ\", \"Ґ\")\nX_CASE_MAP(\"ḃ\", \"Ḃ\")\nX_CASE_MAP(\"ḋ\", \"Ḋ\")\nX_CASE_MAP(\"ḟ\", \"Ḟ\")\nX_CASE_MAP(\"ḡ\", \"Ḡ\")\nX_CASE_MAP(\"ḣ\", \"Ḣ\")\nX_CASE_MAP(\"ḧ\", \"Ḧ\")\nX_CASE_MAP(\"ḱ\", \"Ḱ\")\nX_CASE_MAP(\"ḿ\", \"Ḿ\")\nX_CASE_MAP(\"ṁ\", \"Ṁ\")\nX_CASE_MAP(\"ṅ\", \"Ṅ\")\nX_CASE_MAP(\"ṕ\", \"Ṕ\")\nX_CASE_MAP(\"ṗ\", \"Ṗ\")\nX_CASE_MAP(\"ṙ\", \"Ṙ\")\nX_CASE_MAP(\"ṡ\", \"Ṡ\")\nX_CASE_MAP(\"ṫ\", \"Ṫ\")\nX_CASE_MAP(\"ṽ\", \"Ṽ\")\nX_CASE_MAP(\"ẁ\", \"Ẁ\")\nX_CASE_MAP(\"ẃ\", \"Ẃ\")\nX_CASE_MAP(\"ẅ\", \"Ẅ\")\nX_CASE_MAP(\"ẇ\", \"Ẇ\")\nX_CASE_MAP(\"ẋ\", \"Ẋ\")\nX_CASE_MAP(\"ẍ\", \"Ẍ\")\nX_CASE_MAP(\"ẏ\", \"Ẏ\")\nX_CASE_MAP(\"ẑ\", \"Ẑ\")\nX_CASE_MAP(\"ẽ\", \"Ẽ\")\nX_CASE_MAP(\"ỳ\", \"Ỳ\")\nX_CASE_MAP(\"ỹ\", \"Ỹ\")\n"
  },
  {
    "path": "src/trx/core/strings/common.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n\n#include <ctype.h>\n#include <pcre2.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n// Number of static buffers in a rotating ring for String_FormatStatic, etc.\n#define M_MAX_STATIC_BUFFERS 8\n\ntypedef struct {\n    char *buf;\n    size_t capacity;\n} M_STATIC_BUFFER;\n\nstatic M_STATIC_BUFFER m_StaticBufferRing[M_MAX_STATIC_BUFFERS] = {};\nstatic int m_StaticBufNext = 0;\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    for (int32_t i = 0; i < M_MAX_STATIC_BUFFERS; i++) {\n        Memory_FreePointer(&m_StaticBufferRing[i].buf);\n        m_StaticBufferRing[i].capacity = 0;\n    }\n}\n\nstatic M_STATIC_BUFFER *M_CycleStaticBuffer(void)\n{\n    const int32_t idx = m_StaticBufNext;\n    m_StaticBufNext = (m_StaticBufNext + 1) % M_MAX_STATIC_BUFFERS;\n    return &m_StaticBufferRing[idx];\n}\n\nstatic void M_AddPage(\n    const char *text, int32_t start_pos, int32_t length, VECTOR *const pages)\n{\n    char substr[length + 1];\n    while (text[start_pos] == '\\n' && text[start_pos] != '\\0') {\n        start_pos++;\n        length--;\n    }\n    strncpy(substr, text + start_pos, length);\n    substr[length] = '\\0';\n\n    const char *page = Memory_DupStr(substr);\n    Vector_Add(pages, &page);\n}\n\nbool String_EndsWith(const char *str, const char *suffix)\n{\n    int str_len = strlen(str);\n    int suffix_len = strlen(suffix);\n\n    if (suffix_len > str_len) {\n        return 0;\n    }\n\n    return strcmp(str + str_len - suffix_len, suffix) == 0;\n}\n\nbool String_Equivalent(const char *a, const char *b)\n{\n    if (a == nullptr || b == nullptr) {\n        return false;\n    }\n\n    size_t a_size = strlen(a);\n    size_t b_size = strlen(b);\n    if (a_size != b_size) {\n        return false;\n    }\n\n    for (size_t i = 0; i < a_size; i++) {\n        if (tolower(a[i]) != tolower(b[i])) {\n            return false;\n        }\n    }\n    return true;\n}\n\nconst char *String_CaseSubstring(const char *subject, const char *pattern)\n{\n    if (subject == nullptr || pattern == nullptr) {\n        return nullptr;\n    }\n\n    size_t str_size = strlen(subject);\n    size_t substr_size = strlen(pattern);\n    if (substr_size > str_size) {\n        return nullptr;\n    }\n    if (substr_size == 0) {\n        return subject;\n    }\n\n    for (size_t i = 0; i < str_size + 1 - substr_size; i++) {\n        bool equivalent = true;\n        for (size_t j = 0; j < substr_size; j++) {\n            if (tolower(subject[i + j]) != tolower(pattern[j])) {\n                equivalent = false;\n                break;\n            }\n        }\n        if (equivalent) {\n            return subject + i;\n        }\n    }\n    return nullptr;\n}\n\nbool String_Match(const char *const subject, const char *const pattern)\n{\n    if (subject == nullptr || pattern == nullptr) {\n        return 0;\n    }\n\n    const unsigned char *const usubject = (const unsigned char *)subject;\n    const unsigned char *const upattern = (const unsigned char *)pattern;\n    const uint32_t options = PCRE2_CASELESS;\n\n    const uint32_t ovec_size = 128;\n\n    int err_code;\n    PCRE2_SIZE err_offset;\n    pcre2_code *const re = pcre2_compile(\n        upattern, PCRE2_ZERO_TERMINATED, options, &err_code, &err_offset,\n        nullptr);\n    if (re == nullptr) {\n        PCRE2_UCHAR8 buffer[128];\n        pcre2_get_error_message(err_code, buffer, 120);\n        LOG_ERROR(\"%d\\t%s\", err_code, buffer);\n        return false;\n    }\n\n    pcre2_match_data *const match_data =\n        pcre2_match_data_create(ovec_size, nullptr);\n    const int flags = 0;\n    const int rc = pcre2_match(\n        re, usubject, PCRE2_ZERO_TERMINATED, 0, flags, match_data, nullptr);\n    pcre2_match_data_free(match_data);\n    pcre2_code_free(re);\n\n    return rc > 0;\n}\n\nbool String_IsEmpty(const char *const value)\n{\n    return String_Match(value, \"^\\\\s*$\");\n}\n\nbool String_ParseBool(const char *const value, bool *const target)\n{\n    if (String_Match(value, \"^(0|false|off)$\")) {\n        if (target != nullptr) {\n            *target = false;\n        }\n        return true;\n    }\n\n    if (String_Match(value, \"^(1|true|on)$\")) {\n        if (target != nullptr) {\n            *target = true;\n        }\n        return true;\n    }\n\n    return false;\n}\n\nbool String_ParseInteger(const char *const value, int32_t *const target)\n{\n    return sscanf(value, \"%d\", target) == 1;\n}\n\nbool String_ParseDecimal(const char *const value, float *const target)\n{\n    bool has_dot = false;\n    for (size_t i = 0; i < strlen(value); i++) {\n        if (i == 0 && value[i] == '-') {\n            continue;\n        }\n        if (!isdigit(value[i])) {\n            if (value[i] == '.') {\n                if (has_dot) {\n                    return false;\n                }\n                has_dot = true;\n            } else {\n                return false;\n            }\n        }\n    }\n    if (target != nullptr) {\n        *target = atof(value);\n    }\n    return true;\n}\n\nbool String_ParseRGB888(const char *value, RGB_888 *const target)\n{\n    if (value[0] == '#') {\n        value++;\n    }\n    return sscanf(\n               value, \"%02hhX%02hhX%02hhX\", &target->r, &target->g, &target->b)\n        == 3;\n}\n\nbool String_ParseRGBA8888(const char *value, RGBA_8888 *const target)\n{\n    if (value[0] == '#') {\n        value++;\n    }\n    return sscanf(\n               value, \"%02hhX%02hhX%02hhX%02hhX\", &target->r, &target->g,\n               &target->b, &target->a)\n        == 4;\n}\n\nVECTOR *String_Paginate(const char *const text, const int32_t max_lines)\n{\n    VECTOR *const pages = Vector_Create(sizeof(char *));\n    int32_t line_count = 0;\n    int32_t start_pos = 0;\n    int32_t current_length = 0;\n\n    const char *iter_text = text;\n\n    while (*iter_text != '\\0') {\n        current_length++;\n        if (*iter_text == '\\n') {\n            line_count++;\n        }\n\n        if (line_count == max_lines || *iter_text == '\\f') {\n            M_AddPage(text, start_pos, current_length, pages);\n\n            start_pos += current_length;\n            current_length = 0;\n            line_count = 0;\n        }\n\n        *iter_text++;\n    }\n\n    // Anything that is left becomes its own page.\n    if (pages->count == 0 || current_length != 0) {\n        M_AddPage(text, start_pos, current_length, pages);\n    }\n\n    return pages;\n}\n\nchar *String_Format(const char *const fmt, ...)\n{\n    va_list args;\n    va_start(args, fmt);\n\n    int len = vsnprintf(nullptr, 0, fmt, args);\n    if (len < 0) {\n        va_end(args);\n        return nullptr;\n    }\n\n    char *const result = Memory_Alloc(len + 1);\n    va_end(args);\n    va_start(args, fmt);\n    vsnprintf(result, len + 1, fmt, args);\n    va_end(args);\n    return result;\n}\n\nvoid String_FormatInto(\n    char **target_buf, size_t *target_cap, const char *const fmt, ...)\n{\n    va_list args;\n    va_start(args, fmt);\n    String_FormatIntoV(target_buf, target_cap, fmt, args);\n    va_end(args);\n}\n\nvoid String_FormatIntoV(\n    char **target_buf, size_t *target_cap, const char *const fmt, va_list args)\n{\n    va_list args_copy;\n    va_copy(args_copy, args);\n    const int32_t len = vsnprintf(nullptr, 0, fmt, args_copy);\n    va_end(args_copy);\n    if (len < 0) {\n        return;\n    }\n    const size_t needed = (size_t)len + 1;\n    if (*target_cap < needed) {\n        *target_buf = Memory_Realloc(*target_buf, needed);\n        *target_cap = needed;\n    }\n    vsnprintf(*target_buf, *target_cap, fmt, args);\n}\n\nconst char *String_FormatStatic(const char *const fmt, ...)\n{\n    va_list args;\n    va_start(args, fmt);\n    const char *const result = String_FormatStaticV(fmt, args);\n    va_end(args);\n    return result;\n}\n\nconst char *String_FormatStaticV(const char *const fmt, va_list args)\n{\n    M_STATIC_BUFFER *const buffer = M_CycleStaticBuffer();\n    String_FormatIntoV(&buffer->buf, &buffer->capacity, fmt, args);\n    return buffer->buf;\n}\n"
  },
  {
    "path": "src/trx/core/strings/common.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/vector.h>\n\n#include <stdarg.h>\n#include <stdint.h>\n\nbool String_EndsWith(const char *str, const char *suffix);\nbool String_Equivalent(const char *a, const char *b);\n\nconst char *String_CaseSubstring(const char *subject, const char *pattern);\nbool String_Match(const char *subject, const char *pattern);\n\nbool String_IsEmpty(const char *value);\nbool String_ParseBool(const char *value, bool *target);\nbool String_ParseInteger(const char *value, int32_t *target);\nbool String_ParseDecimal(const char *value, float *target);\nbool String_ParseRGB888(const char *value, RGB_888 *target);\nbool String_ParseRGBA8888(const char *value, RGBA_8888 *target);\n\nsize_t String_GetCharByteSize(const char *ptr);\nchar *String_ToUpper(const char *text);\nchar *String_ToUpperPattern(const char *text);\n\nVECTOR *String_Paginate(const char *text, int32_t max_lines);\n\n// ============================================================================\n\nchar *String_Format(const char *fmt, ...);\n\n// Like String_Format, but prints into a specified string buffer.\n// If the buffer is too small, reallocates it to fit the string.\nvoid String_FormatInto(\n    char **target_buf, size_t *target_cap, const char *fmt, ...);\n\n// Like String_FormatInto, but accepts a va_list of arguments.\nvoid String_FormatIntoV(\n    char **target_buf, size_t *target_cap, const char *fmt, va_list args);\n\n// Like String_Format, but writes into a static buffer that grows as needed.\n// The caller must not free() the result; it will be freed on program exit.\nconst char *String_FormatStatic(const char *fmt, ...);\n\n// Like String_FormatStatic, but accepts a va_list of arguments.\nconst char *String_FormatStaticV(const char *fmt, va_list args);\n"
  },
  {
    "path": "src/trx/core/strings/fuzzy_match.c",
    "content": "#include <trx/core/strings/fuzzy_match.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings/common.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define FULL_MATCH_SCORE_BONUS 100\n#define WORD_MATCH_SCORE_BONUS 50\n#define PERCENT_MATCH_SCORE 50\n#define LETTER_MATCH_SCORE_BONUS 1\n\nstatic STRING_FUZZY_SCORE M_GetScore(\n    const char *const user_input, const char *const reference,\n    const int32_t weight)\n{\n    const int32_t percent_score =\n        PERCENT_MATCH_SCORE * strlen(user_input) / strlen(reference);\n    const int32_t letter_score = LETTER_MATCH_SCORE_BONUS * strlen(user_input);\n\n    char *word_regex = Memory_Alloc(strlen(user_input) + 20);\n    char *full_regex = Memory_Alloc(strlen(user_input) + 20);\n    sprintf(word_regex, \"\\\\b%s\\\\b\", user_input);\n    sprintf(full_regex, \"^\\\\s*%s\\\\s*$\", user_input);\n\n    // Assume a partial match\n    bool is_full = false;\n    bool is_word = false;\n    int32_t score = letter_score + percent_score;\n    if (String_Match(reference, full_regex)) {\n        // Got a full match\n        is_full = true;\n        score += FULL_MATCH_SCORE_BONUS;\n    } else if (String_Match(reference, word_regex)) {\n        // Got a word match\n        is_word = true;\n        score += WORD_MATCH_SCORE_BONUS;\n    } else if (String_CaseSubstring(reference, user_input) == nullptr) {\n        // No match.\n        score = 0;\n    }\n\n    Memory_FreePointer(&word_regex);\n    Memory_FreePointer(&full_regex);\n\n    return (STRING_FUZZY_SCORE) {\n        .is_full = is_full,\n        .is_word = is_word,\n        .score = score * weight,\n    };\n}\n\nstatic void M_DiscardNonFullMatches(VECTOR *const matches)\n{\n    bool has_full_match = false;\n    for (int32_t i = 0; i < matches->count; i++) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n        if (match->score.is_full) {\n            has_full_match = true;\n        }\n    }\n    if (has_full_match) {\n        for (int32_t i = matches->count - 1; i >= 0; i--) {\n            const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n            if (!match->score.is_full) {\n                Vector_RemoveAt(matches, i);\n            }\n        }\n    }\n}\n\nstatic void M_DiscardNonWordMatches(VECTOR *const matches)\n{\n    bool has_word_match = false;\n    for (int32_t i = 0; i < matches->count; i++) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n        if (match->score.is_word) {\n            has_word_match = true;\n        }\n    }\n    if (has_word_match) {\n        for (int32_t i = matches->count - 1; i >= 0; i--) {\n            const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n            if (!match->score.is_word) {\n                Vector_RemoveAt(matches, i);\n            }\n        }\n    }\n}\n\nstatic void M_SortMatches(VECTOR *const matches)\n{\n    // sort by match length so that best-matching results appear first\n    for (int32_t i = 0; i < matches->count; i++) {\n        const STRING_FUZZY_MATCH *const match_1 = Vector_Get(matches, i);\n        for (int32_t j = i + 1; j < matches->count; j++) {\n            const STRING_FUZZY_MATCH *const match_2 = Vector_Get(matches, j);\n            if (match_1->score.score < match_2->score.score) {\n                Vector_Swap(matches, i, j);\n            }\n        }\n    }\n}\n\nstatic void M_DiscardDuplicateMatches(VECTOR *const matches)\n{\n    for (int32_t i = matches->count - 1; i >= 0; i--) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n        bool is_unique = true;\n        for (int32_t j = 0; j < matches->count; j++) {\n            const STRING_FUZZY_MATCH *const other_match =\n                Vector_Get(matches, j);\n            if (j != i && match->value == other_match->value) {\n                is_unique = false;\n                break;\n            }\n        }\n        if (!is_unique) {\n            Vector_RemoveAt(matches, i);\n        }\n    }\n}\n\nVECTOR *String_FuzzyMatch(const char *user_input, const VECTOR *const source)\n{\n    VECTOR *matches = Vector_Create(sizeof(STRING_FUZZY_MATCH));\n\n    for (int32_t i = 0; i < source->count; i++) {\n        const STRING_FUZZY_SOURCE *const source_item =\n            Vector_Get((VECTOR *)source, i);\n        const STRING_FUZZY_SCORE score =\n            M_GetScore(user_input, source_item->key, source_item->weight);\n\n        if (score.score <= 0) {\n            continue;\n        }\n\n        STRING_FUZZY_MATCH match = {\n            .key = source_item->key,\n            .value = source_item->value,\n            .score = score,\n        };\n        Vector_Add(matches, &match);\n    }\n\n    M_DiscardNonFullMatches(matches);\n    M_DiscardNonWordMatches(matches);\n    M_DiscardDuplicateMatches(matches);\n    M_SortMatches(matches);\n\n    return matches;\n}\n"
  },
  {
    "path": "src/trx/core/strings/fuzzy_match.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    const char *key;\n    void *value;\n    int32_t weight;\n} STRING_FUZZY_SOURCE;\n\ntypedef struct {\n    bool is_full;\n    bool is_word;\n    int32_t score;\n} STRING_FUZZY_SCORE;\n\ntypedef struct {\n    const char *key;\n    void *value;\n    STRING_FUZZY_SCORE score;\n} STRING_FUZZY_MATCH;\n\n// Takes a vector of STRING_FUZZY_SOURCE.\n// Returns a vector of STRING_FUZZY_MATCH.\nVECTOR *String_FuzzyMatch(const char *user_input, const VECTOR *source);\n"
  },
  {
    "path": "src/trx/core/strings.h",
    "content": "#pragma once\n\n#include <trx/core/strings/common.h>\n#include <trx/core/strings/fuzzy_match.h>\n"
  },
  {
    "path": "src/trx/core/thread_pool.c",
    "content": "#include <trx/core/thread_pool.h>\n\n#include <trx/core/memory.h>\n\n#include <SDL2/SDL_cpuinfo.h>\n#include <SDL2/SDL_thread.h>\n\ntypedef struct JOB {\n    THREAD_FUNC func;\n    void *user_data;\n    struct JOB *next;\n} JOB;\n\nstruct THREAD_POOL {\n    SDL_Thread **threads;\n    size_t num_threads;\n    SDL_mutex *queue_mutex;\n    SDL_cond *queue_cond;\n    JOB *queue_head;\n    JOB *queue_tail;\n    bool stop;\n    size_t working_count;\n    SDL_cond *working_cond;\n};\n\nstatic int32_t M_WorkerThread(void *const arg)\n{\n    THREAD_POOL *const pool = arg;\n\n    while (true) {\n        SDL_LockMutex(pool->queue_mutex);\n        while (!pool->stop && pool->queue_head == nullptr) {\n            SDL_CondWait(pool->queue_cond, pool->queue_mutex);\n        }\n        if (pool->stop && pool->queue_head == nullptr) {\n            SDL_UnlockMutex(pool->queue_mutex);\n            break;\n        }\n        JOB *const job = pool->queue_head;\n        pool->queue_head = job->next;\n        if (pool->queue_head == nullptr) {\n            pool->queue_tail = nullptr;\n        }\n        pool->working_count++;\n        SDL_UnlockMutex(pool->queue_mutex);\n\n        job->func(job->user_data);\n        Memory_Free(job);\n\n        SDL_LockMutex(pool->queue_mutex);\n        pool->working_count--;\n        if (pool->working_count == 0 && pool->queue_head == nullptr) {\n            SDL_CondSignal(pool->working_cond);\n        }\n        SDL_UnlockMutex(pool->queue_mutex);\n    }\n    return 0;\n}\n\nTHREAD_POOL *ThreadPool_Create(int32_t num_threads)\n{\n    if (num_threads <= 0) {\n        num_threads = SDL_GetCPUCount();\n        if (num_threads <= 0) {\n            num_threads = 1;\n        }\n    }\n\n    THREAD_POOL *const pool = Memory_Alloc(sizeof(*pool));\n    pool->threads = Memory_Alloc(sizeof(SDL_Thread *) * num_threads);\n    pool->num_threads = num_threads;\n    pool->queue_mutex = SDL_CreateMutex();\n    pool->queue_cond = SDL_CreateCond();\n    pool->working_cond = SDL_CreateCond();\n    pool->queue_head = pool->queue_tail = nullptr;\n    pool->stop = false;\n    pool->working_count = 0;\n\n    for (int32_t i = 0; i < num_threads; i++) {\n        pool->threads[i] = SDL_CreateThread(M_WorkerThread, \"worker\", pool);\n    }\n    return pool;\n}\n\nvoid ThreadPool_Destroy(THREAD_POOL *const pool)\n{\n    if (pool == nullptr) {\n        return;\n    }\n    SDL_LockMutex(pool->queue_mutex);\n    pool->stop = true;\n    SDL_CondBroadcast(pool->queue_cond);\n    SDL_UnlockMutex(pool->queue_mutex);\n\n    for (size_t i = 0; i < pool->num_threads; i++) {\n        SDL_WaitThread(pool->threads[i], nullptr);\n    }\n    SDL_DestroyCond(pool->working_cond);\n    SDL_DestroyCond(pool->queue_cond);\n    SDL_DestroyMutex(pool->queue_mutex);\n    Memory_Free(pool->threads);\n    Memory_Free(pool);\n}\n\nbool ThreadPool_AddJob(\n    THREAD_POOL *const pool, THREAD_FUNC func, void *const user_data)\n{\n    JOB *const job = Memory_Alloc(sizeof(JOB));\n    job->func = func;\n    job->user_data = user_data;\n    job->next = nullptr;\n\n    SDL_LockMutex(pool->queue_mutex);\n    if (pool->queue_tail) {\n        pool->queue_tail->next = job;\n        pool->queue_tail = job;\n    } else {\n        pool->queue_head = pool->queue_tail = job;\n    }\n    SDL_CondSignal(pool->queue_cond);\n    SDL_UnlockMutex(pool->queue_mutex);\n    return true;\n}\n\nvoid ThreadPool_Wait(THREAD_POOL *pool)\n{\n    SDL_LockMutex(pool->queue_mutex);\n    while (pool->queue_head != nullptr || pool->working_count != 0) {\n        SDL_CondWait(pool->working_cond, pool->queue_mutex);\n    }\n    SDL_UnlockMutex(pool->queue_mutex);\n}\n"
  },
  {
    "path": "src/trx/core/thread_pool.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct THREAD_POOL THREAD_POOL;\ntypedef void (*THREAD_FUNC)(void *userdata);\n\n// Create a thread pool with the given number of worker threads.\n// Returns nullptr on failure.\nTHREAD_POOL *ThreadPool_Create(int32_t num_threads);\n\n// Destroy a thread pool, freeing all resources.\n// Blocks until all worker threads have exited.\nvoid ThreadPool_Destroy(THREAD_POOL *pool);\n\n// Submit a job to the thread pool. Returns true on success.\nbool ThreadPool_AddJob(THREAD_POOL *pool, THREAD_FUNC func, void *user_data);\n\n// Wait for all submitted jobs to complete.\nvoid ThreadPool_Wait(THREAD_POOL *pool);\n"
  },
  {
    "path": "src/trx/core/utils.h",
    "content": "#pragma once\n\n#define Q(x) #x\n#define QUOTE(x) Q(x)\n#define CONCAT_(a, b) a##b\n#define CONCAT(a, b) CONCAT_(a, b)\n\n#define ARRAY_SIZE(x) (sizeof((x)) / sizeof((x)[0]))\n#define SQUARE(A) ((A) * (A))\n#ifndef ABS\n    #define ABS(x) (((x) < 0) ? (-(x)) : (x))\n    #define MIN(x, y) ((x) <= (y) ? (x) : (y))\n    #define MAX(x, y) ((x) >= (y) ? (x) : (y))\n#endif\n\n#define MIN3(x, y, z) MIN(MIN((x), (y)), (z))\n#define MAX3(x, y, z) MAX(MAX((x), (y)), (z))\n\n#define CLAMPL(a, b)                                                           \\\n    do {                                                                       \\\n        if ((a) < (b))                                                         \\\n            (a) = (b);                                                         \\\n    } while (0)\n#define CLAMPG(a, b)                                                           \\\n    do {                                                                       \\\n        if ((a) > (b))                                                         \\\n            (a) = (b);                                                         \\\n    } while (0)\n#define CLAMP(a, b, c)                                                         \\\n    do {                                                                       \\\n        if ((a) < (b))                                                         \\\n            (a) = (b);                                                         \\\n        else if ((a) > (c))                                                    \\\n            (a) = (c);                                                         \\\n    } while (0)\n\n#define MINMAX(target, low, high) MAX(MIN((target), (low)), (target))\n\n#define SWAP(a, b)                                                             \\\n    do {                                                                       \\\n        typeof(a) SWAP_tmp_ = (a);                                             \\\n        (a) = (b);                                                             \\\n        (b) = SWAP_tmp_;                                                       \\\n    } while (0)\n#define SWAP2(a, b, tmp)                                                       \\\n    do {                                                                       \\\n        (tmp) = (a);                                                           \\\n        (a) = (b);                                                             \\\n        (b) = (tmp);                                                           \\\n    } while (0)\n#define TOGGLE(target)                                                         \\\n    do {                                                                       \\\n        (target) = !(target);                                                  \\\n    } while (0);\n#define CYCLE(target, rate, number_of)                                         \\\n    do {                                                                       \\\n        (target) += (rate);                                                    \\\n        (target) += (number_of);                                               \\\n        (target) %= (number_of);                                               \\\n    } while (0);\n\n#define ALIGN(a, bytes) ((a + (bytes) - 1) & (~(bytes - 1)))\n#define TOGGLE_BIT(target_var, target_bit, condition)                          \\\n    do {                                                                       \\\n        if (condition) {                                                       \\\n            (target_var) |= (target_bit);                                      \\\n        } else {                                                               \\\n            (target_var) &= ~(target_bit);                                     \\\n        }                                                                      \\\n    } while (0)\n\n#define MKTAG(a, b, c, d)                                                      \\\n    ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))\n\n#define LERP(a, b, ratio) ((a) + ((b) - (a)) * (ratio))\n"
  },
  {
    "path": "src/trx/core/vector.c",
    "content": "#include <trx/core/vector.h>\n\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n\n#include <stdint.h>\n#include <string.h>\n\n#define VECTOR_DEFAULT_CAPACITY 4\n#define VECTOR_GROWTH_RATE 2\n#define P(obj) ((*obj->priv))\n\nstruct VECTOR_PRIV {\n    char *items;\n};\n\nstatic void M_EnsureCapacity(VECTOR *const vector, const int32_t n)\n{\n    while (vector->count + n > vector->capacity) {\n        vector->capacity *= VECTOR_GROWTH_RATE;\n        P(vector).items = Memory_Realloc(\n            P(vector).items, vector->item_size * vector->capacity);\n    }\n}\n\nVECTOR *Vector_Create(const size_t item_size)\n{\n    return Vector_CreateAtCapacity(item_size, VECTOR_DEFAULT_CAPACITY);\n}\n\nVECTOR *Vector_CreateAtCapacity(const size_t item_size, const int32_t capacity)\n{\n    VECTOR *const vector = Memory_Alloc(sizeof(VECTOR));\n    vector->count = 0;\n    vector->capacity = capacity;\n    vector->item_size = item_size;\n    vector->priv = Memory_Alloc(sizeof(struct VECTOR_PRIV));\n    P(vector).items = Memory_Alloc(item_size * capacity);\n\n    return vector;\n}\n\nvoid Vector_EnsureCapacity(VECTOR *const vector, const int32_t capacity)\n{\n    if (vector->capacity >= capacity) {\n        return;\n    }\n    vector->capacity = capacity;\n    P(vector).items =\n        Memory_Realloc(P(vector).items, vector->item_size * capacity);\n}\n\nvoid Vector_Free(VECTOR *vector)\n{\n    if (vector == nullptr) {\n        return;\n    }\n    Memory_FreePointer(&P(vector).items);\n    Memory_FreePointer(&vector->priv);\n    Memory_FreePointer(&vector);\n}\n\nint32_t Vector_IndexOf(const VECTOR *const vector, const void *const item)\n{\n    for (int32_t i = 0; i < vector->count; i++) {\n        if (memcmp(\n                P(vector).items + i * vector->item_size, item,\n                vector->item_size)\n            == 0) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nint32_t Vector_LastIndexOf(const VECTOR *const vector, const void *const item)\n{\n    const char *const items = P(vector).items;\n    for (int32_t i = vector->count - 1; i >= 0; i--) {\n        if (memcmp(items + i * vector->item_size, item, vector->item_size)\n            == 0) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nbool Vector_Contains(const VECTOR *const vector, const void *const item)\n{\n    return Vector_IndexOf(vector, item) != -1;\n}\n\nvoid *Vector_Get(const VECTOR *const vector, const int32_t index)\n{\n    ASSERT(index >= 0 && index < vector->count);\n    char *const items = P(vector).items;\n    return (void *)(items + index * vector->item_size);\n}\n\nvoid *Vector_GetData(const VECTOR *const vector)\n{\n    return P(vector).items;\n}\n\nvoid *Vector_Expand(VECTOR *const vector, const int32_t count)\n{\n    ASSERT(count >= 0);\n    M_EnsureCapacity(vector, count);\n    char *const items = P(vector).items;\n    void *const result = items + vector->count * vector->item_size;\n    vector->count += count;\n    return result;\n}\n\nvoid Vector_Add(VECTOR *const vector, const void *const item)\n{\n    memcpy(Vector_Expand(vector, 1), item, vector->item_size);\n}\n\nvoid Vector_Insert(\n    VECTOR *const vector, const int32_t index, const void *const item)\n{\n    ASSERT(index >= 0 && index <= vector->count);\n    M_EnsureCapacity(vector, 1);\n    char *const items = P(vector).items;\n    if (index < vector->count) {\n        memmove(\n            items + (index + 1) * vector->item_size,\n            items + index * vector->item_size,\n            (vector->count - index) * vector->item_size);\n    }\n    memcpy(items + index * vector->item_size, item, vector->item_size);\n    vector->count++;\n}\n\nvoid Vector_Swap(\n    VECTOR *const vector, const int32_t index1, const int32_t index2)\n{\n    ASSERT(index1 >= 0 && index1 < vector->count);\n    ASSERT(index2 >= 0 && index2 < vector->count);\n    if (index1 == index2) {\n        return;\n    }\n    char *const items = P(vector).items;\n    void *tmp = Memory_Alloc(vector->item_size);\n    memcpy(tmp, items + index1 * vector->item_size, vector->item_size);\n    memcpy(\n        items + index1 * vector->item_size, items + index2 * vector->item_size,\n        vector->item_size);\n    memcpy(items + index2 * vector->item_size, tmp, vector->item_size);\n    Memory_FreePointer(&tmp);\n}\n\nbool Vector_Remove(VECTOR *const vector, const void *item)\n{\n    const int32_t index = Vector_IndexOf(vector, item);\n    if (index == -1) {\n        return false;\n    }\n    Vector_RemoveAt(vector, index);\n    return true;\n}\n\nvoid Vector_RemoveAt(VECTOR *const vector, const int32_t index)\n{\n    ASSERT(index >= 0 && index < vector->count);\n    char *const items = P(vector).items;\n    memset(items + index * vector->item_size, 0, vector->item_size);\n    if (index + 1 < vector->count) {\n        memmove(\n            items + index * vector->item_size,\n            items + (index + 1) * vector->item_size,\n            (vector->count - (index + 1)) * vector->item_size);\n    }\n    vector->count--;\n}\n\nvoid Vector_Reverse(VECTOR *const vector)\n{\n    int32_t i = 0;\n    int32_t j = vector->count - 1;\n    void *tmp = Memory_Alloc(vector->item_size);\n    char *const items = P(vector).items;\n    for (; i < j; i++, j--) {\n        memcpy(tmp, items + i * vector->item_size, vector->item_size);\n        memcpy(\n            items + i * vector->item_size, items + j * vector->item_size,\n            vector->item_size);\n        memcpy(items + j * vector->item_size, tmp, vector->item_size);\n    }\n    Memory_FreePointer(&tmp);\n}\n\nvoid Vector_Clear(VECTOR *const vector)\n{\n    vector->count = 0;\n}\n\nvoid Vector_ClearRealloc(VECTOR *const vector)\n{\n    vector->count = 0;\n    vector->capacity = VECTOR_DEFAULT_CAPACITY;\n    P(vector).items =\n        Memory_Realloc(P(vector).items, vector->item_size * vector->capacity);\n    memset(P(vector).items, 0, vector->item_size * vector->count);\n}\n"
  },
  {
    "path": "src/trx/core/vector.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\nstruct VECTOR_PRIV;\n\ntypedef struct {\n    int32_t count;\n    int32_t capacity;\n    size_t item_size;\n\n    struct VECTOR_PRIV *priv;\n} VECTOR;\n\nVECTOR *Vector_Create(size_t item_size);\nVECTOR *Vector_CreateAtCapacity(size_t item_size, int32_t capacity);\nvoid Vector_EnsureCapacity(VECTOR *vector, int32_t capacity);\nvoid Vector_Free(VECTOR *vector);\n\nint32_t Vector_IndexOf(const VECTOR *vector, const void *item);\nint32_t Vector_LastIndexOf(const VECTOR *vector, const void *item);\nbool Vector_Contains(const VECTOR *vector, const void *item);\n\nvoid *Vector_Get(const VECTOR *vector, int32_t index);\nvoid *Vector_GetData(const VECTOR *vector);\nvoid *Vector_Expand(VECTOR *vector, int32_t count);\nvoid Vector_Add(VECTOR *vector, const void *item);\nvoid Vector_Insert(VECTOR *vector, int32_t index, const void *item);\nvoid Vector_Swap(VECTOR *vector, int32_t index1, int32_t index2);\n\nbool Vector_Remove(VECTOR *vector, const void *item);\nvoid Vector_RemoveAt(VECTOR *vector, int32_t index);\n\nvoid Vector_Reverse(VECTOR *vector);\nvoid Vector_Clear(VECTOR *vector);\nvoid Vector_ClearRealloc(VECTOR *vector);\n"
  },
  {
    "path": "src/trx/core/virtual_file.c",
    "content": "#include <trx/core/virtual_file.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n\n#include <string.h>\n\nVFILE *VFile_CreateFromPath(const char *const path)\n{\n    MYFILE *fp = File_Open(path, FILE_OPEN_READ);\n    if (!fp) {\n        LOG_ERROR(\"Can't open file %s\", path);\n        return nullptr;\n    }\n\n    const size_t data_size = File_Size(fp);\n    char *data = Memory_Alloc(data_size);\n    File_ReadData(fp, data, data_size);\n    if (File_Pos(fp) != data_size) {\n        LOG_ERROR(\"Can't read file %s\", path);\n        Memory_FreePointer(&data);\n        File_Close(fp);\n        return nullptr;\n    }\n    File_Close(fp);\n\n    VFILE *const file = Memory_Alloc(sizeof(VFILE));\n    file->content = data;\n    file->size = data_size;\n    file->cur_ptr = file->content;\n    return file;\n}\n\nVFILE *VFile_CreateFromBuffer(const char *data, size_t size)\n{\n    VFILE *const file = Memory_Alloc(sizeof(VFILE));\n    file->content = Memory_Dup(data, size);\n    file->size = size;\n    file->cur_ptr = file->content;\n    return file;\n}\n\nvoid VFile_Close(VFILE *file)\n{\n    ASSERT(file != nullptr);\n    Memory_FreePointer(&file->content);\n    Memory_FreePointer(&file);\n}\n\nsize_t VFile_GetPos(const VFILE *file)\n{\n    return file->cur_ptr - file->content;\n}\n\nvoid VFile_SetPos(VFILE *const file, const size_t pos)\n{\n    ASSERT(pos <= file->size);\n    file->cur_ptr = file->content + pos;\n}\n\nvoid VFile_Skip(VFILE *const file, const int32_t offset)\n{\n    ASSERT(VFile_TrySkip(file, offset));\n}\n\nbool VFile_TrySkip(VFILE *const file, const int32_t offset)\n{\n    const size_t cur_pos = VFile_GetPos(file);\n    if (cur_pos + offset > file->size) {\n        return false;\n    }\n    file->cur_ptr += offset;\n    return true;\n}\n\nvoid VFile_Read(VFILE *const file, void *const target, const size_t size)\n{\n    ASSERT(VFile_TryRead(file, target, size));\n}\n\nbool VFile_TryRead(VFILE *const file, void *const target, const size_t size)\n{\n    ASSERT(file != nullptr);\n    if (size == 0) {\n        return true;\n    }\n    ASSERT(target != nullptr);\n\n    const size_t cur_pos = VFile_GetPos(file);\n    if (cur_pos + size > file->size) {\n        return false;\n    }\n    memcpy(target, file->cur_ptr, size);\n    file->cur_ptr += size;\n    return true;\n}\n\nint8_t VFile_ReadS8(VFILE *file)\n{\n    int8_t result;\n    VFile_Read(file, &result, sizeof(result));\n    return result;\n}\n\nint16_t VFile_ReadS16(VFILE *file)\n{\n    int16_t result;\n    VFile_Read(file, &result, sizeof(result));\n    return result;\n}\n\nint32_t VFile_ReadS32(VFILE *file)\n{\n    int32_t result;\n    VFile_Read(file, &result, sizeof(result));\n    return result;\n}\n\nuint8_t VFile_ReadU8(VFILE *file)\n{\n    uint8_t result;\n    VFile_Read(file, &result, sizeof(result));\n    return result;\n}\n\nuint16_t VFile_ReadU16(VFILE *file)\n{\n    uint16_t result;\n    VFile_Read(file, &result, sizeof(result));\n    return result;\n}\n\nuint32_t VFile_ReadU32(VFILE *file)\n{\n    uint32_t result;\n    VFile_Read(file, &result, sizeof(result));\n    return result;\n}\n\n#define DEFINE_TRY_READ(name, type)                                            \\\n    bool name(VFILE *const file, type *const dst)                              \\\n    {                                                                          \\\n        return VFile_TryRead(file, dst, sizeof(type));                         \\\n    }\n\nDEFINE_TRY_READ(VFile_TryReadS8, int8_t)\nDEFINE_TRY_READ(VFile_TryReadS16, int16_t)\nDEFINE_TRY_READ(VFile_TryReadS32, int32_t)\nDEFINE_TRY_READ(VFile_TryReadU8, uint8_t)\nDEFINE_TRY_READ(VFile_TryReadU16, uint16_t)\nDEFINE_TRY_READ(VFile_TryReadU32, uint32_t)\n"
  },
  {
    "path": "src/trx/core/virtual_file.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef struct {\n    char *content;\n    size_t size;\n    char *cur_ptr;\n} VFILE;\n\nVFILE *VFile_CreateFromPath(const char *path);\nVFILE *VFile_CreateFromBuffer(const char *data, size_t size);\nvoid VFile_Close(VFILE *file);\n\nsize_t VFile_GetPos(const VFILE *file);\nvoid VFile_SetPos(VFILE *file, size_t pos);\nvoid VFile_Skip(VFILE *file, int32_t offset);\n\nvoid VFile_Read(VFILE *file, void *target, size_t size);\nint8_t VFile_ReadS8(VFILE *file);\nint16_t VFile_ReadS16(VFILE *file);\nint32_t VFile_ReadS32(VFILE *file);\nuint8_t VFile_ReadU8(VFILE *file);\nuint16_t VFile_ReadU16(VFILE *file);\nuint32_t VFile_ReadU32(VFILE *file);\n\nbool VFile_TrySkip(VFILE *file, int32_t offset);\nbool VFile_TryRead(VFILE *file, void *target, size_t size);\nbool VFile_TryReadS8(VFILE *file, int8_t *dst);\nbool VFile_TryReadS16(VFILE *file, int16_t *dst);\nbool VFile_TryReadS32(VFILE *file, int32_t *dst);\nbool VFile_TryReadU8(VFILE *file, uint8_t *dst);\nbool VFile_TryReadU16(VFILE *file, uint16_t *dst);\nbool VFile_TryReadU32(VFILE *file, uint32_t *dst);\n"
  },
  {
    "path": "src/trx/debug.h",
    "content": "#pragma once\n\n#include <trx/core/log.h>\n\n#define ASSERT(x)                                                              \\\n    do {                                                                       \\\n        if (!(x)) {                                                            \\\n            LOG_ERROR(\"Assertion failed: %s\", #x);                             \\\n            __builtin_trap();                                                  \\\n        }                                                                      \\\n    } while (0)\n\n#define ASSERT_FAIL()                                                          \\\n    do {                                                                       \\\n        LOG_ERROR(\"Assertion failed\");                                         \\\n        __builtin_trap();                                                      \\\n    } while (0)\n\n#define ASSERT_FAIL_FMT(fmt, ...)                                              \\\n    do {                                                                       \\\n        LOG_ERROR(\"Assertion failed: \" fmt __VA_OPT__(, ) __VA_ARGS__);        \\\n        __builtin_trap();                                                      \\\n    } while (0)\n\n#define SOFT_ASSERT(x, msg)                                                    \\\n    do {                                                                       \\\n        if (!(x)) {                                                            \\\n            LOG_ERROR(\"Warning: %s (%s)\", msg, #x);                            \\\n        }                                                                      \\\n    } while (0)\n"
  },
  {
    "path": "src/trx/game/anims/commands.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/game/anims.h>\n#include <trx/game/game_buf.h>\n#include <trx/version.h>\n\nstatic void M_ParseCommand(ANIM_COMMAND *const command, const int16_t **data)\n{\n    const int16_t *data_ptr = *data;\n    command->type = *data_ptr++;\n\n    switch (command->type) {\n    case AC_MOVE_ORIGIN: {\n        XYZ_16 *const pos = GameBuf_Alloc(sizeof(XYZ_16), GBUF_ANIM_COMMANDS);\n        pos->x = *data_ptr++;\n        pos->y = *data_ptr++;\n        pos->z = *data_ptr++;\n        command->data = (void *)pos;\n        break;\n    }\n\n    case AC_JUMP_VELOCITY: {\n        ANIM_COMMAND_VELOCITY_DATA *const cmd_data = GameBuf_Alloc(\n            sizeof(ANIM_COMMAND_VELOCITY_DATA), GBUF_ANIM_COMMANDS);\n        cmd_data->fall_speed = *data_ptr++;\n        cmd_data->speed = *data_ptr++;\n        command->data = (void *)cmd_data;\n        break;\n    }\n\n    case AC_SOUND_FX:\n    case AC_EFFECT: {\n        ANIM_COMMAND_EFFECT_DATA *const cmd_data =\n            GameBuf_Alloc(sizeof(ANIM_COMMAND_EFFECT_DATA), GBUF_ANIM_COMMANDS);\n        cmd_data->frame_num = *data_ptr++;\n        const int16_t effect_data = *data_ptr++;\n        cmd_data->effect_num = effect_data & 0x3FFF;\n        cmd_data->fx_type = 0;\n        if (command->type == AC_EFFECT && g_TRVersion == 3) {\n            cmd_data->fx_type = effect_data & 0xC000;\n            cmd_data->environment = ACE_ALL;\n        } else {\n            cmd_data->environment = (effect_data & 0xC000) >> 14;\n        }\n        command->data = (void *)cmd_data;\n        break;\n    }\n\n    default:\n        command->data = nullptr;\n        break;\n    }\n\n    *data = data_ptr;\n}\n\nvoid Anim_LoadCommands(const int16_t *data)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n\n    const int32_t anim_count = Anim_GetTotalCount();\n    for (int32_t i = 0; i < anim_count; i++) {\n        ANIM *const anim = Anim_GetAnim(i);\n        if (anim->num_commands == 0) {\n            continue;\n        }\n\n        anim->commands = GameBuf_Alloc(\n            sizeof(ANIM_COMMAND) * anim->num_commands, GBUF_ANIM_COMMANDS);\n        const int16_t *data_ptr = &data[anim->command_idx];\n        for (int32_t j = 0; j < anim->num_commands; j++) {\n            ANIM_COMMAND *const command = &anim->commands[j];\n            M_ParseCommand(command, &data_ptr);\n        }\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/anims/common.c",
    "content": "#include <trx/game/anims/common.h>\n\n#include <trx/debug.h>\n#include <trx/game/game_buf.h>\n\nstatic int32_t m_AnimCount = 0;\nstatic int32_t m_ChangeCount = 0;\nstatic int32_t m_RangeCount = 0;\nstatic int32_t m_BoneCount = 0;\nstatic ANIM *m_Anims = nullptr;\nstatic ANIM_CHANGE *m_Changes = nullptr;\nstatic ANIM_RANGE *m_Ranges = nullptr;\nstatic ANIM_BONE *m_Bones = nullptr;\nstatic ANIM m_NullAnim = {};\n\nvoid Anim_InitialiseAnims(const int32_t num_anims)\n{\n    m_AnimCount = num_anims;\n    m_Anims = GameBuf_Alloc(sizeof(ANIM) * num_anims, GBUF_ANIMS);\n}\n\nvoid Anim_InitialiseChanges(const int32_t num_changes)\n{\n    m_ChangeCount = num_changes;\n    m_Changes =\n        GameBuf_Alloc(sizeof(ANIM_CHANGE) * num_changes, GBUF_ANIM_CHANGES);\n}\n\nvoid Anim_InitialiseRanges(const int32_t num_ranges)\n{\n    m_RangeCount = num_ranges;\n    m_Ranges = GameBuf_Alloc(sizeof(ANIM_RANGE) * num_ranges, GBUF_ANIM_RANGES);\n}\n\nvoid Anim_InitialiseBones(const int32_t num_bones)\n{\n    m_BoneCount = num_bones;\n    m_Bones = GameBuf_Alloc(sizeof(ANIM_BONE) * num_bones, GBUF_ANIM_BONES);\n}\n\nint32_t Anim_GetTotalCount(void)\n{\n    return m_AnimCount;\n}\n\nANIM *Anim_GetAnim(const int32_t anim_idx)\n{\n    if (anim_idx < 0 || anim_idx >= m_AnimCount) {\n        return &m_NullAnim;\n    }\n    return &m_Anims[anim_idx];\n}\n\nANIM_CHANGE *Anim_GetChange(const int32_t change_idx)\n{\n    ASSERT(change_idx >= 0 && change_idx < m_ChangeCount);\n    return &m_Changes[change_idx];\n}\n\nANIM_RANGE *Anim_GetRange(const int32_t range_idx)\n{\n    ASSERT(range_idx >= 0 && range_idx < m_RangeCount);\n    return &m_Ranges[range_idx];\n}\n\nANIM_BONE *Anim_GetBone(const int32_t bone_idx)\n{\n    ANIM_BONE *const result = Anim_TryGetBone(bone_idx);\n    ASSERT(result != nullptr);\n    return result;\n}\n\nANIM_BONE *Anim_TryGetBone(const int32_t bone_idx)\n{\n    if (bone_idx >= 0 && bone_idx < m_BoneCount) {\n        return &m_Bones[bone_idx];\n    }\n    return nullptr;\n}\n\nbool Anim_TestAbsFrameEqual(const int16_t abs_frame, const int16_t frame)\n{\n    return abs_frame == frame;\n}\n\nbool Anim_TestAbsFrameRange(\n    const int16_t abs_frame, const int16_t start, const int16_t end)\n{\n    return abs_frame >= start && abs_frame <= end;\n}\n\nbool Anim_HasChange(const ANIM *const anim, const int16_t goal_state_id)\n{\n    for (int32_t i = 0; i < anim->num_changes; i++) {\n        const ANIM_CHANGE *const change = Anim_GetChange(anim->change_idx + i);\n        if (change->goal_anim_state == goal_state_id) {\n            return true;\n        }\n    }\n    return false;\n}\n\nbool Anim_HasFXCommand(const ANIM *const anim, const int16_t fx_num)\n{\n    return Anim_HasFXCommandBetween(\n        anim, fx_num, 0, anim->frame_end - anim->frame_base);\n}\n\nbool Anim_HasFXCommandBetween(\n    const ANIM *const anim, const int16_t fx_num, const int32_t frame_a,\n    const int32_t frame_b)\n{\n    for (int32_t i = 0; i < anim->num_commands; i++) {\n        const ANIM_COMMAND *const cmd = &anim->commands[i];\n        if (cmd->type != AC_EFFECT) {\n            continue;\n        }\n\n        const ANIM_COMMAND_EFFECT_DATA *const data =\n            (ANIM_COMMAND_EFFECT_DATA *)cmd->data;\n        if (data->effect_num != fx_num) {\n            continue;\n        }\n\n        const int32_t frame_num = data->frame_num - anim->frame_base;\n        if (frame_num >= frame_a && frame_num <= frame_b) {\n            return true;\n        }\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "src/trx/game/anims/common.h",
    "content": "#pragma once\n\n#include <trx/game/anims/types.h>\n#include <trx/game/level/format/format.h>\n\n#define NO_ANIM (-1)\n\nvoid Anim_InitialiseAnims(int32_t num_anims);\nvoid Anim_InitialiseChanges(int32_t num_changes);\nvoid Anim_InitialiseRanges(int32_t num_ranges);\nvoid Anim_InitialiseBones(int32_t num_bones);\n\nvoid Anim_LoadCommands(const int16_t *data);\n\nvoid Anim_InitialiseFrames(int32_t num_frames);\nint32_t Anim_GetTotalFrameCount(\n    const LEVEL_FORMAT_LOADER *loader, int32_t frame_data_length);\nvoid Anim_LoadFrames(\n    const LEVEL_FORMAT_LOADER *loader, const int16_t *data,\n    int32_t data_length);\n\nint32_t Anim_GetTotalCount(void);\nANIM *Anim_GetAnim(int32_t anim_idx);\nANIM_CHANGE *Anim_GetChange(int32_t change_idx);\nANIM_RANGE *Anim_GetRange(int32_t range_idx);\nANIM_BONE *Anim_GetBone(int32_t bone_idx);\nANIM_BONE *Anim_TryGetBone(int32_t bone_idx);\n\nbool Anim_TestAbsFrameEqual(int16_t abs_frame, int16_t frame);\nbool Anim_TestAbsFrameRange(int16_t abs_frame, int16_t start, int16_t end);\nbool Anim_HasChange(const ANIM *anim, int16_t goal_state_id);\nbool Anim_HasFXCommand(const ANIM *anim, int16_t fx_num);\nbool Anim_HasFXCommandBetween(\n    const ANIM *anim, int16_t fx_num, int32_t frame_a, int32_t frame_b);\n"
  },
  {
    "path": "src/trx/game/anims/enum.h",
    "content": "#pragma once\n\n// clang-format off\ntypedef enum {\n    AC_NULL          = 0,\n    AC_MOVE_ORIGIN   = 1,\n    AC_JUMP_VELOCITY = 2,\n    AC_ATTACK_READY  = 3,\n    AC_DEACTIVATE    = 4,\n    AC_SOUND_FX      = 5,\n    AC_EFFECT        = 6,\n} ANIM_COMMAND_TYPE;\n\ntypedef enum {\n    ACE_ALL   = 0,\n    ACE_LAND  = 1,\n    ACE_WATER = 2,\n} ANIM_COMMAND_ENVIRONMENT;\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/anims/frames.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/anims.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/objects/common.h>\n#include <trx/version.h>\n\n#include <math.h>\n\ntypedef enum {\n    RPM_ALL = 0,\n    RPM_X = 1,\n    RPM_Y = 2,\n    RPM_Z = 3,\n} ROT_PACK_MODE;\n\nstatic ANIM_FRAME *m_Frames = nullptr;\n\nstatic int32_t M_GetAnimFrameCount(\n    const LEVEL_FORMAT_LOADER *const loader, const int32_t anim_idx,\n    const int32_t frame_data_length)\n{\n    const ANIM *const anim = Anim_GetAnim(anim_idx);\n    if (loader->game_version == 1) {\n        return (int32_t)ceil(\n            ((anim->frame_end - anim->frame_base) / (float)anim->interpolation)\n            + 1);\n    } else {\n        uint32_t next_ofs = anim_idx == Anim_GetTotalCount() - 1\n            ? (unsigned)(sizeof(int16_t) * frame_data_length)\n            : Anim_GetAnim(anim_idx + 1)->frame_ofs;\n        if (anim->frame_size == 0) {\n            ASSERT(next_ofs - anim->frame_ofs == 0);\n            return 0;\n        }\n        return (next_ofs - anim->frame_ofs)\n            / (int32_t)(sizeof(int16_t) * anim->frame_size);\n    }\n}\n\nstatic OBJECT *M_GetAnimObject(const int32_t anim_idx)\n{\n    for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) {\n        OBJECT *const obj = Object_Get(i);\n        if (obj->loaded && obj->mesh_count >= 0 && obj->anim_idx == anim_idx) {\n            return obj;\n        }\n    }\n\n    return nullptr;\n}\n\nstatic ANIM_FRAME *M_FindFrameBase(const uint32_t frame_ofs)\n{\n    const int32_t anim_count = Anim_GetTotalCount();\n    for (int32_t i = 0; i < anim_count; i++) {\n        const ANIM *const anim = Anim_GetAnim(i);\n        if (anim->frame_ofs == frame_ofs) {\n            return anim->frame_ptr;\n        }\n    }\n\n    return nullptr;\n}\n\nstatic void M_ExtractRotation(\n    XYZ_16 *const rot, const int16_t rot_val_1, const int16_t rot_val_2)\n{\n    rot->x = (rot_val_1 & 0x3FF0) << 2;\n    rot->y = (((rot_val_1 & 0xF) << 6) | ((rot_val_2 & 0xFC00) >> 10)) << 6;\n    rot->z = (rot_val_2 & 0x3FF) << 6;\n}\n\nstatic void M_ParseMeshRotation(\n    const LEVEL_FORMAT_LOADER *const loader, XYZ_16 *const rot,\n    const int16_t **data)\n{\n    const int16_t *data_ptr = *data;\n    if (loader->game_version == 1) {\n        const int16_t rot_val_1 = *data_ptr++;\n        const int16_t rot_val_2 = *data_ptr++;\n        M_ExtractRotation(rot, rot_val_2, rot_val_1);\n    } else {\n        rot->x = 0;\n        rot->y = 0;\n        rot->z = 0;\n\n        const int16_t rot_val_1 = *data_ptr++;\n        const ROT_PACK_MODE mode = (ROT_PACK_MODE)((rot_val_1 & 0xC000) >> 14);\n        switch (mode) {\n        case RPM_X:\n            rot->x = (rot_val_1 & 0x3FF) << 6;\n            break;\n        case RPM_Y:\n            rot->y = (rot_val_1 & 0x3FF) << 6;\n            break;\n        case RPM_Z:\n            rot->z = (rot_val_1 & 0x3FF) << 6;\n            break;\n        default:\n            const int16_t rot_val_2 = *data_ptr++;\n            M_ExtractRotation(rot, rot_val_1, rot_val_2);\n            break;\n        }\n    }\n    *data = data_ptr;\n}\n\nstatic int32_t M_ParseFrame(\n    const LEVEL_FORMAT_LOADER *const loader, ANIM_FRAME *const frame,\n    const int16_t *data_ptr, int16_t mesh_count, const uint8_t frame_size)\n{\n    const int16_t *const frame_start = data_ptr;\n\n    frame->bounds.min.x = *data_ptr++;\n    frame->bounds.max.x = *data_ptr++;\n    frame->bounds.min.y = *data_ptr++;\n    frame->bounds.max.y = *data_ptr++;\n    frame->bounds.min.z = *data_ptr++;\n    frame->bounds.max.z = *data_ptr++;\n    frame->offset.x = *data_ptr++;\n    frame->offset.y = *data_ptr++;\n    frame->offset.z = *data_ptr++;\n    if (loader->game_version == 1) {\n        mesh_count = *data_ptr++;\n    }\n\n    frame->mesh_rots =\n        GameBuf_Alloc(sizeof(XYZ_16) * mesh_count, GBUF_ANIM_FRAMES);\n    for (int32_t i = 0; i < mesh_count; i++) {\n        XYZ_16 *const rot = &frame->mesh_rots[i];\n        M_ParseMeshRotation(loader, rot, &data_ptr);\n    }\n\n    if (loader->game_version > 1) {\n        data_ptr += MAX(0, frame_size - (data_ptr - frame_start));\n    }\n\n    return data_ptr - frame_start;\n}\n\nint32_t Anim_GetTotalFrameCount(\n    const LEVEL_FORMAT_LOADER *const loader, const int32_t frame_data_length)\n{\n    const int32_t anim_count = Anim_GetTotalCount();\n    int32_t total_frame_count = 0;\n    for (int32_t i = 0; i < anim_count; i++) {\n        total_frame_count += M_GetAnimFrameCount(loader, i, frame_data_length);\n    }\n    return total_frame_count;\n}\n\nvoid Anim_InitialiseFrames(const int32_t num_frames)\n{\n    LOG_INFO(\"%d anim frames\", num_frames);\n    m_Frames = GameBuf_Alloc(sizeof(ANIM_FRAME) * num_frames, GBUF_ANIM_FRAMES);\n}\n\nvoid Anim_LoadFrames(\n    const LEVEL_FORMAT_LOADER *const loader, const int16_t *data,\n    const int32_t data_length)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n\n    const int32_t anim_count = Anim_GetTotalCount();\n    OBJECT *cur_obj = nullptr;\n    int32_t frame_idx = 0;\n\n    for (int32_t i = 0; i < anim_count; i++) {\n        OBJECT *const next_obj = M_GetAnimObject(i);\n        const bool obj_changed = next_obj != nullptr;\n        if (obj_changed) {\n            cur_obj = next_obj;\n            cur_obj->anim_count = 0;\n        }\n\n        if (cur_obj == nullptr) {\n            continue;\n        }\n\n        ANIM *const anim = Anim_GetAnim(i);\n        cur_obj->anim_count++;\n        const int32_t frame_count = M_GetAnimFrameCount(loader, i, data_length);\n        const int16_t *data_ptr = &data[anim->frame_ofs / sizeof(int16_t)];\n        for (int32_t j = 0; j < frame_count; j++) {\n            ANIM_FRAME *const frame = &m_Frames[frame_idx++];\n            if (j == 0) {\n                anim->frame_ptr = frame;\n                if (obj_changed) {\n                    cur_obj->frame_base = frame;\n                }\n            }\n\n            data_ptr += M_ParseFrame(\n                loader, frame, data_ptr, cur_obj->mesh_count, anim->frame_size);\n        }\n    }\n\n    // Some OG data contains objects that point to the previous object's frames,\n    // so ensure everything that's loaded is configured as such.\n    for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) {\n        OBJECT *const obj = Object_Get(i);\n        if (obj->loaded && obj->mesh_count >= 0 && obj->anim_idx == NO_ANIM\n            && obj->frame_base == nullptr) {\n            obj->frame_base = M_FindFrameBase(obj->frame_ofs);\n        }\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/anims/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/anims/enum.h>\n\ntypedef struct {\n    int16_t goal_anim_state;\n    int16_t num_ranges;\n    int16_t range_idx;\n} ANIM_CHANGE;\n\ntypedef struct {\n    int16_t start_frame;\n    int16_t end_frame;\n    int16_t link_anim_num;\n    int16_t link_frame_num;\n} ANIM_RANGE;\n\ntypedef struct {\n    ANIM_COMMAND_TYPE type;\n    void *data;\n} ANIM_COMMAND;\n\ntypedef struct {\n    int16_t fall_speed;\n    int16_t speed;\n} ANIM_COMMAND_VELOCITY_DATA;\n\ntypedef struct {\n    int16_t frame_num;\n    int16_t effect_num;\n    ANIM_COMMAND_ENVIRONMENT environment;\n    int16_t fx_type;\n} ANIM_COMMAND_EFFECT_DATA;\n\ntypedef struct {\n    bool matrix_pop;\n    bool matrix_push;\n    XYZ_BOOL rot;\n    XYZ_32 pos;\n} ANIM_BONE;\n\ntypedef struct {\n    BOUNDS_16 bounds;\n    XYZ_16 offset;\n    XYZ_16 *mesh_rots;\n} ANIM_FRAME;\n\ntypedef struct {\n    ANIM_FRAME *frame_ptr;\n    uint32_t frame_ofs;\n    uint8_t interpolation;\n    uint8_t frame_size;\n    int16_t current_anim_state;\n    int32_t velocity;\n    int32_t acceleration;\n    int16_t frame_base;\n    int16_t frame_end;\n    int16_t jump_anim_num;\n    int16_t jump_frame_num;\n    int16_t num_changes;\n    int16_t change_idx;\n    int16_t num_commands;\n    int16_t command_idx;\n    ANIM_COMMAND *commands;\n} ANIM;\n"
  },
  {
    "path": "src/trx/game/anims.h",
    "content": "#pragma once\n\n#include <trx/game/anims/common.h>\n#include <trx/game/anims/enum.h>\n#include <trx/game/anims/types.h>\n"
  },
  {
    "path": "src/trx/game/camera/box_camera.c",
    "content": "#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\n// clang-format off\n#define M_MAX_ELEVATION   (85 * DEG_1) // = 15470\n#define M_COMBAT_DISTANCE (WALL_L * 5 / 2) // = 2560\n#define M_LOOK_DISTANCE   (WALL_L * 3 / 2) // = 1536\n#define M_LOOK_CLAMP      (STEP_L + 50) // = 296\n// clang-format on\n\n#define M_SHIFT_ARGS                                                           \\\n    int32_t *x, int32_t *y, int32_t *h, int32_t target_x, int32_t target_y,    \\\n        int32_t target_h, int32_t left, int32_t top, int32_t right,            \\\n        int32_t bottom\n\ntypedef struct {\n    int16_t chase_speed;\n    int32_t min_square;\n    int32_t shift_scale;\n    bool test_shift_pair;\n    bool clip_shift_height;\n    bool test_early_lb_shift;\n    bool use_fixed_los_check;\n    bool override_chase_speed;\n    CAMERA_LOOK_SETTINGS look_settings;\n    CAMERA_LOOK_SETTINGS look_settings_surf;\n} M_SETTINGS;\n\nstatic const M_SETTINGS m_CameraSettings[CAMERA_MODE_NUMBER_OF] = {\n    [CAMERA_MODE_TR1] = {\n        .chase_speed = 12,\n        .min_square = SQUARE(WALL_L / 4),\n        .shift_scale = 1,\n        .test_shift_pair = false,\n        .clip_shift_height = false,\n        .test_early_lb_shift = true,\n        .use_fixed_los_check = false,\n        .override_chase_speed = false,\n        .look_settings = {\n            // clang-format off\n            .max_head_rotation = +50 * DEG_1,\n            .min_head_rotation = -50 * DEG_1,\n            .head_turn         = +2 * DEG_1,\n            .max_head_tilt     = +22 * DEG_1,\n            .min_head_tilt     = -42 * DEG_1,\n            .torso_head_rot_y  = 1.0f,\n            .torso_head_rot_x  = 1.0f,\n            // clang-format on\n        },\n        .look_settings_surf = {\n            // clang-format off\n            .head_turn         = +3 * DEG_1,\n            .max_head_rotation = +50 * DEG_1,\n            .min_head_rotation = -50 * DEG_1,\n            .max_head_tilt     = +40 * DEG_1,\n            .min_head_tilt     = -40 * DEG_1,\n            .torso_head_rot_y  = 0.5f,\n            .torso_head_rot_x  = 0.0f,\n            // clang-format on\n        },\n    },\n\n    [CAMERA_MODE_TR2] = {\n        .chase_speed = 10,\n        .min_square = SQUARE(WALL_L / 3),\n        .shift_scale = 2,\n        .test_shift_pair = true,\n        .clip_shift_height = true,\n        .test_early_lb_shift = false,\n        .use_fixed_los_check = true,\n        .override_chase_speed = true,\n        .look_settings = {\n            // clang-format off\n            .head_turn         = +2 * DEG_1,\n            .max_head_rotation = +44 * DEG_1,\n            .min_head_rotation = -44 * DEG_1,\n            .max_head_tilt     = +22 * DEG_1,\n            .min_head_tilt     = -42 * DEG_1,\n            .torso_head_rot_y  = 1.0f,\n            .torso_head_rot_x  = 1.0f,\n            // clang-format on\n        },\n        .look_settings_surf = {\n            // clang-format off\n            .head_turn         = +3 * DEG_1,\n            .max_head_rotation = +50 * DEG_1,\n            .min_head_rotation = -50 * DEG_1,\n            .max_head_tilt     = +40 * DEG_1,\n            .min_head_tilt     = -40 * DEG_1,\n            .torso_head_rot_y  = 0.5f,\n            .torso_head_rot_x  = 0.0f,\n            // clang-format on\n        },\n    },\n};\n\nstatic BOX_INFO m_FixedBox = {};\n\nstatic const M_SETTINGS *M_GetSettings(void)\n{\n    return &m_CameraSettings[g_Config.visuals.camera_mode];\n}\n\nstatic int16_t M_GetChaseSpeed(void)\n{\n    return M_GetSettings()->chase_speed;\n}\n\nstatic const CAMERA_LOOK_SETTINGS *M_GetLookSettingsFunc(const bool on_surface)\n{\n    return on_surface ? &M_GetSettings()->look_settings_surf\n                      : &M_GetSettings()->look_settings;\n}\n\nstatic const BOX_INFO *M_GetBox(\n    const SECTOR *const sector, const int32_t x, const int32_t z,\n    const bool generate_box)\n{\n    if (sector->box != NO_BOX) {\n        return Box_GetBox(sector->box);\n    }\n\n    if (!generate_box) {\n        return nullptr;\n    }\n\n    // A level may have blocked specific sector or room pathfinding, so create a\n    // dummy one-sector box to prevent erratic camera positioning.\n    m_FixedBox.left = ROUND_TO_SECTOR(z);\n    m_FixedBox.top = ROUND_TO_SECTOR(x);\n    m_FixedBox.right = ROUND_TO_SECTOR_END(z);\n    m_FixedBox.bottom = ROUND_TO_SECTOR_END(x);\n    return &m_FixedBox;\n}\n\nstatic const SECTOR *M_GetSector(\n    const int32_t x, const int32_t y, const int32_t z, int16_t room_num)\n{\n    const XYZ_32 pos = { x, y, z };\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (y > height || y < ceiling) {\n        return nullptr;\n    }\n    return sector;\n}\n\nstatic bool M_IsGoodPosition(\n    const int32_t x, const int32_t y, const int32_t z, int16_t room_num)\n{\n    return M_GetSector(x, y, z, room_num) != nullptr;\n}\n\nstatic int32_t M_ShiftClamp(GAME_VECTOR *const pos, const int32_t clamp)\n{\n    const SECTOR *const sector = Room_GetSector(pos->pos, &pos->room_num);\n    const BOX_INFO *const box = M_GetBox(sector, pos->x, pos->z, true);\n\n    const XYZ_32 old_pos = pos->pos;\n\n    const int32_t left = box->left + clamp;\n    const int32_t right = box->right - clamp;\n    if (pos->z < left\n        && !M_IsGoodPosition(pos->x, pos->y, pos->z - clamp, pos->room_num)) {\n        pos->z = left;\n    } else if (\n        pos->z > right\n        && !M_IsGoodPosition(pos->x, pos->y, pos->z + clamp, pos->room_num)) {\n        pos->z = right;\n    }\n\n    const int32_t top = box->top + clamp;\n    const int32_t bottom = box->bottom - clamp;\n    if (pos->x < top\n        && !M_IsGoodPosition(pos->x - clamp, pos->y, pos->z, pos->room_num)) {\n        pos->x = top;\n    } else if (\n        pos->x > bottom\n        && !M_IsGoodPosition(pos->x + clamp, pos->y, pos->z, pos->room_num)) {\n        pos->x = bottom;\n    }\n\n    int32_t height = Room_GetHeight(sector, old_pos) - clamp;\n    int32_t ceiling = Room_GetCeiling(sector, old_pos) + clamp;\n\n    if (height < ceiling) {\n        ceiling = (height + ceiling) >> 1;\n        height = ceiling;\n    }\n\n    if (old_pos.y > height) {\n        return height - old_pos.y;\n    } else if (old_pos.y < ceiling) {\n        return ceiling - old_pos.y;\n    }\n\n    return 0;\n}\n\nstatic void M_SmartShift(GAME_VECTOR *const target, void (*shift)(M_SHIFT_ARGS))\n{\n    LOS_Check(&g_Camera.target, target, false);\n\n    const ROOM *room = Room_Get(g_Camera.target.room_num);\n    const SECTOR *sector =\n        Room_GetWorldSector(room, g_Camera.target.x, g_Camera.target.z);\n    const BOX_INFO *box =\n        M_GetBox(sector, g_Camera.target.x, g_Camera.target.z, true);\n\n    room = Room_Get(target->room_num);\n    sector = Room_GetWorldSector(room, target->x, target->z);\n    if (room->flags.swamp) {\n        target->y = room->max_ceiling - STEP_L;\n        sector = Room_GetSector(target->pos, &target->room_num);\n    }\n\n    if (target->z < box->left || target->z > box->right || target->x < box->top\n        || target->x > box->bottom) {\n        box = M_GetBox(sector, target->x, target->z, true);\n    }\n\n    int32_t left = box->left;\n    int32_t right = box->right;\n    int32_t top = box->top;\n    int32_t bottom = box->bottom;\n\n    const M_SETTINGS *const settings = M_GetSettings();\n\n    int32_t test = ROUND_TO_SECTOR_END(target->z - WALL_L);\n    const SECTOR *const good_left =\n        M_GetSector(target->x, target->y, test, target->room_num);\n    if (good_left != nullptr) {\n        box = M_GetBox(good_left, target->x, test, false);\n        if (box != nullptr && box->left < left) {\n            left = box->left;\n        }\n    } else if (settings->test_shift_pair) {\n        left = test;\n    }\n\n    test = ROUND_TO_SECTOR(target->z + WALL_L);\n    const SECTOR *const good_right =\n        M_GetSector(target->x, target->y, test, target->room_num);\n    if (good_right != nullptr) {\n        box = M_GetBox(good_right, target->x, test, false);\n        if (box != nullptr && box->right > right) {\n            right = box->right;\n        }\n    } else if (settings->test_shift_pair) {\n        right = test;\n    }\n\n    test = ROUND_TO_SECTOR_END(target->x - WALL_L);\n    const SECTOR *const good_top =\n        M_GetSector(test, target->y, target->z, target->room_num);\n    if (good_top != nullptr) {\n        box = M_GetBox(good_top, test, target->z, false);\n        if (box != nullptr && box->top < top) {\n            top = box->top;\n        }\n    } else if (settings->test_shift_pair) {\n        top = test;\n    }\n\n    test = ROUND_TO_SECTOR(target->x + WALL_L);\n    const SECTOR *const good_bottom =\n        M_GetSector(test, target->y, target->z, target->room_num);\n    if (good_bottom != nullptr) {\n        box = M_GetBox(good_bottom, test, target->z, false);\n        if (box != nullptr && box->bottom > bottom) {\n            bottom = box->bottom;\n        }\n    } else if (settings->test_shift_pair) {\n        bottom = test;\n    }\n\n    left += STEP_L;\n    right -= STEP_L;\n    top += STEP_L;\n    bottom -= STEP_L;\n\n    GAME_VECTOR target_a = {\n        .x = target->x,\n        .y = target->y,\n        .z = target->z,\n        .room_num = target->room_num,\n    };\n\n    GAME_VECTOR target_b = {\n        .x = target->x,\n        .y = target->y,\n        .z = target->z,\n        .room_num = target->room_num,\n    };\n\n    bool clip = false;\n    bool prefer_a = !settings->test_shift_pair;\n\n#define L_SHIFT(axis1, axis2, l1, l2, r1, r2)                                  \\\n    shift(                                                                     \\\n        &target_a.axis1, &target_a.axis2, &target_a.y, g_Camera.target.axis1,  \\\n        g_Camera.target.axis2, g_Camera.target.y, l1, l2, r1, r2);             \\\n    shift(                                                                     \\\n        &target_b.axis1, &target_b.axis2, &target_b.y, g_Camera.target.axis1,  \\\n        g_Camera.target.axis2, g_Camera.target.y, l1, r2, r1, l2)\n\n    if (!settings->test_shift_pair) {\n        if (target->z < left && good_left == nullptr) {\n            clip = true;\n            if (target->x < g_Camera.target.x) {\n                L_SHIFT(z, x, left, top, right, bottom);\n            } else {\n                L_SHIFT(z, x, left, bottom, right, top);\n            }\n        } else if (target->z > right && good_right == nullptr) {\n            clip = true;\n            if (target->x < g_Camera.target.x) {\n                L_SHIFT(z, x, right, top, left, bottom);\n            } else {\n                L_SHIFT(z, x, right, bottom, left, top);\n            }\n        } else if (target->x < top && good_top == nullptr) {\n            clip = true;\n            if (target->z < g_Camera.target.z) {\n                L_SHIFT(x, z, top, left, bottom, right);\n            } else {\n                L_SHIFT(x, z, top, right, bottom, left);\n            }\n        } else if (target->x > bottom && good_bottom == nullptr) {\n            clip = true;\n            if (target->z < g_Camera.target.z) {\n                L_SHIFT(x, z, bottom, left, top, right);\n            } else {\n                L_SHIFT(x, z, bottom, right, top, left);\n            }\n        }\n    } else {\n        if (ABS(target->z - g_Camera.target.z)\n            > ABS(target->x - g_Camera.target.x)) {\n            if (target->z < left && good_left == nullptr) {\n                clip = true;\n                prefer_a = g_Camera.pos.x < g_Camera.target.x;\n                L_SHIFT(z, x, left, top, right, bottom);\n            } else if (target->z > right && good_right == nullptr) {\n                clip = true;\n                prefer_a = g_Camera.pos.x < g_Camera.target.x;\n                L_SHIFT(z, x, right, top, left, bottom);\n            } else if (target->x < top && good_top == nullptr) {\n                clip = true;\n                prefer_a = target->z < g_Camera.target.z;\n                L_SHIFT(x, z, top, left, bottom, right);\n            } else if (target->x > bottom && good_bottom == nullptr) {\n                clip = true;\n                prefer_a = target->z < g_Camera.target.z;\n                L_SHIFT(x, z, bottom, left, top, right);\n            }\n        } else {\n            if (target->x < top && good_top == nullptr) {\n                clip = true;\n                prefer_a = g_Camera.pos.z < g_Camera.target.z;\n                L_SHIFT(x, z, top, left, bottom, right);\n            } else if (target->x > bottom && good_bottom == nullptr) {\n                clip = true;\n                prefer_a = g_Camera.pos.z < g_Camera.target.z;\n                L_SHIFT(x, z, bottom, left, top, right);\n            } else if (target->z < left && good_left == nullptr) {\n                clip = true;\n                prefer_a = target->x < g_Camera.target.x;\n                L_SHIFT(z, x, left, top, right, bottom);\n            } else if (target->z > right && good_right == nullptr) {\n                clip = true;\n                prefer_a = target->x < g_Camera.target.x;\n                L_SHIFT(z, x, right, top, left, bottom);\n            }\n        }\n    }\n\n#undef L_SHIFT\n\n    if (!clip) {\n        return;\n    }\n\n    if (settings->test_shift_pair) {\n        if (prefer_a) {\n            prefer_a = LOS_Check(&g_Camera.target, &target_a, false);\n        } else {\n            prefer_a = !LOS_Check(&g_Camera.target, &target_b, false);\n        }\n    }\n\n    if (prefer_a) {\n        target->pos = target_a.pos;\n    } else {\n        target->pos = target_b.pos;\n    }\n\n    Room_GetSector(target->pos, &target->room_num);\n}\n\nstatic void M_Clip(M_SHIFT_ARGS)\n{\n    const int32_t x_diff = *x - target_x;\n    const int32_t y_diff = *y - target_y;\n    const int32_t h_diff = *h - target_h;\n    int32_t height = *h;\n\n    if ((right > left) != (target_x < left)) {\n        if (x_diff != 0) {\n            *y = target_y + (left - target_x) * y_diff / x_diff;\n            height = target_h + (left - target_x) * h_diff / x_diff;\n        }\n        *x = left;\n    }\n\n    if ((bottom > top && target_y > top && (*y) < top)\n        || (bottom < top && target_y < top && (*y) > top)) {\n        if (y_diff != 0) {\n            *x = target_x + (top - target_y) * x_diff / y_diff;\n            height = target_h + (top - target_y) * h_diff / y_diff;\n        }\n        *y = top;\n    }\n\n    const M_SETTINGS *const settings = M_GetSettings();\n    if (settings->clip_shift_height) {\n        *h = height;\n    }\n}\n\nstatic void M_Shift(M_SHIFT_ARGS)\n{\n    const int32_t l_square = SQUARE(target_x - left);\n    const int32_t r_square = SQUARE(target_x - right);\n    const int32_t t_square = SQUARE(target_y - top);\n    const int32_t b_square = SQUARE(target_y - bottom);\n\n    const int32_t tl_square = t_square + l_square;\n    const int32_t tr_square = t_square + r_square;\n    const int32_t bl_square = b_square + l_square;\n\n    const M_SETTINGS *const settings = M_GetSettings();\n    const int32_t scaled_target =\n        g_Camera.target_square * settings->shift_scale;\n\n    int32_t shift;\n    if (g_Camera.target_square < tl_square) {\n        *x = left;\n        shift = g_Camera.target_square - l_square;\n        if (shift >= 0) {\n            shift = Math_Sqrt(shift);\n            *y = target_y + (top >= bottom ? shift : -shift);\n        }\n    } else if (tl_square > settings->min_square) {\n        *x = left;\n        *y = top;\n    } else if (g_Camera.target_square < bl_square) {\n        *x = left;\n        shift = g_Camera.target_square - l_square;\n        if (shift >= 0) {\n            shift = Math_Sqrt(shift);\n            *y = target_y + (top < bottom ? shift : -shift);\n        }\n    } else if (\n        settings->test_early_lb_shift && bl_square > settings->min_square) {\n        *x = left;\n        *y = bottom;\n    } else if (scaled_target < tr_square) {\n        shift = scaled_target - t_square;\n        if (shift >= 0) {\n            shift = Math_Sqrt(shift);\n            *x = target_x + (left < right ? shift : -shift);\n            *y = top;\n        }\n    } else if (settings->test_early_lb_shift || bl_square <= tr_square) {\n        *x = right;\n        *y = top;\n    } else {\n        *x = left;\n        *y = bottom;\n    }\n}\n\nstatic void M_Move(const GAME_VECTOR *const target, const int32_t speed)\n{\n    const GAME_VECTOR old_pos = g_Camera.pos;\n    GAME_VECTOR pos = g_Camera.pos;\n    pos.x += (target->x - pos.x) / speed;\n    pos.z += (target->z - pos.z) / speed;\n    pos.y += (target->y - pos.y) / speed;\n    pos.room_num = target->room_num;\n\n    Camera_SetChunky(false);\n\n    const SECTOR *sector = Room_GetSector(pos.pos, &pos.room_num);\n    int32_t height = Room_GetHeight(sector, pos.pos);\n    if (height == NO_HEIGHT) {\n        // Attempt to clamp within the previous sector's height bounds. Only if\n        // that fails continue to revert fully to the last good Y position.\n        pos.room_num = old_pos.room_num;\n        sector = Room_GetSector(old_pos.pos, &pos.room_num);\n        height = Room_GetHeight(sector, old_pos.pos);\n        const int32_t old_ceiling = Room_GetCeiling(sector, old_pos.pos);\n        CLAMP(pos.y, old_ceiling + STEP_L, height - STEP_L);\n        sector = Room_GetSector(pos.pos, &pos.room_num);\n        height = Room_GetHeight(sector, pos.pos);\n        if (height == NO_HEIGHT) {\n            pos.y = old_pos.y;\n            pos.room_num = old_pos.room_num;\n            sector = Room_GetSector(pos.pos, &pos.room_num);\n            height = Room_GetHeight(sector, pos.pos);\n        }\n    }\n\n    height -= STEP_L;\n    if (pos.y >= height && target->y >= height) {\n        LOS_Check(&g_Camera.target, &pos, false);\n        sector = Room_GetSector(pos.pos, &pos.room_num);\n        height = Room_GetHeight(sector, pos.pos) - STEP_L;\n    }\n\n    g_Camera.pos = pos;\n\n    int32_t ceiling = Room_GetCeiling(sector, pos.pos) + STEP_L;\n    if (height < ceiling) {\n        ceiling = (height + ceiling) >> 1;\n        height = ceiling;\n    }\n\n    Camera_ApplyBounce();\n\n    if (g_Camera.pos.y > height) {\n        g_Camera.shift = height - g_Camera.pos.y;\n    } else if (g_Camera.pos.y < ceiling) {\n        g_Camera.shift = ceiling - g_Camera.pos.y;\n    } else {\n        g_Camera.shift = 0;\n    }\n\n    Camera_UpdateMicPosition();\n}\n\nstatic void M_Chase(const ITEM *const item)\n{\n    g_Camera.target_elevation += item->rot.x;\n    g_Camera.target_elevation = MIN(g_Camera.target_elevation, M_MAX_ELEVATION);\n    g_Camera.target_elevation =\n        MAX(g_Camera.target_elevation, -M_MAX_ELEVATION);\n\n    const int32_t distance =\n        (g_Camera.target_distance * Math_Cos(g_Camera.target_elevation))\n        >> W2V_SHIFT;\n    const int16_t angle = g_Camera.target_angle + item->rot.y;\n\n    g_Camera.target_square = SQUARE(distance);\n\n    const XYZ_32 offset = {\n        .y = (g_Camera.target_distance * Math_Sin(g_Camera.target_elevation))\n            >> W2V_SHIFT,\n        .x = -((distance * Math_Sin(angle)) >> W2V_SHIFT),\n        .z = -((distance * Math_Cos(angle)) >> W2V_SHIFT),\n    };\n\n    GAME_VECTOR target = {\n        .x = g_Camera.target.x + offset.x,\n        .y = g_Camera.target.y + offset.y,\n        .z = g_Camera.target.z + offset.z,\n        .room_num = g_Camera.pos.room_num,\n    };\n\n    const M_SETTINGS *const settings = M_GetSettings();\n    const int16_t speed =\n        settings->override_chase_speed || g_Camera.fixed_camera\n        ? g_Camera.speed\n        : settings->chase_speed;\n    M_SmartShift(&target, M_Shift);\n    M_Move(&target, speed);\n}\n\nstatic void M_Combat(const ITEM *const item)\n{\n    g_Camera.target.z = item->pos.z;\n    g_Camera.target.x = item->pos.x;\n    g_Camera.target_distance = M_COMBAT_DISTANCE;\n\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->target != nullptr) {\n        g_Camera.target_angle = lara_info->target_angles[0] + item->rot.y;\n        g_Camera.target_elevation = lara_info->target_angles[1] + item->rot.x;\n    } else {\n        g_Camera.target_angle =\n            lara_info->torso_rot.y + lara_info->head_rot.y + item->rot.y;\n        g_Camera.target_elevation =\n            lara_info->torso_rot.x + lara_info->head_rot.x + item->rot.x;\n    }\n\n    const int32_t distance =\n        (M_COMBAT_DISTANCE * Math_Cos(g_Camera.target_elevation)) >> W2V_SHIFT;\n\n    const XYZ_32 offset = {\n        .y =\n            +((g_Camera.target_distance * Math_Sin(g_Camera.target_elevation))\n              >> W2V_SHIFT),\n        .x = -((distance * Math_Sin(g_Camera.target_angle)) >> W2V_SHIFT),\n        .z = -((distance * Math_Cos(g_Camera.target_angle)) >> W2V_SHIFT),\n    };\n\n    GAME_VECTOR target = {\n        .x = g_Camera.target.x + offset.x,\n        .y = g_Camera.target.y + offset.y,\n        .z = g_Camera.target.z + offset.z,\n        .room_num = g_Camera.pos.room_num,\n    };\n\n    if (lara_info->water_status == LWS_UNDERWATER) {\n        const ITEM *const lara_item = Lara_GetItem();\n        const int32_t water_height =\n            lara_info->water_surface_dist + lara_item->pos.y;\n        if (g_Camera.target.y > water_height && water_height > target.y) {\n            target.y = lara_info->water_surface_dist + lara_item->pos.y;\n            target.z = g_Camera.target.z\n                + (water_height - g_Camera.target.y)\n                    * (target.z - g_Camera.target.z)\n                    / (target.y - g_Camera.target.y);\n            target.x = g_Camera.target.x\n                + (water_height - g_Camera.target.y)\n                    * (target.x - g_Camera.target.x)\n                    / (target.y - g_Camera.target.y);\n        }\n    }\n\n    M_SmartShift(&target, M_Shift);\n    M_Move(&target, g_Camera.speed);\n}\n\nstatic void M_Fixed(void)\n{\n    const OBJECT_VECTOR *const fixed = Camera_GetFixedObject(g_Camera.num);\n    GAME_VECTOR target = {\n        .x = fixed->x,\n        .y = fixed->y,\n        .z = fixed->z,\n        .room_num = fixed->data,\n    };\n\n    const M_SETTINGS *const settings = M_GetSettings();\n    if (settings->use_fixed_los_check\n        && !LOS_Check(&g_Camera.target, &target, false)) {\n        M_ShiftClamp(&target, STEP_L);\n    }\n\n    g_Camera.fixed_camera = true;\n    M_Move(&target, g_Camera.speed);\n\n    if (g_Camera.timer != 0) {\n        g_Camera.timer--;\n        if (g_Camera.timer == 0) {\n            g_Camera.timer = -1;\n        }\n    }\n}\n\nstatic void M_Look(const ITEM *const item)\n{\n    const XYZ_32 old = {\n        .x = g_Camera.target.x,\n        .y = g_Camera.target.y,\n        .z = g_Camera.target.z,\n    };\n\n    g_Camera.target.z = item->pos.z;\n    g_Camera.target.x = item->pos.x;\n    g_Camera.target_distance = M_LOOK_DISTANCE;\n\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    g_Camera.target_angle =\n        item->rot.y + lara_info->torso_rot.y + lara_info->head_rot.y;\n    g_Camera.target_elevation =\n        item->rot.x + lara_info->torso_rot.x + lara_info->head_rot.x;\n\n    const int32_t distance =\n        (M_LOOK_DISTANCE * Math_Cos(g_Camera.target_elevation)) >> W2V_SHIFT;\n\n    g_Camera.shift =\n        (-STEP_L * 2 * Math_Sin(g_Camera.target_elevation)) >> W2V_SHIFT;\n    g_Camera.target.z += (g_Camera.shift * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    g_Camera.target.x += (g_Camera.shift * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n\n    if (!M_IsGoodPosition(\n            g_Camera.target.x, g_Camera.target.y, g_Camera.target.z,\n            g_Camera.target.room_num)) {\n        g_Camera.target.x = item->pos.x;\n        g_Camera.target.z = item->pos.z;\n    }\n\n    g_Camera.target.y += M_ShiftClamp(&g_Camera.target, M_LOOK_CLAMP);\n\n    const XYZ_32 offset = {\n        .y =\n            +((g_Camera.target_distance * Math_Sin(g_Camera.target_elevation))\n              >> W2V_SHIFT),\n        .x = -((distance * Math_Sin(g_Camera.target_angle)) >> W2V_SHIFT),\n        .z = -((distance * Math_Cos(g_Camera.target_angle)) >> W2V_SHIFT),\n    };\n\n    GAME_VECTOR target = {\n        .x = g_Camera.target.x + offset.x,\n        .y = g_Camera.target.y + offset.y,\n        .z = g_Camera.target.z + offset.z,\n        .room_num = g_Camera.pos.room_num,\n    };\n\n    M_SmartShift(&target, M_Clip);\n    g_Camera.target.z = old.z + (g_Camera.target.z - old.z) / g_Camera.speed;\n    g_Camera.target.x = old.x + (g_Camera.target.x - old.x) / g_Camera.speed;\n    M_Move(&target, g_Camera.speed);\n    g_Camera.debuff = 5;\n}\n\nstatic void M_ClampResult(void)\n{\n    XYZ_32 *const pos = &g_Camera.interp.result.pos;\n    const int32_t shift = g_Camera.interp.result.shift;\n    const ROOM *const room = Room_Get(g_Camera.interp.room_num);\n    const SECTOR *sector = Room_GetWorldSector(room, pos->x, pos->z);\n    if (sector->box != NO_BOX) {\n        goto finish;\n    }\n\n    sector = Room_GetWorldSector(room, g_Camera.pos.x, g_Camera.pos.z);\n    if (sector->box == NO_BOX) {\n        goto finish;\n    }\n\n    const BOX_INFO *const box = Box_GetBox(sector->box);\n    CLAMP(pos->x, box->top, box->bottom);\n    CLAMP(pos->z, box->left, box->right);\n\nfinish:\n    const int32_t floor = Room_GetHeightEx(sector, *pos, true, NO_ITEM);\n    const int32_t ceiling = Room_GetCeilingEx(sector, *pos, true);\n    if (floor != NO_HEIGHT && ceiling != NO_HEIGHT) {\n        CLAMP(pos->y, ceiling - shift, floor - shift);\n    }\n    Room_GetSector(\n        (XYZ_32) { pos->x, pos->y + shift, pos->z }, &g_Camera.interp.room_num);\n}\n\nstatic void M_Reset(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    ASSERT(lara_item != nullptr);\n    g_Camera.shift = lara_item->pos.y - WALL_L;\n\n    g_Camera.target.x = lara_item->pos.x;\n    g_Camera.target.y = g_Camera.shift;\n    g_Camera.target.z = lara_item->pos.z;\n    g_Camera.target.room_num = lara_item->room_num;\n\n    g_Camera.pos.x = g_Camera.target.x;\n    g_Camera.pos.y = g_Camera.target.y;\n    g_Camera.pos.z = g_Camera.target.z - 100;\n    g_Camera.pos.room_num = g_Camera.target.room_num;\n}\n\nstatic void M_Update(\n    const ITEM *const item, const bool fixed_camera, int32_t target_y)\n{\n    const BOUNDS_16 *bounds = Item_GetBoundsAccurate(item);\n\n    if (g_Camera.item != nullptr && !fixed_camera) {\n        bounds = Item_GetBoundsAccurate(g_Camera.item);\n\n        const int32_t dx = g_Camera.item->pos.x - item->pos.x;\n        const int32_t dz = g_Camera.item->pos.z - item->pos.z;\n        const int32_t shift = Math_Sqrt(SQUARE(dx) + SQUARE(dz));\n        int16_t angle = Math_Atan(dz, dx) - item->rot.y;\n\n        int16_t tilt = Math_Atan(\n            shift,\n            target_y - (bounds->min.y + bounds->max.y) / 2\n                - g_Camera.item->pos.y);\n        angle >>= 1;\n        tilt >>= 1;\n\n        if (angle > CAMERA_MIN_HEAD_ROTATION && angle < CAMERA_MAX_HEAD_ROTATION\n            && tilt > CAMERA_MIN_HEAD_TILT && tilt < CAMERA_MAX_HEAD_TILT) {\n            LARA_INFO *const lara_info = Lara_GetLaraInfo();\n            int16_t change = angle - lara_info->head_rot.y;\n            if (change > CAMERA_HEAD_TURN) {\n                lara_info->head_rot.y += CAMERA_HEAD_TURN;\n            } else if (change < -CAMERA_HEAD_TURN) {\n                lara_info->head_rot.y -= CAMERA_HEAD_TURN;\n            } else {\n                lara_info->head_rot.y = angle;\n            }\n\n            change = tilt - lara_info->head_rot.x;\n            if (change > CAMERA_HEAD_TURN) {\n                lara_info->head_rot.x += CAMERA_HEAD_TURN;\n            } else if (change < -CAMERA_HEAD_TURN) {\n                lara_info->head_rot.x -= CAMERA_HEAD_TURN;\n            } else {\n                lara_info->head_rot.x += change;\n            }\n            lara_info->torso_rot.x = lara_info->head_rot.x;\n            lara_info->torso_rot.y = lara_info->head_rot.y;\n            g_Camera.type = CAM_LOOK;\n            g_Camera.item->looked_at = true;\n        }\n    }\n\n    if (g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT) {\n        target_y -= STEP_L;\n        g_Camera.target.room_num = item->room_num;\n        if (g_Camera.fixed_camera) {\n            g_Camera.target.y = target_y;\n            g_Camera.speed = 1;\n        } else {\n            g_Camera.target.y += (target_y - g_Camera.target.y) >> 2;\n            g_Camera.speed = g_Camera.type == CAM_LOOK ? CAMERA_LOOK_SPEED\n                                                       : CAMERA_COMBAT_SPEED;\n        }\n        g_Camera.fixed_camera = false;\n        if (g_Camera.type == CAM_LOOK) {\n            M_Look(item);\n        } else {\n            M_Combat(item);\n        }\n    } else {\n        if (fixed_camera) {\n            g_Camera.debuff = 0;\n        }\n        if (g_Camera.debuff > 0) {\n            const XYZ_32 old = g_Camera.target.pos;\n            g_Camera.target.x = (item->pos.x + old.x) / 2;\n            g_Camera.target.z = (item->pos.z + old.z) / 2;\n            g_Camera.debuff--;\n        } else {\n            g_Camera.target.x = item->pos.x;\n            g_Camera.target.z = item->pos.z;\n        }\n\n        if (g_Camera.flags == CF_FOLLOW_CENTRE) {\n            const int32_t shift = (bounds->min.z + bounds->max.z) / 2;\n            g_Camera.target.z += (shift * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n            g_Camera.target.x += (shift * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n        }\n\n        g_Camera.target.room_num = item->room_num;\n        if (g_Camera.fixed_camera != fixed_camera) {\n            g_Camera.target.y = target_y;\n            g_Camera.fixed_camera = true;\n            g_Camera.speed = 1;\n        } else {\n            g_Camera.fixed_camera = false;\n            g_Camera.target.y += (target_y - g_Camera.target.y) / 4;\n        }\n\n        const SECTOR *const sector = Room_GetSector(\n            (XYZ_32) { g_Camera.target.x, target_y, g_Camera.target.z },\n            &g_Camera.target.room_num);\n        const int32_t height = Room_GetHeight(sector, g_Camera.target.pos);\n        if (g_Camera.target.y > height) {\n            Camera_SetChunky(false);\n        }\n\n        if (g_Camera.type == CAM_CHASE || g_Camera.flags == CF_CHASE_OBJECT) {\n            M_Chase(item);\n        } else {\n            M_Fixed();\n        }\n    }\n}\n\nstatic const CAMERA_STRATEGY m_Strategy = {\n    .get_chase_speed_func = M_GetChaseSpeed,\n    .get_look_settings_func = M_GetLookSettingsFunc,\n    .clamp_result_func = M_ClampResult,\n    .reset_func = M_Reset,\n    .update_func = M_Update,\n};\n\nREGISTER_CAMERA(CAMERA_MODE_TR1, m_Strategy)\nREGISTER_CAMERA(CAMERA_MODE_TR2, m_Strategy)\n"
  },
  {
    "path": "src/trx/game/camera/cinematic.c",
    "content": "#include <trx/game/camera/cinematic.h>\n\n#include <trx/game/camera.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\nstatic CINE_FRAME *m_CineFrames = nullptr;\nstatic CINE_DATA m_CineData = {};\n\nstatic void M_UpdateCutscene(const XYZ_32 base_pos, const int16_t angle)\n{\n    const CINE_FRAME *const frame = Camera_GetCurrentCineFrame();\n    const int32_t c = Math_Cos(angle);\n    const int32_t s = Math_Sin(angle);\n\n#define SHIFT(prop, axis1, op, axis2)                                          \\\n    (((frame->prop.shift.axis1 * c) op(frame->prop.shift.axis2 * s))           \\\n     >> W2V_SHIFT)\n\n    const XYZ_32 camera_target = {\n        .x = base_pos.x + SHIFT(target, x, +, z),\n        .y = base_pos.y + frame->target.shift.y,\n        .z = base_pos.z + SHIFT(target, z, -, x),\n    };\n\n    const XYZ_32 camera_pos = {\n        .x = base_pos.x + SHIFT(camera, x, +, z),\n        .y = base_pos.y + frame->camera.shift.y,\n        .z = base_pos.z + SHIFT(camera, z, -, x),\n    };\n\n#undef SHIFT\n\n    const int16_t room_num = Room_GetIndexFromPos(camera_pos);\n    if (room_num != NO_ROOM) {\n        g_Camera.pos.room_num = room_num;\n    }\n\n    g_Camera.target.pos = camera_target;\n    g_Camera.pos.pos = camera_pos;\n    g_Camera.roll = frame->roll;\n    g_Camera.shift = 0;\n\n    Viewport_AlterFOV(frame->fov, FOV_MODE_CUTSCENE);\n}\n\nvoid Camera_InitialiseCineFrames(const int32_t num_frames)\n{\n    m_CineData.frame_count = num_frames;\n    m_CineData.frame_idx = 0;\n    m_CineFrames = num_frames == 0\n        ? nullptr\n        : GameBuf_Alloc(num_frames * sizeof(CINE_FRAME), GBUF_CINEMATIC_FRAMES);\n}\n\nCINE_FRAME *Camera_GetCineFrame(const int32_t frame_idx)\n{\n    if (m_CineFrames == nullptr) {\n        return nullptr;\n    }\n    return &m_CineFrames[frame_idx];\n}\n\nCINE_FRAME *Camera_GetCurrentCineFrame(void)\n{\n    return Camera_GetCineFrame(m_CineData.frame_idx);\n}\n\nCINE_DATA *Camera_GetCineData(void)\n{\n    return &m_CineData;\n}\n\nvoid Camera_InvokeCinematic(\n    const ITEM *const item, const int32_t frame_idx, const int16_t extra_y_rot)\n{\n    g_Camera.type = CAM_CINEMATIC;\n    m_CineData.frame_idx = frame_idx;\n    m_CineData.position.pos = item->pos;\n    m_CineData.position.rot = item->rot;\n    m_CineData.position.rot.y += extra_y_rot;\n}\n\nvoid Camera_LoadCutsceneFrame(void)\n{\n    CINE_DATA *const cine_data = Camera_GetCineData();\n    if (cine_data->frame_count == 0) {\n        return;\n    }\n\n    cine_data->frame_idx++;\n    if (cine_data->frame_idx >= cine_data->frame_count) {\n        cine_data->frame_idx = cine_data->frame_count - 1;\n    }\n\n    M_UpdateCutscene(cine_data->position.pos, cine_data->position.rot.y);\n    Camera_UpdateMicPosition();\n}\n\nvoid Camera_UpdateCutscene(void)\n{\n    const CINE_DATA *const cine_data = Camera_GetCineData();\n    if (cine_data->frame_count == 0) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    M_UpdateCutscene(lara_item->pos, g_Camera.target_angle);\n\n    Camera_EnsureEnvironment();\n}\n"
  },
  {
    "path": "src/trx/game/camera/cinematic.h",
    "content": "#pragma once\n\n#include <trx/game/camera/types.h>\n#include <trx/game/items/types.h>\n\nvoid Camera_InitialiseCineFrames(int32_t num_frames);\nCINE_FRAME *Camera_GetCineFrame(int32_t frame_idx);\nCINE_FRAME *Camera_GetCurrentCineFrame(void);\nCINE_DATA *Camera_GetCineData(void);\nvoid Camera_InvokeCinematic(\n    const ITEM *item, int32_t frame_idx, int16_t extra_y_rot);\nvoid Camera_LoadCutsceneFrame(void);\nvoid Camera_UpdateCutscene(void);\n"
  },
  {
    "path": "src/trx/game/camera/common.c",
    "content": "#include <trx/game/camera/common.h>\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n\n#define M_CHASE_ELEVATION (WALL_L * 3 / 2) // = 1536\n\nstatic CAMERA_STRATEGY m_Strategies[CAMERA_MODE_NUMBER_OF] = {};\n\n// Camera speed option ranges from 1-10, so index 0 is unused.\nstatic const double m_ManualCameraMultiplier[11] = {\n    1.0, .5, .625, .75, .875, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0,\n};\n\nstatic bool m_IsChunky = false;\nstatic bool m_IsInitialised = false;\n\nstatic void M_OffsetAdditionalAngle(const int16_t delta)\n{\n    g_Camera.additional_angle += delta;\n}\n\nstatic void M_OffsetAdditionalElevation(const int16_t delta)\n{\n    // Do not allow elevation to overflow.\n    int32_t new_elevation = g_Camera.additional_elevation + delta;\n    CLAMP(new_elevation, INT16_MIN, INT16_MAX);\n    g_Camera.additional_elevation = new_elevation;\n}\n\nstatic void M_OffsetReset(void)\n{\n    g_Camera.additional_angle = 0;\n    g_Camera.additional_elevation = 0;\n}\n\nstatic const CAMERA_STRATEGY *M_GetStrategy(void)\n{\n    return &m_Strategies[g_Config.visuals.camera_mode];\n}\n\nconst CAMERA_LOOK_SETTINGS *Camera_GetLookSettings(const bool on_surface)\n{\n    return M_GetStrategy()->get_look_settings_func(on_surface);\n}\n\nvoid Camera_RegisterStrategy(\n    const CAMERA_MODE mode, const CAMERA_STRATEGY strategy)\n{\n    m_Strategies[mode] = strategy;\n}\n\nbool Camera_IsChunky(void)\n{\n    return m_IsChunky;\n}\n\nvoid Camera_SetChunky(const bool is_chunky)\n{\n    m_IsChunky = is_chunky;\n}\n\nvoid Camera_Initialise(void)\n{\n    m_IsInitialised = false;\n    Matrix_ResetStack();\n    g_Camera.last = NO_CAMERA;\n    g_Camera.underwater = false;\n    Camera_ResetPosition();\n    Camera_Update();\n    m_IsInitialised = true;\n}\n\nvoid Camera_ResetPosition(void)\n{\n    const CAMERA_STRATEGY *const strategy = M_GetStrategy();\n    strategy->reset_func();\n\n    g_Camera.roll = 0;\n    g_Camera.target_distance = CAMERA_DEFAULT_DISTANCE;\n    g_Camera.item = nullptr;\n    g_Camera.speed = 1;\n    g_Camera.flags = CF_NORMAL;\n    g_Camera.bounce = 0;\n    g_Camera.num = NO_CAMERA;\n    g_Camera.fixed_camera = false;\n    g_Camera.additional_angle = 0;\n    g_Camera.additional_elevation = 0;\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (!lara_info->extra_anim) {\n        g_Camera.type = CAM_CHASE;\n    }\n}\n\nvoid Camera_Reset(void)\n{\n    g_Camera.mic_pos.room_num = NO_ROOM;\n    g_Camera.pos.room_num = NO_ROOM;\n}\n\nvoid Camera_ApplyBounce(void)\n{\n    if (g_Camera.bounce > 0) {\n        g_Camera.pos.y += g_Camera.bounce;\n        g_Camera.target.y += g_Camera.bounce;\n        g_Camera.bounce = 0;\n    } else if (g_Camera.bounce < 0) {\n        const XYZ_32 shake = {\n            .x = g_Camera.bounce * (Random_GetControl() - 0x4000) / 0x7FFF,\n            .y = g_Camera.bounce * (Random_GetControl() - 0x4000) / 0x7FFF,\n            .z = g_Camera.bounce * (Random_GetControl() - 0x4000) / 0x7FFF,\n        };\n        g_Camera.pos.x += shake.x;\n        g_Camera.pos.y += shake.y;\n        g_Camera.pos.z += shake.z;\n        g_Camera.target.y += shake.x;\n        g_Camera.target.y += shake.y;\n        g_Camera.target.z += shake.z;\n        g_Camera.bounce += 5;\n    }\n}\n\nvoid Camera_ClampInterpResult(void)\n{\n    if (g_Camera.type == CAM_PHOTO_MODE) {\n        Room_GetSector(\n            (XYZ_32) {\n                g_Camera.interp.result.pos.x,\n                g_Camera.interp.result.pos.y + g_Camera.interp.result.shift,\n                g_Camera.interp.result.pos.z,\n            },\n            &g_Camera.interp.room_num);\n        return;\n    }\n\n    const CAMERA_STRATEGY *const strategy = M_GetStrategy();\n    strategy->clamp_result_func();\n}\n\nvoid Camera_Update(void)\n{\n    if (g_Camera.type == CAM_PHOTO_MODE) {\n        Camera_PhotoMode_Update();\n        Camera_EnsureEnvironment();\n        return;\n    }\n\n    if (g_Camera.type == CAM_CINEMATIC) {\n        Camera_LoadCutsceneFrame();\n        Camera_EnsureEnvironment();\n        return;\n    }\n\n    if (g_Camera.flags != CF_NO_CHUNKY) {\n        Camera_SetChunky(true);\n    }\n\n    const bool fixed_camera = g_Camera.item != nullptr\n        && (g_Camera.type == CAM_FIXED || g_Camera.type == CAM_HEAVY);\n    const ITEM *const item = fixed_camera ? g_Camera.item : Lara_GetItem();\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    int32_t y = item->pos.y;\n    if (fixed_camera) {\n        y += (bounds->min.y + bounds->max.y) / 2;\n    } else {\n        y += bounds->max.y\n            + (((int32_t)(bounds->min.y - bounds->max.y)) * 3 >> 2);\n    }\n\n    const CAMERA_STRATEGY *const strategy = M_GetStrategy();\n    strategy->update_func(item, fixed_camera, y);\n\n    g_Camera.last = g_Camera.num;\n    g_Camera.fixed_camera = fixed_camera;\n\n    switch (g_Camera.type) {\n    case CAM_LOOK:\n    case CAM_CINEMATIC:\n    case CAM_COMBAT:\n    case CAM_FIXED:\n        g_Camera.additional_angle = 0;\n        g_Camera.additional_elevation = 0;\n        break;\n\n    default:\n        break;\n    }\n\n    if (g_Camera.type != CAM_HEAVY || g_Camera.timer == -1) {\n        g_Camera.type = CAM_CHASE;\n        g_Camera.num = NO_CAMERA;\n        g_Camera.last_item = g_Camera.item;\n        g_Camera.item = nullptr;\n        g_Camera.target_angle = g_Camera.additional_angle;\n        g_Camera.target_elevation = g_Camera.additional_elevation;\n        g_Camera.target_distance = M_CHASE_ELEVATION;\n        g_Camera.flags = CF_NORMAL;\n        if (g_Config.visuals.camera_mode != CAMERA_MODE_TR1) {\n            g_Camera.speed = strategy->get_chase_speed_func();\n        }\n    }\n    Camera_SetChunky(false);\n    if (m_IsInitialised) {\n        Camera_EnsureEnvironment();\n    }\n}\n\nvoid Camera_MoveManual(void)\n{\n    if (g_Input.camera_reset) {\n        M_OffsetReset();\n    }\n\n    if (!g_Config.gameplay.enable_manual_camera) {\n        return;\n    }\n\n    const int16_t camera_delta = (const int32_t)(DEG_90 / LOGIC_FPS)\n        * (double)m_ManualCameraMultiplier[g_Config.gameplay.camera_speed];\n\n    if (g_Input.camera_left) {\n        M_OffsetAdditionalAngle(camera_delta);\n    } else if (g_Input.camera_right) {\n        M_OffsetAdditionalAngle(-camera_delta);\n    }\n    if (g_Input.camera_forward) {\n        M_OffsetAdditionalElevation(-camera_delta);\n    } else if (g_Input.camera_back) {\n        M_OffsetAdditionalElevation(camera_delta);\n    }\n}\n\nvoid Camera_Apply(void)\n{\n    Matrix_LookAt(\n        g_Camera.interp.result.pos.x,\n        g_Camera.interp.result.pos.y + g_Camera.interp.result.shift,\n        g_Camera.interp.result.pos.z, g_Camera.interp.result.target.x,\n        g_Camera.interp.result.target.y, g_Camera.interp.result.target.z,\n        g_Camera.roll);\n}\n"
  },
  {
    "path": "src/trx/game/camera/common.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n#include <trx/game/camera/types.h>\n\nvoid Camera_Update(void);\nvoid Camera_MoveManual(void);\nvoid Camera_Apply(void);\nbool Camera_IsChunky(void);\nvoid Camera_SetChunky(bool is_chunky);\nvoid Camera_Initialise(void);\nvoid Camera_ResetPosition(void);\nvoid Camera_Reset(void);\nvoid Camera_ApplyBounce(void);\nvoid Camera_ClampInterpResult(void);\nconst CAMERA_LOOK_SETTINGS *Camera_GetLookSettings(bool on_surface);\n\nbool Camera_LOSCheck(GAME_VECTOR *start, GAME_VECTOR *target, int32_t shift);\nbool Camera_Collide(GAME_VECTOR *ideal, int32_t shift, bool y_first);\n\nvoid Camera_RegisterStrategy(CAMERA_MODE mode, CAMERA_STRATEGY strategy);\n\n#define REGISTER_CAMERA(mode, strategy)                                        \\\n    __attribute__((constructor)) static void M_RegisterCamera##mode(void)      \\\n    {                                                                          \\\n        Camera_RegisterStrategy(mode, strategy);                               \\\n    }\n"
  },
  {
    "path": "src/trx/game/camera/const.h",
    "content": "#pragma once\n\n#include <trx/game/const.h>\n\n// clang-format off\n#define NO_CAMERA                (-1)\n#define CAMERA_DEFAULT_DISTANCE  (WALL_L * 3 / 2) // = 1536\n#define CAMERA_MAX_HEAD_ROTATION (50 * DEG_1) // = 9100\n#define CAMERA_MIN_HEAD_ROTATION (-CAMERA_MAX_HEAD_ROTATION) // = -9100\n#define CAMERA_MAX_HEAD_TILT     (85 * DEG_1) // = 15470\n#define CAMERA_MIN_HEAD_TILT     (-CAMERA_MAX_HEAD_TILT) // = -15470\n#define CAMERA_HEAD_TURN         (4 * DEG_1) // = 728\n#define CAMERA_COMBAT_SPEED      8\n#define CAMERA_LOOK_SPEED        4\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/camera/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    CAM_CHASE = 0,\n    CAM_FIXED = 1,\n    CAM_LOOK = 2,\n    CAM_COMBAT = 3,\n    CAM_CINEMATIC = 4,\n    CAM_HEAVY = 5,\n    CAM_PHOTO_MODE = 6,\n} CAMERA_TYPE;\n\ntypedef enum {\n    CF_NORMAL = 0,\n    CF_FOLLOW_CENTRE = 1,\n    CF_NO_CHUNKY = 2,\n    CF_CHASE_OBJECT = 3,\n} CAMERA_FLAGS;\n"
  },
  {
    "path": "src/trx/game/camera/environment.c",
    "content": "#include <trx/game/camera/environment.h>\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/output/vars.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\ntypedef enum {\n    TARGET_UNKNOWN,\n    TARGET_INVALID,\n    TARGET_VALID,\n} M_TARGET_STATUS;\n\nstatic void M_AdjustMusicVolume(const bool is_underwater)\n{\n    if (!Game_IsPlaying()) {\n        return;\n    }\n    const bool is_ambient =\n        Music_GetCurrentPlayingTrack() == Music_GetCurrentLoopedTrack();\n    const bool is_cutscene = GF_GetCurrentLevel()->type == GFL_CUTSCENE;\n    const double base_volume = is_cutscene ? g_Config.audio.cutscene_volume\n        : is_ambient                       ? g_Config.audio.ambient_volume\n                                           : g_Config.audio.music_volume;\n    const double multiplier = !is_underwater || is_cutscene ? 1.0\n        : is_ambient ? g_Config.audio.underwater_ambient_volume\n                     : g_Config.audio.underwater_music_volume;\n    Music_SetVolume(base_volume * multiplier);\n}\n\nstatic inline M_TARGET_STATUS M_HandleCameraTrigger(\n    const TRIGGER_CMD *const cmd)\n{\n    const TRIGGER_CAMERA_DATA *const cam_data =\n        (TRIGGER_CAMERA_DATA *)cmd->parameter;\n    if (cam_data->camera_num != g_Camera.last) {\n        return TARGET_INVALID;\n    }\n\n    g_Camera.num = cam_data->camera_num;\n\n    const bool is_look_or_combat =\n        g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT;\n    const bool is_locked = Camera_IsLocked(g_Camera.num);\n    if (g_Camera.timer < 0 || (is_look_or_combat && !is_locked)) {\n        g_Camera.timer = -1;\n        return TARGET_INVALID;\n    }\n\n    g_Camera.type = CAM_FIXED;\n    if (g_Config.visuals.enable_glide_cameras && cam_data->glide != 0) {\n        g_Camera.speed = cam_data->glide + 1;\n    }\n\n    return TARGET_VALID;\n}\n\nstatic inline void M_HandleTargetTrigger(const TRIGGER_CMD *const cmd)\n{\n    const bool is_look_or_combat =\n        g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT;\n    const bool is_locked = Camera_IsLocked(g_Camera.num);\n    if (!is_look_or_combat || is_locked) {\n        g_Camera.item = Item_Get((int16_t)(intptr_t)cmd->parameter);\n    }\n}\n\nstatic inline void M_ValidateTriggerTarget(const M_TARGET_STATUS status)\n{\n    if (g_Camera.item == nullptr) {\n        return;\n    }\n\n    const bool is_new_item = g_Camera.item != g_Camera.last_item;\n    const bool item_was_looked_at = g_Camera.item->looked_at;\n\n    const bool should_clear = (status == TARGET_INVALID)\n        || (status == TARGET_UNKNOWN && item_was_looked_at && is_new_item);\n    if (should_clear) {\n        g_Camera.item = nullptr;\n    }\n}\n\nvoid Camera_RefreshFromTrigger(const TRIGGER *const trigger)\n{\n    M_TARGET_STATUS status = TARGET_UNKNOWN;\n    for (const TRIGGER_CMD *cmd = trigger->command; cmd != nullptr;\n         cmd = cmd->next_cmd) {\n        if (cmd->type == TO_CAMERA) {\n            status = M_HandleCameraTrigger(cmd);\n        } else if (cmd->type == TO_TARGET) {\n            M_HandleTargetTrigger(cmd);\n        }\n    }\n\n    M_ValidateTriggerTarget(status);\n\n    if (g_Config.visuals.camera_mode != CAMERA_MODE_TR1\n        && status != TARGET_UNKNOWN && g_Camera.num == -1\n        && g_Camera.timer > 0) {\n        g_Camera.timer = -1;\n    }\n}\n\nvoid Camera_EnsureEnvironment(void)\n{\n    if (g_Camera.mic_pos.room_num != NO_ROOM) {\n        const ROOM *const room = Room_Get(g_Camera.mic_pos.room_num);\n        if (room->flags.underwater) {\n            M_AdjustMusicVolume(true);\n            Sound_Effect(SFX_UNDERWATER, nullptr, SPM_ALWAYS);\n        } else {\n            M_AdjustMusicVolume(false);\n            Sound_StopEffect(SFX_UNDERWATER);\n        }\n    }\n\n    if (g_Camera.pos.room_num != NO_ROOM) {\n        const ROOM *const room = Room_Get(g_Camera.pos.room_num);\n        g_Camera.underwater = room->flags.underwater;\n    }\n\n    uint8_t reverb_type = 0;\n    if (g_Camera.mic_pos.room_num != NO_ROOM) {\n        const ROOM *const room = Room_Get(g_Camera.mic_pos.room_num);\n        reverb_type = room->reverb_info;\n    }\n    Sound_SetReverbType(reverb_type);\n}\n\nvoid Camera_UpdateMicPosition(void)\n{\n    if (g_Config.audio.enable_lara_mic) {\n        const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n        const ITEM *const lara_item = Lara_GetItem();\n        g_Camera.actual_angle =\n            lara_info->torso_rot.y + lara_info->head_rot.y + lara_item->rot.y;\n        g_Camera.mic_pos.room_num = lara_item->room_num;\n        XYZ_32 pos = { 0, 16, 0 };\n        if (lara_info->water_status == LWS_SURFACE) {\n            pos.y = -36;\n        }\n        if (lara_info->water_surface_dist != -NO_HEIGHT\n            && Lara_GetMeshPos(LM_HEAD, &pos)) {\n            g_Camera.mic_pos.pos = pos;\n            Room_GetSector(pos, &g_Camera.mic_pos.room_num);\n        } else {\n            g_Camera.mic_pos.pos = lara_item->pos;\n        }\n    } else {\n        g_Camera.actual_angle = Math_Atan(\n            g_Camera.target.z - g_Camera.pos.z,\n            g_Camera.target.x - g_Camera.pos.x);\n        g_Camera.mic_pos.pos.x = g_Camera.pos.x\n            + ((g_PhdPersp * Math_Sin(g_Camera.actual_angle)) >> W2V_SHIFT);\n        g_Camera.mic_pos.pos.z = g_Camera.pos.z\n            + ((g_PhdPersp * Math_Cos(g_Camera.actual_angle)) >> W2V_SHIFT);\n        g_Camera.mic_pos.pos.y = g_Camera.pos.y;\n        g_Camera.mic_pos.room_num = g_Camera.pos.room_num;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/camera/environment.h",
    "content": "#pragma once\n\n#include <trx/game/rooms/types.h>\n\nvoid Camera_RefreshFromTrigger(const TRIGGER *trigger);\nvoid Camera_EnsureEnvironment(void);\nvoid Camera_UpdateMicPosition(void);\n"
  },
  {
    "path": "src/trx/game/camera/fixed.c",
    "content": "#include <trx/game/camera/fixed.h>\n\n#include <trx/game/camera/const.h>\n#include <trx/game/game_buf.h>\n\n#define M_LOCKED_CAMERA 1\n\nstatic int32_t m_FixedObjectCount = 0;\nstatic OBJECT_VECTOR *m_FixedObjects = nullptr;\n\nvoid Camera_InitialiseFixedObjects(const int32_t num_objects)\n{\n    m_FixedObjectCount = num_objects + 1;\n    m_FixedObjects =\n        GameBuf_Alloc(m_FixedObjectCount * sizeof(OBJECT_VECTOR), GBUF_CAMERAS);\n}\n\nint32_t Camera_GetFixedObjectCount(void)\n{\n    return m_FixedObjectCount - 1;\n}\n\nint32_t Camera_GetDynamicFixedObjectIdx(void)\n{\n    return m_FixedObjectCount - 1;\n}\n\nvoid Camera_UpdateDynamicFixedObject(const XYZ_32 pos, const int16_t room_num)\n{\n    const int32_t idx = Camera_GetDynamicFixedObjectIdx();\n    OBJECT_VECTOR *const camera = Camera_GetFixedObject(idx);\n    camera->pos = pos;\n    camera->data = room_num;\n}\n\nOBJECT_VECTOR *Camera_GetFixedObject(const int32_t object_idx)\n{\n    if (m_FixedObjects == nullptr) {\n        return nullptr;\n    }\n    return &m_FixedObjects[object_idx];\n}\n\nbool Camera_IsLocked(const int32_t camera_num)\n{\n    if (camera_num == NO_CAMERA) {\n        return false;\n    }\n\n    const OBJECT_VECTOR *const fixed_camera = Camera_GetFixedObject(camera_num);\n    return (fixed_camera->flags & M_LOCKED_CAMERA) != 0;\n}\n"
  },
  {
    "path": "src/trx/game/camera/fixed.h",
    "content": "#pragma once\n\n#include <trx/game/types.h>\n\nvoid Camera_InitialiseFixedObjects(int32_t num_objects);\nint32_t Camera_GetFixedObjectCount(void);\nint32_t Camera_GetDynamicFixedObjectIdx(void);\nvoid Camera_UpdateDynamicFixedObject(XYZ_32 pos, int16_t room_num);\nOBJECT_VECTOR *Camera_GetFixedObject(int32_t object_idx);\nbool Camera_IsLocked(int32_t camera_num);\n"
  },
  {
    "path": "src/trx/game/camera/los_camera.c",
    "content": "#include <trx/core/math.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n\n// clang-format off\n#define M_LOS_STEPS         8\n#define M_MAX_SNAPS         8\n#define M_SNAP_DELTA        (STEP_L * 3) // = 768\n#define M_DEFAULT_ELEVATION (-10 * DEG_1) // = -1820\n#define M_MAX_ELEVATION     (85 * DEG_1) // = 15470\n#define M_CHASE_SHIFT       (STEP_L * 3 / 2) // = 384\n#define M_LOOK_SHIFT        (STEP_L * 7 / 8) // = 224\n#define M_CHASE_SPEED       10\n#define M_MAX_LOOK_TILT     (55 * DEG_1) // = 10010\n#define M_MIN_LOOK_TILT     (-75 * DEG_1) // = -13650\n#define M_MAX_LOOK_ROTATION (80 * DEG_1) // = 14560\n#define M_MIN_LOOK_ROTATION -M_MAX_LOOK_ROTATION // = -14560\n// clang-format on\n\ntypedef struct {\n    struct {\n        int16_t current_anim_state;\n        int16_t goal_anim_state;\n        XYZ_32 pos;\n        XYZ_16 rot;\n        XYZ_16 head_rot;\n        XYZ_16 torso_rot;\n    } lara;\n    CAMERA_TYPE cam_type;\n    int16_t additional_angle;\n    int16_t additional_elevation;\n} M_STATE;\n\ntypedef struct {\n    GAME_VECTOR pos;\n    GAME_VECTOR target;\n} M_IDEAL;\n\nstatic M_STATE m_LastState = {};\nstatic M_IDEAL m_LastIdeal = {};\nstatic M_IDEAL m_LastLookIdeal = {};\nstatic int32_t m_Snaps = 0;\n\nstatic CAMERA_LOOK_SETTINGS m_LookSettings = {\n    // clang-format off\n    .head_turn         = +2 * DEG_1,\n    .max_head_rotation = +44 * DEG_1,\n    .min_head_rotation = -44 * DEG_1,\n    .max_head_tilt     = +30 * DEG_1,\n    .min_head_tilt     = -35 * DEG_1,\n    .torso_head_rot_y  = 1.0f,\n    .torso_head_rot_x  = 1.0f,\n    // clang-format on\n};\n\nstatic int16_t M_GetChaseSpeed(void)\n{\n    return M_CHASE_SPEED;\n}\n\nstatic const CAMERA_LOOK_SETTINGS *M_GetLookSettingsFunc(const bool on_surface)\n{\n    return &m_LookSettings;\n}\n\nstatic bool M_LOS(\n    GAME_VECTOR *const start, GAME_VECTOR *const target, const int32_t shift)\n{\n    const XYZ_32 delta = {\n        .x = (target->x - start->x) >> 3,\n        .y = (target->y - start->y) >> 3,\n        .z = (target->z - start->z) >> 3,\n    };\n\n    XYZ_32 pos = start->pos;\n    int16_t room_num = start->room_num;\n    bool valid_space = false;\n    bool clipped = false;\n\n    int32_t i = 0;\n    for (; i < M_LOS_STEPS; i++) {\n        int16_t next_room_num = room_num;\n        const SECTOR *const sector = Room_GetSector(pos, &next_room_num);\n\n        if (Room_Get(next_room_num)->flags.swamp) {\n            clipped = true;\n            break;\n        }\n\n        const int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n        const int32_t ceiling = Room_GetCeilingEx(sector, pos, true);\n\n        if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height) {\n            if (!valid_space) {\n                pos.x += delta.x;\n                pos.y += delta.y;\n                pos.z += delta.z;\n                continue;\n            }\n\n            clipped = true;\n            break;\n        }\n\n        if (pos.y > height) {\n            const int32_t height_diff = pos.y - height;\n            if (height_diff < shift) {\n                pos.y = height;\n            } else {\n                clipped = true;\n                break;\n            }\n        }\n\n        if (pos.y < ceiling) {\n            const int32_t ceiling_diff = ceiling - pos.y;\n            if (ceiling_diff < shift) {\n                pos.y = ceiling;\n            } else {\n                clipped = true;\n                break;\n            }\n        }\n\n        valid_space = true;\n        room_num = next_room_num;\n        pos.x += delta.x;\n        pos.y += delta.y;\n        pos.z += delta.z;\n    }\n\n    if (i != 0) {\n        pos.x -= delta.x;\n        pos.y -= delta.y;\n        pos.z -= delta.z;\n    }\n\n    Room_GetSector(pos, &room_num);\n    target->pos = pos;\n    target->room_num = room_num;\n\n    return !clipped;\n}\n\nstatic inline void M_ClampY(int16_t room_num, XYZ_32 *const pos)\n{\n    const SECTOR *const sector = Room_GetSector(*pos, &room_num);\n    const int32_t height = Room_GetHeightEx(sector, *pos, true, NO_ITEM);\n    const int32_t ceiling = Room_GetCeilingEx(sector, *pos, true);\n\n    if (ceiling < height && ceiling != NO_HEIGHT && height != NO_HEIGHT) {\n        if (ceiling > pos->y - 255 && height < pos->y + 255) {\n            pos->y = (ceiling + height) >> 1;\n        } else if (height < pos->y + 255) {\n            pos->y = height - 255;\n        } else if (ceiling > pos->y - 255) {\n            pos->y = ceiling + 255;\n        }\n    }\n}\n\nstatic bool M_Collide(\n    GAME_VECTOR *const ideal, const int32_t shift, const bool y_first)\n{\n    XYZ_32 pos = ideal->pos;\n    if (y_first) {\n        M_ClampY(ideal->room_num, &pos);\n    }\n\n#define L_OUT_OF_BOUNDS                                                        \\\n    (height < pos.y || height == NO_HEIGHT || ceiling == NO_HEIGHT             \\\n     || ceiling >= height || pos.y < ceiling)\n\n    // -X clamp\n    int16_t room_num = ideal->room_num;\n    XYZ_32 sample_pos = { .x = pos.x - shift, .y = pos.y, .z = pos.z };\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n    int32_t height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM);\n    int32_t ceiling = Room_GetCeilingEx(sector, sample_pos, true);\n    if (L_OUT_OF_BOUNDS) {\n        pos.x = ROUND_TO_SECTOR(pos.x) + shift;\n    }\n\n    // -Z clamp\n    room_num = ideal->room_num;\n    sample_pos = (XYZ_32) { .x = pos.x, .y = pos.y, .z = pos.z - shift };\n    sector = Room_GetSector(sample_pos, &room_num);\n    height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, sample_pos, true);\n    if (L_OUT_OF_BOUNDS) {\n        pos.z = ROUND_TO_SECTOR(pos.z) + shift;\n    }\n\n    // +X clamp\n    room_num = ideal->room_num;\n    sample_pos = (XYZ_32) { .x = pos.x + shift, .y = pos.y, .z = pos.z };\n    sector = Room_GetSector(sample_pos, &room_num);\n    height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, sample_pos, true);\n    if (L_OUT_OF_BOUNDS) {\n        pos.x = ROUND_TO_SECTOR_END(pos.x) - shift;\n    }\n\n    // +Z clamp\n    room_num = ideal->room_num;\n    sample_pos = (XYZ_32) { .x = pos.x, .y = pos.y, .z = pos.z + shift };\n    sector = Room_GetSector(sample_pos, &room_num);\n    height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, sample_pos, true);\n    if (L_OUT_OF_BOUNDS) {\n        pos.z = ROUND_TO_SECTOR_END(pos.z) - shift;\n    }\n\n    if (!y_first) {\n        M_ClampY(ideal->room_num, &pos);\n    }\n\n    room_num = ideal->room_num;\n    sector = Room_GetSector(pos, &room_num);\n    height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, pos, true);\n    if (L_OUT_OF_BOUNDS) {\n        return true;\n    }\n\n#undef L_OUT_OF_BOUNDS\n\n    Room_GetSector(pos, &ideal->room_num);\n    ideal->pos = pos;\n    return false;\n}\n\nstatic bool M_UpdateLaraState(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    bool same_lara_state =\n        m_LastState.lara.current_anim_state == lara_item->current_anim_state\n        && m_LastState.lara.goal_anim_state == lara_item->goal_anim_state\n        && XYZ_16_AreEquivalent(m_LastState.lara.head_rot, lara->head_rot)\n        && XYZ_32_AreEquivalent(m_LastState.lara.pos, lara_item->pos);\n    bool same_camera_state = m_LastState.cam_type == g_Camera.type;\n    if (g_Camera.type != CAM_LOOK) {\n        same_lara_state &=\n            XYZ_16_AreEquivalent(m_LastState.lara.rot, lara_item->rot)\n            && XYZ_16_AreEquivalent(\n                m_LastState.lara.torso_rot, lara->torso_rot);\n        same_camera_state &=\n            m_LastState.additional_angle == g_Camera.additional_angle\n            && m_LastState.additional_elevation\n                == g_Camera.additional_elevation;\n    }\n    if (same_lara_state && same_camera_state) {\n        return false;\n    }\n\n    m_LastState.lara.current_anim_state = lara_item->current_anim_state;\n    m_LastState.lara.goal_anim_state = lara_item->goal_anim_state;\n    m_LastState.lara.head_rot = lara->head_rot;\n    m_LastState.lara.pos = lara_item->pos;\n    if (g_Camera.type != CAM_LOOK) {\n        m_LastState.lara.rot = lara_item->rot;\n        m_LastState.lara.torso_rot = lara->torso_rot;\n        m_LastState.additional_angle = g_Camera.additional_angle;\n        m_LastState.additional_elevation = g_Camera.additional_elevation;\n    }\n\n    return true;\n}\n\nstatic void M_Move(GAME_VECTOR *const ideal, const int32_t speed)\n{\n    if (M_UpdateLaraState()) {\n        m_LastIdeal.pos = *ideal;\n    } else {\n        *ideal = m_LastIdeal.pos;\n    }\n\n    g_Camera.pos.x += (ideal->x - g_Camera.pos.x) / speed;\n    g_Camera.pos.y += (ideal->y - g_Camera.pos.y) / speed;\n    g_Camera.pos.z += (ideal->z - g_Camera.pos.z) / speed;\n    g_Camera.pos.room_num = ideal->room_num;\n\n    Camera_ApplyBounce();\n\n    XYZ_32 pos = g_Camera.pos.pos;\n    int16_t room_num = g_Camera.pos.room_num;\n    const XYZ_32 sample_pos = { pos.x, pos.y + STEP_L, pos.z };\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n\n    const ROOM *const room = Room_Get(room_num);\n    if (room->flags.swamp) {\n        pos.y = room->max_ceiling - STEP_L;\n        Room_GetSector(pos, &g_Camera.pos.room_num);\n    }\n\n    sector = Room_GetSector(pos, &room_num);\n    int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    int32_t ceiling = Room_GetCeilingEx(sector, pos, true);\n\n    if (pos.y < ceiling || pos.y > height) {\n        M_LOS(&g_Camera.target, &g_Camera.pos, 0);\n        const XYZ_32 delta = {\n            .x = ABS(g_Camera.pos.x - ideal->x),\n            .y = ABS(g_Camera.pos.y - ideal->y),\n            .z = ABS(g_Camera.pos.z - ideal->z),\n        };\n\n        if (delta.x < M_SNAP_DELTA && delta.y < M_SNAP_DELTA\n            && delta.z < M_SNAP_DELTA) {\n            GAME_VECTOR start = *ideal;\n            GAME_VECTOR target = g_Camera.pos;\n\n            if (!M_LOS(&start, &target, 0)) {\n                m_Snaps++;\n                if (m_Snaps >= M_MAX_SNAPS) {\n                    g_Camera.pos = *ideal;\n                    m_Snaps = 0;\n                }\n            }\n        }\n    }\n\n    pos = g_Camera.pos.pos;\n    room_num = g_Camera.pos.room_num;\n    sector = Room_GetSector(pos, &room_num);\n    height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, pos, true);\n\n    if (pos.y - 255 < ceiling && pos.y + 255 > height && ceiling < height\n        && ceiling != NO_HEIGHT && height != NO_HEIGHT) {\n        g_Camera.pos.y = (ceiling + height) >> 1;\n    } else if (\n        pos.y + 255 > height && ceiling < height && ceiling != NO_HEIGHT\n        && height != NO_HEIGHT) {\n        g_Camera.pos.y = height - 255;\n    } else if (\n        pos.y - 255 < ceiling && ceiling < height && ceiling != NO_HEIGHT\n        && height != NO_HEIGHT) {\n        g_Camera.pos.y = ceiling + 255;\n    } else if (\n        ceiling >= height || height == NO_HEIGHT || ceiling == NO_HEIGHT) {\n        g_Camera.pos = *ideal;\n    }\n\n    Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num);\n    m_LastState.cam_type = g_Camera.type;\n\n    Camera_UpdateMicPosition();\n}\n\nstatic GAME_VECTOR M_GetIdeal(\n    const int32_t distance, const int16_t target_rot_y)\n{\n    int32_t farthest = 0x7FFFFFFF;\n    int32_t farthest_num = 0;\n    GAME_VECTOR temp[2] = {};\n    GAME_VECTOR ideals[5] = {};\n\n    for (int32_t i = 0; i < 5; i++) {\n        ideals[i].y =\n            ((Math_Sin(g_Camera.target_elevation) * g_Camera.target_distance)\n             >> W2V_SHIFT)\n            + g_Camera.target.y;\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int16_t angle = i > 0 ? ((i - 1) << W2V_SHIFT)\n                                    : (g_Camera.target_angle + target_rot_y);\n        ideals[i].x =\n            g_Camera.target.x - ((distance * Math_Sin(angle)) >> W2V_SHIFT);\n        ideals[i].z =\n            g_Camera.target.z - ((distance * Math_Cos(angle)) >> W2V_SHIFT);\n\n        ideals[i].room_num = g_Camera.target.room_num;\n\n        if (M_LOS(&g_Camera.target, &ideals[i], 200)) {\n            temp[0] = ideals[i];\n            temp[1] = g_Camera.pos;\n\n            if (i == 0 || M_LOS(&temp[0], &temp[1], 0)) {\n                if (i == 0) {\n                    farthest_num = 0;\n                    break;\n                }\n\n                const int32_t dx = SQUARE(g_Camera.pos.x - ideals[i].x);\n                const int32_t dz = SQUARE(g_Camera.pos.z - ideals[i].z) + dx;\n                if (dz < farthest) {\n                    farthest = dz;\n                    farthest_num = i;\n                }\n            }\n        } else if (i == 0) {\n            temp[0] = ideals[i];\n            temp[1] = g_Camera.pos;\n\n            const int32_t dx = SQUARE(g_Camera.target.x - ideals[i].x);\n            const int32_t dz = SQUARE(g_Camera.target.z - ideals[i].z) + dx;\n            if (dz > 0x90000) {\n                farthest_num = 0;\n                break;\n            }\n        }\n    }\n\n    return ideals[farthest_num];\n}\n\nstatic void M_Chase(const ITEM *const item)\n{\n    if (g_Camera.target_elevation == 0) {\n        g_Camera.target_elevation = M_DEFAULT_ELEVATION;\n    }\n    g_Camera.target_elevation += item->rot.x;\n    CLAMP(g_Camera.target_elevation, -M_MAX_ELEVATION, M_MAX_ELEVATION);\n\n    const int32_t distance =\n        (g_Camera.target_distance * Math_Cos(g_Camera.target_elevation))\n        >> W2V_SHIFT;\n\n    int16_t room_num = g_Camera.target.room_num;\n    const SECTOR *sector = Room_GetSector(\n        (XYZ_32) {\n            g_Camera.target.x,\n            g_Camera.target.y + STEP_L,\n            g_Camera.target.z,\n        },\n        &room_num);\n\n    const ROOM *const room = Room_Get(room_num);\n    if (room->flags.swamp) {\n        g_Camera.target.y = room->max_ceiling - STEP_L;\n    }\n\n    XYZ_32 pos = g_Camera.target.pos;\n    sector = Room_GetSector(pos, &g_Camera.target.room_num);\n    int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    int32_t ceiling = Room_GetCeilingEx(sector, pos, true);\n\n    if (ceiling + 16 > height - 16 && height != NO_HEIGHT\n        && ceiling != NO_HEIGHT) {\n        g_Camera.target.y = (height + ceiling) >> 1;\n        g_Camera.target_elevation = 0;\n    } else if (pos.y > height - 16 && height != NO_HEIGHT) {\n        g_Camera.target.y = height - 16;\n        g_Camera.target_elevation = 0;\n    } else if (pos.y < ceiling + 16 && ceiling != NO_HEIGHT) {\n        g_Camera.target.y = ceiling + 16;\n        g_Camera.target_elevation = 0;\n    }\n\n    sector = Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num);\n    pos = g_Camera.target.pos;\n    room_num = g_Camera.target.room_num;\n    sector = Room_GetSector(g_Camera.target.pos, &room_num);\n    height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, pos, true);\n\n    if (pos.y < ceiling || pos.y > height || ceiling >= height\n        || height == NO_HEIGHT || ceiling == NO_HEIGHT) {\n        g_Camera.target = m_LastIdeal.target;\n    }\n\n    GAME_VECTOR ideal = M_GetIdeal(distance, item->rot.y);\n    M_Collide(&ideal, M_CHASE_SHIFT, true);\n\n    if (m_LastState.cam_type == CAM_FIXED) {\n        g_Camera.speed = 1;\n    }\n\n    M_Move(&ideal, g_Camera.speed);\n}\n\nstatic void M_Combat(const ITEM *const item)\n{\n    g_Camera.target.x = item->pos.x;\n    g_Camera.target.z = item->pos.z;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->target != nullptr) {\n        g_Camera.target_angle = lara->target_angles[0] + item->rot.y;\n        g_Camera.target_elevation = lara->target_angles[1] + item->rot.x;\n    } else {\n        g_Camera.target_angle =\n            lara->torso_rot.y + lara->head_rot.y + item->rot.y;\n        g_Camera.target_elevation =\n            lara->head_rot.x + item->rot.x + lara->torso_rot.x - 2730;\n    }\n\n    int16_t room_num = g_Camera.target.room_num;\n    const SECTOR *sector = Room_GetSector(\n        (XYZ_32) {\n            g_Camera.target.x,\n            g_Camera.target.y + STEP_L,\n            g_Camera.target.z,\n        },\n        &room_num);\n\n    const ROOM *const room = Room_Get(room_num);\n    if (room->flags.swamp) {\n        g_Camera.target.y = room->max_ceiling - STEP_L;\n    }\n\n    XYZ_32 pos = g_Camera.target.pos;\n    sector = Room_GetSector(pos, &g_Camera.target.room_num);\n    int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    int32_t ceiling = Room_GetCeilingEx(sector, pos, true);\n\n    if (ceiling + 64 > height - 64 && height != NO_HEIGHT\n        && ceiling != NO_HEIGHT) {\n        g_Camera.target.y = (ceiling + height) >> 1;\n        g_Camera.target_elevation = 0;\n    } else if (pos.y > height - 64 && height != NO_HEIGHT) {\n        g_Camera.target.y = height - 64;\n        g_Camera.target_elevation = 0;\n    } else if (pos.y < ceiling + 64 && ceiling != NO_HEIGHT) {\n        g_Camera.target.y = ceiling + 64;\n        g_Camera.target_elevation = 0;\n    }\n\n    pos = g_Camera.target.pos;\n    Room_GetSector(pos, &g_Camera.target.room_num);\n    room_num = g_Camera.target.room_num;\n    sector = Room_GetSector(pos, &room_num);\n    height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    ceiling = Room_GetCeilingEx(sector, pos, true);\n\n    if (pos.y < ceiling || pos.y > height || ceiling >= height\n        || height == NO_HEIGHT || ceiling == NO_HEIGHT) {\n        g_Camera.target = m_LastIdeal.target;\n    }\n\n    g_Camera.target_distance = CAMERA_DEFAULT_DISTANCE;\n    const int32_t distance =\n        g_Camera.target_distance * Math_Cos(g_Camera.target_elevation)\n        >> W2V_SHIFT;\n\n    GAME_VECTOR ideal = M_GetIdeal(distance, 0);\n    M_Collide(&ideal, M_CHASE_SHIFT, true);\n\n    if (m_LastState.cam_type == CAM_FIXED) {\n        g_Camera.speed = 1;\n    }\n\n    M_Move(&ideal, g_Camera.speed);\n}\n\nstatic void M_Fixed(void)\n{\n    const OBJECT_VECTOR *const fixed = Camera_GetFixedObject(g_Camera.num);\n    GAME_VECTOR target = {\n        .x = fixed->x,\n        .y = fixed->y,\n        .z = fixed->z,\n        .room_num = fixed->data,\n    };\n\n    g_Camera.fixed_camera = true;\n    M_Move(&target, g_Camera.speed);\n\n    if (g_Camera.timer != 0) {\n        g_Camera.timer--;\n        if (g_Camera.timer == 0) {\n            g_Camera.timer = -1;\n        }\n    }\n}\n\nstatic XYZ_32 M_GetHeadPos(const int32_t x, const int32_t y, const int32_t z)\n{\n    XYZ_32 pos = {\n        .x = x,\n        .y = y,\n        .z = z,\n    };\n    if (!Lara_GetMeshPos(LM_HEAD, &pos)) {\n        // If look is held while loading a level, Lara won't have been drawn yet\n        // so ensure a valid fallback position is used.\n        Collide_GetJointAbsPosition(Lara_GetItem(), &pos, LM_HEAD);\n    }\n    return pos;\n}\n\nstatic void M_Look(const ITEM *const item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    XYZ_16 head_rot = lara->head_rot;\n    XYZ_16 torso_rot = lara->torso_rot;\n\n    lara->torso_rot.x = 0;\n    lara->torso_rot.y = 0;\n    lara->head_rot.x <<= 1;\n    lara->head_rot.y <<= 1;\n\n    CLAMP(lara->head_rot.x, M_MIN_LOOK_TILT, M_MAX_LOOK_TILT);\n    CLAMP(lara->head_rot.y, M_MIN_LOOK_ROTATION, M_MAX_LOOK_ROTATION);\n\n    // Get head-relative points in mesh space (faithful), then project the\n    // camera's ray onto the head->forward axis to remove breathing-induced\n    // roll/tilt wobble without moving the ray off Lara's head.\n    const XYZ_32 head_pos = M_GetHeadPos(0, 0, 0);\n    XYZ_32 pos_1 = M_GetHeadPos(0, 16, 64);\n\n    int16_t room_num = lara_item->room_num;\n    const XYZ_32 sample_pos = { pos_1.x, pos_1.y + STEP_L, pos_1.z };\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n\n    const ROOM *room = Room_Get(room_num);\n    if (room->flags.swamp) {\n        pos_1.y = room->max_ceiling - STEP_L;\n    }\n\n    sector = Room_GetSector(pos_1, &room_num);\n    int32_t height = Room_GetHeight(sector, pos_1);\n    int32_t ceiling = Room_GetCeiling(sector, pos_1);\n\n    if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height\n        || pos_1.y > height || pos_1.y < ceiling) {\n        pos_1 = M_GetHeadPos(0, 16, 0);\n        room_num = lara_item->room_num;\n        sector = Room_GetSector(\n            (XYZ_32) { pos_1.x, pos_1.y + STEP_L, pos_1.z }, &room_num);\n\n        room = Room_Get(room_num);\n        if (room->flags.swamp) {\n            pos_1.y = room->max_ceiling - STEP_L;\n        }\n\n        sector = Room_GetSector(pos_1, &room_num);\n        height = Room_GetHeight(sector, pos_1);\n        ceiling = Room_GetCeiling(sector, pos_1);\n\n        if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height\n            || pos_1.y > height || pos_1.y < ceiling) {\n            pos_1 = M_GetHeadPos(0, 16, -64);\n        }\n    }\n\n    XYZ_32 pos_2 = M_GetHeadPos(0, 0, -1024);\n    XYZ_32 pos_3 = M_GetHeadPos(0, 0, 2048);\n\n    // Constrain the camera ray to pass through Lara's head (OG behavior), but\n    // remove idle-breathing wobble by projecting onto a stable forward axis\n    // derived from yaw + pitch (no roll/tilt from torso animation).\n    const int16_t yaw = lara_item->rot.y + lara->head_rot.y;\n    const int16_t pitch = lara_item->rot.x + lara->head_rot.x;\n    const XYZ_32 axis = XYZ_32_FromYawPitch(yaw, pitch, 1 << W2V_SHIFT);\n\n    const int64_t axis_len2 = XYZ_32_GetLength2_64(axis);\n    if (axis_len2 != 0) {\n        XYZ_32_ProjectPointOntoAxis(head_pos, axis, axis_len2, &pos_1);\n        XYZ_32_ProjectPointOntoAxis(head_pos, axis, axis_len2, &pos_2);\n        XYZ_32_ProjectPointOntoAxis(head_pos, axis, axis_len2, &pos_3);\n    }\n\n    const XYZ_32 delta = {\n        .x = (pos_2.x - pos_1.x) >> 3,\n        .y = (pos_2.y - pos_1.y) >> 3,\n        .z = (pos_2.z - pos_1.z) >> 3,\n    };\n\n    XYZ_32 ideal_pos = pos_1;\n    room_num = lara_item->room_num;\n\n    int32_t i = 0;\n    for (; i < M_LOS_STEPS; i++) {\n        int16_t next_room_num = room_num;\n        sector = Room_GetSector(\n            (XYZ_32) { ideal_pos.x, ideal_pos.y + STEP_L, ideal_pos.z },\n            &next_room_num);\n\n        if (Room_Get(next_room_num)->flags.swamp) {\n            ideal_pos.y = Room_Get(next_room_num)->max_ceiling - STEP_L;\n            break;\n        }\n\n        sector = Room_GetSector(ideal_pos, &next_room_num);\n        height = Room_GetHeight(sector, ideal_pos);\n        ceiling = Room_GetCeiling(sector, ideal_pos);\n\n        if (height == NO_HEIGHT || ceiling == NO_HEIGHT || ceiling >= height\n            || ideal_pos.y > height || ideal_pos.y < ceiling) {\n            break;\n        }\n\n        room_num = next_room_num;\n        ideal_pos.x += delta.x;\n        ideal_pos.y += delta.y;\n        ideal_pos.z += delta.z;\n    }\n\n    if (i != 0) {\n        ideal_pos.x -= delta.x;\n        ideal_pos.y -= delta.y;\n        ideal_pos.z -= delta.z;\n    }\n\n    GAME_VECTOR ideal = {\n        .pos = ideal_pos,\n        .room_num = room_num,\n    };\n    if (M_UpdateLaraState()) {\n        m_LastLookIdeal.pos = ideal;\n        m_LastLookIdeal.target.pos = pos_3;\n    } else {\n        ideal = m_LastLookIdeal.pos;\n        pos_3 = m_LastLookIdeal.target.pos;\n    }\n\n    M_Collide(&ideal, M_LOOK_SHIFT, true);\n\n    if (m_LastState.cam_type == CAM_FIXED) {\n        g_Camera.pos.pos = ideal.pos;\n        g_Camera.target.pos = pos_3;\n    } else {\n        g_Camera.pos.x += (ideal.x - g_Camera.pos.x) >> 2;\n        g_Camera.pos.y += (ideal.y - g_Camera.pos.y) >> 2;\n        g_Camera.pos.z += (ideal.z - g_Camera.pos.z) >> 2;\n        g_Camera.target.x += (pos_3.x - g_Camera.target.x) >> 2;\n        g_Camera.target.y += (pos_3.y - g_Camera.target.y) >> 2;\n        g_Camera.target.z += (pos_3.z - g_Camera.target.z) >> 2;\n    }\n\n    g_Camera.target.room_num = lara_item->room_num;\n\n    if (g_Camera.type == m_LastState.cam_type) {\n        Camera_ApplyBounce();\n    }\n\n    Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num);\n    ideal_pos = g_Camera.pos.pos;\n    room_num = g_Camera.pos.room_num;\n    sector = Room_GetSector(ideal_pos, &room_num);\n    height = Room_GetHeight(sector, ideal_pos);\n    ceiling = Room_GetCeiling(sector, ideal_pos);\n\n    if (ceiling != NO_HEIGHT && height != NO_HEIGHT) {\n        if (ceiling > ideal_pos.y - 255 && height < ideal_pos.y + 255\n            && height > ceiling) {\n            g_Camera.pos.y = (ceiling + height) >> 1;\n        } else if (height < ideal_pos.y + 255 && height > ceiling) {\n            g_Camera.pos.y = height - 255;\n        } else if (ceiling > ideal_pos.y - 255 && height > ceiling) {\n            g_Camera.pos.y = ceiling + 255;\n        }\n    }\n\n    ideal_pos = g_Camera.pos.pos;\n    room_num = g_Camera.pos.room_num;\n    sector = Room_GetSector(ideal_pos, &room_num);\n    height = Room_GetHeight(sector, ideal_pos);\n    ceiling = Room_GetCeiling(sector, ideal_pos);\n\n    if (Room_Get(room_num)->flags.swamp) {\n        g_Camera.pos.y = Room_Get(room_num)->max_ceiling - STEP_L;\n    } else if (\n        ideal_pos.y < ceiling || ideal_pos.y > height || ceiling >= height\n        || height == NO_HEIGHT || ceiling == NO_HEIGHT) {\n        M_LOS(&g_Camera.target, &g_Camera.pos, 0);\n    }\n\n    ideal_pos = g_Camera.pos.pos;\n    room_num = g_Camera.pos.room_num;\n    sector = Room_GetSector(ideal_pos, &room_num);\n    height = Room_GetHeight(sector, ideal_pos);\n    ceiling = Room_GetCeiling(sector, ideal_pos);\n\n    if (ideal_pos.y < ceiling || ideal_pos.y > height || ceiling >= height\n        || height == NO_HEIGHT || ceiling == NO_HEIGHT\n        || Room_Get(room_num)->flags.swamp) {\n        g_Camera.pos.pos = pos_1;\n        g_Camera.pos.room_num = lara_item->room_num;\n    }\n\n    Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num);\n    m_LastState.cam_type = g_Camera.type;\n\n    Camera_UpdateMicPosition();\n\n    lara->head_rot = head_rot;\n    lara->torso_rot = torso_rot;\n}\n\nstatic void M_ClampResult(void)\n{\n    XYZ_32 pos = g_Camera.interp.result.pos;\n    int16_t room_num = g_Camera.interp.room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    const int32_t ceiling = Room_GetCeilingEx(sector, pos, true);\n    if (height == NO_HEIGHT || ceiling == NO_HEIGHT || height < g_Camera.pos.y\n        || ceiling > g_Camera.pos.y) {\n        pos = g_Camera.pos.pos;\n        room_num = g_Camera.pos.room_num;\n    }\n    Room_GetSector(pos, &room_num);\n    g_Camera.interp.result.pos = pos;\n    g_Camera.interp.room_num = room_num;\n}\n\nstatic void M_Reset(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    ASSERT(lara_item != nullptr);\n    g_Camera.shift = 0;\n    m_LastIdeal.target.pos = lara_item->pos;\n    m_LastIdeal.target.pos.y -= WALL_L;\n    m_LastIdeal.target.room_num = lara_item->room_num;\n\n    g_Camera.target = m_LastIdeal.target;\n    g_Camera.pos = m_LastIdeal.target;\n    g_Camera.pos.z -= 100;\n}\n\nstatic void M_Update(\n    const ITEM *const item, const bool fixed_camera, int32_t target_y)\n{\n    Camera_SetChunky(false);\n    if (g_Camera.type != CAM_LOOK) {\n        m_LastIdeal.target = g_Camera.target;\n    }\n\n    const BOUNDS_16 *bounds = Item_GetBoundsAccurate(item);\n\n    if (g_Camera.item != nullptr && !fixed_camera) {\n        bounds = Item_GetBoundsAccurate(g_Camera.item);\n\n        const int32_t dx = g_Camera.item->pos.x - item->pos.x;\n        const int32_t dz = g_Camera.item->pos.z - item->pos.z;\n        const int32_t shift = Math_Sqrt(SQUARE(dx) + SQUARE(dz));\n        int16_t angle = Math_Atan(dz, dx) - item->rot.y;\n\n        int16_t tilt = Math_Atan(\n            shift,\n            target_y - (bounds->min.y + bounds->max.y) / 2\n                - g_Camera.item->pos.y);\n        angle >>= 1;\n        tilt >>= 1;\n\n        if (angle > CAMERA_MIN_HEAD_ROTATION && angle < CAMERA_MAX_HEAD_ROTATION\n            && tilt > CAMERA_MIN_HEAD_TILT && tilt < CAMERA_MAX_HEAD_TILT) {\n            LARA_INFO *const lara_info = Lara_GetLaraInfo();\n            int16_t change = angle - lara_info->head_rot.y;\n            if (change > CAMERA_HEAD_TURN) {\n                lara_info->head_rot.y += CAMERA_HEAD_TURN;\n            } else if (change < -CAMERA_HEAD_TURN) {\n                lara_info->head_rot.y -= CAMERA_HEAD_TURN;\n            } else {\n                lara_info->head_rot.y = angle;\n            }\n\n            change = tilt - lara_info->head_rot.x;\n            if (change > CAMERA_HEAD_TURN) {\n                lara_info->head_rot.x += CAMERA_HEAD_TURN;\n            } else if (change < -CAMERA_HEAD_TURN) {\n                lara_info->head_rot.x -= CAMERA_HEAD_TURN;\n            } else {\n                lara_info->head_rot.x = tilt;\n            }\n            lara_info->torso_rot.x = lara_info->head_rot.x;\n            lara_info->torso_rot.y = lara_info->head_rot.y;\n            g_Camera.type = CAM_LOOK;\n            g_Camera.item->looked_at = true;\n        }\n    }\n\n    if (g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT) {\n        target_y -= STEP_L;\n        g_Camera.target.room_num = item->room_num;\n        if (g_Camera.fixed_camera) {\n            g_Camera.target.y = target_y;\n            g_Camera.speed = 1;\n        } else {\n            g_Camera.target.y += (target_y - g_Camera.target.y) >> 2;\n            g_Camera.speed = g_Camera.type == CAM_LOOK ? CAMERA_LOOK_SPEED\n                                                       : CAMERA_COMBAT_SPEED;\n        }\n        g_Camera.fixed_camera = false;\n        if (g_Camera.type == CAM_LOOK) {\n            M_Look(item);\n        } else {\n            M_Combat(item);\n        }\n    } else {\n        g_Camera.target.x = item->pos.x;\n        g_Camera.target.z = item->pos.z;\n\n        if (g_Camera.flags == CF_FOLLOW_CENTRE) {\n            const int32_t shift = (bounds->min.z + bounds->max.z) / 2;\n            g_Camera.target.z += (shift * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n            g_Camera.target.x += (shift * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n        }\n\n        g_Camera.target.room_num = item->room_num;\n        g_Camera.target.y = target_y;\n\n        if (g_Camera.fixed_camera != fixed_camera) {\n            g_Camera.fixed_camera = true;\n            g_Camera.speed = 1;\n        } else {\n            g_Camera.fixed_camera = false;\n        }\n\n        if (g_Camera.speed != 1 && m_LastState.cam_type != CAM_LOOK) {\n            g_Camera.target.x =\n                ((g_Camera.target.x - m_LastIdeal.target.x) >> 2)\n                + m_LastIdeal.target.x;\n            g_Camera.target.y =\n                ((g_Camera.target.y - m_LastIdeal.target.y) >> 2)\n                + m_LastIdeal.target.y;\n            g_Camera.target.z =\n                ((g_Camera.target.z - m_LastIdeal.target.z) >> 2)\n                + m_LastIdeal.target.z;\n        }\n\n        Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num);\n\n        if (g_Camera.type == CAM_CHASE || g_Camera.flags == CF_CHASE_OBJECT) {\n            M_Chase(item);\n        } else {\n            M_Fixed();\n        }\n    }\n}\n\nbool Camera_LOSCheck(\n    GAME_VECTOR *const start, GAME_VECTOR *const target, const int32_t shift)\n{\n    return M_LOS(start, target, shift);\n}\n\nbool Camera_Collide(\n    GAME_VECTOR *const ideal, const int32_t shift, const bool y_first)\n{\n    return M_Collide(ideal, shift, y_first);\n}\n\nstatic const CAMERA_STRATEGY m_Strategy = {\n    .get_chase_speed_func = M_GetChaseSpeed,\n    .get_look_settings_func = M_GetLookSettingsFunc,\n    .clamp_result_func = M_ClampResult,\n    .reset_func = M_Reset,\n    .update_func = M_Update,\n};\n\nREGISTER_CAMERA(CAMERA_MODE_TR3, m_Strategy)\n"
  },
  {
    "path": "src/trx/game/camera/photo_mode.c",
    "content": "#include <trx/game/camera/photo_mode.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/viewport.h>\n\n#define MIN_PHOTO_FOV 10\n#define MAX_PHOTO_FOV 150\n#define PHOTO_ROT_SHIFT (DEG_1 * 4)\n#define PHOTO_MAX_PITCH_ROLL (DEG_90 - DEG_1)\n#define PHOTO_MAX_SPEED 100\n\n#define M_CosMul(a, b) TRIGMULT2(Math_Cos((a)), (b))\n#define M_SinMul(a, b) TRIGMULT2(Math_Sin((a)), (b))\n\nstatic int32_t m_PhotoSpeed = 0;\nstatic int32_t m_OriginalFOV;\nstatic FOV_MODE m_OriginalFOVMode;\nstatic CAMERA_INFO m_OriginalCamera = {};\nstatic int32_t m_CurrentFOV;\nstatic FOV_MODE m_CurrentFOVMode;\nstatic CAMERA_INFO m_StartingCamera = {};\nstatic struct {\n    bool is_chunky;\n    int32_t fov;\n    FOV_MODE fov_mode;\n    CAMERA_INFO camera;\n} m_PreviousState;\nstatic BOUNDS_32 m_WorldBounds = {};\n\nstatic void M_ResetCamera(const bool exiting)\n{\n    CAMERA_INFO camera = g_Camera;\n    g_Camera = exiting ? m_OriginalCamera : m_StartingCamera;\n    // ensure Camera_EnsureEnvironment() picks up the flag change\n    g_Camera.underwater = camera.underwater;\n    Viewport_AlterFOV(exiting ? -1 : m_OriginalFOV, m_OriginalFOVMode);\n    m_CurrentFOV = m_OriginalFOV / DEG_1;\n}\n\nstatic int32_t M_GetShiftSpeed(const int32_t val)\n{\n    return val * m_PhotoSpeed / (float)PHOTO_MAX_SPEED;\n}\n\nstatic int32_t M_GetRotSpeed(void)\n{\n    return MAX(DEG_1, M_GetShiftSpeed(PHOTO_ROT_SHIFT));\n}\n\nstatic XYZ_32 M_GetShift(const int32_t dx, const int32_t dy, const int32_t dz)\n{\n    const int16_t yaw = g_Camera.target_angle;\n    const int16_t pitch = g_Camera.target_elevation;\n    const int16_t roll = g_Camera.roll;\n\n    const int32_t dx_r = M_CosMul(roll, dx) - M_SinMul(roll, dy);\n    const int32_t dy_r = M_SinMul(roll, dx) + M_CosMul(roll, dy);\n    const int32_t dz_r = dz; // unchanged if roll is around Z\n\n    const int32_t dy_p = M_CosMul(pitch, dy_r) - M_SinMul(pitch, dz_r);\n    const int32_t dz_p = M_SinMul(pitch, dy_r) + M_CosMul(pitch, dz_r);\n    const int32_t dx_p = dx_r; // unchanged if pitch is around X\n\n    const int32_t dx_y = M_CosMul(yaw, dx_p) + M_SinMul(yaw, dz_p);\n    const int32_t dz_y = -M_SinMul(yaw, dx_p) + M_CosMul(yaw, dz_p);\n    const int32_t dy_y = dy_p; // unchanged if yaw is around Y\n\n    return (XYZ_32) { dx_y, dy_y, dz_y };\n}\n\nstatic void M_ShiftCamera(int32_t dx, int32_t dy, int32_t dz)\n{\n    const XYZ_32 shift = M_GetShift(dx, dy, dz);\n    g_Camera.pos.x += shift.x;\n    g_Camera.pos.y += shift.y;\n    g_Camera.pos.z += shift.z;\n    g_Camera.target.x += shift.x;\n    g_Camera.target.y += shift.y;\n    g_Camera.target.z += shift.z;\n}\n\nstatic void M_ApplyRotation(\n    const int32_t d_yaw, const int32_t d_pitch, const int32_t d_roll,\n    const bool respect_roll)\n{\n    int32_t yaw = g_Camera.target_angle;\n    int32_t pitch = g_Camera.target_elevation;\n    int32_t roll = g_Camera.roll;\n\n    // rotate with respect to current upright axis\n    if (respect_roll) {\n        yaw += M_CosMul(roll, d_yaw) + M_SinMul(roll, d_pitch);\n        pitch += M_CosMul(roll, d_pitch) - M_SinMul(roll, d_yaw);\n    } else {\n        yaw += d_yaw;\n        pitch += d_pitch;\n    }\n    roll += d_roll;\n\n    // handle pivoting\n    if (pitch >= DEG_90 || pitch <= -DEG_90) {\n        roll += DEG_180;\n        yaw += DEG_180;\n        pitch = g_Camera.target_elevation;\n    }\n\n    g_Camera.target_angle = yaw;\n    g_Camera.target_elevation = pitch;\n    g_Camera.roll = roll;\n}\n\nstatic void M_RotateCamera(\n    const int32_t d_yaw, const int32_t d_pitch, const int32_t d_roll)\n{\n    M_ApplyRotation(d_yaw, d_pitch, d_roll, true);\n    const XYZ_32 shift = M_GetShift(0, 0, g_Camera.target_distance);\n    g_Camera.target.x = g_Camera.pos.x + shift.x;\n    g_Camera.target.y = g_Camera.pos.y + shift.y;\n    g_Camera.target.z = g_Camera.pos.z + shift.z;\n}\n\nstatic void M_RotateTarget(\n    const int32_t d_yaw, const int32_t d_pitch, const int32_t d_roll)\n{\n    M_ApplyRotation(d_yaw, d_pitch, d_roll, false);\n    const XYZ_32 shift = M_GetShift(0, 0, g_Camera.target_distance);\n    g_Camera.pos.x = g_Camera.target.x - shift.x;\n    g_Camera.pos.y = g_Camera.target.y - shift.y;\n    g_Camera.pos.z = g_Camera.target.z - shift.z;\n}\n\nstatic void M_ClampCameraPos(void)\n{\n    // While the camera is free, we want to clamp to within overall world bounds\n    // to help counteract getting lost in the void.\n    const GAME_VECTOR prev_cam_pos = g_Camera.pos;\n    CLAMP(g_Camera.pos.x, m_WorldBounds.min.x, m_WorldBounds.max.x);\n    CLAMP(g_Camera.pos.y, m_WorldBounds.min.y, m_WorldBounds.max.y);\n    CLAMP(g_Camera.pos.z, m_WorldBounds.min.z, m_WorldBounds.max.z);\n\n    g_Camera.target.x += (g_Camera.pos.x - prev_cam_pos.x);\n    g_Camera.target.y += (g_Camera.pos.y - prev_cam_pos.y);\n    g_Camera.target.z += (g_Camera.pos.z - prev_cam_pos.z);\n}\n\nstatic bool M_CameraInsideRoom(const XYZ_32 pos, const int16_t room_num)\n{\n    return Room_PointInside(Room_Get(room_num), pos);\n}\n\nstatic void M_UpdateCameraRooms(void)\n{\n    Room_GetSector(g_Camera.pos.pos, &g_Camera.pos.room_num);\n    Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num);\n    if (!M_CameraInsideRoom(g_Camera.pos.pos, g_Camera.pos.room_num)) {\n        const int16_t pos_room_num = Room_GetIndexFromPos(g_Camera.pos.pos);\n        const int16_t tar_room_num = Room_GetIndexFromPos(g_Camera.target.pos);\n\n        if (pos_room_num != NO_ROOM) {\n            g_Camera.pos.room_num = pos_room_num;\n            if (tar_room_num == NO_ROOM) {\n                g_Camera.target.room_num = pos_room_num;\n            }\n        }\n        if (tar_room_num != NO_ROOM) {\n            g_Camera.target.room_num = tar_room_num;\n            if (pos_room_num == NO_ROOM) {\n                g_Camera.pos.room_num = tar_room_num;\n            }\n        }\n    }\n\n    Camera_EnsureEnvironment();\n}\n\nstatic bool M_HandleShiftInputs(void)\n{\n    bool result = false;\n\n    const int32_t distance = M_GetShiftSpeed((WALL_L * 5.0) / LOGIC_FPS);\n    if (g_Input.camera_left) {\n        M_ShiftCamera(-distance, 0, 0);\n        result = true;\n    } else if (g_Input.camera_right) {\n        M_ShiftCamera(distance, 0, 0);\n        result = true;\n    }\n\n    if (g_Input.camera_forward) {\n        M_ShiftCamera(0, 0, distance);\n        result = true;\n    } else if (g_Input.camera_back) {\n        M_ShiftCamera(0, 0, -distance);\n        result = true;\n    }\n\n    if (!g_Input.slow && g_Input.camera_up) {\n        M_ShiftCamera(0, -distance, 0);\n        result = true;\n    } else if (!g_Input.slow && g_Input.camera_down) {\n        M_ShiftCamera(0, distance, 0);\n        result = true;\n    }\n\n    return result;\n}\n\nstatic bool M_HandleRotationInputs(void)\n{\n    bool result = false;\n\n    if (g_Input.forward) {\n        M_RotateCamera(0, -M_GetRotSpeed(), 0);\n        result = true;\n    } else if (g_Input.back) {\n        M_RotateCamera(0, M_GetRotSpeed(), 0);\n        result = true;\n    }\n\n    if (g_Input.left) {\n        M_RotateCamera(-M_GetRotSpeed(), 0, 0);\n        result = true;\n    } else if (g_Input.right) {\n        M_RotateCamera(M_GetRotSpeed(), 0, 0);\n        result = true;\n    }\n\n    if (g_Input.slow && g_Input.camera_up) {\n        M_RotateCamera(0, 0, -M_GetRotSpeed());\n        result = true;\n    } else if (g_Input.slow && g_Input.camera_down) {\n        M_RotateCamera(0, 0, M_GetRotSpeed());\n        result = true;\n    }\n\n    return result;\n}\n\nstatic bool M_HandleTargetRotationInputs(void)\n{\n    bool result = false;\n    if (g_InputDB.roll) {\n        M_RotateTarget(-DEG_90, 0, 0);\n        result = true;\n    }\n    return result;\n}\n\nstatic bool M_HandleFOVInputs(void)\n{\n    if (!g_Input.draw) {\n        return false;\n    }\n\n    if (g_Input.slow) {\n        m_CurrentFOV--;\n    } else {\n        m_CurrentFOV++;\n    }\n    CLAMP(m_CurrentFOV, MIN_PHOTO_FOV, MAX_PHOTO_FOV);\n    Viewport_AlterFOV(m_CurrentFOV * DEG_1, m_CurrentFOVMode);\n    return true;\n}\n\nvoid Camera_PhotoMode_Enter(void)\n{\n    m_OriginalCamera = g_Camera;\n\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        g_Camera.target.x - g_Camera.pos.x, g_Camera.target.y - g_Camera.pos.y,\n        g_Camera.target.z - g_Camera.pos.z, angles);\n    g_Camera.target_angle = angles[0];\n    g_Camera.target_elevation = angles[1];\n    g_Camera.target_distance = CAMERA_DEFAULT_DISTANCE;\n    g_Camera.target_square = SQUARE(g_Camera.target_distance);\n\n    g_Camera.target.room_num = g_Camera.pos.room_num;\n    Room_GetSector(g_Camera.target.pos, &g_Camera.target.room_num);\n\n    m_StartingCamera = g_Camera;\n\n    m_OriginalFOV = Viewport_GetEffectiveFOV();\n    m_OriginalFOVMode = Viewport_GetFOVMode();\n    m_CurrentFOV = m_OriginalFOV / DEG_1;\n    m_CurrentFOVMode = m_OriginalFOVMode;\n    g_Camera.type = CAM_PHOTO_MODE;\n    const int32_t border = WALL_L * 5;\n    m_WorldBounds = Room_GetWorldBounds();\n    m_WorldBounds.min.x -= border;\n    m_WorldBounds.min.y -= border;\n    m_WorldBounds.min.z -= border;\n    m_WorldBounds.max.x += border;\n    m_WorldBounds.max.y += border;\n    m_WorldBounds.max.z += border;\n    M_UpdateCameraRooms();\n}\n\nvoid Camera_PhotoMode_Exit(void)\n{\n    Lara_Pose_Clear();\n    Viewport_AlterFOV(m_OriginalFOV, m_OriginalFOVMode);\n    M_ResetCamera(true);\n}\n\nvoid Camera_PhotoMode_Update(void)\n{\n    M_HandleFOVInputs();\n\n    bool changed = false;\n\n    if (g_InputDB.camera_reset) {\n        M_ResetCamera(false);\n        g_Camera.type = CAM_PHOTO_MODE;\n        changed = true;\n    }\n\n    changed |= M_HandleShiftInputs();\n    changed |= M_HandleRotationInputs();\n\n    if (changed) {\n        m_PhotoSpeed++;\n        CLAMPG(m_PhotoSpeed, PHOTO_MAX_SPEED);\n    } else {\n        m_PhotoSpeed = 0;\n    }\n\n    changed |= M_HandleTargetRotationInputs();\n\n    if (changed) {\n        g_Camera.mic_pos = g_Camera.pos;\n        M_ClampCameraPos();\n        M_UpdateCameraRooms();\n    }\n}\n\nvoid Camera_PhotoMode_UpdateFOV(void)\n{\n    M_HandleFOVInputs();\n}\n\nvoid Camera_PhotoMode_Pause(void)\n{\n    m_PreviousState.camera = g_Camera;\n    m_PreviousState.is_chunky = Camera_IsChunky();\n    m_PreviousState.fov = Viewport_GetSystemFOV();\n    m_PreviousState.fov_mode = Viewport_GetFOVMode();\n    g_Camera = m_OriginalCamera;\n}\n\nvoid Camera_PhotoMode_Resume(void)\n{\n    g_Camera = m_PreviousState.camera;\n    Camera_SetChunky(m_PreviousState.is_chunky);\n    Viewport_AlterFOV(m_PreviousState.fov, m_PreviousState.fov_mode);\n}\n"
  },
  {
    "path": "src/trx/game/camera/photo_mode.h",
    "content": "#pragma once\n\nvoid Camera_PhotoMode_Enter(void);\nvoid Camera_PhotoMode_Exit(void);\nvoid Camera_PhotoMode_Update(void);\nvoid Camera_PhotoMode_UpdateFOV(void);\n\nvoid Camera_PhotoMode_Pause(void);\nvoid Camera_PhotoMode_Resume(void);\n"
  },
  {
    "path": "src/trx/game/camera/types.h",
    "content": "#pragma once\n\n#include <trx/game/camera/enum.h>\n#include <trx/game/types.h>\n\ntypedef struct {\n    int16_t head_turn;\n    int16_t max_head_rotation;\n    int16_t min_head_rotation;\n    int16_t max_head_tilt;\n    int16_t min_head_tilt;\n    float torso_head_rot_y;\n    float torso_head_rot_x;\n} CAMERA_LOOK_SETTINGS;\n\ntypedef struct {\n    int16_t (*get_chase_speed_func)(void);\n    const CAMERA_LOOK_SETTINGS *(*get_look_settings_func)(bool on_surface);\n    void (*clamp_result_func)(void);\n    void (*reset_func)(void);\n    void (*update_func)(const ITEM *item, bool fixed_camera, int32_t target_y);\n} CAMERA_STRATEGY;\n\ntypedef struct {\n    GAME_VECTOR pos;\n    GAME_VECTOR target;\n    CAMERA_TYPE type;\n\n    int32_t shift;\n    CAMERA_FLAGS flags;\n    bool fixed_camera;\n    int32_t bounce;\n    bool underwater;\n    int32_t target_distance;\n    int32_t target_square;\n    int16_t target_angle;\n    int16_t actual_angle;\n    int16_t target_elevation;\n    int16_t num;\n    int16_t last;\n    int16_t timer;\n    int16_t speed;\n    int16_t roll;\n    ITEM *item;\n    ITEM *last_item;\n\n    int32_t debuff;\n\n    // used for the manual camera control\n    int16_t additional_angle;\n    int16_t additional_elevation;\n    GAME_VECTOR mic_pos;\n\n    struct {\n        struct {\n            XYZ_32 target;\n            XYZ_32 pos;\n            int32_t shift;\n        } result, prev;\n        int16_t room_num;\n    } interp;\n} CAMERA_INFO;\n\ntypedef struct {\n    struct {\n        XYZ_16 shift;\n    } target, camera;\n    int16_t fov;\n    int16_t roll;\n} CINE_FRAME;\n\ntypedef struct {\n    int16_t frame_idx;\n    int16_t frame_count;\n    struct {\n        XYZ_32 pos;\n        XYZ_16 rot;\n        int16_t target_angle;\n    } position;\n} CINE_DATA;\n"
  },
  {
    "path": "src/trx/game/camera/vars.c",
    "content": "#include <trx/game/camera/vars.h>\n\nCAMERA_INFO g_Camera = {};\n"
  },
  {
    "path": "src/trx/game/camera/vars.h",
    "content": "#pragma once\n\n#include <trx/game/camera/types.h>\n\nextern CAMERA_INFO g_Camera;\n"
  },
  {
    "path": "src/trx/game/camera.h",
    "content": "#pragma once\n\n#include <trx/game/camera/cinematic.h>\n#include <trx/game/camera/common.h>\n#include <trx/game/camera/const.h>\n#include <trx/game/camera/enum.h>\n#include <trx/game/camera/environment.h>\n#include <trx/game/camera/fixed.h>\n#include <trx/game/camera/photo_mode.h>\n#include <trx/game/camera/types.h>\n#include <trx/game/camera/vars.h>\n"
  },
  {
    "path": "src/trx/game/catalog/item_actions.def",
    "content": "X_CATALOG_ID(ITEM_ACTION_TURN_180)\nX_CATALOG_ID(ITEM_ACTION_LARA_NORMAL)\nX_CATALOG_ID(ITEM_ACTION_LARA_HANDS_FREE)\nX_CATALOG_ID(ITEM_ACTION_LARA_DRAW_RIGHT_GUN)\nX_CATALOG_ID(ITEM_ACTION_LARA_DRAW_LEFT_GUN)\nX_CATALOG_ID(ITEM_ACTION_LARA_SHOOT_RIGHT_GUN)\nX_CATALOG_ID(ITEM_ACTION_LARA_SHOOT_LEFT_GUN)\nX_CATALOG_ID(ITEM_ACTION_RESET_HAIR)\nX_CATALOG_ID(ITEM_ACTION_FINISH_LEVEL)\nX_CATALOG_ID(ITEM_ACTION_FLIP_MAP)\nX_CATALOG_ID(ITEM_ACTION_FLOOR_SHAKE)\nX_CATALOG_ID(ITEM_ACTION_BUBBLES)\nX_CATALOG_ID(ITEM_ACTION_EARTHQUAKE)\nX_CATALOG_ID(ITEM_ACTION_FLOOD)\nX_CATALOG_ID(ITEM_ACTION_RAISING_BLOCK)\nX_CATALOG_ID(ITEM_ACTION_STAIRS_TO_SLOPE)\nX_CATALOG_ID(ITEM_ACTION_DROP_SAND)\nX_CATALOG_ID(ITEM_ACTION_POWER_UP)\nX_CATALOG_ID(ITEM_ACTION_EXPLOSION)\nX_CATALOG_ID(ITEM_ACTION_CHAIN_BLOCK)\nX_CATALOG_ID(ITEM_ACTION_FLICKER)\nX_CATALOG_ID(ITEM_ACTION_CHANDELIER)\nX_CATALOG_ID(ITEM_ACTION_RUBBLE)\nX_CATALOG_ID(ITEM_ACTION_PISTON)\nX_CATALOG_ID(ITEM_ACTION_CURTAIN)\nX_CATALOG_ID(ITEM_ACTION_SET_CHANGE)\nX_CATALOG_ID(ITEM_ACTION_STATUE)\nX_CATALOG_ID(ITEM_ACTION_BOILER)\nX_CATALOG_ID(ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1)\nX_CATALOG_ID(ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2)\nX_CATALOG_ID(ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3)\nX_CATALOG_ID(ITEM_ACTION_INVISIBILITY_ON)\nX_CATALOG_ID(ITEM_ACTION_INVISIBILITY_OFF)\nX_CATALOG_ID(ITEM_ACTION_DYNAMIC_LIGHT_ON)\nX_CATALOG_ID(ITEM_ACTION_DYNAMIC_LIGHT_OFF)\nX_CATALOG_ID(ITEM_ACTION_ASSAULT_RESET)\nX_CATALOG_ID(ITEM_ACTION_ASSAULT_STOP)\nX_CATALOG_ID(ITEM_ACTION_ASSAULT_START)\nX_CATALOG_ID(ITEM_ACTION_ASSAULT_FINISHED)\nX_CATALOG_ID(ITEM_ACTION_SHADOW_ON)\nX_CATALOG_ID(ITEM_ACTION_SHADOW_OFF)\nX_CATALOG_ID(ITEM_ACTION_FOOTPRINT)\nX_CATALOG_ID(ITEM_ACTION_ASSAULT_PENALTY_8)\nX_CATALOG_ID(ITEM_ACTION_ASSAULT_PENALTY_30)\nX_CATALOG_ID(ITEM_ACTION_RACETRACK_START)\nX_CATALOG_ID(ITEM_ACTION_RACETRACK_RESET)\nX_CATALOG_ID(ITEM_ACTION_RACETRACK_FINISHED)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_1)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_2)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_3)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_4)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_5)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_6)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_7)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_8)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_9)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_10)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_11)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_12)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_13)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_14)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_15)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_16)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_17)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_18)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_19)\nX_CATALOG_ID(ITEM_ACTION_GYM_HINT_RESET)\nX_CATALOG_ID(ITEM_ACTION_CAMERA_SHAKE)\nX_CATALOG_ID(ITEM_ACTION_LOWERING_BLOCK)\nX_CATALOG_ID(ITEM_ACTION_TURN_90)\n"
  },
  {
    "path": "src/trx/game/catalog/lara_anims.def",
    "content": "X_CATALOG_ID(LA_RUN)\nX_CATALOG_ID(LA_WALK_FORWARD)\nX_CATALOG_ID(LA_WALK_STOP_RIGHT)\nX_CATALOG_ID(LA_WALK_STOP_LEFT)\nX_CATALOG_ID(LA_WALK_TO_RUN_RIGHT)\nX_CATALOG_ID(LA_WALK_TO_RUN_LEFT)\nX_CATALOG_ID(LA_RUN_START)\nX_CATALOG_ID(LA_RUN_TO_WALK_RIGHT)\nX_CATALOG_ID(LA_RUN_TO_STAND_LEFT)\nX_CATALOG_ID(LA_RUN_TO_WALK_LEFT)\nX_CATALOG_ID(LA_RUN_TO_STAND_RIGHT)\nX_CATALOG_ID(LA_STAND_STILL)\nX_CATALOG_ID(LA_TURN_RIGHT_SLOW)\nX_CATALOG_ID(LA_TURN_LEFT_SLOW)\nX_CATALOG_ID(LA_JUMP_FORWARD_LAND_START_UNUSED)\nX_CATALOG_ID(LA_JUMP_FORWARD_LAND_END_UNUSED)\nX_CATALOG_ID(LA_RUN_JUMP_RIGHT_START)\nX_CATALOG_ID(LA_RUN_JUMP_RIGHT_CONTINUE)\nX_CATALOG_ID(LA_RUN_JUMP_LEFT_START)\nX_CATALOG_ID(LA_RUN_JUMP_LEFT_CONTINUE)\nX_CATALOG_ID(LA_WALK_FORWARD_START)\nX_CATALOG_ID(LA_WALK_FORWARD_START_CONTINUE)\nX_CATALOG_ID(LA_JUMP_FORWARD_TO_FREEFALL)\nX_CATALOG_ID(LA_FREEFALL)\nX_CATALOG_ID(LA_FREEFALL_LAND)\nX_CATALOG_ID(LA_FREEFALL_LAND_DEATH)\nX_CATALOG_ID(LA_STAND_TO_JUMP_UP)\nX_CATALOG_ID(LA_STAND_TO_JUMP_UP_CONTINUE)\nX_CATALOG_ID(LA_JUMP_UP)\nX_CATALOG_ID(LA_JUMP_UP_TO_HANG_UNUSED)\nX_CATALOG_ID(LA_JUMP_UP_TO_FREEFALL)\nX_CATALOG_ID(LA_JUMP_UP_LAND)\nX_CATALOG_ID(LA_SMASH_JUMP)\nX_CATALOG_ID(LA_SMASH_JUMP_CONTINUE)\nX_CATALOG_ID(LA_FALL_START)\nX_CATALOG_ID(LA_FALL)\nX_CATALOG_ID(LA_FALL_TO_FREEFALL)\nX_CATALOG_ID(LA_HANG_TO_FREEFALL)\nX_CATALOG_ID(LA_WALK_BACK_END_RIGHT)\nX_CATALOG_ID(LA_WALK_BACK_END_LEFT)\nX_CATALOG_ID(LA_WALK_BACK)\nX_CATALOG_ID(LA_WALK_BACK_START)\nX_CATALOG_ID(LA_CLIMB_3CLICK)\nX_CATALOG_ID(LA_CLIMB_3CLICK_END_TO_RUN)\nX_CATALOG_ID(LA_TURN_RIGHT)\nX_CATALOG_ID(LA_JUMP_FORWARD_TO_FREEFALL_2)\nX_CATALOG_ID(LA_REACH_TO_FREEFALL)\nX_CATALOG_ID(LA_ROLL_ALTERNATE)\nX_CATALOG_ID(LA_ROLL_END_ALTERNATE)\nX_CATALOG_ID(LA_JUMP_FORWARD_END_TO_FREEFALL)\nX_CATALOG_ID(LA_CLIMB_2CLICK)\nX_CATALOG_ID(LA_CLIMB_2CLICK_END)\nX_CATALOG_ID(LA_CLIMB_2CLICK_END_TO_RUN)\nX_CATALOG_ID(LA_WALL_SMASH_LEFT)\nX_CATALOG_ID(LA_WALL_SMASH_RIGHT)\nX_CATALOG_ID(LA_RUN_UP_STEP_RIGHT)\nX_CATALOG_ID(LA_RUN_UP_STEP_LEFT)\nX_CATALOG_ID(LA_WALK_UP_STEP_RIGHT)\nX_CATALOG_ID(LA_WALK_UP_STEP_LEFT)\nX_CATALOG_ID(LA_WALK_DOWN_LEFT)\nX_CATALOG_ID(LA_WALK_DOWN_RIGHT)\nX_CATALOG_ID(LA_WALK_DOWN_BACK_LEFT)\nX_CATALOG_ID(LA_WALK_DOWN_BACK_RIGHT)\nX_CATALOG_ID(LA_WALL_SWITCH_DOWN)\nX_CATALOG_ID(LA_WALL_SWITCH_UP)\nX_CATALOG_ID(LA_SIDE_STEP_LEFT)\nX_CATALOG_ID(LA_SIDE_STEP_LEFT_END)\nX_CATALOG_ID(LA_SIDE_STEP_RIGHT)\nX_CATALOG_ID(LA_SIDE_STEP_RIGHT_END)\nX_CATALOG_ID(LA_ROTATE_LEFT)\nX_CATALOG_ID(LA_SLIDE_FORWARD)\nX_CATALOG_ID(LA_SLIDE_FORWARD_END)\nX_CATALOG_ID(LA_SLIDE_FORWARD_STOP)\nX_CATALOG_ID(LA_STAND_TO_JUMP)\nX_CATALOG_ID(LA_JUMP_BACK_START)\nX_CATALOG_ID(LA_JUMP_BACK)\nX_CATALOG_ID(LA_JUMP_FORWARD_START)\nX_CATALOG_ID(LA_JUMP_FORWARD)\nX_CATALOG_ID(LA_JUMP_LEFT_START)\nX_CATALOG_ID(LA_JUMP_LEFT)\nX_CATALOG_ID(LA_JUMP_RIGHT_START)\nX_CATALOG_ID(LA_JUMP_RIGHT)\nX_CATALOG_ID(LA_LAND)\nX_CATALOG_ID(LA_JUMP_BACK_TO_FREEFALL)\nX_CATALOG_ID(LA_JUMP_LEFT_TO_FREEFALL)\nX_CATALOG_ID(LA_JUMP_RIGHT_TO_FREEFALL)\nX_CATALOG_ID(LA_UNDERWATER_SWIM_FORWARD)\nX_CATALOG_ID(LA_UNDERWATER_SWIM_FORWARD_DRIFT)\nX_CATALOG_ID(LA_SMALL_JUMP_BACK_START)\nX_CATALOG_ID(LA_SMALL_JUMP_BACK)\nX_CATALOG_ID(LA_SMALL_JUMP_BACK_END)\nX_CATALOG_ID(LA_JUMP_UP_START)\nX_CATALOG_ID(LA_LAND_TO_RUN)\nX_CATALOG_ID(LA_FALL_BACK)\nX_CATALOG_ID(LA_JUMP_FORWARD_TO_REACH)\nX_CATALOG_ID(LA_REACH)\nX_CATALOG_ID(LA_REACH_TO_HANG)\nX_CATALOG_ID(LA_CLIMB_ON)\nX_CATALOG_ID(LA_REACH_TO_FREEFALL_2)\nX_CATALOG_ID(LA_FALL_CROUCHING_LANDING)\nX_CATALOG_ID(LA_JUMP_FORWARD_TO_REACH_LATE)\nX_CATALOG_ID(LA_JUMP_FORWARD_START_TO_REACH_ALTERNATE)\nX_CATALOG_ID(LA_CLIMB_ON_END)\nX_CATALOG_ID(LA_STAND_IDLE)\nX_CATALOG_ID(LA_SLIDE_BACKWARD_START)\nX_CATALOG_ID(LA_SLIDE_BACKWARD)\nX_CATALOG_ID(LA_SLIDE_BACKWARD_END)\nX_CATALOG_ID(LA_UNDERWATER_SWIM_TO_IDLE)\nX_CATALOG_ID(LA_UNDERWATER_IDLE)\nX_CATALOG_ID(LA_UNDERWARER_IDLE_TO_SWIM)\nX_CATALOG_ID(LA_ONWATER_IDLE)\nX_CATALOG_ID(LA_ONWATER_TO_STAND_HIGH)\nX_CATALOG_ID(LA_FREEFALL_TO_UNDERWATER)\nX_CATALOG_ID(LA_ONWATER_DIVE_ALTERNATE)\nX_CATALOG_ID(LA_UNDERWATER_TO_ONWATER)\nX_CATALOG_ID(LA_ONWATER_SWIM_FORWARD_DIVE)\nX_CATALOG_ID(LA_ONWATER_SWIM_FORWARD)\nX_CATALOG_ID(LA_ONWATER_SWIM_FORWARD_TO_IDLE)\nX_CATALOG_ID(LA_ONWATER_IDLE_TO_SWIM_FORWARD)\nX_CATALOG_ID(LA_ONWATER_DIVE)\nX_CATALOG_ID(LA_PUSHABLE_GRAB)\nX_CATALOG_ID(LA_PUSHABLE_RELEASE)\nX_CATALOG_ID(LA_PUSHABLE_PULL)\nX_CATALOG_ID(LA_PUSHABLE_PUSH)\nX_CATALOG_ID(LA_UNDERWATER_DEATH)\nX_CATALOG_ID(LA_HIT_FRONT)\nX_CATALOG_ID(LA_HIT_BACK)\nX_CATALOG_ID(LA_HIT_LEFT)\nX_CATALOG_ID(LA_HIT_RIGHT)\nX_CATALOG_ID(LA_UNDERWATER_SWITCH)\nX_CATALOG_ID(LA_UNDERWATER_PICKUP)\nX_CATALOG_ID(LA_USE_KEY)\nX_CATALOG_ID(LA_ONWATER_DEATH)\nX_CATALOG_ID(LA_RUN_DEATH)\nX_CATALOG_ID(LA_USE_PUZZLE)\nX_CATALOG_ID(LA_PICKUP)\nX_CATALOG_ID(LA_SHIMMY_LEFT)\nX_CATALOG_ID(LA_SHIMMY_RIGHT)\nX_CATALOG_ID(LA_STAND_DEATH)\nX_CATALOG_ID(LA_BOULDER_DEATH)\nX_CATALOG_ID(LA_ONWATER_IDLE_TO_SWIM_BACK)\nX_CATALOG_ID(LA_ONWATER_SWIM_BACK)\nX_CATALOG_ID(LA_ONWATER_SWIM_BACK_TO_IDLE)\nX_CATALOG_ID(LA_ONWATER_SWIM_LEFT)\nX_CATALOG_ID(LA_ONWATER_SWIM_RIGHT)\nX_CATALOG_ID(LA_DEATH_JUMP)\nX_CATALOG_ID(LA_ROLL_START)\nX_CATALOG_ID(LA_ROLL_CONTINUE)\nX_CATALOG_ID(LA_ROLL_END)\nX_CATALOG_ID(LA_SPIKE_DEATH)\nX_CATALOG_ID(LA_SWING_IN_SLOW)\nX_CATALOG_ID(LA_SWING_IN_FAST)\nX_CATALOG_ID(LA_SWANDIVE_ROLL)\nX_CATALOG_ID(LA_SWANDIVE_TO_UNDERWATER)\nX_CATALOG_ID(LA_FREEFALL_SWANDIVE)\nX_CATALOG_ID(LA_FREEFALL_SWANDIVE_TO_UNDERWATER)\nX_CATALOG_ID(LA_SWANDIVE_DEATH)\nX_CATALOG_ID(LA_SWANDIVE_LEFT)\nX_CATALOG_ID(LA_SWANDIVE_RIGHT)\nX_CATALOG_ID(LA_SWANDIVE_START)\nX_CATALOG_ID(LA_CLIMB_ON_HANDSTAND)\nX_CATALOG_ID(LA_RUN_JUMP_ROLL_START)\nX_CATALOG_ID(LA_SOMERSAULT)\nX_CATALOG_ID(LA_RUN_JUMP_ROLL_END)\nX_CATALOG_ID(LA_JUMP_FORWARD_ROLL_START)\nX_CATALOG_ID(LA_JUMP_FORWARD_ROLL_END)\nX_CATALOG_ID(LA_JUMP_BACK_ROLL_START)\nX_CATALOG_ID(LA_JUMP_BACK_ROLL_END)\nX_CATALOG_ID(LA_UNDERWATER_ROLL_START)\nX_CATALOG_ID(LA_UNDERWATER_ROLL_END)\nX_CATALOG_ID(LA_ONWATER_TO_STAND_MEDIUM)\nX_CATALOG_ID(LA_WADE)\nX_CATALOG_ID(LA_RUN_TO_WADE_LEFT)\nX_CATALOG_ID(LA_RUN_TO_WADE_RIGHT)\nX_CATALOG_ID(LA_WADE_TO_RUN_LEFT)\nX_CATALOG_ID(LA_WADE_TO_RUN_RIGHT)\nX_CATALOG_ID(LA_WADE_TO_STAND_RIGHT)\nX_CATALOG_ID(LA_WADE_TO_STAND_LEFT)\nX_CATALOG_ID(LA_STAND_TO_WADE)\nX_CATALOG_ID(LA_ONWATER_TO_WADE)\nX_CATALOG_ID(LA_ONWATER_TO_WADE_LOW)\nX_CATALOG_ID(LA_UNDERWATER_TO_STAND)\nX_CATALOG_ID(LA_UNDERWATER_SWIM_TO_STILL_HUDDLE)\nX_CATALOG_ID(LA_UNDERWATER_SWIM_TO_STILL_SPRAWL)\nX_CATALOG_ID(LA_UNDERWATER_SWIM_TO_STILL_MEDIUM)\nX_CATALOG_ID(LA_SLIDE_FORWARD_TO_RUN)\nX_CATALOG_ID(LA_JUMP_NEUTRAL_ROLL)\nX_CATALOG_ID(LA_CONTROLLED_DROP)\nX_CATALOG_ID(LA_CONTROLLED_DROP_CONTINUE)\nX_CATALOG_ID(LA_HANG_TO_JUMP_UP)\nX_CATALOG_ID(LA_HANG_TO_JUMP_UP_CONTINUE)\nX_CATALOG_ID(LA_HANG_TO_JUMP_BACK)\nX_CATALOG_ID(LA_HANG_TO_JUMP_BACK_CONTINUE)\nX_CATALOG_ID(LA_SPRINT)\nX_CATALOG_ID(LA_RUN_TO_SPRINT_LEFT)\nX_CATALOG_ID(LA_RUN_TO_SPRINT_RIGHT)\nX_CATALOG_ID(LA_SPRINT_SLIDE_STAND_LEFT)\nX_CATALOG_ID(LA_SPRINT_SLIDE_STAND_RIGHT)\nX_CATALOG_ID(LA_SPRINT_TO_ROLL_LEFT)\nX_CATALOG_ID(LA_SPRINT_ROLL_LEFT_TO_RUN)\nX_CATALOG_ID(LA_SPRINT_TO_ROLL_RIGHT)\nX_CATALOG_ID(LA_SPRINT_ROLL_RIGHT_TO_RUN)\nX_CATALOG_ID(LA_SPRINT_TO_RUN_LEFT)\nX_CATALOG_ID(LA_SPRINT_TO_RUN_RIGHT)\nX_CATALOG_ID(LA_POSE_RIGHT_START)\nX_CATALOG_ID(LA_POSE_RIGHT_CONTINUE)\nX_CATALOG_ID(LA_POSE_RIGHT_END)\nX_CATALOG_ID(LA_POSE_LEFT_START)\nX_CATALOG_ID(LA_POSE_LEFT_CONTINUE)\nX_CATALOG_ID(LA_POSE_LEFT_END)\nX_CATALOG_ID(LA_STAND_TO_LADDER)\nX_CATALOG_ID(LA_LADDER_UP)\nX_CATALOG_ID(LA_LADDER_UP_STOP_RIGHT)\nX_CATALOG_ID(LA_LADDER_UP_STOP_LEFT)\nX_CATALOG_ID(LA_LADDER_IDLE)\nX_CATALOG_ID(LA_LADDER_UP_START)\nX_CATALOG_ID(LA_LADDER_DOWN_STOP_LEFT)\nX_CATALOG_ID(LA_LADDER_DOWN_STOP_RIGHT)\nX_CATALOG_ID(LA_LADDER_DOWN)\nX_CATALOG_ID(LA_LADDER_DOWN_START)\nX_CATALOG_ID(LA_LADDER_RIGHT)\nX_CATALOG_ID(LA_LADDER_LEFT)\nX_CATALOG_ID(LA_LADDER_HANG)\nX_CATALOG_ID(LA_LADDER_HANG_TO_IDLE)\nX_CATALOG_ID(LA_LADDER_CLIMB_ON)\nX_CATALOG_ID(LA_LADDER_BACKFLIP_START)\nX_CATALOG_ID(LA_LADDER_BACKFLIP_CONTINUE)\nX_CATALOG_ID(LA_LADDER_UP_HANGING)\nX_CATALOG_ID(LA_LADDER_DOWN_HANGING)\nX_CATALOG_ID(LA_LADDER_TO_HANG_DOWN)\nX_CATALOG_ID(LA_LADDER_TO_HANG_RIGHT)\nX_CATALOG_ID(LA_LADDER_TO_HANG_LEFT)\nX_CATALOG_ID(LA_UNKNOWN)\nX_CATALOG_ID(LA_ONWATER_TO_WADE_SHALLOW_UNUSED)\nX_CATALOG_ID(LA_FLARE_THROW)\nX_CATALOG_ID(LA_SWITCH_SMALL_DOWN)\nX_CATALOG_ID(LA_SWITCH_SMALL_UP)\nX_CATALOG_ID(LA_BUTTON_PUSH)\nX_CATALOG_ID(LA_FLARE_PICKUP)\nX_CATALOG_ID(LA_UNDERWATER_FLARE_PICKUP)\nX_CATALOG_ID(LA_KICK)\nX_CATALOG_ID(LA_ZIPLINE_GRAB)\nX_CATALOG_ID(LA_ZIPLINE_RIDE)\nX_CATALOG_ID(LA_ZIPLINE_FALL)\nX_CATALOG_ID(LA_STAND_TO_CROUCH)\nX_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_START)\nX_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_CONTINUE)\nX_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_END)\nX_CATALOG_ID(LA_CROUCH_TO_STAND)\nX_CATALOG_ID(LA_CROUCH_IDLE)\nX_CATALOG_ID(LA_SPRINT_SLIDE_STAND_RIGHT_ALTERNATE)\nX_CATALOG_ID(LA_SPRINT_SLIDE_STAND_LEFT_ALTERNATE)\nX_CATALOG_ID(LA_SPRINT_TO_ROLL_LEFT_ALTERNATE)\nX_CATALOG_ID(LA_MONKEY_GRAB)\nX_CATALOG_ID(LA_MONKEY_IDLE)\nX_CATALOG_ID(LA_MONKEY_FALL)\nX_CATALOG_ID(LA_MONKEY_FORWARD)\nX_CATALOG_ID(LA_MONKEY_STOP_LEFT)\nX_CATALOG_ID(LA_MONKEY_STOP_RIGHT)\nX_CATALOG_ID(LA_MONKEY_IDLE_TO_FORWARD_LEFT)\nX_CATALOG_ID(LA_SPRINT_TO_ROLL_START_ALTERNATE)\nX_CATALOG_ID(LA_SPRINT_TO_ROLL_CONTINUE_ALTERNATE)\nX_CATALOG_ID(LA_SPRINT_TO_ROLL_END_ALTERNATE)\nX_CATALOG_ID(LA_STAND_TO_CROUCH_END)\nX_CATALOG_ID(LA_CROUCH_ROLL_FORWARD_ALTERNATE)\nX_CATALOG_ID(LA_JUMP_FORWARD_START_TO_GRAB_EARLY)\nX_CATALOG_ID(LA_JUMP_FORWARD_START_TO_GRAB_LATE)\nX_CATALOG_ID(LA_RUN_TO_GRAB_RIGHT)\nX_CATALOG_ID(LA_RUN_TO_GRAB_LEFT)\nX_CATALOG_ID(LA_MONKEY_IDLE_TO_FORWARD_RIGHT)\nX_CATALOG_ID(LA_MONKEY_SHIMMY_LEFT)\nX_CATALOG_ID(LA_MONKEY_SHIMMY_LEFT_END)\nX_CATALOG_ID(LA_MONKEY_SHIMMY_RIGHT)\nX_CATALOG_ID(LA_MONKEY_SHIMMY_RIGHT_END)\nX_CATALOG_ID(LA_MONKEY_TURN_AROUND)\nX_CATALOG_ID(LA_CROUCH_TO_CRAWL_START)\nX_CATALOG_ID(LA_CRAWL_TO_CROUCH_START)\nX_CATALOG_ID(LA_CRAWL_FORWARD)\nX_CATALOG_ID(LA_CRAWL_IDLE_TO_FORWARD)\nX_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_START_RIGHT)\nX_CATALOG_ID(LA_CRAWL_IDLE)\nX_CATALOG_ID(LA_CROUCH_TO_CRAWL_END)\nX_CATALOG_ID(LA_CRAWL_TO_CROUCH_END_UNUSED)\nX_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT)\nX_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_START_LEFT)\nX_CATALOG_ID(LA_CRAWL_FORWARD_TO_IDLE_END_LEFT)\nX_CATALOG_ID(LA_CRAWL_TURN_LEFT)\nX_CATALOG_ID(LA_CRAWL_TURN_RIGHT)\nX_CATALOG_ID(LA_MONKEY_TURN_LEFT)\nX_CATALOG_ID(LA_MONKEY_TURN_RIGHT)\nX_CATALOG_ID(LA_CROUCH_TO_CRAWL_CONTINUE)\nX_CATALOG_ID(LA_CRAWL_TO_CROUCH_CONTINUE)\nX_CATALOG_ID(LA_CRAWL_IDLE_TO_BACKWARD)\nX_CATALOG_ID(LA_CRAWL_BACKWARD)\nX_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_START)\nX_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_RIGHT_END)\nX_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_LEFT_START)\nX_CATALOG_ID(LA_CRAWL_BACKWARD_TO_IDLE_LEFT_END)\nX_CATALOG_ID(LA_CRAWL_TURN_LEFT_EARLY_END)\nX_CATALOG_ID(LA_CRAWL_TURN_RIGHT_EARLY_END)\nX_CATALOG_ID(LA_CRAWL_MONKEY_TURN_LEFT_EARLY_END)\nX_CATALOG_ID(LA_CRAWL_MONKEY_TURN_LEFT_LATE_END)\nX_CATALOG_ID(LA_CRAWL_MONKEY_TURN_RIGHT_EARLY_END)\nX_CATALOG_ID(LA_CRAWL_MONKEY_TURN_RIGHT_LATE_END)\nX_CATALOG_ID(LA_HANG_TO_CROUCH_START)\nX_CATALOG_ID(LA_HANG_TO_CROUCH_END)\nX_CATALOG_ID(LA_CRAWL_TO_HANG_START)\nX_CATALOG_ID(LA_CRAWL_TO_HANG_CONTINUE)\nX_CATALOG_ID(LA_CROUCH_PICKUP)\nX_CATALOG_ID(LA_CRAWL_PICKUP)\nX_CATALOG_ID(LA_CROUCH_HIT_FRONT)\nX_CATALOG_ID(LA_CROUCH_HIT_BACK)\nX_CATALOG_ID(LA_CROUCH_HIT_RIGHT)\nX_CATALOG_ID(LA_CROUCH_HIT_LEFT)\nX_CATALOG_ID(LA_CRAWL_HIT_FRONT)\nX_CATALOG_ID(LA_CRAWL_HIT_BACK)\nX_CATALOG_ID(LA_CRAWL_HIT_RIGHT)\nX_CATALOG_ID(LA_CRAWL_HIT_LEFT)\nX_CATALOG_ID(LA_CRAWL_DEATH)\nX_CATALOG_ID(LA_CRAWL_TO_HANG_END)\nX_CATALOG_ID(LA_STAND_TO_CROUCH_ABORT)\nX_CATALOG_ID(LA_RUN_TO_CROUCH_LEFT_START)\nX_CATALOG_ID(LA_RUN_TO_CROUCH_RIGHT_START)\nX_CATALOG_ID(LA_RUN_TO_CROUCH_LEFT_END)\nX_CATALOG_ID(LA_RUN_TO_CROUCH_RIGHT_END)\nX_CATALOG_ID(LA_SPRINT_TO_CROUCH_LEFT)\nX_CATALOG_ID(LA_SPRINT_TO_CROUCH_RIGHT)\nX_CATALOG_ID(LA_CROUCH_PICKUP_FLARE)\nX_CATALOG_ID(LA_CRAWL_JUMP_DOWN)\nX_CATALOG_ID(LA_CROUCH_TURN_LEFT)\nX_CATALOG_ID(LA_CROUCH_TURN_RIGHT)\nX_CATALOG_ID(LA_LADDER_TO_CROUCH_START)\nX_CATALOG_ID(LA_LADDER_TO_CROUCH_END)\n"
  },
  {
    "path": "src/trx/game/catalog/lara_states.def",
    "content": "X_CATALOG_ID(LS_WALK)\nX_CATALOG_ID(LS_RUN)\nX_CATALOG_ID(LS_STOP)\nX_CATALOG_ID(LS_JUMP_FORWARD)\nX_CATALOG_ID(LS_POSE)\nX_CATALOG_ID(LS_FAST_BACK)\nX_CATALOG_ID(LS_TURN_RIGHT)\nX_CATALOG_ID(LS_TURN_LEFT)\nX_CATALOG_ID(LS_DEATH)\nX_CATALOG_ID(LS_FAST_FALL)\nX_CATALOG_ID(LS_HANG)\nX_CATALOG_ID(LS_REACH)\nX_CATALOG_ID(LS_SPLAT)\nX_CATALOG_ID(LS_TREAD)\nX_CATALOG_ID(LS_LAND)\nX_CATALOG_ID(LS_COMPRESS)\nX_CATALOG_ID(LS_WALK_BACK)\nX_CATALOG_ID(LS_SWIM)\nX_CATALOG_ID(LS_GLIDE)\nX_CATALOG_ID(LS_PULL_UP)\nX_CATALOG_ID(LS_FAST_TURN)\nX_CATALOG_ID(LS_STEP_RIGHT)\nX_CATALOG_ID(LS_STEP_LEFT)\nX_CATALOG_ID(LS_ROLL_CONT)\nX_CATALOG_ID(LS_SLIDE)\nX_CATALOG_ID(LS_JUMP_BACK)\nX_CATALOG_ID(LS_JUMP_RIGHT)\nX_CATALOG_ID(LS_JUMP_LEFT)\nX_CATALOG_ID(LS_JUMP_UP)\nX_CATALOG_ID(LS_FALL_BACK)\nX_CATALOG_ID(LS_SHIMMY_LEFT)\nX_CATALOG_ID(LS_SHIMMY_RIGHT)\nX_CATALOG_ID(LS_SLIDE_BACK)\nX_CATALOG_ID(LS_SURF_TREAD)\nX_CATALOG_ID(LS_SURF_SWIM)\nX_CATALOG_ID(LS_DIVE)\nX_CATALOG_ID(LS_PUSH_BLOCK)\nX_CATALOG_ID(LS_PULL_BLOCK)\nX_CATALOG_ID(LS_PP_READY)\nX_CATALOG_ID(LS_PICKUP)\nX_CATALOG_ID(LS_SWITCH_ON)\nX_CATALOG_ID(LS_SWITCH_OFF)\nX_CATALOG_ID(LS_USE_KEY)\nX_CATALOG_ID(LS_USE_PUZZLE)\nX_CATALOG_ID(LS_UW_DEATH)\nX_CATALOG_ID(LS_ROLL)\nX_CATALOG_ID(LS_SPECIAL)\nX_CATALOG_ID(LS_SURF_BACK)\nX_CATALOG_ID(LS_SURF_LEFT)\nX_CATALOG_ID(LS_SURF_RIGHT)\nX_CATALOG_ID(LS_USE_MIDAS)\nX_CATALOG_ID(LS_DIE_MIDAS)\nX_CATALOG_ID(LS_SWAN_DIVE)\nX_CATALOG_ID(LS_FAST_DIVE)\nX_CATALOG_ID(LS_GYMNAST)\nX_CATALOG_ID(LS_WATER_OUT)\nX_CATALOG_ID(LS_CONTROLLED)\nX_CATALOG_ID(LS_TWIST)\nX_CATALOG_ID(LS_WATER_ROLL)\nX_CATALOG_ID(LS_WADE)\nX_CATALOG_ID(LS_RESPONSIVE)\nX_CATALOG_ID(LS_NEUTRAL_ROLL)\nX_CATALOG_ID(LS_SPRINT)\nX_CATALOG_ID(LS_SPRINT_ROLL)\nX_CATALOG_ID(LS_POSE_START)\nX_CATALOG_ID(LS_POSE_END)\nX_CATALOG_ID(LS_POSE_LEFT)\nX_CATALOG_ID(LS_POSE_RIGHT)\nX_CATALOG_ID(LS_CLIMB_STANCE)\nX_CATALOG_ID(LS_CLIMBING)\nX_CATALOG_ID(LS_CLIMB_LEFT)\nX_CATALOG_ID(LS_CLIMB_END)\nX_CATALOG_ID(LS_CLIMB_RIGHT)\nX_CATALOG_ID(LS_CLIMB_DOWN)\nX_CATALOG_ID(LS_LARA_TEST1)\nX_CATALOG_ID(LS_LARA_TEST2)\nX_CATALOG_ID(LS_LARA_TEST3)\nX_CATALOG_ID(LS_FLARE_PICKUP)\nX_CATALOG_ID(LS_KICK)\nX_CATALOG_ID(LS_ZIPLINE)\nX_CATALOG_ID(LS_CROUCH_IDLE)\nX_CATALOG_ID(LS_CROUCH_ROLL)\nX_CATALOG_ID(LS_MONKEY_IDLE)\nX_CATALOG_ID(LS_MONKEY_FORWARD)\nX_CATALOG_ID(LS_MONKEY_LEFT)\nX_CATALOG_ID(LS_MONKEY_RIGHT)\nX_CATALOG_ID(LS_MONKEY_ROLL)\nX_CATALOG_ID(LS_CRAWL_IDLE)\nX_CATALOG_ID(LS_CRAWL_FORWARD)\nX_CATALOG_ID(LS_MONKEY_TURN_LEFT)\nX_CATALOG_ID(LS_MONKEY_TURN_RIGHT)\nX_CATALOG_ID(LS_CRAWL_TURN_LEFT)\nX_CATALOG_ID(LS_CRAWL_TURN_RIGHT)\nX_CATALOG_ID(LS_CRAWL_BACK)\nX_CATALOG_ID(LS_CLIMB_TO_CRAWL)\nX_CATALOG_ID(LS_CRAWL_TO_CLIMB)\nX_CATALOG_ID(LS_CRAWL_JUMP_DOWN)\nX_CATALOG_ID(LS_CROUCH_TURN_LEFT)\nX_CATALOG_ID(LS_CROUCH_TURN_RIGHT)\n"
  },
  {
    "path": "src/trx/game/catalog/manager.c",
    "content": "#include <trx/game/catalog/manager.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/items/actions/ids.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/music/ids.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/sound/ids.h>\n\n#include <ctype.h>\n#include <string.h>\n#include <uthash.h>\n\n// Compile-time table of catalog IDs and their name strings\ntypedef struct {\n    CATALOG_CONTEXT context;\n    CATALOG_ID id;\n    const char *name_str;\n} M_ENTRY;\n\nstatic const M_ENTRY m_CatalogEntryDefs[] = {\n#define X_CATALOG_ID(enum_value) { CATALOG_MUSIC, enum_value, #enum_value },\n#include <trx/game/catalog/music.def>\n#undef X_CATALOG_ID\n#define X_CATALOG_ID(enum_value) { CATALOG_OBJECTS, enum_value, #enum_value },\n#include <trx/game/catalog/objects.def>\n#undef X_CATALOG_ID\n#define X_CATALOG_ID(enum_value) { CATALOG_SAMPLES, enum_value, #enum_value },\n#include <trx/game/catalog/samples.def>\n#undef X_CATALOG_ID\n#define X_CATALOG_ID(enum_value)                                               \\\n    { CATALOG_LARA_STATES, enum_value, #enum_value },\n#include <trx/game/catalog/lara_states.def>\n#undef X_CATALOG_ID\n#define X_CATALOG_ID(enum_value)                                               \\\n    { CATALOG_LARA_ANIMS, enum_value, #enum_value },\n#include <trx/game/catalog/lara_anims.def>\n#undef X_CATALOG_ID\n#define X_CATALOG_ID(enum_value)                                               \\\n    { CATALOG_ITEM_ACTIONS, enum_value, #enum_value },\n#include <trx/game/catalog/item_actions.def>\n#undef X_CATALOG_ID\n};\n\n// Number of catalog entries\nstatic const size_t m_CatalogEntryCount = ARRAY_SIZE(m_CatalogEntryDefs);\n\n// Internal map from name to CATALOG_ID\ntypedef struct {\n    const char *name_str;\n    int32_t enum_value;\n    UT_hash_handle hh;\n} M_NAME_ENTRY;\nstatic M_NAME_ENTRY *m_Name2EnumMap[CATALOG_CONTEXT_MAX] = { nullptr };\n\n// Internal map from game ID to CATALOG_ID\ntypedef struct {\n    int32_t game_id;\n    int32_t enum_value;\n    UT_hash_handle hh;\n} M_GAME_ID_ENTRY;\nstatic M_GAME_ID_ENTRY *m_GameID2EnumMap[CATALOG_CONTEXT_MAX] = { nullptr };\n\n// Parsed game IDs arrays (dynamically sized)\nstatic int32_t **m_CatalogGameIDs = nullptr;\n\n// State flag\nstatic bool m_Initialized = false;\n\n// Helper: clear game_id->enum map\nstatic void M_ClearGameIDMap(M_GAME_ID_ENTRY **const map)\n{\n    M_GAME_ID_ENTRY *cur, *tmp;\n    HASH_ITER(hh, *map, cur, tmp)\n    {\n        HASH_DEL(*map, cur);\n        Memory_Free(cur);\n    }\n}\n\n// Helper: clear name->enum map\nstatic void M_ClearNameMap(M_NAME_ENTRY **const map)\n{\n    M_NAME_ENTRY *cur, *tmp;\n    HASH_ITER(hh, *map, cur, tmp)\n    {\n        HASH_DEL(*map, cur);\n        Memory_Free(cur);\n    }\n}\n\n// Trim leading/trailing whitespace in-place\nstatic char *M_StrTrim(char *s)\n{\n    while (*s && isspace(*s)) {\n        s++;\n    }\n    if (*s == '\\0') {\n        return s;\n    }\n    char *end = s + strlen(s) - 1;\n    while (end > s && isspace(*end)) {\n        *end-- = '\\0';\n    }\n    return s;\n}\n\n// Parse one CSV field into out buffer; advance *p past field\nstatic void M_ParseCSVField(\n    const char **const p, char *const out, const size_t out_sz)\n{\n    const char *src = *p;\n    char *dst = out;\n    const char *const end = out + out_sz - 1;\n    if (*src == '\"') {\n        src++;\n        while (*src && (*src != '\"' || (src[1] == '\"'))) {\n            if (*src == '\"' && src[1] == '\"') {\n                src++;\n            }\n            if (dst < end) {\n                *dst++ = *src;\n            }\n            src++;\n        }\n        if (*src == '\"') {\n            src++;\n        }\n    } else {\n        while (*src && *src != ',') {\n            if (dst < end) {\n                *dst++ = *src;\n            }\n            src++;\n        }\n    }\n    *dst = '\\0';\n    if (*src == ',') {\n        src++;\n    }\n    *p = src;\n}\n\n// Build initial maps on first load\nstatic void M_Initialize(void)\n{\n    const size_t count = m_CatalogEntryCount;\n    m_CatalogGameIDs =\n        Memory_Alloc(sizeof(*m_CatalogGameIDs) * CATALOG_CONTEXT_MAX);\n    for (size_t ctx = 0; ctx < CATALOG_CONTEXT_MAX; ctx++) {\n        m_CatalogGameIDs[ctx] =\n            Memory_Alloc(sizeof(*m_CatalogGameIDs[ctx]) * count);\n    }\n    for (size_t idx = 0; idx < count; idx++) {\n        const CATALOG_CONTEXT ctx = m_CatalogEntryDefs[idx].context;\n        const CATALOG_ID id = m_CatalogEntryDefs[idx].id;\n        m_CatalogGameIDs[ctx][id] = -1;\n        M_NAME_ENTRY *const entry = Memory_Alloc(sizeof(*entry));\n        entry->name_str = m_CatalogEntryDefs[idx].name_str;\n        entry->enum_value = id;\n        HASH_ADD_KEYPTR(\n            hh, m_Name2EnumMap[ctx], entry->name_str,\n            (uint32_t)strlen(entry->name_str), entry);\n    }\n    m_Initialized = true;\n}\n\nbool Catalog_Load(\n    const CATALOG_CONTEXT context, const char *const csv_path,\n    const bool allow_duplicates)\n{\n    if (!m_Initialized) {\n        M_Initialize();\n    }\n    char *file_data;\n    size_t file_size;\n    if (!File_Load(csv_path, &file_data, &file_size)) {\n        return false;\n    }\n\n    const char *pos = file_data;\n    const char *end = file_data + file_size;\n    char line[512];\n    while (pos < end) {\n        size_t len = 0;\n        while (pos < end && *pos != '\\n' && len + 1 < sizeof(line)) {\n            line[len++] = *pos++;\n        }\n        if (pos < end && *pos == '\\n') {\n            pos++;\n        }\n        line[len] = '\\0';\n        if (line[0] == '\\0' || line[0] == '#') {\n            continue;\n        }\n        const char *p = line;\n        char id_buf[32];\n        char name_buf[64];\n        M_ParseCSVField(&p, id_buf, sizeof(id_buf));\n        M_ParseCSVField(&p, name_buf, sizeof(name_buf));\n        char *const id_str = M_StrTrim(id_buf);\n        char *const name_str = M_StrTrim(name_buf);\n        const int32_t game_id = (int32_t)strtol(id_str, nullptr, 10);\n\n        CATALOG_ID id;\n        if (!Catalog_NameToEnum(context, name_str, &id)) {\n            continue;\n        }\n\n        m_CatalogGameIDs[context][id] = game_id;\n        if (game_id >= 0) {\n            M_GAME_ID_ENTRY *existing = nullptr;\n            HASH_FIND_INT(m_GameID2EnumMap[context], &game_id, existing);\n            if (existing == nullptr) {\n                M_GAME_ID_ENTRY *gentry = Memory_Alloc(sizeof(*gentry));\n                gentry->game_id = game_id;\n                gentry->enum_value = id;\n                HASH_ADD_INT(m_GameID2EnumMap[context], game_id, gentry);\n            } else if (!allow_duplicates) {\n                LOG_ERROR(\n                    \"Duplicate game ID %d for context %d\", game_id, context);\n            }\n        }\n    }\n    Memory_FreePointer(&file_data);\n    return true;\n}\n\nbool Catalog_NameToEnum(\n    const CATALOG_CONTEXT context, const char *name, CATALOG_ID *const out_id)\n{\n    M_NAME_ENTRY *entry = nullptr;\n    HASH_FIND_STR(m_Name2EnumMap[context], name, entry);\n    if (entry != nullptr) {\n        *out_id = (CATALOG_ID)entry->enum_value;\n        return true;\n    }\n    return false;\n}\n\nbool Catalog_EnumToGameID(\n    const CATALOG_CONTEXT context, const CATALOG_ID id,\n    int32_t *const out_game_id)\n{\n    if (id < 0 || (size_t)id >= m_CatalogEntryCount) {\n        return false;\n    }\n    const int32_t gid = m_CatalogGameIDs[context][id];\n    if (gid < 0) {\n        return false;\n    }\n    *out_game_id = gid;\n    return true;\n}\n\nbool Catalog_GameIDToEnum(\n    const CATALOG_CONTEXT context, const int32_t game_id,\n    CATALOG_ID *const out_id)\n{\n    M_GAME_ID_ENTRY *entry = nullptr;\n    HASH_FIND_INT(m_GameID2EnumMap[context], &game_id, entry);\n    if (entry != nullptr) {\n        *out_id = (CATALOG_ID)entry->enum_value;\n        return true;\n    }\n    return false;\n}\n\nvoid Catalog_Shutdown(void)\n{\n    if (!m_Initialized) {\n        return;\n    }\n    for (size_t ctx = 0; ctx < CATALOG_CONTEXT_MAX; ctx++) {\n        M_ClearGameIDMap(&m_GameID2EnumMap[ctx]);\n        M_ClearNameMap(&m_Name2EnumMap[ctx]);\n        Memory_Free(m_CatalogGameIDs[ctx]);\n    }\n    Memory_Free(m_CatalogGameIDs);\n    m_CatalogGameIDs = nullptr;\n    m_Initialized = false;\n}\n"
  },
  {
    "path": "src/trx/game/catalog/manager.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\n// Context discriminator for separate catalog namespaces\ntypedef enum CATALOG_CONTEXT {\n    CATALOG_OBJECTS,\n    CATALOG_MUSIC,\n    CATALOG_SAMPLES,\n    CATALOG_LARA_STATES,\n    CATALOG_LARA_ANIMS,\n    CATALOG_ITEM_ACTIONS,\n    CATALOG_CONTEXT_MAX,\n} CATALOG_CONTEXT;\n\ntypedef int32_t CATALOG_ID;\n\n// Load mappings for a specific context from a CSV file of the form:\n// game_id,name[,comment]\n// A game_id of -1 indicates no mapping for that entry.\n// Returns true on success.\nbool Catalog_Load(\n    CATALOG_CONTEXT context, const char *csv_path, bool allow_duplicates);\n\n// Convert an item name to its CATALOG_ID within a context.\n// Returns false if not found.\nbool Catalog_NameToEnum(\n    CATALOG_CONTEXT context, const char *name, CATALOG_ID *out_id);\n\n// Convert a CATALOG_ID to its game-specific ID within a context.\n// Returns false if unmapped.\nbool Catalog_EnumToGameID(\n    CATALOG_CONTEXT context, CATALOG_ID id, int32_t *out_game_id);\n\n// Convert a game-specific ID to its CATALOG_ID within a context.\n// Returns false if not found.\nbool Catalog_GameIDToEnum(\n    CATALOG_CONTEXT context, int32_t game_id, CATALOG_ID *out_id);\n\n// Free internal resources.\nvoid Catalog_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/catalog/music.def",
    "content": "// Common\nX_CATALOG_ID(MX_SECRET)\n\n// TR1\nX_CATALOG_ID(MX_TR1_GYM_HINT_03)\nX_CATALOG_ID(MX_TR1_GYM_HINT_04)\nX_CATALOG_ID(MX_TR1_GYM_HINT_12)\nX_CATALOG_ID(MX_TR1_GYM_HINT_14)\nX_CATALOG_ID(MX_TR1_GYM_HINT_15)\nX_CATALOG_ID(MX_TR1_GYM_HINT_16)\nX_CATALOG_ID(MX_TR1_GYM_HINT_17)\nX_CATALOG_ID(MX_TR1_GYM_HINT_18)\nX_CATALOG_ID(MX_TR1_GYM_HINT_24)\nX_CATALOG_ID(MX_TR1_GYM_HINT_25)\nX_CATALOG_ID(MX_BALDY_SPEECH)\nX_CATALOG_ID(MX_COWBOY_SPEECH)\nX_CATALOG_ID(MX_LARSON_SPEECH)\nX_CATALOG_ID(MX_NATLA_SPEECH)\nX_CATALOG_ID(MX_PIERRE_SPEECH)\nX_CATALOG_ID(MX_SKATEKID_SPEECH)\nX_CATALOG_ID(MX_UNUSED_0)\nX_CATALOG_ID(MX_UNUSED_1)\nX_CATALOG_ID(MX_UNUSED_2)\n\n// TR2\nX_CATALOG_ID(MX_TR2_GYM_HINT_14)\nX_CATALOG_ID(MX_TR2_GYM_HINT_15)\nX_CATALOG_ID(MX_TR2_GYM_HINT_16)\nX_CATALOG_ID(MX_TR2_GYM_HINT_17)\nX_CATALOG_ID(MX_DAGGER_PULL)\nX_CATALOG_ID(MX_CUTSCENE_BATH)\nX_CATALOG_ID(MX_REVEAL_1)\nX_CATALOG_ID(MX_REVEAL_2)\nX_CATALOG_ID(MX_SKIDOO_THEME)\nX_CATALOG_ID(MX_BATTLE_THEME)\n\n// TR3\nX_CATALOG_ID(MX_TR3_GYM_HINT_FAST_TIME)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_01)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_02)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_03)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_04)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_05)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_06)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_07)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_08)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_09)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_10)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_11)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_12)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_13)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_14)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_15)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_16)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_17)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_18)\nX_CATALOG_ID(MX_TR3_GYM_EXERCISE_19)\nX_CATALOG_ID(MX_MINE_CART_THEME)\nX_CATALOG_ID(MX_RIB_THEME)\n"
  },
  {
    "path": "src/trx/game/catalog/objects.def",
    "content": "X_CATALOG_ID(O_LARA)\nX_CATALOG_ID(O_LARA_PISTOLS)\nX_CATALOG_ID(O_LARA_SHOTGUN)\nX_CATALOG_ID(O_LARA_MAGNUMS)\nX_CATALOG_ID(O_LARA_UZIS)\nX_CATALOG_ID(O_LARA_EXTRA)\nX_CATALOG_ID(O_BACON_LARA)\nX_CATALOG_ID(O_WOLF)\nX_CATALOG_ID(O_BEAR)\nX_CATALOG_ID(O_BAT)\nX_CATALOG_ID(O_CROCODILE)\nX_CATALOG_ID(O_ALLIGATOR)\nX_CATALOG_ID(O_LION)\nX_CATALOG_ID(O_LIONESS)\nX_CATALOG_ID(O_PUMA)\nX_CATALOG_ID(O_APE)\nX_CATALOG_ID(O_RAT)\nX_CATALOG_ID(O_VOLE)\nX_CATALOG_ID(O_TREX)\nX_CATALOG_ID(O_RAPTOR)\nX_CATALOG_ID(O_COMPY)\nX_CATALOG_ID(O_ATLANTEAN_WINGED)\nX_CATALOG_ID(O_ATLANTEAN_SHOOTER)\nX_CATALOG_ID(O_ATLANTEAN_GROUND)\nX_CATALOG_ID(O_CENTAUR)\nX_CATALOG_ID(O_MUMMY)\nX_CATALOG_ID(O_DINO_WARRIOR)\nX_CATALOG_ID(O_FISH)\nX_CATALOG_ID(O_LARSON)\nX_CATALOG_ID(O_PIERRE)\nX_CATALOG_ID(O_SKATEBOARD)\nX_CATALOG_ID(O_SKATEKID)\nX_CATALOG_ID(O_COWBOY)\nX_CATALOG_ID(O_BALDY)\nX_CATALOG_ID(O_NATLA)\nX_CATALOG_ID(O_TORSO)\nX_CATALOG_ID(O_FALLING_BLOCK_1)\nX_CATALOG_ID(O_PENDULUM_1)\nX_CATALOG_ID(O_SPIKES)\nX_CATALOG_ID(O_ROLLING_BALL_1)\nX_CATALOG_ID(O_DART)\nX_CATALOG_ID(O_DART_EMITTER)\nX_CATALOG_ID(O_DISC)\nX_CATALOG_ID(O_DISC_EMITTER)\nX_CATALOG_ID(O_DRAWBRIDGE)\nX_CATALOG_ID(O_TEETH_TRAP)\nX_CATALOG_ID(O_DAMOCLES_SWORD)\nX_CATALOG_ID(O_THORS_HANDLE)\nX_CATALOG_ID(O_THORS_HEAD)\nX_CATALOG_ID(O_LIGHTNING_EMITTER)\nX_CATALOG_ID(O_MOVING_BAR)\nX_CATALOG_ID(O_MOVABLE_BLOCK_1)\nX_CATALOG_ID(O_MOVABLE_BLOCK_2)\nX_CATALOG_ID(O_MOVABLE_BLOCK_3)\nX_CATALOG_ID(O_MOVABLE_BLOCK_4)\nX_CATALOG_ID(O_SLIDING_PILLAR)\nX_CATALOG_ID(O_FALLING_CEILING_1)\nX_CATALOG_ID(O_FALLING_CEILING_2)\nX_CATALOG_ID(O_SWITCH_TYPE_NORMAL)\nX_CATALOG_ID(O_SWITCH_TYPE_UW)\nX_CATALOG_ID(O_DOOR_TYPE_1)\nX_CATALOG_ID(O_DOOR_TYPE_2)\nX_CATALOG_ID(O_DOOR_TYPE_3)\nX_CATALOG_ID(O_DOOR_TYPE_4)\nX_CATALOG_ID(O_DOOR_TYPE_5)\nX_CATALOG_ID(O_DOOR_TYPE_6)\nX_CATALOG_ID(O_DOOR_TYPE_7)\nX_CATALOG_ID(O_DOOR_TYPE_8)\nX_CATALOG_ID(O_TRAPDOOR_TYPE_1)\nX_CATALOG_ID(O_TRAPDOOR_TYPE_2)\nX_CATALOG_ID(O_TRAPDOOR_TYPE_3)\nX_CATALOG_ID(O_BRIDGE_FLAT)\nX_CATALOG_ID(O_BRIDGE_TILT_1)\nX_CATALOG_ID(O_BRIDGE_TILT_2)\nX_CATALOG_ID(O_PASSPORT_OPTION)\nX_CATALOG_ID(O_COMPASS_OPTION)\nX_CATALOG_ID(O_STOPWATCH_OPTION)\nX_CATALOG_ID(O_PHOTO_OPTION)\nX_CATALOG_ID(O_COG_1)\nX_CATALOG_ID(O_COG_2)\nX_CATALOG_ID(O_COG_3)\nX_CATALOG_ID(O_PLAYER_1)\nX_CATALOG_ID(O_PLAYER_2)\nX_CATALOG_ID(O_PLAYER_3)\nX_CATALOG_ID(O_PLAYER_4)\nX_CATALOG_ID(O_PASSPORT_CLOSED)\nX_CATALOG_ID(O_SAVE_CRYSTAL_ITEM)\nX_CATALOG_ID(O_PISTOL_ITEM)\nX_CATALOG_ID(O_SHOTGUN_ITEM)\nX_CATALOG_ID(O_MAGNUM_ITEM)\nX_CATALOG_ID(O_UZI_ITEM)\nX_CATALOG_ID(O_PISTOL_AMMO_ITEM)\nX_CATALOG_ID(O_SHOTGUN_AMMO_ITEM)\nX_CATALOG_ID(O_MAGNUM_AMMO_ITEM)\nX_CATALOG_ID(O_UZI_AMMO_ITEM)\nX_CATALOG_ID(O_EXPLOSIVE_ITEM)\nX_CATALOG_ID(O_SMALL_MEDIPACK_ITEM)\nX_CATALOG_ID(O_LARGE_MEDIPACK_ITEM)\nX_CATALOG_ID(O_DETAIL_OPTION)\nX_CATALOG_ID(O_SOUND_OPTION)\nX_CATALOG_ID(O_CONTROL_OPTION)\nX_CATALOG_ID(O_GAMMA_OPTION)\nX_CATALOG_ID(O_PISTOL_OPTION)\nX_CATALOG_ID(O_SHOTGUN_OPTION)\nX_CATALOG_ID(O_MAGNUM_OPTION)\nX_CATALOG_ID(O_UZI_OPTION)\nX_CATALOG_ID(O_PISTOL_AMMO_OPTION)\nX_CATALOG_ID(O_SHOTGUN_AMMO_OPTION)\nX_CATALOG_ID(O_MAGNUM_AMMO_OPTION)\nX_CATALOG_ID(O_UZI_AMMO_OPTION)\nX_CATALOG_ID(O_EXPLOSIVE_OPTION)\nX_CATALOG_ID(O_SMALL_MEDIPACK_OPTION)\nX_CATALOG_ID(O_LARGE_MEDIPACK_OPTION)\nX_CATALOG_ID(O_PUZZLE_ITEM_1)\nX_CATALOG_ID(O_PUZZLE_ITEM_2)\nX_CATALOG_ID(O_PUZZLE_ITEM_3)\nX_CATALOG_ID(O_PUZZLE_ITEM_4)\nX_CATALOG_ID(O_PUZZLE_OPTION_1)\nX_CATALOG_ID(O_PUZZLE_OPTION_2)\nX_CATALOG_ID(O_PUZZLE_OPTION_3)\nX_CATALOG_ID(O_PUZZLE_OPTION_4)\nX_CATALOG_ID(O_PUZZLE_HOLE_1)\nX_CATALOG_ID(O_PUZZLE_HOLE_2)\nX_CATALOG_ID(O_PUZZLE_HOLE_3)\nX_CATALOG_ID(O_PUZZLE_HOLE_4)\nX_CATALOG_ID(O_PUZZLE_DONE_1)\nX_CATALOG_ID(O_PUZZLE_DONE_2)\nX_CATALOG_ID(O_PUZZLE_DONE_3)\nX_CATALOG_ID(O_PUZZLE_DONE_4)\nX_CATALOG_ID(O_LEADBAR_ITEM)\nX_CATALOG_ID(O_LEADBAR_OPTION)\nX_CATALOG_ID(O_MIDAS_TOUCH)\nX_CATALOG_ID(O_KEY_ITEM_1)\nX_CATALOG_ID(O_KEY_ITEM_2)\nX_CATALOG_ID(O_KEY_ITEM_3)\nX_CATALOG_ID(O_KEY_ITEM_4)\nX_CATALOG_ID(O_KEY_OPTION_1)\nX_CATALOG_ID(O_KEY_OPTION_2)\nX_CATALOG_ID(O_KEY_OPTION_3)\nX_CATALOG_ID(O_KEY_OPTION_4)\nX_CATALOG_ID(O_KEY_HOLE_1)\nX_CATALOG_ID(O_KEY_HOLE_2)\nX_CATALOG_ID(O_KEY_HOLE_3)\nX_CATALOG_ID(O_KEY_HOLE_4)\nX_CATALOG_ID(O_PICKUP_ITEM_1)\nX_CATALOG_ID(O_PICKUP_ITEM_2)\nX_CATALOG_ID(O_SCION_ITEM_1)\nX_CATALOG_ID(O_SCION_ITEM_2)\nX_CATALOG_ID(O_SCION_ITEM_3)\nX_CATALOG_ID(O_SCION_ITEM_4)\nX_CATALOG_ID(O_SCION_HOLDER)\nX_CATALOG_ID(O_PICKUP_OPTION_1)\nX_CATALOG_ID(O_PICKUP_OPTION_2)\nX_CATALOG_ID(O_SCION_OPTION)\nX_CATALOG_ID(O_EXPLOSION_1)\nX_CATALOG_ID(O_EXPLOSION_2)\nX_CATALOG_ID(O_SPLASH_1)\nX_CATALOG_ID(O_SPLASH_2)\nX_CATALOG_ID(O_BUBBLE_1)\nX_CATALOG_ID(O_BUBBLE_2)\nX_CATALOG_ID(O_BUBBLE_EMITTER)\nX_CATALOG_ID(O_BLOOD)\nX_CATALOG_ID(O_BLOOD_PINK)\nX_CATALOG_ID(O_DART_EFFECT)\nX_CATALOG_ID(O_CENTAUR_STATUE)\nX_CATALOG_ID(O_PORTACABIN)\nX_CATALOG_ID(O_PODS)\nX_CATALOG_ID(O_RICOCHET)\nX_CATALOG_ID(O_TWINKLE)\nX_CATALOG_ID(O_GUN_FLASH)\nX_CATALOG_ID(O_DUST)\nX_CATALOG_ID(O_BODY_PART)\nX_CATALOG_ID(O_CAMERA_TARGET)\nX_CATALOG_ID(O_WATERFALL)\nX_CATALOG_ID(O_NATLA_GUN)\nX_CATALOG_ID(O_MISSILE_ATLANTEAN_SHARD)\nX_CATALOG_ID(O_MISSILE_ATLANTEAN_BOMB)\nX_CATALOG_ID(O_EMBER)\nX_CATALOG_ID(O_EMBER_EMITTER)\nX_CATALOG_ID(O_FLAME)\nX_CATALOG_ID(O_FLAME_EMITTER)\nX_CATALOG_ID(O_FLAME_EMITTER_BIG)\nX_CATALOG_ID(O_FLAME_EMITTER_SMALL)\nX_CATALOG_ID(O_FLAME_EMITTER_JET)\nX_CATALOG_ID(O_FLAME_EMITTER_SIDE)\nX_CATALOG_ID(O_LAVA_WEDGE)\nX_CATALOG_ID(O_BIG_POD)\nX_CATALOG_ID(O_MOTOR_BOAT)\nX_CATALOG_ID(O_EARTHQUAKE)\nX_CATALOG_ID(O_SKYBOX)\nX_CATALOG_ID(O_PICKUP_AID)\nX_CATALOG_ID(O_GLOW)\nX_CATALOG_ID(O_LARA_HAIR)\nX_CATALOG_ID(O_ALPHABET)\nX_CATALOG_ID(O_ALPHABET_SMALL)\nX_CATALOG_ID(O_LARA_M16)\nX_CATALOG_ID(O_LARA_GRENADE_GUN)\nX_CATALOG_ID(O_LARA_HARPOON_GUN)\nX_CATALOG_ID(O_LARA_FLARE)\nX_CATALOG_ID(O_LARA_SKIDOO)\nX_CATALOG_ID(O_LARA_BOAT)\nX_CATALOG_ID(O_SKIDOO_FAST)\nX_CATALOG_ID(O_BOAT)\nX_CATALOG_ID(O_DOG)\nX_CATALOG_ID(O_CULT_1)\nX_CATALOG_ID(O_CULT_1A)\nX_CATALOG_ID(O_CULT_1B)\nX_CATALOG_ID(O_CULT_2)\nX_CATALOG_ID(O_CULT_3)\nX_CATALOG_ID(O_MOUSE)\nX_CATALOG_ID(O_DRAGON_FRONT)\nX_CATALOG_ID(O_DRAGON_BACK)\nX_CATALOG_ID(O_GONDOLA)\nX_CATALOG_ID(O_SHARK)\nX_CATALOG_ID(O_EEL)\nX_CATALOG_ID(O_BIG_EEL)\nX_CATALOG_ID(O_BARRACUDA)\nX_CATALOG_ID(O_DIVER)\nX_CATALOG_ID(O_WORKER_1)\nX_CATALOG_ID(O_WORKER_2)\nX_CATALOG_ID(O_WORKER_3)\nX_CATALOG_ID(O_WORKER_4)\nX_CATALOG_ID(O_WORKER_5)\nX_CATALOG_ID(O_JELLY)\nX_CATALOG_ID(O_SPIDER)\nX_CATALOG_ID(O_BIG_SPIDER)\nX_CATALOG_ID(O_CROW)\nX_CATALOG_ID(O_TIGER)\nX_CATALOG_ID(O_BARTOLI)\nX_CATALOG_ID(O_XIAN_SPEARMAN)\nX_CATALOG_ID(O_XIAN_SPEARMAN_STATUE)\nX_CATALOG_ID(O_XIAN_KNIGHT)\nX_CATALOG_ID(O_XIAN_KNIGHT_STATUE)\nX_CATALOG_ID(O_YETI)\nX_CATALOG_ID(O_BIRD_GUARDIAN)\nX_CATALOG_ID(O_EAGLE)\nX_CATALOG_ID(O_BANDIT_1)\nX_CATALOG_ID(O_BANDIT_2)\nX_CATALOG_ID(O_BANDIT_2B)\nX_CATALOG_ID(O_SKIDOO_ARMED)\nX_CATALOG_ID(O_SKIDOO_DRIVER)\nX_CATALOG_ID(O_MONK_1)\nX_CATALOG_ID(O_MONK_2)\nX_CATALOG_ID(O_FALLING_BLOCK_2)\nX_CATALOG_ID(O_FALLING_BLOCK_3)\nX_CATALOG_ID(O_LIFT)\nX_CATALOG_ID(O_GENERAL)\nX_CATALOG_ID(O_BIG_BOWL)\nX_CATALOG_ID(O_SMASH_OBJECT_1)\nX_CATALOG_ID(O_SMASH_OBJECT_2)\nX_CATALOG_ID(O_SMASH_OBJECT_3)\nX_CATALOG_ID(O_SMASH_OBJECT_4)\nX_CATALOG_ID(O_PROPELLER_1)\nX_CATALOG_ID(O_POWER_SAW)\nX_CATALOG_ID(O_HOOK)\nX_CATALOG_ID(O_SPINNING_BLADE)\nX_CATALOG_ID(O_BLADE)\nX_CATALOG_ID(O_KILLER_STATUE)\nX_CATALOG_ID(O_ROLLING_BALL_2)\nX_CATALOG_ID(O_ICICLE)\nX_CATALOG_ID(O_SPIKE_WALL)\nX_CATALOG_ID(O_SPRINGBOARD)\nX_CATALOG_ID(O_CEILING_SPIKES)\nX_CATALOG_ID(O_BELL)\nX_CATALOG_ID(O_WATER_SPRITE)\nX_CATALOG_ID(O_SNOW_SPRITE)\nX_CATALOG_ID(O_SKIDOO_TRACK)\nX_CATALOG_ID(O_SWITCH_TYPE_AIRLOCK)\nX_CATALOG_ID(O_SWITCH_TYPE_SMALL)\nX_CATALOG_ID(O_PROPELLER_2)\nX_CATALOG_ID(O_PROPELLER_3)\nX_CATALOG_ID(O_PENDULUM_2)\nX_CATALOG_ID(O_MESH_SWAP_1)\nX_CATALOG_ID(O_MESH_SWAP_2)\nX_CATALOG_ID(O_TEXT_BOX)\nX_CATALOG_ID(O_ROLLING_BALL_3)\nX_CATALOG_ID(O_ZIPLINE_HANDLE)\nX_CATALOG_ID(O_SWITCH_TYPE_BUTTON)\nX_CATALOG_ID(O_PLAYER_5)\nX_CATALOG_ID(O_PLAYER_6)\nX_CATALOG_ID(O_PLAYER_7)\nX_CATALOG_ID(O_PLAYER_8)\nX_CATALOG_ID(O_PLAYER_9)\nX_CATALOG_ID(O_PLAYER_10)\nX_CATALOG_ID(O_PDA_OPTION)\nX_CATALOG_ID(O_HARPOON_ITEM)\nX_CATALOG_ID(O_M16_ITEM)\nX_CATALOG_ID(O_GRENADE_GUN_ITEM)\nX_CATALOG_ID(O_HARPOON_AMMO_ITEM)\nX_CATALOG_ID(O_M16_AMMO_ITEM)\nX_CATALOG_ID(O_GRENADE_AMMO_ITEM)\nX_CATALOG_ID(O_FLAREBOX_ITEM)\nX_CATALOG_ID(O_FLARE_ITEM)\nX_CATALOG_ID(O_HARPOON_OPTION)\nX_CATALOG_ID(O_M16_OPTION)\nX_CATALOG_ID(O_GRENADE_GUN_OPTION)\nX_CATALOG_ID(O_HARPOON_AMMO_OPTION)\nX_CATALOG_ID(O_M16_AMMO_OPTION)\nX_CATALOG_ID(O_GRENADE_AMMO_OPTION)\nX_CATALOG_ID(O_FLAREBOX_OPTION)\nX_CATALOG_ID(O_SECRET_1)\nX_CATALOG_ID(O_SECRET_2)\nX_CATALOG_ID(O_SECRET_3)\nX_CATALOG_ID(O_SPHERE_OF_DOOM_1)\nX_CATALOG_ID(O_SPHERE_OF_DOOM_2)\nX_CATALOG_ID(O_SPHERE_OF_DOOM_3)\nX_CATALOG_ID(O_ALARM_SOUND)\nX_CATALOG_ID(O_BIRD_TWEETER_1)\nX_CATALOG_ID(O_BIRD_TWEETER_2)\nX_CATALOG_ID(O_CLOCK_CHIMES)\nX_CATALOG_ID(O_DRAGON_BONES_1)\nX_CATALOG_ID(O_DRAGON_BONES_2)\nX_CATALOG_ID(O_DRAGON_BONES_3)\nX_CATALOG_ID(O_HOT_LIQUID)\nX_CATALOG_ID(O_SHADOW)\nX_CATALOG_ID(O_BOAT_BITS)\nX_CATALOG_ID(O_MINE)\nX_CATALOG_ID(O_INV_BACKGROUND)\nX_CATALOG_ID(O_FX_RESERVED)\nX_CATALOG_ID(O_GONG_BONGER)\nX_CATALOG_ID(O_GONG)\nX_CATALOG_ID(O_DETONATOR_BOX)\nX_CATALOG_ID(O_COPTER)\nX_CATALOG_ID(O_FLARE_FIRE)\nX_CATALOG_ID(O_GLOW_RESERVED)\nX_CATALOG_ID(O_M16_FLASH)\nX_CATALOG_ID(O_GUN_SHELL)\nX_CATALOG_ID(O_SHOTGUN_SHELL)\nX_CATALOG_ID(O_MISSILE_HARPOON)\nX_CATALOG_ID(O_MISSILE_POISON)\nX_CATALOG_ID(O_MISSILE_FLAME)\nX_CATALOG_ID(O_MISSILE_KNIFE)\nX_CATALOG_ID(O_GRENADE)\nX_CATALOG_ID(O_HARPOON_BOLT)\nX_CATALOG_ID(O_DYING_MONK)\nX_CATALOG_ID(O_DING_DONG)\nX_CATALOG_ID(O_LARA_ALARM)\nX_CATALOG_ID(O_MINI_COPTER)\nX_CATALOG_ID(O_WINSTON)\nX_CATALOG_ID(O_WINSTON_ARMY)\nX_CATALOG_ID(O_ASSAULT_DIGITS)\nX_CATALOG_ID(O_COMBAT_END)\nX_CATALOG_ID(O_CUT_SHOTGUN)\nX_CATALOG_ID(O_MONK_3)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_1)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_2)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_3)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_4)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_5)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_6)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_7)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_8)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_9)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_10)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_11)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_12)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_13)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_14)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_15)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_16)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_17)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_18)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_19)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_20)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_21)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_22)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_23)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_24)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_25)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_26)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_27)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_28)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_29)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_30)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_31)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_32)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_EXTRA)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_GUNS)\nX_CATALOG_ID(O_LARA_SKIN_SWAP_LEGS)\nX_CATALOG_ID(O_LARA_SKIN)\nX_CATALOG_ID(O_COBRA)\nX_CATALOG_ID(O_SWINGING_AXE)\nX_CATALOG_ID(O_SECRET_1_OPTION)\nX_CATALOG_ID(O_SECRET_2_OPTION)\nX_CATALOG_ID(O_SECRET_3_OPTION)\nX_CATALOG_ID(O_LARA_AUTOS)\nX_CATALOG_ID(O_AUTOS_ITEM)\nX_CATALOG_ID(O_AUTOS_AMMO_ITEM)\nX_CATALOG_ID(O_AUTOS_OPTION)\nX_CATALOG_ID(O_AUTOS_AMMO_OPTION)\nX_CATALOG_ID(O_LARA_DESERT_EAGLE)\nX_CATALOG_ID(O_DESERT_EAGLE_ITEM)\nX_CATALOG_ID(O_DESERT_EAGLE_AMMO_ITEM)\nX_CATALOG_ID(O_DESERT_EAGLE_OPTION)\nX_CATALOG_ID(O_DESERT_EAGLE_AMMO_OPTION)\nX_CATALOG_ID(O_LARA_MP5)\nX_CATALOG_ID(O_MP5_ITEM)\nX_CATALOG_ID(O_MP5_AMMO_ITEM)\nX_CATALOG_ID(O_MP5_OPTION)\nX_CATALOG_ID(O_MP5_AMMO_OPTION)\nX_CATALOG_ID(O_LARA_ROCKET_GUN)\nX_CATALOG_ID(O_ROCKET_GUN_ITEM)\nX_CATALOG_ID(O_ROCKET_AMMO_ITEM)\nX_CATALOG_ID(O_ROCKET_GUN_OPTION)\nX_CATALOG_ID(O_ROCKET_AMMO_OPTION)\nX_CATALOG_ID(O_ROCKET)\nX_CATALOG_ID(O_HEAVY_ROCKET)\nX_CATALOG_ID(O_DUMMY)\nX_CATALOG_ID(O_SNOWFLAKE)\nX_CATALOG_ID(O_ANIMATING_1)\nX_CATALOG_ID(O_ANIMATING_2)\nX_CATALOG_ID(O_ANIMATING_3)\nX_CATALOG_ID(O_ANIMATING_4)\nX_CATALOG_ID(O_ANIMATING_5)\nX_CATALOG_ID(O_ANIMATING_6)\nX_CATALOG_ID(O_ANIMATING_7)\nX_CATALOG_ID(O_ANIMATING_8)\nX_CATALOG_ID(O_ANIMATING_9)\nX_CATALOG_ID(O_ANIMATING_10)\nX_CATALOG_ID(O_SMOKE_EMITTER_WHITE)\nX_CATALOG_ID(O_SMOKE_EMITTER_BLACK)\nX_CATALOG_ID(O_STEAM_EMITTER)\nX_CATALOG_ID(O_ASSAULT_TARGET)\nX_CATALOG_ID(O_ELECTRICAL_LIGHT)\nX_CATALOG_ID(O_FLICKERING_LIGHT)\nX_CATALOG_ID(O_STROBE_LIGHT)\nX_CATALOG_ID(O_ON_OFF_LIGHT)\nX_CATALOG_ID(O_PULSE_LIGHT)\nX_CATALOG_ID(O_BEACON_LIGHT)\nX_CATALOG_ID(O_RED_LIGHT)\nX_CATALOG_ID(O_GREEN_LIGHT)\nX_CATALOG_ID(O_BLUE_LIGHT)\nX_CATALOG_ID(O_AMBER_LIGHT)\nX_CATALOG_ID(O_WHITE_LIGHT)\nX_CATALOG_ID(O_QUAD_BIKE)\nX_CATALOG_ID(O_KAYAK)\nX_CATALOG_ID(O_LARA_VEHICLE_ANIM)\nX_CATALOG_ID(O_MOUNTED_GUN)\nX_CATALOG_ID(O_TROPICAL_FISH)\nX_CATALOG_ID(O_PIRAHNAS)\nX_CATALOG_ID(O_QUEST_ITEM_1)\nX_CATALOG_ID(O_QUEST_ITEM_2)\nX_CATALOG_ID(O_QUEST_ITEM_3)\nX_CATALOG_ID(O_QUEST_ITEM_4)\nX_CATALOG_ID(O_QUEST_OPTION_1)\nX_CATALOG_ID(O_QUEST_OPTION_2)\nX_CATALOG_ID(O_QUEST_OPTION_3)\nX_CATALOG_ID(O_QUEST_OPTION_4)\nX_CATALOG_ID(O_AI_AMBUSH)\nX_CATALOG_ID(O_AI_GUARD)\nX_CATALOG_ID(O_AI_FOLLOW)\nX_CATALOG_ID(O_AI_PATROL_1)\nX_CATALOG_ID(O_AI_PATROL_2)\nX_CATALOG_ID(O_AI_MODIFY)\nX_CATALOG_ID(O_AI_X1)\nX_CATALOG_ID(O_AI_X2)\nX_CATALOG_ID(O_AI_X3)\nX_CATALOG_ID(O_TONY)\nX_CATALOG_ID(O_TONY_FIRE_BALL)\nX_CATALOG_ID(O_WILLARD)\nX_CATALOG_ID(O_WILLARD_PLASMA_BALL)\nX_CATALOG_ID(O_GLOBE_SELECT_OPTION)\nX_CATALOG_ID(O_KILL_ALL_TRIGGERED)\nX_CATALOG_ID(O_MONKEY)\nX_CATALOG_ID(O_MESH_SWAP_3)\nX_CATALOG_ID(O_VULTURE)\nX_CATALOG_ID(O_ROLLING_BALL_4)\nX_CATALOG_ID(O_POISON_DART_EMITTER)\nX_CATALOG_ID(O_POISON_DART)\nX_CATALOG_ID(O_SHIVA)\nX_CATALOG_ID(O_ELECTRIC_FENCE)\nX_CATALOG_ID(O_SWITCH_TYPE_WHEEL)\nX_CATALOG_ID(O_TRIBE_AXEMAN)\nX_CATALOG_ID(O_TRIBE_BOSS)\nX_CATALOG_ID(O_TRIBE_PIPEMAN)\nX_CATALOG_ID(O_LIZARD)\nX_CATALOG_ID(O_CARCASS)\nX_CATALOG_ID(O_BAT_EMITTER)\nX_CATALOG_ID(O_TREX_ALPHA)\nX_CATALOG_ID(O_RAPTOR_EMITTER)\nX_CATALOG_ID(O_STHPAC_MERCENARY)\nX_CATALOG_ID(O_TRAIN)\nX_CATALOG_ID(O_PATROL_DOG)\nX_CATALOG_ID(O_HUSKIE)\nX_CATALOG_ID(O_ELECTRIC_CLEANER)\nX_CATALOG_ID(O_PUNK_1)\nX_CATALOG_ID(O_PUNK_2)\nX_CATALOG_ID(O_UPV)\nX_CATALOG_ID(O_SECURITY_GUARD)\nX_CATALOG_ID(O_GAS_EMITTER_GREEN)\nX_CATALOG_ID(O_SWAT_1)\nX_CATALOG_ID(O_SWAT_2)\nX_CATALOG_ID(O_SWAT_3)\nX_CATALOG_ID(O_FUSE_BOX)\nX_CATALOG_ID(O_SOPHIA)\nX_CATALOG_ID(O_SOPHIA_LASER_BOLT)\nX_CATALOG_ID(O_SOPHIA_PLASMA_BALL)\nX_CATALOG_ID(O_TRIGGER_GATE)\nX_CATALOG_ID(O_ROTATING_LASER)\nX_CATALOG_ID(O_SECURITY_LASER_ALARM)\nX_CATALOG_ID(O_SECURITY_LASER_DEADLY)\nX_CATALOG_ID(O_SECURITY_LASER_KILLER)\nX_CATALOG_ID(O_SENTRY_GUN)\nX_CATALOG_ID(O_CIVILIAN)\nX_CATALOG_ID(O_PRISONER)\nX_CATALOG_ID(O_MP_1)\nX_CATALOG_ID(O_MP_2)\nX_CATALOG_ID(O_ORCA)\nX_CATALOG_ID(O_AREA_51_ROCKET)\nX_CATALOG_ID(O_AREA_51_ROCKET_BLAST)\nX_CATALOG_ID(O_AREA_51_ROCKET_SUPPORT)\nX_CATALOG_ID(O_MINE_CART)\nX_CATALOG_ID(O_RX_WORKER_1)\nX_CATALOG_ID(O_RX_WORKER_2)\nX_CATALOG_ID(O_RX_WORKER_3)\nX_CATALOG_ID(O_CRAWLER_MUTANT)\nX_CATALOG_ID(O_DYING_MUTANT)\nX_CATALOG_ID(O_HYBRID_MUTANT)\nX_CATALOG_ID(O_WASP_MUTANT)\nX_CATALOG_ID(O_WASP_MUTANT_EMITTER)\nX_CATALOG_ID(O_CLAW_MUTANT)\nX_CATALOG_ID(O_CLAW_MUTANT_PLASMA_BALL)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_1)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_2)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_3)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_4)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_5)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_6)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_7)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_8)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_9)\nX_CATALOG_ID(O_DISPOSABLE_ANIMATING_10)\nX_CATALOG_ID(O_FIRE_HEAD)\nX_CATALOG_ID(O_RIB)\n"
  },
  {
    "path": "src/trx/game/catalog/samples.def",
    "content": "X_CATALOG_ID(SFX_LARA_NO)\nX_CATALOG_ID(SFX_LARA_DRAW)\nX_CATALOG_ID(SFX_LARA_HOLSTER)\nX_CATALOG_ID(SFX_LARA_PISTOLS)\nX_CATALOG_ID(SFX_LARA_RELOAD)\nX_CATALOG_ID(SFX_LARA_RICOCHET)\nX_CATALOG_ID(SFX_BEAR_FEET)\nX_CATALOG_ID(SFX_BEAR_SNARL)\nX_CATALOG_ID(SFX_BEAR_HURT)\nX_CATALOG_ID(SFX_WOLF_HURT)\nX_CATALOG_ID(SFX_LARA_CLIMB_3)\nX_CATALOG_ID(SFX_LARA_BODYSL)\nX_CATALOG_ID(SFX_LARA_FALL)\nX_CATALOG_ID(SFX_LARA_INJURY)\nX_CATALOG_ID(SFX_LARA_SPLASH)\nX_CATALOG_ID(SFX_LARA_BREATH)\nX_CATALOG_ID(SFX_LARA_BUBBLES)\nX_CATALOG_ID(SFX_LARA_KEY)\nX_CATALOG_ID(SFX_LARA_UZI_FIRE)\nX_CATALOG_ID(SFX_LARA_UZI_STOP)\nX_CATALOG_ID(SFX_LARA_GENERAL_DEATH)\nX_CATALOG_ID(SFX_LARA_FALL_DEATH)\nX_CATALOG_ID(SFX_LARA_MAGNUMS)\nX_CATALOG_ID(SFX_LARA_AUTOS)\nX_CATALOG_ID(SFX_LARA_DESERT_EAGLE)\nX_CATALOG_ID(SFX_LARA_SHOTGUN)\nX_CATALOG_ID(SFX_LARA_EMPTY)\nX_CATALOG_ID(SFX_LARA_SHOTGUN_SHELL)\nX_CATALOG_ID(SFX_LARA_BULLETHIT)\nX_CATALOG_ID(SFX_UNDERWATER)\nX_CATALOG_ID(SFX_PUSHBLOCK_LAND)\nX_CATALOG_ID(SFX_EARTHQUAKE_1)\nX_CATALOG_ID(SFX_EARTHQUAKE_2)\nX_CATALOG_ID(SFX_EARTHQUAKE_LOOP)\nX_CATALOG_ID(SFX_WATERFALL_LOOP)\nX_CATALOG_ID(SFX_WATERFALL_BIG)\nX_CATALOG_ID(SFX_FLOOD)\nX_CATALOG_ID(SFX_LION_HURT)\nX_CATALOG_ID(SFX_RAT_CHIRP)\nX_CATALOG_ID(SFX_THUNDER)\nX_CATALOG_ID(SFX_EXPLOSION_2)\nX_CATALOG_ID(SFX_DAMOCLES_SWORD)\nX_CATALOG_ID(SFX_EXPLOSION_1)\nX_CATALOG_ID(SFX_MENU_ROTATE)\nX_CATALOG_ID(SFX_MENU_LARA_HOME)\nX_CATALOG_ID(SFX_MENU_GAMEBOY)\nX_CATALOG_ID(SFX_MENU_SPININ)\nX_CATALOG_ID(SFX_MENU_SPINOUT)\nX_CATALOG_ID(SFX_MENU_COMPASS)\nX_CATALOG_ID(SFX_MENU_GUNS)\nX_CATALOG_ID(SFX_MENU_PASSPORT)\nX_CATALOG_ID(SFX_MENU_MEDI)\nX_CATALOG_ID(SFX_MENU_CHOOSE)\nX_CATALOG_ID(SFX_RAISINGBLOCK_FX)\nX_CATALOG_ID(SFX_SAND_FX)\nX_CATALOG_ID(SFX_STAIRS_2_SLOPE_FX)\nX_CATALOG_ID(SFX_ATLANTEAN_NEEDLE)\nX_CATALOG_ID(SFX_SKATEBOARD_HIT)\nX_CATALOG_ID(SFX_TORSO_HIT)\nX_CATALOG_ID(SFX_ROLLING_BALL_1_ROLL)\nX_CATALOG_ID(SFX_ROLLING_BALL_1_STOP)\nX_CATALOG_ID(SFX_LAVA_FOUNTAIN)\nX_CATALOG_ID(SFX_LOOP_FOR_SMALL_FIRES)\nX_CATALOG_ID(SFX_DART)\nX_CATALOG_ID(SFX_DISC)\nX_CATALOG_ID(SFX_POWERUP_FX)\nX_CATALOG_ID(SFX_TRAPDOOR_OPEN)\nX_CATALOG_ID(SFX_EXPLOSION_FX)\nX_CATALOG_ID(SFX_ATLANTEAN_DEATH)\nX_CATALOG_ID(SFX_CHAINBLOCK_FX)\nX_CATALOG_ID(SFX_SECRET)\nX_CATALOG_ID(SFX_BALDY_SPEECH)\nX_CATALOG_ID(SFX_COWBOY_SPEECH)\nX_CATALOG_ID(SFX_LARSON_SPEECH)\nX_CATALOG_ID(SFX_NATLA_SPEECH)\nX_CATALOG_ID(SFX_PIERRE_SPEECH)\nX_CATALOG_ID(SFX_SKATEKID_SPEECH)\nX_CATALOG_ID(SFX_LARA_FLARE_IGNITE)\nX_CATALOG_ID(SFX_LARA_FLARE_BURN)\nX_CATALOG_ID(SFX_MASSIVE_CRASH)\nX_CATALOG_ID(SFX_CLICK)\nX_CATALOG_ID(SFX_GLASS_BREAK)\nX_CATALOG_ID(SFX_ENEMY_HIT_1)\nX_CATALOG_ID(SFX_ENEMY_HIT_2)\nX_CATALOG_ID(SFX_M16_FIRE)\nX_CATALOG_ID(SFX_M16_STOP)\nX_CATALOG_ID(SFX_MP5_FIRE)\nX_CATALOG_ID(SFX_ROCKET_FIRE)\nX_CATALOG_ID(SFX_MENU_STOPWATCH)\nX_CATALOG_ID(SFX_SANDBAG_HIT)\nX_CATALOG_ID(SFX_SKIDOO_IDLE)\nX_CATALOG_ID(SFX_SKIDOO_MOVING)\nX_CATALOG_ID(SFX_PULLEY_CRANE)\nX_CATALOG_ID(SFX_CURTAIN)\nX_CATALOG_ID(SFX_BOAT_IDLE)\nX_CATALOG_ID(SFX_BOAT_MOVING)\nX_CATALOG_ID(SFX_CLATTER_1)\nX_CATALOG_ID(SFX_CLATTER_2)\nX_CATALOG_ID(SFX_CLATTER_3)\nX_CATALOG_ID(SFX_SPIKE_WALL)\nX_CATALOG_ID(SFX_LARA_FLESH_WOUND)\nX_CATALOG_ID(SFX_SAW_REVVING)\nX_CATALOG_ID(SFX_SAW_STOP)\nX_CATALOG_ID(SFX_DOOR_CHIME)\nX_CATALOG_ID(SFX_AIRPLANE_IDLE)\nX_CATALOG_ID(SFX_UNDERWATER_FAN_ON)\nX_CATALOG_ID(SFX_UNDERWATER_FAN_OFF)\nX_CATALOG_ID(SFX_SMALL_FAN_ON)\nX_CATALOG_ID(SFX_ROLLING_BALL_2_ROLL)\nX_CATALOG_ID(SFX_ROLLING_BALL_2_STOP)\nX_CATALOG_ID(SFX_ROLLING_BALL_3_ROLL)\nX_CATALOG_ID(SFX_ROLLING_BALL_3_STOP)\nX_CATALOG_ID(SFX_ROLLING_BLADE)\nX_CATALOG_ID(SFX_MONK_CRUNCH)\nX_CATALOG_ID(SFX_PROJECTILE_HIT)\nX_CATALOG_ID(SFX_CHAIN_PULLEY)\nX_CATALOG_ID(SFX_ZIPLINE_GO)\nX_CATALOG_ID(SFX_ZIPLINE_STOP)\nX_CATALOG_ID(SFX_BOWL_POUR)\nX_CATALOG_ID(SFX_WATERFALL_2)\nX_CATALOG_ID(SFX_HELICOPTER_LOOP)\nX_CATALOG_ID(SFX_DRAGON_FEET)\nX_CATALOG_ID(SFX_DRAGON_FIRE)\nX_CATALOG_ID(SFX_WARRIOR_HOVER)\nX_CATALOG_ID(SFX_BIRDS_CHIRP)\nX_CATALOG_ID(SFX_CRUNCH_1)\nX_CATALOG_ID(SFX_CRUNCH_2)\nX_CATALOG_ID(SFX_DRIPS_REVERB)\nX_CATALOG_ID(SFX_STAGE_BACKDROP)\nX_CATALOG_ID(SFX_STONE_DOOR_SLIDE)\nX_CATALOG_ID(SFX_PLATFORM_ALARM)\nX_CATALOG_ID(SFX_DOORBELL)\nX_CATALOG_ID(SFX_BURGLAR_ALARM)\nX_CATALOG_ID(SFX_BOAT_ENGINE)\nX_CATALOG_ID(SFX_BOAT_INTO_WATER)\nX_CATALOG_ID(SFX_BOILER)\nX_CATALOG_ID(SFX_MARCO_BARTOLLI_TRANSFORM)\nX_CATALOG_ID(SFX_WINSTON_GRUNT_1)\nX_CATALOG_ID(SFX_WINSTON_GRUNT_2)\nX_CATALOG_ID(SFX_WINSTON_GRUNT_3)\nX_CATALOG_ID(SFX_WINSTON_CUPS)\nX_CATALOG_ID(SFX_BRITTLE_GROUND_BREAK)\nX_CATALOG_ID(SFX_SPIDER_EXPLODE)\nX_CATALOG_ID(SFX_FOOTSTEPS_MUD)\nX_CATALOG_ID(SFX_FOOTSTEPS_ICE)\nX_CATALOG_ID(SFX_FOOTSTEPS_GRAVEL)\nX_CATALOG_ID(SFX_FOOTSTEPS_SAND_OR_GRASS)\nX_CATALOG_ID(SFX_FOOTSTEPS_WOOD)\nX_CATALOG_ID(SFX_FOOTSTEPS_SNOW)\nX_CATALOG_ID(SFX_FOOTSTEPS_METAL)\nX_CATALOG_ID(SFX_TARGET_HITS)\nX_CATALOG_ID(SFX_TARGET_SMASH)\nX_CATALOG_ID(SFX_QUAD_FRONT_IMPACT)\nX_CATALOG_ID(SFX_QUAD_MOVE)\nX_CATALOG_ID(SFX_QUAD_IDLE)\nX_CATALOG_ID(SFX_EXPLOSION_3)\nX_CATALOG_ID(SFX_SAVE_CRYSTAL)\nX_CATALOG_ID(SFX_ICICLE)\nX_CATALOG_ID(SFX_BLAST_CIRCLE)\nX_CATALOG_ID(SFX_LARA_GET_OUT)\nX_CATALOG_ID(SFX_SHIVA_SWORD_1)\nX_CATALOG_ID(SFX_SHIVA_SWORD_2)\nX_CATALOG_ID(SFX_ROLLING_BALL_4_ROLL)\nX_CATALOG_ID(SFX_ROLLING_BALL_4_STOP)\nX_CATALOG_ID(SFX_BLOWPIPE_BLOW)\nX_CATALOG_ID(SFX_ALARM_1)\nX_CATALOG_ID(SFX_MACAQUE_ROLL)\nX_CATALOG_ID(SFX_LARA_FOOTSTEP)\nX_CATALOG_ID(SFX_LARA_BAREFOOT)\nX_CATALOG_ID(SFX_LARA_THUD)\nX_CATALOG_ID(SFX_BATS_1)\nX_CATALOG_ID(SFX_SHUTTERS_BREAK)\nX_CATALOG_ID(SFX_TRIBOSS_TAKE_HIT)\nX_CATALOG_ID(SFX_TRIBOSS_TURN_CHAIR)\nX_CATALOG_ID(SFX_AMERICAN_HOY)\nX_CATALOG_ID(SFX_ENGLISH_HOY)\nX_CATALOG_ID(SFX_TRAIN_LOOP)\nX_CATALOG_ID(SFX_CLEANER_FUSEBOX)\nX_CATALOG_ID(SFX_CLEANER_LOOP)\nX_CATALOG_ID(SFX_UPV_LOOP)\nX_CATALOG_ID(SFX_UPV_START)\nX_CATALOG_ID(SFX_UPV_STOP)\nX_CATALOG_ID(SFX_UPV_HARPOON)\nX_CATALOG_ID(SFX_SECURITY_GUARD_FIRE)\nX_CATALOG_ID(SFX_LONDON_SWAT_FIRE)\nX_CATALOG_ID(SFX_AMERICAN_SWAT_FIRE)\nX_CATALOG_ID(SFX_SOPHIA_SUMMON)\nX_CATALOG_ID(SFX_SOPHIA_TAKE_HIT)\nX_CATALOG_ID(SFX_SOPHIA_SUMMON_NOT)\nX_CATALOG_ID(SFX_LOWERING_BLOCK)\nX_CATALOG_ID(SFX_HUGE_ROCKET_LOOP)\nX_CATALOG_ID(SFX_SPANNER_CLUNK)\nX_CATALOG_ID(SFX_MINE_CART_CLUNK_START)\nX_CATALOG_ID(SFX_MINE_CART_SREECH_BRAKE)\nX_CATALOG_ID(SFX_MINE_CART_TRACK_LOOP)\nX_CATALOG_ID(SFX_MINE_CART_PULLY_LOOP)\nX_CATALOG_ID(SFX_FLAME_THROWER_LOOP)\nX_CATALOG_ID(SFX_RIB_MOVING)\nX_CATALOG_ID(SFX_RIB_IDLE)\n"
  },
  {
    "path": "src/trx/game/clock/common.c",
    "content": "#include <trx/game/clock/common.h>\n\n#include <trx/config.h>\n#include <trx/game/clock/const.h>\n#include <trx/game/clock/timer.h>\n#include <trx/game/clock/turbo.h>\n\n#include <SDL2/SDL_stdinc.h>\n#include <SDL2/SDL_timer.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <time.h>\n\nstatic bool m_Disabled = false;\nstatic Uint64 m_LastCounter = 0;\nstatic Uint64 m_InitCounter = 0;\nstatic Uint64 m_Frequency = 0;\nstatic double m_Accumulator = 0.0;\nstatic struct {\n    double real_time_at_last_change;\n    double sim_time_at_last_change;\n    double sim_speed;\n} m_Priv;\n\n// Fixed‐FPS simulation in headless mode\nstatic bool m_HeadlessFixedFPS = false;\nstatic double m_HeadlessFPS_DT = 0.0;\nstatic double m_HeadlessOffset = 0.0;\nstatic double m_HeadlessAnchor = 0.0;\n\nstatic double M_GetHighPrecisionCounter(void)\n{\n    if (m_HeadlessFixedFPS) {\n        // Return virtual time = anchor + offset\n        return m_HeadlessAnchor + m_HeadlessOffset;\n    }\n    return (SDL_GetPerformanceCounter() - m_InitCounter) / (double)m_Frequency;\n}\n\nvoid Clock_Init(void)\n{\n    m_Frequency = SDL_GetPerformanceFrequency();\n    m_InitCounter = SDL_GetPerformanceCounter();\n}\n\nvoid Clock_DisableWait(void)\n{\n    m_Disabled = true;\n}\n\nvoid Clock_EnableHeadlessFixedFPS(int32_t fps)\n{\n    if (fps <= 0) {\n        m_HeadlessFixedFPS = false;\n        return;\n    }\n\n    // Anchor to current real time, reset offset\n    m_HeadlessAnchor = M_GetHighPrecisionCounter();\n    m_HeadlessOffset = 0.0;\n    m_HeadlessFPS_DT = 1.0 / (double)fps;\n    m_HeadlessFixedFPS = true;\n}\n\nsize_t Clock_GetDateTime(char *const buffer, const size_t size)\n{\n    time_t lt = time(0);\n    struct tm *tptr = localtime(&lt);\n\n    return snprintf(\n        buffer, size, \"%04d%02d%02d_%02d%02d%02d\", tptr->tm_year + 1900,\n        tptr->tm_mon + 1, tptr->tm_mday, tptr->tm_hour, tptr->tm_min,\n        tptr->tm_sec);\n}\n\nint32_t Clock_GetCurrentFPS(void)\n{\n    return g_Config.rendering.fps;\n}\n\nint32_t Clock_GetFrameAdvance(void)\n{\n    return Clock_GetCurrentFPS() == 30 ? 2 : 1;\n}\n\nvoid Clock_SyncTick(void)\n{\n    m_LastCounter = SDL_GetPerformanceCounter();\n    m_Accumulator = 0.0;\n}\n\nint32_t Clock_WaitTick(void)\n{\n    if (m_Disabled && m_HeadlessFixedFPS) {\n        // Advance virtual time by one fixed frame\n        m_HeadlessOffset += m_HeadlessFPS_DT;\n        return 1;\n    }\n    if (m_Disabled) {\n        return 1;\n    }\n    const Uint64 current_counter = SDL_GetPerformanceCounter();\n\n    // If this is the first call, just initialize and return a frame.\n    if (m_LastCounter == 0) {\n        m_LastCounter = current_counter;\n        return 1;\n    }\n\n    const int32_t fps = Clock_GetCurrentFPS();\n    const double speed_multiplier = Clock_GetSpeedMultiplier();\n\n    // The duration of one frame in performance counter units\n    const double frame_ticks = m_Frequency / (fps * speed_multiplier);\n\n    // Calculate elapsed ticks since last call\n    const double elapsed_ticks = (double)(current_counter - m_LastCounter);\n\n    // Add the elapsed ticks to the accumulator\n    m_Accumulator += elapsed_ticks;\n\n    // Determine how many frames we can \"release\" from the accumulator\n    int32_t frames = (int32_t)(m_Accumulator / frame_ticks);\n\n    if (frames < 1) {\n        // Not enough accumulated time for even one frame\n\n        // Calculate how long we should wait (in ms) to hit the frame boundary\n        double needed = frame_ticks - m_Accumulator;\n        double delay_ms = (needed / m_Frequency) * 1000.0;\n\n        if (delay_ms > 0) {\n            SDL_Delay((Uint32)delay_ms);\n        }\n\n        // After waiting, measure again to be accurate\n        const Uint64 after_delay_counter = SDL_GetPerformanceCounter();\n        const double after_delay_elapsed =\n            (double)(after_delay_counter - current_counter);\n        m_Accumulator += after_delay_elapsed;\n\n        // Now, we should have at least one frame available\n        frames = (int32_t)(m_Accumulator / frame_ticks);\n        if (frames < 1) {\n            // To avoid a possible floating-point corner case, ensure at least\n            // one frame\n            frames = 1;\n        }\n    }\n\n    // Consume the frames from the m_Accumulator\n    m_Accumulator -= frames * frame_ticks;\n\n    // Update the last counter to the current performance counter\n    m_LastCounter = SDL_GetPerformanceCounter();\n\n    return frames;\n}\n\ndouble Clock_GetRealTime(void)\n{\n    return M_GetHighPrecisionCounter();\n}\n\ndouble Clock_GetSimTime(void)\n{\n    const double real_now = M_GetHighPrecisionCounter();\n    const double real_delta = real_now - m_Priv.real_time_at_last_change;\n    return m_Priv.sim_time_at_last_change + real_delta * m_Priv.sim_speed;\n}\n\nvoid Clock_SetSimSpeed(const double new_speed)\n{\n    // First, figure out how much sim time has passed so far\n    const double prev_sim_time = Clock_GetSimTime();\n    // Then re-anchor the reference point\n    m_Priv.real_time_at_last_change = M_GetHighPrecisionCounter();\n    m_Priv.sim_time_at_last_change = prev_sim_time;\n    m_Priv.sim_speed = new_speed;\n}\n"
  },
  {
    "path": "src/trx/game/clock/common.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\nvoid Clock_Init(void);\n\n// Disables any kind of waiting in Clock_WaitTick\nvoid Clock_DisableWait(void);\n\n// In headless mode, simulate a fixed FPS (seconds per frame = 1/fps)\nvoid Clock_EnableHeadlessFixedFPS(int32_t fps);\n\nvoid Clock_SyncTick(void);\nint32_t Clock_WaitTick(void);\n\nsize_t Clock_GetDateTime(char *buffer, size_t size);\n\nint32_t Clock_GetFrameAdvance(void);\nint32_t Clock_GetCurrentFPS(void);\n\nvoid Clock_SetSimSpeed(double new_speed);\ndouble Clock_GetRealTime(void);\ndouble Clock_GetSimTime(void);\n"
  },
  {
    "path": "src/trx/game/clock/const.h",
    "content": "#pragma once\n\n#define LOGIC_FPS 30\n"
  },
  {
    "path": "src/trx/game/clock/timer.c",
    "content": "#include <trx/game/clock/timer.h>\n\n#include <trx/game/clock/common.h>\n\nstatic double M_GetTime(const CLOCK_TIMER *const timer)\n{\n    return timer->type == CLOCK_TIMER_REAL ? Clock_GetRealTime()\n                                           : Clock_GetSimTime();\n}\n\nvoid ClockTimer_Sync(CLOCK_TIMER *const timer)\n{\n    timer->ref = M_GetTime(timer);\n}\n\ndouble ClockTimer_PeekElapsed(const CLOCK_TIMER *const timer)\n{\n    return M_GetTime(timer) - timer->ref;\n}\n\ndouble ClockTimer_TakeElapsed(CLOCK_TIMER *const timer)\n{\n    const double prev_time_sec = timer->ref;\n    const double current_time_sec = M_GetTime(timer);\n    timer->ref = current_time_sec;\n    return current_time_sec - prev_time_sec;\n}\n\nbool ClockTimer_CheckElapsed(const CLOCK_TIMER *const timer, double sec)\n{\n    return (M_GetTime(timer) - timer->ref) >= sec;\n}\n\nbool ClockTimer_CheckElapsedAndTake(CLOCK_TIMER *const timer, double sec)\n{\n    const double current_time_sec = M_GetTime(timer);\n    if ((current_time_sec - timer->ref) >= sec) {\n        timer->ref = current_time_sec;\n        return true;\n    }\n    return false;\n}\n"
  },
  {
    "path": "src/trx/game/clock/timer.h",
    "content": "#pragma once\n\ntypedef enum {\n    CLOCK_TIMER_SIM,\n    CLOCK_TIMER_REAL,\n} CLOCK_TIMER_TYPE;\n\ntypedef struct {\n    double ref;\n    CLOCK_TIMER_TYPE type;\n} CLOCK_TIMER;\n\nvoid ClockTimer_Sync(CLOCK_TIMER *timer);\ndouble ClockTimer_PeekElapsed(const CLOCK_TIMER *timer);\ndouble ClockTimer_TakeElapsed(CLOCK_TIMER *timer);\nbool ClockTimer_CheckElapsed(const CLOCK_TIMER *timer, double sec);\nbool ClockTimer_CheckElapsedAndTake(CLOCK_TIMER *timer, double sec);\n"
  },
  {
    "path": "src/trx/game/clock/turbo.c",
    "content": "#include <trx/game/clock/turbo.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/clock/common.h>\n#include <trx/game/clock/timer.h>\n#include <trx/game/console/common.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <math.h>\n\nvoid Clock_CycleTurboSpeed(const bool forward)\n{\n    int32_t new_speed = Clock_GetTurboSpeed() + (forward ? 1 : -1);\n    if (new_speed < CLOCK_TURBO_SPEED_MIN\n        || new_speed > CLOCK_TURBO_SPEED_MAX) {\n        new_speed = 0;\n    }\n    Clock_SetTurboSpeed(new_speed);\n}\n\nint32_t Clock_GetTurboSpeed(void)\n{\n    return g_Config.gameplay.turbo_speed;\n}\n\nvoid Clock_SetTurboSpeed(int32_t value)\n{\n    CLAMP(value, CLOCK_TURBO_SPEED_MIN, CLOCK_TURBO_SPEED_MAX);\n    if (value == g_Config.gameplay.turbo_speed) {\n        return;\n    }\n    g_Config.gameplay.turbo_speed = value;\n    Config_Update();\n    Console_Log(GS(\"general/osd/speed_set\"), value);\n    Clock_SetSimSpeed(Clock_GetSpeedMultiplier());\n}\n\ndouble Clock_GetSpeedMultiplier(void)\n{\n    if (Clock_GetTurboSpeed() > 0) {\n        return 1.0 + Clock_GetTurboSpeed();\n    } else if (Clock_GetTurboSpeed() < 0) {\n        return pow(2.0, Clock_GetTurboSpeed());\n    } else {\n        return 1.0;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/clock/turbo.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\n#define CLOCK_TURBO_SPEED_MIN -2\n#define CLOCK_TURBO_SPEED_MAX 2\n\nvoid Clock_CycleTurboSpeed(bool forward);\n\nint32_t Clock_GetTurboSpeed(void);\nvoid Clock_SetTurboSpeed(int32_t value);\n\ndouble Clock_GetSpeedMultiplier(void);\n"
  },
  {
    "path": "src/trx/game/clock.h",
    "content": "#pragma once\n\n#include <trx/game/clock/common.h>\n#include <trx/game/clock/const.h>\n#include <trx/game/clock/timer.h>\n#include <trx/game/clock/turbo.h>\n"
  },
  {
    "path": "src/trx/game/collision/common.c",
    "content": "#include <trx/game/collision/common.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/matrix.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\n#define M_HEADROOM 160 // Additional collision space above Lara's head.\n\nstatic bool M_IsOnWalkable(\n    const SECTOR *const sector, const XYZ_32 pos, const int32_t room_height)\n{\n    return g_Config.gameplay.fix_bridge_collision\n        && Room_IsOnWalkable(sector, pos, room_height, NO_ITEM);\n}\n\n// Probes the front, left, and right of Lara and fills in the collision info for\n// each side. The collision info depends on Lara's state. Her state determines\n// how big slope and lava pit sectors are treated. For example, in the walk\n// state, Lara won't walk up big slopes or walk down into lava pits.\nstatic void M_FillSide(\n    const COLL_INFO *const coll, COLL_SIDE *const side, const XYZ_32 pos,\n    const XZ_32 probe, const int32_t obj_height, int16_t *const room_num)\n{\n    const int32_t y = pos.y - obj_height;\n    const int32_t y_top = y - M_HEADROOM;\n\n    int16_t local_room_num = *room_num;\n    int16_t *const test_room_num =\n        g_Config.gameplay.wall_glitch_mode == WALL_GLITCH_FIXED\n        ? &local_room_num\n        : room_num;\n\n    const XYZ_32 sample_pos = {\n        .x = pos.x + probe.x,\n        .y = y_top,\n        .z = pos.z + probe.z,\n    };\n    const SECTOR *sector = Room_GetSector(sample_pos, test_room_num);\n    int32_t height = Room_GetHeight(sector, sample_pos);\n    int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n    const int32_t room_height = height;\n    const int32_t room_ceiling = ceiling;\n    const bool sim_wall = room_height == ceiling && room_height != NO_HEIGHT\n        && !sector->ceiling.is_split && !sector->floor.is_split\n        && sector->ceiling.tilt.x == 0 && sector->ceiling.tilt.z == 0\n        && sector->floor.tilt.x == 0 && sector->floor.tilt.z == 0;\n    if (height != NO_HEIGHT) {\n        height -= pos.y;\n    }\n    if (ceiling != NO_HEIGHT) {\n        ceiling -= y;\n    }\n\n    side->floor = height;\n    side->ceiling = ceiling;\n    side->type = Room_GetHeightType();\n\n    const bool is_on_walkable = M_IsOnWalkable(sector, sample_pos, room_height);\n\n    const bool is_front = side == &coll->side_front;\n    if (is_front) {\n        XYZ_32 front_probe_pos = sample_pos;\n        front_probe_pos.x += probe.x;\n        front_probe_pos.z += probe.z;\n        sector = Room_GetSector(front_probe_pos, room_num);\n        height = Room_GetHeight(sector, front_probe_pos);\n        if (height != NO_HEIGHT) {\n            height -= pos.y;\n        }\n    }\n\n    if (!is_on_walkable) {\n        if (coll->slopes_are_walls\n            && (side->type == HT_BIG_SLOPE || side->type == HT_DIAGONAL)\n            && side->floor < 0\n            && (!is_front\n                || (side->floor < coll->side_mid.floor\n                    && height < side->floor))) {\n            side->floor = -32767;\n        } else if (\n            coll->slopes_are_pits\n            && (side->type == HT_BIG_SLOPE || side->type == HT_DIAGONAL)\n            && side->floor > (is_front ? coll->side_mid.floor : 0)) {\n            side->floor = STEP_L * 2;\n        } else if (\n            coll->lava_is_pit && side->floor > 0\n            && Room_GetPitSector(sector, pos.x, pos.z)->is_death_sector) {\n            side->floor = STEP_L * 2;\n        }\n    } else if (sim_wall) {\n        side->floor = NO_HEIGHT;\n        side->ceiling = NO_HEIGHT;\n    }\n}\n\nint32_t Collide_GetSpheres(\n    const ITEM *const item, SPHERE *const spheres, const bool world_space)\n{\n    if (item == nullptr) {\n        return 0;\n    }\n\n    XYZ_32 pos;\n    if (world_space) {\n        pos = item->pos;\n        Matrix_PushUnit();\n    } else {\n        pos.x = 0;\n        pos.y = 0;\n        pos.z = 0;\n        Matrix_Push();\n        Matrix_TranslateAbs32(item->pos);\n    }\n\n    Matrix_Rot16(item->rot);\n\n    const ANIM_FRAME *const frame = Item_GetBestFrame(item);\n    Matrix_TranslateRel16(frame->offset);\n    Matrix_Rot16(frame->mesh_rots[0]);\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const OBJECT_MESH *mesh = Object_GetMesh(obj->mesh_idx);\n    Matrix_Push();\n    Matrix_TranslateRel16(mesh->center);\n    spheres[0].pos.x = pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n    spheres[0].pos.y = pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n    spheres[0].pos.z = pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n    spheres[0].r = mesh->radius;\n    Matrix_Pop();\n\n    const int16_t *extra_rotation = item->extra_rotations;\n    for (int32_t i = 1; i < obj->mesh_count; i++) {\n        const ANIM_BONE *const bone = Object_GetBone(obj, i - 1);\n        if (bone->matrix_pop) {\n            Matrix_Pop();\n        }\n        if (bone->matrix_push) {\n            Matrix_Push();\n        }\n\n        Matrix_TranslateRel32(bone->pos);\n        Matrix_Rot16(frame->mesh_rots[i]);\n        Object_ApplyExtraRotation(&extra_rotation, bone->rot, false);\n\n        mesh = Object_GetMesh(obj->mesh_idx + i);\n        Matrix_Push();\n        Matrix_TranslateRel16(mesh->center);\n        SPHERE *const sphere = &spheres[i];\n        sphere->pos.x = pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n        sphere->pos.y = pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n        sphere->pos.z = pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n        sphere->r = mesh->radius;\n        Matrix_Pop();\n    }\n\n    Matrix_Pop();\n    return obj->mesh_count;\n}\n\nint32_t Collide_TestCollision(ITEM *const item, const ITEM *const lara_item)\n{\n    SPHERE slist_baddie[34];\n    SPHERE slist_lara[34];\n\n    uint32_t touch_bits = 0;\n    int32_t num1 = Collide_GetSpheres(item, slist_baddie, true);\n    int32_t num2 = Collide_GetSpheres(lara_item, slist_lara, true);\n\n    for (int32_t i = 0; i < num1; i++) {\n        const SPHERE *const ptr1 = &slist_baddie[i];\n        if (ptr1->r <= 0) {\n            continue;\n        }\n\n        for (int32_t j = 0; j < num2; j++) {\n            const SPHERE *const ptr2 = &slist_lara[j];\n            if (ptr2->r <= 0) {\n                continue;\n            }\n\n            const int32_t dx = ptr2->pos.x - ptr1->pos.x;\n            const int32_t dy = ptr2->pos.y - ptr1->pos.y;\n            const int32_t dz = ptr2->pos.z - ptr1->pos.z;\n            const int32_t d1 = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n            const int32_t d2 = SQUARE(ptr1->r + ptr2->r);\n            if (d1 < d2) {\n                touch_bits |= 1 << i;\n                break;\n            }\n        }\n    }\n\n    item->touch_bits = touch_bits;\n    return touch_bits;\n}\n\nvoid Collide_GetJointAbsPosition(\n    const ITEM *const item, XYZ_32 *const out_vec, const int32_t joint)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    ANIM_FRAME *frames[2] = { nullptr, nullptr };\n    int32_t rate = 0;\n    const int32_t frac = Item_GetFrames(item, frames, &rate);\n    const bool use_item_interp =\n        Interpolation_IsActive() && item->enable_interpolation;\n    const XYZ_32 item_pos =\n        use_item_interp ? item->interp.result.pos : item->pos;\n    const XYZ_16 item_rot =\n        use_item_interp ? item->interp.result.rot : item->rot;\n\n    if (frames[0] == nullptr) {\n        Matrix_PushUnit();\n        Matrix_Rot16(item_rot);\n        Matrix_TranslateRel32(*out_vec);\n        out_vec->x = item_pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n        out_vec->y = item_pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n        out_vec->z = item_pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n        Matrix_Pop();\n        return;\n    }\n\n    const ANIM_FRAME *const frame_a = frames[0];\n    const ANIM_FRAME *const frame_b = frames[1];\n    const bool do_interp = frame_b != nullptr && frac != 0 && rate != 0;\n\n    int32_t stack = 1;\n    Matrix_PushUnit();\n    Matrix_Rot16(item_rot);\n    if (do_interp) {\n        Matrix_InitInterpolate(frac, rate);\n        Matrix_TranslateRel16_ID(frame_a->offset, frame_b->offset);\n        Matrix_Rot16_ID(frame_a->mesh_rots[0], frame_b->mesh_rots[0]);\n    } else {\n        Matrix_TranslateRel16(frame_a->offset);\n        Matrix_Rot16(frame_a->mesh_rots[0]);\n    }\n\n    const int16_t *extra_rotation = item->extra_rotations;\n    const int32_t max_joint = obj->mesh_count > 0 ? obj->mesh_count - 1 : 0;\n    const int32_t abs_joint = MIN(max_joint, joint);\n    for (int32_t i = 0; i < abs_joint; i++) {\n        const ANIM_BONE *const bone = Object_GetBone(obj, i);\n        if (bone->matrix_pop) {\n            stack--;\n            if (do_interp) {\n                Matrix_Pop_I();\n            } else {\n                Matrix_Pop();\n            }\n        }\n        if (bone->matrix_push) {\n            stack++;\n            if (do_interp) {\n                Matrix_Push_I();\n            } else {\n                Matrix_Push();\n            }\n        }\n\n        if (do_interp) {\n            Matrix_TranslateRel32_I(bone->pos);\n            Matrix_Rot16_ID(\n                frame_a->mesh_rots[i + 1], frame_b->mesh_rots[i + 1]);\n            Object_ApplyExtraRotation(&extra_rotation, bone->rot, true);\n        } else {\n            Matrix_TranslateRel32(bone->pos);\n            Matrix_Rot16(frame_a->mesh_rots[i + 1]);\n            Object_ApplyExtraRotation(&extra_rotation, bone->rot, false);\n        }\n    }\n\n    if (do_interp) {\n        Matrix_TranslateRel32_I(*out_vec);\n        Matrix_Interpolate();\n    } else {\n        Matrix_TranslateRel32(*out_vec);\n    }\n    out_vec->x = item_pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n    out_vec->y = item_pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n    out_vec->z = item_pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n\n    while (stack--) {\n        if (do_interp) {\n            Matrix_Pop_I();\n        } else {\n            Matrix_Pop();\n        }\n    }\n}\n\nvoid Collide_GetCollisionInfo(\n    COLL_INFO *const coll, const int32_t x_pos, const int32_t y_pos,\n    const int32_t z_pos, int16_t room_num, int32_t obj_height)\n{\n    coll->coll_type = COLL_NONE;\n    coll->shift.x = 0;\n    coll->shift.y = 0;\n    coll->shift.z = 0;\n    coll->quadrant = Math_GetDirection(coll->facing);\n\n    bool reset_room = false;\n    int16_t prev_room_num = room_num;\n    if (obj_height < 0) {\n        reset_room = true;\n        obj_height = -obj_height;\n    }\n\n    int32_t x = x_pos;\n    int32_t z = z_pos;\n    const int32_t y = y_pos - obj_height;\n    const int32_t y_top = y - M_HEADROOM;\n\n    const XYZ_32 sample_pos = { .x = x, .y = y_top, .z = z };\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n    int32_t height = Room_GetHeight(sector, sample_pos);\n    int32_t room_height = height;\n    if (height != NO_HEIGHT) {\n        height -= y_pos;\n    }\n    int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n    if (ceiling != NO_HEIGHT) {\n        ceiling -= y;\n    }\n\n    coll->side_mid.floor = height;\n    coll->side_mid.ceiling = ceiling;\n    coll->side_mid.type = Room_GetHeightType();\n\n    bool is_on_walkable = M_IsOnWalkable(sector, sample_pos, room_height);\n    if (is_on_walkable) {\n        coll->tilt = (XZ_16) {};\n    } else {\n        const ITEM *const lara_item = Lara_GetItem();\n        coll->tilt =\n            Room_GetTiltType(sector, (XYZ_32) { x, lara_item->pos.y, z });\n    }\n\n    XZ_32 probe_left = {};\n    XZ_32 probe_right = {};\n    XZ_32 probe_front = {};\n    switch (coll->quadrant) {\n    case DIR_NORTH:\n        probe_front.x = (coll->radius * Math_Sin(coll->facing)) >> W2V_SHIFT;\n        probe_front.z = coll->radius;\n        probe_left.x = -coll->radius;\n        probe_left.z = coll->radius;\n        probe_right.x = coll->radius;\n        probe_right.z = coll->radius;\n        break;\n\n    case DIR_EAST:\n        probe_front.x = coll->radius;\n        probe_front.z = (coll->radius * Math_Cos(coll->facing)) >> W2V_SHIFT;\n        probe_left.x = coll->radius;\n        probe_left.z = coll->radius;\n        probe_right.x = coll->radius;\n        probe_right.z = -coll->radius;\n        break;\n\n    case DIR_SOUTH:\n        probe_front.x = (coll->radius * Math_Sin(coll->facing)) >> W2V_SHIFT;\n        probe_front.z = -coll->radius;\n        probe_left.x = coll->radius;\n        probe_left.z = -coll->radius;\n        probe_right.x = -coll->radius;\n        probe_right.z = -coll->radius;\n        break;\n\n    case DIR_WEST:\n        probe_front.x = -coll->radius;\n        probe_front.z = (coll->radius * Math_Cos(coll->facing)) >> W2V_SHIFT;\n        probe_left.x = -coll->radius;\n        probe_left.z = -coll->radius;\n        probe_right.x = -coll->radius;\n        probe_right.z = coll->radius;\n        break;\n\n    default:\n        break;\n    }\n\n    if (reset_room) {\n        room_num = prev_room_num;\n    }\n\n    const XYZ_32 probe_base = { x_pos, y_pos, z_pos };\n    M_FillSide(\n        coll, &coll->side_front, probe_base, probe_front, obj_height,\n        &room_num);\n\n    int16_t room_num2;\n    room_num2 = prev_room_num;\n    M_FillSide(\n        coll, &coll->side_left, probe_base, probe_left, obj_height, &room_num2);\n    room_num2 = prev_room_num;\n    M_FillSide(\n        coll, &coll->side_right, probe_base, probe_right, obj_height,\n        &room_num2);\n\n    M_FillSide(\n        coll, &coll->side_left2, probe_base, probe_left, obj_height, &room_num);\n    M_FillSide(\n        coll, &coll->side_right2, probe_base, probe_right, obj_height,\n        &room_num);\n\n    const int16_t static_room_num = g_TRVersion >= 3 ? prev_room_num : room_num;\n    if (Collide_CollideStaticObjects(\n            coll, x_pos, y_pos, z_pos, static_room_num, obj_height)) {\n        const XYZ_32 test_pos = {\n            .x = x_pos + coll->shift.x,\n            .y = y_pos,\n            .z = z_pos + coll->shift.z,\n        };\n        sector = Room_GetSector(test_pos, &room_num);\n        if (Room_GetHeight(sector, test_pos) < test_pos.y - WALL_L / 2\n            || Room_GetCeiling(sector, test_pos) > y) {\n            coll->shift.x = -coll->shift.x;\n            coll->shift.z = -coll->shift.z;\n        }\n    }\n\n    if (coll->side_mid.floor == NO_HEIGHT) {\n        coll->shift.x = coll->old.x - x_pos;\n        coll->shift.y = coll->old.y - y_pos;\n        coll->shift.z = coll->old.z - z_pos;\n        coll->coll_type = COLL_FRONT;\n        return;\n    }\n\n    if (coll->side_mid.floor - coll->side_mid.ceiling <= 0) {\n        coll->shift.x = coll->old.x - x_pos;\n        coll->shift.y = coll->old.y - y_pos;\n        coll->shift.z = coll->old.z - z_pos;\n        coll->coll_type = COLL_CLAMP;\n        return;\n    }\n\n    if (coll->side_mid.ceiling >= 0) {\n        coll->shift.y = coll->side_mid.ceiling;\n        coll->coll_type = COLL_TOP;\n    }\n\n    if (coll->side_front.floor > coll->bad_pos\n        || coll->side_front.floor < coll->bad_neg\n        || coll->side_front.ceiling > coll->bad_ceiling) {\n        if (coll->side_front.type == HT_DIAGONAL\n            || coll->side_front.type == HT_SPLIT_TRI) {\n            coll->shift.x = coll->old.x - x;\n            coll->shift.z = coll->old.z - z;\n        } else {\n            switch (coll->quadrant) {\n            case DIR_NORTH:\n            case DIR_SOUTH:\n                coll->shift.x = coll->old.x - x_pos;\n                coll->shift.z =\n                    Room_FindGridShift(z_pos + probe_front.z, z_pos);\n                break;\n\n            case DIR_EAST:\n            case DIR_WEST:\n                coll->shift.x =\n                    Room_FindGridShift(x_pos + probe_front.x, x_pos);\n                coll->shift.z = coll->old.z - z_pos;\n                break;\n\n            default:\n                break;\n            }\n        }\n\n        coll->coll_type = COLL_FRONT;\n        return;\n    }\n\n    if (coll->side_front.ceiling >= coll->bad_ceiling) {\n        coll->shift.x = coll->old.x - x_pos;\n        coll->shift.y = coll->old.y - y_pos;\n        coll->shift.z = coll->old.z - z_pos;\n        coll->coll_type = COLL_TOP_FRONT;\n        return;\n    }\n\n    if (coll->side_left.floor > coll->bad_pos\n        || coll->side_left.floor < coll->bad_neg) {\n        if (coll->side_left.type == HT_SPLIT_TRI) {\n            coll->shift.x = coll->old.x - x;\n            coll->shift.z = coll->old.z - z;\n        } else {\n            switch (coll->quadrant) {\n            case DIR_NORTH:\n            case DIR_SOUTH:\n                coll->shift.x = Room_FindGridShift(\n                    x_pos + probe_left.x, x_pos + probe_front.x);\n                break;\n\n            case DIR_EAST:\n            case DIR_WEST:\n                coll->shift.z = Room_FindGridShift(\n                    z_pos + probe_left.z, z_pos + probe_front.z);\n                break;\n\n            default:\n                break;\n            }\n        }\n\n        coll->coll_type = COLL_LEFT;\n        return;\n    }\n\n    if (coll->side_right.floor > coll->bad_pos\n        || coll->side_right.floor < coll->bad_neg) {\n        if (coll->side_right.type == HT_SPLIT_TRI) {\n            coll->shift.x = coll->old.x - x;\n            coll->shift.z = coll->old.z - z;\n        } else {\n            switch (coll->quadrant) {\n            case DIR_NORTH:\n            case DIR_SOUTH:\n                coll->shift.x = Room_FindGridShift(\n                    x_pos + probe_right.x, x_pos + probe_front.x);\n                break;\n\n            case DIR_EAST:\n            case DIR_WEST:\n                coll->shift.z = Room_FindGridShift(\n                    z_pos + probe_right.z, z_pos + probe_front.z);\n                break;\n\n            default:\n                break;\n            }\n        }\n\n        coll->coll_type = COLL_RIGHT;\n        return;\n    }\n}\n\nbool Collide_CollideStaticObjects(\n    COLL_INFO *const coll, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t room_num, const int32_t height)\n{\n    coll->hit_static = 0;\n\n    const int32_t in_x_min = x - coll->radius;\n    const int32_t in_x_max = x + coll->radius;\n    const int32_t in_y_min = y - height;\n    const int32_t in_y_max = y;\n    const int32_t in_z_min = z - coll->radius;\n    const int32_t in_z_max = z + coll->radius;\n    XYZ_32 shifter = { .x = 0, .z = 0 };\n\n    Room_GetNearbyRooms(\n        (XYZ_32) { x, y, z }, coll->radius + 50, height + 50, room_num);\n\n    for (int32_t i = 0; i < Room_DrawGetCount(); i++) {\n        const ROOM *const room = Room_Get(Room_DrawGetRoom(i));\n\n        for (int32_t j = 0; j < room->num_static_meshes; j++) {\n            const STATIC_MESH *const mesh = &room->static_meshes[j];\n            const STATIC_OBJECT_3D *const obj =\n                Object_Get3DStatic(mesh->static_num);\n\n            if (!obj->collidable) {\n                continue;\n            }\n\n            int32_t x_min;\n            int32_t x_max;\n            int32_t z_min;\n            int32_t z_max;\n            const int32_t y_min = mesh->pos.y + obj->collision_bounds.min.y;\n            const int32_t y_max = mesh->pos.y + obj->collision_bounds.max.y;\n            switch (mesh->rot.y) {\n            case DEG_90:\n                x_min = mesh->pos.x + obj->collision_bounds.min.z;\n                x_max = mesh->pos.x + obj->collision_bounds.max.z;\n                z_min = mesh->pos.z - obj->collision_bounds.max.x;\n                z_max = mesh->pos.z - obj->collision_bounds.min.x;\n                break;\n\n            case -DEG_180:\n                x_min = mesh->pos.x - obj->collision_bounds.max.x;\n                x_max = mesh->pos.x - obj->collision_bounds.min.x;\n                z_min = mesh->pos.z - obj->collision_bounds.max.z;\n                z_max = mesh->pos.z - obj->collision_bounds.min.z;\n                break;\n\n            case -DEG_90:\n                x_min = mesh->pos.x - obj->collision_bounds.max.z;\n                x_max = mesh->pos.x - obj->collision_bounds.min.z;\n                z_min = mesh->pos.z + obj->collision_bounds.min.x;\n                z_max = mesh->pos.z + obj->collision_bounds.max.x;\n                break;\n\n            default:\n                x_min = mesh->pos.x + obj->collision_bounds.min.x;\n                x_max = mesh->pos.x + obj->collision_bounds.max.x;\n                z_min = mesh->pos.z + obj->collision_bounds.min.z;\n                z_max = mesh->pos.z + obj->collision_bounds.max.z;\n                break;\n            }\n\n            if (in_x_max <= x_min || in_x_min >= x_max || in_y_max <= y_min\n                || in_y_min >= y_max || in_z_max <= z_min\n                || in_z_min >= z_max) {\n                continue;\n            }\n\n            coll->hit_static = 1;\n            if (g_Config.gameplay.enable_soft_statics) {\n                return true;\n            }\n\n            int32_t shl = in_x_max - x_min;\n            int32_t shr = x_max - in_x_min;\n            if (shl < shr) {\n                shifter.x = -shl;\n            } else {\n                shifter.x = shr;\n            }\n\n            shl = in_z_max - z_min;\n            shr = z_max - in_z_min;\n            if (shl < shr) {\n                shifter.z = -shl;\n            } else {\n                shifter.z = shr;\n            }\n\n            switch (coll->quadrant) {\n            case DIR_NORTH:\n                if (shifter.x > coll->radius || shifter.x < -coll->radius) {\n                    coll->coll_type = COLL_FRONT;\n                    coll->shift.x = coll->old.x - x;\n                    coll->shift.z = shifter.z;\n                } else if (shifter.x > 0) {\n                    coll->coll_type = COLL_LEFT;\n                    coll->shift.x = shifter.x;\n                    coll->shift.z = 0;\n                } else if (shifter.x < 0) {\n                    coll->coll_type = COLL_RIGHT;\n                    coll->shift.x = shifter.x;\n                    coll->shift.z = 0;\n                }\n                break;\n\n            case DIR_EAST:\n                if (shifter.z > coll->radius || shifter.z < -coll->radius) {\n                    coll->coll_type = COLL_FRONT;\n                    coll->shift.x = shifter.x;\n                    coll->shift.z = coll->old.z - z;\n                } else if (shifter.z > 0) {\n                    coll->coll_type = COLL_RIGHT;\n                    coll->shift.x = 0;\n                    coll->shift.z = shifter.z;\n                } else if (shifter.z < 0) {\n                    coll->coll_type = COLL_LEFT;\n                    coll->shift.x = 0;\n                    coll->shift.z = shifter.z;\n                }\n                break;\n\n            case DIR_SOUTH:\n                if (shifter.x > coll->radius || shifter.x < -coll->radius) {\n                    coll->coll_type = COLL_FRONT;\n                    coll->shift.x = coll->old.x - x;\n                    coll->shift.z = shifter.z;\n                } else if (shifter.x > 0) {\n                    coll->coll_type = COLL_RIGHT;\n                    coll->shift.x = shifter.x;\n                    coll->shift.z = 0;\n                } else if (shifter.x < 0) {\n                    coll->coll_type = COLL_LEFT;\n                    coll->shift.x = shifter.x;\n                    coll->shift.z = 0;\n                }\n                break;\n\n            case DIR_WEST:\n                if (shifter.z > coll->radius || shifter.z < -coll->radius) {\n                    coll->coll_type = COLL_FRONT;\n                    coll->shift.x = shifter.x;\n                    coll->shift.z = coll->old.z - z;\n                } else if (shifter.z > 0) {\n                    coll->coll_type = COLL_LEFT;\n                    coll->shift.x = 0;\n                    coll->shift.z = shifter.z;\n                } else if (shifter.z < 0) {\n                    coll->coll_type = COLL_RIGHT;\n                    coll->shift.x = 0;\n                    coll->shift.z = shifter.z;\n                }\n                break;\n\n            default:\n                break;\n            }\n\n            return true;\n        }\n    }\n\n    return false;\n}\n\nbool Collide_TestBoundsCollide(\n    const COLL_ITEM *const src_item, const COLL_ITEM *const dst_item,\n    const int32_t radius)\n{\n    const BOUNDS_16 *const src_bounds = &src_item->bounds;\n    const BOUNDS_16 *const dst_bounds = &dst_item->bounds;\n\n    if (src_item->pos.y + src_bounds->min.y\n            >= dst_item->pos.y + dst_bounds->max.y\n        || src_item->pos.y + src_bounds->max.y\n            <= dst_item->pos.y + dst_bounds->min.y) {\n        return false;\n    }\n\n    const int32_t c = Math_Cos(src_item->rot.y);\n    const int32_t s = Math_Sin(src_item->rot.y);\n    const int32_t dx = dst_item->pos.x - src_item->pos.x;\n    const int32_t dz = dst_item->pos.z - src_item->pos.z;\n    const int32_t rx = (c * dx - s * dz) >> W2V_SHIFT;\n    const int32_t rz = (c * dz + s * dx) >> W2V_SHIFT;\n\n    // clang-format off\n    return (\n        rx >= src_bounds->min.x - radius &&\n        rx <= src_bounds->max.x + radius &&\n        rz >= src_bounds->min.z - radius &&\n        rz <= src_bounds->max.z + radius);\n    // clang-format on\n}\n\nvoid Collide_DoProperDetection(ITEM *const item, const XYZ_32 old_pos)\n{\n    int32_t ceiling;\n    int32_t height;\n    int32_t oldonobj;\n    int32_t bs;\n    int32_t yang;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *sector = Room_GetSector(old_pos, &room_num);\n    int32_t oldheight = Room_GetHeight(sector, old_pos);\n    int32_t oldtype = Room_GetHeightType();\n\n    room_num = item->room_num;\n    sector = Room_GetSector(item->pos, &room_num);\n    height = Room_GetHeight(sector, item->pos);\n\n    if (item->pos.y >= height) {\n        bs = 0;\n\n        if ((oldtype == HT_BIG_SLOPE || oldtype == HT_DIAGONAL)\n            && oldheight < height) {\n            yang = (uint16_t)item->rot.y;\n\n            const XZ_16 tilt = Room_GetTiltType(sector, item->pos);\n            if (tilt.x < 0) {\n                if (yang >= DEG_180) {\n                    bs = 1;\n                }\n            } else if (tilt.x > 0) {\n                if (yang <= DEG_180) {\n                    bs = 1;\n                }\n            }\n\n            if (tilt.z < 0) {\n                if (yang >= DEG_90 && yang <= DEG_270) {\n                    bs = 1;\n                }\n            } else if (tilt.z > 0) {\n                if (yang <= DEG_90 || yang >= DEG_270) {\n                    bs = 1;\n                }\n            }\n        }\n\n        const bool x_cross = ROUND_TO_SECTOR(item->pos.x ^ old_pos.x) != 0;\n        const bool z_cross = ROUND_TO_SECTOR(item->pos.z ^ old_pos.z) != 0;\n        if (old_pos.y > height + 32 && !bs && (x_cross || z_cross)) {\n            const bool xs = x_cross && z_cross\n                ? ABS(old_pos.x - item->pos.x) < ABS(old_pos.z - item->pos.z)\n                : true;\n            item->rot.y = x_cross && xs ? -item->rot.y : -DEG_180 - item->rot.y;\n            item->pos = old_pos;\n            item->speed >>= 1;\n        } else if (oldtype != HT_BIG_SLOPE && oldtype != HT_DIAGONAL) {\n            if (item->fall_speed > 0) {\n                if (item->fall_speed > 16) {\n                    if (item->object_id == O_GRENADE) {\n                        item->fall_speed =\n                            (item->fall_speed >> 1) - item->fall_speed;\n                    } else {\n                        item->fall_speed = -(item->fall_speed >> 2);\n\n                        if (item->fall_speed < -100) {\n                            item->fall_speed = -100;\n                        }\n                    }\n                } else {\n                    item->fall_speed = 0;\n\n                    if (item->object_id == O_GRENADE) {\n                        item->speed--;\n                        item->required_anim_state = 1;\n                        item->rot.x = 0;\n                    } else {\n                        item->speed -= 3;\n                    }\n\n                    if (item->speed < 0) {\n                        item->speed = 0;\n                    }\n                }\n            }\n\n            item->pos.y = height;\n        } else {\n            item->speed -= item->speed >> 2;\n\n            const XZ_16 tilt = Room_GetTiltType(sector, item->pos);\n            if (tilt.x < 0 && ABS(tilt.x) - ABS(tilt.z) >= MAX_SLOPE) {\n                if ((uint16_t)item->rot.y > DEG_180) {\n                    item->rot.y = -1 - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed -= tilt.x << 1;\n\n                        if ((uint16_t)item->rot.y > DEG_90\n                            && (uint16_t)item->rot.y < DEG_270) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_90) {\n                                item->rot.y = DEG_90;\n                            }\n                        } else if ((uint16_t)item->rot.y < DEG_90) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_90) {\n                                item->rot.y = DEG_90;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.x > 0 && ABS(tilt.x) - ABS(tilt.z) >= MAX_SLOPE) {\n                if ((uint16_t)item->rot.y < DEG_180) {\n                    item->rot.y = -1 - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed += tilt.x << 1;\n\n                        if ((uint16_t)item->rot.y > DEG_270\n                            || (uint16_t)item->rot.y < DEG_90) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_270) {\n                                item->rot.y = -DEG_90;\n                            }\n                        } else if ((uint16_t)item->rot.y < DEG_270) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_270) {\n                                item->rot.y = -DEG_90;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.z < 0 && ABS(tilt.z) - ABS(tilt.x) >= MAX_SLOPE) {\n                if ((uint16_t)item->rot.y > DEG_90\n                    && (uint16_t)item->rot.y < DEG_270) {\n                    item->rot.y = 0x7FFF - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed -= tilt.z << 1;\n\n                        if ((uint16_t)item->rot.y < DEG_180) {\n                            item->rot.y -= DEG_90;\n\n                            if ((uint16_t)item->rot.y > 61440) {\n                                item->rot.y = 0;\n                            }\n                        } else {\n                            item->rot.y += DEG_90;\n\n                            if ((uint16_t)item->rot.y < DEG_90) {\n                                item->rot.y = 0;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.z > 0 && ABS(tilt.z) - ABS(tilt.x) >= MAX_SLOPE) {\n                if ((uint16_t)item->rot.y > DEG_270\n                    || (uint16_t)item->rot.y < DEG_90) {\n                    item->rot.y = 0x7FFF - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed += tilt.z << 1;\n\n                        if ((uint16_t)item->rot.y > DEG_180) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_180) {\n                                item->rot.y = -DEG_180;\n                            }\n                        } else if ((uint16_t)item->rot.y < DEG_180) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_180) {\n                                item->rot.y = -DEG_180;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.x < 0 && tilt.z < 0) {\n                if ((uint16_t)item->rot.y > DEG_135\n                    && (uint16_t)item->rot.y < DEG_315) {\n                    item->rot.y = -(DEG_90 + 1) - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed -= tilt.z + tilt.x;\n\n                        if ((uint16_t)item->rot.y > DEG_45\n                            && (uint16_t)item->rot.y < DEG_225) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_45) {\n                                item->rot.y = DEG_45;\n                            }\n                        } else if (item->rot.y != DEG_45) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_45) {\n                                item->rot.y = DEG_45;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.x < 0 && tilt.z > 0) {\n                if ((uint16_t)item->rot.y > DEG_225\n                    || (uint16_t)item->rot.y < DEG_45) {\n                    item->rot.y = DEG_90 - 1 - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed += tilt.z - tilt.x;\n\n                        if ((uint16_t)item->rot.y < DEG_315\n                            && (uint16_t)item->rot.y > DEG_135) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_135) {\n                                item->rot.y = DEG_135;\n                            }\n                        } else if (item->rot.y != DEG_135) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_135) {\n                                item->rot.y = DEG_135;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.x > 0 && tilt.z > 0) {\n                if ((uint16_t)item->rot.y > DEG_315\n                    || (uint16_t)item->rot.y < DEG_135) {\n                    item->rot.y = -(DEG_90 + 1) - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed += tilt.z + tilt.x;\n\n                        if ((uint16_t)item->rot.y < DEG_45\n                            || (uint16_t)item->rot.y > DEG_225) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_225) {\n                                item->rot.y = -DEG_135;\n                            }\n                        } else if ((uint16_t)item->rot.y != DEG_225) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_225) {\n                                item->rot.y = -DEG_135;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            } else if (tilt.x > 0 && tilt.z < 0) {\n                if ((uint16_t)item->rot.y > DEG_45\n                    && (uint16_t)item->rot.y < DEG_225) {\n                    item->rot.y = DEG_90 - 1 - item->rot.y;\n\n                    if (item->fall_speed > 0) {\n                        item->fall_speed = -(item->fall_speed >> 1);\n                    }\n                } else {\n                    if (item->speed < 32) {\n                        item->speed += tilt.x - tilt.z;\n\n                        if ((uint16_t)item->rot.y < DEG_135\n                            || (uint16_t)item->rot.y > DEG_315) {\n                            item->rot.y -= 0x1000;\n\n                            if ((uint16_t)item->rot.y < DEG_315) {\n                                item->rot.y = -DEG_45;\n                            }\n                        } else if ((uint16_t)item->rot.y != DEG_315) {\n                            item->rot.y += 0x1000;\n\n                            if ((uint16_t)item->rot.y > DEG_315) {\n                                item->rot.y = -DEG_45;\n                            }\n                        }\n                    }\n\n                    item->fall_speed =\n                        item->fall_speed > 0 ? -(item->fall_speed >> 1) : 0;\n                }\n            }\n\n            item->pos = old_pos;\n        }\n    } else {\n        if (item->fall_speed >= 0) {\n            const XYZ_32 test_pos = {\n                item->pos.x,\n                old_pos.y,\n                item->pos.z,\n            };\n            room_num = item->room_num;\n            sector = Room_GetSector(test_pos, &room_num);\n            height = Room_GetHeight(sector, test_pos);\n            const bool on_walkable =\n                Room_IsOnWalkable(sector, test_pos, height, NO_ITEM);\n\n            if (item->pos.y >= height && on_walkable) {\n                if (item->fall_speed > 16) {\n                    if (item->object_id == O_GRENADE) {\n                        item->fall_speed = -(item->fall_speed / 2);\n                    } else {\n                        item->fall_speed = -(item->fall_speed / 4);\n                        CLAMPL(item->fall_speed, -100);\n                    }\n                } else if (item->fall_speed > 0) {\n                    item->fall_speed = 0;\n                    if (item->object_id == O_GRENADE) {\n                        item->speed--;\n                        item->required_anim_state = 1;\n                        item->rot.x = 0;\n                    } else {\n                        item->speed -= 3;\n                    }\n                    CLAMPL(item->speed, 0);\n                }\n                item->pos.y = height;\n            }\n        }\n\n        room_num = item->room_num;\n        sector = Room_GetSector(item->pos, &room_num);\n        ceiling = Room_GetCeiling(sector, item->pos);\n\n        if (item->pos.y < ceiling) {\n            const bool x_cross = ROUND_TO_SECTOR(item->pos.x ^ old_pos.x) != 0;\n            const bool z_cross = ROUND_TO_SECTOR(item->pos.z ^ old_pos.z) != 0;\n            if (old_pos.y < ceiling && (x_cross || z_cross)) {\n                item->rot.y = x_cross ? -item->rot.y : -DEG_180 - item->rot.y;\n\n                if (item->object_id == O_GRENADE) {\n                    item->speed -= item->speed >> 3;\n                } else {\n                    item->speed >>= 1;\n                }\n\n                item->pos = old_pos;\n            } else {\n                item->pos.y = ceiling;\n            }\n\n            if (item->fall_speed < 0) {\n                item->fall_speed = -item->fall_speed;\n            }\n        }\n    }\n\n    room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n\n    if (item->room_num != room_num) {\n        Item_UpdateRoom(Item_GetIndex(item), room_num);\n    }\n}\n\nvoid Collide_ShiftItem(ITEM *const item, COLL_INFO *const coll)\n{\n    item->pos.x += coll->shift.x;\n    item->pos.y += coll->shift.y;\n    item->pos.z += coll->shift.z;\n    coll->shift.x = 0;\n    coll->shift.y = 0;\n    coll->shift.z = 0;\n}\n"
  },
  {
    "path": "src/trx/game/collision/common.h",
    "content": "#pragma once\n\n#include <trx/game/collision/types.h>\n\nint32_t Collide_GetSpheres(const ITEM *item, SPHERE *spheres, bool world_space);\n\nint32_t Collide_TestCollision(ITEM *item, const ITEM *lara_item);\n\nvoid Collide_GetJointAbsPosition(\n    const ITEM *item, XYZ_32 *out_vec, int32_t joint);\n\nvoid Collide_GetCollisionInfo(\n    COLL_INFO *coll, int32_t x, int32_t y, int32_t z, int16_t room_num,\n    int32_t obj_height);\n\nbool Collide_CollideStaticObjects(\n    COLL_INFO *coll, int32_t x, int32_t y, int32_t z, int16_t room_num,\n    int32_t height);\nbool Collide_TestBoundsCollide(\n    const COLL_ITEM *src_item, const COLL_ITEM *dst_item, int32_t radius);\n\nvoid Collide_DoProperDetection(ITEM *item, XYZ_32 old_pos);\n\nvoid Collide_ShiftItem(ITEM *item, COLL_INFO *coll);\n"
  },
  {
    "path": "src/trx/game/collision/los.c",
    "content": "#include <trx/game/collision/los.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/items.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n#include <math.h>\n\n#define M_CLIP_1 8\n#define M_CLIP_2 8\n#define M_MAX_LOS_ROOMS 200\n\nstatic int32_t m_LOSRooms[M_MAX_LOS_ROOMS] = {};\nstatic int32_t m_LOSNumRooms = 0;\n\nstatic inline bool M_TryPushLOSRoom(const int16_t room_num)\n{\n    if (m_LOSNumRooms < M_MAX_LOS_ROOMS) {\n        m_LOSRooms[m_LOSNumRooms++] = room_num;\n        return true;\n    }\n    return false;\n}\n\nstatic inline bool M_ResetLOSRooms(const int16_t room_num)\n{\n    m_LOSNumRooms = 0;\n    return M_TryPushLOSRoom(room_num);\n}\n\nstatic inline bool M_RoomInLOSRooms(const int16_t room_num)\n{\n    for (int32_t i = 0; i < m_LOSNumRooms; i++) {\n        if (m_LOSRooms[i] == room_num) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// This routine transforms the world-space LOS segment [start,target] into the\n// object's local coordinates (undoing its translation and Y-rotation), then\n// performs a slab intersection test against that local AABB. The first\n// smashable item hit is returned, or NO_ITEM if none.\n//\n// (AABB = Axis-Aligned Bounding Box. It's the rectangular box defined by\n// bounds->min/max along X,Y,Z in the object's local space (no rotation).)\n//\n// @param start  World-space ray origin\n// @param target World-space ray end\n// @param item   Item to check\n// @return       Whether the item collides with the segment\nbool LOS_CheckItemIntersectSegment(\n    const GAME_VECTOR *const start, GAME_VECTOR *const target,\n    const ITEM *const item)\n{\n    const double dx = target->x - start->x;\n    const double dy = target->y - start->y;\n    const double dz = target->z - start->z;\n\n    // Translate into object-local space\n    const double ox = start->x - item->pos.x;\n    const double oy = start->y - item->pos.y;\n    const double oz = start->z - item->pos.z;\n\n    // Unrotate by -rot.y around Y axis\n    const float c = cosf(-item->rot.y * M_PI / DEG_180);\n    const float s = sinf(-item->rot.y * M_PI / DEG_180);\n    const double lx = ox * c + oz * s;\n    const double ly = oy;\n    const double lz = oz * c - ox * s;\n    const double ldx = dx * c + dz * s;\n    const double ldy = dy;\n    const double ldz = dz * c - dx * s;\n\n    // Local AABB extents from item's bounds\n    const BOUNDS_16 *const orig_bounds = Item_GetBoundsAccurate(item);\n    BOUNDS_16 patched_bounds = *orig_bounds;\n\n    const BOUNDS_16 *const bounds = &patched_bounds;\n\n    // Parametric interval [t0..t1] in Q14 fixed-point\n    double t0 = 0.0;\n    double t1 = 1.0;\n\n    // X slab\n    if (ldx != 0) {\n        double t_near = (double)(bounds->min.x - lx) / ldx;\n        double t_far = (double)(bounds->max.x - lx) / ldx;\n        if (t_near > t_far) {\n            SWAP(t_near, t_far);\n        }\n        if (t_near > t1 || t_far < t0) {\n            return false;\n        }\n        CLAMPL(t0, t_near);\n        CLAMPG(t1, t_far);\n    } else if (lx < bounds->min.x || lx > bounds->max.x) {\n        return false;\n    }\n\n    // Y slab\n    if (ldy != 0) {\n        double t_near = (double)(bounds->min.y - ly) / ldy;\n        double t_far = (double)(bounds->max.y - ly) / ldy;\n        if (t_near > t_far) {\n            SWAP(t_near, t_far);\n        }\n        if (t_near > t1 || t_far < t0) {\n            return false;\n        }\n        CLAMPL(t0, t_near);\n        CLAMPG(t1, t_far);\n    } else if (ly < bounds->min.y || ly > bounds->max.y) {\n        return false;\n    }\n\n    // Z slab\n    if (ldz != 0) {\n        double t_near = (double)(bounds->min.z - lz) / ldz;\n        double t_far = (double)(bounds->max.z - lz) / ldz;\n        if (t_near > t_far) {\n            SWAP(t_near, t_far);\n        }\n        if (t_near > t1 || t_far < t0) {\n            return false;\n        }\n        CLAMPL(t0, t_near);\n        CLAMPG(t1, t_far);\n    } else if (lz < bounds->min.z || lz > bounds->max.z) {\n        return false;\n    }\n\n    // world-space hit position = start + t0 * (target-start)\n    target->x = start->x + t0 * dx;\n    target->y = start->y + t0 * dy;\n    target->z = start->z + t0 * dz;\n\n    return true;\n}\n\n// Smashes all objects along the ray path.\n//\n// @param start  World-space ray origin\n// @param target World-space ray end\n// @return       First smashable item's index, or NO_ITEM if none hit\nint32_t LOS_CheckSmashable(\n    const GAME_VECTOR start, const GAME_VECTOR target,\n    XYZ_32 *const out_hit_pos)\n{\n    int32_t best_dist = INT32_MAX;\n    int16_t best_item_num = NO_ITEM;\n\n    for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (item->status == IS_DEACTIVATED) {\n            continue;\n        }\n        if (!Object_IsType(item->object_id, g_SmashableObjects)\n            && !Object_IsType(item->object_id, g_ShatterableObjects)\n            && !Object_IsType(item->object_id, g_HeavyShatterableObjects)) {\n            continue;\n        }\n\n        GAME_VECTOR hit_pos = target;\n        if (!LOS_CheckItemIntersectSegment(&start, &hit_pos, item)) {\n            continue;\n        }\n\n        // Confirm item is reachable via visible rooms\n        {\n            GAME_VECTOR start_tmp = start;\n            GAME_VECTOR target_tmp = {\n                .pos = item->pos,\n            };\n            if (!LOS_Check(&start_tmp, &target_tmp, false)) {\n                continue;\n            }\n        }\n\n        // Ray segment intersects the object's local AABB\n        const int32_t dist = Item_GetDistance(item, start.pos);\n        if (dist < best_dist) {\n            best_dist = dist;\n            best_item_num = item_num;\n            if (out_hit_pos != nullptr) {\n                *out_hit_pos = hit_pos.pos;\n            }\n        }\n    }\n\n    return best_item_num;\n}\n\nstatic int32_t M_CheckX(\n    const GAME_VECTOR *const start, GAME_VECTOR *const target)\n{\n    const int32_t dx = target->x - start->x;\n    if (dx == 0) {\n        return 1;\n    }\n\n    const int32_t dy = ((target->y - start->y) * WALL_L) / dx;\n    const int32_t dz = ((target->z - start->z) * WALL_L) / dx;\n\n    int16_t room_num = start->room_num;\n    int16_t last_room_num = start->room_num;\n\n    M_ResetLOSRooms(room_num);\n\n    if (dx < 0) {\n        XYZ_32 cur_pos;\n        cur_pos.x = ROUND_TO_SECTOR(start->x);\n        cur_pos.y = start->y + ((dy * (cur_pos.x - start->x)) >> WALL_SHIFT);\n        cur_pos.z = start->z + ((dz * (cur_pos.x - start->x)) >> WALL_SHIFT);\n\n        while (cur_pos.x > target->x) {\n            XYZ_32 sample_pos = cur_pos;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = room_num;\n                    return -1;\n                }\n            }\n\n            if (room_num != last_room_num) {\n                last_room_num = room_num;\n                M_TryPushLOSRoom(room_num);\n            }\n\n            sample_pos.x -= 1;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = last_room_num;\n                    return 0;\n                }\n            }\n\n            cur_pos.x -= WALL_L;\n            cur_pos.y -= dy;\n            cur_pos.z -= dz;\n        }\n    } else {\n        XYZ_32 cur_pos;\n        cur_pos.x = ROUND_TO_SECTOR_END(start->x);\n        cur_pos.y = start->y + (((cur_pos.x - start->x) * dy) >> WALL_SHIFT);\n        cur_pos.z = start->z + (((cur_pos.x - start->x) * dz) >> WALL_SHIFT);\n\n        while (cur_pos.x < target->x) {\n            XYZ_32 sample_pos = cur_pos;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = room_num;\n                    return -1;\n                }\n            }\n\n            if (room_num != last_room_num) {\n                last_room_num = room_num;\n                M_TryPushLOSRoom(room_num);\n            }\n\n            sample_pos.x += 1;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = last_room_num;\n                    return 0;\n                }\n            }\n\n            cur_pos.x += WALL_L;\n            cur_pos.y += dy;\n            cur_pos.z += dz;\n        }\n    }\n\n    target->room_num = room_num;\n    return 1;\n}\n\nstatic int32_t M_CheckZ(\n    const GAME_VECTOR *const start, GAME_VECTOR *const target)\n{\n    const int32_t dz = target->z - start->z;\n    if (dz == 0) {\n        return 1;\n    }\n\n    const int32_t dx = ((target->x - start->x) * WALL_L) / dz;\n    const int32_t dy = ((target->y - start->y) * WALL_L) / dz;\n\n    int16_t room_num = start->room_num;\n    int16_t last_room_num = start->room_num;\n\n    M_ResetLOSRooms(room_num);\n\n    if (dz < 0) {\n        XYZ_32 cur_pos;\n        cur_pos.z = ROUND_TO_SECTOR(start->z);\n        cur_pos.x = start->x + ((dx * (cur_pos.z - start->z)) >> WALL_SHIFT);\n        cur_pos.y = start->y + ((dy * (cur_pos.z - start->z)) >> WALL_SHIFT);\n\n        while (cur_pos.z > target->z) {\n            XYZ_32 sample_pos = cur_pos;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = room_num;\n                    return -1;\n                }\n            }\n\n            if (room_num != last_room_num) {\n                last_room_num = room_num;\n                M_TryPushLOSRoom(room_num);\n            }\n\n            sample_pos.z -= 1;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = last_room_num;\n                    return 0;\n                }\n            }\n\n            cur_pos.z -= WALL_L;\n            cur_pos.x -= dx;\n            cur_pos.y -= dy;\n        }\n    } else {\n        XYZ_32 cur_pos;\n        cur_pos.z = ROUND_TO_SECTOR_END(start->z);\n        cur_pos.x = start->x + ((dx * (cur_pos.z - start->z)) >> WALL_SHIFT);\n        cur_pos.y = start->y + ((dy * (cur_pos.z - start->z)) >> WALL_SHIFT);\n\n        while (cur_pos.z < target->z) {\n            XYZ_32 sample_pos = cur_pos;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = room_num;\n                    return -1;\n                }\n            }\n\n            if (room_num != last_room_num) {\n                last_room_num = room_num;\n                M_TryPushLOSRoom(room_num);\n            }\n\n            sample_pos.z += 1;\n\n            {\n                const SECTOR *const sector =\n                    Room_GetSector(sample_pos, &room_num);\n                const int32_t height = Room_GetHeight(sector, sample_pos);\n                const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n                if (cur_pos.y > height || cur_pos.y < ceiling) {\n                    target->pos = cur_pos;\n                    target->room_num = last_room_num;\n                    return 0;\n                }\n            }\n\n            cur_pos.z += WALL_L;\n            cur_pos.x += dx;\n            cur_pos.y += dy;\n        }\n    }\n\n    target->room_num = room_num;\n    return 1;\n}\n\nstatic int32_t M_ClipTargetSimple(\n    const GAME_VECTOR *const start, GAME_VECTOR *const target)\n{\n    const SECTOR *sector = Room_GetSector(target->pos, &target->room_num);\n\n    // This function exists because of issue #4070.\n    int32_t dx = target->x - start->x;\n    int32_t dy = target->y - start->y;\n    int32_t dz = target->z - start->z;\n\n    const int32_t height = Room_GetHeight(sector, target->pos);\n    if (target->y > height && start->y < height) {\n        target->y = height;\n        target->x = start->x + dx * (height - start->y) / dy;\n        target->z = start->z + dz * (height - start->y) / dy;\n        return false;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, target->pos);\n    if (target->y < ceiling && start->y > ceiling) {\n        target->y = ceiling;\n        target->x = start->x + dx * (ceiling - start->y) / dy;\n        target->z = start->z + dz * (ceiling - start->y) / dy;\n        return false;\n    }\n\n    return true;\n}\n\nstatic int32_t M_ClipTargetWithSlopes(\n    const GAME_VECTOR *const start, GAME_VECTOR *const target)\n{\n    int16_t room_num = target->room_num;\n    const SECTOR *sector = Room_GetSector(target->pos, &room_num);\n\n    if (target->y > Room_GetHeight(sector, target->pos)) {\n        const XYZ_32 origin = {\n            start->x + ((M_CLIP_1 - 1) * (target->x - start->x) / M_CLIP_1),\n            start->y + ((M_CLIP_1 - 1) * (target->y - start->y) / M_CLIP_1),\n            start->z + ((M_CLIP_1 - 1) * (target->z - start->z) / M_CLIP_1),\n        };\n        XYZ_32 delta;\n        for (int32_t i = M_CLIP_2 - 1; i > 0; i--) {\n            delta.x = origin.x + (i * (target->x - origin.x) / M_CLIP_2);\n            delta.y = origin.y + (i * (target->y - origin.y) / M_CLIP_2);\n            delta.z = origin.z + (i * (target->z - origin.z) / M_CLIP_2);\n            sector = Room_GetSector(delta, &room_num);\n            if (delta.y < Room_GetHeight(sector, delta)) {\n                break;\n            }\n        }\n\n        target->pos = delta;\n        target->room_num = room_num;\n        return 0;\n    }\n\n    if (target->y < Room_GetCeiling(sector, target->pos)) {\n        const XYZ_32 origin = {\n            start->x + ((M_CLIP_1 - 1) * (target->x - start->x) / M_CLIP_1),\n            start->y + ((M_CLIP_1 - 1) * (target->y - start->y) / M_CLIP_1),\n            start->z + ((M_CLIP_1 - 1) * (target->z - start->z) / M_CLIP_1),\n        };\n        XYZ_32 delta;\n        for (int32_t i = M_CLIP_2 - 1; i > 0; i--) {\n            delta.x = origin.x + (i * (target->x - origin.x) / M_CLIP_2);\n            delta.y = origin.y + (i * (target->y - origin.y) / M_CLIP_2);\n            delta.z = origin.z + (i * (target->z - origin.z) / M_CLIP_2);\n\n            sector = Room_GetSector(delta, &room_num);\n            if (delta.y > Room_GetCeiling(sector, delta)) {\n                break;\n            }\n        }\n\n        target->pos = delta;\n        target->room_num = room_num;\n        return 0;\n    }\n\n    return 1;\n}\n\nbool LOS_Check(\n    const GAME_VECTOR *const start, GAME_VECTOR *const target,\n    const bool use_slope_clipping)\n{\n    const int32_t dx = ABS(target->x - start->x);\n    const int32_t dz = ABS(target->z - start->z);\n\n    int32_t los1;\n    int32_t los2;\n    if (dz > dx) {\n        los1 = M_CheckX(start, target);\n        los2 = M_CheckZ(start, target);\n    } else {\n        los1 = M_CheckZ(start, target);\n        los2 = M_CheckX(start, target);\n    }\n\n    if (!los2) {\n        return false;\n    }\n\n    if (dx == 0 && dz == 0) {\n        target->room_num = start->room_num;\n    }\n\n    const bool clip_result =\n        (use_slope_clipping ? M_ClipTargetWithSlopes(start, target)\n                            : M_ClipTargetSimple(start, target));\n    return clip_result && los1 == 1 && los2 == 1;\n}\n"
  },
  {
    "path": "src/trx/game/collision/los.h",
    "content": "#pragma once\n\n#include <trx/game/types.h>\n\nbool LOS_Check(\n    const GAME_VECTOR *start, GAME_VECTOR *target, bool use_detailed_clipping);\nbool LOS_CheckItemIntersectSegment(\n    const GAME_VECTOR *start, GAME_VECTOR *target, const ITEM *item);\nint32_t LOS_CheckSmashable(\n    GAME_VECTOR start, GAME_VECTOR target, XYZ_32 *out_hit_pos);\n"
  },
  {
    "path": "src/trx/game/collision/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/items/types.h>\n\ntypedef struct {\n    int32_t floor;\n    int32_t ceiling;\n    int32_t type;\n} COLL_SIDE;\n\ntypedef enum {\n    // clang-format off\n    COLL_NONE      = 0x00,\n    COLL_FRONT     = 0x01,\n    COLL_LEFT      = 0x02,\n    COLL_RIGHT     = 0x04,\n    COLL_TOP       = 0x08,\n    COLL_TOP_FRONT = 0x10,\n    COLL_CLAMP     = 0x20,\n    // clang-format on\n} COLL_TYPE;\n\ntypedef struct {\n    COLL_SIDE side_mid;\n    COLL_SIDE side_front;\n    COLL_SIDE side_left;\n    COLL_SIDE side_right;\n    COLL_SIDE side_left2;\n    COLL_SIDE side_right2;\n    int32_t radius;\n    int32_t bad_pos;\n    int32_t bad_neg;\n    int32_t bad_ceiling;\n    XYZ_32 shift;\n    XYZ_32 old;\n    int16_t old_anim_state;\n    int16_t old_anim_num;\n    int16_t old_frame_num;\n    int16_t facing;\n    DIRECTION quadrant;\n    int16_t coll_type;\n    XZ_16 tilt;\n    int8_t hit_by_baddie;\n    int8_t hit_static;\n    // clang-format off\n    uint16_t slopes_are_walls:   1; // 0x01 1\n    uint16_t slopes_are_pits:    1; // 0x02 2\n    uint16_t lava_is_pit:        1; // 0x04 4\n    uint16_t enable_baddie_push: 1; // 0x08 8\n    uint16_t enable_hit:         1; // 0x10 16\n    uint16_t pad:                11;\n    // clang-format on\n} COLL_INFO;\n\ntypedef struct {\n    XYZ_32 pos;\n    int32_t r;\n} SPHERE;\n\ntypedef struct {\n    BOUNDS_16 bounds;\n    XYZ_32 pos;\n    XYZ_16 rot;\n} COLL_ITEM;\n"
  },
  {
    "path": "src/trx/game/collision.h",
    "content": "#pragma once\n\n#include <trx/game/collision/common.h>\n#include <trx/game/collision/types.h>\n"
  },
  {
    "path": "src/trx/game/console/cmd/clear.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    Console_Clear();\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"cls|clear\", M_Entrypoint, GS_ID(\"console/cmd/clear/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/config.c",
    "content": "#include <trx/game/console/cmd/config.h>\n\n#include <trx/config.h>\n#include <trx/config/dynamic_enum.h>\n#include <trx/core/colors.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <stdio.h>\n#include <string.h>\n\nstatic char *M_NormalizeValue(const char *const value)\n{\n    if (value == nullptr) {\n        return nullptr;\n    }\n\n    char *const result = Memory_DupStr(value);\n    for (uint32_t i = 0; i < strlen(result); i++) {\n        if (result[i] == '_') {\n            result[i] = '-';\n        }\n    }\n    return result;\n}\n\nstatic const char *M_Resolve(const char *const option_name)\n{\n    const char *dot = strrchr(option_name, '.');\n    if (dot) {\n        return dot + 1;\n    }\n    return option_name;\n}\n\nstatic const CONFIG_OPTION *M_GetOptionFromKey(const char *const key)\n{\n    VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));\n\n    for (const CONFIG_OPTION *option = Config_GetOptionMap();\n         option->name != nullptr; option++) {\n        STRING_FUZZY_SOURCE source_item = {\n            .key = (const char *)Console_Cmd_Config_NormalizeKey(option->name),\n            .value = (void *)option,\n            .weight = 1,\n        };\n        Vector_Add(source, &source_item);\n    }\n\n    VECTOR *matches = String_FuzzyMatch(key, source);\n    const CONFIG_OPTION *result = nullptr;\n    if (matches->count == 0) {\n        Console_LogError(GS(\"general/osd/config_option_unknown_option\"), key);\n    } else if (matches->count == 1) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, 0);\n        result = match->value;\n    } else if (matches->count == 2) {\n        const STRING_FUZZY_MATCH *const match1 = Vector_Get(matches, 0);\n        const STRING_FUZZY_MATCH *const match2 = Vector_Get(matches, 1);\n        Console_LogError(\n            GS(\"general/osd/ambiguous_input_2\"), match1->key, match2->key);\n    } else if (matches->count >= 3) {\n        const STRING_FUZZY_MATCH *const match1 = Vector_Get(matches, 0);\n        const STRING_FUZZY_MATCH *const match2 = Vector_Get(matches, 1);\n        Console_LogError(\n            GS(\"general/osd/ambiguous_input_3\"), match1->key, match2->key);\n    }\n\n    for (int32_t i = 0; i < source->count; i++) {\n        const STRING_FUZZY_SOURCE *const source_item = Vector_Get(source, i);\n        Memory_Free((char *)source_item->key);\n    }\n\n    Vector_Free(matches);\n    Vector_Free(source);\n    return result;\n}\n\nstatic char *M_FormatValuesList(const VECTOR *const values)\n{\n    if (values == nullptr || values->count == 0) {\n        return nullptr;\n    }\n\n    char *result = nullptr;\n    for (int32_t i = 0; i < values->count; i++) {\n        const char *const value = *(char **)Vector_Get(values, i);\n        if (value == nullptr) {\n            continue;\n        }\n\n        char *normalized = M_NormalizeValue(value);\n        if (normalized == nullptr) {\n            continue;\n        }\n\n        if (result == nullptr) {\n            result = normalized;\n        } else {\n            char *const joined = String_Format(\"%s, %s\", result, normalized);\n            Memory_FreePointer(&result);\n            Memory_FreePointer(&normalized);\n            result = joined;\n        }\n    }\n\n    return result;\n}\n\nstatic char *M_FormatDynamicEnumDefaults(const CONFIG_OPTION *const option)\n{\n    VECTOR *const values = Vector_Create(sizeof(char *));\n    const char *const default_value = \"-\";\n    Vector_Add(values, &default_value);\n\n    const int32_t value_count = Config_DynamicEnum_GetValueCount(option);\n    for (int32_t i = 0; i < value_count; i++) {\n        const char *const value = Config_DynamicEnum_GetValueAt(option, i);\n        if (value != nullptr) {\n            Vector_Add(values, &value);\n        }\n    }\n\n    char *const result = M_FormatValuesList(values);\n    Vector_Free(values);\n    return result;\n}\n\nstatic char *M_GetValueForConsole(const CONFIG_OPTION *const option)\n{\n    if ((option->type == COT_STRING || option->type == COT_DYNAMIC_ENUM)\n        && *(char **)option->target == nullptr) {\n        return Memory_DupStr(\"(null)\");\n    }\n\n    const char *const value = Config_GetOptionValueAsString(option, false);\n    if (value == nullptr) {\n        return nullptr;\n    }\n\n    if (option->type == COT_ENUM || option->type == COT_DYNAMIC_ENUM) {\n        return M_NormalizeValue(value);\n    }\n    return Memory_DupStr(value);\n}\n\nstatic bool M_TryApplyOptionValue(\n    const CONFIG_OPTION *const option, const char *const new_value)\n{\n    if (strcmp(new_value, \"-\") == 0) {\n        return Config_RestoreOptionDefault(option->target);\n    }\n    if (Config_SetOptionValueFromString(option, new_value)) {\n        return true;\n    }\n\n    if (option->type != COT_ENUM && option->type != COT_DYNAMIC_ENUM) {\n        return false;\n    }\n\n    char *normalized = M_NormalizeValue(new_value);\n    if (normalized != nullptr) {\n        const bool different = strcmp(normalized, new_value) != 0;\n        if (different && Config_SetOptionValueFromString(option, normalized)) {\n            Memory_FreePointer(&normalized);\n            return true;\n        }\n        Memory_FreePointer(&normalized);\n    }\n\n    char *underscore = Memory_DupStr(new_value);\n    bool different = false;\n    for (uint32_t i = 0; i < strlen(underscore); i++) {\n        if (underscore[i] == '-') {\n            underscore[i] = '_';\n            different = true;\n        }\n    }\n    if (different && Config_SetOptionValueFromString(option, underscore)) {\n        Memory_FreePointer(&underscore);\n        return true;\n    }\n    Memory_FreePointer(&underscore);\n    return false;\n}\n\n// Builds a source list of all options and returns fuzzy-match results.\n// Caller must free the result vector with Vector_Free().\nstatic VECTOR *M_GetOptionsFuzzy(const char *const key)\n{\n    VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));\n\n    for (const CONFIG_OPTION *option = Config_GetOptionMap();\n         option->name != nullptr; option++) {\n        STRING_FUZZY_SOURCE source_item = {\n            .key = (const char *)Console_Cmd_Config_NormalizeKey(option->name),\n            .value = (void *)option,\n            .weight = 1,\n        };\n        Vector_Add(source, &source_item);\n    }\n\n    VECTOR *matches = String_FuzzyMatch(key, source);\n\n    for (int32_t i = 0; i < source->count; i++) {\n        const STRING_FUZZY_SOURCE *const source_item = Vector_Get(source, i);\n        Memory_Free((char *)source_item->key);\n    }\n\n    Vector_Free(source);\n    return matches;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    COMMAND_RESULT result = CR_BAD_INVOCATION;\n\n    char *key = Memory_DupStr(ctx->args);\n    char *const space = strchr(key, ' ');\n    const char *new_value = nullptr;\n    if (space != nullptr) {\n        new_value = space + 1;\n        space[0] = '\\0'; // nullptr-terminate the key\n    }\n\n    const CONFIG_OPTION *const option = M_GetOptionFromKey(key);\n    if (option == nullptr) {\n        result = CR_FAILURE;\n    } else {\n        result = Console_Cmd_Config_Helper(option, new_value);\n    }\n\ncleanup:\n    Memory_FreePointer(&key);\n    return result;\n}\n\n// Return a comma-delimited list of valid values for the option.\n// Caller must free the result with Memory_Free*().\nstatic char *M_GetAvailableOptions(const CONFIG_OPTION *const option)\n{\n    if (option == nullptr) {\n        return nullptr;\n    }\n\n    switch (option->type) {\n    case COT_BOOL:\n        return Memory_DupStr(GS(\"general/osd/command_bool\"));\n\n    case COT_INT32:\n        return Memory_DupStr(GS(\"general/osd/command_integer\"));\n\n    case COT_DOUBLE:\n    case COT_FLOAT:\n        return Memory_DupStr(GS(\"general/osd/command_decimal\"));\n\n    case COT_FLOAT_PERCENT:\n        return Memory_DupStr(GS(\"general/osd/command_percent\"));\n\n    case COT_ENUM: {\n        const char *enum_name = (const char *)option->param;\n        VECTOR *const values = EnumMap_ListValues(enum_name);\n        if (values == nullptr) {\n            return nullptr;\n        }\n        char *const result = M_FormatValuesList(values);\n        Vector_Free(values);\n        return result;\n    }\n\n    case COT_DYNAMIC_ENUM:\n        return M_FormatDynamicEnumDefaults(option);\n\n    case COT_STRING:\n        return nullptr;\n\n    default:\n        return nullptr;\n    }\n}\n\nchar *Console_Cmd_Config_NormalizeKey(const char *key)\n{\n    // TODO: Once we support arbitrary glyphs, this conversion should\n    // no longer be necessary.\n    char *result = Memory_DupStr(key);\n    for (uint32_t i = 0; i < strlen(result); i++) {\n        if (result[i] == '_') {\n            result[i] = '-';\n        }\n    }\n    return result;\n}\n\nconst CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget(\n    const void *const target)\n{\n    for (const CONFIG_OPTION *option = Config_GetOptionMap();\n         option->name != nullptr; option++) {\n        if (option->target == target) {\n            return option;\n        }\n    }\n\n    return nullptr;\n}\n\nCOMMAND_RESULT Console_Cmd_Config_Helper(\n    const CONFIG_OPTION *const option, const char *const new_value)\n{\n    ASSERT(option != nullptr);\n\n    char *normalized_name = Console_Cmd_Config_NormalizeKey(option->name);\n    COMMAND_RESULT result = CR_FAILURE;\n\n    if (new_value == nullptr || String_IsEmpty(new_value)) {\n        char *value_str = M_GetValueForConsole(option);\n        if (value_str == nullptr) {\n            result = CR_FAILURE;\n            goto cleanup;\n        }\n        Console_Log(\n            GS(\"general/osd/config_option_get\"), normalized_name, value_str);\n        Memory_FreePointer(&value_str);\n        result = CR_SUCCESS;\n        goto cleanup;\n    }\n\n    if (M_TryApplyOptionValue(option, new_value)) {\n        Config_Update();\n        char *value_str = M_GetValueForConsole(option);\n        ASSERT(value_str != nullptr);\n        Console_Log(\n            GS(\"general/osd/config_option_set\"), normalized_name, value_str);\n        Memory_FreePointer(&value_str);\n        result = CR_SUCCESS;\n    } else {\n        // Report bad invocation on the provided new value\n        Console_LogError(GS(\"general/osd/command_bad_invocation\"), new_value);\n        char *available_options = M_GetAvailableOptions(option);\n        if (available_options != nullptr) {\n            Console_Log(\n                GS(\"general/osd/command_valid_values\"), available_options);\n            Memory_FreePointer(&available_options);\n        }\n        result = CR_FAILURE;\n    }\n\ncleanup:\n    Memory_FreePointer(&normalized_name);\n    return result;\n}\n\nVECTOR *Console_Cmd_Config_GetOptionsFromKey(const char *const key)\n{\n    return M_GetOptionsFuzzy(key);\n}\n\nREGISTER_CONSOLE_COMMAND(\"set\", M_Entrypoint, GS_ID(\"console/cmd/set/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/config.h",
    "content": "#pragma once\n\n#include <trx/config/option.h>\n#include <trx/core/vector.h>\n#include <trx/game/console/common.h>\n\n#include <stddef.h>\n\nchar *Console_Cmd_Config_NormalizeKey(const char *key);\nbool Console_Cmd_Config_SetCurrentValue(\n    const CONFIG_OPTION *option, const char *new_value);\n\n// Returns a vector of STRING_FUZZY_MATCH entries for the given key.\n// Caller must free the result with Vector_Free().\nVECTOR *Console_Cmd_Config_GetOptionsFromKey(const char *key);\n\n// Returns a pointer to a CONFIG_OPTION from a pointer into g_Config.\nconst CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget(const void *target);\n\nCOMMAND_RESULT Console_Cmd_Config_Helper(\n    const CONFIG_OPTION *option, const char *new_value);\n"
  },
  {
    "path": "src/trx/game/console/cmd/debug.c",
    "content": "#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/console/cmd/config.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <string.h>\n\ntypedef struct {\n    bool *target;\n    const CONFIG_OPTION *option;\n} DEBUG_OPTION_ENTRY;\n\nstatic DEBUG_OPTION_ENTRY m_AllOptions[] = {\n    { &g_Config.debug.enable_debug_portals, nullptr },\n    { &g_Config.debug.enable_debug_room_clip, nullptr },\n    { &g_Config.debug.enable_debug_triggers, nullptr },\n    { &g_Config.debug.enable_debug_spheres, nullptr },\n    { &g_Config.debug.enable_debug_bounding_boxes, nullptr },\n    { &g_Config.debug.enable_debug_pos, nullptr },\n    { &g_Config.debug.enable_debug_anim, nullptr },\n    { &g_Config.debug.enable_debug_camera, nullptr },\n    { &g_Config.debug.enable_debug_status, nullptr },\n    { nullptr, nullptr }\n};\n\nstatic void M_InitOptions(void)\n{\n    for (int32_t i = 0; m_AllOptions[i].target; i++) {\n        m_AllOptions[i].option =\n            Console_Cmd_Config_GetOptionFromTarget(m_AllOptions[i].target);\n    }\n}\n\nstatic VECTOR *M_BuildMatches(const char *const key)\n{\n    VECTOR *const matches = Console_Cmd_Config_GetOptionsFromKey(key);\n    VECTOR *const filtered = Vector_Create(sizeof(STRING_FUZZY_MATCH));\n\n    for (int32_t i = 0; i < matches->count; i++) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n        const CONFIG_OPTION *const option = match->value;\n        for (int32_t j = 0; m_AllOptions[j].target; j++) {\n            if (option == m_AllOptions[j].option) {\n                Vector_Add(filtered, match);\n                break;\n            }\n        }\n    }\n\n    Vector_Free(matches);\n    return filtered;\n}\n\nstatic void M_LogOption(const CONFIG_OPTION *const option)\n{\n    char *const name = Console_Cmd_Config_NormalizeKey(option->name);\n    Console_Log(\n        GS(\"general/osd/config_option_set\"), name,\n        Config_GetOptionValueAsString(option, false));\n    Memory_Free(name);\n}\n\nstatic void M_ShowOption(const CONFIG_OPTION *const option)\n{\n    char *const name = Console_Cmd_Config_NormalizeKey(option->name);\n    Console_Log(\n        GS(\"general/osd/config_option_get\"), name,\n        Config_GetOptionValueAsString(option, false));\n    Memory_Free(name);\n}\n\nstatic bool M_UpdateOption(const CONFIG_OPTION *const option, const bool enable)\n{\n    bool *const target = (bool *)option->target;\n    if (*target == enable) {\n        return false;\n    }\n\n    *target = enable;\n    M_LogOption(option);\n    return true;\n}\n\nstatic bool M_UpdateAll(const bool enable)\n{\n    bool changed = false;\n    for (int32_t i = 0; m_AllOptions[i].target != nullptr; i++) {\n        if (M_UpdateOption(m_AllOptions[i].option, enable)) {\n            changed = true;\n        }\n    }\n    return changed;\n}\n\nstatic void M_ShowStatus(void)\n{\n    for (int32_t i = 0; m_AllOptions[i].target != nullptr; i++) {\n        M_ShowOption(m_AllOptions[i].option);\n    }\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx)\n{\n    if (m_AllOptions[0].option == nullptr) {\n        M_InitOptions();\n    }\n\n    if (String_IsEmpty(ctx->args)) {\n        M_ShowStatus();\n        return CR_SUCCESS;\n    }\n\n    char *args = Memory_DupStr(ctx->args);\n    char *space = strchr(args, ' ');\n    char *key = args;\n    char *val = nullptr;\n\n    if (space) {\n        *space = '\\0';\n        val = space + 1;\n    }\n    if (val != nullptr && strchr(val, ' ') != nullptr) {\n        Memory_Free(args);\n        return CR_BAD_INVOCATION;\n    }\n\n    bool use_set = false;\n    bool explicit_enable = false;\n    if (val == nullptr && String_ParseBool(key, &explicit_enable)) {\n        if (M_UpdateAll(explicit_enable)) {\n            Config_Update();\n        }\n        Memory_Free(args);\n        return CR_SUCCESS;\n    } else if (val != nullptr && String_ParseBool(val, &explicit_enable)) {\n        use_set = true;\n    } else if (val != nullptr) {\n        Memory_Free(args);\n        return CR_BAD_INVOCATION;\n    }\n\n    VECTOR *const matches = M_BuildMatches(key);\n    if (matches->count == 0) {\n        Console_LogError(GS(\"general/osd/config_option_unknown_option\"), key);\n        Vector_Free(matches);\n        Memory_Free(args);\n        return CR_FAILURE;\n    }\n\n    bool changed = false;\n    for (int32_t i = 0; i < matches->count; i++) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n        const CONFIG_OPTION *const option = match->value;\n        const bool enable =\n            use_set ? explicit_enable : !*(bool *)option->target;\n        if (M_UpdateOption(option, enable)) {\n            changed = true;\n        }\n    }\n\n    if (changed) {\n        Config_Update();\n    }\n    Vector_Free(matches);\n    Memory_Free(args);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"debug\", M_Entrypoint, GS_ID(\"console/cmd/debug/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/die.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/sound.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->hit_points <= 0) {\n        return CR_UNAVAILABLE;\n    }\n\n    Sound_Effect(SFX_LARA_FALL, &lara_item->pos, SPM_NORMAL);\n    Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, SPM_NORMAL);\n    Item_Explode(lara->item_num, -1, 1);\n\n    lara_item->hit_points = 0;\n    lara_item->flags |= IF_ONE_SHOT;\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"abortion\", M_Entrypoint, nullptr)\nREGISTER_CONSOLE_COMMAND(\"natla-?s(uc|tin)ks\", M_Entrypoint, nullptr)\n"
  },
  {
    "path": "src/trx/game/console/cmd/easy_config.c",
    "content": "#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/cmd/config.h>\n#include <trx/game/console/registry.h>\n\ntypedef struct {\n    const char *prefix;\n    void *target;\n} COMMAND_TO_OPTION_MAP;\n\nstatic COMMAND_TO_OPTION_MAP m_CommandToOptionMap[] = {\n    { \"braid\", &g_Config.visuals.enable_braid },\n    { \"cheats\", &g_Config.gameplay.enable_cheats },\n    { \"vsync\", &g_Config.rendering.enable_vsync },\n    { \"wireframe\", &g_Config.rendering.enable_wireframe },\n    { \"fps\", &g_Config.rendering.fps },\n    { \"lighting\", &g_Config.rendering.enable_lighting },\n    { \"textures\", &g_Config.rendering.enable_textures },\n    { nullptr, nullptr },\n};\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    COMMAND_TO_OPTION_MAP *match = m_CommandToOptionMap;\n    while (match->target != nullptr) {\n        if (String_Equivalent(match->prefix, ctx->prefix)) {\n            return Console_Cmd_Config_Helper(\n                Console_Cmd_Config_GetOptionFromTarget(match->target),\n                ctx->args);\n        }\n        match++;\n    }\n\n    return CR_FAILURE;\n}\n\nREGISTER_CONSOLE_COMMAND(\"braid\", M_Entrypoint, GS_ID(\"console/cmd/braid/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"cheats\", M_Entrypoint, GS_ID(\"console/cmd/cheats/help\"))\nREGISTER_CONSOLE_COMMAND(\"vsync\", M_Entrypoint, GS_ID(\"console/cmd/vsync/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"wireframe\", M_Entrypoint, GS_ID(\"console/cmd/wireframe/help\"))\nREGISTER_CONSOLE_COMMAND(\"fps\", M_Entrypoint, GS_ID(\"console/cmd/fps/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"lighting\", M_Entrypoint, GS_ID(\"console/cmd/lighting/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"textures\", M_Entrypoint, GS_ID(\"console/cmd/textures/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/end_level.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/lara/cheat.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    if (GF_GetCurrentLevel() == nullptr\n        || GF_GetCurrentLevel()->type == GFL_TITLE) {\n        return CR_UNAVAILABLE;\n    }\n\n    Lara_Cheat_EndLevel();\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"endlevel\", M_Entrypoint, GS_ID(\"console/cmd/end_level/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"nextlevel\", M_Entrypoint, GS_ID(\"console/cmd/end_level/help\"))\nREGISTER_CONSOLE_COMMAND(\"end-level\", M_Entrypoint, nullptr)\nREGISTER_CONSOLE_COMMAND(\"next-level\", M_Entrypoint, nullptr)\n"
  },
  {
    "path": "src/trx/game/console/cmd/exit_game.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    GF_OverrideCommand((GF_COMMAND) { .action = GF_EXIT_GAME });\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"exit\", M_Entrypoint, GS_ID(\"console/cmd/exit/help\"))\nREGISTER_CONSOLE_COMMAND(\"quit\", M_Entrypoint, GS_ID(\"console/cmd/exit/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/exit_to_title.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    GF_OverrideCommand((GF_COMMAND) { .action = GF_EXIT_TO_TITLE });\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"title\", M_Entrypoint, GS_ID(\"console/cmd/title/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/flipmap.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/rooms.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (GF_GetCurrentLevel() == nullptr\n        || GF_GetCurrentLevel()->type == GFL_TITLE) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (!Game_IsLoaded()) {\n        return CR_UNAVAILABLE;\n    }\n\n    bool new_state = Room_GetFlipStatus();\n    if (String_IsEmpty(ctx->args)) {\n        new_state = !new_state;\n    } else if (!String_ParseBool(ctx->args, &new_state)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    if (Room_GetFlipStatus() == new_state) {\n        Console_LogWarning(\n            new_state ? GS(\"general/osd/flipmap_fail_already_on\")\n                      : GS(\"general/osd/flipmap_fail_already_off\"));\n        return CR_SUCCESS;\n    }\n\n    Room_FlipMap();\n    Console_Log(\n        new_state ? GS(\"general/osd/flipmap_on\")\n                  : GS(\"general/osd/flipmap_off\"));\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"flip\", M_Entrypoint, GS_ID(\"console/cmd/flipmap/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"flipmap\", M_Entrypoint, GS_ID(\"console/cmd/flipmap/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/flood.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/rooms.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    int32_t room_num;\n    if (!String_ParseInteger(ctx->args, &room_num)) {\n        if (!String_IsEmpty(ctx->args)) {\n            return CR_BAD_INVOCATION;\n        }\n\n        ITEM *const lara_item = Lara_GetItem();\n        if (lara_item == nullptr) {\n            return CR_UNAVAILABLE;\n        }\n        room_num = lara_item->room_num;\n    }\n\n    if (String_Equivalent(ctx->prefix, \"flood\")) {\n        Room_Get(room_num)->flags.underwater = true;\n    } else if (String_Equivalent(ctx->prefix, \"drain\")) {\n        Room_Get(room_num)->flags.underwater = false;\n    } else {\n        return CR_UNAVAILABLE;\n    }\n\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"flood\", M_Entrypoint, GS_ID(\"console/cmd/flood/help\"))\nREGISTER_CONSOLE_COMMAND(\"drain\", M_Entrypoint, GS_ID(\"console/cmd/drain/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/fly.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lara/cheat.h>\n#include <trx/game/lara/common.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    bool enable;\n    if (String_ParseBool(ctx->args, &enable)) {\n        if (enable) {\n            Lara_Cheat_EnterFlyMode();\n        } else {\n            Lara_Cheat_ExitFlyMode();\n        }\n        return CR_SUCCESS;\n    }\n\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status == LWS_CHEAT) {\n        Lara_Cheat_ExitFlyMode();\n    } else {\n        Lara_Cheat_EnterFlyMode();\n    }\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"fly\", M_Entrypoint, GS_ID(\"console/cmd/fly/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/give_item.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara/cheat.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/objects/vars.h>\n\n#include <stdio.h>\n#include <string.h>\n\nstatic bool M_CanTargetObjectPickup(const OBJECT_ID obj_id)\n{\n    return Object_IsType(obj_id, g_InvObjects) && Object_Get(obj_id)->loaded\n        && Object_IsType(\n               Object_GetCognateInverse(obj_id, g_ItemToInvObjectMap),\n               g_PickupObjects);\n}\n\nstatic bool M_Match(const COMMAND_CONTEXT *const ctx, const char *const cmd)\n{\n    return String_Equivalent(ctx->prefix, cmd)\n        || String_Equivalent(ctx->args, cmd);\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (M_Match(ctx, \"keys\")) {\n        return Lara_Cheat_GiveAllKeys() ? CR_SUCCESS : CR_FAILURE;\n    }\n\n    if (M_Match(ctx, \"guns\")) {\n        return Lara_Cheat_GiveAllGuns(false) ? CR_SUCCESS : CR_FAILURE;\n    }\n\n    if (M_Match(ctx, \"moreguns\")) {\n        return Lara_Cheat_GiveAllGuns(true) ? CR_SUCCESS : CR_FAILURE;\n    }\n\n    if (String_Equivalent(ctx->args, \"all\")) {\n        return Lara_Cheat_GiveAllItems() ? CR_SUCCESS : CR_FAILURE;\n    }\n\n    int32_t num = 1;\n    const char *args = ctx->args;\n    if (sscanf(ctx->args, \"%d \", &num) == 1) {\n        args = strstr(args, \" \");\n        if (args == nullptr) {\n            return CR_BAD_INVOCATION;\n        }\n        CLAMPG(num, MAX_QTY);\n        args++;\n    }\n\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    bool found = false;\n    int32_t match_count = 0;\n    OBJECT_NAME_MATCH *matches =\n        Object_IdsFromName(args, &match_count, M_CanTargetObjectPickup);\n    for (int32_t i = 0; i < match_count; i++) {\n        const OBJECT_ID obj_id = matches[i].object_id;\n        const char *const obj_name =\n            matches[i].matched_name != nullptr ? matches[i].matched_name : args;\n        Inv_AddItemNTimes(obj_id, num);\n        Console_Log(GS(\"general/osd/give_item\"), obj_name);\n        found = true;\n    }\n    Memory_FreePointer(&matches);\n\n    if (!found) {\n        Console_LogError(GS(\"general/osd/invalid_item\"), args);\n        return CR_FAILURE;\n    }\n\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"give|keys|(?:more)?guns\", M_Entrypoint, GS_ID(\"console/cmd/give/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/give_secret.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n#include <trx/game/stats/common.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_FMT_NUM \"#%d\"\n\nstatic const char *M_FormatAvailable(void)\n{\n    LEVEL_MAX_STATS *const max_stats =\n        Stats_GetLevelMaxStats(Game_GetCurrentLevel());\n    ASSERT(max_stats != nullptr);\n\n    char buf[128] = {};\n    char *ptr = buf;\n    bool first = true;\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        if ((1 << i) & max_stats->all_secrets_mask) {\n            if (!first) {\n                ptr += sprintf(ptr, \", \");\n            }\n            first = false;\n            ptr += sprintf(ptr, M_FMT_NUM, i + 1);\n        }\n    }\n    return String_FormatStatic(\"%s\", buf);\n}\n\nstatic const char *M_FormatPresent(void)\n{\n    char buf[128] = {};\n    char *ptr = buf;\n    bool first = true;\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        if (Stats_HasSecret(i)) {\n            if (!first) {\n                ptr += sprintf(ptr, \", \");\n            }\n            first = false;\n            ptr += sprintf(ptr, M_FMT_NUM, i + 1);\n        }\n    }\n    return String_FormatStatic(\"%s\", buf);\n}\n\nstatic void M_LogInvalid(const int32_t idx)\n{\n    Console_LogError(\n        GS(\"console/cmd/give/invalid_secret\"),\n        String_FormatStatic(M_FMT_NUM, idx + 1), M_FormatAvailable());\n}\n\nstatic COMMAND_RESULT M_TakeSecret(const int32_t idx)\n{\n    if (Stats_RemoveSecret(idx)) {\n        Console_Log(\n            GS(\"console/cmd/give/secret_taken\"),\n            String_FormatStatic(M_FMT_NUM, idx + 1));\n        return CR_SUCCESS;\n    }\n    M_LogInvalid(idx);\n    return CR_FAILURE;\n}\n\nstatic COMMAND_RESULT M_GiveSecret(const int32_t idx)\n{\n    if (Stats_AddSecret(idx)) {\n        Console_Log(\n            GS(\"console/cmd/give/secret_given\"),\n            String_FormatStatic(M_FMT_NUM, idx + 1));\n        return CR_SUCCESS;\n    }\n    M_LogInvalid(idx);\n    return CR_FAILURE;\n}\n\nstatic COMMAND_RESULT M_ListSecrets(void)\n{\n    const LEVEL_MAX_STATS *const max_stats =\n        Stats_GetLevelMaxStats(Game_GetCurrentLevel());\n    ASSERT(max_stats != nullptr);\n    RESUME_INFO *const info = Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n    ASSERT(info != nullptr);\n\n    const char *const buf = M_FormatPresent();\n    Console_Log(\n        strcmp(buf, \"\") == 0 ? GS(\"console/cmd/give/secret_none\")\n                             : GS(\"console/cmd/give/secret_list\"),\n        info->stats.secret_count, max_stats->max_secret_count, buf);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_GiveAllSecrets(void)\n{\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        Stats_AddSecret(i); // Not all `i` are valid, but it's handled inside\n    }\n    return M_ListSecrets();\n}\n\nstatic COMMAND_RESULT M_TakeAllSecrets(void)\n{\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        Stats_RemoveSecret(i); // Not all `i` are valid, but it's handled inside\n    }\n    return M_ListSecrets();\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (String_IsEmpty(ctx->args)) {\n        return M_ListSecrets();\n    }\n\n    if (String_Equivalent(ctx->args, \"give\")) {\n        return M_GiveAllSecrets();\n    }\n\n    if (String_Equivalent(ctx->args, \"take\")) {\n        return M_TakeAllSecrets();\n    }\n\n    char *args = Memory_DupStr(ctx->args);\n    char *subcmd = args;\n    char *param = nullptr;\n\n    if (!String_IsEmpty(ctx->args)) {\n        param = strchr(args, ' ');\n        if (param != nullptr) {\n            *param = '\\0';\n            param++;\n        }\n    }\n\n    if (param == nullptr || String_IsEmpty(param)) {\n        Memory_FreePointer(&args);\n        return CR_BAD_INVOCATION;\n    }\n\n    int32_t num;\n    if (!String_ParseInteger(param, &num)) {\n        Memory_FreePointer(&args);\n        return CR_BAD_INVOCATION;\n    }\n\n    COMMAND_RESULT result = CR_FAILURE;\n    if (String_Equivalent(subcmd, \"take\")) {\n        result = M_TakeSecret(num - 1);\n    } else if (String_Equivalent(subcmd, \"give\")) {\n        result = M_GiveSecret(num - 1);\n    } else {\n        result = CR_BAD_INVOCATION;\n    }\n\n    Memory_FreePointer(&args);\n    return result;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"secret\", M_Entrypoint, GS_ID(\"console/cmd/give_secret/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/heal.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/lara/misc.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (lara_item->hit_points == LARA_MAX_HITPOINTS) {\n        Console_LogWarning(GS(\"general/osd/heal_already_full_hp\"));\n    } else {\n        Console_Log(GS(\"general/osd/heal_success\"));\n    }\n\n    lara_item->hit_points = LARA_MAX_HITPOINTS;\n    lara->poison_timer = 0;\n    Lara_Extinguish();\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"heal\", M_Entrypoint, GS_ID(\"console/cmd/heal/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/help.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <string.h>\n\nstatic COMMAND_RESULT M_ListAllCommands(void)\n{\n    VECTOR *const vec = Console_Registry_GetAll();\n    const CONSOLE_COMMAND **list =\n        (const CONSOLE_COMMAND **)Vector_GetData(vec);\n\n    // compute buffer size\n    int32_t total = 0;\n    for (int32_t i = 0; i < vec->count; i++) {\n        if (list[i]->help_id == nullptr) {\n            continue;\n        }\n        total += strlen(list[i]->prefix) + (i + 1 < vec->count ? 2 : 0);\n    }\n    char *buf = Memory_Alloc(total + 1);\n    for (int32_t i = 0; i < vec->count; i++) {\n        if (list[i]->help_id == nullptr) {\n            continue;\n        }\n        strcat(buf, list[i]->prefix);\n        if (i + 1 < vec->count) {\n            strcat(buf, \", \");\n        }\n    }\n    Console_Log(\"%s\", GS(\"console/cmd/help/list\"));\n    Console_Log(\"%s\", buf);\n    Memory_Free(buf);\n    Vector_Free(vec);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_ShowSpecificCommand(const char *const cmd_name)\n{\n    const CONSOLE_COMMAND *cmd = Console_Registry_Get(cmd_name);\n    if (cmd == nullptr || cmd->help_id == nullptr) {\n        Console_LogError(GS(\"general/osd/unknown_command\"), cmd_name);\n        return CR_FAILURE;\n    }\n    const char *const help = GameString_Get(cmd->help_id);\n    Console_Log(\"%s\", help);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_Help(const COMMAND_CONTEXT *const ctx)\n{\n    if (ctx->args != nullptr && *ctx->args != '\\0') {\n        return M_ShowSpecificCommand(ctx->args);\n    }\n    return M_ListAllCommands();\n}\n\nREGISTER_CONSOLE_COMMAND(\"help\", M_Help, GS_ID(\"console/cmd/help/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/immune.c",
    "content": "#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    bool enable;\n    if (String_ParseBool(ctx->args, &enable)) {\n        g_Config.debug.enable_invulnerability = enable;\n        Config_Update();\n    } else if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    } else {\n        g_Config.debug.enable_invulnerability =\n            !g_Config.debug.enable_invulnerability;\n        Config_Update();\n    }\n\n    Console_Log(\n        g_Config.debug.enable_invulnerability ? GS(\"console/cmd/immune/on\")\n                                              : GS(\"console/cmd/immune/off\"));\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"immune\", M_Entrypoint, GS_ID(\"console/cmd/immune/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"immunity\", M_Entrypoint, GS_ID(\"console/cmd/immune/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/inf_sprint.c",
    "content": "#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    bool enable;\n    if (String_ParseBool(ctx->args, &enable)) {\n        g_Config.debug.enable_endless_sprint = enable;\n        Config_Update();\n    } else if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    } else {\n        g_Config.debug.enable_endless_sprint =\n            !g_Config.debug.enable_endless_sprint;\n        Config_Update();\n    }\n\n    Console_Log(\n        g_Config.debug.enable_endless_sprint\n            ? GS(\"console/cmd/inf_sprint/on\")\n            : GS(\"console/cmd/inf_sprint/off\"));\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"restless\", M_Entrypoint, GS_ID(\"console/cmd/inf_sprint/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/kill.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/cheat.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/misc.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/objects/vars.h>\n\nextern bool CombatEnd_IsWaitingForBoss(void);\nextern OBJECT_ID CombatEnd_GetBossType(void);\n\nstatic bool M_CanTargetObjectCreature(const OBJECT_ID obj_id)\n{\n    return (Object_IsType(obj_id, g_CreatureObjects)\n            || Object_IsType(obj_id, g_LoyalObjects))\n        && Object_Get(obj_id)->loaded;\n}\n\nstatic bool M_KillSingleEnemyInRange(const int32_t max_dist)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    int32_t best_dist = -1;\n    int16_t best_item_num = NO_ITEM;\n    for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (Creature_IsHostile(item)) {\n            const int32_t dist = Item_GetDistance(item, lara_item->pos);\n            if (dist <= max_dist) {\n                if (best_item_num == NO_ITEM || dist < best_dist) {\n                    best_dist = dist;\n                    best_item_num = item_num;\n                }\n            }\n        }\n    }\n    if (best_item_num != NO_ITEM) {\n        if (Lara_Cheat_KillEnemy(best_item_num)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic int32_t M_KillAllEnemiesInRange(const int32_t max_dist)\n{\n    int32_t kill_count = 0;\n    const ITEM *const lara_item = Lara_GetItem();\n    for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (Creature_IsHostile(item)) {\n            const int32_t dist = Item_GetDistance(item, lara_item->pos);\n            if (dist <= max_dist) {\n                // Kill this enemy\n                if (Lara_Cheat_KillEnemy(item_num)) {\n                    kill_count++;\n                }\n            }\n        }\n    }\n    return kill_count;\n}\n\nstatic COMMAND_RESULT M_KillAllEnemies(void)\n{\n    int32_t num_killed = 0;\n\n    for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (!Creature_IsHostile(item)) {\n            continue;\n        }\n        if (item->object_id == CombatEnd_GetBossType()\n            && CombatEnd_IsWaitingForBoss()) {\n            continue;\n        }\n        if (Lara_Cheat_KillEnemy(item_num)) {\n            num_killed++;\n        }\n    }\n\n    if (num_killed == 0) {\n        Console_LogError(GS(\"general/osd/kill_all_fail\"));\n        return CR_FAILURE;\n    }\n\n    Console_Log(GS(\"general/osd/kill_all\"), num_killed);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_KillNearestEnemies(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    int32_t kill_count = M_KillAllEnemiesInRange(WALL_L);\n    if (kill_count == 0) {\n        kill_count = M_KillSingleEnemyInRange(5 * WALL_L);\n    }\n\n    if (kill_count == 0) {\n        // No enemies killed\n        Console_LogError(GS(\"general/osd/kill_fail\"));\n        return CR_FAILURE;\n    } else {\n        // At least one enemy was killed.\n        Console_Log(GS(\"general/osd/kill\"));\n        return CR_SUCCESS;\n    }\n}\n\nstatic COMMAND_RESULT M_KillEnemyType(const char *const enemy_name)\n{\n    bool matches_found = false;\n    int32_t num_killed = 0;\n    int32_t match_count = 0;\n    OBJECT_NAME_MATCH *matches =\n        Object_IdsFromName(enemy_name, &match_count, M_CanTargetObjectCreature);\n\n    for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n\n        bool is_matched = false;\n        for (int32_t i = 0; i < match_count; i++) {\n            if (matches[i].object_id == item->object_id) {\n                is_matched = true;\n                break;\n            }\n        }\n        if (!is_matched) {\n            continue;\n        }\n        matches_found = true;\n\n        if (Lara_Cheat_KillEnemy(item_num)) {\n            num_killed++;\n        }\n    }\n    Memory_FreePointer(&matches);\n\n    if (!matches_found) {\n        Console_LogError(GS(\"general/osd/invalid_object\"), enemy_name);\n        return CR_FAILURE;\n    }\n    if (num_killed == 0) {\n        Console_LogError(GS(\"general/osd/object_not_found\"), enemy_name);\n        return CR_FAILURE;\n    }\n    Console_Log(GS(\"general/osd/kill_all\"), num_killed);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsLoaded()) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (String_Equivalent(ctx->args, \"all\")) {\n        return M_KillAllEnemies();\n    }\n\n    if (String_IsEmpty(ctx->args)) {\n        return M_KillNearestEnemies();\n    }\n\n    return M_KillEnemyType(ctx->args);\n}\n\nREGISTER_CONSOLE_COMMAND(\"kill\", M_Entrypoint, GS_ID(\"console/cmd/kill/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/load_game.c",
    "content": "#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/savegame.h>\n\n#include <stdio.h>\n\ntypedef struct {\n    SAVEGAME_SLOT_POOL pool;\n    int32_t slot_num;\n    bool has_slot_num;\n} M_SLOT_REQUEST;\n\nstatic bool M_TryParseQuickArg(\n    const char *const args, int32_t *const slot_num, bool *const has_slot_num)\n{\n    *has_slot_num = false;\n    if (String_IsEmpty(args)) {\n        return true;\n    }\n\n    char tail = '\\0';\n    if (String_ParseInteger(args, slot_num)\n        || sscanf(args, \" quick %d %c\", slot_num, &tail) == 1\n        || sscanf(args, \" q%d %c\", slot_num, &tail) == 1) {\n        *has_slot_num = true;\n        return true;\n    }\n\n    return false;\n}\n\nstatic bool M_TryParseLoadArg(\n    const char *const args, M_SLOT_REQUEST *const request)\n{\n    int32_t slot_num = -1;\n    if (String_ParseInteger(args, &slot_num)) {\n        request->pool = SAVEGAME_SLOT_POOL_NORMAL;\n        request->slot_num = slot_num;\n        request->has_slot_num = true;\n        return true;\n    }\n\n    char tail = '\\0';\n    if (sscanf(args, \" quick %d %c\", &slot_num, &tail) == 1\n        || sscanf(args, \" q%d %c\", &slot_num, &tail) == 1) {\n        request->pool = SAVEGAME_SLOT_POOL_QUICK;\n        request->slot_num = slot_num;\n        request->has_slot_num = true;\n        return true;\n    }\n\n    if (sscanf(args, \" quick %c\", &tail) == 0\n        || sscanf(args, \" q %c\", &tail) == 0\n        || sscanf(args, \" q%c\", &tail) == 0) {\n        request->pool = SAVEGAME_SLOT_POOL_QUICK;\n        request->slot_num = -1;\n        request->has_slot_num = false;\n        return true;\n    }\n\n    return false;\n}\n\nstatic COMMAND_RESULT M_ExecuteLoad(const M_SLOT_REQUEST request)\n{\n    SAVEGAME_SLOT_REF slot = Savegame_InvalidSlot();\n    int32_t shown_slot_num = request.slot_num;\n    if (request.pool == SAVEGAME_SLOT_POOL_QUICK) {\n        const int32_t visual_count = Savegame_GetQuickVisualCount();\n        const int32_t visual_slot = request.has_slot_num ? request.slot_num : 1;\n        if (visual_slot < 1 || visual_slot > visual_count) {\n            Console_LogError(\n                GS(\"general/osd/load_game_fail_invalid_slot\"), visual_slot);\n            return CR_FAILURE;\n        }\n        slot = Savegame_QuickFromVisualIndex(visual_slot - 1);\n        shown_slot_num = visual_slot;\n    } else {\n        const int32_t slot_idx =\n            request.slot_num - 1; // convert 1-indexing to 0-indexing\n        if (slot_idx < 0\n            || slot_idx >= Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL)) {\n            Console_LogError(\n                GS(\"general/osd/load_game_fail_invalid_slot\"),\n                request.slot_num);\n            return CR_FAILURE;\n        }\n        slot = Savegame_NormalSlot(slot_idx);\n    }\n\n    if (Savegame_IsSlotFree(slot)) {\n        Console_LogError(\n            GS(\"general/osd/load_game_fail_unavailable_slot\"), shown_slot_num);\n        return CR_FAILURE;\n    }\n\n    GF_OverrideCommand((GF_COMMAND) {\n        .action = GF_START_SAVED_GAME,\n        .param = Savegame_SlotToParam(slot),\n    });\n    if (request.pool == SAVEGAME_SLOT_POOL_QUICK) {\n        Console_Log(GS(\"general/osd/quick_load\"), shown_slot_num);\n    } else {\n        Console_Log(GS(\"general/osd/load_game\"), shown_slot_num);\n    }\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    M_SLOT_REQUEST request;\n    if (!M_TryParseLoadArg(ctx->args, &request)) {\n        return CR_BAD_INVOCATION;\n    }\n    return M_ExecuteLoad(request);\n}\n\nstatic COMMAND_RESULT M_EntrypointQL(const COMMAND_CONTEXT *const ctx)\n{\n    M_SLOT_REQUEST request = {\n        .pool = SAVEGAME_SLOT_POOL_QUICK,\n        .slot_num = -1,\n        .has_slot_num = false,\n    };\n    if (!M_TryParseQuickArg(\n            ctx->args, &request.slot_num, &request.has_slot_num)) {\n        return CR_BAD_INVOCATION;\n    }\n    return M_ExecuteLoad(request);\n}\n\nREGISTER_CONSOLE_COMMAND(\"load\", M_Entrypoint, GS_ID(\"console/cmd/load/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"quickload\", M_EntrypointQL, GS_ID(\"console/cmd/load/help\"))\nREGISTER_CONSOLE_COMMAND(\"ql\", M_EntrypointQL, GS_ID(\"console/cmd/load/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/lua.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lua/common.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx)\n{\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    COMMAND_RESULT cmd_result;\n    LUA_RESULT eval_result = Lua_Eval(ctx->args);\n    if (eval_result.code == LUA_ERRSYNTAX) {\n        Console_LogError(\n            GS(\"console/cmd/lua/syntax_error\"), eval_result.message);\n        cmd_result = CR_FAILURE;\n    } else if (eval_result.code != LUA_OK) {\n        Console_LogError(\n            GS(\"console/cmd/lua/runtime_error\"), eval_result.message);\n        cmd_result = CR_FAILURE;\n    } else {\n        cmd_result = CR_SUCCESS;\n    }\n    Lua_FreeResult(&eval_result);\n    return cmd_result;\n}\n\nREGISTER_CONSOLE_COMMAND(\"lua\", M_Entrypoint, GS_ID(\"console/cmd/lua/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/mod.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/shell/common.h>\n#include <trx/game/shell/mod.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (String_IsEmpty(ctx->args)) {\n        const SHELL_ARGS *const args = Shell_GetArgs();\n        Console_Log(\"Currently loaded mod: %s\", args->mod->name);\n        return CR_SUCCESS;\n    }\n\n    const SHELL_MOD *const mod = Shell_GetModByName(ctx->args);\n    if (!Shell_CanSwitchToMod(mod)) {\n        Console_LogError(\"Invalid mod: %s\", ctx->args);\n        return CR_FAILURE;\n    }\n\n    Shell_RequestModSwitch(mod->name);\n    GF_OverrideCommand((GF_COMMAND) { .action = GF_SWITCH_MOD });\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"mod\", M_Entrypoint, GS_ID(\"console/cmd/mod/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/music.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/music.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    int32_t track_to_play = -1;\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    } else if (String_ParseInteger(ctx->args, &track_to_play)) {\n        if (track_to_play == 0 || track_to_play == -1) {\n            Music_Stop();\n            Console_Log(GS(\"console/cmd/play_music/stopped\"));\n        } else if (Music_Play_Direct(track_to_play, MPM_ONCE)) {\n            Console_Log(GS(\"console/cmd/play_music/track\"), track_to_play);\n        } else {\n            Console_LogError(GS(\"console/cmd/play_music/invalid_track\"));\n        }\n        return CR_SUCCESS;\n    } else {\n        return CR_BAD_INVOCATION;\n    }\n}\n\nREGISTER_CONSOLE_COMMAND(\"music\", M_Entrypoint, GS_ID(\"console/cmd/music/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/play_cutscene.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    int32_t cutscene_to_load = -1;\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    } else if (String_ParseInteger(ctx->args, &cutscene_to_load)) {\n        cutscene_to_load--;\n        const GF_LEVEL_TABLE *const level_table =\n            GF_GetLevelTable(GFLT_CUTSCENES);\n        if (cutscene_to_load < 0 || cutscene_to_load >= level_table->count) {\n            Console_LogError(GS(\"general/osd/invalid_cutscene\"));\n            return CR_FAILURE;\n        }\n        const GF_LEVEL *const level = &level_table->levels[cutscene_to_load];\n        GF_OverrideCommand((GF_COMMAND) {\n            .action = GF_START_CINE,\n            .param = cutscene_to_load,\n        });\n        Console_Log(GS(\"general/osd/play_cutscene\"), level->num + 1);\n        return CR_SUCCESS;\n    } else {\n        return CR_BAD_INVOCATION;\n    }\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"cut\", M_Entrypoint, GS_ID(\"console/cmd/play_cutscene/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"cutscene\", M_Entrypoint, GS_ID(\"console/cmd/play_cutscene/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/play_demo.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/demo.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    int32_t demo_to_load = -1;\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS);\n    if (String_IsEmpty(ctx->args)) {\n        demo_to_load = Demo_ChooseLevel(-1);\n    } else if (String_ParseInteger(ctx->args, &demo_to_load)) {\n        demo_to_load--;\n    } else {\n        return CR_BAD_INVOCATION;\n    }\n\n    if (demo_to_load < 0 || demo_to_load >= level_table->count) {\n        Console_LogError(GS(\"general/osd/invalid_demo\"));\n        return CR_FAILURE;\n    }\n    const GF_LEVEL *const level = &level_table->levels[demo_to_load];\n    GF_OverrideCommand((GF_COMMAND) {\n        .action = GF_START_DEMO,\n        .param = demo_to_load,\n    });\n    Console_Log(GS(\"general/osd/play_demo\"), level->num + 1);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"demo\", M_Entrypoint, GS_ID(\"console/cmd/play_demo/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/play_gym.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (String_IsEmpty(ctx->args)) {\n        const GF_LEVEL *const level = GF_GetGymLevel();\n        if (level == nullptr) {\n            Console_LogError(GS(\"general/osd/invalid_level\"));\n            return CR_FAILURE;\n        }\n        GF_OverrideCommand((GF_COMMAND) {\n            .action = GF_SELECT_GAME,\n            .param = level->num,\n        });\n        Console_Log(GS(\"general/osd/play_level\"), level->title);\n        return CR_SUCCESS;\n    } else {\n        return CR_BAD_INVOCATION;\n    }\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"gym\", M_Entrypoint, GS_ID(\"console/cmd/play_gym/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"home\", M_Entrypoint, GS_ID(\"console/cmd/play_gym/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/play_level.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic const GF_LEVEL *M_FindLevel(const char *const user_input)\n{\n    int32_t level_num = -1;\n    if (String_ParseInteger(user_input, &level_num)) {\n        return GF_GetLevelByOrdinalNumber(GFLT_MAIN, level_num);\n    }\n\n    VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));\n    for (int32_t i = 0; i < GF_GetLevelTable(GFLT_MAIN)->count; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        STRING_FUZZY_SOURCE source_item = {\n            .key = level->title,\n            .value = (void *)level,\n            .weight = 1,\n        };\n        if (source_item.key != nullptr) {\n            Vector_Add(source, &source_item);\n        }\n    }\n\n    const GF_LEVEL *const gym_level = GF_GetGymLevel();\n    if (gym_level != nullptr) {\n        STRING_FUZZY_SOURCE source_item = {\n            .key = \"gym\",\n            .value = (void *)gym_level,\n            .weight = 1,\n        };\n        Vector_Add(source, &source_item);\n    }\n\n    const GF_LEVEL *result = nullptr;\n    VECTOR *matches = String_FuzzyMatch(user_input, source);\n    if (matches->count >= 1) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, 0);\n        result = (const GF_LEVEL *)match->value;\n    }\n\n    if (matches != nullptr) {\n        Vector_Free(matches);\n        matches = nullptr;\n    }\n    if (source != nullptr) {\n        Vector_Free(source);\n        source = nullptr;\n    }\n    return result;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    VECTOR *matches = nullptr;\n    const GF_LEVEL *const level = M_FindLevel(ctx->args);\n\n    if (level == nullptr || level->type == GFL_DUMMY\n        || level->type == GFL_CURRENT) {\n        Console_LogError(GS(\"general/osd/invalid_level\"));\n        return CR_FAILURE;\n    }\n\n    GF_OverrideCommand((GF_COMMAND) {\n        .action = GF_SELECT_GAME,\n        .param = level->num,\n    });\n    Console_Log(GS(\"general/osd/play_level\"), level->title);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"play\", M_Entrypoint, GS_ID(\"console/cmd/play_level/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"level\", M_Entrypoint, GS_ID(\"console/cmd/play_level/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/pos.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/const.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/rooms.h>\n\n#include <string.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsLoaded()) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    const GF_LEVEL *const current_level = GF_GetCurrentLevel();\n    if (current_level->type == GFL_TITLE) {\n        return CR_UNAVAILABLE;\n    }\n\n    const char *level_type_fmt = nullptr;\n    int32_t reindex = 0;\n    switch (current_level->type) {\n    case GFL_CUTSCENE:\n        level_type_fmt = GS(\"general/osd/pos_level_fmt_cutscene\");\n        reindex = 1;\n        break;\n    case GFL_DEMO:\n        level_type_fmt = GS(\"general/osd/pos_level_fmt_demo\");\n        reindex = 1;\n        break;\n    default:\n        level_type_fmt = GS(\"general/osd/pos_level_fmt\");\n        reindex = GF_GetGymLevel() == nullptr ? 1 : 0;\n        break;\n    }\n\n    const char *const level_type =\n        String_FormatStatic(level_type_fmt, current_level->num + reindex);\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const char *details;\n    if (lara_item == nullptr) {\n        details = String_FormatStatic(\"%s\", GS(\"general/osd/pos_lara_missing\"));\n    } else {\n        int16_t room_num = lara_item->room_num;\n        const ROOM *const room = Room_Get(room_num);\n        if (Room_GetFlipStatus() && room->flipped_room != NO_ROOM) {\n            room_num = room->flipped_room;\n        }\n        details = String_FormatStatic(\n            GS(\"general/osd/pos_lara_pos_fmt\"), room_num,\n            lara_item->pos.x / (float)WALL_L, lara_item->pos.y / (float)WALL_L,\n            lara_item->pos.z / (float)WALL_L,\n            lara_item->rot.x * 360.0f / (float)DEG_360,\n            lara_item->rot.y * 360.0f / (float)DEG_360,\n            lara_item->rot.z * 360.0f / (float)DEG_360);\n    }\n    const char *const glue = lara_item == nullptr ? \"\\n\" : \"  \";\n\n    const char *const message = current_level->title != nullptr\n            && strcmp(level_type, current_level->title) == 0\n        ? String_FormatStatic(\"%s%s%s\", level_type, glue, details)\n        : String_FormatStatic(\n              \"%s (%s)%s%s\", level_type, current_level->title, glue, details);\n\n    Console_Log(\"%s\", message);\n\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"pos\", M_Entrypoint, GS_ID(\"console/cmd/pos/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/save_game.c",
    "content": "#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/savegame.h>\n\n#include <stdio.h>\n\nstatic bool M_TryParseQuickKeyword(const char *const args)\n{\n    char tail = '\\0';\n    return sscanf(args, \" quick %c\", &tail) == 0\n        || sscanf(args, \" q %c\", &tail) == 0;\n}\n\nstatic COMMAND_RESULT M_HandleQuickSave(void)\n{\n    const SAVEGAME_SLOT_REF slot = Savegame_GetNextQuickSlot();\n    if (!Savegame_IsValidSlotRef(slot)) {\n        Console_LogError(GS(\"general/osd/quick_save_fail_no_slots\"));\n        return CR_FAILURE;\n    }\n\n    Savegame_Save(slot);\n    Console_Log(GS(\"general/osd/quick_save\"));\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_EntrypointQS(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (!String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n    return M_HandleQuickSave();\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    int32_t slot_num;\n    if (String_ParseInteger(ctx->args, &slot_num)) {\n        const int32_t slot_idx =\n            slot_num - 1; // convert 1-indexing to 0-indexing\n        if (slot_idx < 0\n            || slot_idx >= Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL)) {\n            Console_LogError(\n                GS(\"general/osd/save_game_fail_invalid_slot\"), slot_num);\n            return CR_BAD_INVOCATION;\n        }\n\n        Savegame_Save(Savegame_NormalSlot(slot_idx));\n        Console_Log(GS(\"general/osd/save_game\"), slot_num);\n        return CR_SUCCESS;\n    }\n\n    if (M_TryParseQuickKeyword(ctx->args)) {\n        return M_HandleQuickSave();\n    }\n    return CR_BAD_INVOCATION;\n}\n\nREGISTER_CONSOLE_COMMAND(\"save\", M_Entrypoint, GS_ID(\"console/cmd/save/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"quicksave\", M_EntrypointQS, GS_ID(\"console/cmd/save/help\"))\nREGISTER_CONSOLE_COMMAND(\"qs\", M_EntrypointQS, GS_ID(\"console/cmd/save/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/screenshot.c",
    "content": "#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/output.h>\n#include <trx/game/screenshot.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx)\n{\n    const char *const arg = ctx->args;\n    if (arg == nullptr || String_IsEmpty(arg)) {\n        Screenshot_Make(g_Config.rendering.screenshot_format);\n    } else {\n        Screenshot_MakeToPath(arg);\n    }\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"screenshot\", M_Entrypoint, GS_ID(\"console/cmd/screenshot/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/set_health.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/objects/common.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (String_IsEmpty(ctx->args)) {\n        Console_Log(\n            GS(\"general/osd/current_health_get\"), lara_item->hit_points);\n        return CR_SUCCESS;\n    }\n\n    int32_t hp;\n    if (!String_ParseInteger(ctx->args, &hp)) {\n        return CR_BAD_INVOCATION;\n    }\n    CLAMP(hp, 0, LARA_MAX_HITPOINTS);\n\n    lara_item->hit_points = hp;\n    Console_Log(GS(\"general/osd/current_health_set\"), hp);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"hp\", M_Entrypoint, GS_ID(\"console/cmd/hp/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/sfx.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/sound.h>\n\n#include <stdio.h>\n#include <string.h>\n\nstatic char *M_CreateRangeString(void)\n{\n    size_t buffer_size = 64;\n    char *result = Memory_Alloc(buffer_size);\n\n    int32_t prev = -1;\n    int32_t start = -1;\n    const SAMPLE_ID max_id = Sound_GetMaxDirectSampleID();\n    for (SAMPLE_ID i = 0; i <= max_id; i++) {\n        const bool valid = Sound_IsAvailable_Direct(i);\n\n        if (valid && start == -1) {\n            start = i;\n        }\n        if (!valid && start != -1) {\n            char temp[32];\n            if (start == prev) {\n                sprintf(temp, \"%d, \", prev);\n            } else {\n                sprintf(temp, \"%d-%d, \", start, prev);\n            }\n\n            const int32_t len = strlen(temp);\n            if (strlen(result) + len >= buffer_size) {\n                buffer_size *= 2;\n                result = Memory_Realloc(result, buffer_size);\n            }\n\n            strcat(result, temp);\n            start = -1;\n        }\n\n        if (valid) {\n            prev = i;\n        }\n    }\n\n    // Remove the trailing comma and space\n    result[strlen(result) - 2] = '\\0';\n\n    return result;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (String_IsEmpty(ctx->args)) {\n        char *ranges = M_CreateRangeString();\n        Console_Log(GS(\"general/osd/sound_available_samples\"), ranges);\n        Memory_FreePointer(&ranges);\n        return CR_SUCCESS;\n    }\n\n    SAMPLE_ID sfx_id;\n    if (!String_ParseInteger(ctx->args, &sfx_id)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    if (!Sound_IsAvailable_Direct(sfx_id)) {\n        Console_LogError(GS(\"general/osd/invalid_sample\"), sfx_id);\n        return CR_FAILURE;\n    }\n\n    Console_Log(GS(\"general/osd/sound_playing_sample\"), sfx_id);\n    Sound_Effect_Direct(sfx_id, nullptr, SPM_ALWAYS);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"sfx\", M_Entrypoint, GS_ID(\"console/cmd/sfx/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/spawn.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\nstatic GAME_VECTOR M_GetTargetPos(\n    const ITEM *const lara_item, const int16_t angle_add)\n{\n    const int16_t angle = lara_item->rot.y + angle_add;\n    const int32_t s = Math_Sin(angle);\n    const int32_t c = Math_Cos(angle);\n    const int32_t dist = WALL_L;\n    return (GAME_VECTOR) {\n        .x = lara_item->pos.x + ((s * dist) >> W2V_SHIFT),\n        .y = lara_item->pos.y,\n        .z = lara_item->pos.z + ((c * dist) >> W2V_SHIFT),\n        .room_num = lara_item->room_num,\n    };\n}\n\nstatic bool M_FindValidTargetPos(\n    const ITEM *const lara_item, GAME_VECTOR *const out_pos)\n{\n    for (int32_t angle = -DEG_45; angle <= DEG_45; angle += DEG_45) {\n        GAME_VECTOR pos = M_GetTargetPos(lara_item, angle);\n        if (Room_FindValidPos(&pos.pos, &pos.room_num)) {\n            *out_pos = pos;\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_CanSpawnObject(const OBJECT_ID object_id)\n{\n    return !Object_IsType(object_id, g_NullObjects)\n        && !Object_IsType(object_id, g_AnimObjects)\n        && !Object_IsType(object_id, g_InvObjects)\n        && Object_Get(object_id)->loaded;\n}\n\nstatic bool M_ShouldEnableBaddieAI(const OBJECT_ID object_id)\n{\n    if (Object_IsType(object_id, g_CreatureObjects)) {\n        return true;\n    }\n\n    if (Object_IsType(object_id, g_LoyalObjects)) {\n        return true;\n    }\n\n    return Object_Get(object_id)->intelligent;\n}\n\nstatic bool M_SpawnItem(\n    const OBJECT_ID object_id, const GAME_VECTOR target_pos,\n    const XYZ_16 target_rot)\n{\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return false;\n    }\n\n    ITEM *const new_item = Item_Get(item_num);\n    new_item->object_id = object_id;\n    new_item->room_num = target_pos.room_num;\n    new_item->pos = target_pos.pos;\n    new_item->rot = target_rot;\n    new_item->shade.value_1 = -1;\n    Item_Initialise(item_num);\n    Item_AddActive(item_num);\n\n    if (M_ShouldEnableBaddieAI(object_id)) {\n        new_item->status = IS_ACTIVE;\n        LOT_EnableBaddieAI(item_num, true);\n    }\n\n    return true;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->hit_points <= 0) {\n        return CR_UNAVAILABLE;\n    }\n\n    GAME_VECTOR target_pos;\n    if (!M_FindValidTargetPos(lara_item, &target_pos)) {\n        Console_LogError(GS(\"console/cmd/spawn/fail\"));\n        return CR_FAILURE;\n    }\n\n    const char *const args = ctx->args;\n    int32_t match_count = 0;\n    OBJECT_NAME_MATCH *matches = nullptr;\n    if (String_IsEmpty(args)) {\n        return CR_BAD_INVOCATION;\n    } else {\n        matches = Object_IdsFromName(args, &match_count, M_CanSpawnObject);\n    }\n\n    if (match_count <= 0) {\n        Console_LogError(GS(\"general/osd/invalid_item\"), args);\n        Memory_FreePointer(&matches);\n        return CR_FAILURE;\n    }\n\n    const int32_t dx = lara_item->pos.x - target_pos.x;\n    const int32_t dz = lara_item->pos.z - target_pos.z;\n    const XYZ_16 target_rot = { .y = Math_Atan(dz, dx) };\n\n    bool spawned = false;\n    for (int32_t i = 0; i < match_count; i++) {\n        if (M_SpawnItem(matches[i].object_id, target_pos, target_rot)) {\n            spawned = true;\n            break;\n        }\n    }\n    Memory_FreePointer(&matches);\n\n    if (!spawned) {\n        return CR_FAILURE;\n    }\n\n    Console_Log(GS(\"console/cmd/spawn/success\"));\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"spawn\", M_Entrypoint, nullptr)\n"
  },
  {
    "path": "src/trx/game/console/cmd/speed.c",
    "content": "#include <trx/core/strings.h>\n#include <trx/game/clock.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (String_Equivalent(ctx->args, \"\")) {\n        Console_Log(GS(\"general/osd/speed_get\"), Clock_GetTurboSpeed());\n        return CR_SUCCESS;\n    }\n\n    int32_t num = -1;\n    if (String_ParseInteger(ctx->args, &num)) {\n        Clock_SetTurboSpeed(num);\n        return CR_SUCCESS;\n    }\n\n    return CR_BAD_INVOCATION;\n}\n\nREGISTER_CONSOLE_COMMAND(\"speed\", M_Entrypoint, GS_ID(\"console/cmd/speed/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/strings.c",
    "content": "#include <trx/config.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/game_strings/manager.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    const bool success = GameStringManager_ReloadLanguage(g_Config.language);\n    if (success) {\n        Console_Log(\"%s\", GS(\"general/osd/strings_reloaded\"));\n    } else {\n        Console_LogError(\"%s\", GS(\"general/osd/strings_failed\"));\n    }\n    return success ? CR_SUCCESS : CR_FAILURE;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"strings\", M_Entrypoint, GS_ID(\"console/cmd/strings/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/teleport.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/cheat.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n\n#include <math.h>\n#include <stdio.h>\n#include <string.h>\n\nstatic int16_t m_LastTeleportedItemNum = NO_ITEM;\n\nstatic bool M_ObjectCanBePickedUp(const OBJECT_ID obj_id)\n{\n    if (!Object_IsType(obj_id, g_PickupObjects)) {\n        return true;\n    }\n    for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (item->object_id == obj_id && item->status != IS_INVISIBLE) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_CanTargetObject(const OBJECT_ID obj_id)\n{\n    return !Object_IsType(obj_id, g_NullObjects)\n        && !Object_IsType(obj_id, g_AnimObjects)\n        && !Object_IsType(obj_id, g_InvObjects) && Object_Get(obj_id)->loaded\n        && M_ObjectCanBePickedUp(obj_id);\n}\n\nstatic bool M_CanTargetItem(\n    const ITEM *const item, const OBJECT_NAME_MATCH *const matches,\n    const int32_t match_count)\n{\n    // Collected pickups\n    if (Object_IsType(item->object_id, g_PickupObjects)\n        && (item->status == IS_INVISIBLE || item->status == IS_DEACTIVATED\n            || item->room_num == NO_ROOM)) {\n        return false;\n    }\n\n    // Killed enemies and removed items\n    if (item->flags & IF_KILLED) {\n        return false;\n    }\n\n    // Out of bounds items\n    int16_t room_num = item->room_num;\n    if (room_num != NO_ROOM) {\n        const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n        if (Room_GetHeight(sector, item->pos) == NO_HEIGHT) {\n            return false;\n        }\n    }\n\n    // Non-matches to user input\n    bool is_matched = false;\n    for (int32_t j = 0; j < match_count; j++) {\n        if (matches[j].object_id == item->object_id) {\n            is_matched = true;\n            break;\n        }\n    }\n    if (!is_matched) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_CanTargetEnemyItem(const ITEM *const item)\n{\n    if (!Creature_IsHostile(item) || item->room_num == NO_ROOM\n        || item->hit_points <= 0 || (item->flags & IF_KILLED) != 0) {\n        return false;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    return Room_GetHeight(sector, item->pos) != NO_HEIGHT;\n}\n\nstatic const ITEM *M_GetItemToTeleporTo(const char *const user_input)\n{\n    int32_t match_count = 0;\n    OBJECT_NAME_MATCH *matches =\n        Object_IdsFromName(user_input, &match_count, M_CanTargetObject);\n\n    const ITEM *const lara_item = Lara_GetItem();\n\n    int16_t best_item_num = NO_ITEM;\n\n    // Choose the matching item closest to Lara.\n    const int32_t near_distance = WALL_L;\n    int16_t closest_item_num = NO_ITEM;\n    int32_t closest_distance = INT32_MAX;\n    for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (!M_CanTargetItem(item, matches, match_count)) {\n            continue;\n        }\n\n        const int32_t distance = Item_GetDistance(item, lara_item->pos);\n        if (distance < closest_distance) {\n            closest_distance = distance;\n            closest_item_num = item_num;\n        }\n    }\n\n    if (closest_distance > near_distance) {\n        best_item_num = closest_item_num;\n    } else {\n        // If Lara's already very close to a matching item, choose the next\n        // matching item in a round-robin fashion.\n        const int16_t start_idx = (closest_item_num + 1) % Item_GetTotalCount();\n        for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n            int16_t item_num = (start_idx + i) % Item_GetTotalCount();\n            if (item_num == closest_item_num) {\n                continue;\n            }\n\n            const ITEM *const item = Item_Get(item_num);\n            if (!M_CanTargetItem(item, matches, match_count)) {\n                continue;\n            }\n\n            const int32_t distance = Item_GetDistance(item, lara_item->pos);\n            if (distance < near_distance) {\n                continue;\n            }\n\n            best_item_num = item_num;\n            break;\n        }\n    }\n\n    Memory_FreePointer(&matches);\n    return Item_Get(best_item_num);\n}\n\nstatic const ITEM *M_GetHostileEnemyToTeleportTo(void)\n{\n    const int32_t item_count = Item_GetTotalCount();\n    if (item_count <= 0) {\n        return nullptr;\n    }\n\n    int16_t start_idx = 0;\n    if (m_LastTeleportedItemNum >= 0 && m_LastTeleportedItemNum < item_count) {\n        start_idx = (m_LastTeleportedItemNum + 1) % item_count;\n    }\n\n    for (int32_t i = 0; i < item_count; i++) {\n        const int16_t item_num = (start_idx + i) % item_count;\n        const ITEM *const item = Item_Get(item_num);\n        if (!M_CanTargetEnemyItem(item)) {\n            continue;\n        }\n        m_LastTeleportedItemNum = item_num;\n        return item;\n    }\n\n    return nullptr;\n}\n\nstatic inline bool M_IsFloatRound(const float num)\n{\n    return fabsf(num - roundf(num)) < 0.0001f;\n}\n\nstatic void M_AlignLaraToItem(const ITEM *const item)\n{\n    typedef enum {\n        L_DIR_NONE = -1,\n        L_DIR_SAME = 0,\n        L_DIR_OPPOSITE = 1,\n    } L_DIR;\n\n    L_DIR dir = L_DIR_NONE;\n    if (Object_IsType(item->object_id, g_PickupObjects)) {\n        dir = L_DIR_OPPOSITE;\n    } else if (Object_IsType(item->object_id, g_SwitchObjects)) {\n        dir = L_DIR_SAME;\n    } else if (Object_IsType(item->object_id, g_ReceptacleObjects)) {\n        dir = L_DIR_SAME;\n    } else if (Object_IsType(item->object_id, g_DoorObjects)) {\n        dir = L_DIR_OPPOSITE;\n    } else if (item->object_id == O_ZIPLINE_HANDLE) {\n        dir = L_DIR_SAME;\n    }\n    if (dir != L_DIR_NONE) {\n        ITEM *const lara_item = Lara_GetItem();\n        lara_item->rot.x = 0;\n        lara_item->rot.y = item->rot.y;\n        lara_item->rot.z = 0;\n        if (dir == L_DIR_OPPOSITE) {\n            lara_item->rot.y += DEG_180;\n        }\n    }\n}\n\nstatic COMMAND_RESULT M_TeleportToXYZ(\n    float x, const float y, float z, const bool precise_coords)\n{\n    if (!precise_coords) {\n        if (M_IsFloatRound(x)) {\n            x += 0.5f;\n        }\n        if (M_IsFloatRound(z)) {\n            z += 0.5f;\n        }\n    }\n\n    const XYZ_32 pos = {\n        .x = precise_coords ? x : x * WALL_L,\n        .y = precise_coords ? y : y * WALL_L,\n        .z = precise_coords ? z : z * WALL_L,\n    };\n    if (!Lara_Cheat_Teleport(pos, NO_ROOM)) {\n        Console_LogError(GS(\"console/cmd/teleport/pos_fail\"), x, y, z);\n        return CR_FAILURE;\n    }\n\n    Console_Log(GS(\"console/cmd/teleport/pos\"), x, y, z);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_TeleportToItemNum(const int16_t item_num)\n{\n    if (item_num < 0 || item_num >= Item_GetTotalCount()) {\n        Console_LogError(GS(\"console/cmd/teleport/item_fail\"), item_num);\n        return CR_FAILURE;\n    }\n\n    const ITEM *const item = Item_Get(item_num);\n    if (item == nullptr || item->room_num == NO_ROOM) {\n        Console_LogError(GS(\"console/cmd/teleport/item_fail\"), item_num);\n        return CR_FAILURE;\n    }\n\n    if ((item->flags & IF_KILLED) != 0) {\n        Console_LogError(GS(\"console/cmd/teleport/item_fail\"), item_num);\n        return CR_FAILURE;\n    }\n\n    const XYZ_32 pos = {\n        .x = item->pos.x,\n        .y = item->pos.y - STEP_L / 4,\n        .z = item->pos.z,\n    };\n    if (Lara_Cheat_Teleport(pos, item->room_num)) {\n        M_AlignLaraToItem(item);\n        Console_Log(GS(\"console/cmd/teleport/item\"), item_num);\n        return CR_SUCCESS;\n    }\n\n    Console_LogError(GS(\"console/cmd/teleport/item_fail\"), item_num);\n    return CR_FAILURE;\n}\n\nstatic COMMAND_RESULT M_TeleportToRoom(const int16_t room_num)\n{\n    if (room_num < 0 || room_num >= Room_GetCount()) {\n        Console_LogWarning(\n            GS(\"general/osd/invalid_room\"), room_num, Room_GetCount() - 1);\n        return CR_FAILURE;\n    }\n\n    const ROOM *const room = Room_Get(room_num);\n    const int32_t x1 = room->pos.x + WALL_L;\n    const int32_t x2 = room->pos.x + (room->size.x << WALL_SHIFT) - WALL_L;\n    const int32_t y1 = room->min_floor;\n    const int32_t y2 = room->max_ceiling;\n    const int32_t z1 = room->pos.z + WALL_L;\n    const int32_t z2 = room->pos.z + (room->size.z << WALL_SHIFT) - WALL_L;\n\n    bool success = false;\n    for (int32_t i = 0; i < 100; i++) {\n        const XYZ_32 pos = {\n            .x = x1 + Random_GetControl() * (x2 - x1) / 0x7FFF,\n            .y = y1,\n            .z = z1 + Random_GetControl() * (z2 - z1) / 0x7FFF,\n        };\n        if (Lara_Cheat_Teleport(pos, room_num)) {\n            success = true;\n            break;\n        }\n    }\n\n    if (!success) {\n        Console_LogError(GS(\"console/cmd/teleport/room_fail\"), room_num);\n        return CR_FAILURE;\n    }\n\n    Console_Log(GS(\"console/cmd/teleport/room\"), room_num);\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_TeleportToObject(const char *const user_input)\n{\n    // Nearest item of this name\n    if (String_Equivalent(user_input, \"\")) {\n        return CR_BAD_INVOCATION;\n    }\n\n    const ITEM *const best_item = M_GetItemToTeleporTo(user_input);\n    if (best_item == nullptr) {\n        Console_LogError(GS(\"console/cmd/teleport/object_fail\"), user_input);\n        return CR_FAILURE;\n    }\n\n    // Determine which alias matched via Object_IdsFromName\n    int32_t match_count = 0;\n    OBJECT_NAME_MATCH *matches =\n        Object_IdsFromName(user_input, &match_count, M_CanTargetObject);\n    const char *reported_name = user_input;\n    for (int32_t i = 0; i < match_count; i++) {\n        if (matches[i].object_id == best_item->object_id) {\n            if (matches[i].matched_name != nullptr) {\n                reported_name = matches[i].matched_name;\n            }\n            break;\n        }\n    }\n    Memory_FreePointer(&matches);\n\n    const XYZ_32 pos = {\n        .x = best_item->pos.x,\n        .y = best_item->pos.y - STEP_L / 4,\n        .z = best_item->pos.z,\n    };\n    if (Lara_Cheat_Teleport(pos, best_item->room_num)) {\n        M_AlignLaraToItem(best_item);\n        Console_Log(GS(\"console/cmd/teleport/object\"), reported_name);\n    } else {\n        Console_LogError(GS(\"console/cmd/teleport/object_fail\"), reported_name);\n    }\n    return CR_SUCCESS;\n}\n\nstatic COMMAND_RESULT M_TeleportToEnemy(void)\n{\n    ITEM *const enemy_item = (ITEM *)M_GetHostileEnemyToTeleportTo();\n    if (enemy_item == nullptr) {\n        Console_LogError(GS(\"console/cmd/teleport/object_fail\"), \"enemy\");\n        return CR_FAILURE;\n    }\n\n    const XYZ_32 pos = {\n        .x = enemy_item->pos.x,\n        .y = enemy_item->pos.y - STEP_L / 4,\n        .z = enemy_item->pos.z,\n    };\n    if (!Lara_Cheat_Teleport(pos, enemy_item->room_num)) {\n        Console_LogError(GS(\"console/cmd/teleport/object_fail\"), \"enemy\");\n        return CR_FAILURE;\n    }\n\n    M_AlignLaraToItem(enemy_item);\n    Console_Log(GS(\"console/cmd/teleport/object\"), \"enemy\");\n    return CR_SUCCESS;\n}\n\nstatic bool M_TryParseTagNumber(\n    const char *const args, const char tag, int16_t *const out_num)\n{\n    if (args == nullptr) {\n        return false;\n    }\n    if (args[0] != tag) {\n        return false;\n    }\n\n    int32_t num32 = 0;\n    if (!String_ParseInteger(args + 1, &num32)) {\n        return false;\n    }\n    if (num32 < INT16_MIN || num32 > INT16_MAX) {\n        return false;\n    }\n    *out_num = (int16_t)num32;\n    return true;\n}\n\nstatic bool M_TryParseKeywordNumber(\n    const char *const args, const char *const keyword, int16_t *const out_num)\n{\n    if (args == nullptr || keyword == nullptr) {\n        return false;\n    }\n\n    const size_t keyword_len = strlen(keyword);\n    if (strncmp(args, keyword, keyword_len) != 0) {\n        return false;\n    }\n\n    int32_t num32 = 0;\n    if (!String_ParseInteger(args + keyword_len, &num32)) {\n        return false;\n    }\n    if (num32 < INT16_MIN || num32 > INT16_MAX) {\n        return false;\n    }\n    *out_num = (int16_t)num32;\n    return true;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (!lara_item->hit_points) {\n        return CR_UNAVAILABLE;\n    }\n\n    float x, y, z;\n    if (sscanf(ctx->args, \"precise %f %f %f\", &x, &y, &z) == 3) {\n        return M_TeleportToXYZ(x, y, z, true);\n    }\n\n    if (sscanf(ctx->args, \"%f %f %f\", &x, &y, &z) == 3) {\n        return M_TeleportToXYZ(x, y, z, false);\n    }\n\n    int16_t num = 0;\n    if (M_TryParseKeywordNumber(ctx->args, \"item \", &num)) {\n        return M_TeleportToItemNum(num);\n    }\n    if (M_TryParseTagNumber(ctx->args, 'i', &num)) {\n        return M_TeleportToItemNum(num);\n    }\n    if (M_TryParseKeywordNumber(ctx->args, \"room \", &num)) {\n        return M_TeleportToRoom(num);\n    }\n    if (M_TryParseTagNumber(ctx->args, 'r', &num)) {\n        return M_TeleportToRoom(num);\n    }\n\n    int16_t room_num = -1;\n    if (sscanf(ctx->args, \"%hd\", &room_num) == 1) { // legacy\n        return M_TeleportToRoom(room_num);\n    }\n\n    if (String_Equivalent(ctx->args, \"enemy\")) {\n        return M_TeleportToEnemy();\n    }\n\n    return M_TeleportToObject(ctx->args);\n}\n\nREGISTER_CONSOLE_COMMAND(\"tp\", M_Entrypoint, GS_ID(\"console/cmd/tp/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/test_text.c",
    "content": "#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n\n#include <stdio.h>\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    char buf[500] = {};\n    char *ptr = buf;\n    for (int32_t y = 0; y < 3; y++) {\n        for (int32_t x = 0; x < 4; x++) {\n            const int32_t i = y * 4 + x;\n            ptr += sprintf(ptr, \"\\\\{color %d}Color %d\\\\{/color}   \", i, i);\n        }\n        ptr += sprintf(ptr, \"\\n\");\n    }\n    ptr += sprintf(ptr, \"\\\\{dim}Dim\\\\{/dim}\\n\");\n    ptr += sprintf(ptr, \"Secrets: \\\\{secret 1}\\\\{secret 2}\\\\{secret 3}\");\n    Console_Log(\"%s\", buf);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"test-text\", M_Entrypoint, nullptr)\n"
  },
  {
    "path": "src/trx/game/console/cmd/trigger.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/items.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/pathing.h>\n\ntypedef enum {\n    M_TARGET_OK = 0,\n    M_TARGET_INVALID_ITEM,\n    M_TARGET_NO_MATCH,\n    M_TARGET_NOT_FOUND,\n} M_TARGET_RESULT;\n\nstatic bool M_CanTargetObject(const OBJECT_ID object_id)\n{\n    return Object_Get(object_id)->loaded;\n}\n\nstatic void M_SortItemNums(int16_t *const item_nums, const int32_t count)\n{\n    for (int32_t i = 0; i < count - 1; i++) {\n        for (int32_t j = i + 1; j < count; j++) {\n            if (item_nums[i] > item_nums[j]) {\n                SWAP(item_nums[i], item_nums[j]);\n            }\n        }\n    }\n}\n\nstatic char *M_BuildCollapsedItemNums(\n    const int16_t *const item_nums, const int32_t count)\n{\n    if (count <= 0) {\n        return Memory_DupStr(\"\");\n    }\n\n    char *result = nullptr;\n    for (int32_t i = 0; i < count; i++) {\n        const int16_t start = item_nums[i];\n        int16_t end = start;\n        while (i + 1 < count && item_nums[i + 1] == end + 1) {\n            i++;\n            end = item_nums[i];\n        }\n\n        char *segment = nullptr;\n        if (end == start) {\n            segment = String_Format(\"%d\", start);\n        } else {\n            segment = String_Format(\"%d-%d\", start, end);\n        }\n\n        if (result == nullptr) {\n            result = segment;\n        } else {\n            char *const joined = String_Format(\"%s, %s\", result, segment);\n            Memory_FreePointer(&result);\n            Memory_FreePointer(&segment);\n            result = joined;\n        }\n    }\n\n    if (result == nullptr) {\n        return Memory_DupStr(\"\");\n    }\n    return result;\n}\n\nstatic void M_ApplyTrigger(const int16_t item_num, const bool enable)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item == nullptr) {\n        return;\n    }\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (obj->trigger_func != nullptr) {\n        const bool use_default_handling = obj->trigger_func(item, nullptr);\n        if (!use_default_handling) {\n            return;\n        }\n    }\n\n    if (enable) {\n        item->flags |= IF_CODE_BITS;\n        item->timer = 0;\n        Item_AddActive(item_num);\n        if (obj->intelligent) {\n            item->status = IS_ACTIVE;\n            LOT_EnableBaddieAI(item_num, true);\n        }\n    } else {\n        item->flags &= ~IF_CODE_BITS;\n        Item_RemoveActive(item_num);\n        if (obj->intelligent) {\n            item->status = IS_ACTIVE;\n            LOT_DisableBaddieAI(item_num);\n        }\n    }\n}\n\nstatic M_TARGET_RESULT M_TargetItemsFromItemNum(\n    VECTOR *const target_item_nums, const int32_t item_num_arg)\n{\n    if (item_num_arg < 0 || item_num_arg >= Item_GetTotalCount()) {\n        return M_TARGET_INVALID_ITEM;\n    }\n\n    const int16_t item_num = (int16_t)item_num_arg;\n    const ITEM *const item = Item_Get(item_num);\n    if (item == nullptr || item->object_id == O_LARA) {\n        return M_TARGET_INVALID_ITEM;\n    }\n\n    Vector_Add(target_item_nums, &item_num);\n    return M_TARGET_OK;\n}\n\nstatic M_TARGET_RESULT M_TargetItemsFromItemName(\n    VECTOR *const target_item_nums, const char *const item_name)\n{\n    const ITEM *const item = Item_GetByName(item_name);\n    if (item == nullptr) {\n        return M_TARGET_NO_MATCH;\n    }\n\n    const int16_t item_num = Item_GetIndex(item);\n    Vector_Add(target_item_nums, &item_num);\n    return M_TARGET_OK;\n}\n\nstatic M_TARGET_RESULT M_TargetItemsFromObjectName(\n    VECTOR *const target_item_nums, const char *const object_name)\n{\n    int32_t match_count = 0;\n    OBJECT_NAME_MATCH *matches =\n        Object_IdsFromName(object_name, &match_count, M_CanTargetObject);\n    if (match_count <= 0) {\n        Memory_FreePointer(&matches);\n        return M_TARGET_NO_MATCH;\n    }\n\n    const int32_t total_count = Item_GetTotalCount();\n    for (int16_t item_num = 0; item_num < total_count; item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (item == nullptr || item->object_id == O_LARA) {\n            continue;\n        }\n        if ((item->flags & IF_KILLED) != 0) {\n            continue;\n        }\n\n        bool is_matched = false;\n        for (int32_t i = 0; i < match_count; i++) {\n            if (matches[i].object_id == item->object_id) {\n                is_matched = true;\n                break;\n            }\n        }\n        if (!is_matched) {\n            continue;\n        }\n\n        Vector_Add(target_item_nums, &item_num);\n    }\n\n    Memory_FreePointer(&matches);\n    if (target_item_nums->count <= 0) {\n        return M_TARGET_NOT_FOUND;\n    }\n    return M_TARGET_OK;\n}\n\nstatic M_TARGET_RESULT M_GetTargetItems(\n    VECTOR *const target_item_nums, const char *const user_input)\n{\n    int32_t item_num_arg = 0;\n    if (String_ParseInteger(user_input, &item_num_arg)) {\n        return M_TargetItemsFromItemNum(target_item_nums, item_num_arg);\n    }\n\n    const M_TARGET_RESULT named_result =\n        M_TargetItemsFromItemName(target_item_nums, user_input);\n    if (named_result == M_TARGET_OK) {\n        return M_TARGET_OK;\n    }\n\n    return M_TargetItemsFromObjectName(target_item_nums, user_input);\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (!Game_IsLoaded()) {\n        return CR_UNAVAILABLE;\n    }\n\n    const bool is_trigger = String_Equivalent(ctx->prefix, \"trigger\");\n    const bool is_untrigger = String_Equivalent(ctx->prefix, \"untrigger\");\n    if (!is_trigger && !is_untrigger) {\n        return CR_BAD_INVOCATION;\n    }\n    const bool enable = is_trigger;\n\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    VECTOR *const target_item_nums = Vector_Create(sizeof(int16_t));\n    const M_TARGET_RESULT target_result =\n        M_GetTargetItems(target_item_nums, ctx->args);\n    if (target_result != M_TARGET_OK) {\n        switch (target_result) {\n        case M_TARGET_INVALID_ITEM:\n            Console_LogError(GS(\"console/cmd/trigger/invalid_item\"), ctx->args);\n            break;\n        case M_TARGET_NO_MATCH:\n            Console_LogError(GS(\"console/cmd/trigger/no_match\"), ctx->args);\n            break;\n        case M_TARGET_NOT_FOUND:\n            Console_LogError(GS(\"console/cmd/trigger/not_found\"), ctx->args);\n            break;\n        case M_TARGET_OK:\n            break;\n        }\n        Vector_Free(target_item_nums);\n        return CR_FAILURE;\n    }\n\n    int16_t *const item_nums = Vector_GetData(target_item_nums);\n    M_SortItemNums(item_nums, target_item_nums->count);\n    for (int32_t i = 0; i < target_item_nums->count; i++) {\n        M_ApplyTrigger(item_nums[i], enable);\n    }\n\n    char *collapsed =\n        M_BuildCollapsedItemNums(item_nums, target_item_nums->count);\n    Console_Log(\n        enable ? GS(\"console/cmd/trigger/triggered\")\n               : GS(\"console/cmd/trigger/untriggered\"),\n        collapsed);\n    Memory_FreePointer(&collapsed);\n    Vector_Free(target_item_nums);\n\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"trigger\", M_Entrypoint, GS_ID(\"console/cmd/trigger/help\"))\nREGISTER_CONSOLE_COMMAND(\n    \"untrigger\", M_Entrypoint, GS_ID(\"console/cmd/trigger/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/weather.c",
    "content": "#include <trx/core/enum_map.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/fx/weather.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n\n#include <string.h>\n\nstatic char *M_GetAvailableWeatherTypes(void)\n{\n    VECTOR *const values = EnumMap_ListValues(\"WEATHER_TYPE\");\n    if (values == nullptr) {\n        return nullptr;\n    }\n\n    size_t total_len = 1;\n    const char *const sep = \", \";\n    for (int32_t i = 0; i < values->count; i++) {\n        const char *const s = *(char **)Vector_Get(values, i);\n        total_len += strlen(s) + (i + 1 < values->count ? strlen(sep) : 0);\n    }\n\n    char *const result = Memory_Alloc(total_len);\n    for (int32_t i = 0; i < values->count; i++) {\n        const char *const s = *(char **)Vector_Get(values, i);\n        strcat(result, s);\n        if (i + 1 < values->count) {\n            strcat(result, sep);\n        }\n    }\n\n    Vector_Free(values);\n    return result;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)\n{\n    if (GF_GetCurrentLevel() == nullptr\n        || GF_GetCurrentLevel()->type == GFL_TITLE) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (!Game_IsLoaded()) {\n        return CR_UNAVAILABLE;\n    }\n\n    if (String_IsEmpty(ctx->args)) {\n        return CR_BAD_INVOCATION;\n    }\n\n    const int32_t weather_type_raw = ENUM_MAP_GET(WEATHER_TYPE, ctx->args, -1);\n    if (weather_type_raw == -1) {\n        char *available = M_GetAvailableWeatherTypes();\n        if (available != nullptr) {\n            Console_LogError(\n                GS(\"console/cmd/weather/invalid\"), ctx->args, available);\n        }\n        Memory_FreePointer(&available);\n        return CR_FAILURE;\n    }\n\n    FX_Weather_SetWeather((WEATHER_TYPE)weather_type_raw);\n    Console_Log(GS(\"console/cmd/weather/set\"), ctx->args);\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\n    \"weather\", M_Entrypoint, GS_ID(\"console/cmd/weather/help\"))\n"
  },
  {
    "path": "src/trx/game/console/cmd/winston.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\nstatic bool M_TrySummon(const GAME_VECTOR target_pos, const OBJECT_ID object_id)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (item->object_id == object_id) {\n            if (item->status == IS_INVISIBLE || item->status == IS_INACTIVE) {\n                item->status = IS_ACTIVE;\n                Item_AddActive(item_num);\n                LOT_EnableBaddieAI(item_num, true);\n            } else if ((item->flags & IF_KILLED) != 0) {\n                Music_Stop();\n                Console_Log(GS(\"console/cmd/winston/dead\"));\n                return true;\n            }\n            item->pos.x = target_pos.x;\n            item->pos.y = target_pos.y;\n            item->pos.z = target_pos.z;\n            item->rot.y = lara_item->rot.y;\n            Item_UpdateRoom(item_num, target_pos.room_num);\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx)\n{\n    if (!Game_IsPlayable()) {\n        return CR_UNAVAILABLE;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->hit_points <= 0) {\n        return CR_UNAVAILABLE;\n    }\n\n    const OBJECT *const obj = Object_Get(O_WINSTON);\n    if (!obj->loaded) {\n        return CR_UNAVAILABLE;\n    }\n\n    GAME_VECTOR target_pos = {\n        .x = lara_item->pos.x + STEP_L,\n        .y = lara_item->pos.y - WALL_L,\n        .z = lara_item->pos.z + STEP_L,\n        .room_num = lara_item->room_num,\n    };\n    if (!Room_FindValidPos(&target_pos.pos, &target_pos.room_num)) {\n        Console_LogError(GS(\"console/cmd/winston/spawn_failed\"));\n        return CR_FAILURE;\n    }\n\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->killed_loyal_item) {\n        Music_Stop();\n        Console_Log(GS(\"console/cmd/winston/dead\"));\n        return CR_FAILURE;\n    }\n\n    if (M_TrySummon(target_pos, O_WINSTON_ARMY)\n        || M_TrySummon(target_pos, O_WINSTON)) {\n        Console_Log(GS(\"console/cmd/winston/teleported\"));\n        return CR_SUCCESS;\n    }\n\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return CR_FAILURE;\n    }\n    ITEM *const new_item = Item_Get(item_num);\n    new_item->object_id = O_WINSTON;\n    new_item->room_num = target_pos.room_num;\n    new_item->pos.x = target_pos.x;\n    new_item->pos.y = target_pos.y;\n    new_item->pos.z = target_pos.z;\n    new_item->rot.y = lara_item->rot.y;\n    new_item->shade.value_1 = -1;\n    Item_Initialise(item_num);\n    Item_AddActive(item_num);\n    new_item->status = IS_ACTIVE;\n    LOT_EnableBaddieAI(item_num, true);\n\n    Console_Log(GS(\"console/cmd/winston/spawned\"));\n    return CR_SUCCESS;\n}\n\nREGISTER_CONSOLE_COMMAND(\"teatime\", M_Entrypoint, nullptr)\n"
  },
  {
    "path": "src/trx/game/console/common.c",
    "content": "#include <trx/game/console/common.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/console/internal.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/ui.h>\n\n#include <stdarg.h>\n#include <stdio.h>\n#include <string.h>\n\nstatic bool m_IsOpened = false;\nstatic UI_CONSOLE_STATE m_UIState = {};\n\n// Controls whether console commands emit log events to the UI console\nstatic bool m_Verbose = true;\n\nvoid Console_Init(void)\n{\n    UI_Console_Init(&m_UIState);\n\n    Console_History_Init();\n}\n\nvoid Console_Shutdown(void)\n{\n    UI_Console_Free(&m_UIState);\n\n    Console_History_Shutdown();\n\n    m_IsOpened = false;\n}\n\nvoid Console_Open(void)\n{\n    if (m_IsOpened) {\n        return;\n    }\n    m_IsOpened = true;\n    UI_FireEvent(\n        (EVENT) { .name = \"console_open\", .sender = nullptr, .data = nullptr });\n}\n\nvoid Console_Close(void)\n{\n    if (!m_IsOpened) {\n        return;\n    }\n    m_IsOpened = false;\n    UI_FireEvent((EVENT) {\n        .name = \"console_close\", .sender = nullptr, .data = nullptr });\n}\n\nbool Console_IsOpened(void)\n{\n    return m_IsOpened;\n}\n\nvoid Console_LogEx(\n    const LOG_LEVEL level, const char *file, int line, const char *func,\n    const char *const fmt, ...)\n{\n    ASSERT(fmt != nullptr);\n\n    va_list va;\n    va_start(va, fmt);\n    va_list va_copy;\n    va_copy(va_copy, va);\n\n    const size_t text_length = vsnprintf(nullptr, 0, fmt, va);\n    char *text = Memory_Alloc(text_length + 1);\n    va_end(va);\n\n    vsnprintf(text, text_length + 1, fmt, va_copy);\n    va_end(va_copy);\n\n    Log_Message(level, file, line, func, \"%s\", text);\n\n    if (m_Verbose) {\n        UI_FireEvent((EVENT) {\n            .name = \"console_log\",\n            .sender = nullptr,\n            .data = text,\n        });\n    }\n\n    Memory_FreePointer(&text);\n}\n\nvoid Console_SetVerbose(const bool verbose)\n{\n    m_Verbose = verbose;\n}\n\nbool Console_IsVerbose(void)\n{\n    return m_Verbose;\n}\n\nvoid Console_Clear(void)\n{\n    UI_FireEvent((EVENT) {\n        .name = \"console_clear\",\n    });\n}\n\nCOMMAND_RESULT Console_Eval(const char *const cmdline)\n{\n    LOG_INFO(\"executing command: %s\", cmdline);\n\n    const CONSOLE_COMMAND *const matching_cmd = Console_Registry_Get(cmdline);\n    if (matching_cmd == nullptr) {\n        Console_LogError(GS(\"general/osd/unknown_command\"), cmdline);\n        return CR_BAD_INVOCATION;\n    }\n\n    char *prefix = Memory_DupStr(cmdline);\n    char *args = \"\";\n    char *space = strchr(prefix, ' ');\n    if (space != nullptr) {\n        *space = '\\0';\n        args = space + 1;\n    }\n\n    const COMMAND_CONTEXT ctx = {\n        .cmd = matching_cmd,\n        .prefix = prefix,\n        .args = args,\n    };\n    ASSERT(matching_cmd->proc != nullptr);\n    const COMMAND_RESULT result = matching_cmd->proc(&ctx);\n    Memory_FreePointer(&prefix);\n\n    switch (result) {\n    case CR_BAD_INVOCATION:\n        Console_LogError(GS(\"general/osd/command_bad_invocation\"), cmdline);\n        break;\n\n    case CR_UNAVAILABLE:\n        Console_LogError(GS(\"general/osd/command_unavailable\"));\n        break;\n\n    case CR_SUCCESS:\n    case CR_FAILURE:\n        // The commands themselves are responsible for handling logging in\n        // these scenarios.\n        break;\n    }\n    return result;\n}\n\nvoid Console_Control(void)\n{\n    UI_Console_Control(&m_UIState);\n}\n\nvoid Console_Draw(void)\n{\n    UI_Console(&m_UIState);\n}\n"
  },
  {
    "path": "src/trx/game/console/common.h",
    "content": "#pragma once\n\n#include <trx/core/log.h>\n#include <trx/game/console/types.h>\n\n#include <stdint.h>\n\n#define Console_LogGeneric(level, ...)                                         \\\n    Console_LogEx(level, __FILE__, __LINE__, __func__, __VA_ARGS__)\n#define Console_Log(...) Console_LogGeneric(LOG_LEVEL_INFO, __VA_ARGS__)\n#define Console_LogWarning(...)                                                \\\n    Console_LogGeneric(LOG_LEVEL_WARNING, __VA_ARGS__)\n#define Console_LogError(...) Console_LogGeneric(LOG_LEVEL_ERROR, __VA_ARGS__)\n\nvoid Console_Init(void);\nvoid Console_Shutdown(void);\n\nvoid Console_Open(void);\nvoid Console_Close(void);\nbool Console_IsOpened(void);\n\nvoid Console_LogEx(\n    LOG_LEVEL level, const char *file, int line, const char *func,\n    const char *fmt, ...);\nvoid Console_Clear(void);\nCOMMAND_RESULT Console_Eval(const char *cmdline);\n\n// Controls whether console commands emit log events to the UI console\nvoid Console_SetVerbose(bool verbose);\nbool Console_IsVerbose(void);\n\nvoid Console_Control(void);\nvoid Console_Draw(void);\n"
  },
  {
    "path": "src/trx/game/console/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    CR_SUCCESS,\n    CR_FAILURE,\n    CR_UNAVAILABLE,\n    CR_BAD_INVOCATION,\n} COMMAND_RESULT;\n"
  },
  {
    "path": "src/trx/game/console/history.c",
    "content": "#include <trx/game/console/history.h>\n\n#include <trx/core/json/util/file.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\n#include <string.h>\n\n#define MAX_HISTORY_ENTRIES 30\n\nVECTOR *m_History = nullptr;\n\nstatic const char *M_GetPath(void)\n{\n    return String_FormatStatic(\n        \"%s/TR%dX_console_history.json5\", Shell_GetConfigDir(), g_TRVersion);\n}\n\nstatic void M_LoadFromJSON(JSON_VALUE *const doc)\n{\n    JSON_OBJECT *const root_obj = JSON_ValueAsObject(doc);\n    JSON_ARRAY *const arr = JSON_ObjectGetArray(root_obj, \"entries\");\n    if (arr == nullptr) {\n        return;\n    }\n\n    Console_History_Clear();\n    for (size_t i = 0; i < arr->length; i++) {\n        const char *const line = JSON_ArrayGetString(arr, i, nullptr);\n        if (line != nullptr) {\n            Console_History_Append(line);\n        }\n    }\n}\n\nstatic JSON_VALUE *M_DumpToJSON(void)\n{\n    JSON_ARRAY *const arr = JSON_ArrayNew();\n    for (int32_t i = 0; i < Console_History_GetLength(); i++) {\n        JSON_ArrayAppendString(arr, Console_History_Get(i));\n    }\n\n    if (arr->length == 0) {\n        JSON_ArrayFree(arr);\n        return nullptr;\n    }\n    JSON_OBJECT *root_obj = JSON_ObjectNew();\n    JSON_ObjectAppendArray(root_obj, \"entries\", arr);\n    return JSON_ValueFromObject(root_obj);\n}\n\nvoid Console_History_Init(void)\n{\n    m_History = Vector_Create(sizeof(char *));\n    JSON_VALUE *const doc = JSONFile_Read(M_GetPath());\n    if (doc != nullptr) {\n        M_LoadFromJSON(doc);\n        JSON_ValueFree(doc);\n    }\n}\n\nvoid Console_History_Shutdown(void)\n{\n    if (m_History == nullptr) {\n        return;\n    }\n\n    JSON_VALUE *const doc = M_DumpToJSON();\n    if (doc != nullptr) {\n        JSONFile_Write(M_GetPath(), doc);\n        JSON_ValueFree(doc);\n    }\n\n    for (int32_t i = m_History->count - 1; i >= 0; i--) {\n        char *const prompt = *(char **)Vector_Get(m_History, i);\n        Memory_Free(prompt);\n    }\n    Vector_Free(m_History);\n    m_History = nullptr;\n}\n\nint32_t Console_History_GetLength(void)\n{\n    return m_History->count;\n}\n\nvoid Console_History_Clear(void)\n{\n    for (int32_t i = m_History->count - 1; i >= 0; i--) {\n        char *const prompt = *(char **)Vector_Get(m_History, i);\n        Memory_Free(prompt);\n    }\n    Vector_Clear(m_History);\n}\n\nvoid Console_History_Append(const char *const prompt)\n{\n    for (int32_t i = m_History->count - 1; i >= 0; i--) {\n        char *const entry = *(char **)Vector_Get(m_History, i);\n        if (strcmp(entry, prompt) == 0) {\n            Memory_Free(entry);\n            Vector_RemoveAt(m_History, i);\n        }\n    }\n\n    if (m_History->count == MAX_HISTORY_ENTRIES) {\n        char *const oldest = *(char **)Vector_Get(m_History, 0);\n        Memory_Free(oldest);\n        Vector_RemoveAt(m_History, 0);\n    }\n\n    char *const prompt_copy = Memory_DupStr(prompt);\n    Vector_Add(m_History, &prompt_copy);\n}\n\nconst char *Console_History_Get(const int32_t idx)\n{\n    if (idx < 0 || idx >= m_History->count) {\n        return nullptr;\n    }\n    const char *const prompt = *(char **)Vector_Get(m_History, idx);\n    return prompt;\n}\n"
  },
  {
    "path": "src/trx/game/console/history.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nint32_t Console_History_GetLength(void);\nvoid Console_History_Clear(void);\nvoid Console_History_Append(const char *prompt);\nconst char *Console_History_Get(int32_t idx);\n"
  },
  {
    "path": "src/trx/game/console/internal.h",
    "content": "#pragma once\n\nvoid Console_History_Init(void);\nvoid Console_History_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/console/registry.c",
    "content": "#include <trx/game/console/registry.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n\n#include <stdio.h>\n#include <string.h>\n\ntypedef struct M_NODE {\n    CONSOLE_COMMAND cmd;\n    struct M_NODE *next;\n} M_NODE;\n\nstatic M_NODE *m_List = nullptr;\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    M_NODE *current = m_List;\n\n    while (current != nullptr) {\n        M_NODE *const next = current->next;\n        Memory_Free(current);\n        current = next;\n    }\n\n    m_List = nullptr;\n}\n\nconst CONSOLE_COMMAND *Console_Registry_Get(const char *const cmdline)\n{\n    const M_NODE *current = m_List;\n    while (current != nullptr) {\n        const M_NODE *const next = current->next;\n        char regex[strlen(current->cmd.prefix) + 13];\n        sprintf(regex, \"^(%s)(\\\\s+.*)?$\", current->cmd.prefix);\n        if (String_Match(cmdline, regex)) {\n            return &current->cmd;\n        }\n        current = next;\n    }\n    return nullptr;\n}\n\nvoid Console_Registry_Add(CONSOLE_COMMAND cmd)\n{\n    M_NODE *node = Memory_Alloc(sizeof(M_NODE));\n    node->cmd = cmd;\n    node->next = m_List;\n    m_List = node;\n}\n\nVECTOR *Console_Registry_GetAll(void)\n{\n    VECTOR *vec = Vector_Create(sizeof(const CONSOLE_COMMAND *));\n    M_NODE *node = m_List;\n    while (node != nullptr) {\n        const CONSOLE_COMMAND *cmd_ptr = &node->cmd;\n        Vector_Add(vec, &cmd_ptr);\n        node = node->next;\n    }\n    return vec;\n}\n"
  },
  {
    "path": "src/trx/game/console/registry.h",
    "content": "#pragma once\n\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/console/types.h>\n\n#include <stddef.h>\n\nvoid Console_Registry_Add(CONSOLE_COMMAND cmd);\n\nconst CONSOLE_COMMAND *Console_Registry_Get(const char *cmdline);\n\n// Retrieve a vector containing pointers to all registered console commands.\n// The returned vector must be freed via Vector_Free().\nVECTOR *Console_Registry_GetAll(void);\n\n#define REGISTER_CONSOLE_COMMAND(prefix_, proc_, help_)                        \\\n    __attribute__((__constructor__)) static void CONCAT(                       \\\n        M_Register_, __LINE__)(void)                                           \\\n    {                                                                          \\\n        Console_Registry_Add((CONSOLE_COMMAND) {                               \\\n            .prefix = prefix_, .proc = proc_, .help_id = help_ });             \\\n    }\n"
  },
  {
    "path": "src/trx/game/console/types.h",
    "content": "#pragma once\n#include <trx/game/console/enum.h>\n#include <trx/game/game_strings/entries.h>\n\ntypedef struct {\n    const struct CONSOLE_COMMAND *cmd;\n    const char *prefix;\n    const char *args;\n} COMMAND_CONTEXT;\n\ntypedef struct CONSOLE_COMMAND {\n    const char *prefix;\n    COMMAND_RESULT (*proc)(const COMMAND_CONTEXT *ctx);\n    GAME_STRING_ID help_id;\n} CONSOLE_COMMAND;\n"
  },
  {
    "path": "src/trx/game/console.h",
    "content": "#pragma once\n\n#include <trx/game/console/common.h>\n#include <trx/game/console/history.h>\n"
  },
  {
    "path": "src/trx/game/const.h",
    "content": "#pragma once\n\n#include <trx/core/math/const.h>\n\n#define LOGIC_FPS 30\n\n#define STEP_L 256\n#define WALL_L 1024 // = 1 << WALL_SHIFT\n#define WALL_SHIFT 10\n\n#define GRAVITY 6\n#define FAST_FALL_SPEED 128\n\n#define FOV_VALUE_PASSPORT 80\n#define FOV_MODE_GAME FOV_MODE_PC\n#define FOV_MODE_PASSPORT FOV_MODE_PS1\n#define FOV_MODE_CUTSCENE (g_TRVersion == 1 ? FOV_MODE_VERTICAL : FOV_MODE_PC)\n"
  },
  {
    "path": "src/trx/game/creature/alert.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items.h>\n#include <trx/game/pathing.h>\n\nvoid Creature_AlertNearbyGuards(const ITEM *const item)\n{\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM) {\n            continue;\n        }\n\n        const ITEM *const target = Item_Get(creature->item_num);\n        if (target->room_num == item->room_num) {\n            creature->alerted = true;\n            continue;\n        }\n\n        int32_t dx = (target->pos.x - item->pos.x) >> 6;\n        int32_t dy = (target->pos.y - item->pos.y) >> 6;\n        int32_t dz = (target->pos.z - item->pos.z) >> 6;\n        int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n\n        if (dist < 8000) {\n            creature->alerted = true;\n        }\n    }\n}\n\nvoid Creature_AlertAllGuards(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM) {\n            continue;\n        }\n\n        const ITEM *const target = Item_Get(creature->item_num);\n        if (target->object_id == item->object_id\n            && target->status == IS_ACTIVE) {\n            creature->alerted = true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/creature/alert.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nvoid Creature_AlertNearbyGuards(const ITEM *item);\nvoid Creature_AlertAllGuards(int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/creature/behavior.c",
    "content": "#include <trx/config.h>\n#include <trx/core/vector.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/stats.h>\n\n#define M_ALLY_FRIENDLY_FIRE_THRESHOLD 10\n\nstatic bool m_AlliesHostile = false;\nstatic VECTOR *m_AllyObjects = nullptr;\nstatic VECTOR *m_AllyTargetingObjects = nullptr;\n\n__attribute__((constructor)) static void M_Init(void)\n{\n    m_AllyObjects = Vector_Create(sizeof(OBJECT_ID));\n    m_AllyTargetingObjects = Vector_Create(sizeof(OBJECT_ID));\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n#define L_DELETE_VECTOR(vec)                                                   \\\n    if (vec != nullptr) {                                                      \\\n        Vector_Free(vec);                                                      \\\n        vec = nullptr;                                                         \\\n    }\n\n    L_DELETE_VECTOR(m_AllyObjects);\n    L_DELETE_VECTOR(m_AllyTargetingObjects);\n\n#undef L_DELETE_VECTOR\n}\n\nvoid Creature_Reset(void)\n{\n    Creature_SetAlliesHostile(false);\n    Vector_Clear(m_AllyObjects);\n    Vector_Clear(m_AllyTargetingObjects);\n}\n\nbool Creature_AreAlliesHostile(void)\n{\n    return m_AlliesHostile;\n}\n\nvoid Creature_SetAlliesHostile(bool enable)\n{\n    m_AlliesHostile = enable;\n    if (enable) {\n        Stats_MarkAlliesHostile();\n    }\n}\n\nvoid Creature_Hurt(ITEM *const item, const int32_t damage)\n{\n    if (damage <= 0) {\n        return;\n    }\n\n    CREATURE *const creature = item->creature_data;\n    if (creature != nullptr) {\n        creature->hurt_by_lara = true;\n    }\n\n    if (!Creature_IsAlly(item)) {\n        return;\n    }\n\n    switch (g_Config.gameplay.ally_hostility_policy) {\n    case ALLY_HOSTILITY_POLICY_INDIVIDUAL:\n        Stats_MarkAlliesHostile();\n        break;\n\n    case ALLY_HOSTILITY_POLICY_SHARED:\n        if (!m_AlliesHostile) {\n            if (creature != nullptr) {\n                creature->damage_from_lara += damage;\n            }\n            if (item->hit_points <= 0\n                || (creature != nullptr\n                    && (creature->damage_from_lara\n                            > M_ALLY_FRIENDLY_FIRE_THRESHOLD\n                        || creature->mood == MOOD_BORED))) {\n                m_AlliesHostile = true;\n                Stats_MarkAlliesHostile();\n            }\n        }\n        break;\n    }\n}\n\nbool Creature_IsHostile(const ITEM *const item)\n{\n    if (item->object_id != O_SKIDOO_ARMED\n        && !Object_IsType(item->object_id, g_CreatureObjects)) {\n        return false;\n    }\n\n    if (!Creature_IsAlly(item)) {\n        return true;\n    }\n\n    switch (g_Config.gameplay.ally_hostility_policy) {\n    case ALLY_HOSTILITY_POLICY_INDIVIDUAL:\n        const CREATURE *const creature = item->creature_data;\n        return creature != nullptr && creature->hurt_by_lara;\n    case ALLY_HOSTILITY_POLICY_SHARED:\n        return m_AlliesHostile;\n    }\n    return false;\n}\n\nbool Creature_IsAlly(const ITEM *const item)\n{\n    return Vector_Contains(m_AllyObjects, (void *)&item->object_id);\n}\n\nbool Creature_IsAllyTargetingEnemy(const ITEM *const item)\n{\n    return Vector_Contains(m_AllyTargetingObjects, (void *)&item->object_id);\n}\n\nvoid Creature_AddAlly(const OBJECT_ID obj_id)\n{\n    Vector_Add(m_AllyObjects, (void *)&obj_id);\n}\n\nvoid Creature_AddAllyTargetingEnemy(const OBJECT_ID obj_id)\n{\n    Vector_Add(m_AllyTargetingObjects, (void *)&obj_id);\n}\n"
  },
  {
    "path": "src/trx/game/creature/common.c",
    "content": "#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera/vars.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/creature.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/creatures/skidoo_driver.h>\n#include <trx/game/objects/creatures/tribe_boss.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_FLOAT_SPEED      32\n#define M_MAX_DISTANCE     (g_TRVersion < 3 ? WALL_L * 30 : STEP_L * 125) // = 30720 (TR1/2), 32000 (TR3)\n#define M_ATTACK_RANGE     SQUARE(WALL_L * 3) // = 0x900000 = 9437184\n#define M_ESCAPE_CHANCE    2048\n#define M_RECOVER_CHANCE   256\n#define M_TARGET_TOLERANCE 0x400000\n#define M_MAX_TILT         (3 * DEG_1) // = 546\n#define M_MAX_HEAD_CHANGE  (5 * DEG_1) // = 910\n#define M_MAX_JOINT_CHANGE (5 * DEG_1) // = 910\n#define M_HEAD_ARC         (g_TRVersion == 1 ? FRONT_ARC : 0x3000) // = 16384 (TR1), 12288 (TR2)\n#define M_JOINT_ARC        0x3000\n#define M_MAX_X_ROT        (20 * DEG_1) // = 3640\n#define M_BITE_DISTANCE    (g_TRVersion < 3 ? STEP_L : STEP_L * 2)\n#define M_BOX_DAMAGE       20\n// clang-format on\n\nstatic const LARA_TRX_STATE m_CrouchShiftStates[] = {\n    // clang-format off\n    LS_CROUCH_IDLE,\n    LS_CROUCH_ROLL,\n    LS_CROUCH_TURN_LEFT,\n    LS_CROUCH_TURN_RIGHT,\n    LS_CRAWL_IDLE,\n    LS_CRAWL_FORWARD,\n    LS_CRAWL_TURN_LEFT,\n    LS_CRAWL_TURN_RIGHT,\n    LS_TRX_INVALID, // sentinel\n    // clang-format on\n};\n\nstatic bool M_TestSwitchOrKill(\n    const int16_t item_num, const OBJECT_ID target_id)\n{\n    if (Object_Get(target_id)->loaded) {\n        return true;\n    }\n\n    LOG_WARNING(\n        \"Object %d is not loaded; item %d cannot be converted.\", target_id,\n        item_num);\n    Item_Kill(item_num);\n    return false;\n}\n\nstatic void M_GetBaddieTarget(const int16_t item_num, const bool goody)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    ITEM *best_item = nullptr;\n    int32_t best_distance = INT32_MAX;\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const int16_t target_item_num = LOT_GetBaddieSlot(i)->item_num;\n        if (target_item_num == NO_ITEM || target_item_num == item_num) {\n            continue;\n        }\n\n        ITEM *const target = Item_Get(target_item_num);\n        const OBJECT_ID obj_id = target->object_id;\n        if (goody && !Creature_IsAllyTargetingEnemy(target)) {\n            continue;\n        } else if (!goody && !Creature_IsAlly(target)) {\n            continue;\n        }\n\n        const int32_t dx = (target->pos.x - item->pos.x) >> 6;\n        const int32_t dy = (target->pos.y - item->pos.y) >> 6;\n        const int32_t dz = (target->pos.z - item->pos.z) >> 6;\n        const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (distance < best_distance) {\n            best_item = target;\n            best_distance = distance;\n        }\n    }\n\n    if (best_item == nullptr) {\n        if (!goody || Creature_IsHostile(item)) {\n            creature->enemy = lara_item;\n        } else {\n            creature->enemy = nullptr;\n        }\n        return;\n    }\n\n    if (!goody || Creature_IsHostile(item)) {\n        const int32_t dx = (lara_item->pos.x - item->pos.x) >> 6;\n        const int32_t dy = (lara_item->pos.y - item->pos.y) >> 6;\n        const int32_t dz = (lara_item->pos.z - item->pos.z) >> 6;\n        const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (distance < best_distance) {\n            best_item = lara_item;\n            best_distance = distance;\n        }\n    }\n\n    const ITEM *const target = creature->enemy;\n    if (target == nullptr || target->status != IS_ACTIVE) {\n        creature->enemy = best_item;\n    } else {\n        const int32_t dx = (target->pos.x - item->pos.x) >> 6;\n        const int32_t dy = (target->pos.y - item->pos.y) >> 6;\n        const int32_t dz = (target->pos.z - item->pos.z) >> 6;\n        const int32_t distance = SQUARE(dz) + SQUARE(dy) + SQUARE(dx);\n        if (distance < best_distance + M_TARGET_TOLERANCE) {\n            creature->enemy = best_item;\n        }\n    }\n}\n\nstatic ITEM *M_ChooseEnemy(const ITEM *const item)\n{\n    CREATURE *const creature = item->creature_data;\n    if (Creature_IsAlly(item)) {\n        M_GetBaddieTarget(creature->item_num, true);\n    } else if (Creature_IsAllyTargetingEnemy(item)) {\n        M_GetBaddieTarget(creature->item_num, false);\n    } else {\n        creature->enemy = Lara_GetItem();\n    }\n\n    if (creature->enemy != nullptr) {\n        return creature->enemy;\n    }\n    return Lara_GetItem();\n}\n\nstatic bool M_SwitchToWater(\n    const int16_t item_num, const int32_t wh, const HYBRID_INFO *const info)\n{\n    if (wh == NO_HEIGHT) {\n        return false;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->hit_points <= 0\n        && item->current_anim_state == info->land.death_state) {\n        // Dead land creatures should remain in their pose permanently.\n        return false;\n    }\n\n    // Switch to the water creature, but only switch animations if the creature\n    // is alive to avoid savegame reload issues.\n    if (!M_TestSwitchOrKill(item_num, info->water.id)) {\n        return false;\n    }\n\n    item->object_id = info->water.id;\n    if (item->hit_points > 0) {\n        Item_SwitchToAnim(item, info->water.active_anim, 0);\n        item->current_anim_state = Item_GetAnim(item)->current_anim_state;\n        item->goal_anim_state = item->current_anim_state;\n    }\n    item->pos.y = wh;\n\n    return true;\n}\n\nstatic bool M_SwitchToLand(\n    const int16_t item_num, const int32_t wh, const HYBRID_INFO *const info)\n{\n    if (wh != NO_HEIGHT) {\n        return false;\n    }\n\n    if (!M_TestSwitchOrKill(item_num, info->land.id)) {\n        return false;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n\n    // Switch to the land creature regardless of death state.\n    item->object_id = info->land.id;\n    item->rot.x = 0;\n\n    if (item->hit_points > 0) {\n        Item_SwitchToAnim(item, info->land.active_anim, 0);\n        item->current_anim_state = Item_GetAnim(item)->current_anim_state;\n        item->goal_anim_state = item->current_anim_state;\n\n    } else {\n        Item_SwitchToAnim(item, info->land.death_anim, -1);\n        item->current_anim_state = info->land.death_state;\n        item->goal_anim_state = item->current_anim_state;\n\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n        item->floor = Room_GetHeight(sector, item->pos);\n        item->pos.y = item->floor;\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    return true;\n}\n\nconst ITEM *M_GetBaddieOverlap(const int16_t item_num)\n{\n    const ITEM *item = Item_Get(item_num);\n    if (item->speed == 0 || item->hit_points <= 0) {\n        return nullptr;\n    }\n\n    const int32_t x = item->pos.x;\n    const int32_t y = item->pos.y;\n    const int32_t z = item->pos.z;\n    int32_t radius = Object_Get(item->object_id)->radius;\n    if (g_TRVersion < 3) {\n        radius = SQUARE(radius);\n    }\n\n    int16_t link = Room_Get(item->room_num)->item_num;\n    while (link != NO_ITEM && link != item_num) {\n        item = Item_Get(link);\n        if (item != Lara_GetItem() && item->status == IS_ACTIVE) {\n            if (g_TRVersion >= 3 && item->hit_points > 0) {\n                const int32_t dx = ABS(item->pos.x - x);\n                const int32_t dz = ABS(item->pos.z - z);\n                const int32_t distance =\n                    dx > dz ? dx + (dz >> 1) : dz + (dx >> 1);\n                const int32_t item_radius = Object_Get(item->object_id)->radius;\n                if (distance < item_radius + radius) {\n                    return item;\n                }\n            } else if (g_TRVersion < 3 && item->speed != 0) {\n                const XYZ_32 delta = {\n                    item->pos.x - x,\n                    item->pos.y - y,\n                    item->pos.z - z,\n                };\n                const int32_t distance =\n                    SQUARE(delta.x) + SQUARE(delta.y) + SQUARE(delta.z);\n                if (distance < radius) {\n                    return item;\n                }\n            }\n        }\n\n        link = item->next_item;\n    }\n\n    return nullptr;\n}\n\nstatic bool M_TestDrowned(\n    const ITEM *const item, const BOUNDS_16 *const bounds,\n    const int16_t room_num)\n{\n    if (item->hit_points <= 0) {\n        return false;\n    }\n\n    switch (g_Config.gameplay.creature_drown_policy) {\n    case CREATURE_DROWN_POLICY_DEFAULT:\n        return Room_Get(room_num)->flags.underwater;\n    case CREATURE_DROWN_POLICY_SUBMERGED:\n        const int32_t water_height = Room_GetWaterHeight(item->pos, room_num);\n        return water_height != NO_HEIGHT\n            && water_height + STEP_L <= item->pos.y - ABS(bounds->min.y);\n    default:\n        return false;\n    }\n}\n\nvoid Creature_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    // TODO: remove GF check once demo config reset is run before level load\n    if (g_Config.gameplay.enable_enemy_rotation\n        || GF_GetCurrentLevel()->type == GFL_DEMO) {\n        item->rot.y += (Random_GetControl() - DEG_90) >> 1;\n    }\n    item->collidable = true;\n    item->creature_data = nullptr;\n    item->extra_rotations = nullptr;\n}\n\nbool Creature_Activate(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_INVISIBLE) {\n        return item->creature_data != nullptr;\n    }\n\n    if (!LOT_EnableBaddieAI(item_num, false)) {\n        return false;\n    }\n\n    item->status = IS_ACTIVE;\n    return true;\n}\n\nvoid Creature_AIInfo(ITEM *const item, AI_INFO *const info)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    ITEM *enemy = g_TRVersion >= 3 ? creature->enemy : M_ChooseEnemy(item);\n    if (enemy == nullptr) {\n        enemy = Lara_GetItem();\n        creature->enemy = enemy;\n    }\n    const int16_t *const zone = Box_GetLotZone(&creature->lot);\n    const bool use_fixed_fly_zone =\n        g_TRVersion == 3 && creature->lot.setup.fly != 0;\n\n    {\n        const ROOM *const room = Room_Get(item->room_num);\n        item->box_num =\n            Room_GetWorldSector(room, item->pos.x, item->pos.z)->box;\n        info->zone_num =\n            use_fixed_fly_zone ? BOX_FIXED_FLY_ZONE : zone[item->box_num];\n    }\n\n    {\n        const ROOM *const room = Room_Get(enemy->room_num);\n        enemy->box_num =\n            Room_GetWorldSector(room, enemy->pos.x, enemy->pos.z)->box;\n        info->enemy_zone_num =\n            use_fixed_fly_zone ? BOX_FIXED_FLY_ZONE : zone[enemy->box_num];\n    }\n\n    const BOX_INFO *const enemy_box = Box_GetBox(enemy->box_num);\n    // TODO: TR3 defines non-LOT creatures, like cobras and handles them\n    // differently here and in LOT initialisation.\n    if (((enemy_box->overlap_index & creature->lot.setup.block_mask) != 0)\n        || (creature->lot.node[item->box_num].search_num\n            == (creature->lot.search_num | BOX_BLOCKED_SEARCH))) {\n        info->enemy_zone_num |= BOX_BLOCKED;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    const XZ_32 pivot = {\n        .x = (obj->pivot_length * Math_Sin(item->rot.y)) >> W2V_SHIFT,\n        .z = (obj->pivot_length * Math_Cos(item->rot.y)) >> W2V_SHIFT,\n    };\n\n    XZ_32 enemy_pos = {\n        .x = enemy->pos.x,\n        .z = enemy->pos.z,\n    };\n    if (g_TRVersion >= 3) {\n        const int16_t enemy_angle =\n            enemy == lara_item ? lara->move_angle : enemy->rot.y;\n        enemy_pos.x += (14 * enemy->speed * Math_Sin(enemy_angle)) >> W2V_SHIFT;\n        enemy_pos.z += (14 * enemy->speed * Math_Cos(enemy_angle)) >> W2V_SHIFT;\n    }\n\n    int32_t x = enemy_pos.x - pivot.x - item->pos.x;\n    int32_t z = enemy_pos.z - pivot.z - item->pos.z;\n    int32_t y = item->pos.y - enemy->pos.y; // sic, reversed\n\n    if (enemy == lara_item && Lara_HasState(m_CrouchShiftStates)) {\n        y -= STEP_L * 3 / 2;\n    }\n\n    const bool too_far = ABS(z) > M_MAX_DISTANCE || ABS(x) > M_MAX_DISTANCE;\n    if (creature->enemy == nullptr || (g_TRVersion == 3 && too_far)) {\n        info->distance = INT32_MAX;\n    } else if (g_TRVersion < 3 && too_far) {\n        info->distance = SQUARE(M_MAX_DISTANCE);\n    } else {\n        info->distance = SQUARE(x) + SQUARE(z);\n    }\n\n    const int16_t angle = Math_Atan(z, x);\n    info->angle = angle - item->rot.y;\n    info->enemy_facing = angle - enemy->rot.y + DEG_180;\n    info->ahead = info->angle > -FRONT_ARC && info->angle < FRONT_ARC;\n    info->bite = info->ahead\n        && ABS(enemy->pos.y - item->pos.y) <= M_BITE_DISTANCE\n        && (g_TRVersion == 1 || enemy->hit_points > 0);\n\n    x = ABS(x);\n    z = ABS(z);\n    if (x > z) {\n        info->x_angle = Math_Atan(x + (z >> 1), y);\n    } else {\n        info->x_angle = Math_Atan(z + (x >> 1), y);\n    }\n\n    if (g_TRVersion == 3) {\n        if (!creature->hurt_by_lara && creature->enemy == lara_item\n            && !Creature_IsHostile(item) && Creature_IsAlly(item)) {\n            creature->enemy = nullptr;\n            info->ahead = false;\n            info->bite = false;\n            info->distance = INT32_MAX;\n        }\n    }\n}\n\nbool Creature_EnsureHabitat(\n    const int16_t item_num, int32_t *const wh, const HYBRID_INFO *const info)\n{\n    // Test the environment for a hybrid creature. Record the water height and\n    // return whether or not a type conversion has taken place.\n    const ITEM *const item = Item_Get(item_num);\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    if (wh != nullptr) {\n        *wh = water_height;\n    }\n    if (item->status == IS_INACTIVE) {\n        return false;\n    }\n\n    return item->object_id == info->land.id\n        ? M_SwitchToWater(item_num, water_height, info)\n        : M_SwitchToLand(item_num, water_height, info);\n}\n\nvoid Creature_Mood(\n    const ITEM *const item, const AI_INFO *const info, const bool violent)\n{\n    Creature_UpdateMood(item, info, violent);\n    Creature_ApplyMood(item, info, violent);\n}\n\nvoid Creature_UpdateMood(\n    const ITEM *const item, const AI_INFO *const info, const bool violent)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    LOT_INFO *const lot = &creature->lot;\n    const ITEM *enemy = creature->enemy;\n    if (lot->node[item->box_num].search_num\n        == (lot->search_num | BOX_BLOCKED_SEARCH)) {\n        lot->required_box = NO_BOX;\n    }\n\n    if (creature->mood != MOOD_ATTACK && lot->required_box != NO_BOX\n        && !Box_ValidBox(item, info->zone_num, lot->target_box)) {\n        if (info->zone_num == info->enemy_zone_num) {\n            creature->mood = MOOD_BORED;\n        }\n        lot->required_box = NO_BOX;\n    }\n\n    const MOOD_TYPE mood = creature->mood;\n    if (enemy == nullptr) {\n        creature->mood = MOOD_BORED;\n        enemy = Lara_GetItem();\n    } else if (\n        enemy->hit_points <= 0\n        && (g_TRVersion < 3 || enemy == Lara_GetItem())) {\n        creature->mood = MOOD_BORED;\n    } else if (violent) {\n        switch (mood) {\n        case MOOD_BORED:\n        case MOOD_STALK:\n            if (info->zone_num == info->enemy_zone_num) {\n                creature->mood = MOOD_ATTACK;\n            } else if (item->hit_status) {\n                creature->mood = MOOD_ESCAPE;\n            }\n            break;\n\n        case MOOD_ATTACK:\n            if (info->zone_num != info->enemy_zone_num) {\n                creature->mood = MOOD_BORED;\n            }\n            break;\n\n        case MOOD_ESCAPE:\n            if (info->zone_num == info->enemy_zone_num) {\n                creature->mood = MOOD_ATTACK;\n            }\n            break;\n        }\n    } else {\n        switch (mood) {\n        case MOOD_BORED:\n        case MOOD_STALK:\n            if (g_TRVersion >= 3 && creature->alerted\n                && info->zone_num != info->enemy_zone_num) {\n                creature->mood =\n                    info->distance > WALL_L * 3 ? MOOD_STALK : MOOD_BORED;\n            } else if (\n                g_TRVersion < 3 && item->hit_status\n                && (Random_GetControl() < M_ESCAPE_CHANCE\n                    || info->zone_num != info->enemy_zone_num)) {\n                creature->mood = MOOD_ESCAPE;\n            } else if (info->zone_num == info->enemy_zone_num) {\n                if (info->distance < M_ATTACK_RANGE\n                    || (creature->mood == MOOD_STALK\n                        && lot->required_box == NO_BOX)) {\n                    creature->mood = MOOD_ATTACK;\n                } else {\n                    creature->mood = MOOD_STALK;\n                }\n            }\n            break;\n\n        case MOOD_ATTACK:\n            if (item->hit_status\n                && (Random_GetControl() < M_ESCAPE_CHANCE\n                    || info->zone_num != info->enemy_zone_num)) {\n                creature->mood = g_TRVersion < 3 ? MOOD_ESCAPE : MOOD_STALK;\n            } else if (\n                info->zone_num != info->enemy_zone_num\n                && (g_TRVersion < 3 || info->distance > 6 * WALL_L)) {\n                creature->mood = MOOD_BORED;\n            }\n            break;\n\n        case MOOD_ESCAPE:\n            if (info->zone_num == info->enemy_zone_num\n                && Random_GetControl() < M_RECOVER_CHANCE) {\n                creature->mood = MOOD_STALK;\n            }\n            break;\n        }\n    }\n\n    if (mood != creature->mood) {\n        if (mood == MOOD_ATTACK) {\n            Box_TargetBox(lot, lot->target_box);\n        }\n        lot->required_box = NO_BOX;\n    }\n}\n\nvoid Creature_ApplyMood(\n    const ITEM *const item, const AI_INFO *const info, const bool violent)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    LOT_INFO *const lot = &creature->lot;\n    const ITEM *enemy = creature->enemy;\n\n    switch (creature->mood) {\n    case MOOD_BORED: {\n        const int16_t box_num =\n            lot->node[lot->zone_count * Random_GetControl() / 0x7FFF].box_num;\n        if (Box_ValidBox(item, info->zone_num, box_num)) {\n            if (Box_StalkBox(item, enemy, box_num) && creature->enemy != nullptr\n                && enemy->hit_points > 0) {\n                Box_TargetBox(lot, box_num);\n                if (g_TRVersion < 3) {\n                    creature->mood = MOOD_STALK;\n                }\n            } else if (lot->required_box == NO_BOX) {\n                Box_TargetBox(lot, box_num);\n            }\n        }\n        break;\n    }\n\n    case MOOD_ATTACK: {\n        const int32_t smartness = Object_Get(item->object_id)->smartness;\n        if (smartness < 0 || Random_GetControl() < smartness) {\n            lot->target = enemy->pos;\n            lot->required_box = enemy->box_num;\n            if (lot->setup.fly != 0\n                && Lara_GetLaraInfo()->water_status == LWS_ABOVE_WATER) {\n                lot->target.y += Item_GetBestFrame(enemy)->bounds.min.y;\n            }\n        }\n        break;\n    }\n\n    case MOOD_ESCAPE: {\n        const int16_t box_num =\n            lot->node[lot->zone_count * Random_GetControl() / 0x7FFF].box_num;\n        if (Box_ValidBox(item, info->zone_num, box_num)\n            && lot->required_box == NO_BOX) {\n            if (Box_EscapeBox(item, enemy, box_num)) {\n                Box_TargetBox(lot, box_num);\n            } else if (\n                info->zone_num == info->enemy_zone_num\n                && Box_StalkBox(item, enemy, box_num)\n                && (g_TRVersion < 3 || !violent)) {\n                Box_TargetBox(lot, box_num);\n                creature->mood = MOOD_STALK;\n            }\n        }\n        break;\n    }\n\n    case MOOD_STALK: {\n        if (lot->required_box == NO_BOX\n            || !Box_StalkBox(item, enemy, lot->required_box)) {\n            const int16_t box_num =\n                lot->node[lot->zone_count * Random_GetControl() / 0x7FFF]\n                    .box_num;\n            if (Box_ValidBox(item, info->zone_num, box_num)) {\n                if (Box_StalkBox(item, enemy, box_num)) {\n                    Box_TargetBox(lot, box_num);\n                } else if (lot->required_box == NO_BOX) {\n                    Box_TargetBox(lot, box_num);\n                    if (info->zone_num != info->enemy_zone_num) {\n                        creature->mood = MOOD_BORED;\n                    }\n                }\n            }\n        }\n        break;\n    }\n    }\n\n    if (lot->target_box == NO_BOX) {\n        Box_TargetBox(lot, item->box_num);\n    }\n    Box_CalculateTarget(&creature->target, item, lot);\n}\n\nint16_t Creature_Turn(ITEM *const item, int16_t max_turn)\n{\n    const CREATURE *const creature = item->creature_data;\n    if (creature == nullptr || max_turn == 0) {\n        return 0;\n    }\n    if (item->speed == 0 && g_TRVersion < 3) {\n        return 0;\n    }\n\n    const int32_t dx = creature->target.x - item->pos.x;\n    const int32_t dz = creature->target.z - item->pos.z;\n\n    int16_t angle = Math_Atan(dz, dx) - item->rot.y;\n    if (angle > FRONT_ARC || angle < -FRONT_ARC) {\n        const int32_t range = (item->speed * (1 << 14)) / max_turn;\n        if (SQUARE(dx) + SQUARE(dz) < SQUARE(range)) {\n            max_turn /= 2;\n        }\n    }\n\n    CLAMP(angle, -max_turn, max_turn);\n    item->rot.y += angle;\n    return angle;\n}\n\nvoid Creature_Tilt(ITEM *const item, int16_t angle)\n{\n    angle = angle * 4 - item->rot.z;\n    CLAMP(angle, -M_MAX_TILT, M_MAX_TILT);\n    item->rot.z += angle;\n}\n\nvoid Creature_Head(ITEM *const item, const int16_t required)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    int16_t change = required - creature->head_rotation;\n    CLAMP(change, -M_MAX_HEAD_CHANGE, M_MAX_HEAD_CHANGE);\n\n    creature->head_rotation += change;\n    CLAMP(creature->head_rotation, -M_HEAD_ARC, M_HEAD_ARC);\n}\n\nvoid Creature_Neck(ITEM *const item, const int16_t required)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    int16_t change = required - creature->neck_rotation;\n    CLAMP(change, -M_MAX_HEAD_CHANGE, M_MAX_HEAD_CHANGE);\n\n    creature->neck_rotation += change;\n    CLAMP(creature->neck_rotation, -M_HEAD_ARC, M_HEAD_ARC);\n}\n\nvoid Creature_Joint(\n    ITEM *const item, const int16_t joint, const int16_t required)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    int16_t change = required - creature->joint_rotation[joint];\n    CLAMP(change, -M_MAX_JOINT_CHANGE, M_MAX_JOINT_CHANGE);\n\n    creature->joint_rotation[joint] += change;\n    CLAMP(creature->joint_rotation[joint], -M_JOINT_ARC, M_JOINT_ARC);\n}\n\nvoid Creature_Float(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    item->hit_points = 0;\n    item->rot.x = 0;\n\n    const int32_t wh = Room_GetWaterHeight(item->pos, item->room_num);\n    if (item->pos.y > wh) {\n        item->pos.y -= M_FLOAT_SPEED;\n    }\n    CLAMPL(item->pos.y, wh);\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nvoid Creature_Underwater(ITEM *const item, const int32_t depth)\n{\n    const int32_t wh = Room_GetWaterHeight(item->pos, item->room_num);\n    if (item->pos.y >= wh + depth) {\n        return;\n    }\n\n    item->pos.y = wh + depth;\n    if (item->rot.x > 2 * DEG_1) {\n        item->rot.x -= 2 * DEG_1;\n    } else {\n        CLAMPG(item->rot.x, 0);\n    }\n}\n\nbool Creature_CanSeeEnemy(const ITEM *const item, const AI_INFO *const info)\n{\n    // XXX(Dash): I don't understand the need for this function,\n    // when there's CanTargetEnemy().\n\n    const CREATURE *const creature = item->creature_data;\n    const ITEM *const enemy = creature->enemy;\n\n    if (enemy == nullptr || enemy->hit_points <= 0\n        || (enemy != Lara_GetItem() && enemy->creature_data == nullptr)\n        || info->angle - creature->joint_rotation[2] <= -DEG_90\n        || info->angle - creature->joint_rotation[2] >= DEG_90\n        || info->distance >= CREATURE_SHOOT_RANGE) {\n        return false;\n    }\n\n    GAME_VECTOR start = {\n        .x = item->pos.x,\n        .y = item->pos.y - STEP_L * 3,\n        .z = item->pos.z,\n        .room_num = item->room_num,\n    };\n\n    const BOUNDS_16 *const bounds = &Item_GetBestFrame(enemy)->bounds;\n    GAME_VECTOR target = {\n        .x = enemy->pos.x,\n        .y = enemy->pos.y + ((3 * bounds->min.y + bounds->max.y) >> 2),\n        .z = enemy->pos.z,\n        .room_num = enemy->room_num,\n    };\n\n    return LOS_Check(&start, &target, true);\n}\n\nbool Creature_CanTargetEnemy(const ITEM *const item, const AI_INFO *const info)\n{\n    const CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return false;\n    }\n\n    const ITEM *const enemy = creature->enemy;\n    if (enemy == nullptr || !info->ahead\n        || info->distance >= CREATURE_SHOOT_RANGE) {\n        return false;\n    }\n\n    GAME_VECTOR start = { .pos = item->pos, .room_num = item->room_num };\n    GAME_VECTOR target = { .pos = enemy->pos, .room_num = enemy->room_num };\n\n    if (g_TRVersion == 3) {\n        if (enemy->hit_points <= 0\n            || (enemy != Lara_GetItem() && enemy->creature_data == nullptr)) {\n            return false;\n        }\n\n        const BOUNDS_16 *const bounds1 = &Item_GetBestFrame(item)->bounds;\n        const BOUNDS_16 *const bounds2 = &Item_GetBestFrame(enemy)->bounds;\n\n        start.pos.y += (bounds1->max.y + 3 * bounds1->min.y) >> 2;\n        target.pos.y += (bounds2->max.y + 3 * bounds2->min.y) >> 2;\n    } else {\n        start.pos.y -= STEP_L * 3;\n        target.pos.y -= STEP_L * 3;\n    }\n\n    return LOS_Check(&start, &target, true);\n}\n\nvoid Creature_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    if (lara->water_status == LWS_UNDERWATER\n        || lara->water_status == LWS_SURFACE) {\n        return;\n    }\n\n    if (coll->enable_baddie_push) {\n        Lara_Col_ItemPush(\n            item, coll,\n            (g_TRVersion >= 2 || item->hit_points > 0) ? coll->enable_hit\n                                                       : false,\n            false);\n    } else if (coll->enable_hit && g_TRVersion == 3) {\n        const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds;\n        const int32_t s = Math_Sin(lara_item->rot.y);\n        const int32_t c = Math_Cos(lara_item->rot.y);\n        const int32_t x = (bounds->min.x + bounds->max.x) / 2;\n        const int32_t z = (bounds->max.z - bounds->min.z) / 2;\n        const int32_t rx =\n            (lara_item->pos.x - item->pos.x) - ((c * x + s * z) >> W2V_SHIFT);\n        const int32_t rz =\n            (lara_item->pos.z - item->pos.z) - ((c * z - s * x) >> W2V_SHIFT);\n\n        if (bounds->max.z - bounds->min.z > STEP_L) {\n            lara->hit_direction =\n                (lara_item->rot.y + DEG_180 - Math_Atan(rz, rx) + DEG_45)\n                >> W2V_SHIFT;\n            lara->hit_frame++;\n            CLAMPG(lara->hit_frame, 30);\n        }\n    }\n}\n\nbool Creature_Animate(\n    const int16_t item_num, const int16_t angle, const int16_t tilt)\n{\n    ITEM *const item = Item_Get(item_num);\n    const CREATURE *const creature = item->creature_data;\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (creature == nullptr) {\n        return false;\n    }\n\n    const LOT_INFO *const lot = &creature->lot;\n    const XYZ_32 old = item->pos;\n\n    const int16_t *const zone = Box_GetLotZone(lot);\n\n    if (g_TRVersion >= 2 && !Object_IsType(item->object_id, g_WaterObjects)) {\n        int16_t room_num = item->room_num;\n        Room_GetSector(item->pos, &room_num);\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    Item_Animate(item);\n    if (item->status == IS_DEACTIVATED) {\n        Creature_Die(item_num, false);\n        return false;\n    }\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    int32_t y = item->pos.y + bounds->min.y;\n\n    int16_t room_num = item->room_num;\n    XYZ_32 sample_pos = { old.x, y, old.z };\n    Room_GetSector(sample_pos, &room_num);\n    sample_pos.x = item->pos.x;\n    sample_pos.z = item->pos.z;\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n    int32_t height = Box_GetBox(sector->box)->height;\n    int16_t next_box = lot->node[sector->box].exit_box;\n    int32_t next_height =\n        next_box != NO_BOX ? Box_GetBox(next_box)->height : height;\n\n    const bool fly_check = g_TRVersion < 3 || lot->setup.fly == 0;\n\n    const int32_t box_height = Box_GetBox(item->box_num)->height;\n    if (sector->box == NO_BOX\n        || (fly_check && zone[item->box_num] != zone[sector->box])\n        || box_height - height > lot->setup.step\n        || box_height - height < lot->setup.drop) {\n        const int32_t pos_x = item->pos.x >> WALL_SHIFT;\n        const int32_t pos_z = g_TRVersion < 3\n            ? pos_x\n            : item->pos.z >> WALL_SHIFT; // TODO: OG bug in TR1/2?\n        const int32_t shift_x = old.x >> WALL_SHIFT;\n        const int32_t shift_z = old.z >> WALL_SHIFT;\n\n        if (pos_x < shift_x) {\n            item->pos.x = ROUND_TO_SECTOR(old.x);\n        } else if (pos_x > shift_x) {\n            item->pos.x = ROUND_TO_SECTOR_END(old.x);\n        }\n\n        if (pos_z < shift_z) {\n            item->pos.z = ROUND_TO_SECTOR(old.z);\n        } else if (pos_z > shift_z) {\n            item->pos.z = ROUND_TO_SECTOR_END(old.z);\n        }\n\n        sample_pos.x = item->pos.x;\n        sample_pos.y = y;\n        sample_pos.z = item->pos.z;\n        sector = Room_GetSector(sample_pos, &room_num);\n        height = Box_GetBox(sector->box)->height;\n        next_box = lot->node[sector->box].exit_box;\n        next_height =\n            next_box != NO_BOX ? Box_GetBox(next_box)->height : height;\n    }\n\n    const int32_t x = item->pos.x;\n    const int32_t z = item->pos.z;\n    const int32_t pos_x = x & (WALL_L - 1);\n    const int32_t pos_z = z & (WALL_L - 1);\n    int32_t shift_x = 0;\n    int32_t shift_z = 0;\n    const int32_t radius = obj->radius;\n\n    if (pos_z < radius) {\n        if (Box_BadFloor(\n                x, y, z - radius, height, next_height, room_num, lot)) {\n            shift_z = radius - pos_z;\n        }\n\n        if (pos_x < radius) {\n            if (Box_BadFloor(\n                    x - radius, y, z, height, next_height, room_num, lot)) {\n                shift_x = radius - pos_x;\n            } else if (\n                shift_z == 0\n                && Box_BadFloor(\n                    x - radius, y, z - radius, height, next_height, room_num,\n                    lot)) {\n                if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) {\n                    shift_z = radius - pos_z;\n                } else {\n                    shift_x = radius - pos_x;\n                }\n            }\n        } else if (pos_x > WALL_L - radius) {\n            if (Box_BadFloor(\n                    x + radius, y, z, height, next_height, room_num, lot)) {\n                shift_x = WALL_L - radius - pos_x;\n            } else if (\n                shift_z == 0\n                && Box_BadFloor(\n                    x + radius, y, z - radius, height, next_height, room_num,\n                    lot)) {\n                if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) {\n                    shift_z = radius - pos_z;\n                } else {\n                    shift_x = WALL_L - radius - pos_x;\n                }\n            }\n        }\n    } else if (pos_z > WALL_L - radius) {\n        if (Box_BadFloor(\n                x, y, z + radius, height, next_height, room_num, lot)) {\n            shift_z = WALL_L - radius - pos_z;\n        }\n\n        if (pos_x < radius) {\n            if (Box_BadFloor(\n                    x - radius, y, z, height, next_height, room_num, lot)) {\n                shift_x = radius - pos_x;\n            } else if (\n                shift_z == 0\n                && Box_BadFloor(\n                    x - radius, y, z + radius, height, next_height, room_num,\n                    lot)) {\n                if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) {\n                    shift_x = radius - pos_x;\n                } else {\n                    shift_z = WALL_L - radius - pos_z;\n                }\n            }\n        } else if (pos_x > WALL_L - radius) {\n            if (Box_BadFloor(\n                    x + radius, y, z, height, next_height, room_num, lot)) {\n                shift_x = WALL_L - radius - pos_x;\n            } else if (\n                shift_z == 0\n                && Box_BadFloor(\n                    x + radius, y, z + radius, height, next_height, room_num,\n                    lot)) {\n                if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) {\n                    shift_x = WALL_L - radius - pos_x;\n                } else {\n                    shift_z = WALL_L - radius - pos_z;\n                }\n            }\n        }\n    } else if (pos_x < radius) {\n        if (Box_BadFloor(\n                x - radius, y, z, height, next_height, room_num, lot)) {\n            shift_x = radius - pos_x;\n        }\n    } else if (pos_x > WALL_L - radius) {\n        if (Box_BadFloor(\n                x + radius, y, z, height, next_height, room_num, lot)) {\n            shift_x = WALL_L - radius - pos_x;\n        }\n    }\n\n    item->pos.x += shift_x;\n    item->pos.z += shift_z;\n\n    if (shift_x != 0 || shift_z != 0) {\n        sample_pos.x = item->pos.x;\n        sample_pos.y = y;\n        sample_pos.z = item->pos.z;\n        sector = Room_GetSector(sample_pos, &room_num);\n        item->rot.y += angle;\n        Creature_Tilt(item, tilt * 2);\n    }\n\n    if (g_TRVersion < 3\n        || (item->object_id != O_TREX && item->object_id != O_TREX_ALPHA)) {\n        const ITEM *const hit_item = M_GetBaddieOverlap(item_num);\n        if (g_TRVersion < 3 && hit_item != nullptr) {\n            item->pos = old;\n            return true;\n        }\n\n        if (g_TRVersion >= 3 && hit_item != nullptr) {\n            const int16_t item_angle = Math_Atan(\n                                           hit_item->pos.z - item->pos.z,\n                                           hit_item->pos.x - item->pos.x)\n                - item->rot.y;\n            if (item_angle != 0) {\n                if (ABS(item_angle) < 2048) {\n                    item->rot.y -= item_angle;\n                } else if (item_angle > 0) {\n                    item->rot.y -= 2048;\n                } else {\n                    item->rot.y += 2048;\n                }\n            }\n            return true;\n        }\n    }\n\n    if (lot->setup.fly != 0) {\n        int32_t dy = creature->target.y - item->pos.y;\n        CLAMP(dy, -lot->setup.fly, lot->setup.fly);\n\n        height =\n            Room_GetHeight(sector, (XYZ_32) { item->pos.x, y, item->pos.z });\n        if (item->pos.y + dy > height) {\n            if (item->pos.y <= height) {\n                dy = 0;\n                item->pos.y = height;\n            } else {\n                dy = -lot->setup.fly;\n                item->pos.x = old.x;\n                item->pos.z = old.z;\n            }\n        } else if (\n            fly_check || Object_IsType(item->object_id, g_WaterObjects)) {\n            const int32_t ceiling = Room_GetCeiling(\n                sector, (XYZ_32) { item->pos.x, y, item->pos.z });\n            int32_t min_y = bounds->min.y;\n            switch (item->object_id) {\n            case O_ALLIGATOR:\n                min_y = 0;\n                break;\n            case O_SHARK:\n            case O_ORCA:\n                min_y = 128;\n                break;\n            default:\n                break;\n            }\n            if (item->pos.y + min_y + dy < ceiling) {\n                if (item->pos.y + min_y < ceiling) {\n                    item->pos.x = old.x;\n                    item->pos.z = old.z;\n                    dy = lot->setup.fly;\n                } else {\n                    dy = 0;\n                }\n            }\n        } else {\n            sample_pos = (XYZ_32) { item->pos.x, y + STEP_L, item->pos.z };\n            Room_GetSector(sample_pos, &room_num);\n            const ROOM *const room = Room_Get(room_num);\n            if (room->flags.underwater || room->flags.swamp) {\n                dy = -lot->setup.fly;\n            }\n        }\n\n        item->pos.y += dy;\n        sample_pos = (XYZ_32) { item->pos.x, y, item->pos.z };\n        sector = Room_GetSector(sample_pos, &room_num);\n        item->floor = Room_GetHeight(sector, sample_pos);\n\n        int16_t item_angle = item->speed != 0 ? Math_Atan(item->speed, -dy) : 0;\n        if (g_TRVersion >= 2) {\n            CLAMP(item_angle, -M_MAX_X_ROT, M_MAX_X_ROT);\n        }\n\n        if (item_angle < item->rot.x - DEG_1) {\n            item->rot.x -= DEG_1;\n        } else if (item_angle > item->rot.x + DEG_1) {\n            item->rot.x += DEG_1;\n        } else {\n            item->rot.x = item_angle;\n        }\n    } else {\n        sector = Room_GetSector(item->pos, &room_num);\n        if (g_TRVersion == 3) {\n            const int32_t ceiling = Room_GetCeiling(sector, item->pos);\n            int32_t min_y = bounds->min.y;\n            switch (item->object_id) {\n            case O_TREX:\n            case O_TREX_ALPHA:\n            case O_SHIVA:\n            case O_CLAW_MUTANT:\n                min_y = STEP_L * 3;\n                break;\n            default:\n                break;\n            }\n\n            if (item->pos.y + min_y < ceiling) {\n                item->pos = old;\n                sector = Room_GetSector(item->pos, &room_num);\n            }\n        }\n\n        item->floor = Room_GetHeight(sector, item->pos);\n\n        if (item->pos.y > item->floor) {\n            item->pos.y = item->floor;\n        } else if (item->floor - item->pos.y > STEP_L / 4) {\n            item->pos.y += STEP_L / 4;\n        } else if (item->pos.y < item->floor) {\n            item->pos.y = item->floor;\n        }\n        item->rot.x = 0;\n    }\n\n    if (!Object_IsType(item->object_id, g_WaterObjects)) {\n        // Get the room just above the enemy so that if it is in one-click high\n        // water, its effects behave still as though in a dry room.\n        Room_GetSector(\n            (XYZ_32) { item->pos.x, item->pos.y - (STEP_L * 2), item->pos.z },\n            &room_num);\n        if (M_TestDrowned(item, bounds, room_num)) {\n            item->hit_points = 0;\n        }\n    }\n\n    Item_UpdateRoom(item_num, room_num);\n    return true;\n}\n\nvoid Creature_SpecialKill(\n    ITEM *const item, const int32_t kill_anim, const int32_t kill_state,\n    const int32_t lara_kill_state)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n\n    Item_SwitchToAnim(item, kill_anim, 0);\n    item->current_anim_state = kill_state;\n\n    lara_item->pos = item->pos;\n    lara_item->rot = item->rot;\n    lara_item->fall_speed = 0;\n    lara_item->gravity = false;\n    lara_item->speed = 0;\n    lara->air = -1;\n    lara->gun_type = LGT_UNARMED;\n\n    int16_t room_num = item->room_num;\n    Item_UpdateRoom(lara->item_num, room_num);\n\n    Lara_SwitchToExtraState(lara_kill_state);\n\n    g_Camera.pos.room_num = lara_item->room_num;\n}\n\nvoid Creature_TestBoxDamage(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->box_num == NO_BOX\n        || (Box_GetBox(item->box_num)->overlap_index & BOX_BLOCKED) == 0) {\n        return;\n    }\n\n    const XYZ_32 pos = {\n        .x = item->pos.x,\n        .y = item->pos.y - (Random_GetControl() & 0xFF) - 32,\n        .z = item->pos.z,\n    };\n    Spawn_BloodBath(\n        pos.x, pos.y, pos.z, (Random_GetControl() & 0x7F) + STEP_L / 2,\n        Random_GetControl() << 1, item->room_num, 3);\n\n    if (item->hit_points <= 0) {\n        return;\n    }\n\n    item->hit_points -= M_BOX_DAMAGE;\n    if (item->hit_points <= 0) {\n        Stats_AddKill();\n    }\n}\n\nvoid Creature_Die(const int16_t item_num, const bool explode)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    switch (item->object_id) {\n    case O_LIZARD:\n        TribeBoss_SetLizardActive(false);\n        break;\n\n    case O_DRAGON_FRONT:\n    case O_TORSO:\n        item->hit_points = 0;\n        return;\n\n    case O_SKIDOO_ARMED:\n        if (explode) {\n            Item_Explode(item_num, -1, 0);\n            ITEM *const vehicle_item = Item_Get(item_num);\n            vehicle_item->hit_points = 0;\n            vehicle_item->status = IS_INVISIBLE;\n            return;\n        }\n        break;\n\n    case O_SKIDOO_DRIVER:\n        if (explode) {\n            Item_Explode(item_num, -1, 0);\n        }\n        item->hit_points = 0;\n        const int16_t vehicle_item_num = SkidooDriver_GetSkidooItemNum(item);\n        if (vehicle_item_num == NO_ITEM) {\n            return;\n        }\n        ITEM *const vehicle_item = Item_Get(vehicle_item_num);\n        vehicle_item->hit_points = 0;\n        vehicle_item->status = IS_INVISIBLE;\n        return;\n\n    default:\n        break;\n    }\n\n    item->collidable = false;\n    item->hit_points = 0;\n    if (explode) {\n        Item_Explode(item_num, -1, 0);\n        Item_Kill(item_num);\n    } else {\n        Item_RemoveActive(item_num);\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->intelligent) {\n        LOT_DisableBaddieAI(item_num);\n    }\n    item->flags |= IF_ONE_SHOT;\n\n    Carrier_TestItemDrops(item_num);\n}\n\nint32_t Creature_Vault(\n    const int16_t item_num, const int16_t angle, int32_t vault,\n    const int32_t shift)\n{\n    ITEM *const item = Item_Get(item_num);\n    const int16_t room_num = item->room_num;\n    const XYZ_32 old = item->pos;\n\n    Creature_Animate(item_num, angle, 0);\n\n    if (item->floor > old.y + STEP_L * 7 / 2) {\n        vault = -4;\n    } else if (\n        item->floor > old.y + STEP_L * 5 / 2 && item->object_id == O_MONKEY) {\n        vault = -3;\n    } else if (\n        item->floor > old.y + STEP_L * 3 / 2 && item->object_id == O_MONKEY) {\n        vault = -2;\n    } else if (item->pos.y > old.y - STEP_L * 3 / 2) {\n        return 0;\n    } else if (item->pos.y > old.y - STEP_L * 5 / 2) {\n        vault = 2;\n    } else if (item->pos.y > old.y - STEP_L * 7 / 2) {\n        vault = 3;\n    } else {\n        vault = 4;\n    }\n\n    const int32_t old_x_sector = old.x >> WALL_SHIFT;\n    const int32_t old_z_sector = old.z >> WALL_SHIFT;\n    const int32_t x_sector = item->pos.x >> WALL_SHIFT;\n    const int32_t z_sector = item->pos.z >> WALL_SHIFT;\n    if (old_z_sector == z_sector) {\n        if (old_x_sector == x_sector) {\n            return 0;\n        }\n\n        if (old_x_sector >= x_sector) {\n            item->rot.y = -DEG_90;\n            item->pos.x = (old_x_sector * WALL_L) + shift;\n        } else {\n            item->rot.y = DEG_90;\n            item->pos.x = (x_sector * WALL_L) - shift;\n        }\n    } else if (old_x_sector == x_sector) {\n        if (old_z_sector >= z_sector) {\n            item->rot.y = -DEG_180;\n            item->pos.z = (old_z_sector * WALL_L) + shift;\n        } else {\n            item->rot.y = 0;\n            item->pos.z = (z_sector * WALL_L) - shift;\n        }\n    }\n\n    item->floor = old.y;\n    item->pos.y = old.y;\n\n    Item_UpdateRoom(item_num, room_num);\n    return vault;\n}\n\nint16_t Creature_Effect(\n    const ITEM *const item, const BITE *const bite,\n    int16_t (*const spawn)(\n        int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n        int16_t room_num))\n{\n    XYZ_32 pos = bite->pos;\n    Collide_GetJointAbsPosition(item, &pos, bite->mesh_num);\n    return spawn(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num);\n}\n\nint16_t Creature_AIGuard(CREATURE *const creature)\n{\n    if (Item_Get(creature->item_num)->ai_bits & AI_MODIFY) {\n        return 0;\n    }\n\n    const int32_t rnd = Random_GetControl();\n    if (rnd < 256) {\n        creature->head_left = true;\n        creature->head_right = true;\n    } else if (rnd < 384) {\n        creature->head_left = true;\n        creature->head_right = false;\n    } else if (rnd < 512) {\n        creature->head_left = false;\n        creature->head_right = true;\n    }\n\n    if (creature->head_left && creature->head_right) {\n        return 0;\n    }\n    if (creature->head_left) {\n        return -DEG_90;\n    }\n    if (creature->head_right) {\n        return DEG_90;\n    }\n    return 0;\n}\n\nstatic bool M_SameZone(const CREATURE *const creature, ITEM *const target_item)\n{\n    if (creature->lot.setup.fly != 0) {\n        return true;\n    }\n\n    int16_t *const zone = Box_GetGroundZone(\n        Room_GetFlipStatus(), (creature->lot.setup.step >> 8) - 1);\n    ITEM *const item = Item_Get(creature->item_num);\n\n    const ROOM *room = Room_Get(item->room_num);\n    item->box_num = Room_GetWorldSector(room, item->pos.x, item->pos.z)->box;\n\n    room = Room_Get(target_item->room_num);\n    target_item->box_num =\n        Room_GetWorldSector(room, target_item->pos.x, target_item->pos.z)->box;\n\n    return zone[item->box_num] == zone[target_item->box_num];\n}\n\nvoid Creature_GetAITarget(CREATURE *const creature)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    ITEM *const item = Item_Get(creature->item_num);\n    ITEM *const enemy = creature->enemy;\n    const OBJECT_ID enemy_object_id =\n        enemy != nullptr ? enemy->object_id : NO_OBJECT;\n\n    uint8_t ai_bits = item->ai_bits;\n\n    if (ai_bits & AI_GUARD) {\n        creature->enemy = lara_item;\n\n        if (creature->alerted) {\n            item->ai_bits &= ~AI_GUARD;\n\n            if (ai_bits & AI_AMBUSH) {\n                item->ai_bits |= AI_MODIFY;\n            }\n        }\n    } else if (ai_bits & AI_PATROL_1) {\n        if (creature->alerted || creature->hurt_by_lara) {\n            item->ai_bits &= ~AI_PATROL_1;\n\n            if (ai_bits & AI_AMBUSH) {\n                item->ai_bits |= AI_MODIFY;\n            }\n        } else if (!creature->patrol_2 && enemy_object_id != O_AI_PATROL_1) {\n            for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n                ITEM *const target = Item_Get(i);\n\n                if (target->object_id == O_AI_PATROL_1\n                    && target->room_num != NO_ROOM\n                    && M_SameZone(creature, target)\n                    && target->rot.y == item->ai_tag) {\n                    creature->enemy = target;\n                    return;\n                }\n            }\n        } else if (creature->patrol_2 && enemy_object_id != O_AI_PATROL_2) {\n            for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n                ITEM *const target = Item_Get(i);\n\n                if (target->object_id == O_AI_PATROL_2\n                    && target->room_num != NO_ROOM\n                    && M_SameZone(creature, target)\n                    && target->rot.y == item->ai_tag) {\n                    creature->enemy = target;\n                    return;\n                }\n            }\n        } else if (\n            ABS(enemy->pos.x - item->pos.x) < 768\n            && ABS(enemy->pos.y - item->pos.y) < 768\n            && ABS(enemy->pos.z - item->pos.z) < 768) {\n            Room_TestTriggers(enemy);\n            creature->patrol_2 = !creature->patrol_2;\n        }\n    } else if (ai_bits & AI_AMBUSH) {\n        if (ai_bits & AI_MODIFY || creature->hurt_by_lara) {\n            if (enemy_object_id != O_AI_AMBUSH) {\n                for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n                    ITEM *const target = Item_Get(i);\n\n                    if (target->object_id == O_AI_AMBUSH\n                        && target->room_num != NO_ROOM\n                        && M_SameZone(creature, target)\n                        && (target->rot.y == item->ai_tag\n                            || item->object_id == O_MONKEY)) {\n                        creature->enemy = target;\n                        return;\n                    }\n                }\n            } else if (item->object_id != O_MONKEY) {\n                if (ABS(enemy->pos.x - item->pos.x) < 768\n                    && ABS(enemy->pos.y - item->pos.y) < 768\n                    && ABS(enemy->pos.z - item->pos.z) < 768) {\n                    Room_TestTriggers(enemy);\n                    creature->reached_goal = 1;\n                    creature->enemy = lara_item;\n                    item->ai_bits &= ~(AI_AMBUSH | AI_MODIFY);\n                    item->ai_bits |= AI_GUARD;\n                    creature->alerted = false;\n                }\n            }\n        } else {\n            creature->enemy = lara_item;\n        }\n    } else if (ai_bits & AI_FOLLOW) {\n        if (creature->hurt_by_lara) {\n            creature->enemy = lara_item;\n            creature->alerted = true;\n            item->ai_bits &= ~AI_FOLLOW;\n        } else if (item->hit_status) {\n            item->ai_bits &= ~AI_FOLLOW;\n        } else if (enemy_object_id != O_AI_FOLLOW) {\n            for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n                ITEM *const target = Item_Get(i);\n\n                if (target->object_id == O_AI_FOLLOW\n                    && target->room_num != NO_ROOM\n                    && M_SameZone(creature, target)\n                    && target->rot.y == item->ai_tag) {\n                    creature->enemy = target;\n                    return;\n                }\n            }\n        } else if (\n            ABS(enemy->pos.x - item->pos.x) < 768\n            && ABS(enemy->pos.y - item->pos.y) < 768\n            && ABS(enemy->pos.z - item->pos.z) < 768) {\n            creature->reached_goal = 1;\n            item->ai_bits &= ~AI_FOLLOW;\n        }\n    } else if (item->object_id == O_MONKEY && item->carried_item == nullptr) {\n        if (creature->hurt_by_lara\n            && g_Config.gameplay.fix_monkey_pickup_priority) {\n            creature->enemy = lara_item;\n            return;\n        }\n        if (item->ai_bits == AI_MODIFY) {\n            if (enemy_object_id != O_KEY_ITEM_4) {\n                for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n                    ITEM *const target = Item_Get(i);\n\n                    if (target->object_id == O_KEY_ITEM_4\n                        && target->room_num != NO_ROOM && !target->ai_bits\n                        && target->status != IS_INVISIBLE && !target->clear_body\n                        && M_SameZone(creature, target)) {\n                        creature->enemy = target;\n                        return;\n                    }\n                }\n            }\n        } else if (enemy_object_id != O_SMALL_MEDIPACK_ITEM) {\n            for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n                ITEM *const target = Item_Get(i);\n                if (target->object_id == O_SMALL_MEDIPACK_ITEM\n                    && target->room_num != NO_ROOM && !target->ai_bits\n                    && target->status != IS_INVISIBLE && !target->clear_body\n                    && M_SameZone(creature, target)) {\n                    creature->enemy = target;\n                    return;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/creature/common.h",
    "content": "#pragma once\n\n#include <trx/game/collision.h>\n#include <trx/game/creature/types.h>\n\nvoid Creature_Initialise(int16_t item_num);\nbool Creature_Activate(int16_t item_num);\nvoid Creature_AIInfo(ITEM *item, AI_INFO *info);\nbool Creature_EnsureHabitat(\n    int16_t item_num, int32_t *wh, const HYBRID_INFO *info);\nvoid Creature_Mood(const ITEM *item, const AI_INFO *info, bool violent);\nvoid Creature_UpdateMood(const ITEM *item, const AI_INFO *info, bool violent);\nvoid Creature_ApplyMood(const ITEM *item, const AI_INFO *info, bool violent);\n\nint16_t Creature_Turn(ITEM *item, int16_t max_turn);\nvoid Creature_Tilt(ITEM *item, int16_t angle);\nvoid Creature_Head(ITEM *item, int16_t required);\nvoid Creature_Neck(ITEM *item, int16_t required);\nvoid Creature_Joint(ITEM *item, int16_t joint, int16_t required);\n\nvoid Creature_Float(int16_t item_num);\nvoid Creature_Underwater(ITEM *item, int32_t depth);\n\nbool Creature_CanSeeEnemy(const ITEM *item, const AI_INFO *info);\nbool Creature_CanTargetEnemy(const ITEM *item, const AI_INFO *info);\nvoid Creature_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\nbool Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt);\n\nvoid Creature_SpecialKill(\n    ITEM *item, int32_t kill_anim, int32_t kill_state, int32_t lara_kill_state);\nvoid Creature_TestBoxDamage(int16_t item_num);\nvoid Creature_Die(int16_t item_num, bool explode);\nint32_t Creature_Vault(\n    int16_t item_num, int16_t angle, int32_t vault, int32_t shift);\n\nvoid Creature_Reset(void);\nbool Creature_AreAlliesHostile(void);\nvoid Creature_SetAlliesHostile(bool enable);\nvoid Creature_Hurt(ITEM *item, int32_t damage);\nbool Creature_IsHostile(const ITEM *item);\nbool Creature_IsAlly(const ITEM *item);\nbool Creature_IsAllyTargetingEnemy(const ITEM *item);\nvoid Creature_AddAlly(OBJECT_ID obj_id);\nvoid Creature_AddAllyTargetingEnemy(OBJECT_ID obj_id);\n\nint16_t Creature_Effect(\n    const ITEM *item, const BITE *bite,\n    int16_t (*spawn)(\n        int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n        int16_t room_num));\n\nbool Creature_Shoot(\n    ITEM *item, const AI_INFO *info, const CREATURE_GUN *gun,\n    int16_t extra_rotation, int32_t damage);\n\nint16_t Creature_AIGuard(CREATURE *creature);\nvoid Creature_GetAITarget(CREATURE *creature);\n"
  },
  {
    "path": "src/trx/game/creature/const.h",
    "content": "#pragma once\n\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n\n#define FRONT_ARC DEG_90\n#define UNIT_SHADOW 256\n\n#define CREATURE_STALK_DIST (3 * WALL_L) // = 3072\n#define CREATURE_ESCAPE_DIST (5 * WALL_L) // = 5120\n#define CREATURE_TARGET_DIST (4 * WALL_L) // = 4096\n\n#define CREATURE_MISS_CHANCE 0x2000\n\n#define CREATURE_SHOOT_RANGE SQUARE((g_TRVersion == 1 ? 7 : 8) * WALL_L)\n// = 51380224 (TR1), 67108864 (TR2)\n"
  },
  {
    "path": "src/trx/game/creature/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    MOOD_BORED = 0,\n    MOOD_ATTACK = 1,\n    MOOD_ESCAPE = 2,\n    MOOD_STALK = 3,\n} MOOD_TYPE;\n"
  },
  {
    "path": "src/trx/game/creature/shooting.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/gun_flash.h>\n#include <trx/game/gun.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks/spawners.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_SHOOT_TARGETING_SPEED 300\n#define M_SHOOT_HIT_CHANCE 0x2000\n\nstatic void M_CalcShootVectors(\n    const ITEM *const item, const ITEM *const target_item, XYZ_32 *const start,\n    XYZ_32 *const target)\n{\n    start->x = item->pos.x;\n    start->y = item->pos.y - STEP_L * 3;\n    start->z = item->pos.z;\n\n    target->x = target_item->pos.x;\n    target->y = target_item->pos.y - STEP_L * 3;\n    target->z = target_item->pos.z;\n\n    const int16_t angle = XYZ_32_GetYaw((XYZ_32) {\n        .x = target->x - start->x,\n        .y = target->y - start->y,\n        .z = target->z - start->z,\n    });\n\n    const int32_t dist = WALL_L * 2;\n    target->x += (dist * Math_Sin(angle)) >> W2V_SHIFT;\n    target->z += (dist * Math_Cos(angle)) >> W2V_SHIFT;\n}\n\nstatic void M_TriggerTR3GunShell(\n    const ITEM *const item, const CREATURE_GUN *const gun)\n{\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    XYZ_32 pos = {\n        .x = gun->muzzle.pos.x >> 2,\n        .y = gun->muzzle.pos.y >> 2,\n        .z = gun->muzzle.pos.z >> 2,\n    };\n    Collide_GetJointAbsPosition(item, &pos, gun->muzzle.mesh_num);\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = pos;\n    effect->room_num = item->room_num;\n    effect->rot.x = 0;\n    effect->rot.y = 0;\n    effect->rot.z = (int16_t)Random_GetControl();\n    effect->speed = (int16_t)((Random_GetControl() & 0x1F) + 16);\n    effect->object_id = O_GUN_SHELL;\n    effect->fall_speed = (int16_t)(-48 - (Random_GetControl() & 7));\n    effect->frame_num = Object_Get(O_GUN_SHELL)->mesh_idx;\n    effect->shade = 0x4210;\n    effect->counter = 1;\n    effect->flag1 = item->rot.y + (Random_GetControl() & 0xFFF) - 0x4800;\n}\n\nstatic void M_TriggerTR3GunSmoke(\n    const ITEM *const item, const CREATURE_GUN *const gun)\n{\n    XYZ_32 pos = {\n        .x = gun->muzzle.pos.x - (gun->muzzle.pos.x >> 2),\n        .y = gun->muzzle.pos.y - (gun->muzzle.pos.y >> 2),\n        .z = gun->muzzle.pos.z - (gun->muzzle.pos.z >> 2),\n    };\n    Collide_GetJointAbsPosition(item, &pos, gun->muzzle.mesh_num);\n\n    GAME_VECTOR smoke_pos = {\n        .pos = pos,\n        .room_num = item->room_num,\n    };\n    Room_GetSector(smoke_pos.pos, &smoke_pos.room_num);\n    Sparks_TriggerGunSmoke(smoke_pos, true, LGT_PISTOLS, 32);\n}\n\nbool Creature_Shoot(\n    ITEM *const item, const AI_INFO *const info, const CREATURE_GUN *const gun,\n    const int16_t extra_rotation, const int32_t damage)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const CREATURE *const creature = item->creature_data;\n    ITEM *const target_item = creature->enemy;\n\n    if (g_TRVersion == 3) {\n        M_TriggerTR3GunShell(item, gun);\n        M_TriggerTR3GunSmoke(item, gun);\n    }\n\n    bool is_targetable;\n    bool is_hit;\n    if (g_TRVersion == 1) {\n        // TR1 targeting is a bit dumb - eg with some minimal effort, Pierre\n        // can't reach Lara in Folly. This branch preserves this behavior.\n        if (info->distance > CREATURE_SHOOT_RANGE) {\n            is_targetable = false;\n            is_hit = false;\n        } else {\n            is_hit = Random_GetControl()\n                < ((CREATURE_SHOOT_RANGE - info->distance)\n                       / (CREATURE_SHOOT_RANGE / 0x7FFF)\n                   - CREATURE_MISS_CHANCE);\n            is_targetable = true;\n        }\n    } else {\n        if (info->distance > CREATURE_SHOOT_RANGE\n            || !Creature_CanTargetEnemy(item, info)) {\n            is_targetable = false;\n            is_hit = false;\n        } else {\n            int32_t distance =\n                (((target_item->speed * Math_Sin(info->enemy_facing))\n                  >> W2V_SHIFT)\n                 * CREATURE_SHOOT_RANGE)\n                / M_SHOOT_TARGETING_SPEED;\n            distance = info->distance + SQUARE(distance);\n            if (distance > CREATURE_SHOOT_RANGE) {\n                is_hit = false;\n            } else {\n                const int32_t chance = M_SHOOT_HIT_CHANCE\n                    + (CREATURE_SHOOT_RANGE - info->distance)\n                        / (CREATURE_SHOOT_RANGE / 0x5000);\n                is_hit = Random_GetControl() < chance;\n            }\n            is_targetable = true;\n        }\n    }\n\n    int16_t effect_num = NO_EFFECT;\n    if (target_item == lara_item) {\n        if (is_hit) {\n            effect_num = Creature_Effect(item, &gun->muzzle, Spawn_GunHit);\n            Item_TakeDamage(target_item, damage, true);\n        } else if (is_targetable) {\n            effect_num = Creature_Effect(item, &gun->muzzle, Spawn_GunMiss);\n        }\n    } else {\n        effect_num = Creature_Effect(item, &gun->muzzle, Spawn_GunShot);\n        if (is_hit) {\n            Item_TakeDamage(target_item, damage / 10, true);\n\n            const OBJECT *const target_obj = Object_Get(target_item->object_id);\n            int32_t joint = Random_GetControl() & 0xF;\n            if (joint >= target_obj->mesh_count) {\n                joint = 0;\n            }\n\n            XYZ_32 pos = {};\n            Collide_GetJointAbsPosition(target_item, &pos, joint);\n            Spawn_Blood(\n                pos.x, pos.y, pos.z, target_item->speed, target_item->rot.y,\n                target_item->room_num);\n        }\n    }\n\n    if (FX_GunFlash_Spawn(item, gun) && effect_num != NO_EFFECT) {\n        // Kill the old-style flash effect just spawned from previous chunk\n        Effect_Kill(effect_num);\n        effect_num = NO_EFFECT;\n    }\n\n    if (effect_num != NO_EFFECT) {\n        Effect_Get(effect_num)->rot.y += extra_rotation;\n    }\n\n    XYZ_32 start, target;\n    M_CalcShootVectors(item, target_item, &start, &target);\n    Gun_SmashItems(\n        (GAME_VECTOR) {\n            .pos = start,\n            .room_num = item->room_num,\n        },\n        (GAME_VECTOR) {\n            .pos = target,\n            .room_num = target_item->room_num,\n        },\n        nullptr, NO_OBJECT);\n\n    return is_targetable;\n}\n"
  },
  {
    "path": "src/trx/game/creature/types.h",
    "content": "#pragma once\n\n#include <trx/game/creature/enum.h>\n#include <trx/game/items.h>\n#include <trx/game/pathing/types.h>\n\ntypedef struct CREATURE {\n    union {\n        // NOTE: creature extra rotations are provided via this array.\n        // Intelligent objects wire item->extra_rotations to joint_rotation.\n        int16_t joint_rotation[4];\n        struct {\n            // These are old TR1-2 aliases.\n            int16_t head_rotation;\n            int16_t neck_rotation;\n            int16_t _extra_rotation[2];\n        };\n    };\n    int16_t maximum_turn;\n    int16_t flags;\n    bool alerted;\n    bool head_left;\n    bool head_right;\n    bool reached_goal;\n    bool hurt_by_lara;\n    int32_t damage_from_lara;\n    bool patrol_2;\n    int16_t item_num;\n    MOOD_TYPE mood;\n    LOT_INFO lot;\n    XYZ_32 target;\n    ITEM *enemy;\n} CREATURE;\n\ntypedef struct {\n    int16_t zone_num;\n    int16_t enemy_zone_num;\n    int32_t distance;\n    bool ahead;\n    bool bite;\n    int16_t angle;\n    int16_t x_angle;\n    int16_t enemy_facing;\n} AI_INFO;\n\ntypedef struct {\n    XYZ_32 pos;\n    int32_t mesh_num;\n} BITE;\n\ntypedef struct {\n    BITE muzzle;\n    bool tr3_enemy_flash;\n    BITE tr3_flash;\n    int16_t tr3_enemy_weapon_flags;\n    int16_t tr3_flash_shade;\n    int16_t tr3_flash_rot_x;\n    struct {\n        BITE bite;\n        RGBA_8888 color;\n        float width;\n    } tr3_laser;\n} CREATURE_GUN;\n\ntypedef struct {\n    struct {\n        OBJECT_ID id;\n        int16_t active_anim;\n        int16_t death_anim;\n        int16_t death_state;\n    } land, water;\n} HYBRID_INFO;\n"
  },
  {
    "path": "src/trx/game/creature.h",
    "content": "#pragma once\n\n#include <trx/game/creature/alert.h>\n#include <trx/game/creature/common.h>\n#include <trx/game/creature/const.h>\n#include <trx/game/creature/enum.h>\n#include <trx/game/creature/types.h>\n"
  },
  {
    "path": "src/trx/game/cutscene.c",
    "content": "#include <trx/game/cutscene.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/smoke.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/lara.h>\n#include <trx/game/lua.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/shell.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\nstatic CAMERA_INFO m_LocalCamera = {};\nstatic OBJECT_MESH **m_CapturedObjectMeshes = nullptr;\nstatic OBJECT_ID *m_CapturedObjectMeshOwners = nullptr;\nstatic int32_t m_CapturedObjectMeshCount = 0;\nstatic bool m_DrawLeftGunFlash = false;\nstatic bool m_DrawRightGunFlash = false;\n\ntypedef struct {\n    bool is_valid;\n    LARA_SKIN_TYPE skin_type;\n    LARA_GUN_TYPE hand_l_type;\n    LARA_GUN_TYPE hand_r_type;\n    LARA_GUN_TYPE thigh_l_type;\n    LARA_GUN_TYPE thigh_r_type;\n    bool holsters_visible;\n} M_LARA_CUTSCENE_STATE;\n\nstatic M_LARA_CUTSCENE_STATE m_LaraCutsceneState = {};\n\nstatic LARA_GUN_TYPE M_GetGunEquipmentType(const LARA_MESH mesh)\n{\n    const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(mesh);\n    if (equipment->type == EQUIPMENT_TYPE_WEAPON) {\n        return (LARA_GUN_TYPE)equipment->data;\n    }\n    return LGT_UNARMED;\n}\n\nstatic void M_CaptureLaraCutsceneState(void)\n{\n    m_LaraCutsceneState.is_valid = true;\n    m_LaraCutsceneState.skin_type = Lara_Skin_GetType();\n    m_LaraCutsceneState.hand_l_type = M_GetGunEquipmentType(LM_HAND_L);\n    m_LaraCutsceneState.hand_r_type = M_GetGunEquipmentType(LM_HAND_R);\n    m_LaraCutsceneState.thigh_l_type = M_GetGunEquipmentType(LM_THIGH_L);\n    m_LaraCutsceneState.thigh_r_type = M_GetGunEquipmentType(LM_THIGH_R);\n    m_LaraCutsceneState.holsters_visible = Lara_Skin_AreHolstersVisible();\n}\n\nstatic void M_CaptureObjectMeshesState(void)\n{\n    Memory_FreePointer(&m_CapturedObjectMeshes);\n    Memory_FreePointer(&m_CapturedObjectMeshOwners);\n    m_CapturedObjectMeshCount = Object_GetMeshCount();\n    if (m_CapturedObjectMeshCount <= 0) {\n        return;\n    }\n\n    m_CapturedObjectMeshes = Memory_Alloc(\n        m_CapturedObjectMeshCount * sizeof(*m_CapturedObjectMeshes));\n    m_CapturedObjectMeshOwners = Memory_Alloc(\n        m_CapturedObjectMeshCount * sizeof(*m_CapturedObjectMeshOwners));\n    for (int32_t i = 0; i < m_CapturedObjectMeshCount; i++) {\n        m_CapturedObjectMeshes[i] = Object_GetMesh(i);\n        m_CapturedObjectMeshOwners[i] = NO_OBJECT;\n    }\n\n    for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) {\n        const OBJECT *const obj = Object_Get(obj_id);\n        if (!obj->loaded || obj->mesh_count <= 0 || obj->mesh_idx < 0) {\n            continue;\n        }\n\n        for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) {\n            const int32_t abs_idx = obj->mesh_idx + mesh_idx;\n            if (abs_idx >= 0 && abs_idx < m_CapturedObjectMeshCount) {\n                m_CapturedObjectMeshOwners[abs_idx] = obj_id;\n            }\n        }\n    }\n}\n\nstatic void M_RestoreObjectMeshesState(void)\n{\n    if (m_CapturedObjectMeshes == nullptr || m_CapturedObjectMeshCount <= 0) {\n        return;\n    }\n\n    const int32_t mesh_count = Object_GetMeshCount();\n    if (mesh_count != m_CapturedObjectMeshCount) {\n        return;\n    }\n\n    for (int32_t i = 0; i < mesh_count; i++) {\n        if (Object_GetMesh(i) == m_CapturedObjectMeshes[i]) {\n            continue;\n        }\n\n        int32_t j = -1;\n        for (int32_t k = i + 1; k < mesh_count; k++) {\n            if (Object_GetMesh(k) == m_CapturedObjectMeshes[i]) {\n                j = k;\n                break;\n            }\n        }\n        if (j < 0) {\n            continue;\n        }\n\n        if (m_CapturedObjectMeshOwners[i] == NO_OBJECT\n            || m_CapturedObjectMeshOwners[j] == NO_OBJECT) {\n            continue;\n        }\n\n        const OBJECT *const obj_1 = Object_Get(m_CapturedObjectMeshOwners[i]);\n        const OBJECT *const obj_2 = Object_Get(m_CapturedObjectMeshOwners[j]);\n        Object_SwapMeshEx(\n            m_CapturedObjectMeshOwners[i], m_CapturedObjectMeshOwners[j],\n            i - obj_1->mesh_idx, j - obj_2->mesh_idx);\n    }\n}\n\nstatic void M_RestoreLaraCutsceneState(void)\n{\n    if (!m_LaraCutsceneState.is_valid) {\n        return;\n    }\n\n    Lara_Skin_SetType(m_LaraCutsceneState.skin_type);\n    Lara_Skin_SetGunEquipment(LM_HAND_L, m_LaraCutsceneState.hand_l_type);\n    Lara_Skin_SetGunEquipment(LM_HAND_R, m_LaraCutsceneState.hand_r_type);\n    Lara_Skin_SetGunEquipment(LM_THIGH_L, m_LaraCutsceneState.thigh_l_type);\n    Lara_Skin_SetGunEquipment(LM_THIGH_R, m_LaraCutsceneState.thigh_r_type);\n    Lara_Skin_SetHolstersVisible(m_LaraCutsceneState.holsters_visible);\n}\n\nstatic bool M_IsCutsceneActor(const ITEM *const item)\n{\n    return (item->object_id >= O_PLAYER_1 && item->object_id <= O_PLAYER_10)\n        || item->object_id == O_LARA;\n}\n\nstatic void M_ResetActorAnimation(ITEM *const item)\n{\n    Item_SwitchToAnim(item, 0, 0);\n    item->prev_frame_num = item->frame_num;\n    item->current_anim_state = Item_GetAnim(item)->current_anim_state;\n    item->goal_anim_state = item->current_anim_state;\n    item->required_anim_state = 0;\n}\n\nstatic void M_ResetActorsToStart(void)\n{\n    M_RestoreObjectMeshesState();\n    M_RestoreLaraCutsceneState();\n\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (M_IsCutsceneActor(item)) {\n            M_ResetActorAnimation(item);\n        }\n    }\n}\n\nstatic void M_ControlGun(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    m_DrawLeftGunFlash = false;\n    m_DrawRightGunFlash = false;\n    if (lara->left_arm.flash_gun > 0) {\n        m_DrawLeftGunFlash = true;\n        lara->left_arm.flash_gun--;\n    }\n    if (lara->right_arm.flash_gun > 0) {\n        m_DrawRightGunFlash = true;\n        lara->right_arm.flash_gun--;\n    }\n\n    Gun_Smoke_Control();\n\n    if (g_Config.visuals.enable_gun_lighting\n        && (m_DrawLeftGunFlash || m_DrawRightGunFlash)) {\n        XYZ_32 pos = { .x = -12, .y = 48, .z = 40 };\n        LARA_MESH mesh = LM_HAND_L;\n        if (m_DrawRightGunFlash) {\n            pos.x = 8;\n            mesh = LM_HAND_R;\n        }\n\n        Collide_GetJointAbsPosition(lara_item, &pos, mesh);\n        pos.x += (Random_GetControl() & 0xFF) - 128;\n        pos.y -= (Random_GetControl() & 0x7F) - 63;\n        pos.z += (Random_GetControl() & 0xFF) - 128;\n        if (g_TRVersion >= 3) {\n            const RGB_888 color = {\n                .r = 192 + (Random_GetControl() & 0x3F),\n                .g = 144 + (Random_GetControl() & 0x1F),\n                .b = Random_GetControl() & 0x3F,\n            };\n            Output_AddDynamicLightRGB(pos, 10, color);\n        } else {\n            Output_AddDynamicLight(pos, 10, 11);\n        }\n    }\n}\n\nstatic void M_DrawGunFlash(const LARA_MESH hand_mesh)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    XYZ_32 pos = {};\n    const bool has_mesh_pos = Lara_GetMeshPos(hand_mesh, &pos);\n    if (!has_mesh_pos) {\n        const ITEM *const lara_item = Lara_GetItem();\n        Collide_GetJointAbsPosition(lara_item, &pos, hand_mesh);\n    }\n\n    Matrix_Push();\n    *g_MatrixPtr = g_ViewMatrix;\n    *g_WMatrixPtr = g_IDMatrix;\n    Matrix_TranslateAbs32(pos);\n    if (has_mesh_pos && lara->mesh_pos_matrices_valid) {\n        MATRIX hand_rot = lara->mesh_pos_matrices[hand_mesh];\n        hand_rot._03 = 0;\n        hand_rot._13 = 0;\n        hand_rot._23 = 0;\n        Matrix_Mul3x3(&hand_rot);\n    }\n    Gun_DrawFlash(LGT_PISTOLS, CLIP_FULLY_VISIBLE, false);\n    Matrix_Pop();\n}\n\nstatic void M_Control(void)\n{\n    Output_ResetDynamicLights();\n    Camera_UpdateCutscene();\n    M_ControlGun();\n    Item_Control();\n    Effect_Control();\n    Sparks_Control();\n    FX_Control();\n    Output_AnimateTextures(1);\n    Lara_Hair_Control(true);\n}\n\nstatic void M_ReplayActors(\n    CINE_DATA *const cine_data, const int32_t start_frame,\n    const int32_t end_frame)\n{\n    for (int32_t frame_idx = start_frame; frame_idx < end_frame; frame_idx++) {\n        Lua_FireEventInt32(LUA_EVENT_BEFORE_CONTROL, 0);\n        cine_data->frame_idx = frame_idx;\n        M_Control();\n        Lua_FireEventInt32(LUA_EVENT_AFTER_CONTROL, 0);\n    }\n}\n\nstatic void M_PlayerControl(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    CAMERA_INFO *const camera = Cutscene_GetCamera();\n    item->rot.y = camera->target_angle;\n    item->pos = camera->pos.pos;\n\n    XYZ_32 pos = {};\n    Collide_GetJointAbsPosition(item, &pos, 0);\n\n    int16_t room_num = Room_GetIndexFromPos(pos);\n    if (room_num != NO_ROOM) {\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    int16_t floor_room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &floor_room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    item->floor = height == NO_HEIGHT ? pos.y : height;\n\n    Lara_Animate(item);\n}\n\nstatic void M_InitialisePlayer(const int16_t item_num)\n{\n    OBJECT *const obj = Object_Get(O_LARA);\n    obj->draw_func = Lara_Draw;\n    obj->control_func = M_PlayerControl;\n    obj->shadow_size = (UNIT_SHADOW * 10) / 16;\n\n    Item_AddActive(item_num);\n    ITEM *const item = Item_Get(item_num);\n    CAMERA_INFO *const camera = Cutscene_GetCamera();\n    Camera_GetCineData()->position.target_angle = item->rot.y;\n    g_Camera.pos.room_num = item->room_num;\n    g_Camera.target_angle = item->rot.y;\n\n    CINE_DATA *const cine_data = Camera_GetCineData();\n    cine_data->position.pos = item->pos;\n\n    camera->pos.pos = item->pos;\n    if (item->room_num != NO_ROOM) {\n        camera->pos.room_num = item->room_num;\n    }\n    camera->target_angle = item->rot.y;\n\n    item->rot.y = 0;\n    item->dynamic_light = false;\n    Item_SwitchToAnim(item, 0, 0);\n    item->goal_anim_state = 0;\n    item->current_anim_state = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->hit_direction = DIR_UNKNOWN;\n}\n\nstatic void M_Skip(const int32_t frames)\n{\n    CINE_DATA *const cine_data = Camera_GetCineData();\n    const int32_t source_frame = cine_data->frame_idx;\n    int32_t target_frame = source_frame + frames;\n    CLAMP(target_frame, 0, cine_data->frame_count - 1);\n\n    if (target_frame == source_frame) {\n        return;\n    }\n\n    if (target_frame > source_frame) {\n        M_ReplayActors(cine_data, source_frame, target_frame);\n    } else {\n        Lua_ReloadLevelScript();\n        M_ResetActorsToStart();\n        M_ReplayActors(cine_data, 0, target_frame);\n    }\n\n    cine_data->frame_idx = target_frame;\n    Camera_UpdateCutscene();\n}\n\nbool Cutscene_Start(const int32_t level_num)\n{\n    const GF_LEVEL *const level = GF_GetLevel(GFLT_CUTSCENES, level_num);\n    ASSERT(GF_GetCurrentLevel() == level);\n\n    m_DrawLeftGunFlash = false;\n    m_DrawRightGunFlash = false;\n    M_InitialisePlayer(Item_GetIndex(Lara_GetItem()));\n    M_CaptureLaraCutsceneState();\n    M_CaptureObjectMeshesState();\n    Camera_GetCineData()->frame_idx = 0;\n\n    if (level->music_track != MX_INACTIVE) {\n        Music_Play_Direct(level->music_track, MPM_ONCE);\n    }\n\n    return true;\n}\n\nvoid Cutscene_End(void)\n{\n    m_DrawLeftGunFlash = false;\n    m_DrawRightGunFlash = false;\n    Memory_FreePointer(&m_CapturedObjectMeshes);\n    Memory_FreePointer(&m_CapturedObjectMeshOwners);\n    m_CapturedObjectMeshCount = 0;\n    Music_Stop();\n}\n\nGF_COMMAND Cutscene_Control(void)\n{\n    Interpolation_Remember();\n    Music_SyncTimestamp(Camera_GetCineData()->frame_idx / (double)LOGIC_FPS);\n\n    Input_Update();\n    Shell_ProcessInput();\n    if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n        return (GF_COMMAND) { .action = GF_LEVEL_COMPLETE };\n    } else if (g_InputDB.pause) {\n        const GF_COMMAND gf_cmd = GF_PauseGame();\n        if (gf_cmd.action != GF_NOOP) {\n            return gf_cmd;\n        }\n    } else if (g_InputDB.toggle_photo_mode) {\n        const GF_COMMAND gf_cmd = GF_EnterPhotoMode();\n        if (gf_cmd.action != GF_NOOP) {\n            return gf_cmd;\n        }\n    } else if (g_InputDB.menu_right || g_InputDB.menu_left) {\n        const int32_t dir = g_InputDB.menu_right ? 1 : -1;\n        const int32_t speed = g_Input.draw ? 15 : (g_Input.slow ? 1 : 5);\n        M_Skip(dir * LOGIC_FPS * speed);\n    }\n\n    M_Control();\n\n    CINE_DATA *const cine_data = Camera_GetCineData();\n    cine_data->frame_idx++;\n    if (cine_data->frame_idx >= cine_data->frame_count) {\n        // Remember the scene after the update to prevent the interpolation\n        // from twitching the camera back and forth.\n        Interpolation_Remember();\n\n        return (GF_COMMAND) { .action = GF_LEVEL_COMPLETE };\n    }\n\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nvoid Cutscene_Draw(void)\n{\n    Interpolation_Interpolate();\n    Camera_Apply();\n    Room_DrawAllRooms(g_Camera.interp.room_num, g_Camera.target.room_num);\n    if (m_DrawLeftGunFlash) {\n        M_DrawGunFlash(LM_HAND_L);\n    }\n    if (m_DrawRightGunFlash) {\n        M_DrawGunFlash(LM_HAND_R);\n    }\n    SceneCompositor_Flush();\n    if (g_Config.visuals.enable_reflections) {\n        Output_Textures_UpdateEnvironmentMap();\n    }\n}\n\nCAMERA_INFO *Cutscene_GetCamera(void)\n{\n    return &m_LocalCamera;\n}\n"
  },
  {
    "path": "src/trx/game/cutscene.h",
    "content": "#pragma once\n\n#include <trx/game/camera/types.h>\n#include <trx/game/game_flow/types.h>\n\nbool Cutscene_Start(int32_t level_num);\nvoid Cutscene_End(void);\nGF_COMMAND Cutscene_Control(void);\nvoid Cutscene_Draw(void);\n\nCAMERA_INFO *Cutscene_GetCamera(void);\n"
  },
  {
    "path": "src/trx/game/demo.c",
    "content": "#include <trx/game/demo.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/overlay.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\n#define L_MODIFY_CONFIG()                                                      \\\n    X_PROCESS_CONFIG(gameplay.disable_healing_between_levels, false);          \\\n    X_PROCESS_CONFIG(gameplay.enable_target_change, false);                    \\\n    X_PROCESS_CONFIG(gameplay.enable_tr2_jumping, g_TRVersion >= 2);           \\\n    X_PROCESS_CONFIG(gameplay.enable_tr2_swim_cancel, g_TRVersion >= 2);       \\\n    X_PROCESS_CONFIG(gameplay.enable_tr2_swimming, g_TRVersion >= 2);          \\\n    X_PROCESS_CONFIG(gameplay.enable_wading, g_TRVersion >= 2);                \\\n    X_PROCESS_CONFIG(gameplay.enable_walk_to_items, false);                    \\\n    X_PROCESS_CONFIG(gameplay.fix_bear_ai, false);                             \\\n    X_PROCESS_CONFIG(gameplay.harpoon_recoil, 4);                              \\\n    X_PROCESS_CONFIG(                                                          \\\n        gameplay.look_mode,                                                    \\\n        g_TRVersion >= 2 ? LOOK_MODE_ENHANCED : LOOK_MODE_RESTRICTED);         \\\n    X_PROCESS_CONFIG(gameplay.start_lara_hitpoints, LARA_MAX_HITPOINTS);       \\\n    X_PROCESS_CONFIG(gameplay.target_mode, TARGET_LOCK_MODE_FULL);             \\\n    X_PROCESS_CONFIG(                                                          \\\n        gameplay.wall_glitch_mode,                                             \\\n        g_TRVersion >= 2 ? WALL_GLITCH_TR2 : WALL_GLITCH_TR1);                 \\\n    X_PROCESS_CONFIG(input.quick_guns_mode, QUICK_GUNS_MODE_DRAW_ONLY);        \\\n    X_PROCESS_CONFIG(visuals.enable_fire_lighting, false);                     \\\n    X_PROCESS_CONFIG(debug.enable_invulnerability, false);\n\ntypedef struct {\n    const uint32_t *demo_ptr;\n    const GF_LEVEL *level;\n    struct {\n        CONFIG config;\n        GAME_BONUS_FLAG bonus_flag;\n    } old_config;\n    uint32_t *data;\n} M_PRIV;\n\nstatic int32_t m_LastDemoNum = 0;\nstatic M_PRIV m_Priv;\n\nstatic void M_PrepareConfig(M_PRIV *const p)\n{\n    // Changing certains settings affects negatively the original game demo\n    // data, so temporarily turn off all relevant enhancements.\n    p->old_config.config = g_Config;\n    p->old_config.bonus_flag = Game_GetBonusFlag();\n    Game_SetBonusFlag(GBF_NONE);\n#define X_PROCESS_CONFIG(var, value) g_Config.var = value;\n    L_MODIFY_CONFIG();\n#undef X_PROCESS_CONFIG\n}\n\nstatic void M_RestoreConfig(M_PRIV *const p)\n{\n    Game_SetBonusFlag(p->old_config.bonus_flag);\n#define X_PROCESS_CONFIG(var, value) g_Config.var = p->old_config.config.var;\n    L_MODIFY_CONFIG();\n#undef X_PROCESS_CONFIG\n}\n\nvoid Demo_LoadData(VFILE *const file, const size_t size)\n{\n    M_PRIV *const p = &m_Priv;\n    if (size == 0) {\n        p->data = nullptr;\n    } else {\n        p->data =\n            GameBuf_Alloc((size + 1) * sizeof(uint32_t), GBUF_DEMO_BUFFER);\n        p->data[size] = -1;\n        VFile_Read(file, p->data, size);\n    }\n}\n\nbool Demo_UpdateInput(void)\n{\n    M_PRIV *const p = &m_Priv;\n    const INPUT_STATE old_demo_input = g_Input;\n\n    union {\n        uint32_t any;\n        struct {\n            // clang-format off\n            uint32_t forward:      1;\n            uint32_t back:         1;\n            uint32_t left:         1;\n            uint32_t right:        1;\n            uint32_t jump:         1;\n            uint32_t draw:         1;\n            uint32_t action:       1;\n            uint32_t slow:         1;\n            uint32_t option:       1;\n            uint32_t look:         1;\n            uint32_t step_left:    1;\n            uint32_t step_right:   1;\n            uint32_t roll:         1;\n            uint32_t _pad:         6;\n            uint32_t use_flare:    1;\n            uint32_t menu_confirm: 1;\n            uint32_t menu_back:    1;\n            uint32_t save:         1;\n            uint32_t load:         1;\n            // clang-format on\n        };\n    } demo_input = { .any = *p->demo_ptr };\n\n    if ((int32_t)demo_input.any == -1) {\n        return false;\n    }\n\n    // Translate demo inputs (that use hardcoded OG key layout) to TRX inputs.\n    g_Input = (INPUT_STATE) {\n        // clang-format off\n        .forward      = demo_input.forward,\n        .back         = demo_input.back,\n        .left         = demo_input.left,\n        .right        = demo_input.right,\n        .jump         = demo_input.jump,\n        .draw         = demo_input.draw,\n        .action       = demo_input.action,\n        .slow         = demo_input.slow,\n        .option       = demo_input.option,\n        .look         = demo_input.look,\n        .step_left    = demo_input.step_left,\n        .step_right   = demo_input.step_right,\n        .roll         = demo_input.roll,\n        .use_flare    = demo_input.use_flare,\n        .menu_confirm = demo_input.menu_confirm,\n        .menu_back    = demo_input.menu_back,\n        .save         = demo_input.save,\n        .load         = demo_input.load,\n        // clang-format on\n    };\n\n    g_InputDB = g_Input;\n    for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) {\n        g_InputDB.any[i] &= ~old_demo_input.any[i];\n    }\n\n    p->demo_ptr++;\n    return true;\n}\n\nbool Demo_Start(const int32_t level_num)\n{\n    M_PRIV *const p = &m_Priv;\n    p->level = GF_GetLevel(GFLT_DEMOS, level_num);\n    ASSERT(p->level != nullptr);\n    ASSERT(GF_GetCurrentLevel() == p->level);\n\n    M_PrepareConfig(p);\n    Interpolation_Remember();\n\n    // Remember old inputs in case the demo was forcefully started with some\n    // keys pressed. In that case, it should only be stopped if the user\n    // presses some other key.\n    Input_Update();\n\n    if (p->data == nullptr) {\n        LOG_ERROR(\"Level '%s' has no demo data\", p->level->path);\n        return false;\n    }\n\n    if (p->level->music_track != MX_INACTIVE) {\n        Music_Play_Direct(p->level->music_track, MPM_LOOP);\n    }\n\n    p->demo_ptr = p->data;\n\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara_item->pos.x = *p->demo_ptr++;\n    lara_item->pos.y = *p->demo_ptr++;\n    lara_item->pos.z = *p->demo_ptr++;\n    lara_item->rot.x = *p->demo_ptr++;\n    lara_item->rot.y = *p->demo_ptr++;\n    lara_item->rot.z = *p->demo_ptr++;\n\n    int16_t room_num = *p->demo_ptr++;\n    Item_UpdateRoom(lara->item_num, room_num);\n    const SECTOR *const sector = Room_GetSector(lara_item->pos, &room_num);\n    lara_item->floor = Room_GetHeight(sector, lara_item->pos);\n\n    if (g_TRVersion >= 2) {\n        lara->last_gun_type = *p->demo_ptr++;\n        Lara_Cheat_GetStuff();\n    } else {\n        lara->last_gun_type = LGT_PISTOLS;\n    }\n\n    if (Gun_IsRifleType(lara->last_gun_type)) {\n        Gun_SetLaraBackMesh(lara->last_gun_type);\n    } else if (\n        lara->last_gun_type != LGT_UNARMED\n        && lara->last_gun_type != LGT_FLARE) {\n        Gun_SetLaraHolsterLMesh(lara->last_gun_type);\n        Gun_SetLaraHolsterRMesh(lara->last_gun_type);\n    }\n\n    Camera_Initialise();\n    Random_SeedDraw(0xD371F947);\n    Random_SeedControl(0xD371F947);\n    g_OverlayFlag = 1;\n\n    Overlay_SetBottomText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_GS_KEY,\n        .gs_key = GS_ID(\"general/misc/demo_mode\"),\n        .flash_enabled = true,\n    });\n    return true;\n}\n\nvoid Demo_End(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_RestoreConfig(p);\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n    Music_Stop();\n}\n\nvoid Demo_Pause(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_RestoreConfig(p);\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n}\n\nvoid Demo_Unpause(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_PrepareConfig(p);\n    Overlay_SetBottomText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_GS_KEY,\n        .gs_key = GS_ID(\"general/misc/demo_mode\"),\n        .flash_enabled = true,\n    });\n}\n\nint32_t Demo_ChooseLevel(const int32_t demo_num)\n{\n    M_PRIV *const p = &m_Priv;\n    const int32_t demo_count = GF_GetLevelTable(GFLT_DEMOS)->count;\n    if (demo_count <= 0) {\n        return -1;\n    } else if (demo_num < 0 || demo_num >= demo_count) {\n        return (m_LastDemoNum++) % demo_count;\n    } else {\n        return demo_num;\n    }\n}\n\nGF_COMMAND Demo_Control(void)\n{\n    return Game_Control(true);\n}\n\nvoid Demo_StopFlashing(void)\n{\n    Overlay_SetBottomText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_GS_KEY,\n        .gs_key = GS_ID(\"general/misc/demo_mode\"),\n        .flash_enabled = false,\n    });\n}\n"
  },
  {
    "path": "src/trx/game/demo.h",
    "content": "#pragma once\n\n#include <trx/core/virtual_file.h>\n#include <trx/game/game_flow/types.h>\n\nvoid Demo_LoadData(VFILE *file, size_t size);\nuint32_t *Demo_GetData(void);\n\nbool Demo_Start(int32_t level_num);\nvoid Demo_End(void);\nvoid Demo_Pause(void);\nvoid Demo_Unpause(void);\nvoid Demo_StopFlashing(void);\n\nbool Demo_UpdateInput(void);\nGF_COMMAND Demo_Control(void);\nint32_t Demo_ChooseLevel(int32_t demo_num);\n"
  },
  {
    "path": "src/trx/game/effects/const.h",
    "content": "#pragma once\n\n#define NO_EFFECT (-1)\n#define MAX_EFFECTS 1000\n"
  },
  {
    "path": "src/trx/game/effects/draw.c",
    "content": "#include <trx/game/effects/draw.h>\n\n#include <trx/game/effects/const.h>\n#include <trx/game/effects/manager.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/version.h>\n\nvoid Effect_Draw(const int16_t effect_num)\n{\n    const EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n    if (!obj->loaded) {\n        return;\n    }\n\n    // TR3 uses BUBBLES1 as a dummy effect carrier and renders the bubble via\n    // an attached spark; keep TR1/TR2 legacy bubble rendering intact.\n    if (g_TRVersion == 3 && effect->object_id == O_BUBBLE_1) {\n        return;\n    }\n\n    if (effect->object_id == O_GLOW) {\n        Output_DrawSprite(\n            effect->interp.result.pos.x, effect->interp.result.pos.y,\n            effect->interp.result.pos.z, Object_Get(O_GLOW)->mesh_idx,\n            effect->shade, COLOR_RGB_F_WHITE, DRAW_BLEND);\n        return;\n    }\n\n    if (obj->effect_draw_func != nullptr) {\n        if (obj->effect_draw_func(effect)) {\n            return;\n        }\n    }\n\n    if (obj->mesh_count < 0) {\n        const RGB_F tint =\n            Object_IsType(effect->object_id, g_WaterSpriteObjects)\n            ? COLOR_RGB_F_WHITE\n            : Output_GetTint();\n        int16_t shade = effect->shade;\n        if (shade == -1) {\n            Output_CalculateLight(effect->pos, effect->room_num);\n            shade = Output_GetLightAdder();\n        }\n        Output_DrawSprite(\n            effect->interp.result.pos.x, effect->interp.result.pos.y,\n            effect->interp.result.pos.z, obj->mesh_idx - effect->frame_num,\n            shade, tint, DRAW_BLEND);\n    } else {\n        Matrix_Push();\n        Matrix_TranslateAbs32(effect->interp.result.pos);\n        Matrix_Rot16(effect->interp.result.rot);\n        if (obj->mesh_count != 0) {\n            Output_CalculateStaticLight(effect->shade);\n            Object_DrawMesh(obj->mesh_idx, -1, false);\n        } else {\n            Output_CalculateLight(effect->interp.result.pos, effect->room_num);\n            Object_DrawMesh(effect->frame_num, -1, false);\n        }\n        Matrix_Pop();\n    }\n}\n"
  },
  {
    "path": "src/trx/game/effects/draw.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid Effect_Draw(int16_t effect_num);\n"
  },
  {
    "path": "src/trx/game/effects/manager.c",
    "content": "#include <trx/game/effects/manager.h>\n\n#include <trx/game/effects/const.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n\nstatic EFFECT *m_Effects = nullptr;\nstatic int16_t m_NextEffectFree = NO_EFFECT;\nstatic int16_t m_NextEffectActive = NO_EFFECT;\n\nstatic void M_RemoveActive(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    int16_t link_num = m_NextEffectActive;\n    if (link_num == effect_num) {\n        m_NextEffectActive = effect->next_active;\n        return;\n    }\n    while (link_num != NO_EFFECT) {\n        EFFECT *const fx_link = Effect_Get(link_num);\n        if (fx_link->next_active == effect_num) {\n            fx_link->next_active = effect->next_active;\n            return;\n        }\n        link_num = fx_link->next_active;\n    }\n}\n\nstatic void M_RemoveDrawn(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    ROOM *const room = Room_Get(effect->room_num);\n    int16_t link_num = room->effect_num;\n    if (link_num == effect_num) {\n        room->effect_num = effect->next_free;\n        return;\n    }\n    while (link_num != NO_EFFECT) {\n        EFFECT *const fx_link = Effect_Get(link_num);\n        if (fx_link->next_free == effect_num) {\n            fx_link->next_free = effect->next_free;\n            return;\n        }\n        link_num = fx_link->next_free;\n    }\n}\n\nvoid Effect_InitialiseArray(void)\n{\n    m_Effects = GameBuf_Alloc(MAX_EFFECTS * sizeof(EFFECT), GBUF_EFFECTS);\n    m_NextEffectFree = 0;\n    m_NextEffectActive = NO_EFFECT;\n    for (int32_t i = 0; i < MAX_EFFECTS - 1; i++) {\n        EFFECT *const effect = Effect_Get(i);\n        effect->next_free = i + 1;\n    }\n    m_Effects[MAX_EFFECTS - 1].next_free = NO_EFFECT;\n}\n\nvoid Effect_Control(void)\n{\n    int16_t effect_num = m_NextEffectActive;\n    while (effect_num != NO_EFFECT) {\n        const EFFECT *const effect = Effect_Get(effect_num);\n        const OBJECT *const obj = Object_Get(effect->object_id);\n        const int16_t next = effect->next_active;\n        if (obj->control_func != nullptr) {\n            obj->control_func(effect_num);\n        }\n        effect_num = next;\n    }\n}\n\nEFFECT *Effect_Get(const int16_t effect_num)\n{\n    return &m_Effects[effect_num];\n}\n\nint16_t Effect_GetIndex(const EFFECT *const effect)\n{\n    return effect - m_Effects;\n}\n\nint16_t Effect_GetInOrderNum(const int16_t effect_num)\n{\n    int16_t order_num = 0;\n    for (int16_t link_num = Effect_GetActiveNum(); link_num != NO_EFFECT;\n         link_num = Effect_Get(link_num)->next_active) {\n        if (link_num == effect_num) {\n            return order_num;\n        }\n        order_num++;\n    }\n    return NO_EFFECT;\n}\n\nint16_t Effect_GetActiveNum(void)\n{\n    return m_NextEffectActive;\n}\n\nint16_t Effect_Create(const int16_t room_num)\n{\n    const int16_t effect_num = m_NextEffectFree;\n    if (effect_num == NO_EFFECT) {\n        return NO_EFFECT;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    m_NextEffectFree = effect->next_free;\n\n    ROOM *const room = Room_Get(room_num);\n    effect->room_num = room_num;\n    effect->next_free = room->effect_num;\n    room->effect_num = effect_num;\n\n    effect->next_active = m_NextEffectActive;\n    m_NextEffectActive = effect_num;\n    effect->shade = SHADE_NEUTRAL;\n\n    return effect_num;\n}\n\nvoid Effect_Kill(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    Sparks_DetachEffect(effect_num);\n    M_RemoveActive(effect_num);\n    M_RemoveDrawn(effect_num);\n\n    effect->next_free = m_NextEffectFree;\n    m_NextEffectFree = effect_num;\n}\n\nvoid Effect_KillAllActive(void)\n{\n    int16_t effect_num = Effect_GetActiveNum();\n    while (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        const int16_t next_effect_num = effect->next_active;\n        const OBJECT *const obj = Object_Get(effect->object_id);\n\n        if (obj->control_func != nullptr\n            && (effect->object_id != O_FLAME || effect->counter >= 0)) {\n            Effect_Kill(effect_num);\n        }\n        effect_num = next_effect_num;\n    }\n}\n\nvoid Effect_UpdateRoom(const int16_t effect_num, const int16_t room_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    ROOM *const old_room = Room_Get(effect->room_num);\n\n    int16_t link_num = old_room->effect_num;\n    if (link_num == effect_num) {\n        old_room->effect_num = effect->next_free;\n    } else {\n        while (link_num != NO_EFFECT) {\n            if (m_Effects[link_num].next_free == effect_num) {\n                m_Effects[link_num].next_free = effect->next_free;\n                break;\n            }\n            link_num = m_Effects[link_num].next_free;\n        }\n    }\n\n    ROOM *const new_room = Room_Get(room_num);\n    effect->room_num = room_num;\n    effect->next_free = new_room->effect_num;\n    new_room->effect_num = effect_num;\n}\n"
  },
  {
    "path": "src/trx/game/effects/manager.h",
    "content": "#pragma once\n\n#include <trx/game/effects/types.h>\n\nvoid Effect_InitialiseArray(void);\nvoid Effect_Control(void);\n\nEFFECT *Effect_Get(int16_t effect_num);\nint16_t Effect_GetIndex(const EFFECT *effect);\nint16_t Effect_GetInOrderNum(int16_t effect_num);\nint16_t Effect_GetActiveNum(void);\nint16_t Effect_Create(int16_t room_num);\nvoid Effect_Kill(int16_t effect_num);\nvoid Effect_KillAllActive(void);\nvoid Effect_UpdateRoom(int16_t effect_num, int16_t room_num);\n"
  },
  {
    "path": "src/trx/game/effects/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/types.h>\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_16 rot;\n    int16_t room_num;\n    OBJECT_ID object_id;\n    int16_t next_free;\n    int16_t next_active;\n    int16_t speed;\n    int16_t fall_speed;\n    int16_t frame_num;\n    int16_t counter;\n    int16_t shade;\n\n    int32_t flag1, flag2;\n\n    struct {\n        struct {\n            XYZ_32 pos;\n            XYZ_16 rot;\n        } result, prev;\n    } interp;\n} EFFECT;\n"
  },
  {
    "path": "src/trx/game/effects.h",
    "content": "#pragma once\n\n#include <trx/game/effects/const.h>\n#include <trx/game/effects/draw.h>\n#include <trx/game/effects/manager.h>\n#include <trx/game/effects/types.h>\n"
  },
  {
    "path": "src/trx/game/enum.c",
    "content": "#include <trx/config/enum.h>\n#include <trx/core/enum_map.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow/types.h>\n#include <trx/game/gun/types.h>\n#include <trx/game/input.h>\n#include <trx/game/lara/skin/types.h>\n#include <trx/game/lara/types.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/screenshot.h>\n#include <trx/game/ui/settings.h>\n\nstatic __attribute__((constructor)) void M_Init(void)\n{\n#define X_INPUT_ROLE(role_name, state_name)                                    \\\n    ENUM_MAP(INPUT_ROLE, role_name, #state_name);\n#include <trx/game/input/roles.def>\n#undef X_INPUT_ROLE\n\n    ENUM_MAP(GAME_BUFFER, GBUF_TEXTURE_PAGES, \"Texture pages\");\n    ENUM_MAP(GAME_BUFFER, GBUF_PALETTES, \"Color palettes\");\n    ENUM_MAP(GAME_BUFFER, GBUF_OBJECT_TEXTURES, \"Object textures\");\n    ENUM_MAP(GAME_BUFFER, GBUF_SPRITE_TEXTURES, \"Sprite textures\");\n    ENUM_MAP(GAME_BUFFER, GBUF_STATIC_OBJECTS_3D, \"Static objects (3D)\");\n    ENUM_MAP(GAME_BUFFER, GBUF_STATIC_OBJECTS_2D, \"Static objects (2D)\");\n    ENUM_MAP(GAME_BUFFER, GBUF_MESH_POINTERS, \"Mesh pointers\");\n    ENUM_MAP(GAME_BUFFER, GBUF_MESHES, \"Meshes\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ANIMS, \"Animations\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ANIM_CHANGES, \"Animation changes\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ANIM_RANGES, \"Animation ranges\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ANIM_COMMANDS, \"Animation commands\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ANIM_BONES, \"Animation bones\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ANIM_FRAMES, \"Animation frames\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ROOMS, \"Rooms\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ROOM_MESH, \"Room meshes\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ROOM_PORTALS, \"Room portals\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ROOM_SECTORS, \"Room sectors\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ROOM_LIGHTS, \"Room lights\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ROOM_STATIC_MESHES, \"Room static meshes\");\n    ENUM_MAP(GAME_BUFFER, GBUF_FLOOR_DATA, \"Floor data\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ITEMS, \"Items\");\n    ENUM_MAP(GAME_BUFFER, GBUF_ITEM_DATA, \"Item data\");\n    ENUM_MAP(GAME_BUFFER, GBUF_EFFECTS, \"Effects\");\n    ENUM_MAP(GAME_BUFFER, GBUF_CAMERAS, \"Cameras\");\n    ENUM_MAP(GAME_BUFFER, GBUF_SOUND_SOURCES, \"Sound sources\");\n    ENUM_MAP(GAME_BUFFER, GBUF_BOXES, \"Boxes\");\n    ENUM_MAP(GAME_BUFFER, GBUF_OVERLAPS, \"Overlaps\");\n    ENUM_MAP(GAME_BUFFER, GBUF_GROUND_ZONE, \"Ground zones\");\n    ENUM_MAP(GAME_BUFFER, GBUF_FLY_ZONE, \"Fly zones\");\n    ENUM_MAP(\n        GAME_BUFFER, GBUF_ANIMATED_TEXTURE_RANGES, \"Animated texture ranges\");\n    ENUM_MAP(GAME_BUFFER, GBUF_CINEMATIC_FRAMES, \"Cinematic frames\");\n    ENUM_MAP(GAME_BUFFER, GBUF_DEMO_BUFFER, \"Demo buffer\");\n    ENUM_MAP(GAME_BUFFER, GBUF_CREATURE_DATA, \"Creature data\");\n    ENUM_MAP(GAME_BUFFER, GBUF_CREATURE_LOT, \"Creature pathfinding\");\n    ENUM_MAP(GAME_BUFFER, GBUF_SAMPLE_INFOS, \"Sample information\");\n    ENUM_MAP(GAME_BUFFER, GBUF_SAMPLES, \"Samples\");\n    ENUM_MAP(GAME_BUFFER, GBUF_WALKABLES, \"Walkables buffer\");\n\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LOOP_GAME, \"loop_game\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_PLAY_FMV, \"play_fmv\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_PLAY_CUTSCENE, \"play_cutscene\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_PLAY_MUSIC, \"play_music\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LOADING_SCREEN, \"loading_screen\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_DISPLAY_PICTURE, \"display_picture\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LEVEL_STATS, \"level_stats\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_TOTAL_STATS, \"total_stats\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_GLOBE_SELECT, \"globe_select\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_EXIT_TO_TITLE, \"exit_to_title\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_LEVEL_COMPLETE, \"level_complete\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_ADD_ITEM, \"give_item\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_WEAPONS, \"remove_weapons\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_AMMO, \"remove_ammo\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_MEDIPACKS, \"remove_medipacks\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_FLARES, \"remove_flares\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_DISABLE_FLOOR, \"disable_floor\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_SETUP_BACON_LARA, \"setup_bacon_lara\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_REMOVE_SCIONS, \"remove_scions\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_ENABLE_SUNSET, \"enable_sunset\");\n    ENUM_MAP(GF_SEQUENCE_EVENT_TYPE, GFS_SET_START_ANIM, \"set_lara_start_anim\");\n    ENUM_MAP(\n        GF_SEQUENCE_EVENT_TYPE, GFS_ADD_SECRET_REWARD, \"add_secret_reward\");\n\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_TITLE, \"title\");\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_NORMAL, \"normal\");\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_CUTSCENE, \"cutscene\");\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_GYM, \"gym\");\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_BONUS, \"bonus\");\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_DUMMY, \"dummy\");\n    ENUM_MAP(GF_LEVEL_TYPE, GFL_CURRENT, \"current\");\n\n    ENUM_MAP(WEATHER_TYPE, WEATHER_NONE, \"none\");\n    ENUM_MAP(WEATHER_TYPE, WEATHER_RAIN, \"rain\");\n    ENUM_MAP(WEATHER_TYPE, WEATHER_SNOW, \"snow\");\n\n    ENUM_MAP(GF_DEATH_TILE, GF_DEATH_TILE_LAVA, \"lava\");\n    ENUM_MAP(GF_DEATH_TILE, GF_DEATH_TILE_RAPIDS, \"rapids\");\n    ENUM_MAP(GF_DEATH_TILE, GF_DEATH_TILE_ELECTRIC, \"electric\");\n\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_UNARMED);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_PISTOLS);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_MAGNUMS);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_UZIS);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_SHOTGUN);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_M16);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_MP5);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_GRENADE);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_HARPOON);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_FLARE);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_SKIDOO);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_AUTOS);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_DESERT_EAGLE);\n    ENUM_MAP_SELF(LARA_GUN_TYPE, LGT_ROCKET);\n\n    ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_DUAL_PISTOLS);\n    ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_SINGLE_PISTOL);\n    ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_RIFLE);\n    ENUM_MAP_SELF(WEAPON_TYPE, WEAPON_TYPE_MOUNTED);\n\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_HP, \"lara_hp\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_HP_POISON, \"lara_hp_poison\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_AIR, \"lara_air\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_STAMINA, \"lara_stamina\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_LARA_EXPOSURE, \"lara_exposure\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_ENEMY_HP, \"enemy_hp\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_ALLY_HP, \"ally_hp\");\n    ENUM_MAP(UI_BAR_TYPE, UI_BAR_PROGRESS, \"progress\");\n\n    ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_NONE);\n    ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_HEAD_ONLY);\n    ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_FULL);\n    ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_MAULED);\n    ENUM_MAP_SELF(LARA_SKIN_BRAID_MODE, BRAID_MODE_TR1_GOLD);\n\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_COMBAT_HEAD);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_MAULED_TORSO);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_GOLD_HEAD);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_TR1_BRAID_GOLD_TORSO);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_DAGGER_HAND);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_DAGGER_HIPS);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_OAR);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_SPANNER);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_DRINK_CAN);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_GLASSES_OPAQUE);\n    ENUM_MAP_SELF(LARA_SKIN_EXTRA_MESH, EXTRA_MESH_GLASSES_TRANSPARENT);\n\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_BREATH);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_TREX_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SCION_PICKUP_1);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_USE_MIDAS);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_MIDAS_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SCION_PICKUP_2);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_TORSO_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_PLUNGER);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_START_ANIM);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_AIRLOCK);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SHARK_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_YETI_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_GONG_BONG);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_GUARD_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_PULL_DAGGER);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_START_HOUSE);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_END_HOUSE);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_SHIVA_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_RAPIDS_DROWN);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_TRAIN_KILL);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_JAIL_WAKE_UP);\n    ENUM_MAP_SELF(LARA_EXTRA_STATE, LS_EXTRA_WILLARD_KILL);\n}\n"
  },
  {
    "path": "src/trx/game/events.c",
    "content": "#include <trx/game/events.h>\n\n#include <trx/config/common.h>\n#include <trx/debug.h>\n\nstatic EVENT_MANAGER *m_GameEventManager = nullptr;\n\nvoid GameEvent_Init(void)\n{\n    m_GameEventManager = EventManager_Create();\n}\n\nvoid GameEvent_Shutdown(void)\n{\n    EventManager_Free(m_GameEventManager);\n    m_GameEventManager = nullptr;\n}\n\nint32_t GameEvent_Subscribe(\n    const char *const event_name, const void *const sender,\n    const GAME_EVENT_LISTENER listener, void *const user_data)\n{\n    ASSERT(m_GameEventManager != nullptr);\n    return EventManager_Subscribe(\n        m_GameEventManager, event_name, sender, listener, user_data);\n}\n\nvoid GameEvent_Unsubscribe(const int32_t listener_id)\n{\n    if (m_GameEventManager != nullptr) {\n        EventManager_Unsubscribe(m_GameEventManager, listener_id);\n    }\n}\n\nvoid GameEvent_Fire(const EVENT event)\n{\n    if (m_GameEventManager != nullptr) {\n        EventManager_Fire(m_GameEventManager, &event);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/events.h",
    "content": "#pragma once\n\n#include <trx/core/event_manager.h>\n\n// Game-level event names\n#define GAME_EVENT_SCREENSHOT \"screenshot\"\n#define GAME_EVENT_COMMAND \"console_command\"\n\ntypedef void (*GAME_EVENT_LISTENER)(const EVENT *event, void *user_data);\n\nvoid GameEvent_Init(void);\nvoid GameEvent_Shutdown(void);\n\nint32_t GameEvent_Subscribe(\n    const char *event_name, const void *sender, GAME_EVENT_LISTENER listener,\n    void *user_data);\nvoid GameEvent_Unsubscribe(int32_t listener_id);\n\nvoid GameEvent_Fire(const EVENT event);\n"
  },
  {
    "path": "src/trx/game/fader.c",
    "content": "#include <trx/game/fader.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/clock.h>\n#include <trx/game/shell.h>\n\nstatic void M_Init(FADER *const fader, FADER_ARGS args)\n{\n    CLAMP(args.initial, 0.0f, 1.0f);\n    CLAMP(args.target, 0.0f, 1.0f);\n\n    if (args.from_current) {\n        args.initial = Fader_GetCurrentValue(fader);\n\n        // Reduce duration proportionally to how close the initial value is to\n        // the target.\n        float ratio = ABS(args.target - args.initial);\n        CLAMP(ratio, 0.0f, 1.0f);\n        args.duration *= ratio;\n        if (ratio < 1.0f) {\n            args.debuff = 0.0f;\n        }\n    }\n\n    fader->args = args;\n    ClockTimer_Sync(&fader->timer);\n}\n\nvoid Fader_InitTo(\n    FADER *const fader, const float initial, const float target,\n    const float duration)\n{\n    M_Init(\n        fader,\n        (FADER_ARGS) {\n            .from_current = false,\n            .initial = initial,\n            .target = target,\n            .duration = duration,\n            .debuff = 0.0f,\n        });\n}\n\nvoid Fader_InitToHold(\n    FADER *const fader, const float initial, const float target,\n    const float duration, const float debuff)\n{\n    M_Init(\n        fader,\n        (FADER_ARGS) {\n            .from_current = false,\n            .initial = initial,\n            .target = target,\n            .duration = duration,\n            .debuff = debuff,\n        });\n}\n\nvoid Fader_InitFromCurrent(\n    FADER *const fader, const float target, const float duration)\n{\n    M_Init(\n        fader,\n        (FADER_ARGS) {\n            .from_current = true,\n            .initial = 0.0f,\n            .target = target,\n            .duration = duration,\n            .debuff = 0.0f,\n        });\n}\n\nvoid Fader_InitFromCurrentHold(\n    FADER *const fader, const float target, const float duration,\n    const float debuff)\n{\n    M_Init(\n        fader,\n        (FADER_ARGS) {\n            .from_current = true,\n            .initial = 0.0f,\n            .target = target,\n            .duration = duration,\n            .debuff = debuff,\n        });\n}\n\nfloat Fader_GetCurrentValue(const FADER *const fader)\n{\n    if (!g_Config.visuals.enable_fade_effects || fader->args.duration <= 0.0) {\n        return fader->args.target;\n    }\n    const float elapsed_time = ClockTimer_PeekElapsed(&fader->timer);\n    const float target_time = fader->args.duration;\n    float ratio = elapsed_time / target_time;\n    CLAMP(ratio, 0.0, 1.0);\n    float value = fader->args.initial\n        + (fader->args.target - fader->args.initial) * (float)ratio;\n    CLAMP(value, 0.0f, 1.0f);\n    return value;\n}\n\nbool Fader_IsActive(const FADER *const fader)\n{\n    if (!g_Config.visuals.enable_fade_effects || fader->args.duration <= 0.0) {\n        return false;\n    }\n    const float elapsed_time = ClockTimer_PeekElapsed(&fader->timer);\n    const float target_time = fader->args.duration + fader->args.debuff;\n    return elapsed_time < target_time;\n}\n"
  },
  {
    "path": "src/trx/game/fader.h",
    "content": "#pragma once\n\n#include <trx/game/clock/timer.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    bool from_current;\n    float initial;\n    float target;\n\n    // This value controls how much to keep the last frame after the animation\n    // is done (1.0 = one second).\n    float debuff;\n    float duration;\n} FADER_ARGS;\n\ntypedef struct {\n    FADER_ARGS args;\n    CLOCK_TIMER timer;\n} FADER;\n\nvoid Fader_InitTo(FADER *fader, float initial, float target, float duration);\nvoid Fader_InitToHold(\n    FADER *fader, float initial, float target, float duration, float debuff);\nvoid Fader_InitFromCurrent(FADER *fader, float target, float duration);\nvoid Fader_InitFromCurrentHold(\n    FADER *fader, float target, float duration, float debuff);\nbool Fader_IsActive(const FADER *fader);\n\nfloat Fader_GetCurrentValue(const FADER *fader);\n"
  },
  {
    "path": "src/trx/game/fmv.c",
    "content": "#include <trx/game/fmv.h>\n\n#include <trx/av/audio.h>\n#include <trx/av/video.h>\n#include <trx/config.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/console.h>\n#include <trx/game/fader.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/output/overlay.h>\n#include <trx/game/output/quad.h>\n#include <trx/game/overlay.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n#include <trx/game/viewport.h>\n\n#include <string.h>\n\nstatic bool m_IsPlaying = false;\n\nstatic const char *const m_FallbackExts[] = {\n    \".mp4\", \".mpeg\", \".webm\", \".avi\", \".fmv\", \".rpl\", nullptr,\n};\n\n#define M_FADE_TIME 0.4f\n#define M_PAUSE_OVERLAY_OPACITY 0.8f\n\ntypedef struct {\n    OUTPUT_QUAD_SURFACE_DESC desc;\n    uint8_t *buffer;\n} M_SURFACE;\n\ntypedef struct {\n    OUTPUT_QUAD *renderer_2d;\n    bool show_pause_overlay;\n    FADER pause_fader;\n} M_RENDER_CONTEXT;\n\nstatic OUTPUT_QUAD_SURFACE_DESC M_MakeSurfaceDesc(\n    const int32_t width, const int32_t height)\n{\n    return (OUTPUT_QUAD_SURFACE_DESC) {\n        .width = width,\n        .height = height,\n        .bit_count = 32,\n        .tex_format = GL_BGRA,\n        .tex_type = GL_UNSIGNED_INT_8_8_8_8_REV,\n        .uv = {\n            { .u = 0.0f, .v = 0.0f },\n            { .u = 1.0f, .v = 0.0f },\n            { .u = 1.0f, .v = 1.0f },\n            { .u = 0.0f, .v = 1.0f },\n        },\n        .pitch = width * 4,\n    };\n}\n\nstatic int32_t M_OpenAudioStream(const char *const file_name)\n{\n    int32_t audio_id = Audio_Stream_CreateFromFile(file_name);\n    if (audio_id != AUDIO_NO_SOUND) {\n        return audio_id;\n    }\n\n    // The video file may lack an audio stream (e.g. remastered .ogv).\n    // Try other FMV extensions to find a file that contains audio.\n    const char *const dot = strrchr(file_name, '.');\n    if (dot == nullptr) {\n        return AUDIO_NO_SOUND;\n    }\n\n    const size_t base_len = (size_t)(dot - file_name);\n    for (const char *const *ext = m_FallbackExts; *ext != nullptr; ext++) {\n        char *const candidate =\n            String_Format(\"%.*s%s\", (int)base_len, file_name, *ext);\n        if (File_Exists(candidate)) {\n            audio_id = Audio_Stream_CreateFromFile(candidate);\n            Memory_Free(candidate);\n            if (audio_id != AUDIO_NO_SOUND) {\n                return audio_id;\n            }\n        } else {\n            Memory_Free(candidate);\n        }\n    }\n    return AUDIO_NO_SOUND;\n}\n\nstatic void *M_AllocateSurface(\n    const int32_t width, const int32_t height, void *const user_data)\n{\n    M_SURFACE *const surface = Memory_Alloc(sizeof(M_SURFACE));\n    surface->desc = M_MakeSurfaceDesc(width, height);\n    surface->buffer = Memory_Alloc(surface->desc.pitch * surface->desc.height);\n    return surface;\n}\n\nstatic void M_DeallocateSurface(void *const surface, void *const user_data)\n{\n    M_SURFACE *const surface_ = surface;\n    Memory_Free(surface_->buffer);\n    Memory_Free(surface_);\n}\n\nstatic void M_ClearSurface(void *const surface, void *const user_data)\n{\n    ASSERT(surface != nullptr);\n    M_SURFACE *const surface_ = surface;\n    memset(surface_->buffer, 0, surface_->desc.pitch * surface_->desc.height);\n}\n\nstatic void M_RenderBegin(void *const surface, void *const user_data)\n{\n    Output_BeginScene();\n}\n\nstatic void M_DrawUI(void)\n{\n    UI_BeginScene();\n    Overlay_Draw();\n    Console_Draw();\n    Console_Control();\n    Console_Control();\n    UI_EndScene();\n    UI_Draw();\n}\n\nstatic float M_GetPauseOverlayOpacity(const M_RENDER_CONTEXT *const ctx)\n{\n    if (!g_Config.ui.pause_fade_effects) {\n        return ctx->show_pause_overlay ? M_PAUSE_OVERLAY_OPACITY : 0.0f;\n    }\n    return Fader_GetCurrentValue(&ctx->pause_fader) * M_PAUSE_OVERLAY_OPACITY;\n}\n\nstatic bool M_ShouldShowPauseText(const M_RENDER_CONTEXT *const ctx)\n{\n    if (!ctx->show_pause_overlay) {\n        return false;\n    }\n    if (!g_Config.ui.pause_fade_effects) {\n        return true;\n    }\n    return !Fader_IsActive(&ctx->pause_fader)\n        && Fader_GetCurrentValue(&ctx->pause_fader) >= 1.0f;\n}\n\nstatic void M_RenderEnd(void *const surface, void *const user_data)\n{\n    Output_EndScene();\n    Output_FlipScreen();\n}\n\nstatic void *M_LockSurface(void *const surface, void *const user_data)\n{\n    ASSERT(surface != nullptr);\n    M_SURFACE *const surface_ = surface;\n    return surface_->buffer;\n}\n\nstatic void M_UnlockSurface(void *const surface, void *const user_data)\n{\n}\n\nstatic void M_UploadSurface(void *const surface, void *const user_data)\n{\n    M_RENDER_CONTEXT *const ctx = user_data;\n    M_SURFACE *const surface_ = surface;\n    const float overlay_opacity = M_GetPauseOverlayOpacity(ctx);\n    Output_Quad_Upload(ctx->renderer_2d, &surface_->desc, surface_->buffer);\n\n    Output_SwitchViewport(VIEWPORT_GAME);\n    Output_Quad_Render(ctx->renderer_2d);\n    if (overlay_opacity > 0.0f) {\n        Output_Overlay_DrawBlackRectangle(overlay_opacity, false);\n    }\n\n    Output_SwitchViewport(VIEWPORT_UI);\n    M_DrawUI();\n}\n\nstatic void M_SetPauseText(const bool show)\n{\n    if (show) {\n        Overlay_SetBottomText((OVERLAY_TEXT) {\n            .kind = UI_OVERLAY_TEXT_GS_KEY,\n            .gs_key = GS_ID(\"general/pause/paused\"),\n        });\n    } else {\n        Overlay_SetBottomText((OVERLAY_TEXT) {});\n    }\n}\n\nstatic void M_RedrawFrame(M_RENDER_CONTEXT *const ctx)\n{\n    const float overlay_opacity = M_GetPauseOverlayOpacity(ctx);\n    Output_BeginScene();\n    Output_SwitchViewport(VIEWPORT_GAME);\n    Output_Quad_Render(ctx->renderer_2d);\n    if (overlay_opacity > 0.0f) {\n        Output_Overlay_DrawBlackRectangle(overlay_opacity, false);\n    }\n\n    Output_SwitchViewport(VIEWPORT_UI);\n    M_DrawUI();\n\n    Output_EndScene();\n    Output_FlipScreen();\n}\n\nstatic bool M_Play(const char *const file_name)\n{\n    if (file_name == nullptr || String_IsEmpty(file_name)) {\n        LOG_ERROR(\"Cannot play FMV: empty file path\");\n        return false;\n    }\n\n    VIDEO *const video = Video_Open(file_name);\n    if (video == nullptr) {\n        return false;\n    }\n\n    M_RENDER_CONTEXT render_ctx = {\n        .renderer_2d = Output_Quad_Create(),\n    };\n\n    Video_SetSurfaceAllocatorFunc(video, M_AllocateSurface, nullptr);\n    Video_SetSurfaceDeallocatorFunc(video, M_DeallocateSurface, nullptr);\n    Video_SetSurfaceClearFunc(video, M_ClearSurface, nullptr);\n    Video_SetRenderBeginFunc(video, M_RenderBegin, nullptr);\n    Video_SetRenderEndFunc(video, M_RenderEnd, nullptr);\n    Video_SetSurfaceLockFunc(video, M_LockSurface, nullptr);\n    Video_SetSurfaceUnlockFunc(video, M_UnlockSurface, nullptr);\n    Video_SetSurfaceUploadFunc(video, M_UploadSurface, &render_ctx);\n    Video_SetAudioEnabled(video, false);\n\n    const int32_t audio_id = M_OpenAudioStream(file_name);\n    bool input_paused = false;\n    bool paused = false;\n\n    g_OldInputDB = g_Input;\n    Fader_InitTo(&render_ctx.pause_fader, 0.0f, 0.0f, 0.0f);\n    M_SetPauseText(false);\n    Video_Start(video);\n    while (video->is_playing) {\n        Shell_ProcessEvents();\n\n        const bool focus_paused =\n            g_Config.gameplay.pause_on_focus_lost && !Shell_IsFocused();\n        Input_Update();\n        Shell_ProcessInput();\n\n        render_ctx.show_pause_overlay = input_paused;\n        M_SetPauseText(M_ShouldShowPauseText(&render_ctx));\n        Overlay_Control();\n\n        const bool should_pause = focus_paused || input_paused;\n        if (should_pause != paused) {\n            Video_SetPaused(video, should_pause);\n            Audio_Stream_SetPaused(audio_id, should_pause);\n            paused = should_pause;\n        }\n\n        const float volume = Audio_IsMuted()\n            ? 0.0f\n            : g_Config.audio.master_volume * g_Config.audio.fmv_volume;\n        Audio_Stream_SetVolume(audio_id, volume);\n        const double audio_ts = Audio_Stream_GetTimestamp(audio_id);\n        if (audio_ts >= 0.0) {\n            Video_SetExternalAudioClock(video, audio_ts);\n        }\n\n        Video_SetSurfaceSize(\n            video, Viewport_GetWidth(VIEWPORT_GAME),\n            Viewport_GetHeight(VIEWPORT_GAME));\n        Video_SetSurfacePixelFormat(video, AV_PIX_FMT_BGRA);\n\n        Video_PumpEvents(video);\n\n        if (paused) {\n            M_RedrawFrame(&render_ctx);\n        }\n\n        if ((g_InputDB.pause || (input_paused && g_InputDB.menu_back))\n            && !focus_paused) {\n            input_paused = !input_paused;\n            if (g_Config.ui.pause_fade_effects) {\n                Fader_InitFromCurrent(\n                    &render_ctx.pause_fader, input_paused ? 1.0f : 0.0f,\n                    M_FADE_TIME);\n            }\n        } else if (\n            (!paused && (g_InputDB.menu_back || g_InputDB.menu_confirm))\n            || GF_GetOverrideCommand().action != GF_NOOP || Shell_IsExiting()) {\n            Video_Stop(video);\n            break;\n        }\n    }\n\n    M_SetPauseText(false);\n    Audio_Stream_Close(audio_id);\n    Video_Close(video);\n\n    Output_Quad_Destroy(render_ctx.renderer_2d);\n    Output_ApplyRenderSettings();\n    return true;\n}\n\nbool FMV_Play(const char *const file_path)\n{\n    Music_Stop();\n    Sound_StopAll();\n\n    if (!g_Config.gameplay.enable_fmv) {\n        return false;\n    }\n\n    m_IsPlaying = true;\n    const bool result = M_Play(file_path);\n    m_IsPlaying = false;\n    return result;\n}\n\nbool FMV_IsPlaying(void)\n{\n    return m_IsPlaying;\n}\n"
  },
  {
    "path": "src/trx/game/fmv.h",
    "content": "#pragma once\n\nbool FMV_Play(const char *file_path);\nbool FMV_IsPlaying(void);\n"
  },
  {
    "path": "src/trx/game/fx/common.c",
    "content": "#include <trx/game/fx/common.h>\n\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/fx/footprint.h>\n#include <trx/game/fx/gun_flash.h>\n#include <trx/game/fx/laser.h>\n#include <trx/game/fx/wake.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/fx/water_particles.h>\n#include <trx/game/fx/weather.h>\n\nvoid FX_Control(void)\n{\n    FX_Ring_Control();\n    FX_Wake_Control();\n    FX_Water_Control();\n    FX_Weather_Control();\n    FX_WaterParticles_Control();\n    FX_Footprint_Control();\n    FX_GunFlash_Control();\n    FX_Laser_Control();\n}\n\nvoid FX_Draw(void)\n{\n    FX_Ring_Draw();\n    FX_Water_Draw();\n    FX_Weather_Draw();\n    FX_WaterParticles_Draw();\n    FX_GunFlash_Draw();\n    FX_Laser_Draw();\n    FX_Footprint_Draw();\n}\n\nvoid FX_Reset(void)\n{\n    FX_Water_Reset();\n    FX_Weather_Reset();\n    FX_WaterParticles_Reset();\n    FX_Footprint_Reset();\n    FX_Wake_Reset();\n    FX_Ring_Reset();\n}\n"
  },
  {
    "path": "src/trx/game/fx/common.h",
    "content": "#pragma once\n\nvoid FX_Reset(void);\nvoid FX_Control(void);\nvoid FX_Draw(void);\n"
  },
  {
    "path": "src/trx/game/fx/explosion_ring.c",
    "content": "#include <trx/game/fx/explosion_ring.h>\n\n#include <trx/core/math/func.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n\n#include <string.h>\n\n#define M_MAX_RINGS 6\n\nstatic FX_RING m_Rings[FX_RING_TYPE_NUMBER_OF][M_MAX_RINGS] = {};\nstatic bool m_Active[FX_RING_TYPE_NUMBER_OF] = {};\n\nstatic void M_RotateZX(\n    XYZ_32 *const out, const XYZ_32 in, const int32_t rot_z,\n    const int32_t rot_x)\n{\n    const int32_t sz = Math_Sin(rot_z);\n    const int32_t cz = Math_Cos(rot_z);\n    const int32_t sx = Math_Sin(rot_x);\n    const int32_t cx = Math_Cos(rot_x);\n\n    const int32_t xz = (in.x * cz - in.y * sz) >> W2V_SHIFT;\n    const int32_t yz = (in.x * sz + in.y * cz) >> W2V_SHIFT;\n    const int32_t zz = in.z;\n\n    out->x = xz;\n    out->y = (yz * cx - zz * sx) >> W2V_SHIFT;\n    out->z = (yz * sx + zz * cx) >> W2V_SHIFT;\n}\n\nstatic void M_RememberRing(FX_RING *const ring)\n{\n    ring->prev_radius = ring->radius;\n    ring->prev_rot = ring->rot;\n    ring->prev_pos = ring->pos;\n}\n\nstatic void M_InterpolateRing(const FX_RING *const ring, FX_RING *const out)\n{\n    *out = *ring;\n    const double ratio = Interpolation_GetWorldRate();\n    out->radius = (int16_t)LERP(ring->prev_radius, ring->radius, ratio);\n    out->rot.x = Math_AngleMean(ring->prev_rot.x, ring->rot.x, ratio);\n    out->rot.z = Math_AngleMean(ring->prev_rot.z, ring->rot.z, ratio);\n    out->pos.x = (int32_t)LERP(ring->prev_pos.x, ring->pos.x, ratio);\n    out->pos.y = (int32_t)LERP(ring->prev_pos.y, ring->pos.y, ratio);\n    out->pos.z = (int32_t)LERP(ring->prev_pos.z, ring->pos.z, ratio);\n}\n\nstatic void M_BuildRingCircle(\n    FX_RING *const ring, const int32_t radius, const int32_t band,\n    const bool clear_inner, const int32_t angle_base)\n{\n    int32_t angle = angle_base;\n    for (int32_t i = 0; i < 8; i++) {\n        FX_EXPLOSION_VERT *const vtx = &ring->verts[band * 8 + i];\n        vtx->pos.x = (radius * Math_Sin(angle << 4)) >> W2V_SHIFT;\n        vtx->pos.z = (radius * Math_Cos(angle << 4)) >> W2V_SHIFT;\n        if (clear_inner && band != 0) {\n            vtx->color = COLOR_RGB_888_BLACK;\n        }\n        angle = (angle + 512) & 0xFFF;\n    }\n}\n\nstatic void M_DrawTexturedRing(const FX_RING *const ring)\n{\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    const int32_t sprite_idx = sprite_base + 4 + ((time4 >> 4) & 3);\n    const int32_t rot_z = ring->rot.z << 4;\n    const int32_t rot_x = ring->rot.x << 4;\n\n    for (int32_t j = 0; j < 8; j++) {\n        const int32_t j2 = (j == 7) ? 0 : (j + 1);\n        const FX_EXPLOSION_VERT *const o0 = &ring->verts[j];\n        const FX_EXPLOSION_VERT *const o1 = &ring->verts[j2];\n        const FX_EXPLOSION_VERT *const i0 = &ring->verts[8 + j];\n        const FX_EXPLOSION_VERT *const i1 = &ring->verts[8 + j2];\n\n        if ((o0->color.r | o0->color.g | o0->color.b | o1->color.r | o1->color.g\n             | o1->color.b | i0->color.r | i0->color.g | i0->color.b\n             | i1->color.r | i1->color.g | i1->color.b)\n            == 0U) {\n            continue;\n        }\n\n        XYZ_32 p_local[4] = {\n            { o0->pos.x, 0, o0->pos.z },\n            { o1->pos.x, 0, o1->pos.z },\n            { i1->pos.x, 0, i1->pos.z },\n            { i0->pos.x, 0, i0->pos.z },\n        };\n        XYZ_32 p_rot[4] = {};\n        XYZ_32 p_world[4] = {};\n        for (int32_t c = 0; c < 4; c++) {\n            M_RotateZX(&p_rot[c], p_local[c], rot_z, rot_x);\n            p_world[c].x = ring->pos.x + p_rot[c].x;\n            p_world[c].y = ring->pos.y + p_rot[c].y;\n            p_world[c].z = ring->pos.z + p_rot[c].z;\n        }\n\n        const RGBA_8888 color[4] = {\n            { o0->color.r, o0->color.g, o0->color.b, 255 },\n            { o1->color.r, o1->color.g, o1->color.b, 255 },\n            { i1->color.r, i1->color.g, i1->color.b, 255 },\n            { i0->color.r, i0->color.g, i0->color.b, 255 },\n        };\n\n        OutputSource_PolyFX_StageSpriteQuadWorld(\n            sprite_idx, p_world, color, DRAW_BLEND_ADD);\n    }\n}\n\nstatic void M_DrawFlatRing(const FX_RING *const ring)\n{\n    const int32_t rot_z = ring->rot.z << 4;\n    const int32_t rot_x = ring->rot.x << 4;\n\n    for (int32_t j = 0; j < 8; j++) {\n        const int32_t j2 = (j == 7) ? 0 : (j + 1);\n        const FX_EXPLOSION_VERT *const o0 = &ring->verts[j];\n        const FX_EXPLOSION_VERT *const o1 = &ring->verts[j2];\n        const FX_EXPLOSION_VERT *const i0 = &ring->verts[8 + j];\n        const FX_EXPLOSION_VERT *const i1 = &ring->verts[8 + j2];\n\n        if ((o0->color.r | o0->color.g | o0->color.b | o1->color.r | o1->color.g\n             | o1->color.b | i0->color.r | i0->color.g | i0->color.b\n             | i1->color.r | i1->color.g | i1->color.b)\n            == 0U) {\n            continue;\n        }\n\n        XYZ_32 p_local[4] = {\n            { o0->pos.x, 0, o0->pos.z },\n            { o1->pos.x, 0, o1->pos.z },\n            { i1->pos.x, 0, i1->pos.z },\n            { i0->pos.x, 0, i0->pos.z },\n        };\n        XYZ_32 p_rot[4] = {};\n        XYZ_32 p_world[4] = {};\n        for (int32_t c = 0; c < 4; c++) {\n            M_RotateZX(&p_rot[c], p_local[c], rot_z, rot_x);\n            p_world[c].x = ring->pos.x + p_rot[c].x;\n            p_world[c].y = ring->pos.y + p_rot[c].y;\n            p_world[c].z = ring->pos.z + p_rot[c].z;\n        }\n\n        const XYZ_32 world_pos[4] = {\n            p_world[0],\n            p_world[1],\n            p_world[2],\n            p_world[3],\n        };\n        const RGBA_8888 color[4] = {\n            { o0->color.r, o0->color.g, o0->color.b, 0xC0 },\n            { o1->color.r, o1->color.g, o1->color.b, 0xC0 },\n            { i1->color.r, i1->color.g, i1->color.b, 0xC0 },\n            { i0->color.r, i0->color.g, i0->color.b, 0xC0 },\n        };\n        OutputSource_PolyFX_StageQuadExt(\n            -1, world_pos, nullptr, color,\n            VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE,\n            DRAW_BLEND_ADD);\n    }\n}\n\nstatic void M_ControlExplosionRings(void)\n{\n    bool any_active = false;\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_BLAST][i];\n        if (ring->on == 0) {\n            continue;\n        }\n\n        M_RememberRing(ring);\n        ring->life--;\n        if (ring->life == 0) {\n            ring->on = 0;\n            continue;\n        }\n\n        ring->radius += ring->speed;\n        any_active = true;\n    }\n    if (!any_active) {\n        m_Active[FX_RING_TYPE_BLAST] = false;\n    }\n}\n\nstatic void M_ControlSummonRings(void)\n{\n    bool any_active = false;\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_SUMMON][i];\n        if (ring->on == 0) {\n            continue;\n        }\n\n        M_RememberRing(ring);\n        ring->life--;\n        ring->radius -= ring->speed;\n        if (ring->life == 0 || ring->radius <= 0) {\n            ring->on = 0;\n            continue;\n        }\n\n        ring->speed += 2;\n        any_active = true;\n    }\n    if (!any_active) {\n        m_Active[FX_RING_TYPE_SUMMON] = false;\n    }\n}\n\nstatic void M_ControlKnockBackRings(void)\n{\n    bool any_active = false;\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i];\n        if (ring->on == 0) {\n            continue;\n        }\n\n        M_RememberRing(ring);\n        ring->life--;\n        if (ring->life == 0) {\n            ring->on = 0;\n            continue;\n        }\n\n        ring->radius += ring->speed;\n        if (ring->speed < 0) {\n            ring->speed--;\n        } else {\n            ring->speed += i == 1 ? 3 : 2;\n        }\n        any_active = true;\n    }\n    if (!any_active) {\n        m_Active[FX_RING_TYPE_KNOCKBACK] = false;\n    }\n}\n\nstatic void M_DrawExplosionRings(const int32_t angle_base)\n{\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING draw_ring = {};\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_BLAST][i];\n        if (ring->on == 0) {\n            continue;\n        }\n\n        M_InterpolateRing(ring, &draw_ring);\n\n        int32_t rad = draw_ring.radius;\n        for (int32_t band = 0; band < 2; band++) {\n            M_BuildRingCircle(&draw_ring, rad, band, false, angle_base);\n            for (int32_t k = 0; k < 8; k++) {\n                FX_EXPLOSION_VERT *const vtx = &draw_ring.verts[band * 8 + k];\n\n                int32_t r = 0;\n                int32_t g = 0;\n                int32_t b = 0;\n                if (draw_ring.on == 2) {\n                    // Tony\n                    r = (Random_GetDraw() & 0x1F) + 224;\n                    g = (r >> 2) + (Random_GetDraw() & 0x3F);\n                    b = Random_GetDraw() & 0x3F;\n                } else if (draw_ring.on == 3) {\n                    // Sophia\n                    r = Random_GetDraw() & 0x3F;\n                    g = (Random_GetDraw() & 0x1F) + 224;\n                    b = (g >> 2) + (Random_GetDraw() & 0x3F);\n                } else if (draw_ring.on == 4) {\n                    // Puna\n                    r = Random_GetDraw() & 0x1F;\n                    b = (Random_GetDraw() & 0x3F) + 224;\n                    g = (b >> 2) + (Random_GetDraw() & 0x3F);\n                } else {\n                    // Willard\n                    r = Random_GetDraw() & 0x3F;\n                    g = (Random_GetDraw() & 0x1F) + 224;\n                    b = (g >> 1) + (Random_GetDraw() & 0x3F);\n                }\n\n                vtx->color = (RGB_888) {\n                    (r * ring->life) >> 5,\n                    (g * ring->life) >> 5,\n                    (b * ring->life) >> 5,\n                };\n            }\n            rad >>= 1;\n        }\n\n        M_DrawTexturedRing(&draw_ring);\n    }\n}\n\nstatic void M_DrawSummonRings(const int32_t angle_base)\n{\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING draw_ring = {};\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_SUMMON][i];\n        if (ring->on == 0) {\n            continue;\n        }\n\n        M_InterpolateRing(ring, &draw_ring);\n\n        const int32_t fade = ring->life > 32 ? (64 - ring->life) << 1\n            : ring->life < 8                 ? ring->life << 3\n                                             : 64;\n        int32_t rad = draw_ring.radius;\n        for (int32_t band = 0; band < 2; band++) {\n            M_BuildRingCircle(&draw_ring, rad, band, true, angle_base);\n            if (band == 0) {\n                for (int32_t k = 0; k < 8; k++) {\n                    FX_EXPLOSION_VERT *const vtx = &draw_ring.verts[k];\n                    const int32_t g = (Random_GetDraw() & 0x1F) + 224;\n                    const int32_t b = (g >> 2) + (Random_GetDraw() & 0x3F);\n                    const int32_t r = Random_GetDraw() & 0x3F;\n                    vtx->color = (RGB_888) {\n                        (r * fade) >> 7,\n                        (g * fade) >> 7,\n                        (b * fade) >> 7,\n                    };\n                }\n            }\n            rad >>= 1;\n        }\n\n        M_DrawFlatRing(&draw_ring);\n    }\n}\n\nstatic void M_DrawKnockBackRings(const int32_t angle_base)\n{\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING draw_ring = {};\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i];\n        if (ring->on == 0) {\n            continue;\n        }\n\n        M_InterpolateRing(ring, &draw_ring);\n\n        int32_t fade = ring->life > 24 ? (32 - ring->life) << 2\n            : ring->life < 16          ? ring->life << 1\n                                       : 32;\n        int32_t rad = draw_ring.radius;\n        for (int32_t band = 0; band < 2; band++) {\n            M_BuildRingCircle(&draw_ring, rad, band, false, angle_base);\n            for (int32_t k = 0; k < 8; k++) {\n                FX_EXPLOSION_VERT *const vtx = &draw_ring.verts[band * 8 + k];\n                const int32_t g = (Random_GetDraw() & 0x1F) + 224;\n                const int32_t b = (g >> 2) + (Random_GetDraw() & 0x3F);\n                const int32_t r = Random_GetDraw() & 0x3F;\n                vtx->color = (RGB_888) {\n                    (r * fade) >> 5,\n                    (g * fade) >> 5,\n                    (b * fade) >> 5,\n                };\n            }\n            rad >>= 1;\n            fade >>= 1;\n        }\n\n        M_DrawTexturedRing(&draw_ring);\n    }\n}\n\nvoid FX_Ring_Reset(void)\n{\n    memset(m_Rings, 0, sizeof(m_Rings));\n    memset(m_Active, 0, sizeof(m_Active));\n}\n\nvoid FX_Ring_Sync(FX_RING *const ring)\n{\n    if (ring == nullptr) {\n        return;\n    }\n    M_RememberRing(ring);\n}\n\nFX_RING *FX_Ring_GetRing(const FX_RING_TYPE type, const int32_t idx)\n{\n    if (idx < 0 || idx >= M_MAX_RINGS) {\n        return nullptr;\n    }\n    m_Active[type] = true;\n    return &m_Rings[type][idx];\n}\n\nFX_RING *FX_Ring_PeekRing(const FX_RING_TYPE type, const int32_t idx)\n{\n    if (idx < 0 || idx >= M_MAX_RINGS) {\n        return nullptr;\n    }\n    return &m_Rings[type][idx];\n}\n\nvoid FX_Ring_Control(void)\n{\n    if (m_Active[FX_RING_TYPE_BLAST]) {\n        M_ControlExplosionRings();\n    }\n\n    if (m_Active[FX_RING_TYPE_SUMMON]) {\n        M_ControlSummonRings();\n    }\n\n    if (m_Active[FX_RING_TYPE_KNOCKBACK]) {\n        M_ControlKnockBackRings();\n    }\n}\n\nvoid FX_Ring_SpawnKnockBack(const XYZ_32 pos)\n{\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i];\n        ring->on = 1;\n        ring->life = 32;\n        ring->speed = ((i == 1) + 1) << 4;\n        ring->pos.x = pos.x;\n        ring->pos.y = pos.y - 512 + (i << 7);\n        ring->pos.z = pos.z;\n        ring->rot.x = 0;\n        ring->rot.z = 0;\n        ring->radius = ((i == 1) + 2) << 8;\n        FX_Ring_Sync(ring);\n    }\n    m_Active[FX_RING_TYPE_KNOCKBACK] = true;\n}\n\nvoid FX_Ring_BounceKnockBack(void)\n{\n    for (int32_t i = 0; i < M_MAX_RINGS; i++) {\n        FX_RING *const ring = &m_Rings[FX_RING_TYPE_KNOCKBACK][i];\n        ring->speed = (int16_t)((-ring->speed) >> 2);\n    }\n}\n\nbool FX_Ring_IsRingActive(const FX_RING_TYPE type)\n{\n    return m_Active[type];\n}\n\nvoid FX_Ring_Draw(void)\n{\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t angle_base = (time4 & 0x3F) << 3;\n    M_DrawExplosionRings(angle_base);\n    M_DrawSummonRings(angle_base);\n    M_DrawKnockBackRings(angle_base);\n}\n"
  },
  {
    "path": "src/trx/game/fx/explosion_ring.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/math/types.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    XZ_16 pos;\n    RGB_888 color;\n} FX_EXPLOSION_VERT;\n\ntypedef struct {\n    int16_t on;\n    int16_t life;\n    int16_t speed;\n    int16_t radius;\n    int16_t prev_radius;\n    XZ_16 rot;\n    XZ_16 prev_rot;\n    XYZ_32 pos;\n    XYZ_32 prev_pos;\n    FX_EXPLOSION_VERT verts[16];\n} FX_RING;\n\ntypedef enum {\n    FX_RING_TYPE_BLAST,\n    FX_RING_TYPE_SUMMON,\n    FX_RING_TYPE_KNOCKBACK,\n    FX_RING_TYPE_NUMBER_OF,\n} FX_RING_TYPE;\n\nvoid FX_Ring_Reset(void);\n\nvoid FX_Ring_Control(void);\nvoid FX_Ring_Draw(void);\nvoid FX_Ring_SpawnKnockBack(XYZ_32 pos);\nvoid FX_Ring_BounceKnockBack(void);\n\nvoid FX_Ring_Sync(FX_RING *ring);\n\nbool FX_Ring_IsRingActive(FX_RING_TYPE type);\nFX_RING *FX_Ring_GetRing(FX_RING_TYPE type, int32_t idx);\nFX_RING *FX_Ring_PeekRing(FX_RING_TYPE type, int32_t idx);\n"
  },
  {
    "path": "src/trx/game/fx/footprint.c",
    "content": "#include <trx/game/fx/footprint.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/collision.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n#include <stdlib.h>\n#include <string.h>\n\n#define M_MAX_FOOTPRINTS 32\n#define M_FOOTPRINT_LIFETIME 512\n#define M_FOOTPRINT_Z_DEPTH_ADJUST -0.5f\n\ntypedef struct {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n    int16_t room_num;\n    int16_t y_rot;\n    int16_t active;\n} M_FOOTPRINT;\n\ntypedef struct {\n    M_FOOTPRINT prints[M_MAX_FOOTPRINTS];\n    int32_t next_idx;\n} M_PRIV;\n\nstatic M_PRIV m_Priv;\nstatic const SAMPLE_TRX_ID m_StepSounds[14] = {\n    SFX_FOOTSTEPS_MUD,\n    SFX_FOOTSTEPS_SNOW,\n    SFX_FOOTSTEPS_SAND_OR_GRASS,\n    SFX_FOOTSTEPS_GRAVEL,\n    SFX_FOOTSTEPS_ICE,\n    SFX_TRX_INVALID,\n    SFX_TRX_INVALID,\n    SFX_FOOTSTEPS_WOOD,\n    SFX_FOOTSTEPS_METAL,\n    SFX_TRX_INVALID,\n    SFX_FOOTSTEPS_SAND_OR_GRASS,\n    SFX_TRX_INVALID,\n    SFX_FOOTSTEPS_WOOD,\n    SFX_FOOTSTEPS_METAL,\n};\n\nvoid FX_Footprint_Reset(void)\n{\n    M_PRIV *const p = &m_Priv;\n    memset(p, 0, sizeof(*p));\n}\n\nvoid FX_Footprint_Add(const ITEM *const lara_item, const bool is_left_foot)\n{\n    M_PRIV *const p = &m_Priv;\n    if (lara_item == nullptr) {\n        return;\n    }\n\n    XYZ_32 pos = {};\n    Collide_GetJointAbsPosition(\n        lara_item, &pos, is_left_foot ? LM_FOOT_L : LM_FOOT_R);\n\n    int16_t room_num = lara_item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    if (sector == nullptr) {\n        return;\n    }\n\n    if (sector->fx < ARRAY_SIZE(m_StepSounds)\n        && m_StepSounds[sector->fx] != SFX_TRX_INVALID) {\n        Sound_Effect(m_StepSounds[sector->fx], &lara_item->pos, SPM_NORMAL);\n    }\n\n    if (sector->fx > 4) {\n        return;\n    }\n\n    const int32_t y = Room_GetHeight(sector, pos);\n    if (y == NO_HEIGHT || Room_IsOnWalkable(sector, pos, y, NO_ITEM)) {\n        return;\n    }\n\n    M_FOOTPRINT *const print = &m_Priv.prints[m_Priv.next_idx];\n    print->x = pos.x;\n    print->y = y;\n    print->z = pos.z;\n    print->room_num = room_num;\n    print->y_rot = lara_item->rot.y;\n    print->active = M_FOOTPRINT_LIFETIME;\n    p->next_idx = (p->next_idx + 1) % M_MAX_FOOTPRINTS;\n}\n\nstatic void M_GetWorldPoint(\n    const M_FOOTPRINT *const print, const XYZ_32 local, XYZ_32 *const out_world)\n{\n    const int32_t s = Math_Sin(print->y_rot);\n    const int32_t c = Math_Cos(print->y_rot);\n    const int32_t dx = TRIGMULT2(local.x, c) + TRIGMULT2(local.z, s);\n    const int32_t dz = TRIGMULT2(local.z, c) - TRIGMULT2(local.x, s);\n    out_world->x = print->x + dx;\n    out_world->y = print->y + local.y;\n    out_world->z = print->z + dz;\n}\n\nstatic int32_t M_GetVertexYOffset(\n    const M_FOOTPRINT *const print, const XYZ_32 world_pos)\n{\n    int16_t room_num = print->room_num;\n    const XYZ_32 pos = { world_pos.x, print->y, world_pos.z };\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    if (sector == nullptr) {\n        return 0;\n    }\n\n    const int32_t height = Room_GetHeight(sector, pos);\n    if (height == NO_HEIGHT) {\n        return 0;\n    }\n\n    int32_t dy = height - print->y;\n    if (ABS(dy) > 128) {\n        dy = 0;\n    }\n    return dy;\n}\n\nvoid FX_Footprint_Control(void)\n{\n    if (!g_Config.visuals.enable_footprints) {\n        return;\n    }\n    M_PRIV *const p = &m_Priv;\n    for (int32_t i = 0; i < M_MAX_FOOTPRINTS; i++) {\n        M_FOOTPRINT *const print = &p->prints[i];\n        if (print->active != 0) {\n            print->active--;\n        }\n    }\n}\n\nvoid FX_Footprint_Draw(void)\n{\n    if (!g_Config.visuals.enable_footprints) {\n        return;\n    }\n    const M_PRIV *const p = &m_Priv;\n    const OBJECT *const obj = Object_Get(O_EXPLOSION_1);\n    if (obj == nullptr || !obj->loaded) {\n        return;\n    }\n\n    const int32_t sprite_idx = obj->mesh_idx + 17;\n    if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) {\n        return;\n    }\n\n    const XYZ_32 corners[3] = {\n        { .x = 0, .y = 0, .z = -64 },\n        { .x = -128, .y = 0, .z = 64 },\n        { .x = 128, .y = 0, .z = 64 },\n    };\n\n    for (int32_t i = 0; i < M_MAX_FOOTPRINTS; i++) {\n        const M_FOOTPRINT *const print = &p->prints[i];\n        if (print->active == 0) {\n            continue;\n        }\n\n        XYZ_32 world[3] = {};\n        for (int32_t j = 0; j < 3; j++) {\n            M_GetWorldPoint(print, corners[j], &world[j]);\n            world[j].y += M_GetVertexYOffset(print, world[j]);\n        }\n\n        int32_t c = print->active < 29 ? (print->active << 2) : 112;\n        CLAMP(c, 0, 255);\n\n        const RGBA_8888 color = { c, c, c, 255 };\n        const RGBA_8888 tri_color[3] = { color, color, color };\n\n        for (int32_t j = 0; j < 4; j++) {\n            OutputSource_PolyFX_StageSpriteTriWorldDepth(\n                sprite_idx, world, tri_color, M_FOOTPRINT_Z_DEPTH_ADJUST,\n                DRAW_BLEND_SUB);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/fx/footprint.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nvoid FX_Footprint_Reset(void);\n\nvoid FX_Footprint_Add(const ITEM *lara_item, bool is_left_foot);\n\nvoid FX_Footprint_Control(void);\nvoid FX_Footprint_Draw(void);\n"
  },
  {
    "path": "src/trx/game/fx/gun_flash.c",
    "content": "#include <trx/game/fx/gun_flash.h>\n\n#include <trx/config.h>\n#include <trx/core/colors.h>\n#include <trx/core/math.h>\n#include <trx/game/collision.h>\n#include <trx/game/items.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\n#define M_MAX_FLASHES 32\n#define M_LIFETIME 3\n#define M_AXIS_UNIT 1024\n\ntypedef struct {\n    bool active;\n    int16_t owner_item_num;\n    int16_t room_num;\n    int16_t lifetime;\n    XZ_16 rot;\n    BITE bite;\n    OBJECT_ID flash_object_id;\n    XYZ_32 light_pos;\n} M_GUN_FLASH;\n\ntypedef struct {\n    M_GUN_FLASH flashes[M_MAX_FLASHES];\n    int32_t next_idx;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic int16_t M_GetRandomRoll(void)\n{\n    const int16_t rnd = (int16_t)Random_GetControl();\n    return (int16_t)((rnd << 14) + (rnd >> 2) - 4096);\n}\n\nstatic bool M_GetOwnerItem(\n    const M_GUN_FLASH *const flash, ITEM **const out_item)\n{\n    if (flash->owner_item_num < 0\n        || flash->owner_item_num >= Item_GetTotalCount()) {\n        return false;\n    }\n    *out_item = Item_Get(flash->owner_item_num);\n    return *out_item != nullptr;\n}\n\nstatic void M_GetJointPose(\n    const ITEM *const item, const BITE bite, XYZ_32 *const out_pos,\n    MATRIX *const out_rot)\n{\n    XYZ_32 pos = bite.pos;\n    Collide_GetJointAbsPosition(item, &pos, bite.mesh_num);\n\n    XYZ_32 axis_x = bite.pos;\n    axis_x.x += M_AXIS_UNIT;\n    Collide_GetJointAbsPosition(item, &axis_x, bite.mesh_num);\n\n    XYZ_32 axis_y = bite.pos;\n    axis_y.y += M_AXIS_UNIT;\n    Collide_GetJointAbsPosition(item, &axis_y, bite.mesh_num);\n\n    XYZ_32 axis_z = bite.pos;\n    axis_z.z += M_AXIS_UNIT;\n    Collide_GetJointAbsPosition(item, &axis_z, bite.mesh_num);\n\n    *out_pos = pos;\n    *out_rot = g_IDMatrix;\n\n    out_rot->_00 = ((int64_t)(axis_x.x - pos.x) << W2V_SHIFT) / M_AXIS_UNIT;\n    out_rot->_10 = ((int64_t)(axis_x.y - pos.y) << W2V_SHIFT) / M_AXIS_UNIT;\n    out_rot->_20 = ((int64_t)(axis_x.z - pos.z) << W2V_SHIFT) / M_AXIS_UNIT;\n\n    out_rot->_01 = ((int64_t)(axis_y.x - pos.x) << W2V_SHIFT) / M_AXIS_UNIT;\n    out_rot->_11 = ((int64_t)(axis_y.y - pos.y) << W2V_SHIFT) / M_AXIS_UNIT;\n    out_rot->_21 = ((int64_t)(axis_y.z - pos.z) << W2V_SHIFT) / M_AXIS_UNIT;\n\n    out_rot->_02 = ((int64_t)(axis_z.x - pos.x) << W2V_SHIFT) / M_AXIS_UNIT;\n    out_rot->_12 = ((int64_t)(axis_z.y - pos.y) << W2V_SHIFT) / M_AXIS_UNIT;\n    out_rot->_22 = ((int64_t)(axis_z.z - pos.z) << W2V_SHIFT) / M_AXIS_UNIT;\n\n    out_rot->_03 = 0;\n    out_rot->_13 = 0;\n    out_rot->_23 = 0;\n}\n\nbool FX_GunFlash_Spawn(\n    const ITEM *const owner_item, const CREATURE_GUN *const gun)\n{\n    if (owner_item == nullptr || gun == nullptr || !gun->tr3_enemy_flash) {\n        return false;\n    }\n\n    M_GUN_FLASH *const flash = &m_Priv.flashes[m_Priv.next_idx];\n    flash->active = true;\n    flash->owner_item_num = Item_GetIndex(owner_item);\n    flash->room_num = owner_item->room_num;\n    flash->lifetime = M_LIFETIME;\n    flash->rot = (XZ_16) { .x = gun->tr3_flash_rot_x, .z = M_GetRandomRoll() };\n    flash->bite = gun->tr3_flash;\n    flash->flash_object_id =\n        (gun->tr3_enemy_weapon_flags & 1) != 0 ? O_M16_FLASH : O_GUN_FLASH;\n    flash->light_pos = owner_item->pos;\n\n    m_Priv.next_idx = (m_Priv.next_idx + 1) % M_MAX_FLASHES;\n    return true;\n}\n\nvoid FX_GunFlash_Control(void)\n{\n    for (int32_t i = 0; i < M_MAX_FLASHES; i++) {\n        M_GUN_FLASH *const flash = &m_Priv.flashes[i];\n        if (!flash->active) {\n            continue;\n        }\n\n        ITEM *owner_item = nullptr;\n        if (!M_GetOwnerItem(flash, &owner_item)) {\n            flash->active = false;\n            continue;\n        }\n\n        flash->room_num = owner_item->room_num;\n        flash->rot.z = M_GetRandomRoll();\n\n        XYZ_32 light_pos = flash->bite.pos;\n        Collide_GetJointAbsPosition(\n            owner_item, &light_pos, flash->bite.mesh_num);\n        flash->light_pos = light_pos;\n\n        if (g_Config.visuals.enable_gun_lighting) {\n            const int32_t falloff = (flash->lifetime << 1) + 8;\n            if (g_TRVersion >= 3) {\n                Output_AddDynamicLightRGB(\n                    flash->light_pos, falloff, (RGB_888) { 192, 128, 32 });\n            } else {\n                Output_AddDynamicLight(flash->light_pos, falloff, 11);\n            }\n        }\n\n        flash->lifetime--;\n        if (flash->lifetime <= 0) {\n            flash->active = false;\n        }\n    }\n}\n\nvoid FX_GunFlash_Draw(void)\n{\n    const OBJECT *const glow_obj = Object_Get(O_GLOW);\n\n    for (int32_t i = 0; i < M_MAX_FLASHES; i++) {\n        const M_GUN_FLASH *const flash = &m_Priv.flashes[i];\n        if (!flash->active) {\n            continue;\n        }\n\n        ITEM *owner_item = nullptr;\n        if (!M_GetOwnerItem(flash, &owner_item)) {\n            continue;\n        }\n\n        const OBJECT *const flash_obj = Object_Get(flash->flash_object_id);\n        if (!flash_obj->loaded) {\n            continue;\n        }\n\n        XYZ_32 flash_pos = {};\n        MATRIX flash_rot = {};\n        M_GetJointPose(owner_item, flash->bite, &flash_pos, &flash_rot);\n\n        if (glow_obj->loaded) {\n            Output_DrawSprite(\n                flash_pos.x, flash_pos.y, flash_pos.z, glow_obj->mesh_idx,\n                SHADE_NEUTRAL, (RGB_F) { 1.0f, 0.89f, 0.13f }, DRAW_BLEND_ADD);\n        }\n\n        Matrix_Push();\n        *g_MatrixPtr = g_ViewMatrix;\n        *g_WMatrixPtr = g_IDMatrix;\n        Matrix_TranslateAbs32(flash_pos);\n        Matrix_Mul3x3(&flash_rot);\n        Matrix_RotX(flash->rot.x);\n        Matrix_RotZ(flash->rot.z);\n        Output_CalculateStaticLightRGB_F((RGB_F) { 1.0f, 0.89f, 0.13f });\n        Object_DrawMesh(flash_obj->mesh_idx, -1, false);\n        Matrix_Pop();\n    }\n}\n"
  },
  {
    "path": "src/trx/game/fx/gun_flash.h",
    "content": "#pragma once\n\n#include <trx/game/creature/types.h>\n#include <trx/game/items/types.h>\n\nbool FX_GunFlash_Spawn(const ITEM *owner_item, const CREATURE_GUN *gun);\nvoid FX_GunFlash_Control(void);\nvoid FX_GunFlash_Draw(void);\n"
  },
  {
    "path": "src/trx/game/fx/laser.c",
    "content": "#include <trx/game/fx/laser.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/los.h>\n#include <trx/game/output/sources/poly_fx.h>\n\n#define M_MAX_LASERS 32\n#define M_LIFETIME 2\n\ntypedef struct {\n    bool active;\n    int16_t owner_item_num;\n    int16_t lifetime;\n    BITE bite;\n    RGBA_8888 color;\n    float width;\n} M_LASER;\n\ntypedef struct {\n    M_LASER lasers[M_MAX_LASERS];\n    int32_t next_idx;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic bool M_GetOwnerItem(const M_LASER *const laser, ITEM **const out_item)\n{\n    if (laser->owner_item_num < 0\n        || laser->owner_item_num >= Item_GetTotalCount()) {\n        return false;\n    }\n    *out_item = Item_Get(laser->owner_item_num);\n    return *out_item != nullptr;\n}\n\nbool FX_Laser_Spawn(const ITEM *const owner_item, const CREATURE_GUN *const gun)\n{\n    if (owner_item == nullptr || gun == nullptr || !gun->tr3_enemy_flash) {\n        return false;\n    }\n\n    M_LASER *const laser = &m_Priv.lasers[m_Priv.next_idx];\n    laser->active = true;\n    laser->lifetime = M_LIFETIME;\n    laser->owner_item_num = Item_GetIndex(owner_item);\n    laser->bite = gun->tr3_laser.bite;\n    laser->color = gun->tr3_laser.color;\n    laser->width = gun->tr3_laser.width;\n\n    m_Priv.next_idx = (m_Priv.next_idx + 1) % M_MAX_LASERS;\n    return true;\n}\n\nvoid FX_Laser_Control(void)\n{\n    for (int32_t i = 0; i < M_MAX_LASERS; i++) {\n        M_LASER *const laser = &m_Priv.lasers[i];\n        if (!laser->active) {\n            continue;\n        }\n\n        ITEM *owner_item = nullptr;\n        if (!M_GetOwnerItem(laser, &owner_item)) {\n            laser->active = false;\n            continue;\n        }\n\n        laser->lifetime--;\n        if (laser->lifetime <= 0) {\n            laser->active = false;\n        }\n    }\n}\n\nvoid FX_Laser_Draw(void)\n{\n    for (int32_t i = 0; i < M_MAX_LASERS; i++) {\n        const M_LASER *const laser = &m_Priv.lasers[i];\n        if (!laser->active) {\n            continue;\n        }\n\n        ITEM *owner_item = nullptr;\n        if (!M_GetOwnerItem(laser, &owner_item)) {\n            continue;\n        }\n\n        GAME_VECTOR start = {\n            .pos = laser->bite.pos,\n            .room_num = owner_item->room_num,\n        };\n        GAME_VECTOR target = start;\n        target.y *= 32;\n        Collide_GetJointAbsPosition(\n            owner_item, &start.pos, laser->bite.mesh_num);\n        Collide_GetJointAbsPosition(\n            owner_item, &target.pos, laser->bite.mesh_num);\n\n        LOS_Check(&start, &target, false);\n\n        const int32_t dist = XYZ_32_GetDistance(start.pos, target.pos);\n        int32_t segment_count = 2 * dist / WALL_L;\n        CLAMP(segment_count, 8, 32);\n\n        const XYZ_32 delta = {\n            .x = target.x - start.x,\n            .y = target.y - start.y,\n            .z = target.z - start.z,\n        };\n\n        for (int32_t j = 0; j < segment_count; j++) {\n            const float seg_start = (float)j / segment_count;\n            const float seg_end = (float)(j + 1) / segment_count;\n\n            const XYZ_32 p1 = {\n                .x = start.x + delta.x * seg_start,\n                .y = start.y + delta.y * seg_start,\n                .z = start.z + delta.z * seg_start,\n            };\n            const XYZ_32 p2 = {\n                .x = start.x + delta.x * seg_end,\n                .y = start.y + delta.y * seg_end,\n                .z = start.z + delta.z * seg_end,\n            };\n            OutputSource_PolyFX_StageLineSegment(\n                p1, laser->color, p2, laser->color, laser->width,\n                DRAW_BLEND_ADD);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/fx/laser.h",
    "content": "#pragma once\n\n#include <trx/game/creature/types.h>\n#include <trx/game/items/types.h>\n\nbool FX_Laser_Spawn(const ITEM *owner_item, const CREATURE_GUN *gun);\nvoid FX_Laser_Control(void);\nvoid FX_Laser_Draw(void);\n"
  },
  {
    "path": "src/trx/game/fx/wake.c",
    "content": "#include <trx/game/fx/wake.h>\n\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/output/sources/poly_fx.h>\n\n#define M_MAX_POINTS 32\n\nstatic const XZ_32 m_Offsets[2] = { { .x = -128, 0 }, { .x = 128, .z = 0 } };\n\nstatic FX_WAKE_POINT m_Points[M_MAX_POINTS][2] = {};\nstatic uint8_t m_Shade = 0;\nstatic uint8_t m_StartIndex = 0;\nstatic bool m_Active = false;\n\nstatic RGBA_8888 M_GrayFromWakeLife(const int32_t life, const int32_t shift)\n{\n    int32_t c = (life >> shift) << 3;\n    CLAMPG(c, 255);\n    return (RGBA_8888) { c, c, c, 255 };\n}\n\nstatic XYZ_32 M_GetWakeOrigin(const ITEM *const item, const XZ_32 offset)\n{\n    XYZ_32 pos = item->interp.result.pos;\n    pos = XYZ_32_OffsetYaw(pos, item->interp.result.rot.y, offset.z);\n    pos = XYZ_32_OffsetYaw(pos, item->interp.result.rot.y + DEG_90, offset.x);\n    return pos;\n}\n\nvoid FX_Wake_Reset(void)\n{\n    for (int32_t i = 0; i < M_MAX_POINTS; i++) {\n        m_Points[i][0].life = 0;\n        m_Points[i][1].life = 0;\n    }\n    m_Active = false;\n}\n\nvoid FX_Wake_Control(void)\n{\n    if (!m_Active) {\n        return;\n    }\n\n    bool any_active = false;\n    for (int32_t i = 0; i < 2; i++) {\n        for (int32_t j = 0; j < M_MAX_POINTS; j++) {\n            FX_WAKE_POINT *const pt = &m_Points[j][i];\n            if (pt->life > 0) {\n                pt->prev_pos[0] = pt->pos[0];\n                pt->prev_pos[1] = pt->pos[1];\n                pt->life--;\n                pt->pos[0].x += pt->vel[0].x;\n                pt->pos[0].z += pt->vel[0].z;\n                pt->pos[1].x += pt->vel[1].x;\n                pt->pos[1].z += pt->vel[1].z;\n                if (pt->life > 0) {\n                    any_active = true;\n                }\n            }\n        }\n    }\n    if (!any_active) {\n        m_Active = false;\n    }\n}\n\nFX_WAKE_POINT *FX_Wake_GetPoint(const int32_t wake_idx, const int32_t side)\n{\n    return &m_Points[wake_idx][side];\n}\n\nuint8_t FX_Wake_GetShade(void)\n{\n    return m_Shade;\n}\n\nvoid FX_Wake_SetShade(const uint8_t shade)\n{\n    m_Shade = shade;\n}\n\nuint8_t FX_Wake_GetStartIndex(void)\n{\n    return m_StartIndex;\n}\n\nvoid FX_Wake_AdvanceStartIndex(void)\n{\n    m_StartIndex = (m_StartIndex + 1) & (M_MAX_POINTS - 1);\n    m_Active = true;\n}\n\nvoid FX_Wake_Draw(const ITEM *const item)\n{\n    const int64_t max_origin_dist_sq =\n        XYZ_32_GetLength2_64((XYZ_32) { WALL_L / 2, 0, 0 });\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n\n    for (int32_t side = 0; side < 2; side++) {\n        const XYZ_32 origin = M_GetWakeOrigin(item, m_Offsets[side]);\n        XYZ_32 prev[2] = { origin, origin };\n\n        int32_t c12 = 64;\n        if (m_Shade < 16) {\n            c12 = (c12 * m_Shade) >> 4;\n        }\n\n        int32_t current = (m_StartIndex - 1) & (M_MAX_POINTS - 1);\n        const FX_WAKE_POINT *const first_pt = &m_Points[current][side];\n        if (first_pt->life != 0) {\n            const XYZ_32 first_pos0 = do_interp\n                ? (XYZ_32) {\n                      .x = (int32_t)LERP(\n                          first_pt->prev_pos[0].x, first_pt->pos[0].x, ratio),\n                      .y = (int32_t)LERP(\n                          first_pt->prev_pos[0].y, first_pt->pos[0].y, ratio),\n                      .z = (int32_t)LERP(\n                          first_pt->prev_pos[0].z, first_pt->pos[0].z, ratio),\n                  }\n                : first_pt->pos[0];\n            const XYZ_32 first_pos1 = do_interp\n                ? (XYZ_32) {\n                      .x = (int32_t)LERP(\n                          first_pt->prev_pos[1].x, first_pt->pos[1].x, ratio),\n                      .y = (int32_t)LERP(\n                          first_pt->prev_pos[1].y, first_pt->pos[1].y, ratio),\n                      .z = (int32_t)LERP(\n                          first_pt->prev_pos[1].z, first_pt->pos[1].z, ratio),\n                  }\n                : first_pt->pos[1];\n            const XYZ_32 delta0 = {\n                .x = origin.x - first_pos0.x,\n                .y = 0,\n                .z = origin.z - first_pos0.z,\n            };\n            const XYZ_32 delta1 = {\n                .x = origin.x - first_pos1.x,\n                .y = 0,\n                .z = origin.z - first_pos1.z,\n            };\n            const int64_t dist0_sq = XYZ_32_GetLength2_64(delta0);\n            const int64_t dist1_sq = XYZ_32_GetLength2_64(delta1);\n\n            if (dist0_sq > max_origin_dist_sq\n                && dist1_sq > max_origin_dist_sq) {\n                // Prevent a long bridge quad when the hull origin drifts away\n                // from the latest wake segment (e.g. moving over dry slopes).\n                prev[0] = first_pos1;\n                prev[1] = first_pos0;\n            }\n        }\n\n        for (int32_t nw = 0; nw < M_MAX_POINTS; nw++) {\n            const FX_WAKE_POINT *const pt = &m_Points[current][side];\n            if (pt->life == 0U) {\n                break;\n            }\n\n            int32_t c34 = pt->life;\n            if (m_Shade < 16) {\n                c34 = (c34 * m_Shade) >> 4;\n            }\n\n            const RGBA_8888 quad_color[4] = {\n                M_GrayFromWakeLife(c12, 2),\n                M_GrayFromWakeLife(c12, 1),\n                M_GrayFromWakeLife(c34, 1),\n                M_GrayFromWakeLife(c34, 2),\n            };\n\n            XYZ_32 curr[2] = {\n                do_interp\n                    ? (XYZ_32) {\n                          .x = (int32_t)LERP(\n                              pt->prev_pos[0].x, pt->pos[0].x, ratio),\n                          .y = (int32_t)LERP(\n                              pt->prev_pos[0].y, pt->pos[0].y, ratio),\n                          .z = (int32_t)LERP(\n                              pt->prev_pos[0].z, pt->pos[0].z, ratio),\n                      }\n                    : pt->pos[0],\n                do_interp\n                    ? (XYZ_32) {\n                          .x = (int32_t)LERP(\n                              pt->prev_pos[1].x, pt->pos[1].x, ratio),\n                          .y = (int32_t)LERP(\n                              pt->prev_pos[1].y, pt->pos[1].y, ratio),\n                          .z = (int32_t)LERP(\n                              pt->prev_pos[1].z, pt->pos[1].z, ratio),\n                      }\n                    : pt->pos[1],\n            };\n            const XYZ_32 quad_world[4] = {\n                prev[0],\n                prev[1],\n                curr[0],\n                curr[1],\n            };\n\n            OutputSource_PolyFX_StageQuadExt(\n                -1, quad_world, nullptr, quad_color,\n                VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE,\n                DRAW_BLEND_ADD);\n\n            prev[0] = curr[1];\n            prev[1] = curr[0];\n            c12 = c34;\n            current = (current - 1) & (M_MAX_POINTS - 1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/fx/wake.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\ntypedef struct {\n    XYZ_32 pos[2];\n    XYZ_32 prev_pos[2];\n    XZ_32 vel[2];\n    uint8_t life;\n} FX_WAKE_POINT;\n\nvoid FX_Wake_Reset(void);\nvoid FX_Wake_Control(void);\n\nFX_WAKE_POINT *FX_Wake_GetPoint(int32_t wake_idx, int32_t side);\n\nuint8_t FX_Wake_GetShade(void);\nvoid FX_Wake_SetShade(uint8_t shade);\n\nuint8_t FX_Wake_GetStartIndex(void);\nvoid FX_Wake_AdvanceStartIndex(void);\n\nvoid FX_Wake_Draw(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/fx/water.c",
    "content": "#include <trx/game/fx/water.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n#include <string.h>\n\n#define M_SPLASH_Z_DEPTH_ADJUST -0.005f\n#define M_RIPPLE_Z_DEPTH_ADJUST -0.005f\n\nstatic const int16_t m_SplashRings[8][2] = {\n    { 0, -24 }, { 17, -17 }, { 24, 0 },  { 17, 17 },\n    { 0, 24 },  { -17, 17 }, { -24, 0 }, { -17, -17 },\n};\n\nstatic const uint8_t m_SplashQuadLinks[32] = {\n    8,  9,  0, 1, 9,  10, 1, 2, 10, 11, 2, 3, 11, 12, 3, 4,\n    12, 13, 4, 5, 13, 14, 5, 6, 14, 15, 6, 7, 15, 8,  7, 0,\n};\n\nstatic FX_WATER_SPLASH m_Splashes[4];\nstatic FX_WATER_RIPPLE m_Ripples[16];\nstatic int32_t m_SplashCount = 0;\nstatic int32_t m_Wibble = 0;\n\nvoid FX_Water_Reset(void)\n{\n    memset(m_Splashes, 0, sizeof(m_Splashes));\n    memset(m_Ripples, 0, sizeof(m_Ripples));\n    m_SplashCount = 0;\n    m_Wibble = 0;\n}\n\nstatic bool M_IsRoomUnderwater(const int16_t room_num)\n{\n    return Room_Get(room_num)->flags.underwater;\n}\n\nstatic void M_RememberRipple(FX_WATER_RIPPLE *const ripple)\n{\n    ripple->prev_size = ripple->size;\n    ripple->prev_life = ripple->life;\n    ripple->prev_init = ripple->init;\n}\n\nstatic void M_RememberSplash(FX_WATER_SPLASH *const splash)\n{\n    splash->prev_life = splash->life;\n    for (int32_t i = 0; i < 48; i++) {\n        FX_WATER_SPLASH_VERT *const v = &splash->v[i];\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n    }\n}\n\nstatic bool M_GetUnderwaterBloodColor(\n    RGBA_8888 *const color, const int32_t life)\n{\n    switch (g_Config.visuals.blood_effects) {\n    case BLOOD_EFFECTS_DISABLED:\n        return false;\n    case BLOOD_EFFECTS_PINK:\n        *color = (RGBA_8888) { life / 2, 0, life, 255 };\n        return true;\n    case BLOOD_EFFECTS_RED:\n        *color = (RGBA_8888) { life, 0, 0, 255 };\n        return true;\n    case BLOOD_EFFECTS_NUMBER_OF:\n        break;\n    }\n    return false;\n}\n\nFX_WATER_RIPPLE *FX_Water_SetupRipple(\n    const int32_t x, const int32_t y, const int32_t z, int32_t size,\n    const bool is_still)\n{\n    int32_t idx = -1;\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) {\n        if ((m_Ripples[i].flags & 1U) == 0U) {\n            idx = i;\n            break;\n        }\n    }\n    if (idx < 0) {\n        return nullptr;\n    }\n\n    FX_WATER_RIPPLE *const ripple = &m_Ripples[idx];\n\n    if (size < 0) {\n        ripple->flags = is_still ? 19U : 3U;\n        size = -size;\n    } else {\n        ripple->flags = 1U;\n    }\n\n    ripple->init = 1U;\n    ripple->size = (uint8_t)size;\n    ripple->life = (uint8_t)((Random_GetControl() & 0xF) + 48);\n    ripple->x = (Random_GetControl() & 0x7F) + x - 64;\n    ripple->y = y;\n    ripple->z = (Random_GetControl() & 0x7F) + z - 64;\n    M_RememberRipple(ripple);\n    return ripple;\n}\n\nvoid FX_Water_SetupSplash(const FX_WATER_SPLASH_SETUP *const setup_)\n{\n    FX_WATER_SPLASH_SETUP setup = *setup_;\n\n    int32_t idx = -1;\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Splashes); i++) {\n        if ((m_Splashes[i].flags & 1U) == 0U) {\n            idx = i;\n            break;\n        }\n    }\n\n    if (idx < 0) {\n        Sound_Effect(\n            SFX_LARA_SPLASH, &(XYZ_32) { setup.x, setup.y, setup.z },\n            SPM_NORMAL);\n        return;\n    }\n\n    FX_WATER_SPLASH *const splash = &m_Splashes[idx];\n    splash->flags = 3U;\n\n    if (setup.outer_friction == -9) {\n        splash->flags = 67U;\n        setup.outer_friction = 9;\n    }\n\n    splash->x = setup.x;\n    splash->y = setup.y;\n    splash->z = setup.z;\n    splash->life = 63U;\n\n    FX_WATER_SPLASH_VERT *v = splash->v;\n\n    for (int32_t i = 0; i < 8; i++) {\n        v->wx = (setup.inner_xz_off * m_SplashRings[i][0]) * 2;\n        v->wy = 0;\n        v->wz = (setup.inner_xz_off * m_SplashRings[i][1]) * 2;\n        v->xv = (setup.inner_xz_vel * m_SplashRings[i][0]) / 12;\n        v->yv = 0;\n        v->zv = (setup.inner_xz_vel * m_SplashRings[i][1]) / 12;\n        v->oxv = v->xv >> 3;\n        v->ozv = v->zv >> 3;\n        v->gravity = 0;\n        v->friction = (uint8_t)(setup.inner_friction - 2);\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n        v++;\n    }\n\n    for (int32_t i = 0; i < 8; i++) {\n        v->wx =\n            ((setup.inner_xz_off + setup.inner_xz_size) * m_SplashRings[i][0])\n            * 2;\n        v->wy = setup.inner_y_size;\n        v->wz =\n            ((setup.inner_xz_off + setup.inner_xz_size) * m_SplashRings[i][1])\n            * 2;\n        v->xv = (setup.inner_xz_vel * m_SplashRings[i][0]) >> 3;\n        v->yv = setup.inner_y_vel;\n        v->zv = (setup.inner_xz_vel * m_SplashRings[i][1]) >> 3;\n        v->oxv = v->xv >> 3;\n        v->ozv = v->zv >> 3;\n        v->gravity = (uint8_t)setup.inner_gravity;\n        v->friction = (uint8_t)setup.inner_friction;\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n        v++;\n    }\n\n    for (int32_t i = 0; i < 8; i++) {\n        v->wx = (setup.middle_xz_off * m_SplashRings[i][0]) * 2;\n        v->wy = 0;\n        v->wz = (setup.middle_xz_off * m_SplashRings[i][1]) * 2;\n        v->xv = (setup.middle_xz_vel * m_SplashRings[i][0]) / 12;\n        v->yv = 0;\n        v->zv = (setup.middle_xz_vel * m_SplashRings[i][1]) / 12;\n        v->oxv = v->xv >> 3;\n        v->ozv = v->zv >> 3;\n        v->gravity = 0;\n        v->friction = (uint8_t)(setup.middle_friction - 2);\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n        v++;\n    }\n\n    for (int32_t i = 0; i < 8; i++) {\n        v->wx =\n            ((setup.middle_xz_off + setup.middle_xz_size) * m_SplashRings[i][0])\n            * 2;\n        v->wy = setup.middle_y_size;\n        v->wz =\n            ((setup.middle_xz_off + setup.middle_xz_size) * m_SplashRings[i][1])\n            * 2;\n        v->xv = (setup.middle_xz_vel * m_SplashRings[i][0]) >> 3;\n        v->yv = setup.middle_y_vel;\n        v->zv = (setup.middle_xz_vel * m_SplashRings[i][1]) >> 3;\n        v->oxv = v->xv >> 3;\n        v->ozv = v->zv >> 3;\n        v->gravity = (uint8_t)setup.middle_gravity;\n        v->friction = (uint8_t)setup.middle_friction;\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n        v++;\n    }\n\n    for (int32_t i = 0; i < 8; i++) {\n        v->wx = (setup.outer_xz_off * m_SplashRings[i][0]) * 2;\n        v->wy = 0;\n        v->wz = (setup.outer_xz_off * m_SplashRings[i][1]) * 2;\n        v->xv = (setup.outer_xz_vel * m_SplashRings[i][0]) / 12;\n        v->yv = 0;\n        v->zv = (setup.outer_xz_vel * m_SplashRings[i][1]) / 12;\n        v->oxv = v->xv >> 3;\n        v->ozv = v->zv >> 3;\n        v->gravity = 0;\n        v->friction = (uint8_t)(setup.outer_friction - 2);\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n        v++;\n    }\n\n    for (int32_t i = 0; i < 8; i++) {\n        v->wx =\n            ((setup.outer_xz_off + setup.outer_xz_size) * m_SplashRings[i][0])\n            * 2;\n        v->wy = 0;\n        v->wz =\n            ((setup.outer_xz_off + setup.outer_xz_size) * m_SplashRings[i][1])\n            * 2;\n        v->xv = (setup.outer_xz_vel * m_SplashRings[i][0]) >> 3;\n        v->yv = 0;\n        v->zv = (setup.outer_xz_vel * m_SplashRings[i][1]) >> 3;\n        v->oxv = v->xv >> 3;\n        v->ozv = v->zv >> 3;\n        v->gravity = 0;\n        v->friction = (uint8_t)setup.outer_friction;\n        v->prev_wx = v->wx;\n        v->prev_wy = v->wy;\n        v->prev_wz = v->wz;\n        v++;\n    }\n    splash->prev_life = splash->life;\n\n    Sound_Effect(\n        SFX_LARA_SPLASH, &(XYZ_32) { setup.x, setup.y, setup.z }, SPM_NORMAL);\n}\n\nvoid FX_Water_Splash(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    if (!M_IsRoomUnderwater(room_num)) {\n        return;\n    }\n\n    const int32_t water_height = Room_GetWaterHeight(item->pos, room_num);\n    FX_WATER_SPLASH_SETUP setup = {\n        .x = item->pos.x,\n        .y = water_height,\n        .z = item->pos.z,\n        .inner_xz_off = 32,\n        .inner_xz_size = 8,\n        .inner_y_size = -128,\n        .inner_xz_vel = 320,\n        .inner_y_vel = (int16_t)(-40 * item->fall_speed),\n        .inner_gravity = 160,\n        .inner_friction = 7,\n        .middle_xz_off = 48,\n        .middle_xz_size = 32,\n        .middle_y_size = -64,\n        .middle_xz_vel = 480,\n        .middle_y_vel = (int16_t)(-20 * item->fall_speed),\n        .middle_gravity = 96,\n        .middle_friction = 8,\n        .outer_xz_off = 32,\n        .outer_xz_size = 128,\n        .outer_xz_vel = 544,\n        .outer_friction = 9,\n    };\n    FX_Water_SetupSplash(&setup);\n}\n\nvoid FX_Water_WadeSplash(\n    const ITEM *const item, const int32_t water_height, const int32_t depth)\n{\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    if (!M_IsRoomUnderwater(room_num)) {\n        return;\n    }\n\n    const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds;\n\n    if (item->pos.y + bounds->min.y > water_height\n        || item->pos.y + bounds->max.y < water_height) {\n        return;\n    }\n\n    if (item->fall_speed > 0 && depth < 474 && m_SplashCount == 0) {\n        const FX_WATER_SPLASH_SETUP setup = {\n            .x = item->pos.x,\n            .y = water_height,\n            .z = item->pos.z,\n            .inner_xz_off = 16,\n            .inner_xz_size = 12,\n            .inner_y_size = -96,\n            .inner_xz_vel = 160,\n            .inner_y_vel = (int16_t)(-72 * item->fall_speed),\n            .inner_gravity = 128,\n            .inner_friction = 7,\n            .middle_xz_off = 24,\n            .middle_xz_size = 24,\n            .middle_y_size = -64,\n            .middle_xz_vel = 224,\n            .middle_y_vel = (int16_t)(-36 * item->fall_speed),\n            .middle_gravity = 72,\n            .middle_friction = 8,\n            .outer_xz_off = 32,\n            .outer_xz_size = 32,\n            .outer_xz_vel = 272,\n            .outer_friction = 9,\n        };\n        FX_Water_SetupSplash(&setup);\n        m_SplashCount = 16;\n    } else if (\n        (m_Wibble & 0xF) == 0\n        && (((Random_GetControl() & 0xF) == 0)\n            || item->current_anim_state != LS(LS_STOP))) {\n        FX_Water_SetupRipple(\n            item->pos.x, water_height, item->pos.z,\n            -16 - (Random_GetControl() & 0xF),\n            item->current_anim_state == LS(LS_STOP));\n    }\n}\n\nvoid FX_Water_TriggerUnderwaterBlood(const XYZ_32 pos, const int32_t size)\n{\n    int32_t idx = -1;\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) {\n        if ((m_Ripples[i].flags & 1U) == 0U) {\n            idx = i;\n            break;\n        }\n    }\n    if (idx < 0) {\n        return;\n    }\n\n    FX_WATER_RIPPLE *const ripple = &m_Ripples[idx];\n    ripple->flags = 0x33U;\n    ripple->init = 1U;\n    ripple->life = (Random_GetControl() & 7) - 16;\n    ripple->size = size;\n    ripple->x = pos.x + (Random_GetControl() & 0x3F) - 32;\n    ripple->y = pos.y;\n    ripple->z = pos.z + (Random_GetControl() & 0x3F) - 32;\n    M_RememberRipple(ripple);\n}\n\nvoid FX_Water_TriggerUnderwaterBloodD(const XYZ_32 pos, const int32_t size)\n{\n    int32_t idx = -1;\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) {\n        if ((m_Ripples[i].flags & 1U) == 0U) {\n            idx = i;\n            break;\n        }\n    }\n    if (idx < 0) {\n        return;\n    }\n\n    FX_WATER_RIPPLE *const ripple = &m_Ripples[idx];\n    ripple->flags = 0x33U;\n    ripple->init = 1U;\n    ripple->life = (Random_GetDraw() & 7) - 16;\n    ripple->size = size;\n    ripple->x = pos.x + (Random_GetDraw() & 0x3F) - 32;\n    ripple->y = pos.y;\n    ripple->z = pos.z + (Random_GetDraw() & 0x3F) - 32;\n    M_RememberRipple(ripple);\n}\n\nstatic RGBA_8888 M_Gray(int32_t c)\n{\n    CLAMP(c, 0, 255);\n    return (RGBA_8888) { c, c, c, 255 };\n}\n\nstatic void M_DrawSplash(\n    const int32_t base_sprite_idx, const FX_WATER_SPLASH *s)\n{\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n\n    XYZ_32 points[48];\n    for (int32_t i = 0; i < 48; i++) {\n        const FX_WATER_SPLASH_VERT *const v = &s->v[i];\n        points[i] = (XYZ_32) {\n            .x = s->x\n                + ((do_interp ? (int16_t)LERP(v->prev_wx, v->wx, ratio) : v->wx)\n                   >> 4),\n            .y = s->y\n                + (do_interp ? (int16_t)LERP(v->prev_wy, v->wy, ratio) : v->wy),\n            .z = s->z\n                + ((do_interp ? (int16_t)LERP(v->prev_wz, v->wz, ratio) : v->wz)\n                   >> 4),\n        };\n    }\n\n    for (int32_t ring = 0; ring < 3; ring++) {\n        int32_t sprite_idx = base_sprite_idx + 8;\n        if (ring == 2 || (ring == 0 && (s->flags & 4U) != 0U)\n            || (ring == 1 && (s->flags & 8U) != 0U)) {\n            sprite_idx = base_sprite_idx + 4 + ((m_Wibble >> 4) & 3);\n        }\n\n        const int32_t life = do_interp\n            ? (int32_t)LERP(s->prev_life, s->life, ratio)\n            : (int32_t)s->life;\n\n        int32_t c = life << 1;\n        CLAMPG(c, 255);\n        const RGBA_8888 c1 = M_Gray(c);\n\n        c = (life - (life >> 2)) << 1;\n        CLAMPG(c, 255);\n        const RGBA_8888 c2 = M_Gray(c);\n\n        const int32_t base = ring * 16;\n        for (int32_t quad = 0; quad < 8; quad++) {\n            const int32_t i0 = m_SplashQuadLinks[quad * 4 + 0] + base;\n            const int32_t i1 = m_SplashQuadLinks[quad * 4 + 1] + base;\n            const int32_t i2 = m_SplashQuadLinks[quad * 4 + 2] + base;\n            const int32_t i3 = m_SplashQuadLinks[quad * 4 + 3] + base;\n\n            const XYZ_32 quad_pos[4] = {\n                points[i0],\n                points[i1],\n                points[i3],\n                points[i2],\n            };\n            const RGBA_8888 quad_color[4] = { c1, c1, c2, c2 };\n            OutputSource_PolyFX_StageSpriteQuadWorldDepth(\n                sprite_idx, quad_pos, quad_color, M_SPLASH_Z_DEPTH_ADJUST,\n                DRAW_BLEND_ADD);\n        }\n    }\n}\n\nstatic void M_DrawRipple(\n    const int32_t base_sprite_idx, const FX_WATER_RIPPLE *r)\n{\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n    const int32_t size = do_interp ? (int32_t)LERP(r->prev_size, r->size, ratio)\n                                   : (int32_t)r->size;\n    const int32_t init = do_interp ? (int32_t)LERP(r->prev_init, r->init, ratio)\n                                   : (int32_t)r->init;\n    const int32_t life = do_interp ? (int32_t)LERP(r->prev_life, r->life, ratio)\n                                   : (int32_t)r->life;\n    const int32_t n = size << 2;\n    int32_t sprite_idx = base_sprite_idx + 9;\n    RGBA_8888 color;\n\n    if ((r->flags & 0x10U) != 0U) {\n        if ((r->flags & 0x20U) != 0U) {\n            sprite_idx = base_sprite_idx;\n            if (!M_GetUnderwaterBloodColor(&color, life)) {\n                return;\n            }\n        } else {\n            int32_t c1 = init != 0 ? (init >> 2) : (life >> 2);\n            c1 <<= 3;\n            CLAMPG(c1, 255);\n            color = M_Gray(c1);\n        }\n    } else {\n        int32_t c1 = init != 0 ? (init >> 1) : (life >> 1);\n        c1 <<= 3;\n        CLAMPG(c1, 255);\n        color = M_Gray(c1);\n    }\n\n    // double-sided\n    const XYZ_32 quad_pos[2][4] = {\n        {\n            { r->x - n, r->y, r->z - n },\n            { r->x + n, r->y, r->z - n },\n            { r->x + n, r->y, r->z + n },\n            { r->x - n, r->y, r->z + n },\n        },\n        {\n            { r->x - n, r->y, r->z - n },\n            { r->x - n, r->y, r->z + n },\n            { r->x + n, r->y, r->z + n },\n            { r->x + n, r->y, r->z - n },\n        },\n    };\n    const RGBA_8888 quad_color[4] = { color, color, color, color };\n    OutputSource_PolyFX_StageSpriteQuadWorldDepth(\n        sprite_idx, quad_pos[0], quad_color, M_RIPPLE_Z_DEPTH_ADJUST,\n        DRAW_BLEND_ADD);\n    OutputSource_PolyFX_StageSpriteQuadWorldDepth(\n        sprite_idx, quad_pos[1], quad_color, M_RIPPLE_Z_DEPTH_ADJUST,\n        DRAW_BLEND_ADD);\n}\n\nvoid FX_Water_Draw(void)\n{\n    const OBJECT *const explosion = Object_Get(O_EXPLOSION_1);\n    if (!explosion->loaded) {\n        return;\n    }\n\n    const int32_t base_sprite_idx = explosion->mesh_idx;\n\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Splashes); i++) {\n        const FX_WATER_SPLASH *const splash = &m_Splashes[i];\n        if ((splash->flags & 1U) == 0U) {\n            continue;\n        }\n        M_DrawSplash(base_sprite_idx, splash);\n    }\n\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) {\n        const FX_WATER_RIPPLE *const ripple = &m_Ripples[i];\n        if ((ripple->flags & 1U) == 0U) {\n            continue;\n        }\n        M_DrawRipple(base_sprite_idx, ripple);\n    }\n}\n\nvoid FX_Water_Control(void)\n{\n    if (m_SplashCount > 0) {\n        m_SplashCount--;\n    }\n\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Splashes); i++) {\n        FX_WATER_SPLASH *const splash = &m_Splashes[i];\n        if ((splash->flags & 1U) == 0U) {\n            continue;\n        }\n\n        M_RememberSplash(splash);\n\n        bool set = false;\n        for (int32_t j = 0; j < 48; j++) {\n            FX_WATER_SPLASH_VERT *const v = &splash->v[j];\n            v->wx += v->xv >> 2;\n            v->wy += (int16_t)(v->yv >> 6);\n            v->wz += v->zv >> 2;\n            v->xv -= v->xv >> v->friction;\n            v->zv -= v->zv >> v->friction;\n\n            if ((v->oxv < 0 && v->xv > v->oxv)\n                || (v->oxv > 0 && v->xv < v->oxv)) {\n                v->xv = v->oxv;\n            } else if (\n                (v->ozv < 0 && v->zv > v->ozv)\n                || (v->ozv > 0 && v->zv < v->ozv)) {\n                v->zv = v->ozv;\n            }\n\n            v->yv += (int32_t)v->gravity << 3;\n            CLAMPG(v->yv, 0x10000);\n\n            if (v->wy > 0) {\n                if (j < 16) {\n                    splash->flags |= 4U;\n                } else if (j < 32) {\n                    splash->flags |= 8U;\n                }\n\n                v->wy = 0;\n                set = true;\n            }\n        }\n\n        if (set) {\n            splash->life--;\n            if (splash->life == 0U) {\n                splash->flags = 0U;\n            }\n        }\n    }\n\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(m_Ripples); i++) {\n        FX_WATER_RIPPLE *const ripple = &m_Ripples[i];\n        if ((ripple->flags & 1U) == 0U) {\n            continue;\n        }\n\n        M_RememberRipple(ripple);\n\n        if (ripple->size < 254U) {\n            ripple->size += 2U;\n        }\n\n        if (ripple->init == 0U) {\n            ripple->life -= 2U;\n            if (ripple->life > 250U) {\n                ripple->flags = 0U;\n            }\n        } else if (ripple->init < ripple->life) {\n            ripple->init += 4U;\n            if (ripple->init >= ripple->life) {\n                ripple->init = 0U;\n            }\n        }\n    }\n\n    m_Wibble = (m_Wibble + 4) & 0xFC;\n}\n"
  },
  {
    "path": "src/trx/game/fx/water.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n#include <trx/game/types.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n    int32_t prev_size;\n    int32_t prev_life;\n    int32_t prev_init;\n    uint8_t flags;\n    uint8_t life;\n    uint8_t size;\n    uint8_t init;\n} FX_WATER_RIPPLE;\n\ntypedef struct {\n    int16_t wx;\n    int16_t wy;\n    int16_t wz;\n    int16_t prev_wx;\n    int16_t prev_wy;\n    int16_t prev_wz;\n    int16_t xv;\n    int32_t yv;\n    int16_t zv;\n    int16_t oxv;\n    int16_t ozv;\n    uint8_t friction;\n    uint8_t gravity;\n} FX_WATER_SPLASH_VERT;\n\ntypedef struct {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n    int32_t prev_life;\n    uint8_t flags;\n    uint8_t life;\n    uint8_t pad[2];\n    FX_WATER_SPLASH_VERT v[48];\n} FX_WATER_SPLASH;\n\ntypedef struct {\n    int32_t x;\n    int32_t y;\n    int32_t z;\n    int16_t inner_xz_off;\n    int16_t inner_xz_size;\n    int16_t inner_y_size;\n    int16_t inner_xz_vel;\n    int16_t inner_y_vel;\n    int16_t inner_gravity;\n    int16_t inner_friction;\n    int16_t middle_xz_off;\n    int16_t middle_xz_size;\n    int16_t middle_y_size;\n    int16_t middle_xz_vel;\n    int16_t middle_y_vel;\n    int16_t middle_gravity;\n    int16_t middle_friction;\n    int16_t outer_xz_off;\n    int16_t outer_xz_size;\n    int16_t outer_xz_vel;\n    int16_t outer_friction;\n} FX_WATER_SPLASH_SETUP;\n\nvoid FX_Water_Reset(void);\nvoid FX_Water_Control(void);\nvoid FX_Water_Draw(void);\n\nFX_WATER_RIPPLE *FX_Water_SetupRipple(\n    int32_t x, int32_t y, int32_t z, int32_t size, bool is_still);\nvoid FX_Water_SetupSplash(const FX_WATER_SPLASH_SETUP *setup);\nvoid FX_Water_Splash(const ITEM *item);\nvoid FX_Water_WadeSplash(const ITEM *item, int32_t water_height, int32_t depth);\n\nvoid FX_Water_TriggerUnderwaterBlood(XYZ_32 pos, int32_t size);\nvoid FX_Water_TriggerUnderwaterBloodD(XYZ_32 pos, int32_t size);\n"
  },
  {
    "path": "src/trx/game/fx/water_particles.c",
    "content": "#include <trx/game/fx/water_particles.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/vars.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/viewport.h>\n\n#include <string.h>\n\n#define M_MAX_WATER_PARTICLES 256\n#define M_MAX_WATER_PARTICLES_ALIVE 16\n#define M_SPAWN_DIST_MASK 0xFFF\n#define M_SPAWN_ANGLE_MASK 0x1FFE\n#define M_SPAWN_Y_MASK 0x7FF\n#define M_BASE_Y_OFF (-1024)\n#define M_MIN_SIZE 4.0f\n#define M_MAX_SIZE 16.0f\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_32 prev_pos;\n    uint8_t life;\n    uint8_t yv;\n    int8_t xv;\n    int8_t zv;\n} M_WATER_PARTICLE;\n\nstatic M_WATER_PARTICLE m_WaterParticles[M_MAX_WATER_PARTICLES];\n\nstatic bool M_IsEnabled(void)\n{\n    if (!g_Config.visuals.enable_weather) {\n        return false;\n    }\n\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    return level != nullptr && level->water_particles;\n}\n\nstatic void M_Clear(void)\n{\n    memset(m_WaterParticles, 0, sizeof(m_WaterParticles));\n}\n\nstatic int64_t M_GetViewDepth(const XYZ_32 pos)\n{\n    // clang-format off\n    return\n        g_ViewMatrix._20 * pos.x +\n        g_ViewMatrix._21 * pos.y +\n        g_ViewMatrix._22 * pos.z +\n        g_ViewMatrix._23;\n    // clang-format on\n}\n\nstatic float M_GetFixedScale(const float unit)\n{\n    const float base_w = 640.0f;\n    const float base_h = 480.0f;\n    const float game_w = (float)Viewport_GetWidth(VIEWPORT_GAME);\n    const float game_h = (float)Viewport_GetHeight(VIEWPORT_GAME);\n    const float x = game_w > base_w ? (game_w * unit) / base_w : unit;\n    const float y = game_h > base_h ? (game_h * unit) / base_h : unit;\n    return MIN(x, y);\n}\n\nstatic void M_Spawn(M_WATER_PARTICLE *const particle)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return;\n    }\n\n    const int32_t dist = Random_GetDraw() & M_SPAWN_DIST_MASK;\n    const int32_t angle = (Random_GetDraw() & M_SPAWN_ANGLE_MASK) * 8;\n    particle->pos = XYZ_32_OffsetYaw(lara_item->pos, angle, dist);\n    particle->pos.y += (Random_GetDraw() & M_SPAWN_Y_MASK) + M_BASE_Y_OFF;\n\n    int16_t room_num = NO_ROOM;\n    Room_GetOutsideStatus(particle->pos, &room_num);\n    if (room_num == NO_ROOM || !Room_Get(room_num)->flags.underwater) {\n        particle->pos.x = 0;\n        return;\n    }\n\n    particle->life = (uint8_t)((Random_GetDraw() & 7) + 16);\n    particle->xv = (int8_t)(Random_GetDraw() & 3);\n    if (particle->xv == 2) {\n        particle->xv = -1;\n    }\n\n    particle->yv = (uint8_t)(((Random_GetDraw() & 7) + 8) << 3);\n    particle->zv = (int8_t)(Random_GetDraw() & 3);\n    if (particle->zv == 2) {\n        particle->zv = -1;\n    }\n\n    particle->prev_pos = particle->pos;\n}\n\nvoid FX_WaterParticles_Reset(void)\n{\n    M_Clear();\n}\n\nvoid FX_WaterParticles_Control(void)\n{\n    if (!M_IsEnabled()) {\n        M_Clear();\n        return;\n    }\n\n    int32_t num_alive = 0;\n    for (int32_t i = 0; i < M_MAX_WATER_PARTICLES; i++) {\n        M_WATER_PARTICLE *const particle = &m_WaterParticles[i];\n\n        if (particle->pos.x == 0 && num_alive < M_MAX_WATER_PARTICLES_ALIVE) {\n            num_alive++;\n            M_Spawn(particle);\n        }\n\n        if (particle->pos.x == 0) {\n            continue;\n        }\n\n        particle->prev_pos = particle->pos;\n        particle->pos.x += particle->xv;\n        particle->pos.y += (particle->yv & 0xF8) >> 6;\n        particle->pos.z += particle->zv;\n\n        if (particle->life == 0) {\n            particle->pos.x = 0;\n            continue;\n        }\n\n        particle->life--;\n        if ((particle->yv & 7) != 7) {\n            particle->yv++;\n        }\n    }\n}\n\nvoid FX_WaterParticles_Draw(void)\n{\n    if (!M_IsEnabled()) {\n        return;\n    }\n\n    const OBJECT *const obj = Object_Get(O_EXPLOSION_1);\n    if (obj == nullptr || !obj->loaded) {\n        return;\n    }\n\n    const int32_t sprite_idx = obj->mesh_idx + 17;\n    if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) {\n        return;\n    }\n\n    const int32_t atlas_idx = Output_Textures_GetSpriteUVWIndex(sprite_idx, 0);\n    const OUTPUT_TEXTURE_SIZE atlas_size =\n        Output_Textures_GetAtlasSize(atlas_idx / 4);\n    const OUTPUT_TEXTURE_SIZE tri_size[3] = { atlas_size, atlas_size,\n                                              atlas_size };\n    const OUTPUT_UVW tri_uvw[3] = {\n        Output_Textures_GetUVW(\n            Output_Textures_GetSpriteUVWIndex(sprite_idx, 1)),\n        Output_Textures_GetUVW(\n            Output_Textures_GetSpriteUVWIndex(sprite_idx, 2)),\n        Output_Textures_GetUVW(\n            Output_Textures_GetSpriteUVWIndex(sprite_idx, 3)),\n    };\n\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n\n    for (int32_t i = 0; i < M_MAX_WATER_PARTICLES; i++) {\n        M_WATER_PARTICLE *const particle = &m_WaterParticles[i];\n        if (particle->pos.x == 0) {\n            continue;\n        }\n\n        const XYZ_32 center = do_interp\n            ? (XYZ_32) {\n                  .x = (int32_t)LERP(\n                      particle->prev_pos.x, particle->pos.x, ratio),\n                  .y = (int32_t)LERP(\n                      particle->prev_pos.y, particle->pos.y, ratio),\n                  .z = (int32_t)LERP(\n                      particle->prev_pos.z, particle->pos.z, ratio),\n              }\n            : particle->pos;\n\n        const int64_t zv = M_GetViewDepth(center);\n        const int64_t near_z = Output_GetNearZ();\n        const int64_t far_z = Output_GetFarZ();\n        const int32_t vpos_z = (int32_t)(zv >> W2V_SHIFT);\n\n        if (vpos_z < 128) {\n            if (particle->life > 16) {\n                particle->life = 16;\n            }\n            continue;\n        }\n\n        if (zv <= near_z || zv >= far_z || g_PhdPersp <= 0) {\n            continue;\n        }\n\n        float size = (float)((g_PhdPersp * (particle->yv >> 3)) / vpos_z);\n        CLAMP(size, M_MIN_SIZE, M_MAX_SIZE);\n        size /= 3.0f;\n        size = M_GetFixedScale(size) / 2.0f;\n\n        uint32_t c;\n        if ((particle->yv & 7) < 7) {\n            c = (uint32_t)(particle->yv & 7);\n        } else if (particle->life > 18) {\n            c = 15;\n        } else {\n            c = particle->life;\n        }\n        c <<= 2;\n        CLAMPG(c, 255);\n\n        const RGBA_8888 color = { c, c, c, 255 };\n        const RGBA_8888 colors[3] = { color, color, color };\n        const XYZ_32 world_pos[3] = { center, center, center };\n        const float disp[3][2] = {\n            { size, -2.0f * size },\n            { size, size },\n            { -2.0f * size, size },\n        };\n\n        OutputSource_PolyFX_StageTriExtUV(\n            world_pos, tri_uvw, tri_size, disp, colors,\n            VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD\n                | VERT_ABS_SPRITE,\n            DRAW_BLEND_ADD);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/fx/water_particles.h",
    "content": "#pragma once\n\nvoid FX_WaterParticles_Reset(void);\nvoid FX_WaterParticles_Control(void);\nvoid FX_WaterParticles_Draw(void);\n"
  },
  {
    "path": "src/trx/game/fx/weather.c",
    "content": "#include <trx/game/fx/weather.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/game/state.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/vars.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/viewport.h>\n\n#include <string.h>\n\n#define M_MAX_WEATHER 256\n#define M_MAX_WEATHER_ALIVE 16\n\n#define M_RAIN_MAX_DISTANCE 6000\n#define M_RAIN_BASE_Y_OFF (-1024)\n#define M_RAIN_BASE_YV 16\n#define M_RAIN_YV_RND_MASK 7\n#define M_RAIN_SPAWN_DIST_MASK 0xFFF\n#define M_RAIN_SPAWN_ANGLE_MASK 0x1FFE\n#define M_RAIN_Y_RND_MASK 0x7FF\n#define M_RAIN_LIFE_BASE 88\n\n#define M_SNOW_LIFE_BASE 96\n#define M_SNOW_YV_MIN 8\n#define M_SNOW_YV_RANGE 24\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_32 prev_pos;\n    int32_t prev_yv;\n    int8_t xv;\n    uint8_t yv;\n    int8_t zv;\n    uint8_t life;\n} M_RAINDROP;\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_32 prev_pos;\n    bool stopped;\n    int32_t prev_yv;\n    int32_t prev_life;\n    int8_t xv;\n    uint8_t yv;\n    int8_t zv;\n    uint8_t life;\n} M_SNOWFLAKE;\n\nstatic M_RAINDROP m_Raindrops[M_MAX_WEATHER];\nstatic M_SNOWFLAKE m_Snowflakes[M_MAX_WEATHER];\nstatic WEATHER_TYPE m_WeatherType = WEATHER_NONE;\n\nstatic void M_ClearWeather(void)\n{\n    memset(m_Raindrops, 0, sizeof(m_Raindrops));\n    memset(m_Snowflakes, 0, sizeof(m_Snowflakes));\n}\n\nstatic int64_t M_GetViewDepth(const XYZ_32 pos)\n{\n    // clang-format off\n    return\n        g_ViewMatrix._20 * pos.x +\n        g_ViewMatrix._21 * pos.y +\n        g_ViewMatrix._22 * pos.z +\n        g_ViewMatrix._23;\n    // clang-format on\n}\n\nstatic bool M_SpawnParticle(XYZ_32 *const pos)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    XYZ_32 lara_pos = {};\n    if (!Lara_GetMeshPos(LM_HIPS, &lara_pos)) {\n        lara_pos = lara_item->pos;\n    }\n\n    const int32_t dist = Random_GetDraw() & M_RAIN_SPAWN_DIST_MASK;\n    const int32_t angle = (Random_GetDraw() & M_RAIN_SPAWN_ANGLE_MASK) * 8;\n    *pos = XYZ_32_OffsetYaw(lara_pos, angle, dist);\n    pos->y += M_RAIN_BASE_Y_OFF - (Random_GetDraw() & M_RAIN_Y_RND_MASK);\n\n    int16_t room_num = NO_ROOM;\n    if (Room_GetOutsideStatus(*pos, &room_num) != 1) {\n        pos->x = 0;\n        return false;\n    }\n\n    return true;\n}\n\nstatic void M_SpawnRainDrop(M_RAINDROP *const drop)\n{\n    if (!M_SpawnParticle(&drop->pos)) {\n        return;\n    }\n\n    drop->yv =\n        (uint8_t)((Random_GetDraw() & M_RAIN_YV_RND_MASK) + M_RAIN_BASE_YV);\n    drop->xv = (int8_t)((Random_GetDraw() & 7) - 4);\n    drop->zv = (int8_t)((Random_GetDraw() & 7) - 4);\n    drop->life = (uint8_t)(M_RAIN_LIFE_BASE - ((int32_t)drop->yv << 1));\n    drop->prev_pos = drop->pos;\n    drop->prev_yv = drop->yv;\n}\n\nstatic void M_UpdateRain(void)\n{\n    const XZ_32 wind = Sparks_GetSmokeWind();\n\n    int32_t num_alive = 0;\n    for (int32_t i = 0; i < M_MAX_WEATHER; i++) {\n        M_RAINDROP *const drop = &m_Raindrops[i];\n\n        if (drop->pos.x == 0 && num_alive < M_MAX_WEATHER_ALIVE) {\n            num_alive++;\n            M_SpawnRainDrop(drop);\n        }\n\n        if (drop->pos.x == 0) {\n            continue;\n        }\n\n        drop->prev_pos = drop->pos;\n        drop->prev_yv = drop->yv;\n\n        int16_t room_num = NO_ROOM;\n        const int32_t outside = Room_GetOutsideStatus(drop->pos, &room_num);\n        if (outside == -2\n            || (room_num != NO_ROOM && Room_Get(room_num)->flags.underwater)\n            || drop->life > 240\n            || ABS(g_Camera.pos.x - drop->pos.x) > M_RAIN_MAX_DISTANCE\n            || ABS(g_Camera.pos.z - drop->pos.z) > M_RAIN_MAX_DISTANCE) {\n            drop->pos.x = 0;\n            continue;\n        }\n\n        drop->pos.x += (int32_t)drop->xv + 4 * wind.x;\n        drop->pos.y += (int32_t)drop->yv * 8;\n        drop->pos.z += (int32_t)drop->zv + 4 * wind.z;\n\n        int32_t rnd = Random_GetDraw();\n        if ((rnd & 3) != 3) {\n            drop->xv += (int8_t)((rnd & 3) - 1);\n            if (drop->xv < -4) {\n                drop->xv = -4;\n            } else if (drop->xv > 4) {\n                drop->xv = 4;\n            }\n        }\n\n        rnd = (rnd >> 2) & 3;\n        if (rnd != 3) {\n            drop->zv += (int8_t)(rnd - 1);\n            if (drop->zv < -4) {\n                drop->zv = -4;\n            } else if (drop->zv > 4) {\n                drop->zv = 4;\n            }\n        }\n\n        drop->life -= 2;\n        if (drop->life > 240) {\n            drop->pos.x = 0;\n        }\n    }\n}\n\nstatic void M_DrawRain(void)\n{\n    const XZ_32 wind = Sparks_GetSmokeWind();\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n\n    for (int32_t i = 0; i < M_MAX_WEATHER; i++) {\n        const M_RAINDROP *const drop = &m_Raindrops[i];\n        if (drop->pos.x == 0) {\n            continue;\n        }\n\n        const int32_t yv = do_interp\n            ? (int32_t)LERP(drop->prev_yv, drop->yv, ratio)\n            : (int32_t)drop->yv;\n        const XYZ_32 to = do_interp\n            ? (XYZ_32) {\n                  .x = (int32_t)LERP(drop->prev_pos.x, drop->pos.x, ratio),\n                  .y = (int32_t)LERP(drop->prev_pos.y, drop->pos.y, ratio),\n                  .z = (int32_t)LERP(drop->prev_pos.z, drop->pos.z, ratio),\n              }\n            : drop->pos;\n        const XYZ_32 from = {\n            to.x - (wind.x * 4),\n            to.y - (yv * 8),\n            to.z - (wind.z * 4),\n        };\n\n        const RGBA_8888 from_color = { 0, 0, 0x20, 0x00 };\n        const RGBA_8888 to_color = { 0x30, 0x40, 0x60, 0x80 };\n        OutputSource_PolyFX_StageLineSegment(\n            from, from_color, to, to_color, 1.0f, DRAW_BLEND);\n    }\n}\n\nstatic void M_SpawnSnowflake(M_SNOWFLAKE *const snow)\n{\n    if (!M_SpawnParticle(&snow->pos)) {\n        return;\n    }\n\n    snow->stopped = false;\n    snow->xv = (int8_t)((Random_GetDraw() & 7) - 4);\n    snow->yv =\n        (uint8_t)(((Random_GetDraw() % M_SNOW_YV_RANGE) + M_SNOW_YV_MIN) * 8);\n    snow->zv = (int8_t)((Random_GetDraw() & 7) - 4);\n    snow->life = (uint8_t)(M_SNOW_LIFE_BASE - ((int32_t)snow->yv << 1));\n    snow->prev_pos = snow->pos;\n    snow->prev_yv = snow->yv;\n    snow->prev_life = snow->life;\n}\n\nstatic void M_UpdateSnow(void)\n{\n    int32_t num_alive = 0;\n    for (int32_t i = 0; i < M_MAX_WEATHER; i++) {\n        M_SNOWFLAKE *const snow = &m_Snowflakes[i];\n\n        if (snow->pos.x == 0 && num_alive < M_MAX_WEATHER_ALIVE) {\n            num_alive++;\n            M_SpawnSnowflake(snow);\n        }\n\n        if (snow->pos.x == 0) {\n            continue;\n        }\n\n        snow->prev_pos = snow->pos;\n        snow->prev_yv = snow->yv;\n        snow->prev_life = snow->life;\n\n        const XYZ_32 old_pos = snow->pos;\n\n        int16_t room_num = NO_ROOM;\n        int32_t outside = 1;\n        if (!snow->stopped) {\n            snow->pos.x += snow->xv;\n            snow->pos.y += (snow->yv & 0xF8) >> 2;\n            snow->pos.z += snow->zv;\n\n            outside = Room_GetOutsideStatus(snow->pos, &room_num);\n            if (outside == -3) {\n                snow->pos.x = 0;\n                continue;\n            }\n\n            if (outside == -2\n                || (room_num != NO_ROOM\n                    && Room_Get(room_num)->flags.underwater)) {\n                snow->stopped = true;\n                snow->pos = old_pos;\n\n                if (snow->life > 16) {\n                    snow->life = 16;\n                }\n                if (snow->yv > 16) {\n                    snow->yv -= 16;\n                }\n            }\n        }\n\n        if (snow->life == 0) {\n            snow->pos.x = 0;\n            continue;\n        }\n\n        if ((ABS(g_Camera.pos.x - snow->pos.x) > M_RAIN_MAX_DISTANCE\n             || ABS(g_Camera.pos.z - snow->pos.z) > M_RAIN_MAX_DISTANCE)\n            && snow->life > 16) {\n            snow->life = 16;\n        }\n\n        const XZ_32 wind = Sparks_GetSmokeWind();\n\n        if (snow->xv < (wind.x * 2)) {\n            snow->xv++;\n        } else if (snow->xv > (wind.x * 2)) {\n            snow->xv--;\n        }\n\n        if (snow->zv < (wind.z * 2)) {\n            snow->zv++;\n        } else if (snow->zv > (wind.z * 2)) {\n            snow->zv--;\n        }\n\n        snow->life -= 2;\n        if ((snow->yv & 7) != 7) {\n            snow->yv++;\n        }\n    }\n}\n\nstatic void M_DrawSnow(void)\n{\n    const OBJECT *const obj = Object_Get(O_SNOWFLAKE);\n    if (!obj->loaded) {\n        return;\n    }\n\n    const int32_t sprite_idx = obj->mesh_idx;\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n    for (int32_t i = 0; i < M_MAX_WEATHER; i++) {\n        M_SNOWFLAKE *const snow = &m_Snowflakes[i];\n        if (snow->pos.x == 0) {\n            continue;\n        }\n\n        const XYZ_32 center = do_interp\n            ? (XYZ_32) {\n                  .x = (int32_t)LERP(snow->prev_pos.x, snow->pos.x, ratio),\n                  .y = (int32_t)LERP(snow->prev_pos.y, snow->pos.y, ratio),\n                  .z = (int32_t)LERP(snow->prev_pos.z, snow->pos.z, ratio),\n              }\n            : snow->pos;\n        const int64_t zv = M_GetViewDepth(center);\n        const int64_t near_z = Output_GetNearZ();\n        const int64_t far_z = Output_GetFarZ();\n        const int32_t vpos_z = (int32_t)(zv >> W2V_SHIFT);\n\n        if (vpos_z < 128 && snow->life > 16) {\n            snow->life = 16;\n        }\n\n        if (zv <= near_z || zv >= far_z) {\n            continue;\n        }\n\n        if (vpos_z < 128 || g_PhdPersp <= 0) {\n            continue;\n        }\n\n        const int32_t yv = do_interp\n            ? (int32_t)LERP(snow->prev_yv, snow->yv, ratio)\n            : (int32_t)snow->yv;\n        const int32_t life = do_interp\n            ? (int32_t)LERP(snow->prev_life, snow->life, ratio)\n            : (int32_t)snow->life;\n\n        const int32_t game_w = Viewport_GetWidth(VIEWPORT_GAME);\n        const int32_t game_h = Viewport_GetHeight(VIEWPORT_GAME);\n        const int32_t ui_w = Viewport_GetWidth(VIEWPORT_UI);\n        const int32_t ui_h = Viewport_GetHeight(VIEWPORT_UI);\n\n        const XYZ_32 world_pos[4] = { center, center, center, center };\n        const float s = 8.0f;\n        const float disp[4][2] = {\n            { -s, -s },\n            { s, -s },\n            { s, s },\n            { -s, s },\n        };\n\n        uint32_t c;\n        if ((yv & 7) < 7) {\n            c = (uint32_t)(yv & 7);\n        } else if (life > 18) {\n            c = 15;\n        } else {\n            c = (uint32_t)life;\n        }\n        c <<= 3;\n        CLAMPG(c, 255);\n\n        {\n            const int32_t fog_start = Output_GetFogStart();\n            const int32_t fog_end = Output_GetFogEnd();\n            float fade = 1.0f;\n            if (fog_end > fog_start && vpos_z > fog_start) {\n                float t = (vpos_z - fog_start) / (float)(fog_end - fog_start);\n                CLAMP(t, 0.0f, 1.0f);\n                fade = 1.0f - t;\n            }\n            c = (uint32_t)(c * fade);\n        }\n\n        const RGBA_8888 color = { c, c, c, 255 };\n        const RGBA_8888 colors[4] = { color, color, color, color };\n\n        OutputSource_PolyFX_StageQuadExt(\n            sprite_idx, world_pos, disp, colors,\n            VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD\n                | VERT_ABS_SPRITE,\n            DRAW_BLEND_ADD);\n    }\n}\n\nvoid FX_Weather_Reset(void)\n{\n    M_ClearWeather();\n}\n\nWEATHER_TYPE FX_Weather_GetWeather(void)\n{\n    return m_WeatherType;\n}\n\nvoid FX_Weather_SetWeather(const WEATHER_TYPE weather_type)\n{\n    m_WeatherType = weather_type;\n}\n\nvoid FX_Weather_Control(void)\n{\n    if (!g_Config.visuals.enable_weather) {\n        return;\n    }\n    const WEATHER_TYPE weather_type = m_WeatherType;\n    if (weather_type == WEATHER_RAIN) {\n        M_UpdateRain();\n    } else if (weather_type == WEATHER_SNOW) {\n        M_UpdateSnow();\n    }\n}\n\nvoid FX_Weather_Draw(void)\n{\n    if (!g_Config.visuals.enable_weather) {\n        return;\n    }\n    switch (m_WeatherType) {\n    case WEATHER_RAIN:\n        M_DrawRain();\n        break;\n    case WEATHER_SNOW:\n        M_DrawSnow();\n        break;\n    default:\n        break;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/fx/weather.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef enum {\n    WEATHER_NONE = 0,\n    WEATHER_RAIN,\n    WEATHER_SNOW,\n} WEATHER_TYPE;\n\nvoid FX_Weather_Reset(void);\nvoid FX_Weather_Control(void);\nvoid FX_Weather_Draw(void);\nWEATHER_TYPE FX_Weather_GetWeather(void);\nvoid FX_Weather_SetWeather(WEATHER_TYPE weather_type);\n"
  },
  {
    "path": "src/trx/game/fx.h",
    "content": "#pragma once\n\n#include <trx/game/fx/common.h>\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/fx/footprint.h>\n#include <trx/game/fx/gun_flash.h>\n#include <trx/game/fx/laser.h>\n#include <trx/game/fx/wake.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/fx/water_particles.h>\n#include <trx/game/fx/weather.h>\n"
  },
  {
    "path": "src/trx/game/game/control.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/clock.h>\n#include <trx/game/console.h>\n#include <trx/game/creature.h>\n#include <trx/game/demo.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gym.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/lua/events.h>\n#include <trx/game/music.h>\n#include <trx/game/option/passport.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_FRAME_BUFFER(key)                                                    \\\n    do {                                                                       \\\n        Shell_ProcessEvents();                                                 \\\n        Output_BeginScene();                                                   \\\n        Game_Draw(true);                                                       \\\n        Input_Update();                                                        \\\n        Output_EndScene();                                                     \\\n        Output_FlipScreen();                                                   \\\n        Clock_WaitTick();                                                      \\\n    } while (g_Input.key);\n\nint32_t g_OverlayFlag = 0;\n\nbool Game_Start(const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx)\n{\n    Game_SetCurrentLevel(level);\n\n    g_OverlayFlag = 1;\n    Camera_Initialise();\n    Interpolation_Remember();\n\n    Sound_StopAll();\n    const bool is_cutscene = level->type == GFL_CUTSCENE;\n    if (level->music_track != MX_INACTIVE\n        && (is_cutscene || Music_GetCurrentLoopedTrack() == MX_INACTIVE)) {\n        Music_Play_Direct(\n            level->music_track, is_cutscene ? MPM_ONCE : MPM_LOOP);\n    }\n\n    const LUA_EVENT_ARG args[] = {\n        { .type = LUA_EVENT_ARG_INT32, .value = { .i32 = level->num } },\n        { .type = LUA_EVENT_ARG_BOOL, .value = { .b = seq_ctx == GFSC_SAVED } },\n    };\n    Lua_FireEventEx(LUA_EVENT_GAME_START, args, 2);\n    return true;\n}\n\nvoid Game_End(void)\n{\n    Savegame_PersistGameToCurrentInfo(Game_GetCurrentLevel());\n    Music_Stop();\n}\n\nGF_COMMAND Game_Control(const bool demo_mode)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Passport.ask_for_save && !lara->extra_anim) {\n        // ask for a save at the start of a level for the save crystals mode\n        const GF_COMMAND gf_cmd = GF_ShowInventory(INV_SAVE_CRYSTAL_MODE);\n        g_Passport.ask_for_save = false;\n        if (gf_cmd.action != GF_NOOP) {\n            return gf_cmd;\n        }\n    }\n\n    Interpolation_Remember();\n    if (!Game_IsInGym() || Gym_TrackManager_IsTimerActive(GYM_TRACK_ASSAULT)\n        || Gym_TrackManager_IsTimerActive(GYM_TRACK_QUAD)\n        || !Object_Get(O_ASSAULT_DIGITS)->loaded) {\n        Stats_UpdateTimer();\n    }\n    if (Game_IsInGym()) {\n        Gym_Control();\n    }\n    if (g_Config.flow.cheat_keys) {\n        Lara_Cheat_CheckKeys();\n    }\n\n    if (Game_IsLevelComplete()) {\n        Sound_StopAll();\n        Music_Stop();\n        return (GF_COMMAND) { .action = GF_LEVEL_COMPLETE };\n    }\n\n    Input_Update();\n    Shell_ProcessInput();\n    if (g_InputDB.toggle_photo_mode) {\n        return GF_EnterPhotoMode();\n    } else if (g_InputDB.pause && lara->death_timer == 0) {\n        return GF_PauseGame();\n    }\n    if (demo_mode) {\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n        if (!Demo_UpdateInput()) {\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n    }\n    Game_ProcessInput();\n\n    if ((g_InputDB.quick_save || g_InputDB.quick_load) && !demo_mode\n        && !g_Config.flow.load_save_disabled) {\n        bool quick_handled = false;\n        if (g_InputDB.quick_save && !lara->extra_anim\n            && lara->death_timer == 0) {\n            const SAVEGAME_SLOT_REF slot = Savegame_GetNextQuickSlot();\n            if (!Savegame_IsValidSlotRef(slot)) {\n                Console_LogError(\n                    \"%s\", GS(\"general/osd/quick_save_fail_no_slots\"));\n            } else if (Savegame_Save(slot)) {\n                Console_Log(\"%s\", GS(\"general/osd/quick_save\"));\n            }\n            quick_handled = true;\n        } else if (g_InputDB.quick_load) {\n            const SAVEGAME_SLOT_REF slot = Savegame_GetBoundSlot();\n            if (!Savegame_IsValidSlotRef(slot)) {\n                Console_LogError(\n                    \"%s\", GS(\"general/osd/quick_load_fail_no_bound_slot\"));\n            } else if (Savegame_IsSlotFree(slot)) {\n                Console_LogError(\n                    \"%s\",\n                    GS(\"general/osd/quick_load_fail_unavailable_bound_slot\"));\n            } else {\n                if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) {\n                    const int32_t visual_index =\n                        Savegame_QuickToVisualIndex(slot);\n                    Console_Log(GS(\"general/osd/quick_load\"), visual_index + 1);\n                } else {\n                    Console_Log(GS(\"general/osd/load_game\"), slot.index + 1);\n                }\n                return (GF_COMMAND) {\n                    .action = GF_START_SAVED_GAME,\n                    .param = Savegame_SlotToParam(slot),\n                };\n            }\n            quick_handled = true;\n        }\n\n        if (quick_handled) {\n            // Prevent mixed bindings (quick + normal save/load on same key)\n            // from also opening the passport save/load flow.\n            g_Input.save = false;\n            g_Input.load = false;\n            g_InputDB.save = false;\n            g_InputDB.load = false;\n        }\n    }\n\n    if (lara->death_timer > DEATH_WAIT\n        || (lara->death_timer > DEATH_WAIT_INPUT\n            && (g_InputDB.menu_confirm || g_InputDB.menu_back)\n            && !g_Input.fly_cheat)\n        || g_OverlayFlag == 2) {\n        if (demo_mode || (g_TRVersion >= 2 && Game_IsInGym())) {\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n        if (g_OverlayFlag == 2) {\n            g_OverlayFlag = 1;\n            return GF_ShowInventory(INV_DEATH_MODE);\n        } else {\n            g_OverlayFlag = 2;\n        }\n    }\n\n    if ((g_InputDB.option || g_InputDB.load || g_InputDB.save\n         || g_OverlayFlag <= 0)\n        && lara->death_timer == 0 && !lara->extra_anim) {\n        if (g_TRVersion == 1 && g_Camera.type == CAM_CINEMATIC) {\n            g_OverlayFlag = 0;\n        } else if (g_OverlayFlag > 0) {\n            if (g_Config.flow.lockout_option_ring\n                && g_Config.flow.load_save_disabled) {\n                g_OverlayFlag = 0;\n            } else if (g_Input.save) {\n                g_OverlayFlag = -2;\n            } else if (g_Input.load) {\n                g_OverlayFlag = -1;\n            } else {\n                g_OverlayFlag = 0;\n            }\n        } else {\n            GF_COMMAND gf_cmd;\n            if (g_OverlayFlag == -1) {\n                gf_cmd = GF_ShowInventory(INV_LOAD_MODE);\n            } else if (g_OverlayFlag == -2) {\n                gf_cmd = GF_ShowInventory(INV_SAVE_MODE);\n            } else {\n                gf_cmd = GF_ShowInventory(INV_GAME_MODE);\n            }\n            g_OverlayFlag = 1;\n            if (gf_cmd.action != GF_NOOP) {\n                return gf_cmd;\n            }\n        }\n    }\n\n    Output_ResetDynamicLights();\n\n    Sound_ResetAmbient();\n    Item_Control();\n    Effect_Control();\n    Sparks_Control();\n\n    Lara_Control();\n    FX_Control();\n    Lara_Hair_Control(false);\n\n    Camera_Update();\n    ItemAction_RunActive();\n    Sound_UpdateEffects();\n    Overlay_Animate(1);\n    Output_AnimateTextures(1);\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nvoid Game_ProcessInput(void)\n{\n    if (GF_GetCurrentLevel()->type == GFL_DEMO) {\n        return;\n    }\n\n    if (g_InputDB.use_small_medi && Inv_RequestItem(O_SMALL_MEDIPACK_OPTION)) {\n        Lara_UseItem(O_SMALL_MEDIPACK_OPTION);\n    }\n    if (g_InputDB.use_big_medi && Inv_RequestItem(O_LARGE_MEDIPACK_OPTION)) {\n        Lara_UseItem(O_LARGE_MEDIPACK_OPTION);\n    }\n\n    if (g_Config.input.enable_buffering_func_keys && Game_IsPlaying()) {\n        if (g_Input.toggle_bilinear_filter) {\n            M_FRAME_BUFFER(toggle_bilinear_filter);\n        } else if (g_Input.toggle_trapezoid_filter) {\n            M_FRAME_BUFFER(toggle_trapezoid_filter);\n        } else if (g_Input.toggle_fps_counter) {\n            M_FRAME_BUFFER(toggle_fps_counter);\n        }\n    }\n\n    if (g_InputDB.toggle_ui) {\n        UI_ToggleState(&g_Config.ui.enable_game_ui);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/game/control.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\nbool Game_Start(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx);\nvoid Game_End(void);\n\nGF_COMMAND Game_Control(bool demo_mode);\n\nvoid Game_ProcessInput(void);\n"
  },
  {
    "path": "src/trx/game/game/draw.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n\nvoid Game_Draw(const bool draw_overlay)\n{\n    Interpolation_Interpolate();\n    Camera_Apply();\n    Room_DrawAllRooms(g_Camera.interp.room_num, g_Camera.target.room_num);\n    if (draw_overlay) {\n        Overlay_DrawGameInfo();\n    }\n    SceneCompositor_Flush();\n    if (g_Config.visuals.enable_reflections) {\n        Output_Textures_UpdateEnvironmentMap();\n    }\n}\n"
  },
  {
    "path": "src/trx/game/game/draw.h",
    "content": "#pragma once\n\nvoid Game_Draw(bool draw_overlay);\n"
  },
  {
    "path": "src/trx/game/game/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    // clang-format off\n    GBF_NONE     = 0,\n    GBF_NGPLUS   = 1 << 0,\n    GBF_JAPANESE = 1 << 1,\n    // clang=format on\n} GAME_BONUS_FLAG;\n"
  },
  {
    "path": "src/trx/game/game/state.c",
    "content": "#include <trx/game/game/state.h>\n\n#include <trx/game/fmv.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n\nstatic bool m_IsPlaying = false;\nstatic const GF_LEVEL *m_CurrentLevel = nullptr;\nstatic GAME_BONUS_FLAG m_BonusFlag = GBF_NONE;\nstatic bool m_IsLevelComplete = false;\n\nvoid Game_SetIsPlaying(const bool is_playing)\n{\n    m_IsPlaying = is_playing;\n    Random_FreezeDraw(!is_playing);\n}\n\nbool Game_IsPlaying(void)\n{\n    return m_IsPlaying;\n}\n\nconst GF_LEVEL *Game_GetCurrentLevel(void)\n{\n    return m_CurrentLevel;\n}\n\nvoid Game_SetCurrentLevel(const GF_LEVEL *const level)\n{\n    m_CurrentLevel = level;\n}\n\nbool Game_IsInGym(void)\n{\n    const GF_LEVEL *const current_level = GF_GetCurrentLevel();\n    return current_level != nullptr && current_level->type == GFL_GYM;\n}\n\nbool Game_IsLoaded(void)\n{\n    if (FMV_IsPlaying()) {\n        return false;\n    }\n    const GF_LEVEL *const current_level = GF_GetCurrentLevel();\n    if (current_level == nullptr || current_level->type == GFL_TITLE) {\n        return false;\n    }\n    return true;\n}\n\nbool Game_IsPlayable(void)\n{\n    if (FMV_IsPlaying()) {\n        return false;\n    }\n\n    const GF_LEVEL *const current_level = GF_GetCurrentLevel();\n    if (current_level == nullptr || current_level->type == GFL_TITLE\n        || current_level->type == GFL_DEMO\n        || current_level->type == GFL_CUTSCENE) {\n        return false;\n    }\n\n    if (!Object_Get(O_LARA)->loaded || Lara_GetItem() == nullptr\n        || !Lara_IsControllable()) {\n        return false;\n    }\n\n    return true;\n}\n\nGAME_BONUS_FLAG Game_GetBonusFlag(void)\n{\n    return m_BonusFlag;\n}\n\nvoid Game_SetBonusFlag(const GAME_BONUS_FLAG flag)\n{\n    m_BonusFlag = flag;\n}\n\nbool Game_IsBonusFlagSet(const GAME_BONUS_FLAG flag)\n{\n    return (m_BonusFlag & flag) != 0;\n}\n\nvoid Game_SetIsLevelComplete(const bool is_complete)\n{\n    m_IsLevelComplete = is_complete;\n}\n\nbool Game_IsLevelComplete(void)\n{\n    return m_IsLevelComplete;\n}\n"
  },
  {
    "path": "src/trx/game/game/state.h",
    "content": "#pragma once\n\n#include <trx/game/game/enum.h>\n#include <trx/game/game_flow.h>\n\nextern int32_t g_OverlayFlag;\n\n// Sets the game's playing state, which in turn toggles random draw lock, and\n// certain overlay displays/animations, such as bars and pickups.\nvoid Game_SetIsPlaying(bool is_playing);\n\n// Returns true if the game is in a playing state - i.e. not suspended, such as\n// during pause, photo mode, or while the inventory is open.\nbool Game_IsPlaying(void);\n\nconst GF_LEVEL *Game_GetCurrentLevel(void);\nvoid Game_SetCurrentLevel(const GF_LEVEL *level);\n\nbool Game_IsInGym(void);\n\n// Returns true if an FMV is not playing and the current level is not the title.\nbool Game_IsLoaded(void);\n\n// Returns true if an FMV is not playing, if the level type is neither the\n// title, a demo or a cutscene, and if Lara is loaded and controllable.\nbool Game_IsPlayable(void);\n\nGAME_BONUS_FLAG Game_GetBonusFlag(void);\nvoid Game_SetBonusFlag(GAME_BONUS_FLAG flag);\nbool Game_IsBonusFlagSet(GAME_BONUS_FLAG flag);\n\nvoid Game_SetIsLevelComplete(bool is_complete);\nbool Game_IsLevelComplete(void);\n"
  },
  {
    "path": "src/trx/game/game.h",
    "content": "#pragma once\n\n#include <trx/game/game/control.h>\n#include <trx/game/game/draw.h>\n#include <trx/game/game/enum.h>\n#include <trx/game/game/state.h>\n"
  },
  {
    "path": "src/trx/game/game_buf.c",
    "content": "#include <trx/game/game_buf.h>\n\n#include <trx/core/enum_map.h>\n#include <trx/core/memory.h>\n\nstatic MEMORY_ARENA_ALLOCATOR m_Allocator[GBUF_NUM_MALLOC_TYPES] = {};\n\nvoid GameBuf_Init(void)\n{\n    for (int32_t i = 0; i < GBUF_NUM_MALLOC_TYPES; i++) {\n        m_Allocator[i].default_chunk_size = 2048;\n    }\n}\n\nvoid GameBuf_Reset(void)\n{\n    for (int32_t i = 0; i < GBUF_NUM_MALLOC_TYPES; i++) {\n        Memory_ArenaReset(&m_Allocator[i]);\n    }\n}\n\nvoid GameBuf_ResetSingle(const GAME_BUFFER buffer)\n{\n    Memory_ArenaReset(&m_Allocator[buffer]);\n}\n\nvoid GameBuf_Shutdown(void)\n{\n    for (int32_t i = 0; i < GBUF_NUM_MALLOC_TYPES; i++) {\n        Memory_ArenaFree(&m_Allocator[i]);\n    }\n}\n\nvoid *GameBuf_Alloc(const size_t alloc_size, const GAME_BUFFER buffer)\n{\n    const size_t aligned_size = Memory_Align(alloc_size);\n    return Memory_ArenaAlloc(&m_Allocator[buffer], aligned_size);\n}\n"
  },
  {
    "path": "src/trx/game/game_buf.h",
    "content": "#pragma once\n\n#include <stddef.h>\n\n// Internal game memory manager using an arena allocator. Memory is allocated\n// in discrete chunks, with each allocation request served via pointer\n// arithmetic within the active chunk. When a request exceeds the current\n// chunk's capacity, a new chunk is allocated to continue servicing\n// allocations. This design offers very fast allocation speeds, but individual\n// blocks cannot be freed – only the entire arena can be reset when needed. For\n// more granular memory management, use Memory_Alloc / Memory_Free.\n\ntypedef enum {\n    // clang-format off\n    GBUF_TEXTURE_PAGES,\n    GBUF_PALETTES,\n    GBUF_OBJECT_TEXTURES,\n    GBUF_SPRITE_TEXTURES,\n    GBUF_STATIC_OBJECTS_3D,\n    GBUF_STATIC_OBJECTS_2D,\n    GBUF_MESH_POINTERS,\n    GBUF_MESHES,\n    GBUF_ANIMS,\n    GBUF_ANIM_CHANGES,\n    GBUF_ANIM_RANGES,\n    GBUF_ANIM_COMMANDS,\n    GBUF_ANIM_BONES,\n    GBUF_ANIM_FRAMES,\n    GBUF_ROOMS,\n    GBUF_ROOM_MESH,\n    GBUF_ROOM_PORTALS,\n    GBUF_ROOM_SECTORS,\n    GBUF_ROOM_LIGHTS,\n    GBUF_ROOM_STATIC_MESHES,\n    GBUF_FLOOR_DATA,\n    GBUF_OUTSIDE_ROOM_TABLE,\n    GBUF_ITEMS,\n    GBUF_ITEM_DATA,\n    GBUF_EFFECTS,\n    GBUF_CAMERAS,\n    GBUF_SOUND_SOURCES,\n    GBUF_BOXES,\n    GBUF_OVERLAPS,\n    GBUF_GROUND_ZONE,\n    GBUF_FLY_ZONE,\n    GBUF_ANIMATED_TEXTURE_RANGES,\n    GBUF_CINEMATIC_FRAMES,\n    GBUF_CREATURE_DATA,\n    GBUF_CREATURE_LOT,\n    GBUF_SAMPLE_INFOS,\n    GBUF_SAMPLES,\n    GBUF_DEMO_BUFFER,\n    GBUF_WALKABLES,\n    GBUF_NUM_MALLOC_TYPES,\n    // clang-format on\n} GAME_BUFFER;\n\nvoid GameBuf_Init(void);\nvoid GameBuf_Shutdown(void);\nvoid GameBuf_ResetSingle(GAME_BUFFER buffer);\nvoid GameBuf_Reset(void);\n\nvoid *GameBuf_Alloc(size_t alloc_size, GAME_BUFFER buffer);\n"
  },
  {
    "path": "src/trx/game/game_flow/common.c",
    "content": "#include <trx/game/game_flow/common.h>\n\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow/vars.h>\n\nstatic const GF_LEVEL *m_CurrentLevel = nullptr;\nstatic GF_COMMAND m_OverrideCommand = { .action = GF_NOOP };\n\nstatic bool M_SkipLevel(const GF_LEVEL *const level)\n{\n    return level->type == GFL_DUMMY || level->type == GFL_CURRENT;\n}\n\nstatic void M_FreeSequence(GF_SEQUENCE *const sequence)\n{\n    Memory_Free(sequence->events);\n}\n\nstatic void M_FreeInjections(INJECTION_DATA *const injections)\n{\n    if (injections->data_paths != nullptr) {\n        for (int32_t i = 0; i < injections->count; i++) {\n            Memory_FreePointer(&injections->data_paths[i]);\n        }\n    }\n    Memory_FreePointer(&injections->data_paths);\n    injections->count = 0;\n}\n\nstatic void M_FreeLevel(GF_LEVEL *const level)\n{\n    Memory_FreePointer(&level->path);\n    Memory_FreePointer(&level->title);\n    Memory_FreePointer(&level->script_path);\n    Memory_FreePointer(&level->lara_outfit);\n    M_FreeInjections(&level->injections);\n    M_FreeSequence(&level->sequence);\n\n    if (level->item_drops.count > 0) {\n        for (int32_t i = 0; i < level->item_drops.count; i++) {\n            Memory_FreePointer(&level->item_drops.data[i].object_ids);\n        }\n        Memory_FreePointer(&level->item_drops.data);\n    }\n    Memory_FreePointer(&level->settings.sfx_path);\n}\n\nstatic void M_FreeLevelTable(GF_LEVEL_TABLE *const level_table)\n{\n    if (level_table != nullptr) {\n        for (int32_t i = 0; i < level_table->count; i++) {\n            M_FreeLevel(&level_table->levels[i]);\n        }\n        Memory_FreePointer(&level_table->levels);\n        level_table->count = 0;\n    }\n}\n\nstatic void M_FreeFMVs(GAME_FLOW *const gf)\n{\n    for (int32_t i = 0; i < gf->fmv_count; i++) {\n        Memory_FreePointer(&gf->fmvs[i].path);\n    }\n    Memory_FreePointer(&gf->fmvs);\n    gf->fmv_count = 0;\n}\n\nvoid GF_Init(void)\n{\n}\n\nvoid GF_Shutdown(void)\n{\n    m_CurrentLevel = nullptr;\n    m_OverrideCommand = (GF_COMMAND) { .action = GF_NOOP };\n\n    GAME_FLOW *const gf = &g_GameFlow;\n    M_FreeInjections(&gf->injections);\n\n    for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) {\n        M_FreeLevelTable(&gf->level_tables[i]);\n    }\n    M_FreeFMVs(gf);\n\n    Memory_FreePointer(&gf->globe.entries);\n    gf->globe.count = 0;\n\n    if (gf->title_level != nullptr) {\n        M_FreeLevel(gf->title_level);\n        Memory_FreePointer(&gf->title_level);\n    }\n\n    Memory_FreePointer(&gf->main_menu_background_path);\n    Memory_FreePointer(&gf->savegame_file_fmt);\n    Memory_FreePointer(&gf->ambient_tracks.ids);\n    gf->ambient_tracks.count = 0;\n    Memory_FreePointer(&gf->settings.sfx_path);\n    Memory_FreePointer(&gf->main_script_path);\n    Memory_FreePointer(&gf->meta.name);\n    Memory_FreePointer(&gf->meta.extends);\n    Memory_FreePointer(&gf->path);\n}\n\nvoid GF_OverrideCommand(const GF_COMMAND command)\n{\n    m_OverrideCommand = command;\n}\n\nGF_COMMAND GF_GetOverrideCommand(void)\n{\n    return m_OverrideCommand;\n}\n\nGF_LEVEL_TABLE_TYPE GF_GetLevelTableType(const GF_LEVEL_TYPE level_type)\n{\n    switch (level_type) {\n    case GFL_GYM:\n    case GFL_NORMAL:\n    case GFL_BONUS:\n    case GFL_DUMMY:\n    case GFL_CURRENT:\n        return GFLT_MAIN;\n\n    case GFL_CUTSCENE:\n        return GFLT_CUTSCENES;\n\n    case GFL_DEMO:\n        return GFLT_DEMOS;\n\n    case GFL_TITLE:\n        return GFLT_TITLE;\n    }\n\n    ASSERT_FAIL();\n}\n\nconst GF_LEVEL_TABLE *GF_GetLevelTable(\n    const GF_LEVEL_TABLE_TYPE level_table_type)\n{\n    return &g_GameFlow.level_tables[level_table_type];\n}\n\nint32_t GF_GetLevelCount(const GF_LEVEL_TABLE_TYPE level_table_type)\n{\n    int32_t count = 0;\n    const GF_LEVEL_TABLE *const tbl = GF_GetLevelTable(level_table_type);\n    for (int32_t i = 0; i < tbl->count; i++) {\n        const GF_LEVEL *const level = &tbl->levels[i];\n        if (level->type == GFL_GYM || M_SkipLevel(level)) {\n            continue;\n        }\n        count++;\n    }\n    return count;\n}\n\nint32_t GF_GetLevelOrdinalNumber(\n    GF_LEVEL_TABLE_TYPE level_table_type, const GF_LEVEL *const ref_level)\n{\n    int32_t ordinal = 1;\n    const GF_LEVEL_TABLE *const tbl = GF_GetLevelTable(level_table_type);\n    for (int32_t i = 0; i < tbl->count; i++) {\n        const GF_LEVEL *level = &tbl->levels[i];\n        if (M_SkipLevel(level)) {\n            continue;\n        }\n        if (level == ref_level) {\n            // Special case: gym levels have no ordinal\n            return (level->type == GFL_GYM) ? 0 : ordinal;\n        }\n        if (level->type != GFL_GYM) {\n            ordinal++;\n        }\n    }\n    return -1;\n}\n\nGF_LEVEL *GF_GetLevelByOrdinalNumber(\n    GF_LEVEL_TABLE_TYPE level_table_type, const int32_t level_num)\n{\n    const GF_LEVEL_TABLE *const tbl = GF_GetLevelTable(level_table_type);\n    for (int32_t i = 0; i < tbl->count; i++) {\n        GF_LEVEL *const level = &tbl->levels[i];\n        if (GF_GetLevelOrdinalNumber(level_table_type, level) == level_num) {\n            return level;\n        }\n    }\n    return nullptr;\n}\n\nconst GF_LEVEL *GF_GetCurrentLevel(void)\n{\n    return m_CurrentLevel;\n}\n\nconst GF_LEVEL *GF_GetTitleLevel(void)\n{\n    return g_GameFlow.title_level;\n}\n\nconst GF_LEVEL *GF_GetGymLevel(void)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (level->type == GFL_GYM && !M_SkipLevel(level)) {\n            return level;\n        }\n    }\n    return nullptr;\n}\n\nconst GF_LEVEL *GF_GetFirstLevel(void)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (level->type == GFL_GYM || M_SkipLevel(level)) {\n            continue;\n        }\n        return level;\n    }\n    return nullptr;\n}\n\nconst GF_LEVEL *GF_GetLastLevel(void)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    const GF_LEVEL *result = nullptr;\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (level->type == GFL_GYM || M_SkipLevel(level)) {\n            continue;\n        }\n        result = level;\n    }\n    return result;\n}\n\nconst GF_LEVEL *GF_GetLevel(\n    const GF_LEVEL_TABLE_TYPE level_table_type, const int32_t num)\n{\n    if (level_table_type == GFLT_TITLE) {\n        return GF_GetTitleLevel();\n    }\n    const GF_LEVEL_TABLE *const level_table =\n        GF_GetLevelTable(level_table_type);\n    ASSERT(level_table != nullptr);\n    if (num < 0 || num >= level_table->count) {\n        LOG_ERROR(\"Invalid cutscene number: %d\", num);\n        return nullptr;\n    }\n    return &level_table->levels[num];\n}\n\nconst GF_LEVEL *GF_GetLevelAfter(const GF_LEVEL *const level)\n{\n    if (level == nullptr) {\n        return nullptr;\n    }\n    const GF_LEVEL_TABLE_TYPE level_table_type =\n        GF_GetLevelTableType(level->type);\n    const GF_LEVEL_TABLE *const level_table =\n        GF_GetLevelTable(level_table_type);\n    for (int32_t i = level->num + 1; i < level_table->count; i++) {\n        const GF_LEVEL *const next_level = &level_table->levels[i];\n        if (!M_SkipLevel(next_level)) {\n            return next_level;\n        }\n    }\n    return nullptr;\n}\n\nconst GF_LEVEL *GF_GetLevelBefore(const GF_LEVEL *const level)\n{\n    if (level == nullptr) {\n        return nullptr;\n    }\n    const GF_LEVEL_TABLE_TYPE level_table_type =\n        GF_GetLevelTableType(level->type);\n    const GF_LEVEL_TABLE *const level_table =\n        GF_GetLevelTable(level_table_type);\n    for (int32_t i = level->num - 1; i >= 0; i--) {\n        const GF_LEVEL *const prev_level = &level_table->levels[i];\n        if (!M_SkipLevel(prev_level)) {\n            return prev_level;\n        }\n    }\n    return nullptr;\n}\n\nvoid GF_SetCurrentLevel(const GF_LEVEL *const level)\n{\n    m_CurrentLevel = level;\n}\n\nvoid GF_SetLevelTitle(GF_LEVEL *const level, const char *const title)\n{\n    Memory_FreePointer(&level->title);\n    level->title = title != nullptr ? Memory_DupStr(title) : nullptr;\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/common.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/savegame/types.h>\n\nvoid GF_Init(void);\nvoid GF_Shutdown(void);\n\nvoid GF_OverrideCommand(GF_COMMAND action);\nGF_COMMAND GF_GetOverrideCommand(void);\n\nGF_LEVEL_TABLE_TYPE GF_GetLevelTableType(const GF_LEVEL_TYPE level_type);\nconst GF_LEVEL_TABLE *GF_GetLevelTable(GF_LEVEL_TABLE_TYPE level_type);\nint32_t GF_GetLevelCount(GF_LEVEL_TABLE_TYPE level_table_type);\n\nconst GF_LEVEL *GF_GetCurrentLevel(void);\nconst GF_LEVEL *GF_GetTitleLevel(void);\nconst GF_LEVEL *GF_GetGymLevel(void);\nconst GF_LEVEL *GF_GetFirstLevel(void);\nconst GF_LEVEL *GF_GetLastLevel(void);\nconst GF_LEVEL *GF_GetLevel(GF_LEVEL_TABLE_TYPE level_table_type, int32_t num);\nconst GF_LEVEL *GF_GetLevelAfter(const GF_LEVEL *level);\nconst GF_LEVEL *GF_GetLevelBefore(const GF_LEVEL *level);\n\n// Get human-readable number (as opposed to index), starting with 1.\n// Returns 0 for Gym levels and -1 for unknown levels.\nint32_t GF_GetLevelOrdinalNumber(\n    GF_LEVEL_TABLE_TYPE level_table_type, const GF_LEVEL *level);\n\n// Get the level based on the human-readable number - opposite of\n// GF_GetLevelOrdinalNumber().\nGF_LEVEL *GF_GetLevelByOrdinalNumber(\n    GF_LEVEL_TABLE_TYPE level_table_type, int32_t level_num);\n\nvoid GF_SetCurrentLevel(const GF_LEVEL *level);\nvoid GF_SetLevelTitle(GF_LEVEL *level, const char *title);\n\n// Returns true if any story cutscenes or FMVs occur before gameplay in any\n// main level up to the level in the specified save slot.\nbool GF_HasAvailableStory(SAVEGAME_SLOT_REF slot);\n"
  },
  {
    "path": "src/trx/game/game_flow/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    GFLT_UNKNOWN = -1,\n    GFLT_TITLE,\n    GFLT_MAIN,\n    GFLT_CUTSCENES,\n    GFLT_DEMOS,\n    GFLT_NUMBER_OF,\n} GF_LEVEL_TABLE_TYPE;\n\ntypedef enum {\n    // Genuine level types\n    GFL_TITLE,\n    GFL_NORMAL,\n    GFL_CUTSCENE,\n    GFL_DEMO,\n    GFL_GYM,\n    GFL_BONUS,\n\n    // Legacy level types to maintain savegame backwards compatibility.\n    // TODO: get rid of these.\n    GFL_DUMMY,\n    GFL_CURRENT,\n} GF_LEVEL_TYPE;\n\ntypedef enum {\n    GFSC_NORMAL,\n    GFSC_SAVED,\n    GFSC_RESTART,\n    GFSC_SELECT,\n    GFSC_STORY,\n} GF_SEQUENCE_CONTEXT;\n\ntypedef enum {\n    GF_NOOP = -1,\n    GF_START_GAME,\n    GF_START_CINE,\n    GF_START_FMV,\n    GF_START_DEMO,\n    GF_EXIT_TO_TITLE,\n    GF_LEVEL_COMPLETE,\n    GF_EXIT_GAME,\n    GF_SWITCH_MOD,\n    GF_START_SAVED_GAME,\n    GF_RESTART_GAME,\n    GF_SELECT_GAME,\n    GF_GLOBE_SELECT,\n    GF_STORY_SO_FAR,\n} GF_ACTION;\n\ntypedef enum {\n    GFS_DISPLAY_PICTURE,\n    GFS_LOOP_GAME,\n    GFS_PLAY_CUTSCENE,\n    GFS_PLAY_FMV,\n    GFS_PLAY_MUSIC,\n    GFS_EXIT_TO_TITLE,\n    GFS_LEVEL_STATS,\n    GFS_TOTAL_STATS,\n    GFS_GLOBE_SELECT,\n    GFS_LEVEL_COMPLETE,\n    GFS_LOADING_SCREEN,\n    GFS_ADD_ITEM,\n    GFS_ADD_SECRET_REWARD,\n    GFS_REMOVE_WEAPONS,\n    GFS_REMOVE_AMMO,\n    GFS_REMOVE_MEDIPACKS,\n    GFS_REMOVE_FLARES,\n    GFS_REMOVE_SCIONS,\n    GFS_DISABLE_FLOOR,\n    GFS_SETUP_BACON_LARA,\n    GFS_SET_START_ANIM,\n    GFS_ENABLE_SUNSET,\n    GFS_NUMBER_OF,\n} GF_SEQUENCE_EVENT_TYPE;\n\ntypedef enum {\n    GF_DEATH_TILE_LAVA,\n    GF_DEATH_TILE_RAPIDS,\n    GF_DEATH_TILE_ELECTRIC,\n} GF_DEATH_TILE;\n"
  },
  {
    "path": "src/trx/game/game_flow/inventory.c",
    "content": "#include <trx/game/game_flow/inventory.h>\n\n#include <trx/config.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/objects.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n\nstatic int8_t m_SecretInvItems[O_NUMBER_OF] = {};\nstatic int8_t m_Add2InvItems[O_NUMBER_OF] = {};\nstatic bool m_RemoveWeapons = false;\nstatic bool m_RemoveAmmo = false;\nstatic bool m_RemoveFlares = false;\nstatic bool m_RemoveMedipacks = false;\nstatic bool m_RemoveScions = false;\n\nstatic bool M_CanHaveItem(const OBJECT_ID object_id)\n{\n    if (Object_IsType(object_id, g_GunObjects) && object_id != O_PISTOL_ITEM\n        && g_Config.gameplay.disable_extra_guns) {\n        return false;\n    }\n    if ((object_id == O_SMALL_MEDIPACK_ITEM\n         || object_id == O_LARGE_MEDIPACK_ITEM)\n        && g_Config.gameplay.disable_medpacks) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_ResumeInfo_HasWeapon(\n    const RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type)\n{\n    switch (gun_type) {\n        // clang-format off\n    case LGT_PISTOLS:      return resume->flags.has_pistols;\n    case LGT_MAGNUMS:      return resume->flags.has_magnums;\n    case LGT_AUTOS:        return resume->flags.has_autos;\n    case LGT_DESERT_EAGLE: return resume->flags.has_desert_eagle;\n    case LGT_UZIS:         return resume->flags.has_uzis;\n    case LGT_SHOTGUN:      return resume->flags.has_shotgun;\n    case LGT_HARPOON:      return resume->flags.has_harpoon;\n    case LGT_M16:          return resume->flags.has_m16;\n    case LGT_MP5:          return resume->flags.has_mp5;\n    case LGT_GRENADE:      return resume->flags.has_grenade;\n    case LGT_ROCKET:       return resume->flags.has_rocket;\n    default: return false;\n        // clang-format on\n    }\n}\n\nstatic void M_ResumeInfo_SetWeapon(\n    RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type,\n    const bool has_weapon)\n{\n    switch (gun_type) {\n        // clang-format off\n    case LGT_PISTOLS:      resume->flags.has_pistols = has_weapon; break;\n    case LGT_MAGNUMS:      resume->flags.has_magnums = has_weapon; break;\n    case LGT_AUTOS:        resume->flags.has_autos = has_weapon; break;\n    case LGT_DESERT_EAGLE: resume->flags.has_desert_eagle = has_weapon; break;\n    case LGT_UZIS:         resume->flags.has_uzis = has_weapon; break;\n    case LGT_SHOTGUN:      resume->flags.has_shotgun = has_weapon; break;\n    case LGT_HARPOON:      resume->flags.has_harpoon = has_weapon; break;\n    case LGT_M16:          resume->flags.has_m16 = has_weapon; break;\n    case LGT_MP5:          resume->flags.has_mp5 = has_weapon; break;\n    case LGT_GRENADE:      resume->flags.has_grenade = has_weapon; break;\n    case LGT_ROCKET:       resume->flags.has_rocket = has_weapon; break;\n    default: break;\n        // clang-format on\n    }\n}\n\nstatic void M_ResumeInfo_AddAmmo(\n    RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type,\n    const int32_t ammo_qty)\n{\n    switch (gun_type) {\n        // clang-format off\n    case LGT_MAGNUMS:      resume->magnum_ammo += ammo_qty; break;\n    case LGT_AUTOS:        resume->autos_ammo += ammo_qty; break;\n    case LGT_DESERT_EAGLE: resume->desert_eagle_ammo += ammo_qty; break;\n    case LGT_UZIS:         resume->uzi_ammo += ammo_qty; break;\n    case LGT_SHOTGUN:      resume->shotgun_ammo += ammo_qty; break;\n    case LGT_HARPOON:      resume->harpoon_ammo += ammo_qty; break;\n    case LGT_M16:          resume->m16_ammo += ammo_qty; break;\n    case LGT_MP5:          resume->mp5_ammo += ammo_qty; break;\n    case LGT_GRENADE:      resume->grenade_ammo += ammo_qty; break;\n    case LGT_ROCKET:       resume->rocket_ammo += ammo_qty; break;\n    default: break;\n        // clang-format on\n    }\n}\n\nstatic void M_ResumeInfo_AddItem(\n    RESUME_INFO *const resume, const OBJECT_ID object_id, const int32_t qty)\n{\n    switch (object_id) {\n    case O_SMALL_MEDIPACK_ITEM:\n    case O_SMALL_MEDIPACK_OPTION:\n        resume->small_medipacks += qty;\n        break;\n    case O_LARGE_MEDIPACK_ITEM:\n    case O_LARGE_MEDIPACK_OPTION:\n        resume->large_medipacks += qty;\n        break;\n    case O_FLAREBOX_ITEM:\n    case O_FLAREBOX_OPTION:\n    case O_FLARE_ITEM:\n        resume->flares += qty;\n        break;\n    default:\n        break;\n    }\n}\n\nstatic void M_ModifyResumeInfo_GunOrAmmo(\n    RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type)\n{\n    const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type);\n    const OBJECT_ID ammo_object_id = Gun_GetAmmoObject(gun_type);\n    const int32_t ammo_pickup_qty = Gun_GetAmmoPickupQuantity(gun_type);\n    const int32_t ammo_initial_qty = Gun_GetAmmoInitialQuantity(gun_type);\n\n    if (!M_CanHaveItem(gun_object_id) || !M_CanHaveItem(ammo_object_id)) {\n        return;\n    }\n\n    M_ResumeInfo_AddAmmo(\n        resume, gun_type, ammo_pickup_qty * m_Add2InvItems[ammo_object_id]);\n    if (!M_ResumeInfo_HasWeapon(resume, gun_type)\n        && m_Add2InvItems[gun_object_id] > 0) {\n        M_ResumeInfo_SetWeapon(resume, gun_type, true);\n        M_ResumeInfo_AddAmmo(resume, gun_type, ammo_initial_qty);\n    }\n}\n\nstatic void M_ModifyResumeInfo_Item(\n    RESUME_INFO *const resume, const OBJECT_ID object_id)\n{\n    if (!M_CanHaveItem(object_id)) {\n        return;\n    }\n\n    M_ResumeInfo_AddItem(resume, object_id, m_Add2InvItems[object_id]);\n}\n\nstatic void M_CollectNewPickup(const OBJECT_ID object_id)\n{\n    Overlay_AddDisplayPickup(object_id);\n    Stats_AddPickup();\n}\n\nstatic void M_ModifyInventory_GunOrAmmo(\n    const GF_INV_TYPE type, const LARA_GUN_TYPE gun_type)\n{\n    const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type);\n    const OBJECT_ID ammo_object_id = Gun_GetAmmoObject(gun_type);\n    const int32_t ammo_pickup_qty = Gun_GetAmmoPickupQuantity(gun_type);\n    const int32_t ammo_initial_qty = Gun_GetAmmoInitialQuantity(gun_type);\n    AMMO_INFO *const ammo_info = Gun_GetAmmoInfo(gun_type);\n\n    if (!M_CanHaveItem(gun_object_id) || !M_CanHaveItem(ammo_object_id)) {\n        return;\n    }\n\n    if (Inv_RequestItem(gun_object_id)) {\n        if (type == GF_INV_SECRET) {\n            // Convert already collected guns into ammo to maintain stats\n            // accuracy.\n            ammo_info->ammo +=\n                ammo_pickup_qty * m_SecretInvItems[ammo_object_id];\n            ammo_info->ammo +=\n                ammo_initial_qty * m_SecretInvItems[gun_object_id];\n            for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) {\n                M_CollectNewPickup(ammo_object_id);\n            }\n            for (int32_t i = 0; i < m_SecretInvItems[gun_object_id]; i++) {\n                M_CollectNewPickup(ammo_object_id);\n            }\n        } else if (type == GF_INV_REGULAR) {\n            ammo_info->ammo += ammo_pickup_qty * m_Add2InvItems[ammo_object_id];\n        }\n    } else if (\n        (type == GF_INV_REGULAR && m_Add2InvItems[gun_object_id] > 0)\n        || (type == GF_INV_SECRET && m_SecretInvItems[gun_object_id] > 0)) {\n\n        Inv_AddItem(gun_object_id);\n\n        if (type == GF_INV_SECRET) {\n            ammo_info->ammo +=\n                ammo_pickup_qty * m_SecretInvItems[ammo_object_id];\n            M_CollectNewPickup(gun_object_id);\n            for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) {\n                M_CollectNewPickup(ammo_object_id);\n            }\n        } else if (type == GF_INV_REGULAR) {\n            ammo_info->ammo += ammo_pickup_qty * m_Add2InvItems[ammo_object_id];\n        }\n    } else if (type == GF_INV_SECRET) {\n        for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) {\n            Inv_AddItem(ammo_object_id);\n            M_CollectNewPickup(ammo_object_id);\n        }\n    } else if (type == GF_INV_REGULAR) {\n        for (int32_t i = 0; i < m_Add2InvItems[ammo_object_id]; i++) {\n            Inv_AddItem(ammo_object_id);\n        }\n    }\n}\n\nstatic void M_ModifyInventory_Item(\n    const GF_INV_TYPE type, const OBJECT_ID object_id)\n{\n    int32_t qty = 0;\n    if (type == GF_INV_SECRET) {\n        qty = m_SecretInvItems[object_id];\n    } else if (type == GF_INV_REGULAR) {\n        qty = m_Add2InvItems[object_id];\n    }\n\n    // Check for gameplay mods from secret rewards\n    if (!M_CanHaveItem(object_id)) {\n        qty = 0;\n    }\n\n    for (int32_t i = 0; i < qty; i++) {\n        if (Inv_AddItem(object_id) && type == GF_INV_SECRET) {\n            M_CollectNewPickup(object_id);\n        }\n    }\n}\n\nvoid GF_InventoryModifier_Scan(const GF_LEVEL *const level)\n{\n    for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) {\n        m_SecretInvItems[i] = 0;\n        m_Add2InvItems[i] = 0;\n    }\n    m_RemoveWeapons = false;\n    m_RemoveAmmo = false;\n    m_RemoveFlares = false;\n    m_RemoveMedipacks = false;\n    m_RemoveScions = false;\n\n    if (level == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < level->sequence.length; i++) {\n        const GF_SEQUENCE_EVENT *const event = &level->sequence.events[i];\n        if (event->type == GFS_ADD_ITEM\n            || event->type == GFS_ADD_SECRET_REWARD) {\n            const GF_ADD_ITEM_DATA *const data = event->data;\n            if (data->object_id < O_FIRST || data->object_id >= O_NUMBER_OF) {\n                continue;\n            }\n            if (data->inv_type == GF_INV_SECRET) {\n                m_SecretInvItems[data->object_id] += data->quantity;\n            } else if (data->inv_type == GF_INV_REGULAR) {\n                m_Add2InvItems[data->object_id] += data->quantity;\n            }\n        } else if (event->type == GFS_REMOVE_WEAPONS) {\n            m_RemoveWeapons = true;\n        } else if (event->type == GFS_REMOVE_AMMO) {\n            m_RemoveAmmo = true;\n        } else if (event->type == GFS_REMOVE_FLARES) {\n            m_RemoveFlares = true;\n        } else if (event->type == GFS_REMOVE_MEDIPACKS) {\n            m_RemoveMedipacks = true;\n        } else if (event->type == GFS_REMOVE_SCIONS) {\n            m_RemoveScions = true;\n        }\n    }\n}\n\nint32_t GF_GetSecretRewardCount(const GF_LEVEL *const level)\n{\n    int32_t sum = 0;\n    if (level == nullptr) {\n        return sum;\n    }\n    for (int32_t i = 0; i < level->sequence.length; i++) {\n        const GF_SEQUENCE_EVENT *const event = &level->sequence.events[i];\n        if (event->type == GFS_ADD_SECRET_REWARD) {\n            const GF_ADD_ITEM_DATA *const data = event->data;\n            if (data->inv_type == GF_INV_SECRET) {\n                sum += data->quantity;\n            }\n        }\n    }\n    return sum;\n}\n\nvoid GF_InventoryModifier_ApplyToResumeInfo(const GF_LEVEL *const level)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n\n    if (m_RemoveWeapons) {\n        resume->flags.has_pistols = false;\n        resume->flags.has_magnums = false;\n        resume->flags.has_autos = false;\n        resume->flags.has_desert_eagle = false;\n        resume->flags.has_uzis = false;\n        resume->flags.has_shotgun = false;\n        resume->flags.has_m16 = false;\n        resume->flags.has_mp5 = false;\n        resume->flags.has_grenade = false;\n        resume->flags.has_rocket = false;\n        resume->flags.has_harpoon = false;\n        resume->holsters_gun_type = LGT_UNARMED;\n        resume->back_gun_type = LGT_UNARMED;\n        resume->equipped_gun_type = LGT_UNARMED;\n        resume->gun_status = LGS_ARMLESS;\n    }\n\n    if (!resume->flags.has_pistols && m_Add2InvItems[O_PISTOL_ITEM]) {\n        resume->flags.has_pistols = true;\n        if (resume->equipped_gun_type == LGT_UNARMED) {\n            resume->equipped_gun_type = LGT_PISTOLS;\n        }\n    }\n\n    if (m_RemoveAmmo) {\n        resume->pistol_ammo = 0;\n        resume->magnum_ammo = 0;\n        resume->autos_ammo = 0;\n        resume->desert_eagle_ammo = 0;\n        resume->uzi_ammo = 0;\n        resume->shotgun_ammo = 0;\n        resume->m16_ammo = 0;\n        resume->mp5_ammo = 0;\n        resume->grenade_ammo = 0;\n        resume->rocket_ammo = 0;\n        resume->harpoon_ammo = 0;\n    }\n\n    if (m_RemoveScions) {\n        resume->num_scions = 0;\n        resume->num_quest_item_1 = 0;\n        resume->num_quest_item_2 = 0;\n        resume->num_quest_item_3 = 0;\n        resume->num_quest_item_4 = 0;\n        m_RemoveScions = false;\n    }\n\n    if (m_RemoveFlares) {\n        resume->flares = 0;\n        m_RemoveFlares = false;\n    }\n\n    if (m_RemoveMedipacks) {\n        resume->large_medipacks = 0;\n        resume->small_medipacks = 0;\n        m_RemoveMedipacks = false;\n    }\n\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_PISTOLS);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_MAGNUMS);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_AUTOS);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_DESERT_EAGLE);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_UZIS);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_SHOTGUN);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_HARPOON);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_M16);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_MP5);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_GRENADE);\n    M_ModifyResumeInfo_GunOrAmmo(resume, LGT_ROCKET);\n\n    M_ModifyResumeInfo_Item(resume, O_SMALL_MEDIPACK_ITEM);\n    M_ModifyResumeInfo_Item(resume, O_LARGE_MEDIPACK_ITEM);\n    M_ModifyResumeInfo_Item(resume, O_FLARE_ITEM);\n}\n\nvoid GF_InventoryModifier_Apply(\n    const GF_LEVEL *const level, const GF_INV_TYPE type)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n\n    // For GF_INV_REGULAR, we must ignore weapons, ammo, medpacks and flares,\n    // as these are handled by RESUME_INFO and\n    // GF_InventoryModifier_ApplyToResumeInfo and Lara_InitialiseInventory.\n\n    if (type == GF_INV_SECRET) {\n        if (m_Add2InvItems[O_PISTOL_ITEM]) {\n            Inv_AddItem(O_PISTOL_ITEM);\n            if (resume->equipped_gun_type == LGT_UNARMED) {\n                resume->equipped_gun_type = LGT_PISTOLS;\n            }\n        }\n\n        M_ModifyInventory_GunOrAmmo(type, LGT_MAGNUMS);\n        M_ModifyInventory_GunOrAmmo(type, LGT_AUTOS);\n        M_ModifyInventory_GunOrAmmo(type, LGT_DESERT_EAGLE);\n        M_ModifyInventory_GunOrAmmo(type, LGT_UZIS);\n        M_ModifyInventory_GunOrAmmo(type, LGT_SHOTGUN);\n        M_ModifyInventory_GunOrAmmo(type, LGT_HARPOON);\n        M_ModifyInventory_GunOrAmmo(type, LGT_M16);\n        M_ModifyInventory_GunOrAmmo(type, LGT_MP5);\n        M_ModifyInventory_GunOrAmmo(type, LGT_GRENADE);\n        M_ModifyInventory_GunOrAmmo(type, LGT_ROCKET);\n    }\n\n    M_ModifyInventory_Item(type, O_PICKUP_ITEM_1);\n    M_ModifyInventory_Item(type, O_PICKUP_ITEM_2);\n    M_ModifyInventory_Item(type, O_PUZZLE_ITEM_1);\n    M_ModifyInventory_Item(type, O_PUZZLE_ITEM_2);\n    M_ModifyInventory_Item(type, O_PUZZLE_ITEM_3);\n    M_ModifyInventory_Item(type, O_PUZZLE_ITEM_4);\n    M_ModifyInventory_Item(type, O_KEY_ITEM_1);\n    M_ModifyInventory_Item(type, O_KEY_ITEM_2);\n    M_ModifyInventory_Item(type, O_KEY_ITEM_3);\n    M_ModifyInventory_Item(type, O_KEY_ITEM_4);\n    M_ModifyInventory_Item(type, O_LEADBAR_ITEM);\n    M_ModifyInventory_Item(type, O_SCION_ITEM_1);\n    M_ModifyInventory_Item(type, O_SCION_ITEM_2);\n\n    if (type == GF_INV_SECRET) {\n        M_ModifyInventory_Item(type, O_SMALL_MEDIPACK_ITEM);\n        M_ModifyInventory_Item(type, O_LARGE_MEDIPACK_ITEM);\n        M_ModifyInventory_Item(type, O_FLARE_ITEM);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/inventory.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\nvoid GF_InventoryModifier_Scan(const GF_LEVEL *level);\nvoid GF_InventoryModifier_Apply(const GF_LEVEL *level, GF_INV_TYPE type);\nvoid GF_InventoryModifier_ApplyToResumeInfo(const GF_LEVEL *level);\nint32_t GF_GetSecretRewardCount(const GF_LEVEL *level);\n"
  },
  {
    "path": "src/trx/game/game_flow/reader.c",
    "content": "#include <trx/game/game_flow/reader.h>\n\n#include <trx/core/enum_map.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_flow/types.h>\n#include <trx/game/game_flow/vars.h>\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/lara/skin/storage.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\n#include <string.h>\n\ntypedef struct {\n    GAME_FLOW *gf;\n    const char *script_path;\n    JSON_READ_IO *io;\n    bool validation_mode;\n} M_CONTEXT;\n\n#define M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(name)                            \\\n    int32_t name(                                                              \\\n        const M_CONTEXT *ctx, GF_SEQUENCE_EVENT *event, void *extra_data,      \\\n        void *user_arg)\n\ntypedef int32_t (*M_SEQUENCE_EVENT_HANDLER_FUNC)(\n    const M_CONTEXT *ctx, GF_SEQUENCE_EVENT *event, void *extra_data,\n    void *user_arg);\n\ntypedef struct {\n    GF_SEQUENCE_EVENT_TYPE event_type;\n    M_SEQUENCE_EVENT_HANDLER_FUNC handler_func;\n    void *handler_func_arg;\n} M_SEQUENCE_EVENT_HANDLER;\n\ntypedef bool (*M_LOAD_ARRAY_FUNC)(\n    const M_CONTEXT *ctx, void *target_elem, size_t target_elem_idx,\n    void *user_arg);\n\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleIntEvent);\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandlePictureEvent);\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleTotalStatsEvent);\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleAddItemEvent);\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleGlobeSelectEvent);\n\nstatic M_SEQUENCE_EVENT_HANDLER m_SequenceEventHandlers[] = {\n    // clang-format off\n    // Events without arguments\n    { GFS_ENABLE_SUNSET,     nullptr, nullptr },\n    { GFS_REMOVE_WEAPONS,    nullptr, nullptr },\n    { GFS_REMOVE_SCIONS,     nullptr, nullptr },\n    { GFS_REMOVE_AMMO,       nullptr, nullptr },\n    { GFS_REMOVE_FLARES,     nullptr, nullptr },\n    { GFS_REMOVE_MEDIPACKS,  nullptr, nullptr },\n    { GFS_LEVEL_COMPLETE,    nullptr, nullptr },\n    { GFS_LEVEL_STATS,       nullptr, nullptr },\n    { GFS_EXIT_TO_TITLE,     nullptr, nullptr },\n    { GFS_GLOBE_SELECT,      M_HandleGlobeSelectEvent, nullptr },\n\n    // Events with integer arguments\n    { GFS_SET_START_ANIM,    M_HandleIntEvent, \"anim\" },\n    { GFS_LOOP_GAME,         M_HandleIntEvent, \"level_id\" },\n    { GFS_PLAY_CUTSCENE,     M_HandleIntEvent, \"cutscene_id\" },\n    { GFS_PLAY_FMV,          M_HandleIntEvent, \"fmv_id\" },\n    { GFS_PLAY_MUSIC,        M_HandleIntEvent, \"music_track\" },\n    { GFS_SETUP_BACON_LARA,  M_HandleIntEvent, \"anchor_room\" },\n    { GFS_DISABLE_FLOOR,     M_HandleIntEvent, \"height\" },\n\n    // Special cases with custom handlers\n    { GFS_LOADING_SCREEN,    M_HandlePictureEvent, nullptr },\n    { GFS_DISPLAY_PICTURE,   M_HandlePictureEvent, nullptr },\n    { GFS_TOTAL_STATS,       M_HandleTotalStatsEvent, nullptr },\n    { GFS_ADD_ITEM,          M_HandleAddItemEvent, nullptr },\n    { GFS_ADD_SECRET_REWARD, M_HandleAddItemEvent, nullptr },\n\n    // Sentinel to mark the end of the table\n    { (GF_SEQUENCE_EVENT_TYPE)-1, nullptr, nullptr },\n    // clang-format on\n};\n\nstatic void M_ExitWithJSONError(const M_CONTEXT *const ctx)\n{\n    JSONFile_ExitWithReadIOError(\n        ctx->io,\n        String_FormatStatic(\"%s: gameflow parse error\", ctx->script_path));\n}\n\nstatic bool M_ReadObjectID(\n    const M_CONTEXT *const ctx, OBJECT_ID *const object_id_out)\n{\n    int32_t game_id;\n    if (JSON_OPTIONAL(JSON_READ_CURRENT(ctx->io, &game_id))) {\n        *object_id_out = Object_FromGameID(game_id);\n    } else {\n        const char *object_key;\n        JSON_MUST(JSON_READ_CURRENT(ctx->io, &object_key));\n        *object_id_out = Object_IdFromKey(object_key);\n    }\n    if (!ctx->validation_mode && *object_id_out == NO_OBJECT) {\n        JSON_ReadIO_SetError(ctx->io, \"'object_id' must be a valid object id\");\n        JSON_FAIL();\n    }\n    JSON_FINISH();\n}\n\nstatic M_SEQUENCE_EVENT_HANDLER *M_GetSequenceEventHandlers(void)\n{\n    return m_SequenceEventHandlers;\n}\n\n// Read a \"path\" value that may be either a plain string or an array of\n// candidate strings tried in order.\n// Pass optional=true to allow the key to be absent (out_path is set to nullptr\n// in that case).\n// Pass path_type=(TRX_DYNAMIC_PATH)-1 to skip resolution and return the first\n// candidate as-is.\nstatic bool M_ReadPath(\n    JSON_READ_IO *const io, const char *const key, const bool optional,\n    const TRX_DYNAMIC_PATH path_type, char **const out_path,\n    const bool suppress_errors)\n{\n    ASSERT(key != nullptr);\n    *out_path = nullptr;\n    const bool resolve = path_type != (TRX_DYNAMIC_PATH)-1;\n\n    if (!JSON_PUSH(io, key)) {\n        if (suppress_errors) {\n            return true;\n        }\n        if (optional) {\n            return true;\n        }\n        return false;\n    }\n\n    // All failure paths below must pop before returning to keep the\n    // push/pop depth balanced and avoid corrupting the parser state.\n    bool ok = false;\n    JSON_ARRAY *const path_array =\n        JSON_ValueAsArray(JSON_ReadIO_GetCurrentValue(io));\n    const int32_t count = path_array != nullptr ? JSON_ARRAY_LEN(io) : 1;\n    if (count <= 0) {\n        if (!suppress_errors) {\n            JSON_ReadIO_SetError(\n                io, \"path array must contain at least one entry\");\n        }\n    } else {\n        for (int32_t i = 0; i < count; i++) {\n            const char *path = nullptr;\n            const bool read_ok = path_array != nullptr\n                ? JSON_READ_A(io, i, &path)\n                : JSON_READ_CURRENT(io, &path);\n            if (!read_ok) {\n                break;\n            }\n            if (!resolve) {\n                *out_path = Memory_DupStr(path);\n                ok = true;\n                break;\n            }\n            const char *const resolved = TRXPath_PeekResolve(path_type, path);\n            if (resolved != nullptr) {\n                *out_path = Memory_DupStr(resolved);\n                ok = true;\n                break;\n            }\n        }\n        if (!ok && resolve) {\n            if (optional) {\n                ok = true; // leave *out_path as nullptr\n            } else if (suppress_errors) {\n                ok = true; // sizing pass should not surface path failures\n            } else {\n                JSON_ReadIO_SetError(\n                    io, \"failed to resolve any path candidate\");\n            }\n        } else if (!ok && suppress_errors) {\n            ok = true; // sizing pass should not surface malformed path values\n        }\n    }\n\n    if (!JSON_POP(io)) {\n        return false;\n    }\n    return ok;\n}\n\nstatic bool M_LoadSettings(\n    const M_CONTEXT *const ctx, GF_LEVEL_SETTINGS *const settings)\n{\n    JSON_READ_IO *const io = ctx->io;\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"fog_start\"))) {\n        JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_start.value));\n        settings->fog_start.is_present = true;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"fog_end\"))) {\n        JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_end.value));\n        settings->fog_end.is_present = true;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"fog_transparency\"))) {\n        JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_transparency.value));\n        settings->fog_transparency.is_present = true;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"fog_color\"))) {\n        JSON_MUST(JSON_READ_CURRENT(io, &settings->fog_color.value));\n        settings->fog_color.is_present = true;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"water_color\"))) {\n        JSON_MUST(JSON_READ_CURRENT(io, &settings->water_color.value));\n        settings->water_color.is_present = true;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"cold_water\"))) {\n        JSON_MUST(JSON_READ_CURRENT(io, &settings->cold_water.value));\n        settings->cold_water.is_present = true;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"death_tile\"))) {\n        const char *tmp_s = nullptr;\n        JSON_MUST(JSON_READ_CURRENT(io, &tmp_s));\n        const int32_t value =\n            EnumMap_Get(ENUM_MAP_NAME(GF_DEATH_TILE), tmp_s, -1);\n        if (value < 0) {\n            JSON_ReadIO_SetError(io, \"Invalid death_tile value '%s'\", tmp_s);\n            JSON_FAIL();\n        }\n        settings->death_tile.is_present = true;\n        settings->death_tile.value = value;\n        JSON_MUST(JSON_POP(io));\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"sfx_path\"))) {\n        JSON_MUST(JSON_POP(io));\n        JSON_SHOULD(M_ReadPath(\n            io, \"sfx_path\", false, TRX_DYNAMIC_PATH_SFX_FILE,\n            &settings->sfx_path, false));\n    }\n    JSON_FINISH();\n}\n\nstatic bool M_LoadLevelItemDrops(\n    const M_CONTEXT *const ctx, GF_LEVEL *const level)\n{\n    JSON_READ_IO *const io = ctx->io;\n    level->item_drops.count = 0;\n\n    if (!JSON_OPTIONAL(JSON_PUSH(io, \"item_drops\"))) {\n        return true;\n    }\n\n    if (ctx->gf->enable_tr2_item_drops) {\n        LOG_WARNING(\n            \"TR2 item drops are enabled: gameflow-defined drops for level \"\n            \"%d will be ignored\",\n            level->num);\n        JSON_MUST(JSON_POP(io));\n        return true;\n    }\n\n    level->item_drops.count = JSON_ARRAY_LEN(io);\n    if (level->item_drops.count < 0) {\n        JSON_FAIL();\n    }\n    level->item_drops.data = Memory_Alloc(\n        sizeof(GF_DROP_ITEM_DATA) * (size_t)level->item_drops.count);\n\n    for (int32_t i = 0; i < level->item_drops.count; i++) {\n        JSON_MUST(JSON_PUSH_INDEX(io, i));\n        GF_DROP_ITEM_DATA *data = &level->item_drops.data[i];\n\n        JSON_MUST(JSON_READ(io, \"enemy_num\", &data->enemy_num));\n\n        JSON_MUST(JSON_PUSH(io, \"object_ids\"));\n        const int32_t object_count = JSON_ARRAY_LEN(io);\n        if (object_count < 0) {\n            JSON_FAIL();\n        }\n        data->count = object_count;\n        data->object_ids = Memory_Alloc(sizeof(int16_t) * data->count);\n        for (int32_t j = 0; j < data->count; j++) {\n            JSON_MUST(JSON_PUSH_INDEX(io, j));\n            OBJECT_ID id = NO_OBJECT;\n            JSON_MUST(M_ReadObjectID(ctx, &id));\n            data->object_ids[j] = (int16_t)id;\n            JSON_MUST(JSON_POP(io));\n        }\n        JSON_MUST(JSON_POP(io));\n        JSON_MUST(JSON_POP(io));\n    }\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic void M_CopyRootSettingsIntoLevel(\n    GF_LEVEL_SETTINGS *const dst, const GF_LEVEL_SETTINGS *const src)\n{\n    *dst = *src;\n    dst->sfx_path =\n        src->sfx_path != nullptr ? Memory_DupStr(src->sfx_path) : nullptr;\n}\n\nstatic void M_ReadModMeta(JSON_READ_IO *const io, GF_MOD_META *const meta)\n{\n    meta->name = nullptr;\n    meta->engine = 0;\n    meta->extends = nullptr;\n\n    const char *tmp_s = nullptr;\n    if (JSON_OPTIONAL(JSON_READ(io, \"name\", &tmp_s)) && tmp_s != nullptr) {\n        meta->name = Memory_DupStr(tmp_s);\n    }\n\n    JSON_OPTIONAL(JSON_READ(io, \"engine\", &meta->engine));\n\n    tmp_s = nullptr;\n    if (JSON_OPTIONAL(JSON_READ(io, \"extends\", &tmp_s)) && tmp_s != nullptr) {\n        meta->extends = Memory_DupStr(tmp_s);\n    }\n}\n\nstatic bool M_LoadRoot(const M_CONTEXT *const ctx)\n{\n    JSON_READ_IO *const io = ctx->io;\n    const char *tmp_s = nullptr;\n\n    M_ReadModMeta(io, &ctx->gf->meta);\n\n    JSON_MUST(JSON_READ(io, \"main_menu_picture\", &tmp_s));\n    ctx->gf->main_menu_background_path =\n        Memory_DupStr(TRXPath_TryResolve(TRX_DYNAMIC_PATH_IMAGE_FILE, tmp_s));\n\n    if (!JSON_READ(io, \"savegame_file_fmt\", &tmp_s) || tmp_s == nullptr) {\n        if (!JSON_READ(io, \"savegame_fmt_bson\", &tmp_s) || tmp_s == nullptr) {\n            JSON_FAIL();\n        }\n        // TODO: remove in TRX 1.5.\n        LOG_WARNING(\n            \"%s: 'savegame_fmt_bson' is deprecated; use 'savegame_file_fmt'\",\n            ctx->script_path);\n    }\n    ctx->gf->savegame_file_fmt = Memory_DupStr(tmp_s);\n\n    tmp_s = nullptr;\n    if (JSON_OPTIONAL(JSON_READ(io, \"main_script\", &tmp_s))\n        && tmp_s != nullptr) {\n        ctx->gf->main_script_path = Memory_DupStr(\n            TRXPath_TryResolve(TRX_DYNAMIC_PATH_SCRIPT_FILE, tmp_s));\n    }\n\n    if (JSON_PUSH(io, \"ambient_tracks\")) {\n        const int32_t count = JSON_ARRAY_LEN(io);\n        if (count < 0) {\n            JSON_FAIL();\n        }\n        if (count > 0) {\n            ctx->gf->ambient_tracks.is_present = true;\n            ctx->gf->ambient_tracks.count = count;\n            ctx->gf->ambient_tracks.ids =\n                Memory_Alloc(sizeof(MUSIC_ID) * (size_t)count);\n            for (int32_t i = 0; i < count; i++) {\n                int32_t track = MX_INACTIVE;\n                JSON_SHOULD(JSON_READ_A(io, i, &track));\n                ctx->gf->ambient_tracks.ids[i] = track;\n            }\n        }\n        JSON_MUST(JSON_POP(io));\n    }\n\n    ctx->gf->enable_tr2_item_drops = g_TRVersion > 1;\n    JSON_READ_D(\n        io, \"enable_tr2_item_drops\", &ctx->gf->enable_tr2_item_drops,\n        g_TRVersion > 1);\n    JSON_READ_D(\n        io, \"convert_dropped_guns\", &ctx->gf->convert_dropped_guns,\n        g_TRVersion > 1);\n    JSON_FINISH();\n}\n\nstatic bool M_LoadGlobeEntry(\n    const M_CONTEXT *const ctx, void *const target_elem, size_t idx,\n    void *const user_arg)\n{\n    GF_GLOBE_ENTRY *const e = target_elem;\n    ASSERT(user_arg == nullptr);\n    JSON_READ_IO *const io = ctx->io;\n    JSON_MUST(JSON_READ(io, \"rot\", &e->rot));\n\n    JSON_MUST(JSON_READ(io, \"start_level_ordinal\", &e->start_level_ordinal));\n    JSON_MUST(JSON_READ(\n        io, \"completion_level_ordinal\", &e->completion_level_ordinal));\n\n    JSON_MUST(JSON_PUSH(io, \"prereq_zones\"));\n    const int32_t prereq_count = JSON_ARRAY_LEN(io);\n    if (prereq_count < 0) {\n        JSON_FAIL();\n    }\n    uint32_t computed_prereq_mask = 0;\n    for (int32_t i = 0; i < prereq_count; i++) {\n        int32_t zone = JSON_INVALID_NUMBER;\n        JSON_MUST(JSON_READ_A(io, i, &zone));\n        if (zone < 0 || zone >= MAX_GLOBE_ZONES) {\n            JSON_ReadIO_SetError(\n                io, \"'prereq_zones' entries must be in range 0..%d\",\n                MAX_GLOBE_ZONES - 1);\n            JSON_FAIL();\n        }\n        computed_prereq_mask |= 1u << zone;\n    }\n    e->prereq_mask = computed_prereq_mask;\n    JSON_MUST(JSON_POP(io));\n\n    int32_t mesh_idx = JSON_INVALID_NUMBER;\n    JSON_MUST(JSON_READ(io, \"mesh_idx\", &mesh_idx));\n    e->mesh_idx = (uint8_t)mesh_idx;\n    JSON_FINISH();\n}\n\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleIntEvent)\n{\n    JSON_READ_IO *const io = ctx->io;\n    if (event != nullptr) {\n        int32_t value;\n        JSON_READ_D(io, user_arg, &value, -1);\n        event->data = (void *)(intptr_t)value;\n    }\n    return 0;\n}\n\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandlePictureEvent)\n{\n    JSON_READ_IO *const io = ctx->io;\n    char *expanded_path = nullptr;\n    JSON_SHOULD(M_ReadPath(\n        io, \"path\", false, TRX_DYNAMIC_PATH_IMAGE_FILE, &expanded_path,\n        event == nullptr));\n\n    if (event != nullptr) {\n        GF_DISPLAY_PICTURE_DATA *const event_data = extra_data;\n        JSON_READ_D(io, \"legal\", &event_data->is_legal, false);\n        JSON_READ_D(io, \"credit\", &event_data->is_credit, false);\n        JSON_READ_D(io, \"display_time\", &event_data->display_time, 5.0f);\n        JSON_READ_D(io, \"fade_in_time\", &event_data->fade_in_time, 1.0f);\n        JSON_READ_D(\n            io, \"fade_out_time\", &event_data->fade_out_time, 1.0f / 3.0f);\n        if (expanded_path != nullptr) {\n            event_data->path =\n                (char *)extra_data + sizeof(GF_DISPLAY_PICTURE_DATA);\n            strcpy(event_data->path, expanded_path);\n        } else {\n            event_data->path = nullptr;\n        }\n        event->data = event_data;\n    }\n\n    const int32_t out_size = sizeof(GF_DISPLAY_PICTURE_DATA)\n        + (expanded_path == nullptr ? 0 : strlen(expanded_path) + 1);\n    Memory_FreePointer(&expanded_path);\n    return out_size;\n\nfail:\n    Memory_FreePointer(&expanded_path);\n    return 0;\n}\n\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleTotalStatsEvent)\n{\n    JSON_READ_IO *const io = ctx->io;\n    char *expanded_path = nullptr;\n    JSON_SHOULD(M_ReadPath(\n        io, \"background_path\", false, TRX_DYNAMIC_PATH_IMAGE_FILE,\n        &expanded_path, event == nullptr));\n    if (expanded_path == nullptr) {\n        if (event != nullptr) {\n            event->data = nullptr;\n        }\n        return 0;\n    }\n    if (event != nullptr) {\n        char *const event_data = extra_data;\n        strcpy(event_data, expanded_path);\n        event->data = event_data;\n    }\n    const int32_t out_size = strlen(expanded_path) + 1;\n    Memory_FreePointer(&expanded_path);\n    return out_size;\n}\n\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleAddItemEvent)\n{\n    JSON_READ_IO *const io = ctx->io;\n    OBJECT_ID obj_id = NO_OBJECT;\n    JSON_MUST(JSON_PUSH(io, \"object_id\"));\n    JSON_MUST(M_ReadObjectID(ctx, &obj_id));\n    JSON_MUST(JSON_POP(io));\n    if (event != nullptr) {\n        GF_ADD_ITEM_DATA *const event_data = extra_data;\n        event_data->object_id = obj_id;\n        JSON_READ_D(io, \"quantity\", &event_data->quantity, 1);\n        event_data->inv_type =\n            event->type == GFS_ADD_ITEM ? GF_INV_REGULAR : GF_INV_SECRET;\n        event->data = event_data;\n    }\n    return sizeof(GF_ADD_ITEM_DATA);\nfail:\n    return -1;\n}\n\nstatic M_DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleGlobeSelectEvent)\n{\n    JSON_READ_IO *const io = ctx->io;\n    const char *image;\n    JSON_READ_D(io, \"image\", &image, nullptr);\n    char *expanded_image =\n        Memory_DupStr(TRXPath_TryResolve(TRX_DYNAMIC_PATH_IMAGE_FILE, image));\n    if (expanded_image == nullptr) {\n        if (event != nullptr) {\n            event->data = nullptr;\n        }\n        return 0;\n    }\n\n    if (event != nullptr) {\n        GF_GLOBE_SELECT_DATA *const event_data = extra_data;\n        event_data->image_path =\n            (char *)extra_data + sizeof(GF_GLOBE_SELECT_DATA);\n        strcpy(event_data->image_path, expanded_image);\n        event->data = event_data;\n    }\n\n    const int32_t out_size =\n        sizeof(GF_GLOBE_SELECT_DATA) + strlen(expanded_image) + 1;\n    Memory_FreePointer(&expanded_image);\n    return out_size;\n}\n\nstatic bool M_LoadArray(\n    const M_CONTEXT *const ctx, const char *const key, int32_t *const count,\n    void **const elements, const size_t element_size,\n    const M_LOAD_ARRAY_FUNC load_func, void *const load_func_arg)\n{\n    if (!JSON_OPTIONAL(JSON_PUSH(ctx->io, key))) {\n        return true;\n    }\n    const int32_t elem_count = JSON_ARRAY_LEN(ctx->io);\n    if (elem_count < 0) {\n        JSON_FAIL();\n    }\n    *count = elem_count;\n    *elements = Memory_Alloc(element_size * (size_t)(*count));\n\n    for (size_t i = 0; i < (size_t)elem_count; i++) {\n        void *const element = (char *)*elements + i * element_size;\n        JSON_MUST(JSON_PUSH_INDEX(ctx->io, i));\n        JSON_MUST(load_func(ctx, element, i, load_func_arg));\n        JSON_MUST(JSON_POP(ctx->io));\n    }\n    JSON_MUST(JSON_POP(ctx->io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadGlobeSelectEntries(const M_CONTEXT *const ctx)\n{\n    ctx->gf->globe.count = 0;\n    ctx->gf->globe.entries = nullptr;\n    return M_LoadArray(\n        ctx, \"globe_select_entries\", &ctx->gf->globe.count,\n        (void **)&ctx->gf->globe.entries, sizeof(GF_GLOBE_ENTRY),\n        M_LoadGlobeEntry, nullptr);\n}\n\nstatic int32_t M_LoadSequenceEvent(\n    const M_CONTEXT *const ctx, GF_SEQUENCE_EVENT *const event,\n    void *const extra_data)\n{\n    const char *type_str = nullptr;\n    JSON_MUST(JSON_READ(ctx->io, \"type\", &type_str));\n    const GF_SEQUENCE_EVENT_TYPE type =\n        ENUM_MAP_GET(GF_SEQUENCE_EVENT_TYPE, type_str, -1);\n    if (type == (GF_SEQUENCE_EVENT_TYPE)-1) {\n        JSON_ReadIO_SetError(\n            ctx->io, \"unknown game flow sequence event type: '%s'\", type_str);\n        JSON_FAIL();\n    }\n\n    const M_SEQUENCE_EVENT_HANDLER *handler = M_GetSequenceEventHandlers();\n    while (handler->event_type != (GF_SEQUENCE_EVENT_TYPE)-1\n           && handler->event_type != type) {\n        handler++;\n    }\n\n    int32_t extra_data_size = 0;\n    if (handler->handler_func != nullptr) {\n        extra_data_size = handler->handler_func(\n            ctx, nullptr, nullptr, handler->handler_func_arg);\n    }\n    if (extra_data_size >= 0 && event != nullptr) {\n        event->type = handler->event_type;\n        if (handler->handler_func != nullptr) {\n            handler->handler_func(\n                ctx, event, extra_data, handler->handler_func_arg);\n        } else {\n            event->data = nullptr;\n        }\n    }\n    return extra_data_size;\nfail:\n    return -1;\n}\n\nstatic bool M_LoadSequence(\n    const M_CONTEXT *const ctx, GF_SEQUENCE *const sequence)\n{\n    JSON_READ_IO *const io = ctx->io;\n    sequence->length = 0;\n    const int32_t seq_count = JSON_ARRAY_LEN(io);\n    if (seq_count < 0) {\n        JSON_FAIL();\n    }\n\n    const size_t event_base_size = sizeof(GF_SEQUENCE_EVENT);\n    size_t total_data_size = 0;\n    for (int32_t i = 0; i < seq_count; i++) {\n        JSON_MUST(JSON_PUSH_INDEX(io, i));\n        const int32_t event_extra_size =\n            M_LoadSequenceEvent(ctx, nullptr, nullptr);\n        JSON_MUST(JSON_POP(io));\n        if (event_extra_size < 0) {\n            JSON_FAIL();\n        }\n        sequence->length++;\n        total_data_size += Memory_Align((size_t)event_extra_size);\n    }\n\n    const size_t events_block_size =\n        Memory_Align((size_t)sequence->length * event_base_size);\n    total_data_size += events_block_size;\n\n    char *const data = Memory_Alloc(total_data_size);\n    char *extra_data_ptr = data + events_block_size;\n    sequence->events = (GF_SEQUENCE_EVENT *)data;\n\n    int32_t j = 0;\n    for (int32_t i = 0; i < seq_count; i++) {\n        JSON_MUST(JSON_PUSH_INDEX(io, i));\n        const int32_t event_extra_size =\n            M_LoadSequenceEvent(ctx, &sequence->events[j], extra_data_ptr);\n        JSON_MUST(JSON_POP(io));\n        if (event_extra_size < 0) {\n            // Parsing this event failed - discard it\n            continue;\n        }\n        extra_data_ptr += Memory_Align((size_t)event_extra_size);\n        j++;\n    }\n    JSON_FINISH();\n}\n\nstatic bool M_LoadLevelInjections(\n    const M_CONTEXT *const ctx, GF_LEVEL *const level)\n{\n    JSON_READ_IO *const io = ctx->io;\n    bool inherit;\n    JSON_READ_D(io, \"inherit_injections\", &inherit, true);\n    int32_t local_count = 0;\n    if (JSON_PUSH(io, \"injections\")) {\n        local_count = JSON_ARRAY_LEN(io);\n        if (local_count < 0) {\n            JSON_FAIL();\n        }\n        JSON_MUST(JSON_POP(io));\n    }\n\n    level->injections.count = 0;\n    if (local_count == 0 && !inherit) {\n        return true;\n    }\n\n    if (inherit) {\n        level->injections.count += ctx->gf->injections.count;\n    }\n    level->injections.count += local_count;\n\n    level->injections.data_paths =\n        Memory_Alloc(sizeof(char *) * level->injections.count);\n\n    int32_t base_index = 0;\n    if (inherit) {\n        for (int32_t i = 0; i < ctx->gf->injections.count; i++) {\n            level->injections.data_paths[i] =\n                Memory_DupStr(ctx->gf->injections.data_paths[i]);\n        }\n        base_index = ctx->gf->injections.count;\n    }\n\n    if (local_count == 0) {\n        return true;\n    }\n\n    JSON_MUST(JSON_PUSH(io, \"injections\"));\n    for (int32_t i = 0; i < local_count; i++) {\n        const char *str = nullptr;\n        JSON_MUST(JSON_READ_A(io, i, &str));\n        level->injections.data_paths[base_index + i] = Memory_DupStr(\n            TRXPath_TryResolve(TRX_DYNAMIC_PATH_INJECTION_FILE, str));\n    }\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadLevelSequence(\n    const M_CONTEXT *const ctx, GF_LEVEL *const level)\n{\n    JSON_MUST(JSON_PUSH(ctx->io, \"sequence\"));\n    JSON_MUST(M_LoadSequence(ctx, &level->sequence));\n    JSON_MUST(JSON_POP(ctx->io));\n\n    for (int32_t i = 0; i < level->sequence.length; i++) {\n        GF_SEQUENCE_EVENT *const event = &level->sequence.events[i];\n        if (event->type == GFS_LOOP_GAME) {\n            event->data = (void *)(intptr_t)level->num;\n        }\n    }\n    JSON_FINISH();\n}\n\nstatic bool M_LoadLevel(\n    const M_CONTEXT *const ctx, void *const target_elem, const size_t idx,\n    void *const user_arg)\n{\n    GF_LEVEL *const level = target_elem;\n    JSON_READ_IO *const io = ctx->io;\n    level->num = idx;\n\n    {\n        level->type = (GF_LEVEL_TYPE)(intptr_t)user_arg;\n        if (JSON_OPTIONAL(JSON_PUSH(io, \"type\"))) {\n            const char *tmp = nullptr;\n            JSON_MUST(JSON_READ_CURRENT(io, &tmp));\n            JSON_MUST(JSON_POP(io));\n            const GF_LEVEL_TYPE user_type =\n                ENUM_MAP_GET(GF_LEVEL_TYPE, tmp, -1);\n            if (user_type == (GF_LEVEL_TYPE)-1) {\n                JSON_ReadIO_SetError(io, \"unrecognized type '%s'\", tmp);\n                JSON_FAIL();\n            }\n\n            if (level->type != GFL_NORMAL\n                && GF_GetLevelTableType(user_type) != GFLT_MAIN) {\n                JSON_ReadIO_SetError(\n                    io, \"cannot override level type=%s to %s\",\n                    ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type),\n                    ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, user_type));\n                JSON_FAIL();\n            }\n            level->type = user_type;\n        }\n    }\n\n    if (level->type == GFL_DUMMY) {\n        return true;\n    }\n\n    {\n        const TRX_DYNAMIC_PATH path_type =\n            (level->type == GFL_DUMMY || level->type == GFL_CURRENT)\n            ? (TRX_DYNAMIC_PATH)-1\n            : (level->type == GFL_TITLE || level->type == GFL_GYM)\n            ? TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE\n            : TRX_DYNAMIC_PATH_LEVEL_FILE;\n        JSON_MUST(\n            M_ReadPath(io, \"path\", false, path_type, &level->path, false));\n    }\n    {\n        const char *tmp_script = nullptr;\n        if (JSON_OPTIONAL(JSON_READ(io, \"script\", &tmp_script))\n            && tmp_script != nullptr) {\n            level->script_path = Memory_DupStr(\n                TRXPath_TryResolve(TRX_DYNAMIC_PATH_SCRIPT_FILE, tmp_script));\n        } else {\n            level->script_path = nullptr;\n        }\n    }\n\n    {\n        level->music_track = MX_INACTIVE;\n        if (JSON_OPTIONAL(JSON_PUSH(io, \"music_track\"))) {\n            JSON_MUST(JSON_READ_CURRENT(io, &level->music_track));\n            JSON_MUST(JSON_POP(io));\n        }\n    }\n\n    {\n        const bool outfit_optional = level->type == GFL_TITLE\n            || level->type == GFL_DUMMY || level->type == GFL_CURRENT;\n        if (!outfit_optional) {\n            const char *tmp = nullptr;\n            JSON_MUST(JSON_READ(io, \"lara_outfit\", &tmp));\n            if (!ctx->validation_mode\n                && !Lara_Skin_IsOutfitAvailable(\n                    Lara_Skin_FindOutfitByName(tmp))) {\n                JSON_ReadIO_SetError(\n                    io, \"invalid 'lara_outfit' value (%s)\", tmp);\n                JSON_FAIL();\n            }\n            level->lara_outfit = Memory_DupStr(tmp);\n        }\n    }\n\n    level->weather_type = WEATHER_NONE;\n    {\n        if (JSON_OPTIONAL(JSON_PUSH(io, \"weather_type\"))) {\n            const char *tmp = nullptr;\n            JSON_MUST(JSON_READ_CURRENT(io, &tmp));\n            JSON_MUST(JSON_POP(io));\n            level->weather_type = ENUM_MAP_GET(WEATHER_TYPE, tmp, WEATHER_NONE);\n        }\n    }\n    JSON_READ_D(io, \"water_particles\", &level->water_particles, false);\n\n    JSON_READ_D(io, \"unobtainable_pickups\", &level->unobtainable.pickups, 0);\n    JSON_READ_D(io, \"unobtainable_kills\", &level->unobtainable.kills, 0);\n    JSON_READ_D(\n        io, \"unobtainable_ally_kills\", &level->unobtainable.ally_kills, 0);\n    JSON_READ_D(io, \"unobtainable_secrets\", &level->unobtainable.secrets, 0);\n\n    M_CopyRootSettingsIntoLevel(&level->settings, &ctx->gf->settings);\n\n    JSON_MUST(M_LoadSettings(ctx, &level->settings));\n    JSON_MUST(M_LoadLevelItemDrops(ctx, level));\n    JSON_MUST(M_LoadLevelSequence(ctx, level));\n    JSON_MUST(M_LoadLevelInjections(ctx, level));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadLevelTable(\n    const M_CONTEXT *const ctx, const char *const key,\n    GF_LEVEL_TABLE *const level_table, const GF_LEVEL_TYPE default_level_type)\n{\n    JSON_MUST(M_LoadArray(\n        ctx, key, &level_table->count, (void **)&level_table->levels,\n        sizeof(GF_LEVEL), M_LoadLevel, (void *)(intptr_t)default_level_type));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadLevels(const M_CONTEXT *const ctx)\n{\n    JSON_MUST(JSON_PUSH(ctx->io, \"levels\"));\n    JSON_MUST(JSON_POP(ctx->io));\n    JSON_MUST(M_LoadLevelTable(\n        ctx, \"levels\", &ctx->gf->level_tables[GFLT_MAIN], GFL_NORMAL));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadCutscenes(const M_CONTEXT *const ctx)\n{\n    return M_LoadLevelTable(\n        ctx, \"cutscenes\", &ctx->gf->level_tables[GFLT_CUTSCENES], GFL_CUTSCENE);\n}\n\nstatic bool M_LoadDemos(const M_CONTEXT *const ctx)\n{\n    return M_LoadLevelTable(\n        ctx, \"demos\", &ctx->gf->level_tables[GFLT_DEMOS], GFL_DEMO);\n}\n\nstatic bool M_LoadTitleLevel(const M_CONTEXT *const ctx)\n{\n    if (!JSON_OPTIONAL(JSON_PUSH(ctx->io, \"title\"))) {\n        return true;\n    }\n    ctx->gf->title_level = Memory_Alloc(sizeof(GF_LEVEL));\n    JSON_MUST(\n        M_LoadLevel(ctx, ctx->gf->title_level, 0, (void *)(intptr_t)GFL_TITLE));\n    JSON_MUST(JSON_POP(ctx->io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadFMV(\n    const M_CONTEXT *const ctx, void *const target_elem, size_t idx,\n    void *const user_arg)\n{\n    GF_FMV *const fmv = target_elem;\n    ASSERT(user_arg == nullptr);\n    JSON_READ_IO *const io = ctx->io;\n    char *path = nullptr;\n    JSON_SHOULD(\n        M_ReadPath(io, \"path\", false, TRX_DYNAMIC_PATH_FMV_FILE, &path, false));\n    fmv->path = path;\n    JSON_READ_D(io, \"legal\", &fmv->is_legal, false);\n    JSON_READ_D(io, \"credit\", &fmv->is_credit, false);\n    JSON_FINISH();\n}\n\nstatic bool M_LoadFMVs(const M_CONTEXT *const ctx)\n{\n    JSON_MUST(M_LoadArray(\n        ctx, \"fmvs\", &ctx->gf->fmv_count, (void **)&ctx->gf->fmvs,\n        sizeof(GF_FMV), M_LoadFMV, nullptr));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadGlobalInjections(const M_CONTEXT *const ctx)\n{\n    JSON_READ_IO *const io = ctx->io;\n    ctx->gf->injections.count = 0;\n    if (!JSON_PUSH(io, \"injections\")) {\n        return true;\n    }\n    ctx->gf->injections.count = JSON_ARRAY_LEN(io);\n    if (ctx->gf->injections.count < 0) {\n        JSON_FAIL();\n    }\n    ctx->gf->injections.data_paths =\n        Memory_Alloc(sizeof(char *) * (size_t)ctx->gf->injections.count);\n    for (int32_t i = 0; i < ctx->gf->injections.count; i++) {\n        const char *str = nullptr;\n        JSON_MUST(JSON_READ_A(io, i, &str));\n        ctx->gf->injections.data_paths[i] = Memory_DupStr(\n            TRXPath_TryResolve(TRX_DYNAMIC_PATH_INJECTION_FILE, str));\n    }\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadGameFlowDoc(\n    JSON_VALUE *const doc, const char *const path, const bool exit_on_error,\n    const bool validation_mode)\n{\n    GF_Shutdown();\n\n    M_CONTEXT ctx = { .gf = &g_GameFlow, .validation_mode = validation_mode };\n    ctx.gf->main_script_path = nullptr;\n    ctx.gf->path = Memory_DupStr(path);\n    ctx.script_path = g_GameFlow.path;\n    ctx.io = nullptr;\n\n    ctx.io = JSON_ReadIO_Create(doc, 0, path);\n\n    JSON_MUST(M_LoadRoot(&ctx));\n    JSON_MUST(M_LoadSettings(&ctx, &ctx.gf->settings));\n    JSON_MUST(M_LoadGlobeSelectEntries(&ctx));\n    JSON_MUST(M_LoadGlobalInjections(&ctx));\n    JSON_MUST(M_LoadLevels(&ctx));\n    JSON_MUST(M_LoadCutscenes(&ctx));\n    JSON_MUST(M_LoadDemos(&ctx));\n    JSON_MUST(M_LoadFMVs(&ctx));\n    JSON_MUST(M_LoadTitleLevel(&ctx));\n\n    JSON_ReadIO_Destroy(ctx.io);\n    JSON_ValueFree(doc);\n    return true;\nfail:\n    if (exit_on_error) {\n        M_ExitWithJSONError(&ctx);\n    } else {\n        LOG_WARNING(\"%s\", JSON_ReadIO_GetError(ctx.io));\n        JSON_ReadIO_Destroy(ctx.io);\n        JSON_ValueFree(doc);\n        GF_Shutdown();\n    }\n    return false;\n}\n\nstatic bool M_LoadGameFlowEx(\n    const char *const path, const bool exit_on_error,\n    const bool validation_mode)\n{\n    JSON_VALUE *const doc = JSONFile_ReadEx(path, exit_on_error);\n    if (doc == nullptr) {\n        if (exit_on_error) {\n            Shell_ExitSystemFmt(\"Failed to open script file %s\", path);\n        }\n        return false;\n    }\n    return M_LoadGameFlowDoc(doc, path, exit_on_error, validation_mode);\n}\n\nvoid GF_LoadFromFile(const char *const path)\n{\n    M_LoadGameFlowEx(path, true, false);\n}\n\nbool GF_TryLoadFromFile(const char *const path)\n{\n    return M_LoadGameFlowEx(path, false, false);\n}\n\nbool GF_ValidateMod(const char *const mod_name, const char *const path)\n{\n    if (!M_LoadGameFlowEx(path, false, true)) {\n        LOG_WARNING(\"Mod '%s' has invalid gameflow data\", mod_name);\n        return false;\n    }\n    GF_Shutdown();\n    return true;\n}\n\nbool GF_ReadModMeta(const char *const path, GF_MOD_META *const meta)\n{\n    ASSERT(meta != nullptr);\n\n    JSON_VALUE *const doc = JSONFile_Read(path);\n    if (doc == nullptr) {\n        return false;\n    }\n\n    JSON_READ_IO *const io = JSON_ReadIO_Create(doc, 0, path);\n    M_ReadModMeta(io, meta);\n    JSON_ReadIO_Destroy(io);\n    JSON_ValueFree(doc);\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/reader.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\n// Load the game flow from a file.\nvoid GF_LoadFromFile(const char *path);\n\n// Load the game flow from a file.\n// Returns false on I/O or parse/validation failure instead of exiting.\nbool GF_TryLoadFromFile(const char *path);\n\n// Load and validate path-backed gameflow references for a mod.\n// Returns false if parsing fails or any required resolved path is missing.\nbool GF_ValidateMod(const char *mod_name, const char *path);\n\n// Quick-parse a gameflow file to extract only the mod metadata fields\n// (\"name\", \"engine\", and \"extends\"). Returns true on success. Caller must\n// free meta->name and meta->extends with Memory_FreePointer().\nbool GF_ReadModMeta(const char *path, GF_MOD_META *meta);\n"
  },
  {
    "path": "src/trx/game/game_flow/sequencer.c",
    "content": "#include <trx/game/game_flow/sequencer.h>\n\n#include <trx/core/enum_map.h>\n#include <trx/debug.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow/sequencer_events.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/level.h>\n#include <trx/game/lua.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n\nstatic GF_COMMAND M_RunEvent(\n    const GF_LEVEL *const level, const GF_SEQUENCE *const sequence,\n    const int32_t event_idx, const GF_SEQUENCE_CONTEXT seq_ctx,\n    void *const seq_ctx_arg)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    LOG_DEBUG(\n        \"event type=%s(%d) data=0x%x\",\n        ENUM_MAP_TO_STRING(GF_SEQUENCE_EVENT_TYPE, event->type), event->type,\n        event->data);\n\n    const GF_SEQUENCE_EVENT_HANDLER event_handler =\n        GF_GetSequenceEventHandler(event->type);\n    if (event_handler == nullptr) {\n        return gf_cmd;\n    }\n\n    gf_cmd = event_handler(level, sequence, event_idx, seq_ctx, seq_ctx_arg);\n    LOG_DEBUG(\n        \"event type=%s(%d) data=0x%x finished, result: action=%s, \"\n        \"param=%d\",\n        ENUM_MAP_TO_STRING(GF_SEQUENCE_EVENT_TYPE, event->type), event->type,\n        event->data, ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action),\n        gf_cmd.param);\n    return gf_cmd;\n}\n\nstatic void M_PreSequenceHook(\n    const GF_SEQUENCE_CONTEXT seq_ctx, void *const seq_ctx_arg)\n{\n    Room_SetAbyssHeight(0);\n    Output_SetSunsetEnabled(false);\n    Lara_SetControllable(false);\n    Lara_SetStartAnimState(LS_EXTRA_BREATH);\n    if (seq_ctx == GFSC_SAVED) {\n        Game_SetBonusFlag(GBF_NONE);\n    }\n}\n\nstatic GF_SEQUENCE_CONTEXT M_SwitchSequenceContext(\n    const GF_SEQUENCE_EVENT *const event, const GF_SEQUENCE_CONTEXT seq_ctx)\n{\n    // Update sequence context if necessary\n    if (event->type != GFS_LOOP_GAME) {\n        return seq_ctx;\n    }\n    switch (seq_ctx) {\n    case GFSC_SAVED:\n    case GFSC_RESTART:\n    case GFSC_SELECT:\n        return GFSC_NORMAL;\n    default:\n        return seq_ctx;\n    }\n}\n\nstatic const GF_LEVEL *M_GetCanonicalNextLevel(const GF_LEVEL *const level)\n{\n    // Canonical order is still used for console-driven linear simulation.\n    return GF_GetLevelAfter(level);\n}\n\nstatic const GF_LEVEL *M_GetLinkedPrevLevel(const GF_LEVEL *const level)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (resume == nullptr) {\n        return nullptr;\n    }\n    if (resume->prev_level == -1) {\n        return nullptr;\n    }\n    return GF_GetLevel(GFLT_MAIN, resume->prev_level);\n}\n\nstatic bool M_IsLevelDescendantOf(\n    const GF_LEVEL *const level, const int32_t ancestor_level_num)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (resume == nullptr) {\n        return false;\n    }\n\n    const int32_t count = GF_GetLevelTable(GFLT_MAIN)->count;\n    int32_t current_prev = resume->prev_level;\n    for (int32_t i = 0; i < count && current_prev != -1; i++) {\n        if (current_prev == ancestor_level_num) {\n            return true;\n        }\n        const GF_LEVEL *const prev_level = GF_GetLevel(GFLT_MAIN, current_prev);\n        if (prev_level == nullptr) {\n            break;\n        }\n        RESUME_INFO *const prev_resume = Savegame_GetCurrentInfo(prev_level);\n        if (prev_resume == nullptr) {\n            break;\n        }\n        current_prev = prev_resume->prev_level;\n    }\n    return false;\n}\n\nGF_COMMAND GF_InterpretSequence(\n    const GF_LEVEL *const level, GF_SEQUENCE_CONTEXT seq_ctx,\n    void *const seq_ctx_arg)\n{\n    ASSERT(level != nullptr);\n    LOG_DEBUG(\n        \"running sequence for level=%d type=%d seq_ctx=%d\", level->num,\n        level->type, seq_ctx);\n\n    if (level->type == GFL_DUMMY || level->type == GFL_CURRENT) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n\n    M_PreSequenceHook(seq_ctx, seq_ctx_arg);\n\n    GF_COMMAND gf_cmd = { .action = GF_EXIT_TO_TITLE };\n\n    const GF_LEVEL *const prev_level = M_GetLinkedPrevLevel(level);\n\n    // before load\n    switch (seq_ctx) {\n    case GFSC_STORY:\n        break;\n\n    case GFSC_SAVED:\n        GF_InventoryModifier_Scan(level);\n        // reset current info to the defaults so that we do not do\n        // Item_GlobalReplace in the inventory initialization routines too early\n        Savegame_InitCurrentInfo();\n        break;\n\n    case GFSC_RESTART:\n        if (level == GF_GetGymLevel() || level == GF_GetFirstLevel()) {\n            Savegame_InitCurrentInfo();\n        } else {\n            const int32_t prev_level_num =\n                Savegame_GetCurrentInfo(level)->prev_level;\n            Savegame_ResetCurrentInfo(level);\n            if (prev_level_num != -1) {\n                const GF_LEVEL *const linked_prev_level =\n                    GF_GetLevel(GFLT_MAIN, prev_level_num);\n                if (linked_prev_level != nullptr) {\n                    Savegame_CarryCurrentInfoToNextLevel(\n                        linked_prev_level, level);\n                }\n            }\n            Savegame_ApplyLogicToCurrentInfo(level);\n        }\n        if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {\n            GF_InventoryModifier_Scan(level);\n            GF_InventoryModifier_ApplyToResumeInfo(level);\n        }\n        break;\n\n    case GFSC_SELECT: {\n        const SAVEGAME_SLOT_REF slot = Savegame_GetBoundSlot();\n        if (Savegame_IsValidSlotRef(slot)) {\n            // select level feature\n            Savegame_InitCurrentInfo();\n            if (level->num > GF_GetFirstLevel()->num) {\n                Savegame_LoadOnlyResumeInfo(slot);\n\n                const int32_t prev_level_num =\n                    Savegame_GetCurrentInfo(level)->prev_level;\n\n                const GF_LEVEL_TABLE *const level_table =\n                    GF_GetLevelTable(GFLT_MAIN);\n                for (int32_t i = 0; i < level_table->count; i++) {\n                    const GF_LEVEL *const tmp_level = &level_table->levels[i];\n                    if (tmp_level->type == GFL_GYM) {\n                        continue;\n                    }\n                    if (tmp_level == level\n                        || M_IsLevelDescendantOf(tmp_level, level->num)) {\n                        Savegame_ResetCurrentInfo(tmp_level);\n                    }\n                }\n\n                if (prev_level_num != -1) {\n                    const GF_LEVEL *const linked_prev_level =\n                        GF_GetLevel(GFLT_MAIN, prev_level_num);\n                    if (linked_prev_level != nullptr) {\n                        Savegame_CarryCurrentInfoToNextLevel(\n                            linked_prev_level, level);\n                    }\n                }\n\n                Savegame_ApplyLogicToCurrentInfo(level);\n                GF_InventoryModifier_Scan(level);\n                GF_InventoryModifier_ApplyToResumeInfo(level);\n            } else {\n                Savegame_ApplyLogicToCurrentInfo(level);\n                GF_InventoryModifier_Scan(level);\n                GF_InventoryModifier_ApplyToResumeInfo(level);\n            }\n        } else {\n            // console /play level feature\n            Inv_RemoveAllItems();\n            if (level == GF_GetGymLevel()) {\n                Savegame_InitCurrentInfo();\n                GF_InventoryModifier_Scan(level);\n                GF_InventoryModifier_ApplyToResumeInfo(level);\n            } else {\n                const GF_LEVEL *tmp_level = GF_GetFirstLevel();\n                Savegame_ResetCurrentInfo(tmp_level);\n                while (tmp_level != nullptr && tmp_level <= level) {\n                    Savegame_ApplyLogicToCurrentInfo(tmp_level);\n                    GF_InventoryModifier_Scan(tmp_level);\n                    GF_InventoryModifier_ApplyToResumeInfo(tmp_level);\n                    if (tmp_level == level) {\n                        break;\n                    }\n\n                    const GF_LEVEL *const next_level =\n                        M_GetCanonicalNextLevel(tmp_level);\n                    if (next_level != nullptr) {\n                        Savegame_CarryCurrentInfoToNextLevel(\n                            tmp_level, next_level);\n                    }\n                    tmp_level = next_level;\n                }\n            }\n        }\n        break;\n    }\n\n    default:\n        if (level->type == GFL_GYM) {\n            Savegame_ResetCurrentInfo(level);\n            Savegame_ApplyLogicToCurrentInfo(level);\n        } else if (level->type == GFL_DEMO) {\n            Savegame_ApplyLogicToCurrentInfo(level);\n        } else if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {\n            Savegame_ApplyLogicToCurrentInfo(level);\n            GF_InventoryModifier_Scan(level);\n            GF_InventoryModifier_ApplyToResumeInfo(level);\n        }\n    }\n\n    // Run any level Lua script\n    Lua_ClearLevelListeners();\n    Lua_SetScriptContext(LUA_CONTEXT_LEVEL);\n    if (level->script_path != nullptr) {\n        LUA_RESULT res = Lua_EvalFile(level->script_path);\n        if (res.code != LUA_OK) {\n            LOG_ERROR(\"Lua level script error: %s\", res.message);\n        }\n        Lua_FreeResult(&res);\n    }\n    Lua_SetScriptContext(LUA_CONTEXT_GLOBAL);\n\n    // load the level\n    if (seq_ctx != GFSC_STORY || level->type == GFL_CUTSCENE) {\n        if (!Level_Initialise(level, seq_ctx)) {\n            Game_SetCurrentLevel(nullptr);\n            GF_SetCurrentLevel(nullptr);\n            if (level->type == GFL_TITLE) {\n                gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME };\n            } else {\n                gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n            }\n        }\n    }\n\n    const GF_SEQUENCE *const sequence = &level->sequence;\n    for (int32_t i = 0; i < sequence->length; i++) {\n        const GF_SEQUENCE_EVENT *const event = &sequence->events[i];\n        gf_cmd = M_RunEvent(level, sequence, i, seq_ctx, seq_ctx_arg);\n        if (gf_cmd.action != GF_NOOP) {\n            return gf_cmd;\n        }\n\n        // Update sequence context if necessary\n        seq_ctx = M_SwitchSequenceContext(event, seq_ctx);\n    }\n\n    LOG_DEBUG(\n        \"sequence finished: action=%s param=%d\",\n        ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action), gf_cmd.param);\n    return gf_cmd;\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/sequencer.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/savegame/types.h>\n\nGF_COMMAND GF_EnterPhotoMode(void);\nGF_COMMAND GF_PauseGame(void);\nGF_COMMAND GF_ShowInventory(INVENTORY_MODE inv_mode);\nbool GF_ShowInventoryKeys(OBJECT_ID receptacle_type_id);\nGF_COMMAND GF_RunTitle(void);\nGF_COMMAND GF_RunDemo(int32_t demo_num);\nGF_COMMAND GF_RunCutscene(int32_t cutscene_num, bool cross_fade_in);\nGF_COMMAND GF_RunGame(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx);\nGF_COMMAND GF_RunGlobeSelect(const char *background_path);\n\nGF_COMMAND GF_DoFrontendSequence(void);\nGF_COMMAND GF_DoDemoSequence(int32_t demo_num);\nGF_COMMAND GF_DoCutsceneSequence(int32_t cutscene_num, bool cross_fade_in);\n\nGF_COMMAND GF_InterpretSequence(\n    const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx, void *seq_ctx_arg);\n\nGF_COMMAND GF_DoLevelSequence(\n    const GF_LEVEL *start_level, GF_SEQUENCE_CONTEXT seq_ctx);\n\nGF_COMMAND GF_PlayAvailableStory(SAVEGAME_SLOT_REF slot);\n"
  },
  {
    "path": "src/trx/game/game_flow/sequencer_events.c",
    "content": "#include <trx/game/game_flow/sequencer_events.h>\n\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/debug.h>\n#include <trx/game/fmv.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow/sequencer.h>\n#include <trx/game/game_flow/util.h>\n#include <trx/game/game_flow/vars.h>\n#include <trx/game/lara.h>\n#include <trx/game/lua.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/creatures/bacon_lara.h>\n#include <trx/game/option/passport.h>\n#include <trx/game/output.h>\n#include <trx/game/phase.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell/paths.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_GF_HANDLER(name)                                                     \\\n    static GF_COMMAND name(                                                    \\\n        const GF_LEVEL *const level, const GF_SEQUENCE *const sequence,        \\\n        const int32_t event_idx, const GF_SEQUENCE_CONTEXT seq_ctx,            \\\n        void *const seq_ctx_arg)\n\n// clang-format off\n#define X_EVENT_HANDLER_LIST \\\n    X(GFS_EXIT_TO_TITLE,     M_HandleExitToTitle)                              \\\n    X(GFS_LEVEL_COMPLETE,    M_HandleLevelComplete)                            \\\n    X(GFS_LOOP_GAME,         M_HandlePlayLevel)                                \\\n    X(GFS_PLAY_CUTSCENE,     M_HandlePlayCutscene)                             \\\n    X(GFS_PLAY_FMV,          M_HandlePlayFMV)                                  \\\n    X(GFS_PLAY_MUSIC,        M_HandlePlayMusic)                                \\\n    X(GFS_ADD_ITEM,          M_HandleInventoryModifier)                        \\\n    X(GFS_REMOVE_WEAPONS,    M_HandleInventoryModifier)                        \\\n    X(GFS_REMOVE_AMMO,       M_HandleInventoryModifier)                        \\\n    X(GFS_REMOVE_MEDIPACKS,  M_HandleInventoryModifier)                        \\\n    X(GFS_REMOVE_SCIONS,     M_HandleInventoryModifier)                        \\\n    X(GFS_ADD_SECRET_REWARD, M_HandleInventoryModifier)                        \\\n    X(GFS_REMOVE_FLARES,     M_HandleInventoryModifier)                        \\\n    X(GFS_LOADING_SCREEN,    M_HandlePicture)                                  \\\n    X(GFS_DISPLAY_PICTURE,   M_HandlePicture)                                  \\\n    X(GFS_LEVEL_STATS,       M_HandleLevelStats)                               \\\n    X(GFS_TOTAL_STATS,       M_HandleTotalStats)                               \\\n    X(GFS_GLOBE_SELECT,      M_HandleGlobeSelect)                              \\\n    X(GFS_SET_START_ANIM,    M_HandleSetStartAnim)                             \\\n    X(GFS_ENABLE_SUNSET,     M_HandleEnableSunset)                             \\\n    X(GFS_SETUP_BACON_LARA,  M_HandleSetupBaconLara)                           \\\n    X(GFS_DISABLE_FLOOR,     M_HandleDisableFloor)\n// clang-format on\n\n#define X(id, name) M_GF_HANDLER(name);\nX_EVENT_HANDLER_LIST\n#undef X\n\nstatic GF_SEQUENCE_EVENT_HANDLER m_EventHandlers[GFS_NUMBER_OF] = {\n#define X(id, name) [id] = name,\n    X_EVENT_HANDLER_LIST\n#undef X\n    // clang-format on\n};\n\nstatic void M_FinishLevelBasic(void)\n{\n    const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n\n    if (current_level == GF_GetLastLevel()) {\n        g_Config.profile.new_game_plus_unlock = true;\n        Config_Update();\n    }\n\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level);\n    if (resume != nullptr) {\n        resume->flags.available = true;\n        resume->level_completed = true;\n    }\n}\n\nstatic const GF_LEVEL *M_GetCanonicalNextLevel(const GF_LEVEL *const level)\n{\n    // Canonical order is still used for regular level flow; resume inheritance\n    // is non-linear and tracked via RESUME_INFO.prev_level.\n    return GF_GetLevelAfter(level);\n}\n\nM_GF_HANDLER(M_HandleExitToTitle)\n{\n    return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n}\n\nM_GF_HANDLER(M_HandleLevelComplete)\n{\n    if (seq_ctx != GFSC_NORMAL) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    M_FinishLevelBasic();\n    const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n    const GF_LEVEL *const next_level = M_GetCanonicalNextLevel(current_level);\n\n    if (next_level == nullptr) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    Savegame_PersistGameToCurrentInfo(next_level);\n    RESUME_INFO *const next_resume = Savegame_GetCurrentInfo(next_level);\n    if (next_resume != nullptr) {\n        next_resume->prev_level = current_level->num;\n    }\n    if (next_level->type == GFL_BONUS && !Stats_CheckAllSecretsCollected()) {\n        return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n    }\n    return (GF_COMMAND) {\n        .action = GF_START_GAME,\n        .param = next_level->num,\n    };\n}\n\nM_GF_HANDLER(M_HandlePlayLevel)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n\n    if (seq_ctx == GFSC_STORY) {\n        const int32_t savegame_level_num = (int32_t)(intptr_t)seq_ctx_arg;\n        if (savegame_level_num == level->num) {\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        } else {\n            return (GF_COMMAND) { .action = GF_NOOP };\n        }\n    }\n\n    if (Lara_GetItem() != nullptr) {\n        Lara_Initialise(level);\n    }\n\n    if (level->music_track != MX_INACTIVE) {\n        Music_Stop();\n    }\n\n    Lua_FireEventInt32(LUA_EVENT_AFTER_LEVEL_FILE, level->num);\n\n    // post load\n    switch (seq_ctx) {\n    case GFSC_SAVED: {\n        const SAVEGAME_SLOT_REF slot = Savegame_GetBoundSlot();\n        if (!Savegame_Load(slot)) {\n            LOG_ERROR(\"Failed to load save file!\");\n            Game_SetCurrentLevel(nullptr);\n            GF_SetCurrentLevel(nullptr);\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n        break;\n    }\n\n    default:\n        if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {\n            Savegame_SetInitialVersion(SG_CURRENT_VERSION);\n            GF_InventoryModifier_Scan(Game_GetCurrentLevel());\n            GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_REGULAR);\n        }\n        break;\n    }\n    GF_DisableObjectsIfNeeded();\n\n    Lua_FireEventInt32(LUA_EVENT_AFTER_LEVEL_STATE, level->num);\n\n    g_Passport.ask_for_save = g_Config.gameplay.enable_save_crystals\n        && seq_ctx == GFSC_NORMAL\n        && GF_GetLevelTableType(level->type) == GFLT_MAIN\n        && level != GF_GetFirstLevel() && level != GF_GetGymLevel();\n\n    ASSERT(GF_GetCurrentLevel() == level);\n    if (level->type == GFL_DEMO) {\n        gf_cmd = GF_RunDemo(level->num);\n    } else if (level->type == GFL_CUTSCENE) {\n        gf_cmd = GF_RunCutscene(level->num, (bool)(intptr_t)seq_ctx_arg);\n    } else {\n        if (seq_ctx != GFSC_SAVED && level != GF_GetFirstLevel()) {\n            Lara_RevertToPistolsIfNeeded();\n        }\n        gf_cmd = GF_RunGame(level, seq_ctx);\n    }\n    if (gf_cmd.action == GF_LEVEL_COMPLETE) {\n        gf_cmd.action = GF_NOOP;\n    }\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandlePlayCutscene)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    const GF_SEQUENCE_EVENT *const prev_event =\n        event_idx > 0 ? &sequence->events[event_idx - 1] : nullptr;\n    const int16_t cutscene_num = (int16_t)(intptr_t)event->data;\n    const bool cross_fade_in =\n        prev_event != nullptr && prev_event->type == GFS_LOOP_GAME;\n    if (seq_ctx != GFSC_SAVED && g_Config.gameplay.enable_cutscenes) {\n        gf_cmd = GF_DoCutsceneSequence(cutscene_num, cross_fade_in);\n        if (gf_cmd.action == GF_LEVEL_COMPLETE) {\n            gf_cmd.action = GF_NOOP;\n        }\n    }\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandlePlayFMV)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    const int16_t fmv_id = (int16_t)(intptr_t)event->data;\n    if (seq_ctx == GFSC_SAVED) {\n        return gf_cmd;\n    }\n    if (fmv_id < 0 || fmv_id >= g_GameFlow.fmv_count) {\n        LOG_ERROR(\"Invalid FMV number: %d\", fmv_id);\n        return gf_cmd;\n    }\n    const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id];\n    if (fmv->is_legal && !g_Config.gameplay.enable_legal) {\n        return gf_cmd;\n    }\n    if (fmv->is_credit && !g_Config.gameplay.enable_credits) {\n        return gf_cmd;\n    }\n    FMV_Play(fmv->path);\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandlePlayMusic)\n{\n    if (seq_ctx != GFSC_STORY) {\n        const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n        Music_SetVolume(g_Config.audio.music_volume);\n        Music_Play_Direct((int32_t)(intptr_t)event->data, MPM_ONCE);\n    }\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nM_GF_HANDLER(M_HandlePicture)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    const GF_SEQUENCE_EVENT *const prev_event =\n        event_idx > 0 ? &sequence->events[event_idx - 1] : nullptr;\n    const bool is_after_fmv =\n        prev_event != nullptr && prev_event->type == GFS_PLAY_FMV;\n    if (event->type == GFS_LOADING_SCREEN) {\n        if (g_Config.gameplay.loading_screens == LOADING_SCREENS_DISABLED) {\n            return gf_cmd;\n        } else if (seq_ctx == GFSC_STORY) {\n            return gf_cmd;\n        } else if (\n            g_Config.gameplay.loading_screens == LOADING_SCREENS_NEW_GAMES\n            && seq_ctx != GFSC_NORMAL && seq_ctx != GFSC_SELECT) {\n            return gf_cmd;\n        }\n        Music_Stop();\n    } else if (seq_ctx == GFSC_SAVED) {\n        return gf_cmd;\n    }\n\n    GF_DISPLAY_PICTURE_DATA *data = event->data;\n    if (data->path == nullptr) {\n        return gf_cmd;\n    }\n    if (data->is_legal && !g_Config.gameplay.enable_legal) {\n        return gf_cmd;\n    }\n    if (data->is_credit && !g_Config.gameplay.enable_credits) {\n        return gf_cmd;\n    }\n\n    PHASE *const phase = Phase_Picture_Create((PHASE_PICTURE_ARGS) {\n        .file_name = data->path,\n        .display_time = data->display_time,\n        .fade_in_time = data->fade_in_time,\n        .fade_out_time = data->fade_out_time,\n        .display_time_includes_fades = g_TRVersion >= 2,\n        .loading_pic = event->type == GFS_LOADING_SCREEN,\n        .block_cross_fade_in = is_after_fmv,\n    });\n    gf_cmd = PhaseExecutor_Run(phase);\n    Phase_Picture_Destroy(phase);\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandleInventoryModifier)\n{\n    // handled in GF_InventoryModifier_Apply\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nM_GF_HANDLER(M_HandleLevelStats)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    if (seq_ctx != GFSC_NORMAL) {\n        return gf_cmd;\n    }\n\n    PHASE *const phase = Phase_Stats_Create((PHASE_STATS_ARGS) {\n        .background_type = Game_IsInGym() ? BK_TRANSPARENT_MEDIUM\n                                          : g_Config.ui.stats_background_style,\n        .level_num = -1,\n        .show_final_stats = false,\n        .use_bare_style = g_Config.ui.stats.style == STATS_STYLE_BARE,\n    });\n    gf_cmd = PhaseExecutor_Run(phase);\n    Phase_Stats_Destroy(phase);\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandleTotalStats)\n{\n    GF_COMMAND gf_cmd = { .action = GF_EXIT_TO_TITLE };\n    if (seq_ctx != GFSC_NORMAL) {\n        return gf_cmd;\n    }\n    if (!g_Config.gameplay.enable_total_stats) {\n        return gf_cmd;\n    }\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    PHASE *const phase = Phase_Stats_Create((PHASE_STATS_ARGS) {\n        .background_type = BK_IMAGE,\n        .background_path = event->data,\n        .show_final_stats = true,\n        .use_bare_style = false,\n        .level_num = -1,\n    });\n    gf_cmd = PhaseExecutor_Run(phase);\n    Phase_Stats_Destroy(phase);\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandleGlobeSelect)\n{\n    if (seq_ctx != GFSC_NORMAL) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    M_FinishLevelBasic();\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    const GF_GLOBE_SELECT_DATA *const data = event->data;\n    const GF_COMMAND gf_cmd =\n        GF_RunGlobeSelect(data != nullptr ? data->image_path : nullptr);\n    if (gf_cmd.action == GF_START_GAME) {\n        const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n        const GF_LEVEL *const next_level = GF_GetLevel(GFLT_MAIN, gf_cmd.param);\n        if (next_level != nullptr) {\n            Savegame_PersistGameToCurrentInfo(next_level);\n            RESUME_INFO *const next_resume =\n                Savegame_GetCurrentInfo(next_level);\n            if (next_resume != nullptr) {\n                next_resume->prev_level =\n                    current_level != nullptr ? current_level->num : -1;\n            }\n        }\n    }\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandleSetStartAnim)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n    if (seq_ctx != GFSC_STORY) {\n        Lara_SetStartAnimState((LARA_EXTRA_STATE)(intptr_t)event->data);\n    }\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandleEnableSunset)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    if (seq_ctx != GFSC_STORY) {\n        Output_SetSunsetEnabled(true);\n    }\n    return gf_cmd;\n}\n\nM_GF_HANDLER(M_HandleSetupBaconLara)\n{\n    // TODO: move me to lua!\n    if (seq_ctx != GFSC_STORY) {\n        const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n        const int32_t anchor_room = (int32_t)(intptr_t)event->data;\n        if (!BaconLara_InitialiseAnchor(anchor_room)) {\n            LOG_ERROR(\"Could not anchor Bacon Lara to room %d\", anchor_room);\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n    }\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nM_GF_HANDLER(M_HandleDisableFloor)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    if (seq_ctx != GFSC_STORY) {\n        const GF_SEQUENCE_EVENT *const event = &sequence->events[event_idx];\n        Room_SetAbyssHeight((int16_t)(intptr_t)event->data);\n    }\n    return gf_cmd;\n}\n\nGF_SEQUENCE_EVENT_HANDLER GF_GetSequenceEventHandler(\n    const GF_SEQUENCE_EVENT_TYPE event_type)\n{\n    return m_EventHandlers[event_type];\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/sequencer_events.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\ntypedef GF_COMMAND (*GF_SEQUENCE_EVENT_HANDLER)(\n    const GF_LEVEL *, const GF_SEQUENCE *, int32_t event_id,\n    GF_SEQUENCE_CONTEXT, void *);\n\nGF_SEQUENCE_EVENT_HANDLER GF_GetSequenceEventHandler(\n    GF_SEQUENCE_EVENT_TYPE event_type);\n"
  },
  {
    "path": "src/trx/game/game_flow/sequencer_misc.c",
    "content": "#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/game/demo.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_flow/sequencer.h>\n#include <trx/game/game_flow/vars.h>\n#include <trx/game/game_strings/table.h>\n#include <trx/game/inventory.h>\n#include <trx/game/inventory_ring/control.h>\n#include <trx/game/level.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/phase.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell/common.h>\n\nGF_COMMAND GF_RunTitle(void)\n{\n    Savegame_UnbindSlot();\n    GameStringTable_Apply(nullptr);\n    const GF_LEVEL *const title_level = GF_GetTitleLevel();\n    if (!Level_Initialise(title_level, GFSC_NORMAL)) {\n        return (GF_COMMAND) { .action = GF_EXIT_GAME };\n    }\n    return GF_ShowInventory(INV_TITLE_MODE);\n}\n\nGF_COMMAND GF_EnterPhotoMode(void)\n{\n    PHASE *const subphase = Phase_PhotoMode_Create();\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(subphase);\n    Phase_PhotoMode_Destroy(subphase);\n    return gf_cmd;\n}\n\nGF_COMMAND GF_PauseGame(void)\n{\n    PHASE *const subphase = Phase_Pause_Create();\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(subphase);\n    Phase_Pause_Destroy(subphase);\n    return gf_cmd;\n}\n\nGF_COMMAND GF_ShowInventory(const INVENTORY_MODE mode)\n{\n    PHASE *const phase = Phase_Inventory_Create(mode);\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(phase);\n    Phase_Inventory_Destroy(phase);\n    return gf_cmd;\n}\n\nbool GF_ShowInventoryKeys(const OBJECT_ID receptacle_type_id)\n{\n    if (!InvRing_IsRingAvailable(RT_KEYS)) {\n        return false;\n    }\n    if (g_Config.gameplay.enable_auto_item_selection) {\n        const OBJECT_ID obj_id = Object_GetCognateInverse(\n            receptacle_type_id, g_KeyItemToReceptacleMap);\n        InvRing_SetRequestedObjectID(obj_id);\n    } else {\n        Inv_ClearSelection();\n    }\n    const GF_COMMAND gf_cmd = GF_ShowInventory(INV_KEYS_MODE);\n    if (gf_cmd.action != GF_NOOP) {\n        GF_OverrideCommand(gf_cmd);\n    }\n    return true;\n}\n\nGF_COMMAND GF_RunDemo(const int32_t demo_num)\n{\n    PHASE *const demo_phase = Phase_Demo_Create(demo_num);\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(demo_phase);\n    Phase_Demo_Destroy(demo_phase);\n    return gf_cmd;\n}\n\nGF_COMMAND GF_RunCutscene(const int32_t cutscene_num, const bool cross_fade_in)\n{\n    PHASE *const cutscene_phase = Phase_Cutscene_Create((PHASE_CUTSCENE_ARGS) {\n        .level_num = cutscene_num,\n        .cross_fade_in = cross_fade_in,\n    });\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(cutscene_phase);\n    Phase_Cutscene_Destroy(cutscene_phase);\n    return gf_cmd;\n}\n\nGF_COMMAND GF_RunGame(\n    const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx)\n{\n    PHASE *const phase = Phase_Game_Create(level, seq_ctx);\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(phase);\n    Phase_Game_Destroy(phase);\n    return gf_cmd;\n}\n\nGF_COMMAND GF_RunGlobeSelect(const char *const background_path)\n{\n    PHASE *const phase = Phase_GlobeSelect_Create((PHASE_GLOBE_SELECT_ARGS) {\n        .background_path = background_path,\n    });\n    const GF_COMMAND gf_cmd = PhaseExecutor_Run(phase);\n    Phase_GlobeSelect_Destroy(phase);\n    return gf_cmd;\n}\n\nGF_COMMAND GF_DoFrontendSequence(void)\n{\n    const SHELL_ARGS *const args = Shell_GetArgs();\n    if (args != nullptr) {\n        if (args->save_to_load >= 0) {\n            return (GF_COMMAND) {\n                .action = GF_START_SAVED_GAME,\n                .param = Savegame_SlotToParam(\n                    Savegame_NormalSlot(args->save_to_load)),\n            };\n        }\n\n        if (args->level_to_select >= 0) {\n            const GF_LEVEL *const level =\n                GF_GetLevelByOrdinalNumber(GFLT_MAIN, args->level_to_select);\n            if (level == nullptr) {\n                Shell_ExitSystemFmt(\n                    \"Invalid level number: %d\", args->level_to_select);\n            }\n            return (GF_COMMAND) {\n                .action = GF_SELECT_GAME,\n                .param = level->num,\n            };\n        }\n\n        if (args->level_to_play != nullptr) {\n            return (GF_COMMAND) {\n                .action = GF_START_GAME,\n                .param = 0,\n            };\n        }\n    }\n\n    if (g_GameFlow.title_level == nullptr) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    return GF_InterpretSequence(g_GameFlow.title_level, GFSC_NORMAL, nullptr);\n}\n\nGF_COMMAND GF_DoLevelSequence(\n    const GF_LEVEL *const start_level, const GF_SEQUENCE_CONTEXT seq_ctx)\n{\n    const GF_LEVEL *current_level = start_level;\n    const GF_LEVEL_TABLE_TYPE level_table_type =\n        GF_GetLevelTableType(current_level->type);\n    const int32_t level_count = GF_GetLevelTable(level_table_type)->count;\n    while (true) {\n        const GF_COMMAND gf_cmd =\n            GF_InterpretSequence(current_level, seq_ctx, nullptr);\n\n        if (gf_cmd.action != GF_NOOP && gf_cmd.action != GF_LEVEL_COMPLETE) {\n            return gf_cmd;\n        }\n        if (Game_IsInGym()) {\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n        if (current_level->num + 1 >= level_count) {\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n        }\n        current_level++;\n    }\n}\n\nGF_COMMAND GF_DoDemoSequence(int32_t demo_num)\n{\n    demo_num = Demo_ChooseLevel(demo_num);\n    if (demo_num < 0) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    const GF_LEVEL *const level = GF_GetLevel(GFLT_DEMOS, demo_num);\n    if (level == nullptr) {\n        LOG_ERROR(\"Missing demo: %d\", demo_num);\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    return GF_InterpretSequence(level, GFSC_NORMAL, nullptr);\n}\n\nGF_COMMAND GF_DoCutsceneSequence(\n    const int32_t cutscene_num, const bool cross_fade_in)\n{\n    const GF_LEVEL *const level = GF_GetLevel(GFLT_CUTSCENES, cutscene_num);\n    if (level == nullptr) {\n        LOG_ERROR(\"Missing cutscene: %d\", cutscene_num);\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n    return GF_InterpretSequence(\n        level, GFSC_NORMAL, (void *)(intptr_t)cross_fade_in);\n}\n\nGF_COMMAND GF_PlayAvailableStory(const SAVEGAME_SLOT_REF slot)\n{\n    const int32_t savegame_level = Savegame_GetLevelNumber(slot);\n    const bool prev_enable_legal = g_Config.gameplay.enable_legal;\n    g_Config.gameplay.enable_legal = false;\n\n    // Play intro FMVs and cutscenes\n    GF_DoFrontendSequence();\n\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i <= MIN(savegame_level, level_table->count); i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        if (level->type == GFL_GYM) {\n            continue;\n        }\n        const GF_COMMAND gf_cmd = GF_InterpretSequence(\n            level, GFSC_STORY, (void *)(intptr_t)savegame_level);\n        if (gf_cmd.action == GF_EXIT_TO_TITLE\n            || gf_cmd.action == GF_EXIT_GAME) {\n            break;\n        }\n    }\n\n    g_Config.gameplay.enable_legal = prev_enable_legal;\n    return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n}\n\nbool GF_HasAvailableStory(const SAVEGAME_SLOT_REF slot)\n{\n    if (Savegame_IsSlotFree(slot)) {\n        return false;\n    }\n    const int32_t savegame_level = Savegame_GetLevelNumber(slot);\n\n    // Check intro FMVs and cutscenes in frontend sequence (skip legal FMVs)\n    const GF_LEVEL *const title_level = GF_GetTitleLevel();\n    if (title_level != nullptr) {\n        const GF_SEQUENCE *const seq = &title_level->sequence;\n        for (int32_t j = 0; j < seq->length; j++) {\n            const GF_SEQUENCE_EVENT *const ev = &seq->events[j];\n            if (ev->type == GFS_PLAY_CUTSCENE) {\n                return true;\n            }\n            if (ev->type == GFS_PLAY_FMV) {\n                const int32_t fmv_id = (int32_t)(intptr_t)ev->data;\n                const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id];\n                if (!fmv->is_legal && !fmv->is_credit) {\n                    return true;\n                }\n            }\n        }\n    }\n\n    // Check for any cutscenes or FMVs up until the save point\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    const int32_t max_level = MIN(savegame_level, level_table->count);\n    for (int32_t i = 0; i <= max_level; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        if (level->type == GFL_GYM) {\n            continue;\n        }\n        const GF_SEQUENCE *const seq = &level->sequence;\n        for (int32_t j = 0; j < seq->length; j++) {\n            const GF_SEQUENCE_EVENT *const ev = &seq->events[j];\n            // Stop checking after the saved level\n            if (ev->type == GFS_LOOP_GAME) {\n                break;\n            }\n            if (ev->type == GFS_PLAY_CUTSCENE) {\n                return true;\n            }\n            if (ev->type == GFS_PLAY_FMV) {\n                const int32_t fmv_id = (int32_t)(intptr_t)ev->data;\n                const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id];\n                if (!fmv->is_legal && !fmv->is_credit) {\n                    return true;\n                }\n            }\n        }\n    }\n    return false;\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/types.h",
    "content": "#pragma once\n\n#include <trx/game/fx/weather.h>\n#include <trx/game/game_flow/enum.h>\n#include <trx/game/music/ids.h>\n#include <trx/game/objects/types.h>\n\n#include <stdint.h>\n\ntypedef struct GF_COMMAND {\n    GF_ACTION action;\n    int32_t param;\n} GF_COMMAND;\n\n// ----------------------------------------------------------------------------\n// Sequencer structures\n// ----------------------------------------------------------------------------\n\ntypedef struct {\n    GF_SEQUENCE_EVENT_TYPE type;\n    void *data;\n} GF_SEQUENCE_EVENT;\n\ntypedef struct {\n    int32_t length;\n    GF_SEQUENCE_EVENT *events;\n} GF_SEQUENCE;\n\n// Concrete events data\n\ntypedef struct {\n    char *path;\n    bool is_legal;\n    bool is_credit;\n    float display_time;\n    float fade_in_time;\n    float fade_out_time;\n} GF_DISPLAY_PICTURE_DATA;\n\ntypedef struct {\n    char *image_path;\n} GF_GLOBE_SELECT_DATA;\n\ntypedef enum {\n    GF_INV_REGULAR,\n    GF_INV_SECRET,\n} GF_INV_TYPE;\n\ntypedef struct {\n    OBJECT_ID object_id;\n    GF_INV_TYPE inv_type;\n    int32_t quantity;\n} GF_ADD_ITEM_DATA;\n\n// ----------------------------------------------------------------------------\n// Game flow level structures\n// ----------------------------------------------------------------------------\n\ntypedef struct {\n    int32_t count;\n    char **data_paths;\n} INJECTION_DATA;\n\ntypedef struct {\n    const char *path;\n    bool is_legal;\n    bool is_credit;\n} GF_FMV;\n\ntypedef struct {\n    bool is_present;\n    int32_t count;\n    MUSIC_ID *ids;\n} GF_AMBIENT_DATA;\n\ntypedef struct {\n    struct {\n        bool is_present;\n        float value;\n    } fog_start, fog_end;\n    struct {\n        bool is_present;\n        bool value;\n    } fog_transparency;\n    struct {\n        bool is_present;\n        RGB_888 value;\n    } fog_color;\n    struct {\n        bool is_present;\n        RGB_888 value;\n    } water_color;\n    char *sfx_path;\n    struct {\n        bool is_present;\n        bool value;\n    } cold_water;\n    struct {\n        bool is_present;\n        int32_t value;\n    } death_tile;\n} GF_LEVEL_SETTINGS;\n\ntypedef struct {\n    int32_t enemy_num;\n    int32_t count;\n    int16_t *object_ids;\n} GF_DROP_ITEM_DATA;\n\ntypedef struct {\n    int32_t num;\n    GF_LEVEL_TYPE type;\n    char *path;\n    char *title;\n    // Path to a Lua script executed when this level loads\n    char *script_path;\n\n    MUSIC_ID music_track;\n    char *lara_outfit;\n    GF_SEQUENCE sequence;\n    INJECTION_DATA injections;\n\n    GF_LEVEL_SETTINGS settings;\n    WEATHER_TYPE weather_type;\n    bool water_particles;\n\n    struct {\n        uint32_t pickups;\n        uint32_t kills;\n        uint32_t ally_kills;\n        uint32_t secrets;\n    } unobtainable;\n\n    struct {\n        int32_t count;\n        GF_DROP_ITEM_DATA *data;\n    } item_drops;\n} GF_LEVEL;\n\ntypedef struct {\n    int32_t count;\n    GF_LEVEL *levels;\n} GF_LEVEL_TABLE;\n\ntypedef struct {\n    XYZ_16 rot;\n    int32_t start_level_ordinal;\n    int32_t completion_level_ordinal;\n    uint32_t prereq_mask;\n    uint8_t mesh_idx;\n} GF_GLOBE_ENTRY;\n\n// ----------------------------------------------------------------------------\n// Mod metadata\n// ----------------------------------------------------------------------------\n\ntypedef struct {\n    char *name;\n    int32_t engine;\n    char *extends;\n} GF_MOD_META;\n\n// ----------------------------------------------------------------------------\n// Game flow structures\n// ----------------------------------------------------------------------------\n\ntypedef struct {\n    char *path;\n\n    GF_MOD_META meta;\n\n    GF_LEVEL *title_level;\n    GF_LEVEL_TABLE level_tables[GFLT_NUMBER_OF];\n\n    // FMVs\n    struct {\n        int32_t fmv_count;\n        GF_FMV *fmvs;\n    };\n\n    // savegame settings\n    struct {\n        char *savegame_file_fmt;\n    };\n\n    // global settings\n    struct {\n        char *main_menu_background_path;\n        bool enable_tr2_item_drops;\n        bool convert_dropped_guns;\n        GF_AMBIENT_DATA ambient_tracks;\n    };\n\n    // other data\n    GF_LEVEL_SETTINGS settings;\n    INJECTION_DATA injections;\n\n    // Globe select entries\n    struct {\n        int32_t count;\n        GF_GLOBE_ENTRY *entries;\n    } globe;\n\n    // Path to a global Lua script executed after game initialization\n    char *main_script_path;\n} GAME_FLOW;\n"
  },
  {
    "path": "src/trx/game/game_flow/util.c",
    "content": "#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/game.h>\n#include <trx/game/items.h>\n#include <trx/game/objects.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n\nstatic void M_DisableObject(const OBJECT_ID object_id)\n{\n    OBJECT *const obj = Object_Get(object_id);\n    obj->loaded = false;\n    obj->collision_func = nullptr;\n    obj->control_func = nullptr;\n    obj->draw_func = nullptr;\n    obj->floor_height_func = nullptr;\n    obj->ceiling_height_func = nullptr;\n}\n\nstatic void M_ReplaceObject(\n    const OBJECT_ID src_object_id, const OBJECT_ID dst_object_id)\n{\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (item->object_id == src_object_id) {\n            item->object_id = dst_object_id;\n        }\n    }\n}\n\nvoid GF_DisableObjectsIfNeeded(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level == nullptr\n        || (level->type != GFL_NORMAL && level->type != GFL_BONUS\n            && level->type != GFL_GYM)) {\n        return;\n    }\n    if (g_Config.gameplay.disable_medpacks) {\n        M_DisableObject(O_SMALL_MEDIPACK_ITEM);\n        M_DisableObject(O_LARGE_MEDIPACK_ITEM);\n    }\n\n    if (g_Config.gameplay.disable_extra_guns) {\n        const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        ASSERT(resume != nullptr);\n        for (int32_t i = 0; g_GunObjects[i] != NO_OBJECT; i++) {\n            if (resume->flags.has_pistols) {\n                M_DisableObject(g_GunObjects[i]);\n            } else {\n                M_ReplaceObject(g_GunObjects[i], O_PISTOL_ITEM);\n            }\n            M_DisableObject(\n                Object_GetCognate(g_GunObjects[i], g_GunAmmoObjectMap));\n        }\n    }\n}\n\nint32_t GF_BadGetLevelNum(void)\n{\n    return GF_GetCurrentLevel()->num;\n}\n\nbool GF_BadIsMod(const char *const mod)\n{\n    return Shell_IsCurrentMod(mod);\n}\n"
  },
  {
    "path": "src/trx/game/game_flow/util.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid GF_DisableObjectsIfNeeded(void);\n\n// NOTE: This API should be eliminated!\nint32_t GF_BadGetLevelNum(void);\nbool GF_BadIsMod(const char *mod);\n"
  },
  {
    "path": "src/trx/game/game_flow/vars.c",
    "content": "#include <trx/game/game_flow/vars.h>\n\nGAME_FLOW g_GameFlow = {};\n"
  },
  {
    "path": "src/trx/game/game_flow/vars.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\nextern GAME_FLOW g_GameFlow;\n"
  },
  {
    "path": "src/trx/game/game_flow.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_flow/enum.h>\n#include <trx/game/game_flow/inventory.h>\n#include <trx/game/game_flow/reader.h>\n#include <trx/game/game_flow/sequencer.h>\n#include <trx/game/game_flow/types.h>\n#include <trx/game/game_flow/util.h>\n#include <trx/game/game_flow/vars.h>\n"
  },
  {
    "path": "src/trx/game/game_strings/entries.c",
    "content": "#include <trx/game/game_strings/entries.h>\n\n#include <trx/core/memory.h>\n\n#include <uthash.h>\n\n// One-slot-per-string for stable indirection on reload.\ntypedef struct M_SLOT {\n    const char *value;\n} M_SLOT;\n\ntypedef struct {\n    char *key;\n    M_SLOT *slot;\n    UT_hash_handle hh;\n} M_STRING_ENTRY;\n\nstatic M_STRING_ENTRY *m_StringTable = nullptr;\n\nvoid GameString_Init(void)\n{\n#include <trx/game/game_strings/entries.def>\n}\n\nvoid GameString_Shutdown(void)\n{\n    GameString_Clear();\n}\n\nvoid GameString_Define(const char *const key, const char *value)\n{\n    M_STRING_ENTRY *entry;\n\n    HASH_FIND_STR(m_StringTable, key, entry);\n    if (entry == nullptr) {\n        entry = Memory_Alloc(sizeof(*entry));\n        entry->key = Memory_DupStr(key);\n        entry->slot = Memory_Alloc(sizeof(*entry->slot));\n        entry->slot->value = nullptr;\n        HASH_ADD_KEYPTR(\n            hh, m_StringTable, entry->key, strlen(entry->key), entry);\n    }\n    Memory_Free((void *)entry->slot->value);\n    entry->slot->value = Memory_DupStr(value);\n}\n\nbool GameString_IsKnown(const char *const key)\n{\n    M_STRING_ENTRY *entry;\n    HASH_FIND_STR(m_StringTable, key, entry);\n    return entry != nullptr;\n}\n\nconst char *GameString_Get(const char *const key)\n{\n    M_STRING_ENTRY *entry;\n    HASH_FIND_STR(m_StringTable, key, entry);\n    return entry ? entry->slot->value : nullptr;\n}\n\nconst char *const *GameString_GetPtr(const char *const key)\n{\n    M_STRING_ENTRY *entry;\n    HASH_FIND_STR(m_StringTable, key, entry);\n    return entry ? &entry->slot->value : nullptr;\n}\n\nvoid GameString_Clear(void)\n{\n    M_STRING_ENTRY *entry, *tmp;\n\n    HASH_ITER(hh, m_StringTable, entry, tmp)\n    {\n        HASH_DEL(m_StringTable, entry);\n        Memory_Free(entry->key);\n        Memory_Free((void *)entry->slot->value);\n        Memory_Free(entry->slot);\n        Memory_Free(entry);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/game_strings/entries.def",
    "content": "// Enum values\nGS_DEFINE(dynamic/enums/bar_look/tr1_pc,                                    \"TR1 PC\")\nGS_DEFINE(dynamic/enums/bar_look/tr2_pc,                                    \"TR2 PC\")\nGS_DEFINE(dynamic/enums/bar_look/tr2_ps1,                                   \"TR2 PS1\")\nGS_DEFINE(dynamic/enums/bar_look/tr3_pc,                                    \"TR3 PC\")\nGS_DEFINE(dynamic/enums/bar_look/tr3_ps1,                                   \"TR3 PS1\")\nGS_DEFINE(dynamic/enums/lara_outfit/default,                                \"Default\")\nGS_DEFINE(dynamic/enums/lara_outfit/golden_sophia,                          \"Golden Sophia\")\nGS_DEFINE(dynamic/enums/lara_outfit/sophia,                                 \"Sophia\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_bacon_lara,                         \"Bacon Lara\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_classic,                            \"TR1 Classic\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_combo,                              \"TR1 Combo\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_golden_bacon_lara,                  \"Golden Bacon Lara\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_golden_lara,                        \"TR1 Golden Lara\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_gym,                                \"TR1 Gym\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_mauled,                             \"TR1 Mauled\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr1_ngage,                              \"TR1 N-Gage\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr23_golden_lara,                       \"TR2/3 Golden Lara\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_bomber_jacket,                      \"Bomber Jacket\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_classic,                            \"TR2 Classic\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_diving_suit,                        \"Diving Suit 1\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_diving_suit_alpha,                  \"Diving Suit 2\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_gym,                                \"TR2 Gym\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_robe,                               \"Robe\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr2_vegas,                              \"Vegas\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr3_antarctica,                         \"Antarctica\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr3_catsuit,                            \"Catsuit\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr3_classic,                            \"TR3 Classic\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr3_gym,                                \"TR3 Gym\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr3_nevada,                             \"Nevada\")\nGS_DEFINE(dynamic/enums/lara_outfit/tr3_south_pacific,                      \"South Pacific\")\nGS_DEFINE(enums/ALLY_HOSTILITY_POLICY/ALLY_HOSTILITY_POLICY_INDIVIDUAL,     \"Individual\")\nGS_DEFINE(enums/ALLY_HOSTILITY_POLICY/ALLY_HOSTILITY_POLICY_SHARED,         \"Shared\")\nGS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_16_10,                              \"16:10\")\nGS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_16_9,                               \"16:9\")\nGS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_4_3,                                \"4:3\")\nGS_DEFINE(enums/ASPECT_MODE/ASPECT_MODE_ANY,                                \"Any\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_BLACK,                                   \"Black\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_IMAGE,                                   \"Image\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_MONOCHROME,                              \"Monochrome\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_MONOCHROME_COOL,                         \"Monochrome (cool)\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_MONOCHROME_WARM,                         \"Monochrome (warm)\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_NONE,                                    \"Transparent\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_PATTERN_STATIC,                          \"Static\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_PATTERN_WAVE,                            \"Wave\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_TRANSPARENT_DARK,                        \"Very dark\")\nGS_DEFINE(enums/BACKGROUND_TYPE/BK_TRANSPARENT_MEDIUM,                      \"Dark\")\nGS_DEFINE(enums/BAR_SHOW_MODE/BAR_SHOW_MODE_ALWAYS,                         \"Always\")\nGS_DEFINE(enums/BAR_SHOW_MODE/BAR_SHOW_MODE_BOSS_ONLY,                      \"Boss only\")\nGS_DEFINE(enums/BAR_SHOW_MODE/BAR_SHOW_MODE_NEVER,                          \"Never\")\nGS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_NONE,                    \"None\")\nGS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_PERSPECTIVE,             \"Perspective\")\nGS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_ROLL,                    \"Roll\")\nGS_DEFINE(enums/BILLBOARD_LOCK_MODE/BILLBOARD_LOCK_ROLL_PITCH,              \"Roll & pitch\")\nGS_DEFINE(enums/BLOOD_EFFECTS/BLOOD_EFFECTS_DISABLED,                       \"Disabled\")\nGS_DEFINE(enums/BLOOD_EFFECTS/BLOOD_EFFECTS_PINK,                           \"Pink\")\nGS_DEFINE(enums/BLOOD_EFFECTS/BLOOD_EFFECTS_RED,                            \"Red\")\nGS_DEFINE(enums/CAMERA_MODE/CAMERA_MODE_TR1,                                \"TR1\")\nGS_DEFINE(enums/CAMERA_MODE/CAMERA_MODE_TR2,                                \"TR2\")\nGS_DEFINE(enums/CAMERA_MODE/CAMERA_MODE_TR3,                                \"TR3\")\nGS_DEFINE(enums/CREATURE_DROWN_POLICY/CREATURE_DROWN_POLICY_NEVER,          \"Never\")\nGS_DEFINE(enums/CREATURE_DROWN_POLICY/CREATURE_DROWN_POLICY_DEFAULT,        \"Default\")\nGS_DEFINE(enums/CREATURE_DROWN_POLICY/CREATURE_DROWN_POLICY_SUBMERGED,      \"Submerged\")\nGS_DEFINE(enums/INPUT_BACKEND/INPUT_BACKEND_CONTROLLER,                     \"Controller\")\nGS_DEFINE(enums/INPUT_BACKEND/INPUT_BACKEND_KEYBOARD,                       \"Keyboard\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ACTION,                               \"Action\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_BACK,                          \"Camera Back\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_DOWN,                          \"Camera Down\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_FORWARD,                       \"Camera Forward\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_LEFT,                          \"Camera Left\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_RESET,                         \"Camera Reset\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_RIGHT,                         \"Camera Right\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CAMERA_UP,                            \"Camera Up\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CHANGE_OUTFIT,                        \"Change Outfit\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CHANGE_TARGET,                        \"Change Target\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CROUCH,                               \"Crouch\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_CYCLE_LIGHTING_CONTRAST,              \"Cycle Lighting Contrast\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_DOWN,                                 \"Back\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_DRAW_WEAPON,                          \"Draw Weapon\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ENTER_CONSOLE,                        \"Dev Console\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_AUTOS,                          \"Equip Automatic Pistols\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_DESERT_EAGLE,                   \"Equip Desert Eagle\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_GRENADE_LAUNCHER,               \"Equip Grenade Launcher\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_HARPOON,                        \"Equip Harpoon Gun\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_M16,                            \"Equip M16\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_MAGNUMS,                        \"Equip Magnums\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_MP5,                            \"Equip MP5\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_PISTOLS,                        \"Equip Pistols\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_ROCKET_LAUNCHER,                \"Equip Rocket Launcher\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_SHOTGUN,                        \"Equip Shotgun\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_EQUIP_UZIS,                           \"Equip Uzis\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_FLY_CHEAT,                            \"Fly Cheat\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_FPS,                                  \"Show FPS\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_INVENTORY,                            \"Inventory\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ITEM_CHEAT,                           \"Item Cheat\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_JUMP,                                 \"Jump\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LEFT,                                 \"Left\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LEVEL_SKIP_CHEAT,                     \"Level Skip\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LOAD,                                 \"Load\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_LOOK,                                 \"Look\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_PAUSE,                                \"Pause\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_QUICK_LOAD,                           \"Quick Load\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_QUICK_SAVE,                           \"Quick Save\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_RIGHT,                                \"Right\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_ROLL,                                 \"Roll\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SAVE,                                 \"Save\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SCREENSHOT,                           \"Screenshot\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SLOW,                                 \"Walk\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SPRINT,                               \"Sprint\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_STEP_LEFT,                            \"Step Left\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_STEP_RIGHT,                           \"Step Right\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SWITCH_BORDERS,                       \"Switch Borders Size\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_SWITCH_UPSCALING,                     \"Switch Upscaling Factor\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_BILINEAR_FILTER,               \"Toggle Bilinear Filter\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_FULLSCREEN,                    \"Toggle Fullscreen\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_PHOTO_MODE,                    \"Toggle Photo Mode\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_TEXTURES,                      \"Toggle Textures\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER,              \"Toggle Trapezoid Filter\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_UI,                            \"Toggle UI\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TOGGLE_WIREFRAME,                     \"Toggle Wireframe\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_TURBO_CHEAT,                          \"Turbo Speed\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_UP,                                   \"Run\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_USE_BIG_MEDI,                         \"Large Medi\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_USE_FLARE,                            \"Flare\")\nGS_DEFINE(enums/INPUT_ROLE/INPUT_ROLE_USE_SMALL_MEDI,                       \"Small Medi\")\nGS_DEFINE(enums/JUMP_LOCK_MODE/JUMP_LOCK_DISABLED,                          \"Disabled\")\nGS_DEFINE(enums/JUMP_LOCK_MODE/JUMP_LOCK_LEGACY,                            \"Legacy\")\nGS_DEFINE(enums/JUMP_LOCK_MODE/JUMP_LOCK_TUNED,                             \"Tuned\")\nGS_DEFINE(enums/LIGHTING_CONTRAST/LIGHTING_CONTRAST_HIGH,                   \"High\")\nGS_DEFINE(enums/LIGHTING_CONTRAST/LIGHTING_CONTRAST_LOW,                    \"Low\")\nGS_DEFINE(enums/LIGHTING_CONTRAST/LIGHTING_CONTRAST_MEDIUM,                 \"Medium\")\nGS_DEFINE(enums/LOADING_SCREENS_MODE/LOADING_SCREENS_ALWAYS,                \"Always\")\nGS_DEFINE(enums/LOADING_SCREENS_MODE/LOADING_SCREENS_DISABLED,              \"Disabled\")\nGS_DEFINE(enums/LOADING_SCREENS_MODE/LOADING_SCREENS_NEW_GAMES,             \"New games\")\nGS_DEFINE(enums/LOOK_MODE/LOOK_MODE_ENHANCED,                               \"Enhanced\")\nGS_DEFINE(enums/LOOK_MODE/LOOK_MODE_RESTRICTED,                             \"Restricted\")\nGS_DEFINE(enums/LOOK_MODE/LOOK_MODE_UNRESTRICTED,                           \"Unrestricted\")\nGS_DEFINE(enums/MUSIC_LOAD_CONDITION/MUSIC_LOAD_CONDITION_ALWAYS,           \"Always\")\nGS_DEFINE(enums/MUSIC_LOAD_CONDITION/MUSIC_LOAD_CONDITION_NEVER,            \"Never\")\nGS_DEFINE(enums/MUSIC_LOAD_CONDITION/MUSIC_LOAD_CONDITION_NON_AMBIENT,      \"Non-ambient\")\nGS_DEFINE(enums/PROJECTILE_AREA_DAMAGE/PROJECTILE_AREA_DAMAGE_MULTI_SWEEP,  \"Multi-sweep\")\nGS_DEFINE(enums/PROJECTILE_AREA_DAMAGE/PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP, \"Single-sweep\")\nGS_DEFINE(enums/QUICK_GUNS_MODE/QUICK_GUNS_MODE_DRAW_AND_HOLSTER,           \"Draw or holster\")\nGS_DEFINE(enums/QUICK_GUNS_MODE/QUICK_GUNS_MODE_DRAW_ONLY,                  \"Draw only\")\nGS_DEFINE(enums/SCREENSHOT_FORMAT/SCREENSHOT_FORMAT_JPEG,                   \"JPG\")\nGS_DEFINE(enums/SCREENSHOT_FORMAT/SCREENSHOT_FORMAT_PNG,                    \"PNG\")\nGS_DEFINE(enums/SHADOW_TYPE/SHADOW_TYPE_CIRCLE,                             \"Circle\")\nGS_DEFINE(enums/SHADOW_TYPE/SHADOW_TYPE_OCTAGON,                            \"Octagon\")\nGS_DEFINE(enums/SHADOW_TYPE/SHADOW_TYPE_SPRITE,                             \"Sprite\")\nGS_DEFINE(enums/STATS_STYLE/STATS_STYLE_BARE,                               \"Bare\")\nGS_DEFINE(enums/STATS_STYLE/STATS_STYLE_BORDERED,                           \"Bordered\")\nGS_DEFINE(enums/SUNGLASSES_MODE/SUNGLASSES_MODE_OFF,                        \"Off\")\nGS_DEFINE(enums/SUNGLASSES_MODE/SUNGLASSES_MODE_OPAQUE,                     \"Opaque\")\nGS_DEFINE(enums/SUNGLASSES_MODE/SUNGLASSES_MODE_TRANSPARENT,                \"Transparent\")\nGS_DEFINE(enums/TARGET_LOCK_MODE/TARGET_LOCK_MODE_FULL,                     \"Full lock\")\nGS_DEFINE(enums/TARGET_LOCK_MODE/TARGET_LOCK_MODE_NONE,                     \"No lock\")\nGS_DEFINE(enums/TARGET_LOCK_MODE/TARGET_LOCK_MODE_SEMI,                     \"Semi lock\")\nGS_DEFINE(enums/TEXTURE_FILTER/TEXTURE_FILTER_BILINEAR,                     \"Bilinear\")\nGS_DEFINE(enums/TEXTURE_FILTER/TEXTURE_FILTER_POINT,                        \"Point\")\nGS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_BOTTOM_CENTER,      \"Bottom center\")\nGS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_BOTTOM_LEFT,        \"Bottom left\")\nGS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_BOTTOM_RIGHT,       \"Bottom right\")\nGS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_TOP_CENTER,         \"Top center\")\nGS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_TOP_LEFT,           \"Top left\")\nGS_DEFINE(enums/UI_ELEMENT_LOCATION/UI_ELEMENT_LOCATION_TOP_RIGHT,          \"Top right\")\nGS_DEFINE(enums/UI_STYLE/UI_STYLE_PC,                                       \"PC\")\nGS_DEFINE(enums/UI_STYLE/UI_STYLE_PS1,                                      \"PS1\")\nGS_DEFINE(enums/WALL_GLITCH_MODE/WALL_GLITCH_FIXED,                         \"Fixed\")\nGS_DEFINE(enums/WALL_GLITCH_MODE/WALL_GLITCH_TR1,                           \"TR1\")\nGS_DEFINE(enums/WALL_GLITCH_MODE/WALL_GLITCH_TR2,                           \"TR2\")\n\n// Setting names\nGS_DEFINE(settings/audio.ambient_volume/title, \"Ambient volume\")\nGS_DEFINE(settings/audio.cutscene_volume/title, \"Cutscene volume\")\nGS_DEFINE(settings/audio.enable_lara_mic/title, \"Microphone near Lara\")\nGS_DEFINE(settings/audio.enable_music_in_inventory/title, \"Play music in inventory\")\nGS_DEFINE(settings/audio.enable_music_in_menu/title, \"Main menu music\")\nGS_DEFINE(settings/audio.enable_pitched_sounds/title, \"Pitched sounds\")\nGS_DEFINE(settings/audio.enable_ps1_sfx/title, \"PS1 SFX replacements\")\nGS_DEFINE(settings/audio.enable_underwater_anim_sfx/title, \"Underwater animation SFX\")\nGS_DEFINE(settings/audio.fix_chainblock_secret_sound/title, \"Fix chain block sound\")\nGS_DEFINE(settings/audio.fix_secrets_killing_music/title, \"Layered secret music\")\nGS_DEFINE(settings/audio.fix_speeches_killing_music/title, \"Layered enemy speech\")\nGS_DEFINE(settings/audio.fmv_volume/title, \"FMV volume\")\nGS_DEFINE(settings/audio.inventory_ambient_volume/title, \"Ambient volume (inventory)\")\nGS_DEFINE(settings/audio.inventory_music_volume/title, \"Music volume (inventory)\")\nGS_DEFINE(settings/audio.load_music_triggers/title, \"Fix one-shot music triggers\")\nGS_DEFINE(settings/audio.master_volume/title, \"\\\\{icon music} Master volume\")\nGS_DEFINE(settings/audio.music_load_condition/title, \"Restore music on load\")\nGS_DEFINE(settings/audio.music_volume/title, \"Music volume\")\nGS_DEFINE(settings/audio.mute_out_of_focus/title, \"Mute audio when focus lost\")\nGS_DEFINE(settings/audio.sound_volume/title, \"\\\\{icon sound} Sound volume\")\nGS_DEFINE(settings/audio.underwater_ambient_volume/title, \"Ambient volume (underwater)\")\nGS_DEFINE(settings/audio.underwater_music_volume/title, \"Music volume (underwater)\")\nGS_DEFINE(settings/debug.enable_endless_flare_time/title, \"Endless flare time\")\nGS_DEFINE(settings/debug.enable_endless_sprint/title, \"Endless sprint\")\nGS_DEFINE(settings/gameplay.ally_hostility_policy/title, \"Ally hostility policy\")\nGS_DEFINE(settings/gameplay.camera_speed/title, \"Camera speed\")\nGS_DEFINE(settings/gameplay.change_pierre_spawn/title, \"Change Pierre spawn mode\")\nGS_DEFINE(settings/gameplay.creature_drown_policy/title, \"Creature drown policy\")\nGS_DEFINE(settings/gameplay.disable_extra_guns/title, \"Remove extra guns\")\nGS_DEFINE(settings/gameplay.disable_healing_between_levels/title, \"Persistent damage\")\nGS_DEFINE(settings/gameplay.disable_medpacks/title, \"Remove medipacks\")\nGS_DEFINE(settings/gameplay.disable_trex_collision/title, \"Remove dead T-Rex collision\")\nGS_DEFINE(settings/gameplay.enable_ally_targeting/title, \"Allow targeting allies\")\nGS_DEFINE(settings/gameplay.enable_auto_item_selection/title, \"Key item pre-selection\")\nGS_DEFINE(settings/gameplay.enable_back_slope_stumble/title, \"Backwards slope stumble\")\nGS_DEFINE(settings/gameplay.enable_body_bags/title, \"Body bag triggers\")\nGS_DEFINE(settings/gameplay.enable_boulder_shake/title, \"Enable boulder shake\")\nGS_DEFINE(settings/gameplay.enable_bouncy_grenades/title, \"Bouncy grenades\")\nGS_DEFINE(settings/gameplay.enable_cheats/title, \"Cheats\")\nGS_DEFINE(settings/gameplay.enable_cinematics/title, \"Cinematics\")\nGS_DEFINE(settings/gameplay.enable_compass_stats/title, \"Level statistics in compass\")\nGS_DEFINE(settings/gameplay.enable_console/title, \"Console\")\nGS_DEFINE(settings/gameplay.enable_controlled_drops/title, \"Controlled drops\")\nGS_DEFINE(settings/gameplay.enable_crawl_jump/title, \"Crawl exit jump\")\nGS_DEFINE(settings/gameplay.enable_crawl_tilt/title, \"Crawl tilt\")\nGS_DEFINE(settings/gameplay.enable_crawling/title, \"Crawling\")\nGS_DEFINE(settings/gameplay.enable_credits/title, \"Credit screens\")\nGS_DEFINE(settings/gameplay.enable_crouch_roll/title, \"Crouch roll\")\nGS_DEFINE(settings/gameplay.enable_cutscenes/title, \"Cutscenes\")\nGS_DEFINE(settings/gameplay.enable_demo/title, \"Demo mode\")\nGS_DEFINE(settings/gameplay.enable_enemy_rotation/title, \"Randomize enemy start angle\")\nGS_DEFINE(settings/gameplay.enable_enhanced_saves/title, \"Save effects\")\nGS_DEFINE(settings/gameplay.enable_fmv/title, \"FMVs\")\nGS_DEFINE(settings/gameplay.enable_game_modes/title, \"Game mode selection\")\nGS_DEFINE(settings/gameplay.enable_idle_pose_camera/title, \"Idle pose camera\")\nGS_DEFINE(settings/gameplay.enable_inverted_look/title, \"Inverted look\")\nGS_DEFINE(settings/gameplay.enable_item_examining/title, \"Item examination\")\nGS_DEFINE(settings/gameplay.enable_jump_twists/title, \"Jump twists\")\nGS_DEFINE(settings/gameplay.enable_killer_pushblocks/title, \"Enable killer pushblocks\")\nGS_DEFINE(settings/gameplay.enable_lean_jumping/title, \"Lean jumping\")\nGS_DEFINE(settings/gameplay.enable_ledge_jumps/title, \"Ledge jumps\")\nGS_DEFINE(settings/gameplay.enable_legal/title, \"Legal screens\")\nGS_DEFINE(settings/gameplay.enable_manual_camera/title, \"Manual camera\")\nGS_DEFINE(settings/gameplay.enable_neutral_twists/title, \"Neutral twists\")\nGS_DEFINE(settings/gameplay.enable_pickup_aids/title, \"Pickup aids\")\nGS_DEFINE(settings/gameplay.enable_play_previous_levels/title, \"Play previous levels\")\nGS_DEFINE(settings/gameplay.pause_on_focus_lost/title, \"Pause when focus lost\")\nGS_DEFINE(settings/gameplay.enable_responsive_crawl/title, \"Responsive crawling\")\nGS_DEFINE(settings/gameplay.enable_responsive_sprint/title, \"Responsive sprinting\")\nGS_DEFINE(settings/gameplay.enable_save_crystals/title, \"Save crystals\")\nGS_DEFINE(settings/gameplay.enable_slide_to_run/title, \"Slide-to-run\")\nGS_DEFINE(settings/gameplay.enable_slow_ledge_swing/title, \"Slow ledge swing\")\nGS_DEFINE(settings/gameplay.enable_smooth_wall_deflect/title, \"Smooth wall deflection\")\nGS_DEFINE(settings/gameplay.enable_soft_statics/title, \"Soft static collision\")\nGS_DEFINE(settings/gameplay.enable_sprint/title, \"Sprinting\")\nGS_DEFINE(settings/gameplay.enable_step_roll_boost/title, \"Step roll boost\")\nGS_DEFINE(settings/gameplay.enable_swing_cancel/title, \"Swing cancels\")\nGS_DEFINE(settings/gameplay.enable_target_change/title, \"Target change\")\nGS_DEFINE(settings/gameplay.enable_timer_in_inventory/title, \"Timer counts in inventory\")\nGS_DEFINE(settings/gameplay.enable_toggle_crouch/title, \"Toggle crouch\")\nGS_DEFINE(settings/gameplay.enable_toggle_sprint/title, \"Toggle sprint\")\nGS_DEFINE(settings/gameplay.enable_total_stats/title, \"Final statistics screen\")\nGS_DEFINE(settings/gameplay.enable_tr2_jumping/title, \"Responsive jumping\")\nGS_DEFINE(settings/gameplay.enable_tr2_swim_cancel/title, \"Responsive swim cancel\")\nGS_DEFINE(settings/gameplay.enable_tr2_swimming/title, \"Smooth swimming\")\nGS_DEFINE(settings/gameplay.enable_uw_roll/title, \"Underwater roll\")\nGS_DEFINE(settings/gameplay.enable_wading/title, \"Wading\")\nGS_DEFINE(settings/gameplay.enable_walk_to_items/title, \"Animated interactions\")\nGS_DEFINE(settings/gameplay.fix_alligator_ai/title, \"Fix alligator AI\")\nGS_DEFINE(settings/gameplay.fix_bear_ai/title, \"Fix bear AI\")\nGS_DEFINE(settings/gameplay.fix_bridge_collision/title, \"Fix bridge collision\")\nGS_DEFINE(settings/gameplay.fix_descending_glitch/title, \"Fix breakable floor falls\")\nGS_DEFINE(settings/gameplay.fix_flare_throw_priority/title, \"Fix flare throw priority\")\nGS_DEFINE(settings/gameplay.fix_floor_data_issues/title, \"Fix floor data issues\")\nGS_DEFINE(settings/gameplay.fix_free_flare_glitch/title, \"Fix free flare glitch\")\nGS_DEFINE(settings/gameplay.fix_item_duplication_glitch/title, \"Fix item duplication glitch\")\nGS_DEFINE(settings/gameplay.fix_lara_pickup_embed/title, \"Fix pickup embed glitch\")\nGS_DEFINE(settings/gameplay.fix_m16_accuracy/title, \"Fix M16/MP5 accuracy\")\nGS_DEFINE(settings/gameplay.fix_monkey_pickup_priority/title, \"Fix monkey pickup priority\")\nGS_DEFINE(settings/gameplay.fix_pipeman_aim/title, \"Fix pipeman aim\")\nGS_DEFINE(settings/gameplay.fix_qwop_glitch/title, \"Fix QWOP glitch\")\nGS_DEFINE(settings/gameplay.fix_step_glitch/title, \"Fix step glitch\")\nGS_DEFINE(settings/gameplay.fix_wade_wall_hit/title, \"Fix wading wall hit\")\nGS_DEFINE(settings/gameplay.fix_walk_run_jump/title, \"Fix walk run jump\")\nGS_DEFINE(settings/gameplay.fix_wall_geometry/title, \"Fix wall geometry\")\nGS_DEFINE(settings/gameplay.fix_water_exit/title, \"Fix water exit\")\nGS_DEFINE(settings/gameplay.harpoon_recoil/title, \"Harpoon recoil\")\nGS_DEFINE(settings/gameplay.idle_pose_timeout/title, \"Idle pose timeout\")\nGS_DEFINE(settings/gameplay.jump_lock_mode/title, \"Jump lock mode\")\nGS_DEFINE(settings/gameplay.loading_screens/title, \"Loading screens\")\nGS_DEFINE(settings/gameplay.look_mode/title, \"Look mode\")\nGS_DEFINE(settings/gameplay.maximum_quick_save_slots/title, \"Number of quick save slots\")\nGS_DEFINE(settings/gameplay.maximum_save_slots/title, \"Number of save slots\")\nGS_DEFINE(settings/gameplay.projectile_area_damage/title, \"Projectile area damage\")\nGS_DEFINE(settings/gameplay.remember_gun_status/title, \"Remember guns between levels\")\nGS_DEFINE(settings/gameplay.restore_ps1_enemies/title, \"Restore PS1 enemies\")\nGS_DEFINE(settings/gameplay.start_lara_hitpoints/title, \"Lara's starting health\")\nGS_DEFINE(settings/gameplay.target_mode/title, \"Weapon lock mode\")\nGS_DEFINE(settings/gameplay.wall_glitch_mode/title, \"Wall glitch mode\")\nGS_DEFINE(settings/input.enable_buffering_func_keys/title, \"Buffering (F-keys)\")\nGS_DEFINE(settings/input.enable_buffering_inventory/title, \"Buffering (inventory)\")\nGS_DEFINE(settings/input.enable_responsive_passport/title, \"Responsive passport\")\nGS_DEFINE(settings/input.enable_tr3_sidesteps/title, \"Enhanced sidesteps\")\nGS_DEFINE(settings/input.quick_guns_mode/title, \"Quick gun keys\")\nGS_DEFINE(settings/language/title, \"Language\")\nGS_DEFINE(settings/rendering.anisotropy_filter/title, \"Anisotropy filter\")\nGS_DEFINE(settings/rendering.aspect_mode/title, \"Aspect mode\")\nGS_DEFINE(settings/rendering.borders/title, \"Borders\")\nGS_DEFINE(settings/rendering.enable_trapezoid_filter/title, \"Trapezoid filter\")\nGS_DEFINE(settings/rendering.enable_vsync/title, \"VSync\")\nGS_DEFINE(settings/rendering.fps/title, \"FPS\")\nGS_DEFINE(settings/rendering.lighting_contrast/title, \"Lighting contrast\")\nGS_DEFINE(settings/rendering.screenshot_format/title, \"Screenshot format\")\nGS_DEFINE(settings/rendering.sprite_lock_mode/title, \"Sprite lock mode\")\nGS_DEFINE(settings/rendering.texture_filter/title, \"Texture filter\")\nGS_DEFINE(settings/rendering.ui_filter/title, \"UI filter\")\nGS_DEFINE(settings/rendering.upscaling_factor/title, \"Upscaling factor\")\nGS_DEFINE(settings/rendering.upscaling_filter/title, \"Upscaling filter\")\nGS_DEFINE(settings/ui.airbar_color/title, \"Airbar color\")\nGS_DEFINE(settings/ui.airbar_color_ps1/title, \"Airbar color\")\nGS_DEFINE(settings/ui.airbar_location/title, \"Airbar location\")\nGS_DEFINE(settings/ui.ammo_counter_location/title, \"Ammo counter location\")\nGS_DEFINE(settings/ui.bar_look/title, \"Bars appearance\")\nGS_DEFINE(settings/ui.bar_scale/title, \"Bars scale\")\nGS_DEFINE(settings/ui.enable_bar_flashing/title, \"Flash bars\")\nGS_DEFINE(settings/ui.enable_smooth_bars/title, \"Smooth bars\")\nGS_DEFINE(settings/ui.enable_wraparound/title, \"Scroll wrap\")\nGS_DEFINE(settings/ui.enemy_healthbar_color/title, \"Enemy bar color\")\nGS_DEFINE(settings/ui.enemy_healthbar_color_allies/title, \"Ally bar color\")\nGS_DEFINE(settings/ui.enemy_healthbar_color_allies_ps1/title, \"Ally bar color\")\nGS_DEFINE(settings/ui.enemy_healthbar_color_ps1/title, \"Enemy bar color\")\nGS_DEFINE(settings/ui.enemy_healthbar_location/title, \"Enemy bar location\")\nGS_DEFINE(settings/ui.enemy_healthbar_show_mode/title, \"Enemy bar mode\")\nGS_DEFINE(settings/ui.exposurebar_color/title, \"Exposure bar color\")\nGS_DEFINE(settings/ui.exposurebar_color_ps1/title, \"Exposure bar color\")\nGS_DEFINE(settings/ui.exposurebar_location/title, \"Exposure bar location\")\nGS_DEFINE(settings/ui.healthbar_color/title, \"Healthbar color\")\nGS_DEFINE(settings/ui.healthbar_color_ps1/title, \"Healthbar color\")\nGS_DEFINE(settings/ui.healthbar_location/title, \"Healthbar location\")\nGS_DEFINE(settings/ui.healthbar_poison_color/title, \"Poison healthbar color\")\nGS_DEFINE(settings/ui.healthbar_poison_color_ps1/title, \"Poison healthbar color\")\nGS_DEFINE(settings/ui.inventory_background_style/title, \"Inventory background\")\nGS_DEFINE(settings/ui.inventory_fade_effects/title, \"Inventory fade effects\")\nGS_DEFINE(settings/ui.menu_style/title, \"Menu style\")\nGS_DEFINE(settings/ui.pause_background_style/title, \"Pause background\")\nGS_DEFINE(settings/ui.pause_fade_effects/title, \"Pause fade effects\")\nGS_DEFINE(settings/ui.pickup_scale/title, \"Pickup scale\")\nGS_DEFINE(settings/ui.show_bars/title, \"Show bars\")\nGS_DEFINE(settings/ui.show_pickups_overlay/title, \"Pickups overlay\")\nGS_DEFINE(settings/ui.show_title_version/title, \"Title version text\")\nGS_DEFINE(settings/ui.sprintbar_color/title, \"Sprintbar color\")\nGS_DEFINE(settings/ui.sprintbar_color_ps1/title, \"Sprintbar color\")\nGS_DEFINE(settings/ui.sprintbar_location/title, \"Sprintbar location\")\nGS_DEFINE(settings/ui.stats.show_ammo/title, \"Ammo hits/used\")\nGS_DEFINE(settings/ui.stats.show_deaths/title, \"Deaths\")\nGS_DEFINE(settings/ui.stats.show_distance_travelled/title, \"Distance traveled\")\nGS_DEFINE(settings/ui.stats.show_kills/title, \"Kills\")\nGS_DEFINE(settings/ui.stats.show_level_header/title, \"Level counter\")\nGS_DEFINE(settings/ui.stats.show_medipacks_used/title, \"Health packs used\")\nGS_DEFINE(settings/ui.stats.show_crystals/title, \"Crystals\")\nGS_DEFINE(settings/ui.stats.show_pickups/title, \"Pickups\")\nGS_DEFINE(settings/ui.stats.show_secrets/title, \"Secrets found\")\nGS_DEFINE(settings/ui.stats.show_time_taken/title, \"Time taken\")\nGS_DEFINE(settings/ui.stats.show_totals/title, \"Show totals\")\nGS_DEFINE(settings/ui.stats.style/title, \"Statistics style\")\nGS_DEFINE(settings/ui.stats_background_style/title, \"Stats background\")\nGS_DEFINE(settings/ui.stats_fade_effects/title, \"Stats fade effects\")\nGS_DEFINE(settings/ui.text_scale/title, \"Text scale\")\nGS_DEFINE(settings/visuals.camera_mode/title, \"Camera mode\")\nGS_DEFINE(settings/visuals.enable_3d_pickups/title, \"3D pickups\")\nGS_DEFINE(settings/visuals.enable_braid/title, \"Lara's braid\")\nGS_DEFINE(settings/visuals.enable_breeze/title, \"Breeze\")\nGS_DEFINE(settings/visuals.blood_effects/title, \"Blood effects\")\nGS_DEFINE(settings/visuals.enable_exit_fade_effects/title, \"Fade on game exit\")\nGS_DEFINE(settings/visuals.enable_fade_effects/title, \"Fade effects\")\nGS_DEFINE(settings/visuals.enable_fire_lighting/title, \"Fire lighting\")\nGS_DEFINE(settings/visuals.enable_footprints/title, \"Footprints\")\nGS_DEFINE(settings/visuals.enable_glide_cameras/title, \"Glide cameras\")\nGS_DEFINE(settings/visuals.enable_gun_lighting/title, \"Gun lighting\")\nGS_DEFINE(settings/visuals.enable_ps1_crystals/title, \"PS1 crystal tint\")\nGS_DEFINE(settings/visuals.enable_reflections/title, \"Reflections\")\nGS_DEFINE(settings/visuals.enable_responsive_mesh_tint/title, \"Responsive mesh tint\")\nGS_DEFINE(settings/visuals.enable_shotgun_flash/title, \"Shotgun flash\")\nGS_DEFINE(settings/visuals.enable_skybox/title, \"Skyboxes\")\nGS_DEFINE(settings/visuals.enable_weather/title, \"Weather\")\nGS_DEFINE(settings/visuals.fix_animated_sprites/title, \"Fix sprite animations\")\nGS_DEFINE(settings/visuals.fix_item_rots/title, \"Fix item rotation issues\")\nGS_DEFINE(settings/visuals.fix_texture_issues/title, \"Fix texture issues\")\nGS_DEFINE(settings/visuals.fog_color/title, \"Fog color\")\nGS_DEFINE(settings/visuals.fog_end/title, \"Fog end\")\nGS_DEFINE(settings/visuals.fog_start/title, \"Fog start\")\nGS_DEFINE(settings/visuals.fog_transparency/title, \"Fog transparency\")\nGS_DEFINE(settings/visuals.fov/title, \"Field of view\")\nGS_DEFINE(settings/visuals.game_brightness/title, \"Game brightness\")\nGS_DEFINE(settings/visuals.gamma/title, \"Gamma\")\nGS_DEFINE(settings/visuals.lara_outfit/title, \"Lara's outfit\")\nGS_DEFINE(settings/visuals.shadow_type/title, \"Shadows shape\")\nGS_DEFINE(settings/visuals.sunglasses_mode/title, \"Lara's sunglasses\")\nGS_DEFINE(settings/visuals.ui_brightness/title, \"UI brightness\")\nGS_DEFINE(settings/visuals.water_color/title, \"Water color\")\n\n// Setting descriptions\nGS_DEFINE(settings/audio.ambient_volume/description, \"Adjusts ambient volume.\")\nGS_DEFINE(settings/audio.cutscene_volume/description, \"Adjusts the ingame cutscenes volume.\")\nGS_DEFINE(settings/audio.enable_lara_mic/description, \"Set the microphone to be at Lara's position. If disabled, the microphone will be at the camera's position.\")\nGS_DEFINE(settings/audio.enable_music_in_inventory/description, \"Lets game sounds, ambient and music continue playing in the inventory screen.\")\nGS_DEFINE(settings/audio.enable_music_in_menu/description, \"Plays music in the main menu.\")\nGS_DEFINE(settings/audio.enable_pitched_sounds/description, \"Allows sound effects to be randomly, slightly pitched to vary the game sounds.\")\nGS_DEFINE(settings/audio.enable_ps1_sfx/description, \"Enables specific sound effect replacements using PS1 equivalents.\\n\\n- Uzi fire (TR1 only)\\n- Lara barefoot sounds (TR2 only)\")\nGS_DEFINE(settings/audio.enable_underwater_anim_sfx/description, \"Allows control over playing specific animation sound effects - for objects such as doors and trapdoors - when the camera is underwater.\")\nGS_DEFINE(settings/audio.fix_chainblock_secret_sound/description, \"Prevents the secret sound from incorrectly playing when using the golden key in Tomb of Tihocan.\")\nGS_DEFINE(settings/audio.fix_secrets_killing_music/description, \"Fixes the sound of collecting a secret stopping the active music track.\")\nGS_DEFINE(settings/audio.fix_speeches_killing_music/description, \"Fixes enemies stopping the active music track when they speak.\")\nGS_DEFINE(settings/audio.fmv_volume/description, \"Adjusts the movies volume.\")\nGS_DEFINE(settings/audio.inventory_ambient_volume/description, \"Adjusts ambient volume in inventory screen.\")\nGS_DEFINE(settings/audio.inventory_music_volume/description, \"Adjusts music volume in inventory screen.\")\nGS_DEFINE(settings/audio.load_music_triggers/description, \"Loads previously triggered, one shot music so one shot music tracks do not replay.\")\nGS_DEFINE(settings/audio.master_volume/description, \"Adjusts all ingame volume. The rest of the settings are relative to this volume.\")\nGS_DEFINE(settings/audio.music_load_condition/description, \"Loads the music track that was playing when the game was saved.\\n\\n- Never: do not restore music tracks on load.\\n- Non-ambient: restore only non-ambient music tracks on load.\\n- Always: restore any kind of music track on load.\")\nGS_DEFINE(settings/audio.music_volume/description, \"Adjusts music volume.\")\nGS_DEFINE(settings/audio.mute_out_of_focus/description, \"Mutes all music and sound effects when the game window is not focused.\")\nGS_DEFINE(settings/audio.sound_volume/description, \"Adjusts sound effects volume.\")\nGS_DEFINE(settings/audio.underwater_ambient_volume/description, \"Adjusts ambient volume when underwater.\")\nGS_DEFINE(settings/audio.underwater_music_volume/description, \"Adjusts music volume when underwater.\")\nGS_DEFINE(settings/debug.enable_endless_flare_time/description, \"Prevents the handheld flares from ever going out. Thrown flares will still go out as normal.\")\nGS_DEFINE(settings/debug.enable_endless_sprint/description, \"Prevents Lara from ever getting tired when sprinting. Obstacles will still bring her to a stop.\")\nGS_DEFINE(settings/gameplay.ally_hostility_policy/description, \"Controls how friendly units react when taking damage.\\n\\n- Individual: each ally changes hostility on their own (TR3 style).\\n- Shared: all allies become hostile together (TR2 monk style).\")\nGS_DEFINE(settings/gameplay.camera_speed/description, \"Changes how fast the manual camera moves.\")\nGS_DEFINE(settings/gameplay.change_pierre_spawn/description, \"Makes a freshly triggered (runaway) Pierre replace an already existing (runaway) Pierre.\")\nGS_DEFINE(settings/gameplay.creature_drown_policy/description, \"Controls how land creatures behave in water rooms.\\n\\n- Never: land creatures will never drown (TR1 style).\\n- Default: land creatures will drown in 2-click or deeper water (TR2/3 style).\\n- Submerged: land creatures will drown only when fully submerged.\")\nGS_DEFINE(settings/gameplay.disable_extra_guns/description, \"Removes all weapon and ammo pickups from the game except Pistols (for Pistols Only challenge runs).\")\nGS_DEFINE(settings/gameplay.disable_healing_between_levels/description, \"Stops Lara from healing when starting a new level (for no Heal challenge runs).\")\nGS_DEFINE(settings/gameplay.disable_medpacks/description, \"Removes all medipacks from the game (for No Meds challenge runs).\")\nGS_DEFINE(settings/gameplay.disable_trex_collision/description, \"Removes all collision with T-Rex upon death. This helps when the T-Rex's body blocks the passage out.\")\nGS_DEFINE(settings/gameplay.enable_ally_targeting/description, \"Allows Lara to target allies, such as monks. If disabled, allies will be immune to Lara's ammunition.\")\nGS_DEFINE(settings/gameplay.enable_auto_item_selection/description, \"When Lara presses action against a keyhole or puzzle slot, and she has the corresponding item in the inventory, that item will be pre-selected.\")\nGS_DEFINE(settings/gameplay.enable_back_slope_stumble/description, \"Makes Lara perform a stumble if she hops back and there is a slope behind her (TR3). If disabled, Lara will come to a hard stop against the slope (TR1/2).\")\nGS_DEFINE(settings/gameplay.enable_body_bags/description, \"Enables removal of killed enemies when Lara crosses specific triggers in certain levels. If disabled, dead enemies will always be drawn.\")\nGS_DEFINE(settings/gameplay.enable_boulder_shake/description, \"If enabled, the camera will shake when a boulder is in motion.\")\nGS_DEFINE(settings/gameplay.enable_bouncy_grenades/description, \"Enables TR3-style grenade behavior: they ricochet off walls and slopes and produce a larger blast radius, but at the expense of reduced velocity.\")\nGS_DEFINE(settings/gameplay.enable_cheats/description, \"Enables various cheats:\\n\\n- L: immediately end the level.\\n- I: give Lara all weapons; a boost of ammo and medipacks; and all plot items for the current level.\\n- O: enable fly cheat (swimming midair).\\n  - WALK key: exit fly mode.\\n  - GUN key: open the closest door (doesn't work in some places).\")\nGS_DEFINE(settings/gameplay.enable_cinematics/description, \"Enables cinematics at the beginning of certain levels that have them defined.\")\nGS_DEFINE(settings/gameplay.enable_compass_stats/description, \"Enables showing level statistics when the compass is selected.\")\nGS_DEFINE(settings/gameplay.enable_console/description, \"Enables the developer console.\")\nGS_DEFINE(settings/gameplay.enable_controlled_drops/description, \"Allows Lara to turn mid-air and grab the ledge she just stepped off, if the action input is held while falling.\")\nGS_DEFINE(settings/gameplay.enable_crawl_jump/description, \"Allows Lara to jump out of crawlspaces.\")\nGS_DEFINE(settings/gameplay.enable_crawl_tilt/description, \"Aligns Lara's rotation to the floor geometry when crawling.\")\nGS_DEFINE(settings/gameplay.enable_crawling/description, \"Allows Lara to crouch and crawl.\")\nGS_DEFINE(settings/gameplay.enable_credits/description, \"Enables credits screens shown after completing the game. Does not influence the final statistics screen.\")\nGS_DEFINE(settings/gameplay.enable_crouch_roll/description, \"Allows Lara to do a forward roll while crouched by pressing sprint.\")\nGS_DEFINE(settings/gameplay.enable_cutscenes/description, \"Enables cutscenes playing.\")\nGS_DEFINE(settings/gameplay.enable_demo/description, \"Enables demos showing in the main menu.\")\nGS_DEFINE(settings/gameplay.enable_enemy_rotation/description, \"Applies an additional random angle to some enemies when they are initialised.\")\nGS_DEFINE(settings/gameplay.enable_enhanced_saves/description, \"Enhances savegames so that graphic effects, waterfall mist, flame emitters, and more are saved instead of disappearing on load.\")\nGS_DEFINE(settings/gameplay.enable_fmv/description, \"Enables FMVs playing.\")\nGS_DEFINE(settings/gameplay.enable_game_modes/description, \"Allows new game plus options to be selected from the new game passport menu.\\n\\n- New Game+: unlocks all weapons with infinite ammo; enemies have double the HP.\\n- Japanese NG: weapons do double damage and flare pickups contain 8 rather than 6.\\n- Japanese NG+: combination of New Game+ and Japanese NG.\")\nGS_DEFINE(settings/gameplay.enable_idle_pose_camera/description, \"Adjusts the camera to face Lara during her idle pose animation. Pressing look will reset the camera.\")\nGS_DEFINE(settings/gameplay.enable_inverted_look/description, \"Inverts the Y axis controls when Lara looks.\")\nGS_DEFINE(settings/gameplay.enable_item_examining/description, \"For custom levels - allows item descriptions to be displayed in the inventory where the level author has provided suitable data.\")\nGS_DEFINE(settings/gameplay.enable_jump_twists/description, \"Enables jump twists and somersaults i.e. press roll during jump and swan dive animations.\")\nGS_DEFINE(settings/gameplay.enable_killer_pushblocks/description, \"If enabled, when a pushblock falls from the air and lands on Lara, it will kill her outright. Otherwise, Lara will clip on top of the block and survive.\")\nGS_DEFINE(settings/gameplay.enable_lean_jumping/description, \"Allows Lara to creep forwards or backwards further when performing neutral jumps with the relevant input key pressed.\")\nGS_DEFINE(settings/gameplay.enable_ledge_jumps/description, \"Allows Lara to jump upwards or backwards when hanging from a ledge, provided she has a solid surface in front of her to push against.\")\nGS_DEFINE(settings/gameplay.enable_legal/description, \"Enables legal screen and Core Design FMV at the game start.\")\nGS_DEFINE(settings/gameplay.enable_manual_camera/description, \"Enables the camera keys (\\\\{input camera_forward}\\\\{input camera_back}\\\\{input camera_left}\\\\{input camera_right}) used to control Photo Mode camera, to also rotate the ingame camera.\")\nGS_DEFINE(settings/gameplay.enable_neutral_twists/description, \"Allows Lara to twist in the air while performing a neutral jump. Press jump and roll inputs together while stationary.\")\nGS_DEFINE(settings/gameplay.enable_pickup_aids/description, \"Enables an intermittent twinkling effect near pickup items to highlight their presence.\")\nGS_DEFINE(settings/gameplay.enable_play_previous_levels/description, \"Enables the \\\"Play previous levels\\\" and \\\"Story so far...\\\" features in the New Game selection screen.\")\nGS_DEFINE(settings/gameplay.pause_on_focus_lost/description, \"Stops gameplay from advancing when the game window loses focus.\")\nGS_DEFINE(settings/gameplay.enable_responsive_crawl/description, \"Enables enhancements over original crawling mechanics.\\n\\n- Allows resuming crawling more quickly after coming to a stop.\\n- Allows transitioning from run/sprint to crawl without first coming to a stop.\\n- Allows transitioning from crawl to crouch-roll (if enabled) without manually crouching first.\\n- Allows turning while crouched.\\n- Restores Lara's crawl pickup animation (excluding flares).\")\nGS_DEFINE(settings/gameplay.enable_responsive_sprint/description, \"Enables a more responsive sprinting state for Lara.\\n\\n- allows sprinting whenever Lara has energy, rather than only when her stamina is full.\\n- allows sprinting up stairs rather than being interrupted by Lara's regular run animation.\")\nGS_DEFINE(settings/gameplay.enable_save_crystals/description, \"Limits saving to the beginning of levels and save crystals. Levels have limited, single use save crystals like the PS1 version. Changing this option will require restarting the level.\")\nGS_DEFINE(settings/gameplay.enable_slide_to_run/description, \"Allows Lara to start running immediately when she reaches ground after sliding forwards on a slope. Hold the forward input to activate.\")\nGS_DEFINE(settings/gameplay.enable_slow_ledge_swing/description, \"Allows Lara to swing slowly when she has grabbed a very thin ledge (TR3 style). If disabled, Lara will swing briefly before coming to a resting hanging position (TR1/2 style).\")\nGS_DEFINE(settings/gameplay.enable_smooth_wall_deflect/description, \"Allows Lara to recover more quickly after hitting a wall and a direction key is held together with forward.\")\nGS_DEFINE(settings/gameplay.enable_soft_statics/description, \"Allows Lara to move smoothly against static meshes - similar to TR4+ - rather than coming to a hard stop.\")\nGS_DEFINE(settings/gameplay.enable_sprint/description, \"Allows Lara to sprint, similar to TR3+.\")\nGS_DEFINE(settings/gameplay.enable_step_roll_boost/description, \"Allows Lara to be boosted off a one-click high step if roll is pressed near the edge.\")\nGS_DEFINE(settings/gameplay.enable_swing_cancel/description, \"Allows Lara's ledge-swinging animation to be canceled by letting go and quickly grabbing again.\")\nGS_DEFINE(settings/gameplay.enable_target_change/description, \"Enables TR4+ target changing while aiming weapons. Press the Change Target button while aiming to change targets.\")\nGS_DEFINE(settings/gameplay.enable_timer_in_inventory/description, \"Makes the in-game timer work even while the game is showing the inventory.\")\nGS_DEFINE(settings/gameplay.enable_toggle_crouch/description, \"Allows Lara to stay crouched after pressing the crouch input once. Press crouch again to stand up.\")\nGS_DEFINE(settings/gameplay.enable_toggle_sprint/description, \"Allows Lara to keep sprinting after pressing the sprint input once. Press sprint again to stop sprinting.\")\nGS_DEFINE(settings/gameplay.enable_total_stats/description, \"Enables a total game statistics screen that plays after the credits.\")\nGS_DEFINE(settings/gameplay.enable_tr2_jumping/description, \"Allows Lara to jump at any point while running.\")\nGS_DEFINE(settings/gameplay.enable_tr2_swim_cancel/description, \"Allows Lara to stop more responsively underwater when the swim key is released.\")\nGS_DEFINE(settings/gameplay.enable_tr2_swimming/description, \"Gives Lara's underwater turn rate an acceleration curve for smoother movement, as per TR2+ originally. Disabling this option will give Lara a snappier turn rate, per original TR1.\")\nGS_DEFINE(settings/gameplay.enable_uw_roll/description, \"Allows Lara to roll while underwater.\")\nGS_DEFINE(settings/gameplay.enable_wading/description, \"Allows Lara to wade through shallow water, rather than becoming stuck on the water surface.\")\nGS_DEFINE(settings/gameplay.enable_walk_to_items/description, \"Makes Lara walk to pickups and switches when nearby instead of teleporting to them.\")\nGS_DEFINE(settings/gameplay.fix_alligator_ai/description, \"Fixes alligators dealing no damage if Lara remains still in the water.\")\nGS_DEFINE(settings/gameplay.fix_bear_ai/description, \"Fixes the bear pat attack so it does not miss Lara.\")\nGS_DEFINE(settings/gameplay.fix_bridge_collision/description, \"Fixes Lara not being able to grab parts of some bridges and invisible walls at the edge. Also fixes collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground.\")\nGS_DEFINE(settings/gameplay.fix_descending_glitch/description, \"Fixes sidestepping and walking backwards on breakable tiles causing Lara to immediately descend to the tile underneath.\")\nGS_DEFINE(settings/gameplay.fix_flare_throw_priority/description, \"Fixes Lara prioritising throwing a spent flare while in mid-air, which can lead to being unable to grab ledges.\")\nGS_DEFINE(settings/gameplay.fix_floor_data_issues/description, \"Fixes original issues with floor data/triggers.\")\nGS_DEFINE(settings/gameplay.fix_free_flare_glitch/description, \"Fixes the ability to spawn a free flare when pressing the flare input while picking up any item.\")\nGS_DEFINE(settings/gameplay.fix_item_duplication_glitch/description, \"Fixes the ability to duplicate usage of key items in the inventory.\")\nGS_DEFINE(settings/gameplay.fix_lara_pickup_embed/description, \"Fixes Lara sometimes drifting into walls when collecting underwater items, and fixes Lara embedding under steeply sloped ceilings when picking up an item above water.\")\nGS_DEFINE(settings/gameplay.fix_m16_accuracy/description, \"Fixes the accuracy of the M16/MP5 while Lara is running.\")\nGS_DEFINE(settings/gameplay.fix_monkey_pickup_priority/description, \"Attacked monkeys will prioritize retaliating over collecting Medi packs and Keys.\")\nGS_DEFINE(settings/gameplay.fix_pipeman_aim/description, \"Fixes the pipeman sometimes not being able to aim darts at Lara correctly.\")\nGS_DEFINE(settings/gameplay.fix_qwop_glitch/description, \"Fixes Lara jumping on small steps sometimes resulting in a weird running animation, known as a QWOP state.\")\nGS_DEFINE(settings/gameplay.fix_step_glitch/description, \"Fixes Lara sometimes being pushed into walls adjacent to steps when running up them in a specific way.\")\nGS_DEFINE(settings/gameplay.fix_wade_wall_hit/description, \"Fixes Lara not responding to hitting a wall while wading.\")\nGS_DEFINE(settings/gameplay.fix_walk_run_jump/description, \"Fixes Lara at times not being able to jump immediately after going from her walking to running animation.\")\nGS_DEFINE(settings/gameplay.fix_wall_geometry/description, \"Fixes cases in OG level geometry where tilts inside walls can lead to inaccurate height calculations.\")\nGS_DEFINE(settings/gameplay.fix_water_exit/description, \"Fixes Lara being able to go directly from a water room to an adjacent dry room, or to a dry room below. Additionally, this will prevent Lara from being able to climb out of water onto non-standable slopes.\")\nGS_DEFINE(settings/gameplay.harpoon_recoil/description, \"Sets how often Lara must reload the harpoon gun, based on her current ammo count. For example, if set to 3, she'll need to reload after every third shot. Set to 0 to disable reloading entirely.\")\nGS_DEFINE(settings/gameplay.idle_pose_timeout/description, \"Allows Lara to enter a pose animation when she has been idle for the specified number of seconds. Set to 0 to disable.\")\nGS_DEFINE(settings/gameplay.jump_lock_mode/description, \"For responsive jumping, allows controlling how soon after starting to run that Lara is permitted to jump.\\n\\n- Legacy: matches original TR2 timing.\\n- Tuned: jumping is possible 2 frames earlier.\\n- Disabled: jumping is possible immediately after the start-to-run animation.\")\nGS_DEFINE(settings/gameplay.loading_screens/description, \"Controls loading screens before level loads.\\n\\n- Disabled: never show loading screens.\\n- Always: show loading screens.\\n- New games: skip showing loading screens when loading a save.\")\nGS_DEFINE(settings/gameplay.look_mode/description, \"Allows controlling when Lara is able to use look.\\n\\n- Restricted: look is only permitted when Lara is stationary, and never when underwater.\\n- Enhanced: look is permitted during most animations, aside from ones such as pushing a block.\\n- Unrestricted: look is permitted at any time during normal Lara control.\")\nGS_DEFINE(settings/gameplay.maximum_quick_save_slots/description, \"Changes the number of available quick save slots.\")\nGS_DEFINE(settings/gameplay.maximum_save_slots/description, \"Changes the number of available save slots.\")\nGS_DEFINE(settings/gameplay.projectile_area_damage/description, \"Controls how the area-of-effect for Rocket Launcher and Grenade Launcher propagates.\\n\\n- Single-sweep: TR1 & TR2 behavior.\\n- Multi-sweep: TR3 behavior.\\n\\nThe multi-sweep option often ends up doing double damage to individual enemies.\")\nGS_DEFINE(settings/gameplay.remember_gun_status/description, \"Makes Lara remember which gun she was using last in the previous level when starting a new level. If disabled, Lara will revert to holstered pistols.\")\nGS_DEFINE(settings/gameplay.restore_ps1_enemies/description, \"Adds the mummy that appears in the PlayStation version of City of Khamoon, room 25.\\nChanging this option will require restarting the game.\")\nGS_DEFINE(settings/gameplay.start_lara_hitpoints/description, \"Sets Lara's health value for the beginning of each level.\")\nGS_DEFINE(settings/gameplay.target_mode/description, \"Changes the behavior of how weapons lock onto targets.\\n\\n- Full lock: always keep target lock even if the enemy moves out of sight or dies (OG TR1-3).\\n- Semi lock: keep target lock if the enemy moves out of sight but lose target lock if the enemy dies.\\n- No lock: lose target lock if the enemy goes out of sight or dies (TR4+).\")\nGS_DEFINE(settings/gameplay.wall_glitch_mode/description, \"Allows using TR1 wall glitch behavior in TR2 and vice-versa; equally allows fixing all types of wall glitch.\")\nGS_DEFINE(settings/input.enable_buffering_func_keys/description, \"Enables F-key (1-frame) buffering to achieve precise control of Lara's movement. This function originally only exists in the TombATI port (TR1).\")\nGS_DEFINE(settings/input.enable_buffering_inventory/description, \"Enables inventory (2-frame) buffering to achieve precise control of Lara's movement.\")\nGS_DEFINE(settings/input.enable_responsive_passport/description, \"Disables blocking user input when passport flips pages, scheduling the page flips instead.\")\nGS_DEFINE(settings/input.enable_tr3_sidesteps/description, \"Enables TR3+ style sidesteps, e.g. shift+directional arrows. Dedicated sidestep buttons will still work.\")\nGS_DEFINE(settings/input.quick_guns_mode/description, \"Controls the behavior of the quick gun equip keys.\\n\\n- Draw only: pressing a key will cause Lara to equip the assigned gun.\\n- Draw or holster: same as above, plus Lara will undraw the assigned gun if she's currently carrying it.\")\nGS_DEFINE(settings/language/description, \"Changes the language of the UI text.\")\nGS_DEFINE(settings/rendering.anisotropy_filter/description, \"Enhances texture filtering at distances.\")\nGS_DEFINE(settings/rendering.aspect_mode/description, \"Forces certain game aspect ratios with letterbox.\")\nGS_DEFINE(settings/rendering.borders/description, \"Adds black borders around the game window.\")\nGS_DEFINE(settings/rendering.enable_trapezoid_filter/description, \"Corrects rendering of quadrilaterals.\")\nGS_DEFINE(settings/rendering.enable_vsync/description, \"Turns V-Sync on or off.\")\nGS_DEFINE(settings/rendering.fps/description, \"Sets game frames per second.\")\nGS_DEFINE(settings/rendering.lighting_contrast/description, \"Boosts contrast for dynamic light sources such as flares and gun flashes.\")\nGS_DEFINE(settings/rendering.screenshot_format/description, \"Screenshot file format.\")\nGS_DEFINE(settings/rendering.sprite_lock_mode/description, \"Controls which axes to lock when showing sprites on the screen.\\n\\n- None: show the sprites normally.\\n- Roll: lock the roll axis – useful only in photo mode.\\n- Roll & pitch: ensure the sprites stand upright and do not lie on the ground when looking at them from above.\\n- Perspective: lock roll and pitch axes and addititonally, rotate the sprites slightly towards the center of the screen.\")\nGS_DEFINE(settings/rendering.texture_filter/description, \"Switches between smooth and pixel ingame textures.\")\nGS_DEFINE(settings/rendering.ui_filter/description, \"Switches between smooth and pixel UI textures.\")\nGS_DEFINE(settings/rendering.upscaling_factor/description, \"Upscales game by a set factor, maintaining pixellated look.\")\nGS_DEFINE(settings/rendering.upscaling_filter/description, \"Switches smooth or pixel look for the whole screen.\")\nGS_DEFINE(settings/ui.airbar_color/description, \"Color of the airbar.\")\nGS_DEFINE(settings/ui.airbar_color_ps1/description, \"Color of the airbar.\")\nGS_DEFINE(settings/ui.airbar_location/description, \"Location where the airbar is displayed.\")\nGS_DEFINE(settings/ui.ammo_counter_location/description, \"Location where the ammo counter is displayed.\")\nGS_DEFINE(settings/ui.bar_look/description, \"Controls the visual appearance of the UI bars.\")\nGS_DEFINE(settings/ui.bar_scale/description, \"Changes size of UI bars.\")\nGS_DEFINE(settings/ui.enable_bar_flashing/description, \"Makes Lara's health and oxygen bars blink when she's running low on either resource.\")\nGS_DEFINE(settings/ui.enable_smooth_bars/description, \"Makes the UI bars use smooth color transitions.\")\nGS_DEFINE(settings/ui.enable_wraparound/description, \"Lets directional navigation in menus loop around.\")\nGS_DEFINE(settings/ui.enemy_healthbar_color/description, \"Color of the enemy healthbar.\")\nGS_DEFINE(settings/ui.enemy_healthbar_color_allies/description, \"Color of the allies healthbar. Shown in the location of the enemy healthbars.\")\nGS_DEFINE(settings/ui.enemy_healthbar_color_allies_ps1/description, \"Color of the allies healthbar. Shown in the location of the enemy healthbars.\")\nGS_DEFINE(settings/ui.enemy_healthbar_color_ps1/description, \"Color of the enemy healthbar.\")\nGS_DEFINE(settings/ui.enemy_healthbar_location/description, \"Location where the enemy healthbar is displayed.\")\nGS_DEFINE(settings/ui.enemy_healthbar_show_mode/description, \"Enables showing a healthbar for the active enemy.\")\nGS_DEFINE(settings/ui.exposurebar_color/description, \"Color of the cold water exposure bar.\")\nGS_DEFINE(settings/ui.exposurebar_color_ps1/description, \"Color of the cold water exposure bar.\")\nGS_DEFINE(settings/ui.exposurebar_location/description, \"Location where the cold water exposure bar is displayed.\")\nGS_DEFINE(settings/ui.healthbar_color/description, \"Color of the healthbar.\")\nGS_DEFINE(settings/ui.healthbar_color_ps1/description, \"Color of the healthbar.\")\nGS_DEFINE(settings/ui.healthbar_location/description, \"Location where the healthbar is displayed.\")\nGS_DEFINE(settings/ui.healthbar_poison_color/description, \"Color of the healthbar when Lara is poisoned.\")\nGS_DEFINE(settings/ui.healthbar_poison_color_ps1/description, \"Color of the healthbar when Lara is poisoned.\")\nGS_DEFINE(settings/ui.inventory_background_style/description, \"Changes the way the background for the inventory ring is displayed.\\n\\n- Dark: TR1 (PC).\\n- Very dark: TR1 (PS1).\\n- Static: TR2 (PC).\\n- Wave: TR2 (PS1).\\n- Monochrome: TR3.\")\nGS_DEFINE(settings/ui.inventory_fade_effects/description, \"Fine-tunes the fade effects to be enabled or disabled in the in-game inventory ring. Needs the Fade Effects option to be enabled to work.\")\nGS_DEFINE(settings/ui.menu_style/description, \"Changes how menus are displayed.\\n\\n - PC: UI style matches the PC version.\\n - PS1: UI style matches the PS1 version.\")\nGS_DEFINE(settings/ui.pause_background_style/description, \"Changes the way the background for the pause screen is displayed.\\n\\n- Dark: TR1 (PC).\\n- Very dark: TR1 (PS1).\\n- Static: TR2 (PC).\\n- Wave: TR2 (PS1).\\n- Monochrome: TR3.\")\nGS_DEFINE(settings/ui.pause_fade_effects/description, \"Fine-tunes the fade effects to be enabled or disabled in the pause screen. Needs the Fade Effects option to be enabled to work.\")\nGS_DEFINE(settings/ui.pickup_scale/description, \"Changes size of items animated in the UI when Lara picks something up.\")\nGS_DEFINE(settings/ui.show_bars/description, \"Disables all ingame bars, obscuring information on Lara's health and other resources (for challenge runs).\")\nGS_DEFINE(settings/ui.show_pickups_overlay/description, \"Shows items in the bottom-right corner when Lara picks something up.\")\nGS_DEFINE(settings/ui.show_title_version/description, \"Shows the TRX version string in the title inventory ring.\")\nGS_DEFINE(settings/ui.sprintbar_color/description, \"Color of the sprintbar.\")\nGS_DEFINE(settings/ui.sprintbar_color_ps1/description, \"Color of the sprintbar.\")\nGS_DEFINE(settings/ui.sprintbar_location/description, \"Location where the sprintbar is displayed.\")\nGS_DEFINE(settings/ui.stats.show_ammo/description, \"Shows the ammo row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_deaths/description, \"Shows Lara's deaths in the compass statistics and in the level statistics. Death count is updated in the currently loaded save as soon as Lara dies.\")\nGS_DEFINE(settings/ui.stats.show_distance_travelled/description, \"Shows the distance traveled row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_kills/description, \"Shows the kills row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_level_header/description, \"Shows the current level number at the top of the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_medipacks_used/description, \"Shows the health packs used row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_crystals/description, \"Shows the crystals row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_pickups/description, \"Shows the pickups row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_secrets/description, \"Shows the secrets found row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_time_taken/description, \"Shows the time taken row in the level statistics.\")\nGS_DEFINE(settings/ui.stats.show_totals/description, \"Shows totals next to stats when applicable. Secrets remain unaffected by this setting.\")\nGS_DEFINE(settings/ui.stats.style/description, \"Controls how the statistics dialog is displayed.\\n\\n- Bare: shows the simpler unframed layout.\\n- Bordered: shows the boxed layout.\")\nGS_DEFINE(settings/ui.stats_background_style/description, \"Changes the way the background for the end of level stats is displayed.\\n\\n- Dark: TR1 (PC).\\n- Very dark: TR1 (PS1).\\n- Static: TR2 (PC).\\n- Wave: TR2 (PS1).\\n- Monochrome: TR3.\")\nGS_DEFINE(settings/ui.stats_fade_effects/description, \"Fine-tunes the fade effects to be enabled or disabled in the end of the level statistics screen. Needs the Fade Effects option to be enabled to work.\")\nGS_DEFINE(settings/ui.text_scale/description, \"Changes the size of UI text.\")\nGS_DEFINE(settings/visuals.camera_mode/description, \"Adjusts how camera behaves during actions like using keys.\")\nGS_DEFINE(settings/visuals.enable_3d_pickups/description, \"Enables 3D models to be rendered in place of the sprites for pickup items.\")\nGS_DEFINE(settings/visuals.enable_braid/description, \"Enables Lara's braid.\")\nGS_DEFINE(settings/visuals.enable_breeze/description, \"Enables the breeze effect on Lara's braid in appropriate rooms.\")\nGS_DEFINE(settings/visuals.blood_effects/description, \"Controls blood spark colors.\\n\\n- Disabled: no blood sparks are shown.\\n- Pink: the default in German PC releases of TR3.\\n- Red: the default in all other retail releases.\")\nGS_DEFINE(settings/visuals.enable_exit_fade_effects/description, \"Enables the fade effects when exiting the game to desktop.\")\nGS_DEFINE(settings/visuals.enable_fade_effects/description, \"Enable fade transitions, for example between credit graphics or for inventory and pause screen transitions.\")\nGS_DEFINE(settings/visuals.enable_fire_lighting/description, \"Enables dynamic lighting to be generated beside active flames.\")\nGS_DEFINE(settings/visuals.enable_footprints/description, \"Enables rendering of Lara's footprints on certain surfaces in supported levels.\")\nGS_DEFINE(settings/visuals.enable_glide_cameras/description, \"Enables a glide action on fixed cameras that look at Lara by adopting a smooth speed curve. If disabled, such cameras will change the view to look at Lara immediately.\")\nGS_DEFINE(settings/visuals.enable_gun_lighting/description, \"Enables dynamic lighting to be generated for gunshots and explosions.\")\nGS_DEFINE(settings/visuals.enable_ps1_crystals/description, \"Save crystals will be drawn with a purple tint, more similar to the PS1 type.\")\nGS_DEFINE(settings/visuals.enable_reflections/description, \"Enables reflections on certain objects.\")\nGS_DEFINE(settings/visuals.enable_responsive_mesh_tint/description, \"Enables Lara's individual meshes to be drawn with a water tint if they are themselves located underwater (TR3 style). Otherwise, if Lara is in water, each of her meshes will be drawn with the tint (TR1/2 style).\")\nGS_DEFINE(settings/visuals.enable_shotgun_flash/description, \"Draws flames when firing the shotgun, like for other guns.\")\nGS_DEFINE(settings/visuals.enable_skybox/description, \"Enables the skybox in supported levels.\")\nGS_DEFINE(settings/visuals.enable_weather/description, \"Enables rendering of weather effects in supported levels.\")\nGS_DEFINE(settings/visuals.fix_animated_sprites/description, \"Fixes original underwater plant sprites so they animate properly in water areas.\")\nGS_DEFINE(settings/visuals.fix_item_rots/description, \"Fixes original issues with some incorrectly rotated pickups when using the 3D pickups option.\")\nGS_DEFINE(settings/visuals.fix_texture_issues/description, \"Fixes original issues with missing or incorrect textures/meshes.\")\nGS_DEFINE(settings/visuals.fog_color/description, \"Color of the fog.\")\nGS_DEFINE(settings/visuals.fog_end/description, \"Sets distance in tiles where fog makes everything fully obscured.\")\nGS_DEFINE(settings/visuals.fog_start/description, \"Sets distance in tiles where fog begins to appear.\")\nGS_DEFINE(settings/visuals.fog_transparency/description, \"Whether to enable blending distant geometry into 100% transparent faces.\")\nGS_DEFINE(settings/visuals.fov/description, \"Viewing angle in degrees. Larger values widen the field of view, smaller ones narrow it.\")\nGS_DEFINE(settings/visuals.game_brightness/description, \"Changes game brightness.\")\nGS_DEFINE(settings/visuals.gamma/description, \"Adjusts the gamma curve. Higher values mean brighter lighting. The value of 2.5 means default colors.\")\nGS_DEFINE(settings/visuals.lara_outfit/description, \"Changes Lara's appearance. Choosing Default will respect any regular outfit changes between levels, otherwise the chosen value will persist until changed manually.\")\nGS_DEFINE(settings/visuals.shadow_type/description, \"Selects how entity shadows are rendered.\\n\\n- Octagon: old TR1 and TR2 shadows\\n- Circle: round shadows\\n- Sprite: TR3 texture-based shadows\")\nGS_DEFINE(settings/visuals.sunglasses_mode/description, \"Changes the style of Lara's sunglasses. Note that lenses will be reflective if the relevant option is enabled.\\n\\n- Off: Lara will not wear sunglasses.\\n- Opaque: Lara's sunglasses will have opaque lenses.\\n- Transparent: Lara's sunglasses will have semi-transparent lenses.\")\nGS_DEFINE(settings/visuals.ui_brightness/description, \"Changes UI brightness.\")\nGS_DEFINE(settings/visuals.water_color/description, \"Color of the water.\")\n\nGS_DEFINE(general/stats/basic_fmt, \"%d\")\nGS_DEFINE(general/stats/detail_fmt, \"%d of %d\")\nGS_DEFINE(general/stats/final_statistics, \"Final Statistics\")\nGS_DEFINE(general/stats/bonus_statistics, \"Bonus Statistics\")\nGS_DEFINE(general/stats/assault_title, \"BEST TIMES\")\nGS_DEFINE(general/stats/assault_no_times_set, \"No Times Set\")\nGS_DEFINE(general/stats/assault_finish, \"Finish\")\nGS_DEFINE(general/stats/assault_best_time_fmt, \"%s\")\nGS_DEFINE(general/stats/assault_other_times_fmt, \"%s\")\nGS_DEFINE(general/stats/gym_assault_course, \"Assault Course\")\nGS_DEFINE(general/stats/gym_racetrack_course, \"Race Track Course\")\n\n// Misc\nGS_DEFINE(general/inventory_ring/heading_inventory, \"INVENTORY\")\nGS_DEFINE(general/inventory_ring/heading_game_over, \"GAME OVER\")\nGS_DEFINE(general/inventory_ring/heading_option, \"OPTION\")\nGS_DEFINE(general/inventory_ring/heading_items, \"ITEMS\")\nGS_DEFINE(general/inventory_ring/heading_adventure, \"Adventure\")\nGS_DEFINE(general/inventory_ring/heading_fmt, \"%s\")\nGS_DEFINE(general/inventory_ring/object_name_fmt, \"%s\")\nGS_DEFINE(general/inventory_ring/item_count_fmt, \"\\\\{small}%s\")\nGS_DEFINE(general/overlay/item_count_fmt_pc, \"\\\\{small}%s\")\nGS_DEFINE(general/overlay/item_count_fmt_ps1, \"\\\\{small}%s\")\nGS_DEFINE(general/osd/pos_lara_pos_fmt, \"Room: %d\\nPosition: %.3f, %.3f, %.3f\\nRotation: %.3f, %.3f, %.3f\")\nGS_DEFINE(general/osd/pos_lara_missing, \"Lara not present\")\nGS_DEFINE(general/osd/pos_level_fmt, \"Level %d\")\nGS_DEFINE(general/osd/pos_level_fmt_demo, \"Demo %d\")\nGS_DEFINE(general/osd/pos_level_fmt_cutscene, \"Cutscene %d\")\nGS_DEFINE(general/osd/current_health_get, \"Current Lara's health: %d\")\nGS_DEFINE(general/osd/current_health_set, \"Lara's health set to %d\")\nGS_DEFINE(general/osd/config_option_get, \"%s is currently set to %s\")\nGS_DEFINE(general/osd/config_option_set, \"%s changed to %s\")\nGS_DEFINE(general/osd/config_option_unknown_option, \"Unknown option: %s\")\nGS_DEFINE(general/osd/speed_get, \"Current speed: %d\")\nGS_DEFINE(general/osd/speed_set, \"Speed set to %d\")\nGS_DEFINE(general/osd/lighting_contrast_fmt, \"Lighting Contrast: %s\")\nGS_DEFINE(general/osd/bilinear_filter_on, \"Bilinear filter: on\")\nGS_DEFINE(general/osd/bilinear_filter_off, \"Bilinear filter: off\")\nGS_DEFINE(general/osd/wireframe_mode_on, \"Wireframe mode: on\")\nGS_DEFINE(general/osd/wireframe_mode_off, \"Wireframe mode: off\")\nGS_DEFINE(general/osd/textures_on, \"Textures: on\")\nGS_DEFINE(general/osd/textures_off, \"Textures: off\")\nGS_DEFINE(general/overlay/debug_position, \"Position: \")\nGS_DEFINE(general/overlay/debug_camera_pos, \"Camera origin: \")\nGS_DEFINE(general/overlay/debug_camera_target, \"Camera target: \")\nGS_DEFINE(general/overlay/debug_rotation, \"Rotation: \")\nGS_DEFINE(general/overlay/debug_speed, \"Speed: \")\nGS_DEFINE(general/overlay/debug_animation, \"Animation: \")\nGS_DEFINE(general/overlay/debug_animation_state, \"State: \")\nGS_DEFINE(general/overlay/debug_immune, \"Invulnerability on\")\nGS_DEFINE(general/misc/on, \"On\")\nGS_DEFINE(general/misc/off, \"Off\")\nGS_DEFINE(general/misc/demo_mode, \"Demo Mode\")\nGS_DEFINE(general/misc/direction_keys_keyboard, \"Arrows\")\nGS_DEFINE(general/misc/direction_keys_controller, \"D-Pad\")\nGS_DEFINE(general/osd/heal_already_full_hp, \"Lara's already at full health\")\nGS_DEFINE(general/osd/heal_success, \"Healed Lara back to full health\")\nGS_DEFINE(general/osd/give_item, \"Added %s to Lara's inventory\")\nGS_DEFINE(general/osd/invalid_item, \"Unknown item: %s\")\nGS_DEFINE(general/osd/kill_all, \"Poof! %d enemies gone!\")\nGS_DEFINE(general/osd/kill_all_fail, \"Uh-oh, there are no enemies left to kill...\")\nGS_DEFINE(general/osd/kill, \"Bye-bye!\")\nGS_DEFINE(general/osd/kill_fail, \"No enemy nearby...\")\nGS_DEFINE(general/osd/invalid_object, \"Invalid object\")\nGS_DEFINE(general/osd/invalid_sample, \"Invalid sound: %d\")\nGS_DEFINE(general/osd/object_not_found, \"Object not found\")\nGS_DEFINE(general/osd/sound_available_samples, \"Available sounds: %s\")\nGS_DEFINE(general/osd/sound_playing_sample, \"Playing sound %d\")\nGS_DEFINE(general/osd/unknown_command, \"Unknown command: %s\")\nGS_DEFINE(general/osd/command_bad_invocation, \"Invalid invocation: %s\")\nGS_DEFINE(general/osd/command_valid_values, \"Valid values: %s\")\nGS_DEFINE(general/osd/command_bool, \"on, off\")\nGS_DEFINE(general/osd/command_integer, \"[integer]\")\nGS_DEFINE(general/osd/command_decimal, \"[decimal]\")\nGS_DEFINE(general/osd/command_percent, \"[integer]\")\nGS_DEFINE(general/osd/command_unavailable, \"This command is not currently available\")\nGS_DEFINE(general/osd/invalid_room, \"Invalid room: %d. Valid rooms are 0-%d\")\nGS_DEFINE(console/cmd/teleport/pos,         \"Teleported to position: %.3f %.3f %.3f\")\nGS_DEFINE(console/cmd/teleport/pos_fail,    \"Failed to teleport to position: %.3f %.3f %.3f\")\nGS_DEFINE(console/cmd/teleport/room,        \"Teleported to room: %d\")\nGS_DEFINE(console/cmd/teleport/room_fail,   \"Failed to teleport to room: %d\")\nGS_DEFINE(console/cmd/teleport/object,      \"Teleported to object: %s\")\nGS_DEFINE(console/cmd/teleport/object_fail, \"Failed to teleport to object: %s\")\nGS_DEFINE(console/cmd/teleport/item,        \"Teleported to item: %d\")\nGS_DEFINE(console/cmd/teleport/item_fail,   \"Failed to teleport to item: %d\")\nGS_DEFINE(general/osd/play_level, \"Loading %s\")\nGS_DEFINE(general/osd/play_cutscene, \"Loading cutscene %d\")\nGS_DEFINE(general/osd/play_demo, \"Loading demo %d\")\nGS_DEFINE(general/osd/invalid_level, \"Invalid level\")\nGS_DEFINE(general/osd/invalid_cutscene, \"Invalid cutscene\")\nGS_DEFINE(general/osd/invalid_demo, \"Invalid demo\")\nGS_DEFINE(general/osd/load_game, \"Loaded game from save slot %d\")\nGS_DEFINE(general/osd/load_game_fail_unavailable_slot, \"Save slot %d is not available\")\nGS_DEFINE(general/osd/load_game_fail_invalid_slot, \"Invalid save slot %d\")\nGS_DEFINE(general/osd/save_game, \"Saved game to save slot %d\")\nGS_DEFINE(general/osd/save_game_fail_invalid_slot, \"Invalid save slot %d\")\nGS_DEFINE(general/osd/quick_save, \"Quick-saved\")\nGS_DEFINE(general/osd/quick_save_fail_no_slots, \"No quick save slots are configured\")\nGS_DEFINE(general/osd/quick_load, \"Quick-loaded slot %d\")\nGS_DEFINE(general/osd/quick_load_fail_no_bound_slot, \"No save slot is currently bound\")\nGS_DEFINE(general/osd/quick_load_fail_unavailable_bound_slot, \"The bound save slot is not available\")\nGS_DEFINE(general/osd/flipmap_on, \"Flipmap set to ON\")\nGS_DEFINE(general/osd/flipmap_off, \"Flipmap set to OFF\")\nGS_DEFINE(general/osd/flipmap_fail_already_on, \"Flipmap is already ON\")\nGS_DEFINE(general/osd/flipmap_fail_already_off, \"Flipmap is already OFF\")\nGS_DEFINE(general/osd/ambiguous_input_2, \"Ambiguous input: %s and %s\")\nGS_DEFINE(general/osd/ambiguous_input_3, \"Ambiguous input: %s, %s, ...\")\nGS_DEFINE(general/osd/ui_on, \"UI enabled\")\nGS_DEFINE(general/osd/ui_off, \"UI disabled\")\nGS_DEFINE(general/osd/give_item_all_keys, \"Surprise! Every key item Lara needs is now in her backpack.\")\nGS_DEFINE(general/osd/give_item_all_guns, \"Lock'n'load - Lara's armed to the teeth!\")\nGS_DEFINE(general/osd/give_item_cheat, \"Lara's backpack just got way heavier!\")\nGS_DEFINE(general/osd/complete_level, \"Level complete!\")\nGS_DEFINE(general/osd/door_open, \"Open Sesame!\")\nGS_DEFINE(general/osd/door_close, \"Close Sesame!\")\nGS_DEFINE(general/osd/door_open_fail, \"No doors in Lara's proximity\")\nGS_DEFINE(general/osd/fly_mode_on, \"Fly mode enabled\")\nGS_DEFINE(general/osd/fly_mode_off, \"Fly mode disabled\")\nGS_DEFINE(general/settings/controls/layout/default, \"Default Keys\")\nGS_DEFINE(general/settings/controls/layout/custom_1, \"User Keys 1\")\nGS_DEFINE(general/settings/controls/layout/custom_2, \"User Keys 2\")\nGS_DEFINE(general/settings/controls/layout/custom_3, \"User Keys 3\")\nGS_DEFINE(general/settings/controls/backend/keyboard, \"Keyboard\")\nGS_DEFINE(general/settings/controls/backend/controller, \"Controller\")\nGS_DEFINE(general/settings/controls/customize, \"Customize Controls\")\nGS_DEFINE(general/settings/controls/tabs/basics, \"Movement\")\nGS_DEFINE(general/settings/controls/tabs/items, \"Items\")\nGS_DEFINE(general/settings/controls/tabs/misc, \"Misc\")\nGS_DEFINE(general/settings/controls/tabs/system, \"System\")\nGS_DEFINE(general/pause/paused, \"Paused\")\nGS_DEFINE(general/pause/exit_to_title, \"Exit to title?\")\nGS_DEFINE(general/pause/continue, \"Continue\")\nGS_DEFINE(general/pause/quit, \"Quit\")\nGS_DEFINE(general/pause/are_you_sure, \"Are you sure?\")\nGS_DEFINE(general/pause/yes, \"Yes\")\nGS_DEFINE(general/pause/no, \"No\")\nGS_DEFINE(general/photo_mode/camera_move_prompt, \"Move camera\")\nGS_DEFINE(general/photo_mode/camera_reset_prompt, \"Reset camera\")\nGS_DEFINE(general/photo_mode/camera_roll_prompt, \"Roll camera\")\nGS_DEFINE(general/photo_mode/camera_rotate_90_prompt, \"Rotate camera 90°\")\nGS_DEFINE(general/photo_mode/camera_rotate_prompt, \"Rotate camera\")\nGS_DEFINE(general/photo_mode/lara_move_prompt, \"Move Lara\")\nGS_DEFINE(general/photo_mode/lara_reset_prompt, \"Reset Lara\")\nGS_DEFINE(general/photo_mode/lara_roll_prompt, \"Roll Lara\")\nGS_DEFINE(general/photo_mode/lara_rotate_90_prompt, \"Rotate Lara 90°\")\nGS_DEFINE(general/photo_mode/lara_rotate_prompt, \"Rotate Lara\")\nGS_DEFINE(general/photo_mode/fov_prompt, \"Adjust FOV\")\nGS_DEFINE(general/photo_mode/snap_prompt, \"Take picture\")\nGS_DEFINE(general/photo_mode/title_camera_pos, \"Photo Mode\")\nGS_DEFINE(general/photo_mode/title_lara_pos, \"Moving Lara\")\nGS_DEFINE(general/photo_mode/advance_frame, \"Advance frame\")\nGS_DEFINE(general/photo_mode/change_lara_pose, \"Change Lara's pose\")\nGS_DEFINE(general/photo_mode/toggle_help, \"Toggle help\")\nGS_DEFINE(general/misc/exit, \"Exit\")\nGS_DEFINE(general/misc/hold_fmt, \"Hold %s\")\nGS_DEFINE(general/misc/empty_slot_fmt, \"- EMPTY SLOT -\")\nGS_DEFINE(general/passport/exit_game, \"Exit Game\")\nGS_DEFINE(general/passport/exit_to_title, \"Exit to Title\")\nGS_DEFINE(general/passport/load_game, \"Load Game\")\nGS_DEFINE(general/passport/mode_new_game, \"New Game\")\nGS_DEFINE(general/passport/mode_new_game_jp, \"Japanese NG\")\nGS_DEFINE(general/passport/mode_new_game_jp_plus, \"Japanese NG+\")\nGS_DEFINE(general/passport/mode_new_game_plus, \"New Game+\")\nGS_DEFINE(general/passport/new_game, \"New Game\")\nGS_DEFINE(general/passport/delete_save, \"Delete\")\nGS_DEFINE(general/passport/delete_save_confirm, \"Delete this save?\")\nGS_DEFINE(general/passport/delete_save_failed, \"Failed to delete the chosen save.\")\nGS_DEFINE(general/passport/delete_save_yes, \"Yes\")\nGS_DEFINE(general/passport/delete_save_no, \"No\")\nGS_DEFINE(general/passport/play_previous_levels, \"Play previous levels\")\nGS_DEFINE(general/passport/restart_level, \"Restart Level\")\nGS_DEFINE(general/passport/save_game, \"Save Game\")\nGS_DEFINE(general/passport/save_slot_unsupported, \"This save does not support this feature.\")\nGS_DEFINE(general/passport/select_level, \"Select Level\")\nGS_DEFINE(general/passport/select_mod, \"Select Game\")\nGS_DEFINE(general/passport/select_mode, \"Select Mode\")\nGS_DEFINE(general/passport/select_save, \"Select Save\")\nGS_DEFINE(general/passport/story_so_far, \"Story so far...\")\nGS_DEFINE(general/passport/switch_mod, \"Switch Game\")\nGS_DEFINE(general/osd/trapezoid_filter_on, \"Trapezoid filter: on\")\nGS_DEFINE(general/osd/trapezoid_filter_off, \"Trapezoid filter: off\")\nGS_DEFINE(general/osd/fps_counter_on, \"FPS counter: on\")\nGS_DEFINE(general/osd/fps_counter_off, \"FPS counter: off\")\nGS_DEFINE(general/osd/strings_reloaded, \"Language files reloaded\")\nGS_DEFINE(general/osd/strings_failed, \"Failed to reload the language files\")\nGS_DEFINE(general/osd/upscaling_factor, \"Upscaling Factor: x%d\")\nGS_DEFINE(general/misc/pagination_nav, \"%d / %d\")\nGS_DEFINE(general/actions/examine_item, \"Examine\")\nGS_DEFINE(general/actions/hide_dialog, \"Hide dialog\")\nGS_DEFINE(general/actions/rotate, \"Rotate\")\nGS_DEFINE(general/actions/use_item, \"Use\")\nGS_DEFINE(general/actions/reset_defaults, \"Reset All\")\nGS_DEFINE(general/actions/unbind, \"Unbind\")\n\nGS_DEFINE(console/cmd/lua/syntax_error,   \"Lua syntax error: %s\")\nGS_DEFINE(console/cmd/lua/runtime_error,  \"Lua runtime error: %s\")\n\nGS_DEFINE(console/cmd/give/secret_list,   \"Secrets collected: %d of %d (%s)\")\nGS_DEFINE(console/cmd/give/secret_none,   \"Secrets collected: %d of %d\")\nGS_DEFINE(console/cmd/give/secret_given,  \"Added secret %s\")\nGS_DEFINE(console/cmd/give/secret_taken,  \"Removed secret %s\")\nGS_DEFINE(console/cmd/give/invalid_secret,     \"Invalid secret: %s (valid secrets: %s)\")\n\nGS_DEFINE(console/cmd/immune/on,  \"Lara is now impervious to damage\")\nGS_DEFINE(console/cmd/immune/off, \"Lara is now vulnerable\")\n\nGS_DEFINE(console/cmd/inf_sprint/on,  \"Lara can now sprint forever\")\nGS_DEFINE(console/cmd/inf_sprint/off, \"Lara can no longer sprint forever\")\n\nGS_DEFINE(console/cmd/spawn/success,      \"Requested object spawned near Lara\")\nGS_DEFINE(console/cmd/spawn/fail, \"Failed to spawn requested object\")\n\nGS_DEFINE(console/cmd/trigger/triggered,          \"Triggered item(s): %s\")\nGS_DEFINE(console/cmd/trigger/untriggered,        \"Untriggered item(s): %s\")\nGS_DEFINE(console/cmd/trigger/invalid_item,  \"Invalid item: %s\")\nGS_DEFINE(console/cmd/trigger/no_match,      \"Unknown target: %s\")\nGS_DEFINE(console/cmd/trigger/not_found,     \"No matching items found for: %s\")\n\nGS_DEFINE(console/cmd/winston/spawned,      \"Summoned Winston near Lara\")\nGS_DEFINE(console/cmd/winston/spawn_failed, \"Failed to summon Winston\")\nGS_DEFINE(console/cmd/winston/teleported,   \"Summoned Winston near Lara\")\nGS_DEFINE(console/cmd/winston/dead,         \"Your butler is dead. You monster!\")\n\nGS_DEFINE(console/cmd/play_music/track, \"Playing music track %d\")\nGS_DEFINE(console/cmd/play_music/stopped, \"Music stopped\")\nGS_DEFINE(console/cmd/play_music/invalid_track, \"Invalid music track\")\n\nGS_DEFINE(console/cmd/weather/set, \"Weather set to %s\")\nGS_DEFINE(console/cmd/weather/invalid, \"Invalid weather: %s (valid: %s)\")\n\nGS_DEFINE(console/cmd/help/list,      \"Available commands:\")\nGS_DEFINE(console/cmd/braid/help,         \"Toggles Lara's braid.\")\nGS_DEFINE(console/cmd/cheats/help,        \"Toggles in-game cheats on or off.\")\nGS_DEFINE(console/cmd/clear/help,         \"Clears visible console logs.\")\nGS_DEFINE(console/cmd/debug/help,         \"Toggles visual debug information.\")\nGS_DEFINE(console/cmd/drain/help,         \"Dries the current room, removing the water.\")\nGS_DEFINE(console/cmd/end_level/help,     \"Ends the current level.\")\nGS_DEFINE(console/cmd/exit/help,          \"Exits the game.\")\nGS_DEFINE(console/cmd/flipmap/help,       \"Toggles the flip map.\")\nGS_DEFINE(console/cmd/flood/help,         \"Submerges the current room with water.\")\nGS_DEFINE(console/cmd/fly/help,           \"Toggles the fly-mode cheat.\")\nGS_DEFINE(console/cmd/fps/help,           \"Changes the FPS value.\")\nGS_DEFINE(console/cmd/give/help,          \"Adds a given item to Lara's inventory.\")\nGS_DEFINE(console/cmd/give_secret/help,   \"Lists Lara's secrets, or takes/gives a secret by number.\")\nGS_DEFINE(console/cmd/heal/help,          \"Heals Lara back to full health.\")\nGS_DEFINE(console/cmd/help/help,          \"Shows help for all commands or detailed help for one.\")\nGS_DEFINE(console/cmd/hp/help,            \"Sets Lara's health to the specified value.\")\nGS_DEFINE(console/cmd/immune/help,        \"Toggles invulnerability. (Lara can still be killed in some circumstances.)\")\nGS_DEFINE(console/cmd/inf_sprint/help,    \"Toggles infinite sprint.\")\nGS_DEFINE(console/cmd/kill/help,          \"Kills nearby enemies.\")\nGS_DEFINE(console/cmd/lighting/help,      \"Toggles lighting system.\")\nGS_DEFINE(console/cmd/load/help,          \"Loads game from the given save slot or from a quick save.\")\nGS_DEFINE(console/cmd/lua/help,           \"Executes the given Lua code string.\")\nGS_DEFINE(console/cmd/mod/help,           \"Switches to the specified mod and restarts the game.\")\nGS_DEFINE(console/cmd/music/help,         \"Plays a music track with the given id.\")\nGS_DEFINE(console/cmd/play_cutscene/help, \"Plays a cutscene with the given number.\")\nGS_DEFINE(console/cmd/play_demo/help,     \"Plays a demo with the given number.\")\nGS_DEFINE(console/cmd/play_gym/help,      \"Plays the Gym level.\")\nGS_DEFINE(console/cmd/play_level/help,    \"Plays a level with the given name or number.\")\nGS_DEFINE(console/cmd/pos/help,           \"Shows Lara's position.\")\nGS_DEFINE(console/cmd/save/help,          \"Saves game to the given save slot or to the next quick save slot.\")\nGS_DEFINE(console/cmd/screenshot/help,    \"Saves a screenshot to disk at optional location.\")\nGS_DEFINE(console/cmd/set/help,           \"Displays or updates the given configuration setting.\")\nGS_DEFINE(console/cmd/sfx/help,           \"Plays a sound effect with the given id.\")\nGS_DEFINE(console/cmd/speed/help,         \"Changes the game's speed.\")\nGS_DEFINE(console/cmd/strings/help,       \"Reloads the current language files from disk.\")\nGS_DEFINE(console/cmd/textures/help,      \"Toggles textures.\")\nGS_DEFINE(console/cmd/title/help,         \"Returns to the title screen.\")\nGS_DEFINE(console/cmd/tp/help,            \"Teleports Lara to a given position or room number.\")\nGS_DEFINE(console/cmd/trigger/help,       \"Triggers or untriggers an item by id, item name, or object name.\")\nGS_DEFINE(console/cmd/vsync/help,         \"Toggles vertical sync.\")\nGS_DEFINE(console/cmd/weather/help,       \"Changes the current weather type.\")\nGS_DEFINE(console/cmd/wireframe/help,     \"Toggles wireframe rendering.\")\n\nGS_DEFINE(general/settings/common/all_hidden_disclaimer,    \"Settings are disabled for this level set.\")\nGS_DEFINE(general/settings/common/chroma,                   \"Chroma\")\nGS_DEFINE(general/settings/common/edit_value,               \"Edit value\")\nGS_DEFINE(general/settings/common/frozen_option_disclaimer, \"This setting is enforced by the level builder and cannot be changed.\")\nGS_DEFINE(general/settings/common/hue,                      \"Hue\")\nGS_DEFINE(general/settings/common/lightness,                \"Lightness\")\nGS_DEFINE(general/settings/common/restore_default,          \"Restore default\")\nGS_DEFINE(general/settings/common/toggle_help,              \"Toggle help\")\n\nGS_DEFINE(general/settings/sound/title,                     \"Sound Options\")\nGS_DEFINE(general/settings/sound/tabs/volume,                \"Volume\")\nGS_DEFINE(general/settings/sound/tabs/misc,                  \"Misc\")\n\nGS_DEFINE(general/settings/gameplay/title,                  \"Gameplay Options\")\nGS_DEFINE(general/settings/gameplay/tabs/general,            \"General\")\nGS_DEFINE(general/settings/gameplay/tabs/controls,           \"Controls\")\nGS_DEFINE(general/settings/gameplay/tabs/mods,               \"Mods\")\nGS_DEFINE(general/settings/gameplay/tabs/fixes,              \"Fixes\")\nGS_DEFINE(general/settings/gameplay/tabs/presets,            \"Presets\")\n\nGS_DEFINE(general/config_presets/confirm_description,       \"The following settings will be changed:\")\nGS_DEFINE(general/config_presets/confirm_restart_note,      \"Note: some settings may require a game restart to take effect.\")\nGS_DEFINE(general/config_presets/applied,                   \"Preset applied.\")\nGS_DEFINE(general/config_presets/no_changes,                \"No changes to apply.\")\nGS_DEFINE(general/config_presets/title_fmt,                 \"Apply preset %s?\")\nGS_DEFINE(general/config_presets/empty,                     \"No presets found.\")\n\nGS_DEFINE(dynamic/config_presets/tr1_ps1,                    \"TR1 PS1\")\nGS_DEFINE(dynamic/config_presets/tr1_pc,                     \"TR1 PC\")\nGS_DEFINE(dynamic/config_presets/tr2_ps1,                    \"TR2 PS1\")\nGS_DEFINE(dynamic/config_presets/tr2_pc,                     \"TR2 PC\")\nGS_DEFINE(dynamic/config_presets/tr3_ps1,                    \"TR3 PS1\")\nGS_DEFINE(dynamic/config_presets/tr3_pc,                     \"TR3 PC\")\n\nGS_DEFINE(dynamic/mods/tr1/title,         \"Tomb Raider I\")\nGS_DEFINE(dynamic/mods/tr1-ub/title,      \"Unfinished Business\")\nGS_DEFINE(dynamic/mods/tr1-demo-pc/title, \"Tomb Raider I Demo\")\nGS_DEFINE(dynamic/mods/tr2/title,         \"Tomb Raider II\")\nGS_DEFINE(dynamic/mods/tr2-gm/title,      \"The Golden Mask\")\nGS_DEFINE(dynamic/mods/tr3/title,         \"Tomb Raider III\")\nGS_DEFINE(dynamic/mods/tr3-la/title,      \"The Lost Artifact\")\n\nGS_DEFINE(general/settings/graphic_settings/title,                   \"Graphic Options\")\nGS_DEFINE(general/settings/graphic_settings/tabs/visuals,             \"Visuals\")\nGS_DEFINE(general/settings/graphic_settings/tabs/ui,                  \"UI\")\nGS_DEFINE(general/settings/graphic_settings/tabs/stats,              \"Stats\")\nGS_DEFINE(general/settings/graphic_settings/tabs/bars,             \"Bars\")\nGS_DEFINE(general/settings/graphic_settings/tabs/rendering,           \"Rendering\")\n\nGS_DEFINE(general/stats/level,              \"Level\")\nGS_DEFINE(general/stats/time_taken,         \"Time Taken\")\nGS_DEFINE(general/stats/ammo,               \"Ammo Hits/Used\")\nGS_DEFINE(general/stats/ammo_used,          \"Ammo Used\")\nGS_DEFINE(general/stats/ammo_hits,          \"Hits\")\nGS_DEFINE(general/stats/kills,              \"Kills\")\nGS_DEFINE(general/stats/crystals,           \"Crystals\")\nGS_DEFINE(general/stats/pickups,            \"Pickups\")\nGS_DEFINE(general/stats/deaths,             \"Deaths\")\nGS_DEFINE(general/stats/medipacks_used,     \"Health Packs Used\")\nGS_DEFINE(general/stats/distance_travelled, \"Distance Traveled\")\nGS_DEFINE(general/stats/secrets,            \"Secrets Found\")\nGS_DEFINE(general/stats/none,               \"None\")\n\nGS_DEFINE(general/globe_select/area_1, \"Area 1\")\nGS_DEFINE(general/globe_select/area_2, \"Area 2\")\nGS_DEFINE(general/globe_select/area_3, \"Area 3\")\nGS_DEFINE(general/globe_select/area_4, \"Area 4\")\nGS_DEFINE(general/globe_select/area_5, \"Area 5\")\nGS_DEFINE(general/globe_select/area_6, \"Area 6\")\n"
  },
  {
    "path": "src/trx/game/game_strings/entries.h",
    "content": "#pragma once\n\n// Define a game string mapping: associates an ID with a text value.\n// @param id    Compile-time identifier for the string.\n// @param value Text to map to the identifier.\n#define GS_DEFINE(id, value) GameString_Define(#id, value);\n\n// Retrieve a game string by a slash-separated path literal.\n#define GS(path) GameString_Get(path)\n\n// Return a slash-separated path literal as-is.\n#define GS_ID(path) (path)\n\n// Retrieve a stable slot pointer for a slash-separated path literal.\n#define GS_PTR(path) GameString_GetPtr(path)\n\ntypedef const char *GAME_STRING_ID;\n\n// Initialize the GameString subsystem.\n// Must be called before any GameString_* functions.\nvoid GameString_Init(void);\n\n// Shutdown the GameString subsystem and free all associated resources.\nvoid GameString_Shutdown(void);\n\n// Define a new game string mapping.\n// @param key   Identifier for the string (e.g. \"GAME_OVER\").\n// @param value Text to associate with the identifier.\nvoid GameString_Define(const char *key, const char *value);\n\n// Check whether a game string identifier has been defined.\n// @param key   Identifier to test.\n// @return      true if the identifier is known, false otherwise.\nbool GameString_IsKnown(const char *key);\n\n// Retrieve the game string associated with an identifier.\n// @param key   Identifier for the string.\n// @return      Text mapped to the identifier, or nullptr if unknown.\nconst char *GameString_Get(const char *key);\n\n// Clear all existing game string mappings.\n// Slots remain allocated but values reset.\nvoid GameString_Clear(void);\n\n// Like GameString_Get(), but returns a stable slot pointer that always\n// reflects the current string value for this identifier (updates automatically\n// on reload).\n// @param key   Identifier for the string.\n// @return      Address of the internal string pointer, or nullptr if unknown.\nconst char *const *GameString_GetPtr(const char *key);\n"
  },
  {
    "path": "src/trx/game/game_strings/manager.c",
    "content": "#include <trx/game/game_strings/manager.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/json.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/game_strings/table.h>\n\n#include <string.h>\n\ntypedef struct {\n    char *path;\n    bool load_levels;\n} M_FILE_ENTRY;\n\ntypedef struct {\n    char *lang;\n    VECTOR *files;\n    char *display_name;\n    char *extends;\n} M_LANG_ENTRY;\n\nstatic VECTOR *m_SourceFiles = nullptr;\nstatic VECTOR *m_LangEntries = nullptr;\nstatic EVENT_MANAGER *m_EventManager = nullptr;\n\nstatic void M_ClearFileEntries(VECTOR *const files)\n{\n    for (int32_t i = 0; i < files->count; i++) {\n        const M_FILE_ENTRY *const file_entry = Vector_Get(files, i);\n        Memory_Free(file_entry->path);\n    }\n    Vector_Free(files);\n}\n\nstatic void M_ClearLanguageEntries(void)\n{\n    if (m_LangEntries != nullptr) {\n        for (int32_t i = 0; i < m_LangEntries->count; i++) {\n            const M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i);\n            Memory_Free(lang_entry->lang);\n            Memory_Free(lang_entry->display_name);\n            Memory_Free(lang_entry->extends);\n            M_ClearFileEntries(lang_entry->files);\n        }\n        Vector_Free(m_LangEntries);\n        m_LangEntries = nullptr;\n    }\n}\n\nstatic void M_ClearManager(void)\n{\n    M_ClearLanguageEntries();\n    if (m_SourceFiles != nullptr) {\n        M_ClearFileEntries(m_SourceFiles);\n        m_SourceFiles = nullptr;\n    }\n}\n\nstatic M_LANG_ENTRY *M_FindLangEntry(const char *const lang)\n{\n    for (int32_t i = 0; i < m_LangEntries->count; i++) {\n        M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i);\n        if (String_Equivalent(lang_entry->lang, lang)) {\n            return lang_entry;\n        }\n    }\n    return nullptr;\n}\n\n// Create a new entry with the given file for the given language.\nstatic void M_AddPathForLang(\n    const char *const lang, char *const path, const bool load_levels)\n{\n    const M_LANG_ENTRY *lang_entry = M_FindLangEntry(lang);\n    if (lang_entry == nullptr) {\n        M_LANG_ENTRY new_ent = {\n            .lang = Memory_DupStr(lang),\n            .files = Vector_Create(sizeof(M_FILE_ENTRY)),\n        };\n        Vector_Add(m_LangEntries, &new_ent);\n        lang_entry = M_FindLangEntry(lang);\n    }\n    const M_FILE_ENTRY file_entry = {\n        .path = path,\n        .load_levels = load_levels,\n    };\n    Vector_Add(lang_entry->files, &file_entry);\n}\n\nstatic void M_LoadLanguageNames(void)\n{\n    if (m_LangEntries == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < m_LangEntries->count; i++) {\n        M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i);\n        Memory_FreePointer(&lang_entry->display_name);\n        Memory_FreePointer(&lang_entry->extends);\n        if (lang_entry->files->count <= 0) {\n            continue;\n        }\n        const M_FILE_ENTRY *const file_entry = Vector_Get(lang_entry->files, 0);\n        char *data = nullptr;\n        size_t size = 0;\n        if (!File_Load(file_entry->path, &data, &size) || data == nullptr) {\n            continue;\n        }\n        JSON_PARSE_RESULT pr = { 0 };\n        JSON_VALUE *const root = JSON_ParseEx(\n            data, size, JSON_PARSE_FLAGS_ALLOW_JSON5, nullptr, nullptr, &pr);\n        if (root != nullptr) {\n            JSON_OBJECT *const obj = JSON_ValueAsObject(root);\n            const char *const name =\n                JSON_ObjectGetString(obj, \"language_name\", JSON_INVALID_STRING);\n            if (name != JSON_INVALID_STRING) {\n                lang_entry->display_name = Memory_DupStr(name);\n            }\n            const char *const ext =\n                JSON_ObjectGetString(obj, \"extends\", JSON_INVALID_STRING);\n            if (ext != JSON_INVALID_STRING) {\n                lang_entry->extends = Memory_DupStr(ext);\n            }\n            JSON_ValueFree(root);\n        } else {\n            LOG_WARNING(\n                \"failed to parse 'language_name' in %s: %s\", file_entry->path,\n                JSON_GetErrorDescription(pr.error));\n        }\n        Memory_Free(data);\n    }\n}\n\nstatic void M_ReorderLanguages(void)\n{\n    if (m_LangEntries->count > 1) {\n        VECTOR *ordered = Vector_Create(sizeof(M_LANG_ENTRY));\n        const M_LANG_ENTRY *en_orig = M_FindLangEntry(\"en\");\n        if (en_orig != nullptr) {\n            M_LANG_ENTRY en_entry = *en_orig;\n            Vector_Add(ordered, &en_entry);\n        }\n        VECTOR *others = Vector_Create(sizeof(M_LANG_ENTRY));\n        for (int32_t i = 0; i < m_LangEntries->count; ++i) {\n            const M_LANG_ENTRY *entry = Vector_Get(m_LangEntries, i);\n            if (en_orig != nullptr && String_Equivalent(entry->lang, \"en\")) {\n                continue;\n            }\n            M_LANG_ENTRY e = *entry;\n            Vector_Add(others, &e);\n        }\n        for (int32_t i = 0; i + 1 < others->count; ++i) {\n            for (int32_t j = 0; j + 1 < others->count - i; ++j) {\n                M_LANG_ENTRY *a = Vector_Get(others, j);\n                M_LANG_ENTRY *b = Vector_Get(others, j + 1);\n                const char *an = a->display_name ? a->display_name : \"\";\n                const char *bn = b->display_name ? b->display_name : \"\";\n                if (strcmp(an, bn) > 0) {\n                    Vector_Swap(others, j, j + 1);\n                }\n            }\n        }\n        for (int32_t i = 0; i < others->count; ++i) {\n            M_LANG_ENTRY *entry = Vector_Get(others, i);\n            Vector_Add(ordered, entry);\n        }\n        Vector_Free(others);\n        Vector_Free(m_LangEntries);\n        m_LangEntries = ordered;\n    }\n}\n\nvoid GameStringManager_Init(void)\n{\n    m_EventManager = EventManager_Create();\n    M_ClearManager();\n    m_SourceFiles = Vector_Create(sizeof(M_FILE_ENTRY));\n}\n\nvoid GameStringManager_Shutdown(void)\n{\n    if (m_EventManager != nullptr) {\n        EventManager_Free(m_EventManager);\n        m_EventManager = nullptr;\n    }\n    GameStringTable_Shutdown();\n    M_ClearManager();\n}\n\n// Clear all previously set source strings files.\n// Must be called before GameStringManager_AddSourceFile.\nvoid GameStringManager_ClearSourceFiles(void)\n{\n    M_ClearManager();\n    m_SourceFiles = Vector_Create(sizeof(M_FILE_ENTRY));\n}\n\n// Add a source strings file for language discovery and loading.\n// base_path: path to a base strings JSON5 file.\n// load_levels: true to load level entries from this source; false otherwise.\nvoid GameStringManager_AddSourceFile(\n    const char *const base_path, const bool load_levels)\n{\n    ASSERT(m_SourceFiles != nullptr);\n    if (base_path == nullptr) {\n        return;\n    }\n    const M_FILE_ENTRY fe = {\n        .path = Memory_DupStr(base_path),\n        .load_levels = load_levels,\n    };\n    Vector_Add(m_SourceFiles, &fe);\n}\n\nvoid GameStringManager_DiscoverLanguages(void)\n{\n    if (m_SourceFiles == nullptr) {\n        return;\n    }\n    M_ClearLanguageEntries();\n    m_LangEntries = Vector_Create(sizeof(M_LANG_ENTRY));\n\n    for (int32_t i = 0; i < m_SourceFiles->count; ++i) {\n        const M_FILE_ENTRY *src = Vector_Get(m_SourceFiles, i);\n        char *dir = File_GetParentDirectory(src->path);\n        const char *base =\n            MAX(strrchr(src->path, '\\\\'), strrchr(src->path, '/'));\n        base = (base != nullptr) ? base + 1 : src->path;\n        const char *ext = strrchr(base, '.');\n        if (dir == nullptr || ext == nullptr) {\n            Memory_Free(dir);\n            continue;\n        }\n        size_t stem_len = (size_t)(ext - base);\n        size_t ext_len = strlen(ext);\n\n        void *dh = File_OpenDirectory(dir);\n        if (dh != nullptr) {\n            const char *ent;\n            while ((ent = File_ReadDirectory(dh))) {\n                if (ent[0] == '.') {\n                    continue;\n                }\n                size_t name_len = strlen(ent);\n                if (name_len < stem_len + ext_len) {\n                    continue;\n                }\n                if (strncmp(ent, base, stem_len) != 0\n                    || strcmp(ent + name_len - ext_len, ext) != 0) {\n                    continue;\n                }\n                char *code;\n                if (name_len == stem_len + ext_len) {\n                    code = Memory_DupStr(\"en\");\n                } else if (ent[stem_len] == '-') {\n                    size_t code_len = name_len - stem_len - ext_len - 1;\n                    code = String_Format(\n                        \"%.*s\", (int32_t)code_len, ent + stem_len + 1);\n                } else {\n                    continue;\n                }\n                char *path = String_Format(\"%s/%s\", dir, ent);\n                M_AddPathForLang(code, path, src->load_levels);\n                Memory_Free(code);\n            }\n            File_CloseDirectory(dh);\n        }\n        Memory_Free(dir);\n    }\n\n    M_LoadLanguageNames();\n    M_ReorderLanguages();\n}\n\nVECTOR *GameStringManager_GetAvailableLanguages(void)\n{\n    if (m_LangEntries == nullptr) {\n        return nullptr;\n    }\n    VECTOR *const out = Vector_Create(sizeof(char *));\n    for (int32_t i = 0; i < m_LangEntries->count; i++) {\n        const M_LANG_ENTRY *const lang_entry = Vector_Get(m_LangEntries, i);\n        char *const c = Memory_DupStr(lang_entry->lang);\n        Vector_Add(out, &c);\n    }\n    return out;\n}\n\n// Recursive load of language chain (handles 'extends' fallback between\n// dialects)\nstatic bool M_ReloadLangRec(const char *const lang, VECTOR *const visited)\n{\n    for (int32_t i = 0; i < visited->count; i++) {\n        const char *const prev = *(char **)Vector_Get(visited, i);\n        if (String_Equivalent(prev, lang)) {\n            LOG_WARNING(\"cyclic language extends detected: %s\", lang);\n            return false;\n        }\n    }\n    Vector_Add(visited, &lang);\n    M_LANG_ENTRY *const entry = M_FindLangEntry(lang);\n    if (entry == nullptr) {\n        return false;\n    }\n    if (entry->extends) {\n        if (!M_ReloadLangRec(entry->extends, visited)) {\n            return false;\n        }\n    }\n    for (int32_t i = 0; i < entry->files->count; i++) {\n        const M_FILE_ENTRY *const fe = Vector_Get(entry->files, i);\n        if (!GameStringTable_Load(fe->path, fe->load_levels)) {\n            return false;\n        }\n    }\n    return true;\n}\n\nbool GameStringManager_ReloadLanguage(const char *lang)\n{\n    const M_LANG_ENTRY *const base_entry =\n        m_LangEntries ? M_FindLangEntry(lang) : nullptr;\n    if (base_entry == nullptr) {\n        LOG_WARNING(\"language '%s' not found, defaulting to base\", lang);\n        lang = \"en\";\n    }\n    GameStringTable_Shutdown();\n    GameStringTable_Init();\n    VECTOR *const visited = Vector_Create(sizeof(char *));\n    const bool success = M_ReloadLangRec(lang, visited);\n    Vector_Free(visited);\n    if (success) {\n        GameStringTable_Apply(GF_GetCurrentLevel());\n        if (m_EventManager != nullptr) {\n            const EVENT event = {\n                .name = \"reload_language\",\n                .sender = nullptr,\n                .data = (void *)lang,\n            };\n            EventManager_Fire(m_EventManager, &event);\n        }\n    }\n    return success;\n}\n\nconst char *GameStringManager_GetLanguageName(const char *const code)\n{\n    if (m_LangEntries == nullptr || code == nullptr) {\n        return nullptr;\n    }\n    const M_LANG_ENTRY *const ent = M_FindLangEntry(code);\n    return ent != nullptr ? ent->display_name : nullptr;\n}\n\nint32_t GameStringManager_SubscribeReload(\n    const EVENT_LISTENER listener, void *const user_data)\n{\n    ASSERT(m_EventManager != nullptr);\n    return EventManager_Subscribe(\n        m_EventManager, \"reload_language\", nullptr, listener, user_data);\n}\n\nvoid GameStringManager_UnsubscribeReload(const int32_t listener_id)\n{\n    if (m_EventManager != nullptr) {\n        EventManager_Unsubscribe(m_EventManager, listener_id);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/game_strings/manager.h",
    "content": "// Manages autodiscovery and runtime reload of string bundles per language and\n// mod/OG fallback.\n#pragma once\n\n#include <trx/core/event_manager.h>\n#include <trx/core/vector.h>\n\n// Initialize the string bundle manager.\nvoid GameStringManager_Init(void);\n\n// Shutdown the string bundle manager.\nvoid GameStringManager_Shutdown(void);\n\n// Clear all previously set source strings files.\n// Must be called before GameStringManager_AddSourceFile.\nvoid GameStringManager_ClearSourceFiles(void);\n\n// Add a source strings file for language discovery and loading.\n// base_path: path to a base strings JSON5 file (e.g. cfg/common_strings.json5).\n// load_levels: true to load level names from this source; false otherwise.\nvoid GameStringManager_AddSourceFile(const char *base_path, bool load_levels);\n\n// Discover all available languages from the added source files.\n// Must be called after AddSourceFile calls and before ReloadLanguage.\nvoid GameStringManager_DiscoverLanguages(void);\n\n// Returns a vector of char* of available language codes discovered.\n// Caller owns the returned VECTOR and must free it via Vector_Free and free\n// each string.\nVECTOR *GameStringManager_GetAvailableLanguages(void);\n\n// Get the display name for a language code as defined by \"language_name\"\n// in the strings JSON file. Returns nullptr if unavailable.\n// The returned pointer is owned by the manager; do not free.\nconst char *GameStringManager_GetLanguageName(const char *code);\n\n// Reload all game strings for the given language code.\n// Clears any previously loaded strings, loads OG fallback and mod overrides.\n// lang: language code (e.g. \"en\", \"fr\"). Must be one returned by\n// GetAvailableLanguages.\nbool GameStringManager_ReloadLanguage(const char *lang);\n\n// Subscribe to be notified when the game strings language is reloaded.\n// The listener will receive an EVENT with name \"reload_language\",\n// and .data pointing to the language code (const char *).\nint32_t GameStringManager_SubscribeReload(\n    EVENT_LISTENER listener, void *user_data);\n\n// Unsubscribe from language reload events.\nvoid GameStringManager_UnsubscribeReload(int32_t listener_id);\n"
  },
  {
    "path": "src/trx/game/game_strings/table/common.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/game_strings/table.h>\n#include <trx/game/game_strings/table/priv.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/shell.h>\n\n#include <string.h>\n\ntypedef void (*M_LOAD_STRING_FUNC)(const char *, const char *);\n\nstatic VECTOR *m_GST_Layers = nullptr;\n\nstatic void M_Apply(const GS_TABLE *const table)\n{\n    for (const GS_GAME_STRING_ENTRY *cur = table->game_strings;\n         cur != nullptr && cur->key != nullptr; cur++) {\n        if (cur->value == nullptr) {\n            LOG_ERROR(\"Invalid game string value: %s\", cur->key);\n        } else {\n            GameString_Define(cur->key, cur->value);\n        }\n    }\n\n    for (const GS_OBJECT_ENTRY *cur = table->objects;\n         cur != nullptr && cur->key != nullptr; cur++) {\n        const OBJECT_ID obj_id = Object_IdFromKey(cur->key);\n        if (obj_id == NO_OBJECT) {\n            LOG_ERROR(\"Invalid object id: %s\", cur->key);\n        } else if (cur->names == nullptr) {\n            LOG_ERROR(\"Invalid object name(s): %s\", cur->key);\n        } else {\n            Object_ClearNames(obj_id);\n            for (const char *const *name = cur->names; *name != nullptr;\n                 name++) {\n                Object_AddName(obj_id, *name);\n            }\n            Object_SetDescription(obj_id, cur->description);\n        }\n    }\n}\n\nstatic void M_ApplyLevelTitles(\n    const GS_FILE *const gs_file, const GF_LEVEL_TABLE_TYPE level_table_type)\n{\n    const GF_LEVEL_TABLE *const level_table =\n        GF_GetLevelTable(level_table_type);\n    const GS_LEVEL_TABLE *const gs_level_table =\n        &gs_file->level_tables[level_table_type];\n    if (gs_level_table->count == 0) {\n        return;\n    }\n    ASSERT(gs_level_table->count == level_table->count);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        if (gs_level_table->entries[i].title != nullptr) {\n            GF_SetLevelTitle(\n                &level_table->levels[i], gs_level_table->entries[i].title);\n        }\n    }\n}\n\nstatic void M_ApplyLayer(\n    const GF_LEVEL *const level, const GS_FILE *const gs_file)\n{\n    LOG_DEBUG(\"applying layer: %s\", gs_file->path);\n    M_Apply(&gs_file->global);\n\n    for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) {\n        M_ApplyLevelTitles(gs_file, i);\n    }\n\n    if (level != nullptr) {\n        const GS_LEVEL_TABLE *gs_level_table = nullptr;\n        switch (level->type) {\n        case GFL_TITLE:\n            gs_level_table = nullptr;\n            break;\n        default: {\n            const GF_LEVEL_TABLE_TYPE level_table_type =\n                GF_GetLevelTableType(level->type);\n            gs_level_table = &gs_file->level_tables[level_table_type];\n        }\n        }\n\n        if (gs_level_table != nullptr && gs_level_table->count != 0) {\n            ASSERT(level->num >= 0);\n            ASSERT(level->num < gs_level_table->count);\n            M_Apply(&gs_level_table->entries[level->num].table);\n        }\n    }\n}\n\nvoid GameStringTable_Apply(const GF_LEVEL *const level)\n{\n    Object_ResetAllNames();\n    ASSERT(m_GST_Layers != nullptr);\n    for (int32_t i = 0; i < m_GST_Layers->count; i++) {\n        const GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i);\n        M_ApplyLayer(level, gs_file);\n    }\n}\n\nvoid GameStringTable_Init(void)\n{\n    m_GST_Layers = Vector_Create(sizeof(GS_FILE *));\n}\n\nvoid GameStringTable_Shutdown(void)\n{\n    if (m_GST_Layers != nullptr) {\n        for (int32_t i = 0; i < m_GST_Layers->count; i++) {\n            GS_FILE *const gs_file = *(GS_FILE **)Vector_Get(m_GST_Layers, i);\n            GS_File_Free(gs_file);\n        }\n        Vector_Free(m_GST_Layers);\n        m_GST_Layers = nullptr;\n    }\n}\n\nbool GameStringTable_Load(const char *const path, const bool load_levels)\n{\n    GS_FILE *gs_file = GS_File_CreateFromPath(path, load_levels);\n    if (gs_file == nullptr) {\n        return false;\n    }\n    ASSERT(m_GST_Layers != nullptr);\n    Vector_Add(m_GST_Layers, &gs_file);\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/game_strings/table/priv.c",
    "content": "#include <trx/game/game_strings/table/priv.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n\nstatic void M_FreeTable(GS_TABLE *const gs_table)\n{\n    if (gs_table == nullptr) {\n        return;\n    }\n    if (gs_table->objects != nullptr) {\n        GS_OBJECT_ENTRY *cur = gs_table->objects;\n        while (cur->key != nullptr) {\n            Memory_FreePointer(&cur->key);\n            if (cur->names != nullptr) {\n                for (size_t j = 0; cur->names[j] != nullptr; j++) {\n                    Memory_FreePointer((void **)&cur->names[j]);\n                }\n                Memory_Free(cur->names);\n                cur->names = nullptr;\n            }\n            Memory_FreePointer(&cur->description);\n            cur++;\n        }\n        Memory_Free(gs_table->objects);\n        gs_table->objects = nullptr;\n    }\n\n    if (gs_table->game_strings != nullptr) {\n        GS_GAME_STRING_ENTRY *cur = gs_table->game_strings;\n        while (cur->key != nullptr) {\n            Memory_FreePointer(&cur->key);\n            Memory_FreePointer(&cur->value);\n            cur++;\n        }\n        Memory_Free(gs_table->game_strings);\n        gs_table->game_strings = nullptr;\n    }\n}\n\nstatic void M_FreeLevelsTable(GS_LEVEL_TABLE *const levels)\n{\n    if (levels->entries != nullptr) {\n        for (int32_t i = 0; i < levels->count; i++) {\n            Memory_FreePointer(&levels->entries[i].title);\n            M_FreeTable(&levels->entries[i].table);\n        }\n        Memory_FreePointer(&levels->entries);\n    }\n    levels->count = 0;\n}\n\nvoid GS_File_Free(GS_FILE *const gs_file)\n{\n    if (gs_file == nullptr) {\n        return;\n    }\n    M_FreeTable(&gs_file->global);\n    for (int32_t i = 0; i < GFLT_NUMBER_OF; i++) {\n        M_FreeLevelsTable(&gs_file->level_tables[i]);\n    }\n    Memory_FreePointer(&gs_file->path);\n    Memory_Free(gs_file);\n}\n"
  },
  {
    "path": "src/trx/game/game_strings/table/priv.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n#include <trx/game/game_flow/enum.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    const char *key;\n    const char **names;\n    const char *description;\n} GS_OBJECT_ENTRY;\n\ntypedef struct {\n    const char *key;\n    const char *value;\n} GS_GAME_STRING_ENTRY;\n\ntypedef struct {\n    GS_OBJECT_ENTRY *objects;\n    GS_GAME_STRING_ENTRY *game_strings;\n} GS_TABLE;\n\ntypedef struct {\n    const char *title;\n    GS_TABLE table;\n} GS_LEVEL;\n\ntypedef struct {\n    int32_t count;\n    GS_LEVEL *entries;\n} GS_LEVEL_TABLE;\n\ntypedef struct {\n    char *path;\n    GS_TABLE global;\n    GS_LEVEL_TABLE level_tables[GFLT_NUMBER_OF];\n} GS_FILE;\n\nvoid GS_Table_Free(GS_TABLE *gs_table);\n\nGS_FILE *GS_File_CreateFromPath(const char *path, bool load_levels);\nvoid GS_File_Free(GS_FILE *gs_file);\n"
  },
  {
    "path": "src/trx/game/game_strings/table/reader.c",
    "content": "#include <trx/core/json/util/file.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/table.h>\n#include <trx/game/game_strings/table/priv.h>\n#include <trx/game/shell.h>\n\n#include <string.h>\n\ntypedef struct {\n    JSON_OBJECT *obj;\n    char *prefix;\n} M_STACK_ITEM;\n\nstatic void M_AppendGameStringEntry(\n    GS_TABLE *const out_table, size_t *const io_count,\n    size_t *const io_capacity, char *const key, const char *const value)\n{\n    if (*io_count + 2 > *io_capacity) {\n        *io_capacity *= 2;\n        out_table->game_strings = Memory_Realloc(\n            out_table->game_strings,\n            sizeof(GS_GAME_STRING_ENTRY) * (*io_capacity));\n    }\n    out_table->game_strings[(*io_count)++] = (GS_GAME_STRING_ENTRY) {\n        .key = key,\n        .value = Memory_DupStr(value),\n    };\n}\n\nstatic void M_LoadNestedGameStrings(\n    JSON_OBJECT *const root_obj, const char *const root_key,\n    GS_TABLE *const out_table, size_t *const io_count,\n    size_t *const io_capacity)\n{\n    JSON_OBJECT *const settings_obj = JSON_ObjectGetObject(root_obj, root_key);\n    if (settings_obj == nullptr) {\n        return;\n    }\n\n    VECTOR *const stack = Vector_Create(sizeof(M_STACK_ITEM));\n    const M_STACK_ITEM root_item = {\n        .obj = settings_obj,\n        .prefix = Memory_DupStr(root_key),\n    };\n    Vector_Add(stack, &root_item);\n\n    while (stack->count > 0) {\n        const int32_t top_idx = stack->count - 1;\n        M_STACK_ITEM cur = *(M_STACK_ITEM *)Vector_Get(stack, top_idx);\n        Vector_RemoveAt(stack, top_idx);\n\n        for (JSON_OBJECT_ELEMENT *elem = cur.obj->start; elem != nullptr;\n             elem = elem->next) {\n            const char *const key = elem->name->string;\n            if (key == JSON_INVALID_STRING) {\n                LOG_WARNING(\"Invalid game string key\");\n                continue;\n            }\n\n            char *full_key = String_Format(\"%s/%s\", cur.prefix, key);\n            const char *const value =\n                JSON_ValueGetString(elem->value, JSON_INVALID_STRING);\n            if (value != JSON_INVALID_STRING) {\n                M_AppendGameStringEntry(\n                    out_table, io_count, io_capacity, full_key, value);\n                continue;\n            }\n\n            JSON_OBJECT *const child = JSON_ValueAsObject(elem->value);\n            if (child != nullptr) {\n                const M_STACK_ITEM child_item = {\n                    .obj = child,\n                    .prefix = full_key,\n                };\n                Vector_Add(stack, &child_item);\n                continue;\n            }\n\n            LOG_WARNING(\"Invalid game string entry '%s'\", full_key);\n            Memory_FreePointer(&full_key);\n        }\n\n        Memory_FreePointer(&cur.prefix);\n    }\n\n    Vector_Free(stack);\n}\n\nstatic void M_LoadTableFromJSON(\n    JSON_OBJECT *const root_obj, GS_TABLE *const out_table)\n{\n    // Load objects\n    JSON_OBJECT *const jobjs = JSON_ObjectGetObject(root_obj, \"objects\");\n    if (jobjs != nullptr) {\n        const size_t object_count = jobjs->length;\n        out_table->objects =\n            Memory_Alloc(sizeof(GS_OBJECT_ENTRY) * (object_count + 1));\n\n        JSON_OBJECT_ELEMENT *jobj_elem = jobjs->start;\n        for (size_t i = 0; i < object_count; i++, jobj_elem = jobj_elem->next) {\n            JSON_OBJECT *const jobj_obj = JSON_ValueAsObject(jobj_elem->value);\n\n            const char *const key = jobj_elem->name->string;\n            if (key == JSON_INVALID_STRING) {\n                LOG_WARNING(\n                    \"Invalid game string object entry %d: missing key.\", i);\n                continue;\n            }\n\n            const char *const single_name =\n                JSON_ObjectGetString(jobj_obj, \"name\", JSON_INVALID_STRING);\n            JSON_ARRAY *jnames_arr = JSON_ObjectGetArray(jobj_obj, \"name\");\n            if (jnames_arr == nullptr) {\n                jnames_arr = JSON_ObjectGetArray(jobj_obj, \"names\");\n            }\n\n            if (single_name == JSON_INVALID_STRING\n                && (jnames_arr == nullptr || jnames_arr->length == 0)) {\n                LOG_WARNING(\n                    \"Invalid game string object entry %s: missing name.\", key);\n                continue;\n            }\n\n            GS_OBJECT_ENTRY *const object_entry = &out_table->objects[i];\n            object_entry->key = Memory_DupStr(key);\n            if (jnames_arr != nullptr) {\n                object_entry->names = Memory_Alloc(\n                    sizeof(const char *) * (jnames_arr->length + 1));\n                JSON_ARRAY_ELEMENT *elem = jnames_arr->start;\n                size_t count = 0;\n                for (size_t j = 0; j < jnames_arr->length;\n                     j++, elem = elem->next) {\n                    const char *const name =\n                        JSON_ValueGetString(elem->value, JSON_INVALID_STRING);\n                    if (name != JSON_INVALID_STRING) {\n                        object_entry->names[count] = Memory_DupStr(name);\n                        count++;\n                    }\n                }\n                object_entry->names[count] = nullptr;\n            } else {\n                object_entry->names = Memory_Alloc(sizeof(const char *) * 2);\n                object_entry->names[0] = Memory_DupStr(single_name);\n                object_entry->names[1] = nullptr;\n            }\n\n            const char *const description = JSON_ObjectGetString(\n                jobj_obj, \"description\", JSON_INVALID_STRING);\n            object_entry->description = description != JSON_INVALID_STRING\n                ? Memory_DupStr(description)\n                : nullptr;\n        }\n    }\n\n    // Load localized string tables.\n    const char *const nested_sections[] = {\n        \"general\", \"console\", \"settings\", \"enums\", \"dynamic\",\n    };\n    size_t gs_count = 0;\n    size_t gs_capacity = 0;\n\n    for (size_t i = 0; i < ARRAY_SIZE(nested_sections); i++) {\n        if (JSON_ObjectGetObject(root_obj, nested_sections[i]) == nullptr) {\n            continue;\n        }\n        if (gs_capacity == 0) {\n            gs_capacity = 64;\n            out_table->game_strings =\n                Memory_Alloc(sizeof(GS_GAME_STRING_ENTRY) * gs_capacity);\n        }\n        M_LoadNestedGameStrings(\n            root_obj, nested_sections[i], out_table, &gs_count, &gs_capacity);\n    }\n\n    if (gs_capacity != 0) {\n        if (gs_count + 1 > gs_capacity) {\n            gs_capacity++;\n            out_table->game_strings = Memory_Realloc(\n                out_table->game_strings,\n                sizeof(GS_GAME_STRING_ENTRY) * gs_capacity);\n        }\n        out_table->game_strings[gs_count] =\n            (GS_GAME_STRING_ENTRY) { .key = nullptr, .value = nullptr };\n    }\n}\n\nstatic void M_LoadLevelsFromJSON(\n    JSON_OBJECT *const obj, GS_FILE *const gs_file, const char *const key,\n    const GF_LEVEL_TABLE_TYPE level_table_type)\n{\n    const GF_LEVEL_TABLE *const level_table =\n        GF_GetLevelTable(level_table_type);\n    GS_LEVEL_TABLE *const gs_level_table =\n        &gs_file->level_tables[level_table_type];\n    if (level_table->count == 0) {\n        return;\n    }\n\n    JSON_ARRAY *const jlvl_arr = JSON_ObjectGetArray(obj, key);\n    if (jlvl_arr == nullptr) {\n        return;\n    }\n\n    if (jlvl_arr->length != (size_t)level_table->count) {\n        Shell_ExitSystemFmt(\n            \"%s: '%s' length must match with the game flow level count (got: \"\n            \"%d, expected: %d)\",\n            gs_file->path, key, jlvl_arr->length, level_table->count);\n    }\n\n    gs_level_table->count = jlvl_arr->length;\n    gs_level_table->entries = Memory_Alloc(sizeof(GS_LEVEL) * jlvl_arr->length);\n\n    JSON_ARRAY_ELEMENT *jlvl_elem = jlvl_arr->start;\n    for (size_t i = 0; i < jlvl_arr->length; i++, jlvl_elem = jlvl_elem->next) {\n        GS_LEVEL *const level = &gs_level_table->entries[i];\n\n        JSON_OBJECT *const jlvl_obj = JSON_ValueAsObject(jlvl_elem->value);\n        if (jlvl_obj == nullptr) {\n            Shell_ExitSystemFmt(\n                \"%s: 'levels' elements must be dictionaries\", gs_file->path);\n            return;\n        }\n\n        const char *const title =\n            JSON_ObjectGetString(jlvl_obj, \"title\", JSON_INVALID_STRING);\n        if (title != JSON_INVALID_STRING) {\n            level->title = Memory_DupStr(title);\n        }\n\n        M_LoadTableFromJSON(jlvl_obj, &level->table);\n    }\n}\n\nGS_FILE *GS_File_CreateFromPath(const char *const path, const bool load_levels)\n{\n    GS_FILE *const gs_file = Memory_Alloc(sizeof(*gs_file));\n    gs_file->path = Memory_DupStr(path);\n\n    JSON_VALUE *const doc = JSONFile_ReadEx(path, true);\n    JSON_OBJECT *root_obj = JSON_ValueAsObject(doc);\n    M_LoadTableFromJSON(root_obj, &gs_file->global);\n    if (load_levels) {\n        M_LoadLevelsFromJSON(root_obj, gs_file, \"levels\", GFLT_MAIN);\n        M_LoadLevelsFromJSON(root_obj, gs_file, \"demos\", GFLT_DEMOS);\n        M_LoadLevelsFromJSON(root_obj, gs_file, \"cutscenes\", GFLT_CUTSCENES);\n    }\n\n    JSON_ValueFree(doc);\n    return gs_file;\n}\n"
  },
  {
    "path": "src/trx/game/game_strings/table.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\n#include <stdint.h>\n\nvoid GameStringTable_Init(void);\nvoid GameStringTable_Shutdown(void);\n\nbool GameStringTable_Load(const char *path, bool load_levels);\nvoid GameStringTable_Apply(const GF_LEVEL *level);\n"
  },
  {
    "path": "src/trx/game/gun/common.c",
    "content": "#include <trx/config.h>\n#include <trx/core/colors.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/gun.h>\n#include <trx/game/lara.h>\n#include <trx/game/output.h>\n#include <trx/version.h>\n\n#define M_SHOTGUN_AMMO_CLIP 6\n\nstatic bool M_IsGunType(\n    const LARA_GUN_TYPE gun_type, const WEAPON_TYPE weapon_type)\n{\n    return g_Weapons[gun_type].type == weapon_type;\n}\n\nstatic int32_t M_GetAmmoQuantity(\n    const LARA_GUN_TYPE gun_type, const int32_t shell_count)\n{\n    return MAX(1, shell_count) * Gun_GetAmmoClipCount(gun_type);\n}\n\nvoid Gun_AddDynamicLight(void)\n{\n    if (!g_Config.visuals.enable_gun_lighting) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t c = Math_Cos(lara_item->rot.y);\n    const int32_t s = Math_Sin(lara_item->rot.y);\n    const XYZ_32 pos = {\n        .x = lara_item->pos.x + (s >> (W2V_SHIFT - 10)),\n        .y = lara_item->pos.y - WALL_L / 2,\n        .z = lara_item->pos.z + (c >> (W2V_SHIFT - 10)),\n    };\n    if (g_TRVersion >= 3) {\n        Output_AddDynamicLightRGB(pos, 12, (RGB_888) { 192, 144, 0 });\n    } else {\n        Output_AddDynamicLight(pos, 12, 11);\n    }\n}\n\nOBJECT_ID Gun_GetLaraAnim(const LARA_GUN_TYPE gun_type)\n{\n    return M_IsGunType(gun_type, WEAPON_TYPE_DUAL_PISTOLS)\n        ? O_LARA_PISTOLS\n        : Gun_GetWeaponAnim(gun_type);\n}\n\nOBJECT_ID Gun_GetWeaponAnim(const LARA_GUN_TYPE gun_type)\n{\n    // clang-format off\n    switch (gun_type) {\n    case LGT_UNKNOWN:      return O_LARA;\n    case LGT_UNARMED:      return O_LARA;\n    case LGT_PISTOLS:      return O_LARA_PISTOLS;\n    case LGT_MAGNUMS:      return O_LARA_MAGNUMS;\n    case LGT_AUTOS:        return O_LARA_AUTOS;\n    case LGT_DESERT_EAGLE: return O_LARA_DESERT_EAGLE;\n    case LGT_UZIS:         return O_LARA_UZIS;\n    case LGT_SHOTGUN:      return O_LARA_SHOTGUN;\n    case LGT_M16:          return O_LARA_M16;\n    case LGT_MP5:          return O_LARA_MP5;\n    case LGT_GRENADE:      return O_LARA_GRENADE_GUN;\n    case LGT_ROCKET:       return O_LARA_ROCKET_GUN;\n    case LGT_HARPOON:      return O_LARA_HARPOON_GUN;\n    case LGT_FLARE:        return O_LARA_FLARE;\n    default:               return NO_OBJECT;\n    }\n    // clang-format on\n}\n\nLARA_GUN_TYPE Gun_GetType(const OBJECT_ID obj_id)\n{\n    // clang-format off\n    switch (obj_id) {\n    case O_PISTOL_ITEM:       return LGT_PISTOLS;\n    case O_MAGNUM_ITEM:       return LGT_MAGNUMS;\n    case O_AUTOS_ITEM:        return LGT_AUTOS;\n    case O_DESERT_EAGLE_ITEM: return LGT_DESERT_EAGLE;\n    case O_UZI_ITEM:          return LGT_UZIS;\n    case O_SHOTGUN_ITEM:      return LGT_SHOTGUN;\n    case O_HARPOON_ITEM:      return LGT_HARPOON;\n    case O_M16_ITEM:          return LGT_M16;\n    case O_MP5_ITEM:          return LGT_MP5;\n    case O_GRENADE_GUN_ITEM:  return LGT_GRENADE;\n    case O_ROCKET_GUN_ITEM:   return LGT_ROCKET;\n    default:                  return LGT_UNARMED;\n    }\n    // clang-format on\n}\n\nOBJECT_ID Gun_GetGunObject(const LARA_GUN_TYPE gun_type)\n{\n    // clang-format off\n    switch (gun_type) {\n    case LGT_PISTOLS:      return O_PISTOL_ITEM;\n    case LGT_MAGNUMS:      return O_MAGNUM_ITEM;\n    case LGT_AUTOS:        return O_AUTOS_ITEM;\n    case LGT_DESERT_EAGLE: return O_DESERT_EAGLE_ITEM;\n    case LGT_UZIS:         return O_UZI_ITEM;\n    case LGT_SHOTGUN:      return O_SHOTGUN_ITEM;\n    case LGT_HARPOON:      return O_HARPOON_ITEM;\n    case LGT_M16:          return O_M16_ITEM;\n    case LGT_MP5:          return O_MP5_ITEM;\n    case LGT_GRENADE:      return O_GRENADE_GUN_ITEM;\n    case LGT_ROCKET:       return O_ROCKET_GUN_ITEM;\n    default:               return NO_OBJECT;\n    }\n    // clang-format on\n}\n\nOBJECT_ID Gun_GetAmmoObject(const LARA_GUN_TYPE gun_type)\n{\n    // clang-format off\n    switch (gun_type) {\n    case LGT_PISTOLS:      return O_PISTOL_AMMO_ITEM;\n    case LGT_MAGNUMS:      return O_MAGNUM_AMMO_ITEM;\n    case LGT_AUTOS:        return O_AUTOS_AMMO_ITEM;\n    case LGT_DESERT_EAGLE: return O_DESERT_EAGLE_AMMO_ITEM;\n    case LGT_UZIS:         return O_UZI_AMMO_ITEM;\n    case LGT_SHOTGUN:      return O_SHOTGUN_AMMO_ITEM;\n    case LGT_HARPOON:      return O_HARPOON_AMMO_ITEM;\n    case LGT_M16:          return O_M16_AMMO_ITEM;\n    case LGT_MP5:          return O_MP5_AMMO_ITEM;\n    case LGT_GRENADE:      return O_GRENADE_AMMO_ITEM;\n    case LGT_ROCKET:       return O_ROCKET_AMMO_ITEM;\n    default:               return NO_OBJECT;\n    }\n    // clang-format on\n}\n\nint32_t Gun_GetAmmoInitialQuantity(const LARA_GUN_TYPE gun_type)\n{\n    return M_GetAmmoQuantity(gun_type, g_Weapons[gun_type].ammo.initial_qty);\n}\n\nint32_t Gun_GetAmmoPickupQuantity(const LARA_GUN_TYPE gun_type)\n{\n    return M_GetAmmoQuantity(gun_type, g_Weapons[gun_type].ammo.pickup_qty);\n}\n\nint32_t Gun_GetAmmoClipCount(const LARA_GUN_TYPE gun_type)\n{\n    return gun_type == LGT_SHOTGUN ? M_SHOTGUN_AMMO_CLIP : 1;\n}\n\nint32_t Gun_GetAmmoShellCount(const LARA_GUN_TYPE gun_type)\n{\n    return Gun_GetAmmoPickupQuantity(gun_type) / Gun_GetAmmoClipCount(gun_type);\n}\n\nAMMO_INFO *Gun_GetAmmoInfo(const LARA_GUN_TYPE gun_type)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info == nullptr) {\n        return nullptr;\n    }\n    // clang-format off\n    switch (gun_type) {\n    case LGT_PISTOLS:      return &lara_info->pistol_ammo;\n    case LGT_MAGNUMS:      return &lara_info->magnum_ammo;\n    case LGT_AUTOS:        return &lara_info->autos_ammo;\n    case LGT_DESERT_EAGLE: return &lara_info->desert_eagle_ammo;\n    case LGT_UZIS:         return &lara_info->uzi_ammo;\n    case LGT_SHOTGUN:      return &lara_info->shotgun_ammo;\n    case LGT_HARPOON:      return &lara_info->harpoon_ammo;\n    case LGT_M16:          return &lara_info->m16_ammo;\n    case LGT_MP5:          return &lara_info->mp5_ammo;\n    case LGT_GRENADE:      return &lara_info->grenade_ammo;\n    case LGT_ROCKET:       return &lara_info->rocket_ammo;\n    case LGT_SKIDOO:       return &lara_info->pistol_ammo;\n    default:               return nullptr;\n    }\n    // clang-format on\n}\n\nbool Gun_IsRifleType(const LARA_GUN_TYPE gun_type)\n{\n    return M_IsGunType(gun_type, WEAPON_TYPE_RIFLE);\n}\n\nvoid Gun_SetLaraHandLMesh(const LARA_GUN_TYPE weapon_type)\n{\n    Lara_Skin_SetGunEquipment(LM_HAND_L, weapon_type);\n}\n\nvoid Gun_SetLaraHandRMesh(const LARA_GUN_TYPE weapon_type)\n{\n    Lara_Skin_SetGunEquipment(LM_HAND_R, weapon_type);\n}\n\nvoid Gun_SetLaraBackMesh(const LARA_GUN_TYPE weapon_type)\n{\n    Lara_Skin_SetGunEquipment(LM_TORSO, weapon_type);\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->back_gun_type = weapon_type;\n}\n\nvoid Gun_SetLaraHolsterLMesh(const LARA_GUN_TYPE weapon_type)\n{\n    Lara_Skin_SetGunEquipment(LM_THIGH_L, weapon_type);\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->holsters_gun_type = weapon_type;\n}\n\nvoid Gun_SetLaraHolsterRMesh(const LARA_GUN_TYPE weapon_type)\n{\n    Lara_Skin_SetGunEquipment(LM_THIGH_R, weapon_type);\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->holsters_gun_type = weapon_type;\n}\n"
  },
  {
    "path": "src/trx/game/gun/common.h",
    "content": "#pragma once\n\n#include <trx/game/lara/types.h>\n\nvoid Gun_InitialiseNewWeapon(void);\n\nvoid Gun_SetLaraBackMesh(LARA_GUN_TYPE weapon_type);\nvoid Gun_SetLaraHandLMesh(LARA_GUN_TYPE weapon_type);\nvoid Gun_SetLaraHandRMesh(LARA_GUN_TYPE weapon_type);\nvoid Gun_SetLaraHolsterLMesh(LARA_GUN_TYPE weapon_type);\nvoid Gun_SetLaraHolsterRMesh(LARA_GUN_TYPE weapon_type);\n\n// TODO: make this a struct\nOBJECT_ID Gun_GetLaraAnim(LARA_GUN_TYPE gun_type);\nOBJECT_ID Gun_GetWeaponAnim(LARA_GUN_TYPE gun_type);\nLARA_GUN_TYPE Gun_GetType(OBJECT_ID obj_id);\nOBJECT_ID Gun_GetGunObject(LARA_GUN_TYPE gun_type);\nOBJECT_ID Gun_GetAmmoObject(LARA_GUN_TYPE gun_type);\nint32_t Gun_GetAmmoInitialQuantity(LARA_GUN_TYPE gun_type);\nint32_t Gun_GetAmmoPickupQuantity(LARA_GUN_TYPE gun_type);\nint32_t Gun_GetAmmoShellCount(LARA_GUN_TYPE gun_type);\nint32_t Gun_GetAmmoClipCount(LARA_GUN_TYPE gun_type);\nAMMO_INFO *Gun_GetAmmoInfo(LARA_GUN_TYPE gun_type);\nbool Gun_IsRifleType(LARA_GUN_TYPE gun_type);\n\nvoid Gun_AddDynamicLight(void);\n"
  },
  {
    "path": "src/trx/game/gun/control.c",
    "content": "#include <trx/game/gun/control.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/game.h>\n#include <trx/game/gun/common.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/pistols.h>\n#include <trx/game/gun/rifle.h>\n#include <trx/game/gun/smashing.h>\n#include <trx/game/gun/smoke.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\nstatic struct {\n    LARA_GUN_TYPE gun_type;\n    INPUT_ROLE input_role;\n} m_QuicDrawKeys[] = {\n    { .gun_type = LGT_PISTOLS, .input_role = INPUT_ROLE_EQUIP_PISTOLS },\n    { .gun_type = LGT_SHOTGUN, .input_role = INPUT_ROLE_EQUIP_SHOTGUN },\n    { .gun_type = LGT_MAGNUMS, .input_role = INPUT_ROLE_EQUIP_MAGNUMS },\n    { .gun_type = LGT_AUTOS, .input_role = INPUT_ROLE_EQUIP_AUTOS },\n    { .gun_type = LGT_DESERT_EAGLE,\n      .input_role = INPUT_ROLE_EQUIP_DESERT_EAGLE },\n    { .gun_type = LGT_UZIS, .input_role = INPUT_ROLE_EQUIP_UZIS },\n    { .gun_type = LGT_HARPOON, .input_role = INPUT_ROLE_EQUIP_HARPOON },\n    { .gun_type = LGT_M16, .input_role = INPUT_ROLE_EQUIP_M16 },\n    { .gun_type = LGT_MP5, .input_role = INPUT_ROLE_EQUIP_MP5 },\n    { .gun_type = LGT_GRENADE,\n      .input_role = INPUT_ROLE_EQUIP_GRENADE_LAUNCHER },\n    { .gun_type = LGT_ROCKET, .input_role = INPUT_ROLE_EQUIP_ROCKET_LAUNCHER },\n    { .gun_type = LGT_UNKNOWN, .input_role = (INPUT_ROLE)-1 },\n};\n\nstatic void M_CheckSmashablesBehindTarget(\n    const ITEM *const target, const GAME_VECTOR start,\n    const GAME_VECTOR hit_pos, const int32_t max_dist)\n{\n    if (target == nullptr || target->object_id != O_SOPHIA) {\n        return;\n    }\n\n    // OG does a raycast instead of segment cast when checking for smashables.\n    // TRX normally doesn't do that, but in the Reunion battle against Sophia,\n    // Sophia stands directly in front of the Fuse Box, preventing Lara from\n    // shooting her which is a breaking behavior.\n    //\n    // This function does additional smashable pass by emulating the OG raycast.\n    const int32_t hit_dist = XYZ_32_GetDistance(start.pos, hit_pos.pos);\n    if (hit_dist >= max_dist) {\n        return;\n    }\n\n    const int32_t offset = STEP_L / 16;\n    GAME_VECTOR follow_start = {\n        .x = hit_pos.x + ((offset * g_MatrixPtr->_20) >> W2V_SHIFT),\n        .y = hit_pos.y + ((offset * g_MatrixPtr->_21) >> W2V_SHIFT),\n        .z = hit_pos.z + ((offset * g_MatrixPtr->_22) >> W2V_SHIFT),\n        .room_num = hit_pos.room_num,\n    };\n    Room_GetSector(follow_start.pos, &follow_start.room_num);\n\n    GAME_VECTOR follow_end = {\n        .x = start.x + ((max_dist * g_MatrixPtr->_20) >> W2V_SHIFT),\n        .y = start.y + ((max_dist * g_MatrixPtr->_21) >> W2V_SHIFT),\n        .z = start.z + ((max_dist * g_MatrixPtr->_22) >> W2V_SHIFT),\n        .room_num = start.room_num,\n    };\n    Room_GetSector(follow_end.pos, &follow_end.room_num);\n\n    Gun_SmashItems(follow_start, follow_end, nullptr, NO_OBJECT);\n}\n\nstatic bool M_IsUsableUnderwater(const LARA_GUN_TYPE gun_type)\n{\n    return gun_type == LGT_HARPOON;\n}\n\nstatic bool M_IsTooSubmerged(const LARA_GUN_TYPE gun_type)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    return lara->water_surface_dist > -g_Weapons[gun_type].gun_height;\n}\n\nstatic LARA_GUN_TYPE M_NeedToQuickDraw(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    for (int32_t i = 0; m_QuicDrawKeys[i].gun_type != LGT_UNKNOWN; i++) {\n        if (Input_IsPressedDB(m_QuicDrawKeys[i].input_role)\n            && Inv_RequestItem(Gun_GetGunObject(m_QuicDrawKeys[i].gun_type))\n                > 0) {\n            return m_QuicDrawKeys[i].gun_type;\n        }\n    }\n    return LGT_UNKNOWN;\n}\n\nstatic bool M_QuickDrawWeapon(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const LARA_GUN_TYPE gun_type = M_NeedToQuickDraw();\n    if (gun_type != LGT_UNKNOWN) {\n        lara->request_gun_type = gun_type;\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_CanEquip(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->request_gun_type == LGT_FLARE) {\n        return lara->gun_type != LGT_FLARE;\n    }\n    if (lara->is_crouched && Gun_IsRifleType(lara->request_gun_type)) {\n        return false;\n    }\n    if (Lara_Vehicle_IsMounted()) {\n        return false;\n    }\n    if (!Inv_RequestItem(Gun_GetGunObject(lara->request_gun_type))) {\n        return false;\n    }\n    switch (lara->water_status) {\n    case LWS_CHEAT:\n        return false;\n    case LWS_ABOVE_WATER:\n        return true;\n    case LWS_SURFACE:\n    case LWS_UNDERWATER:\n        return M_IsUsableUnderwater(lara->request_gun_type);\n    case LWS_WADE: {\n        if (M_IsUsableUnderwater(lara->request_gun_type)) {\n            return true;\n        }\n        if (lara->gun_status == LGS_ARMLESS\n            || M_IsTooSubmerged(lara->gun_type)) {\n            return true;\n        }\n        return false;\n    default:\n        ASSERT_FAIL();\n        return false;\n    }\n    }\n}\n\nstatic bool M_HasWeaponAnim(const LARA_GUN_TYPE gun_type)\n{\n    const OBJECT *const obj = Object_Get(Gun_GetLaraAnim(gun_type));\n    return obj->loaded && obj->frame_base != nullptr;\n}\n\nstatic bool M_NeedToDraw(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.draw || lara->request_gun_type != lara->gun_type) {\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_NeedToUndraw(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.draw || lara->request_gun_type != lara->gun_type) {\n        return true;\n    }\n    if (M_QuickDrawWeapon()) {\n        if (g_Config.input.quick_guns_mode == QUICK_GUNS_MODE_DRAW_AND_HOLSTER\n            || lara->request_gun_type != lara->gun_type) {\n            return true;\n        }\n    }\n    switch (lara->water_status) {\n    case LWS_CHEAT:\n        return true;\n    case LWS_UNDERWATER:\n    case LWS_SURFACE:\n        return !M_IsUsableUnderwater(lara->request_gun_type);\n    case LWS_ABOVE_WATER:\n        return false;\n    case LWS_WADE:\n        return !M_IsUsableUnderwater(lara->request_gun_type)\n            && !M_IsTooSubmerged(lara->gun_type);\n    default:\n        return false;\n    }\n}\n\nstatic void M_DecideRequestedWeapon(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (g_Input.draw) {\n        LARA_GUN_TYPE requested_gun = lara->last_gun_type != LGT_UNARMED\n            ? lara->last_gun_type\n            : LGT_PISTOLS;\n        if (Inv_RequestItem(Gun_GetGunObject(requested_gun)) == 0) {\n            for (LARA_GUN_TYPE gun = 0; gun < NUM_WEAPONS; gun++) {\n                if (Inv_RequestItem(Gun_GetGunObject(gun)) > 0) {\n                    requested_gun = gun;\n                    break;\n                }\n            }\n        }\n        if (Inv_RequestItem(Gun_GetGunObject(requested_gun)) != 0) {\n            lara->request_gun_type = requested_gun;\n        }\n        return;\n    }\n\n    if (g_Input.use_flare) {\n        if (lara->gun_type == LGT_FLARE) {\n            lara->gun_status = LGS_UNDRAW;\n        } else if (\n            Inv_RequestItem(O_FLAREBOX_ITEM)\n            && (!g_Config.gameplay.fix_free_flare_glitch\n                || lara_item->current_anim_state != LS(LS_PICKUP))) {\n            lara->request_gun_type = LGT_FLARE;\n        }\n    }\n}\n\nstatic void M_DrawRequestedWeapon(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (M_CanEquip()) {\n        if (!M_HasWeaponAnim(lara->request_gun_type)) {\n            lara->request_gun_type = LGT_UNARMED;\n            lara->gun_type = LGT_UNARMED;\n            lara->gun_status = LGS_ARMLESS;\n            return;\n        }\n\n        if (lara->gun_type == LGT_FLARE) {\n            Lara_Flare_Dispose(false);\n        }\n\n        lara->gun_type = lara->request_gun_type;\n        Gun_InitialiseNewWeapon();\n        lara->gun_status = LGS_DRAW;\n        lara->right_arm.frame_num = 0;\n        lara->left_arm.frame_num = 0;\n    } else {\n        if (lara->request_gun_type != LGT_FLARE\n            && lara->request_gun_type != LGT_UNARMED) {\n            lara->last_gun_type = lara->request_gun_type;\n        }\n        if (lara->gun_type == LGT_FLARE) {\n            lara->request_gun_type = LGT_FLARE;\n        } else {\n            lara->gun_type = lara->request_gun_type;\n        }\n    }\n}\n\nstatic void M_TryUndrawWeapon(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.use_flare && Inv_RequestItem(O_FLAREBOX_ITEM)) {\n        lara->request_gun_type = LGT_FLARE;\n    }\n    if (M_NeedToUndraw()) {\n        lara->gun_status = LGS_UNDRAW;\n    }\n}\n\nstatic void M_UpdateGunState(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->hit_points <= 0) {\n        lara->gun_status = LGS_ARMLESS;\n    } else if (lara->gun_status == LGS_ARMLESS) {\n        if (M_QuickDrawWeapon()) {\n            M_DrawRequestedWeapon();\n        } else {\n            M_DecideRequestedWeapon();\n            if (M_NeedToDraw()) {\n                M_DrawRequestedWeapon();\n            }\n        }\n    } else if (lara->gun_status == LGS_READY) {\n        M_TryUndrawWeapon();\n    } else {\n        M_QuickDrawWeapon();\n    }\n}\n\nvoid Gun_Control(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (lara->extra_anim && lara->gun_status != LGS_HANDS_BUSY) {\n        lara->request_gun_type = LGT_UNARMED;\n        return;\n    }\n\n    if (lara->left_arm.flash_gun > 0) {\n        lara->left_arm.flash_gun--;\n    }\n    if (lara->right_arm.flash_gun > 0) {\n        lara->right_arm.flash_gun--;\n    }\n\n    Gun_Smoke_Control();\n\n    M_UpdateGunState();\n\n    switch (lara->gun_status) {\n    case LGS_ARMLESS:\n    case LGS_HANDS_BUSY:\n        if (lara->gun_type == LGT_FLARE) {\n            Lara_Flare_Control();\n        }\n        break;\n\n    case LGS_DRAW:\n        if (lara->gun_type != LGT_FLARE && lara->gun_type != LGT_UNARMED) {\n            lara->last_gun_type = lara->gun_type;\n        }\n\n        switch (lara->gun_type) {\n        case LGT_PISTOLS:\n        case LGT_MAGNUMS:\n        case LGT_AUTOS:\n        case LGT_DESERT_EAGLE:\n        case LGT_UZIS:\n            if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) {\n                g_Camera.type = CAM_COMBAT;\n            }\n            Gun_Pistols_Draw(lara->gun_type);\n            break;\n\n        case LGT_SHOTGUN:\n        case LGT_M16:\n        case LGT_MP5:\n        case LGT_GRENADE:\n        case LGT_ROCKET:\n        case LGT_HARPOON:\n            if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) {\n                g_Camera.type = CAM_COMBAT;\n            }\n            Gun_Rifle_Draw(lara->gun_type);\n            break;\n\n        case LGT_FLARE:\n            Lara_Flare_Draw();\n            break;\n\n        default:\n            lara->gun_status = LGS_ARMLESS;\n            break;\n        }\n        break;\n\n    case LGS_UNDRAW:\n        Lara_Skin_SetCombatFace(false);\n\n        switch (lara->gun_type) {\n        case LGT_PISTOLS:\n        case LGT_MAGNUMS:\n        case LGT_AUTOS:\n        case LGT_DESERT_EAGLE:\n        case LGT_UZIS:\n            Gun_Pistols_Undraw(lara->gun_type);\n            break;\n\n        case LGT_SHOTGUN:\n        case LGT_M16:\n        case LGT_MP5:\n        case LGT_GRENADE:\n        case LGT_ROCKET:\n        case LGT_HARPOON:\n            Gun_Rifle_Undraw(lara->gun_type);\n            break;\n\n        case LGT_FLARE:\n            Lara_Flare_Undraw();\n            break;\n\n        default:\n            return;\n        }\n        break;\n\n    case LGS_READY:\n        const bool is_firing = lara->pistol_ammo.ammo != 0 && g_Input.action;\n        Lara_Skin_SetCombatFace(is_firing);\n\n        if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) {\n            g_Camera.type = CAM_COMBAT;\n        }\n\n        if (g_Input.action) {\n            AMMO_INFO *const ammo = Gun_GetAmmoInfo(lara->gun_type);\n            ASSERT(ammo != nullptr);\n\n            if (ammo->ammo <= 0) {\n                ammo->ammo = 0;\n                if (g_TRVersion >= 2) {\n                    Sound_Effect(SFX_CLICK, &lara_item->pos, SPM_NORMAL);\n                }\n                lara->request_gun_type =\n                    Inv_RequestItem(O_PISTOL_ITEM) ? LGT_PISTOLS : LGT_UNARMED;\n                break;\n            }\n        }\n\n        switch (lara->gun_type) {\n        case LGT_PISTOLS:\n        case LGT_MAGNUMS:\n        case LGT_AUTOS:\n        case LGT_DESERT_EAGLE:\n        case LGT_UZIS:\n            Gun_Pistols_Control(lara->gun_type);\n            break;\n\n        case LGT_SHOTGUN:\n        case LGT_M16:\n        case LGT_MP5:\n        case LGT_GRENADE:\n        case LGT_ROCKET:\n        case LGT_HARPOON:\n            Gun_Rifle_Control(lara->gun_type);\n            break;\n\n        default:\n            return;\n        }\n        break;\n\n    case LGS_SPECIAL:\n        Lara_Flare_Draw();\n        break;\n\n    default:\n        return;\n    }\n}\n\nvoid Gun_EnsureReady(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_status == LGS_READY && Gun_IsRifleType(lara->gun_type)) {\n        Gun_Rifle_EnsureReady(lara->gun_type);\n    }\n}\n\nint32_t Gun_FireWeapon(\n    const LARA_GUN_TYPE weapon_type, ITEM *const target, const ITEM *const src,\n    const int16_t *const angles)\n{\n    const WEAPON_INFO *const weapon = &g_Weapons[weapon_type];\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    AMMO_INFO *const ammo = Gun_GetAmmoInfo(weapon_type);\n    ASSERT(ammo != nullptr);\n\n    if (ammo == &lara->pistol_ammo || Game_IsBonusFlagSet(GBF_NGPLUS)) {\n        ammo->ammo = 1000;\n    }\n    if (ammo->ammo <= 0) {\n        ammo->ammo = 0;\n        if (g_TRVersion == 1) {\n            Sound_Effect(SFX_LARA_EMPTY, &src->pos, SPM_NORMAL);\n            if (Inv_RequestItem(O_PISTOL_ITEM)) {\n                lara->request_gun_type = LGT_PISTOLS;\n            } else {\n                lara->gun_status = LGS_UNDRAW;\n            }\n        }\n        return 0;\n    }\n    ammo->ammo--;\n    Stats_AddAmmoUsed();\n    lara->has_fired = true;\n\n    const XYZ_32 view_pos = {\n        .x = src->pos.x,\n        .y = src->pos.y - weapon->gun_height,\n        .z = src->pos.z,\n    };\n    const XYZ_16 view_rot = {\n        .x = angles[1]\n            + weapon->shot_accuracy * (Random_GetControl() - DEG_90) / DEG_360,\n        .y = angles[0]\n            + weapon->shot_accuracy * (Random_GetControl() - DEG_90) / DEG_360,\n        .z = 0,\n    };\n    Matrix_GenerateW2V(&view_pos, &view_rot);\n\n    SPHERE spheres[33];\n    int32_t sphere_count = Collide_GetSpheres(target, spheres, false);\n    int32_t best_sphere = -1;\n    int32_t best_dist = INT32_MAX;\n\n    for (int32_t i = 0; i < sphere_count; i++) {\n        const SPHERE *const sphere = &spheres[i];\n        const int32_t r = sphere->r;\n        if (ABS(sphere->pos.x) < r && ABS(sphere->pos.y) < r\n            && sphere->pos.z > r\n            && SQUARE(sphere->pos.x) + SQUARE(sphere->pos.y) <= SQUARE(r)) {\n            const int32_t dist = sphere->pos.z - r;\n            if (dist < best_dist) {\n                best_dist = dist;\n                best_sphere = i;\n            }\n        }\n    }\n\n    GAME_VECTOR start = {\n        .pos = view_pos,\n        .room_num = src->room_num,\n    };\n\n    if (best_sphere < 0) {\n        const int32_t dist = weapon->target_dist;\n        GAME_VECTOR hit_pos = g_TRVersion == 1\n            ? (GAME_VECTOR) {\n                .x = start.x + g_MatrixPtr->_20,\n                .y = start.y + g_MatrixPtr->_21,\n                .z = start.z + g_MatrixPtr->_22,\n                .room_num = start.room_num,\n            }\n            : (GAME_VECTOR) {\n                .x = start.x + ((dist * g_MatrixPtr->_20) >> W2V_SHIFT),\n                .y = start.y + ((dist * g_MatrixPtr->_21) >> W2V_SHIFT),\n                .z = start.z + ((dist * g_MatrixPtr->_22) >> W2V_SHIFT),\n                .room_num = start.room_num,\n            };\n        Room_GetSector(hit_pos.pos, &hit_pos.room_num);\n        const bool object_on_los = LOS_Check(&start, &hit_pos, true);\n        if (Gun_SmashItems(start, hit_pos, &hit_pos.pos, NO_OBJECT)\n            == PROJECTILE_HIT_STOP) {\n            Room_GetSector(hit_pos.pos, &hit_pos.room_num);\n        }\n        if (!object_on_los) {\n            Spawn_RicochetRay(start, hit_pos);\n        }\n        return -1;\n    }\n\n    Stats_AddAmmoHits();\n    GAME_VECTOR hit_pos = {\n        .x = start.x + ((best_dist * g_MatrixPtr->_20) >> W2V_SHIFT),\n        .y = start.y + ((best_dist * g_MatrixPtr->_21) >> W2V_SHIFT),\n        .z = start.z + ((best_dist * g_MatrixPtr->_22) >> W2V_SHIFT),\n        .room_num = start.room_num,\n    };\n    Room_GetSector(hit_pos.pos, &hit_pos.room_num);\n    Gun_SmashItems(start, hit_pos, nullptr, NO_OBJECT);\n    Gun_HitTarget(\n        target, &start, &hit_pos,\n        weapon->damage * (Game_IsBonusFlagSet(GBF_JAPANESE) ? 2 : 1));\n    M_CheckSmashablesBehindTarget(target, start, hit_pos, weapon->target_dist);\n    return 1;\n}\n"
  },
  {
    "path": "src/trx/game/gun/control.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n#include <trx/game/lara/enum.h>\n\nint32_t Gun_FireWeapon(\n    LARA_GUN_TYPE weapon_type, ITEM *target, const ITEM *src,\n    const int16_t *angles);\n\nvoid Gun_Control(void);\nvoid Gun_EnsureReady(void);\n"
  },
  {
    "path": "src/trx/game/gun/misc.c",
    "content": "#include <trx/game/gun/misc.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/game/creature.h>\n#include <trx/game/gun/common.h>\n#include <trx/game/gun/pistols.h>\n#include <trx/game/gun/rifle.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/los.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n\n#define M_NEAR_ANGLE (DEG_1 * 15) // = 2730\n\nstatic ITEM *m_TargetList[LOT_SLOT_COUNT] = {};\nstatic ITEM *m_LastTargetList[LOT_SLOT_COUNT] = {};\nstatic int16_t m_TargetCount = 0;\n\nstatic bool M_TargetListContains(const ITEM *const item, const int16_t count)\n{\n    for (int16_t i = 0; i < count; i++) {\n        if (m_TargetList[i] == item) {\n            return true;\n        }\n    }\n    return false;\n}\n\ntypedef struct {\n    const WEAPON_INFO *weapon;\n    const GAME_VECTOR *start;\n    const ITEM *lara_item;\n    const LARA_INFO *lara;\n    const ITEM *old_target;\n    int32_t max_dist;\n    ITEM *best_target;\n    int16_t best_y_rot;\n    int32_t best_dist;\n    int16_t num_targets;\n    int32_t old_target_dist;\n    int16_t old_target_y_rot;\n    bool old_target_in_list;\n} M_TARGET_CONTEXT;\n\nstatic void M_ConsiderTarget(M_TARGET_CONTEXT *const ctx, ITEM *const item)\n{\n    if (item == ctx->lara_item || !Item_IsTargetable(item)) {\n        return;\n    }\n\n    const int32_t dx = item->pos.x - ctx->start->x;\n    const int32_t dy = item->pos.y - ctx->start->y;\n    const int32_t dz = item->pos.z - ctx->start->z;\n    if (ABS(dx) > ctx->max_dist || ABS(dy) > ctx->max_dist\n        || ABS(dz) > ctx->max_dist) {\n        return;\n    }\n\n    const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n    if (dist >= SQUARE(ctx->max_dist)) {\n        return;\n    }\n\n    GAME_VECTOR target;\n    Gun_FindTargetPoint(item, &target);\n    if (!LOS_Check(ctx->start, &target, true)) {\n        return;\n    }\n\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        target.x - ctx->start->x, target.y - ctx->start->y,\n        target.z - ctx->start->z, angles);\n    angles[0] -= ctx->lara->torso_rot.y + ctx->lara_item->rot.y;\n    angles[1] -= ctx->lara->torso_rot.x + ctx->lara_item->rot.x;\n\n    if (angles[0] < ctx->weapon->lock_angles[0]\n        || angles[0] > ctx->weapon->lock_angles[1]\n        || angles[1] < ctx->weapon->lock_angles[2]\n        || angles[1] > ctx->weapon->lock_angles[3]) {\n        return;\n    }\n\n    if (ctx->num_targets < LOT_SLOT_COUNT) {\n        m_TargetList[ctx->num_targets] = item;\n        ctx->num_targets++;\n    }\n\n    const int16_t y_rot = ABS(angles[0]);\n    if (item == ctx->old_target) {\n        ctx->old_target_dist = dist;\n        ctx->old_target_y_rot = y_rot;\n        ctx->old_target_in_list = true;\n    }\n\n    if (g_TRVersion == 1) {\n        if (y_rot < ctx->best_y_rot) {\n            ctx->best_dist = dist;\n            ctx->best_y_rot = y_rot;\n            ctx->best_target = item;\n        }\n    } else {\n        if (y_rot < ctx->best_y_rot + M_NEAR_ANGLE && dist < ctx->best_dist) {\n            ctx->best_dist = dist;\n            ctx->best_y_rot = y_rot;\n            ctx->best_target = item;\n        }\n    }\n}\n\nstatic void M_DrawGunGlow(const XYZ_32 offset, const RGB_F color)\n{\n    if (g_TRVersion < 3) {\n        return;\n    }\n    const OBJECT *const glow_obj = Object_Get(O_GLOW);\n    if (!glow_obj->loaded) {\n        return;\n    }\n\n    Matrix_Push();\n    Matrix_TranslateRel32(offset);\n    const XYZ_32 pos = {\n        .x = (int32_t)(g_WMatrixPtr->_03 >> W2V_SHIFT),\n        .y = (int32_t)(g_WMatrixPtr->_13 >> W2V_SHIFT),\n        .z = (int32_t)(g_WMatrixPtr->_23 >> W2V_SHIFT),\n    };\n    Matrix_Pop();\n    Output_DrawSprite(\n        pos.x, pos.y, pos.z, glow_obj->mesh_idx, 0, color, DRAW_BLEND_ADD);\n}\n\nvoid Gun_FindTargetPoint(const ITEM *const item, GAME_VECTOR *const target)\n{\n    const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds;\n    const int32_t x = bounds->min.x + (bounds->max.x - bounds->min.x) / 2;\n    const int32_t y = bounds->min.y + (bounds->max.y - bounds->min.y) / 3;\n    const int32_t z = bounds->min.z + (bounds->max.z - bounds->min.z) / 2;\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sy = Math_Sin(item->rot.y);\n    target->pos.x = item->pos.x + ((cy * x + sy * z) >> W2V_SHIFT);\n    target->pos.y = item->pos.y + y;\n    target->pos.z = item->pos.z + ((cy * z - sy * x) >> W2V_SHIFT);\n    target->room_num = item->room_num;\n}\n\nvoid Gun_AimWeapon(const WEAPON_INFO *const weapon, LARA_ARM *const arm)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const int16_t speed = weapon->aim_speed;\n\n    int16_t dest_x = 0;\n    int16_t dest_y = 0;\n    if (arm->lock) {\n        dest_y = lara->target_angles[0];\n        dest_x = lara->target_angles[1];\n    }\n\n    if (arm->rot.y >= dest_y - speed && arm->rot.y <= dest_y + speed) {\n        arm->rot.y = dest_y;\n    } else if (arm->rot.y < dest_y) {\n        arm->rot.y += speed;\n    } else {\n        arm->rot.y -= speed;\n    }\n\n    if (arm->rot.x >= dest_x - speed && arm->rot.x <= dest_x + speed) {\n        arm->rot.x = dest_x;\n    } else if (arm->rot.x < dest_x) {\n        arm->rot.x += speed;\n    } else {\n        arm->rot.x -= speed;\n    }\n\n    arm->rot.z = 0;\n}\n\nvoid Gun_TargetInfo(const WEAPON_INFO *const weapon)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara->target == nullptr) {\n        lara->left_arm.lock = 0;\n        lara->right_arm.lock = 0;\n        lara->target_angles[0] = 0;\n        lara->target_angles[1] = 0;\n        return;\n    }\n\n    GAME_VECTOR target;\n    GAME_VECTOR start = {\n        .pos = {\n            .x = lara_item->pos.x,\n            .y = lara_item->pos.y - 650,\n            .z = lara_item->pos.z,\n        },\n        .room_num = lara_item->room_num,\n    };\n    Gun_FindTargetPoint(lara->target, &target);\n\n    int16_t angles[2];\n    // clang-format off\n    Math_GetVectorAngles(\n        target.pos.x - start.pos.x,\n        target.pos.y - start.pos.y,\n        target.pos.z - start.pos.z,\n        angles);\n    // clang-format on\n\n    angles[0] -= lara_item->rot.y;\n    angles[1] -= lara_item->rot.x;\n\n    if (!LOS_Check(&start, &target, true)) {\n        lara->left_arm.lock = 0;\n        lara->right_arm.lock = 0;\n    } else if (\n        angles[0] >= weapon->lock_angles[0]\n        && angles[0] <= weapon->lock_angles[1]\n        && angles[1] >= weapon->lock_angles[2]\n        && angles[1] <= weapon->lock_angles[3]) {\n        lara->left_arm.lock = 1;\n        lara->right_arm.lock = 1;\n    } else {\n        if (lara->left_arm.lock\n            && (angles[0] < weapon->left_angles[0]\n                || angles[0] > weapon->left_angles[1]\n                || angles[1] < weapon->left_angles[2]\n                || angles[1] > weapon->left_angles[3])) {\n            lara->left_arm.lock = 0;\n        }\n        if (lara->right_arm.lock\n            && (angles[0] < weapon->right_angles[0]\n                || angles[0] > weapon->right_angles[1]\n                || angles[1] < weapon->right_angles[2]\n                || angles[1] > weapon->right_angles[3])) {\n            lara->right_arm.lock = 0;\n        }\n    }\n\n    lara->target_angles[0] = angles[0];\n    lara->target_angles[1] = angles[1];\n}\n\nvoid Gun_InitialiseNewWeapon(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->target = nullptr;\n\n    lara->left_arm.flash_gun = 0;\n    lara->left_arm.frame_num = LF_G_AIM_START;\n    lara->left_arm.lock = 0;\n    lara->left_arm.rot.x = 0;\n    lara->left_arm.rot.y = 0;\n    lara->left_arm.rot.z = 0;\n\n    lara->right_arm.flash_gun = 0;\n    lara->right_arm.frame_num = LF_G_AIM_START;\n    lara->right_arm.lock = 0;\n    lara->right_arm.rot.x = 0;\n    lara->right_arm.rot.y = 0;\n    lara->right_arm.rot.z = 0;\n\n    const OBJECT_ID anim_type = Gun_GetLaraAnim(lara->gun_type);\n    const OBJECT *const obj = Object_Get(anim_type);\n    lara->left_arm.frame_base = obj->frame_base;\n    lara->right_arm.frame_base = obj->frame_base;\n\n    if (lara->gun_status != LGS_ARMLESS) {\n        switch (lara->gun_type) {\n        case LGT_PISTOLS:\n        case LGT_MAGNUMS:\n        case LGT_AUTOS:\n        case LGT_DESERT_EAGLE:\n        case LGT_UZIS:\n            Gun_Pistols_DrawMeshes(lara->gun_type);\n            break;\n\n        case LGT_SHOTGUN:\n        case LGT_M16:\n        case LGT_MP5:\n        case LGT_GRENADE:\n        case LGT_ROCKET:\n        case LGT_HARPOON:\n            Gun_Rifle_DrawMeshes(lara->gun_type);\n            break;\n\n        case LGT_FLARE:\n            Lara_Flare_DrawMeshes();\n            break;\n\n        default:\n            break;\n        }\n    }\n}\n\nvoid Gun_DrawFlash(\n    const LARA_GUN_TYPE weapon_type, const CLIP clip, const bool interpolated)\n{\n    if (weapon_type == LGT_SHOTGUN && !g_Config.visuals.enable_shotgun_flash) {\n        return;\n    }\n\n    OBJECT_ID flash_object_id = O_GUN_FLASH;\n    XYZ_16 rot = {};\n\n    switch (weapon_type) {\n    case LGT_M16:\n    case LGT_MP5:\n        rot.x = -85 * DEG_1;\n        rot.z = ((2 * Random_GetDraw()) & 0x4000);\n        if (weapon_type == LGT_M16) {\n            rot.z += 0x2000;\n        } else {\n            rot.z += (Random_GetDraw() & 0xFFF) + 0x1800;\n        }\n        flash_object_id = O_M16_FLASH;\n        break;\n\n    case LGT_FLARE:\n        rot.x = -DEG_90;\n        rot.y = 2 * Random_GetDraw();\n        flash_object_id = O_FLARE_FIRE;\n        break;\n\n    default:\n        rot.x = -DEG_90;\n        rot.z = 2 * Random_GetDraw();\n        break;\n    }\n\n    const WEAPON_INFO weapon = g_Weapons[weapon_type];\n    if (interpolated) {\n        Matrix_TranslateRel32_I(weapon.flash_pos);\n        Matrix_RotX_I(rot.x);\n        Matrix_RotY_I(rot.y);\n        Matrix_RotZ_I(rot.z);\n    } else {\n        Matrix_TranslateRel32(weapon.flash_pos);\n        Matrix_RotX(rot.x);\n        Matrix_RotY(rot.y);\n        Matrix_RotZ(rot.z);\n    }\n\n    const GAME_VECTOR pos = {\n        .room_num = Lara_GetItem()->room_num,\n        .pos = Matrix_MulVec32_M(g_WMatrixPtr, (XYZ_32) {}),\n    };\n    Output_PushTintOverride(Lara_GetMeshTint(pos));\n\n    if (g_TRVersion < 3) {\n        Output_CalculateStaticLight(weapon.flash_shade);\n    } else {\n        Output_CalculateStaticLightRGB_F(weapon.flash_color);\n    }\n    const OBJECT *const flash_obj = Object_Get(flash_object_id);\n    if (flash_obj->loaded) {\n        Object_DrawMesh(flash_obj->mesh_idx, clip, interpolated);\n    }\n\n    M_DrawGunGlow(weapon.glow_pos, weapon.glow_color);\n    Output_PopTintOverride();\n}\n\nvoid Gun_UpdateLaraMeshes(const OBJECT_ID obj_id)\n{\n    const bool lara_has_rifle = Inv_RequestItem(O_SHOTGUN_ITEM)\n        || Inv_RequestItem(O_HARPOON_ITEM) || Inv_RequestItem(O_M16_ITEM)\n        || Inv_RequestItem(O_MP5_ITEM) || Inv_RequestItem(O_GRENADE_GUN_ITEM)\n        || Inv_RequestItem(O_ROCKET_GUN_ITEM);\n    const bool lara_has_pistols = Inv_RequestItem(O_PISTOL_ITEM)\n        || Inv_RequestItem(O_MAGNUM_ITEM) || Inv_RequestItem(O_AUTOS_ITEM)\n        || Inv_RequestItem(O_DESERT_EAGLE_ITEM) || Inv_RequestItem(O_UZI_ITEM);\n\n    LARA_GUN_TYPE back_gun_type = LGT_UNARMED;\n    LARA_GUN_TYPE holsters_gun_type = LGT_UNARMED;\n\n    if (!lara_has_rifle && obj_id == O_SHOTGUN_ITEM) {\n        back_gun_type = LGT_SHOTGUN;\n    } else if (!lara_has_rifle && obj_id == O_HARPOON_ITEM) {\n        back_gun_type = LGT_HARPOON;\n    } else if (!lara_has_rifle && obj_id == O_M16_ITEM) {\n        back_gun_type = LGT_M16;\n    } else if (!lara_has_rifle && obj_id == O_MP5_ITEM) {\n        back_gun_type = LGT_MP5;\n    } else if (!lara_has_rifle && obj_id == O_GRENADE_GUN_ITEM) {\n        back_gun_type = LGT_GRENADE;\n    } else if (!lara_has_rifle && obj_id == O_ROCKET_GUN_ITEM) {\n        back_gun_type = LGT_ROCKET;\n    } else if (!lara_has_pistols && obj_id == O_PISTOL_ITEM) {\n        holsters_gun_type = LGT_PISTOLS;\n    } else if (!lara_has_pistols && obj_id == O_MAGNUM_ITEM) {\n        holsters_gun_type = LGT_MAGNUMS;\n    } else if (!lara_has_pistols && obj_id == O_AUTOS_ITEM) {\n        holsters_gun_type = LGT_AUTOS;\n    } else if (!lara_has_pistols && obj_id == O_DESERT_EAGLE_ITEM) {\n        holsters_gun_type = LGT_DESERT_EAGLE;\n    } else if (!lara_has_pistols && obj_id == O_UZI_ITEM) {\n        holsters_gun_type = LGT_UZIS;\n    }\n\n    if (back_gun_type != LGT_UNARMED) {\n        Gun_SetLaraBackMesh(back_gun_type);\n    }\n\n    if (holsters_gun_type != LGT_UNARMED) {\n        Gun_SetLaraHolsterLMesh(holsters_gun_type);\n        Gun_SetLaraHolsterRMesh(holsters_gun_type);\n    }\n}\n\nvoid Gun_HitTarget(\n    ITEM *const item, const GAME_VECTOR *const start,\n    const GAME_VECTOR *const hit_pos, int32_t damage)\n{\n    OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->gun_hit_func != nullptr) {\n        const bool use_default =\n            obj->gun_hit_func(item, start, hit_pos, &damage);\n        if (!use_default) {\n            return;\n        }\n    }\n\n    const bool make_ricochet = !Item_ShouldSpawnBlood(item);\n    if (item->object_id == O_SHIVA && make_ricochet) {\n        damage = 0;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool was_alive = item->hit_points > 0;\n    const bool clears_target = was_alive && damage >= item->hit_points;\n    if (clears_target) {\n        if (item->include_in_kill_stats) {\n            Stats_AddKill();\n        }\n        if (g_Config.gameplay.target_mode == TARGET_LOCK_MODE_SEMI) {\n            lara->target = nullptr;\n        }\n    }\n\n    Item_TakeDamage(item, damage, true);\n    if (item->creature_data != nullptr\n        && Object_Get(item->object_id)->intelligent) {\n        Creature_Hurt(item, damage);\n    }\n\n    if (hit_pos != nullptr) {\n        if (make_ricochet) {\n            const GAME_VECTOR pos = {\n                .pos = hit_pos->pos,\n                .room_num = item->room_num,\n            };\n            if (start != nullptr) {\n                Spawn_RicochetRay(*start, pos);\n            } else {\n                Spawn_Ricochet(pos);\n            }\n        } else {\n            Spawn_Blood(\n                hit_pos->x, hit_pos->y, hit_pos->z, item->speed, item->rot.y,\n                item->room_num);\n        }\n    }\n\n    if (item->hit_points > 0) {\n        switch (item->object_id) {\n        case O_WOLF:\n            Sound_Effect(SFX_WOLF_HURT, &item->pos, SPM_NORMAL);\n            break;\n\n        case O_BEAR:\n            Sound_Effect(SFX_BEAR_HURT, &item->pos, SPM_NORMAL);\n            break;\n\n        case O_LION:\n        case O_LIONESS:\n            Sound_Effect(SFX_LION_HURT, &item->pos, SPM_NORMAL);\n            break;\n\n        case O_RAT:\n            Sound_Effect(SFX_RAT_CHIRP, &item->pos, SPM_NORMAL);\n            break;\n\n        case O_SKATEKID:\n            Sound_Effect(SFX_SKATEBOARD_HIT, &item->pos, SPM_NORMAL);\n            break;\n\n        case O_TORSO:\n            Sound_Effect(SFX_TORSO_HIT, &item->pos, SPM_NORMAL);\n            break;\n\n        default:\n            break;\n        }\n    }\n}\n\nvoid Gun_GetNewTarget(const WEAPON_INFO *const weapon)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const old_target = lara->target;\n\n    // Preserve OG targeting behavior.\n    if (g_Config.gameplay.target_mode == TARGET_LOCK_MODE_FULL\n        && !g_Config.gameplay.enable_target_change && !g_Input.action) {\n        lara->target = nullptr;\n    }\n\n    const GAME_VECTOR start = {\n        .x = lara_item->pos.x,\n        .y = lara_item->pos.y - 650,\n        .z = lara_item->pos.z,\n        .room_num = lara_item->room_num,\n    };\n\n    M_TARGET_CONTEXT ctx = {\n        .weapon = weapon,\n        .start = &start,\n        .lara_item = lara_item,\n        .lara = lara,\n        .old_target = old_target,\n        .max_dist = weapon->target_dist,\n        .best_target = nullptr,\n        .best_y_rot = INT16_MAX,\n        .best_dist = INT32_MAX,\n        .num_targets = 0,\n        .old_target_dist = INT32_MAX,\n        .old_target_y_rot = INT16_MAX,\n        .old_target_in_list = false,\n    };\n\n    // First pass: active creatures\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM) {\n            continue;\n        }\n        M_ConsiderTarget(&ctx, Item_Get(creature->item_num));\n    }\n\n    // Second pass: other objects, including skidoo driver, whose targetable\n    // ITEM is NOT in the active item list\n    for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (M_TargetListContains(item, ctx.num_targets)) {\n            continue;\n        }\n        M_ConsiderTarget(&ctx, item);\n    }\n\n    m_TargetCount = ctx.num_targets;\n\n    if ((g_Config.gameplay.target_mode == TARGET_LOCK_MODE_FULL\n         || g_Config.gameplay.target_mode == TARGET_LOCK_MODE_SEMI)\n        && g_Input.action && lara->target != nullptr) {\n        Gun_TargetInfo(weapon);\n        return;\n    }\n\n    if (ctx.num_targets > 0) {\n        bool found_current_target = false;\n        for (int16_t slot = 0; slot < ctx.num_targets; slot++) {\n            if (m_TargetList[slot] == lara->target) {\n                found_current_target = true;\n                break;\n            }\n        }\n\n        if (!found_current_target) {\n            lara->target = ctx.best_target;\n            m_LastTargetList[0] = nullptr;\n        }\n    } else {\n        lara->target = nullptr;\n    }\n\n    if (lara->target != m_LastTargetList[0]) {\n        for (int32_t slot = LOT_SLOT_COUNT - 1; slot > 0; slot--) {\n            m_LastTargetList[slot] = m_LastTargetList[slot - 1];\n        }\n        m_LastTargetList[0] = lara->target;\n    }\n\n    Gun_TargetInfo(weapon);\n}\n\nvoid Gun_ChangeTarget(const WEAPON_INFO *const weapon)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->target = nullptr;\n    bool found_new_target = false;\n\n    for (int16_t new_target = 0; new_target < m_TargetCount; new_target++) {\n        for (int32_t last_target = 0; last_target < LOT_SLOT_COUNT;\n             last_target++) {\n            if (!m_LastTargetList[last_target]) {\n                found_new_target = true;\n                break;\n            }\n\n            if (m_LastTargetList[last_target] == m_TargetList[new_target]) {\n                break;\n            }\n        }\n\n        if (found_new_target) {\n            lara->target = m_TargetList[new_target];\n            break;\n        }\n    }\n\n    if (lara->target != m_LastTargetList[0]) {\n        for (int32_t last_target = LOT_SLOT_COUNT - 1; last_target > 0;\n             last_target--) {\n            m_LastTargetList[last_target] = m_LastTargetList[last_target - 1];\n        }\n        m_LastTargetList[0] = lara->target;\n    }\n\n    Gun_TargetInfo(weapon);\n}\n"
  },
  {
    "path": "src/trx/game/gun/misc.h",
    "content": "#pragma once\n\n// Private gun routines.\n\n#include <trx/game/gun/types.h>\n#include <trx/game/items/types.h>\n#include <trx/game/lara/types.h>\n#include <trx/game/types.h>\n\nvoid Gun_FindTargetPoint(const ITEM *item, GAME_VECTOR *target);\nvoid Gun_AimWeapon(const WEAPON_INFO *weapon, LARA_ARM *arm);\nvoid Gun_TargetInfo(const WEAPON_INFO *weapon);\nvoid Gun_UpdateLaraMeshes(OBJECT_ID obj_id);\n\nvoid Gun_GetNewTarget(const WEAPON_INFO *weapon);\nvoid Gun_ChangeTarget(const WEAPON_INFO *weapon);\nvoid Gun_HitTarget(\n    ITEM *item, const GAME_VECTOR *start, const GAME_VECTOR *hit_pos,\n    int32_t damage);\n\nvoid Gun_DrawFlash(LARA_GUN_TYPE weapon_type, CLIP clip, bool interpolated);\n"
  },
  {
    "path": "src/trx/game/gun/pistols.c",
    "content": "#include <trx/game/gun/pistols.h>\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/gun/common.h>\n#include <trx/game/gun/control.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/smoke.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/input.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_ENABLE_FAST_UZI (g_TRVersion >= 2)\n\ntypedef enum {\n    // clang-format off\n    LA_PISTOLS_AIM    = 0,\n    LA_PISTOLS_UNDRAW = 1,\n    LA_PISTOLS_DRAW   = 2,\n    LA_PISTOLS_RECOIL = 3,\n    // clang-format on\n} LARA_PISTOLS_ANIMATION;\n\ntypedef struct {\n    struct {\n        int16_t start;\n        int16_t extend;\n        int16_t bend;\n        int16_t end;\n    } aim, undraw, draw, recoil;\n} M_FRAME_SETUP;\n\nstatic const M_FRAME_SETUP m_DefaultSetup = {\n    .aim.start = LF_G_AIM_START,\n    .aim.bend = LF_G_AIM_BEND,\n    .aim.extend = LF_G_AIM_EXTEND,\n    .aim.end = LF_G_AIM_END,\n    .undraw.start = LF_G_UNDRAW_START,\n    .undraw.bend = LF_G_UNDRAW_BEND,\n    .undraw.end = LF_G_UNDRAW_END,\n    .draw.start = LF_G_DRAW_START,\n    .draw.end = LF_G_DRAW_END,\n    .recoil.start = LF_G_RECOIL_START,\n    .recoil.end = LF_G_RECOIL_END,\n};\n\nstatic const M_FRAME_SETUP m_DesertEagleSetup = {\n    .aim.start = LF_G_AIM_START,\n    .aim.bend = LF_G_AIM_BEND,\n    .aim.extend = 6,\n    .aim.end = 7,\n    .undraw.start = 8,\n    .undraw.bend = 9,\n    .undraw.end = 14,\n    .draw.start = 15,\n    .draw.end = 28,\n    .recoil.start = 29,\n    .recoil.end = 44,\n};\n\nstatic bool m_SoundRight = false;\nstatic bool m_SoundLeft = false;\n\nstatic bool M_EnableFastSound(const LARA_GUN_TYPE weapon_type)\n{\n    return g_TRVersion >= 2 && weapon_type == LGT_UZIS;\n}\n\nstatic void M_FireSound(const SAMPLE_TRX_ID sample_trx_id, const bool alternate)\n{\n    SAMPLE_ID sample_id = Sound_ToGameID(sample_trx_id);\n    if (sample_id == SFX_INVALID) {\n        return;\n    }\n    if (alternate) {\n        sample_id += 1;\n    }\n    Sound_Effect_Direct(sample_id, &Lara_GetItem()->pos, SPM_NORMAL);\n}\n\nstatic const M_FRAME_SETUP *M_GetSetup(const LARA_GUN_TYPE weapon_type)\n{\n    return weapon_type == LGT_DESERT_EAGLE ? &m_DesertEagleSetup\n                                           : &m_DefaultSetup;\n}\n\nstatic void M_SetArmInfo(LARA_ARM *const arm, const int32_t frame)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const M_FRAME_SETUP *const setup = M_GetSetup(lara->gun_type);\n\n    int16_t anim_idx;\n    if (Anim_TestAbsFrameRange(frame, setup->aim.start, setup->aim.end)) {\n        anim_idx = LA_PISTOLS_AIM;\n    } else if (\n        Anim_TestAbsFrameRange(frame, setup->undraw.start, setup->undraw.end)) {\n        anim_idx = LA_PISTOLS_UNDRAW;\n    } else if (\n        Anim_TestAbsFrameRange(frame, setup->draw.start, setup->draw.end)) {\n        anim_idx = LA_PISTOLS_DRAW;\n    } else if (\n        Anim_TestAbsFrameRange(frame, setup->recoil.start, setup->recoil.end)) {\n        anim_idx = LA_PISTOLS_RECOIL;\n    } else {\n        return;\n    }\n\n    const OBJECT_ID obj_id = Gun_GetLaraAnim(lara->gun_type);\n    const OBJECT *const obj = Object_Get(obj_id);\n    const ANIM *const anim = Object_GetAnim(obj, anim_idx);\n    arm->anim_num = obj->anim_idx + anim_idx;\n    arm->frame_num = frame;\n    arm->frame_base = anim->frame_ptr;\n}\n\nstatic void M_Animate(const LARA_GUN_TYPE weapon_type)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type);\n    const WEAPON_INFO *const weapon = &g_Weapons[weapon_type];\n    const ITEM *const lara_item = Lara_GetItem();\n\n    bool sound_already = false;\n    int16_t angles[2];\n\n    int32_t frame_r = lara->right_arm.frame_num;\n    if (!lara->right_arm.lock && (!g_Input.action || lara->target != nullptr)) {\n        if (Anim_TestAbsFrameRange(\n                frame_r, setup->recoil.start, setup->recoil.end)) {\n            frame_r = setup->aim.end;\n        } else if (\n            Anim_TestAbsFrameRange(frame_r, setup->aim.bend, setup->aim.end)) {\n            frame_r--;\n        }\n        if (m_SoundRight) {\n            M_FireSound(weapon->sample_num, true);\n            m_SoundRight = false;\n        }\n    } else {\n        if (Anim_TestAbsFrameRange(\n                frame_r, setup->aim.start, setup->aim.extend)) {\n            frame_r++;\n        } else if (frame_r == setup->aim.end) {\n            if (g_Input.action) {\n                angles[0] = lara->right_arm.rot.y + lara_item->rot.y;\n                angles[1] = lara->right_arm.rot.x;\n                if (weapon_type != LGT_DESERT_EAGLE\n                    && Gun_FireWeapon(\n                        weapon_type, lara->target, lara_item, angles)) {\n                    lara->right_arm.flash_gun = weapon->flash_time;\n                    Spawn_GunShell(weapon_type, true);\n                    Gun_Smoke_OnFire(weapon_type, true);\n                    if (!sound_already) {\n                        M_FireSound(weapon->sample_num, false);\n                    }\n                    sound_already = true;\n                    if (M_EnableFastSound(weapon_type)) {\n                        m_SoundRight = true;\n                    }\n                }\n                frame_r = setup->recoil.start;\n            } else if (m_SoundRight) {\n                M_FireSound(weapon->sample_num, true);\n                m_SoundRight = false;\n            }\n        } else if (\n            Anim_TestAbsFrameRange(\n                frame_r, setup->recoil.start, setup->recoil.end)) {\n            frame_r++;\n            if (frame_r == setup->recoil.start + weapon->recoil_frame) {\n                frame_r = setup->aim.end;\n            }\n            if (M_EnableFastSound(weapon_type)) {\n                M_FireSound(weapon->sample_num, false);\n                m_SoundRight = true;\n            }\n        }\n    }\n    M_SetArmInfo(&lara->right_arm, frame_r);\n\n    int16_t frame_l = lara->left_arm.frame_num;\n    if (!lara->left_arm.lock && (!g_Input.action || lara->target != nullptr)) {\n        if (Anim_TestAbsFrameRange(\n                frame_l, setup->recoil.start, setup->recoil.end)) {\n            frame_l = setup->aim.end;\n        } else if (\n            Anim_TestAbsFrameRange(frame_l, setup->aim.bend, setup->aim.end)) {\n            frame_l--;\n        }\n        if (m_SoundLeft) {\n            M_FireSound(weapon->sample_num, true);\n            m_SoundLeft = false;\n        }\n    } else if (\n        Anim_TestAbsFrameRange(frame_l, setup->aim.start, setup->aim.extend)) {\n        frame_l++;\n    } else if (frame_l == setup->aim.end) {\n        if (g_Input.action) {\n            angles[0] = lara->left_arm.rot.y + lara_item->rot.y;\n            angles[1] = lara->left_arm.rot.x;\n            if (Gun_FireWeapon(weapon_type, lara->target, lara_item, angles)) {\n                if (weapon_type == LGT_DESERT_EAGLE) {\n                    lara->right_arm.flash_gun = weapon->flash_time;\n                    Spawn_GunShell(weapon_type, true);\n                    Gun_Smoke_OnFire(weapon_type, true);\n                } else {\n                    lara->left_arm.flash_gun = weapon->flash_time;\n                    Spawn_GunShell(weapon_type, false);\n                    Gun_Smoke_OnFire(weapon_type, false);\n                }\n\n                if (!sound_already) {\n                    M_FireSound(weapon->sample_num, false);\n                }\n                if (M_EnableFastSound(weapon_type)) {\n                    m_SoundLeft = true;\n                }\n            }\n            frame_l = setup->recoil.start;\n        } else if (m_SoundLeft) {\n            M_FireSound(weapon->sample_num, true);\n            m_SoundLeft = false;\n        }\n    } else if (\n        Anim_TestAbsFrameRange(\n            frame_l, setup->recoil.start, setup->recoil.end)) {\n        frame_l++;\n        if (frame_l == setup->recoil.start + weapon->recoil_frame) {\n            frame_l = setup->aim.end;\n        }\n        if (M_EnableFastSound(weapon_type)) {\n            M_FireSound(weapon->sample_num, false);\n            m_SoundLeft = true;\n        }\n    }\n    M_SetArmInfo(&lara->left_arm, frame_l);\n}\n\nvoid Gun_Pistols_Control(const LARA_GUN_TYPE weapon_type)\n{\n    const WEAPON_INFO *const weapon = &g_Weapons[weapon_type];\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    Gun_GetNewTarget(weapon);\n    if (g_InputDB.change_target && g_Config.gameplay.enable_target_change) {\n        Gun_ChangeTarget(weapon);\n    }\n\n    Gun_AimWeapon(weapon, &lara->left_arm);\n    if (weapon_type != LGT_DESERT_EAGLE) {\n        Gun_AimWeapon(weapon, &lara->right_arm);\n    }\n\n    const bool lock_head = g_Config.gameplay.look_mode != LOOK_MODE_UNRESTRICTED\n        || g_Camera.type != CAM_LOOK;\n    if (weapon_type == LGT_DESERT_EAGLE) {\n        if (lara->left_arm.lock) {\n            lara->torso_rot.x = lara->left_arm.rot.x;\n            lara->torso_rot.y = lara->left_arm.rot.y;\n            if (lock_head) {\n                lara->head_rot.x = 0;\n                lara->head_rot.y = 0;\n            }\n        }\n    } else if (lara->left_arm.lock && !lara->right_arm.lock) {\n        if (lock_head) {\n            lara->head_rot.x = lara->left_arm.rot.x / 2;\n            lara->head_rot.y = lara->left_arm.rot.y / 2;\n        }\n        lara->torso_rot.x = lara->left_arm.rot.x / 2;\n        lara->torso_rot.y = lara->left_arm.rot.y / 2;\n    } else if (!lara->left_arm.lock && lara->right_arm.lock) {\n        if (lock_head) {\n            lara->head_rot.x = lara->right_arm.rot.x / 2;\n            lara->head_rot.y = lara->right_arm.rot.y / 2;\n        }\n        lara->torso_rot.x = lara->right_arm.rot.x / 2;\n        lara->torso_rot.y = lara->right_arm.rot.y / 2;\n    } else if (lara->right_arm.lock) {\n        if (lock_head) {\n            lara->head_rot.x =\n                (lara->right_arm.rot.x + lara->left_arm.rot.x) / 4;\n            lara->head_rot.y =\n                (lara->right_arm.rot.y + lara->left_arm.rot.y) / 4;\n        }\n        lara->torso_rot.x = (lara->right_arm.rot.x + lara->left_arm.rot.x) / 4;\n        lara->torso_rot.y = (lara->right_arm.rot.y + lara->left_arm.rot.y) / 4;\n    }\n\n    M_Animate(weapon_type);\n\n    if (lara->left_arm.flash_gun || lara->right_arm.flash_gun) {\n        Gun_AddDynamicLight();\n    }\n}\n\nvoid Gun_Pistols_Draw(const LARA_GUN_TYPE weapon_type)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    int16_t frame = lara->left_arm.frame_num + 1;\n    const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type);\n\n    if (!Anim_TestAbsFrameRange(frame, setup->undraw.start, setup->draw.end)) {\n        frame = setup->undraw.start;\n    } else if (Anim_TestAbsFrameEqual(frame, setup->draw.start)) {\n        Gun_Pistols_DrawMeshes(weapon_type);\n        Sound_Effect(SFX_LARA_DRAW, &Lara_GetItem()->pos, SPM_NORMAL);\n    } else if (Anim_TestAbsFrameEqual(frame, setup->draw.end)) {\n        Gun_Pistols_Ready(weapon_type);\n        frame = setup->aim.start;\n    }\n\n    M_SetArmInfo(&lara->right_arm, frame);\n    M_SetArmInfo(&lara->left_arm, frame);\n}\n\nvoid Gun_Pistols_Undraw(const LARA_GUN_TYPE weapon_type)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type);\n\n    int16_t frame_l = lara->left_arm.frame_num;\n    if (Anim_TestAbsFrameRange(\n            frame_l, setup->recoil.start, setup->recoil.end)) {\n        frame_l = setup->aim.end;\n    } else if (\n        Anim_TestAbsFrameRange(frame_l, setup->aim.bend, setup->aim.end)) {\n        lara->left_arm.rot.x -= lara->left_arm.rot.x / frame_l;\n        lara->left_arm.rot.y -= lara->left_arm.rot.y / frame_l;\n        frame_l--;\n    } else if (Anim_TestAbsFrameEqual(frame_l, setup->aim.start)) {\n        lara->left_arm.rot.x = 0;\n        lara->left_arm.rot.y = 0;\n        lara->left_arm.rot.z = 0;\n        frame_l = setup->draw.end;\n    } else if (Anim_TestAbsFrameEqual(frame_l, setup->draw.start)) {\n        Gun_Pistols_UndrawMeshLeft(weapon_type);\n        frame_l--;\n    } else if (\n        Anim_TestAbsFrameRange(frame_l, setup->undraw.bend, setup->draw.end)) {\n        frame_l--;\n    }\n    M_SetArmInfo(&lara->left_arm, frame_l);\n\n    int16_t frame_r = lara->right_arm.frame_num;\n    if (Anim_TestAbsFrameRange(\n            frame_r, setup->recoil.start, setup->recoil.end)) {\n        frame_r = setup->aim.end;\n    } else if (\n        Anim_TestAbsFrameRange(frame_r, setup->aim.bend, setup->aim.end)) {\n        lara->right_arm.rot.x -= lara->right_arm.rot.x / frame_r;\n        lara->right_arm.rot.y -= lara->right_arm.rot.y / frame_r;\n        frame_r--;\n    } else if (Anim_TestAbsFrameEqual(frame_r, setup->aim.start)) {\n        lara->right_arm.rot.x = 0;\n        lara->right_arm.rot.y = 0;\n        lara->right_arm.rot.z = 0;\n        frame_r = setup->draw.end;\n    } else if (Anim_TestAbsFrameEqual(frame_r, setup->draw.start)) {\n        Gun_Pistols_UndrawMeshRight(weapon_type);\n        frame_r--;\n    } else if (\n        Anim_TestAbsFrameRange(frame_r, setup->undraw.bend, setup->draw.end)) {\n        frame_r--;\n    }\n    M_SetArmInfo(&lara->right_arm, frame_r);\n\n    if (Anim_TestAbsFrameEqual(frame_l, setup->undraw.start)\n        && Anim_TestAbsFrameEqual(frame_r, setup->undraw.start)) {\n        lara->gun_status = LGS_ARMLESS;\n        lara->left_arm.lock = 0;\n        lara->right_arm.lock = 0;\n        lara->left_arm.frame_num = setup->aim.start;\n        lara->right_arm.frame_num = setup->aim.start;\n        lara->target = nullptr;\n    }\n\n    if (!g_Input.look || g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) {\n        lara->head_rot.x = (lara->left_arm.rot.x + lara->right_arm.rot.x) / 4;\n        lara->head_rot.y = (lara->left_arm.rot.y + lara->right_arm.rot.y) / 4;\n        lara->torso_rot.x = lara->head_rot.x;\n        lara->torso_rot.y = lara->head_rot.y;\n    }\n}\n\nvoid Gun_Pistols_Ready(const LARA_GUN_TYPE weapon_type)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const M_FRAME_SETUP *const setup = M_GetSetup(weapon_type);\n    lara->gun_status = LGS_READY;\n    lara->target = nullptr;\n\n    const OBJECT *const obj = Object_Get(O_LARA_PISTOLS);\n    lara->left_arm.frame_base = obj->frame_base;\n    lara->left_arm.frame_num = setup->aim.start;\n    lara->left_arm.lock = 0;\n    lara->left_arm.rot.x = 0;\n    lara->left_arm.rot.y = 0;\n    lara->left_arm.rot.z = 0;\n    lara->right_arm.frame_base = obj->frame_base;\n    lara->right_arm.frame_num = setup->aim.start;\n    lara->right_arm.lock = 0;\n    lara->right_arm.rot.x = 0;\n    lara->right_arm.rot.y = 0;\n    lara->right_arm.rot.z = 0;\n\n    if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) {\n        lara->head_rot.x = 0;\n        lara->head_rot.y = 0;\n        lara->torso_rot.x = 0;\n        lara->torso_rot.y = 0;\n    }\n}\n\nvoid Gun_Pistols_DrawMeshes(const LARA_GUN_TYPE weapon_type)\n{\n    Gun_SetLaraHandRMesh(weapon_type);\n    Gun_SetLaraHolsterRMesh(LGT_UNARMED);\n    if (weapon_type != LGT_DESERT_EAGLE) {\n        Gun_SetLaraHandLMesh(weapon_type);\n        Gun_SetLaraHolsterLMesh(LGT_UNARMED);\n    }\n}\n\nvoid Gun_Pistols_UndrawMeshLeft(const LARA_GUN_TYPE weapon_type)\n{\n    if (weapon_type != LGT_DESERT_EAGLE) {\n        Gun_SetLaraHandLMesh(LGT_UNARMED);\n        Gun_SetLaraHolsterLMesh(weapon_type);\n        Sound_Effect(SFX_LARA_HOLSTER, &Lara_GetItem()->pos, SPM_NORMAL);\n    }\n}\n\nvoid Gun_Pistols_UndrawMeshRight(const LARA_GUN_TYPE weapon_type)\n{\n    Gun_SetLaraHandRMesh(LGT_UNARMED);\n    Gun_SetLaraHolsterRMesh(weapon_type);\n    Sound_Effect(SFX_LARA_HOLSTER, &Lara_GetItem()->pos, SPM_NORMAL);\n}\n"
  },
  {
    "path": "src/trx/game/gun/pistols.h",
    "content": "#pragma once\n\n#include <trx/game/lara/types.h>\n\nvoid Gun_Pistols_Control(LARA_GUN_TYPE weapon_type);\n\nvoid Gun_Pistols_Draw(LARA_GUN_TYPE weapon_type);\nvoid Gun_Pistols_Undraw(LARA_GUN_TYPE weapon_type);\nvoid Gun_Pistols_Ready(LARA_GUN_TYPE weapon_type);\n\nvoid Gun_Pistols_DrawMeshes(LARA_GUN_TYPE weapon_type);\nvoid Gun_Pistols_UndrawMeshLeft(LARA_GUN_TYPE weapon_type);\nvoid Gun_Pistols_UndrawMeshRight(LARA_GUN_TYPE weapon_type);\n"
  },
  {
    "path": "src/trx/game/gun/rifle.c",
    "content": "#include <trx/game/gun/rifle.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/gun/common.h>\n#include <trx/game/gun/control.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/smashing.h>\n#include <trx/game/gun/smoke.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_SHOTGUN_PELLET_SCATTER (DEG_1 * 20) // = 3640\n#define M_HARPOON_BOLT_SPEED_TR12 150\n#define M_HARPOON_BOLT_SPEED_TR3 256\n#define M_GRENADE_SPEED 200\n\ntypedef enum {\n    LA_G_AIM = 0,\n    LA_G_DRAW = 1,\n    LA_G_RECOIL = 2,\n    LA_G_UNDRAW = 3,\n    LA_G_UNAIM = 4,\n    LA_G_RELOAD = 5,\n    LA_G_UAIM = 6,\n    LA_G_UUNAIM = 7,\n    LA_G_URECOIL = 8,\n    LA_G_SURF_UNDRAW = 9,\n} M_ANIM;\n\nstatic bool m_M16Firing = false;\nstatic bool m_ReloadHarpoon = false;\n\nstatic void M_SetTR3ProjectileShade(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative\n    // shade forces the dynamic/smoothed lighting path.\n    item->shade.value_1 = -1;\n    item->shade.value_2 = -1;\n}\n\nstatic M_ANIM M_GetReadyAnim(const LARA_GUN_TYPE weapon_type)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    switch (weapon_type) {\n    case LGT_HARPOON:\n        return lara->water_status == LWS_UNDERWATER ? LA_G_UAIM : LA_G_AIM;\n    case LGT_GRENADE:\n        return LA_G_DRAW;\n    default:\n        return LA_G_AIM;\n    }\n}\n\nstatic void M_AnimateGun(ITEM *const item)\n{\n    // While the item is drawn in Lara_Draw, it needs a world position for\n    // sound effect commands in Item_Animate.\n    const ITEM *const lara_item = Lara_GetItem();\n    item->pos.x = lara_item->pos.x;\n    item->pos.y = lara_item->pos.y - LARA_HEIGHT;\n    item->pos.z = lara_item->pos.z;\n    Item_Animate(item);\n}\n\nstatic void M_Ready(const LARA_GUN_TYPE weapon_type)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->gun_status = LGS_READY;\n    lara->target = nullptr;\n\n    const OBJECT *const obj = Object_Get(Gun_GetWeaponAnim(weapon_type));\n    lara->left_arm.frame_base = obj->frame_base;\n    lara->left_arm.frame_num = LF_G_AIM_START;\n    lara->left_arm.lock = 0;\n    lara->left_arm.rot.x = 0;\n    lara->left_arm.rot.y = 0;\n    lara->left_arm.rot.z = 0;\n\n    lara->right_arm.frame_base = obj->frame_base;\n    lara->right_arm.frame_num = LF_G_AIM_START;\n    lara->right_arm.lock = 0;\n    lara->right_arm.rot.x = 0;\n    lara->right_arm.rot.y = 0;\n    lara->right_arm.rot.z = 0;\n\n    if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) {\n        lara->head_rot.x = 0;\n        lara->head_rot.y = 0;\n        lara->torso_rot.x = 0;\n        lara->torso_rot.y = 0;\n    }\n}\n\nstatic void M_FireGeneric(const LARA_GUN_TYPE weapon_type)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    bool fired = false;\n    int16_t angles[2] = {\n        lara->left_arm.rot.y + lara_item->rot.y,\n        lara->left_arm.rot.x,\n    };\n    if (g_TRVersion == 3 && !lara->left_arm.lock) {\n        angles[0] += lara->torso_rot.y;\n        angles[1] += lara->torso_rot.x;\n    }\n\n    const int32_t clip = Gun_GetAmmoClipCount(weapon_type);\n    for (int32_t i = 0; i < clip; i++) {\n        int16_t dangles[2] = {\n            angles[0]\n                + M_SHOTGUN_PELLET_SCATTER * (Random_GetControl() - 0x4000)\n                    / 0x10000,\n            angles[1]\n                + M_SHOTGUN_PELLET_SCATTER * (Random_GetControl() - 0x4000)\n                    / 0x10000,\n        };\n        if (Gun_FireWeapon(weapon_type, lara->target, lara_item, dangles)) {\n            fired = true;\n        }\n    }\n\n    if (fired) {\n        lara->right_arm.flash_gun = g_Weapons[weapon_type].flash_time;\n        Gun_Smoke_OnFire(weapon_type, true);\n        Sound_Effect(\n            g_Weapons[weapon_type].sample_num, &lara_item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_FireM16(const bool running, const LARA_GUN_TYPE weapon_type)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    int16_t angles[2] = {\n        lara->left_arm.rot.y + lara_item->rot.y,\n        lara->left_arm.rot.x,\n    };\n    if (g_TRVersion == 3 && !lara->left_arm.lock) {\n        angles[0] += lara->torso_rot.y;\n        angles[1] += lara->torso_rot.x;\n    }\n\n    if (g_Config.gameplay.fix_m16_accuracy && running) {\n        g_Weapons[weapon_type].shot_accuracy = DEG_1 * 12;\n        g_Weapons[weapon_type].damage = 1;\n    } else {\n        g_Weapons[weapon_type].shot_accuracy = DEG_1 * 4;\n        g_Weapons[weapon_type].damage = 3;\n    }\n\n    if (Gun_FireWeapon(weapon_type, lara->target, lara_item, angles)) {\n        lara->right_arm.flash_gun = g_Weapons[weapon_type].flash_time;\n        Spawn_GunShell(weapon_type, true);\n        Gun_Smoke_OnFire(weapon_type, true);\n    }\n}\n\nstatic void M_FireHarpoon(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->harpoon_ammo.ammo <= 0) {\n        goto finish;\n    }\n\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        goto finish;\n    }\n\n    const WEAPON_INFO *const weapon = &g_Weapons[LGT_HARPOON];\n    const GAME_VECTOR origin = {\n        .pos = {\n            .x = lara_item->pos.x,\n            .y = lara_item->pos.y - weapon->gun_height,\n            .z = lara_item->pos.z,\n        },\n        .room_num = lara_item->room_num,\n    };\n\n    ITEM *const projectile_item = Item_Get(item_num);\n    projectile_item->object_id = O_HARPOON_BOLT;\n    projectile_item->room_num = lara_item->room_num;\n\n    XYZ_32 offset = {\n        .x = -2,\n        .y = 373,\n        .z = 77,\n    };\n\n    Lara_GetJointAbsPosition(&offset, LM_HAND_R);\n    projectile_item->pos = offset;\n    projectile_item->interp.prev.pos = projectile_item->pos;\n    Item_Initialise(item_num);\n\n    if (lara->target != nullptr) {\n        GAME_VECTOR lara_vec;\n        Gun_FindTargetPoint(lara->target, &lara_vec);\n        const int32_t dx = lara_vec.pos.x - projectile_item->pos.x;\n        const int32_t dz = lara_vec.pos.z - projectile_item->pos.z;\n        const int32_t dy = lara_vec.pos.y - projectile_item->pos.y;\n        const int32_t dxz = Math_Sqrt(SQUARE(dx) + SQUARE(dz));\n        projectile_item->rot.y = Math_Atan(dz, dx);\n        projectile_item->rot.x = -Math_Atan(dxz, dy);\n        projectile_item->rot.z = 0;\n    } else {\n        if (g_TRVersion == 3) {\n            projectile_item->rot.x = lara->torso_rot.x + lara_item->rot.x;\n            projectile_item->rot.y = lara->torso_rot.y + lara_item->rot.y;\n        } else {\n            projectile_item->rot.x = lara->left_arm.rot.x + lara_item->rot.x;\n            projectile_item->rot.y = lara->left_arm.rot.y + lara_item->rot.y;\n        }\n        projectile_item->rot.z = 0;\n    }\n\n    const int32_t bolt_speed =\n        g_TRVersion == 3 ? M_HARPOON_BOLT_SPEED_TR3 : M_HARPOON_BOLT_SPEED_TR12;\n    projectile_item->fall_speed =\n        (-bolt_speed * Math_Sin(projectile_item->rot.x)) >> W2V_SHIFT;\n    projectile_item->speed =\n        (bolt_speed * Math_Cos(projectile_item->rot.x)) >> W2V_SHIFT;\n\n    if (g_TRVersion == 3) {\n        M_SetTR3ProjectileShade(projectile_item);\n        projectile_item->hit_points = 256;\n    }\n\n    Item_AddActive(item_num);\n    projectile_item->status = IS_ACTIVE;\n\n    Gun_SmashItems(\n        origin,\n        (GAME_VECTOR) {\n            .pos = projectile_item->pos,\n            .room_num = projectile_item->room_num,\n        },\n        nullptr, projectile_item->object_id);\n\n    lara->harpoon_ammo.ammo--;\n    Stats_AddAmmoUsed();\n\nfinish:\n    const int32_t recoil = g_Config.gameplay.harpoon_recoil;\n    const bool is_ngplus = Game_IsBonusFlagSet(GBF_NGPLUS);\n    if (recoil <= 0) {\n        if (is_ngplus) {\n            lara->harpoon_ammo.ammo++;\n        }\n    } else if ((lara->harpoon_ammo.ammo % recoil) == 0) {\n        if (is_ngplus) {\n            lara->harpoon_ammo.ammo += recoil;\n        }\n        m_ReloadHarpoon = lara->harpoon_ammo.ammo > 0;\n    }\n}\n\nstatic void M_FireGrenade(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara->grenade_ammo.ammo <= 0) {\n        return;\n    }\n    const WEAPON_INFO *const weapon = &g_Weapons[LGT_GRENADE];\n    const GAME_VECTOR origin = {\n        .pos = {\n            .x = lara_item->pos.x,\n            .y = lara_item->pos.y - weapon->gun_height,\n            .z = lara_item->pos.z,\n        },\n        .room_num = lara_item->room_num,\n    };\n\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const projectile_item = Item_Get(item_num);\n    projectile_item->object_id = O_GRENADE;\n    projectile_item->room_num = lara_item->room_num;\n\n    XYZ_32 offset =\n        g_TRVersion == 3 ? (XYZ_32) { 0, 276, 80 } : (XYZ_32) { -2, 373, 77 };\n    Lara_GetJointAbsPosition(&offset, LM_HAND_R);\n    projectile_item->pos = offset;\n    projectile_item->interp.prev.pos = projectile_item->pos;\n    Item_Initialise(item_num);\n\n    int16_t room_num = projectile_item->room_num;\n    const SECTOR *const sector = Room_GetSector(origin.pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, origin.pos);\n    if (height < origin.pos.y) {\n        projectile_item->pos = (XYZ_32) {\n            .x = lara_item->pos.x,\n            .y = origin.pos.y,\n            .z = lara_item->pos.z,\n        };\n    }\n\n    Room_GetSector(projectile_item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    projectile_item->rot.x = lara->left_arm.rot.x + lara_item->rot.x;\n    projectile_item->rot.y = lara->left_arm.rot.y + lara_item->rot.y;\n    projectile_item->rot.z = 0;\n    if (g_TRVersion == 3 && !lara->left_arm.lock) {\n        projectile_item->rot.x += lara->torso_rot.x;\n        projectile_item->rot.y += lara->torso_rot.y;\n    }\n\n    if (g_Config.gameplay.enable_bouncy_grenades) {\n        // TR3 grenades use a timed fuse and bounce/roll physics, so use speed\n        // as horizontal velocity magnitude and fall_speed as vertical velocity.\n        projectile_item->speed = 128;\n        projectile_item->fall_speed =\n            -(projectile_item->speed * Math_Sin(projectile_item->rot.x))\n            >> W2V_SHIFT;\n        projectile_item->current_anim_state = projectile_item->rot.x;\n        projectile_item->goal_anim_state = projectile_item->rot.y;\n        projectile_item->required_anim_state = 0;\n        projectile_item->hit_points = 120;\n    } else {\n        projectile_item->speed = M_GRENADE_SPEED;\n        projectile_item->fall_speed = 0;\n    }\n\n    Item_AddActive(item_num);\n    projectile_item->status = IS_ACTIVE;\n\n    Gun_SmashItems(\n        origin,\n        (GAME_VECTOR) {\n            .pos = projectile_item->pos,\n            .room_num = projectile_item->room_num,\n        },\n        nullptr, projectile_item->object_id);\n\n    if (!Game_IsBonusFlagSet(GBF_NGPLUS)) {\n        lara->grenade_ammo.ammo--;\n    }\n    Stats_AddAmmoUsed();\n\n    Gun_Smoke_OnFire(LGT_GRENADE, true);\n}\n\nstatic void M_FireRocket(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara->rocket_ammo.ammo <= 0) {\n        return;\n    }\n    const WEAPON_INFO *const weapon = &g_Weapons[LGT_ROCKET];\n    const GAME_VECTOR origin = {\n        .pos = {\n        .x = lara_item->pos.x,\n        .y = lara_item->pos.y - weapon->gun_height,\n        .z = lara_item->pos.z,\n        },\n        .room_num = lara_item->room_num,\n    };\n\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const projectile_item = Item_Get(item_num);\n    projectile_item->object_id = O_ROCKET;\n    projectile_item->room_num = lara_item->room_num;\n\n    XYZ_32 offset = {\n        .x = 0,\n        .y = 180,\n        .z = 72,\n    };\n    Lara_GetJointAbsPosition(&offset, LM_HAND_R);\n    projectile_item->pos = offset;\n    projectile_item->interp.prev.pos = projectile_item->pos;\n    Item_Initialise(item_num);\n\n    projectile_item->rot.x = lara->left_arm.rot.x + lara_item->rot.x;\n    projectile_item->rot.y = lara->left_arm.rot.y + lara_item->rot.y;\n    projectile_item->rot.z = 0;\n    if (!lara->left_arm.lock) {\n        projectile_item->rot.x += lara->torso_rot.x;\n        projectile_item->rot.y += lara->torso_rot.y;\n    }\n\n    projectile_item->speed = 16;\n    Item_AddActive(item_num);\n    projectile_item->status = IS_ACTIVE;\n\n    Gun_SmashItems(\n        origin,\n        (GAME_VECTOR) {\n            .pos = projectile_item->pos,\n            .room_num = projectile_item->room_num,\n        },\n        nullptr, projectile_item->object_id);\n\n    if (!Game_IsBonusFlagSet(GBF_NGPLUS)) {\n        lara->rocket_ammo.ammo--;\n    }\n    Stats_AddAmmoUsed();\n\n    if (g_TRVersion >= 3) {\n        Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, 0x5000000 | SPM_PITCH);\n    }\n\n    Gun_Smoke_OnFire(LGT_ROCKET, true);\n\n    if (g_TRVersion == 3) {\n        M_SetTR3ProjectileShade(projectile_item);\n        const XYZ_32 back_128 = XYZ_32_FromYawPitch(\n            projectile_item->rot.y, projectile_item->rot.x, -128);\n        for (int32_t i = 0; i < 8; i++) {\n            const int32_t dist = -(Random_GetControl() & 0x7FF);\n            const XYZ_32 back_vel = XYZ_32_FromYawPitch(\n                projectile_item->rot.y, projectile_item->rot.x, dist);\n            Sparks_TriggerRocketFlame(\n                back_128,\n                (XYZ_32) {\n                    .x = back_vel.x - back_128.x,\n                    .y = back_vel.y - back_128.y,\n                    .z = back_vel.z - back_128.z,\n                },\n                item_num, projectile_item->room_num);\n        }\n    }\n}\n\nstatic void M_Fire(const LARA_GUN_TYPE weapon_type, const bool running)\n{\n    switch (weapon_type) {\n    case LGT_HARPOON:\n        M_FireHarpoon();\n        break;\n    case LGT_GRENADE:\n        if (!running) {\n            M_FireGrenade();\n        }\n        break;\n    case LGT_ROCKET:\n        M_FireRocket();\n        break;\n    case LGT_M16:\n    case LGT_MP5:\n        M_FireM16(running, weapon_type);\n        break;\n    default:\n        if (!running) {\n            M_FireGeneric(weapon_type);\n        }\n        break;\n    }\n}\n\nstatic void M_PlayMachineGunSound(\n    const LARA_GUN_TYPE weapon_type, const bool stopping)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (weapon_type == LGT_M16) {\n        Sound_Effect(\n            stopping ? SFX_M16_STOP : SFX_M16_FIRE, &lara_item->pos,\n            SPM_NORMAL);\n        return;\n    }\n\n    // The MP5 uses a high-pitched explosion when either firing or stopping.\n    // This is intentionally omitted in TR1/2 due to the sample's quality when\n    // played in rapid succession.\n    if (g_TRVersion >= 3) {\n        Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, 0x5000000 | SPM_PITCH);\n    }\n    if (!stopping) {\n        Sound_Effect(SFX_MP5_FIRE, &lara_item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Animate(const LARA_GUN_TYPE weapon_type)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    const bool running = (weapon_type == LGT_M16 || weapon_type == LGT_MP5)\n        && lara_item->speed != 0;\n    ITEM *const item = Item_Get(lara->gun_item_num);\n\n    switch (item->current_anim_state) {\n    case LA_G_AIM:\n        m_M16Firing = false;\n        if (m_ReloadHarpoon) {\n            item->goal_anim_state = LA_G_RELOAD;\n            m_ReloadHarpoon = false;\n        } else if (lara->water_status == LWS_UNDERWATER || running) {\n            item->goal_anim_state = LA_G_UAIM;\n        } else if (\n            (g_Input.action && lara->target == nullptr)\n            || lara->left_arm.lock) {\n            item->goal_anim_state = LA_G_RECOIL;\n        } else {\n            item->goal_anim_state = LA_G_UNAIM;\n        }\n        break;\n\n    case LA_G_UAIM:\n        m_M16Firing = false;\n        if (m_ReloadHarpoon) {\n            item->goal_anim_state = LA_G_RELOAD;\n            m_ReloadHarpoon = false;\n        } else if (lara->water_status != LWS_UNDERWATER && !running) {\n            item->goal_anim_state = LA_G_AIM;\n        } else if (\n            (g_Input.action && lara->target == nullptr)\n            || lara->left_arm.lock) {\n            item->goal_anim_state = LA_G_URECOIL;\n        } else {\n            item->goal_anim_state = LA_G_UUNAIM;\n        }\n        break;\n\n    case LA_G_RECOIL:\n        if (Item_TestFrameEqual(item, 0)) {\n            item->goal_anim_state = LA_G_UNAIM;\n            if (lara->water_status != LWS_UNDERWATER && !running\n                && !m_ReloadHarpoon) {\n                if (g_Input.action) {\n                    if (lara->target == nullptr || lara->left_arm.lock) {\n                        M_Fire(weapon_type, false);\n                        if (weapon_type == LGT_M16 || weapon_type == LGT_MP5) {\n                            M_PlayMachineGunSound(weapon_type, false);\n                            m_M16Firing = true;\n                        }\n                        item->goal_anim_state = LA_G_RECOIL;\n                    }\n                } else if (lara->left_arm.lock) {\n                    item->goal_anim_state = LA_G_AIM;\n                }\n            }\n\n            if (item->goal_anim_state != LA_G_RECOIL && m_M16Firing) {\n                M_PlayMachineGunSound(weapon_type, true);\n                m_M16Firing = false;\n            }\n        } else if (m_M16Firing) {\n            M_PlayMachineGunSound(weapon_type, false);\n        } else if (\n            weapon_type == LGT_SHOTGUN && !g_Input.action\n            && !lara->left_arm.lock) {\n            item->goal_anim_state = LA_G_UNAIM;\n        }\n\n        if (weapon_type == LGT_SHOTGUN && Item_TestFrameEqual(item, 12)) {\n            Spawn_GunShell(LGT_SHOTGUN, true);\n        }\n        break;\n\n    case LA_G_URECOIL:\n        if (Item_TestFrameEqual(item, 0)) {\n            item->goal_anim_state = LA_G_UUNAIM;\n            if ((lara->water_status == LWS_UNDERWATER || running)\n                && !m_ReloadHarpoon) {\n                if (g_Input.action) {\n                    if (lara->target == nullptr || lara->left_arm.lock) {\n                        M_Fire(weapon_type, true);\n                        item->goal_anim_state = LA_G_URECOIL;\n                    }\n                } else if (lara->left_arm.lock) {\n                    item->goal_anim_state = LA_G_UAIM;\n                }\n            }\n        }\n\n        if (item->goal_anim_state == LA_G_URECOIL\n            && (weapon_type == LGT_M16 || weapon_type == LGT_MP5)) {\n            M_PlayMachineGunSound(weapon_type, false);\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    M_AnimateGun(item);\n    lara->left_arm.anim_num = item->anim_num;\n    lara->left_arm.frame_base = Item_GetAnim(item)->frame_ptr;\n    lara->left_arm.frame_num = Item_GetRelativeFrame(item);\n    lara->right_arm.anim_num = item->anim_num;\n    lara->right_arm.frame_base = Item_GetAnim(item)->frame_ptr;\n    lara->right_arm.frame_num = Item_GetRelativeFrame(item);\n}\n\nvoid Gun_Rifle_Control(const LARA_GUN_TYPE weapon_type)\n{\n    const WEAPON_INFO *const weapon = &g_Weapons[weapon_type];\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    Gun_GetNewTarget(weapon);\n    if (g_InputDB.change_target && g_Config.gameplay.enable_target_change) {\n        Gun_ChangeTarget(weapon);\n    }\n\n    Gun_AimWeapon(weapon, &lara->left_arm);\n\n    if (lara->left_arm.lock) {\n        lara->torso_rot.x = lara->left_arm.rot.x;\n        lara->torso_rot.y = lara->left_arm.rot.y;\n        if (g_Config.gameplay.look_mode != LOOK_MODE_UNRESTRICTED\n            || g_Camera.type != CAM_LOOK) {\n            lara->head_rot.x = 0;\n            lara->head_rot.y = 0;\n        }\n    }\n\n    M_Animate(weapon_type);\n\n    if (lara->right_arm.flash_gun\n        && (weapon_type == LGT_SHOTGUN || weapon_type == LGT_M16\n            || weapon_type == LGT_MP5)) {\n        Gun_AddDynamicLight();\n    }\n}\n\nvoid Gun_Rifle_Draw(const LARA_GUN_TYPE weapon_type)\n{\n    ITEM *item;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const WEAPON_INFO *const weapon = &g_Weapons[weapon_type];\n\n    if (lara->gun_item_num != NO_ITEM) {\n        item = Item_Get(lara->gun_item_num);\n    } else {\n        lara->gun_item_num = Item_Create();\n        item = Item_Get(lara->gun_item_num);\n        item->object_id = Gun_GetWeaponAnim(weapon_type);\n        Item_SwitchToAnim(item, weapon->equip_anim_idx, 0);\n        item->goal_anim_state = LA_G_DRAW;\n        item->current_anim_state = LA_G_DRAW;\n        item->status = IS_ACTIVE;\n        item->room_num = NO_ROOM;\n        const OBJECT *const obj = Object_Get(item->object_id);\n        lara->right_arm.frame_base = obj->frame_base;\n        lara->left_arm.frame_base = obj->frame_base;\n    }\n\n    M_AnimateGun(item);\n\n    if (item->current_anim_state == LA_G_AIM\n        || item->current_anim_state == LA_G_UAIM) {\n        M_Ready(weapon_type);\n    } else if (Item_TestFrameEqual(item, weapon->draw_frame)) {\n        Gun_Rifle_DrawMeshes(weapon_type);\n    } else if (lara->water_status == LWS_UNDERWATER) {\n        item->goal_anim_state = LA_G_UAIM;\n    }\n\n    lara->left_arm.anim_num = item->anim_num;\n    lara->left_arm.frame_base = Item_GetAnim(item)->frame_ptr;\n    lara->left_arm.frame_num = Item_GetRelativeFrame(item);\n    lara->right_arm.anim_num = item->anim_num;\n    lara->right_arm.frame_base = Item_GetAnim(item)->frame_ptr;\n    lara->right_arm.frame_num = Item_GetRelativeFrame(item);\n}\n\nvoid Gun_Rifle_Undraw(const LARA_GUN_TYPE weapon_type)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    ITEM *const item = Item_Get(lara->gun_item_num);\n    const ANIM *const anim = Item_GetAnim(item);\n    if (lara->water_status == LWS_SURFACE\n        && Anim_HasChange(anim, LA_G_SURF_UNDRAW)) {\n        item->goal_anim_state = LA_G_SURF_UNDRAW;\n    } else {\n        item->goal_anim_state = LA_G_UNDRAW;\n    }\n    M_AnimateGun(item);\n\n    const WEAPON_INFO *const weapon = &g_Weapons[weapon_type];\n    if (item->status == IS_DEACTIVATED) {\n        Item_Kill(lara->gun_item_num);\n        lara->gun_item_num = NO_ITEM;\n        lara->gun_status = LGS_ARMLESS;\n        lara->target = nullptr;\n        lara->left_arm.frame_num = 0;\n        lara->left_arm.lock = 0;\n        lara->right_arm.frame_num = 0;\n        lara->right_arm.lock = 0;\n    } else if (\n        item->current_anim_state == LA_G_UNDRAW\n        && Item_TestFrameEqual(item, weapon->undraw_frame)) {\n        Gun_Rifle_UndrawMeshes(weapon_type);\n    }\n\n    if (!g_Input.look || g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) {\n        lara->head_rot.x = 0;\n        lara->head_rot.y = 0;\n        lara->torso_rot.x += lara->torso_rot.x / -2;\n        lara->torso_rot.y += lara->torso_rot.y / -2;\n    }\n    lara->left_arm.anim_num = item->anim_num;\n    lara->left_arm.frame_base = Item_GetAnim(item)->frame_ptr;\n    lara->left_arm.frame_num = Item_GetRelativeFrame(item);\n    lara->right_arm.anim_num = item->anim_num;\n    lara->right_arm.frame_base = Item_GetAnim(item)->frame_ptr;\n    lara->right_arm.frame_num = Item_GetRelativeFrame(item);\n}\n\nvoid Gun_Rifle_DrawMeshes(const LARA_GUN_TYPE weapon_type)\n{\n    Gun_SetLaraHandLMesh(LGT_UNARMED);\n    Gun_SetLaraHandRMesh(weapon_type);\n    Gun_SetLaraBackMesh(LGT_UNARMED);\n}\n\nvoid Gun_Rifle_UndrawMeshes(const LARA_GUN_TYPE weapon_type)\n{\n    Gun_SetLaraHandLMesh(LGT_UNARMED);\n    Gun_SetLaraHandRMesh(LGT_UNARMED);\n    Gun_SetLaraBackMesh(weapon_type);\n}\n\nvoid Gun_Rifle_EnsureReady(const LARA_GUN_TYPE weapon_type)\n{\n    Gun_Rifle_Draw(weapon_type);\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const item = Item_Get(lara->gun_item_num);\n    const int16_t goal_anim = M_GetReadyAnim(weapon_type);\n\n    do {\n        Gun_Rifle_Draw(weapon_type);\n    } while (Item_GetRelativeAnim(item) != goal_anim);\n}\n"
  },
  {
    "path": "src/trx/game/gun/rifle.h",
    "content": "#pragma once\n\n#include <trx/game/lara/types.h>\n\nvoid Gun_Rifle_Control(LARA_GUN_TYPE weapon_type);\nvoid Gun_Rifle_Draw(LARA_GUN_TYPE weapon_type);\nvoid Gun_Rifle_Undraw(LARA_GUN_TYPE weapon_type);\n\nvoid Gun_Rifle_DrawMeshes(LARA_GUN_TYPE weapon_type);\nvoid Gun_Rifle_UndrawMeshes(LARA_GUN_TYPE weapon_type);\nvoid Gun_Rifle_EnsureReady(LARA_GUN_TYPE weapon_type);\n"
  },
  {
    "path": "src/trx/game/gun/smashing.c",
    "content": "#include <trx/game/gun/smashing.h>\n\n#include <trx/game/collision/los.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/objects/vars.h>\n\n// TODO: meh\nextern void Smashable_Smash(int16_t item_num);\n\nGUN_SMASH_POLICY Gun_GetSmashPolicy(const ITEM *const item)\n{\n    if (Object_IsType(item->object_id, g_ShatterableObjects)) {\n        return GUN_SMASH_POLICY_CONTINUE;\n    }\n    if (Object_IsType(item->object_id, g_SmashableObjects)) {\n        return GUN_SMASH_POLICY_STOP;\n    }\n    if (Object_IsType(item->object_id, g_HeavyShatterableObjects)) {\n        return GUN_SMASH_POLICY_HEAVY;\n    }\n    return GUN_SMASH_POLICY_NONE;\n}\n\nvoid Gun_SmashItem(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    switch (item->object_id) {\n    case O_SMASH_OBJECT_1:\n    case O_SMASH_OBJECT_4:\n        Smashable_Smash(item_num);\n        break;\n\n    case O_BELL:\n    case O_CARCASS:\n    case O_FUSE_BOX:\n    case O_SMASH_OBJECT_2:\n    case O_SMASH_OBJECT_3:\n        if (item->status != IS_ACTIVE) {\n            item->status = IS_ACTIVE;\n            Item_AddActive(item_num);\n        }\n        break;\n\n    case O_SCION_ITEM_3:\n        Gun_HitTarget(item, nullptr, nullptr, item->hit_points);\n        break;\n\n    default:\n        break;\n    }\n}\n\nPROJECTILE_HIT Gun_SmashItems(\n    const GAME_VECTOR start, const GAME_VECTOR target,\n    XYZ_32 *const out_hit_pos, const OBJECT_ID missile_obj_id)\n{\n    int32_t hits = 0;\n    int16_t last_item_num = NO_ITEM;\n    const bool is_heavy_missile =\n        Object_IsType(missile_obj_id, g_HeavyMissileObjects);\n    while (true) {\n        const int16_t item_num = LOS_CheckSmashable(start, target, out_hit_pos);\n        if (item_num == NO_ITEM || item_num == last_item_num) {\n            break;\n        }\n        last_item_num = item_num;\n\n        const ITEM *const item = Item_Get(item_num);\n        const GUN_SMASH_POLICY policy = Gun_GetSmashPolicy(item);\n        switch (policy) {\n        case GUN_SMASH_POLICY_HEAVY:\n            if (is_heavy_missile) {\n                Gun_SmashItem(item_num);\n            }\n            return PROJECTILE_HIT_STOP;\n        case GUN_SMASH_POLICY_STOP:\n            Gun_SmashItem(item_num);\n            return PROJECTILE_HIT_STOP;\n        case GUN_SMASH_POLICY_CONTINUE:\n            Gun_SmashItem(item_num);\n            hits++;\n            break;\n        default:\n            break;\n        }\n    }\n    return hits > 0 ? PROJECTILE_HIT_SHATTER : PROJECTILE_HIT_NONE;\n}\n"
  },
  {
    "path": "src/trx/game/gun/smashing.h",
    "content": "#pragma once\n\n#include <trx/game/items.h>\n\ntypedef enum {\n    PROJECTILE_HIT_NONE,\n    PROJECTILE_HIT_SHATTER, // some objects were shattered\n    PROJECTILE_HIT_STOP, // a hard object (eg a bell) has been hit\n} PROJECTILE_HIT;\n\ntypedef enum {\n    GUN_SMASH_POLICY_NONE,\n    GUN_SMASH_POLICY_CONTINUE,\n    GUN_SMASH_POLICY_STOP,\n    GUN_SMASH_POLICY_HEAVY,\n} GUN_SMASH_POLICY;\n\nGUN_SMASH_POLICY Gun_GetSmashPolicy(const ITEM *item);\n\nvoid Gun_SmashItem(int16_t item_num);\n\nPROJECTILE_HIT Gun_SmashItems(\n    GAME_VECTOR start, GAME_VECTOR target, XYZ_32 *out_hit_pos,\n    OBJECT_ID missile_obj_id);\n"
  },
  {
    "path": "src/trx/game/gun/smoke.c",
    "content": "#include <trx/game/gun/smoke.h>\n\n#include <trx/game/gun/vars.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/misc.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\nstatic XYZ_32 M_GetHandAbsPosition(const LARA_MESH hand, XYZ_32 offset)\n{\n    Lara_GetMeshPos(hand, &offset);\n    return offset;\n}\n\nstatic XYZ_32 M_GetMuzzleOffset(\n    const LARA_GUN_TYPE weapon_type, const bool is_right_hand)\n{\n    return is_right_hand ? g_Weapons[weapon_type].muzzle_pos\n                         : g_Weapons[weapon_type].muzzle_pos_alt;\n}\n\nvoid Gun_Smoke_OnFire(const LARA_GUN_TYPE weapon_type, const bool is_right_hand)\n{\n    if (g_TRVersion != 3) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const int32_t count = g_Weapons[weapon_type].smoke_count;\n    if (count == 0) {\n        return;\n    }\n\n    lara->tr3_smoke_weapon = weapon_type;\n    if (is_right_hand) {\n        lara->tr3_smoke_count_r = count;\n    } else {\n        lara->tr3_smoke_count_l = count;\n    }\n\n    const LARA_MESH hand = is_right_hand ? LM_HAND_R : LM_HAND_L;\n    const XYZ_32 muzzle_pos = weapon_type == LGT_SHOTGUN && is_right_hand\n        ? M_GetHandAbsPosition(hand, (XYZ_32) { .x = 0, .y = 228, .z = 32 })\n        : M_GetHandAbsPosition(\n              hand, M_GetMuzzleOffset(weapon_type, is_right_hand));\n\n    GAME_VECTOR pos = { .pos = muzzle_pos,\n                        .room_num = Lara_GetItem()->room_num };\n    Room_GetSector(pos.pos, &pos.room_num);\n\n    if (weapon_type == LGT_SHOTGUN && is_right_hand) {\n        const XYZ_32 muzzle_tip_pos =\n            M_GetHandAbsPosition(hand, (XYZ_32) { .x = 0, .y = 1508, .z = 32 });\n        const XYZ_32 vel = {\n            .x = muzzle_tip_pos.x - muzzle_pos.x,\n            .y = muzzle_tip_pos.y - muzzle_pos.y,\n            .z = muzzle_tip_pos.z - muzzle_pos.z,\n        };\n\n        for (int32_t i = 0; i < 7; i++) {\n            Sparks_TriggerGunSmokeDirected(pos, vel, true, weapon_type, count);\n        }\n\n        const XYZ_32 vel_sparks = {\n            .x = (muzzle_tip_pos.x - muzzle_pos.x) << 1,\n            .y = (muzzle_tip_pos.y - muzzle_pos.y) << 1,\n            .z = (muzzle_tip_pos.z - muzzle_pos.z) << 1,\n        };\n\n        for (int32_t i = 0; i < 12; i++) {\n            Sparks_TriggerShotgunSparks(muzzle_pos, vel_sparks);\n        }\n    } else {\n        Sparks_TriggerGunSmoke(pos, true, weapon_type, count);\n    }\n}\n\nvoid Gun_Smoke_Control(void)\n{\n    if (g_TRVersion != 3) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->tr3_smoke_count_l == 0 && lara->tr3_smoke_count_r == 0) {\n        return;\n    }\n\n    const LARA_GUN_TYPE weapon_type = lara->tr3_smoke_weapon;\n\n    if (lara->tr3_smoke_count_l > 0) {\n        const XYZ_32 muzzle_pos = M_GetHandAbsPosition(\n            LM_HAND_L, M_GetMuzzleOffset(weapon_type, false));\n        GAME_VECTOR pos = { .pos = muzzle_pos,\n                            .room_num = Lara_GetItem()->room_num };\n        Room_GetSector(pos.pos, &pos.room_num);\n        Sparks_TriggerGunSmoke(\n            pos, false, weapon_type, lara->tr3_smoke_count_l);\n        lara->tr3_smoke_count_l--;\n    }\n\n    if (lara->tr3_smoke_count_r > 0) {\n        const XYZ_32 muzzle_pos = M_GetHandAbsPosition(\n            LM_HAND_R, M_GetMuzzleOffset(weapon_type, true));\n        GAME_VECTOR pos = { .pos = muzzle_pos,\n                            .room_num = Lara_GetItem()->room_num };\n        Room_GetSector(pos.pos, &pos.room_num);\n        Sparks_TriggerGunSmoke(\n            pos, false, weapon_type, lara->tr3_smoke_count_r);\n        lara->tr3_smoke_count_r--;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/gun/smoke.h",
    "content": "#pragma once\n\n#include <trx/game/lara/enum.h>\n\nvoid Gun_Smoke_Control(void);\nvoid Gun_Smoke_OnFire(LARA_GUN_TYPE weapon_type, bool is_right_hand);\n"
  },
  {
    "path": "src/trx/game/gun/types.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/math/types.h>\n#include <trx/game/sound/ids.h>\n\ntypedef enum {\n    WEAPON_TYPE_DUAL_PISTOLS,\n    WEAPON_TYPE_SINGLE_PISTOL,\n    WEAPON_TYPE_RIFLE,\n    WEAPON_TYPE_MOUNTED,\n    NUM_WEAPON_TYPES,\n} WEAPON_TYPE;\n\ntypedef struct {\n    int32_t initial_qty;\n    int32_t pickup_qty;\n    int32_t pickup_qty_alt;\n} WEAPON_AMMO_INFO;\n\ntypedef struct {\n    WEAPON_TYPE type;\n    int16_t lock_angles[4];\n    int16_t left_angles[4];\n    int16_t right_angles[4];\n    int16_t aim_speed;\n    int16_t shot_accuracy;\n    int32_t gun_height;\n    int32_t damage;\n    WEAPON_AMMO_INFO ammo;\n    int32_t target_dist;\n    int16_t equip_anim_idx;\n    int16_t draw_frame;\n    int16_t undraw_frame;\n    int16_t recoil_frame;\n    int16_t flash_time;\n    int16_t flash_shade;\n    RGB_F flash_color;\n    XYZ_32 flash_pos;\n    XYZ_32 flash_pos_alt;\n    SAMPLE_TRX_ID sample_num;\n    RGB_F glow_color;\n    XYZ_32 glow_pos;\n    XYZ_32 muzzle_pos;\n    XYZ_32 muzzle_pos_alt;\n    XYZ_32 shell_pos;\n    XYZ_32 shell_pos_alt;\n    int32_t smoke_count;\n    bool is_available;\n} WEAPON_INFO;\n"
  },
  {
    "path": "src/trx/game/gun/vars.c",
    "content": "#include <trx/game/gun/vars.h>\n\n#include <trx/core/enum_map.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/log.h>\n#include <trx/game/catalog/manager.h>\n#include <trx/game/const.h>\n#include <trx/game/shell.h>\n\nWEAPON_INFO g_Weapons[NUM_WEAPONS] = {};\n\nstatic void M_ReadAngles(\n    JSON_OBJECT *const obj, const char *const name, const char *const path,\n    const char *const key, int16_t *const angles)\n{\n    JSON_ARRAY *const arr = JSON_ObjectGetArray(obj, key);\n    if (arr == nullptr) {\n        return;\n    }\n    if (arr->length != 4) {\n        Shell_ExitSystemFmt(\"invalid '%s' for '%s' in %s\", key, name, path);\n    }\n    for (size_t i = 0; i < 4; i++) {\n        angles[i] = JSON_ArrayGetInt(arr, i, angles[i]) * DEG_1;\n    }\n}\n\nstatic void M_ReadRGB_F(JSON_VALUE *const value, RGB_F *const target)\n{\n    JSON_OBJECT *const obj = JSON_ValueAsObject(value);\n    if (obj != nullptr) {\n        target->r = JSON_ObjectGetDouble(obj, \"r\", 0.0);\n        target->g = JSON_ObjectGetDouble(obj, \"g\", 0.0);\n        target->b = JSON_ObjectGetDouble(obj, \"b\", 0.0);\n    }\n    JSON_ARRAY *const arr = JSON_ValueAsArray(value);\n    if (arr != nullptr && arr->length == 3) {\n        target->r = JSON_ArrayGetDouble(arr, 0, 0.0);\n        target->g = JSON_ArrayGetDouble(arr, 1, 0.0);\n        target->b = JSON_ArrayGetDouble(arr, 2, 0.0);\n    }\n}\n\nstatic void M_ReadXYZ32(JSON_VALUE *const value, XYZ_32 *const target)\n{\n    JSON_OBJECT *const obj = JSON_ValueAsObject(value);\n    if (obj != nullptr) {\n        target->x = JSON_ObjectGetInt(obj, \"x\", 0);\n        target->y = JSON_ObjectGetInt(obj, \"y\", 0);\n        target->z = JSON_ObjectGetInt(obj, \"z\", 0);\n    }\n    JSON_ARRAY *const arr = JSON_ValueAsArray(value);\n    if (arr != nullptr && arr->length == 3) {\n        target->x = JSON_ArrayGetInt(arr, 0, 0);\n        target->y = JSON_ArrayGetInt(arr, 1, 0);\n        target->z = JSON_ArrayGetInt(arr, 2, 0);\n    }\n}\n\nstatic void M_ReadAmmoInfo(JSON_OBJECT *const obj, const int32_t type)\n{\n    JSON_OBJECT *const ammo_obj = JSON_ObjectGetObject(obj, \"ammo\");\n    if (ammo_obj == nullptr) {\n        return;\n    }\n\n    g_Weapons[type].ammo.initial_qty = JSON_ObjectGetInt(\n        ammo_obj, \"initial_qty\", g_Weapons[type].ammo.initial_qty);\n    g_Weapons[type].ammo.pickup_qty = JSON_ObjectGetInt(\n        ammo_obj, \"pickup_qty\", g_Weapons[type].ammo.pickup_qty);\n    g_Weapons[type].ammo.pickup_qty_alt = JSON_ObjectGetInt(\n        ammo_obj, \"pickup_qty_alt\", g_Weapons[type].ammo.pickup_qty_alt);\n}\n\nvoid Gun_LoadVars(const char *const path)\n{\n#define L_READ_ANGLE(name, target)                                             \\\n    target = JSON_ObjectGetInt(obj, name, target) * DEG_1;\n#define L_READ_DIST(name, target)                                              \\\n    target = JSON_ObjectGetDouble(obj, name, target / (float)WALL_L) * WALL_L;\n#define L_READ_INT(name, target) target = JSON_ObjectGetInt(obj, name, target)\n\n    JSON_VALUE *const root = JSONFile_ReadEx(path, true);\n    JSON_OBJECT *const root_obj = JSON_ValueAsObject(root);\n    if (root_obj == nullptr) {\n        Shell_ExitSystemFmt(\"invalid weapons vars file: %s\", path);\n    }\n\n    for (JSON_OBJECT_ELEMENT *elem = root_obj->start; elem != nullptr;\n         elem = elem->next) {\n        const char *const name = elem->name->string;\n        const int32_t type = ENUM_MAP_GET(LARA_GUN_TYPE, name, -1);\n        if (type < 0 || type >= NUM_WEAPONS) {\n            Shell_ExitSystemFmt(\"unknown weapon type '%s' in %s\", name, path);\n        }\n\n        JSON_OBJECT *const obj = JSON_ValueAsObject(elem->value);\n\n        // weapon type\n        const char *const weapon_type =\n            JSON_ObjectGetString(obj, \"type\", JSON_INVALID_STRING);\n        if (weapon_type != JSON_INVALID_STRING && weapon_type[0] != '\\0') {\n            const int32_t weapon_type_val =\n                ENUM_MAP_GET(WEAPON_TYPE, weapon_type, -1);\n            if (weapon_type_val < 0 || weapon_type_val >= NUM_WEAPON_TYPES) {\n                Shell_ExitSystemFmt(\n                    \"unknown weapon type '%s' in %s\", weapon_type, path);\n            } else {\n                g_Weapons[type].type = weapon_type_val;\n            }\n        }\n\n        // angles\n        M_ReadAngles(\n            obj, name, path, \"lock_angles\", g_Weapons[type].lock_angles);\n        M_ReadAngles(\n            obj, name, path, \"left_angles\", g_Weapons[type].left_angles);\n        M_ReadAngles(\n            obj, name, path, \"right_angles\", g_Weapons[type].right_angles);\n\n        // scalar properties\n        L_READ_ANGLE(\"aim_speed\", g_Weapons[type].aim_speed);\n        L_READ_ANGLE(\"shot_accuracy\", g_Weapons[type].shot_accuracy);\n        L_READ_INT(\"gun_height\", g_Weapons[type].gun_height);\n        L_READ_INT(\"damage\", g_Weapons[type].damage);\n        L_READ_DIST(\"target_dist\", g_Weapons[type].target_dist);\n        L_READ_INT(\"equip_anim_idx\", g_Weapons[type].equip_anim_idx);\n        L_READ_INT(\"draw_frame\", g_Weapons[type].draw_frame);\n        L_READ_INT(\"undraw_frame\", g_Weapons[type].undraw_frame);\n        L_READ_INT(\"recoil_frame\", g_Weapons[type].recoil_frame);\n        L_READ_INT(\"flash_time\", g_Weapons[type].flash_time);\n        L_READ_INT(\"flash_shade\", g_Weapons[type].flash_shade);\n        L_READ_INT(\"smoke_count\", g_Weapons[type].smoke_count);\n\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"flash_pos\"), &g_Weapons[type].flash_pos);\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"flash_pos_alt\"),\n            &g_Weapons[type].flash_pos_alt);\n        M_ReadRGB_F(\n            JSON_ObjectGetValue(obj, \"flash_color\"),\n            &g_Weapons[type].flash_color);\n\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"glow_pos\"), &g_Weapons[type].glow_pos);\n        M_ReadRGB_F(\n            JSON_ObjectGetValue(obj, \"glow_color\"),\n            &g_Weapons[type].glow_color);\n\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"muzzle_pos\"),\n            &g_Weapons[type].muzzle_pos);\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"muzzle_pos_alt\"),\n            &g_Weapons[type].muzzle_pos_alt);\n\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"shell_pos\"), &g_Weapons[type].shell_pos);\n        M_ReadXYZ32(\n            JSON_ObjectGetValue(obj, \"shell_pos_alt\"),\n            &g_Weapons[type].shell_pos_alt);\n\n        M_ReadAmmoInfo(obj, type);\n\n        // sample_num\n        const char *const sample =\n            JSON_ObjectGetString(obj, \"sample_num\", JSON_INVALID_STRING);\n        if (sample != JSON_INVALID_STRING && sample[0] != '\\0') {\n            CATALOG_ID sample_id;\n            if (!Catalog_NameToEnum(CATALOG_SAMPLES, sample, &sample_id)) {\n                LOG_WARNING(\n                    \"unknown sample '%s' for '%s' in %s\", sample, name, path);\n            } else {\n                g_Weapons[type].sample_num = sample_id;\n            }\n        }\n\n        g_Weapons[type].is_available =\n            JSON_ObjectGetBool(obj, \"is_available\", true);\n    }\n\n    JSON_ValueFree(root);\n#undef L_READ_ANGLE\n#undef L_READ_DIST\n#undef L_READ_INT\n}\n"
  },
  {
    "path": "src/trx/game/gun/vars.h",
    "content": "#pragma once\n\n#include <trx/game/gun/types.h>\n#include <trx/game/lara/enum.h>\n\nextern WEAPON_INFO g_Weapons[NUM_WEAPONS];\n\nvoid Gun_LoadVars(const char *path);\n"
  },
  {
    "path": "src/trx/game/gun.h",
    "content": "#pragma once\n\n#include <trx/game/gun/common.h>\n#include <trx/game/gun/control.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/pistols.h>\n#include <trx/game/gun/rifle.h>\n#include <trx/game/gun/smashing.h>\n#include <trx/game/gun/vars.h>\n"
  },
  {
    "path": "src/trx/game/gym.c",
    "content": "#include <trx/game/gym.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing/lot.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_NO_TIME (-1)\n#define M_MAX_ASSAULT_TIME_FRAMES (60 * 60 * LOGIC_FPS - 3) // 59:59\n\ntypedef struct {\n    int32_t is_inventory_open_enabled;\n    int16_t completion_timer;\n    GYM_TRACK_TYPE active_track_type;\n\n    struct {\n        bool timer_display;\n        bool timer_active;\n        int32_t penalty_display_timer;\n        int32_t penalty_frames;\n        int32_t target_penalty_frames;\n        int32_t timer_auto_hide_timer;\n        bool pad_touched_this_frame;\n        bool pad_lock;\n    } assault_course;\n\n    struct {\n        bool timer_display;\n        bool timer_active;\n        int32_t lap_time;\n        int32_t lap_time_display_timer;\n    } quad_course;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {\n    .is_inventory_open_enabled = -1,\n};\n\nstatic int32_t M_CountAssaultTargets(void)\n{\n    int32_t remaining = 0;\n    for (int16_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (item->object_id != O_ASSAULT_TARGET) {\n            continue;\n        }\n\n        if ((item->flags & IF_KILLED) == 0\n            && item->timer > GYM_ASSAULT_TARGET_TIME) {\n            remaining++;\n        }\n    }\n    LOG_INFO(\"remaining=%d\", remaining);\n    return remaining;\n}\n\nstatic void M_ResetAssaultTargets(void)\n{\n    M_PRIV *const p = &m_Priv;\n\n    const OBJECT *const obj = Object_Get(O_ASSAULT_TARGET);\n    if (!obj->loaded) {\n        return;\n    }\n\n    for (int16_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (item->object_id != O_ASSAULT_TARGET) {\n            continue;\n        }\n\n        if ((item->flags & IF_KILLED) != 0) {\n            // Rockets can kill targets via Creature_Die()+Item_Kill(), which\n            // removes them from room draw + item lists. Use Item_Initialise()\n            // to re-link them back.\n\n            // Clear the flags so that Item_Initialise doesn't mark the item\n            // as active preemptively.\n            item->flags = 0;\n\n            Item_Initialise(item_num);\n        } else if (\n            item->status != IS_INACTIVE && obj->initialise_func != nullptr) {\n            obj->initialise_func(item_num);\n        }\n    }\n}\n\nstatic int32_t M_GetBestTime(void)\n{\n    const GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats;\n    return assault->total_attempts > 0 ? (int32_t)assault->entries[0].time\n                                       : M_NO_TIME;\n}\n\nstatic bool M_StoreCourseTime(GYM_TRACK_STATS *const stats, const uint32_t time)\n{\n    int32_t insert_idx = -1;\n    const int32_t limit = MAX_ASSAULT_TIMES;\n    for (int32_t i = 0; i < limit; i++) {\n        if (stats->entries[i].time == 0 || time < stats->entries[i].time) {\n            insert_idx = i;\n            break;\n        }\n    }\n    if (insert_idx == -1) {\n        return false;\n    }\n\n    for (int32_t i = limit - 1; i > insert_idx; i--) {\n        stats->entries[i] = stats->entries[i - 1];\n    }\n\n    stats->total_attempts++;\n    stats->entries[insert_idx].time = time;\n    stats->entries[insert_idx].attempt_num = stats->total_attempts;\n    Config_Update();\n    return true;\n}\n\nstatic bool M_IsOnQuadBike(void)\n{\n    const ITEM *const vehicle = Lara_Vehicle_GetItem();\n    return vehicle != nullptr && vehicle->object_id == O_QUAD_BIKE;\n}\n\nvoid Gym_SetInventoryOpenEnabled(const bool enabled)\n{\n    M_PRIV *const p = &m_Priv;\n    p->is_inventory_open_enabled = enabled;\n}\n\nbool Gym_IsInventoryOpenEnabled(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->is_inventory_open_enabled == -1) {\n        p->is_inventory_open_enabled = g_TRVersion >= 2;\n    }\n    return p->is_inventory_open_enabled;\n}\n\nvoid Gym_Control(void)\n{\n    if (g_TRVersion < 3) {\n        return;\n    }\n\n    M_PRIV *const p = &m_Priv;\n\n    if (p->assault_course.pad_lock\n        && !p->assault_course.pad_touched_this_frame) {\n        p->assault_course.pad_lock = false;\n    }\n    p->assault_course.pad_touched_this_frame = false;\n\n    if (p->assault_course.penalty_display_timer > 0) {\n        p->assault_course.penalty_display_timer--;\n    }\n    if (!p->assault_course.timer_active && p->assault_course.timer_display\n        && p->assault_course.timer_auto_hide_timer > 0) {\n        p->assault_course.timer_auto_hide_timer--;\n        if (p->assault_course.timer_auto_hide_timer == 0) {\n            p->assault_course.timer_display = false;\n            p->active_track_type = GYM_TRACK_NONE;\n        }\n    }\n\n    if (p->quad_course.lap_time_display_timer > 0) {\n        p->quad_course.lap_time_display_timer--;\n    }\n}\n\nstatic void M_Assault_Finish(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!p->assault_course.timer_active) {\n        return;\n    }\n\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n\n    uint32_t final_time = resume->stats.timer;\n    if (g_TRVersion >= 3) {\n        const int32_t targets_remaining = M_CountAssaultTargets();\n        p->assault_course.penalty_display_timer = 10 * LOGIC_FPS;\n        p->assault_course.target_penalty_frames =\n            10 * LOGIC_FPS * targets_remaining;\n        CLAMPG(\n            p->assault_course.target_penalty_frames, M_MAX_ASSAULT_TIME_FRAMES);\n\n        final_time += (uint32_t)p->assault_course.penalty_frames\n            + (uint32_t)p->assault_course.target_penalty_frames;\n        CLAMPG(final_time, M_MAX_ASSAULT_TIME_FRAMES);\n        resume->stats.timer = final_time;\n    }\n\n    if (g_TRVersion >= 3) {\n        if (final_time < (uint32_t)(180 * LOGIC_FPS)) {\n            Music_Play(MX_TR3_GYM_HINT_FAST_TIME, MPM_ONCE);\n        }\n        p->assault_course.timer_auto_hide_timer = 15 * LOGIC_FPS;\n    } else {\n        const int32_t current_best_time = M_GetBestTime();\n        if (current_best_time <= 0) {\n            if (final_time < (uint32_t)(100 * LOGIC_FPS)) {\n                // \"Gosh! That was my best time yet!\"\n                Music_Play(MX_TR2_GYM_HINT_15, MPM_ONCE);\n            } else {\n                // \"Congratulations! You did it! But perhaps I could've been\n                // faster.\"\n                Music_Play(MX_TR2_GYM_HINT_17, MPM_ONCE);\n            }\n        } else if (final_time < (uint32_t)current_best_time) {\n            // \"Gosh! That was my best time yet!\"\n            Music_Play(MX_TR2_GYM_HINT_15, MPM_ONCE);\n        } else if (final_time < (uint32_t)current_best_time + 5 * LOGIC_FPS) {\n            // \"Almost. Perhaps another try and I might beat it.\"\n            Music_Play(MX_TR2_GYM_HINT_16, MPM_ONCE);\n        } else {\n            // \"Great. But nowhere near my best time.\"\n            Music_Play(MX_TR2_GYM_HINT_14, MPM_ONCE);\n        }\n    }\n\n    p->assault_course.timer_active = false;\n\n    M_StoreCourseTime(&g_Config.profile.assault_stats, final_time);\n}\n\nstatic void M_Racetrack_Finish(void)\n{\n    if (!M_IsOnQuadBike()) {\n        return;\n    }\n\n    M_PRIV *const p = &m_Priv;\n    if (!p->quad_course.timer_active) {\n        return;\n    }\n\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n    uint32_t final_time = resume->stats.timer;\n    CLAMPG(final_time, M_MAX_ASSAULT_TIME_FRAMES);\n    resume->stats.timer = final_time;\n\n    p->quad_course.lap_time = (int32_t)final_time;\n    p->quad_course.lap_time_display_timer = 10 * LOGIC_FPS;\n    M_StoreCourseTime(&g_Config.profile.racetrack_stats, final_time);\n    p->quad_course.timer_active = false;\n}\n\nGYM_TRACK_TYPE Gym_TrackManager_GetActiveTrackType(void)\n{\n    M_PRIV *const p = &m_Priv;\n    return p->active_track_type;\n}\n\nbool Gym_TrackManager_HasStats(const GYM_TRACK_TYPE track)\n{\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        return g_TRVersion >= 2;\n    case GYM_TRACK_QUAD:\n        return g_TRVersion >= 3;\n    default:\n        return false;\n    }\n}\n\nconst GYM_TRACK_STATS *Gym_TrackManager_GetStats(const GYM_TRACK_TYPE track)\n{\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        return &g_Config.profile.assault_stats;\n    case GYM_TRACK_QUAD:\n        return &g_Config.profile.racetrack_stats;\n    default:\n        return nullptr;\n    }\n}\n\nbool Gym_TrackManager_IsTimerDisplay(const GYM_TRACK_TYPE track)\n{\n    M_PRIV *const p = &m_Priv;\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        return p->assault_course.timer_display;\n    case GYM_TRACK_QUAD:\n        return p->quad_course.timer_display;\n    default:\n        return false;\n    }\n}\n\nbool Gym_TrackManager_IsTimerActive(const GYM_TRACK_TYPE track)\n{\n    M_PRIV *const p = &m_Priv;\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        return p->assault_course.timer_active;\n    case GYM_TRACK_QUAD:\n        return p->quad_course.timer_active;\n    default:\n        return false;\n    }\n}\n\nvoid Gym_TrackManager_Reset(const GYM_TRACK_TYPE track)\n{\n    M_PRIV *const p = &m_Priv;\n    p->active_track_type = GYM_TRACK_NONE;\n\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        p->assault_course.timer_active = false;\n        p->assault_course.timer_display = false;\n        p->assault_course.penalty_frames = 0;\n        p->assault_course.target_penalty_frames = 0;\n        p->assault_course.penalty_display_timer = 0;\n        p->assault_course.timer_auto_hide_timer = 0;\n        p->assault_course.pad_touched_this_frame = false;\n        p->assault_course.pad_lock = false;\n        break;\n\n    case GYM_TRACK_QUAD:\n        p->quad_course.timer_active = false;\n        p->quad_course.timer_display = false;\n        p->quad_course.lap_time = 0;\n        p->quad_course.lap_time_display_timer = 0;\n        break;\n\n    default:\n        break;\n    }\n}\n\nvoid Gym_TrackManager_Start(const GYM_TRACK_TYPE track)\n{\n    M_PRIV *const p = &m_Priv;\n    p->active_track_type = track;\n\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n    resume->stats.timer = 0;\n\n    switch (track) {\n    case GYM_TRACK_ASSAULT: {\n        p->assault_course.timer_active = true;\n        p->assault_course.timer_display = true;\n        p->assault_course.penalty_frames = 0;\n        p->assault_course.target_penalty_frames = 0;\n        p->assault_course.penalty_display_timer = 0;\n        p->assault_course.timer_auto_hide_timer = 0;\n        p->assault_course.pad_touched_this_frame = false;\n        p->assault_course.pad_lock = false;\n        M_ResetAssaultTargets();\n        break;\n    }\n\n    case GYM_TRACK_QUAD: {\n        if (!M_IsOnQuadBike()) {\n            return;\n        }\n\n        p->quad_course.timer_active = true;\n        p->quad_course.timer_display = true;\n        break;\n    default:\n        break;\n    }\n    }\n}\n\nvoid Gym_TrackManager_Stop(const GYM_TRACK_TYPE track)\n{\n    M_PRIV *const p = &m_Priv;\n    p->active_track_type = GYM_TRACK_NONE;\n\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        p->assault_course.timer_active = false;\n        p->assault_course.timer_display = true;\n        p->assault_course.timer_auto_hide_timer = 0;\n        break;\n\n    case GYM_TRACK_QUAD:\n        if (!M_IsOnQuadBike()) {\n            return;\n        }\n        p->quad_course.timer_active = false;\n        p->quad_course.timer_display = true;\n        break;\n\n    default:\n        break;\n    }\n}\n\nvoid Gym_TrackManager_Finish(const GYM_TRACK_TYPE track)\n{\n    M_PRIV *const p = &m_Priv;\n\n    switch (track) {\n    case GYM_TRACK_ASSAULT:\n        M_Assault_Finish();\n        break;\n\n    case GYM_TRACK_QUAD:\n        M_Racetrack_Finish();\n        break;\n\n    default:\n        break;\n    }\n}\n\nvoid Gym_TrackManager_AddPenaltySeconds(\n    const GYM_TRACK_TYPE track, const int32_t seconds)\n{\n    ASSERT(track == GYM_TRACK_ASSAULT);\n    M_PRIV *const p = &m_Priv;\n    if (seconds <= 0 || !p->assault_course.timer_active) {\n        return;\n    }\n\n    p->assault_course.penalty_display_timer = 4 * LOGIC_FPS;\n    p->assault_course.penalty_frames += seconds * LOGIC_FPS;\n    CLAMPG(p->assault_course.penalty_frames, M_MAX_ASSAULT_TIME_FRAMES);\n}\n\nint32_t Gym_TrackManager_GetPenaltyDisplayTimer(const GYM_TRACK_TYPE track)\n{\n    if (track != GYM_TRACK_ASSAULT) {\n        return 0;\n    }\n    M_PRIV *const p = &m_Priv;\n    return p->assault_course.penalty_display_timer;\n}\n\nint32_t Gym_TrackManager_GetPenaltyFrames(const GYM_TRACK_TYPE track)\n{\n    if (track != GYM_TRACK_ASSAULT) {\n        return 0;\n    }\n    M_PRIV *const p = &m_Priv;\n    return p->assault_course.penalty_frames;\n}\n\nint32_t Gym_TrackManager_GetTargetPenaltyFrames(const GYM_TRACK_TYPE track)\n{\n    if (track != GYM_TRACK_ASSAULT) {\n        return 0;\n    }\n    M_PRIV *const p = &m_Priv;\n    return p->assault_course.target_penalty_frames;\n}\n\nbool Gym_TrackManager_OnPadContact(\n    const GYM_TRACK_TYPE track, const bool on_ground)\n{\n    if (g_TRVersion < 3) {\n        return true;\n    }\n    if (track != GYM_TRACK_ASSAULT) {\n        return true;\n    }\n    if (!Game_IsInGym()) {\n        return true;\n    }\n\n    M_PRIV *const p = &m_Priv;\n    p->assault_course.pad_touched_this_frame = true;\n    if (!on_ground) {\n        return true;\n    }\n    if (p->assault_course.pad_lock) {\n        return false;\n    }\n\n    p->assault_course.pad_lock = true;\n    return true;\n}\n\nint32_t Gym_TrackManager_GetLapTimeDisplayTimer(const GYM_TRACK_TYPE track)\n{\n    if (track != GYM_TRACK_QUAD) {\n        return 0;\n    }\n    M_PRIV *const p = &m_Priv;\n    return p->quad_course.lap_time_display_timer;\n}\n\nint32_t Gym_TrackManager_GetLapTime(const GYM_TRACK_TYPE track)\n{\n    if (track != GYM_TRACK_QUAD) {\n        return 0;\n    }\n    M_PRIV *const p = &m_Priv;\n    return p->quad_course.lap_time;\n}\n\nbool Gym_CanPlayMusicTrack(MUSIC_ID *const track_id)\n{\n    const uint16_t flags = Music_GetTrackFlags(*track_id);\n    const ITEM *const lara = Lara_GetItem();\n    switch (Music_FromGameID(*track_id)) {\n    case MX_TR1_GYM_HINT_03:\n        if ((flags & IF_ONE_SHOT) != 0\n            && lara->current_anim_state == LS(LS_JUMP_UP)) {\n            *track_id = Music_ToGameID(MX_TR1_GYM_HINT_04);\n        }\n        break;\n\n    case MX_TR1_GYM_HINT_12:\n        if (lara->current_anim_state != LS(LS_HANG)) {\n            return false;\n        }\n        break;\n\n    case MX_TR1_GYM_HINT_16:\n        if (lara->current_anim_state != LS(LS_HANG)) {\n            return false;\n        }\n        break;\n\n    case MX_TR1_GYM_HINT_17:\n        if ((flags & IF_ONE_SHOT) != 0\n            && lara->current_anim_state == LS(LS_HANG)) {\n            *track_id = Music_ToGameID(MX_TR1_GYM_HINT_18);\n        }\n        break;\n\n    case MX_TR1_GYM_HINT_24:\n        if (lara->current_anim_state != LS(LS_SURF_TREAD)) {\n            return false;\n        }\n        break;\n\n    case MX_TR1_GYM_HINT_25:\n        if ((flags & IF_ONE_SHOT) != 0) {\n            M_PRIV *const p = &m_Priv;\n            p->completion_timer++;\n            if (p->completion_timer == LOGIC_FPS * 4) {\n                Game_SetIsLevelComplete(true);\n                p->completion_timer = 0;\n            }\n        } else if (lara->current_anim_state != LS(LS_WATER_OUT)) {\n            return false;\n        }\n        break;\n\n    default:\n        return true;\n    }\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/gym.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n#include <trx/game/music/ids.h>\n\n#include <stdint.h>\n\n#define GYM_ASSAULT_TARGET_TIME (10 * LOGIC_FPS)\n\ntypedef enum {\n    GYM_TRACK_NONE = -1,\n    GYM_TRACK_QUAD,\n    GYM_TRACK_ASSAULT,\n    GYM_TRACK_NUMBER_OF\n} GYM_TRACK_TYPE;\n\nvoid Gym_SetInventoryOpenEnabled(bool enabled);\nbool Gym_IsInventoryOpenEnabled(void);\n\nGYM_TRACK_TYPE Gym_TrackManager_GetActiveTrackType(void);\nbool Gym_TrackManager_HasStats(GYM_TRACK_TYPE track);\nconst GYM_TRACK_STATS *Gym_TrackManager_GetStats(GYM_TRACK_TYPE track);\nbool Gym_TrackManager_IsTimerDisplay(GYM_TRACK_TYPE track);\nbool Gym_TrackManager_IsTimerActive(GYM_TRACK_TYPE track);\nvoid Gym_TrackManager_Reset(GYM_TRACK_TYPE track);\nvoid Gym_TrackManager_Start(GYM_TRACK_TYPE track);\nvoid Gym_TrackManager_Stop(GYM_TRACK_TYPE track);\nvoid Gym_TrackManager_Finish(GYM_TRACK_TYPE track);\n\n// Assault-only extensions (no-op / 0 for other tracks).\nvoid Gym_TrackManager_AddPenaltySeconds(GYM_TRACK_TYPE track, int32_t seconds);\nint32_t Gym_TrackManager_GetPenaltyDisplayTimer(GYM_TRACK_TYPE track);\nint32_t Gym_TrackManager_GetPenaltyFrames(GYM_TRACK_TYPE track);\nint32_t Gym_TrackManager_GetTargetPenaltyFrames(GYM_TRACK_TYPE track);\nbool Gym_TrackManager_OnPadContact(GYM_TRACK_TYPE track, bool on_ground);\n\n// Quad-only extensions (0 for other tracks).\nint32_t Gym_TrackManager_GetLapTimeDisplayTimer(GYM_TRACK_TYPE track);\nint32_t Gym_TrackManager_GetLapTime(GYM_TRACK_TYPE track);\n\n// TR3 assault course extensions (targets + penalties).\nvoid Gym_Control(void);\n\n// Potentially converts the requested track id based on Lara's state. Returns\n// true if the track should be played.\nbool Gym_CanPlayMusicTrack(MUSIC_ID *track_id);\n"
  },
  {
    "path": "src/trx/game/inject/common.c",
    "content": "#include <trx/game/inject/common.h>\n\n#include <trx/config.h>\n#include <trx/core/benchmark.h>\n#include <trx/core/memory.h>\n#include <trx/core/thread_pool.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/items.h>\n#include <trx/game/level.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/version.h>\n\n#include <string.h>\n#include <zlib.h>\n\n#define M_VIRTUAL_NAME \"virtual_injection\"\n\ntypedef struct {\n    INJECTION *injection;\n    const char *path;\n} M_LOAD_JOB;\n\nstatic bool (*m_Testers[ITT_NUMBER_OF])(\n    const INJECTION_CONTEXT *, const INJECTION *injection) = {};\nstatic void (*m_Handlers[ICT_NUMBER_OF])(\n    const INJECTION_CONTEXT *, INJECTION_CHUNK chunk) = {};\n\nstatic INJECTION_CONTEXT m_Context = {};\nstatic int32_t m_NumInjections = 0;\nstatic INJECTION *m_Injections = nullptr;\n\nstatic int32_t m_DataCounts[IDT_NUMBER_OF] = {};\nstatic int32_t m_MaxStaticObject3DId = -1;\nstatic int32_t m_MaxStaticObject2DId = -1;\nstatic VECTOR *m_RoomMeta = nullptr;\nstatic LEVEL_CONTEXT_INFO m_CachedInfo = {};\nstatic uint16_t *m_PaletteMap = nullptr;\nstatic size_t m_PaletteMapSize = 0;\n\nstatic bool M_IsRelevant(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_FILE_TYPE type)\n{\n    const bool stats = (ctx->mode == INJECTION_MODE_STATS);\n\n    if (stats) {\n        switch (type) {\n        case IFT_GENERAL:\n        case IFT_FLOOR_DATA:\n        case IFT_PS1_ENEMY:\n            break;\n\n        default:\n            return false;\n        }\n    }\n\n    switch (type) {\n    case IFT_GENERAL:\n    case IFT_LARA_ANIMS:\n    case IFT_BRAID:\n    case IFT_SKYBOX:\n        return true;\n\n    case IFT_FLOOR_DATA:\n        return g_Config.gameplay.fix_floor_data_issues;\n\n    case IFT_ITEM_POSITION:\n        return g_Config.visuals.fix_item_rots;\n\n    case IFT_TEXTURE_FIX:\n        return g_Config.visuals.fix_texture_issues;\n\n    case IFT_ALTER_ANIM_SPRITE:\n        return g_Config.visuals.fix_animated_sprites == (g_TRVersion >= 2);\n\n    case IFT_PS1_SFX:\n        return g_Config.audio.enable_ps1_sfx;\n\n    case IFT_PS1_ENEMY: {\n        if (!g_Config.gameplay.restore_ps1_enemies) {\n            return false;\n        }\n        const SAVEGAME_INFO *const info =\n            Savegame_GetSavegameInfo(Savegame_GetBoundSlot());\n        if (info != nullptr && (info->initial_version == SG_VERSION_LEGACY)) {\n            return false;\n        }\n        return true;\n    }\n\n    default:\n        return false;\n    }\n}\n\nstatic INJECTION_CHUNK M_ReadChunk(const INJECTION *const injection)\n{\n    return (INJECTION_CHUNK) {\n        .injection = injection,\n        .type = VFile_ReadS32(injection->fp),\n        .num_blocks = VFile_ReadS32(injection->fp),\n        .total_size = VFile_ReadS32(injection->fp),\n    };\n}\n\nstatic void M_InitialiseBlock(\n    VFILE *const file, const INJECTION_VERSION version)\n{\n    const INJECTION_DATA_TYPE data_type = VFile_ReadS32(file);\n    const int32_t data_count = VFile_ReadS32(file);\n    const int32_t data_size = VFile_ReadS32(file);\n    if (data_type >= 0 && data_type < IDT_NUMBER_OF) {\n        m_DataCounts[data_type] += data_count;\n    }\n\n    switch (data_type) {\n    case IDT_STATIC_OBJECTS: {\n        for (int32_t i = 0; i < data_count; i++) {\n            const int32_t static_id = VFile_ReadS32(file);\n            if (static_id > m_MaxStaticObject3DId) {\n                m_MaxStaticObject3DId = static_id;\n            }\n            VFile_Skip(file, 28);\n        }\n        return;\n    }\n\n    case IDT_SPRITE_SEQUENCES: {\n        for (int32_t i = 0; i < data_count; i++) {\n            const INJECT_OBJECT_TYPE obj_type = VFile_ReadS32(file);\n            const int32_t obj_id = VFile_ReadS32(file);\n            if (obj_type == OBJ_TYPE_STATIC2D\n                && obj_id > m_MaxStaticObject2DId) {\n                m_MaxStaticObject2DId = obj_id;\n            }\n            if (obj_type == OBJ_TYPE_OBJECT && version < INJ_VERSION_5) {\n                VFile_Skip(file, 16);\n            }\n            VFile_Skip(file, sizeof(int16_t) * 2);\n        }\n        return;\n    }\n\n    case IDT_ROOM_EDIT_META: {\n        if (m_RoomMeta == nullptr) {\n            m_RoomMeta = Vector_Create(sizeof(INJECTION_MESH_META));\n        }\n        for (int32_t i = 0; i < data_count; i++) {\n            INJECTION_MESH_META meta = {\n                .room_index = VFile_ReadS16(file),\n                .num_vertices = VFile_ReadS16(file),\n                .num_quads = VFile_ReadS16(file),\n                .num_triangles = VFile_ReadS16(file),\n                .num_static_2ds = VFile_ReadS16(file),\n            };\n            if (version >= INJ_VERSION_3) {\n                meta.num_static_3ds = VFile_ReadS16(file);\n            }\n            Vector_Add(m_RoomMeta, &meta);\n        }\n\n        return;\n    }\n\n    case IDT_SAMPLE_INFOS: {\n        for (int32_t i = 0; i < data_count; i++) {\n            // Skip ID, volume and chance\n            VFile_Skip(file, 3 * sizeof(int16_t));\n            const int16_t flags = VFile_ReadS16(file);\n            if (version >= INJ_VERSION_6) {\n                // Skip range and pitch\n                VFile_Skip(file, sizeof(int32_t) + sizeof(int8_t));\n            }\n            const int16_t num_samples = (flags >> 2) & 0xF;\n            m_DataCounts[IDT_SAMPLE_INDICES] += num_samples;\n            if (g_TRVersion == 1 || version >= INJ_VERSION_4) {\n                for (int32_t j = 0; j < num_samples; j++) {\n                    const int32_t sample_length = VFile_ReadS32(file);\n                    m_DataCounts[IDT_SAMPLE_DATA] += sample_length;\n                    VFile_Skip(file, sizeof(char) * sample_length);\n                }\n            } else if (g_TRVersion >= 2) {\n                VFile_Skip(file, sizeof(uint32_t));\n            }\n        }\n\n        return;\n    }\n\n    default:\n        break;\n    }\n\n    VFile_Skip(file, data_size);\n}\n\nstatic void M_ReadVFile(\n    INJECTION *const injection, VFILE *const file, const char *const file_name)\n{\n    const char *const inj_name =\n        file_name == nullptr ? M_VIRTUAL_NAME : file_name;\n    char *payload = nullptr;\n    injection->path = Memory_DupStr(inj_name);\n\n    const uint32_t magic = VFile_ReadU32(file);\n    if (magic != INJECTION_MAGIC) {\n        LOG_WARNING(\"Invalid injection magic in %s\", inj_name);\n        goto cleanup;\n    }\n\n    injection->version = VFile_ReadS32(file);\n    if (injection->version < INJ_VERSION_2\n        || injection->version > INJ_CURRENT_VERSION) {\n        LOG_WARNING(\n            \"%s uses unsupported version %d\", inj_name, injection->version);\n        goto cleanup;\n    }\n\n    injection->type = VFile_ReadS32(file);\n    if (injection->type < 0 || injection->type >= IFT_NUMBER_OF) {\n        LOG_WARNING(\"%s is of unknown type %d\", inj_name, injection->type);\n        goto cleanup;\n    }\n\n    injection->relevant = M_IsRelevant(&m_Context, injection->type);\n    if (!injection->relevant) {\n        goto cleanup;\n    }\n\n    const int32_t uncompressed_size = VFile_ReadS32(file);\n    const int32_t compressed_size = VFile_ReadS32(file);\n\n    const char *compressed = file->cur_ptr;\n    payload = Memory_Alloc(uncompressed_size);\n\n    uLongf uncompressed_sizef = uncompressed_size;\n    const int32_t error_code = uncompress(\n        (Bytef *)payload, &uncompressed_sizef, (const Bytef *)compressed,\n        (uLongf)compressed_size);\n    if (error_code != Z_OK) {\n        LOG_WARNING(\"Failed to decompress injection payload (%d)\", error_code);\n        injection->relevant = false;\n        goto cleanup;\n    }\n\n    injection->fp = VFile_CreateFromBuffer(payload, uncompressed_size);\n    if (m_Context.mode != INJECTION_MODE_STATS) {\n        LOG_INFO(\"%s queued for injection\", inj_name);\n    }\n\ncleanup:\n    Memory_FreePointer(&payload);\n    VFile_Close(file);\n}\n\nstatic void M_InitialiseInjection(INJECTION *const injection)\n{\n    if (!injection->relevant || injection->fp == nullptr) {\n        return;\n    }\n\n    VFile_SetPos(injection->fp, 0);\n\n    {\n        // Tests are executed after the main level data is loaded.\n        VFile_Skip(injection->fp, sizeof(int32_t));\n        const int32_t test_size = VFile_ReadS32(injection->fp);\n        VFile_Skip(injection->fp, test_size);\n    }\n\n    const int32_t num_chunks = VFile_ReadS32(injection->fp);\n    for (int32_t i = 0; i < num_chunks; i++) {\n        const INJECTION_CHUNK chunk = M_ReadChunk(injection);\n        for (int32_t j = 0; j < chunk.num_blocks; j++) {\n            M_InitialiseBlock(injection->fp, injection->version);\n        }\n    }\n\n    VFile_SetPos(injection->fp, 0);\n}\n\nstatic void M_LoadFromFile(\n    INJECTION *const injection, const char *const file_name)\n{\n    VFILE *const file = VFile_CreateFromPath(file_name);\n    if (file == nullptr) {\n        LOG_WARNING(\"Could not open %s\", file_name);\n        return;\n    }\n\n    M_ReadVFile(injection, file, file_name);\n}\n\nstatic void M_LoadInjectionJob(void *const user_data)\n{\n    const M_LOAD_JOB *const job = user_data;\n    M_LoadFromFile(job->injection, job->path);\n}\n\nstatic bool M_IsApplicable(const INJECTION *const injection)\n{\n    const int32_t test_count = VFile_ReadS32(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n\n    bool applicable = true;\n    for (int32_t i = 0; i < test_count; i++) {\n        const INJECTION_TEST_TYPE type = VFile_ReadS32(injection->fp);\n        if (m_Testers[type] == nullptr) {\n            LOG_WARNING(\"Unknown injection test type %d\", type);\n            applicable = false;\n            break;\n        } else {\n            applicable &= m_Testers[type](&m_Context, injection);\n        }\n    }\n\n    return applicable;\n}\n\nvoid Inject_RegisterTester(\n    const INJECTION_TEST_TYPE type,\n    bool (*test_func)(const INJECTION_CONTEXT *, const INJECTION *injection))\n{\n    m_Testers[type] = test_func;\n}\n\nvoid Inject_RegisterHandler(\n    const INJECTION_CHUNK_TYPE type,\n    void (*handle_func)(const INJECTION_CONTEXT *, INJECTION_CHUNK chunk))\n{\n    m_Handlers[type] = handle_func;\n}\n\nvoid Inject_RegisterPaletteMap(const uint16_t *palette_map, const int32_t size)\n{\n    Memory_FreePointer(&m_PaletteMap);\n    m_PaletteMap = Memory_Alloc(size * sizeof(int16_t));\n    m_PaletteMapSize = size;\n    memcpy(m_PaletteMap, palette_map, size * sizeof(int16_t));\n}\n\nuint16_t Inject_GetPaletteIndex(const uint16_t index)\n{\n    ASSERT(index < m_PaletteMapSize);\n    return m_PaletteMap == nullptr ? 0 : m_PaletteMap[index];\n}\n\nvoid Inject_InitLevel(const GF_LEVEL *const level, const INJECTION_MODE mode)\n{\n    m_Context.mode = mode;\n    m_NumInjections = level->injections.count;\n    if (m_NumInjections == 0) {\n        return;\n    }\n\n    BENCHMARK benchmark = Benchmark_Start();\n\n    m_Injections = Memory_Alloc(sizeof(INJECTION) * m_NumInjections);\n    if (m_NumInjections > 1) {\n        M_LOAD_JOB *const jobs =\n            Memory_Alloc(sizeof(M_LOAD_JOB) * m_NumInjections);\n\n        THREAD_POOL *const pool = ThreadPool_Create(-1);\n        ASSERT(pool != nullptr);\n        for (int32_t i = 0; i < m_NumInjections; i++) {\n            jobs[i] = (M_LOAD_JOB) {\n                .injection = &m_Injections[i],\n                .path = level->injections.data_paths[i],\n            };\n            ThreadPool_AddJob(pool, M_LoadInjectionJob, &jobs[i]);\n        }\n\n        ThreadPool_Wait(pool);\n        ThreadPool_Destroy(pool);\n\n        Memory_Free(jobs);\n    } else {\n        M_LoadFromFile(&m_Injections[0], level->injections.data_paths[0]);\n    }\n\n    for (int32_t i = 0; i < m_NumInjections; i++) {\n        M_InitialiseInjection(&m_Injections[i]);\n    }\n\n    if (m_Context.mode != INJECTION_MODE_STATS) {\n        Benchmark_End(&benchmark, nullptr);\n    }\n}\n\nvoid Inject_AppendInjection(VFILE *const file)\n{\n    m_Injections =\n        Memory_Realloc(m_Injections, sizeof(INJECTION) * (m_NumInjections + 1));\n    INJECTION *const injection = &m_Injections[m_NumInjections++];\n    M_ReadVFile(injection, file, nullptr);\n    M_InitialiseInjection(injection);\n}\n\nvoid Inject_AllInjections(void)\n{\n    if (m_Injections == nullptr) {\n        return;\n    }\n\n    BENCHMARK benchmark = Benchmark_Start();\n\n    for (int32_t i = 0; i < m_NumInjections; i++) {\n        INJECTION *const injection = &m_Injections[i];\n        if (!injection->relevant) {\n            continue;\n        }\n\n        // Allow checks to be done on an injection's applicability after the\n        // main level data has loaded.\n        if (!M_IsApplicable(injection)) {\n            LOG_WARNING(\n                \"Injection type %d is not applicable to the current level\",\n                injection->type);\n            continue;\n        }\n\n        if (m_Context.mode != INJECTION_MODE_STATS) {\n            LOG_DEBUG(\"Processing %s\", injection->path);\n        }\n\n        // Cache the current status to allow individual handlers to increment\n        // counts but still have access to current indices as required.\n        m_CachedInfo = *Level_Context_GetInfo();\n\n        const int32_t num_chunks = VFile_ReadS32(injection->fp);\n        for (int32_t j = 0; j < num_chunks; j++) {\n            const INJECTION_CHUNK chunk = M_ReadChunk(injection);\n            if (chunk.type < 0 || chunk.type >= ICT_NUMBER_OF\n                || m_Handlers[chunk.type] == nullptr) {\n                LOG_WARNING(\"Unrecognised chunk type %d\", chunk.type);\n                VFile_Skip(injection->fp, chunk.total_size);\n                continue;\n            }\n\n            m_Handlers[chunk.type](&m_Context, chunk);\n        }\n\n        ASSERT(VFile_GetPos(injection->fp) == injection->fp->size);\n    }\n\n    if (m_Context.mode != INJECTION_MODE_STATS) {\n        Benchmark_End(&benchmark, nullptr);\n    }\n}\n\nvoid Inject_Cleanup(void)\n{\n    if (m_Injections == nullptr) {\n        return;\n    }\n\n    BENCHMARK benchmark = Benchmark_Start();\n\n    for (int32_t i = 0; i < m_NumInjections; i++) {\n        INJECTION *const injection = &m_Injections[i];\n        if (injection->fp != nullptr) {\n            VFile_Close(injection->fp);\n        }\n        Memory_FreePointer(&injection->path);\n    }\n\n    for (int32_t i = 0; i < IDT_NUMBER_OF; i++) {\n        m_DataCounts[i] = 0;\n    }\n    m_MaxStaticObject3DId = -1;\n    m_MaxStaticObject2DId = -1;\n\n    Memory_FreePointer(&m_Injections);\n    Memory_FreePointer(&m_PaletteMap);\n    m_NumInjections = 0;\n    m_CachedInfo = (LEVEL_CONTEXT_INFO) {};\n\n    if (m_RoomMeta != nullptr) {\n        Vector_Free(m_RoomMeta);\n        m_RoomMeta = nullptr;\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nINJECTION_MESH_META Inject_GetRoomMeshMeta(const int32_t room_index)\n{\n    INJECTION_MESH_META summed_meta = {};\n    if (m_RoomMeta == nullptr) {\n        return summed_meta;\n    }\n\n    for (int32_t i = 0; i < m_RoomMeta->count; i++) {\n        const INJECTION_MESH_META *const meta = Vector_Get(m_RoomMeta, i);\n        if (meta->room_index != room_index) {\n            continue;\n        }\n\n        summed_meta.num_vertices += meta->num_vertices;\n        summed_meta.num_quads += meta->num_quads;\n        summed_meta.num_triangles += meta->num_triangles;\n        summed_meta.num_static_2ds += meta->num_static_2ds;\n        summed_meta.num_static_3ds += meta->num_static_3ds;\n    }\n\n    return summed_meta;\n}\n\nint32_t Inject_GetDataCount(const INJECTION_DATA_TYPE type)\n{\n    return m_DataCounts[type];\n}\n\nint32_t Inject_GetMaxStaticObject3DId(void)\n{\n    return m_MaxStaticObject3DId;\n}\n\nint32_t Inject_GetMaxStaticObject2DId(void)\n{\n    return m_MaxStaticObject2DId;\n}\n\nLEVEL_CONTEXT_INFO Inject_GetCachedInfo(void)\n{\n    return m_CachedInfo;\n}\n"
  },
  {
    "path": "src/trx/game/inject/common.h",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/inject/types.h>\n#include <trx/game/level.h>\n\n#define INJECTION_MAGIC MKTAG('T', 'R', 'X', 'J')\n\nvoid Inject_InitLevel(const GF_LEVEL *level, INJECTION_MODE mode);\nvoid Inject_AppendInjection(VFILE *file);\nvoid Inject_AllInjections(void);\nvoid Inject_Cleanup(void);\n\nINJECTION_MESH_META Inject_GetRoomMeshMeta(int32_t room_index);\nint32_t Inject_GetDataCount(INJECTION_DATA_TYPE type);\nint32_t Inject_GetMaxStaticObject3DId(void);\nint32_t Inject_GetMaxStaticObject2DId(void);\nLEVEL_CONTEXT_INFO Inject_GetCachedInfo(void);\n\nvoid Inject_RegisterPaletteMap(const uint16_t *palette_map, int32_t size);\nuint16_t Inject_GetPaletteIndex(uint16_t index);\n\nvoid Inject_RegisterTester(\n    const INJECTION_TEST_TYPE type,\n    bool (*test_func)(const INJECTION_CONTEXT *, const INJECTION *injection));\nvoid Inject_RegisterHandler(\n    INJECTION_CHUNK_TYPE type,\n    void (*handle_func)(const INJECTION_CONTEXT *, INJECTION_CHUNK chunk));\n\n#define REGISTER_INJECT_TESTER(test_type, test_func)                           \\\n    __attribute__((constructor)) static void                                   \\\n    M_RegisterInjectTester##test_type(void)                                    \\\n    {                                                                          \\\n        Inject_RegisterTester(test_type, test_func);                           \\\n    }\n\n#define REGISTER_INJECTOR(chunk_type, handle_func)                             \\\n    __attribute__((constructor)) static void M_RegisterInjector##chunk_type(   \\\n        void)                                                                  \\\n    {                                                                          \\\n        Inject_RegisterHandler(chunk_type, handle_func);                       \\\n    }\n"
  },
  {
    "path": "src/trx/game/inject/data/anims.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/anims.h>\n#include <trx/game/inject.h>\n#include <trx/game/inject/utils.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/sections/append.h>\n#include <trx/game/objects.h>\n\nstatic void M_HandleAnimData(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo();\n\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            VFile_Skip(chunk.injection->fp, data_size);\n            continue;\n        }\n\n        switch (data_type) {\n        case IDT_ANIMS: {\n            Level_Section_AppendAnims(\n                level_info->anims.anim_count, data_count, chunk.injection->fp);\n            level_info->anims.anim_count += data_count;\n\n            for (int32_t j = 0; j < data_count; j++) {\n                ANIM *const anim =\n                    Anim_GetAnim(cached_info.anims.anim_count + j);\n                anim->jump_anim_num += cached_info.anims.anim_count;\n                anim->frame_ofs += cached_info.anims.frame_count * 2;\n                anim->change_idx += cached_info.anims.change_count;\n                anim->command_idx += cached_info.anims.command_count;\n            }\n            break;\n        }\n\n        case IDT_ANIM_BONES: {\n            Level_Section_AppendAnimBones(\n                level_info->anims.bone_count, data_count, chunk.injection->fp);\n            level_info->anims.bone_count += data_count;\n            break;\n        }\n\n        case IDT_ANIM_CHANGES: {\n            Level_Section_AppendAnimChanges(\n                level_info->anims.change_count, data_count,\n                chunk.injection->fp);\n            level_info->anims.change_count += data_count;\n\n            for (int32_t j = 0; j < data_count; j++) {\n                ANIM_CHANGE *const change =\n                    Anim_GetChange(cached_info.anims.change_count + j);\n                change->range_idx += cached_info.anims.range_count;\n            }\n            break;\n        }\n\n        case IDT_ANIM_COMMANDS: {\n            Level_Section_AppendAnimCommands(\n                level_info->anims.command_count, data_count,\n                chunk.injection->fp);\n            level_info->anims.command_count += data_count;\n            break;\n        }\n\n        case IDT_ANIM_FRAMES: {\n            Level_Section_AppendAnimFrames(\n                level_info->anims.frame_count, data_count, chunk.injection->fp);\n            level_info->anims.frame_count += data_count;\n            break;\n        }\n\n        case IDT_ANIM_RANGES: {\n            Level_Section_AppendAnimRanges(\n                level_info->anims.range_count, data_count, chunk.injection->fp);\n            level_info->anims.range_count += data_count;\n\n            for (int32_t j = 0; j < data_count; j++) {\n                ANIM_RANGE *const range =\n                    Anim_GetRange(cached_info.anims.range_count + j);\n                range->link_anim_num += cached_info.anims.anim_count;\n            }\n            break;\n        }\n\n        default:\n            LOG_WARNING(\"Unknown data type: %d\", data_type);\n            VFile_Skip(chunk.injection->fp, data_size);\n            break;\n        }\n    }\n}\n\nstatic void M_CommandEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo();\n    int16_t cmd_idx = cached_info.anims.command_count;\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        const int32_t anim_idx = VFile_ReadS32(injection->fp);\n        const int32_t num_raw_cmds = VFile_ReadS32(injection->fp);\n        const int32_t num_anim_cmds = VFile_ReadS32(injection->fp);\n\n        const OBJECT *const obj = Object_Get(obj_info.id);\n        if (ctx->mode == INJECTION_MODE_STATS || !obj->loaded) {\n            continue;\n        }\n\n        ANIM *const anim = Object_GetAnim(obj, anim_idx);\n        anim->command_idx = cmd_idx;\n        anim->num_commands = num_anim_cmds;\n        cmd_idx += num_raw_cmds;\n    }\n}\n\nREGISTER_INJECTOR(ICT_ANIMATION_DATA, M_HandleAnimData)\nREGISTER_INJECT_EDITOR(IDT_ANIM_CMD_EDITS, M_CommandEdits)\n"
  },
  {
    "path": "src/trx/game/inject/data/camera.c",
    "content": "#include <trx/debug.h>\n#include <trx/game/camera/cinematic.h>\n#include <trx/game/inject.h>\n\nstatic void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file)\n{\n    vertex->x = VFile_ReadS16(file);\n    vertex->y = VFile_ReadS16(file);\n    vertex->z = VFile_ReadS16(file);\n}\n\nstatic void M_HandleCineFrames(\n    const INJECTION *const injection, const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        CINE_FRAME *const frame = Camera_GetCineFrame(i);\n        M_ReadVertex(&frame->target.shift, injection->fp);\n        M_ReadVertex(&frame->camera.shift, injection->fp);\n        frame->fov = VFile_ReadS16(injection->fp);\n        frame->roll = VFile_ReadS16(injection->fp);\n    }\n}\n\nstatic void M_HandleCameraData(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            VFile_Skip(chunk.injection->fp, data_size);\n            continue;\n        }\n\n        switch (data_type) {\n        case IDT_CINEMATIC_FRAMES:\n            M_HandleCineFrames(chunk.injection, data_count);\n            break;\n        default:\n            LOG_WARNING(\"Unknown data type: %d\", data_type);\n            VFile_Skip(chunk.injection->fp, data_size);\n            break;\n        }\n    }\n}\n\nREGISTER_INJECTOR(ICT_CAMERA_DATA, M_HandleCameraData)\n"
  },
  {
    "path": "src/trx/game/inject/data/meshes.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/sections/append.h>\n\nstatic void M_HandleMeshData(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    int32_t mesh_ptr_count = 0;\n    int32_t *mesh_indices = nullptr;\n    const size_t chunk_start_pos = VFile_GetPos(chunk.injection->fp);\n\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            VFile_Skip(chunk.injection->fp, data_size);\n            continue;\n        }\n\n        switch (data_type) {\n        case IDT_MESH_POINTERS: {\n            const int32_t alloc_size = data_count * sizeof(int32_t);\n            mesh_indices = Memory_Alloc(alloc_size);\n            VFile_Read(chunk.injection->fp, mesh_indices, alloc_size);\n            mesh_ptr_count = data_count;\n            break;\n        }\n\n        case IDT_OBJECT_MESHES: {\n            ASSERT(mesh_indices != nullptr);\n            Level_Section_AppendObjectMeshes(\n                mesh_ptr_count, mesh_indices, chunk.injection->fp);\n            LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo();\n            info->mesh_ptr_count += mesh_ptr_count;\n            break;\n        }\n\n        default:\n            LOG_WARNING(\"Unknown data type: %d\", data_type);\n            VFile_Skip(chunk.injection->fp, data_size);\n            break;\n        }\n    }\n\n    Memory_FreePointer(&mesh_indices);\n\n    // Not all mesh data is necessarily read, so ensure to move to the end.\n    VFile_SetPos(chunk.injection->fp, chunk_start_pos + chunk.total_size);\n}\n\nREGISTER_INJECTOR(ICT_MESH_DATA, M_HandleMeshData)\n"
  },
  {
    "path": "src/trx/game/inject/data/objects.c",
    "content": "#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/inject.h>\n#include <trx/game/objects/common.h>\n\nstatic VECTOR *m_ProcessedMeshes = nullptr;\n\nstatic void M_ReadBounds16(BOUNDS_16 *const bounds, VFILE *const file)\n{\n    bounds->min.x = VFile_ReadS16(file);\n    bounds->max.x = VFile_ReadS16(file);\n    bounds->min.y = VFile_ReadS16(file);\n    bounds->max.y = VFile_ReadS16(file);\n    bounds->min.z = VFile_ReadS16(file);\n    bounds->max.z = VFile_ReadS16(file);\n}\n\nstatic void M_AlignTextureReferences(\n    OBJECT_MESH *const mesh, const int32_t tex_info_base)\n{\n    if (Vector_Contains(m_ProcessedMeshes, (void *)mesh)) {\n        return;\n    }\n    Vector_Add(m_ProcessedMeshes, (void *)mesh);\n\n    for (int32_t j = 0; j < mesh->tex_faces.count; j++) {\n        mesh->tex_faces.data[j].texture_idx += tex_info_base;\n    }\n    for (int32_t j = 0; j < mesh->flat_faces.count; j++) {\n        FACE *const face = &mesh->flat_faces.data[j];\n        face->palette_idx = Inject_GetPaletteIndex(face->palette_idx);\n    }\n}\n\nstatic void M_ReadObject(const INJECTION_CHUNK chunk)\n{\n    const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo();\n    const INJECTION_OBJECT_INFO obj_info =\n        Inject_ReadObjectPtr(chunk.injection);\n    OBJECT *const obj = Object_TryGet(obj_info.id);\n    if (obj == nullptr) {\n        LOG_WARNING(\"Invalid object %d\", obj_info.id);\n        VFile_Skip(chunk.injection->fp, 14);\n        return;\n    }\n\n    const int16_t num_meshes = VFile_ReadS16(chunk.injection->fp);\n    const int16_t mesh_idx = VFile_ReadS16(chunk.injection->fp);\n    const int32_t bone_idx =\n        VFile_ReadS32(chunk.injection->fp) / ANIM_BONE_SIZE;\n\n    // Omitted mesh data indicates that we wish to retain what's already\n    // defined in level data to avoid duplicate texture page usage.\n    if (!obj->loaded || num_meshes != 0) {\n        obj->mesh_count = num_meshes;\n        obj->mesh_idx = mesh_idx + cached_info.mesh_ptr_count;\n        obj->bone_idx = bone_idx + cached_info.anims.bone_count;\n    }\n\n    // Ommitted animation data marks that existing related object data should be\n    // retained i.e. mesh replacement only.\n    const uint32_t frame_ofs = VFile_ReadU32(chunk.injection->fp);\n    const int16_t anim_idx = VFile_ReadS16(chunk.injection->fp);\n    if ((int32_t)frame_ofs != -1) {\n        obj->frame_ofs = frame_ofs;\n        obj->frame_base = nullptr;\n        obj->anim_idx = anim_idx;\n        if (obj->anim_idx != -1) {\n            obj->anim_idx += cached_info.anims.anim_count;\n        }\n        obj->loaded = true;\n    }\n\n    for (int32_t i = 0; i < num_meshes; i++) {\n        OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + i);\n        M_AlignTextureReferences(mesh, cached_info.textures.object_count);\n    }\n}\n\nstatic void M_ReadStaticObject3D(const INJECTION_CHUNK chunk)\n{\n    const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo();\n    const int32_t static_id = VFile_ReadS32(chunk.injection->fp);\n\n    if (static_id < 0 || static_id >= Object_GetStaticObjects3DCount()) {\n        LOG_WARNING(\"Invalid static 3D %d\", static_id);\n        VFile_Skip(chunk.injection->fp, 2 + 12 + 12 + 2);\n        return;\n    }\n\n    STATIC_OBJECT_3D *const obj = Object_Get3DStatic(static_id);\n    obj->mesh_idx = VFile_ReadS16(chunk.injection->fp);\n    obj->mesh_idx += cached_info.mesh_ptr_count;\n    obj->loaded = true;\n\n    M_ReadBounds16(&obj->draw_bounds, chunk.injection->fp);\n    M_ReadBounds16(&obj->collision_bounds, chunk.injection->fp);\n\n    const uint16_t flags = VFile_ReadU16(chunk.injection->fp);\n    obj->collidable = (flags & 1) == 0;\n    obj->visible = (flags & 2) != 0;\n\n    OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx);\n    M_AlignTextureReferences(mesh, cached_info.textures.object_count);\n}\n\nstatic void M_HandleObjectData(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    m_ProcessedMeshes = Vector_Create(sizeof(OBJECT_MESH *));\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            VFile_Skip(chunk.injection->fp, data_size);\n            continue;\n        }\n\n        for (int32_t j = 0; j < data_count; j++) {\n            switch (data_type) {\n            case IDT_OBJECTS:\n                M_ReadObject(chunk);\n                break;\n            case IDT_STATIC_OBJECTS:\n                M_ReadStaticObject3D(chunk);\n                break;\n            default:\n                LOG_WARNING(\"Unrecognised object data type %d\", data_type);\n                VFile_Skip(chunk.injection->fp, data_size);\n                break;\n            }\n        }\n    }\n\n    Vector_Free(m_ProcessedMeshes);\n}\n\nREGISTER_INJECTOR(ICT_OBJECT_DATA, M_HandleObjectData)\n"
  },
  {
    "path": "src/trx/game/inject/data/sound.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/inject.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\nstatic void M_HandleSFXData(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    ASSERT(chunk.num_blocks == 1);\n    const INJECTION_DATA_TYPE data_type = VFile_ReadS32(chunk.injection->fp);\n    ASSERT(data_type == IDT_SAMPLE_INFOS);\n    const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n    VFile_Skip(chunk.injection->fp, sizeof(int32_t));\n\n    for (int32_t i = 0; i < data_count; i++) {\n        const SAMPLE_ID sfx_id = VFile_ReadS16(chunk.injection->fp);\n\n        SAMPLE_INFO *const sample_info = Sound_GetOrCreateSample(sfx_id);\n        sample_info->volume = VFile_ReadS16(chunk.injection->fp);\n        sample_info->randomness = VFile_ReadS16(chunk.injection->fp);\n        sample_info->flags.all = VFile_ReadU16(chunk.injection->fp);\n        if (chunk.injection->version >= INJ_VERSION_6) {\n            sample_info->range = VFile_ReadS32(chunk.injection->fp);\n            sample_info->pitch = VFile_ReadS8(chunk.injection->fp);\n        } else {\n            sample_info->range = 10 * WALL_L;\n            sample_info->pitch = 0;\n        }\n\n        if (g_TRVersion == 1) {\n            switch (sample_info->flags.mode_bits) {\n            case 0:\n                sample_info->mode = SAMPLE_MODE_WAIT;\n                break;\n            case 1:\n                sample_info->mode = SAMPLE_MODE_RESTART;\n                break;\n            case 2:\n                sample_info->mode = SAMPLE_MODE_LOOPED;\n                break;\n            case 3:\n                sample_info->mode = SAMPLE_MODE_NORMAL;\n                break;\n            }\n        } else {\n            switch (sample_info->flags.mode_bits) {\n            case 0:\n                sample_info->mode = SAMPLE_MODE_NORMAL;\n                break;\n            case 1:\n                sample_info->mode = SAMPLE_MODE_WAIT;\n                break;\n            case 2:\n                sample_info->mode = SAMPLE_MODE_RESTART;\n                break;\n            case 3:\n                sample_info->mode = SAMPLE_MODE_LOOPED;\n                break;\n            }\n        }\n\n        const int16_t num_samples = sample_info->flags.num_samples;\n        if (g_TRVersion == 1 || chunk.injection->version >= INJ_VERSION_4) {\n            sample_info->number = Sound_ReserveSampleData(-1, num_samples);\n            for (int32_t j = 0; j < num_samples; j++) {\n                const int32_t sample_length =\n                    VFile_ReadS32(chunk.injection->fp);\n                char *const data = Memory_Alloc(sample_length);\n                VFile_Read(chunk.injection->fp, data, sample_length);\n                Sound_LoadSampleData(\n                    sample_info->number + j, data, sample_length);\n                Memory_Free(data);\n            }\n        } else if (g_TRVersion >= 2) {\n            VFile_Skip(chunk.injection->fp, sizeof(int32_t));\n        }\n    }\n}\n\nREGISTER_INJECTOR(ICT_SFX_DATA, M_HandleSFXData)\n"
  },
  {
    "path": "src/trx/game/inject/data/textures.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/sections/append.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output/const.h>\n#include <trx/version.h>\n\nstatic uint16_t M_RemapRGB8(const RGB_888 rgb)\n{\n    const LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    uint16_t best_idx = 0;\n    int32_t best_diff = INT32_MAX;\n    for (int32_t i = 1; i < level_info->palette.size; i++) {\n        const RGB_888 test_rgb = level_info->palette.data_24[i];\n        const int32_t dr = rgb.r - test_rgb.r;\n        const int32_t dg = rgb.g - test_rgb.g;\n        const int32_t db = rgb.b - test_rgb.b;\n        const int32_t diff = SQUARE(dr) + SQUARE(dg) + SQUARE(db);\n        if (diff < best_diff) {\n            best_diff = diff;\n            best_idx = i;\n        }\n    }\n\n    return best_idx;\n}\n\nstatic void M_HandlePalette(\n    const INJECTION *const injection, const int32_t data_count)\n{\n    uint16_t palette_map[data_count];\n    for (int32_t i = 0; i < data_count; i++) {\n        const RGB_888 rgb = {\n            .r = VFile_ReadU8(injection->fp) * 4,\n            .g = VFile_ReadU8(injection->fp) * 4,\n            .b = VFile_ReadU8(injection->fp) * 4,\n        };\n        palette_map[i] = i == 0 ? 0 : M_RemapRGB8(rgb);\n    }\n\n    Inject_RegisterPaletteMap(palette_map, data_count);\n}\n\nstatic void M_HandleTexturePages(\n    const INJECTION *const injection, const int32_t data_count)\n{\n    LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo();\n    if (info->textures.pages_32 == nullptr) {\n        VFile_Skip(\n            injection->fp, data_count * TEXTURE_PAGE_SIZE * sizeof(RGBA_8888));\n        VFile_Skip(injection->fp, data_count * TEXTURE_PAGE_SIZE);\n        return;\n    }\n\n    RGBA_8888 *const output_32 =\n        &info->textures.pages_32[info->textures.page_count * TEXTURE_PAGE_SIZE];\n    VFile_Read(\n        injection->fp, output_32,\n        data_count * TEXTURE_PAGE_SIZE * sizeof(RGBA_8888));\n\n    uint8_t *output_8 =\n        &info->textures.pages_8[info->textures.page_count * TEXTURE_PAGE_SIZE];\n    uint8_t *input_8 = Memory_Alloc(data_count * TEXTURE_PAGE_SIZE);\n    VFile_Read(injection->fp, input_8, data_count * TEXTURE_PAGE_SIZE);\n\n    uint8_t *input_ptr = input_8;\n    for (int32_t i = 0; i < data_count * TEXTURE_PAGE_SIZE; i++) {\n        *output_8++ = Inject_GetPaletteIndex(*input_ptr++);\n    }\n\n    Memory_FreePointer(&input_8);\n    info->textures.page_count += data_count;\n}\n\nstatic void M_HandleSpriteSequences(\n    const INJECTION *const injection, const int32_t data_count)\n{\n    LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        const int16_t num_meshes = VFile_ReadS16(injection->fp);\n        const int16_t mesh_idx = VFile_ReadS16(injection->fp);\n\n        if (obj_info.type == OBJ_TYPE_OBJECT) {\n            OBJECT *const obj = Object_TryGet(obj_info.id);\n            if (obj == nullptr) {\n                LOG_WARNING(\"Invalid object %d\", obj_info.id);\n            } else {\n                obj->mesh_count = num_meshes;\n                obj->mesh_idx = mesh_idx + level_info->textures.sprite_count;\n                obj->loaded = true;\n            }\n        } else if (obj_info.type == OBJ_TYPE_STATIC2D) {\n            STATIC_OBJECT_2D *const obj = Object_Get2DStatic(obj_info.id);\n            if (obj == nullptr) {\n                LOG_WARNING(\"Invalid static 2D %d\", obj_info.id);\n                continue;\n            }\n            obj->frame_count = ABS(num_meshes);\n            obj->texture_idx = mesh_idx + level_info->textures.sprite_count;\n            obj->loaded = true;\n        } else {\n            LOG_WARNING(\"Invalid object type %d\", obj_info.type);\n        }\n        level_info->textures.sprite_count += ABS(num_meshes);\n    }\n}\n\nstatic void M_HandleTextureData(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            VFile_Skip(chunk.injection->fp, data_size);\n            continue;\n        }\n\n        switch (data_type) {\n        case IDT_PALETTE:\n            M_HandlePalette(chunk.injection, data_count);\n            break;\n        case IDT_TEXTURE_PAGES:\n            M_HandleTexturePages(chunk.injection, data_count);\n            break;\n        default:\n            LOG_WARNING(\"Unknown data type: %d\", data_type);\n            VFile_Skip(chunk.injection->fp, data_size);\n            break;\n        }\n    }\n}\n\nstatic void M_HandleTextureInfo(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    const LEVEL_CONTEXT_INFO cached_info = Inject_GetCachedInfo();\n    const int32_t page_base = cached_info.textures.page_count;\n\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            VFile_Skip(chunk.injection->fp, data_size);\n            continue;\n        }\n\n        switch (data_type) {\n        case IDT_OBJECT_TEXTURES:\n            Level_Section_AppendObjectTextures(\n                level_info->textures.object_count, page_base, data_count,\n                chunk.injection->fp, false);\n            level_info->textures.object_count += data_count;\n            break;\n        case IDT_SPRITE_TEXTURES:\n            Level_Section_AppendSpriteTextures(\n                level_info->textures.sprite_count, page_base, data_count,\n                chunk.injection->fp);\n            break;\n        case IDT_SPRITE_SEQUENCES:\n            M_HandleSpriteSequences(chunk.injection, data_count);\n            break;\n        default:\n            LOG_WARNING(\"Unknown data type: %d\", data_type);\n            VFile_Skip(chunk.injection->fp, data_size);\n            break;\n        }\n    }\n}\n\nREGISTER_INJECTOR(ICT_TEXTURE_DATA, M_HandleTextureData)\nREGISTER_INJECTOR(ICT_TEXTURE_INFO, M_HandleTextureInfo)\n"
  },
  {
    "path": "src/trx/game/inject/editor.c",
    "content": "#include <trx/core/log.h>\n#include <trx/game/inject.h>\n\nstatic void (*m_Handlers[IDT_NUMBER_OF])(\n    const INJECTION_CONTEXT *ctx, const INJECTION *injection,\n    int32_t element_count) = {};\n\nstatic void M_HandleDataEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION_CHUNK chunk)\n{\n    for (int32_t i = 0; i < chunk.num_blocks; i++) {\n        const INJECTION_DATA_TYPE data_type =\n            VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_count = VFile_ReadS32(chunk.injection->fp);\n        const int32_t data_size = VFile_ReadS32(chunk.injection->fp);\n\n        if (m_Handlers[data_type] == nullptr) {\n            if (data_type != IDT_ROOM_EDIT_META) {\n                LOG_WARNING(\"Unknown data type: %d\", data_type);\n            }\n            VFile_Skip(chunk.injection->fp, data_size);\n        } else {\n            m_Handlers[data_type](ctx, chunk.injection, data_count);\n        }\n    }\n}\n\nvoid Inject_RegisterEditor(\n    const INJECTION_DATA_TYPE type,\n    void (*handle_func)(\n        const INJECTION_CONTEXT *ctx, const INJECTION *injection,\n        int32_t element_count))\n{\n    m_Handlers[type] = handle_func;\n}\n\nREGISTER_INJECTOR(ICT_DATA_EDITS, M_HandleDataEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editor.h",
    "content": "#include <trx/game/inject/types.h>\n\nvoid Inject_RegisterEditor(\n    INJECTION_DATA_TYPE type,\n    void (*handle_func)(\n        const INJECTION_CONTEXT *ctx, const INJECTION *injection,\n        int32_t element_count));\n\n#define REGISTER_INJECT_EDITOR(data_type, handle_func)                         \\\n    __attribute__((constructor)) static void                                   \\\n    M_RegisterInjectEditor##data_type(void)                                    \\\n    {                                                                          \\\n        Inject_RegisterEditor(data_type, handle_func);                         \\\n    }\n"
  },
  {
    "path": "src/trx/game/inject/editors/anims.c",
    "content": "#include <trx/game/inject.h>\n#include <trx/game/objects/common.h>\n\n#include <string.h>\n\nstatic void M_FrameEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        const int32_t anim_idx = VFile_ReadS32(injection->fp);\n        const int32_t packed_rot = VFile_ReadS32(injection->fp);\n\n        const OBJECT *const obj = Object_Get(obj_info.id);\n        if (ctx->mode == INJECTION_MODE_STATS || !obj->loaded) {\n            continue;\n        }\n\n        const ANIM *const anim = Object_GetAnim(obj, anim_idx);\n        int16_t *data_ptr =\n            &level_info->anims.frames[anim->frame_ofs / sizeof(int16_t)];\n        data_ptr += 10;\n        memcpy(data_ptr, &packed_rot, sizeof(int32_t));\n    }\n}\n\nstatic void M_FrameReplacements(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        const int32_t num_anims = VFile_ReadS32(injection->fp);\n\n        const OBJECT *const obj = Object_Get(obj_info.id);\n        for (int32_t j = 0; j < num_anims; j++) {\n            const int32_t anim_idx = VFile_ReadS32(injection->fp);\n            const int32_t num_frames = VFile_ReadS32(injection->fp);\n\n            if (ctx->mode == INJECTION_MODE_STATS) {\n                VFile_Skip(injection->fp, num_frames * sizeof(int16_t));\n            } else {\n                const ANIM *const anim = Object_GetAnim(obj, anim_idx);\n                int16_t *const data_ptr =\n                    &level_info->anims\n                         .frames[anim->frame_ofs / sizeof(int16_t)];\n                VFile_Read(\n                    injection->fp, data_ptr, num_frames * sizeof(int16_t));\n            }\n        }\n    }\n}\n\nstatic void M_AnimEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        const OBJECT *const obj = Object_Get(obj_info.id);\n        const int32_t anim_idx = VFile_ReadS32(injection->fp);\n        const int32_t velocity = VFile_ReadS32(injection->fp);\n        if (ctx->mode == INJECTION_MODE_STATS) {\n            continue;\n        }\n\n        ANIM *const anim = Object_GetAnim(obj, anim_idx);\n        anim->velocity = velocity;\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_FRAME_EDITS, M_FrameEdits)\nREGISTER_INJECT_EDITOR(IDT_FRAME_REPLACE, M_FrameReplacements)\nREGISTER_INJECT_EDITOR(IDT_ANIM_EDITS, M_AnimEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editors/floor_data.c",
    "content": "#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/game/camera.h>\n#include <trx/game/inject.h>\n#include <trx/game/items.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\n#define NULL_FD_INDEX ((uint16_t)(-1))\n\nstatic void M_TriggerTypeChange(\n    const INJECTION *const injection, const SECTOR *const sector)\n{\n    const uint8_t new_type = VFile_ReadU8(injection->fp);\n    if (sector != nullptr && sector->trigger != nullptr) {\n        sector->trigger->type = new_type;\n    }\n}\n\nstatic void M_TriggerParameterChange(\n    const INJECTION *const injection, const SECTOR *const sector)\n{\n    const uint8_t cmd_type = VFile_ReadU8(injection->fp);\n    const int16_t old_param = VFile_ReadS16(injection->fp);\n    const int16_t new_param = VFile_ReadS16(injection->fp);\n    if (sector == nullptr || sector->trigger == nullptr) {\n        return;\n    }\n\n    // If we can find an action item for the given sector that matches\n    // the command type and old (current) parameter, change it to the\n    // new parameter.\n    TRIGGER_CMD *cmd = sector->trigger->command;\n    for (; cmd != nullptr; cmd = cmd->next_cmd) {\n        if (cmd->type != cmd_type) {\n            continue;\n        }\n\n        if (cmd->type == TO_CAMERA) {\n            TRIGGER_CAMERA_DATA *const cam_data =\n                (TRIGGER_CAMERA_DATA *)cmd->parameter;\n            if (cam_data->camera_num == old_param) {\n                cam_data->camera_num = new_param;\n                break;\n            }\n        } else {\n            if ((int16_t)(intptr_t)cmd->parameter == old_param) {\n                cmd->parameter = (void *)(intptr_t)new_param;\n                break;\n            }\n        }\n    }\n}\n\nstatic void M_SetMusicOneShot(const SECTOR *const sector)\n{\n    if (sector == nullptr || sector->trigger == nullptr) {\n        return;\n    }\n\n    const TRIGGER_CMD *cmd = sector->trigger->command;\n    for (; cmd != nullptr; cmd = cmd->next_cmd) {\n        if (cmd->type == TO_CD) {\n            sector->trigger->one_shot = true;\n            break;\n        }\n    }\n}\n\nstatic void M_FixGlideCamera(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const SECTOR *const sector)\n{\n    if (ctx->mode == INJECTION_MODE_STATS) {\n        VFile_Skip(injection->fp, 8);\n        return;\n    }\n    const uint8_t camera_timer = VFile_ReadU8(injection->fp);\n    const uint8_t glide_timer = VFile_ReadU8(injection->fp);\n    const XYZ_16 camera_shift = {\n        .x = VFile_ReadS16(injection->fp),\n        .y = VFile_ReadS16(injection->fp),\n        .z = VFile_ReadS16(injection->fp),\n    };\n\n    if (sector == nullptr || sector->trigger == nullptr) {\n        return;\n    }\n    if (!g_Config.visuals.enable_glide_cameras) {\n        return;\n    }\n\n    const TRIGGER_CMD *cmd = sector->trigger->command;\n    for (; cmd != nullptr; cmd = cmd->next_cmd) {\n        if (cmd->type != TO_CAMERA) {\n            continue;\n        }\n\n        TRIGGER_CAMERA_DATA *const cam_data =\n            (TRIGGER_CAMERA_DATA *)cmd->parameter;\n        cam_data->timer = camera_timer;\n        cam_data->glide = glide_timer << 3;\n\n        OBJECT_VECTOR *const camera =\n            Camera_GetFixedObject(cam_data->camera_num);\n        camera->pos.x += camera_shift.x;\n        camera->pos.y += camera_shift.y;\n        camera->pos.z += camera_shift.z;\n        break;\n    }\n}\n\nstatic void M_InsertFloorData(\n    const INJECTION *const injection, SECTOR *const sector)\n{\n    const int32_t data_length = VFile_ReadS32(injection->fp);\n    if (data_length < 0) {\n        LOG_WARNING(\n            \"Skipping floor data insert with invalid length: %d\", data_length);\n        return;\n    }\n\n    if (data_length == 0) {\n        Room_PopulateSectorData(sector, nullptr, NULL_FD_INDEX, NULL_FD_INDEX);\n        return;\n    }\n\n    int16_t *data = Memory_Alloc(sizeof(int16_t) * data_length);\n    VFile_Read(injection->fp, data, sizeof(int16_t) * data_length);\n\n    if (sector == nullptr) {\n        Memory_FreePointer(&data);\n        return;\n    }\n\n    // This will reset all FD properties in the sector based on the raw data\n    // imported. We pass a dummy null index to allow it to read from the\n    // beginning of the array.\n    Room_PopulateSectorData(sector, data, 0, NULL_FD_INDEX);\n    Memory_FreePointer(&data);\n}\n\nstatic void M_RoomShift(\n    const INJECTION *const injection, const int16_t room_num)\n{\n    const uint32_t x_shift = ROUND_TO_SECTOR(VFile_ReadU32(injection->fp));\n    const uint32_t z_shift = ROUND_TO_SECTOR(VFile_ReadU32(injection->fp));\n    const int32_t y_shift = ROUND_TO_CLICK(VFile_ReadS32(injection->fp));\n\n    ROOM *const room = Room_Get(room_num);\n    room->pos.x += x_shift;\n    room->pos.z += z_shift;\n    room->min_floor += y_shift;\n    room->max_ceiling += y_shift;\n\n    // Move any items in the room to match.\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (item->room_num != room_num) {\n            continue;\n        }\n\n        item->pos.x += x_shift;\n        item->pos.y += y_shift;\n        item->pos.z += z_shift;\n    }\n\n    if (y_shift == 0) {\n        return;\n    }\n\n    // Update the sector floor and ceiling heights to match.\n    for (int32_t i = 0; i < room->size.z * room->size.x; i++) {\n        SECTOR *const sector = &room->sectors[i];\n        if (sector->floor.height == NO_HEIGHT\n            || sector->ceiling.height == NO_HEIGHT) {\n            continue;\n        }\n\n        sector->floor.height += y_shift;\n        sector->ceiling.height += y_shift;\n    }\n\n    // Update vertex Y values to match; x and z are room-relative.\n    for (int32_t i = 0; i < room->mesh.num_vertices; i++) {\n        room->mesh.vertices[i].pos.y += y_shift;\n    }\n}\n\nstatic void M_TriggeredItem(const INJECTION *const injection)\n{\n    if (Item_GetLevelCount() == MAX_ITEMS) {\n        VFile_Skip(\n            injection->fp,\n            sizeof(int16_t) * 4 + sizeof(int32_t) * 3 + sizeof(uint16_t));\n        LOG_WARNING(\"Cannot add more than %d items\", MAX_ITEMS);\n        return;\n    }\n\n    const int16_t item_num = Item_CreateLevelItem();\n    ITEM *const item = Item_Get(item_num);\n\n    const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n    item->object_id = obj_info.id;\n    item->room_num = VFile_ReadS16(injection->fp);\n    item->pos.x = VFile_ReadS32(injection->fp);\n    item->pos.y = VFile_ReadS32(injection->fp);\n    item->pos.z = VFile_ReadS32(injection->fp);\n    item->rot.y = VFile_ReadS16(injection->fp);\n    item->shade.value_1 = VFile_ReadS16(injection->fp);\n    if (g_TRVersion >= 2) {\n        item->shade.value_2 = item->shade.value_1;\n    }\n    item->flags = VFile_ReadU16(injection->fp);\n\n    if (injection->version < INJ_VERSION_7) {\n        return;\n    }\n\n    const int32_t name_length = VFile_ReadS32(injection->fp);\n    if (name_length <= 0) {\n        return;\n    }\n\n    if (name_length > 4096) {\n        LOG_WARNING(\"Item name too long %d\", name_length);\n        VFile_Skip(injection->fp, name_length);\n        return;\n    }\n\n    char *name = Memory_Alloc((size_t)(name_length + 1));\n    VFile_Read(injection->fp, name, name_length);\n    name[name_length] = '\\0';\n    Item_SetName(item_num, name);\n    Memory_FreePointer(&name);\n}\n\nstatic void M_RoomProperties(\n    const INJECTION *const injection, const int16_t room_num)\n{\n    const uint16_t flags = VFile_ReadU16(injection->fp);\n    ROOM *const room = Room_Get(room_num);\n    // clang-format off\n    room->flags.underwater  = (flags & 0x01) != 0;\n    room->flags.outside     = (flags & 0x08) != 0;\n    room->flags.dynamic_lit = (flags & 0x10) != 0;\n    room->flags.wind        = (flags & 0x20) != 0;\n    room->flags.inside      = (flags & 0x40) != 0;\n    room->flags.swamp       = (flags & 0x80) != 0;\n    // clang-format on\n}\n\nstatic void M_SectorOverwrite(\n    const INJECTION *const injection, SECTOR *const sector)\n{\n    const uint16_t fd_idx = VFile_ReadU16(injection->fp);\n    const int16_t box_idx = VFile_ReadS16(injection->fp);\n    const int16_t pit_room = VFile_ReadS16(injection->fp);\n    const int16_t floor = VFile_ReadS16(injection->fp);\n    const int16_t sky_room = VFile_ReadS16(injection->fp);\n    const int16_t ceiling = VFile_ReadS16(injection->fp);\n\n    if (sector == nullptr) {\n        return;\n    }\n\n    sector->idx = fd_idx;\n    sector->box = box_idx;\n    sector->portal_room.pit = pit_room;\n    sector->floor.height = floor;\n    sector->portal_room.sky = sky_room;\n    sector->ceiling.height = ceiling;\n}\n\nstatic void M_FixZones(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const SECTOR *const sector)\n{\n    if (ctx->mode == INJECTION_MODE_STATS || sector == nullptr\n        || sector->box == NO_BOX) {\n        VFile_Skip(\n            injection->fp, 2 * sizeof(int16_t) * (Box_GetZoneCount() + 1));\n        return;\n    }\n\n    const int16_t box_idx = sector->box;\n    for (int32_t flip_status = 0; flip_status < 2; flip_status++) {\n        for (int32_t zone_idx = 0; zone_idx < Box_GetZoneCount(); zone_idx++) {\n            int16_t *const ground_zone =\n                Box_GetGroundZone(flip_status, zone_idx);\n            ground_zone[box_idx] = VFile_ReadS16(injection->fp);\n        }\n\n        int16_t *const fly_zone = Box_GetFlyZone(flip_status);\n        fly_zone[box_idx] = VFile_ReadS16(injection->fp);\n    }\n}\n\nstatic void M_SetSectorPortals(\n    const INJECTION *const injection, SECTOR *const sector)\n{\n    if (sector == nullptr) {\n        VFile_Skip(injection->fp, 3 * sizeof(int16_t));\n        return;\n    }\n\n    sector->portal_room.wall = VFile_ReadS16(injection->fp);\n    sector->portal_room.sky = VFile_ReadS16(injection->fp);\n    sector->portal_room.pit = VFile_ReadS16(injection->fp);\n}\n\nstatic void M_SetSectorClimbability(\n    const INJECTION *const injection, SECTOR *const sector)\n{\n    const int32_t direction = VFile_ReadS32(injection->fp);\n    if (sector != nullptr) {\n        sector->ladder = (LADDER_DIRECTION)direction;\n    }\n}\n\nstatic void M_SetSectorTriangulation(\n    const INJECTION *const injection, SECTOR *const sector)\n{\n    const int32_t type = VFile_ReadS32(injection->fp);\n\n#define L_TRIANGULATE(test_type, surface)                                      \\\n    do {                                                                       \\\n        if ((type & (1 << test_type)) != 0) {                                  \\\n            const int16_t func_data = VFile_ReadS16(injection->fp);            \\\n            const int16_t tilt_data = VFile_ReadS16(injection->fp);            \\\n            if (sector != nullptr) {                                           \\\n                sector->surface.tilt = (XZ_16) {};                             \\\n                Room_ReadTriangulation(                                        \\\n                    &sector->surface, func_data, tilt_data);                   \\\n            }                                                                  \\\n        }                                                                      \\\n    } while (0)\n\n    L_TRIANGULATE(SURFACE_FLOOR, floor);\n    L_TRIANGULATE(SURFACE_CEILING, ceiling);\n\n#undef L_TRIANGULATE\n}\n\nstatic void M_FloorDataEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int16_t room_num = VFile_ReadS16(injection->fp);\n        const uint16_t x = VFile_ReadU16(injection->fp);\n        const uint16_t z = VFile_ReadU16(injection->fp);\n        const int32_t fd_edit_count = VFile_ReadS32(injection->fp);\n\n        // Verify that the given room and coordinates are accurate.\n        // Individual FD functions must check that sector is actually set.\n        const ROOM *room = nullptr;\n        SECTOR *sector = nullptr;\n        if (room_num < 0 || room_num >= Room_GetCount()) {\n            LOG_WARNING(\"Room index %d is invalid\", room_num);\n        } else {\n            room = Room_Get(room_num);\n            if (x >= room->size.x || z >= room->size.z) {\n                LOG_WARNING(\n                    \"Sector [%d,%d] is invalid for room %d\", x, z, room_num);\n            } else {\n                sector = Room_GetUnitSector(room, x, z);\n            }\n        }\n\n        for (int32_t j = 0; j < fd_edit_count; j++) {\n            const FLOOR_EDIT_TYPE edit_type = VFile_ReadS32(injection->fp);\n            switch (edit_type) {\n            case FET_TRIGGER_TYPE:\n                M_TriggerTypeChange(injection, sector);\n                break;\n            case FET_TRIGGER_PARAM:\n                M_TriggerParameterChange(injection, sector);\n                break;\n            case FET_MUSIC_ONESHOT:\n                M_SetMusicOneShot(sector);\n                break;\n            case FET_GLIDE_CAMERA:\n                M_FixGlideCamera(ctx, injection, sector);\n                break;\n            case FET_FD_INSERT:\n                M_InsertFloorData(injection, sector);\n                break;\n            case FET_ROOM_SHIFT:\n                M_RoomShift(injection, room_num);\n                break;\n            case FET_TRIGGER_ITEM:\n                M_TriggeredItem(injection);\n                break;\n            case FET_ROOM_PROPERTIES:\n                M_RoomProperties(injection, room_num);\n                break;\n            case FET_SECTOR_OVERWRITE:\n                M_SectorOverwrite(injection, sector);\n                break;\n            case FET_ZONE_FIX:\n                M_FixZones(ctx, injection, sector);\n                break;\n            case FET_PORTALS:\n                M_SetSectorPortals(injection, sector);\n                break;\n            case FET_CLIMB:\n                M_SetSectorClimbability(injection, sector);\n                break;\n            case FET_TRIANGULATE:\n                M_SetSectorTriangulation(injection, sector);\n                break;\n            case FET_DELETE_TRIGGER:\n                if (sector != nullptr) {\n                    sector->trigger = nullptr;\n                }\n                break;\n            default:\n                LOG_WARNING(\"Unknown floor data edit type: %d\", edit_type);\n                break;\n            }\n        }\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_FLOOR_EDITS, M_FloorDataEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editors/items.c",
    "content": "#include <trx/core/log.h>\n#include <trx/game/camera.h>\n#include <trx/game/inject.h>\n#include <trx/game/items.h>\n\nstatic void M_ItemPosEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int16_t item_num = VFile_ReadS16(injection->fp);\n        const int16_t y_rot = VFile_ReadS16(injection->fp);\n        const GAME_VECTOR pos = {\n            .x = VFile_ReadS32(injection->fp),\n            .y = VFile_ReadS32(injection->fp),\n            .z = VFile_ReadS32(injection->fp),\n            .room_num = VFile_ReadS16(injection->fp),\n        };\n\n        if (item_num < 0 || item_num >= Item_GetTotalCount()) {\n            LOG_WARNING(\"Item number %d is out of level item range\", item_num);\n            continue;\n        }\n\n        ITEM *const item = Item_Get(item_num);\n        item->rot.y = y_rot;\n        item->pos = pos.pos;\n        item->room_num = pos.room_num;\n    }\n}\n\nstatic void M_ItemFlagEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int16_t item_num = VFile_ReadS16(injection->fp);\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        const uint16_t flags = VFile_ReadU16(injection->fp);\n\n        if (item_num < 0 || item_num >= Item_GetTotalCount()) {\n            LOG_WARNING(\"Item number %d is out of level item range\", item_num);\n            continue;\n        }\n\n        ITEM *const item = Item_Get(item_num);\n        item->object_id = obj_info.id;\n        item->flags = flags;\n    }\n}\n\nstatic void M_CameraEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int16_t camera_num = VFile_ReadS16(injection->fp);\n        const XYZ_32 pos = {\n            .x = VFile_ReadS32(injection->fp),\n            .y = VFile_ReadS32(injection->fp),\n            .z = VFile_ReadS32(injection->fp),\n        };\n        const int16_t room_num = VFile_ReadS16(injection->fp);\n        const int16_t flags = VFile_ReadS16(injection->fp);\n\n        if (camera_num < 0 || camera_num >= Camera_GetFixedObjectCount()) {\n            LOG_WARNING(\n                \"Camera number %d is out of level camera range\", camera_num);\n            continue;\n        }\n\n        OBJECT_VECTOR *const camera = Camera_GetFixedObject(camera_num);\n        camera->pos = pos;\n        camera->data = room_num;\n        camera->flags = flags;\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_ITEM_POS_EDITS, M_ItemPosEdits)\nREGISTER_INJECT_EDITOR(IDT_ITEM_FLAG_EDITS, M_ItemFlagEdits)\nREGISTER_INJECT_EDITOR(IDT_CAMERA_EDITS, M_CameraEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editors/meshes.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/inject.h>\n#include <trx/game/objects/common.h>\n\ntypedef struct {\n    INJECTION_OBJECT_INFO obj_info;\n    int16_t source_identifier;\n    FACE_TYPE face_type;\n    int16_t face_index;\n    int32_t target_count;\n    int16_t *targets;\n} FACE_EDIT;\n\ntypedef struct {\n    int16_t index;\n    XYZ_16 shift;\n} VERTEX_EDIT;\n\ntypedef struct {\n    INJECTION_OBJECT_INFO obj_info;\n    int16_t mesh_idx;\n    XYZ_16 centre_shift;\n    int32_t radius_shift;\n    int32_t face_edit_count;\n    int32_t vertex_edit_count;\n    FACE_EDIT *face_edits;\n    VERTEX_EDIT *vertex_edits;\n} MESH_EDIT;\n\nstatic BOUNDS_16 M_ReadBounds16(VFILE *const file)\n{\n    BOUNDS_16 bounds = {};\n    bounds.min.x = VFile_ReadS16(file);\n    bounds.max.x = VFile_ReadS16(file);\n    bounds.min.y = VFile_ReadS16(file);\n    bounds.max.y = VFile_ReadS16(file);\n    bounds.min.z = VFile_ReadS16(file);\n    bounds.max.z = VFile_ReadS16(file);\n    return bounds;\n}\n\nstatic uint16_t *M_GetMeshTexture(const FACE_EDIT *const edit)\n{\n    const OBJECT *const obj = Object_Get(edit->obj_info.id);\n    if (!obj->loaded) {\n        return nullptr;\n    }\n    ASSERT(edit->source_identifier >= 0);\n    ASSERT(edit->source_identifier < obj->mesh_count);\n\n    const OBJECT_MESH *const mesh =\n        Object_GetMesh(obj->mesh_idx + edit->source_identifier);\n\n    if (edit->face_type == FT_TEXTURED_QUAD) {\n        ASSERT(edit->face_index >= 0);\n        ASSERT(edit->face_index < mesh->tex_face4s.count);\n        FACE *const face = &mesh->tex_face4s.data[edit->face_index];\n        return &face->texture_idx;\n    }\n    if (edit->face_type == FT_TEXTURED_TRIANGLE) {\n        ASSERT(edit->face_index >= 0);\n        ASSERT(edit->face_index < mesh->tex_face3s.count);\n        FACE *const face = &mesh->tex_face3s.data[edit->face_index];\n        return &face->texture_idx;\n    }\n\n    if (edit->face_type == FT_COLOURED_QUAD) {\n        ASSERT(edit->face_index >= 0);\n        ASSERT(edit->face_index < mesh->flat_face4s.count);\n        FACE *const face = &mesh->flat_face4s.data[edit->face_index];\n        return &face->palette_idx;\n    }\n    if (edit->face_type == FT_COLOURED_TRIANGLE) {\n        ASSERT(edit->face_index >= 0);\n        ASSERT(edit->face_index < mesh->flat_face3s.count);\n        FACE *const face = &mesh->flat_face3s.data[edit->face_index];\n        return &face->palette_idx;\n    }\n\n    return nullptr;\n}\n\nstatic void M_ApplyFaceEdit(\n    const FACE_EDIT *const edit, FACE *const faces, const int32_t face_count,\n    const uint16_t texture)\n{\n    for (int32_t i = 0; i < edit->target_count; i++) {\n        ASSERT(edit->targets[i] >= 0);\n        ASSERT(edit->targets[i] < face_count);\n        FACE *const face = &faces[edit->targets[i]];\n        face->texture_idx = texture;\n    }\n}\n\nstatic void M_ApplyMeshEdit(const MESH_EDIT *const edit)\n{\n    OBJECT_MESH *mesh;\n    if (edit->obj_info.type == OBJ_TYPE_OBJECT) {\n        const OBJECT *const obj = Object_Get(edit->obj_info.id);\n        if (!obj->loaded) {\n            return;\n        }\n        ASSERT(edit->mesh_idx >= 0);\n        ASSERT(edit->mesh_idx < obj->mesh_count);\n\n        mesh = Object_GetMesh(obj->mesh_idx + edit->mesh_idx);\n    } else if (edit->obj_info.type == OBJ_TYPE_STATIC3D) {\n        if (edit->obj_info.id < 0\n            || edit->obj_info.id >= Object_GetStaticObjects3DCount()) {\n            return;\n        }\n        const STATIC_OBJECT_3D *const obj =\n            Object_Get3DStatic(edit->obj_info.id);\n        mesh = Object_GetMesh(obj->mesh_idx);\n    } else {\n        LOG_WARNING(\"Invalid mesh edit type %d\", edit->obj_info.type);\n        return;\n    }\n\n    mesh->center.x += edit->centre_shift.x;\n    mesh->center.y += edit->centre_shift.y;\n    mesh->center.z += edit->centre_shift.z;\n    mesh->radius += edit->radius_shift;\n\n    for (int32_t i = 0; i < edit->vertex_edit_count; i++) {\n        const VERTEX_EDIT *const vertex_edit = &edit->vertex_edits[i];\n        if (vertex_edit->index < 0\n            || vertex_edit->index >= mesh->num_vertices) {\n            const int32_t object_id = edit->obj_info.type == OBJ_TYPE_OBJECT\n                ? Object_ToGameID(edit->obj_info.id)\n                : edit->obj_info.id;\n            LOG_ERROR(\n                \"Invalid mesh vertex edit: obj_type=%d obj_id=%d mesh_idx=%d \"\n                \"vertex_idx=%d num_vertices=%d\",\n                edit->obj_info.type, object_id, edit->mesh_idx,\n                vertex_edit->index, mesh->num_vertices);\n        }\n        ASSERT(vertex_edit->index >= 0);\n        ASSERT(vertex_edit->index < mesh->num_vertices);\n        XYZ_16 *const vertex = &mesh->vertices[vertex_edit->index];\n        vertex->x += vertex_edit->shift.x;\n        vertex->y += vertex_edit->shift.y;\n        vertex->z += vertex_edit->shift.z;\n    }\n\n    // Find each face we are interested in and replace its texture\n    // or palette reference with the one selected from each edit's\n    // instructions.\n    for (int32_t i = 0; i < edit->face_edit_count; i++) {\n        const FACE_EDIT *const face_edit = &edit->face_edits[i];\n        uint16_t texture;\n        if (face_edit->source_identifier < 0) {\n            texture = Inject_GetPaletteIndex(-face_edit->source_identifier);\n        } else {\n            const uint16_t *const tex_ptr = M_GetMeshTexture(face_edit);\n            if (tex_ptr == nullptr) {\n                continue;\n            }\n            texture = *tex_ptr;\n        }\n\n        switch (face_edit->face_type) {\n        case FT_TEXTURED_QUAD:\n            M_ApplyFaceEdit(\n                face_edit, mesh->tex_face4s.data, mesh->tex_face4s.count,\n                texture);\n            break;\n        case FT_TEXTURED_TRIANGLE:\n            M_ApplyFaceEdit(\n                face_edit, mesh->tex_face3s.data, mesh->tex_face3s.count,\n                texture);\n            break;\n        case FT_COLOURED_QUAD:\n            M_ApplyFaceEdit(\n                face_edit, mesh->flat_face4s.data, mesh->flat_face4s.count,\n                texture);\n            break;\n        case FT_COLOURED_TRIANGLE:\n            M_ApplyFaceEdit(\n                face_edit, mesh->flat_face3s.data, mesh->flat_face3s.count,\n                texture);\n            break;\n        }\n    }\n}\n\nstatic void M_MeshEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        MESH_EDIT edit = {\n            .obj_info = Inject_ReadObjectPtr(injection),\n            .mesh_idx = VFile_ReadS16(injection->fp),\n            .centre_shift.x = VFile_ReadS16(injection->fp),\n            .centre_shift.y = VFile_ReadS16(injection->fp),\n            .centre_shift.z = VFile_ReadS16(injection->fp),\n            .radius_shift = VFile_ReadS32(injection->fp),\n        };\n\n        edit.face_edit_count = VFile_ReadS32(injection->fp);\n        edit.face_edits =\n            Memory_Alloc(sizeof(FACE_EDIT) * edit.face_edit_count);\n        for (int32_t j = 0; j < edit.face_edit_count; j++) {\n            FACE_EDIT *const face_edit = &edit.face_edits[j];\n            face_edit->obj_info = Inject_ReadObjectPtr(injection);\n            face_edit->source_identifier = VFile_ReadS16(injection->fp);\n            face_edit->face_type = VFile_ReadS32(injection->fp);\n            face_edit->face_index = VFile_ReadS16(injection->fp);\n\n            face_edit->target_count = VFile_ReadS32(injection->fp);\n            face_edit->targets =\n                Memory_Alloc(sizeof(int16_t) * face_edit->target_count);\n            VFile_Read(\n                injection->fp, face_edit->targets,\n                sizeof(int16_t) * face_edit->target_count);\n        }\n\n        edit.vertex_edit_count = VFile_ReadS32(injection->fp);\n        edit.vertex_edits =\n            Memory_Alloc(sizeof(VERTEX_EDIT) * edit.vertex_edit_count);\n        for (int32_t j = 0; j < edit.vertex_edit_count; j++) {\n            VERTEX_EDIT *vertex_edit = &edit.vertex_edits[j];\n            vertex_edit->index = VFile_ReadS16(injection->fp);\n            vertex_edit->shift.x = VFile_ReadS16(injection->fp);\n            vertex_edit->shift.y = VFile_ReadS16(injection->fp);\n            vertex_edit->shift.z = VFile_ReadS16(injection->fp);\n        }\n\n        if (ctx->mode != INJECTION_MODE_STATS) {\n            M_ApplyMeshEdit(&edit);\n        }\n\n        for (int32_t j = 0; j < edit.face_edit_count; j++) {\n            FACE_EDIT *face_edit = &edit.face_edits[j];\n            Memory_FreePointer(&face_edit->targets);\n        }\n\n        Memory_FreePointer(&edit.face_edits);\n        Memory_FreePointer(&edit.vertex_edits);\n    }\n}\n\nstatic void M_Object3DEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int32_t obj_id = VFile_ReadS32(injection->fp);\n        const bool collidable = VFile_ReadU8(injection->fp) == 1;\n        const bool visible = VFile_ReadU8(injection->fp) == 1;\n        const BOUNDS_16 collision_bounds = M_ReadBounds16(injection->fp);\n        const BOUNDS_16 draw_bounds = M_ReadBounds16(injection->fp);\n\n        if (obj_id < 0 || obj_id >= Object_GetStaticObjects3DCount()) {\n            continue;\n        }\n        STATIC_OBJECT_3D *const obj = Object_Get3DStatic(obj_id);\n        if (!obj->loaded) {\n            continue;\n        }\n\n        obj->collidable = collidable;\n        obj->visible = visible;\n        obj->collision_bounds = collision_bounds;\n        obj->draw_bounds = draw_bounds;\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_MESH_EDITS, M_MeshEdits)\nREGISTER_INJECT_EDITOR(IDT_OBJECT_3D_EDITS, M_Object3DEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editors/objects.c",
    "content": "#include <trx/game/inject.h>\n#include <trx/game/objects.h>\n\nstatic void M_ObjectTypeEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO base_obj_info =\n            Inject_ReadObjectPtr(injection);\n        const INJECTION_OBJECT_INFO target_obj_info =\n            Inject_ReadObjectPtr(injection);\n\n        OBJECT *const base_obj = Object_TryGet(base_obj_info.id);\n        const OBJECT *const target_obj = Object_TryGet(target_obj_info.id);\n        if (base_obj == nullptr || target_obj == nullptr) {\n            continue;\n        }\n        base_obj->setup_func = target_obj->setup_func;\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_OBJ_TYPE_EDITS, M_ObjectTypeEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editors/rooms.c",
    "content": "#include <trx/core/log.h>\n#include <trx/game/inject.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\nstatic uint16_t *M_GetRoomTexture(\n    const int16_t room_num, const FACE_TYPE face_type, const int16_t face_index)\n{\n    const ROOM *const room = Room_Get(room_num);\n    if (face_type == FT_TEXTURED_QUAD && face_index < room->mesh.face4s.count) {\n        FACE *const face = &room->mesh.face4s.data[face_index];\n        return &face->texture_idx;\n    }\n\n    if (face_type == FT_TEXTURED_TRIANGLE\n        && face_index < room->mesh.face3s.count) {\n        FACE *const face = &room->mesh.face3s.data[face_index];\n        return &face->texture_idx;\n    }\n\n    LOG_WARNING(\n        \"Invalid room face lookup: %d, %d, %d\", room_num, face_type,\n        face_index);\n    return nullptr;\n}\n\nstatic void M_TextureRoomFace(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    const FACE_TYPE target_face_type = VFile_ReadS32(injection->fp);\n    const int16_t target_face = VFile_ReadS16(injection->fp);\n    const int16_t source_room = VFile_ReadS16(injection->fp);\n    const FACE_TYPE source_face_type = VFile_ReadS32(injection->fp);\n    const int16_t source_face = VFile_ReadS16(injection->fp);\n\n    const uint16_t *const source_texture =\n        M_GetRoomTexture(source_room, source_face_type, source_face);\n    uint16_t *const target_texture =\n        M_GetRoomTexture(target_room, target_face_type, target_face);\n    if (source_texture != nullptr && target_texture != nullptr) {\n        *target_texture = *source_texture;\n    }\n}\n\nstatic uint16_t *M_GetRoomFaceVertices(\n    const int16_t room_num, const FACE_TYPE face_type, const int16_t face_index)\n{\n    if (room_num < 0 || room_num >= Room_GetCount()) {\n        LOG_WARNING(\"Room index %d is invalid\", room_num);\n        return nullptr;\n    }\n\n    const ROOM *const room = Room_Get(room_num);\n    if (face_type == FT_TEXTURED_QUAD) {\n        if (face_index < 0 || face_index >= room->mesh.face4s.count) {\n            LOG_WARNING(\n                \"Face4 index %d, room %d is invalid\", face_index, room_num);\n            return nullptr;\n        }\n\n        FACE *const face = &room->mesh.face4s.data[face_index];\n        return (uint16_t *)(void *)&face->vertices;\n    }\n\n    if (face_type == FT_TEXTURED_TRIANGLE) {\n        if (face_index < 0 || face_index >= room->mesh.face3s.count) {\n            LOG_WARNING(\n                \"Face3 index %d, room %d is invalid\", face_index, room_num);\n            return nullptr;\n        }\n\n        FACE *const face = &room->mesh.face3s.data[face_index];\n        return (uint16_t *)(void *)&face->vertices;\n    }\n\n    return nullptr;\n}\n\nstatic void M_MoveRoomFace(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    const FACE_TYPE face_type = VFile_ReadS32(injection->fp);\n    const int16_t target_face = VFile_ReadS16(injection->fp);\n    const int32_t vertex_count = VFile_ReadS32(injection->fp);\n\n    for (int32_t j = 0; j < vertex_count; j++) {\n        const int16_t vertex_index = VFile_ReadS16(injection->fp);\n        const int16_t new_vertex = VFile_ReadS16(injection->fp);\n\n        uint16_t *const vertices =\n            M_GetRoomFaceVertices(target_room, face_type, target_face);\n        if (vertices != nullptr) {\n            vertices[vertex_index] = new_vertex;\n        }\n    }\n}\n\nstatic void M_AlterRoomVertex(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n    const int16_t target_vertex = VFile_ReadS16(injection->fp);\n    const int16_t x_change = VFile_ReadS16(injection->fp);\n    const int16_t y_change = VFile_ReadS16(injection->fp);\n    const int16_t z_change = VFile_ReadS16(injection->fp);\n    const int16_t shade_change = VFile_ReadS16(injection->fp);\n\n    if (target_room < 0 || target_room >= Room_GetCount()) {\n        LOG_WARNING(\"Room index %d is invalid\", target_room);\n        return;\n    }\n\n    const ROOM *const room = Room_Get(target_room);\n    if (target_vertex < 0 || target_vertex >= room->mesh.num_vertices) {\n        LOG_WARNING(\n            \"Vertex index %d, room %d is invalid\", target_vertex, target_room);\n        return;\n    }\n\n    ROOM_VERTEX *const vertex = &room->mesh.vertices[target_vertex];\n    vertex->pos.x += x_change;\n    vertex->pos.y += y_change;\n    vertex->pos.z += z_change;\n    vertex->light_base += shade_change;\n}\n\nstatic void M_SetVertexFlags(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n    const int16_t target_vertex = VFile_ReadS16(injection->fp);\n    const uint16_t flags = VFile_ReadU16(injection->fp);\n\n    if (target_room < 0 || target_room >= Room_GetCount()) {\n        LOG_WARNING(\"Room index %d is invalid\", target_room);\n        return;\n    }\n\n    const ROOM *const room = Room_Get(target_room);\n    if (target_vertex < 0 || target_vertex >= room->mesh.num_vertices) {\n        LOG_WARNING(\n            \"Vertex index %d, room %d is invalid\", target_vertex, target_room);\n        return;\n    }\n\n    ROOM_VERTEX *const vertex = &room->mesh.vertices[target_vertex];\n    if (g_TRVersion == 1) {\n        vertex->flags.disable_wibble = (flags & 0x2000u) != 0u;\n    } else {\n        vertex->flags.disable_wibble = (flags & 0x8000u) != 0u;\n        vertex->light_table_value = flags & 0xFF;\n    }\n}\n\nstatic void M_RotateRoomFace(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    const FACE_TYPE face_type = VFile_ReadS32(injection->fp);\n    const int16_t target_face = VFile_ReadS16(injection->fp);\n    const uint8_t num_rotations = VFile_ReadU8(injection->fp);\n\n    uint16_t *const face_vertices =\n        M_GetRoomFaceVertices(target_room, face_type, target_face);\n    if (face_vertices == nullptr) {\n        return;\n    }\n\n    const int32_t num_vertices = face_type == FT_TEXTURED_QUAD ? 4 : 3;\n    uint16_t *vertices[num_vertices];\n    for (int32_t i = 0; i < num_vertices; i++) {\n        vertices[i] = face_vertices + i;\n    }\n\n    for (int32_t i = 0; i < num_rotations; i++) {\n        const uint16_t first = *vertices[0];\n        for (int32_t j = 0; j < num_vertices - 1; j++) {\n            *vertices[j] = *vertices[j + 1];\n        }\n        *vertices[num_vertices - 1] = first;\n    }\n}\n\nstatic void M_AddRoomFace(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    const FACE_TYPE face_type = VFile_ReadS32(injection->fp);\n    const int16_t source_room = VFile_ReadS16(injection->fp);\n    const int16_t source_face = VFile_ReadS16(injection->fp);\n\n    const int32_t num_vertices = face_type == FT_TEXTURED_QUAD ? 4 : 3;\n    uint16_t vertices[num_vertices];\n    for (int32_t i = 0; i < num_vertices; i++) {\n        vertices[i] = VFile_ReadU16(injection->fp);\n    }\n\n    if (target_room < 0 || target_room >= Room_GetCount()) {\n        LOG_WARNING(\"Room index %d is invalid\", target_room);\n        return;\n    }\n\n    const uint16_t *const source_texture =\n        M_GetRoomTexture(source_room, face_type, source_face);\n    if (source_texture == nullptr) {\n        return;\n    }\n\n    ROOM *const room = Room_Get(target_room);\n    uint16_t *face_vertices;\n    if (face_type == FT_TEXTURED_QUAD) {\n        FACE *const face = &room->mesh.face4s.data[room->mesh.face4s.count];\n        face->texture_idx = *source_texture;\n        for (int32_t i = 0; i < face->vertex_count; i++) {\n            face->texture_zw[i].z = 1.0f;\n            face->texture_zw[i].w = 1.0f;\n        }\n        face_vertices = face->vertices;\n        room->mesh.face4s.count++;\n    } else {\n        FACE *const face = &room->mesh.face3s.data[room->mesh.face3s.count];\n        face->vertex_count = 3;\n        for (int32_t i = 0; i < face->vertex_count; i++) {\n            face->texture_zw[i].z = 1.0f;\n            face->texture_zw[i].w = 1.0f;\n        }\n        face->texture_idx = *source_texture;\n        face_vertices = face->vertices;\n        room->mesh.face3s.count++;\n    }\n\n    for (int32_t i = 0; i < num_vertices; i++) {\n        face_vertices[i] = vertices[i];\n    }\n}\n\nstatic void M_AddRoomVertex(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n    const XYZ_16 pos = {\n        .x = VFile_ReadS16(injection->fp),\n        .y = VFile_ReadS16(injection->fp),\n        .z = VFile_ReadS16(injection->fp),\n    };\n    int16_t shade = 0;\n    RGBA_8888 color = COLOR_RGBA_8888_WHITE;\n    if (g_TRVersion < 3) {\n        shade = VFile_ReadS16(injection->fp);\n    } else {\n        color = Color_ARGB1555ToRGBA8888(VFile_ReadU16(injection->fp));\n        color.a = 255;\n    }\n\n    ROOM *const room = Room_Get(target_room);\n    ROOM_VERTEX *const vertex = &room->mesh.vertices[room->mesh.num_vertices];\n    vertex->pos = pos;\n    vertex->light_base = shade;\n    vertex->flags.disable_wibble = false;\n    vertex->flags.move = false;\n    vertex->flags.glow = false;\n    vertex->light_table_value = 0;\n    vertex->color = color;\n    room->mesh.num_vertices++;\n}\n\nstatic void M_AddRoomStatic2D(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n    const int32_t id = VFile_ReadS32(injection->fp);\n    const uint16_t vertex = VFile_ReadU16(injection->fp);\n    const uint16_t frame_idx = VFile_ReadU16(injection->fp);\n\n    const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(id);\n    if (obj == nullptr) {\n        LOG_WARNING(\"Invalid static 2D id: %d\", id);\n        return;\n    }\n    if (!obj->loaded) {\n        LOG_WARNING(\"Static 2D %d is not loaded\", id);\n        return;\n    }\n\n    if (frame_idx >= obj->frame_count) {\n        LOG_WARNING(\"Invalid frame (%d) on static 2D %d\", frame_idx, id);\n        return;\n    }\n\n    ROOM *const room = Room_Get(target_room);\n    ROOM_SPRITE *const sprite =\n        &room->mesh.sprites.data[room->mesh.sprites.count];\n    sprite->vertex = vertex;\n    sprite->texture = obj->texture_idx + frame_idx;\n    room->mesh.sprites.count++;\n}\n\nstatic void M_AddRoomStatic3D(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n    ROOM *const room = Room_Get(target_room);\n\n    STATIC_MESH *const mesh = &room->static_meshes[room->num_static_meshes];\n    mesh->pos.x = VFile_ReadS32(injection->fp);\n    mesh->pos.y = VFile_ReadS32(injection->fp);\n    mesh->pos.z = VFile_ReadS32(injection->fp);\n    mesh->rot.y = VFile_ReadS16(injection->fp);\n    mesh->shade.value_1 = VFile_ReadS16(injection->fp);\n    if (g_TRVersion >= 2) {\n        mesh->shade.value_2 = mesh->shade.value_1;\n    }\n    mesh->static_num = VFile_ReadS16(injection->fp);\n    mesh->draw_num = -1;\n\n    room->num_static_meshes++;\n}\n\nstatic void M_EditRoomStatic3D(const INJECTION *const injection)\n{\n    const int16_t target_room = VFile_ReadS16(injection->fp);\n    VFile_Skip(injection->fp, sizeof(int32_t));\n    const ROOM *const room = Room_Get(target_room);\n    const int32_t mesh_idx = VFile_ReadS32(injection->fp);\n    if (mesh_idx < 0 || mesh_idx >= room->num_static_meshes) {\n        LOG_WARNING(\n            \"Invalid static mesh index (%d) for room %d\", mesh_idx,\n            target_room);\n        VFile_Skip(injection->fp, 4 * sizeof(int32_t));\n        return;\n    }\n\n    STATIC_MESH *const mesh = &room->static_meshes[mesh_idx];\n    mesh->pos.x = VFile_ReadS32(injection->fp);\n    mesh->pos.y = VFile_ReadS32(injection->fp);\n    mesh->pos.z = VFile_ReadS32(injection->fp);\n    mesh->rot.y = VFile_ReadS16(injection->fp);\n    mesh->shade.value_1 = VFile_ReadS16(injection->fp);\n    if (g_TRVersion >= 2) {\n        mesh->shade.value_2 = mesh->shade.value_1;\n    }\n}\n\nstatic void M_RoomMeshEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const ROOM_MESH_EDIT_TYPE type = VFile_ReadS32(injection->fp);\n        switch (type) {\n        case RMET_TEXTURE_FACE:\n            M_TextureRoomFace(injection);\n            break;\n        case RMET_MOVE_FACE:\n            M_MoveRoomFace(injection);\n            break;\n        case RMET_ALTER_VERTEX:\n            M_AlterRoomVertex(injection);\n            break;\n        case RMET_VERTEX_FLAGS:\n            M_SetVertexFlags(injection);\n            break;\n        case RMET_ROTATE_FACE:\n            M_RotateRoomFace(injection);\n            break;\n        case RMET_ADD_FACE:\n            M_AddRoomFace(injection);\n            break;\n        case RMET_ADD_VERTEX:\n            M_AddRoomVertex(injection);\n            break;\n        case RMET_ADD_STATIC_2D:\n            M_AddRoomStatic2D(injection);\n            break;\n        case RMET_ADD_STATIC_3D:\n            M_AddRoomStatic3D(injection);\n            break;\n        case RMET_EDIT_STATIC_3D:\n            M_EditRoomStatic3D(injection);\n            break;\n        default:\n            LOG_WARNING(\"Unrecognised room mesh edit type: %d\", type);\n            break;\n        }\n    }\n}\n\nstatic void M_RoomPortalEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int16_t base_room = VFile_ReadS16(injection->fp);\n        const int16_t link_room = VFile_ReadS16(injection->fp);\n        const int16_t portal_index = VFile_ReadS16(injection->fp);\n\n        if (base_room < 0 || base_room >= Room_GetCount()) {\n            VFile_Skip(injection->fp, sizeof(int16_t) * 12);\n            LOG_WARNING(\"Room index %d is invalid\", base_room);\n            continue;\n        }\n\n        const ROOM *const room = Room_Get(base_room);\n        PORTAL *portal = nullptr;\n        for (int32_t j = 0; j < room->portals->count; j++) {\n            const PORTAL room_portal = room->portals->portal[j];\n            if (room_portal.room_num == link_room && j == portal_index) {\n                portal = &room->portals->portal[j];\n                break;\n            }\n        }\n\n        if (portal == nullptr) {\n            VFile_Skip(injection->fp, sizeof(int16_t) * 12);\n            LOG_WARNING(\n                \"Room index %d has no matching portal to %d\", base_room,\n                link_room);\n            continue;\n        }\n\n        bool empty_portal = true;\n        for (int32_t j = 0; j < 4; j++) {\n            portal->vertex[j].x += VFile_ReadS16(injection->fp);\n            portal->vertex[j].y += VFile_ReadS16(injection->fp);\n            portal->vertex[j].z += VFile_ReadS16(injection->fp);\n            empty_portal &= portal->vertex[j].x == 0 && portal->vertex[j].y == 0\n                && portal->vertex[j].z == 0;\n        }\n\n        // An injection that has reset all vertices such that the portal size is\n        // now 0, should be interpreted as a command to ignore that portal.\n        if (empty_portal) {\n            for (int32_t j = portal_index + 1; j < room->portals->count; j++) {\n                room->portals->portal[j - 1] = room->portals->portal[j];\n            }\n            room->portals->portal[room->portals->count - 1] = *portal;\n            room->portals->count--;\n        }\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_ROOM_EDITS, M_RoomMeshEdits)\nREGISTER_INJECT_EDITOR(IDT_VIS_PORTAL_EDITS, M_RoomPortalEdits)\n"
  },
  {
    "path": "src/trx/game/inject/editors/textures.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/game/inject.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n\nstatic void M_TextureEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    const LEVEL_CONTEXT_INFO *const level_info = Level_Context_GetInfo();\n    for (int32_t i = 0; i < data_count; i++) {\n        const uint16_t target_page = VFile_ReadU16(injection->fp);\n        const uint8_t target_x = VFile_ReadU8(injection->fp);\n        const uint8_t target_y = VFile_ReadU8(injection->fp);\n        const uint16_t source_width = VFile_ReadU16(injection->fp);\n        const uint16_t source_height = VFile_ReadU16(injection->fp);\n\n        const int32_t size = source_width * source_height * sizeof(RGBA_8888);\n        RGBA_8888 *source_img = Memory_Alloc(size);\n        VFile_Read(injection->fp, source_img, size);\n\n        if (target_page >= level_info->textures.page_count) {\n            LOG_WARNING(\"Texture page %d is beyond level range\", target_page);\n            continue;\n        }\n\n        RGBA_8888 *page =\n            &level_info->textures.pages_32[target_page * TEXTURE_PAGE_SIZE];\n        for (int32_t y = 0; y < source_height; y++) {\n            for (int32_t x = 0; x < source_width; x++) {\n                const int32_t target_pixel =\n                    (y + target_y) * TEXTURE_PAGE_WIDTH + x + target_x;\n                RGBA_8888 *const target_rgb = &page[target_pixel];\n                *target_rgb = source_img[y * source_width + x];\n            }\n        }\n\n        Memory_FreePointer(&source_img);\n    }\n}\n\nstatic void M_SpriteEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n        int16_t x0 = VFile_ReadS16(injection->fp);\n        int16_t y0 = VFile_ReadS16(injection->fp);\n        int16_t x1 = VFile_ReadS16(injection->fp);\n        int16_t y1 = VFile_ReadS16(injection->fp);\n\n        const OBJECT *const obj = Object_TryGet(obj_info.id);\n        if (obj == nullptr || !obj->loaded) {\n            continue;\n        }\n        if (obj->mesh_idx < 0\n            || obj->mesh_idx >= Output_GetSpriteTextureCount()) {\n            LOG_WARNING(\n                \"Invalid sprite texture index %d for object %d\", obj->mesh_idx,\n                obj_info.id);\n            continue;\n        }\n        SPRITE_TEXTURE *const sprite_texture =\n            Output_GetSpriteTexture(obj->mesh_idx);\n        sprite_texture->x0 = x0;\n        sprite_texture->x1 = x1;\n        sprite_texture->y0 = y0;\n        sprite_texture->y1 = y1;\n    }\n}\n\nstatic void M_AnimTextureEdits(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection,\n    const int32_t data_count)\n{\n    for (int32_t i = 0; i < data_count; i++) {\n        const int32_t range_idx = VFile_ReadS32(injection->fp);\n        const int32_t num_textures = VFile_ReadS32(injection->fp);\n        if (num_textures <= 0) {\n            continue;\n        }\n\n        ANIMATED_TEXTURE_RANGE *const range =\n            Output_GetAnimatedTextureRange(range_idx);\n        const size_t range_size = num_textures * sizeof(int16_t);\n        if (range->num_textures == num_textures) {\n            VFile_Read(injection->fp, range->textures, range_size);\n        } else {\n            LOG_WARNING(\n                \"Expected %d textures for animation range %d; got %d\",\n                range->num_textures, range_idx, num_textures);\n            VFile_Skip(injection->fp, range_size);\n        }\n    }\n}\n\nREGISTER_INJECT_EDITOR(IDT_TEXTURE_EDITS, M_TextureEdits)\nREGISTER_INJECT_EDITOR(IDT_SPRITE_EDITS, M_SpriteEdits)\nREGISTER_INJECT_EDITOR(IDT_ANIM_TEXTURES, M_AnimTextureEdits)\n"
  },
  {
    "path": "src/trx/game/inject/enum.h",
    "content": "#pragma once\n\n// clang-format off\ntypedef enum {\n    INJECTION_MODE_STATS,\n    INJECTION_MODE_FULL,\n} INJECTION_MODE;\n\ntypedef enum {\n    INJ_VERSION_1 = 1,\n    INJ_VERSION_2 = 2,\n    INJ_VERSION_3 = 3,\n    INJ_VERSION_4 = 4,\n    INJ_VERSION_5 = 5,\n    INJ_VERSION_6 = 6,\n    INJ_VERSION_7 = 7,\n    INJ_CURRENT_VERSION = INJ_VERSION_7,\n} INJECTION_VERSION;\n\ntypedef enum {\n    IFT_GENERAL           = 0,\n    IFT_BRAID             = 1,\n    IFT_TEXTURE_FIX       = 2,\n    IFT_PS1_SFX           = 3,\n    IFT_FLOOR_DATA        = 4,\n    IFT_LARA_ANIMS        = 5,\n    IFT_ITEM_POSITION     = 6,\n    IFT_PS1_ENEMY         = 7,\n    IFT_ALTER_ANIM_SPRITE = 8,\n    IFT_SKYBOX            = 9,\n    IFT_PS1_CRYSTAL       = 10,\n    IFT_NUMBER_OF         = 11,\n} INJECTION_FILE_TYPE;\n\ntypedef enum {\n    ITT_ITEM_META  = 0,\n    ITT_ROOM_COUNT = 1,\n    ITT_ROOM_META  = 2,\n    ITT_NUMBER_OF  = 3,\n} INJECTION_TEST_TYPE;\n\ntypedef enum {\n    ICT_TEXTURE_DATA   = 0,\n    ICT_TEXTURE_INFO   = 1,\n    ICT_MESH_DATA      = 2,\n    ICT_ANIMATION_DATA = 3,\n    ICT_OBJECT_DATA    = 4,\n    ICT_SFX_DATA       = 5,\n    ICT_DATA_EDITS     = 6,\n    ICT_CAMERA_DATA    = 7,\n    ICT_NUMBER_OF      = 8,\n} INJECTION_CHUNK_TYPE;\n\ntypedef enum {\n    IDT_PALETTE          = 0,\n    IDT_TEXTURE_PAGES    = 1,\n    IDT_OBJECT_TEXTURES  = 2,\n    IDT_SPRITE_TEXTURES  = 3,\n    IDT_SPRITE_SEQUENCES = 4,\n    IDT_OBJECT_MESHES    = 5,\n    IDT_MESH_POINTERS    = 6,\n    IDT_ANIM_CHANGES     = 7,\n    IDT_ANIM_RANGES      = 8,\n    IDT_ANIM_COMMANDS    = 9,\n    IDT_ANIM_BONES       = 10,\n    IDT_ANIM_FRAMES      = 11,\n    IDT_ANIMS            = 12,\n    IDT_OBJECTS          = 13,\n    IDT_SAMPLE_INFOS     = 14,\n    IDT_SAMPLE_INDICES   = 15,\n    IDT_SAMPLE_DATA      = 16,\n    IDT_FLOOR_EDITS      = 17,\n    IDT_ITEM_POS_EDITS   = 18,\n    IDT_MESH_EDITS       = 19,\n    IDT_TEXTURE_EDITS    = 20,\n    IDT_ROOM_EDIT_META   = 21,\n    IDT_ROOM_EDITS       = 22,\n    IDT_VIS_PORTAL_EDITS = 23,\n    IDT_CAMERA_EDITS     = 24,\n    IDT_FRAME_EDITS      = 25,\n    IDT_OBJECT_3D_EDITS  = 26,\n    IDT_ANIM_CMD_EDITS   = 27,\n    IDT_SPRITE_EDITS     = 28,\n    IDT_STATIC_OBJECTS   = 29,\n    IDT_CINEMATIC_FRAMES = 30,\n    IDT_OBJ_TYPE_EDITS   = 31,\n    IDT_FRAME_REPLACE    = 32,\n    IDT_ITEM_FLAG_EDITS  = 33,\n    IDT_ANIM_EDITS       = 34,\n    IDT_ANIM_TEXTURES    = 35,\n    IDT_NUMBER_OF        = 36,\n} INJECTION_DATA_TYPE;\n\ntypedef enum {\n    FT_TEXTURED_QUAD     = 0,\n    FT_TEXTURED_TRIANGLE = 1,\n    FT_COLOURED_QUAD     = 2,\n    FT_COLOURED_TRIANGLE = 3,\n} FACE_TYPE;\n\ntypedef enum {\n    FET_TRIGGER_PARAM     = 0,\n    FET_MUSIC_ONESHOT     = 1,\n    FET_FD_INSERT         = 2,\n    FET_ROOM_SHIFT        = 3,\n    FET_TRIGGER_ITEM      = 4,\n    FET_ROOM_PROPERTIES   = 5,\n    FET_TRIGGER_TYPE      = 6,\n    FET_SECTOR_OVERWRITE  = 7,\n    FET_GLIDE_CAMERA      = 8,\n    FET_ZONE_FIX          = 9,\n    FET_PORTALS           = 10,\n    FET_CLIMB             = 11,\n    FET_DELETE_TRIGGER    = 12,\n    FET_TRIANGULATE       = 13,\n} FLOOR_EDIT_TYPE;\n\ntypedef enum {\n    RMET_TEXTURE_FACE   = 0,\n    RMET_MOVE_FACE      = 1,\n    RMET_ALTER_VERTEX   = 2,\n    RMET_ROTATE_FACE    = 3,\n    RMET_ADD_FACE       = 4,\n    RMET_ADD_VERTEX     = 5,\n    RMET_ADD_STATIC_2D  = 6,\n    RMET_ADD_STATIC_3D  = 7,\n    RMET_EDIT_STATIC_3D = 8,\n    RMET_VERTEX_FLAGS   = 9,\n} ROOM_MESH_EDIT_TYPE;\n// clang-format on\n\ntypedef enum {\n    OBJ_TYPE_OBJECT = 0,\n    OBJ_TYPE_STATIC2D = 1,\n    OBJ_TYPE_STATIC3D = 2,\n} INJECT_OBJECT_TYPE;\n"
  },
  {
    "path": "src/trx/game/inject/testers/items.c",
    "content": "#include <trx/game/inject.h>\n#include <trx/game/items.h>\n#include <trx/game/objects/common.h>\n\nstatic bool M_TestItemMeta(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection)\n{\n    const int32_t item_num = VFile_ReadS32(injection->fp);\n    const INJECTION_OBJECT_INFO obj_info = Inject_ReadObjectPtr(injection);\n    const XYZ_32 pos = {\n        .x = VFile_ReadS32(injection->fp),\n        .y = VFile_ReadS32(injection->fp),\n        .z = VFile_ReadS32(injection->fp),\n    };\n    const int16_t room_num = VFile_ReadS16(injection->fp);\n    const int16_t y_rot = VFile_ReadS16(injection->fp);\n\n    if (item_num < 0 || item_num >= Item_GetTotalCount()) {\n        return false;\n    }\n\n    const ITEM *const item = Item_Get(item_num);\n    return item->object_id == obj_info.id\n        && XYZ_32_AreEquivalent(item->pos, pos) && item->room_num == room_num\n        && item->rot.y == y_rot;\n}\n\nREGISTER_INJECT_TESTER(ITT_ITEM_META, M_TestItemMeta)\n"
  },
  {
    "path": "src/trx/game/inject/testers/rooms.c",
    "content": "#include <trx/game/inject.h>\n#include <trx/game/rooms.h>\n\nstatic bool M_TestRoomCount(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection)\n{\n    const int32_t num_rooms = VFile_ReadS32(injection->fp);\n    return num_rooms == Room_GetCount();\n}\n\nstatic bool M_TestRoomMeta(\n    const INJECTION_CONTEXT *const ctx, const INJECTION *const injection)\n{\n    const int32_t room_num = VFile_ReadS32(injection->fp);\n    const int32_t x_pos = VFile_ReadS32(injection->fp);\n    const int32_t z_pos = VFile_ReadS32(injection->fp);\n    const int32_t min_floor = VFile_ReadS32(injection->fp);\n    const int32_t max_ceiling = VFile_ReadS32(injection->fp);\n    const uint16_t x_size = VFile_ReadU16(injection->fp);\n    const uint16_t z_size = VFile_ReadU16(injection->fp);\n\n    if (room_num < 0 || room_num >= Room_GetCount()) {\n        return false;\n    }\n\n    const ROOM *const room = Room_Get(room_num);\n    return room->pos.x == x_pos && room->pos.z == z_pos\n        && room->min_floor == min_floor && room->max_ceiling == max_ceiling\n        && room->size.x == x_size && room->size.z == z_size;\n}\n\nREGISTER_INJECT_TESTER(ITT_ROOM_COUNT, M_TestRoomCount)\nREGISTER_INJECT_TESTER(ITT_ROOM_META, M_TestRoomMeta)\n"
  },
  {
    "path": "src/trx/game/inject/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/core/virtual_file.h>\n#include <trx/game/inject/enum.h>\n#include <trx/game/objects/ids.h>\n\ntypedef struct {\n    INJECTION_MODE mode;\n} INJECTION_CONTEXT;\n\ntypedef struct {\n    char *path;\n    VFILE *fp;\n    INJECTION_VERSION version;\n    INJECTION_FILE_TYPE type;\n    bool relevant;\n} INJECTION;\n\ntypedef struct {\n    const INJECTION *injection;\n    INJECTION_CHUNK_TYPE type;\n    int32_t num_blocks;\n    int32_t total_size;\n} INJECTION_CHUNK;\n\ntypedef struct {\n    int16_t room_index;\n    int16_t num_vertices;\n    int16_t num_quads;\n    int16_t num_triangles;\n    int16_t num_static_2ds;\n    int16_t num_static_3ds;\n} INJECTION_MESH_META;\n\ntypedef struct {\n    INJECT_OBJECT_TYPE type;\n    int32_t id;\n} INJECTION_OBJECT_INFO;\n"
  },
  {
    "path": "src/trx/game/inject/utils.c",
    "content": "#include <trx/game/inject/utils.h>\n\n#include <trx/game/objects/common.h>\n\nINJECTION_OBJECT_INFO Inject_ReadObjectPtr(const INJECTION *const injection)\n{\n    INJECTION_OBJECT_INFO obj_info = {\n        .type = VFile_ReadS32(injection->fp),\n        .id = VFile_ReadS32(injection->fp),\n    };\n\n    if (obj_info.type == OBJ_TYPE_OBJECT) {\n        obj_info.id = Object_FromGameID(obj_info.id);\n        if (injection->version < INJ_VERSION_5) {\n            VFile_Skip(injection->fp, 16);\n        }\n    }\n\n    return obj_info;\n}\n"
  },
  {
    "path": "src/trx/game/inject/utils.h",
    "content": "#pragma once\n\n#include <trx/core/virtual_file.h>\n#include <trx/game/inject/types.h>\n\nINJECTION_OBJECT_INFO Inject_ReadObjectPtr(const INJECTION *injection);\n"
  },
  {
    "path": "src/trx/game/inject.h",
    "content": "#pragma once\n\n#include <trx/game/inject/common.h>\n#include <trx/game/inject/editor.h>\n#include <trx/game/inject/types.h>\n#include <trx/game/inject/utils.h>\n"
  },
  {
    "path": "src/trx/game/input/backends/base.h",
    "content": "#pragma once\n\n#include <trx/game/input/common.h>\n\n#include <SDL2/SDL_events.h>\n\ntypedef struct {\n    void (*init)(void);\n    void (*shutdown)(void);\n    void (*discover)(void);\n    bool (*custom_update)(INPUT_STATE *result, INPUT_LAYOUT layout);\n    void (*process_event)(const SDL_Event *event);\n    bool (*is_pressed)(INPUT_LAYOUT layout, INPUT_ROLE role);\n    bool (*is_role_conflicted)(INPUT_LAYOUT layout, INPUT_ROLE role);\n    const char *(*get_name)(INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n    void (*unassign_role)(INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n    bool (*assign_from_json_object)(\n        INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot,\n        JSON_OBJECT *bind_obj);\n    bool (*assign_to_json_object)(\n        INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot,\n        JSON_OBJECT *bind_obj);\n    void (*reset_layout)(INPUT_LAYOUT layout);\n    bool (*read_and_assign)(INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n    void (*resolve_combos)(INPUT_LAYOUT layout, INPUT_STATE *result);\n} INPUT_BACKEND_IMPL;\n"
  },
  {
    "path": "src/trx/game/input/backends/controller.c",
    "content": "#include <trx/game/input/backends/controller.h>\n\n#include <trx/core/log.h>\n#include <trx/game/input/backends/internal.h>\n#include <trx/game/input/combo.h>\n\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_gamecontroller.h>\n#include <string.h>\n\ntypedef enum {\n    BT_BUTTON = 0,\n    BT_AXIS = 1,\n} BUTTON_TYPE;\n\ntypedef struct {\n    BUTTON_TYPE type;\n    union {\n        SDL_GameControllerButton button;\n        SDL_GameControllerAxis axis;\n    } bind;\n    int16_t axis_dir;\n} CONTROLLER_MAP;\n\ntypedef struct {\n    INPUT_ROLE role;\n    CONTROLLER_MAP map;\n} BUILTIN_CONTROLLER_LAYOUT;\n\nstatic BUILTIN_CONTROLLER_LAYOUT m_BuiltinLayout[] = {\n#define INPUT_CONTROLLER_ASSIGN_BUTTON(role, bind)                             \\\n    { role, { BT_BUTTON, { .button = bind }, 0 } },\n#define INPUT_CONTROLLER_ASSIGN_AXIS(role, bind, axis_dir)                     \\\n    { role, { BT_AXIS, { .axis = bind }, axis_dir } },\n#include <trx/game/input/backends/controller.def>\n    // guard\n    { -1, { 0, { 0 }, 0 } },\n};\n\n// clang-format off\n#define M_ICON_X              \"\\\\{controller button cross}\"\n#define M_ICON_CIRCLE         \"\\\\{controller button circle}\"\n#define M_ICON_SQUARE         \"\\\\{controller button square}\"\n#define M_ICON_TRIANGLE       \"\\\\{controller button triangle}\"\n#define M_ICON_UP             \"\\\\{controller dpad up}\"\n#define M_ICON_DOWN           \"\\\\{controller dpad down}\"\n#define M_ICON_LEFT           \"\\\\{controller dpad left}\"\n#define M_ICON_RIGHT          \"\\\\{controller dpad right}\"\n#define M_ICON_L1             \"\\\\{controller button l1}\"\n#define M_ICON_R1             \"\\\\{controller button r1}\"\n#define M_ICON_L2             \"\\\\{controller button l2}\"\n#define M_ICON_R2             \"\\\\{controller button r2}\"\n\n#define M_NAME_L_STICK        \"\\\\{controller lstick}\"\n#define M_NAME_L_ANALOG_UP    \"\\\\{controller lstick up}\"\n#define M_NAME_L_ANALOG_RIGHT \"\\\\{controller lstick right}\"\n#define M_NAME_L_ANALOG_DOWN  \"\\\\{controller lstick down}\"\n#define M_NAME_L_ANALOG_LEFT  \"\\\\{controller lstick left}\"\n#define M_NAME_R_STICK        \"\\\\{controller rstick}\"\n#define M_NAME_R_ANALOG_UP    \"\\\\{controller rstick up}\"\n#define M_NAME_R_ANALOG_RIGHT \"\\\\{controller rstick right}\"\n#define M_NAME_R_ANALOG_DOWN  \"\\\\{controller rstick down}\"\n#define M_NAME_R_ANALOG_LEFT  \"\\\\{controller rstick left}\"\n#define M_NAME_L_TRIGGER      \"\\\\{controller trigger left}\"\n#define M_NAME_R_TRIGGER      \"\\\\{controller trigger right}\"\n\n#define M_NAME_ZL             \"\\\\{controller button zl}\"\n#define M_NAME_ZR             \"\\\\{controller button zr}\"\n\n#define M_NAME_XBOX           \"\\\\{controller button xbox}\"\n#define M_NAME_PS             \"\\\\{controller button ps}\"\n#define M_NAME_PS_SHARE       \"\\\\{controller button share}\"\n#define M_NAME_PS_OPTIONS     \"\\\\{controller button options}\"\n\n#define M_NAME_BACK           \"\\\\{controller button back}\"\n#define M_NAME_HOME           \"\\\\{controller button home}\"\n#define M_NAME_START          \"\\\\{controller button home}\"\n#define M_NAME_SHARE          \"\\\\{controller button share}\"\n#define M_NAME_CAPTURE        \"\\\\{controller button capture}\"\n#define M_NAME_TOUCHPAD       \"\\\\{controller button touchpad}\"\n#define M_NAME_MIC            \"\\\\{controller button mic}\"\n#define M_NAME_PADDLE_1       \"\\\\{controller button paddle 1}\"\n#define M_NAME_PADDLE_2       \"\\\\{controller button paddle 2}\"\n#define M_NAME_PADDLE_3       \"\\\\{controller button paddle 3}\"\n#define M_NAME_PADDLE_4       \"\\\\{controller button paddle 4}\"\n\n#define M_NAME_L_BUMPER       \"\\\\{controller bumper left}\"\n#define M_NAME_R_BUMPER       \"\\\\{controller bumper right}\"\n\n#define M_NAME_A              \"\\\\{controller button a}\"\n#define M_NAME_B              \"\\\\{controller button b}\"\n#define M_NAME_X              \"\\\\{controller button x}\"\n#define M_NAME_Y              \"\\\\{controller button y}\"\n// clang-format on\n\ntypedef struct {\n    int32_t key_count;\n    CONTROLLER_MAP keys[INPUT_COMBO_MAX_KEYS];\n} CONTROLLER_BINDING;\n\ntypedef struct {\n    CONTROLLER_BINDING slots[INPUT_BINDING_SLOTS];\n} CONTROLLER_ROLE_BINDING;\n\nstatic CONTROLLER_ROLE_BINDING m_Layout[INPUT_LAYOUT_NUMBER_OF]\n                                       [INPUT_ROLE_NUMBER_OF];\n\nstatic SDL_GameController *m_Controller = nullptr;\nstatic const char *m_ControllerName = nullptr;\nstatic SDL_GameControllerType m_ControllerType = SDL_CONTROLLER_TYPE_UNKNOWN;\n\nstatic bool m_Conflicts[INPUT_LAYOUT_NUMBER_OF][INPUT_ROLE_NUMBER_OF] = {};\n\n// Internal controller state tables updated via SDL events\nstatic bool m_ButtonState[SDL_CONTROLLER_BUTTON_MAX] = {};\nstatic int16_t m_AxisState[SDL_CONTROLLER_AXIS_MAX] = {};\n\nstatic const char *M_GetButtonName(const SDL_GameControllerButton button)\n{\n    // First switch: Handle platform-specific deviations from defaults\n    switch (m_ControllerType) {\n    case SDL_CONTROLLER_TYPE_PS3:\n    case SDL_CONTROLLER_TYPE_PS4:\n    case SDL_CONTROLLER_TYPE_PS5:\n        // clang-format off\n        switch (button) {\n        case SDL_CONTROLLER_BUTTON_A:             return M_ICON_X;\n        case SDL_CONTROLLER_BUTTON_B:             return M_ICON_CIRCLE;\n        case SDL_CONTROLLER_BUTTON_X:             return M_ICON_SQUARE;\n        case SDL_CONTROLLER_BUTTON_Y:             return M_ICON_TRIANGLE;\n        case SDL_CONTROLLER_BUTTON_BACK:          return M_NAME_PS_OPTIONS;\n        case SDL_CONTROLLER_BUTTON_START:         return M_NAME_PS_SHARE;\n        case SDL_CONTROLLER_BUTTON_LEFTSTICK:     return M_NAME_L_STICK;\n        case SDL_CONTROLLER_BUTTON_RIGHTSTICK:    return M_NAME_R_STICK;\n        case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:  return M_ICON_L1;\n        case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return M_ICON_R1;\n        case SDL_CONTROLLER_BUTTON_MISC1:         return M_NAME_MIC;\n        case SDL_CONTROLLER_BUTTON_GUIDE:         return M_NAME_PS;\n        default: break;\n        }\n        // clang-format on\n        break;\n\n    case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:\n    case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:\n        // clang-format off\n        switch (button) {\n        case SDL_CONTROLLER_BUTTON_A:             return M_NAME_B;\n        case SDL_CONTROLLER_BUTTON_B:             return M_NAME_A;\n        case SDL_CONTROLLER_BUTTON_X:             return M_NAME_Y;\n        case SDL_CONTROLLER_BUTTON_Y:             return M_NAME_X;\n        case SDL_CONTROLLER_BUTTON_START:         return M_NAME_START;\n        case SDL_CONTROLLER_BUTTON_MISC1:         return M_NAME_CAPTURE;\n        default: break;\n        }\n        // clang-format on\n        break;\n\n    case SDL_CONTROLLER_TYPE_XBOX360:\n    case SDL_CONTROLLER_TYPE_XBOXONE:\n        // clang-format off\n        switch (button) {\n        case SDL_CONTROLLER_BUTTON_GUIDE:         return M_NAME_XBOX;\n        default: break;\n        }\n        // clang-format on\n        break;\n\n    default:\n        break;\n    }\n\n    // Second switch: Provide default mappings for all keys\n    switch (button) {\n    case SDL_CONTROLLER_BUTTON_INVALID:\n    case SDL_CONTROLLER_BUTTON_MAX:\n        return nullptr;\n\n        // clang-format off\n    case SDL_CONTROLLER_BUTTON_A:             return M_NAME_A;\n    case SDL_CONTROLLER_BUTTON_B:             return M_NAME_B;\n    case SDL_CONTROLLER_BUTTON_X:             return M_NAME_X;\n    case SDL_CONTROLLER_BUTTON_Y:             return M_NAME_Y;\n    case SDL_CONTROLLER_BUTTON_BACK:          return M_NAME_BACK;\n    case SDL_CONTROLLER_BUTTON_GUIDE:         return M_NAME_HOME;\n    case SDL_CONTROLLER_BUTTON_START:         return M_NAME_START;\n    case SDL_CONTROLLER_BUTTON_LEFTSTICK:     return M_NAME_L_STICK;\n    case SDL_CONTROLLER_BUTTON_RIGHTSTICK:    return M_NAME_R_STICK;\n    case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:  return M_NAME_L_BUMPER;\n    case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return M_NAME_R_BUMPER;\n    case SDL_CONTROLLER_BUTTON_DPAD_UP:       return M_ICON_UP;\n    case SDL_CONTROLLER_BUTTON_DPAD_DOWN:     return M_ICON_DOWN;\n    case SDL_CONTROLLER_BUTTON_DPAD_LEFT:     return M_ICON_LEFT;\n    case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:    return M_ICON_RIGHT;\n    case SDL_CONTROLLER_BUTTON_MISC1:         return M_NAME_SHARE;\n    case SDL_CONTROLLER_BUTTON_PADDLE1:       return M_NAME_PADDLE_1;\n    case SDL_CONTROLLER_BUTTON_PADDLE2:       return M_NAME_PADDLE_2;\n    case SDL_CONTROLLER_BUTTON_PADDLE3:       return M_NAME_PADDLE_3;\n    case SDL_CONTROLLER_BUTTON_PADDLE4:       return M_NAME_PADDLE_4;\n    case SDL_CONTROLLER_BUTTON_TOUCHPAD:      return M_NAME_TOUCHPAD;\n        // clang-format on\n\n    default:\n        return \"????\";\n    }\n}\n\n// Update internal controller button/axis state from SDL events.\n// @param event     Event to process.\nstatic void M_ProcessEvent(const SDL_Event *const event)\n{\n    switch (event->type) {\n    case SDL_CONTROLLERBUTTONDOWN:\n        m_ButtonState[event->cbutton.button] = true;\n        break;\n    case SDL_CONTROLLERBUTTONUP:\n        m_ButtonState[event->cbutton.button] = false;\n        break;\n    case SDL_CONTROLLERAXISMOTION: {\n        const Sint16 value = event->caxis.value;\n        if (value < -SDL_JOYSTICK_AXIS_MAX / 2) {\n            m_AxisState[event->caxis.axis] = -1;\n        } else if (value > SDL_JOYSTICK_AXIS_MAX / 2) {\n            m_AxisState[event->caxis.axis] = 1;\n        } else {\n            m_AxisState[event->caxis.axis] = 0;\n        }\n        break;\n    }\n    default:\n        break;\n    }\n}\n\nstatic bool M_JoyBtn(const SDL_GameControllerButton button)\n{\n    if (m_Controller == nullptr || button == SDL_CONTROLLER_BUTTON_INVALID) {\n        return false;\n    }\n    return m_ButtonState[button];\n}\n\nstatic int16_t M_JoyAxis(const SDL_GameControllerAxis axis)\n{\n    if (m_Controller == nullptr || axis == SDL_CONTROLLER_AXIS_INVALID) {\n        return false;\n    }\n    return m_AxisState[axis];\n}\n\nstatic bool M_CheckMap(const CONTROLLER_MAP *const map)\n{\n    if (map->type == BT_BUTTON) {\n        return M_JoyBtn(map->bind.button);\n    } else {\n        return M_JoyAxis(map->bind.axis) == map->axis_dir;\n    }\n}\n\nstatic bool M_CheckBinding(const CONTROLLER_BINDING *const bind)\n{\n    if (bind->key_count == 0) {\n        return false;\n    }\n    for (int32_t k = 0; k < bind->key_count; k++) {\n        if (!M_CheckMap(&bind->keys[k])) {\n            return false;\n        }\n    }\n    return true;\n}\n\n// Combo adapter forward declarations.\nstatic INPUT_COMBO_BINDING M_GetComboBinding(\n    INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\nstatic bool M_ComboKeysEqual(const void *a, const void *b);\n\nstatic bool M_GetBindState(const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n        const CONTROLLER_BINDING *bind = &m_Layout[layout][role].slots[slot];\n        if (bind->key_count >= 2\n            && Input_ComboIsKeyImmediate(\n                layout, &bind->keys[0], M_GetComboBinding, M_ComboKeysEqual)) {\n            continue;\n        }\n        if (M_CheckBinding(bind)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic const CONTROLLER_BINDING *M_GetBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    return &m_Layout[layout][role].slots[slot];\n}\n\nstatic bool M_MapsEqual(\n    const CONTROLLER_MAP *const a, const CONTROLLER_MAP *const b)\n{\n    if (a->type != b->type) {\n        return false;\n    }\n    if (a->type == BT_BUTTON) {\n        return a->bind.button == b->bind.button;\n    }\n    return a->bind.axis == b->bind.axis && a->axis_dir == b->axis_dir;\n}\n\nstatic bool M_BindingsEqual(\n    const CONTROLLER_BINDING *const a, const CONTROLLER_BINDING *const b)\n{\n    if (a->key_count != b->key_count || a->key_count == 0) {\n        return false;\n    }\n    for (int32_t i = 0; i < a->key_count; i++) {\n        if (!M_MapsEqual(&a->keys[i], &b->keys[i])) {\n            return false;\n        }\n    }\n    return true;\n}\n\nstatic bool M_CheckConflict(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role1, const INPUT_ROLE role2)\n{\n    for (int32_t s1 = 0; s1 < INPUT_BINDING_SLOTS; s1++) {\n        const CONTROLLER_BINDING *b1 = M_GetBinding(layout, role1, s1);\n        if (b1->key_count == 0) {\n            continue;\n        }\n        for (int32_t s2 = 0; s2 < INPUT_BINDING_SLOTS; s2++) {\n            const CONTROLLER_BINDING *b2 = M_GetBinding(layout, role2, s2);\n            if (M_BindingsEqual(b1, b2)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nstatic void M_AssignConflict(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const bool conflict)\n{\n    m_Conflicts[layout][role] = conflict;\n}\n\nstatic void M_CheckConflicts(const INPUT_LAYOUT layout)\n{\n    Input_ConflictHelper(layout, M_CheckConflict, M_AssignConflict);\n    Input_ComboCheckConflicts(\n        layout, M_GetComboBinding, M_ComboKeysEqual, m_Conflicts[layout]);\n}\n\nstatic int16_t M_GetAssignedButtonType(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot);\n    return bind->key_count > 0 ? bind->keys[0].type : BT_BUTTON;\n}\n\nstatic int16_t M_GetAssignedBind(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot);\n    if (bind->key_count == 0) {\n        return SDL_CONTROLLER_BUTTON_INVALID;\n    }\n    const CONTROLLER_MAP *map = &bind->keys[0];\n    if (map->type == BT_BUTTON) {\n        return map->bind.button;\n    } else {\n        return map->bind.axis;\n    }\n}\n\nstatic int16_t M_GetAssignedAxisDir(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot);\n    return bind->key_count > 0 ? bind->keys[0].axis_dir : 0;\n}\n\nstatic void M_AssignBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    const CONTROLLER_BINDING *const bind)\n{\n    m_Layout[layout][role].slots[slot] = *bind;\n    M_CheckConflicts(layout);\n}\n\nstatic void M_AssignButton(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    const int16_t button)\n{\n    const CONTROLLER_BINDING bind = {\n        .key_count = button != SDL_CONTROLLER_BUTTON_INVALID ? 1 : 0,\n        .keys = { { BT_BUTTON, { .button = button }, 0 } },\n    };\n    M_AssignBinding(layout, role, slot, &bind);\n}\n\nstatic void M_AssignAxis(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    const int16_t axis, const int16_t axis_dir)\n{\n    const CONTROLLER_BINDING bind = {\n        .key_count = 1,\n        .keys = { { BT_AXIS, { .axis = axis }, axis_dir } },\n    };\n    M_AssignBinding(layout, role, slot, &bind);\n}\n\nstatic SDL_GameController *M_FindController(void)\n{\n    if (m_Controller != nullptr) {\n        return m_Controller;\n    }\n\n    int32_t controllers = SDL_NumJoysticks();\n    LOG_INFO(\"%d controllers\", controllers);\n    for (int32_t i = 0; i < controllers; i++) {\n        m_ControllerName = SDL_GameControllerNameForIndex(i);\n        m_ControllerType = SDL_GameControllerTypeForIndex(i);\n        bool is_game_controller = SDL_IsGameController(i);\n        LOG_DEBUG(\n            \"controller %d: %s %d (%d)\", i, m_ControllerName, m_ControllerType,\n            is_game_controller);\n        if (is_game_controller) {\n            SDL_GameController *const result = SDL_GameControllerOpen(i);\n            if (result == nullptr) {\n                LOG_ERROR(\"Could not open controller: %s\", SDL_GetError());\n            }\n            return result;\n        }\n    }\n\n    return nullptr;\n}\n\nstatic void M_ResetLayout(const INPUT_LAYOUT layout)\n{\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        m_Layout[layout][role] = m_Layout[INPUT_LAYOUT_DEFAULT][role];\n    }\n    M_CheckConflicts(layout);\n}\n\nstatic void M_Discover(void)\n{\n    if (m_Controller != nullptr) {\n        SDL_GameControllerClose(m_Controller);\n        m_Controller = nullptr;\n    }\n    m_Controller = M_FindController();\n}\n\nstatic void M_Init(void)\n{\n    // first, reset all roles to unbound\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n            m_Layout[INPUT_LAYOUT_DEFAULT][role].slots[slot] =\n                (CONTROLLER_BINDING) { .key_count = 0 };\n        }\n    }\n    // then load actually defined default bindings into slot 0\n    for (int32_t i = 0; m_BuiltinLayout[i].role != (INPUT_ROLE)-1; i++) {\n        const BUILTIN_CONTROLLER_LAYOUT *const builtin = &m_BuiltinLayout[i];\n        m_Layout[INPUT_LAYOUT_DEFAULT][builtin->role].slots[0] =\n            (CONTROLLER_BINDING) {\n                .key_count = 1,\n                .keys = { builtin->map },\n            };\n    }\n    M_CheckConflicts(INPUT_LAYOUT_DEFAULT);\n\n    for (int32_t layout = INPUT_LAYOUT_CUSTOM_1;\n         layout < INPUT_LAYOUT_NUMBER_OF; layout++) {\n        M_ResetLayout(layout);\n    }\n\n    int32_t result = SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR);\n    if (result < 0) {\n        LOG_ERROR(\"Error while calling SDL_Init: 0x%lx\", result);\n    } else {\n        M_Discover();\n    }\n}\n\nstatic void M_Shutdown(void)\n{\n    if (m_Controller != nullptr) {\n        SDL_GameControllerClose(m_Controller);\n        m_Controller = nullptr;\n    }\n}\n\nstatic bool M_CustomUpdate(INPUT_STATE *const result, const INPUT_LAYOUT layout)\n{\n    if (m_Controller == nullptr) {\n        return false;\n    }\n    result->menu_back |= M_JoyBtn(SDL_CONTROLLER_BUTTON_Y);\n    result->menu_skip = result->menu_confirm || result->menu_back;\n    return true;\n}\n\nstatic bool M_IsPressed(const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    return M_GetBindState(layout, role);\n}\n\nstatic const char *M_GetAxisName(\n    const SDL_GameControllerAxis axis, const int16_t axis_dir)\n{\n    // clang-format off\n    switch (m_ControllerType) {\n        case SDL_CONTROLLER_TYPE_PS3:\n        case SDL_CONTROLLER_TYPE_PS4:\n        case SDL_CONTROLLER_TYPE_PS5:\n            switch (axis) {\n                case SDL_CONTROLLER_AXIS_INVALID:         return nullptr;\n                case SDL_CONTROLLER_AXIS_LEFTX:           return axis_dir == -1 ? M_NAME_L_ANALOG_LEFT : M_NAME_L_ANALOG_RIGHT;\n                case SDL_CONTROLLER_AXIS_LEFTY:           return axis_dir == -1 ? M_NAME_L_ANALOG_UP : M_NAME_L_ANALOG_DOWN;\n                case SDL_CONTROLLER_AXIS_RIGHTX:          return axis_dir == -1 ? M_NAME_R_ANALOG_LEFT : M_NAME_R_ANALOG_RIGHT;\n                case SDL_CONTROLLER_AXIS_RIGHTY:          return axis_dir == -1 ? M_NAME_R_ANALOG_UP : M_NAME_R_ANALOG_DOWN;\n                case SDL_CONTROLLER_AXIS_TRIGGERLEFT:     return M_ICON_L2;\n                case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:    return M_ICON_R2;\n                case SDL_CONTROLLER_AXIS_MAX:             return nullptr;\n            }\n            break;\n\n        case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:\n        case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:\n            switch (axis) {\n                case SDL_CONTROLLER_AXIS_INVALID:         return nullptr;\n                case SDL_CONTROLLER_AXIS_LEFTX:           return axis_dir == -1 ? M_NAME_L_ANALOG_LEFT : M_NAME_L_ANALOG_RIGHT;\n                case SDL_CONTROLLER_AXIS_LEFTY:           return axis_dir == -1 ? M_NAME_L_ANALOG_UP : M_NAME_L_ANALOG_DOWN;\n                case SDL_CONTROLLER_AXIS_RIGHTX:          return axis_dir == -1 ? M_NAME_R_ANALOG_LEFT : M_NAME_R_ANALOG_RIGHT;\n                case SDL_CONTROLLER_AXIS_RIGHTY:          return axis_dir == -1 ? M_NAME_R_ANALOG_UP : M_NAME_R_ANALOG_DOWN;\n                case SDL_CONTROLLER_AXIS_TRIGGERLEFT:     return M_NAME_ZL;\n                case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:    return M_NAME_ZR;\n                case SDL_CONTROLLER_AXIS_MAX:             return nullptr;\n            }\n            break;\n\n        case SDL_CONTROLLER_TYPE_XBOX360:\n        case SDL_CONTROLLER_TYPE_XBOXONE:\n        default:\n            switch (axis) {\n                case SDL_CONTROLLER_AXIS_INVALID:         return nullptr;\n                case SDL_CONTROLLER_AXIS_LEFTX:           return axis_dir == -1 ? M_NAME_L_ANALOG_LEFT : M_NAME_L_ANALOG_RIGHT;\n                case SDL_CONTROLLER_AXIS_LEFTY:           return axis_dir == -1 ? M_NAME_L_ANALOG_UP : M_NAME_L_ANALOG_DOWN;\n                case SDL_CONTROLLER_AXIS_RIGHTX:          return axis_dir == -1 ? M_NAME_R_ANALOG_LEFT : M_NAME_R_ANALOG_RIGHT;\n                case SDL_CONTROLLER_AXIS_RIGHTY:          return axis_dir == -1 ? M_NAME_R_ANALOG_UP : M_NAME_R_ANALOG_DOWN;\n                case SDL_CONTROLLER_AXIS_TRIGGERLEFT:     return M_NAME_L_TRIGGER;\n                case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:    return M_NAME_R_TRIGGER;\n                case SDL_CONTROLLER_AXIS_MAX:             return nullptr;\n            }\n            break;\n\n    }\n    // clang-format on\n    return nullptr;\n}\n\nstatic bool M_IsRoleConflicted(const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    return m_Conflicts[layout][role];\n}\n\nstatic const char *M_GetMapName(const CONTROLLER_MAP *const map)\n{\n    if (map->type == BT_BUTTON) {\n        return M_GetButtonName(map->bind.button);\n    } else {\n        return M_GetAxisName(map->bind.axis, map->axis_dir);\n    }\n}\n\nstatic const char *M_GetName(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot);\n    if (bind->key_count == 0) {\n        return nullptr;\n    }\n    if (bind->key_count == 1) {\n        return M_GetMapName(&bind->keys[0]);\n    }\n    // Build composite name for multi-key combo\n    static char buf[256];\n    buf[0] = '\\0';\n    for (int32_t k = 0; k < bind->key_count; k++) {\n        if (k > 0) {\n            strcat(buf, \"+\");\n        }\n        const char *name = M_GetMapName(&bind->keys[k]);\n        if (name != nullptr) {\n            strcat(buf, name);\n        }\n    }\n    return buf;\n}\n\nstatic void M_UnassignRole(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const CONTROLLER_BINDING empty = { .key_count = 0 };\n    M_AssignBinding(layout, role, slot, &empty);\n}\n\nstatic bool M_AssignFromJSONObject(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    JSON_OBJECT *const bind_obj)\n{\n    JSON_ARRAY *const combo_arr = JSON_ObjectGetArray(bind_obj, \"combo\");\n    if (combo_arr != nullptr) {\n        // New combo format: \"combo\": [{button_type, bind, axis_dir}, ...]\n        const int32_t count = combo_arr->length < INPUT_COMBO_MAX_KEYS\n            ? (int32_t)combo_arr->length\n            : INPUT_COMBO_MAX_KEYS;\n        CONTROLLER_BINDING cb = { .key_count = count };\n        for (int32_t i = 0; i < count; i++) {\n            JSON_OBJECT *const key_obj = JSON_ArrayGetObject(combo_arr, i);\n            cb.keys[i].type =\n                JSON_ObjectGetInt(key_obj, \"button_type\", BT_BUTTON);\n            const int16_t b = JSON_ObjectGetInt(\n                key_obj, \"bind\", SDL_CONTROLLER_BUTTON_INVALID);\n            if (cb.keys[i].type == BT_BUTTON) {\n                cb.keys[i].bind.button = b;\n            } else {\n                cb.keys[i].bind.axis = b;\n            }\n            cb.keys[i].axis_dir = JSON_ObjectGetInt(key_obj, \"axis_dir\", 0);\n        }\n        M_AssignBinding(layout, role, slot, &cb);\n    } else {\n        // Legacy single-key format\n        int16_t button_type = M_GetAssignedButtonType(layout, role, slot);\n        button_type = JSON_ObjectGetInt(bind_obj, \"button_type\", button_type);\n\n        int16_t bind = M_GetAssignedBind(layout, role, slot);\n        bind = JSON_ObjectGetInt(bind_obj, \"bind\", bind);\n\n        int16_t axis_dir = M_GetAssignedAxisDir(layout, role, slot);\n        axis_dir = JSON_ObjectGetInt(bind_obj, \"axis_dir\", axis_dir);\n\n        if (button_type == BT_BUTTON) {\n            M_AssignButton(layout, role, slot, bind);\n        } else {\n            M_AssignAxis(layout, role, slot, bind, axis_dir);\n        }\n    }\n    return true;\n}\n\nstatic bool M_AssignToJSONObject(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    JSON_OBJECT *const bind_obj)\n{\n    const CONTROLLER_BINDING *user = M_GetBinding(layout, role, slot);\n    const CONTROLLER_BINDING *def =\n        M_GetBinding(INPUT_LAYOUT_DEFAULT, role, slot);\n\n    if (M_BindingsEqual(user, def)\n        || (user->key_count == 0 && def->key_count == 0)) {\n        return false;\n    }\n\n    if (user->key_count == 0) {\n        // Explicitly unbound\n        JSON_ObjectAppendInt(bind_obj, \"button_type\", (int16_t)BT_BUTTON);\n        JSON_ObjectAppendInt(\n            bind_obj, \"bind\", (int16_t)SDL_CONTROLLER_BUTTON_INVALID);\n        JSON_ObjectAppendInt(bind_obj, \"axis_dir\", (int16_t)0);\n    } else if (user->key_count == 1) {\n        // Single key (legacy format for backward compat)\n        const CONTROLLER_MAP *map = &user->keys[0];\n        JSON_ObjectAppendInt(bind_obj, \"button_type\", map->type);\n        JSON_ObjectAppendInt(\n            bind_obj, \"bind\",\n            map->type == BT_BUTTON ? map->bind.button : map->bind.axis);\n        JSON_ObjectAppendInt(bind_obj, \"axis_dir\", map->axis_dir);\n    } else {\n        // Multi-key combo\n        JSON_ARRAY *const arr = JSON_ArrayNew();\n        for (int32_t i = 0; i < user->key_count; i++) {\n            const CONTROLLER_MAP *map = &user->keys[i];\n            JSON_OBJECT *const key_obj = JSON_ObjectNew();\n            JSON_ObjectAppendInt(key_obj, \"button_type\", map->type);\n            JSON_ObjectAppendInt(\n                key_obj, \"bind\",\n                map->type == BT_BUTTON ? map->bind.button : map->bind.axis);\n            JSON_ObjectAppendInt(key_obj, \"axis_dir\", map->axis_dir);\n            JSON_ArrayAppendObject(arr, key_obj);\n        }\n        JSON_ObjectAppendArray(bind_obj, \"combo\", arr);\n    }\n    return true;\n}\n\n// Per-button/axis tracking for combo prefix deferral.\n// Index: buttons use their enum directly, axes use\n// SDL_CONTROLLER_BUTTON_MAX + axis*2 + (axis_dir == 1 ? 1 : 0).\n#define M_PREFIX_SLOTS (SDL_CONTROLLER_BUTTON_MAX + SDL_CONTROLLER_AXIS_MAX * 2)\nstatic bool m_PrefixWasHeld[M_PREFIX_SLOTS];\nstatic bool m_PrefixComboFired[M_PREFIX_SLOTS];\n\nstatic int32_t M_MapToPrefixIdx(const CONTROLLER_MAP *const map)\n{\n    if (map->type == BT_BUTTON) {\n        return map->bind.button;\n    }\n    return SDL_CONTROLLER_BUTTON_MAX + map->bind.axis * 2\n        + (map->axis_dir == 1 ? 1 : 0);\n}\n\nstatic bool M_IsInputHeld(const CONTROLLER_MAP *const map)\n{\n    return M_CheckMap(map);\n}\n\n// Combo adapter functions for the shared combo layer.\nstatic INPUT_COMBO_BINDING M_GetComboBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const CONTROLLER_BINDING *b = M_GetBinding(layout, role, slot);\n    return (INPUT_COMBO_BINDING) {\n        .key_count = b->key_count,\n        .keys = b->keys,\n        .key_stride = sizeof(CONTROLLER_MAP),\n    };\n}\n\nstatic bool M_ComboKeysEqual(const void *const a, const void *const b)\n{\n    return M_MapsEqual((const CONTROLLER_MAP *)a, (const CONTROLLER_MAP *)b);\n}\n\nstatic INPUT_COMBO_BINDING M_ToCombo(const CONTROLLER_BINDING *const b)\n{\n    return (INPUT_COMBO_BINDING) {\n        .key_count = b->key_count,\n        .keys = b->keys,\n        .key_stride = sizeof(CONTROLLER_MAP),\n    };\n}\n\nstatic const CONTROLLER_BINDING *M_GetPressedBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n        const CONTROLLER_BINDING *bind = M_GetBinding(layout, role, slot);\n        if (M_CheckBinding(bind)) {\n            return bind;\n        }\n    }\n    return nullptr;\n}\n\nstatic void M_ResolvePrefixDeferral(\n    const INPUT_LAYOUT layout, INPUT_STATE *const result,\n    const CONTROLLER_MAP *const map)\n{\n    const int32_t idx = M_MapToPrefixIdx(map);\n    const bool held = M_IsInputHeld(map);\n\n    if (held\n        && Input_ComboIsStarter(\n            layout, map, M_GetComboBinding, M_ComboKeysEqual)) {\n        const INPUT_ROLE role = Input_ComboFindDeferrableRole(\n            layout, map, M_GetComboBinding, M_ComboKeysEqual);\n        if (role != (INPUT_ROLE)-1) {\n            InputState_ClearRole(result, role);\n        }\n    }\n\n    if (!held && m_PrefixWasHeld[idx] && !m_PrefixComboFired[idx]) {\n        const INPUT_ROLE role = Input_ComboFindDeferrableRole(\n            layout, map, M_GetComboBinding, M_ComboKeysEqual);\n        if (role != (INPUT_ROLE)-1) {\n            InputState_SetRole(result, role, true);\n        }\n    }\n\n    m_PrefixWasHeld[idx] = held;\n}\n\n// Per-role deferral tracking for combo disambiguation.\nstatic bool m_RoleWasActive[INPUT_ROLE_NUMBER_OF];\nstatic bool m_RoleLongerFired[INPUT_ROLE_NUMBER_OF];\n\nstatic void M_ResolveCombos(\n    const INPUT_LAYOUT layout, INPUT_STATE *const result)\n{\n    // Phase 1: Collect active bindings.\n    const CONTROLLER_BINDING *active[INPUT_ROLE_NUMBER_OF] = {};\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (InputState_GetRole(*result, role)) {\n            active[role] = M_GetPressedBinding(layout, role);\n        }\n    }\n\n    // Suppress invalid combos (non-capturing sustained + immediate).\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] != nullptr\n            && Input_ComboSustainedHasImmediate(\n                layout, M_ToCombo(active[role]), M_GetComboBinding,\n                M_ComboKeysEqual)) {\n            InputState_ClearRole(result, role);\n            active[role] = nullptr;\n        }\n    }\n\n    // Phase 2: Subset suppression — longer active combos suppress shorter.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] == nullptr) {\n            continue;\n        }\n        for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) {\n            if (other == role || active[other] == nullptr) {\n                continue;\n            }\n            if (Input_ComboIsProperSubset(\n                    M_ToCombo(active[role]), M_ToCombo(active[other]),\n                    M_ComboKeysEqual)) {\n                InputState_ClearRole(result, role);\n                break;\n            }\n        }\n    }\n\n    // Phase 3: Combo deferral — if an active combo's binding is a proper\n    // subset of some (not necessarily active) longer binding, defer it.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] == nullptr) {\n            continue;\n        }\n        if (((Input_IsRoleImmediate(role) || Input_IsRoleSustained(role))\n             && active[role]->key_count <= 1)\n            || !Input_IsRoleRebindable(role)) {\n            continue;\n        }\n        if (Input_ComboHasLonger(\n                layout, role, M_ToCombo(active[role]), M_GetComboBinding,\n                M_ComboKeysEqual)) {\n            InputState_ClearRole(result, role);\n            if (!m_RoleWasActive[role]) {\n                m_RoleLongerFired[role] = false;\n            }\n            m_RoleWasActive[role] = true;\n        }\n    }\n\n    // Reset prefix tracking for newly pressed inputs.\n    for (SDL_GameControllerButton btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX;\n         btn++) {\n        const int32_t idx = (int32_t)btn;\n        if (M_JoyBtn(btn) && !m_PrefixWasHeld[idx]) {\n            m_PrefixComboFired[idx] = false;\n        }\n    }\n    for (SDL_GameControllerAxis axis = 0; axis < SDL_CONTROLLER_AXIS_MAX;\n         axis++) {\n        for (int16_t dir = -1; dir <= 1; dir += 2) {\n            const CONTROLLER_MAP map = { BT_AXIS, { .axis = axis }, dir };\n            const int32_t idx = M_MapToPrefixIdx(&map);\n            if (M_JoyAxis(axis) == dir && !m_PrefixWasHeld[idx]) {\n                m_PrefixComboFired[idx] = false;\n            }\n        }\n    }\n\n    // Phase 4: Mark longer-combo-fired state.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] == nullptr || active[role]->key_count < 2) {\n            continue;\n        }\n        for (int32_t k = 0; k < active[role]->key_count; k++) {\n            const int32_t idx = M_MapToPrefixIdx(&active[role]->keys[k]);\n            m_PrefixComboFired[idx] = true;\n        }\n        for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) {\n            if (!m_RoleWasActive[other]) {\n                continue;\n            }\n            const CONTROLLER_BINDING *ob = M_GetPressedBinding(layout, other);\n            if (ob != nullptr\n                && Input_ComboIsProperSubset(\n                    M_ToCombo(ob), M_ToCombo(active[role]), M_ComboKeysEqual)) {\n                m_RoleLongerFired[other] = true;\n            }\n        }\n    }\n\n    // Phase 5: Fire deferred roles on release.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (!m_RoleWasActive[role]) {\n            continue;\n        }\n        const CONTROLLER_BINDING *bind = M_GetPressedBinding(layout, role);\n        if (bind != nullptr) {\n            continue;\n        }\n        if (!m_RoleLongerFired[role]) {\n            InputState_SetRole(result, role, true);\n        }\n        m_RoleWasActive[role] = false;\n        m_RoleLongerFired[role] = false;\n    }\n\n    // Phase 6: Single-input prefix deferral.\n    for (SDL_GameControllerButton btn = 0; btn < SDL_CONTROLLER_BUTTON_MAX;\n         btn++) {\n        const CONTROLLER_MAP map = { BT_BUTTON, { .button = btn }, 0 };\n        M_ResolvePrefixDeferral(layout, result, &map);\n    }\n    for (SDL_GameControllerAxis axis = 0; axis < SDL_CONTROLLER_AXIS_MAX;\n         axis++) {\n        for (int16_t dir = -1; dir <= 1; dir += 2) {\n            const CONTROLLER_MAP map = { BT_AXIS, { .axis = axis }, dir };\n            M_ResolvePrefixDeferral(layout, result, &map);\n        }\n    }\n}\n\n// Combo capture state for listen mode.\nstatic CONTROLLER_BINDING m_CaptureBuffer = { .key_count = 0 };\nstatic bool m_CaptureActive = false;\n\nstatic bool M_CaptureHasMap(const CONTROLLER_MAP *const map)\n{\n    for (int32_t i = 0; i < m_CaptureBuffer.key_count; i++) {\n        if (M_MapsEqual(&m_CaptureBuffer.keys[i], map)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_ReadAndAssign(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    // Count currently held inputs and accumulate new ones into the buffer.\n    bool any_held = false;\n    for (SDL_GameControllerButton button = 0;\n         button < SDL_CONTROLLER_BUTTON_MAX; button++) {\n        if (!M_JoyBtn(button)) {\n            continue;\n        }\n        any_held = true;\n        const CONTROLLER_MAP map = { BT_BUTTON, { .button = button }, 0 };\n        if (!M_CaptureHasMap(&map)\n            && m_CaptureBuffer.key_count < INPUT_COMBO_MAX_KEYS) {\n            m_CaptureBuffer.keys[m_CaptureBuffer.key_count++] = map;\n        }\n        m_CaptureActive = true;\n    }\n    for (SDL_GameControllerAxis axis = 0; axis < SDL_CONTROLLER_AXIS_MAX;\n         axis++) {\n        const int16_t axis_dir = M_JoyAxis(axis);\n        if (axis_dir == 0) {\n            continue;\n        }\n        any_held = true;\n        const CONTROLLER_MAP map = { BT_AXIS, { .axis = axis }, axis_dir };\n        if (!M_CaptureHasMap(&map)\n            && m_CaptureBuffer.key_count < INPUT_COMBO_MAX_KEYS) {\n            m_CaptureBuffer.keys[m_CaptureBuffer.key_count++] = map;\n        }\n        m_CaptureActive = true;\n    }\n\n    // If the first input captured is bound to an immediate role (movement,\n    // action, etc.), assign as single key right away — don't wait for combo.\n    if (m_CaptureActive && m_CaptureBuffer.key_count == 1 && any_held\n        && Input_ComboIsKeyImmediate(\n            layout, &m_CaptureBuffer.keys[0], M_GetComboBinding,\n            M_ComboKeysEqual)) {\n        M_AssignBinding(layout, role, slot, &m_CaptureBuffer);\n        m_CaptureBuffer.key_count = 0;\n        m_CaptureActive = false;\n        return true;\n    }\n\n    // All inputs released after at least one was captured — assign the chord.\n    if (!any_held && m_CaptureActive) {\n        M_AssignBinding(layout, role, slot, &m_CaptureBuffer);\n        m_CaptureBuffer.key_count = 0;\n        m_CaptureActive = false;\n        return true;\n    }\n    return false;\n}\n\nINPUT_BACKEND_IMPL g_Input_Controller = {\n    .init = M_Init,\n    .shutdown = M_Shutdown,\n    .discover = M_Discover,\n    .process_event = M_ProcessEvent,\n    .custom_update = M_CustomUpdate,\n    .is_pressed = M_IsPressed,\n    .is_role_conflicted = M_IsRoleConflicted,\n    .get_name = M_GetName,\n    .unassign_role = M_UnassignRole,\n    .assign_from_json_object = M_AssignFromJSONObject,\n    .assign_to_json_object = M_AssignToJSONObject,\n    .reset_layout = M_ResetLayout,\n    .read_and_assign = M_ReadAndAssign,\n    .resolve_combos = M_ResolveCombos,\n};\n"
  },
  {
    "path": "src/trx/game/input/backends/controller.def",
    "content": "INPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_UP,             SDL_CONTROLLER_BUTTON_DPAD_UP)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_DOWN,           SDL_CONTROLLER_BUTTON_DPAD_DOWN)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_LEFT,           SDL_CONTROLLER_BUTTON_DPAD_LEFT)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_RIGHT,          SDL_CONTROLLER_BUTTON_DPAD_RIGHT)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_STEP_LEFT,        SDL_CONTROLLER_AXIS_TRIGGERLEFT,  1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_STEP_RIGHT,       SDL_CONTROLLER_AXIS_TRIGGERRIGHT, 1)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_SLOW,           SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_JUMP,           SDL_CONTROLLER_BUTTON_X)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_ACTION,         SDL_CONTROLLER_BUTTON_A)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_DRAW_WEAPON,    SDL_CONTROLLER_BUTTON_Y)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_LOOK,           SDL_CONTROLLER_BUTTON_LEFTSHOULDER)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_ROLL,           SDL_CONTROLLER_BUTTON_B)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_INVENTORY,      SDL_CONTROLLER_BUTTON_BACK)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_CROUCH,         SDL_CONTROLLER_BUTTON_RIGHTSTICK)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_PAUSE,          SDL_CONTROLLER_BUTTON_START)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_CHANGE_TARGET,  SDL_CONTROLLER_BUTTON_LEFTSTICK)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_FORWARD,   SDL_CONTROLLER_AXIS_RIGHTY, -1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_BACK,      SDL_CONTROLLER_AXIS_RIGHTY, 1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_LEFT,      SDL_CONTROLLER_AXIS_RIGHTX, -1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_RIGHT,     SDL_CONTROLLER_AXIS_RIGHTX, 1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_UP,        SDL_CONTROLLER_AXIS_LEFTY,  -1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_CAMERA_DOWN,      SDL_CONTROLLER_AXIS_LEFTY,  1)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_MENU_CONFIRM,   SDL_CONTROLLER_BUTTON_A)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_MENU_BACK,      SDL_CONTROLLER_BUTTON_B)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_RESET_BINDINGS, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)\nINPUT_CONTROLLER_ASSIGN_BUTTON(INPUT_ROLE_UNBIND_KEY,     SDL_CONTROLLER_BUTTON_X)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_MENU_TAB_LEFT,    SDL_CONTROLLER_AXIS_TRIGGERLEFT,  1)\nINPUT_CONTROLLER_ASSIGN_AXIS(INPUT_ROLE_MENU_TAB_RIGHT,   SDL_CONTROLLER_AXIS_TRIGGERRIGHT, 1)\n"
  },
  {
    "path": "src/trx/game/input/backends/controller.h",
    "content": "#include <trx/game/input/backends/base.h>\n\nextern INPUT_BACKEND_IMPL g_Input_Controller;\n"
  },
  {
    "path": "src/trx/game/input/backends/internal.c",
    "content": "#include <trx/game/input/backends/internal.h>\n\n#include <trx/config.h>\n\nstatic bool M_IsManualCameraInput(const INPUT_ROLE role)\n{\n    return role == INPUT_ROLE_CAMERA_FORWARD || role == INPUT_ROLE_CAMERA_BACK\n        || role == INPUT_ROLE_CAMERA_LEFT || role == INPUT_ROLE_CAMERA_RIGHT;\n}\n\nvoid Input_ConflictHelper(\n    const INPUT_LAYOUT layout,\n    bool (*check_conflict_func)(\n        INPUT_LAYOUT layout, INPUT_ROLE role1, INPUT_ROLE role2),\n    void (*assign_conflict_func)(\n        INPUT_LAYOUT layout, INPUT_ROLE role, bool conflict))\n{\n    for (INPUT_ROLE role1 = 0; role1 < INPUT_ROLE_NUMBER_OF; role1++) {\n        if (!Input_IsRoleRebindable(role1)) {\n            continue;\n        }\n\n        bool conflict = false;\n        for (INPUT_ROLE role2 = 0; role2 < INPUT_ROLE_NUMBER_OF; role2++) {\n            if (!Input_IsRoleRebindable(role2)) {\n                continue;\n            }\n\n            if (role1 == role2) {\n                continue;\n            }\n\n            if (!g_Config.gameplay.enable_manual_camera\n                && (M_IsManualCameraInput(role1)\n                    || M_IsManualCameraInput(role2))) {\n                continue;\n            }\n\n            if (check_conflict_func(layout, role1, role2)) {\n                conflict = true;\n            }\n        }\n\n        assign_conflict_func(layout, role1, conflict);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/input/backends/internal.h",
    "content": "#pragma once\n\n#include <trx/game/input/backends/base.h>\n#include <trx/game/input/common.h>\n\nvoid Input_UpdateFromBackend(\n    INPUT_STATE *result, INPUT_LAYOUT layout,\n    const INPUT_BACKEND_IMPL *backend);\n\nvoid Input_ConflictHelper(\n    INPUT_LAYOUT layout,\n    bool (*check_conflict_func)(\n        INPUT_LAYOUT layout, INPUT_ROLE role1, INPUT_ROLE role2),\n    void (*assign_conflict_func)(\n        INPUT_LAYOUT layout, INPUT_ROLE role, bool conflict));\n"
  },
  {
    "path": "src/trx/game/input/backends/keyboard.c",
    "content": "#include <trx/game/input/backends/keyboard.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/input/backends/internal.h>\n#include <trx/game/input/combo.h>\n#include <trx/version.h>\n\n#include <SDL2/SDL_keyboard.h>\n#include <string.h>\n\n// Key state table updated via SDL events.\n#define KEY_DOWN(a) (m_KeyboardState[(a)])\n\ntypedef struct {\n    INPUT_ROLE role;\n    SDL_Scancode scancode;\n} BUILTIN_KEYBOARD_LAYOUT;\n\ntypedef struct {\n    int32_t key_count;\n    SDL_Scancode keys[INPUT_COMBO_MAX_KEYS];\n} KEYBOARD_BINDING;\n\ntypedef struct {\n    KEYBOARD_BINDING slots[INPUT_BINDING_SLOTS];\n} KEYBOARD_ROLE_BINDING;\n\nstatic bool m_KeyboardState[SDL_NUM_SCANCODES] = {};\nstatic bool m_Conflicts[INPUT_LAYOUT_NUMBER_OF][INPUT_ROLE_NUMBER_OF] = {};\n\nstatic const BUILTIN_KEYBOARD_LAYOUT m_BuiltinLayoutBase[] = {\n// clang-format off\n#define INPUT_KEYBOARD_ASSIGN(role, key) { role, key },\n#include <trx/game/input/backends/keyboard.def>\n    { -1, SDL_SCANCODE_UNKNOWN },\n    // clang-format on\n};\n\nstatic BUILTIN_KEYBOARD_LAYOUT m_BuiltinLayout[ARRAY_SIZE(m_BuiltinLayoutBase)];\n\nstatic KEYBOARD_ROLE_BINDING m_Layout[INPUT_LAYOUT_NUMBER_OF]\n                                     [INPUT_ROLE_NUMBER_OF];\n\n// Update internal controller button/axis state from SDL events.\n// @param event     Event to process.\nstatic void M_ProcessEvent(const SDL_Event *const event)\n{\n    switch (event->type) {\n    case SDL_KEYDOWN:\n        if (!event->key.repeat) {\n            m_KeyboardState[event->key.keysym.scancode] = true;\n        }\n        break;\n    case SDL_KEYUP:\n        m_KeyboardState[event->key.keysym.scancode] = false;\n        break;\n    default:\n        break;\n    }\n}\n\nstatic const char *M_GetScancodeName(SDL_Scancode scancode)\n{\n    // clang-format off\n    switch (scancode) {\n        case SDL_SCANCODE_LCTRL:              return \"\\\\{keyboard l_ctrl}\";\n        case SDL_SCANCODE_RCTRL:              return \"\\\\{keyboard r_ctrl}\";\n        case SDL_SCANCODE_RSHIFT:             return \"\\\\{keyboard r_shift}\";\n        case SDL_SCANCODE_LSHIFT:             return \"\\\\{keyboard l_shift}\";\n        case SDL_SCANCODE_RALT:               return \"\\\\{keyboard l_alt}\";\n        case SDL_SCANCODE_LALT:               return \"\\\\{keyboard r_alt}\";\n        case SDL_SCANCODE_LGUI:               return \"\\\\{keyboard l_win}\";\n        case SDL_SCANCODE_RGUI:               return \"\\\\{keyboard r_win}\";\n\n        case SDL_SCANCODE_LEFT:               return \"\\\\{keyboard left}\";\n        case SDL_SCANCODE_UP:                 return \"\\\\{keyboard up}\";\n        case SDL_SCANCODE_RIGHT:              return \"\\\\{keyboard right}\";\n        case SDL_SCANCODE_DOWN:               return \"\\\\{keyboard down}\";\n\n        case SDL_SCANCODE_RETURN:             return \"\\\\{keyboard return}\";\n        case SDL_SCANCODE_ESCAPE:             return \"\\\\{keyboard escape}\";\n        case SDL_SCANCODE_BACKSPACE:          return \"\\\\{keyboard backspace}\";\n        case SDL_SCANCODE_TAB:                return \"\\\\{keyboard tab}\";\n        case SDL_SCANCODE_SPACE:              return \"\\\\{keyboard space}\";\n        case SDL_SCANCODE_CAPSLOCK:           return \"\\\\{keyboard caps_lock}\";\n        case SDL_SCANCODE_PRINTSCREEN:        return \"\\\\{keyboard print_screen}\";\n        case SDL_SCANCODE_SCROLLLOCK:         return \"\\\\{keyboard scroll_lock}\";\n        case SDL_SCANCODE_PAUSE:              return \"\\\\{keyboard pause}\";\n        case SDL_SCANCODE_INSERT:             return \"\\\\{keyboard insert}\";\n        case SDL_SCANCODE_HOME:               return \"\\\\{keyboard home}\";\n        case SDL_SCANCODE_PAGEUP:             return \"\\\\{keyboard page_up}\";\n        case SDL_SCANCODE_DELETE:             return \"\\\\{keyboard delete}\";\n        case SDL_SCANCODE_END:                return \"\\\\{keyboard end}\";\n        case SDL_SCANCODE_PAGEDOWN:           return \"\\\\{keyboard page_down}\";\n\n        case SDL_SCANCODE_A:                  return \"\\\\{keyboard a}\";\n        case SDL_SCANCODE_B:                  return \"\\\\{keyboard b}\";\n        case SDL_SCANCODE_C:                  return \"\\\\{keyboard c}\";\n        case SDL_SCANCODE_D:                  return \"\\\\{keyboard d}\";\n        case SDL_SCANCODE_E:                  return \"\\\\{keyboard e}\";\n        case SDL_SCANCODE_F:                  return \"\\\\{keyboard f}\";\n        case SDL_SCANCODE_G:                  return \"\\\\{keyboard g}\";\n        case SDL_SCANCODE_H:                  return \"\\\\{keyboard h}\";\n        case SDL_SCANCODE_I:                  return \"\\\\{keyboard i}\";\n        case SDL_SCANCODE_J:                  return \"\\\\{keyboard j}\";\n        case SDL_SCANCODE_K:                  return \"\\\\{keyboard k}\";\n        case SDL_SCANCODE_L:                  return \"\\\\{keyboard l}\";\n        case SDL_SCANCODE_M:                  return \"\\\\{keyboard m}\";\n        case SDL_SCANCODE_N:                  return \"\\\\{keyboard n}\";\n        case SDL_SCANCODE_O:                  return \"\\\\{keyboard o}\";\n        case SDL_SCANCODE_P:                  return \"\\\\{keyboard p}\";\n        case SDL_SCANCODE_Q:                  return \"\\\\{keyboard q}\";\n        case SDL_SCANCODE_R:                  return \"\\\\{keyboard r}\";\n        case SDL_SCANCODE_S:                  return \"\\\\{keyboard s}\";\n        case SDL_SCANCODE_T:                  return \"\\\\{keyboard t}\";\n        case SDL_SCANCODE_U:                  return \"\\\\{keyboard u}\";\n        case SDL_SCANCODE_V:                  return \"\\\\{keyboard v}\";\n        case SDL_SCANCODE_W:                  return \"\\\\{keyboard w}\";\n        case SDL_SCANCODE_X:                  return \"\\\\{keyboard x}\";\n        case SDL_SCANCODE_Y:                  return \"\\\\{keyboard y}\";\n        case SDL_SCANCODE_Z:                  return \"\\\\{keyboard z}\";\n\n        case SDL_SCANCODE_0:                  return \"\\\\{keyboard 0}\";\n        case SDL_SCANCODE_1:                  return \"\\\\{keyboard 1}\";\n        case SDL_SCANCODE_2:                  return \"\\\\{keyboard 2}\";\n        case SDL_SCANCODE_3:                  return \"\\\\{keyboard 3}\";\n        case SDL_SCANCODE_4:                  return \"\\\\{keyboard 4}\";\n        case SDL_SCANCODE_5:                  return \"\\\\{keyboard 5}\";\n        case SDL_SCANCODE_6:                  return \"\\\\{keyboard 6}\";\n        case SDL_SCANCODE_7:                  return \"\\\\{keyboard 7}\";\n        case SDL_SCANCODE_8:                  return \"\\\\{keyboard 8}\";\n        case SDL_SCANCODE_9:                  return \"\\\\{keyboard 9}\";\n\n        case SDL_SCANCODE_MINUS:              return \"\\\\{keyboard minus}\";\n        case SDL_SCANCODE_EQUALS:             return \"\\\\{keyboard equals}\";\n        case SDL_SCANCODE_LEFTBRACKET:        return \"\\\\{keyboard left_square_bracket}\";\n        case SDL_SCANCODE_RIGHTBRACKET:       return \"\\\\{keyboard right_square_bracket}\";\n        case SDL_SCANCODE_BACKSLASH:          return \"\\\\{keyboard backslash}\";\n        case SDL_SCANCODE_NONUSHASH:          return \"\\\\{keyboard hash}\";\n        case SDL_SCANCODE_SEMICOLON:          return \"\\\\{keyboard semicolon}\";\n        case SDL_SCANCODE_APOSTROPHE:         return \"\\\\{keyboard apostrophe}\";\n        case SDL_SCANCODE_GRAVE:              return \"\\\\{keyboard backtick}\";\n        case SDL_SCANCODE_COMMA:              return \"\\\\{keyboard comma}\";\n        case SDL_SCANCODE_PERIOD:             return \"\\\\{keyboard period}\";\n        case SDL_SCANCODE_SLASH:              return \"\\\\{keyboard slash}\";\n        case SDL_SCANCODE_NONUSBACKSLASH:     return \"\\\\{keyboard backslash}\";\n\n        case SDL_SCANCODE_F1:                 return \"\\\\{keyboard f1}\";\n        case SDL_SCANCODE_F2:                 return \"\\\\{keyboard f2}\";\n        case SDL_SCANCODE_F3:                 return \"\\\\{keyboard f3}\";\n        case SDL_SCANCODE_F4:                 return \"\\\\{keyboard f4}\";\n        case SDL_SCANCODE_F5:                 return \"\\\\{keyboard f5}\";\n        case SDL_SCANCODE_F6:                 return \"\\\\{keyboard f6}\";\n        case SDL_SCANCODE_F7:                 return \"\\\\{keyboard f7}\";\n        case SDL_SCANCODE_F8:                 return \"\\\\{keyboard f8}\";\n        case SDL_SCANCODE_F9:                 return \"\\\\{keyboard f9}\";\n        case SDL_SCANCODE_F10:                return \"\\\\{keyboard f10}\";\n        case SDL_SCANCODE_F11:                return \"\\\\{keyboard f11}\";\n        case SDL_SCANCODE_F12:                return \"\\\\{keyboard f12}\";\n        case SDL_SCANCODE_F13:                return \"\\\\{keyboard f13}\";\n        case SDL_SCANCODE_F14:                return \"\\\\{keyboard f14}\";\n        case SDL_SCANCODE_F15:                return \"\\\\{keyboard f15}\";\n        case SDL_SCANCODE_F16:                return \"\\\\{keyboard f16}\";\n        case SDL_SCANCODE_F17:                return \"\\\\{keyboard f17}\";\n        case SDL_SCANCODE_F18:                return \"\\\\{keyboard f18}\";\n        case SDL_SCANCODE_F19:                return \"\\\\{keyboard f19}\";\n        case SDL_SCANCODE_F20:                return \"\\\\{keyboard f20}\";\n        case SDL_SCANCODE_F21:                return \"\\\\{keyboard f21}\";\n        case SDL_SCANCODE_F22:                return \"\\\\{keyboard f22}\";\n        case SDL_SCANCODE_F23:                return \"\\\\{keyboard f23}\";\n        case SDL_SCANCODE_F24:                return \"\\\\{keyboard f24}\";\n\n        case SDL_SCANCODE_NUMLOCKCLEAR:       return \"\\\\{keyboard num_lock}\";\n        case SDL_SCANCODE_KP_0:               return \"\\\\{keyboard num_0}\";\n        case SDL_SCANCODE_KP_1:               return \"\\\\{keyboard num_1}\";\n        case SDL_SCANCODE_KP_2:               return \"\\\\{keyboard num_2}\";\n        case SDL_SCANCODE_KP_3:               return \"\\\\{keyboard num_3}\";\n        case SDL_SCANCODE_KP_4:               return \"\\\\{keyboard num_4}\";\n        case SDL_SCANCODE_KP_5:               return \"\\\\{keyboard num_5}\";\n        case SDL_SCANCODE_KP_6:               return \"\\\\{keyboard num_6}\";\n        case SDL_SCANCODE_KP_7:               return \"\\\\{keyboard num_7}\";\n        case SDL_SCANCODE_KP_8:               return \"\\\\{keyboard num_8}\";\n        case SDL_SCANCODE_KP_9:               return \"\\\\{keyboard num_9}\";\n        case SDL_SCANCODE_KP_PERIOD:          return \"\\\\{keyboard num_period}\";\n        case SDL_SCANCODE_KP_DIVIDE:          return \"\\\\{keyboard num_divide}\";\n        case SDL_SCANCODE_KP_MULTIPLY:        return \"\\\\{keyboard num_multiply}\";\n        case SDL_SCANCODE_KP_MINUS:           return \"\\\\{keyboard num_minus}\";\n        case SDL_SCANCODE_KP_PLUS:            return \"\\\\{keyboard num_plus}\";\n        case SDL_SCANCODE_KP_EQUALS:          return \"\\\\{keyboard num_equals}\";\n        case SDL_SCANCODE_KP_EQUALSAS400:     return \"\\\\{keyboard num_equals}\";\n        case SDL_SCANCODE_KP_COMMA:           return \"\\\\{keyboard num_comma}\";\n        case SDL_SCANCODE_KP_ENTER:           return \"\\\\{keyboard num_enter}\";\n\n        // extra keys\n        case SDL_SCANCODE_APPLICATION:        return \"MENU\";\n        case SDL_SCANCODE_POWER:              return \"POWER\";\n        case SDL_SCANCODE_EXECUTE:            return \"EXEC\";\n        case SDL_SCANCODE_HELP:               return \"HELP\";\n        case SDL_SCANCODE_MENU:               return \"MENU\";\n        case SDL_SCANCODE_SELECT:             return \"SEL\";\n        case SDL_SCANCODE_STOP:               return \"STOP\";\n        case SDL_SCANCODE_AGAIN:              return \"AGAIN\";\n        case SDL_SCANCODE_UNDO:               return \"UNDO\";\n        case SDL_SCANCODE_CUT:                return \"CUT\";\n        case SDL_SCANCODE_COPY:               return \"COPY\";\n        case SDL_SCANCODE_PASTE:              return \"PASTE\";\n        case SDL_SCANCODE_FIND:               return \"FIND\";\n        case SDL_SCANCODE_MUTE:               return \"MUTE\";\n        case SDL_SCANCODE_VOLUMEUP:           return \"VOLUP\";\n        case SDL_SCANCODE_VOLUMEDOWN:         return \"VOLDN\";\n        case SDL_SCANCODE_ALTERASE:           return \"ALTER\";\n        case SDL_SCANCODE_SYSREQ:             return \"SYSRQ\";\n        case SDL_SCANCODE_CANCEL:             return \"CNCEL\";\n        case SDL_SCANCODE_CLEAR:              return \"CLEAR\";\n        case SDL_SCANCODE_PRIOR:              return \"PRIOR\";\n        case SDL_SCANCODE_RETURN2:            return \"RETURN\";\n        case SDL_SCANCODE_SEPARATOR:          return \"SEP\";\n        case SDL_SCANCODE_OUT:                return \"OUT\";\n        case SDL_SCANCODE_OPER:               return \"OPER\";\n        case SDL_SCANCODE_CLEARAGAIN:         return \"CLEAR\";\n        case SDL_SCANCODE_CRSEL:              return \"CRSEL\";\n        case SDL_SCANCODE_EXSEL:              return \"EXSEL\";\n        case SDL_SCANCODE_KP_00:              return \"PAD00\";\n        case SDL_SCANCODE_KP_000:             return \"PAD000\";\n        case SDL_SCANCODE_THOUSANDSSEPARATOR: return \"TSEP\";\n        case SDL_SCANCODE_DECIMALSEPARATOR:   return \"DSEP\";\n        case SDL_SCANCODE_CURRENCYUNIT:       return \"CURU\";\n        case SDL_SCANCODE_CURRENCYSUBUNIT:    return \"CURSU\";\n        case SDL_SCANCODE_KP_LEFTPAREN:       return \"PAD(\";\n        case SDL_SCANCODE_KP_RIGHTPAREN:      return \"PAD)\";\n        case SDL_SCANCODE_KP_LEFTBRACE:       return \"PAD{\";\n        case SDL_SCANCODE_KP_RIGHTBRACE:      return \"PAD}\";\n        case SDL_SCANCODE_KP_TAB:             return \"PADT\";\n        case SDL_SCANCODE_KP_BACKSPACE:       return \"PADBK\";\n        case SDL_SCANCODE_KP_A:               return \"PADA\";\n        case SDL_SCANCODE_KP_B:               return \"PADB\";\n        case SDL_SCANCODE_KP_C:               return \"PADC\";\n        case SDL_SCANCODE_KP_D:               return \"PADD\";\n        case SDL_SCANCODE_KP_E:               return \"PADE\";\n        case SDL_SCANCODE_KP_F:               return \"PADF\";\n        case SDL_SCANCODE_KP_XOR:             return \"PADXR\";\n        case SDL_SCANCODE_KP_POWER:           return \"PAD^\";\n        case SDL_SCANCODE_KP_PERCENT:         return \"PAD%\";\n        case SDL_SCANCODE_KP_LESS:            return \"PAD<\";\n        case SDL_SCANCODE_KP_GREATER:         return \"PAD>\";\n        case SDL_SCANCODE_KP_AMPERSAND:       return \"PAD&\";\n        case SDL_SCANCODE_KP_DBLAMPERSAND:    return \"PAD&&\";\n        case SDL_SCANCODE_KP_VERTICALBAR:     return \"PAD|\";\n        case SDL_SCANCODE_KP_DBLVERTICALBAR:  return \"PAD||\";\n        case SDL_SCANCODE_KP_COLON:           return \"PAD:\";\n        case SDL_SCANCODE_KP_HASH:            return \"PAD#\";\n        case SDL_SCANCODE_KP_SPACE:           return \"PADSP\";\n        case SDL_SCANCODE_KP_AT:              return \"PAD@\";\n        case SDL_SCANCODE_KP_EXCLAM:          return \"PAD!\";\n        case SDL_SCANCODE_KP_MEMSTORE:        return \"PADMS\";\n        case SDL_SCANCODE_KP_MEMRECALL:       return \"PADMR\";\n        case SDL_SCANCODE_KP_MEMCLEAR:        return \"PADMC\";\n        case SDL_SCANCODE_KP_MEMADD:          return \"PADMA\";\n        case SDL_SCANCODE_KP_MEMSUBTRACT:     return \"PADM-\";\n        case SDL_SCANCODE_KP_MEMMULTIPLY:     return \"PADM*\";\n        case SDL_SCANCODE_KP_MEMDIVIDE:       return \"PADM/\";\n        case SDL_SCANCODE_KP_PLUSMINUS:       return \"PAD+-\";\n        case SDL_SCANCODE_KP_CLEAR:           return \"PADCL\";\n        case SDL_SCANCODE_KP_CLEARENTRY:      return \"PADCL\";\n        case SDL_SCANCODE_KP_BINARY:          return \"PAD02\";\n        case SDL_SCANCODE_KP_OCTAL:           return \"PAD08\";\n        case SDL_SCANCODE_KP_DECIMAL:         return \"PAD10\";\n        case SDL_SCANCODE_KP_HEXADECIMAL:     return \"PAD16\";\n        case SDL_SCANCODE_MODE:               return \"MODE\";\n        case SDL_SCANCODE_AUDIONEXT:          return \"NEXT\";\n        case SDL_SCANCODE_AUDIOPREV:          return \"PREV\";\n        case SDL_SCANCODE_AUDIOSTOP:          return \"STOP\";\n        case SDL_SCANCODE_AUDIOPLAY:          return \"PLAY\";\n        case SDL_SCANCODE_AUDIOMUTE:          return \"MUTE\";\n        case SDL_SCANCODE_MEDIASELECT:        return \"MEDIA\";\n        case SDL_SCANCODE_WWW:                return \"WWW\";\n        case SDL_SCANCODE_MAIL:               return \"MAIL\";\n        case SDL_SCANCODE_CALCULATOR:         return \"CALC\";\n        case SDL_SCANCODE_COMPUTER:           return \"COMP\";\n        case SDL_SCANCODE_AC_SEARCH:          return \"SRCH\";\n        case SDL_SCANCODE_AC_HOME:            return \"HOME\";\n        case SDL_SCANCODE_AC_BACK:            return \"BACK\";\n        case SDL_SCANCODE_AC_FORWARD:         return \"FRWD\";\n        case SDL_SCANCODE_AC_STOP:            return \"STOP\";\n        case SDL_SCANCODE_AC_REFRESH:         return \"RFRSH\";\n        case SDL_SCANCODE_AC_BOOKMARKS:       return \"BKMK\";\n        case SDL_SCANCODE_BRIGHTNESSDOWN:     return \"BNDN\";\n        case SDL_SCANCODE_BRIGHTNESSUP:       return \"BNUP\";\n        case SDL_SCANCODE_DISPLAYSWITCH:      return \"DPSW\";\n        case SDL_SCANCODE_KBDILLUMTOGGLE:     return \"KBDIT\";\n        case SDL_SCANCODE_KBDILLUMDOWN:       return \"KBDID\";\n        case SDL_SCANCODE_KBDILLUMUP:         return \"KBDIU\";\n        case SDL_SCANCODE_EJECT:              return \"EJECT\";\n        case SDL_SCANCODE_SLEEP:              return \"SLEEP\";\n        case SDL_SCANCODE_APP1:               return \"APP1\";\n        case SDL_SCANCODE_APP2:               return \"APP2\";\n        case SDL_SCANCODE_AUDIOREWIND:        return \"RWND\";\n        case SDL_SCANCODE_AUDIOFASTFORWARD:   return \"FF\";\n        case SDL_SCANCODE_UNKNOWN:            return nullptr;\n\n        default:                              return \"\\\\{keyboard unknown}\";\n    }\n    // clang-format on\n}\n\nstatic bool M_CheckScancode(const SDL_Scancode scancode)\n{\n    if (scancode == SDL_SCANCODE_UNKNOWN) {\n        return false;\n    }\n    if (scancode == SDL_SCANCODE_RETURN && KEY_DOWN(SDL_SCANCODE_LALT)) {\n        return false;\n    }\n#ifdef _WIN32\n    if (scancode == SDL_SCANCODE_F4\n        && (KEY_DOWN(SDL_SCANCODE_LALT) || KEY_DOWN(SDL_SCANCODE_RALT))) {\n        return false;\n    }\n#endif\n    if (KEY_DOWN(scancode)) {\n        return true;\n    }\n    if (scancode == SDL_SCANCODE_LCTRL) {\n        return KEY_DOWN(SDL_SCANCODE_RCTRL);\n    }\n    if (scancode == SDL_SCANCODE_RCTRL) {\n        return KEY_DOWN(SDL_SCANCODE_LCTRL);\n    }\n    if (scancode == SDL_SCANCODE_LSHIFT) {\n        return KEY_DOWN(SDL_SCANCODE_RSHIFT);\n    }\n    if (scancode == SDL_SCANCODE_RSHIFT) {\n        return KEY_DOWN(SDL_SCANCODE_LSHIFT);\n    }\n    if (scancode == SDL_SCANCODE_LALT) {\n        return KEY_DOWN(SDL_SCANCODE_RALT);\n    }\n    if (scancode == SDL_SCANCODE_RALT) {\n        return KEY_DOWN(SDL_SCANCODE_LALT);\n    }\n    return false;\n}\n\nstatic bool M_CheckBinding(const KEYBOARD_BINDING *const bind)\n{\n    if (bind->key_count == 0) {\n        return false;\n    }\n    for (int32_t k = 0; k < bind->key_count; k++) {\n        if (!M_CheckScancode(bind->keys[k])) {\n            return false;\n        }\n    }\n    return true;\n}\n\n// Combo adapter forward declarations.\nstatic INPUT_COMBO_BINDING M_GetComboBinding(\n    INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\nstatic bool M_ComboKeysEqual(const void *a, const void *b);\n\nstatic bool M_Key(const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n        const KEYBOARD_BINDING *bind = &m_Layout[layout][role].slots[slot];\n        if (bind->key_count >= 2\n            && Input_ComboIsKeyImmediate(\n                layout, &bind->keys[0], M_GetComboBinding, M_ComboKeysEqual)) {\n            continue;\n        }\n        if (M_CheckBinding(bind)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic const KEYBOARD_BINDING *M_GetBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    return &m_Layout[layout][role].slots[slot];\n}\n\nstatic bool M_BindingsEqual(\n    const KEYBOARD_BINDING *const a, const KEYBOARD_BINDING *const b)\n{\n    if (a->key_count != b->key_count || a->key_count == 0) {\n        return false;\n    }\n    for (int32_t i = 0; i < a->key_count; i++) {\n        if (a->keys[i] != b->keys[i]) {\n            return false;\n        }\n    }\n    return true;\n}\n\nstatic bool M_CheckConflict(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role1, const INPUT_ROLE role2)\n{\n    for (int32_t s1 = 0; s1 < INPUT_BINDING_SLOTS; s1++) {\n        const KEYBOARD_BINDING *b1 = M_GetBinding(layout, role1, s1);\n        if (b1->key_count == 0) {\n            continue;\n        }\n        for (int32_t s2 = 0; s2 < INPUT_BINDING_SLOTS; s2++) {\n            const KEYBOARD_BINDING *b2 = M_GetBinding(layout, role2, s2);\n            if (M_BindingsEqual(b1, b2)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nstatic void M_AssignConflict(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, bool conflict)\n{\n    m_Conflicts[layout][role] = conflict;\n}\n\nstatic void M_CheckConflicts(const INPUT_LAYOUT layout)\n{\n    Input_ConflictHelper(layout, M_CheckConflict, M_AssignConflict);\n    Input_ComboCheckConflicts(\n        layout, M_GetComboBinding, M_ComboKeysEqual, m_Conflicts[layout]);\n}\n\nstatic void M_AssignBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    const KEYBOARD_BINDING *const bind)\n{\n    m_Layout[layout][role].slots[slot] = *bind;\n    M_CheckConflicts(layout);\n}\n\nstatic void M_ResetLayout(const INPUT_LAYOUT layout)\n{\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        m_Layout[layout][role] = m_Layout[INPUT_LAYOUT_DEFAULT][role];\n    }\n    M_CheckConflicts(layout);\n}\n\nstatic BUILTIN_KEYBOARD_LAYOUT *M_GetBuiltInLayout(const INPUT_ROLE role)\n{\n    for (int32_t i = 0; m_BuiltinLayout[i].role != (INPUT_ROLE)-1; i++) {\n        BUILTIN_KEYBOARD_LAYOUT *const builtin = &m_BuiltinLayout[i];\n        if (builtin->role == role) {\n            return builtin;\n        }\n    }\n    return nullptr;\n}\n\nstatic void M_HandleBuiltInDefaults(void)\n{\n#define L_BIND(role, code)                                                     \\\n    do {                                                                       \\\n        M_GetBuiltInLayout(role)->scancode = code;                             \\\n    } while (0)\n\n    if (g_TRVersion == 2) {\n        L_BIND(INPUT_ROLE_EQUIP_MAGNUMS, SDL_SCANCODE_UNKNOWN);\n        L_BIND(INPUT_ROLE_EQUIP_AUTOS, SDL_SCANCODE_3);\n    } else if (g_TRVersion == 3) {\n        L_BIND(INPUT_ROLE_USE_SMALL_MEDI, SDL_SCANCODE_0);\n        L_BIND(INPUT_ROLE_USE_BIG_MEDI, SDL_SCANCODE_9);\n        L_BIND(INPUT_ROLE_EQUIP_MAGNUMS, SDL_SCANCODE_UNKNOWN);\n        L_BIND(INPUT_ROLE_EQUIP_DESERT_EAGLE, SDL_SCANCODE_3);\n        L_BIND(INPUT_ROLE_EQUIP_M16, SDL_SCANCODE_UNKNOWN);\n        L_BIND(INPUT_ROLE_EQUIP_MP5, SDL_SCANCODE_6);\n        L_BIND(INPUT_ROLE_EQUIP_ROCKET_LAUNCHER, SDL_SCANCODE_7);\n        L_BIND(INPUT_ROLE_EQUIP_GRENADE_LAUNCHER, SDL_SCANCODE_8);\n    }\n\n#undef L_BIND\n}\n\nstatic void M_Init(void)\n{\n    memcpy(m_BuiltinLayout, m_BuiltinLayoutBase, sizeof(m_BuiltinLayout));\n\n    // first, reset all roles to unbound\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n            m_Layout[INPUT_LAYOUT_DEFAULT][role].slots[slot] =\n                (KEYBOARD_BINDING) { .key_count = 0 };\n        }\n    }\n    // allow specific engines to re-assign default bindings\n    M_HandleBuiltInDefaults();\n    // then load actually defined default bindings into slot 0\n    for (int32_t i = 0; m_BuiltinLayout[i].role != (INPUT_ROLE)-1; i++) {\n        const BUILTIN_KEYBOARD_LAYOUT *const builtin = &m_BuiltinLayout[i];\n        m_Layout[INPUT_LAYOUT_DEFAULT][builtin->role].slots[0] =\n            (KEYBOARD_BINDING) {\n                .key_count = builtin->scancode != SDL_SCANCODE_UNKNOWN ? 1 : 0,\n                .keys = { builtin->scancode },\n            };\n    }\n    M_CheckConflicts(INPUT_LAYOUT_DEFAULT);\n\n    for (int32_t layout = INPUT_LAYOUT_CUSTOM_1;\n         layout < INPUT_LAYOUT_NUMBER_OF; layout++) {\n        M_ResetLayout(layout);\n    }\n}\n\nstatic bool M_CustomUpdate(INPUT_STATE *const result, const INPUT_LAYOUT layout)\n{\n    // we only do this for keyboard input\n    result->menu_confirm |= result->action;\n    result->toggle_fullscreen =\n        KEY_DOWN(SDL_SCANCODE_RETURN) && KEY_DOWN(SDL_SCANCODE_LALT);\n    result->menu_skip = result->menu_confirm || result->menu_back;\n    return true;\n}\n\nstatic bool M_IsPressed(const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    return M_Key(layout, role);\n}\n\nstatic bool M_IsRoleConflicted(const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    return m_Conflicts[layout][role];\n}\n\nstatic const char *M_GetName(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const KEYBOARD_BINDING *bind = M_GetBinding(layout, role, slot);\n    if (bind->key_count == 0) {\n        return nullptr;\n    }\n    if (bind->key_count == 1) {\n        return M_GetScancodeName(bind->keys[0]);\n    }\n    // Build composite name for multi-key combo\n    static char buf[256];\n    buf[0] = '\\0';\n    for (int32_t k = 0; k < bind->key_count; k++) {\n        if (k > 0) {\n            strcat(buf, \"+\");\n        }\n        const char *name = M_GetScancodeName(bind->keys[k]);\n        if (name != nullptr) {\n            strcat(buf, name);\n        }\n    }\n    return buf;\n}\n\nstatic void M_UnassignRole(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const KEYBOARD_BINDING empty = { .key_count = 0 };\n    M_AssignBinding(layout, role, slot, &empty);\n}\n\nstatic bool M_AssignFromJSONObject(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    JSON_OBJECT *const bind_obj)\n{\n    JSON_ARRAY *const combo_arr = JSON_ObjectGetArray(bind_obj, \"combo\");\n    if (combo_arr != nullptr) {\n        // New combo format: \"combo\": [scancode1, scancode2, ...]\n        const int32_t count = combo_arr->length < INPUT_COMBO_MAX_KEYS\n            ? (int32_t)combo_arr->length\n            : INPUT_COMBO_MAX_KEYS;\n        KEYBOARD_BINDING bind = { .key_count = count };\n        for (int32_t i = 0; i < count; i++) {\n            bind.keys[i] = JSON_ArrayGetInt(combo_arr, i, SDL_SCANCODE_UNKNOWN);\n        }\n        M_AssignBinding(layout, role, slot, &bind);\n    } else {\n        // Legacy single-key format: \"scancode\": N\n        const KEYBOARD_BINDING *current = M_GetBinding(layout, role, slot);\n        const SDL_Scancode default_sc =\n            current->key_count > 0 ? current->keys[0] : SDL_SCANCODE_UNKNOWN;\n        const SDL_Scancode user_sc =\n            JSON_ObjectGetInt(bind_obj, \"scancode\", default_sc);\n        const KEYBOARD_BINDING bind = {\n            .key_count = user_sc != SDL_SCANCODE_UNKNOWN ? 1 : 0,\n            .keys = { user_sc },\n        };\n        M_AssignBinding(layout, role, slot, &bind);\n    }\n    return true;\n}\n\nstatic bool M_AssignToJSONObject(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot,\n    JSON_OBJECT *const bind_obj)\n{\n    const KEYBOARD_BINDING *user = M_GetBinding(layout, role, slot);\n    const KEYBOARD_BINDING *def =\n        M_GetBinding(INPUT_LAYOUT_DEFAULT, role, slot);\n\n    if (M_BindingsEqual(user, def)\n        || (user->key_count == 0 && def->key_count == 0)) {\n        return false;\n    }\n\n    if (user->key_count <= 1) {\n        // Single key: use legacy \"scancode\" for backward compatibility\n        JSON_ObjectAppendInt(\n            bind_obj, \"scancode\",\n            user->key_count == 1 ? user->keys[0] : SDL_SCANCODE_UNKNOWN);\n    } else {\n        // Multi-key combo\n        JSON_ARRAY *const arr = JSON_ArrayNew();\n        for (int32_t i = 0; i < user->key_count; i++) {\n            JSON_ArrayAppendInt(arr, user->keys[i]);\n        }\n        JSON_ObjectAppendArray(bind_obj, \"combo\", arr);\n    }\n    return true;\n}\n\n// Per-scancode tracking for combo prefix deferral.\nstatic bool m_PrefixWasHeld[SDL_NUM_SCANCODES];\nstatic bool m_PrefixComboFired[SDL_NUM_SCANCODES];\n\n// Combo adapter functions for the shared combo layer.\nstatic INPUT_COMBO_BINDING M_GetComboBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    const KEYBOARD_BINDING *b = M_GetBinding(layout, role, slot);\n    return (INPUT_COMBO_BINDING) {\n        .key_count = b->key_count,\n        .keys = b->keys,\n        .key_stride = sizeof(SDL_Scancode),\n    };\n}\n\nstatic bool M_ComboKeysEqual(const void *const a, const void *const b)\n{\n    return *(const SDL_Scancode *)a == *(const SDL_Scancode *)b;\n}\n\nstatic INPUT_COMBO_BINDING M_ToCombo(const KEYBOARD_BINDING *const b)\n{\n    return (INPUT_COMBO_BINDING) {\n        .key_count = b->key_count,\n        .keys = b->keys,\n        .key_stride = sizeof(SDL_Scancode),\n    };\n}\n\nstatic const KEYBOARD_BINDING *M_GetPressedBinding(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role)\n{\n    for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n        const KEYBOARD_BINDING *bind = M_GetBinding(layout, role, slot);\n        if (M_CheckBinding(bind)) {\n            return bind;\n        }\n    }\n    return nullptr;\n}\n\n// Per-role deferral tracking for combo disambiguation.\nstatic bool m_RoleWasActive[INPUT_ROLE_NUMBER_OF];\nstatic bool m_RoleLongerFired[INPUT_ROLE_NUMBER_OF];\n\nstatic void M_ResolveCombos(\n    const INPUT_LAYOUT layout, INPUT_STATE *const result)\n{\n    // Phase 1: Collect active bindings.\n    const KEYBOARD_BINDING *active[INPUT_ROLE_NUMBER_OF] = {};\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (InputState_GetRole(*result, role)) {\n            active[role] = M_GetPressedBinding(layout, role);\n        }\n    }\n\n    // Suppress invalid combos (non-capturing sustained + immediate).\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] != nullptr\n            && Input_ComboSustainedHasImmediate(\n                layout, M_ToCombo(active[role]), M_GetComboBinding,\n                M_ComboKeysEqual)) {\n            InputState_ClearRole(result, role);\n            active[role] = nullptr;\n        }\n    }\n\n    // Phase 2: Subset suppression — longer active combos suppress shorter.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] == nullptr) {\n            continue;\n        }\n        for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) {\n            if (other == role || active[other] == nullptr) {\n                continue;\n            }\n            if (Input_ComboIsProperSubset(\n                    M_ToCombo(active[role]), M_ToCombo(active[other]),\n                    M_ComboKeysEqual)) {\n                InputState_ClearRole(result, role);\n                break;\n            }\n        }\n    }\n\n    // Phase 3: Combo deferral — if an active combo's binding is a proper\n    // subset of some (not necessarily active) longer binding, defer it.\n    // This handles both single-key and multi-key prefix disambiguation.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] == nullptr) {\n            continue;\n        }\n        if (((Input_IsRoleImmediate(role) || Input_IsRoleSustained(role))\n             && active[role]->key_count <= 1)\n            || !Input_IsRoleRebindable(role)) {\n            continue;\n        }\n        if (Input_ComboHasLonger(\n                layout, role, M_ToCombo(active[role]), M_GetComboBinding,\n                M_ComboKeysEqual)) {\n            InputState_ClearRole(result, role);\n            if (!m_RoleWasActive[role]) {\n                m_RoleLongerFired[role] = false;\n            }\n            m_RoleWasActive[role] = true;\n        }\n    }\n\n    // Reset prefix tracking for newly pressed keys.\n    for (SDL_Scancode sc = 0; sc < SDL_NUM_SCANCODES; sc++) {\n        if (KEY_DOWN(sc) && !m_PrefixWasHeld[sc]) {\n            m_PrefixComboFired[sc] = false;\n        }\n    }\n\n    // Phase 4: Mark longer-combo-fired state.\n    // When a combo fires, mark all shorter deferred combos as superseded.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (active[role] == nullptr || active[role]->key_count < 2) {\n            continue;\n        }\n        // Mark scancodes for single-key prefix deferral.\n        for (int32_t k = 0; k < active[role]->key_count; k++) {\n            m_PrefixComboFired[active[role]->keys[k]] = true;\n        }\n        // Mark shorter deferred roles as superseded.\n        for (INPUT_ROLE other = 0; other < INPUT_ROLE_NUMBER_OF; other++) {\n            if (!m_RoleWasActive[other]) {\n                continue;\n            }\n            const KEYBOARD_BINDING *ob = M_GetPressedBinding(layout, other);\n            if (ob != nullptr\n                && Input_ComboIsProperSubset(\n                    M_ToCombo(ob), M_ToCombo(active[role]), M_ComboKeysEqual)) {\n                m_RoleLongerFired[other] = true;\n            }\n        }\n    }\n\n    // Phase 5: Fire deferred roles on release.\n    // When a deferred role's binding is no longer fully pressed and no\n    // longer combo fired, inject the role for one frame.\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (!m_RoleWasActive[role]) {\n            continue;\n        }\n        const KEYBOARD_BINDING *bind = M_GetPressedBinding(layout, role);\n        if (bind != nullptr) {\n            continue;\n        }\n        if (!m_RoleLongerFired[role]) {\n            InputState_SetRole(result, role, true);\n        }\n        m_RoleWasActive[role] = false;\n        m_RoleLongerFired[role] = false;\n    }\n\n    // Phase 6: Single-key prefix deferral.\n    for (SDL_Scancode sc = 0; sc < SDL_NUM_SCANCODES; sc++) {\n        const bool held = KEY_DOWN(sc);\n\n        if (held\n            && Input_ComboIsStarter(\n                layout, &sc, M_GetComboBinding, M_ComboKeysEqual)) {\n            const INPUT_ROLE role = Input_ComboFindDeferrableRole(\n                layout, &sc, M_GetComboBinding, M_ComboKeysEqual);\n            if (role != (INPUT_ROLE)-1) {\n                InputState_ClearRole(result, role);\n            }\n        }\n\n        if (!held && m_PrefixWasHeld[sc] && !m_PrefixComboFired[sc]) {\n            const INPUT_ROLE role = Input_ComboFindDeferrableRole(\n                layout, &sc, M_GetComboBinding, M_ComboKeysEqual);\n            if (role != (INPUT_ROLE)-1) {\n                InputState_SetRole(result, role, true);\n            }\n        }\n\n        m_PrefixWasHeld[sc] = held;\n    }\n}\n\n// Combo capture state for listen mode.\nstatic KEYBOARD_BINDING m_CaptureBuffer = { .key_count = 0 };\nstatic bool m_CaptureActive = false;\n\nstatic bool M_CaptureHasKey(const SDL_Scancode scancode)\n{\n    for (int32_t i = 0; i < m_CaptureBuffer.key_count; i++) {\n        if (m_CaptureBuffer.keys[i] == scancode) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_ReadAndAssign(\n    const INPUT_LAYOUT layout, const INPUT_ROLE role, const int32_t slot)\n{\n    // Count currently held keys and accumulate new ones into the buffer.\n    bool any_held = false;\n    for (SDL_Scancode sc = 0; sc < SDL_NUM_SCANCODES; sc++) {\n        if (!KEY_DOWN(sc)) {\n            continue;\n        }\n        any_held = true;\n        if (!M_CaptureHasKey(sc)\n            && m_CaptureBuffer.key_count < INPUT_COMBO_MAX_KEYS) {\n            m_CaptureBuffer.keys[m_CaptureBuffer.key_count++] = sc;\n        }\n        m_CaptureActive = true;\n    }\n\n    // If the first key captured is bound to an immediate role (movement,\n    // action, etc.), assign as single key right away — don't wait for combo.\n    if (m_CaptureActive && m_CaptureBuffer.key_count == 1 && any_held\n        && Input_ComboIsKeyImmediate(\n            layout, &m_CaptureBuffer.keys[0], M_GetComboBinding,\n            M_ComboKeysEqual)) {\n        M_AssignBinding(layout, role, slot, &m_CaptureBuffer);\n        m_CaptureBuffer.key_count = 0;\n        m_CaptureActive = false;\n        return true;\n    }\n\n    // All keys released after at least one was captured — assign the chord.\n    if (!any_held && m_CaptureActive) {\n        M_AssignBinding(layout, role, slot, &m_CaptureBuffer);\n        m_CaptureBuffer.key_count = 0;\n        m_CaptureActive = false;\n        return true;\n    }\n    return false;\n}\n\nINPUT_BACKEND_IMPL g_Input_Keyboard = {\n    .init = M_Init,\n    .shutdown = nullptr,\n    .discover = nullptr,\n    .process_event = M_ProcessEvent,\n    .custom_update = M_CustomUpdate,\n    .is_pressed = M_IsPressed,\n    .is_role_conflicted = M_IsRoleConflicted,\n    .get_name = M_GetName,\n    .unassign_role = M_UnassignRole,\n    .assign_from_json_object = M_AssignFromJSONObject,\n    .assign_to_json_object = M_AssignToJSONObject,\n    .reset_layout = M_ResetLayout,\n    .read_and_assign = M_ReadAndAssign,\n    .resolve_combos = M_ResolveCombos,\n};\n"
  },
  {
    "path": "src/trx/game/input/backends/keyboard.def",
    "content": "INPUT_KEYBOARD_ASSIGN(INPUT_ROLE_UP,                      SDL_SCANCODE_UP)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_DOWN,                    SDL_SCANCODE_DOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LEFT,                    SDL_SCANCODE_LEFT)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_RIGHT,                   SDL_SCANCODE_RIGHT)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_JUMP,                    SDL_SCANCODE_RALT)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_DRAW_WEAPON,             SDL_SCANCODE_SPACE)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ACTION,                  SDL_SCANCODE_RCTRL)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_SLOW,                    SDL_SCANCODE_RSHIFT)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LOOK,                    SDL_SCANCODE_KP_0)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_STEP_LEFT,               SDL_SCANCODE_DELETE)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_STEP_RIGHT,              SDL_SCANCODE_PAGEDOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ROLL,                    SDL_SCANCODE_END)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_SPRINT,                  SDL_SCANCODE_PERIOD)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CROUCH,                  SDL_SCANCODE_C)\n\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ENTER_CONSOLE,           SDL_SCANCODE_SLASH)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_SAVE,                    SDL_SCANCODE_F5)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LOAD,                    SDL_SCANCODE_F6)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_QUICK_SAVE,              SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_QUICK_LOAD,              SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_PAUSE,                   SDL_SCANCODE_P)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_UI,               SDL_SCANCODE_H)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_PHOTO_MODE,       SDL_SCANCODE_F1)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CHANGE_TARGET,           SDL_SCANCODE_Z)\n\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_FLY_CHEAT,               SDL_SCANCODE_O)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_ITEM_CHEAT,              SDL_SCANCODE_I)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_LEVEL_SKIP_CHEAT,        SDL_SCANCODE_L)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TURBO_CHEAT,             SDL_SCANCODE_TAB)\n\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_UP,               SDL_SCANCODE_Q)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_DOWN,             SDL_SCANCODE_E)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_FORWARD,          SDL_SCANCODE_W)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_BACK,             SDL_SCANCODE_S)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_LEFT,             SDL_SCANCODE_A)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CAMERA_RIGHT,            SDL_SCANCODE_D)\n\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_PISTOLS,           SDL_SCANCODE_1)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_SHOTGUN,           SDL_SCANCODE_2)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_MAGNUMS,           SDL_SCANCODE_3)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_AUTOS,             SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_DESERT_EAGLE,      SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_UZIS,              SDL_SCANCODE_4)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_HARPOON,           SDL_SCANCODE_5)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_M16,               SDL_SCANCODE_6)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_MP5,               SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_GRENADE_LAUNCHER,  SDL_SCANCODE_7)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_EQUIP_ROCKET_LAUNCHER,   SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_USE_SMALL_MEDI,          SDL_SCANCODE_8)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_USE_BIG_MEDI,            SDL_SCANCODE_9)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_USE_FLARE,               SDL_SCANCODE_COMMA)\n\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_FULLSCREEN,       SDL_SCANCODE_UNKNOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_FPS,                     SDL_SCANCODE_F2)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_BILINEAR_FILTER,  SDL_SCANCODE_F3)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER, SDL_SCANCODE_F4)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_WIREFRAME,        SDL_SCANCODE_F7)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_TOGGLE_TEXTURES,         SDL_SCANCODE_F8)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CYCLE_LIGHTING_CONTRAST, SDL_SCANCODE_F9)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_CHANGE_OUTFIT,           SDL_SCANCODE_T)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_RESET_BINDINGS,          SDL_SCANCODE_R)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_UNBIND_KEY,              SDL_SCANCODE_BACKSPACE)\n\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_INVENTORY,               SDL_SCANCODE_ESCAPE)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_BACK,               SDL_SCANCODE_ESCAPE)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_CONFIRM,            SDL_SCANCODE_RETURN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_UP,                 SDL_SCANCODE_UP)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_DOWN,               SDL_SCANCODE_DOWN)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_LEFT,               SDL_SCANCODE_LEFT)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_RIGHT,              SDL_SCANCODE_RIGHT)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_TAB_LEFT,           SDL_SCANCODE_DELETE)\nINPUT_KEYBOARD_ASSIGN(INPUT_ROLE_MENU_TAB_RIGHT,          SDL_SCANCODE_PAGEDOWN)\n"
  },
  {
    "path": "src/trx/game/input/backends/keyboard.h",
    "content": "#include <trx/game/input/backends/base.h>\n\nextern INPUT_BACKEND_IMPL g_Input_Keyboard;\n"
  },
  {
    "path": "src/trx/game/input/combo.c",
    "content": "#include <trx/game/input/combo.h>\n\n#include <trx/game/input/common.h>\n\nstatic bool M_IsKeyNonCapturingSustained(\n    const INPUT_LAYOUT layout, const void *const key,\n    const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) {\n        if (!Input_IsRoleSustained(r) || Input_IsRoleCapturing(r)) {\n            continue;\n        }\n        for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) {\n            const INPUT_COMBO_BINDING b = get_binding(layout, r, s);\n            if (b.key_count == 1 && keys_equal(Input_ComboKeyAt(&b, 0), key)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nbool Input_ComboIsProperSubset(\n    const INPUT_COMBO_BINDING sub, const INPUT_COMBO_BINDING super,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    if (sub.key_count == 0 || sub.key_count >= super.key_count) {\n        return false;\n    }\n    for (int32_t i = 0; i < sub.key_count; i++) {\n        bool found = false;\n        for (int32_t j = 0; j < super.key_count; j++) {\n            if (keys_equal(\n                    Input_ComboKeyAt(&sub, i), Input_ComboKeyAt(&super, j))) {\n                found = true;\n                break;\n            }\n        }\n        if (!found) {\n            return false;\n        }\n    }\n    return true;\n}\n\nbool Input_ComboHasLonger(\n    const INPUT_LAYOUT layout, const INPUT_ROLE skip_role,\n    const INPUT_COMBO_BINDING bind, const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) {\n        if (r == skip_role) {\n            continue;\n        }\n        for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) {\n            const INPUT_COMBO_BINDING b = get_binding(layout, r, s);\n            if (Input_ComboIsProperSubset(bind, b, keys_equal)\n                && keys_equal(\n                    Input_ComboKeyAt(&bind, 0), Input_ComboKeyAt(&b, 0))) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nbool Input_ComboIsStarter(\n    const INPUT_LAYOUT layout, const void *const key,\n    const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) {\n        for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) {\n            const INPUT_COMBO_BINDING b = get_binding(layout, r, s);\n            if (b.key_count >= 2 && keys_equal(Input_ComboKeyAt(&b, 0), key)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nINPUT_ROLE Input_ComboFindDeferrableRole(\n    const INPUT_LAYOUT layout, const void *const key,\n    const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) {\n        if (Input_IsRoleImmediate(r) || Input_IsRoleSustained(r)\n            || !Input_IsRoleRebindable(r)) {\n            continue;\n        }\n        for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) {\n            const INPUT_COMBO_BINDING b = get_binding(layout, r, s);\n            if (b.key_count == 1 && keys_equal(Input_ComboKeyAt(&b, 0), key)) {\n                return r;\n            }\n        }\n    }\n    return (INPUT_ROLE)-1;\n}\n\nbool Input_ComboIsKeyImmediate(\n    const INPUT_LAYOUT layout, const void *const key,\n    const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    for (INPUT_ROLE r = 0; r < INPUT_ROLE_NUMBER_OF; r++) {\n        if (!Input_IsRoleImmediate(r) || !Input_IsRoleRebindable(r)) {\n            continue;\n        }\n        for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) {\n            const INPUT_COMBO_BINDING b = get_binding(layout, r, s);\n            if (b.key_count == 1 && keys_equal(Input_ComboKeyAt(&b, 0), key)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nbool Input_ComboStartsWithImmediate(\n    const INPUT_LAYOUT layout, const INPUT_COMBO_BINDING bind,\n    const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    if (bind.key_count < 2) {\n        return false;\n    }\n    return Input_ComboIsKeyImmediate(\n        layout, Input_ComboKeyAt(&bind, 0), get_binding, keys_equal);\n}\n\nbool Input_ComboSustainedHasImmediate(\n    const INPUT_LAYOUT layout, const INPUT_COMBO_BINDING bind,\n    const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal)\n{\n    if (bind.key_count < 2) {\n        return false;\n    }\n    if (!M_IsKeyNonCapturingSustained(\n            layout, Input_ComboKeyAt(&bind, 0), get_binding, keys_equal)) {\n        return false;\n    }\n    for (int32_t k = 1; k < bind.key_count; k++) {\n        if (Input_ComboIsKeyImmediate(\n                layout, Input_ComboKeyAt(&bind, k), get_binding, keys_equal)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nvoid Input_ComboCheckConflicts(\n    const INPUT_LAYOUT layout, const INPUT_COMBO_GET_BINDING get_binding,\n    const INPUT_COMBO_KEYS_EQUAL keys_equal,\n    bool conflicts[INPUT_ROLE_NUMBER_OF])\n{\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        if (conflicts[role]) {\n            continue;\n        }\n        for (int32_t s = 0; s < INPUT_BINDING_SLOTS; s++) {\n            const INPUT_COMBO_BINDING b = get_binding(layout, role, s);\n            if (Input_ComboStartsWithImmediate(\n                    layout, b, get_binding, keys_equal)\n                || Input_ComboSustainedHasImmediate(\n                    layout, b, get_binding, keys_equal)) {\n                conflicts[role] = true;\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/input/combo.h",
    "content": "#pragma once\n\n#include <trx/game/input/common.h>\n\n#include <stdbool.h>\n#include <stdint.h>\n\n// Generic view of a backend-specific binding for combo operations.\n// Backends create these from their concrete binding types.\ntypedef struct {\n    int32_t key_count;\n    const void *keys;\n    int32_t key_stride;\n} INPUT_COMBO_BINDING;\n\ntypedef bool (*INPUT_COMBO_KEYS_EQUAL)(const void *a, const void *b);\ntypedef INPUT_COMBO_BINDING (*INPUT_COMBO_GET_BINDING)(\n    INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n\nstatic inline const void *Input_ComboKeyAt(\n    const INPUT_COMBO_BINDING *b, int32_t idx)\n{\n    return (const char *)b->keys + idx * b->key_stride;\n}\n\n// Check if sub's keys are a proper subset of super's keys.\nbool Input_ComboIsProperSubset(\n    INPUT_COMBO_BINDING sub, INPUT_COMBO_BINDING super,\n    INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Check if any binding in the layout is a longer combo that shares the\n// same starter key (keys[0]) as bind.\nbool Input_ComboHasLonger(\n    INPUT_LAYOUT layout, INPUT_ROLE skip_role, INPUT_COMBO_BINDING bind,\n    INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Check if a key is the first key of any multi-key combo.\nbool Input_ComboIsStarter(\n    INPUT_LAYOUT layout, const void *key, INPUT_COMBO_GET_BINDING get_binding,\n    INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Find the role with a single-key binding matching the given key,\n// excluding immediate, sustained, and non-rebindable roles (which are\n// never deferred). Returns (INPUT_ROLE)-1 if none found.\nINPUT_ROLE Input_ComboFindDeferrableRole(\n    INPUT_LAYOUT layout, const void *key, INPUT_COMBO_GET_BINDING get_binding,\n    INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Check if a key (as a single-key binding) is bound to a rebindable\n// immediate role.\nbool Input_ComboIsKeyImmediate(\n    INPUT_LAYOUT layout, const void *key, INPUT_COMBO_GET_BINDING get_binding,\n    INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Check if a combo starts with an immediate role's key.\nbool Input_ComboStartsWithImmediate(\n    INPUT_LAYOUT layout, INPUT_COMBO_BINDING bind,\n    INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Check if a combo starts with a non-capturing sustained role's key and\n// contains an immediate role's key. Such combos are invalid because the\n// immediate action fires before the combo can form.\nbool Input_ComboSustainedHasImmediate(\n    INPUT_LAYOUT layout, INPUT_COMBO_BINDING bind,\n    INPUT_COMBO_GET_BINDING get_binding, INPUT_COMBO_KEYS_EQUAL keys_equal);\n\n// Run combo-specific conflict checks on all roles. Flags bindings that\n// start with an immediate key or are invalid sustained+immediate combos.\n// Must be called after the normal conflict helper has run.\nvoid Input_ComboCheckConflicts(\n    INPUT_LAYOUT layout, INPUT_COMBO_GET_BINDING get_binding,\n    INPUT_COMBO_KEYS_EQUAL keys_equal, bool conflicts[INPUT_ROLE_NUMBER_OF]);\n"
  },
  {
    "path": "src/trx/game/input/common.c",
    "content": "#include <trx/game/input/common.h>\n\n#include <trx/core/enum_map.h>\n#include <trx/core/strings.h>\n#include <trx/game/clock.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input/backends/controller.h>\n#include <trx/game/input/backends/keyboard.h>\n#include <trx/version.h>\n\n#include <SDL2/SDL_keyboard.h>\n#include <ctype.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n\ntypedef enum {\n    HOLD_INACTIVE,\n    HOLD_DELAY,\n    HOLD_REPEATING,\n} M_HOLD_STATE;\n\ntypedef struct {\n    CLOCK_TIMER delay_timer;\n    CLOCK_TIMER repeat_timer;\n    double delay_time;\n    double hold_time;\n    M_HOLD_STATE state;\n    INPUT_ROLE role;\n} M_HOLD_CHECK;\n\nINPUT_STATE g_Input = {};\nINPUT_STATE g_InputDB = {};\nINPUT_STATE g_OldInputDB = {};\n\nstatic bool m_ListenMode = false;\n\nstatic M_HOLD_CHECK m_HoldChecks[] = {\n    { .role = INPUT_ROLE_MENU_UP, .delay_time = 0.4, .hold_time = 0.1 },\n    { .role = INPUT_ROLE_MENU_DOWN, .delay_time = 0.4, .hold_time = 0.1 },\n    { .role = INPUT_ROLE_MENU_LEFT, .delay_time = 0.4, .hold_time = 0.2 },\n    { .role = INPUT_ROLE_MENU_RIGHT, .delay_time = 0.4, .hold_time = 0.2 },\n    { .role = INPUT_ROLE_MENU_SKIP, .delay_time = 0.4, .hold_time = 0.1 },\n    { .role = (INPUT_ROLE)-1 }, // sentinel\n};\n\nstatic bool m_IsRoleHardcoded[INPUT_ROLE_NUMBER_OF] = {\n    // clang-format off\n    [INPUT_ROLE_RESET_BINDINGS]           = true,\n    [INPUT_ROLE_UNBIND_KEY]               = true,\n    [INPUT_ROLE_MENU_CONFIRM]             = true,\n    [INPUT_ROLE_MENU_BACK]                = true,\n    [INPUT_ROLE_MENU_LEFT]                = true,\n    [INPUT_ROLE_MENU_RIGHT]               = true,\n    [INPUT_ROLE_MENU_UP]                  = true,\n    [INPUT_ROLE_MENU_DOWN]                = true,\n    [INPUT_ROLE_MENU_TAB_LEFT]            = true,\n    [INPUT_ROLE_MENU_TAB_RIGHT]           = true,\n    // clang-format on\n};\n\nstatic bool m_IsRoleImmediate[INPUT_ROLE_NUMBER_OF] = {\n    // clang-format off\n    [INPUT_ROLE_UP]              = true,\n    [INPUT_ROLE_DOWN]            = true,\n    [INPUT_ROLE_LEFT]            = true,\n    [INPUT_ROLE_RIGHT]           = true,\n    [INPUT_ROLE_JUMP]            = true,\n    [INPUT_ROLE_ROLL]            = true,\n    // clang-format on\n};\n\nstatic bool m_IsRoleSustained[INPUT_ROLE_NUMBER_OF] = {\n    // clang-format off\n    [INPUT_ROLE_ACTION]      = true,\n    [INPUT_ROLE_STEP_LEFT]   = true,\n    [INPUT_ROLE_STEP_RIGHT]  = true,\n    [INPUT_ROLE_LOOK]        = true,\n    [INPUT_ROLE_SLOW]        = true,\n    [INPUT_ROLE_CROUCH]      = true,\n    [INPUT_ROLE_SPRINT]      = true,\n    // clang-format on\n};\n\nstatic bool m_IsRoleCapturing[INPUT_ROLE_NUMBER_OF] = {\n    // clang-format off\n    [INPUT_ROLE_LOOK] = true,\n    // clang-format on\n};\n\nstatic bool m_IsRoleNonUnbindable[INPUT_ROLE_NUMBER_OF] = {\n    // clang-format off\n    [INPUT_ROLE_UP]          = true,\n    [INPUT_ROLE_DOWN]        = true,\n    [INPUT_ROLE_LEFT]        = true,\n    [INPUT_ROLE_RIGHT]       = true,\n    [INPUT_ROLE_DRAW_WEAPON] = true,\n    [INPUT_ROLE_ACTION]      = true,\n    [INPUT_ROLE_JUMP]        = true,\n    [INPUT_ROLE_ROLL]        = true,\n    [INPUT_ROLE_LOOK]        = true,\n    [INPUT_ROLE_SLOW]        = true,\n    [INPUT_ROLE_INVENTORY]   = true,\n    // clang-format on\n};\n\nstatic const GAME_STRING_ID m_LayoutMap[INPUT_LAYOUT_NUMBER_OF] = {\n    [INPUT_LAYOUT_DEFAULT] = GS_ID(\"general/settings/controls/layout/default\"),\n    [INPUT_LAYOUT_CUSTOM_1] =\n        GS_ID(\"general/settings/controls/layout/custom_1\"),\n    [INPUT_LAYOUT_CUSTOM_2] =\n        GS_ID(\"general/settings/controls/layout/custom_2\"),\n    [INPUT_LAYOUT_CUSTOM_3] =\n        GS_ID(\"general/settings/controls/layout/custom_3\"),\n};\n\nstatic INPUT_BACKEND_IMPL *M_GetBackend(const INPUT_BACKEND backend)\n{\n    switch (backend) {\n    case INPUT_BACKEND_KEYBOARD:\n        return &g_Input_Keyboard;\n    case INPUT_BACKEND_CONTROLLER:\n        return &g_Input_Controller;\n    default:\n        return nullptr;\n    }\n}\n\nstatic bool M_IsPressed(const INPUT_STATE input, const INPUT_ROLE role)\n{\n    switch (role) {\n#define X_INPUT_ROLE(role_name, state_name)                                    \\\n    case role_name:                                                            \\\n        return input.state_name;\n#include <trx/game/input/roles.def>\n#undef X_INPUT_ROLE\n    case INPUT_ROLE_NUMBER_OF:\n        break;\n    }\n    return false;\n}\n\nstatic INPUT_STATE M_SetPressed(\n    INPUT_STATE input, const INPUT_ROLE role, const bool is_pressed)\n{\n    switch (role) {\n#define X_INPUT_ROLE(role_name, state_name)                                    \\\n    case role_name:                                                            \\\n        input.state_name = is_pressed;                                         \\\n        break;\n#include <trx/game/input/roles.def>\n#undef X_INPUT_ROLE\n    case INPUT_ROLE_NUMBER_OF:\n        break;\n    }\n    return input;\n}\n\nvoid Input_Reset(void)\n{\n    InputState_Clear(&g_Input);\n    InputState_Clear(&g_InputDB);\n    InputState_Clear(&g_OldInputDB);\n\n    for (int32_t i = 0; m_HoldChecks[i].role != (INPUT_ROLE)-1; i++) {\n        M_HOLD_CHECK *const hold_check = &m_HoldChecks[i];\n        hold_check->state = HOLD_INACTIVE;\n        ClockTimer_Sync(&hold_check->delay_timer);\n        ClockTimer_Sync(&hold_check->repeat_timer);\n    }\n}\n\nvoid Input_Init(void)\n{\n    for (int32_t i = 0; m_HoldChecks[i].role != (INPUT_ROLE)-1; i++) {\n        m_HoldChecks[i].delay_timer.type = CLOCK_TIMER_REAL;\n        m_HoldChecks[i].repeat_timer.type = CLOCK_TIMER_REAL;\n    }\n    Input_Reset();\n    if (g_Input_Keyboard.init != nullptr) {\n        g_Input_Keyboard.init();\n    }\n    if (g_Input_Controller.init != nullptr) {\n        g_Input_Controller.init();\n    }\n}\n\nvoid Input_Shutdown(void)\n{\n    Input_Reset();\n    if (g_Input_Keyboard.shutdown != nullptr) {\n        g_Input_Keyboard.shutdown();\n    }\n    if (g_Input_Controller.shutdown != nullptr) {\n        g_Input_Controller.shutdown();\n    }\n}\n\nvoid Input_Discover(void)\n{\n    if (g_Input_Keyboard.discover != nullptr) {\n        g_Input_Keyboard.discover();\n    }\n    if (g_Input_Controller.discover != nullptr) {\n        g_Input_Controller.discover();\n    }\n}\n\nbool Input_IsRoleRebindable(const INPUT_ROLE role)\n{\n    return !m_IsRoleHardcoded[role];\n}\n\nbool Input_IsRoleUnbindable(const INPUT_ROLE role)\n{\n    return !m_IsRoleNonUnbindable[role];\n}\n\nbool Input_IsRoleImmediate(const INPUT_ROLE role)\n{\n    return m_IsRoleImmediate[role];\n}\n\nbool Input_IsRoleSustained(const INPUT_ROLE role)\n{\n    return m_IsRoleSustained[role];\n}\n\nbool Input_IsRoleCapturing(const INPUT_ROLE role)\n{\n    return m_IsRoleCapturing[role];\n}\n\nbool Input_IsPressed(const INPUT_ROLE role)\n{\n    return M_IsPressed(g_Input, role);\n}\n\nbool Input_IsPressedDB(const INPUT_ROLE role)\n{\n    return M_IsPressed(g_InputDB, role);\n}\n\nbool Input_IsPressedEx(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    const INPUT_ROLE role)\n{\n    return M_GetBackend(backend)->is_pressed(layout, role);\n}\n\nbool Input_IsKeyConflicted(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    const INPUT_ROLE role)\n{\n    return M_GetBackend(backend)->is_role_conflicted(layout, role);\n}\n\nbool Input_ReadAndAssignRole(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    const INPUT_ROLE role, const int32_t slot)\n{\n    // Check for canceling from other devices\n    for (INPUT_BACKEND other_backend = 0;\n         other_backend < INPUT_BACKEND_NUMBER_OF; other_backend++) {\n        if (other_backend == backend) {\n            continue;\n        }\n        if (Input_IsPressedEx(other_backend, layout, INPUT_ROLE_MENU_BACK)\n            || Input_IsPressedEx(other_backend, layout, INPUT_ROLE_INVENTORY)) {\n            return true;\n        }\n    }\n\n    return M_GetBackend(backend)->read_and_assign(layout, role, slot);\n}\n\nvoid Input_UnassignRole(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    const INPUT_ROLE role, const int32_t slot)\n{\n    M_GetBackend(backend)->unassign_role(layout, role, slot);\n}\n\nconst char *Input_GetKeyName(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    const INPUT_ROLE role, const int32_t slot)\n{\n    return M_GetBackend(backend)->get_name(layout, role, slot);\n}\n\nvoid Input_ResetLayout(const INPUT_BACKEND backend, const INPUT_LAYOUT layout)\n{\n    M_GetBackend(backend)->reset_layout(layout);\n}\n\nvoid Input_EnterListenMode(void)\n{\n    m_ListenMode = true;\n}\n\nvoid Input_ExitListenMode(void)\n{\n    m_ListenMode = false;\n    Input_Update();\n    InputState_Copy(&g_OldInputDB, g_Input);\n    InputState_Copy(&g_InputDB, g_Input);\n}\n\nbool Input_IsInListenMode(void)\n{\n    return m_ListenMode;\n}\n\nvoid Input_ProcessEvent(const SDL_Event *event)\n{\n    if (g_Input_Keyboard.process_event != nullptr) {\n        g_Input_Keyboard.process_event(event);\n    }\n    if (g_Input_Controller.process_event != nullptr) {\n        g_Input_Controller.process_event(event);\n    }\n}\n\nbool Input_AssignFromJSONObject(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    JSON_OBJECT *const bind_obj)\n{\n    INPUT_ROLE role = (INPUT_ROLE)-1;\n\n    // TR1X <=4.5, TR2X <=0.5\n    const int32_t role_idx = JSON_ObjectGetInt(bind_obj, \"role\", -1);\n    // clang-format off\n    switch (role_idx) {\n    case 0: role = INPUT_ROLE_UP; break;\n    case 1: role = INPUT_ROLE_DOWN; break;\n    case 2: role = INPUT_ROLE_LEFT; break;\n    case 3: role = INPUT_ROLE_RIGHT; break;\n    case 4: role = INPUT_ROLE_STEP_LEFT; break;\n    case 5: role = INPUT_ROLE_STEP_RIGHT; break;\n    case 6: role = INPUT_ROLE_SLOW; break;\n    case 7: role = INPUT_ROLE_JUMP; break;\n    case 8: role = INPUT_ROLE_ACTION; break;\n    case 9: role = INPUT_ROLE_DRAW_WEAPON; break;\n    case 10: role = INPUT_ROLE_LOOK; break;\n    case 11: role = INPUT_ROLE_ROLL; break;\n    case 12: role = INPUT_ROLE_INVENTORY; break;\n    case 13: role = INPUT_ROLE_FLY_CHEAT; break;\n    case 14: role = INPUT_ROLE_ITEM_CHEAT; break;\n    case 15: role = INPUT_ROLE_LEVEL_SKIP_CHEAT; break;\n    case 16: role = INPUT_ROLE_TURBO_CHEAT; break;\n    case 17: role = INPUT_ROLE_PAUSE; break;\n    case 18: role = INPUT_ROLE_CAMERA_FORWARD; break;\n    case 19: role = INPUT_ROLE_CAMERA_BACK; break;\n    case 20: role = INPUT_ROLE_CAMERA_LEFT; break;\n    case 21: role = INPUT_ROLE_CAMERA_RIGHT; break;\n    case 22: role = INPUT_ROLE_CAMERA_RESET; break;\n    case 23: role = INPUT_ROLE_EQUIP_PISTOLS; break;\n    case 24: role = INPUT_ROLE_EQUIP_SHOTGUN; break;\n    case 25: role = INPUT_ROLE_EQUIP_MAGNUMS; break;\n    case 26: role = INPUT_ROLE_EQUIP_UZIS; break;\n    case 27: role = INPUT_ROLE_USE_SMALL_MEDI; break;\n    case 28: role = INPUT_ROLE_USE_BIG_MEDI; break;\n    case 29: role = INPUT_ROLE_SAVE; break;\n    case 30: role = INPUT_ROLE_LOAD; break;\n    case 31: role = INPUT_ROLE_FPS; break;\n    case 32: role = INPUT_ROLE_TOGGLE_BILINEAR_FILTER; break;\n    case 33: role = INPUT_ROLE_ENTER_CONSOLE; break;\n    case 34: role = INPUT_ROLE_CHANGE_TARGET; break;\n    case 35: role = INPUT_ROLE_TOGGLE_UI; break;\n    case 36: role = INPUT_ROLE_CAMERA_UP; break;\n    case 37: role = INPUT_ROLE_CAMERA_DOWN; break;\n    case 38: role = INPUT_ROLE_TOGGLE_PHOTO_MODE; break;\n    case 39: role = INPUT_ROLE_UNBIND_KEY; break;\n    case 40: role = INPUT_ROLE_RESET_BINDINGS; break;\n    case 42: role = INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER; break;\n    case 43: role = INPUT_ROLE_MENU_CONFIRM; break;\n    case 44: role = INPUT_ROLE_MENU_BACK; break;\n    case 45: role = INPUT_ROLE_MENU_LEFT; break;\n    case 46: role = INPUT_ROLE_MENU_UP; break;\n    case 47: role = INPUT_ROLE_MENU_DOWN; break;\n    case 48: role = INPUT_ROLE_MENU_RIGHT; break;\n    case 49: role = INPUT_ROLE_SCREENSHOT; break;\n    case 50: role = INPUT_ROLE_TOGGLE_FULLSCREEN; break;\n    }\n    // clang-format on\n\n    // TR1X >= 4.6, TR2X >= 0.6\n    if (role == (INPUT_ROLE)-1) {\n        role = ENUM_MAP_GET(\n            INPUT_ROLE, JSON_ObjectGetString(bind_obj, \"role\", \"\"),\n            (int32_t)(INPUT_ROLE)-1);\n    }\n\n    if (role == (INPUT_ROLE)-1) {\n        return false;\n    }\n\n    const int32_t slot = JSON_ObjectGetInt(bind_obj, \"slot\", 0);\n    return M_GetBackend(backend)->assign_from_json_object(\n        layout, role, slot, bind_obj);\n}\n\nbool Input_AssignToJSONObject(\n    const INPUT_BACKEND backend, const INPUT_LAYOUT layout,\n    JSON_OBJECT *const bind_obj, const INPUT_ROLE role, const int32_t slot)\n{\n    JSON_ObjectAppendString(\n        bind_obj, \"role\", ENUM_MAP_TO_STRING(INPUT_ROLE, role));\n    if (slot != 0) {\n        JSON_ObjectAppendInt(bind_obj, \"slot\", slot);\n    }\n    return M_GetBackend(backend)->assign_to_json_object(\n        layout, role, slot, bind_obj);\n}\n\nconst char *const *Input_GetLayoutNamePtr(const INPUT_LAYOUT layout)\n{\n    return GameString_GetPtr(m_LayoutMap[layout]);\n}\n\nINPUT_STATE Input_GetDebounced(const INPUT_STATE input)\n{\n    INPUT_STATE result;\n    for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) {\n        result.any[i] = input.any[i] & ~g_OldInputDB.any[i];\n    }\n\n    // Allow holding certain keys\n    for (int32_t i = 0; m_HoldChecks[i].role != (INPUT_ROLE)-1; i++) {\n        M_HOLD_CHECK *const hold_check = &m_HoldChecks[i];\n        if (!M_IsPressed(input, hold_check->role)) {\n            hold_check->state = HOLD_INACTIVE;\n        } else if (hold_check->state == HOLD_INACTIVE) {\n            hold_check->state = HOLD_DELAY;\n            ClockTimer_Sync(&hold_check->delay_timer);\n        } else if (\n            hold_check->state == HOLD_DELAY\n            && ClockTimer_CheckElapsedAndTake(\n                &hold_check->delay_timer, hold_check->delay_time)) {\n            hold_check->state = HOLD_REPEATING;\n        } else if (\n            hold_check->state == HOLD_REPEATING\n            && ClockTimer_CheckElapsedAndTake(\n                &hold_check->repeat_timer, hold_check->hold_time)) {\n            result = M_SetPressed(result, hold_check->role, true);\n        }\n    }\n\n    g_OldInputDB = input;\n    return result;\n}\n\nconst char *Input_GetRoleName(const INPUT_ROLE role)\n{\n    return EnumMap_GetLabel(ENUM_MAP_NAME(INPUT_ROLE), role);\n}\n\nconst char *Input_KeyDescFromSDL(SDL_Scancode scancode, SDL_Keymod mod)\n{\n    // clang-format off\n    const char *mods = \"\";\n    if (mod & KMOD_CTRL)  { mods = String_FormatStatic(\"%sctrl+\",  mods); }\n    if (mod & KMOD_SHIFT) { mods = String_FormatStatic(\"%sshift+\", mods); }\n    if (mod & KMOD_ALT)   { mods = String_FormatStatic(\"%salt+\",   mods); }\n    if (mod & KMOD_GUI)   { mods = String_FormatStatic(\"%sgui+\",   mods); }\n    // clang-format on\n\n    const char *const name = SDL_GetScancodeName(scancode);\n    if (name == nullptr || name[0] == '\\0') {\n        return nullptr;\n    }\n\n    char *const full = (char *)String_FormatStatic(\"%s%s\", mods, name);\n    for (size_t i = 0; i < strlen(full); i++) {\n        full[i] = (char)tolower((unsigned char)full[i]);\n    }\n    return full;\n}\n\nbool Input_ParseKeyDesc(\n    const char *const desc, SDL_Scancode *const scancode, SDL_Keymod *const mod)\n{\n    if (desc == nullptr || scancode == nullptr || mod == nullptr) {\n        return false;\n    }\n\n    SDL_Keymod m = KMOD_NONE;\n    const char *keystr = desc;\n    const char *last = strrchr(desc, '+');\n\n    if (last != nullptr) {\n        for (const char *tok = desc; tok < last; tok = strchr(tok, '+') + 1) {\n            const size_t len =\n                strchr(tok, '+') ? strchr(tok, '+') - tok : last - tok;\n            if (strncmp(tok, \"ctrl\", len) == 0) {\n                m |= KMOD_CTRL;\n            } else if (strncmp(tok, \"shift\", len) == 0) {\n                m |= KMOD_SHIFT;\n            } else if (strncmp(tok, \"alt\", len) == 0) {\n                m |= KMOD_ALT;\n            } else if (strncmp(tok, \"gui\", len) == 0) {\n                m |= KMOD_GUI;\n            }\n        }\n        keystr = last + 1;\n    }\n\n    *scancode = SDL_GetScancodeFromName(keystr);\n    *mod = m;\n    return *scancode != SDL_SCANCODE_UNKNOWN;\n}\n\nvoid InputState_Clear(INPUT_STATE *const state)\n{\n    for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) {\n        state->any[i] = 0;\n    }\n}\n\nvoid InputState_Copy(INPUT_STATE *const dst, const INPUT_STATE src)\n{\n    for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) {\n        dst->any[i] = src.any[i];\n    }\n}\n\nbool InputState_IsAnyPressed(const INPUT_STATE state)\n{\n    for (int32_t i = 0; i < INPUT_STATE_ANY_WORDS; i++) {\n        if (state.any[i] != 0) {\n            return true;\n        }\n    }\n    return false;\n}\n\nbool InputState_GetRole(const INPUT_STATE state, const INPUT_ROLE role)\n{\n    return M_IsPressed(state, role);\n}\n\nvoid InputState_SetRole(\n    INPUT_STATE *const state, const INPUT_ROLE role, const bool value)\n{\n    *state = M_SetPressed(*state, role, value);\n}\n\nvoid InputState_ClearRole(INPUT_STATE *const state, const INPUT_ROLE role)\n{\n    *state = M_SetPressed(*state, role, false);\n}\n"
  },
  {
    "path": "src/trx/game/input/common.h",
    "content": "#pragma once\n\n#include <trx/config/enum.h>\n#include <trx/core/json.h>\n\n#include <SDL2/SDL_events.h>\n#include <stdint.h>\n\n#define INPUT_COMBO_MAX_KEYS 3\n#define INPUT_BINDING_SLOTS 2\n\ntypedef enum {\n#define X_INPUT_ROLE(role_name, state_name) role_name,\n#include <trx/game/input/roles.def>\n    INPUT_ROLE_NUMBER_OF,\n#undef X_INPUT_ROLE\n} INPUT_ROLE;\n\n#define INPUT_STATE_ANY_WORDS ((INPUT_ROLE_NUMBER_OF + 63) / 64)\n\ntypedef union {\n    uint64_t any[INPUT_STATE_ANY_WORDS];\n    struct {\n#define X_INPUT_ROLE(role_name, state_name) uint64_t state_name : 1;\n#include <trx/game/input/roles.def>\n#undef X_INPUT_ROLE\n    };\n} INPUT_STATE;\n\nextern INPUT_STATE g_Input;\nextern INPUT_STATE g_InputDB;\nextern INPUT_STATE g_OldInputDB;\n\nvoid Input_Init(void);\nvoid Input_Shutdown(void);\nvoid Input_Discover(void);\nvoid Input_Update(void);\nvoid Input_Reset(void);\n\n// Processes a SDL event to update global input state before polling.\n// @param event     Event to process.\nvoid Input_ProcessEvent(const SDL_Event *event);\n\n// Checks whether the given role can be assigned to by the player.\n// Hard-coded roles are exempt from conflict checks (eg will never flash in the\n// controls dialog).\nbool Input_IsRoleRebindable(INPUT_ROLE role);\n\n// Checks whether the given role can be completely unbound by the player.\nbool Input_IsRoleUnbindable(INPUT_ROLE role);\n\n// Checks whether the given role uses keys that fire immediately and cannot\n// be the first key of a combo (movement, jump, action, roll).\nbool Input_IsRoleImmediate(INPUT_ROLE role);\n\n// Checks whether the given role is a held state that fires immediately but\n// can still be the first key of a combo (look, walk). Unlike immediate\n// roles, these don't block combo formation because the held state is not\n// disrupted when a longer combo completes.\nbool Input_IsRoleSustained(INPUT_ROLE role);\n\n// Checks whether the given role captures other inputs when held, redirecting\n// them to different actions (e.g. look mode repurposes movement keys for\n// camera control). Capturing roles can start combos with immediate keys\n// because the immediate keys lose their normal meaning while captured.\nbool Input_IsRoleCapturing(INPUT_ROLE role);\n\n// Returns whether the key assigned to the given role is also used elsewhere\n// within the custom layout.\nbool Input_IsKeyConflicted(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role);\n\n// Checks if the key is currently pressed. Tied to Input_Update(), so updates\n// at most at the game running FPS.\nbool Input_IsPressed(INPUT_ROLE role);\n\n// Checks if the key is currently pressed with a debounce, e.g. only true\n// for the game frame the player starts to hold the key at.\nbool Input_IsPressedDB(INPUT_ROLE role);\n\n// Given the input layout and input key role, check if the assorted key is\n// pressed, bypassing Input_Update.\nbool Input_IsPressedEx(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role);\n\n// If there is anything pressed, assigns the pressed key to the given key role\n// and returns true. If nothing is pressed, immediately returns false.\nbool Input_ReadAndAssignRole(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n\n// Remove assigned key from a given key role.\nvoid Input_UnassignRole(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n\n// Get a stable pointer to the layout human-readable name.\nconst char *const *Input_GetLayoutNamePtr(const INPUT_LAYOUT layout);\n\n// Given the input layout and input key role, get the assigned key name.\nconst char *Input_GetKeyName(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, INPUT_ROLE role, int32_t slot);\n\n// Reset a given layout to the default.\nvoid Input_ResetLayout(INPUT_BACKEND backend, INPUT_LAYOUT layout);\n\n// Disables updating g_Input.\nvoid Input_EnterListenMode(void);\n\n// Enables updating g_Input.\nvoid Input_ExitListenMode(void);\n\n// Checks whether updates are disabled.\nbool Input_IsInListenMode(void);\n\n// Restores the user configuration by converting the JSON object back into the\n// original input layout.\nbool Input_AssignFromJSONObject(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, JSON_OBJECT *bind_obj);\n\n// Converts the original input layout into a JSON object for storing the user\n// configuration.\nbool Input_AssignToJSONObject(\n    INPUT_BACKEND backend, INPUT_LAYOUT layout, JSON_OBJECT *bind_obj,\n    INPUT_ROLE role, int32_t slot);\n\n// Return a copy of the input state with only newly pressed roles set.\nINPUT_STATE Input_GetDebounced(const INPUT_STATE input);\n\n// Get the human-readable name of the given role.\nconst char *Input_GetRoleName(INPUT_ROLE role);\n\n// Serialize a scancode and modifier mask into a human-readable key\n// description, e.g. \"ctrl+shift+up\". The returned string must not be held onto.\nconst char *Input_KeyDescFromSDL(SDL_Scancode scancode, SDL_Keymod mod);\n\n// Parse a human-readable key description into scancode and modifier mask.\n// e.g. \"ctrl+shift+up\" → scancode SDL_SCANCODE_UP, mod KMOD_CTRL|KMOD_SHIFT.\n// Returns true if parsing succeeded, false otherwise.\nbool Input_ParseKeyDesc(\n    const char *desc, SDL_Scancode *scancode, SDL_Keymod *mod);\n\n// Reset all roles in the input state to inactive.\nvoid InputState_Clear(INPUT_STATE *state);\n\n// Copy the source input state into the destination.\nvoid InputState_Copy(INPUT_STATE *dst, INPUT_STATE src);\n\n// Check whether any role is active in the input state.\nbool InputState_IsAnyPressed(INPUT_STATE state);\n\n// Check whether the given role is active in the input state.\nbool InputState_GetRole(INPUT_STATE state, INPUT_ROLE role);\n\n// Set or clear the given role in the input state.\nvoid InputState_SetRole(INPUT_STATE *state, INPUT_ROLE role, bool value);\n\n// Clear the given role in the input state.\nvoid InputState_ClearRole(INPUT_STATE *state, INPUT_ROLE role);\n"
  },
  {
    "path": "src/trx/game/input/roles.def",
    "content": "X_INPUT_ROLE(INPUT_ROLE_UP,                      forward)\nX_INPUT_ROLE(INPUT_ROLE_DOWN,                    back)\nX_INPUT_ROLE(INPUT_ROLE_LEFT,                    left)\nX_INPUT_ROLE(INPUT_ROLE_RIGHT,                   right)\nX_INPUT_ROLE(INPUT_ROLE_STEP_LEFT,               step_left)\nX_INPUT_ROLE(INPUT_ROLE_STEP_RIGHT,              step_right)\nX_INPUT_ROLE(INPUT_ROLE_SLOW,                    slow)\nX_INPUT_ROLE(INPUT_ROLE_CROUCH,                  crouch)\nX_INPUT_ROLE(INPUT_ROLE_JUMP,                    jump)\nX_INPUT_ROLE(INPUT_ROLE_ACTION,                  action)\nX_INPUT_ROLE(INPUT_ROLE_DRAW_WEAPON,             draw)\nX_INPUT_ROLE(INPUT_ROLE_LOOK,                    look)\nX_INPUT_ROLE(INPUT_ROLE_ROLL,                    roll)\nX_INPUT_ROLE(INPUT_ROLE_SPRINT,                  sprint)\nX_INPUT_ROLE(INPUT_ROLE_INVENTORY,               option)\nX_INPUT_ROLE(INPUT_ROLE_CHANGE_TARGET,           change_target)\n\nX_INPUT_ROLE(INPUT_ROLE_ENTER_CONSOLE,           enter_console)\nX_INPUT_ROLE(INPUT_ROLE_MENU_CONFIRM,            menu_confirm)\nX_INPUT_ROLE(INPUT_ROLE_MENU_BACK,               menu_back)\nX_INPUT_ROLE(INPUT_ROLE_MENU_LEFT,               menu_left)\nX_INPUT_ROLE(INPUT_ROLE_MENU_UP,                 menu_up)\nX_INPUT_ROLE(INPUT_ROLE_MENU_DOWN,               menu_down)\nX_INPUT_ROLE(INPUT_ROLE_MENU_RIGHT,              menu_right)\nX_INPUT_ROLE(INPUT_ROLE_MENU_SKIP,               menu_skip)\nX_INPUT_ROLE(INPUT_ROLE_MENU_TAB_LEFT,           menu_tab_left)\nX_INPUT_ROLE(INPUT_ROLE_MENU_TAB_RIGHT,          menu_tab_right)\n\nX_INPUT_ROLE(INPUT_ROLE_FLY_CHEAT,               fly_cheat)\nX_INPUT_ROLE(INPUT_ROLE_ITEM_CHEAT,              item_cheat)\nX_INPUT_ROLE(INPUT_ROLE_LEVEL_SKIP_CHEAT,        level_skip_cheat)\nX_INPUT_ROLE(INPUT_ROLE_TURBO_CHEAT,             turbo_cheat)\n\nX_INPUT_ROLE(INPUT_ROLE_SAVE,                    save)\nX_INPUT_ROLE(INPUT_ROLE_LOAD,                    load)\nX_INPUT_ROLE(INPUT_ROLE_QUICK_SAVE,              quick_save)\nX_INPUT_ROLE(INPUT_ROLE_QUICK_LOAD,              quick_load)\n\nX_INPUT_ROLE(INPUT_ROLE_SCREENSHOT,              screenshot)\nX_INPUT_ROLE(INPUT_ROLE_FPS,                     toggle_fps_counter)\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_FULLSCREEN,       toggle_fullscreen)\n\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_PISTOLS,           equip_pistols)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_SHOTGUN,           equip_shotgun)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_MAGNUMS,           equip_magnums)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_AUTOS,             equip_autos)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_DESERT_EAGLE,      equip_desert_eagle)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_UZIS,              equip_uzis)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_HARPOON,           equip_harpoon)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_M16,               equip_m16)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_MP5,               equip_mp5)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_GRENADE_LAUNCHER,  equip_grenade_launcher)\nX_INPUT_ROLE(INPUT_ROLE_EQUIP_ROCKET_LAUNCHER,   equip_rocket_launcher)\nX_INPUT_ROLE(INPUT_ROLE_USE_SMALL_MEDI,          use_small_medi)\nX_INPUT_ROLE(INPUT_ROLE_USE_BIG_MEDI,            use_big_medi)\nX_INPUT_ROLE(INPUT_ROLE_USE_FLARE,               use_flare)\n\nX_INPUT_ROLE(INPUT_ROLE_PAUSE,                   pause)\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_PHOTO_MODE,       toggle_photo_mode)\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_UI,               toggle_ui)\n\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_BILINEAR_FILTER,  toggle_bilinear_filter)\nX_INPUT_ROLE(INPUT_ROLE_CYCLE_LIGHTING_CONTRAST, cycle_lighting_contrast)\nX_INPUT_ROLE(INPUT_ROLE_CHANGE_OUTFIT,           change_outfit)\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER, toggle_trapezoid_filter)\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_WIREFRAME,        toggle_wireframe)\nX_INPUT_ROLE(INPUT_ROLE_TOGGLE_TEXTURES,         toggle_textures)\nX_INPUT_ROLE(INPUT_ROLE_SWITCH_UPSCALING,        switch_upscaling)\nX_INPUT_ROLE(INPUT_ROLE_SWITCH_BORDERS,          switch_borders)\nX_INPUT_ROLE(INPUT_ROLE_RESET_BINDINGS,          reset_bindings)\nX_INPUT_ROLE(INPUT_ROLE_UNBIND_KEY,              unbind_key)\n\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_FORWARD,          camera_forward)\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_BACK,             camera_back)\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_LEFT,             camera_left)\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_RIGHT,            camera_right)\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_UP,               camera_up)\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_DOWN,             camera_down)\nX_INPUT_ROLE(INPUT_ROLE_CAMERA_RESET,            camera_reset)\n"
  },
  {
    "path": "src/trx/game/input/update.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input/backends/base.h>\n#include <trx/game/input/backends/controller.h>\n#include <trx/game/input/backends/keyboard.h>\n#include <trx/game/input/common.h>\n#include <trx/game/lara.h>\n#include <trx/version.h>\n\nstatic void M_UpdateFromBackend(\n    INPUT_STATE *const s, const INPUT_BACKEND_IMPL *const backend,\n    const int32_t layout)\n{\n#define X_INPUT_ROLE(role, state) s->state |= backend->is_pressed(layout, role);\n#include <trx/game/input/roles.def>\n#undef X_INPUT_ROLE\n    backend->custom_update(s, layout);\n}\n\nvoid Input_Update(void)\n{\n    InputState_Clear(&g_Input);\n\n    M_UpdateFromBackend(\n        &g_Input, &g_Input_Keyboard, g_Config.input.keyboard_layout);\n    M_UpdateFromBackend(\n        &g_Input, &g_Input_Controller, g_Config.input.controller_layout);\n\n    // Suppress roles whose bindings are subsets of longer active combos.\n    g_Input_Keyboard.resolve_combos(g_Config.input.keyboard_layout, &g_Input);\n    g_Input_Controller.resolve_combos(\n        g_Config.input.controller_layout, &g_Input);\n\n    g_Input.camera_reset |= g_Input.look;\n    g_Input.menu_up |= g_Input.forward;\n    g_Input.menu_down |= g_Input.back;\n    g_Input.menu_left |= g_Input.left;\n    g_Input.menu_right |= g_Input.right;\n    g_Input.menu_back |= g_Input.option;\n    g_Input.option &= g_Camera.type != CAM_CINEMATIC;\n    g_Input.roll |= g_Input.forward && g_Input.back;\n    if (g_Input.left && g_Input.right) {\n        g_Input.left = 0;\n        g_Input.right = 0;\n    }\n\n    if (!g_Config.gameplay.enable_crawling) {\n        g_Input.crouch = 0;\n    }\n\n    if (g_Config.input.enable_tr3_sidesteps) {\n        if (g_Input.slow && !g_Input.forward && !g_Input.back\n            && !g_Input.step_left && !g_Input.step_right) {\n            if (g_Input.left) {\n                g_Input.left = 0;\n                g_Input.step_left = 1;\n            } else if (g_Input.right) {\n                g_Input.right = 0;\n                g_Input.step_right = 1;\n            }\n        }\n    }\n\n    if (!g_Config.gameplay.enable_target_change\n        || Lara_GetLaraInfo()->gun_status != LGS_READY) {\n        g_Input.change_target = 0;\n    }\n\n    g_InputDB = Input_GetDebounced(g_Input);\n\n    if (Input_IsInListenMode()) {\n        InputState_Clear(&g_Input);\n        InputState_Clear(&g_InputDB);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/input.h",
    "content": "#pragma once\n\n#include <trx/game/input/common.h>\n"
  },
  {
    "path": "src/trx/game/interpolation.c",
    "content": "#include <trx/game/interpolation.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/game/shell.h>\n\n#include <stdint.h>\n\n#define CAM_MAX_DELTA (STEP_L * 3 / 2)\n\n#define REMEMBER(target, member) (target)->interp.prev.member = (target)->member\n\n#define COMMIT(target, member) (target)->interp.result.member = (target)->member\n\n#define DIFF(target, member)                                                   \\\n    (ABS(((target)->member) - ((target)->interp.prev.member)))\n\n#define INTERPOLATE_F(target, member, ratio)                                   \\\n    (target)->interp.result.member = ((target)->interp.prev.member)            \\\n        + (((target)->member - ((target)->interp.prev.member)) * (ratio));\n\n#define INTERPOLATE(target, member, ratio, max_diff)                           \\\n    if (DIFF((target), member) >= (max_diff)) {                                \\\n        COMMIT((target), member);                                              \\\n    } else {                                                                   \\\n        INTERPOLATE_F(target, member, ratio);                                  \\\n    }\n\n#define INTERPOLATE_ROT_F(target, member, ratio)                               \\\n    (target)->interp.result.member = Math_AngleMean(                           \\\n        (target)->interp.prev.member, (target)->member, (ratio))\n\n#define INTERPOLATE_ROT(target, member, ratio, max_diff)                       \\\n    if (!Math_AngleInCone(                                                     \\\n            (target)->member, (target)->interp.prev.member, (max_diff))) {     \\\n        COMMIT((target), member);                                              \\\n    } else {                                                                   \\\n        INTERPOLATE_ROT_F(target, member, ratio);                              \\\n    }\n\nstatic bool m_IsEnabled = true;\nstatic double m_Rate = 0.0;\nstatic double m_WorldRate = 0.0;\nstatic double m_CameraRate = 0.0;\n\nstatic int32_t M_GetFPS(void)\n{\n    return g_Config.rendering.fps;\n}\n\nstatic XYZ_32 M_GetItemMaxDelta(const ITEM *const item)\n{\n    int32_t max_xz = 128;\n    int32_t max_y = MAX(128, ABS(item->fall_speed) * 2);\n    switch (item->object_id) {\n    case O_BAT:\n        max_xz = 0;\n        max_y = 0;\n        break;\n\n    case O_DART:\n    case O_DISC:\n    case O_BOAT:\n    case O_RIB:\n    case O_SKIDOO_ARMED:\n    case O_SKIDOO_TRACK:\n    case O_SKIDOO_FAST:\n    case O_SKIDOO_DRIVER:\n    case O_GRENADE:\n        max_xz = 200;\n        break;\n\n    case O_QUAD_BIKE:\n    case O_KAYAK:\n    case O_UPV:\n        max_xz = 300;\n        break;\n\n    case O_MINE_CART:\n        max_xz = 512;\n        max_y = 200;\n        break;\n\n    case O_POISON_DART:\n    case O_ROCKET:\n    case O_HEAVY_ROCKET:\n        max_xz = 1000;\n        max_y = 200;\n        break;\n\n    case O_SOPHIA_LASER_BOLT:\n        max_xz = 1000;\n        max_y = 1000;\n        break;\n\n    case O_HARPOON_BOLT:\n        max_xz = 300;\n        max_y = 200;\n        break;\n\n    case O_LARA:\n    case O_LARA_EXTRA: {\n        const ITEM *const vehicle = Lara_Vehicle_GetItem();\n        if (vehicle == nullptr) {\n            break;\n        }\n        return M_GetItemMaxDelta(vehicle);\n    }\n\n    default:\n        break;\n    }\n    return (XYZ_32) { .x = max_xz, .y = max_y, .z = max_xz };\n}\n\nstatic XYZ_32 M_GetEffectMaxDelta(const EFFECT *const effect)\n{\n    int32_t max_xz = 128;\n    int32_t max_y = MAX(128, effect->fall_speed * 2);\n    switch (effect->object_id) {\n    case O_NATLA_GUN:\n    case O_MISSILE_ATLANTEAN_BOMB:\n        max_xz = 220;\n        break;\n    case O_MISSILE_ATLANTEAN_SHARD:\n        max_xz = 250;\n        break;\n    case O_MISSILE_FLAME:\n        max_xz = 200;\n        break;\n    case O_MISSILE_KNIFE:\n    case O_MISSILE_HARPOON:\n        max_xz = 150;\n        break;\n\n    default:\n        break;\n    }\n\n    return (XYZ_32) { .x = max_xz, .y = max_y, .z = max_xz };\n}\n\nstatic void M_RememberCamera(void)\n{\n    REMEMBER(&g_Camera, shift);\n    REMEMBER(&g_Camera, pos.x);\n    REMEMBER(&g_Camera, pos.y);\n    REMEMBER(&g_Camera, pos.z);\n    REMEMBER(&g_Camera, target.x);\n    REMEMBER(&g_Camera, target.y);\n    REMEMBER(&g_Camera, target.z);\n}\n\nstatic void M_CommitCamera(void)\n{\n    COMMIT(&g_Camera, shift);\n    COMMIT(&g_Camera, pos.x);\n    COMMIT(&g_Camera, pos.y);\n    COMMIT(&g_Camera, pos.z);\n    COMMIT(&g_Camera, target.x);\n    COMMIT(&g_Camera, target.y);\n    COMMIT(&g_Camera, target.z);\n}\n\nstatic void M_InterpolateCamera(const double ratio)\n{\n    INTERPOLATE_F(&g_Camera, shift, ratio);\n    INTERPOLATE_F(&g_Camera, pos.x, ratio);\n    INTERPOLATE_F(&g_Camera, pos.y, ratio);\n    INTERPOLATE_F(&g_Camera, pos.z, ratio);\n    INTERPOLATE_F(&g_Camera, target.x, ratio);\n    INTERPOLATE_F(&g_Camera, target.y, ratio);\n    INTERPOLATE_F(&g_Camera, target.z, ratio);\n}\n\nstatic void M_RememberLara(LARA_INFO *const lara)\n{\n    ASSERT(lara != nullptr);\n    REMEMBER(&lara->left_arm, rot.x);\n    REMEMBER(&lara->left_arm, rot.y);\n    REMEMBER(&lara->left_arm, rot.z);\n    REMEMBER(&lara->right_arm, rot.x);\n    REMEMBER(&lara->right_arm, rot.y);\n    REMEMBER(&lara->right_arm, rot.z);\n    REMEMBER(lara, torso_rot.x);\n    REMEMBER(lara, torso_rot.y);\n    REMEMBER(lara, torso_rot.z);\n    REMEMBER(lara, head_rot.x);\n    REMEMBER(lara, head_rot.y);\n    REMEMBER(lara, head_rot.z);\n}\n\nstatic void M_InterpolateLara(const double ratio, LARA_INFO *const lara)\n{\n    ASSERT(lara != nullptr);\n    INTERPOLATE_ROT(&lara->left_arm, rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(&lara->left_arm, rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(&lara->left_arm, rot.z, ratio, DEG_45);\n    INTERPOLATE_ROT(&lara->right_arm, rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(&lara->right_arm, rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(&lara->right_arm, rot.z, ratio, DEG_45);\n    INTERPOLATE_ROT(lara, torso_rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(lara, torso_rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(lara, torso_rot.z, ratio, DEG_45);\n    INTERPOLATE_ROT(lara, head_rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(lara, head_rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(lara, head_rot.z, ratio, DEG_45);\n}\n\nstatic void M_CommitLara(LARA_INFO *const lara)\n{\n    ASSERT(lara != nullptr);\n    COMMIT(&lara->left_arm, rot.x);\n    COMMIT(&lara->left_arm, rot.y);\n    COMMIT(&lara->left_arm, rot.z);\n    COMMIT(&lara->right_arm, rot.x);\n    COMMIT(&lara->right_arm, rot.y);\n    COMMIT(&lara->right_arm, rot.z);\n    COMMIT(lara, torso_rot.x);\n    COMMIT(lara, torso_rot.y);\n    COMMIT(lara, torso_rot.z);\n    COMMIT(lara, head_rot.x);\n    COMMIT(lara, head_rot.y);\n    COMMIT(lara, head_rot.z);\n}\n\nstatic void M_RememberBraidSegment(HAIR_SEGMENT *const segment)\n{\n    ASSERT(segment != nullptr);\n    REMEMBER(segment, pos.x);\n    REMEMBER(segment, pos.y);\n    REMEMBER(segment, pos.z);\n    REMEMBER(segment, rot.x);\n    REMEMBER(segment, rot.y);\n    REMEMBER(segment, rot.z);\n}\n\nstatic void M_InterpolateBraidSegment(\n    HAIR_SEGMENT *const segment, const double ratio, const XYZ_32 max_delta)\n{\n    ASSERT(segment != nullptr);\n    INTERPOLATE(segment, pos.x, ratio, max_delta.x);\n    INTERPOLATE(segment, pos.y, ratio, max_delta.y);\n    INTERPOLATE(segment, pos.z, ratio, max_delta.z);\n    INTERPOLATE_ROT(segment, rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(segment, rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(segment, rot.z, ratio, DEG_45);\n}\n\nstatic void M_CommitBraidSegment(HAIR_SEGMENT *const segment)\n{\n    ASSERT(segment != nullptr);\n    COMMIT(segment, pos.x);\n    COMMIT(segment, pos.y);\n    COMMIT(segment, pos.z);\n    COMMIT(segment, rot.x);\n    COMMIT(segment, rot.y);\n    COMMIT(segment, rot.z);\n}\n\nstatic void M_RememberBraid(void)\n{\n    for (int32_t i = 0; i < Lara_Hair_GetSegmentCount(); i++) {\n        M_RememberBraidSegment(Lara_Hair_GetSegment(i));\n    }\n}\n\nstatic void M_CommitBraid(void)\n{\n    for (int32_t i = 0; i < Lara_Hair_GetSegmentCount(); i++) {\n        M_CommitBraidSegment(Lara_Hair_GetSegment(i));\n    }\n}\n\nstatic void M_InterpolateBraid(const double ratio, ITEM *const lara_item)\n{\n    ASSERT(lara_item != nullptr);\n    const XYZ_32 max_delta = M_GetItemMaxDelta(lara_item);\n    for (int32_t i = 0; i < Lara_Hair_GetSegmentCount(); i++) {\n        M_InterpolateBraidSegment(Lara_Hair_GetSegment(i), ratio, max_delta);\n    }\n}\n\nstatic void M_RememberItem(ITEM *const item)\n{\n    REMEMBER(item, floor);\n    REMEMBER(item, pos.x);\n    REMEMBER(item, pos.y);\n    REMEMBER(item, pos.z);\n    REMEMBER(item, rot.x);\n    REMEMBER(item, rot.y);\n    REMEMBER(item, rot.z);\n    item->prev_frame_num = item->frame_num;\n}\n\nstatic void M_CommitItem(ITEM *const item)\n{\n    COMMIT(item, floor);\n    COMMIT(item, pos.x);\n    COMMIT(item, pos.y);\n    COMMIT(item, pos.z);\n    COMMIT(item, rot.x);\n    COMMIT(item, rot.y);\n    COMMIT(item, rot.z);\n}\n\nstatic void M_InterpolateItem(ITEM *const item, const double ratio)\n{\n    const XYZ_32 max_delta = M_GetItemMaxDelta(item);\n    INTERPOLATE(item, floor, ratio, max_delta.y);\n    INTERPOLATE(item, pos.x, ratio, max_delta.x);\n    INTERPOLATE(item, pos.y, ratio, max_delta.y);\n    INTERPOLATE(item, pos.z, ratio, max_delta.z);\n    INTERPOLATE_ROT(item, rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(item, rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(item, rot.z, ratio, DEG_45);\n}\n\nstatic void M_RememberItems(void)\n{\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        M_RememberItem(Item_Get(i));\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item != nullptr) {\n        M_RememberItem(lara_item);\n    }\n}\n\nstatic void M_InterpolateItems(const double ratio)\n{\n    const int16_t lara_vehicle_num = Lara_Vehicle_GetIndex();\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (((item->flags & IF_KILLED) || item->status == IS_INACTIVE)\n            && i != lara_vehicle_num) {\n            M_CommitItem(item);\n        } else {\n            M_InterpolateItem(item, ratio);\n        }\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item != nullptr) {\n        M_InterpolateItem(lara_item, ratio);\n    }\n}\n\nstatic void M_RememberEffect(EFFECT *const effect)\n{\n    ASSERT(effect != nullptr);\n    REMEMBER(effect, pos.x);\n    REMEMBER(effect, pos.y);\n    REMEMBER(effect, pos.z);\n    REMEMBER(effect, rot.x);\n    REMEMBER(effect, rot.y);\n    REMEMBER(effect, rot.z);\n}\n\nstatic void M_InterpolateEffect(const double ratio, EFFECT *const effect)\n{\n    ASSERT(effect != nullptr);\n    const XYZ_32 max_delta = M_GetEffectMaxDelta(effect);\n    INTERPOLATE(effect, pos.x, ratio, max_delta.x);\n    INTERPOLATE(effect, pos.y, ratio, max_delta.y);\n    INTERPOLATE(effect, pos.z, ratio, max_delta.z);\n    INTERPOLATE_ROT(effect, rot.x, ratio, DEG_45);\n    INTERPOLATE_ROT(effect, rot.y, ratio, DEG_45);\n    INTERPOLATE_ROT(effect, rot.z, ratio, DEG_45);\n}\n\nstatic void M_RememberEffects(void)\n{\n    int16_t effect_num = Effect_GetActiveNum();\n    while (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        M_RememberEffect(effect);\n        effect_num = effect->next_active;\n    }\n}\n\nstatic void M_InterpolateEffects(const double ratio)\n{\n    int16_t effect_num = Effect_GetActiveNum();\n    while (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        M_InterpolateEffect(ratio, effect);\n        effect_num = effect->next_active;\n    }\n}\n\nvoid Interpolation_Enable(void)\n{\n    m_IsEnabled = true;\n}\n\nvoid Interpolation_Disable(void)\n{\n    m_IsEnabled = false;\n}\n\nbool Interpolation_IsEnabled(void)\n{\n    return m_IsEnabled;\n}\n\nbool Interpolation_IsActive(void)\n{\n    return m_IsEnabled && M_GetFPS() == 60 && !Shell_IsExiting();\n}\n\ndouble Interpolation_GetWorldRate(void)\n{\n    if (!Interpolation_IsActive()) {\n        return 1.0;\n    }\n    return m_WorldRate;\n}\n\ndouble Interpolation_GetCameraRate(void)\n{\n    if (!Interpolation_IsActive()) {\n        return 1.0;\n    }\n    return m_CameraRate;\n}\n\ndouble Interpolation_GetRate(void)\n{\n    return m_Rate;\n}\n\nvoid Interpolation_SetRate(double rate)\n{\n    m_Rate = rate;\n}\n\nvoid Interpolation_Remember(void)\n{\n    if (g_Camera.pos.room_num != NO_ROOM) {\n        M_RememberCamera();\n    }\n\n    if (g_Camera.type == CAM_PHOTO_MODE) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara != nullptr) {\n        M_RememberLara(lara);\n    }\n\n    if (Lara_Hair_IsActive()) {\n        M_RememberBraid();\n    }\n    M_RememberItems();\n    M_RememberEffects();\n}\n\nvoid Interpolation_Interpolate(void)\n{\n    if (g_Camera.type != CAM_PHOTO_MODE) {\n        m_WorldRate = m_Rate;\n    }\n    m_CameraRate = m_Rate;\n\n    if (g_Camera.pos.room_num != NO_ROOM) {\n        if (DIFF(&g_Camera, shift) >= 128\n            || DIFF(&g_Camera, pos.x) >= CAM_MAX_DELTA\n            || DIFF(&g_Camera, pos.y) >= CAM_MAX_DELTA\n            || DIFF(&g_Camera, pos.z) >= CAM_MAX_DELTA\n            || DIFF(&g_Camera, target.x) >= CAM_MAX_DELTA\n            || DIFF(&g_Camera, target.y) >= CAM_MAX_DELTA\n            || DIFF(&g_Camera, target.z) >= CAM_MAX_DELTA) {\n            M_CommitCamera();\n        } else {\n            M_InterpolateCamera(m_CameraRate);\n        }\n\n        g_Camera.interp.room_num = g_Camera.pos.room_num;\n        Camera_ClampInterpResult();\n    }\n\n    if (g_Camera.type == CAM_PHOTO_MODE) {\n        return;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (lara != nullptr) {\n        M_InterpolateLara(m_WorldRate, lara);\n    }\n    M_InterpolateItems(m_WorldRate);\n    M_InterpolateEffects(m_WorldRate);\n    if (lara_item != nullptr && Lara_Hair_IsActive()) {\n        M_InterpolateBraid(m_WorldRate, lara_item);\n    }\n}\n\nvoid Interpolation_CommitLara(void)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item != nullptr) {\n        M_CommitItem(lara_item);\n    }\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara != nullptr) {\n        M_CommitLara(lara);\n    }\n    M_CommitBraid();\n}\n\nvoid Interpolation_CommitBraid(void)\n{\n    M_CommitBraid();\n}\n"
  },
  {
    "path": "src/trx/game/interpolation.h",
    "content": "#pragma once\n\nvoid Interpolation_Enable(void);\nvoid Interpolation_Disable(void);\nbool Interpolation_IsEnabled(void);\nbool Interpolation_IsActive(void);\n\ndouble Interpolation_GetWorldRate(void);\ndouble Interpolation_GetCameraRate(void);\ndouble Interpolation_GetRate(void);\nvoid Interpolation_SetRate(double rate);\n\nvoid Interpolation_Interpolate(void);\nvoid Interpolation_Remember(void);\n\n// Instantly discard interpolation data\nvoid Interpolation_CommitLara(void);\nvoid Interpolation_CommitBraid(void);\n"
  },
  {
    "path": "src/trx/game/inventory.c",
    "content": "#include <trx/game/inventory.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/stats.h>\n\nINVENTORY_MODE g_Inv_Mode = INV_TITLE_MODE;\n\nstatic int32_t M_GetFlareQuantity(void)\n{\n    return Game_IsBonusFlagSet(GBF_JAPANESE)\n        ? g_Weapons[LGT_FLARE].ammo.pickup_qty_alt\n        : g_Weapons[LGT_FLARE].ammo.pickup_qty;\n}\n\nstatic INVENTORY_ITEM *M_GetGunInvItem(const LARA_GUN_TYPE gun_type)\n{\n    // clang-format off\n    switch (gun_type) {\n    case LGT_PISTOLS:      return InvRing_GetByObjectID(O_PISTOL_OPTION);\n    case LGT_SHOTGUN:      return InvRing_GetByObjectID(O_SHOTGUN_OPTION);\n    case LGT_MAGNUMS:      return InvRing_GetByObjectID(O_MAGNUM_OPTION);\n    case LGT_AUTOS:        return InvRing_GetByObjectID(O_AUTOS_OPTION);\n    case LGT_DESERT_EAGLE: return InvRing_GetByObjectID(O_DESERT_EAGLE_OPTION);\n    case LGT_UZIS:         return InvRing_GetByObjectID(O_UZI_OPTION);\n    case LGT_HARPOON:      return InvRing_GetByObjectID(O_HARPOON_OPTION);\n    case LGT_M16:          return InvRing_GetByObjectID(O_M16_OPTION);\n    case LGT_MP5:          return InvRing_GetByObjectID(O_MP5_OPTION);\n    case LGT_GRENADE:      return InvRing_GetByObjectID(O_GRENADE_GUN_OPTION);\n    case LGT_ROCKET:       return InvRing_GetByObjectID(O_ROCKET_GUN_OPTION);\n    default:               return nullptr;\n    }\n    // clang-format on\n}\n\nstatic INVENTORY_ITEM *M_GetAmmoInvItem(const LARA_GUN_TYPE gun_type)\n{\n    // clang-format off\n    switch (gun_type) {\n    case LGT_PISTOLS:      return InvRing_GetByObjectID(O_PISTOL_AMMO_OPTION);\n    case LGT_SHOTGUN:      return InvRing_GetByObjectID(O_SHOTGUN_AMMO_OPTION);\n    case LGT_MAGNUMS:      return InvRing_GetByObjectID(O_MAGNUM_AMMO_OPTION);\n    case LGT_AUTOS:        return InvRing_GetByObjectID(O_AUTOS_AMMO_OPTION);\n    case LGT_DESERT_EAGLE: return InvRing_GetByObjectID(O_DESERT_EAGLE_AMMO_OPTION);\n    case LGT_UZIS:         return InvRing_GetByObjectID(O_UZI_AMMO_OPTION);\n    case LGT_HARPOON:      return InvRing_GetByObjectID(O_HARPOON_AMMO_OPTION);\n    case LGT_M16:          return InvRing_GetByObjectID(O_M16_AMMO_OPTION);\n    case LGT_MP5:          return InvRing_GetByObjectID(O_MP5_AMMO_OPTION);\n    case LGT_GRENADE:      return InvRing_GetByObjectID(O_GRENADE_AMMO_OPTION);\n    case LGT_ROCKET:       return InvRing_GetByObjectID(O_ROCKET_AMMO_OPTION);\n    default:               return nullptr;\n    }\n    // clang-format on\n}\n\nstatic void M_IncreaseAmmo(const LARA_GUN_TYPE gun_type, const int32_t qty)\n{\n    AMMO_INFO *const ammo = Gun_GetAmmoInfo(gun_type);\n    ammo->ammo += qty;\n    CLAMPG(ammo->ammo, MAX_QTY);\n}\n\nstatic RING_TYPE M_GetRingType(const INVENTORY_ITEM *const inv_item)\n{\n    if (inv_item->inv_pos < 100) {\n        return RT_MAIN;\n    } else if (inv_item->inv_pos < 200) {\n        return RT_KEYS;\n    } else if (inv_item->inv_pos < 300) {\n        return RT_OPTION;\n    } else {\n        return RT_GLOBE_SELECT;\n    }\n}\n\nstatic void M_AddGun(const LARA_GUN_TYPE gun_type)\n{\n    const OBJECT_ID gun_object = Gun_GetGunObject(gun_type);\n    const OBJECT_ID ammo_object = Gun_GetAmmoObject(gun_type);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    for (int32_t i = Inv_RequestItem(ammo_object); i > 0; i--) {\n        Inv_RemoveItem(ammo_object);\n        M_IncreaseAmmo(gun_type, Gun_GetAmmoPickupQuantity(gun_type));\n    }\n    M_IncreaseAmmo(gun_type, Gun_GetAmmoInitialQuantity(gun_type));\n    Inv_InsertItem(M_GetGunInvItem(gun_type));\n    if (lara->last_gun_type == LGT_UNARMED) {\n        lara->last_gun_type = gun_type;\n    }\n    Item_GlobalReplace(gun_object, ammo_object);\n}\n\nstatic void M_AddAmmo(const LARA_GUN_TYPE gun_type)\n{\n    const OBJECT_ID gun_object = Gun_GetGunObject(gun_type);\n    if (Inv_RequestItem(gun_object)) {\n        M_IncreaseAmmo(gun_type, Gun_GetAmmoPickupQuantity(gun_type));\n    } else {\n        Inv_InsertItem(M_GetAmmoInvItem(gun_type));\n    }\n}\n\nbool Inv_AddItemNTimes(const OBJECT_ID object_id, const int32_t qty)\n{\n    bool result = false;\n    for (int32_t i = 0; i < qty; i++) {\n        result |= Inv_AddItem(object_id);\n    }\n    return result;\n}\n\nOBJECT_ID Inv_GetItemOption(const OBJECT_ID object_id)\n{\n    if (Object_IsType(object_id, g_InvObjects)) {\n        return object_id;\n    }\n    return Object_GetCognate(object_id, g_ItemToInvObjectMap);\n}\n\nOBJECT_ID Inv_GetItemPickup(const OBJECT_ID object_id)\n{\n    if (Object_IsType(object_id, g_InvObjects)) {\n        return Object_GetCognateInverse(object_id, g_ItemToInvObjectMap);\n    }\n    return object_id;\n}\n\nvoid Inv_InsertItem(INVENTORY_ITEM *const inv_item)\n{\n    Inv_InsertItemEx(inv_item, 1);\n}\n\nvoid Inv_InsertItemEx(INVENTORY_ITEM *const inv_item, const int32_t qty)\n{\n    ASSERT(inv_item != nullptr);\n    INV_RING_SOURCE *const source = &g_InvRing_Source[M_GetRingType(inv_item)];\n\n    int32_t n;\n    for (n = 0; n < source->count; n++) {\n        if (source->items[n]->inv_pos > inv_item->inv_pos) {\n            break;\n        }\n    }\n\n    for (int32_t i = source->count; i > n - 1; i--) {\n        source->items[i + 1] = source->items[i];\n        source->qtys[i + 1] = source->qtys[i];\n    }\n    source->items[n] = inv_item;\n    source->qtys[n] = MIN(qty, MAX_QTY);\n    source->count++;\n}\n\nbool Inv_RemoveItem(const OBJECT_ID object_id)\n{\n    const OBJECT_ID inv_object_id = Inv_GetItemOption(object_id);\n    for (RING_TYPE ring_type = 0; ring_type < RT_NUMBER_OF; ring_type++) {\n        INV_RING_SOURCE *const source = &g_InvRing_Source[ring_type];\n        for (int32_t i = 0; i < source->count; i++) {\n            if (source->items[i]->object_id != inv_object_id) {\n                continue;\n            }\n\n            source->qtys[i]--;\n\n            if (g_Config.gameplay.fix_item_duplication_glitch) {\n                for (int32_t j = i; j < source->count; j++) {\n                    if (j == source->current) {\n                        source->current = 0;\n                    }\n                }\n            }\n\n            if (source->qtys[i] == 0) {\n                source->count--;\n                for (int32_t j = i; j < source->count; j++) {\n                    source->items[j] = source->items[j + 1];\n                    source->qtys[j] = source->qtys[j + 1];\n                }\n            }\n            return true;\n        }\n    }\n    return false;\n}\n\nint32_t Inv_RequestItem(const OBJECT_ID object_id)\n{\n    const OBJECT_ID inv_object_id = Inv_GetItemOption(object_id);\n    for (RING_TYPE ring_type = 0; ring_type < RT_NUMBER_OF; ring_type++) {\n        INV_RING_SOURCE *const source = &g_InvRing_Source[ring_type];\n        for (int32_t i = 0; i < source->count; i++) {\n            if (source->items[i] != nullptr\n                && source->items[i]->object_id == inv_object_id) {\n                return source->qtys[i];\n            }\n        }\n    }\n    return 0;\n}\n\nvoid Inv_ClearSelection(void)\n{\n    g_InvRing_Source[RT_MAIN].current = 0;\n    g_InvRing_Source[RT_KEYS].current = 0;\n}\n\nvoid Inv_RemoveAllItems(void)\n{\n    g_InvRing_Source[RT_MAIN].count = 0;\n    g_InvRing_Source[RT_KEYS].count = 0;\n    g_InvRing_Source[RT_GLOBE_SELECT].count = 0;\n\n    // Reset main ring\n    Inv_AddItem(O_STOPWATCH_OPTION);\n    Inv_AddItem(O_COMPASS_OPTION);\n    Inv_AddItem(O_GLOBE_SELECT_OPTION);\n\n    Inv_ClearSelection();\n}\n\nbool Inv_AddItem(const OBJECT_ID object_id)\n{\n    const OBJECT_ID inv_object_id = Inv_GetItemOption(object_id);\n    const OBJECT_ID pickup_object_id = Inv_GetItemPickup(object_id);\n    const OBJECT *const object =\n        Object_Get(inv_object_id == NO_OBJECT ? object_id : inv_object_id);\n    if (!object->loaded) {\n        return false;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (Object_IsType(pickup_object_id, g_GunObjects)) {\n        Gun_UpdateLaraMeshes(pickup_object_id);\n        if (lara->gun_type == LGT_UNARMED) {\n            lara->gun_type = Gun_GetType(pickup_object_id);\n            const bool hands_busy = lara->gun_status == LGS_HANDS_BUSY;\n            lara->gun_status = LGS_ARMLESS;\n            Gun_InitialiseNewWeapon();\n            if (hands_busy) {\n                lara->gun_status = LGS_HANDS_BUSY;\n            }\n        }\n    }\n\n    const int32_t qty = object_id == O_FLAREBOX_ITEM ? M_GetFlareQuantity() : 1;\n    for (RING_TYPE ring_type = 0; ring_type < RT_NUMBER_OF; ring_type++) {\n        INV_RING_SOURCE *const source = &g_InvRing_Source[ring_type];\n        for (int32_t i = 0; i < source->count; i++) {\n            if (source->items[i]->object_id == inv_object_id) {\n                source->qtys[i] += qty;\n                CLAMPG(source->qtys[i], MAX_QTY);\n                return true;\n            }\n        }\n    }\n\n    // Pistols\n    if (inv_object_id == O_PISTOL_OPTION) {\n        Inv_InsertItem(InvRing_GetByObjectID(O_PISTOL_OPTION));\n        if (lara->last_gun_type == LGT_UNARMED) {\n            lara->last_gun_type = LGT_PISTOLS;\n        }\n        return true;\n    }\n\n    // Other guns\n    if (Object_IsType(pickup_object_id, g_GunObjects)) {\n        M_AddGun(Gun_GetType(pickup_object_id));\n        return true;\n    }\n    if (Object_IsType(pickup_object_id, g_GunAmmoObjects)) {\n        M_AddAmmo(Gun_GetType(\n            Object_GetCognateInverse(pickup_object_id, g_GunAmmoObjectMap)));\n        return true;\n    }\n\n    // Other cases\n    for (int32_t i = 0; i < g_InvRing_Items->count; i++) {\n        INVENTORY_ITEM *const inv_item =\n            *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i);\n        if (inv_item->object_id == object_id\n            || inv_item->object_id == inv_object_id) {\n            Inv_InsertItemEx(inv_item, qty);\n            return true;\n        }\n    }\n    return false;\n}\n\nbool Inv_AddPickup(const ITEM *const item)\n{\n    if (Object_IsType(item->object_id, g_SecretObjects)) {\n        Stats_MarkSecretCollected(item);\n        if (Stats_CheckAllLevelSecretsPickedUp()) {\n            GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_SECRET);\n        }\n        return true;\n    }\n\n    return Inv_AddItem(item->object_id);\n}\n"
  },
  {
    "path": "src/trx/game/inventory.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/lara/types.h>\n#include <trx/game/objects/ids.h>\n\n#include <stdint.h>\n\nextern INVENTORY_MODE g_Inv_Mode;\n\nbool Inv_AddItemNTimes(OBJECT_ID obj_id, int32_t qty);\nOBJECT_ID Inv_GetItemOption(OBJECT_ID obj_id);\nOBJECT_ID Inv_GetItemPickup(OBJECT_ID obj_id);\nvoid Inv_InsertItem(INVENTORY_ITEM *inv_item);\nvoid Inv_InsertItemEx(INVENTORY_ITEM *inv_item, int32_t qty);\nbool Inv_RemoveItem(OBJECT_ID obj_id);\nint32_t Inv_RequestItem(OBJECT_ID obj_id);\nvoid Inv_ClearSelection(void);\nvoid Inv_RemoveAllItems(void);\n\nbool Inv_AddItem(OBJECT_ID obj_id);\nbool Inv_AddPickup(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/inventory_ring/control.c",
    "content": "#include <trx/game/inventory_ring/control.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/game/camera.h>\n#include <trx/game/console.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gun.h>\n#include <trx/game/gym.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/inventory.h>\n#include <trx/game/inventory_ring/priv.h>\n#include <trx/game/inventory_ring/vars.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/option.h>\n#include <trx/game/option/examine.h>\n#include <trx/game/option/globe_select.h>\n#include <trx/game/option/passport.h>\n#include <trx/game/option/stats.h>\n#include <trx/game/output/overlay.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_INV_RING_FADE_TIME_FAST                                              \\\n    (INV_RING_CLOSE_FRAMES / INV_RING_FRAMES / (float)LOGIC_FPS)\n#define M_INV_RING_FADE_TIME_TO_BLACK 0.25f\n#define M_RING_SWITCH_FRAMES (96 / 2)\n#define M_SELECTING_FRAMES (32 / 2)\n\nstatic CLOCK_TIMER m_DemoTimer = { .type = CLOCK_TIMER_SIM };\nstatic int32_t m_StartLevel;\nstatic OBJECT_ID m_InvChosen = NO_OBJECT;\nstatic INV_RING *m_ActiveRing = nullptr;\n\nINV_RING *InvRing_GetActiveRing(void)\n{\n    return m_ActiveRing;\n}\n\nstatic void M_ShowAmmoQuantity(const char *const fmt, const int32_t qty)\n{\n    if (!Game_IsBonusFlagSet(GBF_NGPLUS)) {\n        InvRing_ShowItemQuantity(fmt, qty);\n    }\n}\n\nstatic void M_RingIsOpen(INV_RING *const ring)\n{\n    InvRing_ShowHeader(ring);\n}\n\nstatic void M_RingIsNotOpen(INV_RING *const ring)\n{\n    InvRing_RemoveHeader();\n    InvRing_ShowExamine(NO_OBJECT, false);\n}\n\nstatic void M_RingNotActive(\n    const INV_RING *const ring, const INVENTORY_ITEM *const inv_item)\n{\n    InvRing_ShowItemName(inv_item);\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const int32_t qty = Inv_RequestItem(inv_item->object_id);\n\n    switch (inv_item->object_id) {\n    case O_SHOTGUN_OPTION:\n        M_ShowAmmoQuantity(\n            g_TRVersion == 1 ? \"%5d \\\\{ammo shotgun}\" : \"%5d\",\n            lara->shotgun_ammo.ammo / Gun_GetAmmoClipCount(LGT_SHOTGUN));\n        break;\n    case O_MAGNUM_OPTION:\n        M_ShowAmmoQuantity(\n            g_TRVersion == 1 ? \"%5d \\\\{ammo magnums}\" : \"%5d\",\n            lara->magnum_ammo.ammo);\n        break;\n    case O_AUTOS_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->autos_ammo.ammo);\n        break;\n    case O_DESERT_EAGLE_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->desert_eagle_ammo.ammo);\n        break;\n    case O_UZI_OPTION:\n        M_ShowAmmoQuantity(\n            g_TRVersion == 1 ? \"%5d \\\\{ammo uzis}\" : \"%5d\",\n            lara->uzi_ammo.ammo);\n        break;\n    case O_HARPOON_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->harpoon_ammo.ammo);\n        break;\n    case O_M16_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->m16_ammo.ammo);\n        break;\n    case O_MP5_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->mp5_ammo.ammo);\n        break;\n    case O_GRENADE_GUN_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->grenade_ammo.ammo);\n        break;\n    case O_ROCKET_GUN_OPTION:\n        M_ShowAmmoQuantity(\"%5d\", lara->rocket_ammo.ammo);\n        break;\n\n    case O_SHOTGUN_AMMO_OPTION:\n        M_ShowAmmoQuantity(\"%d\", qty * Gun_GetAmmoShellCount(LGT_SHOTGUN));\n        break;\n    case O_MAGNUM_AMMO_OPTION:\n    case O_AUTOS_AMMO_OPTION:\n    case O_DESERT_EAGLE_AMMO_OPTION:\n    case O_UZI_AMMO_OPTION:\n    case O_HARPOON_AMMO_OPTION:\n    case O_M16_AMMO_OPTION:\n    case O_MP5_AMMO_OPTION:\n        M_ShowAmmoQuantity(\"%d\", qty * 2);\n        break;\n    case O_GRENADE_AMMO_OPTION:\n    case O_ROCKET_AMMO_OPTION:\n    case O_FLAREBOX_OPTION:\n        M_ShowAmmoQuantity(\"%d\", qty);\n        break;\n\n    case O_SMALL_MEDIPACK_OPTION:\n    case O_LARGE_MEDIPACK_OPTION:\n        Overlay_ForceHealthBar(true);\n        if (qty > 1) {\n            InvRing_ShowItemQuantity(\"%d\", qty);\n        }\n        break;\n\n    case O_PUZZLE_OPTION_1:\n    case O_PUZZLE_OPTION_2:\n    case O_PUZZLE_OPTION_3:\n    case O_PUZZLE_OPTION_4:\n    case O_KEY_OPTION_1:\n    case O_KEY_OPTION_2:\n    case O_KEY_OPTION_3:\n    case O_KEY_OPTION_4:\n    case O_QUEST_OPTION_1:\n    case O_QUEST_OPTION_2:\n    case O_QUEST_OPTION_3:\n    case O_QUEST_OPTION_4:\n    case O_PICKUP_OPTION_1:\n    case O_PICKUP_OPTION_2:\n    case O_LEADBAR_OPTION:\n    case O_SCION_OPTION:\n        if (qty > 1) {\n            InvRing_ShowItemQuantity(\"%d\", qty);\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    InvRing_ShowExamine(\n        inv_item->object_id,\n        ring->status == RNG_OPEN\n            && Option_Examine_CanExamine(inv_item->object_id));\n}\n\nstatic void M_RingActive(void)\n{\n    InvRing_RemoveItemTexts();\n    InvRing_ShowExamine(NO_OBJECT, false);\n}\n\nstatic bool M_AnimateInventoryItem(INVENTORY_ITEM *const inv_item)\n{\n    if (inv_item->current_frame == inv_item->goal_frame) {\n        InvRing_SelectMeshes(inv_item);\n        return false;\n    }\n\n    if (inv_item->anim_count > 0) {\n        inv_item->anim_count--;\n    } else {\n        inv_item->anim_count = inv_item->anim_speed;\n        inv_item->current_frame += inv_item->anim_direction;\n        if (inv_item->current_frame >= inv_item->frames_total) {\n            inv_item->current_frame = 0;\n        } else if (inv_item->current_frame < 0) {\n            inv_item->current_frame = inv_item->frames_total - 1;\n        }\n    }\n\n    InvRing_SelectMeshes(inv_item);\n    return true;\n}\n\nstatic GF_COMMAND M_Finish(INV_RING *const ring, const bool apply_changes)\n{\n    // TODO: Make this function not have any side effects.\n    // Consider adding new GF_ constants, but research other solutions first.\n\n    if (ring->mode == INV_GLOBE_SELECT_MODE) {\n        if (ring->globe_select.confirmed && ring->globe_select.selection >= 0\n            && ring->globe_select.selection < MAX_GLOBE_ZONES) {\n            const int32_t start_level_num =\n                ring->globe_select\n                    .start_level_num[ring->globe_select.selection];\n            if (start_level_num >= 0) {\n                return (GF_COMMAND) {\n                    .action = GF_START_GAME,\n                    .param = start_level_num,\n                };\n            }\n        }\n        return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n    }\n\n    if (m_StartLevel != -1) {\n        return (GF_COMMAND) {\n            .action = GF_SELECT_GAME,\n            .param = m_StartLevel,\n        };\n    }\n\n    if (Shell_IsExiting()) {\n        return (GF_COMMAND) { .action = GF_EXIT_GAME };\n    } else if (GF_GetOverrideCommand().action != GF_NOOP) {\n        return GF_GetOverrideCommand();\n    } else if (ring->is_demo_needed) {\n        return (GF_COMMAND) { .action = GF_START_DEMO, .param = -1 };\n    }\n\n    switch (m_InvChosen) {\n    case O_PASSPORT_OPTION:\n        switch (g_Passport.select_action) {\n        case PASSPORT_ACTION_LOAD_GAME: {\n            if (apply_changes) {\n                Inv_RemoveAllItems();\n            }\n            return (GF_COMMAND) {\n                .action = GF_START_SAVED_GAME,\n                .param = Savegame_SlotToParam(g_Passport.select_save_slot),\n            };\n        }\n\n        case PASSPORT_ACTION_NEW_GAME:\n            if (apply_changes) {\n                Savegame_InitCurrentInfo();\n            }\n            Savegame_UnbindSlot();\n            return (GF_COMMAND) {\n                .action = GF_START_GAME,\n                .param = g_Passport.select_level,\n            };\n\n        case PASSPORT_ACTION_SWITCH_MOD:\n            return (GF_COMMAND) { .action = GF_SWITCH_MOD };\n\n        case PASSPORT_ACTION_SAVE_GAME: {\n            if (apply_changes) {\n                Savegame_Save(g_Passport.select_save_slot);\n            }\n            return (GF_COMMAND) { .action = GF_NOOP };\n        }\n\n        case PASSPORT_ACTION_RESTART:\n            return (GF_COMMAND) {\n                .action = GF_RESTART_GAME,\n                .param = Game_GetCurrentLevel()->num,\n            };\n\n        case PASSPORT_ACTION_EXIT_TO_TITLE:\n            return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n\n        case PASSPORT_ACTION_EXIT_GAME:\n            return (GF_COMMAND) { .action = GF_EXIT_GAME };\n\n        case PASSPORT_ACTION_SELECT_LEVEL:\n            return (GF_COMMAND) {\n                .action = GF_SELECT_GAME,\n                .param = g_Passport.select_level,\n            };\n\n        case PASSPORT_ACTION_GLOBE_SELECT:\n            return (GF_COMMAND) {\n                .action = GF_GLOBE_SELECT,\n                .param = g_Passport.select_level,\n            };\n\n        case PASSPORT_ACTION_STORY_SO_FAR:\n            return (GF_COMMAND) {\n                .action = GF_STORY_SO_FAR,\n                .param = Savegame_SlotToParam(g_Passport.select_save_slot),\n            };\n        }\n        break;\n\n    case O_PHOTO_OPTION:\n        if (apply_changes) {\n            Savegame_UnbindSlot();\n        }\n        if (GF_GetGymLevel() != nullptr) {\n            return (GF_COMMAND) {\n                .action = GF_START_GAME,\n                .param = GF_GetGymLevel()->num,\n            };\n        }\n        break;\n\n    case O_PISTOL_OPTION:\n    case O_SHOTGUN_OPTION:\n    case O_MAGNUM_OPTION:\n    case O_AUTOS_OPTION:\n    case O_DESERT_EAGLE_OPTION:\n    case O_UZI_OPTION:\n    case O_HARPOON_OPTION:\n    case O_M16_OPTION:\n    case O_MP5_OPTION:\n    case O_GRENADE_GUN_OPTION:\n    case O_ROCKET_GUN_OPTION:\n    case O_SMALL_MEDIPACK_OPTION:\n    case O_LARGE_MEDIPACK_OPTION:\n    case O_FLAREBOX_OPTION:\n    case O_KEY_OPTION_1:\n    case O_KEY_OPTION_2:\n    case O_KEY_OPTION_3:\n    case O_KEY_OPTION_4:\n    case O_PUZZLE_OPTION_1:\n    case O_PUZZLE_OPTION_2:\n    case O_PUZZLE_OPTION_3:\n    case O_PUZZLE_OPTION_4:\n    case O_LEADBAR_OPTION:\n    case O_SCION_OPTION:\n        if (apply_changes) {\n            Lara_UseItem(m_InvChosen);\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nstatic bool M_CheckDemoTimer(const INV_RING *const ring)\n{\n    if (!g_Config.gameplay.enable_demo\n        || GF_GetLevelTable(GFLT_DEMOS)->count == 0) {\n        return false;\n    }\n\n    if (ring->mode != INV_TITLE_MODE || InputState_IsAnyPressed(g_Input)\n        || InputState_IsAnyPressed(g_InputDB) || Console_IsOpened()) {\n        ClockTimer_Sync(&m_DemoTimer);\n        return false;\n    }\n\n    return ring->status == RNG_OPEN\n        && ClockTimer_CheckElapsed(&m_DemoTimer, g_Config.flow.demo_delay);\n}\n\nstatic void M_SetupRingSwitchClose(\n    INV_RING *const ring, const RING_STATUS status_target)\n{\n    InvRing_SetStatusTransition(\n        ring, RNG_CLOSING, status_target, M_RING_SWITCH_FRAMES / 2);\n}\n\nstatic void M_TransitionToRing(\n    INV_RING *const ring, const RING_TYPE source_type,\n    const RING_TYPE target_type)\n{\n    g_InvRing_Source[source_type].current = ring->current_object;\n    g_InvRing_Source[source_type].count = ring->number_of_objects;\n    ring->type = target_type;\n    ring->list = g_InvRing_Source[target_type].items;\n    ring->number_of_objects = g_InvRing_Source[target_type].count;\n    ring->current_object = g_InvRing_Source[target_type].current;\n    InvRing_SetStatusTransition(\n        ring, RNG_OPENING, RNG_OPEN, M_RING_SWITCH_FRAMES / 2);\n}\n\nstatic void M_SnapshotRingState(INV_RING *const ring)\n{\n    ring->prev_radius = ring->radius;\n    ring->prev_camera_y = ring->camera.pos.y;\n    ring->prev_camera_pitch = ring->camera_pitch;\n    ring->prev_ring_rot_y = ring->ring_pos.rot.y;\n}\n\nstatic void M_SnapshotItemState(INVENTORY_ITEM *const inv_item)\n{\n    inv_item->prev_x_rot_pt = inv_item->x_rot_pt;\n    inv_item->prev_x_rot = inv_item->x_rot;\n    inv_item->prev_y_rot = inv_item->y_rot;\n    inv_item->prev_y_trans = inv_item->y_trans;\n    inv_item->prev_z_trans = inv_item->z_trans;\n    inv_item->prev_manual_rot = inv_item->manual_rot;\n}\n\nstatic void M_SnapshotFrameState(INV_RING *const ring)\n{\n    M_SnapshotRingState(ring);\n    for (int32_t i = 0; i < ring->number_of_objects; i++) {\n        M_SnapshotItemState(ring->list[i]);\n    }\n}\n\nstatic GF_COMMAND M_Control(INV_RING *const ring)\n{\n    if (ring->status == RNG_OPENING) {\n        if (ring->mode == INV_TITLE_MODE\n            && (Fader_IsActive(&ring->top_fader)\n                || Fader_IsActive(&ring->back_fader))) {\n            return (GF_COMMAND) { .action = GF_NOOP };\n        }\n\n        ClockTimer_Sync(&m_DemoTimer);\n        if (!ring->has_spun_out) {\n            Sound_Effect(SFX_MENU_SPININ, nullptr, SPM_ALWAYS);\n            ring->has_spun_out = true;\n        }\n    }\n\n    if (ring->status == RNG_FADING_OUT) {\n        if (ring->mode == INV_TITLE_MODE) {\n            const GF_COMMAND gf_cmd = M_Finish(ring, true);\n            ring->is_done = true;\n            ring->status = RNG_DONE;\n            return gf_cmd;\n        }\n\n        if (!Fader_IsActive(&ring->back_fader)\n            && !Fader_IsActive(&ring->top_fader)) {\n            Fader_InitFromCurrentHold(\n                &ring->top_fader, 1.0f, M_INV_RING_FADE_TIME_TO_BLACK,\n                1.0f / (float)LOGIC_FPS);\n        }\n\n        if (Fader_IsActive(&ring->top_fader)\n            || Fader_IsActive(&ring->back_fader)) {\n            return (GF_COMMAND) { .action = GF_NOOP };\n        }\n        ring->status = RNG_DONE;\n    }\n\n    if (ring->status == RNG_DONE && !ring->is_done) {\n        const GF_COMMAND gf_cmd = M_Finish(ring, true);\n        ring->is_done = true;\n        // Returning to game – resume music\n        if (gf_cmd.action == GF_NOOP) {\n            Music_Unpause();\n            Sound_UnpauseAll();\n        }\n        return gf_cmd;\n    }\n\n    InvRing_CalcAdders(ring, INV_RING_ROTATE_DURATION);\n\n    Input_Update();\n    // Do the demo inactivity check prior to postprocessing of the inputs.\n    if (M_CheckDemoTimer(ring)) {\n        ring->is_demo_needed = true;\n    }\n    Shell_ProcessInput();\n    Game_ProcessInput();\n\n    if (ring->mode == INV_GLOBE_SELECT_MODE) {\n        m_StartLevel = -1;\n    } else {\n        m_StartLevel = Game_IsLevelComplete() ? g_Passport.select_level : -1;\n    }\n\n    if (g_Config.gameplay.enable_timer_in_inventory\n        && !(Game_IsInGym() && Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT))) {\n        Stats_UpdateTimer();\n    }\n\n    if (Shell_IsExiting()) {\n        return (GF_COMMAND) { .action = GF_EXIT_GAME };\n    }\n\n    if ((ring->mode == INV_SAVE_MODE || ring->mode == INV_SAVE_CRYSTAL_MODE\n         || ring->mode == INV_LOAD_MODE || ring->mode == INV_DEATH_MODE\n         || ring->mode == INV_GLOBE_SELECT_MODE)\n        && !ring->is_pass_open) {\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) { .menu_confirm = 1 };\n    }\n\n    if (ring->mode != INV_TITLE_MODE && !Fader_IsActive(&ring->back_fader)\n        && !Fader_IsActive(&ring->top_fader) && ring->status != RNG_OPENING) {\n        for (int i = 0; i < ring->number_of_objects; i++) {\n            INVENTORY_ITEM *const inv_item = ring->list[i];\n            if (inv_item->object_id == O_COMPASS_OPTION) {\n                Option_Stats_UpdateCompassNeedle(inv_item);\n            }\n        }\n    }\n\n    if (ring->rotating) {\n        return (GF_COMMAND) { .action = GF_NOOP };\n    }\n\n    switch (ring->status) {\n    case RNG_OPEN:\n        if (g_Input.menu_right && ring->number_of_objects > 1) {\n            InvRing_RotateLeft(ring);\n            Sound_Effect(SFX_MENU_ROTATE, nullptr, SPM_ALWAYS);\n            break;\n        }\n\n        if (g_Input.menu_left && ring->number_of_objects > 1) {\n            InvRing_RotateRight(ring);\n            Sound_Effect(SFX_MENU_ROTATE, nullptr, SPM_ALWAYS);\n            break;\n        }\n\n        if (m_StartLevel != -1 || ring->is_demo_needed\n            || (g_InputDB.menu_back && ring->mode != INV_TITLE_MODE\n                && ring->mode != INV_GLOBE_SELECT_MODE)) {\n            Sound_Effect(SFX_MENU_SPINOUT, nullptr, SPM_ALWAYS);\n            m_InvChosen = NO_OBJECT;\n\n            if (ring->type == RT_MAIN) {\n                g_InvRing_Source[RT_MAIN].current = ring->current_object;\n            } else if (ring->type != RT_NUMBER_OF) {\n                g_InvRing_Source[ring->type].current = ring->current_object;\n            }\n\n            if (M_Finish(ring, false).action != GF_NOOP) {\n                InvRing_SetStatusTransition(\n                    ring, RNG_CLOSING, RNG_FADING_OUT, INV_RING_CLOSE_FRAMES);\n            } else {\n                InvRing_SetStatusTransition(\n                    ring, RNG_CLOSING, RNG_DONE, INV_RING_CLOSE_FRAMES);\n                if (g_Config.visuals.enable_fade_effects\n                    && g_Config.ui.inventory_fade_effects) {\n                    Fader_InitFromCurrent(\n                        &ring->back_fader, 0.0f, M_INV_RING_FADE_TIME_FAST);\n                }\n            }\n\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n\n        const bool examine = g_InputDB.look && InvRing_CanExamine();\n        if (g_InputDB.menu_confirm || examine) {\n            if ((ring->mode == INV_SAVE_MODE\n                 || ring->mode == INV_SAVE_CRYSTAL_MODE\n                 || ring->mode == INV_LOAD_MODE || ring->mode == INV_DEATH_MODE\n                 || ring->mode == INV_GLOBE_SELECT_MODE)\n                && !ring->is_pass_open) {\n                ring->is_pass_open = true;\n            }\n\n            g_InvRing_Source[ring->type].current = ring->current_object;\n            INVENTORY_ITEM *const inv_item =\n                g_InvRing_Source[ring->type].items[ring->current_object];\n\n            if (examine) {\n                inv_item->action = ACTION_EXAMINE;\n                inv_item->goal_frame = 0;\n                inv_item->anim_direction = 1;\n            } else {\n                inv_item->action = ACTION_USE;\n                inv_item->goal_frame = inv_item->open_frame;\n                inv_item->anim_direction = 1;\n            }\n            InvRing_SetStatusTransition(\n                ring, RNG_SELECTING, RNG_SELECTED, M_SELECTING_FRAMES);\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n\n            switch (inv_item->object_id) {\n            case O_COMPASS_OPTION:\n                Sound_Effect(SFX_MENU_COMPASS, nullptr, SPM_ALWAYS);\n                break;\n\n            case O_STOPWATCH_OPTION:\n                break;\n\n            case O_PHOTO_OPTION:\n                Sound_Effect(SFX_MENU_LARA_HOME, nullptr, SPM_ALWAYS);\n                break;\n\n            case O_CONTROL_OPTION:\n                Sound_Effect(SFX_MENU_GAMEBOY, nullptr, SPM_ALWAYS);\n                break;\n\n            case O_PISTOL_OPTION:\n            case O_SHOTGUN_OPTION:\n            case O_MAGNUM_OPTION:\n            case O_AUTOS_OPTION:\n            case O_DESERT_EAGLE_OPTION:\n            case O_UZI_OPTION:\n            case O_HARPOON_OPTION:\n            case O_M16_OPTION:\n            case O_MP5_OPTION:\n            case O_GRENADE_GUN_OPTION:\n            case O_ROCKET_GUN_OPTION:\n                Sound_Effect(SFX_MENU_GUNS, nullptr, SPM_ALWAYS);\n                break;\n\n            default:\n                Sound_Effect(SFX_MENU_CHOOSE, nullptr, SPM_ALWAYS);\n                break;\n            }\n        }\n\n        if (g_InputDB.menu_up && ring->mode != INV_TITLE_MODE\n            && ring->mode != INV_KEYS_MODE\n            && ring->mode != INV_GLOBE_SELECT_MODE) {\n            if (ring->type == RT_MAIN) {\n                if (g_InvRing_Source[RT_KEYS].count > 0) {\n                    M_SetupRingSwitchClose(ring, RNG_MAIN2KEYS);\n                }\n                g_Input = (INPUT_STATE) {};\n                g_InputDB = (INPUT_STATE) {};\n            } else if (ring->type == RT_OPTION) {\n                if (g_InvRing_Source[RT_MAIN].count > 0) {\n                    M_SetupRingSwitchClose(ring, RNG_OPTION2MAIN);\n                }\n                g_InputDB = (INPUT_STATE) {};\n            }\n        } else if (\n            g_InputDB.menu_down && ring->mode != INV_TITLE_MODE\n            && ring->mode != INV_KEYS_MODE\n            && ring->mode != INV_GLOBE_SELECT_MODE) {\n            if (ring->type == RT_MAIN) {\n                if (g_InvRing_Source[RT_OPTION].count > 0\n                    && !InvRing_IsOptionLockedOut()) {\n                    M_SetupRingSwitchClose(ring, RNG_MAIN2OPTION);\n                }\n                g_InputDB = (INPUT_STATE) {};\n            } else if (ring->type == RT_KEYS) {\n                if (g_InvRing_Source[RT_MAIN].count > 0) {\n                    M_SetupRingSwitchClose(ring, RNG_KEYS2MAIN);\n                }\n                g_Input = (INPUT_STATE) {};\n                g_InputDB = (INPUT_STATE) {};\n            }\n        }\n        break;\n\n    case RNG_MAIN2OPTION:\n        M_TransitionToRing(ring, RT_MAIN, RT_OPTION);\n        break;\n\n    case RNG_MAIN2KEYS:\n        M_TransitionToRing(ring, RT_MAIN, RT_KEYS);\n        break;\n\n    case RNG_KEYS2MAIN:\n        M_TransitionToRing(ring, RT_KEYS, RT_MAIN);\n        break;\n\n    case RNG_OPTION2MAIN:\n        M_TransitionToRing(ring, RT_OPTION, RT_MAIN);\n        break;\n\n    case RNG_SELECTED: {\n        INVENTORY_ITEM *inv_item = ring->list[ring->current_object];\n        if (inv_item->object_id == O_PASSPORT_CLOSED) {\n            inv_item->object_id = O_PASSPORT_OPTION;\n        }\n\n        bool busy = false;\n        for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) {\n            busy = false;\n            if (inv_item->y_rot == inv_item->y_rot_sel) {\n                busy = M_AnimateInventoryItem(inv_item);\n            }\n        }\n\n        Option_Control(inv_item, busy);\n\n        if (!busy) {\n            if (g_InputDB.menu_back && ring->mode != INV_GLOBE_SELECT_MODE) {\n                InvRing_SetStatusTransition(\n                    ring, RNG_CLOSING_ITEM, RNG_DESELECT, 0);\n                g_Input = (INPUT_STATE) {};\n                g_InputDB = (INPUT_STATE) {};\n                if (ring->mode == INV_LOAD_MODE || ring->mode == INV_SAVE_MODE\n                    || ring->mode == INV_SAVE_CRYSTAL_MODE) {\n                    InvRing_SetStatusTransition(\n                        ring, RNG_CLOSING_ITEM, RNG_EXITING_INVENTORY, 0);\n                    g_Input = (INPUT_STATE) {};\n                    g_InputDB = (INPUT_STATE) {};\n                }\n            }\n\n            if (g_InputDB.menu_confirm) {\n                m_InvChosen = inv_item->object_id;\n                if (ring->type == RT_MAIN) {\n                    g_InvRing_Source[RT_MAIN].current = ring->current_object;\n                } else if (ring->type != RT_NUMBER_OF) {\n                    g_InvRing_Source[ring->type].current = ring->current_object;\n                }\n\n                if (ring->mode == INV_TITLE_MODE\n                    && (inv_item->object_id == O_DETAIL_OPTION\n                        || inv_item->object_id == O_SOUND_OPTION\n                        || inv_item->object_id == O_PDA_OPTION\n                        || inv_item->object_id == O_CONTROL_OPTION\n                        || inv_item->object_id == O_GLOBE_SELECT_OPTION)) {\n                    InvRing_SetStatusTransition(\n                        ring, RNG_CLOSING_ITEM, RNG_DESELECT, 0);\n                } else {\n                    InvRing_SetStatusTransition(\n                        ring, RNG_CLOSING_ITEM, RNG_EXITING_INVENTORY, 0);\n                }\n                g_Input = (INPUT_STATE) {};\n                g_InputDB = (INPUT_STATE) {};\n            }\n        }\n        break;\n    }\n\n    case RNG_DESELECT: {\n        INVENTORY_ITEM *const inv_item = ring->list[ring->current_object];\n        Option_Close(inv_item);\n        Sound_Effect(SFX_MENU_SPINOUT, nullptr, SPM_ALWAYS);\n        InvRing_SetStatusTransition(\n            ring, RNG_DESELECTING, RNG_OPEN, M_SELECTING_FRAMES);\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n        break;\n    }\n\n    case RNG_CLOSING_ITEM: {\n        INVENTORY_ITEM *inv_item = ring->list[ring->current_object];\n        for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) {\n            if (!M_AnimateInventoryItem(inv_item)) {\n                if (inv_item->object_id == O_PASSPORT_OPTION) {\n                    inv_item->object_id = O_PASSPORT_CLOSED;\n                    inv_item->current_frame = 0;\n                }\n\n                InvRing_SetStatusTransition(\n                    ring, ring->status_target, ring->status_target,\n                    M_SELECTING_FRAMES);\n                break;\n            }\n        }\n        break;\n    }\n\n    case RNG_EXITING_INVENTORY:\n        if (ring->status_frames == 0) {\n            if (M_Finish(ring, false).action != GF_NOOP) {\n                // Fade to black. Do it later once reaching RNG_FADING_OUT.\n                InvRing_SetStatusTransition(\n                    ring, RNG_CLOSING, RNG_FADING_OUT, INV_RING_CLOSE_FRAMES);\n            } else {\n                // Fade to game. Do it as soon as the ring starts to close.\n                InvRing_SetStatusTransition(\n                    ring, RNG_CLOSING, RNG_DONE, INV_RING_CLOSE_FRAMES);\n                if (g_Config.visuals.enable_fade_effects\n                    && g_Config.ui.inventory_fade_effects) {\n                    Fader_InitFromCurrent(\n                        &ring->back_fader, 0.0f, M_INV_RING_FADE_TIME_FAST);\n                }\n            }\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    if (ring->status == RNG_OPEN || ring->status == RNG_SELECTING\n        || ring->status == RNG_SELECTED || ring->status == RNG_DESELECTING\n        || ring->status == RNG_DESELECT || ring->status == RNG_CLOSING_ITEM) {\n        if (!ring->rotating\n            && ((!g_Input.menu_left && !g_Input.menu_right)\n                || ring->number_of_objects <= 1)) {\n            INVENTORY_ITEM *const inv_item = ring->list[ring->current_object];\n            M_RingNotActive(ring, inv_item);\n        }\n        M_RingIsOpen(ring);\n    } else {\n        M_RingIsNotOpen(ring);\n    }\n\n    if (ring->status == RNG_OPENING || ring->status == RNG_CLOSING\n        || ring->status == RNG_MAIN2OPTION || ring->status == RNG_OPTION2MAIN\n        || ring->status == RNG_EXITING_INVENTORY\n        || ring->status == RNG_FADING_OUT || ring->status == RNG_DONE\n        || ring->rotating) {\n        M_RingActive();\n    }\n\n    Interpolation_Remember();\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nvoid InvRing_RemoveAllText(void)\n{\n    InvRing_RemoveHeader();\n    InvRing_RemoveItemTexts();\n    InvRing_ClearButtonHint();\n}\n\nINV_RING *InvRing_Open(const INVENTORY_MODE mode)\n{\n    if (mode == INV_KEYS_MODE && g_InvRing_Source[RT_KEYS].count == 0) {\n        m_InvChosen = NO_OBJECT;\n        return nullptr;\n    }\n\n    m_InvChosen = NO_OBJECT;\n\n    g_InvRing_OldCamera = g_Camera;\n    m_StartLevel = -1;\n\n    if (mode == INV_TITLE_MODE) {\n        InvRing_ShowVersionText();\n        Savegame_ScanSavedGames();\n    } else {\n        InvRing_RemoveVersionText();\n    }\n\n    if (mode != INV_GLOBE_SELECT_MODE) {\n        // Reset option ring\n        g_InvRing_Source[RT_OPTION].count = 0;\n        Inv_InsertItem(\n            InvRing_GetByObjectID(O_PASSPORT_CLOSED) != nullptr\n                ? InvRing_GetByObjectID(O_PASSPORT_CLOSED)\n                : InvRing_GetByObjectID(O_PASSPORT_OPTION));\n        if (g_TRVersion == 1) {\n            Inv_InsertItem(InvRing_GetByObjectID(O_CONTROL_OPTION));\n            Inv_InsertItem(InvRing_GetByObjectID(O_SOUND_OPTION));\n            Inv_InsertItem(InvRing_GetByObjectID(O_DETAIL_OPTION));\n        } else {\n            Inv_InsertItem(InvRing_GetByObjectID(O_DETAIL_OPTION));\n            Inv_InsertItem(InvRing_GetByObjectID(O_CONTROL_OPTION));\n            Inv_InsertItem(InvRing_GetByObjectID(O_SOUND_OPTION));\n        }\n        Inv_InsertItem(InvRing_GetByObjectID(O_PDA_OPTION));\n        if (mode == INV_TITLE_MODE && GF_GetGymLevel() != nullptr) {\n            Inv_InsertItem(InvRing_GetByObjectID(O_PHOTO_OPTION));\n        }\n    } else if (g_InvRing_Source[RT_GLOBE_SELECT].count == 0) {\n        INVENTORY_ITEM *const globe =\n            InvRing_GetByObjectID(O_GLOBE_SELECT_OPTION);\n        if (globe != nullptr) {\n            Inv_InsertItem(globe);\n        }\n    }\n\n    g_InvRing_Source[RT_KEYS].current = 0;\n    for (int32_t i = 0; i < g_InvRing_Source[RT_KEYS].count; i++) {\n        InvRing_InitInvItem(g_InvRing_Source[RT_KEYS].items[i]);\n    }\n\n    g_InvRing_Source[RT_MAIN].current = 0;\n    for (int32_t i = 0; i < g_InvRing_Source[RT_MAIN].count; i++) {\n        InvRing_InitInvItem(g_InvRing_Source[RT_MAIN].items[i]);\n    }\n\n    g_InvRing_Source[RT_OPTION].current = 0;\n    for (int32_t i = 0; i < g_InvRing_Source[RT_OPTION].count; i++) {\n        g_InvRing_Source[RT_OPTION].qtys[i] = 1;\n        InvRing_InitInvItem(g_InvRing_Source[RT_OPTION].items[i]);\n    }\n\n    g_InvRing_Source[RT_GLOBE_SELECT].current = 0;\n    for (int32_t i = 0; i < g_InvRing_Source[RT_GLOBE_SELECT].count; i++) {\n        g_InvRing_Source[RT_GLOBE_SELECT].qtys[i] = 1;\n        InvRing_InitInvItem(g_InvRing_Source[RT_GLOBE_SELECT].items[i]);\n    }\n\n    if (mode == INV_TITLE_MODE && GF_GetGymLevel() != nullptr\n        && Gym_IsInventoryOpenEnabled()) {\n        for (int32_t i = 0; i < g_InvRing_Source[RT_OPTION].count; i++) {\n            if (g_InvRing_Source[RT_OPTION].items[i]->object_id\n                == O_PHOTO_OPTION) {\n                g_InvRing_Source[RT_OPTION].current = i;\n            }\n        }\n    }\n\n    if (!g_Config.audio.enable_music_in_inventory && mode != INV_TITLE_MODE) {\n        Music_Pause();\n        Sound_PauseAll();\n    }\n\n    INV_RING *const ring = Memory_Alloc(sizeof(INV_RING));\n    ring->mode = mode;\n    ring->background_style = mode == INV_TITLE_MODE\n        ? BK_IMAGE\n        : g_Config.ui.inventory_background_style;\n    ring->background_path = ring->background_style == BK_IMAGE\n        ? g_GameFlow.main_menu_background_path\n        : nullptr;\n\n    switch (mode) {\n    case INV_GLOBE_SELECT_MODE:\n        ring->background_style = BK_NONE;\n        ring->background_path = nullptr;\n        InvRing_InitRing(\n            ring, RT_GLOBE_SELECT, g_InvRing_Source[RT_GLOBE_SELECT].items,\n            g_InvRing_Source[RT_GLOBE_SELECT].count,\n            g_InvRing_Source[RT_GLOBE_SELECT].current);\n        Option_GlobeSelect_UpdateSelectable(ring);\n        break;\n\n    case INV_TITLE_MODE:\n    case INV_SAVE_MODE:\n    case INV_SAVE_CRYSTAL_MODE:\n    case INV_LOAD_MODE:\n    case INV_DEATH_MODE:\n        InvRing_InitRing(\n            ring, RT_OPTION, g_InvRing_Source[RT_OPTION].items,\n            g_InvRing_Source[RT_OPTION].count,\n            g_InvRing_Source[RT_OPTION].current);\n        break;\n\n    case INV_KEYS_MODE:\n        InvRing_InitRing(\n            ring, RT_KEYS, g_InvRing_Source[RT_KEYS].items,\n            g_InvRing_Source[RT_KEYS].count, g_InvRing_Source[RT_MAIN].current);\n        break;\n\n    default:\n        if (g_InvRing_Source[RT_MAIN].count > 0) {\n            InvRing_InitRing(\n                ring, RT_MAIN, g_InvRing_Source[RT_MAIN].items,\n                g_InvRing_Source[RT_MAIN].count,\n                g_InvRing_Source[RT_MAIN].current);\n        } else {\n            InvRing_InitRing(\n                ring, RT_OPTION, g_InvRing_Source[RT_OPTION].items,\n                g_InvRing_Source[RT_OPTION].count,\n                g_InvRing_Source[RT_OPTION].current);\n        }\n        break;\n    }\n\n    g_Inv_Mode = mode;\n    Interpolation_Remember();\n\n    if (mode == INV_TITLE_MODE) {\n        if (ring->background_path != nullptr) {\n            Output_Overlay_LoadImage(ring->background_path);\n        }\n        Fader_InitTo(&ring->top_fader, 1.0f, 0.0f, M_INV_RING_FADE_TIME_FAST);\n    } else {\n        Fader_InitTo(&ring->back_fader, 0.0f, 1.0f, M_INV_RING_FADE_TIME_FAST);\n    }\n\n    return ring;\n}\n\nvoid InvRing_Close(INV_RING *const ring)\n{\n    InvRing_RemoveAllText();\n    InvRing_RemoveVersionText();\n\n    if (ring->list != nullptr) {\n        INVENTORY_ITEM *const inv_item = ring->list[ring->current_object];\n        if (inv_item != nullptr) {\n            Option_Close(inv_item);\n        }\n    }\n    if (ring->mode == INV_TITLE_MODE) {\n        Music_Stop();\n        Sound_StopAll();\n    }\n\n    if (g_Config.input.enable_buffering_inventory) {\n        g_OldInputDB = (INPUT_STATE) {};\n    }\n\n    m_InvChosen = NO_OBJECT;\n    Memory_Free(ring);\n}\n\nGF_COMMAND InvRing_Control(INV_RING *const ring)\n{\n    InvRing_AdjustMusicVolume(ring);\n    m_ActiveRing = ring;\n    INVENTORY_ITEM **const prev_list = ring->list;\n    M_SnapshotFrameState(ring);\n    GF_COMMAND gf_cmd = M_Control(ring);\n\n    if (ring->status != RNG_OPENING && ring->status != RNG_DONE\n        && ring->status != RNG_FADING_OUT) {\n        for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) {\n            for (int32_t i = 0; i < ring->number_of_objects; i++) {\n                InvRing_UpdateInventoryItem(ring, ring->list[i]);\n            }\n        }\n    }\n\n    if (ring->status != RNG_DONE\n        && (ring->status != RNG_OPENING\n            || (ring->mode != INV_TITLE_MODE\n                || (!Fader_IsActive(&ring->top_fader)\n                    && !Fader_IsActive(&ring->back_fader))))) {\n        for (int32_t frame = 0; frame < INV_RING_FRAMES; frame++) {\n            InvRing_DoMotions(ring);\n        }\n    }\n\n    if (ring->list != prev_list) {\n        M_SnapshotFrameState(ring);\n    }\n\n    // Running motions in control can reach RNG_DONE in this same tick.\n    // Finalize immediately so phase code receives the non-NOOP GF command.\n    if (gf_cmd.action == GF_NOOP && ring->status == RNG_DONE\n        && !ring->is_done) {\n        gf_cmd = M_Control(ring);\n    }\n\n    m_ActiveRing = nullptr;\n    Overlay_Animate(1);\n    return gf_cmd;\n}\n\nbool InvRing_IsRingAvailable(const RING_TYPE ring_type)\n{\n    if (ring_type == RT_OPTION && InvRing_IsOptionLockedOut()) {\n        return false;\n    }\n    return g_InvRing_Source[ring_type].count > 0;\n}\n\nbool InvRing_IsOptionLockedOut(void)\n{\n    return g_Config.flow.lockout_option_ring;\n}\n\nINVENTORY_ITEM *InvRing_GetByObjectID(const OBJECT_ID object_id)\n{\n    for (int32_t i = 0; i < g_InvRing_Items->count; i++) {\n        INVENTORY_ITEM *const item =\n            *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i);\n        if (item->object_id == object_id) {\n            return item;\n        }\n    }\n    return nullptr;\n}\n"
  },
  {
    "path": "src/trx/game/inventory_ring/control.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/objects/types.h>\n\ntypedef void (*INV_RING_BUTTON_HINT_DRAWER)(void *user_data);\n\nINV_RING *InvRing_Open(INVENTORY_MODE mode);\nvoid InvRing_Close(INV_RING *ring);\n\nGF_COMMAND InvRing_Control(INV_RING *ring);\nbool InvRing_IsRingAvailable(RING_TYPE ring_type);\nINV_RING *InvRing_GetActiveRing(void);\n\nvoid InvRing_AdjustMusicVolume(const INV_RING *ring);\nvoid InvRing_SetRequestedObjectID(OBJECT_ID obj_id);\nvoid InvRing_SetButtonHintDrawer(\n    INV_RING_BUTTON_HINT_DRAWER draw_func, void *user_data);\nvoid InvRing_ClearButtonHint(void);\n\nvoid InvRing_RemoveAllText(void);\n\nINVENTORY_ITEM *InvRing_GetByObjectID(OBJECT_ID object_id);\n"
  },
  {
    "path": "src/trx/game/inventory_ring/draw.c",
    "content": "#include <trx/game/inventory_ring/draw.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/game.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/inventory_ring/priv.h>\n#include <trx/game/inventory_ring/vars.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/option.h>\n#include <trx/game/option/globe_select.h>\n#include <trx/game/option/stats.h>\n#include <trx/game/output.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\n#include <math.h>\n\n#define M_CAMERA_2_RING 598\n#define M_SHADE_NORMAL SHADE_LOW\n#define M_SHADE_SELECTED SHADE_NEUTRAL\n\nstatic XYZ_32 M_VectorViewFromWorld(const XYZ_32 v_world)\n{\n    return Matrix_MulVec32_M(&g_ViewMatrix, v_world);\n}\n\nstatic int16_t M_LerpI16(\n    const int16_t prev_value, const int16_t cur_value, const double rate)\n{\n    return (int16_t)round(LERP(prev_value, cur_value, rate));\n}\n\nstatic int32_t M_LerpI32(\n    const int32_t prev_value, const int32_t cur_value, const double rate)\n{\n    return (int32_t)round(LERP(prev_value, cur_value, rate));\n}\n\nstatic int16_t M_LerpAngleI16(\n    const int16_t prev_value, const int16_t cur_value, const double rate)\n{\n    const int32_t prev_u16 = (uint16_t)prev_value;\n    const int32_t cur_u16 = (uint16_t)cur_value;\n    const int32_t interp = Math_AngleMean(prev_u16, cur_u16, rate);\n    return (int16_t)(uint16_t)interp;\n}\n\nstatic float M_GlobeSelectPulse01(const float time)\n{\n    const int16_t angle = (((uint64_t)time) % 16ULL) * DEG_360 / 16;\n    const float s = (float)Math_Sin(angle);\n    return (s + 16384.0f) / (16384.0f * 2.0f);\n}\n\nstatic void M_GlobeSelectApplyLight(\n    const INV_RING *const ring, const uint32_t bit, const int32_t mesh_idx)\n{\n    const float ambient_u8 = 32.0f / 255.0f;\n    const RGB_F ambient = { ambient_u8, ambient_u8, ambient_u8 };\n\n    RGB_F colors[3] = {};\n\n    if (bit == 1u) {\n        colors[0] = (RGB_F) { 0, 256.0f / 4096.0f, 3840.0f / 4096.0f };\n        colors[1] = (RGB_F) { 0, 256.0f / 4096.0f, 3840.0f / 4096.0f };\n        colors[2] = (RGB_F) { 0, 256.0f / 4096.0f, 3840.0f / 4096.0f };\n    } else if ((bit & 0x7Eu) != 0u) {\n        const float pulse = M_GlobeSelectPulse01(Output_GetTime());\n        const int32_t area_idx = Option_GlobeSelect_AreaFromMeshIdx(mesh_idx);\n        const bool completed = area_idx >= 0 && area_idx < MAX_GLOBE_ZONES\n            && !ring->globe_select.selectable[area_idx];\n\n        const RGB_F marker = completed ? (RGB_F) { pulse, 0.0f, 0.0f }\n                                       : (RGB_F) { 0.0f, pulse, 0.0f };\n        colors[0] = marker;\n        colors[1] = marker;\n        colors[2] = marker;\n    } else {\n        colors[0] =\n            (RGB_F) { 256.0f / 4096.0f, 1024.0f / 4096.0f, 256.0f / 4096.0f };\n        colors[1] =\n            (RGB_F) { 256.0f / 4096.0f, 1024.0f / 4096.0f, 256.0f / 4096.0f };\n        colors[2] =\n            (RGB_F) { 256.0f / 4096.0f, 1024.0f / 4096.0f, 256.0f / 4096.0f };\n    }\n\n    const XYZ_32 dirs_offsets[3] = {\n        { .x = 0x1000, .y = -0x1000, .z = 0xC00 },\n        { .x = -0x1000, .y = -0x1000, .z = 0xC00 },\n        { .x = 0, .y = 0x800, .z = 0xC00 },\n    };\n    const XYZ_32 dirs_view[3] = {\n        M_VectorViewFromWorld(dirs_offsets[0]),\n        M_VectorViewFromWorld(dirs_offsets[1]),\n        M_VectorViewFromWorld(dirs_offsets[2]),\n    };\n\n    Output_SetTR3Light(ambient, colors, dirs_view);\n}\n\nstatic int32_t M_GetFrames(\n    const INV_RING *const ring, const INVENTORY_ITEM *const inv_item,\n    ANIM_FRAME **const out_frame1, ANIM_FRAME **const out_frame2,\n    int32_t *const out_rate)\n{\n    const OBJECT *const obj = Object_Get(inv_item->object_id);\n    const INVENTORY_ITEM *const cur_inv_item = ring->list[ring->current_object];\n\n    if (inv_item != cur_inv_item || inv_item->current_frame == 0\n        || (ring->status != RNG_SELECTED && ring->status != RNG_CLOSING_ITEM)) {\n        // only apply to animations, eg. the states where Inv_AnimateItem is\n        // being actively called\n        goto fallback;\n    }\n\n    if (inv_item->current_frame == inv_item->goal_frame\n        || inv_item->frames_total == 1 || g_Config.rendering.fps == 30) {\n        goto fallback;\n    }\n\n    const int32_t cur_frame_num = inv_item->current_frame;\n    int32_t next_frame_num = inv_item->current_frame + inv_item->anim_direction;\n    if (next_frame_num < 0) {\n        next_frame_num = 0;\n    }\n    if (next_frame_num >= inv_item->frames_total) {\n        next_frame_num = 0;\n    }\n\n    *out_frame1 = &obj->frame_base[cur_frame_num];\n    *out_frame2 = &obj->frame_base[next_frame_num];\n    *out_rate = 10;\n    return (Interpolation_GetRate() - 0.5) * 10.0;\n\n    // OG\nfallback:\n    *out_frame1 = &obj->frame_base[inv_item->current_frame];\n    *out_frame2 = *out_frame1;\n    *out_rate = 1;\n    return 0;\n}\n\nstatic void M_DrawItem(\n    const INV_RING *const ring, const INVENTORY_ITEM *const inv_item,\n    const int16_t view_rot_y)\n{\n    const double interp_rate = Interpolation_GetRate();\n    const int16_t draw_x_rot_pt = M_LerpAngleI16(\n        inv_item->prev_x_rot_pt, inv_item->x_rot_pt, interp_rate);\n    const int16_t draw_x_rot =\n        M_LerpAngleI16(inv_item->prev_x_rot, inv_item->x_rot, interp_rate);\n    const int16_t draw_y_rot =\n        M_LerpAngleI16(inv_item->prev_y_rot, inv_item->y_rot, interp_rate);\n    const int32_t draw_y_trans =\n        M_LerpI32(inv_item->prev_y_trans, inv_item->y_trans, interp_rate);\n    const int32_t draw_z_trans =\n        M_LerpI32(inv_item->prev_z_trans, inv_item->z_trans, interp_rate);\n    MATRIX draw_manual_rot = inv_item->prev_manual_rot;\n    Matrix_Slerp3x3_M(&draw_manual_rot, &inv_item->manual_rot, interp_rate);\n\n    int32_t shade = M_SHADE_NORMAL;\n    if (ring->status != RNG_FADING_OUT && ring->status != RNG_DONE) {\n        if (ring->rotating) {\n            float t = (ring->rot_count / (float)INV_RING_ROTATE_DURATION);\n            CLAMP(t, 0.0f, 1.0f);\n            if (inv_item == ring->list[ring->rotate_from_object]) {\n                t = 1.0f - t;\n            } else if (inv_item != ring->list[ring->rotate_to_object]) {\n                t = 1.0f;\n            }\n            shade = LERP((float)M_SHADE_SELECTED, (float)M_SHADE_NORMAL, t);\n        } else if (inv_item == ring->list[ring->current_object]) {\n            shade = M_SHADE_SELECTED;\n        }\n    }\n    Output_SetLightAdder(shade);\n\n    Matrix_TranslateRel(0, draw_y_trans, draw_z_trans);\n\n    Matrix_RotX(-draw_x_rot_pt);\n    Matrix_RotY(-view_rot_y);\n    Matrix_Mul3x3(&draw_manual_rot);\n    Matrix_RotY(view_rot_y);\n    Matrix_RotX(draw_x_rot_pt);\n\n    Matrix_RotY(draw_y_rot);\n    Matrix_RotX(draw_x_rot);\n\n    const OBJECT *const obj = Object_Get(inv_item->object_id);\n    if (!obj->loaded || obj->mesh_count < 0) {\n        return;\n    }\n\n    if (inv_item->object_id == O_GLOBE_SELECT_OPTION) {\n        Matrix_Rot16(ring->globe_select.rot);\n\n        InvRing_Light(ring);\n        ANIM_FRAME *const frame = &obj->frame_base[0];\n        const uint32_t mesh_bits = ring->globe_select.meshes_drawn;\n        for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) {\n            if (mesh_idx == 0) {\n                Matrix_TranslateRel16(frame->offset);\n                Matrix_Rot16(frame->mesh_rots[mesh_idx]);\n            } else {\n                const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n                if (bone->matrix_pop) {\n                    Matrix_Pop();\n                }\n                if (bone->matrix_push) {\n                    Matrix_Push();\n                }\n\n                Matrix_TranslateRel32(bone->pos);\n                Matrix_Rot16(frame->mesh_rots[mesh_idx]);\n            }\n\n            const uint32_t bit = 1u << mesh_idx;\n            if ((mesh_bits & bit) == 0u) {\n                continue;\n            }\n\n            M_GlobeSelectApplyLight(ring, bit, mesh_idx);\n            Object_DrawMesh(obj->mesh_idx + mesh_idx, 0, false);\n        }\n        return;\n    }\n\n    int32_t rate;\n    ANIM_FRAME *frame1;\n    ANIM_FRAME *frame2;\n    const int32_t frac = M_GetFrames(ring, inv_item, &frame1, &frame2, &rate);\n    if (inv_item->object_id == O_COMPASS_OPTION) {\n        const int16_t extra_rotation[1] = {\n            Option_Stats_GetCompassNeedleAngle()\n        };\n        Object_GetBone(obj, 0)->rot.y = true;\n        Object_DrawInterpolatedObject(\n            obj, inv_item->meshes_drawn, extra_rotation, frame1, frame2, frac,\n            rate);\n    } else if (inv_item->object_id == O_STOPWATCH_OPTION) {\n        const RESUME_INFO *const current_info =\n            Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n        const int32_t total_seconds = current_info->stats.timer / LOGIC_FPS;\n        const int32_t hours = (total_seconds % 43200) * DEG_1 * -360 / 43200;\n        const int32_t minutes = (total_seconds % 3600) * DEG_1 * -360 / 3600;\n        const int32_t seconds = (total_seconds % 60) * DEG_1 * -360 / 60;\n\n        const int16_t extra_rotation[3] = { hours, minutes, seconds };\n        Object_GetBone(obj, 3)->rot.z = true;\n        Object_GetBone(obj, 4)->rot.z = true;\n        Object_GetBone(obj, 5)->rot.z = true;\n        Object_DrawInterpolatedObject(\n            obj, inv_item->meshes_drawn, extra_rotation, frame1, frame2, frac,\n            rate);\n    } else {\n        Object_DrawInterpolatedObject(\n            obj, inv_item->meshes_drawn, nullptr, frame1, frame2, frac, rate);\n    }\n}\n\nconst INVENTORY_ITEM *InvRing_GetInvItem(const OBJECT_ID obj_id)\n{\n    for (int32_t i = 0; i < g_InvRing_Items->count; i++) {\n        INVENTORY_ITEM *const item =\n            *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i);\n        if (item->object_id == obj_id) {\n            return item;\n        }\n    }\n    return nullptr;\n}\n\nvoid InvRing_Draw(INV_RING *const ring)\n{\n    InvRing_DrawUI(ring);\n    const double interp_rate = Interpolation_GetRate();\n    const int16_t draw_radius =\n        M_LerpI16(ring->prev_radius, ring->radius, interp_rate);\n    const int16_t draw_camera_y =\n        M_LerpI16(ring->prev_camera_y, ring->camera.pos.y, interp_rate);\n    const int16_t draw_ring_rot_y = M_LerpAngleI16(\n        ring->prev_ring_rot_y, ring->ring_pos.rot.y, interp_rate);\n    const int16_t draw_camera_pitch = M_LerpAngleI16(\n        ring->prev_camera_pitch, ring->camera_pitch, interp_rate);\n\n    INV_RING draw_ring = *ring;\n    draw_ring.radius = draw_radius;\n    draw_ring.camera.pos.y = draw_camera_y;\n    draw_ring.camera_pitch = draw_camera_pitch;\n    draw_ring.ring_pos.rot.y = draw_ring_rot_y;\n    draw_ring.camera.pos.z = draw_radius + M_CAMERA_2_RING;\n\n    if (ring->mode == INV_TITLE_MODE) {\n        if (ring->background_path != nullptr) {\n            Output_Overlay_DrawImageBilinear(ring->background_path);\n        }\n        Interpolation_Interpolate();\n    } else {\n        const float opacity = g_Config.ui.inventory_fade_effects\n            ? Fader_GetCurrentValue(&ring->back_fader)\n            : ring->back_fader.args.target;\n\n        switch (ring->background_style) {\n        case BK_NONE:\n            if (ring->mode != INV_GLOBE_SELECT_MODE) {\n                Output_Overlay_DrawGame();\n            }\n            break;\n\n        case BK_TRANSPARENT_MEDIUM:\n            Output_Overlay_DrawGame();\n            Output_Overlay_DrawBlackRectangle(opacity * 0.5f, false);\n            break;\n\n        case BK_TRANSPARENT_DARK:\n            Output_Overlay_DrawGame();\n            Output_Overlay_DrawBlackRectangle(opacity * 0.8f, false);\n            break;\n\n        case BK_BLACK:\n            Output_Overlay_DrawGame();\n            Output_Overlay_DrawBlackRectangle(opacity * 1.0f, false);\n            break;\n\n        case BK_MONOCHROME:\n            Output_Overlay_DrawGameMono(opacity);\n            break;\n\n        case BK_MONOCHROME_COOL:\n            Output_Overlay_DrawGameMonoCool(opacity);\n            break;\n\n        case BK_MONOCHROME_WARM:\n            Output_Overlay_DrawGameMonoWarm(opacity);\n            break;\n\n        case BK_PATTERN_STATIC:\n        case BK_PATTERN_WAVE:\n            if (opacity < 1.0f) {\n                Output_Overlay_DrawGame();\n            }\n            Output_Overlay_DrawPatternOpacity(\n                ring->background_style == BK_PATTERN_WAVE, opacity);\n            break;\n\n        case BK_IMAGE:\n            if (ring->background_path != nullptr\n                && Output_Overlay_LoadImage(ring->background_path)) {\n                Output_Overlay_DrawImageBilinear(ring->background_path);\n                Output_Overlay_DrawBlackRectangle(1.0f - opacity, false);\n            } else {\n                Output_Overlay_DrawBlackRectangle(1.0f, false);\n            }\n            break;\n\n        default:\n            Output_Overlay_DrawGame();\n            break;\n        }\n    }\n    Output_Flush();\n\n    const int16_t old_fov = Viewport_GetSystemFOV();\n    const FOV_MODE old_fov_mode = Viewport_GetFOVMode();\n    Viewport_AlterFOV(FOV_VALUE_PASSPORT * DEG_1, FOV_MODE_PASSPORT);\n    Output_ApplyFOV();\n\n    XYZ_32 view_pos;\n    XYZ_16 view_rot;\n    InvRing_GetView(&draw_ring, &view_pos, &view_rot);\n    Matrix_GenerateW2V(&view_pos, &view_rot);\n    const int32_t old_fog_start = Output_GetFogStart();\n    const int32_t old_fog_end = Output_GetFogEnd();\n    Output_SetFogStart(20 * WALL_L);\n    Output_SetFogEnd(100 * WALL_L);\n\n    InvRing_Light(&draw_ring);\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(draw_ring.ring_pos.pos);\n    Matrix_Rot16(draw_ring.ring_pos.rot);\n\n    if (!(ring->mode == INV_TITLE_MODE\n          && (Fader_IsActive(&ring->top_fader)\n              || Fader_IsActive(&ring->back_fader))\n          && ring->status == RNG_OPENING)) {\n        int16_t angle = 0;\n        for (int32_t i = 0; i < draw_ring.number_of_objects; i++) {\n            INVENTORY_ITEM *const inv_item = draw_ring.list[i];\n            Matrix_Push();\n            Matrix_RotY(angle);\n            Matrix_TranslateRel(draw_ring.radius, 0, 0);\n            Matrix_RotY(DEG_90);\n            const int16_t draw_x_rot_pt = M_LerpAngleI16(\n                inv_item->prev_x_rot_pt, inv_item->x_rot_pt, interp_rate);\n            Matrix_RotX(draw_x_rot_pt);\n            M_DrawItem(&draw_ring, inv_item, view_rot.y);\n            angle += draw_ring.angle_adder;\n            Matrix_Pop();\n        }\n    }\n\n    Matrix_Pop();\n    SceneCompositor_Flush();\n    Output_SetFogStart(old_fog_start);\n    Output_SetFogEnd(old_fog_end);\n    Viewport_AlterFOV(old_fov, old_fov_mode);\n\n    if (ring->status == RNG_SELECTED) {\n        INVENTORY_ITEM *const inv_item = ring->list[ring->current_object];\n        if (inv_item->object_id == O_PASSPORT_CLOSED) {\n            inv_item->object_id = O_PASSPORT_OPTION;\n        }\n        Option_Draw(inv_item);\n    }\n\n    float top_opacity = Fader_GetCurrentValue(&ring->top_fader);\n    if (ring->mode == INV_TITLE_MODE && ring->status != RNG_OPENING) {\n        top_opacity = 0.0f;\n    }\n    Output_Overlay_DrawBlackRectangle(top_opacity, true);\n}\n"
  },
  {
    "path": "src/trx/game/inventory_ring/draw.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid InvRing_Draw(INV_RING *ring);\n\nconst INVENTORY_ITEM *InvRing_GetInvItem(OBJECT_ID obj_id);\n"
  },
  {
    "path": "src/trx/game/inventory_ring/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    INV_GAME_MODE,\n    INV_TITLE_MODE,\n    INV_KEYS_MODE,\n    INV_SAVE_MODE,\n    INV_LOAD_MODE,\n    INV_DEATH_MODE,\n    INV_SAVE_CRYSTAL_MODE,\n    INV_GLOBE_SELECT_MODE,\n} INVENTORY_MODE;\n\ntypedef enum {\n    RT_MAIN = 0,\n    RT_OPTION = 1,\n    RT_KEYS = 2,\n    RT_GLOBE_SELECT = 3,\n    RT_NUMBER_OF,\n} RING_TYPE;\n\ntypedef enum {\n    RNG_OPENING,\n    RNG_OPEN,\n    RNG_CLOSING,\n    RNG_MAIN2OPTION,\n    RNG_MAIN2KEYS,\n    RNG_KEYS2MAIN,\n    RNG_OPTION2MAIN,\n    RNG_SELECTING,\n    RNG_SELECTED,\n    RNG_DESELECTING,\n    RNG_DESELECT,\n    RNG_CLOSING_ITEM,\n    RNG_EXITING_INVENTORY,\n    RNG_DONE,\n    RNG_FADING_OUT,\n} RING_STATUS;\n"
  },
  {
    "path": "src/trx/game/inventory_ring/priv.c",
    "content": "#include <trx/game/inventory_ring/priv.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/strings.h>\n#include <trx/game/const.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/inventory_ring/vars.h>\n#include <trx/game/matrix.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/output/state.h>\n#include <trx/game/overlay.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n#include <trx/version.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_RING_SWITCH_FRAMES (96 / 2)\n#define M_CAMERA_Y_OFFSET (-96)\n#define M_MANUAL_ROT_RESET_RATE 0.15\n\ntypedef enum {\n    // clang-format off\n    PASS_MESH_SPINE    = 1 << 0,\n    PASS_MESH_FRONT    = 1 << 1,\n    PASS_MESH_IN_FRONT = 1 << 2,\n    PASS_MESH_PAGE_2   = 1 << 3,\n    PASS_MESH_BACK     = 1 << 4,\n    PASS_MESH_IN_BACK  = 1 << 5,\n    PASS_MESH_PAGE_1   = 1 << 6,\n    PASS_MESH_COMMON   = PASS_MESH_SPINE | PASS_MESH_BACK | PASS_MESH_IN_BACK | PASS_MESH_FRONT,\n    // clang-format on\n} PASS_MESH;\n\nstatic bool m_ShowExamine = false;\nstatic bool m_ShowUseItemButton = false;\nstatic void (*m_ButtonHintDrawFunc)(void *) = nullptr;\nstatic void *m_ButtonHintUserData = nullptr;\nstatic char *m_CountText = nullptr;\nstatic size_t m_CountTextCap = 0;\nstatic OBJECT_ID m_RequestedObjectID = NO_OBJECT;\n\nstatic void M_DrawExamineHint(void *const user_data)\n{\n    UI_BeginStack(UI_STACK_HORIZONTAL);\n    UI_ButtonLabel(INPUT_ROLE_LOOK, GS(\"general/actions/examine_item\"));\n    if (m_ShowUseItemButton) {\n        UI_Spacer(60.0f, 0.0f);\n        UI_ButtonLabel(INPUT_ROLE_ACTION, GS(\"general/actions/use_item\"));\n    }\n    UI_EndStack();\n}\n\nstatic void M_AdjustRot(int16_t *const rot, const int16_t dest_rot)\n{\n    const int32_t delta = dest_rot - *rot;\n    if (delta != 0) {\n        if (delta > 0 && delta < DEG_180) {\n            *rot += 1024;\n        } else {\n            *rot -= 1024;\n        }\n        *rot &= ~(1024 - 1);\n    }\n}\n\nstatic XYZ_32 M_VectorViewFromWorld(const XYZ_32 v_world)\n{\n    return Matrix_MulVec32_M(&g_ViewMatrix, v_world);\n}\n\nstatic void M_HandleRequestedObject(INV_RING *const ring)\n{\n    if (m_RequestedObjectID == NO_OBJECT) {\n        return;\n    }\n\n    for (int32_t i = 0; i < ring->number_of_objects; i++) {\n        const OBJECT_ID object_id = ring->list[i]->object_id;\n        if (object_id == m_RequestedObjectID\n            && Inv_RequestItem(object_id) > 0) {\n            ring->current_object = i;\n            break;\n        }\n    }\n\n    m_RequestedObjectID = NO_OBJECT;\n}\n\nstatic void M_MotionInit(INV_RING *const ring)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->radius_target = 0;\n    motion->radius_rate = 0;\n    motion->camera_y_target = 0;\n    motion->camera_y_rate = 0;\n    motion->camera_pitch_target = 0;\n    motion->camera_pitch_rate = 0;\n    motion->rotate_target = 0;\n    motion->rotate_rate = 0;\n    motion->item_pt_x_rot_target = 0;\n    motion->item_pt_x_rot_rate = 0;\n    motion->item_x_rot_target = 0;\n    motion->item_x_rot_rate = 0;\n    motion->item_y_trans_target = 0;\n    motion->item_y_trans_rate = 0;\n    motion->item_z_trans_target = 0;\n    motion->item_z_trans_rate = 0;\n\n    motion->misc = 0;\n}\n\nstatic void M_MotionCameraPos(INV_RING *const ring, const int16_t target)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->camera_y_target = target;\n    motion->camera_y_rate = (target - ring->camera.pos.y) / ring->status_frames;\n}\n\nstatic void M_MotionCameraPitch(INV_RING *const ring, const int16_t target)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->camera_pitch_target = target;\n    motion->camera_pitch_rate = target / ring->status_frames;\n    motion->misc = target;\n}\n\nstatic void M_MotionRotation(\n    INV_RING *const ring, const int16_t rotation, const int16_t target)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->rotate_target = target;\n    motion->rotate_rate = rotation / ring->status_frames;\n}\n\nstatic void M_MotionRadius(INV_RING *const ring, const int16_t target)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->radius_target = target;\n    motion->radius_rate = (target - ring->radius) / ring->status_frames;\n}\n\nstatic void M_MotionItemSelect(\n    INV_RING *const ring, const INVENTORY_ITEM *const inv_item)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->item_pt_x_rot_target = inv_item->x_rot_pt_sel;\n    motion->item_pt_x_rot_rate = inv_item->x_rot_pt_sel / ring->status_frames;\n    motion->item_x_rot_target = inv_item->x_rot_sel;\n    motion->item_x_rot_rate =\n        (inv_item->x_rot_sel - inv_item->x_rot_nosel) / ring->status_frames;\n    motion->item_y_trans_target = inv_item->y_trans_sel;\n    motion->item_y_trans_rate = inv_item->y_trans_sel / ring->status_frames;\n    motion->item_z_trans_target = inv_item->z_trans_sel;\n    motion->item_z_trans_rate = inv_item->z_trans_sel / ring->status_frames;\n}\n\nstatic void M_MotionItemDeselect(\n    INV_RING *const ring, const INVENTORY_ITEM *const inv_item)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    motion->item_pt_x_rot_target = 0;\n    motion->item_pt_x_rot_rate =\n        -(inv_item->x_rot_pt_sel / ring->status_frames);\n    motion->item_x_rot_target = inv_item->x_rot_nosel;\n    motion->item_x_rot_rate =\n        (inv_item->x_rot_nosel - inv_item->x_rot_sel) / ring->status_frames;\n    motion->item_y_trans_target = 0;\n    motion->item_y_trans_rate = -(inv_item->y_trans_sel / ring->status_frames);\n    motion->item_z_trans_target = 0;\n    motion->item_z_trans_rate = -(inv_item->z_trans_sel / ring->status_frames);\n}\n\nvoid InvRing_AdjustMusicVolume(const INV_RING *const ring)\n{\n    if (ring->mode == INV_TITLE_MODE) {\n        Music_SetVolume(g_Config.audio.music_volume);\n        return;\n    }\n    const bool is_ambient =\n        Music_GetCurrentPlayingTrack() == Music_GetCurrentLoopedTrack();\n    const double base_volume = is_ambient ? g_Config.audio.ambient_volume\n                                          : g_Config.audio.music_volume;\n    const double multiplier = is_ambient\n        ? g_Config.audio.inventory_ambient_volume\n        : g_Config.audio.inventory_music_volume;\n    Music_SetVolume(base_volume * multiplier);\n\n    if (ring->mode != INV_GLOBE_SELECT_MODE) {\n        Sound_ResetAmbient();\n        Sound_UpdateEffects();\n    }\n}\n\nvoid InvRing_SetRequestedObjectID(const OBJECT_ID obj_id)\n{\n    m_RequestedObjectID = obj_id;\n}\n\nvoid InvRing_InitRing(\n    INV_RING *const ring, const RING_TYPE type, INVENTORY_ITEM **const list,\n    const int16_t qty, const int16_t current)\n{\n    ring->type = type;\n    ring->list = list;\n    ring->radius = 0;\n    ring->prev_radius = 0;\n    ring->number_of_objects = qty;\n    ring->current_object = current;\n    ring->angle_adder = DEG_360 / qty;\n\n    ring->is_pass_open = false;\n    ring->is_demo_needed = false;\n    ring->has_spun_out = false;\n\n    M_HandleRequestedObject(ring);\n\n    if (ring->mode == INV_TITLE_MODE) {\n        ring->camera_pitch = 1024;\n    } else {\n        ring->camera_pitch = 0;\n    }\n    ring->prev_camera_pitch = ring->camera_pitch;\n\n    ring->rotating = false;\n    ring->rotate_from_object = 0;\n    ring->rotate_to_object = 0;\n    ring->rot_count = 0;\n    ring->target_object = 0;\n    ring->rot_adder = 0;\n    ring->rot_adder_l = 0;\n    ring->rot_adder_r = 0;\n\n    ring->camera.pos.x = 0;\n    ring->camera.pos.y = INV_RING_CAMERA_START_HEIGHT;\n    ring->camera.pos.z = 896;\n    ring->camera.rot.x = 0;\n    ring->camera.rot.y = 0;\n    ring->camera.rot.z = 0;\n\n    ring->status = RNG_OPENING;\n    ring->status_target = RNG_OPEN;\n    ring->status_frames = INV_RING_OPEN_FRAMES;\n\n    M_MotionRadius(ring, INV_RING_RADIUS);\n    M_MotionCameraPos(ring, INV_RING_CAMERA_HEIGHT);\n    M_MotionRotation(\n        ring, INV_RING_OPEN_ROTATION,\n        -DEG_90 - ring->current_object * ring->angle_adder);\n\n    ring->ring_pos.pos.x = 0;\n    ring->ring_pos.pos.y = 0;\n    ring->ring_pos.pos.z = 0;\n    ring->ring_pos.rot.x = 0;\n    ring->ring_pos.rot.y = ring->motion.rotate_target - INV_RING_OPEN_ROTATION;\n    ring->prev_ring_rot_y = ring->ring_pos.rot.y;\n    ring->ring_pos.rot.z = 0;\n\n    ring->light.x = -1536;\n    ring->light.y = 256;\n    ring->light.z = 1024;\n\n    ring->prev_camera_y = ring->camera.pos.y;\n\n    m_ShowExamine = false;\n    m_ShowUseItemButton = false;\n    m_ButtonHintDrawFunc = nullptr;\n    m_ButtonHintUserData = nullptr;\n}\n\nvoid InvRing_InitInvItem(INVENTORY_ITEM *const inv_item)\n{\n    inv_item->meshes_drawn = inv_item->meshes_sel;\n    inv_item->current_frame = 0;\n    inv_item->goal_frame = 0;\n    inv_item->manual_rot = g_IDMatrix;\n    inv_item->x_rot_pt = 0;\n    inv_item->prev_x_rot_pt = inv_item->x_rot_pt;\n    inv_item->x_rot = inv_item->x_rot_nosel;\n    inv_item->prev_x_rot = inv_item->x_rot;\n    inv_item->y_rot = 0;\n    inv_item->prev_y_rot = inv_item->y_rot;\n    inv_item->y_trans = 0;\n    inv_item->prev_y_trans = inv_item->y_trans;\n    inv_item->z_trans = 0;\n    inv_item->prev_z_trans = inv_item->z_trans;\n    inv_item->action = ACTION_USE;\n    inv_item->prev_manual_rot = inv_item->manual_rot;\n    if (inv_item->object_id == O_PASSPORT_OPTION) {\n        inv_item->object_id = O_PASSPORT_CLOSED;\n    }\n}\n\nvoid InvRing_GetView(\n    const INV_RING *const ring, XYZ_32 *const out_pos, XYZ_16 *const out_rot)\n{\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        -ring->camera.pos.x, M_CAMERA_Y_OFFSET - ring->camera.pos.y,\n        ring->radius - ring->camera.pos.z, angles);\n    out_pos->x = ring->camera.pos.x;\n    out_pos->y = ring->camera.pos.y;\n    out_pos->z = ring->camera.pos.z;\n    out_rot->x = angles[1] + ring->camera_pitch;\n    out_rot->y = angles[0];\n    out_rot->z = 0;\n}\n\nvoid InvRing_Light(const INV_RING *const ring)\n{\n    int16_t angles[2];\n    Math_GetVectorAngles(ring->light.x, ring->light.y, ring->light.z, angles);\n    Output_SetLightDivider(0x6000);\n    Output_RotateLight(angles[1], angles[0]);\n\n    if (g_TRVersion >= 3) {\n        // OG Inv_RingLight() LightCol columns are (sun, spot, dynamic):\n        // sun = (3312, 1664, 0);\n        // spot = (3312, 3312, 3312);\n        // dynamic = (0, 0, 3072) with an ambient of (32, 32, 32).\n        const float ambient_u8 = 32.0f / 255.0f;\n        const RGB_F ambient = { ambient_u8, ambient_u8, ambient_u8 };\n        const RGB_F colors[3] = {\n            {\n                .r = 3312.0f / 4096.0f,\n                .g = 1664.0f / 4096.0f,\n                .b = 0.0f,\n            },\n            {\n                .r = 3312.0f / 4096.0f,\n                .g = 3312.0f / 4096.0f,\n                .b = 3312.0f / 4096.0f,\n            },\n            {\n                .r = 0.0f,\n                .g = 0.0f,\n                .b = 3072.0f / 4096.0f,\n            },\n        };\n        const XYZ_32 dirs_view[3] = {\n            M_VectorViewFromWorld((XYZ_32) {\n                .x = 0x4000,\n                .y = -0x4000,\n                .z = 0x3000,\n            }),\n            M_VectorViewFromWorld((XYZ_32) {\n                .x = -0x4000,\n                .y = -0x4000,\n                .z = 0x3000,\n            }),\n            M_VectorViewFromWorld(\n                (XYZ_32) { .x = 0, .y = 0x2000, .z = 0x3000 }),\n        };\n        Output_SetTR3Light(ambient, colors, dirs_view);\n    }\n}\n\nvoid InvRing_CalcAdders(INV_RING *const ring, const int16_t rotation_duration)\n{\n    ring->angle_adder = DEG_360 / ring->number_of_objects;\n    ring->rot_adder_l = ring->angle_adder / rotation_duration;\n    ring->rot_adder_r = -ring->angle_adder / rotation_duration;\n}\n\nvoid InvRing_DoMotions(INV_RING *const ring)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n\n    if (ring->status_frames != 0) {\n        ring->radius += motion->radius_rate;\n        ring->camera.pos.y += motion->camera_y_rate;\n        ring->ring_pos.rot.y += motion->rotate_rate;\n        ring->camera_pitch += motion->camera_pitch_rate;\n\n        INVENTORY_ITEM *const inv_item = ring->list[ring->current_object];\n        inv_item->x_rot_pt += motion->item_pt_x_rot_rate;\n        inv_item->x_rot += motion->item_x_rot_rate;\n        inv_item->y_trans += motion->item_y_trans_rate;\n        inv_item->z_trans += motion->item_z_trans_rate;\n\n        ring->status_frames--;\n        if (ring->status_frames == 0) {\n            ring->status = ring->status_target;\n\n            if (motion->radius_rate != 0) {\n                motion->radius_rate = 0;\n                ring->radius = motion->radius_target;\n            }\n            if (motion->camera_y_rate != 0) {\n                motion->camera_y_rate = 0;\n                ring->camera.pos.y = motion->camera_y_target;\n            }\n            if (motion->rotate_rate != 0) {\n                motion->rotate_rate = 0;\n                ring->ring_pos.rot.y = motion->rotate_target;\n            }\n            if (motion->item_pt_x_rot_rate != 0) {\n                motion->item_pt_x_rot_rate = 0;\n                inv_item->x_rot_pt = motion->item_pt_x_rot_target;\n            }\n            if (motion->item_x_rot_rate != 0) {\n                motion->item_x_rot_rate = 0;\n                inv_item->x_rot = motion->item_x_rot_target;\n            }\n            if (motion->item_y_trans_rate != 0) {\n                motion->item_y_trans_rate = 0;\n                inv_item->y_trans = motion->item_y_trans_target;\n            }\n            if (motion->item_z_trans_rate != 0) {\n                motion->item_z_trans_rate = 0;\n                inv_item->z_trans = motion->item_z_trans_target;\n            }\n            if (motion->camera_pitch_rate != 0) {\n                motion->camera_pitch_rate = 0;\n                ring->camera_pitch = motion->camera_pitch_target;\n            }\n        }\n    }\n\n    if (ring->rotating) {\n        ring->ring_pos.rot.y += ring->rot_adder;\n        ring->rot_count--;\n\n        if (ring->rot_count == 0) {\n            ring->current_object = ring->target_object;\n            ring->ring_pos.rot.y =\n                -DEG_90 - ring->target_object * ring->angle_adder;\n            ring->rotating = false;\n        }\n    }\n}\n\nvoid InvRing_RotateLeft(INV_RING *const ring)\n{\n    ring->rotating = true;\n    ring->rotate_from_object = ring->current_object;\n    if (ring->current_object <= 0) {\n        ring->target_object = ring->number_of_objects - 1;\n    } else {\n        ring->target_object = ring->current_object - 1;\n    }\n    ring->rotate_to_object = ring->target_object;\n    ring->rot_count = INV_RING_ROTATE_DURATION;\n    ring->rot_adder = ring->rot_adder_l;\n}\n\nvoid InvRing_RotateRight(INV_RING *const ring)\n{\n    ring->rotating = true;\n    ring->rotate_from_object = ring->current_object;\n    if (ring->current_object + 1 >= ring->number_of_objects) {\n        ring->target_object = 0;\n    } else {\n        ring->target_object = ring->current_object + 1;\n    }\n    ring->rotate_to_object = ring->target_object;\n    ring->rot_count = INV_RING_ROTATE_DURATION;\n    ring->rot_adder = ring->rot_adder_r;\n}\n\nvoid InvRing_SetStatusTransition(\n    INV_RING *const ring, const RING_STATUS status,\n    const RING_STATUS status_target, const int16_t frames)\n{\n    INV_RING_MOTION *const motion = &ring->motion;\n    ring->status_frames = frames;\n    ring->status = status;\n    ring->status_target = status_target;\n    motion->radius_rate = 0;\n    motion->camera_y_rate = 0;\n\n    const INVENTORY_ITEM *const inv_item = ring->list[ring->current_object];\n\n    switch (status) {\n    case RNG_OPENING:\n        M_MotionRadius(ring, INV_RING_RADIUS);\n        ring->camera_pitch = -ring->motion.misc;\n        ring->motion.camera_pitch_rate =\n            ring->motion.misc / (M_RING_SWITCH_FRAMES / 2);\n        ring->motion.camera_pitch_target = 0;\n        InvRing_CalcAdders(ring, INV_RING_ROTATE_DURATION);\n        M_MotionRotation(\n            ring, INV_RING_OPEN_ROTATION,\n            -DEG_90 - ring->angle_adder * ring->current_object);\n        ring->ring_pos.rot.y =\n            ring->motion.rotate_target + INV_RING_OPEN_ROTATION;\n        break;\n\n    case RNG_CLOSING:\n        M_MotionRadius(ring, 0);\n\n        switch (status_target) {\n        case RNG_DONE:\n        case RNG_FADING_OUT:\n            M_MotionCameraPos(ring, INV_RING_CAMERA_START_HEIGHT);\n            break;\n        case RNG_MAIN2KEYS:\n        case RNG_OPTION2MAIN:\n            M_MotionCameraPitch(ring, DEG_45);\n            break;\n        case RNG_MAIN2OPTION:\n        case RNG_KEYS2MAIN:\n            M_MotionCameraPitch(ring, -DEG_45);\n            break;\n        default:\n            break;\n        }\n\n        M_MotionRotation(\n            ring, INV_RING_CLOSE_ROTATION,\n            ring->ring_pos.rot.y - INV_RING_CLOSE_ROTATION);\n        break;\n\n    case RNG_SELECTING:\n        M_MotionRotation(\n            ring, 0, -DEG_90 - ring->angle_adder * ring->current_object);\n        M_MotionItemSelect(ring, inv_item);\n        break;\n\n    case RNG_DESELECT:\n    case RNG_EXITING_INVENTORY:\n        M_MotionItemDeselect(ring, inv_item);\n        break;\n\n    case RNG_DESELECTING:\n        M_MotionRotation(\n            ring, 0, -DEG_90 - ring->angle_adder * ring->current_object);\n        break;\n\n    default:\n        break;\n    }\n}\n\nvoid InvRing_SelectMeshes(INVENTORY_ITEM *const inv_item)\n{\n    switch (inv_item->object_id) {\n    case O_PASSPORT_OPTION: {\n        struct {\n            int32_t frame;\n            uint32_t meshes;\n        } frame_map[] = {\n            { 14, PASS_MESH_IN_FRONT | PASS_MESH_PAGE_1 },\n            { 18, PASS_MESH_IN_FRONT | PASS_MESH_PAGE_1 | PASS_MESH_PAGE_2 },\n            { 19, PASS_MESH_PAGE_1 | PASS_MESH_PAGE_2 },\n            { 23, PASS_MESH_PAGE_1 | PASS_MESH_PAGE_2 | PASS_MESH_IN_BACK },\n            { 28, PASS_MESH_PAGE_2 | PASS_MESH_IN_BACK },\n            { 29, 0 },\n            { -1, -1 }, // sentinel\n        };\n\n        for (int32_t i = 0; frame_map[i].frame != -1; i++) {\n            if (inv_item->current_frame <= frame_map[i].frame) {\n                inv_item->meshes_drawn = PASS_MESH_COMMON | frame_map[i].meshes;\n                break;\n            }\n        }\n        break;\n    }\n\n    case O_COMPASS_OPTION:\n    case O_STOPWATCH_OPTION:\n        if (inv_item->current_frame == 0 || inv_item->current_frame >= 18) {\n            inv_item->meshes_drawn = inv_item->meshes_sel;\n        } else {\n            inv_item->meshes_drawn = -1;\n        }\n        break;\n\n    default:\n        inv_item->meshes_drawn = -1;\n        break;\n    }\n}\n\nvoid InvRing_ShowItemName(const INVENTORY_ITEM *const inv_item)\n{\n    if (inv_item->object_id == O_PASSPORT_OPTION\n        || inv_item->object_id == O_GLOBE_SELECT_OPTION) {\n        return;\n    }\n    Overlay_SetBottomText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_OBJECT_NAME,\n        .object_id = inv_item->object_id,\n        .fmt_gs_key = GS_ID(\"general/inventory_ring/object_name_fmt\"),\n    });\n}\n\nvoid InvRing_ShowItemQuantity(const char *const fmt, const int32_t qty)\n{\n    const char *const full_fmt =\n        String_FormatStatic(GS(\"general/inventory_ring/item_count_fmt\"), fmt);\n    String_FormatInto(&m_CountText, &m_CountTextCap, full_fmt, qty);\n}\n\nvoid InvRing_SetButtonHintDrawer(void (*draw_func)(void *), void *user_data)\n{\n    m_ButtonHintDrawFunc = draw_func;\n    m_ButtonHintUserData = user_data;\n}\n\nvoid InvRing_ClearButtonHint(void)\n{\n    InvRing_SetButtonHintDrawer(nullptr, nullptr);\n}\n\nvoid InvRing_ShowExamine(const OBJECT_ID object_id, const bool show)\n{\n    m_ShowExamine = show;\n    m_ShowUseItemButton = show;\n    if (show) {\n        switch (object_id) {\n        case O_QUEST_ITEM_1:\n        case O_QUEST_ITEM_2:\n        case O_QUEST_ITEM_3:\n        case O_QUEST_ITEM_4:\n        case O_QUEST_OPTION_1:\n        case O_QUEST_OPTION_2:\n        case O_QUEST_OPTION_3:\n        case O_QUEST_OPTION_4:\n        case O_PICKUP_OPTION_1:\n        case O_PICKUP_OPTION_2:\n            m_ShowUseItemButton = false;\n            break;\n        default:\n            break;\n        }\n        InvRing_SetButtonHintDrawer(M_DrawExamineHint, nullptr);\n    } else if (m_ButtonHintDrawFunc == M_DrawExamineHint) {\n        InvRing_ClearButtonHint();\n    }\n}\n\nvoid InvRing_DrawUI(INV_RING *const ring)\n{\n    UI_BeginModal(0.5f, 1.0f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_CENTER },\n        .spacing = { .v = 20.0f },\n    });\n\n    if (m_ButtonHintDrawFunc != nullptr) {\n        m_ButtonHintDrawFunc(m_ButtonHintUserData);\n    }\n\n    if (m_CountText != nullptr && m_CountText[0] != '\\0') {\n        UI_BeginOffset(64.0f, 0.0f);\n        UI_Label(m_CountText);\n        UI_EndOffset();\n    }\n    UI_Spacer(0.0f, 50.0f);\n    UI_EndStack();\n    UI_EndModal();\n}\n\nvoid InvRing_RemoveItemTexts(void)\n{\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n    if (m_CountText != nullptr) {\n        strcpy(m_CountText, \"\");\n    }\n}\n\nvoid InvRing_ShowHeader(INV_RING *const ring)\n{\n    if (ring->mode == INV_TITLE_MODE) {\n        return;\n    }\n\n    switch (ring->type) {\n    case RT_MAIN:\n        Overlay_SetTopText((OVERLAY_TEXT) {\n            .kind = UI_OVERLAY_TEXT_GS_KEY,\n            .gs_key = GS_ID(\"general/inventory_ring/heading_inventory\"),\n            .fmt_gs_key = GS_ID(\"general/inventory_ring/heading_fmt\"),\n        });\n        break;\n    case RT_OPTION:\n        if (ring->mode == INV_DEATH_MODE) {\n            Overlay_SetTopText((OVERLAY_TEXT) {\n                .kind = UI_OVERLAY_TEXT_GS_KEY,\n                .gs_key = GS_ID(\"general/inventory_ring/heading_game_over\"),\n                .fmt_gs_key = GS_ID(\"general/inventory_ring/heading_fmt\"),\n            });\n        } else {\n            Overlay_SetTopText((OVERLAY_TEXT) {\n                .kind = UI_OVERLAY_TEXT_GS_KEY,\n                .gs_key = GS_ID(\"general/inventory_ring/heading_option\"),\n                .fmt_gs_key = GS_ID(\"general/inventory_ring/heading_fmt\"),\n            });\n        }\n        break;\n    case RT_KEYS:\n        Overlay_SetTopText((OVERLAY_TEXT) {\n            .kind = UI_OVERLAY_TEXT_GS_KEY,\n            .gs_key = GS_ID(\"general/inventory_ring/heading_items\"),\n            .fmt_gs_key = GS_ID(\"general/inventory_ring/heading_fmt\"),\n        });\n        break;\n    case RT_GLOBE_SELECT:\n        break;\n    case RT_NUMBER_OF:\n        break;\n    }\n\n    if (ring->mode != INV_GAME_MODE) {\n        return;\n    }\n\n    const bool show_up_arrow = ring->type == RT_OPTION\n        || (ring->type == RT_MAIN && g_InvRing_Source[RT_KEYS].count > 0);\n    const bool show_bottom_arrow = ring->type == RT_KEYS\n        || (ring->type == RT_MAIN && !InvRing_IsOptionLockedOut());\n\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_TL, show_up_arrow);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_TR, show_up_arrow);\n\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BL, show_bottom_arrow);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BR, show_bottom_arrow);\n}\n\nvoid InvRing_RemoveHeader(void)\n{\n    Overlay_SetTopText((OVERLAY_TEXT) { 0 });\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_TL, false);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_TR, false);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BL, false);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BR, false);\n}\n\nbool InvRing_CanExamine(void)\n{\n    return g_Config.gameplay.enable_item_examining && m_ShowExamine;\n}\n\nvoid InvRing_ShowVersionText(void)\n{\n    Overlay_ShowVersion(true);\n}\n\nvoid InvRing_RemoveVersionText(void)\n{\n    Overlay_ShowVersion(false);\n}\n\nvoid InvRing_UpdateInventoryItem(\n    const INV_RING *const ring, INVENTORY_ITEM *const inv_item)\n{\n\n    if (inv_item != ring->list[ring->current_object]) {\n        if (inv_item->y_rot < 0) {\n            inv_item->y_rot += 256;\n        } else if (inv_item->y_rot > 0) {\n            inv_item->y_rot -= 256;\n        }\n    } else if (ring->rotating) {\n        if (inv_item->y_rot > 0) {\n            inv_item->y_rot -= 512;\n        } else if (inv_item->y_rot < 0) {\n            inv_item->y_rot += 512;\n        }\n    } else if (\n        ring->status == RNG_SELECTED || ring->status == RNG_DESELECTING\n        || ring->status == RNG_SELECTING || ring->status == RNG_DESELECT\n        || ring->status == RNG_CLOSING_ITEM) {\n\n        if (inv_item->has_manual_rot) {\n            return;\n        }\n\n        M_AdjustRot(&inv_item->y_rot, inv_item->y_rot_sel);\n        Matrix_Slerp3x3_M(\n            &inv_item->manual_rot, &g_IDMatrix, M_MANUAL_ROT_RESET_RATE);\n    } else if (\n        ring->number_of_objects == 1\n        || (!g_Input.menu_right && !g_Input.menu_left)) {\n        inv_item->y_rot += 256;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/inventory_ring/priv.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n#include <trx/game/inventory_ring/types.h>\n\n#define INV_RING_FRAMES 2\n#define INV_RING_CLOSE_FRAMES 32\n#define INV_RING_CLOSE_ROTATION -DEG_180\n#define INV_RING_OPEN_ROTATION -DEG_180\n#define INV_RING_ROTATE_DURATION 24\n#define INV_RING_OPEN_FRAMES 32\n#define INV_RING_CAMERA_HEIGHT (-0x100) // = -256\n#define INV_RING_CAMERA_START_HEIGHT (-0x600) // = -1536\n#define INV_RING_RADIUS 688\n\ntypedef enum {\n    INV_RING_ARROW_TL,\n    INV_RING_ARROW_TR,\n    INV_RING_ARROW_BL,\n    INV_RING_ARROW_BR,\n} INV_RING_ARROW;\n\nvoid InvRing_InitRing(\n    INV_RING *ring, RING_TYPE type, INVENTORY_ITEM **list, int16_t qty,\n    int16_t current);\nvoid InvRing_InitInvItem(INVENTORY_ITEM *inv_item);\n\nvoid InvRing_GetView(const INV_RING *ring, XYZ_32 *out_pos, XYZ_16 *out_rot);\nvoid InvRing_Light(const INV_RING *ring);\nvoid InvRing_CalcAdders(INV_RING *ring, int16_t rotation_duration);\nvoid InvRing_DoMotions(INV_RING *ring);\nvoid InvRing_RotateLeft(INV_RING *ring);\nvoid InvRing_RotateRight(INV_RING *ring);\n\nvoid InvRing_SetStatusTransition(\n    INV_RING *ring, RING_STATUS status, RING_STATUS status_target,\n    int16_t frames);\n\nvoid InvRing_ShowItemName(const INVENTORY_ITEM *inv_item);\nvoid InvRing_ShowItemQuantity(const char *fmt, int32_t qty);\nvoid InvRing_RemoveItemTexts(void);\nvoid InvRing_SelectMeshes(INVENTORY_ITEM *inv_item);\nvoid InvRing_ShowHeader(INV_RING *ring);\nvoid InvRing_RemoveHeader(void);\nvoid InvRing_SetButtonHintDrawer(void (*draw_func)(void *), void *user_data);\nvoid InvRing_ClearButtonHint(void);\nvoid InvRing_ShowExamine(OBJECT_ID object_id, bool show);\nbool InvRing_CanExamine(void);\nvoid InvRing_ShowVersionText(void);\nvoid InvRing_RemoveVersionText(void);\nvoid InvRing_DrawUI(INV_RING *ring);\n\nvoid InvRing_UpdateInventoryItem(\n    const INV_RING *ring, INVENTORY_ITEM *inv_item);\n\nbool InvRing_IsOptionLockedOut(void);\n"
  },
  {
    "path": "src/trx/game/inventory_ring/types.h",
    "content": "#pragma once\n\n#include <trx/config/enum.h>\n#include <trx/game/fader.h>\n#include <trx/game/inventory_ring/enum.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects/types.h>\n\n#include <stdint.h>\n\n#define MAX_QTY 999999\n#define MAX_GLOBE_ZONES 6\n\ntypedef struct {\n    int16_t shape;\n    XYZ_16 pos;\n    int32_t param1;\n    int32_t param2;\n    int16_t sprite_num;\n} INVENTORY_SPRITE;\n\ntypedef enum {\n    ACTION_USE = 0,\n    ACTION_EXAMINE = 1,\n} INVENTORY_ITEM_ACTION;\n\ntypedef struct {\n    OBJECT_ID object_id;\n    int16_t frames_total;\n    int16_t current_frame;\n    int16_t goal_frame;\n    int16_t open_frame;\n    int16_t anim_direction;\n    int16_t anim_speed;\n    int16_t anim_count;\n    int16_t x_rot_pt_sel;\n    int16_t x_rot_pt;\n    int16_t x_rot_sel;\n    int16_t x_rot_nosel;\n    int16_t x_rot;\n    int16_t y_rot_sel;\n    int16_t y_rot;\n    int16_t prev_y_rot;\n    int32_t y_trans_sel;\n    int32_t y_trans;\n    int32_t prev_y_trans;\n    int32_t z_trans_sel;\n    int32_t z_trans;\n    int32_t prev_z_trans;\n    int16_t prev_x_rot_pt;\n    int16_t prev_x_rot;\n    MATRIX manual_rot;\n    MATRIX prev_manual_rot;\n    bool has_manual_rot;\n    uint32_t meshes_sel;\n    uint32_t meshes_drawn;\n    int16_t inv_pos;\n    INVENTORY_ITEM_ACTION action;\n} INVENTORY_ITEM;\n\ntypedef struct {\n    int16_t radius_target;\n    int16_t radius_rate;\n    int16_t camera_y_target;\n    int16_t camera_y_rate;\n    int16_t camera_pitch_target;\n    int16_t camera_pitch_rate;\n    int16_t rotate_target;\n    int16_t rotate_rate;\n    int16_t item_pt_x_rot_target;\n    int16_t item_pt_x_rot_rate;\n    int16_t item_x_rot_target;\n    int16_t item_x_rot_rate;\n    int32_t item_y_trans_target;\n    int32_t item_y_trans_rate;\n    int32_t item_z_trans_target;\n    int32_t item_z_trans_rate;\n    int32_t misc;\n} INV_RING_MOTION;\n\ntypedef struct {\n    int16_t current;\n    int16_t count;\n    int32_t qtys[24];\n    INVENTORY_ITEM *items[24];\n} INV_RING_SOURCE;\n\ntypedef struct {\n    INVENTORY_MODE mode;\n    INVENTORY_ITEM **list;\n    RING_TYPE type;\n    int16_t radius;\n    int16_t prev_radius;\n    int16_t camera_pitch;\n    int16_t prev_camera_pitch;\n    bool rotating;\n    int16_t rotate_from_object;\n    int16_t rotate_to_object;\n    int16_t rot_count;\n    int16_t current_object;\n    int16_t target_object;\n    int16_t number_of_objects;\n    RING_STATUS status;\n    RING_STATUS status_target;\n    int16_t status_frames;\n    int16_t angle_adder;\n    int16_t rot_adder;\n    int16_t rot_adder_l;\n    int16_t rot_adder_r;\n    struct {\n        XYZ_32 pos;\n        XYZ_16 rot;\n    } ring_pos;\n    int16_t prev_ring_rot_y;\n    struct {\n        XYZ_32 pos;\n        XYZ_16 rot;\n    } camera;\n    int16_t prev_camera_y;\n    XYZ_32 light;\n    INV_RING_MOTION motion;\n\n    bool is_demo_needed;\n    bool is_pass_open;\n    bool is_done;\n    bool has_spun_out;\n    int32_t old_fov;\n\n    FADER top_fader;\n    FADER back_fader;\n\n    BACKGROUND_TYPE background_style;\n    const char *background_path;\n    struct {\n        XYZ_16 rot;\n        int32_t selection;\n        uint32_t meshes_drawn;\n        bool confirmed;\n        bool selectable[MAX_GLOBE_ZONES];\n        int32_t start_level_num[MAX_GLOBE_ZONES];\n    } globe_select;\n} INV_RING;\n"
  },
  {
    "path": "src/trx/game/inventory_ring/vars.c",
    "content": "#include <trx/game/inventory_ring/vars.h>\n\n#include <trx/core/json/util/file.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/catalog/manager.h>\n#include <trx/game/shell.h>\n\nCAMERA_INFO g_InvRing_OldCamera = {};\nVECTOR *g_InvRing_Items = nullptr;\nINV_RING_SOURCE g_InvRing_Source[RT_NUMBER_OF] = {};\n\n__attribute__((constructor)) static void M_Init(void)\n{\n    g_InvRing_Items = Vector_Create(sizeof(INVENTORY_ITEM *));\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    for (int32_t i = 0; i < g_InvRing_Items->count; i++) {\n        INVENTORY_ITEM *const item =\n            *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i);\n        Memory_Free(item);\n    }\n    Vector_Free(g_InvRing_Items);\n    g_InvRing_Items = nullptr;\n}\n\nvoid InvRing_LoadVars(const char *const path)\n{\n#define L_READ_INT(key, target) target = JSON_ObjectGetInt(obj, key, target);\n\n    for (int32_t i = 0; i < g_InvRing_Items->count; i++) {\n        INVENTORY_ITEM *const item =\n            *(INVENTORY_ITEM **)Vector_Get(g_InvRing_Items, i);\n        Memory_Free(item);\n    }\n    Vector_Clear(g_InvRing_Items);\n    for (int32_t i = 0; i < RT_NUMBER_OF; i++) {\n        g_InvRing_Source[i].count = 0;\n    }\n\n    JSON_VALUE *const root = JSONFile_ReadEx(path, true);\n    JSON_ARRAY *const arr = JSON_ValueAsArray(root);\n    if (arr == nullptr) {\n        Shell_ExitSystemFmt(\"invalid inventory ring vars file: %s\", path);\n    }\n    ASSERT(g_InvRing_Items != nullptr);\n\n    for (size_t i = 0; i < arr->length; i++) {\n        JSON_OBJECT *const obj = JSON_ArrayGetObject(arr, i);\n        const char *const name =\n            JSON_ObjectGetString(obj, \"object_id\", JSON_INVALID_STRING);\n        CATALOG_ID id;\n        if (!Catalog_NameToEnum(CATALOG_OBJECTS, name, &id)) {\n            Shell_ExitSystemFmt(\"unknown object_id '%s' in %s\", name, path);\n        }\n        INVENTORY_ITEM *const item = Memory_Alloc(sizeof(*item));\n        item->object_id = id;\n        L_READ_INT(\"frames_total\", item->frames_total);\n        L_READ_INT(\"current_frame\", item->current_frame);\n        L_READ_INT(\"goal_frame\", item->goal_frame);\n        L_READ_INT(\"open_frame\", item->open_frame);\n        L_READ_INT(\"anim_direction\", item->anim_direction);\n        L_READ_INT(\"anim_speed\", item->anim_speed);\n        L_READ_INT(\"anim_count\", item->anim_count);\n        L_READ_INT(\"x_rot_pt_sel\", item->x_rot_pt_sel);\n        L_READ_INT(\"x_rot_pt\", item->x_rot_pt);\n        L_READ_INT(\"x_rot_sel\", item->x_rot_sel);\n        L_READ_INT(\"x_rot_nosel\", item->x_rot_nosel);\n        L_READ_INT(\"x_rot\", item->x_rot);\n        L_READ_INT(\"y_rot_sel\", item->y_rot_sel);\n        L_READ_INT(\"y_rot\", item->y_rot);\n        L_READ_INT(\"y_trans_sel\", item->y_trans_sel);\n        L_READ_INT(\"y_trans\", item->y_trans);\n        L_READ_INT(\"z_trans_sel\", item->z_trans_sel);\n        L_READ_INT(\"z_trans\", item->z_trans);\n        L_READ_INT(\"meshes_sel\", item->meshes_sel);\n        L_READ_INT(\"meshes_drawn\", item->meshes_drawn);\n        L_READ_INT(\"inv_pos\", item->inv_pos);\n        Vector_Add(g_InvRing_Items, &item);\n    }\n\n    JSON_ValueFree(root);\n#undef L_READ_INT\n}\n"
  },
  {
    "path": "src/trx/game/inventory_ring/vars.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n#include <trx/game/camera/types.h>\n#include <trx/game/inventory_ring/types.h>\n\nextern CAMERA_INFO g_InvRing_OldCamera;\nextern INV_RING_SOURCE g_InvRing_Source[RT_NUMBER_OF];\nextern VECTOR *g_InvRing_Items;\n\nvoid InvRing_LoadVars(const char *path);\n"
  },
  {
    "path": "src/trx/game/inventory_ring.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/control.h>\n#include <trx/game/inventory_ring/draw.h>\n#include <trx/game/inventory_ring/enum.h>\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/inventory_ring/vars.h>\n"
  },
  {
    "path": "src/trx/game/items/actions/common.c",
    "content": "#include <trx/game/items/actions.h>\n#include <trx/game/rooms.h>\n\nstatic void (*m_Routines[ITEM_ACTION_NUMBER_OF])(ITEM *item) = {};\nstatic int16_t m_FXType = 0;\n\nint16_t ItemAction_GetFXType(void)\n{\n    return m_FXType;\n}\n\nvoid ItemAction_Register(\n    const ITEM_TRX_ACTION action, void (*const action_func)(ITEM *item))\n{\n    m_Routines[action] = action_func;\n}\n\nvoid ItemAction_Run(const ITEM_TRX_ACTION action_id, ITEM *const item)\n{\n    if (action_id >= 0 && action_id < ITEM_ACTION_NUMBER_OF\n        && m_Routines[action_id] != nullptr) {\n        m_Routines[action_id](item);\n    }\n}\n\nstatic void M_RunWithFX(\n    const ITEM_TRX_ACTION action_id, ITEM *const item, const int16_t fx_type)\n{\n    m_FXType = fx_type;\n    ItemAction_Run(action_id, item);\n    m_FXType = 0;\n}\n\nvoid ItemAction_RunDirect(const ITEM_ACTION action_id, ITEM *const item)\n{\n    const ITEM_TRX_ACTION trx_id = ItemAction_FromGameID(action_id);\n    ItemAction_Run(trx_id, item);\n}\n\nvoid ItemAction_RunDirectWithFX(\n    const ITEM_ACTION action_id, ITEM *const item, const int16_t fx_type)\n{\n    const ITEM_TRX_ACTION trx_id = ItemAction_FromGameID(action_id);\n    M_RunWithFX(trx_id, item, fx_type);\n}\n\nvoid ItemAction_RunActive(void)\n{\n    const int32_t flip_effect = Room_GetFlipEffect();\n    if (flip_effect != -1) {\n        ItemAction_RunDirect(flip_effect, nullptr);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/items/actions/effects.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\nstatic void M_ChainBlock(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (g_Config.audio.fix_chainblock_secret_sound) {\n        if (flip_timer == 0) {\n            Sound_Effect(SFX_CHAINBLOCK_FX, nullptr, SPM_NORMAL);\n            Room_SetFlipTimer(1);\n            return;\n        }\n    }\n\n    if (flip_timer == 0) {\n        Sound_Effect(SFX_SECRET, nullptr, SPM_NORMAL);\n    }\n\n    if (flip_timer == 54) {\n        Sound_Effect(SFX_LARA_SPLASH, nullptr, SPM_NORMAL);\n        Room_SetFlipEffect(-1);\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_Flood(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (flip_timer > 4 * LOGIC_FPS) {\n        Room_SetFlipEffect(-1);\n        Room_IncrementFlipTimer(1);\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    XYZ_32 pos = {\n        .x = lara_item->pos.x,\n        .y = g_Camera.target.pos.y,\n        .z = lara_item->pos.z,\n    };\n    if (flip_timer >= LOGIC_FPS) {\n        pos.y += 100 * (flip_timer - LOGIC_FPS);\n    } else {\n        pos.y += 100 * (LOGIC_FPS - flip_timer);\n    }\n\n    Sound_Effect(SFX_FLOOD, &pos, SPM_ALWAYS);\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_Explosion(ITEM *const item)\n{\n    // TODO: unify\n    Sound_Effect(\n        g_TRVersion == 1 ? SFX_EXPLOSION_FX : SFX_EXPLOSION_1, nullptr,\n        SPM_NORMAL);\n    g_Camera.bounce = -75;\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Earthquake(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (flip_timer == 0) {\n        Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL);\n        g_Camera.bounce = -250;\n    } else if (flip_timer == 3) {\n        Sound_Effect(SFX_EARTHQUAKE_1, nullptr, SPM_NORMAL);\n    } else if (flip_timer == 35) {\n        Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL);\n    } else if (flip_timer == 20 || flip_timer == 50 || flip_timer == 70) {\n        Sound_Effect(SFX_EARTHQUAKE_2, nullptr, SPM_NORMAL);\n    }\n\n    if (flip_timer == 104) {\n        Room_SetFlipEffect(-1);\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_Flicker(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (flip_timer > 125) {\n        Room_FlipMap();\n        Room_SetFlipEffect(-1);\n    } else if (\n        flip_timer == 90 || flip_timer == 92 || flip_timer == 105\n        || flip_timer == 107) {\n        Room_FlipMap();\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_FloorShake(ITEM *item)\n{\n    const int32_t max_dist = WALL_L * 16; // = 0x4000\n    const int32_t max_bounce = 100;\n\n    if (item == nullptr) {\n        item = Lara_GetItem();\n    }\n\n    const int32_t dx = item->pos.x - g_Camera.pos.x;\n    const int32_t dy = item->pos.y - g_Camera.pos.y;\n    const int32_t dz = item->pos.z - g_Camera.pos.z;\n    const int32_t dist = SQUARE(dz) + SQUARE(dy) + SQUARE(dx);\n\n    if (ABS(dx) < max_dist && ABS(dy) < max_dist && ABS(dz) < max_dist) {\n        g_Camera.bounce =\n            max_bounce * (SQUARE(WALL_L) - dist / 256) / SQUARE(WALL_L);\n    }\n}\n\nstatic void M_RaisingBlock(ITEM *const item)\n{\n    Sound_Effect(SFX_RAISINGBLOCK_FX, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_PowerUp(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (flip_timer > LOGIC_FPS * 4) {\n        Room_SetFlipEffect(-1);\n    } else {\n        const XYZ_32 pos = {\n            .x = g_Camera.target.x,\n            .y = g_Camera.target.y + flip_timer * 100,\n            .z = g_Camera.target.z,\n        };\n        Sound_Effect(SFX_POWERUP_FX, &pos, SPM_NORMAL);\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_Stairs2Slope(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (flip_timer == 5) {\n        Sound_Effect(SFX_STAIRS_2_SLOPE_FX, nullptr, SPM_NORMAL);\n        Room_SetFlipEffect(-1);\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_DropSand(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    if (flip_timer > LOGIC_FPS * 4) {\n        Room_SetFlipEffect(-1);\n    } else {\n        if (flip_timer == 0) {\n            Sound_Effect(SFX_TRAPDOOR_OPEN, nullptr, SPM_NORMAL);\n        }\n        const XYZ_32 pos = {\n            .x = g_Camera.target.x,\n            .y = g_Camera.target.y + flip_timer * 100,\n            .z = g_Camera.target.z,\n        };\n        Sound_Effect(SFX_SAND_FX, &pos, SPM_NORMAL);\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_Chandelier(ITEM *const item)\n{\n    const int32_t flip_timer = Room_GetFlipTimer();\n    Sound_Effect(SFX_CHAIN_PULLEY, nullptr, SPM_NORMAL);\n    if (flip_timer >= LOGIC_FPS) {\n        Room_SetFlipEffect(-1);\n    }\n    Room_IncrementFlipTimer(1);\n}\n\nstatic void M_Rubble(ITEM *const item)\n{\n    Sound_Effect(SFX_MASSIVE_CRASH, nullptr, SPM_NORMAL);\n    g_Camera.bounce = -350;\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Piston(ITEM *const item)\n{\n    Sound_Effect(SFX_PULLEY_CRANE, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Curtain(ITEM *const item)\n{\n    Sound_Effect(SFX_CURTAIN, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_SetChange(ITEM *const item)\n{\n    Sound_Effect(SFX_STAGE_BACKDROP, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Statue(ITEM *const item)\n{\n    Sound_Effect(SFX_STONE_DOOR_SLIDE, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Boiler(ITEM *const item)\n{\n    Sound_Effect(SFX_BOILER, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_CameraShake(ITEM *const item)\n{\n    g_Camera.bounce = -350;\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_LoweringBlock(ITEM *const item)\n{\n    Sound_Effect(SFX_LOWERING_BLOCK, nullptr, SPM_NORMAL);\n    Room_SetFlipEffect(-1);\n}\n\nREGISTER_ITEM_ACTION(ITEM_ACTION_CHAIN_BLOCK, M_ChainBlock)\nREGISTER_ITEM_ACTION(ITEM_ACTION_FLOOD, M_Flood)\nREGISTER_ITEM_ACTION(ITEM_ACTION_EXPLOSION, M_Explosion)\nREGISTER_ITEM_ACTION(ITEM_ACTION_EARTHQUAKE, M_Earthquake)\nREGISTER_ITEM_ACTION(ITEM_ACTION_FLICKER, M_Flicker)\nREGISTER_ITEM_ACTION(ITEM_ACTION_FLOOR_SHAKE, M_FloorShake)\nREGISTER_ITEM_ACTION(ITEM_ACTION_RAISING_BLOCK, M_RaisingBlock)\nREGISTER_ITEM_ACTION(ITEM_ACTION_POWER_UP, M_PowerUp)\nREGISTER_ITEM_ACTION(ITEM_ACTION_STAIRS_TO_SLOPE, M_Stairs2Slope)\nREGISTER_ITEM_ACTION(ITEM_ACTION_DROP_SAND, M_DropSand)\nREGISTER_ITEM_ACTION(ITEM_ACTION_CHANDELIER, M_Chandelier)\nREGISTER_ITEM_ACTION(ITEM_ACTION_RUBBLE, M_Rubble)\nREGISTER_ITEM_ACTION(ITEM_ACTION_PISTON, M_Piston)\nREGISTER_ITEM_ACTION(ITEM_ACTION_CURTAIN, M_Curtain)\nREGISTER_ITEM_ACTION(ITEM_ACTION_SET_CHANGE, M_SetChange)\nREGISTER_ITEM_ACTION(ITEM_ACTION_STATUE, M_Statue)\nREGISTER_ITEM_ACTION(ITEM_ACTION_BOILER, M_Boiler)\nREGISTER_ITEM_ACTION(ITEM_ACTION_CAMERA_SHAKE, M_CameraShake)\nREGISTER_ITEM_ACTION(ITEM_ACTION_LOWERING_BLOCK, M_LoweringBlock)\n"
  },
  {
    "path": "src/trx/game/items/actions/footprint.c",
    "content": "#include <trx/game/fx/footprint.h>\n#include <trx/game/items/actions.h>\n\nstatic void M_Footprint(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    const bool is_left = ItemAction_GetFXType() == 0x4000;\n    FX_Footprint_Add(item, is_left);\n}\n\nREGISTER_ITEM_ACTION(ITEM_ACTION_FOOTPRINT, M_Footprint)\n"
  },
  {
    "path": "src/trx/game/items/actions/general.c",
    "content": "#include <trx/game/game.h>\n#include <trx/game/gym.h>\n#include <trx/game/items.h>\n#include <trx/game/rooms.h>\n\nstatic void M_FinishLevel(ITEM *const item)\n{\n    Game_SetIsLevelComplete(true);\n}\n\nstatic void M_FlipMap(ITEM *const item)\n{\n    Room_FlipMap();\n}\n\nstatic void M_AssaultStart(ITEM *const item)\n{\n    Gym_TrackManager_Start(GYM_TRACK_ASSAULT);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_AssaultStop(ITEM *const item)\n{\n    Gym_TrackManager_Stop(GYM_TRACK_ASSAULT);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_AssaultReset(ITEM *const item)\n{\n    Gym_TrackManager_Reset(GYM_TRACK_ASSAULT);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_AssaultFinished(ITEM *const item)\n{\n    Gym_TrackManager_Finish(GYM_TRACK_ASSAULT);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_AssaultPenalty8(ITEM *const item)\n{\n    Gym_TrackManager_AddPenaltySeconds(GYM_TRACK_ASSAULT, 8);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_AssaultPenalty30(ITEM *const item)\n{\n    Gym_TrackManager_AddPenaltySeconds(GYM_TRACK_ASSAULT, 30);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_RacetrackStart(ITEM *const item)\n{\n    Gym_TrackManager_Start(GYM_TRACK_QUAD);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_RacetrackReset(ITEM *const item)\n{\n    Gym_TrackManager_Stop(GYM_TRACK_QUAD);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_RacetrackFinished(ITEM *const item)\n{\n    Gym_TrackManager_Finish(GYM_TRACK_QUAD);\n    Room_SetFlipEffect(-1);\n}\n\nREGISTER_ITEM_ACTION(ITEM_ACTION_FINISH_LEVEL, M_FinishLevel)\nREGISTER_ITEM_ACTION(ITEM_ACTION_FLIP_MAP, M_FlipMap)\nREGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_RESET, M_AssaultReset)\nREGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_STOP, M_AssaultStop)\nREGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_START, M_AssaultStart)\nREGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_FINISHED, M_AssaultFinished)\nREGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_PENALTY_8, M_AssaultPenalty8)\nREGISTER_ITEM_ACTION(ITEM_ACTION_ASSAULT_PENALTY_30, M_AssaultPenalty30)\nREGISTER_ITEM_ACTION(ITEM_ACTION_RACETRACK_START, M_RacetrackStart)\nREGISTER_ITEM_ACTION(ITEM_ACTION_RACETRACK_RESET, M_RacetrackReset)\nREGISTER_ITEM_ACTION(ITEM_ACTION_RACETRACK_FINISHED, M_RacetrackFinished)\n"
  },
  {
    "path": "src/trx/game/items/actions/gym_tr3.c",
    "content": "#include <trx/game/game.h>\n#include <trx/game/items/actions.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/rooms.h>\n\nstatic int32_t m_ExerciseNumber = 0;\n\nstatic void M_PlayExerciseTrack(\n    const int32_t expected_num, const MUSIC_TRX_ID track)\n{\n    if (!Game_IsInGym()) {\n        m_ExerciseNumber = 0;\n        return;\n    }\n\n    if (m_ExerciseNumber == expected_num) {\n        Music_Play(track, MPM_ONCE);\n        m_ExerciseNumber++;\n    }\n}\n\nstatic void M_Exercise01(ITEM *const item)\n{\n    M_PlayExerciseTrack(0, MX_TR3_GYM_EXERCISE_01);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise02(ITEM *const item)\n{\n    M_PlayExerciseTrack(1, MX_TR3_GYM_EXERCISE_02);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise03(ITEM *const item)\n{\n    M_PlayExerciseTrack(2, MX_TR3_GYM_EXERCISE_03);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise04(ITEM *const item)\n{\n    M_PlayExerciseTrack(3, MX_TR3_GYM_EXERCISE_04);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise05(ITEM *const item)\n{\n    M_PlayExerciseTrack(4, MX_TR3_GYM_EXERCISE_05);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise06(ITEM *const item)\n{\n    M_PlayExerciseTrack(5, MX_TR3_GYM_EXERCISE_06);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise07(ITEM *const item)\n{\n    M_PlayExerciseTrack(6, MX_TR3_GYM_EXERCISE_07);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise08(ITEM *const item)\n{\n    M_PlayExerciseTrack(7, MX_TR3_GYM_EXERCISE_08);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise09(ITEM *const item)\n{\n    M_PlayExerciseTrack(8, MX_TR3_GYM_EXERCISE_09);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise10(ITEM *const item)\n{\n    M_PlayExerciseTrack(9, MX_TR3_GYM_EXERCISE_10);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise11(ITEM *const item)\n{\n    M_PlayExerciseTrack(10, MX_TR3_GYM_EXERCISE_11);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise12(ITEM *const item)\n{\n    M_PlayExerciseTrack(11, MX_TR3_GYM_EXERCISE_12);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise13(ITEM *const item)\n{\n    M_PlayExerciseTrack(12, MX_TR3_GYM_EXERCISE_13);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise14(ITEM *const item)\n{\n    M_PlayExerciseTrack(13, MX_TR3_GYM_EXERCISE_14);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise15(ITEM *const item)\n{\n    M_PlayExerciseTrack(14, MX_TR3_GYM_EXERCISE_15);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise16(ITEM *const item)\n{\n    M_PlayExerciseTrack(15, MX_TR3_GYM_EXERCISE_16);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise17(ITEM *const item)\n{\n    M_PlayExerciseTrack(16, MX_TR3_GYM_EXERCISE_17);\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise18_SurfaceOnly(ITEM *const item)\n{\n    if (!Game_IsInGym()) {\n        m_ExerciseNumber = 0;\n        Room_SetFlipEffect(-1);\n        return;\n    }\n\n    if (m_ExerciseNumber == 17\n        && Lara_GetLaraInfo()->water_status == LWS_SURFACE) {\n        Music_Play(MX_TR3_GYM_EXERCISE_18, MPM_ONCE);\n        m_ExerciseNumber++;\n    }\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_Exercise19_Reset(ITEM *const item)\n{\n    if (!Game_IsInGym()) {\n        m_ExerciseNumber = 0;\n        Room_SetFlipEffect(-1);\n        return;\n    }\n\n    if (m_ExerciseNumber == 18) {\n        Music_Play(MX_TR3_GYM_EXERCISE_19, MPM_ONCE);\n        m_ExerciseNumber = 0;\n    }\n    Room_SetFlipEffect(-1);\n}\n\nstatic void M_ResetExercises(ITEM *const item)\n{\n    m_ExerciseNumber = 0;\n    Room_SetFlipEffect(-1);\n}\n\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_1, M_Exercise01)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_2, M_Exercise02)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_3, M_Exercise03)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_4, M_Exercise04)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_5, M_Exercise05)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_6, M_Exercise06)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_7, M_Exercise07)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_8, M_Exercise08)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_9, M_Exercise09)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_10, M_Exercise10)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_11, M_Exercise11)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_12, M_Exercise12)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_13, M_Exercise13)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_14, M_Exercise14)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_15, M_Exercise15)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_16, M_Exercise16)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_17, M_Exercise17)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_18, M_Exercise18_SurfaceOnly)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_19, M_Exercise19_Reset)\nREGISTER_ITEM_ACTION(ITEM_ACTION_GYM_HINT_RESET, M_ResetExercises)\n"
  },
  {
    "path": "src/trx/game/items/actions/ids.c",
    "content": "#include <trx/game/catalog/manager.h>\n#include <trx/game/items.h>\n\nITEM_ACTION ItemAction_ToGameID(const ITEM_TRX_ACTION action)\n{\n    ITEM_ACTION out;\n    if (Catalog_EnumToGameID(CATALOG_ITEM_ACTIONS, action, &out)) {\n        return out;\n    }\n    return ITEM_ACTION_INVALID;\n}\n\nITEM_TRX_ACTION ItemAction_FromGameID(const ITEM_ACTION action)\n{\n    ITEM_TRX_ACTION out;\n    if (Catalog_GameIDToEnum(CATALOG_ITEM_ACTIONS, action, &out)) {\n        return out;\n    }\n    return ITEM_TRX_ACTION_INVALID;\n}\n"
  },
  {
    "path": "src/trx/game/items/actions/ids.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef enum {\n    ITEM_ACTION_INVALID = -1,\n} ITEM_ACTION;\n\ntypedef enum {\n    ITEM_TRX_ACTION_INVALID = -1,\n#define X_CATALOG_ID(enum_value) enum_value,\n#include <trx/game/catalog/item_actions.def>\n#undef X_CATALOG_ID\n    ITEM_ACTION_NUMBER_OF,\n} ITEM_TRX_ACTION;\n\nITEM_ACTION ItemAction_ToGameID(ITEM_TRX_ACTION action);\nITEM_TRX_ACTION ItemAction_FromGameID(ITEM_ACTION action);\n"
  },
  {
    "path": "src/trx/game/items/actions/items.c",
    "content": "#include <trx/game/lara.h>\n\nstatic void M_Turn180(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    item->rot.x = -item->rot.x;\n    item->rot.y += DEG_180;\n    if (item == Lara_GetItem()\n        && item->current_anim_state != LS(LS_ROLL_CONT)) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->move_angle += DEG_180;\n    }\n}\n\nstatic void M_Turn90(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    item->rot.y += DEG_90;\n}\n\nstatic void M_InvisibilityOn(ITEM *const item)\n{\n    if (item != nullptr) {\n        item->status = IS_INVISIBLE;\n    }\n}\n\nstatic void M_InvisibilityOff(ITEM *const item)\n{\n    if (item != nullptr) {\n        item->status = IS_ACTIVE;\n    }\n}\n\nstatic void M_ShadowOn(ITEM *const item)\n{\n    if (item != nullptr) {\n        item->enable_shadow = true;\n    }\n}\n\nstatic void M_ShadowOff(ITEM *const item)\n{\n    if (item != nullptr) {\n        item->enable_shadow = false;\n    }\n}\n\nstatic void M_DynamicLightOn(ITEM *const item)\n{\n    if (item != nullptr) {\n        item->dynamic_light = true;\n    }\n}\n\nstatic void M_DynamicLightOff(ITEM *const item)\n{\n    if (item != nullptr) {\n        item->dynamic_light = false;\n    }\n}\n\nstatic void M_SwapMeshes(ITEM *const item, const OBJECT_ID swap_id)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    const OBJECT *const obj_1 = Object_Get(item->object_id);\n    for (int32_t mesh_idx = 0; mesh_idx < obj_1->mesh_count; mesh_idx++) {\n        Object_SwapMesh(item->object_id, swap_id, mesh_idx);\n    }\n}\n\nstatic void M_SwapMeshesWithMeshSwap1(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    M_SwapMeshes(item, O_MESH_SWAP_1);\n}\n\nstatic void M_SwapMeshesWithMeshSwap2(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    M_SwapMeshes(item, O_MESH_SWAP_2);\n}\n\nstatic void M_SwapMeshesWithMeshSwap3(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    M_SwapMeshes(item, O_MESH_SWAP_3);\n}\n\nREGISTER_ITEM_ACTION(ITEM_ACTION_TURN_180, M_Turn180)\nREGISTER_ITEM_ACTION(ITEM_ACTION_TURN_90, M_Turn90)\nREGISTER_ITEM_ACTION(ITEM_ACTION_INVISIBILITY_ON, M_InvisibilityOn)\nREGISTER_ITEM_ACTION(ITEM_ACTION_INVISIBILITY_OFF, M_InvisibilityOff)\nREGISTER_ITEM_ACTION(ITEM_ACTION_SHADOW_ON, M_ShadowOn)\nREGISTER_ITEM_ACTION(ITEM_ACTION_SHADOW_OFF, M_ShadowOff)\nREGISTER_ITEM_ACTION(ITEM_ACTION_DYNAMIC_LIGHT_ON, M_DynamicLightOn)\nREGISTER_ITEM_ACTION(ITEM_ACTION_DYNAMIC_LIGHT_OFF, M_DynamicLightOff)\nREGISTER_ITEM_ACTION(\n    ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_1, M_SwapMeshesWithMeshSwap1)\nREGISTER_ITEM_ACTION(\n    ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_2, M_SwapMeshesWithMeshSwap2)\nREGISTER_ITEM_ACTION(\n    ITEM_ACTION_SWAP_MESHES_WITH_MESH_SWAP_3, M_SwapMeshesWithMeshSwap3)\n"
  },
  {
    "path": "src/trx/game/items/actions/lara.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/gun/smoke.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\nstatic void M_Normal(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->extra_anim = false;\n    item->current_anim_state = LS(LS_STOP);\n    item->goal_anim_state = LS(LS_STOP);\n    Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n    g_Camera.type = CAM_CHASE;\n    Viewport_AlterFOV(-1, FOV_MODE_GAME);\n}\n\nstatic void M_HandsFree(ITEM *const item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->gun_status = LGS_ARMLESS;\n}\n\nstatic void M_ToggleGun(\n    ITEM *const item, const LARA_MESH thigh_mesh_idx,\n    const LARA_MESH hand_mesh_idx)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    const bool armed =\n        Lara_Skin_GetEquipment(hand_mesh_idx)->type == EQUIPMENT_TYPE_WEAPON;\n    if (armed) {\n        Lara_Skin_SetGunEquipment(thigh_mesh_idx, LGT_PISTOLS);\n        Lara_Skin_SetGunEquipment(hand_mesh_idx, LGT_UNARMED);\n    } else {\n        Lara_Skin_SetGunEquipment(thigh_mesh_idx, LGT_UNARMED);\n        Lara_Skin_SetGunEquipment(hand_mesh_idx, LGT_PISTOLS);\n    }\n}\n\nstatic void M_ToggleRightGun(ITEM *const item)\n{\n    M_ToggleGun(item, LM_THIGH_R, LM_HAND_R);\n}\n\nstatic void M_ToggleLeftGun(ITEM *const item)\n{\n    M_ToggleGun(item, LM_THIGH_L, LM_HAND_L);\n}\n\nstatic void M_ShootRightGun(ITEM *const item)\n{\n    Lara_GetLaraInfo()->right_arm.flash_gun = 3;\n    if (g_TRVersion == 3) {\n        Spawn_GunShell(LGT_PISTOLS, true);\n        Gun_Smoke_OnFire(LGT_PISTOLS, true);\n    }\n}\n\nstatic void M_ShootLeftGun(ITEM *const item)\n{\n    Lara_GetLaraInfo()->left_arm.flash_gun = 3;\n    if (g_TRVersion == 3) {\n        Spawn_GunShell(LGT_PISTOLS, false);\n        Gun_Smoke_OnFire(LGT_PISTOLS, false);\n    }\n}\n\nstatic void M_ResetHair(ITEM *const item)\n{\n    Lara_Hair_Initialise();\n}\n\nstatic void M_Bubbles(ITEM *const item)\n{\n    // XXX: until we get RoboLara, it makes sense for her to breathe underwater\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara->water_status == LWS_CHEAT\n        && !Room_Get(lara_item->room_num)->flags.underwater) {\n        return;\n    }\n\n    const int32_t count = g_TRVersion == 3 ? (Random_GetControl() & 3) + 2\n                                           : (Random_GetDraw() * 3) / 0x8000;\n    if (count == 0) {\n        return;\n    }\n\n    Sound_Effect(SFX_LARA_BUBBLES, &lara_item->pos, SPM_UNDERWATER);\n\n    XYZ_32 offset = { .x = 0, .y = 0, .z = 50 };\n    Collide_GetJointAbsPosition(lara_item, &offset, LM_HEAD);\n\n    for (int32_t i = 0; i < count; i++) {\n        Spawn_Bubble(&offset, lara_item->room_num);\n    }\n}\n\nREGISTER_ITEM_ACTION(ITEM_ACTION_LARA_NORMAL, M_Normal)\nREGISTER_ITEM_ACTION(ITEM_ACTION_LARA_HANDS_FREE, M_HandsFree)\nREGISTER_ITEM_ACTION(ITEM_ACTION_LARA_DRAW_RIGHT_GUN, M_ToggleRightGun)\nREGISTER_ITEM_ACTION(ITEM_ACTION_LARA_DRAW_LEFT_GUN, M_ToggleLeftGun)\nREGISTER_ITEM_ACTION(ITEM_ACTION_LARA_SHOOT_RIGHT_GUN, M_ShootRightGun)\nREGISTER_ITEM_ACTION(ITEM_ACTION_LARA_SHOOT_LEFT_GUN, M_ShootLeftGun)\nREGISTER_ITEM_ACTION(ITEM_ACTION_RESET_HAIR, M_ResetHair)\nREGISTER_ITEM_ACTION(ITEM_ACTION_BUBBLES, M_Bubbles)\n"
  },
  {
    "path": "src/trx/game/items/actions.h",
    "content": "#pragma once\n\n#include <trx/game/items/actions/ids.h>\n#include <trx/game/items/types.h>\n\n#include <stdint.h>\n\nvoid ItemAction_Register(\n    ITEM_TRX_ACTION action, void (*action_func)(ITEM *item));\nvoid ItemAction_Run(ITEM_TRX_ACTION action_id, ITEM *item);\nvoid ItemAction_RunDirect(ITEM_ACTION action_id, ITEM *item);\nvoid ItemAction_RunDirectWithFX(\n    ITEM_ACTION action_id, ITEM *item, int16_t fx_type);\nvoid ItemAction_RunActive(void);\nint16_t ItemAction_GetFXType(void);\n\n#define REGISTER_ITEM_ACTION(action, action_func)                              \\\n    __attribute__((constructor)) static void M_RegisterActionHandler##action(  \\\n        void)                                                                  \\\n    {                                                                          \\\n        ItemAction_Register(action, action_func);                              \\\n    }\n"
  },
  {
    "path": "src/trx/game/items/anim.c",
    "content": "#include <trx/game/items/anim.h>\n\n#include <trx/config.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items/actions.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\n#define M_SFX_SURF_DISTANCE ((STEP_L * 2) + 1)\n#define M_FRAME_INTERP_SCALE 1024\n\nstatic bool M_ShouldPlaySFXAlways(\n    const ITEM *const item, const bool item_underwater)\n{\n    if (item == Lara_GetItem()) {\n        return true;\n    }\n\n    if (item->object_id == O_LARA_HARPOON_GUN) {\n        return true;\n    }\n\n    int16_t room_num = item->room_num;\n    if (room_num == NO_ROOM) {\n        return false;\n    }\n\n    const int32_t dist =\n        item_underwater ? -M_SFX_SURF_DISTANCE : +M_SFX_SURF_DISTANCE;\n    Room_GetSector(\n        (XYZ_32) { item->pos.x, item->pos.y + dist, item->pos.z }, &room_num);\n    const ROOM *const nearby_room = Room_Get(room_num);\n    const bool near_underwater = nearby_room->flags.underwater;\n    return item_underwater != near_underwater;\n}\n\nANIM *Item_GetAnim(const ITEM *const item)\n{\n    return Anim_GetAnim(item->anim_num);\n}\n\nbool Item_TestAnimEqual(const ITEM *const item, const int16_t anim_idx)\n{\n    return Item_TestObjAnimEqual(item, anim_idx, item->object_id);\n}\n\nbool Item_TestObjAnimEqual(\n    const ITEM *const item, const int16_t anim_idx, const OBJECT_ID obj_id)\n{\n    const OBJECT *const obj = Object_Get(obj_id);\n    return item->anim_num == obj->anim_idx + anim_idx;\n}\n\nint16_t Item_GetRelativeAnim(const ITEM *const item)\n{\n    return Item_GetRelativeObjAnim(item, item->object_id);\n}\n\nint16_t Item_GetRelativeObjAnim(const ITEM *const item, const OBJECT_ID obj_id)\n{\n    return item->anim_num - Object_Get(obj_id)->anim_idx;\n}\n\nint16_t Item_GetRelativeFrame(const ITEM *const item)\n{\n    return item->frame_num - Item_GetAnim(item)->frame_base;\n}\n\nvoid Item_SwitchToAnim(\n    ITEM *const item, const int16_t anim_idx, const int16_t frame)\n{\n    Item_SwitchToObjAnim(item, anim_idx, frame, item->object_id);\n}\n\nvoid Item_SwitchToObjAnim(\n    ITEM *const item, const int16_t anim_idx, const int16_t frame,\n    const OBJECT_ID obj_id)\n{\n    const OBJECT *const obj = Object_Get(obj_id);\n    if (obj->anim_idx == NO_ANIM) {\n        item->anim_num = NO_ANIM;\n    } else {\n        item->anim_num = obj->anim_idx + anim_idx;\n    }\n\n    const ANIM *const anim = Item_GetAnim(item);\n    if (frame < 0) {\n        item->frame_num = anim->frame_end + frame + 1;\n    } else {\n        item->frame_num = anim->frame_base + frame;\n    }\n}\n\nbool Item_TestFrameEqual(const ITEM *const item, const int16_t frame)\n{\n    const ANIM *const anim = Item_GetAnim(item);\n    const int16_t base_frame =\n        frame < 0 ? (anim->frame_end + 1) : anim->frame_base;\n    return Anim_TestAbsFrameEqual(item->frame_num, base_frame + frame);\n}\n\nbool Item_TestFrameRange(\n    const ITEM *const item, const int16_t start, const int16_t end)\n{\n    return Anim_TestAbsFrameRange(\n        item->frame_num, Item_GetAnim(item)->frame_base + start,\n        Item_GetAnim(item)->frame_base + end);\n}\n\nANIM_FRAME *Item_GetBestFrame(const ITEM *const item)\n{\n    ANIM_FRAME *frames[2];\n    int32_t rate = 0;\n    const int32_t frac = Item_GetFrames(item, frames, &rate);\n    return frames[(frac > rate / 2) ? 1 : 0];\n}\n\nint32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frames[], int32_t *rate)\n{\n    const ANIM *const anim = Item_GetAnim(item);\n    if (anim->frame_ptr == nullptr) {\n        frames[0] = nullptr;\n        return 0;\n    }\n\n    const int32_t cur_frame_num = item->frame_num - anim->frame_base;\n    const int32_t last_frame_num = anim->frame_end - anim->frame_base;\n    const int32_t key_frame_span = anim->interpolation;\n    const int32_t first_key_frame_num = cur_frame_num / key_frame_span;\n    const int32_t second_key_frame_num = first_key_frame_num + 1;\n\n    int32_t interp_frame_num = cur_frame_num;\n    double interp_frame_sub = 0.0;\n    const double alpha = Interpolation_GetWorldRate();\n    if (alpha >= 0.0 && alpha <= 1.0) {\n        const bool prev_in_anim = item->prev_frame_num >= anim->frame_base\n            && item->prev_frame_num <= anim->frame_end;\n        if (prev_in_anim) {\n            const int32_t prev_frame_num =\n                item->prev_frame_num - anim->frame_base;\n            const int32_t frame_delta = cur_frame_num - prev_frame_num;\n            if (frame_delta > 0) {\n                const OBJECT *const obj = Object_Get(item->object_id);\n                const bool allow_interp = obj->can_interpolate_func == nullptr\n                    || obj->can_interpolate_func(\n                        item, first_key_frame_num, second_key_frame_num);\n                if (allow_interp) {\n                    const double frame_pos =\n                        prev_frame_num + (frame_delta * alpha);\n                    if (frame_pos < last_frame_num) {\n                        interp_frame_num = (int32_t)frame_pos;\n                        interp_frame_sub = frame_pos - interp_frame_num;\n                    }\n                }\n            }\n        }\n    }\n\n    const int32_t key_frame_shift = interp_frame_num % key_frame_span;\n    const int32_t frame_a = interp_frame_num / key_frame_span;\n    const int32_t frame_b = frame_a + 1;\n    frames[0] = &anim->frame_ptr[frame_a];\n    frames[1] = &anim->frame_ptr[frame_b];\n\n    int32_t denominator = key_frame_span;\n    if (key_frame_shift != 0 || interp_frame_sub > 0.0) {\n        const int32_t second_key_frame_num2 =\n            (interp_frame_num / key_frame_span + 1) * key_frame_span;\n        if (second_key_frame_num2 > anim->frame_end) {\n            denominator += anim->frame_end - second_key_frame_num2;\n        }\n    }\n\n    const double numerator = key_frame_shift + interp_frame_sub;\n    *rate = denominator * M_FRAME_INTERP_SCALE;\n    return (int32_t)((numerator * M_FRAME_INTERP_SCALE) + 0.5);\n}\n\nvoid Item_Animate(ITEM *const item)\n{\n    item->hit_status = false;\n    item->touch_bits = 0;\n    item->prev_frame_num = item->frame_num;\n    item->frame_num++;\n\n    const ANIM *anim = Item_GetAnim(item);\n    if (anim->num_changes > 0 && Item_GetAnimChange(item, anim)) {\n        anim = Item_GetAnim(item);\n        item->current_anim_state = anim->current_anim_state;\n\n        if (item->required_anim_state == anim->current_anim_state) {\n            item->required_anim_state = 0;\n        }\n    }\n\n    if (item->frame_num > anim->frame_end) {\n        for (int32_t i = 0; i < anim->num_commands; i++) {\n            const ANIM_COMMAND *const command = &anim->commands[i];\n            switch (command->type) {\n            case AC_MOVE_ORIGIN: {\n                const XYZ_16 *const pos = (XYZ_16 *)command->data;\n                Item_Translate(item, pos->x, pos->y, pos->z);\n                break;\n            }\n\n            case AC_JUMP_VELOCITY: {\n                const ANIM_COMMAND_VELOCITY_DATA *const data =\n                    (ANIM_COMMAND_VELOCITY_DATA *)command->data;\n                item->fall_speed = data->fall_speed;\n                item->speed = data->speed;\n                item->gravity = true;\n                break;\n            }\n\n            case AC_DEACTIVATE:\n                const OBJECT *const obj = Object_Get(item->object_id);\n                item->after_death = obj->intelligent ? 1 : 64;\n                item->status = IS_DEACTIVATED;\n                break;\n\n            default:\n                break;\n            }\n        }\n\n        item->anim_num = anim->jump_anim_num;\n        item->frame_num = anim->jump_frame_num;\n        anim = Item_GetAnim(item);\n\n        if (item->current_anim_state != anim->current_anim_state) {\n            item->current_anim_state = anim->current_anim_state;\n            item->goal_anim_state = anim->current_anim_state;\n        }\n\n        if (item->required_anim_state == item->current_anim_state) {\n            item->required_anim_state = 0;\n        }\n    }\n\n    for (int32_t i = 0; i < anim->num_commands; i++) {\n        const ANIM_COMMAND *const command = &anim->commands[i];\n        switch (command->type) {\n        case AC_SOUND_FX: {\n            const ANIM_COMMAND_EFFECT_DATA *const data =\n                (ANIM_COMMAND_EFFECT_DATA *)command->data;\n            Item_PlayAnimSFX(item, data);\n            break;\n        }\n\n        case AC_EFFECT:\n            const ANIM_COMMAND_EFFECT_DATA *const data =\n                (ANIM_COMMAND_EFFECT_DATA *)command->data;\n            if (item->frame_num == data->frame_num) {\n                ItemAction_RunDirectWithFX(\n                    data->effect_num, item, data->fx_type);\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    if (item->gravity) {\n        item->fall_speed += item->fall_speed < FAST_FALL_SPEED ? GRAVITY : 1;\n        item->pos.y += item->fall_speed;\n    } else {\n        int32_t speed = anim->velocity;\n        if (anim->acceleration != 0) {\n            speed += anim->acceleration * (item->frame_num - anim->frame_base);\n        }\n        item->speed = speed >> 16;\n    }\n\n    item->pos.x += (item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z += (item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n}\n\nbool Item_GetAnimChange(ITEM *const item, const ANIM *const anim)\n{\n    if (item->current_anim_state == item->goal_anim_state) {\n        return false;\n    }\n\n    for (int32_t i = 0; i < anim->num_changes; i++) {\n        const ANIM_CHANGE *const change = Anim_GetChange(anim->change_idx + i);\n        if (change->goal_anim_state != item->goal_anim_state) {\n            continue;\n        }\n\n        for (int32_t j = 0; j < change->num_ranges; j++) {\n            const ANIM_RANGE *const range =\n                Anim_GetRange(change->range_idx + j);\n\n            if (Anim_TestAbsFrameRange(\n                    item->frame_num, range->start_frame, range->end_frame)) {\n                item->anim_num = range->link_anim_num;\n                item->frame_num = range->link_frame_num;\n                return true;\n            }\n        }\n    }\n\n    return false;\n}\n\nvoid Item_PlayAnimSFX(\n    const ITEM *const item, const ANIM_COMMAND_EFFECT_DATA *const data)\n{\n    if (item->frame_num != data->frame_num) {\n        return;\n    }\n\n    const bool is_lara = item == Lara_GetItem();\n    const bool item_underwater =\n        item->room_num != NO_ROOM && Room_Get(item->room_num)->flags.underwater;\n    const ANIM_COMMAND_ENVIRONMENT mode = data->environment;\n\n    if (mode != ACE_ALL && item->room_num != NO_ROOM) {\n        int32_t height = NO_HEIGHT;\n        if (is_lara) {\n            height = Lara_GetLaraInfo()->water_surface_dist;\n        } else if (item_underwater) {\n            height = -STEP_L;\n        }\n\n        const bool in_water = height < 0 && height != NO_HEIGHT;\n        if ((mode == ACE_WATER && !in_water)\n            || (mode == ACE_LAND && in_water)) {\n            return;\n        }\n    }\n\n    const bool play_always = M_ShouldPlaySFXAlways(item, item_underwater);\n    SOUND_PLAY_MODE play_mode = SPM_NORMAL;\n    if (play_always) {\n        play_mode = SPM_ALWAYS;\n    } else if (\n        Object_IsType(item->object_id, g_WaterObjects)\n        || (g_Config.audio.enable_underwater_anim_sfx && item_underwater)) {\n        play_mode = SPM_UNDERWATER;\n    }\n\n    const SAMPLE_ID sfx_num =\n        is_lara ? Lara_Skin_GetAnimSFX(data->effect_num) : data->effect_num;\n    Sound_Effect_Direct(sfx_num, &item->pos, play_mode);\n}\n"
  },
  {
    "path": "src/trx/game/items/anim.h",
    "content": "#pragma once\n\n#include <trx/game/anims/types.h>\n#include <trx/game/items/types.h>\n\nANIM *Item_GetAnim(const ITEM *item);\nbool Item_TestAnimEqual(const ITEM *item, int16_t anim_idx);\nbool Item_TestObjAnimEqual(\n    const ITEM *item, int16_t anim_idx, OBJECT_ID obj_id);\nint16_t Item_GetRelativeAnim(const ITEM *item);\nint16_t Item_GetRelativeObjAnim(const ITEM *item, OBJECT_ID obj_id);\nint16_t Item_GetRelativeFrame(const ITEM *item);\nvoid Item_SwitchToAnim(ITEM *item, int16_t anim_idx, int16_t frame);\nvoid Item_SwitchToObjAnim(\n    ITEM *item, int16_t anim_idx, int16_t frame, OBJECT_ID obj_id);\n\n// Tests if the given item's current relative animation frame matches the\n// provided value. If a negative value is passed, the test is performed from the\n// end of the animation's frame set.\nbool Item_TestFrameEqual(const ITEM *item, int16_t frame);\nbool Item_TestFrameRange(const ITEM *item, int16_t start, int16_t end);\n\nANIM_FRAME *Item_GetBestFrame(const ITEM *item);\nint32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate);\n\nvoid Item_Animate(ITEM *item);\nbool Item_GetAnimChange(ITEM *item, const ANIM *anim);\nvoid Item_PlayAnimSFX(const ITEM *item, const ANIM_COMMAND_EFFECT_DATA *data);\n"
  },
  {
    "path": "src/trx/game/items/carrier.c",
    "content": "#include <trx/game/items/carrier.h>\n\n#include <trx/core/log.h>\n#include <trx/core/vector.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_flow/vars.h>\n#include <trx/game/inventory.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/game/rooms/utils.h>\n#include <trx/version.h>\n\n#define M_DROP_FAST_RATE GRAVITY\n#define M_DROP_SLOW_RATE 1\n#define M_DROP_FAST_TURN (DEG_1 * 5)\n#define M_DROP_SLOW_TURN (DEG_1 * 3)\n\nstatic int16_t m_AnimatingCount = 0;\n\nstatic const GAME_OBJECT_PAIR m_LegacyMap[] = {\n    { O_PIERRE, O_SCION_ITEM_2 }, { O_COWBOY, O_MAGNUM_ITEM },\n    { O_SKATEKID, O_UZI_ITEM },   { O_BALDY, O_SHOTGUN_ITEM },\n    { NO_OBJECT, NO_OBJECT },\n};\n\nstatic bool M_ShouldCenterDrop(const OBJECT_ID obj_id)\n{\n    switch (obj_id) {\n    case O_QUEST_ITEM_1:\n    case O_QUEST_ITEM_2:\n    case O_QUEST_ITEM_3:\n    case O_QUEST_ITEM_4:\n        return false;\n\n    default:\n        return g_TRVersion == 3;\n    }\n}\n\nstatic OBJECT_ID M_ConvertDroppedGun(const OBJECT_ID obj_id)\n{\n    if (g_GameFlow.convert_dropped_guns && Object_IsType(obj_id, g_GunObjects)\n        && Inv_RequestItem(obj_id) && obj_id != O_PISTOL_ITEM) {\n        return Object_GetCognate(obj_id, g_GunAmmoObjectMap);\n    }\n    return obj_id;\n}\n\nstatic ITEM *M_GetCarrier(const int16_t item_num)\n{\n    if (item_num < 0 || item_num >= Item_GetLevelCount()) {\n        return nullptr;\n    }\n\n    // Allow carried items to be allocated to holder objects (pods/statues),\n    // but then have those items dropped by the actual creatures within.\n    ITEM *item = Item_Get(item_num);\n    const OBJECT *obj = Object_Get(item->object_id);\n    if (obj->carrier_item_num_func != nullptr) {\n        const int16_t child_item_num = obj->carrier_item_num_func(item);\n        if (child_item_num == NO_ITEM) {\n            return nullptr;\n        }\n        item = Item_Get(child_item_num);\n    }\n\n    obj = Object_Get(item->object_id);\n    if (!obj->loaded) {\n        return nullptr;\n    }\n\n    return item;\n}\n\nstatic ITEM *M_EnsureCarriedPickupItem(\n    const ITEM *const carrier, CARRIED_ITEM *const carried_item)\n{\n    if (carried_item->spawn_num == NO_ITEM) {\n        return nullptr;\n    }\n\n    if (carried_item->spawn_num < Item_GetTotalCount()) {\n        return Item_Get(carried_item->spawn_num);\n    }\n\n    // Gameflow drops can reference runtime-spawned pickup indices that do not\n    // exist yet after a fresh level load. Re-spawn and rebind the index.\n    const int16_t spawn_num = Item_Spawn(carrier, carried_item->object_id);\n    if (spawn_num == NO_ITEM) {\n        carried_item->spawn_num = NO_ITEM;\n        return nullptr;\n    }\n    carried_item->spawn_num = spawn_num;\n    return Item_Get(carried_item->spawn_num);\n}\n\nstatic bool M_IsCarrierType(const OBJECT_ID obj_id)\n{\n    bool is_enemy = Object_IsType(obj_id, g_CreatureObjects);\n    // Eels are hostile but cannot be killed, so must be excluded. Monks may be\n    // allocated drop items whether or not they are hostile. Drop items must be\n    // assigned to the skidoo and not the rider to avoid issues with /kill, and\n    // O_DRAGON_BACK is the active dragon, but having this in g_CreatureObjects\n    // also creates issues with /kill, hence a separate check is required here.\n    is_enemy &= obj_id != O_EEL && obj_id != O_BIG_EEL;\n    is_enemy &= obj_id != O_SKIDOO_DRIVER;\n    is_enemy |= obj_id == O_DRAGON_BACK || obj_id == O_SKIDOO_ARMED;\n    return is_enemy;\n}\n\nstatic CARRIED_ITEM *M_GetFirstDropItem(const ITEM *const carrier)\n{\n    bool can_drop = carrier->hit_points <= 0;\n    const OBJECT *const obj = Object_Get(carrier->object_id);\n    if (obj->can_drop_items_func != nullptr) {\n        can_drop = obj->can_drop_items_func(carrier);\n    }\n    return can_drop ? carrier->carried_item : nullptr;\n}\n\nstatic void M_AnimateDrop(CARRIED_ITEM *const item)\n{\n    if (item->status != DS_FALLING) {\n        return;\n    }\n\n    ITEM *const pickup = Item_Get(item->spawn_num);\n    int16_t room_num = pickup->room_num;\n    // For cases where a flyer has dropped an item exactly on a portal, we need\n    // to ensure that the initial sector is in the room above, hence we test\n    // slightly above the initial y position.\n    const SECTOR *const sector = Room_GetSector(\n        (XYZ_32) { pickup->pos.x, pickup->pos.y - 10, pickup->pos.z },\n        &room_num);\n    const int32_t height = Room_GetHeight(sector, pickup->pos);\n    const bool in_water = Room_Get(pickup->room_num)->flags.underwater;\n\n    if (sector->portal_room.pit == NO_ROOM && pickup->pos.y >= height) {\n        item->status = DS_DROPPED;\n        pickup->status = IS_INACTIVE;\n        pickup->pos.y = height;\n        pickup->fall_speed = 0;\n        m_AnimatingCount--;\n    } else {\n        pickup->status = IS_ACTIVE;\n        pickup->fall_speed +=\n            (!in_water && pickup->fall_speed < FAST_FALL_SPEED)\n            ? M_DROP_FAST_RATE\n            : M_DROP_SLOW_RATE;\n        pickup->pos.y += pickup->fall_speed;\n        pickup->rot.y += in_water ? M_DROP_SLOW_TURN : M_DROP_FAST_TURN;\n\n        if (sector->portal_room.pit != NO_ROOM\n            && pickup->pos.y > sector->floor.height) {\n            room_num = sector->portal_room.pit;\n        }\n    }\n\n    Item_UpdateRoom(item->spawn_num, room_num);\n\n    // Track animating status in the carrier for saving/loading.\n    item->pos = pickup->pos;\n    item->rot = pickup->rot;\n    item->room_num = pickup->room_num;\n    item->fall_speed = pickup->fall_speed;\n}\n\nstatic void M_InitialiseDataDrops(void)\n{\n    VECTOR *const pickups = Vector_Create(sizeof(int16_t));\n\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const carrier = M_GetCarrier(i);\n        if (carrier == nullptr || !M_IsCarrierType(carrier->object_id)) {\n            continue;\n        }\n\n        const ROOM *const room = Room_Get(carrier->room_num);\n        int16_t pickup_num = room->item_num;\n        while (pickup_num != NO_ITEM) {\n            ITEM *const pickup = Item_Get(pickup_num);\n            if (Object_IsType(pickup->object_id, g_PickupObjects)\n                && XYZ_32_AreEquivalent(pickup->pos, carrier->pos)) {\n                Vector_Add(pickups, (void *)&pickup_num);\n                Item_RemoveDrawn(pickup_num);\n                pickup->room_num = NO_ROOM;\n            }\n\n            pickup_num = pickup->next_item;\n        }\n\n        if (pickups->count == 0) {\n            continue;\n        }\n\n        carrier->carried_item =\n            GameBuf_Alloc(sizeof(CARRIED_ITEM) * pickups->count, GBUF_ITEMS);\n        CARRIED_ITEM *drop = carrier->carried_item;\n        for (int32_t j = 0; j < pickups->count; j++) {\n            drop->spawn_num = *(const int16_t *)Vector_Get(pickups, j);\n            Item_RemoveDrawn(drop->spawn_num);\n            drop->room_num = NO_ROOM;\n            drop->fall_speed = 0;\n            drop->status = DS_CARRIED;\n\n            if (j < pickups->count - 1) {\n                drop->next_item = drop + 1;\n                drop++;\n            } else {\n                drop->next_item = nullptr;\n            }\n        }\n\n        Vector_Clear(pickups);\n    }\n\n    Vector_Free(pickups);\n}\n\nstatic void M_InitialiseGameFlowDrops(const GF_LEVEL *const level)\n{\n    int32_t total_item_count = Item_GetLevelCount();\n    for (int32_t i = 0; i < level->item_drops.count; i++) {\n        const GF_DROP_ITEM_DATA *const data = &level->item_drops.data[i];\n\n        ITEM *const item = M_GetCarrier(data->enemy_num);\n        if (!item) {\n            LOG_WARNING(\"%d does not refer to a loaded item\", data->enemy_num);\n            continue;\n        }\n\n        if (total_item_count + data->count > MAX_ITEMS) {\n            LOG_WARNING(\"Too many items being loaded\");\n            return;\n        }\n\n        if (item->carried_item) {\n            LOG_WARNING(\"Item %d is already carrying\", data->enemy_num);\n            continue;\n        }\n\n        if (!M_IsCarrierType(item->object_id)) {\n            LOG_WARNING(\n                \"Item %d of type %d cannot carry items\", data->enemy_num,\n                item->object_id);\n            continue;\n        }\n\n        if (data->count == 0) {\n            LOG_WARNING(\n                \"There are no drop items defined for enemy %d\",\n                data->enemy_num);\n            continue;\n        }\n\n        item->carried_item =\n            GameBuf_Alloc(sizeof(CARRIED_ITEM) * data->count, GBUF_ITEMS);\n        CARRIED_ITEM *drop = item->carried_item;\n        for (int32_t j = 0; j < data->count; j++) {\n            drop->object_id = data->object_ids[j];\n            drop->spawn_num = NO_ITEM;\n            drop->room_num = NO_ROOM;\n            drop->fall_speed = 0;\n\n            if (Object_IsType(drop->object_id, g_PickupObjects)) {\n                drop->status = DS_CARRIED;\n                total_item_count++;\n            } else {\n                LOG_WARNING(\n                    \"Items of type %d cannot be carried\", drop->object_id);\n                drop->object_id = NO_OBJECT;\n                drop->status = DS_COLLECTED;\n            }\n\n            if (j < data->count - 1) {\n                drop->next_item = drop + 1;\n                drop++;\n            } else {\n                drop->next_item = nullptr;\n            }\n        }\n    }\n}\n\nvoid Carrier_InitialiseLevel(const GF_LEVEL *const level)\n{\n    m_AnimatingCount = 0;\n    if (g_GameFlow.enable_tr2_item_drops) {\n        M_InitialiseDataDrops();\n    } else {\n        M_InitialiseGameFlowDrops(level);\n    }\n}\n\nint32_t Carrier_GetItemCount(const int16_t item_num)\n{\n    const ITEM *const carrier = M_GetCarrier(item_num);\n    if (carrier == nullptr) {\n        return 0;\n    }\n\n    const CARRIED_ITEM *item = carrier->carried_item;\n    int32_t count = 0;\n    while (item != nullptr) {\n        if (item->object_id != NO_OBJECT) {\n            count++;\n        }\n        item = item->next_item;\n    }\n\n    return count;\n}\n\nbool Carrier_IsItemCarried(const int16_t item_num)\n{\n    // This only applies to TR2-style drops; gameflow drop item numbers are not\n    // assigned until they are dropped, so this would always logically be false.\n    const ITEM *const item = Item_Get(item_num);\n    return item->room_num == NO_ROOM;\n}\n\nDROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item)\n{\n    if (item->status == DS_DROPPED) {\n        const ITEM *const pickup = Item_Get(item->spawn_num);\n        return pickup->status == IS_INVISIBLE ? DS_COLLECTED : DS_DROPPED;\n    }\n    return item->status;\n}\n\nvoid Carrier_SyncItem(\n    const int16_t carrier_item_num, CARRIED_ITEM *const carried_item)\n{\n    const ITEM *const carrier = Item_Get(carrier_item_num);\n    ITEM *const pickup_item = M_EnsureCarriedPickupItem(carrier, carried_item);\n    if (pickup_item == nullptr) {\n        return;\n    }\n\n    switch (carried_item->status) {\n    case DS_CARRIED:\n        if (pickup_item->room_num != NO_ROOM) {\n            Item_UpdateRoom(carried_item->spawn_num, NO_ROOM);\n        }\n        break;\n\n    case DS_FALLING:\n    case DS_DROPPED:\n        pickup_item->pos = carried_item->pos;\n        pickup_item->rot.y = carried_item->rot.y;\n        pickup_item->fall_speed = carried_item->fall_speed;\n        if (carried_item->status == DS_DROPPED) {\n            pickup_item->status = IS_INACTIVE;\n        } else {\n            m_AnimatingCount++;\n        }\n        pickup_item->object_id = M_ConvertDroppedGun(pickup_item->object_id);\n        Item_UpdateRoom(carried_item->spawn_num, carried_item->room_num);\n        break;\n\n    case DS_COLLECTED:\n        if (pickup_item->room_num != NO_ROOM) {\n            Item_UpdateRoom(carried_item->spawn_num, NO_ROOM);\n        }\n        pickup_item->status = IS_INVISIBLE;\n        break;\n    }\n}\n\nvoid Carrier_TestItemDrops(const int16_t item_num)\n{\n    const ITEM *const carrier = Item_Get(item_num);\n    CARRIED_ITEM *item = M_GetFirstDropItem(carrier);\n    if (item == nullptr) {\n        return;\n    }\n\n    // The enemy is killed (plus is not runaway) and is carrying at\n    // least one item. Ensure that each item has not already spawned,\n    // convert guns to ammo if applicable, and spawn the items.\n    do {\n        if (item->status != DS_CARRIED) {\n            continue;\n        }\n\n        if (item->spawn_num == NO_ITEM) {\n            // This is a gameflow-defined drop, so a spawn number is required.\n            const OBJECT_ID obj_id = M_ConvertDroppedGun(item->object_id);\n            item->spawn_num = Item_Spawn(carrier, obj_id);\n        } else {\n            // TR2-style item drops will already have a spawn number.\n            Item_UpdateRoom(item->spawn_num, carrier->room_num);\n            ITEM *const pickup = Item_Get(item->spawn_num);\n            pickup->pos = carrier->pos;\n            pickup->rot = carrier->rot;\n            pickup->status = IS_INACTIVE;\n        }\n\n        ITEM *const pickup = Item_Get(item->spawn_num);\n        if (M_ShouldCenterDrop(pickup->object_id)) {\n            int16_t room_num = carrier->room_num;\n            pickup->pos.x = ROUND_TO_SECTOR(carrier->pos.x) + WALL_L / 2;\n            pickup->pos.z = ROUND_TO_SECTOR(carrier->pos.z) + WALL_L / 2;\n            const SECTOR *const sector = Room_GetSector(pickup->pos, &room_num);\n            pickup->pos.y = Room_GetHeight(\n                sector,\n                (XYZ_32) { pickup->pos.x, carrier->pos.y, pickup->pos.z });\n        }\n\n        item->status = DS_FALLING;\n        m_AnimatingCount++;\n\n        if (item->room_num != NO_ROOM) {\n            // Handle reloading a save with a falling or landed item.\n            pickup->pos = item->pos;\n            pickup->fall_speed = item->fall_speed;\n            Item_UpdateRoom(item->spawn_num, item->room_num);\n        }\n\n    } while ((item = item->next_item) != nullptr);\n}\n\nvoid Carrier_AnimateDrops(void)\n{\n    if (m_AnimatingCount == 0) {\n        return;\n    }\n\n    // Make items that spawn in mid-air or water gracefully fall to the floor.\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        const ITEM *const carrier = Item_Get(i);\n        CARRIED_ITEM *item = carrier->carried_item;\n        while (item != nullptr) {\n            M_AnimateDrop(item);\n            item = item->next_item;\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/items/carrier.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/items/types.h>\n\nvoid Carrier_InitialiseLevel(const GF_LEVEL *level);\nint32_t Carrier_GetItemCount(int16_t item_num);\nbool Carrier_IsItemCarried(int16_t item_num);\nvoid Carrier_TestItemDrops(int16_t item_num);\n\nvoid Carrier_SyncItem(int16_t item_num, CARRIED_ITEM *carried_item);\nDROP_STATUS Carrier_GetSaveStatus(const CARRIED_ITEM *item);\n\nvoid Carrier_AnimateDrops(void);\n"
  },
  {
    "path": "src/trx/game/items/col.c",
    "content": "#include <trx/game/items/col.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\nstatic BOUNDS_16 m_NullBounds = {};\nstatic BOUNDS_16 m_InterpolatedBounds = {};\n\nint16_t Item_GetHeight(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, item->pos);\n\n    return height;\n}\n\nint32_t Item_GetDistance(const ITEM *const item, const XYZ_32 target)\n{\n    return XYZ_32_GetDistance(item->pos, target);\n}\n\nvoid Item_Translate(\n    ITEM *const item, const int32_t x, const int32_t y, const int32_t z)\n{\n    const int32_t c = Math_Cos(item->rot.y);\n    const int32_t s = Math_Sin(item->rot.y);\n    item->pos.x += ((c * x + s * z) >> W2V_SHIFT);\n    item->pos.y += y;\n    item->pos.z += ((c * z - s * x) >> W2V_SHIFT);\n}\n\nbool Item_Test3DRange(\n    const int32_t x, const int32_t y, const int32_t z, const int32_t range)\n{\n    return ABS(x) < range && ABS(y) < range && ABS(z) < range\n        && (SQUARE(x) + SQUARE(y) + SQUARE(z) < SQUARE(range));\n}\n\nbool Item_IsNearby(\n    const ITEM *const item_1, const ITEM *const item_2, const int32_t distance)\n{\n    return XYZ_32_IsNearby(item_1->pos, item_2->pos, distance);\n}\n\nbool Item_TestBoundsCollide(\n    const ITEM *const src_item, const ITEM *const dst_item,\n    const int32_t radius)\n{\n    const COLL_ITEM src = {\n        .bounds = Item_GetBestFrame(src_item)->bounds,\n        .pos = src_item->pos,\n        .rot = src_item->rot,\n    };\n    const COLL_ITEM dst = {\n        .bounds = Item_GetBestFrame(dst_item)->bounds,\n        .pos = dst_item->pos,\n        .rot = dst_item->rot,\n    };\n    return Collide_TestBoundsCollide(&src, &dst, radius);\n}\n\nbool Item_TestStatic3DBoundsCollide(\n    const STATIC_MESH *const mesh, const ITEM *const dst_item,\n    const int32_t radius)\n{\n    const COLL_ITEM src = {\n        .bounds = Object_Get3DStatic(mesh->static_num)->collision_bounds,\n        .pos = mesh->pos,\n        .rot = { .y = mesh->rot.y },\n    };\n    const COLL_ITEM dst = {\n        .bounds = Item_GetBestFrame(dst_item)->bounds,\n        .pos = dst_item->pos,\n        .rot = dst_item->rot,\n    };\n    return Collide_TestBoundsCollide(&src, &dst, radius);\n}\n\nconst BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *const item)\n{\n    int32_t rate;\n    ANIM_FRAME *frames[2];\n    const int32_t frac = Item_GetFrames(item, frames, &rate);\n    if (frames[0] == nullptr) {\n        return &m_NullBounds;\n    }\n\n    if (frac == 0) {\n        return &frames[0]->bounds;\n    }\n\n#define CALC(target, b1, b2, prop)                                             \\\n    target->prop = (b1)->prop + ((((b2)->prop - (b1)->prop) * frac) / rate);\n    BOUNDS_16 *const result = &m_InterpolatedBounds;\n    CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.x);\n    CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.x);\n    CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.y);\n    CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.y);\n    CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.z);\n    CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.z);\n#undef CALC\n\n    return result;\n}\n\nBOUNDS_16 Item_RotateBounds(const ITEM *const item, const int16_t rot_y)\n{\n    const BOUNDS_16 *bounds = &Item_GetBestFrame(item)->bounds;\n    BOUNDS_16 rot_bounds = {};\n    if (bounds == nullptr) {\n        return rot_bounds;\n    }\n\n    switch (rot_y) {\n    case 0:\n    default:\n        rot_bounds = *bounds;\n        break;\n    case DEG_90:\n        rot_bounds.min.x = bounds->min.z;\n        rot_bounds.max.x = bounds->max.z;\n        rot_bounds.min.z = -bounds->max.x;\n        rot_bounds.max.z = -bounds->min.x;\n        break;\n    case -DEG_180:\n        rot_bounds.min.x = -bounds->max.x;\n        rot_bounds.max.x = -bounds->min.x;\n        rot_bounds.min.z = -bounds->max.z;\n        rot_bounds.max.z = -bounds->min.z;\n        break;\n    case -DEG_90:\n        rot_bounds.min.x = -bounds->max.z;\n        rot_bounds.max.x = -bounds->min.z;\n        rot_bounds.min.z = bounds->min.x;\n        rot_bounds.max.z = bounds->max.x;\n        break;\n    }\n    return rot_bounds;\n}\n"
  },
  {
    "path": "src/trx/game/items/col.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n#include <trx/game/rooms/types.h>\n\nint16_t Item_GetHeight(const ITEM *item);\nint32_t Item_GetDistance(const ITEM *item, XYZ_32 target);\nvoid Item_Translate(ITEM *item, int32_t x, int32_t y, int32_t z);\nbool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range);\nbool Item_IsNearby(const ITEM *item_1, const ITEM *item_2, int32_t distance);\n\nbool Item_TestBoundsCollide(\n    const ITEM *src_item, const ITEM *dst_item, int32_t radius);\nbool Item_TestStatic3DBoundsCollide(\n    const STATIC_MESH *mesh, const ITEM *dst_item, int32_t radius);\nconst BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item);\nBOUNDS_16 Item_RotateBounds(const ITEM *item, int16_t rot_y);\n"
  },
  {
    "path": "src/trx/game/items/const.h",
    "content": "#pragma once\n\n#define NO_ITEM (-1)\n#define MAX_ITEMS 10240\n"
  },
  {
    "path": "src/trx/game/items/draw.c",
    "content": "#include <trx/game/items/draw.h>\n\n#include <trx/game/items/utils.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\nvoid Item_ControlDraw(ITEM *const item)\n{\n    if (g_TRVersion == 3 && item->status != IS_INVISIBLE\n        && item->after_death < 32 && item->after_death > 0) {\n        item->after_death++;\n\n        if (!Item_ShouldSpawnBlood(item)) {\n            return;\n        }\n\n        if (item->after_death == 2 || item->after_death == 5\n            || item->after_death == 11 || item->after_death == 20\n            || item->after_death == 27 || item->after_death == 32\n            || (Random_GetDraw() & 7) == 0) {\n            Spawn_BloodBathD(\n                item->pos.x, item->pos.y - 64, item->pos.z, 0,\n                Random_GetDraw() << 1, item->room_num, 1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/items/draw.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nvoid Item_ControlDraw(ITEM *item);\n"
  },
  {
    "path": "src/trx/game/items/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    // clang-format off\n    DS_CARRIED   = 0,\n    DS_FALLING   = 1,\n    DS_DROPPED   = 2,\n    DS_COLLECTED = 3,\n    // clang-format on\n} DROP_STATUS;\n\ntypedef enum {\n    // clang-format off\n    IF_ONE_SHOT_SWITCH      = 0x0040,\n    IF_ONE_SHOT_ANTITRIGGER = 0x0080,\n    IF_ONE_SHOT             = 0x0100,\n    IF_CODE_BITS            = 0x3E00,\n    IF_REVERSE              = 0x4000,\n    IF_INVISIBLE            = 0x0100,\n    IF_KILLED               = 0x8000,\n    // clang-format on\n} ITEM_FLAG;\n\ntypedef enum {\n    // clang-format off\n    AI_GUARD    = 1 << 0,\n    AI_AMBUSH   = 1 << 1,\n    AI_PATROL_1 = 1 << 2,\n    AI_MODIFY   = 1 << 3,\n    AI_FOLLOW   = 1 << 4,\n    // clang-format on\n} AI_BITS;\n\ntypedef enum {\n    // clang-format off\n    IS_INACTIVE    = 0,\n    IS_ACTIVE      = 1,\n    IS_DEACTIVATED = 2,\n    IS_INVISIBLE   = 3,\n    // clang-format on\n} ITEM_STATUS;\n"
  },
  {
    "path": "src/trx/game/items/manager.c",
    "content": "#include <trx/game/items/manager.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/shoal.h>\n#include <trx/game/output/const.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n#include <string.h>\n\nstatic int32_t m_LevelItemCount = 0;\nstatic int16_t m_MaxUsedItemCount = 0;\nstatic ITEM *m_Items = nullptr;\nstatic int16_t m_NextItemActive = NO_ITEM;\nstatic int16_t m_NextItemFree = NO_ITEM;\n\nstatic inline bool M_ItemBoundsIntersectsPortal(\n    const ITEM *item, const ROOM *room, const PORTAL *const portal)\n{\n    // Axis-aligned bound intersection; ignores item rotation.\n    const BOUNDS_16 *const frame_bounds = &Item_GetBestFrame(item)->bounds;\n    if (frame_bounds == nullptr) {\n        return false;\n    }\n    const BOUNDS_32 bounds = {\n        .min = {\n            item->pos.x + frame_bounds->min.x,\n            item->pos.y + frame_bounds->min.y,\n            item->pos.z + frame_bounds->min.z,\n        },\n        .max = {\n            item->pos.x + frame_bounds->max.x,\n            item->pos.y + frame_bounds->max.y,\n            item->pos.z + frame_bounds->max.z,\n        },\n    };\n\n    if (Bounds32_Intersect(&bounds, &portal->bounds)) {\n        return true;\n    }\n\n    const ROOM *const own_room = Room_Get(item->room_num);\n    if (own_room == nullptr) {\n        return false;\n    }\n    const BOUNDS_32 room_bounds = Room_GetRoomBounds(own_room);\n    return !Bounds32_Intersect(&bounds, &room_bounds);\n}\n\nvoid Item_InitialiseItems(const int32_t num_items)\n{\n    m_Items = GameBuf_Alloc(sizeof(ITEM) * MAX_ITEMS, GBUF_ITEMS);\n    m_LevelItemCount = num_items;\n    m_MaxUsedItemCount = num_items;\n    m_NextItemFree = num_items;\n    m_NextItemActive = NO_ITEM;\n\n    for (int32_t i = m_NextItemFree; i < MAX_ITEMS - 1; i++) {\n        ITEM *const item = &m_Items[i];\n        item->active = false;\n        item->next_item = i + 1;\n    }\n    m_Items[MAX_ITEMS - 1].next_item = NO_ITEM;\n}\n\nITEM *Item_Get(const int16_t item_num)\n{\n    if (item_num == NO_ITEM) {\n        return nullptr;\n    }\n    return &m_Items[item_num];\n}\n\nint16_t Item_GetIndex(const ITEM *const item)\n{\n    return item - Item_Get(0);\n}\n\nITEM *Item_Find(const OBJECT_ID obj_id)\n{\n    for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (item->object_id == obj_id) {\n            return item;\n        }\n    }\n\n    return nullptr;\n}\n\nbool Item_SetName(const int16_t item_num, const char *const name)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item == nullptr) {\n        return false;\n    }\n    if (name != nullptr) {\n        ITEM *const existing = Item_GetByName(name);\n        if (existing != nullptr && existing != item) {\n            return false;\n        }\n    }\n    if (name != nullptr) {\n        item->name = GameBuf_Alloc(strlen(name) + 1, GBUF_ITEMS);\n        strcpy(item->name, name);\n    } else {\n        item->name = nullptr;\n    }\n    return true;\n}\n\nITEM *Item_GetByName(const char *const name)\n{\n    if (name == nullptr) {\n        return nullptr;\n    }\n    // search through all items for matching name\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (item->name != nullptr && strcmp(item->name, name) == 0) {\n            return item;\n        }\n    }\n    return nullptr;\n}\n\nint32_t Item_GetLevelCount(void)\n{\n    return m_LevelItemCount;\n}\n\nint32_t Item_GetTotalCount(void)\n{\n    return m_MaxUsedItemCount;\n}\n\nint16_t Item_GetNextActive(void)\n{\n    return m_NextItemActive;\n}\n\nint16_t Item_Create(void)\n{\n    const int16_t item_num = m_NextItemFree;\n    if (item_num != NO_ITEM) {\n        m_Items[item_num].flags = 0;\n        m_NextItemFree = m_Items[item_num].next_item;\n    }\n    m_MaxUsedItemCount = MAX(m_MaxUsedItemCount, item_num + 1);\n    return item_num;\n}\n\nint16_t Item_CreateLevelItem(void)\n{\n    const int16_t item_num = Item_Create();\n    if (item_num != NO_ITEM) {\n        m_LevelItemCount++;\n    }\n    return item_num;\n}\n\nint16_t Item_Spawn(const ITEM *const item, const OBJECT_ID obj_id)\n{\n    const int16_t spawn_num = Item_Create();\n    if (spawn_num != NO_ITEM) {\n        ITEM *const spawn = Item_Get(spawn_num);\n        spawn->object_id = obj_id;\n        spawn->room_num = item->room_num;\n        spawn->pos = item->pos;\n        spawn->rot = item->rot;\n        Item_Initialise(spawn_num);\n        spawn->status = IS_INACTIVE;\n        spawn->shade.value_1 = SHADE_NEUTRAL;\n    }\n    return spawn_num;\n}\n\nvoid Item_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    Item_SwitchToAnim(item, 0, 0);\n    item->goal_anim_state = Item_GetAnim(item)->current_anim_state;\n    item->current_anim_state = item->goal_anim_state;\n    item->required_anim_state = 0;\n    item->rot.x = 0;\n    item->rot.z = 0;\n    item->speed = 0;\n    item->fall_speed = 0;\n    item->hit_points = obj->hit_points;\n    item->max_hit_points = obj->hit_points;\n    item->timer = 0;\n    item->mesh_bits = 0xFFFFFFFF;\n    item->touch_bits = 0;\n    item->ai_bits = 0;\n    item->ai_tag = 0;\n    item->after_death = 0;\n    item->creature_data = nullptr;\n    item->extra_rotations = nullptr;\n    item->priv = nullptr;\n    item->carried_item = nullptr;\n\n    item->interp.result.pos = item->pos;\n    item->interp.result.rot = item->rot;\n\n    item->active = false;\n    item->status = IS_INACTIVE;\n    item->gravity = false;\n    item->hit_status = false;\n    item->collidable = true;\n    item->looked_at = false;\n    item->enable_interpolation = true;\n    item->enable_shadow = true;\n    item->dynamic_light = false;\n    item->include_in_kill_stats = true;\n\n    item->clear_body = false;\n    if ((item->flags & IF_KILLED) != 0) {\n        item->clear_body = true;\n        item->flags &= ~IF_KILLED;\n    }\n\n    if ((item->flags & IF_INVISIBLE) != 0) {\n        item->status = IS_INVISIBLE;\n        item->flags &= ~IF_INVISIBLE;\n    } else if (g_TRVersion >= 2 && obj->intelligent) {\n        item->status = IS_INVISIBLE;\n    }\n\n    if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) {\n        item->flags &= ~IF_CODE_BITS;\n        item->flags |= IF_REVERSE;\n        Item_AddActive(item_num);\n        item->status = IS_ACTIVE;\n    }\n\n    ROOM *const room = Room_Get(item->room_num);\n    item->next_item = room->item_num;\n    room->item_num = item_num;\n\n    const SECTOR *const sector =\n        Room_GetWorldSector(room, item->pos.x, item->pos.z);\n    item->floor = sector->floor.height;\n\n    // TODO: remove GF check once demo config reset is run before level load\n    if (Game_IsBonusFlagSet(GBF_NGPLUS)\n        && GF_GetCurrentLevel()->type != GFL_DEMO) {\n        item->hit_points *= 2;\n    }\n\n    if (obj->priv_size != 0) {\n        if (item->priv != nullptr) {\n            memset(item->priv, 0, obj->priv_size);\n        } else {\n            item->priv = GameBuf_Alloc(obj->priv_size, GBUF_ITEM_DATA);\n        }\n    }\n\n    if (obj->initialise_func != nullptr) {\n        obj->initialise_func(item_num);\n    }\n\n    if (item->room_num != NO_ROOM) {\n        Room_AddDrawnItem(item->room_num, item_num);\n    }\n}\n\nvoid Item_Control(void)\n{\n    int16_t item_num = Item_GetNextActive();\n    while (item_num != NO_ITEM) {\n        const ITEM *const item = Item_Get(item_num);\n        const int16_t next = item->next_active;\n        const OBJECT *obj = Object_Get(item->object_id);\n        if ((item->flags & IF_KILLED) == 0 && obj->control_func != nullptr) {\n            obj->control_func(item_num);\n        }\n        item_num = next;\n    }\n\n    Carrier_AnimateDrops();\n}\n\nvoid Item_Kill(const int16_t item_num)\n{\n    Sparks_DetachItem(item_num);\n    Item_RemoveActive(item_num);\n    Item_RemoveDrawn(item_num);\n\n    ITEM *const item = &m_Items[item_num];\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item == lara->target) {\n        lara->target = nullptr;\n    }\n\n    item->flags |= IF_KILLED;\n\n    if (item_num >= m_LevelItemCount) {\n        item->next_item = m_NextItemFree;\n        m_NextItemFree = item_num;\n    }\n\n    while (m_MaxUsedItemCount > 0\n           && m_Items[m_MaxUsedItemCount - 1].flags & IF_KILLED) {\n        m_MaxUsedItemCount--;\n    }\n}\n\nvoid Item_KillAllActive(void)\n{\n    int16_t item_num = Item_GetNextActive();\n    while (item_num != NO_ITEM) {\n        ITEM *const item = Item_Get(item_num);\n        const int16_t next_item_num = item->next_active;\n\n        if (item->active && (item->flags & IF_REVERSE) == 0\n            && item->object_id != O_LARA\n            && item->object_id != O_SAVE_CRYSTAL_ITEM\n            && !Object_IsType(item->object_id, g_PickupObjects)\n            && !Object_IsType(item->object_id, g_DoorObjects)) {\n            Item_Kill(item_num);\n\n            if (Object_IsType(item->object_id, g_ShoalObjects)) {\n                Shoal_TriggerDeactivate(item);\n            }\n        }\n        item_num = next_item_num;\n    }\n}\n\nvoid Item_RemoveActive(const int16_t item_num)\n{\n    ITEM *const item = &m_Items[item_num];\n    if (!item->active) {\n        return;\n    }\n\n    item->active = false;\n\n    int16_t link_num = m_NextItemActive;\n    if (link_num == item_num) {\n        m_NextItemActive = item->next_active;\n        return;\n    }\n\n    while (link_num != NO_ITEM) {\n        if (m_Items[link_num].next_active == item_num) {\n            m_Items[link_num].next_active = item->next_active;\n            return;\n        }\n        link_num = m_Items[link_num].next_active;\n    }\n}\n\nvoid Item_RemoveDrawn(const int16_t item_num)\n{\n    const ITEM *const item = &m_Items[item_num];\n    if (item->room_num == NO_ROOM) {\n        return;\n    }\n\n    ROOM *const room = Room_Get(item->room_num);\n    Room_RemoveDrawnItem(item->room_num, item_num);\n    if (room->portals != nullptr) {\n        for (int32_t i = 0; i < room->portals->count; i++) {\n            const PORTAL *const portal = &room->portals->portal[i];\n            Room_RemoveDrawnItem(portal->room_num, item_num);\n        }\n    }\n\n    int16_t link_num = room->item_num;\n    if (link_num == item_num) {\n        room->item_num = item->next_item;\n        return;\n    }\n\n    while (link_num != NO_ITEM) {\n        if (m_Items[link_num].next_item == item_num) {\n            m_Items[link_num].next_item = item->next_item;\n            return;\n        }\n        link_num = m_Items[link_num].next_item;\n    }\n}\n\nvoid Item_ClearKilled(void)\n{\n    // Remove corpses and other killed items. Part of OG performance\n    // improvements, generously used in Opera House and Barkhang Monastery\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->intelligent && item->clear_body && item->hit_points <= 0\n            && (item->flags & IF_KILLED) == 0) {\n            Item_Kill(i);\n        }\n    }\n}\n\nvoid Item_AddActive(const int16_t item_num)\n{\n    ITEM *const item = &m_Items[item_num];\n    if (Object_Get(item->object_id)->control_func == nullptr) {\n        item->status = IS_INACTIVE;\n        return;\n    }\n\n    if (item->active) {\n        return;\n    }\n\n    item->active = true;\n    item->next_active = m_NextItemActive;\n    m_NextItemActive = item_num;\n}\n\nvoid Item_UpdateRoom(const int16_t item_num, const int16_t room_num)\n{\n    ITEM *const item = &m_Items[item_num];\n    const int16_t old_room_num = item->room_num;\n\n    // Add to new-room draw queues (including portal rooms)\n    // draw queue removal for primary room and portal rooms\n    if (old_room_num != NO_ROOM) {\n        Room_RemoveDrawnItem(old_room_num, item_num);\n        const ROOM *const room = Room_Get(old_room_num);\n        if (room != nullptr && room->portals != nullptr) {\n            for (int32_t i = 0; i < room->portals->count; i++) {\n                Room_RemoveDrawnItem(\n                    room->portals->portal[i].room_num, item_num);\n            }\n        }\n    }\n    if (room_num != NO_ROOM) {\n        Room_AddDrawnItem(room_num, item_num);\n        const ROOM *const neighbor_room = Room_Get(room_num);\n        if (neighbor_room != nullptr && neighbor_room->portals != nullptr) {\n            for (int32_t i = 0; i < neighbor_room->portals->count; i++) {\n                const PORTAL *const portal = &neighbor_room->portals->portal[i];\n                if (M_ItemBoundsIntersectsPortal(item, neighbor_room, portal)) {\n                    Room_AddDrawnItem(portal->room_num, item_num);\n                }\n            }\n        }\n    }\n\n    if (old_room_num != room_num) {\n        ROOM *room = nullptr;\n        if (old_room_num != NO_ROOM) {\n            room = Room_Get(old_room_num);\n            int16_t link_num = room->item_num;\n            if (link_num == item_num) {\n                room->item_num = item->next_item;\n            } else {\n                while (link_num != NO_ITEM) {\n                    if (m_Items[link_num].next_item == item_num) {\n                        m_Items[link_num].next_item = item->next_item;\n                        break;\n                    }\n                    link_num = m_Items[link_num].next_item;\n                }\n            }\n        }\n\n        if (room_num == NO_ROOM) {\n            Item_RemoveDrawn(item_num);\n            item->room_num = NO_ROOM;\n        } else {\n            room = Room_Get(room_num);\n            item->room_num = room_num;\n            item->next_item = room->item_num;\n            room->item_num = item_num;\n        }\n    }\n}\n\nint32_t Item_GlobalReplace(\n    const OBJECT_ID src_obj_id, const OBJECT_ID dst_obj_id)\n{\n    int32_t changed = 0;\n\n    for (int32_t item_num = 0; item_num < m_MaxUsedItemCount; item_num++) {\n        ITEM *const item = &m_Items[item_num];\n        if (item->object_id == src_obj_id) {\n            item->object_id = dst_obj_id;\n            changed++;\n        }\n    }\n\n    return changed;\n}\n"
  },
  {
    "path": "src/trx/game/items/manager.h",
    "content": "#pragma once\n\n#include <trx/game/anims.h>\n#include <trx/game/items/types.h>\n\nvoid Item_InitialiseItems(int32_t num_items);\nITEM *Item_Get(int16_t num);\nint16_t Item_GetIndex(const ITEM *item);\nITEM *Item_Find(OBJECT_ID obj_id);\n\nint32_t Item_GetLevelCount(void);\nint32_t Item_GetTotalCount(void);\n\nint16_t Item_GetNextActive(void);\n\nint16_t Item_Create(void);\nint16_t Item_CreateLevelItem(void);\nint16_t Item_Spawn(const ITEM *item, OBJECT_ID obj_id);\n\nvoid Item_Initialise(int16_t item_num);\nvoid Item_Control(void);\nvoid Item_Kill(int16_t item_num);\nvoid Item_KillAllActive(void);\nvoid Item_RemoveActive(int16_t item_num);\nvoid Item_RemoveDrawn(int16_t item_num);\nvoid Item_ClearKilled(void);\nvoid Item_AddActive(int16_t item_num);\nvoid Item_UpdateRoom(int16_t item_num, int16_t room_num);\n\nint32_t Item_GlobalReplace(OBJECT_ID src_obj_id, OBJECT_ID dst_obj_id);\n\n// Set the name of the item, storing a copy of the provided string.\n// Returns false if the name is already used by another item.\nbool Item_SetName(int16_t item_num, const char *name);\n\n// Retrieve an item by its name, or nullptr if not found\nITEM *Item_GetByName(const char *name);\n"
  },
  {
    "path": "src/trx/game/items/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/items/enum.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/output/types.h>\n\ntypedef struct CARRIED_ITEM {\n    OBJECT_ID object_id;\n    int16_t spawn_num;\n    XYZ_32 pos;\n    XYZ_16 rot;\n    int16_t room_num;\n    int16_t fall_speed;\n    DROP_STATUS status;\n    struct CARRIED_ITEM *next_item;\n} CARRIED_ITEM;\n\ntypedef struct TRAP_DATA TRAP_DATA;\ntypedef struct CREATURE CREATURE;\n\ntypedef struct {\n    int32_t floor;\n    uint32_t touch_bits;\n    uint32_t mesh_bits;\n    int16_t after_death;\n    OBJECT_ID object_id;\n    int16_t current_anim_state;\n    int16_t goal_anim_state;\n    int16_t required_anim_state;\n    int16_t anim_num;\n    int16_t frame_num;\n    int16_t prev_frame_num;\n    int16_t room_num;\n    int16_t next_item;\n    int16_t next_active;\n    int16_t speed;\n    int16_t fall_speed;\n    int16_t hit_points;\n    int16_t max_hit_points;\n    int16_t box_num;\n    int16_t timer;\n    uint16_t flags;\n    uint8_t ai_bits;\n    int16_t ai_tag;\n\n    SHADE shade;\n    union {\n        CREATURE *creature_data;\n        TRAP_DATA *trap_data;\n    };\n    int16_t *extra_rotations;\n    void *priv;\n    CARRIED_ITEM *carried_item;\n    char *name;\n\n    XYZ_32 pos;\n    XYZ_16 rot;\n\n    ITEM_STATUS status;\n    bool enable_interpolation;\n    bool enable_shadow;\n    bool active;\n    bool gravity;\n    bool hit_status;\n    bool collidable;\n    bool looked_at;\n    bool dynamic_light;\n    bool clear_body;\n    bool include_in_kill_stats;\n\n    struct {\n        struct {\n            int32_t floor;\n            XYZ_32 pos;\n            XYZ_16 rot;\n        } result, prev;\n    } interp;\n} ITEM;\n"
  },
  {
    "path": "src/trx/game/items/utils.c",
    "content": "#include <trx/game/items/utils.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\nstatic bool M_UseTR3ExplodingEffects(const ITEM *const item)\n{\n    if (g_TRVersion < 3) {\n        return false;\n    }\n\n    // TODO: potentially add a flag/function ptr to OBJECT\n    return item->object_id != O_CLAW_MUTANT\n        && !Object_IsType(item->object_id, g_ShatterableObjects)\n        && !Object_IsType(item->object_id, g_HeavyShatterableObjects);\n}\n\nstatic bool M_IsFloating(const ITEM *const item)\n{\n    return Object_IsType(item->object_id, g_WaterObjects)\n        && Object_Get(item->object_id)->intelligent && item->hit_points <= 0;\n}\n\nbool Item_IsAlive(const ITEM *const item)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->is_alive_func != nullptr) {\n        return obj->is_alive_func(item);\n    }\n\n    if (obj->intelligent && Object_IsType(item->object_id, g_WaterObjects)) {\n        return item->hit_points > 0;\n    }\n    return (item->hit_points > 0) || (item->active);\n}\n\nbool Item_IsTargetable(const ITEM *const item)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (Object_IsType(item->object_id, g_ProjectileObjects)) {\n        return false;\n    }\n\n    if (obj->is_targetable_func != nullptr) {\n        return obj->is_targetable_func(item);\n    }\n\n    return item->hit_points > 0 && item->status == IS_ACTIVE\n        && (g_Config.gameplay.enable_ally_targeting\n            || Creature_IsHostile(item));\n}\n\nbool Item_CanTakeDamage(const ITEM *const item)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->can_take_damage_func != nullptr) {\n        return obj->can_take_damage_func(item);\n    }\n\n    return Item_IsAlive(item) || M_IsFloating(item);\n}\n\nbool Item_CanBeProjectileTarget(const ITEM *const item)\n{\n    if (Object_IsType(item->object_id, g_ProjectileObjects)) {\n        return false;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->can_be_projectile_target_func != nullptr) {\n        return obj->can_be_projectile_target_func(item);\n    }\n\n    if (Object_IsType(item->object_id, g_ShatterableObjects)\n        || Object_IsType(item->object_id, g_SmashableObjects)) {\n        return true;\n    }\n\n    if (M_IsFloating(item)) {\n        return true;\n    }\n\n    if (!item->collidable || item->status == IS_INVISIBLE\n        || obj->collision_func == nullptr) {\n        return false;\n    }\n\n    return Item_IsTargetable(item);\n}\n\nvoid Item_TakeDamage(\n    ITEM *const item, const int16_t damage, const bool hit_status)\n{\n    if (!Item_CanTakeDamage(item)) {\n        return;\n    }\n\n    item->hit_points -= damage;\n    CLAMPL(item->hit_points, 0);\n\n    if (hit_status) {\n        item->hit_status = true;\n    }\n}\n\nbool Item_IsMeshVisible(const ITEM *const item, const int32_t mesh_num)\n{\n    if (mesh_num < 0 || mesh_num >= 32) {\n        return false;\n    }\n\n    const uint32_t bit = 1u << mesh_num;\n    return (item->mesh_bits & bit) != 0;\n}\n\nvoid Item_SetMeshVisibleMask(\n    ITEM *const item, const uint32_t mesh_mask, const bool visible)\n{\n    if (visible) {\n        item->mesh_bits |= mesh_mask;\n    } else {\n        item->mesh_bits &= ~mesh_mask;\n    }\n}\n\nvoid Item_SetMeshVisible(\n    ITEM *const item, const int32_t mesh_num, const bool visible)\n{\n    if (mesh_num < 0 || mesh_num >= 32) {\n        return;\n    }\n\n    const uint32_t bit = 1u << mesh_num;\n    Item_SetMeshVisibleMask(item, bit, visible);\n}\n\nvoid Item_ResetMeshBits(ITEM *const item)\n{\n    item->mesh_bits = UINT32_MAX;\n}\n\nint32_t Item_Explode(\n    const int16_t item_num, const int32_t mesh_bits, const int16_t damage)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (!obj->loaded) {\n        return 0;\n    }\n\n    Output_CalculateLight(item->pos, item->room_num);\n\n    const ANIM_FRAME *const best_frame = Item_GetBestFrame(item);\n\n    Matrix_PushUnit();\n    Matrix_Rot16(item->rot);\n    Matrix_TranslateRel16(best_frame->offset);\n    Matrix_Rot16(best_frame->mesh_rots[0]);\n\n    const int32_t speed_shift = item->object_id == O_TORSO ? 7 : 8;\n    const bool is_tr3 = M_UseTR3ExplodingEffects(item);\n\n    // main mesh\n    int32_t bit = 1;\n    if ((mesh_bits & bit) && (item->mesh_bits & bit)) {\n        const int16_t effect_num = Effect_Create(item->room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *const effect = Effect_Get(effect_num);\n            effect->pos.x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n            effect->pos.y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n            effect->pos.z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n            effect->rot.y = (Random_GetControl() - 0x4000) * 2;\n            effect->room_num = item->room_num;\n            effect->speed = Random_GetControl() >> speed_shift;\n            effect->fall_speed = -Random_GetControl() >> speed_shift;\n            effect->counter =\n                is_tr3 ? ((damage << 2) | (Random_GetControl() & 3)) : damage;\n            effect->object_id = O_BODY_PART;\n            effect->frame_num = Object_GetItemMeshIndex(item, 0);\n            effect->shade = Output_GetLightAdder() - 0x300;\n        }\n        item->mesh_bits &= ~bit;\n    }\n\n    // additional meshes\n    const int16_t *extra_rotation = item->extra_rotations;\n    for (int32_t i = 1; i < obj->mesh_count; i++) {\n        const ANIM_BONE *const bone = Object_GetBone(obj, i - 1);\n        if (bone->matrix_pop) {\n            Matrix_Pop();\n        }\n        if (bone->matrix_push) {\n            Matrix_Push();\n        }\n\n        Matrix_TranslateRel32(bone->pos);\n        Matrix_Rot16(best_frame->mesh_rots[i]);\n        Object_ApplyExtraRotation(&extra_rotation, bone->rot, false);\n\n        bit <<= 1;\n        if ((mesh_bits & bit) && (item->mesh_bits & bit)) {\n            const int16_t effect_num = Effect_Create(item->room_num);\n            if (effect_num != NO_EFFECT) {\n                EFFECT *const effect = Effect_Get(effect_num);\n                effect->pos.x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n                effect->pos.y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n                effect->pos.z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n                effect->rot.y = (Random_GetControl() - 0x4000) * 2;\n                effect->room_num = item->room_num;\n                effect->speed = Random_GetControl() >> speed_shift;\n                effect->fall_speed = -Random_GetControl() >> speed_shift;\n                effect->counter = is_tr3\n                    ? ((damage << 2) | (Random_GetControl() & 3))\n                    : damage;\n                effect->object_id = O_BODY_PART;\n                effect->frame_num = Object_GetItemMeshIndex(item, i);\n                effect->shade = Output_GetLightAdder() - 0x300;\n            }\n            item->mesh_bits &= ~bit;\n        }\n    }\n\n    Matrix_Pop();\n\n    return !(item->mesh_bits & (0x7FFFFFFF >> (31 - obj->mesh_count)));\n}\n\nbool Item_ShouldSpawnBlood(const ITEM *const item)\n{\n    if (item == nullptr) {\n        return true;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->should_spawn_blood_func != nullptr) {\n        return obj->should_spawn_blood_func(item);\n    }\n\n    return true;\n}\n\nint16_t Item_FindTypeInRoom(const int16_t room_num, const OBJECT_ID obj_id)\n{\n    int16_t linked_item_num = Room_Get(room_num)->item_num;\n    while (linked_item_num != NO_ITEM) {\n        const ITEM *const linked_item = Item_Get(linked_item_num);\n        if (linked_item->object_id == obj_id) {\n            return linked_item_num;\n        }\n        linked_item_num = linked_item->next_item;\n    }\n    return NO_ITEM;\n}\n\nint16_t Item_FindTypeAtPos(\n    const int16_t room_num, const XYZ_32 pos, const OBJECT_ID obj_id)\n{\n    const ROOM *const room = Room_Get(room_num);\n    int16_t item_num = room->item_num;\n    while (item_num != NO_ITEM) {\n        const ITEM *const item = Item_Get(item_num);\n        if (item->object_id == obj_id && XYZ_32_AreEquivalent(item->pos, pos)) {\n            return item_num;\n        }\n        item_num = item->next_item;\n    }\n    return NO_ITEM;\n}\n\nbool Item_IsTriggerActiveRO(const ITEM *const item)\n{\n    const bool ok = (item->flags & IF_REVERSE) == 0;\n    if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) {\n        return !ok;\n    }\n    if (item->timer == 0) {\n        return ok;\n    }\n    if (item->timer == -1) {\n        return !ok;\n    }\n    return ok;\n}\n\nbool Item_IsTriggerActive(ITEM *const item)\n{\n    const bool result = Item_IsTriggerActiveRO(item);\n    if (item->timer != 0 && item->timer != -1) {\n        item->timer--;\n        if (item->timer == 0) {\n            item->timer = -1;\n        }\n    }\n    return result;\n}\n"
  },
  {
    "path": "src/trx/game/items/utils.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\n#define ITEM_ADJUST_ROT(source, target, rot)                                   \\\n    do {                                                                       \\\n        if ((int16_t)(target - source) > rot) {                                \\\n            source += rot;                                                     \\\n        } else if ((int16_t)(target - source) < -rot) {                        \\\n            source -= rot;                                                     \\\n        } else {                                                               \\\n            source = target;                                                   \\\n        }                                                                      \\\n    } while (0)\n\nbool Item_IsTriggerActiveRO(const ITEM *item);\nbool Item_IsTriggerActive(ITEM *item);\n\nbool Item_IsAlive(const ITEM *item);\nbool Item_IsTargetable(const ITEM *item);\nbool Item_CanTakeDamage(const ITEM *item);\nbool Item_CanBeProjectileTarget(const ITEM *item);\n\nvoid Item_TakeDamage(ITEM *item, int16_t damage, bool hit_status);\n\nbool Item_IsMeshVisible(const ITEM *item, int32_t mesh_num);\nvoid Item_SetMeshVisible(ITEM *item, int32_t mesh_num, bool visible);\nvoid Item_SetMeshVisibleMask(ITEM *item, uint32_t mesh_mask, bool visible);\nvoid Item_ResetMeshBits(ITEM *item);\n\n// Mesh_bits: which meshes to affect.\n// Damage:\n// * Positive values - deal damage, enable body part explosions.\n// * Negative values - deal damage, disable body part explosions.\n// * Zero - don't deal any damage, disable body part explosions.\nint32_t Item_Explode(int16_t item_num, int32_t mesh_bits, int16_t damage);\n\nbool Item_ShouldSpawnBlood(const ITEM *item);\n\nint16_t Item_FindTypeInRoom(int16_t room_num, OBJECT_ID obj_id);\nint16_t Item_FindTypeAtPos(int16_t room_num, XYZ_32 pos, OBJECT_ID obj_id);\n"
  },
  {
    "path": "src/trx/game/items/walkable.c",
    "content": "#include <trx/game/items/walkable.h>\n\n#include <trx/core/log.h>\n#include <trx/debug.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/items.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/rooms.h>\n\n#include <stdlib.h>\n\n#define M_QUADRANT_COUNT 4\n\ntypedef struct {\n    SECTOR *sectors[M_QUADRANT_COUNT];\n    int32_t count;\n} M_CANDIDATE_SECTORS;\n\ntypedef struct {\n    WALKABLE *nodes;\n    int32_t capacity;\n    int32_t active_count;\n} M_SETUP;\n\nstatic M_SETUP *m_Setup = nullptr;\nstatic int32_t m_SetupCount = 0;\n\nstatic SECTOR *M_GetItemPitSector(const XYZ_32 pos, int16_t room_num)\n{\n    SECTOR *const sector = Room_GetSector(pos, &room_num);\n    return Room_GetPitSector(sector, pos.x, pos.z);\n}\n\nstatic bool M_HasCandidateSector(\n    const M_CANDIDATE_SECTORS *const candidates, const SECTOR *const sector)\n{\n    for (int32_t i = 0; i < candidates->count; i++) {\n        if (candidates->sectors[i] == sector) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic M_CANDIDATE_SECTORS M_GetCandidateSectors(\n    const XYZ_32 base_pos, int16_t room_num)\n{\n    // Probe evenly around the centre position for cases where a walkable is\n    // placed above a triangle portal, so detecting the correct sector at all\n    // possible positions.\n    M_CANDIDATE_SECTORS candidates = { 0 };\n\n    const XYZ_32 mid_pos = {\n        .x = ROUND_TO_SECTOR(base_pos.x) + STEP_L * 2,\n        .y = base_pos.y,\n        .z = ROUND_TO_SECTOR(base_pos.z) + STEP_L * 2,\n    };\n\n    const XZ_32 deltas[M_QUADRANT_COUNT] = {\n        { -STEP_L, 0 },\n        { STEP_L, 0 },\n        { 0, -STEP_L },\n        { 0, STEP_L },\n    };\n\n    for (int32_t i = 0; i < M_QUADRANT_COUNT; i++) {\n        const XZ_32 delta = deltas[i];\n        const XYZ_32 pos = {\n            .x = mid_pos.x + delta.x,\n            .y = base_pos.y,\n            .z = mid_pos.z + delta.z,\n        };\n        SECTOR *const sector = M_GetItemPitSector(pos, room_num);\n        if (!M_HasCandidateSector(&candidates, sector)) {\n            candidates.sectors[candidates.count] = sector;\n            candidates.count++;\n        }\n    }\n\n    return candidates;\n}\n\nstatic M_SETUP *M_GetSetup(const int16_t item_num)\n{\n    ASSERT(m_Setup != nullptr);\n    ASSERT(item_num >= 0 && item_num < m_SetupCount);\n    return &m_Setup[item_num];\n}\n\nstatic bool M_SectorContainsWalkable(\n    const SECTOR *const sector, const int16_t item_num)\n{\n    const WALKABLE *walkable = sector->walkable;\n    while (walkable != nullptr) {\n        if (walkable->item_num == item_num) {\n            return true;\n        }\n        walkable = walkable->next;\n    }\n    return false;\n}\n\nstatic void M_InsertSorted(WALKABLE **walkables, WALKABLE *const node)\n{\n    while (*walkables != nullptr && (*walkables)->pos.y >= node->pos.y) {\n        walkables = &(*walkables)->next;\n    }\n\n    node->next = *walkables;\n    *walkables = node;\n}\n\nstatic void M_Remove(\n    const int16_t item_num, const XYZ_32 pos, const int16_t room_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->add_walkable_func == nullptr) {\n        return;\n    }\n\n    M_SETUP *const setup = M_GetSetup(item_num);\n    if (setup->capacity == 0 || setup->nodes == nullptr) {\n        return;\n    }\n\n    const M_CANDIDATE_SECTORS sectors = M_GetCandidateSectors(pos, room_num);\n    for (int32_t i = 0; i < sectors.count; i++) {\n        SECTOR *const sector = sectors.sectors[i];\n        WALKABLE *walkable = sector->walkable;\n        WALKABLE *prev = nullptr;\n        while (walkable != nullptr) {\n            if (walkable->item_num == item_num) {\n                if (prev != nullptr) {\n                    prev->next = walkable->next;\n                } else {\n                    sector->walkable = walkable->next;\n                }\n                break;\n            }\n            prev = walkable;\n            walkable = walkable->next;\n        }\n    }\n\n    setup->active_count = 0;\n}\n\nvoid Walkable_AllocateNodes(const ITEM *const item, const int32_t footprint)\n{\n    const int16_t item_num = Item_GetIndex(item);\n    M_SETUP *const setup = M_GetSetup(item_num);\n    setup->capacity = footprint * M_QUADRANT_COUNT;\n    setup->active_count = 0;\n    setup->nodes = nullptr;\n    if (setup->capacity > 0) {\n        setup->nodes =\n            GameBuf_Alloc(sizeof(WALKABLE) * setup->capacity, GBUF_WALKABLES);\n    }\n}\n\nvoid Walkable_Add(const int16_t item_num, const XYZ_32 pos)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->add_walkable_func == nullptr) {\n        return;\n    }\n\n    M_SETUP *const setup = M_GetSetup(item_num);\n    if (setup->capacity == 0 || setup->nodes == nullptr) {\n        return;\n    }\n    const M_CANDIDATE_SECTORS sectors =\n        M_GetCandidateSectors(pos, item->room_num);\n\n    for (int32_t i = 0; i < sectors.count; i++) {\n        SECTOR *const sector = sectors.sectors[i];\n        if (M_SectorContainsWalkable(sector, item_num)) {\n            continue;\n        }\n\n        if (setup->active_count >= setup->capacity) {\n            LOG_WARNING(\n                \"Walkable %d at (%d, %d, %d) has no more allocated sector \"\n                \"nodes.\",\n                item_num, pos.x, pos.y, pos.z);\n            break;\n        }\n\n        WALKABLE *const node = &setup->nodes[setup->active_count];\n        node->item_num = item_num;\n        node->pos = pos;\n        node->next = nullptr;\n        M_InsertSorted(&sector->walkable, node);\n        setup->active_count++;\n    }\n}\n\nvoid Walkable_Remove(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    M_Remove(item_num, item->pos, item->room_num);\n}\n\nvoid Walkable_Reposition(\n    const int16_t item_num, const GAME_VECTOR start, const GAME_VECTOR target)\n{\n    M_Remove(item_num, start.pos, start.room_num);\n    Walkable_Add(item_num, target.pos);\n}\n\nvoid Walkable_Reset(void)\n{\n    const int32_t room_count = Room_GetCount();\n    for (int32_t room_idx = 0; room_idx < room_count; room_idx++) {\n        const ROOM *const room = Room_Get(room_idx);\n        const int32_t num_sectors = room->size.x * room->size.z;\n        for (int32_t i = 0; i < num_sectors; i++) {\n            room->sectors[i].walkable = nullptr;\n        }\n    }\n\n    for (int32_t i = 0; i < m_SetupCount; i++) {\n        M_SETUP *const setup = M_GetSetup(i);\n        setup->active_count = 0;\n    }\n}\n\nvoid Walkable_ResetLevel(void)\n{\n    Walkable_Reset();\n    const int32_t item_count = Item_GetLevelCount();\n    m_SetupCount = item_count;\n    m_Setup = nullptr;\n    if (item_count > 0) {\n        m_Setup = GameBuf_Alloc(sizeof(M_SETUP) * item_count, GBUF_WALKABLES);\n    }\n}\n\nvoid Walkable_Shutdown(void)\n{\n    m_Setup = nullptr;\n    m_SetupCount = 0;\n}\n"
  },
  {
    "path": "src/trx/game/items/walkable.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/types.h>\n\n#include <stdint.h>\n\nvoid Walkable_AllocateNodes(const ITEM *item, int32_t footprint);\nvoid Walkable_Add(int16_t item_num, XYZ_32 pos);\nvoid Walkable_Remove(int16_t item_num);\nvoid Walkable_Reset(void);\nvoid Walkable_ResetLevel(void);\nvoid Walkable_Shutdown(void);\nvoid Walkable_Reposition(\n    int16_t item_num, GAME_VECTOR start, GAME_VECTOR target);\n"
  },
  {
    "path": "src/trx/game/items.h",
    "content": "#pragma once\n\n#include <trx/game/items/actions.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/items/col.h>\n#include <trx/game/items/const.h>\n#include <trx/game/items/draw.h>\n#include <trx/game/items/enum.h>\n#include <trx/game/items/manager.h>\n#include <trx/game/items/types.h>\n#include <trx/game/items/utils.h>\n#include <trx/game/items/walkable.h>\n"
  },
  {
    "path": "src/trx/game/lara/breath.c",
    "content": "#include <trx/game/lara/breath.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/collision.h>\n#include <trx/game/lara.h>\n#include <trx/game/level/settings.h>\n#include <trx/game/output/state.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\nstatic bool M_CanBreatheVisible(const ITEM *const lara_item)\n{\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    if (g_TRVersion != 3) {\n        return false;\n    }\n\n    if (!Level_HasColdWater()) {\n        return false;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->extra_anim) {\n        return false;\n    }\n\n    if (lara->water_status == LWS_CHEAT || lara->water_status == LWS_UNDERWATER\n        || lara_item->hit_points < 0) {\n        return false;\n    }\n\n    if (lara_item->current_anim_state == LS(LS_STOP)) {\n        return Item_GetRelativeFrame(lara_item) >= 30;\n    }\n\n    if (lara_item->current_anim_state == LS(LS_CROUCH_IDLE)) {\n        return Item_GetRelativeFrame(lara_item) >= 30;\n    }\n\n    const int32_t time = (int32_t)Output_GetTimeInGame() % 64;\n    return time >= 32 && time <= 48;\n}\n\nvoid Lara_Breath_Control(const ITEM *const lara_item)\n{\n    if (!M_CanBreatheVisible(lara_item)) {\n        return;\n    }\n\n    XYZ_32 pos = { .x = 0, .y = -4, .z = 64 };\n    Collide_GetJointAbsPosition(lara_item, &pos, LM_HEAD);\n\n    XYZ_32 end = {\n        .x = (Random_GetControl() & 7) - 4,\n        .y = (Random_GetControl() & 7) - 8,\n        .z = (Random_GetControl() & 0x7F) + 64,\n    };\n    Collide_GetJointAbsPosition(lara_item, &end, LM_HEAD);\n\n    const XYZ_32 vel = {\n        .x = end.x - pos.x,\n        .y = end.y - pos.y,\n        .z = end.z - pos.z,\n    };\n\n    Sparks_TriggerBreath(pos, vel, lara_item->room_num);\n}\n"
  },
  {
    "path": "src/trx/game/lara/breath.h",
    "content": "#pragma once\n\n#include <trx/game/items.h>\n\nvoid Lara_Breath_Control(const ITEM *lara_item);\n"
  },
  {
    "path": "src/trx/game/lara/cheat.c",
    "content": "#include <trx/game/lara/cheat.h>\n\n#include <trx/core/vector.h>\n#include <trx/game/camera.h>\n#include <trx/game/console.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\nstatic void M_GiveAllKeysImpl(void)\n{\n    Inv_AddItem(O_PUZZLE_ITEM_1);\n    Inv_AddItem(O_PUZZLE_ITEM_2);\n    Inv_AddItem(O_PUZZLE_ITEM_3);\n    Inv_AddItem(O_PUZZLE_ITEM_4);\n    Inv_AddItem(O_KEY_ITEM_1);\n    Inv_AddItem(O_KEY_ITEM_2);\n    Inv_AddItem(O_KEY_ITEM_3);\n    Inv_AddItem(O_KEY_ITEM_4);\n    Inv_AddItem(O_QUEST_ITEM_1);\n    Inv_AddItem(O_QUEST_ITEM_2);\n    Inv_AddItem(O_QUEST_ITEM_3);\n    Inv_AddItem(O_QUEST_ITEM_4);\n    Inv_AddItem(O_PICKUP_ITEM_1);\n    Inv_AddItem(O_PICKUP_ITEM_2);\n    Inv_AddItem(O_LEADBAR_ITEM);\n}\n\nstatic void M_GiveAllGunsImpl(const bool ignore_exclusions)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const bool bonus_flag = Game_IsBonusFlagSet(GBF_NGPLUS);\n    Inv_AddItem(O_PISTOL_ITEM);\n    if (Lara_Cheat_GiveGun(LGT_SHOTGUN, ignore_exclusions)) {\n        lara_info->shotgun_ammo.ammo = bonus_flag ? 10001 : 300;\n    }\n    if (Lara_Cheat_GiveGun(LGT_MAGNUMS, ignore_exclusions)) {\n        lara_info->magnum_ammo.ammo = bonus_flag ? 10001 : 1000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_AUTOS, ignore_exclusions)) {\n        lara_info->autos_ammo.ammo = bonus_flag ? 10001 : 1000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_DESERT_EAGLE, ignore_exclusions)) {\n        lara_info->desert_eagle_ammo.ammo = bonus_flag ? 10001 : 1000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_UZIS, ignore_exclusions)) {\n        lara_info->uzi_ammo.ammo = bonus_flag ? 10001 : 2000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_HARPOON, ignore_exclusions)) {\n        lara_info->harpoon_ammo.ammo = bonus_flag ? 10001 : 300;\n    }\n    if (Lara_Cheat_GiveGun(LGT_M16, ignore_exclusions)) {\n        lara_info->m16_ammo.ammo = bonus_flag ? 10001 : 300;\n    }\n    if (Lara_Cheat_GiveGun(LGT_MP5, ignore_exclusions)) {\n        lara_info->mp5_ammo.ammo = bonus_flag ? 10001 : 300;\n    }\n    if (Lara_Cheat_GiveGun(LGT_GRENADE, ignore_exclusions)) {\n        lara_info->grenade_ammo.ammo = bonus_flag ? 10001 : 300;\n    }\n    if (Lara_Cheat_GiveGun(LGT_ROCKET, ignore_exclusions)) {\n        lara_info->rocket_ammo.ammo = bonus_flag ? 10001 : 300;\n    }\n}\n\nstatic void M_GiveAllMedpacksImpl(void)\n{\n    if (g_Weapons[LGT_FLARE].is_available) {\n        Inv_AddItemNTimes(O_FLAREBOX_ITEM, 10);\n    }\n    Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, 10);\n    Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, 10);\n}\n\nstatic void M_ReinitialiseGunMeshes(void)\n{\n    Lara_Mesh_Initialise(Game_GetCurrentLevel());\n    Gun_InitialiseNewWeapon();\n}\n\nstatic void M_ClearHandWeaponMeshes(void)\n{\n    Gun_SetLaraHandRMesh(LGT_UNARMED);\n    if (!Lara_Flare_IsMeshActive()) {\n        Gun_SetLaraHandLMesh(LGT_UNARMED);\n    }\n}\n\nstatic void M_ResetGunStatus(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const bool has_flare = Lara_Flare_IsMeshActive();\n    if (has_flare) {\n        lara_info->gun_type = LGT_FLARE;\n        return;\n    }\n\n    lara_info->gun_status = LGS_ARMLESS;\n    lara_info->gun_type = LGT_UNARMED;\n    lara_info->request_gun_type = LGT_UNARMED;\n    lara_info->gun_item_num = NO_ITEM;\n    lara_info->left_arm.frame_num = 0;\n    lara_info->left_arm.lock = 0;\n    lara_info->right_arm.frame_num = 0;\n    lara_info->right_arm.lock = 0;\n    lara_info->left_arm.anim_num = lara_item->anim_num;\n    lara_info->right_arm.anim_num = lara_item->anim_num;\n\n    const ANIM *const anim = Item_GetAnim(lara_item);\n    lara_info->left_arm.frame_base = anim->frame_ptr;\n    lara_info->right_arm.frame_base = anim->frame_ptr;\n}\n\nbool Lara_Cheat_GiveAllKeys(void)\n{\n    if (Lara_GetItem() == nullptr) {\n        return false;\n    }\n\n    M_GiveAllKeysImpl();\n\n    Sound_Effect(SFX_LARA_KEY, nullptr, SPM_ALWAYS);\n    Console_Log(GS(\"general/osd/give_item_all_keys\"));\n    return true;\n}\n\nbool Lara_Cheat_GiveAllGuns(const bool ignore_exclusions)\n{\n    if (Lara_GetItem() == nullptr) {\n        return false;\n    }\n\n    M_GiveAllGunsImpl(ignore_exclusions);\n\n    Sound_Effect(SFX_LARA_RELOAD, nullptr, SPM_ALWAYS);\n    Console_Log(GS(\"general/osd/give_item_all_guns\"));\n    return true;\n}\n\nbool Lara_Cheat_GiveGun(\n    const LARA_GUN_TYPE gun_type, const bool ignore_exclusions)\n{\n    const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type);\n    if (gun_object_id == NO_OBJECT) {\n        return false;\n    }\n\n    if (!ignore_exclusions && !g_Weapons[gun_type].is_available) {\n        return false;\n    }\n\n    return Inv_AddItem(gun_object_id);\n}\n\nbool Lara_Cheat_GiveAllItems(void)\n{\n    if (Lara_GetItem() == nullptr) {\n        return false;\n    }\n\n    M_GiveAllGunsImpl(false);\n    M_GiveAllKeysImpl();\n    M_GiveAllMedpacksImpl();\n\n    Sound_Effect(SFX_LARA_HOLSTER, nullptr, SPM_NORMAL);\n    Console_Log(GS(\"general/osd/give_item_cheat\"));\n    return true;\n}\n\nvoid Lara_Cheat_GetStuff(void)\n{\n    M_GiveAllGunsImpl(false);\n    M_GiveAllMedpacksImpl();\n}\n\nvoid Lara_Cheat_EndLevel(void)\n{\n    Game_SetIsLevelComplete(true);\n    Console_Log(GS(\"general/osd/complete_level\"));\n}\n\nbool Lara_Cheat_KillEnemy(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_KILLED) != 0) {\n        return false;\n    }\n    if (!Item_IsAlive(item) && item->status != IS_ACTIVE) {\n        return false;\n    }\n\n    if (Object_IsType(item->object_id, g_LoyalObjects)) {\n        LARA_INFO *const lara_info = Lara_GetLaraInfo();\n        lara_info->killed_loyal_item = true;\n    }\n\n    Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL);\n    Creature_Die(item_num, true);\n    return true;\n}\n\nbool Lara_Cheat_OpenNearestDoor(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    int32_t opened = 0;\n    int32_t closed = 0;\n\n    const int32_t shift = 8; // constant shift to avoid overflow errors\n    const int32_t max_dist = SQUARE((WALL_L * 2) >> shift);\n    for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (!Object_IsType(item->object_id, g_DoorObjects)\n            && !Object_IsType(item->object_id, g_TrapdoorObjects)) {\n            continue;\n        }\n\n        const int32_t dx = (item->pos.x - lara_item->pos.x) >> shift;\n        const int32_t dy = (item->pos.y - lara_item->pos.y) >> shift;\n        const int32_t dz = (item->pos.z - lara_item->pos.z) >> shift;\n        const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (dist > max_dist) {\n            continue;\n        }\n\n        if (!item->active) {\n            Item_AddActive(item_num);\n            item->flags |= IF_CODE_BITS;\n            opened++;\n        } else if ((item->flags & IF_CODE_BITS) != 0) {\n            item->flags &= ~IF_CODE_BITS;\n            closed++;\n        } else {\n            item->flags |= IF_CODE_BITS;\n            opened++;\n        }\n        item->timer = 0;\n        item->touch_bits = 0;\n    }\n\n    if (opened > 0 || closed > 0) {\n        Console_Log(\n            opened > 0 ? GS(\"general/osd/door_open\")\n                       : GS(\"general/osd/door_close\"));\n        return true;\n    }\n    Console_LogError(GS(\"general/osd/door_open_fail\"));\n    return false;\n}\n\nbool Lara_Cheat_EnterFlyMode(void)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    if ((lara_item->flags & IF_ONE_SHOT) != 0) {\n        // The explosion cheat has been used, so Lara's death is permanent.\n        return false;\n    }\n\n    Viewport_AlterFOV(-1, FOV_MODE_GAME);\n\n    if (lara_info->extra_anim || lara_item->hit_points < 0) {\n        M_ResetGunStatus();\n        M_ClearHandWeaponMeshes();\n    } else if (Gun_IsRifleType(lara_info->gun_type)) {\n        while (lara_info->gun_item_num != NO_ITEM) {\n            Gun_Rifle_Undraw(lara_info->gun_type);\n        }\n    }\n\n    if (lara_info->gun_status == LGS_HANDS_BUSY\n        || (lara_info->gun_status == LGS_UNDRAW\n            && Lara_Skin_GetEquipment(LM_TORSO)->type\n                == EQUIPMENT_TYPE_WEAPON)) {\n        lara_info->gun_status = LGS_ARMLESS;\n        M_ClearHandWeaponMeshes();\n    }\n\n    lara_info->extra_anim = false;\n    Lara_Vehicle_Dismount();\n    if (lara_info->water_status != LWS_UNDERWATER\n        || lara_item->hit_points <= 0) {\n        lara_item->pos.y -= STEP_L;\n        lara_item->current_anim_state = LS(LS_SWIM);\n        lara_item->goal_anim_state = LS(LS_SWIM);\n        Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_SWIM_FORWARD_DRIFT), 0);\n        lara_item->gravity = false;\n        lara_item->rot.x = 30 * DEG_1;\n        lara_item->fall_speed = 30;\n        lara_info->head_rot.x = 0;\n        lara_info->head_rot.y = 0;\n        lara_info->torso_rot.x = 0;\n        lara_info->torso_rot.y = 0;\n    }\n    lara_info->water_status = LWS_CHEAT;\n    lara_info->hit_effect_count = 0;\n    lara_info->hit_effect = nullptr;\n    lara_info->hit_frame = 0;\n    lara_info->hit_direction = DIR_UNKNOWN;\n    lara_info->air = LARA_MAX_AIR;\n    lara_info->death_timer = 0;\n    lara_info->mesh_effects = 0;\n    lara_item->enable_shadow = true;\n    lara_item->hit_points = LARA_MAX_HITPOINTS;\n    lara_info->interact_target.item_num = NO_ITEM;\n    lara_info->interact_target.is_moving = false;\n    lara_info->interact_target.move_count = 0;\n\n    Lara_Extinguish();\n    M_ReinitialiseGunMeshes();\n    Lara_Skin_ApplyOutfit();\n    g_Camera.type = CAM_CHASE;\n\n    Console_Log(GS(\"general/osd/fly_mode_on\"));\n    return true;\n}\n\nbool Lara_Cheat_ExitFlyMode(void)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    const ROOM *const room = Room_Get(lara_item->room_num);\n    const int16_t water_height =\n        Room_GetWaterHeight(lara_item->pos, lara_item->room_num);\n\n    if (room->flags.underwater\n        || (water_height != NO_HEIGHT && water_height > 0\n            && !room->flags.swamp)) {\n        lara_info->water_status = LWS_UNDERWATER;\n    } else {\n        lara_info->water_status =\n            room->flags.swamp ? LWS_WADE : LWS_ABOVE_WATER;\n        Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n        lara_item->goal_anim_state = LS(LS_STOP);\n        lara_item->current_anim_state = LS(LS_STOP);\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n        lara_info->head_rot.x = 0;\n        lara_info->head_rot.y = 0;\n        lara_info->torso_rot.x = 0;\n        lara_info->torso_rot.y = 0;\n    }\n\n    if (lara_info->gun_item_num != NO_ITEM) {\n        lara_info->gun_status = LGS_UNDRAW;\n    } else {\n        lara_info->gun_status = LGS_ARMLESS;\n        M_ClearHandWeaponMeshes();\n        M_ReinitialiseGunMeshes();\n    }\n\n    if (lara_info->water_status == LWS_ABOVE_WATER) {\n        // Prevent Lara from jumping if the player holds the swim button\n        // during the fly cheat exit (#4470)\n        InputState_Clear(&g_Input);\n        InputState_Clear(&g_InputDB);\n        Lara_Control();\n    }\n\n    Console_Log(GS(\"general/osd/fly_mode_off\"));\n    return true;\n}\n\nbool Lara_Cheat_Teleport(XYZ_32 pos, int16_t room_num)\n{\n    if (!Room_FindValidPos(&pos, &room_num)) {\n        return false;\n    }\n\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeightEx(sector, pos, true, NO_ITEM);\n    if (height == NO_HEIGHT) {\n        return false;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    lara_item->pos.x = pos.x;\n    lara_item->pos.y = pos.y;\n    lara_item->pos.z = pos.z;\n    lara_item->floor = height;\n\n    const int16_t item_num = Item_GetIndex(lara_item);\n    Item_UpdateRoom(item_num, room_num);\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->gun_status == LGS_HANDS_BUSY) {\n        lara_info->gun_status = LGS_ARMLESS;\n    }\n\n    Lara_Vehicle_Dismount();\n    if (lara_info->extra_anim) {\n        const ROOM *const room = Room_Get(lara_item->room_num);\n        const bool room_submerged = room->flags.underwater;\n        const int16_t water_height =\n            Room_GetWaterHeight(lara_item->pos, lara_item->room_num);\n\n        if (room_submerged || (water_height != NO_HEIGHT && water_height > 0)) {\n            lara_info->water_status = LWS_UNDERWATER;\n            lara_item->current_anim_state = LS(LS_SWIM);\n            lara_item->goal_anim_state = LS(LS_SWIM);\n            Item_SwitchToAnim(\n                lara_item, LA(LA_UNDERWATER_SWIM_FORWARD_DRIFT), 0);\n        } else {\n            lara_info->water_status = LWS_ABOVE_WATER;\n            lara_item->current_anim_state = LS(LS_STOP);\n            lara_item->goal_anim_state = LS(LS_STOP);\n            Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n            lara_item->rot.x = 0;\n            lara_item->rot.z = 0;\n            lara_info->head_rot.x = 0;\n            lara_info->head_rot.y = 0;\n            lara_info->torso_rot.x = 0;\n            lara_info->torso_rot.y = 0;\n        }\n\n        lara_info->extra_anim = false;\n        M_ResetGunStatus();\n        M_ReinitialiseGunMeshes();\n    }\n\n    lara_info->hit_effect_count = 0;\n    lara_info->hit_effect = nullptr;\n    lara_info->hit_frame = 0;\n    lara_info->hit_direction = DIR_UNKNOWN;\n    lara_info->air = LARA_MAX_AIR;\n    lara_info->death_timer = 0;\n    lara_info->mesh_effects = 0;\n\n    if (g_Camera.type == CAM_PHOTO_MODE) {\n        Lara_Hair_Control(false);\n        Interpolation_CommitLara();\n    } else {\n        g_Camera.type = CAM_CHASE;\n        Viewport_AlterFOV(-1, FOV_MODE_GAME);\n        Camera_ResetPosition();\n    }\n\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/lara/cheat.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n#include <trx/game/lara/types.h>\n\nbool Lara_Cheat_GiveAllKeys(void);\nbool Lara_Cheat_GiveAllGuns(bool ignore_exclusions);\nbool Lara_Cheat_GiveGun(LARA_GUN_TYPE gun_type, bool ignore_exclusions);\nbool Lara_Cheat_GiveAllItems(void);\nvoid Lara_Cheat_GetStuff(void);\nvoid Lara_Cheat_EndLevel(void);\nbool Lara_Cheat_KillEnemy(int16_t item_num);\nbool Lara_Cheat_OpenNearestDoor(void);\nbool Lara_Cheat_EnterFlyMode(void);\nbool Lara_Cheat_ExitFlyMode(void);\nbool Lara_Cheat_Teleport(XYZ_32 pos, int16_t room_num);\n"
  },
  {
    "path": "src/trx/game/lara/cheat_keys.c",
    "content": "#include <trx/game/lara/cheat_keys.h>\n\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\n#define M_MIN_TURN 94208\n\ntypedef enum {\n    // clang-format off\n    CHEAT_INITIAL,\n    CHEAT_STEP_FORWARD,\n    CHEAT_STEP_FORWARD_STOP,\n    CHEAT_STEP_BACK,\n    CHEAT_STEP_BACK_STOP,\n    CHEAT_TURN_LEFT,\n    CHEAT_TURN_RIGHT,\n    CHEAT_TURN_STOP,\n    CHEAT_FINAL_JUMP,\n    // clang-format on\n} M_CHEAT_STATE;\n\nstatic int32_t m_CheatState = CHEAT_INITIAL;\nstatic LARA_GUN_TYPE m_InitialGunType = LGT_UNARMED;\nstatic LARA_GUN_STATE m_InitialGunState = LGS_ARMLESS;\nstatic int16_t m_CheatAngle = 0;\nstatic int32_t m_CheatTurn = 0;\n\nstatic void M_CompleteLevel(void)\n{\n    Game_SetIsLevelComplete(true);\n}\n\nstatic void M_GiveItems(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (Lara_Cheat_GiveGun(LGT_SHOTGUN, false)) {\n        lara_info->shotgun_ammo.ammo = 500;\n    }\n    if (Lara_Cheat_GiveGun(LGT_MAGNUMS, false)) {\n        lara_info->magnum_ammo.ammo = 500;\n    }\n    if (Lara_Cheat_GiveGun(LGT_AUTOS, false)) {\n        lara_info->autos_ammo.ammo = 500;\n    }\n    if (Lara_Cheat_GiveGun(LGT_DESERT_EAGLE, false)) {\n        lara_info->desert_eagle_ammo.ammo = 500;\n    }\n    if (Lara_Cheat_GiveGun(LGT_UZIS, false)) {\n        lara_info->uzi_ammo.ammo = 5000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_HARPOON, false)) {\n        lara_info->harpoon_ammo.ammo = 5000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_GRENADE, false)) {\n        lara_info->grenade_ammo.ammo = 5000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_ROCKET, false)) {\n        lara_info->rocket_ammo.ammo = 5000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_M16, false)) {\n        lara_info->m16_ammo.ammo = 5000;\n    }\n    if (Lara_Cheat_GiveGun(LGT_MP5, false)) {\n        lara_info->mp5_ammo.ammo = 5000;\n    }\n    Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, 50);\n    Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, 50);\n    if (g_Weapons[LGT_FLARE].is_available) {\n        Inv_AddItemNTimes(O_FLARE_ITEM, 50);\n    }\n    Sound_Effect(SFX_LARA_HOLSTER, nullptr, SPM_ALWAYS);\n}\n\nstatic void M_ExplodeLara(void)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n    Item_Explode(lara_info->item_num, -1, 1);\n    Sound_Effect(SFX_EXPLOSION_1, &lara_item->pos, SPM_NORMAL);\n    lara_item->hit_points = 0;\n    lara_item->status = IS_INVISIBLE;\n    lara_item->collidable = false;\n    lara_item->flags |= IF_INVISIBLE;\n}\n\nstatic bool M_ProcessOutcome(\n    const LARA_INFO *const lara_info, const ITEM *const lara_item)\n{\n    if (lara_item->fall_speed <= 0) {\n        return false;\n    }\n\n    const LARA_STATE state = lara_item->current_anim_state;\n\n    switch (g_TRVersion) {\n    case 1:\n        if (state == LS(LS_JUMP_FORWARD)) {\n            M_CompleteLevel();\n        } else if (state == LS(LS_JUMP_BACK)) {\n            M_GiveItems();\n        } else if (state == LS(LS_SWAN_DIVE)) {\n            M_ExplodeLara();\n        }\n        break;\n\n    case 2:\n        if (m_InitialGunType == LGT_FLARE\n            && lara_info->gun_type == m_InitialGunType\n            && lara_info->gun_status == m_InitialGunState) {\n            if (state == LS(LS_JUMP_FORWARD)) {\n                M_CompleteLevel();\n            } else if (state == LS(LS_JUMP_BACK)) {\n                M_GiveItems();\n            }\n        } else if (state == LS(LS_JUMP_FORWARD) || state == LS(LS_JUMP_BACK)) {\n            M_ExplodeLara();\n        }\n        break;\n\n    case 3:\n        if (m_InitialGunType == LGT_PISTOLS && m_InitialGunState == LGS_READY\n            && lara_info->gun_type == m_InitialGunType\n            && lara_info->gun_status == m_InitialGunState) {\n            if (state == LS(LS_JUMP_FORWARD)) {\n                M_CompleteLevel();\n            } else if (state == LS(LS_JUMP_BACK)) {\n                M_GiveItems();\n            }\n        } else if (state == LS(LS_JUMP_FORWARD) || state == LS(LS_JUMP_BACK)) {\n            M_ExplodeLara();\n        }\n    }\n\n    return true;\n}\n\nstatic LARA_STATE M_GetBackstepState(void)\n{\n    return g_TRVersion == 3 ? LS(LS_CROUCH_IDLE) : LS(LS_WALK_BACK);\n}\n\nvoid Lara_Cheat_CheckKeys(void)\n{\n    if (Game_IsInGym()) {\n        return;\n    }\n\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_STATE ls = lara_item->current_anim_state;\n    const LARA_STATE backstep_state = M_GetBackstepState();\n\n    switch (m_CheatState) {\n    case CHEAT_INITIAL:\n        m_CheatState = ls == LS(LS_WALK) ? CHEAT_STEP_FORWARD : CHEAT_INITIAL;\n        break;\n\n    case CHEAT_STEP_FORWARD:\n        m_InitialGunType = lara_info->gun_type;\n        m_InitialGunState = lara_info->gun_status;\n        if (ls != LS(LS_WALK)) {\n            m_CheatState =\n                ls == LS(LS_STOP) ? CHEAT_STEP_FORWARD_STOP : CHEAT_INITIAL;\n        }\n        break;\n\n    case CHEAT_STEP_FORWARD_STOP:\n        if (ls != LS(LS_STOP)) {\n            m_CheatState =\n                ls == backstep_state ? CHEAT_STEP_BACK : CHEAT_INITIAL;\n        }\n        break;\n\n    case CHEAT_STEP_BACK:\n        if (ls != backstep_state) {\n            m_CheatState =\n                ls == LS(LS_STOP) ? CHEAT_STEP_BACK_STOP : CHEAT_INITIAL;\n        }\n        break;\n\n    case CHEAT_STEP_BACK_STOP:\n        if (ls != LS(LS_STOP)) {\n            m_CheatTurn = 0;\n            m_CheatAngle = lara_item->rot.y;\n            if (ls == LS(LS_TURN_LEFT)) {\n                m_CheatState = CHEAT_TURN_LEFT;\n            } else if (ls == LS(LS_TURN_RIGHT)) {\n                m_CheatState = CHEAT_TURN_RIGHT;\n            } else {\n                m_CheatState = CHEAT_INITIAL;\n            }\n        }\n        break;\n\n    case CHEAT_TURN_LEFT:\n        if (ls != LS(LS_TURN_LEFT) && ls != LS(LS_FAST_TURN)) {\n            m_CheatState =\n                m_CheatTurn < -M_MIN_TURN ? CHEAT_TURN_STOP : CHEAT_INITIAL;\n        } else {\n            m_CheatTurn += (int16_t)(lara_item->rot.y - m_CheatAngle);\n            m_CheatAngle = lara_item->rot.y;\n        }\n        break;\n\n    case CHEAT_TURN_RIGHT:\n        if (ls != LS(LS_TURN_RIGHT) && ls != LS(LS_FAST_TURN)) {\n            m_CheatState =\n                m_CheatTurn > M_MIN_TURN ? CHEAT_TURN_STOP : CHEAT_INITIAL;\n        } else {\n            m_CheatTurn += (int16_t)(lara_item->rot.y - m_CheatAngle);\n            m_CheatAngle = lara_item->rot.y;\n        }\n        break;\n\n    case CHEAT_TURN_STOP:\n        if (ls != LS(LS_STOP)) {\n            m_CheatState =\n                ls == LS(LS_COMPRESS) ? CHEAT_FINAL_JUMP : CHEAT_INITIAL;\n        }\n        break;\n\n    case CHEAT_FINAL_JUMP:\n        if (M_ProcessOutcome(lara_info, lara_item)) {\n            m_CheatState = CHEAT_INITIAL;\n        }\n        break;\n\n    default:\n        m_CheatState = CHEAT_INITIAL;\n        break;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lara/cheat_keys.h",
    "content": "#pragma once\n\nvoid Lara_Cheat_CheckKeys(void);\n"
  },
  {
    "path": "src/trx/game/lara/col/climb.c",
    "content": "#include <trx/config.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_CLIMB_SHIFT            70\n#define M_CLIMB_HANG             900\n#define M_CLIMB_WIDTH_LEFT       80\n#define M_CLIMB_WIDTH_RIGHT      120\n#define M_CLIMB_HEIGHT           (WALL_L / 2) // = 512\n#define M_VAULT_ANGLE            (30 * DEG_1) // = 5460\n#define M_VAULT_GAP              (-LARA_HEIGHT + STEP_L / 8) // = -730\n#define M_LF_HANG                21\n#define M_LF_STOP_HANG           9\n#define M_LF_CLIMB_L_SHIFT_START 28\n#define M_LF_CLIMB_L_SHIFT_END   29\n#define M_LF_CLIMB_R_SHIFT       57\n#define M_LEDGE_JUMP_PUSH_HEIGHT (STEP_L - 16)                    // = 240\n#define M_LEDGE_JUMP_HEIGHT_UP   (LARA_HEIGHT + (STEP_L * 3) / 8) // = 858\n#define M_LEDGE_JUMP_HEIGHT_BACK (LARA_HEIGHT - (STEP_L * 5) / 4) // = 442\n#define M_HANG_SHIFT             (g_TRVersion >= 3 ? 4 : 2)\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    CLIMB_RESULT_CRAWL = -2,\n    CLIMB_RESULT_NEG  = -1,\n    CLIMB_RESULT_NONE = 0,\n    CLIMB_RESULT_POS  = 1,\n    // clang-format on\n} M_CLIMB_RESULT;\n\nstatic M_CLIMB_RESULT M_TestClimbPos(\n    const ITEM *const item, const int32_t front, const int32_t right,\n    const int32_t origin, const int32_t item_height, int32_t *const shift)\n{\n    const int32_t y = item->pos.y + origin;\n    int32_t x;\n    int32_t z;\n    int32_t x_front = 0;\n    int32_t z_front = 0;\n\n    switch (Math_GetDirection(item->rot.y)) {\n    case DIR_NORTH:\n        x = item->pos.x + right;\n        z = item->pos.z + front;\n        z_front = M_HANG_SHIFT;\n        break;\n\n    case DIR_EAST:\n        x = item->pos.x + front;\n        z = item->pos.z - right;\n        x_front = M_HANG_SHIFT;\n        break;\n\n    case DIR_SOUTH:\n        x = item->pos.x - right;\n        z = item->pos.z - front;\n        z_front = -M_HANG_SHIFT;\n        break;\n\n    case DIR_WEST:\n        x = item->pos.x - front;\n        z = item->pos.z + right;\n        x_front = -M_HANG_SHIFT;\n        break;\n\n    default:\n        x = front;\n        z = front;\n        break;\n    }\n\n    *shift = 0;\n    bool hang = true;\n    if (!Lara_GetLaraInfo()->climb_status) {\n        return CLIMB_RESULT_NONE;\n    }\n\n    int16_t room_num = item->room_num;\n    XYZ_32 sample_pos = { x, y - 128, z };\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n    sample_pos.y = y;\n    int32_t height = Room_GetHeight(sector, sample_pos);\n    if (height == NO_HEIGHT) {\n        return CLIMB_RESULT_NONE;\n    }\n\n    height -= y + item_height + STEP_L / 2;\n    if (height < -M_CLIMB_SHIFT) {\n        return CLIMB_RESULT_NONE;\n    }\n    if (height < 0) {\n        *shift = height;\n    }\n\n    int32_t ceiling = Room_GetCeiling(sector, sample_pos) - y;\n    if (ceiling > M_CLIMB_SHIFT) {\n        return CLIMB_RESULT_NONE;\n    }\n    if (ceiling > 0) {\n        if (*shift) {\n            return CLIMB_RESULT_NONE;\n        }\n        *shift = ceiling;\n    }\n\n    if (item_height + height < M_CLIMB_HANG) {\n        hang = false;\n    }\n\n    const int32_t x2 = x + x_front;\n    const int32_t z2 = z + z_front;\n    sample_pos.x = x2;\n    sample_pos.y = y;\n    sample_pos.z = z2;\n    sector = Room_GetSector(sample_pos, &room_num);\n    height = Room_GetHeight(sector, sample_pos);\n    if (height != NO_HEIGHT) {\n        height -= y;\n    }\n\n    if (height > M_CLIMB_SHIFT) {\n        ceiling = Room_GetCeiling(sector, sample_pos) - y;\n        if (ceiling >= M_CLIMB_HEIGHT) {\n            return CLIMB_RESULT_POS;\n        }\n\n        if (ceiling > M_CLIMB_HEIGHT - M_CLIMB_SHIFT) {\n            if (*shift > 0) {\n                return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE;\n            }\n            *shift = ceiling - M_CLIMB_HEIGHT;\n            return CLIMB_RESULT_POS;\n        }\n\n        if (ceiling > 0) {\n            return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE;\n        }\n\n        if (ceiling > -M_CLIMB_SHIFT && hang && *shift <= 0) {\n            if (*shift > ceiling) {\n                *shift = ceiling;\n            }\n\n            return CLIMB_RESULT_NEG;\n        }\n\n        return CLIMB_RESULT_NONE;\n    }\n\n    if (height > 0) {\n        if (*shift < 0) {\n            return CLIMB_RESULT_NONE;\n        }\n        if (height > *shift) {\n            *shift = height;\n        }\n    }\n\n    room_num = item->room_num;\n    sample_pos = (XYZ_32) { x, item_height + y, z };\n    Room_GetSector(sample_pos, &room_num);\n    sample_pos.x = x2;\n    sample_pos.z = z2;\n    sector = Room_GetSector(sample_pos, &room_num);\n    ceiling = Room_GetCeiling(sector, sample_pos);\n    if (ceiling == NO_HEIGHT) {\n        return CLIMB_RESULT_POS;\n    }\n\n    ceiling -= y;\n    if (ceiling <= height) {\n        return CLIMB_RESULT_POS;\n    }\n\n    if (ceiling >= M_CLIMB_HEIGHT) {\n        return CLIMB_RESULT_POS;\n    }\n\n    if (ceiling > M_CLIMB_HEIGHT - M_CLIMB_SHIFT) {\n        if (*shift > 0) {\n            return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE;\n        }\n        *shift = ceiling - M_CLIMB_HEIGHT;\n        return CLIMB_RESULT_POS;\n    }\n\n    return hang ? CLIMB_RESULT_NEG : CLIMB_RESULT_NONE;\n}\n\nstatic bool M_TestClimbStance(ITEM *const item, COLL_INFO *const coll)\n{\n    int32_t shift_r;\n    const M_CLIMB_RESULT result_r = M_TestClimbPos(\n        item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, -700,\n        STEP_L * 2, &shift_r);\n    if (result_r != CLIMB_RESULT_POS) {\n        return false;\n    }\n\n    int32_t shift_l;\n    const M_CLIMB_RESULT result_l = M_TestClimbPos(\n        item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), -700,\n        STEP_L * 2, &shift_l);\n    if (result_l != CLIMB_RESULT_POS) {\n        return false;\n    }\n\n    int32_t shift = 0;\n    if (shift_r) {\n        if (shift_l) {\n            if ((shift_r < 0) != (shift_l < 0)) {\n                return false;\n            }\n            if (shift_r < 0 && shift_l < shift_r) {\n                shift = shift_l;\n            } else if (shift_r > 0 && shift_l > shift_r) {\n                shift = shift_l;\n            } else {\n                shift = shift_r;\n            }\n        } else {\n            shift = shift_r;\n        }\n    } else if (shift_l) {\n        shift = shift_l;\n    }\n\n    item->pos.y += shift;\n    return true;\n}\n\nstatic bool M_TestHangStop(\n    const ITEM *const item, const COLL_INFO *const coll, const bool front_floor,\n    int32_t *const height_diff)\n{\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    *height_diff = coll->side_front.floor - bounds->min.y;\n    return ABS(coll->side_left2.floor - coll->side_right2.floor) >= SLOPE_DIF\n        || coll->side_mid.ceiling >= 0 || coll->coll_type != COLL_FRONT\n        || front_floor || coll->hit_static || *height_diff < -SLOPE_DIF\n        || *height_diff > SLOPE_DIF;\n}\n\nvoid Lara_Col_HangTest(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = NO_BAD_NEG;\n    coll->bad_ceiling = 0;\n    Lara_Col_GetInfo(item, coll);\n    const bool flag = coll->side_front.floor < 200;\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n\n    const DIRECTION dir = Math_GetDirection(item->rot.y);\n    switch (dir) {\n    case DIR_NORTH:\n        item->pos.z += M_HANG_SHIFT;\n        break;\n    case DIR_EAST:\n        item->pos.x += M_HANG_SHIFT;\n        break;\n    case DIR_SOUTH:\n        item->pos.z -= M_HANG_SHIFT;\n        break;\n    case DIR_WEST:\n        item->pos.x -= M_HANG_SHIFT;\n        break;\n    default:\n        break;\n    }\n\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    Lara_Col_GetInfo(item, coll);\n\n    if (lara->climb_status) {\n        if (!g_Input.action || item->hit_points <= 0) {\n            XYZ_32 pos = {\n                .x = 0,\n                .y = 0,\n                .z = 0,\n            };\n            Collide_GetJointAbsPosition(item, &pos, 0);\n            if (dir == DIR_NORTH || dir == DIR_SOUTH) {\n                item->pos.x = pos.x;\n            } else {\n                item->pos.z = pos.z;\n            }\n\n            item->goal_anim_state = LS(LS_JUMP_FORWARD);\n            item->current_anim_state = LS(LS_JUMP_FORWARD);\n            Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n            item->pos.y += STEP_L;\n            item->gravity = true;\n            item->speed = 2;\n            item->fall_speed = 1;\n            lara->gun_status = LGS_ARMLESS;\n            return;\n        }\n\n        if (!Lara_Col_TestLadderHang(item, coll)) {\n            int32_t height_diff = 0;\n            if ((item->current_anim_state != LS(LS_SHIMMY_LEFT)\n                 && item->current_anim_state != LS(LS_SHIMMY_RIGHT))\n                || M_TestHangStop(item, coll, flag, &height_diff)) {\n                item->pos = coll->old;\n                item->goal_anim_state = LS(LS_HANG);\n                item->current_anim_state = LS(LS_HANG);\n                Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), M_LF_HANG);\n            }\n            return;\n        }\n\n        if (Item_TestAnimEqual(item, LA(LA_REACH_TO_HANG))\n            && Item_TestFrameEqual(item, M_LF_HANG)\n            && M_TestClimbStance(item, coll)) {\n            item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        }\n        return;\n    }\n\n    if (!g_Input.action || item->hit_points <= 0\n        || coll->side_front.floor > 0) {\n        item->goal_anim_state = LS(LS_JUMP_UP);\n        item->current_anim_state = LS(LS_JUMP_UP);\n        Item_SwitchToAnim(item, LA(LA_JUMP_UP), M_LF_STOP_HANG);\n        const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n        if (g_Config.gameplay.enable_swing_cancel && item->hit_points > 0) {\n            item->pos.y += bounds->max.y;\n        } else {\n            item->pos.y += coll->side_front.floor - bounds->min.y + 2;\n        }\n        item->pos.x += coll->shift.x;\n        item->pos.z += coll->shift.z;\n        item->gravity = true;\n        item->speed = 2;\n        item->fall_speed = 1;\n        lara->gun_status = LGS_ARMLESS;\n        return;\n    }\n\n    int32_t height_diff = 0;\n    if (M_TestHangStop(item, coll, flag, &height_diff)) {\n        item->pos = coll->old;\n        if (item->current_anim_state == LS(LS_SHIMMY_LEFT)\n            || item->current_anim_state == LS(LS_SHIMMY_RIGHT)) {\n            item->goal_anim_state = LS(LS_HANG);\n            item->current_anim_state = LS(LS_HANG);\n            Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), M_LF_HANG);\n        }\n        return;\n    }\n\n    switch (dir) {\n    case DIR_NORTH:\n    case DIR_SOUTH:\n        item->pos.z += coll->shift.z;\n        break;\n\n    case DIR_EAST:\n    case DIR_WEST:\n        item->pos.x += coll->shift.x;\n        break;\n\n    default:\n        break;\n    }\n\n    if (g_TRVersion >= 2 || (height_diff >= -STEP_L && height_diff <= STEP_L)) {\n        item->pos.y += height_diff;\n    }\n}\n\nstatic bool M_TestLadderRelease(ITEM *const item)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    if (g_Input.action && item->hit_points > 0) {\n        return false;\n    }\n\n    item->goal_anim_state = LS(LS_JUMP_FORWARD);\n    item->current_anim_state = LS(LS_JUMP_FORWARD);\n    Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n    item->gravity = true;\n    item->speed = 2;\n    item->fall_speed = 1;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->gun_status = LGS_ARMLESS;\n    return true;\n}\n\nstatic M_CLIMB_RESULT M_TestClimbUpPos(\n    const ITEM *const item, const int32_t front, const int32_t right,\n    int32_t *const shift, int32_t *const ledge)\n{\n    const int32_t y = item->pos.y - M_CLIMB_HEIGHT - STEP_L;\n    int32_t x;\n    int32_t z;\n    int32_t x_front = 0;\n    int32_t z_front = 0;\n\n    switch (Math_GetDirection(item->rot.y)) {\n    case DIR_NORTH:\n        x = item->pos.x + right;\n        z = item->pos.z + front;\n        z_front = M_HANG_SHIFT;\n        break;\n\n    case DIR_EAST:\n        x = item->pos.x + front;\n        z = item->pos.z - right;\n        x_front = M_HANG_SHIFT;\n        break;\n\n    case DIR_SOUTH:\n        x = item->pos.x - right;\n        z = item->pos.z - front;\n        z_front = -M_HANG_SHIFT;\n        break;\n\n    case DIR_WEST:\n        z = item->pos.z + right;\n        x = item->pos.x - front;\n        x_front = -M_HANG_SHIFT;\n        break;\n\n    default:\n        x = front;\n        z = front;\n        break;\n    }\n\n    *shift = 0;\n\n    const SECTOR *sector;\n    int32_t height;\n    int32_t ceiling;\n\n    int16_t room_num = item->room_num;\n    XYZ_32 sample_pos = { x, y, z };\n    sector = Room_GetSector(sample_pos, &room_num);\n    ceiling = Room_GetCeiling(sector, sample_pos) + STEP_L - y;\n    if (ceiling > M_CLIMB_SHIFT) {\n        return CLIMB_RESULT_NONE;\n    }\n\n    if (ceiling > 0) {\n        *shift = ceiling;\n    }\n\n    const int32_t x2 = x + x_front;\n    const int32_t z2 = z + z_front;\n    sample_pos.x = x2;\n    sample_pos.z = z2;\n    sector = Room_GetSector(sample_pos, &room_num);\n    height = Room_GetHeightEx(sector, sample_pos, true, NO_ITEM);\n    if (height == NO_HEIGHT) {\n        *ledge = NO_HEIGHT;\n        return CLIMB_RESULT_POS;\n    }\n\n    height -= y;\n    *ledge = height;\n    if (height > STEP_L / 2) {\n        ceiling = Room_GetCeiling(sector, sample_pos) - y;\n        if (ceiling >= M_CLIMB_HEIGHT) {\n            return CLIMB_RESULT_POS;\n        }\n\n        if (height - ceiling > LARA_HEIGHT) {\n            *shift = height;\n            return CLIMB_RESULT_NEG;\n        }\n\n        if (g_Config.gameplay.enable_crawling\n            && height - ceiling >= M_CLIMB_HEIGHT) {\n            return CLIMB_RESULT_CRAWL;\n        }\n\n        return CLIMB_RESULT_NONE;\n    }\n\n    if (height > 0 && height > *shift) {\n        *shift = height;\n    }\n\n    room_num = item->room_num;\n    sample_pos = (XYZ_32) { x, y + M_CLIMB_HEIGHT, z };\n    Room_GetSector(sample_pos, &room_num);\n    sample_pos.x = x2;\n    sample_pos.z = z2;\n    sector = Room_GetSector(sample_pos, &room_num);\n    ceiling = Room_GetCeiling(sector, sample_pos) - y;\n    if (ceiling <= height) {\n        return CLIMB_RESULT_POS;\n    }\n\n    if (ceiling >= M_CLIMB_HEIGHT) {\n        return CLIMB_RESULT_POS;\n    }\n    return CLIMB_RESULT_NONE;\n}\n\nstatic bool M_TestLedgeJump(const ITEM *const item, const COLL_INFO *const coll)\n{\n    if (!g_Input.jump || !(g_Input.forward ^ g_Input.back)\n        || (g_Input.forward && g_Input.slow)\n        || !g_Config.gameplay.enable_ledge_jumps\n        || !Lara_State_IsResponsive(LA_REACH_TO_HANG)) {\n        return false;\n    }\n\n    // Lara needs sufficient space above to avoid the animation pushing her into\n    // the ceiling.\n    const int32_t jump_height =\n        g_Input.forward ? M_LEDGE_JUMP_HEIGHT_UP : M_LEDGE_JUMP_HEIGHT_BACK;\n    if (coll->side_mid.ceiling >= -jump_height) {\n        return false;\n    }\n\n    // Test for a solid surface in front of Lara to push against.\n    const XYZ_32 pos = {\n        .x = item->pos.x + ((Math_Sin(item->rot.y) * STEP_L) >> W2V_SHIFT),\n        .z = item->pos.z + ((Math_Cos(item->rot.y) * STEP_L) >> W2V_SHIFT),\n        .y = item->pos.y,\n    };\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    return height == NO_HEIGHT || height < pos.y\n        || (ceiling - pos.y) >= -M_LEDGE_JUMP_PUSH_HEIGHT;\n}\n\nstatic void M_Hang(ITEM *const item, COLL_INFO *const coll)\n{\n    Lara_Col_HangTest(item, coll);\n    if (item->goal_anim_state != LS(LS_HANG)) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!lara->climb_status && M_TestLedgeJump(item, coll)) {\n        item->goal_anim_state = LS(g_Input.forward ? LS_JUMP_UP : LS_JUMP_BACK);\n        return;\n    }\n\n    if (g_Input.forward) {\n        if (coll->side_front.floor > -850 && coll->side_front.floor < -650\n            && coll->side_front.floor - coll->side_front.ceiling >= 0\n            && coll->side_left2.floor - coll->side_left2.ceiling >= 0\n            && coll->side_right2.floor - coll->side_right2.ceiling >= 0\n            && !coll->hit_static) {\n            item->goal_anim_state = LS(g_Input.slow ? LS_GYMNAST : LS_PULL_UP);\n            return;\n        } else if (\n            lara->climb_status && Item_TestAnimEqual(item, LA(LA_REACH_TO_HANG))\n            && Item_TestFrameEqual(item, M_LF_HANG)\n            && coll->side_mid.ceiling <= -256) {\n            item->goal_anim_state = LS(LS_HANG);\n            item->current_anim_state = LS(LS_HANG);\n            Item_SwitchToAnim(item, LA(LA_LADDER_UP_HANGING), 0);\n            return;\n        }\n    }\n\n    if (g_Config.gameplay.enable_crawling && (g_Input.forward || g_Input.crouch)\n        && coll->side_front.floor > -850 && coll->side_front.floor < -650\n        && coll->side_front.floor - coll->side_front.ceiling >= -256\n        && coll->side_left2.floor - coll->side_left2.ceiling >= -256\n        && coll->side_right2.floor - coll->side_right2.ceiling >= -256\n        && !coll->hit_static) {\n        item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL);\n        item->required_anim_state = LS(LS_CROUCH_IDLE);\n        lara->crouching = true;\n    } else if (\n        g_Input.back && lara->climb_status\n        && Item_TestAnimEqual(item, LA(LA_REACH_TO_HANG))\n        && Item_TestFrameEqual(item, M_LF_HANG)) {\n        item->goal_anim_state = LS(LS_HANG);\n        item->current_anim_state = LS(LS_HANG);\n        Item_SwitchToAnim(item, LA(LA_LADDER_DOWN_HANGING), 0);\n    }\n}\n\nstatic void M_Shimmy(ITEM *const item, COLL_INFO *const coll)\n{\n    const int32_t angle =\n        item->current_anim_state == LS(LS_SHIMMY_LEFT) ? -DEG_90 : DEG_90;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y + angle;\n    Lara_Col_HangTest(item, coll);\n    lara->move_angle = item->rot.y + angle;\n}\n\nstatic void M_StanceLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    if (M_TestLadderRelease(item)\n        || !Item_TestAnimEqual(item, LA(LA_LADDER_IDLE))) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.forward) {\n        if (item->goal_anim_state == LS(LS_PULL_UP)) {\n            return;\n        }\n\n        item->goal_anim_state = LS(LS_CLIMB_STANCE);\n\n        int32_t shift_r = 0;\n        int32_t ledge_r = 0;\n        M_CLIMB_RESULT result_r = M_TestClimbUpPos(\n            item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, &shift_r,\n            &ledge_r);\n\n        int32_t shift_l = 0;\n        int32_t ledge_l = 0;\n        M_CLIMB_RESULT result_l = M_TestClimbUpPos(\n            item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), &shift_l,\n            &ledge_l);\n\n        if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE) {\n            return;\n        }\n\n        if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG\n            || result_r == CLIMB_RESULT_CRAWL\n            || result_l == CLIMB_RESULT_CRAWL) {\n            if (ABS(ledge_l - ledge_r) > 120) {\n                return;\n            }\n            if (result_r == CLIMB_RESULT_NEG && result_l == CLIMB_RESULT_NEG) {\n                item->goal_anim_state = LS(LS_PULL_UP);\n                item->pos.y += (ledge_l + ledge_r) / 2 - STEP_L;\n            } else {\n                item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL);\n                item->required_anim_state = LS(LS_CROUCH_IDLE);\n                lara->crouching = true;\n            }\n            return;\n        }\n\n        int32_t shift = shift_l;\n        if (shift_r) {\n            if (shift_l) {\n                if ((shift_r < 0) != (shift_l < 0)) {\n                    return;\n                }\n                if (shift_r > 0 && shift_r > shift_l) {\n                    shift = shift_r;\n                } else if (shift_r < 0 && shift_r < shift_l) {\n                    shift = shift_r;\n                }\n            } else {\n                shift = shift_r;\n            }\n        }\n\n        item->goal_anim_state = LS(LS_CLIMBING);\n        item->pos.y += shift;\n    } else if (g_Input.back) {\n        if (item->goal_anim_state == LS(LS_HANG)) {\n            return;\n        }\n\n        item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        item->pos.y += STEP_L;\n\n        int32_t shift_r = 0;\n        const M_CLIMB_RESULT result_r = M_TestClimbPos(\n            item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT,\n            -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_r);\n\n        int32_t shift_l = 0;\n        const M_CLIMB_RESULT result_l = M_TestClimbPos(\n            item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT),\n            -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_l);\n\n        item->pos.y -= STEP_L;\n        if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE) {\n            return;\n        }\n\n        int32_t shift = shift_l;\n        if (shift_r && shift_l) {\n            if ((shift_r < 0) != (shift_l < 0)) {\n                return;\n            }\n            if (shift_r < 0 && shift_r < shift_l) {\n                shift = shift_r;\n            } else if (shift_r > 0 && shift_r > shift_l) {\n                shift = shift_r;\n            }\n        }\n\n        if (result_r == CLIMB_RESULT_POS && result_l == CLIMB_RESULT_POS) {\n            item->goal_anim_state = LS(LS_CLIMB_DOWN);\n            item->pos.y += shift;\n        } else {\n            item->goal_anim_state = LS(LS_HANG);\n        }\n    }\n}\n\nstatic void M_SideLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    if (M_TestLadderRelease(item)) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    int32_t right;\n    if (item->current_anim_state == LS(LS_CLIMB_LEFT)) {\n        lara->move_angle = item->rot.y - DEG_90;\n        right = -(coll->radius + M_CLIMB_WIDTH_LEFT);\n    } else {\n        lara->move_angle = item->rot.y + DEG_90;\n        right = coll->radius + M_CLIMB_WIDTH_RIGHT;\n    }\n\n    int32_t shift;\n    const M_CLIMB_RESULT result = M_TestClimbPos(\n        item, coll->radius, right, -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift);\n\n    if (result == CLIMB_RESULT_POS) {\n        if (g_Input.left) {\n            item->goal_anim_state = LS(LS_CLIMB_LEFT);\n        } else if (g_Input.right) {\n            item->goal_anim_state = LS(LS_CLIMB_RIGHT);\n        } else {\n            item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        }\n        item->pos.y += shift;\n        return;\n    }\n\n    if (result != CLIMB_RESULT_NONE) {\n        item->goal_anim_state = LS(LS_HANG);\n        do {\n            Item_Animate(item);\n        } while (item->current_anim_state != LS(LS_HANG));\n        item->pos.x = coll->old.x;\n        item->pos.z = coll->old.z;\n        return;\n    }\n\n    item->pos.x = coll->old.x;\n    item->pos.z = coll->old.z;\n    item->goal_anim_state = LS(LS_CLIMB_STANCE);\n    item->current_anim_state = LS(LS_CLIMB_STANCE);\n    if (coll->old_anim_state == LS(LS_CLIMB_STANCE)) {\n        item->frame_num = coll->old_frame_num;\n        item->anim_num = coll->old_anim_num;\n        Lara_Animate(item);\n    } else {\n        Item_SwitchToAnim(item, LA(LA_LADDER_IDLE), 0);\n    }\n}\n\nstatic void M_UpLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    if (M_TestLadderRelease(item)\n        || !Item_TestAnimEqual(item, LA(LA_LADDER_UP))) {\n        return;\n    }\n\n    int32_t yshift;\n    if (Item_TestFrameEqual(item, 0)) {\n        yshift = 0;\n    } else if (\n        Item_TestFrameRange(\n            item, M_LF_CLIMB_L_SHIFT_START, M_LF_CLIMB_L_SHIFT_END)) {\n        yshift = -STEP_L;\n    } else if (Item_TestFrameEqual(item, M_LF_CLIMB_R_SHIFT)) {\n        yshift = -STEP_L * 2;\n    } else {\n        return;\n    }\n\n    item->pos.y += yshift - STEP_L;\n\n    int32_t shift_r = 0;\n    int32_t ledge_r = 0;\n    M_CLIMB_RESULT result_r = M_TestClimbUpPos(\n        item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, &shift_r,\n        &ledge_r);\n\n    int32_t shift_l = 0;\n    int32_t ledge_l = 0;\n    M_CLIMB_RESULT result_l = M_TestClimbUpPos(\n        item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT), &shift_l,\n        &ledge_l);\n\n    item->pos.y += STEP_L;\n\n    if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE\n        || !g_Input.forward) {\n        item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        if (yshift) {\n            Lara_Animate(item);\n        }\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG\n        || result_r == CLIMB_RESULT_CRAWL || result_l == CLIMB_RESULT_CRAWL) {\n        item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        Lara_Animate(item);\n        if (ABS(ledge_l - ledge_r) <= 120) {\n            if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG) {\n                item->goal_anim_state = LS(LS_PULL_UP);\n                item->pos.y += (ledge_r + ledge_l) / 2 - STEP_L;\n            } else {\n                item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL);\n                item->required_anim_state = LS(LS_CROUCH_IDLE);\n                lara->crouching = true;\n            }\n        }\n        return;\n    }\n\n    item->goal_anim_state = LS(LS_CLIMBING);\n    item->pos.y -= yshift;\n}\n\nstatic void M_DownLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    if (M_TestLadderRelease(item)\n        || !Item_TestAnimEqual(item, LA(LA_LADDER_DOWN))) {\n        return;\n    }\n\n    int32_t yshift;\n    if (Item_TestFrameEqual(item, 0)) {\n        yshift = 0;\n    } else if (\n        Item_TestFrameRange(\n            item, M_LF_CLIMB_L_SHIFT_START, M_LF_CLIMB_L_SHIFT_END)) {\n        yshift = STEP_L;\n    } else if (Item_TestFrameEqual(item, M_LF_CLIMB_R_SHIFT)) {\n        yshift = STEP_L * 2;\n    } else {\n        return;\n    }\n\n    item->pos.y += yshift + STEP_L;\n\n    int32_t shift_r = 0;\n    const M_CLIMB_RESULT result_r = M_TestClimbPos(\n        item, coll->radius, coll->radius + M_CLIMB_WIDTH_RIGHT, -M_CLIMB_HEIGHT,\n        M_CLIMB_HEIGHT, &shift_r);\n\n    int32_t shift_l = 0;\n    const M_CLIMB_RESULT result_l = M_TestClimbPos(\n        item, coll->radius, -(coll->radius + M_CLIMB_WIDTH_LEFT),\n        -M_CLIMB_HEIGHT, M_CLIMB_HEIGHT, &shift_l);\n\n    item->pos.y -= STEP_L;\n\n    if (result_r == CLIMB_RESULT_NONE || result_l == CLIMB_RESULT_NONE\n        || !g_Input.back) {\n        item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        if (yshift != 0) {\n            Lara_Animate(item);\n        }\n        return;\n    }\n\n#if 0\n    int32_t shift = shift_l;\n#endif\n    if (shift_r && shift_l) {\n        if ((shift_r < 0) != (shift_l < 0)) {\n            item->goal_anim_state = LS(LS_CLIMB_STANCE);\n            Lara_Animate(item);\n            return;\n        }\n#if 0\n        if (shift_r < 0 && shift_r < shift_l) {\n            shift = shift_r;\n        } else if (shift_r > 0 && shift_r > shift_l) {\n            shift = shift_r;\n        }\n#endif\n    }\n\n    if (result_r == CLIMB_RESULT_NEG || result_l == CLIMB_RESULT_NEG) {\n        Item_SwitchToAnim(item, LA(LA_LADDER_IDLE), 0);\n        item->current_anim_state = LS(LS_CLIMB_STANCE);\n        item->goal_anim_state = LS(LS_HANG);\n        Lara_Animate(item);\n        return;\n    }\n\n    item->goal_anim_state = LS(LS_CLIMB_DOWN);\n    item->pos.y -= yshift;\n}\n\nbool Lara_Col_TestVault(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (coll->coll_type != COLL_FRONT || !g_Input.action\n        || lara->gun_status != LGS_ARMLESS) {\n        return false;\n    }\n\n    const DIRECTION dir = Math_GetDirectionCone(item->rot.y, M_VAULT_ANGLE);\n    if (dir == DIR_UNKNOWN) {\n        return false;\n    }\n    const int16_t angle = Math_DirectionToAngle(dir);\n\n    const int32_t left_floor = coll->side_left2.floor;\n    const int32_t left_ceiling = coll->side_left2.ceiling;\n    const int32_t right_floor = coll->side_right2.floor;\n    const int32_t right_ceiling = coll->side_right2.ceiling;\n    const int32_t front_floor = coll->side_front.floor;\n    const int32_t front_ceiling = coll->side_front.ceiling;\n    const bool slope = ABS(left_floor - right_floor) >= SLOPE_DIF;\n    const int32_t mid = STEP_L / 2;\n    const ROOM *const room = Room_Get(item->room_num);\n\n    if (front_floor >= -STEP_L * 2 - mid && front_floor <= -STEP_L * 2 + mid) {\n        if (slope || front_floor - front_ceiling < 0\n            || left_floor - left_ceiling < 0 || right_floor - right_ceiling < 0\n            || (room->flags.swamp && lara->water_surface_dist < -768)) {\n            return false;\n        }\n        item->goal_anim_state = LS(LS_STOP);\n        item->current_anim_state = LS(LS_PULL_UP);\n        Item_SwitchToAnim(item, LA(LA_CLIMB_2CLICK), 0);\n        item->pos.y += front_floor + STEP_L * 2;\n        lara->gun_status = LGS_HANDS_BUSY;\n    } else if (\n        front_floor >= -STEP_L * 3 - mid && front_floor <= -STEP_L * 3 + mid) {\n        if (slope || front_floor - front_ceiling < 0\n            || left_floor - left_ceiling < 0 || right_floor - right_ceiling < 0\n            || (room->flags.swamp && lara->water_surface_dist < -768)) {\n            return false;\n        }\n        item->goal_anim_state = LS(LS_STOP);\n        item->current_anim_state = LS(LS_PULL_UP);\n        Item_SwitchToAnim(item, LA(LA_CLIMB_3CLICK), 0);\n        item->pos.y += front_floor + STEP_L * 3;\n        lara->gun_status = LGS_HANDS_BUSY;\n    } else if (\n        !lara->climb_status\n        && front_floor - coll->side_mid.ceiling < M_VAULT_GAP) {\n        return false;\n    } else if (\n        !slope && front_floor >= -STEP_L * 7 - mid\n        && front_floor <= -STEP_L * 4 + mid) {\n        if (room->flags.swamp) {\n            return false;\n        }\n        item->goal_anim_state = LS(LS_JUMP_UP);\n        item->current_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n        lara->calc_fall_speed =\n            -(Math_Sqrt(-2 * GRAVITY * (front_floor + 800)) + 3);\n        Lara_Animate(item);\n    } else if (\n        lara->climb_status && front_floor <= -1920\n        && lara->water_status != LWS_WADE && left_floor <= -STEP_L * 8 + mid\n        && right_floor <= -STEP_L * 8\n        && coll->side_mid.ceiling <= -STEP_L * 8 + mid + LARA_HEIGHT) {\n        item->goal_anim_state = LS(LS_JUMP_UP);\n        item->current_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n        lara->calc_fall_speed = -116;\n        Lara_Animate(item);\n    } else if (\n        lara->climb_status\n        && (front_floor < -STEP_L * 4 || front_ceiling >= LARA_HEIGHT - STEP_L)\n        && coll->side_mid.ceiling <= -STEP_L * 5 + LARA_HEIGHT) {\n        Lara_Col_Shift(coll);\n        if (M_TestClimbStance(item, coll)) {\n            item->goal_anim_state = LS(LS_CLIMB_STANCE);\n            item->current_anim_state = LS(LS_STOP);\n            Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n            Lara_Animate(item);\n            item->rot.y = angle;\n            lara->gun_status = LGS_HANDS_BUSY;\n            lara->sprinting = false;\n            return true;\n        }\n        return false;\n    } else {\n        return false;\n    }\n\n    item->rot.y = angle;\n    Lara_Col_Shift(coll);\n    lara->sprinting = false;\n    lara->crouching = false;\n    return true;\n}\n\nbool Lara_Col_TestLadderHang(ITEM *const item, const COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!lara->climb_status || item->fall_speed < 0) {\n        return false;\n    }\n\n    const DIRECTION dir = Math_GetDirection(item->rot.y);\n    switch (dir) {\n    case DIR_NORTH:\n    case DIR_SOUTH:\n        item->pos.z += coll->shift.z;\n        break;\n\n    case DIR_EAST:\n    case DIR_WEST:\n        item->pos.x += coll->shift.x;\n        break;\n\n    default:\n        break;\n    }\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    const int32_t y = bounds->min.y;\n    const int32_t h = bounds->max.y - y;\n\n    int32_t shift;\n    if (M_TestClimbPos(item, coll->radius, coll->radius, y, h, &shift)\n        == CLIMB_RESULT_NONE) {\n        return false;\n    }\n\n    if (M_TestClimbPos(item, coll->radius, -coll->radius, y, h, &shift)\n        == CLIMB_RESULT_NONE) {\n        return false;\n    }\n\n    const M_CLIMB_RESULT result =\n        M_TestClimbPos(item, coll->radius, 0, y, h, &shift);\n    if (result == CLIMB_RESULT_NEG) {\n        item->pos.y += shift;\n    }\n    return result != CLIMB_RESULT_NONE;\n}\n\n// clang-format off\nREGISTER_LARA_COL(LS_HANG,         M_Hang)\nREGISTER_LARA_COL(LS_SHIMMY_LEFT,  M_Shimmy)\nREGISTER_LARA_COL(LS_SHIMMY_RIGHT, M_Shimmy)\nREGISTER_LARA_COL(LS_CLIMB_STANCE, M_StanceLadder)\nREGISTER_LARA_COL(LS_CLIMB_LEFT,   M_SideLadder)\nREGISTER_LARA_COL(LS_CLIMB_RIGHT,  M_SideLadder)\nREGISTER_LARA_COL(LS_CLIMBING,     M_UpLadder)\nREGISTER_LARA_COL(LS_CLIMB_DOWN,   M_DownLadder)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/col/crouch.c",
    "content": "#include <trx/config.h>\n#include <trx/game/input.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/misc.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n#include <trx/game/rooms/geometry.h>\n#include <trx/game/rooms/utils.h>\n\n// clang-format off\n#define M_CROUCH_RADIUS            200\n#define M_CRAWL_BACK_RADIUS        250\n#define M_CRAWL_BAD_POS            255\n#define M_CRAWL_BAD_NEG           -255\n#define M_CRAWL_BAD_CEILING        400\n#define M_CROUCH_CEILING_THRESHOLD -362\n#define M_CRAWL_TO_HANG_RADIUS     200\n#define M_CRAWL_TO_HANG_HEIGHT     870\n#define M_CRAWL_TO_HANG_XZ_OFFSET  100\n#define M_CRAWL_TO_HANG_FALL_SPEED 512\n#define M_CRAWL_TO_HANG_BAD_CEILING ((STEP_L * 3) / 4) // = 192\n#define M_CRAWL_TO_HANG_FALL_FRAME 9\n#define M_CRAWL_TILT_RADIUS        140\n#define M_CRAWL_TILT_HEIGHT        238\n#define M_CRAWL_TILT_RATE          (DEG_1 * 3)         // = 546\n#define M_CRAWL_TILT_MAX           DEG_45\n// clang-format on\n\nstatic bool M_DeflectEdgeCrawl(ITEM *const item, COLL_INFO *const coll)\n{\n    switch (coll->coll_type) {\n    case COLL_FRONT:\n    case COLL_TOP_FRONT:\n        Lara_Col_Shift(coll);\n        item->speed = 0;\n        item->gravity = false;\n        return true;\n\n    case COLL_LEFT:\n        Lara_Col_Shift(coll);\n        item->rot.y += LARA_TURN_UNDO;\n        break;\n\n    case COLL_RIGHT:\n        Lara_Col_Shift(coll);\n        item->rot.y -= LARA_TURN_UNDO;\n        break;\n\n    default:\n        break;\n    }\n\n    return false;\n}\n\nstatic bool M_HasStaticBehind(const ITEM *const item, const int16_t angle)\n{\n    COLL_INFO test = {\n        .radius = 50,\n    };\n\n    const int32_t x = item->pos.x + ((Math_Sin(angle) * 512) >> W2V_SHIFT);\n    const int32_t z = item->pos.z + ((Math_Cos(angle) * 512) >> W2V_SHIFT);\n    return Collide_CollideStaticObjects(\n        &test, x, item->pos.y, z, item->room_num, 300);\n}\n\nstatic bool M_IsBadDestination(const ITEM *const item, const int16_t angle)\n{\n    XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, STEP_L);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    pos.y = Room_GetHeight(sector, pos) - STEP_L;\n    Room_GetSector(pos, &room_num);\n    const ROOM *const room = Room_Get(room_num);\n    return room->flags.swamp || room->flags.underwater;\n}\n\nstatic void M_Crouch(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->is_crouched = true;\n    lara->move_angle = item->rot.y;\n\n    coll->facing = lara->move_angle;\n    coll->radius = M_CROUCH_RADIUS;\n    coll->bad_pos = STEPUP_HEIGHT;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->slopes_are_walls = 1;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        -LARA_HEIGHT_CROUCH);\n\n    if (Lara_Col_Fallen(item, coll)) {\n        lara->gun_status = LGS_ARMLESS;\n        return;\n    }\n\n    lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD;\n    Lara_Col_Shift(coll);\n    item->pos.y += coll->side_mid.floor;\n\n    const bool crouch_active = g_Config.gameplay.enable_toggle_crouch\n        ? !(lara->crouching && g_InputDB.crouch)\n        : g_Input.crouch;\n\n    if ((!crouch_active || lara->water_status == LWS_WADE)\n        && !lara->keep_crouched\n        && Item_TestAnimEqual(item, LA(LA_CROUCH_IDLE))) {\n        lara->crouching = false;\n        item->goal_anim_state = LS(LS_STOP);\n    } else if (g_Config.gameplay.enable_responsive_crawl) {\n        if (g_Input.left) {\n            item->goal_anim_state = LS(LS_CROUCH_TURN_LEFT);\n        } else if (g_Input.right) {\n            item->goal_anim_state = LS(LS_CROUCH_TURN_RIGHT);\n        }\n    }\n}\n\nstatic void M_CrouchRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n\n    coll->facing = lara->move_angle;\n    coll->radius = M_CROUCH_RADIUS;\n    coll->bad_pos = STEPUP_HEIGHT;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->slopes_are_walls = 1;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        -LARA_HEIGHT_CROUCH);\n\n    if (Lara_Col_Fallen(item, coll)) {\n        lara->gun_status = LGS_ARMLESS;\n    } else if (!Lara_Col_TestSlide(item, coll)) {\n        lara->keep_crouched =\n            coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD;\n\n        if (coll->side_mid.floor < coll->bad_neg\n            || coll->side_front.floor > coll->bad_pos\n            || M_IsBadDestination(item, lara->move_angle)) {\n            item->pos = coll->old;\n            return;\n        }\n\n        Lara_Col_Shift(coll);\n\n        if (coll->coll_type == COLL_TOP || coll->coll_type == COLL_CLAMP) {\n            item->pos = coll->old;\n            item->speed = 0;\n        } else {\n            item->pos.y += coll->side_mid.floor;\n        }\n    }\n}\n\nstatic void M_CrouchTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->is_crouched = true;\n    lara->move_angle = item->rot.y;\n\n    coll->facing = lara->move_angle;\n    coll->radius = M_CROUCH_RADIUS;\n    coll->bad_pos = STEPUP_HEIGHT;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->slopes_are_walls = 1;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        LARA_HEIGHT_CROUCH);\n\n    if (Lara_Col_Fallen(item, coll)) {\n        lara->gun_status = LGS_ARMLESS;\n        return;\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD;\n\n    Lara_Col_Shift(coll);\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_CrawlIdle(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    if (item->goal_anim_state == LS(LS_CRAWL_TO_CLIMB)) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->is_crouched = true;\n    lara->move_angle = item->rot.y;\n\n    coll->facing = lara->move_angle;\n    coll->radius = M_CROUCH_RADIUS;\n    coll->bad_pos = M_CRAWL_BAD_POS;\n    coll->bad_neg = M_CRAWL_BAD_NEG;\n    coll->bad_ceiling = M_CRAWL_BAD_CEILING;\n    coll->slopes_are_walls = 1;\n    coll->slopes_are_pits = 1;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        LARA_HEIGHT_CROUCH);\n    Lara_Col_CrawlTilt(item);\n\n    if (Lara_Col_Fallen(item, coll)) {\n        lara->gun_status = LGS_ARMLESS;\n        return;\n    }\n\n    lara->keep_crouched = coll->side_mid.ceiling >= M_CROUCH_CEILING_THRESHOLD;\n\n    Lara_Col_Shift(coll);\n    item->pos.y += coll->side_mid.floor;\n\n    const bool crouch_active = g_Config.gameplay.enable_toggle_crouch\n        ? lara->crouching\n        : g_Input.crouch;\n    if ((!crouch_active && !lara->keep_crouched) || g_Input.draw\n        || (g_Config.gameplay.enable_toggle_crouch && !g_Input.forward\n            && g_InputDB.crouch)) {\n        item->goal_anim_state = LS(LS_CROUCH_IDLE);\n        return;\n    }\n\n    bool allow_movement = Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))\n        || Item_TestAnimEqual(item, LA(LA_CROUCH_TO_CRAWL_END));\n    if (g_Config.gameplay.enable_responsive_crawl) {\n        allow_movement |=\n            Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_LEFT))\n            || Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT));\n    }\n\n    if (!allow_movement) {\n        return;\n    }\n\n    if (g_Input.forward) {\n        const int16_t h = Lara_FloorFront(item, item->rot.y, 256);\n        if (h < 255 && h > -255 && Room_GetHeightType() != HT_BIG_SLOPE) {\n            item->goal_anim_state = LS(LS_CRAWL_FORWARD);\n        }\n    } else if (g_Input.back) {\n        int32_t h = Lara_CeilingFront(item, item->rot.y, -300, LARA_HEIGHT);\n        if (h == NO_HEIGHT || h > 256) {\n            return;\n        }\n\n        h = Lara_FloorFront(item, item->rot.y, -300);\n        if (h < 255 && h > -255 && Room_GetHeightType() != HT_BIG_SLOPE) {\n            item->goal_anim_state = LS(LS_CRAWL_BACK);\n            return;\n        }\n\n        if (g_Input.action && h > 768\n            && !M_HasStaticBehind(item, item->rot.y + DEG_180)) {\n            const XYZ_32 old_pos = item->pos;\n            const XYZ_16 old_rot = item->rot;\n\n            const DIRECTION dir = Math_GetDirection(item->rot.y);\n            switch (dir) {\n            case DIR_NORTH:\n                item->rot.y = 0;\n                item->pos.z = ROUND_TO_SECTOR(item->pos.z) + 225;\n                break;\n\n            case DIR_EAST:\n                item->rot.y = DEG_90;\n                item->pos.x = ROUND_TO_SECTOR(item->pos.x) + 225;\n                break;\n\n            case DIR_SOUTH:\n                item->rot.y = -DEG_180;\n                item->pos.z = ROUND_TO_SECTOR_END(item->pos.z) - 225;\n                break;\n\n            case DIR_WEST:\n                item->rot.y = -DEG_90;\n                item->pos.x = ROUND_TO_SECTOR_END(item->pos.x) - 225;\n                break;\n            default:\n                break;\n            }\n\n            h = Lara_FloorFront(item, item->rot.y, 0);\n            if (h > 255 || h < -255 || Room_GetHeightType() == HT_BIG_SLOPE) {\n                item->pos = old_pos;\n                item->rot = old_rot;\n            } else {\n                item->goal_anim_state = LS(LS_CRAWL_TO_CLIMB);\n            }\n        }\n    } else if (g_Input.left) {\n        Item_SwitchToAnim(item, LA(LA_CRAWL_TURN_LEFT), 0);\n        item->goal_anim_state = LS(LS_CRAWL_TURN_LEFT);\n        item->current_anim_state = LS(LS_CRAWL_TURN_LEFT);\n    } else if (g_Input.right) {\n        Item_SwitchToAnim(item, LA(LA_CRAWL_TURN_RIGHT), 0);\n        item->goal_anim_state = LS(LS_CRAWL_TURN_RIGHT);\n        item->current_anim_state = LS(LS_CRAWL_TURN_RIGHT);\n    }\n}\n\nstatic void M_CrawlForward(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->is_crouched = true;\n    lara->move_angle = item->rot.y;\n\n    coll->radius = M_CROUCH_RADIUS;\n    coll->bad_pos = M_CRAWL_BAD_POS;\n    coll->bad_neg = M_CRAWL_BAD_NEG;\n    coll->bad_ceiling = M_CRAWL_BAD_CEILING;\n    coll->slopes_are_walls = 1;\n    coll->slopes_are_pits = 1;\n    coll->facing = lara->move_angle;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        -LARA_HEIGHT_CROUCH);\n    Lara_Col_CrawlTilt(item);\n\n    if (M_DeflectEdgeCrawl(item, coll)\n        || M_IsBadDestination(item, lara->move_angle)) {\n        item->current_anim_state = LS(LS_CRAWL_IDLE);\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        if (!Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))) {\n            Item_SwitchToAnim(item, LA(LA_CRAWL_IDLE), 0);\n        }\n    } else if (Lara_Col_Fallen(item, coll)) {\n        lara->gun_status = LGS_ARMLESS;\n    } else {\n        Lara_Col_Shift(coll);\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_CrawlTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        LARA_HEIGHT_CROUCH);\n    Lara_Col_CrawlTilt(item);\n}\n\nstatic void M_CrawlBack(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->is_crouched = true;\n    lara->move_angle = item->rot.y + DEG_180;\n\n    coll->radius = M_CRAWL_BACK_RADIUS;\n    coll->bad_pos = M_CRAWL_BAD_POS;\n    coll->bad_neg = M_CRAWL_BAD_NEG;\n    coll->bad_ceiling = M_CRAWL_BAD_CEILING;\n    coll->slopes_are_walls = 1;\n    coll->slopes_are_pits = 1;\n    coll->facing = lara->move_angle;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        -LARA_HEIGHT_CROUCH);\n    Lara_Col_CrawlTilt(item);\n\n    if (M_DeflectEdgeCrawl(item, coll)\n        || M_IsBadDestination(item, lara->move_angle)) {\n        item->current_anim_state = LS(LS_CRAWL_IDLE);\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        if (!Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))) {\n            Item_SwitchToAnim(item, LA(LA_CRAWL_IDLE), 0);\n        }\n    } else if (Lara_Col_Fallen(item, coll)) {\n        lara->gun_status = LGS_ARMLESS;\n    } else {\n        Lara_Col_Shift(coll);\n        item->pos.y += coll->side_mid.floor;\n        lara->move_angle = item->rot.y;\n    }\n}\n\nstatic void M_CrawlToClimb(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!Item_TestAnimEqual(item, LA(LA_CRAWL_TO_HANG_END))) {\n        return;\n    }\n\n    item->fall_speed = M_CRAWL_TO_HANG_FALL_SPEED;\n    item->pos.y |= 255;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n\n    coll->radius = M_CRAWL_TO_HANG_RADIUS;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_CRAWL_TO_HANG_BAD_CEILING;\n    coll->facing = lara->move_angle;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        M_CRAWL_TO_HANG_HEIGHT);\n\n    int32_t edge = 0;\n    const EDGE_CATCH edge_catch = Lara_Col_TestEdgeCatch(item, coll, &edge);\n    if (edge_catch == EDGE_CATCH_NONE\n        || (edge_catch == EDGE_CATCH_NEG\n            && !Lara_Col_TestLadderHang(item, coll))) {\n        // LA_CRAWL_TO_HANG_END will loop indefinitely, so in cases where Lara\n        // cannot grab the edge, make her fall and she will then either re-grab\n        // it on a better position, or continue falling if the ledge is a slope.\n        Item_SwitchToAnim(item, LA(LA_JUMP_UP), M_CRAWL_TO_HANG_FALL_FRAME);\n        item->current_anim_state = LS(LS_JUMP_UP);\n        item->goal_anim_state = LS(LS_JUMP_UP);\n        item->gravity = true;\n        item->speed = 2;\n        item->fall_speed = 1;\n        lara->gun_status = LGS_ARMLESS;\n        return;\n    }\n\n    const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE);\n    if (dir == DIR_UNKNOWN) {\n        return;\n    }\n    const int16_t angle = Math_DirectionToAngle(dir);\n\n    const SWING_CATCH swing_catch = Lara_Col_TestHangSwingIn(item, angle);\n    if (swing_catch == SWING_CATCH_SLOW) {\n        lara->head_rot.x = 0;\n        lara->head_rot.y = 0;\n        lara->torso_rot.x = 0;\n        lara->torso_rot.y = 0;\n        Item_SwitchToAnim(item, LA(LA_SWING_IN_SLOW), 0);\n    } else if (swing_catch == SWING_CATCH_FAST) {\n        Item_SwitchToAnim(item, LA(LA_SWING_IN_FAST), 0);\n    } else {\n        Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), 0);\n    }\n    const ANIM *const anim = Item_GetAnim(item);\n    item->current_anim_state = anim->current_anim_state;\n    item->goal_anim_state = anim->current_anim_state;\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    if (edge_catch == EDGE_CATCH_POS) {\n        item->pos.y += coll->side_front.floor - bounds->min.y;\n\n        switch (dir) {\n        case DIR_NORTH:\n            item->pos.z =\n                ROUND_TO_SECTOR_END(item->pos.z) - M_CRAWL_TO_HANG_XZ_OFFSET;\n            item->pos.x += coll->shift.x;\n            break;\n\n        case DIR_EAST:\n            item->pos.x =\n                ROUND_TO_SECTOR_END(item->pos.x) - M_CRAWL_TO_HANG_XZ_OFFSET;\n            item->pos.z += coll->shift.z;\n            break;\n\n        case DIR_SOUTH:\n            item->pos.z =\n                ROUND_TO_SECTOR(item->pos.z) + M_CRAWL_TO_HANG_XZ_OFFSET;\n            item->pos.x += coll->shift.x;\n            break;\n\n        case DIR_WEST:\n            item->pos.x =\n                ROUND_TO_SECTOR(item->pos.x) + M_CRAWL_TO_HANG_XZ_OFFSET;\n            item->pos.z += coll->shift.z;\n            break;\n\n        default:\n            break;\n        }\n    } else {\n        item->pos.y = edge - bounds->min.y;\n    }\n\n    item->rot.y = angle;\n    item->speed = 2;\n    item->fall_speed = 1;\n    item->gravity = true;\n    lara->gun_status = LGS_HANDS_BUSY;\n}\n\nstatic XZ_32 M_GetWalkableTilt(const ITEM *const item, const int32_t y_pos)\n{\n    const int32_t base_x = ROUND_TO_SECTOR(item->pos.x);\n    const int32_t base_z = ROUND_TO_SECTOR(item->pos.z);\n    const XZ_32 offsets[3] = {\n        { 1, 1 },\n        { 3, 1 },\n        { 1, 3 },\n    };\n\n    int32_t heights[3] = {};\n    for (int32_t i = 0; i < 3; i++) {\n        const XYZ_32 off_pos = {\n            .x = base_x | (offsets[i].x * STEP_L - 1),\n            .z = base_z | (offsets[i].z * STEP_L - 1),\n            .y = y_pos,\n        };\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(off_pos, &room_num);\n        heights[i] = Room_GetHeight(sector, off_pos);\n    }\n\n    return (XZ_32) { heights[1] - heights[0], heights[2] - heights[0] };\n}\n\nstatic int16_t M_GetTilt(const int32_t delta, const int32_t radius)\n{\n    int16_t rot = Math_Atan(2 * radius, delta);\n    if ((delta > 0 && rot > 0) || (delta < 0 && rot < 0)) {\n        rot = -rot;\n    }\n    return rot;\n}\n\nstatic void M_ApproachTilt(const int16_t target, int16_t *const current)\n{\n    if (ABS(target - *current) < M_CRAWL_TILT_RATE) {\n        *current = target;\n    } else if (target > *current) {\n        *current += M_CRAWL_TILT_RATE;\n    } else {\n        *current -= M_CRAWL_TILT_RATE;\n    }\n    CLAMP(*current, -M_CRAWL_TILT_MAX, M_CRAWL_TILT_MAX);\n}\n\nvoid Lara_Col_CrawlTilt(ITEM *const item)\n{\n    if (!g_Config.gameplay.enable_crawl_tilt) {\n        return;\n    }\n\n    const XYZ_32 pos = {\n        .x = item->pos.x,\n        .y = item->pos.y - M_CRAWL_TILT_HEIGHT,\n        .z = item->pos.z,\n    };\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n\n    XYZ_F plane = {};\n    if (Room_IsOnWalkable(sector, pos, height, NO_ITEM)) {\n        const XZ_32 tilt = M_GetWalkableTilt(item, pos.y);\n        plane.x = tilt.x * 2.0f / WALL_L;\n        plane.z = tilt.z * 2.0f / WALL_L;\n    } else {\n        const XZ_16 tilt = Room_GetTiltType(sector, pos);\n        plane.x = -tilt.x / 4.0f;\n        plane.z = -tilt.z / 4.0f;\n    }\n\n    plane.y = item->pos.y - plane.x * item->pos.x - plane.z * item->pos.z;\n\n    int32_t heights[4] = {};\n    for (int32_t i = 0; i < 4; i++) {\n        const XYZ_32 test_pos = XYZ_32_OffsetYaw(\n            pos, item->rot.y + DEG_90 * i, M_CRAWL_TILT_RADIUS);\n        room_num = item->room_num;\n        const SECTOR *const test_sector = Room_GetSector(test_pos, &room_num);\n        heights[i] = Room_GetHeight(test_sector, test_pos);\n\n        if (ABS(height - heights[i]) > M_CRAWL_TILT_RADIUS / 2) {\n            heights[i] = plane.x * test_pos.x + plane.z * test_pos.z + plane.y;\n        }\n    }\n\n    const XZ_32 delta = {\n        .x = heights[0] - heights[2],\n        .z = heights[3] - heights[1],\n    };\n    const XZ_16 rot = {\n        .x = M_GetTilt(delta.x, M_CRAWL_TILT_RADIUS),\n        .z = M_GetTilt(delta.z, M_CRAWL_TILT_RADIUS),\n    };\n    M_ApproachTilt(rot.x, &item->rot.x);\n    M_ApproachTilt(rot.z, &item->rot.z);\n}\n\n// clang-format off\nREGISTER_LARA_COL(LS_CROUCH_IDLE,       M_Crouch)\nREGISTER_LARA_COL(LS_CROUCH_ROLL,       M_CrouchRoll)\nREGISTER_LARA_COL(LS_CROUCH_TURN_LEFT,  M_CrouchTurn)\nREGISTER_LARA_COL(LS_CROUCH_TURN_RIGHT, M_CrouchTurn)\nREGISTER_LARA_COL(LS_CRAWL_IDLE,        M_CrawlIdle)\nREGISTER_LARA_COL(LS_CRAWL_FORWARD,     M_CrawlForward)\nREGISTER_LARA_COL(LS_CRAWL_TURN_LEFT,   M_CrawlTurn)\nREGISTER_LARA_COL(LS_CRAWL_TURN_RIGHT,  M_CrawlTurn)\nREGISTER_LARA_COL(LS_CRAWL_BACK,        M_CrawlBack)\nREGISTER_LARA_COL(LS_CRAWL_TO_CLIMB,    M_CrawlToClimb)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/col/jump.c",
    "content": "#include <trx/config.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n#include <trx/game/rooms/enum.h>\n#include <trx/game/rooms/utils.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_LF_START_HANG    12\n#define M_LF_FAST_FALL     1\n#define M_BAD_JUMP_CEILING ((STEP_L * 3) / 4) // = 192\n#define M_HEAD_CLEARANCE   (-STEP_L / 8) // = -32\n#define M_LADDER_CLEARANCE (-STEPUP_HEIGHT) // = -384\n// clang-format on\n\nEDGE_CATCH Lara_Col_TestEdgeCatch(\n    const ITEM *const item, const COLL_INFO *const coll, int32_t *const edge)\n{\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    int32_t hdif1 = coll->side_front.floor - bounds->min.y;\n    int32_t hdif2 = hdif1 + item->fall_speed;\n    if ((hdif1 < 0 && hdif2 < 0) || (hdif1 > 0 && hdif2 > 0)) {\n        hdif1 = item->pos.y + bounds->min.y;\n        hdif2 = hdif1 + item->fall_speed;\n        if ((hdif1 >> (WALL_SHIFT - 2)) == (hdif2 >> (WALL_SHIFT - 2))) {\n            return EDGE_CATCH_NONE;\n        }\n        if (item->fall_speed > 0) {\n            *edge = hdif2 & ~(STEP_L - 1);\n        } else {\n            *edge = hdif1 & ~(STEP_L - 1);\n        }\n        return EDGE_CATCH_NEG;\n    }\n\n    return ABS(coll->side_left2.floor - coll->side_right2.floor) < SLOPE_DIF\n        ? EDGE_CATCH_POS\n        : EDGE_CATCH_NONE;\n}\n\nSWING_CATCH Lara_Col_TestHangSwingIn(\n    const ITEM *const item, const int16_t angle)\n{\n    // Tests whether a forward hang grab should transition into thin-ledge\n    // swing (\"swinging inwards\"). The probe samples one click ahead in the\n    // hang direction and requires:\n    // - valid floor at probe point;\n    // - probe floor above Lara;\n    // - enough overhead clearance for swing-in.\n    // The extra clearance guard follows TR3-5 logic: `y - ceiling - 819 > -72`\n    // but uses a relaxed threshold to preserve the animation on certain slope\n    // edge cases.\n\n    XYZ_32 pos = item->pos;\n    int16_t room_num = item->room_num;\n    switch (angle) {\n    case 0:\n        pos.z += STEP_L;\n        break;\n    case DEG_90:\n        pos.x += STEP_L;\n        break;\n    case -DEG_180:\n        pos.z -= STEP_L;\n        break;\n    case -DEG_90:\n        pos.x -= STEP_L;\n        break;\n    }\n\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    int32_t height = Room_GetHeight(sector, pos);\n    int32_t ceiling = Room_GetCeiling(sector, pos);\n    const bool has_height = height != NO_HEIGHT;\n    const int32_t height_delta = height - pos.y;\n    const int32_t ceiling_delta = ceiling - pos.y;\n    if (!has_height || height_delta <= 0 || ceiling_delta >= -400) {\n        return SWING_CATCH_NONE;\n    }\n    const bool thin_ledge = pos.y - ceiling - 819 > -110;\n    return thin_ledge && g_Config.gameplay.enable_slow_ledge_swing\n        ? SWING_CATCH_SLOW\n        : SWING_CATCH_FAST;\n}\n\nstatic bool M_TestHangJump(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || coll->hit_static) {\n        return false;\n    }\n\n    if (coll->coll_type == COLL_TOP || coll->coll_type == COLL_TOP_FRONT) {\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(\n            (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n        if ((sector->ladder & LADDER_CEILING) != 0) {\n            Item_SwitchToAnim(item, LA(LA_SWING_IN_SLOW), 0);\n            item->current_anim_state = LS(LS_MONKEY_IDLE);\n            item->goal_anim_state = LS(LS_MONKEY_IDLE);\n            item->gravity = false;\n            item->speed = 0;\n            item->fall_speed = 0;\n            lara->gun_status = LGS_HANDS_BUSY;\n            Lara_Col_MonkeySwingSnap(item);\n            return true;\n        }\n    }\n\n    if (coll->coll_type != COLL_FRONT || coll->side_mid.ceiling > -STEPUP_HEIGHT\n        || coll->side_mid.floor < 200) {\n        return false;\n    }\n\n    int32_t edge;\n    const EDGE_CATCH edge_catch = Lara_Col_TestEdgeCatch(item, coll, &edge);\n    bool ladder_hang = false;\n    if (edge_catch == EDGE_CATCH_NEG) {\n        ladder_hang = Lara_Col_TestLadderHang(item, coll);\n    }\n    if (edge_catch == EDGE_CATCH_NONE\n        || (edge_catch == EDGE_CATCH_NEG && !ladder_hang)) {\n        return false;\n    }\n\n    const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE);\n    if (dir == DIR_UNKNOWN) {\n        return false;\n    }\n    const int16_t angle = Math_DirectionToAngle(dir);\n\n    const SWING_CATCH swing_catch = Lara_Col_TestHangSwingIn(item, angle);\n    if (swing_catch == SWING_CATCH_SLOW) {\n        Item_SwitchToAnim(item, LA(LA_SWING_IN_SLOW), 0);\n    } else if (swing_catch == SWING_CATCH_FAST) {\n        Item_SwitchToAnim(item, LA(LA_SWING_IN_FAST), 0);\n    } else {\n        Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), 0);\n    }\n    const ANIM *const anim = Item_GetAnim(item);\n    item->current_anim_state = anim->current_anim_state;\n    item->goal_anim_state = anim->current_anim_state;\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    if (edge_catch == EDGE_CATCH_POS) {\n        item->pos.y += coll->side_front.floor - bounds->min.y;\n        switch (dir) {\n        case DIR_NORTH:\n            item->pos.z = ROUND_TO_SECTOR_END(item->pos.z) - LARA_RADIUS;\n            item->pos.x += coll->shift.x;\n            break;\n\n        case DIR_EAST:\n            item->pos.x = ROUND_TO_SECTOR_END(item->pos.x) - LARA_RADIUS;\n            item->pos.z += coll->shift.z;\n            break;\n\n        case DIR_SOUTH:\n            item->pos.z = ROUND_TO_SECTOR(item->pos.z) + LARA_RADIUS;\n            item->pos.x += coll->shift.x;\n            break;\n\n        case DIR_WEST:\n            item->pos.x = ROUND_TO_SECTOR(item->pos.x) + LARA_RADIUS;\n            item->pos.z += coll->shift.z;\n            break;\n\n        default:\n            item->pos.x += coll->shift.x;\n            item->pos.z += coll->shift.z;\n            break;\n        }\n    } else {\n        item->pos.y = edge - bounds->min.y;\n    }\n\n    item->rot.y = angle;\n    item->speed = 2;\n    item->gravity = true;\n    item->fall_speed = 1;\n    lara->gun_status = LGS_HANDS_BUSY;\n    return true;\n}\n\nstatic bool M_TestHangJumpUp(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || coll->hit_static) {\n        return false;\n    }\n\n    if (coll->coll_type == COLL_TOP || coll->coll_type == COLL_TOP_FRONT) {\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(\n            (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n        if ((sector->ladder & LADDER_CEILING) != 0) {\n            Item_SwitchToAnim(item, LA(LA_MONKEY_GRAB), 0);\n            item->current_anim_state = LS(LS_MONKEY_IDLE);\n            item->goal_anim_state = LS(LS_MONKEY_IDLE);\n            item->gravity = false;\n            item->speed = 0;\n            item->fall_speed = 0;\n            lara->gun_status = LGS_HANDS_BUSY;\n\n            Lara_Col_MonkeySwingSnap(item);\n            return true;\n        }\n    }\n\n    if (coll->coll_type != COLL_FRONT\n        || coll->side_mid.ceiling\n            > (lara->climb_status ? M_LADDER_CLEARANCE : M_HEAD_CLEARANCE)) {\n        return false;\n    }\n\n    int32_t edge;\n    const EDGE_CATCH edge_catch = Lara_Col_TestEdgeCatch(item, coll, &edge);\n    if (edge_catch == EDGE_CATCH_NONE\n        || (edge_catch == EDGE_CATCH_NEG\n            && !Lara_Col_TestLadderHang(item, coll))) {\n        return false;\n    }\n\n    const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE);\n    if (dir == DIR_UNKNOWN) {\n        return false;\n    }\n    const int16_t angle = Math_DirectionToAngle(dir);\n\n    item->goal_anim_state = LS(LS_HANG);\n    item->current_anim_state = LS(LS_HANG);\n    Item_SwitchToAnim(item, LA(LA_REACH_TO_HANG), M_LF_START_HANG);\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    if (edge_catch == EDGE_CATCH_POS) {\n        item->pos.y += coll->side_front.floor - bounds->min.y;\n    } else {\n        item->pos.y = edge - bounds->min.y + (g_TRVersion >= 3 ? 4 : 0);\n    }\n    item->pos.x += coll->shift.x;\n    item->pos.z += coll->shift.z;\n    item->rot.y = angle;\n    item->speed = 0;\n    item->gravity = false;\n    item->fall_speed = 0;\n    lara->gun_status = LGS_HANDS_BUSY;\n    return true;\n}\n\nstatic void M_SlideEdgeJump(ITEM *const item, COLL_INFO *const coll)\n{\n    Lara_Col_Shift(coll);\n\n    switch (coll->coll_type) {\n    case COLL_LEFT:\n        item->rot.y += LARA_DEFLECT_ANGLE;\n        break;\n\n    case COLL_RIGHT:\n        item->rot.y -= LARA_DEFLECT_ANGLE;\n        break;\n\n    case COLL_TOP:\n    case COLL_TOP_FRONT:\n        CLAMPL(item->fall_speed, 1);\n        break;\n\n    case COLL_CLAMP:\n        item->pos.z -= (Math_Cos(coll->facing) * 100) >> W2V_SHIFT;\n        item->pos.x -= (Math_Sin(coll->facing) * 100) >> W2V_SHIFT;\n        item->speed = 0;\n        coll->side_mid.floor = 0;\n        if (item->fall_speed <= 0) {\n            item->fall_speed = 16;\n        }\n        break;\n    }\n}\n\nstatic void M_Compress(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = NO_BAD_NEG;\n    coll->bad_ceiling = 0;\n\n    Lara_Col_GetInfo(item, coll);\n\n    if (coll->side_mid.ceiling > -100) {\n        Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n        item->goal_anim_state = LS(LS_STOP);\n        item->current_anim_state = LS(LS_STOP);\n        item->gravity = false;\n        item->speed = 0;\n        item->fall_speed = 0;\n        item->pos = coll->old;\n    }\n\n    if (g_TRVersion >= 2 && coll->side_mid.floor > -STEP_L\n        && coll->side_mid.floor < STEP_L) {\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_NeutralJumpRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = NO_BAD_NEG;\n    coll->bad_ceiling = 0;\n\n    Lara_Col_GetInfo(item, coll);\n\n    if (coll->side_mid.ceiling > -100) {\n        Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n        item->goal_anim_state = LS(LS_STOP);\n        item->current_anim_state = LS(LS_STOP);\n        item->speed = 0;\n        item->pos = coll->old;\n    }\n}\n\nstatic void M_UpJump(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n    coll->facing = lara->move_angle;\n    if (g_Config.gameplay.enable_lean_jumping && item->speed < 0) {\n        coll->facing += DEG_180;\n    }\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num, 870);\n    if (M_TestHangJumpUp(item, coll)) {\n        return;\n    }\n\n    M_SlideEdgeJump(item, coll);\n    if (g_Config.gameplay.enable_lean_jumping) {\n        if (coll->coll_type != COLL_NONE) {\n            item->speed = item->speed > 0 ? 2 : -2;\n        } else if (item->fall_speed < -70) {\n            if (g_Input.forward && item->speed < 5) {\n                item->speed++;\n            } else if (g_Input.back && item->speed > -5) {\n                item->speed -= 2;\n            }\n        }\n    }\n\n    if (item->fall_speed <= 0 || coll->side_mid.floor > 0) {\n        return;\n    }\n\n    switch (Lara_Col_LandedBad(item)) {\n    case LANDED_OK:\n        item->goal_anim_state = LS(LS_STOP);\n        break;\n    case LANDED_BAD:\n        item->goal_anim_state = LS(LS_DEATH);\n        break;\n    case LANDED_HANDLED:\n        break;\n    }\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_ForwardJump(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->speed < 0\n        && g_Config.gameplay.wall_glitch_mode != WALL_GLITCH_TR1) {\n        lara->move_angle = item->rot.y + DEG_180;\n    } else {\n        lara->move_angle = item->rot.y;\n    }\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_DeflectEdgeJump(item, coll);\n    if (item->speed < 0\n        && g_Config.gameplay.wall_glitch_mode != WALL_GLITCH_TR1) {\n        lara->move_angle = item->rot.y;\n    }\n\n    if (coll->side_mid.floor > 0 || item->fall_speed <= 0) {\n        return;\n    }\n\n    switch (Lara_Col_LandedBad(item)) {\n    case LANDED_OK:\n        if (lara->water_status != LWS_WADE && g_Input.forward\n            && !g_Input.slow) {\n            item->goal_anim_state = LS(LS_RUN);\n        } else {\n            item->goal_anim_state = LS(LS_STOP);\n        }\n        break;\n    case LANDED_BAD:\n        item->goal_anim_state = LS(LS_DEATH);\n        break;\n    case LANDED_HANDLED:\n        break;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n    item->speed = 0;\n    if (g_Config.gameplay.wall_glitch_mode != WALL_GLITCH_FIXED\n        || coll->side_front.type != COLL_FRONT) {\n        Lara_Animate(item);\n    }\n}\n\nstatic void M_SideBackJump(ITEM *const item, COLL_INFO *const coll)\n{\n    int32_t angle = 0;\n    switch (LS_U(item->current_anim_state)) {\n    case LS_JUMP_BACK:\n        angle = DEG_180;\n        break;\n    case LS_JUMP_RIGHT:\n        angle = DEG_90;\n        break;\n    case LS_JUMP_LEFT:\n        angle = -DEG_90;\n        break;\n    default:\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y + angle;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_DeflectEdgeJump(item, coll);\n    if (item->fall_speed <= 0 || coll->side_mid.floor > 0) {\n        return;\n    }\n\n    switch (Lara_Col_LandedBad(item)) {\n    case LANDED_OK:\n        item->goal_anim_state = LS(LS_STOP);\n        break;\n    case LANDED_BAD:\n        item->goal_anim_state = LS(LS_DEATH);\n        break;\n    case LANDED_HANDLED:\n        break;\n    }\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_FallBack(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y + DEG_180;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_DeflectEdgeJump(item, coll);\n\n    if (coll->side_mid.floor > 0 || item->fall_speed <= 0) {\n        return;\n    }\n\n    switch (Lara_Col_LandedBad(item)) {\n    case LANDED_OK:\n        item->goal_anim_state = LS(LS_STOP);\n        break;\n    case LANDED_BAD:\n        item->goal_anim_state = LS(LS_DEATH);\n        break;\n    case LANDED_HANDLED:\n        break;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_Reach(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = true;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = 0;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    if (M_TestHangJump(item, coll)) {\n        return;\n    }\n\n    M_SlideEdgeJump(item, coll);\n    if (item->fall_speed <= 0 || coll->side_mid.floor > 0) {\n        return;\n    }\n\n    switch (Lara_Col_LandedBad(item)) {\n    case LANDED_OK:\n        item->goal_anim_state = LS(LS_STOP);\n        break;\n    case LANDED_BAD:\n        item->goal_anim_state = LS(LS_DEATH);\n        break;\n    case LANDED_HANDLED:\n        break;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_SwanDive(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_DeflectEdgeJump(item, coll);\n    if (coll->side_mid.floor > 0 || item->fall_speed <= 0) {\n        return;\n    }\n\n    item->goal_anim_state = LS(LS_STOP);\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_FastDive(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_DeflectEdgeJump(item, coll);\n\n    if (coll->side_mid.floor > 0 || item->fall_speed <= 0) {\n        return;\n    }\n\n    if (item->fall_speed > 133 && !g_Config.debug.enable_invulnerability) {\n        item->goal_anim_state = LS(LS_DEATH);\n    } else {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_FastFall(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = true;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = M_BAD_JUMP_CEILING;\n\n    Lara_Col_GetInfo(item, coll);\n    M_SlideEdgeJump(item, coll);\n    if (coll->side_mid.floor > 0) {\n        return;\n    }\n\n    switch (Lara_Col_LandedBad(item)) {\n    case LANDED_OK:\n        item->goal_anim_state = LS(LS_STOP);\n        item->current_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(item, LA(LA_FREEFALL_LAND), 0);\n        break;\n    case LANDED_BAD:\n        item->goal_anim_state = LS(LS_DEATH);\n        break;\n    case LANDED_HANDLED:\n        break;\n    }\n\n    Sound_StopEffect(SFX_LARA_FALL);\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y += coll->side_mid.floor;\n}\n\nvoid Lara_Col_DeflectEdgeJump(ITEM *const item, COLL_INFO *const coll)\n{\n    Lara_Col_Shift(coll);\n    switch (coll->coll_type) {\n    case COLL_FRONT:\n    case COLL_TOP_FRONT:\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        if (lara->climb_status && item->speed == 2) {\n            break;\n        }\n\n        if (g_Config.gameplay.wall_glitch_mode == WALL_GLITCH_TR1\n            || coll->side_mid.floor > (STEP_L * 2)) {\n            item->goal_anim_state = LS(LS_FAST_FALL);\n            item->current_anim_state = LS(LS_FAST_FALL);\n            Item_SwitchToAnim(item, LA(LA_SMASH_JUMP), M_LF_FAST_FALL);\n        } else if (coll->side_mid.floor <= (STEP_L / 2)) {\n            item->goal_anim_state = LS(LS_LAND);\n            item->current_anim_state = LS(LS_LAND);\n            Item_SwitchToAnim(item, LA(LA_JUMP_UP_LAND), 0);\n        }\n        item->speed /= 4;\n        lara->move_angle += DEG_180;\n        CLAMPL(item->fall_speed, 1);\n        break;\n\n    case COLL_LEFT:\n        item->rot.y += LARA_DEFLECT_ANGLE;\n        break;\n\n    case COLL_RIGHT:\n        item->rot.y -= LARA_DEFLECT_ANGLE;\n        break;\n\n    case COLL_TOP:\n        CLAMPL(item->fall_speed, 1);\n        break;\n\n    case COLL_CLAMP:\n        item->pos.z -= (Math_Cos(coll->facing) * 100) >> W2V_SHIFT;\n        item->pos.x -= (Math_Sin(coll->facing) * 100) >> W2V_SHIFT;\n        item->speed = 0;\n        coll->side_mid.floor = 0;\n        if (item->fall_speed <= 0) {\n            item->fall_speed = 16;\n        }\n        break;\n    }\n}\n\nLANDED_STATE Lara_Col_LandedBad(ITEM *const item)\n{\n    const XYZ_32 pos = item->pos;\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height =\n        Room_GetHeight(sector, (XYZ_32) { pos.x, pos.y - LARA_HEIGHT, pos.z });\n    item->pos.y = height;\n    item->floor = height;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool was_alive = item->hit_points > 0;\n    const bool was_extra_anim = lara->extra_anim;\n    Room_TestTriggers(item);\n    if (was_alive && item->hit_points <= 0 && !was_extra_anim\n        && lara->extra_anim) {\n        // Support rapids drown from any height\n        return LANDED_HANDLED;\n    }\n\n    item->pos.y = pos.y;\n    const int32_t land_speed = item->fall_speed - DAMAGE_START;\n    if (land_speed <= 0) {\n        return LANDED_OK;\n    }\n\n    if (g_Config.debug.enable_invulnerability) {\n        return false;\n    } else if (land_speed <= DAMAGE_LENGTH) {\n        Lara_TakeDamage(\n            LARA_MAX_HITPOINTS * SQUARE(land_speed) / SQUARE(DAMAGE_LENGTH),\n            false);\n    } else {\n        item->hit_points = -1;\n    }\n\n    // #675: Original bug to keep. Correct operator would be <=\n    return item->hit_points < 0 ? LANDED_BAD : LANDED_OK;\n}\n\n// clang-format off\nREGISTER_LARA_COL(LS_COMPRESS,     M_Compress)\nREGISTER_LARA_COL(LS_NEUTRAL_ROLL, M_NeutralJumpRoll)\nREGISTER_LARA_COL(LS_JUMP_UP,      M_UpJump)\nREGISTER_LARA_COL(LS_JUMP_FORWARD, M_ForwardJump)\nREGISTER_LARA_COL(LS_JUMP_BACK,    M_SideBackJump)\nREGISTER_LARA_COL(LS_JUMP_RIGHT,   M_SideBackJump)\nREGISTER_LARA_COL(LS_JUMP_LEFT,    M_SideBackJump)\nREGISTER_LARA_COL(LS_FALL_BACK,    M_FallBack)\nREGISTER_LARA_COL(LS_REACH,        M_Reach)\nREGISTER_LARA_COL(LS_SWAN_DIVE,    M_SwanDive)\nREGISTER_LARA_COL(LS_FAST_DIVE,    M_FastDive)\nREGISTER_LARA_COL(LS_FAST_FALL,    M_FastFall)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/col/land.c",
    "content": "#include <trx/config.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\n#define M_LF_WALK_STEP_L_START 0\n#define M_LF_WALK_STEP_L_NEAR_END 5\n#define M_LF_WALK_STEP_L_END 6\n#define M_LF_WALK_STEP_R_START 7\n#define M_LF_WALK_STEP_R_MID 22\n#define M_LF_WALK_STEP_R_NEAR_END 23\n#define M_LF_WALK_STEP_R_END 25\n#define M_LF_WALK_STEP_L_2_START 26\n#define M_LF_WALK_STEP_L_2_END 35\n#define M_LF_WALK_BACK_R_START 26\n#define M_LF_WALK_BACK_R_END 55\n\n#define M_LF_RUN_L_START 0\n#define M_LF_RUN_L_HEEL_GROUND 3\n#define M_LF_RUN_L_END 9\n#define M_LF_RUN_R_START 10\n#define M_LF_RUN_R_FOOT_GROUND 14\n#define M_LF_RUN_R_END 21\n\n#define M_LF_WADE_L_START 0\n#define M_LF_WADE_L_END 9\n#define M_LF_WADE_R_START 10\n#define M_LF_WADE_R_END 21\n#define M_LF_WADE_STEP_L_START 3\n#define M_LF_WADE_STEP_L_END 14\n\n#define M_LF_SPRINT_STEP_L_START 4\n#define M_LF_SPRINT_STEP_L_END 13\n\n#define M_CONTROLLED_DROP_MIN_HEIGHT (LARA_HEIGHT + (STEP_L * 3) / 4) // 954\n\nstatic int16_t m_OldSlideAngle = 1;\n\nstatic bool M_TestWall(\n    const ITEM *const item, const int32_t front, const int32_t right,\n    const int32_t down)\n{\n    XYZ_32 pos = item->pos;\n    pos.y += down;\n\n    const DIRECTION dir = Math_GetDirection(item->rot.y);\n    switch (dir) {\n    case DIR_NORTH:\n        pos.x -= right;\n        break;\n    case DIR_EAST:\n        pos.z -= right;\n        break;\n    case DIR_SOUTH:\n        pos.x += right;\n        break;\n    case DIR_WEST:\n        pos.z += right;\n        break;\n    default:\n        break;\n    }\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n\n    switch (dir) {\n    case DIR_NORTH:\n        pos.z += front;\n        break;\n    case DIR_EAST:\n        pos.x += front;\n        break;\n    case DIR_SOUTH:\n        pos.z -= front;\n        break;\n    case DIR_WEST:\n        pos.x -= front;\n        break;\n    default:\n        break;\n    }\n\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (height != NO_HEIGHT && height - pos.y > 0 && ceiling - pos.y < 0) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_CanControlDrop(\n    const ITEM *const item, const COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || !g_Config.gameplay.enable_controlled_drops\n        || coll->side_mid.floor < M_CONTROLLED_DROP_MIN_HEIGHT) {\n        return false;\n    }\n\n    COLL_INFO old_coll = {\n        .facing = lara->move_angle,\n        .bad_pos = STEPUP_HEIGHT,\n        .bad_neg = -STEPUP_HEIGHT,\n        .slopes_are_pits = 1,\n        .slopes_are_walls = 1,\n    };\n    Collide_GetCollisionInfo(\n        &old_coll, coll->old.x, coll->old.y, coll->old.z, item->room_num,\n        LARA_HEIGHT);\n\n    if (old_coll.side_mid.floor != 0) {\n        return false;\n    }\n\n    const DIRECTION dir =\n        Math_GetDirectionCone(item->rot.y + DEG_180, LARA_HANG_ANGLE);\n    if (dir == DIR_UNKNOWN) {\n        return false;\n    }\n\n    switch (old_coll.quadrant) {\n    case DIR_NORTH:\n    case DIR_SOUTH:\n        return ABS(old_coll.tilt.x) < MAX_SLOPE;\n    case DIR_EAST:\n    case DIR_WEST:\n        return ABS(old_coll.tilt.z) < MAX_SLOPE;\n    default:\n        return false;\n    }\n}\n\nbool Lara_Col_Fallen(ITEM *const item, const COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (coll->side_mid.floor <= STEPUP_HEIGHT\n        || lara->water_status == LWS_WADE) {\n        return false;\n    }\n    if (M_CanControlDrop(item, coll)) {\n        item->current_anim_state = LS(LS_REACH);\n        item->goal_anim_state = LS(LS_REACH);\n        Item_SwitchToAnim(item, LA(LA_CONTROLLED_DROP), 0);\n        item->speed = 2;\n    } else {\n        item->current_anim_state = LS(LS_JUMP_FORWARD);\n        item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n    }\n    item->gravity = true;\n    item->fall_speed = 0;\n    lara->sprinting = false;\n    lara->crouching = false;\n    return true;\n}\n\nbool Lara_Col_TestSlide(ITEM *const item, COLL_INFO *const coll)\n{\n    if (ABS(coll->tilt.x) <= MAX_SLOPE && ABS(coll->tilt.z) <= MAX_SLOPE) {\n        return false;\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (room->flags.swamp) {\n        return false;\n    }\n\n    int16_t angle = 0;\n    if (coll->tilt.x > MAX_SLOPE) {\n        angle = -DEG_90;\n    } else if (coll->tilt.x < -MAX_SLOPE) {\n        angle = DEG_90;\n    }\n\n    if (coll->tilt.z > 2 && coll->tilt.z > ABS(coll->tilt.x)) {\n        angle = -DEG_180;\n    } else if (coll->tilt.z < -2 && -coll->tilt.z > ABS(coll->tilt.x)) {\n        angle = 0;\n    }\n\n    const int16_t angle_dif = angle - item->rot.y;\n    Lara_Col_Shift(coll);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (angle_dif >= -DEG_90 && angle_dif <= DEG_90) {\n        if (item->current_anim_state == LS(LS_SLIDE)\n            && m_OldSlideAngle == angle) {\n            lara->sprinting = false;\n            lara->crouching = false;\n            return true;\n        }\n        item->goal_anim_state = LS(LS_SLIDE);\n        item->current_anim_state = LS(LS_SLIDE);\n        Item_SwitchToAnim(item, LA(LA_SLIDE_FORWARD), 0);\n        item->rot.y = angle;\n    } else {\n        if (item->current_anim_state == LS(LS_SLIDE_BACK)\n            && m_OldSlideAngle == angle) {\n            lara->sprinting = false;\n            lara->crouching = false;\n            return true;\n        }\n        item->goal_anim_state = LS(LS_SLIDE_BACK);\n        item->current_anim_state = LS(LS_SLIDE_BACK);\n        Item_SwitchToAnim(item, LA(LA_SLIDE_BACKWARD_START), 0);\n        item->rot.y = angle + DEG_180;\n    }\n\n    lara->move_angle = angle;\n    lara->sprinting = false;\n    lara->crouching = false;\n    m_OldSlideAngle = angle;\n    return true;\n}\n\nstatic bool M_DeflectEdge(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    switch (coll->coll_type) {\n    case COLL_FRONT:\n    case COLL_TOP_FRONT:\n        Lara_Col_Shift(coll);\n        item->goal_anim_state = LS(LS_STOP);\n        item->current_anim_state = LS(LS_STOP);\n        item->gravity = false;\n        item->speed = 0;\n        return true;\n\n    case COLL_LEFT:\n        Lara_Col_Shift(coll);\n        item->rot.y += LARA_DEFLECT_ANGLE;\n        return false;\n\n    case COLL_RIGHT:\n        Lara_Col_Shift(coll);\n        item->rot.y -= LARA_DEFLECT_ANGLE;\n        return false;\n\n    default:\n        return false;\n    }\n}\n\nbool Lara_Col_TestCeiling(ITEM *const item, const COLL_INFO *const coll)\n{\n    if (coll->coll_type != COLL_TOP && coll->coll_type != COLL_CLAMP) {\n        return false;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->sprinting = false;\n    lara->crouching = false;\n\n    item->pos = coll->old;\n    item->goal_anim_state = LS(LS_STOP);\n    item->current_anim_state = LS(LS_STOP);\n    Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n    item->speed = 0;\n    item->gravity = false;\n    item->fall_speed = 0;\n    return true;\n}\n\nstatic void M_CollideStop(ITEM *const item, const COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->sprinting = false;\n    lara->crouching = false;\n\n    if (g_Config.gameplay.enable_smooth_wall_deflect) {\n        switch (LS_U(coll->old_anim_state)) {\n        case LS_STOP:\n        case LS_TURN_RIGHT:\n        case LS_TURN_LEFT:\n        case LS_FAST_TURN:\n            item->current_anim_state = coll->old_anim_state;\n            item->anim_num = coll->old_anim_num;\n            item->frame_num = coll->old_frame_num;\n            if (g_Input.left) {\n                item->goal_anim_state = LS(LS_TURN_LEFT);\n            } else if (g_Input.right) {\n                item->goal_anim_state = LS(LS_TURN_RIGHT);\n            } else {\n                item->goal_anim_state = LS(LS_STOP);\n            }\n            Lara_Animate(item);\n            return;\n\n        default:\n            break;\n        }\n    }\n\n    Item_SwitchToAnim(item, LA(LA_STAND_STILL), 0);\n}\n\nstatic void M_Default(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = STEPUP_HEIGHT;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->slopes_are_pits = 1;\n    coll->slopes_are_walls = 1;\n    Lara_Col_GetInfo(item, coll);\n}\n\nstatic void M_Pickup(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    if (Item_TestAnimEqual(item, LA(LA_CRAWL_PICKUP))) {\n        Lara_Col_CrawlTilt(item);\n    }\n}\n\nstatic void M_PullUp(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    if (Item_TestAnimEqual(item, LA(LA_CLIMB_2CLICK))\n        && Item_TestFrameEqual(item, -1)) {\n        Lara_UpdateRoomToHeight(-WALL_L);\n        Lara_Animate(item);\n    }\n}\n\nstatic void M_Walk(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n    coll->lava_is_pit = 1;\n    M_Default(item, coll);\n\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) {\n        return;\n    }\n\n    if (M_DeflectEdge(item, coll)) {\n        if (Item_TestAnimEqual(item, LA(LA_WALK_FORWARD))\n            && Item_TestFrameRange(\n                item, M_LF_WALK_STEP_R_START, M_LF_WALK_STEP_R_END)) {\n            Item_SwitchToAnim(item, LA(LA_WALK_STOP_RIGHT), 0);\n        } else if (\n            Item_TestAnimEqual(item, LA(LA_WALK_FORWARD))\n            && (Item_TestFrameRange(\n                    item, M_LF_WALK_STEP_L_START, M_LF_WALK_STEP_L_END)\n                || Item_TestFrameRange(\n                    item, M_LF_WALK_STEP_L_2_START, M_LF_WALK_STEP_L_2_END))) {\n            Item_SwitchToAnim(item, LA(LA_WALK_STOP_LEFT), 0);\n        } else {\n            M_CollideStop(item, coll);\n        }\n    }\n\n    if (Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor > STEP_L / 2) {\n        if (Item_TestAnimEqual(item, LA(LA_WALK_FORWARD))\n            && Item_TestFrameRange(\n                item, M_LF_WALK_STEP_L_END, M_LF_WALK_STEP_R_NEAR_END)) {\n            Item_SwitchToAnim(item, LA(LA_WALK_DOWN_LEFT), 0);\n        } else {\n            Item_SwitchToAnim(item, LA(LA_WALK_DOWN_RIGHT), 0);\n        }\n    }\n\n    if (coll->side_mid.floor >= -STEPUP_HEIGHT\n        && coll->side_mid.floor < -STEP_L / 2) {\n        if (Item_TestAnimEqual(item, LA(LA_WALK_FORWARD))\n            && Item_TestFrameRange(\n                item, M_LF_WALK_STEP_L_NEAR_END, M_LF_WALK_STEP_R_MID)) {\n            Item_SwitchToAnim(item, LA(LA_WALK_UP_STEP_LEFT), 0);\n        } else {\n            Item_SwitchToAnim(item, LA(LA_WALK_UP_STEP_RIGHT), 0);\n        }\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_WalkBack(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y + DEG_180;\n    item->gravity = false;\n    item->fall_speed = 0;\n    if (lara->water_status == LWS_WADE) {\n        coll->bad_pos = NO_BAD_POS;\n    } else {\n        coll->bad_pos = STEPUP_HEIGHT;\n    }\n    coll->slopes_are_pits = 1;\n    coll->slopes_are_walls = 1;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->lava_is_pit = 1;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll)) {\n        return;\n    }\n\n    if (M_DeflectEdge(item, coll)) {\n        M_CollideStop(item, coll);\n    }\n\n    if (g_Config.gameplay.fix_descending_glitch\n        && Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor > STEP_L / 2\n        && coll->side_mid.floor < STEPUP_HEIGHT) {\n        if (Item_TestFrameRange(\n                item, M_LF_WALK_BACK_R_START, M_LF_WALK_BACK_R_END)) {\n            Item_SwitchToAnim(item, LA(LA_WALK_DOWN_BACK_RIGHT), 0);\n        } else {\n            Item_SwitchToAnim(item, LA(LA_WALK_DOWN_BACK_LEFT), 0);\n        }\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (coll->side_mid.floor >= 0 && room->flags.swamp) {\n        item->pos.y += 2;\n    } else if (lara->water_status == LWS_WADE && coll->side_mid.floor >= 50) {\n        item->pos.y += 50;\n    } else {\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_SideStep(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->current_anim_state == LS(LS_STEP_RIGHT)) {\n        lara->move_angle = item->rot.y + DEG_90;\n    } else {\n        lara->move_angle = item->rot.y - DEG_90;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    if (lara->water_status == LWS_WADE) {\n        coll->bad_pos = NO_BAD_POS;\n    } else {\n        coll->bad_pos = STEP_L / 2;\n    }\n    coll->slopes_are_pits = 1;\n    coll->slopes_are_walls = 1;\n    coll->bad_neg = -STEP_L / 2;\n    coll->bad_ceiling = 0;\n    coll->lava_is_pit = 1;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll)) {\n        return;\n    }\n\n    if (M_DeflectEdge(item, coll)) {\n        M_CollideStop(item, coll);\n    }\n\n    if (g_Config.gameplay.fix_descending_glitch\n        && Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (!Lara_Col_TestSlide(item, coll)) {\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_Run(ITEM *const item, COLL_INFO *const coll)\n{\n    if (g_Config.gameplay.fix_qwop_glitch) {\n        item->gravity = false;\n        item->fall_speed = 0;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->slopes_are_walls = 1;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    Lara_Col_GetInfo(item, coll);\n\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) {\n        return;\n    }\n\n    if (M_DeflectEdge(item, coll)) {\n        item->rot.z = 0;\n        if (M_TestWall(item, STEP_L, 0, -STEP_L * 5 / 2)) {\n            item->current_anim_state = LS(LS_SPLAT);\n            const bool is_run_anim = Item_TestAnimEqual(item, LA(LA_RUN));\n            if (is_run_anim\n                && Item_TestFrameRange(\n                    item, M_LF_RUN_L_START, M_LF_RUN_L_END)) {\n                Item_SwitchToAnim(item, LA(LA_WALL_SMASH_LEFT), 0);\n                return;\n            }\n            if (is_run_anim\n                && Item_TestFrameRange(\n                    item, M_LF_RUN_R_START, M_LF_RUN_R_END)) {\n                Item_SwitchToAnim(item, LA(LA_WALL_SMASH_RIGHT), 0);\n                return;\n            }\n        }\n        M_CollideStop(item, coll);\n    }\n\n    if (Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor >= -STEPUP_HEIGHT\n        && coll->side_mid.floor < -STEP_L / 2) {\n        if (g_Config.gameplay.fix_step_glitch\n            && (coll->side_front.floor < -STEPUP_HEIGHT\n                || coll->side_front.floor >= -STEP_L / 2)) {\n            coll->side_mid.floor = 0;\n        } else {\n            if (Item_TestFrameRange(\n                    item, M_LF_RUN_L_HEEL_GROUND, M_LF_RUN_R_FOOT_GROUND)) {\n                Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_LEFT), 0);\n            } else {\n                Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_RIGHT), 0);\n            }\n        }\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    item->pos.y += MIN(coll->side_mid.floor, 50);\n}\n\nstatic void M_Stop(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n    M_Default(item, coll);\n\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_Fallen(item, coll)\n        || Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (!room->flags.swamp && g_Config.gameplay.fix_step_glitch\n        && coll->side_mid.floor > 100) {\n        item->current_anim_state = LS(LS_JUMP_FORWARD);\n        item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n        item->gravity = true;\n        item->fall_speed = 0;\n        return;\n    }\n\n    Lara_Col_Shift(coll);\n    if (room->flags.swamp && coll->side_mid.floor >= 0) {\n        item->pos.y += 2;\n        CLAMPG(item->pos.y, item->floor);\n    } else {\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_FastBack(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y + DEG_180;\n    item->gravity = false;\n    item->fall_speed = 0;\n    coll->slopes_are_pits = 1;\n    coll->slopes_are_walls = !g_Config.gameplay.enable_back_slope_stumble;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor <= 200) {\n        if (!g_Config.gameplay.enable_back_slope_stumble\n            || !Lara_Col_TestSlide(item, coll)) {\n            if (M_DeflectEdge(item, coll)) {\n                M_CollideStop(item, coll);\n            }\n            item->pos.y += coll->side_mid.floor;\n        }\n    } else {\n        Item_SwitchToAnim(item, LA(LA_FALL_BACK), 0);\n        item->current_anim_state = LS(LS_FALL_BACK);\n        item->goal_anim_state = LS(LS_FALL_BACK);\n        item->gravity = true;\n        item->fall_speed = 0;\n    }\n}\n\nstatic void M_Turn(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n    M_Default(item, coll);\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (coll->side_mid.floor > 100 && !room->flags.swamp) {\n        Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n        item->current_anim_state = LS(LS_JUMP_FORWARD);\n        item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        item->gravity = true;\n        item->fall_speed = 0;\n        return;\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor < 0 || !room->flags.swamp) {\n        item->pos.y += coll->side_mid.floor;\n    } else {\n        item->pos.y += 2;\n    }\n}\n\nstatic void M_Death(ITEM *const item, COLL_INFO *const coll)\n{\n    if (g_TRVersion >= 2) {\n        Sound_StopEffect(SFX_LARA_FALL);\n    }\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = STEPUP_HEIGHT;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->radius = LARA_RADIUS * 4;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_Shift(coll);\n\n    item->pos.y += coll->side_mid.floor;\n    item->hit_points = -1;\n    lara->air = -1;\n}\n\nstatic void M_Splat(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    Lara_Col_Shift(coll);\n    if (!g_Config.gameplay.fix_step_glitch && coll->side_mid.floor > -STEP_L\n        && coll->side_mid.floor < STEP_L) {\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_Slide(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    if (item->current_anim_state == LS(LS_SLIDE_BACK)) {\n        lara->move_angle += DEG_180;\n    }\n\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEP_L * 2;\n    coll->bad_ceiling = 0;\n    Lara_Col_GetInfo(item, coll);\n\n    if (Lara_Col_TestCeiling(item, coll)) {\n        return;\n    }\n\n    M_DeflectEdge(item, coll);\n\n    if (coll->side_mid.floor > 200) {\n        if (item->current_anim_state == LS(LS_SLIDE)) {\n            if (M_CanControlDrop(item, coll)) {\n                item->current_anim_state = LS(LS_REACH);\n                item->goal_anim_state = LS(LS_REACH);\n                Item_SwitchToAnim(item, LA(LA_CONTROLLED_DROP), 2);\n                item->speed = 2;\n            } else {\n                item->goal_anim_state = LS(LS_JUMP_FORWARD);\n                item->current_anim_state = LS(LS_JUMP_FORWARD);\n                Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n            }\n        } else {\n            item->goal_anim_state = LS(LS_FALL_BACK);\n            item->current_anim_state = LS(LS_FALL_BACK);\n            Item_SwitchToAnim(item, LA(LA_FALL_BACK), 0);\n        }\n        item->gravity = true;\n        item->fall_speed = 0;\n        return;\n    }\n\n    Lara_Col_TestSlide(item, coll);\n    item->pos.y += coll->side_mid.floor;\n    if (ABS(coll->tilt.x) <= MAX_SLOPE && ABS(coll->tilt.z) <= MAX_SLOPE) {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic void M_Roll(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    item->gravity = false;\n    item->fall_speed = 0;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->slopes_are_walls = 1;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (g_Config.gameplay.enable_step_roll_boost) {\n        if (coll->side_mid.floor > 200) {\n            item->current_anim_state = LS(LS_JUMP_FORWARD);\n            item->goal_anim_state = LS(LS_JUMP_FORWARD);\n            Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n            item->gravity = true;\n            item->fall_speed = 0;\n            return;\n        }\n    } else if (Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    Lara_Col_Shift(coll);\n    item->pos.y += coll->side_mid.floor;\n}\n\nstatic void M_RollContinue(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    item->gravity = false;\n    item->fall_speed = 0;\n    lara->move_angle = item->rot.y + DEG_180;\n    coll->slopes_are_walls = 1;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor > 200) {\n        Item_SwitchToAnim(item, LA(LA_FALL_BACK), 0);\n        item->current_anim_state = LS(LS_FALL_BACK);\n        item->goal_anim_state = LS(LS_FALL_BACK);\n        item->gravity = true;\n        item->fall_speed = 0;\n    } else {\n        Lara_Col_Shift(coll);\n        item->pos.y += coll->side_mid.floor;\n    }\n}\n\nstatic void M_Wade(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->slopes_are_walls = 1;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) {\n        return;\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (M_DeflectEdge(item, coll)) {\n        item->rot.z = 0;\n        if (g_Config.gameplay.fix_wade_wall_hit\n            && (coll->side_front.type == HT_WALL\n                || coll->side_front.type == HT_SPLIT_TRI)\n            && coll->side_front.floor < -STEP_L * 5 / 2\n            && coll->old_anim_state == LS(LS_WADE)\n            && Item_TestAnimEqual(item, LA(LA_WADE)) && !room->flags.swamp) {\n            item->current_anim_state = LS(LS_SPLAT);\n            if (Item_TestFrameRange(item, M_LF_WADE_L_START, M_LF_WADE_L_END)) {\n                Item_SwitchToAnim(item, LA(LA_WALL_SMASH_LEFT), 0);\n                return;\n            }\n            if (Item_TestFrameRange(item, M_LF_WADE_R_START, M_LF_WADE_R_END)) {\n                Item_SwitchToAnim(item, LA(LA_WALL_SMASH_RIGHT), 0);\n                return;\n            }\n        }\n        M_CollideStop(item, coll);\n    }\n\n    if (!room->flags.swamp && Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor >= -STEPUP_HEIGHT\n        && coll->side_mid.floor < -STEP_L / 2 && !room->flags.swamp) {\n        if (Item_TestFrameRange(\n                item, M_LF_WADE_STEP_L_START, M_LF_WADE_STEP_L_END)) {\n            Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_LEFT), 0);\n        } else {\n            Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_RIGHT), 0);\n        }\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (coll->side_mid.floor >= 50 && !room->flags.swamp) {\n        item->pos.y += 50;\n    } else if (coll->side_mid.floor < 0 || !room->flags.swamp) {\n        item->pos.y += coll->side_mid.floor;\n    } else {\n        item->pos.y += 2;\n    }\n}\n\nstatic void M_Sprint(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->slopes_are_walls = 1;\n\n    Lara_Col_GetInfo(item, coll);\n    if (Lara_Col_TestCeiling(item, coll) || Lara_Col_TestVault(item, coll)) {\n        return;\n    }\n\n    if (M_DeflectEdge(item, coll)) {\n        item->rot.z = 0;\n        if (M_TestWall(item, STEP_L, 0, -STEP_L * 5 / 2)) {\n            Item_SwitchToAnim(item, LA(LA_WALL_SMASH_LEFT), 0);\n            lara->sprinting = false;\n            return;\n        }\n\n        M_CollideStop(item, coll);\n    }\n\n    if (Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (!g_Config.gameplay.enable_responsive_sprint\n        && coll->side_mid.floor >= -STEPUP_HEIGHT\n        && coll->side_mid.floor < -STEP_L / 2) {\n        if (Item_TestFrameRange(\n                item, M_LF_SPRINT_STEP_L_START, M_LF_SPRINT_STEP_L_END)) {\n            Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_LEFT), 0);\n        } else {\n            Item_SwitchToAnim(item, LA(LA_RUN_UP_STEP_RIGHT), 0);\n        }\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    item->pos.y += MIN(coll->side_mid.floor, 50);\n}\n\nstatic void M_SprintRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    if (item->speed < 0) {\n        lara->move_angle += DEG_180;\n    }\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEP_L;\n    coll->bad_ceiling = STEPUP_HEIGHT / 2;\n    coll->slopes_are_walls = 1;\n\n    Lara_Col_GetInfo(item, coll);\n    Lara_Col_DeflectEdgeJump(item, coll);\n    if (Lara_Col_Fallen(item, coll)) {\n        return;\n    }\n\n    if (item->speed < 0) {\n        lara->move_angle = item->rot.y;\n    }\n\n    if (coll->side_mid.floor <= 0 && item->fall_speed > 0) {\n        if (Lara_Col_LandedBad(item)) {\n            item->goal_anim_state = LS(LS_DEATH);\n        } else if (\n            lara->water_status == LWS_WADE || !g_Input.forward\n            || g_Input.slow) {\n            item->goal_anim_state = LS(LS_STOP);\n        } else {\n            item->goal_anim_state = LS(LS_RUN);\n        }\n\n        item->fall_speed = 0;\n        item->gravity = false;\n        item->speed = 0;\n        item->pos.y += coll->side_mid.floor;\n        Lara_Animate(item);\n    }\n\n    Lara_Col_Shift(coll);\n    item->pos.y += coll->side_mid.floor;\n}\n\n// clang-format off\nREGISTER_LARA_COL(LS_PUSH_BLOCK,   M_Default)\nREGISTER_LARA_COL(LS_PULL_BLOCK,   M_Default)\nREGISTER_LARA_COL(LS_PP_READY,     M_Default)\nREGISTER_LARA_COL(LS_PICKUP,       M_Pickup)\nREGISTER_LARA_COL(LS_SWITCH_ON,    M_Default)\nREGISTER_LARA_COL(LS_SWITCH_OFF,   M_Default)\nREGISTER_LARA_COL(LS_USE_KEY,      M_Default)\nREGISTER_LARA_COL(LS_USE_PUZZLE,   M_Default)\nREGISTER_LARA_COL(LS_USE_MIDAS,    M_Default)\nREGISTER_LARA_COL(LS_DIE_MIDAS,    M_Default)\nREGISTER_LARA_COL(LS_GYMNAST,      M_Default)\nREGISTER_LARA_COL(LS_WATER_OUT,    M_Default)\nREGISTER_LARA_COL(LS_PULL_UP,      M_PullUp)\nREGISTER_LARA_COL(LS_CONTROLLED,   M_Default)\nREGISTER_LARA_COL(LS_FLARE_PICKUP, M_Default)\nREGISTER_LARA_COL(LS_WALK,         M_Walk)\nREGISTER_LARA_COL(LS_WALK_BACK,    M_WalkBack)\nREGISTER_LARA_COL(LS_STEP_RIGHT,   M_SideStep)\nREGISTER_LARA_COL(LS_STEP_LEFT,    M_SideStep)\nREGISTER_LARA_COL(LS_RUN,          M_Run)\nREGISTER_LARA_COL(LS_STOP,         M_Stop)\nREGISTER_LARA_COL(LS_POSE,         M_Stop)\nREGISTER_LARA_COL(LS_POSE_START,   M_Stop)\nREGISTER_LARA_COL(LS_POSE_END,     M_Stop)\nREGISTER_LARA_COL(LS_LAND,         M_Stop)\nREGISTER_LARA_COL(LS_FAST_TURN,    M_Stop)\nREGISTER_LARA_COL(LS_FAST_BACK,    M_FastBack)\nREGISTER_LARA_COL(LS_TURN_RIGHT,   M_Turn)\nREGISTER_LARA_COL(LS_TURN_LEFT,    M_Turn)\nREGISTER_LARA_COL(LS_DEATH,        M_Death)\nREGISTER_LARA_COL(LS_SPLAT,        M_Splat)\nREGISTER_LARA_COL(LS_SLIDE,        M_Slide)\nREGISTER_LARA_COL(LS_SLIDE_BACK,   M_Slide)\nREGISTER_LARA_COL(LS_ROLL,         M_Roll)\nREGISTER_LARA_COL(LS_ROLL_CONT,    M_RollContinue)\nREGISTER_LARA_COL(LS_WADE,         M_Wade)\nREGISTER_LARA_COL(LS_SPRINT,       M_Sprint)\nREGISTER_LARA_COL(LS_SPRINT_ROLL,  M_SprintRoll)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/col/monkey.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n\n// clang-format off\n#define M_MONKEY_RADIUS           100\n#define M_MONKEY_HEIGHT           600\n#define M_MONKEY_CEILING_SHIFT    50\n#define M_MONKEY_FALL_FRAME       9\n#define M_CAM_MONKEY_ELEVATION   (10 * DEG_1) // = 1820\n#define M_LF_CLIMB_1_START       54\n#define M_LF_CLIMB_1_END         60\n#define M_LF_CLIMB_2_START       78\n#define M_LF_CLIMB_2_END         84\n#define M_LF_CLIMB_3_START       102\n#define M_LF_CLIMB_3_END         155\n// clang-format on\n\nstatic bool M_CanMonkeySwing(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(\n        (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n    return (sector->ladder & LADDER_CEILING) != 0;\n}\n\nstatic bool M_CanClimb(const ITEM *const item, const COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!lara->climb_status || !g_Input.forward\n        || coll->side_mid.ceiling > -STEP_L) {\n        return false;\n    }\n\n    if (Item_TestAnimEqual(item, LA(LA_MONKEY_IDLE))) {\n        return true;\n    }\n\n    if (!Item_TestAnimEqual(item, LA(LA_SWING_IN_SLOW))) {\n        return false;\n    }\n\n    return Item_TestFrameRange(item, M_LF_CLIMB_1_START, M_LF_CLIMB_1_END)\n        || Item_TestFrameRange(item, M_LF_CLIMB_2_START, M_LF_CLIMB_2_END)\n        || Item_TestFrameRange(item, M_LF_CLIMB_3_START, M_LF_CLIMB_3_END);\n}\n\nstatic void M_MonkeySwingFall(ITEM *const item)\n{\n    item->goal_anim_state = LS(LS_JUMP_UP);\n    item->current_anim_state = LS(LS_JUMP_UP);\n    Item_SwitchToAnim(item, LA(LA_JUMP_UP), M_MONKEY_FALL_FRAME);\n\n    item->gravity = true;\n    item->speed = 2;\n    item->fall_speed = 1;\n    item->pos.y += STEP_L;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->gun_status = LGS_ARMLESS;\n}\n\nstatic void M_GetMonkeyCollisionInfo(\n    ITEM *const item, COLL_INFO *const coll, const int16_t move_angle,\n    const int32_t bad_neg, const bool slopes_are_walls)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = move_angle;\n\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = bad_neg;\n    coll->bad_ceiling = 0;\n    coll->facing = lara->move_angle;\n    coll->radius = M_MONKEY_RADIUS;\n    coll->slopes_are_walls = slopes_are_walls;\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        M_MONKEY_HEIGHT);\n}\n\nstatic bool M_IsDirOctant(const int16_t rot)\n{\n    const int16_t abs_rot = ABS(rot);\n    return abs_rot >= DEG_45 && abs_rot <= DEG_135;\n}\n\nstatic bool M_TestMonkeySide(\n    ITEM *const item, COLL_INFO *const coll, const int16_t angle_delta,\n    const bool is_right)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const int16_t old_move_angle = lara->move_angle;\n    lara->move_angle = item->rot.y + angle_delta;\n\n    M_GetMonkeyCollisionInfo(\n        item, coll, lara->move_angle, is_right ? -STEPUP_HEIGHT : NO_BAD_NEG,\n        false);\n\n    bool ok = true;\n    if (ABS(coll->side_mid.ceiling - coll->side_front.ceiling)\n        > M_MONKEY_CEILING_SHIFT) {\n        ok = false;\n        goto cleanup;\n    }\n\n    if (coll->coll_type != COLL_NONE) {\n        const bool oct = M_IsDirOctant(item->rot.y);\n        if (!oct && coll->coll_type == COLL_FRONT) {\n            ok = false;\n            goto cleanup;\n        }\n\n        if (!is_right) {\n            if ((!oct && coll->coll_type == COLL_LEFT)\n                || (oct\n                    && (coll->coll_type == COLL_RIGHT\n                        || coll->coll_type == COLL_LEFT))) {\n                ok = false;\n                goto cleanup;\n            }\n        } else {\n            if (oct\n                && (coll->coll_type == COLL_FRONT\n                    || coll->coll_type == COLL_RIGHT\n                    || coll->coll_type == COLL_LEFT)) {\n                ok = false;\n                goto cleanup;\n            }\n        }\n    }\n\ncleanup:\n    lara->move_angle = old_move_angle;\n    return ok;\n}\n\nstatic bool M_HandleIdleState(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!M_CanMonkeySwing(item)) {\n        return false;\n    }\n\n    if (!g_Input.action || item->hit_points <= 0) {\n        M_MonkeySwingFall(item);\n        return true;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->speed = 0;\n\n    M_GetMonkeyCollisionInfo(item, coll, item->rot.y, NO_BAD_NEG, false);\n    if (coll->side_mid.ceiling < -STEP_L) {\n        // Lara is in a slow-swing state far below the ceiling.\n        return false;\n    }\n\n    if (g_Input.forward && coll->coll_type != COLL_FRONT\n        && ABS(coll->side_mid.ceiling - coll->side_front.ceiling)\n            < M_MONKEY_CEILING_SHIFT) {\n        item->goal_anim_state = LS(LS_MONKEY_FORWARD);\n    } else if (\n        g_Input.step_left && M_TestMonkeySide(item, coll, -DEG_90, false)) {\n        item->goal_anim_state = LS(LS_MONKEY_LEFT);\n    } else if (\n        g_Input.step_right && M_TestMonkeySide(item, coll, DEG_90, true)) {\n        item->goal_anim_state = LS(LS_MONKEY_RIGHT);\n    } else if (g_Input.left) {\n        item->goal_anim_state = LS(LS_MONKEY_TURN_LEFT);\n    } else if (g_Input.right) {\n        item->goal_anim_state = LS(LS_MONKEY_TURN_RIGHT);\n    }\n\n    Lara_Col_MonkeySwingSnap(item);\n    return true;\n}\n\nstatic void M_MonkeyIdle(ITEM *const item, COLL_INFO *const coll)\n{\n    if (M_HandleIdleState(item, coll)) {\n        return;\n    }\n\n    // Monkey idle state can be the result of swinging on a thin ledge as well\n    // as actually being on monkeybars. LA_SWING_IN_SLOW links to this state.\n    Lara_Col_HangTest(item, coll);\n    if (item->goal_anim_state != LS(LS_MONKEY_IDLE)) {\n        return;\n    }\n\n    if (g_Input.forward && coll->side_front.floor > -850\n        && coll->side_front.floor < -650\n        && coll->side_front.floor - coll->side_front.ceiling >= 0\n        && coll->side_left2.floor - coll->side_left2.ceiling >= 0\n        && coll->side_right2.floor - coll->side_right2.ceiling >= 0\n        && !coll->hit_static) {\n        item->goal_anim_state = LS(g_Input.slow ? LS_GYMNAST : LS_PULL_UP);\n        return;\n    }\n\n    if (M_CanClimb(item, coll)) {\n        item->goal_anim_state = LS(LS_HANG);\n        item->current_anim_state = LS(LS_HANG);\n        Item_SwitchToAnim(item, LA(LA_LADDER_UP_HANGING), 0);\n        return;\n    }\n\n    if ((g_Input.forward || g_Input.crouch) && coll->side_front.floor > -850\n        && coll->side_front.floor < -650\n        && coll->side_front.floor - coll->side_front.ceiling >= -256\n        && coll->side_left2.floor - coll->side_left2.ceiling >= -256\n        && coll->side_right2.floor - coll->side_right2.ceiling >= -256\n        && !coll->hit_static) {\n        item->goal_anim_state = LS(LS_CLIMB_TO_CRAWL);\n        item->required_anim_state = LS(LS_CROUCH_IDLE);\n    } else if (g_Input.left || g_Input.step_left) {\n        item->goal_anim_state = LS(LS_SHIMMY_LEFT);\n    } else if (g_Input.right || g_Input.step_right) {\n        item->goal_anim_state = LS(LS_SHIMMY_RIGHT);\n    }\n}\n\nstatic void M_MonkeyForward(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!g_Input.action || !M_CanMonkeySwing(item)) {\n        M_MonkeySwingFall(item);\n        return;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n\n    M_GetMonkeyCollisionInfo(item, coll, item->rot.y, NO_BAD_NEG, false);\n\n    if (coll->coll_type == COLL_FRONT\n        || ABS(coll->side_mid.ceiling - coll->side_front.ceiling)\n            > M_MONKEY_CEILING_SHIFT) {\n        Item_SwitchToAnim(item, LA(LA_MONKEY_IDLE), 0);\n        item->current_anim_state = LS(LS_MONKEY_IDLE);\n        item->goal_anim_state = LS(LS_MONKEY_IDLE);\n        Lara_Col_MonkeySwingSnap(item);\n        return;\n    }\n\n    g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION;\n    Lara_Col_MonkeySwingSnap(item);\n}\n\nstatic void M_MonkeySide(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!g_Input.action || !M_CanMonkeySwing(item)) {\n        M_MonkeySwingFall(item);\n        return;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->speed = 0;\n\n    const bool is_right = item->current_anim_state == LS(LS_MONKEY_RIGHT);\n    const int16_t angle_delta = is_right ? DEG_90 : -DEG_90;\n\n    if (M_TestMonkeySide(item, coll, angle_delta, is_right)) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->move_angle = item->rot.y + angle_delta;\n        g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION;\n        Lara_Col_MonkeySwingSnap(item);\n    } else {\n        Item_SwitchToAnim(item, LA(LA_MONKEY_IDLE), 0);\n        item->current_anim_state = LS(LS_MONKEY_IDLE);\n        item->goal_anim_state = LS(LS_MONKEY_IDLE);\n        Lara_Col_MonkeySwingSnap(item);\n    }\n}\n\nstatic void M_MonkeyTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!g_Input.action || !M_CanMonkeySwing(item)) {\n        M_MonkeySwingFall(item);\n        return;\n    }\n\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->speed = 0;\n\n    M_GetMonkeyCollisionInfo(item, coll, item->rot.y, -STEPUP_HEIGHT, true);\n\n    Lara_Col_MonkeySwingSnap(item);\n}\n\nstatic void M_MonkeyRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    M_MonkeyForward(item, coll);\n}\n\n// clang-format off\nREGISTER_LARA_COL(LS_MONKEY_IDLE,       M_MonkeyIdle)\nREGISTER_LARA_COL(LS_MONKEY_FORWARD,    M_MonkeyForward)\nREGISTER_LARA_COL(LS_MONKEY_LEFT,       M_MonkeySide)\nREGISTER_LARA_COL(LS_MONKEY_RIGHT,      M_MonkeySide)\nREGISTER_LARA_COL(LS_MONKEY_TURN_LEFT,  M_MonkeyTurn)\nREGISTER_LARA_COL(LS_MONKEY_TURN_RIGHT, M_MonkeyTurn)\nREGISTER_LARA_COL(LS_MONKEY_ROLL,       M_MonkeyRoll)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/col/swim.c",
    "content": "#include <trx/config.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n\n#define M_HEIGHT_SURF 700\n\nstatic bool M_TestWaterStepOut(ITEM *const item, const COLL_INFO *const coll)\n{\n    if (coll->coll_type == COLL_FRONT || coll->side_mid.type == HT_BIG_SLOPE\n        || coll->side_mid.type == HT_DIAGONAL || coll->side_mid.floor >= 0) {\n        return false;\n    }\n\n    if (coll->side_mid.floor < -STEP_L / 2) {\n        item->current_anim_state = LS(LS_WATER_OUT);\n        item->goal_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(item, LA(LA_ONWATER_TO_WADE), 0);\n    } else if (item->goal_anim_state == LS(LS_SURF_LEFT)) {\n        item->goal_anim_state = LS(LS_STEP_LEFT);\n    } else if (item->goal_anim_state == LS(LS_SURF_RIGHT)) {\n        item->goal_anim_state = LS(LS_STEP_RIGHT);\n    } else {\n        item->current_anim_state = LS(LS_WADE);\n        item->goal_anim_state = LS(LS_WADE);\n        Item_SwitchToAnim(item, LA(LA_WADE), 0);\n    }\n\n    item->pos.y += coll->side_front.floor + M_HEIGHT_SURF - 5;\n    Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2);\n    item->gravity = false;\n    item->rot.x = 0;\n    item->rot.z = 0;\n    item->speed = 0;\n    item->fall_speed = 0;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->water_status = LWS_WADE;\n    return true;\n}\n\nstatic bool M_TestWaterClimbOut(ITEM *const item, const COLL_INFO *const coll)\n{\n    const int32_t coll_hdif =\n        ABS(coll->side_left2.floor - coll->side_right2.floor);\n    if (coll->coll_type != COLL_FRONT || !g_Input.action\n        || coll_hdif >= SLOPE_DIF) {\n        return false;\n    }\n\n    if (coll->side_front.ceiling > 0\n        || coll->side_mid.ceiling > -STEPUP_HEIGHT) {\n        return false;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Config.gameplay.fix_water_exit) {\n        if (coll->side_front.type == HT_BIG_SLOPE) {\n            return false;\n        }\n    } else if (item->rot.y != lara->move_angle) {\n        return false;\n    }\n\n    if (lara->gun_status != LGS_ARMLESS\n        && (lara->gun_status != LGS_READY || lara->gun_type != LGT_FLARE)) {\n        return false;\n    }\n\n    const int32_t lara_hdif = coll->side_front.floor + M_HEIGHT_SURF;\n    if (lara_hdif <= -STEP_L * 2 || lara_hdif > M_HEIGHT_SURF - STEPUP_HEIGHT) {\n        return false;\n    }\n\n    const DIRECTION dir = Math_GetDirectionCone(item->rot.y, LARA_HANG_ANGLE);\n    if (dir == DIR_UNKNOWN) {\n        return false;\n    }\n\n    item->pos.y += lara_hdif - 5;\n    Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2);\n\n    switch (dir) {\n    case DIR_NORTH:\n        item->pos.z = ROUND_TO_SECTOR(item->pos.z) + WALL_L + LARA_RADIUS;\n        break;\n    case DIR_EAST:\n        item->pos.x = ROUND_TO_SECTOR(item->pos.x) + WALL_L + LARA_RADIUS;\n        break;\n    case DIR_SOUTH:\n        item->pos.z = ROUND_TO_SECTOR(item->pos.z) - LARA_RADIUS;\n        break;\n    case DIR_WEST:\n        item->pos.x = ROUND_TO_SECTOR(item->pos.x) - LARA_RADIUS;\n        break;\n    case DIR_UNKNOWN:\n        return false;\n    }\n\n    if (lara_hdif < -STEP_L / 2) {\n        Item_SwitchToAnim(item, LA(LA_ONWATER_TO_STAND_HIGH), 0);\n    } else if (lara_hdif < STEP_L / 2) {\n        Item_SwitchToAnim(item, LA(LA_ONWATER_TO_STAND_MEDIUM), 0);\n    } else {\n        Item_SwitchToAnim(item, LA(LA_ONWATER_TO_WADE_LOW), 0);\n    }\n\n    item->current_anim_state = LS(LS_WATER_OUT);\n    item->goal_anim_state = LS(LS_STOP);\n    item->rot.y = Math_DirectionToAngle(dir);\n    item->rot.x = 0;\n    item->rot.z = 0;\n    item->gravity = false;\n    item->speed = 0;\n    item->fall_speed = 0;\n    lara->gun_status = LGS_HANDS_BUSY;\n    lara->water_status = LWS_ABOVE_WATER;\n    return true;\n}\n\nstatic void M_TestWaterDepth(ITEM *const item, const COLL_INFO *const coll)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t water_depth =\n        Lara_GetWaterDepth(item->pos.x, item->pos.y, item->pos.z, room_num);\n\n    if (g_Config.gameplay.fix_water_exit && water_depth == NO_HEIGHT) {\n        item->pos = coll->old;\n        item->fall_speed = 0;\n        return;\n    }\n\n    if (water_depth == NO_HEIGHT || water_depth > STEP_L * 2) {\n        return;\n    }\n\n    Item_SwitchToAnim(item, LA(LA_UNDERWATER_TO_STAND), 0);\n    item->current_anim_state = LS(LS_WATER_OUT);\n    item->goal_anim_state = LS(LS_STOP);\n    item->rot.x = 0;\n    item->rot.z = 0;\n    item->gravity = false;\n    item->speed = 0;\n    item->fall_speed = 0;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->water_status = LWS_WADE;\n    item->pos.y = Room_GetHeight(sector, item->pos);\n}\n\nstatic void M_CommonSurface(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    coll->facing = lara->move_angle;\n\n    int32_t obj_height = M_HEIGHT_SURF;\n    if (g_Config.gameplay.enable_wading) {\n        obj_height += 100;\n    }\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y + M_HEIGHT_SURF, item->pos.z,\n        item->room_num, obj_height);\n\n    Lara_Col_Shift(coll);\n\n    if (coll->coll_type == COLL_LEFT) {\n        item->rot.y += 5 * DEG_1;\n    } else if (coll->coll_type == COLL_RIGHT) {\n        item->rot.y -= 5 * DEG_1;\n    } else if (\n        coll->coll_type != COLL_NONE\n        || (coll->side_mid.floor < 0\n            && (coll->side_mid.type == HT_BIG_SLOPE\n                || coll->side_mid.type == HT_DIAGONAL))) {\n        item->fall_speed = 0;\n        item->pos = coll->old;\n    }\n\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    if (water_height - item->pos.y <= -100) {\n        item->current_anim_state = LS(LS_DIVE);\n        item->goal_anim_state = LS(LS_SWIM);\n        Item_SwitchToAnim(item, LA(LA_ONWATER_DIVE), 0);\n        item->rot.x = -45 * DEG_1;\n        item->fall_speed = 80;\n        lara->water_status = LWS_UNDERWATER;\n        return;\n    }\n\n    if (g_Config.gameplay.enable_wading) {\n        M_TestWaterStepOut(item, coll);\n    } else {\n        M_TestWaterClimbOut(item, coll);\n    }\n}\n\nstatic void M_ForwardSurface(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    M_CommonSurface(item, coll);\n    if (g_Config.gameplay.enable_wading) {\n        M_TestWaterClimbOut(item, coll);\n    }\n}\n\nstatic void M_SideBackSurface(ITEM *const item, COLL_INFO *const coll)\n{\n    int32_t angle = 0;\n    switch (LS_U(item->current_anim_state)) {\n    case LS_SURF_BACK:\n        angle = -DEG_180;\n        break;\n    case LS_SURF_LEFT:\n        angle = -DEG_90;\n        break;\n    case LS_SURF_RIGHT:\n        angle = DEG_90;\n        break;\n    default:\n        break;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y + angle;\n    M_CommonSurface(item, coll);\n}\n\nstatic void M_Swim(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->rot.x < -DEG_90 || item->rot.x > DEG_90) {\n        lara->move_angle = item->rot.y + DEG_180;\n    } else {\n        lara->move_angle = item->rot.y;\n    }\n\n    coll->facing = lara->move_angle;\n\n    int32_t height;\n    if (g_Config.gameplay.enable_wading) {\n        height = (LARA_HEIGHT * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n        if (height < 0) {\n            height = -height;\n        }\n        CLAMPL(height, 200);\n        coll->bad_neg = -height;\n    } else {\n        height = LARA_HEIGHT_UW;\n    }\n\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y + height / 2, item->pos.z,\n        item->room_num, height);\n    Lara_Col_Shift(coll);\n\n    switch (coll->coll_type) {\n    case COLL_FRONT:\n        if (item->rot.x > 35 * DEG_1) {\n            item->rot.x += LARA_UW_WALL_DEFLECT;\n        } else if (item->rot.x < -35 * DEG_1) {\n            item->rot.x -= LARA_UW_WALL_DEFLECT;\n        } else {\n            item->fall_speed = 0;\n        }\n        break;\n\n    case COLL_TOP:\n        if (item->rot.x >= -45 * DEG_1) {\n            item->rot.x -= LARA_UW_WALL_DEFLECT;\n        }\n        break;\n\n    case COLL_TOP_FRONT:\n        item->fall_speed = 0;\n        break;\n\n    case COLL_LEFT:\n        item->rot.y += 5 * DEG_1;\n        break;\n\n    case COLL_RIGHT:\n        item->rot.y -= 5 * DEG_1;\n        break;\n\n    case COLL_CLAMP:\n        item->pos = coll->old;\n        item->fall_speed = 0;\n        return;\n    }\n\n    if (coll->side_mid.floor < 0) {\n        item->rot.x += LARA_UW_WALL_DEFLECT;\n        item->pos.y = coll->side_mid.floor + item->pos.y;\n    }\n\n    if (g_Config.gameplay.enable_wading && lara->water_status != LWS_CHEAT\n        && !lara->extra_anim) {\n        M_TestWaterDepth(item, coll);\n    }\n}\n\nstatic void M_UWDeath(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->air = -1;\n    lara->gun_status = LGS_HANDS_BUSY;\n    item->hit_points = -1;\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    if (water_height != NO_HEIGHT && water_height < item->pos.y - 100) {\n        item->pos.y -= 5;\n    }\n    M_Swim(item, coll);\n}\n\n// clang-format off\nREGISTER_LARA_COL(LS_SURF_SWIM,  M_ForwardSurface)\nREGISTER_LARA_COL(LS_SURF_TREAD, M_SideBackSurface)\nREGISTER_LARA_COL(LS_SURF_BACK,  M_SideBackSurface)\nREGISTER_LARA_COL(LS_SURF_LEFT,  M_SideBackSurface)\nREGISTER_LARA_COL(LS_SURF_RIGHT, M_SideBackSurface)\nREGISTER_LARA_COL(LS_SWIM,       M_Swim)\nREGISTER_LARA_COL(LS_TREAD,      M_Swim)\nREGISTER_LARA_COL(LS_GLIDE,      M_Swim)\nREGISTER_LARA_COL(LS_DIVE,       M_Swim)\nREGISTER_LARA_COL(LS_WATER_ROLL, M_Swim)\nREGISTER_LARA_COL(LS_UW_DEATH,   M_UWDeath)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/col.c",
    "content": "#include <trx/game/lara/col.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n#define M_MONKEY_CEILING_SNAP 704\n#define M_PUSH_TIMEOUT 15\n\nstatic void (*m_CollisionRoutines[LS_NUMBER_OF])(\n    ITEM *item, COLL_INFO *coll) = {};\n\nstatic void M_Push(\n    const COLL_ITEM *const item, COLL_INFO *const coll, const bool hit_on,\n    const bool big_push)\n{\n    ITEM *const target_item = Lara_GetItem();\n    int32_t dx = target_item->pos.x - item->pos.x;\n    int32_t dz = target_item->pos.z - item->pos.z;\n    const int32_t c = Math_Cos(item->rot.y);\n    const int32_t s = Math_Sin(item->rot.y);\n    int32_t rx = (c * dx - s * dz) >> W2V_SHIFT;\n    int32_t rz = (c * dz + s * dx) >> W2V_SHIFT;\n\n    const BOUNDS_16 *const bounds = &item->bounds;\n    int32_t min_x = bounds->min.x;\n    int32_t max_x = bounds->max.x;\n    int32_t min_z = bounds->min.z;\n    int32_t max_z = bounds->max.z;\n\n    if (big_push) {\n        max_x += coll->radius;\n        min_z -= coll->radius;\n        max_z += coll->radius;\n        min_x -= coll->radius;\n    }\n\n    if (rx < min_x || rx > max_x || rz < min_z || rz > max_z) {\n        return;\n    }\n\n    const int32_t l = rx - min_x;\n    const int32_t r = max_x - rx;\n    const int32_t t = max_z - rz;\n    const int32_t b = rz - min_z;\n\n    if (l <= r && l <= t && l <= b) {\n        rx -= l;\n    } else if (r <= l && r <= t && r <= b) {\n        rx += r;\n    } else if (t <= l && t <= r && t <= b) {\n        rz += t;\n    } else {\n        rz = min_z;\n    }\n\n    target_item->pos.x = item->pos.x + ((rz * s + rx * c) >> W2V_SHIFT);\n    target_item->pos.z = item->pos.z + ((rz * c - rx * s) >> W2V_SHIFT);\n\n    rz = (bounds->max.z + bounds->min.z) / 2;\n    rx = (bounds->max.x + bounds->min.x) / 2;\n    dx -= (c * rx + s * rz) >> W2V_SHIFT;\n    dz -= (c * rz - s * rx) >> W2V_SHIFT;\n\n    if (hit_on && bounds->max.y - bounds->min.y > STEP_L) {\n        Lara_TakeHit(target_item, dx, dz);\n    }\n\n    const int16_t old_facing = coll->facing;\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -STEPUP_HEIGHT;\n    coll->bad_ceiling = 0;\n    coll->facing = Math_Atan(\n        target_item->pos.z - coll->old.z, target_item->pos.x - coll->old.x);\n    Collide_GetCollisionInfo(\n        coll, target_item->pos.x, target_item->pos.y, target_item->pos.z,\n        target_item->room_num, LARA_HEIGHT);\n    coll->facing = old_facing;\n\n    if (coll->coll_type != COLL_NONE) {\n        target_item->pos.x = coll->old.x;\n        target_item->pos.z = coll->old.z;\n    } else {\n        coll->old = target_item->pos;\n        Lara_UpdateRoomToHeight(-10);\n    }\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->interact_target.is_moving\n        && lara_info->interact_target.move_count > M_PUSH_TIMEOUT) {\n        lara_info->interact_target.is_moving = false;\n        lara_info->gun_status = LGS_ARMLESS;\n    }\n}\n\nvoid Lara_Col_Register(\n    const LARA_TRX_STATE state,\n    void (*const handle_func)(ITEM *item, COLL_INFO *coll))\n{\n    ASSERT(state >= 0 && state < LS_NUMBER_OF);\n    m_CollisionRoutines[state] = handle_func;\n}\n\nvoid Lara_Col_Update(ITEM *const item, COLL_INFO *const coll)\n{\n    const LARA_TRX_STATE state = LS_U(item->current_anim_state);\n    if (state >= 0 && state < LS_NUMBER_OF\n        && m_CollisionRoutines[state] != nullptr) {\n        m_CollisionRoutines[state](item, coll);\n    }\n}\n\nvoid Lara_Col_GetInfo(const ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    coll->facing = lara->move_angle;\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        LARA_HEIGHT);\n}\n\nvoid Lara_Col_Shift(COLL_INFO *const coll)\n{\n    Collide_ShiftItem(Lara_GetItem(), coll);\n}\n\nvoid Lara_Col_MonkeySwingSnap(ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t ceiling = Room_GetCeiling(sector, item->pos);\n    if (ceiling != NO_HEIGHT) {\n        item->pos.y = ceiling + M_MONKEY_CEILING_SNAP;\n    }\n}\n\nvoid Lara_Col_ItemPush(\n    const ITEM *const item, COLL_INFO *const coll, const bool hit_on,\n    const bool big_push)\n{\n    const COLL_ITEM src_item = {\n        .bounds = Item_GetBestFrame(item)->bounds,\n        .pos = item->pos,\n        .rot = item->rot,\n    };\n    M_Push(&src_item, coll, hit_on, big_push);\n}\n\nvoid Lara_Col_Static3DPush(const STATIC_MESH *const mesh, COLL_INFO *const coll)\n{\n    const COLL_ITEM src_item = {\n        .bounds = Object_Get3DStatic(mesh->static_num)->collision_bounds,\n        .pos = mesh->pos,\n        .rot = { .y = mesh->rot.y },\n    };\n    M_Push(&src_item, coll, false, true);\n}\n\nvoid Lara_Col_WadeSplash(ITEM *const item)\n{\n    if (!g_Config.gameplay.enable_wading) {\n        return;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status == LWS_CHEAT) {\n        return;\n    }\n\n    const int32_t water_depth = Lara_GetWaterDepth(\n        item->pos.x, item->pos.y, item->pos.z, item->room_num);\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds;\n    if (water_height != NO_HEIGHT && water_depth != NO_HEIGHT\n        && bounds != nullptr && item->pos.y + bounds->min.y <= water_height\n        && item->pos.y + bounds->max.y >= water_height\n        && water_depth < LARA_SWIM_DEPTH - STEP_L) {\n        Spawn_Splash(item);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lara/col.h",
    "content": "#pragma once\n\n#include <trx/game/collision.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/rooms/types.h>\n\ntypedef enum {\n    // clang-format off\n    EDGE_CATCH_NEG  = -1,\n    EDGE_CATCH_NONE = 0,\n    EDGE_CATCH_POS  = 1,\n    // clang-format on\n} EDGE_CATCH;\n\ntypedef enum {\n    LANDED_OK,\n    LANDED_BAD,\n    LANDED_HANDLED,\n} LANDED_STATE;\n\ntypedef enum {\n    SWING_CATCH_NONE,\n    SWING_CATCH_FAST,\n    SWING_CATCH_SLOW,\n} SWING_CATCH;\n\nvoid Lara_Col_Register(\n    LARA_TRX_STATE state, void (*handle_func)(ITEM *item, COLL_INFO *coll));\nvoid Lara_Col_Update(ITEM *item, COLL_INFO *coll);\nvoid Lara_Col_GetInfo(const ITEM *item, COLL_INFO *coll);\nvoid Lara_Col_Shift(COLL_INFO *coll);\nbool Lara_Col_TestVault(ITEM *item, COLL_INFO *coll);\nbool Lara_Col_TestSlide(ITEM *item, COLL_INFO *coll);\nbool Lara_Col_TestLadderHang(ITEM *item, const COLL_INFO *coll);\nbool Lara_Col_TestCeiling(ITEM *item, const COLL_INFO *coll);\nSWING_CATCH Lara_Col_TestHangSwingIn(const ITEM *item, int16_t angle);\nEDGE_CATCH Lara_Col_TestEdgeCatch(\n    const ITEM *item, const COLL_INFO *coll, int32_t *edge);\nbool Lara_Col_Fallen(ITEM *item, const COLL_INFO *coll);\nvoid Lara_Col_DeflectEdgeJump(ITEM *item, COLL_INFO *coll);\nLANDED_STATE Lara_Col_LandedBad(ITEM *item);\nvoid Lara_Col_MonkeySwingSnap(ITEM *item);\nvoid Lara_Col_HangTest(ITEM *item, COLL_INFO *coll);\nvoid Lara_Col_ItemPush(\n    const ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push);\nvoid Lara_Col_Static3DPush(const STATIC_MESH *mesh, COLL_INFO *coll);\nvoid Lara_Col_WadeSplash(ITEM *item);\nvoid Lara_Col_CrawlTilt(ITEM *item);\n"
  },
  {
    "path": "src/trx/game/lara/common.c",
    "content": "#include <trx/game/lara/common.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/catalog/manager.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/draw.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_MOVE_ANIM_VELOCITY 12\n#define M_MOVE_SPEED 16\n#define M_MOVE_ANGLE (2 * DEG_1) // = 364\n\nstatic const LARA_TRX_ANIMATION m_InvalidInterpAnims[] = {\n    // clang-format off\n    LA_JUMP_NEUTRAL_ROLL,\n    LA_JUMP_BACK_ROLL_START,\n    LA_JUMP_BACK_ROLL_END,\n    LA_CONTROLLED_DROP_CONTINUE,\n    LA_HANG_TO_JUMP_BACK,\n    LA_TRX_INVALID, // sentinel\n    // clang-format on\n};\n\nstatic LARA_INFO m_Lara = {};\nstatic ITEM *m_LaraItem = nullptr;\nstatic bool m_Controllable = false;\nstatic int16_t m_DeathCameraTarget = NO_ITEM;\nstatic LARA_EXTRA_STATE m_StartAnimState = LS_EXTRA_BREATH;\n\nstatic bool M_IsInvalidInterpAnim(const LARA_TRX_ANIMATION anim_idx)\n{\n    for (int32_t i = 0; m_InvalidInterpAnims[i] != LA_TRX_INVALID; i++) {\n        if (m_InvalidInterpAnims[i] == anim_idx) {\n            return true;\n        }\n    }\n    return false;\n}\n\nLARA_INFO *Lara_GetLaraInfo(void)\n{\n    return &m_Lara;\n}\n\nITEM *Lara_GetItem(void)\n{\n    return m_LaraItem;\n}\n\nvoid Lara_InitialiseLoad(int16_t item_num)\n{\n    m_Lara.item_num = item_num;\n    if (item_num == NO_ITEM) {\n        m_LaraItem = nullptr;\n    } else {\n        m_LaraItem = Item_Get(item_num);\n    }\n}\n\nstatic int32_t M_GetStartingHitPoints(void)\n{\n    if (g_Config.gameplay.disable_healing_between_levels) {\n        const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level);\n        if (resume != nullptr) {\n            return resume->lara_hitpoints;\n        }\n    }\n    return g_Config.gameplay.start_lara_hitpoints;\n}\n\nvoid Lara_Initialise(const GF_LEVEL *const level)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_item->collidable = false;\n\n    m_Controllable = true;\n    m_DeathCameraTarget = NO_ITEM;\n    Lara_Vehicle_SetIndex(NO_ITEM);\n\n    lara_item->hit_points = M_GetStartingHitPoints();\n    lara_info->gun_item_num = NO_ITEM;\n    lara_info->flare.age = 0;\n    lara_info->flare.control = false;\n    lara_info->flare.frame_num = 0;\n    lara_info->calc_fall_speed = 0;\n    lara_info->pose_count = 0;\n    lara_info->hit_direction = DIR_UNKNOWN;\n    lara_info->hit_effect = nullptr;\n    lara_info->hit_effect_count = 0;\n    lara_info->hit_frame = 0;\n    lara_info->air = LARA_MAX_AIR;\n    lara_info->sprint_timer = LARA_MAX_SPRINT;\n    lara_info->exposure_timer = LARA_MAX_EXPOSURE;\n    lara_info->water_surface_dist = 100;\n    lara_info->death_timer = 0;\n    lara_info->dive_timer = 0;\n    lara_info->idle_timer = 0;\n    lara_info->current.active = 0;\n    lara_info->extra_anim = false;\n    lara_info->burn = false;\n    lara_info->electric = 0;\n    lara_info->climb_status = false;\n    lara_info->sprinting = false;\n    lara_info->killed_loyal_item = false;\n    lara_info->mesh_effects = 0;\n    lara_info->torso_rot.x = 0;\n    lara_info->torso_rot.y = 0;\n    lara_info->torso_rot.z = 0;\n    lara_info->head_rot.x = 0;\n    lara_info->head_rot.y = 0;\n    lara_info->head_rot.z = 0;\n    lara_info->move_angle = 0;\n    lara_info->turn_rate = 0;\n    lara_info->target = nullptr;\n    lara_info->last_pos = lara_item->pos;\n    lara_info->right_arm.flash_gun = 0;\n    lara_info->left_arm.flash_gun = 0;\n    lara_info->right_arm.lock = 0;\n    lara_info->left_arm.lock = 0;\n    lara_info->interact_target.is_moving = false;\n    lara_info->interact_target.item_num = NO_ITEM;\n    lara_info->interact_target.move_count = 0;\n    lara_info->poison_timer = 0;\n    lara_info->tr3_smoke_count_l = 0;\n    lara_info->tr3_smoke_count_r = 0;\n    lara_info->mesh_pos_matrices_valid = false;\n\n    LOT_InitialiseLOT(&lara_info->lot);\n    lara_info->lot.setup.step = WALL_L * 20;\n    lara_info->lot.setup.drop = -WALL_L * 20;\n    lara_info->lot.setup.fly = STEP_L;\n\n    Lara_Skin_Initialise();\n    if (level->type == GFL_CUTSCENE) {\n        Lara_Mesh_Initialise(level);\n        lara_info->gun_status = LGS_ARMLESS;\n    } else {\n        Lara_InitialiseInventory(level);\n    }\n\n    Lara_Control_Initialise(level->type, m_StartAnimState);\n}\n\nvoid Lara_InitialiseInventory(const GF_LEVEL *const level)\n{\n    Inv_RemoveAllItems();\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n\n    if (resume != nullptr) {\n        lara_info->pistol_ammo.ammo = 1000;\n        if (resume->flags.has_pistols) {\n            Inv_AddItem(O_PISTOL_ITEM);\n        }\n\n        if (resume->flags.has_magnums) {\n            Inv_AddItem(O_MAGNUM_ITEM);\n            lara_info->magnum_ammo.ammo = resume->magnum_ammo;\n            Item_GlobalReplace(O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_MAGNUM_AMMO_ITEM,\n                resume->magnum_ammo / Gun_GetAmmoPickupQuantity(LGT_MAGNUMS));\n            lara_info->magnum_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_autos) {\n            Inv_AddItem(O_AUTOS_ITEM);\n            lara_info->autos_ammo.ammo = resume->autos_ammo;\n            Item_GlobalReplace(O_AUTOS_ITEM, O_AUTOS_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_AUTOS_AMMO_ITEM,\n                resume->autos_ammo / Gun_GetAmmoPickupQuantity(LGT_AUTOS));\n            lara_info->autos_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_desert_eagle) {\n            Inv_AddItem(O_DESERT_EAGLE_ITEM);\n            lara_info->desert_eagle_ammo.ammo = resume->desert_eagle_ammo;\n            Item_GlobalReplace(O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_DESERT_EAGLE_AMMO_ITEM,\n                resume->desert_eagle_ammo\n                    / Gun_GetAmmoPickupQuantity(LGT_DESERT_EAGLE));\n            lara_info->desert_eagle_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_uzis) {\n            Inv_AddItem(O_UZI_ITEM);\n            lara_info->uzi_ammo.ammo = resume->uzi_ammo;\n            Item_GlobalReplace(O_UZI_ITEM, O_UZI_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_UZI_AMMO_ITEM,\n                resume->uzi_ammo / Gun_GetAmmoPickupQuantity(LGT_UZIS));\n            lara_info->uzi_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_shotgun) {\n            Inv_AddItem(O_SHOTGUN_ITEM);\n            lara_info->shotgun_ammo.ammo = resume->shotgun_ammo;\n            Item_GlobalReplace(O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_SHOTGUN_AMMO_ITEM,\n                resume->shotgun_ammo / Gun_GetAmmoPickupQuantity(LGT_SHOTGUN));\n            lara_info->shotgun_ammo.ammo = 0;\n        }\n\n        Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, resume->small_medipacks);\n        Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, resume->large_medipacks);\n        Inv_AddItemNTimes(O_FLARE_ITEM, resume->flares);\n        Inv_AddItemNTimes(O_SCION_ITEM_1, resume->num_scions);\n        Inv_AddItemNTimes(O_QUEST_ITEM_1, resume->num_quest_item_1);\n        Inv_AddItemNTimes(O_QUEST_ITEM_2, resume->num_quest_item_2);\n        Inv_AddItemNTimes(O_QUEST_ITEM_3, resume->num_quest_item_3);\n        Inv_AddItemNTimes(O_QUEST_ITEM_4, resume->num_quest_item_4);\n        if (resume->flags.has_m16) {\n            Inv_AddItem(O_M16_ITEM);\n            lara_info->m16_ammo.ammo = resume->m16_ammo;\n            Item_GlobalReplace(O_M16_ITEM, O_M16_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_M16_AMMO_ITEM,\n                resume->m16_ammo / Gun_GetAmmoPickupQuantity(LGT_M16));\n            lara_info->m16_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_mp5) {\n            Inv_AddItem(O_MP5_ITEM);\n            lara_info->mp5_ammo.ammo = resume->mp5_ammo;\n            Item_GlobalReplace(O_MP5_ITEM, O_MP5_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_MP5_AMMO_ITEM,\n                resume->mp5_ammo / Gun_GetAmmoPickupQuantity(LGT_MP5));\n            lara_info->mp5_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_grenade) {\n            Inv_AddItem(O_GRENADE_GUN_ITEM);\n            lara_info->grenade_ammo.ammo = resume->grenade_ammo;\n            Item_GlobalReplace(O_GRENADE_GUN_ITEM, O_GRENADE_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_GRENADE_AMMO_ITEM,\n                resume->grenade_ammo / Gun_GetAmmoPickupQuantity(LGT_GRENADE));\n            lara_info->grenade_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_rocket) {\n            Inv_AddItem(O_ROCKET_GUN_ITEM);\n            lara_info->rocket_ammo.ammo = resume->rocket_ammo;\n            Item_GlobalReplace(O_ROCKET_GUN_ITEM, O_ROCKET_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_ROCKET_AMMO_ITEM,\n                resume->rocket_ammo / Gun_GetAmmoPickupQuantity(LGT_ROCKET));\n            lara_info->rocket_ammo.ammo = 0;\n        }\n\n        if (resume->flags.has_harpoon) {\n            Inv_AddItem(O_HARPOON_ITEM);\n            lara_info->harpoon_ammo.ammo = resume->harpoon_ammo;\n            Item_GlobalReplace(O_HARPOON_ITEM, O_HARPOON_AMMO_ITEM);\n        } else {\n            Inv_AddItemNTimes(\n                O_HARPOON_AMMO_ITEM,\n                resume->harpoon_ammo / Gun_GetAmmoPickupQuantity(LGT_HARPOON));\n            lara_info->harpoon_ammo.ammo = 0;\n        }\n\n        if (g_Config.gameplay.remember_gun_status) {\n            lara_info->gun_status = resume->gun_status;\n            lara_info->gun_type = resume->equipped_gun_type;\n        }\n        lara_info->last_gun_type = resume->equipped_gun_type;\n        lara_info->holsters_gun_type = resume->holsters_gun_type;\n        lara_info->back_gun_type = resume->back_gun_type;\n    }\n\n    if (!g_Config.gameplay.remember_gun_status) {\n        lara_info->gun_status = LGS_ARMLESS;\n        lara_info->gun_type = lara_info->last_gun_type;\n    }\n    lara_info->request_gun_type = lara_info->last_gun_type;\n    Lara_Mesh_Initialise(level);\n    Gun_InitialiseNewWeapon();\n    Gun_EnsureReady();\n}\n\nvoid Lara_RevertToPistolsIfNeeded(void)\n{\n    if (g_Config.gameplay.remember_gun_status\n        || !Inv_RequestItem(O_PISTOL_ITEM)) {\n        return;\n    }\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->last_gun_type = LGT_PISTOLS;\n    lara_info->holsters_gun_type = LGT_PISTOLS;\n\n    if (lara_info->gun_status != LGS_ARMLESS) {\n        lara_info->holsters_gun_type = LGT_UNARMED;\n        lara_info->request_gun_type = LGT_PISTOLS;\n        lara_info->gun_type = LGT_PISTOLS;\n    }\n    if (Inv_RequestItem(O_SHOTGUN_ITEM)) {\n        lara_info->back_gun_type = LGT_SHOTGUN;\n    } else {\n        lara_info->back_gun_type = LGT_UNARMED;\n    }\n    Gun_InitialiseNewWeapon();\n    Gun_SetLaraHolsterLMesh(lara_info->holsters_gun_type);\n    Gun_SetLaraHolsterRMesh(lara_info->holsters_gun_type);\n    Gun_SetLaraBackMesh(lara_info->back_gun_type);\n}\n\nvoid Lara_UseItem(const OBJECT_ID obj_id)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n\n    LARA_GUN_TYPE request_gun_type = LGT_UNARMED;\n    switch (obj_id) {\n    case O_PISTOL_ITEM:\n    case O_PISTOL_OPTION:\n        request_gun_type = LGT_PISTOLS;\n        break;\n\n    case O_SHOTGUN_ITEM:\n    case O_SHOTGUN_OPTION:\n        request_gun_type = LGT_SHOTGUN;\n        break;\n\n    case O_MAGNUM_ITEM:\n    case O_MAGNUM_OPTION:\n        request_gun_type = LGT_MAGNUMS;\n        break;\n\n    case O_AUTOS_ITEM:\n    case O_AUTOS_OPTION:\n        request_gun_type = LGT_AUTOS;\n        break;\n\n    case O_DESERT_EAGLE_ITEM:\n    case O_DESERT_EAGLE_OPTION:\n        request_gun_type = LGT_DESERT_EAGLE;\n        break;\n\n    case O_UZI_ITEM:\n    case O_UZI_OPTION:\n        request_gun_type = LGT_UZIS;\n        break;\n\n    case O_HARPOON_ITEM:\n    case O_HARPOON_OPTION:\n        request_gun_type = LGT_HARPOON;\n        break;\n\n    case O_M16_ITEM:\n    case O_M16_OPTION:\n        request_gun_type = LGT_M16;\n        break;\n\n    case O_MP5_ITEM:\n    case O_MP5_OPTION:\n        request_gun_type = LGT_MP5;\n        break;\n\n    case O_GRENADE_GUN_ITEM:\n    case O_GRENADE_GUN_OPTION:\n        request_gun_type = LGT_GRENADE;\n        break;\n\n    case O_ROCKET_GUN_ITEM:\n    case O_ROCKET_GUN_OPTION:\n        request_gun_type = LGT_ROCKET;\n        break;\n\n    case O_FLAREBOX_ITEM:\n    case O_FLAREBOX_OPTION:\n        lara_info->request_gun_type = LGT_FLARE;\n        break;\n\n    case O_SMALL_MEDIPACK_ITEM:\n    case O_SMALL_MEDIPACK_OPTION:\n        if ((lara_item->hit_points > 0\n             && lara_item->hit_points < LARA_MAX_HITPOINTS)\n            || lara_info->poison_timer != 0) {\n            lara_info->poison_timer = 0;\n            lara_item->hit_points += LARA_MAX_HITPOINTS / 2;\n            CLAMPG(lara_item->hit_points, LARA_MAX_HITPOINTS);\n            Inv_RemoveItem(O_SMALL_MEDIPACK_ITEM);\n            Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS);\n            Stats_AddMedipacksUsed(0.5);\n        }\n        break;\n\n    case O_LARGE_MEDIPACK_ITEM:\n    case O_LARGE_MEDIPACK_OPTION:\n        if ((lara_item->hit_points > 0\n             && lara_item->hit_points < LARA_MAX_HITPOINTS)\n            || lara_info->poison_timer != 0) {\n            lara_info->poison_timer = 0;\n            lara_item->hit_points = LARA_MAX_HITPOINTS;\n            Inv_RemoveItem(O_LARGE_MEDIPACK_ITEM);\n            Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS);\n            Stats_AddMedipacksUsed(1);\n        }\n        break;\n\n    case O_KEY_ITEM_1:\n    case O_KEY_OPTION_1:\n    case O_KEY_ITEM_2:\n    case O_KEY_OPTION_2:\n    case O_KEY_ITEM_3:\n    case O_KEY_OPTION_3:\n    case O_KEY_ITEM_4:\n    case O_KEY_OPTION_4:\n    case O_PUZZLE_ITEM_1:\n    case O_PUZZLE_OPTION_1:\n    case O_PUZZLE_ITEM_2:\n    case O_PUZZLE_OPTION_2:\n    case O_PUZZLE_ITEM_3:\n    case O_PUZZLE_OPTION_3:\n    case O_PUZZLE_ITEM_4:\n    case O_PUZZLE_OPTION_4:\n    case O_LEADBAR_ITEM:\n    case O_LEADBAR_OPTION:\n    case O_SCION_ITEM_1:\n    case O_SCION_ITEM_2:\n    case O_SCION_ITEM_3:\n    case O_SCION_ITEM_4:\n    case O_SCION_OPTION: {\n        const int16_t receptacle_item_num = Object_FindReceptacle(obj_id);\n        if (receptacle_item_num == NO_ITEM\n            || lara_info->interact_target.item_num != NO_ITEM) {\n            Sound_Effect(SFX_LARA_NO, nullptr, SPM_NORMAL);\n            return;\n        }\n\n        lara_info->interact_target.item_num = receptacle_item_num;\n        lara_info->interact_target.is_moving = true;\n        lara_info->interact_target.move_count = 0;\n        break;\n    }\n\n    default:\n        break;\n    }\n\n    if (request_gun_type != LGT_UNARMED) {\n        lara_info->request_gun_type = request_gun_type;\n        if (lara_info->gun_status == LGS_ARMLESS\n            && lara_info->gun_type == request_gun_type) {\n            lara_info->gun_type = LGT_UNARMED;\n        }\n    }\n}\n\nvoid Lara_SetStartAnimState(const LARA_EXTRA_STATE state)\n{\n    m_StartAnimState = state;\n}\n\nbool Lara_IsControllable(void)\n{\n    return m_Controllable;\n}\n\nvoid Lara_SetControllable(const bool controllable)\n{\n    m_Controllable = controllable;\n}\n\nbool Lara_CanInterpolate(\n    const ITEM *const item, const int32_t frame_a, const int32_t frame_b)\n{\n    if (item->frame_num == item->prev_frame_num) {\n        return false;\n    }\n\n    const LARA_ANIMATION anim_idx = Item_GetRelativeAnim(item);\n    if (!M_IsInvalidInterpAnim(LA_U(anim_idx))) {\n        return true;\n    }\n\n    // Avoid the flip 180 command having a bad effect on interpolated frames\n    // on rate 1 animations, such as neutral jump twist. TODO: improve this.\n    const ANIM *const anim = Item_GetAnim(item);\n    return !Anim_HasFXCommandBetween(\n        anim, ITEM_ACTION_TURN_180, frame_a, frame_b);\n}\n\nITEM *Lara_GetDeathCameraTarget(void)\n{\n    return Item_Get(m_DeathCameraTarget);\n}\n\nvoid Lara_SetDeathCameraTarget(const int16_t item_num)\n{\n    m_DeathCameraTarget = item_num;\n}\n\nOBJECT_ID Lara_GetAnimationObject(void)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->extra_anim) {\n        return O_LARA_EXTRA;\n    }\n\n    const ITEM *const vehicle = Lara_Vehicle_GetItem();\n    if (vehicle == nullptr) {\n        return O_LARA;\n    }\n\n    switch (vehicle->object_id) {\n    case O_BOAT:\n        return O_LARA_BOAT;\n    case O_SKIDOO_FAST:\n        return O_LARA_SKIDOO;\n    default:\n        return O_LARA_VEHICLE_ANIM;\n    }\n}\n\nvoid Lara_Animate(ITEM *const item)\n{\n    const ROOM *const room = Room_Get(item->room_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    item->prev_frame_num = item->frame_num;\n    item->frame_num++;\n\n    const ANIM *anim = Item_GetAnim(item);\n    if (anim->num_changes > 0 && Item_GetAnimChange(item, anim)) {\n        anim = Item_GetAnim(item);\n        item->current_anim_state = anim->current_anim_state;\n    }\n\n    if (item->frame_num > anim->frame_end) {\n        for (int32_t i = 0; i < anim->num_commands; i++) {\n            const ANIM_COMMAND *const command = &anim->commands[i];\n            switch (command->type) {\n            case AC_MOVE_ORIGIN: {\n                const XYZ_16 *const pos = (XYZ_16 *)command->data;\n                Item_Translate(item, pos->x, pos->y, pos->z);\n                break;\n            }\n\n            case AC_JUMP_VELOCITY: {\n                const ANIM_COMMAND_VELOCITY_DATA *const data =\n                    (ANIM_COMMAND_VELOCITY_DATA *)command->data;\n                item->fall_speed = data->fall_speed;\n                item->speed = data->speed;\n                item->gravity = true;\n                if (lara->calc_fall_speed != 0) {\n                    item->fall_speed = lara->calc_fall_speed;\n                    lara->calc_fall_speed = 0;\n                }\n                break;\n            }\n\n            case AC_ATTACK_READY:\n                if (lara->gun_status != LGS_SPECIAL) {\n                    lara->gun_status = LGS_ARMLESS;\n                }\n                break;\n            default:\n                break;\n            }\n        }\n\n        item->anim_num = anim->jump_anim_num;\n        item->frame_num = anim->jump_frame_num;\n        anim = Item_GetAnim(item);\n        item->current_anim_state = anim->current_anim_state;\n    }\n\n    for (int32_t i = 0; i < anim->num_commands; i++) {\n        const ANIM_COMMAND *const command = &anim->commands[i];\n\n        switch (command->type) {\n        case AC_SOUND_FX: {\n            const ANIM_COMMAND_EFFECT_DATA *const data =\n                (ANIM_COMMAND_EFFECT_DATA *)command->data;\n            Item_PlayAnimSFX(item, data);\n            break;\n        }\n\n        case AC_EFFECT: {\n            const ANIM_COMMAND_EFFECT_DATA *const data =\n                (ANIM_COMMAND_EFFECT_DATA *)command->data;\n            if (item->frame_num != data->frame_num) {\n                break;\n            }\n\n            if (g_TRVersion == 3) {\n                ItemAction_RunDirectWithFX(\n                    data->effect_num, item, data->fx_type);\n                break;\n            }\n\n            const ANIM_COMMAND_ENVIRONMENT type = data->environment;\n            const int32_t height = lara->water_surface_dist;\n            if ((type == ACE_WATER && (height >= 0 || height == NO_HEIGHT))\n                || (type == ACE_LAND && height < 0 && height != NO_HEIGHT\n                    && !room->flags.swamp)) {\n                break;\n            }\n\n            ItemAction_RunDirect(data->effect_num, item);\n            break;\n        }\n\n        default:\n            break;\n        }\n    }\n\n    const int32_t rel_frame = item->frame_num - anim->frame_base;\n    if (!item->gravity) {\n        int32_t speed = anim->velocity;\n        if (lara->water_status == LWS_WADE && room->flags.swamp) {\n            speed /= 2;\n            speed += (anim->acceleration * rel_frame) / 4;\n        } else {\n            speed += anim->acceleration * rel_frame;\n        }\n        item->speed = (int16_t)(speed >> 16);\n    } else if (room->flags.swamp) {\n        item->speed -= item->speed >> 3;\n        if (ABS(item->speed) < 8) {\n            item->speed = 0;\n            item->gravity = false;\n        }\n        if (item->fall_speed > 128) {\n            item->fall_speed /= 2;\n        }\n        item->fall_speed -= item->fall_speed / 4;\n        CLAMPL(item->fall_speed, 4);\n    } else {\n        int32_t speed = anim->velocity + anim->acceleration * (rel_frame - 1);\n        item->speed -= (int16_t)(speed >> 16);\n        speed += anim->acceleration;\n        item->speed += (int16_t)(speed >> 16);\n\n        item->fall_speed += item->fall_speed < FAST_FALL_SPEED ? GRAVITY : 1;\n        item->pos.y += item->fall_speed;\n    }\n\n    item->pos.x += (item->speed * Math_Sin(lara->move_angle)) >> W2V_SHIFT;\n    item->pos.z += (item->speed * Math_Cos(lara->move_angle)) >> W2V_SHIFT;\n}\n\nvoid Lara_AnimateUntil(ITEM *lara_item, int32_t goal)\n{\n    lara_item->goal_anim_state = goal;\n    do {\n        Lara_Animate(lara_item);\n    } while (lara_item->current_anim_state != goal);\n}\n\nconst ANIM_FRAME *Lara_GetHitFrame(const ITEM *const item)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->hit_direction < 0) {\n        return nullptr;\n    }\n\n    // clang-format off\n    LARA_ANIMATION anim_idx;\n    if (lara->is_crouched) {\n        switch (lara->hit_direction) {\n        case DIR_EAST:  anim_idx = LA(LA_CROUCH_HIT_LEFT); break;\n        case DIR_SOUTH: anim_idx = LA(LA_CROUCH_HIT_BACK); break;\n        case DIR_WEST:  anim_idx = LA(LA_CROUCH_HIT_RIGHT); break;\n        default:        anim_idx = LA(LA_CROUCH_HIT_FRONT); break;\n        }\n    } else {\n        switch (lara->hit_direction) {\n        case DIR_EAST:  anim_idx = LA(LA_HIT_LEFT); break;\n        case DIR_SOUTH: anim_idx = LA(LA_HIT_BACK); break;\n        case DIR_WEST:  anim_idx = LA(LA_HIT_RIGHT); break;\n        default:        anim_idx = LA(LA_HIT_FRONT); break;\n        }\n    }\n    // clang-format on\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const ANIM *const anim = Object_GetAnim(obj, anim_idx);\n    return &anim->frame_ptr[lara->hit_frame];\n}\n\nvoid Lara_TakeDamage(const int16_t damage, const bool hit_status)\n{\n    if (g_Config.debug.enable_invulnerability) {\n        return;\n    }\n    Item_TakeDamage(Lara_GetItem(), damage, hit_status);\n}\n\n// TODO: This does the same thing in principle as Lara_GetJointAbsPosition().\n// Consider merging these functions into a single function.\nbool Lara_GetMeshPos(const LARA_MESH mesh, XYZ_32 *const out_pos)\n{\n    ASSERT(out_pos != nullptr);\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!lara->mesh_pos_matrices_valid) {\n        return false;\n    }\n\n    MATRIX *const m = g_MatrixPtr;\n    *m = lara->mesh_pos_matrices[mesh];\n    Matrix_TranslateRel32(*out_pos);\n    *out_pos = (XYZ_32) {\n        .x = (m->_03 >> W2V_SHIFT),\n        .y = (m->_13 >> W2V_SHIFT),\n        .z = (m->_23 >> W2V_SHIFT),\n    };\n    return true;\n}\n\nbool Lara_TestBoundsCollide(const ITEM *const item, const int32_t radius)\n{\n    return Item_TestBoundsCollide(item, Lara_GetItem(), radius);\n}\n\nbool Lara_TestPosition(\n    const ITEM *const item, const OBJECT_BOUNDS *const bounds)\n{\n    const ITEM *const lara = Lara_GetItem();\n    const XYZ_16 ref_rot = bounds->ignore_rot\n        ? (XYZ_16) { .x = 0, .y = lara->rot.y, .z = 0 }\n        : item->rot;\n    const XYZ_16 rot = {\n        .x = lara->rot.x - ref_rot.x,\n        .y = lara->rot.y - ref_rot.y,\n        .z = lara->rot.z - ref_rot.z,\n    };\n    const XYZ_32 dist = {\n        .x = lara->pos.x - item->pos.x,\n        .y = lara->pos.y - item->pos.y,\n        .z = lara->pos.z - item->pos.z,\n    };\n\n    // clang-format off\n    if (rot.x < bounds->rot.min.x ||\n        rot.x > bounds->rot.max.x ||\n        rot.y < bounds->rot.min.y ||\n        rot.y > bounds->rot.max.y ||\n        rot.z < bounds->rot.min.z ||\n        rot.z > bounds->rot.max.z\n    ) {\n        return false;\n    }\n    // clang-format on\n\n    Matrix_PushUnit();\n    Matrix_Rot16(ref_rot);\n    const MATRIX *const m = g_MatrixPtr;\n    const XYZ_32 shift = {\n        .x = (dist.x * m->_00 + dist.y * m->_10 + dist.z * m->_20) >> W2V_SHIFT,\n        .y = (dist.x * m->_01 + dist.y * m->_11 + dist.z * m->_21) >> W2V_SHIFT,\n        .z = (dist.x * m->_02 + dist.y * m->_12 + dist.z * m->_22) >> W2V_SHIFT,\n    };\n    Matrix_Pop();\n\n    // clang-format off\n    return (\n        shift.x >= bounds->shift.min.x &&\n        shift.x <= bounds->shift.max.x &&\n        shift.y >= bounds->shift.min.y &&\n        shift.y <= bounds->shift.max.y &&\n        shift.z >= bounds->shift.min.z &&\n        shift.z <= bounds->shift.max.z\n    );\n    // clang-format on\n}\n\nvoid Lara_AlignPosition(const ITEM *const item, const XYZ_32 *const vec)\n{\n    ITEM *const lara = Lara_GetItem();\n    lara->rot = item->rot;\n    Matrix_PushUnit();\n    Matrix_Rot16(item->rot);\n    const MATRIX *const m = g_MatrixPtr;\n    const XYZ_32 shift = {\n        .x = (vec->x * m->_00 + vec->y * m->_01 + vec->z * m->_02) >> W2V_SHIFT,\n        .y = (vec->x * m->_10 + vec->y * m->_11 + vec->z * m->_12) >> W2V_SHIFT,\n        .z = (vec->x * m->_20 + vec->y * m->_21 + vec->z * m->_22) >> W2V_SHIFT,\n    };\n    Matrix_Pop();\n\n    const XYZ_32 new_pos = {\n        .x = item->pos.x + shift.x,\n        .y = item->pos.y + shift.y,\n        .z = item->pos.z + shift.z,\n    };\n\n    if (g_Config.gameplay.fix_lara_pickup_embed) {\n        int16_t room_num = lara->room_num;\n        const SECTOR *const sector = Room_GetSector(new_pos, &room_num);\n        const int32_t height = Room_GetHeight(sector, new_pos);\n        const int32_t ceiling = Room_GetCeiling(sector, new_pos);\n\n        if (ABS(height - lara->pos.y) > STEP_L\n            || ABS(ceiling - lara->pos.y) < LARA_HEIGHT) {\n            return;\n        }\n    }\n\n    lara->pos = new_pos;\n}\n\nbool Lara_IsNearItem(const XYZ_32 *const pos, const int32_t distance)\n{\n    const ITEM *const item = Lara_GetItem();\n    const XYZ_32 d = {\n        .x = pos->x - item->pos.x,\n        .y = pos->y - item->pos.y,\n        .z = pos->z - item->pos.z,\n    };\n    if (ABS(d.x) > distance || ABS(d.z) > distance || ABS(d.y) > WALL_L * 3) {\n        return false;\n    }\n\n    if (SQUARE(d.x) + SQUARE(d.z) > SQUARE(distance)) {\n        return false;\n    }\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    return d.y >= bounds->min.y && d.y <= bounds->max.y + 100;\n}\n\nbool Lara_MovePosition(const ITEM *const ref_item, const XYZ_32 *const vec)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const bool walk_to_items = g_Config.gameplay.enable_walk_to_items\n        && ref_item->object_id != O_FLARE_ITEM;\n    const bool lara_on_land = lara_info->water_status != LWS_UNDERWATER\n        && lara_info->water_status != LWS_CHEAT;\n    const int32_t velocity =\n        walk_to_items && lara_on_land ? M_MOVE_ANIM_VELOCITY : M_MOVE_SPEED;\n\n    ITEM *const lara_item = Lara_GetItem();\n    const XYZ_16 new_rot = ref_item->rot;\n\n    Matrix_PushUnit();\n    Matrix_Rot16(new_rot);\n    const MATRIX *const m = g_MatrixPtr;\n    const XYZ_32 shift = {\n        .x = (vec->y * m->_01 + vec->z * m->_02 + vec->x * m->_00) >> W2V_SHIFT,\n        .y = (vec->x * m->_10 + vec->z * m->_12 + vec->y * m->_11) >> W2V_SHIFT,\n        .z = (vec->y * m->_21 + vec->x * m->_20 + vec->z * m->_22) >> W2V_SHIFT,\n    };\n    Matrix_Pop();\n\n    const XYZ_32 new_pos = {\n        .x = ref_item->pos.x + shift.x,\n        .y = ref_item->pos.y + shift.y,\n        .z = ref_item->pos.z + shift.z,\n    };\n\n    if (ref_item->object_id == O_FLARE_ITEM) {\n        int16_t room_num = lara_item->room_num;\n        const SECTOR *const sector = Room_GetSector(new_pos, &room_num);\n        const int32_t height = Room_GetHeight(sector, new_pos);\n        if (ABS(height - lara_item->pos.y) > STEP_L * 2) {\n            return false;\n        }\n        if (XYZ_32_GetDistance(new_pos, lara_item->pos) < STEP_L) {\n            return true;\n        }\n    }\n\n    const XYZ_32 dpos = {\n        .x = new_pos.x - lara_item->pos.x,\n        .y = new_pos.y - lara_item->pos.y,\n        .z = new_pos.z - lara_item->pos.z,\n    };\n    const int32_t length = XYZ_32_GetLength(dpos);\n    if (velocity >= length) {\n        lara_item->pos = new_pos;\n    } else {\n        lara_item->pos.x += velocity * dpos.x / length;\n        lara_item->pos.y += velocity * dpos.y / length;\n        lara_item->pos.z += velocity * dpos.z / length;\n    }\n\n    if (walk_to_items && !lara_info->interact_target.is_moving) {\n        if (lara_on_land) {\n            const int16_t step_to_anim_num[4] = {\n                LA(LA_SIDE_STEP_LEFT),\n                LA(LA_WALK_FORWARD),\n                LA(LA_SIDE_STEP_RIGHT),\n                LA(LA_WALK_BACK),\n            };\n            const int16_t step_to_anim_state[4] = {\n                LS(LS_STEP_LEFT),\n                LS(LS_WALK),\n                LS(LS_STEP_RIGHT),\n                LS(LS_WALK_BACK),\n            };\n\n            const int32_t dx = lara_item->pos.x - new_pos.x;\n            const int32_t dz = lara_item->pos.z - new_pos.z;\n            const int32_t angle = (DEG_360 - Math_Atan(dx, dz)) % DEG_360;\n            const uint32_t src_quadrant = (uint32_t)(angle + DEG_45) / DEG_90;\n            const uint32_t dst_quadrant =\n                (uint32_t)(new_rot.y + DEG_45) / DEG_90;\n            const DIRECTION quadrant = (src_quadrant - dst_quadrant) % 4;\n\n            Item_SwitchToAnim(lara_item, step_to_anim_num[quadrant], 0);\n            lara_item->goal_anim_state = step_to_anim_state[quadrant];\n            lara_item->current_anim_state = step_to_anim_state[quadrant];\n\n            lara_info->gun_status = LGS_HANDS_BUSY;\n        }\n\n        lara_info->interact_target.is_moving = lara_on_land;\n        lara_info->interact_target.move_count = 0;\n    }\n\n    const int16_t rotation = M_MOVE_ANGLE;\n    ITEM_ADJUST_ROT(lara_item->rot.x, new_rot.x, rotation);\n    ITEM_ADJUST_ROT(lara_item->rot.y, new_rot.y, rotation);\n    ITEM_ADJUST_ROT(lara_item->rot.z, new_rot.z, rotation);\n\n    return XYZ_32_AreEquivalent(lara_item->pos, new_pos)\n        && XYZ_16_AreEquivalent(lara_item->rot, new_rot);\n}\n\nLARA_ANIMATION Lara_AnimToGameID(const LARA_TRX_ANIMATION anim)\n{\n    int32_t out;\n    if (!Catalog_EnumToGameID(CATALOG_LARA_ANIMS, anim, &out)) {\n        out = -1;\n    }\n    return out;\n}\n\nLARA_STATE Lara_StateToGameID(const LARA_TRX_STATE state)\n{\n    int32_t out;\n    if (!Catalog_EnumToGameID(CATALOG_LARA_STATES, state, &out)) {\n        out = -1;\n    }\n    return out;\n}\n\nLARA_TRX_ANIMATION Lara_AnimFromGameID(const LARA_ANIMATION anim)\n{\n    int32_t out;\n    if (!Catalog_GameIDToEnum(CATALOG_LARA_ANIMS, anim, &out)) {\n        out = -1;\n    }\n    return out;\n}\n\nLARA_TRX_STATE Lara_StateFromGameID(const LARA_STATE state)\n{\n    int32_t out;\n    if (!Catalog_GameIDToEnum(CATALOG_LARA_STATES, state, &out)) {\n        out = -1;\n    }\n    return out;\n}\n"
  },
  {
    "path": "src/trx/game/lara/common.h",
    "content": "#pragma once\n\n#include <trx/game/collision.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/lara/types.h>\n\n#define LA(anim) Lara_AnimToGameID(anim)\n#define LA_U(anim) Lara_AnimFromGameID(anim)\n#define LS(state) Lara_StateToGameID(state)\n#define LS_U(state) Lara_StateFromGameID(state)\n\nLARA_INFO *Lara_GetLaraInfo(void);\nITEM *Lara_GetItem(void);\nvoid Lara_Initialise(const GF_LEVEL *level);\nvoid Lara_InitialiseLoad(int16_t item_num);\nvoid Lara_InitialiseInventory(const GF_LEVEL *level);\nvoid Lara_RevertToPistolsIfNeeded(void);\nvoid Lara_UseItem(OBJECT_ID obj_id);\nvoid Lara_SetStartAnimState(LARA_EXTRA_STATE state);\nbool Lara_IsControllable(void);\nvoid Lara_SetControllable(bool controllable);\nbool Lara_CanInterpolate(const ITEM *item, int32_t frame_a, int32_t frame_b);\nITEM *Lara_GetDeathCameraTarget(void);\nvoid Lara_SetDeathCameraTarget(int16_t item_num);\nOBJECT_ID Lara_GetAnimationObject(void);\nvoid Lara_Animate(ITEM *item);\nvoid Lara_AnimateUntil(ITEM *lara_item, int32_t goal);\nconst ANIM_FRAME *Lara_GetHitFrame(const ITEM *item);\nvoid Lara_TakeDamage(int16_t damage, bool hit_status);\n\nbool Lara_GetMeshPos(LARA_MESH mesh, XYZ_32 *out_pos);\nbool Lara_TestBoundsCollide(const ITEM *item, int32_t radius);\nbool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *bounds);\nvoid Lara_AlignPosition(const ITEM *item, const XYZ_32 *vec);\nbool Lara_MovePosition(const ITEM *item, const XYZ_32 *vec);\nbool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance);\n\nLARA_ANIMATION Lara_AnimToGameID(LARA_TRX_ANIMATION anim);\nLARA_STATE Lara_StateToGameID(LARA_TRX_STATE state);\nLARA_TRX_ANIMATION Lara_AnimFromGameID(LARA_ANIMATION anim);\nLARA_TRX_STATE Lara_StateFromGameID(LARA_STATE state);\n"
  },
  {
    "path": "src/trx/game/lara/const.h",
    "content": "#pragma once\n\n#include <trx/game/const.h>\n#include <trx/game/rooms/const.h>\n\n#define LARA_ORIGINAL_ANIM_COUNT (g_TRVersion == 1 ? 160 : 218)\n\n#define LARA_MAX_HITPOINTS 1000\n#define LARA_MAX_AIR 1800\n#define LARA_DIVE_WAIT 10\n#define LARA_MAX_SPRINT (4 * LOGIC_FPS)\n#define LARA_MAX_EXPOSURE (20 * LOGIC_FPS)\n\n#define LARA_HEIGHT 762\n#define LARA_HEIGHT_UW 400\n#define LARA_HEIGHT_CROUCH 400\n#define LARA_RADIUS 100\n#define LARA_SWIM_DEPTH 730\n\n#define LARA_TURN_UNDO (2 * DEG_1) // = 364\n#define LARA_TURN_RATE ((DEG_1 / 4) + LARA_TURN_UNDO) // = 409\n#define LARA_SLOW_TURN ((DEG_1 * 2) + LARA_TURN_UNDO) // = 728\n#define LARA_MED_TURN ((DEG_1 * 4) + LARA_TURN_UNDO) // = 1092\n\n#define LARA_LEAN_UNDO DEG_1 // = 182\n#define LARA_LEAN_RATE 273\n#define LARA_LEAN_MAX ((10 * DEG_1) + LARA_LEAN_UNDO) // = 2002\n\n#define LARA_UW_WALL_DEFLECT (2 * DEG_1) // = 364\n#define LARA_DEFLECT_ANGLE (5 * DEG_1) // = 910\n#define LARA_HANG_ANGLE (35 * DEG_1) // = 6370\n\n#define NO_BAD_POS (-NO_HEIGHT) // = 32512\n#define NO_BAD_NEG (NO_HEIGHT) // = -32512\n#define STEPUP_HEIGHT ((STEP_L * 3) / 2) // = 384\n#define SLOPE_DIF 60\n\n#define DAMAGE_START 140\n#define DAMAGE_LENGTH 14\n\n// TODO: move to merged game.c\n#define DEATH_WAIT (5 * 2 * LOGIC_FPS) // = 300\n#define DEATH_WAIT_INPUT (2 * LOGIC_FPS) // = 60\n\n#define CAM_WADE_ELEVATION (-22 * DEG_1) // = -4004\n"
  },
  {
    "path": "src/trx/game/lara/control.c",
    "content": "#include <trx/game/lara/control.h>\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/gym.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/breath.h>\n#include <trx/game/lara/electric.h>\n#include <trx/game/level/settings.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_MAX_COLL_ROOMS    20\n#define M_ITEM_COLL_DIST    CREATURE_TARGET_DIST // = 4096\n#define M_STATIC_COLL_DIST  (WALL_L * 3)         // = 3072\n#define M_MOVE_TIMEOUT      90\n#define M_UW_DAMAGE         5\n#define M_SWAMP_DAMAGE      10\n#define M_DIVE_TILT_MED     (45 * DEG_1)         // = 8190\n#define M_DIVE_TILT_MAX     (85 * DEG_1)         // = 15470\n#define M_DIVE_TILT_MAX_ALT (100 * DEG_1)        // = 18200\n#define M_RADIUS_SURF       LARA_RADIUS          // = 100\n#define M_RADIUS_UW         300\n#define M_WADE_DEPTH        256\n#define M_LEAN_UNDO_SURF    (LARA_LEAN_UNDO * 2) // = 364\n#define M_LEAN_UNDO_UW      M_LEAN_UNDO_SURF     // = 364\n#define M_LEAN_MAX_UW       (LARA_LEAN_MAX * 2)  // = 4004\n// clang-format on\n\nstatic int32_t m_OpenDoorsCheatCooldown = 0;\n\nextern bool Skidoo_Control(void);\nextern bool UPV_Control(void);\nextern bool QuadBike_Control(void);\nextern bool Kayak_Control(void);\nextern bool MountedGun_Control(void);\nextern bool MineCart_Control(void);\n\nstatic SECTOR *M_GetCurrentSector(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    int16_t room_num = lara_item->room_num;\n    return Room_GetSector(\n        (XYZ_32) { lara_item->pos.x, MAX_HEIGHT, lara_item->pos.z }, &room_num);\n}\n\nstatic void M_Cheat(void)\n{\n    if (!g_Config.gameplay.enable_cheats) {\n        return;\n    }\n\n    if (g_InputDB.level_skip_cheat) {\n        Lara_Cheat_EndLevel();\n    }\n\n    if (g_InputDB.item_cheat) {\n        Lara_Cheat_GiveAllItems();\n    }\n\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->water_status != LWS_CHEAT && g_InputDB.fly_cheat) {\n        Lara_Cheat_EnterFlyMode();\n    }\n}\n\nstatic void M_WaterCurrent_TR12(COLL_INFO *const coll)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    int16_t room_num = lara_item->room_num;\n    const ROOM *const room = Room_Get(lara_item->room_num);\n    lara_item->box_num =\n        Room_GetWorldSector(room, lara_item->pos.x, lara_item->pos.z)->box;\n\n    XYZ_32 target;\n    if (Box_CalculateTarget(&target, lara_item, &lara->lot) == TARGET_NONE) {\n        return;\n    }\n\n#define L_SHIFT(_axis)                                                         \\\n    do {                                                                       \\\n        target._axis -= lara_item->pos._axis;                                  \\\n        if (target._axis > lara->current.active) {                             \\\n            lara_item->pos._axis += lara->current.active;                      \\\n        } else if (target._axis < -lara->current.active) {                     \\\n            lara_item->pos._axis -= lara->current.active;                      \\\n        } else {                                                               \\\n            lara_item->pos._axis += target._axis;                              \\\n        }                                                                      \\\n    } while (0)\n\n    L_SHIFT(x);\n    L_SHIFT(y);\n    L_SHIFT(z);\n#undef L_SHIFT\n\n    lara->current.active = 0;\n    coll->facing = Math_Atan(\n        lara_item->pos.z - coll->old.z, lara_item->pos.x - coll->old.x);\n    Collide_GetCollisionInfo(\n        coll, lara_item->pos.x, lara_item->pos.y + LARA_HEIGHT_UW / 2,\n        lara_item->pos.z, room_num, LARA_HEIGHT_UW);\n\n    switch (coll->coll_type) {\n    case COLL_FRONT:\n        if (lara_item->rot.x > 35 * DEG_1) {\n            lara_item->rot.x += LARA_UW_WALL_DEFLECT;\n        } else if (lara_item->rot.x < -35 * DEG_1) {\n            lara_item->rot.x -= LARA_UW_WALL_DEFLECT;\n        } else {\n            lara_item->fall_speed = 0;\n        }\n        break;\n\n    case COLL_TOP:\n        lara_item->rot.x -= LARA_UW_WALL_DEFLECT;\n        break;\n\n    case COLL_TOP_FRONT:\n        lara_item->fall_speed = 0;\n        break;\n\n    case COLL_LEFT:\n        lara_item->rot.y += 5 * DEG_1;\n        break;\n\n    case COLL_RIGHT:\n        lara_item->rot.y -= 5 * DEG_1;\n        break;\n\n    default:\n        break;\n    }\n\n    if (coll->side_mid.floor < 0) {\n        lara_item->pos.y += coll->side_mid.floor;\n        lara_item->rot.x += LARA_UW_WALL_DEFLECT;\n    }\n    Lara_Col_Shift(coll);\n\n    coll->old = lara_item->pos;\n}\n\nstatic void M_WaterCurrent_TR3(COLL_INFO *const coll)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (lara->current.active != 0) {\n        const OBJECT_VECTOR *const sink =\n            Camera_GetFixedObject(lara->current.active - 1);\n        const int32_t speed = sink->data;\n        const int32_t angle =\n            -Math_Atan(lara_item->pos.x - sink->x, lara_item->pos.z - sink->z)\n            - DEG_90;\n        lara->current.vel.x +=\n            (((speed * Math_Sin(angle)) >> 4) - lara->current.vel.x) >> 4;\n        lara->current.vel.z +=\n            (((speed * Math_Cos(angle)) >> 4) - lara->current.vel.z) >> 4;\n        lara_item->pos.y += (sink->y - lara_item->pos.y) >> 4;\n    } else {\n        int32_t shifter;\n        int32_t abs_vel;\n\n        abs_vel = ABS(lara->current.vel.x);\n        if (abs_vel > 16) {\n            shifter = 4;\n        } else if (abs_vel > 8) {\n            shifter = 3;\n        } else {\n            shifter = 2;\n        }\n\n        lara->current.vel.x -= lara->current.vel.x >> shifter;\n        if (ABS(lara->current.vel.x) < 4) {\n            lara->current.vel.x = 0;\n        }\n\n        abs_vel = ABS(lara->current.vel.z);\n        if (abs_vel > 16) {\n            shifter = 4;\n        } else if (abs_vel > 8) {\n            shifter = 3;\n        } else {\n            shifter = 2;\n        }\n\n        lara->current.vel.z -= lara->current.vel.z >> shifter;\n        if (ABS(lara->current.vel.z) < 4) {\n            lara->current.vel.z = 0;\n        }\n\n        if (!lara->current.vel.x && !lara->current.vel.z) {\n            return;\n        }\n    }\n\n    lara_item->pos.x += lara->current.vel.x >> 8;\n    lara_item->pos.z += lara->current.vel.z >> 8;\n    lara->current.active = 0;\n    coll->facing = Math_Atan(\n        lara_item->pos.z - coll->old.z, lara_item->pos.x - coll->old.x);\n    Collide_GetCollisionInfo(\n        coll, lara_item->pos.x, lara_item->pos.y + 200, lara_item->pos.z,\n        lara_item->room_num, 400);\n\n    switch (coll->coll_type) {\n    case COLL_FRONT:\n        if (lara_item->rot.x > 35 * DEG_1) {\n            lara_item->rot.x += 2 * DEG_1;\n        } else if (lara_item->rot.x < -35 * DEG_1) {\n            lara_item->rot.x -= 2 * DEG_1;\n        } else {\n            lara_item->fall_speed = 0;\n        }\n        break;\n\n    case COLL_TOP:\n        lara_item->rot.x -= 2 * DEG_1;\n        break;\n\n    case COLL_TOP_FRONT:\n        lara_item->fall_speed = 0;\n        break;\n\n    case COLL_LEFT:\n        lara_item->rot.y += 5 * DEG_1;\n        break;\n\n    case COLL_RIGHT:\n        lara_item->rot.y -= 5 * DEG_1;\n        break;\n    }\n\n    if (coll->side_mid.floor < 0) {\n        lara_item->pos.y += coll->side_mid.floor;\n    }\n\n    Lara_Col_Shift(coll);\n    coll->old = lara_item->pos;\n}\n\nstatic void M_WaterCurrent(COLL_INFO *const coll)\n{\n    if (g_TRVersion < 3) {\n        M_WaterCurrent_TR12(coll);\n    } else {\n        M_WaterCurrent_TR3(coll);\n    }\n}\n\nstatic void M_SoftStaticCollision(COLL_INFO *const coll)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    Room_GetNearbyRooms(\n        lara_item->pos, coll->radius + 50, LARA_HEIGHT + 50,\n        lara_item->room_num);\n\n    for (int32_t i = 0; i < Room_DrawGetCount(); i++) {\n        const ROOM *const room = Room_Get(Room_DrawGetRoom(i));\n        for (int32_t j = 0; j < room->num_static_meshes; j++) {\n            const STATIC_MESH *const mesh = &room->static_meshes[j];\n            const STATIC_OBJECT_3D *const obj =\n                Object_Get3DStatic(mesh->static_num);\n            if (!obj->collidable\n                || !XYZ_32_IsNearby(\n                    lara_item->pos, mesh->pos, M_STATIC_COLL_DIST)) {\n                continue;\n            }\n\n            if (Item_TestStatic3DBoundsCollide(mesh, lara_item, coll->radius)) {\n                Lara_Col_Static3DPush(mesh, coll);\n            }\n        }\n    }\n}\n\nstatic void M_ObjectCollision(COLL_INFO *const coll)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->hit_direction = DIR_UNKNOWN;\n    lara_item->hit_status = false;\n    if (lara_item->hit_points <= 0) {\n        return;\n    }\n\n    int16_t nearby_rooms[M_MAX_COLL_ROOMS];\n    const int32_t room_count = Room_GetAdjoiningRooms(\n        lara_item->room_num, nearby_rooms, M_MAX_COLL_ROOMS);\n\n    for (int32_t i = 0; i < room_count; i++) {\n        const ROOM *const room = Room_Get(nearby_rooms[i]);\n        int16_t item_num = room->item_num;\n        while (item_num != NO_ITEM) {\n            const ITEM *const item = Item_Get(item_num);\n            // The collision routine can destroy the item - need to store the\n            // next item beforehand.\n            const int16_t next_item_num = item->next_item;\n\n            if (lara_info->water_status == LWS_CHEAT\n                && !Object_IsType(item->object_id, g_PickupObjects)\n                && !Object_IsType(item->object_id, g_SwitchObjects)) {\n                goto loop_end;\n            }\n            if (!item->collidable || item->status == IS_INVISIBLE) {\n                goto loop_end;\n            }\n\n            const OBJECT *const obj = Object_Get(item->object_id);\n            if (obj->collision_func == nullptr\n                || !Item_IsNearby(lara_item, item, M_ITEM_COLL_DIST)) {\n                goto loop_end;\n            }\n\n            obj->collision_func(item_num, lara_item, coll);\n\n        loop_end:\n            item_num = next_item_num;\n        }\n    }\n\n    if (g_Config.gameplay.enable_soft_statics) {\n        M_SoftStaticCollision(coll);\n    }\n\n    if (lara_info->hit_effect_count != 0 && lara_info->hit_effect != nullptr\n        && coll->enable_hit) {\n        const int32_t dx = lara_info->hit_effect->pos.x - lara_item->pos.x;\n        const int32_t dz = lara_info->hit_effect->pos.z - lara_item->pos.z;\n        Lara_TakeHit(lara_item, dx, dz);\n        lara_info->hit_effect_count--;\n    }\n\n    if (lara_info->hit_direction < 0) {\n        lara_info->hit_frame = 0;\n    }\n}\n\nstatic void M_UpdateEnvironment(void)\n{\n    ITEM *const item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->extra_anim) {\n        return;\n    }\n\n    if (Lara_Vehicle_IsMounted()) {\n        return;\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    const int32_t water_depth = Lara_GetWaterDepth(\n        item->pos.x, item->pos.y, item->pos.z, item->room_num);\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    const int32_t water_height_diff =\n        water_height == NO_HEIGHT ? NO_HEIGHT : item->pos.y - water_height;\n    lara_info->water_surface_dist = -water_height_diff;\n\n    if (g_TRVersion == 3) {\n        FX_Water_WadeSplash(item, water_height, water_depth);\n    } else if (\n        g_Config.gameplay.enable_wading\n        && lara_info->water_status != LWS_CHEAT) {\n        // Create splash if Lara lands in wading height water. TR3+ feature.\n        const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds;\n        if (bounds != nullptr && item->pos.y + bounds->min.y <= water_height\n            && item->pos.y + bounds->max.y >= water_height\n            && item->fall_speed > 0 && water_depth < LARA_SWIM_DEPTH - STEP_L) {\n            Spawn_Splash(item);\n        }\n    }\n\n    switch (lara_info->water_status) {\n    case LWS_ABOVE_WATER: {\n        if (g_Config.gameplay.enable_wading\n            && (water_height_diff == NO_HEIGHT\n                || water_height_diff < M_WADE_DEPTH)) {\n            break;\n        }\n\n        if (water_depth > LARA_SWIM_DEPTH - STEP_L && !room->flags.swamp) {\n            if (room->flags.underwater) {\n                lara_info->air = LARA_MAX_AIR;\n                lara_info->water_status = LWS_UNDERWATER;\n                item->gravity = false;\n                item->pos.y += 100;\n                Lara_UpdateRoomToHeight(0);\n                Sound_StopEffect(SFX_LARA_FALL);\n                if (item->current_anim_state == LS(LS_SWAN_DIVE)) {\n                    item->rot.x = -M_DIVE_TILT_MED;\n                    item->goal_anim_state = LS(LS_DIVE);\n                    Lara_Animate(item);\n                    item->fall_speed *= 2;\n                } else if (item->current_anim_state == LS(LS_FAST_DIVE)) {\n                    item->rot.x = -M_DIVE_TILT_MAX;\n                    item->goal_anim_state = LS(LS_DIVE);\n                    Lara_Animate(item);\n                    item->fall_speed *= 2;\n                } else {\n                    item->rot.x = -M_DIVE_TILT_MED;\n                    Item_SwitchToAnim(item, LA(LA_FREEFALL_TO_UNDERWATER), 0);\n                    item->current_anim_state = LS(LS_DIVE);\n                    item->goal_anim_state = LS(LS_SWIM);\n                    item->fall_speed = (item->fall_speed * 3) / 2;\n                }\n                lara_info->head_rot.x = 0;\n                lara_info->head_rot.y = 0;\n                lara_info->torso_rot.x = 0;\n                lara_info->torso_rot.y = 0;\n                Spawn_Splash(item);\n            }\n        } else if (\n            g_Config.gameplay.enable_wading\n            && water_height_diff > M_WADE_DEPTH) {\n            lara_info->water_status = LWS_WADE;\n            if (!item->gravity) {\n                item->goal_anim_state = LS(LS_STOP);\n            } else if (room->flags.swamp) {\n                if (item->current_anim_state == LS(LS_SWAN_DIVE)\n                    || item->current_anim_state == LS(LS_FAST_DIVE)) {\n                    item->pos.y = water_height + 1000;\n                }\n                Item_SwitchToAnim(item, LA(LA_WADE), 0);\n                item->current_anim_state = LS(LS_WADE);\n                item->goal_anim_state = LS(LS_WADE);\n            }\n        }\n\n        break;\n    }\n\n    case LWS_UNDERWATER: {\n        if (room->flags.underwater) {\n            break;\n        }\n\n        if (water_depth == NO_HEIGHT || ABS(water_height_diff) >= STEP_L) {\n            lara_info->water_status = LWS_ABOVE_WATER;\n            Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n            item->current_anim_state = LS(LS_JUMP_FORWARD);\n            item->goal_anim_state = LS(LS_JUMP_FORWARD);\n            item->gravity = true;\n            item->speed = item->fall_speed / 4;\n            item->fall_speed = 0;\n            item->rot.x = 0;\n            item->rot.z = 0;\n            lara_info->head_rot.x = 0;\n            lara_info->head_rot.y = 0;\n            lara_info->torso_rot.x = 0;\n            lara_info->torso_rot.y = 0;\n            if (g_TRVersion == 1) {\n                lara_info->gun_status = LGS_ARMLESS;\n            }\n        } else {\n            lara_info->water_status = LWS_SURFACE;\n            Item_SwitchToAnim(item, LA(LA_UNDERWATER_TO_ONWATER), 0);\n            item->current_anim_state = LS(LS_SURF_TREAD);\n            item->goal_anim_state = LS(LS_SURF_TREAD);\n            item->fall_speed = 0;\n            item->pos.y += 1 - water_height_diff;\n            item->rot.x = 0;\n            item->rot.z = 0;\n            lara_info->dive_timer = LARA_DIVE_WAIT + 1;\n            lara_info->head_rot.x = 0;\n            lara_info->head_rot.y = 0;\n            lara_info->torso_rot.x = 0;\n            lara_info->torso_rot.y = 0;\n            Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2);\n            Sound_Effect(SFX_LARA_BREATH, &item->pos, SPM_ALWAYS);\n        }\n        break;\n    }\n\n    case LWS_SURFACE: {\n        if (room->flags.underwater) {\n            break;\n        }\n\n        if (g_Config.gameplay.enable_wading\n            && water_height_diff > M_WADE_DEPTH) {\n            lara_info->water_status = LWS_WADE;\n            Item_SwitchToAnim(item, LA(LA_STAND_IDLE), 0);\n            item->current_anim_state = LS(LS_STOP);\n            item->goal_anim_state = LS(LS_WADE);\n            Item_Animate(item);\n            item->fall_speed = 0;\n        } else {\n            lara_info->water_status = LWS_ABOVE_WATER;\n            Item_SwitchToAnim(item, LA(LA_FALL_START), 0);\n            item->current_anim_state = LS(LS_JUMP_FORWARD);\n            item->goal_anim_state = LS(LS_JUMP_FORWARD);\n            item->gravity = true;\n            item->speed = item->fall_speed / 4;\n            if (g_TRVersion == 1) {\n                lara_info->gun_status = LGS_ARMLESS;\n            }\n        }\n        item->rot.x = 0;\n        item->rot.z = 0;\n        lara_info->head_rot.x = 0;\n        lara_info->head_rot.y = 0;\n        lara_info->torso_rot.x = 0;\n        lara_info->torso_rot.y = 0;\n        break;\n    }\n\n    case LWS_WADE: {\n        g_Camera.target_elevation = CAM_WADE_ELEVATION;\n\n        if (water_height_diff <= M_WADE_DEPTH) {\n            lara_info->water_status = LWS_ABOVE_WATER;\n            if (item->current_anim_state == LS(LS_WADE)) {\n                item->goal_anim_state = LS(LS_RUN);\n            }\n        } else if (water_height_diff > LARA_SWIM_DEPTH && !room->flags.swamp) {\n            lara_info->water_status = LWS_SURFACE;\n            item->pos.y += 1 - water_height_diff;\n\n            LARA_ANIMATION anim_idx;\n            switch (LS_U(item->current_anim_state)) {\n            case LS_WALK_BACK:\n                item->goal_anim_state = LS(LS_SURF_BACK);\n                anim_idx = LA(LA_ONWATER_IDLE_TO_SWIM_BACK);\n                break;\n\n            case LS_STEP_RIGHT:\n                item->goal_anim_state = LS(LS_SURF_RIGHT);\n                anim_idx = LA(LA_ONWATER_SWIM_RIGHT);\n                break;\n\n            case LS_STEP_LEFT:\n                item->goal_anim_state = LS(LS_SURF_LEFT);\n                anim_idx = LA(LA_ONWATER_SWIM_LEFT);\n                break;\n\n            default:\n                item->goal_anim_state = LS(LS_SURF_SWIM);\n                anim_idx = LA(LA_ONWATER_SWIM_FORWARD);\n                break;\n            }\n\n            item->current_anim_state = item->goal_anim_state;\n            Item_SwitchToAnim(item, anim_idx, 0);\n\n            item->rot.z = 0;\n            item->rot.x = 0;\n            item->gravity = false;\n            item->fall_speed = 0;\n            lara_info->dive_timer = 0;\n            lara_info->torso_rot.y = 0;\n            lara_info->torso_rot.x = 0;\n            lara_info->head_rot.y = 0;\n            lara_info->head_rot.x = 0;\n            Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2);\n        }\n        break;\n    }\n\n    default:\n        break;\n    }\n}\n\nstatic void M_UndoRot(int16_t *const rot, const int16_t rate)\n{\n    if (*rot < -rate) {\n        *rot += rate;\n    } else if (*rot > rate) {\n        *rot -= rate;\n    } else {\n        *rot = 0;\n    }\n}\n\nstatic void M_HandleAboveWater(COLL_INFO *const coll)\n{\n    ITEM *const item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    coll->old = item->pos;\n    coll->old_anim_state = item->current_anim_state;\n    coll->old_anim_num = item->anim_num;\n    coll->old_frame_num = item->frame_num;\n    coll->radius = LARA_RADIUS;\n\n    coll->lava_is_pit = 0;\n    coll->slopes_are_walls = 0;\n    coll->slopes_are_pits = 0;\n    coll->enable_hit = 1;\n    coll->enable_baddie_push = 1;\n\n    Lara_Look_Update();\n\n    const ITEM *const vehicle = Lara_Vehicle_GetItem();\n    if (vehicle != nullptr) {\n        // TODO: make this Object_Get(…)->control\n        switch (vehicle->object_id) {\n        case O_SKIDOO_FAST:\n            if (Skidoo_Control()) {\n                return;\n            }\n            break;\n\n        case O_QUAD_BIKE:\n            if (QuadBike_Control()) {\n                return;\n            }\n            break;\n\n        case O_KAYAK:\n            if (Kayak_Control()) {\n                return;\n            }\n            break;\n\n        case O_UPV:\n            if (UPV_Control()) {\n                return;\n            }\n            break;\n\n        case O_MOUNTED_GUN:\n            if (MountedGun_Control()) {\n                coll->enable_hit = false;\n                coll->enable_baddie_push = false;\n                M_ObjectCollision(coll);\n                return;\n            }\n            break;\n\n        case O_MINE_CART:\n            if (MineCart_Control()) {\n                return;\n            }\n            break;\n\n        default:\n            Gun_Control();\n            return;\n        }\n\n        if (!Lara_Vehicle_IsMounted()\n            && (lara_info->water_status == LWS_UNDERWATER\n                || lara_info->water_status == LWS_SURFACE)) {\n            // When dismounting an underwater vehicle, do not continue\n            // with above-surface control, and instead run relevant\n            // underwater or surface routines\n            return;\n        }\n    }\n\n    lara_info->is_crouched = false;\n    Lara_State_Update(item, coll);\n\n    M_UndoRot(&item->rot.x, LARA_LEAN_UNDO);\n    M_UndoRot(&item->rot.z, LARA_LEAN_UNDO);\n    M_UndoRot(&lara_info->turn_rate, LARA_TURN_UNDO);\n    item->rot.y += lara_info->turn_rate;\n\n    Lara_Animate(item);\n    const SECTOR *const sector = M_GetCurrentSector();\n\n    if (!lara_info->extra_anim && lara_info->water_status != LWS_CHEAT) {\n        M_ObjectCollision(coll);\n        if (!Lara_Vehicle_IsMounted() && !lara_info->extra_anim) {\n            Lara_Col_Update(item, coll);\n        }\n    }\n\n    Lara_UpdateRoomToHeight(-LARA_HEIGHT / 2);\n    Gun_Control();\n    Room_TestSectorTrigger(item, sector);\n}\n\nstatic void M_HandleUnderwater(COLL_INFO *const coll)\n{\n    ITEM *const item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    coll->old = item->pos;\n    coll->radius = M_RADIUS_UW;\n\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = -LARA_HEIGHT_UW;\n    coll->bad_ceiling = LARA_HEIGHT_UW;\n\n    coll->slopes_are_walls = 0;\n    coll->slopes_are_pits = 0;\n    coll->lava_is_pit = 0;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n\n    Lara_Look_Update();\n    Lara_State_Update(item, coll);\n\n    if (item->rot.z > M_LEAN_UNDO_UW) {\n        item->rot.z -= M_LEAN_UNDO_UW;\n    } else if (item->rot.z < -M_LEAN_UNDO_UW) {\n        item->rot.z += M_LEAN_UNDO_UW;\n    } else {\n        item->rot.z = 0;\n    }\n\n    if (g_Config.gameplay.enable_tr2_swimming) {\n        CLAMP(item->rot.x, -M_DIVE_TILT_MAX, M_DIVE_TILT_MAX);\n        CLAMP(item->rot.z, -M_LEAN_MAX_UW, M_LEAN_MAX_UW);\n\n        if (lara_info->turn_rate < -LARA_TURN_UNDO) {\n            lara_info->turn_rate += LARA_TURN_UNDO;\n        } else if (lara_info->turn_rate > LARA_TURN_UNDO) {\n            lara_info->turn_rate -= LARA_TURN_UNDO;\n        } else {\n            lara_info->turn_rate = 0;\n        }\n        item->rot.y += lara_info->turn_rate;\n    } else {\n        CLAMP(item->rot.x, -M_DIVE_TILT_MAX_ALT, M_DIVE_TILT_MAX_ALT);\n        CLAMP(item->rot.z, -M_LEAN_MAX_UW, M_LEAN_MAX_UW);\n    }\n\n    if (lara_info->current.active && lara_info->water_status != LWS_CHEAT) {\n        M_WaterCurrent(coll);\n    } else {\n        LOT_ClearLOT(&lara_info->lot);\n    }\n\n    Lara_Animate(item);\n    item->pos.y -=\n        (item->fall_speed * Math_Sin(item->rot.x)) >> (W2V_SHIFT + 2);\n    item->pos.x +=\n        (Math_Cos(item->rot.x)\n         * ((item->fall_speed * Math_Sin(item->rot.y)) >> (W2V_SHIFT + 2)))\n        >> W2V_SHIFT;\n    item->pos.z +=\n        (Math_Cos(item->rot.x)\n         * ((item->fall_speed * Math_Cos(item->rot.y)) >> (W2V_SHIFT + 2)))\n        >> W2V_SHIFT;\n\n    const SECTOR *const sector = M_GetCurrentSector();\n    if (!lara_info->extra_anim) {\n        M_ObjectCollision(coll);\n    }\n\n    if (lara_info->water_status == LWS_CHEAT) {\n        if (m_OpenDoorsCheatCooldown > 0) {\n            m_OpenDoorsCheatCooldown--;\n        } else if (g_Input.draw) {\n            m_OpenDoorsCheatCooldown = LOGIC_FPS;\n            Lara_Cheat_OpenNearestDoor();\n        }\n    }\n\n    if (!Lara_Vehicle_IsMounted() && !lara_info->extra_anim) {\n        Lara_Col_Update(item, coll);\n    }\n\n    Lara_UpdateRoomToHeight(0);\n    Gun_Control();\n    Room_TestSectorTrigger(item, sector);\n}\n\nstatic void M_HandleSurface(COLL_INFO *const coll)\n{\n    ITEM *const item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    g_Camera.target_elevation = CAM_WADE_ELEVATION;\n\n    coll->old = item->pos;\n    coll->radius = M_RADIUS_SURF;\n\n    coll->bad_pos = NO_BAD_POS;\n    coll->bad_neg = g_TRVersion == 1 ? -100 : -STEP_L / 2;\n    coll->bad_ceiling = 100;\n\n    coll->slopes_are_walls = 0;\n    coll->slopes_are_pits = 0;\n    coll->lava_is_pit = 0;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n\n    Lara_Look_Update();\n    Lara_State_Update(item, coll);\n\n    if (item->rot.z > M_LEAN_UNDO_SURF) {\n        item->rot.z -= M_LEAN_UNDO_SURF;\n    } else if (item->rot.z < -M_LEAN_UNDO_SURF) {\n        item->rot.z += M_LEAN_UNDO_SURF;\n    } else {\n        item->rot.z = 0;\n    }\n\n    if (lara_info->current.active && lara_info->water_status != LWS_CHEAT) {\n        M_WaterCurrent(coll);\n    } else {\n        LOT_ClearLOT(&lara_info->lot);\n    }\n\n    Lara_Animate(item);\n    item->pos.x +=\n        (item->fall_speed * Math_Sin(lara_info->move_angle)) >> (W2V_SHIFT + 2);\n    item->pos.z +=\n        (item->fall_speed * Math_Cos(lara_info->move_angle)) >> (W2V_SHIFT + 2);\n\n    const SECTOR *const sector = M_GetCurrentSector();\n\n    M_ObjectCollision(coll);\n    if (!Lara_Vehicle_IsMounted() && !lara_info->extra_anim) {\n        Lara_Col_Update(item, coll);\n    }\n\n    Lara_UpdateRoomToHeight(100);\n    Gun_Control();\n    Room_TestSectorTrigger(item, sector);\n}\n\nstatic void M_HandleExposure(void)\n{\n    if (!Level_HasColdWater()) {\n        return;\n    }\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    switch (lara_info->water_status) {\n    case LWS_ABOVE_WATER:\n        lara_info->exposure_timer++;\n        CLAMPG(lara_info->exposure_timer, LARA_MAX_EXPOSURE);\n        break;\n    case LWS_WADE:\n        lara_info->exposure_timer--;\n        break;\n    case LWS_UNDERWATER:\n    case LWS_SURFACE:\n        lara_info->exposure_timer -= 2;\n        break;\n    case LWS_CHEAT:\n        lara_info->exposure_timer = LARA_MAX_EXPOSURE;\n        break;\n    }\n\n    if (lara_info->exposure_timer < 0) {\n        lara_info->exposure_timer = -1;\n        Lara_TakeDamage(10, false);\n    }\n}\n\nstatic void M_HandleEnvironment(void)\n{\n    ITEM *const item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    COLL_INFO coll = {};\n\n    if (item->current_anim_state != LS(LS_SPRINT)) {\n        lara_info->sprint_timer++;\n        CLAMPG(lara_info->sprint_timer, LARA_MAX_SPRINT);\n    }\n    if (item->current_anim_state != LS(LS_STOP)\n        && item->current_anim_state != LS(LS_POSE)) {\n        lara_info->idle_timer = 0;\n    }\n\n    switch (lara_info->water_status) {\n    case LWS_ABOVE_WATER:\n    case LWS_WADE: {\n        const ROOM *const room = Room_Get(item->room_num);\n        if (room->flags.swamp && lara_info->water_surface_dist < -775) {\n            if (item->hit_points >= 0) {\n                lara_info->air -= 6;\n                if (lara_info->air < 0) {\n                    lara_info->air = -1;\n                    Lara_TakeDamage(M_SWAMP_DAMAGE, false);\n                }\n            }\n        } else if (!Lara_Vehicle_IsOnType(O_UPV) && item->hit_points >= 0) {\n            // TODO: make option for air replenish mode\n            lara_info->air += g_TRVersion >= 3 ? 10 : LARA_MAX_AIR;\n            CLAMPG(lara_info->air, LARA_MAX_AIR);\n        }\n        M_HandleAboveWater(&coll);\n        break;\n    }\n\n    case LWS_UNDERWATER:\n        if (item->hit_points >= 0) {\n            lara_info->air--;\n            if (lara_info->air < 0) {\n                lara_info->air = -1;\n                Lara_TakeDamage(M_UW_DAMAGE, false);\n            }\n        }\n        M_HandleUnderwater(&coll);\n        break;\n\n    case LWS_SURFACE:\n        if (item->hit_points >= 0) {\n            lara_info->air += 10;\n            CLAMPG(lara_info->air, LARA_MAX_AIR);\n        }\n        M_HandleSurface(&coll);\n        break;\n\n    case LWS_CHEAT:\n        item->hit_points = LARA_MAX_HITPOINTS;\n        lara_info->poison_timer = 0;\n        lara_info->death_timer = 0;\n        M_HandleUnderwater(&coll);\n        if (g_InputDB.slow && !g_Input.look && !g_Input.fly_cheat) {\n            Lara_Cheat_ExitFlyMode();\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    M_HandleExposure();\n}\n\nstatic void M_HandleStartState(const LARA_EXTRA_STATE start_state)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const XYZ_16 old_rot = lara_item->rot;\n\n    Lara_SwitchToExtraState(start_state);\n    if (g_Config.gameplay.enable_cinematics) {\n        Camera_InvokeCinematic(lara_item, 0, 0);\n        return;\n    }\n\n    // Skip the starting cinematic, but force animation control to play out to\n    // honour extra state specifics.\n    COLL_INFO coll = {};\n    do {\n        Lara_State_Update(lara_item, &coll);\n        Lara_Animate(lara_item);\n    } while (lara_info->extra_anim);\n    lara_item->rot = old_rot;\n}\n\nvoid Lara_Control_Initialise(\n    const GF_LEVEL_TYPE level_type, const LARA_EXTRA_STATE start_state)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    if ((level_type == GFL_NORMAL || level_type == GFL_BONUS)\n        && start_state != LS_EXTRA_BREATH) {\n        lara_info->water_status = LWS_ABOVE_WATER;\n        M_HandleStartState(start_state);\n    } else if (Room_Get(lara_item->room_num)->flags.underwater) {\n        lara_info->water_status = LWS_UNDERWATER;\n        lara_item->fall_speed = 0;\n        lara_item->goal_anim_state = LS(LS_TREAD);\n        lara_item->current_anim_state = LS(LS_TREAD);\n        Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_IDLE), 0);\n    } else {\n        lara_info->water_status = LWS_ABOVE_WATER;\n        lara_item->goal_anim_state = LS(LS_STOP);\n        lara_item->current_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n    }\n}\n\nvoid Lara_Control(void)\n{\n    ITEM *const item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    const int32_t time4 = (int32_t)Output_GetTimeInGame() * 4;\n    if (lara_info->poison_timer >= 16 && (time4 & 0xFF) == 0) {\n        CLAMPG(lara_info->poison_timer, 256);\n        Lara_TakeDamage(lara_info->poison_timer >> 4, false);\n    }\n    if (lara_info->electric != 0) {\n        if (lara_info->electric < 16) {\n            lara_info->electric++;\n        }\n        Lara_Electricity_UpdatePoints();\n        Lara_Electricity_EmitLight();\n    }\n\n    if (lara_info->has_fired && (time4 & 0x7F) == 0) {\n        Creature_AlertNearbyGuards(item);\n        lara_info->has_fired = false;\n    }\n\n    if (item->hit_points > 0 && g_Config.debug.enable_invulnerability) {\n        item->hit_points = LARA_MAX_HITPOINTS;\n        lara_info->poison_timer = 0;\n    }\n\n    M_Cheat();\n\n    if (lara_info->interact_target.is_moving\n        && lara_info->interact_target.move_count++ > M_MOVE_TIMEOUT) {\n        lara_info->interact_target.is_moving = false;\n        lara_info->gun_status = LGS_ARMLESS;\n    }\n\n    M_UpdateEnvironment();\n\n    if (item->hit_points <= 0) {\n        item->hit_points = -1;\n        if (Game_IsInGym()) {\n            Gym_SetInventoryOpenEnabled(true);\n        }\n        if (lara_info->death_timer == 0) {\n            Music_Stop();\n            Stats_AddDeath();\n        }\n        lara_info->death_timer++;\n        lara_info->target = nullptr;\n\n        if ((item->flags & IF_ONE_SHOT) != 0) {\n            lara_info->death_timer++;\n            return;\n        }\n    } else if (Room_IsAbyssHeight(item->pos.y)) {\n        item->hit_points = -1;\n        lara_info->death_timer = 9 * LOGIC_FPS;\n    }\n\n    Camera_MoveManual();\n    M_HandleEnvironment();\n    Lara_Breath_Control(item);\n\n    Stats_AddDistanceTravelled(item->pos, lara_info->last_pos);\n    lara_info->last_pos = item->pos;\n}\n"
  },
  {
    "path": "src/trx/game/lara/control.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow.h>\n#include <trx/game/lara/types.h>\n\nvoid Lara_Control_Initialise(\n    GF_LEVEL_TYPE level_type, LARA_EXTRA_STATE start_state);\nvoid Lara_Control(void);\n"
  },
  {
    "path": "src/trx/game/lara/draw.c",
    "content": "#include <trx/game/lara/draw.h>\n\n#include <trx/config.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/items/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/electric.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/vars.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\nstatic bool m_CacheMatrices = false;\nstatic bool m_IsLara = true;\n\nstatic GAME_VECTOR M_GetLaraLightSample(const ITEM *const item)\n{\n    GAME_VECTOR sample_pos = {\n        .pos = { 0, 0, 0 },\n        .room_num = item->room_num,\n    };\n    Lara_GetJointAbsPosition(&sample_pos.pos, LM_TORSO);\n    return sample_pos;\n}\n\nstatic void M_CacheMatrix(const LARA_MESH mesh)\n{\n    if (!m_CacheMatrices) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->mesh_pos_matrices[mesh] = *g_WMatrixPtr;\n}\n\nstatic void M_DrawEquipmentMesh(\n    const OBJECT_MESH *const mesh, const CLIP clip, const bool interpolated)\n{\n    const GAME_VECTOR pos = {\n        .room_num = Lara_GetItem()->room_num,\n        .pos = Matrix_MulVec32_M(\n            g_WMatrixPtr,\n            (XYZ_32) {\n                mesh->center.x,\n                mesh->center.y - 24,\n                mesh->center.z,\n            }),\n    };\n    Output_PushTintOverride(Lara_GetMeshTint(pos));\n    if (interpolated) {\n        Output_DrawObjectMesh_I(mesh, clip);\n    } else {\n        Output_DrawObjectMesh(mesh, clip);\n    }\n    Output_PopTintOverride();\n}\n\nstatic void M_DrawLaraMesh(\n    const ITEM *const item, const LARA_MESH mesh_num, const CLIP clip,\n    const bool interpolated)\n{\n    if (m_IsLara && !Item_IsMeshVisible(item, mesh_num)) {\n        return;\n    }\n\n    const OBJECT_MESH *const mesh = Lara_Mesh_Get(mesh_num);\n    XYZ_32 origin = XYZ_32_From16(mesh->center);\n    switch (mesh_num) {\n    case LM_TORSO:\n    case LM_CALF_L:\n    case LM_CALF_R:\n    case LM_FOOT_L:\n    case LM_FOOT_R:\n        origin.y -= 24;\n        break;\n    case LM_HEAD:\n        origin.y -= 8;\n        break;\n    default:\n        break;\n    }\n    const GAME_VECTOR pos = {\n        .room_num = item->room_num,\n        .pos = Matrix_MulVec32_M(g_WMatrixPtr, origin),\n    };\n    Output_PushTintOverride(Lara_GetMeshTint(pos));\n    if (interpolated) {\n        Output_DrawObjectMesh_I(mesh, clip);\n    } else {\n        Output_DrawObjectMesh(mesh, clip);\n    }\n    Output_PopTintOverride();\n}\n\nstatic void M_DrawBodyPart(\n    const LARA_MESH mesh, const ANIM_BONE *const bone,\n    const XYZ_16 *mesh_rots_1, const XYZ_16 *mesh_rots_2, const CLIP clip)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (mesh_rots_2 != nullptr) {\n        Matrix_TranslateRel32_I(bone[mesh - 1].pos);\n        Matrix_Rot16_ID(mesh_rots_1[mesh], mesh_rots_2[mesh]);\n        M_CacheMatrix(mesh);\n        M_DrawLaraMesh(Lara_GetItem(), mesh, clip, true);\n    } else {\n        Matrix_TranslateRel32(bone[mesh - 1].pos);\n        Matrix_Rot16(mesh_rots_1[mesh]);\n        M_CacheMatrix(mesh);\n        M_DrawLaraMesh(Lara_GetItem(), mesh, clip, false);\n    }\n}\n\nstatic inline void M_DrawEquipment(\n    const LARA_MESH mesh, const CLIP clip, const bool interpolated)\n{\n    if (!m_IsLara) {\n        return;\n    }\n\n    if (!Item_IsMeshVisible(Lara_GetItem(), mesh)) {\n        return;\n    }\n\n    const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(mesh);\n    if (!equipment->visible || equipment->mesh == nullptr) {\n        return;\n    }\n\n    M_DrawEquipmentMesh(equipment->mesh, clip, interpolated);\n}\n\nstatic bool M_Draw_I(\n    const ITEM *const item, const ANIM_FRAME *const frame1,\n    const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n\n    if (!Lara_Vehicle_IsMounted()) {\n        Output_DrawShadow(obj->shadow_size, bounds, item);\n    }\n\n    MATRIX saved_matrix = *g_MatrixPtr;\n    MATRIX wsaved_matrix = *g_WMatrixPtr;\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n\n    const CLIP clip = Output_CheckBoundsClip(&frame1->bounds);\n    if (clip == CLIP_NOT_VISIBLE && !m_IsLara) {\n        m_CacheMatrices = false;\n        Matrix_Pop();\n        return false;\n    }\n\n    if (g_Config.debug.enable_debug_bounding_boxes) {\n        Output_DrawCuboid(&frame1->bounds);\n    }\n\n    m_CacheMatrices = m_IsLara;\n    if (m_CacheMatrices) {\n        lara->mesh_pos_matrices_valid = false;\n    }\n\n    Matrix_Push();\n    Output_CalculateObjectLightingAt(item, M_GetLaraLightSample(item));\n\n    const ANIM_BONE *const bone =\n        m_IsLara ? Lara_Skin_GetBoneBase() : Object_GetBone(obj, 0);\n    const XYZ_16 *mesh_rots_1 = frame1->mesh_rots;\n    const XYZ_16 *mesh_rots_2 = frame2->mesh_rots;\n\n    Matrix_InitInterpolate(frac, rate);\n    Matrix_TranslateRel16_ID(frame1->offset, frame2->offset);\n    Matrix_Rot16_ID(mesh_rots_1[LM_HIPS], mesh_rots_2[LM_HIPS]);\n    M_CacheMatrix(LM_HIPS);\n    M_DrawLaraMesh(item, LM_HIPS, clip, true);\n    M_DrawEquipment(LM_HIPS, clip, true);\n\n    Matrix_Push_I();\n    M_DrawBodyPart(LM_THIGH_L, bone, mesh_rots_1, mesh_rots_2, clip);\n    M_DrawEquipment(LM_THIGH_L, clip, true);\n    M_DrawBodyPart(LM_CALF_L, bone, mesh_rots_1, mesh_rots_2, clip);\n    M_DrawBodyPart(LM_FOOT_L, bone, mesh_rots_1, mesh_rots_2, clip);\n    Matrix_Pop_I();\n\n    Matrix_Push_I();\n    M_DrawBodyPart(LM_THIGH_R, bone, mesh_rots_1, mesh_rots_2, clip);\n    M_DrawEquipment(LM_THIGH_R, clip, true);\n    M_DrawBodyPart(LM_CALF_R, bone, mesh_rots_1, mesh_rots_2, clip);\n    M_DrawBodyPart(LM_FOOT_R, bone, mesh_rots_1, mesh_rots_2, clip);\n    Matrix_Pop_I();\n\n    Matrix_TranslateRel32_I(bone[LM_TORSO - 1].pos);\n    if (Lara_IsM16Active()) {\n        mesh_rots_2 =\n            lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots;\n        mesh_rots_1 = mesh_rots_2;\n    }\n\n    Matrix_Rot16_ID(mesh_rots_1[LM_TORSO], mesh_rots_2[LM_TORSO]);\n    Matrix_Rot16_I(lara->interp.result.torso_rot);\n    M_CacheMatrix(LM_TORSO);\n    M_DrawLaraMesh(item, LM_TORSO, clip, true);\n    M_DrawEquipment(LM_TORSO, clip, true);\n\n    Matrix_Push_I();\n    Matrix_TranslateRel32_I(bone[LM_HEAD - 1].pos);\n    Matrix_Rot16_ID(mesh_rots_1[LM_HEAD], mesh_rots_2[LM_HEAD]);\n    Matrix_Rot16_I(lara->interp.result.head_rot);\n    M_CacheMatrix(LM_HEAD);\n    M_DrawLaraMesh(item, LM_HEAD, clip, true);\n    M_DrawEquipment(LM_HEAD, clip, true);\n\n    *g_MatrixPtr = saved_matrix;\n    *g_WMatrixPtr = wsaved_matrix;\n    if (m_IsLara) {\n        Lara_Hair_Draw();\n    }\n    Matrix_Pop_I();\n\n    LARA_GUN_TYPE gun_type = LGT_UNARMED;\n    if (lara->gun_status == LGS_READY || lara->gun_status == LGS_SPECIAL\n        || lara->gun_status == LGS_DRAW || lara->gun_status == LGS_UNDRAW) {\n        gun_type = lara->gun_type;\n    }\n\n    switch (gun_type) {\n    case LGT_UNARMED:\n    case LGT_FLARE:\n        Matrix_Push_I();\n        M_DrawBodyPart(LM_UARM_R, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawEquipment(LM_HAND_R, clip, true);\n        Matrix_Pop_I();\n\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[LM_UARM_L - 1].pos);\n        if (lara->flare.control) {\n            const ANIM *const anim = Anim_GetAnim(lara->left_arm.anim_num);\n            mesh_rots_1 =\n                lara->left_arm\n                    .frame_base[lara->left_arm.frame_num - anim->frame_base]\n                    .mesh_rots;\n            mesh_rots_2 = mesh_rots_1;\n        }\n\n        Matrix_Rot16_ID(mesh_rots_1[LM_UARM_L], mesh_rots_2[LM_UARM_L]);\n        M_CacheMatrix(LM_UARM_L);\n        M_DrawLaraMesh(item, LM_UARM_L, clip, true);\n\n        M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawEquipment(LM_HAND_L, clip, true);\n\n        if (g_TRVersion < 3 && lara->gun_type == LGT_FLARE\n            && lara->left_arm.flash_gun) {\n            Gun_DrawFlash(LGT_FLARE, clip, true);\n        }\n        Matrix_Pop();\n        break;\n\n    case LGT_PISTOLS:\n    case LGT_MAGNUMS:\n    case LGT_AUTOS:\n    case LGT_DESERT_EAGLE:\n    case LGT_UZIS: {\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[LM_UARM_R - 1].pos);\n        Matrix_InterpolateArm();\n        if (gun_type == LGT_DESERT_EAGLE) {\n            Matrix_Rot16(lara->interp.result.torso_rot);\n        } else {\n            Matrix_Rot16(lara->right_arm.interp.result.rot);\n        }\n\n        const ANIM *anim = Anim_GetAnim(lara->right_arm.anim_num);\n        mesh_rots_1 =\n            lara->right_arm\n                .frame_base[lara->right_arm.frame_num - anim->frame_base]\n                .mesh_rots;\n        Matrix_Rot16(mesh_rots_1[LM_UARM_R]);\n        M_CacheMatrix(LM_UARM_R);\n        M_DrawLaraMesh(item, LM_UARM_R, clip, false);\n\n        M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, nullptr, clip);\n        M_DrawEquipment(LM_HAND_R, clip, false);\n        if (lara->right_arm.flash_gun) {\n            saved_matrix = *g_MatrixPtr;\n            wsaved_matrix = *g_WMatrixPtr;\n        }\n        Matrix_Pop_I();\n\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[LM_UARM_L - 1].pos);\n        Matrix_InterpolateArm();\n        if (gun_type == LGT_DESERT_EAGLE) {\n            Matrix_Rot16(lara->interp.result.torso_rot);\n        } else {\n            Matrix_Rot16(lara->left_arm.interp.result.rot);\n        }\n\n        anim = Anim_GetAnim(lara->left_arm.anim_num);\n        mesh_rots_1 =\n            lara->left_arm\n                .frame_base[lara->left_arm.frame_num - anim->frame_base]\n                .mesh_rots;\n        Matrix_Rot16(mesh_rots_1[LM_UARM_L]);\n        M_CacheMatrix(LM_UARM_L);\n        M_DrawLaraMesh(item, LM_UARM_L, clip, false);\n\n        M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, nullptr, clip);\n        M_DrawEquipment(LM_HAND_L, clip, false);\n\n        if (lara->left_arm.flash_gun) {\n            Gun_DrawFlash(gun_type, clip, false);\n        }\n        if (lara->right_arm.flash_gun) {\n            *g_MatrixPtr = saved_matrix;\n            *g_WMatrixPtr = wsaved_matrix;\n            Gun_DrawFlash(gun_type, clip, false);\n        }\n        Matrix_Pop();\n        break;\n    }\n\n    case LGT_SHOTGUN:\n    case LGT_M16:\n    case LGT_MP5:\n    case LGT_GRENADE:\n    case LGT_ROCKET:\n    case LGT_HARPOON: {\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[LM_UARM_R - 1].pos);\n        mesh_rots_1 =\n            lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots;\n        mesh_rots_2 = mesh_rots_1;\n        Matrix_Rot16_ID(mesh_rots_1[LM_UARM_R], mesh_rots_2[LM_UARM_R]);\n        M_CacheMatrix(LM_UARM_R);\n        M_DrawLaraMesh(item, LM_UARM_R, clip, true);\n\n// NOTE: gcc wrongly complains about mesh_rots_1 possibly being nullptr.\n// While this is not the case, it's curious how the pistols subtract the\n// frame_base from lara->*_arm.frame_num to access the mesh_rots, and the\n// rifles do not.\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Warray-bounds\"\n\n        M_DrawBodyPart(LM_LARM_R, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawBodyPart(LM_HAND_R, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawEquipment(LM_HAND_R, clip, true);\n\n        if (lara->right_arm.flash_gun) {\n            saved_matrix = *g_MatrixPtr;\n            wsaved_matrix = *g_WMatrixPtr;\n        }\n        Matrix_Pop_I();\n\n        Matrix_Push_I();\n        M_DrawBodyPart(LM_UARM_L, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawBodyPart(LM_LARM_L, bone, mesh_rots_1, mesh_rots_2, clip);\n        M_DrawBodyPart(LM_HAND_L, bone, mesh_rots_1, mesh_rots_2, clip);\n\n#pragma GCC diagnostic pop\n\n        if (lara->right_arm.flash_gun) {\n            *g_MatrixPtr = saved_matrix;\n            *g_WMatrixPtr = wsaved_matrix;\n            Gun_DrawFlash(gun_type, clip, false);\n        }\n        Matrix_Pop();\n        break;\n    }\n\n    default:\n        break;\n    }\n\n    if (m_CacheMatrices) {\n        lara->mesh_pos_matrices_valid = true;\n    }\n    m_CacheMatrices = false;\n\n    Matrix_Pop();\n    Matrix_Pop();\n    return true;\n}\n\nbool Lara_Draw(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    m_IsLara = item == lara_item;\n    if (m_IsLara\n        && (item->status == IS_INVISIBLE || (item->flags & IF_ONE_SHOT) != 0)) {\n        return false;\n    }\n\n    const int32_t top = g_PhdTop;\n    const int32_t left = g_PhdLeft;\n    const int32_t right = g_PhdRight;\n    const int32_t bottom = g_PhdBottom;\n\n    g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME);\n    g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME);\n    g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME);\n    g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ANIM_FRAME *frames[2];\n    if (lara->hit_direction < 0) {\n        int32_t rate;\n        const int32_t frac = Item_GetFrames(lara_item, frames, &rate);\n        if (frac != 0 && Lara_Pose_Get() == nullptr) {\n            M_Draw_I(item, frames[0], frames[1], frac, rate);\n            goto finish;\n        }\n    }\n\n    const ANIM_FRAME *const hit_frame = Lara_GetHitFrame(item);\n    const ANIM_FRAME *const frame =\n        hit_frame == nullptr ? frames[0] : hit_frame;\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const BOUNDS_16 *const shadow_bounds = Item_GetBoundsAccurate(item);\n    if (!Lara_Vehicle_IsMounted()) {\n        Output_DrawShadow(obj->shadow_size, shadow_bounds, item);\n    }\n\n    MATRIX saved_matrix = *g_MatrixPtr;\n    MATRIX wsaved_matrix = *g_WMatrixPtr;\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n    const MATRIX item_matrix = *g_MatrixPtr;\n    const MATRIX item_wmatrix = *g_WMatrixPtr;\n    const CLIP clip = Output_CheckBoundsClip(&frame->bounds);\n    if (clip == CLIP_NOT_VISIBLE && !m_IsLara) {\n        m_CacheMatrices = false;\n        Matrix_Pop();\n        return false;\n    }\n\n    if (g_Config.debug.enable_debug_bounding_boxes) {\n        Output_DrawCuboid(&frame->bounds);\n    }\n\n    m_CacheMatrices = m_IsLara;\n    if (m_CacheMatrices) {\n        lara->mesh_pos_matrices_valid = false;\n    }\n\n    Matrix_Push();\n    Output_CalculateObjectLightingAt(item, M_GetLaraLightSample(item));\n\n    const ANIM_BONE *const bone =\n        m_IsLara ? Lara_Skin_GetBoneBase() : Object_GetBone(obj, 0);\n    const LARA_POSE *const pose = Lara_Pose_Get();\n    const XYZ_16 *mesh_rots = pose != nullptr ? pose->rots : frame->mesh_rots;\n\n    Matrix_TranslateRel16(pose != nullptr ? pose->offset : frame->offset);\n    Matrix_Rot16(mesh_rots[LM_HIPS]);\n    M_CacheMatrix(LM_HIPS);\n    M_DrawLaraMesh(item, LM_HIPS, clip, false);\n    M_DrawEquipment(LM_HIPS, clip, false);\n\n    Matrix_Push();\n    M_DrawBodyPart(LM_THIGH_L, bone, mesh_rots, nullptr, clip);\n    M_DrawEquipment(LM_THIGH_L, clip, false);\n    M_DrawBodyPart(LM_CALF_L, bone, mesh_rots, nullptr, clip);\n    M_DrawBodyPart(LM_FOOT_L, bone, mesh_rots, nullptr, clip);\n    Matrix_Pop();\n\n    Matrix_Push();\n    M_DrawBodyPart(LM_THIGH_R, bone, mesh_rots, nullptr, clip);\n    M_DrawEquipment(LM_THIGH_R, clip, false);\n    M_DrawBodyPart(LM_CALF_R, bone, mesh_rots, nullptr, clip);\n    M_DrawBodyPart(LM_FOOT_R, bone, mesh_rots, nullptr, clip);\n    Matrix_Pop();\n\n    Matrix_TranslateRel32(bone[LM_TORSO - 1].pos);\n    if (Lara_IsM16Active() && pose == nullptr) {\n        mesh_rots =\n            lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots;\n    }\n\n    Matrix_Rot16(mesh_rots[LM_TORSO]);\n    Matrix_Rot16(lara->interp.result.torso_rot);\n    M_CacheMatrix(LM_TORSO);\n    M_DrawLaraMesh(item, LM_TORSO, clip, false);\n    M_DrawEquipment(LM_TORSO, clip, false);\n\n    Matrix_Push();\n    Matrix_TranslateRel32(bone[LM_HEAD - 1].pos);\n    Matrix_Rot16(mesh_rots[LM_HEAD]);\n    Matrix_Rot16(lara->interp.result.head_rot);\n    M_CacheMatrix(LM_HEAD);\n    M_DrawLaraMesh(item, LM_HEAD, clip, false);\n    M_DrawEquipment(LM_HEAD, clip, false);\n\n    *g_MatrixPtr = saved_matrix;\n    *g_WMatrixPtr = wsaved_matrix;\n    if (m_IsLara) {\n        Lara_Hair_Draw();\n    }\n\n    Matrix_Pop();\n\n    LARA_GUN_TYPE gun_type = LGT_UNARMED;\n    if (pose == nullptr\n        && (lara->gun_status == LGS_READY || lara->gun_status == LGS_SPECIAL\n            || lara->gun_status == LGS_DRAW\n            || lara->gun_status == LGS_UNDRAW)) {\n        gun_type = lara->gun_type;\n    }\n\n    switch (gun_type) {\n    case LGT_UNARMED:\n    case LGT_FLARE:\n        Matrix_Push();\n        M_DrawBodyPart(LM_UARM_R, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_LARM_R, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_R, bone, mesh_rots, nullptr, clip);\n        M_DrawEquipment(LM_HAND_R, clip, false);\n        Matrix_Pop();\n\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos);\n        if (lara->flare.control && pose == nullptr) {\n            const ANIM *const anim = Anim_GetAnim(lara->left_arm.anim_num);\n            mesh_rots =\n                lara->left_arm\n                    .frame_base[lara->left_arm.frame_num - anim->frame_base]\n                    .mesh_rots;\n        }\n\n        Matrix_Rot16(mesh_rots[LM_UARM_L]);\n        M_DrawLaraMesh(item, LM_UARM_L, clip, false);\n\n        M_DrawBodyPart(LM_LARM_L, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_L, bone, mesh_rots, nullptr, clip);\n        M_DrawEquipment(LM_HAND_L, clip, false);\n\n        if (g_TRVersion < 3 && lara->gun_type == LGT_FLARE\n            && lara->left_arm.flash_gun) {\n            Gun_DrawFlash(LGT_FLARE, clip, false);\n        }\n\n        Matrix_Pop();\n        break;\n\n    case LGT_PISTOLS:\n    case LGT_MAGNUMS:\n    case LGT_AUTOS:\n    case LGT_DESERT_EAGLE:\n    case LGT_UZIS: {\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos);\n        g_MatrixPtr->_00 = item_matrix._00;\n        g_MatrixPtr->_01 = item_matrix._01;\n        g_MatrixPtr->_02 = item_matrix._02;\n        g_MatrixPtr->_10 = item_matrix._10;\n        g_MatrixPtr->_11 = item_matrix._11;\n        g_MatrixPtr->_12 = item_matrix._12;\n        g_MatrixPtr->_20 = item_matrix._20;\n        g_MatrixPtr->_21 = item_matrix._21;\n        g_MatrixPtr->_22 = item_matrix._22;\n        g_WMatrixPtr->_00 = item_wmatrix._00;\n        g_WMatrixPtr->_01 = item_wmatrix._01;\n        g_WMatrixPtr->_02 = item_wmatrix._02;\n        g_WMatrixPtr->_10 = item_wmatrix._10;\n        g_WMatrixPtr->_11 = item_wmatrix._11;\n        g_WMatrixPtr->_12 = item_wmatrix._12;\n        g_WMatrixPtr->_20 = item_wmatrix._20;\n        g_WMatrixPtr->_21 = item_wmatrix._21;\n        g_WMatrixPtr->_22 = item_wmatrix._22;\n\n        if (gun_type == LGT_DESERT_EAGLE) {\n            Matrix_Rot16(lara->interp.result.torso_rot);\n        } else {\n            Matrix_Rot16(lara->right_arm.interp.result.rot);\n        }\n        if (pose == nullptr) {\n            const ANIM *const anim = Anim_GetAnim(lara->right_arm.anim_num);\n            mesh_rots =\n                lara->right_arm\n                    .frame_base[lara->right_arm.frame_num - anim->frame_base]\n                    .mesh_rots;\n        }\n        Matrix_Rot16(mesh_rots[LM_UARM_R]);\n        M_DrawLaraMesh(item, LM_UARM_R, clip, false);\n\n        M_DrawBodyPart(LM_LARM_R, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_R, bone, mesh_rots, nullptr, clip);\n        M_DrawEquipment(LM_HAND_R, clip, false);\n\n        if (lara->right_arm.flash_gun) {\n            saved_matrix = *g_MatrixPtr;\n            wsaved_matrix = *g_WMatrixPtr;\n        }\n        Matrix_Pop();\n\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos);\n        g_MatrixPtr->_00 = item_matrix._00;\n        g_MatrixPtr->_01 = item_matrix._01;\n        g_MatrixPtr->_02 = item_matrix._02;\n        g_MatrixPtr->_10 = item_matrix._10;\n        g_MatrixPtr->_11 = item_matrix._11;\n        g_MatrixPtr->_12 = item_matrix._12;\n        g_MatrixPtr->_20 = item_matrix._20;\n        g_MatrixPtr->_21 = item_matrix._21;\n        g_MatrixPtr->_22 = item_matrix._22;\n        g_WMatrixPtr->_00 = item_wmatrix._00;\n        g_WMatrixPtr->_01 = item_wmatrix._01;\n        g_WMatrixPtr->_02 = item_wmatrix._02;\n        g_WMatrixPtr->_10 = item_wmatrix._10;\n        g_WMatrixPtr->_11 = item_wmatrix._11;\n        g_WMatrixPtr->_12 = item_wmatrix._12;\n        g_WMatrixPtr->_20 = item_wmatrix._20;\n        g_WMatrixPtr->_21 = item_wmatrix._21;\n        g_WMatrixPtr->_22 = item_wmatrix._22;\n\n        if (gun_type == LGT_DESERT_EAGLE) {\n            Matrix_Rot16(lara->interp.result.torso_rot);\n        } else {\n            Matrix_Rot16(lara->left_arm.interp.result.rot);\n        }\n        if (pose == nullptr) {\n            const ANIM *const anim = Anim_GetAnim(lara->left_arm.anim_num);\n            mesh_rots =\n                lara->left_arm\n                    .frame_base[lara->left_arm.frame_num - anim->frame_base]\n                    .mesh_rots;\n        }\n        Matrix_Rot16(mesh_rots[LM_UARM_L]);\n        M_CacheMatrix(LM_UARM_L);\n        M_DrawLaraMesh(item, LM_UARM_L, clip, false);\n\n        M_DrawBodyPart(LM_LARM_L, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_L, bone, mesh_rots, nullptr, clip);\n        M_DrawEquipment(LM_HAND_L, clip, false);\n\n        if (lara->left_arm.flash_gun) {\n            Gun_DrawFlash(gun_type, clip, false);\n        }\n        if (lara->right_arm.flash_gun) {\n            *g_MatrixPtr = saved_matrix;\n            *g_WMatrixPtr = wsaved_matrix;\n            Gun_DrawFlash(gun_type, clip, false);\n        }\n\n        Matrix_Pop();\n        break;\n    }\n\n    case LGT_SHOTGUN:\n    case LGT_M16:\n    case LGT_MP5:\n    case LGT_GRENADE:\n    case LGT_ROCKET:\n    case LGT_HARPOON: {\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos);\n        if (pose == nullptr) {\n            mesh_rots =\n                lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots;\n        }\n        Matrix_Rot16(mesh_rots[LM_UARM_R]);\n        M_CacheMatrix(LM_UARM_R);\n        M_DrawLaraMesh(item, LM_UARM_R, clip, false);\n\n        M_DrawBodyPart(LM_LARM_R, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_R, bone, mesh_rots, nullptr, clip);\n        M_DrawEquipment(LM_HAND_R, clip, false);\n\n        if (lara->right_arm.flash_gun) {\n            saved_matrix = *g_MatrixPtr;\n            wsaved_matrix = *g_WMatrixPtr;\n        }\n        Matrix_Pop();\n\n        Matrix_Push();\n        M_DrawBodyPart(LM_UARM_L, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_LARM_L, bone, mesh_rots, nullptr, clip);\n        M_DrawBodyPart(LM_HAND_L, bone, mesh_rots, nullptr, clip);\n\n        if (lara->right_arm.flash_gun) {\n            *g_MatrixPtr = saved_matrix;\n            *g_WMatrixPtr = wsaved_matrix;\n            Gun_DrawFlash(gun_type, clip, false);\n        }\n\n        Matrix_Pop();\n        break;\n    }\n\n    default:\n        break;\n    }\n\n    if (m_CacheMatrices) {\n        lara->mesh_pos_matrices_valid = true;\n    }\n    m_CacheMatrices = false;\n\n    Matrix_Pop();\n    Matrix_Pop();\n\nfinish:\n    if (m_IsLara && lara->electric != 0) {\n        Lara_Electricity_Draw(0, lara_item);\n        Lara_Electricity_Draw(1, lara_item);\n    }\n\n    g_PhdLeft = left;\n    g_PhdRight = right;\n    g_PhdTop = top;\n    g_PhdBottom = bottom;\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/lara/draw.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nbool Lara_Draw(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/lara/electric.c",
    "content": "#include <trx/game/lara/electric.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/collision.h>\n#include <trx/game/lara.h>\n#include <trx/game/output/lights.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n\nstatic const uint8_t m_LaraMeshes[28] = {\n    0, 1, 1, 2, 2, 3,  0, 4,  4,  5,  5,  6,  0, 7,\n    7, 8, 8, 9, 9, 10, 7, 11, 11, 12, 12, 13, 7, 14,\n};\nstatic const uint8_t m_LaraLastPoints[14] = {\n    0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1,\n};\nstatic const uint8_t m_LaraLineCounts[6] = { 12, 12, 4, 12, 12, 4 };\n\ntypedef struct {\n    XYZ_16 pos;\n    XYZ_16 vel;\n} M_ELECTRIC_POINT;\n\nstatic M_ELECTRIC_POINT m_ElectricityPoints[32] = {};\n\nXYZ_16 Lara_Electricity_GetPoint(const int32_t idx)\n{\n    return m_ElectricityPoints[idx & 31].pos;\n}\n\nvoid Lara_Electricity_UpdatePoints(void)\n{\n    for (int32_t i = 0; i < 32; i++) {\n        const int32_t rnd = Random_GetDraw();\n        int16_t x = m_ElectricityPoints[i].pos.x;\n        int16_t y = m_ElectricityPoints[i].pos.y;\n        int16_t z = m_ElectricityPoints[i].pos.z;\n        int16_t xv = m_ElectricityPoints[i].vel.x;\n        int16_t yv = m_ElectricityPoints[i].vel.y;\n        int16_t zv = m_ElectricityPoints[i].vel.z;\n\n        if (((x > 256 || x < -256) && (y > 256 || y < -256)\n             && (z > 256 || z < -256))\n            || x > 384 || x < -128 || y > 384 || y < -128 || z > 384\n            || z < -128) {\n            x = 0;\n            y = 0;\n            z = 0;\n            xv = 0;\n            yv = 0;\n            zv = 0;\n        }\n\n        if (xv != 0) {\n            if (xv >= 0) {\n                xv += 2;\n            } else {\n                xv -= 2;\n            }\n        } else if ((rnd & 1) != 0) {\n            xv = -1 - (Random_GetDraw() & 3);\n        } else {\n            xv = (Random_GetDraw() & 3) + 1;\n        }\n\n        if (yv != 0) {\n            if (yv >= 0) {\n                yv += 2;\n            } else {\n                yv -= 2;\n            }\n        } else if ((rnd & 2) != 0) {\n            yv = -1 - (Random_GetDraw() & 3);\n        } else {\n            yv = (Random_GetDraw() & 3) + 1;\n        }\n\n        if (zv != 0) {\n            if (zv >= 0) {\n                zv++;\n            } else {\n                zv--;\n            }\n        } else if ((rnd & 4) != 0) {\n            zv = -1 - (Random_GetDraw() & 3);\n        } else {\n            zv = (Random_GetDraw() & 3) + 1;\n        }\n\n        x += xv;\n        y += yv;\n        z += zv;\n\n        m_ElectricityPoints[i].pos.x = x;\n        m_ElectricityPoints[i].pos.y = y;\n        m_ElectricityPoints[i].pos.z = z;\n        m_ElectricityPoints[i].vel.x = xv;\n        m_ElectricityPoints[i].vel.y = yv;\n        m_ElectricityPoints[i].vel.z = zv;\n    }\n}\n\nvoid Lara_Electricity_EmitLight(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->electric == 0) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t electric = lara->electric;\n\n    int32_t r = 0;\n    int32_t g = 0;\n    int32_t b = 0;\n    int32_t falloff = 0;\n\n    if (electric < 12) {\n        r = (Random_GetControl() & 7) - electric + 16;\n        g = 32 - electric;\n        b = 255;\n        falloff = (Random_GetControl() & 1) - 2 * electric + 25;\n        r <<= 3;\n        g <<= 3;\n    } else {\n        r = 0;\n        g = (Random_GetControl() & 0x3F) + 64;\n        b = (Random_GetControl() & 0x3F) + 128;\n        falloff = (Random_GetControl() & 3) + 8;\n    }\n\n    CLAMP(r, 0, 255);\n    CLAMP(g, 0, 255);\n    CLAMP(b, 0, 255);\n    CLAMP(falloff, 0, 255);\n\n    Output_AddDynamicLightRGB(lara_item->pos, falloff, (RGB_888) { r, g, b });\n}\n\nvoid Lara_Electricity_Draw(const int32_t lr, const ITEM *const item)\n{\n    XYZ_32 pos[96];\n    int16_t dists[96];\n    int32_t num = 0;\n\n    for (int32_t i = 0; i < 14; i++) {\n        const int32_t mesh1 = m_LaraMeshes[2 * i];\n        const int32_t mesh2 = m_LaraMeshes[2 * i + 1];\n        const M_ELECTRIC_POINT *const points =\n            &m_ElectricityPoints[(5 * i) & 0xF];\n        int32_t point_idx = 0;\n\n        XYZ_32 pos1 = { .x = 0, .y = 0, .z = 0 };\n        XYZ_32 pos2 = { .x = 0, .y = 0, .z = 0 };\n\n        if (lr != 0) {\n            pos1.x = -48;\n            pos1.z = -48;\n        } else {\n            pos1.x = 48;\n            pos1.z = 48;\n        }\n\n        Collide_GetJointAbsPosition(item, &pos1, mesh1);\n\n        if (m_LaraLastPoints[i] == 0 || i == 13) {\n            if (lr != 0) {\n                pos2.x = -48;\n                pos2.z = -48;\n            } else {\n                pos2.x = 48;\n                pos2.z = 48;\n            }\n\n            if (i == 13) {\n                pos2.y = -64;\n            }\n        }\n\n        Collide_GetJointAbsPosition(item, &pos2, mesh2);\n\n        int32_t x = pos1.x;\n        int32_t y = pos1.y;\n        int32_t z = pos1.z;\n        const int32_t x_step = (pos2.x - pos1.x) >> 2;\n        const int32_t y_step = (pos2.y - pos1.y) >> 2;\n        const int32_t z_step = (pos2.z - pos1.z) >> 2;\n\n        for (int32_t j = 0; j < 5; j++) {\n            if (j == 4 && m_LaraLastPoints[i] == 0) {\n                break;\n            }\n\n            int32_t mx = x;\n            int32_t my = y;\n            int32_t mz = z;\n            if (j == 4 && m_LaraLastPoints[i] != 0) {\n                mx = pos2.x;\n                my = pos2.y;\n                mz = pos2.z;\n            }\n\n            if (j == 0 || j == 4) {\n                dists[num] = 0;\n            } else {\n                const XYZ_16 point = points[point_idx].pos;\n                point_idx++;\n\n                if (lr != 0) {\n                    mx -= point.x >> 3;\n                    my -= point.y >> 3;\n                    mz -= point.z >> 3;\n                } else {\n                    mx += point.x >> 3;\n                    my += point.y >> 3;\n                    mz += point.z >> 3;\n                }\n\n                int32_t p_x = ABS(point.x);\n                int32_t p_y = ABS(point.y);\n                int32_t p_z = ABS(point.z);\n                if (p_y > p_x) {\n                    p_x = p_y;\n                }\n                if (p_z > p_x) {\n                    p_x = p_z;\n                }\n                dists[num] = (int16_t)p_x;\n            }\n\n            pos[num].x = mx;\n            pos[num].y = my;\n            pos[num].z = mz;\n            num++;\n\n            x += x_step;\n            y += y_step;\n            z += z_step;\n        }\n    }\n\n    int32_t idx = 0;\n    for (int32_t i = 0; i < 6; i++) {\n        for (int32_t j = 0; j < m_LaraLineCounts[i]; j++) {\n            const XYZ_32 from = pos[idx];\n            const XYZ_32 to = pos[idx + 1];\n            int32_t c0 = dists[idx];\n            int32_t c1 = dists[idx + 1];\n            idx++;\n\n            if (c0 > 255) {\n                c0 = 511 - c0;\n                if (c0 < 0) {\n                    c0 = 0;\n                }\n            }\n\n            if (c1 > 255) {\n                c1 = 511 - c1;\n                if (c1 < 0) {\n                    c1 = 0;\n                }\n            }\n\n            if (lr != 0) {\n                c0 >>= 1;\n                c1 >>= 1;\n            }\n\n            const RGBA_8888 from_color = { 0, (uint8_t)c0, (uint8_t)c0, 0xC0 };\n            const RGBA_8888 to_color = { 0, (uint8_t)c1, (uint8_t)c1, 0xC0 };\n            OutputSource_PolyFX_StageLineSegment(\n                from, from_color, to, to_color, 1.0f, DRAW_BLEND_ADD);\n        }\n        idx++;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lara/electric.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nvoid Lara_Electricity_UpdatePoints(void);\nvoid Lara_Electricity_EmitLight(void);\nvoid Lara_Electricity_Draw(int32_t lr, const ITEM *item);\nXYZ_16 Lara_Electricity_GetPoint(int32_t idx);\n"
  },
  {
    "path": "src/trx/game/lara/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    LS_INVALID = -1,\n} LARA_STATE;\n\ntypedef enum {\n    LS_TRX_INVALID = -1,\n#define X_CATALOG_ID(enum_value) enum_value,\n#include <trx/game/catalog/lara_states.def>\n#undef X_CATALOG_ID\n    LS_NUMBER_OF,\n} LARA_TRX_STATE;\n\ntypedef enum {\n    LA_INVALID = -1,\n} LARA_ANIMATION;\n\ntypedef enum {\n    LA_TRX_INVALID = -1,\n#define X_CATALOG_ID(enum_value) enum_value,\n#include <trx/game/catalog/lara_anims.def>\n#undef X_CATALOG_ID\n    LA_NUMBER_OF,\n} LARA_TRX_ANIMATION;\n\n// clang-format off\ntypedef enum {\n    LWS_ABOVE_WATER  = 0,\n    LWS_UNDERWATER   = 1,\n    LWS_SURFACE      = 2,\n    LWS_CHEAT        = 3,\n    LWS_WADE         = 4,\n} LARA_WATER_STATE;\n\ntypedef enum {\n    LGS_ARMLESS    = 0,\n    LGS_HANDS_BUSY = 1,\n    LGS_DRAW       = 2,\n    LGS_UNDRAW     = 3,\n    LGS_READY      = 4,\n    LGS_SPECIAL    = 5,\n} LARA_GUN_STATE;\n\ntypedef enum {\n    LM_HIPS      = 0,\n    LM_THIGH_L   = 1,\n    LM_CALF_L    = 2,\n    LM_FOOT_L    = 3,\n    LM_THIGH_R   = 4,\n    LM_CALF_R    = 5,\n    LM_FOOT_R    = 6,\n    LM_TORSO     = 7,\n    LM_UARM_R    = 8,\n    LM_LARM_R    = 9,\n    LM_HAND_R    = 10,\n    LM_UARM_L    = 11,\n    LM_LARM_L    = 12,\n    LM_HAND_L    = 13,\n    LM_HEAD      = 14,\n    LM_FIRST     = LM_HIPS,\n    LM_NUMBER_OF = 15,\n} LARA_MESH;\n// clang-format on\n\n// clang-format off\ntypedef enum {\n    LS_EXTRA_BREATH         = 0,\n    LS_EXTRA_TREX_KILL      = 1,\n    LS_EXTRA_SCION_PICKUP_1 = 2,\n    LS_EXTRA_USE_MIDAS      = 3,\n    LS_EXTRA_MIDAS_KILL     = 4,\n    LS_EXTRA_SCION_PICKUP_2 = 5,\n    LS_EXTRA_TORSO_KILL     = 6,\n    LS_EXTRA_PLUNGER        = 7,\n    LS_EXTRA_START_ANIM     = 8,\n    LS_EXTRA_AIRLOCK        = 9,\n    LS_EXTRA_SHARK_KILL     = 10,\n    LS_EXTRA_YETI_KILL      = 11,\n    LS_EXTRA_GONG_BONG      = 12,\n    LS_EXTRA_GUARD_KILL     = 13,\n    LS_EXTRA_PULL_DAGGER    = 14,\n    LS_EXTRA_START_HOUSE    = 15,\n    LS_EXTRA_END_HOUSE      = 16,\n    LS_EXTRA_SHIVA_KILL     = 17,\n    LS_EXTRA_RAPIDS_DROWN   = 18,\n    LS_EXTRA_TRAIN_KILL     = 19,\n    LS_EXTRA_JAIL_WAKE_UP   = 20,\n    LS_EXTRA_WILLARD_KILL   = 21,\n    LS_EXTRA_NUMBER_OF,\n} LARA_EXTRA_STATE;\n// clang-format on\n\n// clang-format off\ntypedef enum {\n    LGT_UNKNOWN      = -1, // for legacy saves\n    LGT_UNARMED      = 0,\n    LGT_PISTOLS      = 1,\n    LGT_MAGNUMS      = 2,\n    LGT_UZIS         = 3,\n    LGT_SHOTGUN      = 4,\n    LGT_M16          = 5,\n    LGT_GRENADE      = 6,\n    LGT_HARPOON      = 7,\n    LGT_FLARE        = 8,\n    LGT_SKIDOO       = 9,\n    LGT_AUTOS        = 10,\n    LGT_DESERT_EAGLE = 11,\n    LGT_MP5          = 12,\n    LGT_ROCKET       = 13,\n    NUM_WEAPONS,\n} LARA_GUN_TYPE;\n// clang-format on\n\n// clang-format off\ntypedef enum {\n    LF_G_AIM_START    = 0,\n    LF_G_AIM_BEND     = 1,\n    LF_G_AIM_EXTEND   = 3,\n    LF_G_AIM_END      = 4,\n    LF_G_UNDRAW_START = 5,\n    LF_G_UNDRAW_BEND  = 6,\n    LF_G_UNDRAW_END   = 12,\n    LF_G_DRAW_START   = 13,\n    LF_G_DRAW_END     = 23,\n    LF_G_RECOIL_START = 24,\n    LF_G_RECOIL_END   = 32,\n} LARA_GUN_ANIMATION_FRAME;\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/flare.c",
    "content": "#include <trx/game/lara/flare.h>\n\n#include <trx/config.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/general/flare_item.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\ntypedef enum {\n    // clang-format off\n    LA_FLARES_HOLD   = 0,\n    LA_FLARES_THROW  = 1,\n    LA_FLARES_DRAW   = 2,\n    LA_FLARES_IGNITE = 3,\n    LA_FLARES_IDLE   = 4,\n    // clang-format on\n} M_LARA_FLARE_ANIMATION;\n\ntypedef enum {\n    // clang-format off\n    LF_FL_HOLD_FT       = 1,\n    LF_FL_THROW_FT      = 32,\n    LF_FL_DRAW_FT       = 39,\n    LF_FL_IGNITE_FT     = 23,\n    LF_FL_2_HOLD_FT     = 15,\n\n    LF_FL_HOLD          = 0,\n    LF_FL_THROW         = (LF_FL_HOLD + LF_FL_HOLD_FT), // = 1\n    LF_FL_THROW_RELEASE = (LF_FL_THROW + 20), // = 21\n    LF_FL_DRAW          = (LF_FL_THROW + LF_FL_THROW_FT), // = 33\n    LF_FL_IGNITE        = (LF_FL_DRAW + LF_FL_DRAW_FT), // = 72\n    LF_FL_2_HOLD        = (LF_FL_IGNITE + LF_FL_IGNITE_FT), // = 95\n    LF_FL_END           = (LF_FL_2_HOLD + LF_FL_2_HOLD_FT), // = 110\n    LF_FL_DRAW_GOT_IT   = (LF_FL_DRAW + 13), // = 46\n    // clang-format on\n} M_LARA_FLARE_FRAME;\n\nstatic const LARA_TRX_STATE m_HoldStates[] = {\n    // clang-format off\n    LS_WALK,\n    LS_STOP,\n    LS_POSE,\n    LS_TURN_RIGHT,\n    LS_TURN_LEFT,\n    LS_WALK_BACK,\n    LS_FAST_TURN,\n    LS_STEP_LEFT,\n    LS_STEP_RIGHT,\n    LS_WADE,\n    LS_PICKUP,\n    LS_SWITCH_ON,\n    LS_SWITCH_OFF,\n    LS_TRX_INVALID, // sentinel\n    // clang-format on\n};\n\nstatic const LARA_TRX_STATE m_ThrowStates[] = {\n    // clang-format off\n    LS_FAST_FALL,\n    LS_SWAN_DIVE,\n    LS_FAST_DIVE,\n    LS_TRX_INVALID, // sentinel\n    // clang-format on\n};\n\nstatic XYZ_32 m_IgnitePos = {};\n\nstatic void M_InitialiseState(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->gun_status = LGS_ARMLESS;\n    lara_info->left_arm.rot.x = 0;\n    lara_info->left_arm.rot.y = 0;\n    lara_info->left_arm.rot.z = 0;\n    lara_info->right_arm.rot.x = 0;\n    lara_info->right_arm.rot.y = 0;\n    lara_info->right_arm.rot.z = 0;\n    lara_info->left_arm.lock = 0;\n    lara_info->right_arm.lock = 0;\n    lara_info->target = nullptr;\n}\n\nstatic void M_DoIgniteEffects(const XYZ_32 flare_pos, int16_t room_num)\n{\n    m_IgnitePos = flare_pos;\n    Room_GetSector(m_IgnitePos, &room_num);\n    const ROOM *const room = Room_Get(room_num);\n    const SOUND_PLAY_MODE mode =\n        room->flags.underwater ? SPM_UNDERWATER : SPM_NORMAL;\n    Sound_Effect(SFX_LARA_FLARE_IGNITE, &m_IgnitePos, mode);\n}\n\nstatic bool M_CanThrowFlare(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->gun_status != LGS_ARMLESS) {\n        return false;\n    }\n\n    if (!g_Config.gameplay.fix_flare_throw_priority) {\n        return true;\n    }\n\n    if (lara_info->water_status != LWS_ABOVE_WATER\n        && lara_info->water_status != LWS_WADE) {\n        return true;\n    }\n\n    // Airborne states that would not allow ledge grabbing anyway.\n    if (Lara_HasState(m_ThrowStates)) {\n        return true;\n    }\n\n    // Neither airborne nor about to be.\n    return !lara_item->gravity && !g_Input.jump;\n}\n\nstatic void M_ControlInHand(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const int32_t flare_age = g_Config.debug.enable_endless_flare_time\n        ? MIN(Flare_GetMaxAge() / 2, lara_info->flare.age)\n        : lara_info->flare.age;\n\n    XYZ_32 vec = {\n        .x = 11,\n        .y = 32,\n        .z = 41,\n    };\n    Lara_GetJointAbsPosition(&vec, LM_HAND_L);\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (flare_age == 0) {\n        M_DoIgniteEffects(vec, lara_item->room_num);\n    }\n\n    lara_info->left_arm.flash_gun = Flare_GenerateLight(vec, flare_age);\n\n    if (flare_age >= Flare_GetMaxAge()) {\n        if (M_CanThrowFlare()) {\n            lara_info->gun_status = LGS_UNDRAW;\n        }\n        return;\n    }\n\n    lara_info->flare.age = flare_age + 1;\n    Flare_GenerateEffects(&lara_item->pos, vec, lara_item->room_num);\n\n    if (!lara_info->left_arm.flash_gun) {\n        return;\n    }\n\n    if (g_TRVersion < 3) {\n        return;\n    }\n\n    XYZ_32 vec_2 = {\n        .x = 8,\n        .y = 36,\n        .z = WALL_L + (Random_GetDraw() & 0xFF),\n    };\n    Lara_GetJointAbsPosition(&vec_2, LM_HAND_L);\n    const XYZ_32 vel = {\n        .x = vec_2.x - vec.x,\n        .y = vec_2.y - vec.y,\n        .z = vec_2.z - vec.z,\n    };\n    for (int32_t i = 0; i < (Random_GetDraw() & 3) + 4; i++) {\n        const bool smoke = (i >> 2) != 0;\n        Sparks_TriggerFlareSparks(vec, vel, smoke);\n    }\n}\n\nstatic void M_SetArm(const int32_t flare_frame)\n{\n    int16_t anim_idx;\n    if (flare_frame < LF_FL_THROW) {\n        anim_idx = LA_FLARES_HOLD;\n    } else if (flare_frame < LF_FL_DRAW) {\n        anim_idx = LA_FLARES_THROW;\n    } else if (flare_frame < LF_FL_IGNITE) {\n        anim_idx = LA_FLARES_DRAW;\n    } else if (flare_frame < LF_FL_2_HOLD) {\n        anim_idx = LA_FLARES_IGNITE;\n    } else {\n        anim_idx = LA_FLARES_IDLE;\n    }\n\n    const OBJECT *const obj = Object_Get(O_LARA_FLARE);\n    const ANIM *const anim = Object_GetAnim(obj, anim_idx);\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->left_arm.anim_num = obj->anim_idx + anim_idx;\n    lara_info->left_arm.frame_base = anim->frame_ptr;\n}\n\nstatic bool M_CanUseFlareControl(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->current_anim_state == LS(LS_PICKUP)) {\n        const LARA_TRX_ANIMATION anim = LA_U(Item_GetRelativeAnim(lara_item));\n        return anim != LA_CROUCH_PICKUP && anim != LA_CRAWL_PICKUP;\n    }\n    return Lara_Vehicle_IsMounted() || Lara_HasState(m_HoldStates);\n}\n\nstatic void M_ControlArmless(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (M_CanUseFlareControl()) {\n        if (!lara_info->flare.control) {\n            lara_info->left_arm.frame_num = LF_FL_2_HOLD;\n            lara_info->flare.control = true;\n        } else if (lara_info->left_arm.frame_num != LF_FL_HOLD) {\n            lara_info->left_arm.frame_num++;\n            if (lara_info->left_arm.frame_num == LF_FL_END) {\n                lara_info->left_arm.frame_num = LF_FL_HOLD;\n            }\n        }\n    } else {\n        lara_info->flare.control = false;\n    }\n\n    M_ControlInHand();\n    M_SetArm(lara_info->left_arm.frame_num);\n}\n\nstatic void M_ControlBusyHands(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->flare.control = M_CanUseFlareControl();\n    M_ControlInHand();\n    M_SetArm(lara_info->left_arm.frame_num);\n}\n\nstatic void M_UndrawMeshes(void)\n{\n    Lara_Skin_ClearEquipment(LM_HAND_L);\n}\n\nvoid Lara_Flare_Control(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->gun_status == LGS_ARMLESS) {\n        M_ControlArmless();\n    } else if (lara_info->gun_status == LGS_HANDS_BUSY) {\n        M_ControlBusyHands();\n    }\n}\n\nvoid Lara_Flare_Draw(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    if (lara_item->current_anim_state == LS(LS_FLARE_PICKUP)\n        || lara_item->current_anim_state == LS(LS_PICKUP)) {\n        M_ControlInHand();\n        lara_info->flare.control = false;\n        lara_info->left_arm.frame_num = LF_FL_2_HOLD - 2;\n        M_SetArm(lara_info->left_arm.frame_num);\n        return;\n    }\n\n    int32_t frame_num = lara_info->left_arm.frame_num + 1;\n    lara_info->flare.control = true;\n\n    if (frame_num < LF_FL_DRAW || frame_num > LF_FL_2_HOLD - 1) {\n        frame_num = LF_FL_DRAW;\n    } else if (frame_num == LF_FL_DRAW_GOT_IT) {\n        Lara_Flare_DrawMeshes();\n        if (!Game_IsBonusFlagSet(GBF_NGPLUS)) {\n            Inv_RemoveItem(O_FLAREBOX_ITEM);\n        }\n    } else if (frame_num >= LF_FL_IGNITE && frame_num <= LF_FL_2_HOLD - 2) {\n        if (frame_num == LF_FL_IGNITE) {\n            lara_info->flare.age = 0;\n        }\n        M_ControlInHand();\n    } else if (frame_num == LF_FL_2_HOLD - 1) {\n        M_InitialiseState();\n        M_ControlInHand();\n        frame_num = LF_FL_HOLD;\n    }\n\n    lara_info->left_arm.frame_num = frame_num;\n    M_SetArm(frame_num);\n}\n\nvoid Lara_Flare_Undraw(void)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    int16_t frame_num_1 = lara_info->left_arm.frame_num;\n    int16_t frame_num_2 = lara_info->flare.frame_num;\n    const bool is_mounted = Lara_Vehicle_IsMounted();\n\n    lara_info->flare.control = true;\n\n    if (lara_item->goal_anim_state == LS(LS_STOP) && !is_mounted) {\n        if (Item_TestAnimEqual(lara_item, LA(LA_STAND_IDLE))) {\n            int16_t throw_frame = frame_num_1;\n            if (throw_frame < LF_FL_THROW || throw_frame >= LF_FL_DRAW) {\n                throw_frame = LF_FL_THROW;\n            }\n            Item_SwitchToAnim(lara_item, LA(LA_FLARE_THROW), throw_frame);\n            lara_info->flare.frame_num = lara_item->frame_num;\n            frame_num_2 = lara_item->frame_num;\n            frame_num_1 = throw_frame;\n        }\n\n        if (Item_TestAnimEqual(lara_item, LA(LA_FLARE_THROW))) {\n            lara_info->flare.control = false;\n            const OBJECT *const obj = Object_Get(O_LARA);\n            const ANIM *const anim = Object_GetAnim(obj, LA(LA_FLARE_THROW));\n            if (frame_num_2 >= anim->frame_base + LF_FL_THROW_FT - 1) {\n                lara_info->gun_type = lara_info->last_gun_type;\n                lara_info->request_gun_type = lara_info->last_gun_type;\n                lara_info->gun_status = LGS_ARMLESS;\n                Gun_InitialiseNewWeapon();\n                lara_info->target = nullptr;\n                lara_info->right_arm.lock = 0;\n                lara_info->left_arm.lock = 0;\n                Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n                lara_info->flare.frame_num = lara_item->frame_num;\n                lara_item->current_anim_state = LS(LS_STOP);\n                lara_item->goal_anim_state = LS(LS_STOP);\n                return;\n            }\n            lara_info->flare.frame_num = frame_num_2 + 1;\n        }\n    } else if (lara_item->current_anim_state == LS(LS_STOP) && !is_mounted) {\n        Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n    }\n\n    if (frame_num_1 == LF_FL_HOLD) {\n        frame_num_1 = LF_FL_THROW;\n    } else if (frame_num_1 >= LF_FL_IGNITE && frame_num_1 < LF_FL_2_HOLD) {\n        frame_num_1++;\n        if (frame_num_1 == LF_FL_2_HOLD - 1) {\n            frame_num_1 = LF_FL_THROW;\n        }\n    } else if (frame_num_1 >= LF_FL_THROW && frame_num_1 < LF_FL_DRAW) {\n        frame_num_1++;\n        if (frame_num_1 == LF_FL_THROW_RELEASE) {\n            Lara_Flare_Dispose(true);\n        } else if (frame_num_1 == LF_FL_DRAW) {\n            frame_num_1 = 0;\n            lara_info->gun_type = lara_info->last_gun_type;\n            lara_info->request_gun_type = lara_info->last_gun_type;\n            lara_info->gun_status = LGS_ARMLESS;\n            Gun_InitialiseNewWeapon();\n            lara_info->target = nullptr;\n            lara_info->flare.control = false;\n            lara_info->right_arm.lock = 0;\n            lara_info->left_arm.lock = 0;\n            lara_info->flare.frame_num = 0;\n        }\n    } else if (frame_num_1 >= LF_FL_2_HOLD && frame_num_1 < LF_FL_END) {\n        frame_num_1++;\n        if (frame_num_1 == LF_FL_END) {\n            frame_num_1 = LF_FL_THROW;\n        }\n    }\n\n    if (frame_num_1 >= LF_FL_THROW && frame_num_1 < LF_FL_THROW_RELEASE) {\n        M_ControlInHand();\n    }\n\n    lara_info->left_arm.frame_num = frame_num_1;\n    M_SetArm(frame_num_1);\n}\n\nvoid Lara_Flare_Dispose(const bool thrown)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        goto finish;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    item->object_id = O_FLARE_ITEM;\n    item->room_num = lara_item->room_num;\n\n    XYZ_32 vec = {\n        .x = -16,\n        .y = 32,\n        .z = 42,\n    };\n    Lara_GetJointAbsPosition(&vec, LM_HAND_L);\n\n    const SECTOR *const sector = Room_GetSector(vec, &item->room_num);\n    const int32_t height = Room_GetHeight(sector, vec);\n    if (height < vec.y) {\n        item->pos.x = lara_item->pos.x;\n        item->pos.y = vec.y;\n        item->pos.z = lara_item->pos.z;\n        item->rot.y = -lara_item->rot.y;\n        item->room_num = lara_item->room_num;\n    } else {\n        item->pos.x = vec.x;\n        item->pos.y = vec.y;\n        item->pos.z = vec.z;\n        if (thrown) {\n            item->rot.y = lara_item->rot.y;\n        } else {\n            item->rot.y = lara_item->rot.y - DEG_45;\n        }\n    }\n\n    Item_Initialise(item_num);\n\n    item->rot.z = 0;\n    item->rot.x = 0;\n    item->shade.value_1 = -1;\n\n    if (thrown) {\n        item->speed = lara_item->speed + 50;\n        item->fall_speed = lara_item->fall_speed - 50;\n    } else {\n        item->speed = lara_item->speed + 10;\n        item->fall_speed = lara_item->fall_speed + 50;\n    }\n\n    if (Flare_GenerateLight(item->pos, lara_info->flare.age)) {\n        FlareItem_SetAge(item, lara_info->flare.age, true);\n    } else {\n        FlareItem_SetAge(item, lara_info->flare.age, false);\n    }\n\n    Item_AddActive(item_num);\n    item->status = IS_ACTIVE;\n\nfinish:\n    M_UndrawMeshes();\n    if (!thrown) {\n        lara_info->flare.control = false;\n    }\n}\n\nbool Lara_Flare_IsMeshActive(void)\n{\n    const LARA_SKIN_EQUIPMENT *const equipment =\n        Lara_Skin_GetEquipment(LM_HAND_L);\n    return equipment->type == EQUIPMENT_TYPE_WEAPON\n        && equipment->data == LGT_FLARE;\n}\n\nvoid Lara_Flare_DrawMeshes(void)\n{\n    Lara_Skin_SetGunEquipment(LM_HAND_L, LGT_FLARE);\n}\n"
  },
  {
    "path": "src/trx/game/lara/flare.h",
    "content": "#pragma once\n\nbool Lara_Flare_IsMeshActive(void);\nvoid Lara_Flare_DrawMeshes(void);\n\nvoid Lara_Flare_Control(void);\nvoid Lara_Flare_Draw(void);\nvoid Lara_Flare_Undraw(void);\nvoid Lara_Flare_Dispose(bool thrown);\n"
  },
  {
    "path": "src/trx/game/lara/hair.c",
    "content": "#include <trx/game/lara/hair.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output/state.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n#define M_HAIR_SEGMENTS 6\n#define M_HAIR_SPHERES 5\n#define M_BONE_IDX(segment)                                                    \\\n    (segment == M_HAIR_SEGMENTS ? (segment - 2) : (segment - 1))\n\nstatic bool m_IsFirstHair;\nstatic SPHERE m_HairSpheres[M_HAIR_SPHERES];\nstatic XYZ_32 m_HairVelocity[M_HAIR_SEGMENTS + 1];\nstatic HAIR_SEGMENT m_HairSegments[M_HAIR_SEGMENTS + 1];\n\nstatic void M_CalculateSpheres(const ANIM_FRAME *const frame)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT *const lara_obj = Object_Get(O_LARA);\n\n    const LARA_POSE *const pose = Lara_Pose_Get();\n    const XYZ_16 *mesh_rots = pose != nullptr ? pose->rots : frame->mesh_rots;\n\n    Matrix_TranslateRel16(pose != nullptr ? pose->offset : frame->offset);\n    Matrix_Rot16(mesh_rots[LM_HIPS]);\n\n    Matrix_Push();\n    const OBJECT_MESH *mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HIPS);\n    Matrix_TranslateRel16(mesh->center);\n    m_HairSpheres[0].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[0].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[0].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[0].r = mesh->radius;\n    Matrix_Pop();\n\n    const ANIM_BONE *bone = Object_GetBone(lara_obj, 0);\n    Matrix_TranslateRel32(bone[LM_TORSO - 1].pos);\n    if (Lara_IsM16Active() && pose == nullptr) {\n        mesh_rots =\n            lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots;\n    }\n\n    Matrix_Rot16(mesh_rots[LM_TORSO]);\n    Matrix_Rot16(lara->interp.result.torso_rot);\n    Matrix_Push();\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_TORSO);\n    Matrix_TranslateRel16(mesh->center);\n    m_HairSpheres[1].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[1].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[1].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[1].r = mesh->radius;\n    Matrix_Pop();\n\n    Matrix_Push();\n    Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos);\n    Matrix_Rot16(mesh_rots[LM_UARM_R]);\n\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_R);\n    Matrix_TranslateRel16(mesh->center);\n    m_HairSpheres[3].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[3].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[3].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[3].r = mesh->radius * 3 / 2;\n    Matrix_Pop();\n\n    Matrix_Push();\n    Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos);\n    Matrix_Rot16(mesh_rots[LM_UARM_L]);\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_L);\n    Matrix_TranslateRel16(mesh->center);\n    m_HairSpheres[4].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[4].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[4].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[4].r = mesh->radius * 3 / 2;\n    Matrix_Pop();\n\n    Matrix_TranslateRel32(bone[LM_HEAD - 1].pos);\n    Matrix_Rot16(mesh_rots[LM_HEAD]);\n    Matrix_Rot16(lara->interp.result.head_rot);\n\n    Matrix_Push();\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HEAD);\n    Matrix_TranslateRel16(mesh->center);\n    m_HairSpheres[2].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[2].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[2].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[2].r = mesh->radius;\n    Matrix_Pop();\n\n    Matrix_TranslateRel32(Lara_Skin_GetBraidOffset());\n}\n\nstatic void M_CalculateSpheres_I(\n    const ANIM_FRAME *const frame_1, const ANIM_FRAME *const frame_2,\n    const int32_t frac, const int32_t rate)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT *const lara_obj = Object_Get(O_LARA);\n\n    const XYZ_16 *mesh_rots_1 = frame_1->mesh_rots;\n    const XYZ_16 *mesh_rots_2 = frame_2->mesh_rots;\n    Matrix_InitInterpolate(frac, rate);\n    Matrix_TranslateRel16_ID(frame_1->offset, frame_2->offset);\n    Matrix_Rot16_ID(mesh_rots_1[LM_HIPS], mesh_rots_2[LM_HIPS]);\n\n    Matrix_Push_I();\n    const OBJECT_MESH *mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HIPS);\n    Matrix_TranslateRel16_I(mesh->center);\n    Matrix_Interpolate();\n    m_HairSpheres[0].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[0].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[0].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[0].r = mesh->radius;\n    Matrix_Pop_I();\n\n    const ANIM_BONE *bone = Object_GetBone(lara_obj, 0);\n    Matrix_TranslateRel32_I(bone[LM_TORSO - 1].pos);\n    if (Lara_IsM16Active()) {\n        mesh_rots_1 =\n            lara->right_arm.frame_base[lara->right_arm.frame_num].mesh_rots;\n        mesh_rots_2 = mesh_rots_1;\n    }\n\n    Matrix_Rot16_ID(mesh_rots_1[LM_TORSO], mesh_rots_2[LM_TORSO]);\n    Matrix_Rot16_I(lara->interp.result.torso_rot);\n\n    Matrix_Push_I();\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_TORSO);\n    Matrix_TranslateRel16_I(mesh->center);\n    Matrix_Interpolate();\n    m_HairSpheres[1].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[1].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[1].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[1].r = mesh->radius;\n    Matrix_Pop_I();\n\n    Matrix_Push_I();\n    Matrix_TranslateRel32_I(bone[LM_UARM_R - 1].pos);\n    Matrix_Rot16_ID(mesh_rots_1[LM_UARM_R], mesh_rots_2[LM_UARM_R]);\n\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_R);\n    Matrix_TranslateRel16_I(mesh->center);\n    Matrix_Interpolate();\n    m_HairSpheres[3].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[3].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[3].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[3].r = mesh->radius * 3 / 2;\n    Matrix_Pop_I();\n\n    Matrix_Push_I();\n    Matrix_TranslateRel32_I(bone[LM_UARM_L - 1].pos);\n    Matrix_Rot16_ID(mesh_rots_1[LM_UARM_L], mesh_rots_2[LM_UARM_L]);\n\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_UARM_L);\n    Matrix_TranslateRel16_I(mesh->center);\n    Matrix_Interpolate();\n    m_HairSpheres[4].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[4].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[4].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[4].r = mesh->radius * 3 / 2;\n    Matrix_Pop_I();\n\n    Matrix_TranslateRel32_I(bone[LM_HEAD - 1].pos);\n    Matrix_Rot16_ID(mesh_rots_1[LM_HEAD], mesh_rots_2[LM_HEAD]);\n    Matrix_Rot16_I(lara->interp.result.head_rot);\n\n    Matrix_Push_I();\n    mesh = Object_GetMesh(lara_obj->mesh_idx + LM_HEAD);\n    Matrix_TranslateRel16_I(mesh->center);\n    Matrix_Interpolate();\n    m_HairSpheres[2].pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n    m_HairSpheres[2].pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n    m_HairSpheres[2].pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n    m_HairSpheres[2].r = mesh->radius;\n    Matrix_Pop_I();\n\n    Matrix_TranslateRel32_I(Lara_Skin_GetBraidOffset());\n    Matrix_Interpolate();\n}\n\nvoid Lara_Hair_Initialise(void)\n{\n    const ANIM_BONE *const bones = Lara_Skin_GetBraidBoneBase();\n    if (bones == nullptr) {\n        return;\n    }\n\n    m_IsFirstHair = true;\n    m_HairSegments[0].rot.x = -DEG_90;\n    m_HairSegments[0].rot.y = 0;\n\n    for (int32_t i = 1; i <= M_HAIR_SEGMENTS; i++) {\n        const ANIM_BONE *const bone = &bones[M_BONE_IDX(i)];\n        m_HairSegments[i].pos = bone->pos;\n        m_HairSegments[i].rot.x = -DEG_90;\n        m_HairSegments[i].rot.y = 0;\n        m_HairSegments[i].rot.z = 0;\n        m_HairVelocity[i - 1] = (XYZ_32) {};\n    }\n}\n\nvoid Lara_Hair_Control(const bool in_cutscene)\n{\n    if (!Lara_Hair_IsActive()) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    const ANIM_FRAME *frame_1;\n    const ANIM_FRAME *frame_2;\n    int32_t frac;\n    int32_t rate;\n    const ANIM_FRAME *const hit_frame = Lara_GetHitFrame(lara_item);\n    if (!in_cutscene && hit_frame != nullptr) {\n        frame_1 = hit_frame;\n        frac = 0;\n    } else {\n        ANIM_FRAME *frmptr[2];\n        frac = Item_GetFrames(lara_item, frmptr, &rate);\n        frame_1 = frmptr[0];\n        frame_2 = frmptr[1];\n    }\n\n    Matrix_PushUnit();\n    Matrix_TranslateSet32(lara_item->pos);\n    Matrix_Rot16(lara_item->rot);\n\n    if (frac == 0 || Lara_Pose_Get() != nullptr) {\n        M_CalculateSpheres(frame_1);\n    } else {\n        M_CalculateSpheres_I(frame_1, frame_2, frac, rate);\n    }\n\n    const XYZ_32 pos = {\n        .x = g_MatrixPtr->_03 >> W2V_SHIFT,\n        .y = g_MatrixPtr->_13 >> W2V_SHIFT,\n        .z = g_MatrixPtr->_23 >> W2V_SHIFT,\n    };\n    Matrix_Pop();\n\n    const ANIM_BONE *const bones = Lara_Skin_GetBraidBoneBase();\n\n    HAIR_SEGMENT *const fs = &m_HairSegments[0];\n    fs->pos = pos;\n\n    if (m_IsFirstHair) {\n        m_IsFirstHair = false;\n        for (int32_t i = 1; i <= M_HAIR_SEGMENTS; i++) {\n            const ANIM_BONE *const bone = &bones[M_BONE_IDX(i)];\n            const HAIR_SEGMENT *const ps = &m_HairSegments[i - 1];\n            HAIR_SEGMENT *const s = &m_HairSegments[i];\n\n            Matrix_PushUnit();\n            Matrix_TranslateSet32(ps->pos);\n            Matrix_RotY(ps->rot.y);\n            Matrix_RotX(ps->rot.x);\n            Matrix_TranslateRel32(bone->pos);\n\n            s->pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n            s->pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n            s->pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n\n            Matrix_Pop();\n        }\n        return;\n    }\n\n    int16_t room_num = lara_item->room_num;\n    int32_t water_height;\n    if (in_cutscene) {\n        water_height = NO_HEIGHT;\n    } else {\n        water_height = Room_GetWaterHeight(\n            (XYZ_32) {\n                lara_item->pos.x\n                    + (frame_1->bounds.min.x + frame_1->bounds.max.x) / 2,\n                lara_item->pos.y\n                    + (frame_1->bounds.max.y + frame_1->bounds.min.y) / 2,\n                lara_item->pos.z\n                    + (frame_1->bounds.max.z + frame_1->bounds.min.z) / 2,\n            },\n            room_num);\n    }\n\n    const SECTOR *const sector = Room_GetSector(fs->pos, &room_num);\n    int32_t height = Room_GetHeight(sector, fs->pos);\n    if (height < fs->pos.y) {\n        height = lara_item->floor;\n    }\n\n    const XZ_32 smoke_wind = Sparks_GetSmokeWind();\n    const int32_t hair_wind_z = Sparks_GetHairWindZ();\n\n    for (int32_t i = 1; i <= M_HAIR_SEGMENTS; i++) {\n        HAIR_SEGMENT *const ps = &m_HairSegments[i - 1];\n        HAIR_SEGMENT *const s = &m_HairSegments[i];\n\n        m_HairVelocity[0] = s->pos;\n\n        s->pos.x += m_HairVelocity[i].x * 3 / 4;\n        s->pos.y += m_HairVelocity[i].y * 3 / 4;\n        s->pos.z += m_HairVelocity[i].z * 3 / 4;\n\n        if (g_TRVersion == 3) {\n            if (lara_info->water_status == LWS_ABOVE_WATER\n                && Room_Get(room_num)->flags.wind) {\n                s->pos.x += smoke_wind.x;\n                s->pos.z += smoke_wind.z;\n            }\n\n            if (water_height == NO_HEIGHT || s->pos.y < water_height) {\n                s->pos.y += 10;\n                if (water_height != NO_HEIGHT && s->pos.y > water_height) {\n                    s->pos.y = water_height;\n                }\n            }\n\n            if (s->pos.y > height) {\n                s->pos.x = m_HairVelocity[0].x;\n                if (s->pos.y - height <= STEP_L) {\n                    s->pos.y = height;\n                }\n                s->pos.z = m_HairVelocity[0].z;\n            }\n        } else {\n            switch (lara_info->water_status) {\n            case LWS_ABOVE_WATER:\n                s->pos.y += 10;\n                if (water_height != NO_HEIGHT && s->pos.y > water_height) {\n                    s->pos.y = water_height;\n                } else if (s->pos.y > height) {\n                    s->pos.y = height;\n                } else {\n                    s->pos.z += hair_wind_z;\n                }\n                break;\n\n            case LWS_UNDERWATER:\n            case LWS_SURFACE:\n            case LWS_WADE:\n                CLAMP(s->pos.y, water_height, height);\n                break;\n\n            default:\n                break;\n            }\n        }\n\n        for (int32_t j = 0; j < M_HAIR_SPHERES; j++) {\n            const SPHERE *const sphere = &m_HairSpheres[j];\n            const int32_t dx = s->pos.x - sphere->pos.x;\n            const int32_t dy = s->pos.y - sphere->pos.y;\n            const int32_t dz = s->pos.z - sphere->pos.z;\n            int32_t dist = SQUARE(dz) + SQUARE(dy) + SQUARE(dx);\n            if (dist < SQUARE(sphere->r)) {\n                dist = Math_Sqrt(dist);\n                CLAMPL(dist, 1);\n                s->pos.x = sphere->pos.x + sphere->r * dx / dist;\n                s->pos.y = sphere->pos.y + sphere->r * dy / dist;\n                s->pos.z = sphere->pos.z + sphere->r * dz / dist;\n            }\n        }\n\n        const int32_t dx = s->pos.x - ps->pos.x;\n        const int32_t dz = s->pos.z - ps->pos.z;\n        const int32_t distance = Math_Sqrt(SQUARE(dx) + SQUARE(dz));\n        ps->rot.y = Math_Atan(dz, dx);\n        ps->rot.x = -Math_Atan(distance, s->pos.y - ps->pos.y);\n\n        Matrix_PushUnit();\n        Matrix_TranslateSet32(ps->pos);\n        Matrix_RotY(ps->rot.y);\n        Matrix_RotX(ps->rot.x);\n\n        const ANIM_BONE *const bone = &bones[M_BONE_IDX(i)];\n        Matrix_TranslateRel32(bone->pos);\n\n        s->pos.x = g_MatrixPtr->_03 >> W2V_SHIFT;\n        s->pos.y = g_MatrixPtr->_13 >> W2V_SHIFT;\n        s->pos.z = g_MatrixPtr->_23 >> W2V_SHIFT;\n\n        m_HairVelocity[i].x = s->pos.x - m_HairVelocity[0].x;\n        m_HairVelocity[i].y = s->pos.y - m_HairVelocity[0].y;\n        m_HairVelocity[i].z = s->pos.z - m_HairVelocity[0].z;\n\n        Matrix_Pop();\n    }\n}\n\nvoid Lara_Hair_Draw(void)\n{\n    if (!Lara_Hair_IsActive()) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t mesh_idx = Lara_Skin_GetBraidMeshIdx();\n\n    for (int32_t i = 0; i < M_HAIR_SEGMENTS; i++) {\n        const HAIR_SEGMENT *const s = &m_HairSegments[i];\n        Matrix_Push();\n        Matrix_TranslateAbs32(s->interp.result.pos);\n        Matrix_RotY(s->interp.result.rot.y);\n        Matrix_RotX(s->interp.result.rot.x);\n\n        Output_PushTintOverride(Lara_GetMeshTint((GAME_VECTOR) {\n            .pos = s->interp.result.pos, .room_num = lara_item->room_num }));\n        Object_DrawMesh(mesh_idx + i, CLIP_FULLY_VISIBLE, false);\n        Output_PopTintOverride();\n        Matrix_Pop();\n    }\n}\n\nbool Lara_Hair_IsActive(void)\n{\n    return g_Config.visuals.enable_braid && Object_Get(O_LARA)->loaded\n        && Lara_Skin_IsBraidSupported();\n}\n\nint32_t Lara_Hair_GetSegmentCount(void)\n{\n    return M_HAIR_SEGMENTS;\n}\n\nHAIR_SEGMENT *Lara_Hair_GetSegment(const int32_t n)\n{\n    return &m_HairSegments[n];\n}\n"
  },
  {
    "path": "src/trx/game/lara/hair.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/objects/types.h>\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_16 rot;\n    struct {\n        struct {\n            XYZ_32 pos;\n            XYZ_16 rot;\n        } result, prev;\n    } interp;\n} HAIR_SEGMENT;\n\nvoid Lara_Hair_Initialise(void);\nbool Lara_Hair_IsActive(void);\nvoid Lara_Hair_Control(bool in_cutscene);\nvoid Lara_Hair_Draw(void);\n\nint32_t Lara_Hair_GetSegmentCount(void);\nHAIR_SEGMENT *Lara_Hair_GetSegment(int32_t n);\n"
  },
  {
    "path": "src/trx/game/lara/look.c",
    "content": "#include <trx/game/lara/look.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n\nstatic const LARA_TRX_STATE m_StopStates[] = {\n    LS_STOP,\n    LS_SURF_TREAD,\n    LS_POSE,\n    LS_TRX_INVALID,\n};\n\nstatic const LARA_TRX_STATE m_BlockingStates[] = {\n    // clang-format off\n    LS_JUMP_RIGHT,\n    LS_JUMP_LEFT,\n    LS_SPLAT,\n    LS_STEP_RIGHT,\n    LS_STEP_LEFT,\n    LS_PUSH_BLOCK,\n    LS_PULL_BLOCK,\n    LS_PICKUP,\n    LS_FLARE_PICKUP,\n    LS_SWITCH_ON,\n    LS_SWITCH_OFF,\n    LS_USE_KEY,\n    LS_USE_PUZZLE,\n    LS_NEUTRAL_ROLL,\n    LS_TRX_INVALID,\n    // clang-format on\n};\n\nstatic const LARA_EXTRA_STATE m_PermittedExtraStates[] = {\n    // clang-format off\n    LS_EXTRA_BREATH,\n    LS_EXTRA_AIRLOCK,\n    (LARA_EXTRA_STATE)-1,\n    // clang-format on\n};\n\nstatic void M_Reset(void)\n{\n    if (g_Camera.type == CAM_LOOK) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const CAMERA_LOOK_SETTINGS *const look = Camera_GetLookSettings(false);\n\n    if (lara->head_rot.x <= -look->head_turn\n        || lara->head_rot.x >= look->head_turn) {\n        lara->head_rot.x -= lara->head_rot.x / 8;\n    } else {\n        lara->head_rot.x = 0;\n    }\n\n    if (lara->head_rot.y <= -look->head_turn\n        || lara->head_rot.y >= look->head_turn) {\n        lara->head_rot.y += lara->head_rot.y / -8;\n    } else {\n        lara->head_rot.y = 0;\n    }\n\n    lara->torso_rot.x = lara->head_rot.x;\n    lara->torso_rot.y = lara->head_rot.y;\n}\n\nstatic bool M_IsLaraIdle(void)\n{\n    const ITEM *const vehicle = Lara_Vehicle_GetItem();\n    if (vehicle != nullptr) {\n        return vehicle->speed == 0;\n    }\n    return Lara_HasState(m_StopStates);\n}\n\nstatic bool M_IsStatePermitted(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->hit_points <= 0) {\n        return false;\n    }\n\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->extra_anim) {\n        return g_Config.gameplay.look_mode == LOOK_MODE_UNRESTRICTED\n            && Lara_HasExtraState(m_PermittedExtraStates);\n    }\n\n    return g_Config.gameplay.look_mode == LOOK_MODE_UNRESTRICTED\n        || !Lara_HasState(m_BlockingStates);\n}\n\nvoid Lara_Look_LeftRight(void)\n{\n    g_Camera.type = CAM_LOOK;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const CAMERA_LOOK_SETTINGS *const look =\n        Camera_GetLookSettings(lara->water_status == LWS_SURFACE);\n\n    if (g_Input.left) {\n        g_Input.left = 0;\n        if (lara->head_rot.y > look->min_head_rotation) {\n            lara->head_rot.y -= look->head_turn;\n        }\n    } else if (g_Input.right) {\n        g_Input.right = 0;\n        if (lara->head_rot.y < look->max_head_rotation) {\n            lara->head_rot.y += look->head_turn;\n        }\n    }\n\n    if (lara->gun_status != LGS_HANDS_BUSY && !Lara_Vehicle_IsMounted()) {\n        lara->torso_rot.y = lara->head_rot.y * look->torso_head_rot_y;\n    }\n}\n\nvoid Lara_Look_UpDown(void)\n{\n    g_Camera.type = CAM_LOOK;\n\n    if (g_Config.gameplay.enable_inverted_look) {\n        bool temp_forward;\n        SWAP2(g_Input.forward, g_Input.back, temp_forward);\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const CAMERA_LOOK_SETTINGS *const look =\n        Camera_GetLookSettings(lara->water_status == LWS_SURFACE);\n\n    if (g_Input.forward) {\n        g_Input.forward = 0;\n        if (lara->head_rot.x > look->min_head_tilt) {\n            lara->head_rot.x -= look->head_turn;\n        }\n    } else if (g_Input.back) {\n        g_Input.back = 0;\n        if (lara->head_rot.x < look->max_head_tilt) {\n            lara->head_rot.x += look->head_turn;\n        }\n    }\n\n    if (lara->gun_status != LGS_HANDS_BUSY) {\n        lara->torso_rot.x = lara->head_rot.x * look->torso_head_rot_x;\n    }\n}\n\nvoid Lara_Look_Update(void)\n{\n    if (g_Input.look && g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED\n        && !M_IsLaraIdle()) {\n        if (g_Camera.type == CAM_LOOK) {\n            g_Camera.type = CAM_CHASE;\n        }\n        M_Reset();\n        return;\n    }\n\n    if (g_Input.look && M_IsStatePermitted()) {\n        Lara_Look_LeftRight();\n    } else {\n        M_Reset();\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lara/look.h",
    "content": "#pragma once\n\nvoid Lara_Look_UpDown(void);\nvoid Lara_Look_LeftRight(void);\nvoid Lara_Look_Update(void);\n"
  },
  {
    "path": "src/trx/game/lara/mesh.c",
    "content": "#include <trx/game/lara/mesh.h>\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n\nstatic OBJECT_MESH *m_Meshes[LM_NUMBER_OF] = {};\n\nstatic LARA_GUN_TYPE M_DetermineHolsterGun(void)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->holsters_gun_type == LGT_UNARMED) {\n        if (lara_info->gun_type != LGT_UNARMED\n            && !Gun_IsRifleType(lara_info->gun_type)) {\n            return lara_info->gun_type;\n        } else if (Inv_RequestItem(O_PISTOL_ITEM)) {\n            return LGT_PISTOLS;\n        } else if (Inv_RequestItem(O_MAGNUM_ITEM)) {\n            return LGT_MAGNUMS;\n        } else if (Inv_RequestItem(O_AUTOS_ITEM)) {\n            return LGT_AUTOS;\n        } else if (Inv_RequestItem(O_DESERT_EAGLE_ITEM)) {\n            return LGT_DESERT_EAGLE;\n        } else if (Inv_RequestItem(O_UZI_ITEM)) {\n            return LGT_UZIS;\n        }\n    }\n    return lara_info->holsters_gun_type;\n}\n\nstatic LARA_GUN_TYPE M_DetermineBackGun(void)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->back_gun_type != LGT_UNARMED) {\n        return lara_info->back_gun_type;\n    }\n\n    if (Inv_RequestItem(O_SHOTGUN_ITEM)) {\n        return LGT_SHOTGUN;\n    } else if (Inv_RequestItem(O_M16_ITEM)) {\n        return LGT_M16;\n    } else if (Inv_RequestItem(O_MP5_ITEM)) {\n        return LGT_MP5;\n    } else if (Inv_RequestItem(O_GRENADE_GUN_ITEM)) {\n        return LGT_GRENADE;\n    } else if (Inv_RequestItem(O_ROCKET_GUN_ITEM)) {\n        return LGT_ROCKET;\n    } else if (Inv_RequestItem(O_HARPOON_ITEM)) {\n        return LGT_HARPOON;\n    }\n    return LGT_UNARMED;\n}\n\nstatic void M_InitialiseCutsceneLevel(void)\n{\n    Lara_Skin_SetGunEquipment(LM_THIGH_L, LGT_PISTOLS);\n    Lara_Skin_SetGunEquipment(LM_THIGH_R, LGT_PISTOLS);\n}\n\nstatic void M_InitialiseNormalLevel(const GF_LEVEL *const level)\n{\n    const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n\n    const LARA_GUN_TYPE holster_gun = M_DetermineHolsterGun();\n    if (holster_gun != LGT_UNARMED && holster_gun != LGT_FLARE) {\n        Gun_SetLaraHolsterLMesh(holster_gun);\n        Gun_SetLaraHolsterRMesh(holster_gun);\n    }\n\n    const LARA_GUN_TYPE back_gun = M_DetermineBackGun();\n    if (back_gun != LGT_UNARMED) {\n        Gun_SetLaraBackMesh(back_gun);\n    }\n\n    if (resume != nullptr && resume->equipped_gun_type == LGT_FLARE) {\n        Lara_Skin_SetGunEquipment(LM_HAND_L, LGT_FLARE);\n    }\n}\n\nvoid Lara_Mesh_Initialise(const GF_LEVEL *const level)\n{\n    const OBJECT *const skin_obj = Object_Get(O_LARA_SKIN);\n    if (skin_obj->loaded) {\n        OBJECT *const lara_obj = Object_Get(O_LARA);\n        lara_obj->mesh_idx = skin_obj->mesh_idx;\n    }\n\n    if (level->type == GFL_CUTSCENE) {\n        M_InitialiseCutsceneLevel();\n    } else {\n        M_InitialiseNormalLevel(level);\n    }\n}\n\nvoid Lara_Mesh_SwapSingle(const LARA_MESH mesh, const OBJECT_ID obj_id)\n{\n    const OBJECT *const obj = Object_Get(obj_id);\n    Lara_Mesh_Set(mesh, Object_GetMesh(obj->mesh_idx + mesh));\n}\n\nvoid Lara_Mesh_SwapAll(const OBJECT_ID obj_id)\n{\n    if (!Object_Get(obj_id)->loaded) {\n        return;\n    }\n\n    for (LARA_MESH mesh = LM_FIRST; mesh < LM_NUMBER_OF; mesh++) {\n        Lara_Mesh_SwapSingle(mesh, obj_id);\n    }\n}\n\nvoid Lara_Mesh_Set(const LARA_MESH mesh, OBJECT_MESH *const mesh_ptr)\n{\n    m_Meshes[mesh] = mesh_ptr;\n}\n\nOBJECT_MESH *Lara_Mesh_Get(const LARA_MESH mesh)\n{\n    return m_Meshes[mesh];\n}\n\nRGB_F Lara_GetMeshTint(const GAME_VECTOR pos)\n{\n    if (!g_Config.visuals.enable_responsive_mesh_tint || g_Camera.underwater) {\n        return Output_GetTint();\n    }\n\n    int16_t room_num = pos.room_num;\n    Room_GetSector(pos.pos, &room_num);\n    const int32_t water_height = Room_GetWaterHeight(pos.pos, room_num);\n\n    if (!Room_Get(room_num)->flags.underwater) {\n        return COLOR_RGB_F_WHITE;\n    } else if (water_height == NO_HEIGHT) {\n        return Output_GetWaterColor();\n    } else if (pos.y > water_height) {\n        return Output_GetWaterColor();\n    } else {\n        return COLOR_RGB_F_WHITE;\n    }\n}\n\nint32_t Lara_GetMeshIndex(const ITEM *const item, const int32_t mesh_idx)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const int32_t fallback = obj->mesh_idx + mesh_idx;\n\n    const OBJECT_MESH *const mesh = Lara_Mesh_Get(mesh_idx);\n    if (mesh == nullptr) {\n        return fallback;\n    }\n\n    const int32_t resolved = Object_GetMeshIndex(mesh);\n    if (resolved < 0) {\n        return fallback;\n    }\n\n    return resolved;\n}\n"
  },
  {
    "path": "src/trx/game/lara/mesh.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/objects.h>\n\nvoid Lara_Mesh_Initialise(const GF_LEVEL *level);\nvoid Lara_Mesh_SwapSingle(LARA_MESH mesh, OBJECT_ID obj_id);\nvoid Lara_Mesh_SwapAll(OBJECT_ID obj_id);\nvoid Lara_Mesh_Set(LARA_MESH mesh, OBJECT_MESH *mesh_ptr);\nOBJECT_MESH *Lara_Mesh_Get(LARA_MESH mesh);\nRGB_F Lara_GetMeshTint(GAME_VECTOR pos);\nint32_t Lara_GetMeshIndex(const ITEM *item, int32_t mesh_idx);\n"
  },
  {
    "path": "src/trx/game/lara/misc.c",
    "content": "#include <trx/game/lara/misc.h>\n\n#include <trx/config.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/level/settings.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects/effects/flame.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\nstatic void M_GetJointAbsPosition_I(\n    XYZ_32 *const vec, const ANIM_FRAME *const frame1,\n    const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const ITEM *const item = Lara_GetItem();\n    const OBJECT *obj = Object_Get(item->object_id);\n\n    Matrix_PushUnit();\n    Matrix_Rot16(item->rot);\n\n    const ANIM_BONE *const bone = Object_GetBone(obj, 0);\n    const XYZ_16 *mesh_rots_1 = frame1->mesh_rots;\n    const XYZ_16 *mesh_rots_2 = frame2->mesh_rots;\n    Matrix_InitInterpolate(frac, rate);\n\n    Matrix_TranslateRel16_ID(frame1->offset, frame2->offset);\n    Matrix_Rot16_ID(mesh_rots_1[LM_HIPS], mesh_rots_2[LM_HIPS]);\n\n    Matrix_TranslateRel32_I(bone[LM_TORSO - 1].pos);\n    Matrix_Rot16_ID(mesh_rots_1[LM_TORSO], mesh_rots_2[LM_TORSO]);\n    Matrix_Rot16_I(lara_info->torso_rot);\n\n    LARA_GUN_TYPE gun_type = LGT_UNARMED;\n    if (lara_info->gun_status == LGS_READY\n        || lara_info->gun_status == LGS_SPECIAL\n        || lara_info->gun_status == LGS_DRAW\n        || lara_info->gun_status == LGS_UNDRAW) {\n        gun_type = lara_info->gun_type;\n    }\n\n    if (lara_info->gun_type == LGT_FLARE) {\n        Matrix_Interpolate();\n        Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos);\n        if (lara_info->flare.control) {\n            const LARA_ARM *const arm = &lara_info->left_arm;\n            const ANIM *const anim = Anim_GetAnim(arm->anim_num);\n            mesh_rots_1 =\n                arm->frame_base[arm->frame_num - anim->frame_base].mesh_rots;\n        }\n        Matrix_Rot16(mesh_rots_1[LM_UARM_L]);\n\n        Matrix_TranslateRel32(bone[LM_LARM_L - 1].pos);\n        Matrix_Rot16(mesh_rots_1[LM_LARM_L]);\n\n        Matrix_TranslateRel32(bone[LM_HAND_L - 1].pos);\n        Matrix_Rot16(mesh_rots_1[LM_HAND_L]);\n    } else if (gun_type != LGT_UNARMED) {\n        Matrix_Interpolate();\n        Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos);\n\n        const LARA_ARM *const arm = &lara_info->right_arm;\n        const ANIM *const anim = Anim_GetAnim(arm->anim_num);\n        mesh_rots_1 = arm->frame_base[arm->frame_num].mesh_rots;\n        Matrix_Rot16(mesh_rots_1[LM_UARM_R]);\n\n        Matrix_TranslateRel32(bone[LM_LARM_R - 1].pos);\n        Matrix_Rot16(mesh_rots_1[LM_LARM_R]);\n\n        Matrix_TranslateRel32(bone[LM_HAND_R - 1].pos);\n        Matrix_Rot16(mesh_rots_1[LM_HAND_R]);\n    }\n\n    Matrix_TranslateRel32(*vec);\n    vec->x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n    vec->y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n    vec->z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n    Matrix_Pop();\n}\n\n// TODO: joint is ignored - this only works for hands.\nvoid Lara_GetJointAbsPosition(XYZ_32 *const vec, const LARA_MESH joint)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    ANIM_FRAME *frmptr[2] = { nullptr, nullptr };\n    if (lara_info->hit_direction < 0) {\n        int32_t rate;\n        const int32_t frac = Item_GetFrames(lara_item, frmptr, &rate);\n        if (frac != 0) {\n            M_GetJointAbsPosition_I(vec, frmptr[0], frmptr[1], frac, rate);\n            return;\n        }\n    }\n\n    const ANIM_FRAME *const hit_frame = Lara_GetHitFrame(lara_item);\n    const ANIM_FRAME *const frame_ptr =\n        hit_frame == nullptr ? frmptr[0] : hit_frame;\n\n    Matrix_PushUnit();\n    Matrix_Rot16(lara_item->rot);\n\n    const XYZ_16 *mesh_rots = frame_ptr->mesh_rots;\n    const OBJECT *const obj = Object_Get(lara_item->object_id);\n    const ANIM_BONE *bone = Object_GetBone(obj, 0);\n\n    Matrix_TranslateRel16(frame_ptr->offset);\n    Matrix_Rot16(mesh_rots[LM_HIPS]);\n\n    Matrix_TranslateRel32(bone[LM_TORSO - 1].pos);\n    Matrix_Rot16(mesh_rots[LM_TORSO]);\n    Matrix_Rot16(lara_info->torso_rot);\n\n    LARA_GUN_TYPE gun_type = LGT_UNARMED;\n    if (lara_info->gun_status == LGS_READY\n        || lara_info->gun_status == LGS_SPECIAL\n        || lara_info->gun_status == LGS_DRAW\n        || lara_info->gun_status == LGS_UNDRAW) {\n        gun_type = lara_info->gun_type;\n    }\n\n    if (lara_info->gun_type == LGT_FLARE) {\n        Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos);\n        if (lara_info->flare.control) {\n            const LARA_ARM *const arm = &lara_info->left_arm;\n            const ANIM *const anim = Anim_GetAnim(arm->anim_num);\n            mesh_rots =\n                arm->frame_base[arm->frame_num - anim->frame_base].mesh_rots;\n        }\n        Matrix_Rot16(mesh_rots[LM_UARM_L]);\n\n        Matrix_TranslateRel32(bone[LM_LARM_L - 1].pos);\n        Matrix_Rot16(mesh_rots[LM_LARM_L]);\n\n        Matrix_TranslateRel32(bone[LM_HAND_L - 1].pos);\n        Matrix_Rot16(mesh_rots[LM_HAND_L]);\n    } else if (gun_type != LGT_UNARMED) {\n        Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos);\n\n        const LARA_ARM *const arm = &lara_info->right_arm;\n        const ANIM *const anim = Anim_GetAnim(arm->anim_num);\n        mesh_rots = arm->frame_base[arm->frame_num].mesh_rots;\n        Matrix_Rot16(mesh_rots[LM_UARM_R]);\n\n        Matrix_TranslateRel32(bone[LM_LARM_R - 1].pos);\n        Matrix_Rot16(mesh_rots[LM_LARM_R]);\n\n        Matrix_TranslateRel32(bone[LM_HAND_R - 1].pos);\n        Matrix_Rot16(mesh_rots[LM_HAND_R]);\n    }\n\n    Matrix_TranslateRel32(*vec);\n    vec->x = lara_item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT);\n    vec->y = lara_item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT);\n    vec->z = lara_item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT);\n    Matrix_Pop();\n}\n\nvoid Lara_RefuseInteraction(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (!XYZ_32_AreEquivalent(\n            lara_info->interact_target.initial_pos, lara_item->pos)) {\n        lara_info->interact_target.initial_pos = lara_item->pos;\n        Sound_Effect(SFX_LARA_NO, &lara_item->pos, SPM_ALWAYS);\n    }\n}\n\nvoid Lara_TakeHit(ITEM *const lara_item, const int32_t dx, const int32_t dz)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const int16_t hit_angle = lara_item->rot.y + DEG_180 - Math_Atan(dz, dx);\n    lara_info->hit_direction = Math_GetDirection(hit_angle);\n    if (lara_info->hit_frame == 0) {\n        Sound_Effect(\n            g_TRVersion == 1 ? SFX_LARA_BODYSL : SFX_LARA_INJURY,\n            &lara_item->pos, SPM_NORMAL);\n    }\n    lara_info->hit_frame++;\n    if (lara_info->interact_target.is_moving\n        && lara_info->gun_status == LGS_HANDS_BUSY) {\n        lara_info->gun_status = LGS_ARMLESS;\n    }\n    lara_info->interact_target.is_moving = false;\n    lara_info->interact_target.item_num = NO_ITEM;\n    CLAMPG(lara_info->hit_frame, 34);\n}\n\nvoid Lara_TouchDeathSector(const GF_DEATH_TILE death_tile)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_item->hit_points < 0 || lara_info->water_status == LWS_CHEAT) {\n        return;\n    }\n\n    int16_t room_num = lara_item->room_num;\n    const XYZ_32 pos = { lara_item->pos.x, MAX_HEIGHT, lara_item->pos.z };\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    if (lara_item->floor != height) {\n        return;\n    }\n\n    if (g_Config.debug.enable_invulnerability) {\n        switch (death_tile) {\n        case GF_DEATH_TILE_RAPIDS:\n        case GF_DEATH_TILE_ELECTRIC:\n            Lara_CatchFire();\n            break;\n        case GF_DEATH_TILE_LAVA:\n            Lara_TouchLava();\n            break;\n        }\n        return;\n    }\n\n    lara_item->hit_points = -1;\n    lara_item->hit_status = true;\n\n    switch (death_tile) {\n    case GF_DEATH_TILE_RAPIDS:\n        Lara_RapidsDrown();\n        break;\n    case GF_DEATH_TILE_ELECTRIC:\n        lara_info->electric = 1;\n        break;\n    case GF_DEATH_TILE_LAVA:\n        Lara_TouchLava();\n        break;\n    }\n}\n\nvoid Lara_TouchLava(void)\n{\n    if (g_TRVersion == 3) {\n        Lara_CatchFire();\n        return;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->burn || lara_info->water_status != LWS_ABOVE_WATER) {\n        return;\n    }\n\n    const OBJECT *const obj = Object_Get(O_FLAME);\n    for (int32_t i = 0; i < 10; i++) {\n        const int16_t effect_num = Effect_Create(lara_item->room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *const effect = Effect_Get(effect_num);\n            effect->object_id = O_FLAME;\n            effect->frame_num = obj->mesh_count * Random_GetControl() / 0x7FFF;\n            effect->counter = -1 - 24 * Random_GetControl() / 0x7FFF;\n        }\n    }\n    lara_info->burn = true;\n}\n\nvoid Lara_RapidsDrown(void)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    Lara_SwitchToExtraState(LS_EXTRA_RAPIDS_DROWN);\n\n    lara_item->gravity = false;\n    lara_item->hit_points = -1;\n    lara_item->hit_status = true;\n    lara_item->fall_speed = 0;\n    lara_item->speed = 0;\n\n    lara_info->gun_type = LGT_UNARMED;\n}\n\nint32_t Lara_FloorFront(\n    const ITEM *const item, const int16_t ang, const int32_t dist)\n{\n    XYZ_32 pos = item->pos;\n    pos.y -= LARA_HEIGHT;\n    pos = XYZ_32_OffsetYaw(pos, ang, dist);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    int32_t height = Room_GetHeight(sector, pos);\n    if (height != NO_HEIGHT) {\n        height -= item->pos.y;\n        if (height > 0\n            && Room_GetPitSector(sector, pos.x, pos.z)->is_death_sector) {\n            return STEP_L * 2;\n        }\n    }\n    return height;\n}\n\nint32_t Lara_CeilingFront(\n    const ITEM *const item, const int16_t ang, const int32_t dist,\n    const int32_t item_height)\n{\n    const int32_t x = item->pos.x + ((dist * Math_Sin(ang)) >> W2V_SHIFT);\n    const int32_t y = item->pos.y - item_height;\n    const int32_t z = item->pos.z + ((dist * Math_Cos(ang)) >> W2V_SHIFT);\n    const XYZ_32 pos = { x, y, z };\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    int32_t height = Room_GetCeiling(sector, pos);\n    if (height != NO_HEIGHT) {\n        height += item_height - item->pos.y;\n    }\n    return height;\n}\n\nvoid Lara_UpdateRoomToHeight(const int32_t height)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    XYZ_32 pos = lara_item->pos;\n    pos.y += height;\n\n    int16_t room_num = lara_item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    lara_item->floor = Room_GetHeight(sector, pos);\n\n    const int16_t item_num = Item_GetIndex(lara_item);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nint32_t Lara_GetWaterDepth(\n    const int32_t x, const int32_t y, const int32_t z, int16_t room_num)\n{\n    const ROOM *room = Room_Get(room_num);\n    const SECTOR *sector;\n\n    while (true) {\n        int32_t z_sector = (z - room->pos.z) >> WALL_SHIFT;\n        int32_t x_sector = (x - room->pos.x) >> WALL_SHIFT;\n\n        if (z_sector <= 0) {\n            z_sector = 0;\n            if (x_sector < 1) {\n                x_sector = 1;\n            } else if (x_sector > room->size.x - 2) {\n                x_sector = room->size.x - 2;\n            }\n        } else if (z_sector >= room->size.z - 1) {\n            z_sector = room->size.z - 1;\n            if (x_sector < 1) {\n                x_sector = 1;\n            } else if (x_sector > room->size.x - 2) {\n                x_sector = room->size.x - 2;\n            }\n        } else if (x_sector < 0) {\n            x_sector = 0;\n        } else if (x_sector >= room->size.x) {\n            x_sector = room->size.x - 1;\n        }\n\n        sector = Room_GetUnitSector(room, x_sector, z_sector);\n        if (sector->portal_room.wall == NO_ROOM) {\n            break;\n        }\n        room_num = sector->portal_room.wall;\n        room = Room_Get(room_num);\n    }\n\n    if (room->flags.underwater || room->flags.swamp) {\n        while (sector->portal_room.sky != NO_ROOM) {\n            room = Room_Get(sector->portal_room.sky);\n            if (!room->flags.underwater && !room->flags.swamp) {\n                const XYZ_32 pos = { x, y, z };\n                const int32_t water_height = Room_GetWaterHeight(pos, room_num);\n                sector = Room_GetSector(pos, &room_num);\n                return Room_GetHeight(sector, pos) - water_height;\n            }\n            sector = Room_GetWorldSector(room, x, z);\n        }\n        return 0x7FFF;\n    }\n\n    while (sector->portal_room.pit != NO_ROOM) {\n        room = Room_Get(sector->portal_room.pit);\n        if (room->flags.underwater || room->flags.swamp) {\n            const XYZ_32 pos = { x, y, z };\n            const int32_t water_height = Room_GetWaterHeight(pos, room_num);\n            sector = Room_GetSector(pos, &room_num);\n            return Room_GetHeight(sector, pos) - water_height;\n        }\n        sector = Room_GetWorldSector(room, x, z);\n    }\n    return NO_HEIGHT;\n}\n\nbool Lara_IsM16Active(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara->gun_item_num == NO_ITEM || lara_item->hit_points <= 0\n        || (lara->gun_type != LGT_M16 && lara->gun_type != LGT_MP5)) {\n        return false;\n    }\n\n    const ITEM *const item = Item_Get(lara->gun_item_num);\n    return item->current_anim_state == 0 || item->current_anim_state == 2\n        || item->current_anim_state == 4;\n}\n\nvoid Lara_CatchFireEx(const FLAME_TYPE type)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->burn || lara_info->water_status == LWS_CHEAT) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int16_t effect_num = Effect_Create(lara_item->room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    if (g_TRVersion == 3) {\n        // TR3 effects use Collide_GetJointAbsPosition but only every x frames,\n        // which lets Lara briefly catch fire even if she touches liquids\n        // (for example, when running into the boiling water in Tony's room).\n        effect->pos = (XYZ_32) {};\n    } else {\n        effect->pos = lara_item->pos;\n    }\n    effect->frame_num = g_TRVersion == 3 ? type : 0;\n    effect->object_id = O_FLAME;\n    effect->counter = -1;\n    lara_info->burn = true;\n}\n\nvoid Lara_CatchFire(void)\n{\n    Lara_CatchFireEx(FLAME_SMALL);\n}\n\nvoid Lara_Extinguish(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    lara_info->electric = 0;\n\n    if (!lara_info->burn) {\n        return;\n    }\n\n    lara_info->burn = false;\n\n    // put out flame objects\n    int16_t effect_num = Effect_GetActiveNum();\n    while (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        const int16_t next_effect_num = effect->next_active;\n        if (effect->object_id == O_FLAME && effect->counter < 0) {\n            effect->counter = 0;\n            Effect_Kill(effect_num);\n        }\n        effect_num = next_effect_num;\n    }\n}\n\nbool Lara_HasState(const LARA_TRX_STATE *const test_arr)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->extra_anim) {\n        return false;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    for (int32_t i = 0; test_arr[i] != LS_TRX_INVALID; i++) {\n        if (test_arr[i] == LS_U(lara_item->current_anim_state)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nbool Lara_HasExtraState(const LARA_EXTRA_STATE *const test_arr)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (!lara_info->extra_anim) {\n        return false;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    for (int32_t i = 0; test_arr[i] != (LARA_EXTRA_STATE)-1; i++) {\n        if (test_arr[i] == (LARA_EXTRA_STATE)lara_item->current_anim_state) {\n            return true;\n        }\n    }\n    return false;\n}\n\nvoid Lara_SwitchToExtraState(const LARA_EXTRA_STATE goal_state)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    Item_SwitchToObjAnim(lara_item, LS_EXTRA_BREATH, 0, O_LARA_EXTRA);\n    lara_item->current_anim_state = LS_EXTRA_BREATH;\n    lara_item->goal_anim_state = goal_state;\n    Item_Animate(lara_item);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->gun_status = LGS_HANDS_BUSY;\n    lara->hit_direction = DIR_UNKNOWN;\n    lara->extra_anim = true;\n}\n"
  },
  {
    "path": "src/trx/game/lara/misc.h",
    "content": "#pragma once\n\n#include <trx/game/collision.h>\n#include <trx/game/game_flow/enum.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/objects/effects/flame.h>\n\nvoid Lara_GetJointAbsPosition(XYZ_32 *vec, LARA_MESH joint);\nvoid Lara_RefuseInteraction(void);\nvoid Lara_TakeHit(ITEM *lara_item, int32_t dx, int32_t dz);\nvoid Lara_Extinguish(void);\nvoid Lara_TouchLava(void);\nvoid Lara_TouchDeathSector(GF_DEATH_TILE death_tile);\nvoid Lara_RapidsDrown(void);\n\nint32_t Lara_FloorFront(const ITEM *item, int16_t ang, int32_t dist);\nint32_t Lara_CeilingFront(\n    const ITEM *item, int16_t ang, int32_t dist, int32_t item_height);\nvoid Lara_CatchFireEx(FLAME_TYPE type);\nvoid Lara_CatchFire(void);\n\nvoid Lara_UpdateRoomToHeight(int32_t height);\nint32_t Lara_GetWaterDepth(int32_t x, int32_t y, int32_t z, int16_t room_num);\n\n// Returns true if Lara has the M16 equipped and is in either anim state: 0\n// (start aim); 2 (firing); or 4 (stopping firing).\nbool Lara_IsM16Active(void);\nbool Lara_HasState(const LARA_TRX_STATE *test_arr);\nbool Lara_HasExtraState(const LARA_EXTRA_STATE *test_arr);\nvoid Lara_SwitchToExtraState(LARA_EXTRA_STATE goal_state);\n"
  },
  {
    "path": "src/trx/game/lara/pose.c",
    "content": "#include <trx/game/lara/pose.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items.h>\n#include <trx/game/lara/hair.h>\n#include <trx/game/objects.h>\n#include <trx/game/shell.h>\n\n#define M_NO_POSE (-1)\n\nstatic VECTOR *m_Poses = nullptr;\nstatic int32_t m_ActivePose = M_NO_POSE;\n\nstatic void M_WarnWithJSONError(const JSON_READ_IO *const io)\n{\n    char warning_message[1024];\n    JSON_ReadIO_FormatError(\n        io, false, warning_message, sizeof(warning_message));\n    LOG_WARNING(\"%s\", warning_message);\n}\n\nstatic bool M_LoadPose(JSON_READ_IO *const io, LARA_POSE *const pose)\n{\n    JSON_MUST(JSON_READ(io, \"offset\", &pose->offset));\n    JSON_MUST(JSON_PUSH(io, \"rots\"));\n    const int32_t rot_count = JSON_ARRAY_LEN(io);\n    if (rot_count < 0) {\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n    if (rot_count != LM_NUMBER_OF) {\n        JSON_ReadIO_SetError(\n            io, \"expected exactly %d rotations, got %d\", LM_NUMBER_OF,\n            rot_count);\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n        JSON_MUST(JSON_READ_A(io, i, &pose->rots[i]));\n    }\n\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadPosesArray(JSON_READ_IO *const io, VECTOR *const poses)\n{\n    const int32_t pose_count = JSON_ARRAY_LEN(io);\n    if (pose_count < 0) {\n        JSON_FAIL();\n    }\n\n    for (int32_t i = 0; i < pose_count; i++) {\n        JSON_MUST(JSON_PUSH_INDEX(io, i));\n\n        LARA_POSE pose = {};\n        if (JSON_SHOULD(M_LoadPose(io, &pose))) {\n            Vector_Add(poses, &pose);\n        }\n        JSON_MUST(JSON_POP(io));\n    }\n\n    JSON_FINISH();\n}\n\nstatic void M_LoadPoses(void)\n{\n    m_Poses = Vector_Create(sizeof(LARA_POSE));\n    ASSERT(m_Poses != nullptr);\n\n    const char *const poses_path =\n        TRXPath_TryResolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, \"poses.json5\");\n    if (poses_path == nullptr) {\n        return;\n    }\n    JSON_VALUE *const doc = JSONFile_Read(poses_path);\n    if (doc == nullptr) {\n        return;\n    }\n\n    JSON_READ_IO *const io = JSON_ReadIO_Create(doc, 0, poses_path);\n    if (!M_LoadPosesArray(io, m_Poses)) {\n        M_WarnWithJSONError(io);\n    }\n    JSON_ReadIO_Destroy(io);\n    JSON_ValueFree(doc);\n}\n\nvoid Lara_Pose_Init(void)\n{\n    if (m_Poses == nullptr) {\n        M_LoadPoses();\n    }\n}\n\nvoid Lara_Pose_Shutdown(void)\n{\n    if (m_Poses != nullptr) {\n        Vector_Free(m_Poses);\n        m_Poses = nullptr;\n    }\n}\n\nbool Lara_Pose_IsAvailable(void)\n{\n    return m_Poses->count > 0 && Object_Get(O_LARA)->loaded\n        && GF_GetCurrentLevel()->type != GFL_CUTSCENE;\n}\n\nvoid Lara_Pose_Clear(void)\n{\n    if (m_ActivePose != M_NO_POSE) {\n        LOG_DEBUG(\"Clearing Lara's pose\");\n    }\n    m_ActivePose = M_NO_POSE;\n}\n\nvoid Lara_Pose_Cycle(const int32_t dir)\n{\n    if (!Lara_Pose_IsAvailable()) {\n        return;\n    }\n    if (m_ActivePose == M_NO_POSE) {\n        m_ActivePose = (dir > 0) ? 0 : m_Poses->count - 1;\n    } else {\n        m_ActivePose += dir;\n        m_ActivePose += m_Poses->count;\n        m_ActivePose %= m_Poses->count;\n    }\n\n    LOG_DEBUG(\"Active Lara pose: %d\", m_ActivePose);\n    Lara_Hair_Control(true);\n    Interpolation_CommitBraid();\n}\n\nconst LARA_POSE *Lara_Pose_Get(void)\n{\n    if (m_ActivePose == M_NO_POSE) {\n        return nullptr;\n    }\n    return Vector_Get(m_Poses, m_ActivePose);\n}\n"
  },
  {
    "path": "src/trx/game/lara/pose.h",
    "content": "#pragma once\n\n#include <trx/game/lara/enum.h>\n#include <trx/game/types.h>\n\ntypedef struct {\n    XYZ_16 offset;\n    XYZ_16 rots[LM_NUMBER_OF];\n} LARA_POSE;\n\nvoid Lara_Pose_Init();\nvoid Lara_Pose_Shutdown();\n\nbool Lara_Pose_IsAvailable(void);\nvoid Lara_Pose_Clear(void);\nvoid Lara_Pose_Cycle(int32_t dir);\nconst LARA_POSE *Lara_Pose_Get(void);\n"
  },
  {
    "path": "src/trx/game/lara/skin/common.c",
    "content": "#include <trx/game/lara/skin/common.h>\n\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gun.h>\n#include <trx/game/lara.h>\n#include <trx/version.h>\n\n#define M_NO_OUTFIT (-1)\n#define M_NO_MESH (-1)\n\nstatic LARA_SKIN_TYPE m_SkinType = LARA_SKIN_TYPE_DEFAULT;\nstatic bool m_HolstersVisible = true;\nstatic bool m_UseCombatFace = false;\nstatic LARA_GUN_TYPE m_HolsterType_L = LGT_UNARMED;\nstatic LARA_GUN_TYPE m_HolsterType_R = LGT_UNARMED;\nstatic LARA_SKIN_EQUIPMENT m_Equipment[LM_NUMBER_OF] = {};\n\nstatic inline const LARA_SKIN_OUTFIT *M_GetCurrentOutfit(void)\n{\n    if (!Lara_Skin_IsOutfitAvailable(m_SkinType)) {\n        m_SkinType = Lara_Skin_GetDefaultType();\n    }\n    return Lara_Skin_GetOutfit(m_SkinType);\n}\n\nstatic LARA_SKIN_TYPE M_ResolveOutfitTypeFromName(\n    const char *const outfit_name, const bool warn_on_invalid,\n    const char *const source)\n{\n    if (outfit_name == nullptr) {\n        return LARA_SKIN_TYPE_DEFAULT;\n    }\n\n    const LARA_SKIN_TYPE type = Lara_Skin_FindOutfitByName(outfit_name);\n    if (Lara_Skin_IsOutfitAvailable(type)) {\n        return type;\n    }\n\n    if (warn_on_invalid) {\n        LOG_WARNING(\n            \"Invalid outfit '%s' from %s; falling back to default\", outfit_name,\n            source);\n    }\n    return LARA_SKIN_TYPE_DEFAULT;\n}\n\nstatic LARA_SKIN_TYPE M_GetFallbackOutfitType(void)\n{\n    return Lara_Skin_GetDefaultType();\n}\n\nstatic void M_SetConfigOutfit(const char *const outfit_name)\n{\n    ASSERT(outfit_name != nullptr);\n    char *const old = g_Config.visuals.lara_outfit;\n    g_Config.visuals.lara_outfit = Memory_DupStr(outfit_name);\n    // Keep the old pointer alive until after the duplication so Config_Update\n    // can reliably detect a string change via pointer identity.\n    Memory_Free(old);\n}\n\nstatic LARA_SKIN_TYPE M_GetCurrentLevelOutfitType(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level == nullptr) {\n        return M_GetFallbackOutfitType();\n    }\n\n    const LARA_SKIN_TYPE level_type = M_ResolveOutfitTypeFromName(\n        level->lara_outfit, true, \"gameflow level setting\");\n    if (level_type != LARA_SKIN_TYPE_DEFAULT) {\n        return level_type;\n    }\n    return M_GetFallbackOutfitType();\n}\n\nstatic int32_t M_GetBraidDependentMeshIdx(\n    const LARA_MESH mesh_idx, const LARA_SKIN_OUTFIT *const outfit)\n{\n    if (mesh_idx != LM_TORSO && mesh_idx != LM_HEAD) {\n        return M_NO_MESH;\n    }\n\n    LARA_SKIN_EXTRA_MESH extra_id;\n    switch (outfit->braid.mode) {\n    case BRAID_MODE_TR1_HEAD_ONLY:\n        if (mesh_idx != LM_HEAD) {\n            return M_NO_MESH;\n        }\n        extra_id = EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD;\n        break;\n    case BRAID_MODE_TR1_FULL:\n        extra_id = mesh_idx == LM_TORSO ? EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO\n                                        : EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD;\n        break;\n    case BRAID_MODE_TR1_MAULED:\n        extra_id = mesh_idx == LM_TORSO ? EXTRA_MESH_TR1_BRAID_MAULED_TORSO\n                                        : EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD;\n        break;\n    case BRAID_MODE_TR1_GOLD:\n        extra_id = mesh_idx == LM_TORSO ? EXTRA_MESH_TR1_BRAID_GOLD_TORSO\n                                        : EXTRA_MESH_TR1_BRAID_GOLD_HEAD;\n        break;\n    default:\n        return M_NO_MESH;\n    }\n\n    const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA);\n    const int32_t offset = Lara_Skin_GetExtraMeshOffset(extra_id);\n    return extra_obj->mesh_idx + offset;\n}\n\nstatic int32_t M_GetNoHolsterMeshIdx(\n    const LARA_MESH mesh, const LARA_SKIN_OUTFIT *const outfit)\n{\n    if (m_HolstersVisible) {\n        return M_NO_MESH;\n    }\n\n    if (mesh != LM_THIGH_L && mesh != LM_THIGH_R) {\n        return M_NO_MESH;\n    }\n\n    const OBJECT *const obj = Object_Get(O_LARA_SKIN_SWAP_LEGS);\n    if (!obj->loaded) {\n        return M_NO_MESH;\n    }\n\n    const int32_t offset = mesh == LM_THIGH_L\n        ? outfit->no_holster_offsets.left\n        : outfit->no_holster_offsets.right;\n    if (offset == M_NO_MESH) {\n        return M_NO_MESH;\n    }\n\n    return obj->mesh_idx + offset;\n}\n\nstatic inline int32_t M_GetRelativeBraidOffset(void)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    if (!outfit->braid.enabled) {\n        return M_NO_MESH;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    int32_t offset = outfit->braid.mesh_offset;\n    if (outfit->is_reflective || (lara->mesh_effects & (1 << LM_HEAD)) != 0) {\n        offset = outfit->braid.gold_offset;\n    }\n\n    return offset;\n}\n\nstatic inline int32_t M_GetMeshIdx(\n    const LARA_MESH mesh, const LARA_SKIN_OUTFIT *const outfit)\n{\n    const OBJECT *const skin_obj = Object_Get(outfit->obj_id);\n    int32_t offset = M_NO_MESH;\n\n    if (g_Config.visuals.enable_braid) {\n        offset = M_GetBraidDependentMeshIdx(mesh, outfit);\n    }\n    if (offset == M_NO_MESH) {\n        offset = M_GetNoHolsterMeshIdx(mesh, outfit);\n    }\n    if (offset == M_NO_MESH) {\n        offset = skin_obj->mesh_idx + mesh;\n    }\n    return offset;\n}\n\nstatic inline void M_ApplyMeshIfValid(\n    const LARA_MESH mesh, const LARA_SKIN_OUTFIT *const outfit)\n{\n    const int32_t mesh_idx = M_GetMeshIdx(mesh, outfit);\n    if (mesh_idx != M_NO_MESH) {\n        Lara_Mesh_Set(mesh, Object_GetMesh(mesh_idx));\n    }\n}\n\nstatic int32_t M_GetCombatFaceMeshIdx(const LARA_SKIN_OUTFIT *const outfit)\n{\n    int32_t offset = outfit->combat_face_offset;\n    if (offset == M_NO_MESH) {\n        return M_NO_MESH;\n    }\n\n    if (g_Config.visuals.enable_braid) {\n        switch (outfit->braid.mode) {\n        case BRAID_MODE_TR1_HEAD_ONLY:\n        case BRAID_MODE_TR1_FULL:\n        case BRAID_MODE_TR1_MAULED:\n            offset =\n                Lara_Skin_GetExtraMeshOffset(EXTRA_MESH_TR1_BRAID_COMBAT_HEAD);\n            break;\n        default:\n            break;\n        }\n    }\n\n    const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA);\n    return extra_obj->mesh_idx + offset;\n}\n\nstatic const LARA_SKIN_OUTFIT *M_GetExtraOutfit(const LARA_EXTRA_STATE state)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    const LARA_SKIN_TYPE extra_type = outfit->extra_outfits[state];\n    if (extra_type == LARA_SKIN_TYPE_DEFAULT) {\n        return nullptr;\n    }\n\n    if (!Lara_Skin_IsOutfitAvailable(extra_type)) {\n        return nullptr;\n    }\n\n    return Lara_Skin_GetOutfit(extra_type);\n}\n\nstatic void M_SetEquipment(\n    const LARA_MESH mesh, const LARA_SKIN_EQUIPMENT_TYPE type,\n    const int32_t data, const int32_t offset)\n{\n    LARA_SKIN_EQUIPMENT *const equipment = &m_Equipment[mesh];\n    equipment->type = type;\n    equipment->data = data;\n    switch (type) {\n    case EQUIPMENT_TYPE_WEAPON:\n        const OBJECT *const gun_swap_obj = Object_Get(O_LARA_SKIN_SWAP_GUNS);\n        equipment->mesh = Object_GetMesh(gun_swap_obj->mesh_idx + offset);\n        break;\n    case EQUIPMENT_TYPE_EXTRA:\n        const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA);\n        equipment->mesh = Object_GetMesh(extra_obj->mesh_idx + offset);\n        break;\n    default:\n        equipment->mesh = nullptr;\n        break;\n    }\n}\n\nstatic void M_SetGunEquipment(\n    const LARA_MESH mesh, const LARA_GUN_TYPE gun_type,\n    const LARA_SKIN_OUTFIT *const outfit)\n{\n    const LARA_SKIN_MESH_MAP map = outfit->gun_map->mesh_offsets[gun_type];\n\n    int32_t offset = M_NO_MESH;\n    switch (mesh) {\n    case LM_THIGH_L:\n        offset = map.thigh.left;\n        break;\n    case LM_THIGH_R:\n        offset = map.thigh.right;\n        break;\n    case LM_HAND_L:\n        offset = map.hand.left;\n        break;\n    case LM_HAND_R:\n        offset = map.hand.right;\n        break;\n    case LM_TORSO:\n        offset = map.torso;\n        break;\n    default:\n        break;\n    }\n\n    if (offset == M_NO_MESH) {\n        Lara_Skin_ClearEquipment(mesh);\n    } else {\n        M_SetEquipment(mesh, EQUIPMENT_TYPE_WEAPON, gun_type, offset);\n    }\n}\n\nstatic void M_SetCombatFace(const bool enabled)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    int32_t mesh_idx = M_NO_MESH;\n    if (enabled) {\n        mesh_idx = M_GetCombatFaceMeshIdx(outfit);\n    } else {\n        mesh_idx = M_GetMeshIdx(LM_HEAD, outfit);\n    }\n\n    if (mesh_idx != M_NO_MESH) {\n        Lara_Mesh_Set(LM_HEAD, Object_GetMesh(mesh_idx));\n        m_UseCombatFace = enabled;\n    }\n}\n\nstatic void M_UpdateSunglasses(void)\n{\n    const SUNGLASSES_MODE mode = g_Config.visuals.sunglasses_mode;\n    if (mode == SUNGLASSES_MODE_OFF\n        || !M_GetCurrentOutfit()->supports_sunglasses) {\n        Lara_Skin_ClearEquipment(LM_HEAD);\n        return;\n    }\n\n    const LARA_SKIN_EXTRA_MESH mesh = mode == SUNGLASSES_MODE_OPAQUE\n        ? EXTRA_MESH_GLASSES_OPAQUE\n        : EXTRA_MESH_GLASSES_TRANSPARENT;\n    Lara_Skin_SetExtraEquipment(LM_HEAD, mesh);\n}\n\nvoid Lara_Skin_Initialise(void)\n{\n    const OBJECT *const extra_obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA);\n    ASSERT(extra_obj->loaded);\n\n    const OBJECT *const gun_swap_obj = Object_Get(O_LARA_SKIN_SWAP_GUNS);\n    ASSERT(gun_swap_obj->loaded);\n\n    m_SkinType = M_NO_OUTFIT;\n    m_HolsterType_L = LGT_UNARMED;\n    m_HolsterType_R = LGT_UNARMED;\n    m_UseCombatFace = false;\n\n    m_HolstersVisible = true;\n    for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n        m_Equipment[i].visible = true;\n        Lara_Skin_ClearEquipment(i);\n    }\n\n    const int32_t hair_segment_count = Lara_Hair_GetSegmentCount();\n    const int32_t outfit_count = Lara_Skin_GetOutfitCount();\n    for (int32_t i = 0; i < outfit_count; i++) {\n        const LARA_SKIN_OUTFIT *const outfit = Lara_Skin_GetOutfit(i);\n        if (!outfit->is_defined) {\n            continue;\n        }\n\n        const OBJECT *const skin_obj = Object_Get(outfit->obj_id);\n        ASSERT(skin_obj->loaded);\n        ASSERT(skin_obj->mesh_count == LM_NUMBER_OF);\n        if (!outfit->is_reflective) {\n            continue;\n        }\n\n        for (int32_t j = 0; j < LM_NUMBER_OF; j++) {\n            Object_SetMeshReflectiveEx(skin_obj->mesh_idx + j, true);\n            const int32_t extra_idx = M_GetBraidDependentMeshIdx(j, outfit);\n            if (extra_idx != M_NO_MESH) {\n                Object_SetMeshReflectiveEx(extra_idx, true);\n            }\n        }\n\n        for (int32_t j = 0; j < NUM_WEAPONS; j++) {\n            const LARA_SKIN_MESH_MAP map = outfit->gun_map->mesh_offsets[j];\n            if (map.thigh.left != M_NO_MESH) {\n                Object_SetMeshReflectiveEx(\n                    gun_swap_obj->mesh_idx + map.thigh.left, true);\n            }\n            if (map.thigh.right != M_NO_MESH) {\n                Object_SetMeshReflectiveEx(\n                    gun_swap_obj->mesh_idx + map.thigh.right, true);\n            }\n        }\n\n        if (!outfit->braid.enabled || outfit->braid.gold_offset == M_NO_MESH) {\n            continue;\n        }\n\n        for (int32_t j = 0; j < hair_segment_count; j++) {\n            Object_SetMeshReflectiveEx(\n                extra_obj->mesh_idx + outfit->braid.gold_offset + j, true);\n        }\n    }\n\n    Lara_Skin_ApplyOutfitFromConfig();\n}\n\nvoid Lara_Skin_ApplyOutfitFromConfig(void)\n{\n    if (!Game_IsLoaded()) {\n        return;\n    }\n\n    LARA_SKIN_TYPE skin_type = M_GetCurrentLevelOutfitType();\n    if (g_Config.visuals.lara_outfit != nullptr) {\n        const LARA_SKIN_TYPE config_type =\n            Lara_Skin_FindOutfitByName(g_Config.visuals.lara_outfit);\n        if (!Lara_Skin_IsOutfitAvailable(config_type)) {\n            LOG_WARNING(\n                \"Invalid outfit '%s' from config.visuals.lara_outfit; falling \"\n                \"back to default\",\n                g_Config.visuals.lara_outfit);\n            skin_type = M_GetCurrentLevelOutfitType();\n        } else {\n            skin_type = config_type;\n        }\n    }\n\n    Lara_Skin_SetType(skin_type);\n}\n\nvoid Lara_Skin_CycleOutfit(const int32_t dir)\n{\n    if (!Game_IsLoaded()) {\n        return;\n    }\n\n    if (Config_IsOptionEnforced(&g_Config.visuals.lara_outfit)) {\n        return;\n    }\n\n    // Update the config twice to guarantee the change is submitted in cases\n    // where Lara_Skin_SetType has been called manually for non-permanent swaps\n    // e.g. by Lua in cutscenes.\n    const char *const current_name = Lara_Skin_GetOutfitName(m_SkinType);\n    ASSERT(current_name != nullptr);\n\n    if (g_Config.visuals.lara_outfit == nullptr\n        || !String_Equivalent(g_Config.visuals.lara_outfit, current_name)) {\n        M_SetConfigOutfit(current_name);\n        Config_Update();\n    }\n\n    const int32_t outfit_count = Lara_Skin_GetOutfitCount();\n    int32_t type = m_SkinType;\n    do {\n        type += dir;\n        type += outfit_count;\n        type %= outfit_count;\n    } while (!Lara_Skin_IsOutfitAvailable(type)\n             || !Lara_Skin_GetOutfit(type)->is_selectable);\n\n    M_SetConfigOutfit(Lara_Skin_GetOutfitName(type));\n    Config_Update();\n}\n\nLARA_SKIN_TYPE Lara_Skin_GetType(void)\n{\n    return m_SkinType;\n}\n\nbool Lara_Skin_IsDefaultType(void)\n{\n    if (g_Config.visuals.lara_outfit != nullptr) {\n        const LARA_SKIN_TYPE config_type =\n            Lara_Skin_FindOutfitByName(g_Config.visuals.lara_outfit);\n        if (Lara_Skin_IsOutfitAvailable(config_type)) {\n            return m_SkinType == config_type;\n        }\n        return m_SkinType == M_GetCurrentLevelOutfitType();\n    }\n    return m_SkinType == M_GetCurrentLevelOutfitType();\n}\n\nvoid Lara_Skin_SetType(const LARA_SKIN_TYPE skin_type)\n{\n    LARA_SKIN_TYPE new_skin_type = skin_type;\n    if (!Lara_Skin_IsOutfitAvailable(new_skin_type)) {\n        new_skin_type = M_GetFallbackOutfitType();\n    }\n    if (m_SkinType == new_skin_type) {\n        return;\n    }\n\n    m_SkinType = new_skin_type;\n    Lara_Skin_ApplyOutfit();\n}\n\nvoid Lara_Skin_ApplyOutfit(void)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n        M_ApplyMeshIfValid(i, outfit);\n    }\n\n    M_SetGunEquipment(LM_THIGH_L, m_HolsterType_L, outfit);\n    M_SetGunEquipment(LM_THIGH_R, m_HolsterType_R, outfit);\n    M_SetCombatFace(m_UseCombatFace);\n    M_UpdateSunglasses();\n}\n\nvoid Lara_Skin_SetCombatFace(const bool enabled)\n{\n    if (m_UseCombatFace != enabled) {\n        M_SetCombatFace(enabled);\n    }\n}\n\nvoid Lara_Skin_SwapAllExtra(const LARA_EXTRA_STATE state)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetExtraOutfit(state);\n    if (outfit == nullptr) {\n        return;\n    }\n\n    for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n        M_ApplyMeshIfValid(i, outfit);\n    }\n\n    M_SetGunEquipment(LM_THIGH_L, m_HolsterType_L, outfit);\n    M_SetGunEquipment(LM_THIGH_R, m_HolsterType_R, outfit);\n}\n\nvoid Lara_Skin_SwapSingleExtra(\n    const LARA_MESH mesh, const LARA_EXTRA_STATE state)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetExtraOutfit(state);\n    if (outfit == nullptr) {\n        return;\n    }\n\n    M_ApplyMeshIfValid(mesh, outfit);\n\n    if (mesh == LM_THIGH_L) {\n        M_SetGunEquipment(LM_THIGH_L, m_HolsterType_L, outfit);\n    } else if (mesh == LM_THIGH_R) {\n        M_SetGunEquipment(LM_THIGH_R, m_HolsterType_R, outfit);\n    }\n}\n\nconst ANIM_BONE *Lara_Skin_GetBoneBase(void)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    const OBJECT *const skin_obj = Object_Get(outfit->obj_id);\n    return Object_TryGetBone(skin_obj, 0);\n}\n\nbool Lara_Skin_IsBraidSupported(void)\n{\n    return Lara_Skin_GetBraidMeshIdx() != M_NO_MESH;\n}\n\nXYZ_32 Lara_Skin_GetBraidOffset(void)\n{\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    return outfit->braid.hair_pos;\n}\n\nint32_t Lara_Skin_GetBraidMeshIdx(void)\n{\n    const int32_t offset = M_GetRelativeBraidOffset();\n    if (offset == M_NO_MESH) {\n        return offset;\n    }\n\n    const OBJECT *const obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA);\n    return obj->mesh_idx + offset;\n}\n\nconst ANIM_BONE *Lara_Skin_GetBraidBoneBase(void)\n{\n    const int32_t offset = M_GetRelativeBraidOffset();\n    if (offset == M_NO_MESH) {\n        return nullptr;\n    }\n\n    const OBJECT *const obj = Object_Get(O_LARA_SKIN_SWAP_EXTRA);\n    return Object_TryGetBone(obj, offset);\n}\n\nbool Lara_Skin_AreHolstersVisible(void)\n{\n    return m_HolstersVisible;\n}\n\nvoid Lara_Skin_SetHolstersVisible(const bool visible)\n{\n    m_HolstersVisible = visible;\n    m_Equipment[LM_THIGH_L].visible = visible;\n    m_Equipment[LM_THIGH_R].visible = visible;\n\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    M_ApplyMeshIfValid(LM_THIGH_L, outfit);\n    M_ApplyMeshIfValid(LM_THIGH_R, outfit);\n}\n\nvoid Lara_Skin_ClearEquipment(const LARA_MESH mesh)\n{\n    M_SetEquipment(mesh, EQUIPMENT_TYPE_NONE, M_NO_MESH, M_NO_MESH);\n}\n\nvoid Lara_Skin_SetExtraEquipment(\n    const LARA_MESH mesh, const LARA_SKIN_EXTRA_MESH extra_mesh)\n{\n    const int32_t offset = Lara_Skin_GetExtraMeshOffset(extra_mesh);\n    M_SetEquipment(mesh, EQUIPMENT_TYPE_EXTRA, extra_mesh, offset);\n}\n\nvoid Lara_Skin_SetGunEquipment(\n    const LARA_MESH mesh, const LARA_GUN_TYPE gun_type)\n{\n    if (gun_type < 0 || gun_type >= NUM_WEAPONS) {\n        return;\n    }\n    M_SetGunEquipment(mesh, gun_type, M_GetCurrentOutfit());\n\n    if (mesh == LM_THIGH_L) {\n        m_HolsterType_L = gun_type;\n    } else if (mesh == LM_THIGH_R) {\n        m_HolsterType_R = gun_type;\n    }\n\n    if ((mesh == LM_THIGH_L || mesh == LM_THIGH_R)\n        && !Gun_IsRifleType(gun_type)) {\n        Lara_Skin_SetHolstersVisible(true);\n    }\n}\n\nconst LARA_SKIN_EQUIPMENT *Lara_Skin_GetEquipment(const LARA_MESH mesh)\n{\n    return &m_Equipment[mesh];\n}\n\nSAMPLE_ID Lara_Skin_GetAnimSFX(const SAMPLE_ID sample_id)\n{\n    if (g_TRVersion == 2 && !g_Config.audio.enable_ps1_sfx) {\n        return sample_id;\n    }\n\n    const LARA_SKIN_OUTFIT *const outfit = M_GetCurrentOutfit();\n    if (outfit->footstep_sample_id == SFX_TRX_INVALID\n        || Sound_FromGameID(sample_id) != SFX_LARA_FOOTSTEP) {\n        return sample_id;\n    }\n\n    return Sound_ToGameID(outfit->footstep_sample_id);\n}\n\n// TODO: remove in TRX 1.5.\nvoid Lara_Skin_ExtractLegacyEquipment(const OBJECT_MESH **const meshes)\n{\n#define L_DETERMINE_EQUIPMENT(mesh)                                            \\\n    do {                                                                       \\\n        if (meshes[mesh] == nullptr) {                                         \\\n            break;                                                             \\\n        }                                                                      \\\n        for (int32_t i = 0; i < NUM_WEAPONS; i++) {                            \\\n            if (i == LGT_SKIDOO) {                                             \\\n                continue;                                                      \\\n            }                                                                  \\\n            const OBJECT *const obj = Object_Get(Gun_GetWeaponAnim(i));        \\\n            if (obj->loaded                                                    \\\n                && Object_GetMesh(obj->mesh_idx + mesh) == meshes[mesh]) {     \\\n                Lara_Skin_SetGunEquipment(mesh, i);                            \\\n                break;                                                         \\\n            }                                                                  \\\n        }                                                                      \\\n    } while (0)\n\n    L_DETERMINE_EQUIPMENT(LM_THIGH_L);\n    L_DETERMINE_EQUIPMENT(LM_THIGH_R);\n    L_DETERMINE_EQUIPMENT(LM_HAND_L);\n    L_DETERMINE_EQUIPMENT(LM_HAND_R);\n\n#undef L_DETERMINE_EQUIPMENT\n}\n"
  },
  {
    "path": "src/trx/game/lara/skin/common.h",
    "content": "#pragma once\n\n#include <trx/game/lara/enum.h>\n#include <trx/game/lara/skin/types.h>\n#include <trx/game/sound/ids.h>\n\nvoid Lara_Skin_Initialise(void);\nvoid Lara_Skin_ApplyOutfitFromConfig(void);\nvoid Lara_Skin_CycleOutfit(int32_t dir);\nLARA_SKIN_TYPE Lara_Skin_GetType(void);\nbool Lara_Skin_IsDefaultType(void);\nvoid Lara_Skin_SetType(LARA_SKIN_TYPE skin_type);\nvoid Lara_Skin_ApplyOutfit(void);\n\nvoid Lara_Skin_SetCombatFace(bool enabled);\nvoid Lara_Skin_SwapAllExtra(LARA_EXTRA_STATE state);\nvoid Lara_Skin_SwapSingleExtra(LARA_MESH mesh, LARA_EXTRA_STATE state);\nconst ANIM_BONE *Lara_Skin_GetBoneBase(void);\n\nbool Lara_Skin_IsBraidSupported(void);\nXYZ_32 Lara_Skin_GetBraidOffset(void);\nint32_t Lara_Skin_GetBraidMeshIdx(void);\nconst ANIM_BONE *Lara_Skin_GetBraidBoneBase(void);\n\nbool Lara_Skin_AreHolstersVisible(void);\nvoid Lara_Skin_SetHolstersVisible(bool visible);\nvoid Lara_Skin_ClearEquipment(LARA_MESH mesh);\nvoid Lara_Skin_SetGunEquipment(LARA_MESH mesh, LARA_GUN_TYPE gun_type);\nvoid Lara_Skin_SetExtraEquipment(\n    LARA_MESH mesh, LARA_SKIN_EXTRA_MESH extra_mesh);\nconst LARA_SKIN_EQUIPMENT *Lara_Skin_GetEquipment(LARA_MESH mesh);\n\nSAMPLE_ID Lara_Skin_GetAnimSFX(SAMPLE_ID sample_id);\n\nvoid Lara_Skin_ExtractLegacyEquipment(const OBJECT_MESH **meshes);\n"
  },
  {
    "path": "src/trx/game/lara/skin/enum.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef int32_t LARA_SKIN_TYPE;\n\n#define LARA_SKIN_TYPE_DEFAULT (-1)\n\ntypedef enum {\n    // clang-format off\n    BRAID_MODE_NONE,          // No body adjustments needed\n    BRAID_MODE_TR1_HEAD_ONLY, // Head replacement only (no backpack present)\n    BRAID_MODE_TR1_FULL,      // Head and torso replacement\n    BRAID_MODE_TR1_MAULED,    // Head and mauled torso replacement\n    BRAID_MODE_TR1_GOLD,      // Gold head and torso replacement\n    NUM_BRAID_MODES,\n    // clang-format on\n} LARA_SKIN_BRAID_MODE;\n\ntypedef enum {\n    EXTRA_MESH_TR1_BRAID_DEFAULT_HEAD,\n    EXTRA_MESH_TR1_BRAID_COMBAT_HEAD,\n    EXTRA_MESH_TR1_BRAID_DEFAULT_TORSO,\n    EXTRA_MESH_TR1_BRAID_MAULED_TORSO,\n    EXTRA_MESH_TR1_BRAID_GOLD_HEAD,\n    EXTRA_MESH_TR1_BRAID_GOLD_TORSO,\n    EXTRA_MESH_DAGGER_HAND,\n    EXTRA_MESH_DAGGER_HIPS,\n    EXTRA_MESH_OAR,\n    EXTRA_MESH_SPANNER,\n    EXTRA_MESH_DRINK_CAN,\n    EXTRA_MESH_GLASSES_OPAQUE,\n    EXTRA_MESH_GLASSES_TRANSPARENT,\n    NUM_EXTRA_MESHES,\n} LARA_SKIN_EXTRA_MESH;\n\ntypedef enum {\n    // clang-format off\n    EQUIPMENT_TYPE_NONE   = 0,\n    EQUIPMENT_TYPE_WEAPON = 1,\n    EQUIPMENT_TYPE_EXTRA  = 2,\n    // clang-format on\n} LARA_SKIN_EQUIPMENT_TYPE;\n"
  },
  {
    "path": "src/trx/game/lara/skin/storage.c",
    "content": "#include <trx/game/lara/skin/storage.h>\n\n#include <trx/config.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/catalog/manager.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lara.h>\n#include <trx/game/shell.h>\n\n#include <string.h>\n#include <uthash.h>\n\ntypedef struct {\n    char *name;\n    char *name_gs;\n    LARA_SKIN_OUTFIT outfit;\n} M_OUTFIT_ENTRY;\n\ntypedef struct M_OUTFIT_LOOKUP {\n    char *name;\n    int32_t index;\n    UT_hash_handle hh;\n} M_OUTFIT_LOOKUP;\n\nstatic VECTOR *m_GunMaps = nullptr;\nstatic M_OUTFIT_ENTRY *m_Outfits = nullptr;\nstatic int32_t m_OutfitCount = 0;\nstatic M_OUTFIT_LOOKUP *m_OutfitLookup = nullptr;\nstatic int32_t m_ExtraMeshOffsets[NUM_EXTRA_MESHES] = {};\n\nstatic void M_ExitWithJSONError(\n    const char *const source_path, const JSON_READ_IO *const io)\n{\n    JSONFile_ExitWithReadIOError(\n        io, String_FormatStatic(\"%s: outfits parse error\", source_path));\n}\n\nstatic void M_SeedDynamicEnumValues(void)\n{\n    const CONFIG_OPTION *const option =\n        Config_GetOption(&g_Config.visuals.lara_outfit);\n    Config_DynamicEnum_ResetValues(option);\n    Config_DynamicEnum_AddValue(\n        option, nullptr, GS_ID(\"dynamic/enums/lara_outfit/default\"));\n    for (int32_t i = 0; i < m_OutfitCount; i++) {\n        if (!m_Outfits[i].outfit.is_selectable) {\n            continue;\n        }\n        Config_DynamicEnum_AddValue(\n            option, m_Outfits[i].name, m_Outfits[i].name_gs);\n    }\n}\n\nstatic void M_ResetOutfits(void)\n{\n    M_OUTFIT_LOOKUP *entry = nullptr;\n    M_OUTFIT_LOOKUP *tmp = nullptr;\n    HASH_ITER(hh, m_OutfitLookup, entry, tmp)\n    {\n        HASH_DEL(m_OutfitLookup, entry);\n        Memory_FreePointer(&entry);\n    }\n\n    if (m_Outfits != nullptr) {\n        for (int32_t i = 0; i < m_OutfitCount; i++) {\n            Memory_FreePointer(&m_Outfits[i].name);\n            Memory_FreePointer(&m_Outfits[i].name_gs);\n        }\n        Memory_FreePointer(&m_Outfits);\n    }\n\n    m_OutfitCount = 0;\n    m_OutfitLookup = nullptr;\n}\n\nLARA_SKIN_TYPE Lara_Skin_FindOutfitByName(const char *const name)\n{\n    if (name == nullptr) {\n        return LARA_SKIN_TYPE_DEFAULT;\n    }\n\n    M_OUTFIT_LOOKUP *entry = nullptr;\n    HASH_FIND_STR(m_OutfitLookup, name, entry);\n    if (entry == nullptr) {\n        return -1;\n    }\n\n    return entry->index;\n}\n\nLARA_SKIN_TYPE Lara_Skin_GetDefaultType(void)\n{\n    return m_OutfitCount > 0 ? 0 : LARA_SKIN_TYPE_DEFAULT;\n}\n\nstatic bool M_ReadGunMaps(JSON_READ_IO *const io)\n{\n    JSON_MUST(JSON_PUSH(io, \"gun_maps\"));\n\n    const int32_t map_count = JSON_ARRAY_LEN(io);\n    if (map_count < 0) {\n        JSON_FAIL();\n    }\n\n    for (int32_t i = 0; i < map_count; ++i) {\n        JSON_MUST(JSON_PUSH_INDEX(io, i));\n        if (JSON_ReadIO_GetCurrentObject(io) == nullptr) {\n            JSON_ReadIO_SetError(io, \"gun map %d must be an object\", i);\n            JSON_FAIL();\n        }\n        LARA_SKIN_GUN_MAP map = {};\n\n        for (int32_t j = 0; j < NUM_WEAPONS; j++) {\n            LARA_SKIN_MESH_MAP *const mesh_map = &map.mesh_offsets[j];\n            memset(mesh_map, -1, sizeof(LARA_SKIN_MESH_MAP));\n\n            const char *const gun_name =\n                EnumMap_ToString(ENUM_MAP_NAME(LARA_GUN_TYPE), j);\n            if (!JSON_OPTIONAL(JSON_PUSH(io, gun_name))) {\n                continue;\n            }\n\n            JSON_OPTIONAL(JSON_READ(io, \"hand_r\", &mesh_map->hand.right));\n            JSON_OPTIONAL(JSON_READ(io, \"hand_l\", &mesh_map->hand.left));\n            JSON_OPTIONAL(JSON_READ(io, \"thigh_r\", &mesh_map->thigh.right));\n            JSON_OPTIONAL(JSON_READ(io, \"thigh_l\", &mesh_map->thigh.left));\n            JSON_OPTIONAL(JSON_READ(io, \"torso\", &mesh_map->torso));\n            JSON_MUST(JSON_POP(io));\n        }\n\n        Vector_Add(m_GunMaps, &map);\n        JSON_MUST(JSON_POP(io));\n    }\n\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_ReadExtraMeshes(JSON_READ_IO *const io)\n{\n    if (!JSON_OPTIONAL(JSON_PUSH(io, \"extra_meshes\"))) {\n        return false;\n    }\n\n    JSON_OBJECT *const extra_obj = JSON_ReadIO_GetCurrentObject(io);\n    if (extra_obj == nullptr) {\n        JSON_ReadIO_SetError(io, \"'extra_meshes' must be an object\");\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    for (JSON_OBJECT_ELEMENT *elem = extra_obj->start; elem != nullptr;\n         elem = elem->next) {\n        const char *const name = elem->name->string;\n        const int32_t type = ENUM_MAP_GET(LARA_SKIN_EXTRA_MESH, name, -1);\n        if (type < 0 || type >= NUM_EXTRA_MESHES) {\n            JSON_ReadIO_SetError(io, \"unknown extra mesh type '%s'\", name);\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n\n        JSON_MUST(JSON_READ(io, name, &m_ExtraMeshOffsets[type]));\n    }\n\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadBraid(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit)\n{\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"braid\"))) {\n        const char *braid_mode_name = nullptr;\n        if (JSON_OPTIONAL(JSON_READ(io, \"mode\", &braid_mode_name))) {\n            const int32_t mode =\n                ENUM_MAP_GET(LARA_SKIN_BRAID_MODE, braid_mode_name, -1);\n            if (mode < 0 || mode >= NUM_BRAID_MODES) {\n                JSON_ReadIO_SetError(\n                    io, \"unknown braid mode '%s'\", braid_mode_name);\n                JSON_MUST(JSON_POP(io));\n                JSON_FAIL();\n            }\n            outfit->braid.mode = mode;\n        }\n\n        JSON_READ_D(io, \"mesh_offset\", &outfit->braid.mesh_offset, 0);\n        JSON_READ_D(io, \"gold_offset\", &outfit->braid.gold_offset, 0);\n        JSON_READ_D(io, \"hair_pos\", &outfit->braid.hair_pos, (XYZ_32) {});\n        outfit->braid.enabled = true;\n        JSON_MUST(JSON_POP(io));\n    } else {\n        outfit->braid.enabled = false;\n    }\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadGunMap(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit)\n{\n    int32_t map_idx = -1;\n    JSON_READ_D(io, \"gun_map\", &map_idx, -1);\n    if (map_idx < 0 || map_idx >= m_GunMaps->count) {\n        JSON_ReadIO_SetError(io, \"invalid gun map '%d'\", map_idx);\n        JSON_FAIL();\n    }\n    outfit->gun_map = (LARA_SKIN_GUN_MAP *)Vector_Get(m_GunMaps, map_idx);\n    JSON_FINISH();\n}\n\nstatic bool M_LoadSFX(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit)\n{\n    const char *feet_sample_name = nullptr;\n    if (JSON_OPTIONAL(JSON_READ(io, \"footstep_sample_id\", &feet_sample_name))) {\n        CATALOG_ID feet_sample_id;\n        if (!Catalog_NameToEnum(\n                CATALOG_SAMPLES, feet_sample_name, &feet_sample_id)) {\n            JSON_ReadIO_SetError(\n                io, \"unknown sample id '%s'\", feet_sample_name);\n            JSON_FAIL();\n        }\n        outfit->footstep_sample_id = feet_sample_id;\n    } else {\n        outfit->footstep_sample_id = SFX_TRX_INVALID;\n    }\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadNoHolsters(\n    JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit)\n{\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"no_holster_offsets\"))) {\n        JSON_READ_D(io, \"thigh_l\", &outfit->no_holster_offsets.left, -1);\n        JSON_READ_D(io, \"thigh_r\", &outfit->no_holster_offsets.right, -1);\n        JSON_MUST(JSON_POP(io));\n    } else {\n        outfit->no_holster_offsets.left = -1;\n        outfit->no_holster_offsets.right = -1;\n    }\n    JSON_FINISH();\n}\n\nstatic bool M_LoadExtras(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit)\n{\n    for (int32_t j = 0; j < LS_EXTRA_NUMBER_OF; j++) {\n        outfit->extra_outfits[j] = LARA_SKIN_TYPE_DEFAULT;\n    }\n\n    if (JSON_OPTIONAL(JSON_PUSH(io, \"extra_outfits\"))) {\n        JSON_OBJECT *const extra_obj = JSON_ReadIO_GetCurrentObject(io);\n        if (extra_obj == nullptr) {\n            JSON_ReadIO_SetError(io, \"'extra_outfits' must be an object\");\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n\n        for (JSON_OBJECT_ELEMENT *elem = extra_obj->start; elem != nullptr;\n             elem = elem->next) {\n            const char *const state_name = elem->name->string;\n            const int32_t state =\n                ENUM_MAP_GET(LARA_EXTRA_STATE, state_name, -1);\n            if (state < 0 || state >= LS_EXTRA_NUMBER_OF) {\n                JSON_ReadIO_SetError(\n                    io, \"unknown Lara extra state '%s'\", state_name);\n                JSON_MUST(JSON_POP(io));\n                JSON_FAIL();\n            }\n\n            const char *outfit_name = nullptr;\n            JSON_MUST(JSON_READ(io, state_name, &outfit_name));\n            const LARA_SKIN_TYPE type = Lara_Skin_FindOutfitByName(outfit_name);\n            if (type < 0 || type >= m_OutfitCount) {\n                JSON_ReadIO_SetError(io, \"unknown outfit '%s'\", outfit_name);\n                JSON_MUST(JSON_POP(io));\n                JSON_FAIL();\n            }\n            outfit->extra_outfits[state] = type;\n        }\n        JSON_MUST(JSON_POP(io));\n    }\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadOutfit(JSON_READ_IO *const io, LARA_SKIN_OUTFIT *const outfit)\n{\n    const char *mesh_obj_name = nullptr;\n    JSON_MUST(JSON_READ(io, \"mesh_object\", &mesh_obj_name));\n\n    CATALOG_ID mesh_object_id;\n    if (!Catalog_NameToEnum(CATALOG_OBJECTS, mesh_obj_name, &mesh_object_id)) {\n        JSON_ReadIO_SetError(\n            io, \"unknown outfit object_id '%s'\", mesh_obj_name);\n        JSON_FAIL();\n    }\n    outfit->obj_id = mesh_object_id;\n\n    JSON_READ_D(io, \"is_reflective\", &outfit->is_reflective, false);\n    JSON_READ_D(io, \"is_selectable\", &outfit->is_selectable, true);\n    JSON_READ_D(io, \"combat_face_offset\", &outfit->combat_face_offset, -1);\n    JSON_READ_D(io, \"supports_sunglasses\", &outfit->supports_sunglasses, true);\n\n    JSON_MUST(M_LoadBraid(io, outfit));\n    JSON_MUST(M_LoadGunMap(io, outfit));\n    JSON_MUST(M_LoadSFX(io, outfit));\n    JSON_MUST(M_LoadNoHolsters(io, outfit));\n    JSON_MUST(M_LoadExtras(io, outfit));\n\n    outfit->is_defined = true;\n    JSON_FINISH();\n}\n\nstatic bool M_ReadOutfits(JSON_READ_IO *const io)\n{\n    JSON_MUST(JSON_PUSH(io, \"outfits\"));\n\n    JSON_OBJECT *const outfits_map = JSON_ReadIO_GetCurrentObject(io);\n    if (outfits_map == nullptr) {\n        JSON_ReadIO_SetError(io, \"'outfits' must be an object\");\n        JSON_FAIL();\n    }\n\n    size_t outfit_count = 0;\n    for (JSON_OBJECT_ELEMENT *elem = outfits_map->start; elem != nullptr;\n         elem = elem->next) {\n        outfit_count++;\n    }\n\n    if (outfit_count == 0) {\n        JSON_ReadIO_SetError(io, \"missing outfits in configuration\");\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    m_Outfits = Memory_Alloc(sizeof(*m_Outfits) * outfit_count);\n    m_OutfitCount = (int32_t)outfit_count;\n\n    size_t idx = 0;\n    for (JSON_OBJECT_ELEMENT *elem = outfits_map->start; elem != nullptr;\n         elem = elem->next) {\n        const char *const name = elem->name->string;\n        JSON_MUST(JSON_PUSH(io, name));\n\n        M_OUTFIT_ENTRY *const outfit = &m_Outfits[idx];\n        outfit->name = Memory_DupStr(name);\n\n        const char *name_gs = nullptr;\n        if (!JSON_READ(io, \"name_gs\", &name_gs)) {\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n        outfit->name_gs = Memory_DupStr(name_gs);\n\n        M_OUTFIT_LOOKUP *existing = nullptr;\n        HASH_FIND_STR(m_OutfitLookup, outfit->name, existing);\n        if (existing != nullptr) {\n            JSON_ReadIO_SetError(io, \"duplicate outfit '%s'\", name);\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n\n        M_OUTFIT_LOOKUP *const lookup = Memory_Alloc(sizeof(*lookup));\n        lookup->name = outfit->name;\n        lookup->index = (int32_t)idx;\n        HASH_ADD_KEYPTR(\n            hh, m_OutfitLookup, lookup->name, strlen(lookup->name), lookup);\n        JSON_MUST(JSON_POP(io));\n        idx++;\n    }\n\n    idx = 0;\n    for (JSON_OBJECT_ELEMENT *elem = outfits_map->start; elem != nullptr;\n         elem = elem->next) {\n        JSON_MUST(JSON_PUSH(io, elem->name->string));\n        if (!M_LoadOutfit(io, &m_Outfits[idx].outfit)) {\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n        JSON_MUST(JSON_POP(io));\n        idx++;\n    }\n\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadFile(JSON_READ_IO *const io)\n{\n    JSON_MUST(M_ReadGunMaps(io));\n    JSON_MUST(M_ReadExtraMeshes(io));\n    JSON_MUST(M_ReadOutfits(io));\n    JSON_FINISH();\n}\n\nvoid Lara_Skin_LoadFromFile(const char *const path)\n{\n    char *source_path = Memory_DupStr(path);\n    JSON_READ_IO *io = nullptr;\n\n    if (m_GunMaps != nullptr) {\n        Vector_Free(m_GunMaps);\n        m_GunMaps = nullptr;\n    }\n    m_GunMaps = Vector_Create(sizeof(LARA_SKIN_GUN_MAP));\n\n    M_ResetOutfits();\n    M_SeedDynamicEnumValues();\n    memset(m_ExtraMeshOffsets, 0, sizeof(m_ExtraMeshOffsets));\n\n    LOG_INFO(\"Reading outfit definitions from %s\", source_path);\n    JSON_VALUE *const doc = JSONFile_ReadEx(source_path, true);\n    if (doc == nullptr) {\n        Shell_ExitSystemFmt(\"invalid outfits file: %s\", source_path);\n        goto cleanup;\n    }\n\n    io = JSON_ReadIO_Create(doc, 0, source_path);\n    if (!M_LoadFile(io)) {\n        const char *const error = JSON_ReadIO_GetError(io);\n        if (error != nullptr && error[0] != '\\0') {\n            M_ExitWithJSONError(source_path, io);\n        }\n    }\n\n    M_SeedDynamicEnumValues();\n\ncleanup:\n    if (io != nullptr) {\n        JSON_ReadIO_Destroy(io);\n    }\n    JSON_ValueFree(doc);\n    Memory_FreePointer(&source_path);\n}\n\nvoid Lara_Skin_Shutdown(void)\n{\n    if (m_GunMaps != nullptr) {\n        Vector_Free(m_GunMaps);\n        m_GunMaps = nullptr;\n    }\n\n    M_ResetOutfits();\n}\n\nint32_t Lara_Skin_GetOutfitCount(void)\n{\n    return m_OutfitCount;\n}\n\nbool Lara_Skin_IsOutfitAvailable(const LARA_SKIN_TYPE skin_type)\n{\n    return skin_type >= 0 && skin_type < m_OutfitCount\n        && m_Outfits[skin_type].outfit.is_defined;\n}\n\nconst LARA_SKIN_OUTFIT *Lara_Skin_GetOutfit(const LARA_SKIN_TYPE skin_type)\n{\n    ASSERT(skin_type >= 0 && skin_type < m_OutfitCount);\n    return &m_Outfits[skin_type].outfit;\n}\n\nconst char *Lara_Skin_GetOutfitName(const LARA_SKIN_TYPE skin_type)\n{\n    if (skin_type < 0 || skin_type >= m_OutfitCount) {\n        return nullptr;\n    }\n    return m_Outfits[skin_type].name;\n}\n\nint32_t Lara_Skin_GetExtraMeshOffset(const LARA_SKIN_EXTRA_MESH mesh)\n{\n    ASSERT(mesh >= 0 && mesh < NUM_EXTRA_MESHES);\n    return m_ExtraMeshOffsets[mesh];\n}\n"
  },
  {
    "path": "src/trx/game/lara/skin/storage.h",
    "content": "#pragma once\n\n#include <trx/game/lara/skin/types.h>\n\nvoid Lara_Skin_LoadFromFile(const char *path);\nvoid Lara_Skin_Shutdown(void);\nint32_t Lara_Skin_GetOutfitCount(void);\nbool Lara_Skin_IsOutfitAvailable(LARA_SKIN_TYPE skin_type);\nconst LARA_SKIN_OUTFIT *Lara_Skin_GetOutfit(LARA_SKIN_TYPE skin_type);\nconst char *Lara_Skin_GetOutfitName(LARA_SKIN_TYPE skin_type);\nLARA_SKIN_TYPE Lara_Skin_FindOutfitByName(const char *name);\nLARA_SKIN_TYPE Lara_Skin_GetDefaultType(void);\nint32_t Lara_Skin_GetExtraMeshOffset(LARA_SKIN_EXTRA_MESH mesh);\n"
  },
  {
    "path": "src/trx/game/lara/skin/types.h",
    "content": "#pragma once\n\n#include <trx/game/lara/skin/enum.h>\n#include <trx/game/lara/types.h>\n#include <trx/game/sound/ids.h>\n\ntypedef struct {\n    int32_t right;\n    int32_t left;\n} MESH_PAIR;\n\ntypedef struct {\n    MESH_PAIR hand;\n    MESH_PAIR thigh;\n    int32_t torso;\n} LARA_SKIN_MESH_MAP;\n\ntypedef struct {\n    LARA_SKIN_MESH_MAP mesh_offsets[NUM_WEAPONS];\n} LARA_SKIN_GUN_MAP;\n\ntypedef struct {\n    LARA_SKIN_BRAID_MODE mode;\n    bool enabled;\n    int32_t mesh_offset;\n    int32_t gold_offset;\n    XYZ_32 hair_pos;\n} LARA_SKIN_BRAID;\n\ntypedef struct {\n    bool is_defined;\n    OBJECT_ID obj_id;\n    LARA_SKIN_GUN_MAP *gun_map;\n    LARA_SKIN_BRAID braid;\n    bool is_selectable;\n    bool is_reflective;\n    bool supports_sunglasses;\n    SAMPLE_TRX_ID footstep_sample_id;\n    int32_t combat_face_offset;\n    MESH_PAIR no_holster_offsets;\n    int32_t extra_outfits[LS_EXTRA_NUMBER_OF];\n} LARA_SKIN_OUTFIT;\n\ntypedef struct {\n    LARA_SKIN_EQUIPMENT_TYPE type;\n    int32_t data;\n    bool visible;\n    const OBJECT_MESH *mesh;\n} LARA_SKIN_EQUIPMENT;\n"
  },
  {
    "path": "src/trx/game/lara/skin.h",
    "content": "#pragma once\n\n#include <trx/game/lara/skin/common.h>\n#include <trx/game/lara/skin/storage.h>\n"
  },
  {
    "path": "src/trx/game/lara/state/climb.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_CAM_HANG_ANGLE             0\n#define M_CAM_HANG_ELEVATION         (-60 * DEG_1)              // = -10920\n#define M_CAM_CLIMB_LEFT_ANGLE       (-30 * DEG_1)              // = -5460\n#define M_CAM_CLIMB_LEFT_ELEVATION   (-15 * DEG_1)              // = -2730\n#define M_CAM_CLIMB_RIGHT_ANGLE      (-M_CAM_CLIMB_LEFT_ANGLE)  // = 5460\n#define M_CAM_CLIMB_RIGHT_ELEVATION  M_CAM_CLIMB_LEFT_ELEVATION // = -2730\n#define M_CAM_CLIMB_STANCE_ELEVATION (-20 * DEG_1)              // = -3640\n#define M_CAM_CLIMBING_ELEVATION     (30 * DEG_1)               // = 5460\n#define M_CAM_CLIMB_END_ELEVATION    (-45 * DEG_1)              // = -8190\n#define M_CAM_CLIMB_DOWN_ELEVATION   M_CAM_CLIMB_END_ELEVATION  // = -8190\n// clang-format on\n\nstatic void M_Hang(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    g_Camera.target_angle = M_CAM_HANG_ANGLE;\n    g_Camera.target_elevation = M_CAM_HANG_ELEVATION;\n    if (g_Input.left || g_Input.step_left) {\n        item->goal_anim_state = LS(LS_SHIMMY_LEFT);\n    } else if (g_Input.right || g_Input.step_right) {\n        item->goal_anim_state = LS(LS_SHIMMY_RIGHT);\n    }\n}\n\nstatic void M_Shimmy(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    g_Camera.target_angle = M_CAM_HANG_ANGLE;\n    g_Camera.target_elevation = M_CAM_HANG_ELEVATION;\n\n    const bool stop = item->current_anim_state == LS(LS_SHIMMY_LEFT)\n        ? (!g_Input.left && !g_Input.step_left)\n        : (!g_Input.right && !g_Input.step_right);\n    if (stop) {\n        item->goal_anim_state = LS(LS_HANG);\n    }\n}\n\nstatic void M_StanceLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    g_Camera.target_elevation = M_CAM_CLIMB_STANCE_ELEVATION;\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    if (g_Input.left || g_Input.step_left) {\n        item->goal_anim_state = LS(LS_CLIMB_LEFT);\n    } else if (g_Input.right || g_Input.step_right) {\n        item->goal_anim_state = LS(LS_CLIMB_RIGHT);\n    } else if (g_Input.jump) {\n        item->goal_anim_state = LS(LS_JUMP_BACK);\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->gun_status = LGS_ARMLESS;\n        lara->move_angle = item->rot.y + DEG_180;\n    }\n}\n\nstatic void M_SideLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    if (item->current_anim_state == LS(LS_CLIMB_LEFT)) {\n        g_Camera.target_angle = M_CAM_CLIMB_LEFT_ANGLE;\n        g_Camera.target_elevation = M_CAM_CLIMB_LEFT_ELEVATION;\n        if (!g_Input.left && !g_Input.step_left) {\n            item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        }\n    } else {\n        g_Camera.target_angle = M_CAM_CLIMB_RIGHT_ANGLE;\n        g_Camera.target_elevation = M_CAM_CLIMB_RIGHT_ELEVATION;\n        if (!g_Input.right && !g_Input.step_right) {\n            item->goal_anim_state = LS(LS_CLIMB_STANCE);\n        }\n    }\n}\n\nstatic void M_UpDownLadder(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    switch (LS_U(item->current_anim_state)) {\n    case LS_CLIMBING:\n        g_Camera.target_elevation = M_CAM_CLIMBING_ELEVATION;\n        break;\n    case LS_CLIMB_DOWN:\n        g_Camera.target_elevation = M_CAM_CLIMB_DOWN_ELEVATION;\n        break;\n    case LS_CLIMB_END:\n        g_Camera.flags = CF_FOLLOW_CENTRE;\n        g_Camera.target_angle = M_CAM_CLIMB_END_ELEVATION;\n        break;\n    default:\n        break;\n    }\n}\n\n// clang-format off\nREGISTER_LARA_STATE(LS_HANG,         M_Hang)\nREGISTER_LARA_STATE(LS_SHIMMY_LEFT,  M_Shimmy)\nREGISTER_LARA_STATE(LS_SHIMMY_RIGHT, M_Shimmy)\nREGISTER_LARA_STATE(LS_CLIMB_STANCE, M_StanceLadder)\nREGISTER_LARA_STATE(LS_CLIMB_LEFT,   M_SideLadder)\nREGISTER_LARA_STATE(LS_CLIMB_RIGHT,  M_SideLadder)\nREGISTER_LARA_STATE(LS_CLIMBING,     M_UpDownLadder)\nREGISTER_LARA_STATE(LS_CLIMB_DOWN,   M_UpDownLadder)\nREGISTER_LARA_STATE(LS_CLIMB_END,    M_UpDownLadder)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state/crouch.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/input.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/flare.h>\n#include <trx/game/lara/misc.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/objects/general/flare_item.h>\n#include <trx/game/rooms.h>\n#include <trx/game/rooms/geometry.h>\n\n// clang-format off\n#define M_CAM_CRAWL_ELEVATION (-DEG_1 * 23)      // = -4186\n#define M_CRAWL_TURN_RATE     ((DEG_1 * 2) + 45) // = 409\n#define M_CRAWL_TURN_MAX      (DEG_1 * 3)        // = 546\n#define M_CRAWL_TURN_SLOW     (DEG_1 * 3 / 2)    // = 273\n#define M_JUMP_DIST           (STEP_L * 3)       // = 768\n#define M_JUMP_HEIGHT         (STEP_L * 2)       // = 512\n#define M_JUMP_START_SHIFT    (STEP_L * 3 / 8)   // = 96\n#define M_JUMP_TARGET_SHIFT   (STEP_L * 5 / 8)   // = 160\n// clang-format on\n\nstatic bool M_CanEnterCrawlFromCrouch(const ITEM *const item)\n{\n    return item->current_anim_state == LS(LS_CROUCH_IDLE)\n        && Item_GetRelativeFrame(item) > 1;\n}\n\nstatic bool M_CanCrouchRoll(const ITEM *const item, const LARA_INFO *const lara)\n{\n    if (!g_Config.gameplay.enable_crouch_roll || g_Input.draw\n        || !g_Input.sprint) {\n        return false;\n    }\n\n    if (item->current_anim_state == LS(LS_CROUCH_IDLE)\n        && lara->gun_status != LGS_ARMLESS) {\n        return false;\n    }\n\n    if (Room_Get(item->room_num)->flags.swamp) {\n        return false;\n    }\n\n    if (!(g_Config.gameplay.enable_toggle_crouch ? lara->crouching\n                                                 : g_Input.crouch)\n        && (!lara->keep_crouched || lara->water_status == LWS_WADE)) {\n        return false;\n    }\n\n    const int32_t height_far = Lara_FloorFront(item, item->rot.y, STEP_L * 2);\n    const int32_t height_near = Lara_FloorFront(item, item->rot.y, STEP_L);\n    if (height_far >= STEPUP_HEIGHT || height_near < -STEPUP_HEIGHT) {\n        return false;\n    }\n\n    if (Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))) {\n        if (!g_Config.gameplay.enable_responsive_crawl) {\n            return false;\n        }\n    } else if (\n        !Item_TestAnimEqual(item, LA(LA_CROUCH_IDLE))\n        && !Item_TestAnimEqual(item, LA(LA_STAND_TO_CROUCH_END))) {\n        return false;\n    }\n\n    if (lara->gun_type == LGT_FLARE\n        && (lara->flare.age <= 0 || lara->flare.age >= Flare_GetMaxAge())) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_CanJumpDown(const ITEM *const item, const LARA_INFO *const lara)\n{\n    if (!g_Config.gameplay.enable_crawl_jump || !g_Input.jump) {\n        return false;\n    }\n\n    if (item->current_anim_state == LS(LS_CROUCH_IDLE)\n        && lara->gun_status != LGS_ARMLESS) {\n        return false;\n    }\n\n    if (!Item_TestAnimEqual(item, LA(LA_CRAWL_IDLE))\n        && !Item_TestAnimEqual(item, LA(LA_CROUCH_TO_CRAWL_END))\n        && !Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_RIGHT))\n        && !Item_TestAnimEqual(item, LA(LA_CRAWL_FORWARD_TO_IDLE_END_LEFT))\n        && !Item_TestAnimEqual(item, LA(LA_CROUCH_IDLE))) {\n        return false;\n    }\n\n    const int32_t floor_front = Lara_FloorFront(item, item->rot.y, M_JUMP_DIST);\n    const int32_t ceiling_front =\n        Lara_CeilingFront(item, item->rot.y, M_JUMP_DIST, M_JUMP_HEIGHT);\n    if (floor_front < M_JUMP_HEIGHT || ceiling_front == NO_HEIGHT\n        || ceiling_front > 0) {\n        return false;\n    }\n\n    const GAME_VECTOR start = {\n        .x = item->pos.x,\n        .y = item->pos.y - M_JUMP_START_SHIFT,\n        .z = item->pos.z,\n        .room_num = item->room_num,\n    };\n    GAME_VECTOR target = { .pos = XYZ_32_OffsetYaw(\n                               start.pos, item->rot.y, M_JUMP_DIST) };\n    target.y += M_JUMP_TARGET_SHIFT;\n    return LOS_Check(&start, &target, false);\n}\n\nstatic void M_CrouchIdle(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool crouch_active = g_Config.gameplay.enable_toggle_crouch\n        ? lara->crouching || lara->keep_crouched\n        : g_Input.crouch || lara->keep_crouched;\n    lara->sprinting = false;\n    lara->is_crouched = true;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        return;\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    if ((g_Input.forward || g_Input.back) && crouch_active\n        && lara->gun_status == LGS_ARMLESS && M_CanEnterCrawlFromCrouch(item)) {\n        lara->torso_rot.x = 0;\n        lara->torso_rot.y = 0;\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        return;\n    }\n\n    if (M_CanCrouchRoll(item, lara)) {\n        lara->torso_rot.x = 0;\n        lara->torso_rot.y = 0;\n        Item_SwitchToAnim(item, LA(LA_CROUCH_ROLL_FORWARD_START), 0);\n        item->current_anim_state = LS(LS_CROUCH_ROLL);\n        item->goal_anim_state = LS(LS_CROUCH_ROLL);\n    } else if (crouch_active && M_CanJumpDown(item, lara)) {\n        Lara_AnimateUntil(item, LS(LS_CRAWL_IDLE));\n        item->goal_anim_state = LS(LS_CRAWL_JUMP_DOWN);\n    }\n}\n\nstatic void M_CrouchRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_elevation = -3640;\n    item->goal_anim_state = LS(LS_CROUCH_IDLE);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->is_crouched = true;\n}\n\nstatic void M_CrouchTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0 || !g_Config.gameplay.enable_responsive_crawl) {\n        item->goal_anim_state = LS(LS_CROUCH_IDLE);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    const bool left_turn = item->current_anim_state == LS(LS_CROUCH_TURN_LEFT);\n    item->rot.y += left_turn ? -M_CRAWL_TURN_SLOW : M_CRAWL_TURN_SLOW;\n\n    if (!(left_turn ? g_Input.left : g_Input.right)) {\n        item->goal_anim_state = LS(LS_CROUCH_IDLE);\n    }\n}\n\nstatic void M_CrawlIdle(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_DEATH);\n        return;\n    }\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->torso_rot.x = 0;\n    lara->torso_rot.y = 0;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n    g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION;\n\n    if (Item_TestAnimEqual(item, LA(LA_CROUCH_TO_CRAWL_START))) {\n        lara->gun_status = LGS_HANDS_BUSY;\n    }\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (M_CanCrouchRoll(item, lara)) {\n        Lara_AnimateUntil(item, LS(LS_CROUCH_IDLE));\n        item->goal_anim_state = LS(LS_CROUCH_ROLL);\n    } else if (M_CanJumpDown(item, lara)) {\n        item->goal_anim_state = LS(LS_CRAWL_JUMP_DOWN);\n    }\n\n    g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION;\n}\n\nstatic void M_CrawlForward(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        return;\n    }\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool crouch_active = g_Config.gameplay.enable_toggle_crouch\n        ? lara->crouching\n        : g_Input.crouch || lara->keep_crouched;\n    lara->torso_rot.x = 0;\n    lara->torso_rot.y = 0;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n    g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION;\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (!g_Input.forward || (!crouch_active && !lara->keep_crouched)) {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        return;\n    }\n\n    if (g_Input.left) {\n        lara->turn_rate -= M_CRAWL_TURN_RATE;\n        CLAMPL(lara->turn_rate, -M_CRAWL_TURN_MAX);\n    } else if (g_Input.right) {\n        lara->turn_rate += M_CRAWL_TURN_RATE;\n        CLAMPG(lara->turn_rate, M_CRAWL_TURN_MAX);\n    }\n}\n\nstatic void M_CrawlTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->torso_rot.x = 0;\n    lara->torso_rot.y = 0;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n    g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION;\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    const bool left_turn = item->current_anim_state == LS(LS_CRAWL_TURN_LEFT);\n    item->rot.y += left_turn ? -M_CRAWL_TURN_SLOW : M_CRAWL_TURN_SLOW;\n\n    if (!(left_turn ? g_Input.left : g_Input.right)) {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n    }\n}\n\nstatic void M_CrawlBack(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n        return;\n    }\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->torso_rot.x = 0;\n    lara->torso_rot.y = 0;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n    g_Camera.target_elevation = M_CAM_CRAWL_ELEVATION;\n\n    if (Lara_Col_TestSlide(item, coll)) {\n        return;\n    }\n\n    if (g_Input.back) {\n        if (g_Input.right) {\n            lara->turn_rate -= M_CRAWL_TURN_RATE;\n            CLAMPL(lara->turn_rate, -M_CRAWL_TURN_MAX);\n        } else if (g_Input.left) {\n            lara->turn_rate += M_CRAWL_TURN_RATE;\n            CLAMPG(lara->turn_rate, M_CRAWL_TURN_MAX);\n        }\n    } else {\n        item->goal_anim_state = LS(LS_CRAWL_IDLE);\n    }\n}\n\nstatic void M_CrawlJumpDown(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_baddie_push = 0;\n    coll->enable_hit = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->gun_status = LGS_ARMLESS;\n}\n\n// clang-format off\nREGISTER_LARA_STATE(LS_CROUCH_IDLE,       M_CrouchIdle)\nREGISTER_LARA_STATE(LS_CROUCH_ROLL,       M_CrouchRoll)\nREGISTER_LARA_STATE(LS_CROUCH_TURN_LEFT,  M_CrouchTurn)\nREGISTER_LARA_STATE(LS_CROUCH_TURN_RIGHT, M_CrouchTurn)\nREGISTER_LARA_STATE(LS_CRAWL_IDLE,        M_CrawlIdle)\nREGISTER_LARA_STATE(LS_CRAWL_FORWARD,     M_CrawlForward)\nREGISTER_LARA_STATE(LS_CRAWL_TURN_LEFT,   M_CrawlTurn)\nREGISTER_LARA_STATE(LS_CRAWL_TURN_RIGHT,  M_CrawlTurn)\nREGISTER_LARA_STATE(LS_CRAWL_BACK,        M_CrawlBack)\nREGISTER_LARA_STATE(LS_CRAWL_JUMP_DOWN,   M_CrawlJumpDown)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state/extra.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/game.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/effects/twinkle.h>\n#include <trx/game/output/state.h>\n#include <trx/game/overlay.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks/spawners.h>\n#include <trx/game/stats.h>\n#include <trx/game/viewport.h>\n\n// clang-format off\n#define M_LF_PICKUP_SCION              44\n#define M_LF_PICKUP_GOLD_BAR           113\n#define M_LF_SHARK_DEATH_END           56\n#define M_LF_SHARK_DEATH_TIMER_DELAY   25\n#define M_LF_TREX_DEATH_TIMER_DELAY    45\n#define M_LF_YETI_DEATH_TIMER_DELAY    70\n#define M_LF_DRAGON_DAGGER_PULLED      1\n#define M_LF_DRAGON_DAGGER_STORED      180\n#define M_LF_DRAGON_DAGGER_DISPLAY     210\n#define M_LF_DRAGON_DAGGER_ANIM_END    239\n#define M_LF_START_HOUSE_BEGIN         1\n#define M_LF_START_HOUSE_DAGGER_STORED 401\n#define M_LF_SHOWER_START              1\n#define M_LF_SHOWER_SHOTGUN_PICKUP     316\n#define M_CAM_YETI_KILL_ANGLE         (160 * DEG_1) // = 29120\n#define M_CAM_YETI_KILL_DISTANCE      (3 * WALL_L)  // = 3072\n#define M_CAM_SHARK_KILL_ANGLE        (160 * DEG_1) // = 29120\n#define M_CAM_SHARK_KILL_DISTANCE     (3 * WALL_L)  // = 3072\n#define M_CAM_AIRLOCK_ANGLE           (80 * DEG_1)  // = 14560\n#define M_CAM_AIRLOCK_ELEVATION       (-25 * DEG_1) // = -4550\n#define M_CAM_GONG_BONG_ANGLE         (-25 * DEG_1) // = -4550\n#define M_CAM_GONG_BONG_ELEVATION     (-20 * DEG_1) // = -3640\n#define M_CAM_GONG_BONG_DISTANCE      (3 * WALL_L)  // = 3072\n#define M_CAM_BEAST_KILL_ANGLE        (170 * DEG_1) // = 30940\n#define M_CAM_BEAST_KILL_ELEVATION    (-25 * DEG_1) // = -4550\n#define M_CAM_TORSO_KILL_DISTANCE     (2 * WALL_L)  // = 2048\n// clang-format on\n\ntypedef struct {\n    int16_t frame_idx;\n    LARA_MESH mesh;\n} M_MIDAS_STEP;\n\nstatic const M_MIDAS_STEP m_MidasSteps[] = {\n    { .frame_idx = 5, .mesh = LM_FOOT_L },\n    { .frame_idx = 5, .mesh = LM_FOOT_R },\n    { .frame_idx = 70, .mesh = LM_CALF_L },\n    { .frame_idx = 90, .mesh = LM_THIGH_L },\n    { .frame_idx = 100, .mesh = LM_CALF_R },\n    { .frame_idx = 120, .mesh = LM_HIPS },\n    { .frame_idx = 120, .mesh = LM_THIGH_R },\n    { .frame_idx = 135, .mesh = LM_TORSO },\n    { .frame_idx = 150, .mesh = LM_UARM_L },\n    { .frame_idx = 163, .mesh = LM_LARM_L },\n    { .frame_idx = 174, .mesh = LM_HAND_L },\n    { .frame_idx = 186, .mesh = LM_UARM_R },\n    { .frame_idx = 195, .mesh = LM_LARM_R },\n    { .frame_idx = 218, .mesh = LM_HAND_R },\n    { .frame_idx = 225, .mesh = LM_HEAD },\n};\n\nstatic void M_ScionPedestal(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!Item_TestFrameEqual(item, M_LF_PICKUP_SCION)\n        || lara->interact_target.item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const scion = Item_Get(lara->interact_target.item_num);\n    const ITEM_ACTION action = ItemAction_ToGameID(ITEM_ACTION_FINISH_LEVEL);\n    if (!Anim_HasFXCommand(Item_GetAnim(item), action)) {\n        Overlay_AddDisplayPickup(scion->object_id);\n    }\n\n    Inv_AddItem(scion->object_id);\n    scion->status = IS_INVISIBLE;\n    Item_RemoveDrawn(lara->interact_target.item_num);\n    Stats_AddPickup();\n    lara->interact_target.item_num = NO_ITEM;\n}\n\nstatic void M_UseMidas(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    Twinkle_SparkleItem(item, (1 << LM_HAND_L) | (1 << LM_HAND_R));\n\n    if (Item_TestFrameEqual(item, M_LF_PICKUP_GOLD_BAR)) {\n        Overlay_AddDisplayPickup(O_PUZZLE_ITEM_1);\n        Inv_RemoveItem(O_LEADBAR_ITEM);\n        Inv_AddItem(O_PUZZLE_ITEM_1);\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->interact_target.item_num = NO_ITEM;\n    }\n}\n\nstatic void M_MidasKill(ITEM *const item, COLL_INFO *const coll)\n{\n    item->gravity = false;\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const int32_t frame_num = Item_GetRelativeFrame(item);\n\n    for (size_t i = 0; i < ARRAY_SIZE(m_MidasSteps); i++) {\n        const M_MIDAS_STEP *const step = &m_MidasSteps[i];\n        if (step->frame_idx > frame_num) {\n            continue;\n        }\n\n        lara->mesh_effects |= (1 << step->mesh);\n        Lara_Skin_SwapSingleExtra(step->mesh, LS_EXTRA_MIDAS_KILL);\n        switch (step->mesh) {\n        case LM_TORSO:\n        case LM_HAND_L:\n        case LM_HAND_R:\n            Lara_Skin_ClearEquipment(step->mesh);\n            break;\n        default:\n            break;\n        }\n    }\n\n    Twinkle_SparkleItem(item, lara->mesh_effects);\n}\n\nstatic void M_Breath(ITEM *const item, COLL_INFO *const coll)\n{\n    Item_SwitchToAnim(item, LA(LA_STAND_IDLE), 0);\n    item->goal_anim_state = LS(LS_STOP);\n    item->current_anim_state = LS(LS_STOP);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->extra_anim = false;\n    lara->gun_status = LGS_ARMLESS;\n    if (g_Camera.type != CAM_HEAVY) {\n        g_Camera.type = CAM_CHASE;\n    }\n    Viewport_AlterFOV(-1, FOV_MODE_GAME);\n}\n\nstatic void M_YetiKill(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_angle = M_CAM_YETI_KILL_ANGLE;\n    g_Camera.target_distance = M_CAM_YETI_KILL_DISTANCE;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->hit_direction = DIR_UNKNOWN;\n    if (Item_TestFrameRange(item, 0, M_LF_YETI_DEATH_TIMER_DELAY)) {\n        lara->death_timer = 1;\n    }\n}\n\nstatic void M_SharkKill(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_angle = M_CAM_SHARK_KILL_ANGLE;\n    g_Camera.target_distance = M_CAM_SHARK_KILL_DISTANCE;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->hit_direction = DIR_UNKNOWN;\n\n    if (Item_TestFrameEqual(item, M_LF_SHARK_DEATH_END)) {\n        const int32_t water_height =\n            Room_GetWaterHeight(item->pos, item->room_num);\n        if (water_height != NO_HEIGHT && water_height < item->pos.y - 100) {\n            item->pos.y -= 5;\n        }\n    }\n\n    if (Item_TestFrameRange(item, 0, M_LF_SHARK_DEATH_TIMER_DELAY)) {\n        lara->death_timer = 1;\n    }\n}\n\nstatic void M_Airlock(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_angle = M_CAM_AIRLOCK_ANGLE;\n    g_Camera.target_elevation = M_CAM_AIRLOCK_ELEVATION;\n}\n\nstatic void M_GongBong(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_angle = M_CAM_GONG_BONG_ANGLE;\n    g_Camera.target_elevation = M_CAM_GONG_BONG_ELEVATION;\n    g_Camera.target_distance = M_CAM_GONG_BONG_DISTANCE;\n}\n\nstatic void M_BeastKill(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.flags = CF_FOLLOW_CENTRE;\n    g_Camera.target_angle = M_CAM_BEAST_KILL_ANGLE;\n    g_Camera.target_elevation = M_CAM_BEAST_KILL_ELEVATION;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->hit_direction = DIR_UNKNOWN;\n\n    if (item->current_anim_state == LS_EXTRA_TREX_KILL) {\n        if (Item_TestFrameRange(item, 0, M_LF_TREX_DEATH_TIMER_DELAY)) {\n            lara->death_timer = 1;\n        }\n    } else if (item->current_anim_state == LS_EXTRA_TORSO_KILL) {\n        g_Camera.target_distance = M_CAM_TORSO_KILL_DISTANCE;\n    }\n}\n\nstatic void M_WillardKill(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.type = CAM_CHASE;\n    g_Camera.flags = CF_FOLLOW_CENTRE;\n    g_Camera.target_angle = M_CAM_BEAST_KILL_ANGLE;\n    g_Camera.target_elevation = M_CAM_BEAST_KILL_ELEVATION;\n}\n\nstatic void M_RapidsDrown(ITEM *const item, COLL_INFO *const coll)\n{\n    Collide_GetCollisionInfo(\n        coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n        LARA_HEIGHT);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->pos.y = Room_GetHeight(sector, item->pos) + 384;\n\n    item->rot.y += 1024;\n\n    const int32_t time4 = (int32_t)Output_GetTimeInGame() * 4;\n    if ((time4 & 3) == 0) {\n        Sparks_TriggerWaterfallMist(\n            item->pos.x, item->pos.y, item->pos.z,\n            Random_GetControl() & 0x0FFF);\n    }\n}\n\nstatic void M_PullDagger(ITEM *const item, COLL_INFO *const coll)\n{\n    if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_PULLED)) {\n        Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_DAGGER_HAND);\n        Music_Play(MX_DAGGER_PULL, MPM_ONCE);\n    } else if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_STORED)) {\n        Lara_Skin_ClearEquipment(LM_HAND_R);\n        Inv_AddItem(O_PUZZLE_ITEM_2);\n        Stats_AddPickup();\n    } else if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_DISPLAY)) {\n        Overlay_AddDisplayPickup(O_PUZZLE_ITEM_2);\n    } else if (Item_TestFrameEqual(item, M_LF_DRAGON_DAGGER_ANIM_END)) {\n        item->rot.y += DEG_90;\n    }\n}\n\nstatic void M_StartHouse(ITEM *const item, COLL_INFO *const coll)\n{\n    if (Item_TestFrameEqual(item, M_LF_START_HOUSE_BEGIN)) {\n        Music_Play(MX_REVEAL_2, MPM_ONCE);\n        Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_DAGGER_HAND);\n    } else if (Item_TestFrameEqual(item, M_LF_START_HOUSE_DAGGER_STORED)) {\n        Lara_Skin_ClearEquipment(LM_HAND_R);\n        Lara_Skin_SetExtraEquipment(LM_HIPS, EXTRA_MESH_DAGGER_HIPS);\n        Inv_AddItem(O_PUZZLE_ITEM_1);\n    }\n}\n\nstatic void M_EndHouse(ITEM *const item, COLL_INFO *const coll)\n{\n    item->hit_points = LARA_MAX_HITPOINTS;\n    Lara_SetControllable(false);\n\n    if (Item_TestFrameEqual(item, M_LF_SHOWER_START)) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        Lara_Skin_ClearEquipment(LM_TORSO);\n        Lara_Skin_SetCombatFace(false);\n        Lara_Skin_ClearEquipment(LM_HAND_R);\n        Lara_Skin_ClearEquipment(LM_HIPS);\n        Music_Play(MX_CUTSCENE_BATH, MPM_ONCE);\n    } else if (Item_TestFrameEqual(item, M_LF_SHOWER_SHOTGUN_PICKUP)) {\n        Lara_Skin_SetGunEquipment(LM_HAND_R, LGT_SHOTGUN);\n    } else if (Item_TestFrameEqual(item, -1)) {\n        Game_SetIsLevelComplete(true);\n    }\n\n    if (Music_GetCurrentPlayingTrack() == Music_ToGameID(MX_CUTSCENE_BATH)) {\n        const int32_t frame_num = Item_GetRelativeFrame(item);\n        const double ts = (frame_num - M_LF_SHOWER_START) / (double)LOGIC_FPS;\n        Music_SyncTimestamp(ts);\n    }\n}\n\nstatic void M_TrainKill(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.num = Camera_GetDynamicFixedObjectIdx();\n    g_Camera.type = CAM_FIXED;\n    g_Camera.speed = 1;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->hit_direction = DIR_UNKNOWN;\n    item->gravity = false;\n    item->hit_points = -1;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->pos.y = Room_GetHeight(sector, item->pos);\n\n    if (Item_TestFrameEqual(item, -30)) {\n        lara->death_timer = 1;\n    }\n}\n\nstatic void M_JailWakeUp(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!Item_TestFrameEqual(item, -2)) {\n        return;\n    }\n\n    Item_Animate(item);\n    if (!g_Config.gameplay.enable_cinematics) {\n        return;\n    }\n\n    XYZ_32 pos = {};\n    Lara_GetMeshPos(LM_HIPS, &pos);\n    item->pos.x = pos.x;\n    item->pos.z = pos.z;\n    item->interp.prev.pos = item->pos;\n    item->interp.prev.rot = item->rot;\n}\n\n// clang-format off\nREGISTER_LARA_EXTRA(LS_EXTRA_BREATH,         M_Breath)\nREGISTER_LARA_EXTRA(LS_EXTRA_SCION_PICKUP_1, M_ScionPedestal)\nREGISTER_LARA_EXTRA(LS_EXTRA_USE_MIDAS,      M_UseMidas)\nREGISTER_LARA_EXTRA(LS_EXTRA_MIDAS_KILL,     M_MidasKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_YETI_KILL,      M_YetiKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_GUARD_KILL,     M_YetiKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_SHARK_KILL,     M_SharkKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_AIRLOCK,        M_Airlock)\nREGISTER_LARA_EXTRA(LS_EXTRA_GONG_BONG,      M_GongBong)\nREGISTER_LARA_EXTRA(LS_EXTRA_TREX_KILL,      M_BeastKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_TORSO_KILL,     M_BeastKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_PULL_DAGGER,    M_PullDagger)\nREGISTER_LARA_EXTRA(LS_EXTRA_START_HOUSE,    M_StartHouse)\nREGISTER_LARA_EXTRA(LS_EXTRA_END_HOUSE,      M_EndHouse)\nREGISTER_LARA_EXTRA(LS_EXTRA_SHIVA_KILL,     M_BeastKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_WILLARD_KILL,   M_WillardKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_RAPIDS_DROWN,   M_RapidsDrown)\nREGISTER_LARA_EXTRA(LS_EXTRA_TRAIN_KILL,     M_TrainKill)\nREGISTER_LARA_EXTRA(LS_EXTRA_JAIL_WAKE_UP,   M_JailWakeUp)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state/jump.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_SCREAM_SPEED          (DAMAGE_START + DAMAGE_LENGTH) // = 154\n#define M_JUMP_TURN             ((DEG_1 * 1) + LARA_TURN_UNDO) // = 546\n#define M_FAST_FALL_SPEED       (FAST_FALL_SPEED + 3)          // = 131\n#define M_SWING_FAST_FALL_SPEED (M_FAST_FALL_SPEED + 2)        // = 133\n#define M_CAM_BACK_JUMP_ANGLE   (135 * DEG_1)                  // = 24570\n#define M_CAM_REACH_ANGLE       (85 * DEG_1)                   // = 15470\n#define M_CAM_ZIPLINE_ANGLE     (70 * DEG_1)                   // = 12740\n#define M_LF_NEUTRAL_TWIST_WADE_SPLASH -5\n// clang-format on\n\nstatic void M_Compress(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status != LWS_WADE) {\n        if (g_Input.forward\n            && Lara_FloorFront(item, item->rot.y, STEP_L) >= -STEPUP_HEIGHT) {\n            item->goal_anim_state = LS(LS_JUMP_FORWARD);\n            lara->move_angle = item->rot.y;\n        } else if (\n            g_Input.left\n            && Lara_FloorFront(item, item->rot.y - DEG_90, STEP_L)\n                >= -STEPUP_HEIGHT) {\n            item->goal_anim_state = LS(LS_JUMP_LEFT);\n            lara->move_angle = item->rot.y - DEG_90;\n        } else if (\n            g_Input.right\n            && Lara_FloorFront(item, item->rot.y + DEG_90, STEP_L)\n                >= -STEPUP_HEIGHT) {\n            item->goal_anim_state = LS(LS_JUMP_RIGHT);\n            lara->move_angle = item->rot.y + DEG_90;\n        } else if (\n            g_Input.back\n            && Lara_FloorFront(item, item->rot.y + DEG_180, STEP_L)\n                >= -STEPUP_HEIGHT) {\n            item->goal_anim_state = LS(LS_JUMP_BACK);\n            lara->move_angle = item->rot.y + DEG_180;\n        } else if (\n            g_Input.roll && g_Config.gameplay.enable_neutral_twists\n            && Lara_State_IsResponsive(LA_STAND_TO_JUMP)) {\n            item->goal_anim_state = LS(LS_RESPONSIVE);\n        }\n    }\n\n    if (item->fall_speed > M_FAST_FALL_SPEED) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n    }\n}\n\nstatic void M_UpJump(ITEM *const item, COLL_INFO *const coll)\n{\n    const int16_t fast_speed = g_Config.gameplay.enable_swing_cancel\n        ? M_SWING_FAST_FALL_SPEED\n        : M_FAST_FALL_SPEED;\n    if (item->fall_speed > fast_speed) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n    }\n}\n\nstatic void M_NeutralJumpRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    if (g_Input.jump && g_Input.roll) {\n        item->goal_anim_state = LS(LS_RESPONSIVE);\n    } else if (g_Input.jump) {\n        item->goal_anim_state = LS(LS_COMPRESS);\n    } else if (g_Input.roll) {\n        item->goal_anim_state = LS(LS_ROLL);\n    } else {\n        if (Item_TestFrameEqual(item, M_LF_NEUTRAL_TWIST_WADE_SPLASH)) {\n            Lara_Col_WadeSplash(item);\n        }\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic void M_ForwardJump(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->goal_anim_state == LS(LS_SWAN_DIVE)\n        || item->goal_anim_state == LS(LS_REACH)) {\n        item->goal_anim_state = LS(LS_JUMP_FORWARD);\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->goal_anim_state != LS(LS_DEATH)\n        && item->goal_anim_state != LS(LS_STOP)\n        && item->goal_anim_state != LS(LS_RUN)) {\n        if (g_Input.action && lara->gun_status == LGS_ARMLESS) {\n            item->goal_anim_state = LS(LS_REACH);\n        }\n        if (g_Config.gameplay.enable_jump_twists\n            && (g_Input.roll || g_Input.back)) {\n            item->goal_anim_state = LS(LS_TWIST);\n        }\n        if (g_Input.slow && lara->gun_status == LGS_ARMLESS) {\n            item->goal_anim_state = LS(LS_SWAN_DIVE);\n        }\n        if (item->fall_speed > M_FAST_FALL_SPEED) {\n            item->goal_anim_state = LS(LS_FAST_FALL);\n        }\n    }\n\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -M_JUMP_TURN);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, +M_JUMP_TURN);\n    }\n}\n\nstatic void M_BackJump(ITEM *const item, COLL_INFO *const coll)\n{\n    if (!Item_TestAnimEqual(item, LA(LA_HANG_TO_JUMP_BACK))) {\n        g_Camera.target_angle = M_CAM_BACK_JUMP_ANGLE;\n    }\n    if (item->fall_speed > M_FAST_FALL_SPEED) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n        return;\n    }\n\n    if (item->goal_anim_state == LS(LS_RUN)) {\n        item->goal_anim_state = LS(LS_STOP);\n    } else if (\n        g_Config.gameplay.enable_jump_twists\n        && (g_Input.forward || g_Input.roll)\n        && item->goal_anim_state != LS(LS_STOP)) {\n        item->goal_anim_state = LS(LS_TWIST);\n    }\n}\n\nstatic void M_SideJump(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->fall_speed > M_FAST_FALL_SPEED) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n        return;\n    }\n\n    // TODO: unused animation transition, perhaps look at restoring\n    const bool twist_input = item->current_anim_state == LS(LS_JUMP_LEFT)\n        ? g_Input.right\n        : g_Input.left;\n    if (g_Config.gameplay.enable_jump_twists && twist_input\n        && item->goal_anim_state != LS(LS_STOP)) {\n        item->goal_anim_state = LS(LS_TWIST);\n    }\n}\n\nstatic void M_FallBack(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->fall_speed > M_FAST_FALL_SPEED) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n        return;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.action && lara->gun_status == LGS_ARMLESS) {\n        item->goal_anim_state = LS(LS_REACH);\n    }\n}\n\nstatic void M_Reach(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_angle = M_CAM_REACH_ANGLE;\n    if (item->fall_speed > M_FAST_FALL_SPEED) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n    }\n}\n\nstatic void M_SwanDive(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n    if (item->fall_speed > M_FAST_FALL_SPEED\n        && item->goal_anim_state != LS(LS_DIVE)) {\n        item->goal_anim_state = LS(LS_FAST_DIVE);\n    }\n}\n\nstatic void M_FastDive(ITEM *item, COLL_INFO *coll)\n{\n    if (g_Config.gameplay.enable_jump_twists && g_Input.roll\n        && item->goal_anim_state == LS(LS_FAST_DIVE)) {\n        item->goal_anim_state = LS(LS_TWIST);\n    }\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 1;\n    item->speed = item->speed * 95 / 100;\n}\n\nstatic void M_FastFall(ITEM *const item, COLL_INFO *const coll)\n{\n    item->speed = item->speed * 95 / 100;\n    const bool scream = g_TRVersion == 1 ? (item->fall_speed >= M_SCREAM_SPEED)\n                                         : (item->fall_speed == M_SCREAM_SPEED);\n    if (scream && !g_Config.debug.enable_invulnerability) {\n        Sound_Effect(SFX_LARA_FALL, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Zipline(ITEM *const item, COLL_INFO *const coll)\n{\n    g_Camera.target_angle = M_CAM_ZIPLINE_ANGLE;\n\n    if (!g_Input.action) {\n        item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        Lara_Animate(item);\n        item->gravity = true;\n        item->speed = 100;\n        item->fall_speed = 40;\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->move_angle = item->rot.y;\n    }\n}\n\n// clang-format off\nREGISTER_LARA_STATE(LS_COMPRESS,     M_Compress)\nREGISTER_LARA_STATE(LS_JUMP_UP,      M_UpJump)\nREGISTER_LARA_STATE(LS_NEUTRAL_ROLL, M_NeutralJumpRoll)\nREGISTER_LARA_STATE(LS_JUMP_FORWARD, M_ForwardJump)\nREGISTER_LARA_STATE(LS_JUMP_BACK,    M_BackJump)\nREGISTER_LARA_STATE(LS_JUMP_RIGHT,   M_SideJump)\nREGISTER_LARA_STATE(LS_JUMP_LEFT,    M_SideJump)\nREGISTER_LARA_STATE(LS_FALL_BACK,    M_FallBack)\nREGISTER_LARA_STATE(LS_REACH,        M_Reach)\nREGISTER_LARA_STATE(LS_SWAN_DIVE,    M_SwanDive)\nREGISTER_LARA_STATE(LS_FAST_DIVE,    M_FastDive)\nREGISTER_LARA_STATE(LS_FAST_FALL,    M_FastFall)\nREGISTER_LARA_STATE(LS_ZIPLINE,      M_Zipline)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state/land.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_WALK_DIST                104\n#define M_WALK_BACK_DIST           140\n#define M_LF_ROLL                  2\n#define M_CANCEL_POSE_TIME         (10 * LOGIC_FPS)               // = 300\n#define M_CANCEL_POSE_CHANCE       0x40                           // = 64\n#define M_FAST_TURN                ((DEG_1 * 6) + LARA_TURN_UNDO) // = 1456\n#define M_FAST_FALL_SPEED          (FAST_FALL_SPEED + 3)          // = 131\n#define M_SPRINT_TURN_RATE         ((DEG_1 * 2) + 45)             // = 409\n#define M_SPRINT_TURN_MAX          (DEG_1 * 4)                    // = 728\n#define M_SPRINT_LEAN_MAX          (DEG_1 * 16)                   // = 2192\n#define M_CAM_SLIDE_ELEVATION      (-45 * DEG_1)                  // = -8190\n#define M_CAM_PUSH_BLOCK_ANGLE     (35 * DEG_1)                   // = 6370\n#define M_CAM_PUSH_BLOCK_ELEVATION (-25 * DEG_1)                  // = -4550\n#define M_CAM_PP_READY_ANGLE       (75 * DEG_1)                   // = 13650\n#define M_CAM_PICKUP_ANGLE         (-130 * DEG_1)                 // = -23660\n#define M_CAM_PICKUP_ELEVATION     (-15 * DEG_1)                  // = -2730\n#define M_CAM_PICKUP_DISTANCE      WALL_L                         // = 1024\n#define M_CAM_SWITCH_ON_ANGLE      (80 * DEG_1)                   // = 14560\n#define M_CAM_SWITCH_ON_ELEVATION  (-25 * DEG_1)                  // = -4550\n#define M_CAM_SWITCH_ON_DISTANCE   WALL_L                         // = 1024\n#define M_CAM_SWITCH_ON_SPEED      6\n#define M_CAM_USE_KEY_ANGLE        (-M_CAM_SWITCH_ON_ANGLE)       // = -14560\n#define M_CAM_USE_KEY_ELEVATION    M_CAM_SWITCH_ON_ELEVATION      // = -4550\n#define M_CAM_USE_KEY_DISTANCE     WALL_L                         // = 1024\n#define M_CAM_SPECIAL_ANGLE        (170 * DEG_1)                  // = 30940\n#define M_CAM_SPECIAL_ELEVATION    (-25 * DEG_1)                  // = -4550\n#define M_CAM_SPECIAL_DISTANCE     (2 * WALL_L)                   // = 2048\n#define M_CAM_POSE_RIGHT_ANGLE     M_CAM_SPECIAL_ANGLE            // = 30940\n#define M_CAM_POSE_LEFT_ANGLE     -M_CAM_SPECIAL_ANGLE            // = -30940\n// clang-format on\n\nstatic bool m_JumpPermitted = true;\nstatic const int16_t m_JumpLockFrames[JUMP_LOCK_NUMBER_OF] = {\n    // clang-format off\n    [JUMP_LOCK_LEGACY]   = 4,\n    [JUMP_LOCK_TUNED]    = 2,\n    [JUMP_LOCK_DISABLED] = 19,\n    // clang-format on\n};\n\nstatic bool M_CanPose(void)\n{\n    if (g_Config.gameplay.idle_pose_timeout == 0) {\n        return false;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    return !g_Input.draw && !g_Input.look && lara->hit_direction == DIR_UNKNOWN\n        && lara->gun_status == LGS_ARMLESS\n        && lara->water_status == LWS_ABOVE_WATER && !g_Input.use_flare\n        && !lara->flare.control && !Lara_Vehicle_IsMounted();\n}\n\nstatic bool M_ShouldStopPosing(void)\n{\n    return !M_CanPose() || g_Input.forward || g_Input.back || g_Input.left\n        || g_Input.right || g_Input.step_left || g_Input.step_right\n        || g_Input.jump || g_Input.action;\n}\n\nstatic void M_Default(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n}\n\nstatic void M_PullUp(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    if (g_Input.forward && Item_TestAnimEqual(item, LA(LA_CLIMB_2CLICK_END))) {\n        item->goal_anim_state = LS(LS_RUN);\n    }\n}\n\nstatic void M_Walk(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -LARA_SLOW_TURN);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, +LARA_SLOW_TURN);\n    }\n\n    if (g_Input.forward) {\n        if (lara->water_status == LWS_WADE) {\n            item->goal_anim_state = LS(LS_WADE);\n        } else if (g_Input.slow) {\n            item->goal_anim_state = LS(LS_WALK);\n        } else {\n            if (g_Config.gameplay.fix_walk_run_jump) {\n                m_JumpPermitted = true;\n            }\n            item->goal_anim_state = LS(LS_RUN);\n        }\n    } else {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic LARA_STATE M_GetRunToCrouchState(void)\n{\n    return LS(\n        g_Config.gameplay.enable_responsive_crawl ? LS_CROUCH_IDLE : LS_STOP);\n}\n\nstatic bool M_RequestSprint(LARA_INFO *const lara)\n{\n    if (g_Config.gameplay.enable_toggle_sprint) {\n        if (g_InputDB.sprint) {\n            lara->sprinting = !lara->sprinting;\n        }\n        return lara->sprinting;\n    } else {\n        lara->sprinting = false;\n        return g_Input.sprint;\n    }\n}\n\nstatic bool M_RequestDuck(LARA_INFO *const lara)\n{\n    if (g_Config.gameplay.enable_toggle_crouch) {\n        if (g_InputDB.crouch) {\n            lara->crouching = true;\n        }\n        return lara->crouching;\n    } else {\n        lara->crouching = false;\n        return g_Input.crouch;\n    }\n}\n\nstatic void M_Run(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_DEATH);\n        return;\n    }\n\n    if (g_Input.roll) {\n        Lara_Col_WadeSplash(item);\n        item->current_anim_state = LS(LS_ROLL);\n        item->goal_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(item, LA(LA_ROLL_START), M_LF_ROLL);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool sprint_requested = M_RequestSprint(lara);\n    if ((item->hit_points <= 0 || lara->sprint_timer <= 0\n         || lara->water_status == LWS_WADE || !g_Config.gameplay.enable_sprint)\n        && g_Config.gameplay.enable_toggle_sprint) {\n        lara->sprinting = false;\n    }\n\n    if (sprint_requested && g_Config.gameplay.enable_sprint\n        && lara->water_status != LWS_WADE\n        && item->current_anim_state == LS(LS_RUN) && lara->sprint_timer > 0\n        && (g_Config.gameplay.enable_responsive_sprint\n            || lara->sprint_timer == LARA_MAX_SPRINT)) {\n        item->goal_anim_state = LS(LS_SPRINT);\n        return;\n    }\n\n    if (M_RequestDuck(lara)) {\n        item->goal_anim_state = M_GetRunToCrouchState();\n        return;\n    }\n\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -M_FAST_TURN);\n        item->rot.z -= LARA_LEAN_RATE;\n        CLAMPL(item->rot.z, -LARA_LEAN_MAX);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, +M_FAST_TURN);\n        item->rot.z += LARA_LEAN_RATE;\n        CLAMPG(item->rot.z, +LARA_LEAN_MAX);\n    }\n\n    const bool responsive_jumping =\n        g_Config.gameplay.enable_tr2_jumping && Lara_State_IsResponsive(LA_RUN);\n    if (responsive_jumping) {\n        const int16_t unlock_frame =\n            m_JumpLockFrames[g_Config.gameplay.jump_lock_mode];\n        if (Item_TestAnimEqual(item, LA(LA_RUN_START))) {\n            m_JumpPermitted =\n                g_Config.gameplay.jump_lock_mode == JUMP_LOCK_DISABLED;\n        } else if (\n            !Item_TestAnimEqual(item, LA(LA_RUN))\n            || Item_TestFrameEqual(item, unlock_frame)) {\n            m_JumpPermitted = true;\n        }\n    } else {\n        m_JumpPermitted = true;\n    }\n\n    if (g_Input.jump && m_JumpPermitted && !item->gravity) {\n        item->goal_anim_state =\n            LS(responsive_jumping ? LS_RESPONSIVE : LS_JUMP_FORWARD);\n    } else if (g_Input.forward) {\n        if (lara->water_status == LWS_WADE) {\n            item->goal_anim_state = LS(LS_WADE);\n        } else if (g_Input.slow) {\n            item->goal_anim_state = LS(LS_WALK);\n        } else {\n            item->goal_anim_state = LS(LS_RUN);\n        }\n    } else {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic void M_Wade(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->sprinting = false;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    g_Camera.target_elevation = CAM_WADE_ELEVATION;\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (room->flags.swamp) {\n        if (g_Input.left) {\n            lara->turn_rate -= LARA_TURN_RATE;\n            CLAMPL(lara->turn_rate, -LARA_SLOW_TURN);\n            item->rot.z -= LARA_LEAN_RATE;\n            CLAMPL(item->rot.z, -LARA_LEAN_MAX / 2);\n        } else if (g_Input.right) {\n            lara->turn_rate += LARA_TURN_RATE;\n            CLAMPG(lara->turn_rate, LARA_SLOW_TURN);\n            item->rot.z += LARA_LEAN_RATE;\n            CLAMPG(item->rot.z, LARA_LEAN_MAX / 2);\n        }\n\n        if (g_Input.forward) {\n            item->goal_anim_state = LS(LS_WADE);\n        } else {\n            item->goal_anim_state = LS(LS_STOP);\n        }\n    } else {\n        if (g_Input.left) {\n            lara->turn_rate -= LARA_TURN_RATE;\n            CLAMPL(lara->turn_rate, -M_FAST_TURN);\n            item->rot.z -= LARA_LEAN_RATE;\n            CLAMPL(item->rot.z, -LARA_LEAN_MAX);\n        } else if (g_Input.right) {\n            lara->turn_rate += LARA_TURN_RATE;\n            CLAMPG(lara->turn_rate, M_FAST_TURN);\n            item->rot.z += LARA_LEAN_RATE;\n            CLAMPG(item->rot.z, LARA_LEAN_MAX);\n        }\n\n        if (g_Input.forward) {\n            if (lara->water_status != LWS_ABOVE_WATER) {\n                item->goal_anim_state = LS(LS_WADE);\n            } else {\n                item->goal_anim_state = LS(LS_RUN);\n            }\n        } else {\n            item->goal_anim_state = LS(LS_STOP);\n        }\n    }\n}\n\nstatic void M_WalkBack(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.back && (g_Input.slow || lara->water_status == LWS_WADE)) {\n        item->goal_anim_state = LS(LS_WALK_BACK);\n    } else {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -LARA_SLOW_TURN);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, LARA_SLOW_TURN);\n    }\n}\n\nstatic void M_Stop(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->sprinting = false;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_DEATH);\n        return;\n    }\n\n    if (lara->interact_target.is_moving) {\n        return;\n    }\n\n    if (M_RequestDuck(lara) && lara->water_status != LWS_WADE\n        && item->current_anim_state == LS(LS_STOP)\n        && (lara->gun_status == LGS_ARMLESS || !Gun_IsRifleType(lara->gun_type))\n        && !Lara_Vehicle_IsMounted()) {\n        item->goal_anim_state = LS(LS_CROUCH_IDLE);\n        return;\n    }\n\n    if (g_Input.roll && lara->water_status != LWS_WADE) {\n        if (g_Input.jump && g_Config.gameplay.enable_neutral_twists\n            && Item_TestAnimEqual(item, LA(LA_STAND_IDLE))\n            && Lara_State_IsResponsive(LA_STAND_TO_JUMP)) {\n            item->current_anim_state = LS(LS_NEUTRAL_ROLL);\n            item->goal_anim_state = LS(LS_STOP);\n            Item_SwitchToAnim(item, LA(LA_JUMP_NEUTRAL_ROLL), 0);\n        } else if (!g_Input.jump || !g_Config.gameplay.enable_neutral_twists) {\n            Lara_Col_WadeSplash(item);\n            item->current_anim_state = LS(LS_ROLL);\n            item->goal_anim_state = LS(LS_STOP);\n            Item_SwitchToAnim(item, LA(LA_ROLL_START), M_LF_ROLL);\n        }\n        return;\n    }\n\n    lara->crouching = false;\n    item->goal_anim_state = LS(LS_STOP);\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n        if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) {\n            Lara_Look_LeftRight();\n            return;\n        }\n    }\n\n    int32_t fheight = NO_HEIGHT;\n    int32_t rheight = NO_HEIGHT;\n    if (g_Input.forward) {\n        fheight = Lara_FloorFront(item, item->rot.y, M_WALK_DIST);\n    } else if (g_Input.back) {\n        rheight =\n            Lara_FloorFront(item, item->rot.y + DEG_180, M_WALK_BACK_DIST);\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (room->flags.swamp) {\n        if (g_Input.left) {\n            item->goal_anim_state = LS(LS_TURN_LEFT);\n        } else if (g_Input.right) {\n            item->goal_anim_state = LS(LS_TURN_RIGHT);\n        }\n    } else if (g_Input.step_left) {\n        const int32_t h = Lara_FloorFront(item, item->rot.y - DEG_90, 148);\n        const int32_t c =\n            Lara_CeilingFront(item, item->rot.y - DEG_90, 148, LARA_HEIGHT);\n        if (g_TRVersion < 3\n            || (h < 128 && h > -128 && Room_GetHeightType() != HT_BIG_SLOPE\n                && c <= 0)) {\n            item->goal_anim_state = LS(LS_STEP_LEFT);\n        }\n    } else if (g_Input.step_right) {\n        const int32_t h = Lara_FloorFront(item, item->rot.y + DEG_90, 148);\n        const int32_t c =\n            Lara_CeilingFront(item, item->rot.y + DEG_90, 148, LARA_HEIGHT);\n        if (g_TRVersion < 3\n            || (h < 128 && h > -128 && Room_GetHeightType() != HT_BIG_SLOPE\n                && c <= 0)) {\n            item->goal_anim_state = LS(LS_STEP_RIGHT);\n        }\n    } else if (g_Input.left) {\n        item->goal_anim_state = LS(LS_TURN_LEFT);\n    } else if (g_Input.right) {\n        item->goal_anim_state = LS(LS_TURN_RIGHT);\n    }\n\n    if (lara->water_status == LWS_WADE) {\n        if (g_Input.jump && !room->flags.swamp) {\n            item->goal_anim_state = LS(LS_COMPRESS);\n        }\n\n        if (g_Input.forward) {\n            if (room->flags.swamp || g_Input.slow) {\n                M_Wade(item, coll);\n            } else {\n                M_Walk(item, coll);\n            }\n        } else if (g_Input.back) {\n            M_WalkBack(item, coll);\n        }\n    } else if (g_Input.jump) {\n        item->goal_anim_state = LS(LS_COMPRESS);\n    } else if (g_Input.forward) {\n        bool bad_floor = false;\n        bool bad_ceiling = false;\n        if (g_Config.gameplay.wall_glitch_mode == WALL_GLITCH_FIXED) {\n            const int32_t h = Lara_FloorFront(item, item->rot.y, M_WALK_DIST);\n            const int32_t c =\n                Lara_CeilingFront(item, item->rot.y, M_WALK_DIST, LARA_HEIGHT);\n            const HEIGHT_TYPE height_type = Room_GetHeightType();\n            bad_floor = height_type == HT_BIG_SLOPE && h < 0;\n            bad_ceiling = c > 0 && !g_Input.action;\n        }\n\n        if (bad_floor || bad_ceiling) {\n            item->goal_anim_state = LS_STOP;\n        } else if (g_Input.slow) {\n            M_Walk(item, coll);\n        } else {\n            M_Run(item, coll);\n        }\n    } else if (g_Input.back) {\n        if (g_Input.slow) {\n            if (g_TRVersion < 3\n                || (rheight < (STEPUP_HEIGHT - 1)\n                    && rheight > -(STEPUP_HEIGHT - 1)\n                    && Room_GetHeightType() != HT_BIG_SLOPE)) {\n                M_WalkBack(item, coll);\n            }\n        } else if (g_TRVersion < 3 || rheight > -(STEPUP_HEIGHT - 1)) {\n            item->goal_anim_state = LS(LS_FAST_BACK);\n        }\n    }\n\n    if (item->goal_anim_state == LS(LS_STOP) && M_CanPose()) {\n        lara->idle_timer++;\n        const int32_t timeout = g_Config.gameplay.idle_pose_timeout * LOGIC_FPS;\n        CLAMPG(lara->idle_timer, timeout);\n        if (lara->idle_timer == timeout) {\n            lara->idle_timer = 0;\n            item->goal_anim_state =\n                LS(Random_GetControl() < 0x4000 ? LS_POSE_LEFT : LS_POSE_RIGHT);\n        }\n    } else {\n        lara->idle_timer = 0;\n    }\n}\n\nstatic void M_Pose(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_DEATH);\n        return;\n    }\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n        if (g_Config.gameplay.look_mode == LOOK_MODE_RESTRICTED) {\n            Lara_Look_LeftRight();\n            return;\n        }\n    }\n\n    bool cancel_camera = false;\n    if (g_Input.roll && !g_Input.jump) {\n        item->goal_anim_state = LS(LS_ROLL);\n        cancel_camera = true;\n    } else if (item->current_anim_state == LS(LS_POSE)) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->idle_timer++;\n        if (M_ShouldStopPosing()\n            || (lara->idle_timer >= M_CANCEL_POSE_TIME\n                && Random_GetControl() < M_CANCEL_POSE_CHANCE)) {\n            item->goal_anim_state = LS(LS_STOP);\n            cancel_camera = true;\n        }\n    }\n\n    if (g_Config.gameplay.enable_idle_pose_camera) {\n        if (item->current_anim_state == LS(LS_POSE_START)\n            && Item_TestFrameEqual(item, -1) && g_Camera.type == CAM_CHASE) {\n            g_Camera.additional_angle =\n                Item_TestAnimEqual(item, LA(LA_POSE_RIGHT_START))\n                ? M_CAM_POSE_RIGHT_ANGLE\n                : M_CAM_POSE_LEFT_ANGLE;\n        } else if (cancel_camera) {\n            g_Camera.additional_angle = 0;\n        }\n    }\n}\n\nstatic void M_FastBack(ITEM *const item, COLL_INFO *const coll)\n{\n    item->goal_anim_state = LS(LS_STOP);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -LARA_MED_TURN);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, LARA_MED_TURN);\n    }\n}\n\nstatic void M_Turn(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    const bool left_turn = item->current_anim_state == LS(LS_TURN_LEFT);\n    const bool turn_input = left_turn ? g_Input.left : g_Input.right;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (left_turn) {\n        lara->turn_rate -= LARA_TURN_RATE;\n    } else {\n        lara->turn_rate += LARA_TURN_RATE;\n    }\n\n    if (lara->gun_status == LGS_READY) {\n        item->goal_anim_state = LS(LS_FAST_TURN);\n    } else if (left_turn && lara->turn_rate < -LARA_SLOW_TURN) {\n        if (g_Input.slow) {\n            lara->turn_rate = -LARA_SLOW_TURN;\n        } else {\n            item->goal_anim_state = LS(LS_FAST_TURN);\n        }\n    } else if (!left_turn && lara->turn_rate > LARA_SLOW_TURN) {\n        if (g_Input.slow) {\n            lara->turn_rate = LARA_SLOW_TURN;\n        } else {\n            item->goal_anim_state = LS(LS_FAST_TURN);\n        }\n    }\n\n    if (g_Input.forward) {\n        if (lara->water_status == LWS_WADE) {\n            item->goal_anim_state = LS(LS_WADE);\n        } else if (g_Input.slow) {\n            item->goal_anim_state = LS(LS_WALK);\n        } else {\n            item->goal_anim_state = LS(LS_RUN);\n        }\n    } else if (!turn_input) {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic void M_FastTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->turn_rate >= 0) {\n        lara->turn_rate = M_FAST_TURN;\n        if (!g_Input.right) {\n            item->goal_anim_state = LS(LS_STOP);\n        }\n    } else {\n        lara->turn_rate = -M_FAST_TURN;\n        if (!g_Input.left) {\n            item->goal_anim_state = LS(LS_STOP);\n        }\n    }\n}\n\nstatic void M_SideStep(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_STOP);\n        return;\n    }\n\n    const bool step_input = item->current_anim_state == LS(LS_STEP_LEFT)\n        ? g_Input.step_left\n        : g_Input.step_right;\n    if (!step_input) {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -LARA_SLOW_TURN);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, LARA_SLOW_TURN);\n    }\n}\n\nstatic void M_Slide(ITEM *const item, COLL_INFO *const coll)\n{\n    const bool sliding_forward = item->current_anim_state == LS(LS_SLIDE);\n    bool opposite_input;\n    if (sliding_forward) {\n        g_Camera.flags = CF_NO_CHUNKY;\n        g_Camera.target_elevation = M_CAM_SLIDE_ELEVATION;\n        opposite_input = g_Input.back;\n    } else {\n        opposite_input = g_Input.forward;\n    }\n\n    if (Item_TestAnimEqual(item, LA(LA_SLIDE_FORWARD_TO_RUN))) {\n        item->goal_anim_state =\n            LS(g_Input.sprint && g_Config.gameplay.enable_sprint ? LS_SPRINT\n                                                                 : LS_RUN);\n    } else if (\n        sliding_forward && g_Config.gameplay.enable_slide_to_run\n        && item->goal_anim_state == LS(LS_STOP) && g_Input.forward\n        && Lara_State_IsResponsive(LA_SLIDE_FORWARD)) {\n        item->goal_anim_state = LS(LS_RESPONSIVE);\n    } else if (\n        g_Input.jump\n        && (!g_Config.gameplay.enable_jump_twists || !opposite_input)) {\n        item->goal_anim_state =\n            LS(sliding_forward ? LS_JUMP_FORWARD : LS_JUMP_BACK);\n    }\n}\n\nstatic void M_Roll(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n}\n\nstatic void M_PushBlock(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    g_Camera.flags = CF_FOLLOW_CENTRE;\n    g_Camera.target_angle = M_CAM_PUSH_BLOCK_ANGLE;\n    g_Camera.target_elevation = M_CAM_PUSH_BLOCK_ELEVATION;\n}\n\nstatic void M_PPReady(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    g_Camera.target_angle = M_CAM_PP_READY_ANGLE;\n    if (!g_Input.action) {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic void M_Pickup(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    g_Camera.target_angle = M_CAM_PICKUP_ANGLE;\n    g_Camera.target_elevation = M_CAM_PICKUP_ELEVATION;\n    g_Camera.target_distance = M_CAM_PICKUP_DISTANCE;\n\n    if (item->current_anim_state == LS(LS_FLARE_PICKUP)\n        && Item_TestFrameEqual(item, -1)) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->gun_status = LGS_ARMLESS;\n    }\n}\n\nstatic void M_SwitchOn(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    g_Camera.target_angle = M_CAM_SWITCH_ON_ANGLE;\n    g_Camera.target_elevation = M_CAM_SWITCH_ON_ELEVATION;\n    g_Camera.target_distance = M_CAM_SWITCH_ON_DISTANCE;\n    g_Camera.speed = M_CAM_SWITCH_ON_SPEED;\n}\n\nstatic void M_UseKey(ITEM *const item, COLL_INFO *const coll)\n{\n    M_Default(item, coll);\n    g_Camera.target_angle = M_CAM_USE_KEY_ANGLE;\n    g_Camera.target_elevation = M_CAM_USE_KEY_ELEVATION;\n    g_Camera.target_distance = M_CAM_USE_KEY_DISTANCE;\n}\n\nstatic void M_Special(ITEM *const item, COLL_INFO *const coll)\n{\n    ITEM *const target_item = Lara_GetDeathCameraTarget();\n    if (target_item != nullptr) {\n        g_Camera.item = target_item;\n        g_Camera.flags = CF_CHASE_OBJECT;\n        g_Camera.type = CAM_FIXED;\n        g_Camera.target_angle = item->rot.y;\n        g_Camera.target_distance = M_CAM_SPECIAL_DISTANCE;\n    } else {\n        g_Camera.flags = CF_FOLLOW_CENTRE;\n        g_Camera.target_angle = M_CAM_SPECIAL_ANGLE;\n    }\n    g_Camera.target_elevation = M_CAM_SPECIAL_ELEVATION;\n}\n\nstatic void M_Sprint(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (item->hit_points <= 0 || lara->sprint_timer <= 0\n        || lara->water_status == LWS_WADE) {\n        lara->sprinting = false;\n        item->goal_anim_state = LS(LS_RUN);\n        return;\n    }\n\n    if (g_Config.gameplay.enable_toggle_sprint\n            ? (!lara->sprinting || g_InputDB.sprint)\n            : !g_Input.sprint) {\n        lara->sprinting = false;\n        item->goal_anim_state = LS(LS_RUN);\n        return;\n    }\n\n    if (!g_Config.debug.enable_endless_sprint) {\n        lara->sprint_timer--;\n    }\n\n    if (g_Input.roll) {\n        Lara_Col_WadeSplash(item);\n        lara->sprinting = false;\n        item->current_anim_state = LS(LS_ROLL);\n        item->goal_anim_state = LS(LS_STOP);\n        Item_SwitchToAnim(item, LA(LA_ROLL_START), M_LF_ROLL);\n        return;\n    }\n\n    if (M_RequestDuck(lara)) {\n        item->goal_anim_state = M_GetRunToCrouchState();\n        return;\n    }\n    if (g_Input.left) {\n        lara->turn_rate -= M_SPRINT_TURN_RATE;\n        CLAMPL(lara->turn_rate, -M_SPRINT_TURN_MAX);\n        item->rot.z -= LARA_LEAN_RATE;\n        CLAMPL(item->rot.z, -M_SPRINT_LEAN_MAX);\n    } else if (g_Input.right) {\n        lara->turn_rate += M_SPRINT_TURN_RATE;\n        CLAMPG(lara->turn_rate, M_SPRINT_TURN_MAX);\n        item->rot.z += LARA_LEAN_RATE;\n        CLAMPG(item->rot.z, M_SPRINT_LEAN_MAX);\n    }\n\n    if (g_Input.jump && !item->gravity) {\n        item->goal_anim_state = LS(LS_SPRINT_ROLL);\n    } else if (g_Input.forward) {\n        item->goal_anim_state = LS(g_Input.slow ? LS_WALK : LS_SPRINT);\n    } else if (!g_Input.left && !g_Input.right) {\n        item->goal_anim_state = LS(LS_STOP);\n    }\n}\n\nstatic void M_SprintRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->sprinting = false;\n\n    if (item->goal_anim_state != LS(LS_DEATH)\n        && item->goal_anim_state != LS(LS_STOP)\n        && item->goal_anim_state != LS(LS_RUN)\n        && item->fall_speed > M_FAST_FALL_SPEED) {\n        item->goal_anim_state = LS(LS_FAST_FALL);\n    }\n}\n\n// clang-format off\nREGISTER_LARA_STATE(LS_GYMNAST,      M_Default)\nREGISTER_LARA_STATE(LS_PULL_UP,      M_PullUp)\nREGISTER_LARA_STATE(LS_WALK,         M_Walk)\nREGISTER_LARA_STATE(LS_RUN,          M_Run)\nREGISTER_LARA_STATE(LS_STOP,         M_Stop)\nREGISTER_LARA_STATE(LS_POSE,         M_Pose)\nREGISTER_LARA_STATE(LS_POSE_START,   M_Pose)\nREGISTER_LARA_STATE(LS_POSE_END,     M_Pose)\nREGISTER_LARA_STATE(LS_FAST_BACK,    M_FastBack)\nREGISTER_LARA_STATE(LS_TURN_RIGHT,   M_Turn)\nREGISTER_LARA_STATE(LS_TURN_LEFT,    M_Turn)\nREGISTER_LARA_STATE(LS_FAST_TURN,    M_FastTurn)\nREGISTER_LARA_STATE(LS_DEATH,        M_Default)\nREGISTER_LARA_STATE(LS_WALK_BACK,    M_WalkBack)\nREGISTER_LARA_STATE(LS_STEP_RIGHT,   M_SideStep)\nREGISTER_LARA_STATE(LS_STEP_LEFT,    M_SideStep)\nREGISTER_LARA_STATE(LS_SLIDE,        M_Slide)\nREGISTER_LARA_STATE(LS_SLIDE_BACK,   M_Slide)\nREGISTER_LARA_STATE(LS_ROLL,         M_Roll)\nREGISTER_LARA_STATE(LS_ROLL_CONT,    M_Roll)\nREGISTER_LARA_STATE(LS_PUSH_BLOCK,   M_PushBlock)\nREGISTER_LARA_STATE(LS_PULL_BLOCK,   M_PushBlock)\nREGISTER_LARA_STATE(LS_PP_READY,     M_PPReady)\nREGISTER_LARA_STATE(LS_PICKUP,       M_Pickup)\nREGISTER_LARA_STATE(LS_SWITCH_ON,    M_SwitchOn)\nREGISTER_LARA_STATE(LS_SWITCH_OFF,   M_SwitchOn)\nREGISTER_LARA_STATE(LS_USE_KEY,      M_UseKey)\nREGISTER_LARA_STATE(LS_USE_PUZZLE,   M_UseKey)\nREGISTER_LARA_STATE(LS_SPECIAL,      M_Special)\nREGISTER_LARA_STATE(LS_WADE,         M_Wade)\nREGISTER_LARA_STATE(LS_SPRINT,       M_Sprint)\nREGISTER_LARA_STATE(LS_SPRINT_ROLL,  M_SprintRoll)\nREGISTER_LARA_STATE(LS_CONTROLLED,   M_Default)\nREGISTER_LARA_STATE(LS_FLARE_PICKUP, M_Pickup)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state/monkey.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n#include <trx/game/rooms.h>\n#include <trx/game/rooms/enum.h>\n\n// clang-format off\n#define M_CAM_MONKEY_ELEVATION  (10 * DEG_1)  // = 1820\n#define M_CAM_HANG_ANGLE        0\n#define M_CAM_HANG_ELEVATION    (-60 * DEG_1) // = -10920\n#define M_MONKEY_TURN           ((DEG_1 * 1) + LARA_TURN_UNDO) // = 546\n// clang-format on\n\nstatic bool M_CanMonkeySwing(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(\n        (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n    return (sector->ladder & LADDER_CEILING) != 0;\n}\n\nstatic void M_MonkeyIdle(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n\n    if (M_CanMonkeySwing(item)) {\n        if (g_Input.action && item->hit_points > 0) {\n            g_Camera.target_angle = M_CAM_HANG_ANGLE;\n            g_Camera.target_elevation = M_CAM_HANG_ELEVATION;\n        }\n        return;\n    }\n\n    if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) {\n        Lara_Look_UpDown();\n    }\n}\n\nstatic void M_MonkeyForward(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_MONKEY_IDLE);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->move_angle = item->rot.y;\n\n    if (g_Input.forward) {\n        item->goal_anim_state = LS(LS_MONKEY_FORWARD);\n    } else {\n        item->goal_anim_state = LS(LS_MONKEY_IDLE);\n    }\n\n    if (g_Input.left) {\n        lara->turn_rate -= LARA_TURN_RATE;\n        CLAMPL(lara->turn_rate, -M_MONKEY_TURN);\n    } else if (g_Input.right) {\n        lara->turn_rate += LARA_TURN_RATE;\n        CLAMPG(lara->turn_rate, +M_MONKEY_TURN);\n    }\n}\n\nstatic void M_MonkeyShimmy(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->current_anim_state == LS(LS_MONKEY_LEFT)) {\n        lara->move_angle = item->rot.y - DEG_90;\n    } else {\n        lara->move_angle = item->rot.y + DEG_90;\n    }\n\n    const bool stop = item->current_anim_state == LS(LS_MONKEY_LEFT)\n        ? !g_Input.step_left\n        : !g_Input.step_right;\n    if (stop) {\n        item->goal_anim_state = LS(LS_MONKEY_IDLE);\n    }\n}\n\nstatic void M_MonkeyTurn(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    g_Camera.target_elevation = M_CAM_MONKEY_ELEVATION;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_MONKEY_IDLE);\n        return;\n    }\n\n    if (item->current_anim_state == LS(LS_MONKEY_TURN_LEFT)) {\n        item->rot.y -= LARA_LEAN_RATE;\n        if (!g_Input.left) {\n            item->goal_anim_state = LS(LS_MONKEY_IDLE);\n        }\n    } else {\n        item->rot.y += LARA_LEAN_RATE;\n        if (!g_Input.right) {\n            item->goal_anim_state = LS(LS_MONKEY_IDLE);\n        }\n    }\n}\n\nstatic void M_MonkeyRoll(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    item->goal_anim_state = LS(LS_MONKEY_IDLE);\n}\n\n// clang-format off\nREGISTER_LARA_STATE(LS_MONKEY_IDLE,       M_MonkeyIdle)\nREGISTER_LARA_STATE(LS_MONKEY_FORWARD,    M_MonkeyForward)\nREGISTER_LARA_STATE(LS_MONKEY_LEFT,       M_MonkeyShimmy)\nREGISTER_LARA_STATE(LS_MONKEY_RIGHT,      M_MonkeyShimmy)\nREGISTER_LARA_STATE(LS_MONKEY_TURN_LEFT,  M_MonkeyTurn)\nREGISTER_LARA_STATE(LS_MONKEY_TURN_RIGHT, M_MonkeyTurn)\nREGISTER_LARA_STATE(LS_MONKEY_ROLL,       M_MonkeyRoll)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state/swim.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/util.h>\n\n// clang-format off\n#define M_FRICTION       6\n#define M_LEAN_RATE      (2 * LARA_LEAN_RATE) // = 546\n#define M_TURN_RATE      (2 * DEG_1)          // = 364\n#define M_MAX_SURF_SPEED 60\n#define M_MAX_SWIM_SPEED 200\n// clang-format on\n\nstatic void M_SwimTurn(ITEM *const item)\n{\n    if (g_Input.forward) {\n        item->rot.x -= M_TURN_RATE;\n    } else if (g_Input.back) {\n        item->rot.x += M_TURN_RATE;\n    }\n\n    if (g_Config.gameplay.enable_tr2_swimming) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        if (g_Input.left) {\n            lara->turn_rate -= LARA_TURN_RATE;\n            CLAMPL(lara->turn_rate, -LARA_MED_TURN);\n            item->rot.z -= M_LEAN_RATE;\n        } else if (g_Input.right) {\n            lara->turn_rate += LARA_TURN_RATE;\n            CLAMPG(lara->turn_rate, LARA_MED_TURN);\n            item->rot.z += M_LEAN_RATE;\n        }\n    } else {\n        if (g_Input.left) {\n            item->rot.y -= LARA_MED_TURN;\n            item->rot.z -= M_LEAN_RATE;\n        } else if (g_Input.right) {\n            item->rot.y += LARA_MED_TURN;\n            item->rot.z += M_LEAN_RATE;\n        }\n    }\n}\n\nstatic void M_Tread(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_UW_DEATH);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    if (g_Config.gameplay.enable_uw_roll && g_Input.roll) {\n        item->current_anim_state = LS(LS_WATER_ROLL);\n        Item_SwitchToAnim(item, LA(LA_UNDERWATER_ROLL_START), 0);\n        return;\n    }\n\n    if (g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED && g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    M_SwimTurn(item);\n    if (g_Input.jump) {\n        item->goal_anim_state = LS(LS_SWIM);\n    }\n    item->fall_speed -= M_FRICTION;\n    CLAMPL(item->fall_speed, 0);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_status == LGS_HANDS_BUSY) {\n        lara->gun_status = LGS_ARMLESS;\n    }\n}\n\nstatic void M_Swim(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_UW_DEATH);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    if (g_Config.gameplay.enable_uw_roll && g_Input.roll) {\n        item->current_anim_state = LS(LS_WATER_ROLL);\n        Item_SwitchToAnim(item, LA(LA_UNDERWATER_ROLL_START), 0);\n        return;\n    }\n\n    M_SwimTurn(item);\n    item->fall_speed += 8;\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status == LWS_CHEAT) {\n        CLAMPG(item->fall_speed, M_MAX_SWIM_SPEED * 2);\n    } else {\n        CLAMPG(item->fall_speed, M_MAX_SWIM_SPEED);\n    }\n\n    if (!g_Input.jump) {\n        item->goal_anim_state =\n            LS(g_Config.gameplay.enable_tr2_swim_cancel\n                       && Lara_State_IsResponsive(LA_UNDERWATER_SWIM_FORWARD)\n                   ? LS_RESPONSIVE\n                   : LS_GLIDE);\n    }\n}\n\nstatic void M_Glide(ITEM *item, COLL_INFO *coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_UW_DEATH);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    if (g_Config.gameplay.enable_uw_roll && g_Input.roll) {\n        item->current_anim_state = LS(LS_WATER_ROLL);\n        Item_SwitchToAnim(item, LA(LA_UNDERWATER_ROLL_START), 0);\n        return;\n    }\n\n    M_SwimTurn(item);\n    if (g_Input.jump) {\n        item->goal_anim_state = LS(LS_SWIM);\n    }\n    item->fall_speed -= M_FRICTION;\n    CLAMPL(item->fall_speed, 0);\n    if (item->fall_speed <= M_MAX_SWIM_SPEED * 2 / 3) {\n        item->goal_anim_state = LS(LS_TREAD);\n    }\n}\n\nstatic void M_TreadSurface(ITEM *const item, COLL_INFO *const coll)\n{\n    item->fall_speed -= 4;\n    CLAMPL(item->fall_speed, 0);\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_UW_DEATH);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n        return;\n    }\n\n    if (g_Input.left) {\n        item->rot.y -= LARA_SLOW_TURN;\n    } else if (g_Input.right) {\n        item->rot.y += LARA_SLOW_TURN;\n    }\n\n    if (g_Input.forward) {\n        item->goal_anim_state = LS(LS_SURF_SWIM);\n    } else if (g_Input.back) {\n        item->goal_anim_state = LS(LS_SURF_BACK);\n    }\n\n    if (g_Input.step_left) {\n        item->goal_anim_state = LS(LS_SURF_LEFT);\n    } else if (g_Input.step_right) {\n        item->goal_anim_state = LS(LS_SURF_RIGHT);\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Input.jump) {\n        lara->dive_timer++;\n        if (lara->dive_timer == LARA_DIVE_WAIT) {\n            Item_SwitchToAnim(item, LA(LA_ONWATER_DIVE), 0);\n            item->goal_anim_state = LS(LS_SWIM);\n            item->current_anim_state = LS(LS_DIVE);\n            item->rot.x = -45 * DEG_1;\n            item->fall_speed = 80;\n            lara->water_status = LWS_UNDERWATER;\n        }\n    } else {\n        lara->dive_timer = 0;\n    }\n}\n\nstatic void M_ForwardSurface(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_UW_DEATH);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->dive_timer = 0;\n    if (g_Input.left) {\n        item->rot.y -= LARA_SLOW_TURN;\n    } else if (g_Input.right) {\n        item->rot.y += LARA_SLOW_TURN;\n    }\n    if (!g_Input.forward || g_Input.jump) {\n        item->goal_anim_state = LS(LS_SURF_TREAD);\n    }\n    item->fall_speed += 8;\n    CLAMPG(item->fall_speed, M_MAX_SURF_SPEED);\n}\n\nstatic void M_SideBackSurface(ITEM *const item, COLL_INFO *const coll)\n{\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = LS(LS_UW_DEATH);\n        return;\n    }\n\n    coll->enable_hit = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->dive_timer = 0;\n\n    if (g_Input.left) {\n        item->rot.y -= M_TURN_RATE;\n    } else if (g_Input.right) {\n        item->rot.y += M_TURN_RATE;\n    }\n\n    bool stop = false;\n    switch (LS_U(item->current_anim_state)) {\n    case LS_SURF_BACK:\n        stop = !g_Input.back;\n        break;\n    case LS_SURF_LEFT:\n        stop = !g_Input.step_left;\n        break;\n    case LS_SURF_RIGHT:\n        stop = !g_Input.step_right;\n        break;\n    default:\n        break;\n    }\n\n    if (stop) {\n        item->goal_anim_state = LS(LS_SURF_TREAD);\n    }\n\n    item->fall_speed += 8;\n    CLAMPG(item->fall_speed, M_MAX_SURF_SPEED);\n}\n\nstatic void M_Dive(ITEM *const item, COLL_INFO *const coll)\n{\n    if (g_Input.forward) {\n        item->rot.x -= DEG_1;\n    }\n}\n\nstatic void M_UWDeath(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    item->gravity = false;\n    item->fall_speed -= 8;\n    CLAMPL(item->fall_speed, 0);\n\n    if (item->rot.x >= -M_TURN_RATE && item->rot.x <= M_TURN_RATE) {\n        item->rot.x = 0;\n    } else if (item->rot.x >= 0) {\n        item->rot.x -= M_TURN_RATE;\n    } else {\n        item->rot.x += M_TURN_RATE;\n    }\n}\n\nstatic void M_WaterOut(ITEM *const item, COLL_INFO *const coll)\n{\n    coll->enable_hit = 0;\n    coll->enable_baddie_push = 0;\n    g_Camera.flags = CF_FOLLOW_CENTRE;\n}\n\nstatic void M_UWTwist(ITEM *const item, COLL_INFO *const coll)\n{\n    item->fall_speed = 0;\n    item->goal_anim_state = LS(LS_TREAD);\n}\n\n// clang-format off\nREGISTER_LARA_STATE(LS_TREAD,      M_Tread)\nREGISTER_LARA_STATE(LS_SWIM,       M_Swim)\nREGISTER_LARA_STATE(LS_GLIDE,      M_Glide)\nREGISTER_LARA_STATE(LS_SURF_TREAD, M_TreadSurface)\nREGISTER_LARA_STATE(LS_SURF_SWIM,  M_ForwardSurface)\nREGISTER_LARA_STATE(LS_DIVE,       M_Dive)\nREGISTER_LARA_STATE(LS_UW_DEATH,   M_UWDeath)\nREGISTER_LARA_STATE(LS_SURF_BACK,  M_SideBackSurface)\nREGISTER_LARA_STATE(LS_SURF_LEFT,  M_SideBackSurface)\nREGISTER_LARA_STATE(LS_SURF_RIGHT, M_SideBackSurface)\nREGISTER_LARA_STATE(LS_WATER_OUT,  M_WaterOut)\nREGISTER_LARA_STATE(LS_WATER_ROLL, M_UWTwist)\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/lara/state.c",
    "content": "#include <trx/game/lara/state.h>\n\n#include <trx/debug.h>\n#include <trx/game/lara.h>\n\nstatic const LARA_TRX_ANIMATION m_TestResponsiveAnims[] = {\n    // clang-format off\n    LA_RUN,\n    LA_UNDERWATER_SWIM_FORWARD,\n    LA_SLIDE_FORWARD,\n    LA_STAND_TO_JUMP,\n    LA_REACH_TO_HANG,\n    LA_TRX_INVALID,\n    // clang-format on\n};\n\nstatic bool m_ResponsiveAnims[LA_NUMBER_OF] = {};\nstatic void (*m_StateRoutines[LS_NUMBER_OF])(ITEM *item, COLL_INFO *coll) = {};\nstatic void (*m_ExtraRoutines[LS_EXTRA_NUMBER_OF])(\n    ITEM *item, COLL_INFO *coll) = {};\n\nstatic bool M_HasResponsiveState(const LARA_TRX_ANIMATION anim_idx)\n{\n    const OBJECT *const obj = Object_Get(O_LARA);\n    if (!obj->loaded) {\n        return false;\n    }\n\n    const ANIM *const anim = Object_GetAnim(obj, LA(anim_idx));\n    for (int32_t i = 0; i < anim->num_changes; i++) {\n        const ANIM_CHANGE *const change = Anim_GetChange(anim->change_idx + i);\n        if (change->goal_anim_state == LS(LS_RESPONSIVE)) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nvoid Lara_State_Register(\n    const LARA_TRX_STATE state,\n    void (*const handle_func)(ITEM *item, COLL_INFO *coll))\n{\n    m_StateRoutines[state] = handle_func;\n}\n\nvoid Lara_State_RegisterExtra(\n    const LARA_EXTRA_STATE state,\n    void (*const handle_func)(ITEM *item, COLL_INFO *coll))\n{\n    ASSERT(state >= 0 && state < LS_EXTRA_NUMBER_OF);\n    m_ExtraRoutines[state] = handle_func;\n}\n\nvoid Lara_State_Initialise(void)\n{\n    for (int32_t i = 0; m_TestResponsiveAnims[i] != LA_TRX_INVALID; i++) {\n        const LARA_TRX_ANIMATION anim = m_TestResponsiveAnims[i];\n        m_ResponsiveAnims[anim] = M_HasResponsiveState(anim);\n    }\n}\n\nbool Lara_State_IsResponsive(const LARA_TRX_ANIMATION anim_idx)\n{\n    return m_ResponsiveAnims[anim_idx];\n}\n\nvoid Lara_State_Update(ITEM *const item, COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status != LWS_SURFACE && lara->extra_anim) {\n        if (m_ExtraRoutines[item->current_anim_state] != nullptr) {\n            m_ExtraRoutines[item->current_anim_state](item, coll);\n        }\n        return;\n    }\n\n    const LARA_TRX_STATE state = LS_U(item->current_anim_state);\n    if (state >= 0 && state < LS_NUMBER_OF) {\n        if (m_StateRoutines[state] != nullptr) {\n            m_StateRoutines[state](item, coll);\n        }\n        return;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lara/state.h",
    "content": "#include <trx/game/collision.h>\n#include <trx/game/lara/enum.h>\n\nvoid Lara_State_Register(\n    LARA_TRX_STATE state, void (*handle_func)(ITEM *item, COLL_INFO *coll));\nvoid Lara_State_RegisterExtra(\n    LARA_EXTRA_STATE state, void (*handle_func)(ITEM *item, COLL_INFO *coll));\nvoid Lara_State_Initialise(void);\nbool Lara_State_IsResponsive(LARA_TRX_ANIMATION anim_idx);\nvoid Lara_State_Update(ITEM *item, COLL_INFO *coll);\n"
  },
  {
    "path": "src/trx/game/lara/types.h",
    "content": "#pragma once\n\n#include <trx/game/anims.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects/types.h>\n#include <trx/game/items/types.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/types.h>\n\ntypedef struct {\n    ANIM_FRAME *frame_base;\n    int16_t frame_num;\n    int16_t anim_num;\n    int16_t lock;\n    XYZ_16 rot;\n    int16_t flash_gun;\n\n    struct {\n        struct {\n            XYZ_16 rot;\n        } result, prev;\n    } interp;\n} LARA_ARM;\n\ntypedef struct {\n    int32_t ammo;\n} AMMO_INFO;\n\ntypedef struct {\n    int16_t item_num;\n    LARA_GUN_STATE gun_status;\n    LARA_GUN_TYPE gun_type;\n    LARA_GUN_TYPE request_gun_type;\n    LARA_GUN_TYPE last_gun_type;\n\n    LARA_WATER_STATE water_status;\n    int32_t water_surface_dist;\n    int16_t turn_rate;\n    int16_t move_angle;\n    XYZ_16 head_rot;\n    XYZ_16 torso_rot;\n    int16_t calc_fall_speed;\n    int16_t pose_count;\n    int16_t hit_frame;\n    int16_t hit_direction;\n    int16_t air;\n    int16_t dive_timer;\n    int16_t death_timer;\n    int16_t sprint_timer;\n    int16_t exposure_timer;\n    int16_t poison_timer;\n    int32_t idle_timer;\n    struct {\n        int32_t active;\n        XZ_16 vel;\n    } current;\n    LOT_INFO lot;\n    XYZ_32 last_pos;\n\n    int16_t hit_effect_count;\n    EFFECT *hit_effect;\n    int32_t mesh_effects;\n    OBJECT_MESH *mesh_ptrs[LM_NUMBER_OF];\n\n    ITEM *target;\n    int16_t target_angles[2];\n\n    LARA_ARM left_arm;\n    LARA_ARM right_arm;\n    AMMO_INFO pistol_ammo;\n    AMMO_INFO magnum_ammo;\n    AMMO_INFO autos_ammo;\n    AMMO_INFO desert_eagle_ammo;\n    AMMO_INFO uzi_ammo;\n    AMMO_INFO shotgun_ammo;\n\n    struct {\n        struct {\n            XYZ_16 head_rot;\n            XYZ_16 torso_rot;\n        } result, prev;\n    } interp;\n\n    bool extra_anim;\n    bool burn;\n    int16_t electric;\n    bool climb_status;\n    bool is_crouched;\n    bool keep_crouched;\n    bool killed_loyal_item;\n\n    struct {\n        int32_t item_num;\n        int32_t move_count;\n        bool is_moving;\n        XYZ_32 initial_pos;\n    } interact_target;\n\n    LARA_GUN_TYPE holsters_gun_type;\n    LARA_GUN_TYPE back_gun_type;\n    int16_t gun_item_num;\n    AMMO_INFO harpoon_ammo;\n    AMMO_INFO grenade_ammo;\n    AMMO_INFO rocket_ammo;\n    AMMO_INFO m16_ammo;\n    AMMO_INFO mp5_ammo;\n    struct {\n        bool control;\n        int16_t age;\n        int16_t frame_num;\n    } flare;\n\n    MATRIX mesh_pos_matrices[LM_NUMBER_OF];\n    bool mesh_pos_matrices_valid;\n\n    // TR3: persistent gun smoke spawned from muzzle after firing.\n    int32_t tr3_smoke_count_l;\n    int32_t tr3_smoke_count_r;\n    LARA_GUN_TYPE tr3_smoke_weapon;\n    bool has_fired;\n\n    // TRR modern controls stuff\n    bool crouching;\n    bool sprinting;\n} LARA_INFO;\n"
  },
  {
    "path": "src/trx/game/lara/util.h",
    "content": "#pragma once\n\n#include <trx/game/lara/col.h>\n#include <trx/game/lara/state.h>\n\n#define REGISTER_LARA_COL(state, handle_func)                                  \\\n    __attribute__((constructor)) static void M_RegisterColHandler##state(void) \\\n    {                                                                          \\\n        Lara_Col_Register(state, handle_func);                                 \\\n    }\n\n#define REGISTER_LARA_STATE(state, handle_func)                                \\\n    __attribute__((constructor)) static void M_RegisterStateHandler##state(    \\\n        void)                                                                  \\\n    {                                                                          \\\n        Lara_State_Register(state, handle_func);                               \\\n    }\n\n#define REGISTER_LARA_EXTRA(state, handle_func)                                \\\n    __attribute__((constructor)) static void                                   \\\n    M_RegisterExtraStateHandler##state(void)                                   \\\n    {                                                                          \\\n        Lara_State_RegisterExtra(state, handle_func);                          \\\n    }\n"
  },
  {
    "path": "src/trx/game/lara/vehicle.c",
    "content": "#include <trx/game/lara/vehicle.h>\n\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/skin/common.h>\n\nstatic int16_t m_VehicleItemNum = NO_ITEM;\n\nbool Lara_Vehicle_IsMounted(void)\n{\n    return m_VehicleItemNum != NO_ITEM;\n}\n\nbool Lara_Vehicle_IsOnType(const OBJECT_ID obj_id)\n{\n    if (!Lara_Vehicle_IsMounted()) {\n        return false;\n    }\n\n    const ITEM *const vehicle = Lara_Vehicle_GetItem();\n    return vehicle->object_id == obj_id;\n}\n\nvoid Lara_Vehicle_SetIndex(const int16_t item_num)\n{\n    m_VehicleItemNum = item_num;\n}\n\nint16_t Lara_Vehicle_GetIndex(void)\n{\n    return m_VehicleItemNum;\n}\n\nITEM *Lara_Vehicle_GetItem(void)\n{\n    return m_VehicleItemNum == NO_ITEM ? nullptr : Item_Get(m_VehicleItemNum);\n}\n\nvoid Lara_Vehicle_Dismount(void)\n{\n    if (!Lara_Vehicle_IsMounted()) {\n        return;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    ITEM *const vehicle = Lara_Vehicle_GetItem();\n    Item_SwitchToAnim(vehicle, 0, 0);\n    Lara_Vehicle_SetIndex(NO_ITEM);\n\n    lara_item->current_anim_state = LS(LS_STOP);\n    lara_item->goal_anim_state = LS(LS_STOP);\n    Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n\n    lara_item->rot.x = 0;\n    lara_item->rot.z = 0;\n\n    const LARA_SKIN_EQUIPMENT *const hand_r_equipment =\n        Lara_Skin_GetEquipment(LM_HAND_R);\n    if (hand_r_equipment->type == EQUIPMENT_TYPE_EXTRA\n        && hand_r_equipment->data == EXTRA_MESH_OAR) {\n        Lara_Skin_ClearEquipment(LM_HAND_R);\n        Item_SetMeshVisibleMask(lara_item, INT32_MAX, true);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lara/vehicle.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nbool Lara_Vehicle_IsMounted(void);\nbool Lara_Vehicle_IsOnType(OBJECT_ID obj_id);\nvoid Lara_Vehicle_SetIndex(int16_t item_num);\nint16_t Lara_Vehicle_GetIndex(void);\nITEM *Lara_Vehicle_GetItem(void);\n\nvoid Lara_Vehicle_Dismount(void);\n"
  },
  {
    "path": "src/trx/game/lara.h",
    "content": "#pragma once\n\n#include <trx/game/lara/cheat.h>\n#include <trx/game/lara/cheat_keys.h>\n#include <trx/game/lara/col.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/lara/control.h>\n#include <trx/game/lara/draw.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/lara/flare.h>\n#include <trx/game/lara/hair.h>\n#include <trx/game/lara/look.h>\n#include <trx/game/lara/mesh.h>\n#include <trx/game/lara/misc.h>\n#include <trx/game/lara/skin.h>\n#include <trx/game/lara/state.h>\n#include <trx/game/lara/types.h>\n#include <trx/game/lara/vehicle.h>\n"
  },
  {
    "path": "src/trx/game/level/cache.c",
    "content": "#include <trx/game/level/cache.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/hash.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/shell.h>\n\n#include <inttypes.h>\n#include <stdlib.h>\n#include <string.h>\n#include <uthash.h>\n\n#define M_CACHE_MAGIC UINT32_C(0x4C434831)\n\ntypedef struct {\n    uint32_t magic;\n    uint64_t checksum;\n} M_CACHE_HEADER;\n\ntypedef struct M_LEVEL_HASH_ENTRY {\n    const GF_LEVEL *level;\n    uint64_t hash;\n    UT_hash_handle hh;\n} M_LEVEL_HASH_ENTRY;\n\nstatic M_LEVEL_HASH_ENTRY *m_LevelHashMap = nullptr;\n\nstatic void M_ClearLevelHashMap(void)\n{\n    M_LEVEL_HASH_ENTRY *current = nullptr;\n    M_LEVEL_HASH_ENTRY *tmp = nullptr;\n    HASH_ITER(hh, m_LevelHashMap, current, tmp)\n    {\n        HASH_DEL(m_LevelHashMap, current);\n        Memory_FreePointer(&current);\n    }\n}\n\nstatic __attribute__((constructor)) void M_Initialise(void)\n{\n    m_LevelHashMap = nullptr;\n}\n\nstatic __attribute__((destructor)) void M_Shutdown(void)\n{\n    M_ClearLevelHashMap();\n}\n\nstatic void M_GetFileMeta(\n    const char *const path, uint64_t *const out_size, uint64_t *const out_mtime)\n{\n    uint64_t size = 0;\n    uint64_t mtime = 0;\n    File_GetMeta(path, &size, &mtime);\n\n    if (out_size != nullptr) {\n        *out_size = size;\n    }\n    if (out_mtime != nullptr) {\n        *out_mtime = mtime;\n    }\n}\n\nstatic uint64_t M_ComputeLevelHash(const GF_LEVEL *const level)\n{\n    uint64_t checksum = Hash_FNV1a64_Init();\n    checksum = Hash_FNV1a64_UpdateU32(checksum, (uint32_t)level->num);\n    checksum = Hash_FNV1a64_UpdateU32(checksum, (uint32_t)level->type);\n    checksum = Hash_FNV1a64_UpdateString(checksum, level->path);\n    checksum =\n        Hash_FNV1a64_UpdateU32(checksum, (uint32_t)level->injections.count);\n\n    if (level->path != nullptr) {\n        uint64_t file_size = 0;\n        uint64_t file_mtime = 0;\n        M_GetFileMeta(level->path, &file_size, &file_mtime);\n        checksum = Hash_FNV1a64_UpdateU64(checksum, file_size);\n        checksum = Hash_FNV1a64_UpdateU64(checksum, file_mtime);\n    }\n\n    for (int32_t i = 0; i < level->injections.count; i++) {\n        const char *const path = level->injections.data_paths[i];\n        uint64_t file_size = 0;\n        uint64_t file_mtime = 0;\n        checksum = Hash_FNV1a64_UpdateString(checksum, path);\n        if (path != nullptr) {\n            M_GetFileMeta(path, &file_size, &file_mtime);\n            checksum = Hash_FNV1a64_UpdateU64(checksum, file_size);\n            checksum = Hash_FNV1a64_UpdateU64(checksum, file_mtime);\n        }\n    }\n\n    return checksum;\n}\n\nstatic uint64_t M_GetLevelHash(const GF_LEVEL *const level)\n{\n    M_LEVEL_HASH_ENTRY *entry = nullptr;\n    HASH_FIND_PTR(m_LevelHashMap, &level, entry);\n    if (entry != nullptr) {\n        return entry->hash;\n    }\n\n    entry = Memory_Alloc(sizeof(*entry));\n    entry->level = level;\n    entry->hash = M_ComputeLevelHash(level);\n    HASH_ADD_PTR(m_LevelHashMap, level, entry);\n    return entry->hash;\n}\n\nuint64_t LevelCache_InitChecksum(\n    const char *const scope, const uint32_t version)\n{\n    uint64_t checksum = Hash_FNV1a64_Init();\n    checksum = Hash_FNV1a64_UpdateString(checksum, scope);\n    checksum = Hash_FNV1a64_UpdateU32(checksum, version);\n    return checksum;\n}\n\nuint64_t LevelCache_UpdateLevelChecksum(\n    uint64_t checksum, const GF_LEVEL *const level)\n{\n    if (level == nullptr) {\n        return checksum;\n    }\n\n    checksum = Hash_FNV1a64_UpdateU64(checksum, M_GetLevelHash(level));\n    return checksum;\n}\n\nconst char *LevelCache_GetLevelKey(const GF_LEVEL *const level)\n{\n    if (level == nullptr) {\n        return nullptr;\n    }\n\n    char type_key = 'u';\n    switch (level->type) {\n    case GFL_TITLE:\n        type_key = 't';\n        break;\n    case GFL_NORMAL:\n        type_key = 'l';\n        break;\n    case GFL_CUTSCENE:\n        type_key = 'c';\n        break;\n    case GFL_DEMO:\n        type_key = 'd';\n        break;\n    case GFL_GYM:\n        type_key = 'g';\n        break;\n    case GFL_BONUS:\n        type_key = 'b';\n        break;\n    case GFL_DUMMY:\n        type_key = 'x';\n        break;\n    case GFL_CURRENT:\n        type_key = 'r';\n        break;\n    }\n\n    const char *name = level->path != nullptr ? level->path : \"unknown\";\n    const char *const slash = strrchr(name, '/');\n    const char *const backslash = strrchr(name, '\\\\');\n    if (slash != nullptr && backslash != nullptr) {\n        name = slash > backslash ? slash + 1 : backslash + 1;\n    } else if (slash != nullptr) {\n        name = slash + 1;\n    } else if (backslash != nullptr) {\n        name = backslash + 1;\n    }\n\n    const size_t stem_len = strcspn(name, \".\");\n    return String_FormatStatic(\n        \"%c%d_%.*s\", type_key, level->num, (int)stem_len, name);\n}\n\nstatic const char *M_GetPath(const char *const filename)\n{\n    const SHELL_ARGS *const args = Shell_GetArgs();\n    if (args == nullptr || args->mod == nullptr || args->mod->name == nullptr) {\n        return nullptr;\n    }\n\n    return String_FormatStatic(\n        \"%s/%s/%s\", Shell_GetCacheDir(), args->mod->name, filename);\n}\n\nMYFILE *LevelCache_OpenBinaryRead(\n    const char *const filename, const uint64_t checksum)\n{\n    const char *const path = M_GetPath(filename);\n    if (path == nullptr) {\n        return nullptr;\n    }\n\n    MYFILE *const file = File_Open(path, FILE_OPEN_READ);\n    if (file == nullptr) {\n        return nullptr;\n    }\n\n    M_CACHE_HEADER header;\n    if (!File_ReadData(file, &header, sizeof(header))\n        || header.magic != M_CACHE_MAGIC || header.checksum != checksum) {\n        File_Close(file);\n        return nullptr;\n    }\n\n    return file;\n}\n\nMYFILE *LevelCache_OpenBinaryWrite(\n    const char *const filename, const uint64_t checksum)\n{\n    const char *const path = M_GetPath(filename);\n    if (path == nullptr) {\n        return nullptr;\n    }\n\n    File_EnsureParentDirectories(path);\n\n    MYFILE *const file = File_Open(path, FILE_OPEN_WRITE);\n    if (file == nullptr) {\n        return nullptr;\n    }\n\n    const M_CACHE_HEADER header = {\n        .magic = M_CACHE_MAGIC,\n        .checksum = checksum,\n    };\n    File_WriteData(file, &header, sizeof(header));\n\n    return file;\n}\n\nJSON_VALUE *LevelCache_ReadJSON(\n    const char *const filename, const uint64_t checksum)\n{\n    const char *const path = M_GetPath(filename);\n    if (path == nullptr) {\n        return nullptr;\n    }\n\n    JSON_VALUE *const root = JSONFile_Read(path);\n    if (root == nullptr) {\n        return nullptr;\n    }\n\n    JSON_OBJECT *const root_obj = JSON_ValueAsObject(root);\n    const char *const checksum_str = root_obj != nullptr\n        ? JSON_ObjectGetString(root_obj, \"checksum\", nullptr)\n        : nullptr;\n    if (checksum_str == nullptr\n        || (uint64_t)strtoull(checksum_str, nullptr, 16) != checksum) {\n        JSON_ValueFree(root);\n        return nullptr;\n    }\n\n    return root;\n}\n\nbool LevelCache_WriteJSON(\n    const char *const filename, const uint64_t checksum, JSON_VALUE *const root)\n{\n    const char *const path = M_GetPath(filename);\n    JSON_OBJECT *const root_obj =\n        root != nullptr ? JSON_ValueAsObject(root) : nullptr;\n    if (path == nullptr || root_obj == nullptr) {\n        return false;\n    }\n\n    File_EnsureParentDirectories(path);\n\n    JSON_ObjectAppendString(\n        root_obj, \"checksum\", String_FormatStatic(\"%016\" PRIx64, checksum));\n\n    return JSONFile_Write(path, root);\n}\n"
  },
  {
    "path": "src/trx/game/level/cache.h",
    "content": "#pragma once\n\n#include <trx/core/filesystem.h>\n#include <trx/core/json.h>\n#include <trx/game/game_flow/types.h>\n\n#include <stddef.h>\n#include <stdint.h>\n\nuint64_t LevelCache_InitChecksum(const char *scope, uint32_t version);\nuint64_t LevelCache_UpdateLevelChecksum(\n    uint64_t checksum, const GF_LEVEL *level);\n\nconst char *LevelCache_GetLevelKey(const GF_LEVEL *level);\n\nMYFILE *LevelCache_OpenBinaryRead(const char *filename, uint64_t checksum);\nMYFILE *LevelCache_OpenBinaryWrite(const char *filename, uint64_t checksum);\n\nJSON_VALUE *LevelCache_ReadJSON(const char *filename, uint64_t checksum);\nbool LevelCache_WriteJSON(\n    const char *filename, uint64_t checksum, JSON_VALUE *root);\n"
  },
  {
    "path": "src/trx/game/level/common.c",
    "content": "#include <trx/config.h>\n#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/table.h>\n#include <trx/game/gym.h>\n#include <trx/game/lara.h>\n#include <trx/game/level.h>\n#include <trx/game/lua.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/option.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/savegame.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/ui.h>\n\nvoid Level_Unload(void)\n{\n    Music_ResetTrackFlags();\n    Sound_ResetSamples();\n\n    Lara_InitialiseLoad(NO_ITEM);\n\n    Gym_TrackManager_Reset(GYM_TRACK_ASSAULT);\n    Gym_TrackManager_Reset(GYM_TRACK_QUAD);\n    Creature_Reset();\n    Object_Reset();\n    Camera_Reset();\n    Walkable_Reset();\n\n    Output_SetTimeInGame(0.0f);\n    Output_DispatchLevelUnload();\n\n    Sound_StopAll();\n    Viewport_AlterFOV(-1, FOV_MODE_GAME);\n}\n\nbool Level_Initialise(\n    const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    LOG_DEBUG(\"num=%d (%s)\", level->num, level->path);\n    if (level->type == GFL_DEMO) {\n        Random_SeedDraw(0xD371F947);\n        Random_SeedControl(0xD371F947);\n    }\n\n    Game_SetIsLevelComplete(false);\n    if (level->type != GFL_TITLE && level->type != GFL_DEMO) {\n        Gym_SetInventoryOpenEnabled(false);\n    }\n    if (level->type != GFL_TITLE && level->type != GFL_CUTSCENE) {\n        Game_SetCurrentLevel(level);\n    }\n    GF_SetCurrentLevel(level);\n\n    if (level->type != GFL_TITLE) {\n        // TODO: move me elsewhere\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        if (resume != nullptr) {\n            resume->stats.timer = 0;\n            resume->stats.secret_flags = 0;\n            resume->stats.secret_count = 0;\n            resume->stats.pickup_count = 0;\n            resume->stats.kill_count = 0;\n            resume->stats.ammo_hits = 0;\n            resume->stats.ammo_used = 0;\n            resume->stats.medipacks_used = 0;\n            resume->stats.distance_travelled = 0;\n        }\n    }\n\n    if (level == nullptr) {\n        return false;\n    }\n\n    Level_Unload();\n\n    Lua_FireEventInt32(LUA_EVENT_BEFORE_LEVEL_FILE, level->num);\n\n    Level_Pipeline_Load(level);\n\n    UI_LoadText();\n    Output_SetSkyboxEnabled(Object_Get(O_SKYBOX)->loaded);\n    Output_DispatchLevelLoad();\n\n    GameStringTable_Apply(level);\n\n    Effect_InitialiseArray();\n    LOT_InitialiseArray();\n    FX_Reset();\n    FX_Weather_SetWeather(level->weather_type);\n    Sparks_Reset();\n\n    Option_Reset();\n    Overlay_Reset();\n    Overlay_SetHealthBarTimer(100);\n\n    Benchmark_End(&benchmark, nullptr);\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/level/common.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\nvoid Level_Unload(void);\nbool Level_Initialise(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx);\n"
  },
  {
    "path": "src/trx/game/level/context.c",
    "content": "#include <trx/game/level/context.h>\n\n#include <string.h>\n\nstatic LEVEL_CONTEXT m_LoadContext = {};\n\nvoid Level_Context_Reset(const LEVEL_FORMAT_LOADER *const loader)\n{\n    memset(&m_LoadContext, 0, sizeof(m_LoadContext));\n    m_LoadContext.loader = loader;\n}\n\nLEVEL_CONTEXT *Level_Context_Get(void)\n{\n    return &m_LoadContext;\n}\n\nLEVEL_CONTEXT_INFO *Level_Context_GetInfo(void)\n{\n    return &m_LoadContext.info;\n}\n"
  },
  {
    "path": "src/trx/game/level/context.h",
    "content": "#pragma once\n\n#include <trx/game/output/types.h>\n\ntypedef struct LEVEL_FORMAT_LOADER LEVEL_FORMAT_LOADER;\n\ntypedef struct {\n    struct {\n        int32_t anim_count;\n        int32_t change_count;\n        int32_t range_count;\n        int32_t command_count;\n        int16_t *commands;\n        int32_t bone_count;\n        int32_t frame_count;\n        int16_t *frames;\n    } anims;\n\n    struct {\n        int32_t object_count;\n        int32_t sprite_count;\n        int32_t page_count;\n        uint8_t *pages_8;\n        RGBA_8888 *pages_32;\n    } textures;\n\n    struct {\n        int32_t size;\n        RGB_888 *data_24;\n        RGB_888 *data_32;\n    } palette;\n\n    struct {\n        int32_t offset_count;\n        int32_t *offsets;\n\n        // TR1-specific\n        int32_t data_size;\n        char *data;\n    } samples;\n\n    int32_t mesh_ptr_count;\n} LEVEL_CONTEXT_INFO;\n\ntypedef struct {\n    LEVEL_CONTEXT_INFO info;\n    const LEVEL_FORMAT_LOADER *loader;\n} LEVEL_CONTEXT;\n\nvoid Level_Context_Reset(const LEVEL_FORMAT_LOADER *loader);\nLEVEL_CONTEXT *Level_Context_Get(void);\nLEVEL_CONTEXT_INFO *Level_Context_GetInfo(void);\n"
  },
  {
    "path": "src/trx/game/level/finalize/animations.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/game/anims.h>\n#include <trx/game/level/finalize.h>\n\nvoid Level_Finalize_LoadAnimCommands(LEVEL_CONTEXT *const ctx)\n{\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    Anim_LoadCommands(info->anims.commands);\n    Memory_FreePointer(&info->anims.commands);\n}\n\nvoid Level_Finalize_LoadAnimFrames(LEVEL_CONTEXT *const ctx)\n{\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    const int32_t frame_count =\n        Anim_GetTotalFrameCount(loader, info->anims.frame_count);\n    Anim_InitialiseFrames(frame_count);\n    Anim_LoadFrames(loader, info->anims.frames, info->anims.frame_count);\n    Memory_FreePointer(&info->anims.frames);\n}\n"
  },
  {
    "path": "src/trx/game/level/finalize/gameplay_objects.c",
    "content": "#include <trx/game/game_flow/util.h>\n#include <trx/game/items.h>\n#include <trx/game/items/walkable.h>\n#include <trx/game/lara.h>\n#include <trx/game/level/finalize.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\nstatic uint8_t M_GetAIBit(const OBJECT_ID object_id)\n{\n    switch (object_id) {\n        // clang-format off\n    case O_AI_GUARD:    return AI_GUARD;\n    case O_AI_AMBUSH:   return AI_AMBUSH;\n    case O_AI_PATROL_1: return AI_PATROL_1;\n    case O_AI_MODIFY:   return AI_MODIFY;\n    case O_AI_FOLLOW:   return AI_FOLLOW;\n    // clang-format on\n    default:\n        return 0;\n    }\n}\n\nstatic void M_AssignAIBits(void)\n{\n    const int32_t item_count = Item_GetLevelCount();\n    for (int32_t i = 0; i < item_count; i++) {\n        ITEM *const item = Item_Get(i);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (!obj->intelligent || item->room_num == NO_ROOM) {\n            continue;\n        }\n\n        ROOM *const room = Room_Get(item->room_num);\n        int16_t ai_item_num = room->item_num;\n        while (ai_item_num != NO_ITEM) {\n            ITEM *const ai_item = Item_Get(ai_item_num);\n            const int16_t next_num = ai_item->next_item;\n            const uint8_t ai_bit = M_GetAIBit(ai_item->object_id);\n            if (ai_bit != 0 && ai_item->pos.x == item->pos.x\n                && ai_item->pos.z == item->pos.z) {\n                item->ai_bits |= ai_bit;\n                item->ai_tag = ai_item->rot.y;\n                if (!(ai_item->object_id == O_AI_PATROL_1\n                      && (GF_BadGetLevelNum() == 15\n                          || GF_BadGetLevelNum() == 14))) {\n                    Item_Kill(ai_item_num);\n                    ai_item->room_num = NO_ROOM;\n                }\n            }\n            ai_item_num = next_num;\n        }\n    }\n}\n\nvoid Level_Finalize_LoadObjectsAndItems(LEVEL_CONTEXT *const ctx)\n{\n    // Object and item setup/initialisation must take place after injections\n    // have been processed. A cached item count must be used as individual\n    // initialisations may increment the total item count.\n    Object_SetupAllObjects();\n    Walkable_ResetLevel();\n\n    const int32_t item_count = Item_GetLevelCount();\n    for (int32_t i = 0; i < item_count; i++) {\n        Item_Initialise(i);\n    }\n\n    // Must take place after item initialization.\n    Level_Finalize_LoadWalkables(ctx);\n\n    M_AssignAIBits();\n    Lara_State_Initialise();\n}\n\nvoid Level_Finalize_LoadWalkables(LEVEL_CONTEXT *const ctx)\n{\n    for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->add_walkable_func != nullptr) {\n            obj->add_walkable_func(item_num);\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/level/finalize/render_assets.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/memory.h>\n#include <trx/core/thread_pool.h>\n#include <trx/core/utils.h>\n#include <trx/game/level/finalize.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n\n#include <string.h>\n\nstatic void M_FixTrapezoidRatios(FACE *const face, const XYZ_16 vertices[4])\n{\n    // This function attempts to correct texture coordinate ratios for a\n    // quadrilateral so the GPU, which typically renders a quad as two\n    // triangles, does not warp the texture disproportionately across the quad's\n    // diagonal divisions. By comparing the 3D edge lengths (in world space)\n    // with the corresponding 2D UV edge lengths, it computes a scale factor\n    // that preserves the trapezoidal shape in texture space and allows the\n    // shader to warp the texture uniformly across all four corners.\n    //\n    // In many software rasterization or older GPU pipelines, triangles can get\n    // rendered using affine interpolation of texture coordinates, causing\n    // visible warping when a four-sided polygon is split internally.\n    // The original approach (coded by XProger) handled only rectangular UV\n    // maps by simply scaling the edges; this updated version takes into\n    // account the actual UV trapezoid to achieve a more uniform texture\n    // projection.\n\n    // 1) Gather the coordinate and texture information\n    const OBJECT_TEXTURE *const tex =\n        Output_GetObjectTexture(face->texture_idx);\n    const TEXTURE_UV *uvs[4] = {\n        &tex->uv[0],\n        &tex->uv[1],\n        &tex->uv[2],\n        &tex->uv[3],\n    };\n    TEXTURE_ZW_F *zws[4] = {\n        &face->texture_zw[0],\n        &face->texture_zw[1],\n        &face->texture_zw[2],\n        &face->texture_zw[3],\n    };\n    XYZ_F c0, c1, c2, c3, *coords[4] = { &c0, &c1, &c2, &c3 };\n    for (int32_t i = 0; i < 4; i++) {\n        coords[i]->x = vertices[i].x;\n        coords[i]->y = vertices[i].y;\n        coords[i]->z = vertices[i].z;\n    }\n\n    // 2) Compute geometric edges\n    //    a = c0-c1, b = c3-c2, c = c0-c3, d = c1-c2\n    const XYZ_F a = XYZ_F_Subtract(c0, c1);\n    const XYZ_F b = XYZ_F_Subtract(c3, c2);\n    const XYZ_F c = XYZ_F_Subtract(c0, c3);\n    const XYZ_F d = XYZ_F_Subtract(c1, c2);\n\n    const float a_l = XYZ_F_Length(a);\n    const float b_l = XYZ_F_Length(b);\n    const float c_l = XYZ_F_Length(c);\n    const float d_l = XYZ_F_Length(d);\n\n    // 3) Compute dot-products in 3D to see which edges differ more\n    const float ab = XYZ_F_DotProduct(a, b) / (a_l * b_l);\n    const float cd = XYZ_F_DotProduct(c, d) / (c_l * d_l);\n\n    // 4) Compute tx, ty in for orientation\n    const float tx = ABS(uvs[0]->u - uvs[3]->u);\n    const float ty = ABS(uvs[0]->v - uvs[3]->v);\n\n    // 5) Measure the same edges in UV space so we know the \"current\" shape\n    XYZ_F uv0 = { uvs[0]->u, uvs[0]->v, 0.0f };\n    XYZ_F uv1 = { uvs[1]->u, uvs[1]->v, 0.0f };\n    XYZ_F uv2 = { uvs[2]->u, uvs[2]->v, 0.0f };\n    XYZ_F uv3 = { uvs[3]->u, uvs[3]->v, 0.0f };\n\n    // au = uv0 - uv1, bu = uv3 - uv2, cu = uv0 - uv3, du = uv1 - uv2\n    const XYZ_F au = XYZ_F_Subtract(uv0, uv1);\n    const XYZ_F bu = XYZ_F_Subtract(uv3, uv2);\n    const XYZ_F cu = XYZ_F_Subtract(uv0, uv3);\n    const XYZ_F du = XYZ_F_Subtract(uv1, uv2);\n\n    const float au_l = XYZ_F_Length(au);\n    const float bu_l = XYZ_F_Length(bu);\n    const float cu_l = XYZ_F_Length(cu);\n    const float du_l = XYZ_F_Length(du);\n\n    // We'll reuse the same ab/cd dot logic in UV if needed, but typically\n    // we only need the lengths to find the ratio vs. geometry.\n\n    // 6) Figure out the correction ratios per-corner, taking care of both\n    //    geometry and UV mesh proportions\n    if (ab > cd) {\n        const int k = (tx > ty) ? 1 : 0; // pick axis\n        if (a_l > b_l) {\n            // geometry ratio = (b_l / a_l)\n            // uv ratio       = (bu_l / au_l)  (avoid /0 check if needed)\n            const float geom_ratio = (a_l > 1e-6f) ? (b_l / a_l) : 1.0f;\n            const float uv_ratio = (au_l > 1e-6f) ? (bu_l / au_l) : 1.0f;\n            const float fix = geom_ratio / uv_ratio; // final scale\n            zws[2]->zw[k] = fix;\n            zws[3]->zw[k] = fix;\n        } else if (a_l < b_l) {\n            const float geom_ratio = (b_l > 1e-6f) ? (a_l / b_l) : 1.0f;\n            const float uv_ratio = (bu_l > 1e-6f) ? (au_l / bu_l) : 1.0f;\n            const float fix = geom_ratio / uv_ratio;\n            zws[0]->zw[k] = fix;\n            zws[1]->zw[k] = fix;\n        }\n    } else if (ab < cd) {\n        const int k = (tx > ty) ? 0 : 1; // pick axis\n        if (c_l > d_l) {\n            const float geom_ratio = (c_l > 1e-6f) ? (d_l / c_l) : 1.0f;\n            const float uv_ratio = (cu_l > 1e-6f) ? (du_l / cu_l) : 1.0f;\n            const float fix = geom_ratio / uv_ratio;\n            zws[1]->zw[k] = fix;\n            zws[2]->zw[k] = fix;\n        } else if (c_l < d_l) {\n            const float geom_ratio = (d_l > 1e-6f) ? (c_l / d_l) : 1.0f;\n            const float uv_ratio = (du_l > 1e-6f) ? (cu_l / du_l) : 1.0f;\n            const float fix = geom_ratio / uv_ratio;\n            zws[0]->zw[k] = fix;\n            zws[3]->zw[k] = fix;\n        }\n    }\n}\n\nstatic void M_PremultiplyTexturePage(void *userdata)\n{\n    const int32_t page = *(int32_t *)userdata;\n    Output_LockTexturePage32(page);\n    RGBA_8888 *ptr = Output_GetTexturePage32(page);\n    const float inv255 = 1.0f / 255.0f;\n    for (int32_t i = 0; i < TEXTURE_PAGE_SIZE; i++, ptr++) {\n        ptr->r *= ptr->a * inv255;\n        ptr->g *= ptr->a * inv255;\n        ptr->b *= ptr->a * inv255;\n    }\n    Output_UnlockTexturePage32(page);\n}\n\nstatic void M_UpdateReflectivity(OBJECT_MESH *const mesh, FACE *const face)\n{\n    const OBJECT_TEXTURE *const texture =\n        Output_GetObjectTexture(face->texture_idx);\n    const bool reflective = texture->draw_type == DRAW_REFLECTIVE_OPAQUE\n        || texture->draw_type == DRAW_REFLECTIVE_BLEND_ADD;\n    face->enable_reflections |= reflective;\n    mesh->enable_reflections |= reflective;\n}\n\nvoid Level_Finalize_LoadTextures(LEVEL_CONTEXT *const ctx)\n{\n    for (int32_t room_num = 0; room_num < Room_GetCount(); room_num++) {\n        const ROOM *const room = Room_Get(room_num);\n        for (int32_t j = 0; j < room->mesh.face3s.count; j++) {\n            const FACE *const face = &room->mesh.face3s.data[j];\n            OBJECT_TEXTURE *const texture =\n                Output_GetObjectTexture(face->texture_idx);\n            texture->uv_count = 3;\n        }\n    }\n\n    for (int32_t i = 0; i < Object_GetMeshCount(); i++) {\n        OBJECT_MESH *const mesh = Object_GetMesh(i);\n        for (int32_t j = 0; j < mesh->tex_face3s.count; j++) {\n            FACE *const face = &mesh->tex_face3s.data[j];\n            OBJECT_TEXTURE *const texture =\n                Output_GetObjectTexture(face->texture_idx);\n            texture->uv_count = 3;\n            M_UpdateReflectivity(mesh, face);\n        }\n    }\n\n    for (int32_t room_num = 0; room_num < Room_GetCount(); room_num++) {\n        ROOM *const room = Room_Get(room_num);\n        for (int32_t j = 0; j < room->mesh.face4s.count; j++) {\n            FACE *const face = &room->mesh.face4s.data[j];\n            XYZ_16 vertices[4] = {\n                room->mesh.vertices[face->vertices[0]].pos,\n                room->mesh.vertices[face->vertices[1]].pos,\n                room->mesh.vertices[face->vertices[2]].pos,\n                room->mesh.vertices[face->vertices[3]].pos,\n            };\n            M_FixTrapezoidRatios(face, vertices);\n        }\n    }\n\n    for (int32_t i = 0; i < Object_GetMeshCount(); i++) {\n        OBJECT_MESH *const mesh = Object_GetMesh(i);\n        for (int32_t j = 0; j < mesh->tex_face4s.count; j++) {\n            FACE *const face = &mesh->tex_face4s.data[j];\n            XYZ_16 vertices[4] = {\n                mesh->vertices[face->vertices[0]],\n                mesh->vertices[face->vertices[1]],\n                mesh->vertices[face->vertices[2]],\n                mesh->vertices[face->vertices[3]],\n            };\n            M_FixTrapezoidRatios(face, vertices);\n            M_UpdateReflectivity(mesh, face);\n        }\n    }\n}\n\nvoid Level_Finalize_LoadTexturePages(LEVEL_CONTEXT *const ctx)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    const int32_t num_pages = info->textures.page_count;\n    Output_InitialiseTexturePages(num_pages, loader->game_version >= 2);\n\n    for (int32_t i = 0; i < num_pages; i++) {\n        if (loader->game_version >= 2) {\n            uint8_t *const target_8 = Output_GetTexturePage8(i);\n            const uint8_t *const source_8 =\n                &info->textures.pages_8[i * TEXTURE_PAGE_SIZE];\n            memcpy(target_8, source_8, TEXTURE_PAGE_SIZE * sizeof(uint8_t));\n        }\n\n        const RGBA_8888 *const source_32 =\n            &info->textures.pages_32[i * TEXTURE_PAGE_SIZE];\n        RGBA_8888 *const target_32 = Output_GetTexturePage32(i);\n        memcpy(target_32, source_32, TEXTURE_PAGE_SIZE * sizeof(RGBA_8888));\n    }\n    Benchmark_End(&benchmark, \"copied texture data\");\n\n    {\n        int32_t *pages = Memory_Alloc(num_pages * sizeof(int32_t));\n        THREAD_POOL *const pool = ThreadPool_Create(-1);\n        for (int32_t i = 0; i < num_pages; i++) {\n            pages[i] = i;\n        }\n        for (int32_t i = 0; i < num_pages; i++) {\n            ThreadPool_AddJob(pool, M_PremultiplyTexturePage, &pages[i]);\n        }\n        ThreadPool_Wait(pool);\n        ThreadPool_Destroy(pool);\n        Memory_Free(pages);\n    }\n    Benchmark_End(&benchmark, \"premultiplied alpha\");\n\n    Memory_FreePointer(&info->textures.pages_8);\n    Memory_FreePointer(&info->textures.pages_32);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Finalize_LoadPalettes(LEVEL_CONTEXT *const ctx)\n{\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    Output_InitialisePalettes(\n        info->palette.size, info->palette.data_24, info->palette.data_32);\n    Memory_FreePointer(&info->palette.data_24);\n    Memory_FreePointer(&info->palette.data_32);\n}\n"
  },
  {
    "path": "src/trx/game/level/finalize/rooms.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/level/finalize.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n#include <string.h>\n\nstatic inline bool M_BoundsIntersectsPortal(\n    const STATIC_MESH *const mesh, const ROOM *const room,\n    const PORTAL *const portal)\n{\n    const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(mesh->static_num);\n    const BOUNDS_32 bounds = {\n        .min = {\n            .x = mesh->pos.x + obj->draw_bounds.min.x,\n            .y = mesh->pos.y + obj->draw_bounds.min.y,\n            .z = mesh->pos.z + obj->draw_bounds.min.z,\n        },\n        .max = {\n            .x = mesh->pos.x + obj->draw_bounds.max.x,\n            .y = mesh->pos.y + obj->draw_bounds.max.y,\n            .z = mesh->pos.z + obj->draw_bounds.max.z,\n        },\n    };\n    return Bounds32_Intersect(&bounds, &portal->bounds);\n}\n\nstatic void M_ComputePortalBounds(void)\n{\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        ROOM *const room = Room_Get(i);\n        PORTALS *const portals = room->portals;\n        if (portals == nullptr) {\n            continue;\n        }\n        for (uint16_t p = 0; p < portals->count; p++) {\n            PORTAL *const portal = &portals->portal[p];\n            BOUNDS_32 *const bounds = &portal->bounds;\n            bounds->min.x = room->pos.x + portal->vertex[0].x;\n            bounds->min.y = room->pos.y + portal->vertex[0].y;\n            bounds->min.z = room->pos.z + portal->vertex[0].z;\n            bounds->max.x = room->pos.x + portal->vertex[0].x;\n            bounds->max.y = room->pos.y + portal->vertex[0].y;\n            bounds->max.z = room->pos.z + portal->vertex[0].z;\n            for (int32_t k = 1; k < 4; k++) {\n                bounds->min.x =\n                    MIN(bounds->min.x, room->pos.x + portal->vertex[k].x);\n                bounds->min.y =\n                    MIN(bounds->min.y, room->pos.y + portal->vertex[k].y);\n                bounds->min.z =\n                    MIN(bounds->min.z, room->pos.z + portal->vertex[k].z);\n                bounds->max.x =\n                    MAX(bounds->max.x, room->pos.x + portal->vertex[k].x);\n                bounds->max.y =\n                    MAX(bounds->max.y, room->pos.y + portal->vertex[k].y);\n                bounds->max.z =\n                    MAX(bounds->max.z, room->pos.z + portal->vertex[k].z);\n            }\n        }\n    }\n}\n\nstatic void M_FixStaticsVisibility(void)\n{\n    int32_t total_rooms = Room_GetCount();\n    int32_t draw_num = 0;\n    VECTOR **room_stat_vecs =\n        Memory_Alloc(sizeof(*room_stat_vecs) * total_rooms);\n\n    for (int32_t i = 0; i < total_rooms; i++) {\n        room_stat_vecs[i] = Vector_Create(sizeof(STATIC_MESH));\n        ROOM *const room = Room_Get(i);\n        for (int32_t m = 0; m < room->num_static_meshes; m++) {\n            STATIC_MESH *const static_mesh = &room->static_meshes[m];\n            if (Object_IsValidStatid3D(static_mesh->static_num)) {\n                ASSERT(draw_num < MAX_ITEMS);\n                static_mesh->draw_num = draw_num++;\n                Vector_Add(room_stat_vecs[i], static_mesh);\n            } else {\n                LOG_WARNING(\n                    \"Invalid static 3D (id %d) in room %d\",\n                    static_mesh->static_num, i);\n            }\n        }\n    }\n\n    for (int32_t i = 0; i < total_rooms; i++) {\n        ROOM *const room = Room_Get(i);\n        PORTALS *const portals = room->portals;\n        if (portals == nullptr) {\n            continue;\n        }\n        for (uint16_t p = 0; p < portals->count; p++) {\n            const PORTAL *const portal = &portals->portal[p];\n            ROOM *const dest_room = Room_Get(portal->room_num);\n            if (room->flip_status != dest_room->flip_status) {\n                continue;\n            }\n            int32_t orig_count = room_stat_vecs[i]->count;\n            for (int32_t m = 0; m < orig_count; m++) {\n                const STATIC_MESH *const mesh =\n                    Vector_Get(room_stat_vecs[i], m);\n                if (!M_BoundsIntersectsPortal(mesh, room, portal)) {\n                    continue;\n                }\n                if (Vector_Contains(room_stat_vecs[portal->room_num], mesh)) {\n                    continue;\n                }\n                Vector_Add(room_stat_vecs[portal->room_num], mesh);\n                LOG_WARNING(\n                    \"Static #%d bleeds into room #%d\", mesh->static_num,\n                    portal->room_num);\n            }\n        }\n    }\n\n    int32_t total_needed = 0;\n    for (int32_t i = 0; i < total_rooms; i++) {\n        total_needed += room_stat_vecs[i]->count;\n    }\n    if (total_needed == 0) {\n        for (int32_t i = 0; i < total_rooms; i++) {\n            Vector_Free(room_stat_vecs[i]);\n        }\n        Memory_FreePointer(&room_stat_vecs);\n    } else {\n        STATIC_MESH *all_statics = GameBuf_Alloc(\n            sizeof(STATIC_MESH) * total_needed, GBUF_ROOM_STATIC_MESHES);\n        int32_t offset = 0;\n        for (int32_t i = 0; i < total_rooms; i++) {\n            ROOM *const room = Room_Get(i);\n            room->static_meshes = &all_statics[offset];\n            room->num_static_meshes = 0;\n            VECTOR *vec = room_stat_vecs[i];\n            for (int32_t m = 0; m < vec->count; m++) {\n                room->static_meshes[room->num_static_meshes++] =\n                    *(STATIC_MESH *)Vector_Get(vec, m);\n            }\n            offset += vec->count;\n            Vector_Free(vec);\n        }\n        Memory_FreePointer(&room_stat_vecs);\n    }\n}\n\nstatic void M_FixStaticsCollision(void)\n{\n    const int32_t count = Object_GetStaticObjects3DCount();\n    for (int32_t i = 0; i < count; i++) {\n        STATIC_OBJECT_3D *const obj = Object_Get3DStatic(i);\n        if (!obj->loaded || !obj->collidable) {\n            continue;\n        }\n\n        const XYZ_32 hitbox = {\n            .x = obj->collision_bounds.max.x - obj->collision_bounds.min.x,\n            .y = obj->collision_bounds.max.y - obj->collision_bounds.min.y,\n            .z = obj->collision_bounds.max.z - obj->collision_bounds.min.z,\n        };\n\n        if (hitbox.x <= 0 && hitbox.y <= 0 && hitbox.z <= 0) {\n            LOG_WARNING(\n                \"Static %d is marked as collidable, but has degenerate \"\n                \"hitbox (%d x %d x %d)\",\n                i, hitbox.x, hitbox.y, hitbox.z);\n            obj->collidable = false;\n        }\n    }\n}\n\nvoid Level_Finalize_LoadRooms(LEVEL_CONTEXT *const ctx)\n{\n    M_ComputePortalBounds();\n    M_FixStaticsCollision();\n    M_FixStaticsVisibility();\n}\n"
  },
  {
    "path": "src/trx/game/level/finalize.h",
    "content": "#pragma once\n\n#include <trx/game/level/context.h>\n\nvoid Level_Finalize_LoadRooms(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadTextures(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadTexturePages(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadPalettes(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadAnimCommands(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadAnimFrames(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadObjectsAndItems(LEVEL_CONTEXT *ctx);\nvoid Level_Finalize_LoadWalkables(LEVEL_CONTEXT *ctx);\n"
  },
  {
    "path": "src/trx/game/level/format/format.h",
    "content": "#pragma once\n\n#include <trx/core/virtual_file.h>\n#include <trx/game/game_flow/types.h>\n\ntypedef enum {\n    LEVEL_FORMAT_PROBE_MINIMAL,\n    LEVEL_FORMAT_PROBE_STATS,\n} LEVEL_FORMAT_PROBE_MODE;\n\ntypedef enum {\n    LEVEL_FORMAT_LAYOUT_UNKNOWN = -1,\n    LEVEL_FORMAT_LAYOUT_TR1X,\n    LEVEL_FORMAT_LAYOUT_TR1,\n    LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC,\n    LEVEL_FORMAT_LAYOUT_TR2X,\n    LEVEL_FORMAT_LAYOUT_TR2,\n    LEVEL_FORMAT_LAYOUT_TR3,\n    LEVEL_FORMAT_LAYOUT_TR3X,\n    LEVEL_FORMAT_LAYOUT_NUMBER_OF,\n} LEVEL_FORMAT_LAYOUT;\n\ntypedef struct LEVEL_FORMAT_LOADER {\n    int32_t game_version;\n    LEVEL_FORMAT_LAYOUT layout;\n    bool (*probe)(\n        const struct LEVEL_FORMAT_LOADER *, VFILE *file,\n        LEVEL_FORMAT_PROBE_MODE mode);\n    bool (*load)(const struct LEVEL_FORMAT_LOADER *, VFILE *file);\n} LEVEL_FORMAT_LOADER;\n\nLEVEL_FORMAT_LAYOUT Level_Format_GuessLayout(VFILE *file);\nconst LEVEL_FORMAT_LOADER *Level_Format_GuessLoader(VFILE *file);\nconst LEVEL_FORMAT_LOADER *Level_Format_LoadFromFile(const GF_LEVEL *level);\n"
  },
  {
    "path": "src/trx/game/level/format/format_tr1.c",
    "content": "#include <trx/core/log.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/format/priv.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/output/const.h>\n\n#define M_SAMPLE_COUNT 256\n\nstatic bool M_Probe(\n    const LEVEL_FORMAT_LOADER *const loader, VFILE *const file,\n    const LEVEL_FORMAT_PROBE_MODE mode)\n{\n    VFile_SetPos(file, 0);\n    LEVEL_CONTEXT probe_ctx = {\n        .loader = loader,\n    };\n\n    int32_t version;\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &version));\n    if (version != 32) {\n        return false;\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(TEXTURE_PAGE_SIZE); // textures\n    LEVEL_FORMAT_SKIP_OR_FAIL(4);\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        uint16_t room_count;\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &room_count));\n        for (int32_t i = 0; i < room_count; i++) {\n            LEVEL_FORMAT_SKIP_OR_FAIL(16);\n            LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // meshes\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(32); // portals\n\n            int16_t size_z;\n            int16_t size_x;\n            LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_z));\n            LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_x));\n            LEVEL_FORMAT_SKIP_OR_FAIL(size_z * size_x * 8);\n            LEVEL_FORMAT_SKIP_OR_FAIL(2);\n\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(18); // lights\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(18); // static meshes\n            LEVEL_FORMAT_SKIP_OR_FAIL(4);\n        }\n\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // floor data\n    } else {\n        Level_Section_ReadRooms(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // object meshes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // object mesh pointers\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // animations\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(6); // animation changes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // animation ranges\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation commands\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // animation bones\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation frames\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(18); // objects\n    } else {\n        Level_Section_ReadObjects(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // static objects\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(20); // textures\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sprites\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sprites sequences\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) {\n        LEVEL_FORMAT_SKIP_OR_FAIL(768); // palette\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // cameras\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sound effects\n\n    int32_t box_count;\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &box_count));\n    LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 20);\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // overlaps\n    LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 12); // zones\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animated texture ranges\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(22); // items\n    } else {\n        Level_Section_ReadItems(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_OR_FAIL(32 * 256); // light table\n\n    if (loader->layout != LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) {\n        LEVEL_FORMAT_SKIP_OR_FAIL(768); // palette\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(16); // cinematic frames\n    LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(1); // demo data\n\n    LEVEL_FORMAT_SKIP_OR_FAIL(2 * M_SAMPLE_COUNT); // sample lut\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sample infos\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(1); // sample data\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // samples\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1X) {\n        uint32_t inj_magic;\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &inj_magic));\n        LEVEL_FORMAT_TRY_OR_FAIL(inj_magic == INJECTION_MAGIC);\n    }\n\n    return true;\n}\n\nstatic bool M_Load(const LEVEL_FORMAT_LOADER *const loader, VFILE *const file)\n{\n    LEVEL_CONTEXT *const ctx = Level_Context_Get();\n    VFile_SetPos(file, 4);\n\n    // Read texture pages once the palette is available.\n    const int32_t num_pages = VFile_ReadS32(file);\n    VFile_Skip(file, num_pages * TEXTURE_PAGE_SIZE * sizeof(uint8_t));\n\n    const int32_t file_level_num = VFile_ReadS32(file);\n    LOG_INFO(\"file level num: %d\", file_level_num);\n\n    Level_Section_ReadRooms(ctx, file);\n    Level_Section_ReadObjectMeshes(ctx, file);\n    Level_Section_ReadAnims(ctx, file);\n    Level_Section_ReadAnimChanges(ctx, file);\n    Level_Section_ReadAnimRanges(ctx, file);\n    Level_Section_ReadAnimCommands(ctx, file);\n    Level_Section_ReadAnimBones(ctx, file);\n    Level_Section_ReadAnimFrames(ctx, file);\n    Level_Section_ReadObjects(ctx, file);\n    Level_Section_ReadStaticObjects(ctx, file);\n    Level_Section_ReadObjectTextures(ctx, file);\n    Level_Section_ReadSpriteTextures(ctx, file);\n    Level_Section_ReadSpriteSequences(ctx, file);\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) {\n        Level_Section_ReadPalettes(ctx, file);\n    }\n\n    Level_Section_ReadCamerasAndSinks(ctx, file);\n    Level_Section_ReadSoundSources(ctx, file);\n    Level_Section_ReadPathingData(ctx, file);\n    Level_Section_ReadAnimatedTextureRanges(ctx, file);\n    Level_Section_ReadItems(ctx, file);\n    Level_Section_ReadLightMap(ctx, file);\n\n    if (loader->layout != LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC) {\n        Level_Section_ReadPalettes(ctx, file);\n    }\n\n    Level_Section_ReadCinematicFrames(ctx, file);\n    Level_Section_ReadDemoData(ctx, file);\n    Level_Section_ReadSamples(ctx, file);\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR1X) {\n        VFILE *const embedded_injection = VFile_CreateFromBuffer(\n            file->cur_ptr, file->size - VFile_GetPos(file));\n        Inject_AppendInjection(embedded_injection);\n    }\n\n    VFile_SetPos(file, 4);\n    Level_Section_ReadTexturePages(ctx, file);\n\n    return true;\n}\n\nstatic const LEVEL_FORMAT_LOADER m_LevelLoaderTR1 = {\n    .game_version = 1,\n    .layout = LEVEL_FORMAT_LAYOUT_TR1,\n    .probe = M_Probe,\n    .load = M_Load,\n};\n\nstatic const LEVEL_FORMAT_LOADER m_LevelLoaderTR1DemoPC = {\n    .game_version = 1,\n    .layout = LEVEL_FORMAT_LAYOUT_TR1_DEMO_PC,\n    .probe = M_Probe,\n    .load = M_Load,\n};\n\nstatic const LEVEL_FORMAT_LOADER m_LevelLoaderTR1X = {\n    .game_version = 1,\n    .layout = LEVEL_FORMAT_LAYOUT_TR1X,\n    .probe = M_Probe,\n    .load = M_Load,\n};\n\nREGISTER_LEVEL_FORMAT_LOADER(100, m_LevelLoaderTR1X)\nREGISTER_LEVEL_FORMAT_LOADER(110, m_LevelLoaderTR1)\nREGISTER_LEVEL_FORMAT_LOADER(120, m_LevelLoaderTR1DemoPC)\n"
  },
  {
    "path": "src/trx/game/level/format/format_tr2.c",
    "content": "#include <trx/game/inject.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/format/priv.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/output/const.h>\n\n#define M_SAMPLE_COUNT 370\n\nstatic bool M_Probe(\n    const LEVEL_FORMAT_LOADER *const loader, VFILE *const file,\n    const LEVEL_FORMAT_PROBE_MODE mode)\n{\n    VFile_SetPos(file, 0);\n    LEVEL_CONTEXT probe_ctx = {\n        .loader = loader,\n    };\n\n    int32_t version;\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &version));\n    if (version != 45) {\n        return false;\n    }\n\n    LEVEL_FORMAT_SKIP_OR_FAIL(1792); // palettes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(TEXTURE_PAGE_SIZE * 3); // texture pages\n    LEVEL_FORMAT_SKIP_OR_FAIL(4); // unused version number\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        uint16_t room_count;\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &room_count));\n        for (int32_t i = 0; i < room_count; i++) {\n            LEVEL_FORMAT_SKIP_OR_FAIL(16);\n            LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // meshes\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(32); // portals\n\n            int16_t size_z;\n            int16_t size_x;\n            LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_z));\n            LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_x));\n            LEVEL_FORMAT_SKIP_OR_FAIL(size_z * size_x * 8); // sectors\n\n            LEVEL_FORMAT_SKIP_OR_FAIL(6); // lighting\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(24); // lights\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(20); // static meshes\n            LEVEL_FORMAT_SKIP_OR_FAIL(4);\n        }\n\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // floor data\n    } else {\n        Level_Section_ReadRooms(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // object meshes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // object mesh pointers\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // animations\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(6); // animation changes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // animation ranges\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation commands\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // animation bones\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation frames\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(18); // objects\n    } else {\n        Level_Section_ReadObjects(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // static objects\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(20); // object textures\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sprite textures\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sprites sequences\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // cameras/sinks\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sound sources\n\n    int32_t box_count;\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &box_count));\n    LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 8);\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // overlaps\n    LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 20); // zones\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animated texture ranges\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(24); // items\n    } else {\n        Level_Section_ReadItems(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_OR_FAIL(32 * 256); // light table\n    LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(16); // cinematic frames\n    LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(1); // demo data\n    LEVEL_FORMAT_SKIP_OR_FAIL(2 * M_SAMPLE_COUNT); // sample lut\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sample infos\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // samples\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR2X) {\n        uint32_t inj_magic;\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &inj_magic));\n        LEVEL_FORMAT_TRY_OR_FAIL(inj_magic == INJECTION_MAGIC);\n    }\n\n    return true;\n}\n\nstatic bool M_Load(const LEVEL_FORMAT_LOADER *const loader, VFILE *const file)\n{\n    LEVEL_CONTEXT *const ctx = Level_Context_Get();\n    VFile_SetPos(file, 4);\n\n    Level_Section_ReadPalettes(ctx, file);\n    Level_Section_ReadTexturePages(ctx, file);\n    VFile_Skip(file, 4);\n    Level_Section_ReadRooms(ctx, file);\n\n    Level_Section_ReadObjectMeshes(ctx, file);\n\n    Level_Section_ReadAnims(ctx, file);\n    Level_Section_ReadAnimChanges(ctx, file);\n    Level_Section_ReadAnimRanges(ctx, file);\n    Level_Section_ReadAnimCommands(ctx, file);\n    Level_Section_ReadAnimBones(ctx, file);\n    Level_Section_ReadAnimFrames(ctx, file);\n\n    Level_Section_ReadObjects(ctx, file);\n    Level_Section_ReadStaticObjects(ctx, file);\n    Level_Section_ReadObjectTextures(ctx, file);\n\n    Level_Section_ReadSpriteTextures(ctx, file);\n    Level_Section_ReadSpriteSequences(ctx, file);\n    Level_Section_ReadCamerasAndSinks(ctx, file);\n    Level_Section_ReadSoundSources(ctx, file);\n    Level_Section_ReadPathingData(ctx, file);\n    Level_Section_ReadAnimatedTextureRanges(ctx, file);\n    Level_Section_ReadItems(ctx, file);\n\n    Level_Section_ReadLightMap(ctx, file);\n    Level_Section_ReadCinematicFrames(ctx, file);\n    Level_Section_ReadDemoData(ctx, file);\n    Level_Section_ReadSamples(ctx, file);\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR2X) {\n        VFILE *const embedded_injection = VFile_CreateFromBuffer(\n            file->cur_ptr, file->size - VFile_GetPos(file));\n        Inject_AppendInjection(embedded_injection);\n    }\n    return true;\n}\n\nstatic const LEVEL_FORMAT_LOADER m_LevelLoaderTR2 = {\n    .game_version = 2,\n    .layout = LEVEL_FORMAT_LAYOUT_TR2,\n    .load = M_Load,\n    .probe = M_Probe,\n};\n\nstatic const LEVEL_FORMAT_LOADER m_LevelLoaderTR2X = {\n    .game_version = 2,\n    .layout = LEVEL_FORMAT_LAYOUT_TR2X,\n    .probe = M_Probe,\n    .load = M_Load,\n};\n\nREGISTER_LEVEL_FORMAT_LOADER(200, m_LevelLoaderTR2X)\nREGISTER_LEVEL_FORMAT_LOADER(210, m_LevelLoaderTR2)\n"
  },
  {
    "path": "src/trx/game/level/format/format_tr3.c",
    "content": "#include <trx/game/inject.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/format/priv.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/output/const.h>\n\n#define M_SAMPLE_COUNT 370\n\nstatic bool M_Probe(\n    const LEVEL_FORMAT_LOADER *const loader, VFILE *const file,\n    const LEVEL_FORMAT_PROBE_MODE mode)\n{\n    VFile_SetPos(file, 0);\n    LEVEL_CONTEXT probe_ctx = {\n        .loader = loader,\n    };\n\n    uint32_t version;\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &version));\n    if (!(version == 0xFF080038ULL || version == 0xFF180038ULL)) {\n        return false;\n    }\n\n    LEVEL_FORMAT_SKIP_OR_FAIL(1792); // palettes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(TEXTURE_PAGE_SIZE * 3); // texture pages\n    LEVEL_FORMAT_SKIP_OR_FAIL(4); // unused version number\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        uint16_t room_count;\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &room_count));\n        for (int32_t i = 0; i < room_count; i++) {\n            LEVEL_FORMAT_SKIP_OR_FAIL(16);\n            LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // meshes\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(32); // portals\n\n            int16_t size_z;\n            int16_t size_x;\n            LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_z));\n            LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS16(file, &size_x));\n            LEVEL_FORMAT_SKIP_OR_FAIL(size_z * size_x * 8); // sectors\n\n            LEVEL_FORMAT_SKIP_OR_FAIL(4); // lighting\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(24); // lights\n            LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(20); // static meshes\n            LEVEL_FORMAT_SKIP_OR_FAIL(7);\n        }\n\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // floor data\n    } else {\n        Level_Section_ReadRooms(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // object meshes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // object mesh pointers\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // animations\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(6); // animation changes\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // animation ranges\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation commands\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // animation bones\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animation frames\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(18); // objects\n    } else {\n        Level_Section_ReadObjects(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(32); // static objects\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sprite textures\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sprites sequences\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // cameras/sinks\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(16); // sound sources\n\n    int32_t box_count;\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &box_count));\n    LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 8);\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // overlaps\n    LEVEL_FORMAT_SKIP_OR_FAIL(box_count * 20); // zones\n\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(2); // animated texture ranges\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(20); // object textures\n\n    if (mode == LEVEL_FORMAT_PROBE_MINIMAL) {\n        LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(24); // items\n    } else {\n        Level_Section_ReadItems(&probe_ctx, file);\n    }\n\n    LEVEL_FORMAT_SKIP_OR_FAIL(32 * 256); // light table\n    LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(16); // cinematic frames\n    LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(1); // demo data\n    LEVEL_FORMAT_SKIP_OR_FAIL(2 * M_SAMPLE_COUNT); // sample lut\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(8); // sample infos\n    LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(4); // samples\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR3X) {\n        uint32_t inj_magic;\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU32(file, &inj_magic));\n        LEVEL_FORMAT_TRY_OR_FAIL(inj_magic == INJECTION_MAGIC);\n    }\n\n    return true;\n}\n\nstatic bool M_Load(const LEVEL_FORMAT_LOADER *const loader, VFILE *const file)\n{\n    LEVEL_CONTEXT *const ctx = Level_Context_Get();\n    VFile_SetPos(file, 4);\n\n    Level_Section_ReadPalettes(ctx, file);\n    Level_Section_ReadTexturePages(ctx, file);\n    VFile_Skip(file, 4);\n    Level_Section_ReadRooms(ctx, file);\n\n    Level_Section_ReadObjectMeshes(ctx, file);\n\n    Level_Section_ReadAnims(ctx, file);\n    Level_Section_ReadAnimChanges(ctx, file);\n    Level_Section_ReadAnimRanges(ctx, file);\n    Level_Section_ReadAnimCommands(ctx, file);\n    Level_Section_ReadAnimBones(ctx, file);\n    Level_Section_ReadAnimFrames(ctx, file);\n\n    Level_Section_ReadObjects(ctx, file);\n    Level_Section_ReadStaticObjects(ctx, file);\n\n    Level_Section_ReadSpriteTextures(ctx, file);\n    Level_Section_ReadSpriteSequences(ctx, file);\n    Level_Section_ReadCamerasAndSinks(ctx, file);\n    Level_Section_ReadSoundSources(ctx, file);\n    Level_Section_ReadPathingData(ctx, file);\n\n    Level_Section_ReadAnimatedTextureRanges(ctx, file);\n    Level_Section_ReadObjectTextures(ctx, file);\n    Level_Section_ReadItems(ctx, file);\n\n    Level_Section_ReadLightMap(ctx, file);\n    Level_Section_ReadCinematicFrames(ctx, file);\n    Level_Section_ReadDemoData(ctx, file);\n    Level_Section_ReadSamples(ctx, file);\n\n    if (loader->layout == LEVEL_FORMAT_LAYOUT_TR3X) {\n        VFILE *const embedded_injection = VFile_CreateFromBuffer(\n            file->cur_ptr, file->size - VFile_GetPos(file));\n        Inject_AppendInjection(embedded_injection);\n    }\n    return true;\n}\n\nstatic const LEVEL_FORMAT_LOADER m_LevelLoaderTR3 = {\n    .game_version = 3,\n    .layout = LEVEL_FORMAT_LAYOUT_TR3,\n    .load = M_Load,\n    .probe = M_Probe,\n};\n\nREGISTER_LEVEL_FORMAT_LOADER(300, m_LevelLoaderTR3)\n"
  },
  {
    "path": "src/trx/game/level/format/pipeline.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/format/priv.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\ntypedef struct {\n    int32_t priority;\n    const LEVEL_FORMAT_LOADER *loader;\n} M_REGISTERED_LOADER;\n\nstatic VECTOR *m_Loaders = nullptr;\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    if (m_Loaders != nullptr) {\n        Vector_Free(m_Loaders);\n        m_Loaders = nullptr;\n    }\n}\n\nvoid Level_Format_RegisterLoader(\n    const int32_t priority, const LEVEL_FORMAT_LOADER *const loader)\n{\n    if (m_Loaders == nullptr) {\n        m_Loaders = Vector_Create(sizeof(M_REGISTERED_LOADER));\n    }\n\n    M_REGISTERED_LOADER registered = {\n        .priority = priority,\n        .loader = loader,\n    };\n\n    int32_t insert_idx = m_Loaders->count;\n    for (int32_t i = 0; i < m_Loaders->count; i++) {\n        const M_REGISTERED_LOADER *const test = Vector_Get(m_Loaders, i);\n        if (priority < test->priority) {\n            insert_idx = i;\n            break;\n        }\n    }\n    Vector_Insert(m_Loaders, insert_idx, &registered);\n}\n\nstatic int32_t M_GetRegisteredLoaderCount(void)\n{\n    if (m_Loaders == nullptr) {\n        return 0;\n    }\n    return m_Loaders->count;\n}\n\nstatic const LEVEL_FORMAT_LOADER *M_GetRegisteredLoader(const int32_t index)\n{\n    return ((M_REGISTERED_LOADER *)Vector_Get(m_Loaders, index))->loader;\n}\n\nconst LEVEL_FORMAT_LOADER *Level_Format_GuessLoader(VFILE *const file)\n{\n    const LEVEL_FORMAT_LOADER *result = nullptr;\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t loader_count = M_GetRegisteredLoaderCount();\n    for (int32_t i = 0; i < loader_count; i++) {\n        const LEVEL_FORMAT_LOADER *const loader = M_GetRegisteredLoader(i);\n        if (loader->probe(loader, file, LEVEL_FORMAT_PROBE_MINIMAL)) {\n            result = loader;\n            break;\n        }\n    }\n    Benchmark_End(&benchmark, nullptr);\n    return result;\n}\n\nLEVEL_FORMAT_LAYOUT Level_Format_GuessLayout(VFILE *const file)\n{\n    const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file);\n    if (loader != nullptr) {\n        return loader->layout;\n    }\n    return LEVEL_FORMAT_LAYOUT_UNKNOWN;\n}\n\nconst LEVEL_FORMAT_LOADER *Level_Format_LoadFromFile(\n    const GF_LEVEL *const level)\n{\n    GameBuf_Reset();\n\n    BENCHMARK benchmark = Benchmark_Start();\n    VFILE *const file = VFile_CreateFromPath(level->path);\n    if (file == nullptr) {\n        Shell_ExitSystemFmt(\"Could not open %s\", level->path);\n    }\n\n    const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file);\n    if (loader == nullptr) {\n        Shell_ExitSystemFmt(\"Failed to load %s\", level->path);\n    }\n    g_TRVersion = loader->game_version;\n    Level_Context_Reset(loader);\n    ASSERT(loader->load != nullptr);\n    loader->load(loader, file);\n\n    VFile_Close(file);\n    Benchmark_End(&benchmark, nullptr);\n    return loader;\n}\n"
  },
  {
    "path": "src/trx/game/level/format/priv.h",
    "content": "#pragma once\n\n#include <trx/core/utils.h>\n#include <trx/game/level/format/format.h>\n\nvoid Level_Format_RegisterLoader(\n    int32_t priority, const LEVEL_FORMAT_LOADER *loader);\n\n#define REGISTER_LEVEL_FORMAT_LOADER(priority_, loader_)                       \\\n    __attribute__((__constructor__)) static void CONCAT(                       \\\n        M_RegisterLevelFormatLoader_, __LINE__)(void)                          \\\n    {                                                                          \\\n        Level_Format_RegisterLoader(priority_, &(loader_));                    \\\n    }\n\n// Helper control-flow macros\n// =============================================================================\n#define LEVEL_FORMAT_TRY_OR_FAIL(call_)                                        \\\n    do {                                                                       \\\n        if (!(call_)) {                                                        \\\n            return false;                                                      \\\n        }                                                                      \\\n    } while (0)\n\n#define LEVEL_FORMAT_SKIP_OR_FAIL(size_)                                       \\\n    LEVEL_FORMAT_TRY_OR_FAIL(VFile_TrySkip(file, size_))\n\n#define LEVEL_FORMAT_SKIP_ARR_S32_OR_FAIL(size_)                               \\\n    do {                                                                       \\\n        int32_t count;                                                         \\\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadS32(file, &count));              \\\n        LEVEL_FORMAT_SKIP_OR_FAIL(count * (size_));                            \\\n    } while (0)\n\n#define LEVEL_FORMAT_SKIP_ARR_U16_OR_FAIL(size_)                               \\\n    do {                                                                       \\\n        uint16_t count;                                                        \\\n        LEVEL_FORMAT_TRY_OR_FAIL(VFile_TryReadU16(file, &count));              \\\n        LEVEL_FORMAT_SKIP_OR_FAIL(count * (size_));                            \\\n    } while (0)\n"
  },
  {
    "path": "src/trx/game/level/pipeline.c",
    "content": "#include <trx/config.h>\n#include <trx/core/benchmark.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/game/inject.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/level.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/atlantean.h>\n#include <trx/game/rooms.h>\n#include <trx/game/shell/paths.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#include <stdlib.h>\n#include <string.h>\n\ntypedef struct {\n    int32_t game_index;\n    int32_t file_index;\n} M_SAMPLE_ENTRY;\n\nstatic int32_t M_CompareSampleOffsets(const void *const a, const void *const b)\n{\n    const M_SAMPLE_ENTRY *const entry_a = (M_SAMPLE_ENTRY *)a;\n    const M_SAMPLE_ENTRY *const entry_b = (M_SAMPLE_ENTRY *)b;\n    return entry_a->file_index - entry_b->file_index;\n}\n\nstatic void M_InitialiseSamplesFromFile(\n    LEVEL_CONTEXT *const ctx, const char *file_name)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    M_SAMPLE_ENTRY *entries = nullptr;\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n\n    MYFILE *fp = nullptr;\n    if (file_name == nullptr) {\n        goto finish;\n    }\n\n    fp = File_Open(file_name, FILE_OPEN_READ);\n    if (fp == nullptr) {\n        LOG_ERROR(\"Could not open %s samples file\", file_name);\n        goto finish;\n    }\n    LOG_DEBUG(\"Loading samples from %s\", file_name);\n\n    const int32_t sample_count = info->samples.offset_count;\n    entries = Memory_Alloc(sizeof(M_SAMPLE_ENTRY) * sample_count);\n    for (int32_t i = 0; i < sample_count; i++) {\n        entries[i].game_index = i;\n        entries[i].file_index = info->samples.offsets[i];\n    }\n    qsort(\n        entries, sample_count, sizeof(M_SAMPLE_ENTRY), M_CompareSampleOffsets);\n\n    for (int32_t i = 0, current_sample = 0; current_sample < sample_count;\n         i++) {\n        uint32_t header[11] = {};\n        File_ReadData(fp, header, 11 * sizeof(uint32_t));\n        if (header[0] != MKTAG('R', 'I', 'F', 'F')\n            || header[2] != MKTAG('W', 'A', 'V', 'E')\n            || header[9] != MKTAG('d', 'a', 't', 'a')) {\n            LOG_ERROR(\"Unexpected sample header for sample %d\", i);\n            goto finish;\n        }\n\n        const size_t header_size = 11 * sizeof(uint32_t);\n        const size_t aligned_size = (header[10] + 1) & ~1;\n        const size_t size = aligned_size + header_size;\n        const M_SAMPLE_ENTRY *const entry = &entries[current_sample];\n        if (entry->file_index != i) {\n            File_Seek(fp, aligned_size, FILE_SEEK_CUR);\n            continue;\n        }\n\n        char *sample_data = Memory_Alloc(size);\n        memcpy(sample_data, header, header_size);\n        File_ReadData(fp, sample_data + header_size, aligned_size);\n        Sound_LoadSampleData(entry->game_index, sample_data, size);\n        Memory_FreePointer(&sample_data);\n\n        current_sample++;\n    }\n\nfinish:\n    if (fp != nullptr) {\n        File_Close(fp);\n    }\n    Memory_FreePointer(&entries);\n    Memory_FreePointer(&info->samples.offsets);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nstatic void M_InitialiseSamplesFromLevelInfo(LEVEL_CONTEXT *const ctx)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    const int32_t sample_count = info->samples.offset_count;\n\n    // TODO: this assumes that sample pointers are sorted - adopt TR2's approach\n    // of sorting by index, verifying WAV headers and using WAV sample size.\n    for (int32_t i = 0; i < sample_count; i++) {\n        const int32_t current_offset = info->samples.offsets[i];\n        const int32_t next_offset = i + 1 >= sample_count\n            ? info->samples.data_size\n            : info->samples.offsets[i + 1];\n\n        const char *const sample_data = &info->samples.data[current_offset];\n        const size_t sample_size = next_offset - current_offset;\n        Sound_LoadSampleData(i, sample_data, sample_size);\n    }\n\n    Memory_FreePointer(&info->samples.offsets);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nstatic void M_MarkWaterEdgeVertices(void)\n{\n    if (!g_Config.visuals.fix_texture_issues) {\n        return;\n    }\n\n    BENCHMARK benchmark = Benchmark_Start();\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        const int32_t y_test =\n            room->flags.underwater ? room->max_ceiling : room->min_floor;\n        for (int32_t j = 0; j < room->mesh.num_vertices; j++) {\n            ROOM_VERTEX *const vertex = &room->mesh.vertices[j];\n            if (vertex->pos.y == y_test) {\n                vertex->flags.disable_wibble = true;\n            }\n        }\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nstatic void M_CompleteSetup(\n    const LEVEL_FORMAT_LOADER *const loader, const GF_LEVEL *const level)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    LEVEL_CONTEXT *const ctx = Level_Context_Get();\n\n    // We inject explosions sprites and sounds, although in the original game,\n    // some levels lack them, resulting in no audio or visual effects when\n    // killing mutants. This is to maintain that feature.\n    Atlantean_ToggleExplosions(Object_Get(O_EXPLOSION_1)->loaded);\n\n    Inject_AllInjections();\n\n    Level_Finalize_LoadAnimFrames(ctx);\n    Level_Finalize_LoadAnimCommands(ctx);\n    if (g_TRVersion == 1) {\n        M_MarkWaterEdgeVertices();\n    }\n    Level_Finalize_LoadObjectsAndItems(ctx);\n\n    // Configure enemies who carry and drop items\n    Carrier_InitialiseLevel(level);\n\n    Level_Finalize_LoadRooms(ctx);\n    Level_Finalize_LoadTextures(ctx);\n    Level_Finalize_LoadTexturePages(ctx);\n    Level_Finalize_LoadPalettes(ctx);\n\n    if (loader->game_version == 1) {\n        M_InitialiseSamplesFromLevelInfo(ctx);\n    } else {\n        M_InitialiseSamplesFromFile(ctx, level->settings.sfx_path);\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Pipeline_Load(const GF_LEVEL *const level)\n{\n    LOG_INFO(\"%d (%s)\", level->num, level->path);\n    BENCHMARK benchmark = Benchmark_Start();\n\n    Inject_InitLevel(level, INJECTION_MODE_FULL);\n    const LEVEL_FORMAT_LOADER *const loader = Level_Format_LoadFromFile(level);\n    M_CompleteSetup(loader, level);\n    Inject_Cleanup();\n\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/pipeline.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/finalize.h>\n#include <trx/game/level/sections/append.h>\n#include <trx/game/level/sections/read.h>\n\nvoid Level_Pipeline_Load(const GF_LEVEL *level);\n"
  },
  {
    "path": "src/trx/game/level/sections/anims.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/game/anims.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/sections/append.h>\n#include <trx/game/level/sections/read.h>\n\nstatic void M_ReadPosition(XYZ_32 *const pos, VFILE *const file)\n{\n    pos->x = VFile_ReadS32(file);\n    pos->y = VFile_ReadS32(file);\n    pos->z = VFile_ReadS32(file);\n}\n\nvoid Level_Section_ReadAnims(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_anims = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->anims.anim_count = num_anims;\n    LOG_INFO(\"anims: %d\", num_anims);\n    Anim_InitialiseAnims(num_anims + Inject_GetDataCount(IDT_ANIMS));\n    Level_Section_AppendAnims(0, num_anims, file);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendAnims(\n    const int32_t base_idx, const int32_t num_anims, VFILE *const file)\n{\n    for (int32_t i = 0; i < num_anims; i++) {\n        ANIM *const anim = Anim_GetAnim(base_idx + i);\n        anim->frame_ofs = VFile_ReadU32(file);\n        anim->frame_ptr = nullptr; // filled later by the animation frame loader\n        anim->interpolation = VFile_ReadU8(file);\n        anim->frame_size = VFile_ReadU8(file);\n        anim->current_anim_state = VFile_ReadS16(file);\n        anim->velocity = VFile_ReadS32(file);\n        anim->acceleration = VFile_ReadS32(file);\n        anim->frame_base = VFile_ReadS16(file);\n        anim->frame_end = VFile_ReadS16(file);\n        anim->jump_anim_num = VFile_ReadS16(file);\n        anim->jump_frame_num = VFile_ReadS16(file);\n        anim->num_changes = VFile_ReadS16(file);\n        anim->change_idx = VFile_ReadS16(file);\n        anim->num_commands = VFile_ReadS16(file);\n        anim->command_idx = VFile_ReadS16(file);\n    }\n}\n\nvoid Level_Section_ReadAnimChanges(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_anim_changes = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->anims.change_count = num_anim_changes;\n    LOG_INFO(\"anim changes: %d\", num_anim_changes);\n    Anim_InitialiseChanges(\n        num_anim_changes + Inject_GetDataCount(IDT_ANIM_CHANGES));\n    Level_Section_AppendAnimChanges(0, num_anim_changes, file);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendAnimChanges(\n    const int32_t base_idx, const int32_t num_changes, VFILE *const file)\n{\n    for (int32_t i = 0; i < num_changes; i++) {\n        ANIM_CHANGE *const anim_change = Anim_GetChange(base_idx + i);\n        anim_change->goal_anim_state = VFile_ReadS16(file);\n        anim_change->num_ranges = VFile_ReadS16(file);\n        anim_change->range_idx = VFile_ReadS16(file);\n    }\n}\n\nvoid Level_Section_ReadAnimRanges(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_anim_ranges = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->anims.range_count = num_anim_ranges;\n    LOG_INFO(\"anim ranges: %d\", num_anim_ranges);\n    Anim_InitialiseRanges(\n        num_anim_ranges + Inject_GetDataCount(IDT_ANIM_RANGES));\n    Level_Section_AppendAnimRanges(0, num_anim_ranges, file);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendAnimRanges(\n    const int32_t base_idx, const int32_t num_ranges, VFILE *const file)\n{\n    for (int32_t i = 0; i < num_ranges; i++) {\n        ANIM_RANGE *const anim_range = Anim_GetRange(base_idx + i);\n        anim_range->start_frame = VFile_ReadS16(file);\n        anim_range->end_frame = VFile_ReadS16(file);\n        anim_range->link_anim_num = VFile_ReadS16(file);\n        anim_range->link_frame_num = VFile_ReadS16(file);\n    }\n}\n\nvoid Level_Section_ReadAnimCommands(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_commands = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->anims.command_count = num_commands;\n    LOG_INFO(\"anim commands: %d\", num_commands);\n    info->anims.commands = Memory_Alloc(\n        sizeof(int16_t)\n        * (num_commands + Inject_GetDataCount(IDT_ANIM_COMMANDS)));\n    Level_Section_AppendAnimCommands(0, num_commands, file);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendAnimCommands(\n    const int32_t base_idx, const int32_t num_commands, VFILE *const file)\n{\n    LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo();\n    VFile_Read(\n        file, &info->anims.commands[base_idx], sizeof(int16_t) * num_commands);\n}\n\nvoid Level_Section_ReadAnimBones(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_anim_bones = VFile_ReadS32(file) / ANIM_BONE_SIZE;\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->anims.bone_count = num_anim_bones;\n    LOG_INFO(\"anim bones: %d\", num_anim_bones);\n    Anim_InitialiseBones(num_anim_bones + Inject_GetDataCount(IDT_ANIM_BONES));\n    Level_Section_AppendAnimBones(0, num_anim_bones, file);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendAnimBones(\n    const int32_t base_idx, const int32_t num_bones, VFILE *const file)\n{\n    for (int32_t i = 0; i < num_bones; i++) {\n        ANIM_BONE *const bone = Anim_GetBone(base_idx + i);\n        const int32_t flags = VFile_ReadS32(file);\n        bone->matrix_pop = (flags & 1) != 0;\n        bone->matrix_push = (flags & 2) != 0;\n        bone->rot.x = false;\n        bone->rot.y = false;\n        bone->rot.z = false;\n        M_ReadPosition(&bone->pos, file);\n    }\n}\n\nvoid Level_Section_ReadAnimFrames(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t raw_data_count = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->anims.frame_count = raw_data_count;\n    LOG_INFO(\"raw anim frames: %d\", raw_data_count);\n    info->anims.frames = Memory_Alloc(\n        sizeof(int16_t)\n        * (raw_data_count + Inject_GetDataCount(IDT_ANIM_FRAMES)));\n    Level_Section_AppendAnimFrames(0, raw_data_count, file);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendAnimFrames(\n    const int32_t base_idx, const int32_t num_frames, VFILE *const file)\n{\n    LEVEL_CONTEXT_INFO *const info = Level_Context_GetInfo();\n    VFile_Read(\n        file, &info->anims.frames[base_idx], sizeof(int16_t) * num_frames);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/append.h",
    "content": "#pragma once\n\n#include <trx/core/virtual_file.h>\n\n#include <stdint.h>\n\nvoid Level_Section_AppendObjectMeshes(\n    int32_t num_offsets, const int32_t *offsets, VFILE *file);\nvoid Level_Section_AppendAnims(\n    int32_t base_idx, int32_t num_anims, VFILE *file);\nvoid Level_Section_AppendAnimChanges(\n    int32_t base_idx, int32_t num_changes, VFILE *file);\nvoid Level_Section_AppendAnimRanges(\n    int32_t base_idx, int32_t num_ranges, VFILE *file);\nvoid Level_Section_AppendAnimCommands(\n    int32_t base_idx, int32_t num_commands, VFILE *file);\nvoid Level_Section_AppendAnimBones(\n    int32_t base_idx, int32_t num_bones, VFILE *file);\nvoid Level_Section_AppendAnimFrames(\n    int32_t base_idx, int32_t num_frames, VFILE *file);\nvoid Level_Section_AppendObjectTextures(\n    int32_t base_idx, int16_t base_page_idx, int32_t num_textures, VFILE *file,\n    bool use_tr3_adjustment);\nvoid Level_Section_AppendSpriteTextures(\n    int32_t base_idx, int16_t base_page_idx, int32_t num_textures, VFILE *file);\n"
  },
  {
    "path": "src/trx/game/level/sections/audio.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/sound.h>\n\nstatic size_t M_GetSampleCount(const LEVEL_FORMAT_LOADER *const loader)\n{\n    switch (loader->game_version) {\n    case 1:\n        return 256;\n    case 2:\n    case 3:\n        return 370;\n    default:\n        ASSERT_FAIL();\n    }\n    return 0;\n}\n\nvoid Level_Section_ReadSamples(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n\n    const int32_t sample_count = M_GetSampleCount(loader);\n    int16_t *const sample_lut = Memory_Alloc(sizeof(int16_t) * sample_count);\n    int16_t *const sample_lut_inv =\n        Memory_Alloc(sizeof(int16_t) * sample_count);\n    VFile_Read(file, sample_lut, sizeof(int16_t) * sample_count);\n    for (int32_t i = 0; i < sample_count; i++) {\n        if (sample_lut[i] != -1) {\n            sample_lut_inv[sample_lut[i]] = i;\n        }\n    }\n\n    const int32_t num_sample_infos = VFile_ReadS32(file);\n    LOG_INFO(\"sample infos: %d\", num_sample_infos);\n    for (int32_t i = 0; i < num_sample_infos; i++) {\n        SAMPLE_INFO *const sample_info =\n            Sound_GetOrCreateSample(sample_lut_inv[i]);\n        ASSERT(sample_info != nullptr);\n        sample_info->number = VFile_ReadS16(file);\n\n        if (loader->game_version >= 3) {\n            sample_info->volume = VFile_ReadU8(file) << 7;\n            sample_info->range = VFile_ReadU8(file) * WALL_L;\n        } else {\n            sample_info->volume = VFile_ReadU16(file);\n            sample_info->range = 10 * WALL_L;\n        }\n\n        if (loader->game_version >= 3) {\n            sample_info->randomness = VFile_ReadU8(file);\n            sample_info->pitch = VFile_ReadS8(file);\n        } else {\n            sample_info->randomness = VFile_ReadU16(file);\n            sample_info->pitch = 0;\n        }\n\n        sample_info->flags.all = VFile_ReadU16(file);\n\n        Sound_ReserveSampleData(\n            sample_info->number, sample_info->flags.num_samples);\n        if (loader->game_version == 1) {\n            switch (sample_info->flags.mode_bits) {\n            case 0:\n                sample_info->mode = SAMPLE_MODE_WAIT;\n                break;\n            case 1:\n                sample_info->mode = SAMPLE_MODE_RESTART;\n                break;\n            case 2:\n                sample_info->mode = SAMPLE_MODE_LOOPED;\n                break;\n            case 3:\n                LOG_WARNING(\n                    \"Unexpected sample mode for sample %d. flags=%0X\", i,\n                    sample_info->flags);\n                break;\n            }\n        } else {\n            switch (sample_info->flags.mode_bits) {\n            case 0:\n                sample_info->mode = SAMPLE_MODE_NORMAL;\n                break;\n            case 1:\n                sample_info->mode = SAMPLE_MODE_WAIT;\n                break;\n            case 2:\n                sample_info->mode = SAMPLE_MODE_RESTART;\n                break;\n            case 3:\n                sample_info->mode = SAMPLE_MODE_LOOPED;\n                break;\n            }\n        }\n    }\n\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    if (loader->game_version == 1) {\n        const int32_t data_size = VFile_ReadS32(file);\n        info->samples.data_size = data_size;\n        LOG_INFO(\"%d sample data size\", data_size);\n\n        info->samples.data = GameBuf_Alloc(\n            data_size + Inject_GetDataCount(IDT_SAMPLE_DATA), GBUF_SAMPLES);\n        VFile_Read(file, info->samples.data, sizeof(char) * data_size);\n    }\n\n    const int32_t num_offsets = VFile_ReadS32(file);\n    LOG_INFO(\"samples: %d\", num_offsets);\n    info->samples.offset_count = num_offsets;\n\n    info->samples.offsets = Memory_Alloc(\n        sizeof(int32_t)\n        * (num_offsets + Inject_GetDataCount(IDT_SAMPLE_INDICES)));\n    VFile_Read(file, info->samples.offsets, sizeof(int32_t) * num_offsets);\n\n    Memory_Free(sample_lut);\n    Memory_Free(sample_lut_inv);\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/cinematics.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/demo.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/sound.h>\n\nstatic void M_ReadPosition(XYZ_32 *const pos, VFILE *const file)\n{\n    pos->x = VFile_ReadS32(file);\n    pos->y = VFile_ReadS32(file);\n    pos->z = VFile_ReadS32(file);\n}\n\nstatic void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file)\n{\n    vertex->x = VFile_ReadS16(file);\n    vertex->y = VFile_ReadS16(file);\n    vertex->z = VFile_ReadS16(file);\n}\n\nstatic void M_ReadObjectVector(OBJECT_VECTOR *const obj, VFILE *const file)\n{\n    M_ReadPosition(&obj->pos, file);\n    obj->data = VFile_ReadS16(file);\n    obj->flags = VFile_ReadS16(file);\n}\n\nvoid Level_Section_ReadCinematicFrames(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int16_t num_frames = VFile_ReadS16(file);\n    const int32_t inj_frames = Inject_GetDataCount(IDT_CINEMATIC_FRAMES);\n    LOG_INFO(\"cinematic frames: %d\", num_frames);\n    Camera_InitialiseCineFrames(MAX(num_frames, inj_frames));\n    for (int32_t i = 0; i < num_frames; i++) {\n        CINE_FRAME *const frame = Camera_GetCineFrame(i);\n        M_ReadVertex(&frame->target.shift, file);\n        M_ReadVertex(&frame->camera.shift, file);\n        frame->fov = VFile_ReadS16(file);\n        frame->roll = VFile_ReadS16(file);\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadCamerasAndSinks(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_objects = VFile_ReadS32(file);\n    LOG_DEBUG(\"fixed cameras/sinks: %d\", num_objects);\n    Camera_InitialiseFixedObjects(num_objects);\n    for (int32_t i = 0; i < num_objects; i++) {\n        M_ReadObjectVector(Camera_GetFixedObject(i), file);\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadDemoData(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const uint16_t size = VFile_ReadU16(file);\n    LOG_INFO(\"demo buffer size: %d\", size);\n    Demo_LoadData(file, size);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadSoundSources(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_sources = VFile_ReadS32(file);\n    LOG_INFO(\"sound sources: %d\", num_sources);\n    Sound_InitialiseSources(num_sources);\n    for (int32_t i = 0; i < num_sources; i++) {\n        M_ReadObjectVector(Sound_GetSource(i), file);\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/meshes.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/sections/append.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/objects.h>\n\nstatic void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file)\n{\n    vertex->x = VFile_ReadS16(file);\n    vertex->y = VFile_ReadS16(file);\n    vertex->z = VFile_ReadS16(file);\n}\n\nstatic void M_ReadFace(\n    FACE *const face, const size_t vertex_count, VFILE *const file)\n{\n    face->vertex_count = vertex_count;\n    for (size_t i = 0; i < vertex_count; i++) {\n        face->vertices[i] = VFile_ReadU16(file);\n        face->texture_zw[i].z = 1.0f;\n        face->texture_zw[i].w = 1.0f;\n    }\n    const uint16_t texture_idx = VFile_ReadU16(file);\n    face->texture_idx = texture_idx & 0x7FFF;\n    face->double_sided = (texture_idx & 0x8000) != 0;\n    face->enable_reflections = false;\n}\n\nstatic void M_ReadObjectMesh(OBJECT_MESH *const mesh, VFILE *const file)\n{\n    M_ReadVertex(&mesh->center, file);\n    mesh->radius = VFile_ReadS16(file);\n    VFile_Skip(file, sizeof(int16_t));\n\n    mesh->enable_reflections = false;\n    mesh->enable_caustics = false;\n    mesh->depth_adjustment = 0.005;\n\n    {\n        mesh->num_vertices = VFile_ReadS16(file);\n        mesh->vertices =\n            GameBuf_Alloc(sizeof(XYZ_16) * mesh->num_vertices, GBUF_MESHES);\n        for (int32_t i = 0; i < mesh->num_vertices; i++) {\n            M_ReadVertex(&mesh->vertices[i], file);\n        }\n    }\n\n    {\n        mesh->num_lights = VFile_ReadS16(file);\n        if (mesh->num_lights > 0) {\n            mesh->lighting.normals =\n                GameBuf_Alloc(sizeof(XYZ_16) * mesh->num_lights, GBUF_MESHES);\n            for (int32_t i = 0; i < mesh->num_lights; i++) {\n                M_ReadVertex(&mesh->lighting.normals[i], file);\n            }\n        } else {\n            mesh->lighting.lights = GameBuf_Alloc(\n                sizeof(int16_t) * ABS(mesh->num_lights), GBUF_MESHES);\n            for (int32_t i = 0; i < ABS(mesh->num_lights); i++) {\n                mesh->lighting.lights[i] = VFile_ReadS16(file);\n            }\n        }\n    }\n\n    {\n        mesh->tex_face4s.count = VFile_ReadS16(file);\n        size_t pos = VFile_GetPos(file);\n        VFile_Skip(file, 10 * mesh->tex_face4s.count);\n        mesh->tex_face3s.count = VFile_ReadS16(file);\n        VFile_Skip(file, 8 * mesh->tex_face3s.count);\n        mesh->flat_face4s.count = VFile_ReadS16(file);\n        VFile_Skip(file, 10 * mesh->flat_face4s.count);\n        mesh->flat_face3s.count = VFile_ReadS16(file);\n        VFile_SetPos(file, pos);\n\n        mesh->tex_faces.count = mesh->tex_face4s.count + mesh->tex_face3s.count;\n        mesh->flat_faces.count =\n            mesh->flat_face4s.count + mesh->flat_face3s.count;\n        mesh->all_faces.count = mesh->tex_faces.count + mesh->flat_faces.count;\n        FACE *face_ptr =\n            GameBuf_Alloc(sizeof(FACE) * mesh->all_faces.count, GBUF_MESHES);\n\n        mesh->all_faces.data = face_ptr;\n        mesh->tex_faces.data = face_ptr;\n        mesh->tex_face4s.data = face_ptr;\n        for (int32_t i = 0; i < mesh->tex_face4s.count; i++) {\n            M_ReadFace(face_ptr++, 4, file);\n        }\n        VFile_Skip(file, 2);\n\n        mesh->tex_face3s.data = face_ptr;\n        for (int32_t i = 0; i < mesh->tex_face3s.count; i++) {\n            M_ReadFace(face_ptr++, 3, file);\n        }\n        VFile_Skip(file, 2);\n\n        mesh->flat_faces.data = face_ptr;\n        mesh->flat_face4s.data = face_ptr;\n        for (int32_t i = 0; i < mesh->flat_face4s.count; i++) {\n            M_ReadFace(face_ptr++, 4, file);\n        }\n        VFile_Skip(file, 2);\n\n        mesh->flat_face3s.data = face_ptr;\n        for (int32_t i = 0; i < mesh->flat_face3s.count; i++) {\n            M_ReadFace(face_ptr++, 3, file);\n        }\n        VFile_Skip(file, 2);\n    }\n}\n\nvoid Level_Section_ReadObjectMeshes(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_meshes = VFile_ReadS32(file);\n    LOG_INFO(\"object mesh data: %d\", num_meshes);\n\n    const size_t data_start_pos = VFile_GetPos(file);\n    VFile_Skip(file, num_meshes * sizeof(int16_t));\n\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->mesh_ptr_count = VFile_ReadS32(file);\n    LOG_INFO(\"object mesh offsets: %d\", info->mesh_ptr_count);\n    const int32_t alloc_size = info->mesh_ptr_count * sizeof(int32_t);\n    int32_t *mesh_offsets = Memory_Alloc(alloc_size);\n    VFile_Read(file, mesh_offsets, alloc_size);\n\n    const size_t end_pos = VFile_GetPos(file);\n    VFile_SetPos(file, data_start_pos);\n\n    Object_InitialiseMeshes(\n        info->mesh_ptr_count + Inject_GetDataCount(IDT_MESH_POINTERS));\n    Level_Section_AppendObjectMeshes(info->mesh_ptr_count, mesh_offsets, file);\n\n    VFile_SetPos(file, end_pos);\n    Memory_FreePointer(&mesh_offsets);\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendObjectMeshes(\n    const int32_t num_offsets, const int32_t *const offsets, VFILE *const file)\n{\n#define L_ALIGN 2\n\n    // Savegames identify meshes by their file pointer values divided by 2.\n    // (Historically, meshes were stored in int16_t[] arrays, so the so-called\n    // \"pointers\" are really just array indices into that layout.)\n    //\n    // Original level meshes work fine under this scheme, but injected meshes\n    // are different, as they come from separate VFiles and bring their own\n    // pointer values. To prevent conflicts, calling\n    // Level_Section_AppendObjectMeshes() for injected content must assign\n    // unique pseudo-pointers.\n    //\n    // Rules for injected meshes:\n    // - Pointers do not need to match real file offsets.\n    // - They only need to be unique and preserve ordering.\n    //\n    // Only the original level data requires true offset congruence so that old\n    // savegames remain compatible. For everything else, simple linear indexing\n    // is sufficient.\n    int32_t base_index = 0;\n    if (Object_GetMeshCount() > 0) {\n        // NOTE(Dash): Not assuming offsets are strictly increasing, so we scan\n        // all meshes and pick the max.\n        for (int32_t i = 0; i < Object_GetMeshCount(); i++) {\n            base_index =\n                MAX(base_index, Object_GetMeshOffset(Object_GetMesh(i)));\n        }\n        base_index += L_ALIGN;\n    }\n\n    // Construct and store distinct meshes only e.g. Lara's hips are referenced\n    // by several pointers as a dummy mesh.\n    VECTOR *const unique_offsets =\n        Vector_CreateAtCapacity(sizeof(int32_t), num_offsets);\n    int32_t pointer_map[num_offsets];\n    for (int32_t i = 0; i < num_offsets; i++) {\n        const int32_t pointer = offsets[i] + base_index;\n        const int32_t index = Vector_IndexOf(unique_offsets, (void *)&pointer);\n        if (index == -1) {\n            pointer_map[i] = unique_offsets->count;\n            Vector_Add(unique_offsets, (void *)&pointer);\n        } else {\n            pointer_map[i] = index;\n        }\n    }\n\n    OBJECT_MESH *const meshes =\n        GameBuf_Alloc(sizeof(OBJECT_MESH) * unique_offsets->count, GBUF_MESHES);\n    size_t start_pos = VFile_GetPos(file);\n    for (int32_t i = 0; i < unique_offsets->count; i++) {\n        const int32_t pointer = *(const int32_t *)Vector_Get(unique_offsets, i);\n        VFile_SetPos(file, start_pos + pointer - base_index);\n        M_ReadObjectMesh(&meshes[i], file);\n\n        // The original data position is required for backward compatibility\n        // with savegame files, specifically for Lara's mesh pointers.\n        Object_SetMeshOffset(&meshes[i], pointer / L_ALIGN);\n    }\n\n    for (int32_t i = 0; i < num_offsets; i++) {\n        Object_StoreMesh(&meshes[pointer_map[i]]);\n    }\n#undef L_ALIGN\n\n    LOG_INFO(\"%d unique meshes constructed\", unique_offsets->count);\n\n    Vector_Free(unique_offsets);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/objects.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/objects.h>\n#include <trx/game/shell.h>\n\nstatic void M_ReadPosition(XYZ_32 *const pos, VFILE *const file)\n{\n    pos->x = VFile_ReadS32(file);\n    pos->y = VFile_ReadS32(file);\n    pos->z = VFile_ReadS32(file);\n}\n\nstatic void M_ReadShade(\n    const LEVEL_FORMAT_LOADER *const loader, SHADE *const shade,\n    VFILE *const file)\n{\n    shade->value_1 = VFile_ReadS16(file);\n    if (loader->game_version == 1) {\n        shade->value_2 = shade->value_1;\n    } else {\n        shade->value_2 = VFile_ReadS16(file);\n    }\n}\n\nstatic void M_ReadBounds16(BOUNDS_16 *const bounds, VFILE *const file)\n{\n    bounds->min.x = VFile_ReadS16(file);\n    bounds->max.x = VFile_ReadS16(file);\n    bounds->min.y = VFile_ReadS16(file);\n    bounds->max.y = VFile_ReadS16(file);\n    bounds->min.z = VFile_ReadS16(file);\n    bounds->max.z = VFile_ReadS16(file);\n}\n\nvoid Level_Section_ReadObjects(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    const int32_t num_objects = VFile_ReadS32(file);\n    LOG_INFO(\"objects: %d\", num_objects);\n    for (int32_t i = 0; i < num_objects; i++) {\n        OBJECT fallback_obj = {};\n        const int32_t game_obj_id = VFile_ReadS32(file);\n        OBJECT *obj = Object_GetByGameID(game_obj_id);\n        if (obj == nullptr) {\n            if (loader->game_version == 3) {\n                // TODO: remove this check after we implement the items\n                obj = &fallback_obj;\n            } else {\n                Shell_ExitSystemFmt(\"Invalid object ID: %d\", game_obj_id);\n            }\n        }\n        obj->mesh_count = VFile_ReadS16(file);\n        obj->mesh_idx = VFile_ReadS16(file);\n        obj->bone_idx = VFile_ReadS32(file) / ANIM_BONE_SIZE;\n        obj->frame_ofs = VFile_ReadU32(file);\n        obj->frame_base = nullptr;\n        obj->anim_idx = VFile_ReadS16(file);\n        obj->loaded = true;\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadStaticObjects(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_objects = VFile_ReadS32(file);\n    LOG_INFO(\"static objects: %d\", num_objects);\n\n    typedef struct {\n        int32_t static_id;\n        int16_t mesh_idx;\n        BOUNDS_16 draw_bounds;\n        BOUNDS_16 collision_bounds;\n        uint16_t flags;\n    } M_STATIC_OBJ_3D_TEMP;\n\n    M_STATIC_OBJ_3D_TEMP *tmp_statics =\n        Memory_Alloc(sizeof(M_STATIC_OBJ_3D_TEMP) * num_objects);\n\n    int32_t max_static_id = -1;\n    for (int32_t i = 0; i < num_objects; i++) {\n        tmp_statics[i].static_id = VFile_ReadS32(file);\n        if (tmp_statics[i].static_id < 0) {\n            Shell_ExitSystemFmt(\n                \"Invalid static ID: %d\", tmp_statics[i].static_id);\n        }\n        max_static_id = MAX(max_static_id, tmp_statics[i].static_id);\n\n        tmp_statics[i].mesh_idx = VFile_ReadS16(file);\n        M_ReadBounds16(&tmp_statics[i].draw_bounds, file);\n        M_ReadBounds16(&tmp_statics[i].collision_bounds, file);\n        tmp_statics[i].flags = VFile_ReadU16(file);\n    }\n\n    LOG_INFO(\"max static id: %d\", max_static_id);\n    int32_t injection_max_id = Inject_GetMaxStaticObject3DId();\n    if (injection_max_id < 0) {\n        injection_max_id = -1;\n    }\n    const int32_t capacity = MAX(max_static_id, injection_max_id) + 1;\n    Object_InitialiseStaticObjects3D(capacity);\n\n    for (int32_t i = 0; i < num_objects; i++) {\n        STATIC_OBJECT_3D *const obj =\n            Object_Get3DStatic(tmp_statics[i].static_id);\n        obj->mesh_idx = tmp_statics[i].mesh_idx;\n        obj->loaded = true;\n        obj->draw_bounds = tmp_statics[i].draw_bounds;\n        obj->collision_bounds = tmp_statics[i].collision_bounds;\n\n        obj->collidable = (tmp_statics[i].flags & 1) == 0;\n        obj->visible = (tmp_statics[i].flags & 2) != 0;\n\n        Object_GetMesh(obj->mesh_idx)->enable_caustics = obj->visible;\n    }\n\n    Memory_FreePointer(&tmp_statics);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadSpriteSequences(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_sequences = VFile_ReadS32(file);\n    LOG_DEBUG(\"sprite sequences: %d\", num_sequences);\n\n    int32_t injection_max_id = Inject_GetMaxStaticObject2DId();\n    if (injection_max_id < 0) {\n        injection_max_id = -1;\n    }\n    const int32_t capacity = MAX(num_sequences - 1, injection_max_id) + 1;\n    Object_InitialiseStaticObjects2D(capacity);\n\n    int32_t static_id = 0;\n    for (int32_t i = 0; i < num_sequences; i++) {\n        const int32_t id = VFile_ReadS32(file);\n        const int16_t num_meshes = VFile_ReadS16(file);\n        const int16_t mesh_idx = VFile_ReadS16(file);\n\n        // In OG, a sprite was determined as either a game or static type based\n        // on the original total game object count. As IDs are freely assignable\n        // in TRX, a defined list of game sprites must instead be referred to.\n        const OBJECT_ID object_id = Object_FromGameID(id);\n        if (object_id != NO_OBJECT\n            && Object_IsType(object_id, g_GameSpriteObjects)) {\n            OBJECT *const obj = Object_Get(object_id);\n            obj->mesh_count = num_meshes;\n            obj->mesh_idx = mesh_idx;\n            obj->anim_idx = NO_ANIM;\n            obj->loaded = true;\n        } else {\n            STATIC_OBJECT_2D *const obj = Object_Get2DStatic(static_id);\n            if (obj == nullptr) {\n                Shell_ExitSystemFmt(\"Invalid sprite slot (%d)\", id);\n                break;\n            }\n            obj->frame_count = ABS(num_meshes);\n            obj->texture_idx = mesh_idx;\n            obj->loaded = true;\n            static_id++;\n        }\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadItems(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    const int32_t num_items = VFile_ReadS32(file);\n    LOG_INFO(\"items: %d\", num_items);\n    if (num_items > MAX_ITEMS) {\n        Shell_ExitSystem(\"Too many items\");\n        goto finish;\n    }\n\n    Item_InitialiseItems(num_items);\n    for (int32_t i = 0; i < num_items; i++) {\n        ITEM *const item = Item_Get(i);\n        const int16_t obj_id = VFile_ReadS16(file);\n        item->object_id = Object_FromGameID(obj_id);\n        if (item->object_id == NO_OBJECT) {\n            if (loader->game_version == 3) {\n                // TODO: remove this check after we implement the items\n                LOG_ERROR(\"Unsupported object #%d\", obj_id);\n                item->object_id = O_DUMMY;\n            } else {\n                Shell_ExitSystemFmt(\n                    \"Bad object number (%d) on item %d\", obj_id, i);\n                goto finish;\n            }\n        }\n\n        item->room_num = VFile_ReadS16(file);\n        M_ReadPosition(&item->pos, file);\n        item->rot.y = VFile_ReadS16(file);\n        M_ReadShade(loader, &item->shade, file);\n        item->flags = VFile_ReadS16(file);\n    }\n\nfinish:\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/pathing.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/game/const.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/pathing.h>\n\n#include <string.h>\n\nvoid Level_Section_ReadPathingData(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    const int32_t num_boxes = VFile_ReadS32(file);\n    Box_InitialiseBoxes(num_boxes);\n    for (int32_t i = 0; i < num_boxes; i++) {\n        BOX_INFO *const box = Box_GetBox(i);\n        if (loader->game_version == 1) {\n            box->left = VFile_ReadS32(file);\n            box->right = VFile_ReadS32(file);\n            box->top = VFile_ReadS32(file);\n            box->bottom = VFile_ReadS32(file);\n        } else {\n            box->left = VFile_ReadU8(file) << WALL_SHIFT;\n            box->right = (VFile_ReadU8(file) << WALL_SHIFT) - 1;\n            box->top = VFile_ReadU8(file) << WALL_SHIFT;\n            box->bottom = (VFile_ReadU8(file) << WALL_SHIFT) - 1;\n        }\n        box->height = VFile_ReadS16(file);\n        box->overlap_index = VFile_ReadS16(file);\n        if (loader->game_version == 3\n            && (box->overlap_index & BOX_BLOCKABLE) != 0) {\n            box->overlap_index |= BOX_BLOCKED;\n        }\n    }\n\n    const int32_t num_overlaps = VFile_ReadS32(file);\n    int16_t *const overlaps = Box_InitialiseOverlaps(num_overlaps);\n    VFile_Read(file, overlaps, sizeof(int16_t) * num_overlaps);\n\n    for (int32_t flip_status = 0; flip_status < 2; flip_status++) {\n        for (int32_t zone_idx = 0; zone_idx < Box_GetZoneCount(); zone_idx++) {\n            int16_t *const ground_zone =\n                Box_GetGroundZone(flip_status, zone_idx);\n            VFile_Read(file, ground_zone, sizeof(int16_t) * num_boxes);\n\n            if (loader->game_version == 1 && zone_idx == 1) {\n                // TODO: remove once TombEditor is updated to generate the same\n                // number of zones as TR2 via injections. This allows enemies of\n                // LOT_SETUP_CLIMBER type to safely be used in TR1 in the\n                // meantime.\n                int16_t *const duped_zone = Box_GetGroundZone(flip_status, 3);\n                memcpy(duped_zone, ground_zone, sizeof(int16_t) * num_boxes);\n            }\n        }\n\n        int16_t *const fly_zone = Box_GetFlyZone(flip_status);\n        VFile_Read(file, fly_zone, sizeof(int16_t) * num_boxes);\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/read.h",
    "content": "#pragma once\n\n#include <trx/core/virtual_file.h>\n#include <trx/game/level/context.h>\n\n#define ANIM_BONE_SIZE 4\n\nvoid Level_Section_ReadPalettes(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadTexturePages(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadRooms(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadObjectMeshes(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnims(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnimChanges(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnimRanges(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnimCommands(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnimBones(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnimFrames(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadObjects(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadStaticObjects(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadObjectTextures(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadSpriteTextures(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadSpriteSequences(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadPathingData(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadAnimatedTextureRanges(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadLightMap(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadCinematicFrames(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadCamerasAndSinks(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadItems(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadDemoData(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadSoundSources(LEVEL_CONTEXT *ctx, VFILE *file);\nvoid Level_Section_ReadSamples(LEVEL_CONTEXT *ctx, VFILE *file);\n"
  },
  {
    "path": "src/trx/game/level/sections/rooms.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/colors.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/output/bind.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/shell.h>\n#include <trx/game/viewport.h>\n\n#define M_NO_ROOM_LEGACY 255\n#define M_NO_BOX_TR3_LEGACY 0x7FF\n\nstatic void M_ReadPosition(XYZ_32 *const pos, VFILE *const file)\n{\n    pos->x = VFile_ReadS32(file);\n    pos->y = VFile_ReadS32(file);\n    pos->z = VFile_ReadS32(file);\n}\n\nstatic void M_ReadVertex(XYZ_16 *const vertex, VFILE *const file)\n{\n    vertex->x = VFile_ReadS16(file);\n    vertex->y = VFile_ReadS16(file);\n    vertex->z = VFile_ReadS16(file);\n}\n\nstatic void M_ReadShade(\n    const LEVEL_FORMAT_LOADER *const loader, SHADE *const shade,\n    VFILE *const file)\n{\n    shade->value_1 = VFile_ReadS16(file);\n    if (loader->game_version == 1) {\n        shade->value_2 = shade->value_1;\n    } else {\n        shade->value_2 = VFile_ReadS16(file);\n    }\n}\n\nstatic void M_ReadFace(\n    FACE *const face, const size_t vertex_count, VFILE *const file)\n{\n    face->vertex_count = vertex_count;\n    for (size_t i = 0; i < vertex_count; i++) {\n        face->vertices[i] = VFile_ReadU16(file);\n        face->texture_zw[i].z = 1.0f;\n        face->texture_zw[i].w = 1.0f;\n    }\n    const uint16_t texture_idx = VFile_ReadU16(file);\n    face->texture_idx = texture_idx & 0x7FFF;\n    face->double_sided = (texture_idx & 0x8000) != 0;\n    face->enable_reflections = false;\n}\n\nstatic void M_ReadRoomMesh(\n    const LEVEL_FORMAT_LOADER *const loader, const int32_t room_num,\n    VFILE *const file, const INJECTION_MESH_META inj_data)\n{\n    ROOM *const room = Room_Get(room_num);\n    const uint32_t mesh_length = VFile_ReadU32(file);\n    const size_t start_pos = VFile_GetPos(file);\n\n    {\n        room->mesh.num_vertices = VFile_ReadS16(file);\n        const int32_t alloc_count =\n            room->mesh.num_vertices + inj_data.num_vertices;\n        room->mesh.vertices =\n            GameBuf_Alloc(sizeof(ROOM_VERTEX) * alloc_count, GBUF_ROOM_MESH);\n        for (int32_t i = 0; i < room->mesh.num_vertices; i++) {\n            ROOM_VERTEX *const vertex = &room->mesh.vertices[i];\n            M_ReadVertex(&vertex->pos, file);\n            if (loader->game_version == 1) {\n                vertex->light_base = VFile_ReadS16(file);\n                vertex->flags.disable_wibble = false;\n                vertex->flags.move = false;\n                vertex->flags.glow = false;\n                vertex->color = COLOR_RGBA_8888_WHITE;\n            } else if (loader->game_version == 2) {\n                vertex->light_base = VFile_ReadS16(file);\n                vertex->light_table_value = VFile_ReadU8(file);\n                const uint8_t flags = VFile_ReadU8(file);\n                vertex->flags.disable_wibble = (flags & 0x80u) != 0u;\n                vertex->flags.move = false;\n                vertex->flags.glow = false;\n                VFile_Skip(file, 2);\n                vertex->color = COLOR_RGBA_8888_WHITE;\n            } else if (loader->game_version == 3) {\n                VFile_Skip(file, 2); // lighting - unused in TR3\n                const uint16_t flags = VFile_ReadU16(file);\n                vertex->flags.disable_wibble = (flags & 0x8000u) != 0u;\n                vertex->flags.move = (flags & 0x2000u) != 0u;\n                vertex->flags.glow = (flags & 0x4000u) != 0u;\n                vertex->color = Color_ARGB1555ToRGBA8888(VFile_ReadU16(file));\n                vertex->color.a = 255;\n                vertex->light_base = 0;\n            }\n        }\n    }\n\n    {\n        room->mesh.face4s.count = VFile_ReadS16(file);\n        const size_t pos = VFile_GetPos(file);\n        VFile_Skip(file, 10 * room->mesh.face4s.count);\n        room->mesh.face3s.count = VFile_ReadS16(file);\n        VFile_SetPos(file, pos);\n\n        room->mesh.all_faces.count = room->mesh.face4s.count\n            + inj_data.num_quads + room->mesh.face3s.count\n            + inj_data.num_triangles;\n        FACE *face_ptr = GameBuf_Alloc(\n            sizeof(FACE) * room->mesh.all_faces.count, GBUF_ROOM_MESH);\n\n        room->mesh.all_faces.data = face_ptr;\n        room->mesh.face4s.data = face_ptr;\n        for (int32_t i = 0; i < room->mesh.face4s.count; i++) {\n            M_ReadFace(face_ptr++, 4, file);\n        }\n        for (int32_t i = 0; i < inj_data.num_quads; i++) {\n            face_ptr->vertex_count = 4;\n            face_ptr++;\n        }\n\n        VFile_Skip(file, 2);\n\n        room->mesh.face3s.data = face_ptr;\n        for (int32_t i = 0; i < room->mesh.face3s.count; i++) {\n            M_ReadFace(face_ptr++, 3, file);\n        }\n        for (int32_t i = 0; i < inj_data.num_triangles; i++) {\n            face_ptr->vertex_count = 3;\n            face_ptr++;\n        }\n    }\n\n    {\n        room->mesh.sprites.count = VFile_ReadS16(file);\n        const int32_t alloc_count =\n            room->mesh.sprites.count + inj_data.num_static_2ds;\n        room->mesh.sprites.data =\n            GameBuf_Alloc(sizeof(ROOM_SPRITE) * alloc_count, GBUF_ROOM_MESH);\n        for (int32_t i = 0; i < room->mesh.sprites.count; i++) {\n            ROOM_SPRITE *const sprite = &room->mesh.sprites.data[i];\n            sprite->vertex = VFile_ReadU16(file);\n            sprite->texture = VFile_ReadU16(file);\n        }\n    }\n\n    const size_t total_read =\n        (VFile_GetPos(file) - start_pos) / sizeof(int16_t);\n    ASSERT(total_read == mesh_length);\n}\n\nstatic XYZ_16 M_ComputePortalNormal(PORTAL *const p)\n{\n    // This fixes a bug in TombEditor where certain portals would get emitted\n    // with wrong normals. TE is guaranteed to emit normals with a good sign in\n    // the Y component, but for sloped ceiling portals, their X and Z\n    // compontents have the wrong sign.\n    //\n    // To fix this, we compute the normal the regular way. We don't know which\n    // way the portal faces, but since the Y component is guaranteed to be\n    // good, we can orient our vector using this information, which should fix\n    // the X/Z components.\n\n    ASSERT(p != nullptr);\n\n    // Geometric normal (ab x ac)\n    const XYZ_32 a = { p->vertex[0].x, p->vertex[0].y, p->vertex[0].z };\n    const XYZ_32 b = { p->vertex[1].x, p->vertex[1].y, p->vertex[1].z };\n    const XYZ_32 c = { p->vertex[2].x, p->vertex[2].y, p->vertex[2].z };\n    const XYZ_32 ab = { b.x - a.x, b.y - a.y, b.z - a.z };\n    const XYZ_32 ac = { c.x - a.x, c.y - a.y, c.z - a.z };\n    XYZ_32 n = {\n        (ab.y * ac.z) - (ab.z * ac.y),\n        (ab.z * ac.x) - (ab.x * ac.z),\n        (ab.x * ac.y) - (ab.y * ac.x),\n    };\n\n    // Degenerate guard\n    if (n.x == 0 && n.y == 0 && n.z == 0) {\n        return (XYZ_16) { .x = 0, .y = 1, .z = 0 };\n    }\n\n    // Integer normalization\n    const int32_t gx = ABS(n.x);\n    const int32_t gy = ABS(n.y);\n    const int32_t gz = ABS(n.z);\n    int32_t g = gx;\n    if (gy != 0) {\n        g = Math_GCD(g, gy);\n    }\n    if (gz != 0) {\n        g = Math_GCD(g, gz);\n    }\n    if (g == 0) {\n        g = 1;\n    }\n    n.x /= g;\n    n.y /= g;\n    n.z /= g;\n\n    // NOTE: we only care about horizontal portals.\n    if (p->normal.y == 0) {\n        return p->normal;\n    }\n    if (p->normal.y != n.y) {\n        n.x *= -1;\n        n.y *= -1;\n        n.z *= -1;\n    }\n    return (XYZ_16) { n.x, n.y, n.z };\n}\n\nvoid Level_Section_ReadRooms(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n\n    const int32_t num_rooms = VFile_ReadS16(file);\n    LOG_INFO(\"rooms: %d\", num_rooms);\n    if (num_rooms > MAX_ROOMS) {\n        Shell_ExitSystem(\"Too many rooms\");\n        goto finish;\n    }\n\n    Room_InitialiseRooms(num_rooms);\n    for (int32_t i = 0; i < num_rooms; i++) {\n        ROOM *const room = Room_Get(i);\n\n        room->pos.x = VFile_ReadS32(file);\n        room->pos.y = 0;\n        room->pos.z = VFile_ReadS32(file);\n\n        room->min_floor = VFile_ReadS32(file);\n        room->max_ceiling = VFile_ReadS32(file);\n\n        const INJECTION_MESH_META inj_data = Inject_GetRoomMeshMeta(i);\n        M_ReadRoomMesh(loader, i, file, inj_data);\n\n        const int16_t num_portals = VFile_ReadS16(file);\n        if (num_portals <= 0) {\n            room->portals = nullptr;\n        } else {\n            room->portals = GameBuf_Alloc(\n                sizeof(PORTAL) * num_portals + sizeof(PORTALS),\n                GBUF_ROOM_PORTALS);\n            room->portals->count = num_portals;\n            for (int32_t j = 0; j < num_portals; j++) {\n                PORTAL *const portal = &room->portals->portal[j];\n                portal->room_num = VFile_ReadS16(file);\n                M_ReadVertex(&portal->normal, file);\n                for (int32_t k = 0; k < 4; k++) {\n                    M_ReadVertex(&portal->vertex[k], file);\n                }\n            }\n        }\n\n        room->size.z = VFile_ReadS16(file);\n        room->size.x = VFile_ReadS16(file);\n\n        const int32_t sector_count = room->size.x * room->size.z;\n        room->sectors =\n            GameBuf_Alloc(sizeof(SECTOR) * sector_count, GBUF_ROOM_SECTORS);\n        for (int32_t j = 0; j < sector_count; j++) {\n            SECTOR *const sector = &room->sectors[j];\n            sector->idx = VFile_ReadU16(file);\n            if (loader->game_version == 3) {\n                uint16_t misc_info = VFile_ReadU16(file);\n                sector->fx = (uint8_t)(misc_info & 0x0F);\n                sector->box = (int16_t)((misc_info & 0x7FF0) >> 4);\n                sector->stopper = (bool)((misc_info & 0x8000) >> 15);\n                if (sector->box == M_NO_BOX_TR3_LEGACY) {\n                    sector->box = NO_BOX;\n                }\n            } else {\n                sector->fx = 0;\n                sector->box = VFile_ReadS16(file);\n                sector->stopper = false;\n            }\n            sector->portal_room.pit = VFile_ReadU8(file);\n            sector->floor.height = VFile_ReadS8(file) * STEP_L;\n            sector->portal_room.sky = VFile_ReadU8(file);\n            sector->ceiling.height = VFile_ReadS8(file) * STEP_L;\n            if (sector->portal_room.pit == M_NO_ROOM_LEGACY) {\n                sector->portal_room.pit = NO_ROOM;\n            }\n            if (sector->portal_room.sky == M_NO_ROOM_LEGACY) {\n                sector->portal_room.sky = NO_ROOM;\n            }\n        }\n\n        room->ambient = VFile_ReadS16(file);\n        if (loader->game_version == 1) {\n            room->light_mode = RLM_NORMAL;\n        } else if (loader->game_version == 2) {\n            VFile_Skip(file, sizeof(int16_t)); // Unused second ambient\n            room->light_mode = VFile_ReadS16(file);\n        } else {\n            room->light_mode = VFile_ReadS16(file);\n        }\n\n        room->num_lights = VFile_ReadS16(file);\n        room->lights = room->num_lights == 0\n            ? nullptr\n            : GameBuf_Alloc(sizeof(LIGHT) * room->num_lights, GBUF_ROOM_LIGHTS);\n        for (int32_t j = 0; j < room->num_lights; j++) {\n            LIGHT *const light = &room->lights[j];\n            if (loader->game_version == 3) {\n                // TR3 room lights use the LIGHT_INFO struct layout:\n                // pos (s32*3) + rgb (u8*3) + type (u8) + union (8 bytes).\n                M_ReadPosition(&light->pos, file);\n                light->color.r = VFile_ReadU8(file);\n                light->color.g = VFile_ReadU8(file);\n                light->color.b = VFile_ReadU8(file);\n                light->type = VFile_ReadU8(file);\n                if (light->type != 0u) {\n                    light->dir.x = VFile_ReadS16(file);\n                    light->dir.y = VFile_ReadS16(file);\n                    light->dir.z = VFile_ReadS16(file);\n                    VFile_Skip(file, sizeof(int16_t)); // pad\n                    light->shade.value_1 = 0;\n                    light->shade.value_2 = 0;\n                    light->falloff.value_1 = 0;\n                    light->falloff.value_2 = 0;\n                } else {\n                    int32_t intensity = VFile_ReadS32(file);\n                    const int32_t falloff = VFile_ReadS32(file);\n                    CLAMP(intensity, INT16_MIN, INT16_MAX);\n                    light->shade.value_1 = (int16_t)intensity;\n                    light->shade.value_2 = (int16_t)intensity;\n                    light->falloff.value_1 = falloff;\n                    light->falloff.value_2 = falloff;\n                    light->dir = (XYZ_16) { 0, 0, 0 };\n                }\n            } else {\n                M_ReadPosition(&light->pos, file);\n                M_ReadShade(loader, &light->shade, file);\n                light->falloff.value_1 = VFile_ReadS32(file);\n                if (loader->game_version >= 2) {\n                    light->falloff.value_2 = VFile_ReadS32(file);\n                } else {\n                    light->falloff.value_2 = light->falloff.value_1;\n                }\n                light->color = COLOR_RGB_888_WHITE;\n                light->type = 0;\n                light->dir = (XYZ_16) { 0, 0, 0 };\n            }\n        }\n\n        room->num_static_meshes = VFile_ReadS16(file);\n        const int32_t static_count =\n            room->num_static_meshes + inj_data.num_static_3ds;\n        room->static_meshes = static_count == 0\n            ? nullptr\n            : GameBuf_Alloc(\n                  sizeof(STATIC_MESH) * static_count, GBUF_ROOM_STATIC_MESHES);\n        for (int32_t j = 0; j < room->num_static_meshes; j++) {\n            STATIC_MESH *const mesh = &room->static_meshes[j];\n            M_ReadPosition(&mesh->pos, file);\n            mesh->rot.y = VFile_ReadS16(file);\n            M_ReadShade(loader, &mesh->shade, file);\n            mesh->static_num = VFile_ReadS16(file);\n            mesh->draw_num = -1;\n        }\n\n        room->flipped_room = VFile_ReadS16(file);\n\n        const uint16_t flags = VFile_ReadU16(file);\n        // clang-format off\n        room->flags.underwater  = (flags & 0x01) != 0;\n        room->flags.outside     = (flags & 0x08) != 0;\n        room->flags.dynamic_lit = (flags & 0x10) != 0;\n        room->flags.wind        = (flags & 0x20) != 0;\n        room->flags.inside      = (flags & 0x40) != 0;\n        room->flags.swamp       = (flags & 0x80) != 0;\n        // clang-format on\n\n        OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room);\n        bind->bound_left = Viewport_GetMaxX(VIEWPORT_GAME);\n        bind->bound_top = Viewport_GetMaxY(VIEWPORT_GAME);\n        bind->bound_bottom = Viewport_GetMinY(VIEWPORT_GAME);\n        bind->bound_right = Viewport_GetMinX(VIEWPORT_GAME);\n        room->item_num = NO_ITEM;\n        room->effect_num = NO_EFFECT;\n\n        if (loader->game_version == 3) {\n            room->water_scheme = VFile_ReadU8(file);\n            room->reverb_info = VFile_ReadU8(file);\n            VFile_Skip(file, 1);\n        }\n    }\n\n    for (int32_t i = 0; i < num_rooms; i++) {\n        ROOM *const room = Room_Get(i);\n        if (room->portals == nullptr) {\n            continue;\n        }\n        for (int32_t j = 0; j < room->portals->count; j++) {\n            PORTAL *const portal = &room->portals->portal[j];\n            const XYZ_16 new_normal = M_ComputePortalNormal(portal);\n            if (new_normal.x != portal->normal.x\n                || new_normal.y != portal->normal.y\n                || new_normal.z != portal->normal.z) {\n                LOG_WARNING(\"Fixed room %d, portal normal %d\", i, j);\n                portal->normal = new_normal;\n            }\n        }\n    }\n\n    Room_InitialiseFlipStatus();\n\n    const int32_t floor_data_size = VFile_ReadS32(file);\n    int16_t *floor_data = Memory_Alloc(sizeof(int16_t) * floor_data_size);\n    VFile_Read(file, floor_data, sizeof(int16_t) * floor_data_size);\n\n    Room_ParseFloorData(floor_data);\n    Memory_FreePointer(&floor_data);\n\n    Room_BuildOutsideTable();\n\nfinish:\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/sections/textures.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/colors.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/thread_pool.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/inject.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/sections/append.h>\n#include <trx/game/level/sections/read.h>\n#include <trx/game/output.h>\n\ntypedef struct {\n    const RGB_888 *palette;\n    const uint8_t *input_8_page;\n    const uint16_t *input_16_page;\n    RGBA_8888 *output_32_page;\n} M_TEXTURE_PAGE_DECODE_JOB;\n\nstatic void M_DecodeTR3ObjectTextureUVs(OBJECT_TEXTURE *const texture)\n{\n    int16_t *const uv = (int16_t *)&texture->uv[0].u;\n    uint8_t flags = 0;\n\n    for (int32_t i = 0; i < 8; i++) {\n        if ((uv[i] & 0x80) != 0) {\n            uv[i] |= 0x00FF;\n            flags |= 1 << i;\n        } else {\n            uv[i] &= 0xFF00;\n        }\n    }\n\n    for (int32_t i = 0; i < 8; i++) {\n        if ((flags & 1) != 0) {\n            uv[i] -= 256;\n        } else {\n            uv[i] += 256;\n        }\n        flags >>= 1;\n    }\n}\n\nstatic void M_Decode8BitTexturePage(void *const userdata)\n{\n    const M_TEXTURE_PAGE_DECODE_JOB *const job = userdata;\n    const uint8_t *input = job->input_8_page;\n    RGBA_8888 *output = job->output_32_page;\n\n    for (int32_t i = 0; i < TEXTURE_PAGE_SIZE; i++) {\n        const uint8_t index = *input++;\n        const RGB_888 pix = job->palette[index];\n        output->r = pix.r;\n        output->g = pix.g;\n        output->b = pix.b;\n        output->a = index == 0 ? 0 : 0xFF;\n        output++;\n    }\n}\n\nstatic void M_Decode16BitTexturePage(void *const userdata)\n{\n    const M_TEXTURE_PAGE_DECODE_JOB *const job = userdata;\n    const uint16_t *input = job->input_16_page;\n    RGBA_8888 *output = job->output_32_page;\n\n    for (int32_t i = 0; i < TEXTURE_PAGE_SIZE; i++) {\n        *output++ = Color_ARGB1555ToRGBA8888(*input++);\n    }\n}\n\nvoid Level_Section_ReadPalettes(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n\n    const int32_t palette_size = 256;\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->palette.size = palette_size;\n\n    info->palette.data_24 = Memory_Alloc(sizeof(RGB_888) * palette_size);\n    VFile_Read(file, info->palette.data_24, sizeof(RGB_888) * palette_size);\n    info->palette.data_24[0].r = 0;\n    info->palette.data_24[0].g = 0;\n    info->palette.data_24[0].b = 0;\n    for (int32_t i = 1; i < palette_size; i++) {\n        RGB_888 *const col = &info->palette.data_24[i];\n        col->r = (col->r << 2) | (col->r >> 4);\n        col->g = (col->g << 2) | (col->g >> 4);\n        col->b = (col->b << 2) | (col->b >> 4);\n    }\n\n    if (loader->game_version == 1) {\n        info->palette.data_32 = nullptr;\n    } else {\n        RGBA_8888 palette_16[palette_size];\n        info->palette.data_32 = Memory_Alloc(sizeof(RGB_888) * palette_size);\n        VFile_Read(file, palette_16, sizeof(RGBA_8888) * palette_size);\n        for (int32_t i = 0; i < palette_size; i++) {\n            info->palette.data_32[i].r = palette_16[i].r;\n            info->palette.data_32[i].g = palette_16[i].g;\n            info->palette.data_32[i].b = palette_16[i].b;\n        }\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadTexturePages(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n\n    const int32_t num_pages = VFile_ReadS32(file);\n    const LEVEL_FORMAT_LOADER *const loader = ctx->loader;\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->textures.page_count = num_pages;\n    LOG_INFO(\"texture pages: %d\", num_pages);\n\n    const int32_t extra_pages = Inject_GetDataCount(IDT_TEXTURE_PAGES);\n    const int32_t texture_size_8_bit =\n        (num_pages + extra_pages) * TEXTURE_PAGE_SIZE * sizeof(uint8_t);\n    const int32_t texture_size_32_bit =\n        (num_pages + extra_pages) * TEXTURE_PAGE_SIZE * sizeof(RGBA_8888);\n\n    info->textures.pages_8 = Memory_Alloc(texture_size_8_bit);\n    VFile_Read(file, info->textures.pages_8, num_pages * TEXTURE_PAGE_SIZE);\n\n    info->textures.pages_32 = Memory_Alloc(texture_size_32_bit);\n\n    THREAD_POOL *const pool = ThreadPool_Create(-1);\n    M_TEXTURE_PAGE_DECODE_JOB *const jobs =\n        Memory_Alloc(sizeof(*jobs) * num_pages);\n    uint16_t *input_16 = nullptr;\n    for (int32_t i = 0; i < num_pages; i++) {\n        jobs[i].palette = info->palette.data_24;\n        jobs[i].input_8_page = &info->textures.pages_8[i * TEXTURE_PAGE_SIZE];\n        jobs[i].input_16_page = nullptr;\n        jobs[i].output_32_page =\n            &info->textures.pages_32[i * TEXTURE_PAGE_SIZE];\n    }\n\n    if (loader->game_version == 1) {\n        for (int32_t i = 0; i < num_pages; i++) {\n            ThreadPool_AddJob(pool, M_Decode8BitTexturePage, &jobs[i]);\n        }\n    } else {\n        const int32_t texture_size_16_bit =\n            num_pages * TEXTURE_PAGE_SIZE * sizeof(uint16_t);\n        input_16 = Memory_Alloc(texture_size_16_bit);\n        VFile_Read(file, input_16, texture_size_16_bit);\n        for (int32_t i = 0; i < num_pages; i++) {\n            jobs[i].input_16_page = &input_16[i * TEXTURE_PAGE_SIZE];\n            ThreadPool_AddJob(pool, M_Decode16BitTexturePage, &jobs[i]);\n        }\n    }\n\n    ThreadPool_Wait(pool);\n    Memory_FreePointer(&input_16);\n    Memory_Free(jobs);\n    ThreadPool_Destroy(pool);\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadObjectTextures(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_textures = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->textures.object_count = num_textures;\n    LOG_INFO(\"object textures: %d\", num_textures);\n    Output_InitialiseObjectTextures(\n        num_textures + Inject_GetDataCount(IDT_OBJECT_TEXTURES));\n    Level_Section_AppendObjectTextures(\n        0, 0, num_textures, file, ctx->loader->game_version >= 3);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendObjectTextures(\n    const int32_t base_idx, const int16_t base_page_idx,\n    const int32_t num_textures, VFILE *const file,\n    const bool use_tr3_adjustment)\n{\n    for (int32_t i = 0; i < num_textures; i++) {\n        OBJECT_TEXTURE *const texture = Output_GetObjectTexture(base_idx + i);\n        texture->uv_count = 4; // Default to 4 vertices\n        texture->draw_type = VFile_ReadU16(file);\n        texture->tex_page = VFile_ReadU16(file) + base_page_idx;\n        for (int32_t j = 0; j < 4; j++) {\n            texture->uv[j].u = VFile_ReadU16(file);\n            texture->uv[j].v = VFile_ReadU16(file);\n        }\n        if (use_tr3_adjustment) {\n            M_DecodeTR3ObjectTextureUVs(texture);\n        }\n    }\n}\n\nvoid Level_Section_ReadSpriteTextures(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t num_textures = VFile_ReadS32(file);\n    LEVEL_CONTEXT_INFO *const info = &ctx->info;\n    info->textures.sprite_count = num_textures;\n    LOG_INFO(\"sprite textures: %d\", num_textures);\n    Output_InitialiseSpriteTextures(\n        num_textures + Inject_GetDataCount(IDT_SPRITE_TEXTURES));\n    Level_Section_AppendSpriteTextures(0, 0, num_textures, file);\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_AppendSpriteTextures(\n    const int32_t base_idx, const int16_t base_page_idx,\n    const int32_t num_textures, VFILE *const file)\n{\n    for (int32_t i = 0; i < num_textures; i++) {\n        SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(base_idx + i);\n        sprite->tex_page = VFile_ReadU16(file) + base_page_idx;\n        sprite->offset = VFile_ReadU16(file);\n        sprite->width = VFile_ReadU16(file);\n        sprite->height = VFile_ReadU16(file);\n        sprite->x0 = VFile_ReadS16(file);\n        sprite->y0 = VFile_ReadS16(file);\n        sprite->x1 = VFile_ReadS16(file);\n        sprite->y1 = VFile_ReadS16(file);\n    }\n}\n\nvoid Level_Section_ReadAnimatedTextureRanges(\n    LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    const int32_t data_size = VFile_ReadS32(file);\n    const size_t end_position =\n        VFile_GetPos(file) + data_size * sizeof(int16_t);\n\n    const int16_t num_ranges = VFile_ReadS16(file);\n    LOG_INFO(\"animated texture ranges: %d\", num_ranges);\n    Output_InitialiseAnimatedTextures(num_ranges);\n\n    for (int32_t i = 0; i < num_ranges; i++) {\n        ANIMATED_TEXTURE_RANGE *const range = Output_GetAnimatedTextureRange(i);\n        range->next_range = i == num_ranges - 1\n            ? nullptr\n            : Output_GetAnimatedTextureRange(i + 1);\n\n        // Level data is tied to the original logic in Output_AnimateTextures\n        // and hence stores one less than the actual count here.\n        range->num_textures = VFile_ReadS16(file) + 1;\n        range->textures = GameBuf_Alloc(\n            sizeof(int16_t) * range->num_textures,\n            GBUF_ANIMATED_TEXTURE_RANGES);\n        VFile_Read(\n            file, range->textures, sizeof(int16_t) * range->num_textures);\n    }\n\n    VFile_SetPos(file, end_position);\n    Benchmark_End(&benchmark, nullptr);\n}\n\nvoid Level_Section_ReadLightMap(LEVEL_CONTEXT *const ctx, VFILE *const file)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    for (int32_t i = 0; i < 32; i++) {\n        LIGHT_MAP *const light_map = Output_GetLightMap(i);\n        VFile_Read(file, light_map->index, sizeof(uint8_t) * 256);\n        light_map->index[0] = 0;\n    }\n\n    for (int32_t i = 0; i < 32; i++) {\n        const LIGHT_MAP *const light_map = Output_GetLightMap(i);\n        for (int32_t j = 0; j < 256; j++) {\n            SHADE_MAP *const shade_map = Output_GetShadeMap(j);\n            shade_map->index[i] = light_map->index[j];\n        }\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/level/settings.c",
    "content": "#include <trx/game/level/settings.h>\n\n#include <trx/config.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_flow/vars.h>\n\nRGB_888 Level_GetWaterColor(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level != nullptr && level->settings.water_color.is_present) {\n        return level->settings.water_color.value;\n    }\n    return g_Config.visuals.water_color;\n}\n\nRGBA_8888 Level_GetFogColor(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    RGB_888 color = { 0, 0, 0 };\n    uint8_t alpha = 255;\n    if (level != nullptr && level->settings.fog_transparency.is_present\n        && level->settings.fog_transparency.value) {\n        alpha = 0;\n    } else if (level != nullptr && level->settings.fog_color.is_present) {\n        color = level->settings.fog_color.value;\n    } else if (g_Config.visuals.fog_transparency) {\n        alpha = 0;\n    } else {\n        color = g_Config.visuals.fog_color;\n    }\n    return (RGBA_8888) { .r = color.r, .g = color.g, .b = color.b, .a = alpha };\n}\n\nfloat Level_GetFogStart(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level != nullptr && level->settings.fog_start.is_present) {\n        return level->settings.fog_start.value;\n    }\n    return g_Config.visuals.fog_start;\n}\n\nfloat Level_GetFogEnd(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level != nullptr && level->settings.fog_end.is_present) {\n        return level->settings.fog_end.value;\n    }\n    return g_Config.visuals.fog_end;\n}\n\nbool Level_HasColdWater(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level != nullptr && level->settings.cold_water.is_present) {\n        return level->settings.cold_water.value;\n    }\n\n    if (g_GameFlow.settings.cold_water.is_present) {\n        return g_GameFlow.settings.cold_water.value;\n    }\n\n    return false;\n}\n\nGF_DEATH_TILE Level_GetDeathTile(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level != nullptr && level->settings.death_tile.is_present) {\n        return level->settings.death_tile.value;\n    }\n\n    if (g_GameFlow.settings.death_tile.is_present) {\n        return g_GameFlow.settings.death_tile.value;\n    }\n\n    return GF_DEATH_TILE_LAVA;\n}\n"
  },
  {
    "path": "src/trx/game/level/settings.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/game/game_flow/types.h>\n\nRGB_888 Level_GetWaterColor(void);\nRGBA_8888 Level_GetFogColor(void);\nfloat Level_GetFogStart(void);\nfloat Level_GetFogEnd(void);\nbool Level_HasColdWater(void);\nGF_DEATH_TILE Level_GetDeathTile(void);\n"
  },
  {
    "path": "src/trx/game/level.h",
    "content": "#pragma once\n\n#include <trx/game/level/common.h>\n#include <trx/game/level/context.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/level/pipeline.h>\n#include <trx/game/level/settings.h>\n"
  },
  {
    "path": "src/trx/game/los.h",
    "content": "#pragma once\n\n#include <trx/game/collision/los.h>\n"
  },
  {
    "path": "src/trx/game/lua/assault_stats.c",
    "content": "#include <trx/config.h>\n#include <trx/config/types.h>\n#include <trx/game/const.h>\n#include <trx/game/gym.h>\n#include <trx/game/lua/common.h>\n\n#include <lauxlib.h>\n#include <stdint.h>\n\nstatic bool M_StoreAssaultTime(const float time)\n{\n    GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats;\n    uint32_t logic_time = (uint32_t)(time * LOGIC_FPS);\n    int32_t insert_idx = -1;\n\n    for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) {\n        if (assault->entries[i].time == 0\n            || logic_time < assault->entries[i].time) {\n            insert_idx = i;\n            break;\n        }\n    }\n    if (insert_idx == -1) {\n        return false;\n    }\n\n    for (int32_t i = MAX_ASSAULT_TIMES - 1; i > insert_idx; i--) {\n        assault->entries[i] = assault->entries[i - 1];\n    }\n\n    assault->total_attempts++;\n    assault->entries[insert_idx].time = logic_time;\n    assault->entries[insert_idx].attempt_num = assault->total_attempts;\n    Config_Update();\n    return true;\n}\n\nstatic bool M_RemoveAssaultTimeAtIndex(const int32_t idx)\n{\n    GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats;\n    if (idx < 0 || idx >= MAX_ASSAULT_TIMES) {\n        return false;\n    }\n    if (assault->entries[idx].time == 0) {\n        return false;\n    }\n\n    for (int32_t i = idx; i < MAX_ASSAULT_TIMES - 1; i++) {\n        assault->entries[i] = assault->entries[i + 1];\n    }\n\n    assault->entries[MAX_ASSAULT_TIMES - 1].time = 0;\n    assault->entries[MAX_ASSAULT_TIMES - 1].attempt_num = 0;\n    Config_Update();\n    return true;\n}\n\n// trxc.assault_stats.record(time) -> bool\nstatic int M_L_AssaultStatsRecord(lua_State *const L)\n{\n    if (!Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) {\n        return luaL_error(L, \"Assault stats unavailable\");\n    }\n    const float time = (float)luaL_checknumber(L, 1);\n    if (time <= 0.0f) {\n        return luaL_error(L, \"Time must be > 0\");\n    }\n\n    const bool ok = M_StoreAssaultTime(time);\n    lua_pushboolean(L, ok);\n    return 1;\n}\n\n// trxc.assault_stats.remove(index) -> bool\nstatic int M_L_AssaultStatsRemove(lua_State *const L)\n{\n    if (!Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) {\n        return luaL_error(L, \"Assault stats unavailable\");\n    }\n    const int64_t index_1 = luaL_checkinteger(L, 1);\n    if (index_1 < 1 || index_1 > MAX_ASSAULT_TIMES) {\n        return luaL_error(\n            L, \"Index out of range: %lld (expected 1..%d)\", (long long)index_1,\n            MAX_ASSAULT_TIMES);\n    }\n\n    const bool ok = M_RemoveAssaultTimeAtIndex(index_1 - 1);\n    lua_pushboolean(L, ok);\n    return 1;\n}\n\n// trxc.assault_stats.list() -> { { time = float, attempt_num = int }, ... }\nstatic int M_L_AssaultStatsList(lua_State *const L)\n{\n    if (!Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)) {\n        return luaL_error(L, \"Assault stats unavailable\");\n    }\n\n    const GYM_TRACK_STATS *const assault = &g_Config.profile.assault_stats;\n    lua_newtable(L);\n    int32_t out_idx = 1;\n\n    for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) {\n        if (assault->entries[i].time == 0) {\n            break;\n        }\n\n        lua_newtable(L);\n        lua_pushnumber(\n            L, (lua_Number)((float)assault->entries[i].time / LOGIC_FPS));\n        lua_setfield(L, -2, \"time\");\n        lua_pushinteger(L, (lua_Integer)assault->entries[i].attempt_num);\n        lua_setfield(L, -2, \"attempt_num\");\n        lua_seti(L, -2, out_idx);\n        out_idx++;\n    }\n\n    return 1;\n}\n\nvoid LUA_CreateAssaultStats(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_pushcfunction(L, M_L_AssaultStatsRecord);\n    lua_setfield(L, -2, \"record\");\n    lua_pushcfunction(L, M_L_AssaultStatsRemove);\n    lua_setfield(L, -2, \"remove\");\n    lua_pushcfunction(L, M_L_AssaultStatsList);\n    lua_setfield(L, -2, \"list\");\n\n    lua_setfield(L, -2, \"assault_stats\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/camera.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/lua/common.h>\n#include <trx/game/rooms/const.h>\n\n#include <lauxlib.h>\n\n// trxc.camera.get_pos() → {x, y, z}\nstatic int M_L_CameraGetPos(lua_State *const L)\n{\n    lua_newtable(L);\n    lua_pushinteger(L, g_Camera.pos.x);\n    lua_setfield(L, -2, \"x\");\n    lua_pushinteger(L, g_Camera.pos.y);\n    lua_setfield(L, -2, \"y\");\n    lua_pushinteger(L, g_Camera.pos.z);\n    lua_setfield(L, -2, \"z\");\n    return 1;\n}\n\n// trxc.camera.get_room() → int (1-based) or nil\nstatic int M_L_CameraGetRoom(lua_State *const L)\n{\n    if (g_Camera.pos.room_num == NO_ROOM) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, g_Camera.pos.room_num + 1);\n    }\n    return 1;\n}\n\n// trxc.camera.get_target_pos() → {x, y, z}\nstatic int M_L_CameraGetTargetPos(lua_State *const L)\n{\n    lua_newtable(L);\n    lua_pushinteger(L, g_Camera.target.x);\n    lua_setfield(L, -2, \"x\");\n    lua_pushinteger(L, g_Camera.target.y);\n    lua_setfield(L, -2, \"y\");\n    lua_pushinteger(L, g_Camera.target.z);\n    lua_setfield(L, -2, \"z\");\n    return 1;\n}\n\n// trxc.camera.get_target_room() → int (1-based) or nil\nstatic int M_L_CameraGetTargetRoom(lua_State *const L)\n{\n    if (g_Camera.target.room_num == NO_ROOM) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, g_Camera.target.room_num + 1);\n    }\n    return 1;\n}\n\n// trxc.camera.shake(intensity)\nstatic int M_L_CameraShake(lua_State *const L)\n{\n    g_Camera.bounce = (int32_t)luaL_checkinteger(L, 1);\n    return 0;\n}\n\n// trxc.camera.reset()\nstatic int M_L_CameraReset(lua_State *const L)\n{\n    Camera_ResetPosition();\n    return 0;\n}\n\nvoid LUA_CreateCamera(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_CameraGetPos);\n    lua_setfield(L, -2, \"get_pos\");\n    lua_pushcfunction(L, M_L_CameraGetRoom);\n    lua_setfield(L, -2, \"get_room\");\n    lua_pushcfunction(L, M_L_CameraGetTargetPos);\n    lua_setfield(L, -2, \"get_target_pos\");\n    lua_pushcfunction(L, M_L_CameraGetTargetRoom);\n    lua_setfield(L, -2, \"get_target_room\");\n    lua_pushcfunction(L, M_L_CameraShake);\n    lua_setfield(L, -2, \"shake\");\n    lua_pushcfunction(L, M_L_CameraReset);\n    lua_setfield(L, -2, \"reset\");\n    lua_setfield(L, -2, \"camera\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/catalog.c",
    "content": "#include <trx/core/memory.h>\n#include <trx/game/lara/enum.h>\n\n#include <ctype.h>\n#include <lauxlib.h>\n#include <stdint.h>\n#include <string.h>\n\nstatic void M_PushCatalogKey(\n    lua_State *const L, const char *const name, const char *const prefix,\n    const int32_t value)\n{\n    const char *key = name;\n    if (prefix != nullptr) {\n        const size_t prefix_len = strlen(prefix);\n        if (strncmp(name, prefix, prefix_len) == 0) {\n            key = name + prefix_len;\n        }\n    }\n\n    const size_t key_len = strlen(key);\n    char *const lower_key = Memory_Alloc(key_len + 1);\n    for (size_t i = 0; i < key_len; i++) {\n        lower_key[i] = (char)tolower((unsigned char)key[i]);\n    }\n    lower_key[key_len] = '\\0';\n\n    lua_pushinteger(L, value);\n    lua_setfield(L, -2, lower_key);\n    Memory_Free(lower_key);\n}\n\nstatic void M_PushObjects(lua_State *const L)\n{\n    lua_newtable(L);\n\n    int32_t id = 0;\n#define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, \"O_\", id++);\n#include \"trx/game/catalog/objects.def\"\n#undef X_CATALOG_ID\n\n    lua_setfield(L, -2, \"objects\");\n}\n\nstatic void M_PushFlipEffects(lua_State *const L)\n{\n    lua_newtable(L);\n\n    int32_t id = 0;\n#define X_CATALOG_ID(enum_value)                                               \\\n    M_PushCatalogKey(L, #enum_value, \"ITEM_ACTION_\", id++);\n#include \"trx/game/catalog/item_actions.def\"\n#undef X_CATALOG_ID\n\n    lua_setfield(L, -2, \"flip_effects\");\n}\n\nstatic void M_PushLaraStates(lua_State *const L)\n{\n    lua_newtable(L);\n\n    int32_t id = 0;\n#define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, \"LS_\", id++);\n#include \"trx/game/catalog/lara_states.def\"\n#undef X_CATALOG_ID\n\n    lua_setfield(L, -2, \"lara_states\");\n}\n\nstatic void M_PushWeapons(lua_State *const L)\n{\n    lua_newtable(L);\n\n#define X_LUA_WEAPON(enum_value)                                               \\\n    M_PushCatalogKey(L, #enum_value, \"LGT_\", enum_value);\n    X_LUA_WEAPON(LGT_UNARMED);\n    X_LUA_WEAPON(LGT_PISTOLS);\n    X_LUA_WEAPON(LGT_MAGNUMS);\n    X_LUA_WEAPON(LGT_UZIS);\n    X_LUA_WEAPON(LGT_SHOTGUN);\n    X_LUA_WEAPON(LGT_M16);\n    X_LUA_WEAPON(LGT_GRENADE);\n    X_LUA_WEAPON(LGT_HARPOON);\n    X_LUA_WEAPON(LGT_FLARE);\n    X_LUA_WEAPON(LGT_SKIDOO);\n    X_LUA_WEAPON(LGT_AUTOS);\n    X_LUA_WEAPON(LGT_DESERT_EAGLE);\n    X_LUA_WEAPON(LGT_MP5);\n    X_LUA_WEAPON(LGT_ROCKET);\n#undef X_LUA_WEAPON\n\n    lua_setfield(L, -2, \"weapons\");\n}\n\nstatic void M_PushLaraAnims(lua_State *const L)\n{\n    lua_newtable(L);\n\n    int32_t id = 0;\n#define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, \"LA_\", id++);\n#include \"trx/game/catalog/lara_anims.def\"\n#undef X_CATALOG_ID\n\n    lua_setfield(L, -2, \"lara_anims\");\n}\n\nstatic void M_PushMusic(lua_State *const L)\n{\n    lua_newtable(L);\n\n    int32_t id = 0;\n#define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, \"MX_\", id++);\n#include \"trx/game/catalog/music.def\"\n#undef X_CATALOG_ID\n\n    lua_setfield(L, -2, \"music\");\n}\n\nstatic void M_PushSamples(lua_State *const L)\n{\n    lua_newtable(L);\n\n    int32_t id = 0;\n#define X_CATALOG_ID(enum_value) M_PushCatalogKey(L, #enum_value, \"SFX_\", id++);\n#include \"trx/game/catalog/samples.def\"\n#undef X_CATALOG_ID\n\n    lua_setfield(L, -2, \"samples\");\n}\n\nvoid LUA_CreateCatalog(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    M_PushObjects(L);\n    M_PushFlipEffects(L);\n    M_PushLaraStates(L);\n    M_PushWeapons(L);\n    M_PushLaraAnims(L);\n    M_PushMusic(L);\n    M_PushSamples(L);\n\n    lua_setfield(L, -2, \"catalog\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/common.c",
    "content": "#include <trx/game/lua/common.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/lua/embedded_scripts.h>\n#include <trx/game/lua/events.h>\n\n#include <lauxlib.h>\n#include <lua.h>\n#include <lualib.h>\n#include <string.h>\n\ntypedef struct {\n    lua_State *state;\n    LUA_CONTEXT context;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {\n    .context = LUA_CONTEXT_GLOBAL,\n};\n\n// Initialize internal APIs\nextern void LUA_CreateCatalog(lua_State *L);\nextern void LUA_CreateCamera(lua_State *L);\nextern void LUA_CreateConsole(lua_State *L);\nextern void LUA_CreateEvents(lua_State *L);\nextern void LUA_CreateItems(lua_State *L);\nextern void LUA_CreateLara(lua_State *L);\nextern void LUA_CreateLog(lua_State *L);\nextern void LUA_CreateMusic(lua_State *L);\nextern void LUA_CreateSound(lua_State *L);\nextern void LUA_CreateConfig(lua_State *L);\nextern void LUA_CreateRooms(lua_State *L);\nextern void LUA_CreateGame(lua_State *L);\nextern void LUA_CreateCreatures(lua_State *L);\nextern void LUA_CreateObjects(lua_State *const L);\nextern void LUA_CreateAssaultStats(lua_State *const L);\n\nstatic int M_LoadFile(lua_State *const L, const char *const path)\n{\n    return luaL_loadfile(L, path);\n}\n\n// Shared loader+pcall helper for Eval/EvalFile to capture errors with source\nstatic LUA_RESULT M_LuaLoadAndRun(\n    lua_State *const L, int (*const loader)(lua_State *, const char *),\n    const char *const src)\n{\n    LUA_RESULT result = { .code = LUA_OK, .message = nullptr };\n    int status = loader(L, src);\n    if (status != LUA_OK) {\n        result.code = status;\n        result.message = Memory_DupStr(lua_tostring(L, -1));\n        lua_pop(L, 1);\n        return result;\n    }\n    status = lua_pcall(L, 0, LUA_MULTRET, 0);\n    if (status != LUA_OK) {\n        result.code = status;\n        result.message = Memory_DupStr(lua_tostring(L, -1));\n        lua_pop(L, 1);\n    }\n    return result;\n}\n\n// Loader closure for embedded TRX modules, invoked via package.preload.\nstatic int M_TRXEmbeddedModuleLoader(lua_State *const L)\n{\n    const uint8_t *const data = lua_touserdata(L, lua_upvalueindex(1));\n    const size_t size = (size_t)lua_tointeger(L, lua_upvalueindex(2));\n    const char *const chunk_name = lua_tostring(L, lua_upvalueindex(3));\n    int status = luaL_loadbuffer(L, (const char *)data, size, chunk_name);\n    if (status != LUA_OK) {\n        lua_error(L);\n    }\n    status = lua_pcall(L, 0, LUA_MULTRET, 0);\n    if (status != LUA_OK) {\n        lua_error(L);\n    }\n    // Return all values pushed by the chunk.\n    return lua_gettop(L);\n}\n\nstatic void M_LoadTRXCModule(lua_State *const L, void (*loader)(lua_State *))\n{\n    LOG_DEBUG(\"Loading TRXC module %p\", loader);\n    loader(L);\n}\n\nstatic char *M_DeriveTRXModuleName(const char *path)\n{\n    char *raw = Memory_DupStr(path);\n    size_t raw_len = strlen(raw);\n\n    // Drop \".lua\"\n    if (raw_len > 4 && strcmp(raw + raw_len - 4, \".lua\") == 0) {\n        raw[raw_len - 4] = '\\0';\n    }\n\n    // Convert '/' → '.'\n    for (char *c = raw; *c; ++c) {\n        if (*c == '/') {\n            *c = '.';\n        }\n    }\n\n    // Prefix \"trx.\"\n    const char *modprefix = \"trx.\";\n    size_t prefix_len = strlen(modprefix);\n    raw_len = strlen(raw);\n    char *name = Memory_Alloc(prefix_len + raw_len + 1);\n    memcpy(name, modprefix, prefix_len);\n    memcpy(name + prefix_len, raw, raw_len + 1);\n\n    Memory_FreePointer(&raw);\n    return name;\n}\n\nstatic void M_RegisterTRXPreloadEmbedded(\n    lua_State *const L, const uint8_t *const data, const size_t size,\n    const char *const chunk_name, const char *const name)\n{\n    lua_getglobal(L, \"package\");\n    lua_getfield(L, -1, \"preload\");\n    lua_pushlightuserdata(L, (void *)data);\n    lua_pushinteger(L, (lua_Integer)size);\n    lua_pushstring(L, chunk_name);\n    lua_pushcclosure(L, M_TRXEmbeddedModuleLoader, 3);\n    lua_setfield(L, -2, name);\n    lua_pop(L, 2);\n}\n\nstatic void M_RequireTRXModule(lua_State *const L, const char *name)\n{\n    lua_getglobal(L, \"require\");\n    lua_pushstring(L, name);\n    if (lua_pcall(L, 1, LUA_MULTRET, 0) != LUA_OK) {\n        LOG_ERROR(\"Failed to require module %s: %s\", name, lua_tostring(L, -1));\n        lua_pop(L, 1);\n    }\n    lua_settop(L, 0);\n}\n\nstatic void M_LoadTRXScripts(lua_State *const L)\n{\n    for (const LUA_EMBEDDED_SCRIPT *script = g_LUA_EmbeddedScripts;\n         script->path != nullptr; script++) {\n        LOG_DEBUG(\"Loading TRX module %s\", script->path);\n        char *name = M_DeriveTRXModuleName(script->path);\n        const char *const chunk_name =\n            String_FormatStatic(\"@trx/%s\", script->path);\n        M_RegisterTRXPreloadEmbedded(\n            L, script->data, script->size, chunk_name, name);\n        M_RequireTRXModule(L, name);\n        Memory_FreePointer(&name);\n    }\n}\n\nvoid LUA_Init(void)\n{\n    lua_State *const L = luaL_newstate();\n    ASSERT(L != nullptr);\n    luaL_openlibs(L);\n\n    lua_newtable(L);\n    lua_setglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_setglobal(L, \"trx\");\n\n    // Initialize internal modules\n    M_LoadTRXCModule(L, LUA_CreateCatalog);\n    M_LoadTRXCModule(L, LUA_CreateCamera);\n    M_LoadTRXCModule(L, LUA_CreateConsole);\n    M_LoadTRXCModule(L, LUA_CreateEvents);\n    M_LoadTRXCModule(L, LUA_CreateItems);\n    M_LoadTRXCModule(L, LUA_CreateLara);\n    M_LoadTRXCModule(L, LUA_CreateLog);\n    M_LoadTRXCModule(L, LUA_CreateMusic);\n    M_LoadTRXCModule(L, LUA_CreateSound);\n    M_LoadTRXCModule(L, LUA_CreateConfig);\n    M_LoadTRXCModule(L, LUA_CreateRooms);\n    M_LoadTRXCModule(L, LUA_CreateGame);\n    M_LoadTRXCModule(L, LUA_CreateCreatures);\n    M_LoadTRXCModule(L, LUA_CreateObjects);\n    M_LoadTRXCModule(L, LUA_CreateAssaultStats);\n\n    M_PRIV *const p = &m_Priv;\n    p->state = L;\n\n    M_LoadTRXScripts(L);\n}\n\nvoid LUA_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    Lua_ShutdownEvents();\n    if (p->state != nullptr) {\n        lua_close(p->state);\n        p->state = nullptr;\n    }\n}\n\nLUA_CONTEXT Lua_GetScriptContext(void)\n{\n    M_PRIV *const p = &m_Priv;\n    return p->context;\n}\n\nvoid Lua_SetScriptContext(const LUA_CONTEXT context)\n{\n    M_PRIV *const p = &m_Priv;\n    p->context = context;\n}\n\nLUA_RESULT Lua_Eval(const char *const code)\n{\n    M_PRIV *const p = &m_Priv;\n    return M_LuaLoadAndRun(p->state, luaL_loadstring, code);\n}\n\nLUA_RESULT Lua_EvalFile(const char *const path)\n{\n    M_PRIV *const p = &m_Priv;\n    return M_LuaLoadAndRun(p->state, M_LoadFile, path);\n}\n\nvoid Lua_ReloadLevelScript(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level == nullptr) {\n        return;\n    }\n\n    Lua_ClearLevelListeners();\n    Lua_SetScriptContext(LUA_CONTEXT_LEVEL);\n\n    if (level->script_path != nullptr) {\n        LUA_RESULT res = Lua_EvalFile(level->script_path);\n        if (res.code != LUA_OK) {\n            LOG_ERROR(\"Lua level script error: %s\", res.message);\n        }\n        Lua_FreeResult(&res);\n    }\n\n    Lua_SetScriptContext(LUA_CONTEXT_GLOBAL);\n}\n\nvoid Lua_FreeResult(LUA_RESULT *const result)\n{\n    if (result != nullptr) {\n        Memory_FreePointer(&result->message);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/lua/common.h",
    "content": "#pragma once\n\n#include <lualib.h>\n#include <stdint.h>\n\n// Result of evaluating a Lua chunk.\ntypedef struct {\n    int32_t code; // LUA_OK, LUA_ERRSYNTAX, LUA_ERRRUN, etc.\n    char *message; // Error text (nullptr if code == LUA_OK).\n} LUA_RESULT;\n\ntypedef enum {\n    LUA_CONTEXT_GLOBAL,\n    LUA_CONTEXT_LEVEL,\n} LUA_CONTEXT;\n\nvoid LUA_Init(void);\nvoid LUA_Shutdown(void);\n\n// Set script context: level script vs global script\nLUA_CONTEXT Lua_GetScriptContext(void);\nvoid Lua_SetScriptContext(LUA_CONTEXT context);\n\n// Evaluate a Lua code string. Caller must free the result with Lua_FreeResult.\nLUA_RESULT Lua_Eval(const char *code);\n\n// Free the LUA eval result.\nvoid Lua_FreeResult(LUA_RESULT *result);\n\n// Evaluate a Lua script file. Caller must free the result with Lua_FreeResult.\nLUA_RESULT Lua_EvalFile(const char *path);\n\n// Reload current level script and reset level-scoped listeners.\nvoid Lua_ReloadLevelScript(void);\n"
  },
  {
    "path": "src/trx/game/lua/config.c",
    "content": "#include <trx/config/common.h>\n#include <trx/game/lua/common.h>\n\n#include <lauxlib.h>\n\n// trxc.config.get(key)\nstatic int M_L_ConfigGet(lua_State *const L)\n{\n    const char *const key = luaL_checkstring(L, 1);\n    const CONFIG_OPTION *const opt = Config_GetOptionByPath(key);\n    if (opt == nullptr) {\n        return luaL_error(L, \"Unknown option: %s\", key);\n    }\n    const char *const value = Config_GetOptionValueAsString(opt, false);\n    lua_pushstring(L, value);\n    return 1;\n}\n\n// trxc.config.set(key, value)\nstatic int M_L_ConfigSet(lua_State *const L)\n{\n    const char *const key = luaL_checkstring(L, 1);\n    const char *const new_value = luaL_checkstring(L, 2);\n    const CONFIG_OPTION *const opt = Config_GetOptionByPath(key);\n    if (opt == nullptr) {\n        return luaL_error(L, \"Unknown option: %s\", key);\n    }\n    const bool ok = Config_SetOptionValueFromString(opt, new_value);\n    if (!ok) {\n        return luaL_error(L, \"Failed to set option %s to %s\", key, new_value);\n    }\n    Config_Update();\n    return 0;\n}\n\n// trxc.config.list()\nstatic int M_L_ConfigList(lua_State *const L)\n{\n    lua_newtable(L);\n    const CONFIG_OPTION *opt = Config_GetOptionMap();\n    while (opt->name != nullptr) {\n        const char *const value = Config_GetOptionValueAsString(opt, false);\n        lua_pushstring(L, value);\n        lua_setfield(L, -2, opt->name);\n        opt++;\n    }\n    return 1;\n}\n\nvoid LUA_CreateConfig(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_ConfigGet);\n    lua_setfield(L, -2, \"get\");\n    lua_pushcfunction(L, M_L_ConfigSet);\n    lua_setfield(L, -2, \"set\");\n    lua_pushcfunction(L, M_L_ConfigList);\n    lua_setfield(L, -2, \"list\");\n    lua_setfield(L, -2, \"config\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/console.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/strings.h>\n#include <trx/game/console/common.h>\n#include <trx/game/console/registry.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lua/common.h>\n\n#include <lauxlib.h>\n\n// trxc.console.log(...)\nstatic int M_L_ConsoleLog(lua_State *const L)\n{\n    int num_args = lua_gettop(L);\n    if (num_args < 2) {\n        return 0;\n    }\n\n    const LOG_LEVEL log_level = (int)lua_tointeger(L, 1);\n    const char *msg = nullptr;\n\n    for (int32_t i = 2; i <= num_args; i++) {\n        lua_getglobal(L, \"tostring\");\n        lua_pushvalue(L, i);\n        lua_call(L, 1, 1);\n        const char *arg = lua_tostring(L, -1);\n        lua_pop(L, 1);\n        msg = (i > 2) ? String_FormatStatic(\"%s %s\", msg, arg)\n                      : String_FormatStatic(\"%s\", arg);\n    }\n\n    lua_Debug ar;\n    const char *src = \"?\";\n    const char *func = \"?\";\n    int line = 0;\n    if (lua_getstack(L, 2, &ar) && lua_getinfo(L, \"nSl\", &ar)) {\n        src = ar.short_src;\n        func = ar.name ? ar.name : \"?\";\n        line = ar.currentline;\n    }\n    Console_LogEx(log_level, src, line, func, \"%s\", msg);\n    return 0;\n}\n\n// trxc.console.clear()\nstatic int M_L_ConsoleClear(lua_State *const L)\n{\n    Console_Clear();\n    return 0;\n}\n\n// trxc.console.eval(cmd, { verbose = bool })\nstatic int M_L_ConsoleEval(lua_State *const L)\n{\n    const char *cmd = luaL_checkstring(L, 1);\n    bool verbose = false;\n    if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {\n        lua_getfield(L, 2, \"verbose\");\n        verbose = lua_toboolean(L, -1);\n        lua_pop(L, 1);\n    }\n    const bool old_verbose = Console_IsVerbose();\n    Console_SetVerbose(verbose);\n    COMMAND_RESULT res = Console_Eval(cmd);\n    Console_SetVerbose(old_verbose);\n    const char *err = \"unknown error\";\n    switch (res) {\n    case CR_BAD_INVOCATION:\n        err = \"bad invocation\";\n        break;\n    case CR_UNAVAILABLE:\n        err = \"unavailable\";\n        break;\n    case CR_FAILURE:\n        err = \"failure\";\n        break;\n    case CR_SUCCESS:\n        return 0;\n    }\n    return luaL_error(L, \"console.eval %s: %s\", err, cmd);\n}\n\nvoid LUA_CreateConsole(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_ConsoleLog);\n    lua_setfield(L, -2, \"log\");\n    lua_pushcfunction(L, M_L_ConsoleEval);\n    lua_setfield(L, -2, \"eval\");\n    lua_pushcfunction(L, M_L_ConsoleClear);\n    lua_setfield(L, -2, \"clear\");\n    lua_setfield(L, -2, \"console\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/creatures.c",
    "content": "#include <trx/game/creature.h>\n\n#include <lauxlib.h>\n\n// trxc.creatures.are_allies_hostile() → bool\nstatic int M_L_CreaturesAreAlliesHostile(lua_State *const L)\n{\n    const bool hostile = Creature_AreAlliesHostile();\n    lua_pushboolean(L, hostile);\n    return 1;\n}\n\n// trxc.creatures.set_allies_hostile(enable)\nstatic int M_L_CreaturesSetAlliesHostile(lua_State *const L)\n{\n    const bool hostile = lua_toboolean(L, 1) != 0;\n    Creature_SetAlliesHostile(hostile);\n    return 0;\n}\n\n// trxc.creatures.add_ally(obj_id)\nstatic int M_L_CreaturesAddAlly(lua_State *const L)\n{\n    const OBJECT_ID obj_id = luaL_checkinteger(L, 1);\n    Creature_AddAlly(obj_id);\n    return 0;\n}\n\n// trxc.creatures.add_ally_target(obj_id)\nstatic int M_L_CreaturesAddAllyTarget(lua_State *const L)\n{\n    const OBJECT_ID obj_id = luaL_checkinteger(L, 1);\n    Creature_AddAllyTargetingEnemy(obj_id);\n    return 0;\n}\n\nvoid LUA_CreateCreatures(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_CreaturesAreAlliesHostile);\n    lua_setfield(L, -2, \"are_allies_hostile\");\n    lua_pushcfunction(L, M_L_CreaturesSetAlliesHostile);\n    lua_setfield(L, -2, \"set_allies_hostile\");\n    lua_pushcfunction(L, M_L_CreaturesAddAlly);\n    lua_setfield(L, -2, \"add_ally\");\n    lua_pushcfunction(L, M_L_CreaturesAddAllyTarget);\n    lua_setfield(L, -2, \"add_ally_target\");\n    lua_setfield(L, -2, \"creatures\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/embedded_scripts.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef struct {\n    const char *path;\n    const uint8_t *data;\n    size_t size;\n} LUA_EMBEDDED_SCRIPT;\n\nextern const LUA_EMBEDDED_SCRIPT g_LUA_EmbeddedScripts[];\n"
  },
  {
    "path": "src/trx/game/lua/events.c",
    "content": "#include <trx/game/lua/events.h>\n\n#include <trx/core/log.h>\n#include <trx/core/vector.h>\n#include <trx/game/lua/common.h>\n\n#include <lauxlib.h>\n#include <lua.h>\n\ntypedef struct {\n    int32_t ref;\n    LUA_EVENT_TYPE type;\n    bool level_scoped;\n} M_LISTENER;\n\nstatic lua_State *m_L = nullptr;\nstatic VECTOR *m_Listeners = nullptr;\n\nstatic void M_ClearAllListeners(const bool unref_from_lua)\n{\n    if (m_Listeners == nullptr) {\n        return;\n    }\n\n    if (unref_from_lua && m_L != nullptr) {\n        for (int32_t i = 0; i < m_Listeners->count; i++) {\n            const M_LISTENER *const lst = Vector_Get(m_Listeners, i);\n            luaL_unref(m_L, LUA_REGISTRYINDEX, lst->ref);\n        }\n    }\n\n    Vector_Free(m_Listeners);\n    m_Listeners = nullptr;\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    M_ClearAllListeners(false);\n    m_L = nullptr;\n}\n\nvoid Lua_ShutdownEvents(void)\n{\n    M_ClearAllListeners(true);\n    m_L = nullptr;\n}\n\n// trxc.events.attach(event_type, callback) → id\nstatic int32_t M_L_EventsAttach(lua_State *const L)\n{\n    const LUA_EVENT_TYPE ev = luaL_checkinteger(L, 1);\n    luaL_checktype(L, 2, LUA_TFUNCTION);\n    lua_pushvalue(L, 2);\n    const int32_t ref = luaL_ref(L, LUA_REGISTRYINDEX);\n    if (m_Listeners == nullptr) {\n        m_Listeners = Vector_Create(sizeof(M_LISTENER));\n    }\n    const M_LISTENER listener = {\n        .ref = ref,\n        .type = ev,\n        .level_scoped = Lua_GetScriptContext() == LUA_CONTEXT_LEVEL,\n    };\n    Vector_Add(m_Listeners, &listener);\n    lua_pushinteger(L, ref);\n    return 1;\n}\n\n// trxc.events.detach(id)\nstatic int32_t M_L_EventsDetach(lua_State *const L)\n{\n    int32_t id = luaL_checkinteger(L, 1);\n    if (m_Listeners == nullptr) {\n        return 0;\n    }\n    for (int32_t i = 0; i < m_Listeners->count; i++) {\n        const M_LISTENER *const lst = Vector_Get(m_Listeners, i);\n        if (lst->ref == id) {\n            luaL_unref(L, LUA_REGISTRYINDEX, lst->ref);\n            Vector_RemoveAt(m_Listeners, i);\n            break;\n        }\n    }\n    return 0;\n}\n\nvoid Lua_ClearLevelListeners(void)\n{\n    lua_State *const L = m_L;\n    if (L == nullptr) {\n        return;\n    }\n    if (m_Listeners == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < m_Listeners->count;) {\n        M_LISTENER *const lst = Vector_Get(m_Listeners, i);\n        if (lst->level_scoped) {\n            luaL_unref(L, LUA_REGISTRYINDEX, lst->ref);\n            Vector_RemoveAt(m_Listeners, i);\n        } else {\n            i++;\n        }\n    }\n}\n\nstatic void M_PushArg(lua_State *const L, const LUA_EVENT_ARG arg)\n{\n    switch (arg.type) {\n    case LUA_EVENT_ARG_NIL:\n        lua_pushnil(L);\n        break;\n    case LUA_EVENT_ARG_INT32:\n        lua_pushinteger(L, arg.value.i32);\n        break;\n    case LUA_EVENT_ARG_BOOL:\n        lua_pushboolean(L, arg.value.b);\n        break;\n    case LUA_EVENT_ARG_NUMBER:\n        lua_pushnumber(L, arg.value.number);\n        break;\n    case LUA_EVENT_ARG_STRING:\n        if (arg.value.str != nullptr) {\n            lua_pushstring(L, arg.value.str);\n        } else {\n            lua_pushnil(L);\n        }\n        break;\n    }\n}\n\nvoid Lua_FireEventEx(\n    const LUA_EVENT_TYPE ev, const LUA_EVENT_ARG *const args,\n    const int32_t arg_count)\n{\n    lua_State *const L = m_L;\n    if (L == nullptr || m_Listeners == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < m_Listeners->count; i++) {\n        M_LISTENER *const lst = Vector_Get(m_Listeners, i);\n        if (lst->type != ev) {\n            continue;\n        }\n        lua_rawgeti(L, LUA_REGISTRYINDEX, lst->ref);\n        for (int32_t arg_idx = 0; arg_idx < arg_count; arg_idx++) {\n            M_PushArg(L, args[arg_idx]);\n        }\n        if (lua_pcall(L, arg_count, 0, 0) != LUA_OK) {\n            LOG_ERROR(\"Lua event handler error: %s\", lua_tostring(L, -1));\n            lua_pop(L, 1);\n        }\n    }\n}\n\nvoid Lua_FireEventInt32(const LUA_EVENT_TYPE ev, const int32_t arg)\n{\n    const LUA_EVENT_ARG args[] = {\n        { .type = LUA_EVENT_ARG_INT32, .value = { .i32 = arg } },\n    };\n    Lua_FireEventEx(ev, args, 1);\n}\n\nvoid LUA_CreateEvents(lua_State *const L)\n{\n    m_L = L;\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_pushcfunction(L, M_L_EventsAttach);\n    lua_setfield(L, -2, \"attach\");\n    lua_pushcfunction(L, M_L_EventsDetach);\n    lua_setfield(L, -2, \"detach\");\n\n    lua_newtable(L);\n    lua_pushinteger(L, LUA_EVENT_BEFORE_LEVEL_FILE);\n    lua_setfield(L, -2, \"BEFORE_LEVEL_FILE\");\n    lua_pushinteger(L, LUA_EVENT_AFTER_LEVEL_FILE);\n    lua_setfield(L, -2, \"AFTER_LEVEL_FILE\");\n    lua_pushinteger(L, LUA_EVENT_AFTER_LEVEL_STATE);\n    lua_setfield(L, -2, \"AFTER_LEVEL_STATE\");\n    lua_pushinteger(L, LUA_EVENT_GAME_START);\n    lua_setfield(L, -2, \"GAME_START\");\n    lua_pushinteger(L, LUA_EVENT_PICKUP);\n    lua_setfield(L, -2, \"PICKUP\");\n    lua_pushinteger(L, LUA_EVENT_BEFORE_CONTROL);\n    lua_setfield(L, -2, \"BEFORE_CONTROL\");\n    lua_pushinteger(L, LUA_EVENT_AFTER_CONTROL);\n    lua_setfield(L, -2, \"AFTER_CONTROL\");\n    lua_setfield(L, -2, \"EventType\");\n\n    lua_setfield(L, -2, \"events\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/events.h",
    "content": "// Lua event listener support\n#pragma once\n\n#include <lualib.h>\n#include <stdint.h>\n\n// Event types for Lua listeners\ntypedef enum {\n    LUA_EVENT_BEFORE_LEVEL_FILE,\n    LUA_EVENT_AFTER_LEVEL_FILE,\n    LUA_EVENT_AFTER_LEVEL_STATE,\n    LUA_EVENT_GAME_START,\n    LUA_EVENT_PICKUP,\n    LUA_EVENT_BEFORE_CONTROL,\n    LUA_EVENT_AFTER_CONTROL,\n} LUA_EVENT_TYPE;\n\ntypedef enum {\n    LUA_EVENT_ARG_NIL,\n    LUA_EVENT_ARG_INT32,\n    LUA_EVENT_ARG_BOOL,\n    LUA_EVENT_ARG_NUMBER,\n    LUA_EVENT_ARG_STRING,\n} LUA_EVENT_ARG_TYPE;\n\ntypedef struct {\n    LUA_EVENT_ARG_TYPE type;\n    union {\n        int32_t i32;\n        bool b;\n        double number;\n        const char *str;\n    } value;\n} LUA_EVENT_ARG;\n\n// Initialize event API in Lua state\nvoid LUA_CreateEvents(lua_State *L);\nvoid Lua_ShutdownEvents(void);\n\n// Clear all listeners declared during the current level script\nvoid Lua_ClearLevelListeners(void);\n\n// Fire a Lua event of given type with arbitrary arguments\nvoid Lua_FireEventEx(\n    LUA_EVENT_TYPE ev, const LUA_EVENT_ARG *args, int32_t arg_count);\n\n// Fire a Lua event of given type with int32 argument\nvoid Lua_FireEventInt32(LUA_EVENT_TYPE ev, int32_t arg);\n"
  },
  {
    "path": "src/trx/game/lua/game.c",
    "content": "#include <trx/game/game_flow.h>\n#include <trx/game/lua/common.h>\n#include <trx/game/savegame.h>\n#include <trx/version.h>\n\n#include <lauxlib.h>\n\n// trxc.game.get_version() → int\nstatic int M_L_GameVersion(lua_State *const L)\n{\n    lua_pushinteger(L, g_TRVersion);\n    return 1;\n}\n\n// trxc.game.get_trx_version() → string\nstatic int M_L_TRXVersion(lua_State *const L)\n{\n    lua_pushstring(L, g_TRXVersion);\n    return 1;\n}\n\n// trxc.game.count_levels() → int\nstatic int M_L_GameCountLevels(lua_State *const L)\n{\n    const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1);\n    lua_pushinteger(L, GF_GetLevelCount(table_type));\n    return 1;\n}\n\n// Level property getters\nstatic int M_L_GameLevelGetNum(lua_State *const L)\n{\n    const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1);\n    const int32_t idx = luaL_checkinteger(L, 2);\n    const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1);\n    lua_pushinteger(\n        L, lvl != nullptr ? GF_GetLevelOrdinalNumber(table_type, lvl) : 0);\n    return 1;\n}\n\nstatic int M_L_GameLevelGetName(lua_State *const L)\n{\n    const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1);\n    const int32_t idx = luaL_checkinteger(L, 2);\n    const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1);\n    if (lvl != nullptr && lvl->title != nullptr) {\n        lua_pushstring(L, lvl->title);\n    } else {\n        lua_pushnil(L);\n    }\n    return 1;\n}\n\nstatic int M_L_GameLevelGetPath(lua_State *const L)\n{\n    const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1);\n    const int32_t idx = luaL_checkinteger(L, 2);\n    const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1);\n    if (lvl != nullptr && lvl->path != nullptr) {\n        lua_pushstring(L, lvl->path);\n    } else {\n        lua_pushnil(L);\n    }\n    return 1;\n}\n\nstatic int M_L_GameLevelGetType(lua_State *const L)\n{\n    const GF_LEVEL_TABLE_TYPE table_type = luaL_checkinteger(L, 1);\n    const int32_t idx = luaL_checkinteger(L, 2);\n    const GF_LEVEL *const lvl = GF_GetLevel(table_type, idx - 1);\n    lua_pushinteger(L, lvl != nullptr ? lvl->type : 0);\n    return 1;\n}\n\nstatic int M_L_GameLevelGetCurrentLevelTable(lua_State *const L)\n{\n    const GF_LEVEL *const lvl = GF_GetCurrentLevel();\n    lua_pushinteger(\n        L, lvl != nullptr ? GF_GetLevelTableType(lvl->type) : GFLT_UNKNOWN);\n    return 1;\n}\n\nstatic int M_L_GameLevelGetCurrentLevelIndex(lua_State *const L)\n{\n    const GF_LEVEL *const lvl = GF_GetCurrentLevel();\n    lua_pushinteger(L, lvl != nullptr ? lvl->num : -1);\n    return 1;\n}\n\n// trxc.game.play_level(num) → nil\nstatic int M_L_GamePlayLevel(lua_State *const L)\n{\n    const int32_t level_idx = luaL_checkinteger(L, 1) - 1;\n    const int32_t count = GF_GetLevelCount(GFLT_MAIN);\n    if (level_idx < 0 || level_idx >= count) {\n        return luaL_error(L, \"invalid level number: %d\", level_idx);\n    }\n    const GF_LEVEL *const current_level = GF_GetCurrentLevel();\n    if (current_level != nullptr) {\n        const GF_LEVEL *const next_level = GF_GetLevel(GFLT_MAIN, level_idx);\n        if (next_level != nullptr) {\n            Savegame_PersistGameToCurrentInfo(next_level);\n            RESUME_INFO *const resume = Savegame_GetCurrentInfo(next_level);\n            if (resume != nullptr) {\n                resume->prev_level = current_level->num;\n            }\n        }\n    }\n    GF_OverrideCommand((GF_COMMAND) {\n        .action = GF_START_GAME,\n        .param = level_idx,\n    });\n    return 0;\n}\n\n// trxc.game.play_cutscene(num) → nil\nstatic int M_L_GamePlayCutscene(lua_State *const L)\n{\n    const int32_t idx = luaL_checkinteger(L, 1) - 1;\n    const int32_t count = GF_GetLevelCount(GFLT_CUTSCENES);\n    if (idx < 0 || idx >= count) {\n        return luaL_error(L, \"invalid cutscene number: %d\", idx);\n    }\n    GF_OverrideCommand((GF_COMMAND) {\n        .action = GF_START_CINE,\n        .param = idx,\n    });\n    return 0;\n}\n\n// trxc.game.play_demo(num) → nil\nstatic int M_L_GamePlayDemo(lua_State *const L)\n{\n    const int32_t idx = luaL_checkinteger(L, 1) - 1;\n    const int32_t count = GF_GetLevelCount(GFLT_DEMOS);\n    if (idx < 0 || idx >= count) {\n        return luaL_error(L, \"invalid demo number: %d\", idx);\n    }\n    GF_OverrideCommand((GF_COMMAND) {\n        .action = GF_START_DEMO,\n        .param = idx,\n    });\n    return 0;\n}\n\nvoid LUA_CreateGame(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_pushcfunction(L, M_L_GameVersion);\n    lua_setfield(L, -2, \"get_version\");\n    lua_pushcfunction(L, M_L_TRXVersion);\n    lua_setfield(L, -2, \"get_trx_version\");\n    lua_pushcfunction(L, M_L_GameCountLevels);\n    lua_setfield(L, -2, \"count_levels\");\n    lua_pushcfunction(L, M_L_GameLevelGetNum);\n    lua_setfield(L, -2, \"get_level_num\");\n    lua_pushcfunction(L, M_L_GameLevelGetName);\n    lua_setfield(L, -2, \"get_level_name\");\n    lua_pushcfunction(L, M_L_GameLevelGetPath);\n    lua_setfield(L, -2, \"get_level_path\");\n    lua_pushcfunction(L, M_L_GameLevelGetType);\n    lua_setfield(L, -2, \"get_level_type\");\n    lua_pushcfunction(L, M_L_GameLevelGetCurrentLevelTable);\n    lua_setfield(L, -2, \"get_current_level_table\");\n    lua_pushcfunction(L, M_L_GameLevelGetCurrentLevelIndex);\n    lua_setfield(L, -2, \"get_current_level_idx\");\n\n    lua_pushcfunction(L, M_L_GamePlayLevel);\n    lua_setfield(L, -2, \"play_level\");\n    lua_pushcfunction(L, M_L_GamePlayCutscene);\n    lua_setfield(L, -2, \"play_cutscene\");\n    lua_pushcfunction(L, M_L_GamePlayDemo);\n    lua_setfield(L, -2, \"play_demo\");\n\n    lua_newtable(L);\n    lua_pushinteger(L, GFLT_MAIN);\n    lua_setfield(L, -2, \"MAIN\");\n    lua_pushinteger(L, GFLT_CUTSCENES);\n    lua_setfield(L, -2, \"CUTSCENES\");\n    lua_pushinteger(L, GFLT_DEMOS);\n    lua_setfield(L, -2, \"DEMOS\");\n    lua_setfield(L, -2, \"LevelTable\");\n\n    lua_newtable(L);\n    lua_pushinteger(L, GFL_NORMAL);\n    lua_setfield(L, -2, \"NORMAL\");\n    lua_pushinteger(L, GFL_CUTSCENE);\n    lua_setfield(L, -2, \"CUTSCENE\");\n    lua_pushinteger(L, GFL_DEMO);\n    lua_setfield(L, -2, \"DEMO\");\n    lua_pushinteger(L, GFL_GYM);\n    lua_setfield(L, -2, \"GYM\");\n    lua_pushinteger(L, GFL_BONUS);\n    lua_setfield(L, -2, \"BONUS\");\n    lua_setfield(L, -2, \"LevelType\");\n\n    lua_setfield(L, -2, \"game\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/items.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/items.h>\n#include <trx/game/lua/common.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n#include <lauxlib.h>\n\n#define M_ITEM_GETTER(L)                                                       \\\n    const int idx = luaL_checkinteger(L, 1);                                   \\\n    const ITEM *const item = Item_Get(idx - 1);                                \\\n    if (item == nullptr) {                                                     \\\n        lua_pushnil(L);                                                        \\\n        return 1;                                                              \\\n    }\n\n#define M_ITEM_SETTER(L)                                                       \\\n    const int idx = luaL_checkinteger(L, 1);                                   \\\n    ITEM *const item = Item_Get(idx - 1);                                      \\\n    if (item == nullptr) {                                                     \\\n        return 1;                                                              \\\n    }\n\n// trxc.items.item_count() → int\nstatic int M_L_ItemsCount(lua_State *const L)\n{\n    lua_pushinteger(L, Item_GetTotalCount());\n    return 1;\n}\n\n// trxc.items.get(index or name) → int (1-based) or nil\nstatic int M_L_ItemsGet(lua_State *const L)\n{\n    int result = 0;\n    if (lua_type(L, 1) == LUA_TNUMBER) {\n        const int idx = luaL_checkinteger(L, 1);\n        const ITEM *const item = Item_Get(idx - 1);\n        if (item != nullptr) {\n            result = idx;\n        }\n    } else {\n        const char *const name = luaL_checkstring(L, 1);\n        const ITEM *const item = Item_GetByName(name);\n        if (item != nullptr) {\n            result = Item_GetIndex(item) + 1;\n        }\n    }\n    if (result) {\n        lua_pushinteger(L, result);\n    } else {\n        lua_pushnil(L);\n    }\n    return 1;\n}\n\n// trxc.items.get_pos(index) → {x, y, z} or nil\nstatic int M_L_ItemGetPos(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_newtable(L);\n    lua_pushinteger(L, item->pos.x);\n    lua_setfield(L, -2, \"x\");\n    lua_pushinteger(L, item->pos.y);\n    lua_setfield(L, -2, \"y\");\n    lua_pushinteger(L, item->pos.z);\n    lua_setfield(L, -2, \"z\");\n    return 1;\n}\n\n// trxc.items.get_rot(index) → {x, y, z} or nil\nstatic int M_L_ItemGetRot(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_newtable(L);\n    lua_pushinteger(L, item->rot.x);\n    lua_setfield(L, -2, \"x\");\n    lua_pushinteger(L, item->rot.y);\n    lua_setfield(L, -2, \"y\");\n    lua_pushinteger(L, item->rot.z);\n    lua_setfield(L, -2, \"z\");\n    return 1;\n}\n\n// trxc.items.get_anim(index) → int or nil\nstatic int M_L_ItemGetAnim(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, Item_GetRelativeAnim(item));\n    return 1;\n}\n\n// trxc.items.get_frame(index) → int or nil\nstatic int M_L_ItemGetFrame(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, Item_GetRelativeFrame(item));\n    return 1;\n}\n\n// trxc.items.get_room(index) → int or nil\nstatic int M_L_ItemGetRoom(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, item->room_num + 1);\n    return 1;\n}\n\n// trxc.items.get_status(index) → int or nil\nstatic int M_L_ItemGetStatus(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, (int)item->status);\n    return 1;\n}\n\n// trxc.items.get_flags(index) → int or nil\nstatic int M_L_ItemGetFlags(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, (int)item->flags);\n    return 1;\n}\n\n// trxc.items.get_timer(index) → int or nil\nstatic int M_L_ItemGetTimer(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, (int)item->timer);\n    return 1;\n}\n\n// trxc.items.get_object_id(index) → int or nil\nstatic int M_L_ItemGetObjectId(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, item->object_id);\n    return 1;\n}\n\n// trxc.items.get_hit_points(index) → int or nil\nstatic int M_L_ItemGetHitPoints(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, item->hit_points);\n    return 1;\n}\n\n// trxc.items.get_max_hit_points(index) → int or nil\nstatic int M_L_ItemGetMaxHitPoints(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    lua_pushinteger(L, item->max_hit_points);\n    return 1;\n}\n\n// trxc.items.get_name(index) → string or nil\nstatic int M_L_ItemGetName(lua_State *const L)\n{\n    M_ITEM_GETTER(L);\n    if (item->name == nullptr) {\n        lua_pushnil(L);\n    } else {\n        lua_pushstring(L, item->name);\n    }\n    return 1;\n}\n\n// trxc.items.set_pos(index, {x,y,z})\nstatic int M_L_ItemSetPos(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    luaL_checktype(L, 2, LUA_TTABLE);\n    lua_getfield(L, 2, \"x\");\n    item->pos.x = luaL_checkinteger(L, -1);\n    lua_pop(L, 1);\n    lua_getfield(L, 2, \"y\");\n    item->pos.y = luaL_checkinteger(L, -1);\n    lua_pop(L, 1);\n    lua_getfield(L, 2, \"z\");\n    item->pos.z = luaL_checkinteger(L, -1);\n    lua_pop(L, 1);\n    const int16_t room_num = Room_GetIndexFromPos(item->pos);\n    Item_UpdateRoom(idx - 1, room_num);\n    return 0;\n}\n\n// trxc.items.set_anim(index, anim_idx)\nstatic int M_L_ItemSetAnim(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    const int32_t anim_idx = luaL_checkinteger(L, 2);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->anim_idx == NO_ANIM) {\n        return luaL_error(L, \"object has no animations\");\n    }\n    if (anim_idx < 0 || anim_idx >= Anim_GetTotalCount()\n        || anim_idx >= obj->anim_count) {\n        return luaL_error(L, \"invalid animation index\");\n    }\n    ANIM *const anim = Anim_GetAnim(obj->anim_idx + anim_idx);\n    if (anim->frame_ptr == nullptr) {\n        return luaL_error(L, \"invalid animation index\");\n    }\n    item->anim_num = obj->anim_idx + anim_idx;\n    item->frame_num = anim->frame_base;\n    return 0;\n}\n\n// trxc.items.set_frame(index, frame_idx)\nstatic int M_L_ItemSetFrame(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    const int32_t frame_idx = luaL_checkinteger(L, 2);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (obj->anim_idx == NO_ANIM) {\n        return luaL_error(L, \"object has no animations\");\n    }\n    const ANIM *const anim = Item_GetAnim(item);\n    if (frame_idx < 0) {\n        if (anim->frame_end + frame_idx + 1 < anim->frame_base) {\n            return luaL_error(L, \"invalid frame index\");\n        }\n        item->frame_num = anim->frame_end + frame_idx + 1;\n    } else {\n        if (anim->frame_base + frame_idx >= anim->frame_end) {\n            return luaL_error(L, \"invalid frame index\");\n        }\n        item->frame_num = anim->frame_base + frame_idx;\n    }\n    return 0;\n}\n\n// trxc.items.set_hit_points(index, hp)\nstatic int M_L_ItemSetHitPoints(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    item->hit_points = luaL_checkinteger(L, 2);\n    item->max_hit_points = MAX(item->hit_points, item->max_hit_points);\n    return 0;\n}\n\n// trxc.items.set_max_hit_points(index, max_hp)\nstatic int M_L_ItemSetMaxHitPoints(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    item->max_hit_points = luaL_checkinteger(L, 2);\n    return 0;\n}\n\n// trxc.items.set_rot(index, {x,y,z})\nstatic int M_L_ItemSetRot(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    luaL_checktype(L, 2, LUA_TTABLE);\n    lua_getfield(L, 2, \"x\");\n    item->rot.x = luaL_checkinteger(L, -1);\n    lua_pop(L, 1);\n    lua_getfield(L, 2, \"y\");\n    item->rot.y = luaL_checkinteger(L, -1);\n    lua_pop(L, 1);\n    lua_getfield(L, 2, \"z\");\n    item->rot.z = luaL_checkinteger(L, -1);\n    lua_pop(L, 1);\n    return 0;\n}\n\n// trxc.items.set_name(index, name)\nstatic int M_L_ItemSetName(lua_State *const L)\n{\n    M_ITEM_SETTER(L);\n    const char *const new_name = luaL_checkstring(L, 2);\n    if (!Item_SetName(Item_GetIndex(item), new_name)) {\n        return luaL_error(L, \"item name '%s' already in use\", new_name);\n    }\n    return 0;\n}\n\nvoid LUA_CreateItems(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_ItemsCount);\n    lua_setfield(L, -2, \"count\");\n    lua_pushcfunction(L, M_L_ItemsGet);\n    lua_setfield(L, -2, \"get\");\n    lua_pushcfunction(L, M_L_ItemGetPos);\n    lua_setfield(L, -2, \"get_pos\");\n    lua_pushcfunction(L, M_L_ItemGetRot);\n    lua_setfield(L, -2, \"get_rot\");\n    lua_pushcfunction(L, M_L_ItemGetAnim);\n    lua_setfield(L, -2, \"get_anim\");\n    lua_pushcfunction(L, M_L_ItemGetFrame);\n    lua_setfield(L, -2, \"get_frame\");\n    lua_pushcfunction(L, M_L_ItemGetRoom);\n    lua_setfield(L, -2, \"get_room\");\n    lua_pushcfunction(L, M_L_ItemGetStatus);\n    lua_setfield(L, -2, \"get_status\");\n    lua_pushcfunction(L, M_L_ItemGetFlags);\n    lua_setfield(L, -2, \"get_flags\");\n    lua_pushcfunction(L, M_L_ItemGetTimer);\n    lua_setfield(L, -2, \"get_timer\");\n    lua_pushcfunction(L, M_L_ItemGetObjectId);\n    lua_setfield(L, -2, \"get_object_id\");\n    lua_pushcfunction(L, M_L_ItemGetHitPoints);\n    lua_setfield(L, -2, \"get_hit_points\");\n    lua_pushcfunction(L, M_L_ItemGetMaxHitPoints);\n    lua_setfield(L, -2, \"get_max_hit_points\");\n    lua_pushcfunction(L, M_L_ItemGetName);\n    lua_setfield(L, -2, \"get_name\");\n    lua_pushcfunction(L, M_L_ItemSetPos);\n    lua_setfield(L, -2, \"set_pos\");\n    lua_pushcfunction(L, M_L_ItemSetRot);\n    lua_setfield(L, -2, \"set_rot\");\n    lua_pushcfunction(L, M_L_ItemSetAnim);\n    lua_setfield(L, -2, \"set_anim\");\n    lua_pushcfunction(L, M_L_ItemSetFrame);\n    lua_setfield(L, -2, \"set_frame\");\n    lua_pushcfunction(L, M_L_ItemSetHitPoints);\n    lua_setfield(L, -2, \"set_hit_points\");\n    lua_pushcfunction(L, M_L_ItemSetMaxHitPoints);\n    lua_setfield(L, -2, \"set_max_hit_points\");\n    lua_pushcfunction(L, M_L_ItemSetName);\n    lua_setfield(L, -2, \"set_name\");\n    lua_setfield(L, -2, \"items\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/lara.c",
    "content": "#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/skin/storage.h>\n#include <trx/game/lua/common.h>\n\n#include <lauxlib.h>\n\n// item_num = trxc.lara.get_item()\nstatic int M_L_GetLaraItem(lua_State *const L)\n{\n    const ITEM *const item = Lara_GetItem();\n    int result = 0;\n    if (item != nullptr) {\n        result = Item_GetIndex(item) + 1;\n    }\n    if (result == 0) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, result);\n    }\n    return 1;\n}\n\n// item_num = trxc.lara.get_target()\nstatic int M_L_GetLaraTarget(lua_State *const L)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->target == nullptr) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, Item_GetIndex(lara->target) + 1);\n    }\n    return 1;\n}\n\n// trxc.lara.get_exposure_bar() → int\nstatic int M_L_LaraGetExposureBar(lua_State *const L)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lua_pushinteger(L, lara->exposure_timer);\n    return 1;\n}\n\n// trxc.lara.set_exposure_bar(timer)\nstatic int M_L_LaraSetExposureBar(lua_State *const L)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->exposure_timer = luaL_checkinteger(L, 1);\n    return 0;\n}\n\n// trxc.lara.get_air_bar() → int\nstatic int M_L_LaraGetAirBar(lua_State *const L)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lua_pushinteger(L, lara->air);\n    return 1;\n}\n\n// trxc.lara.set_air_bar(timer)\nstatic int M_L_LaraSetAirBar(lua_State *const L)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->air = luaL_checkinteger(L, 1);\n    return 0;\n}\n\n// trxc.lara.get_outfit() → string\nstatic int M_L_LaraGetOutfit(lua_State *const L)\n{\n    const int32_t outfit_idx = Lara_Skin_GetType();\n    const char *const outfit_name = Lara_Skin_GetOutfitName(outfit_idx);\n    if (outfit_name == nullptr) {\n        lua_pushnil(L);\n    } else {\n        lua_pushstring(L, outfit_name);\n    }\n    return 1;\n}\n\n// trxc.lara.set_outfit(outfit_name)\nstatic int M_L_LaraSetOutfit(lua_State *const L)\n{\n    const char *const outfit_name = luaL_checkstring(L, 1);\n    const int32_t outfit_idx = Lara_Skin_FindOutfitByName(outfit_name);\n    if (!Lara_Skin_IsOutfitAvailable(outfit_idx)) {\n        return luaL_error(L, \"unknown Lara outfit: %s\", outfit_name);\n    }\n    Lara_Skin_SetType(outfit_idx);\n    return 0;\n}\n\n// trxc.lara.set_extra_equipment(lara_mesh, extra_mesh)\nstatic int M_L_LaraSetExtraEquipment(lua_State *const L)\n{\n    const LARA_MESH lara_mesh = luaL_checkinteger(L, 1);\n    const LARA_SKIN_EXTRA_MESH extra_mesh = luaL_checkinteger(L, 2);\n    Lara_Skin_SetExtraEquipment(lara_mesh, extra_mesh);\n    return 0;\n}\n\n// trxc.lara.clear_equipment(lara_mesh)\nstatic int M_L_LaraClearEquipment(lua_State *const L)\n{\n    const LARA_MESH lara_mesh = luaL_checkinteger(L, 1);\n    Lara_Skin_ClearEquipment(lara_mesh);\n    return 0;\n}\n\n// trxc.lara.are_holsters_visible() → bool\nstatic int M_L_LaraAreHolstersVisible(lua_State *const L)\n{\n    lua_pushboolean(L, Lara_Skin_AreHolstersVisible());\n    return 1;\n}\n\n// trxc.lara.set_holsters_visible(visible)\nstatic int M_L_LaraSetHolstersVisible(lua_State *const L)\n{\n    const bool visible = lua_toboolean(L, 1) != 0;\n    Lara_Skin_SetHolstersVisible(visible);\n    return 0;\n}\n\n// trxc.lara.has_pistol_weapon() → bool\nstatic int M_L_LaraHasPistolWeapon(lua_State *const L)\n{\n    bool has_pistol = false;\n    for (int32_t i = 0; i < NUM_WEAPONS; i++) {\n        const WEAPON_INFO *const weapon = &g_Weapons[i];\n        if ((weapon->type == WEAPON_TYPE_DUAL_PISTOLS\n             || weapon->type == WEAPON_TYPE_SINGLE_PISTOL)\n            && Inv_RequestItem(Gun_GetGunObject(i))) {\n            has_pistol = true;\n            break;\n        }\n    }\n    lua_pushboolean(L, has_pistol);\n    return 1;\n}\n\n// trxc.lara.get_extra_anim() → int\nstatic int M_L_LaraGetExtraAnim(lua_State *const L)\n{\n    if (Lara_GetLaraInfo()->extra_anim) {\n        lua_pushinteger(\n            L, Item_GetRelativeObjAnim(Lara_GetItem(), O_LARA_EXTRA));\n    } else {\n        lua_pushinteger(L, NO_ANIM);\n    }\n    return 1;\n}\n\n// trxc.lara.get_equipped_gun() → int\nstatic int M_L_LaraGetEquippedGun(lua_State *const L)\n{\n    lua_pushinteger(L, Lara_GetLaraInfo()->gun_type);\n    return 1;\n}\n\nvoid LUA_CreateLara(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_newtable(L);\n    lua_pushinteger(L, LM_HIPS);\n    lua_setfield(L, -2, \"hips\");\n    lua_pushinteger(L, LM_THIGH_L);\n    lua_setfield(L, -2, \"thigh_l\");\n    lua_pushinteger(L, LM_CALF_L);\n    lua_setfield(L, -2, \"calf_l\");\n    lua_pushinteger(L, LM_FOOT_L);\n    lua_setfield(L, -2, \"foot_l\");\n    lua_pushinteger(L, LM_THIGH_R);\n    lua_setfield(L, -2, \"thigh_r\");\n    lua_pushinteger(L, LM_CALF_R);\n    lua_setfield(L, -2, \"calf_r\");\n    lua_pushinteger(L, LM_FOOT_R);\n    lua_setfield(L, -2, \"foot_r\");\n    lua_pushinteger(L, LM_TORSO);\n    lua_setfield(L, -2, \"torso\");\n    lua_pushinteger(L, LM_UARM_R);\n    lua_setfield(L, -2, \"uarm_r\");\n    lua_pushinteger(L, LM_LARM_R);\n    lua_setfield(L, -2, \"larm_r\");\n    lua_pushinteger(L, LM_HAND_R);\n    lua_setfield(L, -2, \"hand_r\");\n    lua_pushinteger(L, LM_UARM_L);\n    lua_setfield(L, -2, \"uarm_l\");\n    lua_pushinteger(L, LM_LARM_L);\n    lua_setfield(L, -2, \"larm_l\");\n    lua_pushinteger(L, LM_HAND_L);\n    lua_setfield(L, -2, \"hand_l\");\n    lua_pushinteger(L, LM_HEAD);\n    lua_setfield(L, -2, \"head\");\n    lua_setfield(L, -2, \"mesh\");\n\n    lua_newtable(L);\n    lua_pushinteger(L, EXTRA_MESH_DAGGER_HAND);\n    lua_setfield(L, -2, \"dagger_hand\");\n    lua_pushinteger(L, EXTRA_MESH_DAGGER_HIPS);\n    lua_setfield(L, -2, \"dagger_hips\");\n    lua_pushinteger(L, EXTRA_MESH_OAR);\n    lua_setfield(L, -2, \"oar\");\n    lua_pushinteger(L, EXTRA_MESH_SPANNER);\n    lua_setfield(L, -2, \"spanner\");\n    lua_pushinteger(L, EXTRA_MESH_DRINK_CAN);\n    lua_setfield(L, -2, \"drink_can\");\n    lua_pushinteger(L, EXTRA_MESH_GLASSES_OPAQUE);\n    lua_setfield(L, -2, \"glasses_opaque\");\n    lua_pushinteger(L, EXTRA_MESH_GLASSES_TRANSPARENT);\n    lua_setfield(L, -2, \"glasses_transparent\");\n    lua_setfield(L, -2, \"extra_mesh\");\n\n    lua_pushcfunction(L, M_L_GetLaraItem);\n    lua_setfield(L, -2, \"get_item\");\n    lua_pushcfunction(L, M_L_GetLaraTarget);\n    lua_setfield(L, -2, \"get_target\");\n    lua_pushcfunction(L, M_L_LaraGetExposureBar);\n    lua_setfield(L, -2, \"get_exposure_bar\");\n    lua_pushcfunction(L, M_L_LaraSetExposureBar);\n    lua_setfield(L, -2, \"set_exposure_bar\");\n    lua_pushcfunction(L, M_L_LaraGetAirBar);\n    lua_setfield(L, -2, \"get_air_bar\");\n    lua_pushcfunction(L, M_L_LaraSetAirBar);\n    lua_setfield(L, -2, \"set_air_bar\");\n    lua_pushcfunction(L, M_L_LaraGetOutfit);\n    lua_setfield(L, -2, \"get_outfit\");\n    lua_pushcfunction(L, M_L_LaraSetOutfit);\n    lua_setfield(L, -2, \"set_outfit\");\n    lua_pushcfunction(L, M_L_LaraSetExtraEquipment);\n    lua_setfield(L, -2, \"set_extra_equipment\");\n    lua_pushcfunction(L, M_L_LaraClearEquipment);\n    lua_setfield(L, -2, \"clear_equipment\");\n    lua_pushcfunction(L, M_L_LaraAreHolstersVisible);\n    lua_setfield(L, -2, \"are_holsters_visible\");\n    lua_pushcfunction(L, M_L_LaraSetHolstersVisible);\n    lua_setfield(L, -2, \"set_holsters_visible\");\n    lua_pushcfunction(L, M_L_LaraHasPistolWeapon);\n    lua_setfield(L, -2, \"has_pistol_weapon\");\n    lua_pushcfunction(L, M_L_LaraGetExtraAnim);\n    lua_setfield(L, -2, \"get_extra_anim\");\n    lua_pushcfunction(L, M_L_LaraGetEquippedGun);\n    lua_setfield(L, -2, \"get_equipped_gun\");\n    lua_setfield(L, -2, \"lara\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/log.c",
    "content": "#include <trx/core/log.h>\n\n#include <lauxlib.h>\n#include <lua.h>\n\n// trxc.log.log(level, msg)\nstatic int M_L_LogGeneric(lua_State *const L)\n{\n    const LOG_LEVEL level = luaL_checkinteger(L, 1);\n    const char *const msg = luaL_checkstring(L, 2);\n    lua_Debug ar;\n    const char *src = \"?\";\n    const char *func = \"?\";\n    int line = 0;\n    if (lua_getstack(L, 2, &ar) && lua_getinfo(L, \"nSl\", &ar)) {\n        src = ar.short_src;\n        func = ar.name ? ar.name : \"?\";\n        line = ar.currentline;\n    }\n    Log_Message(level, src, line, func, \"%s\", msg);\n    return 0;\n}\n\nvoid LUA_CreateLog(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_pushcfunction(L, M_L_LogGeneric);\n    lua_setfield(L, -2, \"log\");\n\n    lua_newtable(L);\n    lua_pushinteger(L, LOG_LEVEL_INFO);\n    lua_setfield(L, -2, \"INFO\");\n    lua_pushinteger(L, LOG_LEVEL_WARNING);\n    lua_setfield(L, -2, \"WARNING\");\n    lua_pushinteger(L, LOG_LEVEL_ERROR);\n    lua_setfield(L, -2, \"ERROR\");\n    lua_pushinteger(L, LOG_LEVEL_DEBUG);\n    lua_setfield(L, -2, \"DEBUG\");\n    lua_setfield(L, -2, \"LogLevel\");\n\n    lua_setfield(L, -2, \"log\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/music.c",
    "content": "#include <trx/game/lua/common.h>\n#include <trx/game/music/common.h>\n\n#include <lauxlib.h>\n\n// trxc.music.get_track()\nstatic int M_L_MusicGetTrack(lua_State *const L)\n{\n    const MUSIC_ID track = Music_GetCurrentPlayingTrack();\n    if (track < 0) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, (lua_Integer)track);\n    }\n    return 1;\n}\n\n// trxc.music.play_track(id[, opts])\nstatic int M_L_MusicPlayTrack(lua_State *const L)\n{\n    const lua_Integer id = luaL_checkinteger(L, 1);\n    const MUSIC_PLAY_MODE mode = luaL_checkinteger(L, 2);\n    if (!Music_Play_Direct((MUSIC_ID)id, mode)) {\n        return luaL_error(\n            L, \"invalid music track or mode (id=%d, mode=%d)\", id, mode);\n    }\n    return 0;\n}\n\n// trxc.music.pause()\nstatic int M_L_MusicPause(lua_State *const L)\n{\n    Music_Pause();\n    return 0;\n}\n\n// trxc.music.unpause()\nstatic int M_L_MusicUnpause(lua_State *const L)\n{\n    Music_Unpause();\n    return 0;\n}\n\n// trxc.music.stop()\nstatic int M_L_MusicStop(lua_State *const L)\n{\n    Music_Stop();\n    return 0;\n}\n\nvoid LUA_CreateMusic(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_newtable(L);\n    lua_pushinteger(L, MPM_ONCE);\n    lua_setfield(L, -2, \"ONCE\");\n    lua_pushinteger(L, MPM_LOOP);\n    lua_setfield(L, -2, \"LOOP\");\n    lua_pushinteger(L, MPM_DELAY);\n    lua_setfield(L, -2, \"DELAY\");\n    lua_pushinteger(L, MPM_NO_REPEAT);\n    lua_setfield(L, -2, \"NO_REPEAT\");\n    lua_pushinteger(L, MPM_OVERLAY);\n    lua_setfield(L, -2, \"OVERLAY\");\n    lua_setfield(L, -2, \"PlayMode\");\n\n    lua_pushcfunction(L, M_L_MusicGetTrack);\n    lua_setfield(L, -2, \"get_track\");\n    lua_pushcfunction(L, M_L_MusicPlayTrack);\n    lua_setfield(L, -2, \"play_track\");\n    lua_pushcfunction(L, M_L_MusicPlayTrack);\n    lua_setfield(L, -2, \"play\");\n    lua_pushcfunction(L, M_L_MusicPause);\n    lua_setfield(L, -2, \"pause\");\n    lua_pushcfunction(L, M_L_MusicUnpause);\n    lua_setfield(L, -2, \"unpause\");\n    lua_pushcfunction(L, M_L_MusicStop);\n    lua_setfield(L, -2, \"stop\");\n\n    lua_setfield(L, -2, \"music\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/objects.c",
    "content": "#include <trx/game/objects.h>\n\n#include <lauxlib.h>\n\n// trxc.objects.swap_mesh(obj1_id, obj2_id, mesh1_num, mesh2_num)\nstatic int M_L_ObjectsSwapMesh(lua_State *const L)\n{\n    const int32_t arg_count = lua_gettop(L);\n    const OBJECT_ID obj1_id = luaL_checkinteger(L, 1);\n    const OBJECT_ID obj2_id = luaL_checkinteger(L, 2);\n    if (arg_count == 2) {\n        Object_SwapAllMeshes(obj1_id, obj2_id);\n    } else {\n        const int32_t mesh1_num = luaL_checkinteger(L, 3);\n        const int32_t mesh2_num = luaL_checkinteger(L, 4);\n        Object_SwapMeshEx(obj1_id, obj2_id, mesh1_num, mesh2_num);\n    }\n    return 0;\n}\n\nvoid LUA_CreateObjects(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_ObjectsSwapMesh);\n    lua_setfield(L, -2, \"swap_mesh\");\n    lua_setfield(L, -2, \"objects\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/rooms.c",
    "content": "#include <trx/game/items/actions/ids.h>\n#include <trx/game/lua/common.h>\n#include <trx/game/rooms.h>\n\n#include <lauxlib.h>\n\n#define M_ROOM_GETTER(L)                                                       \\\n    const int idx = luaL_checkinteger(L, 1);                                   \\\n    const ROOM *const room = Room_Get(idx - 1);                                \\\n    if (room == nullptr) {                                                     \\\n        lua_pushnil(L);                                                        \\\n        return 1;                                                              \\\n    }\n\n#define M_ROOM_SETTER(L)                                                       \\\n    const int idx = luaL_checkinteger(L, 1);                                   \\\n    ROOM *const room = Room_Get(idx - 1);                                      \\\n    if (room == nullptr) {                                                     \\\n        return 1;                                                              \\\n    }\n\n// trxc.rooms.count() → int\nstatic int M_L_RoomsCount(lua_State *const L)\n{\n    lua_pushinteger(L, Room_GetCount());\n    return 1;\n}\n\n// trxc.rooms.get(index) → int (1-based) or nil\nstatic int M_L_RoomGet(lua_State *const L)\n{\n    const int idx = luaL_checkinteger(L, 1);\n    const ROOM *const room = Room_Get(idx - 1);\n    if (room == nullptr) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, idx);\n    }\n    return 1;\n}\n\n// trxc.rooms.get_underwater(index) → bool or nil\nstatic int M_L_RoomGetUnderwater(lua_State *const L)\n{\n    M_ROOM_GETTER(L);\n    lua_pushboolean(L, room->flags.underwater);\n    return 1;\n}\n\n// trxc.rooms.get_wind(index) → bool or nil\nstatic int M_L_RoomGetWind(lua_State *const L)\n{\n    M_ROOM_GETTER(L);\n    lua_pushboolean(L, room->flags.wind);\n    return 1;\n}\n\n// trxc.rooms.get_flip_status(index) → integer\nstatic int M_L_RoomGetFlipStatus(lua_State *const L)\n{\n    M_ROOM_GETTER(L);\n    lua_pushinteger(L, room->flip_status);\n    return 1;\n}\n\n// trxc.rooms.get_flip_room(index) → integer or nil\nstatic int M_L_RoomGetFlippedRoom(lua_State *const L)\n{\n    M_ROOM_GETTER(L);\n    if (room->flipped_room == NO_ROOM) {\n        lua_pushnil(L);\n    } else {\n        lua_pushinteger(L, room->flipped_room + 1);\n    }\n    return 1;\n}\n\n// trxc.rooms.set_underwater(index, bool)\nstatic int M_L_RoomSetUnderwater(lua_State *const L)\n{\n    M_ROOM_SETTER(L);\n    room->flags.underwater = lua_toboolean(L, 2);\n    return 1;\n}\n\n// trxc.rooms.set_wind(index, bool)\nstatic int M_L_RoomSetWind(lua_State *const L)\n{\n    M_ROOM_SETTER(L);\n    room->flags.wind = lua_toboolean(L, 2);\n    return 1;\n}\n\n// trxc.rooms.get_bounds() → table\nstatic int M_L_RoomGetBounds(lua_State *const L)\n{\n    M_ROOM_GETTER(L);\n    const BOUNDS_32 bounds = Room_GetRoomBounds(Room_Get(idx - 1));\n    lua_newtable(L);\n    lua_pushinteger(L, bounds.min.x);\n    lua_setfield(L, -2, \"min_x\");\n    lua_pushinteger(L, bounds.min.y);\n    lua_setfield(L, -2, \"min_y\");\n    lua_pushinteger(L, bounds.min.z);\n    lua_setfield(L, -2, \"min_z\");\n    lua_pushinteger(L, bounds.max.x);\n    lua_setfield(L, -2, \"max_x\");\n    lua_pushinteger(L, bounds.max.y);\n    lua_setfield(L, -2, \"max_y\");\n    lua_pushinteger(L, bounds.max.z);\n    lua_setfield(L, -2, \"max_z\");\n    return 1;\n}\n\n// trxc.rooms.flip()\nstatic int M_L_RoomFlip(lua_State *const L)\n{\n    Room_FlipMap();\n    return 0;\n}\n\n// trxc.rooms.flip_effect(effect_id, [timer])\nstatic int M_L_RoomFlipEffect(lua_State *const L)\n{\n    const int32_t trx_effect_id = luaL_checkinteger(L, 1);\n    if (trx_effect_id == -1) {\n        Room_SetFlipEffect(-1);\n    } else {\n        const ITEM_ACTION game_id = ItemAction_ToGameID(trx_effect_id);\n        if (game_id == ITEM_ACTION_INVALID) {\n            return luaL_error(L, \"invalid flip effect id\");\n        }\n        Room_SetFlipEffect(game_id);\n    }\n\n    const int arg_count = lua_gettop(L);\n    if (arg_count >= 2) {\n        const int32_t timer = luaL_checkinteger(L, 2);\n        Room_SetFlipTimer(timer);\n    }\n\n    return 0;\n}\n\nvoid LUA_CreateRooms(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n\n    lua_newtable(L);\n    lua_pushinteger(L, RFS_UNFLIPPED);\n    lua_setfield(L, -2, \"UNFLIPPED\");\n    lua_pushinteger(L, RFS_FLIPPED);\n    lua_setfield(L, -2, \"FLIPPED\");\n    lua_pushinteger(L, RFS_NONE);\n    lua_setfield(L, -2, \"NONE\");\n    lua_setfield(L, -2, \"FlipStatus\");\n\n    lua_pushcfunction(L, M_L_RoomsCount);\n    lua_setfield(L, -2, \"count\");\n    lua_pushcfunction(L, M_L_RoomGet);\n    lua_setfield(L, -2, \"get\");\n    lua_pushcfunction(L, M_L_RoomGetUnderwater);\n    lua_setfield(L, -2, \"get_underwater\");\n    lua_pushcfunction(L, M_L_RoomGetWind);\n    lua_setfield(L, -2, \"get_wind\");\n    lua_pushcfunction(L, M_L_RoomSetUnderwater);\n    lua_setfield(L, -2, \"set_underwater\");\n    lua_pushcfunction(L, M_L_RoomSetWind);\n    lua_setfield(L, -2, \"set_wind\");\n    lua_pushcfunction(L, M_L_RoomGetBounds);\n    lua_setfield(L, -2, \"get_bounds\");\n    lua_pushcfunction(L, M_L_RoomGetFlipStatus);\n    lua_setfield(L, -2, \"get_flip_status\");\n    lua_pushcfunction(L, M_L_RoomGetFlippedRoom);\n    lua_setfield(L, -2, \"get_flipped_room\");\n    lua_pushcfunction(L, M_L_RoomFlip);\n    lua_setfield(L, -2, \"flip\");\n    lua_pushcfunction(L, M_L_RoomFlipEffect);\n    lua_setfield(L, -2, \"flip_effect\");\n    lua_setfield(L, -2, \"rooms\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua/sound.c",
    "content": "#include <trx/game/lua/common.h>\n#include <trx/game/sound/common.h>\n\n#include <lauxlib.h>\n\n// trxc.sound.is_available(id)\nstatic int M_L_SoundIsAvailable(lua_State *const L)\n{\n    const SAMPLE_ID id = (SAMPLE_ID)luaL_checkinteger(L, 1);\n    lua_pushboolean(L, Sound_IsAvailable_Direct(id));\n    return 1;\n}\n\n// trxc.sound.play(id[, opts])\nstatic int M_L_SoundPlay(lua_State *const L)\n{\n    const SAMPLE_ID id = (SAMPLE_ID)luaL_checkinteger(L, 1);\n    if (!Sound_IsAvailable_Direct(id)) {\n        return luaL_error(L, \"invalid sound track: %d\", (int)id);\n    }\n    XYZ_32 pos;\n    const XYZ_32 *pos_ptr = nullptr;\n    if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {\n        lua_getfield(L, 2, \"pos\");\n        if (lua_istable(L, -1)) {\n            lua_getfield(L, -1, \"x\");\n            pos.x = (int32_t)luaL_optinteger(L, -1, 0);\n            lua_pop(L, 1);\n            lua_getfield(L, -1, \"y\");\n            pos.y = (int32_t)luaL_optinteger(L, -1, 0);\n            lua_pop(L, 1);\n            lua_getfield(L, -1, \"z\");\n            pos.z = (int32_t)luaL_optinteger(L, -1, 0);\n            lua_pop(L, 1);\n            pos_ptr = &pos;\n        }\n        lua_pop(L, 1);\n    }\n    Sound_Effect_Direct(id, pos_ptr, SPM_ALWAYS | SPM_STATIC_POS);\n    return 0;\n}\n\n// trxc.sound.stop(id)\nstatic int M_L_SoundStop(lua_State *const L)\n{\n    const SAMPLE_ID id = (SAMPLE_ID)luaL_checkinteger(L, 1);\n    Sound_StopEffect_Direct(id);\n    return 0;\n}\n\n// trxc.sound.stop_all()\nstatic int M_L_SoundStopAll(lua_State *const L)\n{\n    Sound_StopAll();\n    return 0;\n}\n\nvoid LUA_CreateSound(lua_State *const L)\n{\n    lua_getglobal(L, \"trxc\");\n    lua_newtable(L);\n    lua_pushcfunction(L, M_L_SoundIsAvailable);\n    lua_setfield(L, -2, \"is_available\");\n    lua_pushcfunction(L, M_L_SoundPlay);\n    lua_setfield(L, -2, \"play\");\n    lua_pushcfunction(L, M_L_SoundStop);\n    lua_setfield(L, -2, \"stop\");\n    lua_pushcfunction(L, M_L_SoundStopAll);\n    lua_setfield(L, -2, \"stop_all\");\n    lua_setfield(L, -2, \"sound\");\n    lua_pop(L, 1);\n}\n"
  },
  {
    "path": "src/trx/game/lua.h",
    "content": "#pragma once\n\n#include <trx/game/lua/common.h>\n#include <trx/game/lua/events.h>\n"
  },
  {
    "path": "src/trx/game/matrix.c",
    "content": "#include <trx/game/matrix.h>\n\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n\n#include <float.h>\n#include <math.h>\n\n#define MAX_MATRICES 40\n#define MAX_NESTED_MATRICES 32\n\nstatic MATRIX m_MatrixStack[MAX_MATRICES] = {};\nstatic MATRIX m_WMatrixStack[MAX_MATRICES] = {};\nstatic int32_t m_IMRate = 0;\nstatic int32_t m_IMFrac = 0;\nstatic MATRIX *m_IMMatrixPtr = nullptr;\nstatic MATRIX *m_WIMMatrixPtr = nullptr;\nstatic MATRIX m_IMMatrixStack[MAX_NESTED_MATRICES] = {};\nstatic MATRIX m_WIMMatrixStack[MAX_NESTED_MATRICES] = {};\n\nMATRIX *g_MatrixPtr = &m_MatrixStack[0];\nMATRIX *g_WMatrixPtr = &m_WMatrixStack[0];\n\nXYZ_32 g_ViewPos = {};\nMATRIX g_ViewMatrix = {};\nMATRIX g_IDMatrix = {\n    // clang-format off\n    ._00 = 1 << W2V_SHIFT, ._01 = 0, ._02 = 0, ._03 = 0,\n    ._10 = 0, ._11 = 1 << W2V_SHIFT, ._12 = 0, ._13 = 0,\n    ._20 = 0, ._21 = 0, ._22 = 1 << W2V_SHIFT, ._23 = 0,\n    // clang-format on\n};\n\nstatic inline void M_QuaternionNormalize(QUATERNION *q)\n{\n    const double n2 = q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w;\n    if (n2 > 0.0) {\n        const double inv = 1.0 / sqrt(n2);\n        q->x *= inv;\n        q->y *= inv;\n        q->z *= inv;\n        q->w *= inv;\n    } else {\n        // fallback: identity\n        q->x = q->y = q->z = 0.0;\n        q->w = 1.0;\n    }\n}\n\n// One inexpensive polar-decomposition iteration to orthonormalize R (3x3).\n// R <- R * (3I - R^T R) / 2  (Newton step toward orthogonal)\n// Works great if R is already close to rotation.\nstatic void M_Double3x3Ortho(double r[3][3])\n{\n    double rt_r[3][3] = {};\n    for (int32_t i = 0; i < 3; i++) {\n        for (int32_t j = 0; j < 3; j++) {\n            for (int32_t k = 0; k < 3; k++) {\n                rt_r[i][j] += r[k][i] * r[k][j];\n            }\n        }\n    }\n\n    double m[3][3];\n    for (int32_t i = 0; i < 3; i++) {\n        for (int32_t j = 0; j < 3; j++) {\n            m[i][j] = 3.0 * (i == j) - rt_r[i][j];\n        }\n    }\n\n    double rn[3][3] = {};\n    for (int32_t i = 0; i < 3; i++) {\n        for (int32_t j = 0; j < 3; j++) {\n            for (int32_t k = 0; k < 3; k++) {\n                rn[i][j] += r[i][k] * m[k][j];\n            }\n        }\n    }\n\n    // divide by 2\n    for (int32_t i = 0; i < 3; i++) {\n        for (int32_t j = 0; j < 3; j++) {\n            r[i][j] = 0.5 * rn[i][j];\n        }\n    }\n}\n\n// Extract 3x3 rotation (in doubles) from fixed-point MATRIX.\nstatic void M_Double3x3FromMatrix(const MATRIX *const m, double e[3][3])\n{\n    const double s = (1 << W2V_SHIFT);\n    e[0][0] = m->_00 / s;\n    e[0][1] = m->_01 / s;\n    e[0][2] = m->_02 / s;\n    e[1][0] = m->_10 / s;\n    e[1][1] = m->_11 / s;\n    e[1][2] = m->_12 / s;\n    e[2][0] = m->_20 / s;\n    e[2][1] = m->_21 / s;\n    e[2][2] = m->_22 / s;\n}\n\n// Remove uniform scale if present (estimate from row lengths).\n// Use average row length to reduce noise.\nstatic void M_Double3x3RemoveScale(double e[3][3])\n{\n    const double rl0 =\n        sqrt(e[0][0] * e[0][0] + e[0][1] * e[0][1] + e[0][2] * e[0][2]);\n    const double rl1 =\n        sqrt(e[1][0] * e[1][0] + e[1][1] * e[1][1] + e[1][2] * e[1][2]);\n    const double rl2 =\n        sqrt(e[2][0] * e[2][0] + e[2][1] * e[2][1] + e[2][2] * e[2][2]);\n    const double scale = (rl0 + rl1 + rl2) / 3.0;\n    if (scale <= 0.0) {\n        return;\n    }\n    const double inv = 1.0 / scale;\n    for (int32_t i = 0; i < 3; i++) {\n        for (int32_t j = 0; j < 3; j++) {\n            e[i][j] *= inv;\n        }\n    }\n}\n\n// Write 3x3 back to MATRIX as fixed-point, with rounding.\nstatic void M_Double3x3ToMatrix(const double e[3][3], MATRIX *m)\n{\n    const double s = (double)(1 << W2V_SHIFT);\n    m->_00 = (int32_t)llround(e[0][0] * s);\n    m->_01 = (int32_t)llround(e[0][1] * s);\n    m->_02 = (int32_t)llround(e[0][2] * s);\n    m->_10 = (int32_t)llround(e[1][0] * s);\n    m->_11 = (int32_t)llround(e[1][1] * s);\n    m->_12 = (int32_t)llround(e[1][2] * s);\n    m->_20 = (int32_t)llround(e[2][0] * s);\n    m->_21 = (int32_t)llround(e[2][1] * s);\n    m->_22 = (int32_t)llround(e[2][2] * s);\n}\n\nstatic void M_MatrixToQuaternion(const MATRIX *m, QUATERNION *q)\n{\n    double e[3][3];\n    M_Double3x3FromMatrix(m, e);\n    M_Double3x3RemoveScale(e);\n    // Orthonormalize (fast, one Newton step is usually enough).\n    M_Double3x3Ortho(e);\n\n    const double tr = e[0][0] + e[1][1] + e[2][2];\n    if (tr > 0.0) {\n        const double s = sqrt(tr + 1.0) * 2.0; // 4*w\n        q->w = 0.25 * s;\n        q->x = (e[2][1] - e[1][2]) / s;\n        q->y = (e[0][2] - e[2][0]) / s;\n        q->z = (e[1][0] - e[0][1]) / s;\n    } else {\n        // Pick the biggest diagonal for numerical stability\n        int32_t i = 0;\n        if (e[1][1] > e[0][0]) {\n            i = 1;\n        }\n        if (e[2][2] > e[i][i]) {\n            i = 2;\n        }\n        const int32_t j = (i + 1) % 3;\n        const int32_t k = (i + 2) % 3;\n        const double s = sqrt(e[i][i] - e[j][j] - e[k][k] + 1.0) * 2.0;\n        double qv[3];\n        qv[i] = 0.25 * s;\n        qv[j] = (e[j][i] + e[i][j]) / s;\n        qv[k] = (e[k][i] + e[i][k]) / s;\n        q->x = qv[0];\n        q->y = qv[1];\n        q->z = qv[2];\n        q->w = (e[k][j] - e[j][k]) / s;\n    }\n    M_QuaternionNormalize(q);\n}\n\nstatic void M_MatrixFromQuaternion(const QUATERNION *const qin, MATRIX *const m)\n{\n    QUATERNION q = *qin;\n    M_QuaternionNormalize(&q);\n\n    const double xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z;\n    const double xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z;\n    const double wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z;\n\n    double e[3][3];\n    e[0][0] = 1.0 - 2.0 * (yy + zz);\n    e[0][1] = 2.0 * (xy - wz);\n    e[0][2] = 2.0 * (xz + wy);\n    e[1][0] = 2.0 * (xy + wz);\n    e[1][1] = 1.0 - 2.0 * (xx + zz);\n    e[1][2] = 2.0 * (yz - wx);\n    e[2][0] = 2.0 * (xz - wy);\n    e[2][1] = 2.0 * (yz + wx);\n    e[2][2] = 1.0 - 2.0 * (xx + yy);\n\n    // Optional: one more orthonormalization step to crush rounding noise\n    M_Double3x3Ortho(e);\n    M_Double3x3ToMatrix(e, m);\n}\n\nstatic void M_QuaternionSlerp(\n    const QUATERNION *const qa, const QUATERNION *const qb, const double t,\n    QUATERNION *const out)\n{\n    QUATERNION a = *qa, b = *qb;\n    M_QuaternionNormalize(&a);\n    M_QuaternionNormalize(&b);\n\n    double cosom = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;\n    if (cosom < 0.0) { // take shortest path\n        cosom = -cosom;\n        b.x = -b.x;\n        b.y = -b.y;\n        b.z = -b.z;\n        b.w = -b.w;\n    }\n\n    // Guard acos input, and use nlerp for tiny angles\n    CLAMP(cosom, -1.0, 1.0);\n\n    // Threshold tuned for double precision\n    const double EPS = 1e-12;\n    double scale0;\n    double scale1;\n    if (1.0 - cosom > EPS) {\n        const double omega = acos(cosom);\n        const double sinom = 1.0 / sin(omega);\n        scale0 = sin((1.0 - t) * omega) * sinom;\n        scale1 = sin(t * omega) * sinom;\n    } else {\n        // Nearly parallel: nlerp, then normalize\n        scale0 = 1.0 - t;\n        scale1 = t;\n    }\n\n    out->x = scale0 * a.x + scale1 * b.x;\n    out->y = scale0 * a.y + scale1 * b.y;\n    out->z = scale0 * a.z + scale1 * b.z;\n    out->w = scale0 * a.w + scale1 * b.w;\n    M_QuaternionNormalize(out);\n}\n\nstatic void M_ScaleX(MATRIX *const m, const int32_t scale)\n{\n    m->_00 = ((int64_t)m->_00 * scale) >> W2V_SHIFT;\n    m->_10 = ((int64_t)m->_10 * scale) >> W2V_SHIFT;\n    m->_20 = ((int64_t)m->_20 * scale) >> W2V_SHIFT;\n}\n\nstatic void M_ScaleY(MATRIX *const m, const int32_t scale)\n{\n    m->_01 = ((int64_t)m->_01 * scale) >> W2V_SHIFT;\n    m->_11 = ((int64_t)m->_11 * scale) >> W2V_SHIFT;\n    m->_21 = ((int64_t)m->_21 * scale) >> W2V_SHIFT;\n}\n\nstatic void M_ScaleZ(MATRIX *const m, const int32_t angle)\n{\n    m->_02 = ((int64_t)m->_02 * angle) >> W2V_SHIFT;\n    m->_12 = ((int64_t)m->_12 * angle) >> W2V_SHIFT;\n    m->_22 = ((int64_t)m->_22 * angle) >> W2V_SHIFT;\n}\n\nstatic void M_RotX(MATRIX *const m, const int16_t angle)\n{\n    if (angle == 0) {\n        return;\n    }\n    const int32_t sx = Math_Sin(angle);\n    const int32_t cx = Math_Cos(angle);\n\n    int32_t r0, r1;\n    r0 = m->_01 * cx + m->_02 * sx;\n    r1 = m->_02 * cx - m->_01 * sx;\n    m->_01 = r0 >> W2V_SHIFT;\n    m->_02 = r1 >> W2V_SHIFT;\n\n    r0 = m->_11 * cx + m->_12 * sx;\n    r1 = m->_12 * cx - m->_11 * sx;\n    m->_11 = r0 >> W2V_SHIFT;\n    m->_12 = r1 >> W2V_SHIFT;\n\n    r0 = m->_21 * cx + m->_22 * sx;\n    r1 = m->_22 * cx - m->_21 * sx;\n    m->_21 = r0 >> W2V_SHIFT;\n    m->_22 = r1 >> W2V_SHIFT;\n}\n\nstatic void M_RotY(MATRIX *const m, const int16_t angle)\n{\n    if (angle == 0) {\n        return;\n    }\n\n    const int32_t sy = Math_Sin(angle);\n    const int32_t cy = Math_Cos(angle);\n\n    int32_t r0, r1;\n    r0 = m->_00 * cy - m->_02 * sy;\n    r1 = m->_02 * cy + m->_00 * sy;\n    m->_00 = r0 >> W2V_SHIFT;\n    m->_02 = r1 >> W2V_SHIFT;\n\n    r0 = m->_10 * cy - m->_12 * sy;\n    r1 = m->_12 * cy + m->_10 * sy;\n    m->_10 = r0 >> W2V_SHIFT;\n    m->_12 = r1 >> W2V_SHIFT;\n\n    r0 = m->_20 * cy - m->_22 * sy;\n    r1 = m->_22 * cy + m->_20 * sy;\n    m->_20 = r0 >> W2V_SHIFT;\n    m->_22 = r1 >> W2V_SHIFT;\n}\n\nstatic void M_RotZ(MATRIX *const m, const int16_t angle)\n{\n    if (angle == 0) {\n        return;\n    }\n\n    const int32_t sz = Math_Sin(angle);\n    const int32_t cz = Math_Cos(angle);\n\n    int32_t r0, r1;\n    r0 = m->_00 * cz + m->_01 * sz;\n    r1 = m->_01 * cz - m->_00 * sz;\n    m->_00 = r0 >> W2V_SHIFT;\n    m->_01 = r1 >> W2V_SHIFT;\n\n    r0 = m->_10 * cz + m->_11 * sz;\n    r1 = m->_11 * cz - m->_10 * sz;\n    m->_10 = r0 >> W2V_SHIFT;\n    m->_11 = r1 >> W2V_SHIFT;\n\n    r0 = m->_20 * cz + m->_21 * sz;\n    r1 = m->_21 * cz - m->_20 * sz;\n    m->_20 = r0 >> W2V_SHIFT;\n    m->_21 = r1 >> W2V_SHIFT;\n}\n\nstatic void M_RotYXZ(MATRIX *const m, const XYZ_16 rotation)\n{\n    M_RotY(m, rotation.y);\n    M_RotX(m, rotation.x);\n    M_RotZ(m, rotation.z);\n}\n\nstatic void M_TranslateRel(MATRIX *const m, const XYZ_32 offset)\n{\n    m->_03 += offset.x * m->_00 + offset.y * m->_01 + offset.z * m->_02;\n    m->_13 += offset.x * m->_10 + offset.y * m->_11 + offset.z * m->_12;\n    m->_23 += offset.x * m->_20 + offset.y * m->_21 + offset.z * m->_22;\n}\n\nstatic void M_TranslateSet(MATRIX *const m, const XYZ_32 pos)\n{\n    const int64_t scale = (int64_t)(1 << W2V_SHIFT);\n    m->_03 = (int64_t)pos.x * scale;\n    m->_13 = (int64_t)pos.y * scale;\n    m->_23 = (int64_t)pos.z * scale;\n}\n\nvoid Matrix_Mul3x3_M(\n    MATRIX *const out, const MATRIX *const lhs, const MATRIX *const rhs)\n{\n#define L_MUL(r_, c_)                                                          \\\n    (((lhs->_##r_##0 * rhs->_0##c_) + (lhs->_##r_##1 * rhs->_1##c_)            \\\n      + (lhs->_##r_##2 * rhs->_2##c_))                                         \\\n     >> W2V_SHIFT)\n\n    const int64_t r00 = L_MUL(0, 0);\n    const int64_t r01 = L_MUL(0, 1);\n    const int64_t r02 = L_MUL(0, 2);\n    const int64_t r10 = L_MUL(1, 0);\n    const int64_t r11 = L_MUL(1, 1);\n    const int64_t r12 = L_MUL(1, 2);\n    const int64_t r20 = L_MUL(2, 0);\n    const int64_t r21 = L_MUL(2, 1);\n    const int64_t r22 = L_MUL(2, 2);\n\n    out->_00 = r00;\n    out->_01 = r01;\n    out->_02 = r02;\n    out->_10 = r10;\n    out->_11 = r11;\n    out->_12 = r12;\n    out->_20 = r20;\n    out->_21 = r21;\n    out->_22 = r22;\n\n#undef L_MUL\n}\n\nstatic void M_InterpolateArm(MATRIX *const m, const MATRIX *const mi)\n{\n    m->_00 = m[-2]._00;\n    m->_01 = m[-2]._01;\n    m->_02 = m[-2]._02;\n    m->_03 += ((mi->_03 - m->_03) * m_IMFrac) / m_IMRate;\n    m->_10 = m[-2]._10;\n    m->_11 = m[-2]._11;\n    m->_12 = m[-2]._12;\n    m->_13 += ((mi->_13 - m->_13) * m_IMFrac) / m_IMRate;\n    m->_20 = m[-2]._20;\n    m->_21 = m[-2]._21;\n    m->_22 = m[-2]._22;\n    m->_23 += ((mi->_23 - m->_23) * m_IMFrac) / m_IMRate;\n}\n\nstatic void M_Interpolate(\n    const MATRIX *const m1, const MATRIX *const m2, MATRIX *const result)\n{\n    double rate = (m_IMRate != 0) ? ((double)m_IMFrac / (double)m_IMRate) : 0.0;\n    CLAMP(rate, 0.0, 1.0);\n\n    QUATERNION q1, q2, q;\n    M_MatrixToQuaternion(m1, &q1);\n    M_MatrixToQuaternion(m2, &q2);\n    M_QuaternionSlerp(&q1, &q2, rate, &q);\n    M_MatrixFromQuaternion(&q, result);\n\n    result->_03 = (int32_t)llround(m1->_03 + (m2->_03 - m1->_03) * rate);\n    result->_13 = (int32_t)llround(m1->_13 + (m2->_13 - m1->_13) * rate);\n    result->_23 = (int32_t)llround(m1->_23 + (m2->_23 - m1->_23) * rate);\n}\n\nvoid Matrix_ResetStack(void)\n{\n    g_MatrixPtr = &m_MatrixStack[0];\n    g_WMatrixPtr = &m_WMatrixStack[0];\n}\n\nvoid Matrix_GenerateW2V(const XYZ_32 *pos, const XYZ_16 *rot)\n{\n    const int32_t sx = Math_Sin(rot->x);\n    const int32_t cx = Math_Cos(rot->x);\n    const int32_t sy = Math_Sin(rot->y);\n    const int32_t cy = Math_Cos(rot->y);\n    const int32_t sz = Math_Sin(rot->z);\n    const int32_t cz = Math_Cos(rot->z);\n\n    g_ViewPos = *pos;\n    g_ViewMatrix._00 = TRIGMULT3(sx, sy, sz) + TRIGMULT2(cy, cz);\n    g_ViewMatrix._01 = TRIGMULT2(cx, sz);\n    g_ViewMatrix._02 = TRIGMULT3(sx, cy, sz) - TRIGMULT2(sy, cz);\n    g_ViewMatrix._10 = TRIGMULT3(sx, sy, cz) - TRIGMULT2(cy, sz);\n    g_ViewMatrix._11 = TRIGMULT2(cx, cz);\n    g_ViewMatrix._12 = TRIGMULT3(sx, cy, cz) + TRIGMULT2(sy, sz);\n    g_ViewMatrix._20 = TRIGMULT2(cx, sy);\n    g_ViewMatrix._21 = -sx;\n    g_ViewMatrix._22 = TRIGMULT2(cx, cy);\n    g_ViewMatrix._03 = 0;\n    g_ViewMatrix._13 = 0;\n    g_ViewMatrix._23 = 0;\n    M_TranslateRel(&g_ViewMatrix, (XYZ_32) { -pos->x, -pos->y, -pos->z });\n\n    g_MatrixPtr = &m_MatrixStack[0];\n    m_MatrixStack[0] = g_ViewMatrix;\n\n    g_WMatrixPtr = &m_WMatrixStack[0];\n    g_WMatrixPtr[0] = g_IDMatrix;\n}\n\nbool Matrix_Push(void)\n{\n    if (g_MatrixPtr + 1 - m_MatrixStack >= MAX_MATRICES) {\n        return false;\n    }\n    if (g_WMatrixPtr + 1 - m_WMatrixStack >= MAX_MATRICES) {\n        return false;\n    }\n    g_MatrixPtr++;\n    g_MatrixPtr[0] = g_MatrixPtr[-1];\n    g_WMatrixPtr++;\n    g_WMatrixPtr[0] = g_WMatrixPtr[-1];\n    return true;\n}\n\nbool Matrix_PushUnit(void)\n{\n    if (g_MatrixPtr + 1 - m_MatrixStack >= MAX_MATRICES) {\n        return false;\n    }\n    if (g_WMatrixPtr + 1 - m_WMatrixStack >= MAX_MATRICES) {\n        return false;\n    }\n    g_MatrixPtr++;\n    *g_MatrixPtr = g_IDMatrix;\n    g_WMatrixPtr++;\n    *g_WMatrixPtr = g_IDMatrix;\n    return true;\n}\n\nvoid Matrix_Pop(void)\n{\n    g_MatrixPtr--;\n    g_WMatrixPtr--;\n}\n\nvoid Matrix_Scale(const int32_t scale)\n{\n    Matrix_ScaleX(scale);\n    Matrix_ScaleY(scale);\n    Matrix_ScaleZ(scale);\n}\n\nvoid Matrix_ScaleX(const int32_t scale)\n{\n    M_ScaleX(g_MatrixPtr, scale);\n    M_ScaleX(g_WMatrixPtr, scale);\n}\n\nvoid Matrix_ScaleY(const int32_t scale)\n{\n    M_ScaleY(g_MatrixPtr, scale);\n    M_ScaleY(g_WMatrixPtr, scale);\n}\n\nvoid Matrix_ScaleZ(const int32_t scale)\n{\n    M_ScaleZ(g_MatrixPtr, scale);\n    M_ScaleZ(g_WMatrixPtr, scale);\n}\n\nvoid Matrix_RotX(const int16_t angle)\n{\n    M_RotX(g_MatrixPtr, angle);\n    M_RotX(g_WMatrixPtr, angle);\n}\n\nvoid Matrix_RotY(const int16_t angle)\n{\n    M_RotY(g_MatrixPtr, angle);\n    M_RotY(g_WMatrixPtr, angle);\n}\n\nvoid Matrix_RotZ(const int16_t angle)\n{\n    M_RotZ(g_MatrixPtr, angle);\n    M_RotZ(g_WMatrixPtr, angle);\n}\n\nvoid Matrix_Rot16(const XYZ_16 rotation)\n{\n    M_RotYXZ(g_MatrixPtr, rotation);\n    M_RotYXZ(g_WMatrixPtr, rotation);\n}\n\nvoid Matrix_RotX_M(MATRIX *const m, const int16_t angle)\n{\n    M_RotX(m, angle);\n}\n\nvoid Matrix_RotY_M(MATRIX *const m, const int16_t angle)\n{\n    M_RotY(m, angle);\n}\n\nvoid Matrix_RotZ_M(MATRIX *const m, const int16_t angle)\n{\n    M_RotZ(m, angle);\n}\n\nvoid Matrix_Slerp3x3_M(\n    MATRIX *const lhs_out, const MATRIX *const rhs, const double t)\n{\n    QUATERNION q1, q2, q;\n    M_MatrixToQuaternion(lhs_out, &q1);\n    M_MatrixToQuaternion(rhs, &q2);\n\n    double clamped_t = t;\n    CLAMP(clamped_t, 0.0, 1.0);\n    M_QuaternionSlerp(&q1, &q2, clamped_t, &q);\n    M_MatrixFromQuaternion(&q, lhs_out);\n}\n\nvoid Matrix_Mul3x3(const MATRIX *const rhs)\n{\n    Matrix_Mul3x3_M(g_MatrixPtr, g_MatrixPtr, rhs);\n    Matrix_Mul3x3_M(g_WMatrixPtr, g_WMatrixPtr, rhs);\n}\n\nvoid Matrix_TranslateRel(const int32_t dx, const int32_t dy, const int32_t dz)\n{\n    Matrix_TranslateRel32((XYZ_32) { dx, dy, dz });\n}\n\nvoid Matrix_TranslateRel16(const XYZ_16 offset)\n{\n    Matrix_TranslateRel32(XYZ_32_From16(offset));\n}\n\nvoid Matrix_TranslateRel32(const XYZ_32 offset)\n{\n    M_TranslateRel(g_MatrixPtr, offset);\n    M_TranslateRel(g_WMatrixPtr, offset);\n}\n\nvoid Matrix_TranslateAbs(const int32_t x, const int32_t y, const int32_t z)\n{\n    MATRIX *const m = g_MatrixPtr;\n    const MATRIX *const v = &g_ViewMatrix;\n    m->_03 = x * v->_00 + y * v->_01 + z * v->_02 + v->_03;\n    m->_13 = x * v->_10 + y * v->_11 + z * v->_12 + v->_13;\n    m->_23 = x * v->_20 + y * v->_21 + z * v->_22 + v->_23;\n    M_TranslateSet(g_WMatrixPtr, (XYZ_32) { x, y, z });\n}\n\nvoid Matrix_TranslateAbs16(const XYZ_16 offset)\n{\n    Matrix_TranslateAbs(offset.x, offset.y, offset.z);\n}\n\nvoid Matrix_TranslateAbs32(const XYZ_32 offset)\n{\n    Matrix_TranslateAbs(offset.x, offset.y, offset.z);\n}\n\nvoid Matrix_TranslateSet32(const XYZ_32 origin)\n{\n    M_TranslateSet(g_MatrixPtr, origin);\n    M_TranslateSet(g_WMatrixPtr, origin);\n}\n\nvoid Matrix_TranslateSet32_M(MATRIX *const m, const XYZ_32 origin)\n{\n    M_TranslateSet(m, origin);\n}\n\nvoid Matrix_InitInterpolate(const int32_t frac, const int32_t rate)\n{\n    m_IMFrac = frac;\n    m_IMRate = rate;\n    m_IMMatrixPtr = &m_IMMatrixStack[0];\n    *m_IMMatrixPtr = *g_MatrixPtr;\n    m_WIMMatrixPtr = &m_WIMMatrixStack[0];\n    *m_WIMMatrixPtr = *g_WMatrixPtr;\n}\n\nvoid Matrix_Interpolate(void)\n{\n    M_Interpolate(g_MatrixPtr, m_IMMatrixPtr, g_MatrixPtr);\n    M_Interpolate(g_WMatrixPtr, m_WIMMatrixPtr, g_WMatrixPtr);\n}\n\nvoid Matrix_InterpolateArm(void)\n{\n    M_InterpolateArm(g_MatrixPtr, m_IMMatrixPtr);\n    M_InterpolateArm(g_WMatrixPtr, m_WIMMatrixPtr);\n}\n\nvoid Matrix_Push_I(void)\n{\n    Matrix_Push();\n    m_IMMatrixPtr[1] = m_IMMatrixPtr[0];\n    m_IMMatrixPtr++;\n    m_WIMMatrixPtr[1] = m_WIMMatrixPtr[0];\n    m_WIMMatrixPtr++;\n}\n\nvoid Matrix_Pop_I(void)\n{\n    Matrix_Pop();\n    m_IMMatrixPtr--;\n    m_WIMMatrixPtr--;\n}\n\nvoid Matrix_TranslateRel_I(const int32_t x, const int32_t y, const int32_t z)\n{\n    Matrix_TranslateRel32_I((XYZ_32) { x, y, z });\n}\n\nvoid Matrix_TranslateRel16_I(const XYZ_16 offset)\n{\n    Matrix_TranslateRel32_I(XYZ_32_From16(offset));\n}\n\nvoid Matrix_TranslateRel32_I(const XYZ_32 offset)\n{\n    M_TranslateRel(g_MatrixPtr, offset);\n    M_TranslateRel(g_WMatrixPtr, offset);\n    M_TranslateRel(m_IMMatrixPtr, offset);\n    M_TranslateRel(m_WIMMatrixPtr, offset);\n}\n\nvoid Matrix_TranslateRel_ID(\n    const int32_t x, const int32_t y, const int32_t z, const int32_t x2,\n    const int32_t y2, const int32_t z2)\n{\n    Matrix_TranslateRel32_ID((XYZ_32) { x, y, z }, (XYZ_32) { x2, y2, z2 });\n}\n\nvoid Matrix_TranslateRel16_ID(const XYZ_16 offset_1, const XYZ_16 offset_2)\n{\n    Matrix_TranslateRel32_ID(XYZ_32_From16(offset_1), XYZ_32_From16(offset_2));\n}\n\nvoid Matrix_TranslateRel32_ID(const XYZ_32 offset_1, const XYZ_32 offset_2)\n{\n    M_TranslateRel(g_MatrixPtr, offset_1);\n    M_TranslateRel(g_WMatrixPtr, offset_1);\n    M_TranslateRel(m_IMMatrixPtr, offset_2);\n    M_TranslateRel(m_WIMMatrixPtr, offset_2);\n}\n\nvoid Matrix_RotY_I(const int16_t angle)\n{\n    M_RotY(g_MatrixPtr, angle);\n    M_RotY(g_WMatrixPtr, angle);\n    M_RotY(m_IMMatrixPtr, angle);\n    M_RotY(m_WIMMatrixPtr, angle);\n}\n\nvoid Matrix_RotX_I(const int16_t angle)\n{\n    M_RotX(g_MatrixPtr, angle);\n    M_RotX(g_WMatrixPtr, angle);\n    M_RotX(m_IMMatrixPtr, angle);\n    M_RotX(m_WIMMatrixPtr, angle);\n}\n\nvoid Matrix_RotZ_I(const int16_t angle)\n{\n    M_RotZ(g_MatrixPtr, angle);\n    M_RotZ(g_WMatrixPtr, angle);\n    M_RotZ(m_IMMatrixPtr, angle);\n    M_RotZ(m_WIMMatrixPtr, angle);\n}\n\nvoid Matrix_Rot16_I(const XYZ_16 rotation)\n{\n    M_RotYXZ(g_MatrixPtr, rotation);\n    M_RotYXZ(g_WMatrixPtr, rotation);\n    M_RotYXZ(m_IMMatrixPtr, rotation);\n    M_RotYXZ(m_WIMMatrixPtr, rotation);\n}\n\nvoid Matrix_Rot16_ID(const XYZ_16 rotation_1, const XYZ_16 rotation_2)\n{\n    M_RotYXZ(g_MatrixPtr, rotation_1);\n    M_RotYXZ(g_WMatrixPtr, rotation_1);\n    M_RotYXZ(m_IMMatrixPtr, rotation_2);\n    M_RotYXZ(m_WIMMatrixPtr, rotation_2);\n}\n\nvoid Matrix_LookAt(\n    const int32_t source_x, const int32_t source_y, const int32_t source_z,\n    const int32_t target_x, const int32_t target_y, const int32_t target_z,\n    const int16_t roll)\n{\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        target_x - source_x, target_y - source_y, target_z - source_z, angles);\n\n    const XYZ_32 view_pos = {\n        .x = source_x,\n        .y = source_y,\n        .z = source_z,\n    };\n    const XYZ_16 view_rot = {\n        .x = angles[1],\n        .y = angles[0],\n        .z = roll,\n    };\n    Matrix_GenerateW2V(&view_pos, &view_rot);\n}\n\nXYZ_32 Matrix_MulVec32_M(const MATRIX *const m, const XYZ_32 v)\n{\n    return (XYZ_32) {\n        (m->_00 * v.x + m->_01 * v.y + m->_02 * v.z + m->_03) >> W2V_SHIFT,\n        (m->_10 * v.x + m->_11 * v.y + m->_12 * v.z + m->_13) >> W2V_SHIFT,\n        (m->_20 * v.x + m->_21 * v.y + m->_22 * v.z + m->_23) >> W2V_SHIFT,\n    };\n}\n\nXYZ_32 Matrix_GetOffset_M(const MATRIX *const m)\n{\n    return (XYZ_32) {\n        .x = m->_03 >> W2V_SHIFT,\n        .y = m->_13 >> W2V_SHIFT,\n        .z = m->_23 >> W2V_SHIFT,\n    };\n}\n"
  },
  {
    "path": "src/trx/game/matrix.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n\n#define TRIGMULT2(A, B) (((A) * (B)) >> W2V_SHIFT)\n#define TRIGMULT3(A, B, C) (TRIGMULT2((TRIGMULT2(A, B)), C))\n\ntypedef struct QUATERNION {\n    double x;\n    double y;\n    double z;\n    double w;\n} QUATERNION;\n\ntypedef struct {\n    int64_t _00, _01, _02, _03, _10, _11, _12, _13, _20, _21, _22, _23;\n} MATRIX;\n\nextern MATRIX *g_MatrixPtr;\nextern MATRIX *g_WMatrixPtr;\nextern XYZ_32 g_ViewPos;\nextern MATRIX g_ViewMatrix;\nextern MATRIX g_IDMatrix;\n\nvoid Matrix_ResetStack(void);\n\nvoid Matrix_GenerateW2V(const XYZ_32 *pos, const XYZ_16 *rot);\nvoid Matrix_LookAt(\n    int32_t xsrc, int32_t ysrc, int32_t zsrc, int32_t xtar, int32_t ytar,\n    int32_t ztar, int16_t roll);\n\nbool Matrix_Push(void);\nbool Matrix_PushUnit(void);\nvoid Matrix_Pop(void);\n\nvoid Matrix_Scale(int32_t scale);\nvoid Matrix_ScaleX(int32_t sx);\nvoid Matrix_ScaleY(int32_t sy);\nvoid Matrix_ScaleZ(int32_t sz);\n\nvoid Matrix_RotX(int16_t rx);\nvoid Matrix_RotY(int16_t ry);\nvoid Matrix_RotZ(int16_t rz);\nvoid Matrix_Rot16(XYZ_16 rotation);\nvoid Matrix_RotX_M(MATRIX *m, int16_t rx);\nvoid Matrix_RotY_M(MATRIX *m, int16_t ry);\nvoid Matrix_RotZ_M(MATRIX *m, int16_t rz);\nvoid Matrix_Mul3x3_M(MATRIX *out, const MATRIX *lhs, const MATRIX *rhs);\nvoid Matrix_Slerp3x3_M(MATRIX *lhs_out, const MATRIX *rhs, double t);\nvoid Matrix_Mul3x3(const MATRIX *rhs);\n\nvoid Matrix_TranslateRel(int32_t x, int32_t y, int32_t z);\nvoid Matrix_TranslateRel16(XYZ_16 offset);\nvoid Matrix_TranslateRel32(XYZ_32 offset);\nvoid Matrix_TranslateAbs(int32_t x, int32_t y, int32_t z);\nvoid Matrix_TranslateAbs16(XYZ_16 offset);\nvoid Matrix_TranslateAbs32(XYZ_32 offset);\nvoid Matrix_TranslateSet32(XYZ_32 origin);\nvoid Matrix_TranslateSet32_M(MATRIX *out, XYZ_32 origin);\n\nvoid Matrix_Push_I(void);\nvoid Matrix_Pop_I(void);\n\nvoid Matrix_RotY_I(int16_t ang);\nvoid Matrix_RotX_I(int16_t ang);\nvoid Matrix_RotZ_I(int16_t ang);\nvoid Matrix_Rot16_I(const XYZ_16 rotation);\nvoid Matrix_Rot16_ID(XYZ_16 rotation_1, XYZ_16 rotation_2);\n\nvoid Matrix_TranslateRel_I(int32_t x, int32_t y, int32_t z);\nvoid Matrix_TranslateRel16_I(XYZ_16 offset);\nvoid Matrix_TranslateRel32_I(XYZ_32 offset);\nvoid Matrix_TranslateRel_ID(\n    int32_t x, int32_t y, int32_t z, int32_t x2, int32_t y2, int32_t z2);\nvoid Matrix_TranslateRel16_ID(XYZ_16 offset_1, XYZ_16 offset_2);\nvoid Matrix_TranslateRel32_ID(XYZ_32 offset_1, XYZ_32 offset_2);\n\nvoid Matrix_InitInterpolate(int32_t frac, int32_t rate);\nvoid Matrix_Interpolate(void);\nvoid Matrix_InterpolateArm(void);\n\nXYZ_32 Matrix_MulVec32_M(const MATRIX *m, const XYZ_32 v);\nXYZ_32 Matrix_GetOffset_M(const MATRIX *m);\n"
  },
  {
    "path": "src/trx/game/music/backend_cdaudio.c",
    "content": "#include <trx/game/music/backend_cdaudio.h>\n\n#include <trx/av/audio.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/music/const.h>\n\n#include <inttypes.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <string.h>\n\ntypedef struct {\n    uint64_t from;\n    uint64_t to;\n    bool active;\n} M_CDAUDIO_TRACK;\n\ntypedef struct {\n    const char *path;\n    const char *control_path;\n    const char *description;\n    int32_t num_tracks;\n    M_CDAUDIO_TRACK *tracks;\n} M_BACKEND_DATA;\n\nstatic bool M_Parse(M_BACKEND_DATA *const data)\n{\n    ASSERT(data != nullptr);\n\n    char *track_content = nullptr;\n    size_t track_content_size;\n    if (!File_Load(data->control_path, &track_content, &track_content_size)) {\n        LOG_WARNING(\"Cannot find CDAudio control file: %s\", data->control_path);\n        return false;\n    }\n\n    VECTOR *const tracks = Vector_Create(sizeof(M_CDAUDIO_TRACK));\n    size_t offset = 0;\n    while (offset < track_content_size) {\n        while (track_content[offset] == '\\n' || track_content[offset] == '\\r') {\n            if (++offset >= track_content_size) {\n                goto parse_end;\n            }\n        }\n\n        uint64_t track_num;\n        uint64_t from;\n        uint64_t to;\n        const int32_t result = sscanf(\n            &track_content[offset], \"%\" PRIu64 \" %\" PRIu64 \" %\" PRIu64,\n            &track_num, &from, &to);\n\n        M_CDAUDIO_TRACK track = {};\n        if (result == 3 && track_num > 0) {\n            track.active = true;\n            track.from = from;\n            track.to = to;\n        }\n        Vector_Add(tracks, (void *)&track);\n\n        while (track_content[offset] != '\\n' && track_content[offset] != '\\r') {\n            if (++offset >= track_content_size) {\n                goto parse_end;\n            }\n        }\n    }\n\nparse_end:\n    Memory_Free(track_content);\n\n    data->num_tracks = tracks->count;\n    const size_t data_size = sizeof(M_CDAUDIO_TRACK) * data->num_tracks;\n    data->tracks = Memory_Alloc(data_size);\n    memcpy(data->tracks, Vector_GetData(tracks), data_size);\n    Vector_Free(tracks);\n\n    // reindex wrong track boundaries\n    for (int32_t i = 0; i < data->num_tracks; i++) {\n        if (!data->tracks[i].active) {\n            continue;\n        }\n\n        if (i < data->num_tracks - 1\n            && data->tracks[i].from >= data->tracks[i].to) {\n            for (int32_t j = i + 1; j < data->num_tracks; j++) {\n                if (data->tracks[j].active) {\n                    data->tracks[i].to = data->tracks[j].from;\n                    break;\n                }\n            }\n        }\n\n        if (data->tracks[i].from >= data->tracks[i].to && i > 0) {\n            for (int32_t j = i - 1; j >= 0; j--) {\n                if (data->tracks[j].active) {\n                    data->tracks[i].from = data->tracks[j].to;\n                    break;\n                }\n            }\n        }\n    }\n\n    return true;\n}\n\nstatic bool M_Init(MUSIC_BACKEND *const backend)\n{\n    ASSERT(backend != nullptr);\n    M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n\n    MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ);\n    if (fp == nullptr) {\n        return false;\n    }\n    File_Close(fp);\n\n    if (!M_Parse(data)) {\n        LOG_ERROR(\"Failed to parse CDAudio data\");\n        return false;\n    }\n\n    return true;\n}\n\nstatic const char *M_Describe(const MUSIC_BACKEND *const backend)\n{\n    ASSERT(backend != nullptr);\n    const M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n    return data->description;\n}\n\nstatic int32_t M_Play(\n    const MUSIC_BACKEND *const backend, const int32_t track_id)\n{\n    ASSERT(backend != nullptr);\n    const M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n\n    const int32_t track_idx = track_id - 1;\n    const M_CDAUDIO_TRACK *track = &data->tracks[track_idx];\n    if (track_idx < 0 || track_idx >= data->num_tracks) {\n        LOG_ERROR(\"Invalid track: %d\", track_id);\n        return -1;\n    }\n\n    if (!track->active) {\n        LOG_ERROR(\"Invalid track: %d\", track_id);\n        return -1;\n    }\n\n    const int32_t audio_stream_id = Audio_Stream_CreateFromFile(data->path);\n    Audio_Stream_SetStartTimestamp(audio_stream_id, track->from / 1000.0);\n    Audio_Stream_SetStopTimestamp(audio_stream_id, track->to / 1000.0);\n    Audio_Stream_SeekTimestamp(audio_stream_id, 0.0f);\n    return audio_stream_id;\n}\n\nstatic void M_Shutdown(MUSIC_BACKEND *backend)\n{\n    if (backend == nullptr) {\n        return;\n    }\n\n    if (backend->data != nullptr) {\n        M_BACKEND_DATA *const data = backend->data;\n        Memory_FreePointer(&data->path);\n        Memory_FreePointer(&data->control_path);\n        Memory_FreePointer(&data->description);\n        Memory_FreePointer(&data->tracks);\n    }\n    Memory_FreePointer(&backend->data);\n    Memory_FreePointer(&backend);\n}\n\nMUSIC_BACKEND *Music_Backend_CDAudio_Factory(\n    const char *const path, const char *const control_path)\n{\n    ASSERT(path != nullptr);\n    ASSERT(control_path != nullptr);\n\n    const char *description_fmt = \"CDAudio (path: %s)\";\n    const size_t description_size = snprintf(nullptr, 0, description_fmt, path);\n    char *description = Memory_Alloc(description_size + 1);\n    sprintf(description, description_fmt, path);\n\n    M_BACKEND_DATA *const data = Memory_Alloc(sizeof(M_BACKEND_DATA));\n    data->path = Memory_DupStr(path);\n    data->control_path = Memory_DupStr(control_path);\n    data->description = description;\n\n    MUSIC_BACKEND *const backend = Memory_Alloc(sizeof(MUSIC_BACKEND));\n    backend->data = data;\n    backend->init = M_Init;\n    backend->describe = M_Describe;\n    backend->play = M_Play;\n    backend->shutdown = M_Shutdown;\n    return backend;\n}\n"
  },
  {
    "path": "src/trx/game/music/backend_cdaudio.h",
    "content": "#pragma once\n\n#include <trx/game/music/types.h>\n\nMUSIC_BACKEND *Music_Backend_CDAudio_Factory(\n    const char *path, const char *control_path);\n"
  },
  {
    "path": "src/trx/game/music/backend_cdaudio_wad.c",
    "content": "#include <trx/game/music/backend_cdaudio_wad.h>\n\n#include <trx/av/audio.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_CDAUDIO_WAD_TRACK_COUNT 130\n#define M_CDAUDIO_WAD_WFX_OFFSET 20\n#define M_CDAUDIO_WAD_WFX_SIZE 50\n#define M_CDAUDIO_WAD_WAV_HEADER_SIZE 90\n\ntypedef struct {\n    char name[260];\n    uint32_t size;\n    uint32_t offset;\n} M_CDAUDIO_WAD_TRACK_DESC;\n\n_Static_assert(\n    sizeof(M_CDAUDIO_WAD_TRACK_DESC) == 268,\n    \"Unexpected cdaudio.wad track info size\");\n\ntypedef struct {\n    uint32_t size;\n    uint32_t offset;\n    bool active;\n} M_CDAUDIO_WAD_TRACK;\n\ntypedef struct {\n    const char *path;\n    const char *description;\n    M_CDAUDIO_WAD_TRACK tracks[M_CDAUDIO_WAD_TRACK_COUNT];\n    uint8_t wfx[M_CDAUDIO_WAD_WFX_SIZE];\n    bool has_wfx;\n} M_BACKEND_DATA;\n\nstatic bool M_LoadTrackAsWaveFile(\n    const MUSIC_BACKEND *const backend, const int32_t track_id,\n    uint8_t **const out_data, size_t *const out_size)\n{\n    ASSERT(backend != nullptr);\n    ASSERT(out_data != nullptr);\n    ASSERT(out_size != nullptr);\n\n    const M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n\n    *out_data = nullptr;\n    *out_size = 0;\n\n    const int32_t track_idx = track_id - 1;\n    if (track_idx < 0 || track_idx >= M_CDAUDIO_WAD_TRACK_COUNT) {\n        LOG_ERROR(\"Invalid track: %d\", track_id);\n        return false;\n    }\n\n    const M_CDAUDIO_WAD_TRACK *const track = &data->tracks[track_idx];\n    if (!track->active || track->size == 0) {\n        LOG_ERROR(\"Invalid track: %d\", track_id);\n        return false;\n    }\n\n    MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ);\n    if (fp == nullptr) {\n        return false;\n    }\n\n    const size_t file_size = File_Size(fp);\n    if ((size_t)track->offset >= file_size) {\n        LOG_ERROR(\n            \"Invalid track offset for %d: offset %lu, file size %zu\", track_id,\n            track->offset, file_size);\n        File_Close(fp);\n        return false;\n    }\n\n    // Some installations have invalid track sizes which would result in reading\n    // beyond EOF if left unchecked. The data can still be valid, so clamp such\n    // tracks to the logical remaining file length.\n    const size_t remaining = file_size - (size_t)track->offset;\n    const size_t track_size =\n        M_CDAUDIO_WAD_WAV_HEADER_SIZE + (size_t)track->size;\n    const size_t total_size = MIN(track_size, remaining);\n    uint8_t *const buf = Memory_Alloc(total_size);\n\n    File_Seek(fp, (size_t)track->offset, FILE_SEEK_SET);\n    const bool ok = File_ReadData(fp, buf, total_size);\n    File_Close(fp);\n    if (!ok) {\n        Memory_Free(buf);\n        return false;\n    }\n\n    *out_data = buf;\n    *out_size = total_size;\n    return true;\n}\n\nstatic bool M_ReadAllTrackInfos(MYFILE *const fp, M_BACKEND_DATA *const data)\n{\n    ASSERT(fp != nullptr);\n    ASSERT(data != nullptr);\n\n    M_CDAUDIO_WAD_TRACK_DESC track_infos[M_CDAUDIO_WAD_TRACK_COUNT] = {};\n    File_Skip(fp, sizeof(M_CDAUDIO_WAD_TRACK_DESC));\n    if (!File_ReadItems(\n            fp, track_infos, M_CDAUDIO_WAD_TRACK_COUNT,\n            sizeof(M_CDAUDIO_WAD_TRACK_DESC))) {\n        return false;\n    }\n\n    data->has_wfx = false;\n\n    int32_t first_active_idx = -1;\n    for (int32_t i = 0; i < M_CDAUDIO_WAD_TRACK_COUNT; i++) {\n        const bool is_active = track_infos[i].size != 0;\n        data->tracks[i].active = is_active;\n        data->tracks[i].size = track_infos[i].size;\n        data->tracks[i].offset = track_infos[i].offset;\n\n        if (first_active_idx < 0 && is_active) {\n            first_active_idx = i;\n        }\n    }\n\n    if (first_active_idx < 0) {\n        return true;\n    }\n\n    const size_t wfx_pos = (size_t)data->tracks[first_active_idx].offset\n        + M_CDAUDIO_WAD_WFX_OFFSET;\n    File_Seek(fp, wfx_pos, FILE_SEEK_SET);\n    if (!File_ReadData(fp, data->wfx, M_CDAUDIO_WAD_WFX_SIZE)) {\n        return false;\n    }\n    data->has_wfx = true;\n    return true;\n}\n\nstatic bool M_Init(MUSIC_BACKEND *const backend)\n{\n    ASSERT(backend != nullptr);\n    M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n\n    MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ);\n    if (fp == nullptr) {\n        return false;\n    }\n\n    const bool ok = M_ReadAllTrackInfos(fp, data);\n    File_Close(fp);\n    return ok;\n}\n\nstatic const char *M_Describe(const MUSIC_BACKEND *const backend)\n{\n    ASSERT(backend != nullptr);\n    const M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n    return data->description;\n}\n\nstatic int32_t M_Play(\n    const MUSIC_BACKEND *const backend, const int32_t track_id)\n{\n    ASSERT(backend != nullptr);\n\n    uint8_t *wav_data = nullptr;\n    size_t wav_size = 0;\n    if (!M_LoadTrackAsWaveFile(backend, track_id, &wav_data, &wav_size)) {\n        return AUDIO_NO_SOUND;\n    }\n\n    const int32_t stream_id = Audio_Stream_CreateFromMemory(wav_data, wav_size);\n    if (stream_id < 0) {\n        Memory_Free(wav_data);\n    }\n    return stream_id;\n}\n\nstatic void M_Shutdown(MUSIC_BACKEND *backend)\n{\n    if (backend == nullptr) {\n        return;\n    }\n\n    if (backend->data != nullptr) {\n        M_BACKEND_DATA *const data = backend->data;\n        Memory_FreePointer(&data->path);\n        Memory_FreePointer(&data->description);\n    }\n    Memory_FreePointer(&backend->data);\n    Memory_FreePointer(&backend);\n}\n\nMUSIC_BACKEND *Music_Backend_CDAudioWad_Factory(const char *const path)\n{\n    ASSERT(path != nullptr);\n\n    M_BACKEND_DATA *const data = Memory_Alloc(sizeof(M_BACKEND_DATA));\n    *data = (M_BACKEND_DATA) {\n        .path = Memory_DupStr(path),\n        .description = String_Format(\"CDAudio WAD (path: %s)\", path),\n    };\n\n    MUSIC_BACKEND *const backend = Memory_Alloc(sizeof(MUSIC_BACKEND));\n    backend->data = data;\n    backend->init = M_Init;\n    backend->describe = M_Describe;\n    backend->play = M_Play;\n    backend->shutdown = M_Shutdown;\n    return backend;\n}\n"
  },
  {
    "path": "src/trx/game/music/backend_cdaudio_wad.h",
    "content": "#pragma once\n\n#include <trx/game/music/types.h>\n\nMUSIC_BACKEND *Music_Backend_CDAudioWad_Factory(const char *path);\n"
  },
  {
    "path": "src/trx/game/music/backend_files.c",
    "content": "#include <trx/game/music/backend_files.h>\n\n#include <trx/av/audio.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/shell/paths.h>\n\n#include <stdio.h>\n\ntypedef struct {\n    const char *dir;\n    const char *description;\n} M_BACKEND_DATA;\n\nstatic const char *m_ExtensionsToTry[] = {\n    \".flac\", \".ogg\", \".mp3\", \".wav\", \".wma\", nullptr,\n};\n\nstatic char *M_GetTrackFileName(const char *base_dir, int32_t track)\n{\n    char *tmp_path = String_Format(\"%s/track%02d.flac\", base_dir, track);\n    char *result = TRXPath_GuessExtension(tmp_path, m_ExtensionsToTry);\n    Memory_FreePointer(&tmp_path);\n\n    if (result == nullptr) {\n        tmp_path = String_Format(\"%s/%d.flac\", base_dir, track);\n        result = TRXPath_GuessExtension(tmp_path, m_ExtensionsToTry);\n        Memory_FreePointer(&tmp_path);\n    }\n    return result;\n}\n\nstatic bool M_Init(MUSIC_BACKEND *const backend)\n{\n    ASSERT(backend != nullptr);\n    const M_BACKEND_DATA *data = backend->data;\n    ASSERT(data->dir != nullptr);\n    return File_DirExists(data->dir);\n}\n\nstatic const char *M_Describe(const MUSIC_BACKEND *const backend)\n{\n    ASSERT(backend != nullptr);\n    const M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n    return data->description;\n}\n\nstatic int32_t M_Play(\n    const MUSIC_BACKEND *const backend, const int32_t track_id)\n{\n    ASSERT(backend != nullptr);\n    const M_BACKEND_DATA *const data = backend->data;\n    ASSERT(data != nullptr);\n\n    char *file_path = M_GetTrackFileName(data->dir, track_id);\n    if (file_path == nullptr) {\n        LOG_ERROR(\"Invalid track: %d\", track_id);\n        return -1;\n    }\n\n    const int32_t stream_id = Audio_Stream_CreateFromFile(file_path);\n    Memory_Free(file_path);\n    return stream_id;\n}\n\nstatic void M_Shutdown(MUSIC_BACKEND *backend)\n{\n    if (backend == nullptr) {\n        return;\n    }\n\n    if (backend->data != nullptr) {\n        M_BACKEND_DATA *const data = backend->data;\n        Memory_FreePointer(&data->dir);\n        Memory_FreePointer(&data->description);\n    }\n    Memory_FreePointer(&backend->data);\n    Memory_FreePointer(&backend);\n}\n\nMUSIC_BACKEND *Music_Backend_Files_Factory(const char *path)\n{\n    ASSERT(path != nullptr);\n\n    const char *description_fmt = \"Directory (directory: %s)\";\n    const size_t description_size = snprintf(nullptr, 0, description_fmt, path);\n    char *description = Memory_Alloc(description_size + 1);\n    sprintf(description, description_fmt, path);\n\n    M_BACKEND_DATA *const data = Memory_Alloc(sizeof(M_BACKEND_DATA));\n    data->dir = Memory_DupStr(path);\n    data->description = description;\n\n    MUSIC_BACKEND *const backend = Memory_Alloc(sizeof(MUSIC_BACKEND));\n    backend->data = data;\n    backend->init = M_Init;\n    backend->describe = M_Describe;\n    backend->play = M_Play;\n    backend->shutdown = M_Shutdown;\n    return backend;\n}\n"
  },
  {
    "path": "src/trx/game/music/backend_files.h",
    "content": "#pragma once\n\n#include <trx/game/music/types.h>\n\nMUSIC_BACKEND *Music_Backend_Files_Factory(const char *path);\n"
  },
  {
    "path": "src/trx/game/music/common.c",
    "content": "#include <trx/game/music/common.h>\n\n#include <trx/av/audio.h>\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/level.h>\n#include <trx/game/music.h>\n#include <trx/game/music/backend_cdaudio.h>\n#include <trx/game/music/backend_cdaudio_wad.h>\n#include <trx/game/music/backend_files.h>\n#include <trx/game/shell/paths.h>\n#include <trx/version.h>\n\nstatic bool m_Initialised = false;\nstatic uint16_t m_MusicTrackFlags[MAX_MUSIC_TRACKS] = {};\nstatic MUSIC_ID m_TrackCurrent = MX_INACTIVE;\nstatic MUSIC_ID m_TrackDelayed = MX_INACTIVE;\nstatic MUSIC_ID m_TrackLooped = MX_INACTIVE;\n// Remember the last played track, whether normal or looped, to prevent\n// immediately restarting it if Lara remains on the same trigger.\nstatic MUSIC_ID m_TrackLastPlayed = MX_INACTIVE;\nstatic MUSIC_ID m_TrackLastLooped = MX_INACTIVE;\n\ntypedef struct {\n    int32_t audio_stream_id;\n    MUSIC_ID track_id;\n    MUSIC_PLAY_MODE mode;\n    bool active;\n} M_MUSIC_STREAM;\n\nstatic float m_MusicVolume = 0.0f;\nstatic MUSIC_BACKEND *m_Backend = nullptr;\nstatic M_MUSIC_STREAM m_MainStream = {\n    .audio_stream_id = -1,\n    .track_id = MX_INACTIVE,\n    .mode = MPM_ONCE,\n    .active = false,\n};\nstatic M_MUSIC_STREAM m_OverlayStreams[MUSIC_MAX_OVERLAY_TRACKS] = {};\n\nstatic MUSIC_BACKEND *M_FindBackend(void)\n{\n    VECTOR *all_backends = Vector_Create(sizeof(MUSIC_BACKEND *));\n    const char *const music_dir =\n        TRXPath_PeekResolve(TRX_DYNAMIC_PATH_MUSIC_DIR, nullptr);\n    if (music_dir != nullptr) {\n        Vector_Add(\n            all_backends,\n            &(MUSIC_BACKEND *) { Music_Backend_Files_Factory(music_dir) });\n    }\n\n    if (g_TRVersion >= 2) {\n        const char *const cdaudio_dat_path =\n            TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, \"cdaudio.dat\");\n        const char *const cdaudio_wav_path =\n            TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, \"cdaudio.wav\");\n        const char *const cdaudio_mp3_path =\n            TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, \"cdaudio.mp3\");\n\n        if (cdaudio_dat_path != nullptr && cdaudio_wav_path != nullptr) {\n            Vector_Add(\n                all_backends,\n                &(MUSIC_BACKEND *) { Music_Backend_CDAudio_Factory(\n                    cdaudio_wav_path, cdaudio_dat_path) });\n        }\n        if (cdaudio_dat_path != nullptr && cdaudio_mp3_path != nullptr) {\n            Vector_Add(\n                all_backends,\n                &(MUSIC_BACKEND *) { Music_Backend_CDAudio_Factory(\n                    cdaudio_mp3_path, cdaudio_dat_path) });\n        }\n    }\n    if (g_TRVersion >= 3) {\n        const char *const cdaudio_wad_path =\n            TRXPath_PeekResolve(TRX_DYNAMIC_PATH_CDAUDIO_FILE, \"cdaudio.wad\");\n        if (cdaudio_wad_path != nullptr) {\n            Vector_Add(\n                all_backends,\n                &(MUSIC_BACKEND *) {\n                    Music_Backend_CDAudioWad_Factory(cdaudio_wad_path) });\n        }\n    }\n\n    MUSIC_BACKEND *backend = nullptr;\n    for (int32_t i = 0; i < all_backends->count; i++) {\n        MUSIC_BACKEND *const tmp_backend =\n            *(MUSIC_BACKEND **)Vector_Get(all_backends, i);\n        if (tmp_backend->init(tmp_backend)) {\n            backend = tmp_backend;\n            break;\n        }\n    }\n\n    for (int32_t i = 0; i < all_backends->count; i++) {\n        MUSIC_BACKEND *const tmp_backend =\n            *(MUSIC_BACKEND **)Vector_Get(all_backends, i);\n        if (tmp_backend != backend) {\n            tmp_backend->shutdown(tmp_backend);\n        }\n    }\n    Vector_Free(all_backends);\n\n    return backend;\n}\n\nstatic void M_StreamReset(M_MUSIC_STREAM *const stream)\n{\n    stream->audio_stream_id = -1;\n    stream->track_id = MX_INACTIVE;\n    stream->mode = MPM_ONCE;\n    stream->active = false;\n}\n\nstatic void M_StreamClose(M_MUSIC_STREAM *const stream)\n{\n    if (!stream->active || stream->audio_stream_id < 0) {\n        M_StreamReset(stream);\n        return;\n    }\n\n    // We are only interested in calling M_StreamFinished if a stream\n    // finished by itself. In cases where we end the streams early by hand,\n    // we clear the finish callback in order to avoid resuming the BGM playback\n    // just after we stop it.\n    Audio_Stream_SetFinishCallback(stream->audio_stream_id, nullptr, nullptr);\n    Audio_Stream_Close(stream->audio_stream_id);\n    M_StreamReset(stream);\n}\n\nstatic void M_StopMainStream(void)\n{\n    M_StreamClose(&m_MainStream);\n}\n\nstatic void M_StopOverlayStreams(void)\n{\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        M_StreamClose(&m_OverlayStreams[i]);\n    }\n}\n\nstatic void M_ResetStreamState(void)\n{\n    M_StreamReset(&m_MainStream);\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        M_StreamReset(&m_OverlayStreams[i]);\n    }\n}\n\nstatic void M_StreamFinished(const int32_t stream_id, void *const user_data)\n{\n    M_MUSIC_STREAM *const stream = user_data;\n    if (stream == nullptr) {\n        return;\n    }\n    if (!stream->active || stream->audio_stream_id != stream_id) {\n        return;\n    }\n\n    if (stream == &m_MainStream) {\n        // When the main stream finishes, play the remembered BGM.\n        m_TrackCurrent = MX_INACTIVE;\n        M_StreamReset(stream);\n        if (m_TrackLooped >= 0) {\n            m_TrackLastLooped = MX_INACTIVE;\n            Music_Play_Direct(m_TrackLooped, MPM_LOOP);\n        }\n    } else {\n        M_StreamReset(stream);\n    }\n}\n\nstatic bool M_IsBrokenTrack(const MUSIC_ID track_id)\n{\n    if (track_id < 0) {\n        return true;\n    }\n    if (g_TRVersion > 1) {\n        return false;\n    }\n    const MUSIC_TRX_ID track = Music_FromGameID(track_id);\n    return track == MX_UNUSED_0 || track == MX_UNUSED_1 || track == MX_UNUSED_2;\n}\n\nstatic bool M_IsAmbientTrack(const MUSIC_ID track_id)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level != nullptr && level->music_track == track_id) {\n        return true;\n    }\n\n    const GF_AMBIENT_DATA *const ambient_data = &g_GameFlow.ambient_tracks;\n    if (ambient_data == nullptr) {\n        return false;\n    }\n    for (int32_t i = 0; i < ambient_data->count; i++) {\n        if (ambient_data->ids[i] == track_id) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic void M_SyncVolume(const M_MUSIC_STREAM *const stream)\n{\n    if (stream == nullptr || !stream->active || stream->audio_stream_id < 0) {\n        return;\n    }\n    const float volume = stream->mode == MPM_OVERLAY\n        ? g_Config.audio.music_volume * g_Config.audio.master_volume\n        : m_MusicVolume;\n    Audio_Stream_SetVolume(stream->audio_stream_id, volume);\n}\n\nstatic void M_SyncVolumes(void)\n{\n    M_SyncVolume(&m_MainStream);\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        M_SyncVolume(&m_OverlayStreams[i]);\n    }\n}\n\nstatic int32_t M_GetFreeOverlaySlot(void)\n{\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        if (!m_OverlayStreams[i].active) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nstatic bool M_PlayOverlayTrack(const MUSIC_ID track_id)\n{\n    if (m_Backend == nullptr) {\n        LOG_WARNING(\n            \"Not playing overlay track %d because no backend is available\",\n            track_id);\n        return false;\n    }\n\n    const int32_t slot = M_GetFreeOverlaySlot();\n    if (slot < 0) {\n        LOG_WARNING(\n            \"Not playing overlay track %d because all %d overlay slots are in \"\n            \"use\",\n            track_id, MUSIC_MAX_OVERLAY_TRACKS);\n        return false;\n    }\n\n    const int32_t stream_id = m_Backend->play(m_Backend, track_id);\n    if (stream_id < 0) {\n        LOG_ERROR(\"Failed to create overlay stream for track %d\", track_id);\n        return false;\n    }\n\n    m_OverlayStreams[slot].audio_stream_id = stream_id;\n    m_OverlayStreams[slot].track_id = track_id;\n    m_OverlayStreams[slot].mode = MPM_OVERLAY;\n    m_OverlayStreams[slot].active = true;\n    M_SyncVolume(&m_OverlayStreams[slot]);\n    Audio_Stream_SetIsLooped(stream_id, false);\n    Audio_Stream_SetFinishCallback(\n        stream_id, M_StreamFinished, &m_OverlayStreams[slot]);\n    return true;\n}\n\nstatic bool M_GetMainTrackState(MUSIC_STREAM_STATE *const state)\n{\n    if (!m_MainStream.active || state == nullptr) {\n        return false;\n    }\n\n    state->track_id = MX_INACTIVE;\n    state->mode = MPM_ONCE;\n    state->timestamp = Audio_Stream_GetTimestamp(m_MainStream.audio_stream_id);\n\n    if (m_TrackCurrent != MX_INACTIVE) {\n        state->track_id = m_TrackCurrent;\n        return true;\n    }\n    if (m_TrackLooped != MX_INACTIVE) {\n        state->track_id = m_TrackLooped;\n        state->mode = MPM_LOOP;\n        return true;\n    }\n    return false;\n}\n\nbool Music_Init(void)\n{\n    m_Initialised = true;\n    if (m_Backend != nullptr) {\n        m_Backend->shutdown(m_Backend);\n        m_Backend = nullptr;\n    }\n    m_Backend = M_FindBackend();\n    if (m_Backend == nullptr) {\n        LOG_ERROR(\"No music backend is available\");\n        goto finish;\n    }\n\n    LOG_INFO(\"Chosen music backend: %s\", m_Backend->describe(m_Backend));\n    Music_SetVolume(g_Config.audio.music_volume);\n\nfinish:\n    m_TrackCurrent = MX_INACTIVE;\n    m_TrackLastPlayed = MX_INACTIVE;\n    m_TrackDelayed = MX_INACTIVE;\n    m_TrackLooped = MX_INACTIVE;\n    m_TrackLastLooped = MX_INACTIVE;\n    M_ResetStreamState();\n    return Audio_Init();\n}\n\nvoid Music_Shutdown(void)\n{\n    m_Initialised = false;\n    M_StopMainStream();\n    M_StopOverlayStreams();\n    M_ResetStreamState();\n    if (m_Backend != nullptr) {\n        m_Backend->shutdown(m_Backend);\n        m_Backend = nullptr;\n    }\n    Audio_Shutdown();\n}\n\nbool Music_Play_Direct(const MUSIC_ID track_id, const MUSIC_PLAY_MODE mode)\n{\n    if (!m_Initialised) {\n        return false;\n    }\n\n    if (M_IsBrokenTrack(track_id)) {\n        return false;\n    }\n\n    if (mode == MPM_OVERLAY) {\n        LOG_INFO(\"Playing overlay track %d\", track_id);\n        return M_PlayOverlayTrack(track_id);\n    }\n\n    if (track_id == m_TrackCurrent) {\n        return true;\n    }\n\n    if (mode == MPM_NO_REPEAT && track_id == m_TrackLastPlayed) {\n        return true;\n    }\n\n    const bool is_looped = mode == MPM_LOOP || M_IsAmbientTrack(track_id);\n    if (is_looped && track_id == m_TrackLastLooped) {\n        return true;\n    }\n\n    if (mode == MPM_DELAY) {\n        m_TrackDelayed = track_id;\n        return true;\n    }\n\n    if (is_looped && m_TrackCurrent != MX_INACTIVE) {\n        // OG TR3 behaviour: do not interrupt a regular track when the ambient\n        // changes; remember the new ambient and restore it when the track ends.\n        m_TrackDelayed = MX_INACTIVE;\n        m_TrackLooped = track_id;\n        m_TrackLastLooped = track_id;\n        return true;\n    }\n\n    M_StopMainStream();\n\n    if (m_Backend == nullptr) {\n        LOG_WARNING(\n            \"Not playing track %d because no backend is available\", track_id);\n        goto finish;\n    }\n\n    LOG_INFO(\"Playing track %d, mode: %d\", track_id, mode);\n\n    const int32_t stream_id = m_Backend->play(m_Backend, track_id);\n    if (stream_id < 0) {\n        LOG_ERROR(\"Failed to create music stream for track %d\", track_id);\n        goto finish;\n    }\n\n    m_MainStream.audio_stream_id = stream_id;\n    m_MainStream.track_id = track_id;\n    m_MainStream.mode = is_looped ? MPM_LOOP : MPM_ONCE;\n    m_MainStream.active = true;\n    M_SyncVolume(&m_MainStream);\n    Audio_Stream_SetIsLooped(stream_id, is_looped);\n    Audio_Stream_SetFinishCallback(stream_id, M_StreamFinished, &m_MainStream);\n\nfinish:\n    m_TrackDelayed = MX_INACTIVE;\n    if (is_looped) {\n        // Reset the regular track outside of M_StreamFinished so that\n        // Music_GetCurrentPlayingTrack returns the looped track; otherwise, the\n        // stopped track could be stored in the savegame despite being inactive.\n        m_TrackCurrent = MX_INACTIVE;\n        m_TrackLooped = track_id;\n        m_TrackLastLooped = track_id;\n    } else {\n        m_TrackCurrent = track_id;\n        m_TrackLastPlayed = track_id;\n    }\n    return true;\n}\n\nbool Music_Play(const MUSIC_TRX_ID track, const MUSIC_PLAY_MODE mode)\n{\n    return Music_Play_Direct(Music_ToGameID(track), mode);\n}\n\nvoid Music_Stop(void)\n{\n    m_TrackCurrent = MX_INACTIVE;\n    m_TrackLastPlayed = MX_INACTIVE;\n    m_TrackDelayed = MX_INACTIVE;\n    m_TrackLooped = MX_INACTIVE;\n    m_TrackLastLooped = MX_INACTIVE;\n    M_StopMainStream();\n    M_StopOverlayStreams();\n    M_ResetStreamState();\n}\n\nvoid Music_StopTrack_Direct(const MUSIC_ID track)\n{\n    if (track != m_TrackCurrent || M_IsBrokenTrack(track)) {\n        return;\n    }\n\n    M_StopMainStream();\n    m_TrackCurrent = MX_INACTIVE;\n    if (m_TrackLooped >= 0) {\n        Music_Play_Direct(m_TrackLooped, MPM_LOOP);\n    }\n}\n\nvoid Music_Pause(void)\n{\n    if (m_MainStream.active && m_MainStream.audio_stream_id >= 0) {\n        Audio_Stream_Pause(m_MainStream.audio_stream_id);\n    }\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        if (m_OverlayStreams[i].active\n            && m_OverlayStreams[i].audio_stream_id >= 0) {\n            Audio_Stream_Pause(m_OverlayStreams[i].audio_stream_id);\n        }\n    }\n}\n\nvoid Music_Unpause(void)\n{\n    if (m_MainStream.active && m_MainStream.audio_stream_id >= 0) {\n        Audio_Stream_Unpause(m_MainStream.audio_stream_id);\n    }\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        if (m_OverlayStreams[i].active\n            && m_OverlayStreams[i].audio_stream_id >= 0) {\n            Audio_Stream_Unpause(m_OverlayStreams[i].audio_stream_id);\n        }\n    }\n}\n\ndouble Music_GetTimestamp(void)\n{\n    if (!m_MainStream.active || m_MainStream.audio_stream_id < 0) {\n        return -1.0;\n    }\n    return Audio_Stream_GetTimestamp(m_MainStream.audio_stream_id);\n}\n\nbool Music_SeekTimestamp(const double timestamp)\n{\n    if (!m_MainStream.active || m_MainStream.audio_stream_id < 0) {\n        return false;\n    }\n    return Audio_Stream_SeekTimestamp(m_MainStream.audio_stream_id, timestamp);\n}\n\nbool Music_SyncTimestamp(const double timestamp)\n{\n    if (!m_MainStream.active || m_MainStream.audio_stream_id < 0) {\n        return false;\n    }\n    return Audio_Stream_SyncTimestamp(m_MainStream.audio_stream_id, timestamp);\n}\n\nint32_t Music_GetStreamCount(void)\n{\n    int32_t count = 0;\n    if (m_MainStream.active) {\n        count++;\n    }\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        if (m_OverlayStreams[i].active) {\n            count++;\n        }\n    }\n    return count;\n}\n\nbool Music_GetStreamState(\n    const int32_t index, MUSIC_STREAM_STATE *const out_state)\n{\n    if (index < 0 || out_state == nullptr) {\n        return false;\n    }\n\n    int32_t stream_index = 0;\n    if (m_MainStream.active) {\n        if (stream_index == index) {\n            return M_GetMainTrackState(out_state);\n        }\n        stream_index++;\n    }\n\n    for (int32_t i = 0; i < MUSIC_MAX_OVERLAY_TRACKS; i++) {\n        if (!m_OverlayStreams[i].active) {\n            continue;\n        }\n\n        if (stream_index == index) {\n            out_state->track_id = m_OverlayStreams[i].track_id;\n            out_state->mode = m_OverlayStreams[i].mode;\n            out_state->timestamp =\n                Audio_Stream_GetTimestamp(m_OverlayStreams[i].audio_stream_id);\n            return true;\n        }\n        stream_index++;\n    }\n\n    return false;\n}\n\nbool Music_SeekTrackTimestamp(\n    const MUSIC_ID track_id, const MUSIC_PLAY_MODE mode, const double timestamp)\n{\n    if (mode == MPM_OVERLAY) {\n        for (int32_t i = MUSIC_MAX_OVERLAY_TRACKS - 1; i >= 0; i--) {\n            if (!m_OverlayStreams[i].active\n                || m_OverlayStreams[i].track_id != track_id) {\n                continue;\n            }\n            return Audio_Stream_SeekTimestamp(\n                m_OverlayStreams[i].audio_stream_id, timestamp);\n        }\n        return false;\n    }\n\n    MUSIC_STREAM_STATE state = {};\n    if (!M_GetMainTrackState(&state)) {\n        return false;\n    }\n    if (state.track_id != track_id || state.mode != mode) {\n        return false;\n    }\n    return Audio_Stream_SeekTimestamp(m_MainStream.audio_stream_id, timestamp);\n}\n\nMUSIC_ID Music_GetDelayedTrack(void)\n{\n    return m_TrackDelayed;\n}\n\nMUSIC_ID Music_GetCurrentPlayingTrack(void)\n{\n    return m_TrackCurrent == MX_INACTIVE ? m_TrackLooped : m_TrackCurrent;\n}\n\nMUSIC_ID Music_GetCurrentLoopedTrack(void)\n{\n    return m_TrackLooped;\n}\n\nvoid Music_SetVolume(float volume)\n{\n    volume *= g_Config.audio.master_volume;\n    if (volume != m_MusicVolume) {\n        m_MusicVolume = volume;\n        M_SyncVolumes();\n    }\n}\n\nvoid Music_ResetTrackFlags(void)\n{\n    for (int32_t i = 0; i < MAX_MUSIC_TRACKS; i++) {\n        m_MusicTrackFlags[i] = 0;\n    }\n}\n\nuint16_t Music_GetTrackFlags(const MUSIC_ID track_id)\n{\n    return m_MusicTrackFlags[track_id];\n}\n\nvoid Music_SetTrackFlags(const MUSIC_ID track_id, const uint16_t flags)\n{\n    m_MusicTrackFlags[track_id] = flags;\n}\n"
  },
  {
    "path": "src/trx/game/music/common.h",
    "content": "#pragma once\n\n#include <trx/game/music/enum.h>\n#include <trx/game/music/ids.h>\n\n#include <stdint.h>\n\n#define MUSIC_MAX_OVERLAY_TRACKS 3\n\ntypedef struct {\n    MUSIC_ID track_id;\n    MUSIC_PLAY_MODE mode;\n    double timestamp;\n} MUSIC_STREAM_STATE;\n\nbool Music_Init(void);\nvoid Music_Shutdown(void);\n\n// Stops playing current track and plays a single track.\n//\n// MPM_ONCE:\n//   Plays the track once. Once playback is done, if there is an active looped\n//   track, the playback resumes from the start of the looped track.\n// MPM_LOOP:\n//   Activates looped playback for the chosen track.\n// MPM_NO_REPEAT:\n//   A track with this play mode will not trigger in succession.\n// MPM_DELAY:\n//   A track does not get played and instead is only marked for later playback.\n//   The track to play is available with Music_GetDelayedTrack().\n// MPM_OVERLAY:\n//   Plays a non-looping track without interrupting active background music.\nbool Music_Play_Direct(MUSIC_ID track, MUSIC_PLAY_MODE mode);\n\n// Stops the provided single track and restarts the looped track if applicable.\nvoid Music_StopTrack_Direct(MUSIC_ID track);\n\n// Play a music track with a semantical ID that will get mapped to a specific\n// music track slot depending on the game.\nbool Music_Play(MUSIC_TRX_ID track, MUSIC_PLAY_MODE mode);\n\n// Stops all music streams, including looped, active, and overlay tracks.\nvoid Music_Stop(void);\n\n// Pauses the music.\nvoid Music_Pause(void);\n\n// Unpauses the music.\nvoid Music_Unpause(void);\n\n// Get the current timestamp of the current stream in seconds.\ndouble Music_GetTimestamp(void);\n\n// Seek to timestamp of current stream.\nbool Music_SeekTimestamp(double timestamp);\n\n// Seeks to the given timestamp if the drift is too big.\nbool Music_SyncTimestamp(double timestamp);\n\n// Returns the number of currently active serializable streams.\nint32_t Music_GetStreamCount(void);\n\n// Returns stream state by active index [0..Music_GetStreamCount()).\nbool Music_GetStreamState(int32_t index, MUSIC_STREAM_STATE *state);\n\n// Seeks timestamp for the active stream that matches track and mode.\nbool Music_SeekTrackTimestamp(\n    MUSIC_ID track, MUSIC_PLAY_MODE mode, double timestamp);\n\n// Returns the delayed track. Ignores looped tracks.\nMUSIC_ID Music_GetDelayedTrack(void);\n\n// Returns the currently playing track. Includes looped music.\nMUSIC_ID Music_GetCurrentPlayingTrack(void);\n\n// Returns the looped track.\nMUSIC_ID Music_GetCurrentLoopedTrack(void);\n\n// Sets the game volume.\nvoid Music_SetVolume(float volume);\n\n// Resets all track trigger mask flags.\nvoid Music_ResetTrackFlags(void);\n\n// Returns trigger mask flags for the given track.\nuint16_t Music_GetTrackFlags(MUSIC_ID track_id);\n\n// Sets the trigger mask flags for the given track.\nvoid Music_SetTrackFlags(MUSIC_ID track_id, uint16_t flags);\n"
  },
  {
    "path": "src/trx/game/music/const.h",
    "content": "#pragma once\n\n#define MAX_MUSIC_TRACKS 1024\n"
  },
  {
    "path": "src/trx/game/music/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    MPM_ONCE,\n    MPM_LOOP,\n    MPM_DELAY,\n    MPM_NO_REPEAT,\n    MPM_OVERLAY,\n} MUSIC_PLAY_MODE;\n"
  },
  {
    "path": "src/trx/game/music/ids.c",
    "content": "#include <trx/game/catalog/manager.h>\n#include <trx/game/music.h>\n\nMUSIC_ID Music_ToGameID(const MUSIC_TRX_ID music_track)\n{\n    int32_t out;\n    if (Catalog_EnumToGameID(CATALOG_MUSIC, music_track, &out)) {\n        return out;\n    }\n    return MX_INACTIVE;\n}\n\nMUSIC_TRX_ID Music_FromGameID(const MUSIC_ID track_id)\n{\n    CATALOG_ID out;\n    if (Catalog_GameIDToEnum(CATALOG_MUSIC, track_id, &out)) {\n        return out;\n    }\n    return MX_TRX_INVALID;\n}\n"
  },
  {
    "path": "src/trx/game/music/ids.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef enum {\n    MX_INACTIVE = -1,\n} MUSIC_ID;\n\ntypedef enum {\n    MX_TRX_INVALID = -1,\n#define X_CATALOG_ID(enum_value) enum_value,\n#include <trx/game/catalog/music.def>\n#undef X_CATALOG_ID\n    MX_NUMBER_OF,\n} MUSIC_TRX_ID;\n\nMUSIC_ID Music_ToGameID(MUSIC_TRX_ID music_track);\nMUSIC_TRX_ID Music_FromGameID(MUSIC_ID track_id);\n"
  },
  {
    "path": "src/trx/game/music/types.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct MUSIC_BACKEND {\n    bool (*init)(struct MUSIC_BACKEND *backend);\n    const char *(*describe)(const struct MUSIC_BACKEND *backend);\n    int32_t (*play)(const struct MUSIC_BACKEND *backend, int32_t track_id);\n    void (*shutdown)(struct MUSIC_BACKEND *backend);\n    void *data;\n} MUSIC_BACKEND;\n"
  },
  {
    "path": "src/trx/game/music.h",
    "content": "#pragma once\n\n#include <trx/game/music/common.h>\n#include <trx/game/music/const.h>\n#include <trx/game/music/ids.h>\n#include <trx/game/music/types.h>\n"
  },
  {
    "path": "src/trx/game/objects/col.c",
    "content": "#include <trx/game/objects/col.h>\n\n#include <trx/game/lara.h>\n\nvoid Object_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    if (coll->enable_baddie_push) {\n        Lara_Col_ItemPush(item, coll, false, true);\n    }\n}\n\nvoid Object_Collision_Trap(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->status == IS_ACTIVE) {\n        if (Lara_TestBoundsCollide(item, coll->radius)) {\n            Collide_TestCollision(item, lara_item);\n        }\n    } else if (item->status != IS_INVISIBLE) {\n        Object_Collision(item_num, lara_item, coll);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/objects/col.h",
    "content": "#include <trx/game/collision.h>\n\nvoid Object_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\nvoid Object_Collision_Trap(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n"
  },
  {
    "path": "src/trx/game/objects/common.c",
    "content": "#include <trx/game/objects/common.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/anims.h>\n#include <trx/game/catalog/manager.h>\n#include <trx/game/const.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/output/common.h>\n\nstatic OBJECT m_Objects[O_NUMBER_OF] = {};\nstatic STATIC_OBJECT_3D *m_StaticObjects3D = nullptr;\nstatic STATIC_OBJECT_2D *m_StaticObjects2D = nullptr;\nstatic int32_t m_StaticObjects3DCount = 0;\nstatic int32_t m_StaticObjects2DCount = 0;\nstatic OBJECT_MESH **m_MeshPointers = nullptr;\nstatic int32_t m_MeshCount = 0;\n\nvoid Object_Reset(void)\n{\n    for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) {\n        m_Objects[i].loaded = false;\n    }\n\n    m_StaticObjects3D = nullptr;\n    m_StaticObjects2D = nullptr;\n    m_StaticObjects3DCount = 0;\n    m_StaticObjects2DCount = 0;\n    m_MeshPointers = nullptr;\n    m_MeshCount = 0;\n}\n\nvoid Object_InitialiseStaticObjects3D(const int32_t count)\n{\n    ASSERT(count >= 0);\n    m_StaticObjects3DCount = count;\n    m_StaticObjects3D =\n        GameBuf_Alloc(sizeof(STATIC_OBJECT_3D) * count, GBUF_STATIC_OBJECTS_3D);\n}\n\nvoid Object_InitialiseStaticObjects2D(const int32_t count)\n{\n    ASSERT(count >= 0);\n    m_StaticObjects2DCount = count;\n    m_StaticObjects2D =\n        GameBuf_Alloc(sizeof(STATIC_OBJECT_2D) * count, GBUF_STATIC_OBJECTS_2D);\n}\n\nint32_t Object_GetStaticObjects3DCount(void)\n{\n    return m_StaticObjects3DCount;\n}\n\nint32_t Object_GetStaticObjects2DCount(void)\n{\n    return m_StaticObjects2DCount;\n}\n\nOBJECT *Object_TryGet(const OBJECT_ID object_id)\n{\n    if (object_id < O_FIRST || object_id >= O_NUMBER_OF) {\n        return nullptr;\n    }\n    return &m_Objects[object_id];\n}\n\nOBJECT *Object_Get(const OBJECT_ID object_id)\n{\n    ASSERT(object_id >= O_FIRST && object_id < O_NUMBER_OF);\n    return &m_Objects[object_id];\n}\n\nOBJECT *Object_GetByGameID(const int32_t game_id)\n{\n    OBJECT_ID object_id = Object_FromGameID(game_id);\n    if (object_id == NO_OBJECT) {\n        return nullptr;\n    }\n    return &m_Objects[object_id];\n}\n\nSTATIC_OBJECT_3D *Object_Get3DStatic(const int32_t static_id)\n{\n    return &m_StaticObjects3D[static_id];\n}\n\nbool Object_IsValidStatid3D(const int32_t static_id)\n{\n    return static_id >= 0 && static_id < m_StaticObjects3DCount;\n}\n\nSTATIC_OBJECT_2D *Object_Get2DStatic(const int32_t static_id)\n{\n    if (m_StaticObjects2D == nullptr) {\n        return nullptr;\n    }\n    if (static_id < 0 || static_id >= m_StaticObjects2DCount) {\n        return nullptr;\n    }\n    return &m_StaticObjects2D[static_id];\n}\n\nOBJECT_ID Object_FromGameID(const int32_t game_id)\n{\n    int32_t out;\n    if (Catalog_GameIDToEnum(CATALOG_OBJECTS, game_id, &out)) {\n        return out;\n    }\n    return NO_OBJECT;\n}\n\nint32_t Object_ToGameID(const OBJECT_ID object_id)\n{\n    int32_t out;\n    if (Catalog_EnumToGameID(CATALOG_OBJECTS, object_id, &out)) {\n        return out;\n    }\n    return -1;\n}\n\nbool Object_IsType(const OBJECT_ID object_id, const OBJECT_ID *test_arr)\n{\n    for (int32_t i = 0; test_arr[i] != NO_OBJECT; i++) {\n        if (test_arr[i] == object_id) {\n            return true;\n        }\n    }\n    return false;\n}\n\nOBJECT_ID Object_GetCognate(OBJECT_ID key_id, const GAME_OBJECT_PAIR *test_map)\n{\n    const GAME_OBJECT_PAIR *pair = &test_map[0];\n    while (pair->key_id != NO_OBJECT) {\n        if (pair->key_id == key_id) {\n            return pair->value_id;\n        }\n        pair++;\n    }\n\n    return NO_OBJECT;\n}\n\nOBJECT_ID Object_GetCognateInverse(\n    OBJECT_ID value_id, const GAME_OBJECT_PAIR *test_map)\n{\n    const GAME_OBJECT_PAIR *pair = &test_map[0];\n    while (pair->key_id != NO_OBJECT) {\n        if (pair->value_id == value_id) {\n            return pair->key_id;\n        }\n        pair++;\n    }\n\n    return NO_OBJECT;\n}\n\nvoid Object_InitialiseMeshes(const int32_t mesh_count)\n{\n    m_MeshPointers =\n        GameBuf_Alloc(sizeof(OBJECT_MESH *) * mesh_count, GBUF_MESH_POINTERS);\n    m_MeshCount = 0;\n}\n\nvoid Object_StoreMesh(OBJECT_MESH *const mesh)\n{\n    m_MeshPointers[m_MeshCount] = mesh;\n    m_MeshCount++;\n}\n\nOBJECT_MESH *Object_GetMesh(const int32_t index)\n{\n    return m_MeshPointers[index];\n}\n\nint32_t Object_GetItemMeshIndex(const ITEM *const item, const int32_t mesh_idx)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const int32_t fallback = obj->mesh_idx + mesh_idx;\n\n    if (obj->get_mesh_index_func == nullptr) {\n        return fallback;\n    }\n\n    const int32_t resolved = obj->get_mesh_index_func(item, mesh_idx);\n    if (resolved < 0) {\n        return fallback;\n    }\n\n    return resolved;\n}\n\nint32_t Object_GetMeshIndex(const OBJECT_MESH *const mesh)\n{\n    for (int32_t i = 0; i < m_MeshCount; i++) {\n        if (mesh == m_MeshPointers[i]) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nint32_t Object_GetMeshCount(void)\n{\n    return m_MeshCount;\n}\n\nOBJECT_MESH *Object_FindMesh(const int32_t data_offset)\n{\n    for (int32_t i = 0; i < m_MeshCount; i++) {\n        OBJECT_MESH *const mesh = m_MeshPointers[i];\n        if (Object_GetMeshOffset(mesh) == data_offset) {\n            return mesh;\n        }\n    }\n\n    return nullptr;\n}\n\nint32_t Object_GetMeshOffset(const OBJECT_MESH *const mesh)\n{\n    return (int32_t)(intptr_t)mesh->priv;\n}\n\nvoid Object_SetMeshOffset(OBJECT_MESH *const mesh, const int32_t data_offset)\n{\n    mesh->priv = (void *)(intptr_t)data_offset;\n}\n\nvoid Object_SwapMesh(\n    const OBJECT_ID object1_id, const OBJECT_ID object2_id,\n    const int32_t mesh_num)\n{\n    Object_SwapMeshEx(object1_id, object2_id, mesh_num, mesh_num);\n}\n\nvoid Object_SwapAllMeshes(\n    const OBJECT_ID object1_id, const OBJECT_ID object2_id)\n{\n    const OBJECT *const obj1 = Object_Get(object1_id);\n    const OBJECT *const obj2 = Object_Get(object2_id);\n    if (!obj1->loaded || !obj2->loaded) {\n        return;\n    }\n\n    const int32_t mesh_count = MIN(obj1->mesh_count, obj2->mesh_count);\n    for (int32_t i = 0; i < mesh_count; i++) {\n        Object_SwapMeshEx(object1_id, object2_id, i, i);\n    }\n}\n\nvoid Object_SwapMeshEx(\n    const OBJECT_ID object1_id, const OBJECT_ID object2_id,\n    const int32_t mesh_num1, const int32_t mesh_num2)\n{\n    const OBJECT *const obj1 = Object_Get(object1_id);\n    const OBJECT *const obj2 = Object_Get(object2_id);\n    if (!obj1->loaded || !obj2->loaded) {\n        return;\n    }\n\n    const int32_t mesh_idx1 = obj1->mesh_idx + mesh_num1;\n    const int32_t mesh_idx2 = obj2->mesh_idx + mesh_num2;\n    SWAP(m_MeshPointers[mesh_idx1], m_MeshPointers[mesh_idx2]);\n\n    Output_DispatchObjectMeshSwap(mesh_idx1, mesh_idx2);\n}\n\nANIM *Object_GetAnim(const OBJECT *const obj, const int32_t anim_idx)\n{\n    return Anim_GetAnim(obj->anim_idx + anim_idx);\n}\n\nANIM_BONE *Object_GetBone(const OBJECT *const obj, const int32_t bone_idx)\n{\n    return Anim_GetBone(obj->bone_idx + bone_idx);\n}\n\nANIM_BONE *Object_TryGetBone(const OBJECT *const obj, const int32_t bone_idx)\n{\n    return Anim_TryGetBone(obj->bone_idx + bone_idx);\n}\n\nOBJECT_ID Object_FindReceptacleKey(const OBJECT_ID receptacle_obj_id)\n{\n    return Object_GetCognateInverse(\n        receptacle_obj_id, g_KeyItemToReceptacleMap);\n}\n\nint16_t Object_FindReceptacle(const OBJECT_ID object_id)\n{\n    // Iterate through all matching receptacles\n    const GAME_OBJECT_PAIR *const map = g_KeyItemToReceptacleMap;\n    for (int32_t i = 0; map[i].key_id != NO_OBJECT; i++) {\n        if (map[i].key_id != object_id) {\n            continue;\n        }\n\n        // Iterate through all level items that match this receptacle\n        const OBJECT_ID receptacle_to_check = map[i].value_id;\n        for (int16_t item_num = 0; item_num < Item_GetLevelCount();\n             item_num++) {\n            const ITEM *const item = Item_Get(item_num);\n            if (item->object_id != receptacle_to_check) {\n                continue;\n            }\n\n            const OBJECT *const obj = Object_Get(item->object_id);\n            if (obj->is_usable_func != nullptr\n                && !obj->is_usable_func(item_num)) {\n                continue;\n            }\n\n            // If Lara is standing near one, that's our keyhole\n            if (Lara_TestPosition(item, obj->bounds_func())) {\n                return item_num;\n            }\n        }\n    }\n\n    return NO_ITEM;\n}\n\nbool Object_CanInterpolate(\n    const ITEM *const item, const int32_t frame_a, const int32_t frame_b)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    return item->enable_interpolation && obj->enable_interpolation\n        && item->prev_frame_num != item->frame_num;\n}\n\nvoid Object_SetReflective(const OBJECT_ID obj_id, const bool enabled)\n{\n    const OBJECT *const obj = Object_Get(obj_id);\n    for (int32_t i = 0; i < obj->mesh_count; i++) {\n        Object_SetMeshReflective(obj_id, i, enabled);\n    }\n}\n\nvoid Object_SetMeshReflective(\n    const OBJECT_ID obj_id, const int32_t mesh_idx, const bool enabled)\n{\n    const OBJECT *const obj = Object_Get(obj_id);\n    if (!obj->loaded) {\n        return;\n    }\n\n    Object_SetMeshReflectiveEx(obj->mesh_idx + mesh_idx, enabled);\n}\n\nvoid Object_SetMeshReflectiveEx(const int32_t abs_mesh_idx, const bool enabled)\n{\n    OBJECT_MESH *const mesh = Object_GetMesh(abs_mesh_idx);\n    mesh->enable_reflections = enabled;\n    for (int32_t i = 0; i < mesh->all_faces.count; i++) {\n        mesh->all_faces.data[i].enable_reflections = enabled;\n    }\n    Output_DispatchObjectMeshUpdate(abs_mesh_idx);\n}\n"
  },
  {
    "path": "src/trx/game/objects/common.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/anims.h>\n#include <trx/game/collision.h>\n#include <trx/game/items.h>\n#include <trx/game/objects/draw.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/objects/types.h>\n\nvoid Object_Reset(void);\n\n// Retrieve an object by its TRX internal index. Trying to retrieve an invalid\n// object is a fatal error.\nOBJECT *Object_Get(OBJECT_ID object_id);\n\n// Retrieve an object by its TRX internal index. Trying to retrieve an invalid\n// object returns nullptr.\nOBJECT *Object_TryGet(OBJECT_ID object_id);\n\n// Retrieve an object by its game ID. Returns nullptr if not found.\nOBJECT *Object_GetByGameID(int32_t game_id);\n\n// Convert a game ID to OBJECT_ID.\nOBJECT_ID Object_FromGameID(int32_t game_id);\n\n// Convert a OBJECT_ID to a game ID (opposite of Object_FromGameID).\nint32_t Object_ToGameID(OBJECT_ID object_id);\n\n// Other functions ============================================================\n\nvoid Object_InitialiseStaticObjects3D(int32_t count);\nvoid Object_InitialiseStaticObjects2D(int32_t count);\nint32_t Object_GetStaticObjects3DCount(void);\nint32_t Object_GetStaticObjects2DCount(void);\n\nbool Object_IsValidStatid3D(int32_t static_id);\nSTATIC_OBJECT_3D *Object_Get3DStatic(int32_t static_id);\nSTATIC_OBJECT_2D *Object_Get2DStatic(int32_t static_id);\n\nbool Object_IsType(OBJECT_ID object_id, const OBJECT_ID *test_arr);\n\nOBJECT_ID Object_GetCognate(OBJECT_ID key_id, const GAME_OBJECT_PAIR *test_map);\n\nOBJECT_ID Object_GetCognateInverse(\n    OBJECT_ID value_id, const GAME_OBJECT_PAIR *test_map);\n\nvoid Object_InitialiseMeshes(int32_t mesh_count);\nvoid Object_StoreMesh(OBJECT_MESH *mesh);\n\nint32_t Object_GetMeshCount(void);\nOBJECT_MESH *Object_FindMesh(int32_t data_offset);\nint32_t Object_GetMeshIndex(const OBJECT_MESH *mesh);\nint32_t Object_GetMeshOffset(const OBJECT_MESH *mesh);\nvoid Object_SetMeshOffset(OBJECT_MESH *mesh, int32_t data_offset);\n\nOBJECT_MESH *Object_GetMesh(int32_t index);\nint32_t Object_GetItemMeshIndex(const ITEM *item, int32_t mesh_idx);\nvoid Object_SwapMesh(\n    OBJECT_ID object1_id, OBJECT_ID object2_id, int32_t mesh_num);\nvoid Object_SwapAllMeshes(OBJECT_ID object1_id, OBJECT_ID object2_id);\nvoid Object_SwapMeshEx(\n    OBJECT_ID object1_id, OBJECT_ID object2_id, int32_t mesh_num1,\n    int32_t mesh_num2);\n\nANIM *Object_GetAnim(const OBJECT *obj, int32_t anim_idx);\nANIM_BONE *Object_GetBone(const OBJECT *obj, int32_t bone_idx);\nANIM_BONE *Object_TryGetBone(const OBJECT *obj, int32_t bone_idx);\n\n// Given a key or puzzle object, find a matching receptacle item number to\n// establish the interaction target. Takes into account current Lara's\n// position.\nint16_t Object_FindReceptacle(OBJECT_ID obj_id);\n\n// Given a receptacle object ID, find a matching key/puzzle object ID.\nOBJECT_ID Object_FindReceptacleKey(const OBJECT_ID receptacle_obj_id);\n\nvoid Object_SetReflective(OBJECT_ID obj_id, bool enabled);\nvoid Object_SetMeshReflective(OBJECT_ID obj_id, int32_t mesh_idx, bool enabled);\nvoid Object_SetMeshReflectiveEx(int32_t abs_mesh_idx, bool enabled);\n\nbool Object_CanInterpolate(const ITEM *item, int32_t frame_a, int32_t frame_b);\n\n#define REGISTER_OBJECT(object_id, setup_func_)                                \\\n    __attribute__((constructor)) static void M_RegisterObject##object_id(void) \\\n    {                                                                          \\\n        Object_Get(object_id)->setup_func = setup_func_;                       \\\n    }\n"
  },
  {
    "path": "src/trx/game/objects/creatures/ape.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n#define APE_ATTACK_DAMAGE 200\n#define APE_TOUCH 0xFF00\n#define APE_DIE_ANIM 7\n#define APE_RUN_TURN (DEG_1 * 5) // = 910\n#define APE_DISPLAY_ANGLE (DEG_1 * 45) // = 8190\n#define APE_ATTACK_RANGE SQUARE(430) // = 184900\n#define APE_PANIC_RANGE SQUARE(WALL_L * 2) // = 4194304\n#define APE_JUMP_CHANCE 160\n#define APE_WARN1_CHANCE (APE_JUMP_CHANCE + 160) // = 320\n#define APE_WARN2_CHANCE (APE_WARN1_CHANCE + 160) // = 480\n#define APE_RUN_LEFT_CHANCE (APE_WARN2_CHANCE + 272) // = 752\n#define APE_ATTACK_FLAG 1\n#define APE_VAULT_ANIM 19\n#define APE_TURN_L_FLAG 2\n#define APE_TURN_R_FLAG 4\n#define APE_SHIFT 75\n#define APE_HITPOINTS 22\n#define APE_RADIUS (WALL_L / 3) // = 341\n#define APE_SMARTNESS 0x7FFF\n\ntypedef enum {\n    APE_STATE_EMPTY = 0,\n    APE_STATE_STOP = 1,\n    APE_STATE_WALK = 2,\n    APE_STATE_RUN = 3,\n    APE_STATE_ATTACK = 4,\n    APE_STATE_DEATH = 5,\n    APE_STATE_WARNING_1 = 6,\n    APE_STATE_WARNING_2 = 7,\n    APE_STATE_RUN_LEFT = 8,\n    APE_STATE_RUN_RIGHT = 9,\n    APE_STATE_JUMP = 10,\n    APE_STATE_VAULT = 11,\n} APE_STATE;\n\nstatic BITE m_ApeBite = { .pos = { 0, -19, 75 }, .mesh_num = 15 };\n\nstatic bool M_Vault(int16_t item_num, int16_t angle)\n{\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const ape = item->creature_data;\n    int32_t x = item->pos.x >> WALL_SHIFT;\n    int32_t y = item->pos.y;\n    int32_t z = item->pos.z >> WALL_SHIFT;\n    int16_t room_num = item->room_num;\n\n    if (ape->flags & APE_TURN_L_FLAG) {\n        item->rot.y -= DEG_90;\n        ape->flags &= ~APE_TURN_L_FLAG;\n    } else if (ape->flags & APE_TURN_R_FLAG) {\n        item->rot.y += DEG_90;\n        ape->flags &= ~APE_TURN_R_FLAG;\n    }\n\n    Creature_Animate(item_num, angle, 0);\n\n    if (item->pos.y > y - STEP_L * 3 / 2) {\n        return false;\n    }\n\n    int32_t x_floor = item->pos.x >> WALL_SHIFT;\n    int32_t z_floor = item->pos.z >> WALL_SHIFT;\n\n    if (z == z_floor) {\n        if (x == x_floor) {\n            return false;\n        }\n\n        if (x >= x_floor) {\n            item->rot.y = -DEG_90;\n            item->pos.x = (x << WALL_SHIFT) + APE_SHIFT;\n        } else {\n            item->rot.y = DEG_90;\n            item->pos.x = (x_floor << WALL_SHIFT) - APE_SHIFT;\n        }\n    } else if (x == x_floor) {\n        if (z < z_floor) {\n            item->rot.y = 0;\n            item->pos.z = (z_floor << WALL_SHIFT) - APE_SHIFT;\n        } else {\n            item->rot.y = -DEG_180;\n            item->pos.z = (z << WALL_SHIFT) + APE_SHIFT;\n        }\n    }\n\n    item->floor = y;\n    item->pos.y = y;\n\n    Item_UpdateRoom(item_num, room_num);\n\n    return true;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const ape = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != APE_STATE_DEATH) {\n            item->current_anim_state = APE_STATE_DEATH;\n            Item_SwitchToAnim(\n                item, APE_DIE_ANIM + (int16_t)(Random_GetControl() / 0x4000),\n                0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, ape->maximum_turn);\n\n        if (item->hit_status || info.distance < APE_PANIC_RANGE) {\n            ape->flags |= APE_ATTACK_FLAG;\n        }\n\n        switch (item->current_anim_state) {\n        case APE_STATE_STOP:\n            if (ape->flags & APE_TURN_L_FLAG) {\n                item->rot.y -= DEG_90;\n                ape->flags &= ~APE_TURN_L_FLAG;\n            } else if (ape->flags & APE_TURN_R_FLAG) {\n                item->rot.y += DEG_90;\n                ape->flags &= ~APE_TURN_R_FLAG;\n            }\n\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.bite && info.distance < APE_ATTACK_RANGE) {\n                item->goal_anim_state = APE_STATE_ATTACK;\n            } else if (\n                !(ape->flags & APE_ATTACK_FLAG)\n                && info.zone_num == info.enemy_zone_num && info.ahead) {\n                int16_t random = Random_GetControl() >> 5;\n                if (random < APE_JUMP_CHANCE) {\n                    item->goal_anim_state = APE_STATE_JUMP;\n                } else if (random < APE_WARN1_CHANCE) {\n                    item->goal_anim_state = APE_STATE_WARNING_1;\n                } else if (random < APE_WARN2_CHANCE) {\n                    item->goal_anim_state = APE_STATE_WARNING_2;\n                } else if (random < APE_RUN_LEFT_CHANCE) {\n                    item->goal_anim_state = APE_STATE_RUN_LEFT;\n                    ape->maximum_turn = 0;\n                } else {\n                    item->goal_anim_state = APE_STATE_RUN_RIGHT;\n                    ape->maximum_turn = 0;\n                }\n            } else {\n                item->goal_anim_state = APE_STATE_RUN;\n            }\n            break;\n\n        case APE_STATE_RUN:\n            ape->maximum_turn = APE_RUN_TURN;\n            if (!ape->flags && info.angle > -APE_DISPLAY_ANGLE\n                && info.angle < APE_DISPLAY_ANGLE) {\n                item->goal_anim_state = APE_STATE_STOP;\n            } else if (info.ahead && (item->touch_bits & APE_TOUCH)) {\n                item->required_anim_state = APE_STATE_ATTACK;\n                item->goal_anim_state = APE_STATE_STOP;\n            } else if (ape->mood != MOOD_ESCAPE) {\n                int16_t random = Random_GetControl();\n                if (random < APE_JUMP_CHANCE) {\n                    item->required_anim_state = APE_STATE_JUMP;\n                    item->goal_anim_state = APE_STATE_STOP;\n                } else if (random < APE_WARN1_CHANCE) {\n                    item->required_anim_state = APE_STATE_WARNING_1;\n                    item->goal_anim_state = APE_STATE_STOP;\n                } else if (random < APE_WARN2_CHANCE) {\n                    item->required_anim_state = APE_STATE_WARNING_2;\n                    item->goal_anim_state = APE_STATE_STOP;\n                }\n            }\n            break;\n\n        case APE_STATE_RUN_LEFT:\n            if (!(ape->flags & APE_TURN_R_FLAG)) {\n                item->rot.y -= DEG_90;\n                ape->flags |= APE_TURN_R_FLAG;\n            }\n            item->goal_anim_state = APE_STATE_STOP;\n            break;\n\n        case APE_STATE_RUN_RIGHT:\n            if (!(ape->flags & APE_TURN_L_FLAG)) {\n                item->rot.y += DEG_90;\n                ape->flags |= APE_TURN_L_FLAG;\n            }\n            item->goal_anim_state = APE_STATE_STOP;\n            break;\n\n        case APE_STATE_ATTACK:\n            if (!item->required_anim_state && (item->touch_bits & APE_TOUCH)) {\n                Creature_Effect(item, &m_ApeBite, Spawn_Blood);\n                Lara_TakeDamage(APE_ATTACK_DAMAGE, true);\n                item->required_anim_state = APE_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n\n    if (item->current_anim_state == APE_STATE_VAULT) {\n        Creature_Animate(item_num, angle, 0);\n    } else if (M_Vault(item_num, angle)) {\n        ape->maximum_turn = 0;\n        item->current_anim_state = APE_STATE_VAULT;\n        Item_SwitchToAnim(item, APE_VAULT_ANIM, 0);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = APE_HITPOINTS;\n    obj->pivot_length = 250;\n    obj->radius = APE_RADIUS;\n    obj->smartness = APE_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_JUMPER);\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_APE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/atlantean.c",
    "content": "#include <trx/game/objects/creatures/atlantean.h>\n\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_CHARGE_DAMAGE     100\n#define M_LUNGE_DAMAGE      150\n#define M_PUNCH_DAMAGE      200\n#define M_PART_DAMAGE       100\n#define M_WALK_TURN         (DEG_1 * 2)             // = 364\n#define M_RUN_TURN          (DEG_1 * 6)             // = 1092\n#define M_POSE_CHANCE       80\n#define M_UNPOSE_CHANCE     256\n#define M_WALK_RANGE        SQUARE(WALL_L * 9 / 2)  // = 21233664\n#define M_ATTACK_1_RANGE    SQUARE(600)             // = 360000\n#define M_ATTACK_2_RANGE    SQUARE(WALL_L * 5 / 2)  // = 6553600\n#define M_ATTACK_3_RANGE    SQUARE(300)             // = 90000\n#define M_ATTACK_RANGE      SQUARE(WALL_L * 15 / 4) // = 14745600\n#define M_TOUCH_BITS        0b00000110'01111000     // = 0x678\n#define M_HITPOINTS         50\n#define M_RADIUS            (WALL_L / 3)            // = 341\n#define M_DEFAULT_SMARTNESS 0x7FFF\n#define M_SHOOTER_SMARTNESS 0x2000\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_ATTACK_1,\n    M_STATE_DEATH,\n    M_STATE_POSE,\n    M_STATE_ATTACK_2,\n    M_STATE_ATTACK_3,\n    M_STATE_AIM_1,\n    M_STATE_AIM_2,\n    M_STATE_SHOOT,\n    M_STATE_MUMMY,\n    M_STATE_FLY,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_FLAG_BULLET_1 = 1 << 0,\n    M_FLAG_BULLET_2 = 1 << 1,\n    M_FLAG_FLY      = 1 << 2,\n    M_FLAG_TWIST    = 1 << 3,\n    // clang-format on\n} M_FLAG;\n\nstatic bool m_EnableExplosions = true;\nstatic const BITE m_Bite = { .pos = { -27, 98, 0 }, .mesh_num = 10 };\nstatic const BITE m_Rocket = { .pos = { 51, 213, 0 }, .mesh_num = 14 };\nstatic const BITE m_Shard = { .pos = { -35, 269, 0 }, .mesh_num = 9 };\n\nstatic void M_InitialiseGround(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n    Item_Get(item_num)->mesh_bits = 0xFFE07FFF;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        Item_Explode(\n            item_num, -1, m_EnableExplosions ? M_PART_DAMAGE : -M_PART_DAMAGE);\n        Sound_Effect(SFX_ATLANTEAN_DEATH, &item->pos, SPM_NORMAL);\n        LOT_DisableBaddieAI(item_num);\n        Item_Kill(item_num);\n        item->status = IS_DEACTIVATED;\n        Carrier_TestItemDrops(item_num);\n        return;\n    }\n    creature->lot.setup.step = STEP_L;\n    creature->lot.setup.drop = -STEP_L;\n    creature->lot.setup.fly = 0;\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n\n    bool shoot_1 = false;\n    bool shoot_2 = false;\n    if (item->object_id != O_ATLANTEAN_GROUND\n        && Creature_CanTargetEnemy(item, &info)\n        && (info.zone_num != info.enemy_zone_num\n            || info.distance > M_ATTACK_RANGE)) {\n        if (info.angle > 0 && info.angle < DEG_45) {\n            shoot_1 = true;\n        } else if (info.angle < 0 && info.angle > -DEG_45) {\n            shoot_2 = true;\n        }\n    }\n\n    if (item->object_id == O_ATLANTEAN_WINGED) {\n        if (item->current_anim_state == M_STATE_FLY) {\n            if ((creature->flags & M_FLAG_FLY) != 0\n                && creature->mood != MOOD_ESCAPE\n                && info.zone_num == info.enemy_zone_num) {\n                creature->flags &= ~M_FLAG_FLY;\n            }\n\n            if ((creature->flags & M_FLAG_FLY) == 0) {\n                Creature_Mood(item, &info, true);\n            }\n\n            creature->lot.setup.step = WALL_L * 30;\n            creature->lot.setup.drop = -WALL_L * 30;\n            creature->lot.setup.fly = STEP_L / 8;\n            Creature_AIInfo(item, &info);\n        } else if (\n            (info.zone_num != info.enemy_zone_num && !shoot_1 && !shoot_2\n             && (!info.ahead || creature->mood == MOOD_BORED))\n            || creature->mood == MOOD_ESCAPE) {\n            creature->flags |= M_FLAG_FLY;\n        }\n    }\n\n    if (info.ahead) {\n        head = info.angle;\n    }\n\n    if (item->current_anim_state != M_STATE_FLY) {\n        Creature_Mood(item, &info, false);\n    } else if ((creature->flags & M_FLAG_FLY) != 0) {\n        Creature_Mood(item, &info, true);\n    }\n\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    switch (item->current_anim_state) {\n    case M_STATE_MUMMY:\n        item->goal_anim_state = M_STATE_STOP;\n        break;\n\n    case M_STATE_STOP:\n        creature->flags &= ~(M_FLAG_BULLET_1 | M_FLAG_BULLET_2 | M_FLAG_TWIST);\n        if ((creature->flags & M_FLAG_FLY) != 0) {\n            item->goal_anim_state = M_STATE_FLY;\n        } else if ((item->touch_bits & M_TOUCH_BITS) != 0) {\n            item->goal_anim_state = M_STATE_ATTACK_3;\n        } else if (info.bite && info.distance < M_ATTACK_3_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK_3;\n        } else if (info.bite && info.distance < M_ATTACK_1_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK_1;\n        } else if (shoot_1) {\n            item->goal_anim_state = M_STATE_AIM_1;\n        } else if (shoot_2) {\n            item->goal_anim_state = M_STATE_AIM_2;\n        } else if (\n            creature->mood == MOOD_BORED\n            || (creature->mood == MOOD_STALK && info.distance < M_WALK_RANGE)) {\n            item->goal_anim_state = M_STATE_POSE;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_POSE:\n        head = 0;\n        if (shoot_1 || shoot_2 || (creature->flags & M_FLAG_FLY) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_STALK) {\n            if (info.distance < M_WALK_RANGE) {\n                if (info.zone_num == info.enemy_zone_num\n                    || Random_GetControl() < M_UNPOSE_CHANCE) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            && Random_GetControl() < M_UNPOSE_CHANCE) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (\n            creature->mood == MOOD_ATTACK || creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_WALK:\n        creature->maximum_turn = M_WALK_TURN;\n        if (shoot_1 || shoot_2 || (creature->flags & M_FLAG_FLY) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood == MOOD_ATTACK || creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood == MOOD_BORED\n            || (creature->mood == MOOD_STALK\n                && info.zone_num != info.enemy_zone_num)) {\n            if (Random_GetControl() < M_POSE_CHANCE) {\n                item->goal_anim_state = M_STATE_POSE;\n            }\n        } else if (\n            creature->mood == MOOD_STALK && info.distance > M_WALK_RANGE) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_RUN:\n        creature->maximum_turn = M_RUN_TURN;\n        if ((creature->flags & M_FLAG_FLY) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if ((item->touch_bits & M_TOUCH_BITS) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.bite && info.distance < M_ATTACK_1_RANGE) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.ahead && info.distance < M_ATTACK_2_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK_2;\n        } else if (shoot_1 || shoot_2) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood == MOOD_BORED\n            || (creature->mood == MOOD_STALK && info.distance < M_WALK_RANGE)) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_ATTACK_1:\n        if (item->required_anim_state == M_STATE_EMPTY\n            && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Creature_Effect(item, &m_Bite, Spawn_Blood);\n            Lara_TakeDamage(M_LUNGE_DAMAGE, true);\n            item->required_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_ATTACK_2:\n        if (item->required_anim_state == M_STATE_EMPTY\n            && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Creature_Effect(item, &m_Bite, Spawn_Blood);\n            Lara_TakeDamage(M_CHARGE_DAMAGE, true);\n            item->required_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_ATTACK_3:\n        if (item->required_anim_state == M_STATE_EMPTY\n            && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Creature_Effect(item, &m_Bite, Spawn_Blood);\n            Lara_TakeDamage(M_PUNCH_DAMAGE, true);\n            item->required_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        creature->flags |= M_FLAG_TWIST;\n        creature->flags |= M_FLAG_BULLET_1;\n        if (shoot_1) {\n            item->goal_anim_state = M_STATE_SHOOT;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        creature->flags |= M_FLAG_BULLET_2;\n        if (shoot_2) {\n            item->goal_anim_state = M_STATE_SHOOT;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_SHOOT:\n        if ((creature->flags & M_FLAG_BULLET_1) != 0) {\n            creature->flags &= ~M_FLAG_BULLET_1;\n            Creature_Effect(item, &m_Shard, Spawn_AtlanteanShard);\n        } else if ((creature->flags & M_FLAG_BULLET_2) != 0) {\n            creature->flags &= ~M_FLAG_BULLET_2;\n            Creature_Effect(item, &m_Rocket, Spawn_AtlanteanBomb);\n        }\n        break;\n\n    case M_STATE_FLY:\n        if ((creature->flags & M_FLAG_FLY) == 0 && item->pos.y == item->floor) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n    }\n\n    if ((creature->flags & M_FLAG_TWIST) == 0) {\n        creature->head_rotation = creature->neck_rotation;\n    }\n\n    Creature_Head(item, head);\n\n    if ((creature->flags & M_FLAG_TWIST) == 0) {\n        creature->neck_rotation = creature->head_rotation;\n        creature->head_rotation = 0;\n    } else {\n        creature->neck_rotation = 0;\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_SetupWinged(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 3;\n    obj->hit_points = M_HITPOINTS;\n    obj->pivot_length = 150;\n    obj->radius = M_RADIUS;\n    obj->smartness = M_DEFAULT_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST);\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 2)->rot.y = true;\n}\n\nstatic void M_SetupShooter(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    *obj = *Object_Get(O_ATLANTEAN_WINGED);\n    obj->setup_func = M_SetupShooter;\n    obj->initialise_func = M_InitialiseGround;\n    obj->smartness = M_SHOOTER_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_DEFAULT);\n}\n\nstatic void M_SetupGround(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    *obj = *Object_Get(O_ATLANTEAN_WINGED);\n    obj->setup_func = M_SetupGround;\n    obj->initialise_func = M_InitialiseGround;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_DEFAULT);\n}\n\nvoid Atlantean_ToggleExplosions(bool enable)\n{\n    m_EnableExplosions = enable;\n}\n\nREGISTER_OBJECT(O_ATLANTEAN_WINGED, M_SetupWinged)\nREGISTER_OBJECT(O_ATLANTEAN_SHOOTER, M_SetupShooter)\nREGISTER_OBJECT(O_ATLANTEAN_GROUND, M_SetupGround)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/atlantean.h",
    "content": "#pragma once\n\nvoid Atlantean_ToggleExplosions(bool enable);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bacon_lara.c",
    "content": "#include <trx/game/objects/creatures/bacon_lara.h>\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\n#define M_SMASH_JUMP_FRAME 1\n\ntypedef struct {\n    bool status;\n} M_PRIV;\n\nstatic int32_t m_AnchorX = -1;\nstatic int32_t m_AnchorZ = -1;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"status\", &p->status));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"status\", p->status);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    const OBJECT *const lara_obj = Object_Get(O_LARA);\n    OBJECT *const bacon_obj = Object_Get(O_BACON_LARA);\n    bacon_obj->anim_idx = lara_obj->anim_idx;\n    bacon_obj->frame_base = lara_obj->frame_base;\n    p->status = false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (m_AnchorX == -1) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (Item_IsTriggerActive(item)) {\n        if (!LOT_EnableBaddieAI(item_num, true)) {\n            return;\n        }\n        item->status = IS_ACTIVE;\n    }\n\n    if (item->hit_points < LARA_MAX_HITPOINTS) {\n        Lara_TakeDamage((LARA_MAX_HITPOINTS - item->hit_points) * 10, false);\n        item->hit_points = LARA_MAX_HITPOINTS;\n    }\n\n    if (!p->status) {\n        const XYZ_32 pos = {\n            .x = 2 * m_AnchorX - lara_item->pos.x,\n            .z = 2 * m_AnchorZ - lara_item->pos.z,\n            .y = lara_item->pos.y,\n        };\n\n        int16_t room_num = item->room_num;\n        const SECTOR *sector = Room_GetSector(pos, &room_num);\n        const int32_t h = Room_GetHeight(sector, pos);\n        item->floor = h;\n\n        room_num = lara_item->room_num;\n        sector = Room_GetSector(lara_item->pos, &room_num);\n        int32_t lh = Room_GetHeight(sector, lara_item->pos);\n\n        const int16_t relative_anim = Item_GetRelativeAnim(lara_item);\n        const int16_t relative_frame = Item_GetRelativeFrame(lara_item);\n        Item_SwitchToObjAnim(item, relative_anim, relative_frame, O_LARA);\n        item->pos = pos;\n        item->rot = lara_item->rot;\n        item->rot.y -= DEG_180;\n        Item_UpdateRoom(item_num, lara_item->room_num);\n\n        if (h >= lh + WALL_L && !lara_item->gravity) {\n            item->current_anim_state = LS(LS_FAST_FALL);\n            item->goal_anim_state = LS(LS_FAST_FALL);\n            Item_SwitchToAnim(item, LA(LA_SMASH_JUMP), M_SMASH_JUMP_FRAME);\n            item->speed = 0;\n            item->fall_speed = 0;\n            item->gravity = true;\n            item->pos.y += 50;\n            p->status = true;\n        }\n    }\n\n    if (p->status) {\n        Item_Animate(item);\n\n        int16_t room_num = item->room_num;\n        const SECTOR *sector = Room_GetSector(item->pos, &room_num);\n        const int32_t h = Room_GetHeight(sector, item->pos);\n        item->floor = h;\n\n        Room_TestTriggers(item);\n        if (item->pos.y >= h) {\n            item->floor = h;\n            item->pos.y = h;\n            Room_TestTriggers(item);\n            item->gravity = false;\n            item->fall_speed = 0;\n            item->goal_anim_state = LS(LS_DEATH);\n            item->required_anim_state = LS(LS_DEATH);\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    if (p->status || item->current_anim_state == LS(LS_DEATH)) {\n        return Object_DrawAnimatingItem(item);\n    }\n\n    OBJECT_MESH *old_mesh_ptrs[LM_NUMBER_OF];\n\n    for (LARA_MESH mesh = LM_FIRST; mesh < LM_NUMBER_OF; mesh++) {\n        old_mesh_ptrs[mesh] = Lara_Mesh_Get(mesh);\n        Lara_Mesh_SwapSingle(mesh, O_BACON_LARA);\n    }\n\n    Lara_Draw(item);\n\n    for (LARA_MESH mesh = LM_FIRST; mesh < LM_NUMBER_OF; mesh++) {\n        Lara_Mesh_Set(mesh, old_mesh_ptrs[mesh]);\n    }\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->collision_func = Creature_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->hit_points = LARA_MAX_HITPOINTS;\n    obj->shadow_size = (UNIT_SHADOW * 10) / 16;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nbool BaconLara_InitialiseAnchor(const int32_t room_index)\n{\n    if (room_index >= Room_GetCount()) {\n        return false;\n    }\n    const ROOM *const room = Room_Get(room_index);\n    m_AnchorX = room->pos.x + room->size.x * (WALL_L >> 1);\n    m_AnchorZ = room->pos.z + room->size.z * (WALL_L >> 1);\n    return true;\n}\n\nREGISTER_OBJECT(O_BACON_LARA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bacon_lara.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nbool BaconLara_InitialiseAnchor(int32_t room_index);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/baldy.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/spawn.h>\n\n#define BALDY_SHOT_DAMAGE 150\n#define BALDY_WALK_TURN (DEG_1 * 3) // = 546\n#define BALDY_RUN_TURN (DEG_1 * 6) // = 1092\n#define BALDY_WALK_RANGE SQUARE(WALL_L * 4) // = 16777216\n#define BALDY_DIE_ANIM 14\n#define BALDY_HITPOINTS 200\n#define BALDY_RADIUS (WALL_L / 10) // = 102\n#define BALDY_SMARTNESS 0x7FFF\n\ntypedef enum {\n    BALDY_STATE_EMPTY = 0,\n    BALDY_STATE_STOP = 1,\n    BALDY_STATE_WALK = 2,\n    BALDY_STATE_RUN = 3,\n    BALDY_STATE_AIM = 4,\n    BALDY_STATE_DEATH = 5,\n    BALDY_STATE_SHOOT = 6,\n} BALDY_STATE;\n\nstatic const CREATURE_GUN m_BaldyGun = {\n    .muzzle = { .pos = { -20, 440, 20 }, .mesh_num = 9 },\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n    Item_Get(item_num)->current_anim_state = BALDY_STATE_RUN;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->hit_points <= 0) {\n            const uint16_t flags =\n                Music_GetTrackFlags(Music_ToGameID(MX_BALDY_SPEECH));\n            Music_SetTrackFlags(\n                Music_ToGameID(MX_BALDY_SPEECH), flags | IF_ONE_SHOT);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const baldy = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != BALDY_STATE_DEATH) {\n            item->current_anim_state = BALDY_STATE_DEATH;\n            Item_SwitchToAnim(item, BALDY_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, baldy->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case BALDY_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = BALDY_STATE_AIM;\n            } else if (baldy->mood == MOOD_BORED) {\n                item->goal_anim_state = BALDY_STATE_WALK;\n            } else {\n                item->goal_anim_state = BALDY_STATE_RUN;\n            }\n            break;\n\n        case BALDY_STATE_WALK:\n            baldy->maximum_turn = BALDY_WALK_TURN;\n            if (baldy->mood == MOOD_ESCAPE || !info.ahead) {\n                item->required_anim_state = BALDY_STATE_RUN;\n                item->goal_anim_state = BALDY_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = BALDY_STATE_AIM;\n                item->goal_anim_state = BALDY_STATE_STOP;\n            } else if (info.distance > BALDY_WALK_RANGE) {\n                item->required_anim_state = BALDY_STATE_RUN;\n                item->goal_anim_state = BALDY_STATE_STOP;\n            }\n            break;\n\n        case BALDY_STATE_RUN:\n            baldy->maximum_turn = BALDY_RUN_TURN;\n            tilt = angle / 2;\n            if (baldy->mood != MOOD_ESCAPE || info.ahead) {\n                if (Creature_CanTargetEnemy(item, &info)) {\n                    item->required_anim_state = BALDY_STATE_AIM;\n                    item->goal_anim_state = BALDY_STATE_STOP;\n                } else if (info.ahead && info.distance < BALDY_WALK_RANGE) {\n                    item->required_anim_state = BALDY_STATE_WALK;\n                    item->goal_anim_state = BALDY_STATE_STOP;\n                }\n            }\n            break;\n\n        case BALDY_STATE_AIM:\n            baldy->flags = 0;\n            if (item->required_anim_state) {\n                item->goal_anim_state = BALDY_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = BALDY_STATE_SHOOT;\n            } else {\n                item->goal_anim_state = BALDY_STATE_STOP;\n            }\n            break;\n\n        case BALDY_STATE_SHOOT:\n            if (!baldy->flags) {\n                info.distance /= 2;\n                Creature_Shoot(\n                    item, &info, &m_BaldyGun, head, BALDY_SHOT_DAMAGE);\n                baldy->flags = 1;\n            }\n            if (baldy->mood == MOOD_ESCAPE) {\n                item->required_anim_state = BALDY_STATE_RUN;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = BALDY_HITPOINTS;\n    obj->radius = BALDY_RADIUS;\n    obj->smartness = BALDY_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_BALDY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bandit_1.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/creatures/bandit_common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BANDIT_1_HITPOINTS      45\n#define BANDIT_1_SHOOT_DAMAGE   8\n#define BANDIT_1_WALK_TURN      (DEG_1 * 4) // = 728\n#define BANDIT_1_RUN_TURN       (DEG_1 * 6) // = 1092\n#define BANDIT_1_WALK_RANGE     SQUARE(WALL_L * 2) // = 4194304\n#define BANDIT_1_SHOOT_1_CHANCE 0x2000\n#define BANDIT_1_SHOOT_2_CHANCE 0x4000\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BANDIT_1_STATE_EMPTY    = 0,\n    BANDIT_1_STATE_WAIT     = 1,\n    BANDIT_1_STATE_WALK     = 2,\n    BANDIT_1_STATE_RUN      = 3,\n    BANDIT_1_STATE_AIM_1    = 4,\n    BANDIT_1_STATE_SHOOT_1  = 5,\n    BANDIT_1_STATE_AIM_2    = 6,\n    BANDIT_1_STATE_SHOOT_2  = 7,\n    BANDIT_1_STATE_SHOOT_3A = 8,\n    BANDIT_1_STATE_SHOOT_3B = 9,\n    BANDIT_1_STATE_SHOOT_4A = 10,\n    BANDIT_1_STATE_AIM_3    = 11,\n    BANDIT_1_STATE_AIM_4    = 12,\n    BANDIT_1_STATE_DEATH    = 13,\n    BANDIT_1_STATE_SHOOT_4B = 14,\n    // clang-format on\n} BANDIT_1_STATE;\n\ntypedef enum {\n    BANDIT_1_ANIM_DEATH = 14,\n} BANDIT_1_ANIM;\n\nstatic const CREATURE_GUN m_Bandit1Gun = {\n    .muzzle = { .pos = { .x = -2, .y = 150, .z = 19 }, .mesh_num = 17 },\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        item->hit_points = 0;\n        if (item->current_anim_state != BANDIT_1_STATE_DEATH) {\n            Item_SwitchToAnim(item, BANDIT_1_ANIM_DEATH, 0);\n            item->current_anim_state = BANDIT_1_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case BANDIT_1_STATE_WAIT:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = BANDIT_1_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance > BANDIT_1_WALK_RANGE) {\n                    item->goal_anim_state = BANDIT_1_STATE_WALK;\n                } else {\n                    const int32_t random = Random_GetControl();\n                    if (random < BANDIT_1_SHOOT_1_CHANCE) {\n                        item->goal_anim_state = BANDIT_1_STATE_SHOOT_1;\n                    } else if (random < BANDIT_1_SHOOT_2_CHANCE) {\n                        item->goal_anim_state = BANDIT_1_STATE_SHOOT_2;\n                    } else {\n                        item->goal_anim_state = BANDIT_1_STATE_AIM_3;\n                    }\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (info.ahead) {\n                    item->goal_anim_state = BANDIT_1_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = BANDIT_1_STATE_WALK;\n                }\n            } else {\n                item->goal_anim_state = BANDIT_1_STATE_RUN;\n            }\n            break;\n\n        case BANDIT_1_STATE_WALK:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = BANDIT_1_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = BANDIT_1_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance > BANDIT_1_WALK_RANGE\n                    && info.zone_num == info.enemy_zone_num) {\n                    item->goal_anim_state = BANDIT_1_STATE_AIM_4;\n                } else {\n                    item->goal_anim_state = BANDIT_1_STATE_WAIT;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (info.ahead) {\n                    item->goal_anim_state = BANDIT_1_STATE_WALK;\n                } else {\n                    item->goal_anim_state = BANDIT_1_STATE_WAIT;\n                }\n            } else {\n                item->goal_anim_state = BANDIT_1_STATE_RUN;\n            }\n            break;\n\n        case BANDIT_1_STATE_RUN:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            tilt = angle / 2;\n            creature->maximum_turn = BANDIT_1_RUN_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = BANDIT_1_STATE_WAIT;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = BANDIT_1_STATE_WALK;\n            }\n            break;\n\n        case BANDIT_1_STATE_SHOOT_1:\n        case BANDIT_1_STATE_SHOOT_2:\n        case BANDIT_1_STATE_SHOOT_3A:\n        case BANDIT_1_STATE_SHOOT_3B:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (!Creature_Shoot(\n                    item, &info, &m_Bandit1Gun, head, BANDIT_1_SHOOT_DAMAGE)) {\n                item->goal_anim_state = BANDIT_1_STATE_WAIT;\n            }\n            break;\n\n        case BANDIT_1_STATE_SHOOT_4A:\n        case BANDIT_1_STATE_SHOOT_4B:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (!Creature_Shoot(\n                    item, &info, &m_Bandit1Gun, head, BANDIT_1_SHOOT_DAMAGE)) {\n                item->goal_anim_state = BANDIT_1_STATE_WALK;\n            }\n            if (info.distance < BANDIT_1_WALK_RANGE) {\n                item->goal_anim_state = BANDIT_1_STATE_WALK;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = BANDIT_1_HITPOINTS;\n    obj->radius = BANDIT_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 8)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_BANDIT_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bandit_2.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/creatures/bandit_common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BANDIT_2_HITPOINTS      50\n#define BANDIT_2_SHOOT_DAMAGE   50\n#define BANDIT_2_WALK_TURN      (DEG_1 * 4) // = 728\n#define BANDIT_2_RUN_TURN       (DEG_1 * 6) // = 1092\n#define BANDIT_2_WALK_RANGE     SQUARE(WALL_L * 2) // = 4194304\n#define BANDIT_2_WALK_CHANCE    0x4000\n#define BANDIT_2_SHOOT_1_CHANCE 0x2000\n#define BANDIT_2_SHOOT_2_CHANCE 0x5000\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BANDIT_2_STATE_EMPTY    = 0,\n    BANDIT_2_STATE_AIM_4    = 1,\n    BANDIT_2_STATE_WAIT     = 2,\n    BANDIT_2_STATE_WALK     = 3,\n    BANDIT_2_STATE_RUN      = 4,\n    BANDIT_2_STATE_AIM_1    = 5,\n    BANDIT_2_STATE_AIM_2    = 6,\n    BANDIT_2_STATE_SHOOT_1  = 7,\n    BANDIT_2_STATE_SHOOT_2  = 8,\n    BANDIT_2_STATE_SHOOT_4A = 9,\n    BANDIT_2_STATE_SHOOT_4B = 10,\n    BANDIT_2_STATE_DEATH    = 11,\n    BANDIT_2_STATE_AIM_5    = 12,\n    BANDIT_2_STATE_SHOOT_5  = 13,\n    // clang-format on\n} BANDIT_2_STATE;\n\ntypedef enum {\n    BANDIT_2_ANIM_DEATH = 9,\n} BANDIT_2_ANIM;\n\nstatic const CREATURE_GUN m_Bandit2Gun = {\n    .muzzle = { .pos = { .x = -1, .y = 230, .z = 9 }, .mesh_num = 17 },\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != BANDIT_2_STATE_DEATH) {\n            Item_SwitchToAnim(item, BANDIT_2_ANIM_DEATH, 0);\n            item->current_anim_state = BANDIT_2_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case BANDIT_2_STATE_WAIT:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = BANDIT_2_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance > BANDIT_2_WALK_RANGE\n                    && Random_GetControl() < BANDIT_2_WALK_CHANCE) {\n                    item->goal_anim_state = BANDIT_2_STATE_WALK;\n                } else {\n                    const int32_t random = Random_GetControl();\n                    if (random < BANDIT_2_SHOOT_1_CHANCE) {\n                        item->goal_anim_state = BANDIT_2_STATE_SHOOT_1;\n                    } else if (random < BANDIT_2_SHOOT_2_CHANCE) {\n                        item->goal_anim_state = BANDIT_2_STATE_SHOOT_2;\n                    } else {\n                        item->goal_anim_state = BANDIT_2_STATE_AIM_5;\n                    }\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (!info.ahead || Random_GetControl() < 0x100) {\n                    item->goal_anim_state = BANDIT_2_STATE_WALK;\n                }\n            } else {\n                item->goal_anim_state = BANDIT_2_STATE_RUN;\n            }\n            break;\n\n        case BANDIT_2_STATE_WALK:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = BANDIT_2_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = BANDIT_2_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance < BANDIT_2_WALK_RANGE\n                    || info.zone_num == info.enemy_zone_num\n                    || Random_GetControl() < 0x400) {\n                    item->goal_anim_state = BANDIT_2_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = BANDIT_2_STATE_AIM_4;\n                }\n            } else if (creature->mood != MOOD_BORED) {\n                item->goal_anim_state = BANDIT_2_STATE_RUN;\n            } else if (info.ahead && Random_GetControl() < 0x400) {\n                item->goal_anim_state = BANDIT_2_STATE_WAIT;\n            }\n            break;\n\n        case BANDIT_2_STATE_RUN:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = BANDIT_2_RUN_TURN;\n            tilt = angle / 2;\n            if (creature->mood == MOOD_ESCAPE) {\n            } else if (\n                creature->mood == MOOD_BORED\n                || Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = BANDIT_2_STATE_WAIT;\n            }\n            break;\n\n        case BANDIT_2_STATE_AIM_1:\n        case BANDIT_2_STATE_AIM_2:\n        case BANDIT_2_STATE_AIM_4:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            break;\n\n        case BANDIT_2_STATE_AIM_5:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = BANDIT_2_STATE_SHOOT_5;\n            } else {\n                item->goal_anim_state = BANDIT_2_STATE_WAIT;\n            }\n            break;\n\n        case BANDIT_2_STATE_SHOOT_1:\n        case BANDIT_2_STATE_SHOOT_2:\n        case BANDIT_2_STATE_SHOOT_5:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                if (!Creature_Shoot(\n                        item, &info, &m_Bandit2Gun, head, BANDIT_2_SHOOT_DAMAGE)\n                    || Random_GetControl() < 0x2000) {\n                    item->goal_anim_state = BANDIT_2_STATE_WAIT;\n                }\n                creature->flags = 1;\n            }\n            break;\n\n        case BANDIT_2_STATE_SHOOT_4A:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags != 1) {\n                if (!Creature_Shoot(\n                        item, &info, &m_Bandit2Gun, head,\n                        BANDIT_2_SHOOT_DAMAGE)) {\n                    item->goal_anim_state = BANDIT_2_STATE_WALK;\n                }\n                creature->flags = 1;\n            }\n            if (info.distance < BANDIT_2_WALK_RANGE) {\n                item->goal_anim_state = BANDIT_2_STATE_WALK;\n            }\n            break;\n\n        case BANDIT_2_STATE_SHOOT_4B:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags != 2) {\n                if (!Creature_Shoot(\n                        item, &info, &m_Bandit2Gun, head,\n                        BANDIT_2_SHOOT_DAMAGE)) {\n                    item->goal_anim_state = BANDIT_2_STATE_WALK;\n                }\n                creature->flags = 2;\n            }\n            if (info.distance < BANDIT_2_WALK_RANGE) {\n                item->goal_anim_state = BANDIT_2_STATE_WALK;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup2A(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = BANDIT_2_HITPOINTS;\n    obj->radius = BANDIT_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 8)->rot.y = true;\n}\n\nstatic void M_Setup2B(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    const OBJECT *const ref_obj = Object_Get(O_BANDIT_2);\n    if (ref_obj->loaded) {\n        obj->anim_idx = ref_obj->anim_idx;\n        obj->frame_base = ref_obj->frame_base;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = BANDIT_2_HITPOINTS;\n    obj->radius = BANDIT_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 8)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_BANDIT_2, M_Setup2A)\nREGISTER_OBJECT(O_BANDIT_2B, M_Setup2B)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bandit_common.h",
    "content": "#pragma once\n\n#define BANDIT_RADIUS (WALL_L / 10) // = 102\n"
  },
  {
    "path": "src/trx/game/objects/creatures/barracuda.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BARRACUDA_HITPOINTS   12\n#define BARRACUDA_TOUCH_BITS  0b11100000 // = 0xE0\n#define BARRACUDA_RADIUS      (WALL_L / 5) // = 204\n#define BARRACUDA_BITE_DAMAGE 100\n#define BARA_SWIM_1_TURN      (DEG_1 * 2) // = 364\n#define BARA_SWIM_2_TURN      (DEG_1 * 4) // = 728\n#define BARA_ATTACK_1_RANGE   SQUARE(WALL_L * 2 / 3) // = 465124\n#define BARA_ATTACK_2_RANGE   SQUARE(WALL_L / 3) // = 116281\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BARRACUDA_STATE_EMPTY    = 0,\n    BARRACUDA_STATE_STOP     = 1,\n    BARRACUDA_STATE_SWIM_1   = 2,\n    BARRACUDA_STATE_SWIM_2   = 3,\n    BARRACUDA_STATE_ATTACK_1 = 4,\n    BARRACUDA_STATE_ATTACK_2 = 5,\n    BARRACUDA_STATE_DEATH    = 6,\n    // clang-format on\n} BARRACUDA_STATE;\n\ntypedef enum {\n    BARRACUDA_ANIM_DEATH = 6,\n} BARRACUDA_ANIM;\n\nstatic const BITE m_BarracudaBite = {\n    .pos = { .x = 2, .y = -60, .z = 121 },\n    .mesh_num = 7,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        int16_t head = 0;\n        int16_t angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case BARRACUDA_STATE_STOP:\n            creature->flags = 0;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = BARRACUDA_STATE_SWIM_1;\n            } else if (info.ahead && info.distance < BARA_ATTACK_1_RANGE) {\n                item->goal_anim_state = BARRACUDA_STATE_ATTACK_1;\n            } else if (creature->mood == MOOD_STALK) {\n                item->goal_anim_state = BARRACUDA_STATE_SWIM_1;\n            } else {\n                item->goal_anim_state = BARRACUDA_STATE_SWIM_2;\n            }\n            break;\n\n        case BARRACUDA_STATE_SWIM_1:\n            creature->maximum_turn = BARA_SWIM_1_TURN;\n            if (creature->mood == MOOD_BORED) {\n            } else if (\n                info.ahead && (item->touch_bits & BARRACUDA_TOUCH_BITS) != 0) {\n                item->goal_anim_state = BARRACUDA_STATE_STOP;\n            } else if (creature->mood != MOOD_STALK) {\n                item->goal_anim_state = BARRACUDA_STATE_SWIM_2;\n            }\n            break;\n\n        case BARRACUDA_STATE_SWIM_2:\n            creature->maximum_turn = BARA_SWIM_2_TURN;\n            creature->flags = 0;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = BARRACUDA_STATE_SWIM_1;\n            } else if (info.ahead && info.distance < BARA_ATTACK_2_RANGE) {\n                item->goal_anim_state = BARRACUDA_STATE_ATTACK_2;\n            } else if (info.ahead && info.distance < BARA_ATTACK_1_RANGE) {\n                item->goal_anim_state = BARRACUDA_STATE_STOP;\n            } else if (creature->mood == MOOD_STALK) {\n                item->goal_anim_state = BARRACUDA_STATE_SWIM_1;\n            }\n            break;\n\n        case BARRACUDA_STATE_ATTACK_1:\n        case BARRACUDA_STATE_ATTACK_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0\n                && (item->touch_bits & BARRACUDA_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(BARRACUDA_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_BarracudaBite, Spawn_Blood);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n\n        Creature_Head(item, head);\n        Creature_Animate(item_num, angle, 0);\n        Creature_Underwater(item, STEP_L);\n    } else {\n        if (item->current_anim_state != BARRACUDA_ANIM_DEATH) {\n            Item_SwitchToAnim(item, BARRACUDA_ANIM_DEATH, 0);\n            item->current_anim_state = BARRACUDA_STATE_DEATH;\n            Carrier_TestItemDrops(item_num);\n        }\n        Creature_Float(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = BARRACUDA_HITPOINTS;\n    obj->radius = BARRACUDA_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 200;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_BARRACUDA, M_Setup)\nREGISTER_OBJECT(O_FISH, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bartoli.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/spawn.h>\n\n#define M_BOOM_TIME 130\n#define M_BARTOLI_RANGE (WALL_L * 5) // = 5120\n\ntypedef struct {\n    int16_t dragon_item_num;\n} M_PRIV;\n\nstatic void M_CreateBoom(const OBJECT_ID obj_id, const ITEM *const origin_item)\n{\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const sphere_item = Item_Get(item_num);\n    sphere_item->object_id = obj_id;\n    sphere_item->pos.x = origin_item->pos.x;\n    sphere_item->pos.y = origin_item->pos.y + 256;\n    sphere_item->pos.z = origin_item->pos.z;\n    sphere_item->room_num = origin_item->room_num;\n    sphere_item->shade.value_1 = -1;\n    Item_Initialise(item_num);\n    Item_AddActive(item_num);\n    sphere_item->status = IS_ACTIVE;\n}\n\nstatic void M_ConvertBartoliToDragon(const int16_t item_num)\n{\n    const ITEM *const bartoli_item = Item_Get(item_num);\n    const M_PRIV *const p = bartoli_item->priv;\n    const int16_t dragon_item_num = p->dragon_item_num;\n    if (dragon_item_num != NO_ITEM) {\n        ITEM *const dragon_item = Item_Get(dragon_item_num);\n        const OBJECT *const dragon_obj = Object_Get(dragon_item->object_id);\n        if (dragon_obj->activate_func != nullptr) {\n            dragon_obj->activate_func(dragon_item);\n        }\n    }\n    Item_Kill(item_num);\n}\n\nstatic bool M_CheckLaraProximity(const ITEM *const origin_item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = ABS(lara_item->pos.x - origin_item->pos.x);\n    const int32_t dz = ABS(lara_item->pos.z - origin_item->pos.z);\n    return dx < M_BARTOLI_RANGE && dz < M_BARTOLI_RANGE;\n}\n\nstatic int16_t M_GetCarrierItemNum(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->dragon_item_num;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->dragon_item_num = Item_CreateLevelItem();\n    ASSERT(p->dragon_item_num != NO_ITEM);\n\n    ITEM *const dragon_item = Item_Get(p->dragon_item_num);\n    dragon_item->object_id = O_DRAGON_BACK;\n    dragon_item->pos = item->pos;\n    dragon_item->rot.y = item->rot.y;\n    dragon_item->room_num = item->room_num;\n    Item_Initialise(p->dragon_item_num);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->timer == 0) {\n        if (M_CheckLaraProximity(item)) {\n            item->timer = 1;\n        }\n        return;\n    }\n\n    item->timer++;\n    if ((item->timer & 7) == 0) {\n        g_Camera.bounce = item->timer;\n    }\n\n    Spawn_MysticLight(item_num);\n    Item_Animate(item);\n\n    if (item->timer == M_BOOM_TIME + 0) {\n        M_CreateBoom(O_SPHERE_OF_DOOM_1, item);\n    } else if (item->timer == M_BOOM_TIME + 10) {\n        M_CreateBoom(O_SPHERE_OF_DOOM_2, item);\n    } else if (item->timer == M_BOOM_TIME + 20) {\n        M_CreateBoom(O_SPHERE_OF_DOOM_3, item);\n    } else if (item->timer >= M_BOOM_TIME + 20) {\n        M_ConvertBartoliToDragon(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->carrier_item_num_func = M_GetCarrierItemNum;\n    obj->priv_size = sizeof(M_PRIV);\n\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BARTOLI, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bat.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n#define BAT_ATTACK_DAMAGE 2\n#define BAT_TURN (20 * DEG_1) // = 3640\n#define BAT_HITPOINTS 1\n#define BAT_RADIUS (WALL_L / 10) // = 102\n#define BAT_SMARTNESS 0x400\n\ntypedef enum {\n    BAT_STATE_EMPTY = 0,\n    BAT_STATE_STOP = 1,\n    BAT_STATE_FLY = 2,\n    BAT_STATE_ATTACK = 3,\n    BAT_STATE_FALL = 4,\n    BAT_STATE_DEATH = 5,\n} BAT_STATE;\n\nstatic BITE m_BatBite = { .pos = { 0, 16, 45 }, .mesh_num = 4 };\n\nstatic void M_FixEmbeddedPosition(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status == IS_ACTIVE) {\n        return;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t ceiling = Room_GetCeiling(sector, item->pos);\n\n    // The bats animation and frame have to be changed to the hanging\n    // one to properly measure them. Save it so it can be restored\n    // after.\n    const int16_t old_anim = Item_GetRelativeAnim(item);\n    const int16_t old_frame = Item_GetRelativeFrame(item);\n\n    Item_SwitchToAnim(item, 0, 0);\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    Item_SwitchToAnim(item, old_anim, old_frame);\n\n    const int16_t bat_height = ABS(bounds->min.y);\n\n    // Only move the bat if it's above the calculated position,\n    // Palace Midas has many bats that aren't intended to be at\n    // ceiling level.\n    if (item->pos.y < ceiling + bat_height) {\n        item->pos.y = ceiling + bat_height;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const bat = item->creature_data;\n    int16_t angle = 0;\n    if (item->hit_points <= 0) {\n        if (item->pos.y < item->floor) {\n            item->gravity = true;\n            item->goal_anim_state = BAT_STATE_FALL;\n            item->speed = 0;\n        } else {\n            item->gravity = false;\n            item->fall_speed = 0;\n            item->goal_anim_state = BAT_STATE_DEATH;\n            item->pos.y = item->floor;\n        }\n        Creature_Animate(item_num, 0, 0);\n        return;\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n        angle = Creature_Turn(item, BAT_TURN);\n\n        switch (item->current_anim_state) {\n        case BAT_STATE_STOP:\n            item->goal_anim_state = BAT_STATE_FLY;\n            break;\n\n        case BAT_STATE_FLY:\n            if (item->touch_bits) {\n                item->goal_anim_state = BAT_STATE_ATTACK;\n                Creature_Animate(item_num, angle, 0);\n                return;\n            }\n            break;\n\n        case BAT_STATE_ATTACK:\n            if (item->touch_bits) {\n                Creature_Effect(item, &m_BatBite, Spawn_Blood);\n                Lara_TakeDamage(BAT_ATTACK_DAMAGE, true);\n            } else {\n                item->goal_anim_state = BAT_STATE_FLY;\n                bat->mood = MOOD_BORED;\n            }\n            break;\n        }\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n\n    // Almost all of the bats in the OG levels are embedded in the ceiling.\n    // This will move all bats up to the ceiling of their rooms and down\n    // by the height of their hanging animation.\n    M_FixEmbeddedPosition(item_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = BAT_HITPOINTS;\n    obj->radius = BAT_RADIUS;\n    obj->smartness = BAT_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_BAT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bear.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n// clang-format off\n#define BEAR_CHARGE_DAMAGE 3\n#define BEAR_SLAM_DAMAGE   200\n#define BEAR_ATTACK_DAMAGE 200\n#define BEAR_PAT_DAMAGE    400\n#define BEAR_TOUCH         0x2406C\n#define BEAR_ROAR_CHANCE   80\n#define BEAR_REAR_CHANCE   768\n#define BEAR_DROP_CHANCE   1536\n#define BEAR_REAR_RANGE    SQUARE(WALL_L * 2) // = 4194304\n#define BEAR_ATTACK_RANGE  SQUARE(WALL_L) // = 1048576\n#define BEAR_PAT_RANGE     SQUARE(600) // = 360000\n#define BEAR_FIX_PAT_RANGE SQUARE(300) // = 90000\n#define BEAR_RUN_TURN      (5 * DEG_1) // = 910\n#define BEAR_WALK_TURN     (2 * DEG_1) // = 364\n#define BEAR_EAT_RANGE     SQUARE(WALL_L * 3 / 4) // = 589824\n#define BEAR_HITPOINTS     (g_TRVersion == 1 ? 20 : 30)\n#define BEAR_RADIUS        (WALL_L / 3) // = 341\n#define BEAR_SMARTNESS     0x4000\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BEAR_STATE_STROLL   = 0,\n    BEAR_STATE_STOP     = 1,\n    BEAR_STATE_WALK     = 2,\n    BEAR_STATE_RUN      = 3,\n    BEAR_STATE_REAR     = 4,\n    BEAR_STATE_ROAR     = 5,\n    BEAR_STATE_ATTACK_1 = 6,\n    BEAR_STATE_ATTACK_2 = 7,\n    BEAR_STATE_EAT      = 8,\n    BEAR_STATE_DEATH    = 9,\n    // clang-format on\n} BEAR_STATE;\n\nstatic BITE m_BearHeadBite = { .pos = { 0, 96, 335 }, .mesh_num = 14 };\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    OBJECT *const obj = Object_Get(item->object_id);\n    obj->pivot_length = g_Config.gameplay.fix_bear_ai ? 0 : 500;\n\n    CREATURE *const bear = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        angle = Creature_Turn(item, DEG_1);\n\n        switch (item->current_anim_state) {\n        case BEAR_STATE_WALK:\n            item->goal_anim_state = BEAR_STATE_REAR;\n            break;\n\n        case BEAR_STATE_RUN:\n        case BEAR_STATE_STROLL:\n            item->goal_anim_state = BEAR_STATE_STOP;\n            break;\n\n        case BEAR_STATE_REAR:\n            bear->flags = 1;\n            item->goal_anim_state = BEAR_STATE_DEATH;\n            break;\n\n        case BEAR_STATE_STOP:\n            bear->flags = 0;\n            item->goal_anim_state = BEAR_STATE_DEATH;\n            break;\n\n        case BEAR_STATE_DEATH:\n            if (bear != nullptr && bear->flags != 0\n                && (item->touch_bits & BEAR_TOUCH) != 0) {\n                Lara_TakeDamage(BEAR_SLAM_DAMAGE, true);\n                bear->flags = 0;\n            }\n            break;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, bear->maximum_turn);\n\n        const bool dead_enemy = Lara_GetItem()->hit_points <= 0;\n        if (item->hit_status) {\n            bear->flags = 1;\n        }\n\n        switch ((int16_t)item->current_anim_state) {\n        case BEAR_STATE_STOP:\n            if (dead_enemy) {\n                if (info.bite && info.distance < BEAR_EAT_RANGE) {\n                    item->goal_anim_state = BEAR_STATE_EAT;\n                } else {\n                    item->goal_anim_state = BEAR_STATE_STROLL;\n                }\n            } else if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (bear->mood == MOOD_BORED) {\n                item->goal_anim_state = BEAR_STATE_STROLL;\n            } else {\n                item->goal_anim_state = BEAR_STATE_RUN;\n            }\n            break;\n\n        case BEAR_STATE_STROLL:\n            bear->maximum_turn = BEAR_WALK_TURN;\n            if (dead_enemy && (item->touch_bits & BEAR_TOUCH) && info.ahead) {\n                item->goal_anim_state = BEAR_STATE_STOP;\n            } else if (bear->mood != MOOD_BORED) {\n                item->goal_anim_state = BEAR_STATE_STOP;\n                if (bear->mood == MOOD_ESCAPE) {\n                    item->required_anim_state = BEAR_STATE_STROLL;\n                }\n            } else if (Random_GetControl() < BEAR_ROAR_CHANCE) {\n                item->required_anim_state = BEAR_STATE_ROAR;\n                item->goal_anim_state = BEAR_STATE_STOP;\n            }\n            break;\n\n        case BEAR_STATE_RUN:\n            bear->maximum_turn = BEAR_RUN_TURN;\n            if (item->touch_bits & BEAR_TOUCH) {\n                Lara_TakeDamage(BEAR_CHARGE_DAMAGE, true);\n            }\n            if (bear->mood == MOOD_BORED || dead_enemy) {\n                item->goal_anim_state = BEAR_STATE_STOP;\n            } else if (info.ahead && !item->required_anim_state) {\n                if (!bear->flags && info.distance < BEAR_REAR_RANGE\n                    && Random_GetControl() < BEAR_REAR_CHANCE) {\n                    item->required_anim_state = BEAR_STATE_REAR;\n                    item->goal_anim_state = BEAR_STATE_STOP;\n                } else if (info.distance < BEAR_ATTACK_RANGE) {\n                    item->goal_anim_state = BEAR_STATE_ATTACK_1;\n                }\n            }\n            break;\n\n        case BEAR_STATE_REAR:\n            if (bear->flags) {\n                item->required_anim_state = BEAR_STATE_STROLL;\n                item->goal_anim_state = BEAR_STATE_STOP;\n            } else if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (bear->mood == MOOD_BORED || bear->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = BEAR_STATE_STOP;\n            } else if (\n                info.bite\n                && info.distance\n                    < (g_Config.gameplay.fix_bear_ai ? BEAR_FIX_PAT_RANGE\n                                                     : BEAR_PAT_RANGE)) {\n                item->goal_anim_state = BEAR_STATE_ATTACK_2;\n            } else {\n                item->goal_anim_state = BEAR_STATE_WALK;\n            }\n            break;\n\n        case BEAR_STATE_WALK:\n            if (bear->flags) {\n                item->required_anim_state = BEAR_STATE_STROLL;\n                item->goal_anim_state = BEAR_STATE_REAR;\n            } else if (info.ahead && (item->touch_bits & BEAR_TOUCH)) {\n                item->goal_anim_state = BEAR_STATE_REAR;\n            } else if (bear->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = BEAR_STATE_REAR;\n                item->required_anim_state = BEAR_STATE_STROLL;\n            } else if (\n                bear->mood == MOOD_BORED\n                || Random_GetControl() < BEAR_ROAR_CHANCE) {\n                item->required_anim_state = BEAR_STATE_ROAR;\n                item->goal_anim_state = BEAR_STATE_REAR;\n            } else if (\n                info.distance > BEAR_REAR_RANGE\n                || Random_GetControl() < BEAR_DROP_CHANCE) {\n                item->required_anim_state = BEAR_STATE_STOP;\n                item->goal_anim_state = BEAR_STATE_REAR;\n            }\n            break;\n\n        case BEAR_STATE_ATTACK_1:\n            if (!item->required_anim_state && (item->touch_bits & BEAR_TOUCH)) {\n                Creature_Effect(item, &m_BearHeadBite, Spawn_Blood);\n                Lara_TakeDamage(BEAR_ATTACK_DAMAGE, true);\n                item->required_anim_state = BEAR_STATE_STOP;\n            }\n            break;\n\n        case BEAR_STATE_ATTACK_2:\n            if (!item->required_anim_state && (item->touch_bits & BEAR_TOUCH)) {\n                Lara_TakeDamage(BEAR_PAT_DAMAGE, true);\n                item->required_anim_state = BEAR_STATE_REAR;\n            }\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = BEAR_HITPOINTS;\n    obj->radius = BEAR_RADIUS;\n    obj->smartness = BEAR_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_BEAR, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/big_eel.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BIG_EEL_HITPOINTS  20\n#define BIG_EEL_TOUCH_BITS 0b00000001'10000000 // = 0x180\n#define BIG_EEL_DAMAGE     500\n#define BIG_EEL_ANGLE      (DEG_1 * 10) // = 1820\n#define BIG_EEL_RANGE      (WALL_L * 6) // = 6144\n#define BIG_EEL_MOVE       (WALL_L / 10) // = 102\n#define BIG_EEL_LENGTH     (WALL_L * 5 / 2) // = 2560\n#define BIG_EEL_SLIDE      (BIG_EEL_RANGE - BIG_EEL_LENGTH) // = 3584\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BIG_EEL_STATE_EMPTY  = 0,\n    BIG_EEL_STATE_ATTACK = 1,\n    BIG_EEL_STATE_STOP   = 2,\n    BIG_EEL_STATE_DEATH  = 3,\n    // clang-format on\n} BIG_EEL_STATE;\n\ntypedef enum {\n    BIG_EEL_ANIM_DEATH = 2,\n} BIG_EEL_ANIM;\n\ntypedef struct {\n    int32_t pos;\n} M_PRIV;\n\nstatic const BITE m_BigEelBite = {\n    .pos = { .x = 7, .y = 157, .z = 333 },\n    .mesh_num = 7,\n};\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const ITEM *const lara_item = Lara_GetItem();\n    M_PRIV *const p = item->priv;\n\n    int32_t pos = p->pos;\n    item->pos.z -= (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    item->pos.x -= ((pos * Math_Sin(item->rot.y)) >> W2V_SHIFT);\n\n    if (item->hit_points <= 0) {\n        if (pos < BIG_EEL_SLIDE) {\n            pos += BIG_EEL_MOVE;\n        }\n        if (item->current_anim_state != BIG_EEL_STATE_DEATH) {\n            Item_SwitchToAnim(item, BIG_EEL_ANIM_DEATH, 0);\n            item->current_anim_state = BIG_EEL_STATE_DEATH;\n        }\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        const int16_t angle = Math_Atan(dz, dx) - item->rot.y;\n        const int32_t distance = Math_Sqrt(SQUARE(dx) + SQUARE(dz));\n\n        switch (item->current_anim_state) {\n        case BIG_EEL_STATE_STOP:\n            if (pos > 0) {\n                pos -= BIG_EEL_MOVE;\n            }\n            if (distance <= BIG_EEL_RANGE && ABS(angle) < BIG_EEL_ANGLE) {\n                item->goal_anim_state = BIG_EEL_STATE_ATTACK;\n            }\n            break;\n\n        case BIG_EEL_STATE_ATTACK:\n            if (pos < distance - BIG_EEL_LENGTH) {\n                pos += BIG_EEL_MOVE;\n            }\n            if (item->required_anim_state == BIG_EEL_STATE_EMPTY\n                && (item->touch_bits & BIG_EEL_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(BIG_EEL_DAMAGE, true);\n                Creature_Effect(item, &m_BigEelBite, Spawn_Blood);\n                item->required_anim_state = BIG_EEL_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    item->pos.x += (pos * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z += (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    p->pos = pos;\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->priv_size = sizeof(M_PRIV);\n\n    obj->hit_points = BIG_EEL_HITPOINTS;\n\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BIG_EEL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/big_spider.c",
    "content": "#include <trx/game/objects/creatures/big_spider.h>\n\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BIG_SPIDER_HITPOINTS 40\n#define BIG_SPIDER_RADIUS    (WALL_L / 3) // = 341\n#define BIG_SPIDER_TURN      (DEG_1 * 4) // = 728\n#define BIG_SPIDER_DAMAGE    100\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BIG_SPIDER_STATE_EMPTY    = 0,\n    BIG_SPIDER_STATE_STOP     = 1,\n    BIG_SPIDER_STATE_WALK_1   = 2,\n    BIG_SPIDER_STATE_WALK_2   = 3,\n    BIG_SPIDER_STATE_ATTACK_1 = 4,\n    BIG_SPIDER_STATE_ATTACK_2 = 5,\n    BIG_SPIDER_STATE_ATTACK_3 = 6,\n    BIG_SPIDER_STATE_DEATH    = 7,\n    // clang-format on\n} BIG_SPIDER_STATE;\n\ntypedef enum {\n    BIG_SPIDER_ANIM_DEATH = 2,\n} BIG_SPIDER_ANIM;\n\nstatic const BITE m_SpiderBite = {\n    .pos = { .x = 0, .y = 0, .z = 41 },\n    .mesh_num = 1,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t tilt = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, BIG_SPIDER_TURN);\n\n        switch (item->current_anim_state) {\n        case BIG_SPIDER_STATE_STOP:\n            creature->flags = 0;\n            if (creature->mood == MOOD_BORED) {\n                break;\n            } else if (info.ahead && item->touch_bits != 0) {\n                item->goal_anim_state = BIG_SPIDER_STATE_ATTACK_1;\n            } else if (creature->mood == MOOD_STALK) {\n                item->goal_anim_state = BIG_SPIDER_STATE_WALK_1;\n            } else if (\n                creature->mood == MOOD_ESCAPE\n                || creature->mood == MOOD_ATTACK) {\n                item->goal_anim_state = BIG_SPIDER_STATE_WALK_2;\n            }\n            break;\n\n        case BIG_SPIDER_STATE_WALK_1:\n            if (creature->mood == MOOD_BORED) {\n                break;\n            } else if (info.ahead && item->touch_bits != 0) {\n                item->goal_anim_state = BIG_SPIDER_STATE_STOP;\n            } else if (\n                creature->mood == MOOD_ESCAPE\n                || creature->mood == MOOD_ATTACK) {\n                item->goal_anim_state = BIG_SPIDER_STATE_WALK_2;\n            }\n            break;\n\n        case BIG_SPIDER_STATE_WALK_2:\n            creature->flags = 0;\n            if (info.ahead && item->touch_bits != 0) {\n                item->goal_anim_state = BIG_SPIDER_STATE_STOP;\n            } else if (\n                creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) {\n                item->goal_anim_state = BIG_SPIDER_STATE_WALK_1;\n            }\n            break;\n\n        case BIG_SPIDER_STATE_ATTACK_1:\n            if (!creature->flags && item->touch_bits != 0) {\n                Lara_TakeDamage(BIG_SPIDER_DAMAGE, true);\n                Creature_Effect(item, &m_SpiderBite, Spawn_Blood);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    } else if (item->current_anim_state != BIG_SPIDER_STATE_DEATH) {\n        Item_SwitchToAnim(item, BIG_SPIDER_ANIM_DEATH, 0);\n        item->current_anim_state = BIG_SPIDER_STATE_DEATH;\n    }\n\n    Creature_Animate(item_num, angle, tilt);\n}\n\nvoid BigSpider_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = BIG_SPIDER_HITPOINTS;\n    obj->radius = BIG_SPIDER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BIG_SPIDER, BigSpider_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/big_spider.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n\nvoid BigSpider_Setup(OBJECT *obj);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bird.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_DAMAGE            20\n#define M_RADIUS            (WALL_L / 5) // = 204\n#define M_ATTACK_RANGE      SQUARE(WALL_L / 2) // = 262144\n#define M_TURN              (DEG_1 * 3) // = 546\n#define M_START_ANIM        5\n#define M_DIE_ANIM          8\n#define M_EAGLE_HITPOINTS   20\n#define M_CROW_HITPOINTS    (g_TRVersion == 3 ? 8 : 15)\n#define M_CROW_START_ANIM   14\n#define M_CROW_DIE_ANIM     1\n#define M_VULTURE_HITPOINTS 18\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY = 0,\n    M_STATE_FLY = 1,\n    M_STATE_STOP = 2,\n    M_STATE_GLIDE = 3,\n    M_STATE_FALL = 4,\n    M_STATE_DEATH = 5,\n    M_STATE_ATTACK = 6,\n    M_STATE_EAT = 7,\n} M_STATE;\n\nstatic const BITE m_BirdBite = {\n    .pos = { .x = 15, .y = 46, .z = 21 },\n    .mesh_num = 6,\n};\nstatic const BITE m_CrowBite = {\n    .pos = { .x = 2, .y = 10, .z = 60 },\n    .mesh_num = 14,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n    ITEM *const item = Item_Get(item_num);\n    if (item->object_id == O_CROW) {\n        Item_SwitchToAnim(item, M_CROW_START_ANIM, 0);\n        item->goal_anim_state = M_STATE_EAT;\n        item->current_anim_state = M_STATE_EAT;\n    } else {\n        Item_SwitchToAnim(item, M_START_ANIM, 0);\n        item->goal_anim_state = M_STATE_STOP;\n        item->current_anim_state = M_STATE_STOP;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const bird = item->creature_data;\n\n    if (item->hit_points <= 0) {\n        switch (item->current_anim_state) {\n        case M_STATE_FALL:\n            if (item->pos.y > item->floor) {\n                item->pos.y = item->floor;\n                item->gravity = false;\n                item->fall_speed = 0;\n                item->goal_anim_state = M_STATE_DEATH;\n            }\n            break;\n\n        case M_STATE_DEATH:\n            item->pos.y = item->floor;\n            break;\n\n        default:\n            const int16_t anim_idx =\n                item->object_id == O_CROW ? M_CROW_DIE_ANIM : M_DIE_ANIM;\n            Item_SwitchToAnim(item, anim_idx, 0);\n            item->current_anim_state = M_STATE_FALL;\n            item->gravity = true;\n            item->speed = 0;\n            break;\n        }\n        item->rot.x = 0;\n        Creature_Animate(item_num, 0, 0);\n        return;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    Creature_Mood(item, &info, false);\n\n    const int16_t angle = Creature_Turn(item, M_TURN);\n\n    switch (item->current_anim_state) {\n    case M_STATE_FLY:\n        bird->flags = 0;\n        if (item->required_anim_state != M_STATE_EMPTY) {\n            item->goal_anim_state = item->required_anim_state;\n        }\n        if (bird->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.ahead && info.distance < M_ATTACK_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK;\n        } else {\n            item->goal_anim_state = M_STATE_GLIDE;\n        }\n        break;\n\n    case M_STATE_STOP:\n    case M_STATE_EAT:\n        item->pos.y = item->floor;\n        if (bird->mood != MOOD_BORED) {\n            item->goal_anim_state = M_STATE_FLY;\n        }\n        break;\n\n    case M_STATE_GLIDE:\n        if (bird->mood == MOOD_BORED) {\n            item->required_anim_state = M_STATE_STOP;\n            item->goal_anim_state = M_STATE_FLY;\n        } else if (info.ahead && info.distance < M_ATTACK_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK;\n        }\n        break;\n\n    case M_STATE_ATTACK:\n        if (bird->flags == 0 && item->touch_bits != 0) {\n            Lara_TakeDamage(M_DAMAGE, true);\n            if (item->object_id == O_CROW) {\n                Creature_Effect(item, &m_CrowBite, Spawn_Blood);\n            } else {\n                Creature_Effect(item, &m_BirdBite, Spawn_Blood);\n            }\n            bird->flags = 1;\n        }\n        break;\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic bool M_SetupCommon(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return false;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    return true;\n}\n\nstatic void M_SetupEagle(OBJECT *const obj)\n{\n    if (!M_SetupCommon(obj)) {\n        return;\n    }\n\n    obj->hit_points = M_EAGLE_HITPOINTS;\n}\n\nstatic void M_SetupCrow(OBJECT *const obj)\n{\n    if (!M_SetupCommon(obj)) {\n        return;\n    }\n\n    obj->hit_points = M_CROW_HITPOINTS;\n}\n\nstatic void M_SetupVulture(OBJECT *const obj)\n{\n    if (!M_SetupCommon(obj)) {\n        return;\n    }\n\n    obj->hit_points = M_VULTURE_HITPOINTS;\n}\n\nREGISTER_OBJECT(O_EAGLE, M_SetupEagle)\nREGISTER_OBJECT(O_CROW, M_SetupCrow)\nREGISTER_OBJECT(O_VULTURE, M_SetupVulture)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/bird_guardian.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BIRD_GUARDIAN_HITPOINTS      200\n#define BIRD_GUARDIAN_TOUCH_BITS_L   0b00001100'00000000'00000000 // = 0x0C0000\n#define BIRD_GUARDIAN_TOUCH_BITS_R   0b01100000'00000000'00000000 // = 0x600000\n#define BIRD_GUARDIAN_RADIUS         (WALL_L / 3) // = 341\n#define BIRD_GUARDIAN_WALK_TURN      (DEG_1 * 4) // = 728\n#define BIRD_GUARDIAN_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576\n#define BIRD_GUARDIAN_ATTACK_2_RANGE SQUARE(WALL_L * 2) // = 4194304\n#define BIRD_GUARDIAN_PUNCH_DAMAGE   200\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BIRD_GUARDIAN_STATE_EMPTY   = 0,\n    BIRD_GUARDIAN_STATE_WAIT    = 1,\n    BIRD_GUARDIAN_STATE_WALK    = 2,\n    BIRD_GUARDIAN_STATE_AIM_1   = 3,\n    BIRD_GUARDIAN_STATE_PUNCH_1 = 4,\n    BIRD_GUARDIAN_STATE_AIM_2   = 5,\n    BIRD_GUARDIAN_STATE_PUNCH_2 = 6,\n    BIRD_GUARDIAN_STATE_PUNCH_R = 7,\n    BIRD_GUARDIAN_STATE_WAIT_2  = 8,\n    BIRD_GUARDIAN_STATE_DEATH   = 9,\n    BIRD_GUARDIAN_STATE_AIM_3   = 10,\n    BIRD_GUARDIAN_STATE_PUNCH_3 = 11,\n    // clang-format on\n} BIRD_GUARDIAN_STATE;\n\ntypedef enum {\n    BIRD_GUARDIAN_ANIM_DEATH = 20,\n} BIRD_GUARDIAN_ANIM;\n\nstatic const BITE m_BirdGuardianBiteL = {\n    .pos = { .x = 0, .y = 224, .z = 0, },\n    .mesh_num = 19,\n};\n\nstatic const BITE m_BirdGuardianBiteR = {\n    .pos = { .x = 0, .y = 224, .z = 0, },\n    .mesh_num = 22,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case BIRD_GUARDIAN_STATE_WAIT:\n            creature->maximum_turn = 0;\n            if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_1_RANGE) {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = BIRD_GUARDIAN_STATE_AIM_1;\n                } else {\n                    item->goal_anim_state = BIRD_GUARDIAN_STATE_AIM_3;\n                }\n            } else if (info.ahead && creature->mood == MOOD_BORED) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT_2;\n            } else if (info.ahead && creature->mood == MOOD_STALK) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT_2;\n            } else {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WALK;\n            }\n            break;\n\n        case BIRD_GUARDIAN_STATE_WAIT_2:\n            if (creature->mood != MOOD_BORED) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT;\n            } else if (!info.ahead) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT;\n            }\n            break;\n\n        case BIRD_GUARDIAN_STATE_WALK:\n            creature->maximum_turn = BIRD_GUARDIAN_WALK_TURN;\n            if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_2_RANGE) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_AIM_2;\n            } else if (info.ahead && creature->mood == MOOD_BORED) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT;\n            } else if (info.ahead && creature->mood == MOOD_STALK) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT;\n            }\n            break;\n\n        case BIRD_GUARDIAN_STATE_AIM_1:\n            creature->flags = 0;\n            if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_1_RANGE) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_PUNCH_1;\n            } else {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT;\n            }\n            break;\n\n        case BIRD_GUARDIAN_STATE_AIM_2:\n            creature->flags = 0;\n            if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_2_RANGE) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_PUNCH_2;\n            } else {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WALK;\n            }\n            break;\n\n        case BIRD_GUARDIAN_STATE_AIM_3:\n            creature->flags = 0;\n            if (info.ahead && info.distance < BIRD_GUARDIAN_ATTACK_1_RANGE) {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_PUNCH_3;\n            } else {\n                item->goal_anim_state = BIRD_GUARDIAN_STATE_WAIT;\n            }\n            break;\n\n        case BIRD_GUARDIAN_STATE_PUNCH_1:\n        case BIRD_GUARDIAN_STATE_PUNCH_2:\n        case BIRD_GUARDIAN_STATE_PUNCH_R:\n        case BIRD_GUARDIAN_STATE_PUNCH_3:\n            if ((creature->flags & 1) == 0\n                && (item->touch_bits & BIRD_GUARDIAN_TOUCH_BITS_R) != 0) {\n                Creature_Effect(item, &m_BirdGuardianBiteR, Spawn_Blood);\n                Lara_TakeDamage(BIRD_GUARDIAN_PUNCH_DAMAGE, true);\n                creature->flags |= 1;\n            }\n            if ((creature->flags & 2) == 0\n                && (item->touch_bits & BIRD_GUARDIAN_TOUCH_BITS_L) != 0) {\n                Creature_Effect(item, &m_BirdGuardianBiteL, Spawn_Blood);\n                Lara_TakeDamage(BIRD_GUARDIAN_PUNCH_DAMAGE, true);\n                creature->flags |= 2;\n            }\n            break;\n\n        default:\n            break;\n        }\n    } else if (item->current_anim_state != BIRD_GUARDIAN_STATE_DEATH) {\n        Item_SwitchToAnim(item, BIRD_GUARDIAN_ANIM_DEATH, 0);\n        item->current_anim_state = BIRD_GUARDIAN_STATE_DEATH;\n    }\n\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = BIRD_GUARDIAN_HITPOINTS;\n    obj->radius = BIRD_GUARDIAN_RADIUS;\n    if (g_Config.visuals.fix_texture_issues) {\n        obj->shadow_size = UNIT_SHADOW / 2;\n    }\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 14)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_BIRD_GUARDIAN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/centaur.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define CENTAUR_PART_DAMAGE 100\n#define CENTAUR_REAR_DAMAGE 200\n#define CENTAUR_TOUCH 0x30199\n#define CENTAUR_DIE_ANIM 8\n#define CENTAUR_TURN (DEG_1 * 4) // = 728\n#define CENTAUR_REAR_CHANCE 96\n#define CENTAUR_REAR_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296\n#define CENTAUR_HITPOINTS 120\n#define CENTAUR_RADIUS (WALL_L / 3) // = 341\n#define CENTAUR_SMARTNESS 0x7FFF\n\ntypedef enum {\n    CENTAUR_STATE_EMPTY = 0,\n    CENTAUR_STATE_STOP = 1,\n    CENTAUR_STATE_SHOOT = 2,\n    CENTAUR_STATE_RUN = 3,\n    CENTAUR_STATE_AIM = 4,\n    CENTAUR_STATE_DEATH = 5,\n    CENTAUR_STATE_WARNING = 6,\n} CENTAUR_STATE;\n\nstatic BITE m_CentaurRocket = { .pos = { 11, 415, 41 }, .mesh_num = 13 };\nstatic BITE m_CentaurRear = { .pos = { 50, 30, 0 }, .mesh_num = 5 };\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const centaur = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != CENTAUR_STATE_DEATH) {\n            item->current_anim_state = CENTAUR_STATE_DEATH;\n            Item_SwitchToAnim(item, CENTAUR_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, CENTAUR_TURN);\n\n        switch (item->current_anim_state) {\n        case CENTAUR_STATE_STOP:\n            centaur->neck_rotation = 0;\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.bite && info.distance < CENTAUR_REAR_RANGE) {\n                item->goal_anim_state = CENTAUR_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CENTAUR_STATE_AIM;\n            } else {\n                item->goal_anim_state = CENTAUR_STATE_RUN;\n            }\n            break;\n\n        case CENTAUR_STATE_RUN:\n            if (info.bite && info.distance < CENTAUR_REAR_RANGE) {\n                item->required_anim_state = CENTAUR_STATE_WARNING;\n                item->goal_anim_state = CENTAUR_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = CENTAUR_STATE_AIM;\n                item->goal_anim_state = CENTAUR_STATE_STOP;\n            } else if (Random_GetControl() < CENTAUR_REAR_CHANCE) {\n                item->required_anim_state = CENTAUR_STATE_WARNING;\n                item->goal_anim_state = CENTAUR_STATE_STOP;\n            }\n            break;\n\n        case CENTAUR_STATE_AIM:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CENTAUR_STATE_SHOOT;\n            } else {\n                item->goal_anim_state = CENTAUR_STATE_STOP;\n            }\n            break;\n\n        case CENTAUR_STATE_SHOOT:\n            if (item->required_anim_state == CENTAUR_STATE_EMPTY) {\n                item->required_anim_state = CENTAUR_STATE_AIM;\n                int16_t effect_num = Creature_Effect(\n                    item, &m_CentaurRocket, Spawn_AtlanteanBomb);\n                if (effect_num != NO_EFFECT) {\n                    centaur->neck_rotation = Effect_Get(effect_num)->rot.x;\n                }\n            }\n            break;\n\n        case CENTAUR_STATE_WARNING:\n            if (item->required_anim_state == CENTAUR_STATE_EMPTY\n                && (item->touch_bits & CENTAUR_TOUCH)) {\n                Creature_Effect(item, &m_CentaurRear, Spawn_Blood);\n                Lara_TakeDamage(CENTAUR_REAR_DAMAGE, true);\n                item->required_anim_state = CENTAUR_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n\n    if (item->status == IS_DEACTIVATED) {\n        Sound_Effect(SFX_ATLANTEAN_DEATH, &item->pos, SPM_NORMAL);\n        Item_Explode(item_num, -1, CENTAUR_PART_DAMAGE);\n        Item_Kill(item_num);\n        item->status = IS_DEACTIVATED;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 3;\n    obj->hit_points = CENTAUR_HITPOINTS;\n    obj->pivot_length = 400;\n    obj->radius = CENTAUR_RADIUS;\n    obj->smartness = CENTAUR_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST);\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 10)->rot.x = true;\n    Object_GetBone(obj, 10)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_CENTAUR, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/centaur_statue.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/sound.h>\n\n#define STATUE_EXPLODE_DIST (WALL_L * 7 / 2) // = 3584\n#define CENTAUR_REARING_ANIM 7\n#define CENTAUR_REARING_FRAME 36\n\ntypedef struct {\n    int16_t centaur_item_num;\n} M_PRIV;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    OBJECT *const obj = Object_Get(O_CENTAUR);\n    if (!obj->loaded) {\n        p->centaur_item_num = NO_ITEM;\n        return;\n    }\n\n    const int16_t centaur_item_num = Item_CreateLevelItem();\n    ASSERT(centaur_item_num != NO_ITEM);\n\n    ITEM *const centaur = Item_Get(centaur_item_num);\n    centaur->object_id = O_CENTAUR;\n    centaur->room_num = item->room_num;\n    centaur->pos.x = item->pos.x;\n    centaur->pos.y = item->pos.y;\n    centaur->pos.z = item->pos.z;\n    centaur->flags = IF_INVISIBLE;\n    centaur->shade.value_1 = -1;\n\n    Item_Initialise(centaur_item_num);\n\n    Item_SwitchToAnim(centaur, CENTAUR_REARING_ANIM, CENTAUR_REARING_FRAME);\n    centaur->current_anim_state = Item_GetAnim(centaur)->current_anim_state;\n    centaur->goal_anim_state = centaur->current_anim_state;\n    centaur->rot.y = item->rot.y;\n\n    p->centaur_item_num = centaur_item_num;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->flags & IF_KILLED) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t x = lara_item->pos.x - item->pos.x;\n    const int32_t y = lara_item->pos.y - item->pos.y;\n    const int32_t z = lara_item->pos.z - item->pos.z;\n\n    if (y > -WALL_L && y < WALL_L\n        && SQUARE(x) + SQUARE(z) < SQUARE(STATUE_EXPLODE_DIST)) {\n        Item_Explode(item_num, -1, 0);\n        Item_Kill(item_num);\n        item->status = IS_DEACTIVATED;\n\n        const M_PRIV *const p = item->priv;\n        if (p->centaur_item_num != NO_ITEM) {\n            ITEM *const centaur = Item_Get(p->centaur_item_num);\n            centaur->touch_bits = 0;\n            Item_AddActive(p->centaur_item_num);\n            LOT_EnableBaddieAI(p->centaur_item_num, true);\n            centaur->status = IS_ACTIVE;\n            Sound_Effect(SFX_EXPLOSION_1, &centaur->pos, SPM_NORMAL);\n        } else {\n            Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL);\n        }\n    }\n}\n\nstatic int16_t M_GetCarrierItemNum(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->centaur_item_num;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->carrier_item_num_func = M_GetCarrierItemNum;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->enable_interpolation = false;\n}\n\nREGISTER_OBJECT(O_CENTAUR_STATUE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/civilian.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS         (WALL_L / 10)          // = 102\n#define M_HIT_POINTS     15\n#define M_PUNCH_1_DAMAGE 40\n#define M_PUNCH_3_DAMAGE 50\n#define M_TOUCH_BITS     0b00100100'00000000\n#define M_ALERT_DIST     SQUARE(WALL_L)         // = 1048576\n#define M_ESCAPE_DIST    SQUARE(WALL_L * 3)     // = 9437184\n#define M_RUN_DIST       SQUARE(WALL_L * 2)     // = 4194304\n#define M_WALK_DIST      SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_1  SQUARE(WALL_L / 3)     // = 116281\n#define M_ATTACK_DIST_2  SQUARE(WALL_L * 2 / 3) // = 465124\n#define M_ATTACK_DIST_3  SQUARE(WALL_L)         // = 1048576\n#define M_WALK_TURN      (DEG_1 * 5)            // = 910\n#define M_RUN_TURN       (DEG_1 * 6)            // = 1092\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_PUNCH_3,\n    M_STATE_AIM_3,\n    M_STATE_WAIT,\n    M_STATE_AIM_2,\n    M_STATE_AIM_1,\n    M_STATE_PUNCH_2,\n    M_STATE_PUNCH_1,\n    M_STATE_RUN,\n    M_STATE_DEATH,\n    M_STATE_UP_4,\n    M_STATE_UP_2,\n    M_STATE_UP_3,\n    M_STATE_DOWN_4,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STOP   = 6,\n    M_ANIM_DEATH  = 26,\n    M_ANIM_UP_4   = 27,\n    M_ANIM_UP_2   = 28,\n    M_ANIM_UP_3   = 29,\n    M_ANIM_DOWN_4 = 30,\n    // clang-format on\n} M_ANIM;\n\nstatic const BITE m_Bite = {\n    .pos = { 0, 0, 0 },\n    .mesh_num = 13,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n    item->goal_anim_state = M_STATE_STOP;\n}\n\nstatic bool M_Vault(ITEM *const item, const int16_t angle)\n{\n    const int32_t vault_result =\n        Creature_Vault(Item_GetIndex(item), angle, 2, 260);\n    switch (vault_result) {\n    case -4:\n        Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0);\n        item->current_anim_state = M_STATE_DOWN_4;\n        return true;\n    case 2:\n        Item_SwitchToAnim(item, M_ANIM_UP_2, 0);\n        item->current_anim_state = M_STATE_UP_2;\n        return true;\n    case 3:\n        Item_SwitchToAnim(item, M_ANIM_UP_3, 0);\n        item->current_anim_state = M_STATE_UP_3;\n        return true;\n    case 4:\n        Item_SwitchToAnim(item, M_ANIM_UP_4, 0);\n        item->current_anim_state = M_STATE_UP_4;\n        return true;\n    default:\n        return false;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    Creature_TestBoxDamage(item_num);\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->lot.setup.step = STEP_L;\n        }\n        goto finish;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        creature->enemy = lara_item;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_UpdateMood(item, &info, true);\n    if (creature->enemy == lara_item && info.distance > M_ESCAPE_DIST\n        && ABS(info.enemy_facing) < 0x3000) {\n        creature->mood = MOOD_ESCAPE;\n    }\n    Creature_ApplyMood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if ((item->ai_bits & AI_FOLLOW) == 0\n        && (item->hit_status || lara_info.distance < M_ALERT_DIST\n            || Creature_CanSeeEnemy(item, &lara_info))) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n    case M_STATE_WAIT:\n        if (item->current_anim_state == M_STATE_WAIT\n            && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) {\n            item->goal_anim_state = M_STATE_STOP;\n            break;\n        }\n\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n        head = lara_info.angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            if (item->required_anim_state != M_STATE_NULL) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_AIM_1;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_AIM_2;\n        } else if (info.bite && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() < 256) {\n                item->required_anim_state = M_STATE_WAIT;\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_AIM_3;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            (item->ai_bits & AI_FOLLOW) != 0\n            && (creature->reached_goal || lara_info.distance > M_RUN_DIST)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (info.ahead && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        creature->flags = 0;\n        if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_PUNCH_1;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        creature->flags = 0;\n        if (info.ahead && info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_PUNCH_2;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_3:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        creature->flags = 0;\n        if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_PUNCH_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Lara_TakeDamage(M_PUNCH_1_DAMAGE, true);\n            Creature_Effect(item, &m_Bite, Spawn_Blood);\n            Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n            creature->flags = 1;\n        }\n        break;\n\n    case M_STATE_PUNCH_2:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Lara_TakeDamage(M_PUNCH_1_DAMAGE, true);\n            Creature_Effect(item, &m_Bite, Spawn_Blood);\n            Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n            creature->flags = 1;\n        }\n\n        if (info.ahead && info.distance > M_ATTACK_DIST_2\n            && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        }\n        break;\n\n    case M_STATE_PUNCH_3:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (creature->flags != 2 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Lara_TakeDamage(M_PUNCH_3_DAMAGE, true);\n            Creature_Effect(item, &m_Bite, Spawn_Blood);\n            Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n            creature->flags = 2;\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n\n    if (item->current_anim_state >= M_STATE_DEATH) {\n        creature->maximum_turn = 0;\n        Creature_Animate(item_num, angle, 0);\n    } else if (M_Vault(item, angle)) {\n        creature->maximum_turn = 0;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_CIVILIAN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/claw_mutant.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/claw_mutant_internal.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS        STEP_L\n#define M_HIT_POINTS    130\n#define M_DAMAGE        100\n#define M_TOUCH_BITS    0b00000000'10010000\n#define M_ALERT_DIST    SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_1 SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_2 SQUARE(WALL_L * 2)     // = 4194304\n#define M_ATTACK_DIST_3 SQUARE(WALL_L * 4 / 3) // = 1864135\n#define M_FIRE_DIST     SQUARE(WALL_L * 3)     // = 9437184\n#define M_WALK_TURN     (DEG_1 * 3)            // = 546\n#define M_RUN_TURN      (DEG_1 * 4)            // = 728\n#define M_PLASMA_FRAME  28\n// clang-format on\n\ntypedef enum {\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_RUN_ATTACK,\n    M_STATE_WALK_ATTACK_1,\n    M_STATE_WALK_ATTACK_2,\n    M_STATE_SLASH_LEFT,\n    M_STATE_SLASH_RIGHT,\n    M_STATE_DEATH,\n    M_STATE_CLAW_ATTACK,\n    M_STATE_FIRE_ATTACK,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 20,\n} M_ANIM;\n\ntypedef struct {\n    bool recently_fired;\n} M_PRIV;\n\nstatic const BITE m_ClawLeft = {\n    .pos = { .x = 19, .y = -13, .z = 3 },\n    .mesh_num = 7,\n};\nstatic const BITE m_ClawRight = {\n    .pos = { .x = 19, .y = -13, .z = 3 },\n    .mesh_num = 4,\n};\nstatic const BITE m_PlasmaEmitter = {\n    .pos = { .x = -32, .y = -16, .z = -192 },\n    .mesh_num = 13,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"recently_fired\", &p->recently_fired));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"recently_fired\", p->recently_fired);\n}\n\nstatic void M_TriggerPlasmaCharge(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const ITEM *const lara_item = Lara_GetItem();\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 48;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.b = 255;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 192;\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->friction = 3;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.y = (Random_GetControl() & 0xF) + 16;\n    spark->vel.z = (Random_GetControl() & 0x1F) - 16;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->gravity = (Random_GetControl() & 0x1F) + 16;\n    spark->node_num = 6;\n    spark->max_y_vel = (Random_GetControl() & 7) + 16;\n    spark->effect_num = item_num;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    spark->size.width = (Random_GetControl() & 0x1F) + 64;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 2;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerLight(const ITEM *const item)\n{\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    int32_t scale = 0;\n    if (frame_idx > 16) {\n        const ANIM *const anim = Item_GetAnim(item);\n        const int16_t temp = anim->frame_base - item->frame_num + 44;\n        scale = MIN(temp, 16);\n    } else {\n        scale = frame_idx;\n    }\n\n    if (scale <= 0) {\n        return;\n    }\n\n    const int32_t rnd = Random_GetControl();\n    const RGB_888 color = {\n        .r = (scale * (rnd & 0x3F)) >> 4,\n        .g = (scale * (192 - ((rnd >> 6) & 0x1F))) >> 4,\n        .b = (scale * (255 - ((rnd >> 4) & 0x1F))) >> 4,\n    };\n    XYZ_32 pos = m_PlasmaEmitter.pos;\n    Collide_GetJointAbsPosition(item, &pos, m_PlasmaEmitter.mesh_num);\n    Output_AddDynamicLightRGB(pos, 13, color);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        } else if (Item_TestFrameEqual(item, -1)) {\n            Creature_Die(item_num, true);\n            for (int32_t i = 0; i < 3; i++) {\n                const int32_t dynamic = i == 0 ? -2 : -1;\n                Sparks_TriggerExplosionSparks(item->pos, 3, dynamic, 2, 0);\n            }\n            Sound_Effect(SFX_EXPLOSION_2, &item->pos, SPM_NORMAL);\n            return;\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    ITEM *const lara_item = Lara_GetItem();\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.angle = info.angle;\n        lara_info.distance = info.distance;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    const bool violent = info.zone_num == info.enemy_zone_num;\n    Creature_Mood(item, &info, violent);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if (item->hit_status || lara_info.distance < M_ALERT_DIST\n        || Creature_CanSeeEnemy(item, &lara_info)) {\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        creature->maximum_turn = 0;\n        creature->flags = 0;\n        head = info.angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            item->goal_anim_state = M_STATE_STOP;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n            if (info.angle < 0) {\n                item->goal_anim_state = M_STATE_SLASH_LEFT;\n            } else {\n                item->goal_anim_state = M_STATE_SLASH_RIGHT;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n            item->goal_anim_state = M_STATE_CLAW_ATTACK;\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && ((info.distance > M_FIRE_DIST && !p->recently_fired)\n                || info.zone_num != info.enemy_zone_num)) {\n            item->goal_anim_state = M_STATE_FIRE_ATTACK;\n        } else if (creature->mood == MOOD_BORED) {\n            Random_GetControl();\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (item->required_anim_state) {\n            item->goal_anim_state = item->required_anim_state;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            if (info.angle < 0) {\n                item->goal_anim_state = M_STATE_WALK_ATTACK_1;\n            } else {\n                item->goal_anim_state = M_STATE_WALK_ATTACK_2;\n            }\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && ((info.distance > M_FIRE_DIST && !p->recently_fired)\n                || info.zone_num != info.enemy_zone_num)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n\n        if ((item->ai_bits & AI_GUARD) != 0 || creature->mood == MOOD_BORED\n            || (creature->flags != 0 && info.ahead)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_2) {\n            if (lara_item->speed != 0) {\n                item->goal_anim_state = M_STATE_RUN_ATTACK;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && ((info.distance > M_FIRE_DIST && !p->recently_fired)\n                || info.zone_num != info.enemy_zone_num)) {\n            creature->maximum_turn = M_WALK_TURN;\n            item->goal_anim_state = M_STATE_STOP;\n        }\n\n        creature->flags = 0;\n        break;\n\n    case M_STATE_RUN_ATTACK:\n    case M_STATE_WALK_ATTACK_1:\n    case M_STATE_WALK_ATTACK_2:\n    case M_STATE_SLASH_LEFT:\n    case M_STATE_SLASH_RIGHT:\n    case M_STATE_CLAW_ATTACK:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Lara_TakeDamage(100, true);\n            Creature_Effect(item, &m_ClawLeft, Spawn_Blood);\n            Creature_Effect(item, &m_ClawRight, Spawn_Blood);\n            creature->flags = 1;\n        }\n\n        p->recently_fired = false;\n        break;\n\n    case M_STATE_FIRE_ATTACK:\n        if (ABS(info.angle) < M_WALK_TURN) {\n            item->rot.y += info.angle;\n        } else if (info.angle < 0) {\n            item->rot.y -= M_WALK_TURN;\n        } else {\n            item->rot.y += M_WALK_TURN;\n        }\n\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle >> 1;\n        }\n\n        const int16_t frame_idx = Item_GetRelativeFrame(item);\n        if (frame_idx == 0 && (Random_GetControl() & 3) == 0) {\n            p->recently_fired = true;\n        }\n\n        if (frame_idx < M_PLASMA_FRAME) {\n            M_TriggerPlasmaCharge(item_num);\n        } else if (frame_idx == M_PLASMA_FRAME) {\n            ClawMutant_TriggerPlasmaBall(item, nullptr, item->room_num);\n        }\n        M_TriggerLight(item);\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_x);\n    Creature_Joint(item, 1, torso_y);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 0)->rot.x = true;\n    Object_GetBone(obj, 0)->rot.z = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_CLAW_MUTANT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/claw_mutant_internal.h",
    "content": "#pragma once\n\n#include <trx/game/items.h>\n\nvoid ClawMutant_TriggerPlasmaBall(\n    const ITEM *item, const XYZ_32 *pos, int16_t room_num);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/claw_mutant_plasma_ball.c",
    "content": "#include <trx/debug.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/creatures/claw_mutant_internal.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n\n#define M_DAMAGE 200\n\ntypedef enum {\n    M_TYPE_ATTACHED,\n    M_TYPE_DETACHED,\n} M_TYPE;\n\nstatic const BITE m_Bite = {\n    .pos = { .x = -32, .y = -16, .z = -192 },\n    .mesh_num = 13,\n};\nstatic const uint8_t m_Falloffs[2] = { 13, 7 };\n\nstatic void M_TriggerPlasmaBallFlame(\n    const int16_t effect_num, const M_TYPE type, const XYZ_32 vel)\n{\n    const EFFECT *const effect = Effect_Get(effect_num);\n    const ITEM *const lara_item = Lara_GetItem();\n\n    const int32_t dx = lara_item->pos.x - effect->pos.x;\n    const int32_t dz = lara_item->pos.z - effect->pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 48;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.b = 255;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 192;\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = vel.x + (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = vel.y;\n    spark->vel.z = vel.z + (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 5;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = +16 + (Random_GetControl() & 0xF);\n        }\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->effect_num = effect_num;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    spark->size.width = (Random_GetControl() & 0x1F) + 64;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 2;\n\n    if (type == M_TYPE_ATTACHED) {\n        spark->scalar = 2;\n        spark->vel.x <<= 2;\n        spark->vel.y = (Random_GetControl() & 0x1FF) - 256;\n        spark->vel.z <<= 2;\n        spark->friction = 85;\n        spark->dst_size.width >>= 1;\n        spark->dst_size.height >>= 1;\n    }\n\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const XYZ_32 old_pos = effect->pos;\n    const M_TYPE type = effect->flag1;\n\n    if (effect->speed < 384 && type == M_TYPE_ATTACHED) {\n        effect->speed += (effect->speed >> 3) + 4;\n    }\n\n    if (type == M_TYPE_DETACHED) {\n        effect->fall_speed++;\n        if (effect->speed > 8) {\n            effect->speed -= 2;\n        }\n\n        if (effect->rot.x > -0x3C00) {\n            effect->rot.x -= 0x100;\n        }\n    }\n\n    const int32_t speed =\n        (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n    effect->pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, speed);\n    effect->pos.y += effect->fall_speed\n        - ((effect->speed * Math_Sin(effect->rot.x)) >> W2V_SHIFT);\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if ((time4 & 4) != 0) {\n        XYZ_32 vel = {};\n        if (type == M_TYPE_DETACHED) {\n            vel.y = ABS(old_pos.y - effect->pos.y) << 3;\n        }\n        M_TriggerPlasmaBallFlame(effect_num, type, vel);\n    }\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n    const int32_t ceiling = Room_GetCeiling(sector, effect->pos);\n\n    if (effect->pos.y >= height || effect->pos.y < ceiling) {\n        if (type == M_TYPE_ATTACHED && !Room_Get(room_num)->flags.underwater) {\n            const int32_t rnd = (Random_GetControl() & 3) + 5;\n            for (int32_t i = 0; i < rnd; i++) {\n                ClawMutant_TriggerPlasmaBall(\n                    nullptr, &old_pos, effect->room_num);\n            }\n        }\n\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (type == M_TYPE_ATTACHED && Lara_IsNearItem(&effect->pos, 200)) {\n        const int32_t rnd = (Random_GetControl() & 1) + 3;\n        for (int32_t i = 0; i < rnd; i++) {\n            ClawMutant_TriggerPlasmaBall(\n                nullptr, &effect->pos, effect->room_num);\n        }\n\n        Lara_TakeDamage(M_DAMAGE, true);\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n\n    const int32_t color_base = Random_GetControl();\n    const RGB_888 color = {\n        .r = color_base & 0x3F,\n        .g = 192 - ((color_base >> 6) & 0x1F),\n        .b = 255 - ((color_base >> 4) & 0x1F),\n    };\n    Output_AddDynamicLightRGB(effect->pos, m_Falloffs[type], color);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n}\n\nvoid ClawMutant_TriggerPlasmaBall(\n    const ITEM *const item, const XYZ_32 *const pos, const int16_t room_num)\n{\n    const M_TYPE type = item == nullptr ? M_TYPE_DETACHED : M_TYPE_ATTACHED;\n\n    XYZ_32 spawn_pos;\n    int16_t angles[2];\n    int16_t speed;\n    if (type == M_TYPE_DETACHED) {\n        ASSERT(pos != nullptr);\n        spawn_pos = *pos;\n        angles[0] = Random_GetControl() << 1;\n        angles[1] = DEG_45;\n        speed = (Random_GetControl() & 0xF) + 16;\n    } else {\n        spawn_pos = m_Bite.pos;\n        Collide_GetJointAbsPosition(item, &spawn_pos, m_Bite.mesh_num);\n\n        const ITEM *const lara_item = Lara_GetItem();\n        Math_GetVectorAngles(\n            lara_item->pos.x - spawn_pos.x,\n            lara_item->pos.y - spawn_pos.y - STEP_L,\n            lara_item->pos.z - spawn_pos.z, angles);\n        angles[0] = item->rot.y;\n        speed = (Random_GetControl() & 7) + 8;\n    }\n\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_ITEM) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = spawn_pos;\n    effect->rot.x = angles[1];\n    effect->rot.y = angles[0];\n    effect->object_id = O_CLAW_MUTANT_PLASMA_BALL;\n    effect->speed = speed;\n    effect->fall_speed = 0;\n    effect->flag1 = type;\n}\n\nREGISTER_OBJECT(O_CLAW_MUTANT_PLASMA_BALL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/cobra.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\nstatic BITE m_CobraBite = { .pos = { 0, 0, 0 }, .mesh_num = 13 };\n\ntypedef enum {\n    COBRA_STATE_WAKING_UP = 0,\n    COBRA_STATE_ALERT = 1,\n    COBRA_STATE_BITE = 2,\n    COBRA_STATE_SLEEP = 3,\n    COBRA_STATE_DEATH = 4,\n} M_COBRA_STATE;\n\ntypedef enum {\n    COBRA_ANIM_SLEEP = 2,\n    COBRA_ANIM_DEATH = 4,\n} M_COBRA_ANIM;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Creature_Initialise(item_num);\n    Item_SwitchToAnim(item, COBRA_ANIM_SLEEP, 45);\n    item->current_anim_state = COBRA_STATE_SLEEP;\n    item->goal_anim_state = COBRA_STATE_SLEEP;\n}\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return item->hit_points > 0 && item->status == IS_ACTIVE\n        && item->current_anim_state != COBRA_STATE_SLEEP;\n}\n\nstatic bool M_CanTakeDamage(const ITEM *const item)\n{\n    return item->hit_points > 0;\n}\n\nstatic bool M_CanBeProjectileTarget(const ITEM *const item)\n{\n    return item->hit_points > 0 && item->collidable;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    int32_t forget_radius = SQUARE(3 * WALL_L);\n    int32_t alert_radius = SQUARE(1.5 * WALL_L);\n    int32_t attack_radius = SQUARE(WALL_L);\n\n    // TODO: do not hardcode this\n    if (g_TRVersion == 3 && GF_BadGetLevelNum() >= 9) {\n        forget_radius = SQUARE(2.5 * WALL_L);\n        alert_radius = SQUARE(1.25 * WALL_L);\n        attack_radius = SQUARE(682);\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    if (creature == nullptr) {\n        return;\n    }\n\n    int16_t angle = 0;\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != COBRA_STATE_DEATH) {\n            Item_SwitchToAnim(item, COBRA_ANIM_DEATH, 0);\n            item->current_anim_state = COBRA_ANIM_DEATH;\n        }\n\n        goto finish;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    info.angle += 3072;\n    creature->target.x = lara_item->pos.x;\n    creature->target.z = lara_item->pos.z;\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (ABS(info.angle) < DEG_1 * 10) {\n        item->rot.y += info.angle;\n    } else if (info.angle < 0) {\n        item->rot.y -= DEG_1 * 10;\n    } else {\n        item->rot.y += DEG_1 * 10;\n    }\n\n    switch (item->current_anim_state) {\n    case COBRA_STATE_WAKING_UP:\n        break;\n\n    case COBRA_STATE_ALERT:\n        creature->flags = 0;\n        if (info.distance > forget_radius) {\n            item->goal_anim_state = COBRA_STATE_SLEEP;\n        } else if (\n            lara_item->hit_points > 0\n            && ((info.ahead && info.distance < attack_radius)\n                || item->hit_status || lara_item->speed > 15)) {\n            item->goal_anim_state = COBRA_STATE_BITE;\n        }\n        break;\n\n    case COBRA_STATE_BITE:\n        if (creature->flags != 1 && (item->touch_bits & 0x2000) != 0) {\n            creature->flags = 1;\n            Lara_TakeDamage(80, true);\n            lara->poison_timer = 256;\n            Creature_Effect(item, &m_CobraBite, Spawn_Blood);\n        }\n        break;\n\n    case COBRA_STATE_SLEEP:\n        creature->flags = 0;\n        if (info.distance < alert_radius && lara_item->hit_points > 0) {\n            item->goal_anim_state = COBRA_STATE_WAKING_UP;\n        }\n        break;\n    }\n\nfinish:\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->can_take_damage_func = M_CanTakeDamage;\n    obj->can_be_projectile_target_func = M_CanBeProjectileTarget;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = 8;\n    obj->radius = 102;\n\n    // obj->non_lot = true; // TODO(TR3)\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_COBRA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/compy.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RUN_TURN      (10 * DEG_1)\n#define M_STOP_TURN     (3 * DEG_1)\n\n#define M_UPSET_SPEED   15\n#define M_HIT_RANGE     SQUARE(WALL_L / 3)\n#define M_ATTACK_ANGLE  0x3000\n#define M_JUMP_CHANCE   0x1000\n#define M_ATTACK_CHANCE 0x1F\n#define M_TOUCH_BITS    0x04\n#define M_HIT_FLAG      1\n// clang-format on\n\ntypedef enum {\n    M_STATE_STOP,\n    M_STATE_RUN,\n    M_STATE_JUMP,\n    M_STATE_ATTACK,\n    M_STATE_DEATH,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 6,\n} M_ANIM;\n\ntypedef struct {\n    int32_t scared_timer;\n    bool attack_lara;\n} M_SHARED_PRIV;\n\ntypedef struct {\n    bool attack_lara;\n    int32_t scared_timer;\n    int32_t carcass_item_num;\n    M_SHARED_PRIV *shared;\n} M_PRIV;\n\nstatic M_SHARED_PRIV m_SharedPriv = {};\nstatic BITE m_Bite = {\n    .pos = { .x = 0, .y = 0, .z = 0 },\n    .mesh_num = 2,\n};\n\nstatic bool M_FindCarcass(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    p->carcass_item_num = Item_FindTypeInRoom(item->room_num, O_ANIMATING_6);\n    return p->carcass_item_num != NO_ITEM;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    Creature_Initialise(item_num);\n    p->carcass_item_num = NO_ITEM;\n    p->shared = &m_SharedPriv;\n    p->shared->attack_lara = false;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"scared_timer\", &p->scared_timer));\n    JSON_SHOULD(JSON_READ(io, \"attack_lara\", &p->attack_lara));\n    JSON_SHOULD(JSON_READ(io, \"shared_scared_timer\", &p->shared->scared_timer));\n    JSON_SHOULD(JSON_READ(io, \"shared_attack_lara\", &p->shared->attack_lara));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"scared_timer\", p->scared_timer);\n    JSONW_WRITE(io, \"attack_lara\", p->attack_lara);\n    JSONW_WRITE(io, \"shared_scared_timer\", p->shared->scared_timer);\n    JSONW_WRITE(io, \"shared_attack_lara\", p->shared->attack_lara);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n    int16_t torso = 0;\n    int16_t head = 0;\n\n    if (p->carcass_item_num == NO_ITEM) {\n        M_FindCarcass(item);\n    }\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n        goto finish;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n\n    if (creature->mood == MOOD_BORED && p->carcass_item_num != NO_ITEM) {\n        ITEM *const raptor = Item_Get(p->carcass_item_num);\n        const int32_t dx = raptor->pos.x - item->pos.x;\n        const int32_t dz = raptor->pos.z - item->pos.z;\n        info.distance = SQUARE(dx) + SQUARE(dz);\n        info.angle = Math_Atan(dz, dx) - item->rot.y;\n        info.ahead = info.angle > -DEG_90 && info.angle < DEG_90;\n    }\n\n    const int16_t bits = (item_num & 7) * 0x200 - 0x700;\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (p->shared->scared_timer == 0 && !p->shared->attack_lara\n        && ((info.enemy_facing < M_ATTACK_ANGLE\n             && info.enemy_facing > -M_ATTACK_ANGLE\n             && lara_item->speed > M_UPSET_SPEED)\n            || lara_item->current_anim_state == LS(LS_ROLL)\n            || item->hit_status)) {\n        p->scared_timer = (bits + 0x700) >> 7;\n        p->shared->scared_timer = 280;\n    } else if (p->shared->scared_timer > 0) {\n        if (p->scared_timer > 0) {\n            p->scared_timer--;\n        } else {\n            creature->mood = MOOD_ESCAPE;\n            p->shared->scared_timer--;\n        }\n        if (Random_GetControl() < M_ATTACK_CHANCE && item->timer > 180) {\n            p->shared->attack_lara = true;\n        }\n    } else if (info.zone_num == info.enemy_zone_num) {\n        creature->mood = MOOD_ATTACK;\n    } else {\n        creature->mood = MOOD_BORED;\n    }\n\n    switch (creature->mood) {\n    case MOOD_ATTACK:\n        creature->target =\n            XYZ_32_OffsetYaw(creature->enemy->pos, bits + item->rot.y, WALL_L);\n        break;\n\n    case MOOD_ESCAPE:\n    case MOOD_STALK: {\n        creature->target =\n            XYZ_32_OffsetYaw(item->pos, bits + info.angle + DEG_180, WALL_L);\n        int16_t room_num = item->room_num;\n        SECTOR *const sector = Room_GetSector(creature->target, &room_num);\n        if (ABS(Box_GetBox(sector->box)->height - item->pos.y) > STEP_L) {\n            creature->mood = MOOD_BORED;\n            p->scared_timer = p->shared->scared_timer;\n        }\n        break;\n    }\n\n    case MOOD_BORED:\n        if (p->carcass_item_num != NO_ITEM) {\n            ITEM *const raptor = Item_Get(p->carcass_item_num);\n            creature->target.x = raptor->pos.x;\n            creature->target.z = raptor->pos.z;\n        }\n        break;\n    }\n\n    angle = Creature_Turn(item, creature->maximum_turn);\n    torso = info.ahead ? info.angle : 0;\n    head = -(info.angle / 4);\n    item->timer++;\n\n    if (item->hit_status && item->timer > 200 && Random_GetControl() < 0xC1C) {\n        p->shared->attack_lara = true;\n    }\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        creature->flags &= ~M_HIT_FLAG;\n        creature->maximum_turn = M_STOP_TURN;\n\n        if (creature->mood == MOOD_ATTACK) {\n            if (info.ahead && info.distance < M_HIT_RANGE * 4) {\n                if (!p->shared->attack_lara) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (Random_GetControl() < DEG_90) {\n                    item->goal_anim_state = M_STATE_ATTACK;\n                } else {\n                    item->goal_anim_state = M_STATE_JUMP;\n                }\n            } else if (\n                info.distance\n                > M_HIT_RANGE * (9 - 4 * p->shared->attack_lara)) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (creature->mood == MOOD_BORED) {\n            if (info.ahead && info.distance < M_HIT_RANGE * 3\n                && p->carcass_item_num != NO_ITEM) {\n                if (Random_GetControl() < DEG_90) {\n                    item->goal_anim_state = M_STATE_ATTACK;\n                } else {\n                    item->goal_anim_state = M_STATE_JUMP;\n                }\n            } else if (info.distance > M_HIT_RANGE * 3) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (Random_GetControl() < M_JUMP_CHANCE) {\n            item->goal_anim_state = M_STATE_JUMP;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        creature->flags &= ~M_HIT_FLAG;\n        creature->maximum_turn = M_RUN_TURN;\n        if (info.angle < M_ATTACK_ANGLE && info.angle > -M_ATTACK_ANGLE\n            && info.distance < M_HIT_RANGE * (9 - 4 * p->shared->attack_lara)) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_JUMP:\n    case M_STATE_ATTACK:\n        creature->maximum_turn = M_RUN_TURN;\n        if (!(creature->flags & M_HIT_FLAG)) {\n            if ((item->touch_bits & M_TOUCH_BITS) && p->shared->attack_lara) {\n                creature->flags |= M_HIT_FLAG;\n                Lara_TakeDamage(90, true);\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n            } else if (\n                info.distance < M_HIT_RANGE && info.ahead\n                && p->carcass_item_num != NO_ITEM\n                && creature->mood != MOOD_ATTACK) {\n                creature->flags |= M_HIT_FLAG;\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n            }\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, angle >> 1);\n    Creature_Joint(item, 0, torso);\n    Creature_Joint(item, 1, head);\n    Creature_Animate(item_num, angle, angle >> 1);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->radius = 102;\n    obj->shadow_size = 85;\n    obj->hit_points = 10;\n    obj->pivot_length = 50;\n\n    // obj->non_lot = true; // TODO(TR3)\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 1)->rot.y = true;\n    Object_GetBone(obj, 2)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_COMPY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/cowboy.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/savegame.h>\n#include <trx/game/spawn.h>\n\n#define COWBOY_SHOT_DAMAGE 70\n#define COWBOY_WALK_TURN (DEG_1 * 3) // = 546\n#define COWBOY_RUN_TURN (DEG_1 * 6) // = 1092\n#define COWBOY_WALK_RANGE SQUARE(WALL_L * 3) // = 9437184\n#define COWBOY_DIE_ANIM 7\n#define COWBOY_HITPOINTS 150\n#define COWBOY_RADIUS (WALL_L / 10) // = 102\n#define COWBOY_SMARTNESS 0x7FFF\n\ntypedef enum {\n    COWBOY_STATE_EMPTY = 0,\n    COWBOY_STATE_STOP = 1,\n    COWBOY_STATE_WALK = 2,\n    COWBOY_STATE_RUN = 3,\n    COWBOY_STATE_AIM = 4,\n    COWBOY_STATE_DEATH = 5,\n    COWBOY_STATE_SHOOT = 6,\n} COWBOY_STATE;\n\nstatic const CREATURE_GUN m_CowboyGun1 = {\n    .muzzle = { .pos = { 1, 200, 41 }, .mesh_num = 5 },\n};\nstatic const CREATURE_GUN m_CowboyGun2 = {\n    .muzzle = { .pos = { -2, 200, 40 }, .mesh_num = 8 },\n};\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->hit_points <= 0) {\n            const uint16_t flags =\n                Music_GetTrackFlags(Music_ToGameID(MX_COWBOY_SPEECH));\n            Music_SetTrackFlags(\n                Music_ToGameID(MX_COWBOY_SPEECH), flags | IF_ONE_SHOT);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const cowboy = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != COWBOY_STATE_DEATH) {\n            item->current_anim_state = COWBOY_STATE_DEATH;\n            Item_SwitchToAnim(item, COWBOY_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, cowboy->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case COWBOY_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = COWBOY_STATE_AIM;\n            } else if (cowboy->mood == MOOD_BORED) {\n                item->goal_anim_state = COWBOY_STATE_WALK;\n            } else {\n                item->goal_anim_state = COWBOY_STATE_RUN;\n            }\n            break;\n\n        case COWBOY_STATE_WALK:\n            cowboy->maximum_turn = COWBOY_WALK_TURN;\n            if (cowboy->mood == MOOD_ESCAPE || !info.ahead) {\n                item->required_anim_state = COWBOY_STATE_RUN;\n                item->goal_anim_state = COWBOY_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = COWBOY_STATE_AIM;\n                item->goal_anim_state = COWBOY_STATE_STOP;\n            } else if (info.distance > COWBOY_WALK_RANGE) {\n                item->required_anim_state = COWBOY_STATE_RUN;\n                item->goal_anim_state = COWBOY_STATE_STOP;\n            }\n            break;\n\n        case COWBOY_STATE_RUN:\n            cowboy->maximum_turn = COWBOY_RUN_TURN;\n            tilt = angle / 2;\n            if (cowboy->mood != MOOD_ESCAPE || info.ahead) {\n                if (Creature_CanTargetEnemy(item, &info)) {\n                    item->required_anim_state = COWBOY_STATE_AIM;\n                    item->goal_anim_state = COWBOY_STATE_STOP;\n                } else if (info.ahead && info.distance < COWBOY_WALK_RANGE) {\n                    item->required_anim_state = COWBOY_STATE_WALK;\n                    item->goal_anim_state = COWBOY_STATE_STOP;\n                }\n            }\n            break;\n\n        case COWBOY_STATE_AIM:\n            cowboy->flags = 0;\n            if (item->required_anim_state) {\n                item->goal_anim_state = COWBOY_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = COWBOY_STATE_SHOOT;\n            } else {\n                item->goal_anim_state = COWBOY_STATE_STOP;\n            }\n            break;\n\n        case COWBOY_STATE_SHOOT:\n            if (!cowboy->flags) {\n                Creature_Shoot(\n                    item, &info, &m_CowboyGun1, head, COWBOY_SHOT_DAMAGE);\n            } else if (cowboy->flags == 6) {\n                if (Creature_CanTargetEnemy(item, &info)) {\n                    Creature_Shoot(\n                        item, &info, &m_CowboyGun2, head, COWBOY_SHOT_DAMAGE);\n                } else {\n                    int16_t effect_num = Creature_Effect(\n                        item, &m_CowboyGun2.muzzle, Spawn_GunShot);\n                    if (effect_num != NO_EFFECT) {\n                        Effect_Get(effect_num)->rot.y += head;\n                    }\n                }\n            }\n            cowboy->flags++;\n\n            if (cowboy->mood == MOOD_ESCAPE) {\n                item->required_anim_state = COWBOY_STATE_RUN;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = COWBOY_HITPOINTS;\n    obj->radius = COWBOY_RADIUS;\n    obj->smartness = COWBOY_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_COWBOY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/crawler_mutant.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n\n// clang-format off\n#define M_RADIUS          (WALL_L / 5)       // = 204\n#define M_HIT_POINTS      50\n#define M_MAX_POISON      256\n#define M_MAX_BURN_TIME   80\n#define M_START_BURN_MESH 9\n#define M_ALERT_DIST      SQUARE(WALL_L)     // = 1048576\n#define M_ATTACK_DIST     SQUARE(WALL_L * 2) // = 4194304\n#define M_WALK_TURN       (DEG_1 * 3)        // = 546\n// clang-format on\n\ntypedef enum {\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_BURP,\n    M_STATE_DEATH,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 5,\n} M_ANIM;\n\ntypedef struct {\n    int16_t burn_timer;\n} M_PRIV;\n\nstatic const BITE m_Gas = {\n    .pos = { 0, 48, 140 },\n    .mesh_num = 10,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"burn_timer\", &p->burn_timer));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"burn_timer\", p->burn_timer);\n}\n\nstatic void M_TriggerGas(\n    const XYZ_32 pos, const XYZ_32 vel, const int16_t effect_num)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x3F) + 128;\n    spark->src_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->src_color.b = 32;\n    spark->dst_color.r = (Random_GetControl() & 0xF) + 32;\n    spark->dst_color.g = (Random_GetControl() & 0xF) + 32;\n    spark->dst_color.b = 0;\n\n    if (vel.x != 0 || vel.y != 0 || vel.z != 0) {\n        spark->col_fade_speed = 6;\n        spark->fade_to_black = 2;\n        spark->life = (Random_GetControl() & 1) + 16;\n    } else {\n        spark->col_fade_speed = 8;\n        spark->fade_to_black = 16;\n        spark->life = (Random_GetControl() & 3) + 28;\n    }\n\n    spark->s_life = spark->life;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = vel.x + (Random_GetControl() & 0xF) - 16;\n    spark->vel.y = vel.y;\n    spark->vel.z = vel.z + (Random_GetControl() & 0xF) - 16;\n    spark->friction = 0;\n\n    if ((Random_GetControl() & 1) != 0) {\n        if (effect_num < 0) {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n                | SPARK_F_SCALE;\n        } else {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n                | SPARK_F_SPRITE | SPARK_F_SCALE;\n        }\n\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = +16 + (Random_GetControl() & 0xF);\n        }\n    } else if (effect_num < 0) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->max_y_vel = 0;\n    spark->effect_num = effect_num;\n    spark->gravity = 0;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    const int32_t size = (Random_GetControl() & 0x1F) + 48;\n    if (vel.x != 0 || vel.y != 0 || vel.z != 0) {\n        spark->size.width = size >> 5;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = size >> 1;\n        spark->size.height = spark->size.width;\n        spark->src_size.height = spark->size.height;\n        spark->dst_size.height = spark->dst_size.width;\n\n        if (effect_num == -2) {\n            spark->scalar = 2;\n        } else {\n            spark->scalar = 3;\n        }\n    } else {\n        spark->scalar = 4;\n        spark->size.width = size >> 4;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = size >> 1;\n        spark->size.height = spark->size.width;\n        spark->src_size.height = spark->size.height;\n        spark->dst_size.height = spark->dst_size.width;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerGasThrower(\n    const ITEM *const item, const BITE *const bite, const int16_t speed)\n{\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num == NO_ITEM) {\n        return;\n    }\n\n    XYZ_32 pos_1 = bite->pos;\n    Collide_GetJointAbsPosition(item, &pos_1, bite->mesh_num);\n\n    XYZ_32 pos_2 = {\n        .x = bite->pos.x,\n        .y = bite->pos.y << 1,\n        .z = bite->pos.z << 3,\n    };\n    Collide_GetJointAbsPosition(item, &pos_2, bite->mesh_num);\n\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        pos_2.x - pos_1.x, pos_2.y - pos_1.y, pos_2.z - pos_1.z, angles);\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = pos_1;\n    effect->rot.x = angles[1];\n    effect->rot.y = angles[0];\n    effect->speed = speed << 2;\n    effect->object_id = O_MISSILE_POISON;\n    effect->counter = 20;\n    effect->flag1 = 1;\n\n    M_TriggerGas((XYZ_32) {}, (XYZ_32) {}, effect_num);\n\n    for (int32_t i = 0; i < 2; i++) {\n        const int32_t s = Random_GetControl() % (speed << 2) + 32;\n        const int32_t r = (s * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n        const XYZ_32 vel = {\n            .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT,\n            .y = -((s * Math_Sin(effect->rot.x)) >> W2V_SHIFT),\n            .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT,\n        };\n        M_TriggerGas(\n            effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -1);\n    }\n\n    {\n        const int32_t r = ((speed << 1) * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n        const XYZ_32 vel = {\n            .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT,\n            .y = -(((speed << 1) * Math_Sin(effect->rot.x)) >> W2V_SHIFT),\n            .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT,\n        };\n        M_TriggerGas(\n            effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -2);\n    }\n}\n\nstatic void M_BurnDeath(ITEM *const item)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    for (int32_t mesh = M_START_BURN_MESH; mesh < obj->mesh_count; mesh++) {\n        XYZ_32 pos = {};\n        Collide_GetJointAbsPosition(item, &pos, mesh);\n        Sparks_TriggerFireFlame(pos, -1, 255);\n    }\n\n    int32_t scale = 0;\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    if (frame_idx > 16) {\n        const ANIM *const anim = Item_GetAnim(item);\n        const int16_t remaining_frames = anim->frame_end - item->frame_num;\n        scale = MIN(remaining_frames, 16);\n    } else {\n        scale = frame_idx;\n    }\n\n    const int32_t rnd = Random_GetControl();\n    const RGB_888 color = {\n        .r = (scale * (255 - ((rnd >> 4) & 0x1F))) >> 4,\n        .g = (scale * (192 - ((rnd >> 6) & 0x3F))) >> 4,\n        .b = (scale * (rnd & 0x3F)) >> 4,\n    };\n    Output_AddDynamicLightRGB(item->pos, 12, color);\n}\n\nstatic void M_GasDeath(ITEM *const item)\n{\n    const ANIM *const anim = Item_GetAnim(item);\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    const int16_t end_frame_idx = anim->frame_end - anim->frame_base - 8;\n\n    if (frame_idx >= 1 && frame_idx <= end_frame_idx) {\n        int16_t speed = frame_idx + 1;\n        if (speed > 24) {\n            const int16_t remaining_frames = end_frame_idx - frame_idx;\n            if (remaining_frames <= 0) {\n                speed = 1;\n            } else if (remaining_frames > 24) {\n                speed = (Random_GetControl() & 0xF) + 8;\n            } else {\n                speed = remaining_frames;\n            }\n        }\n\n        CLAMPL(speed, 1);\n        M_TriggerGasThrower(item, &m_Gas, speed);\n    }\n}\n\nstatic void M_CalculateEnemy(ITEM *const item)\n{\n    CREATURE *const mutant = item->creature_data;\n    ITEM *const lara_item = Lara_GetItem();\n    mutant->enemy = lara_item;\n\n    int32_t dx = lara_item->pos.x - item->pos.x;\n    int32_t dz = lara_item->pos.z - item->pos.z;\n    int32_t best_distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == mutant) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        if (candidate->object_id != O_LARA\n            && candidate->object_id != O_RX_WORKER_3) {\n            continue;\n        }\n\n        dx = candidate->pos.x - item->pos.x;\n        dz = candidate->pos.z - item->pos.z;\n        const int32_t distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n        if (distance < best_distance) {\n            mutant->enemy = (ITEM *)candidate;\n            best_distance = distance;\n        }\n    }\n}\n\nstatic void M_ControlCrawler(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    M_PRIV *const p = item->priv;\n    if (p->burn_timer > M_MAX_BURN_TIME) {\n        item->hit_points = 0;\n    }\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->flags = 0;\n        } else if (p->burn_timer > M_MAX_BURN_TIME) {\n            M_BurnDeath(item);\n        } else {\n            M_GasDeath(item);\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        M_CalculateEnemy(item);\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.angle = info.angle;\n        lara_info.distance = info.distance;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    const bool violent = info.zone_num == info.enemy_zone_num;\n    Creature_UpdateMood(item, &info, violent);\n    if (creature->enemy == lara_item && lara->poison_timer >= M_MAX_POISON) {\n        creature->mood = MOOD_ESCAPE;\n    }\n    Creature_ApplyMood(item, &info, violent);\n\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if ((lara_info.distance < M_ALERT_DIST || item->hit_status\n         || Creature_CanSeeEnemy(item, &lara_info))\n        && (item->ai_bits & AI_FOLLOW) == 0) {\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        creature->maximum_turn = 0;\n        creature->flags = 0;\n        head = info.angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            item->goal_anim_state = M_STATE_STOP;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            head = 0;\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && info.distance < M_ATTACK_DIST) {\n            item->goal_anim_state = M_STATE_BURP;\n        } else if (item->required_anim_state != 0) {\n            item->goal_anim_state = item->required_anim_state;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WALK:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            head = 0;\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && info.distance < M_ATTACK_DIST) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_BURP:\n        if (ABS(info.angle) < M_WALK_TURN) {\n            item->rot.y += info.angle;\n        } else if (info.angle < 0) {\n            item->rot.y -= M_WALK_TURN;\n        } else {\n            item->rot.y += M_WALK_TURN;\n        }\n\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle / 2;\n        }\n\n        if (Item_TestFrameRange(item, 35, 58)) {\n            if (creature->flags < 24) {\n                creature->flags += 3;\n            }\n\n            const int16_t speed = creature->flags < 24\n                ? creature->flags\n                : ((Random_GetControl() & 0xF) + 8);\n            M_TriggerGasThrower(item, &m_Gas, speed);\n            if (creature->enemy != nullptr && creature->enemy != lara_item) {\n                creature->enemy->hit_status = true;\n            }\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, 0);\n    Creature_Joint(item, 0, torso_x);\n    Creature_Joint(item, 1, torso_y);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_HandleEvent(\n    ITEM *const item, const OBJECT_EVENT event, const void *const data)\n{\n    if (event != OBJECT_EVENT_BURNT) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    p->burn_timer++;\n}\n\nstatic void M_ControlDying(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_ACTIVE) {\n        return;\n    }\n\n    M_GasDeath(item);\n    Item_Animate(item);\n}\n\nstatic void M_SetupCrawler(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_ControlCrawler;\n    obj->event_func = M_HandleEvent;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 8)->rot.x = true;\n    Object_GetBone(obj, 8)->rot.z = true;\n    Object_GetBone(obj, 9)->rot.y = true;\n}\n\nstatic void M_SetupDying(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_ControlDying;\n\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_CRAWLER_MUTANT, M_SetupCrawler)\nREGISTER_OBJECT(O_DYING_MUTANT, M_SetupDying)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/crocodile.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_SHADOW_SIZE              (UNIT_SHADOW / (g_TRVersion < 3 ? 3 : 2))\n#define M_PIVOT_LENGTH             (g_TRVersion < 3 ? 600 : 200)\n#define M_HITPOINTS                (g_TRVersion < 3 ? 20 : 42)\n\n#define M_CROCODILE_BITE_DAMAGE    100\n#define M_CROCODILE_BITE_RANGE     SQUARE(435) // = 189225\n#define M_CROCODILE_FASTTURN_ANGLE 0x4000\n#define M_CROCODILE_FASTTURN_RANGE SQUARE(WALL_L * 3) // = 9437184\n#define M_CROCODILE_FASTTURN_TURN  (6 * DEG_1) // = 1092\n#define M_CROCODILE_TOUCH          0x3FC\n#define M_CROCODILE_TURN           (3 * DEG_1) // = 546\n#define M_CROCODILE_HITPOINTS      M_HITPOINTS\n#define M_CROCODILE_RADIUS         (WALL_L / 3) // = 341\n#define M_CROCODILE_SMARTNESS      0x2000\n\n#define M_ALLIGATOR_BITE_DAMAGE    100\n#define M_ALLIGATOR_FLOAT_SPEED    (WALL_L / 32) // = 32\n#define M_ALLIGATOR_TURN           (3 * DEG_1) // = 546\n#define M_ALLIGATOR_HITPOINTS      M_HITPOINTS\n#define M_ALLIGATOR_RADIUS         (WALL_L / 3) // = 341\n#define M_ALLIGATOR_SMARTNESS      0x400\n#define M_ALLIGATOR_BITE_FRAME     42\n// clang-format on\n\ntypedef enum {\n    M_CROCODILE_DIE_ANIM = 11,\n} M_CROCODILE_ANIM;\n\ntypedef enum {\n    M_ALLIGATOR_DIE_ANIM = 4,\n} M_ALLIGATOR_ANIM;\n\ntypedef enum {\n    M_CROCODILE_STATE_EMPTY = 0,\n    M_CROCODILE_STATE_STOP = 1,\n    M_CROCODILE_STATE_RUN = 2,\n    M_CROCODILE_STATE_WALK = 3,\n    M_CROCODILE_STATE_FAST_TURN = 4,\n    M_CROCODILE_STATE_ATTACK_1 = 5,\n    M_CROCODILE_STATE_ATTACK_2 = 6,\n    M_CROCODILE_STATE_DEATH = 7,\n} M_CROCODILE_STATE;\n\ntypedef enum {\n    M_ALLIGATOR_STATE_EMPTY = 0,\n    M_ALLIGATOR_STATE_SWIM = 1,\n    M_ALLIGATOR_STATE_ATTACK = 2,\n    M_ALLIGATOR_STATE_DEATH = 3,\n} M_ALLIGATOR_STATE;\n\nstatic BITE m_CrocodileBite = { .pos = { 5, -21, 467 }, .mesh_num = 9 };\n\nstatic const HYBRID_INFO m_CrocodileInfo = {\n    .land.id = O_CROCODILE,\n    .land.active_anim = M_CROCODILE_STATE_EMPTY,\n    .land.death_anim = M_CROCODILE_DIE_ANIM,\n    .land.death_state = M_CROCODILE_STATE_DEATH,\n    .water.id = O_ALLIGATOR,\n    .water.active_anim = M_ALLIGATOR_STATE_EMPTY,\n    .water.death_anim = M_ALLIGATOR_DIE_ANIM,\n    .water.death_state = M_ALLIGATOR_STATE_DEATH,\n};\n\nstatic void M_UpdateCreatureLOT(const ITEM *const item)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    OBJECT *const obj = Object_Get(item->object_id);\n    creature->lot.setup = obj->lot_setup;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (Creature_EnsureHabitat(\n                Item_GetIndex(item), nullptr, &m_CrocodileInfo)) {\n            M_UpdateCreatureLOT(item);\n        }\n    }\n}\n\nstatic void M_ControlCrocodile(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const croc = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_CROCODILE_STATE_DEATH) {\n            item->current_anim_state = M_CROCODILE_STATE_DEATH;\n            Item_SwitchToAnim(item, M_CROCODILE_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        if (item->current_anim_state == M_CROCODILE_STATE_FAST_TURN) {\n            item->rot.y += M_CROCODILE_FASTTURN_TURN;\n        } else {\n            angle = Creature_Turn(item, M_CROCODILE_TURN);\n        }\n\n        switch (item->current_anim_state) {\n        case M_CROCODILE_STATE_STOP:\n            if (info.bite && info.distance < M_CROCODILE_BITE_RANGE) {\n                item->goal_anim_state = M_CROCODILE_STATE_ATTACK_1;\n            } else if (croc->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_CROCODILE_STATE_RUN;\n            } else if (croc->mood == MOOD_ATTACK) {\n                if ((info.angle < -M_CROCODILE_FASTTURN_ANGLE\n                     || info.angle > M_CROCODILE_FASTTURN_ANGLE)\n                    && info.distance > M_CROCODILE_FASTTURN_RANGE) {\n                    item->goal_anim_state = M_CROCODILE_STATE_FAST_TURN;\n                } else {\n                    item->goal_anim_state = M_CROCODILE_STATE_RUN;\n                }\n            } else if (croc->mood == MOOD_STALK) {\n                item->goal_anim_state = M_CROCODILE_STATE_WALK;\n            }\n            break;\n\n        case M_CROCODILE_STATE_WALK:\n            if (info.ahead && (item->touch_bits & M_CROCODILE_TOUCH)) {\n                item->goal_anim_state = M_CROCODILE_STATE_STOP;\n            } else if (croc->mood == MOOD_ATTACK || croc->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_CROCODILE_STATE_RUN;\n            } else if (croc->mood == MOOD_BORED) {\n                item->goal_anim_state = M_CROCODILE_STATE_STOP;\n            }\n            break;\n\n        case M_CROCODILE_STATE_FAST_TURN:\n            if (info.angle > -M_CROCODILE_FASTTURN_ANGLE\n                && info.angle < M_CROCODILE_FASTTURN_ANGLE) {\n                item->goal_anim_state = M_CROCODILE_STATE_WALK;\n            }\n            break;\n\n        case M_CROCODILE_STATE_RUN:\n            if (info.ahead && (item->touch_bits & M_CROCODILE_TOUCH)) {\n                item->goal_anim_state = M_CROCODILE_STATE_STOP;\n            } else if (croc->mood == MOOD_STALK) {\n                item->goal_anim_state = M_CROCODILE_STATE_WALK;\n            } else if (croc->mood == MOOD_BORED) {\n                item->goal_anim_state = M_CROCODILE_STATE_STOP;\n            } else if (\n                croc->mood == MOOD_ATTACK\n                && info.distance > M_CROCODILE_FASTTURN_RANGE\n                && (info.angle < -M_CROCODILE_FASTTURN_ANGLE\n                    || info.angle > M_CROCODILE_FASTTURN_ANGLE)) {\n                item->goal_anim_state = M_CROCODILE_STATE_STOP;\n            }\n            break;\n\n        case M_CROCODILE_STATE_ATTACK_1:\n            if (item->required_anim_state == M_CROCODILE_STATE_EMPTY) {\n                Creature_Effect(item, &m_CrocodileBite, Spawn_Blood);\n                Lara_TakeDamage(M_CROCODILE_BITE_DAMAGE, true);\n                item->required_anim_state = M_CROCODILE_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    if (croc != nullptr) {\n        Creature_Head(item, head);\n    }\n\n    // Test conversion to alligator and set relevant pathfinding values.\n    if (Creature_EnsureHabitat(item_num, nullptr, &m_CrocodileInfo)) {\n        M_UpdateCreatureLOT(item);\n    }\n\n    if (croc != nullptr) {\n        Creature_Animate(item_num, angle, 0);\n    } else {\n        Item_Animate(item);\n    }\n}\n\nstatic void M_ControlAlligator(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const gator = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_ALLIGATOR_STATE_DEATH) {\n            item->current_anim_state = M_ALLIGATOR_STATE_DEATH;\n            Item_SwitchToAnim(item, M_ALLIGATOR_DIE_ANIM, 0);\n            item->hit_points = 0;\n            Carrier_TestItemDrops(item_num);\n        }\n\n        // Test if we should convert to a crocodile. If not, control the death\n        // pose of the alligator in the water.\n        if (g_TRVersion >= 3\n            || !Creature_EnsureHabitat(item_num, nullptr, &m_CrocodileInfo)) {\n            Creature_Float(item_num);\n        }\n        return;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n\n    if (info.ahead) {\n        head = info.angle;\n    }\n\n    Creature_UpdateMood(item, &info, true);\n    if (g_TRVersion >= 3) {\n        const ITEM *const lara_item = Lara_GetItem();\n        if (!Room_Get(lara_item->room_num)->flags.underwater\n            && !Lara_Vehicle_IsMounted()) {\n            gator->mood = MOOD_BORED;\n        }\n    }\n    Creature_ApplyMood(item, &info, true);\n\n    Creature_Turn(item, M_ALLIGATOR_TURN);\n\n    switch (item->current_anim_state) {\n    case M_ALLIGATOR_STATE_SWIM:\n        if (info.bite && item->touch_bits) {\n            item->goal_anim_state = M_ALLIGATOR_STATE_ATTACK;\n            if (g_Config.gameplay.fix_alligator_ai) {\n                item->required_anim_state = M_ALLIGATOR_STATE_SWIM;\n            }\n        }\n        break;\n\n    case M_ALLIGATOR_STATE_ATTACK:\n        if (item->frame_num\n            == (g_Config.gameplay.fix_alligator_ai\n                    ? M_ALLIGATOR_BITE_FRAME\n                    : Item_GetAnim(item)->frame_base)) {\n            item->required_anim_state = M_ALLIGATOR_STATE_EMPTY;\n        }\n\n        if (info.bite && item->touch_bits) {\n            if (item->required_anim_state == M_ALLIGATOR_STATE_EMPTY) {\n                Creature_Effect(item, &m_CrocodileBite, Spawn_Blood);\n                Lara_TakeDamage(M_ALLIGATOR_BITE_DAMAGE, true);\n                item->required_anim_state = M_ALLIGATOR_STATE_SWIM;\n            }\n            if (g_Config.gameplay.fix_alligator_ai) {\n                item->goal_anim_state = M_ALLIGATOR_STATE_SWIM;\n            }\n        } else {\n            item->goal_anim_state = M_ALLIGATOR_STATE_SWIM;\n        }\n        break;\n    }\n\n    Creature_Joint(item, 0, head);\n    if (g_TRVersion < 3) {\n        int32_t wh = 0;\n        if (Creature_EnsureHabitat(item_num, &wh, &m_CrocodileInfo)) {\n            M_UpdateCreatureLOT(item);\n        } else {\n            CLAMPL(item->pos.y, wh + STEP_L);\n        }\n    } else {\n        Creature_Underwater(item, STEP_L);\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->initialise_func = Creature_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = M_SHADOW_SIZE;\n    obj->pivot_length = M_PIVOT_LENGTH;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->handle_save_func = M_HandleSave;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nstatic void M_SetupCrocodile(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->control_func = M_ControlCrocodile;\n    obj->hit_points = M_CROCODILE_HITPOINTS;\n    obj->radius = M_CROCODILE_RADIUS;\n    obj->smartness = M_CROCODILE_SMARTNESS;\n}\n\nstatic void M_SetupAlligator(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->control_func = M_ControlAlligator;\n    obj->hit_points = M_ALLIGATOR_HITPOINTS;\n    obj->radius = M_ALLIGATOR_RADIUS;\n    obj->smartness = M_ALLIGATOR_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n}\n\nREGISTER_OBJECT(O_ALLIGATOR, M_SetupAlligator)\nREGISTER_OBJECT(O_CROCODILE, M_SetupCrocodile)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/cultist_1.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/cultist_common.h>\n#include <trx/game/random.h>\n\n// clang-format off\n#define CULTIST_1_HITPOINTS     25\n#define CULTIST_1_SHOOT_DAMAGE  50\n#define CULTIST_1_WALK_TURN     (DEG_1 * 5) // = 910\n#define CULTIST_1_RUN_TURN      (DEG_1 * 5) // = 910\n#define CULTIST_1_RUN_RANGE     SQUARE(WALL_L * 2) // = 4194304\n#define CULTIST_1_POSE_CHANCE   0x500 // = 1280\n#define CULTIST_1_UNPOSE_CHANCE 0x100 // = 256\n#define CULTIST_1_WALK_CHANCE   (CULTIST_1_POSE_CHANCE + 0x500) // = 2560\n#define CULTIST_1_UNWALK_CHANCE 0x300 // = 768\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    CULTIST_1_STATE_EMPTY   = 0,\n    CULTIST_1_STATE_WALK    = 1,\n    CULTIST_1_STATE_RUN     = 2,\n    CULTIST_1_STATE_STOP    = 3,\n    CULTIST_1_STATE_WAIT_1  = 4,\n    CULTIST_1_STATE_WAIT_2  = 5,\n    CULTIST_1_STATE_AIM_1   = 6,\n    CULTIST_1_STATE_SHOOT_1 = 7,\n    CULTIST_1_STATE_AIM_2   = 8,\n    CULTIST_1_STATE_SHOOT_2 = 9,\n    CULTIST_1_STATE_AIM_3   = 10,\n    CULTIST_1_STATE_SHOOT_3 = 11,\n    CULTIST_1_STATE_DEATH   = 12,\n    // clang-format on\n} CULTIST_1_STATE;\n\ntypedef enum {\n    CULTIST_1_ANIM_DEATH = 20,\n} CULTIST_1_ANIM;\n\nstatic const CREATURE_GUN m_Cultist1Gun = {\n    .muzzle = { .pos = { .x = 3, .y = 331, .z = 56 }, .mesh_num = 10 },\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Random_GetControl() < 0x4000) {\n        item->mesh_bits &= ~0b00110000;\n    }\n    if (item->object_id == O_CULT_1B) {\n        // clang-format off\n        // TODO: clang-format >=20 formats this wrongly\n        item->mesh_bits &= ~0b00011111'10000000'00000000;\n        // clang-format on\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != CULTIST_1_STATE_DEATH) {\n            Item_SwitchToAnim(\n                item, Random_GetControl() / 0x4000 + CULTIST_1_ANIM_DEATH, 0);\n            item->current_anim_state = CULTIST_1_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case CULTIST_1_STATE_STOP:\n            creature->maximum_turn = 0;\n            if (item->required_anim_state != CULTIST_1_STATE_EMPTY) {\n                item->goal_anim_state = item->required_anim_state;\n            }\n            break;\n\n        case CULTIST_1_STATE_WAIT_1:\n            if (creature->mood == MOOD_ESCAPE) {\n                item->required_anim_state = CULTIST_1_STATE_RUN;\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n                item->required_anim_state = Random_GetControl() < 0x4000\n                    ? CULTIST_1_STATE_AIM_1\n                    : CULTIST_1_STATE_AIM_3;\n            } else if (creature->mood == MOOD_BORED && info.ahead) {\n                const int16_t random = Random_GetControl();\n                if (random < CULTIST_1_POSE_CHANCE) {\n                    item->required_anim_state = CULTIST_1_STATE_WAIT_2;\n                    item->goal_anim_state = CULTIST_1_STATE_STOP;\n                } else if (random < CULTIST_1_WALK_CHANCE) {\n                    item->required_anim_state = CULTIST_1_STATE_WALK;\n                    item->goal_anim_state = CULTIST_1_STATE_STOP;\n                }\n            } else if (\n                creature->mood == MOOD_BORED\n                || info.distance < CULTIST_1_RUN_RANGE) {\n                item->required_anim_state = CULTIST_1_STATE_WALK;\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            } else {\n                item->required_anim_state = CULTIST_1_STATE_RUN;\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_1_STATE_WAIT_2:\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n                item->required_anim_state = CULTIST_1_STATE_AIM_1;\n            } else if (\n                creature->mood != MOOD_BORED\n                || Random_GetControl() < CULTIST_1_UNPOSE_CHANCE\n                || !info.ahead) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_1_STATE_WALK:\n            creature->maximum_turn = CULTIST_1_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = CULTIST_1_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n                item->required_anim_state = Random_GetControl() < 0x4000\n                    ? CULTIST_1_STATE_AIM_1\n                    : CULTIST_1_STATE_AIM_3;\n            } else if (info.distance > CULTIST_1_RUN_RANGE || !info.ahead) {\n                item->goal_anim_state = CULTIST_1_STATE_RUN;\n            } else if (\n                creature->mood == MOOD_BORED && info.ahead\n                && Random_GetControl() < CULTIST_1_UNWALK_CHANCE) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_1_STATE_RUN:\n            creature->maximum_turn = CULTIST_1_RUN_TURN;\n            creature->flags = 0;\n            tilt = angle / 4;\n            if (creature->mood == MOOD_ESCAPE) {\n                if (Creature_CanTargetEnemy(item, &info)) {\n                    item->goal_anim_state = CULTIST_1_STATE_SHOOT_2;\n                }\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance < CULTIST_1_RUN_RANGE\n                    || info.zone_num != info.enemy_zone_num) {\n                    item->goal_anim_state = CULTIST_1_STATE_STOP;\n                } else {\n                    item->goal_anim_state = CULTIST_1_STATE_SHOOT_2;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_1_STATE_AIM_1:\n        case CULTIST_1_STATE_AIM_3:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state =\n                    item->current_anim_state == CULTIST_1_STATE_AIM_1\n                    ? CULTIST_1_STATE_SHOOT_1\n                    : CULTIST_1_STATE_SHOOT_3;\n            } else {\n                item->goal_anim_state = CULTIST_1_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_1_STATE_SHOOT_1:\n        case CULTIST_1_STATE_SHOOT_3:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Shoot(\n                    item, &info, &m_Cultist1Gun, head, CULTIST_1_SHOOT_DAMAGE);\n                creature->flags = 1;\n            }\n            break;\n\n        case CULTIST_1_STATE_SHOOT_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (item->required_anim_state == CULTIST_1_STATE_EMPTY) {\n                if (!Creature_Shoot(\n                        item, &info, &m_Cultist1Gun, head,\n                        CULTIST_1_SHOOT_DAMAGE)) {\n                    item->goal_anim_state = CULTIST_1_STATE_RUN;\n                }\n                item->required_anim_state = CULTIST_1_STATE_SHOOT_2;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup1(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = CULTIST_1_HITPOINTS;\n    obj->radius = CULTIST_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 50;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n}\n\nstatic void M_Setup1A(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    const OBJECT *const ref_obj = Object_Get(O_CULT_1);\n    if (ref_obj->loaded) {\n        obj->frame_base = ref_obj->frame_base;\n        obj->anim_idx = ref_obj->anim_idx;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = CULTIST_1_HITPOINTS;\n    obj->radius = CULTIST_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 50;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n}\n\nstatic void M_Setup1B(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    const OBJECT *const ref_obj = Object_Get(O_CULT_1);\n    if (ref_obj->loaded) {\n        obj->frame_base = ref_obj->frame_base;\n        obj->anim_idx = ref_obj->anim_idx;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = CULTIST_1_HITPOINTS;\n    obj->radius = CULTIST_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 50;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_CULT_1, M_Setup1)\nREGISTER_OBJECT(O_CULT_1A, M_Setup1A)\nREGISTER_OBJECT(O_CULT_1B, M_Setup1B)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/cultist_2.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/cultist_common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define CULTIST_2_HITPOINTS   60\n#define CULTIST_2_WALK_TURN   (DEG_1 * 3) // = 546\n#define CULTIST_2_RUN_TURN    (DEG_1 * 6) // = 1092\n#define CULTIST_2_WALK_RANGE  SQUARE(WALL_L * 4) // = 16777216\n#define CULTIST_2_KNIFE_RANGE SQUARE(WALL_L * 6) // = 37748736\n#define CULTIST_2_STOP_RANGE  SQUARE(WALL_L * 5 / 2) // = 6553600\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    CULTIST_2_STATE_EMPTY     = 0,\n    CULTIST_2_STATE_STOP      = 1,\n    CULTIST_2_STATE_WALK      = 2,\n    CULTIST_2_STATE_RUN       = 3,\n    CULTIST_2_STATE_AIM_1_L   = 4,\n    CULTIST_2_STATE_SHOOT_1_L = 5,\n    CULTIST_2_STATE_AIM_1_R   = 6,\n    CULTIST_2_STATE_SHOOT_1_R = 7,\n    CULTIST_2_STATE_AIM_2     = 8,\n    CULTIST_2_STATE_SHOOT_2   = 9,\n    CULTIST_2_STATE_DEATH     = 10,\n    // clang-format on\n} CULTIST_2_STATE;\n\ntypedef enum {\n    CULTIST_2_ANIM_DEATH = 23,\n} CULTIST_2_ANIM;\n\nstatic const BITE m_Cultist2LeftHand = {\n    .pos = { .x = 0, .y = 0, .z = 0 },\n    .mesh_num = 5,\n};\n\nstatic const BITE m_Cultist2RightHand = {\n    .pos = { .x = 0, .y = 0, .z = 0 },\n    .mesh_num = 8,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t tilt = 0;\n    int16_t neck = 0;\n    int16_t head = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != CULTIST_2_STATE_DEATH) {\n            Item_SwitchToAnim(item, CULTIST_2_ANIM_DEATH, 0);\n            item->current_anim_state = CULTIST_2_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case CULTIST_2_STATE_STOP:\n            creature->maximum_turn = 0;\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = CULTIST_2_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_2_STATE_AIM_2;\n            } else if (creature->mood == MOOD_BORED) {\n                if (!info.ahead || info.distance > CULTIST_2_KNIFE_RANGE) {\n                    item->goal_anim_state = CULTIST_2_STATE_WALK;\n                }\n            } else if (info.ahead && info.distance < CULTIST_2_WALK_RANGE) {\n                item->goal_anim_state = CULTIST_2_STATE_WALK;\n            } else {\n                item->goal_anim_state = CULTIST_2_STATE_RUN;\n            }\n            break;\n\n        case CULTIST_2_STATE_WALK:\n            creature->maximum_turn = CULTIST_2_WALK_TURN;\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = CULTIST_2_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance < CULTIST_2_STOP_RANGE\n                    || info.zone_num != info.enemy_zone_num) {\n                    item->goal_anim_state = CULTIST_2_STATE_STOP;\n                } else if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = CULTIST_2_STATE_AIM_1_L;\n                } else {\n                    item->goal_anim_state = CULTIST_2_STATE_AIM_1_R;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (info.ahead && info.distance < CULTIST_2_KNIFE_RANGE) {\n                    item->goal_anim_state = CULTIST_2_STATE_STOP;\n                }\n            } else if (!info.ahead || info.distance > CULTIST_2_WALK_RANGE) {\n                item->goal_anim_state = CULTIST_2_STATE_RUN;\n            }\n            break;\n\n        case CULTIST_2_STATE_RUN:\n            creature->maximum_turn = CULTIST_2_RUN_TURN;\n            tilt = angle / 4;\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (creature->mood == MOOD_ESCAPE) {\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_2_STATE_WALK;\n            } else if (creature->mood == MOOD_BORED) {\n                if (info.ahead && info.distance < CULTIST_2_KNIFE_RANGE) {\n                    item->goal_anim_state = CULTIST_2_STATE_STOP;\n                } else {\n                    item->goal_anim_state = CULTIST_2_STATE_WALK;\n                }\n            } else if (info.ahead && info.distance < CULTIST_2_WALK_RANGE) {\n                item->goal_anim_state = CULTIST_2_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_2_STATE_AIM_1_L:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_2_STATE_SHOOT_1_L;\n            } else {\n                item->goal_anim_state = CULTIST_2_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_2_STATE_AIM_1_R:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_2_STATE_SHOOT_1_R;\n            } else {\n                item->goal_anim_state = CULTIST_2_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_2_STATE_AIM_2:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_2_STATE_SHOOT_2;\n            } else {\n                item->goal_anim_state = CULTIST_2_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_2_STATE_SHOOT_1_L:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Effect(item, &m_Cultist2LeftHand, Spawn_Knife);\n                creature->flags = 1;\n            }\n            break;\n\n        case CULTIST_2_STATE_SHOOT_1_R:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Effect(item, &m_Cultist2RightHand, Spawn_Knife);\n                creature->flags = 1;\n            }\n            break;\n\n        case CULTIST_2_STATE_SHOOT_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Effect(item, &m_Cultist2LeftHand, Spawn_Knife);\n                Creature_Effect(item, &m_Cultist2RightHand, Spawn_Knife);\n                creature->flags = CULTIST_2_STATE_STOP;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Neck(item, neck);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = CULTIST_2_HITPOINTS;\n    obj->radius = CULTIST_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 50;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 8)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_CULT_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/cultist_3.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/cultist_common.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define CULTIST_3_HITPOINTS   150\n#define CULTIST_3_SHOT_DAMAGE 50\n#define CULTIST_3_WALK_TURN   (DEG_1 * 3) // = 546\n#define CULTIST_3_RUN_TURN    (DEG_1 * 3) // = 546\n#define CULTIST_3_STOP_RANGE  SQUARE(WALL_L * 3) // = 9437184\n#define CULTIST_3_RUN_RANGE   SQUARE(WALL_L * 5) // = 26214400\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    CULTIST_3_STATE_EMPTY   = 0,\n    CULTIST_3_STATE_STOP    = 1,\n    CULTIST_3_STATE_WAIT    = 2,\n    CULTIST_3_STATE_WALK    = 3,\n    CULTIST_3_STATE_RUN     = 4,\n    CULTIST_3_STATE_AIM_L   = 5,\n    CULTIST_3_STATE_AIM_R   = 6,\n    CULTIST_3_STATE_SHOOT_L = 7,\n    CULTIST_3_STATE_SHOOT_R = 8,\n    CULTIST_3_STATE_AIM_2   = 9,\n    CULTIST_3_STATE_SHOOT_2 = 10,\n    CULTIST_3_STATE_DEATH   = 11,\n    // clang-format on\n} CULTIST_3_STATE;\n\ntypedef enum {\n    // clang-format off\n    CULTIST_3_ANIM_WAIT  = 3,\n    CULTIST_3_ANIM_DEATH = 32,\n    // clang-format on\n} CULTIST_3_ANIM;\n\nstatic const CREATURE_GUN m_Cultist3LeftGun = {\n    .muzzle = { .pos = { .x = -2, .y = 275, .z = 23 }, .mesh_num = 6 },\n};\n\nstatic const CREATURE_GUN m_Cultist3RightGun = {\n    .muzzle = { .pos = { .x = 2, .y = 275, .z = 23 }, .mesh_num = 10 },\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, CULTIST_3_ANIM_WAIT, 0);\n    item->goal_anim_state = CULTIST_3_STATE_WAIT;\n    item->current_anim_state = CULTIST_3_STATE_WAIT;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t body = 0;\n    int16_t left = 0;\n    int16_t right = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != CULTIST_3_STATE_DEATH) {\n            Item_SwitchToAnim(item, CULTIST_3_ANIM_DEATH, 0);\n            item->current_anim_state = CULTIST_3_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        const ITEM *const lara_item = Lara_GetItem();\n        switch (item->current_anim_state) {\n        case CULTIST_3_STATE_STOP:\n        case CULTIST_3_STATE_WAIT:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->mood == MOOD_BORED && lara_item->hit_points <= 0) {\n                item->goal_anim_state = CULTIST_3_STATE_WAIT;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance > CULTIST_3_STOP_RANGE) {\n                    item->goal_anim_state = CULTIST_3_STATE_WALK;\n                } else {\n                    item->goal_anim_state = CULTIST_3_STATE_AIM_2;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = CULTIST_3_STATE_RUN;\n            } else if (creature->mood == MOOD_ATTACK) {\n                if (info.distance > CULTIST_3_RUN_RANGE || !info.ahead) {\n                    item->goal_anim_state = CULTIST_3_STATE_RUN;\n                } else {\n                    item->goal_anim_state = CULTIST_3_STATE_WALK;\n                }\n            } else if (creature->mood == MOOD_STALK || !info.ahead) {\n                item->goal_anim_state = CULTIST_3_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_3_STATE_WALK:\n            creature->maximum_turn = CULTIST_3_WALK_TURN;\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance < CULTIST_3_STOP_RANGE\n                    || info.zone_num != info.enemy_zone_num) {\n                    item->goal_anim_state = CULTIST_3_STATE_STOP;\n                } else if (info.angle < 0) {\n                    item->goal_anim_state = CULTIST_3_STATE_AIM_L;\n                } else {\n                    item->goal_anim_state = CULTIST_3_STATE_AIM_R;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = CULTIST_3_STATE_RUN;\n            } else if (\n                creature->mood == MOOD_STALK || creature->mood == MOOD_ATTACK) {\n                if (info.distance > CULTIST_3_RUN_RANGE || !info.ahead) {\n                    item->goal_anim_state = CULTIST_3_STATE_RUN;\n                }\n            } else if (lara_item->hit_points <= 0) {\n                item->goal_anim_state = CULTIST_3_STATE_WAIT;\n            } else if (info.ahead) {\n                item->goal_anim_state = CULTIST_3_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_3_STATE_RUN:\n            creature->maximum_turn = CULTIST_3_RUN_TURN;\n            tilt = angle / 4;\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.zone_num != info.enemy_zone_num) {\n                    item->goal_anim_state = CULTIST_3_STATE_STOP;\n                } else if (info.angle < 0) {\n                    item->goal_anim_state = CULTIST_3_STATE_AIM_L;\n                } else {\n                    item->goal_anim_state = CULTIST_3_STATE_AIM_R;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (lara_item->hit_points <= 0) {\n                    item->goal_anim_state = CULTIST_3_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = CULTIST_3_STATE_STOP;\n                }\n            } else if (info.ahead && info.distance < CULTIST_3_RUN_RANGE) {\n                item->goal_anim_state = CULTIST_3_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_3_STATE_AIM_L:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n                left = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_3_STATE_SHOOT_L;\n            } else {\n                item->goal_anim_state = CULTIST_3_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_3_STATE_AIM_R:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n                right = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_3_STATE_SHOOT_R;\n            } else {\n                item->goal_anim_state = CULTIST_3_STATE_WALK;\n            }\n            break;\n\n        case CULTIST_3_STATE_AIM_2:\n            creature->flags = 0;\n            if (info.ahead) {\n                body = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = CULTIST_3_STATE_SHOOT_2;\n            } else {\n                item->goal_anim_state = CULTIST_3_STATE_STOP;\n            }\n            break;\n\n        case CULTIST_3_STATE_SHOOT_L:\n            if (info.ahead) {\n                head = info.angle;\n                left = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Shoot(\n                    item, &info, &m_Cultist3LeftGun, head,\n                    CULTIST_3_SHOT_DAMAGE);\n                creature->flags = 1;\n            }\n            break;\n\n        case CULTIST_3_STATE_SHOOT_R:\n            if (info.ahead) {\n                head = info.angle;\n                right = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Shoot(\n                    item, &info, &m_Cultist3RightGun, head,\n                    CULTIST_3_SHOT_DAMAGE);\n                creature->flags = 1;\n            }\n            break;\n\n        case CULTIST_3_STATE_SHOOT_2:\n            if (info.ahead) {\n                body = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Shoot(\n                    item, &info, &m_Cultist3LeftGun, 0, CULTIST_3_SHOT_DAMAGE);\n                Creature_Shoot(\n                    item, &info, &m_Cultist3RightGun, 0, CULTIST_3_SHOT_DAMAGE);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    Object_GetBone(obj, 0)->rot.y = body != 0;\n    Object_GetBone(obj, 2)->rot.y = left != 0;\n    Object_GetBone(obj, 6)->rot.y = right != 0;\n    Object_GetBone(obj, 10)->rot.y = head != 0;\n\n    if (body != 0) {\n        Creature_Head(item, body);\n    } else if (left != 0) {\n        Creature_Head(item, left);\n        Creature_Neck(item, head);\n    } else if (right != 0) {\n        Creature_Head(item, right);\n        Creature_Neck(item, head);\n    } else if (head != 0) {\n        Creature_Head(item, head);\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = CULTIST_3_HITPOINTS;\n    obj->radius = CULTIST_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_CULT_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/cultist_common.h",
    "content": "#pragma once\n\n#include <trx/game/const.h>\n\n#define CULTIST_RADIUS (WALL_L / 10) // = 102\n"
  },
  {
    "path": "src/trx/game/objects/creatures/diver.c",
    "content": "#include <trx/game/collision/los.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_SWIM_TURN (3 * DEG_1)  // = 546\n#define M_FRONT_ARC DEG_45\n#define M_HITPOINTS 20\n#define M_RADIUS    (WALL_L / 3) // = 341\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_SWIM_1,\n    M_STATE_SWIM_2,\n    M_STATE_SHOOT_1,\n    M_STATE_AIM_1,\n    M_STATE_NULL_1,\n    M_STATE_AIM_2,\n    M_STATE_SHOOT_2,\n    M_STATE_NULL_2,\n    M_STATE_DEATH,\n    // clang-format on\n} DIVER_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 16,\n} M_ANIM;\n\nstatic const BITE m_DiverBite = {\n    .pos = { .x = 17, .y = 164, .z = 44 },\n    .mesh_num = 18,\n};\n\nstatic int32_t M_GetWaterSurface(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t room_num)\n{\n    const ROOM *room = Room_Get(room_num);\n    const SECTOR *sector = Room_GetWorldSector(room, x, z);\n\n    if (room->flags.underwater) {\n        while (sector->portal_room.sky != NO_ROOM) {\n            room = Room_Get(sector->portal_room.sky);\n            if (!room->flags.underwater) {\n                return sector->ceiling.height;\n            }\n            sector = Room_GetWorldSector(room, x, z);\n        }\n    } else {\n        while (sector->portal_room.pit != NO_ROOM) {\n            room = Room_Get(sector->portal_room.pit);\n            if (room->flags.underwater) {\n                return sector->floor.height;\n            }\n            sector = Room_GetWorldSector(room, x, z);\n        }\n    }\n    return NO_HEIGHT;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            Carrier_TestItemDrops(item_num);\n        }\n        Creature_Float(item_num);\n        return;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    Creature_Mood(item, &info, false);\n\n    bool shoot;\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (lara->water_status == LWS_ABOVE_WATER) {\n        const GAME_VECTOR start = {\n            .x = item->pos.x,\n            .y = item->pos.y - STEP_L,\n            .z = item->pos.z,\n            .room_num = item->room_num,\n        };\n        GAME_VECTOR target = {\n            .x = lara_item->pos.x,\n            .y = lara_item->pos.y - (LARA_HEIGHT - 150),\n            .z = lara_item->pos.z,\n            .room_num = lara_item->room_num,\n        };\n        shoot = LOS_Check(&start, &target, true);\n\n        if (shoot) {\n            creature->target = lara_item->pos;\n        }\n\n        if (info.angle < -M_FRONT_ARC || info.angle > M_FRONT_ARC) {\n            shoot = false;\n        }\n    } else if (info.angle > -M_FRONT_ARC && info.angle < M_FRONT_ARC) {\n        const GAME_VECTOR start = {\n            .x = item->pos.x,\n            .y = item->pos.y,\n            .z = item->pos.z,\n            .room_num = item->room_num,\n        };\n        GAME_VECTOR target = {\n            .x = lara_item->pos.x,\n            .y = lara_item->pos.y,\n            .z = lara_item->pos.z,\n            .room_num = lara_item->room_num,\n        };\n        shoot = LOS_Check(&start, &target, true);\n    } else {\n        shoot = false;\n    }\n\n    int16_t head = 0;\n    int16_t neck = 0;\n    int16_t angle = Creature_Turn(item, creature->maximum_turn);\n    int32_t water_level =\n        M_GetWaterSurface(item->pos.x, item->pos.y, item->pos.z, item->room_num)\n        + STEP_L * 2;\n\n    switch (item->current_anim_state) {\n    case M_STATE_SWIM_1:\n        creature->maximum_turn = M_SWIM_TURN;\n        if (shoot) {\n            neck = -info.angle;\n        }\n        if (creature->target.y < water_level\n            && item->pos.y < water_level + creature->lot.setup.fly) {\n            item->goal_anim_state = M_STATE_SWIM_2;\n        } else if (creature->mood != MOOD_ESCAPE && shoot) {\n            item->goal_anim_state = M_STATE_AIM_1;\n        }\n        break;\n\n    case M_STATE_SWIM_2:\n        creature->maximum_turn = M_SWIM_TURN;\n        if (shoot) {\n            head = info.angle;\n        }\n        if (creature->target.y > water_level) {\n            item->goal_anim_state = M_STATE_SWIM_1;\n        } else if (creature->mood != MOOD_ESCAPE && shoot) {\n            item->goal_anim_state = M_STATE_AIM_2;\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n        if (shoot) {\n            neck = -info.angle;\n        }\n        if (creature->flags == 0) {\n            Creature_Effect(item, &m_DiverBite, Spawn_Harpoon);\n            creature->flags = 1;\n        }\n        break;\n\n    case M_STATE_SHOOT_2:\n        if (shoot) {\n            head = info.angle;\n        }\n        if (creature->flags == 0) {\n            Creature_Effect(item, &m_DiverBite, Spawn_Harpoon);\n            creature->flags = 1;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        creature->flags = 0;\n        if (shoot) {\n            neck = -info.angle;\n        }\n        if (!shoot || creature->mood == MOOD_ESCAPE\n            || (creature->target.y < water_level\n                && item->pos.y < water_level + creature->lot.setup.fly)) {\n            item->goal_anim_state = M_STATE_SWIM_1;\n        } else {\n            item->goal_anim_state = M_STATE_SHOOT_1;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        creature->flags = 0;\n        if (shoot) {\n            head = info.angle;\n        }\n        if (!shoot || creature->mood == MOOD_ESCAPE\n            || creature->target.y > water_level) {\n            item->goal_anim_state = M_STATE_SWIM_2;\n        } else {\n            item->goal_anim_state = M_STATE_SHOOT_2;\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n\n    Creature_Animate(item_num, angle, 0);\n\n    switch (item->current_anim_state) {\n    case M_STATE_SWIM_1:\n    case M_STATE_AIM_1:\n    case M_STATE_SHOOT_1:\n        Creature_Underwater(item, WALL_L / 2);\n        break;\n\n    default:\n        item->pos.y = water_level - WALL_L / 2;\n        break;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 50;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    if (g_TRVersion < 3) {\n        Object_GetBone(obj, 10)->rot.y = true;\n        Object_GetBone(obj, 14)->rot.z = true;\n    }\n}\n\nREGISTER_OBJECT(O_DIVER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/dog.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_HITPOINTS       10\n#define M_TOUCH_BITS      0b00000001'00111111'01110000'00000000\n#define M_RADIUS          (WALL_L / 3) // = 341\n#define M_WALK_TURN       (3 * DEG_1) // = 546\n#define M_RUN_TURN        (6 * DEG_1) // = 1092\n#define M_ATTACK_1_RANGE  SQUARE(WALL_L / 3) // = 116281\n#define M_ATTACK_2_RANGE  SQUARE(WALL_L * 3 / 4) // = 589824\n#define M_ATTACK_3_RANGE  SQUARE(WALL_L * 2 / 3) // = 465124\n#define M_LEAP_DAMAGE     200\n#define M_BITE_DAMAGE     100\n#define M_LUNGE_DAMAGE    100\n#define M_BARK_CHANCE     0x300\n#define M_CROUCH_CHANCE   (M_BARK_CHANCE + 0x300) // = 0x600\n#define M_STAND_CHANCE    (M_CROUCH_CHANCE + 0x500) // = 0xB00\n#define M_WALK_CHANCE     (M_STAND_CHANCE + 0x2000) // = 0x2600\n#define M_UNCROUCH_CHANCE 0x100\n#define M_UNSTAND_CHANCE  0x200\n#define M_UNBARK_CHANCE   0x500\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_STOP,\n    M_STATE_BARK,\n    M_STATE_CROUCH,\n    M_STATE_STAND,\n    M_STATE_ATTACK_1,\n    M_STATE_ATTACK_2,\n    M_STATE_ATTACK_3,\n    M_STATE_DEATH,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 13,\n} M_ANIM;\n\nstatic const BITE m_DogBite = {\n    .pos = { .x = 0, .y = 30, .z = 141 },\n    .mesh_num = 20,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case M_STATE_WALK:\n            creature->maximum_turn = M_WALK_TURN;\n            if (creature->mood == MOOD_BORED) {\n                const int16_t random = Random_GetControl();\n                if (random < M_BARK_CHANCE) {\n                    item->required_anim_state = M_STATE_BARK;\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (random < M_CROUCH_CHANCE) {\n                    item->required_anim_state = M_STATE_CROUCH;\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (random < M_STAND_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_RUN:\n            tilt = angle;\n            creature->maximum_turn = M_RUN_TURN;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.distance < M_ATTACK_2_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_2;\n            }\n            break;\n\n        case M_STATE_STOP:\n            creature->maximum_turn = 0;\n            creature->flags = 0;\n            if (creature->mood != MOOD_BORED) {\n                if (creature->mood == MOOD_ESCAPE) {\n                    item->goal_anim_state = M_STATE_RUN;\n                } else if (info.distance < M_ATTACK_1_RANGE && info.ahead) {\n                    item->goal_anim_state = M_STATE_ATTACK_1;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (item->required_anim_state != M_STATE_EMPTY) {\n                item->goal_anim_state = item->required_anim_state;\n            } else {\n                const int16_t random = Random_GetControl();\n                if (random < M_BARK_CHANCE) {\n                    item->goal_anim_state = M_STATE_BARK;\n                } else if (random < M_CROUCH_CHANCE) {\n                    item->goal_anim_state = M_STATE_CROUCH;\n                } else if (random < M_WALK_CHANCE) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            }\n            break;\n\n        case M_STATE_BARK:\n            if (creature->mood || Random_GetControl() < M_UNBARK_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_CROUCH:\n            if (creature->mood || Random_GetControl() < M_UNCROUCH_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_STAND:\n            if (creature->mood || Random_GetControl() < M_UNSTAND_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_ATTACK_1:\n            creature->maximum_turn = 0;\n            if (creature->flags != 1 && info.ahead\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Creature_Effect(item, &m_DogBite, Spawn_Blood);\n                Lara_TakeDamage(M_BITE_DAMAGE, true);\n                creature->flags = 1;\n            }\n            if (info.distance > M_ATTACK_1_RANGE\n                && info.distance < M_ATTACK_3_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_3;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_ATTACK_2:\n            if (creature->flags != 2\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Creature_Effect(item, &m_DogBite, Spawn_Blood);\n                Lara_TakeDamage(M_LUNGE_DAMAGE, true);\n                creature->flags = 2;\n            }\n            if (info.distance < M_ATTACK_1_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_1;\n            } else if (info.distance < M_ATTACK_3_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_3;\n            }\n            break;\n\n        case M_STATE_ATTACK_3:\n            creature->maximum_turn = M_RUN_TURN;\n            if (creature->flags != 3\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Creature_Effect(item, &m_DogBite, Spawn_Blood);\n                Lara_TakeDamage(M_LUNGE_DAMAGE, true);\n                creature->flags = 3;\n            }\n            if (info.distance < M_ATTACK_1_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    } else if (item->current_anim_state != M_STATE_DEATH) {\n        Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n        item->current_anim_state = M_STATE_DEATH;\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, tilt);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 300;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 19)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_DOG, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/dragon.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/creature.h>\n#include <trx/game/input.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n\n// clang-format off\n#define M_CLOSE_SHIFT    900\n#define M_FAR_SHIFT      2300\n#define M_MID_SHIFT      ((M_CLOSE_SHIFT + M_FAR_SHIFT) / 2) // = 1600\n#define M_COL_L          (-WALL_L / 2) // = -512\n#define M_COL_R          (+WALL_L / 2) // = +512\n#define M_CLOSE_RANGE    SQUARE(WALL_L * 3) // = 9437184\n#define M_STOP_RANGE     SQUARE(WALL_L * 6) // = 37748736\n#define M_WALK_TURN      (DEG_1 * 2) // = 364\n#define M_NEED_TURN      (DEG_1) // = 182\n#define M_TOUCH_L        0x7F000000\n#define M_TOUCH_R        0x000000FE\n#define M_ALMOST_LIVE    100\n#define M_LIVE_TIME      330\n#define M_ONE_PHASE_TIME 178\n#define M_LIGHT_TIME     (-20)\n#define M_BONE_TIME      (-100)\n#define M_DISSOLVE_TIME  (-240)\n#define M_DISSOLVE_SHIFT 10\n#define M_HITPOINTS      300\n#define M_TOUCH_DAMAGE   10\n#define M_SWIPE_DAMAGE   250\n#define M_RADIUS         (WALL_L / 3) // = 341\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_WALK,\n    M_STATE_LEFT,\n    M_STATE_RIGHT,\n    M_STATE_AIM,\n    M_STATE_FIRE,\n    M_STATE_STOP,\n    M_STATE_TURN_LEFT,\n    M_STATE_TURN_RIGHT,\n    M_STATE_SWIPE_LEFT,\n    M_STATE_SWIPE_RIGHT,\n    M_STATE_DEATH,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_DIE       = 21,\n    M_ANIM_DEAD      = 22,\n    M_ANIM_RESURRECT = 23,\n    // clang-format on\n} M_ANIM;\n\ntypedef enum {\n    M_MODE_ONE_PHASE,\n    M_MODE_TWO_PHASE,\n} M_MODE;\n\ntypedef struct {\n    int16_t dragon_front_item_num;\n    M_MODE mode;\n} M_PRIV;\n\nstatic const BITE m_DragonMouth = {\n    .pos = { .x = 35, .y = 171, .z = 1168 },\n    .mesh_num = 12,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"mode\", &p->mode));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"mode\", p->mode);\n}\n\nstatic int16_t M_GetFrontItemNum(const ITEM *const dragon_back_item)\n{\n    const M_PRIV *const p = dragon_back_item->priv;\n    if (p == nullptr) {\n        return NO_ITEM;\n    }\n    return p->dragon_front_item_num;\n}\n\nstatic bool M_IsTwoPhaseMode(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr && p->mode == M_MODE_TWO_PHASE;\n}\n\nstatic bool M_CanDropItemsBack(const ITEM *const item)\n{\n    return item->hit_points <= 0 && item->status == IS_DEACTIVATED;\n}\n\nstatic void M_InitialiseFront(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->include_in_kill_stats = false;\n}\n\nstatic void M_InitialiseBack(const int16_t item_num)\n{\n    ITEM *const dragon_back_item = Item_Get(item_num);\n    M_PRIV *const p = dragon_back_item->priv;\n    p->mode = M_MODE_TWO_PHASE;\n\n    dragon_back_item->status = IS_INVISIBLE;\n    dragon_back_item->shade.value_1 = -1;\n    dragon_back_item->mesh_bits = 0x1FFFFF;\n\n    p->dragon_front_item_num = Item_CreateLevelItem();\n    ASSERT(p->dragon_front_item_num != NO_ITEM);\n\n    ITEM *const dragon_front_item = Item_Get(p->dragon_front_item_num);\n    dragon_front_item->object_id = O_DRAGON_FRONT;\n    dragon_front_item->pos = dragon_back_item->pos;\n    dragon_front_item->rot.y = dragon_back_item->rot.y;\n    dragon_front_item->room_num = dragon_back_item->room_num;\n    dragon_front_item->flags = IF_INVISIBLE;\n    dragon_front_item->shade.value_1 = -1;\n    Item_Initialise(p->dragon_front_item_num);\n}\n\nstatic bool M_TriggerBack(ITEM *const item, const TRIGGER *const trigger)\n{\n    M_PRIV *const p = item->priv;\n    p->mode = M_MODE_ONE_PHASE;\n    return true;\n}\n\nstatic void M_ActivateBack(ITEM *const dragon_back_item)\n{\n    if (dragon_back_item->active\n        || dragon_back_item->status == IS_DEACTIVATED) {\n        return;\n    }\n\n    const int16_t dragon_front_item_num = M_GetFrontItemNum(dragon_back_item);\n    if (dragon_front_item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const dragon_front_item = Item_Get(dragon_front_item_num);\n    dragon_back_item->touch_bits = 0;\n    dragon_front_item->touch_bits = 0;\n\n    LOT_EnableBaddieAI(dragon_front_item_num, true);\n    Item_AddActive(dragon_front_item_num);\n    Item_AddActive(Item_GetIndex(dragon_back_item));\n    dragon_back_item->status = IS_ACTIVE;\n}\n\nstatic void M_MarkDragonDead(ITEM *const dragon_back_item)\n{\n    const int16_t dragon_front_item_num = M_GetFrontItemNum(dragon_back_item);\n    if (dragon_front_item_num == NO_ITEM) {\n        return;\n    }\n    const ITEM *const dragon_front_item = Item_Get(dragon_front_item_num);\n    CREATURE *const creature = dragon_front_item->creature_data;\n    creature->flags = -1;\n    Stats_AddKill();\n\n    // Allow drops to occur at the beginning of the cinematic camera for a\n    // better window to avoid seeing the items spawn.\n    const ITEM_STATUS current_status = dragon_back_item->status;\n    dragon_back_item->status = IS_DEACTIVATED;\n    Carrier_TestItemDrops(Item_GetIndex(dragon_back_item));\n    dragon_back_item->status = current_status;\n}\n\nstatic void M_PushLaraAway(\n    ITEM *const lara_item, ITEM *const dragon_item, const int32_t shift)\n{\n    const int32_t cy = Math_Cos(dragon_item->rot.y);\n    const int32_t sy = Math_Sin(dragon_item->rot.y);\n    const int32_t base = shift < M_MID_SHIFT ? M_CLOSE_SHIFT : M_FAR_SHIFT;\n    lara_item->pos.x += (cy * (base - shift)) >> W2V_SHIFT;\n    lara_item->pos.z -= (sy * (base - shift)) >> W2V_SHIFT;\n}\n\nstatic void M_PullDagger(ITEM *const lara_item, ITEM *const dragon_back_item)\n{\n    lara_item->pos = dragon_back_item->pos;\n    lara_item->rot = dragon_back_item->rot;\n    lara_item->fall_speed = 0;\n    lara_item->gravity = false;\n    lara_item->speed = 0;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    Item_UpdateRoom(lara->item_num, dragon_back_item->room_num);\n\n    Lara_SwitchToExtraState(LS_EXTRA_PULL_DAGGER);\n\n    Camera_InvokeCinematic(lara_item, 0, 0);\n\n    M_MarkDragonDead(dragon_back_item);\n}\n\nstatic void M_Bones(const int16_t item_num)\n{\n    const int16_t bone_front_item_num = Item_Create();\n    const int16_t bone_back_item_num = Item_Create();\n\n    if (bone_back_item_num == NO_ITEM || bone_front_item_num == NO_ITEM) {\n        return;\n    }\n\n    const ITEM *const dragon_item = Item_Get(item_num);\n\n    ITEM *const bone_back = Item_Get(bone_back_item_num);\n    bone_back->object_id = O_DRAGON_BONES_3;\n    bone_back->pos = dragon_item->pos;\n    bone_back->rot.x = 0;\n    bone_back->rot.y = dragon_item->rot.y;\n    bone_back->rot.z = 0;\n    bone_back->room_num = dragon_item->room_num;\n    bone_back->shade.value_1 = -1;\n    Item_Initialise(bone_back_item_num);\n\n    ITEM *const bone_front = Item_Get(bone_front_item_num);\n    bone_front->object_id = O_DRAGON_BONES_2;\n    bone_front->pos = dragon_item->pos;\n    bone_front->rot.x = 0;\n    bone_front->rot.y = dragon_item->rot.y;\n    bone_front->rot.z = 0;\n    bone_front->room_num = dragon_item->room_num;\n    bone_front->shade.value_1 = -1;\n    Item_Initialise(bone_front_item_num);\n    bone_front->mesh_bits = ~0xC00000u;\n}\n\nstatic void M_HandleSaveBack(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->status == IS_DEACTIVATED && M_IsTwoPhaseMode(item)) {\n            const int32_t y_pos = item->pos.y;\n            int16_t room_num = item->room_num;\n            const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n            item->pos.y = Room_GetHeight(sector, item->pos);\n            const int16_t item_num = Item_GetIndex(item);\n            M_Bones(item_num);\n            item->pos.y = y_pos;\n        }\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) {\n        return;\n    }\n\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    if (item->current_anim_state != M_STATE_DEATH) {\n        Lara_Col_ItemPush(item, coll, true, false);\n        return;\n    }\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sy = Math_Sin(item->rot.y);\n    const int32_t side_shift = (cy * dz + sy * dx) >> W2V_SHIFT;\n\n    if (side_shift <= M_COL_L || side_shift >= M_COL_R) {\n        Lara_Col_ItemPush(item, coll, true, false);\n        return;\n    }\n\n    const int32_t shift = (cy * dx - sy * dz) >> W2V_SHIFT;\n    const int32_t angle = lara_item->rot.y - item->rot.y;\n    if (g_Input.action && item->object_id == O_DRAGON_BACK\n        && M_IsTwoPhaseMode(item)\n        && (Item_TestAnimEqual(item, M_ANIM_DEAD)\n            || (Item_TestAnimEqual(item, M_ANIM_RESURRECT)\n                && Item_TestFrameRange(item, 0, M_ALMOST_LIVE)))\n        && !lara_item->gravity && shift <= M_MID_SHIFT\n        && shift > M_CLOSE_SHIFT - 350 && side_shift > -350 && side_shift < 350\n        && angle > DEG_90 - 30 * DEG_1 && angle < DEG_90 + 30 * DEG_1) {\n        M_PullDagger(lara_item, item);\n    } else {\n        M_PushLaraAway(lara_item, item, shift);\n    }\n}\n\nstatic void M_ControlFront(const int16_t item_num)\n{\n}\n\nstatic void M_ControlBack(const int16_t item_num)\n{\n    const int16_t dragon_back_item_num = item_num;\n    ITEM *const dragon_back_item = Item_Get(item_num);\n\n    const int16_t dragon_front_item_num = M_GetFrontItemNum(dragon_back_item);\n    if (dragon_front_item_num == NO_ITEM) {\n        return;\n    }\n    ITEM *const dragon_front_item = Item_Get(dragon_front_item_num);\n    if (!Creature_Activate(dragon_front_item_num)) {\n        return;\n    }\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    CREATURE *const creature = dragon_front_item->creature_data;\n    const OBJECT *const front_obj = Object_Get(O_DRAGON_FRONT);\n    const bool is_two_phase = M_IsTwoPhaseMode(dragon_back_item);\n\n    if (dragon_front_item->hit_points <= 0) {\n        if (dragon_front_item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(dragon_front_item, M_ANIM_DIE, 0);\n            dragon_front_item->goal_anim_state = M_STATE_DEATH;\n            dragon_front_item->current_anim_state = M_STATE_DEATH;\n            creature->flags = 0;\n        } else if (creature->flags >= 0) {\n            creature->flags++;\n            if (is_two_phase) {\n                Spawn_MysticLight(dragon_front_item_num);\n                if (creature->flags == M_LIVE_TIME) {\n                    dragon_front_item->goal_anim_state = M_STATE_STOP;\n                }\n                if (creature->flags > M_LIVE_TIME + M_ALMOST_LIVE) {\n                    dragon_front_item->hit_points = front_obj->hit_points / 2;\n                }\n            } else if (creature->flags == M_ONE_PHASE_TIME) {\n                M_MarkDragonDead(dragon_back_item);\n                creature->flags = M_DISSOLVE_TIME;\n            }\n        } else {\n            if (creature->flags > M_LIGHT_TIME) {\n                Output_AddDynamicLight(\n                    dragon_front_item->pos,\n                    ((4 * Random_GetDraw()) >> 15) + 12 + creature->flags / 2,\n                    ((4 * Random_GetDraw()) >> 15) + 10 + creature->flags / 2);\n            }\n\n            if (creature->flags == M_BONE_TIME) {\n                M_Bones(dragon_back_item_num);\n            } else if (creature->flags == M_DISSOLVE_TIME) {\n                Room_TestTriggers(dragon_back_item);\n                LOT_DisableBaddieAI(dragon_front_item_num);\n                dragon_front_item->status = IS_DEACTIVATED;\n                dragon_back_item->status = IS_DEACTIVATED;\n                if (is_two_phase) {\n                    Item_Kill(dragon_front_item_num);\n                    Item_Kill(dragon_back_item_num);\n                } else {\n                    Item_RemoveActive(dragon_front_item_num);\n                    Item_RemoveActive(dragon_back_item_num);\n                    dragon_front_item->collidable = false;\n                    dragon_back_item->collidable = false;\n                    dragon_front_item->flags |= IF_ONE_SHOT;\n                    dragon_back_item->flags |= IF_ONE_SHOT;\n                }\n            } else if (creature->flags < M_BONE_TIME) {\n                dragon_front_item->pos.y += M_DISSOLVE_SHIFT;\n                dragon_back_item->pos.y += M_DISSOLVE_SHIFT;\n            }\n\n            creature->flags--;\n            return;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(dragon_front_item, &info);\n        Creature_Mood(dragon_front_item, &info, true);\n\n        angle = Creature_Turn(dragon_front_item, M_WALK_TURN);\n        const bool is_ahead = info.ahead && info.distance > M_CLOSE_RANGE\n            && info.distance < M_STOP_RANGE;\n        if (dragon_front_item->touch_bits) {\n            Lara_TakeDamage(M_TOUCH_DAMAGE, true);\n        }\n\n        switch (dragon_front_item->current_anim_state) {\n        case M_STATE_WALK:\n            creature->flags = 0;\n            if (is_ahead) {\n                dragon_front_item->goal_anim_state = M_STATE_STOP;\n            } else if (angle < -M_NEED_TURN) {\n                if (info.distance < M_STOP_RANGE && info.ahead) {\n                    dragon_front_item->goal_anim_state = M_STATE_STOP;\n                } else {\n                    dragon_front_item->goal_anim_state = M_STATE_LEFT;\n                }\n            } else if (angle > M_NEED_TURN) {\n                if (info.distance < M_STOP_RANGE && info.ahead) {\n                    dragon_front_item->goal_anim_state = M_STATE_STOP;\n                } else {\n                    dragon_front_item->goal_anim_state = M_STATE_RIGHT;\n                }\n            }\n            break;\n\n        case M_STATE_LEFT:\n            if (angle > -M_NEED_TURN || is_ahead) {\n                dragon_front_item->goal_anim_state = M_STATE_WALK;\n            }\n            break;\n\n        case M_STATE_RIGHT:\n            if (angle < M_NEED_TURN || is_ahead) {\n                dragon_front_item->goal_anim_state = M_STATE_WALK;\n            }\n            break;\n\n        case M_STATE_AIM:\n            dragon_front_item->rot.y -= angle;\n            if (info.ahead) {\n                head = -info.angle;\n            }\n\n            if (is_ahead) {\n                creature->flags = 30;\n                dragon_front_item->goal_anim_state = M_STATE_FIRE;\n            } else {\n                creature->flags = 0;\n                dragon_front_item->goal_anim_state = M_STATE_AIM;\n            }\n            break;\n\n        case M_STATE_FIRE:\n            dragon_front_item->rot.y -= angle;\n            if (info.ahead) {\n                head = -info.angle;\n            }\n\n            Sound_Effect(SFX_DRAGON_FIRE, &dragon_front_item->pos, SPM_NORMAL);\n\n            if (creature->flags != 0) {\n                if (info.ahead) {\n                    Creature_Effect(\n                        dragon_front_item, &m_DragonMouth, Spawn_FireStream);\n                }\n                creature->flags--;\n            } else {\n                dragon_front_item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_STOP:\n            dragon_front_item->rot.y -= angle;\n            if (is_ahead) {\n                dragon_front_item->goal_anim_state = M_STATE_AIM;\n            } else if (info.distance > M_STOP_RANGE || !info.ahead) {\n                dragon_front_item->goal_anim_state = M_STATE_WALK;\n            } else if (info.distance >= M_CLOSE_RANGE || creature->flags != 0) {\n                if (info.angle < 0) {\n                    dragon_front_item->goal_anim_state = M_STATE_TURN_LEFT;\n                } else {\n                    dragon_front_item->goal_anim_state = M_STATE_TURN_RIGHT;\n                }\n            } else {\n                creature->flags = 1;\n                if (info.angle < 0) {\n                    dragon_front_item->goal_anim_state = M_STATE_SWIPE_LEFT;\n                } else {\n                    dragon_front_item->goal_anim_state = M_STATE_SWIPE_RIGHT;\n                }\n            }\n            break;\n\n        case M_STATE_TURN_LEFT:\n            creature->flags = 0;\n            dragon_front_item->rot.y += -DEG_1 - angle;\n            break;\n\n        case M_STATE_TURN_RIGHT:\n            creature->flags = 0;\n            dragon_front_item->rot.y += DEG_1 - angle;\n            break;\n\n        case M_STATE_SWIPE_LEFT:\n            if ((dragon_front_item->touch_bits & M_TOUCH_L) != 0) {\n                Lara_TakeDamage(M_SWIPE_DAMAGE, true);\n                creature->flags = 0;\n            }\n            break;\n\n        case M_STATE_SWIPE_RIGHT:\n            if ((dragon_front_item->touch_bits & M_TOUCH_R) != 0) {\n                Lara_TakeDamage(M_SWIPE_DAMAGE, true);\n                creature->flags = 0;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Head(dragon_front_item, head);\n    Creature_Animate(dragon_front_item_num, angle, 0);\n    dragon_back_item->current_anim_state =\n        dragon_front_item->current_anim_state;\n    const int16_t anim_num = Item_GetRelativeAnim(dragon_front_item);\n    const int16_t frame_num = Item_GetRelativeFrame(dragon_front_item);\n    Item_SwitchToAnim(dragon_back_item, anim_num, frame_num);\n    dragon_back_item->pos = dragon_front_item->pos;\n    dragon_back_item->rot = dragon_front_item->rot;\n    Item_UpdateRoom(dragon_back_item_num, dragon_front_item->room_num);\n}\n\nstatic void M_SetupFront(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    SOFT_ASSERT(\n        Object_Get(O_DRAGON_BACK)->loaded, \"Dragon back object missing\");\n    obj->initialise_func = M_InitialiseFront;\n    obj->control_func = M_ControlFront;\n    obj->collision_func = M_Collision;\n\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->pivot_length = 300;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 10)->rot.z = true;\n}\n\nstatic void M_SetupBack(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_InitialiseBack;\n    obj->handle_save_func = M_HandleSaveBack;\n    obj->trigger_func = M_TriggerBack;\n    obj->activate_func = M_ActivateBack;\n    obj->control_func = M_ControlBack;\n    obj->collision_func = M_Collision;\n    obj->can_drop_items_func = M_CanDropItemsBack;\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->radius = M_RADIUS;\n\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n    obj->priv_size = sizeof(M_PRIV);\n}\n\nREGISTER_OBJECT(O_DRAGON_FRONT, M_SetupFront)\nREGISTER_OBJECT(O_DRAGON_BACK, M_SetupBack)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/eel.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define EEL_HITPOINTS  5\n#define EEL_TOUCH_BITS 0b00000001'10000000 // = 0x180\n#define EEL_DAMAGE     50\n#define EEL_ANGLE      (DEG_1 * 10) // = 1820\n#define EEL_RANGE      (WALL_L * 2) // = 2048\n#define EEL_MOVE       (WALL_L / 10) // = 102\n#define EEL_TURN       (DEG_1 / 2) // = 91\n#define EEL_LENGTH     (WALL_L / 2) // = 512\n#define EEL_SLIDE      (EEL_RANGE - EEL_LENGTH) // = 1536\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    EEL_STATE_EMPTY  = 0,\n    EEL_STATE_ATTACK = 1,\n    EEL_STATE_STOP   = 2,\n    EEL_STATE_DEATH  = 3,\n    // clang-format on\n} EEL_STATE;\n\ntypedef enum {\n    EEL_ANIM_DEATH = 3,\n} EEL_ANIM;\n\ntypedef struct {\n    int32_t pos;\n} M_PRIV;\n\nstatic const BITE m_EelBite = {\n    .pos = { .x = 7, .y = 157, .z = 333 },\n    .mesh_num = 7,\n};\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    int32_t pos = p->pos;\n    item->pos.x -= (pos * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z -= (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n\n    if (item->hit_points <= 0) {\n        if (pos < EEL_SLIDE) {\n            pos += EEL_MOVE;\n        }\n        if (item->current_anim_state != EEL_STATE_DEATH) {\n            Item_SwitchToAnim(item, EEL_ANIM_DEATH, 0);\n            item->current_anim_state = EEL_STATE_DEATH;\n        }\n    } else {\n        const ITEM *const lara_item = Lara_GetItem();\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        const int16_t quadrant = (item->rot.y + DEG_45) & 0xC000;\n        const int16_t angle = Math_Atan(dz, dx);\n        const int32_t distance = Math_Sqrt(SQUARE(dx) + SQUARE(dz));\n\n        switch (item->current_anim_state) {\n        case EEL_STATE_STOP:\n            if (pos > 0) {\n                pos -= EEL_MOVE;\n            }\n            if (distance <= EEL_RANGE && ABS(angle - quadrant) < EEL_ANGLE) {\n                item->goal_anim_state = EEL_STATE_ATTACK;\n            }\n            break;\n\n        case EEL_STATE_ATTACK:\n            if (pos < distance - EEL_LENGTH) {\n                pos += EEL_MOVE;\n            }\n            if (angle < item->rot.y - EEL_TURN) {\n                item->rot.y -= EEL_TURN;\n            } else if (angle > item->rot.y + EEL_TURN) {\n                item->rot.y += EEL_TURN;\n            }\n            if (item->required_anim_state == EEL_STATE_EMPTY\n                && (item->touch_bits & EEL_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(EEL_DAMAGE, true);\n                Creature_Effect(item, &m_EelBite, Spawn_Blood);\n                item->required_anim_state = EEL_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    item->pos.x += (pos * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z += (pos * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    p->pos = pos;\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->priv_size = sizeof(M_PRIV);\n\n    obj->hit_points = EEL_HITPOINTS;\n\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_EEL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/hybrid_mutant.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS           (WALL_L / 8)           // = 128\n#define M_HIT_POINTS       90\n#define M_SLASH_DAMAGE     100\n#define M_KICK_DAMAGE      80\n#define M_JUMP_DAMAGE      20\n#define M_TOUCH_BITS_LEFT  0b00000000'10000000\n#define M_TOUCH_BITS_RIGHT 0b00001000'00000000\n#define M_ALERT_DIST       SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_1    SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_2    SQUARE(WALL_L * 2)     // = 4194304\n#define M_ATTACK_DIST_3    SQUARE(WALL_L * 4 / 3) // = 1863225\n#define M_JUMP_ANGLE       DEG_45\n#define M_WALK_TURN        (DEG_1 * 3)            // = 546\n#define M_RUN_TURN         (DEG_1 * 6)            // = 1092\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_JUMP_START,\n    M_STATE_JUMP_MID,\n    M_STATE_JUMP_END,\n    M_STATE_SLASH,\n    M_STATE_KICK,\n    M_STATE_RUN_ATTACK,\n    M_STATE_WALK_ATTACK,\n    M_STATE_DEATH,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 18,\n} M_ANIM;\n\nstatic const BITE m_BiteLeft = {\n    .pos = { 19, -13, 3 },\n    .mesh_num = 7,\n};\nstatic const BITE m_BiteRight = {\n    .pos = { 19, -13, 3 },\n    .mesh_num = 14,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    ITEM *const lara_item = Lara_GetItem();\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.angle = info.angle;\n        lara_info.distance = info.distance;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if ((lara_info.distance < M_ALERT_DIST || item->hit_status\n         || Creature_CanSeeEnemy(item, &lara_info))) {\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        head = info.angle;\n        creature->maximum_turn = 0;\n        creature->flags = 0;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            item->goal_anim_state = M_STATE_STOP;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target == item || !info.ahead) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            info.angle < M_JUMP_ANGLE && info.angle > -M_JUMP_ANGLE\n            && info.distance > M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_JUMP_START;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n            if (info.angle < 0) {\n                item->goal_anim_state = M_STATE_SLASH;\n            } else {\n                item->goal_anim_state = M_STATE_KICK;\n            }\n        } else if (item->required_anim_state != M_STATE_NULL) {\n            item->goal_anim_state = item->required_anim_state;\n        } else if (info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            creature->maximum_turn = M_WALK_TURN;\n            item->goal_anim_state = M_STATE_WALK_ATTACK;\n        } else if (\n            creature->mood == MOOD_ESCAPE || creature->mood == MOOD_ATTACK) {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood == MOOD_BORED\n            || (creature->mood == MOOD_ESCAPE && lara->target != item\n                && info.ahead)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->flags != 0 && info.ahead) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_2) {\n            if (lara_item->speed != 0) {\n                item->goal_anim_state = M_STATE_RUN_ATTACK;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        creature->flags = 0;\n        break;\n\n    case M_STATE_JUMP_START:\n        creature->maximum_turn = M_WALK_TURN;\n        break;\n\n    case M_STATE_JUMP_MID:\n    case M_STATE_JUMP_END:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = 0;\n\n        if ((item->touch_bits & M_TOUCH_BITS_LEFT) != 0) {\n            Lara_TakeDamage(M_JUMP_DAMAGE, true);\n            Creature_Effect(item, &m_BiteLeft, Spawn_Blood);\n        }\n        break;\n\n    case M_STATE_SLASH:\n    case M_STATE_RUN_ATTACK:\n    case M_STATE_WALK_ATTACK:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (creature->flags == 0\n            && (item->touch_bits & M_TOUCH_BITS_LEFT) != 0) {\n            Lara_TakeDamage(M_SLASH_DAMAGE, true);\n            Creature_Effect(item, &m_BiteLeft, Spawn_Blood);\n            creature->flags = 1;\n        }\n\n        if (!info.bite || info.distance >= M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n\n        if (Item_TestFrameEqual(item, -1)) {\n            creature->flags = 0;\n        }\n        break;\n\n    case M_STATE_KICK:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (creature->flags == 0\n            && (item->touch_bits & M_TOUCH_BITS_RIGHT) != 0) {\n            Lara_TakeDamage(M_KICK_DAMAGE, true);\n            Creature_Effect(item, &m_BiteRight, Spawn_Blood);\n            creature->flags = 1;\n        }\n\n        if (!info.bite || info.distance >= M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n\n        if (Item_TestFrameEqual(item, -1)) {\n            creature->flags = 0;\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, 0);\n    Creature_Joint(item, 0, torso_x);\n    Creature_Joint(item, 1, torso_y);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.x = true;\n    Object_GetBone(obj, 0)->rot.z = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_HYBRID_MUTANT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/jelly.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define JELLY_HITPOINTS    10\n#define JELLY_RADIUS       (WALL_L / 10) // = 102\n#define JELLY_STING_DAMAGE 5\n#define JELLY_TURN         (DEG_1 * 90) // = 16380\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    JELLY_STATE_EMPTY = 0,\n    JELLY_STATE_MOVE  = 1,\n    JELLY_STATE_STOP  = 2,\n    // clang-format on\n} JELLY_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    if (item->hit_points <= 0) {\n        if (Item_Explode(item_num, -1, 0)) {\n            LOT_DisableBaddieAI(item_num);\n            Item_Kill(item_num);\n            item->status = IS_DEACTIVATED;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        int16_t angle = Creature_Turn(item, JELLY_TURN);\n\n        switch (item->current_anim_state) {\n        case JELLY_STATE_STOP:\n            if (creature->mood != MOOD_BORED) {\n                item->goal_anim_state = JELLY_STATE_MOVE;\n            }\n            break;\n\n        case JELLY_STATE_MOVE:\n            if (creature->mood == MOOD_BORED || item->touch_bits != 0) {\n                item->goal_anim_state = JELLY_STATE_STOP;\n            }\n            break;\n        }\n\n        if (item->touch_bits != 0) {\n            Lara_TakeDamage(JELLY_STING_DAMAGE, true);\n        }\n\n        Creature_Head(item, 0);\n        Creature_Animate(item_num, angle, 0);\n        Creature_Underwater(item, 0);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = JELLY_HITPOINTS;\n    obj->radius = JELLY_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_JELLY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/larson.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n\n#define LARSON_POSE_CHANCE 0x60 // = 96\n#define LARSON_SHOT_DAMAGE 50\n#define LARSON_WALK_TURN (DEG_1 * 3) // = 546\n#define LARSON_RUN_TURN (DEG_1 * 6) // = 1092\n#define LARSON_WALK_RANGE SQUARE(WALL_L * 3) // = 9437184\n#define LARSON_DIE_ANIM 15\n#define LARSON_HITPOINTS 50\n#define LARSON_RADIUS (WALL_L / 10) // = 102\n#define LARSON_SMARTNESS 0x7FFF\n\ntypedef enum {\n    LARSON_STATE_EMPTY = 0,\n    LARSON_STATE_STOP = 1,\n    LARSON_STATE_WALK = 2,\n    LARSON_STATE_RUN = 3,\n    LARSON_STATE_AIM = 4,\n    LARSON_STATE_DEATH = 5,\n    LARSON_STATE_POSE = 6,\n    LARSON_STATE_SHOOT = 7,\n} LARSON_STATE;\n\nstatic const CREATURE_GUN m_LarsonGun = {\n    .muzzle = { .pos = { -60, 170, 0 }, .mesh_num = 14 },\n};\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->hit_points <= 0) {\n            const uint16_t flags =\n                Music_GetTrackFlags(Music_ToGameID(MX_LARSON_SPEECH));\n            Music_SetTrackFlags(\n                Music_ToGameID(MX_LARSON_SPEECH), flags | IF_ONE_SHOT);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const person = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != LARSON_STATE_DEATH) {\n            item->current_anim_state = LARSON_STATE_DEATH;\n            Item_SwitchToAnim(item, LARSON_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, person->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case LARSON_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (person->mood == MOOD_BORED) {\n                item->goal_anim_state = Random_GetControl() < LARSON_POSE_CHANCE\n                    ? LARSON_STATE_POSE\n                    : LARSON_STATE_WALK;\n            } else if (person->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = LARSON_STATE_RUN;\n            } else {\n                item->goal_anim_state = LARSON_STATE_WALK;\n            }\n            break;\n\n        case LARSON_STATE_POSE:\n            if (person->mood != MOOD_BORED) {\n                item->goal_anim_state = LARSON_STATE_STOP;\n            } else if (Random_GetControl() < LARSON_POSE_CHANCE) {\n                item->required_anim_state = LARSON_STATE_WALK;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            }\n            break;\n\n        case LARSON_STATE_WALK:\n            person->maximum_turn = LARSON_WALK_TURN;\n            if (person->mood == MOOD_BORED\n                && Random_GetControl() < LARSON_POSE_CHANCE) {\n                item->required_anim_state = LARSON_STATE_POSE;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            } else if (person->mood == MOOD_ESCAPE) {\n                item->required_anim_state = LARSON_STATE_RUN;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = LARSON_STATE_AIM;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            } else if (!info.ahead || info.distance > LARSON_WALK_RANGE) {\n                item->required_anim_state = LARSON_STATE_RUN;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            }\n            break;\n\n        case LARSON_STATE_RUN:\n            person->maximum_turn = LARSON_RUN_TURN;\n            tilt = angle / 2;\n            if (person->mood == MOOD_BORED\n                && Random_GetControl() < LARSON_POSE_CHANCE) {\n                item->required_anim_state = LARSON_STATE_POSE;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = LARSON_STATE_AIM;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            } else if (info.ahead && info.distance < LARSON_WALK_RANGE) {\n                item->required_anim_state = LARSON_STATE_WALK;\n                item->goal_anim_state = LARSON_STATE_STOP;\n            }\n            break;\n\n        case LARSON_STATE_AIM:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = LARSON_STATE_SHOOT;\n            } else {\n                item->goal_anim_state = LARSON_STATE_STOP;\n            }\n            break;\n\n        case LARSON_STATE_SHOOT:\n            if (!item->required_anim_state) {\n                Creature_Shoot(\n                    item, &info, &m_LarsonGun, head, LARSON_SHOT_DAMAGE);\n                item->required_anim_state = LARSON_STATE_AIM;\n            }\n            if (person->mood == MOOD_ESCAPE) {\n                item->required_anim_state = LARSON_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = LARSON_HITPOINTS;\n    obj->radius = LARSON_RADIUS;\n    obj->smartness = LARSON_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_LARSON, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/lion.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n#define LION_BITE_DAMAGE 250\n#define LION_POUNCE_DAMAGE 150\n#define LION_TOUCH 0x380066\n#define LION_WALK_TURN (2 * DEG_1) // = 364\n#define LION_RUN_TURN (5 * DEG_1) // = 910\n#define LION_ROAR_CHANCE 128\n#define LION_POUNCE_RANGE SQUARE(WALL_L) // = 1048576\n#define LION_DIE_ANIM 7\n#define LION_HITPOINTS 30\n#define LION_RADIUS (WALL_L / 3) // = 341\n#define LION_SMARTNESS 0x7FFF\n\n#define LIONESS_HITPOINTS 25\n#define LIONESS_RADIUS (WALL_L / 3) // = 341\n#define LIONESS_SMARTNESS 0x2000\n\n#define PUMA_DIE_ANIM 4\n#define PUMA_HITPOINTS 45\n#define PUMA_RADIUS (WALL_L / 3) // = 341\n#define PUMA_SMARTNESS 0x2000\n\ntypedef enum {\n    LION_STATE_EMPTY = 0,\n    LION_STATE_STOP = 1,\n    LION_STATE_WALK = 2,\n    LION_STATE_RUN = 3,\n    LION_STATE_ATTACK_1 = 4,\n    LION_STATE_DEATH = 5,\n    LION_STATE_WARNING = 6,\n    LION_STATE_ATTACK_2 = 7,\n} LION_STATE;\n\nstatic BITE m_LionBite = { .pos = { -2, -10, 132 }, .mesh_num = 21 };\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const lion = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != LION_STATE_DEATH) {\n            item->current_anim_state = LION_STATE_DEATH;\n            int16_t anim_idx =\n                item->object_id == O_PUMA ? PUMA_DIE_ANIM : LION_DIE_ANIM;\n            Item_SwitchToAnim(\n                item, anim_idx + (int16_t)(Random_GetControl() / 0x4000), 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, lion->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case LION_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (lion->mood == MOOD_BORED) {\n                item->goal_anim_state = LION_STATE_WALK;\n            } else if (info.ahead && (item->touch_bits & LION_TOUCH)) {\n                item->goal_anim_state = LION_STATE_ATTACK_2;\n            } else if (info.ahead && info.distance < LION_POUNCE_RANGE) {\n                item->goal_anim_state = LION_STATE_ATTACK_1;\n            } else {\n                item->goal_anim_state = LION_STATE_RUN;\n            }\n            break;\n\n        case LION_STATE_WALK:\n            lion->maximum_turn = LION_WALK_TURN;\n            if (lion->mood != MOOD_BORED) {\n                item->goal_anim_state = LION_STATE_STOP;\n            } else if (Random_GetControl() < LION_ROAR_CHANCE) {\n                item->required_anim_state = LION_STATE_WARNING;\n                item->goal_anim_state = LION_STATE_STOP;\n            }\n            break;\n\n        case LION_STATE_RUN:\n            lion->maximum_turn = LION_RUN_TURN;\n            tilt = angle;\n            if (lion->mood == MOOD_BORED) {\n                item->goal_anim_state = LION_STATE_STOP;\n            } else if (info.ahead && info.distance < LION_POUNCE_RANGE) {\n                item->goal_anim_state = LION_STATE_STOP;\n            } else if ((item->touch_bits & LION_TOUCH) && info.ahead) {\n                item->goal_anim_state = LION_STATE_STOP;\n            } else if (\n                lion->mood != MOOD_ESCAPE\n                && Random_GetControl() < LION_ROAR_CHANCE) {\n                item->required_anim_state = LION_STATE_WARNING;\n                item->goal_anim_state = LION_STATE_STOP;\n            }\n            break;\n\n        case LION_STATE_ATTACK_1:\n            if (item->required_anim_state == LION_STATE_EMPTY\n                && (item->touch_bits & LION_TOUCH)) {\n                Lara_TakeDamage(LION_POUNCE_DAMAGE, true);\n                item->required_anim_state = LION_STATE_STOP;\n            }\n            break;\n\n        case LION_STATE_ATTACK_2:\n            if (item->required_anim_state == LION_STATE_EMPTY\n                && (item->touch_bits & LION_TOUCH)) {\n                Creature_Effect(item, &m_LionBite, Spawn_Blood);\n                Lara_TakeDamage(LION_BITE_DAMAGE, true);\n                item->required_anim_state = LION_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, tilt);\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_QUADRUPED);\n    obj->pivot_length = 400;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 19)->rot.y = true;\n}\n\nstatic void M_SetupLion(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->hit_points = LION_HITPOINTS;\n    obj->radius = LION_RADIUS;\n    obj->smartness = LION_SMARTNESS;\n}\n\nstatic void M_SetupLioness(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->hit_points = LIONESS_HITPOINTS;\n    obj->radius = LIONESS_RADIUS;\n    obj->smartness = LIONESS_SMARTNESS;\n}\n\nstatic void M_SetupPuma(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->hit_points = PUMA_HITPOINTS;\n    obj->radius = PUMA_RADIUS;\n    obj->smartness = PUMA_SMARTNESS;\n}\n\nREGISTER_OBJECT(O_LION, M_SetupLion)\nREGISTER_OBJECT(O_LIONESS, M_SetupLioness)\nREGISTER_OBJECT(O_PUMA, M_SetupPuma)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/lizard.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/tribe_boss.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_SWIPE_DAMAGE     120\n#define M_BITE_DAMAGE      100\n#define M_RUN_TURN         (DEG_1 * 4) // = 728\n#define M_WALK_TURN        (DEG_1 * 10) // = 1820\n#define M_ATTACK_0_RANGE   SQUARE(WALL_L * 5 / 2) // = 0x640000\n#define M_ATTACK_1_RANGE   SQUARE(WALL_L * 3 / 4) // = 0x90000\n#define M_ATTACK_2_RANGE   SQUARE(WALL_L * 3 / 2) // = 0x240000\n#define M_WALK_RANGE       SQUARE(WALL_L * 2)\n#define M_WALK_CHANCE      0x100\n#define M_WAIT_CHANCE      0x100\n#define M_BITE_TOUCH_BITS  0xC00\n#define M_SWIPE_TOUCH_BITS 0x20\n#define M_VAULT_SHIFT      260\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_PUNCH_2,\n    M_STATE_AIM_2,\n    M_STATE_WAIT,\n    M_STATE_AIM_1,\n    M_STATE_AIM_0,\n    M_STATE_PUNCH_1,\n    M_STATE_PUNCH_B,\n    M_STATE_RUN,\n    M_STATE_DEATH,\n    M_STATE_CLIMB_3,\n    M_STATE_CLIMB_1,\n    M_STATE_CLIMB_2,\n    M_STATE_FALL_3\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_SLIDE_1 = 23,\n    M_ANIM_DEATH = 26,\n    M_ANIM_CLIMB_3 = 27,\n    M_ANIM_CLIMB_1 = 28,\n    M_ANIM_CLIMB_2 = 29,\n    M_ANIM_FALL_3 = 30,\n    M_ANIM_SLIDE_2 = 31,\n} M_ANIM;\n\nstatic BITE m_BiteHit = {\n    .pos = { .x = 0, .y = -120, .z = 120 },\n    .mesh_num = 10,\n};\nstatic BITE m_SwipeHit = {\n    .pos = { .x = 0, .y = 0, .z = 0 },\n    .mesh_num = 5,\n};\nstatic BITE m_GasHit = {\n    .pos = { .x = 0, .y = -64, .z = 56 },\n    .mesh_num = 9,\n};\n\nstatic void M_TriggerGas(\n    const XYZ_32 pos, const XYZ_32 vel, const int32_t effect_num)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->src_color.b = 32;\n    spark->dst_color.r = 0;\n    spark->dst_color.g = (Random_GetControl() & 0xF) + 32;\n    spark->dst_color.b = 0;\n\n    if (vel.x != 0 || vel.y != 0 || vel.z != 0) {\n        spark->col_fade_speed = 6;\n        spark->fade_to_black = 2;\n        spark->life = (Random_GetControl() & 1) + 12;\n    } else {\n        spark->col_fade_speed = 8;\n        spark->fade_to_black = 16;\n        spark->life = (Random_GetControl() & 3) + 20;\n    }\n\n    spark->s_life = spark->life;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = vel.x + (Random_GetControl() & 0xF) - 16;\n    spark->vel.y = vel.y;\n    spark->vel.z = vel.z + (Random_GetControl() & 0xF) - 16;\n    spark->friction = 0;\n\n    if (Random_GetControl() & 1) {\n        if (effect_num < 0) {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n                | SPARK_F_SCALE;\n        } else {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n                | SPARK_F_SPRITE | SPARK_F_SCALE;\n        }\n\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else if (effect_num < 0) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->max_y_vel = 0;\n    spark->effect_num = (uint8_t)effect_num;\n    spark->gravity = 0;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    const int32_t size = (Random_GetControl() & 0x1F) + 48;\n    if (vel.x != 0 || vel.y != 0 || vel.z != 0) {\n        spark->size.width = size >> 5;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = size >> 1;\n        spark->size.height = spark->size.width;\n        spark->src_size.height = spark->size.height;\n        spark->dst_size.height = spark->dst_size.width;\n\n        if (effect_num == -2) {\n            spark->scalar = 2;\n        } else {\n            spark->scalar = 3;\n        }\n    } else {\n        spark->scalar = 4;\n        spark->size.width = size >> 4;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = size >> 1;\n        spark->size.height = spark->size.width;\n        spark->src_size.height = spark->size.height;\n        spark->dst_size.height = spark->dst_size.width;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nstatic int16_t M_TriggerGasThrower(\n    const ITEM *const item, const BITE *const bite, const int16_t speed)\n{\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num == NO_ITEM) {\n        return NO_ITEM;\n    }\n\n    XYZ_32 pos = bite->pos;\n    Collide_GetJointAbsPosition(item, &pos, bite->mesh_num);\n\n    XYZ_32 pos1 = {\n        .x = bite->pos.x,\n        .y = bite->pos.y << 3,\n        .z = bite->pos.z << 2,\n    };\n    Collide_GetJointAbsPosition(item, &pos1, bite->mesh_num);\n\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        pos1.x - pos.x, pos1.y - pos.y, pos1.z - pos.z, angles);\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = pos;\n    effect->room_num = item->room_num;\n    effect->rot.x = angles[1];\n    effect->rot.z = 0;\n    effect->rot.y = angles[0];\n    effect->speed = speed << 2;\n    effect->object_id = O_MISSILE_POISON;\n    effect->counter = 20;\n    M_TriggerGas((XYZ_32) {}, (XYZ_32) {}, effect_num);\n\n    for (int32_t i = 0; i < 2; i++) {\n        const int32_t s = Random_GetControl() % (speed << 2) + 32;\n        const int32_t r = (s * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n        XYZ_32 vel = {\n            .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT,\n            .y = -((s * Math_Sin(effect->rot.x)) >> W2V_SHIFT),\n            .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT,\n        };\n        M_TriggerGas(\n            effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -1);\n    }\n\n    {\n        const int32_t r = ((speed << 1) * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n        const XYZ_32 vel = {\n            .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT,\n            .y = -(((speed << 1) * Math_Sin(effect->rot.x)) >> W2V_SHIFT),\n            .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT,\n        };\n        M_TriggerGas(\n            effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -2);\n    }\n\n    return effect_num;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t neck = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        } else if (\n            TribeBoss_IsLizardActive() && Item_GetRelativeFrame(item) == 50) {\n            Creature_Die(item_num, true);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        if (Box_GetBox(creature->enemy->box_num)->overlap_index\n            & BOX_BLOCKED_SEARCH) {\n            creature->mood = MOOD_ATTACK;\n        }\n\n        LARA_INFO *const lara_info = Lara_GetLaraInfo();\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case M_STATE_STOP:\n            creature->flags = 0;\n\n            if (info.ahead) {\n                neck = info.angle;\n            }\n\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (item->required_anim_state) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else {\n                    item->goal_anim_state = M_STATE_WAIT;\n                }\n            } else if (info.bite && info.distance < M_ATTACK_1_RANGE) {\n                item->goal_anim_state = M_STATE_AIM_1;\n            } else if (\n                Creature_CanTargetEnemy(item, &info) && info.bite\n                && info.distance < M_ATTACK_0_RANGE\n                && (lara_info->poison_timer < 256\n                    || (Box_GetBox(creature->enemy->box_num)->overlap_index\n                        & BOX_BLOCKED_SEARCH))) {\n                item->goal_anim_state = M_STATE_AIM_0;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_WALK:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n\n            if (Item_GetRelativeAnim(item) == M_ANIM_SLIDE_1\n                || Item_GetRelativeAnim(item) == M_ANIM_SLIDE_2) {\n                creature->maximum_turn = 0;\n            } else {\n                creature->maximum_turn = M_WALK_TURN;\n            }\n\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < M_WAIT_CHANCE) {\n                    item->required_anim_state = M_STATE_WAIT;\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else if (info.bite && info.distance < M_ATTACK_1_RANGE) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.bite && info.distance < M_ATTACK_2_RANGE) {\n                item->goal_anim_state = M_STATE_AIM_2;\n            } else if (\n                Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_ATTACK_0_RANGE\n                && (lara_info->poison_timer < 256\n                    || (Box_GetBox(creature->enemy->box_num)->overlap_index\n                        & BOX_BLOCKED_SEARCH))) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.distance > M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_PUNCH_2:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (creature->flags != 2 && item->touch_bits & M_BITE_TOUCH_BITS) {\n                Lara_TakeDamage(M_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_BiteHit, Spawn_Blood);\n                creature->flags = 2;\n            }\n            break;\n\n        case M_STATE_AIM_2:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = M_WALK_TURN;\n            creature->flags = 0;\n            if (info.bite && info.distance < M_ATTACK_2_RANGE) {\n                item->goal_anim_state = M_STATE_PUNCH_2;\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n            break;\n\n        case M_STATE_WAIT:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (creature->mood != MOOD_BORED) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (Random_GetControl() < M_WALK_CHANCE) {\n                item->required_anim_state = M_STATE_WALK;\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_AIM_1:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = M_WALK_TURN;\n            creature->flags = 0;\n            if (info.ahead && info.distance < M_ATTACK_1_RANGE) {\n                item->goal_anim_state = M_STATE_PUNCH_1;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_AIM_0:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n\n            if (info.bite && info.distance < M_ATTACK_0_RANGE\n                && (lara_info->poison_timer < 256\n                    || (Box_GetBox(creature->enemy->box_num)->overlap_index\n                        & BOX_BLOCKED_SEARCH))) {\n                item->goal_anim_state = M_STATE_PUNCH_B;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_PUNCH_1:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n\n            if (!creature->flags && item->touch_bits & M_SWIPE_TOUCH_BITS) {\n                Lara_TakeDamage(M_SWIPE_DAMAGE, true);\n                Creature_Effect(item, &m_SwipeHit, Spawn_Blood);\n                creature->flags = 1;\n            }\n\n            if (info.distance < M_ATTACK_2_RANGE) {\n                item->goal_anim_state = M_STATE_PUNCH_2;\n            }\n            break;\n\n        case M_STATE_PUNCH_B:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n\n            if (Item_GetRelativeFrame(item) >= 7\n                && Item_GetRelativeFrame(item) <= 28) {\n                if (creature->flags < 24) {\n                    creature->flags += 2;\n                }\n\n                int32_t f;\n                if (creature->flags < 24) {\n                    f = creature->flags;\n                } else {\n                    f = (Random_GetControl() & 0xF) + 8;\n                }\n\n                M_TriggerGasThrower(item, &m_GasHit, f);\n            }\n\n            if (Item_GetRelativeFrame(item) > 28) {\n                creature->flags = 0;\n            }\n            break;\n\n        case M_STATE_RUN:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n\n            creature->maximum_turn = M_RUN_TURN;\n            tilt = angle / 2;\n\n            if (creature->mood != MOOD_ESCAPE) {\n                if (creature->mood == MOOD_BORED) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else if (info.bite && info.distance < M_ATTACK_1_RANGE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (\n                    Creature_CanTargetEnemy(item, &info)\n                    && info.distance < M_ATTACK_0_RANGE\n                    && (lara_info->poison_timer < 256\n                        || (Box_GetBox(creature->enemy->box_num)->overlap_index\n                            & BOX_BLOCKED_SEARCH))) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (info.ahead && info.distance < M_WALK_RANGE) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, 0);\n    Creature_Joint(item, 1, neck);\n\n    if (item->current_anim_state >= M_STATE_DEATH) {\n        Creature_Animate(item_num, angle, 0);\n    } else {\n        switch (Creature_Vault(item_num, angle, 2, M_VAULT_SHIFT)) {\n        case -4:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_FALL_3, 0);\n            item->current_anim_state = M_STATE_FALL_3;\n            break;\n\n        case 2:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_CLIMB_1, 0);\n            item->current_anim_state = M_STATE_CLIMB_1;\n            break;\n\n        case 3:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_CLIMB_2, 0);\n            item->current_anim_state = M_STATE_CLIMB_2;\n            break;\n\n        case 4:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_CLIMB_3, 0);\n            item->current_anim_state = M_STATE_CLIMB_3;\n            break;\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = 36;\n    obj->radius = 204;\n    obj->pivot_length = 0;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 1)->rot.z = true;\n    Object_GetBone(obj, 9)->rot.z = true;\n}\n\nREGISTER_OBJECT(O_LIZARD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/mercenary.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n\n// clang-format off\n#define M_HITPOINTS   30\n#define M_DAMAGE      28\n#define M_RADIUS      (WALL_L / 10) // = 102\n#define M_WALK_TURN   (DEG_1 * 5) // = 910\n#define M_RUN_TURN    (DEG_1 * 10) // = 1820\n#define M_RUN_RANGE   SQUARE(WALL_L * 2) // = 4194304\n#define M_SHOOT_RANGE SQUARE(WALL_L * 3) // = 9437184\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STOP         = 12,\n    M_ANIM_WALK_TO_STOP = 17,\n    M_ANIM_DEATH        = 19,\n    // clang-format on\n} M_ANIM;\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_WAIT,\n    M_STATE_SHOOT_1,\n    M_STATE_SHOOT_2,\n    M_STATE_DEATH,\n    M_STATE_AIM_1,\n    M_STATE_AIM_2,\n    M_STATE_AIM_3,\n    M_STATE_SHOOT_3\n} M_STATE;\n\nstatic const CREATURE_GUN m_MercenaryGun = {\n    .muzzle = { .pos = { 0, 300, 64 }, .mesh_num = 7 },\n    .tr3_enemy_flash = true,\n    .tr3_flash = { .pos = { 0, 300, 56 }, .mesh_num = 7 },\n    .tr3_enemy_weapon_flags = 1,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_90,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Creature_Initialise(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n    item->goal_anim_state = M_STATE_STOP;\n}\n\nstatic void M_CalculateTarget(ITEM *const item)\n{\n    CREATURE *const mercenary = item->creature_data;\n    const ITEM *const lara_item = Lara_GetItem();\n    if (mercenary->hurt_by_lara) {\n        mercenary->enemy = (ITEM *)lara_item;\n        return;\n    }\n\n    int32_t best_distance = INT32_MAX;\n    mercenary->enemy = nullptr;\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == mercenary) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        if (candidate == lara_item || candidate->object_id == item->object_id) {\n            continue;\n        }\n\n        const XYZ_32 delta = {\n            .x = candidate->pos.x - item->pos.x,\n            .y = 0,\n            .z = candidate->pos.z - item->pos.z,\n        };\n        const int32_t distance = XYZ_32_GetLength2(delta);\n        if (distance < best_distance) {\n            mercenary->enemy = (ITEM *)candidate;\n            best_distance = distance;\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->flags = (Random_GetControl() & 3) == 0 ? 1 : 0;\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        M_CalculateTarget(item);\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    int32_t enemy_dist;\n    int32_t enemy_angle;\n    const ITEM *const lara_item = Lara_GetItem();\n    if (creature->enemy == lara_item) {\n        enemy_dist = info.distance;\n        enemy_angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        enemy_angle = Math_Atan(dz, dx) - item->rot.y;\n        enemy_dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, creature->enemy != lara_item);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (item->hit_status) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n        head = enemy_angle;\n\n        if (Item_TestAnimEqual(item, M_ANIM_WALK_TO_STOP)) {\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n        }\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance >= M_SHOOT_RANGE\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (Random_GetControl() < 0x4000) {\n                item->goal_anim_state = M_STATE_AIM_1;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_3;\n            }\n        } else if (\n            (!creature->alerted && creature->mood == MOOD_BORED)\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal || enemy_dist > M_RUN_RANGE))) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood != MOOD_BORED && info.distance > M_RUN_RANGE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = enemy_angle;\n        creature->flags = 0;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (\n            (item->ai_bits & AI_GUARD) != 0\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal || enemy_dist > M_RUN_RANGE))) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance < M_SHOOT_RANGE\n                || info.zone_num != info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_2;\n            }\n        } else if (creature->mood == MOOD_BORED) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.distance > M_RUN_RANGE) {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal || enemy_dist > M_RUN_RANGE))) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            break;\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            || creature->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (\n            creature->mood == MOOD_STALK && (item->ai_bits & AI_FOLLOW) == 0\n            && info.distance < M_RUN_RANGE) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WAIT:\n        head = enemy_angle;\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_SHOOT_1;\n        } else if (creature->mood != MOOD_BORED || !info.ahead) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n    case M_STATE_SHOOT_2:\n    case M_STATE_SHOOT_3:\n        if (item->current_anim_state == M_STATE_SHOOT_3\n            && item->goal_anim_state != M_STATE_STOP\n            && (creature->mood == MOOD_ESCAPE || info.distance > M_SHOOT_RANGE\n                || !Creature_CanTargetEnemy(item, &info))) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (creature->flags != 0) {\n            creature->flags--;\n        } else if (creature->enemy != nullptr) {\n            Creature_Shoot(item, &info, &m_MercenaryGun, torso_y, M_DAMAGE);\n            creature->flags = 5;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n    case M_STATE_AIM_2:\n    case M_STATE_AIM_3:\n        creature->flags = 0;\n        if (!info.ahead) {\n            break;\n        }\n\n        torso_x = info.x_angle;\n        torso_y = info.angle;\n        if (Creature_CanTargetEnemy(item, &info)) {\n            if (item->current_anim_state == M_STATE_AIM_1) {\n                item->goal_anim_state = M_STATE_SHOOT_1;\n            } else if (item->current_anim_state == M_STATE_AIM_2) {\n                item->goal_anim_state = M_STATE_SHOOT_2;\n            } else {\n                item->goal_anim_state = M_STATE_SHOOT_3;\n            }\n        } else {\n            item->goal_anim_state = item->current_anim_state == M_STATE_AIM_2\n                ? M_STATE_WALK\n                : M_STATE_STOP;\n        }\n        break;\n\n    default:\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->pivot_length = 0;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.x = true;\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_STHPAC_MERCENARY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/monk.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define MONK_HITPOINTS         30\n#define MONK_RADIUS            (WALL_L / 10) // = 102\n#define MONK_BIFF_DAMAGE       150\n#define MONK_BIFF_ENEMY_DAMAGE 5\n#define MONK_WALK_TURN         (DEG_1 * 3) // = 546\n#define MONK_RUN_TURN          (DEG_1 * 4) // = 728\n#define MONK_RUN_TURN_FAST     (DEG_1 * 5) // = 910\n#define MONK_CLOSE_RANGE       SQUARE(WALL_L / 2) // = 262144\n#define MONK_LONG_RANGE        SQUARE(WALL_L) // = 1048576\n#define MONK_ATTACK_5_RANGE    SQUARE(WALL_L * 3) // = 9437184\n#define MONK_WALK_RANGE        SQUARE(WALL_L * 2) // = 4194304\n#define MONK_HIT_RANGE         (STEP_L * 2) // = 512\n#define MONK_TOUCH_BITS        0b01000000'00000000 // = 0x4000\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    MONK_STATE_EMPTY    = 0,\n    MONK_STATE_WAIT_1   = 1,\n    MONK_STATE_WALK     = 2,\n    MONK_STATE_RUN      = 3,\n    MONK_STATE_ATTACK_1 = 4,\n    MONK_STATE_ATTACK_2 = 5,\n    MONK_STATE_ATTACK_3 = 6,\n    MONK_STATE_ATTACK_4 = 7,\n    MONK_STATE_AIM_3    = 8,\n    MONK_STATE_DEATH    = 9,\n    MONK_STATE_ATTACK_5 = 10,\n    MONK_STATE_WAIT_2   = 11,\n    // clang-format on\n} MONK_STATE;\n\ntypedef enum {\n    MONK_ANIM_DEATH = 20,\n} MONK_ANIM;\n\nstatic const BITE m_MonkHit = {\n    .pos = { .x = -23, .y = 16, .z = 265 },\n    .mesh_num = 14,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != MONK_STATE_DEATH) {\n            Item_SwitchToAnim(\n                item, Random_GetControl() / 0x4000 + MONK_ANIM_DEATH, 0);\n            item->current_anim_state = MONK_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        const LARA_INFO *const lara = Lara_GetLaraInfo();\n        switch (item->current_anim_state) {\n        case MONK_STATE_WAIT_1:\n            creature->flags &= 0xFFF;\n            if (!Creature_IsHostile(item) && info.ahead\n                && lara->target == item) {\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = MONK_STATE_WALK;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = MONK_STATE_RUN;\n            } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) {\n                if (Random_GetControl() < 0x7000) {\n                    item->goal_anim_state = MONK_STATE_ATTACK_1;\n                } else {\n                    item->goal_anim_state = MONK_STATE_WAIT_2;\n                }\n            } else if (!info.ahead) {\n                item->goal_anim_state = MONK_STATE_RUN;\n            } else if (info.distance < MONK_LONG_RANGE) {\n                item->goal_anim_state = MONK_STATE_ATTACK_4;\n            } else if (info.distance < MONK_WALK_RANGE) {\n                item->goal_anim_state = MONK_STATE_WALK;\n            } else {\n                item->goal_anim_state = MONK_STATE_RUN;\n            }\n            break;\n\n        case MONK_STATE_WAIT_2:\n            creature->flags &= 0xFFF;\n            if (!Creature_IsHostile(item) && info.ahead\n                && lara->target == item) {\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = MONK_STATE_WALK;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = MONK_STATE_RUN;\n            } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) {\n                const int16_t random = Random_GetControl();\n                if (random < 0x3000) {\n                    item->goal_anim_state = MONK_STATE_ATTACK_2;\n                } else if (random < 0x6000) {\n                    item->goal_anim_state = MONK_STATE_AIM_3;\n                } else {\n                    item->goal_anim_state = MONK_STATE_WAIT_1;\n                }\n            } else if (info.ahead && info.distance < MONK_WALK_RANGE) {\n                item->goal_anim_state = MONK_STATE_WALK;\n            } else {\n                item->goal_anim_state = MONK_STATE_RUN;\n            }\n            break;\n\n        case MONK_STATE_WALK:\n            creature->maximum_turn = MONK_WALK_TURN;\n            if (creature->mood == MOOD_BORED) {\n                if (!Creature_IsHostile(item) && info.ahead\n                    && lara->target == item) {\n                    if (Random_GetControl() < 0x4000) {\n                        item->goal_anim_state = MONK_STATE_WAIT_1;\n                    } else {\n                        item->goal_anim_state = MONK_STATE_WAIT_2;\n                    }\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = MONK_STATE_RUN;\n            } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = MONK_STATE_WAIT_1;\n                } else {\n                    item->goal_anim_state = MONK_STATE_WAIT_2;\n                }\n            } else if (!info.ahead || info.distance > MONK_WALK_RANGE) {\n                item->goal_anim_state = MONK_STATE_RUN;\n            }\n            break;\n\n        case MONK_STATE_RUN:\n            creature->flags &= 0xFFF;\n            creature->maximum_turn = MONK_RUN_TURN;\n            if (Creature_IsHostile(item)) {\n                creature->maximum_turn = MONK_RUN_TURN_FAST;\n            }\n            tilt = angle / 4;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = MONK_STATE_WAIT_1;\n            } else if (creature->mood == MOOD_ESCAPE) {\n            } else if (info.ahead && info.distance < MONK_CLOSE_RANGE) {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = MONK_STATE_WAIT_1;\n                } else {\n                    item->goal_anim_state = MONK_STATE_WAIT_2;\n                }\n            } else if (info.ahead && info.distance < MONK_ATTACK_5_RANGE) {\n                item->goal_anim_state = MONK_STATE_ATTACK_5;\n            }\n            break;\n\n        case MONK_STATE_AIM_3:\n            if (!info.ahead || info.distance > MONK_CLOSE_RANGE) {\n                item->goal_anim_state = MONK_STATE_WAIT_2;\n            } else {\n                item->goal_anim_state = MONK_STATE_ATTACK_3;\n            }\n            break;\n\n        case MONK_STATE_ATTACK_1:\n        case MONK_STATE_ATTACK_2:\n        case MONK_STATE_ATTACK_3:\n        case MONK_STATE_ATTACK_4:\n        case MONK_STATE_ATTACK_5:\n            if (creature->enemy == Lara_GetItem()) {\n                if ((creature->flags & 0xF000) == 0\n                    && (item->touch_bits & MONK_TOUCH_BITS) != 0) {\n                    Lara_TakeDamage(MONK_BIFF_DAMAGE, true);\n                    Sound_Effect(SFX_MONK_CRUNCH, &item->pos, SPM_NORMAL);\n                    Creature_Effect(item, &m_MonkHit, Spawn_Blood);\n                    creature->flags |= 0x1000;\n                }\n            } else if (\n                (creature->flags & 0xF000) == 0 && creature->enemy != nullptr) {\n                const int32_t dx = ABS(creature->enemy->pos.x - item->pos.x);\n                const int32_t dy = ABS(creature->enemy->pos.y - item->pos.y);\n                const int32_t dz = ABS(creature->enemy->pos.z - item->pos.z);\n                if (dx < MONK_HIT_RANGE && dy < MONK_HIT_RANGE\n                    && dz < MONK_HIT_RANGE) {\n                    Item_TakeDamage(\n                        creature->enemy, MONK_BIFF_ENEMY_DAMAGE, true);\n                    Sound_Effect(SFX_MONK_CRUNCH, &item->pos, SPM_NORMAL);\n                    creature->flags |= 0x1000;\n                }\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = MONK_HITPOINTS;\n    obj->radius = MONK_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n}\n\nstatic void M_Setup1(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->pivot_length = 0;\n}\n\nstatic void M_Setup2(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n}\n\nstatic void M_Setup3(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->pivot_length = 0;\n    obj->shadow_size = 0;\n}\n\nREGISTER_OBJECT(O_MONK_1, M_Setup1)\nREGISTER_OBJECT(O_MONK_2, M_Setup2)\nREGISTER_OBJECT(O_MONK_3, M_Setup3)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/monkey.c",
    "content": "#include <trx/config.h>\n#include <trx/core/math/geom.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/draw.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/shell.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_DAMAGE_NORMAL 40\n#define M_DAMAGE_JUMP   50\n#define M_WALK_TURN     (7 * DEG_1)\n#define M_RUN_TURN      (11 * DEG_1)\n#define M_JUMP_RANGE    SQUARE(WALL_L * 2/3) // = 465124\n#define M_WALK_RANGE    SQUARE(WALL_L * 2/3) // = 465124\n#define M_ATTACK_RANGE  SQUARE(WALL_L / 3) // = 116281\n#define M_ROLL_RANGE    SQUARE(WALL_L) // = 1048576\n#define M_WAIT_CHANCE   256\n#define M_F_PICKUP      12\n// clang-format on\n\nstatic BITE m_MonkeyBite = {\n    .pos = { 10, 10, 11 },\n    .mesh_num = 13,\n};\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_STAND,\n    M_STATE_RUN,\n    M_STATE_PICKUP,\n    M_STATE_SIT,\n    M_STATE_EAT,\n    M_STATE_SCRATCH,\n    M_STATE_ROLL,\n    M_STATE_ANGRY,\n    M_STATE_DEATH,\n    M_STATE_ATTACK_LOW,\n    M_STATE_ATTACK_HIGH,\n    M_STATE_ATTACK_JUMP,\n    M_STATE_CLIMB_4,\n    M_STATE_CLIMB_3,\n    M_STATE_CLIMB_2,\n    M_STATE_DOWN_4,\n    M_STATE_DOWN_3,\n    M_STATE_DOWN_2\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_SIT = 2,\n    M_ANIM_DEATH = 14,\n    M_ANIM_CLIMB_2 = 19,\n    M_ANIM_CLIMB_3 = 18,\n    M_ANIM_CLIMB_4 = 17,\n    M_ANIM_DOWN_2 = 22,\n    M_ANIM_DOWN_3 = 21,\n    M_ANIM_DOWN_4 = 20,\n} M_ANIM;\n\nstatic void M_Bite(ITEM *const item, ITEM *const enemy, const int32_t dmg)\n{\n    CREATURE *const creature = item->creature_data;\n    if (enemy == Lara_GetItem()) {\n        if (creature->flags == 0 && item->touch_bits & 0x2400) {\n            Lara_TakeDamage(dmg, true);\n            creature->flags = 1;\n            Creature_Effect(item, &m_MonkeyBite, Spawn_Blood);\n        }\n    } else if (creature->flags == 0 && enemy != nullptr) {\n        if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n            && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n            && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n            Item_TakeDamage(enemy, dmg / 2, true);\n            creature->flags = 1;\n            Creature_Effect(item, &m_MonkeyBite, Spawn_Blood);\n        }\n    }\n}\n\nstatic bool M_CarryPickup(\n    ITEM *const item, CREATURE *const creature, const int16_t item_num)\n{\n    if (creature->enemy == nullptr) {\n        return false;\n    }\n\n    if (creature->enemy->object_id != O_SMALL_MEDIPACK_ITEM\n        && creature->enemy->object_id != O_KEY_ITEM_4) {\n        return false;\n    }\n\n    if (!Item_TestFrameEqual(item, M_F_PICKUP)) {\n        return false;\n    }\n\n    if (creature->enemy->room_num == NO_ROOM\n        || creature->enemy->status == IS_INVISIBLE\n        || creature->enemy->clear_body) {\n        creature->enemy = nullptr;\n        return true;\n    }\n\n    const int16_t pickup_num = Item_GetIndex(creature->enemy);\n    if (item->carried_item == nullptr) {\n        item->carried_item = GameBuf_Alloc(sizeof(CARRIED_ITEM), GBUF_ITEMS);\n        item->carried_item->next_item = nullptr;\n    }\n    item->carried_item->object_id = creature->enemy->object_id;\n    item->carried_item->spawn_num = pickup_num;\n    item->carried_item->pos = creature->enemy->pos;\n    item->carried_item->rot = creature->enemy->rot;\n    item->carried_item->room_num = NO_ROOM;\n    item->carried_item->fall_speed = 0;\n    item->carried_item->status = DS_CARRIED;\n    Item_UpdateRoom(pickup_num, NO_ROOM);\n    creature->enemy->carried_item = nullptr;\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        CREATURE *const slot = LOT_GetBaddieSlot(i);\n        if (slot->item_num != NO_ITEM && slot->item_num != item_num\n            && slot->enemy == creature->enemy) {\n            slot->enemy = nullptr;\n        }\n    }\n    creature->enemy = nullptr;\n\n    if (item->ai_bits != AI_MODIFY) {\n        item->ai_bits |= AI_AMBUSH | AI_MODIFY;\n    }\n\n    return true;\n}\n\nstatic bool M_DropPickup(ITEM *const item, CREATURE *const creature)\n{\n    if (creature->enemy == nullptr) {\n        return false;\n    }\n\n    if (creature->enemy->object_id != O_AI_AMBUSH) {\n        return false;\n    }\n\n    if (!Item_TestFrameEqual(item, M_F_PICKUP)) {\n        return false;\n    }\n\n    item->ai_bits = 0;\n    ITEM *const pickup = Item_Get(item->carried_item->spawn_num);\n    pickup->pos = item->pos;\n    Item_UpdateRoom(item->carried_item->spawn_num, item->room_num);\n    pickup->ai_bits = AI_GUARD;\n    item->carried_item = nullptr;\n    creature->enemy = nullptr;\n    return true;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Creature_Initialise(item_num);\n    Item_SwitchToAnim(item, M_ANIM_SIT, 0);\n    item->current_anim_state = M_STATE_SIT;\n    item->goal_anim_state = M_STATE_SIT;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    int16_t angle = 0;\n    int16_t tilt = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            item->mesh_bits = -1;\n        }\n    } else {\n        Creature_GetAITarget(creature);\n        if (creature->hurt_by_lara\n            && g_Config.gameplay.fix_monkey_pickup_priority) {\n            creature->enemy = lara_item;\n        }\n\n        if (item->ai_bits == AI_MODIFY) {\n            if (item->carried_item == nullptr) {\n                item->mesh_bits = 0xFFFF6F6F;\n            } else {\n                item->mesh_bits = 0xFFFF6E6F;\n            }\n        } else if (item->carried_item == nullptr) {\n            item->mesh_bits = -1;\n        } else {\n            item->mesh_bits = 0xFFFFFEFF;\n        }\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        int32_t dist;\n        if (creature->enemy == lara_item) {\n            dist = info.distance;\n        } else {\n            const int32_t dx = lara_item->pos.x - item->pos.x;\n            const int32_t dz = lara_item->pos.z - item->pos.z;\n            Math_Atan(dz, dx);\n            dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n        }\n\n        Creature_UpdateMood(item, &info, true);\n        if (Lara_Vehicle_GetIndex() != NO_ITEM) {\n            creature->mood = MOOD_ESCAPE;\n        }\n\n        Creature_ApplyMood(item, &info, true);\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        if (item->hit_status) {\n            ITEM *const enemy = creature->enemy;\n            creature->enemy = lara_item;\n            Creature_AlertAllGuards(item_num);\n            creature->enemy = enemy;\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_WALK:\n            creature->maximum_turn = M_WALK_TURN;\n\n            if (item->ai_bits & AI_PATROL_1) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < M_WAIT_CHANCE) {\n                    item->goal_anim_state = M_STATE_SIT;\n                }\n            } else if (info.bite && info.distance < M_JUMP_RANGE) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else {\n                item->goal_anim_state = M_STATE_STAND;\n            }\n\n            break;\n\n        case M_STATE_STAND:\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n\n            if (item->ai_bits & AI_GUARD) {\n                Creature_AIGuard(creature);\n                if (!(Random_GetControl() & 0xF)) {\n                    if (Random_GetControl() & 1) {\n                        item->goal_anim_state = M_STATE_ANGRY;\n                    } else {\n                        item->goal_anim_state = M_STATE_SIT;\n                    }\n                }\n            } else if (item->ai_bits & AI_PATROL_1) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                if (lara->target != item && info.ahead) {\n                    item->goal_anim_state = M_STATE_STAND;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (item->required_anim_state != M_STATE_EMPTY) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (!(Random_GetControl() & 0xF)) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else if (!(Random_GetControl() & 0xF)) {\n                    if (Random_GetControl() & 1) {\n                        item->goal_anim_state = M_STATE_ANGRY;\n                    } else {\n                        item->goal_anim_state = M_STATE_SIT;\n                    }\n                }\n            } else if (\n                item->ai_bits & AI_FOLLOW\n                && (creature->reached_goal || dist > SQUARE(WALL_L * 2))) {\n                if (item->required_anim_state != M_STATE_EMPTY) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (info.ahead) {\n                    item->goal_anim_state = M_STATE_SIT;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (info.bite && info.distance < M_ATTACK_RANGE) {\n                if (lara_item->pos.y < item->pos.y) {\n                    item->goal_anim_state = M_STATE_ATTACK_HIGH;\n                } else {\n                    item->goal_anim_state = M_STATE_ATTACK_LOW;\n                }\n            } else if (info.bite && info.distance < M_JUMP_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_JUMP;\n            } else if (info.bite && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                info.distance < M_WALK_RANGE && creature->enemy != lara_item\n                && creature->enemy != nullptr\n                && creature->enemy->object_id != O_AI_PATROL_1\n                && creature->enemy->object_id != O_AI_PATROL_2\n                && ABS(item->pos.y - creature->enemy->pos.y) < STEP_L) {\n                item->goal_anim_state = M_STATE_PICKUP;\n            } else if (info.bite && info.distance < M_ROLL_RANGE) {\n                item->goal_anim_state = M_STATE_ROLL;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_RUN:\n            creature->maximum_turn = M_RUN_TURN;\n            tilt = angle / 2;\n\n            if (item->ai_bits & AI_GUARD) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                if (lara->target != item && info.ahead) {\n                    item->goal_anim_state = M_STATE_STAND;\n                }\n            } else if (\n                item->ai_bits & AI_FOLLOW\n                && (creature->reached_goal || dist > SQUARE(WALL_L * 2))) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_ROLL;\n            } else if (info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (info.bite && info.distance < M_ROLL_RANGE) {\n                item->goal_anim_state = M_STATE_ROLL;\n            }\n\n            break;\n\n        case M_STATE_PICKUP:\n            creature->reached_goal = true;\n            if (creature->enemy == nullptr) {\n                break;\n            }\n\n            if (M_CarryPickup(item, creature, item_num)) {\n                break;\n            } else if (M_DropPickup(item, creature)) {\n                break;\n            } else {\n                creature->maximum_turn = 0;\n\n                if (ABS(info.angle) < M_WALK_TURN) {\n                    item->rot.y += info.angle;\n                } else if (info.angle < 0) {\n                    item->rot.y -= M_WALK_TURN;\n                } else {\n                    item->rot.y += M_WALK_TURN;\n                }\n            }\n            break;\n\n        case M_STATE_SIT:\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n\n            if (item->ai_bits & AI_GUARD) {\n                Creature_AIGuard(creature);\n\n                if (!(Random_GetControl() & 0xF)) {\n                    if (Random_GetControl() & 1) {\n                        item->goal_anim_state = M_STATE_SCRATCH;\n                    } else {\n                        item->goal_anim_state = M_STATE_EAT;\n                    }\n                }\n            } else if (item->ai_bits & AI_PATROL_1) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (creature->mood == MOOD_BORED) {\n                if (item->required_anim_state != M_STATE_EMPTY) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (!(Random_GetControl() & 0xF)) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else if (!(Random_GetControl() & 0xF)) {\n                    if (Random_GetControl() & 1) {\n                        item->goal_anim_state = M_STATE_SCRATCH;\n                    } else {\n                        item->goal_anim_state = M_STATE_EAT;\n                    }\n                }\n            } else if (\n                item->ai_bits & AI_FOLLOW\n                && (creature->reached_goal || dist > SQUARE(WALL_L * 2))) {\n                if (item->required_anim_state != M_STATE_EMPTY) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (info.ahead) {\n                    item->goal_anim_state = M_STATE_SIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STAND;\n                }\n            } else if (info.bite && info.distance < M_JUMP_RANGE) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (info.bite && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_STAND;\n            }\n            break;\n\n        case M_STATE_ATTACK_LOW:\n            if (info.ahead) {\n                torso_y = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (ABS(info.angle) < M_WALK_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_WALK_TURN;\n            } else {\n                item->rot.y += M_WALK_TURN;\n            }\n            M_Bite(item, creature->enemy, M_DAMAGE_NORMAL);\n            break;\n\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wimplicit-fallthrough\"\n        case M_STATE_ATTACK_HIGH:\n            if (info.ahead) {\n                torso_y = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (ABS(info.angle) < M_WALK_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_WALK_TURN;\n            } else {\n                item->rot.y += M_WALK_TURN;\n            }\n            M_Bite(item, creature->enemy, M_DAMAGE_NORMAL);\n\n            // OG mistake\n            // break;\n\n        case M_STATE_ATTACK_JUMP:\n            if (info.ahead) {\n                torso_y = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (ABS(info.angle) < M_WALK_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_WALK_TURN;\n            } else {\n                item->rot.y += M_WALK_TURN;\n            }\n            M_Bite(item, creature->enemy, M_DAMAGE_JUMP);\n            break;\n#pragma GCC diagnostic pop\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n\n    if (item->current_anim_state >= M_STATE_CLIMB_4) {\n        creature->maximum_turn = 0;\n        Creature_Animate(item_num, angle, 0);\n    } else {\n        switch (Creature_Vault(item_num, angle, 2, 128)) {\n        case -4:\n            creature->maximum_turn = 0;\n            Item_SwitchToObjAnim(item, M_ANIM_DOWN_4, 0, O_MONKEY);\n            item->current_anim_state = M_STATE_DOWN_4;\n            break;\n\n        case -3:\n            creature->maximum_turn = 0;\n            Item_SwitchToObjAnim(item, M_ANIM_DOWN_3, 0, O_MONKEY);\n            item->current_anim_state = M_STATE_DOWN_3;\n            break;\n\n        case -2:\n            creature->maximum_turn = 0;\n            Item_SwitchToObjAnim(item, M_ANIM_DOWN_2, 0, O_MONKEY);\n            item->current_anim_state = M_STATE_DOWN_2;\n            break;\n\n        case 2:\n            creature->maximum_turn = 0;\n            Item_SwitchToObjAnim(item, M_ANIM_CLIMB_2, 0, O_MONKEY);\n            item->current_anim_state = M_STATE_CLIMB_2;\n            break;\n\n        case 3:\n            creature->maximum_turn = 0;\n            Item_SwitchToObjAnim(item, M_ANIM_CLIMB_3, 0, O_MONKEY);\n            item->current_anim_state = M_STATE_CLIMB_3;\n            break;\n\n        case 4:\n            creature->maximum_turn = 0;\n            Item_SwitchToObjAnim(item, M_ANIM_CLIMB_4, 0, O_MONKEY);\n            item->current_anim_state = M_STATE_CLIMB_4;\n            break;\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    const OBJECT *swap;\n    if (item->ai_bits == AI_MODIFY) {\n        swap = Object_Get(O_MESH_SWAP_3);\n    } else {\n        swap = Object_Get(O_MESH_SWAP_2);\n    }\n    return Object_DrawAnimatingItemWithSwap(item, swap);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    if (!Object_Get(O_MESH_SWAP_2)->loaded) {\n        Shell_ExitSystem(\"Monkey requires O_MESH_SWAP_2 (pickups)\");\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->draw_func = M_Draw;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = 8;\n    obj->radius = 102;\n    obj->pivot_length = 0;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.z = true;\n    Object_GetBone(obj, 7)->rot.x = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_MONKEY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/mouse.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_HITPOINTS     4\n#define M_TOUCH_BITS    0b01111111 // = 0x7F\n#define M_RADIUS        (WALL_L / 10) // = 102\n#define M_RUN_TURN      (DEG_1 * 6) // = 1092\n#define M_ATTACK_RANGE  SQUARE(WALL_L / 3) // = 116281\n#define M_BITE_DAMAGE   20\n#define M_WAIT_1_CHANCE 0x500 // = 1280\n#define M_WAIT_2_CHANCE (M_WAIT_1_CHANCE + 0x500) // = 2560\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_RUN,\n    M_STATE_STOP,\n    M_STATE_WAIT_1,\n    M_STATE_WAIT_2,\n    M_STATE_ATTACK,\n    M_STATE_DEATH,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 9,\n} M_ANIM;\n\nstatic const BITE m_MouseBite = {\n    .pos = { .x = 0, .y = 0, .z = 57 },\n    .mesh_num = 2,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n        angle = Creature_Turn(item, M_RUN_TURN);\n\n        switch (item->current_anim_state) {\n        case M_STATE_RUN:\n            creature->maximum_turn = M_RUN_TURN;\n            if (creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) {\n                const int32_t random = Random_GetControl();\n                if (random < M_WAIT_1_CHANCE) {\n                    item->required_anim_state = M_STATE_WAIT_1;\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (random < M_WAIT_2_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else if (info.ahead && info.distance < M_ATTACK_RANGE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_STOP:\n            creature->maximum_turn = 0;\n            if (item->required_anim_state != M_STATE_NULL) {\n                item->goal_anim_state = item->required_anim_state;\n            }\n            break;\n\n        case M_STATE_WAIT_1:\n            if (Random_GetControl() < M_WAIT_1_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_WAIT_2:\n            if (creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) {\n                const int32_t random = Random_GetControl();\n                if (random < M_WAIT_1_CHANCE) {\n                    item->required_anim_state = M_STATE_WAIT_1;\n                } else if (random > M_WAIT_2_CHANCE) {\n                    item->required_anim_state = M_STATE_RUN;\n                }\n            } else if (info.distance < M_ATTACK_RANGE) {\n                item->required_anim_state = M_STATE_ATTACK;\n            } else {\n                item->required_anim_state = M_STATE_RUN;\n            }\n\n            if (item->required_anim_state != M_STATE_NULL) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_ATTACK:\n            if (item->required_anim_state == M_STATE_NULL\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_MouseBite, Spawn_Blood);\n                item->required_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 50;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 3)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_MOUSE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/mp_1.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS          (WALL_L / 5)           // = 204\n#define M_HIT_POINTS      25\n#define M_PUNCH_1_DAMAGE  80\n#define M_PUNCH_3_DAMAGE  100\n#define M_KICK_DAMAGE     150\n#define M_HIT_TOUCH_BITS  0b00100100'00000000\n#define M_KICK_TOUCH_BITS 0b00000000'01100000\n#define M_ALERT_DIST      SQUARE(WALL_L)         // = 1048576\n#define M_ALERT_HEIGHT    WALL_L                 // = 1024\n#define M_RUN_DIST        SQUARE(WALL_L * 2)     // = 4194304\n#define M_WALK_DIST       SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_1   SQUARE(WALL_L / 2)     // = 262144\n#define M_ATTACK_DIST_2   SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_3   SQUARE(WALL_L * 5 / 4) // = 1638400\n#define M_KICK_DIST       SQUARE(WALL_L * 3 / 2) // = 2359296\n#define M_WALK_TURN       (DEG_1 * 6)            // = 1092\n#define M_RUN_TURN        (DEG_1 * 7)            // = 1274\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_PUNCH_3,\n    M_STATE_AIM_3,\n    M_STATE_WAIT,\n    M_STATE_AIM_2,\n    M_STATE_AIM_1,\n    M_STATE_PUNCH_2,\n    M_STATE_PUNCH_1,\n    M_STATE_RUN,\n    M_STATE_DEATH,\n    M_STATE_KICK,\n    M_STATE_UP_4,\n    M_STATE_UP_2,\n    M_STATE_UP_3,\n    M_STATE_DOWN_4,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STOP   = 6,\n    M_ANIM_DEATH  = 26,\n    M_ANIM_UP_4   = 27,\n    M_ANIM_UP_2   = 28,\n    M_ANIM_UP_3   = 29,\n    M_ANIM_DOWN_4 = 30,\n    // clang-format on\n} M_ANIM;\n\nstatic const BITE m_HitBite = {\n    .pos = { 247, 10, 11 },\n    .mesh_num = 13,\n};\nstatic const BITE m_KickBite = {\n    .pos = { 0, 0, 100 },\n    .mesh_num = 6,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n    item->goal_anim_state = M_STATE_STOP;\n}\n\nstatic void M_CalculateEnemy(ITEM *const item)\n{\n    CREATURE *const mp = item->creature_data;\n    const ITEM *const lara_item = Lara_GetItem();\n    mp->enemy = (ITEM *)lara_item;\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    int32_t best_distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == mp) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        if (candidate != lara_item && candidate->object_id != O_PRISONER) {\n            continue;\n        }\n\n        const XYZ_32 delta = {\n            .x = candidate->pos.x - item->pos.x,\n            .y = 0,\n            .z = candidate->pos.z - item->pos.z,\n        };\n        if (ABS(delta.x) > 0x7D00 || ABS(delta.z) > 0x7D00) {\n            continue;\n        }\n\n        const int32_t distance = XYZ_32_GetLength2(delta);\n        if (distance < best_distance) {\n            mp->enemy = (ITEM *)candidate;\n            best_distance = distance;\n        }\n    }\n}\n\nstatic bool M_Vault(ITEM *const item, const int16_t angle)\n{\n    const int32_t vault_result =\n        Creature_Vault(Item_GetIndex(item), angle, 2, 260);\n    switch (vault_result) {\n    case -4:\n        Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0);\n        item->current_anim_state = M_STATE_DOWN_4;\n        return true;\n    case 2:\n        Item_SwitchToAnim(item, M_ANIM_UP_2, 0);\n        item->current_anim_state = M_STATE_UP_2;\n        return true;\n    case 3:\n        Item_SwitchToAnim(item, M_ANIM_UP_3, 0);\n        item->current_anim_state = M_STATE_UP_3;\n        return true;\n    case 4:\n        Item_SwitchToAnim(item, M_ANIM_UP_4, 0);\n        item->current_anim_state = M_STATE_UP_4;\n        return true;\n    default:\n        return false;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    Creature_TestBoxDamage(item_num);\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->lot.setup.step = STEP_L;\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        M_CalculateEnemy(item);\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    ITEM *const lara_item = Lara_GetItem();\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if (item->hit_status\n        || ((lara_info.distance < M_ALERT_DIST\n             || Creature_CanSeeEnemy(item, &lara_info))\n            && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT)) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n    case M_STATE_WAIT:\n        if (item->current_anim_state == M_STATE_WAIT\n            && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) {\n            item->goal_anim_state = M_STATE_STOP;\n            break;\n        }\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n        head = lara_info.angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead && !item->hit_status) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            if (item->required_anim_state != M_STATE_NULL) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_AIM_1;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_AIM_2;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        creature->maximum_turn = M_WALK_TURN;\n        head = lara_info.angle;\n        creature->flags = 0;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() < 256) {\n                item->required_anim_state = M_STATE_WAIT;\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            info.bite && info.distance < M_KICK_DIST && info.x_angle < 0) {\n            item->goal_anim_state = M_STATE_KICK;\n        } else if (info.bite) {\n            if (info.distance < M_ATTACK_DIST_1) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.distance < M_ATTACK_DIST_3) {\n                item->goal_anim_state = M_STATE_AIM_3;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            (item->ai_bits & AI_FOLLOW) != 0\n            && (creature->reached_goal || lara_info.distance > M_RUN_DIST)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (info.ahead && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n\n        if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_PUNCH_1;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n\n        if (info.ahead && info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_PUNCH_2;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_3:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n\n        if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_PUNCH_1:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (enemy == lara_item) {\n            if (creature->flags == 0\n                && (item->touch_bits & M_HIT_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_PUNCH_1_DAMAGE, true);\n                Creature_Effect(item, &m_HitBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        } else if (creature->flags == 0 && enemy != nullptr) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 16, true);\n                Creature_Effect(item, &m_HitBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        }\n        break;\n\n    case M_STATE_PUNCH_2:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (enemy == lara_item) {\n            if (creature->flags == 0\n                && (item->touch_bits & M_HIT_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_PUNCH_1_DAMAGE, true);\n                Creature_Effect(item, &m_HitBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        } else if (creature->flags == 0 && enemy != nullptr) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 16, true);\n                Creature_Effect(item, &m_HitBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        }\n\n        if (info.ahead && info.distance > M_ATTACK_DIST_2\n            && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        }\n        break;\n\n    case M_STATE_PUNCH_3:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (enemy == lara_item) {\n            if (creature->flags != 2\n                && (item->touch_bits & M_HIT_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_PUNCH_3_DAMAGE, true);\n                Creature_Effect(item, &m_HitBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 2;\n            }\n        } else if (creature->flags != 2 && enemy != nullptr) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_PUNCH_3_DAMAGE / 16, true);\n                Creature_Effect(item, &m_HitBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 2;\n            }\n        }\n        break;\n\n    case M_STATE_KICK:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        const int16_t frame_num = Item_GetRelativeFrame(item);\n        if (enemy == lara_item) {\n            if (creature->flags != 1\n                && (item->touch_bits & M_KICK_TOUCH_BITS) != 0\n                && frame_num > 8) {\n                Lara_TakeDamage(M_KICK_DAMAGE, true);\n                Creature_Effect(item, &m_KickBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        } else if (creature->flags != 0 && enemy != nullptr && frame_num > 8) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_KICK_DAMAGE / 16, true);\n                Creature_Effect(item, &m_KickBite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n\n    if (item->current_anim_state >= M_STATE_DEATH) {\n        creature->maximum_turn = 0;\n        Creature_Animate(item_num, angle, 0);\n    } else if (M_Vault(item, angle)) {\n        creature->maximum_turn = 0;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_MP_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/mp_2.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS          (WALL_L / 10)          // = 102\n#define M_HIT_POINTS      28\n#define M_DAMAGE          32\n#define M_ALERT_DIST      SQUARE(WALL_L)         // = 1048576\n#define M_WALK_DIST       SQUARE(WALL_L * 3 / 2) // = 2359296\n#define M_RUN_DIST        SQUARE(WALL_L * 2)     // = 4194304\n#define M_WALK_TURN       (DEG_1 * 6)            // = 1092\n#define M_RUN_TURN        (DEG_1 * 10)           // = 1820\n#define M_DUCK_TURN       (DEG_1 * 1)            // = 182\n#define M_SHOOT_1_CHANCE  0x2000\n#define M_SHOOT_2_CHANCE  0x4000\n#define M_DUCK_CHANCE     0x3\n#define M_DUCK_END_CHANCE 0x1F\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_WAIT,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_AIM_1,\n    M_STATE_SHOOT_1,\n    M_STATE_AIM_2,\n    M_STATE_SHOOT_2,\n    M_STATE_SHOOT_3A,\n    M_STATE_SHOOT_3B,\n    M_STATE_SHOOT_4A,\n    M_STATE_AIM_3,\n    M_STATE_AIM_4,\n    M_STATE_DEATH,\n    M_STATE_SHOOT_4B,\n    M_STATE_DUCK_START,\n    M_STATE_DUCKED,\n    M_STATE_DUCK_AIM,\n    M_STATE_DUCK_SHOOT,\n    M_STATE_DUCK_WALK,\n    M_STATE_DUCK_END,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_SHOOT_1    = 1,\n    M_ANIM_AIM_1      = 12,\n    M_ANIM_DEATH      = 14,\n    M_ANIM_WALK_STOP  = 17,\n    M_ANIM_AIM_4A     = 18,\n    M_ANIM_AIM_4B     = 19,\n    M_ANIM_RUN_STOP_1 = 27,\n    M_ANIM_RUN_STOP_2 = 28,\n    // clang-format on\n} M_ANIM;\n\nstatic const CREATURE_GUN m_Gun = {\n    .muzzle = { .pos = { 0, 160, 40 }, .mesh_num = 13 },\n    .tr3_enemy_flash = true,\n    .tr3_flash = { .pos = { 0, 192, 40 }, .mesh_num = 13 },\n    .tr3_enemy_weapon_flags = 0,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_90,\n};\n\nstatic void M_FireFinalShot(\n    ITEM *const item, int16_t *const head, int16_t *const torso_y)\n{\n    if (!Item_TestFrameEqual(item, 1)) {\n        return;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n    if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) {\n        return;\n    }\n\n    *head = info.angle;\n    *torso_y = info.angle;\n    Creature_Shoot(item, &info, &m_Gun, info.angle, M_DAMAGE);\n    Sound_Effect(SFX_LONDON_SWAT_FIRE, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_CalculateEnemy(ITEM *const item)\n{\n    CREATURE *const mp = item->creature_data;\n    const ITEM *const lara_item = Lara_GetItem();\n    mp->enemy = (ITEM *)lara_item;\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    int32_t best_distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == mp) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        if (candidate != lara_item && candidate->object_id != O_PRISONER) {\n            continue;\n        }\n\n        const XYZ_32 delta = {\n            .x = candidate->pos.x - item->pos.x,\n            .y = 0,\n            .z = candidate->pos.z - item->pos.z,\n        };\n        const int32_t distance = XYZ_32_GetLength2(delta);\n        if (distance < best_distance) {\n            mp->enemy = (ITEM *)candidate;\n            best_distance = distance;\n        }\n    }\n}\n\nstatic bool M_IsNearCover(const ITEM *const item, const AI_INFO *const info)\n{\n    const XYZ_32 pos =\n        XYZ_32_OffsetYaw(item->pos, item->rot.y + info->angle, WALL_L);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    return item->pos.y > height + STEPUP_HEIGHT\n        && item->pos.y < height + STEPUP_HEIGHT * 3\n        && info->distance > M_ALERT_DIST;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    Creature_TestBoxDamage(item_num);\n\n    if (item->hit_points <= 0) {\n        item->hit_points = 0;\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        } else if ((Random_GetControl() & 3) == 0) {\n            M_FireFinalShot(item, &head, &torso_y);\n        }\n\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        M_CalculateEnemy(item);\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    ITEM *const lara_item = Lara_GetItem();\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, creature->enemy != lara_item);\n    angle = Creature_Turn(item, creature->maximum_turn);\n    const bool near_cover = M_IsNearCover(item, &lara_info);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if (item->hit_status || lara_info.distance < M_ALERT_DIST\n        || Creature_CanSeeEnemy(item, &lara_info)) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    const int16_t anim_idx = Item_GetRelativeAnim(item);\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    switch (item->current_anim_state) {\n    case M_STATE_WAIT:\n        head = lara_info.angle;\n        creature->maximum_turn = 0;\n\n        if (anim_idx == M_ANIM_WALK_STOP || anim_idx == M_ANIM_RUN_STOP_1\n            || anim_idx == M_ANIM_RUN_STOP_2) {\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n        }\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->goal_anim_state = M_STATE_DUCK_START;\n        } else if (item->required_anim_state == M_STATE_DUCK_START) {\n            item->goal_anim_state = M_STATE_DUCK_START;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            const int32_t rnd = Random_GetControl();\n            if (rnd < M_SHOOT_1_CHANCE) {\n                item->goal_anim_state = M_STATE_SHOOT_1;\n            } else if (rnd < M_SHOOT_2_CHANCE) {\n                item->goal_anim_state = M_STATE_SHOOT_2;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_3;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance > M_WALK_DIST\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_AIM_4;\n            } else {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (creature->mood != MOOD_BORED) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (info.ahead) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood != MOOD_ESCAPE) {\n            if (Creature_CanTargetEnemy(item, &info)\n                || ((item->ai_bits & AI_FOLLOW) != 0\n                    && (creature->reached_goal\n                        || lara_info.distance > M_RUN_DIST))) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if (anim_idx == M_ANIM_AIM_1\n            || (anim_idx == M_ANIM_SHOOT_1 && frame_idx == 10)) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->required_anim_state = M_STATE_WAIT;\n            }\n        } else if (\n            item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0\n            && near_cover) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if (item->required_anim_state == M_STATE_WAIT) {\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_2:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if (frame_idx == 0) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (\n            item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0\n            && near_cover) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_3A:\n    case M_STATE_SHOOT_3B:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if (frame_idx == 0 || frame_idx == 11) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (\n            item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0\n            && near_cover) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_AIM_4:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if ((anim_idx == M_ANIM_AIM_4A && frame_idx == 17)\n            || (anim_idx == M_ANIM_AIM_4B && frame_idx == 6)) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->required_anim_state = M_STATE_WALK;\n            }\n        } else if (\n            item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0\n            && near_cover) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n\n        if (info.distance < M_WALK_DIST) {\n            item->required_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_SHOOT_4A:\n    case M_STATE_SHOOT_4B:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if (item->required_anim_state == M_STATE_WALK) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        if (frame_idx == 16\n            && !Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        if (info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_DUCKED:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = 0;\n\n        if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_DUCK_AIM;\n        } else if (\n            item->hit_status || !near_cover\n            || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) {\n            item->goal_anim_state = M_STATE_DUCK_END;\n        } else {\n            item->goal_anim_state = M_STATE_DUCK_WALK;\n        }\n        break;\n\n    case M_STATE_DUCK_AIM:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_DUCK_TURN;\n\n        if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_DUCK_SHOOT;\n        } else {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    case M_STATE_DUCK_SHOOT:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n\n        if (frame_idx == 0\n            && (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)\n                || (Random_GetControl() & 7) == 0)) {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    case M_STATE_DUCK_WALK:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (Creature_CanTargetEnemy(item, &info) || item->hit_status\n            || !near_cover\n            || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_MP_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/mummy.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n\n#define MUMMY_HITPOINTS 18\n\ntypedef enum {\n    MUMMY_STATE_EMPTY = 0,\n    MUMMY_STATE_STOP = 1,\n    MUMMY_STATE_DEATH = 2,\n} MUMMY_STATE;\n\nstatic bool M_CanDropItems(const ITEM *const item)\n{\n    return item->hit_points <= 0 || item->status == IS_DEACTIVATED;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->touch_bits = 0;\n    item->mesh_bits = 0xFFFF87FF;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    int16_t head = 0;\n\n    if (Item_IsTriggerActive(item)) {\n        if (!LOT_EnableBaddieAI(item_num, true)) {\n            return;\n        }\n        item->status = IS_ACTIVE;\n    }\n\n    if (item->current_anim_state == MUMMY_STATE_STOP) {\n        const ITEM *const lara_item = Lara_GetItem();\n        head =\n            Math_Atan(\n                lara_item->pos.z - item->pos.z, lara_item->pos.x - item->pos.x)\n            - item->rot.y;\n        CLAMP(head, -FRONT_ARC, FRONT_ARC);\n\n        if (item->hit_points <= 0 || item->touch_bits) {\n            item->goal_anim_state = MUMMY_STATE_DEATH;\n        }\n    }\n\n    Creature_Head(item, head);\n    Item_Animate(item);\n\n    if (item->status == IS_DEACTIVATED) {\n        // Count kill if Lara touches mummy and it falls.\n        if (item->hit_points > 0) {\n            Stats_AddKill();\n        }\n        Item_RemoveActive(item_num);\n        Carrier_TestItemDrops(item_num);\n        item->hit_points = 0;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->can_drop_items_func = M_CanDropItems;\n\n    obj->hit_points = MUMMY_HITPOINTS;\n\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 2)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_MUMMY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/natla.c",
    "content": "#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define NATLA_FLY_MODE 0x8000\n#define NATLA_TIMER 0x7FFF\n#define NATLA_FIRE_ARC (DEG_1 * 30) // = 5460\n#define NATLA_FLY_TURN (DEG_1 * 5) // = 910\n#define NATLA_RUN_TURN (DEG_1 * 6) // = 1092\n#define NATLA_LAND_CHANCE 256\n#define NATLA_DIE_TIME (LOGIC_FPS * 16) // = 480\n#define NATLA_HITPOINTS 400\n#define NATLA_RADIUS (WALL_L / 5) // = 204\n#define NATLA_SMARTNESS 0x7FFF\n\ntypedef enum {\n    NATLA_STATE_EMPTY = 0,\n    NATLA_STATE_STOP = 1,\n    NATLA_STATE_FLY = 2,\n    NATLA_STATE_RUN = 3,\n    NATLA_STATE_AIM = 4,\n    NATLA_STATE_SEMIDEATH = 5,\n    NATLA_STATE_SHOOT = 6,\n    NATLA_STATE_FALL = 7,\n    NATLA_STATE_STAND = 8,\n    NATLA_STATE_DEATH = 9,\n} NATLA_STATE;\n\nstatic BITE m_NatlaGun = { .pos = { 5, 220, 7 }, .mesh_num = 4 };\n\nstatic int32_t M_GetStage2HitPoints(const ITEM *const item)\n{\n    return item->max_hit_points / 2;\n}\n\nstatic bool M_GunHit(\n    ITEM *const item, const GAME_VECTOR *const start,\n    const GAME_VECTOR *const hit_pos, int32_t *const damage)\n{\n    if (item->current_anim_state == NATLA_STATE_SEMIDEATH) {\n        if (damage != nullptr) {\n            *damage = 0;\n        }\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return item->hit_points > 0 && item->status == IS_ACTIVE\n        && item->current_anim_state != NATLA_STATE_SEMIDEATH;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const natla = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n    int16_t gun = natla->head_rotation * 7 / 8;\n    int16_t timer = natla->flags & NATLA_TIMER;\n    int16_t facing = (int16_t)(intptr_t)item->priv;\n\n    if (item->hit_points <= 0\n        && item->current_anim_state != NATLA_STATE_SEMIDEATH) {\n        item->goal_anim_state = NATLA_STATE_DEATH;\n    } else if (item->hit_points <= M_GetStage2HitPoints(item)) {\n        natla->lot.setup.step = STEP_L;\n        natla->lot.setup.drop = -STEP_L;\n        natla->lot.setup.fly = 0;\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead && item->current_anim_state != NATLA_STATE_SEMIDEATH) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, NATLA_RUN_TURN);\n\n        int8_t shoot = info.angle > -NATLA_FIRE_ARC\n            && info.angle < NATLA_FIRE_ARC\n            && Creature_CanTargetEnemy(item, &info);\n\n        if (facing) {\n            item->rot.y += facing;\n            facing = 0;\n        }\n\n        switch (item->current_anim_state) {\n        case NATLA_STATE_FALL:\n            if (item->pos.y < item->floor) {\n                item->gravity = true;\n                item->speed = 0;\n            } else {\n                item->gravity = false;\n                item->goal_anim_state = NATLA_STATE_SEMIDEATH;\n                item->pos.y = item->floor;\n                timer = 0;\n            }\n            break;\n\n        case NATLA_STATE_STAND:\n            if (!shoot) {\n                item->goal_anim_state = NATLA_STATE_RUN;\n            }\n            if (timer >= 20) {\n                int16_t effect_num =\n                    Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanShard);\n                if (effect_num != NO_EFFECT) {\n                    EFFECT *effect = Effect_Get(effect_num);\n                    gun = effect->rot.x;\n                    Sound_Effect(\n                        SFX_ATLANTEAN_NEEDLE, &effect->pos, SPM_NORMAL);\n                }\n                timer = 0;\n            }\n            break;\n\n        case NATLA_STATE_RUN:\n            tilt = angle;\n            if (timer >= 20) {\n                int16_t effect_num =\n                    Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanShard);\n                if (effect_num != NO_EFFECT) {\n                    EFFECT *effect = Effect_Get(effect_num);\n                    gun = effect->rot.x;\n                    Sound_Effect(\n                        SFX_ATLANTEAN_NEEDLE, &effect->pos, SPM_NORMAL);\n                }\n                timer = 0;\n            }\n            if (shoot) {\n                item->goal_anim_state = NATLA_STATE_STAND;\n            }\n            break;\n\n        case NATLA_STATE_SEMIDEATH:\n            if (timer == NATLA_DIE_TIME) {\n                item->goal_anim_state = NATLA_STATE_STAND;\n                natla->flags = 0;\n                timer = 0;\n                item->hit_points = M_GetStage2HitPoints(item);\n                const MUSIC_PLAY_MODE mode =\n                    g_Config.audio.fix_speeches_killing_music ? MPM_OVERLAY\n                                                              : MPM_NO_REPEAT;\n                Music_Play(MX_NATLA_SPEECH, mode);\n            } else {\n                if (g_Config.gameplay.target_mode == TARGET_LOCK_MODE_SEMI\n                    || g_Config.gameplay.target_mode == TARGET_LOCK_MODE_NONE) {\n                    LARA_INFO *const lara = Lara_GetLaraInfo();\n                    lara->target = nullptr;\n                }\n                item->hit_points = 0;\n            }\n            break;\n\n        case NATLA_STATE_FLY:\n            item->goal_anim_state = NATLA_STATE_FALL;\n            timer = 0;\n            break;\n\n        case NATLA_STATE_STOP:\n        case NATLA_STATE_AIM:\n        case NATLA_STATE_SHOOT:\n            item->goal_anim_state = NATLA_STATE_SEMIDEATH;\n            item->flags = 0;\n            timer = 0;\n            break;\n        }\n    } else {\n        natla->lot.setup.step = STEP_L;\n        natla->lot.setup.drop = -STEP_L;\n        natla->lot.setup.fly = 0;\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        int8_t shoot = info.angle > -NATLA_FIRE_ARC\n            && info.angle < NATLA_FIRE_ARC\n            && Creature_CanTargetEnemy(item, &info);\n        if (item->current_anim_state == NATLA_STATE_FLY\n            && (natla->flags & NATLA_FLY_MODE)) {\n            if (shoot && Random_GetControl() < NATLA_LAND_CHANCE) {\n                natla->flags &= ~NATLA_FLY_MODE;\n            }\n            if (!(natla->flags & NATLA_FLY_MODE)) {\n                Creature_Mood(item, &info, true);\n            }\n            natla->lot.setup.step = WALL_L * 20;\n            natla->lot.setup.drop = -WALL_L * 20;\n            natla->lot.setup.fly = STEP_L / 8;\n            Creature_AIInfo(item, &info);\n        } else if (!shoot) {\n            natla->flags |= NATLA_FLY_MODE;\n        }\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        if (item->current_anim_state != NATLA_STATE_FLY\n            || (natla->flags & NATLA_FLY_MODE)) {\n            Creature_Mood(item, &info, false);\n        }\n\n        item->rot.y -= facing;\n        angle = Creature_Turn(item, NATLA_FLY_TURN);\n\n        if (item->current_anim_state == NATLA_STATE_FLY) {\n            if (info.angle > NATLA_FLY_TURN) {\n                facing += NATLA_FLY_TURN;\n            } else if (info.angle < -NATLA_FLY_TURN) {\n                facing -= NATLA_FLY_TURN;\n            } else {\n                facing += info.angle;\n            }\n            item->rot.y += facing;\n        } else {\n            item->rot.y += facing - angle;\n            facing = 0;\n        }\n\n        switch (item->current_anim_state) {\n        case NATLA_STATE_STOP:\n            timer = 0;\n            if (natla->flags & NATLA_FLY_MODE) {\n                item->goal_anim_state = NATLA_STATE_FLY;\n            } else {\n                item->goal_anim_state = NATLA_STATE_AIM;\n            }\n            break;\n\n        case NATLA_STATE_FLY:\n            if (!(natla->flags & NATLA_FLY_MODE)\n                && item->pos.y == item->floor) {\n                item->goal_anim_state = NATLA_STATE_STOP;\n            }\n            if (timer >= 30) {\n                int16_t effect_num =\n                    Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb);\n                if (effect_num != NO_EFFECT) {\n                    EFFECT *effect = Effect_Get(effect_num);\n                    gun = effect->rot.x;\n                    Sound_Effect(\n                        SFX_ATLANTEAN_NEEDLE, &effect->pos, SPM_NORMAL);\n                }\n                timer = 0;\n            }\n            break;\n\n        case NATLA_STATE_AIM:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (shoot) {\n                item->goal_anim_state = NATLA_STATE_SHOOT;\n            } else {\n                item->goal_anim_state = NATLA_STATE_STOP;\n            }\n            break;\n\n        case NATLA_STATE_SHOOT:\n            if (!item->required_anim_state) {\n                int16_t effect_num =\n                    Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb);\n                if (effect_num != NO_EFFECT) {\n                    EFFECT *effect = Effect_Get(effect_num);\n                    gun = effect->rot.x;\n                }\n                effect_num =\n                    Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb);\n                if (effect_num != NO_EFFECT) {\n                    EFFECT *effect = Effect_Get(effect_num);\n                    effect->rot.y += (Random_GetControl() - 0x4000) / 4;\n                }\n                effect_num =\n                    Creature_Effect(item, &m_NatlaGun, Spawn_AtlanteanBomb);\n                if (effect_num != NO_EFFECT) {\n                    EFFECT *effect = Effect_Get(effect_num);\n                    effect->rot.y += (Random_GetControl() - 0x4000) / 4;\n                }\n                item->required_anim_state = NATLA_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n\n    natla->neck_rotation = -head;\n    if (gun) {\n        natla->head_rotation = gun;\n    }\n\n    timer++;\n    natla->flags &= ~NATLA_TIMER;\n    natla->flags |= timer & NATLA_TIMER;\n\n    item->rot.y -= facing;\n    Creature_Animate(item_num, angle, 0);\n    item->rot.y += facing;\n\n    item->priv = (void *)(intptr_t)facing;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->collision_func = Creature_Collision;\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->gun_hit_func = M_GunHit;\n    obj->is_targetable_func = M_IsTargetable;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = NATLA_HITPOINTS;\n    obj->radius = NATLA_RADIUS;\n    obj->smartness = NATLA_SMARTNESS;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 2)->rot.x = true;\n    Object_GetBone(obj, 2)->rot.z = true;\n}\n\nREGISTER_OBJECT(O_NATLA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/natla_gun.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n\n    effect->frame_num--;\n    if (effect->frame_num <= obj->mesh_count) {\n        Effect_Kill(effect_num);\n    }\n\n    if (effect->frame_num == -1) {\n        return;\n    }\n\n    const XYZ_32 pos =\n        XYZ_32_OffsetYaw(effect->pos, effect->rot.y, effect->speed);\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n\n    if (pos.y >= Room_GetHeight(sector, pos)\n        || pos.y <= Room_GetCeiling(sector, pos)) {\n        return;\n    }\n\n    const int16_t new_effect_num = Effect_Create(room_num);\n    if (new_effect_num != NO_EFFECT) {\n        EFFECT *const new_effect = Effect_Get(new_effect_num);\n        new_effect->pos = pos;\n        new_effect->rot.y = effect->rot.y;\n        new_effect->room_num = room_num;\n        new_effect->speed = effect->speed;\n        new_effect->frame_num = 0;\n        new_effect->object_id = O_NATLA_GUN;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nREGISTER_OBJECT(O_NATLA_GUN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/orca.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/fx.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n\n#define M_FAST_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296\n#define M_ATTACK_1_RANGE SQUARE(WALL_L * 3 / 4) // = 589824\n#define M_FAST_TURN (DEG_1 * 2) // = 364\n#define M_SLOW_TURN (DEG_1 * 2) // = 364\n#define M_RADIUS (WALL_L / 3) // = 341\n\ntypedef enum {\n    M_STATE_SLOW,\n    M_STATE_FAST,\n    M_STATE_JUMP,\n    M_STATE_SPLASH,\n    M_STATE_SLOW_BUTT,\n    M_STATE_FAST_BUTT,\n    M_STATE_BREACH,\n    M_STATE_ROLL_180\n} M_STATE;\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic bool M_CanBeProjectileTarget(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    const ITEM *const lara_item = Lara_GetItem();\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n    Creature_UpdateMood(item, &info, true);\n\n    if (!(Room_Get(lara_item->room_num)->flags.underwater)\n        && Lara_Vehicle_GetItem() == nullptr) {\n        creature->mood = MOOD_BORED;\n    }\n\n    Creature_ApplyMood(item, &info, true);\n    const int16_t angle = Creature_Turn(item, creature->maximum_turn);\n\n    switch (item->current_anim_state) {\n    case M_STATE_SLOW:\n        creature->flags = 0;\n        creature->maximum_turn = M_SLOW_TURN;\n\n        if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() & 0xFF) {\n                item->goal_anim_state = M_STATE_SLOW;\n            } else {\n                item->goal_anim_state = M_STATE_JUMP;\n            }\n        } else if (info.ahead && info.distance < M_ATTACK_1_RANGE) {\n            item->goal_anim_state = M_STATE_SLOW_BUTT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_FAST;\n        } else if (info.distance > M_FAST_RANGE) {\n            if (info.angle >= 0x5000 || info.angle <= -0x5000) {\n                item->goal_anim_state = M_STATE_ROLL_180;\n            } else if (Random_GetControl() & 0x3F) {\n                item->goal_anim_state = M_STATE_FAST;\n            } else {\n                item->goal_anim_state = M_STATE_BREACH;\n            }\n        }\n\n        break;\n\n    case M_STATE_FAST:\n        creature->flags = 0;\n        creature->maximum_turn = M_FAST_TURN;\n\n        if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() & 0xFF) {\n                item->goal_anim_state = M_STATE_SLOW;\n            } else {\n                item->goal_anim_state = M_STATE_JUMP;\n            }\n        } else if (creature->mood != MOOD_ESCAPE) {\n            if (info.ahead && info.distance < M_FAST_RANGE\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_SLOW;\n            } else if (\n                info.distance > M_FAST_RANGE && !(Random_GetControl() & 0x7F)) {\n                item->goal_anim_state = M_STATE_JUMP;\n            } else if (info.distance > M_FAST_RANGE && !info.ahead) {\n                item->goal_anim_state = M_STATE_SLOW;\n            }\n        }\n        break;\n\n    case M_STATE_ROLL_180:\n        creature->maximum_turn = 0;\n        if (Item_GetRelativeFrame(item) == 59) {\n            item->rot.x = -item->rot.x;\n            item->rot.y += DEG_180;\n            item->interp.prev.pos = item->pos;\n            item->interp.prev.rot = item->rot;\n            item->interp.result.pos = item->pos;\n            item->interp.result.rot = item->rot;\n        }\n        break;\n    }\n\n    Creature_Animate(item_num, angle, 0);\n    Creature_Underwater(item, WALL_L / 5);\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if ((time4 & 4) != 0) {\n        XYZ_32 pos = { .x = -32, .y = 16, .z = -300 };\n        Collide_GetJointAbsPosition(item, &pos, 5);\n\n        int16_t room_num = item->room_num;\n        Room_GetSector(pos, &room_num);\n        const int32_t water_height = Room_GetWaterHeight(pos, room_num);\n\n        if (water_height != NO_HEIGHT && pos.y < water_height) {\n            FX_WATER_RIPPLE *const ripple = FX_Water_SetupRipple(\n                pos.x, water_height, pos.z, -2 - (Random_GetControl() & 1), 0);\n            if (ripple != nullptr) {\n                ripple->init = 0;\n            }\n        }\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (!GF_BadIsMod(\"tr3-la\")) {\n        Creature_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    Lara_Col_ItemPush(item, coll, coll->enable_hit, false);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->can_be_projectile_target_func = M_CanBeProjectileTarget;\n\n    obj->shadow_size = 128;\n    obj->pivot_length = 200;\n    obj->radius = M_RADIUS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n    obj->lot_setup.block_mask = BOX_BLOCKABLE;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_ORCA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/patrol_dog.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_LUNGE_RANGE          SQUARE(WALL_L)\n#define M_LUNGE_TOUCH_BITS     0x6648\n#define M_LUNGE_DAMAGE         50\n#define M_STALK_RANGE          SQUARE(WALL_L + (WALL_L / 2)) // = 0x240000\n#define M_STALK_TURN           (3 * DEG_1)\n#define M_BITE_RANGE           SQUARE(WALL_L * 5 / 12) // = 0x2c4e4\n#define M_BITE_TOUCH_BITS      0x48\n#define M_BITE_DAMAGE          12\n#define M_RUN_TURN             (6 * DEG_1)\n#define M_STAT_TURN            (1 * DEG_1)\n#define M_WALK_TURN            (3 * DEG_1)\n#define M_MINIMUM_SLEEP_TIME   (30 * 10) // = 300\n#define M_SLEEP_CHANCE         0x100 // = 256\n#define M_SLEEP_2_STAND_CHANCE 0x80 // = 128\n#define M_STAT_CHANCE          0x100 // = 256\n#define M_WALK_CHANCE          0x1000 // = 4096\n#define M_AWARE_RANGE          SQUARE(3 * WALL_L) // = 0x900000\n// clang-format on\n\nstatic BITE m_DogBite = {\n    .pos = { .x = 0, .y = 0, .z = 100 },\n    .mesh_num = 3,\n};\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_JUMP,\n    M_STATE_STALK,\n    M_STATE_ATTACK_1,\n    M_STATE_HOWL,\n    M_STATE_SLEEP,\n    M_STATE_CROUCH,\n    M_STATE_TURN,\n    M_STATE_DEATH,\n    M_STATE_ATTACK_2\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_STOP = 8,\n    M_ANIM_DEATH_1 = 20,\n    M_ANIM_DEATH_2 = 21,\n    M_ANIM_DEATH_3 = 22,\n} M_ANIM;\n\nstatic M_ANIM m_DeathAnimCount = 4;\nstatic M_ANIM m_DeathAnims[4] = {\n    M_ANIM_DEATH_1,\n    M_ANIM_DEATH_2,\n    M_ANIM_DEATH_3,\n    M_ANIM_DEATH_1,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t x_head = 0;\n\n    ITEM *const lara_item = Lara_GetItem();\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(\n                item, m_DeathAnims[Random_GetControl() % m_DeathAnimCount], 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n    } else {\n        if (item->ai_bits != 0) {\n            Creature_GetAITarget(creature);\n        } else {\n            creature->enemy = lara_item;\n        }\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        int32_t dist;\n        if (creature->enemy == lara_item) {\n            dist = info.distance;\n        } else {\n            dist = XYZ_32_GetLength2((XYZ_32) {\n                .x = lara_item->pos.x - item->pos.x,\n                .y = 0,\n                .z = lara_item->pos.z - item->pos.z,\n            });\n        }\n\n        if (info.ahead) {\n            head = info.angle;\n            x_head = info.x_angle;\n        }\n\n        Creature_Mood(item, &info, true);\n        if (creature->mood == MOOD_BORED) {\n            creature->maximum_turn >>= 1;\n        }\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n        if (creature->hurt_by_lara\n            || (dist < M_AWARE_RANGE && !(item->ai_bits & AI_MODIFY))) {\n            Creature_AlertAllGuards(item_num);\n            item->ai_bits &= ~AI_MODIFY;\n        }\n\n        const int16_t rnd = Random_GetControl();\n        const int16_t frame = Item_GetRelativeFrame(item);\n\n        switch (item->current_anim_state) {\n        case M_STATE_NULL:\n        case M_STATE_SLEEP:\n            head = 0;\n            x_head = 0;\n            if (creature->mood != MOOD_BORED && item->ai_bits != AI_MODIFY) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                creature->flags++;\n                creature->maximum_turn = 0;\n                if (creature->flags > M_MINIMUM_SLEEP_TIME\n                    && rnd < M_SLEEP_2_STAND_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n            break;\n\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wimplicit-fallthrough\"\n        case M_STATE_CROUCH:\n            if (item->required_anim_state != 0) {\n                item->goal_anim_state = item->required_anim_state;\n                break;\n            }\n#pragma GCC diagnostic pop\n\n        case M_STATE_STOP:\n            creature->maximum_turn = 0;\n\n            if (item->ai_bits & AI_GUARD) {\n                head = Creature_AIGuard(creature);\n\n                if (!(Random_GetControl() & 0xFF)) {\n                    if (item->current_anim_state == M_STATE_STOP) {\n                        item->goal_anim_state = M_STATE_CROUCH;\n                    } else {\n                        item->goal_anim_state = M_STATE_STOP;\n                    }\n                }\n            } else if (\n                item->current_anim_state == M_STATE_CROUCH\n                && rnd < M_SLEEP_2_STAND_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (item->ai_bits & AI_PATROL_1) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                LARA_INFO *const lara = Lara_GetLaraInfo();\n                if (lara->target == item || !info.ahead || item->hit_status) {\n                    item->required_anim_state = M_STATE_RUN;\n                    item->goal_anim_state = M_STATE_CROUCH;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                creature->flags = 0;\n                creature->maximum_turn = M_STAT_TURN;\n\n                if (rnd < M_SLEEP_CHANCE && item->ai_bits & AI_MODIFY\n                    && item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_SLEEP;\n                    creature->flags = 0;\n                } else if (rnd < M_WALK_CHANCE) {\n                    if (item->current_anim_state == M_STATE_STOP) {\n                        item->goal_anim_state = M_STATE_WALK;\n                    } else {\n                        item->goal_anim_state = M_STATE_STOP;\n                    }\n                } else if (!(rnd & 0x1F)) {\n                    item->goal_anim_state = M_STATE_HOWL;\n                }\n            } else {\n                item->required_anim_state = M_STATE_RUN;\n\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_CROUCH;\n                }\n            }\n\n            break;\n\n        case M_STATE_WALK:\n            creature->maximum_turn = M_WALK_TURN;\n            if (item->ai_bits & AI_PATROL_1) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (creature->mood == MOOD_BORED && rnd < M_STAT_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_STALK;\n            }\n            break;\n\n        case M_STATE_RUN:\n            creature->maximum_turn = M_RUN_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                LARA_INFO *const lara = Lara_GetLaraInfo();\n                if (lara->target != item && info.ahead) {\n                    item->goal_anim_state = M_STATE_CROUCH;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_CROUCH;\n            } else if (info.bite && info.distance < M_LUNGE_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_1;\n            } else if (info.distance < M_STALK_RANGE) {\n                item->required_anim_state = M_STATE_STALK;\n                item->goal_anim_state = M_STATE_CROUCH;\n            }\n            break;\n\n        case M_STATE_STALK:\n            creature->maximum_turn = M_STALK_TURN;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_CROUCH;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (info.bite && info.distance < M_BITE_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_2;\n                item->required_anim_state = M_STATE_STALK;\n            } else if (info.distance > M_STALK_RANGE || item->hit_status) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_ATTACK_1:\n            if (info.bite && item->touch_bits & M_LUNGE_TOUCH_BITS && frame >= 4\n                && frame <= 14) {\n                Creature_Effect(item, &m_DogBite, Spawn_Blood);\n                Lara_TakeDamage(M_LUNGE_DAMAGE, true);\n            }\n            item->goal_anim_state = M_STATE_RUN;\n            break;\n\n        case M_STATE_HOWL:\n            head = 0;\n            x_head = 0;\n            break;\n\n        case M_STATE_ATTACK_2:\n            if (info.bite && item->touch_bits & M_BITE_TOUCH_BITS\n                && ((frame >= 9 && frame <= 12)\n                    || (frame >= 22 && frame <= 25))) {\n                Creature_Effect(item, &m_DogBite, Spawn_Blood);\n                Lara_TakeDamage(M_BITE_DAMAGE, true);\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, 0);\n    Creature_Joint(item, 0, head);\n    Creature_Joint(item, 1, x_head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->shadow_size = 128;\n    obj->hit_points = 16;\n    obj->pivot_length = 300;\n    obj->radius = 341;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 2)->rot.y = true;\n    Object_GetBone(obj, 2)->rot.x = true;\n}\n\nREGISTER_OBJECT(O_PATROL_DOG, M_Setup)\nREGISTER_OBJECT(O_HUSKIE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/pierre.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/creature.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n\n#define PIERRE_POSE_CHANCE 0x60 // = 96\n#define PIERRE_SHOT_DAMAGE 50\n#define PIERRE_WALK_TURN (DEG_1 * 3) // = 546\n#define PIERRE_RUN_TURN (DEG_1 * 6) // = 1092\n#define PIERRE_WALK_RANGE SQUARE(WALL_L * 3) // = 9437184\n#define PIERRE_DIE_ANIM 12\n#define PIERRE_WIMP_CHANCE 0x2000\n#define PIERRE_RUN_HITPOINTS 40\n#define PIERRE_DISAPPEAR 10\n#define PIERRE_HITPOINTS 70\n#define PIERRE_RADIUS (WALL_L / 10) // = 102\n#define PIERRE_SMARTNESS 0x7FFF\n\ntypedef enum {\n    PIERRE_STATE_EMPTY = 0,\n    PIERRE_STATE_STOP = 1,\n    PIERRE_STATE_WALK = 2,\n    PIERRE_STATE_RUN = 3,\n    PIERRE_STATE_AIM = 4,\n    PIERRE_STATE_DEATH = 5,\n    PIERRE_STATE_POSE = 6,\n    PIERRE_STATE_SHOOT = 7,\n} PIERRE_STATE;\n\nstatic const CREATURE_GUN m_PierreGun1 = {\n    .muzzle = { .pos = { 60, 200, 0 }, .mesh_num = 11 },\n};\nstatic const CREATURE_GUN m_PierreGun2 = {\n    .muzzle = { .pos = { -57, 200, 0 }, .mesh_num = 14 },\n};\nstatic int16_t m_PierreItemNum = NO_ITEM;\n\nstatic bool M_CanDropItems(const ITEM *const item)\n{\n    return item->hit_points <= 0 && (item->flags & IF_ONE_SHOT) != 0;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->hit_points <= 0 && (item->flags & IF_ONE_SHOT)) {\n            const uint16_t flags =\n                Music_GetTrackFlags(Music_ToGameID(MX_PIERRE_SPEECH));\n            Music_SetTrackFlags(\n                Music_ToGameID(MX_PIERRE_SPEECH), flags | IF_ONE_SHOT);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (g_Config.gameplay.change_pierre_spawn) {\n        if (m_PierreItemNum == NO_ITEM) {\n            m_PierreItemNum = item_num;\n        } else if (m_PierreItemNum != item_num) {\n            ITEM *old_pierre = Item_Get(m_PierreItemNum);\n            if (old_pierre->flags & IF_ONE_SHOT) {\n                if (!(item->flags & IF_ONE_SHOT)) {\n                    Item_Kill(item_num);\n                }\n            } else {\n                Item_Kill(m_PierreItemNum);\n                m_PierreItemNum = item_num;\n            }\n        }\n    } else {\n        if (m_PierreItemNum == NO_ITEM) {\n            m_PierreItemNum = item_num;\n        } else if (m_PierreItemNum != item_num) {\n            if (item->flags & IF_ONE_SHOT) {\n                Item_Kill(m_PierreItemNum);\n            } else {\n                Item_Kill(item_num);\n            }\n        }\n    }\n\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    CREATURE *const pierre = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= PIERRE_RUN_HITPOINTS\n        && !(item->flags & IF_ONE_SHOT)) {\n        item->hit_points = PIERRE_RUN_HITPOINTS;\n        pierre->flags++;\n    }\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != PIERRE_STATE_DEATH) {\n            item->current_anim_state = PIERRE_STATE_DEATH;\n            Item_SwitchToAnim(item, PIERRE_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        if (pierre->flags) {\n            info.enemy_zone_num = -1;\n            item->hit_status = true;\n        }\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, pierre->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case PIERRE_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (pierre->mood == MOOD_BORED) {\n                item->goal_anim_state = Random_GetControl() < PIERRE_POSE_CHANCE\n                    ? PIERRE_STATE_POSE\n                    : PIERRE_STATE_WALK;\n            } else if (pierre->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = PIERRE_STATE_RUN;\n            } else {\n                item->goal_anim_state = PIERRE_STATE_WALK;\n            }\n            break;\n\n        case PIERRE_STATE_POSE:\n            if (pierre->mood != MOOD_BORED) {\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            } else if (Random_GetControl() < PIERRE_POSE_CHANCE) {\n                item->required_anim_state = PIERRE_STATE_WALK;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            }\n            break;\n\n        case PIERRE_STATE_WALK:\n            pierre->maximum_turn = PIERRE_WALK_TURN;\n            if (pierre->mood == MOOD_BORED\n                && Random_GetControl() < PIERRE_POSE_CHANCE) {\n                item->required_anim_state = PIERRE_STATE_POSE;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            } else if (pierre->mood == MOOD_ESCAPE) {\n                item->required_anim_state = PIERRE_STATE_RUN;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = PIERRE_STATE_AIM;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            } else if (!info.ahead || info.distance > PIERRE_WALK_RANGE) {\n                item->required_anim_state = PIERRE_STATE_RUN;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            }\n            break;\n\n        case PIERRE_STATE_RUN:\n            pierre->maximum_turn = PIERRE_RUN_TURN;\n            tilt = angle / 2;\n            if (pierre->mood == MOOD_BORED\n                && Random_GetControl() < PIERRE_POSE_CHANCE) {\n                item->required_anim_state = PIERRE_STATE_POSE;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->required_anim_state = PIERRE_STATE_AIM;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            } else if (info.ahead && info.distance < PIERRE_WALK_RANGE) {\n                item->required_anim_state = PIERRE_STATE_WALK;\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            }\n            break;\n\n        case PIERRE_STATE_AIM:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = PIERRE_STATE_SHOOT;\n            } else {\n                item->goal_anim_state = PIERRE_STATE_STOP;\n            }\n            break;\n\n        case PIERRE_STATE_SHOOT:\n            if (!item->required_anim_state) {\n                Creature_Shoot(\n                    item, &info, &m_PierreGun1, head, PIERRE_SHOT_DAMAGE / 2);\n                Creature_Shoot(\n                    item, &info, &m_PierreGun2, head, PIERRE_SHOT_DAMAGE / 2);\n                item->required_anim_state = PIERRE_STATE_AIM;\n            }\n            if (pierre->mood == MOOD_ESCAPE\n                && Random_GetControl() > PIERRE_WIMP_CHANCE) {\n                item->required_anim_state = PIERRE_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n\n    if (pierre->flags) {\n        GAME_VECTOR target;\n        target.x = item->pos.x;\n        target.y = item->pos.y - WALL_L;\n        target.z = item->pos.z;\n\n        GAME_VECTOR start;\n        start.x = g_Camera.pos.x;\n        start.y = g_Camera.pos.y;\n        start.z = g_Camera.pos.z;\n        start.room_num = g_Camera.pos.room_num;\n\n        if (LOS_Check(&start, &target, true)) {\n            pierre->flags = 1;\n        } else if (pierre->flags > PIERRE_DISAPPEAR) {\n            item->hit_points = 0;\n            LOT_DisableBaddieAI(item_num);\n            Item_Kill(item_num);\n            m_PierreItemNum = NO_ITEM;\n        }\n    }\n\n    int16_t wh = Room_GetWaterHeight(item->pos, item->room_num);\n    if (wh != NO_HEIGHT) {\n        item->hit_points = 0;\n        LOT_DisableBaddieAI(item_num);\n        Item_Kill(item_num);\n        m_PierreItemNum = NO_ITEM;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = Creature_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->can_drop_items_func = M_CanDropItems;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = PIERRE_HITPOINTS;\n    obj->radius = PIERRE_RADIUS;\n    obj->smartness = PIERRE_SMARTNESS;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    m_PierreItemNum = NO_ITEM;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_PIERRE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/pod.c",
    "content": "#include <trx/game/objects/creatures/pod.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n\n#define POD_EXPLODE_DIST (WALL_L * 4) // = 4096\n\ntypedef enum {\n    POD_STATE_SET = 0,\n    POD_STATE_EXPLODE = 1,\n} POD_STATE;\n\ntypedef struct {\n    int16_t bug_item_num;\n} M_PRIV;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->bug_item_num = NO_ITEM;\n\n    const int16_t bug_item_num = Item_CreateLevelItem();\n    if (bug_item_num != NO_ITEM) {\n        ITEM *const bug = Item_Get(bug_item_num);\n        bug->object_id = Pod_GetBugObjectID(item);\n        bug->room_num = item->room_num;\n        bug->pos.x = item->pos.x;\n        bug->pos.y = item->pos.y;\n        bug->pos.z = item->pos.z;\n        bug->rot.y = item->rot.y;\n        bug->flags = IF_INVISIBLE;\n        bug->shade.value_1 = -1;\n\n        Item_Initialise(bug_item_num);\n        p->bug_item_num = bug_item_num;\n    }\n\n    item->flags = 0;\n    item->mesh_bits = 0xFF0001FF;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->status == IS_DEACTIVATED) {\n            item->mesh_bits = 0x1FF;\n            item->collidable = false;\n        }\n    }\n}\n\nstatic int16_t M_GetCarrierItemNum(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->bug_item_num;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->goal_anim_state != POD_STATE_EXPLODE) {\n        int32_t explode = 0;\n\n        if (item->flags & IF_ONE_SHOT) {\n            explode = 1;\n        } else if (item->object_id == O_BIG_POD) {\n            explode = 1;\n        } else {\n            const ITEM *const lara_item = Lara_GetItem();\n            int32_t x = lara_item->pos.x - item->pos.x;\n            int32_t y = lara_item->pos.y - item->pos.y;\n            int32_t z = lara_item->pos.z - item->pos.z;\n            if (ABS(x) < POD_EXPLODE_DIST && ABS(y) < POD_EXPLODE_DIST\n                && ABS(z) < POD_EXPLODE_DIST) {\n                explode = 1;\n            }\n        }\n\n        if (explode) {\n            item->goal_anim_state = POD_STATE_EXPLODE;\n            item->mesh_bits = 0xFFFFFF;\n            item->collidable = false;\n            Item_Explode(item_num, 0xFFFE00, 0);\n\n            const M_PRIV *const p = item->priv;\n            if (p->bug_item_num != NO_ITEM) {\n                ITEM *const bug = Item_Get(p->bug_item_num);\n                if (Object_Get(bug->object_id)->loaded) {\n                    bug->touch_bits = 0;\n                    Item_AddActive(p->bug_item_num);\n                    if (LOT_EnableBaddieAI(p->bug_item_num, false)) {\n                        bug->status = IS_ACTIVE;\n                    } else {\n                        bug->status = IS_INVISIBLE;\n                    }\n                }\n            }\n            item->status = IS_DEACTIVATED;\n        }\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->carrier_item_num_func = M_GetCarrierItemNum;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nOBJECT_ID Pod_GetBugObjectID(const ITEM *const item)\n{\n    switch ((item->flags & IF_CODE_BITS) >> 9) {\n    case 1:\n        return O_ATLANTEAN_SHOOTER;\n    case 2:\n        return O_CENTAUR;\n    case 4:\n        return O_TORSO;\n    case 8:\n        return O_ATLANTEAN_GROUND;\n    default:\n        return O_ATLANTEAN_WINGED;\n    }\n}\n\nREGISTER_OBJECT(O_PODS, M_Setup)\nREGISTER_OBJECT(O_BIG_POD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/pod.h",
    "content": "#pragma once\n\n#include <trx/game/objects/common.h>\n\nOBJECT_ID Pod_GetBugObjectID(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/prisoner.c",
    "content": "#include <trx/config.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS         (WALL_L / 10)          // = 102\n#define M_HIT_POINTS     20\n#define M_PUNCH_1_DAMAGE 40\n#define M_PUNCH_3_DAMAGE 50\n#define M_TOUCH_BITS     0b00100100'00000000\n#define M_RUN_DIST       SQUARE(WALL_L * 2)     // = 4194304\n#define M_WALK_DIST      SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_DIST_1  SQUARE(WALL_L / 3)     // = 116281\n#define M_ATTACK_DIST_2  SQUARE(WALL_L * 2 / 3) // = 465124\n#define M_ATTACK_DIST_3  SQUARE(WALL_L * 3 / 4) // = 589824\n#define M_WALK_TURN      (DEG_1 * 7)            // = 1274\n#define M_RUN_TURN       (DEG_1 * 11)           // = 2002\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_PUNCH_3,\n    M_STATE_AIM_3,\n    M_STATE_WAIT,\n    M_STATE_AIM_2,\n    M_STATE_AIM_1,\n    M_STATE_PUNCH_2,\n    M_STATE_PUNCH_1,\n    M_STATE_RUN,\n    M_STATE_DEATH,\n    M_STATE_UP_4,\n    M_STATE_UP_2,\n    M_STATE_UP_3,\n    M_STATE_DOWN_4,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STOP   = 6,\n    M_ANIM_DEATH  = 26,\n    M_ANIM_UP_4   = 27,\n    M_ANIM_UP_2   = 28,\n    M_ANIM_UP_3   = 29,\n    M_ANIM_DOWN_4 = 30,\n    // clang-format on\n} M_ANIM;\n\nstatic const BITE m_Bite = {\n    .pos = { 10, 10, 11 },\n    .mesh_num = 13,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n    item->goal_anim_state = M_STATE_STOP;\n}\n\nstatic void M_CalculateEnemy(ITEM *const item)\n{\n    CREATURE *const prisoner = item->creature_data;\n    const ITEM *const lara_item = Lara_GetItem();\n    if (prisoner->hurt_by_lara) {\n        prisoner->enemy = (ITEM *)lara_item;\n        return;\n    }\n\n    int32_t best_distance = INT32_MAX;\n    prisoner->enemy = nullptr;\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == prisoner) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        if (candidate == lara_item || candidate->object_id == item->object_id\n            || candidate->object_id == O_SENTRY_GUN\n            || candidate->hit_points <= 0) {\n            continue;\n        }\n\n        const XYZ_32 delta = {\n            .x = candidate->pos.x - item->pos.x,\n            .y = 0,\n            .z = candidate->pos.z - item->pos.z,\n        };\n        if (ABS(delta.x) > 0x7D00 || ABS(delta.z) > 0x7D00) {\n            continue;\n        }\n\n        const int32_t distance = XYZ_32_GetLength2(delta);\n        if (distance < best_distance) {\n            prisoner->enemy = (ITEM *)candidate;\n            best_distance = distance;\n        }\n    }\n}\n\nstatic bool M_Vault(ITEM *const item, const int16_t angle)\n{\n    const int32_t vault_result =\n        Creature_Vault(Item_GetIndex(item), angle, 2, 260);\n    switch (vault_result) {\n    case -4:\n        Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0);\n        item->current_anim_state = M_STATE_DOWN_4;\n        return true;\n    case 2:\n        Item_SwitchToAnim(item, M_ANIM_UP_2, 0);\n        item->current_anim_state = M_STATE_UP_2;\n        return true;\n    case 3:\n        Item_SwitchToAnim(item, M_ANIM_UP_3, 0);\n        item->current_anim_state = M_STATE_UP_3;\n        return true;\n    case 4:\n        Item_SwitchToAnim(item, M_ANIM_UP_4, 0);\n        item->current_anim_state = M_STATE_UP_4;\n        return true;\n    default:\n        return false;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    Creature_TestBoxDamage(item_num);\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->lot.setup.step = STEP_L;\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0 && item->ai_bits != AI_MODIFY) {\n        Creature_GetAITarget(creature);\n    } else {\n        M_CalculateEnemy(item);\n    }\n\n    if (item->ai_bits == AI_MODIFY) {\n        item->hit_points = M_HIT_POINTS * 10;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    const bool hurt_by_lara = creature->hurt_by_lara;\n    if (creature->enemy == nullptr && creature->alerted\n        && g_Config.gameplay.ally_hostility_policy\n            == ALLY_HOSTILITY_POLICY_SHARED) {\n        creature->enemy = lara_item;\n    } else if (!hurt_by_lara && creature->enemy == lara_item) {\n        creature->enemy = nullptr;\n    }\n\n    // Enforce the following state to avoid Creature_AIInfo resetting ahead,\n    // bite and distance when the prisoner is friendly.\n    ITEM *const enemy = creature->enemy;\n    if (enemy == nullptr) {\n        creature->enemy = lara_item;\n        creature->hurt_by_lara = true;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    creature->enemy = enemy;\n    creature->hurt_by_lara = hurt_by_lara;\n\n    int32_t enemy_dist;\n    int32_t enemy_angle;\n    if (creature->enemy == lara_item) {\n        enemy_dist = info.distance;\n        enemy_angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        enemy_angle = Math_Atan(dz, dx) - item->rot.y;\n        enemy_dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (creature->hurt_by_lara) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n    case M_STATE_WAIT:\n        if (item->current_anim_state == M_STATE_WAIT\n            && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) {\n            item->goal_anim_state = M_STATE_STOP;\n            break;\n        }\n\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n        head = enemy_angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead && !item->hit_status) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal || enemy_dist > M_RUN_DIST))) {\n            if (item->required_anim_state != M_STATE_NULL) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_AIM_1;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_AIM_2;\n        } else if (info.bite && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = enemy_angle;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() < 256) {\n                item->required_anim_state = M_STATE_WAIT;\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_AIM_3;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            (item->ai_bits & AI_FOLLOW) != 0\n            && (creature->reached_goal || enemy_dist > M_RUN_DIST)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (info.ahead && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n\n        if (info.bite && info.distance < M_ATTACK_DIST_1) {\n            item->goal_anim_state = M_STATE_PUNCH_1;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n\n        if (info.ahead && info.distance < M_ATTACK_DIST_2) {\n            item->goal_anim_state = M_STATE_PUNCH_2;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_3:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n\n        if (info.bite && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_PUNCH_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (enemy == lara_item) {\n            if (creature->flags == 0\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_PUNCH_1_DAMAGE, true);\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        } else if (creature->flags == 0 && enemy != nullptr) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 2, true);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n            }\n        }\n        break;\n\n    case M_STATE_PUNCH_2:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (enemy == lara_item) {\n            if (creature->flags == 0\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_PUNCH_1_DAMAGE, true);\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n        } else if (creature->flags == 0 && enemy != nullptr) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_PUNCH_1_DAMAGE / 2, true);\n                creature->flags = 1;\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n            }\n        }\n\n        if (info.ahead && info.distance > M_ATTACK_DIST_2\n            && info.distance < M_ATTACK_DIST_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        }\n        break;\n\n    case M_STATE_PUNCH_3:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (enemy == lara_item) {\n            if (creature->flags != 2\n                && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_PUNCH_3_DAMAGE, true);\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 2;\n            }\n        } else if (creature->flags != 2 && enemy != nullptr) {\n            if (ABS(enemy->pos.x - item->pos.x) < STEP_L\n                && ABS(enemy->pos.y - item->pos.y) <= STEP_L\n                && ABS(enemy->pos.z - item->pos.z) < STEP_L) {\n                Item_TakeDamage(enemy, M_PUNCH_3_DAMAGE / 2, true);\n                Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                creature->flags = 2;\n                Creature_Effect(item, &m_Bite, Spawn_Blood);\n            }\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n\n    if (item->current_anim_state >= M_STATE_DEATH) {\n        creature->maximum_turn = 0;\n        Creature_Animate(item_num, angle, 0);\n    } else if (M_Vault(item, angle)) {\n        creature->maximum_turn = 0;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_PRISONER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/punk.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS         (WALL_L / 10)          // = 102\n#define M_HITPOINTS      20\n#define M_TOUCH_BITS     0b0100100'00000000\n#define M_IGNITE_COUNT   3\n#define M_SWIPE_DAMAGE   100\n#define M_HIT_DAMAGE     80\n#define M_MAX_FIRE_DIST  (WALL_L * 16)          // = 16384\n#define M_ALERT_DIST     SQUARE(WALL_L)         // = 1048576\n#define M_ALERT_HEIGHT   (STEP_L * 5)           // = 1280\n#define M_WALK_DIST      SQUARE(WALL_L)         // = 1048576\n#define M_RUN_DIST       SQUARE(WALL_L * 2)     // = 4194304\n#define M_ATTACK_RANGE_1 SQUARE(WALL_L / 2)     // = 262144\n#define M_ATTACK_RANGE_2 SQUARE(WALL_L)         // = 1048576\n#define M_ATTACK_RANGE_3 SQUARE(WALL_L * 5 / 4) // = 1638400\n#define M_WALK_TURN      (DEG_1 * 5)            // = 910\n#define M_RUN_TURN       (DEG_1 * 6)            // = 1092\n#define M_WAIT_CHANCE    0x100\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_PUNCH_3,\n    M_STATE_AIM_3,\n    M_STATE_WAIT,\n    M_STATE_AIM_2,\n    M_STATE_AIM_1,\n    M_STATE_PUNCH_2,\n    M_STATE_PUNCH_1,\n    M_STATE_RUN,\n    M_STATE_DEATH,\n    M_STATE_UP_2,\n    M_STATE_UP_3,\n    M_STATE_UP_4,\n    M_STATE_DOWN_4,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STAND  = 6,\n    M_ANIM_DEATH  = 26,\n    M_ANIM_UP_4   = 27,\n    M_ANIM_UP_2   = 28,\n    M_ANIM_UP_3   = 29,\n    M_ANIM_DOWN_4 = 30,\n    // clang-format on\n} M_ANIM;\n\ntypedef struct {\n    struct {\n        bool initialised;\n        bool on_fire;\n        uint8_t hit_count;\n    } stick;\n} M_PRIV;\n\nstatic const BITE m_Bite = {\n    .pos = { 16, 48, 320 },\n    .mesh_num = 13,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"stick_initialised\", &p->stick.initialised));\n    JSON_SHOULD(JSON_READ(io, \"stick_on_fire\", &p->stick.on_fire));\n    JSON_SHOULD(JSON_READ(io, \"stick_hit_count\", &p->stick.hit_count));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"stick_initialised\", p->stick.initialised);\n    JSONW_WRITE(io, \"stick_on_fire\", p->stick.on_fire);\n    JSONW_WRITE(io, \"stick_hit_count\", p->stick.hit_count);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STAND, 0);\n    item->current_anim_state = M_STATE_STOP;\n    item->goal_anim_state = M_STATE_STOP;\n}\n\nstatic void M_InitialiseStick(ITEM *const punk_item)\n{\n    M_PRIV *const p = punk_item->priv;\n    const int16_t fire_item_idx = Item_FindTypeAtPos(\n        punk_item->room_num, punk_item->pos, O_FLAME_EMITTER_BIG);\n    if (fire_item_idx != NO_ITEM) {\n        ITEM *const fire_item = Item_Get(fire_item_idx);\n        Item_Kill(fire_item_idx);\n        fire_item->room_num = NO_ROOM;\n        p->stick.on_fire = true;\n    }\n\n    p->stick.initialised = true;\n}\n\nstatic void M_TriggerFireSparks(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const XZ_32 delta = {\n        .x = lara_item->pos.x - item->pos.x,\n        .z = lara_item->pos.z - item->pos.z,\n    };\n    if (ABS(delta.x) > M_MAX_FIRE_DIST || ABS(delta.z) > M_MAX_FIRE_DIST) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.b = 48;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 5;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->gravity = -16 - (Random_GetControl() & 0x1F);\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->max_y_vel = -16 - (Random_GetControl() & 7);\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->gravity = -16 - (Random_GetControl() & 0x1F);\n        spark->max_y_vel = -16 - (Random_GetControl() & 7);\n    }\n\n    spark->node_num = 2;\n    spark->item_num = Item_GetIndex(item);\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    uint8_t size = (Random_GetControl() & 0x1F) + 64;\n    spark->src_size.width = size;\n    spark->size.width = size;\n    spark->src_size.height = size;\n    spark->size.height = size;\n    size >>= 2;\n    spark->dst_size.width = size;\n    spark->dst_size.height = size;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerFireLight(const ITEM *const item)\n{\n    const int32_t rnd = Random_GetControl();\n    XYZ_32 pos = {\n        .x = m_Bite.pos.x + (rnd & 0xF) - 8,\n        .y = m_Bite.pos.y + ((rnd >> 4) & 0xF) - 8,\n        .z = m_Bite.pos.z + ((rnd >> 8) & 0xF) - 8,\n    };\n    Collide_GetJointAbsPosition(item, &pos, m_Bite.mesh_num);\n    const RGB_888 color = {\n        .r = 255 - ((rnd >> 4) & 0x1F),\n        .g = 192 - ((rnd >> 6) & 0x1F),\n        .b = rnd & 0x3F,\n    };\n    Output_AddDynamicLightRGB(pos, 13, color);\n}\n\nstatic void M_HitLara(ITEM *const item, const int16_t damage)\n{\n    Lara_TakeDamage(damage, true);\n    Creature_Effect(item, &m_Bite, Spawn_Blood);\n    Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n\n    M_PRIV *const p = item->priv;\n    p->stick.hit_count++;\n    CLAMPG(p->stick.hit_count, M_IGNITE_COUNT);\n    if (p->stick.on_fire && p->stick.hit_count == M_IGNITE_COUNT) {\n        Lara_CatchFire();\n    }\n}\n\nstatic bool M_Vault(ITEM *const item, const int16_t angle)\n{\n    const int32_t vault_result =\n        Creature_Vault(Item_GetIndex(item), angle, 2, 260);\n    switch (vault_result) {\n    case -4:\n        Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0);\n        item->current_anim_state = M_STATE_DOWN_4;\n        return true;\n    case 2:\n        Item_SwitchToAnim(item, M_ANIM_UP_2, 0);\n        item->current_anim_state = M_STATE_UP_2;\n        return true;\n    case 3:\n        Item_SwitchToAnim(item, M_ANIM_UP_3, 0);\n        item->current_anim_state = M_STATE_UP_3;\n        return true;\n    case 4:\n        Item_SwitchToAnim(item, M_ANIM_UP_4, 0);\n        item->current_anim_state = M_STATE_UP_4;\n        return true;\n    default:\n        return false;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    M_PRIV *const p = item->priv;\n\n    if (!p->stick.initialised) {\n        M_InitialiseStick(item);\n    }\n\n    if (p->stick.on_fire) {\n        M_TriggerFireSparks(item);\n        M_TriggerFireLight(item);\n    }\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            item->goal_anim_state = M_STATE_DEATH;\n            creature->lot.setup.step = STEP_L;\n        }\n        goto finish;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        creature->enemy = lara_item;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    if (!creature->alerted && creature->enemy == lara_item) {\n        creature->enemy = nullptr;\n    }\n\n    Creature_Mood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if (item->hit_status\n        || ((lara_info.distance < M_ALERT_DIST\n             || Creature_CanSeeEnemy(item, &lara_info))\n            && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT\n            && Creature_IsHostile(item) && (item->ai_bits & AI_FOLLOW) == 0)) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_ENGLISH_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n    case M_STATE_WAIT:\n        if (item->current_anim_state == M_STATE_WAIT\n            && (creature->alerted || item->goal_anim_state == M_STATE_RUN)) {\n            item->goal_anim_state = M_STATE_STOP;\n            break;\n        }\n\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n        head = lara_info.angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead && !item->hit_status) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            if (item->required_anim_state != M_STATE_NULL) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_RANGE_1) {\n            item->goal_anim_state = M_STATE_AIM_1;\n        } else if (info.bite && info.distance < M_ATTACK_RANGE_2) {\n            item->goal_anim_state = M_STATE_AIM_2;\n        } else if (info.bite && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        creature->maximum_turn = M_WALK_TURN;\n        head = lara_info.angle;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() < M_WAIT_CHANCE) {\n                item->required_anim_state = M_STATE_WAIT;\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.bite && info.distance < M_ATTACK_RANGE_1) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.bite && info.distance < M_ATTACK_RANGE_3) {\n            item->goal_anim_state = M_STATE_AIM_3;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            (item->ai_bits & AI_FOLLOW) != 0\n            && (creature->reached_goal || lara_info.distance > M_RUN_DIST)) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (info.ahead && info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n        if (info.bite && info.distance < M_ATTACK_RANGE_1) {\n            item->goal_anim_state = M_STATE_PUNCH_1;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n        if (info.ahead && info.distance < M_ATTACK_RANGE_2) {\n            item->goal_anim_state = M_STATE_PUNCH_2;\n        } else {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_3:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        creature->maximum_turn = M_WALK_TURN;\n        creature->flags = 0;\n        if (info.bite && info.distance < M_ATTACK_RANGE_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_PUNCH_1:\n    case M_STATE_PUNCH_2:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        creature->maximum_turn = M_WALK_TURN;\n        if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            M_HitLara(item, M_HIT_DAMAGE);\n            creature->flags = 1;\n        }\n\n        if (item->current_anim_state == M_STATE_PUNCH_2 && info.ahead\n            && info.distance > M_ATTACK_RANGE_2\n            && info.distance < M_ATTACK_RANGE_3) {\n            item->goal_anim_state = M_STATE_PUNCH_3;\n        }\n        break;\n\n    case M_STATE_PUNCH_3:\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        creature->maximum_turn = M_WALK_TURN;\n        if (creature->flags != 2 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            M_HitLara(item, M_SWIPE_DAMAGE);\n            creature->flags = 2;\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n\n    if (item->current_anim_state >= M_STATE_DEATH) {\n        creature->maximum_turn = 0;\n        Creature_Animate(item_num, angle, 0);\n    } else if (M_Vault(item, angle)) {\n        creature->maximum_turn = 0;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = M_HITPOINTS;\n\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_PUNK_1, M_Setup)\nREGISTER_OBJECT(O_PUNK_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/raptor.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_ATTACK_RANGE   SQUARE(WALL_L * 3 / 2) // = 2359296\n#define M_BITE_DAMAGE    100\n#define M_CHARGE_DAMAGE  100\n#define M_CLOSE_RANGE    SQUARE(g_TRVersion < 3 ? 680 : 585) // = 462400 / 342225\n#define M_LUNGE_DAMAGE   100\n#define M_LUNGE_RANGE    SQUARE(WALL_L * 3 / 2) // = 2359296\n#define M_ROAR_CHANCE    (g_TRVersion < 3 ? 256 : 128)\n#define M_RUN_TURN       (4 * DEG_1) // = 728\n#define M_TOUCH          0xFF7C00\n#define M_WALK_TURN      (g_TRVersion < 3 ? (1 * DEG_1) : (2 * DEG_1)) // = 182 / 364\n#define M_HITPOINTS      (g_TRVersion == 3 ? 90 : 20)\n#define M_RADIUS         (WALL_L / 3) // = 341\n#define M_PIVOT_LENGTH   (g_TRVersion == 3 ? 600 : 400)\n#define M_SMARTNESS      0x4000\n#define M_INFIGHT_CHANCE 0x400\n#define M_INFIGHT_RANGE  0x400000\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_ATTACK_1,\n    M_STATE_DEATH,\n    M_STATE_WARNING,\n    M_STATE_ATTACK_2,\n    M_STATE_ATTACK_3,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 9,\n} M_ANIM;\n\nstatic BITE m_RaptorBite = {\n    .pos = { 0, 66, 318 },\n    .mesh_num = 22,\n};\n\nstatic void M_CalculateTarget(const ITEM *const item)\n{\n    CREATURE *const raptor = item->creature_data;\n    int32_t best_distance = INT32_MAX;\n    const ITEM *best_item = nullptr;\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == raptor) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        const XYZ_32 delta = {\n            .x = (candidate->pos.x - item->pos.x) >> 6,\n            .y = (candidate->pos.y - item->pos.y) >> 6,\n            .z = (candidate->pos.z - item->pos.z) >> 6,\n        };\n        const int32_t distance = XYZ_32_GetLength2(delta);\n        if (distance < best_distance) {\n            best_item = candidate;\n            best_distance = distance;\n        }\n    }\n\n    if (best_item != nullptr\n        && (best_item->object_id != item->object_id\n            || (Random_GetControl() < M_INFIGHT_CHANCE\n                && best_distance < M_INFIGHT_RANGE))) {\n        raptor->enemy = (ITEM *)best_item;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const XYZ_32 lara_delta = {\n        .x = (lara_item->pos.x - item->pos.x) >> 6,\n        .y = (lara_item->pos.y - item->pos.y) >> 6,\n        .z = (lara_item->pos.z - item->pos.z) >> 6,\n    };\n    if (XYZ_32_GetLength2(lara_delta) < best_distance) {\n        raptor->enemy = (ITEM *)lara_item;\n    }\n}\n\nstatic void M_Attack(ITEM *const item, const AI_INFO *const info)\n{\n    int32_t damage = 0;\n    int16_t next_state = M_STATE_EMPTY;\n    switch (item->current_anim_state) {\n    case M_STATE_ATTACK_1:\n        damage = M_LUNGE_DAMAGE;\n        next_state = M_STATE_STOP;\n        break;\n    case M_STATE_ATTACK_2:\n        damage = M_CHARGE_DAMAGE;\n        next_state = M_STATE_RUN;\n        break;\n    case M_STATE_ATTACK_3:\n        damage = M_BITE_DAMAGE;\n        next_state = M_STATE_STOP;\n        break;\n    default:\n        return;\n    }\n\n    if (g_TRVersion < 3) {\n        if (item->required_anim_state == M_STATE_EMPTY && info->ahead\n            && (item->touch_bits & M_TOUCH) != 0) {\n            Creature_Effect(item, &m_RaptorBite, Spawn_Blood);\n            Lara_TakeDamage(damage, true);\n            item->required_anim_state = next_state;\n        }\n        return;\n    }\n\n    CREATURE *const raptor = item->creature_data;\n    const ITEM *const lara_item = Lara_GetItem();\n    if (raptor->enemy == lara_item) {\n        if ((raptor->flags & 1) != 0 || (item->touch_bits & M_TOUCH) == 0) {\n            return;\n        }\n\n        raptor->flags |= 1;\n        Creature_Effect(item, &m_RaptorBite, Spawn_Blood);\n        if (lara_item->hit_points <= 0) {\n            raptor->flags |= 2;\n        }\n\n        Lara_TakeDamage(damage, true);\n        item->required_anim_state = next_state;\n        return;\n    }\n\n    if ((raptor->flags & 1) != 0 || raptor->enemy == nullptr\n        || !XYZ_32_IsNearby(raptor->enemy->pos, item->pos, WALL_L / 2)) {\n        return;\n    }\n\n    raptor->flags |= 1;\n    Creature_Effect(item, &m_RaptorBite, Spawn_Blood);\n\n    raptor->enemy->hit_points -= damage / 4;\n    raptor->enemy->hit_status = true;\n    if (raptor->enemy->hit_points <= 0) {\n        raptor->flags |= 2;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const raptor = item->creature_data;\n    int16_t head = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    const bool is_tr3 = g_TRVersion == 3;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            item->current_anim_state = M_STATE_DEATH;\n            const int32_t offset = Random_GetControl() > 0x4000 ? 0 : 1;\n            Item_SwitchToAnim(item, M_ANIM_DEATH + offset, 0);\n        }\n        goto finish;\n    }\n\n    if (is_tr3\n        && (raptor->enemy == nullptr || (Random_GetControl() & 0x7F) == 0)) {\n        M_CalculateTarget(item);\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(raptor);\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n\n    if (info.ahead) {\n        head = info.angle;\n    }\n\n    Creature_Mood(item, &info, true);\n    if (is_tr3 && raptor->mood == MOOD_BORED) {\n        raptor->maximum_turn /= 2;\n    }\n\n    angle = Creature_Turn(item, raptor->maximum_turn);\n    neck = angle * -6;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        raptor->flags &= ~1;\n        raptor->maximum_turn = 0;\n\n        if (item->required_anim_state != M_STATE_EMPTY) {\n            item->goal_anim_state = item->required_anim_state;\n        } else if (is_tr3 && (raptor->flags & 2) != 0) {\n            raptor->flags &= ~2;\n            item->goal_anim_state = M_STATE_WARNING;\n        } else if ((item->touch_bits & M_TOUCH) != 0) {\n            item->goal_anim_state = M_STATE_ATTACK_3;\n        } else if (info.bite && info.distance < M_CLOSE_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK_3;\n        } else if (info.bite && info.distance < M_LUNGE_RANGE) {\n            item->goal_anim_state = M_STATE_ATTACK_1;\n        } else if (\n            is_tr3 && raptor->mood == MOOD_ESCAPE && lara->target != item\n            && info.ahead && !item->hit_status) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (raptor->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        raptor->flags &= ~1;\n        raptor->maximum_turn = M_WALK_TURN;\n\n        if (raptor->mood != MOOD_BORED) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.ahead && Random_GetControl() < M_ROAR_CHANCE) {\n            item->required_anim_state = M_STATE_WARNING;\n            item->goal_anim_state = M_STATE_STOP;\n            raptor->flags &= ~2;\n        }\n        break;\n\n    case M_STATE_RUN:\n        tilt = angle;\n        raptor->flags &= ~1;\n        raptor->maximum_turn = M_RUN_TURN;\n\n        if ((item->touch_bits & M_TOUCH) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (is_tr3 && (raptor->flags & 2) != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n            item->required_anim_state = M_STATE_WARNING;\n            raptor->flags &= ~2;\n        } else if (info.bite && info.distance < M_ATTACK_RANGE) {\n            if (item->goal_anim_state == M_STATE_RUN) {\n                if (Random_GetControl() < 0x2000) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else {\n                    item->goal_anim_state = M_STATE_ATTACK_2;\n                }\n            }\n        } else if (\n            info.ahead && raptor->mood != MOOD_ESCAPE\n            && Random_GetControl() < M_ROAR_CHANCE\n            && (!is_tr3\n                || (raptor->enemy != nullptr\n                    && raptor->enemy->object_id != O_ANIMATING_6))) {\n            item->required_anim_state = M_STATE_WARNING;\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (raptor->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            is_tr3 && raptor->mood == MOOD_ESCAPE && lara->target != item\n            && info.ahead) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_ATTACK_1:\n    case M_STATE_ATTACK_2:\n    case M_STATE_ATTACK_3:\n        raptor->maximum_turn = M_WALK_TURN;\n        tilt = angle;\n        M_Attack(item, &info);\n        break;\n\n    default:\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    if (is_tr3) {\n        Creature_Joint(item, 0, head / 2);\n        Creature_Joint(item, 1, head / 2);\n        Creature_Joint(item, 2, neck);\n        Creature_Joint(item, 3, neck);\n    } else {\n        Creature_Head(item, head);\n    }\n    Creature_Animate(item_num, angle, tilt);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = M_HITPOINTS;\n    obj->pivot_length = M_PIVOT_LENGTH;\n    obj->radius = M_RADIUS;\n    obj->smartness = M_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 21)->rot.y = true;\n    if (g_TRVersion >= 3) {\n        Object_GetBone(obj, 20)->rot.y = true;\n        Object_GetBone(obj, 23)->rot.y = true;\n        Object_GetBone(obj, 25)->rot.y = true;\n    }\n}\n\nREGISTER_OBJECT(O_RAPTOR, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/rat.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n#define RAT_BITE_DAMAGE 20\n#define RAT_CHARGE_DAMAGE 20\n#define RAT_TOUCH 0x300018F\n#define RAT_DIE_ANIM 8\n#define RAT_RUN_TURN (DEG_1 * 6) // = 1092\n#define RAT_BITE_RANGE SQUARE(341) // = 116281\n#define RAT_CHARGE_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296\n#define RAT_POSE_CHANCE 256\n#define RAT_HITPOINTS 5\n#define RAT_RADIUS (WALL_L / 5) // = 204\n#define RAT_SMARTNESS 0x2000\n\n#define VOLE_DIE_ANIM 2\n#define VOLE_SWIM_TURN (DEG_1 * 3) // = 546\n#define VOLE_ATTACK_RANGE SQUARE(300) // = 90000\n\ntypedef enum {\n    RAT_STATE_EMPTY = 0,\n    RAT_STATE_STOP = 1,\n    RAT_STATE_ATTACK_2 = 2,\n    RAT_STATE_RUN = 3,\n    RAT_STATE_ATTACK_1 = 4,\n    RAT_STATE_DEATH = 5,\n    RAT_STATE_POSE = 6,\n} RAT_STATE;\n\ntypedef enum {\n    VOLE_STATE_EMPTY = 0,\n    VOLE_STATE_SWIM = 1,\n    VOLE_STATE_ATTACK = 2,\n    VOLE_STATE_DEATH = 3,\n} VOLE_STATE;\n\nstatic BITE m_RatBite = { .pos = { 0, -11, 108 }, .mesh_num = 3 };\n\nstatic const HYBRID_INFO m_RatInfo = {\n    .land.id = O_RAT,\n    .land.active_anim = RAT_STATE_EMPTY,\n    .land.death_anim = RAT_DIE_ANIM,\n    .land.death_state = RAT_STATE_DEATH,\n    .water.id = O_VOLE,\n    .water.active_anim = VOLE_STATE_EMPTY,\n    .water.death_anim = VOLE_DIE_ANIM,\n    .water.death_state = VOLE_STATE_DEATH,\n};\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        Creature_EnsureHabitat(Item_GetIndex(item), nullptr, &m_RatInfo);\n    }\n}\n\nstatic void M_ControlRat(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const rat = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != RAT_STATE_DEATH) {\n            item->current_anim_state = RAT_STATE_DEATH;\n            Item_SwitchToAnim(item, RAT_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, RAT_RUN_TURN);\n\n        switch (item->current_anim_state) {\n        case RAT_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (info.bite && info.distance < RAT_BITE_RANGE) {\n                item->goal_anim_state = RAT_STATE_ATTACK_1;\n            } else {\n                item->goal_anim_state = RAT_STATE_RUN;\n            }\n            break;\n\n        case RAT_STATE_RUN:\n            if (info.ahead && (item->touch_bits & RAT_TOUCH)) {\n                item->goal_anim_state = RAT_STATE_STOP;\n            } else if (info.bite && info.distance < RAT_CHARGE_RANGE) {\n                item->goal_anim_state = RAT_STATE_ATTACK_2;\n            } else if (info.ahead && Random_GetControl() < RAT_POSE_CHANCE) {\n                item->required_anim_state = RAT_STATE_POSE;\n                item->goal_anim_state = RAT_STATE_STOP;\n            }\n            break;\n\n        case RAT_STATE_ATTACK_1:\n            if (item->required_anim_state == RAT_STATE_EMPTY && info.ahead\n                && (item->touch_bits & RAT_TOUCH)) {\n                Creature_Effect(item, &m_RatBite, Spawn_Blood);\n                Lara_TakeDamage(RAT_BITE_DAMAGE, true);\n                item->required_anim_state = RAT_STATE_STOP;\n            }\n            break;\n\n        case RAT_STATE_ATTACK_2:\n            if (item->required_anim_state == RAT_STATE_EMPTY && info.ahead\n                && (item->touch_bits & RAT_TOUCH)) {\n                Creature_Effect(item, &m_RatBite, Spawn_Blood);\n                Lara_TakeDamage(RAT_CHARGE_DAMAGE, true);\n                item->required_anim_state = RAT_STATE_RUN;\n            }\n            break;\n\n        case RAT_STATE_POSE:\n            if (rat->mood != MOOD_BORED\n                || Random_GetControl() < RAT_POSE_CHANCE) {\n                item->goal_anim_state = RAT_STATE_STOP;\n            }\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n\n    Creature_EnsureHabitat(item_num, nullptr, &m_RatInfo);\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_ControlVole(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != VOLE_STATE_DEATH) {\n            item->current_anim_state = VOLE_STATE_DEATH;\n            Item_SwitchToAnim(item, VOLE_DIE_ANIM, 0);\n            Carrier_TestItemDrops(item_num);\n        }\n\n        Creature_Head(item, head);\n\n        Item_Animate(item);\n\n        Creature_EnsureHabitat(item_num, nullptr, &m_RatInfo);\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        angle = Creature_Turn(item, VOLE_SWIM_TURN);\n\n        switch (item->current_anim_state) {\n        case VOLE_STATE_SWIM:\n            if (info.ahead && (item->touch_bits & RAT_TOUCH)) {\n                item->goal_anim_state = VOLE_STATE_ATTACK;\n            }\n            break;\n\n        case VOLE_STATE_ATTACK:\n            if (item->required_anim_state == VOLE_STATE_EMPTY && info.ahead\n                && (item->touch_bits & RAT_TOUCH)) {\n                Creature_Effect(item, &m_RatBite, Spawn_Blood);\n                Lara_TakeDamage(RAT_BITE_DAMAGE, true);\n                item->required_anim_state = VOLE_STATE_SWIM;\n            }\n            item->goal_anim_state = VOLE_STATE_EMPTY;\n            break;\n        }\n\n        Creature_Head(item, head);\n\n        int32_t wh;\n        Creature_EnsureHabitat(item_num, &wh, &m_RatInfo);\n\n        int32_t height = item->pos.y;\n        item->pos.y = item->floor;\n\n        Creature_Animate(item_num, angle, 0);\n\n        if (height != NO_HEIGHT) {\n            if (wh - height < -STEP_L / 8) {\n                item->pos.y = height - STEP_L / 8;\n            } else if (wh - height > STEP_L / 8) {\n                item->pos.y = height + STEP_L / 8;\n            } else {\n                item->pos.y = wh;\n            }\n        }\n    }\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->initialise_func = Creature_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = RAT_HITPOINTS;\n    obj->pivot_length = 200;\n    obj->radius = RAT_RADIUS;\n    obj->smartness = RAT_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->handle_save_func = M_HandleSave;\n    Object_GetBone(obj, 1)->rot.y = true;\n}\n\nstatic void M_SetupRat(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->control_func = M_ControlRat;\n}\n\nstatic void M_SetupVole(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    M_SetupBase(obj);\n    obj->control_func = M_ControlVole;\n}\n\nREGISTER_OBJECT(O_RAT, M_SetupRat)\nREGISTER_OBJECT(O_VOLE, M_SetupVole)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/rx_worker_1.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n// clang-format off\n#define M_RADIUS          (WALL_L / 10)      // = 102\n#define M_HIT_POINTS      34\n#define M_DAMAGE          35\n#define M_ALERT_DIST      SQUARE(WALL_L)     // = 1048576\n#define M_WALK_DIST       SQUARE(WALL_L * 2) // = 4194304\n#define M_WALK_TURN       (DEG_1 * 6)        // = 1092\n#define M_RUN_TURN        (DEG_1 * 10)       // = 1820\n#define M_DUCK_TURN       DEG_1              // = 182\n#define M_SHOOT_1_CHANCE  0x2000\n#define M_SHOOT_2_CHANCE  0x4000\n#define M_DUCK_CHANCE     0x3\n#define M_DUCK_END_CHANCE 0x1F\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_WAIT,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_AIM_1,\n    M_STATE_SHOOT_1,\n    M_STATE_AIM_2,\n    M_STATE_SHOOT_2,\n    M_STATE_SHOOT_3A,\n    M_STATE_SHOOT_3B,\n    M_STATE_SHOOT_4A,\n    M_STATE_AIM_3,\n    M_STATE_AIM_4,\n    M_STATE_DEATH,\n    M_STATE_SHOOT_4B,\n    M_STATE_DUCK_START,\n    M_STATE_DUCKED,\n    M_STATE_DUCK_AIM,\n    M_STATE_DUCK_SHOOT,\n    M_STATE_DUCK_WALK,\n    M_STATE_DUCK_END,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_AIM_1      = 1,\n    M_ANIM_SHOOT_1    = 12,\n    M_ANIM_DEATH      = 14,\n    M_ANIM_WALK_STOP  = 17,\n    M_ANIM_AIM_4A     = 18,\n    M_ANIM_AIM_4B     = 19,\n    M_ANIM_RUN_STOP_1 = 27,\n    M_ANIM_RUN_STOP_2 = 28,\n    // clang-format on\n} M_ANIM;\n\nstatic const CREATURE_GUN m_Gun = {\n    .muzzle = { .pos = { 0, 160, 40 }, .mesh_num = 13 },\n    .tr3_enemy_flash = true,\n    .tr3_flash = { .pos = { 0, 192, 40 }, .mesh_num = 13 },\n    .tr3_enemy_weapon_flags = 0,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_90,\n};\n\nstatic void M_FireFinalShot(\n    ITEM *const item, int16_t *const head, int16_t *const torso_y)\n{\n    if (!Item_TestFrameEqual(item, 47)) {\n        return;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n    if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) {\n        return;\n    }\n\n    *head = info.angle;\n    *torso_y = info.angle;\n    Creature_Shoot(item, &info, &m_Gun, info.angle, M_DAMAGE * 3);\n    Sound_Effect(SFX_LONDON_SWAT_FIRE, &item->pos, SPM_NORMAL);\n}\n\nstatic bool M_IsNearCover(const ITEM *const item, const AI_INFO *const info)\n{\n    const XYZ_32 pos =\n        XYZ_32_OffsetYaw(item->pos, item->rot.y + info->angle, WALL_L);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    return item->pos.y > height + STEPUP_HEIGHT\n        && item->pos.y < height + STEPUP_HEIGHT * 3\n        && info->distance > M_ALERT_DIST;\n}\n\nstatic bool M_ShouldDuck(const ITEM *const item, const bool near_cover)\n{\n    return item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0\n        && near_cover;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        item->hit_points = 0;\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        } else {\n            M_FireFinalShot(item, &head, &torso_y);\n        }\n\n        goto finish;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        creature->enemy = lara_item;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, creature->enemy != lara_item);\n    angle = Creature_Turn(item, creature->maximum_turn);\n    const bool near_cover = M_IsNearCover(item, &lara_info);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if ((lara_info.distance < M_ALERT_DIST || item->hit_status\n         || Creature_CanSeeEnemy(item, &lara_info))\n        && (item->ai_bits & AI_FOLLOW) == 0) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    const int16_t anim_idx = Item_GetRelativeAnim(item);\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    switch (item->current_anim_state) {\n    case M_STATE_WAIT:\n        head = lara_info.angle;\n        creature->maximum_turn = 0;\n\n        if (anim_idx == M_ANIM_WALK_STOP || anim_idx == M_ANIM_RUN_STOP_1\n            || anim_idx == M_ANIM_RUN_STOP_2) {\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n        }\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->goal_anim_state = M_STATE_DUCK_START;\n        } else if (item->required_anim_state == M_STATE_DUCK_START) {\n            item->goal_anim_state = M_STATE_DUCK_START;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance > M_WALK_DIST) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                const int32_t rnd = Random_GetControl();\n                if (rnd < M_SHOOT_1_CHANCE) {\n                    item->goal_anim_state = M_STATE_SHOOT_1;\n                } else if (rnd < M_SHOOT_2_CHANCE) {\n                    item->goal_anim_state = M_STATE_SHOOT_2;\n                } else {\n                    item->goal_anim_state = M_STATE_AIM_3;\n                }\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_WALK_DIST))) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance > M_WALK_DIST\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_AIM_4;\n            } else {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (creature->mood == MOOD_BORED) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood != MOOD_ESCAPE) {\n            if (Creature_CanTargetEnemy(item, &info)\n                || ((item->ai_bits & AI_FOLLOW) != 0\n                    && (creature->reached_goal\n                        || lara_info.distance > M_WALK_DIST))) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (anim_idx == M_ANIM_SHOOT_1\n            || (anim_idx == M_ANIM_AIM_1 && frame_idx == 10)) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->required_anim_state = M_STATE_WAIT;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (item->required_anim_state == M_STATE_WAIT) {\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_2:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (frame_idx == 0) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_3A:\n    case M_STATE_SHOOT_3B:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (frame_idx == 0 || frame_idx == 11) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_AIM_4:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if ((anim_idx == M_ANIM_AIM_4A && frame_idx == 17)\n            || (anim_idx == M_ANIM_AIM_4B && frame_idx == 6)) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n                item->required_anim_state = M_STATE_WALK;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n\n        if (info.distance < M_WALK_DIST) {\n            item->required_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_SHOOT_4A:\n    case M_STATE_SHOOT_4B:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (item->required_anim_state == M_STATE_WALK) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        if (frame_idx == 16\n            && !Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        if (info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_DUCKED:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = 0;\n\n        if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_DUCK_AIM;\n        } else if (\n            item->hit_status || !near_cover\n            || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) {\n            item->goal_anim_state = M_STATE_DUCK_END;\n        } else {\n            item->goal_anim_state = M_STATE_DUCK_WALK;\n        }\n        break;\n\n    case M_STATE_DUCK_AIM:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_DUCK_TURN;\n\n        if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_DUCK_SHOOT;\n        } else {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    case M_STATE_DUCK_SHOOT:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n\n        if (frame_idx == 0) {\n            if (!Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE)\n                || (Random_GetControl() & 7) == 0) {\n                item->goal_anim_state = M_STATE_DUCKED;\n            }\n        }\n        break;\n\n    case M_STATE_DUCK_WALK:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (Creature_CanTargetEnemy(item, &info) || item->hit_status\n            || !near_cover\n            || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    case M_STATE_DUCK_END:\n        if (ABS(info.angle) < M_WALK_TURN) {\n            item->rot.y += info.angle;\n        } else if (info.angle < 0) {\n            item->rot.y -= M_WALK_TURN;\n        } else {\n            item->rot.y += M_WALK_TURN;\n        }\n        break;\n    }\n\nfinish:\n    CLAMP(torso_y, -DEG_45, DEG_45);\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_RX_WORKER_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/rx_worker_2.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n\n// clang-format off\n#define M_RADIUS      (WALL_L / 10) // = 102\n#define M_HIT_POINTS  30\n#define M_DAMAGE      28\n#define M_ALERT_DIST  SQUARE(WALL_L)     // = 1048576\n#define M_RUN_DIST    SQUARE(WALL_L * 2) // = 4194304\n#define M_SHOOT_DIST  SQUARE(WALL_L * 3) // = 9437184\n#define M_RUN_TURN    (DEG_1 * 10)   // = 1820\n#define M_WALK_TURN   (DEG_1 * 5) // = 910\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_WAIT,\n    M_STATE_SHOOT_1,\n    M_STATE_SHOOT_2,\n    M_STATE_DEATH,\n    M_STATE_AIM_1,\n    M_STATE_AIM_2,\n    M_STATE_AIM_3,\n    M_STATE_SHOOT_3,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STOP         = 12,\n    M_ANIM_WALK_TO_STOP = 17,\n    M_ANIM_DEATH        = 19,\n    // clang-format on\n} M_ANIM;\n\nstatic const CREATURE_GUN m_Gun = {\n    .muzzle = { .pos = { 0, 400, 64 }, .mesh_num = 7 },\n    .tr3_enemy_flash = true,\n    .tr3_flash = { .pos = { 0, 400, 64 }, .mesh_num = 7 },\n    .tr3_enemy_weapon_flags = 1,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_90,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Creature_Initialise(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n    item->goal_anim_state = M_STATE_STOP;\n}\n\nstatic void M_FireFinalShot(\n    ITEM *const item, int16_t *const head, int16_t *const torso_y)\n{\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    if (frame_idx <= 3 || frame_idx >= 31 || (item->frame_num & 3) != 0) {\n        return;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    *head = info.angle;\n    *torso_y = info.angle;\n    Creature_Shoot(item, &info, &m_Gun, 0, 0);\n    Sound_Effect(SFX_LONDON_SWAT_FIRE, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->flags = (Random_GetControl() & 3) == 0 ? 1 : 0;\n        } else if (creature->flags != 0) {\n            M_FireFinalShot(item, &head, &torso_y);\n        }\n\n        goto finish;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        creature->enemy = lara_item;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, creature->enemy != lara_item);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if ((lara_info.distance < M_ALERT_DIST || item->hit_status\n         || Creature_CanSeeEnemy(item, &lara_info))\n        && (item->ai_bits & AI_FOLLOW) == 0) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n\n        if (Item_TestAnimEqual(item, M_ANIM_WALK_TO_STOP)) {\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n        }\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance >= M_SHOOT_DIST\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (Random_GetControl() < 0x4000) {\n                item->goal_anim_state = M_STATE_AIM_1;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_3;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood != MOOD_BORED && info.distance > M_RUN_DIST) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (\n            (item->ai_bits & AI_GUARD) != 0\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance < M_SHOOT_DIST\n                || info.zone_num != info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_2;\n            }\n        } else if (creature->mood == MOOD_BORED) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.distance > M_RUN_DIST) {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood != MOOD_ESCAPE) {\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                creature->mood == MOOD_BORED\n                || (creature->mood == MOOD_STALK\n                    && (item->ai_bits & AI_FOLLOW) == 0\n                    && info.distance < M_RUN_DIST)) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n\n    case M_STATE_WAIT:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                if (item->current_anim_state == M_STATE_STOP) {\n                    item->goal_anim_state = M_STATE_WAIT;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            }\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_SHOOT_1;\n        } else if (creature->mood != MOOD_BORED || !info.ahead) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n    case M_STATE_SHOOT_2:\n    case M_STATE_SHOOT_3:\n        if (item->current_anim_state == M_STATE_SHOOT_3\n            && item->goal_anim_state != M_STATE_STOP\n            && (creature->mood == MOOD_ESCAPE || info.distance > M_SHOOT_DIST\n                || !Creature_CanTargetEnemy(item, &info))) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (creature->flags != 0) {\n            creature->flags--;\n        } else {\n            Creature_Shoot(item, &info, &m_Gun, torso_y, M_DAMAGE);\n            creature->flags = 5;\n        }\n        break;\n\n    case M_STATE_AIM_1:\n    case M_STATE_AIM_3:\n        creature->flags = 0;\n\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state =\n                    item->current_anim_state == M_STATE_AIM_1 ? M_STATE_SHOOT_1\n                                                              : M_STATE_SHOOT_3;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        }\n        break;\n\n    case M_STATE_AIM_2:\n        creature->flags = 0;\n\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = M_STATE_SHOOT_2;\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = M_HIT_POINTS;\n    obj->radius = M_RADIUS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.x = true;\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_RX_WORKER_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/rx_worker_3.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\n// clang-format off\n#define M_RADIUS          (WALL_L / 10)      // = 102\n#define M_HIT_POINTS      36\n#define M_MAX_FLAME_SPEED 40\n#define M_ALERT_DIST      SQUARE(WALL_L)     // = 1048576\n#define M_ATTACK_DIST     SQUARE(WALL_L * 4) // = 16777216\n#define M_WALK_TURN       (DEG_1 * 5)        // = 910\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_WAIT,\n    M_STATE_SHOOT_1,\n    M_STATE_SHOOT_2,\n    M_STATE_DEATH,\n    M_STATE_AIM_1,\n    M_STATE_AIM_2,\n    M_STATE_AIM_3,\n    M_STATE_SHOOT_3,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STOP  = 12,\n    M_ANIM_DEATH = 19,\n    // clang-format on\n} M_ANIM;\n\nstatic const BITE m_Gun = {\n    .pos = { 0, 340, 64 },\n    .mesh_num = 7,\n};\n\nstatic void M_TriggerPilotFlame(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.g = spark->src_color.r;\n    spark->src_color.b = (Random_GetControl() & 0x3F) - 64;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) - 64;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 0x80;\n    spark->dst_color.b = 32;\n    spark->fade_to_black = 4;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 20;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.y = -(Random_GetControl() & 3);\n    spark->vel.z = (Random_GetControl() & 0x1F) - 16;\n    spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n        | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->effect_num = Item_GetIndex(item);\n    spark->node_num = 0;\n    spark->friction = 4;\n    spark->gravity = -2 - (Random_GetControl() & 3);\n    spark->max_y_vel = -4 - (Random_GetControl() & 3);\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 0;\n    spark->dst_size.width = (Random_GetControl() & 7) + 32;\n    spark->src_size.width = spark->dst_size.width >> 1;\n    spark->size.width = spark->src_size.width;\n    spark->src_size.height = spark->src_size.width;\n    spark->size.height = spark->src_size.width;\n    spark->dst_size.height = spark->dst_size.width;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerFlameSparks(\n    const XYZ_32 pos, const XYZ_32 vel, const int16_t effect_num)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.g = spark->src_color.r;\n    spark->src_color.b = (Random_GetControl() & 0x3F) - 64;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) - 64;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 0x80;\n    spark->dst_color.b = 32;\n\n    if (vel.x != 0 || vel.y != 0 || vel.z != 0) {\n        spark->col_fade_speed = 6;\n        spark->fade_to_black = 2;\n        spark->life = (Random_GetControl() & 1) + 12;\n    } else {\n        spark->col_fade_speed = 8;\n        spark->fade_to_black = 16;\n        spark->life = (Random_GetControl() & 3) + 20;\n    }\n\n    spark->s_life = spark->life;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = vel.x + (Random_GetControl() & 0xF) - 16;\n    spark->vel.y = vel.y;\n    spark->vel.z = vel.z + (Random_GetControl() & 0xF) - 16;\n    spark->friction = 0;\n\n    if ((Random_GetControl() & 1) != 0) {\n        if (effect_num < 0) {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n                | SPARK_F_SCALE;\n        } else {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n                | SPARK_F_SPRITE | SPARK_F_SCALE;\n        }\n\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = +16 + (Random_GetControl() & 0xF);\n        }\n    } else if (effect_num < 0) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->max_y_vel = 0;\n    spark->effect_num = effect_num;\n    spark->gravity = 0;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    const int32_t size = (Random_GetControl() & 0x1F) + 64;\n    if (vel.x != 0 || vel.y != 0 || vel.z != 0) {\n        spark->size.width = size >> 5;\n        spark->src_size.width = size >> 5;\n        spark->size.height = size >> 5;\n        spark->src_size.height = size >> 5;\n\n        if (effect_num == -2) {\n            spark->scalar = 2;\n        } else {\n            spark->scalar = 3;\n        }\n\n        spark->dst_size.width = size >> 1;\n        spark->dst_size.height = size >> 1;\n    } else {\n        spark->scalar = 4;\n        spark->size.width = size >> 4;\n        spark->src_size.width = size >> 4;\n        spark->size.height = size >> 4;\n        spark->src_size.height = size >> 4;\n        spark->dst_size.width = size >> 1;\n        spark->dst_size.height = size >> 1;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerFlamethrower(const ITEM *const item, const int16_t speed)\n{\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    XYZ_32 pos_1 = m_Gun.pos;\n    Collide_GetJointAbsPosition(item, &pos_1, m_Gun.mesh_num);\n\n    XYZ_32 pos_2 = m_Gun.pos;\n    pos_2.y <<= 1;\n    Collide_GetJointAbsPosition(item, &pos_2, m_Gun.mesh_num);\n\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        pos_2.x - pos_1.x, pos_2.y - pos_1.y, pos_2.z - pos_1.z, angles);\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = pos_1;\n    effect->rot.x = angles[1];\n    effect->rot.y = angles[0];\n    effect->speed = speed << 2;\n    effect->object_id = O_MISSILE_FLAME;\n    effect->counter = 20;\n    effect->flag1 = 0;\n\n    M_TriggerFlameSparks((XYZ_32) {}, (XYZ_32) {}, effect_num);\n\n    for (int32_t i = 0; i < 2; i++) {\n        const int32_t s = Random_GetControl() % (speed << 2) + 32;\n        const int32_t r = (s * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n        const XYZ_32 vel = {\n            .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT,\n            .y = -((s * Math_Sin(effect->rot.x)) >> W2V_SHIFT),\n            .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT,\n        };\n        M_TriggerFlameSparks(\n            effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -1);\n    }\n\n    {\n        const int32_t r = ((speed << 1) * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n        const XYZ_32 vel = {\n            .x = (r * Math_Sin(effect->rot.y)) >> W2V_SHIFT,\n            .y = -(((speed << 1) * Math_Sin(effect->rot.x)) >> W2V_SHIFT),\n            .z = (r * Math_Cos(effect->rot.y)) >> W2V_SHIFT,\n        };\n        M_TriggerFlameSparks(\n            effect->pos, (XYZ_32) { vel.x << 5, vel.y << 5, vel.z << 5 }, -2);\n    }\n}\n\nstatic void M_TriggerLights(const ITEM *const item)\n{\n    XYZ_32 pos = m_Gun.pos;\n    Collide_GetJointAbsPosition(item, &pos, m_Gun.mesh_num);\n\n    const int32_t rnd = Random_GetControl();\n    if (item->current_anim_state == M_STATE_SHOOT_2\n        || item->current_anim_state == M_STATE_SHOOT_3) {\n        const RGB_888 color = {\n            .r = 255 - ((rnd >> 4) & 0x1F),\n            .g = 192 - ((rnd >> 6) & 0x1F),\n            .b = rnd & 0x3F,\n        };\n        Output_AddDynamicLightRGB(pos, (rnd & 3) + 10, color);\n    } else {\n        const RGB_888 color = {\n            .r = 192 - ((rnd >> 4) & 0x1F),\n            .g = 128 - ((rnd >> 6) & 0x1F),\n            .b = rnd & 0x1F,\n        };\n        Output_AddDynamicLightRGB(pos, (rnd & 3) + 6, color);\n        M_TriggerPilotFlame(item);\n    }\n}\n\nstatic void M_CalculateEnemy(ITEM *const item)\n{\n    CREATURE *const worker = item->creature_data;\n    if (Creature_IsHostile(item)) {\n        worker->enemy = Lara_GetItem();\n        return;\n    }\n\n    worker->enemy = nullptr;\n    int32_t best_distance = INT32_MAX;\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        const CREATURE *const creature = LOT_GetBaddieSlot(i);\n        if (creature->item_num == NO_ITEM || creature == worker) {\n            continue;\n        }\n\n        const ITEM *const candidate = Item_Get(creature->item_num);\n        if (candidate->object_id == O_LARA\n            || candidate->object_id == O_RX_WORKER_2\n            || candidate->object_id == O_RX_WORKER_3) {\n            continue;\n        }\n\n        const int32_t dx = candidate->pos.x - item->pos.x;\n        const int32_t dz = candidate->pos.z - item->pos.z;\n        const int32_t distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n        if (distance < best_distance) {\n            worker->enemy = (ITEM *)candidate;\n            best_distance = distance;\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    M_TriggerLights(item);\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n        goto finish;\n    }\n\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        M_CalculateEnemy(item);\n    }\n\n    // Enforce the following state to avoid Creature_AIInfo resetting ahead,\n    // bite and distance when the creature is friendly.\n    ITEM *const lara_item = Lara_GetItem();\n    const bool hurt_by_lara = creature->hurt_by_lara;\n    ITEM *const enemy = creature->enemy;\n    if (enemy == nullptr) {\n        creature->enemy = lara_item;\n        creature->hurt_by_lara = true;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    creature->enemy = enemy;\n    creature->hurt_by_lara = hurt_by_lara;\n\n    AI_INFO lara_info = {};\n    const bool is_ally = !Creature_IsHostile(item);\n    if (creature->enemy == lara_item) {\n        lara_info.angle = info.angle;\n        lara_info.distance = info.distance;\n        if (!creature->hurt_by_lara && is_ally) {\n            creature->enemy = nullptr;\n        }\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        info.x_angle -= DEG_45 / 4;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, false);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (item->hit_status\n        || (!is_ally\n            && (lara_info.distance < M_ALERT_DIST\n                || Creature_CanSeeEnemy(item, &lara_info)))) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_AMERICAN_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) {\n            if (info.distance >= M_ATTACK_DIST) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_3;\n            }\n        } else if (\n            creature->mood == MOOD_BORED && info.ahead\n            && (Random_GetControl() & 0xFF) == 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (\n            creature->mood == MOOD_ATTACK\n            || (Random_GetControl() & 0xFF) == 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n            item->current_anim_state = M_STATE_STOP;\n            item->goal_anim_state = M_STATE_STOP;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (\n            Creature_CanTargetEnemy(item, &info)\n            && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) {\n            if (info.distance >= M_ATTACK_DIST) {\n                item->goal_anim_state = M_STATE_AIM_2;\n            } else {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (creature->mood == MOOD_BORED && info.ahead) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WAIT:\n        head = lara_info.angle;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (\n            (Creature_CanTargetEnemy(item, &info)\n             && info.distance < M_ATTACK_DIST\n             && (enemy != lara_item || creature->hurt_by_lara || !is_ally))\n            || creature->mood != MOOD_BORED\n            || (Random_GetControl() & 0xFF) == 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_AIM_2:\n    case M_STATE_AIM_3:\n        creature->flags = 0;\n\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n\n            if (Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_ATTACK_DIST\n                && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) {\n                item->goal_anim_state =\n                    item->current_anim_state == M_STATE_AIM_2 ? M_STATE_SHOOT_2\n                                                              : M_STATE_SHOOT_3;\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n\n    case M_STATE_SHOOT_2:\n    case M_STATE_SHOOT_3:\n        if (creature->flags < M_MAX_FLAME_SPEED) {\n            creature->flags += (creature->flags >> 2) + 1;\n        }\n\n        const M_STATE stop_state = item->current_anim_state == M_STATE_SHOOT_2\n            ? M_STATE_WALK\n            : M_STATE_STOP;\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n\n            if (Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_ATTACK_DIST\n                && (enemy != lara_item || creature->hurt_by_lara || !is_ally)) {\n                item->goal_anim_state = item->current_anim_state;\n            } else {\n                item->goal_anim_state = stop_state;\n            }\n        } else {\n            item->goal_anim_state = stop_state;\n        }\n\n        const int16_t speed = creature->flags < M_MAX_FLAME_SPEED\n            ? creature->flags\n            : ((Random_GetControl() & 0x1F) + 12);\n        M_TriggerFlamethrower(item, speed);\n        Sound_Effect(SFX_FLAME_THROWER_LOOP, &item->pos, SPM_NORMAL);\n        if (enemy != nullptr) {\n            const OBJECT *const obj = Object_Get(enemy->object_id);\n            if (obj->event_func != nullptr) {\n                obj->event_func(enemy, OBJECT_EVENT_BURNT, nullptr);\n            }\n        }\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.x = true;\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_RX_WORKER_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/security_guard.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n// clang-format off\n#define M_RADIUS          (WALL_L / 10)      // = 102\n#define M_HIT_POINTS      28\n#define M_DAMAGE          32\n#define M_ALERT_DIST      SQUARE(WALL_L)     // = 1048576\n#define M_ALERT_HEIGHT    (WALL_L * 2)       // = 2048\n#define M_WALK_DIST       SQUARE(WALL_L * 2) // = 4194304\n#define M_WALK_TURN       (DEG_1 * 5)        // = 910\n#define M_RUN_TURN        (DEG_1 * 10)       // = 1820\n#define M_DUCK_TURN       DEG_1              // = 182\n#define M_SHOOT_1_CHANCE  0x2000\n#define M_SHOOT_2_CHANCE  0x4000\n#define M_DUCK_CHANCE     0x3\n#define M_DUCK_END_CHANCE 0x1F\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_WAIT,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_AIM_1,\n    M_STATE_SHOOT_1,\n    M_STATE_AIM_2,\n    M_STATE_SHOOT_2,\n    M_STATE_SHOOT_3A,\n    M_STATE_SHOOT_3B,\n    M_STATE_SHOOT_4A,\n    M_STATE_AIM_3,\n    M_STATE_AIM_4,\n    M_STATE_DEATH,\n    M_STATE_SHOOT_4B,\n    M_STATE_DUCK_START,\n    M_STATE_DUCKED,\n    M_STATE_DUCK_AIM,\n    M_STATE_DUCK_SHOOT,\n    M_STATE_DUCK_WALK,\n    M_STATE_DUCK_END,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_SHOOT_1    = 1,\n    M_ANIM_AIM_1      = 12,\n    M_ANIM_DEATH      = 14,\n    M_ANIM_WALK_STOP  = 17,\n    M_ANIM_AIM_4A     = 18,\n    M_ANIM_AIM_4B     = 19,\n    M_ANIM_RUN_STOP_1 = 27,\n    M_ANIM_RUN_STOP_2 = 28,\n    // clang-format on\n} M_ANIM;\n\nstatic const CREATURE_GUN m_GuardGun = {\n    .muzzle = { .pos = { 0, 160, 40 }, .mesh_num = 13 },\n    .tr3_enemy_flash = true,\n    .tr3_flash = { .pos = { 0, 192, 40 }, .mesh_num = 13 },\n    .tr3_enemy_weapon_flags = 0,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_90,\n};\n\nstatic void M_FireFinalShot(\n    ITEM *const item, int16_t *const head, int16_t *const torso_y)\n{\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    if (frame_idx != 3 && frame_idx != 28) {\n        return;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) {\n        return;\n    }\n\n    *head = info.angle;\n    *torso_y = info.angle;\n    Creature_Shoot(item, &info, &m_GuardGun, info.angle, M_DAMAGE * 2);\n    Sound_Effect(SFX_SECURITY_GUARD_FIRE, &item->pos, SPM_NORMAL);\n}\n\nstatic bool M_IsNearCover(const ITEM *const item, const AI_INFO *const info)\n{\n    const XYZ_32 pos =\n        XYZ_32_OffsetYaw(item->pos, item->rot.y + info->angle, WALL_L);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    return item->pos.y > height + STEPUP_HEIGHT\n        && item->pos.y < height + STEPUP_HEIGHT * 3\n        && info->distance > M_ALERT_DIST;\n}\n\nstatic bool M_ShouldDuck(const ITEM *const item, const bool near_cover)\n{\n    return item->hit_status && (Random_GetControl() & M_DUCK_CHANCE) == 0\n        && near_cover;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        item->hit_points = 0;\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        } else if ((Random_GetControl() & 1) != 0) {\n            M_FireFinalShot(item, &head, &torso_y);\n        }\n\n        goto finish;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        creature->enemy = lara_item;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, creature->enemy != lara_item);\n    angle = Creature_Turn(item, creature->maximum_turn);\n    const bool near_cover = M_IsNearCover(item, &lara_info);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if (item->hit_status\n        || ((lara_info.distance < M_ALERT_DIST\n             || Creature_CanSeeEnemy(item, &lara_info))\n            && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT)) {\n        if (!creature->alerted) {\n            Sound_Effect(SFX_ENGLISH_HOY, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    const int16_t anim_idx = Item_GetRelativeAnim(item);\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n\n    switch (item->current_anim_state) {\n    case M_STATE_WAIT:\n        head = lara_info.angle;\n        creature->maximum_turn = 0;\n\n        if (anim_idx == M_ANIM_WALK_STOP || anim_idx == M_ANIM_RUN_STOP_1\n            || anim_idx == M_ANIM_RUN_STOP_2) {\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n        }\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->goal_anim_state = M_STATE_DUCK_START;\n        } else if (item->required_anim_state == M_STATE_DUCK_START) {\n            item->goal_anim_state = M_STATE_DUCK_START;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance > M_WALK_DIST) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                const int32_t rnd = Random_GetControl();\n                if (rnd < M_SHOOT_1_CHANCE) {\n                    item->goal_anim_state = M_STATE_SHOOT_1;\n                } else if (rnd < M_SHOOT_2_CHANCE) {\n                    item->goal_anim_state = M_STATE_SHOOT_2;\n                } else {\n                    item->goal_anim_state = M_STATE_AIM_3;\n                }\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_WALK_DIST))) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n            head = 0;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance > M_WALK_DIST\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_AIM_4;\n            } else {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (creature->mood == MOOD_BORED) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (near_cover && (lara->target == item || item->hit_status)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        } else if (creature->mood != MOOD_ESCAPE) {\n            if (Creature_CanTargetEnemy(item, &info)\n                || ((item->ai_bits & AI_FOLLOW) != 0\n                    && (creature->reached_goal\n                        || lara_info.distance > M_WALK_DIST))) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n\n    case M_STATE_AIM_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (anim_idx == M_ANIM_AIM_1\n            || (anim_idx == M_ANIM_SHOOT_1 && frame_idx == 10)) {\n            if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) {\n                item->required_anim_state = M_STATE_WAIT;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (item->required_anim_state == M_STATE_WAIT) {\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_2:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (frame_idx == 0) {\n            if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_SHOOT_3A:\n    case M_STATE_SHOOT_3B:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (frame_idx == 0 || frame_idx == 11) {\n            if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n        break;\n\n    case M_STATE_AIM_4:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if ((anim_idx == M_ANIM_AIM_4A && frame_idx == 16)\n            || (anim_idx == M_ANIM_AIM_4B && frame_idx == 6)) {\n            if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        } else if (M_ShouldDuck(item, near_cover)) {\n            item->required_anim_state = M_STATE_DUCK_START;\n            item->goal_anim_state = M_STATE_WAIT;\n        }\n\n        if (info.distance < M_WALK_DIST) {\n            item->required_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_SHOOT_4A:\n    case M_STATE_SHOOT_4B:\n        if (info.ahead) {\n            torso_x = info.x_angle;\n            torso_y = info.angle;\n        }\n\n        if (item->required_anim_state == M_STATE_WALK) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        if (frame_idx == 16\n            && !Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n\n        if (info.distance < M_WALK_DIST) {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_DUCKED:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = 0;\n\n        if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_DUCK_AIM;\n        } else if (\n            item->hit_status || !near_cover\n            || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) {\n            item->goal_anim_state = M_STATE_DUCK_END;\n        } else {\n            item->goal_anim_state = M_STATE_DUCK_WALK;\n        }\n        break;\n\n    case M_STATE_DUCK_AIM:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n        creature->maximum_turn = M_DUCK_TURN;\n\n        if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_DUCK_SHOOT;\n        } else {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    case M_STATE_DUCK_SHOOT:\n        if (info.ahead) {\n            torso_y = info.angle;\n        }\n\n        if (frame_idx != 0) {\n            break;\n        }\n\n        if (!Creature_Shoot(item, &info, &m_GuardGun, torso_y, M_DAMAGE)\n            || (Random_GetControl() & 7) == 0) {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    case M_STATE_DUCK_WALK:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_WALK_TURN;\n\n        if (Creature_CanTargetEnemy(item, &info) || item->hit_status\n            || !near_cover\n            || (info.ahead && (Random_GetControl() & M_DUCK_END_CHANCE) == 0)) {\n            item->goal_anim_state = M_STATE_DUCKED;\n        }\n        break;\n\n    default:\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_SECURITY_GUARD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/shark.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define SHARK_HITPOINTS       30\n#define SHARK_TOUCH_BITS      0b00110100'00000000 // = 0x3400\n#define SHARK_RADIUS          (WALL_L / 3) // = 341\n#define SHARK_BITE_DAMAGE     400\n#define SHARK_SWIM_2_RANGE    SQUARE(WALL_L * 3) // = 9437184\n#define SHARK_ATTACK_1_RANGE  SQUARE(WALL_L * 3 / 4) // = 589824\n#define SHARK_ATTACK_2_RANGE  SQUARE(WALL_L * 4 / 3) // = 1863225\n#define SHARK_SWIM_1_TURN     (DEG_1 / 2) // = 91\n#define SHARK_SWIM_2_TURN     (DEG_1 * 2) // = 364\n#define SHARK_ATTACK_1_CHANCE 0x800\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    SHARK_STATE_STOP     = 0,\n    SHARK_STATE_SWIM_1   = 1,\n    SHARK_STATE_SWIM_2   = 2,\n    SHARK_STATE_ATTACK_1 = 3,\n    SHARK_STATE_ATTACK_2 = 4,\n    SHARK_STATE_DEATH    = 5,\n    SHARK_STATE_KILL     = 6,\n    // clang-format on\n} SHARK_STATE;\n\ntypedef enum {\n    // clang-format off\n    SHARK_ANIM_DEATH = 4,\n    SHARK_ANIM_KILL  = 19,\n    // clang-format on\n} SHARK_ANIM;\n\nstatic const BITE m_SharkBite = {\n    .pos = { .x = 17, .y = -22, .z = 344 },\n    .mesh_num = 12,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const bool lara_was_alive = lara_item->hit_points > 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != SHARK_STATE_DEATH) {\n            Item_SwitchToAnim(item, SHARK_ANIM_DEATH, 0);\n            item->current_anim_state = SHARK_STATE_DEATH;\n            Carrier_TestItemDrops(item_num);\n        }\n        Creature_Float(item_num);\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        int16_t head = 0;\n        int16_t angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case SHARK_STATE_STOP:\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n            if (info.ahead && info.distance < SHARK_ATTACK_1_RANGE\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = SHARK_STATE_ATTACK_1;\n            } else {\n                item->goal_anim_state = SHARK_STATE_SWIM_1;\n            }\n            break;\n\n        case SHARK_STATE_SWIM_1:\n            creature->maximum_turn = SHARK_SWIM_1_TURN;\n            if (creature->mood == MOOD_BORED) {\n            } else if (info.ahead && info.distance < SHARK_ATTACK_1_RANGE) {\n                item->goal_anim_state = SHARK_STATE_STOP;\n            } else if (\n                creature->mood == MOOD_ESCAPE\n                || info.distance > SHARK_SWIM_2_RANGE || !info.ahead) {\n                item->goal_anim_state = SHARK_STATE_SWIM_2;\n            }\n            break;\n\n        case SHARK_STATE_SWIM_2:\n            creature->flags = 0;\n            creature->maximum_turn = SHARK_SWIM_2_TURN;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = SHARK_STATE_SWIM_1;\n            } else if (creature->mood == MOOD_ESCAPE) {\n            } else if (\n                info.ahead && info.distance < SHARK_ATTACK_2_RANGE\n                && info.zone_num == info.enemy_zone_num) {\n                if (Random_GetControl() < SHARK_ATTACK_1_CHANCE) {\n                    item->goal_anim_state = SHARK_STATE_STOP;\n                } else if (info.distance < SHARK_ATTACK_1_RANGE) {\n                    item->goal_anim_state = SHARK_STATE_ATTACK_2;\n                }\n            }\n            break;\n\n        case SHARK_STATE_ATTACK_1:\n        case SHARK_STATE_ATTACK_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n\n            if (creature->flags == 0\n                && (item->touch_bits & SHARK_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(SHARK_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_SharkBite, Spawn_Blood);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n\n        if (lara_was_alive && lara_item->hit_points <= 0) {\n            Creature_SpecialKill(\n                item, SHARK_ANIM_KILL, SHARK_STATE_KILL, LS_EXTRA_SHARK_KILL);\n        } else if (item->current_anim_state == SHARK_STATE_KILL) {\n            Item_Animate(item);\n        } else {\n            Creature_Head(item, head);\n            Creature_Animate(item_num, angle, 0);\n            Creature_Underwater(item, SHARK_RADIUS);\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->draw_func = Object_DrawUnclippedItem;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = SHARK_HITPOINTS;\n    obj->radius = SHARK_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 200;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n    obj->lot_setup.block_mask = BOX_BLOCKABLE;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 9)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_SHARK, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/shiva.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/draw.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_WALK_TURN (4 * DEG_1)\n#define M_PINCER_RANGE SQUARE(WALL_L * 5 / 4)\n#define M_CHOPPER_RANGE SQUARE(WALL_L * 4 / 3)\n// clang-format on\n\nstatic BITE m_RightBlade = { .pos = { 0, 0, 920 }, .mesh_num = 22 };\nstatic BITE m_LeftBlade = { .pos = { 0, 0, 920 }, .mesh_num = 13 };\n\ntypedef enum {\n    M_STATE_WAIT,\n    M_STATE_WALK,\n    M_STATE_WAIT_DEF,\n    M_STATE_WALK_DEF,\n    M_STATE_START,\n    M_STATE_PINCER,\n    M_STATE_KILL,\n    M_STATE_CHOPPER,\n    M_STATE_WALK_BACK,\n    M_STATE_DEATH\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 22,\n    M_ANIM_START_ANIM = 14,\n    M_ANIM_KILL_ANIM = 18,\n} M_ANIM;\n\ntypedef struct {\n    int32_t effect_mesh;\n} M_PRIV;\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    if (item->current_anim_state != M_STATE_WAIT_DEF\n        && item->current_anim_state != M_STATE_WALK_DEF\n        && item->current_anim_state != M_STATE_START) {\n        return true;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = item->pos.x - lara_item->pos.x;\n    const int32_t dz = item->pos.z - lara_item->pos.z;\n    const int16_t angle = DEG_180 - item->rot.y + Math_Atan(dz, dx);\n    if (angle <= -DEG_90 || angle >= DEG_90) {\n        return true;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    // XXX: This uses Lara's currently equipped gun. If she swaps weapons before\n    // a projectile impact resolves, this can differ from the projectile weapon.\n    if (lara->gun_type == LGT_ROCKET || lara->gun_type == LGT_GRENADE\n        || lara->gun_type == LGT_HARPOON) {\n        return true;\n    }\n\n    return false;\n}\n\nstatic void M_TriggerSmoke(const XYZ_32 pos, const bool uw)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n\n    if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    if (uw) {\n        spark->src_color.r = 0;\n        spark->src_color.g = 0;\n        spark->src_color.b = 0;\n        spark->dst_color.r = 192;\n        spark->dst_color.g = 192;\n        spark->dst_color.b = 208;\n    } else {\n        spark->src_color.r = 144;\n        spark->src_color.g = 144;\n        spark->src_color.b = 144;\n        spark->dst_color.r = 64;\n        spark->dst_color.g = 64;\n        spark->dst_color.b = 64;\n    }\n\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 64;\n    spark->life = (Random_GetControl() & 0x1F) + 96;\n    spark->s_life = spark->life;\n\n    if (uw) {\n        spark->draw_type = DRAW_BLEND_ADD;\n    } else {\n        spark->draw_type = DRAW_BLEND_SUB;\n    }\n\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n    spark->vel.y = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n\n    if (uw) {\n        spark->friction = 20;\n        spark->pos.y += 32;\n        spark->vel.y >>= 4;\n    } else {\n        spark->friction = 6;\n    }\n\n    spark->flags =\n        SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->rot_angle = Random_GetControl() & 0xFFF;\n\n    if (Random_GetControl() & 1) {\n        spark->rot_add = -16 - (Random_GetControl() & 0xF);\n    } else {\n        spark->rot_add = (Random_GetControl() & 0xF) + 16;\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    if (uw) {\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n    } else {\n        spark->gravity = -3 - (Random_GetControl() & 3);\n        spark->max_y_vel = -4 - (Random_GetControl() & 3);\n    }\n\n    spark->dst_size.width = (Random_GetControl() & 0x1F) + 128;\n    spark->size.width = spark->dst_size.width >> 2;\n    spark->src_size.width = spark->size.width >> 2;\n\n    spark->dst_size.height =\n        spark->dst_size.width + (Random_GetControl() & 0x1F) + 32;\n    spark->size.height = spark->dst_size.height >> 3;\n    spark->src_size.height = spark->size.height;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Damage(\n    ITEM *const item, CREATURE *const creature, const int32_t damage)\n{\n    if (creature->flags == 0 && item->touch_bits & 0x2400000) {\n        Lara_TakeDamage(damage, true);\n        Creature_Effect(item, &m_RightBlade, Spawn_Blood);\n        creature->flags = 1;\n        Sound_Effect(SFX_MACAQUE_ROLL, &item->pos, SPM_NORMAL);\n    }\n\n    if (creature->flags == 0 && item->touch_bits & 0x2400) {\n        Lara_TakeDamage(damage, true);\n        Creature_Effect(item, &m_LeftBlade, Spawn_Blood);\n        creature->flags = 1;\n        Sound_Effect(SFX_MACAQUE_ROLL, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"effect_mesh\", &p->effect_mesh));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"effect_mesh\", p->effect_mesh);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (item->object_id == O_SHIVA) {\n        Item_SwitchToAnim(item, M_ANIM_START_ANIM, 0);\n        item->current_anim_state = M_STATE_START;\n        item->goal_anim_state = M_STATE_START;\n    }\n\n    item->status = IS_INACTIVE;\n    item->mesh_bits = 0;\n    p->effect_mesh = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    M_PRIV *const p = item->priv;\n\n    const bool lara_alive = lara_item->hit_points > 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n    int16_t head_y = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        if (creature->mood == MOOD_ESCAPE) {\n            creature->target.x = lara_item->pos.x;\n            creature->target.z = lara_item->pos.z;\n        }\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        if (item->current_anim_state != M_STATE_START) {\n            item->mesh_bits = INT32_MAX;\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_WAIT:\n            if (info.ahead) {\n                head_y = info.angle;\n            }\n\n            if (creature->flags < 0) {\n                creature->flags++;\n                const XYZ_32 smoke_pos = {\n                    .x = item->pos.x + (Random_GetControl() & 0x5FF) - 768,\n                    .y = item->pos.y - (Random_GetControl() & 0x5FF),\n                    .z = item->pos.z + (Random_GetControl() & 0x5FF) - 768,\n                };\n                M_TriggerSmoke(smoke_pos, true);\n            } else {\n                if (creature->flags == 1) {\n                    creature->flags = 0;\n                }\n\n                creature->maximum_turn = 0;\n\n                if (creature->mood == MOOD_ESCAPE) {\n                    int16_t room_num = item->room_num;\n                    const XYZ_32 offset = XYZ_32_OffsetYaw(\n                        item->pos, item->rot.y + DEG_180, WALL_L);\n                    const SECTOR *const sector =\n                        Room_GetSector(offset, &room_num);\n\n                    if (creature->flags != 0 || sector->box == 0x7FF\n                        || Box_GetBox(sector->box)->overlap_index\n                            & BOX_BLOCKABLE) {\n                        item->goal_anim_state = M_STATE_WAIT_DEF;\n                    } else {\n                        item->goal_anim_state = M_STATE_WALK_BACK;\n                    }\n                } else if (creature->mood == MOOD_BORED) {\n                    if (Random_GetControl() < 1024) {\n                        item->goal_anim_state = M_STATE_WALK;\n                    }\n                } else if (info.bite && info.distance < M_PINCER_RANGE) {\n                    item->goal_anim_state = M_STATE_PINCER;\n                    creature->flags = 0;\n                } else if (info.bite && info.distance < M_CHOPPER_RANGE) {\n                    item->goal_anim_state = M_STATE_CHOPPER;\n                    creature->flags = 0;\n                } else if (item->hit_status && info.ahead) {\n                    item->goal_anim_state = M_STATE_WAIT_DEF;\n                    creature->flags = 4;\n                } else {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            }\n            break;\n\n        case M_STATE_WALK:\n            if (info.ahead) {\n                head_y = info.angle;\n            }\n\n            creature->maximum_turn = M_WALK_TURN;\n\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else if (info.bite && info.distance < M_CHOPPER_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT;\n                creature->flags = 0;\n            } else if (item->hit_status) {\n                item->goal_anim_state = M_STATE_WALK_DEF;\n                creature->flags = 4;\n            }\n            break;\n\n        case M_STATE_WAIT_DEF:\n            if (info.ahead) {\n                head_y = info.angle;\n            }\n\n            creature->maximum_turn = 0;\n            if (item->hit_status || creature->mood == MOOD_ESCAPE) {\n                creature->flags = 4;\n            }\n\n            if ((info.bite && info.distance < M_CHOPPER_RANGE)\n                || (Item_GetRelativeFrame(item) == 0 && creature->flags == 0)\n                || !info.ahead) {\n                item->goal_anim_state = M_STATE_WAIT;\n                creature->flags = 0;\n            } else if (creature->flags != 0) {\n                item->goal_anim_state = M_STATE_WAIT_DEF;\n            }\n\n            if (Item_GetRelativeFrame(item) == 0 && creature->flags > 1) {\n                creature->flags -= 2;\n            }\n            break;\n\n        case M_STATE_WALK_DEF:\n            if (info.ahead) {\n                head_y = info.angle;\n            }\n\n            creature->maximum_turn = M_WALK_TURN;\n            if (item->hit_status) {\n                creature->flags = 4;\n            }\n\n            if ((info.bite && info.distance < M_PINCER_RANGE)\n                || (Item_GetRelativeFrame(item) == 0 && creature->flags == 0)) {\n                item->goal_anim_state = M_STATE_WALK;\n                creature->flags = 0;\n            } else if (creature->flags != 0) {\n                item->goal_anim_state = M_STATE_WALK_DEF;\n            }\n\n            if (Item_GetRelativeFrame(item) == 0) {\n                creature->flags = 0;\n            }\n            break;\n\n        case M_STATE_START:\n            creature->maximum_turn = 0;\n\n            if (creature->flags != 0) {\n                creature->flags--;\n            } else {\n                if (item->mesh_bits == 0) {\n                    p->effect_mesh = 0;\n                }\n\n                item->mesh_bits <<= 1;\n                item->mesh_bits |= 1;\n                creature->flags = 1;\n                XYZ_32 smoke_pos = { 0, 0, 256 };\n                Collide_GetJointAbsPosition(item, &smoke_pos, p->effect_mesh++);\n                Sparks_TriggerExplosionSparks(\n                    smoke_pos, 2, 0, 0, item->room_num);\n                M_TriggerSmoke(smoke_pos, true);\n            }\n\n            if (item->mesh_bits == INT32_MAX) {\n                item->goal_anim_state = M_STATE_WAIT;\n                p->effect_mesh = 0;\n                creature->flags = -45;\n            }\n            break;\n\n        case M_STATE_PINCER:\n            if (info.ahead) {\n                torso_y = info.angle;\n                torso_x = info.x_angle;\n                head_y = info.angle;\n            }\n            creature->maximum_turn = M_WALK_TURN;\n            M_Damage(item, creature, 150);\n            break;\n\n        case M_STATE_KILL: {\n            creature->maximum_turn = 0;\n            head_y = 0;\n            torso_x = 0;\n            torso_y = 0;\n            const int32_t frame = Item_GetRelativeFrame(item);\n            if (frame == 10 || frame == 21 || frame == 33) {\n                Creature_Effect(item, &m_RightBlade, Spawn_Blood);\n                Creature_Effect(item, &m_LeftBlade, Spawn_Blood);\n            }\n            break;\n        }\n\n        case M_STATE_CHOPPER:\n            head_y = info.angle;\n            torso_y = info.angle;\n            if (info.x_angle > 0) {\n                torso_x = info.x_angle;\n            }\n            creature->maximum_turn = M_WALK_TURN;\n            M_Damage(item, creature, 180);\n            break;\n\n        case M_STATE_WALK_BACK:\n            if (info.ahead) {\n                head_y = info.angle;\n            }\n            creature->maximum_turn = M_WALK_TURN;\n\n            if ((info.ahead && info.distance < M_CHOPPER_RANGE)\n                || (Item_GetRelativeFrame(item) == 0 && creature->flags == 0)) {\n                item->goal_anim_state = M_STATE_WAIT;\n            } else if (item->hit_status) {\n                creature->flags = 4;\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n            break;\n        }\n    }\n\n    if (lara_alive && lara_item->hit_points <= 0) {\n        Creature_SpecialKill(\n            item, M_ANIM_KILL_ANIM, M_STATE_KILL, LS_EXTRA_SHIVA_KILL);\n        return;\n    }\n\n    Creature_Tilt(item, 0);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head_y - torso_y);\n    Creature_Joint(item, 3, 0);\n    Creature_Animate(item_num, angle, 0);\n}\n\nbool M_Draw(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n\n    if (item->hit_points <= 0 && item->status != IS_ACTIVE\n        && item->mesh_bits != 0) {\n        ITEM *const mutable_item = (ITEM *)item;\n        mutable_item->mesh_bits >>= 1;\n        XYZ_32 smoke_pos = { 0, 0, 256 };\n        Collide_GetJointAbsPosition(item, &smoke_pos, p->effect_mesh++);\n        M_TriggerSmoke(smoke_pos, true);\n    }\n\n    const OBJECT *const swap = Object_Get(O_MESH_SWAP_1);\n    return Object_DrawAnimatingItemWithSwap(item, swap);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    if (!Object_Get(O_MESH_SWAP_1)->loaded) {\n        Shell_ExitSystem(\"Shiva requires O_MESH_SWAP_1 (statue)\");\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n    obj->draw_func = M_Draw;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = 100;\n    obj->radius = 341;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 25)->rot.x = true;\n    Object_GetBone(obj, 25)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_SHIVA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/skate_kid.c",
    "content": "#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n\n#define SKATE_KID_STOP_SHOT_DAMAGE 50\n#define SKATE_KID_SKATE_SHOT_DAMAGE 40\n#define SKATE_KID_STOP_RANGE SQUARE(WALL_L * 4) // = 16777216\n#define SKATE_KID_DONT_STOP_RANGE SQUARE(WALL_L * 5 / 2) // = 6553600\n#define SKATE_KID_TOO_CLOSE SQUARE(WALL_L) // = 1048576\n#define SKATE_KID_SKATE_TURN (DEG_1 * 4) // = 728\n#define SKATE_KID_PUSH_CHANCE 0x200\n#define SKATE_KID_SKATE_CHANCE 0x400\n#define SKATE_KID_DIE_ANIM 13\n#define SKATE_KID_HITPOINTS 125\n#define SKATE_KID_RADIUS (WALL_L / 5) // = 204\n#define SKATE_KID_SMARTNESS 0x7FFF\n#define SKATE_KID_SPEECH_HITPOINTS 120\n#define SKATE_KID_SPEECH_STARTED 1\n\ntypedef enum {\n    SKATE_KID_STATE_STOP = 0,\n    SKATE_KID_STATE_SHOOT_1 = 1,\n    SKATE_KID_STATE_SKATE = 2,\n    SKATE_KID_STATE_PUSH = 3,\n    SKATE_KID_STATE_SHOOT_2 = 4,\n    SKATE_KID_STATE_DEATH = 5,\n} SKATE_KID_STATE;\n\ntypedef struct {\n    int16_t skateboard_item_num;\n} M_PRIV;\n\nstatic const CREATURE_GUN m_KidGun1 = {\n    .muzzle = { .pos = { 0, 150, 34 }, .mesh_num = 7 },\n};\nstatic const CREATURE_GUN m_KidGun2 = {\n    .muzzle = { .pos = { 0, 150, 37 }, .mesh_num = 4 },\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->skateboard_item_num = NO_ITEM;\n\n    Creature_Initialise(item_num);\n    item->current_anim_state = SKATE_KID_STATE_SKATE;\n\n    if (!Object_Get(O_SKATEBOARD)->loaded) {\n        return;\n    }\n\n    const int16_t skateboard_item_num = Item_Create();\n    if (skateboard_item_num == NO_ITEM) {\n        LOG_WARNING(\"Failed to create skateboard item for skate kid.\");\n        return;\n    }\n\n    ITEM *const skateboard_item = Item_Get(skateboard_item_num);\n    skateboard_item->object_id = O_SKATEBOARD;\n    skateboard_item->pos = item->pos;\n    skateboard_item->rot = item->rot;\n    skateboard_item->room_num = item->room_num;\n    skateboard_item->status = item->status;\n    skateboard_item->collidable = false;\n    skateboard_item->shade.value_1 = -1;\n    Item_Initialise(skateboard_item_num);\n\n    p->skateboard_item_num = skateboard_item_num;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const kid = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != SKATE_KID_STATE_DEATH) {\n            item->current_anim_state = SKATE_KID_STATE_DEATH;\n            Item_SwitchToAnim(item, SKATE_KID_DIE_ANIM, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, SKATE_KID_SKATE_TURN);\n\n        if (item->hit_points < SKATE_KID_SPEECH_HITPOINTS\n            && !(item->flags & SKATE_KID_SPEECH_STARTED)) {\n            const MUSIC_PLAY_MODE mode =\n                g_Config.audio.fix_speeches_killing_music ? MPM_OVERLAY\n                                                          : MPM_NO_REPEAT;\n            Music_Play(MX_SKATEKID_SPEECH, mode);\n            item->flags |= SKATE_KID_SPEECH_STARTED;\n        }\n\n        switch (item->current_anim_state) {\n        case SKATE_KID_STATE_STOP:\n            kid->flags = 0;\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = SKATE_KID_STATE_SHOOT_1;\n            } else {\n                item->goal_anim_state = SKATE_KID_STATE_SKATE;\n            }\n            break;\n\n        case SKATE_KID_STATE_SKATE:\n            kid->flags = 0;\n            if (Random_GetControl() < SKATE_KID_PUSH_CHANCE) {\n                item->goal_anim_state = SKATE_KID_STATE_PUSH;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance > SKATE_KID_DONT_STOP_RANGE\n                    && info.distance < SKATE_KID_STOP_RANGE\n                    && kid->mood != MOOD_ESCAPE) {\n                    item->goal_anim_state = SKATE_KID_STATE_STOP;\n                } else {\n                    item->goal_anim_state = SKATE_KID_STATE_SHOOT_2;\n                }\n            }\n            break;\n\n        case SKATE_KID_STATE_PUSH:\n            if (Random_GetControl() < SKATE_KID_SKATE_CHANCE) {\n                item->goal_anim_state = SKATE_KID_STATE_SKATE;\n            }\n            break;\n\n        case SKATE_KID_STATE_SHOOT_1:\n        case SKATE_KID_STATE_SHOOT_2:\n            if (!kid->flags && Creature_CanTargetEnemy(item, &info)) {\n                Creature_Shoot(\n                    item, &info, &m_KidGun1, head,\n                    item->current_anim_state == SKATE_KID_STATE_SHOOT_1\n                        ? SKATE_KID_STOP_SHOT_DAMAGE\n                        : SKATE_KID_SKATE_SHOT_DAMAGE);\n\n                Creature_Shoot(\n                    item, &info, &m_KidGun2, head,\n                    item->current_anim_state == SKATE_KID_STATE_SHOOT_1\n                        ? SKATE_KID_STOP_SHOT_DAMAGE\n                        : SKATE_KID_SKATE_SHOT_DAMAGE);\n\n                kid->flags = 1;\n            }\n            if (kid->mood == MOOD_ESCAPE\n                || info.distance < SKATE_KID_TOO_CLOSE) {\n                item->required_anim_state = SKATE_KID_STATE_SKATE;\n            }\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, 0);\n\n    if (p->skateboard_item_num != NO_ITEM) {\n        ITEM *const skateboard_item = Item_Get(p->skateboard_item_num);\n        skateboard_item->pos = item->pos;\n        skateboard_item->rot = item->rot;\n        skateboard_item->status = item->status;\n        Item_UpdateRoom(p->skateboard_item_num, item->room_num);\n\n        const int16_t relative_anim = Item_GetRelativeAnim(item);\n        const int16_t relative_frame = Item_GetRelativeFrame(item);\n        Item_SwitchToObjAnim(\n            skateboard_item, relative_anim, relative_frame, O_SKATEBOARD);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = SKATE_KID_HITPOINTS;\n    obj->radius = SKATE_KID_RADIUS;\n    obj->smartness = SKATE_KID_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n\n    if (!Object_Get(O_SKATEBOARD)->loaded) {\n        LOG_WARNING(\n            \"Skateboard object (%d) is not loaded and so will not be drawn.\",\n            O_SKATEBOARD);\n    }\n}\n\nstatic void M_SetupSkateboard(OBJECT *const obj)\n{\n    obj->control_func = nullptr;\n}\n\nREGISTER_OBJECT(O_SKATEKID, M_Setup)\nREGISTER_OBJECT(O_SKATEBOARD, M_SetupSkateboard)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/skidoo_driver.c",
    "content": "#include <trx/game/objects/creatures/skidoo_driver.h>\n\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/vehicles/skidoo_common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/sound.h>\n\n#define SKIDOO_DRIVER_MIN_TURN (SKIDOO_MAX_TURN / 3) // = 364\n#define SKIDOO_DRIVER_TARGET_ANGLE (DEG_1 * 15) // = 2730\n#define SKIDOO_DRIVER_WAIT_RANGE SQUARE(WALL_L * 4) // = 0x1000000\n#define SKIDOO_DRIVER_SHOT_DAMAGE 10\n#define SKIDOO_DRIVER_LARA_DAMAGE 50\n\ntypedef enum {\n    // clang-format off\n    SKIDOO_DRIVER_STATE_EMPTY       = 0,\n    SKIDOO_DRIVER_STATE_WAIT        = 1,\n    SKIDOO_DRIVER_STATE_MOVING      = 2,\n    SKIDOO_DRIVER_STATE_START_LEFT  = 3,\n    SKIDOO_DRIVER_STATE_START_RIGHT = 4,\n    SKIDOO_DRIVER_STATE_LEFT        = 5,\n    SKIDOO_DRIVER_STATE_RIGHT       = 6,\n    SKIDOO_DRIVER_STATE_DEATH       = 7,\n    // clang-format on\n} SKIDOO_DRIVER_STATE;\n\ntypedef enum {\n    SKIDOO_DRIVER_ANIM_DEATH = 10,\n} SKIDOO_DRIVER_ANIM;\n\ntypedef struct {\n    int16_t skidoo_item_num;\n} M_PRIV;\n\nstatic void M_KillDriver(ITEM *const driver_item)\n{\n    const int32_t driver_item_num = Item_GetIndex(driver_item);\n    Item_RemoveActive(driver_item_num);\n    driver_item->collidable = 0;\n    driver_item->flags |= IF_ONE_SHOT;\n    driver_item->hit_points = 0;\n}\n\nstatic void M_MakeMountable(ITEM *const skidoo_item)\n{\n    if (skidoo_item->status == IS_INVISIBLE) {\n        return;\n    }\n\n    const int32_t skidoo_item_num = Item_GetIndex(skidoo_item);\n    LOT_DisableBaddieAI(skidoo_item_num);\n    skidoo_item->object_id = O_SKIDOO_FAST;\n    skidoo_item->status = IS_DEACTIVATED;\n    Skidoo_Initialise(skidoo_item_num);\n\n    SKIDOO_INFO *const skidoo_data = skidoo_item->priv;\n    skidoo_data->track_mesh = SKIDOO_GUN_MESH;\n}\n\nstatic void M_ControlDead(ITEM *const driver_item, ITEM *const skidoo_item)\n{\n    if (driver_item->current_anim_state == SKIDOO_DRIVER_STATE_DEATH) {\n        Item_Animate(driver_item);\n    } else {\n        driver_item->pos.x = skidoo_item->pos.x;\n        driver_item->pos.y = skidoo_item->pos.y;\n        driver_item->pos.z = skidoo_item->pos.z;\n        driver_item->rot.y = skidoo_item->rot.y;\n        driver_item->room_num = skidoo_item->room_num;\n        Item_SwitchToAnim(driver_item, SKIDOO_DRIVER_ANIM_DEATH, 0);\n        driver_item->current_anim_state = SKIDOO_DRIVER_STATE_DEATH;\n        Carrier_TestItemDrops(Item_GetIndex(skidoo_item));\n\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        if (lara->target == skidoo_item) {\n            lara->target = nullptr;\n        }\n    }\n\n    switch (skidoo_item->current_anim_state) {\n    case SKIDOO_DRIVER_STATE_MOVING:\n    case SKIDOO_DRIVER_STATE_WAIT:\n        skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_WAIT;\n        break;\n    default:\n        skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING;\n        break;\n    }\n}\n\nstatic int16_t M_ControlAlive(ITEM *const driver_item, ITEM *const skidoo_item)\n{\n    CREATURE *const driver_data = skidoo_item->creature_data;\n\n    AI_INFO info;\n    Creature_AIInfo(skidoo_item, &info);\n    Creature_Mood(skidoo_item, &info, true);\n\n    int16_t angle = Creature_Turn(skidoo_item, SKIDOO_MAX_TURN / 2);\n\n    switch (skidoo_item->current_anim_state) {\n    case SKIDOO_DRIVER_STATE_WAIT:\n        if (driver_data->mood != MOOD_BORED\n            && (ABS(info.angle) >= SKIDOO_DRIVER_TARGET_ANGLE\n                || info.distance >= SKIDOO_DRIVER_WAIT_RANGE)) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING;\n        }\n        break;\n\n    case SKIDOO_DRIVER_STATE_MOVING:\n        if (driver_data->mood == MOOD_BORED) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_WAIT;\n        } else if (\n            ABS(info.angle) < SKIDOO_DRIVER_TARGET_ANGLE\n            && info.distance < SKIDOO_DRIVER_WAIT_RANGE) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_WAIT;\n        } else if (angle < -SKIDOO_DRIVER_MIN_TURN) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_START_LEFT;\n        } else if (angle > SKIDOO_DRIVER_MIN_TURN) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_START_RIGHT;\n        }\n        break;\n\n    case SKIDOO_DRIVER_STATE_START_LEFT:\n    case SKIDOO_DRIVER_STATE_LEFT:\n        if (angle >= -SKIDOO_DRIVER_MIN_TURN) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING;\n        } else {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_LEFT;\n        }\n        break;\n\n    case SKIDOO_DRIVER_STATE_START_RIGHT:\n    case SKIDOO_DRIVER_STATE_RIGHT:\n        if (angle >= -SKIDOO_DRIVER_MIN_TURN) {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_MOVING;\n        } else {\n            skidoo_item->goal_anim_state = SKIDOO_DRIVER_STATE_LEFT;\n        }\n        break;\n    }\n\n    if (driver_item->current_anim_state != SKIDOO_DRIVER_STATE_DEATH) {\n        const ITEM *const lara_item = Lara_GetItem();\n        if (driver_data->flags == 0\n            && ABS(info.angle) < SKIDOO_DRIVER_TARGET_ANGLE\n            && lara_item->hit_points > 0) {\n            const int32_t damage = Lara_Vehicle_IsMounted()\n                ? SKIDOO_DRIVER_SHOT_DAMAGE\n                : SKIDOO_DRIVER_LARA_DAMAGE;\n\n            const bool left_targetable = Creature_Shoot(\n                skidoo_item, &info,\n                &(CREATURE_GUN) {\n                    .muzzle = g_Skidoo_RightGun,\n                },\n                0, damage);\n            const bool right_targetable = Creature_Shoot(\n                skidoo_item, &info,\n                &(CREATURE_GUN) {\n                    .muzzle = g_Skidoo_LeftGun,\n                },\n                0, damage);\n            if (left_targetable || right_targetable) {\n                driver_data->flags = 5;\n            }\n        }\n\n        if (driver_data->flags != 0) {\n            Sound_Effect(SFX_LARA_UZI_FIRE, &skidoo_item->pos, SPM_NORMAL);\n            driver_data->flags--;\n        }\n    }\n\n    return angle;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const skidoo_driver = Item_Get(item_num);\n    M_PRIV *const p = skidoo_driver->priv;\n\n    const int16_t skidoo_item_num = Item_CreateLevelItem();\n    ASSERT(skidoo_item_num != NO_ITEM);\n\n    ITEM *const skidoo = Item_Get(skidoo_item_num);\n    skidoo->object_id = O_SKIDOO_ARMED;\n    skidoo->pos.x = skidoo_driver->pos.x;\n    skidoo->pos.y = skidoo_driver->pos.y;\n    skidoo->pos.z = skidoo_driver->pos.z;\n    skidoo->rot.y = skidoo_driver->rot.y;\n    skidoo->room_num = skidoo_driver->room_num;\n    skidoo->flags = IF_ONE_SHOT;\n    skidoo->shade.value_1 = -1;\n    Item_Initialise(skidoo_item_num);\n\n    p->skidoo_item_num = skidoo_item_num;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->status == IS_DEACTIVATED) {\n            item->hit_points = 0;\n            const M_PRIV *const p = item->priv;\n            const int16_t skidoo_num = p->skidoo_item_num;\n            ITEM *const skidoo = Item_Get(skidoo_num);\n            skidoo->object_id = O_SKIDOO_FAST;\n            Skidoo_Initialise(skidoo_num);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t driver_item_num)\n{\n    ITEM *const driver_item = Item_Get(driver_item_num);\n    const M_PRIV *const p = driver_item->priv;\n    const int16_t skidoo_item_num = p->skidoo_item_num;\n    ITEM *const skidoo_item = Item_Get(skidoo_item_num);\n\n    if (skidoo_item->creature_data == nullptr) {\n        LOT_EnableBaddieAI(skidoo_item_num, true);\n        skidoo_item->status = IS_ACTIVE;\n    }\n\n    CREATURE *const driver_data = skidoo_item->creature_data;\n    int16_t angle = 0;\n\n    if (skidoo_item->hit_points <= 0) {\n        M_ControlDead(driver_item, skidoo_item);\n    } else {\n        angle = M_ControlAlive(driver_item, skidoo_item);\n    }\n\n    if (skidoo_item->current_anim_state == SKIDOO_DRIVER_STATE_WAIT) {\n        driver_data->head_rotation = 0;\n        Sound_Effect(SFX_SKIDOO_IDLE, &skidoo_item->pos, SPM_NORMAL);\n    } else {\n        driver_data->head_rotation = driver_data->head_rotation == 1 ? 2 : 1;\n        if (skidoo_item->status != IS_INVISIBLE) {\n            Skidoo_DoSnowEffect(skidoo_item);\n        }\n\n        const int32_t pitch_delta =\n            (SKIDOO_MAX_SPEED - skidoo_item->speed) * 100;\n        const int32_t pitch = (SOUND_DEFAULT_PITCH - pitch_delta) << 8;\n        Sound_Effect(SFX_SKIDOO_MOVING, &skidoo_item->pos, SPM_PITCH | pitch);\n    }\n\n    Creature_Animate(skidoo_item_num, angle, 0);\n\n    if (driver_item->current_anim_state == SKIDOO_DRIVER_STATE_DEATH) {\n        if (driver_item->status == IS_DEACTIVATED && skidoo_item->speed == 0\n            && skidoo_item->fall_speed == 0) {\n            M_KillDriver(driver_item);\n            M_MakeMountable(skidoo_item);\n        }\n    } else {\n        driver_item->pos.x = skidoo_item->pos.x;\n        driver_item->pos.y = skidoo_item->pos.y;\n        driver_item->pos.z = skidoo_item->pos.z;\n        driver_item->rot.y = skidoo_item->rot.y;\n        Item_UpdateRoom(driver_item_num, skidoo_item->room_num);\n        const int16_t anim_num =\n            Item_GetRelativeObjAnim(skidoo_item, O_SKIDOO_ARMED);\n        const int16_t frame_num = Item_GetRelativeFrame(skidoo_item);\n        Item_SwitchToAnim(driver_item, anim_num, frame_num);\n    }\n}\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->is_targetable_func = M_IsTargetable;\n\n    obj->hit_points = 1;\n\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nint16_t SkidooDriver_GetSkidooItemNum(const ITEM *const driver_item)\n{\n    const M_PRIV *const p = driver_item->priv;\n    return p->skidoo_item_num;\n}\n\nREGISTER_OBJECT(O_SKIDOO_DRIVER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/skidoo_driver.h",
    "content": "#pragma once\n\n#include <trx/game/objects/common.h>\n\n#define SKIDOO_DRIVER_HITPOINTS 100\n\nint16_t SkidooDriver_GetSkidooItemNum(const ITEM *driver_item);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/sophia.c",
    "content": "#include \"sophia_internal.h\"\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math/geom.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/electric.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/stats.h>\n\n// clang-format off\n#define M_SMALL_FLASH    10\n\n#define M_RIGHT_PRONG    0\n#define M_CENTER_PRONG   1\n#define M_LEFT_PRONG     2\n\n#define M_VAULT_SHIFT    96\n#define M_AWARE_DISTANCE SQUARE(WALL_L)\n#define M_WALK_TURN      (4 * DEG_1)\n#define M_RUN_TURN       (7 * DEG_1)\n#define M_WALK_RANGE     SQUARE(WALL_L)\n#define M_LAUGH_CHANCE   0x100\n#define M_BIG_ZAP_TIMER  600\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STAND,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_SUMMON,\n    M_STATE_BIG_ZAP,\n    M_STATE_DEATH,\n    M_STATE_LAUGH,\n    M_STATE_LITTLE_ZAP,\n    M_STATE_VAULT_2,\n    M_STATE_VAULT_3,\n    M_STATE_VAULT_4,\n    M_STATE_GO_DOWN\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STAND_TO_SUMMON = 1,\n    M_ANIM_SUMMON          = 2,\n    M_ANIM_UP_2            = 9,\n    M_ANIM_UP_3            = 18,\n    M_ANIM_UP_4            = 15,\n    M_ANIM_DEATH           = 17,\n    M_ANIM_DOWN_4          = 21,\n    // clang-format on\n} M_ANIM;\n\ntypedef struct {\n    XYZ_16 pos;\n    RGB_888 sub;\n    RGB_888 color;\n} M_SHIELD_POINT;\n\ntypedef struct {\n    bool dropped_item;\n    uint8_t ring_count;\n    int16_t explode_count;\n    bool dead;\n    bool charged;\n    int16_t death_counter;\n    int16_t hp_counter;\n    int16_t fuse_box_num;\n    uint8_t wand_glow_phase;\n    bool knockback_active;\n    M_SHIELD_POINT shield[5][8];\n    int32_t final_height;\n} M_PRIV;\n\nstatic const BITE m_WandBite[3] = {\n    { .pos = { .x = 16, .y = 56, .z = 356 }, .mesh_num = 10 },\n    { .pos = { .x = -28, .y = 48, .z = 304 }, .mesh_num = 10 },\n    { .pos = { .x = -72, .y = 48, .z = 356 }, .mesh_num = 10 },\n};\n\nstatic const int32_t m_Heights[5] = { -1536, -1280, -832, -384, 0 };\nstatic const int32_t m_Dist[5] = { 200, 400, 500, 500, 475 };\nstatic const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 };\nstatic const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 };\nstatic const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 };\nstatic int32_t m_DeathDist[5] = {};\nstatic int32_t m_DeathHeights[5] = {};\n\nstatic void M_ExplodeLondonBoss(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n\n    if (p->explode_count == 1 || p->explode_count == 15\n        || p->explode_count == 25 || p->explode_count == 35\n        || p->explode_count == 45 || p->explode_count == 55) {\n        const XYZ_32 pos = {\n            .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512,\n            .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256,\n            .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512,\n        };\n\n        FX_RING *const ring =\n            FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count);\n        if (ring != nullptr) {\n            ring->pos = pos;\n            ring->on = 3;\n            FX_Ring_Sync(ring);\n            p->ring_count++;\n        }\n\n        Sparks_TriggerExplosionSparks(pos, 3, -2, 2, 0);\n        for (int32_t i = 0; i < 2; i++) {\n            Sparks_TriggerExplosionSparks(pos, 3, -1, 2, 0);\n        }\n\n        Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH);\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        if (p->explode_count < 128) {\n            m_DeathDist[i] =\n                (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7);\n            m_DeathHeights[i] = m_DHeights2[i]\n                + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7);\n        }\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t y = m_DeathHeights[i];\n        const int32_t dist = m_DeathDist[i];\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        int32_t angle = (time4 & 0x3F) << 3;\n\n        for (int32_t j = 0; j < 8; j++) {\n            M_SHIELD_POINT *const shield = &p->shield[i][j];\n            shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13;\n            shield->pos.y = y;\n            shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13;\n\n            if (i != 0 && i != 4 && p->explode_count < 64) {\n                const int32_t r = Random_GetDraw() & 0x3F;\n                const int32_t g = (Random_GetDraw() & 0x1F) + 224;\n                const int32_t b = (g >> 2) + (Random_GetDraw() & 0x3F);\n                shield->color = (RGB_888) {\n                    .r = ((64 - p->explode_count) * r) >> 6,\n                    .g = ((64 - p->explode_count) * g) >> 6,\n                    .b = ((64 - p->explode_count) * b) >> 6,\n                };\n            } else {\n                shield->color = COLOR_RGB_888_BLACK;\n            }\n\n            angle = (angle + 512) & 0xFFF;\n        }\n    }\n}\n\nstatic void M_Die(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->hit_points = 0;\n    item->collidable = false;\n    Item_Kill(item_num);\n    LOT_DisableBaddieAI(item_num);\n    item->flags |= IF_INVISIBLE;\n}\n\nstatic bool M_KnockBackCollision(const FX_RING *const ring)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    if (Lara_GetLaraInfo()->water_status == LWS_CHEAT) {\n        return false;\n    }\n\n    const XYZ_32 delta = {\n        .x = lara_item->pos.x - ring->pos.x,\n        .y = 0,\n        .z = lara_item->pos.z - ring->pos.z,\n    };\n    if (XYZ_32_GetLength2(delta) >= SQUARE(ring->radius)) {\n        return false;\n    }\n\n    Lara_TakeDamage(200, true);\n\n    const int16_t angle = Math_Atan(delta.z, delta.x);\n    const int16_t dy = lara_item->rot.y - angle;\n    if (ABS(dy) >= DEG_90) {\n        lara_item->rot.y = angle + DEG_180;\n        lara_item->speed = -75;\n    } else {\n        lara_item->rot.y = angle;\n        lara_item->speed = 75;\n    }\n\n    lara_item->gravity = true;\n    lara_item->fall_speed = -50;\n    lara_item->rot.x = 0;\n    lara_item->rot.z = 0;\n    Item_SwitchToAnim(lara_item, LA(LA_FALL_START), 0);\n    lara_item->current_anim_state = LS_JUMP_FORWARD;\n    lara_item->goal_anim_state = LS_JUMP_FORWARD;\n\n    Sparks_TriggerExplosionSparks(\n        lara_item->pos, 3, -2, 2, lara_item->room_num);\n    for (int32_t i = 0; i < 3; i++) {\n        Sophia_TriggerPlasmaBall(\n            2, lara_item->pos, lara_item->room_num, Random_GetControl() << 1);\n    }\n\n    return true;\n}\n\nstatic int32_t M_FindFinalHeight(void)\n{\n    int32_t height = NO_HEIGHT;\n    for (int16_t i = 0; i < Item_GetLevelCount(); i++) {\n        const ITEM *const item = Item_Get(i);\n        if (item->object_id != O_AI_AMBUSH) {\n            continue;\n        }\n        if (height == NO_HEIGHT || item->pos.y < height) {\n            height = item->pos.y;\n        }\n    }\n    return height;\n}\n\nstatic int16_t M_FindFuseBox(const ITEM *const item)\n{\n    int16_t fuse_box_num = NO_ITEM;\n    int32_t best_dist = INT32_MAX;\n    for (int16_t i = 0; i < Item_GetLevelCount(); i++) {\n        const ITEM *const other_item = Item_Get(i);\n        if (other_item->object_id != O_FUSE_BOX) {\n            continue;\n        }\n\n        const int32_t dist = XYZ_32_GetLength2((XYZ_32) {\n            .x = other_item->pos.x - item->pos.x,\n            .y = 0,\n            .z = other_item->pos.z - item->pos.z,\n        });\n        if (dist < best_dist) {\n            best_dist = dist;\n            fuse_box_num = i;\n        }\n    }\n    return fuse_box_num;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"dropped_item\", &p->dropped_item));\n    JSON_SHOULD(JSON_READ(io, \"ring_count\", &p->ring_count));\n    JSON_SHOULD(JSON_READ(io, \"explode_count\", &p->explode_count));\n    JSON_SHOULD(JSON_READ(io, \"dead\", &p->dead));\n    JSON_SHOULD(JSON_READ(io, \"charged\", &p->charged));\n    JSON_SHOULD(JSON_READ(io, \"death_counter\", &p->death_counter));\n    JSON_SHOULD(JSON_READ(io, \"hp_counter\", &p->hp_counter));\n    JSON_SHOULD(JSON_READ(io, \"fuse_box_num\", &p->fuse_box_num));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"dropped_item\", p->dropped_item);\n    JSONW_WRITE(io, \"ring_count\", p->ring_count);\n    JSONW_WRITE(io, \"explode_count\", p->explode_count);\n    JSONW_WRITE(io, \"dead\", p->dead);\n    JSONW_WRITE(io, \"charged\", p->charged);\n    JSONW_WRITE(io, \"death_counter\", p->death_counter);\n    JSONW_WRITE(io, \"hp_counter\", p->hp_counter);\n    JSONW_WRITE(io, \"fuse_box_num\", p->fuse_box_num);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->dropped_item = false;\n    p->dead = 0;\n    p->ring_count = 0;\n    p->explode_count = 0;\n    p->final_height = M_FindFinalHeight();\n    p->fuse_box_num = M_FindFuseBox(item);\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t dist = m_Dist[i];\n        int32_t angle = 0;\n\n        for (int32_t j = 0; j < 8; j++) {\n            M_SHIELD_POINT *const shield = &p->shield[i][j];\n            shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13;\n            shield->pos.y = m_Heights[i];\n            shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13;\n            shield->color = COLOR_RGB_888_BLACK;\n            angle += 512;\n        }\n    }\n}\n\nstatic bool M_GunHit(\n    ITEM *const item, const GAME_VECTOR *const start,\n    const GAME_VECTOR *const hit_pos, int32_t *const damage)\n{\n    if (damage != nullptr) {\n        *damage = 0;\n    }\n    return true;\n}\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (p->death_counter == 0 && p->fuse_box_num != NO_ITEM) {\n        const ITEM *const fuse_box = Item_Get(p->fuse_box_num);\n        if (!Item_TestAnimEqual(fuse_box, 0)) {\n            Stats_AddKill();\n            p->death_counter = 1;\n        }\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n    int16_t head = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (p->death_counter != 0) {\n        if (p->death_counter == 1) {\n            item->hit_points = 0;\n        }\n\n        RGB_888 color;\n        int32_t falloff;\n        if (p->death_counter < 12) {\n            falloff = (Random_GetControl() & 1) - (p->death_counter << 1) + 25;\n            color = (RGB_888) {\n                .r = (Random_GetControl() & 0x3F) - (p->death_counter << 3)\n                    + 128,\n                .g = 256 - (p->death_counter << 3),\n                .b = 255,\n            };\n        } else {\n            falloff = (Random_GetControl() & 3) + 8;\n            color = (RGB_888) {\n                .r = 0,\n                .g = (Random_GetControl() & 0x3F) + 64,\n                .b = (Random_GetControl() & 0x3F) + 128,\n            };\n        }\n\n        Output_AddDynamicLightRGB(item->pos, falloff, color);\n    }\n\n    XYZ_32 points[3];\n    for (int32_t i = 0; i < 3; i++) {\n        points[i] = m_WandBite[i].pos;\n        Collide_GetJointAbsPosition(item, &points[i], m_WandBite[i].mesh_num);\n    }\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n\n        if (Item_TestFrameEqual(item, -2)) {\n            item->mesh_bits = 0;\n            item->frame_num = Item_GetAnim(item)->frame_end - 1;\n\n            if (p->explode_count == 0) {\n                p->ring_count = 0;\n\n                for (int32_t i = 0; i < 6; i++) {\n                    FX_RING *const ring =\n                        FX_Ring_GetRing(FX_RING_TYPE_BLAST, i);\n                    ring->on = 0;\n                    ring->life = 32;\n                    ring->radius = 512;\n                    ring->speed = 128 + (i << 5);\n                    ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) << 4;\n                    ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) << 4;\n                    FX_Ring_Sync(ring);\n                }\n\n                if (!p->dropped_item) {\n                    Carrier_TestItemDrops(item_num);\n                    p->dropped_item = true;\n                }\n            }\n\n            if (p->explode_count < 256) {\n                p->explode_count++;\n            }\n\n            if (p->explode_count > 128 && p->ring_count == 6\n                && FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life == 0) {\n                M_Die(item_num);\n                p->dead = 1;\n            } else {\n                M_ExplodeLondonBoss(item);\n            }\n\n            return;\n        }\n    } else {\n        if (item->ai_bits) {\n            Creature_GetAITarget(creature);\n        }\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        AI_INFO lara_info;\n        if (creature->enemy == lara_item) {\n            lara_info.angle = info.angle;\n            lara_info.x_angle = info.x_angle;\n            lara_info.distance = info.distance;\n        } else {\n            int32_t x = lara_item->pos.x - item->pos.x;\n            int32_t y = item->pos.y - lara_item->pos.y;\n            int32_t z = lara_item->pos.z - item->pos.z;\n            lara_info.angle = Math_Atan(z, x) - item->rot.y;\n            lara_info.distance = SQUARE(x) + SQUARE(z);\n\n            if (ABS(x) <= ABS(z)) {\n                z = z + (x >> 1);\n            } else {\n                z = x + (z >> 1);\n            }\n\n            lara_info.x_angle = Math_Atan(z, y);\n        }\n\n        Creature_Mood(item, &info, true);\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        ITEM *const enemy = creature->enemy;\n        creature->enemy = lara_item;\n\n        if (item->hit_status || lara_info.distance < M_AWARE_DISTANCE\n            || Creature_CanSeeEnemy(item, &lara_info)\n            || lara_item->pos.y < item->pos.y) {\n            Creature_AlertAllGuards(item_num);\n        }\n\n        creature->enemy = enemy;\n\n        if (lara_item->pos.y < item->pos.y) {\n            creature->hurt_by_lara = 1;\n        }\n\n        if (item->timer > 0) {\n            item->timer--;\n        }\n\n        item->hit_points = 300;\n\n        switch (item->current_anim_state) {\n        case M_STATE_LAUGH:\n            if (ABS(lara_info.angle) < M_WALK_TURN) {\n                item->rot.y += lara_info.angle;\n            } else if (lara_info.angle >= 0) {\n                item->rot.y += M_WALK_TURN;\n            } else {\n                item->rot.y -= M_WALK_TURN;\n            }\n\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wimplicit-fallthrough\"\n            if (creature->alerted) {\n                item->goal_anim_state = M_STATE_STAND;\n                break;\n            }\n#pragma GCC diagnostic pop\n\n        case M_STATE_STAND:\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n\n            if (creature->reached_goal) {\n                creature->reached_goal = 0;\n                item->ai_bits |= AI_AMBUSH;\n                item->ai_tag += 0x2000;\n            }\n\n            head = lara_info.angle;\n\n            if (item->ai_bits & AI_GUARD) {\n                if ((lara_info.angle < -0x3000 || lara_info.angle > 0x3000)\n                    && item->pos.y > p->final_height) {\n                    item->goal_anim_state = M_STATE_WALK;\n                    creature->maximum_turn = M_WALK_TURN;\n                }\n            } else if (\n                (item->pos.y <= p->final_height\n                 || item->pos.y < lara_item->pos.y)\n                && !(Random_GetControl() & 0xF) && !p->charged && item->timer) {\n                item->goal_anim_state = M_STATE_LAUGH;\n            } else if (\n                creature->reached_goal || lara_item->pos.y > item->pos.y\n                || item->pos.y <= p->final_height) {\n                if (p->charged) {\n                    item->goal_anim_state = M_STATE_BIG_ZAP;\n                } else if (item->timer) {\n                    item->goal_anim_state = M_STATE_LITTLE_ZAP;\n                } else {\n                    item->goal_anim_state = M_STATE_SUMMON;\n                }\n            } else if (item->ai_bits & AI_PATROL_1) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                creature->mood == MOOD_ESCAPE\n                || item->pos.y > lara_item->pos.y) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (\n                creature->mood == MOOD_BORED\n                || (item->ai_bits & AI_FOLLOW\n                    && (creature->reached_goal\n                        || lara_info.distance > 0x400000))) {\n                if (item->required_anim_state) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (info.ahead) {\n                    item->goal_anim_state = M_STATE_STAND;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (info.bite && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_WALK:\n            head = lara_info.angle;\n            creature->flags = 0;\n            creature->maximum_turn = M_WALK_TURN;\n\n            if (item->ai_bits & AI_GUARD\n                || (creature->reached_goal && !(item->ai_bits & AI_FOLLOW))) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (item->ai_bits & AI_PATROL_1) {\n                item->goal_anim_state = M_STATE_WALK;\n                head = 0;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < M_LAUGH_CHANCE) {\n                    item->required_anim_state = M_STATE_LAUGH;\n                    item->goal_anim_state = M_STATE_STAND;\n                }\n            } else if (info.distance > M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_RUN:\n            if (info.ahead) {\n                head = info.angle;\n            }\n\n            creature->maximum_turn = M_RUN_TURN;\n            tilt = angle >> 1;\n\n            if (item->ai_bits & AI_GUARD\n                || (creature->reached_goal && !(item->ai_bits & AI_FOLLOW))) {\n                item->goal_anim_state = M_STATE_STAND;\n            } else if (creature->mood != MOOD_ESCAPE) {\n                if (item->ai_bits & AI_FOLLOW\n                    && (creature->reached_goal\n                        || lara_info.distance > 0x400000)) {\n                    item->goal_anim_state = M_STATE_STAND;\n                } else if (creature->mood == MOOD_BORED) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else if (info.ahead && info.distance < M_WALK_RANGE) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            }\n            break;\n\n        case M_STATE_SUMMON:\n            head = lara_info.angle;\n\n            if (creature->reached_goal) {\n                creature->reached_goal = 0;\n                item->ai_bits = AI_AMBUSH;\n                item->ai_tag += 0x2000;\n            }\n\n            if (Item_TestAnimEqual(item, M_ANIM_STAND_TO_SUMMON)) {\n                if (Item_TestFrameEqual(item, 0)) {\n                    p->hp_counter = item->hit_points;\n                    item->timer = M_BIG_ZAP_TIMER;\n                } else if (\n                    item->hit_status\n                    && item->goal_anim_state != M_STATE_STAND) {\n                    Sound_StopEffect(SFX_SOPHIA_SUMMON);\n                    Sound_Effect(SFX_SOPHIA_TAKE_HIT, &item->pos, SPM_NORMAL);\n                    Sound_Effect(SFX_SOPHIA_SUMMON_NOT, &item->pos, SPM_NORMAL);\n                    item->goal_anim_state = M_STATE_STAND;\n                }\n            } else if (\n                Item_TestAnimEqual(item, M_ANIM_SUMMON)\n                && Item_TestFrameEqual(item, -1)) {\n                p->charged = true;\n            }\n\n            if (ABS(lara_info.angle) < M_WALK_TURN) {\n                item->rot.y += lara_info.angle;\n            } else if (lara_info.angle >= 0) {\n                item->rot.y += M_WALK_TURN;\n            } else {\n                item->rot.y -= M_WALK_TURN;\n            }\n\n            const int32_t time4 = Output_GetTimeInGame() * 4;\n            if ((time4 & 7) == 0) {\n                XYZ_32 pos = {\n                    .x = item->pos.x,\n                    .y = (Random_GetControl() & 0x1FF) - 256,\n                    .z = item->pos.z,\n                };\n                Sophia_TriggerLaserBolt(pos, item, 2, 0);\n\n                for (int32_t i = 0; i < 6; i++) {\n                    FX_RING *const ring =\n                        FX_Ring_GetRing(FX_RING_TYPE_SUMMON, i);\n                    if (ring->on == 0) {\n                        const int32_t r = Random_GetControl() & 0x3FF;\n                        ring->on = 3;\n                        ring->life = 64;\n                        ring->speed = (Random_GetControl() & 0xF) + 16;\n                        ring->pos.x = item->pos.x;\n                        ring->pos.y = item->pos.y - r + 128;\n                        ring->pos.z = item->pos.z;\n                        ring->rot.x =\n                            16 * ((Random_GetControl() & 0x1FF) - 256);\n                        ring->rot.z =\n                            16 * ((Random_GetControl() & 0x1FF) - 256);\n                        ring->radius = 2048 - ABS(r - 512);\n                        FX_Ring_Sync(ring);\n                        break;\n                    }\n                }\n            }\n\n            creature->maximum_turn = 0;\n            break;\n\n        case M_STATE_BIG_ZAP:\n            if (creature->reached_goal) {\n                creature->reached_goal = 0;\n                item->ai_bits = AI_AMBUSH;\n                item->ai_tag += 0x2000;\n            }\n\n            p->charged = false;\n\n            if (ABS(lara_info.angle) < M_WALK_TURN) {\n                item->rot.y += lara_info.angle;\n            } else if (lara_info.angle >= 0) {\n                item->rot.y += M_WALK_TURN;\n            } else {\n                item->rot.y -= M_WALK_TURN;\n            }\n\n            creature->maximum_turn = 0;\n            torso_x = lara_info.x_angle;\n            torso_y = lara_info.angle;\n\n            if (Item_TestFrameEqual(item, 36)) {\n                Sophia_TriggerLaserBolt(\n                    points[M_RIGHT_PRONG], item, 0, item->rot.y + 512);\n                Sophia_TriggerLaserBolt(\n                    points[M_CENTER_PRONG], item, 1, item->rot.y);\n                Sophia_TriggerLaserBolt(\n                    points[M_LEFT_PRONG], item, 0, item->rot.y - 512);\n            }\n            break;\n\n        case M_STATE_LITTLE_ZAP:\n            if (creature->reached_goal) {\n                creature->reached_goal = 0;\n                item->ai_bits = AI_AMBUSH;\n                item->ai_tag += 0x2000;\n            }\n\n            if (ABS(lara_info.angle) < M_WALK_TURN) {\n                item->rot.y += lara_info.angle;\n            } else if (lara_info.angle >= 0) {\n                item->rot.y += M_WALK_TURN;\n            } else {\n                item->rot.y -= M_WALK_TURN;\n            }\n\n            creature->maximum_turn = 0;\n            torso_x = lara_info.x_angle;\n            torso_y = lara_info.angle;\n\n            if (Item_TestFrameEqual(item, 14)) {\n                Sophia_TriggerLaserBolt(\n                    points[M_RIGHT_PRONG], item, 0, item->rot.y + 512);\n                Sophia_TriggerLaserBolt(\n                    points[M_LEFT_PRONG], item, 0, item->rot.y - 512);\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n\n    if ((item->current_anim_state >= M_STATE_VAULT_2\n         && item->current_anim_state <= M_STATE_GO_DOWN)\n        || item->current_anim_state == M_STATE_DEATH) {\n        creature->maximum_turn = 0;\n        Creature_Animate(item_num, angle, 0);\n    } else {\n        switch (Creature_Vault(item_num, angle, 2, M_VAULT_SHIFT)) {\n        case -4:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_DOWN_4, 0);\n            item->current_anim_state = M_STATE_GO_DOWN;\n            break;\n\n        case 2:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_UP_2, 0);\n            item->current_anim_state = M_STATE_VAULT_2;\n            break;\n\n        case 3:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_UP_3, 0);\n            item->current_anim_state = M_STATE_VAULT_3;\n            break;\n\n        case 4:\n            creature->maximum_turn = 0;\n            Item_SwitchToAnim(item, M_ANIM_UP_4, 0);\n            item->current_anim_state = M_STATE_VAULT_4;\n            break;\n        }\n    }\n\n    int32_t g = (Random_GetControl() & 7)\n        + ABS(Math_Sin(p->wand_glow_phase << 10) >> 9);\n    CLAMPG(g, 31);\n    g <<= 3;\n    Output_AddDynamicLightRGB(\n        points[M_CENTER_PRONG], M_SMALL_FLASH, (RGB_888) { 0, g >> 1, g >> 2 });\n\n    p->wand_glow_phase = (p->wand_glow_phase + 1) & 0x3F;\n\n    if (item->hit_points > 0 && !p->knockback_active\n        && lara_item->hit_points > 0) {\n        const XYZ_32 delta = {\n            .x = lara_item->pos.x - item->pos.x,\n            .y = lara_item->pos.y - item->pos.y - 256,\n            .z = lara_item->pos.z - item->pos.z,\n        };\n        if (XYZ_32_GetLength(delta) < 2816) {\n            p->knockback_active = true;\n            FX_Ring_SpawnKnockBack(item->pos);\n        }\n    } else if (p->knockback_active) {\n        const FX_RING *const ring = FX_Ring_PeekRing(FX_RING_TYPE_KNOCKBACK, 1);\n        if (ring != nullptr && ring->on != 0 && ring->speed >= 0\n            && M_KnockBackCollision(ring)) {\n            FX_Ring_BounceKnockBack();\n        }\n        if (!FX_Ring_IsRingActive(FX_RING_TYPE_KNOCKBACK)) {\n            p->knockback_active = false;\n        }\n    }\n\n    if (item->hit_points <= 0 && p->explode_count == 0) {\n        Lara_Electricity_UpdatePoints();\n    }\n}\n\nstatic void M_DrawShield(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    for (int32_t band = 0; band < 4; band++) {\n        const int32_t sprite_idx =\n            sprite_base + 18 + ((band + (time4 >> 3)) & 7);\n\n        for (int32_t j = 0; j < 8; j++) {\n            const int32_t j2 = (j == 7) ? 0 : (j + 1);\n            const M_SHIELD_POINT *const s00 = &p->shield[band][j];\n            const M_SHIELD_POINT *const s01 = &p->shield[band][j2];\n            const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j];\n            const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2];\n\n            if (((s00->color.r | s00->color.g | s00->color.b | s01->color.r\n                  | s01->color.g | s01->color.b | s11->color.r | s11->color.g\n                  | s11->color.b | s10->color.r | s10->color.g | s10->color.b)\n                 == 0U)) {\n                continue;\n            }\n\n            const XYZ_32 world_pos[4] = {\n                {\n                    item->pos.x + s00->pos.x,\n                    item->pos.y + s00->pos.y,\n                    item->pos.z + s00->pos.z,\n                },\n                {\n                    item->pos.x + s01->pos.x,\n                    item->pos.y + s01->pos.y,\n                    item->pos.z + s01->pos.z,\n                },\n                {\n                    item->pos.x + s11->pos.x,\n                    item->pos.y + s11->pos.y,\n                    item->pos.z + s11->pos.z,\n                },\n                {\n                    item->pos.x + s10->pos.x,\n                    item->pos.y + s10->pos.y,\n                    item->pos.z + s10->pos.z,\n                },\n            };\n            const RGBA_8888 color[4] = {\n                { s00->color.r, s00->color.g, s00->color.b, 255 },\n                { s01->color.r, s01->color.g, s01->color.b, 255 },\n                { s11->color.r, s11->color.g, s11->color.b, 255 },\n                { s10->color.r, s10->color.g, s10->color.b, 255 },\n            };\n            OutputSource_PolyFX_StageSpriteQuadWorld(\n                sprite_idx, world_pos, color, DRAW_BLEND_ADD);\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    const bool result = Object_DrawAnimatingItem(item);\n\n    const M_PRIV *const p = item->priv;\n    if (p->explode_count != 0) {\n        M_DrawShield(item);\n    }\n\n    if (item->hit_points <= 0 && p->explode_count == 0) {\n        Lara_Electricity_Draw(0, item);\n        Lara_Electricity_Draw(1, item);\n    }\n\n    return result;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->draw_func = M_Draw;\n    obj->gun_hit_func = M_GunHit;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n    obj->lot_setup.drop = -STEP_L * 3;\n    obj->shadow_size = 0;\n    obj->hit_points = 300;\n    obj->pivot_length = 50;\n    obj->radius = 102;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_SOPHIA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/sophia_internal.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n#include <trx/game/items/types.h>\n\nvoid Sophia_TriggerPlasmaBall(\n    int32_t type, XYZ_32 pos, int16_t room_num, int16_t angle);\nvoid Sophia_TriggerLaserBolt(\n    XYZ_32 pos, const ITEM *item, int32_t type, int16_t angle);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/sophia_laser_bolt.c",
    "content": "#include \"sophia_internal.h\"\n\n#include <trx/core/math/func.h>\n#include <trx/game/items/manager.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\n#define M_SPEED 384\n\ntypedef struct {\n    int16_t light_falloff;\n    int16_t light_phase;\n    bool summon_bolt;\n    int16_t summon_lifetime;\n} M_PRIV;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    const XYZ_32 old_pos = item->pos;\n    const int16_t old_room_num = item->room_num;\n\n    const int32_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT;\n    item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, speed);\n    item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n\n    if (item->speed < M_SPEED) {\n        item->speed += (item->speed >> 3) + 2;\n    }\n\n    if (p->summon_bolt && item->speed > 192) {\n        p->summon_lifetime++;\n\n        if (p->summon_lifetime >= 16) {\n            Item_Kill(item_num);\n            return;\n        }\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    if (item->room_num != room_num) {\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    if (!p->summon_bolt) {\n        const bool hit = Lara_IsNearItem(&item->pos, 400);\n        item->floor = Room_GetHeight(sector, item->pos);\n        const int32_t c = Room_GetCeiling(sector, item->pos);\n\n        if (hit || item->pos.y >= item->floor || item->pos.y <= c) {\n            Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL);\n            int32_t extras = (p->light_falloff >= 0) + 2;\n\n            Sparks_TriggerExplosionSparks(\n                old_pos, extras, -2, 2, item->room_num);\n            for (int32_t i = 0; i < extras; i++) {\n                Sparks_TriggerExplosionSparks(\n                    old_pos, 2, -1, 2, item->room_num);\n            }\n\n            extras++;\n\n            for (int32_t i = 0; i < extras; i++) {\n                Sophia_TriggerPlasmaBall(1, old_pos, old_room_num, item->rot.y);\n            }\n\n            if (hit) {\n                Lara_TakeDamage(30 + ((p->light_falloff >= 0) << 9), true);\n            } else {\n                const ITEM *const lara_item = Lara_GetItem();\n                const XYZ_32 delta = {\n                    .x = lara_item->pos.x - item->pos.x,\n                    .y = lara_item->pos.y - item->pos.y - 256,\n                    .z = lara_item->pos.z - item->pos.z,\n                };\n                const int32_t dist = XYZ_32_GetLength(delta);\n                if (dist < WALL_L) {\n                    Lara_TakeDamage(\n                        (WALL_L - dist) >> (6 - 2 * (p->light_falloff >= 0)),\n                        true);\n                }\n            }\n\n            Item_Kill(item_num);\n            return;\n        }\n    }\n\n    int32_t g = 255 - (Random_GetControl() & 0x3F);\n    int32_t b = g >> 1;\n    int32_t falloff;\n\n    if (p->light_falloff < 0) {\n        if (p->summon_bolt) {\n            falloff = 16 - p->summon_lifetime;\n            g = (falloff * g) >> 4;\n            b = (falloff * b) >> 4;\n        }\n\n        falloff = -p->light_falloff;\n\n        if (falloff > 10) {\n            p->light_phase += 2;\n            p->light_falloff++;\n        }\n    } else {\n        falloff = p->light_falloff;\n\n        if (falloff > 16) {\n            p->light_phase += 4;\n            p->light_falloff--;\n        }\n    }\n\n    Output_AddDynamicLightRGB(item->pos, falloff, (RGB_888) { 0, g, b });\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    const XYZ_32 origin = item->interp.result.pos;\n    const XYZ_16 rot = item->interp.result.rot;\n\n    const int32_t beam_speed = (item->speed * Math_Cos(rot.x)) >> W2V_SHIFT;\n    const XYZ_32 dir = {\n        .x = (beam_speed * Math_Sin(rot.y)) >> W2V_SHIFT,\n        .y = -((item->speed * Math_Sin(rot.x)) >> W2V_SHIFT),\n        .z = (beam_speed * Math_Cos(rot.y)) >> W2V_SHIFT,\n    };\n\n    Matrix_PushUnit();\n    Matrix_Rot16(rot);\n\n    const int32_t radius =\n        p->summon_bolt ? p->light_phase >> 1 : p->light_phase;\n    XYZ_32 base[4] = {};\n    for (int32_t i = 0; i < 4; i++) {\n        const XYZ_32 local = {\n            .x = (i == 0 || i == 3) ? -radius : radius,\n            .y = (i < 2) ? -radius : radius,\n            .z = 0,\n        };\n        base[i] = Matrix_MulVec32_M(g_WMatrixPtr, local);\n    }\n\n    Matrix_Pop();\n\n    const int32_t section_count = p->summon_bolt ? 3 : 6;\n    XYZ_32 sections[6][4] = {};\n    uint8_t intensities[6] = {};\n\n    for (int32_t i = 0; i < 4; i++) {\n        sections[0][i] = origin;\n    }\n    intensities[0] = 128;\n\n    const int32_t ring_count = section_count - 2;\n    for (int32_t i = 0; i < ring_count; i++) {\n        const int32_t step = i + 2;\n        const XYZ_32 center = {\n            .x = origin.x - dir.x * step,\n            .y = origin.y - dir.y * step,\n            .z = origin.z - dir.z * step,\n        };\n        const int32_t scale = 4 - i;\n        for (int32_t j = 0; j < 4; j++) {\n            sections[i + 1][j] = (XYZ_32) {\n                .x = center.x + (base[j].x * scale) / 4,\n                .y = center.y + (base[j].y * scale) / 4,\n                .z = center.z + (base[j].z * scale) / 4,\n            };\n        }\n        intensities[i + 1] = 64 - (i << 4);\n    }\n\n    const int32_t end_step = p->summon_bolt ? 5 : 6;\n    const XYZ_32 end = {\n        .x = origin.x - dir.x * end_step,\n        .y = origin.y - dir.y * end_step,\n        .z = origin.z - dir.z * end_step,\n    };\n    for (int32_t i = 0; i < 4; i++) {\n        sections[section_count - 1][i] = end;\n    }\n\n    const int32_t fade = p->summon_bolt ? 16 - p->summon_lifetime : 16;\n    for (int32_t i = 0; i < section_count - 1; i++) {\n        int32_t c0 = intensities[i];\n        int32_t c1 = intensities[i + 1];\n        if (p->summon_bolt) {\n            c0 = (c0 * fade) >> 4;\n            c1 = (c1 * fade) >> 4;\n        }\n\n        const RGBA_8888 color0 = { c0 >> 2, c0, c0 >> 1, 0xC0 };\n        const RGBA_8888 color1 = { c1 >> 2, c1, c1 >> 1, 0xC0 };\n        for (int32_t j = 0; j < 4; j++) {\n            const int32_t next = (j + 1) & 3;\n            const XYZ_32 world_pos[4] = {\n                sections[i][j],\n                sections[i + 1][next],\n                sections[i + 1][j],\n                sections[i][next],\n            };\n            const RGBA_8888 color[4] = { color0, color0, color1, color1 };\n            OutputSource_PolyFX_StageQuadExt(\n                -1, world_pos, nullptr, color,\n                VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE,\n                DRAW_BLEND_ADD);\n\n            const XYZ_32 world_pos_rev[4] = {\n                world_pos[3],\n                world_pos[2],\n                world_pos[1],\n                world_pos[0],\n            };\n            OutputSource_PolyFX_StageQuadExt(\n                -1, world_pos_rev, nullptr, color,\n                VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE,\n                DRAW_BLEND_ADD);\n        }\n    }\n\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->priv_size = sizeof(M_PRIV);\n}\n\nvoid Sophia_TriggerLaserBolt(\n    const XYZ_32 pos, const ITEM *const item, const int32_t type,\n    const int16_t angle)\n{\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const bolt = Item_Get(item_num);\n    bolt->object_id = O_SOPHIA_LASER_BOLT;\n    bolt->room_num = item->room_num;\n    bolt->pos = pos;\n    Item_Initialise(item_num);\n\n    M_PRIV *const bolt_priv = bolt->priv;\n    if (type == 2) {\n        bolt->pos.y += item->pos.y - 384;\n        bolt->rot.x = -pos.y << 5;\n        bolt->rot.y = Random_GetControl() << 1;\n    } else {\n        const ITEM *const lara_item = Lara_GetItem();\n        int16_t angles[2];\n        Math_GetVectorAngles(\n            lara_item->pos.x - pos.x, lara_item->pos.y - pos.y - 256,\n            lara_item->pos.z - pos.z, angles);\n        bolt->rot.x = angles[1];\n        bolt->rot.y = angle;\n        bolt->rot.z = 0;\n    }\n\n    if (type == 1) {\n        bolt->speed = 24;\n        bolt_priv->light_falloff = 31;\n        bolt_priv->light_phase = 16;\n    } else {\n        bolt->speed = 16;\n        bolt_priv->light_falloff = -24;\n        bolt_priv->light_phase = 4;\n\n        if (type == 2) {\n            bolt_priv->summon_bolt = true;\n        }\n    }\n\n    Item_AddActive(item_num);\n    bolt->status = IS_ACTIVE;\n}\n\nREGISTER_OBJECT(O_SOPHIA_LASER_BOLT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/sophia_plasma_ball.c",
    "content": "#include \"sophia_internal.h\"\n\n#include <trx/core/math/func.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n\nstatic const uint8_t m_Falloffs[2] = { 13, 7 };\n\nstatic void M_TriggerPlasmaBallFlame(const int16_t effect_num, const XYZ_32 vel)\n{\n    const EFFECT *const effect = Effect_Get(effect_num);\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - effect->pos.x;\n    const int32_t dz = lara_item->pos.z - effect->pos.z;\n\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 48;\n    spark->src_color.g = 255;\n    spark->src_color.b = (Random_GetControl() & 0x1F) + 48;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 128;\n\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = vel.x + (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = vel.y;\n    spark->vel.z = vel.z + (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 5;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->effect_num = effect_num;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    spark->size.width = (Random_GetControl() & 0x1F) + 64;\n    spark->size.height = spark->size.width;\n    spark->src_size.width = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->dst_size.height = spark->size.height >> 2;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->fall_speed++;\n    const int32_t old_y = effect->pos.y;\n\n    if (effect->speed > 8) {\n        effect->speed -= 2;\n    }\n    if (effect->rot.x > -15360) {\n        effect->rot.x -= 256;\n    }\n\n    const int32_t speed =\n        (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n    effect->pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, speed);\n    effect->pos.y += effect->fall_speed\n        - ((effect->speed * Math_Sin(effect->rot.x)) >> W2V_SHIFT);\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if ((time4 & 0xF) == 0) {\n        M_TriggerPlasmaBallFlame(\n            effect_num,\n            (XYZ_32) { .x = 0, .y = ABS(old_y - effect->pos.y) << 3, .z = 0 });\n    }\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t h = Room_GetHeight(sector, effect->pos);\n    const int32_t c = Room_GetCeiling(sector, effect->pos);\n\n    if (effect->pos.y >= h || effect->pos.y < c\n        || Room_Get(room_num)->flags.underwater) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->flag2 == 0 && Lara_IsNearItem(&effect->pos, 200)) {\n        Lara_TakeDamage(25, true);\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n\n    const int32_t color_base = Random_GetControl();\n    const RGB_888 color = {\n        .r = color_base & 0x3F,\n        .g = 255 - ((color_base >> 4) & 0x1F),\n        .b = 192 - ((color_base >> 6) & 0x1F),\n    };\n    Output_AddDynamicLightRGB(effect->pos, m_Falloffs[effect->flag1], color);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n}\n\nvoid Sophia_TriggerPlasmaBall(\n    const int32_t type, const XYZ_32 pos, const int16_t room_num,\n    const int16_t angle)\n{\n    const int16_t fx_num = Effect_Create(room_num);\n    if (fx_num == NO_ITEM) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(fx_num);\n    effect->speed = (Random_GetControl() & 0x1F) + 64;\n    effect->pos = pos;\n    effect->rot.x = DEG_45;\n    effect->rot.y = angle + Random_GetControl() + DEG_90;\n    effect->object_id = O_SOPHIA_PLASMA_BALL;\n    effect->fall_speed = 0;\n    effect->flag1 = 1;\n    effect->flag2 = type == 2;\n}\n\nREGISTER_OBJECT(O_SOPHIA_PLASMA_BALL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/spider.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define SPIDER_HITPOINTS      5\n#define SPIDER_TURN           (DEG_1 * 8) // = 1456\n#define SPIDER_RADIUS         (WALL_L / 10) // = 102\n#define SPIDER_ATTACK_2_RANGE SQUARE(WALL_L / 2) // = 262144\n#define SPIDER_ATTACK_3_RANGE SQUARE(WALL_L / 5) // = 41616\n#define SPIDER_DAMAGE         25\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    SPIDER_STATE_EMPTY    = 0,\n    SPIDER_STATE_STOP     = 1,\n    SPIDER_STATE_WALK_1   = 2,\n    SPIDER_STATE_WALK_2   = 3,\n    SPIDER_STATE_ATTACK_1 = 4,\n    SPIDER_STATE_ATTACK_2 = 5,\n    SPIDER_STATE_ATTACK_3 = 6,\n    SPIDER_STATE_DEATH    = 7,\n    // clang-format on\n} SPIDER_STATE;\n\ntypedef enum {\n    SPIDER_ANIM_LEAP = 2,\n} SPIDER_ANIM;\n\nstatic const BITE m_SpiderBite = {\n    .pos = { .x = 0, .y = 0, .z = 41 },\n    .mesh_num = 1,\n};\n\nstatic void M_Leap(const int16_t item_num, const int16_t angle)\n{\n    ITEM *const item = Item_Get(item_num);\n    const XYZ_32 old_pos = item->pos;\n    const int16_t old_room_num = item->room_num;\n\n    Creature_Animate(item_num, angle, 0);\n    if (item->pos.y > old_pos.y - STEP_L * 3 / 2) {\n        return;\n    }\n\n    item->pos = old_pos;\n    Item_UpdateRoom(item_num, old_room_num);\n    Item_SwitchToAnim(item, SPIDER_ANIM_LEAP, 0);\n    item->current_anim_state = SPIDER_STATE_ATTACK_2;\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    int16_t angle = 0;\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, SPIDER_TURN);\n\n        switch (item->current_anim_state) {\n        case SPIDER_STATE_STOP:\n            creature->flags = 0;\n            if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < 256) {\n                    item->goal_anim_state = SPIDER_STATE_WALK_1;\n                }\n            } else if (info.ahead && item->touch_bits != 0) {\n                item->goal_anim_state = SPIDER_STATE_ATTACK_1;\n            } else if (creature->mood == MOOD_STALK) {\n                item->goal_anim_state = SPIDER_STATE_WALK_1;\n            } else if (\n                creature->mood == MOOD_ESCAPE\n                || creature->mood == MOOD_ATTACK) {\n                item->goal_anim_state = SPIDER_STATE_WALK_2;\n            }\n            break;\n\n        case SPIDER_STATE_WALK_1:\n            if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < 256) {\n                    item->goal_anim_state = SPIDER_STATE_STOP;\n                }\n            } else if (\n                creature->mood == MOOD_ESCAPE\n                || creature->mood == MOOD_ATTACK) {\n                item->goal_anim_state = SPIDER_STATE_WALK_2;\n            }\n            break;\n\n        case SPIDER_STATE_WALK_2:\n            creature->flags = 0;\n            if (creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) {\n                item->goal_anim_state = SPIDER_STATE_WALK_1;\n            } else if (info.ahead && item->touch_bits != 0) {\n                item->goal_anim_state = SPIDER_STATE_STOP;\n            } else if (info.ahead && info.distance < SPIDER_ATTACK_3_RANGE) {\n                item->goal_anim_state = SPIDER_STATE_ATTACK_3;\n            } else if (info.ahead && info.distance < SPIDER_ATTACK_2_RANGE) {\n                item->goal_anim_state = SPIDER_STATE_ATTACK_2;\n            }\n            break;\n\n        case SPIDER_STATE_ATTACK_1:\n        case SPIDER_STATE_ATTACK_2:\n        case SPIDER_STATE_ATTACK_3:\n            if (creature->flags == 0 && item->touch_bits != 0) {\n                Creature_Effect(item, &m_SpiderBite, Spawn_Blood);\n                Lara_TakeDamage(SPIDER_DAMAGE, true);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    } else if (Item_Explode(item_num, -1, 0)) {\n        LOT_DisableBaddieAI(item_num);\n        Item_Kill(item_num);\n        item->status = IS_DEACTIVATED;\n        Sound_Effect(SFX_SPIDER_EXPLODE, &item->pos, SPM_NORMAL);\n        Carrier_TestItemDrops(item_num);\n        return;\n    }\n\n    M_Leap(item_num, angle);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = SPIDER_HITPOINTS;\n    obj->radius = SPIDER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_JUMPER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_SPIDER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/swat.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/fx/laser.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS       (WALL_L / 10)      // = 102\n#define M_HIT_POINTS   45\n#define M_DAMAGE       28\n#define M_ALERT_DIST   SQUARE(WALL_L)     // = 1048576\n#define M_ALERT_HEIGHT (WALL_L * 2)       // = 2048\n#define M_RUN_DIST     SQUARE(WALL_L * 2) // = 4194304\n#define M_SHOOT_DIST   SQUARE(WALL_L * 3) // = 9437184\n#define M_WALK_TURN    (DEG_1 * 6)        // = 1092\n#define M_RUN_TURN     (DEG_1 * 9)        // = 1638\n// clang-format on\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_WAIT,\n    M_STATE_SHOOT_1,\n    M_STATE_SHOOT_2,\n    M_STATE_DEATH,\n    M_STATE_AIM_1,\n    M_STATE_AIM_2,\n    M_STATE_AIM_3,\n    M_STATE_SHOOT_3,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_STAND    = 12,\n    M_ANIM_WALK_END = 17,\n    M_ANIM_DEATH    = 19,\n    // clang-format on\n} M_ANIM;\n\nstatic const CREATURE_GUN m_SwatGun = {\n    .muzzle = { .pos = { 0, 300, 64 }, .mesh_num = 7 },\n    .tr3_enemy_flash = true,\n    .tr3_flash = { .pos = { 0, 300, 56 }, .mesh_num = 7 },\n    .tr3_enemy_weapon_flags = 1,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_90,\n    .tr3_laser = {\n        .bite = {\n            .pos = { 0, 200, 106 },\n            .mesh_num = 7,\n        },\n        .color = { 0xFF, 0x02, 0x03, 0xDE },\n        .width = 2.0f,\n    },\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Creature_Initialise(item_num);\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_STAND, 0);\n    item->goal_anim_state = M_STATE_STOP;\n    item->current_anim_state = M_STATE_STOP;\n}\n\nstatic void M_TriggerLaser(const ITEM *const item)\n{\n    if (item->hit_points > 0 || !Item_TestFrameEqual(item, -1)) {\n        FX_Laser_Spawn(item, &m_SwatGun);\n    }\n}\n\nstatic void M_FireFinalShot(\n    ITEM *const item, int16_t *const head, int16_t *const torso_y)\n{\n    const int16_t frame_idx = Item_GetRelativeFrame(item);\n    if (frame_idx <= 44 || frame_idx >= 52 || (item->frame_num & 3) != 0) {\n        return;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    if (!Creature_CanTargetEnemy(item, &info) || ABS(info.angle) >= DEG_45) {\n        return;\n    }\n\n    *head = info.angle;\n    *torso_y = info.angle;\n    Creature_Shoot(item, &info, &m_SwatGun, info.angle, M_DAMAGE * 3);\n    const SAMPLE_TRX_ID fire_sfx = item->object_id == O_SWAT_3\n        ? SFX_AMERICAN_SWAT_FIRE\n        : SFX_LONDON_SWAT_FIRE;\n    Sound_Effect(fire_sfx, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    Creature_TestBoxDamage(item_num);\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            creature->flags = (Random_GetControl() & 1) == 0 ? 1 : 0;\n        } else if (creature->flags != 0) {\n            M_FireFinalShot(item, &head, &torso_y);\n        }\n        goto finish;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->ai_bits != 0) {\n        Creature_GetAITarget(creature);\n    } else {\n        creature->enemy = lara_item;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    AI_INFO lara_info = {};\n    if (creature->enemy == lara_item) {\n        lara_info.distance = info.distance;\n        lara_info.angle = info.angle;\n    } else {\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        lara_info.angle = Math_Atan(dz, dx) - item->rot.y;\n        lara_info.distance = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    }\n\n    Creature_Mood(item, &info, creature->enemy != lara_item);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    ITEM *const enemy = creature->enemy;\n    creature->enemy = lara_item;\n    if (item->hit_status\n        || ((lara_info.distance < M_ALERT_DIST\n             || Creature_CanSeeEnemy(item, &lara_info))\n            && ABS(lara_item->pos.y - item->pos.y) < M_ALERT_HEIGHT)) {\n        if (!creature->alerted) {\n            const SAMPLE_TRX_ID alert_sfx = item->object_id == O_SWAT_3\n                ? SFX_AMERICAN_HOY\n                : SFX_ENGLISH_HOY;\n            Sound_Effect(alert_sfx, &item->pos, SPM_NORMAL);\n        }\n        Creature_AlertAllGuards(item_num);\n    }\n    creature->enemy = enemy;\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n\n        if (Item_GetRelativeAnim(item) == M_ANIM_WALK_END) {\n            if (ABS(info.angle) < M_RUN_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_RUN_TURN;\n            } else {\n                item->rot.y += M_RUN_TURN;\n            }\n        }\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                item->goal_anim_state = M_STATE_WAIT;\n            }\n        } else if ((item->ai_bits & AI_PATROL_1) != 0) {\n            head = 0;\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance >= M_SHOOT_DIST\n                && info.zone_num == info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (Random_GetControl() < 0x4000) {\n                item->goal_anim_state = M_STATE_AIM_1;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_3;\n            }\n        } else if (\n            creature->mood == MOOD_BORED\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.distance > M_RUN_DIST) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else {\n            item->goal_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_WAIT:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = 0;\n\n        if ((item->ai_bits & AI_GUARD) != 0) {\n            head = Creature_AIGuard(creature);\n            if ((Random_GetControl() & 0xFF) == 0) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_SHOOT_1;\n        } else if (creature->mood != MOOD_BORED || !info.ahead) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_WALK:\n        head = lara_info.angle;\n        creature->flags = 0;\n        creature->maximum_turn = M_WALK_TURN;\n\n        if ((item->ai_bits & AI_PATROL_1) != 0) {\n            head = 0;\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = M_STATE_RUN;\n        } else if (\n            (item->ai_bits & AI_GUARD) != 0\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (Creature_CanTargetEnemy(item, &info)) {\n            if (info.distance < M_SHOOT_DIST\n                || info.zone_num != info.enemy_zone_num) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_AIM_2;\n            }\n        } else if (creature->mood == MOOD_BORED) {\n            if (info.ahead) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else if (info.distance > M_RUN_DIST) {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_RUN:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->maximum_turn = M_RUN_TURN;\n        tilt = angle / 2;\n\n        if ((item->ai_bits & AI_GUARD) != 0\n            || ((item->ai_bits & AI_FOLLOW) != 0\n                && (creature->reached_goal\n                    || lara_info.distance > M_RUN_DIST))) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood != MOOD_ESCAPE) {\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                creature->mood == MOOD_BORED\n                || (creature->mood == MOOD_STALK\n                    && (item->ai_bits & AI_FOLLOW) == 0\n                    && info.distance < M_RUN_DIST)) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n        }\n        break;\n\n    case M_STATE_AIM_1:\n    case M_STATE_AIM_2:\n    case M_STATE_AIM_3:\n        creature->flags = 0;\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n\n            if (!Creature_CanTargetEnemy(item, &info)) {\n                if (item->current_anim_state == M_STATE_AIM_2) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else if (item->current_anim_state == M_STATE_AIM_1) {\n                item->goal_anim_state = M_STATE_SHOOT_1;\n            } else if (item->current_anim_state == M_STATE_AIM_2) {\n                item->goal_anim_state = M_STATE_SHOOT_2;\n            } else {\n                item->goal_anim_state = M_STATE_SHOOT_3;\n            }\n        }\n        break;\n\n    case M_STATE_SHOOT_1:\n    case M_STATE_SHOOT_2:\n    case M_STATE_SHOOT_3:\n        if (item->current_anim_state == M_STATE_SHOOT_3\n            && item->goal_anim_state != M_STATE_STOP\n            && (creature->mood == MOOD_ESCAPE || info.distance > M_SHOOT_DIST\n                || !Creature_CanTargetEnemy(item, &info))) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n\n        if (info.ahead) {\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n        }\n\n        if (creature->flags == 0) {\n            Creature_Shoot(item, &info, &m_SwatGun, torso_y, M_DAMAGE);\n            creature->flags = 5;\n        } else {\n            creature->flags--;\n        }\n        break;\n\n    default:\n        break;\n    }\n\nfinish:\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head);\n    Creature_Animate(item_num, angle, 0);\n\n    M_TriggerLaser(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n\n    obj->intelligent = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n    obj->save_position = true;\n\n    Object_GetBone(obj, 0)->rot.x = true;\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_SWAT_1, M_Setup)\nREGISTER_OBJECT(O_SWAT_2, M_Setup)\nREGISTER_OBJECT(O_SWAT_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tiger.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n// clang-format off\n#define TIGER_HITPOINTS      (g_TRVersion == 3 ? 24 : 20)\n#define TIGER_TOUCH_BITS     0b00000111'11111101'11000000'00000000\n#define TIGER_RADIUS         (WALL_L / 3) // = 341\n#define TIGER_WALK_TURN      (DEG_1 * 3) // = 546\n#define TIGER_RUN_TURN       (DEG_1 * 6) // = 1092\n#define TIGER_ATTACK_1_RANGE SQUARE(WALL_L / 3) // = 116281\n#define TIGER_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296\n#define TIGER_ATTACK_3_RANGE SQUARE(WALL_L) // = 1048576\n#define TIGER_BITE_DAMAGE    (g_TRVersion == 3 ? 90 : 100)\n#define TIGER_ROAR_CHANCE    96\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    TIGER_STATE_EMPTY    = 0,\n    TIGER_STATE_STOP     = 1,\n    TIGER_STATE_WALK     = 2,\n    TIGER_STATE_RUN      = 3,\n    TIGER_STATE_WAIT     = 4,\n    TIGER_STATE_ROAR     = 5,\n    TIGER_STATE_ATTACK_1 = 6,\n    TIGER_STATE_ATTACK_2 = 7,\n    TIGER_STATE_ATTACK_3 = 8,\n    TIGER_STATE_DEATH    = 9,\n    // clang-format on\n} TIGER_STATE;\n\ntypedef enum {\n    TIGER_ANIM_DEATH = 11,\n} TIGER_ANIM;\n\nstatic const BITE m_TigerBite = {\n    .pos = { .x = 19, .y = -13, .z = 3 },\n    .mesh_num = 26,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_UpdateMood(item, &info, true);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        if (g_TRVersion == 3 && creature->alerted\n            && info.zone_num != info.enemy_zone_num) {\n            creature->mood = MOOD_ESCAPE;\n        }\n        Creature_ApplyMood(item, &info, true);\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case TIGER_STATE_STOP:\n            creature->maximum_turn = 0;\n            creature->flags = 0;\n\n            if (creature->mood == MOOD_ESCAPE) {\n                if (g_TRVersion < 3) {\n                    item->goal_anim_state = TIGER_STATE_RUN;\n                } else if (lara->target == item || !info.ahead) {\n                    item->goal_anim_state = TIGER_STATE_RUN;\n                } else {\n                    item->goal_anim_state = TIGER_STATE_STOP;\n                }\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < TIGER_ROAR_CHANCE) {\n                    item->goal_anim_state = TIGER_STATE_ROAR;\n                }\n                item->goal_anim_state = TIGER_STATE_WALK;\n            } else if (\n                (g_TRVersion == 3 ? info.bite : info.ahead)\n                && info.distance < TIGER_ATTACK_1_RANGE) {\n                item->goal_anim_state = TIGER_STATE_ATTACK_1;\n            } else if (\n                (g_TRVersion == 3 ? info.bite : info.ahead)\n                && info.distance < TIGER_ATTACK_3_RANGE) {\n                creature->maximum_turn = TIGER_WALK_TURN;\n                item->goal_anim_state = TIGER_STATE_ATTACK_3;\n            } else if (item->required_anim_state != TIGER_STATE_EMPTY) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (\n                creature->mood != MOOD_ATTACK\n                && Random_GetControl() < TIGER_ROAR_CHANCE) {\n                item->goal_anim_state = TIGER_STATE_ROAR;\n            } else {\n                item->goal_anim_state = TIGER_STATE_RUN;\n            }\n            break;\n\n        case TIGER_STATE_WALK:\n            creature->maximum_turn = TIGER_WALK_TURN;\n            if (g_TRVersion == 3\n                && (creature->mood == MOOD_ESCAPE\n                    || creature->mood == MOOD_ATTACK)) {\n                item->goal_anim_state = TIGER_STATE_RUN;\n            } else if (Random_GetControl() < TIGER_ROAR_CHANCE) {\n                item->goal_anim_state = TIGER_STATE_STOP;\n                item->required_anim_state = TIGER_STATE_ROAR;\n            }\n            break;\n\n        case TIGER_STATE_RUN:\n            creature->maximum_turn = TIGER_RUN_TURN;\n            if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = TIGER_STATE_STOP;\n            } else if (creature->flags != 0 && info.ahead) {\n                item->goal_anim_state = TIGER_STATE_STOP;\n            } else if (\n                (g_TRVersion == 3 ? info.bite : info.ahead)\n                && info.distance < TIGER_ATTACK_2_RANGE) {\n                const ITEM *const lara_item = Lara_GetItem();\n                if (lara_item->speed != 0) {\n                    item->goal_anim_state = TIGER_STATE_ATTACK_2;\n                } else {\n                    item->goal_anim_state = TIGER_STATE_STOP;\n                }\n            } else if (\n                creature->mood != MOOD_ATTACK\n                && Random_GetControl() < TIGER_ROAR_CHANCE) {\n                item->required_anim_state = TIGER_STATE_ROAR;\n                item->goal_anim_state = TIGER_STATE_STOP;\n            } else if (\n                g_TRVersion == 3 && creature->mood == MOOD_ESCAPE\n                && lara->target != item && info.ahead) {\n                item->goal_anim_state = TIGER_STATE_STOP;\n            }\n            creature->flags = 0;\n            break;\n\n        case TIGER_STATE_ATTACK_1:\n        case TIGER_STATE_ATTACK_2:\n        case TIGER_STATE_ATTACK_3:\n            if (creature->flags == 0\n                && (item->touch_bits & TIGER_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(TIGER_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_TigerBite, Spawn_Blood);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    } else if (item->current_anim_state != TIGER_STATE_DEATH) {\n        Item_SwitchToAnim(item, TIGER_ANIM_DEATH, 0);\n        item->current_anim_state = TIGER_STATE_DEATH;\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, tilt);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = TIGER_HITPOINTS;\n    obj->radius = TIGER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 200;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 21)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_TIGER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tony.c",
    "content": "#include \"tony_internal.h\"\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\ntypedef enum {\n    M_PHASE_DORMANT = 0,\n    M_PHASE_AWAKENED = 1,\n    M_PHASE_PHASE2 = 2,\n} M_PHASE;\n\ntypedef enum {\n    M_STATE_WAIT,\n    M_STATE_RISE,\n    M_STATE_FLOAT,\n    M_STATE_ZAPP,\n    M_STATE_ROCK_ZAPP,\n    M_STATE_BIG_ROOM,\n    M_STATE_DEATH\n} M_STATE;\n\ntypedef struct {\n    XYZ_16 pos;\n    RGB_888 sub;\n    RGB_888 color;\n} M_SHIELD_POINT;\n\ntypedef struct {\n    bool dropped_item;\n    uint8_t ring_count;\n    int16_t explode_count;\n    bool dead;\n    M_SHIELD_POINT shield[5][8];\n    M_PHASE phase;\n\n    // Alternates the chosen attack while in the FLOAT state (ROCK_ZAPP vs ZAPP)\n    bool attack_toggle;\n} M_PRIV;\n\nstatic const int32_t m_Heights[5] = { -1536, -1280, -832, -384, 0 };\nstatic const int32_t m_Dist[5] = { 200, 400, 500, 500, 475 };\nstatic const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 };\nstatic const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 };\nstatic const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 };\nstatic int32_t m_DeathDist[5] = {};\nstatic int32_t m_DeathHeights[5] = {};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"dropped_item\", &p->dropped_item));\n    JSON_SHOULD(JSON_READ(io, \"ring_count\", &p->ring_count));\n    JSON_SHOULD(JSON_READ(io, \"explode_count\", &p->explode_count));\n    JSON_SHOULD(JSON_READ(io, \"dead\", &p->dead));\n    JSON_SHOULD(JSON_READ(io, \"phase\", &p->phase));\n    JSON_SHOULD(JSON_READ(io, \"attack_toggle\", &p->attack_toggle));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"dropped_item\", p->dropped_item);\n    JSONW_WRITE(io, \"ring_count\", p->ring_count);\n    JSONW_WRITE(io, \"explode_count\", p->explode_count);\n    JSONW_WRITE(io, \"dead\", p->dead);\n    JSONW_WRITE(io, \"phase\", p->phase);\n    JSONW_WRITE(io, \"attack_toggle\", p->attack_toggle);\n}\n\nstatic void M_TriggerFlame(int16_t item_num, int32_t node)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n\n    ITEM *const item = Item_Get(item_num);\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n\n    if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.b = 48;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 5;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->gravity = -16 - (Random_GetControl() & 0x1F);\n    spark->max_y_vel = -16 - (Random_GetControl() & 7);\n    spark->item_num = item_num;\n    spark->node_num = node;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    spark->size.width = (Random_GetControl() & 0x1F) + 64;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 2;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Explode(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n\n    if (item->hit_points <= 0\n        && (p->explode_count == 1 || p->explode_count == 15\n            || p->explode_count == 25 || p->explode_count == 35\n            || p->explode_count == 45 || p->explode_count == 55)) {\n        const XYZ_32 pos = {\n            .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512,\n            .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256,\n            .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512,\n        };\n\n        FX_RING *const ring =\n            FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count);\n        if (ring != nullptr) {\n            ring->pos = pos;\n            ring->on = 2;\n            FX_Ring_Sync(ring);\n            p->ring_count++;\n        }\n\n        Sparks_TriggerExplosionSparks(pos, 3, -2, 0, 0);\n        for (int32_t i = 0; i < 2; i++) {\n            Sparks_TriggerExplosionSparks(pos, 3, -1, 0, 0);\n        }\n\n        Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH);\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        if (p->explode_count < 128) {\n            m_DeathDist[i] =\n                (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7);\n            m_DeathHeights[i] = m_DHeights2[i]\n                + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7);\n        }\n    }\n\n    if (p->explode_count > 64) {\n        return;\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t y = m_DeathHeights[i];\n        const int32_t dist = m_DeathDist[i];\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        int32_t angle = (time4 & 0x3F) << 3;\n\n        for (int32_t j = 0; j < 8; j++) {\n            M_SHIELD_POINT *const shield = &p->shield[i][j];\n            shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13;\n            shield->pos.y = y;\n            shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13;\n\n            if (i != 0 && i != 4 && p->explode_count < 64) {\n                int32_t r = (Random_GetDraw() & 0x1F) + 224;\n                int32_t g = (r >> 2) + (Random_GetDraw() & 0x3F);\n                int32_t b = Random_GetDraw() & 0x3F;\n\n                if (item->hit_points <= 0) {\n                    r = ((64 - p->explode_count) * r) >> 6;\n                    g = ((64 - p->explode_count) * g) >> 6;\n                    b = ((64 - p->explode_count) * b) >> 6;\n                } else {\n                    r = ((128 - p->explode_count) * r) >> 7;\n                    g = ((128 - p->explode_count) * g) >> 7;\n                    b = ((128 - p->explode_count) * b) >> 7;\n                }\n\n                shield->color = (RGB_888) { r, g, b };\n            } else {\n                shield->color = COLOR_RGB_888_BLACK;\n            }\n\n            angle = (angle + 512) & 0xFFF;\n        }\n    }\n}\n\nstatic bool M_CanDropItems(const ITEM *const item)\n{\n    if (item->hit_points > 0) {\n        return false;\n    }\n    if ((item->flags & IF_KILLED) != 0) {\n        return true;\n    }\n    return item->current_anim_state == M_STATE_DEATH\n        && Item_GetRelativeFrame(item) >= 110;\n}\n\nstatic bool M_CanBeExploded(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Die(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->hit_points = 0;\n    item->collidable = false;\n    Item_Kill(item_num);\n    LOT_DisableBaddieAI(item_num);\n    item->flags |= IF_INVISIBLE;\n}\n\nstatic void M_Initialise(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->dead = false;\n    p->dropped_item = false;\n    p->ring_count = 0;\n    p->explode_count = 0;\n    p->attack_toggle = false;\n    p->phase = M_PHASE_DORMANT;\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t dist = m_Dist[i];\n        int32_t angle = 0;\n\n        for (int32_t j = 0; j < 8; j++) {\n            M_SHIELD_POINT *const shield = &p->shield[i][j];\n            shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13;\n            shield->pos.y = m_Heights[i];\n            shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13;\n            shield->color = COLOR_RGB_888_BLACK;\n            angle += 512;\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const tony = item->creature_data;\n    int16_t angle = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, 6, 0);\n            item->current_anim_state = M_STATE_DEATH;\n        }\n\n        if (Item_GetRelativeFrame(item) > 110) {\n            Item_SwitchToAnim(item, Item_GetRelativeAnim(item), 110);\n            item->mesh_bits = 0;\n\n            if (!p->explode_count) {\n                p->ring_count = 0;\n\n                for (int32_t i = 0; i < 6; i++) {\n                    FX_RING *const ring =\n                        FX_Ring_GetRing(FX_RING_TYPE_BLAST, i);\n                    if (ring == nullptr) {\n                        continue;\n                    }\n                    ring->on = 0;\n                    ring->life = 32;\n                    ring->radius = 512;\n                    ring->speed = 128 + (i << 5);\n                    ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF;\n                    ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF;\n                    FX_Ring_Sync(ring);\n                }\n\n                if (!p->dropped_item) {\n                    Carrier_TestItemDrops(item_num);\n                    p->dropped_item = true;\n                }\n            }\n\n            if (p->explode_count < 256) {\n                p->explode_count++;\n            }\n\n            if (p->explode_count > 128 && p->ring_count == 6\n                && FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life == 0) {\n                M_Die(item_num);\n                p->dead = true;\n            } else {\n                M_Explode(item);\n            }\n\n            return;\n        }\n    } else {\n        if (p->phase != M_PHASE_PHASE2) {\n            item->hit_points = item->max_hit_points;\n        }\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (p->phase != M_PHASE_DORMANT) {\n            tony->target.x = lara_item->pos.x;\n            tony->target.z = lara_item->pos.z;\n            angle = Creature_Turn(item, tony->maximum_turn);\n        } else {\n            const int32_t x = item->pos.x - lara_item->pos.x;\n            const int32_t z = item->pos.z - lara_item->pos.z;\n            if (SQUARE(x) + SQUARE(z) < 0x1900000) {\n                p->phase = M_PHASE_AWAKENED;\n            }\n\n            angle = 0;\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_WAIT:\n            tony->maximum_turn = 0;\n            if (item->goal_anim_state != M_STATE_RISE\n                && p->phase != M_PHASE_DORMANT) {\n                item->goal_anim_state = M_STATE_RISE;\n            }\n            break;\n\n        case M_STATE_RISE:\n            if (Item_GetRelativeFrame(item) <= 16) {\n                tony->maximum_turn = 0;\n            } else {\n                tony->maximum_turn = 364;\n            }\n            break;\n\n        case M_STATE_FLOAT:\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n            tony->maximum_turn = 364;\n\n            if (p->explode_count == 0) {\n                if (item->goal_anim_state != M_STATE_BIG_ROOM\n                    && p->phase != M_PHASE_PHASE2) {\n                    item->goal_anim_state = M_STATE_BIG_ROOM;\n                    tony->maximum_turn = 0;\n                }\n\n                const int32_t time4 = Output_GetTimeInGame() * 4;\n                if (item->goal_anim_state != M_STATE_ROCK_ZAPP\n                    && p->phase == M_PHASE_PHASE2 && !(time4 & 0xFF)\n                    && !p->attack_toggle) {\n                    item->goal_anim_state = M_STATE_ROCK_ZAPP;\n                    p->attack_toggle = true;\n                }\n\n                if (item->goal_anim_state != M_STATE_ZAPP\n                    && item->goal_anim_state != M_STATE_ROCK_ZAPP\n                    && p->phase == M_PHASE_PHASE2 && !(time4 & 0xFF)\n                    && p->attack_toggle) {\n                    item->goal_anim_state = M_STATE_ZAPP;\n                    p->attack_toggle = false;\n                }\n            }\n            break;\n\n        case M_STATE_ZAPP:\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n            tony->maximum_turn = 182;\n            if (Item_GetRelativeFrame(item) == 28) {\n                TonyBoss_TriggerFireBall(\n                    item, 2, nullptr, item->room_num, item->rot.y, 0);\n            }\n            break;\n\n        case M_STATE_ROCK_ZAPP:\n            torso_y = info.angle;\n            torso_x = info.x_angle;\n            tony->maximum_turn = 0;\n            if (Item_GetRelativeFrame(item) == 40) {\n                TonyBoss_TriggerFireBall(\n                    item, 0, nullptr, item->room_num, 0, 0);\n                TonyBoss_TriggerFireBall(\n                    item, 1, nullptr, item->room_num, 0, 0);\n            }\n            break;\n\n        case M_STATE_BIG_ROOM:\n            tony->maximum_turn = 0;\n            if (Item_GetRelativeFrame(item) == 56) {\n                p->phase = M_PHASE_PHASE2;\n                p->explode_count = 1;\n            }\n            break;\n        }\n    }\n\n    if (item->current_anim_state == M_STATE_ROCK_ZAPP\n        || item->current_anim_state == M_STATE_ZAPP\n        || item->current_anim_state == M_STATE_BIG_ROOM) {\n        int32_t f = Item_GetRelativeFrame(item);\n        if (f > 16) {\n            f = Item_GetAnim(item)->frame_end - item->frame_num;\n            CLAMPG(f, 16);\n        }\n\n        const int32_t r = Random_GetControl();\n        const RGB_888 color = {\n            .r = (f * (255 - ((r >> 4) & 0x1F))) >> 4,\n            .g = (f * (192 - ((r >> 6) & 0x1F))) >> 4,\n            .b = (f * (r & 0x3F)) >> 4,\n        };\n\n        XYZ_32 pos = {};\n        Collide_GetJointAbsPosition(item, &pos, 10);\n        Output_AddDynamicLightRGB(pos, 12, color);\n        M_TriggerFlame(item_num, 5);\n\n        if (item->current_anim_state == M_STATE_ROCK_ZAPP\n            || item->current_anim_state == M_STATE_BIG_ROOM) {\n            pos.x = 0;\n            pos.y = 0;\n            pos.z = 0;\n            Collide_GetJointAbsPosition(item, &pos, 13);\n            Output_AddDynamicLightRGB(pos, 12, color);\n            M_TriggerFlame(item_num, 4);\n        }\n    }\n\n    if (p->explode_count != 0 && item->hit_points > 0) {\n        M_Explode(item);\n        p->explode_count++;\n\n        if (p->explode_count == 32) {\n            Room_FlipMap();\n        }\n\n        if (p->explode_count > 64) {\n            p->ring_count = 0;\n            p->explode_count = 0;\n        }\n    }\n\n    Creature_Joint(item, 0, torso_y >> 1);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, torso_y >> 1);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_DrawShield(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    for (int32_t band = 0; band < 4; band++) {\n        const int32_t sprite_idx =\n            sprite_base + 18 + ((band + (time4 >> 3)) & 7);\n\n        for (int32_t j = 0; j < 8; j++) {\n            const int32_t j2 = (j == 7) ? 0 : (j + 1);\n            const M_SHIELD_POINT *const s00 = &p->shield[band][j];\n            const M_SHIELD_POINT *const s01 = &p->shield[band][j2];\n            const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j];\n            const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2];\n\n            const int32_t idx00 = band * 8 + j;\n            const int32_t idx01 = band * 8 + j2;\n            const int32_t idx10 = (band + 1) * 8 + j;\n            const int32_t idx11 = (band + 1) * 8 + j2;\n\n            RGB_888 c00 = s00->color;\n            RGB_888 c01 = s01->color;\n            RGB_888 c10 = s10->color;\n            RGB_888 c11 = s11->color;\n\n            if (idx00 >= 8 && idx00 <= 31) {\n                c00.r = (uint8_t)MAX(0, (int32_t)c00.r - (int32_t)s00->sub.r);\n                c00.g = (uint8_t)MAX(0, (int32_t)c00.g - (int32_t)s00->sub.g);\n                c00.b = (uint8_t)MAX(0, (int32_t)c00.b - (int32_t)s00->sub.b);\n            }\n            if (idx01 >= 8 && idx01 <= 31) {\n                c01.r = (uint8_t)MAX(0, (int32_t)c01.r - (int32_t)s01->sub.r);\n                c01.g = (uint8_t)MAX(0, (int32_t)c01.g - (int32_t)s01->sub.g);\n                c01.b = (uint8_t)MAX(0, (int32_t)c01.b - (int32_t)s01->sub.b);\n            }\n            if (idx10 >= 8 && idx10 <= 31) {\n                c10.r = (uint8_t)MAX(0, (int32_t)c10.r - (int32_t)s10->sub.r);\n                c10.g = (uint8_t)MAX(0, (int32_t)c10.g - (int32_t)s10->sub.g);\n                c10.b = (uint8_t)MAX(0, (int32_t)c10.b - (int32_t)s10->sub.b);\n            }\n            if (idx11 >= 8 && idx11 <= 31) {\n                c11.r = (uint8_t)MAX(0, (int32_t)c11.r - (int32_t)s11->sub.r);\n                c11.g = (uint8_t)MAX(0, (int32_t)c11.g - (int32_t)s11->sub.g);\n                c11.b = (uint8_t)MAX(0, (int32_t)c11.b - (int32_t)s11->sub.b);\n            }\n\n            if (((c00.r | c00.g | c00.b | c01.r | c01.g | c01.b | c11.r | c11.g\n                  | c11.b | c10.r | c10.g | c10.b)\n                 == 0U)) {\n                continue;\n            }\n\n            const XYZ_32 world_pos[4] = {\n                {\n                    item->pos.x + s00->pos.x,\n                    item->pos.y + s00->pos.y,\n                    item->pos.z + s00->pos.z,\n                },\n                {\n                    item->pos.x + s01->pos.x,\n                    item->pos.y + s01->pos.y,\n                    item->pos.z + s01->pos.z,\n                },\n                {\n                    item->pos.x + s11->pos.x,\n                    item->pos.y + s11->pos.y,\n                    item->pos.z + s11->pos.z,\n                },\n                {\n                    item->pos.x + s10->pos.x,\n                    item->pos.y + s10->pos.y,\n                    item->pos.z + s10->pos.z,\n                },\n            };\n            const RGBA_8888 color[4] = {\n                { c00.r, c00.g, c00.b, 255 },\n                { c01.r, c01.g, c01.b, 255 },\n                { c11.r, c11.g, c11.b, 255 },\n                { c10.r, c10.g, c10.b, 255 },\n            };\n            OutputSource_PolyFX_StageSpriteQuadWorld(\n                sprite_idx, world_pos, color, DRAW_BLEND_ADD);\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    if ((item->hit_points <= 0 && p->explode_count > 64)) {\n        return false;\n    }\n\n    const bool result = Object_DrawAnimatingItem(item);\n    if (p->explode_count != 0) {\n        if (p->explode_count != 0 && p->explode_count <= 64) {\n            M_DrawShield(item);\n        }\n    }\n    return result;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->draw_func = M_Draw;\n    obj->can_drop_items_func = M_CanDropItems;\n    obj->can_be_exploded_func = M_CanBeExploded;\n\n    obj->shadow_size = 0;\n    obj->hit_points = 100;\n    obj->pivot_length = 50;\n    obj->radius = 102;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_TONY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tony_fire_ball.c",
    "content": "#include \"tony_internal.h\"\n\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\nstatic void M_TriggerFireBallFlame(\n    const int16_t effect_num, const int32_t type, const int32_t xv,\n    const int32_t yv, const int32_t zv)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n\n    const EFFECT *const effect = Effect_Get(effect_num);\n    const int32_t dx = lara_item->pos.x - effect->pos.x;\n    const int32_t dz = lara_item->pos.z - effect->pos.z;\n    if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.b = 48;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) + xv - 128;\n    spark->vel.y = (int16_t)yv;\n    spark->vel.z = (Random_GetControl() & 0xFF) + zv - 128;\n    spark->friction = 5;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->effect_num = effect_num;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    spark->size.width = (Random_GetControl() & 0x1F) + 64;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 2;\n\n    if (!type || type == 1) {\n        spark->gravity = (Random_GetControl() & 0x1F) + 16;\n        spark->max_y_vel = (Random_GetControl() & 0xF) + 48;\n        spark->scalar = 2;\n        spark->vel.y *= -16;\n    } else if (type == 4 || type == 5 || type == 6) {\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n    } else if (type == 3) {\n        spark->gravity = -16 - (Random_GetControl() & 0x1F);\n        spark->max_y_vel = -64 - (Random_GetControl() & 0x1F);\n        spark->scalar = 2;\n        spark->vel.y <<= 4;\n    } else if (type == 2) {\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n        spark->scalar = 2;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nvoid TonyBoss_TriggerFireBall(\n    ITEM *const item, const int32_t type, const XYZ_32 *const pos,\n    const int16_t room_num, int16_t angle, int32_t speed)\n{\n    XYZ_32 effect_pos = {};\n    int32_t fall_speed;\n\n    switch (type) {\n    case 0:\n        Collide_GetJointAbsPosition(item, &effect_pos, 10);\n        angle = item->rot.y;\n        fall_speed = -16;\n        speed = 0;\n        break;\n    case 1:\n        Collide_GetJointAbsPosition(item, &effect_pos, 13);\n        angle = item->rot.y;\n        fall_speed = -16;\n        speed = 0;\n        break;\n    case 2:\n        Collide_GetJointAbsPosition(item, &effect_pos, 13);\n        speed = 160;\n        fall_speed = -32 - (Random_GetControl() & 7);\n        break;\n    case 3:\n        effect_pos = *pos;\n        speed = 0;\n        fall_speed = (Random_GetControl() & 3) + 4;\n        break;\n    case 4:\n        effect_pos = *pos;\n        speed += (Random_GetControl() & 3);\n        angle = Random_GetControl() << 1;\n        fall_speed = (Random_GetControl() & 3) - 2;\n        break;\n    case 5:\n        effect_pos = *pos;\n        speed = (Random_GetControl() & 7) + 48;\n        angle += (Random_GetControl() & 0x1FFF) + 0x7000;\n        fall_speed = -16 - (Random_GetControl() & 0xF);\n        break;\n    default:\n        effect_pos = *pos;\n        speed = (Random_GetControl() & 0x1F) + 32;\n        angle = Random_GetControl() << 1;\n        fall_speed = -32 - (Random_GetControl() & 0x1F);\n        break;\n    }\n\n    const int16_t fx_num = Effect_Create(room_num);\n    if (fx_num == NO_EFFECT) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(fx_num);\n    effect->pos = effect_pos;\n    effect->rot.y = angle;\n    effect->object_id = O_TONY_FIRE_BALL;\n    effect->speed = speed;\n    effect->fall_speed = fall_speed;\n    effect->flag1 = type;\n    effect->flag2 = (Random_GetControl() & 3) + 1;\n\n    if (type == 5) {\n        effect->flag2 <<= 1;\n    } else if (type == 2) {\n        effect->flag2 = 0;\n    }\n}\n\nstatic void M_Control(const int16_t effect_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    uint8_t falloffs[7] = { 16, 0, 14, 9, 7, 7, 7 };\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    XYZ_32 old_pos = effect->pos;\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n\n    if (!effect->flag1 || effect->flag1 == 1) {\n        effect->fall_speed += (effect->fall_speed >> 3) + 1;\n        CLAMPL(effect->fall_speed, -4096);\n        effect->pos.y += effect->fall_speed;\n        if (time4 & 4) {\n            M_TriggerFireBallFlame(effect_num, effect->flag1, 0, 0, 0);\n        }\n    } else if (effect->flag1 == 3) {\n        effect->fall_speed += 2;\n        effect->pos.y += effect->fall_speed;\n\n        if (time4 & 4) {\n            M_TriggerFireBallFlame(effect_num, 3, 0, 0, 0);\n        }\n    } else {\n        if (effect->flag1 != 2) {\n            if (effect->speed > 48) {\n                effect->speed--;\n            }\n        }\n\n        effect->fall_speed += effect->flag2;\n\n        if (effect->fall_speed > 512) {\n            effect->fall_speed = 512;\n        }\n\n        effect->pos.x += effect->speed * Math_Sin(effect->rot.y) >> W2V_SHIFT;\n        effect->pos.y += effect->fall_speed >> 1;\n        effect->pos.z += effect->speed * Math_Cos(effect->rot.y) >> W2V_SHIFT;\n        const int32_t dx = (old_pos.x - effect->pos.x) << 3;\n        const int32_t dy = (old_pos.y - effect->pos.y) << 3;\n        const int32_t dz = (old_pos.z - effect->pos.z) << 3;\n\n        if (time4 & 4) {\n            M_TriggerFireBallFlame(effect_num, effect->flag1, dx, dy, dz);\n        }\n    }\n\n    int16_t room_num = effect->room_num;\n    SECTOR *sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t h = Room_GetHeight(sector, effect->pos);\n    const int32_t c = Room_GetCeiling(sector, effect->pos);\n\n    if (effect->pos.y >= h || effect->pos.y < c) {\n        if (!effect->flag1 || effect->flag1 == 1 || effect->flag1 == 2\n            || effect->flag1 == 3) {\n            Sparks_TriggerExplosionSparks(old_pos, 3, -2, 0, effect->room_num);\n\n            if (!effect->flag1 || effect->flag1 == 1) {\n                for (int32_t i = 0; i < 2; i++) {\n                    Sparks_TriggerExplosionSparks(\n                        old_pos, 3, -1, 0, effect->room_num);\n                }\n            }\n\n            XYZ_32 pos = old_pos;\n\n            int32_t count;\n            if (effect->flag1 == 2) {\n                count = 7;\n            } else {\n                count = 3;\n            }\n\n            int32_t type;\n            if (effect->flag1 == 2) {\n                type = 5;\n            } else if (effect->flag1 == 3) {\n                type = 6;\n            } else {\n                type = 4;\n            }\n\n            for (int32_t i = 0; i < count; i++) {\n                TonyBoss_TriggerFireBall(\n                    nullptr, type, &pos, effect->room_num, effect->rot.y,\n                    (i << 2) + 32);\n            }\n\n            if (!effect->flag1 || effect->flag1 == 1) {\n                room_num = lara_item->room_num;\n                sector = Room_GetSector(lara_item->pos, &room_num);\n                pos.x = lara_item->pos.x + (Random_GetControl() & 0x3FF) - 512;\n                pos.z = lara_item->pos.z + (Random_GetControl() & 0x3FF) - 512;\n                pos.y = Room_GetCeiling(sector, lara_item->pos) + 256;\n                Sparks_TriggerExplosionSparks(pos, 3, -2, 0, room_num);\n                TonyBoss_TriggerFireBall(nullptr, 3, &pos, room_num, 0, 0);\n            }\n        }\n\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (Room_Get(room_num)->flags.underwater) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!lara->burn && Lara_IsNearItem(&effect->pos, 200)) {\n        Effect_Kill(effect_num);\n        Lara_TakeDamage(200, true);\n        Lara_CatchFire();\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, lara_item->room_num);\n    }\n\n    if (falloffs[effect->flag1]) {\n        const uint8_t r = Random_GetControl();\n        const RGB_888 color = {\n            .r = 255 - ((r >> 4) & 0x1F),\n            .g = 192 - ((r >> 6) & 0x1F),\n            .b = r & 0x3F,\n        };\n        Output_AddDynamicLightRGB(effect->pos, falloffs[effect->flag1], color);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n}\n\nREGISTER_OBJECT(O_TONY_FIRE_BALL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tony_internal.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n#include <trx/game/items/types.h>\n\nvoid TonyBoss_TriggerFireBall(\n    ITEM *item, int32_t type, const XYZ_32 *pos, int16_t room_num,\n    int16_t angle, int32_t speed);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/torso.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n// clang-format off\n#define M_PART_DAMAGE        250\n#define M_ATTACK_DAMAGE      500\n#define M_TOUCH_DAMAGE       5\n#define M_NEED_TURN          (DEG_1 * 45) // = 8190\n#define M_TURN               (DEG_1 * 3) // = 546\n#define M_ATTACK_RANGE       SQUARE(2600) // = 6760000\n#define M_CLOSE_RANGE        SQUARE(2250) // = 5062500\n#define M_TOUCH_LEFT         0x7FF0\n#define M_TOUCH_RIGHT        0x3FF8000\n#define M_TOUCH              (M_TOUCH_LEFT | M_TOUCH_RIGHT)\n#define M_HITPOINTS          500\n#define M_RADIUS             (WALL_L / 3) // = 341\n#define M_SMARTNESS          0x7FFF\n#define M_FRAME_TURN_L_START 14\n#define M_FRAME_TURN_L_END   22\n#define M_FRAME_TURN_R_START 17\n#define M_FRAME_TURN_R_END   22\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    TORSO_ANIM_TURN_L = 8,\n    TORSO_ANIM_DIE    = 13,\n    TORSO_ANIM_TURN_R = 17,\n    TORSO_ANIM_KILL   = 19,\n    // clang-format on\n} M_ANIM;\n\ntypedef enum {\n    // clang-format off\n    TORSO_STATE_EMPTY    = 0,\n    TORSO_STATE_STOP     = 1,\n    TORSO_STATE_TURN_L   = 2,\n    TORSO_STATE_TURN_R   = 3,\n    TORSO_STATE_ATTACK_1 = 4,\n    TORSO_STATE_ATTACK_2 = 5,\n    TORSO_STATE_ATTACK_3 = 6,\n    TORSO_STATE_FORWARD  = 7,\n    TORSO_STATE_SET      = 8,\n    TORSO_STATE_FALL     = 9,\n    TORSO_STATE_DEATH    = 10,\n    TORSO_STATE_KILL     = 11,\n    // clang-format on\n} M_STATE;\n\nstatic void M_KillLara(ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    Lara_TakeDamage(lara_item->hit_points, true);\n    Creature_SpecialKill(\n        item, TORSO_ANIM_KILL, TORSO_STATE_KILL, LS_EXTRA_TORSO_KILL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const torso = item->creature_data;\n    int16_t head = 0;\n    ITEM *const lara_item = Lara_GetItem();\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != TORSO_STATE_DEATH) {\n            item->current_anim_state = TORSO_STATE_DEATH;\n            Item_SwitchToAnim(item, TORSO_ANIM_DIE, 0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, true);\n\n        int16_t angle =\n            Math_Atan(\n                torso->target.z - item->pos.z, torso->target.x - item->pos.x)\n            - item->rot.y;\n\n        if (item->touch_bits) {\n            Lara_TakeDamage(M_TOUCH_DAMAGE, true);\n        }\n\n        switch (item->current_anim_state) {\n        case TORSO_STATE_SET:\n            item->goal_anim_state = TORSO_STATE_FALL;\n            item->gravity = true;\n            break;\n\n        case TORSO_STATE_STOP:\n            if (lara_item->hit_points <= 0) {\n                break;\n            }\n\n            torso->flags = 0;\n            if (angle > M_NEED_TURN) {\n                item->goal_anim_state = TORSO_STATE_TURN_R;\n            } else if (angle < -M_NEED_TURN) {\n                item->goal_anim_state = TORSO_STATE_TURN_L;\n            } else if (info.distance >= M_ATTACK_RANGE) {\n                item->goal_anim_state = TORSO_STATE_FORWARD;\n            } else if (lara_item->hit_points > M_ATTACK_DAMAGE) {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = TORSO_STATE_ATTACK_1;\n                } else {\n                    item->goal_anim_state = TORSO_STATE_ATTACK_2;\n                }\n            } else if (info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = TORSO_STATE_ATTACK_3;\n            } else {\n                item->goal_anim_state = TORSO_STATE_FORWARD;\n            }\n            break;\n\n        case TORSO_STATE_FORWARD:\n            if (angle < -M_TURN) {\n                item->goal_anim_state -= M_TURN;\n            } else if (angle > M_TURN) {\n                item->goal_anim_state += M_TURN;\n            } else {\n                item->goal_anim_state += angle;\n            }\n\n            if (angle > M_NEED_TURN || angle < -M_NEED_TURN) {\n                item->goal_anim_state = TORSO_STATE_STOP;\n            } else if (info.distance < M_ATTACK_RANGE) {\n                item->goal_anim_state = TORSO_STATE_STOP;\n            }\n            break;\n\n        case TORSO_STATE_TURN_L:\n            if (!torso->flags) {\n                torso->flags = item->frame_num;\n            } else if (\n                Item_TestAnimEqual(item, TORSO_ANIM_TURN_L)\n                && Item_TestFrameRange(\n                    item, M_FRAME_TURN_L_START, M_FRAME_TURN_L_END)) {\n                item->rot.y -= DEG_1 * 9;\n            }\n\n            if (angle > -M_NEED_TURN) {\n                item->goal_anim_state = TORSO_STATE_STOP;\n            }\n            break;\n\n        case TORSO_STATE_TURN_R:\n            if (!torso->flags) {\n                torso->flags = item->frame_num;\n            } else if (\n                Item_TestAnimEqual(item, TORSO_ANIM_TURN_R)\n                && Item_TestFrameRange(\n                    item, M_FRAME_TURN_R_START, M_FRAME_TURN_R_END)) {\n                item->rot.y += DEG_1 * 14;\n            }\n\n            if (angle < M_NEED_TURN) {\n                item->goal_anim_state = TORSO_STATE_STOP;\n            }\n            break;\n\n        case TORSO_STATE_ATTACK_1:\n            if (!torso->flags && (item->touch_bits & M_TOUCH_RIGHT)) {\n                Lara_TakeDamage(M_ATTACK_DAMAGE, true);\n                torso->flags = 1;\n            }\n            break;\n\n        case TORSO_STATE_ATTACK_2:\n            if (!torso->flags && (item->touch_bits & M_TOUCH)) {\n                Lara_TakeDamage(M_ATTACK_DAMAGE, true);\n                torso->flags = 1;\n            }\n            break;\n\n        case TORSO_STATE_ATTACK_3:\n            if ((item->touch_bits & M_TOUCH_RIGHT)\n                || lara_item->hit_points <= 0) {\n                M_KillLara(item);\n            }\n            break;\n\n        case TORSO_STATE_KILL:\n            g_Camera.target_distance = WALL_L * 2;\n            g_Camera.flags = CF_FOLLOW_CENTRE;\n            break;\n        }\n    }\n\n    Creature_Head(item, head);\n\n    if (item->current_anim_state == TORSO_STATE_FALL) {\n        Item_Animate(item);\n\n        if (item->pos.y > item->floor) {\n            item->goal_anim_state = TORSO_STATE_STOP;\n            item->gravity = false;\n            item->pos.y = item->floor;\n            g_Camera.bounce = 500;\n        }\n    } else {\n        Creature_Animate(item_num, 0, 0);\n    }\n\n    if (item->status == IS_DEACTIVATED) {\n        Sound_Effect(SFX_ATLANTEAN_DEATH, &item->pos, SPM_NORMAL);\n        Item_Explode(item_num, -1, M_PART_DAMAGE);\n        Room_TestTriggers(item);\n\n        Item_Kill(item_num);\n        item->status = IS_DEACTIVATED;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = Creature_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 3;\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->smartness = M_SMARTNESS;\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 1)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_TORSO, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/trex.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n\n// clang-format off\n#define M_SHADOW_SIZE    (UNIT_SHADOW / (g_TRVersion == 1 ? 4 : 2))\n#define M_PIVOT_LENGTH   (g_TRVersion == 1 ? 2000 : 1800)\n#define M_HITPOINTS      100\n#define M_TOUCH_BITS     0b00110000'00000000\n#define M_RADIUS         (WALL_L / 3) // = 341\n#define M_RUN_TURN       (DEG_1 * 4) // = 728\n#define M_WALK_TURN      (DEG_1 * 2) // = 364\n#define M_FRONT_ARC      FRONT_ARC\n#define M_RUN_RANGE      SQUARE(WALL_L * 5) // = 26214400\n#define M_ATTACK_RANGE   SQUARE(WALL_L * 4) // = 16777216\n#define M_BITE_RANGE     SQUARE(1500) // = 2250000\n#define M_TOUCH_DAMAGE   1\n#define M_TRAMPLE_DAMAGE 10\n#define M_BITE_DAMAGE    10000\n#define M_ROAR_CHANCE    512\n#define M_SMARTNESS      0x7FFF\n// clang-format on\n\ntypedef enum {\n    M_ANIM_KILL = 11,\n} M_ANIM;\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_ATTACK_1,\n    M_STATE_DEATH,\n    M_STATE_ROAR,\n    M_STATE_ATTACK_2,\n    M_STATE_KILL,\n} M_STATE;\n\nstatic void M_KillLara(ITEM *const item)\n{\n    Lara_TakeDamage(M_BITE_DAMAGE, true);\n    Creature_SpecialKill(item, M_ANIM_KILL, M_STATE_KILL, LS_EXTRA_TREX_KILL);\n    Lara_Skin_SwapAllExtra(LS_EXTRA_TREX_KILL);\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (g_Config.gameplay.disable_trex_collision\n        && Item_Get(item_num)->hit_points <= 0) {\n        return;\n    }\n\n    Creature_Collision(item_num, lara_item, coll);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = item->current_anim_state == M_STATE_STOP\n            ? M_STATE_DEATH\n            : M_STATE_STOP;\n        goto finish;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    if (info.ahead) {\n        head = info.angle;\n    }\n    Creature_Mood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (item->touch_bits != 0) {\n        if (item->current_anim_state == M_STATE_RUN) {\n            Lara_TakeDamage(M_TRAMPLE_DAMAGE, false);\n        } else {\n            Lara_TakeDamage(M_TOUCH_DAMAGE, false);\n        }\n    }\n\n    creature->flags = creature->mood != MOOD_ESCAPE && !info.ahead\n        && info.enemy_facing > -M_FRONT_ARC && info.enemy_facing < M_FRONT_ARC;\n\n    if (creature->flags == 0 && info.distance > M_BITE_RANGE\n        && info.distance < M_ATTACK_RANGE && info.bite) {\n        creature->flags = 1;\n    }\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        if (item->required_anim_state != M_STATE_EMPTY) {\n            item->goal_anim_state = item->required_anim_state;\n        } else if (info.distance < M_BITE_RANGE && info.bite) {\n            item->goal_anim_state = M_STATE_ATTACK_2;\n        } else if (creature->mood == MOOD_BORED || creature->flags != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        creature->maximum_turn = M_WALK_TURN;\n        if (creature->mood != MOOD_BORED || creature->flags == 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.ahead && Random_GetControl() < M_ROAR_CHANCE) {\n            item->required_anim_state = M_STATE_ROAR;\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_RUN:\n        creature->maximum_turn = M_RUN_TURN;\n        if (info.distance < M_RUN_RANGE && info.bite) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->flags != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood != MOOD_ESCAPE && info.ahead\n            && Random_GetControl() < M_ROAR_CHANCE) {\n            item->required_anim_state = M_STATE_ROAR;\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->mood == MOOD_BORED) {\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_ATTACK_2:\n        if ((item->touch_bits & M_TOUCH_BITS) != 0) {\n            M_KillLara(item);\n        }\n        item->required_anim_state = M_STATE_WALK;\n        break;\n    }\n\nfinish:\n    Creature_Head(item, head / 2);\n    if (creature != nullptr) {\n        creature->neck_rotation = creature->head_rotation;\n    }\n\n    Creature_Animate(item_num, angle, 0);\n    if (g_TRVersion == 1) {\n        item->collidable = true;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    if (g_TRVersion == 1) {\n        obj->initialise_func = Creature_Initialise;\n    }\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->shadow_size = M_SHADOW_SIZE;\n    obj->pivot_length = M_PIVOT_LENGTH;\n    obj->smartness = M_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 10)->rot.y = true;\n    Object_GetBone(obj, 11)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_TREX, M_Setup)\nREGISTER_OBJECT(O_DINO_WARRIOR, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/trex_alpha.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/general/flare_item.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_PIVOT_LENGTH      1800\n#define M_HITPOINTS         800\n#define M_TOUCH_BITS        0b00110000'00000000\n#define M_RADIUS            (WALL_L / 3) // = 341\n#define M_RUN_TURN          (DEG_1 * 4) // = 728\n#define M_WALK_TURN         (DEG_1 * 2) // = 364\n#define M_FRONT_ARC         FRONT_ARC\n#define M_RUN_RANGE         SQUARE(WALL_L * 5) // = 26214400\n#define M_ATTACK_RANGE      SQUARE(WALL_L * 4) // = 16777216\n#define M_HIT_RADIUS        SQUARE(M_RADIUS * 2) // = 465124\n#define M_BITE_RANGE        SQUARE(1500) // = 2250000\n#define M_TOUCH_DAMAGE      1\n#define M_TRAMPLE_DAMAGE    10\n#define M_BITE_DAMAGE       10000\n#define M_RAPTOR_DAMAGE     (M_TRAMPLE_DAMAGE * 5) // = 50\n#define M_ROAR_CHANCE       256\n#define M_SMARTNESS         0x7FFF\n#define M_ATTACK_FRAME      20\n#define M_AGGRESSION_TIME   (LOGIC_FPS * 4) // = 120\n#define M_DISTRACTION_COUNT 3\n#define M_FLARE_SEEN        (-1)\n// clang-format on\n\ntypedef enum {\n    M_ANIM_KILL = 11,\n} M_ANIM;\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_ATTACK_1,\n    M_STATE_DEATH,\n    M_STATE_ROAR,\n    M_STATE_ATTACK_2,\n    M_STATE_KILL,\n    M_STATE_LONG_ROAR_START,\n    M_STATE_LONG_ROAR_MID,\n    M_STATE_LONG_ROAR_END,\n    M_STATE_SNIFF_START,\n    M_STATE_SNIFF_MID,\n    M_STATE_SNIFF_END,\n} M_STATE;\n\ntypedef struct {\n    int32_t aggression_timer;\n    int32_t distraction_count;\n} M_PRIV;\n\nstatic BITE m_Bite = {\n    .pos = { .x = 0, .y = 32, .z = 64 },\n    .mesh_num = 13,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"aggression_timer\", &p->aggression_timer));\n    JSON_SHOULD(JSON_READ(io, \"distraction_count\", &p->distraction_count));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"aggression_timer\", p->aggression_timer);\n    JSONW_WRITE(io, \"distraction_count\", p->distraction_count);\n}\n\nstatic void M_KillLara(ITEM *const item)\n{\n    Lara_TakeDamage(M_BITE_DAMAGE, true);\n    Creature_SpecialKill(item, M_ANIM_KILL, M_STATE_KILL, LS_EXTRA_TREX_KILL);\n    Lara_Skin_SwapAllExtra(LS_EXTRA_TREX_KILL);\n}\n\nstatic bool M_IsCandidateTarget(const ITEM *const item)\n{\n    if (item->object_id == O_RAPTOR) {\n        return item->status == IS_ACTIVE && item->hit_points > 0;\n    }\n    if (item->object_id == O_FLARE_ITEM) {\n        return FlareItem_IsActive(item) && item->hit_points != M_FLARE_SEEN;\n    }\n    return false;\n}\n\nstatic void M_CalculateTarget(ITEM *const item)\n{\n    CREATURE *const creature = item->creature_data;\n    if (creature->hurt_by_lara) {\n        creature->enemy = Lara_GetItem();\n        return;\n    }\n\n    creature->enemy = nullptr;\n    int32_t best_distance = INT32_MAX;\n    Room_GetNearbyRooms(item->pos, WALL_L * 4, 0, item->room_num);\n\n    for (int32_t i = 0; i < Room_DrawGetCount(); i++) {\n        const ROOM *const nearby_room = Room_Get(Room_DrawGetRoom(i));\n        int16_t target_item_num = nearby_room->item_num;\n        while (target_item_num != NO_ITEM) {\n            const ITEM *const candidate = Item_Get(target_item_num);\n            if (!M_IsCandidateTarget(candidate)) {\n                goto loopend;\n            }\n\n            const XYZ_32 delta = {\n                .x = (candidate->pos.x - item->pos.x) >> 6,\n                .y = (candidate->pos.y - item->pos.y) >> 6,\n                .z = (candidate->pos.z - item->pos.z) >> 6,\n            };\n            const int32_t distance = XYZ_32_GetLength2(delta);\n            if (distance < best_distance) {\n                creature->enemy = (ITEM *)candidate;\n                best_distance = distance;\n            }\n        loopend:\n            target_item_num = candidate->next_item;\n        }\n    }\n\n    if (creature->enemy != nullptr\n        && creature->enemy->object_id == O_FLARE_ITEM) {\n        creature->enemy->hit_points = 1;\n    }\n}\n\nstatic bool M_CanAttack(const ITEM *const item, const ITEM *const target)\n{\n    if (target == Lara_GetItem()) {\n        return (item->touch_bits & M_TOUCH_BITS) != 0;\n    }\n\n    if (target == nullptr || Item_GetRelativeFrame(item) != M_ATTACK_FRAME) {\n        return false;\n    }\n\n    const XYZ_32 pos = {\n        .x =\n            ABS(target->pos.x\n                - (item->pos.x\n                   + ((M_PIVOT_LENGTH * Math_Sin(item->rot.y)) >> W2V_SHIFT))),\n        .y = ABS(target->pos.y - item->pos.y),\n        .z =\n            ABS(target->pos.z\n                - (item->pos.z\n                   + ((M_PIVOT_LENGTH * Math_Cos(item->rot.y)) >> W2V_SHIFT))),\n    };\n    return pos.x < M_HIT_RADIUS && pos.y <= M_HIT_RADIUS\n        && pos.z < M_HIT_RADIUS;\n}\n\nstatic void M_Attack(ITEM *const item, ITEM *const target)\n{\n    if (target == Lara_GetItem()) {\n        Creature_Effect(item, &m_Bite, Spawn_Blood);\n        M_KillLara(item);\n    } else if (target->object_id == O_RAPTOR) {\n        Creature_Effect(item, &m_Bite, Spawn_Blood);\n        target->hit_points -= M_RAPTOR_DAMAGE;\n        target->hit_status = true;\n    } else if (target->object_id == O_FLARE_ITEM) {\n        target->hit_points = M_FLARE_SEEN;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        item->goal_anim_state = item->current_anim_state == M_STATE_STOP\n            ? M_STATE_DEATH\n            : M_STATE_STOP;\n        goto finish;\n    }\n\n    M_CalculateTarget(item);\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    Creature_UpdateMood(item, &info, true);\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (p->aggression_timer == 0 && p->distraction_count == 0\n        && creature->enemy == lara_item) {\n        creature->mood = MOOD_BORED;\n    }\n    Creature_ApplyMood(item, &info, true);\n\n    if (creature->mood == MOOD_BORED) {\n        creature->maximum_turn >>= 1;\n    }\n\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (item->touch_bits != 0) {\n        if (item->current_anim_state == M_STATE_RUN) {\n            Lara_TakeDamage(M_TRAMPLE_DAMAGE, false);\n        } else {\n            Lara_TakeDamage(M_TOUCH_DAMAGE, false);\n        }\n    }\n\n    creature->flags = creature->mood != MOOD_ESCAPE && !info.ahead\n        && info.enemy_facing > -M_FRONT_ARC && info.enemy_facing < M_FRONT_ARC;\n\n    if (creature->flags == 0 && info.distance > M_BITE_RANGE\n        && info.distance < M_ATTACK_RANGE && info.bite) {\n        creature->flags = 1;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_type != LGT_FLARE\n        && (lara_item->current_anim_state == LS(LS_STOP)\n            || lara_item->current_anim_state == LS(LS_CROUCH_IDLE))\n        && lara_item->current_anim_state == lara_item->goal_anim_state\n        && !item->hit_status) {\n        p->aggression_timer--;\n        CLAMPL(p->aggression_timer, 0);\n    } else {\n        p->aggression_timer = M_AGGRESSION_TIME;\n        p->distraction_count = M_DISTRACTION_COUNT;\n    }\n\n    switch (item->current_anim_state) {\n    case M_STATE_STOP:\n        if (item->required_anim_state != M_STATE_EMPTY) {\n            item->goal_anim_state = item->required_anim_state;\n        } else if (creature->mood == MOOD_BORED || creature->flags != 0) {\n            item->goal_anim_state = M_STATE_WALK;\n        } else if (creature->mood == MOOD_ESCAPE) {\n            if (lara->target != item && info.ahead && !item->hit_status) {\n                item->goal_anim_state = Random_GetControl() < M_ROAR_CHANCE\n                    ? M_STATE_ROAR\n                    : M_STATE_STOP;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n        } else if (info.distance < M_BITE_RANGE && info.bite) {\n            if (p->aggression_timer != 0) {\n                item->goal_anim_state = M_STATE_ATTACK_2;\n            } else if ((Random_GetControl() & 1) != 0) {\n                if (p->distraction_count != 0) {\n                    item->goal_anim_state = M_STATE_LONG_ROAR_START;\n                }\n            } else if (p->distraction_count != 0) {\n                item->goal_anim_state = M_STATE_SNIFF_START;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_RUN;\n        }\n        break;\n\n    case M_STATE_WALK:\n        creature->maximum_turn = M_WALK_TURN;\n        if (creature->mood != MOOD_BORED || creature->flags == 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (info.ahead && Random_GetControl() < M_ROAR_CHANCE) {\n            item->required_anim_state = M_STATE_ROAR;\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_RUN:\n        creature->maximum_turn = M_RUN_TURN;\n        if (info.distance < M_RUN_RANGE && info.bite) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (creature->flags != 0) {\n            item->goal_anim_state = M_STATE_STOP;\n        } else if (\n            creature->mood == MOOD_ESCAPE || !info.ahead\n            || Random_GetControl() >= M_ROAR_CHANCE) {\n            if (creature->mood == MOOD_BORED\n                || (creature->mood == MOOD_ESCAPE && lara->target != item\n                    && info.ahead)) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        } else {\n            item->required_anim_state = M_STATE_ROAR;\n            item->goal_anim_state = M_STATE_STOP;\n        }\n        break;\n\n    case M_STATE_ROAR:\n        creature->maximum_turn = 0;\n        break;\n\n    case M_STATE_ATTACK_2:\n        creature->maximum_turn = M_WALK_TURN;\n        if (M_CanAttack(item, creature->enemy)) {\n            if (creature->enemy == lara_item) {\n                creature->maximum_turn = 0;\n            }\n            M_Attack(item, creature->enemy);\n        }\n\n        if ((Random_GetControl() & 3) == 0) {\n            item->required_anim_state = M_STATE_WALK;\n        }\n        break;\n\n    case M_STATE_KILL:\n        creature->maximum_turn = 0;\n        Creature_Effect(item, &m_Bite, Spawn_Blood);\n        break;\n\n    case M_STATE_LONG_ROAR_START:\n    case M_STATE_SNIFF_START:\n        const bool roar_state =\n            item->current_anim_state == M_STATE_LONG_ROAR_START;\n        creature->maximum_turn = 0;\n        if (p->distraction_count > 0 && Item_TestFrameEqual(item, 0)) {\n            p->distraction_count--;\n            if (creature->enemy != nullptr\n                && creature->enemy->object_id == O_FLARE_ITEM) {\n                M_Attack(item, creature->enemy);\n                if (roar_state) {\n                    p->distraction_count--;\n                } else {\n                    p->distraction_count = 0;\n                }\n            }\n        }\n\n        if (roar_state) {\n            item->goal_anim_state = M_STATE_LONG_ROAR_END;\n        }\n        break;\n\n    case M_STATE_SNIFF_MID:\n        creature->maximum_turn = 0;\n        if (!Item_TestFrameEqual(item, 0)) {\n            break;\n        }\n        if ((Random_GetControl() & 1) != 0 && p->distraction_count != 0\n            && p->aggression_timer == 0) {\n            item->goal_anim_state = M_STATE_SNIFF_MID;\n            p->distraction_count--;\n            CLAMPL(p->distraction_count, 0);\n        } else {\n            item->goal_anim_state = M_STATE_SNIFF_END;\n        }\n        break;\n    }\n\nfinish:\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = M_PIVOT_LENGTH;\n    obj->smartness = M_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_BEAST);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    obj->hit_points = M_HITPOINTS;\n\n    Object_GetBone(obj, 9)->rot.y = true;\n    Object_GetBone(obj, 11)->rot.y = true;\n    Object_GetBone(obj, 20)->rot.y = true;\n    Object_GetBone(obj, 22)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_TREX_ALPHA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tribe_axeman.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_WALK_TURN    (9 * DEG_1)\n#define M_RUN_TURN     (6 * DEG_1)\n#define M_OTHER_TURN   (4 * DEG_1)\n#define M_CLOSE_RANGE  SQUARE(WALL_L * 2 / 3)\n#define M_LONG_RANGE   SQUARE(WALL_L)\n#define M_WALK_RANGE   SQUARE(WALL_L * 2)\n#define M_ESCAPE_RANGE SQUARE(WALL_L * 3)\n#define M_HIT_RANGE    (STEP_L * 2)\n#define M_TOUCH_BITS   (1 << 13) // = 0x2000\n// clang-format on\n\ntypedef struct {\n    bool wants_wait_2;\n} M_PRIV;\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_WAIT_1,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_ATTACK_1,\n    M_STATE_ATTACK_2,\n    M_STATE_ATTACK_3,\n    M_STATE_ATTACK_4,\n    M_STATE_AIM_3,\n    M_STATE_DEATH,\n    M_STATE_ATTACK_5,\n    M_STATE_WAIT_2,\n    M_STATE_ATTACK_6\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH_STAND = 20,\n    M_ANIM_DEATH_DOWN = 21,\n} M_ANIM;\n\ntypedef struct {\n    uint8_t start_frame;\n    uint8_t end_frame;\n    uint8_t damage;\n} M_HIT_FRAME;\n\nstatic BITE m_AxeHit = {\n    .pos = { .x = 0, .y = 16, .z = 265 },\n    .mesh_num = 13,\n};\n\nstatic M_HIT_FRAME m_HitFrames[13] = {\n    {},\n    {},\n    {},\n    {},\n    {},\n    { .start_frame = 2, .end_frame = 12, .damage = 8 },\n    { .start_frame = 8, .end_frame = 9, .damage = 32 },\n    { .start_frame = 19, .end_frame = 28, .damage = 8 },\n    {},\n    {},\n    { .start_frame = 7, .end_frame = 14, .damage = 8 },\n    {},\n    { .start_frame = 15, .end_frame = 19, .damage = 32 }\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"wants_wait_2\", &p->wants_wait_2));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"wants_wait_2\", p->wants_wait_2);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            if (item->current_anim_state == M_STATE_WAIT_1\n                || item->current_anim_state == M_STATE_ATTACK_4) {\n                Item_SwitchToAnim(item, M_ANIM_DEATH_DOWN, 0);\n            } else {\n                Item_SwitchToAnim(item, M_ANIM_DEATH_STAND, 0);\n            }\n            item->current_anim_state = M_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_UpdateMood(item, &info, true);\n\n        if (creature->enemy == lara_item && creature->hurt_by_lara\n            && info.distance > M_ESCAPE_RANGE && info.enemy_facing < 0x3000\n            && info.enemy_facing > -0x3000) {\n            creature->mood = MOOD_ESCAPE;\n        }\n\n        Creature_ApplyMood(item, &info, true);\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_WAIT_1:\n            creature->maximum_turn = M_OTHER_TURN;\n            creature->flags = 0;\n\n            if (creature->mood == MOOD_BORED) {\n                creature->maximum_turn = 0;\n                if (Random_GetControl() < 0x100) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                if (lara->target != item && info.ahead && !item->hit_status) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (p->wants_wait_2) {\n                p->wants_wait_2 = false;\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (info.ahead && info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_4;\n            } else if (info.ahead && info.distance < M_LONG_RANGE) {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else {\n                    item->goal_anim_state = M_STATE_ATTACK_4;\n                }\n            } else if (info.ahead && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_WALK:\n            creature->maximum_turn = M_WALK_TURN;\n            creature->flags = 0;\n            tilt = angle >> 3;\n\n            if (creature->mood == MOOD_BORED) {\n                creature->maximum_turn = 409;\n                if (Random_GetControl() < 0x100) {\n                    if (Random_GetControl() < 0x2000) {\n                        item->goal_anim_state = M_STATE_WAIT_1;\n                    } else {\n                        item->goal_anim_state = M_STATE_WAIT_2;\n                    }\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (info.ahead && info.distance < M_CLOSE_RANGE) {\n                if (Random_GetControl() < 0x2000) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                } else {\n                    item->goal_anim_state = M_STATE_WAIT_2;\n                }\n            } else if (info.distance > M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_RUN:\n            creature->maximum_turn = M_RUN_TURN;\n            creature->flags = 0;\n            tilt = angle >> 2;\n\n            if (creature->mood == MOOD_BORED) {\n                creature->maximum_turn = 1.5f * DEG_1;\n\n                if (Random_GetControl() < 0x100) {\n                    if (Random_GetControl() < 0x4000) {\n                        item->goal_anim_state = M_STATE_WAIT_1;\n                    } else {\n                        item->goal_anim_state = M_STATE_WAIT_2;\n                    }\n                }\n            } else if (\n                creature->mood == MOOD_ESCAPE && lara->target != item\n                && info.ahead) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (info.bite || info.distance < M_WALK_RANGE) {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = M_STATE_ATTACK_6;\n                } else if (Random_GetControl() < 0x2000) {\n                    item->goal_anim_state = M_STATE_ATTACK_5;\n                } else {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            }\n            break;\n\n        case M_STATE_ATTACK_2:\n        case M_STATE_ATTACK_3:\n        case M_STATE_ATTACK_4:\n        case M_STATE_ATTACK_5:\n        case M_STATE_ATTACK_6:\n            p->wants_wait_2 = true;\n            creature->maximum_turn = M_OTHER_TURN;\n            creature->flags = Item_GetRelativeFrame(item);\n            const M_HIT_FRAME *const hit_frame =\n                &m_HitFrames[item->current_anim_state];\n            ITEM *const enemy = creature->enemy;\n\n            if (enemy == lara_item) {\n                if (item->touch_bits & M_TOUCH_BITS\n                    && creature->flags >= hit_frame->start_frame\n                    && creature->flags <= hit_frame->end_frame) {\n                    Lara_TakeDamage(hit_frame->damage, true);\n\n                    for (int32_t i = 0; i < hit_frame->damage; i += 8) {\n                        Creature_Effect(item, &m_AxeHit, Spawn_Blood);\n                    }\n\n                    Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                }\n            } else if (enemy != nullptr) {\n                if (Item_IsNearby(enemy, item, M_HIT_RANGE)) {\n                    if (creature->flags >= hit_frame->start_frame\n                        && creature->flags <= hit_frame->end_frame) {\n                        enemy->hit_points -= 2;\n                        enemy->hit_status = 1;\n                        Creature_Effect(item, &m_AxeHit, Spawn_Blood);\n                        Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                    }\n                }\n            }\n            break;\n\n        case M_STATE_AIM_3:\n            creature->maximum_turn = M_OTHER_TURN;\n            if (info.bite || info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_3;\n            } else {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            }\n            break;\n\n        case M_STATE_WAIT_2:\n            creature->maximum_turn = M_OTHER_TURN;\n            creature->flags = 0;\n\n            if (creature->mood == MOOD_BORED) {\n                creature->maximum_turn = 0;\n                if (Random_GetControl() < 0x100) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                if (lara->target != item && info.ahead && !item->hit_status) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (info.ahead && info.distance < M_CLOSE_RANGE) {\n                if (Random_GetControl() < 0x800) {\n                    item->goal_anim_state = M_STATE_ATTACK_2;\n                } else {\n                    item->goal_anim_state = M_STATE_AIM_3;\n                }\n            } else if (info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, head >> 1);\n    Creature_Joint(item, 1, head >> 1);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = 28;\n    obj->radius = 102;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 13)->rot.y = true;\n    Object_GetBone(obj, 6)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_TRIBE_AXEMAN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tribe_boss.c",
    "content": "#include <trx/game/objects/creatures/tribe_boss.h>\n\n#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/items.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/electric.h>\n#include <trx/game/los.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n#define M_MAX_HEAD_ATTACKS 4\n\ntypedef enum {\n    M_STATE_WAIT = 0,\n    M_STATE_ATTACK_HEAD = 1,\n    M_STATE_ATTACK_HAND = 2,\n    M_STATE_DEATH = 3,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_DEATH = 3,\n} M_ANIM;\n\ntypedef enum {\n    M_ATTACK_HEAD,\n    M_ATTACK_HAND_1,\n    M_ATTACK_HAND_2,\n} M_ATTACK_TYPE;\n\ntypedef struct {\n    XYZ_16 pos;\n    RGB_888 sub;\n    RGB_888 color;\n} M_SHIELD_POINT;\n\ntypedef struct {\n    bool lizard_active;\n} M_SHARED_PRIV;\n\ntypedef struct {\n    XYZ_32 pos;\n    uint16_t y_rot;\n} M_LIZARD_SUMMON_COORDS;\n\ntypedef struct {\n    uint8_t dead;\n    int16_t attack_count;\n    int16_t death_count;\n    uint8_t attack_flag;\n    M_ATTACK_TYPE attack_type;\n    uint8_t attack_head_count;\n    uint8_t ring_count;\n    int16_t explode_count;\n    int16_t lizard_item_num;\n    int16_t lizard_room_num;\n    bool dropped_item;\n    XYZ_32 beam_target;\n    M_SHIELD_POINT shield[5][8];\n    XYZ_32 trig_dynamics[3];\n    bool shield_on;\n    bool shield_active;\n    bool turned;\n    M_LIZARD_SUMMON_COORDS lizard_summon_coords[2];\n    M_SHARED_PRIV *shared;\n} M_PRIV;\n\nstatic const BITE m_HelmetSpikes[5] = {\n    // Helmet spikes\n    { .pos = { .x = 120, .y = 68, .z = 136 }, .mesh_num = 8 },\n    { .pos = { .x = 128, .y = -64, .z = 136 }, .mesh_num = 8 },\n    { .pos = { .x = 8, .y = -120, .z = 136 }, .mesh_num = 8 },\n    { .pos = { .x = -128, .y = -64, .z = 136 }, .mesh_num = 8 },\n    { .pos = { .x = -124, .y = 64, .z = 126 }, .mesh_num = 8 },\n};\n\nstatic BITE m_EnergyHit = {\n    .pos = { .x = 8, .y = 32, .z = 400 },\n    .mesh_num = 8,\n};\n\nstatic const int32_t m_Heights[5] = { -1536, -1280, -832, -384, 0 };\nstatic const int32_t m_Dist[5] = { 200, 400, 500, 500, 475 };\nstatic const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 };\nstatic const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 };\nstatic const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 };\nstatic int32_t m_DeathDist[5] = {};\nstatic int32_t m_DeathHeights[5] = {};\n\nstatic M_SHARED_PRIV m_SharedPriv = {};\n\nstatic int16_t M_FindLizard(const int16_t room_num)\n{\n    for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {\n        const ITEM *const item = Item_Get(item_num);\n        if (item->object_id == O_LIZARD && item->room_num == room_num) {\n            return item_num;\n        }\n    }\n    return NO_ITEM;\n}\n\nstatic void M_RotateHeadXAngle(ITEM *const item)\n{\n    XYZ_32 lpos = {};\n    Collide_GetJointAbsPosition(Lara_GetItem(), &lpos, LM_HIPS);\n\n    XYZ_32 pos = {};\n    Collide_GetJointAbsPosition(item, &pos, 0);\n\n    const int32_t dx = ABS(pos.x - lpos.x);\n    const int32_t dy = pos.y - lpos.y;\n    const int32_t dz = ABS(pos.z - lpos.z);\n    const int16_t ang = Math_Atan(Math_Sqrt(SQUARE(dx) + SQUARE(dz)), dy);\n    if (ABS(ang) < 0x2000) {\n        Creature_Joint(item, 2, ang);\n    } else {\n        Creature_Joint(item, 2, 0);\n    }\n}\n\nstatic void M_Explode(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    p->shield_on = false;\n\n    if (p->explode_count == 1 || p->explode_count == 15\n        || p->explode_count == 25 || p->explode_count == 35\n        || p->explode_count == 45 || p->explode_count == 55) {\n        const XYZ_32 pos = {\n            .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512,\n            .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256,\n            .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512,\n        };\n        FX_RING *const ring =\n            FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count);\n        if (ring != nullptr) {\n            ring->pos = pos;\n            ring->on = 4;\n            FX_Ring_Sync(ring);\n        }\n        p->ring_count++;\n        Sparks_TriggerExplosionSparks(pos, 3, -2, 2, 0);\n\n        for (int32_t i = 0; i < 2; i++) {\n            Sparks_TriggerExplosionSparks(pos, 3, -1, 2, 0);\n        }\n\n        Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH);\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        if (p->explode_count < 128) {\n            m_DeathDist[i] =\n                (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7);\n            m_DeathHeights[i] = m_DHeights2[i]\n                + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7);\n        }\n    }\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t y = m_DeathHeights[i];\n        const int32_t rad = m_DeathDist[i];\n        int32_t angle = (time4 & 0x3F) << 3;\n        for (int32_t j = 0; j < 8; j++) {\n            M_SHIELD_POINT *const shield = &p->shield[i][j];\n            shield->pos = (XYZ_16) {\n                .x = ((rad * Math_Sin(angle << 4)) >> 13),\n                .y = y,\n                .z = ((rad * Math_Cos(angle << 4)) >> 13),\n            };\n\n            if (i != 0 && i != 4 && p->explode_count < 64) {\n                const int32_t m = 64 - p->explode_count;\n                int32_t r = (m * (Random_GetDraw() & 0x1F)) >> 6;\n                int32_t b = (Random_GetDraw() & 0x3F) + 224;\n                int32_t g = (m * ((b >> 2) + (Random_GetDraw() & 0x3F))) >> 6;\n                b = (m * b) >> 6;\n                shield->color = (RGB_888) { r, g, b };\n            } else {\n                shield->color = (RGB_888) {};\n            }\n\n            angle = (angle + 512) & 0xFFF;\n        }\n    }\n}\n\nstatic void M_Die(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->collidable = 0;\n    item->hit_points = 0;\n    Item_Kill(item_num);\n    LOT_DisableBaddieAI(item_num);\n    item->flags |= IF_INVISIBLE;\n}\n\nstatic bool M_CanBeExploded(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_TriggerSummonSmoke(const XYZ_32 pos)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 16;\n    spark->src_color.g = 64;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 8;\n    spark->dst_color.g = 32;\n    spark->dst_color.b = 0;\n    spark->fade_to_black = 64;\n    spark->col_fade_speed = (Random_GetControl() & 7) + 16;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->life = (Random_GetControl() & 0xF) + 96;\n    spark->s_life = spark->life;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x7F) - 64;\n    spark->pos.y = pos.y - (Random_GetControl() & 0x1F);\n    spark->pos.z = pos.z + (Random_GetControl() & 0x7F) - 64;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 0;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -4 - (Random_GetControl() & 7);\n        } else {\n            spark->rot_add = (Random_GetControl() & 7) + 4;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->gravity = -8 - (Random_GetControl() & 7);\n    spark->max_y_vel = -4 - (Random_GetControl() & 7);\n    spark->dst_size.width = (Random_GetControl() & 0x1F) + 128;\n    spark->src_size.width = spark->dst_size.width >> 1;\n    spark->size.width = spark->dst_size.width >> 1;\n    spark->dst_size.height =\n        spark->dst_size.width + (Random_GetControl() & 0x1F) + 32;\n    spark->src_size.height = spark->dst_size.height >> 1;\n    spark->size.height = spark->dst_size.height >> 1;\n    Sparks_FinishSetup(spark);\n}\n\nstatic bool M_CanDropItems(const ITEM *const item)\n{\n    if (item->hit_points > 0) {\n        return false;\n    }\n    if ((item->flags & IF_KILLED) != 0) {\n        return true;\n    }\n    return item->current_anim_state == M_STATE_DEATH\n        && Item_GetRelativeFrame(item) > 119;\n}\n\nstatic const M_LIZARD_SUMMON_COORDS *M_GetLizardSummonCoords(\n    const M_PRIV *const p)\n{\n    return &p->lizard_summon_coords[p->attack_type == M_ATTACK_HAND_1 ? 0 : 1];\n}\n\nstatic void M_TriggerLizard(M_PRIV *const p)\n{\n    ITEM *const item = Item_Get(p->lizard_item_num);\n    int16_t room_num = p->lizard_room_num;\n\n    item->object_id = O_LIZARD;\n    item->pos = M_GetLizardSummonCoords(p)->pos;\n    item->anim_num = Object_Get(O_LIZARD)->anim_idx;\n    item->frame_num = Item_GetAnim(item)->frame_base;\n    item->current_anim_state = Item_GetAnim(item)->current_anim_state;\n    item->goal_anim_state = Item_GetAnim(item)->current_anim_state;\n    item->required_anim_state = M_STATE_WAIT;\n    item->rot.x = 0;\n\n    if (p->attack_type == M_ATTACK_HAND_1) {\n        item->rot.y = -0x8000;\n    } else {\n        item->rot.y = 0;\n    }\n\n    item->rot.z = 0;\n    item->timer = 0;\n    item->flags = 0;\n    item->creature_data = nullptr;\n    item->mesh_bits = -1;\n    item->hit_points = Object_Get(O_LIZARD)->hit_points;\n    item->active = 0;\n    item->status = IS_ACTIVE;\n    item->collidable = 1;\n    item->flags &= ~(IF_KILLED | IF_ONE_SHOT);\n    item->include_in_kill_stats = false;\n\n    // Item_Kill removes it from room item chains; reinsert even when room is\n    // unchanged.\n    Item_UpdateRoom(p->lizard_item_num, NO_ROOM);\n    Item_UpdateRoom(p->lizard_item_num, room_num);\n\n    Item_AddActive(p->lizard_item_num);\n    LOT_EnableBaddieAI(p->lizard_item_num, true);\n\n    Room_GetSector(item->pos, &room_num);\n\n    if (item->room_num != room_num) {\n        Item_UpdateRoom(p->lizard_item_num, room_num);\n    }\n\n    TribeBoss_SetLizardActive(true);\n}\n\nstatic void M_TriggerElectricSparks(\n    const ITEM *const item, const XYZ_32 pos, const bool shield)\n{\n    M_PRIV *const p = item->priv;\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    if (dx < -0x5000 || dx > 0x5000 || dz < -0x5000 || dz > 0x5000) {\n        return;\n    }\n\n    p->trig_dynamics[1] = pos;\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = 255;\n    spark->src_color.b = 255;\n\n    if (shield) {\n        spark->dst_color.r = 255;\n        spark->dst_color.g = (Random_GetControl() & 0x7F) + 64;\n        spark->dst_color.b = 0;\n    } else if (\n        p->attack_type == M_ATTACK_HAND_1\n        || p->attack_type == M_ATTACK_HAND_2) {\n        spark->dst_color.r = 0;\n        spark->dst_color.b = (Random_GetControl() & 0x7F) + 64;\n        spark->dst_color.g = (spark->dst_color.b >> 1) + 128;\n    } else {\n        spark->dst_color.r = 0;\n        spark->dst_color.g = (Random_GetControl() & 0x7F) + 64;\n        spark->dst_color.b = (spark->dst_color.g >> 1) + 128;\n    }\n\n    spark->col_fade_speed = 3;\n    spark->fade_to_black = 8;\n    spark->life = 16;\n    spark->s_life = 16;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = 4 * (Random_GetControl() & 0x1FF) - 1024;\n    spark->vel.y = 2 * (Random_GetControl() & 0x1FF) - 512;\n    spark->vel.z = 4 * (Random_GetControl() & 0x1FF) - 1024;\n\n    if (shield) {\n        spark->vel.x >>= 1;\n        spark->vel.y >>= 1;\n        spark->vel.z >>= 1;\n    }\n\n    spark->friction = 4;\n    spark->flags = SPARK_F_SCALE;\n    spark->scalar = 3;\n    spark->size.width = (Random_GetControl() & 1) + 1;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = (Random_GetControl() & 3) + 4;\n    spark->size.height = (Random_GetControl() & 1) + 1;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = (Random_GetControl() & 3) + 4;\n    spark->gravity = 15;\n    spark->max_y_vel = 0;\n    Sparks_FinishSetup(spark);\n}\n\nstatic bool M_LaraOnLOS(\n    const GAME_VECTOR *const src, const GAME_VECTOR *const beam_target)\n{\n    XYZ_32 lara_pos = {};\n    Collide_GetJointAbsPosition(Lara_GetItem(), &lara_pos, LM_HIPS);\n\n    const int32_t bx = beam_target->x - src->x;\n    const int32_t by = beam_target->y - src->y;\n    const int32_t bz = beam_target->z - src->z;\n    const int32_t lx = lara_pos.x - src->x;\n    const int32_t ly = lara_pos.y - src->y;\n    const int32_t lz = lara_pos.z - src->z;\n\n    const int64_t beam_len2 =\n        (int64_t)bx * bx + (int64_t)by * by + (int64_t)bz * bz;\n    if (beam_len2 == 0) {\n        return false;\n    }\n\n    const int64_t proj = (int64_t)lx * bx + (int64_t)ly * by + (int64_t)lz * bz;\n    if (proj <= 0 || proj > beam_len2) {\n        return false;\n    }\n\n    // Keep kill logic close to the actual beam spine, not just generic LOS.\n    const int64_t lara_len2 =\n        (int64_t)lx * lx + (int64_t)ly * ly + (int64_t)lz * lz;\n    int64_t dist2_num = lara_len2 * beam_len2 - proj * proj;\n    if (dist2_num < 0) {\n        dist2_num = 0;\n    }\n\n    const int32_t hit_radius = 192;\n    const int64_t max_dist2_num = (int64_t)hit_radius * hit_radius * beam_len2;\n    if (dist2_num > max_dist2_num) {\n        return false;\n    }\n\n    GAME_VECTOR lara_target = {\n        .pos = { .x = lara_pos.x, .y = lara_pos.y, .z = lara_pos.z },\n        .room_num = Lara_GetItem()->room_num,\n    };\n    return LOS_Check(src, &lara_target, true);\n}\n\nstatic int32_t M_GetElectricIntensity(\n    const int32_t c, const int32_t scale, const bool copy)\n{\n    int32_t value = c;\n    if (value > 255) {\n        value = 511 - value;\n        if (value < 0) {\n            value = 0;\n        }\n    }\n    if (copy) {\n        value >>= 1;\n    }\n    return (scale * value) >> 6;\n}\n\nstatic void M_DrawElectricBeamQuad(\n    const ITEM *const item, const XYZ_32 prev_l, const XYZ_32 prev_r,\n    const XYZ_32 cur_l, const XYZ_32 cur_r, const int32_t c0, const int32_t c1,\n    const bool copy)\n{\n    const M_PRIV *const p = item->priv;\n    const int32_t i0 = M_GetElectricIntensity(c0, 64, copy);\n    const int32_t i1 = M_GetElectricIntensity(c1, 64, copy);\n    RGBA_8888 c_prev = {};\n    RGBA_8888 c_cur = {};\n\n    if (p->attack_type == M_ATTACK_HEAD) {\n        c_prev = (RGBA_8888) { i0 >> 1, i0, i0, 0xC0 };\n        c_cur = (RGBA_8888) { i1 >> 1, i1, i1, 0xC0 };\n    } else {\n        c_prev = (RGBA_8888) { i0 >> 1, i0, i0 >> 1, 0xC0 };\n        c_cur = (RGBA_8888) { i1 >> 1, i1, i1 >> 1, 0xC0 };\n    }\n\n    const XYZ_32 world_pos[4] = { prev_l, prev_r, cur_r, cur_l };\n    const RGBA_8888 color[4] = { c_prev, c_prev, c_cur, c_cur };\n    const float disp[4][2] = {};\n    OutputSource_PolyFX_StageQuadExt(\n        -1, world_pos, disp, color,\n        VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_ADD);\n}\n\nstatic void M_TriggerElectricBeam(\n    const ITEM *const item, GAME_VECTOR *const src, const bool copy,\n    const bool do_state, const bool do_render)\n{\n    M_PRIV *const p = item->priv;\n    GAME_VECTOR target = {};\n    const int16_t angle = (item->rot.y >> 4) & 0xFFF;\n    src->room_num = item->room_num;\n\n    int32_t dx = p->beam_target.x - src->x;\n    int32_t dy = p->beam_target.y - src->y;\n    int32_t dz = p->beam_target.z - src->z;\n    int32_t longest = ABS(dx);\n    if (ABS(dy) > longest) {\n        longest = ABS(dy);\n    }\n    if (ABS(dz) > longest) {\n        longest = ABS(dz);\n    }\n    if (longest < 20480) {\n        longest = 20480 / longest + 1;\n        dx *= longest;\n        dy *= longest;\n        dz *= longest;\n    }\n\n    target.x = src->x + dx;\n    target.y = src->y + dy;\n    target.z = src->z + dz;\n    LOS_Check(src, &target, true);\n\n    if (do_state) {\n        LARA_INFO *const lara_info = Lara_GetLaraInfo();\n        ITEM *const lara_item = Lara_GetItem();\n        if (lara_info->electric == 0 && !copy && p->attack_type == M_ATTACK_HEAD\n            && M_LaraOnLOS(src, &target)\n            && !g_Config.debug.enable_invulnerability) {\n            lara_item->hit_points = 0;\n            lara_info->electric = 1;\n        }\n\n        M_TriggerElectricSparks(item, target.pos, false);\n    }\n\n    if (!do_render) {\n        return;\n    }\n\n    dx = ABS(target.x - src->x);\n    dz = ABS(target.z - src->z);\n    int32_t n_segments = dx >= dz ? dx >> 8 : dz >> 8;\n    CLAMP(n_segments, 8, 24);\n\n    const XYZ_32 src_pos = { src->x, src->y, src->z };\n    const XYZ_32 dst_pos = { target.x, target.y, target.z };\n\n    const int16_t side_angle = (angle + 1024) & 0xFFF;\n    const int32_t x_off = (p->attack_type == M_ATTACK_HEAD)\n        ? (Math_Sin(side_angle << 4) >> 10)\n        : (Math_Sin(side_angle << 4) >> 11);\n    const int32_t z_off = (p->attack_type == M_ATTACK_HEAD)\n        ? (Math_Cos(side_angle << 4) >> 10)\n        : (Math_Cos(side_angle << 4) >> 11);\n\n    int32_t y_off1 = 0;\n    int32_t y_off2 = 0;\n    XYZ_32 prev_l = src_pos;\n    XYZ_32 prev_r = src_pos;\n    int32_t prev_c = 0;\n\n    for (int32_t i = 1; i <= n_segments; i++) {\n        const XYZ_32 center = {\n            .x = src_pos.x + ((dst_pos.x - src_pos.x) * i) / n_segments,\n            .y = src_pos.y + ((dst_pos.y - src_pos.y) * i) / n_segments,\n            .z = src_pos.z + ((dst_pos.z - src_pos.z) * i) / n_segments,\n        };\n\n        int32_t c = 0;\n        XYZ_32 cur_l = center;\n        XYZ_32 cur_r = center;\n        if (i != n_segments) {\n            const XYZ_16 point = Lara_Electricity_GetPoint((copy ? 4 : 0) + i);\n            const int32_t xs = copy ? -point.x : point.x;\n            const int32_t ys = copy ? -(point.y >> 1) : (point.y >> 1);\n            const int32_t zs = copy ? -point.z : point.z;\n\n            y_off1 += (Random_GetDraw() & 0x1F) - 16;\n            y_off2 += (Random_GetDraw() & 0x1F) - 16;\n            CLAMP(y_off1, -192, 192);\n            CLAMP(y_off2, -192, 192);\n\n            cur_l = (XYZ_32) {\n                center.x + xs + x_off,\n                center.y + ys + y_off1,\n                center.z + zs + z_off,\n            };\n            cur_r = (XYZ_32) {\n                center.x + xs - x_off,\n                center.y + ys + y_off2,\n                center.z + zs - z_off,\n            };\n            c = (Random_GetDraw() & 0xFF) >> copy;\n        }\n\n        M_DrawElectricBeamQuad(\n            item, prev_l, prev_r, cur_l, cur_r, prev_c, c, copy);\n        prev_l = cur_l;\n        prev_r = cur_r;\n        prev_c = c;\n    }\n}\n\nstatic void M_DrawElectricChain(\n    const XYZ_32 start, const XYZ_32 end, const bool copy, const int32_t scale,\n    int32_t *const point_idx)\n{\n    XYZ_32 prev = start;\n    int32_t prev_c = 0;\n    for (int32_t j = 1; j <= 4; j++) {\n        XYZ_32 cur = {\n            .x = start.x + ((end.x - start.x) * j) / 4,\n            .y = start.y + ((end.y - start.y) * j) / 4,\n            .z = start.z + ((end.z - start.z) * j) / 4,\n        };\n        int32_t cur_c = 0;\n\n        if (j != 4) {\n            const XYZ_16 point = Lara_Electricity_GetPoint(*point_idx);\n            (*point_idx)++;\n            const int32_t ex = copy ? -point.x : point.x;\n            const int32_t ey = copy ? -point.y : point.y;\n            const int32_t ez = copy ? -point.z : point.z;\n            cur.x += ex >> 3;\n            cur.y += ey >> 3;\n            cur.z += ez >> 3;\n\n            cur_c = ABS(ex);\n            if (ABS(ey) > cur_c) {\n                cur_c = ABS(ey);\n            }\n            if (ABS(ez) > cur_c) {\n                cur_c = ABS(ez);\n            }\n        }\n\n        int32_t i0 = M_GetElectricIntensity(prev_c, scale, copy);\n        int32_t i1 = M_GetElectricIntensity(cur_c, scale, copy);\n        CLAMP(i0, 0, 255);\n        CLAMP(i1, 0, 255);\n        const RGBA_8888 c0 = { 0, (uint8_t)i0, (uint8_t)i0, 0xC0 };\n        const RGBA_8888 c1 = { 0, (uint8_t)i1, (uint8_t)i1, 0xC0 };\n        const float width = 4.0f;\n        OutputSource_PolyFX_StageLineSegment(\n            prev, c0, cur, c1, width, DRAW_BLEND_ADD);\n        prev = cur;\n        prev_c = cur_c;\n    }\n}\n\nstatic void M_TriggerHeadElectricity(\n    const ITEM *const item, const bool copy, const bool do_state,\n    const bool do_render)\n{\n    M_PRIV *const p = item->priv;\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if (dx < -0x4800 || dx > 0x4800 || dz < -0x4800 || dz > 0x4800) {\n        return;\n    }\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t s = (Math_Sin(time4 << 8) >> 10) + 64;\n    int32_t point_idx = 0;\n\n    for (int32_t i = 0; i < 4; i++) {\n        XYZ_32 pos1 = m_HelmetSpikes[i].pos;\n        XYZ_32 pos2 = m_HelmetSpikes[i + 1].pos;\n        Collide_GetJointAbsPosition(item, &pos1, m_HelmetSpikes[i].mesh_num);\n        Collide_GetJointAbsPosition(\n            item, &pos2, m_HelmetSpikes[i + 1].mesh_num);\n\n        if (i == 2 && do_state) {\n            p->trig_dynamics[0] = pos1;\n        }\n        if (do_render) {\n            M_DrawElectricChain(pos1, pos2, copy, s, &point_idx);\n        }\n    }\n\n    if (p->attack_count != 0 && p->death_count == 0\n        && p->attack_type == M_ATTACK_HEAD) {\n        int32_t extra_idx = 16;\n        for (int32_t i = 0; i < 5; i++) {\n            XYZ_32 pos1 = m_HelmetSpikes[i].pos;\n            XYZ_32 pos2 = m_EnergyHit.pos;\n            Collide_GetJointAbsPosition(\n                item, &pos1, m_HelmetSpikes[i].mesh_num);\n            Collide_GetJointAbsPosition(item, &pos2, m_EnergyHit.mesh_num);\n\n            int32_t scale = s;\n            if (p->attack_count < 64) {\n                scale = (p->attack_count * scale) >> 6;\n            }\n            if (do_render) {\n                M_DrawElectricChain(pos1, pos2, copy, scale, &extra_idx);\n            }\n\n            if (do_state && i == 4 && p->attack_count >= 64\n                && p->attack_count <= 128) {\n                p->trig_dynamics[2] = pos2;\n            }\n        }\n    }\n\n    if (p->attack_count != 0 && p->death_count == 0\n        && (p->attack_type == M_ATTACK_HAND_1\n            || p->attack_type == M_ATTACK_HAND_2)) {\n        XYZ_32 pos = {};\n        Collide_GetJointAbsPosition(item, &pos, 14);\n        GAME_VECTOR src = {\n            .pos = { .x = pos.x, .y = pos.y, .z = pos.z },\n            .room_num = item->room_num,\n        };\n\n        if (p->attack_count >= 64) {\n            if (p->attack_count <= 128) {\n                if (do_state) {\n                    p->trig_dynamics[2] = pos;\n                }\n            }\n\n            if (p->attack_count <= 96) {\n                if (do_state && p->attack_count > 90\n                    && !p->shared->lizard_active) {\n                    M_TriggerLizard(p);\n                }\n\n                M_TriggerElectricBeam(item, &src, copy, do_state, do_render);\n\n                if (do_state) {\n                    for (int32_t i = 0; i < 3; i++) {\n                        M_TriggerSummonSmoke(p->beam_target);\n                    }\n                }\n            }\n        }\n    } else if (\n        p->attack_count > 64 && p->death_count == 0\n        && p->attack_type == M_ATTACK_HEAD) {\n        GAME_VECTOR src = { .pos = m_EnergyHit.pos,\n                            .room_num = item->room_num };\n        Collide_GetJointAbsPosition(item, &src.pos, 8);\n        M_TriggerElectricBeam(item, &src, copy, do_state, do_render);\n    }\n}\n\nstatic void M_FindClosestShieldPoint(ITEM *const item, const XYZ_32 pos)\n{\n    M_PRIV *const p = item->priv;\n    const int32_t affected[5] = {\n        0, -1, 1, -8, 8,\n    };\n    int32_t best_dist = INT32_MAX;\n    int32_t point = 0;\n\n    for (int32_t i = 0; i < 40; i++) {\n        const M_SHIELD_POINT *const shield = &p->shield[i >> 3][i & 7];\n        if (i >= 16 && i <= 23) {\n            const int32_t dx = shield->pos.x + item->pos.x - pos.x;\n            const int32_t dy = shield->pos.y + item->pos.y - pos.y;\n            const int32_t dz = shield->pos.z + item->pos.z - pos.z;\n            const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n            if (dist < best_dist) {\n                best_dist = dist;\n                point = i;\n            }\n        }\n    }\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    int32_t c;\n    switch (lara_info->gun_type) {\n    case LGT_PISTOLS:\n    case LGT_UZIS:\n        c = 144;\n        break;\n\n    case LGT_MAGNUMS:\n    case LGT_AUTOS:\n    case LGT_DESERT_EAGLE:\n    case LGT_HARPOON:\n        c = 200;\n        break;\n\n    case LGT_SHOTGUN:\n    case LGT_M16:\n    case LGT_MP5:\n        c = 192;\n        break;\n\n    case LGT_ROCKET:\n    case LGT_GRENADE:\n        c = 224;\n        break;\n\n    default:\n        c = 180;\n        break;\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        int32_t n = point + affected[i];\n\n        if ((n & 7) == 7 && affected[i] == -1) {\n            n += 8;\n        }\n\n        if ((n & 7) == 0 && affected[i] == 1) {\n            n -= 8;\n        }\n\n        M_SHIELD_POINT *const shield = &p->shield[n >> 3][n & 7];\n        int32_t r = shield->color.r;\n        int32_t g = shield->color.g;\n        int32_t b = shield->color.b;\n\n        if (i == 0) {\n            if (c >= 200) {\n                r = c;\n            } else {\n                r += c >> 2;\n                if (r > c) {\n                    r = c;\n                }\n            }\n        } else {\n            if (c >= 200) {\n                r = c >> 1;\n            } else {\n                r += c >> 3;\n                if (r > (c >> 1)) {\n                    r = c >> 1;\n                }\n            }\n        }\n\n        if (i == 0) {\n            if (c >= 200) {\n                g = c;\n            } else {\n                g += c >> 2;\n                if (g > c) {\n                    g = c;\n                }\n            }\n        } else {\n            if (c >= 200) {\n                g = c >> 1;\n            } else {\n                g += c >> 3;\n                if (g > (c >> 1)) {\n                    g = c >> 1;\n                }\n            }\n        }\n\n        if (i == 0) {\n            if (c >= 200) {\n                b = c;\n            } else {\n                b += c >> 2;\n                if (b > c) {\n                    b = c;\n                }\n            }\n        } else {\n            if (c >= 200) {\n                b = c >> 1;\n            } else {\n                b += c >> 3;\n                if (b > (c >> 1)) {\n                    b = c >> 1;\n                }\n            }\n        }\n\n        shield->sub.r = (Random_GetControl() & 7) + 8;\n        shield->sub.g = (Random_GetControl() & 7) + 8;\n        shield->sub.b = (Random_GetControl() & 7) + 8;\n\n        if (lara_info->gun_type == LGT_ROCKET\n            || lara_info->gun_type == LGT_GRENADE) {\n            shield->sub.r >>= 1;\n            shield->sub.g >>= 1;\n            shield->sub.b >>= 1;\n        }\n\n        shield->color = (RGB_888) { r, g, b };\n    }\n\n    for (int32_t i = 0; i < 7; i++) {\n        M_TriggerElectricSparks(item, pos, true);\n    }\n}\n\nstatic bool M_GunHit(\n    ITEM *const item, const GAME_VECTOR *const start,\n    const GAME_VECTOR *const hit_pos, int32_t *const damage)\n{\n    M_PRIV *const p = item->priv;\n    if (p->shield_on) {\n        p->shield_active = true;\n        if (hit_pos != nullptr) {\n            M_FindClosestShieldPoint(item, hit_pos->pos);\n        }\n        if (damage != nullptr) {\n            *damage = 0;\n        }\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    return !p->shield_on;\n}\n\nstatic void M_UpdateShield(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    for (int32_t i = 8; i <= 31; i++) {\n        M_SHIELD_POINT *const shield = &p->shield[i >> 3][i & 7];\n        shield->color.r = MAX(0, (int32_t)shield->color.r - shield->sub.r);\n        shield->color.g = MAX(0, (int32_t)shield->color.g - shield->sub.g);\n        shield->color.b = MAX(0, (int32_t)shield->color.b - shield->sub.b);\n    }\n}\n\nstatic void M_DrawShield(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    for (int32_t band = 0; band < 4; band++) {\n        const int32_t sprite_idx =\n            sprite_base + 18 + ((band + (time4 >> 3)) & 7);\n\n        for (int32_t j = 0; j < 8; j++) {\n            const int32_t j2 = (j == 7) ? 0 : (j + 1);\n            const M_SHIELD_POINT *const s00 = &p->shield[band][j];\n            const M_SHIELD_POINT *const s01 = &p->shield[band][j2];\n            const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j];\n            const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2];\n\n            const RGB_888 c00 = s00->color;\n            const RGB_888 c01 = s01->color;\n            const RGB_888 c10 = s10->color;\n            const RGB_888 c11 = s11->color;\n\n            if (((c00.r | c00.g | c00.b | c01.r | c01.g | c01.b | c11.r | c11.g\n                  | c11.b | c10.r | c10.g | c10.b)\n                 == 0U)) {\n                continue;\n            }\n            const XYZ_32 world_pos[4] = {\n                {\n                    item->pos.x + s00->pos.x,\n                    item->pos.y + s00->pos.y,\n                    item->pos.z + s00->pos.z,\n                },\n                {\n                    item->pos.x + s01->pos.x,\n                    item->pos.y + s01->pos.y,\n                    item->pos.z + s01->pos.z,\n                },\n                {\n                    item->pos.x + s11->pos.x,\n                    item->pos.y + s11->pos.y,\n                    item->pos.z + s11->pos.z,\n                },\n                {\n                    item->pos.x + s10->pos.x,\n                    item->pos.y + s10->pos.y,\n                    item->pos.z + s10->pos.z,\n                },\n            };\n            const RGBA_8888 color[4] = {\n                { c00.r, c00.g, c00.b, 255 },\n                { c01.r, c01.g, c01.b, 255 },\n                { c11.r, c11.g, c11.b, 255 },\n                { c10.r, c10.g, c10.b, 255 },\n            };\n            OutputSource_PolyFX_StageSpriteQuadWorld(\n                sprite_idx, world_pos, color, DRAW_BLEND_ADD);\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    Object_DrawAnimatingItem(item);\n\n    if (p->explode_count == 0) {\n        M_TriggerHeadElectricity(item, false, false, true);\n        M_TriggerHeadElectricity(item, true, false, true);\n    }\n\n    M_DrawShield(item);\n    return true;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    p->shared = &m_SharedPriv;\n    JSON_SHOULD(JSON_READ(io, \"dead\", &p->dead));\n    JSON_SHOULD(JSON_READ(io, \"attack_count\", &p->attack_count));\n    JSON_SHOULD(JSON_READ(io, \"death_count\", &p->death_count));\n    JSON_SHOULD(JSON_READ(io, \"attack_flag\", &p->attack_flag));\n    JSON_SHOULD(JSON_READ(io, \"attack_type\", &p->attack_type));\n    JSON_SHOULD(JSON_READ(io, \"attack_head_count\", &p->attack_head_count));\n    JSON_SHOULD(JSON_READ(io, \"ring_count\", &p->ring_count));\n    JSON_SHOULD(JSON_READ(io, \"explode_count\", &p->explode_count));\n    JSON_SHOULD(JSON_READ(io, \"lizard_item_num\", &p->lizard_item_num));\n    JSON_SHOULD(JSON_READ(io, \"lizard_room_num\", &p->lizard_room_num));\n    JSON_SHOULD(JSON_READ(io, \"dropped_item\", &p->dropped_item));\n    JSON_SHOULD(JSON_READ(io, \"shield_on\", &p->shield_on));\n    JSON_SHOULD(JSON_READ(io, \"shield_active\", &p->shield_active));\n    JSON_SHOULD(JSON_READ(io, \"turned\", &p->turned));\n    JSON_SHOULD(JSON_READ(io, \"beam_target_x\", &p->beam_target.x));\n    JSON_SHOULD(JSON_READ(io, \"beam_target_y\", &p->beam_target.y));\n    JSON_SHOULD(JSON_READ(io, \"beam_target_z\", &p->beam_target.z));\n    JSON_SHOULD(\n        JSON_READ(io, \"shared_lizard_active\", &p->shared->lizard_active));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"dead\", p->dead);\n    JSONW_WRITE(io, \"attack_count\", p->attack_count);\n    JSONW_WRITE(io, \"death_count\", p->death_count);\n    JSONW_WRITE(io, \"attack_flag\", p->attack_flag);\n    JSONW_WRITE(io, \"attack_type\", p->attack_type);\n    JSONW_WRITE(io, \"attack_head_count\", p->attack_head_count);\n    JSONW_WRITE(io, \"ring_count\", p->ring_count);\n    JSONW_WRITE(io, \"explode_count\", p->explode_count);\n    JSONW_WRITE(io, \"lizard_item_num\", p->lizard_item_num);\n    JSONW_WRITE(io, \"lizard_room_num\", p->lizard_room_num);\n    JSONW_WRITE(io, \"dropped_item\", p->dropped_item);\n    JSONW_WRITE(io, \"shield_on\", p->shield_on);\n    JSONW_WRITE(io, \"shield_active\", p->shield_active);\n    JSONW_WRITE(io, \"turned\", p->turned);\n    JSONW_WRITE(io, \"beam_target_x\", p->beam_target.x);\n    JSONW_WRITE(io, \"beam_target_y\", p->beam_target.y);\n    JSONW_WRITE(io, \"beam_target_z\", p->beam_target.z);\n    JSONW_WRITE(io, \"shared_lizard_active\", p->shared->lizard_active);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->shared = &m_SharedPriv;\n    p->lizard_item_num = M_FindLizard(item->room_num);\n    p->lizard_room_num = Item_Get(p->lizard_item_num)->room_num;\n\n    for (int32_t i = 0; i < 2; i++) {\n        const int32_t sign = i == 0 ? -1 : 1;\n        int32_t y_rot = item->rot.y + DEG_180; // after turning\n        XYZ_32 pos = item->pos;\n        pos = XYZ_32_OffsetYaw(pos, y_rot, WALL_L * 3);\n        pos = XYZ_32_OffsetYaw(pos, y_rot + DEG_270 * sign, WALL_L * 5);\n\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(pos, &room_num);\n        pos.y = Room_GetHeight(sector, pos);\n\n        p->lizard_summon_coords[i].pos = pos;\n        p->lizard_summon_coords[i].y_rot = y_rot - DEG_45 * sign;\n    }\n\n    for (int32_t i = 0; i < 3; i++) {\n        p->trig_dynamics[i].x = 0;\n    }\n\n    p->dropped_item = false;\n    p->dead = 0;\n    p->ring_count = 0;\n    p->explode_count = 0;\n    p->attack_head_count = 0;\n    p->death_count = 0;\n    p->attack_flag = 0;\n    p->attack_count = 0;\n    p->shield_active = false;\n    p->shield_on = false;\n    TribeBoss_SetLizardActive(false);\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t y = m_Heights[i];\n        const int32_t r = m_Dist[i];\n        int32_t angle = 0;\n\n        for (int32_t j = 0; j < 8; j++) {\n            p->shield[i][j].pos.x = (int16_t)((r * Math_Sin(angle << 4)) >> 13);\n            p->shield[i][j].pos.y = (int16_t)y;\n            p->shield[i][j].pos.z = (int16_t)((r * Math_Cos(angle << 4)) >> 13);\n            p->shield[i][j].color = (RGB_888) {};\n            angle += 512;\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n    int16_t head = 0;\n    int16_t old_y_rot = INT16_MAX;\n\n    Lara_Electricity_UpdatePoints();\n    p->shield_active = false;\n    M_UpdateShield(item);\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (item->hit_status) {\n            Sound_Effect(SFX_TRIBOSS_TAKE_HIT, &item->pos, SPM_NORMAL);\n        }\n\n        old_y_rot = item->rot.y;\n\n        const ITEM *const lara_item = Lara_GetItem();\n        if (p->attack_flag == 0) {\n            const int32_t x_dist = item->pos.x - lara_item->pos.x;\n            const int32_t z_dist = item->pos.z - lara_item->pos.z;\n\n            if (SQUARE(x_dist) + SQUARE(z_dist) < 0x400000) {\n                p->attack_flag = 1;\n            }\n        }\n\n        creature->target.x = lara_item->pos.x;\n        creature->target.z = lara_item->pos.z;\n\n        if (!p->shared->lizard_active\n            || item->current_anim_state != M_STATE_WAIT) {\n            angle = Creature_Turn(item, creature->maximum_turn);\n        } else {\n            const uint16_t y_test = item->rot.y;\n            if (ABS(0xC000 - y_test) > DEG_1) {\n                item->rot.y += (0xC000 - y_test) >> 3;\n            } else {\n                item->rot.y = -0x4000;\n            }\n        }\n\n        M_RotateHeadXAngle(item);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_WAIT:\n            p->attack_count = 0;\n\n            if (item->goal_anim_state != M_STATE_ATTACK_HEAD\n                && item->goal_anim_state != M_STATE_ATTACK_HAND) {\n                p->shield_on = true;\n            }\n\n            if (p->shared->lizard_active) {\n                Creature_Joint(item, 1, head);\n            } else {\n                Creature_Joint(item, 1, 0);\n            }\n\n            if (p->attack_flag == 0 || p->shared->lizard_active) {\n                creature->maximum_turn = 0;\n            } else {\n                creature->maximum_turn = 546;\n            }\n\n            if (item->goal_anim_state != M_STATE_ATTACK_HEAD\n                && info.angle > -128 && info.angle < 128\n                && lara_item->hit_points > 0\n                && p->attack_head_count < M_MAX_HEAD_ATTACKS\n                && !p->shared->lizard_active && !p->shield_active) {\n                XYZ_32 pos = {};\n                Collide_GetJointAbsPosition(lara_item, &pos, 0);\n                p->beam_target = pos;\n                item->goal_anim_state = M_STATE_ATTACK_HEAD;\n                creature->maximum_turn = 0;\n                p->shield_on = false;\n                p->attack_head_count++;\n                break;\n            }\n\n            if (item->goal_anim_state == M_STATE_ATTACK_HAND\n                || p->attack_head_count < M_MAX_HEAD_ATTACKS\n                || lara_item->hit_points <= 0) {\n                break;\n            }\n\n            creature->maximum_turn = 0;\n\n            if (p->attack_type == M_ATTACK_HEAD) {\n                const int32_t x1 =\n                    lara_item->pos.x - p->lizard_summon_coords[0].pos.x;\n                const int32_t z1 =\n                    lara_item->pos.z - p->lizard_summon_coords[0].pos.z;\n                const int32_t x2 =\n                    lara_item->pos.x - p->lizard_summon_coords[1].pos.x;\n                const int32_t z2 =\n                    lara_item->pos.z - p->lizard_summon_coords[1].pos.z;\n\n                if (SQUARE(x1) + SQUARE(z1) > SQUARE(x2) + SQUARE(z2)) {\n                    p->attack_type = M_ATTACK_HAND_1;\n                } else {\n                    p->attack_type = M_ATTACK_HAND_2;\n                }\n            }\n\n            const uint16_t y_test = item->rot.y;\n            const uint16_t y_rot = M_GetLizardSummonCoords(p)->y_rot;\n            if (ABS(y_rot - y_test) >= DEG_1) {\n                item->rot.y += (y_rot - y_test) >> 4;\n            } else {\n                item->rot.y = y_rot;\n\n                if (!p->shield_active) {\n                    item->goal_anim_state = M_STATE_ATTACK_HAND;\n                    p->beam_target = M_GetLizardSummonCoords(p)->pos;\n                    creature->maximum_turn = 0;\n                    p->attack_head_count = 0;\n                    p->shield_on = false;\n                }\n            }\n            break;\n\n        case M_STATE_ATTACK_HEAD:\n            creature->maximum_turn = 0;\n            p->attack_count += 3;\n            p->attack_type = M_ATTACK_HEAD;\n            Creature_Joint(item, 1, 0);\n            break;\n\n        case M_STATE_ATTACK_HAND:\n            creature->maximum_turn = 0;\n\n            if (p->attack_count < 64) {\n                p->attack_count += 2;\n            } else {\n                p->attack_count += 3;\n            }\n\n            Creature_Joint(item, 1, 0);\n            break;\n        }\n    } else {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n            item->current_anim_state = M_STATE_DEATH;\n            p->death_count = 1;\n        }\n\n        if (Item_GetRelativeFrame(item) > 119) {\n            item->mesh_bits = 0;\n            Item_SwitchToAnim(item, Item_GetRelativeAnim(item), 120);\n            p->death_count = -1;\n\n            if (p->explode_count == 0) {\n                p->ring_count = 0;\n                Sound_Effect(SFX_EXPLOSION_2, &item->pos, SPM_NORMAL);\n\n                for (int32_t i = 0; i < 6; i++) {\n                    FX_RING *const ring =\n                        FX_Ring_GetRing(FX_RING_TYPE_BLAST, i);\n                    if (ring == nullptr) {\n                        continue;\n                    }\n                    ring->on = 0;\n                    ring->life = 32;\n                    ring->radius = 512;\n                    ring->speed = (i << 5) + 128;\n                    ring->rot.x = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF;\n                    ring->rot.z = ((Random_GetControl() & 0x1FF) - 256) & 0xFFF;\n                    FX_Ring_Sync(ring);\n                }\n\n                if (!p->dropped_item) {\n                    Carrier_TestItemDrops(item_num);\n                    p->dropped_item = true;\n                }\n            }\n\n            if (p->explode_count < 256) {\n                p->explode_count++;\n            }\n\n            if (p->explode_count <= 128 || p->ring_count != 6\n                || FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life != 0) {\n                M_Explode(item);\n            } else {\n                M_Die(item_num);\n                p->dead = 1;\n            }\n\n            return;\n        }\n\n        item->rot.z =\n            Random_GetControl() % p->death_count - (p->death_count >> 1);\n\n        if (p->death_count < 2048) {\n            p->death_count += 32;\n        }\n    }\n\n    if (p->attack_count != 0 && p->attack_type == M_ATTACK_HEAD\n        && p->attack_count < 64) {\n        m_EnergyHit.pos.z = 4 * p->attack_count + 136;\n    }\n\n    creature->joint_rotation[0] += 1274;\n    Creature_Animate(item_num, angle, 0);\n\n    p->trig_dynamics[0].x = 0;\n    p->trig_dynamics[1].x = 0;\n    p->trig_dynamics[2].x = 0;\n    if (p->explode_count == 0) {\n        M_TriggerHeadElectricity(item, false, true, false);\n        M_TriggerHeadElectricity(item, true, true, false);\n    }\n\n    for (int32_t i = 0; i < 3; i++) {\n        if (!p->trig_dynamics[i].x) {\n            continue;\n        }\n\n        XYZ_32 pos;\n        RGB_888 color;\n        int32_t falloff;\n        if (i == 0) {\n            pos = p->trig_dynamics[0];\n            falloff = (Random_GetControl() & 3) + 8;\n\n            color.r = 0;\n            color.g = (Random_GetControl() & 0x3F) + 64;\n            color.b = (Random_GetControl() & 0x3F) + 128;\n        } else if (i == 1) {\n            pos = p->trig_dynamics[1];\n            falloff = (Random_GetControl() & 7) + 8;\n\n            color.r = 0;\n            if (p->attack_type == M_ATTACK_HEAD) {\n                color.g = (Random_GetControl() & 0x3F) + 64;\n                color.b = (Random_GetControl() & 0x3F) + 128;\n            } else {\n                color.g = (Random_GetControl() & 0x3F) + 128;\n                color.b = (Random_GetControl() & 0x3F) + 64;\n            }\n        } else {\n            if (p->attack_count == 0) {\n                continue;\n            }\n\n            pos = p->trig_dynamics[2];\n            falloff = (128 - p->attack_count) >> 1;\n            CLAMPG(falloff, 31);\n            if (falloff <= 0) {\n                continue;\n            }\n\n            color.r = falloff << 1;\n            if (p->attack_type == M_ATTACK_HEAD) {\n                color.g = (Random_GetControl() & 0x3F) + 128;\n                color.b = (Random_GetControl() & 0x3F) + 192;\n            } else {\n                color.g = (Random_GetControl() & 0x3F) + 192;\n                color.b = (Random_GetControl() & 0x3F) + 128;\n            }\n        }\n\n        Output_AddDynamicLightRGB(pos, falloff, color);\n    }\n\n    if (old_y_rot != item->rot.y && !p->turned) {\n        p->turned = true;\n        Sound_Effect(SFX_TRIBOSS_TURN_CHAIR, &item->pos, 0x800000 | SPM_PITCH);\n        return;\n    }\n\n    if (old_y_rot == item->rot.y) {\n        p->turned = false;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->draw_func = M_Draw;\n    obj->gun_hit_func = M_GunHit;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n    obj->can_drop_items_func = M_CanDropItems;\n    obj->can_be_exploded_func = M_CanBeExploded;\n\n    obj->shadow_size = 0;\n    obj->hit_points = 200;\n    obj->pivot_length = 50;\n    obj->radius = 102;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 4)->rot.y = true;\n    Object_GetBone(obj, 7)->rot.y = true;\n    Object_GetBone(obj, 7)->rot.x = true;\n}\n\nbool TribeBoss_IsLizardActive(void)\n{\n    return m_SharedPriv.lizard_active;\n}\n\nvoid TribeBoss_SetLizardActive(const bool active)\n{\n    m_SharedPriv.lizard_active = active;\n}\n\nREGISTER_OBJECT(O_TRIBE_BOSS, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tribe_boss.h",
    "content": "#pragma once\n\nbool TribeBoss_IsLizardActive(void);\nvoid TribeBoss_SetLizardActive(bool active);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/tribe_pipeman.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_BIFF_DAMAGE       100\n#define M_BIFF_ENEMY_DAMAGE 5\n#define M_WALK_TURN         (9 * DEG_1) // = 1638\n#define M_RUN_TURN          (6 * DEG_1) // = 1092\n#define M_WAIT_TURN         (2 * DEG_1) // = 364\n#define M_PIPE_RANGE        SQUARE(WALL_L * 8) // = 0x4000000\n#define M_CLOSE_RANGE       SQUARE(WALL_L / 2) // = 0x40000\n#define M_WALK_RANGE        SQUARE(WALL_L * 2) // = 0x400000\n#define M_AWARE_DISTANCE    (WALL_L)\n#define M_HIT_RANGE         (STEP_L * 2)\n#define M_TOUCH_BITS        0x2400\n// clang-format on\n\ntypedef enum {\n    M_ANIM_DEATH_STANDING = 20,\n    M_ANIM_DEATH_KNEELING = 21,\n} M_ANIM;\n\ntypedef enum {\n    M_STATE_NULL,\n    M_STATE_WAIT_1,\n    M_STATE_WALK,\n    M_STATE_RUN,\n    M_STATE_ATTACK_1,\n    M_STATE_ATTACK_2,\n    M_STATE_ATTACK_3,\n    M_STATE_ATTACK_4,\n    M_STATE_AIM_3,\n    M_STATE_DEATH,\n    M_STATE_ATTACK_5,\n    M_STATE_WAIT_2\n} M_STATE;\n\nstatic BITE m_BiffHit = {\n    .pos = { .x = 0, .y = 0, .z = -200 },\n    .mesh_num = 13,\n};\nstatic BITE m_ShootHit = {\n    .pos = { .x = 8, .y = 40, .z = -248 },\n    .mesh_num = 13,\n};\n\nstatic void M_SpawnDart(ITEM *const item)\n{\n    const int16_t dart_item_num = Item_Create();\n    if (dart_item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const dart_item = Item_Get(dart_item_num);\n    dart_item->object_id = O_POISON_DART;\n    dart_item->room_num = item->room_num;\n\n    XYZ_32 pos1 = m_ShootHit.pos;\n    Collide_GetJointAbsPosition(item, &pos1, m_ShootHit.mesh_num);\n\n    XYZ_32 pos2 = {};\n    const CREATURE *const creature = item->creature_data;\n    if (g_Config.gameplay.fix_pipeman_aim && creature->enemy != nullptr) {\n        if (creature->enemy == Lara_GetItem()) {\n            Lara_GetMeshPos(LM_TORSO, &pos2);\n        } else {\n            pos2 = creature->enemy->pos;\n        }\n    } else {\n        pos2 = m_ShootHit.pos;\n        pos2.z <<= 1;\n        Collide_GetJointAbsPosition(item, &pos2, m_ShootHit.mesh_num);\n    }\n\n    int16_t angles[2];\n    Math_GetVectorAngles(\n        pos2.x - pos1.x, pos2.y - pos1.y, pos2.z - pos1.z, angles);\n    dart_item->pos = pos1;\n\n    Item_Initialise(dart_item_num);\n    dart_item->rot.x = angles[1];\n    dart_item->rot.y = angles[0];\n    dart_item->speed = 256;\n    Item_AddActive(dart_item_num);\n    dart_item->status = IS_ACTIVE;\n\n    XYZ_32 smoke_pos = {\n        .x = m_ShootHit.pos.x,\n        .y = m_ShootHit.pos.y,\n        .z = m_ShootHit.pos.z + 96,\n    };\n    Collide_GetJointAbsPosition(item, &smoke_pos, m_ShootHit.mesh_num);\n    for (int32_t i = 0; i < 2; i++) {\n        Sparks_TriggerDartSmoke(smoke_pos, (XZ_32) {}, true);\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n    int16_t torso_x = 0;\n    int16_t torso_y = 0;\n    int16_t head = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != M_STATE_DEATH) {\n            if (item->current_anim_state == M_STATE_WAIT_1\n                || item->current_anim_state == M_STATE_ATTACK_1) {\n                Item_SwitchToAnim(item, M_ANIM_DEATH_KNEELING, 0);\n            } else {\n                Item_SwitchToAnim(item, M_ANIM_DEATH_STANDING, 0);\n            }\n            item->current_anim_state = M_STATE_DEATH;\n        }\n    } else {\n        if (item->ai_bits != 0) {\n            Creature_GetAITarget(creature);\n        }\n\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_UpdateMood(item, &info, info.zone_num == info.enemy_zone_num);\n\n        if (item->hit_status && lara->poison_timer >= 256\n            && creature->mood == MOOD_BORED) {\n            creature->mood = MOOD_ESCAPE;\n        }\n\n        Creature_ApplyMood(item, &info, false);\n        angle = Creature_Turn(\n            item,\n            creature->mood == MOOD_BORED ? M_WAIT_TURN\n                                         : creature->maximum_turn);\n\n        if (info.ahead) {\n            head = info.angle >> 1;\n            torso_y = info.angle >> 1;\n        }\n\n        if (item->hit_status\n            || (creature->enemy == lara_item\n                && (info.distance < M_AWARE_DISTANCE\n                    || Creature_CanSeeEnemy(item, &info))\n                && (ABS(lara_item->pos.y - item->pos.y) < WALL_L * 2))) {\n            Creature_AlertAllGuards(item_num);\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_WAIT_1:\n            if (info.ahead) {\n                torso_x = info.x_angle >> 1;\n                torso_y = info.angle;\n            }\n\n            creature->flags &= 0xFFF;\n            creature->maximum_turn = M_WAIT_TURN;\n\n            if (item->ai_bits & AI_GUARD) {\n                head = Creature_AIGuard(creature);\n                torso_x = 0;\n                torso_y = 0;\n                creature->maximum_turn = 0;\n\n                if (!(Random_GetControl() & 0xFF)) {\n                    item->goal_anim_state = M_STATE_WAIT_2;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                if (lara->target == item || !info.ahead || item->hit_status) {\n                    item->goal_anim_state = M_STATE_RUN;\n                } else {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                }\n            } else if (info.bite && info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (info.bite && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_PIPE_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_1;\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < 512) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_WALK:\n            creature->maximum_turn = M_WALK_TURN;\n\n            if (info.bite && info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (info.bite && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_PIPE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_1;\n            } else if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() > 512) {\n                    item->goal_anim_state = M_STATE_WALK;\n                } else if (Random_GetControl() > 512) {\n                    item->goal_anim_state = M_STATE_WAIT_2;\n                } else {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                }\n            } else if (info.distance > M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_RUN:\n            creature->flags &= 0xFFF;\n            creature->maximum_turn = M_RUN_TURN;\n            tilt = angle >> 2;\n\n            if (info.bite && info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (\n                Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_PIPE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_1;\n            }\n\n            if (item->ai_bits & AI_GUARD) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (\n                creature->mood == MOOD_ESCAPE && lara->target != item\n                && info.ahead) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_WAIT_1;\n            }\n            break;\n\n        case M_STATE_ATTACK_1:\n            if (info.ahead) {\n                torso_x = info.x_angle;\n                torso_y = info.angle;\n            }\n\n            creature->maximum_turn = 0;\n\n            if (ABS(info.angle) < M_WAIT_TURN) {\n                item->rot.y += info.angle;\n            } else if (info.angle < 0) {\n                item->rot.y -= M_WAIT_TURN;\n            } else {\n                item->rot.y += M_WAIT_TURN;\n            }\n\n            if (Item_GetRelativeFrame(item) == 15) {\n                M_SpawnDart(item);\n                item->goal_anim_state = M_STATE_WAIT_1;\n            }\n            break;\n\n        case M_STATE_ATTACK_3:\n            ITEM *const enemy = creature->enemy;\n            if (enemy == lara_item) {\n                if (!(creature->flags & 0xF000)\n                    && item->touch_bits & M_TOUCH_BITS) {\n                    Lara_TakeDamage(M_BIFF_DAMAGE, true);\n                    creature->flags |= 0x1000;\n                    Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                    Creature_Effect(item, &m_BiffHit, Spawn_Blood);\n                }\n            } else if (!(creature->flags & 0xF000) && enemy != nullptr) {\n                if (Item_IsNearby(enemy, item, M_HIT_RANGE)) {\n                    Item_TakeDamage(enemy, M_BIFF_ENEMY_DAMAGE, true);\n                    creature->flags |= 0x1000;\n                    Sound_Effect(SFX_LARA_THUD, &item->pos, SPM_NORMAL);\n                }\n            }\n            break;\n\n        case M_STATE_AIM_3:\n            if (!info.bite || info.distance > M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_2;\n            } else {\n                item->goal_anim_state = M_STATE_ATTACK_3;\n            }\n            break;\n\n        case M_STATE_WAIT_2:\n            creature->flags &= 0xFFF;\n            creature->maximum_turn = M_WAIT_TURN;\n\n            if (item->ai_bits & AI_GUARD) {\n                head = Creature_AIGuard(creature);\n                torso_x = 0;\n                torso_y = 0;\n                creature->maximum_turn = 0;\n\n                if (!(Random_GetControl() & 0xFF)) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                }\n            } else if (creature->mood == MOOD_ESCAPE) {\n                if (lara->target != item && info.ahead && !item->hit_status) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                } else {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            } else if (info.bite && info.distance < M_CLOSE_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_3;\n            } else if (info.bite && info.distance < M_WALK_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (\n                Creature_CanTargetEnemy(item, &info)\n                && info.distance < M_PIPE_RANGE) {\n                item->goal_anim_state = M_STATE_WAIT_1;\n            } else if (\n                creature->mood == MOOD_BORED && Random_GetControl() < 512) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Joint(item, 0, torso_y);\n    Creature_Joint(item, 1, torso_x);\n    Creature_Joint(item, 2, head - torso_y);\n    Creature_Joint(item, 3, 0);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = 28;\n    obj->radius = 102;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 6)->rot.x = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.x = true;\n}\n\nREGISTER_OBJECT(O_TRIBE_PIPEMAN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/wasp_mutant.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS         (WALL_L / 5)        // = 204\n#define M_HIT_POINTS     24\n#define M_DAMAGE         50\n#define M_TOUCH_BITS     0b00010000'00000000\n#define M_WAIT_TURN      DEG_1               // = 182\n#define M_FLY_TURN       (DEG_1 * 3)         // = 546\n#define M_LAND_SPEED     (STEP_L / 5)        // = 51\n#define M_ATTACK_DIST    SQUARE(WALL_L / 2)  // = 262144\n#define M_TAKEOFF_DIST   SQUARE(WALL_L * 3)  // = 9437184\n#define M_TAKEOFF_CHANCE 0x80\n// clang-format on\n\ntypedef enum {\n    M_STATE_HOVER,\n    M_STATE_LAND,\n    M_STATE_WAIT,\n    M_STATE_TAKEOFF,\n    M_STATE_ATTACK,\n    M_STATE_FALL,\n    M_STATE_DEATH,\n    M_STATE_MOVE,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_WAIT = 2,\n    M_ANIM_FALL = 5,\n} M_ANIM;\n\ntypedef struct {\n    int16_t light;\n} M_PRIV;\n\nstatic const BITE m_Sting = {\n    .pos = {},\n    .mesh_num = 12,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_OPTIONAL(JSON_READ(io, \"light\", &p->light));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"light\", p->light);\n}\n\nstatic void M_TriggerParticles(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if (dx < -0x4000 || dx > 0x4000 || dz < -0x4000 || dz > 0x4000) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.g = (Random_GetControl() & 0x3F) + 32;\n    spark->src_color.b = spark->src_color.g >> 1;\n    spark->src_color.r = spark->src_color.g >> 2;\n    spark->dst_color.g = (Random_GetControl() & 0x1F) + 224;\n    spark->dst_color.b = spark->dst_color.g >> 1;\n    spark->dst_color.r = spark->dst_color.g >> 2;\n    spark->life = 8;\n    spark->s_life = 8;\n    spark->col_fade_speed = 4;\n    spark->fade_to_black = 2;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = (Random_GetControl() & 0xF) - 8;\n    spark->pos.z = (Random_GetControl() & 0x7F) - 64;\n    spark->vel.x = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.y = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.z = (Random_GetControl() & 0x1F) - 16;\n    spark->flags =\n        SPARK_F_ATTACHED_NODE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->friction = 34;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    spark->effect_num = Item_GetIndex(item);\n    spark->node_num = 1;\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->size.width = (Random_GetControl() & 3) + 3;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 1;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 1;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_WAIT, 0);\n    item->current_anim_state = M_STATE_WAIT;\n    item->goal_anim_state = M_STATE_WAIT;\n\n    M_PRIV *const p = item->priv;\n    p->light = Random_GetControl() & 0x7F;\n}\n\nstatic void M_ControlDeath(ITEM *const item)\n{\n    switch (item->current_anim_state) {\n    case M_STATE_FALL:\n        if (item->pos.y > item->floor) {\n            item->pos.y = item->floor;\n            item->fall_speed = 0;\n            item->gravity = false;\n            item->goal_anim_state = M_STATE_DEATH;\n        }\n        item->rot.x = 0;\n        break;\n\n    case M_STATE_DEATH:\n        item->pos.y = item->floor;\n        item->rot.x = 0;\n        break;\n\n    default:\n        Item_SwitchToAnim(item, M_ANIM_FALL, 0);\n        item->current_anim_state = M_STATE_FALL;\n        item->gravity = true;\n        item->speed = 0;\n        item->rot.x = 0;\n        break;\n    }\n}\n\nstatic void M_TriggerLight(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    const int32_t intensity = ABS(Math_Sin(p->light << 10) * 31) >> 14;\n    const RGB_888 color = {\n        .r = 0,\n        .g = intensity << 3,\n        .b = 0,\n    };\n\n    XYZ_32 pos = {};\n    Collide_GetJointAbsPosition(item, &pos, 10);\n    Output_AddDynamicLightRGB(pos, 10, color);\n\n    p->light = (p->light + 1) & 0x3F;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        M_ControlDeath(item);\n        goto finish;\n    }\n\n    AI_INFO info = {};\n    Creature_AIInfo(item, &info);\n\n    Creature_Mood(item, &info, true);\n    angle = Creature_Turn(item, creature->maximum_turn);\n\n    switch (item->current_anim_state) {\n    case M_STATE_HOVER:\n        creature->flags = 0;\n        creature->maximum_turn = M_FLY_TURN;\n\n        if (item->required_anim_state != 0) {\n            item->goal_anim_state = item->required_anim_state;\n        } else if (\n            item->hit_status || Random_GetControl() < M_TAKEOFF_CHANCE * 3\n            || item->ai_bits == AI_MODIFY) {\n            item->goal_anim_state = M_STATE_MOVE;\n        } else if (\n            (creature->mood != MOOD_BORED\n             && Random_GetControl() >= M_TAKEOFF_CHANCE)\n            || item->hit_status || item->ai_bits == AI_MODIFY) {\n            if (info.ahead && info.distance < M_ATTACK_DIST) {\n                item->goal_anim_state = M_STATE_ATTACK;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_LAND;\n        }\n        break;\n\n    case M_STATE_LAND:\n        item->pos.y += M_LAND_SPEED;\n        CLAMPG(item->pos.y, item->floor);\n        break;\n\n    case M_STATE_WAIT:\n        item->pos.y = item->floor;\n        creature->maximum_turn = M_WAIT_TURN;\n\n        if (item->hit_status || info.distance < M_TAKEOFF_DIST\n            || creature->hurt_by_lara || item->ai_bits == AI_MODIFY) {\n            item->goal_anim_state = M_STATE_TAKEOFF;\n        }\n        break;\n\n    case M_STATE_ATTACK:\n        creature->maximum_turn = M_FLY_TURN;\n\n        if (info.ahead && info.distance < M_ATTACK_DIST) {\n            item->goal_anim_state = M_STATE_ATTACK;\n        } else if (info.distance < M_ATTACK_DIST) {\n            item->goal_anim_state = M_STATE_HOVER;\n        } else {\n            item->goal_anim_state = M_STATE_HOVER;\n            item->required_anim_state = M_STATE_MOVE;\n        }\n\n        if (creature->flags == 0 && (item->touch_bits & M_TOUCH_BITS) != 0) {\n            Lara_TakeDamage(M_DAMAGE, true);\n            Creature_Effect(item, &m_Sting, Spawn_Blood);\n            creature->flags = 1;\n        }\n        break;\n\n    case M_STATE_MOVE:\n        creature->flags = 0;\n        creature->maximum_turn = M_FLY_TURN;\n\n        if (item->required_anim_state != 0) {\n            item->goal_anim_state = item->required_anim_state;\n        } else if (\n            (creature->mood != MOOD_BORED\n             && Random_GetControl() >= M_TAKEOFF_CHANCE)\n            || creature->hurt_by_lara || item->ai_bits == AI_MODIFY) {\n            if (info.ahead && info.distance < M_ATTACK_DIST) {\n                item->goal_anim_state = M_STATE_ATTACK;\n            }\n        } else {\n            item->goal_anim_state = M_STATE_HOVER;\n        }\n        break;\n    }\n\nfinish:\n    M_TriggerLight(item);\n    for (int32_t i = 0; i < 2; i++) {\n        M_TriggerParticles(item);\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = Creature_Collision;\n    obj->control_func = M_Control;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->radius = M_RADIUS;\n    obj->hit_points = M_HIT_POINTS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_FLYER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_WASP_MUTANT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/willard.c",
    "content": "#include \"willard_internal.h\"\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/collision.h>\n#include <trx/game/creature.h>\n#include <trx/game/fx.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/pathing/lot.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n\n// clang-format off\n#define M_TURN              (5 * DEG_1)\n#define M_ATTACK_TURN       (2 * DEG_1)\n#define M_TOUCH_BITS        0x900000\n#define M_BITE_DAMAGE       220\n#define M_TOUCH_DAMAGE      10\n#define M_ATTACK_RANGE      SQUARE(WALL_L * 3 / 2)\n#define M_LUNGE_RANGE       SQUARE(WALL_L * 2)\n#define M_FIRE_RANGE        SQUARE(WALL_L * 4)\n#define M_HP_AFTER_KO       200\n#define M_KO_TIME           280\n#define M_WALK_ATTACK_FRAME 30\n#define M_TURN_180_FRAME    51\n#define M_SHOOT_FRAME       40\n#define M_CHARGE_FRAME_MAX  16\n#define M_PLASMA_X          64\n#define M_PLASMA_Y          410\n// clang-format on\n\ntypedef enum {\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_LUNGE,\n    M_STATE_BIG_KILL,\n    M_STATE_STUNNED,\n    M_STATE_KNOCKOUT,\n    M_STATE_GET_UP,\n    M_STATE_WALK_ATTACK_1,\n    M_STATE_WALK_ATTACK_2,\n    M_STATE_TURN_180,\n    M_STATE_SHOOT,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_BIG_KILL = 6,\n    M_ANIM_STUNNED = 7,\n} M_ANIM;\n\ntypedef struct {\n    XYZ_16 pos;\n    RGB_888 sub;\n    RGB_888 color;\n} M_SHIELD_POINT;\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_16 rot;\n} M_AI_POINT;\n\ntypedef struct {\n    bool puzzle_ready;\n    uint8_t ring_count;\n    int16_t explode_count;\n    bool dead;\n    int16_t death_count;\n    int32_t direction;\n    int32_t desired_direction;\n    M_SHIELD_POINT shield[5][8];\n    int32_t closest_ai_path;\n    int32_t lara_ai_path;\n    int32_t lara_junction;\n    int32_t junction_index[4];\n    M_AI_POINT ai_path[16];\n    M_AI_POINT ai_junction[4];\n} M_PRIV;\n\nstatic const BITE m_BiteLeft = {\n    .pos = { .x = 19, .y = -13, .z = 3 },\n    .mesh_num = 20,\n};\nstatic const BITE m_BiteRight = {\n    .pos = { .x = 19, .y = -13, .z = 3 },\n    .mesh_num = 23,\n};\n\nstatic const int32_t m_DDist[5] = { 1600, 5600, 6400, 5600, 1600 };\nstatic const int32_t m_DHeights1[5] = { -7680, -4224, -768, 2688, 6144 };\nstatic const int32_t m_DHeights2[5] = { -1536, -1152, -768, -384, 0 };\nstatic int32_t m_DeathDist[5] = {};\nstatic int32_t m_DeathHeights[5] = {};\n\nstatic void M_ResetPriv(M_PRIV *const p)\n{\n    *p = (M_PRIV) {};\n    p->closest_ai_path = -1;\n    p->lara_ai_path = -1;\n    p->lara_junction = -1;\n    p->direction = 1;\n    p->desired_direction = 1;\n}\n\nstatic void M_LoadShieldPoint(\n    JSON_READ_IO *const io, M_SHIELD_POINT *const point)\n{\n    JSON_SHOULD(JSON_READ(io, \"pos\", &point->pos));\n    JSON_SHOULD(JSON_READ(io, \"sub\", &point->sub));\n    JSON_SHOULD(JSON_READ(io, \"color\", &point->color));\n}\n\nstatic void M_SaveShieldPoint(\n    JSON_WRITE_IO *const io, const M_SHIELD_POINT *const point)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"pos\", point->pos);\n    JSONW_WRITE(io, \"sub\", point->sub);\n    JSONW_WRITE(io, \"color\", point->color);\n    JSONW_POP_AND_APPEND(io);\n}\n\nstatic void M_LoadAIPoint(JSON_READ_IO *const io, M_AI_POINT *const point)\n{\n    JSON_SHOULD(JSON_READ(io, \"pos\", &point->pos));\n    JSON_SHOULD(JSON_READ(io, \"rot\", &point->rot));\n}\n\nstatic void M_SaveAIPoint(\n    JSON_WRITE_IO *const io, const M_AI_POINT *const point)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"pos\", point->pos);\n    JSONW_WRITE(io, \"rot\", point->rot);\n    JSONW_POP_AND_APPEND(io);\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    M_ResetPriv(p);\n    JSON_SHOULD(JSON_READ(io, \"puzzle_ready\", &p->puzzle_ready));\n    JSON_SHOULD(JSON_READ(io, \"ring_count\", &p->ring_count));\n    JSON_SHOULD(JSON_READ(io, \"explode_count\", &p->explode_count));\n    JSON_SHOULD(JSON_READ(io, \"dead\", &p->dead));\n    JSON_SHOULD(JSON_READ(io, \"death_count\", &p->death_count));\n    JSON_SHOULD(JSON_READ(io, \"direction\", &p->direction));\n    JSON_SHOULD(JSON_READ(io, \"desired_direction\", &p->desired_direction));\n    JSON_SHOULD(JSON_READ(io, \"closest_ai_path\", &p->closest_ai_path));\n    JSON_SHOULD(JSON_READ(io, \"lara_ai_path\", &p->lara_ai_path));\n    JSON_SHOULD(JSON_READ(io, \"lara_junction\", &p->lara_junction));\n\n    if (JSON_SHOULD(JSON_PUSH(io, \"shield\"))) {\n        for (int32_t i = 0; i < 5; i++) {\n            if (!JSON_SHOULD(JSON_PUSH_INDEX(io, i))) {\n                continue;\n            }\n            for (int32_t j = 0; j < 8; j++) {\n                if (!JSON_SHOULD(JSON_PUSH_INDEX(io, j))) {\n                    continue;\n                }\n                M_LoadShieldPoint(io, &p->shield[i][j]);\n                JSON_POP(io);\n            }\n            JSON_POP(io);\n        }\n        JSON_POP(io);\n    }\n\n    if (JSON_SHOULD(JSON_PUSH(io, \"junction_index\"))) {\n        for (int32_t i = 0; i < 4; i++) {\n            JSON_SHOULD(JSON_READ_A(io, i, &p->junction_index[i]));\n        }\n        JSON_POP(io);\n    }\n\n    if (JSON_SHOULD(JSON_PUSH(io, \"ai_path\"))) {\n        for (int32_t i = 0; i < 16; i++) {\n            if (!JSON_SHOULD(JSON_PUSH_INDEX(io, i))) {\n                continue;\n            }\n            M_LoadAIPoint(io, &p->ai_path[i]);\n            JSON_POP(io);\n        }\n        JSON_POP(io);\n    }\n\n    if (JSON_SHOULD(JSON_PUSH(io, \"ai_junction\"))) {\n        for (int32_t i = 0; i < 4; i++) {\n            if (!JSON_SHOULD(JSON_PUSH_INDEX(io, i))) {\n                continue;\n            }\n            M_LoadAIPoint(io, &p->ai_junction[i]);\n            JSON_POP(io);\n        }\n        JSON_POP(io);\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"puzzle_ready\", p->puzzle_ready);\n    JSONW_WRITE(io, \"ring_count\", p->ring_count);\n    JSONW_WRITE(io, \"explode_count\", p->explode_count);\n    JSONW_WRITE(io, \"dead\", p->dead);\n    JSONW_WRITE(io, \"death_count\", p->death_count);\n    JSONW_WRITE(io, \"direction\", p->direction);\n    JSONW_WRITE(io, \"desired_direction\", p->desired_direction);\n    JSONW_WRITE(io, \"closest_ai_path\", p->closest_ai_path);\n    JSONW_WRITE(io, \"lara_ai_path\", p->lara_ai_path);\n    JSONW_WRITE(io, \"lara_junction\", p->lara_junction);\n\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < 5; i++) {\n        JSONW_PUSH_ARRAY(io);\n        for (int32_t j = 0; j < 8; j++) {\n            M_SaveShieldPoint(io, &p->shield[i][j]);\n        }\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"shield\");\n\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < 4; i++) {\n        JSONW_PUSH_VALUE(io, p->junction_index[i]);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"junction_index\");\n\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < 16; i++) {\n        M_SaveAIPoint(io, &p->ai_path[i]);\n    }\n    JSONW_POP_AND_SET(io, \"ai_path\");\n\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < 4; i++) {\n        M_SaveAIPoint(io, &p->ai_junction[i]);\n    }\n    JSONW_POP_AND_SET(io, \"ai_junction\");\n}\n\nstatic void M_TriggerPlasma(\n    const int16_t item_num, const int32_t node, int32_t size)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const ITEM *const lara_item = Lara_GetItem();\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 48;\n    spark->src_color.g = 255;\n    spark->src_color.b = (Random_GetControl() & 0x1F) + 48;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 128;\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->friction = 3;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.y = (Random_GetControl() & 7) + 8;\n    spark->vel.z = (Random_GetControl() & 0x1F) - 16;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->gravity = (Random_GetControl() & 7) + 8;\n    spark->node_num = (uint8_t)node;\n    spark->max_y_vel = (Random_GetControl() & 7) + 16;\n    spark->item_num = item_num;\n    spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    size += Random_GetControl() & 0xF;\n    spark->size.width = (uint8_t)size;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 2;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Explode(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n\n    if (p->explode_count == 1 || p->explode_count == 15\n        || p->explode_count == 25 || p->explode_count == 35\n        || p->explode_count == 45 || p->explode_count == 55) {\n        XYZ_32 pos = {\n            .x = item->pos.x + (Random_GetDraw() & 0x3FF) - 512,\n            .y = item->pos.y - (Random_GetDraw() & 0x3FF) - 256,\n            .z = item->pos.z + (Random_GetDraw() & 0x3FF) - 512,\n        };\n\n        FX_RING *const ring =\n            FX_Ring_GetRing(FX_RING_TYPE_BLAST, p->ring_count);\n        if (ring != nullptr) {\n            ring->pos = pos;\n            ring->on = 1;\n            FX_Ring_Sync(ring);\n            p->ring_count++;\n        }\n\n        for (int32_t i = 0; i < 24; i += 3) {\n            pos = (XYZ_32) {};\n            Collide_GetJointAbsPosition(item, &pos, i);\n            Willard_TriggerPlasmaBall(\n                pos, item->room_num, (int16_t)(Random_GetControl() << 1), 4);\n        }\n\n        Sparks_TriggerExplosionSparks(pos, 3, -2, 2, 0);\n        Sound_Effect(SFX_BLAST_CIRCLE, &item->pos, 0x800000 | SPM_PITCH);\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        if (p->explode_count < 128) {\n            m_DeathDist[i] =\n                (m_DDist[i] >> 4) + ((p->explode_count * m_DDist[i]) >> 7);\n            m_DeathHeights[i] = m_DHeights2[i]\n                + ((p->explode_count * (m_DHeights1[i] - m_DHeights2[i])) >> 7);\n        }\n    }\n\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t y = m_DeathHeights[i];\n        const int32_t dist = m_DeathDist[i];\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        int32_t angle = (time4 & 0x3F) << 3;\n\n        for (int32_t j = 0; j < 8; j++) {\n            M_SHIELD_POINT *const shield = &p->shield[i][j];\n            shield->pos.x = (dist * Math_Sin(angle << 4)) >> 13;\n            shield->pos.y = y;\n            shield->pos.z = (dist * Math_Cos(angle << 4)) >> 13;\n            shield->sub = (RGB_888) { 0, 0, 0 };\n\n            if (i != 0 && i != 4 && p->explode_count < 64) {\n                int32_t r = Random_GetDraw() & 0x3F;\n                int32_t g = (Random_GetDraw() & 0x1F) + 224;\n                int32_t b = (g >> 1) + (Random_GetDraw() & 0x3F);\n\n                const int32_t m = 64 - p->explode_count;\n                r = (m * r) >> 6;\n                g = (m * g) >> 6;\n                b = (m * b) >> 6;\n\n                shield->color = (RGB_888) { r, g, b };\n            } else {\n                shield->color = COLOR_RGB_888_BLACK;\n            }\n\n            angle = (angle + 512) & 0xFFF;\n        }\n    }\n}\n\nstatic void M_Die(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Stats_AddKill();\n    item->hit_points = 0;\n    item->collidable = false;\n    Item_Kill(item_num);\n    LOT_DisableBaddieAI(item_num);\n    item->flags |= IF_INVISIBLE;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    M_ResetPriv(p);\n    item->include_in_kill_stats = false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    CREATURE *const creature = item->creature_data;\n    const bool lara_was_alive = lara_item->hit_points > 0;\n    XYZ_32 pos;\n\n    if (p->closest_ai_path == -1) {\n        int32_t n_junction = 0;\n        int32_t n_path = 0;\n\n        for (int32_t i = Room_Get(item->room_num)->item_num; i != NO_ITEM;\n             i = Item_Get(i)->next_item) {\n            const ITEM *const ai = Item_Get(i);\n\n            if (ai->object_id == O_AI_X1 && n_path < 16) {\n                p->ai_path[n_path].pos = ai->pos;\n                p->ai_path[n_path].rot = ai->rot;\n                n_path++;\n            } else if (ai->object_id == O_AI_X2 && n_junction < 4) {\n                p->ai_junction[n_junction].pos = ai->pos;\n                p->ai_junction[n_junction].rot = ai->rot;\n                n_junction++;\n            }\n        }\n\n        p->closest_ai_path = -1;\n        int32_t best_dist = INT32_MAX;\n        for (int32_t i = 0; i < 16; i++) {\n            const int32_t x = (p->ai_path[i].pos.x - item->pos.x) >> 6;\n            const int32_t z = (p->ai_path[i].pos.z - item->pos.z) >> 6;\n            const int32_t dist = SQUARE(x) + SQUARE(z);\n\n            if (dist < best_dist) {\n                p->closest_ai_path = i;\n                best_dist = dist;\n            }\n        }\n\n        p->lara_ai_path = -1;\n        best_dist = INT32_MAX;\n        for (int32_t i = 0; i < 16; i++) {\n            const int32_t x = (p->ai_path[i].pos.x - lara_item->pos.x) >> 6;\n            const int32_t z = (p->ai_path[i].pos.z - lara_item->pos.z) >> 6;\n            const int32_t dist = SQUARE(x) + SQUARE(z);\n\n            if (dist < best_dist) {\n                p->lara_ai_path = i;\n                best_dist = dist;\n            }\n        }\n\n        for (int32_t j = 0; j < 4; j++) {\n            int32_t index = -1;\n            best_dist = INT32_MAX;\n            for (int32_t i = 0; i < 16; i++) {\n                const int32_t x =\n                    ABS((p->ai_path[i].pos.x - p->ai_junction[j].pos.x) >> 6);\n                const int32_t z =\n                    ABS((p->ai_path[i].pos.z - p->ai_junction[j].pos.z) >> 6);\n                const int32_t dist = x + (z >> 1);\n\n                if (dist < best_dist) {\n                    index = i;\n                    best_dist = dist;\n                }\n            }\n\n            p->junction_index[j] = index;\n        }\n    }\n\n    int32_t best_dist = INT32_MAX;\n    int32_t j = p->closest_ai_path;\n    for (int32_t i = j - 1; i < j + 2; i++) {\n        int32_t n_path;\n        if (i < 0) {\n            n_path = i + 16;\n        } else if (i > 15) {\n            n_path = i - 16;\n        } else {\n            n_path = i;\n        }\n\n        const int32_t x = (p->ai_path[n_path].pos.x - item->pos.x) >> 6;\n        const int32_t z = (p->ai_path[n_path].pos.z - item->pos.z) >> 6;\n        const int32_t dist = SQUARE(x) + SQUARE(z);\n\n        if (dist < best_dist) {\n            p->closest_ai_path = n_path;\n            best_dist = dist;\n        }\n    }\n\n    j = p->lara_ai_path;\n    best_dist = INT32_MAX;\n    for (int32_t i = j - 1; i < j + 2; i++) {\n        int32_t n_path;\n        if (i < 0) {\n            n_path = i + 16;\n        } else if (i > 15) {\n            n_path = i - 16;\n        } else {\n            n_path = i;\n        }\n\n        const int32_t x = (p->ai_path[n_path].pos.x - lara_item->pos.x) >> 6;\n        const int32_t z = (p->ai_path[n_path].pos.z - lara_item->pos.z) >> 6;\n        const int32_t dist = SQUARE(x) + SQUARE(z);\n\n        if (dist < best_dist) {\n            p->lara_ai_path = n_path;\n            best_dist = dist;\n        }\n    }\n\n    int32_t best_dist2 = INT32_MAX;\n    for (int32_t i = 0; i < 4; i++) {\n        const int32_t x = (p->ai_junction[i].pos.x - lara_item->pos.x) >> 6;\n        const int32_t z = (p->ai_junction[i].pos.z - lara_item->pos.z) >> 6;\n        const int32_t dist = SQUARE(x) + SQUARE(z);\n\n        if (dist < best_dist2) {\n            p->lara_junction = i;\n            best_dist2 = dist;\n        }\n    }\n\n    const bool fire =\n        best_dist2 < best_dist || item->pos.y > lara_item->pos.y + 2048;\n    const int32_t x = p->ai_junction[p->lara_junction].pos.x - item->pos.x;\n    const int32_t z = p->ai_junction[p->lara_junction].pos.z - item->pos.z;\n    const int32_t dist = SQUARE(x) + SQUARE(z);\n\n    if (item->hit_points <= 0) {\n        const bool puzzle_complete = Inv_RequestItem(O_QUEST_ITEM_1) > 0\n            && Inv_RequestItem(O_QUEST_ITEM_2) > 0\n            && Inv_RequestItem(O_QUEST_ITEM_3) > 0\n            && Inv_RequestItem(O_QUEST_ITEM_4) > 0;\n\n        if (puzzle_complete && p->puzzle_ready) {\n            if (item->current_anim_state != M_STATE_STUNNED) {\n                Item_SwitchToAnim(item, M_ANIM_STUNNED, 0);\n                item->current_anim_state = M_STATE_STUNNED;\n            } else if (\n                Item_GetRelativeFrame(item) >= Item_GetAnim(item)->frame_end\n                    - Item_GetAnim(item)->frame_base - 2) {\n                item->mesh_bits = 0;\n                Item_SwitchToAnim(item, Item_GetRelativeAnim(item), -2);\n\n                if (!p->explode_count) {\n                    p->ring_count = 0;\n\n                    for (int32_t i = 0; i < 6; i++) {\n                        FX_RING *const ring =\n                            FX_Ring_GetRing(FX_RING_TYPE_BLAST, i);\n                        if (ring == nullptr) {\n                            continue;\n                        }\n                        ring->on = 0;\n                        ring->life = 32;\n                        ring->radius = 512;\n                        ring->speed = (i + 4) << 5;\n                        ring->rot.x =\n                            ((Random_GetControl() & 0x1FF) - 256) & 0xFFF;\n                        ring->rot.z =\n                            ((Random_GetControl() & 0x1FF) - 256) & 0xFFF;\n                        FX_Ring_Sync(ring);\n                    }\n                }\n\n                if (p->explode_count < 256) {\n                    p->explode_count++;\n                }\n\n                if (p->explode_count <= 128 || p->ring_count != 6\n                    || FX_Ring_GetRing(FX_RING_TYPE_BLAST, 5)->life != 0) {\n                    M_Explode(item);\n                } else {\n                    M_Die(item_num);\n                    p->dead = true;\n                }\n\n                return;\n            }\n        } else {\n            creature->maximum_turn = 0;\n\n            switch (item->current_anim_state) {\n            case M_STATE_STOP:\n                item->goal_anim_state = M_STATE_STUNNED;\n                break;\n\n            case M_STATE_STUNNED:\n                p->death_count = M_KO_TIME;\n                break;\n\n            case M_STATE_KNOCKOUT:\n                p->death_count--;\n\n                if (p->death_count < 0) {\n                    item->goal_anim_state = M_STATE_GET_UP;\n                }\n                break;\n\n            case M_STATE_GET_UP:\n                item->hit_points = M_HP_AFTER_KO;\n\n                if (puzzle_complete) {\n                    p->puzzle_ready = true;\n                }\n\n                creature->maximum_turn = M_ATTACK_TURN;\n                break;\n\n            default:\n                item->goal_anim_state = M_STATE_STOP;\n                break;\n            }\n        }\n    } else {\n        AI_INFO info = {};\n        Creature_AIInfo(item, &info);\n\n        if (item->touch_bits) {\n            Lara_TakeDamage(M_TOUCH_DAMAGE, false);\n        }\n\n        const int32_t index = p->lara_ai_path - p->closest_ai_path;\n        if (p->direction == -1 && ((index < 0 && index > -6) || index > 10)) {\n            p->desired_direction = 1;\n        } else if (\n            p->direction == 1 && ((index > 0 && index < 6) || index < -10)) {\n            p->desired_direction = -1;\n        }\n\n        creature->target = XYZ_32_OffsetYaw(\n            p->ai_path[p->closest_ai_path].pos,\n            p->ai_path[p->closest_ai_path].rot.y, WALL_L * p->direction);\n\n        switch (item->current_anim_state) {\n        case M_STATE_STOP:\n            creature->maximum_turn = 0;\n            creature->flags = 0;\n\n            if (p->direction != p->desired_direction) {\n                item->goal_anim_state = M_STATE_TURN_180;\n            } else if (\n                fire && info.ahead && dist < M_FIRE_RANGE\n                && lara_item->hit_points > 0) {\n                item->goal_anim_state = M_STATE_SHOOT;\n            } else if (!info.bite || info.distance >= M_LUNGE_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_LUNGE;\n            }\n            break;\n\n        case M_STATE_WALK:\n            creature->maximum_turn = M_TURN;\n            creature->flags = 0;\n\n            if (p->direction != p->desired_direction) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (fire && info.ahead && dist < M_FIRE_RANGE) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.bite && info.distance < M_ATTACK_RANGE) {\n                if ((Random_GetControl() & 3) == 1) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (\n                    item->frame_num\n                    >= Item_GetAnim(item)->frame_base + M_WALK_ATTACK_FRAME) {\n                    item->goal_anim_state = M_STATE_WALK_ATTACK_1;\n                } else {\n                    item->goal_anim_state = M_STATE_WALK_ATTACK_2;\n                }\n            }\n            break;\n\n        case M_STATE_LUNGE:\n            creature->target.x = lara_item->pos.x;\n            creature->target.z = lara_item->pos.z;\n            creature->maximum_turn = M_ATTACK_TURN;\n\n            if (!creature->flags && item->touch_bits & M_TOUCH_BITS) {\n                Lara_TakeDamage(2 * M_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_BiteLeft, Spawn_Blood);\n                Creature_Effect(item, &m_BiteRight, Spawn_Blood);\n                creature->flags = 1;\n            }\n            break;\n\n        case M_STATE_BIG_KILL:\n            switch (Item_GetRelativeFrame(item)) {\n            case 0:\n            case 43:\n            case 95:\n            case 105:\n                Creature_Effect(item, &m_BiteLeft, Spawn_Blood);\n                break;\n\n            case 61:\n            case 91:\n            case 101:\n                Creature_Effect(item, &m_BiteRight, Spawn_Blood);\n                break;\n            }\n            break;\n\n        case M_STATE_WALK_ATTACK_1:\n        case M_STATE_WALK_ATTACK_2:\n            if (!creature->flags && (item->touch_bits & M_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(M_BITE_DAMAGE, true);\n                Creature_Effect(item, &m_BiteLeft, Spawn_Blood);\n                Creature_Effect(item, &m_BiteRight, Spawn_Blood);\n                creature->flags = 1;\n            }\n\n            if (fire && info.bite && dist < M_FIRE_RANGE) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (info.bite && info.distance < M_ATTACK_RANGE) {\n                if (item->current_anim_state == M_STATE_WALK_ATTACK_1) {\n                    item->goal_anim_state = M_STATE_WALK_ATTACK_2;\n                } else {\n                    item->goal_anim_state = M_STATE_WALK_ATTACK_1;\n                }\n            } else {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n            break;\n\n        case M_STATE_TURN_180:\n            creature->maximum_turn = 0;\n            creature->flags = 0;\n\n            if (Item_GetRelativeFrame(item) == M_TURN_180_FRAME) {\n                item->rot.y += DEG_180;\n                p->direction = -p->direction;\n            }\n            break;\n\n        case M_STATE_SHOOT:\n            creature->target.x = lara_item->pos.x;\n            creature->target.z = lara_item->pos.z;\n            creature->maximum_turn = M_ATTACK_TURN;\n\n            if (Item_GetRelativeFrame(item) == M_SHOOT_FRAME\n                && lara_item->hit_points > 0) {\n                pos.x = -M_PLASMA_X;\n                pos.y = M_PLASMA_Y;\n                pos.z = 0;\n                Collide_GetJointAbsPosition(item, &pos, 20);\n                Willard_TriggerPlasmaBall(\n                    pos, item->room_num, item->rot.y - 4096, 0);\n\n                pos.x = M_PLASMA_X;\n                pos.y = M_PLASMA_Y;\n                pos.z = 0;\n                Collide_GetJointAbsPosition(item, &pos, 23);\n                Willard_TriggerPlasmaBall(\n                    pos, item->room_num, item->rot.y + 4096, 0);\n            }\n\n            int32_t f = Item_GetRelativeFrame(item);\n            if (f > M_CHARGE_FRAME_MAX) {\n                f = Item_GetAnim(item)->frame_end - item->frame_num;\n                CLAMPG(f, M_CHARGE_FRAME_MAX);\n            }\n\n            pos.x = 0;\n            pos.y = 0;\n            pos.z = 0;\n            Collide_GetJointAbsPosition(item, &pos, 17);\n\n            const int32_t color_base = Random_GetControl();\n            const int32_t r = (f * (color_base & 0x3F)) >> 4;\n            const int32_t g = (f * (255 - ((color_base >> 4) & 0x1F))) >> 4;\n            const int32_t b = (f * (192 - ((color_base >> 6) & 0x1F))) >> 4;\n\n            Output_AddDynamicLightRGB(pos, 12, (RGB_888) { r, g, b });\n            M_TriggerPlasma(item_num, 7, f << 2);\n            M_TriggerPlasma(item_num, 8, f << 2);\n            break;\n        }\n\n        if (lara_was_alive && lara_item->hit_points <= 0) {\n            Creature_SpecialKill(\n                item, M_ANIM_BIG_KILL, M_STATE_BIG_KILL, LS_EXTRA_WILLARD_KILL);\n            creature->maximum_turn = 0;\n            return;\n        }\n    }\n\n    const int16_t angle = Creature_Turn(item, creature->maximum_turn);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic bool M_CanBeExploded(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_DrawShield(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    const int32_t sprite_base = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    for (int32_t band = 0; band < 4; band++) {\n        const int32_t sprite_idx =\n            sprite_base + 18 + ((band + (time4 >> 3)) & 7);\n\n        for (int32_t j = 0; j < 8; j++) {\n            const int32_t j2 = (j == 7) ? 0 : (j + 1);\n            const M_SHIELD_POINT *const s00 = &p->shield[band][j];\n            const M_SHIELD_POINT *const s01 = &p->shield[band][j2];\n            const M_SHIELD_POINT *const s10 = &p->shield[band + 1][j];\n            const M_SHIELD_POINT *const s11 = &p->shield[band + 1][j2];\n\n            const int32_t idx00 = band * 8 + j;\n            const int32_t idx01 = band * 8 + j2;\n            const int32_t idx10 = (band + 1) * 8 + j;\n            const int32_t idx11 = (band + 1) * 8 + j2;\n\n            RGB_888 c00 = s00->color;\n            RGB_888 c01 = s01->color;\n            RGB_888 c10 = s10->color;\n            RGB_888 c11 = s11->color;\n\n            if (idx00 >= 8 && idx00 <= 31) {\n                c00.r = (uint8_t)MAX(0, (int32_t)c00.r - (int32_t)s00->sub.r);\n                c00.g = (uint8_t)MAX(0, (int32_t)c00.g - (int32_t)s00->sub.g);\n                c00.b = (uint8_t)MAX(0, (int32_t)c00.b - (int32_t)s00->sub.b);\n            }\n            if (idx01 >= 8 && idx01 <= 31) {\n                c01.r = (uint8_t)MAX(0, (int32_t)c01.r - (int32_t)s01->sub.r);\n                c01.g = (uint8_t)MAX(0, (int32_t)c01.g - (int32_t)s01->sub.g);\n                c01.b = (uint8_t)MAX(0, (int32_t)c01.b - (int32_t)s01->sub.b);\n            }\n            if (idx10 >= 8 && idx10 <= 31) {\n                c10.r = (uint8_t)MAX(0, (int32_t)c10.r - (int32_t)s10->sub.r);\n                c10.g = (uint8_t)MAX(0, (int32_t)c10.g - (int32_t)s10->sub.g);\n                c10.b = (uint8_t)MAX(0, (int32_t)c10.b - (int32_t)s10->sub.b);\n            }\n            if (idx11 >= 8 && idx11 <= 31) {\n                c11.r = (uint8_t)MAX(0, (int32_t)c11.r - (int32_t)s11->sub.r);\n                c11.g = (uint8_t)MAX(0, (int32_t)c11.g - (int32_t)s11->sub.g);\n                c11.b = (uint8_t)MAX(0, (int32_t)c11.b - (int32_t)s11->sub.b);\n            }\n\n            if (((c00.r | c00.g | c00.b | c01.r | c01.g | c01.b | c11.r | c11.g\n                  | c11.b | c10.r | c10.g | c10.b)\n                 == 0U)) {\n                continue;\n            }\n\n            const XYZ_32 world_pos[4] = {\n                {\n                    item->pos.x + s00->pos.x,\n                    item->pos.y + s00->pos.y,\n                    item->pos.z + s00->pos.z,\n                },\n                {\n                    item->pos.x + s01->pos.x,\n                    item->pos.y + s01->pos.y,\n                    item->pos.z + s01->pos.z,\n                },\n                {\n                    item->pos.x + s11->pos.x,\n                    item->pos.y + s11->pos.y,\n                    item->pos.z + s11->pos.z,\n                },\n                {\n                    item->pos.x + s10->pos.x,\n                    item->pos.y + s10->pos.y,\n                    item->pos.z + s10->pos.z,\n                },\n            };\n            const RGBA_8888 color[4] = {\n                { c00.r, c00.g, c00.b, 255 },\n                { c01.r, c01.g, c01.b, 255 },\n                { c11.r, c11.g, c11.b, 255 },\n                { c10.r, c10.g, c10.b, 255 },\n            };\n            OutputSource_PolyFX_StageSpriteQuadWorld(\n                sprite_idx, world_pos, color, DRAW_BLEND_ADD);\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    const bool result = Object_DrawAnimatingItem(item);\n    if (p->explode_count != 0) {\n        FX_Ring_Draw();\n\n        if (p->explode_count <= 64) {\n            M_DrawShield(item);\n        }\n    }\n    return result;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->draw_func = M_Draw;\n    obj->can_be_exploded_func = M_CanBeExploded;\n\n    obj->shadow_size = 128;\n    obj->hit_points = 200;\n    obj->pivot_length = 50;\n    obj->radius = 102;\n    obj->intelligent = true;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_WILLARD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/willard_internal.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n\nvoid Willard_TriggerPlasmaBall(\n    XYZ_32 pos, int16_t room_num, int16_t angle, int16_t type);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/willard_plasma_ball.c",
    "content": "#include \"willard_internal.h\"\n\n#include <trx/core/math/func.h>\n#include <trx/game/collision.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n\nstatic const uint8_t m_Falloffs[5] = { 13, 7, 7, 7, 7 };\n\nstatic void M_TriggerPlasmaBallFlame(\n    const int16_t effect_num, const int32_t type, const XYZ_32 vel)\n{\n    const EFFECT *const effect = Effect_Get(effect_num);\n    const ITEM *const lara_item = Lara_GetItem();\n\n    const int32_t dx = lara_item->pos.x - effect->pos.x;\n    const int32_t dz = lara_item->pos.z - effect->pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 48;\n    spark->src_color.g = 255;\n    spark->src_color.b = (Random_GetControl() & 0x1F) + 48;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 128;\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->life = (Random_GetControl() & 7) + 24;\n    spark->s_life = spark->life;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->friction = 85;\n    spark->pos.x = (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = 2 * (vel.x + (Random_GetControl() & 0xFF)) - 256;\n    spark->vel.y = (Random_GetControl() & 0x1FF) - 256;\n    spark->vel.z = 2 * (vel.z + (Random_GetControl() & 0xFF)) - 256;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->effect_num = effect_num;\n    spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n\n    if (type < 0) {\n        if (type >= -2) {\n            spark->scalar = 2;\n        } else {\n            spark->scalar = 4;\n        }\n\n        spark->size.width = (Random_GetControl() & 0xF) + 16;\n        spark->friction = 5;\n        spark->vel.x = vel.x + (Random_GetControl() & 0xFF) - 128;\n        spark->vel.y = vel.y;\n        spark->vel.z = vel.z + (Random_GetControl() & 0xFF) - 128;\n    } else {\n        spark->scalar = 3;\n        spark->size.width = (uint8_t)type;\n    }\n\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 3;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 3;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Control(const int16_t effect_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    EFFECT *const effect = Effect_Get(effect_num);\n    const XYZ_32 old_pos = effect->pos;\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if (effect->flag1 != 0) {\n        effect->fall_speed += (effect->flag1 != 1) + 1;\n\n        if ((time4 & 0xC) == 0) {\n            if (effect->speed != 0) {\n                effect->speed--;\n            }\n\n            M_TriggerPlasmaBallFlame(\n                effect_num, -1 - effect->flag1,\n                (XYZ_32) {\n                    .x = 0,\n                    .y = -(Random_GetControl() & 0x1F),\n                    .z = 0,\n                });\n        }\n    } else {\n        int16_t angles[2];\n        Math_GetVectorAngles(\n            lara_item->pos.x - old_pos.x, lara_item->pos.y - old_pos.y - 256,\n            lara_item->pos.z - old_pos.z, angles);\n        effect->rot.x = angles[1];\n        effect->rot.y = angles[0];\n\n        if (effect->speed < 512) {\n            effect->speed += (effect->speed >> 4) + 4;\n        }\n\n        if ((time4 & 4) != 0) {\n            M_TriggerPlasmaBallFlame(\n                effect_num, effect->speed >> 1, (XYZ_32) {});\n        }\n    }\n\n    const int32_t speed =\n        (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n    effect->pos = XYZ_32_OffsetYaw(effect->pos, effect->rot.y, speed);\n    effect->pos.y += effect->fall_speed\n        - ((effect->speed * Math_Sin(effect->rot.x)) >> W2V_SHIFT);\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n    const int32_t ceiling = Room_GetCeiling(sector, effect->pos);\n\n    if (effect->pos.y >= height || effect->pos.y < ceiling) {\n        if (effect->flag1 == 0) {\n            const int32_t count = (Random_GetControl() & 3) + 2;\n            for (int32_t i = 0; i < count; i++) {\n                Willard_TriggerPlasmaBall(\n                    old_pos, effect->room_num,\n                    effect->rot.y + (Random_GetControl() & 0x3FFF) + 0x6000, 1);\n            }\n        }\n\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->flag1 == 0 && Lara_IsNearItem(&effect->pos, 200)) {\n        for (int32_t i = 14; i >= 0; i -= 2) {\n            XYZ_32 pos = { 0, 0, 0 };\n            Collide_GetJointAbsPosition(lara_item, &pos, i);\n            Willard_TriggerPlasmaBall(\n                pos, effect->room_num, Random_GetControl() << 1, 1);\n        }\n\n        Lara_CatchFireEx(FLAME_GREEN);\n        Lara_TakeDamage(lara_item->hit_points, false);\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, lara_item->room_num);\n    }\n\n    const int32_t falloff = m_Falloffs[effect->flag1];\n    if (falloff != 0) {\n        const int32_t color_base = Random_GetControl();\n        Output_AddDynamicLightRGB(\n            effect->pos, falloff,\n            (RGB_888) {\n                .r = color_base & 0x3F,\n                .g = 255 - ((color_base >> 4) & 0x1F),\n                .b = 192 - ((color_base >> 6) & 0x1F),\n            });\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n}\n\nvoid Willard_TriggerPlasmaBall(\n    const XYZ_32 pos, const int16_t room_num, const int16_t angle,\n    const int16_t type)\n{\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_ITEM) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = pos;\n    effect->rot.x = 0;\n    effect->rot.y = angle;\n    effect->object_id = O_WILLARD_PLASMA_BALL;\n    effect->speed = type != -16 ? (Random_GetControl() & 0x1F) + 16 : 0;\n    effect->fall_speed = -16 * type;\n    effect->flag1 = type;\n}\n\nREGISTER_OBJECT(O_WILLARD_PLASMA_BALL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/winston.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS     (WALL_L / 10) // = 102\n#define M_STOP_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    WINSTON_STATE_EMPTY = 0,\n    WINSTON_STATE_STOP  = 1,\n    WINSTON_STATE_WALK  = 2,\n    // clang-format on\n} M_STATE;\n\nstatic bool M_IsAlive(const ITEM *const item)\n{\n    return item->hit_points > 0;\n}\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic bool M_CanTakeDamage(const ITEM *const item)\n{\n    return false;\n}\n\nstatic bool M_CanBeProjectileTarget(const ITEM *const item)\n{\n    return false;\n}\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    Creature_Mood(item, &info, true);\n\n    const int16_t angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (item->current_anim_state == WINSTON_STATE_STOP) {\n        if (item->goal_anim_state != WINSTON_STATE_WALK\n            && (info.distance > M_STOP_RANGE || !info.ahead)) {\n            item->goal_anim_state = WINSTON_STATE_WALK;\n            Sound_Effect(SFX_WINSTON_GRUNT_2, &item->pos, SPM_NORMAL);\n        }\n    } else if (info.distance < M_STOP_RANGE) {\n        if (info.ahead) {\n            item->goal_anim_state = WINSTON_STATE_STOP;\n            if ((creature->flags & 1) != 0) {\n                creature->flags--;\n            }\n        } else if ((creature->flags & 1) == 0) {\n            Sound_Effect(SFX_WINSTON_GRUNT_1, &item->pos, SPM_NORMAL);\n            Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL);\n            creature->flags |= 1;\n        }\n    }\n\n    if (item->touch_bits != 0 && (creature->flags & 2) == 0) {\n        Sound_Effect(SFX_WINSTON_GRUNT_3, &item->pos, SPM_NORMAL);\n        Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL);\n        creature->flags |= 2;\n    } else if (item->touch_bits == 0 && (creature->flags & 2) != 0) {\n        creature->flags -= 2;\n    }\n\n    if (Random_GetDraw() < 0x100) {\n        Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL);\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n    obj->is_alive_func = M_IsAlive;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->can_take_damage_func = M_CanTakeDamage;\n    obj->can_be_projectile_target_func = M_CanBeProjectileTarget;\n\n    obj->hit_points = 1;\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 4;\n    obj->smartness = -1;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_WINSTON, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/winston_army.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS     (WALL_L / 10) // = 102\n#define M_STOP_RANGE SQUARE(WALL_L * 3 / 2) // = 2359296\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    M_STATE_EMPTY     = 0,\n    M_STATE_STOP      = 1,\n    M_STATE_WALK      = 2,\n    M_STATE_DEF_1     = 3,\n    M_STATE_DEF_2     = 4,\n    M_STATE_DEF_3     = 5,\n    M_STATE_HIT_1     = 6,\n    M_STATE_HIT_2     = 7,\n    M_STATE_HIT_3     = 8,\n    M_STATE_HIT_DOWN  = 9,\n    M_STATE_FALL_DOWN = 10,\n    M_STATE_GET_UP    = 11,\n    M_STATE_BRUSH_OFF = 12,\n    M_STATE_ON_FLOOR  = 13,\n    // clang-format on\n} M_STATE;\n\ntypedef struct {\n    int16_t knockdown_timer;\n    bool spawn_checked;\n} M_PRIV;\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return false;\n}\n\nstatic bool M_CanBeExploded(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"knockdown_timer\", &p->knockdown_timer));\n    JSON_SHOULD(JSON_READ(io, \"spawn_checked\", &p->spawn_checked));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"knockdown_timer\", p->knockdown_timer);\n    JSONW_WRITE(io, \"spawn_checked\", p->spawn_checked);\n}\n\nstatic bool M_RemoveNormalWinston(void)\n{\n    const int32_t item_count = Item_GetTotalCount();\n    for (int32_t item_num = 0; item_num < item_count; item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (item->object_id != O_WINSTON || (item->flags & IF_KILLED) != 0) {\n            continue;\n        }\n        item->status = IS_INVISIBLE;\n        Item_Kill(item_num);\n        return true;\n    }\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    M_PRIV *const p = item->priv;\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    Creature_Mood(item, &info, true);\n\n    const int16_t angle = Creature_Turn(item, creature->maximum_turn);\n\n    if (!p->spawn_checked) {\n        M_RemoveNormalWinston();\n        p->spawn_checked = true;\n    }\n\n    if (item->hit_points <= 0) {\n        creature->maximum_turn = 0;\n\n        switch (item->current_anim_state) {\n        case M_STATE_HIT_DOWN:\n        case M_STATE_FALL_DOWN:\n            if (item->hit_status) {\n                item->goal_anim_state = M_STATE_HIT_DOWN;\n            } else {\n                p->knockdown_timer--;\n                if (p->knockdown_timer < 0) {\n                    item->goal_anim_state = M_STATE_ON_FLOOR;\n                }\n            }\n\n            break;\n\n        case M_STATE_GET_UP:\n            item->hit_points = 16;\n            if (Random_GetControl() & 1) {\n                creature->flags = 999;\n            }\n            break;\n\n        case M_STATE_ON_FLOOR:\n            if (item->hit_status) {\n                item->goal_anim_state = M_STATE_HIT_DOWN;\n            } else {\n                p->knockdown_timer--;\n                if (p->knockdown_timer < 0) {\n                    item->goal_anim_state = M_STATE_GET_UP;\n                }\n            }\n\n            break;\n\n        default:\n            Item_SwitchToObjAnim(item, 16, 0, O_WINSTON_ARMY);\n            item->current_anim_state = M_STATE_FALL_DOWN;\n            item->goal_anim_state = M_STATE_FALL_DOWN;\n            p->knockdown_timer = 150;\n            break;\n        }\n    } else {\n        switch (item->current_anim_state) {\n        case M_STATE_STOP:\n            creature->maximum_turn = DEG_1 * 2;\n\n            if (creature->flags == 999) {\n                item->goal_anim_state = M_STATE_BRUSH_OFF;\n            } else if (lara->target == item) {\n                item->goal_anim_state = M_STATE_DEF_1;\n            } else if (\n                (info.distance > M_STOP_RANGE || !info.ahead)\n                && item->goal_anim_state != M_STATE_WALK) {\n                item->goal_anim_state = M_STATE_WALK;\n                Sound_Effect(SFX_WINSTON_GRUNT_2, &item->pos, SPM_NORMAL);\n            }\n            break;\n\n        case M_STATE_WALK:\n            creature->maximum_turn = DEG_1 * 2;\n            if (lara->target == item) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.distance < M_STOP_RANGE) {\n                if (info.ahead) {\n                    item->goal_anim_state = M_STATE_STOP;\n                    if (creature->flags & 1) {\n                        creature->flags--;\n                    }\n                } else if ((creature->flags & 1) == 0) {\n                    Sound_Effect(SFX_WINSTON_GRUNT_1, &item->pos, SPM_NORMAL);\n                    Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL);\n                    creature->flags |= 1;\n                }\n            }\n\n            break;\n\n        case M_STATE_DEF_1:\n            creature->maximum_turn = DEG_1 * 2;\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            }\n            if (item->hit_status) {\n                item->goal_anim_state = M_STATE_HIT_1;\n            } else if (lara->target != item) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_DEF_2:\n            creature->maximum_turn = DEG_1 * 2;\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            }\n            if (item->hit_status) {\n                item->goal_anim_state = M_STATE_HIT_2;\n            } else if (lara->target != item) {\n                item->goal_anim_state = M_STATE_DEF_1;\n            }\n            break;\n\n        case M_STATE_DEF_3:\n            creature->maximum_turn = DEG_1 * 2;\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            }\n            if (item->hit_status) {\n                item->goal_anim_state = M_STATE_HIT_3;\n            } else if (lara->target != item) {\n                item->goal_anim_state = M_STATE_DEF_1;\n            }\n            break;\n\n        case M_STATE_HIT_1:\n            if (Random_GetControl() & 1) {\n                item->required_anim_state = M_STATE_DEF_3;\n            } else {\n                item->required_anim_state = M_STATE_DEF_2;\n            }\n            break;\n\n        case M_STATE_HIT_2:\n        case M_STATE_HIT_3:\n            item->required_anim_state = M_STATE_DEF_1;\n            break;\n\n        case M_STATE_BRUSH_OFF:\n            creature->maximum_turn = 0;\n            creature->flags = 0;\n            break;\n        }\n    }\n\n    if (Random_GetControl() < 0x100) {\n        Sound_Effect(SFX_WINSTON_CUPS, &item->pos, SPM_NORMAL);\n    }\n\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n    obj->can_be_exploded_func = M_CanBeExploded;\n\n    obj->hit_points = 20;\n    obj->shadow_size = UNIT_SHADOW / 4;\n    obj->radius = M_RADIUS;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_WINSTON_ARMY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/wolf.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define WOLF_SLEEP_FRAME   96\n#define WOLF_BITE_DAMAGE   100\n#define WOLF_POUNCE_DAMAGE 50\n#define WOLF_WALK_TURN     (2 * DEG_1) // = 364\n#define WOLF_RUN_TURN      (5 * DEG_1) // = 910\n#define WOLF_STALK_TURN    (2 * DEG_1) // = 364\n#define WOLF_ATTACK_RANGE  SQUARE(WALL_L * 3 / 2) // = 2359296\n#define WOLF_STALK_RANGE   SQUARE(WALL_L * 3) // = 9437184\n#define WOLF_BITE_RANGE    SQUARE(345) // = 119025\n#define WOLF_WAKE_CHANCE   32\n#define WOLF_SLEEP_CHANCE  32\n#define WOLF_HOWL_CHANCE   384\n#define WOLF_TOUCH         0x774F\n#define WOLF_HITPOINTS     (g_TRVersion == 1 ? 6 : 10)\n#define WOLF_RADIUS        (WALL_L / 3) // = 341\n#define WOLF_SMARTNESS     0x2000\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    WOLF_STATE_EMPTY     = 0,\n    WOLF_STATE_STOP      = 1,\n    WOLF_STATE_WALK      = 2,\n    WOLF_STATE_RUN       = 3,\n    WOLF_STATE_JUMP      = 4,\n    WOLF_STATE_STALK     = 5,\n    WOLF_STATE_ATTACK    = 6,\n    WOLF_STATE_HOWL      = 7,\n    WOLF_STATE_SLEEP     = 8,\n    WOLF_STATE_CROUCH    = 9,\n    WOLF_STATE_FAST_TURN = 10,\n    WOLF_STATE_DEATH     = 11,\n    WOLF_STATE_BITE      = 12,\n    // clang-format on\n} WOLF_STATE;\n\ntypedef enum {\n    WOLF_ANIM_DEATH = 20,\n} WOLF_ANIM;\n\nstatic BITE m_WolfJawBite = { .pos = { 0, -14, 174 }, .mesh_num = 6 };\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Item_Get(item_num)->frame_num = WOLF_SLEEP_FRAME;\n    Creature_Initialise(item_num);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const wolf = item->creature_data;\n    int16_t head = 0;\n    int16_t angle = 0;\n    int16_t tilt = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != WOLF_STATE_DEATH) {\n            item->current_anim_state = WOLF_STATE_DEATH;\n            Item_SwitchToAnim(\n                item, WOLF_ANIM_DEATH + (int16_t)(Random_GetControl() / 11000),\n                0);\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, wolf->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case WOLF_STATE_SLEEP:\n            head = 0;\n            if (wolf->mood == MOOD_ESCAPE\n                || info.zone_num == info.enemy_zone_num) {\n                item->required_anim_state = WOLF_STATE_CROUCH;\n                item->goal_anim_state = WOLF_STATE_STOP;\n            } else if (Random_GetControl() < WOLF_WAKE_CHANCE) {\n                item->required_anim_state = WOLF_STATE_WALK;\n                item->goal_anim_state = WOLF_STATE_STOP;\n            }\n            break;\n\n        case WOLF_STATE_STOP:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else {\n                item->goal_anim_state = WOLF_STATE_WALK;\n            }\n            break;\n\n        case WOLF_STATE_WALK:\n            wolf->maximum_turn = WOLF_WALK_TURN;\n            if (wolf->mood != MOOD_BORED) {\n                item->goal_anim_state = WOLF_STATE_STALK;\n                item->required_anim_state = WOLF_STATE_EMPTY;\n            } else if (Random_GetControl() < WOLF_SLEEP_CHANCE) {\n                item->required_anim_state = WOLF_STATE_SLEEP;\n                item->goal_anim_state = WOLF_STATE_STOP;\n            }\n            break;\n\n        case WOLF_STATE_CROUCH:\n            if (item->required_anim_state) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (wolf->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WOLF_STATE_RUN;\n            } else if (info.distance < WOLF_BITE_RANGE && info.bite) {\n                item->goal_anim_state = WOLF_STATE_BITE;\n            } else if (wolf->mood == MOOD_STALK) {\n                item->goal_anim_state = WOLF_STATE_STALK;\n            } else if (wolf->mood == MOOD_BORED) {\n                item->goal_anim_state = WOLF_STATE_STOP;\n            } else {\n                item->goal_anim_state = WOLF_STATE_RUN;\n            }\n            break;\n\n        case WOLF_STATE_STALK:\n            wolf->maximum_turn = WOLF_STALK_TURN;\n            if (wolf->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WOLF_STATE_RUN;\n            } else if (info.distance < WOLF_BITE_RANGE && info.bite) {\n                item->goal_anim_state = WOLF_STATE_BITE;\n            } else if (info.distance > WOLF_STALK_RANGE) {\n                item->goal_anim_state = WOLF_STATE_RUN;\n            } else if (wolf->mood == MOOD_ATTACK) {\n                if (!info.ahead || info.distance > WOLF_ATTACK_RANGE\n                    || (info.enemy_facing < FRONT_ARC\n                        && info.enemy_facing > -FRONT_ARC)) {\n                    item->goal_anim_state = WOLF_STATE_RUN;\n                }\n            } else if (Random_GetControl() < WOLF_HOWL_CHANCE) {\n                item->required_anim_state = WOLF_STATE_HOWL;\n                item->goal_anim_state = WOLF_STATE_CROUCH;\n            } else if (wolf->mood == MOOD_BORED) {\n                item->goal_anim_state = WOLF_STATE_CROUCH;\n            }\n            break;\n\n        case WOLF_STATE_RUN:\n            wolf->maximum_turn = WOLF_RUN_TURN;\n            tilt = angle;\n            if (info.ahead && info.distance < WOLF_ATTACK_RANGE) {\n                if (info.distance > (WOLF_ATTACK_RANGE / 2)\n                    && (info.enemy_facing > FRONT_ARC\n                        || info.enemy_facing < -FRONT_ARC)) {\n                    item->required_anim_state = WOLF_STATE_STALK;\n                    item->goal_anim_state = WOLF_STATE_CROUCH;\n                } else {\n                    item->goal_anim_state = WOLF_STATE_ATTACK;\n                    item->required_anim_state = WOLF_STATE_EMPTY;\n                }\n            } else if (\n                wolf->mood == MOOD_STALK && info.distance < WOLF_STALK_RANGE) {\n                item->required_anim_state = WOLF_STATE_STALK;\n                item->goal_anim_state = WOLF_STATE_CROUCH;\n            } else if (wolf->mood == MOOD_BORED) {\n                item->goal_anim_state = WOLF_STATE_CROUCH;\n            }\n            break;\n\n        case WOLF_STATE_ATTACK:\n            tilt = angle;\n            if (item->required_anim_state == WOLF_STATE_EMPTY\n                && (item->touch_bits & WOLF_TOUCH)) {\n                Creature_Effect(item, &m_WolfJawBite, Spawn_Blood);\n                Lara_TakeDamage(WOLF_POUNCE_DAMAGE, true);\n                item->required_anim_state = WOLF_STATE_RUN;\n            }\n            item->goal_anim_state = WOLF_STATE_RUN;\n            break;\n\n        case WOLF_STATE_BITE:\n            if (item->required_anim_state == WOLF_STATE_EMPTY\n                && (item->touch_bits & WOLF_TOUCH) && info.ahead) {\n                Creature_Effect(item, &m_WolfJawBite, Spawn_Blood);\n                Lara_TakeDamage(WOLF_BITE_DAMAGE, true);\n                item->required_anim_state = WOLF_STATE_CROUCH;\n            }\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Animate(item_num, angle, tilt);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->hit_points = WOLF_HITPOINTS;\n    obj->pivot_length = 375;\n    obj->radius = WOLF_RADIUS;\n    obj->smartness = WOLF_SMARTNESS;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_QUADRUPED);\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n\n    Object_GetBone(obj, 2)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_WOLF, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/worker_1.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/creatures/worker_common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define WORKER_1_HITPOINTS     25\n#define WORKER_1_SHOOT_DAMAGE  150\n#define WORKER_1_WALK_TURN     (DEG_1 * 3) // = 546\n#define WORKER_1_RUN_TURN      (DEG_1 * 5) // = 910\n#define WORKER_1_RUN_RANGE     SQUARE(WALL_L * 2) // = 4194304\n#define WORKER_1_SHOOT_1_RANGE SQUARE(WALL_L * 3) // = 9437184\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    WORKER_1_STATE_EMPTY   = 0,\n    WORKER_1_STATE_WALK    = 1,\n    WORKER_1_STATE_STOP    = 2,\n    WORKER_1_STATE_WAIT    = 3,\n    WORKER_1_STATE_SHOOT_1 = 4,\n    WORKER_1_STATE_RUN     = 5,\n    WORKER_1_STATE_SHOOT_2 = 6,\n    WORKER_1_STATE_DEATH   = 7,\n    WORKER_1_STATE_AIM_1   = 8,\n    WORKER_1_STATE_AIM_2   = 9,\n    WORKER_1_STATE_SHOOT_3 = 10,\n    // clang-format on\n} WORKER_1_STATE;\n\ntypedef enum {\n    WORKER_1_ANIM_DEATH = 18,\n} WORKER_1_ANIM;\n\nstatic const CREATURE_GUN m_Worker1Gun = {\n    .muzzle = { .pos = { .x = 0, .y = 281, .z = 40 }, .mesh_num = 9 },\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != WORKER_1_STATE_DEATH) {\n            Item_SwitchToAnim(item, WORKER_1_ANIM_DEATH, 0);\n            item->current_anim_state = WORKER_1_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case WORKER_1_STATE_STOP:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WORKER_1_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance >= WORKER_1_SHOOT_1_RANGE\n                    && info.zone_num == info.enemy_zone_num) {\n                    item->goal_anim_state = WORKER_1_STATE_WALK;\n                } else if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = WORKER_1_STATE_AIM_1;\n                } else {\n                    item->goal_anim_state = WORKER_1_STATE_AIM_2;\n                }\n            } else if (creature->mood == MOOD_BORED && info.ahead) {\n                item->goal_anim_state = WORKER_1_STATE_WAIT;\n            } else if (info.distance > WORKER_1_RUN_RANGE) {\n                item->goal_anim_state = WORKER_1_STATE_RUN;\n            } else {\n                item->goal_anim_state = WORKER_1_STATE_WALK;\n            }\n            break;\n\n        case WORKER_1_STATE_WAIT:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = WORKER_1_STATE_SHOOT_1;\n            } else if (creature->mood != MOOD_BORED || !info.ahead) {\n                item->goal_anim_state = WORKER_1_STATE_STOP;\n            }\n            break;\n\n        case WORKER_1_STATE_WALK:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->flags = 0;\n            creature->maximum_turn = WORKER_1_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WORKER_1_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance < WORKER_1_SHOOT_1_RANGE\n                    || info.zone_num != info.enemy_zone_num) {\n                    item->goal_anim_state = WORKER_1_STATE_STOP;\n                } else {\n                    item->goal_anim_state = WORKER_1_STATE_SHOOT_2;\n                }\n            } else if (creature->mood == MOOD_BORED && info.ahead) {\n                item->goal_anim_state = WORKER_1_STATE_STOP;\n            } else if (info.distance > WORKER_1_RUN_RANGE) {\n                item->goal_anim_state = WORKER_1_STATE_RUN;\n            }\n            break;\n\n        case WORKER_1_STATE_RUN:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = WORKER_1_RUN_TURN;\n            tilt = angle / 2;\n            if (creature->mood == MOOD_ESCAPE) {\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = WORKER_1_STATE_WALK;\n            } else if (\n                creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) {\n                item->goal_anim_state = WORKER_1_STATE_WALK;\n            }\n            break;\n\n        case WORKER_1_STATE_AIM_1:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            break;\n\n        case WORKER_1_STATE_AIM_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = WORKER_1_STATE_SHOOT_3;\n            }\n            break;\n\n        case WORKER_1_STATE_SHOOT_1:\n        case WORKER_1_STATE_SHOOT_3:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Shoot(\n                    item, &info, &m_Worker1Gun, head, WORKER_1_SHOOT_DAMAGE);\n                creature->flags = 1;\n            }\n            if (item->goal_anim_state != WORKER_1_STATE_STOP\n                && (creature->mood == MOOD_ESCAPE\n                    || info.distance > WORKER_1_SHOOT_1_RANGE\n                    || !Creature_CanTargetEnemy(item, &info))) {\n                item->goal_anim_state = WORKER_1_STATE_STOP;\n            }\n            break;\n\n        case WORKER_1_STATE_SHOOT_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0) {\n                Creature_Shoot(\n                    item, &info, &m_Worker1Gun, head, WORKER_1_SHOOT_DAMAGE);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = WORKER_1_HITPOINTS;\n    obj->radius = WORKER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 4)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_WORKER_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/worker_2.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/creatures/worker_common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n#define WORKER_2_HITPOINTS 20\n#define WORKER_5_HITPOINTS 20\n\n// clang-format off\n#define WORKER_2_SHOOT_DAMAGE  30\n#define WORKER_2_WALK_TURN     (DEG_1 * 3) // = 546\n#define WORKER_2_RUN_TURN      (DEG_1 * 5) // = 910\n#define WORKER_2_RUN_RANGE     SQUARE(WALL_L * 2) // = 4194304\n#define WORKER_2_SHOOT_1_RANGE SQUARE(WALL_L * 3) // = 9437184\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    WORKER_2_STATE_EMPTY   = 0,\n    WORKER_2_STATE_STOP    = 1,\n    WORKER_2_STATE_WALK    = 2,\n    WORKER_2_STATE_RUN     = 3,\n    WORKER_2_STATE_WAIT    = 4,\n    WORKER_2_STATE_SHOOT_1 = 5,\n    WORKER_2_STATE_SHOOT_2 = 6,\n    WORKER_2_STATE_DEATH   = 7,\n    WORKER_2_STATE_AIM_1   = 8,\n    WORKER_2_STATE_AIM_2   = 9,\n    WORKER_2_STATE_AIM_3   = 10,\n    WORKER_2_STATE_SHOOT_3 = 11,\n    // clang-format on\n} WORKER_2_STATE;\n\ntypedef enum {\n    WORKER_2_ANIM_DEATH = 19,\n} WORKER_2_ANIM;\n\nstatic const CREATURE_GUN m_Worker2Gun = {\n    .muzzle = { .pos = { .x = 0, .y = 308, .z = 32 }, .mesh_num = 9 },\n};\n\nstatic void M_ShootAtLara(\n    ITEM *const item, CREATURE *const creature, const AI_INFO *const info,\n    const int16_t head)\n{\n    if (item->object_id == O_WORKER_2) {\n        if (creature->flags != 0) {\n            creature->flags--;\n        } else {\n            Creature_Shoot(\n                item, info, &m_Worker2Gun, head, WORKER_2_SHOOT_DAMAGE);\n            creature->flags = 5;\n        }\n    } else {\n        Creature_Effect(item, &m_Worker2Gun.muzzle, Spawn_FireStream);\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t tilt = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != WORKER_2_STATE_DEATH) {\n            Item_SwitchToAnim(item, WORKER_2_ANIM_DEATH, 0);\n            item->current_anim_state = WORKER_2_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case WORKER_2_STATE_STOP:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WORKER_2_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance >= WORKER_2_SHOOT_1_RANGE\n                    && info.zone_num == info.enemy_zone_num) {\n                    item->goal_anim_state = WORKER_2_STATE_WALK;\n                } else if (\n                    item->object_id == O_WORKER_5\n                    || Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = WORKER_2_STATE_AIM_1;\n                } else {\n                    item->goal_anim_state = WORKER_2_STATE_AIM_3;\n                }\n            } else if (creature->mood == MOOD_BORED && info.ahead) {\n                item->goal_anim_state = WORKER_2_STATE_WAIT;\n            } else if (info.distance > WORKER_2_RUN_RANGE) {\n                item->goal_anim_state = WORKER_2_STATE_RUN;\n            } else {\n                item->goal_anim_state = WORKER_2_STATE_WALK;\n            }\n            break;\n\n        case WORKER_2_STATE_WAIT:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = WORKER_2_STATE_SHOOT_1;\n            } else if (creature->mood != MOOD_BORED || !info.ahead) {\n                item->goal_anim_state = WORKER_2_STATE_STOP;\n            }\n            break;\n\n        case WORKER_2_STATE_WALK:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->flags = 0;\n            creature->maximum_turn = WORKER_2_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WORKER_2_STATE_RUN;\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                if (info.distance < WORKER_2_SHOOT_1_RANGE\n                    || info.zone_num != info.enemy_zone_num) {\n                    item->goal_anim_state = WORKER_2_STATE_STOP;\n                } else {\n                    item->goal_anim_state = WORKER_2_STATE_AIM_2;\n                }\n            } else if (creature->mood == MOOD_BORED && info.ahead) {\n                item->goal_anim_state = WORKER_2_STATE_STOP;\n            } else if (info.distance > WORKER_2_RUN_RANGE) {\n                item->goal_anim_state = WORKER_2_STATE_RUN;\n            }\n            break;\n\n        case WORKER_2_STATE_RUN:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            tilt = angle / 2;\n            creature->maximum_turn = WORKER_2_RUN_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n            } else if (Creature_CanTargetEnemy(item, &info)) {\n                item->goal_anim_state = WORKER_2_STATE_WALK;\n            } else if (\n                creature->mood == MOOD_BORED || creature->mood == MOOD_STALK) {\n                item->goal_anim_state = WORKER_2_STATE_WALK;\n            }\n            break;\n\n        case WORKER_2_STATE_AIM_1:\n        case WORKER_2_STATE_AIM_3:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n                if (!Creature_CanTargetEnemy(item, &info)) {\n                    item->goal_anim_state = WORKER_2_STATE_STOP;\n                } else if (item->current_anim_state == WORKER_2_STATE_AIM_1) {\n                    item->goal_anim_state = WORKER_2_STATE_SHOOT_1;\n                } else {\n                    item->goal_anim_state = WORKER_2_STATE_SHOOT_3;\n                }\n            }\n            break;\n\n        case WORKER_2_STATE_AIM_2:\n            creature->flags = 0;\n            if (info.ahead) {\n                head = info.angle;\n                if (Creature_CanTargetEnemy(item, &info)) {\n                    item->goal_anim_state = WORKER_2_STATE_SHOOT_2;\n                } else {\n                    item->goal_anim_state = WORKER_2_STATE_WALK;\n                }\n            }\n            break;\n\n        case WORKER_2_STATE_SHOOT_1:\n        case WORKER_2_STATE_SHOOT_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            M_ShootAtLara(item, creature, &info, head);\n            break;\n\n        case WORKER_2_STATE_SHOOT_3:\n            if (item->goal_anim_state != WORKER_2_STATE_STOP) {\n                if (creature->mood == MOOD_ESCAPE\n                    || info.distance > WORKER_2_SHOOT_1_RANGE\n                    || !Creature_CanTargetEnemy(item, &info)) {\n                    item->goal_anim_state = WORKER_2_STATE_STOP;\n                }\n            }\n            if (info.ahead) {\n                head = info.angle;\n            }\n            M_ShootAtLara(item, creature, &info, head);\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = WORKER_2_HITPOINTS;\n    obj->radius = WORKER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 4)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nstatic void M_Setup5(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = WORKER_5_HITPOINTS;\n    obj->radius = WORKER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 4)->rot.y = true;\n    Object_GetBone(obj, 13)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_WORKER_2, M_Setup)\nREGISTER_OBJECT(O_WORKER_5, M_Setup5)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/worker_3.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/worker_common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define WORKER_3_HITPOINTS      27\n#define WORKER_4_HITPOINTS      27\n#define WORKER_3_HIT_DAMAGE     80\n#define WORKER_3_SWIPE_DAMAGE   100\n#define WORKER_3_WALK_TURN      (DEG_1 * 5) // = 910\n#define WORKER_3_RUN_TURN       (DEG_1 * 6) // = 1092\n#define WORKER_3_ATTACK_0_RANGE SQUARE(WALL_L / 2) // = 262144 = 0x40000\n#define WORKER_3_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576 = 0x100000\n#define WORKER_3_ATTACK_2_RANGE SQUARE(WALL_L * 3 / 2) // = WORKER_3_ATTACK_2_RANGE\n#define WORKER_3_WALK_RANGE     SQUARE(WALL_L * 2) // = 4194304 = 0x400000\n#define WORKER_3_WALK_CHANCE    0x100\n#define WORKER_3_WAIT_CHANCE    0x100\n#define WORKER_3_TOUCH_BITS     0b00000110'00000000 // = 0x600\n#define WORKER_3_VAULT_SHIFT    260\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    WORKER_3_STATE_EMPTY   = 0,\n    WORKER_3_STATE_STOP    = 1,\n    WORKER_3_STATE_WALK    = 2,\n    WORKER_3_STATE_PUNCH_2 = 3,\n    WORKER_3_STATE_AIM_2   = 4,\n    WORKER_3_STATE_WAIT    = 5,\n    WORKER_3_STATE_AIM_1   = 6,\n    WORKER_3_STATE_AIM_0   = 7,\n    WORKER_3_STATE_PUNCH_1 = 8,\n    WORKER_3_STATE_PUNCH_0 = 9,\n    WORKER_3_STATE_RUN     = 10,\n    WORKER_3_STATE_DEATH   = 11,\n    WORKER_3_STATE_CLIMB_3 = 12,\n    WORKER_3_STATE_CLIMB_1 = 13,\n    WORKER_3_STATE_CLIMB_2 = 14,\n    WORKER_3_STATE_FALL_3  = 15,\n    // clang-format on\n} WORKER_3_STATE;\n\ntypedef enum {\n    // clang-format off\n    WORKER_3_ANIM_DEATH   = 26,\n    WORKER_3_ANIM_CLIMB_1 = 28,\n    WORKER_3_ANIM_CLIMB_2 = 29,\n    WORKER_3_ANIM_CLIMB_3 = 27,\n    WORKER_3_ANIM_FALL_3  = 30,\n    // clang-format on\n} WORKER_3_ANIM;\n\nstatic const BITE m_Worker3Hit = {\n    .pos = { .x = 247, .y = 10, .z = 11 },\n    .mesh_num = 10,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t tilt = 0;\n    int16_t angle = 0;\n    int16_t neck = 0;\n    int16_t head = 0;\n\n    if (item->hit_points <= 0) {\n        if (item->current_anim_state != WORKER_3_STATE_DEATH) {\n            Item_SwitchToObjAnim(item, WORKER_3_ANIM_DEATH, 0, O_WORKER_3);\n            item->current_anim_state = WORKER_3_STATE_DEATH;\n        }\n    } else {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, false);\n\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case WORKER_3_STATE_STOP:\n            creature->flags = 0;\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WORKER_3_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (item->required_anim_state != WORKER_3_STATE_EMPTY) {\n                    item->goal_anim_state = item->required_anim_state;\n                } else if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = WORKER_3_STATE_WALK;\n                } else {\n                    item->goal_anim_state = WORKER_3_STATE_WAIT;\n                }\n            } else if (!info.bite) {\n                item->goal_anim_state = WORKER_3_STATE_RUN;\n            } else if (info.distance < WORKER_3_ATTACK_0_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_AIM_0;\n            } else if (info.distance < WORKER_3_ATTACK_1_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_AIM_1;\n            } else if (info.distance < WORKER_3_WALK_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_WALK;\n            } else {\n                item->goal_anim_state = WORKER_3_STATE_RUN;\n            }\n            break;\n\n        case WORKER_3_STATE_WAIT:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            if (creature->mood != MOOD_BORED) {\n                item->goal_anim_state = WORKER_3_STATE_STOP;\n            } else if (Random_GetControl() < WORKER_3_WALK_CHANCE) {\n                item->required_anim_state = WORKER_3_STATE_WALK;\n                item->goal_anim_state = WORKER_3_STATE_STOP;\n            }\n            break;\n\n        case WORKER_3_STATE_WALK:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = WORKER_3_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = WORKER_3_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                if (Random_GetControl() < WORKER_3_WAIT_CHANCE) {\n                    item->required_anim_state = WORKER_3_STATE_WAIT;\n                    item->goal_anim_state = WORKER_3_STATE_STOP;\n                }\n            } else if (!info.bite) {\n                item->goal_anim_state = WORKER_3_STATE_RUN;\n            } else if (info.distance < WORKER_3_ATTACK_0_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_STOP;\n            } else if (info.distance < WORKER_3_ATTACK_2_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_AIM_2;\n            } else {\n                item->goal_anim_state = WORKER_3_STATE_RUN;\n            }\n            break;\n\n        case WORKER_3_STATE_RUN:\n            if (info.ahead) {\n                neck = info.angle;\n            }\n            creature->maximum_turn = WORKER_3_RUN_TURN;\n            tilt = angle / 2;\n            if (creature->mood == MOOD_ESCAPE) {\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = WORKER_3_STATE_WALK;\n            } else if (info.ahead && info.distance < WORKER_3_WALK_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_WALK;\n            }\n            break;\n\n        case WORKER_3_STATE_AIM_0:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            if (info.bite && info.distance < WORKER_3_ATTACK_0_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_PUNCH_0;\n            } else {\n                item->goal_anim_state = WORKER_3_STATE_STOP;\n            }\n            break;\n\n        case WORKER_3_STATE_AIM_1:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            if (info.ahead && info.distance < WORKER_3_ATTACK_1_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_PUNCH_1;\n            } else {\n                item->goal_anim_state = WORKER_3_STATE_STOP;\n            }\n            break;\n\n        case WORKER_3_STATE_AIM_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            creature->flags = 0;\n            if (info.bite && info.distance < WORKER_3_ATTACK_2_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_PUNCH_2;\n            } else {\n                item->goal_anim_state = WORKER_3_STATE_WALK;\n            }\n            break;\n\n        case WORKER_3_STATE_PUNCH_0:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0\n                && (item->touch_bits & WORKER_3_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(WORKER_3_HIT_DAMAGE, true);\n                Creature_Effect(item, &m_Worker3Hit, Spawn_Blood);\n                Sound_Effect(SFX_ENEMY_HIT_2, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n            break;\n\n        case WORKER_3_STATE_PUNCH_1:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags == 0\n                && (item->touch_bits & WORKER_3_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(WORKER_3_HIT_DAMAGE, true);\n                Creature_Effect(item, &m_Worker3Hit, Spawn_Blood);\n                Sound_Effect(SFX_ENEMY_HIT_1, &item->pos, SPM_NORMAL);\n                creature->flags = 1;\n            }\n            if (info.ahead && info.distance > WORKER_3_ATTACK_1_RANGE\n                && info.distance < WORKER_3_ATTACK_2_RANGE) {\n                item->goal_anim_state = WORKER_3_STATE_PUNCH_2;\n            }\n            break;\n\n        case WORKER_3_STATE_PUNCH_2:\n            if (info.ahead) {\n                head = info.angle;\n            }\n            if (creature->flags != 2\n                && (item->touch_bits & WORKER_3_TOUCH_BITS) != 0) {\n                Lara_TakeDamage(WORKER_3_SWIPE_DAMAGE, true);\n                Creature_Effect(item, &m_Worker3Hit, Spawn_Blood);\n                Sound_Effect(SFX_ENEMY_HIT_1, &item->pos, SPM_NORMAL);\n                creature->flags = 2;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    Creature_Tilt(item, tilt);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n\n    if (item->current_anim_state >= WORKER_3_STATE_CLIMB_3) {\n        Creature_Animate(item_num, angle, 0);\n    } else {\n        switch (Creature_Vault(item_num, angle, 2, WORKER_3_VAULT_SHIFT)) {\n        case -4:\n            Item_SwitchToObjAnim(item, WORKER_3_ANIM_FALL_3, 0, O_WORKER_3);\n            item->current_anim_state = WORKER_3_STATE_FALL_3;\n            break;\n\n        case 2:\n            Item_SwitchToObjAnim(item, WORKER_3_ANIM_CLIMB_1, 0, O_WORKER_3);\n            item->current_anim_state = WORKER_3_STATE_CLIMB_1;\n            break;\n\n        case 3:\n            Item_SwitchToObjAnim(item, WORKER_3_ANIM_CLIMB_2, 0, O_WORKER_3);\n            item->current_anim_state = WORKER_3_STATE_CLIMB_2;\n            break;\n\n        case 4:\n            Item_SwitchToObjAnim(item, WORKER_3_ANIM_CLIMB_3, 0, O_WORKER_3);\n            item->current_anim_state = WORKER_3_STATE_CLIMB_3;\n            break;\n\n        default:\n            return;\n        }\n    }\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->radius = WORKER_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 4)->rot.y = true;\n}\n\nstatic void M_Setup3(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    M_SetupBase(obj);\n    obj->hit_points = WORKER_3_HITPOINTS;\n}\n\nstatic void M_Setup4(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    M_SetupBase(obj);\n    obj->hit_points = WORKER_4_HITPOINTS;\n}\n\nREGISTER_OBJECT(O_WORKER_3, M_Setup3)\nREGISTER_OBJECT(O_WORKER_4, M_Setup4)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/worker_common.h",
    "content": "#pragma once\n\n#include <trx/game/const.h>\n\n#define WORKER_RADIUS (WALL_L / 10) // = 102\n"
  },
  {
    "path": "src/trx/game/objects/creatures/xian_common.c",
    "content": "#include <trx/game/objects/creatures/xian_common.h>\n\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/draw.h>\n\nbool XianWarrior_Draw(const ITEM *item)\n{\n    const OBJECT *swap;\n    if (item->object_id == O_XIAN_SPEARMAN) {\n        swap = Object_Get(O_XIAN_SPEARMAN_STATUE);\n    } else {\n        swap = Object_Get(O_XIAN_KNIGHT_STATUE);\n    }\n    return Object_DrawAnimatingItemWithSwap(item, swap);\n}\n"
  },
  {
    "path": "src/trx/game/objects/creatures/xian_common.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nbool XianWarrior_Draw(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/objects/creatures/xian_knight.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/creatures/xian_common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define XIAN_KNIGHT_HITPOINTS      80\n#define XIAN_KNIGHT_TOUCH_BITS     0b11000000'00000000 // = 0xC000\n#define XIAN_KNIGHT_RADIUS         (WALL_L / 5) // = 204\n#define XIAN_KNIGHT_HACK_DAMAGE    300\n#define XIAN_KNIGHT_WALK_TURN      (DEG_1 * 5) // = 910\n#define XIAN_KNIGHT_FLY_TURN       (DEG_1 * 4) // = 728\n#define XIAN_KNIGHT_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576\n#define XIAN_KNIGHT_ATTACK_3_RANGE SQUARE(WALL_L * 2) // = 4194304\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    XIAN_KNIGHT_STATE_EMPTY   = 0,\n    XIAN_KNIGHT_STATE_STOP    = 1,\n    XIAN_KNIGHT_STATE_WALK    = 2,\n    XIAN_KNIGHT_STATE_AIM_1   = 3,\n    XIAN_KNIGHT_STATE_SLASH_1 = 4,\n    XIAN_KNIGHT_STATE_AIM_2   = 5,\n    XIAN_KNIGHT_STATE_SLASH_2 = 6,\n    XIAN_KNIGHT_STATE_WAIT    = 7,\n    XIAN_KNIGHT_STATE_FLY     = 8,\n    XIAN_KNIGHT_STATE_START   = 9,\n    XIAN_KNIGHT_STATE_AIM_3   = 10,\n    XIAN_KNIGHT_STATE_SLASH_3 = 11,\n    XIAN_KNIGHT_STATE_DEATH   = 12,\n    // clang-format on\n} XIAN_KNIGHT_STATE;\n\nstatic const BITE m_XianKnightSword = {\n    .pos = { .x = 0, .y = 37, .z = 550 },\n    .mesh_num = 15,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->status = IS_INACTIVE;\n    item->mesh_bits = 0;\n}\n\nstatic void M_SparkleTrail(const ITEM *const item)\n{\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_TWINKLE;\n        effect->pos.x = item->pos.x + (Random_GetDraw() << 8 >> 15) - 128;\n        effect->pos.y = item->pos.y + (Random_GetDraw() << 8 >> 15) - 256;\n        effect->pos.z = item->pos.z + (Random_GetDraw() << 8 >> 15) - 128;\n        effect->room_num = item->room_num;\n        effect->counter = -30;\n        effect->frame_num = 0;\n    }\n    Sound_Effect(SFX_WARRIOR_HOVER, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n\n    if (item->hit_points <= 0) {\n        item->current_anim_state = XIAN_KNIGHT_STATE_DEATH;\n        item->mesh_bits >>= 1;\n        item->enable_interpolation = false;\n        if (item->mesh_bits == 0) {\n            Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL);\n            item->mesh_bits = -1;\n            item->object_id = O_XIAN_KNIGHT_STATUE;\n            Item_Explode(item_num, -1, 0);\n            item->object_id = O_XIAN_KNIGHT;\n            LOT_DisableBaddieAI(item_num);\n            Item_Kill(item_num);\n            item->status = IS_DEACTIVATED;\n            item->flags |= IF_ONE_SHOT;\n            Carrier_TestItemDrops(item_num);\n        }\n        return;\n    }\n\n    creature->lot.setup.step = STEP_L;\n    creature->lot.setup.drop = -STEP_L;\n    creature->lot.setup.fly = 0;\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    if (item->current_anim_state == XIAN_KNIGHT_STATE_FLY\n        && info.zone_num != info.enemy_zone_num) {\n        creature->lot.setup.step = WALL_L * 20;\n        creature->lot.setup.drop = -WALL_L * 20;\n        creature->lot.setup.fly = STEP_L / 4;\n        Creature_AIInfo(item, &info);\n    }\n    Creature_Mood(item, &info, true);\n\n    angle = Creature_Turn(item, creature->maximum_turn);\n    if (item->current_anim_state != XIAN_KNIGHT_STATE_START) {\n        item->mesh_bits = -1;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    switch (item->current_anim_state) {\n    case XIAN_KNIGHT_STATE_START:\n        if (creature->flags == 0) {\n            item->mesh_bits = (item->mesh_bits << 1) | 1;\n            creature->flags = 3;\n        } else {\n            creature->flags--;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_STOP:\n        creature->maximum_turn = 0;\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        if (lara_item->hit_points <= 0) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_WAIT;\n        } else if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_1_RANGE) {\n            if (Random_GetControl() < 0x4000) {\n                item->goal_anim_state = XIAN_KNIGHT_STATE_AIM_1;\n            } else {\n                item->goal_anim_state = XIAN_KNIGHT_STATE_AIM_2;\n            }\n        } else if (info.zone_num != info.enemy_zone_num) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_FLY;\n        } else {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_WALK;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_WALK:\n        creature->maximum_turn = XIAN_KNIGHT_WALK_TURN;\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        if (lara_item->hit_points <= 0) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_STOP;\n        } else if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_3_RANGE) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_AIM_3;\n        } else if (info.zone_num != info.enemy_zone_num) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_STOP;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_FLY:\n        creature->maximum_turn = XIAN_KNIGHT_FLY_TURN;\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        M_SparkleTrail(item);\n        if (creature->lot.setup.fly == 0) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_STOP;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_AIM_1:\n        creature->flags = 0;\n        if (info.ahead) {\n            head = info.angle;\n        }\n        if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_1_RANGE) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_SLASH_1;\n        } else {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_STOP;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_AIM_2:\n        creature->flags = 0;\n        if (info.ahead) {\n            head = info.angle;\n        }\n        if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_1_RANGE) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_SLASH_2;\n        } else {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_STOP;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_AIM_3:\n        creature->flags = 0;\n        if (info.ahead) {\n            head = info.angle;\n        }\n        if (info.bite && info.distance < XIAN_KNIGHT_ATTACK_3_RANGE) {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_SLASH_3;\n        } else {\n            item->goal_anim_state = XIAN_KNIGHT_STATE_WALK;\n        }\n        break;\n\n    case XIAN_KNIGHT_STATE_SLASH_1:\n    case XIAN_KNIGHT_STATE_SLASH_2:\n    case XIAN_KNIGHT_STATE_SLASH_3:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        if (creature->flags == 0\n            && (item->touch_bits & XIAN_KNIGHT_TOUCH_BITS) != 0) {\n            Lara_TakeDamage(XIAN_KNIGHT_HACK_DAMAGE, true);\n            Creature_Effect(item, &m_XianKnightSword, Spawn_Blood);\n            creature->flags = 1;\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    Creature_Tilt(item, 0);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    SOFT_ASSERT(\n        Object_Get(O_XIAN_KNIGHT_STATUE)->loaded,\n        \"Xian swordsman statue object missing\");\n\n    obj->initialise_func = M_Initialise;\n    obj->draw_func = XianWarrior_Draw;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = XIAN_KNIGHT_HITPOINTS;\n    obj->radius = XIAN_KNIGHT_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 16)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_XIAN_KNIGHT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/xian_spearman.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/xian_common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define XIAN_SPEARMAN_HITPOINTS    100\n#define XIAN_SPEARMAN_HIT_1_DAMAGE 75\n#define XIAN_SPEARMAN_HIT_2_DAMAGE 75\n#define XIAN_SPEARMAN_HIT_5_DAMAGE 75\n#define XIAN_SPEARMAN_HIT_6_DAMAGE 120\n#define XIAN_SPEARMAN_RADIUS       (WALL_L / 5) // = 204\n#define XIAN_SPEARMAN_TOUCH_L_BITS 0b00000000'00001000'00000000 // = 0x00800\n#define XIAN_SPEARMAN_TOUCH_R_BITS 0b00000100'00000000'00000000 // = 0x40000\n#define XIAN_WALK_TURN             (DEG_1 * 3) // = 546\n#define XIAN_RUN_TURN              (DEG_1 * 5) // = 910\n#define XIAN_ATTACK_1_RANGE        SQUARE(WALL_L) // = 1048576\n#define XIAN_ATTACK_2_RANGE        SQUARE(WALL_L * 3 / 2) // = 2359296\n#define XIAN_ATTACK_3_RANGE        SQUARE(WALL_L * 2) // = 4194304\n#define XIAN_ATTACK_4_RANGE        SQUARE(WALL_L * 2) // = 4194304\n#define XIAN_ATTACK_5_RANGE        SQUARE(WALL_L) // = 1048576\n#define XIAN_ATTACK_6_RANGE        SQUARE(WALL_L * 2) // = 4194304\n#define XIAN_RUN_RANGE             SQUARE(WALL_L * 3) // = 9437184\n#define XIAN_STOP_CHANCE           0x200\n#define XIAN_WALK_CHANCE           (XIAN_STOP_CHANCE + 0x200) // = 0x400\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    XIAN_SPEARMAN_STATE_EMPTY  = 0,\n    XIAN_SPEARMAN_STATE_STOP   = 1,\n    XIAN_SPEARMAN_STATE_STOP_2 = 2,\n    XIAN_SPEARMAN_STATE_WALK   = 3,\n    XIAN_SPEARMAN_STATE_RUN    = 4,\n    XIAN_SPEARMAN_STATE_AIM_1  = 5,\n    XIAN_SPEARMAN_STATE_HIT_1  = 6,\n    XIAN_SPEARMAN_STATE_AIM_2  = 7,\n    XIAN_SPEARMAN_STATE_HIT_2  = 8,\n    XIAN_SPEARMAN_STATE_AIM_3  = 9,\n    XIAN_SPEARMAN_STATE_HIT_3  = 10,\n    XIAN_SPEARMAN_STATE_AIM_4  = 11,\n    XIAN_SPEARMAN_STATE_HIT_4  = 12,\n    XIAN_SPEARMAN_STATE_AIM_5  = 13,\n    XIAN_SPEARMAN_STATE_HIT_5  = 14,\n    XIAN_SPEARMAN_STATE_AIM_6  = 15,\n    XIAN_SPEARMAN_STATE_HIT_6  = 16,\n    XIAN_SPEARMAN_STATE_DEATH  = 17,\n    XIAN_SPEARMAN_STATE_START  = 18,\n    XIAN_SPEARMAN_STATE_KILL   = 19,\n    // clang-format on\n} XIAN_SPEARMAN_STATE;\n\ntypedef enum {\n    // clang-format off\n    XIAN_SPEARMAN_ANIM_DEATH = 0,\n    XIAN_SPEARMAN_ANIM_START = 48,\n    XIAN_SPEARMAN_ANIM_KILL  = 49,\n    // clang-format on\n} XIAN_SPEARMAN_ANIM;\n\nstatic const BITE m_XianSpearmanLeftSpear = {\n    .pos = { .x = 0, .y = 0, .z = 920 },\n    .mesh_num = 11,\n};\n\nstatic const BITE m_XianSpearmanRightSpear = {\n    .pos = { .x = 0, .y = 0, .z = 920 },\n    .mesh_num = 18,\n};\n\nstatic void M_DoDamage(\n    const ITEM *const item, CREATURE *const creature, const int32_t damage)\n{\n    if ((creature->flags & 1) == 0\n        && (item->touch_bits & XIAN_SPEARMAN_TOUCH_R_BITS) != 0) {\n        Lara_TakeDamage(damage, true);\n        Creature_Effect(item, &m_XianSpearmanRightSpear, Spawn_Blood);\n        creature->flags |= 1;\n        Sound_Effect(SFX_CRUNCH_2, &item->pos, SPM_NORMAL);\n    }\n\n    if ((creature->flags & 2) == 0\n        && (item->touch_bits & XIAN_SPEARMAN_TOUCH_L_BITS) != 0) {\n        Lara_TakeDamage(damage, true);\n        Creature_Effect(item, &m_XianSpearmanLeftSpear, Spawn_Blood);\n        creature->flags |= 2;\n        Sound_Effect(SFX_CRUNCH_2, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, XIAN_SPEARMAN_ANIM_START, 0);\n    item->goal_anim_state = XIAN_SPEARMAN_STATE_START;\n    item->current_anim_state = XIAN_SPEARMAN_STATE_START;\n    item->status = IS_INACTIVE;\n    item->mesh_bits = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t neck = 0;\n    int16_t angle = 0;\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const bool lara_was_alive = lara_item->hit_points > 0;\n\n    if (item->hit_points <= 0) {\n        item->current_anim_state = XIAN_SPEARMAN_STATE_DEATH;\n        item->mesh_bits >>= 1;\n        item->enable_interpolation = false;\n        if (item->mesh_bits == 0) {\n            Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL);\n            item->mesh_bits = -1;\n            item->object_id = O_XIAN_SPEARMAN_STATUE;\n            Item_Explode(item_num, -1, 0);\n            item->object_id = O_XIAN_SPEARMAN;\n            LOT_DisableBaddieAI(item_num);\n            Item_Kill(item_num);\n            item->status = IS_DEACTIVATED;\n            item->flags |= IF_ONE_SHOT;\n            Carrier_TestItemDrops(item_num);\n        }\n        return;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n    Creature_Mood(item, &info, true);\n\n    angle = Creature_Turn(item, creature->maximum_turn);\n    if (item->current_anim_state != XIAN_SPEARMAN_STATE_START) {\n        item->mesh_bits = -1;\n    }\n\n    switch (item->current_anim_state) {\n    case XIAN_SPEARMAN_STATE_START:\n        if (creature->flags == 0) {\n            item->mesh_bits = (item->mesh_bits << 1) | 1;\n            creature->flags = 3;\n        } else {\n            creature->flags--;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_STOP:\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        creature->maximum_turn = 0;\n        if (creature->mood == MOOD_BORED) {\n            const int32_t random = Random_GetControl();\n            if (random < XIAN_STOP_CHANCE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n            } else if (random < XIAN_WALK_CHANCE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n            }\n        } else if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_1;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_STOP_2:\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        creature->maximum_turn = 0;\n        if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        } else if (creature->mood == MOOD_BORED) {\n            const int32_t random = Random_GetControl();\n            if (random < XIAN_STOP_CHANCE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n            } else if (random < XIAN_WALK_CHANCE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n            }\n        } else if (info.ahead && info.distance < XIAN_ATTACK_5_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_5;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_WALK:\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        creature->maximum_turn = XIAN_WALK_TURN;\n        if (creature->mood == MOOD_ESCAPE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN;\n        } else if (creature->mood == MOOD_BORED) {\n            const int32_t random = Random_GetControl();\n            if (random < XIAN_STOP_CHANCE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n            } else if (random < XIAN_WALK_CHANCE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n            }\n        } else if (info.ahead && info.distance < XIAN_ATTACK_4_RANGE) {\n            if (info.distance < XIAN_ATTACK_2_RANGE) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_2;\n            } else {\n                if (Random_GetControl() < 0x4000) {\n                    item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_3;\n                } else {\n                    item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_4;\n                }\n            }\n        } else if (!info.ahead || info.distance > XIAN_RUN_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_RUN:\n        if (info.ahead) {\n            neck = info.angle;\n        }\n        creature->maximum_turn = XIAN_RUN_TURN;\n        if (creature->mood == MOOD_ESCAPE) {\n        } else if (creature->mood == MOOD_BORED) {\n            if (Random_GetControl() < 0x4000) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n            } else {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n            }\n        } else if (info.ahead && info.distance < XIAN_ATTACK_6_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_AIM_6;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_AIM_1:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->flags = 0;\n        if (!info.ahead || info.distance > XIAN_ATTACK_1_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_1;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_AIM_2:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->flags = 0;\n        if (!info.ahead || info.distance > XIAN_ATTACK_2_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_2;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_AIM_3:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->flags = 0;\n        if (!info.ahead || info.distance > XIAN_ATTACK_3_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_2;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_AIM_4:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->flags = 0;\n        if (!info.ahead || info.distance > XIAN_ATTACK_4_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_2;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_AIM_5:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->flags = 0;\n        if (!info.ahead || info.distance > XIAN_ATTACK_5_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_5;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_AIM_6:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        creature->flags = 0;\n        if (!info.ahead || info.distance > XIAN_ATTACK_6_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_HIT_6;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_HIT_1:\n        M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_1_DAMAGE);\n        break;\n\n    case XIAN_SPEARMAN_STATE_HIT_2:\n    case XIAN_SPEARMAN_STATE_HIT_3:\n    case XIAN_SPEARMAN_STATE_HIT_4:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_2_DAMAGE);\n        if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) {\n            const int32_t random = Random_GetControl();\n            if (random < 0x4000) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n            } else {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n            }\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_HIT_5:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_5_DAMAGE);\n        if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n        }\n        break;\n\n    case XIAN_SPEARMAN_STATE_HIT_6:\n        if (info.ahead) {\n            head = info.angle;\n        }\n        M_DoDamage(item, creature, XIAN_SPEARMAN_HIT_6_DAMAGE);\n        if (info.ahead && info.distance < XIAN_ATTACK_1_RANGE) {\n            const int32_t random = Random_GetControl();\n            if (random < 0x4000) {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP;\n            } else {\n                item->goal_anim_state = XIAN_SPEARMAN_STATE_STOP_2;\n            }\n        } else if (info.ahead && info.distance < XIAN_ATTACK_4_RANGE) {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_WALK;\n        } else {\n            item->goal_anim_state = XIAN_SPEARMAN_STATE_RUN;\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    if (lara_was_alive && lara_item->hit_points <= 0) {\n        Creature_SpecialKill(\n            item, XIAN_SPEARMAN_ANIM_KILL, XIAN_SPEARMAN_STATE_KILL,\n            LS_EXTRA_GUARD_KILL);\n        return;\n    }\n\n    Creature_Tilt(item, 0);\n    Creature_Head(item, head);\n    Creature_Neck(item, neck);\n    Creature_Animate(item_num, angle, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    SOFT_ASSERT(\n        Object_Get(O_XIAN_SPEARMAN_STATUE)->loaded,\n        \"Xian spearman statue object missing\");\n\n    obj->initialise_func = M_Initialise;\n    obj->draw_func = XianWarrior_Draw;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = XIAN_SPEARMAN_HITPOINTS;\n    obj->radius = XIAN_SPEARMAN_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 12)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_XIAN_SPEARMAN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/creatures/yeti.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_HITPOINTS        30\n#define M_TOUCH_BITS_R     0b00000111'00000000 // = 0x0700\n#define M_TOUCH_BITS_L     0b00111000'00000000 // = 0x3800\n#define M_TOUCH_BITS_LR    (M_TOUCH_BITS_R | M_TOUCH_BITS_L) // = 0x3F00\n#define M_RADIUS           (WALL_L / 8) // = 128\n#define M_WALK_TURN        (DEG_1 * 4) // = 728\n#define M_RUN_TURN         (DEG_1 * 6) // = 1092\n#define M_VAULT_SHIFT      300\n#define M_ATTACK_1_RANGE   SQUARE(WALL_L / 2) // = 262144\n#define M_ATTACK_2_RANGE   SQUARE(WALL_L * 2 / 3) // = 465124\n#define M_ATTACK_3_RANGE   SQUARE(WALL_L * 2) // = 4194304\n#define M_RUN_RANGE        SQUARE(WALL_L * 2) // = 4194304\n#define M_PUNCH_DAMAGE     100\n#define M_THUMP_DAMAGE     150\n#define M_CHARGE_DAMAGE    200\n#define M_ATTACK_1_CHANCE  0x4000\n#define M_WAIT_1_CHANCE    0x100\n#define M_WAIT_2_CHANCE    (M_WAIT_1_CHANCE + 0x100) // = 512\n#define M_WALK_CHANCE      (M_WAIT_2_CHANCE + 0x100) // = 768\n#define M_STOP_ROAR_CHANCE 0x200\n// clang-format on\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_RUN,\n    M_STATE_STOP,\n    M_STATE_WALK,\n    M_STATE_ATTACK_1,\n    M_STATE_ATTACK_2,\n    M_STATE_ATTACK_3,\n    M_STATE_WAIT_1,\n    M_STATE_DEATH,\n    M_STATE_WAIT_2,\n    M_STATE_CLIMB_1,\n    M_STATE_CLIMB_2,\n    M_STATE_CLIMB_3,\n    M_STATE_FALL,\n    M_STATE_KILL,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_KILL    = 36,\n    M_ANIM_FALL    = 35,\n    M_ANIM_CLIMB_1 = 34,\n    M_ANIM_CLIMB_2 = 33,\n    M_ANIM_CLIMB_3 = 32,\n    M_ANIM_DEATH   = 31,\n    // clang-format on\n} M_ANIM;\n\nstatic const BITE m_YetiBiteL = {\n    .pos = { .x = 12, .y = 101, .z = 19 },\n    .mesh_num = 13,\n};\n\nstatic const BITE m_YetiBiteR = {\n    .pos = { .x = 12, .y = 101, .z = 19 },\n    .mesh_num = 10,\n};\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n\n    int16_t head = 0;\n    int16_t body = 0;\n    int16_t angle = 0;\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const bool lara_was_alive = lara_item->hit_points > 0;\n\n    if (item->hit_points > 0) {\n        AI_INFO info;\n        Creature_AIInfo(item, &info);\n        Creature_Mood(item, &info, true);\n\n        if (info.ahead) {\n            head = info.angle;\n        }\n        angle = Creature_Turn(item, creature->maximum_turn);\n\n        switch (item->current_anim_state) {\n        case M_STATE_STOP:\n            creature->flags = 0;\n            creature->maximum_turn = 0;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (item->required_anim_state != M_STATE_EMPTY) {\n                item->goal_anim_state = item->required_anim_state;\n            } else if (creature->mood == MOOD_BORED) {\n                const int32_t random = Random_GetControl();\n                if (random < M_WAIT_1_CHANCE || !lara_was_alive) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                } else if (random < M_WAIT_2_CHANCE) {\n                    item->goal_anim_state = M_STATE_WAIT_2;\n                } else if (random < M_WALK_CHANCE) {\n                    item->goal_anim_state = M_STATE_WALK;\n                }\n            } else if (\n                info.ahead && info.distance < M_ATTACK_1_RANGE\n                && Random_GetControl() < M_ATTACK_1_CHANCE) {\n                item->goal_anim_state = M_STATE_ATTACK_1;\n                break;\n            } else if (info.ahead && info.distance < M_ATTACK_2_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_2;\n            } else if (creature->mood == MOOD_STALK) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else {\n                item->goal_anim_state = M_STATE_RUN;\n            }\n            break;\n\n        case M_STATE_WAIT_1:\n            if (creature->mood == MOOD_ESCAPE || item->hit_status) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (creature->mood == MOOD_BORED) {\n                if (lara_was_alive) {\n                    const int32_t random = Random_GetControl();\n                    if (random < M_WAIT_1_CHANCE) {\n                        item->goal_anim_state = M_STATE_STOP;\n                    } else if (random < M_WAIT_2_CHANCE) {\n                        item->goal_anim_state = M_STATE_WAIT_2;\n                    } else if (random < M_WALK_CHANCE) {\n                        item->goal_anim_state = M_STATE_STOP;\n                        item->required_anim_state = M_STATE_WALK;\n                    }\n                }\n            } else if (Random_GetControl() < M_STOP_ROAR_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_WAIT_2:\n            if (creature->mood == MOOD_ESCAPE || item->hit_status) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (creature->mood == MOOD_BORED) {\n                const int32_t random = Random_GetControl();\n                if (random < M_WAIT_1_CHANCE || !lara_was_alive) {\n                    item->goal_anim_state = M_STATE_WAIT_1;\n                } else if (random < M_WAIT_2_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (random < M_WALK_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                    item->required_anim_state = M_STATE_WALK;\n                }\n            } else if (Random_GetControl() < M_STOP_ROAR_CHANCE) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n            break;\n\n        case M_STATE_WALK:\n            creature->maximum_turn = M_WALK_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                item->goal_anim_state = M_STATE_RUN;\n            } else if (creature->mood == MOOD_BORED) {\n                const int32_t random = Random_GetControl();\n                if (random < M_WAIT_1_CHANCE || !lara_was_alive) {\n                    item->goal_anim_state = M_STATE_STOP;\n                    item->required_anim_state = M_STATE_WAIT_1;\n                } else if (random < M_WAIT_2_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                    item->required_anim_state = M_STATE_WAIT_2;\n                } else if (random < M_WALK_CHANCE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                }\n            } else if (creature->mood == MOOD_ATTACK) {\n                if (info.ahead && info.distance < M_ATTACK_2_RANGE) {\n                    item->goal_anim_state = M_STATE_STOP;\n                } else if (info.distance > M_RUN_RANGE) {\n                    item->goal_anim_state = M_STATE_RUN;\n                }\n            }\n            break;\n\n        case M_STATE_RUN:\n            creature->flags = 0;\n            creature->maximum_turn = M_RUN_TURN;\n            if (creature->mood == MOOD_ESCAPE) {\n                break;\n            } else if (creature->mood == MOOD_BORED) {\n                item->goal_anim_state = M_STATE_WALK;\n            } else if (info.ahead && info.distance < M_ATTACK_2_RANGE) {\n                item->goal_anim_state = M_STATE_STOP;\n            } else if (info.ahead && info.distance < M_ATTACK_3_RANGE) {\n                item->goal_anim_state = M_STATE_ATTACK_3;\n            } else if (creature->mood == MOOD_STALK) {\n                item->goal_anim_state = M_STATE_WALK;\n            }\n            break;\n\n        case M_STATE_ATTACK_1:\n            body = head;\n            head = 0;\n            if (creature->flags == 0\n                && (item->touch_bits & M_TOUCH_BITS_R) != 0) {\n                Creature_Effect(item, &m_YetiBiteR, Spawn_Blood);\n                Lara_TakeDamage(M_PUNCH_DAMAGE, true);\n                creature->flags = 1;\n                break;\n            }\n            break;\n\n        case M_STATE_ATTACK_2:\n            body = head;\n            head = 0;\n\n            creature->maximum_turn = M_WALK_TURN;\n            if (creature->flags == 0\n                && (item->touch_bits & M_TOUCH_BITS_LR) != 0) {\n                if ((item->touch_bits & M_TOUCH_BITS_L) != 0) {\n                    Creature_Effect(item, &m_YetiBiteL, Spawn_Blood);\n                }\n                if ((item->touch_bits & M_TOUCH_BITS_R) != 0) {\n                    Creature_Effect(item, &m_YetiBiteR, Spawn_Blood);\n                }\n                Lara_TakeDamage(M_THUMP_DAMAGE, true);\n                creature->flags = 1;\n            }\n            break;\n\n        case M_STATE_ATTACK_3:\n            body = head;\n            head = 0;\n\n            if (creature->flags == 0\n                && (item->touch_bits & M_TOUCH_BITS_LR) != 0) {\n                if ((item->touch_bits & M_TOUCH_BITS_L) != 0) {\n                    Creature_Effect(item, &m_YetiBiteL, Spawn_Blood);\n                }\n                if ((item->touch_bits & M_TOUCH_BITS_R) != 0) {\n                    Creature_Effect(item, &m_YetiBiteR, Spawn_Blood);\n                }\n                Lara_TakeDamage(M_CHARGE_DAMAGE, true);\n                creature->flags = 1;\n            }\n            break;\n\n        default:\n            break;\n        }\n    } else if (item->current_anim_state != M_STATE_DEATH) {\n        Item_SwitchToAnim(item, M_ANIM_DEATH, 0);\n        item->current_anim_state = M_STATE_DEATH;\n    }\n\n    if (lara_was_alive && lara_item->hit_points <= 0) {\n        Creature_SpecialKill(\n            item, M_ANIM_KILL, M_STATE_KILL, LS_EXTRA_YETI_KILL);\n        return;\n    }\n\n    Creature_Head(item, body);\n    Creature_Neck(item, head);\n    if (item->current_anim_state >= M_STATE_CLIMB_1) {\n        Creature_Animate(item_num, angle, 0);\n    } else {\n        switch (Creature_Vault(item_num, angle, 2, M_VAULT_SHIFT)) {\n        case -4:\n            Item_SwitchToAnim(item, M_ANIM_FALL, 0);\n            item->current_anim_state = M_STATE_FALL;\n            break;\n\n        case 2:\n            Item_SwitchToAnim(item, M_ANIM_CLIMB_1, 0);\n            item->current_anim_state = M_STATE_CLIMB_1;\n            break;\n\n        case 3:\n            Item_SwitchToAnim(item, M_ANIM_CLIMB_2, 0);\n            item->current_anim_state = M_STATE_CLIMB_2;\n            break;\n\n        case 4:\n            Item_SwitchToAnim(item, M_ANIM_CLIMB_3, 0);\n            item->current_anim_state = M_STATE_CLIMB_3;\n            break;\n\n        default:\n            return;\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n\n    obj->hit_points = M_HITPOINTS;\n    obj->radius = M_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 100;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_CLIMBER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 6)->rot.y = true;\n    Object_GetBone(obj, 14)->rot.y = true;\n}\n\nREGISTER_OBJECT(O_YETI, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/draw.c",
    "content": "#include <trx/game/objects/draw.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items/col.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/vars.h>\n#include <trx/version.h>\n\nstatic BOUNDS_16 M_GetBoundingBox(\n    const OBJECT *const obj, const ANIM_FRAME *const frame,\n    const uint32_t mesh_bits)\n{\n    const XYZ_16 *const mesh_rots =\n        frame != nullptr ? frame->mesh_rots : nullptr;\n\n    Matrix_PushUnit();\n    if (frame != nullptr) {\n        Matrix_TranslateRel16(frame->offset);\n    }\n    if (mesh_rots != nullptr) {\n        Matrix_Rot16(mesh_rots[0]);\n    }\n\n    BOUNDS_16 new_bounds = {\n        .min.x = 0x7FFF,\n        .min.y = 0x7FFF,\n        .min.z = 0x7FFF,\n        .max.x = -0x7FFF,\n        .max.y = -0x7FFF,\n        .max.z = -0x7FFF,\n    };\n\n    for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) {\n        if (mesh_idx != 0) {\n            const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n            if (bone->matrix_pop) {\n                Matrix_Pop();\n            }\n\n            if (bone->matrix_push) {\n                Matrix_Push();\n            }\n\n            Matrix_TranslateRel32(bone->pos);\n            if (mesh_rots != nullptr) {\n                Matrix_Rot16(mesh_rots[mesh_idx]);\n            }\n        }\n\n        if (!(mesh_bits & (1 << mesh_idx))) {\n            continue;\n        }\n\n        const OBJECT_MESH *const mesh =\n            Object_GetMesh(obj->mesh_idx + mesh_idx);\n        for (int32_t i = 0; i < mesh->num_vertices; i++) {\n            // clang-format off\n            const XYZ_16 *const vertex = &mesh->vertices[i];\n            const MATRIX *const mptr = g_MatrixPtr;\n            const double xv = (\n                mptr->_00 * vertex->x +\n                mptr->_01 * vertex->y +\n                mptr->_02 * vertex->z +\n                mptr->_03\n            );\n            const double yv = (\n                mptr->_10 * vertex->x +\n                mptr->_11 * vertex->y +\n                mptr->_12 * vertex->z +\n                mptr->_13\n            );\n            double zv = (\n                mptr->_20 * vertex->x +\n                mptr->_21 * vertex->y +\n                mptr->_22 * vertex->z +\n                mptr->_23\n            );\n            // clang-format on\n\n            const int32_t x = ((int32_t)xv) >> W2V_SHIFT;\n            const int32_t y = ((int32_t)yv) >> W2V_SHIFT;\n            const int32_t z = ((int32_t)zv) >> W2V_SHIFT;\n\n            new_bounds.min.x = MIN(new_bounds.min.x, x);\n            new_bounds.min.y = MIN(new_bounds.min.y, y);\n            new_bounds.min.z = MIN(new_bounds.min.z, z);\n            new_bounds.max.x = MAX(new_bounds.max.x, x);\n            new_bounds.max.y = MAX(new_bounds.max.y, y);\n            new_bounds.max.z = MAX(new_bounds.max.z, z);\n        }\n    }\n\n    Matrix_Pop();\n    return new_bounds;\n}\n\nbool Object_DrawUnclippedItem(const ITEM *const item)\n{\n    const int32_t left = g_PhdLeft;\n    const int32_t top = g_PhdTop;\n    const int32_t right = g_PhdRight;\n    const int32_t bottom = g_PhdBottom;\n\n    g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME);\n    g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME);\n    g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME);\n    g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME);\n\n    Object_DrawAnimatingItem(item);\n\n    g_PhdLeft = left;\n    g_PhdTop = top;\n    g_PhdRight = right;\n    g_PhdBottom = bottom;\n    return true;\n}\n\nvoid Object_DrawMesh(\n    const int32_t mesh_idx, const CLIP clip, const bool interpolated)\n{\n    const OBJECT_MESH *const mesh = Object_GetMesh(mesh_idx);\n    if (interpolated) {\n        Output_DrawObjectMesh_I(mesh, clip);\n    } else {\n        Output_DrawObjectMesh(mesh, clip);\n    }\n}\n\nvoid Object_DrawStaticObject(\n    const OBJECT *const obj, const ANIM_FRAME *const frame)\n{\n    Matrix_Push();\n    Object_DrawMesh(obj->mesh_idx, 0, false);\n    for (int32_t i = 1; i < obj->mesh_count; i++) {\n        const ANIM_BONE *const bone = Object_GetBone(obj, i - 1);\n        if (bone->matrix_pop) {\n            Matrix_Pop();\n        }\n        if (bone->matrix_push) {\n            Matrix_Push();\n        }\n\n        Matrix_TranslateRel32(bone->pos);\n        Matrix_Rot16(frame->mesh_rots[i]);\n        Object_DrawMesh(obj->mesh_idx + i, 0, false);\n    }\n    Matrix_Pop();\n}\n\nbool Object_DrawAnimatingItem(const ITEM *item)\n{\n    return Object_DrawAnimatingItemWithSwap(item, nullptr);\n}\n\nbool Object_DrawAnimatingItemWithSwap(\n    const ITEM *const item, const OBJECT *const mesh_swap)\n{\n    ANIM_FRAME *frames[2];\n    int32_t rate;\n    const int32_t frac = Item_GetFrames(item, frames, &rate);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n\n    const OBJECT *swap_obj = mesh_swap;\n    if (swap_obj != nullptr && !swap_obj->loaded) {\n        swap_obj = nullptr;\n    }\n    if (swap_obj != nullptr) {\n        ASSERT(swap_obj->mesh_count == obj->mesh_count);\n    }\n\n    if (obj->shadow_size != 0) {\n        Output_DrawShadow(obj->shadow_size, bounds, item);\n    }\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n\n    const CLIP clip = Output_CheckBoundsClip(bounds);\n    if (clip == CLIP_NOT_VISIBLE) {\n        Matrix_Pop();\n        return false;\n    }\n\n    Output_CalculateObjectLighting(item, bounds);\n\n    const int16_t *extra_rotation = item->extra_rotations;\n\n    bool result = Object_DrawInterpolatedObjectWithSwap(\n        obj, item->mesh_bits, extra_rotation, frames[0], frames[1], frac, rate,\n        swap_obj);\n    if (g_Config.debug.enable_debug_bounding_boxes) {\n        Output_DrawCuboid(bounds);\n    }\n    Matrix_Pop();\n    return result;\n}\n\nbool Object_DrawInterpolatedObject(\n    const OBJECT *const obj, const uint32_t mesh_mask,\n    const int16_t *extra_rotation, const ANIM_FRAME *const frame1,\n    const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate)\n{\n    return Object_DrawInterpolatedObjectWithSwap(\n        obj, mesh_mask, extra_rotation, frame1, frame2, frac, rate, nullptr);\n}\n\nbool Object_DrawInterpolatedObjectWithSwap(\n    const OBJECT *const obj, const uint32_t mesh_mask,\n    const int16_t *extra_rotation, const ANIM_FRAME *const frame1,\n    const ANIM_FRAME *const frame2, const int32_t frac, const int32_t rate,\n    const OBJECT *const mesh_swap)\n{\n    if (frame1 == nullptr) {\n        return false;\n    }\n    ASSERT(frame1 != nullptr);\n    const CLIP clip = Output_CheckBoundsClip(&frame1->bounds);\n    if (clip == CLIP_NOT_VISIBLE) {\n        return false;\n    }\n\n    ASSERT(rate != 0);\n    Matrix_Push();\n\n    if (frac != 0) {\n        for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) {\n            if (mesh_idx == 0) {\n                Matrix_InitInterpolate(frac, rate);\n                Matrix_TranslateRel16_ID(frame1->offset, frame2->offset);\n                Matrix_Rot16_ID(\n                    frame1->mesh_rots[mesh_idx], frame2->mesh_rots[mesh_idx]);\n                Object_ApplyExtraRotation(&extra_rotation, obj->base_rot, true);\n            } else {\n                const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n                if (bone->matrix_pop) {\n                    Matrix_Pop_I();\n                }\n                if (bone->matrix_push) {\n                    Matrix_Push_I();\n                }\n\n                Matrix_TranslateRel32_I(bone->pos);\n                Matrix_Rot16_ID(\n                    frame1->mesh_rots[mesh_idx], frame2->mesh_rots[mesh_idx]);\n                Object_ApplyExtraRotation(&extra_rotation, bone->rot, true);\n            }\n\n            if ((mesh_mask & (1u << mesh_idx)) != 0) {\n                Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, true);\n            } else if (mesh_swap != nullptr) {\n                Object_DrawMesh(mesh_swap->mesh_idx + mesh_idx, clip, true);\n            }\n        }\n    } else {\n        for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) {\n            if (mesh_idx == 0) {\n                Matrix_TranslateRel16(frame1->offset);\n                Matrix_Rot16(frame1->mesh_rots[mesh_idx]);\n                Object_ApplyExtraRotation(\n                    &extra_rotation, obj->base_rot, false);\n            } else {\n                const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n                if (bone->matrix_pop) {\n                    Matrix_Pop();\n                }\n                if (bone->matrix_push) {\n                    Matrix_Push();\n                }\n\n                Matrix_TranslateRel32(bone->pos);\n                Matrix_Rot16(frame1->mesh_rots[mesh_idx]);\n                Object_ApplyExtraRotation(&extra_rotation, bone->rot, false);\n            }\n\n            if ((mesh_mask & (1u << mesh_idx)) != 0) {\n                Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, false);\n            } else if (mesh_swap != nullptr) {\n                Object_DrawMesh(mesh_swap->mesh_idx + mesh_idx, clip, false);\n            }\n        }\n    }\n\n    Matrix_Pop();\n    return true;\n}\n\nvoid Object_ApplyExtraRotation(\n    const int16_t **const extra_rotation, const XYZ_BOOL rot_flags,\n    const bool interpolated)\n{\n    const int16_t *rot_ptr = *extra_rotation;\n    if (rot_ptr == nullptr) {\n        return;\n    }\n\n#define APPLY_ROTATION(axis_, flag_)                                           \\\n    if (rot_flags.flag_) {                                                     \\\n        if (interpolated) {                                                    \\\n            Matrix_Rot##axis_##_I(*rot_ptr++);                                 \\\n        } else {                                                               \\\n            Matrix_Rot##axis_(*rot_ptr++);                                     \\\n        }                                                                      \\\n    }\n\n    APPLY_ROTATION(Y, y);\n    APPLY_ROTATION(X, x);\n    APPLY_ROTATION(Z, z);\n\n#undef APPLY_ROTATION\n    *extra_rotation = rot_ptr;\n}\n\nbool Object_DrawSpriteItem(const ITEM *const item)\n{\n    const RGB_F tint = Output_GetTint();\n    SHADE shade = item->shade;\n    if (shade.value_1 < 0) {\n        shade.value_1 = SHADE_NEUTRAL;\n    }\n\n    if (g_TRVersion > 1) {\n        Output_CalculateStaticMeshLight(\n            item->interp.result.pos, shade, Room_Get(item->room_num));\n        shade.value_1 = Output_GetLightAdder() + SHADE_NEUTRAL;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    Output_DrawSprite(\n        item->interp.result.pos.x, item->interp.result.pos.y,\n        item->interp.result.pos.z, obj->mesh_idx - item->frame_num,\n        shade.value_1, tint, DRAW_BLEND);\n    return true;\n}\n\nbool Object_DrawPickupItem(const ITEM *const item)\n{\n    if ((item->flags & IF_INVISIBLE) != 0) {\n        return false;\n    }\n\n    if (!g_Config.visuals.enable_3d_pickups\n        && Object_Get(item->object_id)->loaded) {\n        return Object_DrawSpriteItem(item);\n    }\n\n    // Convert item to menu display item.\n    const OBJECT *obj = Object_TryGet(Inv_GetItemPickup(item->object_id));\n    if (obj == nullptr || !obj->loaded || obj->mesh_count < 0) {\n        obj = Object_TryGet(Inv_GetItemOption(item->object_id));\n    }\n\n    if (obj == nullptr || !obj->loaded || obj->mesh_count < 0) {\n        return Object_DrawSpriteItem(item);\n    }\n\n    // Standardize the bounds and offsets of all pickup items, and handle cases\n    // such as the prayer wheels in Barkhang Monastery, which have no frames.\n    const BOUNDS_16 bounds = M_GetBoundingBox(obj, nullptr, item->mesh_bits);\n    XYZ_16 offset = {};\n\n    const XYZ_16 *mesh_rots = nullptr;\n    if (obj->anim_idx != NO_ANIM) {\n        const ANIM_FRAME *const frame = obj->frame_base;\n        mesh_rots = frame->mesh_rots;\n        offset = frame->offset;\n        if (Object_IsType(item->object_id, g_ElevatedPickupObjects)) {\n            offset.y = (frame->bounds.min.y - frame->offset.y) / 2;\n        } else {\n            offset.y -= frame->bounds.max.y;\n        }\n    } else {\n        offset.y = (bounds.max.y - bounds.min.y) / -2;\n    }\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n    Matrix_TranslateRel16(offset);\n\n    Output_CalculateLight(item->pos, item->room_num);\n\n    const CLIP clip = Output_CheckBoundsClip(&bounds);\n    if (clip != CLIP_NOT_VISIBLE) {\n        for (int32_t mesh_idx = 0; mesh_idx < obj->mesh_count; mesh_idx++) {\n            if (mesh_idx > 0) {\n                const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n                if (bone->matrix_pop) {\n                    Matrix_Pop();\n                }\n                if (bone->matrix_push) {\n                    Matrix_Push();\n                }\n\n                Matrix_TranslateRel32(bone->pos);\n            }\n\n            if (mesh_rots != nullptr) {\n                Matrix_Rot16(mesh_rots[mesh_idx]);\n            }\n\n            if ((item->mesh_bits & (1 << mesh_idx)) != 0) {\n                Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, false);\n            }\n        }\n    }\n\n    Matrix_Pop();\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/objects/draw.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/anims.h>\n#include <trx/game/collision.h>\n#include <trx/game/items.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/objects/types.h>\n\nbool Object_DrawUnclippedItem(const ITEM *item);\nvoid Object_DrawMesh(int32_t mesh_idx, CLIP clip, bool interpolated);\nbool Object_DrawSpriteItem(const ITEM *item);\nbool Object_DrawPickupItem(const ITEM *item);\nvoid Object_DrawStaticObject(const OBJECT *obj, const ANIM_FRAME *frame);\n\nbool Object_DrawAnimatingItem(const ITEM *item);\nbool Object_DrawAnimatingItemWithSwap(\n    const ITEM *item, const OBJECT *mesh_swap);\n\nbool Object_DrawInterpolatedObject(\n    const OBJECT *obj, uint32_t mesh_mask, const int16_t *extra_rotation,\n    const ANIM_FRAME *frame1, const ANIM_FRAME *frame2, int32_t frac,\n    int32_t rate);\nbool Object_DrawInterpolatedObjectWithSwap(\n    const OBJECT *obj, uint32_t mesh_mask, const int16_t *extra_rotation,\n    const ANIM_FRAME *frame1, const ANIM_FRAME *frame2, int32_t frac,\n    int32_t rate, const OBJECT *mesh_swap);\n\nvoid Object_ApplyExtraRotation(\n    const int16_t **extra_rotation, const XYZ_BOOL rot_flags,\n    bool interpolated);\n"
  },
  {
    "path": "src/trx/game/objects/effects/blood.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/version.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n    effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n    effect->counter++;\n    if (effect->counter == 4) {\n        effect->frame_num--;\n        effect->counter = 0;\n        if (effect->frame_num <= obj->mesh_count) {\n            Effect_Kill(effect_num);\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->semi_transparent = g_TRVersion >= 2;\n}\n\nREGISTER_OBJECT(O_BLOOD, M_Setup)\nREGISTER_OBJECT(O_BLOOD_PINK, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/body_part.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks/spawners.h>\n#include <trx/version.h>\n\nstatic void M_SpawnSplash(const GAME_VECTOR pos)\n{\n    const int16_t effect_num = Effect_Create(pos.room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->pos = pos.pos;\n        effect->rot.y = 0;\n        effect->speed = 0;\n        effect->frame_num = 0;\n        effect->object_id = O_SPLASH_1;\n    }\n}\n\nstatic void M_Control_TR12(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->rot.x += 5 * DEG_1;\n    effect->rot.z += 10 * DEG_1;\n    effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.y += effect->fall_speed;\n    effect->fall_speed += GRAVITY;\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n\n    const ROOM *const current_room = Room_Get(effect->room_num);\n    const ROOM *const next_room = Room_Get(room_num);\n    if (!current_room->flags.underwater && next_room->flags.underwater) {\n        M_SpawnSplash(\n            (GAME_VECTOR) { .pos = effect->pos, .room_num = effect->room_num });\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, effect->pos);\n    if (effect->pos.y < ceiling) {\n        effect->pos.y = ceiling;\n        effect->fall_speed = -effect->fall_speed;\n    }\n\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n    if (effect->pos.y >= height) {\n        if (effect->counter > 0) {\n            effect->speed = 0;\n            effect->frame_num = 0;\n            effect->counter = 0;\n            effect->object_id = O_EXPLOSION_1;\n            effect->shade = SHADE_NEUTRAL;\n            Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL);\n        } else {\n            Effect_Kill(effect_num);\n        }\n        return;\n    }\n\n    const int16_t counter_value =\n        (g_TRVersion == 1) ? ABS(effect->counter) : effect->counter;\n    const bool trigger_explosion =\n        (g_TRVersion == 1) ? (effect->counter > 0) : (effect->counter == 0);\n\n    if (Lara_IsNearItem(&effect->pos, counter_value * 2)) {\n        Lara_TakeDamage(counter_value, true);\n\n        if (trigger_explosion) {\n            effect->speed = 0;\n            effect->frame_num = 0;\n            effect->counter = 0;\n            effect->object_id = O_EXPLOSION_1;\n            effect->shade = SHADE_NEUTRAL;\n            Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL);\n\n            LARA_INFO *const lara = Lara_GetLaraInfo();\n            lara->hit_effect_count = 5;\n            lara->hit_effect = effect;\n        } else {\n            Effect_Kill(effect_num);\n        }\n    }\n\n    if (room_num != effect->room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n}\n\nstatic void M_Control_TR3(const int16_t effect_num)\n{\n    int32_t lp;\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->rot.x += 5 * DEG_1;\n    effect->rot.z += 10 * DEG_1;\n    effect->fall_speed += 3;\n    effect->pos.x +=\n        (effect->speed * Math_Sin(effect->rot.y)) >> (W2V_SHIFT + 2);\n    effect->pos.y += effect->fall_speed;\n    effect->pos.z +=\n        (effect->speed * Math_Cos(effect->rot.y)) >> (W2V_SHIFT + 2);\n\n    const int32_t time4 = (int32_t)Output_GetTimeInGame() * 4;\n    if (!(time4 & 0xC)) {\n        if (effect->counter & 1) {\n            Sparks_TriggerFireFlame(effect->pos, effect_num, 0);\n        }\n\n        if (effect->counter & 2) {\n            Sparks_TriggerFireSmoke(effect->pos, -1, 0);\n        }\n    }\n\n    int16_t room_num = effect->room_num;\n    SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    int32_t c = Room_GetCeiling(sector, effect->pos);\n\n    if (effect->pos.y < c) {\n        effect->pos.y = c;\n        effect->fall_speed = -effect->fall_speed;\n    }\n\n    int32_t h = Room_GetHeight(sector, effect->pos);\n\n    if (effect->pos.y >= h) {\n        if (effect->counter & 3) {\n            for (int32_t i = 0; i < 3; i++) {\n                if (effect->counter & 1) {\n                    Sparks_TriggerFireFlame(\n                        (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0);\n                }\n                if (effect->counter & 2) {\n                    Sparks_TriggerFireSmoke(\n                        (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0);\n                }\n            }\n            Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL);\n        }\n\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (Lara_IsNearItem(&effect->pos, effect->counter & ~3)) {\n        Lara_TakeDamage(effect->counter >> 2, true);\n\n        if (effect->counter & 3) {\n            for (int32_t i = 0; i < 3; i++) {\n                if (effect->counter & 1) {\n                    Sparks_TriggerFireFlame(\n                        (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0);\n                }\n                if (effect->counter & 2) {\n                    Sparks_TriggerFireSmoke(\n                        (XYZ_32) { effect->pos.x, h, effect->pos.z }, -1, 0);\n                }\n            }\n\n            Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL);\n        }\n\n        Effect_Kill(effect_num);\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = g_TRVersion == 3 ? M_Control_TR3 : M_Control_TR12;\n    obj->loaded = true;\n    obj->mesh_count = 0;\n}\n\nREGISTER_OBJECT(O_BODY_PART, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/bubble.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\nstatic void M_Control_TR1TR2(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->rot.y += 9 * DEG_1;\n    effect->rot.x += 13 * DEG_1;\n\n    const XYZ_32 pos = {\n        .x = effect->pos.x + ((Math_Sin(effect->rot.y) * 11) >> W2V_SHIFT),\n        .y = effect->pos.y - effect->speed,\n        .z = effect->pos.z + ((Math_Cos(effect->rot.x) * 8) >> W2V_SHIFT),\n    };\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    if (sector == nullptr || !Room_Get(room_num)->flags.underwater) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (ceiling == NO_HEIGHT || pos.y <= ceiling) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n    effect->pos = pos;\n}\n\nstatic void M_Control_TR3(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->rot.y += 9 * DEG_1;\n    effect->rot.x += 13 * DEG_1;\n    effect->speed += effect->fall_speed;\n\n    const XYZ_32 pos = {\n        .x = effect->pos.x + ((3 * Math_Sin(effect->rot.y)) >> W2V_SHIFT),\n        .y = effect->pos.y - ((int32_t)effect->speed >> 8),\n        .z = effect->pos.z + (Math_Cos(effect->rot.x) >> W2V_SHIFT),\n    };\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    if (sector == nullptr) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    const int32_t floor = Room_GetHeight(sector, pos);\n    if (pos.y > floor) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (!Room_Get(room_num)->flags.underwater) {\n        const ROOM *const old_room = Room_Get(effect->room_num);\n        FX_Water_SetupRipple(\n            effect->pos.x, old_room->max_ceiling, effect->pos.z,\n            -2 - (Random_GetControl() & 1), true);\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (ceiling == NO_HEIGHT || pos.y <= ceiling) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n\n    effect->pos = pos;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = g_TRVersion == 3 ? M_Control_TR3 : M_Control_TR1TR2;\n    if (obj->loaded) {\n        for (int32_t i = 0; i < -obj->mesh_count; i++) {\n            Output_GetSpriteTexture(obj->mesh_idx + i)->flags = VERT_ABS_SPRITE;\n        }\n    }\n}\n\nREGISTER_OBJECT(O_BUBBLE_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/dart_effect.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/version.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n\n    effect->counter++;\n    if (effect->counter >= 3) {\n        effect->frame_num--;\n        effect->counter = 0;\n        if (effect->frame_num <= obj->mesh_count) {\n            Effect_Kill(effect_num);\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = Object_DrawSpriteItem;\n    obj->semi_transparent = g_TRVersion >= 2;\n}\n\nREGISTER_OBJECT(O_DART_EFFECT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/ember.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\n#define M_RANGE 200\n#define M_DAMAGE 10\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->fall_speed += GRAVITY;\n    effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.y += effect->fall_speed;\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t ceiling = Room_GetCeiling(sector, effect->pos);\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n\n    if (effect->pos.y >= height || effect->pos.y < ceiling) {\n        Effect_Kill(effect_num);\n    } else if (Lara_IsNearItem(&effect->pos, M_RANGE)) {\n        Lara_TakeDamage(M_DAMAGE, true);\n        Effect_Kill(effect_num);\n    } else if (room_num != effect->room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->semi_transparent = g_TRVersion >= 2;\n}\n\nREGISTER_OBJECT(O_EMBER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/explosion.c",
    "content": "#include <trx/config.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/version.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n    effect->counter++;\n    if (effect->counter == 2) {\n        effect->frame_num--;\n        effect->counter = 0;\n        if (g_Config.visuals.enable_gun_lighting\n            && effect->frame_num > obj->mesh_count) {\n            Output_AddDynamicLight(effect->pos, 13, 11);\n        } else if (effect->frame_num <= obj->mesh_count) {\n            Effect_Kill(effect_num);\n        }\n    } else if (g_Config.visuals.enable_gun_lighting) {\n        Output_AddDynamicLight(effect->pos, 12, 10);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->semi_transparent = g_TRVersion >= 2;\n}\n\nREGISTER_OBJECT(O_EXPLOSION_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/flame.c",
    "content": "#include <trx/game/objects/effects/flame.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n#define M_LIGHT_INTENSITY 11\n#define M_LIGHT_FALLOFF 10\n#define M_DAMAGE_PROXIMITY 600\n#define M_IGNITE_PROXIMITY (g_TRVersion == 1 ? 300 : 450)\n#define M_TOO_NEAR_DAMAGE (g_TRVersion == 1 ? 3 : 5)\n#define M_ON_FIRE_DAMAGE (g_TRVersion == 1 ? 5 : 7)\n\nstatic const uint8_t m_TR3_XZOffsets[16][2] = {\n    { 9, 9 },   { 24, 9 },  { 40, 9 },  { 55, 9 },  { 9, 24 },  { 24, 24 },\n    { 40, 24 }, { 55, 24 }, { 9, 40 },  { 24, 40 }, { 40, 40 }, { 55, 40 },\n    { 9, 55 },  { 24, 55 }, { 40, 55 }, { 55, 55 },\n};\n\nstatic bool M_IsGreenAttachedFlame(const EFFECT *const effect)\n{\n    return effect->counter < 0 && effect->frame_num == FLAME_GREEN;\n}\n\nstatic void M_TR3_SideFlameDetection(\n    const EFFECT *const effect, const int32_t length)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - effect->pos.x;\n    const int32_t dz = lara_item->pos.z - effect->pos.z;\n\n    const int32_t max_dist = 20 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    const int32_t half_w = STEP_L;\n    const int32_t offset = WALL_L / 2;\n    int32_t origin_x = effect->pos.x;\n    int32_t origin_z = effect->pos.z;\n    int32_t xs = 0, xe = 0, zs = 0, ze = 0;\n\n    switch (effect->rot.y) {\n    case 0:\n        origin_z += offset;\n        xs = -half_w;\n        xe = half_w;\n        zs = -length;\n        ze = 0;\n        break;\n\n    case DEG_90:\n        origin_x += offset;\n        xs = -length;\n        xe = 0;\n        zs = -half_w;\n        ze = half_w;\n        break;\n\n    case -DEG_90:\n        origin_x -= offset;\n        xs = 0;\n        xe = length;\n        zs = -half_w;\n        ze = half_w;\n        break;\n\n    case -DEG_180:\n        origin_z -= offset;\n        xs = -half_w;\n        xe = half_w;\n        zs = 0;\n        ze = length;\n        break;\n\n    default:\n        return;\n    }\n\n    const int32_t min_x = origin_x + xs;\n    const int32_t max_x = origin_x + xe;\n    const int32_t min_z = origin_z + zs;\n    const int32_t max_z = origin_z + ze;\n\n    const BOUNDS_16 *const b = Item_GetBoundsAccurate(lara_item);\n    const int32_t lara_min_y = lara_item->pos.y + b->min.y;\n    const int32_t lara_max_y = lara_item->pos.y + b->max.y;\n    const int32_t fire_min_y = effect->pos.y - 384;\n    const int32_t fire_max_y = effect->pos.y + 128;\n\n    if (lara_item->pos.x < min_x || lara_item->pos.x > max_x\n        || lara_item->pos.z < min_z || lara_item->pos.z > max_z\n        || lara_min_y > fire_max_y || lara_max_y < fire_min_y) {\n        return;\n    }\n\n    if (effect->flag1 >= 18) {\n        Lara_CatchFire();\n    } else {\n        Lara_TakeDamage(M_TOO_NEAR_DAMAGE, true);\n    }\n}\n\nstatic inline XYZ_32 M_OffsetPos(\n    const XYZ_32 base_pos, const int32_t dx, const int32_t dy, const int32_t dz)\n{\n    return (XYZ_32) {\n        .x = base_pos.x + dx,\n        .y = base_pos.y + dy,\n        .z = base_pos.z + dz,\n    };\n}\n\nstatic void M_TR3_ControlBig(\n    EFFECT *const effect, const int32_t time4, const int32_t rnd)\n{\n    if ((time4 & 0xC) == 0) {\n        Sparks_TriggerFireFlame(effect->pos, -1, 0);\n        Sparks_TriggerFireSmoke(effect->pos, -1, 0);\n    }\n    Sparks_TriggerStaticFlame(effect->pos, (Random_GetControl() & 0xF) + 96);\n}\n\nstatic void M_TR3_ControlSmall(\n    EFFECT *const effect, const int32_t time4, const int32_t rnd)\n{\n    if (effect->counter >= 0) {\n        const int32_t angle = ((effect->rot.y >> 4) & 4095) << 1;\n        const int32_t s = (288 * Math_Sin(angle << 3)) >> W2V_SHIFT;\n        const int32_t c = (288 * Math_Cos(angle << 3)) >> W2V_SHIFT;\n\n        Sparks_TriggerStaticFlame(\n            M_OffsetPos(effect->pos, s, -192, c),\n            (Random_GetControl() & 15) + 32);\n\n        if ((time4 & 0x18) != 0) {\n            return;\n        }\n\n        Sparks_TriggerFireFlame(M_OffsetPos(effect->pos, s, -224, c), -1, 1);\n\n        if ((time4 & 0x18) == 0) {\n            Sparks_TriggerFireSmoke(M_OffsetPos(effect->pos, s, 0, c), -1, 1);\n        }\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    // Lara is on fire; attach flames to joints and apply damage unless\n    // water extinguishes it.\n    if (lara->water_status == LWS_CHEAT) {\n        effect->counter = 0;\n        Effect_Kill(Effect_GetIndex(effect));\n        lara->burn = 0;\n        return;\n    }\n\n    const bool is_green = M_IsGreenAttachedFlame(effect);\n    const int32_t flame_type = is_green ? 254 : 255;\n    for (int i = 0; i < LM_NUMBER_OF; i++) {\n        if ((time4 & 0xC) == 0) {\n            effect->pos.x = 0;\n            effect->pos.y = 0;\n            effect->pos.z = 0;\n            Collide_GetJointAbsPosition(lara_item, &effect->pos, i);\n            Sparks_TriggerFireFlame(effect->pos, -1, flame_type);\n        }\n    }\n\n    if (g_Config.visuals.enable_fire_lighting) {\n        RGB_888 color;\n        if (is_green) {\n            color.r = (rnd >> 2) & 0x3F;\n            color.g = (rnd & 0x3F) + 192;\n            color.b = ((rnd >> 4) & 0x1F) + 96;\n        } else {\n            color.r = (rnd & 0x3F) + 192;\n            color.g = ((rnd >> 4) & 0x1F) + 96;\n            color.b = 0;\n        }\n        Output_AddDynamicLightRGB(lara_item->pos, 13, color);\n    }\n\n    if (lara_item->room_num != effect->room_num) {\n        Effect_UpdateRoom(Effect_GetIndex(effect), lara_item->room_num);\n    }\n\n    const int32_t wh = Room_GetWaterHeight(effect->pos, effect->room_num);\n    if (wh == NO_HEIGHT || effect->pos.y <= wh\n        || (Room_Get(effect->room_num)->flags.swamp\n            && (GF_BadGetLevelNum() == 4 || GF_BadGetLevelNum() == 18\n                || GF_BadGetLevelNum() == 19))) {\n        Sound_Effect(SFX_LOOP_FOR_SMALL_FIRES, &effect->pos, SPM_NORMAL);\n        Lara_TakeDamage(M_ON_FIRE_DAMAGE, true);\n    } else {\n        effect->counter = 0;\n        Effect_Kill(Effect_GetIndex(effect));\n        lara->burn = false;\n    }\n}\n\nstatic void M_TR3_ControlJet(\n    EFFECT *const effect, const int32_t time4, const int32_t rnd)\n{\n    // Jets cycle between two spawn positions, chosen from a 16-cell grid.\n    if (effect->flag1 != 0) {\n        effect->flag1--;\n    } else {\n        effect->flag1 = (Random_GetControl() & 3) + 8;\n        int32_t new_index = Random_GetControl() & 0x3F;\n        if (effect->flag2 == new_index) {\n            new_index = (new_index + 13) & 0x3F;\n        }\n        effect->flag2 = (int16_t)new_index;\n    }\n\n    const int32_t idx0 = effect->flag2 & 7;\n    const int32_t idx1 = (effect->flag2 >> 3) + 8;\n    const int32_t x0 = (m_TR3_XZOffsets[idx0][0] << 4) - 512;\n    const int32_t z0 = (m_TR3_XZOffsets[idx0][1] << 4) - 512;\n    const int32_t x1 = (m_TR3_XZOffsets[idx1][0] << 4) - 512;\n    const int32_t z1 = (m_TR3_XZOffsets[idx1][1] << 4) - 512;\n    if ((time4 & 4) == 0) {\n        Sparks_TriggerFireFlame(M_OffsetPos(effect->pos, x0, 0, z0), -1, 2);\n    } else {\n        Sparks_TriggerFireFlame(M_OffsetPos(effect->pos, x1, 0, z1), -1, 2);\n    }\n}\n\nstatic void M_TR3_ControlSide(\n    EFFECT *const effect, const int32_t time4, const int32_t rnd)\n{\n    // Side flames: alternate between direction and length phases.\n    const int32_t dist = (rnd & 0xFF) + 512;\n    const int32_t angle = (effect->rot.y >> 3) & 0x1FFE;\n    const int32_t s = (dist * Math_Sin(angle << 3)) >> W2V_SHIFT;\n    const int32_t c = (dist * Math_Cos(angle << 3)) >> W2V_SHIFT;\n\n    if (effect->flag2 != 0) {\n        if ((time4 & 4) != 0) {\n            Sparks_TriggerSideFlame(\n                M_OffsetPos(effect->pos, s, 0, c),\n                ((angle - 4096) & 0x1FFF) << 3,\n                (!(Random_GetControl() & 7)) ? 1 : 0, 1);\n        }\n\n        effect->flag2--;\n    } else {\n        if (effect->flag1 != 0) {\n            if ((time4 & 4) != 0) {\n                int32_t size = 9;\n                if (effect->flag1 > 112) {\n                    size = (129 - effect->flag1) >> 1;\n                } else if (effect->flag1 < 18) {\n                    size = (effect->flag1 >> 1) + 1;\n                }\n\n                Sparks_TriggerSideFlame(\n                    M_OffsetPos(effect->pos, s, 0, c),\n                    ((angle + 4096) & 0x1FFE) << 3, size, 0);\n            }\n\n            effect->flag1 -= 2;\n        } else {\n            effect->flag1 = 128;\n\n            // TODO: do not hardcode this\n            if (GF_BadGetLevelNum() == 7) {\n                effect->flag2 = 120;\n            } else {\n                effect->flag2 = 60;\n            }\n        }\n    }\n}\n\nstatic void M_TR3_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const int32_t rnd = Random_GetControl();\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n\n    // Animation & particle logic.\n    if (effect->frame_num == FLAME_BIG) {\n        M_TR3_ControlBig(effect, time4, rnd);\n    } else if (\n        effect->frame_num == FLAME_SMALL || effect->frame_num == FLAME_GREEN) {\n        M_TR3_ControlSmall(effect, time4, rnd);\n    } else if (effect->frame_num == FLAME_JET) {\n        M_TR3_ControlJet(effect, time4, rnd);\n    } else {\n        M_TR3_ControlSide(effect, time4, rnd);\n    }\n\n    // Light & proximity damage logic (applies to most flame types).\n    const XYZ_32 light_pos = {\n        .x = effect->pos.x + ((rnd & 0xF) << 5),\n        .y = effect->pos.y + ((rnd & 0xF0) << 1),\n        .z = effect->pos.z + ((rnd >> 3) & 0x1E0),\n    };\n    RGB_888 color = {\n        .r = (rnd & 0x3F) + 192,\n        .g = ((rnd >> 4) & 0x1F) + 96,\n        .b = 0,\n    };\n\n    int32_t dist = 0;\n    if (effect->frame_num == FLAME_SIDE) {\n        if (effect->flag2 != 0) {\n            dist = 0;\n        } else if (effect->flag1 < 18) {\n            dist = 2048;\n        } else if (effect->flag1 < 64) {\n            dist = 2048;\n        } else {\n            dist = (128 - effect->flag1) << 5;\n        }\n    }\n\n    if (g_Config.visuals.enable_fire_lighting) {\n        if (effect->frame_num == FLAME_SIDE) {\n            const int16_t angle =\n                ((((effect->rot.y >> 3) & 0xFFFE) - 4096) & 0x1FFE) << 3;\n            Output_AddDynamicLightRGB(\n                XYZ_32_OffsetYaw(light_pos, angle, dist),\n                (effect->flag2 != 0) ? 6 : 13, color);\n        } else {\n            Output_AddDynamicLightRGB(\n                light_pos, 16 - (effect->frame_num << 2), color);\n        }\n    }\n\n    Sound_Effect(SFX_LOOP_FOR_SMALL_FIRES, &effect->pos, SPM_NORMAL);\n\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (effect->counter != 0) {\n        effect->counter--;\n    } else if (effect->frame_num == FLAME_SIDE) {\n        if (!lara->burn && dist) {\n            M_TR3_SideFlameDetection(effect, dist);\n        }\n    } else if (\n        effect->frame_num != FLAME_SMALL && effect->frame_num != FLAME_GREEN) {\n        if (Lara_IsNearItem(&effect->pos, M_DAMAGE_PROXIMITY)) {\n            const XYZ_32 delta = {\n                .x = lara_item->pos.x - effect->pos.x,\n                .y = 0,\n                .z = lara_item->pos.z - effect->pos.z,\n            };\n            dist = XYZ_32_GetLength2(delta);\n            Lara_TakeDamage(M_TOO_NEAR_DAMAGE, true);\n\n            if (dist < 202500) {\n                effect->counter = 100;\n                Lara_CatchFire();\n            }\n        }\n    }\n}\n\nstatic void M_TR12_DoEffects(const EFFECT *const effect)\n{\n    if (!Object_Get(O_FLAME)->loaded) {\n        return;\n    }\n\n    Sound_Effect(SFX_LOOP_FOR_SMALL_FIRES, &effect->pos, SPM_NORMAL);\n    if (!g_Config.visuals.enable_fire_lighting) {\n        return;\n    }\n\n    const int32_t random = Random_GetControl();\n    const XYZ_32 light_pos = {\n        .x = effect->pos.x + (random & 0x140) - 0xA0,\n        .y = effect->pos.y - STEP_L - (random & 0x50),\n        .z = effect->pos.z + (random & 0x140) - 0xA0,\n    };\n\n    if (random > 0x4000) {\n        Output_AddDynamicLight(light_pos, M_LIGHT_INTENSITY, M_LIGHT_FALLOFF);\n    } else if (random > 0x2000) {\n        Output_AddDynamicLight(\n            light_pos, M_LIGHT_INTENSITY - (random & 2), M_LIGHT_FALLOFF);\n    } else {\n        Output_AddDynamicLight(\n            light_pos, M_LIGHT_INTENSITY, M_LIGHT_FALLOFF / 2);\n    }\n}\n\nstatic void M_TR12_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    effect->frame_num--;\n    if (effect->frame_num <= Object_Get(O_FLAME)->mesh_count) {\n        effect->frame_num = 0;\n    }\n\n    if (effect->counter >= 0) {\n        M_TR12_DoEffects(effect);\n        if (effect->counter != 0) {\n            effect->counter--;\n        } else if (Lara_IsNearItem(&effect->pos, M_DAMAGE_PROXIMITY)) {\n            Lara_TakeDamage(M_TOO_NEAR_DAMAGE, true);\n            const int32_t dx = lara_item->pos.x - effect->pos.x;\n            const int32_t dz = lara_item->pos.z - effect->pos.z;\n            const int32_t dist = SQUARE(dx) + SQUARE(dz);\n            if (dist < SQUARE(M_IGNITE_PROXIMITY)) {\n                effect->counter = 100;\n                Lara_CatchFire();\n            }\n        }\n    } else {\n        effect->pos.x = 0;\n        effect->pos.y = 0;\n        if (effect->counter == -1) {\n            effect->pos.z = -100;\n        } else {\n            effect->pos.z = 0;\n        }\n\n        Collide_GetJointAbsPosition(\n            lara_item, &effect->pos, -1 - effect->counter);\n        const int16_t room_num = lara_item->room_num;\n        if (room_num != effect->room_num) {\n            Effect_UpdateRoom(effect_num, room_num);\n        }\n\n        const int32_t water_height =\n            Room_GetWaterHeight(effect->pos, effect->room_num);\n        if ((water_height != NO_HEIGHT && effect->pos.y > water_height)\n            || lara_info->water_status == LWS_CHEAT) {\n            effect->counter = 0;\n            Effect_Kill(Effect_GetIndex(effect));\n            lara_info->burn = false;\n        } else {\n            M_TR12_DoEffects(effect);\n            Lara_TakeDamage(M_ON_FIRE_DAMAGE, false);\n            lara_info->burn = true;\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = g_TRVersion == 3 ? M_TR3_Control : M_TR12_Control;\n    obj->semi_transparent = true;\n}\n\nREGISTER_OBJECT(O_FLAME, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/flame.h",
    "content": "#pragma once\n\ntypedef enum {\n    FLAME_BIG,\n    FLAME_SMALL,\n    FLAME_JET,\n    FLAME_SIDE,\n    FLAME_GREEN,\n} FLAME_TYPE;\n"
  },
  {
    "path": "src/trx/game/objects/effects/glow.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n\n    effect->counter--;\n    if (effect->counter == 0) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    effect->shade += effect->speed;\n    effect->frame_num += effect->fall_speed;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nREGISTER_OBJECT(O_GLOW, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/gun_flash.c",
    "content": "#include <trx/config.h>\n#include <trx/core/colors.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n\n    effect->counter--;\n    if (effect->counter == 0) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    effect->rot.z = Random_GetControl();\n    if (g_Config.visuals.enable_gun_lighting) {\n        if (g_TRVersion >= 3) {\n            Output_AddDynamicLightRGB(\n                effect->pos, 12, (RGB_888) { 192, 144, 0 });\n        } else {\n            Output_AddDynamicLight(effect->pos, 12, 11);\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nREGISTER_OBJECT(O_GUN_FLASH, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/gun_shell.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const int32_t old_x = effect->pos.x;\n    const int32_t old_y = effect->pos.y;\n    const int32_t old_z = effect->pos.z;\n\n    effect->fall_speed += 6;\n    effect->rot.x += 182 * ((effect->speed >> 1) + 7);\n    effect->rot.y += 182 * effect->speed;\n    effect->rot.z += 4186;\n\n    effect->pos.x +=\n        (effect->speed * Math_Sin(effect->flag1)) >> (W2V_SHIFT + 1);\n    effect->pos.y += effect->fall_speed;\n    effect->pos.z +=\n        (effect->speed * Math_Cos(effect->flag1)) >> (W2V_SHIFT + 1);\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n\n    const ROOM *const room = Room_Get(room_num);\n    if (room->flags.underwater) {\n        FX_Water_SetupRipple(\n            effect->pos.x, room->max_ceiling, effect->pos.z,\n            -8 - (Random_GetControl() & 3), true);\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, effect->pos);\n    if (effect->pos.y < ceiling) {\n        Sound_Effect(SFX_LARA_SHOTGUN_SHELL, &effect->pos, SPM_NORMAL);\n        effect->speed -= 4;\n        effect->counter--;\n\n        if (effect->counter < 0 || effect->speed < 8) {\n            Effect_Kill(effect_num);\n            return;\n        }\n\n        effect->fall_speed = -effect->fall_speed;\n        effect->pos.y = ceiling;\n    }\n\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n    if (effect->pos.y >= height) {\n        Sound_Effect(SFX_LARA_SHOTGUN_SHELL, &effect->pos, SPM_NORMAL);\n        effect->speed -= 8;\n        effect->counter--;\n\n        if (effect->counter < 0 || effect->speed < 8) {\n            Effect_Kill(effect_num);\n            return;\n        }\n\n        if (old_y > height) {\n            effect->flag1 += 0x8000;\n            effect->pos.x = old_x;\n            effect->pos.z = old_z;\n        } else {\n            effect->fall_speed = -effect->fall_speed >> 1;\n        }\n\n        effect->pos.y = old_y;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->mesh_count = 0;\n}\n\nREGISTER_OBJECT(O_GUN_SHELL, M_Setup)\nREGISTER_OBJECT(O_SHOTGUN_SHELL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/hot_liquid.c",
    "content": "#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    OBJECT *const obj = Object_Get(O_HOT_LIQUID);\n\n    effect->frame_num--;\n    if (effect->frame_num <= obj->mesh_count) {\n        effect->frame_num = 0;\n    }\n\n    effect->pos.y += effect->fall_speed;\n    effect->fall_speed += GRAVITY;\n\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n\n    if (effect->pos.y >= height) {\n        Sound_Effect(SFX_WATERFALL_2, &effect->pos, SPM_NORMAL);\n        effect->object_id = O_SPLASH_1;\n        effect->pos.y = height;\n        effect->rot.y = 2 * Random_GetDraw();\n        effect->fall_speed = 0;\n        effect->speed = 50;\n        return;\n    }\n\n    if (effect->room_num != room_num) {\n        Effect_UpdateRoom(effect_num, room_num);\n    }\n    Sound_Effect(SFX_BOWL_POUR, &effect->pos, SPM_NORMAL);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->semi_transparent = true;\n}\n\nREGISTER_OBJECT(O_HOT_LIQUID, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/missile.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_SHARD_DAMAGE 30\n#define M_EXPLOSION_DAMAGE 100\n#define M_EXPLOSION_RANGE_BASE WALL_L\n#define M_EXPLOSION_RANGE SQUARE(M_EXPLOSION_RANGE_BASE) // = 1048576\n\nstatic void M_Move(EFFECT *const effect)\n{\n    effect->pos.y += (effect->speed * Math_Sin(-effect->rot.x)) >> W2V_SHIFT;\n    const int32_t speed =\n        (effect->speed * Math_Cos(effect->rot.x)) >> W2V_SHIFT;\n    effect->pos.z += (speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.x += (speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n}\n\nstatic bool M_HitFloorOrCeiling(EFFECT *const effect)\n{\n    int16_t room_num = effect->room_num;\n    const SECTOR *const sector = Room_GetSector(effect->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, effect->pos);\n    const int32_t ceiling = Room_GetCeiling(sector, effect->pos);\n    if (effect->pos.y >= height || effect->pos.y <= ceiling) {\n        return true;\n    }\n    if (room_num != effect->room_num) {\n        Effect_UpdateRoom(Effect_GetIndex(effect), room_num);\n    }\n    return false;\n}\n\nstatic void M_ConvertToRicochet(\n    EFFECT *const effect, const SAMPLE_TRX_ID sample_id)\n{\n    effect->object_id = O_RICOCHET;\n    effect->frame_num = -Random_GetControl() / 11000;\n    effect->speed = 0;\n    effect->counter = 6;\n    Sound_Effect(sample_id, &effect->pos, SPM_NORMAL);\n}\n\nstatic void M_ConvertToBlood(\n    EFFECT *const effect, const SAMPLE_TRX_ID sample_id)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    Spawn_Blood(\n        effect->pos.x, effect->pos.y, effect->pos.z, lara_item->speed,\n        lara_item->rot.y, lara_item->room_num);\n    Effect_Kill(Effect_GetIndex(effect));\n    Sound_Effect(sample_id, &effect->pos, SPM_NORMAL);\n}\n\nstatic void M_ConvertToExplosion(EFFECT *const effect)\n{\n    effect->object_id = O_EXPLOSION_1;\n    effect->frame_num = 0;\n    effect->speed = 0;\n    effect->counter = 0;\n    Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL);\n}\n\nstatic void M_BlastDamage(EFFECT *const effect)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    const int32_t x = effect->pos.x - lara_item->pos.x;\n    const int32_t y = effect->pos.y - lara_item->pos.y;\n    const int32_t z = effect->pos.z - lara_item->pos.z;\n    if (Item_Test3DRange(x, y, z, M_EXPLOSION_RANGE_BASE)) {\n        const int32_t range = SQUARE(x) + SQUARE(y) + SQUARE(z);\n        Lara_TakeDamage(\n            M_EXPLOSION_DAMAGE * (M_EXPLOSION_RANGE - range)\n                / M_EXPLOSION_RANGE,\n            true);\n    }\n}\n\nstatic void M_ControlAtlanteanShard(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n\n    M_Move(effect);\n\n    if (M_HitFloorOrCeiling(effect)) {\n        M_ConvertToRicochet(effect, SFX_LARA_RICOCHET);\n        return;\n    }\n\n    if (!Lara_IsNearItem(&effect->pos, 200)) {\n        return;\n    }\n\n    Lara_TakeDamage(M_SHARD_DAMAGE, true);\n    M_ConvertToBlood(effect, SFX_LARA_BULLETHIT);\n}\n\nstatic void M_ControlAtlanteanBomb(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n\n    M_Move(effect);\n\n    if (M_HitFloorOrCeiling(effect)) {\n        M_ConvertToExplosion(effect);\n        M_BlastDamage(effect);\n        return;\n    }\n\n    if (!Lara_IsNearItem(&effect->pos, 200)) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    M_ConvertToExplosion(effect);\n    effect->rot.y = lara_item->rot.y;\n    effect->speed = lara_item->speed;\n    Lara_TakeDamage(M_EXPLOSION_DAMAGE, true);\n    if (lara_item->hit_points > 0) {\n        Sound_Effect(SFX_LARA_INJURY, &lara_item->pos, SPM_NORMAL);\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->hit_effect = effect;\n        lara->hit_effect_count = 5;\n    }\n}\n\nstatic void M_ControlPoison(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n\n    M_Move(effect);\n    if (M_HitFloorOrCeiling(effect)) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (Lara_IsNearItem(&effect->pos, 350)) {\n        LARA_INFO *const lara_info = Lara_GetLaraInfo();\n        lara_info->poison_timer += 4;\n    }\n\n    if (effect->counter == 0) {\n        Effect_Kill(effect_num);\n    }\n    effect->counter--;\n}\n\nstatic void M_ControlFlame(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n\n    M_Move(effect);\n    if (M_HitFloorOrCeiling(effect)) {\n        if (g_TRVersion == 3) {\n            Sparks_TriggerFlamethrowerHitFlame(effect->pos);\n            const RGB_888 color = { 255, 192, Random_GetControl() & 0x3F };\n            Output_AddDynamicLightRGB(effect->pos, 24, color);\n        } else {\n            Output_AddDynamicLight(effect->pos, 14, 11);\n        }\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (g_TRVersion == 3 && Room_Get(effect->room_num)->flags.underwater) {\n        if (Random_GetControl() & 1) {\n            Sparks_TriggerFlamethrowerSmoke(effect->pos, true);\n        }\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    if (Lara_IsNearItem(&effect->pos, 350)) {\n        Lara_TakeDamage(3, true);\n        Lara_CatchFire();\n        return;\n    }\n\n    if (effect->counter == 0) {\n        if (g_TRVersion < 3) {\n            Output_AddDynamicLight(effect->pos, 14, 11);\n            Sound_Effect(SFX_DRAGON_FIRE, &effect->pos, SPM_NORMAL);\n        }\n        Effect_Kill(effect_num);\n    }\n    effect->counter--;\n}\n\nstatic void M_ControlHarpoon(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    M_Move(effect);\n    if (M_HitFloorOrCeiling(effect)) {\n        M_ConvertToRicochet(effect, SFX_PROJECTILE_HIT);\n        return;\n    }\n\n    if (!Room_Get(effect->room_num)->flags.underwater) {\n        if (effect->rot.x > -0x3000) {\n            effect->rot.x -= DEG_1;\n        }\n    }\n\n    if (Lara_IsNearItem(&effect->pos, 200)) {\n        Lara_TakeDamage(50, true);\n        M_ConvertToBlood(effect, SFX_CRUNCH_1);\n    }\n\n    if (Room_Get(effect->room_num)->flags.underwater) {\n        if (g_TRVersion == 3) {\n            const int32_t time4 = Output_GetTimeInGame() * 4;\n            if ((time4 & 0xF) == 0) {\n                Spawn_BubbleEx(&effect->pos, effect->room_num, 8, 8);\n            }\n            Sparks_TriggerRocketSmoke(effect->pos, 64, effect->room_num);\n        } else {\n            Spawn_Bubble(&effect->pos, effect->room_num);\n        }\n    }\n}\n\nstatic void M_ControlKnife(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    M_Move(effect);\n    if (M_HitFloorOrCeiling(effect)) {\n        M_ConvertToRicochet(effect, SFX_PROJECTILE_HIT);\n        return;\n    }\n\n    if (Lara_IsNearItem(&effect->pos, 200)) {\n        Lara_TakeDamage(50, true);\n        M_ConvertToBlood(effect, SFX_CRUNCH_1);\n    }\n\n    effect->rot.z += 30 * DEG_1;\n}\n\nstatic void M_SetupAtlanteanBomb(OBJECT *const obj)\n{\n    obj->control_func = M_ControlAtlanteanBomb;\n    obj->save_position = true;\n}\n\nstatic void M_SetupAtlanteanShard(OBJECT *const obj)\n{\n    obj->control_func = M_ControlAtlanteanShard;\n    obj->save_position = true;\n}\n\nstatic void M_SetupFlame(OBJECT *const obj)\n{\n    obj->control_func = M_ControlFlame;\n    obj->save_position = true;\n}\n\nstatic void M_SetupPoison(OBJECT *const obj)\n{\n    obj->control_func = M_ControlPoison;\n    obj->save_position = true;\n}\n\nstatic void M_SetupHarpoon(OBJECT *const obj)\n{\n    obj->control_func = M_ControlHarpoon;\n    obj->save_position = true;\n}\n\nstatic void M_SetupKnife(OBJECT *const obj)\n{\n    obj->control_func = M_ControlKnife;\n    obj->save_position = true;\n}\n\nREGISTER_OBJECT(O_MISSILE_ATLANTEAN_SHARD, M_SetupAtlanteanShard)\nREGISTER_OBJECT(O_MISSILE_ATLANTEAN_BOMB, M_SetupAtlanteanBomb)\nREGISTER_OBJECT(O_MISSILE_FLAME, M_SetupFlame)\nREGISTER_OBJECT(O_MISSILE_POISON, M_SetupPoison)\nREGISTER_OBJECT(O_MISSILE_HARPOON, M_SetupHarpoon)\nREGISTER_OBJECT(O_MISSILE_KNIFE, M_SetupKnife)\n"
  },
  {
    "path": "src/trx/game/objects/effects/pickup_aid.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects/common.h>\n\nstatic void M_Control(int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->counter++;\n    if (effect->counter == 1) {\n        effect->counter = 0;\n        effect->frame_num--;\n        if (effect->frame_num <= Object_Get(effect->object_id)->mesh_count) {\n            Effect_Kill(effect_num);\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nREGISTER_OBJECT(O_PICKUP_AID, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/ricochet.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->counter--;\n    if (effect->counter == 0) {\n        Effect_Kill(effect_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nREGISTER_OBJECT(O_RICOCHET, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/snow_sprite.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n\n    effect->frame_num--;\n    if (effect->frame_num <= obj->mesh_count) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n    if (effect->fall_speed != 0) {\n        effect->pos.y += effect->fall_speed;\n        effect->fall_speed += GRAVITY;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nREGISTER_OBJECT(O_SNOW_SPRITE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/splash.c",
    "content": "#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/version.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n\n    effect->frame_num--;\n    if (effect->frame_num <= obj->mesh_count) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->semi_transparent = g_TRVersion >= 2;\n}\n\nREGISTER_OBJECT(O_SPLASH_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/twinkle.c",
    "content": "#include <trx/game/objects/effects/twinkle.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/collision.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n#define M_DISAPPEAR_RANGE STEP_L\n\nstatic void M_SpawnTwinkle(const GAME_VECTOR *const pos)\n{\n    const int16_t effect_num = Effect_Create(pos->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->pos = pos->pos;\n        effect->counter = Object_Get(O_TWINKLE)->mesh_count;\n        effect->object_id = O_TWINKLE;\n        effect->frame_num = 0;\n    }\n}\n\nstatic XYZ_32 M_GetTargetPos(const ITEM *const item)\n{\n    XYZ_32 pos = item->pos;\n    if (item->object_id == O_DRAGON_FRONT) {\n        const int32_t c = Math_Cos(item->rot.y);\n        const int32_t s = Math_Sin(item->rot.y);\n        pos.x += (c * 1100 + s * 490) >> W2V_SHIFT;\n        pos.z += (c * 490 - s * 1100) >> W2V_SHIFT;\n        pos.y -= 540;\n    }\n    return pos;\n}\n\nstatic void M_NudgeTowardsItem(\n    EFFECT *const effect, const XYZ_32 *const target_pos)\n{\n    effect->pos.x += (target_pos->x - effect->pos.x) >> 4;\n    effect->pos.y += (target_pos->y - effect->pos.y) >> 4;\n    effect->pos.z += (target_pos->z - effect->pos.z) >> 4;\n}\n\nstatic bool M_ShouldDisappear(\n    const EFFECT *const effect, const XYZ_32 *const target_pos)\n{\n    const int32_t dx = ABS(effect->pos.x - target_pos->x);\n    const int32_t dy = ABS(effect->pos.y - target_pos->y);\n    const int32_t dz = ABS(effect->pos.z - target_pos->z);\n    return dx < M_DISAPPEAR_RANGE && dy < M_DISAPPEAR_RANGE\n        && dz < M_DISAPPEAR_RANGE;\n}\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n\n    effect->frame_num--;\n    if (effect->frame_num <= obj->mesh_count) {\n        effect->frame_num = 0;\n    }\n\n    if (effect->counter < 0) {\n        effect->counter++;\n        if (effect->counter == 0) {\n            Effect_Kill(effect_num);\n        }\n        return;\n    }\n\n    const ITEM *const item = Item_Get(effect->counter);\n    const XYZ_32 target_pos = M_GetTargetPos(item);\n    M_NudgeTowardsItem(effect, &target_pos);\n    if (M_ShouldDisappear(effect, &target_pos)) {\n        Effect_Kill(effect_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n}\n\nvoid Twinkle_SparkleItem(const ITEM *const item, const uint32_t mesh_mask)\n{\n    SPHERE spheres[34];\n    const int32_t num_spheres = Collide_GetSpheres(item, spheres, true);\n\n    GAME_VECTOR effect_pos = {\n        .pos = {},\n        .room_num = item->room_num,\n    };\n\n    for (int32_t i = 0; i < num_spheres; i++) {\n        if ((mesh_mask & (1 << i)) == 0) {\n            continue;\n        }\n        const SPHERE *const sphere = &spheres[i];\n        effect_pos.x =\n            sphere->pos.x + sphere->r * (Random_GetDraw() - 0x4000) / 0x4000;\n        effect_pos.y =\n            sphere->pos.y + sphere->r * (Random_GetDraw() - 0x4000) / 0x4000;\n        effect_pos.z =\n            sphere->pos.z + sphere->r * (Random_GetDraw() - 0x4000) / 0x4000;\n        M_SpawnTwinkle(&effect_pos);\n    }\n}\n\nREGISTER_OBJECT(O_TWINKLE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/effects/twinkle.h",
    "content": "#pragma once\n\n#include <trx/game/items.h>\n\nvoid Twinkle_SparkleItem(const ITEM *item, uint32_t mesh_mask);\n"
  },
  {
    "path": "src/trx/game/objects/effects/water_sprite.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n\nstatic void M_Control(const int16_t effect_num)\n{\n    EFFECT *const effect = Effect_Get(effect_num);\n    const OBJECT *const obj = Object_Get(effect->object_id);\n\n    effect->counter--;\n    if (effect->counter % 4 == 0) {\n        effect->frame_num--;\n        if (effect->frame_num <= obj->mesh_count) {\n            effect->frame_num = 0;\n        }\n    }\n\n    if (effect->counter == 0 || effect->fall_speed > 0) {\n        Effect_Kill(effect_num);\n        return;\n    }\n\n    effect->pos.x += (effect->speed * Math_Sin(effect->rot.y)) >> W2V_SHIFT;\n    effect->pos.z += (effect->speed * Math_Cos(effect->rot.y)) >> W2V_SHIFT;\n    if (effect->fall_speed != 0) {\n        effect->pos.y += effect->fall_speed;\n        effect->fall_speed += GRAVITY;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->semi_transparent = true;\n}\n\nREGISTER_OBJECT(O_WATER_SPRITE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/ai_node.c",
    "content": "#include <trx/game/objects.h>\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->draw_func = nullptr;\n    obj->hit_points = 0;\n}\n\nREGISTER_OBJECT(O_AI_AMBUSH, M_Setup)\nREGISTER_OBJECT(O_AI_GUARD, M_Setup)\nREGISTER_OBJECT(O_AI_FOLLOW, M_Setup)\nREGISTER_OBJECT(O_AI_PATROL_1, M_Setup)\nREGISTER_OBJECT(O_AI_PATROL_2, M_Setup)\nREGISTER_OBJECT(O_AI_MODIFY, M_Setup)\nREGISTER_OBJECT(O_AI_X1, M_Setup)\nREGISTER_OBJECT(O_AI_X2, M_Setup)\nREGISTER_OBJECT(O_AI_X3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/alarm_sound.c",
    "content": "#include <trx/game/items.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/sound.h>\n\ntypedef struct {\n    int32_t counter;\n} M_PRIV;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) {\n        return;\n    }\n\n    Sound_Effect(SFX_PLATFORM_ALARM, &item->pos, SPM_NORMAL);\n\n    M_PRIV *const p = item->priv;\n    p->counter++;\n    if (p->counter > 6) {\n        Output_AddDynamicLight(item->pos, 12, 11);\n        if (p->counter > 12) {\n            p->counter = 0;\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_ALARM_SOUND, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/animating.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_ANIMATING_1, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_2, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_3, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_4, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_5, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_6, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_7, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_8, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_9, M_Setup)\nREGISTER_OBJECT(O_ANIMATING_10, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/area_51_rocket.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\n#define M_SMOKE_END 512\n\ntypedef enum {\n    M_SUPPORT_STATE_WAIT = 1,\n    M_SUPPORT_STATE_FALL = 2,\n} M_SUPPORT_STATE;\n\ntypedef struct {\n    int16_t fire_room_num;\n} M_PRIV;\n\nstatic bool m_SupportFallen = false;\n\nstatic bool M_DoesRoomLeadToItemCeilingPos(\n    const ITEM *const item, const int16_t room_num)\n{\n    const SECTOR *const sector =\n        Room_GetWorldSector(Room_Get(room_num), item->pos.x, item->pos.z);\n    return sector->portal_room.sky == item->room_num;\n}\n\nstatic int16_t M_FindFireRoomNum(const ITEM *const item)\n{\n    int16_t fire_room_num = item->room_num;\n    const XYZ_32 probe_pos = {\n        .x = item->pos.x,\n        .y = item->pos.y + WALL_L,\n        .z = item->pos.z,\n    };\n    const int16_t room_num = Room_GetIndexFromPos(probe_pos);\n    if (room_num != NO_ROOM && room_num != fire_room_num\n        && M_DoesRoomLeadToItemCeilingPos(item, room_num)) {\n        fire_room_num = room_num;\n    }\n    return fire_room_num;\n}\n\nstatic void M_InitialiseMain(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->priv == nullptr) {\n        item->priv = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA);\n    }\n\n    M_PRIV *const p = item->priv;\n    p->fire_room_num = M_FindFireRoomNum(item);\n}\n\nstatic int32_t M_GetFloorY(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    if (p != nullptr && p->fire_room_num != NO_ROOM) {\n        return Room_Get(p->fire_room_num)->min_floor;\n    }\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    return Room_Get(room_num)->min_floor;\n}\n\nstatic void M_TriggerBlastFire(\n    const XYZ_32 pos, const bool smoke, const int32_t end)\n{\n    SPARK *const spark = end < 0 ? Sparks_GetFreeSpark() : Sparks_GetSpark(end);\n    spark->on = true;\n\n    if (smoke) {\n        spark->src_color.r = 0;\n        spark->src_color.g = 0;\n        spark->src_color.b = 0;\n        spark->dst_color.r = 64;\n        spark->dst_color.g = 64;\n        spark->dst_color.b = 64;\n    } else {\n        spark->src_color.r = (Random_GetControl() & 0x1F) + 128;\n        spark->src_color.g = (Random_GetControl() & 0x1F) + 64;\n        spark->src_color.b = 32;\n        spark->dst_color.r = (Random_GetControl() & 0x1F) + 224;\n        spark->dst_color.g = (Random_GetControl() & 0x1F) + 160;\n        spark->dst_color.b = 32;\n    }\n\n    spark->col_fade_speed = 16;\n\n    if (end) {\n        spark->fade_to_black = (Random_GetControl() & 0x1F) + 32;\n        spark->life = (end >> 1) + 72;\n    } else {\n        spark->fade_to_black = smoke ? 32 : 8;\n        spark->life = (smoke ? 32 : 0) + (Random_GetControl() & 7) + 32;\n    }\n\n    spark->s_life = spark->life;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -(Random_GetControl() & 7);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n\n    spark->friction = 4;\n    spark->flags =\n        SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->rot_angle = Random_GetControl() & 0xFFF;\n\n    if (Random_GetControl() & 1) {\n        spark->rot_add = -16 - (Random_GetControl() & 0xF);\n    } else {\n        spark->rot_add = (Random_GetControl() & 0xF) + 16;\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 4;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n\n    const int32_t size = (Random_GetControl() & 0x3F) + 64;\n    if (end) {\n        spark->size.width = size;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = spark->size.width;\n\n        spark->size.height = spark->size.width;\n        spark->src_size.height = spark->size.height;\n        spark->dst_size.height = spark->size.height;\n    } else {\n        spark->dst_size.width = size;\n        spark->size.width = spark->dst_size.width >> 1;\n        spark->src_size.width = spark->size.width;\n\n        spark->dst_size.height = spark->dst_size.width;\n        spark->size.height = spark->dst_size.height >> 1;\n        spark->src_size.height = spark->size.height;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerRocketSmoke(\n    const XYZ_32 pos, const int32_t yv, const bool fire)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    if (fire) {\n        spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n        spark->src_color.g = spark->src_color.r;\n        spark->src_color.b = (Random_GetControl() & 0x3F) + 192;\n        spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n        spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n        spark->dst_color.b = 32;\n    } else {\n        spark->src_color.r = 0;\n        spark->src_color.g = 0;\n        spark->src_color.b = 0;\n        spark->dst_color.r = (yv >> 5) + 32;\n        spark->dst_color.g = spark->dst_color.r;\n        spark->dst_color.b = spark->dst_color.r;\n    }\n\n    spark->col_fade_speed = 16 - (fire ? yv >> 9 : 0);\n    spark->fade_to_black = !fire ? 16 : 0;\n    spark->life = (Random_GetControl() & 3) - (fire ? yv >> 8 : 0) + 60;\n    spark->s_life = spark->life;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = yv + (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 4;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n\n    int32_t size = (Random_GetControl() & 0x3F) + (yv >> 5) + 64;\n    CLAMPG(size, 255);\n\n    spark->dst_size.width = (uint8_t)size;\n    spark->size.width = spark->dst_size.width >> 2;\n    spark->src_size.width = spark->size.width;\n\n    spark->dst_size.height = spark->dst_size.width;\n    spark->size.height = spark->dst_size.height >> 2;\n    spark->src_size.height = spark->size.height;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_ControlRocket(ITEM *const item)\n{\n    if (item->required_anim_state < M_SMOKE_END) {\n        item->required_anim_state += 8;\n\n        if (item->required_anim_state < M_SMOKE_END) {\n            Sound_Effect(SFX_LARA_FLARE_BURN, nullptr, SPM_NORMAL);\n            item->current_anim_state = 0;\n            item->goal_anim_state = 0;\n        } else {\n            item->required_anim_state += 2048;\n            item->goal_anim_state = 64;\n            Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL);\n        }\n    } else {\n        if (item->current_anim_state) {\n            Sound_Effect(SFX_HUGE_ROCKET_LOOP, nullptr, SPM_NORMAL);\n            item->required_anim_state += 32;\n\n            if (item->required_anim_state > 16000) {\n                Item_Kill(Item_GetIndex(item));\n            } else {\n                const int32_t base = 0x4000 - item->required_anim_state;\n                const RGB_888 color = {\n                    .r = (base * ((Random_GetControl() & 0x1F) + 224)) >> 12,\n                    .g = (base * ((Random_GetControl() & 0x3F) + 96)) >> 12,\n                    .b = (base * (Random_GetControl() & 0x1F)) >> 12,\n                };\n                Output_AddDynamicLightRGB(\n                    (XYZ_32) {\n                        .x = item->pos.x - 7680,\n                        .y = M_GetFloorY(item) - (Random_GetControl() & 0x1FF)\n                            - 256,\n                        .z = item->pos.z - 1024,\n                    },\n                    24, color);\n                g_Camera.bounce = -((0x4000 - item->required_anim_state) >> 6);\n            }\n\n            return;\n        }\n\n        if (!Lara_GetLaraInfo()->burn) {\n            int32_t rad = item->goal_anim_state;\n            CLAMPG(rad, 8192);\n\n            const ITEM *const lara_item = Lara_GetItem();\n            if (lara_item->pos.x > item->pos.x - rad - 1536) {\n                Lara_TakeDamage(lara_item->hit_points, false);\n                Lara_CatchFire();\n            }\n\n            item->goal_anim_state += 80;\n        }\n\n        item->required_anim_state += 32;\n    }\n\n    if (item->required_anim_state < 4096 + M_SMOKE_END) {\n        if (item->goal_anim_state > 768) {\n            Sound_Effect(SFX_HUGE_ROCKET_LOOP, nullptr, SPM_NORMAL);\n        }\n\n        if (item->goal_anim_state > 1024) {\n            item->goal_anim_state = 1024;\n        }\n    } else {\n        Sound_Effect(SFX_HUGE_ROCKET_LOOP, nullptr, SPM_NORMAL);\n        if (item->required_anim_state > 12288) {\n            item->required_anim_state = 12288;\n        }\n\n        if (item->goal_anim_state > 22528) {\n            for (int32_t i = 0; i < 64; i++) {\n                const XYZ_32 pos = {\n                    .x = item->pos.x - (Random_GetControl() & 0xFFF) - 5632,\n                    .y = M_GetFloorY(item) - (Random_GetControl() & 0x7FF),\n                    .z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048,\n                };\n                M_TriggerBlastFire(pos, false, i);\n            }\n\n            for (int32_t i = 64; i < 96; i++) {\n                const XYZ_32 pos = {\n                    .x = item->pos.x - (Random_GetControl() & 0xFFF) - 5632,\n                    .y = M_GetFloorY(item) - (Random_GetControl() & 0x7FF),\n                    .z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048,\n                };\n                M_TriggerBlastFire(pos, true, i);\n            }\n\n            g_Camera.bounce = -((0x4000 - item->required_anim_state) >> 6);\n            item->current_anim_state = 1;\n            return;\n        }\n\n        item->fall_speed--;\n        CLAMPL(item->fall_speed, -1024);\n\n        if (item->fall_speed < -72) {\n            m_SupportFallen = true;\n        }\n\n        item->pos.y += item->fall_speed >> 2;\n        int16_t room_num = item->room_num;\n        Room_GetSector(item->pos, &room_num);\n\n        if (item->room_num != room_num) {\n            Item_UpdateRoom(Item_GetIndex(item), room_num);\n        }\n    }\n\n    if (item->required_anim_state >= 0x2000) {\n        g_Camera.bounce = -((0x4000 - item->required_anim_state) >> 6);\n    } else if (item->required_anim_state <= 64) {\n        g_Camera.bounce = -1;\n    } else {\n        g_Camera.bounce = -(item->required_anim_state >> 6);\n    }\n}\n\nstatic void M_ControlBlast(ITEM *const item)\n{\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if (!(time4 & 0xC)) {\n        int32_t yv;\n        if (item->required_anim_state >= M_SMOKE_END >> 2) {\n            yv = 4 * (item->required_anim_state + (Random_GetControl() & 0x1F))\n                - M_SMOKE_END;\n        } else {\n            yv = Random_GetControl() & 0x1F;\n        }\n        CLAMPG(yv, 6144);\n\n        const bool fire = item->required_anim_state >= M_SMOKE_END;\n        M_TriggerRocketSmoke(\n            (XYZ_32) {\n                .x = item->pos.x - 896,\n                .y = item->pos.y - 64,\n                .z = item->pos.z - 512,\n            },\n            yv, fire);\n        M_TriggerRocketSmoke(\n            (XYZ_32) {\n                .x = item->pos.x - 128,\n                .y = item->pos.y - 64,\n                .z = item->pos.z - 512,\n            },\n            yv, fire);\n        M_TriggerRocketSmoke(\n            (XYZ_32) {\n                .x = item->pos.x - 512,\n                .y = item->pos.y - 64,\n                .z = item->pos.z - 896,\n            },\n            yv, fire);\n        M_TriggerRocketSmoke(\n            (XYZ_32) {\n                .x = item->pos.x - 512,\n                .y = item->pos.y - 64,\n                .z = item->pos.z - 128,\n            },\n            yv, fire);\n    }\n\n    if (item->goal_anim_state != 0) {\n        RGB_888 color = {\n            .r = (Random_GetControl() & 0x1F) + 224,\n            .g = (Random_GetControl() & 0x3F) + 96,\n            .b = Random_GetControl() & 0x1F,\n        };\n        Output_AddDynamicLightRGB(\n            (XYZ_32) {\n                .x = item->pos.x - 512,\n                .y = item->pos.y,\n                .z = item->pos.z - 512,\n            },\n            31, color);\n\n        int32_t rad = item->goal_anim_state;\n        CLAMPG(rad, 8192);\n        const int32_t y_mask = item->goal_anim_state >= 1024 ? 2047 : 255;\n\n        XYZ_32 pos = {\n            .x = item->pos.x - (Random_GetControl() & 0x7FF) - rad + 512,\n            .y = M_GetFloorY(item) - (y_mask & Random_GetControl()),\n            .z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048,\n        };\n\n        if (time4 & 4) {\n            M_TriggerBlastFire(pos, false, -1);\n        }\n\n        pos.x = item->pos.x - rad + 512;\n        pos.z = item->pos.z - 1024;\n        color.r = (Random_GetControl() & 0x1F) + 224;\n        color.g = (Random_GetControl() & 0x3F) + 96;\n        color.b = Random_GetControl() & 0x1F;\n        Output_AddDynamicLightRGB(pos, 24, color);\n\n        if (time4 & 4) {\n            pos.x = item->pos.x - (Random_GetControl() & 0x3FF) - rad - 512;\n            pos.y = M_GetFloorY(item) - (y_mask & Random_GetControl());\n            pos.z = item->pos.z + (Random_GetControl() & 0x7FF) - 2048;\n            M_TriggerBlastFire(pos, true, -1);\n        }\n    }\n}\n\nstatic void M_ControlMain(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    if (item->object_id == O_AREA_51_ROCKET) {\n        M_ControlRocket(item);\n    }\n\n    M_ControlBlast(item);\n}\n\nvoid M_InitialiseSupport(const int16_t item_num)\n{\n    m_SupportFallen = false;\n}\n\nstatic void M_ControlSupport(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!m_SupportFallen) {\n        item->current_anim_state = M_SUPPORT_STATE_WAIT;\n    } else if (\n        item->goal_anim_state != M_SUPPORT_STATE_FALL\n        && item->current_anim_state != M_SUPPORT_STATE_FALL) {\n        item->goal_anim_state = M_SUPPORT_STATE_FALL;\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_SetupRocket(OBJECT *const obj)\n{\n    obj->initialise_func = M_InitialiseMain;\n    obj->control_func = M_ControlMain;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SetupBlast(OBJECT *const obj)\n{\n    obj->initialise_func = M_InitialiseMain;\n    obj->control_func = M_ControlMain;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SetupSupport(OBJECT *const obj)\n{\n    obj->initialise_func = M_InitialiseSupport;\n    obj->control_func = M_ControlSupport;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_AREA_51_ROCKET, M_SetupRocket)\nREGISTER_OBJECT(O_AREA_51_ROCKET_BLAST, M_SetupBlast)\nREGISTER_OBJECT(O_AREA_51_ROCKET_SUPPORT, M_SetupSupport)\n"
  },
  {
    "path": "src/trx/game/objects/general/assault_target.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/gym.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing/lot.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\ntypedef enum {\n    M_STATE_RISE = 0,\n    M_STATE_HIT_1 = 1,\n    M_STATE_HIT_2 = 2,\n    M_STATE_HIT_3 = 3,\n} M_STATE;\n\ntypedef struct {\n    int32_t x_rot_speed;\n    int32_t bounce_stage;\n    bool destroyed;\n    bool targetable;\n} M_PRIV;\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"x_rot_speed\", &p->x_rot_speed));\n    JSON_SHOULD(JSON_READ(io, \"bounce_stage\", &p->bounce_stage));\n    JSON_SHOULD(JSON_READ(io, \"destroyed\", &p->destroyed));\n    p->targetable = !p->destroyed;\n    JSON_OPTIONAL(JSON_READ(io, \"targetable\", &p->targetable));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"x_rot_speed\", p->x_rot_speed);\n    JSONW_WRITE(io, \"bounce_stage\", p->bounce_stage);\n    JSONW_WRITE(io, \"destroyed\", p->destroyed);\n    JSONW_WRITE(io, \"targetable\", p->targetable);\n}\n\nstatic void M_ResetItemState(ITEM *const item, const OBJECT *const obj)\n{\n    Item_SwitchToAnim(item, 0, 0);\n    const ANIM *const anim = Item_GetAnim(item);\n    item->current_anim_state = anim->current_anim_state;\n    item->goal_anim_state = item->current_anim_state;\n    item->required_anim_state = M_STATE_RISE;\n    item->prev_frame_num = item->frame_num;\n    item->rot.x = 0;\n    item->rot.z = 0;\n    item->timer = 0;\n    item->hit_points = obj->hit_points;\n    item->max_hit_points = obj->hit_points;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (item->active) {\n        Item_RemoveActive(item_num);\n    }\n\n    if (item->creature_data != nullptr) {\n        LOT_DisableBaddieAI(item_num);\n    }\n\n    M_PRIV *const p = item->priv;\n    p->x_rot_speed = 0;\n    p->bounce_stage = 0;\n    p->destroyed = false;\n    p->targetable = true;\n\n    item->active = false;\n    item->status = IS_INACTIVE;\n    item->flags = 0;\n    item->collidable = true;\n\n    M_ResetItemState(item, obj);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    if (g_TRVersion < 3) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_ACTIVE) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    if (p == nullptr) {\n        return;\n    }\n\n    if (p->targetable) {\n        if (item->hit_status) {\n            Sound_Effect(SFX_TARGET_HITS, &item->pos, SPM_NORMAL);\n        }\n\n        switch (item->current_anim_state) {\n        case M_STATE_RISE:\n            if (item->hit_points < 6) {\n                item->hit_points = 6;\n                item->goal_anim_state = M_STATE_HIT_1;\n            }\n            break;\n\n        case M_STATE_HIT_1:\n            if (item->hit_points < 4) {\n                Item_SwitchToAnim(item, 2, 0);\n                item->current_anim_state = M_STATE_HIT_2;\n                item->goal_anim_state = M_STATE_HIT_2;\n                item->hit_points = 4;\n            }\n            break;\n\n        case M_STATE_HIT_2:\n            if (item->hit_points < 2) {\n                Item_SwitchToAnim(item, 3, 0);\n                item->current_anim_state = M_STATE_HIT_3;\n                item->goal_anim_state = M_STATE_HIT_3;\n                item->hit_points = 2;\n            }\n            break;\n\n        case M_STATE_HIT_3:\n            if (item->hit_points <= 0) {\n                LARA_INFO *const lara = Lara_GetLaraInfo();\n                if (lara->target == item) {\n                    lara->target = nullptr;\n                }\n                p->targetable = false;\n                p->x_rot_speed = DEG_1 * 10;\n                p->bounce_stage = 0;\n                p->destroyed = true;\n            }\n            break;\n        }\n\n        item->timer++;\n\n        if (item->timer > GYM_ASSAULT_TARGET_TIME) {\n            LARA_INFO *const lara = Lara_GetLaraInfo();\n            if (lara->target == item) {\n                lara->target = nullptr;\n            }\n            p->targetable = false;\n            p->x_rot_speed = DEG_1;\n            p->bounce_stage = 0;\n            p->destroyed = false;\n        }\n    } else {\n        if (p->destroyed) {\n            int32_t rot_x = item->rot.x;\n            rot_x += p->x_rot_speed;\n            p->x_rot_speed += (DEG_1 * 4) >> p->bounce_stage;\n\n            if (rot_x > 0x3800) {\n                if (p->bounce_stage == 2) {\n                    item->rot.x = 0x3800;\n                    Item_RemoveActive(item_num);\n                    return;\n                }\n\n                if (p->bounce_stage == 1) {\n                    Sound_Effect(SFX_TARGET_SMASH, &item->pos, SPM_NORMAL);\n                }\n\n                p->x_rot_speed = (-p->x_rot_speed) >> 2;\n                p->bounce_stage++;\n                rot_x = 0x3800;\n            }\n\n            item->rot.x = rot_x;\n        } else {\n            int32_t rot_x = item->rot.x;\n            rot_x -= p->x_rot_speed;\n            p->x_rot_speed += 91 >> p->bounce_stage;\n\n            if (rot_x < -0x2A00) {\n                if (p->bounce_stage == 2) {\n                    item->rot.x = -0x2A00;\n                    Item_RemoveActive(item_num);\n                    return;\n                }\n\n                Sound_Effect(\n                    SFX_TARGET_HITS, &item->pos, SPM_PITCH | (0x20000 << 8));\n\n                p->x_rot_speed = (-p->x_rot_speed) >> 2;\n                p->bounce_stage++;\n                rot_x = -0x2A00;\n            }\n\n            item->rot.x = (int16_t)rot_x;\n        }\n    }\n\n    Item_Animate(item);\n}\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr && p->targetable && item->status == IS_ACTIVE\n        && item->hit_points > 0;\n}\n\nstatic bool M_CanTakeDamage(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr && p->targetable && item->hit_points > 0;\n}\n\nstatic bool M_CanBeProjectileTarget(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr && p->targetable && item->status == IS_ACTIVE\n        && item->collidable && item->hit_points > 0;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->can_take_damage_func = M_CanTakeDamage;\n    obj->can_be_projectile_target_func = M_CanBeProjectileTarget;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->hit_points = 8;\n    obj->shadow_size = 128;\n    obj->radius = 102;\n    obj->intelligent = false;\n}\n\nREGISTER_OBJECT(O_ASSAULT_TARGET, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/bat_emitter.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n\n#define M_MAX_BATS 32\n#define M_BAT_SPRITE_OFFSET 12\n\ntypedef struct {\n    XYZ_32 pos;\n    XYZ_32 prev_pos;\n    int16_t angle;\n    int16_t prev_angle;\n    int16_t speed;\n    uint8_t wing_y_off;\n    uint8_t prev_wing_y_off;\n    bool active;\n    uint8_t life;\n} M_BAT;\n\ntypedef struct {\n    bool bats_triggered;\n    bool bats_alive;\n    M_BAT bats[M_MAX_BATS];\n\n    struct {\n        bool prepared;\n        int32_t sprite_idx;\n        OUTPUT_UVW tri_uvw[3][3];\n        OUTPUT_TEXTURE_SIZE tri_tex_size[3][3];\n    } draw;\n} M_PRIV;\n\nstatic const XYZ_16 m_BatMesh[5] = {\n    { -192, 0, -48 },  { -192, 0, 48 },  { 96, 0, 0 },\n    { -144, 0, -192 }, { -144, 0, 192 },\n};\n\nstatic const uint8_t m_BatTriangles[3][3] = {\n    { 0, 1, 2 },\n    { 3, 0, 2 },\n    { 1, 4, 2 },\n};\n\n// TR3 UV mapping differs per triangle in the original bat GT3 setup.\nstatic const uint8_t m_BatTriangleSpriteCorners[3][3] = {\n    { 0, 2, 3 },\n    { 1, 0, 2 },\n    { 0, 1, 2 },\n};\n\nstatic int32_t M_GetWingYOffset(const int32_t corner, const uint8_t wing_y_off)\n{\n    if (corner < 3) {\n        const int16_t angle = (((wing_y_off - 32) & 0x3F) << 10);\n        return (Math_Sin(angle) >> 10) - 512;\n    }\n\n    const int16_t angle = wing_y_off << 10;\n    return (Math_Sin(angle) >> 6) - 512;\n}\n\nstatic void M_RememberBat(M_BAT *const bat)\n{\n    bat->prev_pos = bat->pos;\n    bat->prev_angle = bat->angle;\n    bat->prev_wing_y_off = bat->wing_y_off;\n}\n\nstatic uint8_t M_GetInterpolatedWingYOffset(\n    const M_BAT *const bat, const double ratio)\n{\n    int32_t wing_diff =\n        (int32_t)bat->wing_y_off - (int32_t)bat->prev_wing_y_off;\n    if (wing_diff > 32) {\n        wing_diff -= 64;\n    } else if (wing_diff < -32) {\n        wing_diff += 64;\n    }\n\n    int32_t wing_interp = LERP(\n        (int32_t)bat->prev_wing_y_off,\n        (int32_t)bat->prev_wing_y_off + wing_diff, ratio);\n    wing_interp %= 64;\n    if (wing_interp < 0) {\n        wing_interp += 64;\n    }\n    return (uint8_t)wing_interp;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"bats_triggered\", &p->bats_triggered));\n    JSON_SHOULD(JSON_READ(io, \"bats_alive\", &p->bats_alive));\n\n    for (int32_t i = 0; i < M_MAX_BATS; i++) {\n        p->bats[i] = (M_BAT) {};\n    }\n\n    if (p->bats_alive && JSON_SHOULD(JSON_PUSH(io, \"bats\"))) {\n        for (int32_t i = 0; i < M_MAX_BATS; i++) {\n            const char *const key = String_FormatStatic(\"bat_%d\", i);\n            if (JSON_SHOULD(JSON_PUSH(io, key))) {\n                M_BAT *const bat = &p->bats[i];\n                JSON_SHOULD(JSON_READ(io, \"pos\", &bat->pos));\n                JSON_SHOULD(JSON_READ(io, \"angle\", &bat->angle));\n                JSON_SHOULD(JSON_READ(io, \"speed\", &bat->speed));\n                JSON_SHOULD(JSON_READ(io, \"wing_y_off\", &bat->wing_y_off));\n                JSON_SHOULD(JSON_READ(io, \"active\", &bat->active));\n                JSON_SHOULD(JSON_READ(io, \"life\", &bat->life));\n                JSON_SHOULD(JSON_POP(io));\n            }\n        }\n        JSON_SHOULD(JSON_POP(io));\n    }\n\n    p->draw.prepared = false;\n    p->draw.sprite_idx = -1;\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"bats_triggered\", p->bats_triggered);\n    JSONW_WRITE(io, \"bats_alive\", p->bats_alive);\n\n    if (!p->bats_alive) {\n        return;\n    }\n\n    JSONW_PUSH_OBJECT(io);\n    for (int32_t i = 0; i < M_MAX_BATS; i++) {\n        const M_BAT *const bat = &p->bats[i];\n        const char *const key = String_FormatStatic(\"bat_%d\", i);\n        JSONW_PUSH_OBJECT(io);\n        JSONW_WRITE(io, \"pos\", bat->pos);\n        JSONW_WRITE(io, \"angle\", bat->angle);\n        JSONW_WRITE(io, \"speed\", bat->speed);\n        JSONW_WRITE(io, \"wing_y_off\", bat->wing_y_off);\n        JSONW_WRITE(io, \"active\", bat->active);\n        JSONW_WRITE(io, \"life\", bat->life);\n        JSONW_POP_AND_SET(io, key);\n    }\n    JSONW_POP_AND_SET(io, \"bats\");\n}\n\nstatic void M_PrepareDrawData(M_PRIV *const p)\n{\n    if (p->draw.prepared) {\n        return;\n    }\n\n    p->draw.sprite_idx = -1;\n\n    const OBJECT *const explosion = Object_Get(O_EXPLOSION_1);\n    if (explosion == nullptr || !explosion->loaded) {\n        return;\n    }\n\n    const int32_t sprite_idx = explosion->mesh_idx + M_BAT_SPRITE_OFFSET;\n    if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) {\n        return;\n    }\n\n    p->draw.sprite_idx = sprite_idx;\n\n    for (size_t i = 0; i < ARRAY_SIZE(m_BatTriangleSpriteCorners); i++) {\n        for (size_t j = 0; j < ARRAY_SIZE(m_BatTriangleSpriteCorners[0]); j++) {\n            const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex(\n                sprite_idx, m_BatTriangleSpriteCorners[i][j]);\n            p->draw.tri_uvw[i][j] = Output_Textures_GetUVW(uvw_idx);\n            p->draw.tri_tex_size[i][j] =\n                Output_Textures_GetAtlasSize(uvw_idx / 4);\n        }\n    }\n\n    p->draw.prepared = true;\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    if (!p->bats_alive) {\n        return false;\n    }\n\n    if (!p->draw.prepared) {\n        M_PrepareDrawData(p);\n    }\n\n    if (p->draw.sprite_idx < 0\n        || p->draw.sprite_idx >= Output_GetSpriteTextureCount()) {\n        return false;\n    }\n\n    const RGBA_8888 color = { 0x60, 0xA0, 0xF8, 0xFF };\n    const RGBA_8888 tri_color[3] = { color, color, color };\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n\n    for (int32_t i = 0; i < M_MAX_BATS; i++) {\n        const M_BAT *const bat = &p->bats[i];\n        if (!bat->active) {\n            continue;\n        }\n\n        const XYZ_32 draw_pos = do_interp\n            ? (XYZ_32) {\n                  .x = (int32_t)LERP(bat->prev_pos.x, bat->pos.x, ratio),\n                  .y = (int32_t)LERP(bat->prev_pos.y, bat->pos.y, ratio),\n                  .z = (int32_t)LERP(bat->prev_pos.z, bat->pos.z, ratio),\n              }\n            : bat->pos;\n        const int16_t draw_angle = do_interp\n            ? (Math_AngleMean(bat->prev_angle << 4, bat->angle << 4, ratio)\n               >> 4)\n            : bat->angle;\n        const uint8_t draw_wing_y_off = do_interp\n            ? M_GetInterpolatedWingYOffset(bat, ratio)\n            : bat->wing_y_off;\n\n        XYZ_32 world[5] = {};\n        Matrix_Push();\n        Matrix_TranslateAbs32(draw_pos);\n        Matrix_RotY(draw_angle << 4);\n        for (int32_t j = 0; j < 5; j++) {\n            const XYZ_32 local = {\n                .x = m_BatMesh[j].x,\n                .y = m_BatMesh[j].y + M_GetWingYOffset(j, draw_wing_y_off),\n                .z = m_BatMesh[j].z,\n            };\n            world[j] = Matrix_MulVec32_M(g_WMatrixPtr, local);\n        }\n        Matrix_Pop();\n\n        for (size_t j = 0; j < ARRAY_SIZE(m_BatTriangles); j++) {\n            const uint8_t *const tri = m_BatTriangles[j];\n            const XYZ_32 tri_world[3] = {\n                world[tri[0]],\n                world[tri[1]],\n                world[tri[2]],\n            };\n\n            OutputSource_PolyFX_StageTriExtUV(\n                tri_world, p->draw.tri_uvw[j], p->draw.tri_tex_size[j], nullptr,\n                tri_color, VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND);\n        }\n    }\n\n    return true;\n}\n\nstatic void M_Update(M_PRIV *const p)\n{\n    bool any_alive = false;\n\n    for (int32_t i = 0; i < M_MAX_BATS; i++) {\n        M_BAT *const bat = &p->bats[i];\n        if (!bat->active) {\n            continue;\n        }\n\n        M_RememberBat(bat);\n\n        if ((i & 3) == 0 && (Random_GetControl() & 7) == 0) {\n            Sound_Effect(SFX_BATS_1, &bat->pos, SPM_NORMAL);\n        }\n\n        const int16_t angle = bat->angle << 4;\n        const int32_t sin_v = Math_Sin(angle) >> 2;\n        const int32_t cos_v = Math_Cos(angle) >> 2;\n        bat->pos.x -= ((int64_t)bat->speed * cos_v) >> W2V_SHIFT;\n        bat->pos.y -= Random_GetControl() & 3;\n        bat->pos.z += ((int64_t)bat->speed * sin_v) >> W2V_SHIFT;\n        bat->wing_y_off = (bat->wing_y_off + 11) & 0x3F;\n\n        if (bat->life < 128) {\n            bat->pos.y += -4 - (i >> 1);\n\n            if ((Random_GetControl() & 3) == 0) {\n                bat->angle =\n                    (bat->angle + (Random_GetControl() & 0xFF) - 128) & 0xFFF;\n                bat->speed += Random_GetControl() & 3;\n            }\n        }\n\n        bat->speed += 12;\n        CLAMPG(bat->speed, 300);\n\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        if (bat->life != 0 && (time4 & 4) != 0) {\n            bat->life--;\n            if (bat->life == 0) {\n                bat->active = false;\n            }\n        }\n\n        if (bat->active) {\n            any_alive = true;\n        }\n    }\n\n    p->bats_alive = any_alive;\n}\n\nstatic void M_TriggerBats(M_PRIV *const p, const XYZ_32 pos, int16_t ang)\n{\n    ang = (ang - 1024) & 0xFFF;\n\n    for (int32_t i = 0; i < M_MAX_BATS; i++) {\n        M_BAT *const bat = &p->bats[i];\n        bat->pos.x = pos.x + (Random_GetControl() & 0x1FF) - 256;\n        bat->pos.y = pos.y - (Random_GetControl() & 0xFF) + 256;\n        bat->pos.z = pos.z + (Random_GetControl() & 0x1FF) - 256;\n        bat->angle = ((Random_GetControl() & 0x7F) + ang - 64) & 0xFFF;\n        bat->speed = (Random_GetControl() & 0x1F) + 64;\n        bat->wing_y_off = Random_GetControl() & 0x3F;\n        bat->life = (Random_GetControl() & 7) + 144;\n        bat->active = true;\n        M_RememberBat(bat);\n    }\n\n    p->bats_alive = true;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->active == 0) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    if (!p->bats_triggered) {\n        M_TriggerBats(p, item->pos, item->rot.y >> 4);\n        p->bats_triggered = true;\n    } else {\n        M_Update(p);\n    }\n\n    if (!p->bats_alive) {\n        Item_Kill(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_BAT_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/bell.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    BELL_STATE_STOP = 0,\n    BELL_STATE_SWING = 1,\n} BELL_STATE;\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    item->goal_anim_state = BELL_STATE_SWING;\n\n    const SECTOR *const sector = Room_GetSector(item->pos, &item->room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n    Room_TestTriggers(item);\n\n    Item_Animate(item);\n\n    if (item->current_anim_state == BELL_STATE_STOP) {\n        item->status = IS_INACTIVE;\n        Item_RemoveActive(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BELL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/big_bowl.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    // clang-format off\n    BIG_BOWL_STATE_TIP  = 0,\n    BIG_BOWL_STATE_POUR = 1,\n    // clang-format on\n} BIG_BOWL_STATE;\n\nstatic void M_CreateHotLiquid(const ITEM *const bowl_item)\n{\n    const int16_t effect_num = Effect_Create(bowl_item->room_num);\n    const OBJECT *const obj = Object_Get(O_HOT_LIQUID);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_HOT_LIQUID;\n        effect->pos.x = bowl_item->pos.x + STEP_L * 2;\n        effect->pos.z = bowl_item->pos.z + STEP_L * 2;\n        effect->pos.y = bowl_item->pos.y + STEP_L * 2 + 100;\n        effect->room_num = bowl_item->room_num;\n        effect->frame_num = (obj->mesh_count * Random_GetDraw()) >> 15;\n        effect->fall_speed = 0;\n        effect->shade = 2048;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->current_anim_state == BIG_BOWL_STATE_POUR) {\n        M_CreateHotLiquid(item);\n        item->timer++;\n        if (item->timer == 5 * LOGIC_FPS && !Room_GetFlipStatus()) {\n            // TODO: poorly hardcoded flimap number\n            Room_SetFlipSlotFlags(4, IF_CODE_BITS | IF_ONE_SHOT);\n            Room_FlipMap();\n        }\n    }\n\n    Item_Animate(item);\n\n    if (item->status == IS_DEACTIVATED && item->timer >= LOGIC_FPS * 7) {\n        Item_RemoveActive(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BIG_BOWL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/bird_tweeter.c",
    "content": "#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n\n    if (item->object_id == O_BIRD_TWEETER_2) {\n        if (Random_GetDraw() < 1024) {\n            Sound_Effect(SFX_BIRDS_CHIRP, &item->pos, SPM_NORMAL);\n        }\n    } else if (Random_GetDraw() < 256) {\n        Sound_Effect(SFX_DRIPS_REVERB, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n}\n\nREGISTER_OBJECT(O_BIRD_TWEETER_1, M_Setup)\nREGISTER_OBJECT(O_BIRD_TWEETER_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/boat.c",
    "content": "#include <trx/game/objects.h>\n\ntypedef enum {\n    BOAT_STATE_EMPTY = 0,\n    BOAT_STATE_SET = 1,\n    BOAT_STATE_MOVE = 2,\n    BOAT_STATE_STOP = 3,\n} BOAT_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    switch (item->current_anim_state) {\n    case BOAT_STATE_SET:\n        item->goal_anim_state = BOAT_STATE_MOVE;\n        break;\n    case BOAT_STATE_MOVE:\n        item->goal_anim_state = BOAT_STATE_STOP;\n        break;\n    case BOAT_STATE_STOP:\n        Item_Kill(item_num);\n        break;\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n    obj->save_anim = true;\n    obj->save_position = true;\n}\n\nREGISTER_OBJECT(O_MOTOR_BOAT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/bridge_common.c",
    "content": "#include <trx/game/objects/general/bridge_common.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/rooms.h>\n\nbool Bridge_IsSameSector(\n    const int32_t x, const int32_t z, const ITEM *const item)\n{\n    const int32_t sector_x = x / WALL_L;\n    const int32_t sector_z = z / WALL_L;\n    const int32_t item_sector_x = item->pos.x / WALL_L;\n    const int32_t item_sector_z = item->pos.z / WALL_L;\n\n    return sector_x == item_sector_x && sector_z == item_sector_z;\n}\n\nint32_t Bridge_GetOffset(\n    const ITEM *const item, int32_t x, int32_t y, int32_t z)\n{\n    // Set the offset to the max value of 1023 if Lara is outside of the\n    // bridge x/z position depending on its angle. This makes sure\n    // the height is calculated properly for the front collision since\n    // the low end of tilted bridges have a lower height.\n    int32_t offset = 0;\n    if (item->rot.y == 0) {\n        if (g_Config.gameplay.fix_bridge_collision\n            && x <= item->pos.x - WALL_L / 2) {\n            offset = WALL_L - 1;\n        } else {\n            offset = (WALL_L - x) & (WALL_L - 1);\n        }\n    } else if (item->rot.y == -DEG_180) {\n        if (g_Config.gameplay.fix_bridge_collision\n            && x >= item->pos.x + WALL_L / 2) {\n            offset = 0;\n        } else {\n            offset = x & (WALL_L - 1);\n        }\n    } else if (item->rot.y == DEG_90) {\n        if (g_Config.gameplay.fix_bridge_collision\n            && z >= item->pos.z + WALL_L / 2) {\n            offset = WALL_L - 1;\n        } else {\n            offset = z & (WALL_L - 1);\n        }\n    } else {\n        if (g_Config.gameplay.fix_bridge_collision\n            && z <= item->pos.z - WALL_L / 2) {\n            offset = 0;\n        } else {\n            offset = (WALL_L - z) & (WALL_L - 1);\n        }\n        // Fixes an edge case of an invisible wall on the tilt2 bridge floor.\n        // The offset would get set to 0 on a specific z pos at the bottom of a\n        // slope. The game would then set an invisible wall because it thought\n        // Lara was at the high end of the tilt2 slope which is higher than a\n        // step. This fix sets the offset to the max value (1023) when Lara's at\n        // the bottom of the slope.\n        if (g_Config.gameplay.fix_bridge_collision && offset == 0\n            && y < item->pos.y) {\n            offset = (WALL_L - 1 - z) & (WALL_L - 1);\n        }\n    }\n    return offset;\n}\n\nvoid Bridge_FixEmbeddedPosition(int16_t item_num)\n{\n    // Some bridges at floor level are embedded into the floor.\n    // This checks if bridges are below a room's floor level\n    // and moves them up.\n    ITEM *const item = Item_Get(item_num);\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    const int16_t bridge_height = ABS(bounds->max.y) - ABS(bounds->min.y);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(\n        (XYZ_32) { item->pos.x, item->pos.y - bridge_height, item->pos.z },\n        &room_num);\n    const int32_t floor_height = Room_GetHeight(sector, item->pos);\n\n    // Only move the bridge up if it's at floor level and there\n    // isn't a room portal below.\n    if (item->pos.y != floor_height || sector->portal_room.pit != NO_ROOM) {\n        return;\n    }\n\n    item->pos.y = floor_height - bridge_height;\n}\n\nvoid Bridge_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    Walkable_Add(item_num, item->pos);\n}\n"
  },
  {
    "path": "src/trx/game/objects/general/bridge_common.h",
    "content": "#pragma once\n\n#include <trx/game/items.h>\n\nbool Bridge_IsSameSector(int32_t x, int32_t z, const ITEM *item);\nint32_t Bridge_GetOffset(const ITEM *item, int32_t x, int32_t y, int32_t z);\nvoid Bridge_FixEmbeddedPosition(int16_t item_num);\nvoid Bridge_AddWalkable(int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/objects/general/bridge_flat.c",
    "content": "#include <trx/config.h>\n#include <trx/game/const.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/bridge_common.h>\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (g_Config.gameplay.fix_bridge_collision\n        && !Bridge_IsSameSector(x, z, item)) {\n        return height;\n    }\n\n    if (y > item->pos.y) {\n        return height;\n    }\n\n    if (g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) {\n        return height;\n    }\n\n    return item->pos.y;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (g_Config.gameplay.fix_bridge_collision\n        && !Bridge_IsSameSector(x, z, item)) {\n        return height;\n    }\n\n    if (y <= item->pos.y) {\n        return height;\n    }\n\n    if (g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) {\n        return height;\n    }\n\n    return item->pos.y + STEP_L;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Bridge_FixEmbeddedPosition(item_num);\n    Walkable_AllocateNodes(Item_Get(item_num), 1);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->add_walkable_func = Bridge_AddWalkable;\n}\n\nREGISTER_OBJECT(O_BRIDGE_FLAT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/bridge_tilt1.c",
    "content": "#include <trx/config.h>\n#include <trx/game/const.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/bridge_common.h>\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (g_Config.gameplay.fix_bridge_collision\n        && !Bridge_IsSameSector(x, z, item)) {\n        return height;\n    }\n\n    const int32_t offset_height =\n        item->pos.y + (Bridge_GetOffset(item, x, y, z) / 4);\n    if (y > offset_height || item->pos.y >= height) {\n        return height;\n    }\n\n    if (g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) {\n        return height;\n    }\n\n    return offset_height;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (g_Config.gameplay.fix_bridge_collision\n        && !Bridge_IsSameSector(x, z, item)) {\n        return height;\n    }\n\n    const int32_t offset_height =\n        item->pos.y + (Bridge_GetOffset(item, x, y, z) / 4);\n    if (y <= offset_height) {\n        return height;\n    }\n\n    if (g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) {\n        return height;\n    }\n\n    return offset_height + STEP_L;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Bridge_FixEmbeddedPosition(item_num);\n    Walkable_AllocateNodes(Item_Get(item_num), 1);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->add_walkable_func = Bridge_AddWalkable;\n}\n\nREGISTER_OBJECT(O_BRIDGE_TILT_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/bridge_tilt2.c",
    "content": "#include <trx/config.h>\n#include <trx/game/const.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/bridge_common.h>\n\nint16_t M_GetFloorHeight(\n    const ITEM *item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (g_Config.gameplay.fix_bridge_collision\n        && !Bridge_IsSameSector(x, z, item)) {\n        return height;\n    }\n\n    const int32_t offset_height =\n        item->pos.y + (Bridge_GetOffset(item, x, y, z) / 2);\n    if (y > offset_height) {\n        return height;\n    }\n\n    if (g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) {\n        return height;\n    }\n\n    return offset_height;\n}\n\nint16_t M_GetCeilingHeight(\n    const ITEM *item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (g_Config.gameplay.fix_bridge_collision\n        && !Bridge_IsSameSector(x, z, item)) {\n        return height;\n    }\n\n    const int32_t offset_height =\n        item->pos.y + (Bridge_GetOffset(item, x, y, z) / 2);\n    if (y <= offset_height) {\n        return height;\n    }\n\n    if (g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) {\n        return height;\n    }\n\n    return offset_height + STEP_L;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Bridge_FixEmbeddedPosition(item_num);\n    Walkable_AllocateNodes(Item_Get(item_num), 1);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->add_walkable_func = Bridge_AddWalkable;\n}\n\nREGISTER_OBJECT(O_BRIDGE_TILT_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/cabin.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    CABIN_STATE_START = 0,\n    CABIN_STATE_DROP_1 = 1,\n    CABIN_STATE_DROP_2 = 2,\n    CABIN_STATE_DROP_3 = 3,\n    CABIN_STATE_FINISH = 4,\n} CABIN_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) {\n        switch (item->current_anim_state) {\n        case CABIN_STATE_START:\n            item->goal_anim_state = CABIN_STATE_DROP_1;\n            break;\n        case CABIN_STATE_DROP_1:\n            item->goal_anim_state = CABIN_STATE_DROP_2;\n            break;\n        case CABIN_STATE_DROP_2:\n            item->goal_anim_state = CABIN_STATE_DROP_3;\n            break;\n        }\n        item->flags = 0;\n    }\n\n    if (item->current_anim_state == CABIN_STATE_FINISH) {\n        Room_SetFlipSlotFlags(3, IF_CODE_BITS);\n        Room_FlipMap();\n        Item_Kill(item_num);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = Object_DrawUnclippedItem;\n    obj->collision_func = Object_Collision;\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_PORTACABIN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/camera_target.c",
    "content": "#include <trx/game/objects.h>\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->draw_func = nullptr;\n}\n\nREGISTER_OBJECT(O_CAMERA_TARGET, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/carcass.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n// clang-format off\n#define M_ROLL_SHIFT       1\n#define M_ROLL_SHIFT_UW    3\n#define M_MAX_ROLL         0x6000\n#define M_ACCEL            8\n#define M_ACCEL_UW         1\n#define M_MAX_SPEED        (STEP_L * 2)  // = 512\n#define M_MAX_SPEED_UW     (STEP_L / 4)  // = 64\n#define M_INITIAL_SPEED_UW (STEP_L / 16) // = 16\n// clang-format on\n\nstatic void M_SpawnSplash(const ITEM *const item)\n{\n    const ROOM *const room = Room_Get(item->room_num);\n    const FX_WATER_SPLASH_SETUP setup = {\n        .x = item->pos.x,\n        .y = room->max_ceiling,\n        .z = item->pos.z,\n        .inner_xz_off = 16,\n        .inner_xz_size = 16,\n        .inner_y_size = -96,\n        .inner_xz_vel = 160,\n        .inner_y_vel = (int16_t)(-72 * item->fall_speed),\n        .inner_gravity = 128,\n        .inner_friction = 7,\n        .middle_xz_off = 24,\n        .middle_xz_size = 32,\n        .middle_y_size = -64,\n        .middle_xz_vel = 224,\n        .middle_y_vel = (int16_t)(-36 * item->fall_speed),\n        .middle_gravity = 72,\n        .middle_friction = 8,\n        .outer_xz_off = 32,\n        .outer_xz_size = 32,\n        .outer_xz_vel = 272,\n        .outer_friction = 9,\n    };\n    FX_Water_SetupSplash(&setup);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_ACTIVE) {\n        return;\n    }\n\n    item->pos.y += item->fall_speed;\n\n    const bool was_underwater = Room_Get(item->room_num)->flags.underwater;\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    const int16_t height = Room_GetHeight(sector, item->pos) - M_MAX_SPEED_UW;\n    if (item->pos.y >= height) {\n        item->pos.y = height;\n        item->fall_speed = 0;\n        // TODO: this is tied to the slope the carcass lands on in Crash Site\n        item->rot.z = M_MAX_ROLL;\n        return;\n    }\n\n    const ROOM *const current_room = Room_Get(room_num);\n    const bool is_underwater = current_room->flags.underwater;\n    item->rot.z += item->fall_speed\n        << (is_underwater ? M_ROLL_SHIFT_UW : M_ROLL_SHIFT);\n    item->fall_speed += is_underwater ? M_ACCEL_UW : M_ACCEL;\n    CLAMPG(item->rot.z, M_MAX_ROLL);\n    CLAMPG(item->fall_speed, was_underwater ? M_MAX_SPEED_UW : M_MAX_SPEED);\n\n    if (is_underwater && !was_underwater) {\n        M_SpawnSplash(item);\n        item->fall_speed = M_INITIAL_SPEED_UW;\n        item->pos.y = current_room->max_ceiling + 1;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_CARCASS, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/clock_chimes.c",
    "content": "#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/sound.h>\n\nstatic void M_DoChimeSound(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    XYZ_32 pos = lara_item->pos;\n    pos.x += (item->pos.x - lara_item->pos.x) >> 6;\n    pos.y += (item->pos.y - lara_item->pos.y) >> 6;\n    pos.z += (item->pos.z - lara_item->pos.z) >> 6;\n    Sound_Effect(SFX_DOOR_CHIME, &pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->timer == 0) {\n        return;\n    }\n    if (item->timer % 60 == 59) {\n        M_DoChimeSound(item);\n    }\n    item->timer--;\n    if (item->timer == 0) {\n        M_DoChimeSound(item);\n        item->timer = -1;\n        Item_RemoveActive(item_num);\n        item->status = IS_INACTIVE;\n        item->flags &= ~IF_CODE_BITS;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_CLOCK_CHIMES, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/cog.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    COG_STATE_INACTIVE = 0,\n    COG_STATE_ACTIVE = 1,\n} COG_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Item_IsTriggerActive(item)) {\n        item->goal_anim_state = COG_STATE_ACTIVE;\n    } else {\n        item->goal_anim_state = COG_STATE_INACTIVE;\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_COG_1, M_Setup)\nREGISTER_OBJECT(O_COG_2, M_Setup)\nREGISTER_OBJECT(O_COG_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/combat_end.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/vehicle.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/pathing.h>\n\n#define M_CUTSCENE_DELAY (5 * LOGIC_FPS) // = 150\n#define M_BOSS_TYPE O_CULT_3\n\nstatic int16_t m_BossTimer = 0;\nstatic uint16_t m_BossCount = 0;\n\nstatic int32_t M_CountAliveEnemies(void)\n{\n    int32_t count = 0;\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        const ITEM *const item = Item_Get(i);\n        if (item->object_id != M_BOSS_TYPE && Item_IsAlive(item)\n            && Creature_IsHostile(item)) {\n            count++;\n        }\n    }\n    return count;\n}\n\nstatic bool M_IsBossDead(void)\n{\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        const ITEM *const item = Item_Get(i);\n        if (item->object_id == M_BOSS_TYPE && !Item_IsAlive(item)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic int16_t M_FindNearestBoss(void)\n{\n    // Note that in the original, the first boss item was always selected here.\n    // For speedruns, the change here means that is no longer guaranteed, but\n    // positional manipulation can be used for the best outcome.\n    int32_t best_dist = INT32_MAX;\n    int16_t best_item = NO_ITEM;\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        const ITEM *const item = Item_Get(i);\n        if (item->object_id != M_BOSS_TYPE) {\n            continue;\n        }\n\n        if (item->status == IS_ACTIVE || item->status == IS_DEACTIVATED) {\n            best_item = i;\n            break;\n        }\n\n        const ITEM *const lara_item = Lara_GetItem();\n        const GAME_VECTOR start = {\n            .x = lara_item->pos.x,\n            .y = lara_item->pos.y - STEP_L * 2,\n            .z = lara_item->pos.z,\n            .room_num = lara_item->room_num,\n        };\n\n        GAME_VECTOR target = {\n            .x = item->pos.x,\n            .y = item->pos.y - STEP_L * 2,\n            .z = item->pos.z,\n            .room_num = item->room_num,\n        };\n\n        if (!LOS_Check(&start, &target, true)) {\n            const int32_t dx = (lara_item->pos.x - item->pos.x) >> 6;\n            const int32_t dy = (lara_item->pos.y - item->pos.y) >> 6;\n            const int32_t dz = (lara_item->pos.z - item->pos.z) >> 6;\n            const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n            if (dist < best_dist) {\n                best_dist = dist;\n                best_item = i;\n            }\n        }\n    }\n    return best_item;\n}\n\nstatic void M_ActivateNearestBoss(void)\n{\n    const int16_t item_num = M_FindNearestBoss();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_ACTIVE && item->status != IS_DEACTIVATED) {\n        item->touch_bits = 0;\n        item->status = IS_ACTIVE;\n        item->mesh_bits = 0xFFFF1FFF;\n        Item_AddActive(item_num);\n        LOT_EnableBaddieAI(item_num, true);\n    }\n}\n\nstatic void M_PrepareCutscene(const int16_t item_num)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_type == LGT_FLARE) {\n        Lara_Flare_Undraw();\n        lara->flare.control = false;\n        lara->left_arm.lock = false;\n    }\n\n    Lara_Vehicle_Dismount();\n    Gun_SetLaraHandLMesh(LGT_UNARMED);\n    Gun_SetLaraHandRMesh(LGT_UNARMED);\n    lara->water_status = LWS_ABOVE_WATER;\n    lara->target = nullptr;\n\n    ITEM *const item = Item_Get(item_num);\n    Creature_SpecialKill(item, 0, 0, LS_EXTRA_END_HOUSE);\n\n    Camera_InvokeCinematic(item, 428, 0);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    const int32_t alive_enemies = M_CountAliveEnemies();\n    const int32_t is_boss_dead = M_IsBossDead();\n    if (alive_enemies == 0 && m_BossTimer == 0) {\n        m_BossTimer = 1;\n        M_ActivateNearestBoss();\n    } else if (alive_enemies == 0 && is_boss_dead) {\n        m_BossTimer++;\n        if (m_BossTimer == M_CUTSCENE_DELAY) {\n            M_PrepareCutscene(item_num);\n        }\n    }\n}\n\nOBJECT_ID CombatEnd_GetBossType(void)\n{\n    return M_BOSS_TYPE;\n}\n\nbool CombatEnd_IsWaitingForBoss(void)\n{\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        if (Item_Get(i)->object_id == O_COMBAT_END) {\n            return m_BossTimer == 0;\n        }\n    }\n    return false;\n}\n\nbool CombatEnd_IsComplete(void)\n{\n    return m_BossTimer >= M_CUTSCENE_DELAY;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n\n    m_BossTimer = 0;\n}\n\nREGISTER_OBJECT(O_COMBAT_END, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/combat_end.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n\nOBJECT_ID CombatEnd_GetBossType(void);\nbool CombatEnd_IsWaitingForBoss(void);\nbool CombatEnd_IsComplete(void);\n"
  },
  {
    "path": "src/trx/game/objects/general/copter.c",
    "content": "#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\ntypedef enum {\n    // clang-format off\n    COPTER_STATE_EMPTY   = 0,\n    COPTER_STATE_SPIN    = 1,\n    COPTER_STATE_TAKEOFF = 2,\n    // clang-format on\n} COPTER_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (item->current_anim_state == COPTER_STATE_SPIN\n        && (item->flags & IF_ONE_SHOT)) {\n        item->goal_anim_state = COPTER_STATE_TAKEOFF;\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n    XYZ_32 pos = {\n        .x = (bounds->min.x + bounds->max.x) / 2,\n        .y = (bounds->min.y + bounds->max.y) / 2,\n        .z = (bounds->min.z + bounds->max.z) / 2,\n    };\n    pos.x = lara_item->pos.x + ((pos.x - lara_item->pos.x) >> 2);\n    pos.y = lara_item->pos.y + ((pos.y - lara_item->pos.y) >> 2);\n    pos.z = lara_item->pos.z + ((pos.z - lara_item->pos.z) >> 2);\n    Sound_Effect(SFX_HELICOPTER_LOOP, &pos, SPM_NORMAL);\n\n    if (item->status == IS_DEACTIVATED) {\n        Item_Kill(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_COPTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/cutscene_player.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/creature.h>\n#include <trx/game/cutscene.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Item_AddActive(item_num);\n    ITEM *const item = Item_Get(item_num);\n    item->rot.y = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->status = IS_ACTIVE;\n    CAMERA_INFO *const camera = Cutscene_GetCamera();\n    item->rot.y = camera->target_angle;\n    item->pos = camera->pos.pos;\n\n    XYZ_32 pos = {};\n    Collide_GetJointAbsPosition(item, &pos, 0);\n\n    const int16_t room_num = Room_GetIndexFromPos(pos);\n    if (room_num != NO_ROOM) {\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    int16_t floor_room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &floor_room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    item->floor = height == NO_HEIGHT ? pos.y : height;\n\n    if (item->dynamic_light && item->status != IS_INVISIBLE) {\n        pos.x = 0;\n        pos.y = 0;\n        pos.z = 0;\n        Collide_GetJointAbsPosition(item, &pos, 0);\n        Output_AddDynamicLight(pos, 12, 11);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->shadow_size = (UNIT_SHADOW * 10) / 16;\n    obj->control_func = M_Control;\n    obj->hit_points = 1;\n}\n\nREGISTER_OBJECT(O_PLAYER_1, M_Setup)\nREGISTER_OBJECT(O_PLAYER_2, M_Setup)\nREGISTER_OBJECT(O_PLAYER_3, M_Setup)\nREGISTER_OBJECT(O_PLAYER_4, M_Setup)\nREGISTER_OBJECT(O_PLAYER_5, M_Setup)\nREGISTER_OBJECT(O_PLAYER_6, M_Setup)\nREGISTER_OBJECT(O_PLAYER_7, M_Setup)\nREGISTER_OBJECT(O_PLAYER_8, M_Setup)\nREGISTER_OBJECT(O_PLAYER_9, M_Setup)\nREGISTER_OBJECT(O_PLAYER_10, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/detonator_box.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/general/pickup.h>\n#include <trx/game/output.h>\n#include <trx/game/sound.h>\n\n#define M_EXPLOSION_ACTION_FRAME 80\n\nstatic XYZ_32 m_Position = { .x = 0, .y = 0, .z = 0 };\n\nstatic const OBJECT_BOUNDS m_Bounds = {\n    .shift = {\n        .min = { .x = -WALL_L / 4, .y = -100, .z = -WALL_L / 4, },\n        .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 4, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = 0, .z = 0, },\n        .max = { .x = +10 * DEG_1, .y = 0, .z = 0, },\n    },\n    .ignore_rot = true,\n};\n\nstatic void M_ConsumeKeyItem(ITEM *const receptacle_item)\n{\n    const OBJECT_ID key_object_id =\n        Object_FindReceptacleKey(receptacle_item->object_id);\n    if (key_object_id != NO_OBJECT) {\n        Inv_RemoveItem(key_object_id);\n    }\n}\n\nstatic void M_Use(ITEM *const lara_item, ITEM *const receptacle_item)\n{\n    receptacle_item->rot.y = lara_item->rot.y;\n    Lara_AlignPosition(receptacle_item, &m_Position);\n\n    Lara_SwitchToExtraState(LS_EXTRA_PLUNGER);\n    if (Item_TestFrameEqual(lara_item, 0)) {\n        M_ConsumeKeyItem(receptacle_item);\n    }\n\n    receptacle_item->status = IS_ACTIVE;\n    Item_AddActive(Item_GetIndex(receptacle_item));\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->interact_target.is_moving = false;\n    lara->interact_target.item_num = NO_ITEM;\n}\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_Bounds;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_Animate(item);\n\n    if (item->dynamic_light) {\n        Output_AddDynamicLight(item->pos, 13, 11);\n    }\n\n    if (Item_TestFrameEqual(item, M_EXPLOSION_ACTION_FRAME)) {\n        g_Camera.bounce = -150;\n        Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_ALWAYS);\n    }\n\n    if (item->status == IS_DEACTIVATED) {\n        Item_RemoveActive(item_num);\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->extra_anim) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (lara->interact_target.is_moving\n        && lara->interact_target.item_num == item_num) {\n        M_Use(lara_item, item);\n        return;\n    }\n\n    if (item->status != IS_INACTIVE || !g_Input.action\n        || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)) {\n        goto normal_collision;\n    }\n\n    const XYZ_16 old_rot = item->rot;\n    item->rot.x = 0;\n    item->rot.y = lara_item->rot.y;\n    item->rot.z = 0;\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        item->rot = old_rot;\n        goto normal_collision;\n    }\n\n    item->rot = old_rot;\n\n    if (!GF_ShowInventoryKeys(item->object_id)) {\n        Lara_RefuseInteraction();\n    }\n\n    return;\n\nnormal_collision:\n    Object_Collision(item_num, lara_item, coll);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->collision_func = M_Collision;\n    obj->control_func = M_Control;\n    obj->bounds_func = M_Bounds;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_DETONATOR_BOX, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/ding_dong.c",
    "content": "#include <trx/game/objects/common.h>\n#include <trx/game/sound.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) {\n        Sound_Effect(SFX_DOORBELL, &item->pos, SPM_NORMAL);\n        item->flags -= IF_CODE_BITS;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n}\n\nREGISTER_OBJECT(O_DING_DONG, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/disposable_animating.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_IsTriggerActive(item)) {\n        Item_Kill(item_num);\n        return;\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->save_position = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_1, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_2, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_3, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_4, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_5, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_6, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_7, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_8, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_9, M_Setup)\nREGISTER_OBJECT(O_DISPOSABLE_ANIMATING_10, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/door.c",
    "content": "#include <trx/game/objects/general/door.h>\n\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\ntypedef struct {\n    SECTOR *sector;\n    SECTOR old_sector;\n    int16_t box_num;\n} M_DOOR_POS;\n\ntypedef struct {\n    M_DOOR_POS d1;\n    M_DOOR_POS d1flip;\n    M_DOOR_POS d2;\n    M_DOOR_POS d2flip;\n} M_PRIV;\n\nstatic const SECTOR m_BlockedSector = {\n    .idx = 0,\n    .box = NO_BOX,\n    .ceiling.height = NO_HEIGHT,\n    .floor.height = NO_HEIGHT,\n    .ceiling.tilt = {},\n    .floor.tilt = {},\n    .portal_room.sky = NO_ROOM,\n    .portal_room.pit = NO_ROOM,\n    .portal_room.wall = NO_ROOM,\n};\n\nstatic SECTOR *M_GetRoomRelSector(\n    const ROOM *const room, const ITEM *item, const int32_t sector_dx,\n    const int32_t sector_dz)\n{\n    const XZ_32 sector = {\n        .x = ((item->pos.x - room->pos.x) >> WALL_SHIFT) + sector_dx,\n        .z = ((item->pos.z - room->pos.z) >> WALL_SHIFT) + sector_dz,\n    };\n    return Room_GetUnitSector(room, sector.x, sector.z);\n}\n\nstatic bool M_LaraDoorCollision(const SECTOR *const sector)\n{\n    // Check if Lara is on the same tile as the invisible block.\n    const ITEM *const lara = Lara_GetItem();\n    if (lara == nullptr) {\n        return false;\n    }\n\n    int16_t room_num = lara->room_num;\n    const SECTOR *const lara_sector = Room_GetSector(lara->pos, &room_num);\n    return lara_sector == sector;\n}\n\nstatic void M_CopySectorProperties(\n    const SECTOR *const source_sector, SECTOR *const target_sector)\n{\n    target_sector->idx = source_sector->idx;\n    target_sector->box = source_sector->box;\n    target_sector->ceiling.height = source_sector->ceiling.height;\n    target_sector->floor.height = source_sector->floor.height;\n    target_sector->floor.tilt = source_sector->floor.tilt;\n    target_sector->ceiling.tilt = source_sector->ceiling.tilt;\n    target_sector->portal_room.sky = source_sector->portal_room.sky;\n    target_sector->portal_room.pit = source_sector->portal_room.pit;\n    target_sector->portal_room.wall = source_sector->portal_room.wall;\n}\n\nstatic void M_Open(M_DOOR_POS *const d)\n{\n    if (d->sector == nullptr) {\n        return;\n    }\n\n    M_CopySectorProperties(&d->old_sector, d->sector);\n\n    const int16_t box_num = d->box_num;\n    if (box_num != NO_BOX) {\n        Box_GetBox(box_num)->overlap_index &= ~BOX_BLOCKED;\n    }\n}\n\nstatic void M_Check(M_DOOR_POS *const d)\n{\n    // Forcefully remove the invisible block if Lara happens to occupy the same\n    // tile. This ensures that Lara doesn't void if a timed door happens to\n    // close right on her, or the player loads the game while standing on a\n    // closed door's block tile.\n    if (M_LaraDoorCollision(d->sector)) {\n        M_Open(d);\n    }\n}\n\nstatic void M_Shut(M_DOOR_POS *const d)\n{\n    if (d->sector == nullptr) {\n        return;\n    }\n\n    M_CopySectorProperties(&m_BlockedSector, d->sector);\n\n    const int16_t box_num = d->box_num;\n    if (box_num != NO_BOX) {\n        Box_GetBox(box_num)->overlap_index |= BOX_BLOCKED;\n    }\n}\n\nstatic void M_InitialisePortal(\n    const ROOM *const room, const ITEM *const item, const int32_t sector_dx,\n    const int32_t sector_dz, M_DOOR_POS *const door_pos)\n{\n    door_pos->sector = M_GetRoomRelSector(room, item, sector_dx, sector_dz);\n\n    const SECTOR *sector = door_pos->sector;\n\n    const int16_t room_num = door_pos->sector->portal_room.wall;\n    if (room_num != NO_ROOM) {\n        sector =\n            M_GetRoomRelSector(Room_Get(room_num), item, sector_dx, sector_dz);\n    }\n\n    int16_t box_num = sector->box;\n    const BOX_INFO *const box = Box_GetBox(box_num);\n    if ((box->overlap_index & BOX_BLOCKABLE) == 0) {\n        box_num = NO_BOX;\n    }\n    door_pos->box_num = box_num;\n    door_pos->old_sector = *door_pos->sector;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    int32_t dx = 0;\n    int32_t dz = 0;\n    if (item->rot.y == 0) {\n        dz = -1;\n    } else if (item->rot.y == -DEG_180) {\n        dz = 1;\n    } else if (item->rot.y == DEG_90) {\n        dx = -1;\n    } else {\n        dx = 1;\n    }\n\n    int16_t room_num = item->room_num;\n    const ROOM *room = Room_Get(room_num);\n    M_InitialisePortal(room, item, dx, dz, &p->d1);\n\n    if (room->flipped_room == NO_ROOM) {\n        p->d1flip.sector = nullptr;\n    } else {\n        room = Room_Get(room->flipped_room);\n        M_InitialisePortal(room, item, dx, dz, &p->d1flip);\n    }\n\n    room_num = p->d1.sector->portal_room.wall;\n    M_Shut(&p->d1);\n    M_Shut(&p->d1flip);\n\n    if (room_num == NO_ROOM) {\n        p->d2.sector = nullptr;\n        p->d2flip.sector = nullptr;\n    } else {\n        room = Room_Get(room_num);\n        M_InitialisePortal(room, item, 0, 0, &p->d2);\n        if (room->flipped_room == NO_ROOM) {\n            p->d2flip.sector = nullptr;\n        } else {\n            room = Room_Get(room->flipped_room);\n            M_InitialisePortal(room, item, 0, 0, &p->d2flip);\n        }\n\n        M_Shut(&p->d2);\n        M_Shut(&p->d2flip);\n\n        const int16_t prev_room = item->room_num;\n        Item_UpdateRoom(item_num, room_num);\n        item->room_num = prev_room;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (Item_IsTriggerActive(item)) {\n        if (item->current_anim_state == DOOR_STATE_CLOSED) {\n            item->goal_anim_state = DOOR_STATE_OPEN;\n        } else {\n            M_Open(&p->d1);\n            M_Open(&p->d2);\n            M_Open(&p->d1flip);\n            M_Open(&p->d2flip);\n        }\n    } else {\n        if (item->current_anim_state == DOOR_STATE_OPEN) {\n            item->goal_anim_state = DOOR_STATE_CLOSED;\n        } else {\n            M_Shut(&p->d1);\n            M_Shut(&p->d2);\n            M_Shut(&p->d1flip);\n            M_Shut(&p->d2flip);\n        }\n    }\n\n    M_Check(&p->d1);\n    M_Check(&p->d2);\n    M_Check(&p->d1flip);\n    M_Check(&p->d2flip);\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->draw_func = Object_DrawUnclippedItem;\n    obj->collision_func = Door_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nvoid Door_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) {\n        return;\n    }\n\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    if (coll->enable_baddie_push) {\n        Lara_Col_ItemPush(\n            item, coll,\n            coll->enable_hit\n                && item->current_anim_state != item->goal_anim_state,\n            true);\n    }\n}\n\nREGISTER_OBJECT(O_DOOR_TYPE_1, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_2, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_3, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_4, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_5, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_6, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_7, M_Setup)\nREGISTER_OBJECT(O_DOOR_TYPE_8, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/door.h",
    "content": "#pragma once\n\n#include <trx/game/collision.h>\n\ntypedef enum {\n    DOOR_STATE_CLOSED = 0,\n    DOOR_STATE_OPEN = 1,\n} DOOR_STATE;\n\nvoid Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n"
  },
  {
    "path": "src/trx/game/objects/general/drawbridge.c",
    "content": "#include <trx/config.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/items.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/door.h>\n#include <trx/game/objects/traps/movable_block.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    DRAWBRIDGE_STATE_CLOSED = DOOR_STATE_CLOSED,\n    DRAWBRIDGE_STATE_OPEN = DOOR_STATE_OPEN,\n} DRAWBRIDGE_STATE;\n\ntypedef enum {\n    DRAWBRIDGE_ANIM_CLOSED = 3,\n} DRAWBRIDGE_ANIM;\n\nstatic bool M_IsItemOnTop(const ITEM *item, int32_t x, int32_t z)\n{\n    int32_t ix = item->pos.x >> WALL_SHIFT;\n    int32_t iz = item->pos.z >> WALL_SHIFT;\n    x >>= WALL_SHIFT;\n    z >>= WALL_SHIFT;\n\n    if (item->rot.y == 0 && x == ix && (z == iz - 1 || z == iz - 2)) {\n        return true;\n    } else if (\n        item->rot.y == -DEG_180 && x == ix && (z == iz + 1 || z == iz + 2)) {\n        return true;\n    } else if (\n        item->rot.y == DEG_90 && z == iz && (x == ix - 1 || x == ix - 2)) {\n        return true;\n    } else if (\n        item->rot.y == -DEG_90 && z == iz && (x == ix + 1 || x == ix + 2)) {\n        return true;\n    }\n\n    return false;\n}\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (item->current_anim_state != DRAWBRIDGE_STATE_OPEN) {\n        return height;\n    } else if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    } else if (y > item->pos.y) {\n        return height;\n    } else if (\n        g_Config.gameplay.fix_bridge_collision && item->pos.y >= height) {\n        return height;\n    }\n    return item->pos.y;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (item->current_anim_state != DRAWBRIDGE_STATE_OPEN) {\n        return height;\n    } else if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    } else if (y <= item->pos.y) {\n        return height;\n    } else if (\n        g_Config.gameplay.fix_bridge_collision && item->pos.y <= height) {\n        return height;\n    }\n    return item->pos.y + STEP_L;\n}\n\nstatic BOUNDS_16 M_RotateBounds(const BOUNDS_16 bounds, int16_t rot_y)\n{\n    BOUNDS_16 rot_bounds = {};\n\n    switch (rot_y) {\n    case 0:\n    default:\n        rot_bounds = bounds;\n        break;\n    case DEG_90:\n        rot_bounds.min.x = bounds.min.z;\n        rot_bounds.max.x = bounds.max.z;\n        rot_bounds.min.z = -bounds.max.x;\n        rot_bounds.max.z = -bounds.min.x;\n        break;\n    case -DEG_180:\n        rot_bounds.min.x = -bounds.max.x;\n        rot_bounds.max.x = -bounds.min.x;\n        rot_bounds.min.z = -bounds.max.z;\n        rot_bounds.max.z = -bounds.min.z;\n        break;\n    case -DEG_90:\n        rot_bounds.min.x = -bounds.max.z;\n        rot_bounds.max.x = -bounds.min.z;\n        rot_bounds.min.z = bounds.min.x;\n        rot_bounds.max.z = bounds.max.x;\n        break;\n    }\n    return rot_bounds;\n}\n\nstatic void M_GetSectorPositions(const ITEM *const item, VECTOR *sector_pos)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const ANIM_FRAME *const frame =\n        Object_GetAnim(obj, DRAWBRIDGE_ANIM_CLOSED)->frame_ptr;\n    const BOUNDS_16 rot_bounds = M_RotateBounds(frame->bounds, item->rot.y);\n\n    const int32_t x0 = item->pos.x + rot_bounds.min.x;\n    const int32_t x1 = item->pos.x + rot_bounds.max.x - 1; // inclusive\n    const int32_t z0 = item->pos.z + rot_bounds.min.z;\n    const int32_t z1 = item->pos.z + rot_bounds.max.z - 1;\n\n    const int32_t sx0 = Math_FloorDiv(x0, WALL_L);\n    const int32_t sx1 = Math_FloorDiv(x1, WALL_L);\n    const int32_t sz0 = Math_FloorDiv(z0, WALL_L);\n    const int32_t sz1 = Math_FloorDiv(z1, WALL_L);\n\n    // Sector of the drawbridge original position.\n    const int32_t sx_orig = Math_FloorDiv(item->pos.x, WALL_L);\n    const int32_t sz_orig = Math_FloorDiv(item->pos.z, WALL_L);\n\n    for (int32_t sx = sx0; sx <= sx1; ++sx) {\n        for (int32_t sz = sz0; sz <= sz1; ++sz) {\n            if (sx == sx_orig && sz == sz_orig) {\n                // Skip the original sector since it's WALL_L away.\n                continue;\n            }\n            const XYZ_32 pos = {\n                .x = sx * WALL_L + WALL_L / 2,\n                .y = item->pos.y,\n                .z = sz * WALL_L + WALL_L / 2,\n            };\n            Vector_Add(sector_pos, &pos);\n        }\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    VECTOR *const positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    Walkable_AllocateNodes(item, positions->count);\n    Vector_Free(positions);\n}\n\nstatic void M_DropStack(const ITEM *const item)\n{\n    VECTOR *positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    for (int32_t i = 0; i < positions->count; i++) {\n        MovableBlock_DropStack(\n            *(const XYZ_32 *)Vector_Get(positions, i), item->room_num);\n    }\n    Vector_Free(positions);\n}\n\nstatic void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll)\n{\n    const ITEM *const item = Item_Get(item_num);\n    if (item->current_anim_state == DRAWBRIDGE_STATE_CLOSED) {\n        Door_Collision(item_num, lara_item, coll);\n    }\n}\n\nstatic void M_Control(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Item_IsTriggerActive(item)) {\n        item->goal_anim_state = DRAWBRIDGE_STATE_OPEN;\n    } else {\n        item->goal_anim_state = DRAWBRIDGE_STATE_CLOSED;\n        if (item->current_anim_state == DRAWBRIDGE_STATE_OPEN) {\n            M_DropStack(item);\n        }\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    VECTOR *positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    for (int32_t i = 0; i < positions->count; i++) {\n        Walkable_Add(item_num, *(const XYZ_32 *)Vector_Get(positions, i));\n    }\n    Vector_Free(positions);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->collision_func = M_Collision;\n    obj->control_func = M_Control;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->add_walkable_func = M_AddWalkable;\n}\n\nREGISTER_OBJECT(O_DRAWBRIDGE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/dummy.c",
    "content": "#include <trx/game/objects.h>\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->draw_func = nullptr;\n}\n\nREGISTER_OBJECT(O_DUMMY, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/earthquake.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\ntypedef struct {\n    int32_t shake_intensity;\n    int32_t target_intensity;\n    int32_t target_timer;\n} M_PRIV;\n\nstatic void M_ActivateRelatedItem(ITEM *const earth_item)\n{\n    Item_AddActive(Item_GetIndex(earth_item));\n    earth_item->status = IS_ACTIVE;\n    earth_item->flags = IF_CODE_BITS;\n    earth_item->timer = 0;\n}\n\nstatic void M_FindAndActivateRelatedItems(const ITEM *const item)\n{\n    OBJECT_ID object_id_to_activate = NO_OBJECT;\n    const int32_t random = Random_GetControl();\n    if (random < 512) {\n        object_id_to_activate = O_FLAME_EMITTER;\n    } else if (random < 1024) {\n        object_id_to_activate = O_FALLING_CEILING_1;\n    }\n    if (object_id_to_activate == NO_OBJECT\n        || !Object_Get(object_id_to_activate)->loaded) {\n        return;\n    }\n\n    int16_t related_item_num = Room_Get(item->room_num)->item_num;\n    while (related_item_num != NO_ITEM) {\n        ITEM *const earth_item = Item_Get(related_item_num);\n        if (earth_item->object_id == object_id_to_activate\n            && earth_item->status != IS_ACTIVE\n            && earth_item->status != IS_DEACTIVATED) {\n            M_ActivateRelatedItem(earth_item);\n            break;\n        }\n        related_item_num = earth_item->next_item;\n    }\n}\n\nstatic void M_Reset(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->shake_intensity = 0;\n    p->target_intensity = 0;\n    p->target_timer = 0;\n    item->status = IS_INACTIVE;\n    Item_RemoveActive(item_num);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (!Item_IsTriggerActive(item)) {\n        M_Reset(item_num);\n        return;\n    }\n\n    switch (g_TRVersion) {\n    case 1:\n        if (Random_GetDraw() < 256) {\n            g_Camera.bounce = -150;\n            Sound_Effect(SFX_EARTHQUAKE_1, nullptr, SPM_NORMAL);\n        } else if (Random_GetControl() < 1024) {\n            g_Camera.bounce = 50;\n            Sound_Effect(SFX_EARTHQUAKE_2, nullptr, SPM_NORMAL);\n        }\n        break;\n\n    case 2:\n        if (Random_GetDraw() < 512) {\n            Sound_Effect(SFX_EARTHQUAKE_1, nullptr, SPM_NORMAL);\n            g_Camera.bounce = -200;\n        }\n        break;\n\n    case 3: {\n        if (p->target_intensity == 0) {\n            p->target_intensity = 100;\n        }\n\n        if (p->target_timer == 0\n            && ABS(p->shake_intensity - p->target_intensity) < 16) {\n            if (p->target_intensity == 20) {\n                p->target_intensity = 100;\n                p->target_timer = (Random_GetControl() & 0x7F) + 90;\n            } else {\n                p->target_intensity = 20;\n                p->target_timer = (Random_GetControl() & 0x7F) + 30;\n            }\n        }\n\n        if (p->target_timer != 0) {\n            p->target_timer--;\n        }\n\n        if (p->shake_intensity > p->target_intensity) {\n            p->shake_intensity -= (Random_GetControl() & 7) + 2;\n        } else {\n            p->shake_intensity += (Random_GetControl() & 7) + 2;\n        }\n\n        Sound_Effect(\n            SFX_EARTHQUAKE_LOOP, nullptr,\n            ((p->shake_intensity << 16) + 0x1000000) | SPM_PITCH);\n        g_Camera.bounce = -p->shake_intensity;\n        break;\n    }\n    }\n\n    M_FindAndActivateRelatedItems(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_EARTHQUAKE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/final_cutscene.c",
    "content": "#include <trx/game/const.h>\n#include <trx/game/game.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/combat_end.h>\n#include <trx/game/output.h>\n\n#define M_CUTSCENE_DURATION (15 * LOGIC_FPS)\n#define M_FADE_DURATION (3 * LOGIC_FPS)\n\nstatic int32_t m_FadeTimer = -1;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (CombatEnd_IsComplete()) {\n        item->status = IS_ACTIVE;\n        Item_Animate(item);\n\n        if (m_FadeTimer == -1) {\n            m_FadeTimer = M_CUTSCENE_DURATION;\n        } else if (m_FadeTimer > 0) {\n            m_FadeTimer--;\n        }\n    } else {\n        item->status = IS_INVISIBLE;\n        m_FadeTimer = -1;\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    Object_DrawAnimatingItem(item);\n\n    if (m_FadeTimer < 0 || m_FadeTimer > M_FADE_DURATION) {\n        return true;\n    }\n    const float opacity = 1.0f - (m_FadeTimer / (float)M_FADE_DURATION);\n    Output_Overlay_DrawBlackRectangle(opacity, false);\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_CUT_SHOTGUN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/flare_item.c",
    "content": "#include <trx/game/objects/general/flare_item.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/gun.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/pickup.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_FLARE_INTENSITY 12\n#define M_FLARE_FALL_OFF  11\n\n#define M_MAX_FLARE_AGE_TR12   (60 * LOGIC_FPS)                       // = 1800\n#define M_FLARE_OLD_AGE_TR12   (M_MAX_FLARE_AGE_TR12 - 2 * LOGIC_FPS) // = 1740\n#define M_FLARE_YOUNG_AGE_TR12 (LOGIC_FPS)                            // = 30\n\n#define M_MAX_FLARE_AGE_TR3   (30 * LOGIC_FPS)         // = 900\n#define M_FLARE_DYING_AGE_TR3 (M_MAX_FLARE_AGE_TR3 - 90) // = 810\n#define M_FLARE_END_AGE_TR3   (M_MAX_FLARE_AGE_TR3 - 24) // = 876\n// clang-format on\n\ntypedef struct {\n    int32_t raw_age;\n} M_PRIV;\n\nstatic XYZ_32 M_TransformLocalOffset(\n    const XYZ_32 pos, const XYZ_16 rot, const XYZ_32 local_offset)\n{\n    Matrix_PushUnit();\n    Matrix_TranslateAbs32(pos);\n    Matrix_Rot16(rot);\n    Matrix_TranslateRel32(local_offset);\n    const XYZ_32 out = {\n        .x = g_WMatrixPtr->_03 >> W2V_SHIFT,\n        .y = g_WMatrixPtr->_13 >> W2V_SHIFT,\n        .z = g_WMatrixPtr->_23 >> W2V_SHIFT,\n    };\n    Matrix_Pop();\n    return out;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const ROOM *const room = Room_Get(item->room_num);\n    if (room->flags.swamp) {\n        Item_Kill(item_num);\n        return;\n    }\n\n    if (item->fall_speed != 0) {\n        item->rot.x += DEG_1 * 3;\n        item->rot.z += DEG_1 * 5;\n    } else {\n        item->rot.x = 0;\n        item->rot.z = 0;\n    }\n\n    const XYZ_32 old_pos = item->pos;\n    item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed);\n\n    if (room->flags.underwater) {\n        item->fall_speed += (5 - item->fall_speed) / 2;\n        item->speed = item->speed + (5 - item->speed) / 2;\n    } else {\n        item->fall_speed += GRAVITY;\n    }\n    item->pos.y += item->fall_speed;\n\n    Collide_DoProperDetection(item, old_pos);\n\n    int32_t flare_age = FlareItem_GetAge(item);\n    bool is_active = FlareItem_IsActive(item);\n    if (flare_age < Flare_GetMaxAge()) {\n        flare_age++;\n    } else if (item->fall_speed == 0 && item->speed == 0) {\n        Item_Kill(item_num);\n        return;\n    }\n\n    if (Flare_GenerateLight(item->pos, flare_age)) {\n        is_active = true;\n        Flare_GenerateEffects(&item->pos, item->pos, item->room_num);\n    }\n\n    if (g_TRVersion >= 3) {\n        if (flare_age < Flare_GetMaxAge() && is_active) {\n            const BOUNDS_16 *const bounds = &Item_GetBestFrame(item)->bounds;\n            const XYZ_32 flare_size = {\n                .x = bounds->max.x - bounds->min.x,\n                .y = bounds->max.y - bounds->min.y,\n                .z = bounds->max.z - bounds->min.z,\n            };\n            const XYZ_32 flare_offset = {\n                .x = -flare_size.x,\n                .y = -flare_size.y,\n                .z = -flare_size.z,\n            };\n\n            const XYZ_32 flare_pos = item->pos;\n            const XYZ_32 tip_local = {\n                .x = flare_offset.x - 6,\n                .y = flare_offset.y + 6,\n                .z = flare_offset.z + 32,\n            };\n            const XYZ_32 tip_pos =\n                M_TransformLocalOffset(flare_pos, item->rot, tip_local);\n\n            const XYZ_32 vel_local = {\n                .x = (Random_GetControl() & 0x7F) - 64,\n                .y = (Random_GetControl() & 0x7F) - 64,\n                .z = (Random_GetControl() & 0x1FF) + 512,\n            };\n            const XYZ_32 vel_pos =\n                M_TransformLocalOffset(flare_pos, item->rot, vel_local);\n            const XYZ_32 vel = {\n                .x = vel_pos.x - flare_pos.x,\n                .y = vel_pos.y - flare_pos.y,\n                .z = vel_pos.z - flare_pos.z,\n            };\n\n            for (int32_t i = 0; i < (Random_GetControl() & 3) + 4; i++) {\n                const bool smoke = (i >> 2) != 0;\n                Sparks_TriggerFlareSparks(tip_pos, vel, smoke);\n            }\n        }\n    }\n\n    FlareItem_SetAge(item, flare_age, is_active);\n}\n\nstatic void M_DrawFlash(const CLIP clip)\n{\n    WEAPON_INFO *const flare_info = &g_Weapons[LGT_FLARE];\n    SWAP(flare_info->flash_pos, flare_info->flash_pos_alt);\n    Gun_DrawFlash(LGT_FLARE, clip, false);\n    SWAP(flare_info->flash_pos, flare_info->flash_pos_alt);\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    int32_t rate;\n    ANIM_FRAME *frames[2];\n    Item_GetFrames(item, frames, &rate);\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n    const CLIP clip = Output_CheckBoundsClip(&frames[0]->bounds);\n\n    const XYZ_32 flare_size = {\n        .x = frames[0]->bounds.max.x - frames[0]->bounds.min.x,\n        .y = frames[0]->bounds.max.y - frames[0]->bounds.min.y,\n        .z = frames[0]->bounds.max.z - frames[0]->bounds.min.z,\n    };\n    const XYZ_32 flare_offset = {\n        .x = -flare_size.x,\n        .y = -flare_size.y,\n        .z = -flare_size.z,\n    };\n    Matrix_TranslateRel32(flare_offset);\n\n    if (clip == CLIP_NOT_VISIBLE) {\n        goto end;\n    }\n\n    Output_CalculateObjectLighting(item, &frames[0]->bounds);\n    Object_DrawMesh(Object_Get(O_FLARE_ITEM)->mesh_idx, clip, false);\n    if (!FlareItem_IsActive(item)) {\n        goto end;\n    }\n\n    if (g_TRVersion >= 3) {\n        goto end;\n    }\n\n    M_DrawFlash(clip);\n\nend:\n    Matrix_Pop();\n    return true;\n}\n\nstatic bool M_GenerateLight_TR12(const XYZ_32 pos, const int32_t flare_age)\n{\n    if (flare_age >= M_MAX_FLARE_AGE_TR12) {\n        return false;\n    }\n\n    const int32_t random = Random_GetDraw();\n    const XYZ_32 light_pos = {\n        .x = pos.x + (random & 0xA0),\n        .y = pos.y,\n        .z = pos.z,\n    };\n\n    if (flare_age < M_FLARE_YOUNG_AGE_TR12) {\n        const int32_t intensity = M_FLARE_INTENSITY\n                * (flare_age - M_FLARE_YOUNG_AGE_TR12)\n                / (2 * M_FLARE_YOUNG_AGE_TR12)\n            + M_FLARE_INTENSITY;\n        Output_AddDynamicLight(light_pos, intensity, M_FLARE_FALL_OFF);\n        return true;\n    }\n\n    if (flare_age < M_FLARE_OLD_AGE_TR12) {\n        Output_AddDynamicLight(light_pos, M_FLARE_INTENSITY, M_FLARE_FALL_OFF);\n        return true;\n    }\n\n    if (random > 0x2000) {\n        Output_AddDynamicLight(\n            light_pos, M_FLARE_INTENSITY - (random & 3), M_FLARE_FALL_OFF);\n        return true;\n    }\n\n    Output_AddDynamicLight(light_pos, M_FLARE_INTENSITY, M_FLARE_FALL_OFF / 2);\n    return false;\n}\n\nstatic bool M_GenerateLight_TR3(const XYZ_32 pos, const int32_t flare_age)\n{\n    if (flare_age >= M_MAX_FLARE_AGE_TR3) {\n        return false;\n    }\n\n    const int32_t rnd = Random_GetControl();\n    const XYZ_32 light_pos = {\n        .x = pos.x + ((rnd & 0xF) << 3),\n        .y = pos.y + ((rnd >> 1) & 0x78),\n        .z = pos.z + ((rnd >> 5) & 0x78),\n    };\n\n    int32_t r = 0;\n    int32_t g = 0;\n    int32_t b = 0;\n    int32_t falloff = 0;\n\n    if (flare_age < 4) {\n        r = (rnd & 0x1F) + (flare_age << 4) + 160;\n        g = ((rnd >> 4) & 0x1F) + (flare_age << 3) + 32;\n        b = ((rnd >> 8) & 0x1F) + (flare_age << 4);\n        falloff = (rnd & 3) + (flare_age << 2) + 4;\n\n        if (falloff > 16) {\n            falloff -= (rnd >> 12) & 3;\n        }\n    } else if (flare_age < 16) {\n        r = (rnd & 0x3F) + (flare_age << 2) + 128;\n        g = ((rnd >> 4) & 0x1F) + (flare_age << 2) + 64;\n        b = ((rnd >> 8) & 0x1F) + (flare_age << 2) + 16;\n        falloff = (rnd & 1) + flare_age + 2;\n    } else if (flare_age < M_FLARE_DYING_AGE_TR3) {\n        r = (rnd & 0x3F) + 192;\n        g = ((rnd >> 4) & 0x1F) + 128;\n        b = ((rnd >> 8) & 0x20) + (((rnd >> 6) & 0x10) << 1);\n        falloff = 16;\n    } else if (flare_age < M_FLARE_END_AGE_TR3) {\n        if (rnd > 0x2000) {\n            r = (rnd & 0x3F) + 192;\n            g = ((rnd >> 4) & 0x1F) + 64;\n            b = ((rnd >> 8) & 0x20) + (((rnd >> 6) & 0x10) << 1);\n            falloff = 16;\n        } else {\n            const int32_t rnd2 = Random_GetControl();\n            const int32_t rnd3 = Random_GetControl();\n            const int32_t rnd4 = Random_GetControl();\n            r = (rnd2 & 0x3F) + 192;\n            g = (rnd3 & 0x3F) + 64;\n            b = rnd4 & 0x7F;\n            falloff = (Random_GetControl() & 6) + 8;\n            Output_AddDynamicLightRGB(\n                light_pos, falloff, (RGB_888) { r, g, b });\n            return false;\n        }\n    } else {\n        const int32_t rnd2 = Random_GetControl();\n        const int32_t rnd3 = Random_GetControl();\n        const int32_t rnd4 = Random_GetControl();\n        r = (rnd2 & 0x3F) + 192;\n        g = (rnd3 & 0x3F) + 64;\n        b = rnd4 & 0x1F;\n        falloff = 16 - ((flare_age - M_FLARE_END_AGE_TR3) >> 1);\n        Output_AddDynamicLightRGB(light_pos, falloff, (RGB_888) { r, g, b });\n        return (rnd & 1) != 0;\n    }\n\n    Output_AddDynamicLightRGB(light_pos, falloff, (RGB_888) { r, g, b });\n    return true;\n}\n\nvoid Flare_GenerateEffects(\n    const XYZ_32 *const sound_pos, const XYZ_32 flare_pos, int16_t room_num)\n{\n    Room_GetSector(flare_pos, &room_num);\n    if (Room_Get(room_num)->flags.underwater) {\n        Sound_Effect(SFX_LARA_FLARE_BURN, sound_pos, SPM_UNDERWATER);\n        if (Random_GetDraw() < 0x4000) {\n            Spawn_Bubble(&flare_pos, room_num);\n        }\n    } else {\n        Sound_Effect(SFX_LARA_FLARE_BURN, sound_pos, SPM_NORMAL);\n    }\n}\n\nbool Flare_GenerateLight(const XYZ_32 pos, const int32_t flare_age)\n{\n    if (g_TRVersion >= 3) {\n        return M_GenerateLight_TR3(pos, flare_age);\n    } else {\n        return M_GenerateLight_TR12(pos, flare_age);\n    }\n}\n\nint32_t Flare_GetMaxAge(void)\n{\n    return g_TRVersion >= 3 ? M_MAX_FLARE_AGE_TR3 : M_MAX_FLARE_AGE_TR12;\n}\n\nint32_t FlareItem_GetAge(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->raw_age & 0x7FFF;\n}\n\nbool FlareItem_IsActive(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return (p->raw_age & 0x8000) != 0;\n}\n\nvoid FlareItem_SetAge(\n    ITEM *const item, const int32_t flare_age, const bool is_active)\n{\n    M_PRIV *const p = item->priv;\n    p->raw_age = flare_age & 0x7FFF;\n    if (is_active) {\n        p->raw_age |= 0x8000;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->collision_func = Pickup_Collision;\n    obj->bounds_func = Pickup_Bounds;\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_position = true;\n    obj->save_flags = true;\n\n    if (obj->loaded) {\n        for (int32_t i = 0; i < obj->mesh_count; i++) {\n            OBJECT_MESH *const obj_mesh = Object_GetMesh(obj->mesh_idx + i);\n            obj_mesh->depth_adjustment = -0.5;\n        }\n    }\n}\n\nREGISTER_OBJECT(O_FLARE_ITEM, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/flare_item.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/objects/types.h>\n\nvoid Flare_GenerateEffects(\n    const XYZ_32 *sound_pos, XYZ_32 flare_pos, int16_t room_num);\nbool Flare_GenerateLight(XYZ_32 pos, int32_t flare_age);\nint32_t Flare_GetMaxAge(void);\nint32_t FlareItem_GetAge(const ITEM *item);\nbool FlareItem_IsActive(const ITEM *item);\nvoid FlareItem_SetAge(ITEM *item, int32_t flare_age, bool is_active);\n"
  },
  {
    "path": "src/trx/game/objects/general/fuse_box.c",
    "content": "#include <trx/game/game_flow.h>\n#include <trx/game/items.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n\nstatic bool M_CanTakeDamage(const ITEM *const item)\n{\n    return Item_TestAnimEqual(item, 0);\n}\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->hit_points <= 0 && Item_TestAnimEqual(item, 0)) {\n        Item_SwitchToAnim(item, 1, 0);\n\n        XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, 420, item->rot.y + DEG_180);\n        pos.y -= 768;\n        Sparks_TriggerExplosionSparks(pos, 2, 0, 0, item->room_num);\n        Sparks_TriggerExplosionSmoke(pos, 0, item->room_num);\n        Room_TestTriggers(item);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->can_take_damage_func = M_CanTakeDamage;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_FUSE_BOX, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/gas_emitter.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n\n#define M_DISTANCE (16 * WALL_L) // = 16384\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    const int32_t time = Output_GetTimeInGame();\n    if (!Item_IsTriggerActive(item) || (time % 4) != 0) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if (ABS(dx) > M_DISTANCE || ABS(dz) > M_DISTANCE) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 12;\n    spark->dst_color.g = 32;\n    spark->dst_color.b = 0;\n\n    spark->fade_to_black = 32;\n    spark->col_fade_speed = (Random_GetControl() & 7) + 24;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 64;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = item->pos.x + (Random_GetControl() & 0x1FF) - 256;\n    spark->pos.y = item->pos.y - (Random_GetControl() & 0xF) - 264;\n    spark->pos.z = item->pos.z + (Random_GetControl() & 0x3FF) - 512;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -1 - (Random_GetControl() & 1);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 4;\n\n    spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags |= SPARK_F_ROTATE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -4 - (Random_GetControl() & 7);\n        } else {\n            spark->rot_add = (Random_GetControl() & 7) + 4;\n        }\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n\n    const uint8_t size = (Random_GetControl() & 0x1F) + 96;\n    spark->size.width = size >> 1;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width;\n    spark->size.height = (size + (Random_GetControl() & 0x1F) + 32) >> 1;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_GAS_EMITTER_GREEN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/general.c",
    "content": "#include <trx/game/objects/general/general.h>\n\n#include <trx/game/collision.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n\ntypedef enum {\n    // clang-format off\n    GENERAL_STATE_INACTIVE = 0,\n    GENERAL_STATE_ACTIVE = 1,\n    // clang-format on\n} GENERAL_STATE;\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = General_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nvoid General_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item)) {\n        item->goal_anim_state = GENERAL_STATE_ACTIVE;\n    } else {\n        item->goal_anim_state = GENERAL_STATE_INACTIVE;\n    }\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    XYZ_32 pos = { .x = 3000, .y = 720, .z = 0 };\n    Collide_GetJointAbsPosition(item, &pos, 0);\n    Output_AddDynamicLight(pos, 14, 11);\n\n    if (item->status == IS_DEACTIVATED) {\n        Item_RemoveActive(item_num);\n        item->flags |= IF_ONE_SHOT;\n    }\n}\n\nREGISTER_OBJECT(O_GENERAL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/general.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid General_Control(int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/objects/general/gong.c",
    "content": "#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/general/pickup.h>\n\nstatic XYZ_32 m_Position = { .x = 0, .y = 0, .z = 0 };\n\nstatic const OBJECT_BOUNDS m_Bounds = {\n    .shift = {\n        .min = { .x = -WALL_L / 2, .y = -100, .z = -WALL_L / 2 - 300, },\n        .max = { .x = +WALL_L, .y = +100, .z = -WALL_L / 2 + 100, },\n    },\n    .rot = {\n        .min = { .x = -30 * DEG_1, .y = 0, .z = 0, },\n        .max = { .x = +30 * DEG_1, .y = 0, .z = 0, },\n    },\n    .ignore_rot = true,\n};\n\nstatic void M_ConsumeKeyItem(ITEM *const receptacle_item)\n{\n    const OBJECT_ID key_object_id =\n        Object_FindReceptacleKey(receptacle_item->object_id);\n    if (key_object_id != NO_OBJECT) {\n        Inv_RemoveItem(key_object_id);\n    }\n}\n\nstatic void M_CreateGongBonger(ITEM *const lara_item)\n{\n    const int16_t item_gong_bonger_num = Item_Create();\n    if (item_gong_bonger_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const item_gong_bonger = Item_Get(item_gong_bonger_num);\n    item_gong_bonger->object_id = O_GONG_BONGER;\n    item_gong_bonger->pos.x = lara_item->pos.x;\n    item_gong_bonger->pos.y = lara_item->pos.y;\n    item_gong_bonger->pos.z = lara_item->pos.z;\n    item_gong_bonger->rot.x = 0;\n    item_gong_bonger->rot.y = lara_item->rot.y;\n    lara_item->rot.z = 0;\n    item_gong_bonger->room_num = lara_item->room_num;\n\n    Item_Initialise(item_gong_bonger_num);\n    Item_AddActive(item_gong_bonger_num);\n    item_gong_bonger->status = IS_ACTIVE;\n    item_gong_bonger->shade.value_1 = -1;\n}\n\nstatic void M_Use(ITEM *const lara_item, ITEM *const receptacle_item)\n{\n    Lara_AlignPosition(receptacle_item, &m_Position);\n    lara_item->rot.y += DEG_180;\n\n    Lara_SwitchToExtraState(LS_EXTRA_GONG_BONG);\n    if (Item_TestFrameEqual(lara_item, 0)) {\n        M_ConsumeKeyItem(receptacle_item);\n    }\n\n    M_CreateGongBonger(lara_item);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->interact_target.is_moving = false;\n    lara->interact_target.item_num = NO_ITEM;\n}\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_Bounds;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->extra_anim) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (lara->interact_target.is_moving\n        && lara->interact_target.item_num == item_num) {\n        M_Use(lara_item, item);\n        return;\n    }\n\n    if (item->status != IS_INACTIVE || !g_Input.action\n        || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)) {\n        goto normal_collision;\n    }\n\n    const XYZ_16 old_rot = item->rot;\n    item->rot.x = 0;\n    item->rot.y = lara_item->rot.y;\n    item->rot.z = 0;\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        item->rot = old_rot;\n        goto normal_collision;\n    }\n\n    item->rot = old_rot;\n\n    if (!GF_ShowInventoryKeys(item->object_id)) {\n        Lara_RefuseInteraction();\n    }\n\n    return;\n\nnormal_collision:\n    Object_Collision(item_num, lara_item, coll);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->collision_func = M_Collision;\n    obj->bounds_func = M_Bounds;\n}\n\nREGISTER_OBJECT(O_GONG, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/gong_bonger.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n#define GONG_BONGER_STRIKE_FRAME 41\n#define GONG_BONGER_END_FRAME 79\n\nstatic void M_ActivateHeavyTriggers(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    Room_TestTriggers(item);\n    Item_Kill(item_num);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    Item_Animate(item);\n    if (Item_TestFrameEqual(item, GONG_BONGER_STRIKE_FRAME)) {\n        Music_Play(MX_REVEAL_1, MPM_ONCE);\n        g_Camera.bounce -= 50;\n    }\n\n    if (Item_TestFrameEqual(item, GONG_BONGER_END_FRAME)) {\n        M_ActivateHeavyTriggers(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_GONG_BONGER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/grenade.c",
    "content": "#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/smashing.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/general/smashable.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_SPEED 200\n#define M_FALL_SPEED (M_SPEED - 10) // = 190\n\nstatic int32_t M_GetBlastRadius(void)\n{\n    return g_Config.gameplay.enable_bouncy_grenades ? WALL_L : WALL_L / 2;\n}\n\nstatic void M_SetTR3ProjectileShade(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative\n    // shade forces the dynamic/smoothed lighting path.\n    item->shade.value_1 = -1;\n    item->shade.value_2 = -1;\n}\n\nstatic XYZ_32 M_GetLocalZOffset(const ITEM *const item, const int32_t dist)\n{\n    const int32_t cx = Math_Cos(item->rot.x);\n    const int32_t sx = Math_Sin(item->rot.x);\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sy = Math_Sin(item->rot.y);\n\n    const int32_t horz = (dist * cx) >> W2V_SHIFT;\n    return (XYZ_32) {\n        .x = (horz * sy) >> W2V_SHIFT,\n        .y = -(dist * sx) >> W2V_SHIFT,\n        .z = (horz * cy) >> W2V_SHIFT,\n    };\n}\n\nstatic void M_Explode(int16_t grenade_item_num, const XYZ_32 pos)\n{\n    const ITEM *const grenade_item = Item_Get(grenade_item_num);\n    const ROOM *const room = Room_Get(grenade_item->room_num);\n    const bool is_underwater = room != nullptr && room->flags.underwater;\n\n    if (g_TRVersion == 3) {\n        if (is_underwater) {\n            Sparks_TriggerUnderwaterExplosion(grenade_item);\n        } else {\n            Sparks_TriggerExplosionSparks(\n                pos, 3, -2, 0, grenade_item->room_num);\n            for (int32_t i = 0; i < 2; i++) {\n                Sparks_TriggerExplosionSparks(\n                    pos, 3, -1, 0, grenade_item->room_num);\n            }\n        }\n\n        Sound_Effect(\n            SFX_EXPLOSION_1, &grenade_item->pos, 0x1800000 | SPM_PITCH);\n        Sound_Effect(SFX_EXPLOSION_2, &grenade_item->pos, SPM_NORMAL);\n    } else {\n        const int16_t effect_num = Effect_Create(grenade_item->room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *const effect = Effect_Get(effect_num);\n            effect->pos = pos;\n            effect->speed = 0;\n            effect->frame_num = 0;\n            effect->counter = 0;\n            effect->object_id = O_EXPLOSION_1;\n        }\n\n        Sound_Effect(SFX_EXPLOSION_3, nullptr, SPM_NORMAL);\n    }\n\n    Creature_AlertNearbyGuards(grenade_item);\n    Item_Kill(grenade_item_num);\n}\n\nstatic bool M_CanExplodeTarget(const ITEM *const item)\n{\n    const OBJECT *const object = Object_Get(item->object_id);\n    if (object->can_be_exploded_func != nullptr) {\n        return object->can_be_exploded_func(item);\n    }\n\n    // TODO: as some creatures have more than one death animation, have a\n    // way to expose those specific ones for checking, or delegate\n    // responsibility directly to the objects.\n    const ITEM_ACTION action = ItemAction_ToGameID(ITEM_ACTION_FINISH_LEVEL);\n    for (int32_t i = 0; i < object->anim_count; i++) {\n        const ANIM *const anim = Object_GetAnim(object, i);\n        if (Anim_HasFXCommand(anim, action)) {\n            return false;\n        }\n    }\n\n    return true;\n}\n\nstatic bool M_TryExplodeItem(\n    const ITEM *const projectile_item, const GAME_VECTOR old_pos,\n    const int16_t target_item_num, const int32_t radius)\n{\n    ITEM *const target_item = Item_Get(target_item_num);\n\n    const OBJECT *const target_obj = Object_Get(target_item->object_id);\n    if (target_item == Lara_GetItem()) {\n        return false;\n    }\n    if (!target_item->collidable) {\n        return false;\n    }\n\n    if (target_item->status == IS_INVISIBLE\n        || target_obj->collision_func == nullptr) {\n        return false;\n    }\n\n    if (!Item_CanBeProjectileTarget(target_item)) {\n        return false;\n    }\n\n    const ANIM_FRAME *const frame = Item_GetBestFrame(target_item);\n    const BOUNDS_16 *const bounds = &frame->bounds;\n\n    const int32_t cdy = projectile_item->pos.y - target_item->pos.y;\n    if (cdy + radius < bounds->min.y || cdy - radius > bounds->max.y) {\n        return false;\n    }\n\n    const int32_t cy = Math_Cos(target_item->rot.y);\n    const int32_t sy = Math_Sin(target_item->rot.y);\n    const int32_t cdx = projectile_item->pos.x - target_item->pos.x;\n    const int32_t cdz = projectile_item->pos.z - target_item->pos.z;\n    const int32_t odx = old_pos.x - target_item->pos.x;\n    const int32_t odz = old_pos.z - target_item->pos.z;\n\n    const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT;\n    const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT;\n    if ((rx + radius < bounds->min.x && sx + radius < bounds->min.x)\n        || (rx - radius > bounds->max.x && sx - radius > bounds->max.x)) {\n        return false;\n    }\n\n    const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT;\n    const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT;\n    if ((rz + radius < bounds->min.z && sz + radius < bounds->min.z)\n        || (rz - radius > bounds->max.z && sz - radius > bounds->max.z)) {\n        return false;\n    }\n\n    if (!Item_CanTakeDamage(target_item)) {\n        return false;\n    }\n\n    const GAME_VECTOR hit_pos = {\n        .pos = projectile_item->pos,\n        .room_num = projectile_item->room_num,\n    };\n    Gun_HitTarget(\n        target_item, &old_pos, &hit_pos, g_Weapons[LGT_GRENADE].damage);\n    Stats_AddAmmoHits();\n\n    if (Gun_GetSmashPolicy(target_item) != GUN_SMASH_POLICY_NONE) {\n        Gun_SmashItem(target_item_num);\n    } else if (\n        target_item->hit_points <= 0 && M_CanExplodeTarget(target_item)) {\n        Creature_Die(target_item_num, true);\n    }\n    return true;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const GAME_VECTOR old_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n\n    const ROOM *const room = Room_Get(item->room_num);\n    const bool was_underwater = room != nullptr && room->flags.underwater;\n\n    if (g_Config.gameplay.enable_bouncy_grenades) {\n        if (was_underwater) {\n            item->fall_speed += (5 - item->fall_speed) >> 1;\n            item->speed -= item->speed >> 2;\n            if (item->speed != 0) {\n                item->rot.z += DEG_1 * ((item->speed >> 4) + 3);\n                if (item->required_anim_state != 0) {\n                    item->rot.y += DEG_1 * ((item->speed >> 2) + 3);\n                } else {\n                    item->rot.x += DEG_1 * ((item->speed >> 2) + 3);\n                }\n            }\n        } else {\n            item->fall_speed += 3;\n            if (item->speed != 0) {\n                item->rot.z += DEG_1 * ((item->speed >> 2) + 7);\n                if (item->required_anim_state != 0) {\n                    item->rot.y += DEG_1 * ((item->speed >> 1) + 7);\n                } else {\n                    item->rot.x += DEG_1 * ((item->speed >> 1) + 7);\n                }\n            }\n        }\n    }\n\n    if (g_TRVersion == 3) {\n        M_SetTR3ProjectileShade(item);\n        if (!was_underwater && item->speed != 0) {\n            const XYZ_32 back_64 = M_GetLocalZOffset(item, -64);\n            Sparks_TriggerRocketSmoke(\n                (XYZ_32) {\n                    .x = item->pos.x + back_64.x,\n                    .y = item->pos.y + back_64.y,\n                    .z = item->pos.z + back_64.z,\n                },\n                -1, item->room_num);\n        }\n    }\n\n    bool explode = false;\n    int32_t radius = 0;\n\n    if (g_Config.gameplay.enable_bouncy_grenades) {\n        const XYZ_32 vel = {\n            .x = (item->speed * Math_Sin(item->goal_anim_state)) >> W2V_SHIFT,\n            .y = item->fall_speed,\n            .z = (item->speed * Math_Cos(item->goal_anim_state)) >> W2V_SHIFT,\n        };\n        item->pos.x += vel.x;\n        item->pos.y += vel.y;\n        item->pos.z += vel.z;\n\n        const int16_t y_rot = item->rot.y;\n        item->rot.y = item->goal_anim_state;\n        Collide_DoProperDetection(item, old_pos.pos);\n        item->goal_anim_state = item->rot.y;\n        item->rot.y = y_rot;\n\n        if (item->hit_points > 0) {\n            item->hit_points--;\n\n            if (item->hit_points == 0) {\n                radius = M_GetBlastRadius();\n                explode = true;\n            }\n        }\n    } else {\n        item->speed--;\n        if (item->speed < M_FALL_SPEED) {\n            item->fall_speed++;\n        }\n        item->pos.y += item->fall_speed\n            - ((item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT);\n\n        const int16_t speed =\n            (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT;\n        item->pos.z += (speed * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n        item->pos.x += (speed * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n        item->floor = Room_GetHeight(sector, item->pos);\n        Item_UpdateRoom(item_num, room_num);\n\n        if (item->pos.y >= item->floor\n            || item->pos.y <= Room_GetCeiling(sector, item->pos)) {\n            radius = M_GetBlastRadius();\n            explode = true;\n        }\n    }\n\n    if (g_TRVersion == 3) {\n        const ROOM *const new_room = Room_Get(item->room_num);\n        const bool is_underwater =\n            new_room != nullptr && new_room->flags.underwater;\n        if (is_underwater && !was_underwater) {\n            const int32_t inner_y_vel =\n                -2048 - ((int32_t)item->fall_speed << 5);\n            const int32_t middle_y_vel =\n                -1024 - ((int32_t)item->fall_speed << 4);\n            FX_Water_SetupSplash(&(FX_WATER_SPLASH_SETUP) {\n                .x = item->pos.x,\n                .y = new_room->max_ceiling,\n                .z = item->pos.z,\n                .inner_xz_off = 16,\n                .inner_xz_size = 12,\n                .inner_y_size = -96,\n                .inner_xz_vel = 160,\n                .inner_gravity = 128,\n                .inner_y_vel = inner_y_vel,\n                .inner_friction = 7,\n                .middle_xz_off = 24,\n                .middle_xz_size = 24,\n                .middle_y_size = -64,\n                .middle_xz_vel = 224,\n                .middle_gravity = 72,\n                .middle_y_vel = middle_y_vel,\n                .middle_friction = 8,\n                .outer_xz_off = 32,\n                .outer_xz_size = 32,\n                .outer_xz_vel = 272,\n                .outer_friction = 9,\n            });\n        }\n    }\n\n    const GAME_VECTOR new_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n    if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id)\n        == PROJECTILE_HIT_STOP) {\n        explode = true;\n        radius = M_GetBlastRadius();\n    }\n\n    if (g_Config.gameplay.projectile_area_damage\n        == PROJECTILE_AREA_DAMAGE_MULTI_SWEEP) {\n        Room_GetNearbyRooms(item->pos, radius * 4, radius * 4, item->room_num);\n        for (int32_t i = 0; i < Room_DrawGetCount(); i++) {\n            const ROOM *const nearby_room = Room_Get(Room_DrawGetRoom(i));\n            for (int16_t target_item_num = nearby_room->item_num;\n                 target_item_num != NO_ITEM;\n                 target_item_num = Item_Get(target_item_num)->next_item) {\n                if (!M_TryExplodeItem(item, old_pos, target_item_num, radius)) {\n                    continue;\n                }\n\n                if (!explode) {\n                    explode = true;\n                    radius = M_GetBlastRadius();\n                    i = -1;\n                    break;\n                }\n            }\n        }\n    } else {\n        for (int16_t target_item_num = room->item_num;\n             target_item_num != NO_ITEM;\n             target_item_num = Item_Get(target_item_num)->next_item) {\n            if (M_TryExplodeItem(item, old_pos, target_item_num, radius)) {\n                explode = true;\n            }\n        }\n    }\n\n    if (explode) {\n        M_Explode(item_num, old_pos.pos);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_position = true;\n}\n\nREGISTER_OBJECT(O_GRENADE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/harpoon_bolt.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/smashing.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\ntypedef struct {\n    int16_t base_x_rot;\n    bool base_x_rot_valid;\n} M_PRIV;\n\n#define M_TR3_HIT_POINTS 256\n#define M_TR3_WOBBLE_START 192\n#define M_TR3_SPEED_UW 128\n#define M_TR3_SPEED_AIR 256\n\nstatic void M_SetTR3ProjectileShade(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative\n    // shade forces the dynamic/smoothed lighting path.\n    item->shade.value_1 = -1;\n    item->shade.value_2 = -1;\n}\n\nstatic void M_Initialise_TR3(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->priv == nullptr) {\n        item->priv = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA);\n    }\n    M_PRIV *const p = item->priv;\n    p->base_x_rot = 0;\n    p->base_x_rot_valid = false;\n}\n\nstatic void M_Control_TR3(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    const GAME_VECTOR old_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n\n    M_SetTR3ProjectileShade(item);\n\n    item->pos.x += (item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.y += item->fall_speed;\n    item->pos.z += (item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n    Item_UpdateRoom(item_num, room_num);\n\n    const GAME_VECTOR new_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n    if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id)\n        == PROJECTILE_HIT_STOP) {\n        Item_Kill(item_num);\n        return;\n    }\n\n    for (int16_t target_num = Room_Get(item->room_num)->item_num;\n         target_num != NO_ITEM; target_num = Item_Get(target_num)->next_item) {\n        ITEM *const target_item = Item_Get(target_num);\n\n        if (target_item == Lara_GetItem() || item_num == target_num) {\n            continue;\n        }\n\n        if (!target_item->collidable) {\n            continue;\n        }\n\n        if (!Item_CanBeProjectileTarget(target_item)) {\n            continue;\n        }\n\n        const ANIM_FRAME *const frame = Item_GetBestFrame(target_item);\n        if (frame == nullptr) {\n            continue;\n        }\n        const BOUNDS_16 *const bounds = &frame->bounds;\n\n        const int32_t cdy = item->pos.y - target_item->pos.y;\n        if (cdy < bounds->min.y || cdy > bounds->max.y) {\n            continue;\n        }\n\n        const int32_t cy = Math_Cos(target_item->rot.y);\n        const int32_t sy = Math_Sin(target_item->rot.y);\n        const int32_t cdx = item->pos.x - target_item->pos.x;\n        const int32_t cdz = item->pos.z - target_item->pos.z;\n        const int32_t odx = old_pos.x - target_item->pos.x;\n        const int32_t odz = old_pos.z - target_item->pos.z;\n\n        const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT;\n        const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT;\n        if ((rx < bounds->min.x && sx < bounds->min.x)\n            || (rx > bounds->max.x && sx > bounds->max.x)) {\n            continue;\n        }\n\n        const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT;\n        const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT;\n        if ((rz < bounds->min.z && sz < bounds->min.z)\n            || (rz > bounds->max.z && sz > bounds->max.z)) {\n            continue;\n        }\n\n        if (Item_CanTakeDamage(target_item)) {\n            if (Item_ShouldSpawnBlood(target_item)) {\n                Spawn_BloodBath(\n                    item->pos.x, item->pos.y, item->pos.z, 0, 0, item->room_num,\n                    3);\n            }\n            const GAME_VECTOR hit_pos = { .pos = item->pos,\n                                          .room_num = item->room_num };\n            Gun_HitTarget(\n                target_item, &old_pos, &hit_pos, g_Weapons[LGT_HARPOON].damage);\n            Stats_AddAmmoHits();\n        }\n\n        Item_Kill(item_num);\n        return;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, item->pos);\n    if (item->pos.y >= item->floor || item->pos.y <= ceiling) {\n        if (item->hit_points <= 0) {\n            item->hit_points = M_TR3_HIT_POINTS;\n        }\n\n        if (item->hit_points == M_TR3_HIT_POINTS) {\n            if (p != nullptr) {\n                p->base_x_rot = item->rot.x;\n                p->base_x_rot_valid = true;\n            }\n        }\n\n        if (item->hit_points >= M_TR3_WOBBLE_START) {\n            const int32_t base_x_rot = (p != nullptr && p->base_x_rot_valid)\n                ? p->base_x_rot\n                : item->rot.x;\n            const int32_t wobble_angle =\n                (item->hit_points & 7) * (DEG_360 / 16);\n            const int32_t wobble = (Math_Sin(wobble_angle) >> 3) - 1024;\n            item->rot.x =\n                (int16_t)(base_x_rot\n                          + (((item->hit_points - M_TR3_WOBBLE_START) * wobble)\n                             >> 6));\n            item->hit_points--;\n        }\n\n        item->hit_points--;\n        if (item->hit_points <= 0) {\n            Item_Kill(item_num);\n            return;\n        }\n\n        item->fall_speed = 0;\n        item->speed = 0;\n        return;\n    }\n\n    item->rot.z += 35 * DEG_1;\n\n    const ROOM *const room = Room_Get(item->room_num);\n    if (room != nullptr && room->flags.underwater) {\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        if ((time4 & 0xF) == 0) {\n            Spawn_BubbleEx(&item->pos, item->room_num, 2, 8);\n        }\n        Sparks_TriggerRocketSmoke(item->pos, 64, item->room_num);\n\n        item->fall_speed =\n            (int16_t)((-M_TR3_SPEED_UW * Math_Sin(item->rot.x)) >> W2V_SHIFT);\n        item->speed =\n            (int16_t)((M_TR3_SPEED_UW * Math_Cos(item->rot.x)) >> W2V_SHIFT);\n    } else {\n        item->rot.x -= DEG_1;\n        if (item->rot.x < -DEG_90) {\n            item->rot.x = -DEG_90;\n        }\n\n        item->fall_speed =\n            (int16_t)((-M_TR3_SPEED_AIR * Math_Sin(item->rot.x)) >> W2V_SHIFT);\n        item->speed =\n            (int16_t)((M_TR3_SPEED_AIR * Math_Cos(item->rot.x)) >> W2V_SHIFT);\n    }\n}\n\nstatic void M_Control_TR12(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const GAME_VECTOR old_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n\n    if (!Room_Get(item->room_num)->flags.underwater) {\n        item->fall_speed += GRAVITY / 2;\n    }\n\n    item->pos.x += (item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z += (item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    item->pos.y += item->fall_speed;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n    Item_UpdateRoom(item_num, room_num);\n\n    const GAME_VECTOR new_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n\n    bool hit = false;\n    if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id)\n        == PROJECTILE_HIT_STOP) {\n        hit = true;\n    }\n\n    for (int16_t target_num = Room_Get(item->room_num)->item_num;\n         target_num != NO_ITEM; target_num = Item_Get(target_num)->next_item) {\n        ITEM *const target_item = Item_Get(target_num);\n        const OBJECT *const target_obj = Object_Get(target_item->object_id);\n\n        if (target_item == Lara_GetItem() || item_num == target_num) {\n            continue;\n        }\n\n        if (!target_item->collidable) {\n            continue;\n        }\n\n        if (!Item_CanBeProjectileTarget(target_item)) {\n            continue;\n        }\n\n        const ANIM_FRAME *const frame = Item_GetBestFrame(target_item);\n        if (frame == nullptr) {\n            continue;\n        }\n        const BOUNDS_16 *const bounds = &frame->bounds;\n\n        const int32_t cdy = item->pos.y - target_item->pos.y;\n        if (cdy < bounds->min.y || cdy > bounds->max.y) {\n            continue;\n        }\n\n        const int32_t cy = Math_Cos(target_item->rot.y);\n        const int32_t sy = Math_Sin(target_item->rot.y);\n        const int32_t cdx = item->pos.x - target_item->pos.x;\n        const int32_t cdz = item->pos.z - target_item->pos.z;\n        const int32_t odx = old_pos.x - target_item->pos.x;\n        const int32_t odz = old_pos.z - target_item->pos.z;\n\n        const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT;\n        const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT;\n        if ((rx < bounds->min.x && sx < bounds->min.x)\n            || (rx > bounds->max.x && sx > bounds->max.x)) {\n            continue;\n        }\n\n        const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT;\n        const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT;\n        if ((rz < bounds->min.z && sz < bounds->min.z)\n            || (rz > bounds->max.z && sz > bounds->max.z)) {\n            continue;\n        }\n\n        if (Item_CanTakeDamage(target_item)) {\n            if (Item_ShouldSpawnBlood(target_item)) {\n                Spawn_BloodBath(\n                    item->pos.x, item->pos.y, item->pos.z, 0, 0, item->room_num,\n                    5);\n            }\n            const GAME_VECTOR hit_pos = {\n                .pos = item->pos,\n                .room_num = item->room_num,\n            };\n            Gun_HitTarget(\n                target_item, &old_pos, &hit_pos, g_Weapons[LGT_HARPOON].damage);\n            Stats_AddAmmoHits();\n        }\n        hit = true;\n        break;\n    }\n\n    if (!hit) {\n        const int32_t ceiling = Room_GetCeiling(sector, item->pos);\n        if (item->pos.y >= item->floor || item->pos.y <= ceiling) {\n            hit = true;\n        }\n    }\n\n    if (hit) {\n        Item_Kill(item_num);\n    } else if (Room_Get(item->room_num)->flags.underwater) {\n        Spawn_Bubble(&item->pos, item->room_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = g_TRVersion == 3 ? M_Initialise_TR3 : nullptr;\n    obj->control_func = g_TRVersion == 3 ? M_Control_TR3 : M_Control_TR12;\n    obj->save_position = true;\n}\n\nREGISTER_OBJECT(O_HARPOON_BOLT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/keyhole.c",
    "content": "#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/sound.h>\n\n#define M_LF_USE_KEYHOLE 104\n\nstatic XYZ_32 m_KeyholePosition = {\n    .x = 0,\n    .y = 0,\n    .z = WALL_L / 2 - LARA_RADIUS - 50,\n};\n\nstatic const OBJECT_BOUNDS m_KeyholeBounds = {\n    .shift = {\n        .min = { .x = -200, .y = +0, .z = +WALL_L / 2 - 200, },\n        .max = { .x = +200, .y = +0, .z = +WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, },\n        .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, },\n    },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_KeyholeBounds;\n}\n\nstatic void M_Use(ITEM *const lara_item, ITEM *const receptacle_item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    Lara_AlignPosition(receptacle_item, &m_KeyholePosition);\n    Lara_AnimateUntil(lara_item, LS(LS_USE_KEY));\n    lara_item->goal_anim_state = LS(LS_STOP);\n    lara->gun_status = LGS_HANDS_BUSY;\n    lara->interact_target.is_moving = false;\n}\n\nstatic void M_ConsumeKeyItem(ITEM *const receptacle_item)\n{\n    const OBJECT_ID key_object_id =\n        Object_FindReceptacleKey(receptacle_item->object_id);\n    if (key_object_id != NO_OBJECT) {\n        Inv_RemoveItem(key_object_id);\n    }\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->interact_target.item_num = NO_ITEM;\n}\n\nstatic void M_MarkDone(ITEM *const receptacle_item)\n{\n    receptacle_item->status = IS_ACTIVE;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (lara_item->current_anim_state != LS(LS_STOP)) {\n        if (lara_item->current_anim_state == LS(LS_USE_KEY)\n            && Lara_TestPosition(item, obj->bounds_func())\n            && Item_TestFrameEqual(lara_item, M_LF_USE_KEYHOLE)) {\n            M_ConsumeKeyItem(item);\n            M_MarkDone(item);\n        }\n        return;\n    }\n\n    if (lara->interact_target.is_moving\n        && lara->interact_target.item_num == item_num) {\n        M_Use(lara_item, item);\n    }\n\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)) {\n        return;\n    }\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    if (item->status != IS_INACTIVE) {\n        Lara_RefuseInteraction();\n    } else if (!GF_ShowInventoryKeys(item->object_id)) {\n        Lara_RefuseInteraction();\n    }\n}\n\nstatic bool M_IsUsable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    return item->status == IS_INACTIVE;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->collision_func = M_Collision;\n    obj->bounds_func = M_Bounds;\n    obj->save_flags = true;\n    obj->is_usable_func = M_IsUsable;\n}\n\nbool Keyhole_Trigger(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (item->status != IS_ACTIVE || lara->gun_status == LGS_HANDS_BUSY) {\n        return false;\n    }\n    item->status = IS_DEACTIVATED;\n    return true;\n}\n\nREGISTER_OBJECT(O_KEY_HOLE_1, M_Setup)\nREGISTER_OBJECT(O_KEY_HOLE_2, M_Setup)\nREGISTER_OBJECT(O_KEY_HOLE_3, M_Setup)\nREGISTER_OBJECT(O_KEY_HOLE_4, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/keyhole.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nbool Keyhole_Trigger(int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/objects/general/kill_all_triggered.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    Item_KillAllActive();\n    Effect_KillAllActive();\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_KILL_ALL_TRIGGERED, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lara_alarm.c",
    "content": "#include <trx/game/objects/common.h>\n#include <trx/game/sound.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_CODE_BITS) == IF_CODE_BITS) {\n        Sound_Effect(SFX_BURGLAR_ALARM, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_LARA_ALARM, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lift.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/log.h>\n#include <trx/core/math.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/movable_block.h>\n\n#define LIFT_WAIT_TIME (3 * LOGIC_FPS) // = 90\n#define LIFT_SHIFT 16\n#define LIFT_HEIGHT (STEP_L * 5) // = 1280\n#define LIFT_TRAVEL_DIST (STEP_L * 22)\n#define M_LIFT_NUM_FLOOR_SECTORS 4\n#define M_LIFT_NUM_SECTORS 8\n\ntypedef enum {\n    LIFT_STATE_DOOR_CLOSED = 0,\n    LIFT_STATE_DOOR_OPEN = 1,\n} LIFT_STATE;\n\ntypedef enum {\n    LIFT_ANIM_CLOSED = 0,\n} LIFT_ANIM;\n\ntypedef struct {\n    int32_t start_height;\n    int32_t wait_time;\n    bool is_moving;\n    GAME_VECTOR linked[M_LIFT_NUM_SECTORS];\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"start_height\", &p->start_height));\n    JSON_SHOULD(JSON_READ(io, \"wait_time\", &p->wait_time));\n    JSON_SHOULD(JSON_READ(io, \"is_moving\", &p->is_moving));\n    for (int32_t i = 0; i < M_LIFT_NUM_SECTORS; i++) {\n        const char *const key = String_FormatStatic(\"linked_%d\", i);\n        if (JSON_SHOULD(JSON_PUSH(io, key))) {\n            JSON_SHOULD(JSON_READ(io, \"x\", &p->linked[i].pos.x));\n            JSON_SHOULD(JSON_READ(io, \"y\", &p->linked[i].pos.y));\n            JSON_SHOULD(JSON_READ(io, \"z\", &p->linked[i].pos.z));\n            JSON_SHOULD(JSON_POP(io));\n        }\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"start_height\", p->start_height);\n    JSONW_WRITE(io, \"wait_time\", p->wait_time);\n    JSONW_WRITE(io, \"is_moving\", p->is_moving);\n    for (int32_t i = 0; i < M_LIFT_NUM_SECTORS; i++) {\n        const char *const key = String_FormatStatic(\"linked_%d\", i);\n        JSONW_PUSH_OBJECT(io);\n        JSONW_WRITE(io, \"x\", p->linked[i].pos.x);\n        JSONW_WRITE(io, \"y\", p->linked[i].pos.y);\n        JSONW_WRITE(io, \"z\", p->linked[i].pos.z);\n        JSONW_POP_AND_SET(io, key);\n    }\n}\n\nstatic void M_FloorCeiling(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    int32_t *const out_floor, int32_t *const out_ceiling)\n{\n    const XZ_32 lift_tile = {\n        .x = item->pos.x >> WALL_SHIFT,\n        .z = item->pos.z >> WALL_SHIFT,\n    };\n\n    ITEM *const lara_item = Lara_GetItem();\n    const XZ_32 lara_tile = {\n        .x = lara_item->pos.x >> WALL_SHIFT,\n        .z = lara_item->pos.z >> WALL_SHIFT,\n    };\n\n    const XZ_32 test_tile = {\n        .x = x >> WALL_SHIFT,\n        .z = z >> WALL_SHIFT,\n    };\n\n    const DIRECTION direction = Math_GetDirection(item->rot.y);\n    int32_t dx = 0;\n    int32_t dz = 0;\n    switch (direction) {\n    case DIR_NORTH:\n        dx = -1;\n        dz = 1;\n        break;\n    case DIR_EAST:\n        dx = 1;\n        dz = 1;\n        break;\n    case DIR_SOUTH:\n        dx = 1;\n        dz = -1;\n        break;\n    case DIR_WEST:\n        dx = -1;\n        dz = -1;\n        break;\n    default:\n        break;\n    }\n\n    // clang-format off\n    const bool point_in_shaft =\n        (test_tile.x == lift_tile.x || test_tile.x + dx == lift_tile.x) &&\n        (test_tile.z == lift_tile.z || test_tile.z + dz == lift_tile.z);\n\n    const bool lara_in_shaft =\n        (lara_tile.x == lift_tile.x || lara_tile.x + dx == lift_tile.x) &&\n        (lara_tile.z == lift_tile.z || lara_tile.z + dz == lift_tile.z);\n\n    const int32_t lift_bottom = item->pos.y + STEP_L;\n    const int32_t lift_floor = item->pos.y;\n    const int32_t lift_ceiling = item->pos.y - LIFT_HEIGHT + STEP_L;\n    const int32_t lift_top = item->pos.y - LIFT_HEIGHT;\n\n    const bool lara_inside_lift = (lara_item->pos.y < lift_bottom) &&\n                                  (lara_item->pos.y > lift_ceiling);\n    // clang-format on\n\n    *out_floor = 0x7FFF;\n    *out_ceiling = -0x7FFF;\n\n    if (lara_in_shaft) {\n        if (item->current_anim_state == LIFT_STATE_DOOR_CLOSED\n            && lara_inside_lift) {\n            if (point_in_shaft) {\n                *out_floor = lift_floor;\n                *out_ceiling = lift_ceiling;\n            } else {\n                *out_floor = NO_HEIGHT;\n                *out_ceiling = 0x7FFF;\n            }\n        } else if (point_in_shaft) {\n            if (lara_item->pos.y < lift_ceiling) {\n                *out_floor = lift_top;\n            } else if (lara_item->pos.y < lift_bottom) {\n                *out_floor = lift_floor;\n                *out_ceiling = lift_ceiling;\n            } else {\n                *out_ceiling = lift_bottom;\n            }\n        }\n    } else if (point_in_shaft) {\n        if (y <= lift_top) {\n            *out_floor = lift_top;\n        } else if (y >= lift_bottom) {\n            *out_ceiling = lift_bottom;\n        } else if (item->current_anim_state == LIFT_STATE_DOOR_OPEN) {\n            *out_floor = lift_floor;\n            *out_ceiling = lift_ceiling;\n        } else {\n            *out_floor = NO_HEIGHT;\n            *out_ceiling = 0x7FFF;\n        }\n    }\n}\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    int32_t new_floor;\n    int32_t new_ceiling;\n    M_FloorCeiling(item, x, y, z, &new_floor, &new_ceiling);\n    if (new_floor >= height) {\n        return height;\n    }\n    return new_floor;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    int32_t new_floor;\n    int32_t new_ceiling;\n    M_FloorCeiling(item, x, y, z, &new_floor, &new_ceiling);\n    if (new_ceiling <= height) {\n        return height;\n    }\n    return new_ceiling;\n}\n\nstatic void M_GetSectorPositions(\n    const ITEM *const item, VECTOR *const sector_pos)\n{\n    const XZ_32 lift_tile = {\n        .x = item->pos.x >> WALL_SHIFT,\n        .z = item->pos.z >> WALL_SHIFT,\n    };\n\n    // Orient.\n    const DIRECTION dir = Math_GetDirection(item->rot.y);\n    int32_t dx = 0, dz = 0;\n    switch (dir) {\n    case DIR_NORTH:\n        dx = -1;\n        dz = 1;\n        break;\n    case DIR_EAST:\n        dx = 1;\n        dz = 1;\n        break;\n    case DIR_SOUTH:\n        dx = 1;\n        dz = -1;\n        break;\n    case DIR_WEST:\n        dx = -1;\n        dz = -1;\n        break;\n    default:\n        break;\n    }\n\n    // Collect a 2×2 footprint that lines up with the shaft tiles.\n    for (int32_t ix = 0; ix < 2; ix++) {\n        for (int32_t iz = 0; iz < 2; iz++) {\n            const int32_t sx = lift_tile.x - dx * ix;\n            const int32_t sz = lift_tile.z - dz * iz;\n\n            const XYZ_32 pos = {\n                .x = sx * WALL_L + WALL_L / 2,\n                .y = item->pos.y,\n                .z = sz * WALL_L + WALL_L / 2,\n            };\n            Vector_Add(sector_pos, &pos);\n        }\n    }\n\n    // Collect a 2×2 footprint that lines up with the shaft ceiling tiles.\n    for (int32_t ix = 0; ix < 2; ix++) {\n        for (int32_t iz = 0; iz < 2; iz++) {\n            const int32_t sx = lift_tile.x - dx * ix;\n            const int32_t sz = lift_tile.z - dz * iz;\n\n            const XYZ_32 pos = {\n                .x = sx * WALL_L + WALL_L / 2,\n                .y = item->pos.y - LIFT_HEIGHT,\n                .z = sz * WALL_L + WALL_L / 2,\n            };\n            Vector_Add(sector_pos, &pos);\n        }\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->start_height = item->pos.y;\n    p->wait_time = 0;\n    p->is_moving = false;\n\n    VECTOR *const positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    for (int32_t i = 0; i < positions->count; i++) {\n        const GAME_VECTOR linked = {\n            .pos = *(const XYZ_32 *)Vector_Get(positions, i),\n            .room_num = item->room_num,\n        };\n        p->linked[i] = linked;\n    }\n    Walkable_AllocateNodes(item, positions->count);\n    Vector_Free(positions);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    const int32_t bottom = p->start_height;\n    const int32_t top = bottom + LIFT_TRAVEL_DIST;\n    const int32_t target = Item_IsTriggerActive(item) ? top : bottom;\n\n    if (item->pos.y == target) {\n        item->goal_anim_state = LIFT_STATE_DOOR_OPEN;\n        p->wait_time = 0;\n        if (p->is_moving) {\n            for (int32_t i = 0; i < M_LIFT_NUM_FLOOR_SECTORS; i++) {\n                MovableBlock_ShiftStackY(\n                    p->linked[i].pos.y, p->linked[i].pos, item->pos.y,\n                    item->room_num, true);\n                // Don't reposition because item->pos links to a single sector.\n                p->linked[i].pos.y = item->pos.y;\n            }\n            for (int32_t i = M_LIFT_NUM_FLOOR_SECTORS; i < M_LIFT_NUM_SECTORS;\n                 i++) {\n                MovableBlock_ShiftStackY(\n                    p->linked[i].pos.y, p->linked[i].pos,\n                    item->pos.y - LIFT_HEIGHT, item->room_num, true);\n                // Don't reposition because item->pos links to a single sector.\n                p->linked[i].pos.y = item->pos.y - LIFT_HEIGHT;\n            }\n        }\n        p->is_moving = false;\n    } else if (p->wait_time < LIFT_WAIT_TIME) {\n        item->goal_anim_state = LIFT_STATE_DOOR_OPEN;\n        p->wait_time++;\n        // Prevent Lara from interacting with blocks about to move.\n        for (int32_t i = 0; i < M_LIFT_NUM_FLOOR_SECTORS; i++) {\n            MovableBlock_ShiftStackY(\n                p->linked[i].pos.y, p->linked[i].pos, item->pos.y,\n                item->room_num, false);\n        }\n        for (int32_t i = M_LIFT_NUM_FLOOR_SECTORS; i < M_LIFT_NUM_SECTORS;\n             i++) {\n            MovableBlock_ShiftStackY(\n                p->linked[i].pos.y, p->linked[i].pos, item->pos.y - LIFT_HEIGHT,\n                item->room_num, false);\n        }\n    } else {\n        item->goal_anim_state = LIFT_STATE_DOOR_CLOSED;\n        p->is_moving = true;\n        const int32_t delta = target - item->pos.y;\n        const int32_t step = (delta > 0)\n            ? (delta < LIFT_SHIFT ? delta : LIFT_SHIFT)\n            : (delta > -LIFT_SHIFT ? delta : -LIFT_SHIFT);\n        item->pos.y += step;\n        // Raise/lower possible movable blocks on top.\n        for (int32_t i = 0; i < M_LIFT_NUM_FLOOR_SECTORS; i++) {\n            MovableBlock_ShiftStackY(\n                p->linked[i].pos.y, p->linked[i].pos, item->pos.y,\n                item->room_num, false);\n        }\n        // Double check linked positions on save vs load.\n        for (int32_t i = M_LIFT_NUM_FLOOR_SECTORS; i < M_LIFT_NUM_SECTORS;\n             i++) {\n            MovableBlock_ShiftStackY(\n                p->linked[i].pos.y, p->linked[i].pos, item->pos.y - LIFT_HEIGHT,\n                item->room_num, false);\n        }\n    }\n\n    Item_Animate(item);\n\n    // Update room number one click up to avoid lift on a room portal.\n    int16_t room_num = item->room_num;\n    Room_GetSector(\n        (XYZ_32) { item->pos.x, item->pos.y - STEP_L, item->pos.z }, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    VECTOR *positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    for (int32_t i = 0; i < positions->count; i++) {\n        Walkable_Add(item_num, *(const XYZ_32 *)Vector_Get(positions, i));\n    }\n    Vector_Free(positions);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->add_walkable_func = M_AddWalkable;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_LIFT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lights/beacon_light.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n\ntypedef struct {\n    int32_t timer;\n} M_PRIV;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    p->timer = (p->timer + 1) & 0x3F;\n    if (p->timer < 3) {\n        const uint8_t rg = 255 - (Random_GetControl() & 3);\n        const uint8_t b = 255 - (Random_GetControl() & 0x1F);\n        Output_AddDynamicLightRGB(item->pos, 16, (RGB_888) { rg, rg, b });\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_BEACON_LIGHT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lights/colored_light.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/output.h>\n\ntypedef struct {\n    RGB_888 color;\n} M_PRIV;\n\nstatic void M_InitialiseGeneric(const int16_t item_num, const RGB_888 color)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->color = color;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    Output_AddDynamicLightRGB(item->pos, 24, p->color);\n}\n\nstatic void M_SetupCommon(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\n#define M_DEFINE_LIGHT(name, r, g, b)                                          \\\n    static void M_Initialise##name(const int16_t num)                          \\\n    {                                                                          \\\n        M_InitialiseGeneric(num, (RGB_888) { r, g, b });                       \\\n    }                                                                          \\\n    static void M_Setup##name(OBJECT *const obj)                               \\\n    {                                                                          \\\n        M_SetupCommon(obj);                                                    \\\n        obj->initialise_func = M_Initialise##name;                             \\\n    }                                                                          \\\n    REGISTER_OBJECT(O_##name##_LIGHT, M_Setup##name)\n\nM_DEFINE_LIGHT(RED, 255, 0, 0)\nM_DEFINE_LIGHT(GREEN, 0, 255, 0)\nM_DEFINE_LIGHT(BLUE, 0, 0, 255)\nM_DEFINE_LIGHT(AMBER, 255, 192, 0)\nM_DEFINE_LIGHT(WHITE, 224, 224, 255)\n\n#undef M_DEFINE_LIGHT\n"
  },
  {
    "path": "src/trx/game/objects/general/lights/electrical_light.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n\ntypedef struct {\n    int32_t life;\n} M_PRIV;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    int32_t rg, b;\n    if (!Item_IsTriggerActive(item)) {\n        p->life = 0;\n        return;\n    }\n\n    if (p->life < 16) {\n        rg = (Random_GetControl() % 8) << 2;\n        b = rg + (Random_GetControl() % 4);\n        p->life++;\n    } else if (p->life < 96) {\n        if (((int32_t)Output_GetTimeInGame() % 16)\n            && (Random_GetControl() % 8)) {\n            rg = Random_GetControl() % 8;\n        } else {\n            rg = 24 - (Random_GetControl() % 8);\n        }\n\n        b = rg + (Random_GetControl() % 4);\n        p->life++;\n    } else if (p->life < 160) {\n        rg = 12 - (Random_GetControl() % 4);\n        b = rg + (Random_GetControl() % 4);\n\n        if (!(Random_GetControl() % 0x20) && p->life > 128) {\n            p->life = 160;\n        } else {\n            p->life++;\n        }\n    } else {\n        rg = 31 - (Random_GetControl() % 4);\n        b = 31 - (Random_GetControl() % 2);\n\n        if (item->object_id == O_FLICKERING_LIGHT\n            && (Random_GetControl() < 0x200)) {\n            p->life = 0;\n        }\n    }\n\n    rg <<= 3;\n    b <<= 3;\n    Output_AddDynamicLightRGB(item->pos, 16, (RGB_888) { rg, rg, b });\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"life\", &p->life));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"life\", p->life);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_ELECTRICAL_LIGHT, M_Setup)\nREGISTER_OBJECT(O_FLICKERING_LIGHT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lights/on_off_light.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/output.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n    Output_AddDynamicLightRGB(item->pos, 16, COLOR_RGB_888_WHITE);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_ON_OFF_LIGHT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lights/pulse_light.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n\ntypedef struct {\n    int32_t cycle;\n} M_PRIV;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    p->cycle += 728;\n\n    int32_t falloff = ABS((32 * Math_Sin(p->cycle)) >> W2V_SHIFT);\n    if (falloff > 31) {\n        falloff = 31;\n    } else if (falloff < 8) {\n        falloff = 8;\n        p->cycle += 2048;\n    }\n\n    Output_AddDynamicLightRGB(item->pos, falloff, (RGB_888) { 255, 96, 0 });\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_PULSE_LIGHT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/lights/strobe_light.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\ntypedef struct {\n    int32_t life;\n    bool alarm_active;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_OPTIONAL(JSON_READ(io, \"life\", &p->life));\n    JSON_OPTIONAL(JSON_READ(io, \"alarm_active\", &p->alarm_active));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"life\", p->life);\n    JSONW_WRITE(io, \"alarm_active\", p->alarm_active);\n}\n\nvoid M_TriggerAlertLight(\n    const XYZ_32 pos, const RGB_888 color, const int16_t angle,\n    const int16_t room_num)\n{\n    GAME_VECTOR src = { .pos = pos, .room_num = room_num };\n    Room_GetSector(pos, &src.room_num);\n\n    const int32_t dist = 8 * WALL_L;\n    GAME_VECTOR dst = {\n        .pos = {\n            .x = pos.x + ((dist * Math_Sin(angle)) >> W2V_SHIFT),\n            .y = pos.y,\n            .z = pos.z + ((dist * Math_Cos(angle)) >> W2V_SHIFT),\n        },\n    };\n\n    if (!LOS_Check(&src, &dst, false)) {\n        Output_AddDynamicLightRGB(dst.pos, 8, color);\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    if (GF_BadGetLevelNum() == 15 && !p->alarm_active) {\n        return;\n    }\n\n    item->rot.y += 2912;\n    const int16_t angle = item->rot.y + 0x5800;\n\n    M_TriggerAlertLight(\n        (XYZ_32) { item->pos.x, item->pos.y - WALL_L / 2, item->pos.z },\n        (RGB_888) { 255, 64, 0 }, angle, item->room_num);\n\n    Output_AddDynamicLightRGB(\n        (XYZ_32) {\n            item->pos.x + ((STEP_L * Math_Sin(angle)) >> W2V_SHIFT),\n            item->pos.y - STEP_L * 3,\n            item->pos.z + ((STEP_L * Math_Cos(angle)) >> W2V_SHIFT),\n        },\n        6, (RGB_888) { 255, 96, 0 });\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if (!(time4 & 0x7F)) {\n        Sound_Effect(SFX_ALARM_1, &item->pos, SPM_NORMAL);\n    }\n\n    p->life++;\n\n    if (p->life > 60 * LOGIC_FPS) {\n        p->alarm_active = false;\n        p->life = 0;\n    }\n}\n\nstatic void M_HandleEvent(\n    ITEM *const item, const OBJECT_EVENT event, const void *const data)\n{\n    M_PRIV *const p = item->priv;\n    if (event != OBJECT_EVENT_ALERT) {\n        return;\n    }\n\n    p->alarm_active = true;\n    p->life = 0;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->control_func = M_Control;\n    obj->event_func = M_HandleEvent;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_STROBE_LIGHT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/mini_copter.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const ITEM *const lara_item = Lara_GetItem();\n\n    item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, 100);\n\n    XYZ_32 pos = lara_item->pos;\n    pos.x += ((item->pos.x - lara_item->pos.x) >> 2);\n    pos.y += ((item->pos.y - lara_item->pos.y) >> 2);\n    pos.z += ((item->pos.z - lara_item->pos.z) >> 2);\n    Sound_Effect(SFX_HELICOPTER_LOOP, &pos, SPM_NORMAL);\n\n    if (ABS(item->pos.z - lara_item->pos.z) > WALL_L * 30) {\n        Item_Kill(item_num);\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_position = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_MINI_COPTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/moving_bar.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    MOVING_BAR_STATE_INACTIVE = 0,\n    MOVING_BAR_STATE_ACTIVE = 1,\n} MOVING_BAR_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Item_IsTriggerActive(item)) {\n        item->goal_anim_state = MOVING_BAR_STATE_ACTIVE;\n    } else {\n        item->goal_anim_state = MOVING_BAR_STATE_INACTIVE;\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n    obj->save_position = true;\n}\n\nREGISTER_OBJECT(O_MOVING_BAR, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/pickup.c",
    "content": "#include <trx/game/objects/general/pickup.h>\n\n#include <trx/config.h>\n#include <trx/game/effects.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items/anim.h>\n#include <trx/game/lara.h>\n#include <trx/game/lua.h>\n#include <trx/game/objects/general/flare_item.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/game/sparks.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_LF_PICKUP_ERASE        42\n#define M_LF_PICKUP_FLARE        58\n#define M_LF_PICKUP_FLARE_UW     20\n#define M_LF_PICKUP_UW           18\n#define M_LF_PICKUP_CROUCH_1     20\n#define M_LF_PICKUP_CROUCH_2     22\n#define M_LF_PICKUP_CROUCH_FLARE 22\n#define M_LF_PICKUP_CRAWL        20\n#define M_AID_DIST_MIN           (STEP_L * 5)      // 1280\n#define M_AID_DIST_MAX           (WALL_L * 8)      // 8192\n#define M_AID_WAIT_MIN           (LOGIC_FPS * 2.5) // 75\n#define M_AID_WAIT_MAX           (LOGIC_FPS * 5)   // 150\n#define M_AID_WAIT_BREAK_CHANCE  0x1200\n// clang-format on\n\nstatic const OBJECT_BOUNDS m_PickUpBounds = {\n    .shift = {\n        .min = { .x = -WALL_L / 4, .y = -100, .z = -WALL_L / 4, },\n        .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 4, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = 0, .z = 0, },\n        .max = { .x = +10 * DEG_1, .y = 0, .z = 0, },\n    },\n};\n\nstatic const OBJECT_BOUNDS m_PickUpBoundsControlled = {\n    .shift = {\n        .min = { .x = -WALL_L / 4, .y = -200, .z = -WALL_L / 4, },\n        .max = { .x = +WALL_L / 4, .y = +200, .z = +WALL_L / 4, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = 0, .z = 0, },\n        .max = { .x = +10 * DEG_1, .y = 0, .z = 0, },\n    },\n};\n\nstatic const OBJECT_BOUNDS m_PickUpBoundsUW = {\n    .shift = {\n        .min = { .x = -WALL_L / 2, .y = -WALL_L / 2, .z = -WALL_L / 2, },\n        .max = { .x = +WALL_L / 2, .y = +WALL_L / 2, .z = +WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = -45 * DEG_1, .y = -45 * DEG_1, .z = -45 * DEG_1, },\n        .max = { .x = +45 * DEG_1, .y = +45 * DEG_1, .z = +45 * DEG_1, },\n    },\n};\n\nstatic const XYZ_32 m_PickupPosition = { .x = 0, .y = 0, .z = -100 };\nstatic const XYZ_32 m_PickupPositionUW = { .x = 0, .y = -200, .z = -350 };\n\nstatic const OBJECT_ID m_QuestObjects[] = {\n    // clang-format off\n    O_QUEST_ITEM_1,\n    O_QUEST_ITEM_2,\n    O_QUEST_ITEM_3,\n    O_QUEST_ITEM_4,\n    NO_OBJECT,\n    // clang-format on\n};\n\ntypedef struct {\n    int32_t aid_timer;\n    uint32_t secret_mask;\n} M_PRIV;\n\nuint32_t Pickup_GetSecretMask(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->secret_mask;\n}\n\nstatic void M_Initialise(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->aid_timer = -1;\n    p->secret_mask = 0;\n\n    if (Object_IsType(item->object_id, g_SecretObjects)) {\n        const GF_LEVEL *const level = Game_GetCurrentLevel();\n        p->secret_mask = Stats_GetSecretMaskForItem(level, item_num);\n    }\n\n    if (item->status != IS_INVISIBLE) {\n        Item_AddActive(item_num);\n    }\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->status == IS_DEACTIVATED) {\n            const int16_t item_num = Item_GetIndex(item);\n            Item_RemoveDrawn(item_num);\n        }\n    }\n}\n\nstatic bool M_Trigger(ITEM *const item, const TRIGGER *const trigger)\n{\n    if (trigger == nullptr) {\n        return false;\n    }\n    if (trigger->type == TT_SWITCH) {\n        item->flags ^= trigger->mask;\n    } else if (trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER) {\n        item->flags &= ~trigger->mask;\n    } else {\n        item->flags |= trigger->mask;\n    }\n\n    if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) {\n        item->status = IS_INVISIBLE;\n        item->flags |= IF_KILLED;\n    } else if (item->status == IS_INVISIBLE) {\n        item->touch_bits = 0;\n        item->status = IS_ACTIVE;\n        const int16_t item_num = Item_GetIndex(item);\n        Item_AddActive(item_num);\n    }\n\n    return false;\n}\n\nstatic void M_SpawnPickupAid(const ITEM *const item)\n{\n    const OBJECT_ID obj_id =\n        Object_GetCognate(item->object_id, g_ItemToInvObjectMap);\n    if (obj_id == NO_OBJECT) {\n        return;\n    }\n\n    const OBJECT *const obj = Object_Get(obj_id);\n    const ANIM_FRAME *const frame = obj->frame_base;\n    if (!obj->loaded || frame == nullptr) {\n        return;\n    }\n\n    const GAME_VECTOR pos = {\n        .x = item->pos.x + 20 * (Random_GetDraw() - 0x4000) / 0x4000,\n        .y = item->pos.y - ABS(frame->bounds.max.y - frame->bounds.min.y)\n            - 10 * (1 + (Random_GetDraw() - 0x4000) / 0x4000),\n        .z = item->pos.z + 20 * (Random_GetDraw() - 0x4000) / 0x4000,\n        .room_num = item->room_num,\n    };\n\n    if (g_TRVersion >= 3) {\n        for (int32_t i = 0; i < (Random_GetControl() & 3) + 4; i++) {\n            Sparks_TriggerPickupAid(pos.pos, (XZ_32) {});\n        }\n    } else {\n        const int16_t effect_num = Effect_Create(pos.room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *const effect = Effect_Get(effect_num);\n            effect->room_num = pos.room_num;\n            effect->pos = pos.pos;\n            effect->counter = 0;\n            effect->object_id = O_PICKUP_AID;\n            effect->frame_num = 0;\n        }\n    }\n}\n\nstatic void M_ControlPickupAids(ITEM *const item)\n{\n    const ITEM *const lara = Lara_GetItem();\n    if (item->fall_speed != 0 || lara == nullptr\n        || !Object_Get(O_PICKUP_AID)->loaded) {\n        return;\n    }\n\n    const int32_t distance = Item_GetDistance(lara, item->pos);\n    if (distance < M_AID_DIST_MIN || distance > M_AID_DIST_MAX) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    int32_t timer = p->aid_timer;\n    if (timer <= 0\n        || (timer < M_AID_WAIT_MIN\n            && Random_GetDraw() < M_AID_WAIT_BREAK_CHANCE)) {\n        M_SpawnPickupAid(item);\n        timer = M_AID_WAIT_MAX;\n    } else {\n        timer--;\n    }\n\n    p->aid_timer = timer;\n}\n\nstatic void M_ControlPickupLights(ITEM *const item)\n{\n    const int16_t timer = Output_GetTimeInGame();\n    const int16_t angle = Math_Cos((timer & 0x3F) << 10);\n    int32_t c = ABS(angle >> 9);\n    CLAMPG(c, 31);\n    c <<= 3;\n    Output_AddDynamicLightRGB(item->pos, 8, (RGB_888) { 0, c, c >> 1 });\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status == IS_INVISIBLE || item->status == IS_DEACTIVATED) {\n        Item_RemoveActive(item_num);\n        return;\n    }\n\n    if (item->room_num == NO_ROOM) {\n        return;\n    }\n\n    if (g_TRVersion == 3 && Object_IsType(item->object_id, m_QuestObjects)) {\n        item->rot.y += 1024;\n        M_ControlPickupLights(item);\n    } else if (g_Config.gameplay.enable_pickup_aids) {\n        M_ControlPickupAids(item);\n    }\n}\n\nstatic void M_DoPickup(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->object_id == O_FLARE_ITEM) {\n        return;\n    }\n\n    Overlay_AddDisplayPickup(item->object_id);\n    Inv_AddPickup(item);\n    Stats_AddPickup();\n    // Notify Lua pickup listeners\n    Lua_FireEventInt32(LUA_EVENT_PICKUP, item_num); // LUA uses 1-indexing\n\n    item->status = IS_INVISIBLE;\n    item->flags |= IF_KILLED;\n\n    if (g_TRVersion == 3 && Object_IsType(item->object_id, m_QuestObjects)) {\n        if (GF_BadGetLevelNum() == 19\n            || (GF_BadIsMod(\"tr3-la\") && GF_BadGetLevelNum() == 4)) {\n            Item_Kill(item_num);\n        } else {\n            Game_SetIsLevelComplete(true);\n        }\n    } else {\n        Item_RemoveDrawn(item_num);\n        Item_RemoveActive(item_num);\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->interact_target.is_moving = false;\n}\n\nstatic void M_DoFlarePickup(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->request_gun_type = LGT_FLARE;\n    lara->gun_type = LGT_FLARE;\n    Gun_InitialiseNewWeapon();\n    lara->gun_status = LGS_SPECIAL;\n    lara->flare.age = FlareItem_GetAge(item);\n    Item_Kill(item_num);\n    lara->interact_target.is_moving = false;\n}\n\nstatic void M_GetAllAtLaraPos(const ITEM *const item)\n{\n    int16_t pickup_num = Room_Get(item->room_num)->item_num;\n    while (pickup_num != NO_ITEM) {\n        ITEM *const check_item = Item_Get(pickup_num);\n        if (check_item->pos.x == item->pos.x && check_item->pos.z == item->pos.z\n            && Object_Get(check_item->object_id)->collision_func\n                == Pickup_Collision) {\n            M_DoPickup(pickup_num);\n        }\n        pickup_num = check_item->next_item;\n    }\n}\n\nstatic void M_DoControlled(const int16_t item_num, ITEM *const lara_item)\n{\n    ITEM *const item = Item_Get(item_num);\n    const XYZ_16 old_rot = item->rot;\n\n    item->rot.x = 0;\n    item->rot.y = lara_item->rot.y;\n    item->rot.z = 0;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if ((g_Input.action && lara->gun_status == LGS_ARMLESS\n         && !lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP)\n         && !lara->interact_target.is_moving)\n        || (lara->interact_target.is_moving\n            && lara->interact_target.item_num == item_num)) {\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (Lara_TestPosition(item, obj->bounds_func())) {\n            const XYZ_32 pos = {\n                .x = m_PickupPosition.x,\n                .y = lara_item->pos.y - item->pos.y,\n                .z = m_PickupPosition.z,\n            };\n            if (Lara_MovePosition(item, &pos)) {\n                Item_SwitchToAnim(lara_item, LA(LA_PICKUP), 0);\n                lara_item->current_anim_state = LS(LS_PICKUP);\n                lara->head_rot.y = 0;\n                lara->head_rot.x = 0;\n                lara->torso_rot.y = 0;\n                lara->torso_rot.x = 0;\n                lara->interact_target.is_moving = false;\n                lara->gun_status = LGS_HANDS_BUSY;\n            }\n            lara->interact_target.item_num = item_num;\n        } else if (\n            lara->interact_target.is_moving\n            && lara->interact_target.item_num == item_num) {\n            lara->interact_target.is_moving = false;\n            lara->interact_target.item_num = NO_ITEM;\n            lara->gun_status = LGS_ARMLESS;\n        }\n\n        goto cleanup;\n    }\n\n    if (lara->interact_target.item_num != item_num) {\n        goto cleanup;\n    }\n\n    if (lara_item->current_anim_state == LS(LS_PICKUP)) {\n        if (Item_TestFrameEqual(lara_item, M_LF_PICKUP_ERASE)) {\n            M_GetAllAtLaraPos(item);\n            lara->interact_target.item_num = NO_ITEM;\n        }\n        goto cleanup;\n    }\n\ncleanup:\n    item->rot = old_rot;\n}\n\nstatic inline bool M_HasValidPickupState(const ITEM *const lara_item)\n{\n    // TODO: unify under a pickup style config option, but retain sprint slide\n    // test in TR1/2 mode. Snap-pickups in crawl state do not make sense, so\n    // these always use TR3-style.\n    const LARA_TRX_ANIMATION anim = LA_U(Item_GetRelativeAnim(lara_item));\n    const LARA_TRX_STATE state = LS_U(lara_item->current_anim_state);\n    if (g_TRVersion < 3) {\n        if (anim == LA_SPRINT_SLIDE_STAND_RIGHT\n            || anim == LA_SPRINT_SLIDE_STAND_LEFT) {\n            return false;\n        }\n        if (state == LS_STOP) {\n            return true;\n        }\n    }\n\n    return (state == LS_STOP && anim == LA_STAND_IDLE)\n        || (state == LS_CROUCH_IDLE && anim == LA_CROUCH_IDLE)\n        || (state == LS_CRAWL_IDLE && anim == LA_CRAWL_IDLE\n            && g_Config.gameplay.enable_responsive_crawl);\n}\n\nstatic void M_DoAboveWater(const int16_t item_num, ITEM *const lara_item)\n{\n    ITEM *const item = Item_Get(item_num);\n    const LARA_TRX_ANIMATION anim = LA_U(Item_GetRelativeAnim(lara_item));\n\n    // clang-format off\n    const bool is_ducked = (\n        anim == LA_CRAWL_IDLE ||\n        anim == LA_CRAWL_PICKUP ||\n        anim == LA_CROUCH_IDLE ||\n        anim == LA_CROUCH_PICKUP ||\n        anim == LA_CROUCH_PICKUP_FLARE);\n    // clang-format on\n\n    if (g_Config.gameplay.enable_walk_to_items && !is_ducked\n        && item->object_id != O_FLARE_ITEM) {\n        M_DoControlled(item_num, lara_item);\n        return;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const XYZ_16 old_rot = item->rot;\n    item->rot = lara_item->rot;\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        goto cleanup;\n    }\n\n    if (lara_item->current_anim_state == LS(LS_PICKUP)) {\n        const int16_t rel_frame = Item_GetRelativeFrame(lara_item);\n        const bool pickup_now =\n            (anim == LA_PICKUP && rel_frame == M_LF_PICKUP_ERASE)\n            || (anim == LA_CROUCH_PICKUP\n                && (rel_frame == M_LF_PICKUP_CROUCH_1\n                    || rel_frame == M_LF_PICKUP_CROUCH_2))\n            || (anim == LA_CRAWL_PICKUP && rel_frame == M_LF_PICKUP_CRAWL);\n        if (pickup_now) {\n            M_DoPickup(item_num);\n        }\n        goto cleanup;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara_item->current_anim_state == LS(LS_FLARE_PICKUP)) {\n        const int16_t rel_frame = Item_GetRelativeFrame(lara_item);\n        const bool pickup_now =\n            Item_TestFrameEqual(lara_item, M_LF_PICKUP_FLARE)\n            || (anim == LA_CROUCH_PICKUP_FLARE\n                && rel_frame == M_LF_PICKUP_CROUCH_FLARE);\n        if (pickup_now && item->object_id == O_FLARE_ITEM\n            && lara->gun_type != LGT_FLARE) {\n            M_DoFlarePickup(item_num);\n        }\n        goto cleanup;\n    }\n\n    const bool is_flare_item = item->object_id == O_FLARE_ITEM;\n    if (g_Input.action && lara_item->current_anim_state == LS(LS_CRAWL_IDLE)\n        && (is_flare_item || !g_Config.gameplay.enable_responsive_crawl)) {\n        lara_item->goal_anim_state = LS(LS_CROUCH_IDLE);\n        goto cleanup;\n    }\n\n    if (g_Input.action && !lara_item->gravity\n        && (lara->gun_status == LGS_ARMLESS || anim == LA_CRAWL_IDLE)\n        && (lara->gun_type != LGT_FLARE || !is_flare_item)\n        && M_HasValidPickupState(lara_item)) {\n        if (is_flare_item) {\n            Lara_AnimateUntil(lara_item, LS(LS_FLARE_PICKUP));\n        } else {\n            Lara_AlignPosition(item, &m_PickupPosition);\n            Lara_AnimateUntil(lara_item, LS(LS_PICKUP));\n        }\n        if (is_ducked) {\n            lara_item->goal_anim_state =\n                LS(anim == LA_CRAWL_IDLE ? LS_CRAWL_IDLE : LS_CROUCH_IDLE);\n        } else {\n            lara_item->goal_anim_state = LS(LS_STOP);\n        }\n        lara->gun_status = LGS_HANDS_BUSY;\n        lara->head_rot.y = 0;\n        lara->head_rot.x = 0;\n        lara->torso_rot.y = 0;\n        lara->torso_rot.x = 0;\n        goto cleanup;\n    }\n\ncleanup:\n    item->rot = old_rot;\n}\n\nstatic void M_DoUnderwater(const int16_t item_num, ITEM *const lara_item)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const XYZ_16 old_rot = item->rot;\n\n    item->rot.x = -25 * DEG_1;\n    item->rot.y = lara_item->rot.y;\n    item->rot.z = 0;\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        goto cleanup;\n    }\n\n    if (lara_item->current_anim_state == LS(LS_PICKUP)) {\n        if (Item_TestFrameEqual(lara_item, M_LF_PICKUP_UW)) {\n            M_DoPickup(item_num);\n        }\n        goto cleanup;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara_item->current_anim_state == LS(LS_FLARE_PICKUP)) {\n        if (Item_TestFrameEqual(lara_item, M_LF_PICKUP_FLARE_UW)\n            && item->object_id == O_FLARE_ITEM && lara->gun_type != LGT_FLARE) {\n            M_DoFlarePickup(item_num);\n            Lara_Flare_DrawMeshes();\n        }\n        goto cleanup;\n    }\n\n    if (g_Input.action && lara_item->current_anim_state == LS(LS_TREAD)\n        && lara->gun_status == LGS_ARMLESS\n        && (lara->gun_type != LGT_FLARE || item->object_id != O_FLARE_ITEM)) {\n        if (!Lara_MovePosition(item, &m_PickupPositionUW)) {\n            goto cleanup;\n        }\n\n        if (item->object_id == O_FLARE_ITEM) {\n            lara_item->fall_speed = 0;\n            Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_FLARE_PICKUP), 0);\n            lara_item->current_anim_state = LS(LS_FLARE_PICKUP);\n        } else {\n            if (g_Config.gameplay.fix_lara_pickup_embed) {\n                lara_item->fall_speed = 0;\n            }\n            Lara_AnimateUntil(lara_item, LS(LS_PICKUP));\n        }\n        lara_item->goal_anim_state = LS(LS_TREAD);\n        goto cleanup;\n    }\n\ncleanup:\n    item->rot = old_rot;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->trigger_func = M_Trigger;\n    obj->control_func = M_Control;\n    obj->collision_func = Pickup_Collision;\n    obj->bounds_func = Pickup_Bounds;\n    obj->draw_func = Object_DrawPickupItem;\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_position = true;\n    obj->save_flags = true;\n}\n\nconst OBJECT_BOUNDS *Pickup_Bounds(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status == LWS_UNDERWATER\n        || lara->water_status == LWS_CHEAT) {\n        return &m_PickUpBoundsUW;\n    } else if (g_Config.gameplay.enable_walk_to_items) {\n        return &m_PickUpBoundsControlled;\n    } else {\n        return &m_PickUpBounds;\n    }\n}\n\nbool Pickup_Trigger(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_INVISIBLE) {\n        return false;\n    }\n\n    item->status = IS_DEACTIVATED;\n    return true;\n}\n\nvoid Pickup_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    const ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_INVISIBLE) != 0) {\n        return;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status == LWS_ABOVE_WATER\n        || lara->water_status == LWS_WADE) {\n        M_DoAboveWater(item_num, lara_item);\n    } else if (\n        lara->water_status == LWS_UNDERWATER\n        || lara->water_status == LWS_CHEAT) {\n        M_DoUnderwater(item_num, lara_item);\n    }\n}\n\nREGISTER_OBJECT(O_EXPLOSIVE_ITEM, M_Setup)\nREGISTER_OBJECT(O_FLAREBOX_ITEM, M_Setup)\nREGISTER_OBJECT(O_GRENADE_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_GRENADE_GUN_ITEM, M_Setup)\nREGISTER_OBJECT(O_ROCKET_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_ROCKET_GUN_ITEM, M_Setup)\nREGISTER_OBJECT(O_HARPOON_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_HARPOON_ITEM, M_Setup)\nREGISTER_OBJECT(O_KEY_ITEM_1, M_Setup)\nREGISTER_OBJECT(O_KEY_ITEM_2, M_Setup)\nREGISTER_OBJECT(O_KEY_ITEM_3, M_Setup)\nREGISTER_OBJECT(O_KEY_ITEM_4, M_Setup)\nREGISTER_OBJECT(O_LARGE_MEDIPACK_ITEM, M_Setup)\nREGISTER_OBJECT(O_LEADBAR_ITEM, M_Setup)\nREGISTER_OBJECT(O_M16_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_M16_ITEM, M_Setup)\nREGISTER_OBJECT(O_MP5_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_MP5_ITEM, M_Setup)\nREGISTER_OBJECT(O_MAGNUM_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_MAGNUM_ITEM, M_Setup)\nREGISTER_OBJECT(O_AUTOS_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_AUTOS_ITEM, M_Setup)\nREGISTER_OBJECT(O_DESERT_EAGLE_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_DESERT_EAGLE_ITEM, M_Setup)\nREGISTER_OBJECT(O_PICKUP_ITEM_1, M_Setup)\nREGISTER_OBJECT(O_PICKUP_ITEM_2, M_Setup)\nREGISTER_OBJECT(O_QUEST_ITEM_1, M_Setup)\nREGISTER_OBJECT(O_QUEST_ITEM_2, M_Setup)\nREGISTER_OBJECT(O_QUEST_ITEM_3, M_Setup)\nREGISTER_OBJECT(O_QUEST_ITEM_4, M_Setup)\nREGISTER_OBJECT(O_PISTOL_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_PISTOL_ITEM, M_Setup)\nREGISTER_OBJECT(O_PUZZLE_ITEM_1, M_Setup)\nREGISTER_OBJECT(O_PUZZLE_ITEM_2, M_Setup)\nREGISTER_OBJECT(O_PUZZLE_ITEM_3, M_Setup)\nREGISTER_OBJECT(O_PUZZLE_ITEM_4, M_Setup)\nREGISTER_OBJECT(O_SCION_ITEM_2, M_Setup)\nREGISTER_OBJECT(O_SECRET_1, M_Setup)\nREGISTER_OBJECT(O_SECRET_2, M_Setup)\nREGISTER_OBJECT(O_SECRET_3, M_Setup)\nREGISTER_OBJECT(O_SHOTGUN_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_SHOTGUN_ITEM, M_Setup)\nREGISTER_OBJECT(O_SMALL_MEDIPACK_ITEM, M_Setup)\nREGISTER_OBJECT(O_UZI_AMMO_ITEM, M_Setup)\nREGISTER_OBJECT(O_UZI_ITEM, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/pickup.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n\n#include <stdint.h>\n\nbool Pickup_Trigger(int16_t item_num);\nconst OBJECT_BOUNDS *Pickup_Bounds(void);\nvoid Pickup_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\nuint32_t Pickup_GetSecretMask(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/objects/general/puzzle_hole.c",
    "content": "#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/sound.h>\n\n#define M_LF_USE_PUZZLE 80\n\nstatic XYZ_32 m_PuzzleHolePosition = {\n    .x = 0,\n    .y = 0,\n    .z = WALL_L / 2 - LARA_RADIUS - 85,\n};\n\nstatic const OBJECT_BOUNDS m_PuzzleHoleBounds = {\n    .shift = {\n        .min = { .x = -200, .y = 0, .z = WALL_L / 2 - 200, },\n        .max = { .x = +200, .y = 0, .z = WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, },\n        .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, },\n    },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_PuzzleHoleBounds;\n}\n\nstatic bool M_IsUsable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    return item->status == IS_INACTIVE;\n}\n\nstatic void M_Use(ITEM *const lara_item, ITEM *const receptacle_item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    Lara_AlignPosition(receptacle_item, &m_PuzzleHolePosition);\n    Lara_AnimateUntil(lara_item, LS(LS_USE_PUZZLE));\n    lara_item->goal_anim_state = LS(LS_STOP);\n    lara->gun_status = LGS_HANDS_BUSY;\n    lara->interact_target.is_moving = false;\n}\n\nstatic void M_ConsumeKeyItem(ITEM *const receptacle_item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT_ID key_object_id =\n        Object_FindReceptacleKey(receptacle_item->object_id);\n    if (key_object_id != NO_OBJECT) {\n        Inv_RemoveItem(key_object_id);\n    }\n    lara->interact_target.item_num = NO_ITEM;\n}\n\nstatic void M_MarkDone(ITEM *const receptacle_item)\n{\n    const OBJECT_ID done_obj_id = Object_GetCognate(\n        receptacle_item->object_id, g_ReceptacleToReceptacleDoneMap);\n    if (done_obj_id != NO_OBJECT) {\n        receptacle_item->object_id = done_obj_id;\n    }\n    if (receptacle_item->status == IS_ACTIVE) {\n        return;\n    }\n\n    Item_SwitchToObjAnim(receptacle_item, 0, 0, receptacle_item->object_id);\n    const ANIM *const anim = Item_GetAnim(receptacle_item);\n    receptacle_item->current_anim_state = anim->current_anim_state;\n    receptacle_item->goal_anim_state = receptacle_item->current_anim_state;\n    receptacle_item->required_anim_state = 0;\n    receptacle_item->flags = IF_CODE_BITS;\n    receptacle_item->status = IS_ACTIVE;\n    Item_AddActive(Item_GetIndex(receptacle_item));\n    Item_Animate(receptacle_item);\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->status == IS_DEACTIVATED || item->status == IS_ACTIVE) {\n            M_MarkDone(item);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Item_IsTriggerActive(item)) {\n        Item_Animate(item);\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (lara_item->current_anim_state != LS(LS_STOP)) {\n        if (lara_item->current_anim_state == LS(LS_USE_PUZZLE)\n            && Lara_TestPosition(item, obj->bounds_func())\n            && Item_TestFrameEqual(lara_item, M_LF_USE_PUZZLE)) {\n            M_ConsumeKeyItem(item);\n            M_MarkDone(item);\n        }\n        return;\n    }\n\n    if (lara->interact_target.is_moving\n        && lara->interact_target.item_num == item_num) {\n        M_Use(lara_item, item);\n    }\n\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || lara_item->gravity) {\n        return;\n    }\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    if (!GF_ShowInventoryKeys(item->object_id)) {\n        Lara_RefuseInteraction();\n    }\n}\n\nstatic void M_CollisionDone(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)\n        || !Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    // Trying to interact with a complete puzzle hole\n    Lara_RefuseInteraction();\n}\n\nstatic void M_SetupEmpty(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->handle_save_func = M_HandleSave;\n    obj->is_usable_func = M_IsUsable;\n    obj->bounds_func = M_Bounds;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SetupDone(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = M_CollisionDone;\n    obj->bounds_func = M_Bounds;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_PUZZLE_HOLE_1, M_SetupEmpty)\nREGISTER_OBJECT(O_PUZZLE_HOLE_2, M_SetupEmpty)\nREGISTER_OBJECT(O_PUZZLE_HOLE_3, M_SetupEmpty)\nREGISTER_OBJECT(O_PUZZLE_HOLE_4, M_SetupEmpty)\nREGISTER_OBJECT(O_PUZZLE_DONE_1, M_SetupDone)\nREGISTER_OBJECT(O_PUZZLE_DONE_2, M_SetupDone)\nREGISTER_OBJECT(O_PUZZLE_DONE_3, M_SetupDone)\nREGISTER_OBJECT(O_PUZZLE_DONE_4, M_SetupDone)\n"
  },
  {
    "path": "src/trx/game/objects/general/rocket.c",
    "content": "#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/gun/misc.h>\n#include <trx/game/gun/smashing.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/output/lights.h>\n#include <trx/game/output/state.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_BLAST_RADIUS WALL_L // = 1024\n#define M_SPEED (WALL_L / 2) // = 512\n#define M_SPEED_UW (STEP_L / 2) // = 128\n\nstatic void M_SetTR3ProjectileShade(ITEM *const item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    // OG TR3 uses `item->shade = -0x3DF0` on projectiles; in TRX any negative\n    // shade forces the dynamic/smoothed lighting path.\n    item->shade.value_1 = -1;\n    item->shade.value_2 = -1;\n}\n\nstatic XYZ_32 M_GetLocalZOffset(const ITEM *const item, const int32_t dist)\n{\n    const int32_t cx = Math_Cos(item->rot.x);\n    const int32_t sx = Math_Sin(item->rot.x);\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sy = Math_Sin(item->rot.y);\n\n    const int32_t horz = (dist * cx) >> W2V_SHIFT;\n    return (XYZ_32) {\n        .x = (horz * sy) >> W2V_SHIFT,\n        .y = -(dist * sx) >> W2V_SHIFT,\n        .z = (horz * cy) >> W2V_SHIFT,\n    };\n}\n\nstatic void M_Explode(const int16_t rocket_item_num, const XYZ_32 pos)\n{\n    const ITEM *const rocket_item = Item_Get(rocket_item_num);\n    const ROOM *const room = Room_Get(rocket_item->room_num);\n    const bool is_underwater = room != nullptr && room->flags.underwater;\n\n    if (g_TRVersion == 3) {\n        if (is_underwater) {\n            Sparks_TriggerUnderwaterExplosion(rocket_item);\n        } else {\n            Sparks_TriggerExplosionSparks(pos, 3, -2, 0, rocket_item->room_num);\n            for (int32_t i = 0; i < 2; i++) {\n                Sparks_TriggerExplosionSparks(\n                    pos, 3, -1, 0, rocket_item->room_num);\n            }\n        }\n    } else {\n        const int16_t effect_num = Effect_Create(rocket_item->room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *const effect = Effect_Get(effect_num);\n            effect->pos = pos;\n            effect->speed = 0;\n            effect->frame_num = 0;\n            effect->counter = 0;\n            effect->object_id = O_EXPLOSION_1;\n        }\n    }\n\n    const XYZ_32 *const sfx_pos =\n        g_TRVersion >= 3 ? &rocket_item->pos : nullptr;\n    const uint32_t flags =\n        g_TRVersion >= 3 ? (0x1800000 | SPM_PITCH) : SPM_NORMAL;\n    Sound_Effect(SFX_EXPLOSION_1, sfx_pos, flags);\n    Sound_Effect(SFX_EXPLOSION_2, sfx_pos, SPM_NORMAL);\n    Item_Kill(rocket_item_num);\n\n    Creature_AlertNearbyGuards(rocket_item);\n}\n\nstatic bool M_CanExplodeTarget(const ITEM *const item)\n{\n    const OBJECT *const object = Object_Get(item->object_id);\n    if (object->can_be_exploded_func != nullptr) {\n        return object->can_be_exploded_func(item);\n    }\n\n    const ITEM_ACTION action = ItemAction_ToGameID(ITEM_ACTION_FINISH_LEVEL);\n    for (int32_t i = 0; i < object->anim_count; i++) {\n        const ANIM *const anim = Object_GetAnim(object, i);\n        if (Anim_HasFXCommand(anim, action)) {\n            return false;\n        }\n    }\n\n    return true;\n}\n\nstatic bool M_TryExplodeItem(\n    const ITEM *const projectile_item, const GAME_VECTOR old_pos,\n    const int16_t target_item_num, const int32_t radius)\n{\n    ITEM *const target_item = Item_Get(target_item_num);\n    const OBJECT *const target_obj = Object_Get(target_item->object_id);\n    if (target_item == Lara_GetItem()) {\n        return false;\n    }\n    if (!target_item->collidable) {\n        return false;\n    }\n\n    if (target_item->status == IS_INVISIBLE\n        || target_obj->collision_func == nullptr) {\n        return false;\n    }\n\n    if (!Item_CanBeProjectileTarget(target_item)) {\n        return false;\n    }\n\n    const ANIM_FRAME *const frame = Item_GetBestFrame(target_item);\n    const BOUNDS_16 *const bounds = &frame->bounds;\n\n    const int32_t cdy = projectile_item->pos.y - target_item->pos.y;\n    if (cdy + radius < bounds->min.y || cdy - radius > bounds->max.y) {\n        return false;\n    }\n\n    const int32_t cy = Math_Cos(target_item->rot.y);\n    const int32_t sy = Math_Sin(target_item->rot.y);\n    const int32_t cdx = projectile_item->pos.x - target_item->pos.x;\n    const int32_t cdz = projectile_item->pos.z - target_item->pos.z;\n    const int32_t odx = old_pos.x - target_item->pos.x;\n    const int32_t odz = old_pos.z - target_item->pos.z;\n\n    const int32_t rx = (cy * cdx - sy * cdz) >> W2V_SHIFT;\n    const int32_t sx = (cy * odx - sy * odz) >> W2V_SHIFT;\n    if ((rx + radius < bounds->min.x && sx + radius < bounds->min.x)\n        || (rx - radius > bounds->max.x && sx - radius > bounds->max.x)) {\n        return false;\n    }\n\n    const int32_t rz = (sy * cdx + cy * cdz) >> W2V_SHIFT;\n    const int32_t sz = (sy * odx + cy * odz) >> W2V_SHIFT;\n    if ((rz + radius < bounds->min.z && sz + radius < bounds->min.z)\n        || (rz - radius > bounds->max.z && sz - radius > bounds->max.z)) {\n        return false;\n    }\n\n    if (!Item_CanTakeDamage(target_item)) {\n        return false;\n    }\n\n    const GAME_VECTOR hit_pos = {\n        .pos = projectile_item->pos,\n        .room_num = projectile_item->room_num,\n    };\n    Gun_HitTarget(\n        target_item, &old_pos, &hit_pos, g_Weapons[LGT_ROCKET].damage);\n    Stats_AddAmmoHits();\n\n    if (Gun_GetSmashPolicy(target_item) == GUN_SMASH_POLICY_HEAVY) {\n        if (Object_IsType(projectile_item->object_id, g_HeavyMissileObjects)) {\n            Gun_SmashItem(target_item_num);\n        }\n    } else if (Gun_GetSmashPolicy(target_item) != GUN_SMASH_POLICY_NONE) {\n        Gun_SmashItem(target_item_num);\n    } else if (\n        target_item->hit_points <= 0 && M_CanExplodeTarget(target_item)) {\n        Creature_Die(target_item_num, true);\n    }\n    return true;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const GAME_VECTOR old_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n\n    const ROOM *const room = Room_Get(item->room_num);\n    const bool was_underwater = room != nullptr && room->flags.underwater;\n    if (was_underwater) {\n        if (item->speed < M_SPEED_UW) {\n            item->speed += (item->speed >> 2) + 4;\n            CLAMPG(item->speed, M_SPEED_UW);\n        } else {\n            item->speed -= item->speed >> 2;\n        }\n        item->rot.z += DEG_1 * ((item->speed >> 3) + 3);\n    } else {\n        if (item->speed < M_SPEED) {\n            item->speed += (item->speed >> 2) + 4;\n        }\n        item->rot.z += DEG_1 * ((item->speed >> 2) + 7);\n    }\n\n    if (g_TRVersion == 3) {\n        M_SetTR3ProjectileShade(item);\n\n        const XYZ_32 back_128 = M_GetLocalZOffset(item, -128);\n        const int32_t back_dist = -1536 - (Random_GetControl() & 0x1FF);\n        const XYZ_32 back_vel = M_GetLocalZOffset(item, back_dist);\n\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        if ((time4 & 4) != 0) {\n            Sparks_TriggerRocketFlame(\n                back_128,\n                (XYZ_32) {\n                    .x = back_vel.x - back_128.x,\n                    .y = back_vel.y - back_128.y,\n                    .z = back_vel.z - back_128.z,\n                },\n                item_num, item->room_num);\n        }\n\n        Sparks_TriggerRocketSmoke(\n            (XYZ_32) {\n                .x = item->pos.x + back_128.x,\n                .y = item->pos.y + back_128.y,\n                .z = item->pos.z + back_128.z,\n            },\n            -1, item->room_num);\n\n        if (was_underwater) {\n            const XYZ_32 bubble_pos = {\n                .x = item->pos.x + back_128.x,\n                .y = item->pos.y + back_128.y,\n                .z = item->pos.z + back_128.z,\n            };\n            Spawn_BubbleEx(&bubble_pos, item->room_num, 4, 8);\n        }\n\n        if (g_Config.visuals.enable_gun_lighting) {\n            const int32_t rnd = Random_GetControl();\n            const XYZ_32 light_pos = {\n                .x = item->pos.x + back_128.x + (rnd & 0xF) - 8,\n                .y = item->pos.y + back_128.y + ((rnd >> 4) & 0xF) - 8,\n                .z = item->pos.z + back_128.z + ((rnd >> 8) & 0xF) - 8,\n            };\n            const int32_t c = Random_GetControl();\n            const RGB_888 color = {\n                .r = (uint8_t)((c & 0x1F) + 224),\n                .g = (uint8_t)(((c >> 5) & 0x3F) + 128),\n                .b = (uint8_t)((c >> 11) & 0x3F),\n            };\n            Output_AddDynamicLightRGB(light_pos, 14, color);\n        }\n    }\n\n    const int32_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT;\n    item->pos.x += (speed * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n    item->pos.z += (speed * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n    Item_UpdateRoom(item_num, room_num);\n\n    if (g_TRVersion == 3) {\n        const ROOM *const new_room = Room_Get(item->room_num);\n        const bool is_underwater =\n            new_room != nullptr && new_room->flags.underwater;\n        if (is_underwater && !was_underwater) {\n            FX_Water_SetupSplash(&(FX_WATER_SPLASH_SETUP) {\n                .x = item->pos.x,\n                .y = new_room->max_ceiling,\n                .z = item->pos.z,\n                .inner_xz_off = 16,\n                .inner_xz_size = 12,\n                .inner_y_size = -96,\n                .inner_xz_vel = 160,\n                .inner_y_vel = -0x4000,\n                .inner_gravity = 128,\n                .inner_friction = 7,\n                .middle_xz_off = 24,\n                .middle_xz_size = 24,\n                .middle_y_size = -64,\n                .middle_xz_vel = 224,\n                .middle_y_vel = -0x2000,\n                .middle_gravity = 72,\n                .middle_friction = 8,\n                .outer_xz_off = 32,\n                .outer_xz_size = 32,\n                .outer_xz_vel = 272,\n                .outer_friction = 9,\n            });\n        }\n    }\n\n    bool explode = false;\n    int32_t radius = 0;\n    if (item->pos.y >= item->floor\n        || item->pos.y <= Room_GetCeiling(sector, item->pos)) {\n        radius = M_BLAST_RADIUS;\n        explode = true;\n    }\n\n    const GAME_VECTOR new_pos = {\n        .pos = item->pos,\n        .room_num = item->room_num,\n    };\n    if (Gun_SmashItems(old_pos, new_pos, nullptr, item->object_id)\n        == PROJECTILE_HIT_STOP) {\n        explode = true;\n    }\n\n    if (g_Config.gameplay.projectile_area_damage\n        == PROJECTILE_AREA_DAMAGE_MULTI_SWEEP) {\n        Room_GetNearbyRooms(item->pos, radius * 4, radius * 4, item->room_num);\n        for (int32_t i = 0; i < Room_DrawGetCount(); i++) {\n            const ROOM *const nearby_room = Room_Get(Room_DrawGetRoom(i));\n            for (int16_t target_item_num = nearby_room->item_num;\n                 target_item_num != NO_ITEM;\n                 target_item_num = Item_Get(target_item_num)->next_item) {\n                if (!M_TryExplodeItem(item, old_pos, target_item_num, radius)) {\n                    continue;\n                }\n\n                if (!explode) {\n                    explode = true;\n                    radius = WALL_L;\n                    i = -1;\n                    break;\n                }\n            }\n        }\n    } else {\n        for (int16_t target_item_num = room->item_num;\n             target_item_num != NO_ITEM;\n             target_item_num = Item_Get(target_item_num)->next_item) {\n            if (M_TryExplodeItem(item, old_pos, target_item_num, radius)) {\n                explode = true;\n            }\n        }\n    }\n\n    if (explode) {\n        if (was_underwater) {\n            item->pos = old_pos.pos;\n            Item_UpdateRoom(item_num, old_pos.room_num);\n        }\n        M_Explode(item_num, old_pos.pos);\n    }\n}\n\nstatic void M_SetupCommon(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_position = true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    M_SetupCommon(obj);\n}\n\nstatic void M_SetupHeavy(OBJECT *const obj)\n{\n    const OBJECT *const ref_obj = Object_Get(O_ROCKET);\n    if (!ref_obj->loaded) {\n        return;\n    }\n\n    M_SetupCommon(obj);\n    obj->frame_base = ref_obj->frame_base;\n    obj->anim_idx = ref_obj->anim_idx;\n    obj->mesh_idx = ref_obj->mesh_idx;\n    obj->mesh_count = ref_obj->mesh_count;\n    obj->loaded = true;\n}\n\nREGISTER_OBJECT(O_ROCKET, M_Setup)\nREGISTER_OBJECT(O_HEAVY_ROCKET, M_SetupHeavy)\n"
  },
  {
    "path": "src/trx/game/objects/general/save_crystal.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/general/pickup.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_PC_MESH 0b00000000'00000001\n#define M_PS_MESH 0b00000000'00000010\n\ntypedef struct {\n    bool initialised;\n    bool counted_for_stats;\n    bool used_for_save;\n    int16_t initial_angle;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"counted_for_stats\", &p->counted_for_stats));\n    JSON_SHOULD(JSON_READ(io, \"used_for_save\", &p->used_for_save));\n    JSON_SHOULD(JSON_READ(io, \"initialised\", &p->initialised));\n    JSON_SHOULD(JSON_READ(io, \"initial_angle\", &p->initial_angle));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"counted_for_stats\", p->counted_for_stats);\n    JSONW_WRITE(io, \"used_for_save\", p->used_for_save);\n    JSONW_WRITE(io, \"initialised\", p->initialised);\n    JSONW_WRITE(io, \"initial_angle\", p->initial_angle);\n}\n\nstatic void M_CountCrystal(M_PRIV *const p)\n{\n    if (p->counted_for_stats) {\n        return;\n    }\n    p->counted_for_stats = true;\n    Stats_AddCrystal();\n}\n\nstatic const OBJECT_BOUNDS m_SaveCrystal_Bounds = {\n    .shift = {\n        .min = { .x = -STEP_L*3/2, .y = -100, .z = -STEP_L*3/2, },\n        .max = { .x = +STEP_L*3/2, .y = +WALL_L, .z = +STEP_L*3/2, },\n    },\n    .rot = {\n        .min = { .x = -DEG_45, .y = 0, .z = 0, },\n        .max = { .x = +DEG_45, .y = 0, .z = 0, },\n    },\n};\n\nstatic const OBJECT_BOUNDS m_UW_Bounds = {\n    .shift = {\n        .min = { .x = -STEP_L*3/2, .y = -WALL_L, .z = -STEP_L*3/2, },\n        .max = { .x = +STEP_L*3/2, .y = +WALL_L, .z = +STEP_L*3/2, },\n    },\n    .rot = {\n        .min = { .x = -DEG_90, .y = 0, .z = 0, },\n        .max = { .x = +DEG_90, .y = 0, .z = 0, },\n    },\n};\n\nstatic const LARA_TRX_STATE m_StopStates[] = {\n    // clang-format off\n    LS_STOP,\n    LS_TREAD,\n    LS_SURF_TREAD,\n    LS_TRX_INVALID, // sentinel\n    // clang-format on\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    return lara->water_status == LWS_ABOVE_WATER\n            || lara->water_status == LWS_WADE\n        ? &m_SaveCrystal_Bounds\n        : &m_UW_Bounds;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->initialised = false;\n    p->counted_for_stats = false;\n    p->used_for_save = false;\n    p->initial_angle = 0;\n\n    if (g_TRVersion != 3) {\n        if (g_Config.gameplay.enable_save_crystals) {\n            Item_AddActive(item_num);\n        } else {\n            Item_Get(item_num)->status = IS_INVISIBLE;\n        }\n    }\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    switch (stage) {\n    case SAVEGAME_STAGE_AFTER_LOAD:\n        if (item->status == IS_DEACTIVATED) {\n            const int16_t item_num = Item_GetIndex(item);\n            Item_RemoveDrawn(item_num);\n        }\n        break;\n\n    case SAVEGAME_STAGE_BEFORE_SAVE:\n        M_PRIV *const p = item->priv;\n        if (p->used_for_save) {\n            // need to reset the crystal status\n            item->status = IS_DEACTIVATED;\n            p->used_for_save = false;\n            const int16_t item_num = Item_GetIndex(item);\n            Item_RemoveDrawn(item_num);\n        }\n\n    default:\n        break;\n    }\n}\n\nstatic void M_ControlHeal(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->status == IS_INVISIBLE || item->clear_body) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    if (!p->initialised) {\n        p->initialised = true;\n        p->initial_angle = item->pos.y;\n    }\n\n    item->rot.y += 1024;\n    const int32_t timer = Output_GetTimeInGame();\n    const int16_t angle = Math_Cos((timer & 0x3F) << 10);\n    int32_t c = ABS(angle >> 9);\n    CLAMPG(c, 31);\n    c <<= 3;\n\n    item->pos.y = p->initial_angle - ABS(angle >> 6) - 64;\n\n    Output_AddDynamicLightRGB(item->pos, 8, (RGB_888) { 0, c, 0 });\n\n    ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = ABS(item->pos.x - lara_item->pos.x);\n    const int32_t dy = ABS(item->pos.y - lara_item->pos.y);\n    const int32_t dz = ABS(item->pos.z - lara_item->pos.z);\n    if (dx < STEP_L && dy < WALL_L && dz < STEP_L) {\n        M_CountCrystal(p);\n\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->poison_timer = 0;\n        lara_item->hit_points += LARA_MAX_HITPOINTS / 2;\n        CLAMPG(lara_item->hit_points, LARA_MAX_HITPOINTS);\n\n        // PS1: SFX_SAVE_CRYSTAL, PC: SFX_MENU_MEDI\n        Sound_Effect(SFX_MENU_MEDI, &lara_item->pos, SPM_NORMAL);\n\n        Item_Kill(item_num);\n    }\n}\n\nstatic void M_ControlSave(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->mesh_bits = g_Config.visuals.enable_ps1_crystals\n            && Object_Get(item->object_id)->mesh_count > 1\n        ? M_PS_MESH\n        : M_PC_MESH;\n    Item_Animate(item);\n}\n\nstatic void M_CollisionSave(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    Object_Collision(item_num, lara_item, coll);\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || lara_item->gravity) {\n        return;\n    }\n\n    if (!Lara_HasState(m_StopStates)) {\n        return;\n    }\n\n    item->rot.y = lara_item->rot.y;\n    item->rot.z = 0;\n    item->rot.x = 0;\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    if (g_Config.flow.load_save_disabled) {\n        Lara_RefuseInteraction();\n        return;\n    }\n\n    int16_t room_num = lara_item->room_num;\n    const XYZ_32 pos = lara_item->pos;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    const int32_t floor = Room_GetHeight(sector, pos);\n    if (ceiling >= item->pos.y || floor < item->pos.y) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    M_CountCrystal(p);\n    p->used_for_save = true;\n    GF_ShowInventory(INV_SAVE_CRYSTAL_MODE);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->priv_size = sizeof(M_PRIV);\n    if (g_TRVersion == 3) {\n        obj->control_func = M_ControlHeal;\n        obj->collision_func = nullptr;\n        obj->save_flags = true;\n    } else if (g_Config.gameplay.enable_save_crystals) {\n        obj->control_func = M_ControlSave;\n        obj->collision_func = M_CollisionSave;\n        obj->save_flags = true;\n        Object_SetReflective(O_SAVE_CRYSTAL_ITEM, true);\n    }\n    obj->bounds_func = M_Bounds;\n}\n\nREGISTER_OBJECT(O_SAVE_CRYSTAL_ITEM, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/scion1.c",
    "content": "// Tomb of Qualopec and Sanctuary Scion pickup.\n// Triggers O_LARA_EXTRA pedestal pickup animation.\n\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/level.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n\nstatic XYZ_32 m_Scion1_Position = { 0, 640, -310 };\n\nstatic const OBJECT_BOUNDS m_Scion1_Bounds = {\n    .shift = {\n        .min = { .x = -256, .y = +640 - 100, .z = -350, },\n        .max = { .x = +256, .y = +640 + 100, .z = -200, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = 0, .z = 0, },\n        .max = { .x = +10 * DEG_1, .y = 0, .z = 0, },\n    },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_Scion1_Bounds;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->status == IS_DEACTIVATED) {\n            const int16_t item_num = Item_GetIndex(item);\n            Item_RemoveDrawn(item_num);\n        }\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    const XYZ_16 old_rot = item->rot;\n    item->rot.y = lara_item->rot.y;\n    item->rot.x = 0;\n    item->rot.z = 0;\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        goto cleanup;\n    }\n\n    if (g_Input.action && lara->gun_status == LGS_ARMLESS && !lara_item->gravity\n        && lara_item->current_anim_state == LS(LS_STOP)) {\n        lara->interact_target.item_num = item_num;\n        Lara_AlignPosition(item, &m_Scion1_Position);\n        Lara_SwitchToExtraState(LS_EXTRA_SCION_PICKUP_1);\n        Camera_InvokeCinematic(lara_item, 0, 0);\n    }\ncleanup:\n    item->rot = old_rot;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->handle_save_func = M_HandleSave;\n    obj->draw_func = Object_DrawPickupItem;\n    obj->collision_func = M_Collision;\n    obj->save_flags = true;\n    obj->bounds_func = M_Bounds;\n}\n\nREGISTER_OBJECT(O_SCION_ITEM_1, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/scion3.c",
    "content": "// The Great Pyramid shootable Scion.\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\nstatic bool M_ShouldSpawnBlood(const ITEM *const item)\n{\n    return !g_Config.visuals.fix_texture_issues;\n}\n\nstatic bool M_CanTakeDamage(const ITEM *const item)\n{\n    return item->status == IS_ACTIVE;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    static int32_t counter = 0;\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item)) {\n        if (!LOT_EnableBaddieAI(item_num, true)) {\n            return;\n        }\n        item->status = IS_ACTIVE;\n    }\n\n    if (item->hit_points > 0) {\n        counter = 0;\n        Item_Animate(item);\n        return;\n    }\n\n    if (counter == 0) {\n        item->status = IS_INVISIBLE;\n        item->hit_points = 0;\n        Room_TestTriggers(item);\n        Item_RemoveDrawn(item_num);\n    }\n\n    if (counter % 10 == 0) {\n        int16_t effect_num = Effect_Create(item->room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *effect = Effect_Get(effect_num);\n            effect->pos.x = item->pos.x + (Random_GetControl() - 0x4000) / 32;\n            effect->pos.y =\n                item->pos.y + (Random_GetControl() - 0x4000) / 256 - 500;\n            effect->pos.z = item->pos.z + (Random_GetControl() - 0x4000) / 32;\n            effect->speed = 0;\n            effect->frame_num = 0;\n            effect->object_id = O_EXPLOSION_1;\n            effect->counter = 0;\n            Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL);\n            g_Camera.bounce = -200;\n        }\n    }\n\n    counter++;\n    if (counter >= LOGIC_FPS * 3) {\n        Item_Kill(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->can_take_damage_func = M_CanTakeDamage;\n    obj->should_spawn_blood_func = M_ShouldSpawnBlood;\n    obj->hit_points = 5;\n    obj->save_flags = true;\n    obj->save_hitpoints = true;\n}\n\nREGISTER_OBJECT(O_SCION_ITEM_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/scion4.c",
    "content": "// Atlantis Scion - triggers O_LARA_EXTRA reach anim.\n\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n\n#define EXTRA_ANIM_HOLDER_SCION 0\n\nstatic XYZ_32 m_Scion4_Position = { 0, 280, -512 + 105 };\n\nstatic const OBJECT_BOUNDS m_Scion4_Bounds = {\n    .shift = {\n        .min = { .x = -256, .y = +256 - 50, .z = -512 - 350, },\n        .max = { .x = +256, .y = +256 + 50, .z = -200, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = 0, .z = 0, },\n        .max = { .x = +10 * DEG_1, .y = 0, .z = 0, },\n    },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_Scion4_Bounds;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    Item_Animate(Item_Get(item_num));\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    int16_t rotx = item->rot.x;\n    int16_t roty = item->rot.y;\n    int16_t rotz = item->rot.z;\n    item->rot.y = lara_item->rot.y;\n    item->rot.x = 0;\n    item->rot.z = 0;\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        goto cleanup;\n    }\n\n    if (g_Input.action && lara->gun_status == LGS_ARMLESS && !lara_item->gravity\n        && lara_item->current_anim_state == LS(LS_STOP)) {\n        Lara_AlignPosition(item, &m_Scion4_Position);\n        Lara_SwitchToExtraState(LS_EXTRA_SCION_PICKUP_2);\n        Camera_InvokeCinematic(lara_item, 0, -DEG_90);\n    }\ncleanup:\n    item->rot.x = rotx;\n    item->rot.y = roty;\n    item->rot.z = rotz;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->save_flags = true;\n    obj->bounds_func = M_Bounds;\n}\n\nREGISTER_OBJECT(O_SCION_ITEM_4, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/scion_holder.c",
    "content": "#include <trx/game/objects.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    Item_Animate(Item_Get(item_num));\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_SCION_HOLDER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/shoal.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow/util.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#include <stdint.h>\n\n#define M_SHOAL_COUNT 8\n#define M_FISH_PER_SHOAL 24\n\n#define M_LEVEL_RANGES(id, ...)                                                \\\n    { id, sizeof((XYZ_16[])__VA_ARGS__) / sizeof(XYZ_16), __VA_ARGS__ }\n\ntypedef struct {\n    int16_t angle;\n    uint8_t speed;\n    bool on;\n    int16_t angle_time;\n    int16_t speed_time;\n    XYZ_16 range;\n} M_LEADER;\n\ntypedef struct {\n    XYZ_16 pos;\n    uint16_t angle; // 0..4095\n    int16_t dest_y;\n    int8_t ang_add;\n    uint8_t speed;\n    uint8_t acc;\n    uint8_t swim;\n    struct {\n        struct {\n            XYZ_16 pos;\n            uint16_t angle; // 0..4095\n            uint8_t swim;\n        } prev, result;\n    } interp;\n} M_FISH;\n\ntypedef struct {\n    int32_t leader_num;\n    M_FISH fish[M_FISH_PER_SHOAL + 1];\n    M_LEADER leader;\n    int32_t piranha_hit_wait;\n    int16_t carcass_item_num;\n} M_PRIV;\n\ntypedef struct {\n    int32_t level_id;\n    int32_t range_count;\n    XYZ_16 ranges[M_SHOAL_COUNT];\n} M_FISH_LEVEL_CONFIG;\n\nstatic const M_FISH_LEVEL_CONFIG m_FishLevelConfigs[] = {\n    M_LEVEL_RANGES(\n        1,\n        {\n            { .x = 8, .z = 20, .y = 3 },\n        }),\n    M_LEVEL_RANGES(\n        2,\n        {\n            { .x = 4, .z = 4, .y = 2 },\n            { .x = 4, .z = 16, .y = 2 },\n            { .x = 4, .z = 28, .y = 3 },\n        }),\n    M_LEVEL_RANGES(\n        3,\n        {\n            { .x = 4, .z = 12, .y = 1 },\n            { .x = 0, .z = 12, .y = 2 },\n            { .x = 8, .z = 4, .y = 2 },\n            { .x = 4, .z = 8, .y = 1 },\n            { .x = 4, .z = 16, .y = 2 },\n            { .x = 4, .z = 24, .y = 1 },\n            { .x = 12, .z = 4, .y = 1 },\n            { .x = 16, .z = 4, .y = 1 },\n        }),\n    M_LEVEL_RANGES(\n        0,\n        {\n            { .x = 4, .z = 4, .y = 1 },\n            { .x = 16, .z = 8, .y = 2 },\n            { .x = 24, .z = 8, .y = 2 },\n            { .x = 8, .z = 16, .y = 2 },\n            { .x = 8, .z = 12, .y = 1 },\n            { .x = 20, .z = 8, .y = 2 },\n            { .x = 16, .z = 8, .y = 1 },\n        }),\n    M_LEVEL_RANGES(\n        5,\n        {\n            { .x = 12, .z = 12, .y = 6 },\n            { .x = 12, .z = 20, .y = 6 },\n            { .x = 20, .z = 4, .y = 8 },\n        }),\n    M_LEVEL_RANGES(\n        6,\n        {\n            { .x = 20, .z = 4, .y = 6 },\n        }),\n    M_LEVEL_RANGES(\n        7,\n        {\n            { .x = 16, .z = 16, .y = 8 },\n            { .x = 4, .z = 8, .y = 5 },\n        }),\n};\n\nstatic bool M_IsValidShoalNum(const int32_t shoal_num)\n{\n    return shoal_num >= 0 && shoal_num < M_SHOAL_COUNT;\n}\n\nstatic uint16_t M_GetFishAngle12(\n    const int32_t x1, const int32_t z1, const int32_t x2, const int32_t z2)\n{\n    const int32_t dx = x2 - x1;\n    const int32_t dz = z2 - z1;\n    const int32_t fish_angle16 = Math_Atan(dx, dz) - DEG_90;\n    return (fish_angle16 >> 4) & 0xFFF; // 0..4095\n}\n\nstatic int32_t M_GetAngle12Diff(const int32_t a, const int32_t b)\n{\n    int32_t diff = a - b;\n    if (diff > 2048) {\n        diff -= 4096;\n    } else if (diff < -2048) {\n        diff += 4096;\n    }\n    return diff;\n}\n\nstatic bool M_FishNearItem(\n    const XYZ_32 *const pos, const int32_t dist, const ITEM *const item)\n{\n    const int32_t dx = pos->x - item->pos.x;\n    const int32_t dy = ABS(pos->y - item->pos.y);\n    const int32_t dz = pos->z - item->pos.z;\n\n    // clang-format off\n    if (dx < -dist || dx > dist ||\n        dz < -dist || dz > dist ||\n        dy < -3072 || dy > 3072 ||\n        SQUARE(dz) + SQUARE(dx) > SQUARE(dist)\n        || dy > dist) {\n        return false;\n    }\n    // clang-format on\n\n    return true;\n}\n\nstatic void M_SetupShoal(M_PRIV *const p, const int32_t shoal_num)\n{\n    if (p == nullptr || !M_IsValidShoalNum(shoal_num)) {\n        return;\n    }\n\n    M_LEADER *const leader = &p->leader;\n\n    if (g_TRVersion < 3) {\n        goto fallback;\n    }\n\n    const int32_t lvl = GF_BadGetLevelNum();\n    for (size_t i = 0; i < ARRAY_SIZE(m_FishLevelConfigs); i++) {\n        const M_FISH_LEVEL_CONFIG *cfg = &m_FishLevelConfigs[i];\n        if (cfg->level_id == lvl && shoal_num < cfg->range_count) {\n            const XYZ_16 *const r = &cfg->ranges[shoal_num];\n            leader->range.x = (r->x + 2) << 8;\n            leader->range.y = r->y << 8;\n            leader->range.z = (r->z + 2) << 8;\n            return;\n        }\n    }\n\nfallback:\n    leader->range.x = 256;\n    leader->range.y = 256;\n    leader->range.z = 256;\n}\n\nstatic void M_SetupFish(M_PRIV *const p, const ITEM *const item)\n{\n    if (p == nullptr || item == nullptr || !M_IsValidShoalNum(p->leader_num)) {\n        return;\n    }\n\n    M_LEADER *const leader = &p->leader;\n    M_FISH *fish = &p->fish[0];\n\n    const int16_t x = leader->range.x;\n    const int16_t y = leader->range.y;\n    const int16_t z = leader->range.z;\n\n    fish->pos.x = 0;\n    fish->pos.y = 0;\n    fish->pos.z = 0;\n    fish->angle = 0;\n    fish->speed = ((Random_GetControl() & 0x3F) + 8);\n    fish->swim = (Random_GetControl() & 0x3F);\n    fish->interp.prev.pos = fish->pos;\n    fish->interp.prev.angle = fish->angle;\n    fish->interp.prev.swim = fish->swim;\n    fish->interp.result.pos = fish->pos;\n    fish->interp.result.angle = fish->angle;\n    fish->interp.result.swim = fish->swim;\n\n    for (int32_t i = 0; i < M_FISH_PER_SHOAL; i++) {\n        fish = &p->fish[i + 1];\n        fish->pos.x = Random_GetControl() % (x << 1) - x;\n        fish->pos.y = Random_GetControl() % y;\n        fish->pos.z = Random_GetControl() % (z << 1) - z;\n        fish->dest_y = Random_GetControl() % y;\n        fish->angle = Random_GetControl() & 0xFFF;\n        fish->speed = (Random_GetControl() & 0x1F) + 32;\n        fish->swim = Random_GetControl() & 0x3F;\n        fish->interp.prev.pos = fish->pos;\n        fish->interp.prev.angle = fish->angle;\n        fish->interp.prev.swim = fish->swim;\n        fish->interp.result.pos = fish->pos;\n        fish->interp.result.angle = fish->angle;\n        fish->interp.result.swim = fish->swim;\n    }\n\n    leader->on = true;\n    leader->angle = 0;\n    leader->speed = (Random_GetControl() & 0x7F) + 32;\n    leader->angle_time = 0;\n    leader->speed_time = 0;\n    p->piranha_hit_wait = 0;\n}\n\nstatic void M_FindCarcass(const ITEM *const shoal_item)\n{\n    M_PRIV *const p = shoal_item->priv;\n    p->carcass_item_num = Item_FindTypeInRoom(shoal_item->room_num, O_CARCASS);\n}\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    const int32_t leader_num = item->hit_points;\n    if (!M_IsValidShoalNum(leader_num)) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    if (p == nullptr) {\n        return;\n    }\n\n    if (p->leader_num != leader_num) {\n        p->leader_num = leader_num;\n        p->leader.on = false;\n    }\n    M_LEADER *const leader = &p->leader;\n    if (!leader->on) {\n        M_SetupShoal(p, leader_num);\n        M_SetupFish(p, item);\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n\n    int32_t piranha_attack = 0;\n    if (item->object_id == O_PIRAHNAS && lara_item != nullptr) {\n        if (p->carcass_item_num == NO_ITEM) {\n            M_FindCarcass(item);\n        }\n        if (p->carcass_item_num != NO_ITEM) {\n            piranha_attack = 2;\n        } else {\n            piranha_attack = lara_item->room_num == item->room_num;\n        }\n    }\n\n    if (p->piranha_hit_wait != 0) {\n        p->piranha_hit_wait--;\n    }\n\n    M_FISH *const leader_fish = &p->fish[0];\n\n    const ITEM *enemy = lara_item;\n    if (piranha_attack != 0) {\n        if (piranha_attack >= 2) {\n            enemy = Item_Get(p->carcass_item_num);\n        }\n        leader_fish->angle = M_GetFishAngle12(\n            item->pos.x + leader_fish->pos.x, item->pos.z + leader_fish->pos.z,\n            enemy->pos.x, enemy->pos.z);\n        leader->angle = leader_fish->angle;\n        leader->speed = (Random_GetControl() & 0x3F) - 64;\n    }\n\n    int32_t diff = M_GetAngle12Diff(leader_fish->angle, leader->angle);\n    if (diff > 128) {\n        leader_fish->ang_add -= 4;\n        CLAMPL(leader_fish->ang_add, -120);\n    } else if (diff < -128) {\n        leader_fish->ang_add += 4;\n        CLAMPG(leader_fish->ang_add, 120);\n    } else {\n        leader_fish->ang_add -= leader_fish->ang_add >> 2;\n        if (ABS(leader_fish->ang_add) < 4) {\n            leader_fish->ang_add = 0;\n        }\n    }\n    leader_fish->angle = (leader_fish->angle + leader_fish->ang_add) & 0xFFF;\n    if (diff > 1024) {\n        leader_fish->angle =\n            (leader_fish->angle + (leader_fish->ang_add >> 2)) & 0xFFF;\n    }\n\n    diff = (int32_t)leader_fish->speed - (int32_t)leader->speed;\n    if (diff < -4) {\n        int32_t new_speed =\n            (int32_t)leader_fish->speed + (Random_GetControl() & 3) + 1;\n        CLAMPL(new_speed, 0);\n        leader_fish->speed = new_speed;\n    } else if (diff > 4) {\n        int32_t new_speed =\n            (int32_t)leader_fish->speed - (Random_GetControl() & 3) - 1;\n        CLAMPG(new_speed, 255);\n        leader_fish->speed = new_speed;\n    }\n\n    leader_fish->swim = (leader_fish->swim + (leader_fish->speed >> 4)) & 0x3F;\n\n    const int32_t angle16 = leader_fish->angle << 4;\n    int32_t x = leader_fish->pos.x\n        - (leader_fish->speed * Math_Sin(angle16) >> (W2V_SHIFT + 1));\n    int32_t z = leader_fish->pos.z\n        + (leader_fish->speed * Math_Cos(angle16) >> (W2V_SHIFT + 1));\n\n    if (piranha_attack == 0) {\n        if (z < -leader->range.z) {\n            z = -leader->range.z;\n            if (leader_fish->angle < 2048) {\n                leader->angle =\n                    leader_fish->angle - (Random_GetControl() & 0x7F) - 128;\n            } else {\n                leader->angle =\n                    leader_fish->angle + (Random_GetControl() & 0x7F) + 128;\n            }\n            leader->angle_time = (Random_GetControl() & 0xF) + 8;\n            leader->speed_time = 0;\n        } else if (z > leader->range.z) {\n            z = leader->range.z;\n            if (leader_fish->angle > 3072) {\n                leader->angle =\n                    leader_fish->angle - (Random_GetControl() & 0x7F) - 128;\n            } else {\n                leader->angle =\n                    leader_fish->angle + (Random_GetControl() & 0x7F) + 128;\n            }\n            leader->angle_time = (Random_GetControl() & 0xF) + 8;\n            leader->speed_time = 0;\n        }\n\n        if (x < -leader->range.x) {\n            x = -leader->range.x;\n            if (leader_fish->angle < 1024) {\n                leader->angle =\n                    leader_fish->angle - (Random_GetControl() & 0x7F) - 128;\n            } else {\n                leader->angle =\n                    leader_fish->angle + (Random_GetControl() & 0x7F) + 128;\n            }\n            leader->angle_time = (Random_GetControl() & 0xF) + 8;\n            leader->speed_time = 0;\n        } else if (x > leader->range.x) {\n            x = leader->range.x;\n            if (leader_fish->angle < 3072) {\n                leader->angle =\n                    leader_fish->angle - (Random_GetControl() & 0x7F) - 128;\n            } else {\n                leader->angle =\n                    leader_fish->angle + (Random_GetControl() & 0x7F) + 128;\n            }\n            leader->angle_time = (Random_GetControl() & 0xF) + 8;\n            leader->speed_time = 0;\n        }\n\n        if ((Random_GetControl() & 0xF) == 0) {\n            leader->angle_time = 0;\n        }\n\n        if (leader->angle_time != 0) {\n            leader->angle_time--;\n        } else {\n            leader->angle_time = (Random_GetControl() & 0xF) + 8;\n            int32_t delta = (Random_GetControl() & 0x3F) - 24;\n            if ((Random_GetControl() & 3) == 0) {\n                delta *= 32;\n            }\n            leader->angle = (leader->angle + delta) & 0xFFF;\n        }\n\n        if (leader->speed_time != 0) {\n            leader->speed_time--;\n        } else {\n            leader->speed_time = (Random_GetControl() & 0x1F) + 32;\n\n            if ((Random_GetControl() & 7) == 0) {\n                leader->speed = (Random_GetControl() & 0x7F) + 128;\n            } else if ((Random_GetControl() & 3) == 0) {\n                leader->speed += (Random_GetControl() & 0x7F) + 32;\n            } else if (leader->speed > 140) {\n                leader->speed += 208 - (Random_GetControl() & 0x1F);\n            } else {\n                leader->speed_time = (Random_GetControl() & 3) + 4;\n                leader->speed += (Random_GetControl() & 0x1F) - 15;\n            }\n        }\n    }\n\n    leader_fish->pos.x = x;\n    leader_fish->pos.z = z;\n\n    for (int32_t i = 0; i < M_FISH_PER_SHOAL; i++) {\n        M_FISH *const fish = &p->fish[i + 1];\n\n        if (item->object_id == O_PIRAHNAS) {\n            const XYZ_32 fish_pos = {\n                .x = item->pos.x + fish->pos.x,\n                .y = item->pos.y + fish->pos.y,\n                .z = item->pos.z + fish->pos.z,\n            };\n            if (M_FishNearItem(&fish_pos, 256, enemy)) {\n                if (p->piranha_hit_wait == 0) {\n                    Spawn_Blood(\n                        fish_pos.x, fish_pos.y, fish_pos.z, 0, 0,\n                        enemy->room_num);\n                    p->piranha_hit_wait = 8;\n                }\n\n                if (piranha_attack != 2) {\n                    Lara_TakeDamage(4, false);\n                }\n            }\n        }\n\n        const int32_t dx = SQUARE(fish->pos.x - x - 128 * i + 3072);\n        const int32_t dz = SQUARE(fish->pos.z - z + 128 * i - 3072);\n\n        const uint16_t desired =\n            M_GetFishAngle12(fish->pos.x, fish->pos.z, x, z);\n        diff = M_GetAngle12Diff(fish->angle, desired);\n\n        if (diff > 128) {\n            fish->ang_add -= 4;\n            CLAMPL(fish->ang_add, -(i >> 1) - 92);\n        } else if (diff < -128) {\n            fish->ang_add += 4;\n            CLAMPG(fish->ang_add, (i >> 1) + 92);\n        } else {\n            fish->ang_add -= fish->ang_add >> 2;\n            if (ABS(fish->ang_add) < 4) {\n                fish->ang_add = 0;\n            }\n        }\n\n        fish->angle = (fish->angle + fish->ang_add) & 0xFFF;\n        if (diff > 1024) {\n            fish->angle = (fish->angle + (fish->ang_add >> 2)) & 0xFFF;\n        }\n\n        if (dx + dz < 16384 * SQUARE(i) + SQUARE(WALL_L)) {\n            if (fish->speed > 2 * i + 32) {\n                fish->speed -= fish->speed >> 5;\n            }\n        } else {\n            if (fish->speed < (i >> 1) + 160) {\n                fish->speed += (i >> 1) + (Random_GetControl() & 3) + 1;\n            }\n\n            if (fish->speed > (i >> 1) - 4 * i + 160) {\n                fish->speed = (i >> 1) - 4 * i - 96;\n            }\n        }\n\n        if ((Random_GetControl() & 1) != 0) {\n            fish->speed -= Random_GetControl() & 1;\n        } else {\n            fish->speed += Random_GetControl() & 1;\n        }\n\n        CLAMP(fish->speed, 32, 200);\n\n        fish->swim =\n            (fish->swim + (fish->speed >> 4) + (fish->speed >> 5)) & 0x3F;\n\n        const int32_t fish_angle16 = fish->angle << 4;\n        int32_t next_x = fish->pos.x\n            - (fish->speed * Math_Sin(fish_angle16) >> (W2V_SHIFT + 1));\n        int32_t next_z = fish->pos.z\n            + (fish->speed * Math_Cos(fish_angle16) >> (W2V_SHIFT + 1));\n        CLAMP(next_x, -32000, 32000);\n        CLAMP(next_z, -32000, 32000);\n        fish->pos.x = next_x;\n        fish->pos.z = next_z;\n\n        if (piranha_attack == 0) {\n            if (ABS(fish->pos.y - fish->dest_y) < 16) {\n                fish->dest_y = Random_GetControl() % leader->range.y;\n            }\n        } else if (ABS(fish->pos.y - fish->dest_y) < 16 && enemy != nullptr) {\n            fish->dest_y =\n                (enemy->pos.y - item->pos.y + (Random_GetControl() & 0xFF));\n        }\n\n        fish->pos.y += (fish->dest_y - fish->pos.y) >> 4;\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    if (!item->active) {\n        return false;\n    }\n\n    if (item->hit_points == NO_ITEM) {\n        return false;\n    }\n\n    const int32_t leader_num = item->hit_points;\n    if (!M_IsValidShoalNum(leader_num)) {\n        return false;\n    }\n\n    M_PRIV *const p = item->priv;\n    if (p == nullptr) {\n        return false;\n    }\n\n    if (p->leader_num != leader_num) {\n        return false;\n    }\n\n    if (!p->leader.on) {\n        return false;\n    }\n\n    const OBJECT *const explosion_obj = Object_Get(O_EXPLOSION_1);\n    if (explosion_obj == nullptr || !explosion_obj->loaded) {\n        return false;\n    }\n\n    int32_t sprite_idx = explosion_obj->mesh_idx;\n    if (item->object_id == O_PIRAHNAS) {\n        sprite_idx += 10;\n    } else {\n        sprite_idx += 11;\n    }\n\n    if (sprite_idx < 0 || sprite_idx >= Output_GetSpriteTextureCount()) {\n        return false;\n    }\n\n    const XYZ_32 base_pos = item->interp.result.pos;\n    const double ratio = Interpolation_GetWorldRate();\n    const bool do_interp =\n        Interpolation_IsActive() && ratio > 0.0 && ratio < 1.0;\n\n    M_FISH *fish = &p->fish[1];\n\n    for (int32_t i = 0; i < M_FISH_PER_SHOAL; i++, fish++) {\n        if (do_interp) {\n            fish->interp.result.pos.x = (int16_t)LERP(\n                (int32_t)fish->interp.prev.pos.x, (int32_t)fish->pos.x, ratio);\n            fish->interp.result.pos.y = (int16_t)LERP(\n                (int32_t)fish->interp.prev.pos.y, (int32_t)fish->pos.y, ratio);\n            fish->interp.result.pos.z = (int16_t)LERP(\n                (int32_t)fish->interp.prev.pos.z, (int32_t)fish->pos.z, ratio);\n\n            fish->interp.result.angle =\n                (Math_AngleMean(\n                     fish->interp.prev.angle << 4, fish->angle << 4, ratio)\n                 >> 4)\n                & 0xFFF;\n\n            int32_t swim_diff =\n                (int32_t)fish->swim - (int32_t)fish->interp.prev.swim;\n            if (swim_diff > 32) {\n                swim_diff -= 64;\n            } else if (swim_diff < -32) {\n                swim_diff += 64;\n            }\n\n            int32_t swim_interp = LERP(\n                (int32_t)fish->interp.prev.swim,\n                (int32_t)fish->interp.prev.swim + swim_diff, ratio);\n            swim_interp %= 64;\n            if (swim_interp < 0) {\n                swim_interp += 64;\n            }\n            fish->interp.result.swim = swim_interp;\n        } else {\n            fish->interp.result.pos = fish->pos;\n            fish->interp.result.angle = fish->angle;\n            fish->interp.result.swim = fish->swim;\n        }\n\n        const int32_t x = base_pos.x + fish->interp.result.pos.x;\n        const int32_t y = base_pos.y + fish->interp.result.pos.y;\n        const int32_t z = base_pos.z + fish->interp.result.pos.z;\n\n        const int32_t swim_ang16 = fish->interp.result.swim << 10;\n        const int32_t swim_wibble = Math_Sin(swim_ang16) >> 7;\n        const int32_t ang12 =\n            (swim_wibble + fish->interp.result.angle - 2048) & 0xFFF;\n\n        const int32_t size = ((128 * Math_Sin(i << 10)) >> W2V_SHIFT) + 192;\n        const int32_t ang16 = ang12 << 4;\n        const int32_t back_x = x - ((size * Math_Sin(ang16)) >> W2V_SHIFT);\n        const int32_t back_z = z + ((size * Math_Cos(ang16)) >> W2V_SHIFT);\n\n        const XYZ_32 tri_world[3] = {\n            { .x = x, .y = y, .z = z },\n            { .x = back_x, .y = y - size, .z = back_z },\n            { .x = back_x, .y = y + size, .z = back_z },\n        };\n\n        int32_t shade = ang12;\n        if (shade < 1024) {\n            shade -= 512;\n        } else if (shade < 2048) {\n            shade -= 1536;\n        } else if (shade < 3072) {\n            shade -= 2560;\n        } else {\n            shade -= 3584;\n        }\n\n        if (shade > 512 || shade < 0) {\n            shade = 0;\n        } else if (shade < 256) {\n            shade >>= 2;\n        } else {\n            shade = (512 - shade) >> 2;\n        }\n\n        shade += i;\n        if (shade > 128) {\n            shade = 128;\n        }\n\n        shade += 80;\n        CLAMP(shade, 0, 255);\n\n        const RGBA_8888 color = { shade, shade, shade, 255 };\n        const RGBA_8888 tri_color[3] = { color, color, color };\n\n        // OG flips the UV mapping depending on the shoal number (tropical\n        // fish) or the fish index (piranhas).\n        bool use_default_uv = false;\n        if (item->object_id == O_PIRAHNAS) {\n            use_default_uv = (i & 1) != 0;\n        } else {\n            use_default_uv = (leader_num & 1) != 0;\n        }\n\n        if (use_default_uv) {\n            OutputSource_PolyFX_StageSpriteTriWorld(\n                sprite_idx, tri_world, tri_color, DRAW_BLEND);\n        } else {\n            const int32_t sprite_corners[3] = { 2, 3, 1 }; // u2v2, u1v2, u2v1\n            OUTPUT_UVW uvw[3];\n            OUTPUT_TEXTURE_SIZE texture_size[3];\n            for (int32_t j = 0; j < 3; j++) {\n                const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex(\n                    sprite_idx, sprite_corners[j]);\n                uvw[j] = Output_Textures_GetUVW(uvw_idx);\n                texture_size[j] = Output_Textures_GetAtlasSize(uvw_idx / 4);\n            }\n\n            OutputSource_PolyFX_StageTriExtUV(\n                tri_world, uvw, texture_size, nullptr, tri_color,\n                VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND);\n        }\n    }\n\n    if (Interpolation_IsActive() && ratio >= 1.0) {\n        for (int32_t i = 0; i < M_FISH_PER_SHOAL + 1; i++) {\n            p->fish[i].interp.prev.pos = p->fish[i].pos;\n            p->fish[i].interp.prev.angle = p->fish[i].angle;\n            p->fish[i].interp.prev.swim = p->fish[i].swim;\n        }\n    }\n\n    return true;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->enable_shadow = false;\n    item->collidable = false;\n\n    if (item->priv == nullptr) {\n        item->priv = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA);\n    }\n\n    M_PRIV *const p = item->priv;\n    p->leader.on = false;\n    p->leader_num = NO_ITEM;\n    p->piranha_hit_wait = 0;\n    p->carcass_item_num = NO_ITEM;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->draw_func = M_Draw;\n    obj->hit_points = NO_ITEM;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n}\n\nvoid Shoal_TriggerActivate(ITEM *const item, const int16_t trigger_timer)\n{\n    // TO_OBJECT handling stores (timer & 7) in hit_points and clears timer so\n    // it does not count down as a trigger delay.\n    const int32_t leader_num = trigger_timer & 7;\n    item->hit_points = leader_num;\n    item->timer = 0;\n\n    if (M_IsValidShoalNum(leader_num) && item->priv != nullptr) {\n        M_PRIV *const p = item->priv;\n        p->leader_num = leader_num;\n        M_SetupShoal(p, leader_num);\n    }\n}\n\nvoid Shoal_TriggerDeactivate(const ITEM *const item)\n{\n    // Anti-trigger turns the leader off to force a re-setup.\n    const int32_t leader_num = item->hit_points;\n    if (M_IsValidShoalNum(leader_num) && item->priv != nullptr) {\n        M_PRIV *const p = item->priv;\n        p->leader.on = false;\n    }\n}\n\nREGISTER_OBJECT(O_TROPICAL_FISH, M_Setup)\nREGISTER_OBJECT(O_PIRAHNAS, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/shoal.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nvoid Shoal_TriggerActivate(ITEM *item, int16_t trigger_timer);\nvoid Shoal_TriggerDeactivate(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/objects/general/smashable.c",
    "content": "#include <trx/game/objects/general/smashable.h>\n\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\nstatic void M_SetBoxBlocked(const ITEM *const item, const bool blocked)\n{\n    const ROOM *const room = Room_Get(item->room_num);\n    const SECTOR *const sector =\n        Room_GetWorldSector(room, item->pos.x, item->pos.z);\n    BOX_INFO *const box = Box_GetBox(sector->box);\n\n    if (blocked && (box->overlap_index & BOX_BLOCKABLE) != 0) {\n        box->overlap_index |= BOX_BLOCKED;\n    } else if (!blocked && (box->overlap_index & BOX_BLOCKED) != 0) {\n        box->overlap_index &= ~BOX_BLOCKED;\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->flags = 0;\n    item->mesh_bits = 1;\n    M_SetBoxBlocked(item, true);\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if ((item->flags & IF_ONE_SHOT) != 0) {\n            item->mesh_bits = 0x100;\n            M_SetBoxBlocked(item, false);\n        }\n    }\n}\n\nstatic void M_Control1(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_ONE_SHOT) != 0) {\n        return;\n    }\n\n    if (Lara_Vehicle_IsMounted()) {\n        if (Lara_IsNearItem(&item->pos, 512)) {\n            Smashable_Smash(item_num);\n        }\n    } else if (item->touch_bits) {\n        item->touch_bits = 0;\n        const ITEM *const lara_item = Lara_GetItem();\n        const int32_t speed =\n            ABS((lara_item->speed * Math_Cos(lara_item->rot.y - item->rot.y))\n                >> W2V_SHIFT);\n        if (speed >= 50) {\n            Smashable_Smash(item_num);\n        }\n    }\n}\n\nstatic void M_Control2(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if ((item->flags & IF_ONE_SHOT) != 0) {\n        return;\n    }\n\n    M_SetBoxBlocked(item, false);\n\n    item->mesh_bits = ~1;\n    item->collidable = false;\n    Item_Explode(item_num, 65278, 0);\n\n    if (item->object_id == O_SMASH_OBJECT_2) {\n        Sound_Effect(SFX_BRITTLE_GROUND_BREAK, &item->pos, SPM_NORMAL);\n    } else if (item->object_id == O_SMASH_OBJECT_3) {\n        Sound_Effect(SFX_EXPLOSION_1, &item->pos, SPM_NORMAL);\n        Sound_Effect(SFX_EXPLOSION_2, &item->pos, SPM_NORMAL);\n    }\n\n    item->flags |= IF_ONE_SHOT;\n    item->status = IS_DEACTIVATED;\n    Item_RemoveActive(item_num);\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_Setup1(OBJECT *const obj)\n{\n    M_SetupBase(obj);\n    obj->control_func = M_Control1;\n}\n\nstatic void M_Setup2(OBJECT *const obj)\n{\n    M_SetupBase(obj);\n    obj->control_func = M_Control2;\n}\n\nvoid Smashable_Smash(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_SetBoxBlocked(item, false);\n\n    item->collidable = false;\n    item->mesh_bits = ~1;\n    Item_Explode(item_num, 0b11111110'11111110, 0);\n\n    if (item->object_id == O_SMASH_OBJECT_1) {\n        Sound_Effect(SFX_GLASS_BREAK, &item->pos, SPM_NORMAL);\n    } else if (item->object_id == O_SMASH_OBJECT_4) {\n        Sound_Effect(SFX_SHUTTERS_BREAK, &item->pos, SPM_NORMAL);\n    }\n\n    item->flags |= IF_ONE_SHOT;\n    if (item->status == IS_ACTIVE) {\n        Item_RemoveActive(item_num);\n    }\n    item->status = IS_DEACTIVATED;\n}\n\nREGISTER_OBJECT(O_SMASH_OBJECT_1, M_Setup1)\nREGISTER_OBJECT(O_SMASH_OBJECT_2, M_Setup2)\nREGISTER_OBJECT(O_SMASH_OBJECT_3, M_Setup2)\nREGISTER_OBJECT(O_SMASH_OBJECT_4, M_Setup1)\n"
  },
  {
    "path": "src/trx/game/objects/general/smashable.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid Smashable_Smash(int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/objects/general/smoke_emitter.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sparks.h>\n\n#define M_DISTANCE (16 * WALL_L)\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    ITEM *const lara_item = Lara_GetItem();\n\n    int32_t time = Output_GetTimeInGame();\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    if ((time % 4) || (item->object_id == O_STEAM_EMITTER && (time % 8))) {\n        return;\n    }\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if (dx < -M_DISTANCE || dx > M_DISTANCE || dz < -M_DISTANCE\n        || dz > M_DISTANCE) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = 32;\n    spark->dst_color.b = 32;\n\n    spark->fade_to_black = 64;\n    spark->col_fade_speed = (Random_GetControl() & 7) + 16;\n    spark->life = (Random_GetControl() & 0xF) + 96;\n    spark->s_life = spark->life;\n\n    if (item->object_id == O_SMOKE_EMITTER_BLACK) {\n        spark->draw_type = DRAW_BLEND_SUB;\n    } else {\n        spark->draw_type = DRAW_BLEND_ADD;\n    }\n\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = item->pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = item->pos.y + (Random_GetControl() & 0xF) - 8;\n    spark->pos.z = item->pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 4;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -4 - (Random_GetControl() & 7);\n        } else {\n            spark->rot_add = (Random_GetControl() & 7) + 4;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->gravity = -8 - (Random_GetControl() & 7);\n    spark->max_y_vel = -4 - (Random_GetControl() & 7);\n    spark->dst_size.width = (Random_GetControl() & 0x1F) + 128;\n    spark->dst_size.height =\n        spark->dst_size.width + (Random_GetControl() & 0x1F) + 32;\n    spark->size.width = spark->dst_size.width >> 2;\n    spark->size.height = spark->dst_size.height >> 2;\n    spark->src_size.width = spark->size.width;\n    spark->src_size.height = spark->size.height >> 2;\n\n    if (item->object_id == O_STEAM_EMITTER) {\n        spark->gravity >>= 1;\n        spark->vel.y >>= 1;\n        spark->max_y_vel >>= 1;\n        spark->dst_color.r = 24;\n        spark->dst_color.g = 24;\n        spark->dst_color.b = 24;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_SMOKE_EMITTER_WHITE, M_Setup)\nREGISTER_OBJECT(O_SMOKE_EMITTER_BLACK, M_Setup)\nREGISTER_OBJECT(O_STEAM_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/sphere_of_doom.c",
    "content": "#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/sound.h>\n\n#define SPHERE_OF_DOOM_RADIUS (STEP_L * 5 / 2) // = 640\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (Room_Get(lara_item->room_num)->flags.underwater) {\n        return;\n    }\n\n    const ITEM *const item = Item_Get(item_num);\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t radius = (SPHERE_OF_DOOM_RADIUS * item->timer) >> 8;\n\n    if (SQUARE(dx) + SQUARE(dz) >= SQUARE(radius)) {\n        return;\n    }\n\n    const int16_t angle = Math_Atan(dz, dx);\n    const int16_t diff = lara_item->rot.y - angle;\n    if (ABS(diff) < DEG_90) {\n        lara_item->speed = 150;\n        lara_item->rot.y = angle;\n    } else {\n        lara_item->speed = -150;\n        lara_item->rot.y = angle + DEG_180;\n    }\n\n    lara_item->gravity = true;\n    lara_item->fall_speed = -50;\n    lara_item->pos.x =\n        item->pos.x + (((radius + 50) * Math_Sin(angle)) >> W2V_SHIFT);\n    lara_item->pos.z =\n        item->pos.z + (((radius + 50) * Math_Cos(angle)) >> W2V_SHIFT);\n    lara_item->rot.x = 0;\n    lara_item->rot.z = 0;\n    Item_SwitchToAnim(lara_item, LA(LA_FALL_START), 0);\n    lara_item->current_anim_state = LS(LS_JUMP_FORWARD);\n    lara_item->goal_anim_state = LS(LS_JUMP_FORWARD);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->timer += 64;\n    item->rot.y +=\n        item->object_id == O_SPHERE_OF_DOOM_2 ? DEG_1 * 10 : -DEG_1 * 10;\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = item->pos.x - lara_item->pos.x;\n    const int32_t dy = item->pos.y - lara_item->pos.y;\n    const int32_t dz = item->pos.z - lara_item->pos.z;\n    const int32_t radius = (SPHERE_OF_DOOM_RADIUS * item->timer) >> 8;\n    const int32_t dist = Math_Sqrt(SQUARE(dx) + SQUARE(dy) + SQUARE(dz));\n    XYZ_32 pos = lara_item->pos;\n    pos.x += ((dist - radius) * dx) / radius;\n    pos.y += ((dist - radius) * dy) / radius;\n    pos.z += ((dist - radius) * dz) / radius;\n    Sound_Effect(SFX_MARCO_BARTOLLI_TRANSFORM, &pos, SPM_NORMAL);\n    if (item->timer > 60 * 64) {\n        Item_Kill(item_num);\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    const int32_t radius = item->timer << 6;\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_RotY(item->interp.result.rot.y);\n\n    MATRIX *mptr = g_WMatrixPtr;\n    mptr->_00 = ((int64_t)mptr->_00 * radius) >> W2V_SHIFT;\n    mptr->_01 = ((int64_t)mptr->_01 * radius) >> W2V_SHIFT;\n    mptr->_02 = ((int64_t)mptr->_02 * radius) >> W2V_SHIFT;\n    mptr->_10 = ((int64_t)mptr->_10 * radius) >> W2V_SHIFT;\n    mptr->_11 = ((int64_t)mptr->_11 * radius) >> W2V_SHIFT;\n    mptr->_12 = ((int64_t)mptr->_12 * radius) >> W2V_SHIFT;\n    mptr->_20 = ((int64_t)mptr->_20 * radius) >> W2V_SHIFT;\n    mptr->_21 = ((int64_t)mptr->_21 * radius) >> W2V_SHIFT;\n    mptr->_22 = ((int64_t)mptr->_22 * radius) >> W2V_SHIFT;\n\n    mptr = g_MatrixPtr;\n    mptr->_00 = ((int64_t)mptr->_00 * radius) >> W2V_SHIFT;\n    mptr->_01 = ((int64_t)mptr->_01 * radius) >> W2V_SHIFT;\n    mptr->_02 = ((int64_t)mptr->_02 * radius) >> W2V_SHIFT;\n    mptr->_10 = ((int64_t)mptr->_10 * radius) >> W2V_SHIFT;\n    mptr->_11 = ((int64_t)mptr->_11 * radius) >> W2V_SHIFT;\n    mptr->_12 = ((int64_t)mptr->_12 * radius) >> W2V_SHIFT;\n    mptr->_20 = ((int64_t)mptr->_20 * radius) >> W2V_SHIFT;\n    mptr->_21 = ((int64_t)mptr->_21 * radius) >> W2V_SHIFT;\n    mptr->_22 = ((int64_t)mptr->_22 * radius) >> W2V_SHIFT;\n\n    const ANIM_FRAME *const frame_ptr = Item_GetAnim(item)->frame_ptr;\n    const CLIP clip = Output_CheckBoundsClip(&frame_ptr->bounds);\n    if (clip != CLIP_NOT_VISIBLE) {\n        Output_CalculateObjectLighting(item, &frame_ptr->bounds);\n        Object_DrawMesh(Object_Get(item->object_id)->mesh_idx, clip, false);\n    }\n\n    Matrix_Pop();\n    return true;\n}\n\nstatic void M_SetupBase(OBJECT *const obj, const bool transparent)\n{\n    obj->collision_func = M_Collision;\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->semi_transparent = transparent;\n}\n\nstatic void M_SetupTransparent(OBJECT *const obj)\n{\n    M_SetupBase(obj, true);\n}\n\nstatic void M_SetupOpaque(OBJECT *const obj)\n{\n    M_SetupBase(obj, false);\n}\n\nREGISTER_OBJECT(O_SPHERE_OF_DOOM_1, M_SetupTransparent)\nREGISTER_OBJECT(O_SPHERE_OF_DOOM_2, M_SetupTransparent)\nREGISTER_OBJECT(O_SPHERE_OF_DOOM_3, M_SetupOpaque)\n"
  },
  {
    "path": "src/trx/game/objects/general/switch.c",
    "content": "#include <trx/game/objects/general/switch.h>\n\n#include <trx/config.h>\n#include <trx/game/const.h>\n#include <trx/game/input.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/version.h>\n\ntypedef struct {\n    XYZ_32 normal;\n    XYZ_32 controlled;\n} M_SWITCH_POS;\n\nstatic const OBJECT_BOUNDS m_SwitchBounds = {\n    .shift = {\n        .min = { .x = -220, .y = +0, .z = +WALL_L / 2 - 220, },\n        .max = { .x = +220, .y = +0, .z = +WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, },\n        .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, },\n    },\n};\n\nstatic const OBJECT_BOUNDS m_SwitchBoundsControlled = {\n    .shift = {\n        .min = { .x = -WALL_L / 2, .y = +0, .z = -200, },\n        .max = { .x = +WALL_L / 2, .y = +0, .z = +200, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, },\n        .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, },\n    },\n};\n\nstatic const OBJECT_BOUNDS m_SwitchBoundsUW = {\n    .shift = {\n        .min = { .x = -WALL_L, .y = -WALL_L, .z = -WALL_L, },\n        .max = { .x = +WALL_L, .y = +WALL_L, .z = +WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = -80 * DEG_1, .y = -80 * DEG_1, .z = -80 * DEG_1, },\n        .max = { .x = +80 * DEG_1, .y = +80 * DEG_1, .z = +80 * DEG_1, },\n    },\n};\n\nstatic const XYZ_32 m_SwitchUWPosition = { .x = 0, .y = 0, .z = 108 };\n\nstatic const M_SWITCH_POS m_SmallSwitchPosition = {\n    .normal = { .x = 0, .y = 0, .z = 362 },\n    .controlled = { .x = 0, .y = 0, .z = 80 },\n};\n\nstatic const M_SWITCH_POS m_PushSwitchPosition = {\n    .normal = { .x = 0, .y = 0, .z = 292 },\n    .controlled = { .x = 0, .y = 0, .z = 146 },\n};\n\nstatic const M_SWITCH_POS m_WallSwitchPosition = {\n    .normal = { .x = 0, .y = 0, .z = 128 },\n    .controlled = { .x = 0, .y = 0, .z = 64 },\n};\n\nstatic const M_SWITCH_POS m_AirlockPosition = {\n    .normal = { .x = 0, .y = 0, .z = 212 },\n    .controlled = { .x = 0, .y = 0, .z = 106 },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return g_Config.gameplay.enable_walk_to_items ? &m_SwitchBoundsControlled\n                                                  : &m_SwitchBounds;\n}\n\nstatic const OBJECT_BOUNDS *M_BoundsUW(void)\n{\n    return &m_SwitchBoundsUW;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->flags |= IF_CODE_BITS;\n    if (!Item_IsTriggerActive(item)) {\n        item->goal_anim_state = SWITCH_STATE_OFF;\n        item->timer = 0;\n    }\n    Item_Animate(item);\n\n    if (g_TRVersion >= 3 && (item->flags & IF_ONE_SHOT_SWITCH) != 0) {\n        item->flags &= ~IF_ONE_SHOT_SWITCH;\n        item->flags |= IF_ONE_SHOT;\n    }\n}\n\nstatic void M_AlignLara(ITEM *const lara_item, ITEM *const switch_item)\n{\n    lara_item->rot.y = switch_item->rot.y;\n    switch (switch_item->object_id) {\n    case O_SWITCH_TYPE_AIRLOCK:\n    case O_SWITCH_TYPE_WHEEL:\n        Lara_AlignPosition(switch_item, &m_AirlockPosition.normal);\n        break;\n\n    case O_SWITCH_TYPE_SMALL:\n        Lara_AlignPosition(switch_item, &m_SmallSwitchPosition.normal);\n        break;\n\n    case O_SWITCH_TYPE_BUTTON:\n        Lara_AlignPosition(switch_item, &m_PushSwitchPosition.normal);\n        break;\n\n    default:\n        break;\n    }\n}\n\nstatic bool M_MoveLaraControlled(\n    const ITEM *const item, const BOUNDS_16 *const bounds)\n{\n    XYZ_32 shift;\n    switch (item->object_id) {\n    case O_SWITCH_TYPE_AIRLOCK:\n    case O_SWITCH_TYPE_WHEEL:\n        shift = m_AirlockPosition.controlled;\n        break;\n    case O_SWITCH_TYPE_SMALL:\n        shift = m_SmallSwitchPosition.controlled;\n        break;\n    case O_SWITCH_TYPE_BUTTON:\n        shift = m_PushSwitchPosition.controlled;\n        break;\n    default:\n        shift = m_WallSwitchPosition.controlled;\n        break;\n    }\n\n    const XYZ_32 move_vector = {\n        .x = 0,\n        .y = 0,\n        .z = bounds->min.z - shift.z,\n    };\n    return Lara_MovePosition(item, &move_vector);\n}\n\nstatic void M_TurnSwitchOn(ITEM *const switch_item, ITEM *const lara_item)\n{\n    switch (switch_item->object_id) {\n    case O_SWITCH_TYPE_WHEEL:\n        Lara_SwitchToExtraState(LS_EXTRA_AIRLOCK);\n        break;\n    case O_SWITCH_TYPE_SMALL:\n        Item_SwitchToAnim(lara_item, LA(LA_SWITCH_SMALL_DOWN), 0);\n        break;\n\n    case O_SWITCH_TYPE_BUTTON:\n        Item_SwitchToAnim(lara_item, LA(LA_BUTTON_PUSH), 0);\n        break;\n\n    default:\n        Item_SwitchToAnim(lara_item, LA(LA_WALL_SWITCH_DOWN), 0);\n        break;\n    }\n\n    if (!Lara_GetLaraInfo()->extra_anim) {\n        lara_item->current_anim_state = LS(LS_SWITCH_ON);\n    }\n    switch_item->goal_anim_state = SWITCH_STATE_ON;\n}\n\nstatic void M_TurnSwitchOff(ITEM *const switch_item, ITEM *const lara_item)\n{\n    lara_item->current_anim_state = LS(LS_SWITCH_OFF);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    switch (switch_item->object_id) {\n    case O_SWITCH_TYPE_AIRLOCK:\n    case O_SWITCH_TYPE_WHEEL:\n        Lara_SwitchToExtraState(LS_EXTRA_AIRLOCK);\n        break;\n\n    case O_SWITCH_TYPE_SMALL:\n        Item_SwitchToAnim(lara_item, LA(LA_SWITCH_SMALL_UP), 0);\n        break;\n\n    case O_SWITCH_TYPE_BUTTON:\n        Item_SwitchToAnim(lara_item, LA(LA_BUTTON_PUSH), 0);\n        break;\n\n    default:\n        Item_SwitchToAnim(lara_item, LA(LA_WALL_SWITCH_UP), 0);\n        break;\n    }\n\n    switch_item->goal_anim_state = SWITCH_STATE_OFF;\n}\n\nstatic void M_CollisionControlled(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if ((g_Input.action && lara->gun_status == LGS_ARMLESS\n         && !lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP)\n         && item->status == IS_INACTIVE)\n        || (lara->interact_target.is_moving\n            && lara->interact_target.item_num == item_num)) {\n        const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item);\n\n        OBJECT_BOUNDS col_bounds = *Object_Get(item->object_id)->bounds_func();\n        col_bounds.shift.min.x += bounds->min.x;\n        col_bounds.shift.max.x += bounds->max.x;\n        col_bounds.shift.min.z += bounds->min.z;\n        col_bounds.shift.max.z += bounds->max.z;\n\n        if (Lara_TestPosition(item, &col_bounds)) {\n            if (M_MoveLaraControlled(item, bounds)) {\n                if (item->current_anim_state == SWITCH_STATE_OFF) {\n                    M_TurnSwitchOn(item, lara_item);\n                } else {\n                    M_TurnSwitchOff(item, lara_item);\n                }\n                lara->head_rot.x = 0;\n                lara->head_rot.y = 0;\n                lara->torso_rot.x = 0;\n                lara->torso_rot.y = 0;\n                lara->interact_target.is_moving = false;\n                lara->interact_target.item_num = NO_ITEM;\n                lara->gun_status = LGS_HANDS_BUSY;\n                Item_AddActive(item_num);\n                item->status = IS_ACTIVE;\n                Item_Animate(item);\n            } else {\n                lara->interact_target.item_num = item_num;\n            }\n        } else if (\n            lara->interact_target.is_moving\n            && lara->interact_target.item_num == item_num) {\n            lara->interact_target.is_moving = false;\n            lara->gun_status = LGS_ARMLESS;\n        }\n    } else if (\n        lara_item->current_anim_state != LS(LS_SWITCH_ON)\n        && lara_item->current_anim_state != LS(LS_SWITCH_OFF)) {\n        Object_Collision(item_num, lara_item, coll);\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (g_TRVersion >= 3 && (item->flags & IF_ONE_SHOT) != 0) {\n        return;\n    }\n\n    if (g_Config.gameplay.enable_walk_to_items) {\n        M_CollisionControlled(item_num, lara_item, coll);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (!g_Input.action || item->status != IS_INACTIVE\n        || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)\n        || !Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    if (item->object_id == O_SWITCH_TYPE_AIRLOCK\n        && item->current_anim_state == SWITCH_STATE_OFF) {\n        return;\n    }\n\n    M_AlignLara(lara_item, item);\n\n    if (item->current_anim_state == SWITCH_STATE_OFF) {\n        M_TurnSwitchOn(item, lara_item);\n    } else {\n        M_TurnSwitchOff(item, lara_item);\n    }\n\n    if (!lara->extra_anim) {\n        lara_item->goal_anim_state = LS(LS_STOP);\n    }\n    lara->gun_status = LGS_HANDS_BUSY;\n\n    item->status = IS_ACTIVE;\n    Item_AddActive(item_num);\n    Item_Animate(item);\n}\n\nstatic void M_CollisionUW(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (!g_Input.action || item->status != IS_INACTIVE\n        || (lara->water_status != LWS_UNDERWATER\n            && lara->water_status != LWS_CHEAT)\n        || lara->gun_status != LGS_ARMLESS\n        || lara_item->current_anim_state != LS(LS_TREAD)) {\n        return;\n    }\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    if (item->current_anim_state != SWITCH_STATE_ON\n        && item->current_anim_state != SWITCH_STATE_OFF) {\n        return;\n    }\n\n    if (!Lara_MovePosition(item, &m_SwitchUWPosition)) {\n        return;\n    }\n\n    lara_item->fall_speed = 0;\n    Lara_AnimateUntil(lara_item, LS(LS_SWITCH_ON));\n    lara_item->goal_anim_state = LS(LS_TREAD);\n    lara->gun_status = LGS_HANDS_BUSY;\n\n    if (item->current_anim_state == SWITCH_STATE_OFF) {\n        item->goal_anim_state = SWITCH_STATE_ON;\n    } else {\n        item->goal_anim_state = SWITCH_STATE_OFF;\n    }\n    item->status = IS_ACTIVE;\n    Item_AddActive(item_num);\n    Item_Animate(item);\n}\n\nstatic void M_SetupBase(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SetupCommon(OBJECT *const obj)\n{\n    M_SetupBase(obj);\n    obj->collision_func = M_Collision;\n    obj->bounds_func = M_Bounds;\n}\n\nstatic void M_SetupPushButton(OBJECT *const obj)\n{\n    M_SetupCommon(obj);\n    obj->enable_interpolation = false;\n    obj->bounds_func = M_Bounds;\n}\n\nstatic void M_SetupUW(OBJECT *const obj)\n{\n    M_SetupBase(obj);\n    obj->collision_func = M_CollisionUW;\n    obj->bounds_func = M_BoundsUW;\n}\n\nstatic void M_SetupAirlock(OBJECT *const obj)\n{\n    M_SetupCommon(obj);\n    obj->draw_func = Object_DrawUnclippedItem;\n}\n\nbool Switch_Trigger(const int16_t item_num, const int16_t timer)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->object_id == O_SWITCH_TYPE_AIRLOCK) {\n        if (item->status == IS_DEACTIVATED) {\n            Item_RemoveActive(item_num);\n            item->status = IS_INACTIVE;\n            return false;\n        } else if (\n            (item->flags & IF_ONE_SHOT) != 0\n            || item->current_anim_state == SWITCH_STATE_ON) {\n            return false;\n        }\n\n        item->flags |= IF_ONE_SHOT;\n        return true;\n    }\n\n    if (item->status != IS_DEACTIVATED) {\n        return false;\n    }\n\n    if (item->current_anim_state == SWITCH_STATE_ON && timer > 0) {\n        item->timer = timer;\n        if (timer != 1) {\n            item->timer *= LOGIC_FPS;\n        }\n        item->status = IS_ACTIVE;\n    } else {\n        Item_RemoveActive(item_num);\n        item->status = IS_INACTIVE;\n    }\n    return true;\n}\n\nREGISTER_OBJECT(O_SWITCH_TYPE_AIRLOCK, M_SetupAirlock)\nREGISTER_OBJECT(O_SWITCH_TYPE_BUTTON, M_SetupPushButton)\nREGISTER_OBJECT(O_SWITCH_TYPE_NORMAL, M_SetupCommon)\nREGISTER_OBJECT(O_SWITCH_TYPE_SMALL, M_SetupCommon)\nREGISTER_OBJECT(O_SWITCH_TYPE_UW, M_SetupUW)\nREGISTER_OBJECT(O_SWITCH_TYPE_WHEEL, M_SetupCommon)\n"
  },
  {
    "path": "src/trx/game/objects/general/switch.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef enum {\n    SWITCH_STATE_ON = 0,\n    SWITCH_STATE_OFF = 1,\n    SWITCH_STATE_LINK = 2,\n} SWITCH_STATE;\n\nbool Switch_Trigger(int16_t item_num, int16_t timer);\n"
  },
  {
    "path": "src/trx/game/objects/general/trapdoor.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/movable_block.h>\n\ntypedef enum {\n    TRAPDOOR_STATE_CLOSED,\n    TRAPDOOR_STATE_OPEN,\n} TRAPDOOR_STATE;\n\ntypedef enum {\n    TRAPDOOR_ANIM_CLOSED = 0,\n} TRAPDOOR_ANIM;\n\nstatic bool M_IsItemOnTop(\n    const ITEM *const item, const int32_t x, const int32_t z)\n{\n    const BOUNDS_16 *const orig_bounds = &Item_GetBestFrame(item)->bounds;\n    if (orig_bounds == nullptr) {\n        return false;\n    }\n\n    BOUNDS_16 fixed_bounds = {};\n\n    // Bounds need to change in order to account for 2 sector trapdoors\n    // and the trapdoor angle.\n    if (item->rot.y == 0) {\n        fixed_bounds.min.x = orig_bounds->min.x;\n        fixed_bounds.max.x = orig_bounds->max.x;\n        fixed_bounds.min.z = orig_bounds->min.z;\n        fixed_bounds.max.z = orig_bounds->max.z;\n    } else if (item->rot.y == DEG_90) {\n        fixed_bounds.min.x = orig_bounds->min.z;\n        fixed_bounds.max.x = orig_bounds->max.z;\n        fixed_bounds.min.z = -orig_bounds->max.x;\n        fixed_bounds.max.z = -orig_bounds->min.x;\n    } else if (item->rot.y == -DEG_180) {\n        fixed_bounds.min.x = -orig_bounds->max.x;\n        fixed_bounds.max.x = -orig_bounds->min.x;\n        fixed_bounds.min.z = -orig_bounds->max.z;\n        fixed_bounds.max.z = -orig_bounds->min.z;\n    } else if (item->rot.y == -DEG_90) {\n        fixed_bounds.min.x = -orig_bounds->max.z;\n        fixed_bounds.max.x = -orig_bounds->min.z;\n        fixed_bounds.min.z = orig_bounds->min.x;\n        fixed_bounds.max.z = orig_bounds->max.x;\n    }\n\n    if (x <= item->pos.x + fixed_bounds.max.x\n        && x >= item->pos.x + fixed_bounds.min.x\n        && z <= item->pos.z + fixed_bounds.max.z\n        && z >= item->pos.z + fixed_bounds.min.z) {\n        return true;\n    }\n\n    return false;\n}\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    } else if (item->current_anim_state != TRAPDOOR_STATE_CLOSED) {\n        return height;\n    } else if (y > item->pos.y || item->pos.y > height) {\n        return height;\n    } else {\n        return item->pos.y;\n    }\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    } else if (item->current_anim_state != TRAPDOOR_STATE_CLOSED) {\n        return height;\n    } else if (y <= item->pos.y || item->pos.y <= height) {\n        return height;\n    } else {\n        return item->pos.y + STEP_L;\n    }\n}\n\nstatic BOUNDS_16 M_RotateBounds(const BOUNDS_16 bounds, int16_t rot_y)\n{\n    BOUNDS_16 rot_bounds = {};\n\n    switch (rot_y) {\n    case 0:\n    default:\n        rot_bounds = bounds;\n        break;\n    case DEG_90:\n        rot_bounds.min.x = bounds.min.z;\n        rot_bounds.max.x = bounds.max.z;\n        rot_bounds.min.z = -bounds.max.x;\n        rot_bounds.max.z = -bounds.min.x;\n        break;\n    case -DEG_180:\n        rot_bounds.min.x = -bounds.max.x;\n        rot_bounds.max.x = -bounds.min.x;\n        rot_bounds.min.z = -bounds.max.z;\n        rot_bounds.max.z = -bounds.min.z;\n        break;\n    case -DEG_90:\n        rot_bounds.min.x = -bounds.max.z;\n        rot_bounds.max.x = -bounds.min.z;\n        rot_bounds.min.z = bounds.min.x;\n        rot_bounds.max.z = bounds.max.x;\n        break;\n    }\n    return rot_bounds;\n}\n\nstatic void M_GetSectorPositions(const ITEM *const item, VECTOR *sector_pos)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const ANIM_FRAME *const frame =\n        Object_GetAnim(obj, TRAPDOOR_ANIM_CLOSED)->frame_ptr;\n    const BOUNDS_16 rot_bounds = M_RotateBounds(frame->bounds, item->rot.y);\n\n    const int32_t x0 = item->pos.x + rot_bounds.min.x;\n    const int32_t x1 = item->pos.x + rot_bounds.max.x - 1;\n    const int32_t z0 = item->pos.z + rot_bounds.min.z;\n    const int32_t z1 = item->pos.z + rot_bounds.max.z - 1;\n\n    const int32_t sx0 = Math_FloorDiv(x0, WALL_L);\n    const int32_t sx1 = Math_FloorDiv(x1, WALL_L);\n    const int32_t sz0 = Math_FloorDiv(z0, WALL_L);\n    const int32_t sz1 = Math_FloorDiv(z1, WALL_L);\n\n    for (int32_t sx = sx0; sx <= sx1; ++sx) {\n        for (int32_t sz = sz0; sz <= sz1; ++sz) {\n            XYZ_32 pos = {\n                .x = sx * WALL_L + WALL_L / 2,\n                .y = item->pos.y,\n                .z = sz * WALL_L + WALL_L / 2,\n            };\n            Vector_Add(sector_pos, &pos);\n        }\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    VECTOR *const positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    Walkable_AllocateNodes(item, positions->count);\n    Vector_Free(positions);\n}\n\nstatic void M_DropStack(const ITEM *const item)\n{\n    VECTOR *const positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    for (int32_t i = 0; i < positions->count; i++) {\n        MovableBlock_DropStack(\n            *(const XYZ_32 *)Vector_Get(positions, i), item->room_num);\n    }\n    Vector_Free(positions);\n}\n\nstatic void M_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    VECTOR *const positions = Vector_Create(sizeof(XYZ_32));\n    M_GetSectorPositions(item, positions);\n    for (int32_t i = 0; i < positions->count; i++) {\n        Walkable_Add(item_num, *(const XYZ_32 *)Vector_Get(positions, i));\n    }\n    Vector_Free(positions);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Item_IsTriggerActive(item)) {\n        if (item->current_anim_state == TRAPDOOR_STATE_CLOSED) {\n            item->goal_anim_state = TRAPDOOR_STATE_OPEN;\n            M_DropStack(item);\n        }\n    } else {\n        if (item->current_anim_state == TRAPDOOR_STATE_OPEN) {\n            item->goal_anim_state = TRAPDOOR_STATE_CLOSED;\n        }\n    }\n    Item_Animate(item);\n    Item_UpdateRoom(item_num, item->room_num);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->save_flags = true;\n    obj->save_anim = true;\n    obj->add_walkable_func = M_AddWalkable;\n}\n\nREGISTER_OBJECT(O_TRAPDOOR_TYPE_1, M_Setup)\nREGISTER_OBJECT(O_TRAPDOOR_TYPE_2, M_Setup)\nREGISTER_OBJECT(O_TRAPDOOR_TYPE_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/trigger_gate.c",
    "content": "#include <trx/config.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n\n// clang-format off\n#define M_COLOR_ON  ((RGBA_8888) { 0, 255, 0, 255 })\n#define M_COLOR_OFF ((RGBA_8888) { 0, 255, 255, 255 })\n#define M_RADIUS    (STEP_L * 3 / 8)\n// clang-format on\n\nstatic void M_UpdateTrigger(const ITEM *const item, bool enabled)\n{\n    int16_t room_num = item->room_num;\n    SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    if (sector->trigger != nullptr) {\n        sector->trigger->enabled = enabled;\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const bool enabled = Item_IsTriggerActiveRO(item);\n    M_UpdateTrigger(item, enabled);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const bool enabled = Item_IsTriggerActive(item);\n    M_UpdateTrigger(item, enabled);\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    if (!g_Config.debug.enable_debug_triggers) {\n        return false;\n    }\n\n    const RGBA_8888 color =\n        Item_IsTriggerActiveRO(item) ? M_COLOR_ON : M_COLOR_OFF;\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->pos);\n    Output_DrawSphereEx((XYZ_16) { 0, -M_RADIUS, 0 }, M_RADIUS, color);\n    Matrix_Pop();\n\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_TRIGGER_GATE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/waterfall.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/output/state.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n#define M_RANGE (WALL_L * 10) // = 10240\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (g_TRVersion == 3) {\n        if (!Item_IsNearby(item, lara_item, M_RANGE)) {\n            return;\n        }\n\n        if ((int32_t)Output_GetTimeInGame() % 4 == 0) {\n            const XYZ_32 pos = {\n                .x = item->pos.x + ((544 * Math_Sin(item->rot.y)) >> W2V_SHIFT),\n                .y = item->pos.y,\n                .z = item->pos.z + ((544 * Math_Cos(item->rot.y)) >> W2V_SHIFT),\n            };\n            Sparks_TriggerWaterfallMist(pos.x, pos.y, pos.z, item->rot.y);\n        }\n\n        Sound_Effect(SFX_WATERFALL_LOOP, &item->pos, SPM_NORMAL);\n        return;\n    }\n\n    if (g_TRVersion >= 2 && Item_IsNearby(item, lara_item, M_RANGE)) {\n        Sound_Effect(SFX_WATERFALL_LOOP, &item->pos, SPM_NORMAL);\n    }\n\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_SPLASH_1;\n        effect->pos.x =\n            item->pos.x + ((Random_GetDraw() - 0x4000) * WALL_L) / 0x7FFF;\n        effect->pos.y = item->pos.y;\n        effect->pos.z =\n            item->pos.z + ((Random_GetDraw() - 0x4000) * WALL_L) / 0x7FFF;\n        effect->speed = 0;\n        effect->frame_num = 0;\n        effect->shade = -1;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_WATERFALL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/general/zipline.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n#define ZIPLINE_MAX_SPEED 100\n#define ZIPLINE_ACCELERATION 5\n\ntypedef struct {\n    GAME_VECTOR old_pos;\n} M_PRIV;\n\ntypedef enum {\n    ZIPLINE_STATE_EMPTY = 0,\n    ZIPLINE_STATE_GRAB = 1,\n    ZIPLINE_STATE_HANG = 2,\n} ZIPLINE_STATE;\n\nstatic XYZ_32 m_ZiplineHandlePosition = {\n    .x = 0,\n    .y = 0,\n    .z = WALL_L / 2 - 141,\n};\n\nstatic const OBJECT_BOUNDS m_ZiplineHandleBounds = {\n    .shift = {\n        .min = { .x = -WALL_L / 4, .y = -100, .z = +WALL_L / 4, },\n        .max = { .x = +WALL_L / 4, .y = +100, .z = +WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = +0, .y = -25 * DEG_1, .z = +0, },\n        .max = { .x = +0, .y = +25 * DEG_1, .z = +0, },\n    },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_ZiplineHandleBounds;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->old_pos.pos = item->pos;\n    p->old_pos.room_num = item->room_num;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_ACTIVE) {\n        return;\n    }\n\n    if (!(item->flags & IF_ONE_SHOT)) {\n        const M_PRIV *const p = item->priv;\n        item->pos = p->old_pos.pos;\n        Item_UpdateRoom(item_num, p->old_pos.room_num);\n        item->status = IS_INACTIVE;\n        item->goal_anim_state = ZIPLINE_STATE_GRAB;\n        item->current_anim_state = ZIPLINE_STATE_GRAB;\n        Item_SwitchToAnim(item, 0, 0);\n        Item_RemoveActive(item_num);\n        return;\n    }\n\n    if (item->current_anim_state == ZIPLINE_STATE_GRAB) {\n        Item_Animate(item);\n        return;\n    }\n\n    Item_Animate(item);\n    if (item->fall_speed < ZIPLINE_MAX_SPEED) {\n        item->fall_speed += ZIPLINE_ACCELERATION;\n    }\n\n    item->pos.y += item->fall_speed >> 2;\n    item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->fall_speed);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    ITEM *const lara_item = Lara_GetItem();\n    const bool lara_on_zipline =\n        lara_item->current_anim_state == LS(LS_ZIPLINE);\n    if (lara_on_zipline) {\n        lara_item->pos = item->pos;\n    }\n\n    XYZ_32 pos = item->pos;\n    pos.y += STEP_L >> 2;\n    pos = XYZ_32_OffsetYaw(pos, item->rot.y, WALL_L);\n\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    if (Room_GetHeight(sector, pos) > pos.y + STEP_L\n        && Room_GetCeiling(sector, pos) < pos.y - STEP_L) {\n        Sound_Effect(SFX_ZIPLINE_GO, &item->pos, SPM_ALWAYS);\n        return;\n    }\n\n    if (lara_on_zipline) {\n        lara_item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        Lara_Animate(lara_item);\n        lara_item->gravity = true;\n        lara_item->speed = item->fall_speed;\n        lara_item->fall_speed = item->fall_speed >> 2;\n    }\n    Sound_Effect(SFX_ZIPLINE_STOP, &item->pos, SPM_ALWAYS);\n    Item_RemoveActive(item_num);\n    item->status = IS_INACTIVE;\n    item->flags &= ~IF_ONE_SHOT;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    if (item->status != IS_INACTIVE) {\n        return;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    Lara_AlignPosition(item, &m_ZiplineHandlePosition);\n    lara->gun_status = LGS_HANDS_BUSY;\n\n    lara_item->goal_anim_state = LS(LS_ZIPLINE);\n    do {\n        Item_Animate(lara_item);\n    } while (lara_item->current_anim_state != LS(LS_PULL_UP));\n\n    if (!item->active) {\n        Item_AddActive(item_num);\n    }\n\n    item->status = IS_ACTIVE;\n    item->flags |= IF_ONE_SHOT;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->bounds_func = M_Bounds;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_ZIPLINE_HANDLE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/ids.h",
    "content": "#pragma once\n\n#define O_FIRST 0\n\ntypedef enum {\n    NO_OBJECT = -1,\n#define X_CATALOG_ID(enum_value) enum_value,\n#include <trx/game/catalog/objects.def>\n#undef X_CATALOG_ID\n    // sentinel\n    O_NUMBER_OF,\n} OBJECT_ID;\n"
  },
  {
    "path": "src/trx/game/objects/names.c",
    "content": "#include <trx/game/objects/names.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings/fuzzy_match.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/vars.h>\n\n#include <string.h>\n\ntypedef struct {\n    OBJECT_ID target_object_id;\n    OBJECT_ID source_object_id;\n} M_ALIAS;\n\ntypedef struct {\n    OBJECT_ID object_id;\n    const char *key;\n    const char **default_names;\n} M_DEFAULT;\n\ntypedef struct {\n    VECTOR *names;\n    char *description;\n    const char *slot; // stable first-name slot for this object\n} M_NAME_ENTRY;\n\nstatic M_NAME_ENTRY m_NamesTable[O_NUMBER_OF] = {};\nstatic OBJECT_ID m_AliasResolver[O_NUMBER_OF] = {};\n\n// Compile-time default names (ignoring key aliases)\nstatic const M_DEFAULT m_Defaults[] = {\n#define X_OBJ_NAMES(...) ((const char *[]) { __VA_ARGS__, nullptr })\n#define X_OBJ_NAME_DEFINE(object_id_, key_name_, names_array_)                 \\\n    { .object_id = object_id_,                                                 \\\n      .key = key_name_,                                                        \\\n      .default_names = names_array_ },\n#define X_OBJ_ALIAS_DEFINE(target_object_id, source_object_id)\n#include <trx/game/objects/names.def>\n#undef X_OBJ_ALIAS_DEFINE\n#undef X_OBJ_NAME_DEFINE\n#undef X_OBJ_NAMES\n    { .object_id = NO_OBJECT, .key = nullptr, .default_names = nullptr },\n};\n\n// Compile-time aliases (ignoring key strings and names)\nstatic M_ALIAS m_ObjectAliases[] = {\n#define X_OBJ_NAMES(...)\n#define X_OBJ_NAME_DEFINE(object_id_, key_name_, default_name)\n#define X_OBJ_ALIAS_DEFINE(target_object_id_, source_object_id_)               \\\n    { .target_object_id = target_object_id_,                                   \\\n      .source_object_id = source_object_id_ },\n#include <trx/game/objects/names.def>\n#undef X_OBJ_ALIAS_DEFINE\n#undef X_OBJ_NAME_DEFINE\n#undef X_OBJ_NAMES\n    { .target_object_id = NO_OBJECT },\n};\n\nstatic const M_DEFAULT *M_ResolveDefault(OBJECT_ID obj_id)\n{\n    obj_id = m_AliasResolver[obj_id];\n    for (int32_t i = 0; m_Defaults[i].object_id != NO_OBJECT; i++) {\n        if (m_Defaults[i].object_id == obj_id) {\n            return &m_Defaults[i];\n        }\n    }\n    return nullptr;\n}\n\nstatic M_NAME_ENTRY *M_ResolveNameEntry(const OBJECT_ID obj_id)\n{\n    return &m_NamesTable[m_AliasResolver[obj_id]];\n}\n\nstatic void M_ClearAllNames(void)\n{\n    for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) {\n        M_NAME_ENTRY *const entry = &m_NamesTable[obj_id];\n        if (entry->names != nullptr) {\n            for (int32_t i = 0; i < entry->names->count; i++) {\n                char *n = *(char **)Vector_Get(entry->names, i);\n                Memory_FreePointer(&n);\n            }\n            Vector_Free(entry->names);\n            entry->names = nullptr;\n        }\n        Memory_FreePointer(&entry->description);\n        entry->slot = nullptr;\n    }\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    M_ClearAllNames();\n}\n\nvoid Object_ClearNames(const OBJECT_ID obj_id)\n{\n    ASSERT(obj_id >= O_FIRST && obj_id < O_NUMBER_OF);\n    M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id);\n    if (entry->names != nullptr) {\n        for (int32_t i = 0; i < entry->names->count; i++) {\n            char *n = *(char **)Vector_Get(entry->names, i);\n            Memory_FreePointer(&n);\n        }\n        Vector_Clear(entry->names);\n    }\n    entry->slot = nullptr;\n}\n\nvoid Object_AddName(const OBJECT_ID obj_id, const char *const name)\n{\n    ASSERT(obj_id >= O_FIRST && obj_id < O_NUMBER_OF);\n    ASSERT(name != nullptr);\n    M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id);\n    if (entry->names == nullptr) {\n        entry->names = Vector_Create(sizeof(char *));\n    }\n    char *const dup = Memory_DupStr(name);\n    Vector_Add(entry->names, &dup);\n    // on first insertion, update stable slot\n    if (entry->names->count == 1) {\n        entry->slot = dup;\n    }\n}\n\nvoid Object_SetDescription(\n    const OBJECT_ID obj_id, const char *const description)\n{\n    M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id);\n    Memory_FreePointer(&entry->description);\n    if (description != nullptr) {\n        entry->description = Memory_DupStr(description);\n    }\n}\n\nconst char *Object_GetName(const OBJECT_ID obj_id)\n{\n    M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id);\n    return entry ? entry->slot : nullptr;\n}\n\nconst char *const *Object_GetNamePtr(const OBJECT_ID obj_id)\n{\n    M_NAME_ENTRY *entry = M_ResolveNameEntry(obj_id);\n    return entry ? &entry->slot : nullptr;\n}\n\nconst char *Object_GetDescription(OBJECT_ID obj_id)\n{\n    M_NAME_ENTRY *const entry = M_ResolveNameEntry(obj_id);\n    return entry != nullptr ? entry->description : nullptr;\n}\n\nvoid Object_ResetAllNames(void)\n{\n    M_ClearAllNames();\n\n    // Install compile-time aliases\n    for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) {\n        m_AliasResolver[obj_id] = obj_id;\n    }\n    for (int32_t i = 0; m_ObjectAliases[i].target_object_id != NO_OBJECT; i++) {\n        const OBJECT_ID target_object_id = m_ObjectAliases[i].target_object_id;\n        const OBJECT_ID source_object_id = m_ObjectAliases[i].source_object_id;\n        m_AliasResolver[target_object_id] = source_object_id;\n    }\n\n    // Now apply default names\n    for (size_t i = 0; m_Defaults[i].object_id != NO_OBJECT; i++) {\n        for (size_t j = 0; m_Defaults[i].default_names[j] != nullptr; j++) {\n            Object_AddName(\n                m_Defaults[i].object_id, m_Defaults[i].default_names[j]);\n        }\n    }\n}\n\nOBJECT_NAME_MATCH *Object_IdsFromName(\n    const char *user_input, int32_t *out_match_count, bool (*filter)(OBJECT_ID))\n{\n    VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));\n\n    for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) {\n        if (filter != nullptr && !filter(obj_id)) {\n            continue;\n        }\n\n        const M_NAME_ENTRY *const name_entry = M_ResolveNameEntry(obj_id);\n        if (name_entry->names != nullptr) {\n            for (int32_t i = 0; i < name_entry->names->count; i++) {\n                const char *name = *(char **)Vector_Get(name_entry->names, i);\n                if (name != nullptr) {\n                    STRING_FUZZY_SOURCE source_item = {\n                        .key = name,\n                        .value = (void *)(intptr_t)obj_id,\n                        .weight = 2,\n                    };\n                    Vector_Add(source, &source_item);\n                }\n            }\n        }\n\n        if (Object_IsType(obj_id, g_PickupObjects)) {\n            STRING_FUZZY_SOURCE source_item = {\n                .key = \"pickup\",\n                .value = (void *)(intptr_t)obj_id,\n                .weight = 1,\n            };\n            Vector_Add(source, &source_item);\n        }\n    }\n\n    VECTOR *matches = String_FuzzyMatch(user_input, source);\n\n    // Fallback: if no localized matches, fuzzy-search the compile-time English\n    // defaults.\n    if (matches->count == 0) {\n        Vector_Free(matches);\n        Vector_Clear(source);\n\n        for (OBJECT_ID obj_id = O_FIRST; obj_id < O_NUMBER_OF; obj_id++) {\n            if (filter != nullptr && !filter(obj_id)) {\n                continue;\n            }\n            const M_DEFAULT *const def = M_ResolveDefault(obj_id);\n            if (def == nullptr) {\n                continue;\n            }\n            for (const char **name = def->default_names; *name != nullptr;\n                 name++) {\n                // Add primary compile-time default name if it passes the filter\n                STRING_FUZZY_SOURCE s = {\n                    .key = *name,\n                    .value = (void *)(intptr_t)obj_id,\n                    .weight = 2,\n                };\n                Vector_Add(source, &s);\n            }\n        }\n\n        matches = String_FuzzyMatch(user_input, source);\n    }\n\n    OBJECT_NAME_MATCH *results =\n        Memory_Alloc(sizeof(OBJECT_NAME_MATCH) * (matches->count + 1));\n    for (int32_t i = 0; i < matches->count; i++) {\n        const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);\n        results[i].object_id = (OBJECT_ID)(intptr_t)match->value;\n        results[i].matched_name = match->key;\n    }\n    results[matches->count].object_id = NO_OBJECT;\n    results[matches->count].matched_name = nullptr;\n    if (out_match_count != nullptr) {\n        *out_match_count = matches->count;\n    }\n\n    Vector_Free(matches);\n    Vector_Free(source);\n    return results;\n}\n\nOBJECT_ID Object_IdFromKey(const char *const key)\n{\n    for (int32_t i = 0; m_Defaults[i].object_id != NO_OBJECT; i++) {\n        if (strcmp(m_Defaults[i].key, key) == 0) {\n            return m_Defaults[i].object_id;\n        }\n    }\n    return NO_OBJECT;\n}\n"
  },
  {
    "path": "src/trx/game/objects/names.def",
    "content": "// Consumables\nX_OBJ_NAME_DEFINE(O_FLARE_ITEM,          \"flare\",          X_OBJ_NAMES(\"Flare\"))\nX_OBJ_NAME_DEFINE(O_FLAREBOX_ITEM,       \"flares_box\",     X_OBJ_NAMES(\"Flares Box\"))\nX_OBJ_NAME_DEFINE(O_EXPLOSIVE_ITEM,      \"grenade\",        X_OBJ_NAMES(\"Grenade\"))\nX_OBJ_NAME_DEFINE(O_SMALL_MEDIPACK_ITEM, \"small_medipack\", X_OBJ_NAMES(\"Small Medipack\"))\nX_OBJ_NAME_DEFINE(O_LARGE_MEDIPACK_ITEM, \"large_medipack\", X_OBJ_NAMES(\"Large Medipack\"))\n\n// Guns\nX_OBJ_NAME_DEFINE(O_PISTOL_ITEM,       \"pistols\",          X_OBJ_NAMES(\"Pistols\"))\nX_OBJ_NAME_DEFINE(O_SHOTGUN_ITEM,      \"shotgun\",          X_OBJ_NAMES(\"Shotgun\"))\nX_OBJ_NAME_DEFINE(O_MAGNUM_ITEM,       \"magnums\",          X_OBJ_NAMES(\"Magnums\"))\nX_OBJ_NAME_DEFINE(O_AUTOS_ITEM,        \"autos\",            X_OBJ_NAMES(\"Automatic Pistols\"))\nX_OBJ_NAME_DEFINE(O_DESERT_EAGLE_ITEM, \"desert_eagle\",     X_OBJ_NAMES(\"Desert Eagle\"))\nX_OBJ_NAME_DEFINE(O_UZI_ITEM,          \"uzis\",             X_OBJ_NAMES(\"Uzis\"))\nX_OBJ_NAME_DEFINE(O_HARPOON_ITEM,      \"harpoon_gun\",      X_OBJ_NAMES(\"Harpoon Gun\"))\nX_OBJ_NAME_DEFINE(O_M16_ITEM,          \"m16\",              X_OBJ_NAMES(\"M16\"))\nX_OBJ_NAME_DEFINE(O_MP5_ITEM,          \"mp5\",              X_OBJ_NAMES(\"MP5\"))\nX_OBJ_NAME_DEFINE(O_GRENADE_GUN_ITEM,  \"grenade_launcher\", X_OBJ_NAMES(\"Grenade Launcher\"))\nX_OBJ_NAME_DEFINE(O_ROCKET_GUN_ITEM,   \"rocket_launcher\",  X_OBJ_NAMES(\"Rocket Launcher\"))\n\n// Ammo\nX_OBJ_NAME_DEFINE(O_PISTOL_AMMO_ITEM,       \"pistols_ammo\",          X_OBJ_NAMES(\"Pistol Clips\"))\nX_OBJ_NAME_DEFINE(O_SHOTGUN_AMMO_ITEM,      \"shotgun_ammo\",          X_OBJ_NAMES(\"Shotgun Shells\"))\nX_OBJ_NAME_DEFINE(O_MAGNUM_AMMO_ITEM,       \"magnums_ammo\",          X_OBJ_NAMES(\"Magnum Clips\"))\nX_OBJ_NAME_DEFINE(O_AUTOS_AMMO_ITEM,        \"autos_ammo\",            X_OBJ_NAMES(\"Automatic Pistol Clips\"))\nX_OBJ_NAME_DEFINE(O_DESERT_EAGLE_AMMO_ITEM, \"desert_eagle_ammo\",     X_OBJ_NAMES(\"Desert Eagle Clips\"))\nX_OBJ_NAME_DEFINE(O_UZI_AMMO_ITEM,          \"uzis_ammo\",             X_OBJ_NAMES(\"Uzi Clips\"))\nX_OBJ_NAME_DEFINE(O_HARPOON_AMMO_ITEM,      \"harpoon_gun_ammo\",      X_OBJ_NAMES(\"Harpoons\"))\nX_OBJ_NAME_DEFINE(O_M16_AMMO_ITEM,          \"m16_ammo\",              X_OBJ_NAMES(\"M16 Clips\"))\nX_OBJ_NAME_DEFINE(O_MP5_AMMO_ITEM,          \"mp5_ammo\",              X_OBJ_NAMES(\"MP5 Clips\"))\nX_OBJ_NAME_DEFINE(O_GRENADE_AMMO_ITEM,      \"grenade_launcher_ammo\", X_OBJ_NAMES(\"Grenades\"))\nX_OBJ_NAME_DEFINE(O_ROCKET_AMMO_ITEM,       \"rocket_launcher_ammo\",  X_OBJ_NAMES(\"Rockets\"))\n\n// Pickups\nX_OBJ_NAME_DEFINE(O_PICKUP_ITEM_1, \"pickup_1\", X_OBJ_NAMES(\"Pickup Item 1\"))\nX_OBJ_NAME_DEFINE(O_PICKUP_ITEM_2, \"pickup_2\", X_OBJ_NAMES(\"Pickup Item 2\"))\nX_OBJ_NAME_DEFINE(O_QUEST_ITEM_1,  \"quest_1\",  X_OBJ_NAMES(\"Quest Item 1\"))\nX_OBJ_NAME_DEFINE(O_QUEST_ITEM_2,  \"quest_2\",  X_OBJ_NAMES(\"Quest Item 2\"))\nX_OBJ_NAME_DEFINE(O_QUEST_ITEM_3,  \"quest_3\",  X_OBJ_NAMES(\"Quest Item 3\"))\nX_OBJ_NAME_DEFINE(O_QUEST_ITEM_4,  \"quest_4\",  X_OBJ_NAMES(\"Quest Item 4\"))\nX_OBJ_NAME_DEFINE(O_KEY_ITEM_1,    \"key_1\",    X_OBJ_NAMES(\"Key 1\"))\nX_OBJ_NAME_DEFINE(O_KEY_ITEM_2,    \"key_2\",    X_OBJ_NAMES(\"Key 2\"))\nX_OBJ_NAME_DEFINE(O_KEY_ITEM_3,    \"key_3\",    X_OBJ_NAMES(\"Key 3\"))\nX_OBJ_NAME_DEFINE(O_KEY_ITEM_4,    \"key_4\",    X_OBJ_NAMES(\"Key 4\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_1, \"puzzle_1\", X_OBJ_NAMES(\"Puzzle Item 1\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_2, \"puzzle_2\", X_OBJ_NAMES(\"Puzzle Item 2\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_3, \"puzzle_3\", X_OBJ_NAMES(\"Puzzle Item 3\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_ITEM_4, \"puzzle_4\", X_OBJ_NAMES(\"Puzzle Item 4\"))\nX_OBJ_NAME_DEFINE(O_LEADBAR_ITEM,  \"lead_bar\", X_OBJ_NAMES(\"Lead Bar\"))\nX_OBJ_NAME_DEFINE(O_SCION_ITEM_1,  \"scion\",    X_OBJ_NAMES(\"Scion\"))\nX_OBJ_NAME_DEFINE(O_SECRET_1,      \"secret_1\", X_OBJ_NAMES(\"Secret 1\"))\nX_OBJ_NAME_DEFINE(O_SECRET_2,      \"secret_2\", X_OBJ_NAMES(\"Secret 2\"))\nX_OBJ_NAME_DEFINE(O_SECRET_3,      \"secret_3\", X_OBJ_NAMES(\"Secret 3\"))\nX_OBJ_ALIAS_DEFINE(O_SCION_ITEM_2, O_SCION_ITEM_1)\nX_OBJ_ALIAS_DEFINE(O_SCION_ITEM_3, O_SCION_ITEM_1)\nX_OBJ_ALIAS_DEFINE(O_SCION_ITEM_4, O_SCION_ITEM_1)\n\n// Inventory ring\nX_OBJ_NAME_DEFINE(O_COMPASS_OPTION,      \"compass\",   X_OBJ_NAMES(\"Compass\"))\nX_OBJ_NAME_DEFINE(O_STOPWATCH_OPTION,    \"stopwatch\", X_OBJ_NAMES(\"Statistics\"))\nX_OBJ_NAME_DEFINE(O_PASSPORT_OPTION,     \"passport\",  X_OBJ_NAMES(\"Game\"))\nX_OBJ_NAME_DEFINE(O_PHOTO_OPTION,        \"photo\",     X_OBJ_NAMES(\"Lara's Home\"))\nX_OBJ_NAME_DEFINE(O_DETAIL_OPTION,       \"graphics\",  X_OBJ_NAMES(\"Graphics\"))\nX_OBJ_NAME_DEFINE(O_CONTROL_OPTION,      \"controls\",  X_OBJ_NAMES(\"Controls\"))\nX_OBJ_NAME_DEFINE(O_SOUND_OPTION,        \"sound\",     X_OBJ_NAMES(\"Sound\"))\nX_OBJ_NAME_DEFINE(O_GAMMA_OPTION,        \"gamma\",     X_OBJ_NAMES(\"Gamma\"))\nX_OBJ_NAME_DEFINE(O_GLOBE_SELECT_OPTION, \"globe\",     X_OBJ_NAMES(\"Globe\"))\nX_OBJ_NAME_DEFINE(O_PDA_OPTION,          \"pda\",       X_OBJ_NAMES(\"Gameplay\"))\nX_OBJ_ALIAS_DEFINE(O_PASSPORT_CLOSED,    O_PASSPORT_OPTION)\n\n// Inventory ring - consumables\nX_OBJ_ALIAS_DEFINE(O_SMALL_MEDIPACK_OPTION, O_SMALL_MEDIPACK_ITEM)\nX_OBJ_ALIAS_DEFINE(O_LARGE_MEDIPACK_OPTION, O_LARGE_MEDIPACK_ITEM)\nX_OBJ_ALIAS_DEFINE(O_FLAREBOX_OPTION,       O_FLARE_ITEM)\n\n// Inventory ring - guns\nX_OBJ_ALIAS_DEFINE(O_PISTOL_OPTION,       O_PISTOL_ITEM)\nX_OBJ_ALIAS_DEFINE(O_SHOTGUN_OPTION,      O_SHOTGUN_ITEM)\nX_OBJ_ALIAS_DEFINE(O_MAGNUM_OPTION,       O_MAGNUM_ITEM)\nX_OBJ_ALIAS_DEFINE(O_AUTOS_OPTION,        O_AUTOS_ITEM)\nX_OBJ_ALIAS_DEFINE(O_DESERT_EAGLE_OPTION, O_DESERT_EAGLE_ITEM)\nX_OBJ_ALIAS_DEFINE(O_UZI_OPTION,          O_UZI_ITEM)\nX_OBJ_ALIAS_DEFINE(O_HARPOON_OPTION,      O_HARPOON_ITEM)\nX_OBJ_ALIAS_DEFINE(O_M16_OPTION,          O_M16_ITEM)\nX_OBJ_ALIAS_DEFINE(O_MP5_OPTION,          O_MP5_ITEM)\nX_OBJ_ALIAS_DEFINE(O_GRENADE_GUN_OPTION,  O_GRENADE_GUN_ITEM)\nX_OBJ_ALIAS_DEFINE(O_ROCKET_GUN_OPTION,  O_ROCKET_GUN_ITEM)\n\n// Inventory ring - ammo\nX_OBJ_ALIAS_DEFINE(O_PISTOL_AMMO_OPTION,       O_PISTOL_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_SHOTGUN_AMMO_OPTION,      O_SHOTGUN_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_MAGNUM_AMMO_OPTION,       O_MAGNUM_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_AUTOS_AMMO_OPTION,        O_AUTOS_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_DESERT_EAGLE_AMMO_OPTION, O_DESERT_EAGLE_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_UZI_AMMO_OPTION,          O_UZI_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_HARPOON_AMMO_OPTION,      O_HARPOON_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_M16_AMMO_OPTION,          O_M16_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_MP5_AMMO_OPTION,          O_MP5_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_GRENADE_AMMO_OPTION,      O_GRENADE_AMMO_ITEM)\nX_OBJ_ALIAS_DEFINE(O_ROCKET_AMMO_OPTION,      O_ROCKET_AMMO_ITEM)\n\n// Inventory ring - pickups\nX_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_1,       O_PUZZLE_ITEM_1)\nX_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_2,       O_PUZZLE_ITEM_2)\nX_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_3,       O_PUZZLE_ITEM_3)\nX_OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_4,       O_PUZZLE_ITEM_4)\nX_OBJ_ALIAS_DEFINE(O_KEY_OPTION_1,          O_KEY_ITEM_1)\nX_OBJ_ALIAS_DEFINE(O_KEY_OPTION_2,          O_KEY_ITEM_2)\nX_OBJ_ALIAS_DEFINE(O_KEY_OPTION_3,          O_KEY_ITEM_3)\nX_OBJ_ALIAS_DEFINE(O_KEY_OPTION_4,          O_KEY_ITEM_4)\nX_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_1,        O_QUEST_ITEM_1)\nX_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_2,        O_QUEST_ITEM_2)\nX_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_3,        O_QUEST_ITEM_3)\nX_OBJ_ALIAS_DEFINE(O_QUEST_OPTION_4,        O_QUEST_ITEM_4)\nX_OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_1,       O_PICKUP_ITEM_1)\nX_OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_2,       O_PICKUP_ITEM_2)\nX_OBJ_ALIAS_DEFINE(O_LEADBAR_OPTION,        O_LEADBAR_ITEM)\nX_OBJ_ALIAS_DEFINE(O_SCION_OPTION,          O_SCION_ITEM_1)\n\n// Lara animations\nX_OBJ_NAME_DEFINE(O_LARA,                  \"lara\",                  X_OBJ_NAMES(\"Lara\"))\nX_OBJ_NAME_DEFINE(O_LARA_HAIR,             \"lara_hair\",             X_OBJ_NAMES(\"Lara's Braid\"))\nX_OBJ_NAME_DEFINE(O_LARA_PISTOLS,          \"lara_pistols\",          X_OBJ_NAMES(\"Pistols Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_SHOTGUN,          \"lara_shotgun\",          X_OBJ_NAMES(\"Shotgun Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_MAGNUMS,          \"lara_magnums\",          X_OBJ_NAMES(\"Magnums Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_AUTOS,            \"lara_autos\",            X_OBJ_NAMES(\"Automatic Pistols Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_DESERT_EAGLE,     \"lara_desert_eagle\",     X_OBJ_NAMES(\"Desert Eagle Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_UZIS,             \"lara_uzis\",             X_OBJ_NAMES(\"Uzis Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_M16,              \"lara_m16\",              X_OBJ_NAMES(\"M16 Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_MP5,              \"lara_mp5\",              X_OBJ_NAMES(\"MP5 Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_GRENADE_GUN,      \"lara_grenade\",          X_OBJ_NAMES(\"Grenade Launcher Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_GRENADE_GUN,      \"lara_rocket\",           X_OBJ_NAMES(\"Rocket Launcher Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_HARPOON_GUN,      \"lara_harpoon\",          X_OBJ_NAMES(\"Harpoon Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_FLARE,            \"lara_flare\",            X_OBJ_NAMES(\"Flare Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_EXTRA,            \"lara_extra\",            X_OBJ_NAMES(\"Lara's Extra Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_SKIDOO,           \"lara_skidoo\",           X_OBJ_NAMES(\"Snowmobile Animation\"))\nX_OBJ_NAME_DEFINE(O_LARA_BOAT,             \"lara_boat\",             X_OBJ_NAMES(\"Boat Animation\"))\n\n// Vehicles\nX_OBJ_NAME_DEFINE(O_SKIDOO_FAST,           \"skidoo_fast\",           X_OBJ_NAMES(\"Red Snowmobile\"))\nX_OBJ_NAME_DEFINE(O_BOAT,                  \"boat\",                  X_OBJ_NAMES(\"Boat\"))\nX_OBJ_NAME_DEFINE(O_QUAD_BIKE,             \"quad_bike\",             X_OBJ_NAMES(\"Quad Bike\"))\nX_OBJ_NAME_DEFINE(O_KAYAK,                 \"kayak\",                 X_OBJ_NAMES(\"Kayak\"))\nX_OBJ_NAME_DEFINE(O_UPV,                   \"upv\",                   X_OBJ_NAMES(\"UPV\", \"Minisub\"))\nX_OBJ_NAME_DEFINE(O_MOUNTED_GUN,           \"mounted_gun\",           X_OBJ_NAMES(\"Mounted Gun\"))\nX_OBJ_NAME_DEFINE(O_MINE_CART,             \"mine_cart\",             X_OBJ_NAMES(\"Mine Cart\"))\nX_OBJ_NAME_DEFINE(O_RIB,                   \"rib\",                   X_OBJ_NAMES(\"Rigid Inflatable Boat\", \"RIB\"))\n\n// Enemies\nX_OBJ_NAME_DEFINE(O_BACON_LARA,            \"bacon_lara\",            X_OBJ_NAMES(\"Bacon Lara\"))\nX_OBJ_NAME_DEFINE(O_WOLF,                  \"wolf\",                  X_OBJ_NAMES(\"Wolf\"))\nX_OBJ_NAME_DEFINE(O_BEAR,                  \"bear\",                  X_OBJ_NAMES(\"Bear\"))\nX_OBJ_NAME_DEFINE(O_BAT,                   \"bat\",                   X_OBJ_NAMES(\"Bat\"))\nX_OBJ_NAME_DEFINE(O_LIZARD,                \"lizard\",                X_OBJ_NAMES(\"Lizard\"))\nX_OBJ_NAME_DEFINE(O_CROCODILE,             \"crocodile\",             X_OBJ_NAMES(\"Crocodile\"))\nX_OBJ_NAME_DEFINE(O_ALLIGATOR,             \"alligator\",             X_OBJ_NAMES(\"Alligator\"))\nX_OBJ_NAME_DEFINE(O_LION,                  \"lion\",                  X_OBJ_NAMES(\"Lion\"))\nX_OBJ_NAME_DEFINE(O_LIONESS,               \"lioness\",               X_OBJ_NAMES(\"Lioness\", \"Lion\"))\nX_OBJ_NAME_DEFINE(O_PUMA,                  \"puma\",                  X_OBJ_NAMES(\"Puma\"))\nX_OBJ_NAME_DEFINE(O_APE,                   \"ape\",                   X_OBJ_NAMES(\"Ape\"))\nX_OBJ_NAME_DEFINE(O_RAT,                   \"rat\",                   X_OBJ_NAMES(\"Rat\", \"Land Rat\"))\nX_OBJ_NAME_DEFINE(O_VOLE,                  \"vole\",                  X_OBJ_NAMES(\"Vole\", \"Water Rat\"))\nX_OBJ_NAME_DEFINE(O_ORCA,                  \"orca\",                  X_OBJ_NAMES(\"Orca\"))\nX_OBJ_NAME_DEFINE(O_TREX,                  \"trex\",                  X_OBJ_NAMES(\"T-Rex\"))\nX_OBJ_NAME_DEFINE(O_TREX_ALPHA,            \"trex_alpha\",            X_OBJ_NAMES(\"T-Rex Alpha\"))\nX_OBJ_NAME_DEFINE(O_COMPY,                 \"compy\",                 X_OBJ_NAMES(\"Compsognathus\"))\nX_OBJ_NAME_DEFINE(O_CARCASS,               \"carcass\",               X_OBJ_NAMES(\"Carcass\"))\nX_OBJ_NAME_DEFINE(O_RAPTOR,                \"raptor\",                X_OBJ_NAMES(\"Raptor\"))\nX_OBJ_NAME_DEFINE(O_ATLANTEAN_WINGED,      \"atlantean_winged\",      X_OBJ_NAMES(\"Winged Atlantean\"))\nX_OBJ_NAME_DEFINE(O_ATLANTEAN_SHOOTER,     \"atlantean_shooter\",     X_OBJ_NAMES(\"Shooting Atlantean\"))\nX_OBJ_NAME_DEFINE(O_ATLANTEAN_GROUND,      \"atlantean_ground\",      X_OBJ_NAMES(\"Ground Atlantean\"))\nX_OBJ_NAME_DEFINE(O_CENTAUR,               \"centaur\",               X_OBJ_NAMES(\"Centaur\"))\nX_OBJ_NAME_DEFINE(O_MUMMY,                 \"mummy\",                 X_OBJ_NAMES(\"Mummy\"))\nX_OBJ_NAME_DEFINE(O_DINO_WARRIOR,          \"dino_mutant\",           X_OBJ_NAMES(\"Dino Mutant\"))\nX_OBJ_NAME_DEFINE(O_FISH,                  \"fish_mutant\",           X_OBJ_NAMES(\"Mutant Fish\"))\nX_OBJ_NAME_DEFINE(O_LARSON,                \"larson\",                X_OBJ_NAMES(\"Larson\"))\nX_OBJ_NAME_DEFINE(O_PIERRE,                \"pierre\",                X_OBJ_NAMES(\"Pierre\"))\nX_OBJ_NAME_DEFINE(O_SKATEBOARD,            \"skateboard\",            X_OBJ_NAMES(\"Skateboard\"))\nX_OBJ_NAME_DEFINE(O_SKATEKID,              \"skate_kid\",             X_OBJ_NAMES(\"Skate Kid\"))\nX_OBJ_NAME_DEFINE(O_COWBOY,                \"cowboy\",                X_OBJ_NAMES(\"Cowboy\"))\nX_OBJ_NAME_DEFINE(O_BALDY,                 \"baldy\",                 X_OBJ_NAMES(\"Baldy\"))\nX_OBJ_NAME_DEFINE(O_NATLA,                 \"natla\",                 X_OBJ_NAMES(\"Natla\"))\nX_OBJ_NAME_DEFINE(O_TORSO,                 \"torso\",                 X_OBJ_NAMES(\"Torso\", \"Adam\", \"Giant Mutant\"))\nX_OBJ_NAME_DEFINE(O_DOG,                   \"dog\",                   X_OBJ_NAMES(\"Dog\", \"Doberman\"))\nX_OBJ_NAME_DEFINE(O_PATROL_DOG,            \"patrol_dog\",            X_OBJ_NAMES(\"Dog\", \"Patrol Dog\"))\nX_OBJ_NAME_DEFINE(O_HUSKIE,                \"huskie\",                X_OBJ_NAMES(\"Dog\", \"Patrol Dog\", \"Huskie\"))\nX_OBJ_NAME_DEFINE(O_CULT_1,                \"cult_1\",                X_OBJ_NAMES(\"Masked Goon 1\"))\nX_OBJ_NAME_DEFINE(O_CULT_1A,               \"cult_1a\",               X_OBJ_NAMES(\"Masked Goon 2\"))\nX_OBJ_NAME_DEFINE(O_CULT_1B,               \"cult_1b\",               X_OBJ_NAMES(\"Masked Goon 3\"))\nX_OBJ_NAME_DEFINE(O_CULT_2,                \"cult_2\",                X_OBJ_NAMES(\"Knife Thrower\"))\nX_OBJ_NAME_DEFINE(O_CULT_3,                \"cult_3\",                X_OBJ_NAMES(\"Shotgun Goon\"))\nX_OBJ_NAME_DEFINE(O_MOUSE,                 \"mouse\",                 X_OBJ_NAMES(\"Rat\"))\nX_OBJ_NAME_DEFINE(O_MONKEY,                \"monkey\",                X_OBJ_NAMES(\"Monkey\"))\nX_OBJ_NAME_DEFINE(O_DRAGON_FRONT,          \"dragon_front\",          X_OBJ_NAMES(\"Dragon Front\"))\nX_OBJ_NAME_DEFINE(O_DRAGON_BACK,           \"dragon_back\",           X_OBJ_NAMES(\"Dragon Back\"))\nX_OBJ_NAME_DEFINE(O_SHARK,                 \"shark\",                 X_OBJ_NAMES(\"Shark\"))\nX_OBJ_NAME_DEFINE(O_EEL,                   \"eel\",                   X_OBJ_NAMES(\"Eel\"))\nX_OBJ_NAME_DEFINE(O_BIG_EEL,               \"big_eel\",               X_OBJ_NAMES(\"Big Eel\"))\nX_OBJ_NAME_DEFINE(O_BARRACUDA,             \"barracuda\",             X_OBJ_NAMES(\"Barracuda\"))\nX_OBJ_NAME_DEFINE(O_DIVER,                 \"diver\",                 X_OBJ_NAMES(\"Scuba Diver\"))\nX_OBJ_NAME_DEFINE(O_WORKER_1,              \"worker_1\",              X_OBJ_NAMES(\"Gunman Goon 1\"))\nX_OBJ_NAME_DEFINE(O_WORKER_2,              \"worker_2\",              X_OBJ_NAMES(\"Gunman Goon 2\"))\nX_OBJ_NAME_DEFINE(O_WORKER_3,              \"worker_3\",              X_OBJ_NAMES(\"Stick Wielding Goon 1\"))\nX_OBJ_NAME_DEFINE(O_WORKER_4,              \"worker_4\",              X_OBJ_NAMES(\"Stick Wielding Goon 2\"))\nX_OBJ_NAME_DEFINE(O_WORKER_5,              \"worker_5\",              X_OBJ_NAMES(\"Flamethrower Goon\"))\nX_OBJ_NAME_DEFINE(O_JELLY,                 \"jelly\",                 X_OBJ_NAMES(\"Jellyfish\"))\nX_OBJ_NAME_DEFINE(O_SPIDER,                \"spider\",                X_OBJ_NAMES(\"Spider\"))\nX_OBJ_NAME_DEFINE(O_BIG_SPIDER,            \"big_spider\",            X_OBJ_NAMES(\"Giant Spider\"))\nX_OBJ_NAME_DEFINE(O_CROW,                  \"crow\",                  X_OBJ_NAMES(\"Crow\"))\nX_OBJ_NAME_DEFINE(O_TIGER,                 \"tiger\",                 X_OBJ_NAMES(\"Tiger\"))\nX_OBJ_NAME_DEFINE(O_BARTOLI,               \"bartoli\",               X_OBJ_NAMES(\"Marco Bartoli\"))\nX_OBJ_NAME_DEFINE(O_XIAN_SPEARMAN,         \"xian_spearman\",         X_OBJ_NAMES(\"Xian Spearman\"))\nX_OBJ_NAME_DEFINE(O_XIAN_SPEARMAN_STATUE,  \"xian_spearman_statue\",  X_OBJ_NAMES(\"Xian Spearman Statue\"))\nX_OBJ_NAME_DEFINE(O_XIAN_KNIGHT,           \"xian_knight\",           X_OBJ_NAMES(\"Xian Knight\"))\nX_OBJ_NAME_DEFINE(O_XIAN_KNIGHT_STATUE,    \"xian_knight_statue\",    X_OBJ_NAMES(\"Xian Knight Statue\"))\nX_OBJ_NAME_DEFINE(O_YETI,                  \"yeti\",                  X_OBJ_NAMES(\"Yeti\"))\nX_OBJ_NAME_DEFINE(O_BIRD_GUARDIAN,         \"bird_guardian\",         X_OBJ_NAMES(\"Bird Monster\"))\nX_OBJ_NAME_DEFINE(O_EAGLE,                 \"eagle\",                 X_OBJ_NAMES(\"Eagle\"))\nX_OBJ_NAME_DEFINE(O_BANDIT_1,              \"bandit_1\",              X_OBJ_NAMES(\"Mercenary 1\", \"Masked Goon 1\"))\nX_OBJ_NAME_DEFINE(O_BANDIT_2,              \"bandit_2\",              X_OBJ_NAMES(\"Mercenary 2\", \"Masked Goon 2\"))\nX_OBJ_NAME_DEFINE(O_BANDIT_2B,             \"bandit_2b\",             X_OBJ_NAMES(\"Mercenary 3\", \"Masked Goon 3\"))\nX_OBJ_NAME_DEFINE(O_SKIDOO_ARMED,          \"skidoo_armed\",          X_OBJ_NAMES(\"Black Snowmobile\"))\nX_OBJ_NAME_DEFINE(O_SKIDOO_DRIVER,         \"skidoo_driver\",         X_OBJ_NAMES(\"Black Snowmobile Driver\"))\nX_OBJ_NAME_DEFINE(O_MONK_1,                \"monk_1\",                X_OBJ_NAMES(\"Monk 1\"))\nX_OBJ_NAME_DEFINE(O_MONK_2,                \"monk_2\",                X_OBJ_NAMES(\"Monk 2\"))\nX_OBJ_NAME_DEFINE(O_CENTAUR_STATUE,        \"centaur_statue\",        X_OBJ_NAMES(\"Centaur Statue\"))\nX_OBJ_NAME_DEFINE(O_PODS,                  \"pods\",                  X_OBJ_NAMES(\"Pod\"))\nX_OBJ_NAME_DEFINE(O_BIG_POD,               \"big_pod\",               X_OBJ_NAMES(\"Big Pod\"))\nX_OBJ_NAME_DEFINE(O_TONY,                  \"tony\",                  X_OBJ_NAMES(\"Tony\"))\nX_OBJ_NAME_DEFINE(O_WILLARD,               \"willard\",               X_OBJ_NAMES(\"Willard\"))\nX_OBJ_NAME_DEFINE(O_VULTURE,               \"vulture\",               X_OBJ_NAMES(\"Vulture\"))\nX_OBJ_NAME_DEFINE(O_COBRA,                 \"snake\",                 X_OBJ_NAMES(\"Snake\", \"Cobra\"))\nX_OBJ_NAME_DEFINE(O_SHIVA,                 \"shiva\",                 X_OBJ_NAMES(\"Shiva\"))\nX_OBJ_NAME_DEFINE(O_STHPAC_MERCENARY,      \"sthpac_mercenary\",      X_OBJ_NAMES(\"South Pacific Mercenary\"))\nX_OBJ_NAME_DEFINE(O_TRIBE_AXEMAN,          \"tribe_axeman\",          X_OBJ_NAMES(\"Tribe Axeman\"))\nX_OBJ_NAME_DEFINE(O_TRIBE_PIPEMAN,         \"tribe_pipeman\",         X_OBJ_NAMES(\"Tribe Blowpipe User\"))\nX_OBJ_NAME_DEFINE(O_TRIBE_BOSS,            \"tribe_boss\",            X_OBJ_NAMES(\"Tribe Boss\"))\nX_OBJ_NAME_DEFINE(O_PUNK_1,                \"punk_1\",                X_OBJ_NAMES(\"Punk 1\"))\nX_OBJ_NAME_DEFINE(O_PUNK_2,                \"punk_2\",                X_OBJ_NAMES(\"Punk 2\"))\nX_OBJ_NAME_DEFINE(O_SECURITY_GUARD,        \"security_guard\",        X_OBJ_NAMES(\"Security Guard\"))\nX_OBJ_NAME_DEFINE(O_SWAT_1,                \"swat_1\",                X_OBJ_NAMES(\"SWAT 1\"))\nX_OBJ_NAME_DEFINE(O_SWAT_1,                \"swat_2\",                X_OBJ_NAMES(\"SWAT 2\"))\nX_OBJ_NAME_DEFINE(O_SWAT_1,                \"swat_3\",                X_OBJ_NAMES(\"SWAT 3\"))\nX_OBJ_NAME_DEFINE(O_SOPHIA,                \"sophia\",                X_OBJ_NAMES(\"Sophia\"))\nX_OBJ_NAME_DEFINE(O_CIVILIAN,              \"civilian\",              X_OBJ_NAMES(\"Civilian\"))\nX_OBJ_NAME_DEFINE(O_PRISONER,              \"prisoner\",              X_OBJ_NAMES(\"Prisoner\"))\nX_OBJ_NAME_DEFINE(O_MP_1,                  \"mp_1\",                  X_OBJ_NAMES(\"MP 1\"))\nX_OBJ_NAME_DEFINE(O_MP_2,                  \"mp_2\",                  X_OBJ_NAMES(\"MP 2\"))\nX_OBJ_NAME_DEFINE(O_RX_WORKER_1,           \"rx_worker_1\",           X_OBJ_NAMES(\"RX Worker 1\"))\nX_OBJ_NAME_DEFINE(O_RX_WORKER_2,           \"rx_worker_2\",           X_OBJ_NAMES(\"RX Worker 2\"))\nX_OBJ_NAME_DEFINE(O_RX_WORKER_3,           \"rx_worker_3\",           X_OBJ_NAMES(\"RX Worker 3\", \"Flamethrower\"))\nX_OBJ_NAME_DEFINE(O_CRAWLER_MUTANT,        \"crawler_mutant\",        X_OBJ_NAMES(\"Crawler Mutant\"))\nX_OBJ_NAME_DEFINE(O_DYING_MUTANT,          \"dying_mutant\",          X_OBJ_NAMES(\"Dying Mutant\"))\nX_OBJ_NAME_DEFINE(O_HYBRID_MUTANT,         \"hybrid_mutant\",         X_OBJ_NAMES(\"Hybrid Mutant\"))\nX_OBJ_NAME_DEFINE(O_WASP_MUTANT,           \"wasp_mutant\",           X_OBJ_NAMES(\"Wasp Mutant\"))\nX_OBJ_NAME_DEFINE(O_CLAW_MUTANT,           \"claw_mutant\",           X_OBJ_NAMES(\"Claw Mutant\"))\n\n// Traps\nX_OBJ_NAME_DEFINE(O_BLADE,                 \"blade\",                 X_OBJ_NAMES(\"Wall-mounted Blade\"))\nX_OBJ_NAME_DEFINE(O_CEILING_SPIKES,        \"ceiling_spikes\",        X_OBJ_NAMES(\"Spiky Ceiling\"))\nX_OBJ_NAME_DEFINE(O_DAMOCLES_SWORD,        \"damocles_sword\",        X_OBJ_NAMES(\"Damocles Sword\"))\nX_OBJ_NAME_DEFINE(O_DART,                  \"dart\",                  X_OBJ_NAMES(\"Dart\"))\nX_OBJ_NAME_DEFINE(O_DART_EMITTER,          \"dart_emitter\",          X_OBJ_NAMES(\"Dart Emitter\"))\nX_OBJ_NAME_DEFINE(O_DISC,                  \"disc\",                  X_OBJ_NAMES(\"Disc\"))\nX_OBJ_NAME_DEFINE(O_DISC_EMITTER,          \"disc_emitter\",          X_OBJ_NAMES(\"Disc Emitter\"))\nX_OBJ_NAME_DEFINE(O_ELECTRIC_CLEANER,      \"electric_cleaner\",      X_OBJ_NAMES(\"Electric Cleaner\"))\nX_OBJ_NAME_DEFINE(O_EMBER,                 \"ember\",                 X_OBJ_NAMES(\"Ember\"))\nX_OBJ_NAME_DEFINE(O_EMBER_EMITTER,         \"ember_emitter\",         X_OBJ_NAMES(\"Ember Emitter\"))\nX_OBJ_NAME_DEFINE(O_FALLING_BLOCK_1,       \"falling_block_1\",       X_OBJ_NAMES(\"Falling Block 1\", \"Collapsible Floor 1\", \"Collapsible Tiles 1\"))\nX_OBJ_NAME_DEFINE(O_FALLING_BLOCK_2,       \"falling_block_2\",       X_OBJ_NAMES(\"Falling Block 2\", \"Collapsible Floor 2\", \"Collapsible Tiles 2\"))\nX_OBJ_NAME_DEFINE(O_FALLING_BLOCK_3,       \"falling_block_3\",       X_OBJ_NAMES(\"Falling Block 3\", \"Collapsible Floor 3\", \"Collapsible Tiles 3\"))\nX_OBJ_NAME_DEFINE(O_FALLING_CEILING_1,     \"falling_ceiling_1\",     X_OBJ_NAMES(\"Falling Ceiling 1\"))\nX_OBJ_NAME_DEFINE(O_FALLING_CEILING_2,     \"falling_ceiling_2\",     X_OBJ_NAMES(\"Falling Ceiling 2\"))\nX_OBJ_NAME_DEFINE(O_FIRE_HEAD,             \"fire_head\",             X_OBJ_NAMES(\"Fire Head\"))\nX_OBJ_NAME_DEFINE(O_FLAME,                 \"flame\",                 X_OBJ_NAMES(\"Flame\", \"Fire\"))\nX_OBJ_NAME_DEFINE(O_FLAME_EMITTER,         \"flame_emitter\",         X_OBJ_NAMES(\"Flame Emitter\", \"Fire Emitter\"))\nX_OBJ_NAME_DEFINE(O_FLAME_EMITTER_BIG,     \"flame_emitter_big\",     X_OBJ_NAMES(\"Flame Emitter (Big)\", \"Fire Emitter (Big)\"))\nX_OBJ_NAME_DEFINE(O_FLAME_EMITTER_SMALL,   \"flame_emitter_small\",   X_OBJ_NAMES(\"Flame Emitter (Small)\", \"Fire Emitter (Small)\"))\nX_OBJ_NAME_DEFINE(O_FLAME_EMITTER_JET,     \"flame_emitter_jet\",     X_OBJ_NAMES(\"Flame Emitter (Jet)\", \"Fire Emitter (Jet)\"))\nX_OBJ_NAME_DEFINE(O_FLAME_EMITTER_SIDE,    \"flame_emitter_side\",    X_OBJ_NAMES(\"Flame Emitter (Side)\", \"Fire Emitter (Side)\"))\nX_OBJ_NAME_DEFINE(O_GONDOLA,               \"gondola\",               X_OBJ_NAMES(\"Gondola\"))\nX_OBJ_NAME_DEFINE(O_HOOK,                  \"hook\",                  X_OBJ_NAMES(\"Hook\"))\nX_OBJ_NAME_DEFINE(O_ICICLE,                \"icicle\",                X_OBJ_NAMES(\"Icicles\"))\nX_OBJ_NAME_DEFINE(O_KILLER_STATUE,         \"killer_statue\",         X_OBJ_NAMES(\"Statue with Sword\"))\nX_OBJ_NAME_DEFINE(O_LAVA_WEDGE,            \"lava_wedge\",            X_OBJ_NAMES(\"Lava Wedge\"))\nX_OBJ_NAME_DEFINE(O_LIGHTNING_EMITTER,     \"lightning_emitter\",     X_OBJ_NAMES(\"Lightning Emitter\"))\nX_OBJ_NAME_DEFINE(O_MIDAS_TOUCH,           \"midas_touch\",           X_OBJ_NAMES(\"Midas Hand\"))\nX_OBJ_NAME_DEFINE(O_MINE,                  \"mine\",                  X_OBJ_NAMES(\"Aquatic Mine\"))\nX_OBJ_NAME_DEFINE(O_PENDULUM_1,            \"pendulum_1\",            X_OBJ_NAMES(\"Pendulum\", \"Sandbag\", \"Swinging box\"))\nX_OBJ_NAME_DEFINE(O_PENDULUM_2,            \"pendulum_2\",            X_OBJ_NAMES(\"Pendulum\", \"Sandbag\", \"Swinging box\"))\nX_OBJ_NAME_DEFINE(O_POISON_DART,           \"poison_dart\",           X_OBJ_NAMES(\"Poison Dart\"))\nX_OBJ_NAME_DEFINE(O_POISON_DART_EMITTER,   \"poison_dart_emitter\",   X_OBJ_NAMES(\"Poison Dart Emitter\"))\nX_OBJ_NAME_DEFINE(O_POWER_SAW,             \"power_saw\",             X_OBJ_NAMES(\"Power Saw\"))\nX_OBJ_NAME_DEFINE(O_PROPELLER_1,           \"propeller_1\",           X_OBJ_NAMES(\"Airplane Propeller\"))\nX_OBJ_NAME_DEFINE(O_PROPELLER_2,           \"propeller_2\",           X_OBJ_NAMES(\"Underwater Propeller\"))\nX_OBJ_NAME_DEFINE(O_PROPELLER_3,           \"propeller_3\",           X_OBJ_NAMES(\"Air Fan\"))\nX_OBJ_NAME_DEFINE(O_ROTATING_LASER,        \"rotating_laser\",        X_OBJ_NAMES(\"Rotating Laser\"))\nX_OBJ_NAME_DEFINE(O_SECURITY_LASER_ALARM,  \"security_laser_alarm\",  X_OBJ_NAMES(\"Security Laser (Alarm)\"))\nX_OBJ_NAME_DEFINE(O_SECURITY_LASER_DEADLY, \"security_laser_deadly\", X_OBJ_NAMES(\"Security Laser (Deadly)\"))\nX_OBJ_NAME_DEFINE(O_SECURITY_LASER_KILLER, \"security_laser_killer\", X_OBJ_NAMES(\"Security Laser (Killer)\"))\nX_OBJ_NAME_DEFINE(O_ROLLING_BALL_1,        \"rolling_ball_1\",        X_OBJ_NAMES(\"Boulder 1\", \"Rolling Ball 1\"))\nX_OBJ_NAME_DEFINE(O_ROLLING_BALL_2,        \"rolling_ball_2\",        X_OBJ_NAMES(\"Boulder 2\", \"Rolling Ball 2\"))\nX_OBJ_NAME_DEFINE(O_ROLLING_BALL_3,        \"rolling_ball_3\",        X_OBJ_NAMES(\"Boulder 3\", \"Rolling Ball 3\"))\nX_OBJ_NAME_DEFINE(O_ROLLING_BALL_4,        \"rolling_ball_4\",        X_OBJ_NAMES(\"Boulder 4\", \"Rolling Ball 4\"))\nX_OBJ_NAME_DEFINE(O_SPIKES,                \"spikes\",                X_OBJ_NAMES(\"Spikes\"))\nX_OBJ_NAME_DEFINE(O_SPIKE_WALL,            \"spike_wall\",            X_OBJ_NAMES(\"Spike Wall\"))\nX_OBJ_NAME_DEFINE(O_SPINNING_BLADE,        \"spinning_blade\",        X_OBJ_NAMES(\"Spinning Blade\"))\nX_OBJ_NAME_DEFINE(O_SWINGING_AXE,          \"swinging_axe\",          X_OBJ_NAMES(\"Swinging Axe\"))\nX_OBJ_NAME_DEFINE(O_TEETH_TRAP,            \"teeth_trap\",            X_OBJ_NAMES(\"Teeth Trap\", \"Clang-clang Door\"))\nX_OBJ_NAME_DEFINE(O_THORS_HANDLE,          \"thors_handle\",          X_OBJ_NAMES(\"Thor's Hammer Handle\"))\nX_OBJ_NAME_DEFINE(O_THORS_HEAD,            \"thors_head\",            X_OBJ_NAMES(\"Thor's Hammer\"))\nX_OBJ_NAME_DEFINE(O_ELECTRIC_FENCE,        \"electric_fence\",        X_OBJ_NAMES(\"Electric Fence\"))\nX_OBJ_NAME_DEFINE(O_RAPTOR_EMITTER,        \"raptor_emitter\",        X_OBJ_NAMES(\"Raptor Emitter\"))\nX_OBJ_NAME_DEFINE(O_WASP_MUTANT_EMITTER,   \"wasp_mutant_emitter\",   X_OBJ_NAMES(\"Wasp Mutant Emitter\"))\nX_OBJ_NAME_DEFINE(O_TRAIN,                 \"train\",                 X_OBJ_NAMES(\"Train\"))\n\n// General objects\nX_OBJ_NAME_DEFINE(O_BAT_EMITTER,           \"bat_emitter\",           X_OBJ_NAMES(\"Bat Emitter\"))\nX_OBJ_NAME_DEFINE(O_BELL,                  \"bell\",                  X_OBJ_NAMES(\"Bell\"))\nX_OBJ_NAME_DEFINE(O_BIG_BOWL,              \"big_bowl\",              X_OBJ_NAMES(\"Lava Bowl\"))\nX_OBJ_NAME_DEFINE(O_BRIDGE_FLAT,           \"bridge_flat\",           X_OBJ_NAMES(\"Bridge Flat\"))\nX_OBJ_NAME_DEFINE(O_BRIDGE_TILT_1,         \"bridge_tilt_1\",         X_OBJ_NAMES(\"Bridge Tilt 1\"))\nX_OBJ_NAME_DEFINE(O_BRIDGE_TILT_2,         \"bridge_tilt_2\",         X_OBJ_NAMES(\"Bridge Tilt 2\"))\nX_OBJ_NAME_DEFINE(O_COG_1,                 \"cog_1\",                 X_OBJ_NAMES(\"Cog 1\"))\nX_OBJ_NAME_DEFINE(O_COG_2,                 \"cog_2\",                 X_OBJ_NAMES(\"Cog 2\"))\nX_OBJ_NAME_DEFINE(O_COG_3,                 \"cog_3\",                 X_OBJ_NAMES(\"Cog 3\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_1,           \"door_1\",                X_OBJ_NAMES(\"Door 1\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_2,           \"door_2\",                X_OBJ_NAMES(\"Door 2\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_3,           \"door_3\",                X_OBJ_NAMES(\"Door 3\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_4,           \"door_4\",                X_OBJ_NAMES(\"Door 4\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_5,           \"door_5\",                X_OBJ_NAMES(\"Door 5\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_6,           \"door_6\",                X_OBJ_NAMES(\"Door 6\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_7,           \"door_7\",                X_OBJ_NAMES(\"Door 7\"))\nX_OBJ_NAME_DEFINE(O_DOOR_TYPE_8,           \"door_8\",                X_OBJ_NAMES(\"Door 8\"))\nX_OBJ_NAME_DEFINE(O_DRAWBRIDGE,            \"drawbridge\",            X_OBJ_NAMES(\"Drawbridge\"))\nX_OBJ_NAME_DEFINE(O_GENERAL,               \"general\",               X_OBJ_NAMES(\"Minisub\"))\nX_OBJ_NAME_DEFINE(O_KEY_HOLE_1,            \"key_hole_1\",            X_OBJ_NAMES(\"Keyhole 1\"))\nX_OBJ_NAME_DEFINE(O_KEY_HOLE_2,            \"key_hole_2\",            X_OBJ_NAMES(\"Keyhole 2\"))\nX_OBJ_NAME_DEFINE(O_KEY_HOLE_3,            \"key_hole_3\",            X_OBJ_NAMES(\"Keyhole 3\"))\nX_OBJ_NAME_DEFINE(O_KEY_HOLE_4,            \"key_hole_4\",            X_OBJ_NAMES(\"Keyhole 4\"))\nX_OBJ_NAME_DEFINE(O_KILL_ALL_TRIGGERED,    \"kill_all_triggered\",    X_OBJ_NAMES(\"Kill All Triggered\"))\nX_OBJ_NAME_DEFINE(O_LIFT,                  \"lift\",                  X_OBJ_NAMES(\"Lift\"))\nX_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_1,       \"movable_block_1\",       X_OBJ_NAMES(\"Push Block 1\", \"Movable Block 1\"))\nX_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_2,       \"movable_block_2\",       X_OBJ_NAMES(\"Push Block 2\", \"Movable Block 2\"))\nX_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_3,       \"movable_block_3\",       X_OBJ_NAMES(\"Push Block 3\", \"Movable Block 3\"))\nX_OBJ_NAME_DEFINE(O_MOVABLE_BLOCK_4,       \"movable_block_4\",       X_OBJ_NAMES(\"Push Block 4\", \"Movable Block 4\"))\nX_OBJ_NAME_DEFINE(O_MOVING_BAR,            \"moving_bar\",            X_OBJ_NAMES(\"Moving Bar\"))\nX_OBJ_NAME_DEFINE(O_PORTACABIN,            \"portacabin\",            X_OBJ_NAMES(\"Portable Cabin\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_DONE_1,         \"puzzle_done_1\",         X_OBJ_NAMES(\"Puzzle Hole 1 (Done)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_DONE_2,         \"puzzle_done_2\",         X_OBJ_NAMES(\"Puzzle Hole 2 (Done)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_DONE_3,         \"puzzle_done_3\",         X_OBJ_NAMES(\"Puzzle Hole 3 (Done)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_DONE_4,         \"puzzle_done_4\",         X_OBJ_NAMES(\"Puzzle Hole 4 (Done)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_1,         \"puzzle_hole_1\",         X_OBJ_NAMES(\"Puzzle Hole 1 (Empty)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_2,         \"puzzle_hole_2\",         X_OBJ_NAMES(\"Puzzle Hole 2 (Empty)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_3,         \"puzzle_hole_3\",         X_OBJ_NAMES(\"Puzzle Hole 3 (Empty)\"))\nX_OBJ_NAME_DEFINE(O_PUZZLE_HOLE_4,         \"puzzle_hole_4\",         X_OBJ_NAMES(\"Puzzle Hole 4 (Empty)\"))\nX_OBJ_NAME_DEFINE(O_SAVE_CRYSTAL_ITEM,     \"save_crystal\",          X_OBJ_NAMES(\"Savegame Crystal\"))\nX_OBJ_NAME_DEFINE(O_SCION_HOLDER,          \"scion_holder\",          X_OBJ_NAMES(\"Scion Holder\"))\nX_OBJ_NAME_DEFINE(O_SLIDING_PILLAR,        \"sliding_pillar\",        X_OBJ_NAMES(\"Sliding Pillar\"))\nX_OBJ_NAME_DEFINE(O_SMASH_OBJECT_1,        \"smashable_1\",           X_OBJ_NAMES(\"Smashable 1\", \"Breakable Window 1\"))\nX_OBJ_NAME_DEFINE(O_SMASH_OBJECT_2,        \"smashable_2\",           X_OBJ_NAMES(\"Smashable 2\", \"Breakable Window 2\"))\nX_OBJ_NAME_DEFINE(O_SMASH_OBJECT_3,        \"smashable_3\",           X_OBJ_NAMES(\"Smashable 3\", \"Breakable Window 3\"))\nX_OBJ_NAME_DEFINE(O_SMASH_OBJECT_4,        \"smashable_4\",           X_OBJ_NAMES(\"Smashable 4\", \"Breakable Window 4\"))\nX_OBJ_NAME_DEFINE(O_SPRINGBOARD,           \"springboard\",           X_OBJ_NAMES(\"Springboard\"))\nX_OBJ_NAME_DEFINE(O_SWITCH_TYPE_AIRLOCK,   \"switch_type_airlock\",   X_OBJ_NAMES(\"Airlock Switch\"))\nX_OBJ_NAME_DEFINE(O_SWITCH_TYPE_BUTTON,    \"switch_type_button\",    X_OBJ_NAMES(\"Button\", \"Push Button\", \"Switch\"))\nX_OBJ_NAME_DEFINE(O_SWITCH_TYPE_NORMAL,    \"switch_type_normal\",    X_OBJ_NAMES(\"Lever\", \"Switch\"))\nX_OBJ_NAME_DEFINE(O_SWITCH_TYPE_SMALL,     \"switch_type_small\",     X_OBJ_NAMES(\"Small Switch\"))\nX_OBJ_NAME_DEFINE(O_SWITCH_TYPE_UW,        \"switch_type_uw\",        X_OBJ_NAMES(\"Underwater Lever\", \"Underwater Switch\"))\nX_OBJ_NAME_DEFINE(O_SWITCH_TYPE_WHEEL,     \"switch_type_wheel\",     X_OBJ_NAMES(\"Wheel Switch\", \"Pulley Switch\", \"Valve Switch\"))\nX_OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_1,       \"trapdoor_1\",            X_OBJ_NAMES(\"Trapdoor 1\"))\nX_OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_2,       \"trapdoor_2\",            X_OBJ_NAMES(\"Trapdoor 2\"))\nX_OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_3,       \"trapdoor_3\",            X_OBJ_NAMES(\"Trapdoor 3\"))\nX_OBJ_NAME_DEFINE(O_ZIPLINE_HANDLE,        \"zipline_handle\",        X_OBJ_NAMES(\"Zipline Handle\"))\nX_OBJ_NAME_DEFINE(O_ELECTRICAL_LIGHT,      \"electrical_light\",      X_OBJ_NAMES(\"Electrical Light\"))\nX_OBJ_NAME_DEFINE(O_RED_LIGHT,             \"red_light\",             X_OBJ_NAMES(\"Red Light\"))\nX_OBJ_NAME_DEFINE(O_GREEN_LIGHT,           \"green_light\",           X_OBJ_NAMES(\"Green Light\"))\nX_OBJ_NAME_DEFINE(O_BLUE_LIGHT,            \"blue_light\",            X_OBJ_NAMES(\"Blue Light\"))\nX_OBJ_NAME_DEFINE(O_AMBER_LIGHT,           \"amber_light\",           X_OBJ_NAMES(\"Amber Light\"))\nX_OBJ_NAME_DEFINE(O_WHITE_LIGHT,           \"white_light\",           X_OBJ_NAMES(\"White Light\"))\nX_OBJ_NAME_DEFINE(O_ON_OFF_LIGHT,          \"on_off_light\",          X_OBJ_NAMES(\"On/Off Light\"))\nX_OBJ_NAME_DEFINE(O_PULSE_LIGHT,           \"pulse_light\",           X_OBJ_NAMES(\"Pulse Light\"))\nX_OBJ_NAME_DEFINE(O_STROBE_LIGHT,          \"strobe_light\",          X_OBJ_NAMES(\"Strobe Light\"))\nX_OBJ_NAME_DEFINE(O_BEACON_LIGHT,          \"beacon_light\",          X_OBJ_NAMES(\"Beacon Light\"))\nX_OBJ_NAME_DEFINE(O_SMOKE_EMITTER_WHITE,   \"smoke_emitter_white\",   X_OBJ_NAMES(\"Smoke Emitter (White)\"))\nX_OBJ_NAME_DEFINE(O_SMOKE_EMITTER_BLACK,   \"smoke_emitter_black\",   X_OBJ_NAMES(\"Smoke Emitter (Black)\"))\nX_OBJ_NAME_DEFINE(O_STEAM_EMITTER,         \"steam_emitter\",         X_OBJ_NAMES(\"Steam Emitter\"))\nX_OBJ_NAME_DEFINE(O_GAS_EMITTER_GREEN,     \"gas_emitter_green\",     X_OBJ_NAMES(\"Gas Emitter (Green)\"))\nX_OBJ_NAME_DEFINE(O_FUSE_BOX,              \"fuse_box\",              X_OBJ_NAMES(\"Fuse Box\"))\nX_OBJ_NAME_DEFINE(O_SENTRY_GUN,            \"sentry_gun\",            X_OBJ_NAMES(\"Sentry Gun\"))\nX_OBJ_NAME_DEFINE(O_AREA_51_ROCKET,        \"area_51_rocket\",        X_OBJ_NAMES(\"Area 51 Rocket\"))\nX_OBJ_NAME_DEFINE(O_AREA_51_ROCKET_BLAST,  \"area_51_rocket_blast\",  X_OBJ_NAMES(\"Area 51 Rocket Blast\"))\nX_OBJ_NAME_DEFINE(O_AREA_51_ROCKET_SUPPORT,\"area_51_rocket_support\",X_OBJ_NAMES(\"Area 51 Rocket Support\"))\n\n// Misc\nX_OBJ_NAME_DEFINE(O_ALARM_SOUND,             \"alarm_sound\",             X_OBJ_NAMES(\"Alarm\"))\nX_OBJ_NAME_DEFINE(O_ALPHABET,                \"alphabet\",                X_OBJ_NAMES(\"Default font\"))\nX_OBJ_NAME_DEFINE(O_ALPHABET_SMALL,          \"alphabet_small\",          X_OBJ_NAMES(\"Small font\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_1,             \"animating_1\",             X_OBJ_NAMES(\"Animating Object 1\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_2,             \"animating_2\",             X_OBJ_NAMES(\"Animating Object 2\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_3,             \"animating_3\",             X_OBJ_NAMES(\"Animating Object 3\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_4,             \"animating_4\",             X_OBJ_NAMES(\"Animating Object 4\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_5,             \"animating_5\",             X_OBJ_NAMES(\"Animating Object 5\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_6,             \"animating_6\",             X_OBJ_NAMES(\"Animating Object 6\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_7,             \"animating_7\",             X_OBJ_NAMES(\"Animating Object 7\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_8,             \"animating_8\",             X_OBJ_NAMES(\"Animating Object 8\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_9,             \"animating_9\",             X_OBJ_NAMES(\"Animating Object 9\"))\nX_OBJ_NAME_DEFINE(O_ANIMATING_10,            \"animating_10\",            X_OBJ_NAMES(\"Animating Object 10\"))\nX_OBJ_NAME_DEFINE(O_ASSAULT_DIGITS,          \"assault_digits\",          X_OBJ_NAMES(\"Assault Digits\"))\nX_OBJ_NAME_DEFINE(O_ASSAULT_TARGET,          \"assault_target\",          X_OBJ_NAMES(\"Assault Target\"))\nX_OBJ_NAME_DEFINE(O_BIRD_TWEETER_1,          \"bird_tweeter_1\",          X_OBJ_NAMES(\"Dripping Water\"))\nX_OBJ_NAME_DEFINE(O_BIRD_TWEETER_2,          \"bird_tweeter_2\",          X_OBJ_NAMES(\"Singing Birds\"))\nX_OBJ_NAME_DEFINE(O_BLOOD,                   \"blood\",                   X_OBJ_NAMES(\"Blood\"))\nX_OBJ_NAME_DEFINE(O_BLOOD_PINK,              \"blood_pink\",              X_OBJ_NAMES(\"Blood (censored)\"))\nX_OBJ_NAME_DEFINE(O_BOAT_BITS,               \"boat_bits\",               X_OBJ_NAMES(\"Boat Bits\"))\nX_OBJ_NAME_DEFINE(O_BODY_PART,               \"body_part\",               X_OBJ_NAMES(\"Body Part\"))\nX_OBJ_NAME_DEFINE(O_BUBBLE_1,                \"bubble_1\",                X_OBJ_NAMES(\"Bubble 1\"))\nX_OBJ_NAME_DEFINE(O_BUBBLE_2,                \"bubble_2\",                X_OBJ_NAMES(\"Bubble 2\"))\nX_OBJ_NAME_DEFINE(O_BUBBLE_EMITTER,          \"bubble_emitter\",          X_OBJ_NAMES(\"Bubble Emitter\"))\nX_OBJ_NAME_DEFINE(O_CAMERA_TARGET,           \"camera_target\",           X_OBJ_NAMES(\"Camera Target\"))\nX_OBJ_NAME_DEFINE(O_CLOCK_CHIMES,            \"clock_chimes\",            X_OBJ_NAMES(\"Bartoli Hideout clock\"))\nX_OBJ_NAME_DEFINE(O_COMBAT_END,              \"combat_end\",              X_OBJ_NAMES(\"Combat End\"))\nX_OBJ_NAME_DEFINE(O_COPTER,                  \"copter\",                  X_OBJ_NAMES(\"Helicopter\"))\nX_OBJ_NAME_DEFINE(O_CUT_SHOTGUN,             \"cut_shotgun\",             X_OBJ_NAMES(\"Shotgun Shower Animation\"))\nX_OBJ_NAME_DEFINE(O_DART_EFFECT,             \"dart_effect\",             X_OBJ_NAMES(\"Dart Effect\"))\nX_OBJ_NAME_DEFINE(O_DETONATOR_BOX,           \"detonator_box\",           X_OBJ_NAMES(\"Detonator Box\"))\nX_OBJ_NAME_DEFINE(O_DING_DONG,               \"ding_dong\",               X_OBJ_NAMES(\"Doorbell\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_1,  \"disposable_animating_1\",  X_OBJ_NAMES(\"Disposable Animating Object 1\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_2,  \"disposable_animating_2\",  X_OBJ_NAMES(\"Disposable Animating Object 2\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_3,  \"disposable_animating_3\",  X_OBJ_NAMES(\"Disposable Animating Object 3\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_4,  \"disposable_animating_4\",  X_OBJ_NAMES(\"Disposable Animating Object 4\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_5,  \"disposable_animating_5\",  X_OBJ_NAMES(\"Disposable Animating Object 5\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_6,  \"disposable_animating_6\",  X_OBJ_NAMES(\"Disposable Animating Object 6\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_7,  \"disposable_animating_7\",  X_OBJ_NAMES(\"Disposable Animating Object 7\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_8,  \"disposable_animating_8\",  X_OBJ_NAMES(\"Disposable Animating Object 8\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_9,  \"disposable_animating_9\",  X_OBJ_NAMES(\"Disposable Animating Object 9\"))\nX_OBJ_NAME_DEFINE(O_DISPOSABLE_ANIMATING_10, \"disposable_animating_10\", X_OBJ_NAMES(\"Disposable Animating Object 10\"))\nX_OBJ_NAME_DEFINE(O_DRAGON_BONES_1,          \"dragon_bones_1\",          X_OBJ_NAMES(\"Placeholder\"))\nX_OBJ_NAME_DEFINE(O_DRAGON_BONES_2,          \"dragon_bones_2\",          X_OBJ_NAMES(\"Dragon Bones Front\"))\nX_OBJ_NAME_DEFINE(O_DRAGON_BONES_3,          \"dragon_bones_3\",          X_OBJ_NAMES(\"Dragon Bones Back\"))\nX_OBJ_NAME_DEFINE(O_DUST,                    \"dust\",                    X_OBJ_NAMES(\"Dust\"))\nX_OBJ_NAME_DEFINE(O_DYING_MONK,              \"dying_monk\",              X_OBJ_NAMES(\"Dying monk\"))\nX_OBJ_NAME_DEFINE(O_EARTHQUAKE,              \"earthquake\",              X_OBJ_NAMES(\"Earthquake\"))\nX_OBJ_NAME_DEFINE(O_EXPLOSION_1,             \"explosion_1\",             X_OBJ_NAMES(\"Explosion 1\"))\nX_OBJ_NAME_DEFINE(O_EXPLOSION_2,             \"explosion_2\",             X_OBJ_NAMES(\"Explosion 2\"))\nX_OBJ_NAME_DEFINE(O_FLARE_FIRE,              \"flare_fire\",              X_OBJ_NAMES(\"Flare sparks\"))\nX_OBJ_NAME_DEFINE(O_FLICKERING_LIGHT,        \"flickering_light\",        X_OBJ_NAMES(\"Flickering Light\"))\nX_OBJ_NAME_DEFINE(O_FX_RESERVED,             \"fx_reserved\",             X_OBJ_NAMES(\"Gray disk\"))\nX_OBJ_NAME_DEFINE(O_GLOW,                    \"glow\",                    X_OBJ_NAMES(\"Glow\"))\nX_OBJ_NAME_DEFINE(O_GLOW_RESERVED,           \"glow_reserved\",           X_OBJ_NAMES(\"Map Glow\"))\nX_OBJ_NAME_DEFINE(O_GONG,                    \"gong\",                    X_OBJ_NAMES(\"Gong\"))\nX_OBJ_NAME_DEFINE(O_GONG_BONGER,             \"gong_bonger\",             X_OBJ_NAMES(\"Gong Stick\"))\nX_OBJ_NAME_DEFINE(O_GRENADE,                 \"grenade\",                 X_OBJ_NAMES(\"Grenade\"))\nX_OBJ_NAME_DEFINE(O_GUN_FLASH,               \"gun_flash\",               X_OBJ_NAMES(\"Gun Flash\"))\nX_OBJ_NAME_DEFINE(O_GUN_SHELL,               \"gun_shell\",               X_OBJ_NAMES(\"Gun Shell\"))\nX_OBJ_NAME_DEFINE(O_HARPOON_BOLT,            \"harpoon_bolt\",            X_OBJ_NAMES(\"Harpoon Bolt\"))\nX_OBJ_NAME_DEFINE(O_HOT_LIQUID,              \"hot_liquid\",              X_OBJ_NAMES(\"Extra Fire\"))\nX_OBJ_NAME_DEFINE(O_INV_BACKGROUND,          \"inv_background\",          X_OBJ_NAMES(\"Menu Background\"))\nX_OBJ_NAME_DEFINE(O_LARA_ALARM,              \"lara_alarm\",              X_OBJ_NAMES(\"Alarm Bell\"))\nX_OBJ_NAME_DEFINE(O_M16_FLASH,               \"m16_flash\",               X_OBJ_NAMES(\"M16 Flash\"))\nX_OBJ_NAME_DEFINE(O_MESH_SWAP_1,             \"mesh_swap_1\",             X_OBJ_NAMES(\"Mesh Swap 1\"))\nX_OBJ_NAME_DEFINE(O_MESH_SWAP_2,             \"mesh_swap_2\",             X_OBJ_NAMES(\"Mesh Swap 2\"))\nX_OBJ_NAME_DEFINE(O_MESH_SWAP_3,             \"mesh_swap_3\",             X_OBJ_NAMES(\"Mesh Swap 3\"))\nX_OBJ_NAME_DEFINE(O_MINI_COPTER,             \"mini_copter\",             X_OBJ_NAMES(\"Helicopter 2\"))\nX_OBJ_NAME_DEFINE(O_MISSILE_ATLANTEAN_BOMB,  \"missile_atlantean_bomb\",  X_OBJ_NAMES(\"Missile (Atlantean Bomb)\"))\nX_OBJ_NAME_DEFINE(O_MISSILE_ATLANTEAN_SHARD, \"missile_atlantean_shard\", X_OBJ_NAMES(\"Missile (Atlantean Shard)\"))\nX_OBJ_NAME_DEFINE(O_MISSILE_FLAME,           \"missile_flame\",           X_OBJ_NAMES(\"Missile (Flame)\"))\nX_OBJ_NAME_DEFINE(O_MISSILE_HARPOON,         \"missile_harpoon\",         X_OBJ_NAMES(\"Missile (Harpoon)\"))\nX_OBJ_NAME_DEFINE(O_MISSILE_KNIFE,           \"missile_knife\",           X_OBJ_NAMES(\"Missile (Knife)\"))\nX_OBJ_NAME_DEFINE(O_MISSILE_POISON,          \"missile_poison\",          X_OBJ_NAMES(\"Missile (Poison)\"))\nX_OBJ_NAME_DEFINE(O_MOTOR_BOAT,              \"boat\",                    X_OBJ_NAMES(\"Boat\"))\nX_OBJ_NAME_DEFINE(O_NATLA_GUN,               \"natla_gun\",               X_OBJ_NAMES(\"Natla's Gun\"))\nX_OBJ_NAME_DEFINE(O_PICKUP_AID,              \"pickup_aid\",              X_OBJ_NAMES(\"Pickup Aid\"))\nX_OBJ_NAME_DEFINE(O_PIRAHNAS,                \"pirahnas\",                X_OBJ_NAMES(\"Pirahnas\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_1,                \"player_1\",                X_OBJ_NAMES(\"Cutscene Actor 1\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_10,               \"player_10\",               X_OBJ_NAMES(\"Cutscene Actor 10\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_2,                \"player_2\",                X_OBJ_NAMES(\"Cutscene Actor 2\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_3,                \"player_3\",                X_OBJ_NAMES(\"Cutscene Actor 3\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_4,                \"player_4\",                X_OBJ_NAMES(\"Cutscene Actor 4\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_5,                \"player_5\",                X_OBJ_NAMES(\"Cutscene Actor 5\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_6,                \"player_6\",                X_OBJ_NAMES(\"Cutscene Actor 6\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_7,                \"player_7\",                X_OBJ_NAMES(\"Cutscene Actor 7\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_8,                \"player_8\",                X_OBJ_NAMES(\"Cutscene Actor 8\"))\nX_OBJ_NAME_DEFINE(O_PLAYER_9,                \"player_9\",                X_OBJ_NAMES(\"Cutscene Actor 9\"))\nX_OBJ_NAME_DEFINE(O_RICOCHET,                \"ricochet\",                X_OBJ_NAMES(\"Ricochet\"))\nX_OBJ_NAME_DEFINE(O_ROCKET,                  \"rocket\",                  X_OBJ_NAMES(\"Rocket\"))\nX_OBJ_NAME_DEFINE(O_SHADOW,                  \"shadow\",                  X_OBJ_NAMES(\"Shadow\"))\nX_OBJ_NAME_DEFINE(O_SHOTGUN_SHELL,           \"shotgun_shell\",           X_OBJ_NAMES(\"Shotgun Shell\"))\nX_OBJ_NAME_DEFINE(O_SKIDOO_TRACK,            \"skidoo_track\",            X_OBJ_NAMES(\"Snowmobile Track\"))\nX_OBJ_NAME_DEFINE(O_SKYBOX,                  \"skybox\",                  X_OBJ_NAMES(\"Skybox\"))\nX_OBJ_NAME_DEFINE(O_SNOW_SPRITE,             \"snow_sprite\",             X_OBJ_NAMES(\"Snowmobile Wake\"))\nX_OBJ_NAME_DEFINE(O_SPHERE_OF_DOOM_1,        \"sphere_of_doom_1\",        X_OBJ_NAMES(\"Dragon Explosion 1\"))\nX_OBJ_NAME_DEFINE(O_SPHERE_OF_DOOM_2,        \"sphere_of_doom_2\",        X_OBJ_NAMES(\"Dragon Explosion 2\"))\nX_OBJ_NAME_DEFINE(O_SPHERE_OF_DOOM_3,        \"sphere_of_doom_3\",        X_OBJ_NAMES(\"Dragon Explosion 3\"))\nX_OBJ_NAME_DEFINE(O_SPLASH_1,                \"splash_1\",                X_OBJ_NAMES(\"Water Ripples 1\"))\nX_OBJ_NAME_DEFINE(O_SPLASH_2,                \"splash_2\",                X_OBJ_NAMES(\"Water Ripples 2\"))\nX_OBJ_NAME_DEFINE(O_TEXT_BOX,                \"text_box\",                X_OBJ_NAMES(\"UI Frame\"))\nX_OBJ_NAME_DEFINE(O_TROPICAL_FISH,           \"tropical_fish\",           X_OBJ_NAMES(\"Tropical Fish\"))\nX_OBJ_NAME_DEFINE(O_TWINKLE,                 \"twinkle\",                 X_OBJ_NAMES(\"Sparkles\"))\nX_OBJ_NAME_DEFINE(O_WATERFALL,               \"waterfall\",               X_OBJ_NAMES(\"Waterfall Mist\"))\nX_OBJ_NAME_DEFINE(O_WATER_SPRITE,            \"water_sprite\",            X_OBJ_NAMES(\"Boat Wake\"))\nX_OBJ_NAME_DEFINE(O_WINSTON,                 \"winston\",                 X_OBJ_NAMES(\"Winston\"))\nX_OBJ_NAME_DEFINE(O_WINSTON_ARMY,            \"winston_army\",            X_OBJ_NAMES(\"Winston (army)\"))\n"
  },
  {
    "path": "src/trx/game/objects/names.h",
    "content": "#pragma once\n\n#include <trx/game/objects/ids.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    OBJECT_ID object_id;\n    const char *matched_name;\n} OBJECT_NAME_MATCH;\n\n// Get the current name for an object (may change on language reload).\nconst char *Object_GetName(OBJECT_ID obj_id);\n\n// Get a stable pointer-to-pointer for the object name, content of which\n// automatically udpates on each language reload.\nconst char *const *Object_GetNamePtr(OBJECT_ID obj_id);\nconst char *Object_GetDescription(OBJECT_ID obj_id);\n\nvoid Object_ResetAllNames(void);\nvoid Object_ClearNames(OBJECT_ID obj_id);\nvoid Object_AddName(OBJECT_ID obj_id, const char *name);\n\nvoid Object_SetDescription(OBJECT_ID obj_id, const char *description);\n\n// Return a list of matching names, with an optional filter callback to only\n// consider objects satisfying certain criteria. out_match_count may be\n// nullptr. The result must be freed by the caller with Memory_Free().\nOBJECT_NAME_MATCH *Object_IdsFromName(\n    const char *name, int32_t *out_match_count, bool (*filter)(OBJECT_ID));\n\n// Return an unique object id for a given programmatic string.\n// Example:\n//     Given a string \"key_1\", returns O_KEY_1.\nOBJECT_ID Object_IdFromKey(const char *key);\n"
  },
  {
    "path": "src/trx/game/objects/setup.c",
    "content": "#include <trx/config.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/pathing.h>\n\n#define M_DEFAULT_RADIUS 10\n\nstatic void M_SetupLara(void)\n{\n    OBJECT *const obj = Object_Get(O_LARA);\n    obj->initialise_func = Lara_InitialiseLoad;\n    obj->can_interpolate_func = Lara_CanInterpolate;\n    obj->draw_func = nullptr;\n    obj->get_mesh_index_func = Lara_GetMeshIndex;\n\n    obj->shadow_size = (UNIT_SHADOW * 10) / 16;\n    obj->hit_points = g_Config.gameplay.start_lara_hitpoints;\n\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nvoid Object_SetupAllObjects(void)\n{\n    for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) {\n        OBJECT *const obj = Object_Get(i);\n        obj->initialise_func = nullptr;\n        obj->control_func = nullptr;\n        obj->floor_height_func = nullptr;\n        obj->ceiling_height_func = nullptr;\n        obj->draw_func = Object_DrawAnimatingItem;\n        obj->collision_func = nullptr;\n        obj->add_walkable_func = nullptr;\n        obj->is_usable_func = nullptr;\n        obj->can_drop_items_func = nullptr;\n        obj->can_interpolate_func = Object_CanInterpolate;\n        obj->should_spawn_blood_func = nullptr;\n        obj->is_alive_func = nullptr;\n        obj->is_targetable_func = nullptr;\n        obj->can_take_damage_func = nullptr;\n        obj->can_be_projectile_target_func = nullptr;\n        obj->can_be_exploded_func = nullptr;\n        obj->get_mesh_index_func = nullptr;\n        obj->hit_points = 0;\n        obj->pivot_length = 0;\n        obj->radius = M_DEFAULT_RADIUS;\n        obj->shadow_size = 0;\n        obj->enable_interpolation = true;\n        obj->lot_setup = LOT_Setup(LOT_SETUP_DEFAULT);\n\n        obj->save_position = false;\n        obj->save_hitpoints = false;\n        obj->save_flags = false;\n        obj->save_anim = false;\n        obj->load_floor = false;\n        obj->intelligent = false;\n        obj->smartness = -1;\n\n        if (obj->setup_func != nullptr) {\n            obj->setup_func(obj);\n        }\n    }\n\n    M_SetupLara();\n    Lara_Hair_Initialise();\n}\n"
  },
  {
    "path": "src/trx/game/objects/setup.h",
    "content": "#pragma once\n\nvoid Object_SetupAllObjects(void);\n"
  },
  {
    "path": "src/trx/game/objects/traps/blade.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define BLADE_CUT_DAMAGE 100\n#define BLADE_TOUCH_BITS 0b00000010 // = 2\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    BLADE_STATE_EMPTY = 0,\n    BLADE_STATE_STOP  = 1,\n    BLADE_STATE_CUT   = 2,\n    // clang-format on\n} BLADE_STATE;\n\ntypedef enum {\n    // clang-format off\n    BLADE_ANIM_RETURN   = 0,\n    BLADE_ANIM_FINISHED = 1,\n    BLADE_ANIM_SET      = 2,\n    BLADE_ANIM_CUT      = 3,\n    // clang-format on\n} BLADE_ANIM;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    const OBJECT *const obj = Object_Get(O_BLADE);\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, BLADE_ANIM_SET, 0);\n    item->current_anim_state = BLADE_STATE_STOP;\n}\n\nstatic void M_Stop(ITEM *const item)\n{\n    const int16_t anim_idx = Item_GetRelativeAnim(item);\n    if (anim_idx == BLADE_ANIM_CUT) {\n        const ANIM *const anim = Item_GetAnim(item);\n        if (!Item_IsTriggerActive(item) && anim->jump_anim_num == item->anim_num\n            && Item_TestFrameEqual(item, -1)) {\n            item->status = IS_INACTIVE;\n            Item_RemoveActive(Item_GetIndex(item));\n            return;\n        }\n    }\n\n    item->goal_anim_state = BLADE_STATE_STOP;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item)\n        && item->current_anim_state == BLADE_STATE_STOP) {\n        item->goal_anim_state = BLADE_STATE_CUT;\n    } else {\n        M_Stop(item);\n    }\n\n    if ((item->touch_bits & BLADE_TOUCH_BITS) != 0\n        && item->current_anim_state == BLADE_STATE_CUT) {\n        Lara_TakeDamage(BLADE_CUT_DAMAGE, true);\n\n        const ITEM *const lara_item = Lara_GetItem();\n        Spawn_BloodBath(\n            lara_item->pos.x, item->pos.y - STEP_L, lara_item->pos.z,\n            lara_item->speed, lara_item->rot.y, lara_item->room_num, 2);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision_Trap;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BLADE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/bubble_emitter.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        item->status = IS_INACTIVE;\n        Item_RemoveActive(item_num);\n        return;\n    }\n\n    if (Random_GetControl() % 24) {\n        return;\n    }\n    const int32_t count = 1 + Random_GetControl() % 2;\n    for (int32_t i = 0; i < count; i++) {\n        Spawn_Bubble(&item->pos, item->room_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = nullptr;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_BUBBLE_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/cleaner.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\n// clang-format off\n#define M_NODE_COUNT 3\n#define M_TOUCH_BITS 0b11111111'11111100\n#define M_RADIUS     (STEP_L * 2)  // = 512\n#define M_TURN       (DEG_90 / 16) // = 1024\n#define M_VELOCITY   (STEP_L / 4)  // = 64\n#define M_MAX_DIST   (WALL_L * 20) // = 20480\n// clang-format on\n\ntypedef struct {\n    bool resume;\n    int16_t turn;\n    int16_t velocity;\n    uint8_t sparks[M_NODE_COUNT];\n} M_PRIV;\n\ntypedef struct {\n    int16_t joint_idx;\n    int16_t node_idx;\n} M_SPARK_NODE;\n\nstatic const M_SPARK_NODE m_Nodes[M_NODE_COUNT] = {\n    { .joint_idx = 5, .node_idx = 9 },\n    { .joint_idx = 9, .node_idx = 10 },\n    { .joint_idx = 13, .node_idx = 11 },\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"resume\", &p->resume));\n    JSON_SHOULD(JSON_READ(io, \"turn\", &p->turn));\n    JSON_SHOULD(JSON_READ(io, \"velocity\", &p->velocity));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"resume\", p->resume);\n    JSONW_WRITE(io, \"turn\", p->turn);\n    JSONW_WRITE(io, \"velocity\", p->velocity);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    item->pos.x = ROUND_TO_SECTOR(item->pos.x) + STEP_L * 2;\n    item->pos.z = ROUND_TO_SECTOR(item->pos.z) + STEP_L * 2;\n    p->resume = false;\n    p->turn = M_TURN;\n    p->velocity = M_VELOCITY;\n}\n\nstatic void M_TriggerSparks(\n    const XYZ_32 pos, const int16_t item_num, const int16_t node)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const XZ_32 delta = {\n        .x = lara_item->pos.x - pos.x,\n        .z = lara_item->pos.z - pos.z,\n    };\n    if (ABS(delta.x) > M_MAX_DIST || ABS(delta.z) > M_MAX_DIST) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->src_color.g = spark->src_color.r;\n    spark->src_color.b = spark->src_color.r;\n    spark->dst_color.r = spark->src_color.b >> 2;\n    spark->dst_color.g = spark->src_color.b >> 1;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 192;\n\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 8;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->life = (Random_GetControl() & 7) + 20;\n    spark->s_life = spark->life;\n    spark->pos.x = (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = ((Random_GetControl() & 0xFF) << 2) - 512;\n    spark->vel.y = (Random_GetControl() & 7) - 4;\n    spark->vel.z = ((Random_GetControl() & 0xFF) << 2) - 512;\n    spark->friction = 4;\n    spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ITEM | SPARK_F_SCALE;\n    spark->item_num = item_num;\n    spark->node_num = node;\n    spark->scalar = 1;\n    spark->size.width = (Random_GetControl() & 3) + 4;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 1;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 1;\n    spark->max_y_vel = 0;\n    spark->gravity = (Random_GetControl() & 3) + 4;\n    Sparks_FinishSetup(spark);\n}\n\nstatic XZ_32 M_GetDirection(const int16_t yaw)\n{\n    // clang-format off\n    switch (yaw) {\n    case 0:         return (XZ_32) { +0, +1 };\n    case DEG_90:    return (XZ_32) { +1, +0 };\n    case -DEG_90:   return (XZ_32) { -1, +0 };\n    case -DEG_180:  return (XZ_32) { +0, -1 };\n    default:        return (XZ_32) { +0, +0 };\n    }\n    // clang-format on\n}\n\nstatic bool M_IsMidSector(const ITEM *const item)\n{\n    const XZ_32 dir = M_GetDirection(item->rot.y);\n    if (dir.x != 0) {\n        return (item->pos.x & (WALL_L - 1)) == STEP_L * 2;\n    }\n    if (dir.z != 0) {\n        return (item->pos.z & (WALL_L - 1)) == STEP_L * 2;\n    }\n    return false;\n}\n\nstatic bool M_TestSector(\n    const XYZ_32 pos, const XZ_32 dir, int16_t *const room_num)\n{\n    const XYZ_32 test_pos = {\n        .x = pos.x + dir.x * WALL_L,\n        .y = pos.y,\n        .z = pos.z + dir.z * WALL_L,\n    };\n    const SECTOR *sector = Room_GetSector(test_pos, room_num);\n    const int32_t height = Room_GetHeight(sector, test_pos);\n    const ROOM *const room = Room_Get(*room_num);\n    sector = Room_GetWorldSector(room, test_pos.x, test_pos.z);\n    return height == test_pos.y && !sector->stopper;\n}\n\nstatic void M_DecideMove(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    const XZ_32 dir_ahead = M_GetDirection(item->rot.y);\n    const XZ_32 dir_left = M_GetDirection(item->rot.y - DEG_90);\n\n    int16_t room_num = item->room_num;\n    const bool move_left = M_TestSector(item->pos, dir_left, &room_num);\n\n    room_num = item->room_num;\n    const bool move_ahead = M_TestSector(item->pos, dir_ahead, &room_num);\n\n    if (!move_ahead && !move_left && p->turn > 0) {\n        item->rot.y += M_TURN;\n        p->turn = M_TURN;\n    } else if (!move_ahead && !move_left && p->turn < 0) {\n        item->rot.y -= M_TURN;\n        p->turn = -M_TURN;\n    } else if (move_left && p->turn > 0) {\n        item->rot.y -= M_TURN;\n        p->turn = -M_TURN;\n    } else {\n        p->turn = M_TURN;\n        p->resume = true;\n        item->pos.x += dir_ahead.x * p->velocity;\n        item->pos.z += dir_ahead.z * p->velocity;\n\n        XYZ_32 pos = item->pos;\n        pos.x += dir_ahead.x * WALL_L;\n        pos.z += dir_ahead.z * WALL_L;\n        const ROOM *const room = Room_Get(room_num);\n        SECTOR *const sector = Room_GetWorldSector(room, pos.x, pos.z);\n        sector->stopper = true;\n    }\n}\n\nstatic void M_HitLara(ITEM *const item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (g_Config.debug.enable_invulnerability || lara->electric != 0) {\n        return;\n    }\n\n    lara->electric = 1;\n    Lara_GetItem()->hit_points = 0;\n\n    M_PRIV *const p = item->priv;\n    p->velocity = 0;\n    Sound_Effect(SFX_CLEANER_FUSEBOX, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    if (p->velocity == 0) {\n        return;\n    }\n\n    if ((item->rot.y & 0x3FFF) != 0) {\n        item->rot.y += p->turn;\n    } else if (M_IsMidSector(item)) {\n        if (p->resume) {\n            const XYZ_32 pos =\n                XYZ_32_OffsetYaw(item->pos, item->rot.y + DEG_180, WALL_L);\n            int16_t room_num = item->room_num;\n            Room_GetSector(pos, &room_num);\n            const ROOM *const room = Room_Get(room_num);\n            SECTOR *const sector = Room_GetWorldSector(room, pos.x, pos.z);\n            sector->stopper = false;\n            p->resume = false;\n        }\n\n        M_DecideMove(item);\n\n        if (Room_TestTriggers(item)) {\n            p->velocity = 0;\n            Sound_Effect(SFX_CLEANER_FUSEBOX, &item->pos, SPM_NORMAL);\n        }\n    } else {\n        const XZ_32 dir = M_GetDirection(item->rot.y);\n        item->pos.x += dir.x * p->velocity;\n        item->pos.z += dir.z * p->velocity;\n    }\n\n    if ((item->touch_bits & M_TOUCH_BITS) != 0) {\n        M_HitLara(item);\n    }\n\n    Item_Animate(item);\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    Sound_Effect(SFX_CLEANER_LOOP, &item->pos, SPM_NORMAL);\n\n    for (int32_t i = 0; i < M_NODE_COUNT; i++) {\n        const M_SPARK_NODE *const node = &m_Nodes[i];\n        const int32_t rnd = Random_GetControl();\n        if (p->sparks[i] == 0 && (rnd & 7) != 0) {\n            continue;\n        }\n\n        if (p->sparks[i] == 0) {\n            p->sparks[i] = (Random_GetControl() & 7) + 4;\n        } else {\n            p->sparks[i]--;\n        }\n\n        XYZ_32 pos = {\n            .x = -160,\n            .y = -8,\n            .z = 16,\n        };\n        Collide_GetJointAbsPosition(item, &pos, node->joint_idx);\n        M_TriggerSparks(pos, item_num, node->node_idx);\n        pos.x += (Random_GetControl() & 0x1F) - 16;\n        pos.y += (Random_GetControl() & 0x1F) - 16;\n        pos.z += (Random_GetControl() & 0x1F) - 16;\n\n        const int32_t shade = (Random_GetControl() & 0x7F) + 128;\n        const RGB_888 color = { shade >> 2, shade >> 1, shade };\n        Output_AddDynamicLightRGB(pos, 10, color);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->radius = M_RADIUS;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_ELECTRIC_CLEANER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/common.c",
    "content": "#include <trx/game/objects/traps/common.h>\n\n#include <trx/game/game_buf.h>\n#include <trx/game/objects/types.h>\n#include <trx/game/rooms.h>\n\nvoid Trap_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    TRAP_DATA *const data = GameBuf_Alloc(sizeof(TRAP_DATA), GBUF_ITEM_DATA);\n    data->pos = item->pos;\n    data->room_num = item->room_num;\n    item->trap_data = data;\n}\n\nvoid Trap_Reset(ITEM *const item)\n{\n    const TRAP_DATA *const data = item->trap_data;\n    const int16_t item_num = Item_GetIndex(item);\n\n    item->status = IS_INACTIVE;\n    item->pos = data->pos;\n    Item_UpdateRoom(item_num, data->room_num);\n\n    item->goal_anim_state = TRAP_SET;\n    item->current_anim_state = TRAP_SET;\n    Item_SwitchToAnim(item, 0, 0);\n    item->goal_anim_state = Item_GetAnim(item)->current_anim_state;\n    item->current_anim_state = item->goal_anim_state;\n    item->required_anim_state = TRAP_SET;\n    Item_RemoveActive(item_num);\n}\n"
  },
  {
    "path": "src/trx/game/objects/traps/common.h",
    "content": "#pragma once\n\n#include <trx/game/items.h>\n\ntypedef struct TRAP_DATA {\n    XYZ_32 pos;\n    int16_t room_num;\n} TRAP_DATA;\n\nvoid Trap_Initialise(int16_t item_num);\nvoid Trap_Reset(ITEM *item);\n"
  },
  {
    "path": "src/trx/game/objects/traps/damocles_sword.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define M_ACTIVATE_DIST ((WALL_L * 3) / 2)\n#define M_DAMAGE 100\n\nstatic void M_Reset(ITEM *const item)\n{\n    item->required_anim_state = (Random_GetControl() - 0x4000) / 16;\n    item->fall_speed = 50;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(\n        (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->rot.y = Random_GetControl();\n\n    Trap_Initialise(item_num);\n    M_Reset(item);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        Trap_Reset(item);\n        M_Reset(item);\n        return;\n    }\n\n    if (item->status == IS_DEACTIVATED) {\n        return;\n    }\n\n    if (item->gravity) {\n        item->rot.y += item->required_anim_state;\n        item->fall_speed += item->fall_speed < FAST_FALL_SPEED ? GRAVITY : 1;\n        item->pos.y += item->fall_speed;\n        item->pos.x += item->current_anim_state;\n        item->pos.z += item->goal_anim_state;\n\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n        item->floor = Room_GetHeight(sector, item->pos);\n        Item_UpdateRoom(item_num, room_num);\n\n        if (item->pos.y > item->floor) {\n            Sound_Effect(SFX_DAMOCLES_SWORD, &item->pos, SPM_NORMAL);\n            item->pos.y = item->floor + 10;\n            item->gravity = false;\n            item->status = IS_DEACTIVATED;\n        }\n    } else if (item->pos.y != item->floor) {\n        item->rot.y += item->required_anim_state;\n        const ITEM *const lara_item = Lara_GetItem();\n        const int32_t x = lara_item->pos.x - item->pos.x;\n        const int32_t y = lara_item->pos.y - item->pos.y;\n        const int32_t z = lara_item->pos.z - item->pos.z;\n        if (ABS(x) <= M_ACTIVATE_DIST && ABS(z) <= M_ACTIVATE_DIST && y > 0\n            && y < WALL_L * 3) {\n            item->current_anim_state = x / 32;\n            item->goal_anim_state = z / 32;\n            item->gravity = true;\n        }\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (coll->enable_baddie_push) {\n        Lara_Col_ItemPush(item, coll, false, true);\n    }\n    if (item->gravity) {\n        lara_item->hit_points -= M_DAMAGE;\n        int32_t x = lara_item->pos.x + (Random_GetControl() - 0x4000) / 256;\n        int32_t z = lara_item->pos.z + (Random_GetControl() - 0x4000) / 256;\n        int32_t y = lara_item->pos.y - Random_GetControl() / 44;\n        int32_t d = lara_item->rot.y + (Random_GetControl() - 0x4000) / 8;\n        Spawn_Blood(x, y, z, lara_item->speed, d, lara_item->room_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->shadow_size = UNIT_SHADOW;\n    obj->save_position = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_DAMOCLES_SWORD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/dart.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_DART_DAMAGE 50\n#define M_POISON_DART_DAMAGE 25\n#define M_POISON_AMOUNT 160\n#define M_PITCH (DEG_45 / 2)\n\ntypedef struct {\n    bool pending_kill;\n} M_PRIV;\n\nstatic void M_DamageLara(const ITEM *const item)\n{\n    const bool is_poison = item->object_id == O_POISON_DART;\n    const int32_t damage = is_poison ? M_POISON_DART_DAMAGE : M_DART_DAMAGE;\n    Lara_TakeDamage(damage, true);\n    if (is_poison) {\n        LARA_INFO *const lara = Lara_GetLaraInfo();\n        lara->poison_timer += M_POISON_AMOUNT;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    Spawn_Blood(\n        item->pos.x, item->pos.y, item->pos.z, lara_item->speed,\n        lara_item->rot.y, lara_item->room_num);\n}\n\nstatic void M_Hit(\n    const int16_t item_num, const XYZ_32 pos, const int16_t old_room_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n\n    if (item->object_id == O_POISON_DART) {\n        for (int32_t i = 0; i < 4; i++) {\n            Sparks_TriggerDartSmoke(pos, (XZ_32) {}, true);\n        }\n        ITEM *const poison_item = Item_Get(item_num);\n        M_PRIV *const p = poison_item->priv;\n        p->pending_kill = true;\n        Item_UpdateRoom(item_num, old_room_num);\n        return;\n    }\n\n    Item_Kill(item_num);\n    Sound_Effect(SFX_PROJECTILE_HIT, &item->pos, SPM_NORMAL);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_RICOCHET;\n        effect->pos = pos;\n        effect->rot = item->rot;\n        effect->counter = 6;\n        effect->frame_num = -3 * Random_GetControl() / 0x8000;\n    }\n}\n\nstatic void M_Animate(ITEM *const item)\n{\n    if (item->object_id != O_POISON_DART) {\n        Item_Animate(item);\n        return;\n    }\n\n    const int32_t speed = (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT;\n    item->pos.x += (speed * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n    item->pos.z += (speed * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->object_id == O_POISON_DART) {\n        M_PRIV *const p = item->priv;\n        if (p->pending_kill) {\n            Item_Kill(item_num);\n            return;\n        }\n    }\n\n    if (item->touch_bits != 0) {\n        M_DamageLara(item);\n        if (item->object_id == O_POISON_DART) {\n            Item_Kill(item_num);\n            return;\n        }\n    }\n\n    const GAME_VECTOR old_pos = { .pos = item->pos,\n                                  .room_num = item->room_num };\n    M_Animate(item);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    if (item->object_id == O_DISC) {\n        item->rot.x += M_PITCH;\n    }\n    item->floor = Room_GetHeight(sector, item->pos);\n\n    const GAME_VECTOR new_pos = { .pos = item->pos,\n                                  .room_num = item->room_num };\n    if (item->pos.y >= item->floor) {\n        M_Hit(\n            item_num, Spawn_GetRayPos(old_pos, new_pos, STEP_L / 12),\n            old_pos.room_num);\n    }\n}\n\nstatic bool M_DrawPoisonDart(const ITEM *const item)\n{\n    const XYZ_32 origin = item->interp.result.pos;\n    const XYZ_16 rot = item->interp.result.rot;\n    const int32_t size = (-96 * Math_Cos(rot.x)) >> W2V_SHIFT;\n    const XYZ_32 to = {\n        .x = origin.x + ((size * Math_Sin(rot.y)) >> W2V_SHIFT),\n        .y = origin.y + ((96 * Math_Sin(rot.x)) >> W2V_SHIFT),\n        .z = origin.z + ((size * Math_Cos(rot.y)) >> W2V_SHIFT),\n    };\n    const RGBA_8888 color = { 0x78, 0x3C, 0x14, 0xFF };\n    OutputSource_PolyFX_StageLineSegment(\n        origin, color, to, color, 2.0f, DRAW_BLEND);\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->save_flags = true;\n}\n\nstatic void M_SetupPoisonDart(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->draw_func = M_DrawPoisonDart;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_DART, M_Setup)\nREGISTER_OBJECT(O_DISC, M_Setup)\nREGISTER_OBJECT(O_POISON_DART, M_SetupPoisonDart)\n"
  },
  {
    "path": "src/trx/game/objects/traps/dart_emitter.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/log.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n#define M_POISON_FIRE_TIMER 24\n\ntypedef enum {\n    // clang-format off\n    STATE_IDLE   = 0,\n    STATE_FIRE   = 1,\n    STATE_RELOAD = 2,\n    // clang-format on\n} M_STATE;\n\ntypedef struct {\n    int32_t fire_timer;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"fire_timer\", &p->fire_timer));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"fire_timer\", p->fire_timer);\n}\n\nstatic OBJECT_ID M_GetProjectileObjectID(const OBJECT_ID emitter_id)\n{\n    switch (emitter_id) {\n    case O_DART_EMITTER:\n        return O_DART;\n    case O_DISC_EMITTER:\n        return O_DISC;\n    case O_POISON_DART_EMITTER:\n        return O_POISON_DART;\n    default:\n        return NO_OBJECT;\n    }\n}\n\nstatic void M_TriggerPoisonDartSmoke(\n    const ITEM *const item, const int32_t x, const int32_t z)\n{\n    const int32_t x_limit = x != 0 ? ABS(x << 1) - 1 : 0;\n    const int32_t z_limit = x == 0 ? ABS(z << 1) - 1 : 0;\n    for (int32_t i = 0; i < 5; i++) {\n        const int32_t rnd = -Random_GetControl();\n        const XZ_32 vel = {\n            .x = x >= 0 ? (x_limit & rnd) : -(x_limit & rnd),\n            .z = z >= 0 ? (z_limit & rnd) : -(z_limit & rnd),\n        };\n        Sparks_TriggerDartSmoke(item->pos, vel, false);\n    }\n}\n\nstatic void M_CreateProjectile(const ITEM *const item)\n{\n    const OBJECT_ID projectile_obj_id =\n        M_GetProjectileObjectID(item->object_id);\n    if (!Object_Get(projectile_obj_id)->loaded) {\n        LOG_ERROR(\n            \"Projectile object not loaded for item #%d\", Item_GetIndex(item));\n        return;\n    }\n\n    const int16_t projectile_item_num = Item_Create();\n    if (projectile_item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const projectile_item = Item_Get(projectile_item_num);\n    projectile_item->object_id = projectile_obj_id;\n    projectile_item->room_num = item->room_num;\n    projectile_item->shade.value_1 = -1;\n    projectile_item->rot.y = item->rot.y;\n    projectile_item->pos.y = item->pos.y - 512;\n\n    const bool is_poison = item->object_id == O_POISON_DART_EMITTER;\n    const int32_t wall_inset = is_poison ? 0 : 100;\n\n    int32_t x = 0;\n    int32_t z = 0;\n    switch (projectile_item->rot.y) {\n    case 0:\n        z = (is_poison ? 1 : -1) * (WALL_L / 2 - wall_inset);\n        break;\n    case DEG_90:\n        x = (is_poison ? 1 : -1) * (WALL_L / 2 - wall_inset);\n        break;\n    case -DEG_180:\n        z = (is_poison ? -1 : 1) * (WALL_L / 2 - wall_inset);\n        break;\n    case -DEG_90:\n        x = (is_poison ? -1 : 1) * (WALL_L / 2 - wall_inset);\n        break;\n    }\n\n    projectile_item->pos.x = item->pos.x + x;\n    projectile_item->pos.z = item->pos.z + z;\n    Item_Initialise(projectile_item_num);\n    Item_AddActive(projectile_item_num);\n    projectile_item->status = IS_ACTIVE;\n\n    if (is_poison) {\n        projectile_item->rot.y += DEG_180;\n        projectile_item->speed = STEP_L;\n        M_TriggerPoisonDartSmoke(projectile_item, x, z);\n        Sound_Effect(SFX_BLOWPIPE_BLOW, &projectile_item->pos, SPM_NORMAL);\n    } else if (item->object_id == O_DART_EMITTER) {\n        const int16_t effect_num = Effect_Create(projectile_item->room_num);\n        if (effect_num != NO_EFFECT) {\n            EFFECT *const effect = Effect_Get(effect_num);\n            effect->pos = projectile_item->pos;\n            effect->rot = projectile_item->rot;\n            effect->speed = 0;\n            effect->frame_num = 0;\n            effect->counter = 0;\n            effect->object_id = O_DART_EFFECT;\n        }\n        Sound_Effect(SFX_DART, &projectile_item->pos, SPM_NORMAL);\n    } else {\n        Sound_Effect(SFX_DISC, &projectile_item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item)) {\n        if (item->current_anim_state == STATE_IDLE) {\n            item->goal_anim_state = STATE_FIRE;\n        }\n    } else if (item->current_anim_state == STATE_FIRE) {\n        item->goal_anim_state = STATE_IDLE;\n    }\n\n    if (item->current_anim_state == STATE_FIRE\n        && Item_TestFrameEqual(item, 0)) {\n        M_CreateProjectile(item);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_ControlPoisonEmitter(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (!Item_IsTriggerActive(item)) {\n        p->fire_timer = 0;\n        return;\n    }\n\n    if (p->fire_timer > 0) {\n        p->fire_timer--;\n        return;\n    }\n\n    p->fire_timer = M_POISON_FIRE_TIMER;\n    M_CreateProjectile(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n}\n\nstatic void M_SetupPoisonEmitter(OBJECT *const obj)\n{\n    obj->draw_func = nullptr;\n    obj->control_func = M_ControlPoisonEmitter;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_DART_EMITTER, M_Setup)\nREGISTER_OBJECT(O_DISC_EMITTER, M_Setup)\nREGISTER_OBJECT(O_POISON_DART_EMITTER, M_SetupPoisonEmitter)\n"
  },
  {
    "path": "src/trx/game/objects/traps/dying_monk.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/game.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n#define MAX_ROOMIES 2\n\ntypedef struct {\n    int32_t roomies[MAX_ROOMIES];\n} M_PRIV;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    for (int32_t i = 0; i < MAX_ROOMIES; i++) {\n        p->roomies[i] = NO_ITEM;\n    }\n\n    int32_t roomie_count = 0;\n    int16_t test_item_num = Room_Get(item->room_num)->item_num;\n    while (test_item_num != NO_ITEM) {\n        const ITEM *const test_item = Item_Get(test_item_num);\n        const OBJECT *const test_obj = Object_Get(test_item->object_id);\n        if (test_obj->intelligent) {\n            p->roomies[roomie_count++] = test_item_num;\n            if (roomie_count >= MAX_ROOMIES) {\n                break;\n            }\n        }\n        test_item_num = test_item->next_item;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const M_PRIV *const p = item->priv;\n\n    for (int32_t i = 0; i < MAX_ROOMIES; i++) {\n        int32_t test_item_num = p->roomies[i];\n        if (test_item_num != NO_ITEM) {\n            const ITEM *const test_item = Item_Get(test_item_num);\n            if (test_item->hit_points > 0) {\n                return;\n            }\n        }\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = ABS(lara_item->pos.x - item->pos.x);\n    const int32_t dz = ABS(lara_item->pos.z - item->pos.z);\n    if (dx < WALL_L && dz < WALL_L && !lara_item->gravity\n        && lara_item->pos.y == item->pos.y) {\n        Game_SetIsLevelComplete(true);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_DYING_MONK, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/electric_fence.c",
    "content": "#include <trx/core/log.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\n#define M_EPSILON 32\n\ntypedef struct {\n    bool is_initialised;\n    bool is_flat;\n} M_PRIV;\n\nstatic bool M_IsFenceOnDeathSector(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *sector = Room_GetSector(item->pos, &room_num);\n    return sector->is_death_sector;\n}\n\nstatic void M_TriggerFenceSparks(const XYZ_32 pos, const bool kill)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.b = (Random_GetControl() & 63) + 192;\n    spark->src_color.r = spark->src_color.b;\n    spark->src_color.g = spark->src_color.b;\n    spark->dst_color.b = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.r = spark->dst_color.b >> 2;\n    spark->dst_color.g = spark->dst_color.b >> 1; // OG: 1\n\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 16;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = 32 + (Random_GetControl() & 7);\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n\n    spark->pos = pos;\n    spark->vel.x = ((Random_GetControl() & 0xFF) << 1) - 256;\n    spark->vel.y = (Random_GetControl() & 0xF) - ((int32_t)kill << 5) - 8;\n    spark->vel.z = ((Random_GetControl() & 0xFF) << 1) - 256;\n\n    spark->friction = 4;\n    spark->flags = SPARK_F_SCALE;\n    spark->scalar = 1 + kill;\n    spark->max_y_vel = 0;\n    spark->gravity = 16 + (Random_GetControl() & 0xF);\n\n    spark->size.width = 4 + (Random_GetControl() & 3);\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = 1;\n\n    spark->size.height = spark->size.width * 2;\n    spark->src_size.height = spark->src_size.width * 2;\n    spark->dst_size.height = spark->dst_size.width * 2;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TouchFence(const XZ_32 spark_axis, XYZ_32 spark_pos)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    const XYZ_32 old_spark_pos = spark_pos;\n    const int32_t iterations = (Random_GetControl() & 0xF) + 3;\n\n    for (int32_t i = 0; i < iterations; i++) {\n        if (spark_axis.x != 0) {\n            spark_pos.x =\n                lara_item->pos.x + (Random_GetControl() & 0x1FF) - 256;\n        } else {\n            spark_pos.z =\n                lara_item->pos.z + (Random_GetControl() & 0x1FF) - 256;\n        }\n\n        spark_pos.y = lara_item->pos.y - Random_GetControl() % 768;\n        const int32_t spark_count = (Random_GetControl() & 3) + 6;\n\n        for (int32_t j = 0; j < spark_count; j++) {\n            M_TriggerFenceSparks(spark_pos, true);\n            if (spark_axis.x != 0) {\n                spark_pos.x += (spark_axis.x & Random_GetControl() & 7) - 4;\n            } else {\n                spark_pos.z += (spark_axis.z & Random_GetControl() & 7) - 4;\n            }\n            spark_pos.y += (Random_GetControl() & 7) - 4;\n        }\n\n        spark_pos = old_spark_pos;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->electric = 1;\n    lara_item->hit_points = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if (dx < -0x5000 || dx > 0x5000 || dz < -0x5000 || dz > 0x5000) {\n        return;\n    }\n\n    XYZ_32 fence_center = {};\n    XYZ_32 spark_pos = {};\n    XZ_32 spark_axis = {};\n    XZ_32 fence_size = {};\n\n    M_PRIV *const p = item->priv;\n    if (!p->is_initialised) {\n        p->is_initialised = true;\n        p->is_flat = M_IsFenceOnDeathSector(item);\n    }\n\n    switch (item->rot.y) {\n    case 0:\n        fence_center.x = item->pos.x + WALL_L / 2;\n        fence_center.z = item->pos.z + WALL_L / 2;\n        fence_size.x = WALL_L + M_EPSILON;\n        fence_size.z = 128;\n        spark_pos.x = fence_center.x - (WALL_L - M_EPSILON);\n        spark_pos.z = fence_center.z;\n        if (p->is_flat) {\n            spark_axis.x = Random_GetControl() & 0x3FF;\n            spark_pos.z += spark_axis.x * -(Random_GetControl() & 1);\n        }\n        spark_axis.x = WALL_L;\n        spark_axis.z = 0;\n        break;\n\n    case -DEG_90:\n        fence_center.x = item->pos.x - WALL_L / 2;\n        fence_center.z = item->pos.z + WALL_L / 2;\n        fence_size.x = 128;\n        fence_size.z = WALL_L + M_EPSILON;\n        spark_pos.x = fence_center.x;\n        spark_pos.z = fence_center.z - (WALL_L - M_EPSILON);\n        if (p->is_flat) {\n            spark_axis.x = Random_GetControl() & 0x3FF;\n            spark_pos.x += spark_axis.x * -(Random_GetControl() & 1);\n        }\n        spark_axis.x = 0;\n        spark_axis.z = WALL_L;\n        break;\n\n    case -DEG_180:\n        fence_center.x = item->pos.x - WALL_L / 2;\n        fence_center.z = item->pos.z - WALL_L / 2;\n        fence_size.x = WALL_L + M_EPSILON;\n        fence_size.z = 128;\n        spark_pos.x = fence_center.x - (WALL_L - M_EPSILON);\n        spark_pos.z = fence_center.z;\n        if (p->is_flat) {\n            spark_axis.x = Random_GetControl() & 0x3FF;\n            spark_pos.z += spark_axis.x * -(Random_GetControl() & 1);\n        }\n        spark_axis.x = WALL_L;\n        spark_axis.z = 0;\n        break;\n\n    case DEG_90:\n        fence_center.x = item->pos.x + WALL_L / 2;\n        fence_center.z = item->pos.z - WALL_L / 2;\n        fence_size.x = 128;\n        fence_size.z = WALL_L + M_EPSILON;\n        spark_pos.x = fence_center.x;\n        spark_pos.z = fence_center.z - (WALL_L - M_EPSILON);\n        if (p->is_flat) {\n            spark_axis.x = Random_GetControl() & 0x3FF;\n            spark_pos.x += spark_axis.x * -(Random_GetControl() & 1);\n        }\n        spark_axis.x = 0;\n        spark_axis.z = WALL_L;\n        break;\n\n    default:\n        break;\n    }\n\n    if ((Random_GetControl() & 0x1F) == 0) {\n        if (spark_axis.x != 0) {\n            spark_pos.x += Random_GetControl() & spark_axis.x;\n        } else {\n            spark_pos.z += Random_GetControl() & spark_axis.z;\n        }\n\n        if (p->is_flat) {\n            spark_pos.y = item->pos.y - (Random_GetControl() & 0x1F);\n        } else {\n            spark_pos.y = item->pos.y - (Random_GetControl() & 0x7FF)\n                - (Random_GetControl() & 0x3FF);\n        }\n\n        const int32_t spark_count = (Random_GetControl() & 3) + 3;\n        for (int32_t i = 0; i < spark_count; i++) {\n            M_TriggerFenceSparks(spark_pos, false);\n\n            if (spark_axis.x != 0) {\n                spark_pos.x += (spark_axis.x & Random_GetControl() & 7) - 4;\n            } else {\n                spark_pos.z += (spark_axis.z & Random_GetControl() & 7) - 4;\n            }\n\n            spark_pos.y += (Random_GetControl() & 7) - 4;\n        }\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->electric != 0 || p->is_flat\n        || lara_item->pos.x < fence_center.x - fence_size.x\n        || lara_item->pos.x > fence_center.x + fence_size.x\n        || lara_item->pos.z < fence_center.z - fence_size.z\n        || lara_item->pos.z > fence_center.z + fence_size.z\n        || lara_item->pos.y > item->pos.y + M_EPSILON\n        || lara_item->pos.y < item->pos.y - 3 * WALL_L) {\n        return;\n    }\n\n    M_TouchFence(spark_axis, spark_pos);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_ELECTRIC_FENCE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/ember_emitter.c",
    "content": "#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        item->status = IS_INACTIVE;\n        Item_RemoveActive(item_num);\n        return;\n    }\n\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = item->pos;\n    effect->rot.y = 2 * Random_GetControl() + 0x8000;\n    effect->speed = Random_GetControl() >> 10;\n    effect->fall_speed = Random_GetControl() / -200;\n    effect->frame_num = (-4 * Random_GetControl()) / 0x7FFF;\n    effect->object_id = O_EMBER;\n    Sound_Effect(SFX_LAVA_FOUNTAIN, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_EMBER_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/falling_block.c",
    "content": "#include <trx/core/vector.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/objects/traps/movable_block.h>\n#include <trx/game/rooms.h>\n\ntypedef struct {\n    bool heavy_triggered;\n    int32_t origin;\n} M_PRIV;\n\nstatic int32_t M_GetOrigin(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->origin;\n}\n\nstatic void M_CalculateOrigin(ITEM *const item)\n{\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const ANIM *const anim = Object_GetAnim(obj, 0);\n    const ANIM_FRAME *const frame = &anim->frame_ptr[0];\n    M_PRIV *const p = item->priv;\n    p->origin = ROUND_TO_CLICK_SIGNED(frame->offset.y);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Trap_Initialise(item_num);\n    ITEM *const item = Item_Get(item_num);\n    M_CalculateOrigin(item);\n    Walkable_AllocateNodes(item, 1);\n}\n\nstatic void M_DropStack(const ITEM *const item)\n{\n    const int32_t origin = M_GetOrigin(item);\n    const XYZ_32 drop_pos = {\n        .x = item->pos.x,\n        .y = item->pos.y + origin,\n        .z = item->pos.z,\n    };\n    MovableBlock_DropStack(drop_pos, item->room_num);\n}\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    const int32_t origin = M_GetOrigin(item);\n    if (y <= item->pos.y + origin\n        && (item->current_anim_state == TRAP_SET\n            || item->current_anim_state == TRAP_ACTIVATE)) {\n        return item->pos.y + origin;\n    }\n    return height;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    const int32_t origin = M_GetOrigin(item);\n    if (y > item->pos.y + origin\n        && (item->current_anim_state == TRAP_SET\n            || item->current_anim_state == TRAP_ACTIVATE)) {\n        return item->pos.y + origin + STEP_L;\n    }\n    return height;\n}\n\nstatic void M_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    Walkable_Add(item_num, item->pos);\n}\n\nstatic bool M_Trigger(ITEM *const item, const TRIGGER *const trigger)\n{\n    M_PRIV *const p = item->priv;\n    p->heavy_triggered = trigger->type == TT_HEAVY;\n    return true;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const int32_t origin = M_GetOrigin(item);\n\n    switch (item->current_anim_state) {\n    case TRAP_SET:\n        const ITEM *const lara_item = Lara_GetItem();\n        M_PRIV *const p = item->priv;\n        if (!p->heavy_triggered && lara_item->pos.y != item->pos.y + origin) {\n            item->status = IS_INACTIVE;\n            Item_RemoveActive(item_num);\n            return;\n        }\n        item->goal_anim_state = TRAP_ACTIVATE;\n        p->heavy_triggered = false;\n        break;\n\n    case TRAP_ACTIVATE:\n        item->goal_anim_state = TRAP_WORKING;\n        break;\n\n    case TRAP_WORKING:\n        if (item->goal_anim_state != TRAP_FINISHED) {\n            if (!item->gravity) {\n                M_DropStack(item);\n            }\n            item->gravity = true;\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    Item_Animate(item);\n    if (item->status == IS_DEACTIVATED) {\n        if (!Item_IsTriggerActive(item)) {\n            Trap_Reset(item);\n        }\n        return;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    item->floor = Room_GetHeight(sector, item->pos);\n\n    if (item->current_anim_state == TRAP_WORKING\n        && item->pos.y >= item->floor) {\n        item->goal_anim_state = TRAP_FINISHED;\n        item->pos.y = item->floor;\n        item->fall_speed = 0;\n        item->gravity = false;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->trigger_func = M_Trigger;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->add_walkable_func = M_AddWalkable;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_FALLING_BLOCK_1, M_Setup)\nREGISTER_OBJECT(O_FALLING_BLOCK_2, M_Setup)\nREGISTER_OBJECT(O_FALLING_BLOCK_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/falling_ceiling.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/rooms.h>\n\n#define M_DAMAGE 300\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->current_anim_state == TRAP_SET) {\n        item->goal_anim_state = TRAP_ACTIVATE;\n        item->gravity = true;\n    } else if (\n        item->current_anim_state == TRAP_ACTIVATE && item->touch_bits != 0) {\n        Lara_TakeDamage(M_DAMAGE, true);\n    }\n\n    Item_Animate(item);\n    if (item->status == IS_DEACTIVATED) {\n        if (!Item_IsTriggerActive(item)) {\n            Trap_Reset(item);\n        }\n        return;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n\n    item->floor = Room_GetHeight(sector, item->pos);\n    Item_UpdateRoom(item_num, room_num);\n\n    if (item->current_anim_state == TRAP_ACTIVATE\n        && item->pos.y >= item->floor) {\n        item->pos.y = item->floor;\n        item->goal_anim_state = TRAP_WORKING;\n        item->fall_speed = 0;\n        item->gravity = false;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = Trap_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision_Trap;\n    obj->save_position = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_FALLING_CEILING_1, M_Setup)\nREGISTER_OBJECT(O_FALLING_CEILING_2, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/fire_head.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n\n// clang-format off\n#define M_MIN_FALLOFF 8\n#define M_MAX_RANGE   (WALL_L * 2) // = 2048\n#define M_RANGE_STEP  (STEP_L / 8) // = 32\n#define M_SPEED_STEP  (STEP_L / 4) // = 64\n// clang-format on\n\ntypedef enum {\n    M_STATE_IDLE,\n    M_STATE_REAR,\n    M_STATE_BLOW,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_REAR = 1,\n} M_ANIM;\n\ntypedef struct {\n    struct {\n        int32_t max;\n        int32_t current;\n    } blow_loops;\n    int32_t speed;\n    int32_t deadly_range;\n    bool stop;\n} M_PRIV;\n\nstatic const BITE m_Mouth = {\n    .pos = { .x = 0, .y = 128, .z = 0 },\n    .mesh_num = 7,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    if (JSON_SHOULD(JSON_PUSH(io, \"blow_loops\"))) {\n        JSON_SHOULD(JSON_READ(io, \"max\", &p->blow_loops.max));\n        JSON_SHOULD(JSON_READ(io, \"current\", &p->blow_loops.current));\n        JSON_POP(io);\n    }\n    JSON_SHOULD(JSON_READ(io, \"speed\", &p->speed));\n    JSON_SHOULD(JSON_READ(io, \"deadly_range\", &p->deadly_range));\n    JSON_SHOULD(JSON_READ(io, \"stop\", &p->stop));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"max\", p->blow_loops.max);\n    JSONW_WRITE(io, \"current\", p->blow_loops.current);\n    JSONW_POP_AND_SET(io, \"blow_loops\");\n\n    JSONW_WRITE(io, \"speed\", p->speed);\n    JSONW_WRITE(io, \"deadly_range\", p->deadly_range);\n    JSONW_WRITE(io, \"stop\", p->stop);\n}\n\nstatic void M_TriggerFlame(\n    const XYZ_32 pos, const int32_t angle, const int32_t speed)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.g = spark->src_color.r;\n    spark->src_color.b = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 28;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n\n    const int32_t dist = speed - (Random_GetControl() % ((speed >> 3) + 1));\n    spark->vel.x =\n        ((dist * Math_Sin(angle)) >> 13) + (Random_GetControl() & 0x7F) - 64;\n    spark->vel.y = (Random_GetControl() & 7) + 6;\n    spark->vel.z =\n        ((dist * Math_Cos(angle)) >> 13) + (Random_GetControl() & 0x7F) - 64;\n\n    spark->friction = 4;\n    spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags |= SPARK_F_ROTATE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n        spark->rot_add = (Random_GetControl() & 0x3F) - 32;\n    }\n\n    spark->gravity = -8 - (Random_GetControl() & 0xF);\n    spark->max_y_vel = -8 - (Random_GetControl() & 7);\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n    spark->dst_size.width = (speed >> 4) + (Random_GetControl() & 0xF);\n    spark->size.width = spark->dst_size.width >> 2;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.height = spark->dst_size.width;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    Item_SwitchToAnim(item, M_ANIM_REAR, 0);\n    item->current_anim_state = M_STATE_REAR;\n    item->goal_anim_state = M_STATE_REAR;\n}\n\nstatic bool M_Trigger(ITEM *const item, const TRIGGER *const trigger)\n{\n    M_PRIV *const p = item->priv;\n    if (p == nullptr) {\n        return true;\n    }\n\n    if (trigger == nullptr || trigger->type == TT_ANTITRIGGER\n        || trigger->type == TT_ANTIPAD) {\n        return true;\n    }\n\n    item->timer = 0;\n    p->blow_loops.max = trigger->timer;\n    return true;\n}\n\nstatic void M_Reset(M_PRIV *const p)\n{\n    p->blow_loops.current = p->blow_loops.max;\n    p->speed = 0;\n    p->deadly_range = 0;\n    p->stop = false;\n}\n\nstatic bool M_TestFireRange(const ITEM *const item, const XYZ_32 pos)\n{\n    // Originally, item->pos.y was used as the baseline here, but this fails for\n    // the alternate model used in the gold expansion. The following produces\n    // the same baseline as the original base game.\n    const XYZ_32 lara_pos = Lara_GetItem()->pos;\n    const int32_t y_pos = ROUND_TO_CLICK_UP(pos.y);\n    if (lara_pos.y <= y_pos - (WALL_L / 2)\n        || lara_pos.y >= y_pos + (STEP_L * 3)) {\n        return false;\n    }\n\n    const M_PRIV *const p = item->priv;\n    const int32_t forward_range = p->deadly_range;\n    const int32_t side_range = WALL_L / 2;\n    XZ_32 min = { pos.x, pos.z };\n    XZ_32 max = { pos.x, pos.z };\n\n    const DIRECTION dir = Math_GetDirection(item->rot.y);\n    switch (dir) {\n    case DIR_NORTH:\n        max.x += forward_range;\n        min.z -= side_range;\n        max.z += side_range;\n        break;\n    case DIR_EAST:\n        min.x -= side_range;\n        max.x += side_range;\n        min.z -= forward_range;\n        break;\n    case DIR_SOUTH:\n        min.x -= forward_range;\n        min.z -= side_range;\n        max.z += side_range;\n        break;\n    case DIR_WEST:\n        min.x -= side_range;\n        max.x += side_range;\n        max.z += forward_range;\n        break;\n    default:\n        break;\n    }\n\n    return lara_pos.x > min.x && lara_pos.x < max.x && lara_pos.z > min.z\n        && lara_pos.z < max.z;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    if (item->current_anim_state == M_STATE_IDLE) {\n        item->goal_anim_state = M_STATE_REAR;\n        Item_Animate(item);\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    const RGB_888 light_color = {\n        .r = (Random_GetControl() & 0x3F) + 192,\n        .g = (Random_GetControl() & 0x1F) + 96,\n        .b = 0,\n    };\n\n    if (item->current_anim_state == M_STATE_REAR) {\n        XYZ_32 pos = m_Mouth.pos;\n        Collide_GetJointAbsPosition(item, &pos, m_Mouth.mesh_num);\n        Output_AddDynamicLightRGB(pos, M_MIN_FALLOFF, light_color);\n        M_Reset(p);\n    } else {\n        XYZ_32 pos = {};\n        Collide_GetJointAbsPosition(item, &pos, m_Mouth.mesh_num);\n\n        if (p->stop) {\n            if (p->speed != 0) {\n                p->speed -= M_SPEED_STEP;\n            }\n\n            p->deadly_range -= M_RANGE_STEP;\n            CLAMPL(p->deadly_range, 0);\n        } else {\n            if (p->speed < M_MAX_RANGE) {\n                p->speed += M_SPEED_STEP;\n            }\n\n            if (p->deadly_range < M_MAX_RANGE) {\n                p->deadly_range += M_RANGE_STEP;\n            }\n\n            Sound_Effect(SFX_FLAME_THROWER_LOOP, &item->pos, SPM_NORMAL);\n        }\n\n        p->blow_loops.current--;\n        CLAMPL(p->blow_loops.current, 0);\n\n        const int32_t time4 = Output_GetTimeInGame() * 4;\n        if ((time4 & 4) != 0) {\n            M_TriggerFlame(pos, item->rot.y + DEG_90, p->speed);\n        }\n\n        if (p->blow_loops.current == 0 && !p->stop\n            && Item_TestFrameEqual(item, 0)) {\n            p->stop = true;\n            item->goal_anim_state = M_STATE_REAR;\n        }\n\n        Output_AddDynamicLightRGB(\n            pos, (p->speed >> 7) + M_MIN_FALLOFF, light_color);\n\n        const LARA_INFO *const lara = Lara_GetLaraInfo();\n        if (!lara->burn && M_TestFireRange(item, pos)) {\n            Lara_CatchFire();\n        }\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = Object_Collision;\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->trigger_func = M_Trigger;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_FIRE_HEAD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/flame_emitter.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/effects.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/effects/flame.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\ntypedef struct {\n    int16_t effect_num;\n} M_PRIV;\n\ntypedef void (*FLAME_INIT_FUNC)(EFFECT *const effect, const ITEM *const item);\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"fx_num\", Effect_GetInOrderNum(p->effect_num));\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    if (!g_Config.gameplay.enable_enhanced_saves) {\n        return;\n    }\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"fx_num\", &p->effect_num));\n}\n\nstatic void M_KillIfAlive(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    if (p->effect_num == NO_EFFECT) {\n        return;\n    }\n\n    Effect_Kill(p->effect_num);\n    p->effect_num = NO_EFFECT;\n\n    if (g_TRVersion == 1) {\n        Sound_StopEffect(SFX_LOOP_FOR_SMALL_FIRES);\n    }\n}\n\nstatic int16_t M_Spawn(ITEM *const item, const FLAME_INIT_FUNC init_func)\n{\n    M_PRIV *const p = item->priv;\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->pos = item->pos;\n        effect->object_id = O_FLAME;\n        effect->counter = 0;\n        init_func(effect, item);\n    }\n    return effect_num;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->effect_num = NO_EFFECT;\n}\n\nstatic void M_ControlCommon(\n    const int16_t item_num, const FLAME_INIT_FUNC init_func)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    if (!Item_IsTriggerActive(item)) {\n        M_KillIfAlive(item);\n    } else if (p->effect_num == NO_EFFECT) {\n        p->effect_num = M_Spawn(item, init_func);\n    }\n}\n\nstatic void M_InitDefault(EFFECT *const effect, const ITEM *const item)\n{\n    effect->frame_num = 0;\n}\n\nstatic void M_InitBig(EFFECT *const effect, const ITEM *const item)\n{\n    effect->rot.y = item->rot.y;\n    effect->frame_num = FLAME_BIG;\n}\n\nstatic void M_InitSmall(EFFECT *const effect, const ITEM *const item)\n{\n    effect->rot.y = item->rot.y;\n    effect->frame_num = FLAME_SMALL;\n}\n\nstatic void M_InitJet(EFFECT *const effect, const ITEM *const item)\n{\n    effect->rot.y = item->rot.y;\n    effect->frame_num = FLAME_JET;\n    effect->flag1 = 0;\n    effect->flag2 = Random_GetControl() & 0x3F;\n}\n\nstatic void M_InitSide(EFFECT *const effect, const ITEM *const item)\n{\n    effect->rot.y = item->rot.y;\n    effect->frame_num = FLAME_SIDE;\n    effect->flag1 = 0;\n    effect->flag2 = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    M_ControlCommon(item_num, M_InitDefault);\n}\nstatic void M_ControlBig(const int16_t item_num)\n{\n    M_ControlCommon(item_num, M_InitBig);\n}\nstatic void M_ControlSmall(const int16_t item_num)\n{\n    M_ControlCommon(item_num, M_InitSmall);\n}\nstatic void M_ControlJet(const int16_t item_num)\n{\n    M_ControlCommon(item_num, M_InitJet);\n}\nstatic void M_ControlSide(const int16_t item_num)\n{\n    M_ControlCommon(item_num, M_InitSide);\n}\n\nstatic void M_SetupCommon(OBJECT *const obj, void (*control_func)(int16_t))\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = control_func;\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    M_SetupCommon(obj, M_Control);\n}\nstatic void M_SetupBig(OBJECT *const obj)\n{\n    M_SetupCommon(obj, M_ControlBig);\n}\nstatic void M_SetupSmall(OBJECT *const obj)\n{\n    M_SetupCommon(obj, M_ControlSmall);\n}\nstatic void M_SetupJet(OBJECT *const obj)\n{\n    M_SetupCommon(obj, M_ControlJet);\n}\nstatic void M_SetupSide(OBJECT *const obj)\n{\n    M_SetupCommon(obj, M_ControlSide);\n}\n\nREGISTER_OBJECT(O_FLAME_EMITTER, M_Setup)\nREGISTER_OBJECT(O_FLAME_EMITTER_BIG, M_SetupBig)\nREGISTER_OBJECT(O_FLAME_EMITTER_SMALL, M_SetupSmall)\nREGISTER_OBJECT(O_FLAME_EMITTER_JET, M_SetupJet)\nREGISTER_OBJECT(O_FLAME_EMITTER_SIDE, M_SetupSide)\n"
  },
  {
    "path": "src/trx/game/objects/traps/gondola.c",
    "content": "#include <trx/game/objects/traps/gondola.h>\n\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\n#define M_SINK_SPEED 50\n#define M_SINK_ROOM_SHIFT (STEP_L * 3 / 2)\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    switch (item->current_anim_state) {\n    case GONDOLA_STATE_FLOATING:\n        if (item->goal_anim_state == GONDOLA_STATE_CRASH) {\n            item->mesh_bits = 0xFF;\n            Item_Explode(item_num, 240, 0);\n        }\n        break;\n\n    case GONDOLA_STATE_SINK: {\n        item->pos.y = item->pos.y + M_SINK_SPEED;\n        const ANIM_FRAME *const frame = Item_GetBestFrame(item);\n        const int16_t room_shift = frame->bounds.min.y + M_SINK_ROOM_SHIFT;\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(\n            (XYZ_32) { item->pos.x, item->pos.y + room_shift, item->pos.z },\n            &room_num);\n        item->floor = Room_GetHeight(sector, item->pos);\n        Item_UpdateRoom(item_num, room_num);\n\n        if (item->pos.y >= item->floor) {\n            item->goal_anim_state = GONDOLA_STATE_LAND;\n            item->pos.y = item->floor;\n        }\n        break;\n    }\n    }\n\n    Item_Animate(item);\n\n    if (item->status == IS_DEACTIVATED) {\n        Item_RemoveActive(item_num);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n    obj->save_position = true;\n}\n\nREGISTER_OBJECT(O_GONDOLA, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/gondola.h",
    "content": "#pragma once\n\ntypedef enum {\n    GONDOLA_STATE_EMPTY = 0,\n    GONDOLA_STATE_FLOATING = 1,\n    GONDOLA_STATE_CRASH = 2,\n    GONDOLA_STATE_SINK = 3,\n    GONDOLA_STATE_LAND = 4,\n} GONDOLA_STATE;\n"
  },
  {
    "path": "src/trx/game/objects/traps/hook.c",
    "content": "#include <trx/game/creature.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/spawn.h>\n\n#define M_DAMAGE 50\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_IsTriggerActive(item) && Item_TestFrameEqual(item, -1)) {\n        Item_SwitchToAnim(item, 0, 0);\n        item->status = IS_INACTIVE;\n        Item_RemoveActive(item_num);\n        item->enable_interpolation = false;\n        return;\n    }\n\n    item->enable_interpolation = true;\n    if (item->touch_bits != 0) {\n        const ITEM *const lara_item = Lara_GetItem();\n        Lara_TakeDamage(M_DAMAGE, true);\n        Spawn_BloodBath(\n            lara_item->pos.x, lara_item->pos.y - WALL_L / 2, lara_item->pos.z,\n            lara_item->speed, lara_item->rot.y, lara_item->room_num, 3);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        item->enable_interpolation = item->status == IS_ACTIVE;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->handle_save_func = M_HandleSave;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_HOOK, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/icicle.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n\n#define M_DAMAGE 200\n\ntypedef enum {\n    // clang-format off\n    ICICLE_EMPTY = 0,\n    ICICLE_BREAK = 1,\n    ICICLE_FALL  = 2,\n    ICICLE_LAND  = 3,\n    // clang-format on\n} M_STATE;\n\nstatic void M_Reset(ITEM *const item)\n{\n    item->mesh_bits = 0xFFFFFFFF;\n    Trap_Reset(item);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    switch (item->current_anim_state) {\n    case ICICLE_BREAK:\n        item->goal_anim_state = ICICLE_FALL;\n        break;\n\n    case ICICLE_FALL:\n        if (!item->gravity) {\n            item->gravity = true;\n            item->fall_speed = 50;\n        }\n        if (item->touch_bits != 0) {\n            Lara_TakeDamage(M_DAMAGE, true);\n        }\n        break;\n\n    case ICICLE_LAND:\n        item->gravity = false;\n        break;\n    }\n\n    Item_Animate(item);\n    if (item->status == IS_DEACTIVATED) {\n        if (!Item_IsTriggerActive(item)) {\n            M_Reset(item);\n        }\n        return;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    item->floor = Room_GetHeight(sector, item->pos);\n    if (item->current_anim_state == ICICLE_FALL && item->pos.y >= item->floor) {\n        item->pos.y = item->floor;\n        item->gravity = false;\n        item->goal_anim_state = ICICLE_LAND;\n        item->fall_speed = 0;\n        item->mesh_bits = 0b00101011;\n        Sound_Effect(SFX_ICICLE, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = Trap_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision_Trap;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_ICICLE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/killer_statue.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/spawn.h>\n\n#define KILLER_STATUE_CUT_DAMAGE 20\n#define KILLER_STATUE_TOUCH_BITS 0b10000000 // = 128\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    KILLER_STATUE_STATE_EMPTY = 0,\n    KILLER_STATUE_STATE_STOP  = 1,\n    KILLER_STATUE_STATE_CUT   = 2,\n    // clang-format on\n} KILLER_STATUE_STATE;\n\ntypedef enum {\n    // clang-format off\n    KILLER_STATUE_ANIM_RETURN   = 0,\n    KILLER_STATUE_ANIM_FINISHED = 1,\n    KILLER_STATUE_ANIM_CUT      = 2,\n    KILLER_STATUE_ANIM_SET      = 3,\n    // clang-format on\n} KILLER_STATUE_ANIM;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    Item_SwitchToAnim(item, KILLER_STATUE_ANIM_SET, 0);\n    item->current_anim_state = KILLER_STATUE_STATE_STOP;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item)\n        && item->current_anim_state == KILLER_STATUE_STATE_STOP) {\n        item->goal_anim_state = KILLER_STATUE_STATE_CUT;\n    } else {\n        item->goal_anim_state = KILLER_STATUE_STATE_STOP;\n    }\n\n    if ((item->touch_bits & KILLER_STATUE_TOUCH_BITS) != 0\n        && item->current_anim_state == KILLER_STATUE_STATE_CUT) {\n        Lara_TakeDamage(KILLER_STATUE_CUT_DAMAGE, true);\n\n        const ITEM *const lara_item = Lara_GetItem();\n        Spawn_Blood(\n            lara_item->pos.x + (Random_GetControl() - 0x4000) / 256,\n            lara_item->pos.y - Random_GetControl() / 44,\n            lara_item->pos.z + (Random_GetControl() - 0x4000) / 256,\n            lara_item->speed,\n            lara_item->rot.y + (Random_GetControl() - 0x4000) / 8,\n            lara_item->room_num);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision_Trap;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_KILLER_STATUE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/lava_wedge.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/rooms.h>\n\n#define M_SPEED 25\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_IsTriggerActive(item)) {\n        Trap_Reset(item);\n        return;\n    }\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    if (item->status != IS_DEACTIVATED) {\n        XYZ_32 pos = item->pos;\n\n        switch (item->rot.y) {\n        case 0:\n            item->pos.z += M_SPEED;\n            pos.z += 2 * WALL_L;\n            break;\n        case -DEG_180:\n            item->pos.z -= M_SPEED;\n            pos.z -= 2 * WALL_L;\n            break;\n        case DEG_90:\n            item->pos.x += M_SPEED;\n            pos.x += 2 * WALL_L;\n            break;\n        default:\n            item->pos.x -= M_SPEED;\n            pos.x -= 2 * WALL_L;\n            break;\n        }\n\n        const SECTOR *const sector = Room_GetSector(pos, &room_num);\n        if (Room_GetHeight(sector, pos) != item->pos.y) {\n            item->status = IS_DEACTIVATED;\n        }\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->water_status == LWS_CHEAT) {\n        item->touch_bits = 0;\n    }\n\n    if (item->touch_bits) {\n        const ITEM *const lara_item = Lara_GetItem();\n        if (lara_item->hit_points > 0) {\n            Lara_TouchLava();\n        }\n\n        if (g_Config.debug.enable_invulnerability) {\n            return;\n        }\n        g_Camera.item = item;\n        g_Camera.flags = CF_CHASE_OBJECT;\n        g_Camera.type = CAM_FIXED;\n        g_Camera.target_angle = -DEG_180;\n        g_Camera.target_distance = WALL_L * 3;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = Trap_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_position = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_LAVA_WEDGE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/lightning_emitter.c",
    "content": "#include <trx/game/collision.h>\n#include <trx/game/game.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/viewport.h>\n\n#define M_DAMAGE 400\n#define M_STEPS 8\n#define M_RND 64\n#define M_SHOOTS 2\n\ntypedef struct {\n    bool active;\n    int32_t count;\n    bool zapped;\n    bool no_target;\n    XYZ_32 target;\n    int32_t start[M_SHOOTS];\n    XYZ_32 end[M_SHOOTS];\n    XYZ_32 main[M_STEPS];\n    XYZ_32 wibble[M_STEPS];\n    XYZ_32 shoot[M_SHOOTS][M_STEPS];\n} M_PRIV;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (Object_Get(item->object_id)->mesh_count > 1) {\n        item->mesh_bits = 1;\n        p->no_target = false;\n    } else {\n        p->no_target = true;\n    }\n\n    p->active = false;\n    p->count = 1;\n    p->zapped = false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (!Item_IsTriggerActive(item)) {\n        p->count = 1;\n        p->active = false;\n        p->zapped = false;\n\n        if (Room_GetFlipStatus()) {\n            Room_FlipMap();\n        }\n\n        Item_RemoveActive(item_num);\n        item->status = IS_INACTIVE;\n        return;\n    }\n\n    p->count--;\n    if (p->count > 0) {\n        return;\n    }\n\n    if (p->active) {\n        p->active = false;\n        p->count = 35 + (Random_GetControl() * 45) / 0x8000;\n        p->zapped = false;\n        if (Room_GetFlipStatus()) {\n            Room_FlipMap();\n        }\n    } else {\n        p->active = true;\n        p->count = 20;\n\n        for (int32_t i = 0; i < M_STEPS; i++) {\n            p->wibble[i].x = 0;\n            p->wibble[i].y = 0;\n            p->wibble[i].z = 0;\n        }\n\n        const int32_t radius = p->no_target ? WALL_L : WALL_L * 5 / 2;\n        if (Lara_IsNearItem(&item->pos, radius)) {\n            const ITEM *const lara_item = Lara_GetItem();\n            p->target.x = lara_item->pos.x;\n            p->target.y = lara_item->pos.y;\n            p->target.z = lara_item->pos.z;\n\n            Lara_TakeDamage(M_DAMAGE, true);\n\n            p->zapped = true;\n        } else if (p->no_target) {\n            const SECTOR *const sector =\n                Room_GetSector(item->pos, &item->room_num);\n            const int32_t h = Room_GetHeight(sector, item->pos);\n            p->target.x = item->pos.x;\n            p->target.y = h;\n            p->target.z = item->pos.z;\n            p->zapped = false;\n        } else {\n            p->target.x = 0;\n            p->target.y = 0;\n            p->target.z = 0;\n            Collide_GetJointAbsPosition(\n                item, &p->target, 1 + (Random_GetControl() * 5) / 0x7FFF);\n            p->zapped = false;\n        }\n\n        for (int32_t i = 0; i < M_SHOOTS; i++) {\n            p->start[i] = Random_GetControl() * (M_STEPS - 1) / 0x7FFF;\n            p->end[i].x = p->target.x + (Random_GetControl() * WALL_L) / 0x7FFF;\n            p->end[i].y = p->target.y;\n            p->end[i].z = p->target.z + (Random_GetControl() * WALL_L) / 0x7FFF;\n\n            for (int32_t j = 0; j < M_STEPS; j++) {\n                p->shoot[i][j].x = 0;\n                p->shoot[i][j].y = 0;\n                p->shoot[i][j].z = 0;\n            }\n        }\n\n        if (!Room_GetFlipStatus()) {\n            Room_FlipMap();\n        }\n    }\n\n    Sound_Effect(SFX_THUNDER, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    const ITEM *const item = Item_Get(item_num);\n    const M_PRIV *const p = item->priv;\n    if (!p->zapped) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->hit_direction = 1 + (Random_GetControl() * 4) / (DEG_180 - 1);\n    lara->hit_frame++;\n    CLAMPG(lara->hit_frame, 34);\n}\n\nstatic void M_DrawBolts(const ITEM *const item)\n{\n    ANIM_FRAME *frmptr[2];\n    int32_t rate;\n    Item_GetFrames(item, frmptr, &rate);\n\n    M_PRIV *const p = item->priv;\n    if (!p->active) {\n        return;\n    }\n\n    int32_t x1 = item->interp.result.pos.x + frmptr[0]->offset.x;\n    int32_t y1 = item->interp.result.pos.y + frmptr[0]->offset.y;\n    int32_t z1 = item->interp.result.pos.z + frmptr[0]->offset.z;\n\n    int32_t x2 = p->target.x;\n    int32_t y2 = p->target.y;\n    int32_t z2 = p->target.z;\n\n    int32_t dx = (x2 - x1) / M_STEPS;\n    int32_t dy = (y2 - y1) / M_STEPS;\n    int32_t dz = (z2 - z1) / M_STEPS;\n\n    for (int32_t i = 0; i < M_STEPS; i++) {\n        XYZ_32 *pos = &p->wibble[i];\n        if (Game_IsPlaying()) {\n            pos->x += (Random_GetDraw() - 0x4000) * M_RND / 0x8000;\n            pos->y += (Random_GetDraw() - 0x4000) * M_RND / 0x8000;\n            pos->z += (Random_GetDraw() - 0x4000) * M_RND / 0x8000;\n        }\n        if (i == M_STEPS - 1) {\n            pos->y = 0;\n        }\n\n        x2 = x1 + dx + pos->x;\n        y2 = y1 + dy + pos->y;\n        z2 = z1 + dz + pos->z;\n\n        if (i > 0) {\n            Output_DrawLightningSegment((LIGHTNING_SEGMENT) {\n                .from = { x1, y1 + p->wibble[i - 1].y, z1 },\n                .to = { x2, y2, z2 },\n                .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 6 });\n        } else {\n            Output_DrawLightningSegment((LIGHTNING_SEGMENT) {\n                .from = { x1, y1, z1 },\n                .to = { x2, y2, z2 },\n                .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 6 });\n        }\n\n        x1 = x2;\n        y1 += dy;\n        z1 = z2;\n\n        p->main[i].x = x2;\n        p->main[i].y = y2;\n        p->main[i].z = z2;\n    }\n\n    for (int32_t i = 0; i < M_SHOOTS; i++) {\n        int32_t j = p->start[i];\n        x1 = p->main[j].x;\n        y1 = p->main[j].y;\n        z1 = p->main[j].z;\n\n        x2 = p->end[i].x;\n        y2 = p->end[i].y;\n        z2 = p->end[i].z;\n\n        int32_t steps = M_STEPS - j;\n        dx = (x2 - x1) / steps;\n        dy = (y2 - y1) / steps;\n        dz = (z2 - z1) / steps;\n\n        for (int32_t k = 0; k < steps; k++) {\n            XYZ_32 *pos = &p->shoot[i][k];\n            if (Game_IsPlaying()) {\n                pos->x += (Random_GetDraw() - 0x4000) * M_RND / 0x8000;\n                pos->y += (Random_GetDraw() - 0x4000) * M_RND / 0x8000;\n                pos->z += (Random_GetDraw() - 0x4000) * M_RND / 0x8000;\n            }\n            if (k == steps - 1) {\n                pos->y = 0;\n            }\n\n            x2 = x1 + dx + pos->x;\n            y2 = y1 + dy + pos->y;\n            z2 = z1 + dz + pos->z;\n\n            if (k > 0) {\n                Output_DrawLightningSegment((LIGHTNING_SEGMENT) {\n                    .from = { x1, y1 + p->shoot[i][k - 1].y, z1 },\n                    .to = { x2, y2, z2 },\n                    .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 16 });\n            } else {\n                Output_DrawLightningSegment((LIGHTNING_SEGMENT) {\n                    .from = { x1, y1, z1 },\n                    .to = { x2, y2, z2 },\n                    .thickness = Viewport_GetWidth(VIEWPORT_GAME) / 16 });\n            }\n\n            x1 = x2;\n            y1 += dy;\n            z1 = z2;\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    const OBJECT *const obj = Object_Get(O_LIGHTNING_EMITTER);\n    ANIM_FRAME *frmptr[2];\n    int32_t rate;\n    Item_GetFrames(item, frmptr, &rate);\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n    const CLIP clip = Output_CheckBoundsClip(&frmptr[0]->bounds);\n    if (clip == CLIP_NOT_VISIBLE) {\n        Matrix_Pop();\n        return false;\n    }\n\n    Output_CalculateObjectLighting(item, &frmptr[0]->bounds);\n\n    Matrix_TranslateRel16(frmptr[0]->offset);\n    Object_DrawMesh(obj->mesh_idx, clip, false);\n    Matrix_Pop();\n\n    M_DrawBolts(item);\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n    obj->collision_func = M_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_LIGHTNING_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/midas_touch.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/overlay.h>\n#include <trx/game/sound.h>\n\n#define M_RANGE_H (STEP_L * 2)\n#define M_RANGE_V (STEP_L * 3)\n\nstatic const OBJECT_BOUNDS m_MidasTouch_Bounds = {\n    .shift = {\n        .min = { .x = -700, .y = +384 - 100, .z = -700, },\n        .max = { .x = +700, .y = +384 + 100 + 512, .z = +700, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, },\n        .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, },\n    },\n};\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_MidasTouch_Bounds;\n}\n\nstatic void M_KillLara(const ITEM *const item)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    Lara_SwitchToExtraState(LS_EXTRA_MIDAS_KILL);\n    lara_item->hit_points = -1;\n    lara_item->gravity = false;\n    lara->gun_type = LGT_UNARMED;\n    lara->air = -1;\n\n    Camera_InvokeCinematic(lara_item, 0, 0);\n}\n\nstatic bool M_IsUsable(const int16_t item_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    return lara_item->current_anim_state != LS(LS_USE_MIDAS);\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    const DIRECTION quadrant = Math_GetDirection(lara_item->rot.y);\n    switch (quadrant) {\n    case DIR_NORTH:\n        item->rot.y = 0;\n        break;\n    case DIR_EAST:\n        item->rot.y = DEG_90;\n        break;\n    case DIR_SOUTH:\n        item->rot.y = -DEG_180;\n        break;\n    case DIR_WEST:\n        item->rot.y = -DEG_90;\n        break;\n    default:\n        break;\n    }\n\n    if (!lara_item->gravity && lara_item->current_anim_state == LS(LS_STOP)\n        && lara_item->pos.x > item->pos.x - M_RANGE_H\n        && lara_item->pos.x < item->pos.x + M_RANGE_H\n        && lara_item->pos.y > item->pos.y - M_RANGE_V\n        && lara_item->pos.y < item->pos.y + M_RANGE_V\n        && lara_item->pos.z > item->pos.z - M_RANGE_H\n        && lara_item->pos.z < item->pos.z + M_RANGE_H) {\n        M_KillLara(item);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->interact_target.is_moving\n        && lara->interact_target.item_num == item_num) {\n        Lara_SwitchToExtraState(LS_EXTRA_USE_MIDAS);\n        lara->interact_target.is_moving = false;\n    }\n\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || lara_item->current_anim_state != LS(LS_STOP)) {\n        return;\n    }\n\n    if (!Lara_TestPosition(item, obj->bounds_func())) {\n        return;\n    }\n\n    if (!GF_ShowInventoryKeys(item->object_id)) {\n        Lara_RefuseInteraction();\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->collision_func = M_Collision;\n    obj->draw_func = nullptr;\n    obj->bounds_func = M_Bounds;\n    obj->is_usable_func = M_IsUsable;\n}\n\nREGISTER_OBJECT(O_MIDAS_TOUCH, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/mine.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\nstatic bool m_DetonateAllMines = false;\n\nstatic int16_t M_GetBoatItem(const XYZ_32 *const pos, int16_t *const room_num)\n{\n    Room_GetSector(*pos, room_num);\n\n    int16_t item_num = Room_Get(*room_num)->item_num;\n    while (item_num != NO_ITEM) {\n        const ITEM *const item = Item_Get(item_num);\n        if (item->object_id == O_BOAT) {\n            const int32_t dx = item->pos.x - pos->x;\n            const int32_t dz = item->pos.z - pos->z;\n            // TODO: fix overflows and no y check\n            if (SQUARE(dx) + SQUARE(dz) < SQUARE(WALL_L / 2)) {\n                break;\n            }\n        }\n        item_num = item->next_item;\n    }\n    return item_num;\n}\n\nstatic void M_DetonateAll(\n    const ITEM *const mine_item, const int16_t boat_item_num,\n    int16_t boat_room_num)\n{\n    ITEM *const boat_item = Item_Get(boat_item_num);\n    if (Lara_Vehicle_GetIndex() == boat_item_num) {\n        ITEM *const lara_item = Lara_GetItem();\n        Item_Explode(Item_GetIndex(lara_item), -1, 0);\n        lara_item->hit_points = 0;\n        lara_item->flags |= IF_ONE_SHOT;\n    }\n\n    const OBJECT *const obj = Object_Get(O_BOAT_BITS);\n    if (obj->loaded) {\n        boat_item->object_id = O_BOAT_BITS;\n        boat_item->mesh_bits = (1 << obj->mesh_count) - 1;\n        Item_Explode(boat_item_num, -1, 0);\n    }\n    Item_Kill(boat_item_num);\n    boat_item->object_id = O_BOAT;\n\n    Room_TestTriggers(mine_item);\n\n    m_DetonateAllMines = true;\n}\n\nstatic void M_Explode(ITEM *const mine_item)\n{\n    const int16_t effect_num = Effect_Create(mine_item->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_EXPLOSION_1;\n        effect->pos.x = mine_item->pos.x;\n        effect->pos.y = mine_item->pos.y - WALL_L;\n        effect->pos.z = mine_item->pos.z;\n        effect->speed = 0;\n        effect->frame_num = 0;\n        effect->counter = 0;\n    }\n\n    Spawn_Splash(mine_item);\n    Sound_Effect(SFX_EXPLOSION_1, &mine_item->pos, SPM_NORMAL);\n\n    mine_item->flags |= IF_ONE_SHOT;\n    mine_item->mesh_bits = 1;\n    mine_item->collidable = false;\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        if (item->flags & IF_ONE_SHOT) {\n            item->mesh_bits = 1;\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->flags & IF_ONE_SHOT) {\n        return;\n    }\n\n    if (!m_DetonateAllMines) {\n        int16_t boat_room_num = item->room_num;\n        XYZ_32 test_pos = {\n            .x = item->pos.x,\n            .y = item->pos.y - WALL_L * 2,\n            .z = item->pos.z,\n        };\n        const int16_t boat_item_num = M_GetBoatItem(&test_pos, &boat_room_num);\n        if (boat_item_num == NO_ITEM) {\n            return;\n        }\n\n        M_DetonateAll(item, boat_item_num, boat_room_num);\n    } else if (Random_GetControl() < 0x7800) {\n        return;\n    }\n\n    M_Explode(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->enable_interpolation = false;\n\n    m_DetonateAllMines = false;\n}\n\nREGISTER_OBJECT(O_MINE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/movable_block.c",
    "content": "#include <trx/game/objects/traps/movable_block.h>\n\n#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define LF_PPREADY 19\n\ntypedef enum {\n    MOVABLE_BLOCK_STATE_STILL = 1,\n    MOVABLE_BLOCK_STATE_PUSH = 2,\n    MOVABLE_BLOCK_STATE_PULL = 3,\n} MOVABLE_BLOCK_STATE;\n\ntypedef struct {\n    uint16_t gravity_frames;\n    bool is_push_pull;\n    bool is_forced_moving;\n    int16_t extra_rotations[3];\n    int16_t original_rot;\n    int16_t interaction_rot;\n    GAME_VECTOR initial;\n    GAME_VECTOR linked;\n} M_PRIV;\n\nstatic const OBJECT_BOUNDS m_MovableBlock_Bounds = {\n    .shift = {\n        .min = { .x = -300, .y = 0, .z = -WALL_L / 2 - (LARA_RADIUS + 80), },\n        .max = { .x = +300, .y = 0, .z = -WALL_L / 2, },\n    },\n    .rot = {\n        .min = { .x = -10 * DEG_1, .y = -30 * DEG_1, .z = -10 * DEG_1, },\n        .max = { .x = +10 * DEG_1, .y = +30 * DEG_1, .z = +10 * DEG_1, },\n    },\n};\n\n// Collect a stack of blocks.\nstatic void M_GetStack(\n    VECTOR *stack, XYZ_32 stack_pos, int32_t stack_height, int32_t step_y,\n    int16_t room_num);\n\n// Restores blocks' original texturing in case they have unique textures on each\n// side. The game rotates the block in order to align the block with Lara when\n// she tries to push or pull.\nstatic void M_UpdateRotation(ITEM *const item, const int16_t rot_y)\n{\n    item->rot.y = rot_y;\n    M_PRIV *const p = item->priv;\n    // All 3 indices are potentially used in other parts of the code that can\n    // cast item->extra_rotations to structs such as XYZ_16. This is similar to\n    // things such as the compass needle that apply extra rotation.\n    p->extra_rotations[0] = p->original_rot - rot_y;\n}\n\n// Indicates if Lara is currently pushing or pulling a block.\nstatic void M_SetPushPull(ITEM *const item, const bool enable)\n{\n    M_PRIV *const p = item->priv;\n    p->is_push_pull = enable;\n}\n\nstatic bool M_IsPushPull(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr && p->is_push_pull;\n}\n\n// Indicates if blocks are being forcefully moved by other objects such as\n// lifts.\nstatic void M_SetForcedMoving(ITEM *const item, const bool enable)\n{\n    M_PRIV *const p = item->priv;\n    p->is_forced_moving = enable;\n}\n\nstatic bool M_IsForcedMoving(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr && p->is_forced_moving;\n}\n\n// If a stack of multiple blocks need to drop, each subsequently stacked block\n// is delayed by incrementing frames so that higher blocks don't \"land\" on lower\n// blocks and stop moving.\nstatic void M_SetGravityFrames(ITEM *const item, const uint8_t frames)\n{\n    M_PRIV *const p = item->priv;\n    p->gravity_frames = frames;\n}\n\nstatic uint16_t M_GetGravityFrames(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p != nullptr ? p->gravity_frames : 0;\n}\n\n// Handles the block's initial position and room number for walkables.\nstatic void M_SetInitial(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    p->initial.pos = item->pos;\n    p->initial.room_num = item->room_num;\n}\n\nstatic GAME_VECTOR M_GetInitial(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->initial;\n}\n\n// Handles the block's linked position and room number for walkables.\nstatic void M_SetLinked(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    p->linked.pos = item->pos;\n    p->linked.room_num = item->room_num;\n}\n\nstatic GAME_VECTOR M_GetLinked(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->linked;\n}\n\nstatic void M_UpdateStoppers(const ITEM *const item, const bool enabled)\n{\n    const M_PRIV *const p = item->priv;\n    int16_t dir = p->interaction_rot;\n    if (!enabled) {\n        dir += DEG_180;\n    }\n    const ROOM *room = Room_Get(item->room_num);\n    SECTOR *sector = Room_GetWorldSector(room, item->pos.x, item->pos.z);\n    sector->stopper = enabled;\n\n    const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, dir, WALL_L);\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n    room = Room_Get(room_num);\n    sector = Room_GetWorldSector(room, pos.x, pos.z);\n    sector->stopper = enabled;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"gravity_frames\", &p->gravity_frames));\n    JSON_SHOULD(JSON_READ(io, \"is_push_pull\", &p->is_push_pull));\n    JSON_SHOULD(JSON_READ(io, \"is_forced_moving\", &p->is_forced_moving));\n\n    if (JSON_SHOULD(JSON_PUSH(io, \"linked\"))) {\n        JSON_SHOULD(JSON_READ(io, \"x\", &p->linked.pos.x));\n        JSON_SHOULD(JSON_READ(io, \"y\", &p->linked.pos.y));\n        JSON_SHOULD(JSON_READ(io, \"z\", &p->linked.pos.z));\n        JSON_SHOULD(JSON_POP(io));\n    }\n\n    JSON_SHOULD(JSON_READ(io, \"counter_rot_0\", &p->extra_rotations[0]));\n    JSON_SHOULD(JSON_READ(io, \"counter_rot_1\", &p->extra_rotations[1]));\n    JSON_SHOULD(JSON_READ(io, \"counter_rot_2\", &p->extra_rotations[2]));\n    JSON_SHOULD(JSON_READ(io, \"original_rot\", &p->original_rot));\n    JSON_SHOULD(JSON_READ(io, \"interaction_rot\", &p->interaction_rot));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"gravity_frames\", p->gravity_frames);\n    JSONW_WRITE(io, \"is_push_pull\", p->is_push_pull);\n    JSONW_WRITE(io, \"is_forced_moving\", p->is_forced_moving);\n\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"x\", p->linked.pos.x);\n    JSONW_WRITE(io, \"y\", p->linked.pos.y);\n    JSONW_WRITE(io, \"z\", p->linked.pos.z);\n    JSONW_POP_AND_SET(io, \"linked\");\n\n    JSONW_WRITE(io, \"counter_rot_0\", p->extra_rotations[0]);\n    JSONW_WRITE(io, \"counter_rot_1\", p->extra_rotations[1]);\n    JSONW_WRITE(io, \"counter_rot_2\", p->extra_rotations[2]);\n    JSONW_WRITE(io, \"original_rot\", p->original_rot);\n    JSONW_WRITE(io, \"interaction_rot\", p->interaction_rot);\n}\n\nstatic bool M_TestCurrentSector(\n    const ITEM *const item, const int32_t block_height)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n\n    // Check if there is a hard wall above.\n    if (Room_GetHeight(sector, item->pos) == NO_HEIGHT) {\n        return true;\n    }\n\n    // Make sure there is nothing on top of the block.\n    if (Room_GetHeight(\n            sector,\n            (XYZ_32) { item->pos.x, item->pos.y - block_height, item->pos.z })\n        != item->pos.y - block_height) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_TestPush(\n    const ITEM *const item, const int32_t block_height,\n    const DIRECTION quadrant)\n{\n    if (!M_TestCurrentSector(item, block_height)) {\n        return false;\n    }\n\n    XYZ_32 base_pos = item->pos;\n    int16_t room_num = item->room_num;\n\n    switch (quadrant) {\n    case DIR_NORTH:\n        base_pos.z += WALL_L;\n        break;\n    case DIR_EAST:\n        base_pos.x += WALL_L;\n        break;\n    case DIR_SOUTH:\n        base_pos.z -= WALL_L;\n        break;\n    case DIR_WEST:\n        base_pos.x -= WALL_L;\n        break;\n    default:\n        break;\n    }\n\n    const SECTOR *sector = Room_GetSector(base_pos, &room_num);\n    COLL_INFO coll = {\n        .quadrant = quadrant,\n        .radius = 500,\n    };\n    if (Collide_CollideStaticObjects(\n            &coll, base_pos.x, base_pos.y, base_pos.z, room_num, 1000)) {\n        return false;\n    }\n\n    if (Room_GetHeight(sector, base_pos) != base_pos.y) {\n        return false;\n    }\n\n    if (sector->stopper) {\n        return false;\n    }\n\n    const XYZ_32 sample_pos = { base_pos.x, base_pos.y - block_height,\n                                base_pos.z };\n    sector = Room_GetSector(sample_pos, &room_num);\n    if (Room_GetCeiling(sector, sample_pos) > base_pos.y - block_height) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_TestPull(\n    const ITEM *const item, const int32_t block_height,\n    const DIRECTION quadrant)\n{\n    if (!M_TestCurrentSector(item, block_height)) {\n        return false;\n    }\n\n    int32_t x_add = 0;\n    int32_t z_add = 0;\n    switch (quadrant) {\n    case DIR_NORTH:\n        z_add = -WALL_L;\n        break;\n    case DIR_EAST:\n        x_add = -WALL_L;\n        break;\n    case DIR_SOUTH:\n        z_add = WALL_L;\n        break;\n    case DIR_WEST:\n        x_add = WALL_L;\n        break;\n    default:\n        break;\n    }\n\n    // Test block destination sector.\n    XYZ_32 base_pos = {\n        .x = item->pos.x + x_add,\n        .y = item->pos.y,\n        .z = item->pos.z + z_add,\n    };\n    int16_t room_num = item->room_num;\n    const SECTOR *sector = Room_GetSector(base_pos, &room_num);\n\n    COLL_INFO coll = {\n        .quadrant = quadrant,\n        .radius = 500,\n    };\n    if (Collide_CollideStaticObjects(\n            &coll, base_pos.x, base_pos.y, base_pos.z, room_num, 1000)) {\n        return false;\n    }\n\n    if (Room_GetHeight(sector, base_pos) != base_pos.y) {\n        return false;\n    }\n\n    if (sector->stopper) {\n        return false;\n    }\n\n    XYZ_32 sample_pos = { base_pos.x, base_pos.y - block_height, base_pos.z };\n    sector = Room_GetSector(sample_pos, &room_num);\n    if (Room_GetCeiling(sector, sample_pos) > base_pos.y - block_height) {\n        return false;\n    }\n\n    // Test Lara destination sector.\n    base_pos.x += x_add;\n    base_pos.z += z_add;\n    room_num = item->room_num;\n    sector = Room_GetSector(base_pos, &room_num);\n    if (Room_GetHeight(sector, base_pos) != base_pos.y) {\n        return false;\n    }\n\n    sample_pos = (XYZ_32) { base_pos.x, base_pos.y - LARA_HEIGHT, base_pos.z };\n    sector = Room_GetSector(sample_pos, &room_num);\n    if (Room_GetCeiling(sector, sample_pos) > base_pos.y - LARA_HEIGHT) {\n        return false;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    base_pos.x = lara_item->pos.x + x_add;\n    base_pos.y = lara_item->pos.y;\n    base_pos.z = lara_item->pos.z + z_add;\n    room_num = lara_item->room_num;\n    sector = Room_GetSector(base_pos, &room_num);\n    coll.radius = LARA_RADIUS;\n    coll.quadrant = (quadrant + 2) & 3;\n    if (Collide_CollideStaticObjects(\n            &coll, base_pos.x, base_pos.y, base_pos.z, room_num, LARA_HEIGHT)) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_TestDoor(ITEM *lara_item, COLL_INFO *coll)\n{\n    // OG fix: stop pushing blocks through doors\n\n    const int32_t shift = 8; // constant shift to avoid overflow errors\n    const int32_t max_dist = SQUARE((WALL_L * 2) >> shift);\n    for (int item_num = 0; item_num < Item_GetLevelCount(); item_num++) {\n        ITEM *const item = Item_Get(item_num);\n        if (!Object_IsType(item->object_id, g_DoorObjects)) {\n            continue;\n        }\n\n        const int32_t dx = (item->pos.x - lara_item->pos.x) >> shift;\n        const int32_t dy = (item->pos.y - lara_item->pos.y) >> shift;\n        const int32_t dz = (item->pos.z - lara_item->pos.z) >> shift;\n        const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (dist > max_dist) {\n            continue;\n        }\n\n        if (Lara_TestBoundsCollide(item, coll->radius)\n            && Collide_TestCollision(item, lara_item)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_TestSolidPortal(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(lara_item->pos, &room_num);\n    const int32_t height =\n        Room_GetHeightEx(sector, lara_item->pos, true, NO_ITEM);\n    return height == NO_HEIGHT;\n}\n\nstatic bool M_TestDeathCollision(const ITEM *const item, const ITEM *const lara)\n{\n    return g_Config.gameplay.enable_killer_pushblocks\n        && !g_Config.debug.enable_invulnerability && item->gravity\n        && Lara_TestBoundsCollide(item, 0);\n}\n\nstatic bool M_IsItemOnTop(\n    const ITEM *const item, const int32_t x, const int32_t z)\n{\n    const int32_t dx = x - item->pos.x;\n    const int32_t dz = z - item->pos.z;\n\n    // Movable blocks' bounds don't match sector so estimate.\n    return (dx >= -WALL_L / 2 && dx < WALL_L / 2)\n        && (dz >= -WALL_L / 2 && dz < WALL_L / 2);\n}\n\nstatic bool M_TestEmbedCollision(const ITEM *const item, const ITEM *const lara)\n{\n    return M_IsItemOnTop(item, lara->pos.x, lara->pos.z)\n        && lara->pos.y <= item->pos.y && lara->pos.y > item->pos.y - WALL_L\n        && !item->gravity && !lara->gravity\n        && item->current_anim_state == MOVABLE_BLOCK_STATE_STILL\n        && lara->current_anim_state != LS(LS_PULL_BLOCK)\n        && lara->current_anim_state != LS(LS_PUSH_BLOCK);\n}\n\nstatic void M_KillLara(const ITEM *const item, ITEM *const lara)\n{\n    if (lara->hit_points <= 0) {\n        return;\n    }\n\n    lara->hit_points = -1;\n    lara->pos.y = lara->floor;\n    lara->speed = 0;\n    lara->fall_speed = 0;\n    lara->gravity = false;\n    lara->rot.x = 0;\n    lara->rot.z = 0;\n    lara->enable_shadow = false;\n    lara->current_anim_state = LS(LS_SPECIAL);\n    lara->goal_anim_state = LS(LS_SPECIAL);\n    Item_SwitchToAnim(lara, LA(LA_BOULDER_DEATH), 0);\n\n    for (int32_t i = 0; i < 15; i++) {\n        const int32_t x = lara->pos.x + (Random_GetControl() - 0x4000) / 256;\n        const int32_t z = lara->pos.z + (Random_GetControl() - 0x4000) / 256;\n        const int32_t y = lara->pos.y - Random_GetControl() / 64;\n        const int32_t d = lara->rot.y + (Random_GetControl() - 0x4000) / 8;\n        Spawn_Blood(x, y, z, item->speed * 2, d, lara->room_num);\n    }\n\n    if (!Object_Get(O_CAMERA_TARGET)->loaded) {\n        return;\n    }\n\n    const int16_t target_num = Item_Spawn(lara, O_CAMERA_TARGET);\n    if (target_num != NO_ITEM) {\n        ITEM *const target = Item_Get(target_num);\n        target->rot.y = g_Camera.target_angle;\n        target->pos.y = lara->floor - WALL_L;\n        Lara_SetDeathCameraTarget(target_num);\n    }\n}\n\nstatic bool M_IsAgainstFloor(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    return !sector->floor.is_split && sector->floor.tilt.x == 0\n        && sector->floor.tilt.z == 0 && sector->floor.height == item->pos.y;\n}\n\nstatic bool M_IsAgainstCeiling(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const SECTOR *const sky_sector =\n        Room_GetSkySector(sector, item->pos.x, item->pos.z);\n    return !sector->ceiling.is_split && sky_sector->ceiling.tilt.x == 0\n        && sky_sector->ceiling.tilt.z == 0\n        && sky_sector->ceiling.height == item->pos.y - WALL_L;\n}\n\nstatic const OBJECT_BOUNDS *M_Bounds(void)\n{\n    return &m_MovableBlock_Bounds;\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    if (item->status == IS_ACTIVE) {\n        return Object_DrawUnclippedItem(item);\n    } else {\n        return Object_DrawAnimatingItem(item);\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    // Ensure the block is snapped to the grid, otherwise the snapping occurs\n    // during collision tests and can appear jarring. Additional angles are\n    // stored to preserve item appearance in spite of control angle changes.\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    item->extra_rotations = p->extra_rotations;\n    p->original_rot = (((item->rot.y + DEG_180) / DEG_90) * DEG_90) - DEG_180;\n\n    M_UpdateRotation(item, p->original_rot);\n    M_SetGravityFrames(item, 0);\n    M_SetPushPull(item, false);\n    M_SetForcedMoving(item, false);\n    M_SetInitial(item);\n    M_SetLinked(item);\n    MovableBlock_UpdateBox(item, true);\n    Walkable_AllocateNodes(item, 1);\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_BEFORE_LOAD) {\n        MovableBlock_UpdateBox(item, false);\n    } else if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (item->anim_num < obj->anim_idx\n            || item->anim_num >= obj->anim_idx + obj->anim_count) {\n            // #4735 - resolve save issues caused by injections shifting anim\n            // numbers. Remove after some time.\n            Item_SwitchToAnim(item, 0, 0);\n        }\n\n        const int16_t item_num = Item_GetIndex(item);\n        if (item->flags & IF_KILLED) {\n            Walkable_Remove(item_num);\n            return;\n        }\n        if (item->status == IS_ACTIVE && !item->gravity\n            && !M_IsForcedMoving(item)\n            && item->current_anim_state == MOVABLE_BLOCK_STATE_STILL) {\n            Item_RemoveActive(Item_GetIndex(item));\n            item->status = IS_INACTIVE;\n        }\n\n        // Reposition walkable to its linked sector.\n        Walkable_Reposition(item_num, M_GetInitial(item), M_GetLinked(item));\n        MovableBlock_UpdateBox(item, true);\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n\n    if (item->status == IS_INVISIBLE) {\n        return;\n    }\n\n    if (M_TestDeathCollision(item, lara_item)) {\n        M_KillLara(item, lara_item);\n        return;\n    }\n\n    if (M_TestEmbedCollision(item, lara_item)) {\n        lara_item->pos.y = item->pos.y - WALL_L;\n    }\n\n    if (item->current_anim_state == MOVABLE_BLOCK_STATE_STILL) {\n        M_SetPushPull(item, false);\n    }\n\n    if (!g_Input.action || item->status == IS_ACTIVE || lara_item->gravity\n        || lara_item->pos.y != item->pos.y || M_IsForcedMoving(item)) {\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const DIRECTION quadrant = Math_GetDirection(lara_item->rot.y);\n    if (lara_item->current_anim_state == LS(LS_STOP)) {\n        if (g_Input.forward || g_Input.back\n            || lara->gun_status != LGS_ARMLESS) {\n            return;\n        }\n\n        switch (quadrant) {\n        case DIR_NORTH:\n            M_UpdateRotation(item, 0);\n            break;\n        case DIR_EAST:\n            M_UpdateRotation(item, DEG_90);\n            break;\n        case DIR_SOUTH:\n            M_UpdateRotation(item, -DEG_180);\n            break;\n        case DIR_WEST:\n            M_UpdateRotation(item, -DEG_90);\n            break;\n        default:\n            break;\n        }\n\n        if (!Lara_TestPosition(item, obj->bounds_func())) {\n            return;\n        }\n\n        // OG fix: stop pushing blocks through doors\n        if (M_TestDoor(lara_item, coll)) {\n            return;\n        }\n\n        // Prevent Lara moving a block through a non-passable portal\n        if (M_TestSolidPortal(item)) {\n            return;\n        }\n\n        switch (quadrant) {\n        case DIR_NORTH:\n            lara_item->pos.z = ROUND_TO_SECTOR(lara_item->pos.z);\n            lara_item->pos.z += WALL_L - LARA_RADIUS;\n            break;\n        case DIR_SOUTH:\n            lara_item->pos.z = ROUND_TO_SECTOR(lara_item->pos.z);\n            lara_item->pos.z += LARA_RADIUS;\n            break;\n        case DIR_EAST:\n            lara_item->pos.x = ROUND_TO_SECTOR(lara_item->pos.x);\n            lara_item->pos.x += WALL_L - LARA_RADIUS;\n            break;\n        case DIR_WEST:\n            lara_item->pos.x = ROUND_TO_SECTOR(lara_item->pos.x);\n            lara_item->pos.x += LARA_RADIUS;\n            break;\n        default:\n            break;\n        }\n\n        lara_item->rot.y = item->rot.y;\n        lara_item->goal_anim_state = LS(LS_PP_READY);\n\n        Lara_Animate(lara_item);\n\n        if (lara_item->current_anim_state == LS(LS_PP_READY)) {\n            lara->gun_status = LGS_HANDS_BUSY;\n        }\n    } else if (Item_TestAnimEqual(lara_item, LA(LA_PUSHABLE_GRAB))) {\n        if (!Item_TestFrameEqual(lara_item, LF_PPREADY)) {\n            return;\n        }\n\n        if (!Lara_TestPosition(item, obj->bounds_func())) {\n            return;\n        }\n\n        M_PRIV *const p = item->priv;\n        if (g_Input.forward) {\n            if (!M_TestPush(item, WALL_L, quadrant)) {\n                return;\n            }\n            p->interaction_rot = lara_item->rot.y;\n            item->goal_anim_state = MOVABLE_BLOCK_STATE_PUSH;\n            lara_item->goal_anim_state = LS(LS_PUSH_BLOCK);\n        } else if (g_Input.back) {\n            if (!M_TestPull(item, WALL_L, quadrant)) {\n                return;\n            }\n            p->interaction_rot = lara_item->rot.y + DEG_180;\n            item->goal_anim_state = MOVABLE_BLOCK_STATE_PULL;\n            lara_item->goal_anim_state = LS(LS_PULL_BLOCK);\n        } else {\n            return;\n        }\n\n        M_SetLinked(item);\n        item->status = IS_ACTIVE;\n        Item_AddActive(item_num);\n        M_UpdateStoppers(item, true);\n        MovableBlock_UpdateBox(item, false);\n        Item_Animate(item);\n        Lara_Animate(lara_item);\n        M_SetPushPull(item, true);\n    }\n}\n\nstatic void M_ResetPosition(ITEM *const item)\n{\n    const int16_t item_num = Item_GetIndex(item);\n    const GAME_VECTOR linked_pos = M_GetLinked(item);\n    const GAME_VECTOR initial_pos = M_GetInitial(item);\n\n    MovableBlock_UpdateBox(item, false);\n    item->pos = initial_pos.pos;\n    Item_UpdateRoom(item_num, initial_pos.room_num);\n    Walkable_Reposition(item_num, linked_pos, initial_pos);\n    M_SetLinked(item);\n    MovableBlock_UpdateBox(item, true);\n\n    Item_RemoveActive(item_num);\n    item->timer = -1;\n    item->status = IS_INACTIVE;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (item->status == IS_INVISIBLE) {\n        return;\n    }\n\n    if (item->timer > 0 && !M_IsPushPull(item) && !M_IsForcedMoving(item)) {\n        M_ResetPosition(item);\n        return;\n    }\n\n    if (M_GetGravityFrames(item) > 0) {\n        M_SetGravityFrames(item, M_GetGravityFrames(item) - 1);\n        return;\n    }\n\n    if ((item->flags & IF_ONE_SHOT) != 0) {\n        Item_Kill(item_num);\n        Walkable_Remove(item_num);\n        MovableBlock_UpdateBox(item, false);\n        return;\n    }\n\n    Item_Animate(item);\n\n    // Check if the block is floating, on a walkable, or on the pit floor.\n    // ROUND_TO_HALF_CLICK because block can fall through floor to undefined\n    // sector.\n    int16_t room_num = item->room_num;\n    XYZ_32 sample_pos = {\n        item->pos.x,\n        ROUND_TO_HALF_CLICK(item->pos.y),\n        item->pos.z,\n    };\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n    int32_t under_block_height =\n        Room_GetHeightEx(sector, item->pos, true, item_num);\n\n    bool update_room_num = true;\n\n    // Check if tunneled into floor below.\n    if (item->gravity && item->fall_speed > 0) {\n        const int32_t y_prev = item->pos.y - item->fall_speed;\n\n        // Query floor at previous y position.\n        sample_pos.y = y_prev;\n        const SECTOR *prev_sector = Room_GetSector(sample_pos, &room_num);\n        int32_t prev_height =\n            Room_GetHeightEx(prev_sector, sample_pos, true, item_num);\n\n        // If on a walkable at the previous y position, use the rounded previous\n        // y position as the floor.\n        if (Room_IsOnWalkable(\n                prev_sector,\n                (XYZ_32) {\n                    item->pos.x,\n                    ROUND_TO_HALF_CLICK(y_prev),\n                    item->pos.z,\n                },\n                ROUND_TO_HALF_CLICK(y_prev), item_num)) {\n            prev_height = ROUND_TO_HALF_CLICK(y_prev);\n        }\n\n        // If tunneled into the floor, clamp to previous floor height.\n        if (prev_height != NO_HEIGHT && y_prev < prev_height\n            && item->pos.y >= prev_height) {\n            under_block_height = prev_height;\n            update_room_num = false;\n        }\n    }\n\n    if (item->pos.y < under_block_height && !M_IsPushPull(item)\n        && !M_IsForcedMoving(item)) {\n        // Block is activated and floating in the air.\n        item->gravity = true;\n    } else if (item->gravity) {\n        // Block hits the ground or another walkable.\n        item->gravity = false;\n        item->fall_speed = 0;\n        item->pos.y = under_block_height;\n        item->status = IS_DEACTIVATED;\n        ItemAction_Run(ITEM_ACTION_FLOOR_SHAKE, item);\n        Sound_Effect(SFX_PUSHBLOCK_LAND, &item->pos, SPM_NORMAL);\n    } else if (\n        // If block is at/under floor height, no gravity, and isn't being\n        // pushed/pulled anymore. Prevents blocks from getting stuck in\n        // IS_INACTIVE if retriggered.\n        item->pos.y >= under_block_height && !item->gravity\n        && !M_IsPushPull(item) && !M_IsForcedMoving(item)) {\n        item->status = IS_INACTIVE;\n        Item_RemoveActive(item_num);\n    }\n\n    // Don't update room number if on a walkable because room number can fall\n    // through to a pit room (e.g. trapdoors).\n    if (update_room_num) {\n        room_num = item->room_num;\n        Room_GetSectorOnWalkable(\n            (XYZ_32) { item->pos.x, item->pos.y - WALL_L, item->pos.z },\n            &room_num);\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    if (item->status == IS_DEACTIVATED) {\n        const GAME_VECTOR target = {\n            .pos = item->pos,\n            .room_num = item->room_num,\n        };\n        Walkable_Reposition(item_num, M_GetLinked(item), target);\n        M_SetLinked(item);\n        item->status = IS_INACTIVE;\n        Item_RemoveActive(item_num);\n        M_UpdateStoppers(item, false);\n        MovableBlock_UpdateBox(item, true);\n        Room_TestTriggers(item);\n    }\n}\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (item->status == IS_INVISIBLE || item->gravity) {\n        return height;\n    }\n\n    // TODO OG bug: camera and shadow behave like OG during push/pull.\n    if (M_IsPushPull(item)) {\n        return height;\n    }\n\n    if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    }\n\n    if (M_IsAgainstFloor(item) && M_IsAgainstCeiling(item)) {\n        return NO_HEIGHT;\n    }\n\n    // If partially embedded from below e.g. jumping up into an overhead block.\n    if (y <= item->pos.y && y > item->pos.y - WALL_L\n        && M_IsAgainstCeiling(item)) {\n        const SECTOR *const sector = Room_GetWorldSector(\n            Room_Get(item->room_num), item->pos.x, item->pos.z);\n        if (item->pos.y < sector->floor.height) {\n            // If partially embedded from below e.g. jumping up into an overhead\n            // block.\n            return height;\n        } else if (M_IsAgainstFloor(item)) {\n            // Clamped between floor and ceiling. Match up with similar case in\n            // M_GetCeilingHeight to return same sentinel value;\n            return item->pos.y - WALL_L;\n        }\n    }\n\n    // If under the bottom of the block.\n    if (y > item->pos.y) {\n        return height;\n    }\n\n    // If the the top of the block is under the floor height.\n    if (item->pos.y - WALL_L >= height) {\n        return height;\n    }\n\n    return item->pos.y - WALL_L;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *const item, const int32_t x, const int32_t y, const int32_t z,\n    const int16_t height)\n{\n    if (item->status == IS_INVISIBLE || item->gravity) {\n        return height;\n    }\n\n    // TODO OG bug: camera and shadow behave like OG during push/pull.\n    if (M_IsPushPull(item)) {\n        return height;\n    }\n\n    // Only care if we are inside the block footprint.\n    if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    }\n\n    if (M_IsAgainstFloor(item) && M_IsAgainstCeiling(item)) {\n        return NO_HEIGHT;\n    }\n\n    if (y <= item->pos.y && y > item->pos.y - WALL_L && !item->gravity) {\n        if (M_IsAgainstCeiling(item)) {\n            // If clamped betwee floor and ceiling return same sentinel value as\n            // M_GetFloorHeight.\n            return M_IsAgainstFloor(item) ? item->pos.y - WALL_L : item->pos.y;\n        }\n        return height;\n    }\n\n    // If above the top of the block.\n    if (y <= item->pos.y - WALL_L) {\n        return height;\n    }\n\n    // If the the bottom of the block is above the ceiling height.\n    if (item->pos.y <= height) {\n        return height;\n    }\n\n    return item->pos.y;\n}\n\nstatic void M_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    Walkable_Add(item_num, item->pos);\n}\n\nstatic void M_GetStack(\n    VECTOR *const stack, const XYZ_32 stack_pos, int32_t stack_height,\n    const int32_t step_y, const int16_t room_num)\n{\n    int16_t sector_room_num = room_num;\n    SECTOR *sector = Room_GetSector(stack_pos, &sector_room_num);\n    sector = Room_GetPitSector(sector, stack_pos.x, stack_pos.z);\n\n    for (WALKABLE *w = sector->walkable; w != nullptr; w = w->next) {\n        const ITEM *item = Item_Get(w->item_num);\n        if (!Object_IsType(item->object_id, g_MovableBlockObjects)) {\n            continue;\n        }\n        if (w->pos.x == stack_pos.x && w->pos.y == stack_height\n            && w->pos.z == stack_pos.z) {\n            Vector_Add(stack, (void *)&w->item_num);\n            stack_height += step_y;\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->bounds_func = M_Bounds;\n    obj->draw_func = M_Draw;\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->collision_func = M_Collision;\n    obj->control_func = M_Control;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->add_walkable_func = M_AddWalkable;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->base_rot.y = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_position = true;\n}\n\nvoid MovableBlock_UpdateBox(const ITEM *const item, const bool blocked)\n{\n    if (blocked\n        && (item->status == IS_ACTIVE || item->status == IS_INVISIBLE\n            || (item->flags & IF_KILLED) != 0)) {\n        return;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    if (sector->floor.height == item->pos.y && sector->box != NO_BOX) {\n        BOX_INFO *const box = Box_GetBox(sector->box);\n        if (box != nullptr && (box->overlap_index & BOX_BLOCKABLE) != 0) {\n            TOGGLE_BIT(box->overlap_index, BOX_BLOCKED, blocked);\n        }\n    }\n}\n\nvoid MovableBlock_DropStack(const XYZ_32 drop_pos, int16_t room_num)\n{\n    VECTOR *stack = Vector_Create(sizeof(int16_t));\n    M_GetStack(stack, drop_pos, drop_pos.y, -WALL_L, room_num);\n\n    for (int16_t i = stack->count - 1; i >= 0; i--) {\n        const int16_t item_num = *(const int16_t *)Vector_Get(stack, i);\n        ITEM *const item = Item_Get(item_num);\n        M_SetGravityFrames(item, i);\n        item->status = IS_ACTIVE;\n        Item_AddActive(item_num);\n        Item_Animate(item);\n    }\n\n    Vector_Free(stack);\n}\n\nvoid MovableBlock_ShiftStackY(\n    int32_t stack_height, const XYZ_32 old_pos, const int32_t new_y,\n    const int16_t room_num, const bool reposition)\n{\n    VECTOR *stack = Vector_Create(sizeof(int16_t));\n    M_GetStack(stack, old_pos, stack_height, -WALL_L, room_num);\n\n    for (int16_t i = 0; i < stack->count; i++) {\n        const int16_t item_num = *(const int16_t *)Vector_Get(stack, i);\n        ITEM *const item = Item_Get(item_num);\n        item->status = IS_ACTIVE;\n        M_SetForcedMoving(item, true);\n        item->pos.y = new_y;\n        int16_t sector_room_num = room_num;\n        SECTOR *sector = Room_GetSector(\n            (XYZ_32) { item->pos.x, item->pos.y - STEP_L, item->pos.z },\n            &sector_room_num);\n        Item_UpdateRoom(item_num, sector_room_num);\n        if (reposition) {\n            const GAME_VECTOR target = {\n                .pos = item->pos,\n                .room_num = item->room_num,\n            };\n            Walkable_Reposition(item_num, M_GetLinked(item), target);\n            M_SetLinked(item);\n            item->status = IS_INACTIVE;\n            M_SetForcedMoving(item, false);\n        }\n    }\n\n    Vector_Free(stack);\n}\n\nvoid MovableBlock_SlideStack(\n    int32_t stack_height, const XYZ_32 old_pos, const ITEM *const dest_item,\n    const bool reposition)\n{\n    VECTOR *stack = Vector_Create(sizeof(int16_t));\n    M_GetStack(stack, old_pos, stack_height, -WALL_L, dest_item->room_num);\n\n    for (int16_t i = 0; i < stack->count; i++) {\n        const int16_t item_num = *(const int16_t *)Vector_Get(stack, i);\n        ITEM *const item = Item_Get(item_num);\n        item->status = IS_ACTIVE;\n        M_SetForcedMoving(item, true);\n        item->pos.x = dest_item->pos.x;\n        item->pos.z = dest_item->pos.z;\n        int16_t sector_room_num = dest_item->room_num;\n        Room_GetSector(\n            (XYZ_32) { item->pos.x, item->pos.y - STEP_L, item->pos.z },\n            &sector_room_num);\n        Item_UpdateRoom(item_num, sector_room_num);\n        if (reposition) {\n            const GAME_VECTOR target = {\n                .pos = item->pos,\n                .room_num = item->room_num,\n            };\n            Walkable_Reposition(item_num, M_GetLinked(item), target);\n            M_SetLinked(item);\n            item->status = IS_INACTIVE;\n            M_SetForcedMoving(item, false);\n        }\n    }\n\n    Vector_Free(stack);\n}\n\nREGISTER_OBJECT(O_MOVABLE_BLOCK_1, M_Setup)\nREGISTER_OBJECT(O_MOVABLE_BLOCK_2, M_Setup)\nREGISTER_OBJECT(O_MOVABLE_BLOCK_3, M_Setup)\nREGISTER_OBJECT(O_MOVABLE_BLOCK_4, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/movable_block.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n#include <trx/game/rooms.h>\n\n// Block or unblock a block's box overlap index.\nvoid MovableBlock_UpdateBox(const ITEM *item, bool blocked);\n\n// Drop a stack of blocks.\nvoid MovableBlock_DropStack(XYZ_32 drop_pos, int16_t room_num);\n\n// Shift a stack of blocks up or down in the y direction.\nvoid MovableBlock_ShiftStackY(\n    int32_t stack_height, XYZ_32 old_pos, int32_t new_y, int16_t room_num,\n    bool reposition);\n\n// Shift a stack of blocks in the x or z direction..\nvoid MovableBlock_SlideStack(\n    int32_t stack_height, XYZ_32 old_sector, const ITEM *dest_item,\n    bool reposition);\n"
  },
  {
    "path": "src/trx/game/objects/traps/pendulum.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_AXE_DAMAGE      100\n#define M_PENDULUM_DAMAGE 50\n#define M_MAX_FIRE_DIST   (WALL_L * 16) // = 16384\n#define M_FIRE_FALLOFF    11\n// clang-format on\n\ntypedef struct {\n    bool initialised;\n    bool on_fire;\n    int16_t effect_num;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"initialised\", &p->initialised));\n    JSON_SHOULD(JSON_READ(io, \"on_fire\", &p->on_fire));\n    if (g_Config.gameplay.enable_enhanced_saves) {\n        JSON_SHOULD(JSON_READ(io, \"fx_num\", &p->effect_num));\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"initialised\", p->initialised);\n    JSONW_WRITE(io, \"on_fire\", p->on_fire);\n    JSONW_WRITE(io, \"fx_num\", Effect_GetInOrderNum(p->effect_num));\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->effect_num = NO_EFFECT;\n}\n\nstatic void M_InitialiseFire(ITEM *const pendulum_item)\n{\n    M_PRIV *const p = pendulum_item->priv;\n    const OBJECT_ID fire_obj_id =\n        g_TRVersion < 3 ? O_FLAME_EMITTER : O_FLAME_EMITTER_BIG;\n    const int16_t fire_item_idx = Item_FindTypeAtPos(\n        pendulum_item->room_num, pendulum_item->pos, fire_obj_id);\n    if (fire_item_idx != NO_ITEM) {\n        ITEM *const fire_item = Item_Get(fire_item_idx);\n        Item_Kill(fire_item_idx);\n        fire_item->room_num = NO_ROOM;\n        p->on_fire = true;\n    }\n\n    p->initialised = true;\n}\n\nstatic void M_TriggerFireSparks(const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const XZ_32 delta = {\n        .x = lara_item->pos.x - item->pos.x,\n        .z = lara_item->pos.z - item->pos.z,\n    };\n    if (ABS(delta.x) > M_MAX_FIRE_DIST || ABS(delta.z) > M_MAX_FIRE_DIST) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.g = spark->src_color.r >> 1;\n    spark->src_color.b = 0;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->fade_to_black = 8;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 28;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = 0;\n    spark->pos.z = (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = (Random_GetControl() & 0x3F) - 32;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0x3F) - 32;\n    spark->friction = 4;\n    spark->flags = SPARK_F_ATTACHED_NODE | SPARK_F_ALT_SPRITE | SPARK_F_ITEM\n        | SPARK_F_SPRITE | SPARK_F_SCALE;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags |= SPARK_F_ROTATE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n        spark->rot_add = (Random_GetControl() & 0x1F) - 16;\n    }\n\n    spark->node_num = 3;\n    spark->item_num = Item_GetIndex(item);\n    spark->gravity = -16 - (Random_GetControl() & 0x1F);\n    spark->max_y_vel = -16 - (Random_GetControl() & 7);\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n    spark->size.width = (Random_GetControl() & 7) + 32;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 2;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_TriggerFireLight(const ITEM *const item)\n{\n    XYZ_32 pos = { 0, -STEP_L * 2, 0 };\n    Collide_GetJointAbsPosition(item, &pos, 5);\n    const RGB_888 color = {\n        .r = (Random_GetControl() & 0x3F) + 192,\n        .g = (Random_GetControl() & 0x1F) + 96,\n        .b = 0,\n    };\n    Output_AddDynamicLightRGB(pos, M_FIRE_FALLOFF, color);\n}\n\nstatic void M_TriggerFireEffect(const ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    EFFECT *effect = nullptr;\n    if (p->effect_num == NO_EFFECT) {\n        p->effect_num = Effect_Create(item->room_num);\n        if (p->effect_num == NO_EFFECT) {\n            return;\n        }\n        effect = Effect_Get(p->effect_num);\n        effect->object_id = O_FLAME;\n        effect->counter = 0;\n        effect->frame_num = 0;\n    } else {\n        effect = Effect_Get(p->effect_num);\n    }\n\n    XYZ_32 pos = { -32, -STEP_L - 16, 0 };\n    Collide_GetJointAbsPosition(item, &pos, 5);\n    effect->pos = pos;\n}\n\nstatic void M_KillFireEffect(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    if (p->effect_num == NO_EFFECT) {\n        return;\n    }\n\n    Effect_Kill(p->effect_num);\n    p->effect_num = NO_EFFECT;\n    if (g_TRVersion == 1) {\n        Sound_StopEffect(SFX_LOOP_FOR_SMALL_FIRES);\n    }\n}\n\nstatic inline int16_t M_GetDamage(const OBJECT_ID obj_id)\n{\n    return obj_id == O_SWINGING_AXE ? M_AXE_DAMAGE : M_PENDULUM_DAMAGE;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    item->enable_interpolation = true;\n\n    if (!p->initialised) {\n        M_InitialiseFire(item);\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const ANIM *const base_anim = Object_GetAnim(obj, 0);\n\n    bool working;\n    if (Anim_HasChange(base_anim, TRAP_WORKING)) {\n        working = item->current_anim_state == TRAP_WORKING;\n        if (Item_IsTriggerActive(item)) {\n            if (item->current_anim_state == TRAP_SET) {\n                item->goal_anim_state = TRAP_WORKING;\n            }\n        } else {\n            if (item->current_anim_state == TRAP_WORKING) {\n                item->goal_anim_state = TRAP_SET;\n            }\n        }\n    } else {\n        working = true;\n        if (!Item_IsTriggerActive(item) && Item_TestFrameEqual(item, -1)) {\n            Item_SwitchToAnim(item, 0, 0);\n            item->status = IS_INACTIVE;\n            Item_RemoveActive(item_num);\n            item->enable_interpolation = false;\n            M_KillFireEffect(item);\n            return;\n        }\n    }\n\n    if (working && item->touch_bits != 0) {\n        const int16_t damage = M_GetDamage(item->object_id);\n        Lara_TakeDamage(damage, true);\n\n        if (p->on_fire) {\n            Lara_CatchFire();\n        } else {\n            const ITEM *const lara_item = Lara_GetItem();\n            const XYZ_32 pos = {\n                .x = lara_item->pos.x + (Random_GetControl() - 0x4000) / 256,\n                .z = lara_item->pos.z + (Random_GetControl() - 0x4000) / 256,\n                .y = lara_item->pos.y - Random_GetControl() / 44,\n            };\n            Spawn_Blood(\n                pos.x, pos.y, pos.z, lara_item->speed,\n                lara_item->rot.y + (Random_GetControl() - 0x4000) / 8,\n                lara_item->room_num);\n        }\n    }\n\n    const SECTOR *const sector = Room_GetSector(item->pos, &item->room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n\n    if (p->on_fire) {\n        if (g_TRVersion >= 3) {\n            M_TriggerFireSparks(item);\n            M_TriggerFireLight(item);\n        } else {\n            M_TriggerFireEffect(item);\n        }\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        item->enable_interpolation = item->status == IS_ACTIVE;\n    }\n}\n\nstatic void M_SetupCommon(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->handle_save_func = M_HandleSave;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SetupAxe(OBJECT *const obj)\n{\n    M_SetupCommon(obj);\n    obj->collision_func = Object_Collision_Trap;\n}\n\nstatic void M_SetupPendulum(OBJECT *const obj)\n{\n    M_SetupCommon(obj);\n    obj->collision_func = Object_Collision;\n}\n\nREGISTER_OBJECT(O_SWINGING_AXE, M_SetupAxe)\nREGISTER_OBJECT(O_PENDULUM_1, M_SetupPendulum)\nREGISTER_OBJECT(O_PENDULUM_2, M_SetupPendulum)\n"
  },
  {
    "path": "src/trx/game/objects/traps/power_saw.c",
    "content": "#include <trx/game/objects.h>\n#include <trx/game/objects/traps/propeller.h>\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = Propeller_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_POWER_SAW, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/propeller.c",
    "content": "#include <trx/game/objects/traps/propeller.h>\n\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_DAMAGE 200\n\ntypedef enum {\n    // clang-format off\n    M_STATE_ON  = 0,\n    M_STATE_OFF = 1,\n    // clang-format on\n} M_STATE;\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->status == IS_ACTIVE && item->object_id == O_PROPELLER_2\n        && item->current_anim_state == M_STATE_OFF) {\n        Object_Collision(item_num, lara_item, coll);\n    } else {\n        Object_Collision_Trap(item_num, lara_item, coll);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = Propeller_Control;\n    obj->collision_func = M_Collision;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SpawnBlood(const ITEM *const item, const int32_t count)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const XYZ_32 pos = lara_item->pos;\n    Spawn_BloodBath(\n        pos.x, pos.y - WALL_L / 2, pos.z, Random_GetDraw() >> 10,\n        item->rot.y + DEG_90, lara_item->room_num, count);\n}\n\nvoid Propeller_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item) && !(item->flags & IF_ONE_SHOT)) {\n        item->goal_anim_state = M_STATE_ON;\n\n        if ((item->touch_bits & 6) != 0) {\n            Lara_TakeDamage(M_DAMAGE, true);\n            if (g_TRVersion == 3 && GF_BadGetLevelNum() == 9) {\n                // TODO: allow assigning trap damage via Lua\n                Lara_GetItem()->hit_points = -1;\n                M_SpawnBlood(item, 5);\n            }\n\n            M_SpawnBlood(item, 3);\n\n            if (item->object_id == O_POWER_SAW) {\n                Sound_Effect(SFX_SAW_STOP, &item->pos, SPM_NORMAL);\n            }\n        } else if (item->object_id == O_POWER_SAW) {\n            Sound_Effect(SFX_SAW_REVVING, &item->pos, SPM_NORMAL);\n        } else if (item->object_id == O_PROPELLER_1) {\n            Sound_Effect(SFX_AIRPLANE_IDLE, &item->pos, SPM_NORMAL);\n        } else if (item->object_id == O_PROPELLER_2) {\n            Sound_Effect(SFX_UNDERWATER_FAN_ON, &item->pos, SPM_UNDERWATER);\n        } else {\n            Sound_Effect(SFX_SMALL_FAN_ON, &item->pos, SPM_NORMAL);\n        }\n    } else if (item->goal_anim_state != M_STATE_OFF) {\n        if (item->object_id == O_PROPELLER_1) {\n            Sound_Effect(SFX_AIRPLANE_IDLE, &item->pos, SPM_NORMAL);\n        } else if (item->object_id == O_PROPELLER_2) {\n            Sound_Effect(SFX_UNDERWATER_FAN_OFF, &item->pos, SPM_UNDERWATER);\n        }\n        item->goal_anim_state = M_STATE_OFF;\n    }\n\n    Item_Animate(item);\n\n    if (item->status == IS_DEACTIVATED) {\n        Item_RemoveActive(item_num);\n        if (item->object_id != O_POWER_SAW) {\n            item->collidable = false;\n        }\n    }\n}\n\nREGISTER_OBJECT(O_PROPELLER_1, M_Setup)\nREGISTER_OBJECT(O_PROPELLER_2, M_Setup)\nREGISTER_OBJECT(O_PROPELLER_3, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/propeller.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid Propeller_Control(int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/objects/traps/raptor_emitter.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n\n#define M_MAX_SLOTS 3\n\ntypedef struct {\n    int32_t cooldown;\n    int16_t slots[M_MAX_SLOTS];\n} M_PRIV;\n\nstatic void M_SpawnRaptor(const ITEM *const spawner_item, int32_t slot_idx)\n{\n    M_PRIV *const p = spawner_item->priv;\n\n    ITEM *const raptor_item = Item_Get(p->slots[slot_idx]);\n    raptor_item->pos = spawner_item->pos;\n    raptor_item->rot = spawner_item->rot;\n    Item_SwitchToAnim(raptor_item, 0, 0);\n    raptor_item->current_anim_state =\n        Item_GetAnim(raptor_item)->current_anim_state;\n    raptor_item->goal_anim_state = raptor_item->current_anim_state;\n    raptor_item->required_anim_state = 0;\n    raptor_item->flags &= ~(IF_INVISIBLE | IF_KILLED | 3); // 3?\n    raptor_item->creature_data = nullptr;\n    raptor_item->hit_points = Object_Get(raptor_item->object_id)->hit_points;\n    raptor_item->mesh_bits = -1;\n    raptor_item->status = IS_ACTIVE;\n    raptor_item->collidable = true;\n\n    if (raptor_item->active) {\n        Item_RemoveActive(p->slots[slot_idx]);\n    }\n\n    Item_AddActive(p->slots[slot_idx]);\n    Item_UpdateRoom(p->slots[slot_idx], NO_ITEM);\n    Item_UpdateRoom(p->slots[slot_idx], spawner_item->room_num);\n    LOT_EnableBaddieAI(p->slots[slot_idx], true);\n}\n\nstatic void M_PopulateSlots(M_PRIV *const p)\n{\n    int32_t count = 0;\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (item->object_id != O_RAPTOR || !(item->ai_bits & AI_MODIFY)) {\n            continue;\n        }\n\n        p->slots[count++] = i;\n        if (count >= M_MAX_SLOTS) {\n            break;\n        }\n    }\n}\n\nstatic int32_t M_GetEmptySlot(const M_PRIV *const p)\n{\n    for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n        const ITEM *const item = Item_Get(p->slots[i]);\n        if (item->creature_data == nullptr) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"cooldown\", &p->cooldown));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"cooldown\", p->cooldown);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->cooldown = 96 * (item_num & 3);\n    for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n        p->slots[i] = NO_ITEM;\n    }\n}\n\nstatic void M_Control(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!item->active || item->timer <= 0) {\n        return;\n    }\n\n    M_PRIV *const p = item->priv;\n    if (p->slots[0] == NO_ITEM) {\n        M_PopulateSlots(p);\n        return;\n    }\n\n    int16_t m_EmptySlot = M_GetEmptySlot(p);\n    if (m_EmptySlot == -1) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dist = XYZ_32_GetDistance(lara_item->pos, item->pos);\n    if (dist < 4 * WALL_L) {\n        return;\n    }\n\n    if (p->cooldown > 0) {\n        p->cooldown--;\n        return;\n    }\n\n    p->cooldown = 255;\n    item->timer -= 30;\n    M_SpawnRaptor(item, m_EmptySlot);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->draw_func = nullptr;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_RAPTOR_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/rolling_ball.c",
    "content": "#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_DAMAGE_AIR 100\n#define M_SHAKE_RANGE (WALL_L * 10) // = 10240\n#define M_CLEARANCE_UNIT (STEP_L * 3) // = 768\n\nstatic void M_Roll(ITEM *const item)\n{\n    item->gravity = false;\n    item->fall_speed = 0;\n    item->pos.y = item->floor;\n\n    if (g_TRVersion > 1) {\n        if (item->object_id == O_ROLLING_BALL_1) {\n            Sound_Effect(SFX_ROLLING_BALL_1_ROLL, &item->pos, SPM_NORMAL);\n        } else if (item->object_id == O_ROLLING_BALL_2) {\n            Sound_Effect(SFX_ROLLING_BALL_2_ROLL, &item->pos, SPM_NORMAL);\n        } else if (item->object_id == O_ROLLING_BALL_3) {\n            Sound_Effect(SFX_ROLLING_BALL_3_ROLL, &item->pos, SPM_NORMAL);\n        } else if (item->object_id == O_ROLLING_BALL_4) {\n            Sound_Effect(SFX_ROLLING_BALL_4_ROLL, &item->pos, SPM_NORMAL);\n        }\n    }\n\n    if (g_Config.gameplay.enable_boulder_shake) {\n        XYZ_32 mic_pos = g_Camera.mic_pos.pos;\n        mic_pos.y = item->pos.y; // Ignore vertical component\n        const int32_t dist = XYZ_32_GetDistance(mic_pos, item->pos);\n        if (dist < M_SHAKE_RANGE) {\n            g_Camera.bounce = 40 * (dist - M_SHAKE_RANGE) / M_SHAKE_RANGE;\n        }\n    }\n}\n\nstatic bool M_TestStop(const ITEM *const item)\n{\n    int32_t dist;\n    switch (item->object_id) {\n    case O_ROLLING_BALL_1:\n        if (g_TRVersion == 1) {\n            dist = WALL_L / 2;\n        } else if (g_TRVersion == 2) {\n            dist = STEP_L * 3 / 2;\n        } else {\n            dist = STEP_L * 5 / 4;\n        }\n        break;\n    case O_ROLLING_BALL_4:\n        dist = WALL_L * 17 / 16;\n        break;\n    default:\n        dist = WALL_L;\n        break;\n    }\n\n    int16_t room_num = item->room_num;\n    XYZ_32 sample_pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, dist);\n    const SECTOR *sector = Room_GetSector(sample_pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, sample_pos);\n    if (height == NO_HEIGHT || height < item->pos.y) {\n        // Stop at a wall or raised floor.\n        return true;\n    }\n\n    const ANIM_FRAME *const frame = Item_GetBestFrame(item);\n    const BOUNDS_16 *const bounds = &frame->bounds;\n    int16_t item_height = ROUND_TO_CLICK_UP(ABS(bounds->max.y - bounds->min.y));\n    if (item_height > M_CLEARANCE_UNIT) {\n        item_height = (item_height / M_CLEARANCE_UNIT) * M_CLEARANCE_UNIT;\n    }\n\n    sample_pos.y -= item_height;\n    sector = Room_GetSector(sample_pos, &room_num);\n    const int32_t ceiling = Room_GetCeiling(sector, sample_pos);\n    if (ceiling == NO_HEIGHT || (ceiling > item->pos.y && !item->gravity)) {\n        // Stop at a wall or if the ceiling in front is below the floor, as long\n        // as the boulder is not falling.\n        return true;\n    }\n\n    // Stop if the gap in front is too small to logically fit.\n    return ABS(ceiling - height) < item_height;\n}\n\nstatic void M_Stop(ITEM *const item, const XYZ_32 old_pos)\n{\n    if (item->object_id == O_ROLLING_BALL_1) {\n        Sound_Effect(SFX_ROLLING_BALL_1_STOP, &item->pos, SPM_NORMAL);\n        item->status = IS_DEACTIVATED;\n    } else if (item->object_id == O_ROLLING_BALL_2) {\n        Sound_Effect(SFX_ROLLING_BALL_2_STOP, &item->pos, SPM_NORMAL);\n        item->goal_anim_state = TRAP_WORKING;\n    } else if (item->object_id == O_ROLLING_BALL_3) {\n        Sound_Effect(SFX_ROLLING_BALL_3_STOP, &item->pos, SPM_NORMAL);\n        item->goal_anim_state = TRAP_WORKING;\n    } else if (item->object_id == O_ROLLING_BALL_4) {\n        Sound_Effect(SFX_ROLLING_BALL_4_STOP, &item->pos, SPM_NORMAL);\n        item->status = IS_DEACTIVATED;\n    }\n\n    item->pos.x = old_pos.x;\n    item->pos.y = item->floor;\n    item->pos.z = old_pos.z;\n    item->speed = 0;\n    item->fall_speed = 0;\n    item->touch_bits = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    item->enable_interpolation = item->status == IS_ACTIVE;\n\n    if (item->status == IS_DEACTIVATED && !Item_IsTriggerActive(item)) {\n        Trap_Reset(item);\n        return;\n    }\n\n    if (item->status != IS_ACTIVE) {\n        int16_t room_num = item->room_num;\n        const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n        const int32_t height = Room_GetHeight(sector, item->pos);\n        if (item->floor < height) {\n            item->status = IS_ACTIVE;\n            item->floor = height;\n        }\n        return;\n    }\n\n    if (item->goal_anim_state == TRAP_WORKING) {\n        Item_Animate(item);\n        return;\n    }\n\n    if (item->pos.y < item->floor) {\n        if (!item->gravity) {\n            item->gravity = true;\n            item->fall_speed = -10;\n        }\n    } else if (item->current_anim_state == TRAP_SET) {\n        item->goal_anim_state = TRAP_ACTIVATE;\n    }\n\n    const XYZ_32 old_pos = item->pos;\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    item->floor = Room_GetHeight(sector, item->pos);\n    Room_TestTriggers(item);\n\n    if (item->pos.y >= item->floor - STEP_L) {\n        M_Roll(item);\n    }\n\n    if (M_TestStop(item)) {\n        M_Stop(item, old_pos);\n    }\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (item->status != IS_ACTIVE) {\n        if (item->status != IS_INVISIBLE) {\n            Object_Collision(item_num, lara_item, coll);\n        }\n        return;\n    }\n\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    if (lara_item->gravity || g_Config.debug.enable_invulnerability) {\n        if (coll->enable_baddie_push) {\n            Lara_Col_ItemPush(item, coll, coll->enable_hit, true);\n        }\n        if (!g_Config.debug.enable_invulnerability) {\n            lara_item->hit_points -= M_DAMAGE_AIR;\n        }\n\n        // TODO: handle overflows\n        const int32_t dx = lara_item->pos.x - item->pos.x;\n        const int32_t dy =\n            (lara_item->pos.y - 350) - (item->pos.y - WALL_L / 2);\n        const int32_t dz = lara_item->pos.z - item->pos.z;\n        int32_t dist = Math_Sqrt(SQUARE(dx) + SQUARE(dy) + SQUARE(dz));\n        CLAMPL(dist, WALL_L / 2);\n\n        Spawn_Blood(\n            item->pos.x + (dx * WALL_L / 2) / dist,\n            item->pos.y + (dy * WALL_L / 2) / dist - WALL_L / 2,\n            item->pos.z + (dz * WALL_L / 2) / dist, item->speed, item->rot.y,\n            item->room_num);\n    } else {\n        lara_item->hit_status = true;\n        if (lara_item->hit_points > 0) {\n            lara_item->hit_points = -1;\n            Item_UpdateRoom(lara->item_num, item->room_num);\n\n            lara_item->rot.x = 0;\n            lara_item->rot.y = item->rot.y;\n            lara_item->rot.z = 0;\n\n            Item_SwitchToAnim(lara_item, LA(LA_BOULDER_DEATH), 0);\n            lara_item->goal_anim_state =\n                Item_GetAnim(lara_item)->current_anim_state;\n            lara_item->current_anim_state = lara_item->goal_anim_state;\n\n            g_Camera.flags = CF_FOLLOW_CENTRE;\n            g_Camera.target_angle = 170 * DEG_1;\n            g_Camera.target_elevation = -25 * DEG_1;\n\n            for (int32_t i = 0; i < 15; i++) {\n                Spawn_Blood(\n                    lara_item->pos.x + (Random_GetControl() - 0x4000) / 256,\n                    lara_item->pos.y - Random_GetControl() / 64,\n                    lara_item->pos.z + (Random_GetControl() - 0x4000) / 256,\n                    2 * item->speed,\n                    item->rot.y + (Random_GetControl() - 0x4000) / 8,\n                    item->room_num);\n            }\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = Trap_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n    obj->load_floor = true;\n}\n\nREGISTER_OBJECT(O_ROLLING_BALL_1, M_Setup)\nREGISTER_OBJECT(O_ROLLING_BALL_2, M_Setup)\nREGISTER_OBJECT(O_ROLLING_BALL_3, M_Setup)\nREGISTER_OBJECT(O_ROLLING_BALL_4, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/rotating_laser.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/game/items/col.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\ntypedef struct {\n    XYZ_32 origin;\n    XYZ_32 target;\n    int16_t direction;\n    int16_t velocity;\n    int16_t reverse_timer;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"origin\", &p->origin));\n    JSON_SHOULD(JSON_READ(io, \"target\", &p->target));\n    JSON_SHOULD(JSON_READ(io, \"direction\", &p->direction));\n    JSON_SHOULD(JSON_READ(io, \"velocity\", &p->velocity));\n    JSON_SHOULD(JSON_READ(io, \"reverse_timer\", &p->reverse_timer));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"origin\", p->origin);\n    JSONW_WRITE(io, \"target\", p->target);\n    JSONW_WRITE(io, \"direction\", p->direction);\n    JSONW_WRITE(io, \"velocity\", p->velocity);\n    JSONW_WRITE(io, \"reverse_timer\", p->reverse_timer);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->origin = item->pos;\n    p->target = XYZ_32_OffsetYaw(item->pos, item->rot.y, 2560);\n    p->direction = 1;\n    p->velocity = 0;\n    p->reverse_timer = 0;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    item->current_anim_state = 0;\n    Output_AddDynamicLightRGB(\n        (XYZ_32) { item->pos.x, item->pos.y - 64, item->pos.z },\n        (Random_GetControl() & 1) + 8,\n        (RGB_888) {\n            (Random_GetControl() & 0x1F) + 192,\n            Random_GetControl() & 0x1F,\n            Random_GetControl() & 7,\n        });\n    item->mesh_bits = -1 - (Random_GetControl() & 0x14);\n    const int32_t dx = ABS(p->target.x - item->pos.x);\n    const int32_t dz = ABS(p->target.z - item->pos.z);\n\n    if (dx < 768 && dz < 768) {\n        p->reverse_timer = 32;\n        p->target =\n            XYZ_32_OffsetYaw(p->origin, item->rot.y, -2560 * p->direction);\n    }\n\n    if (p->direction == 1) {\n        if (p->reverse_timer != 0) {\n            if (p->velocity != 0) {\n                if (p->velocity > 4) {\n                    p->velocity -= p->velocity >> 2;\n                } else {\n                    p->velocity = 0;\n                }\n            } else {\n                p->reverse_timer--;\n                if (p->reverse_timer == 1) {\n                    p->direction = -1;\n                }\n            }\n        } else {\n            p->velocity += 5;\n            CLAMPG(p->velocity, 512);\n        }\n    } else if (p->reverse_timer != 0) {\n        if (p->velocity != 0) {\n            if (p->velocity < -4) {\n                p->velocity -= p->velocity >> 2;\n            } else {\n                p->velocity = 0;\n            }\n        } else {\n            p->reverse_timer--;\n            if (p->reverse_timer == 1) {\n                p->direction = -p->direction;\n            }\n        }\n    } else {\n        p->velocity -= 5;\n        CLAMPL(p->velocity, -512);\n    }\n\n    item->pos.x += (p->velocity * Math_Sin(item->rot.y)) >> (W2V_SHIFT + 2);\n    item->pos.z += (p->velocity * Math_Cos(item->rot.y)) >> (W2V_SHIFT + 2);\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n\n    if (room_num != item->room_num) {\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (Item_TestBoundsCollide(item, lara_item, 64)) {\n        Lara_TakeDamage(25, false);\n        Spawn_BloodBathD(\n            lara_item->pos.x, item->pos.y - (Random_GetControl() & 0xFF) - 32,\n            lara_item->pos.z, (Random_GetControl() & 0x7F) + 128,\n            (int16_t)(Random_GetControl() << 1), lara_item->room_num, 3);\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_ROTATING_LASER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/security_laser.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/lara.h>\n#include <trx/game/los.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\n#define M_DAMAGE 10\n\nstatic uint8_t m_LaserShades[32] = {};\nstatic const int16_t m_DefaultBeamCount = 1;\n\nstatic bool M_IsTargetable(const ITEM *const item)\n{\n    return false;\n}\n\nstatic void M_LaserSplitterToggle(ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    SECTOR *sector = Room_GetSector(item->pos, &room_num);\n\n    if ((Box_GetBox(sector->box)->overlap_index & BOX_BLOCKED_SEARCH) == 0) {\n        return;\n    }\n\n    const bool is_active = Item_IsTriggerActive(item);\n\n    if (is_active\n        == ((Box_GetBox(sector->box)->overlap_index & BOX_BLOCKED)\n            == BOX_BLOCKED)) {\n        return;\n    }\n\n    XZ_32 step;\n    switch (item->rot.y) {\n    case 0:\n        step = (XZ_32) { 0, -WALL_L };\n        break;\n    case DEG_90:\n        step = (XZ_32) { -WALL_L, 0 };\n        break;\n    case -DEG_180:\n        step = (XZ_32) { 0, WALL_L };\n        break;\n    default:\n        step = (XZ_32) { WALL_L, 0 };\n        break;\n    }\n\n    int32_t x = item->pos.x;\n    int32_t z = item->pos.z;\n    while (sector->box != NO_BOX\n           && (Box_GetBox(sector->box)->overlap_index & BOX_BLOCKED_SEARCH)\n               != 0) {\n        if (is_active) {\n            Box_GetBox(sector->box)->overlap_index |= BOX_BLOCKED;\n        } else {\n            Box_GetBox(sector->box)->overlap_index &= ~BOX_BLOCKED;\n        }\n\n        x += step.x;\n        z += step.z;\n        sector = Room_GetSector((XYZ_32) { x, item->pos.y, z }, &room_num);\n    }\n}\n\nstatic void M_UpdateLaserShades(void)\n{\n    for (int32_t shade_idx = 0; shade_idx < 32; shade_idx++) {\n        uint8_t shade = m_LaserShades[shade_idx];\n        int32_t random_value = Random_GetDraw();\n\n        if (random_value < 1024) {\n            random_value = (random_value & 0xF) + 16;\n        } else if (random_value < 4096) {\n            random_value &= 7;\n        } else if ((random_value & 0x70) == 0) {\n            random_value &= 3;\n        } else {\n            random_value = 0;\n        }\n\n        if (random_value != 0) {\n            shade += (uint8_t)random_value;\n\n            if (shade > 127) {\n                shade = 127;\n            }\n        } else if (shade > 16) {\n            shade -= shade >> 3;\n        } else {\n            shade = 16;\n        }\n\n        m_LaserShades[shade_idx] = shade;\n    }\n}\n\nstatic RGB_888 M_GetLaserColor(const OBJECT_ID object_id)\n{\n    switch (object_id) {\n    case O_SECURITY_LASER_ALARM:\n        return (RGB_888) { 0, UINT8_MAX, 0 };\n    case O_SECURITY_LASER_DEADLY:\n        return (RGB_888) { UINT8_MAX, UINT8_MAX, 0 };\n    default:\n        return (RGB_888) { UINT8_MAX, UINT8_MAX >> 2, 0 };\n    }\n}\n\nstatic XZ_32 M_GetLaserDirection(const ITEM *const item)\n{\n    const int32_t edge_offset = WALL_L / 2 - 1;\n    switch (item->rot.y) {\n    case 0:\n        return (XZ_32) { 0, edge_offset };\n    case DEG_90:\n        return (XZ_32) { edge_offset, 0 };\n    case -DEG_180:\n        return (XZ_32) { 0, -edge_offset };\n    default:\n        return (XZ_32) { -edge_offset, 0 };\n    }\n}\n\nstatic bool M_GetLaserSegment(\n    const ITEM *const item, const XZ_32 dir, const int32_t y,\n    GAME_VECTOR *const s, GAME_VECTOR *const t)\n{\n    s->x = item->pos.x + dir.x;\n    s->y = item->pos.y + y;\n    s->z = item->pos.z + dir.z;\n    s->room_num = item->room_num;\n\n    t->x = item->pos.x - (dir.x << 5);\n    t->y = item->pos.y + y;\n    t->z = item->pos.z - (dir.z << 5);\n\n    LOS_Check(s, t, true);\n    return LOS_CheckItemIntersectSegment(s, t, Lara_GetItem());\n}\n\nstatic void M_DrawLaserBeam(\n    const GAME_VECTOR *const src, const GAME_VECTOR *const dest,\n    const RGB_888 color)\n{\n    const XYZ_32 beam_delta = {\n        .x = dest->x - src->x,\n        .y = dest->y - src->y,\n        .z = dest->z - src->z,\n    };\n\n    int32_t segment_count =\n        XYZ_32_GetDistance(\n            (XYZ_32) { src->x, 0, src->z }, (XYZ_32) { dest->x, 0, dest->z })\n        >> 9;\n    CLAMP(segment_count, 8, 32);\n\n    XYZ_32 segment_start = src->pos;\n    for (int32_t segment_idx = 0; segment_idx < segment_count; segment_idx++) {\n        const float segment_end_ratio =\n            (float)(segment_idx + 1) / segment_count;\n        const XYZ_32 segment_end = {\n            .x = src->x + (int32_t)(beam_delta.x * segment_end_ratio),\n            .y = src->y + (int32_t)(beam_delta.y * segment_end_ratio),\n            .z = src->z + (int32_t)(beam_delta.z * segment_end_ratio),\n        };\n\n        RGBA_8888 from_color = COLOR_RGBA_8888_BLACK;\n        RGBA_8888 to_color = COLOR_RGBA_8888_BLACK;\n\n        if (segment_idx > 0) {\n            const uint8_t shade = m_LaserShades[segment_idx];\n            from_color = (RGBA_8888) {\n                .r = (uint8_t)((shade * color.r) / UINT8_MAX),\n                .g = (uint8_t)((shade * color.g) / UINT8_MAX),\n                .b = (uint8_t)((shade * color.b) / UINT8_MAX),\n                .a = 0xFF,\n            };\n        }\n\n        if (segment_idx + 1 < segment_count) {\n            const uint8_t shade = m_LaserShades[segment_idx + 1];\n            to_color = (RGBA_8888) {\n                .r = (uint8_t)((shade * color.r) / UINT8_MAX),\n                .g = (uint8_t)((shade * color.g) / UINT8_MAX),\n                .b = (uint8_t)((shade * color.b) / UINT8_MAX),\n                .a = 0xFF,\n            };\n        }\n\n        OutputSource_PolyFX_StageLineSegment(\n            segment_start, from_color, segment_end, to_color, 4.0f,\n            DRAW_BLEND_ADD);\n        segment_start = segment_end;\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_LaserSplitterToggle(item);\n}\n\nstatic void M_DamageLara(const ITEM *const item, const int32_t beam_y)\n{\n    if (item->object_id == O_SECURITY_LASER_ALARM) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (item->object_id == O_SECURITY_LASER_KILLER) {\n        Lara_TakeDamage(lara_item->hit_points, false);\n    } else {\n        Lara_TakeDamage(M_DAMAGE, false);\n    }\n\n    Spawn_BloodBath(\n        lara_item->pos.x, item->pos.y + beam_y, lara_item->pos.z,\n        (Random_GetDraw() & 0x7F) + 128, Random_GetDraw() << 1,\n        lara_item->room_num, 1);\n}\n\nstatic void M_ActivateTriggers(const ITEM *const item)\n{\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const target_item = Item_Get(i);\n        if ((target_item->object_id != O_STROBE_LIGHT\n             && target_item->object_id != O_SENTRY_GUN)\n            || !Item_IsTriggerActive(target_item)) {\n            continue;\n        }\n\n        const OBJECT *const target_obj = Object_Get(target_item->object_id);\n        if (target_obj->event_func != nullptr) {\n            target_obj->event_func(target_item, OBJECT_EVENT_ALERT, nullptr);\n        }\n    }\n\n    Room_TestTriggers(item);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_LaserSplitterToggle(item);\n\n    if (!Item_IsTriggerActiveRO(item)) {\n        return;\n    }\n\n    M_UpdateLaserShades();\n\n    const XZ_32 direction = M_GetLaserDirection(item);\n\n    int32_t beam_y = 0;\n    bool tripped = false;\n    for (int32_t beam_idx = 0; beam_idx < item->hit_points; beam_idx++) {\n        GAME_VECTOR start;\n        GAME_VECTOR target;\n\n        if (M_GetLaserSegment(item, direction, beam_y, &start, &target)) {\n            tripped = true;\n            M_DamageLara(item, beam_y);\n        }\n        beam_y -= 256;\n    }\n\n    if (tripped) {\n        M_ActivateTriggers(item);\n    }\n}\n\nstatic bool M_Trigger(ITEM *const item, const TRIGGER *const trigger)\n{\n    if (trigger != nullptr && trigger->type != TT_ANTIPAD\n        && trigger->type != TT_ANTITRIGGER) {\n        item->hit_points = trigger->timer & 7;\n        if (item->hit_points == 0) {\n            item->hit_points = 1;\n        }\n        item->max_hit_points = item->hit_points;\n        item->timer = 0;\n    }\n\n    return true;\n}\n\nstatic bool M_DrawLaser(const ITEM *const item)\n{\n    const RGB_888 color = M_GetLaserColor(item->object_id);\n\n    if (!Item_IsTriggerActiveRO(item)) {\n        return false;\n    }\n\n    const XZ_32 direction = M_GetLaserDirection(item);\n    int32_t beam_y = 0;\n    for (int32_t beam_idx = 0; beam_idx < item->hit_points; beam_idx++) {\n        GAME_VECTOR start;\n        GAME_VECTOR target;\n        M_GetLaserSegment(item, direction, beam_y, &start, &target);\n        M_DrawLaserBeam(&start, &target, color);\n        beam_y -= 256;\n    }\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->draw_func = M_DrawLaser;\n    obj->is_targetable_func = M_IsTargetable;\n    obj->trigger_func = M_Trigger;\n    obj->hit_points = m_DefaultBeamCount;\n\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_SECURITY_LASER_ALARM, M_Setup)\nREGISTER_OBJECT(O_SECURITY_LASER_DEADLY, M_Setup)\nREGISTER_OBJECT(O_SECURITY_LASER_KILLER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/sentry_gun.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/creature.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n\ntypedef enum {\n    M_STATE_FIRE,\n    M_STATE_STILL,\n} M_STATE;\n\ntypedef enum {\n    M_MUZZLE_LEFT,\n    M_MUZZLE_RIGHT,\n} M_MUZZLE;\n\ntypedef struct {\n    int16_t active_muzzle;\n    int16_t muzzle_flash_timer;\n    bool is_alerted;\n    bool has_fired;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_OPTIONAL(JSON_READ(io, \"active_muzzle\", &p->active_muzzle));\n    JSON_OPTIONAL(JSON_READ(io, \"muzzle_flash_timer\", &p->muzzle_flash_timer));\n    JSON_OPTIONAL(JSON_READ(io, \"is_alerted\", &p->is_alerted));\n    JSON_OPTIONAL(JSON_READ(io, \"has_fired\", &p->has_fired));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"active_muzzle\", p->active_muzzle);\n    JSONW_WRITE(io, \"muzzle_flash_timer\", p->muzzle_flash_timer);\n    JSONW_WRITE(io, \"is_alerted\", p->is_alerted);\n    JSONW_WRITE(io, \"has_fired\", p->has_fired);\n}\n\nstatic const CREATURE_GUN m_FireLeft = {\n    .muzzle = {\n        .pos = { .x = 110, .y = -30, .z = -530 },\n        .mesh_num = 2,\n    },\n    .tr3_enemy_flash = true,\n    .tr3_flash = {\n        .pos = { .x = 110, .y = -30, .z = -530 },\n        .mesh_num = 2,\n    },\n    .tr3_enemy_weapon_flags = 1,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_180,\n};\n\nstatic const CREATURE_GUN m_FireRight = {\n    .muzzle = {\n        .pos = { .x = -110, .y = -30, .z = -530 },\n        .mesh_num = 2,\n    },\n    .tr3_enemy_flash = true,\n    .tr3_flash = {\n        .pos = { .x = -110, .y = -30, .z = -530 },\n        .mesh_num = 2,\n    },\n    .tr3_enemy_weapon_flags = 1,\n    .tr3_flash_shade = 600,\n    .tr3_flash_rot_x = -DEG_180,\n};\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    Item_SwitchToAnim(item, 1, 0);\n    item->current_anim_state = M_STATE_STILL;\n    item->goal_anim_state = M_STATE_STILL;\n    p->active_muzzle = M_MUZZLE_LEFT;\n    p->muzzle_flash_timer = 0;\n    p->is_alerted = false;\n    p->has_fired = false;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (p->muzzle_flash_timer > 1) {\n        p->muzzle_flash_timer--;\n\n        XYZ_32 pos;\n        Matrix_Push();\n        if ((Random_GetControl() & 1) != 0) {\n            p->active_muzzle = M_MUZZLE_LEFT;\n            pos = m_FireLeft.muzzle.pos;\n            Collide_GetJointAbsPosition(item, &pos, m_FireLeft.muzzle.mesh_num);\n        } else {\n            p->active_muzzle = M_MUZZLE_RIGHT;\n            pos = m_FireRight.muzzle.pos;\n            Collide_GetJointAbsPosition(\n                item, &pos, m_FireRight.muzzle.mesh_num);\n        }\n\n        const RGB_888 color = { 192, 128, 32 };\n        Output_AddDynamicLightRGB(pos, 2 * p->muzzle_flash_timer + 8, color);\n        Matrix_Pop();\n    }\n\n    if (!Creature_Activate(item_num)) {\n        return;\n    }\n\n    CREATURE *const creature = item->creature_data;\n    if (creature == nullptr) {\n        return;\n    }\n\n    if (item->hit_status) {\n        p->is_alerted = true;\n    }\n\n    if (item->hit_points <= 0) {\n        Item_Explode(item_num, -1, 0);\n        LOT_DisableBaddieAI(item_num);\n        Item_Kill(item_num);\n        item->flags |= IF_INVISIBLE;\n        item->status = IS_DEACTIVATED;\n    }\n\n    if (!p->is_alerted) {\n        return;\n    }\n\n    AI_INFO info;\n    Creature_AIInfo(item, &info);\n\n    const int16_t tilt = -info.x_angle;\n\n    switch (item->current_anim_state) {\n    case M_STATE_FIRE:\n        if (!Creature_CanTargetEnemy(item, &info)) {\n            item->goal_anim_state = M_STATE_STILL;\n        } else if (Item_GetRelativeFrame(item) == 0) {\n            p->has_fired = true;\n\n            if (p->active_muzzle == M_MUZZLE_RIGHT) {\n                Creature_Shoot(\n                    item, &info, &m_FireLeft, creature->joint_rotation[0], 10);\n            } else {\n                Creature_Shoot(\n                    item, &info, &m_FireRight, creature->joint_rotation[0], 10);\n            }\n\n            p->muzzle_flash_timer = 10;\n            Sound_Effect(SFX_LARA_UZI_STOP, &item->pos, SPM_NORMAL);\n        }\n        break;\n\n    case M_STATE_STILL:\n        if (Creature_CanTargetEnemy(item, &info) && !p->has_fired) {\n            item->goal_anim_state = M_STATE_FIRE;\n        } else if (p->has_fired) {\n            if (item->ai_bits == AI_MODIFY) {\n                item->goal_anim_state = M_STATE_FIRE;\n            } else {\n                p->is_alerted = false;\n                p->has_fired = false;\n            }\n        }\n        break;\n    }\n\n    int16_t diff = info.angle - creature->joint_rotation[0];\n    CLAMP(diff, -DEG_1 * 10, DEG_1 * 10);\n\n    creature->joint_rotation[0] += diff;\n    Creature_Joint(item, 1, tilt);\n    Item_Animate(item);\n\n    if (info.angle > 0x4000) {\n        item->rot.y += 0x8000;\n        if (info.angle > 0 || info.angle < 0) {\n            creature->joint_rotation[0] += 0x8000;\n        }\n    } else if (info.angle < -0x4000) {\n        item->rot.y += 0x8000;\n        if (info.angle > 0 || info.angle < 0) {\n            creature->joint_rotation[0] += 0x8000;\n        }\n    }\n}\n\nstatic void M_HandleEvent(\n    ITEM *const item, const OBJECT_EVENT event, const void *const data)\n{\n    M_PRIV *const p = item->priv;\n    if (event != OBJECT_EVENT_ALERT) {\n        return;\n    }\n\n    p->is_alerted = true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Creature_Collision;\n    obj->event_func = M_HandleEvent;\n\n    obj->shadow_size = 0;\n    obj->hit_points = 100;\n    obj->radius = 102;\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 0)->rot.y = true;\n    Object_GetBone(obj, 1)->rot.x = true;\n}\n\nREGISTER_OBJECT(O_SENTRY_GUN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/sliding_pillar.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/const.h>\n#include <trx/game/items/walkable.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/movable_block.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    // clang-format off\n    PILLAR_STATE_READY_FORWARD = 0,\n    PILLAR_STATE_READY_BACK    = 1,\n    PILLAR_STATE_MOVING        = 2,\n    // clang-format on\n} PILLAR_STATE;\n\ntypedef enum {\n    // clang-format off\n    PILLAR_ANIM_READY_FORWARD = 0,\n    PILLAR_ANIM_READY_BACK    = 1,\n    PILLAR_ANIM_FORWARD       = 2,\n    PILLAR_ANIM_BACK          = 3,\n    // clang-format on\n} PILLAR_ANIM;\n\ntypedef struct {\n    GAME_VECTOR initial;\n    GAME_VECTOR linked;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    if (JSON_SHOULD(JSON_PUSH(io, \"linked\"))) {\n        JSON_SHOULD(JSON_READ(io, \"x\", &p->linked.pos.x));\n        JSON_SHOULD(JSON_READ(io, \"y\", &p->linked.pos.y));\n        JSON_SHOULD(JSON_READ(io, \"z\", &p->linked.pos.z));\n        JSON_SHOULD(JSON_POP(io));\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"x\", p->linked.pos.x);\n    JSONW_WRITE(io, \"y\", p->linked.pos.y);\n    JSONW_WRITE(io, \"z\", p->linked.pos.z);\n    JSONW_POP_AND_SET(io, \"linked\");\n}\n\nstatic void M_SetInitial(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    p->initial.pos = item->pos;\n    p->initial.room_num = item->room_num;\n}\n\nstatic GAME_VECTOR M_GetInitial(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->initial;\n}\n\nstatic void M_SetLinked(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    p->linked.pos = item->pos;\n    p->linked.room_num = item->room_num;\n}\n\nstatic GAME_VECTOR M_GetLinked(const ITEM *const item)\n{\n    const M_PRIV *const p = item->priv;\n    return p->linked;\n}\n\nstatic bool M_IsItemOnTop(\n    const ITEM *const item, const int32_t x, const int32_t z)\n{\n    int32_t dx = x - item->pos.x;\n    int32_t dz = z - item->pos.z;\n\n    // Movable blocks' bounds don't match sector so estimate.\n    return (dx >= -WALL_L / 2 && dx < WALL_L / 2)\n        && (dz >= -WALL_L / 2 && dz < WALL_L / 2);\n}\n\nstatic int16_t M_GetFloorHeight(\n    const ITEM *const item, int32_t x, int32_t y, int32_t z, int16_t height)\n{\n    if (item->status == IS_INVISIBLE) {\n        return height;\n    }\n\n    if (item->current_anim_state == PILLAR_STATE_MOVING) {\n        return height;\n    }\n\n    if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    }\n\n    // If under the bottom of the block.\n    if (y > item->pos.y) {\n        return height;\n    }\n\n    // If the the top of the block is under the floor height.\n    if (item->pos.y - WALL_L * 2 >= height) {\n        return height;\n    }\n\n    return item->pos.y - WALL_L * 2;\n}\n\nstatic int16_t M_GetCeilingHeight(\n    const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height)\n{\n    if (item->status == IS_INVISIBLE) {\n        return height;\n    }\n\n    if (item->current_anim_state == PILLAR_STATE_MOVING) {\n        return height;\n    }\n\n    // Only care if we are inside the block footprint.\n    if (!M_IsItemOnTop(item, x, z)) {\n        return height;\n    }\n\n    // If above the top of the block.\n    if (y <= item->pos.y - WALL_L * 2) {\n        return height;\n    }\n\n    // If the the bottom of the block is above the ceiling height.\n    if (item->pos.y <= height) {\n        return height;\n    }\n\n    return item->pos.y;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    MovableBlock_UpdateBox(item, false);\n    M_SetInitial(item);\n    M_SetLinked(item);\n    Walkable_AllocateNodes(item, 1);\n}\n\nstatic void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage)\n{\n    if (stage == SAVEGAME_STAGE_BEFORE_LOAD) {\n        MovableBlock_UpdateBox(item, false);\n    } else if (stage == SAVEGAME_STAGE_AFTER_LOAD) {\n        const int16_t item_num = Item_GetIndex(item);\n        // Reposition walkable to its linked sector.\n        Walkable_Reposition(item_num, M_GetInitial(item), M_GetLinked(item));\n        MovableBlock_UpdateBox(item, true);\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (Item_IsTriggerActive(item)) {\n        if (item->current_anim_state == PILLAR_STATE_READY_FORWARD) {\n            item->goal_anim_state = PILLAR_STATE_READY_BACK;\n        }\n    } else if (item->current_anim_state == PILLAR_STATE_READY_BACK) {\n        item->goal_anim_state = PILLAR_STATE_READY_FORWARD;\n    }\n\n    Item_Animate(item);\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    const GAME_VECTOR linked = M_GetLinked(item);\n\n    if (item->status == IS_ACTIVE\n        && (item->pos.x != linked.pos.x || item->pos.z != linked.pos.z)) {\n        MovableBlock_SlideStack(\n            item->pos.y - WALL_L * 2, linked.pos, item, false);\n        MovableBlock_UpdateBox(item, false);\n    } else if (\n        item->status == IS_DEACTIVATED\n        && (item->pos.x != linked.pos.x || item->pos.z != linked.pos.z)) {\n        item->pos.x &= -WALL_L;\n        item->pos.x += WALL_L / 2;\n        item->pos.z &= -WALL_L;\n        item->pos.z += WALL_L / 2;\n        const GAME_VECTOR target = {\n            .pos = item->pos,\n            .room_num = item->room_num,\n        };\n        Walkable_Reposition(item_num, linked, target);\n        M_SetLinked(item);\n\n        // Reposition possible movable blocks on top.\n        MovableBlock_SlideStack(\n            item->pos.y - WALL_L * 2, linked.pos, item, true);\n\n        MovableBlock_UpdateBox(item, true);\n        item->status = IS_ACTIVE;\n    }\n}\n\nstatic void M_AddWalkable(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n    Walkable_Add(item_num, item->pos);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->handle_save_func = M_HandleSave;\n    obj->control_func = M_Control;\n    obj->floor_height_func = M_GetFloorHeight;\n    obj->ceiling_height_func = M_GetCeilingHeight;\n    obj->save_position = true;\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->add_walkable_func = M_AddWalkable;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n}\n\nREGISTER_OBJECT(O_SLIDING_PILLAR, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/spike_ceiling.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define M_DAMAGE 20\n#define M_SPEED 1\n#define M_STEP_SLOW 5\n#define M_STEP_FAST 10\n\ntypedef struct {\n    int32_t step;\n    bool animate;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"step\", &p->step));\n    JSON_SHOULD(JSON_READ(io, \"animate\", &p->animate));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"step\", p->step);\n    JSONW_WRITE(io, \"animate\", p->animate);\n}\n\nstatic bool M_Trigger(ITEM *const item, const TRIGGER *const trigger)\n{\n    M_PRIV *const p = item->priv;\n    if (p == nullptr) {\n        return true;\n    }\n\n    if (trigger == nullptr || trigger->type == TT_ANTITRIGGER\n        || trigger->type == TT_ANTIPAD) {\n        return true;\n    }\n\n    item->timer = 0;\n    if (trigger->timer == 1) {\n        p->step = M_STEP_FAST;\n        p->animate = true;\n    } else {\n        p->step = M_STEP_SLOW;\n        p->animate = false;\n    }\n    return true;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    Trap_Initialise(item_num);\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    if (p != nullptr) {\n        p->step = M_STEP_SLOW;\n        p->animate = false;\n    }\n}\n\nstatic void M_Move(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const M_PRIV *const p = item->priv;\n    const int32_t step = (p != nullptr && p->step > 0) ? p->step : M_STEP_SLOW;\n    int16_t room_num = item->room_num;\n    const XYZ_32 pos = { item->pos.x, item->pos.y + step, item->pos.z };\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    if (Room_GetHeight(sector, pos) < pos.y + WALL_L) {\n        item->status = IS_DEACTIVATED;\n        Sound_StopEffect(SFX_SPIKE_WALL);\n    } else {\n        item->pos.y = pos.y;\n        Item_UpdateRoom(item_num, room_num);\n        Sound_Effect(SFX_SPIKE_WALL, &item->pos, SPM_NORMAL);\n    }\n}\n\nstatic void M_HitLara(ITEM *const item)\n{\n    Lara_TakeDamage(M_DAMAGE, true);\n\n    const ITEM *const lara_item = Lara_GetItem();\n    Spawn_BloodBath(\n        lara_item->pos.x, item->pos.y + LARA_HEIGHT, lara_item->pos.z, M_SPEED,\n        item->rot.y, lara_item->room_num, 3);\n    item->touch_bits = 0;\n\n    Sound_Effect(SFX_LARA_FLESH_WOUND, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_IsTriggerActive(item)) {\n        Trap_Reset(item);\n    } else if (item->status != IS_DEACTIVATED) {\n        M_Move(item_num);\n    }\n\n    if (item->touch_bits) {\n        M_HitLara(item);\n    }\n\n    if (Item_IsTriggerActive(item) && item->status != IS_DEACTIVATED) {\n        const M_PRIV *const p = item->priv;\n        if (p != nullptr && p->animate) {\n            Item_Animate(item);\n        }\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision_Trap;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->trigger_func = M_Trigger;\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->save_position = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_CEILING_SPIKES, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/spike_wall.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/traps/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define M_DAMAGE 20\n#define M_SPEED 1\n\nstatic void M_Move(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, M_SPEED << 4);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n\n    if (Room_GetHeight(sector, pos) != pos.y) {\n        item->status = IS_DEACTIVATED;\n        Sound_StopEffect(SFX_SPIKE_WALL);\n    } else {\n        item->pos = pos;\n        Item_UpdateRoom(item_num, room_num);\n    }\n\n    Sound_Effect(SFX_SPIKE_WALL, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_HitLara(ITEM *const item)\n{\n    Lara_TakeDamage(M_DAMAGE, true);\n\n    const ITEM *const lara_item = Lara_GetItem();\n    Spawn_BloodBath(\n        lara_item->pos.x, lara_item->pos.y - WALL_L / 2, lara_item->pos.z,\n        M_SPEED, item->rot.y, lara_item->room_num, 3);\n    item->touch_bits = 0;\n\n    Sound_Effect(SFX_LARA_FLESH_WOUND, &item->pos, SPM_NORMAL);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (!Item_IsTriggerActive(item)) {\n        Trap_Reset(item);\n    } else if (item->status != IS_DEACTIVATED) {\n        M_Move(item_num);\n    }\n\n    if (item->touch_bits) {\n        M_HitLara(item);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = Trap_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_position = true;\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_SPIKE_WALL, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/spikes.c",
    "content": "#include <trx/config.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/version.h>\n\n#define M_FALL_SPEED_LIMIT (g_TRVersion == 1 ? 0 : GRAVITY)\n#define M_DAMAGE 15\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (lara_item->hit_points < 0) {\n        return;\n    }\n\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    int32_t blood_spawn_count = Random_GetControl() / 0x6000;\n    if (lara_item->gravity) {\n        if (lara_item->fall_speed > M_FALL_SPEED_LIMIT\n            && !g_Config.debug.enable_invulnerability) {\n            lara_item->hit_points = -1;\n            blood_spawn_count = 20;\n        }\n    } else if (lara_item->speed < 30) {\n        return;\n    }\n\n    lara_item->hit_points -= M_DAMAGE;\n    for (int32_t i = 0; i < blood_spawn_count; i++) {\n        const XYZ_32 pos = {\n            .x = lara_item->pos.x + (Random_GetControl() - 0x4000) / 256,\n            .z = lara_item->pos.z + (Random_GetControl() - 0x4000) / 256,\n            .y = lara_item->pos.y - Random_GetControl() / 64,\n        };\n        Spawn_Blood(\n            pos.x, pos.y, pos.z, 20, Random_GetControl(), item->room_num);\n    }\n\n    if (lara_item->hit_points <= 0) {\n        Item_SwitchToAnim(lara_item, LA(LA_SPIKE_DEATH), 0);\n        lara_item->current_anim_state = LS(LS_DEATH);\n        lara_item->goal_anim_state = LS(LS_DEATH);\n        lara_item->pos.y = item->pos.y;\n        lara_item->gravity = false;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    const int32_t level_num = GF_BadGetLevelNum();\n    if (level_num != 5 && level_num != 7) {\n        return;\n    }\n\n    if (Item_GetRelativeFrame(item) == 0) {\n        if (level_num == 5) {\n            Sound_Effect(SFX_SHIVA_SWORD_2, &item->pos, SPM_ALWAYS);\n        } else {\n            Sound_Effect(SFX_LARA_GET_OUT, &item->pos, SPM_ALWAYS);\n        }\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->collision_func = M_Collision;\n    if (g_TRVersion == 3) {\n        obj->control_func = M_Control;\n    }\n}\n\nREGISTER_OBJECT(O_SPIKES, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/spinning_blade.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define M_DAMAGE 100\n\ntypedef enum {\n    // clang-format off\n    M_STATE_NULL = 0,\n    M_STATE_STOP  = 1,\n    M_STATE_SPIN  = 2,\n    // clang-format on\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_SPIN_FAST = 0,\n    M_ANIM_SPIN_SLOW = 1,\n    M_ANIM_SPIN_END  = 2,\n    M_ANIM_STOP      = 3,\n    // clang-format on\n} M_ANIM;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    Item_SwitchToAnim(item, M_ANIM_STOP, 0);\n    item->current_anim_state = M_STATE_STOP;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    bool flip = false;\n\n    if (item->current_anim_state == M_STATE_SPIN) {\n        if (item->goal_anim_state != M_STATE_STOP) {\n            const XYZ_32 pos =\n                XYZ_32_OffsetYaw(item->pos, item->rot.y, WALL_L * 3 / 2);\n\n            int16_t room_num = item->room_num;\n            const SECTOR *const sector = Room_GetSector(pos, &room_num);\n            if (Room_GetHeight(sector, pos) == NO_HEIGHT) {\n                item->goal_anim_state = M_STATE_STOP;\n            }\n        }\n\n        flip = true;\n        if (item->touch_bits != 0) {\n            Lara_TakeDamage(M_DAMAGE, true);\n\n            const ITEM *const lara_item = Lara_GetItem();\n            Spawn_BloodBath(\n                lara_item->pos.x, lara_item->pos.y - WALL_L / 2,\n                lara_item->pos.z, item->speed * 2, lara_item->rot.y,\n                lara_item->room_num, 2);\n        }\n\n        Sound_Effect(SFX_ROLLING_BLADE, &item->pos, SPM_NORMAL);\n    } else {\n        if (Item_IsTriggerActive(item)) {\n            item->goal_anim_state = M_STATE_SPIN;\n        }\n        flip = false;\n    }\n\n    Item_Animate(item);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, item->pos);\n    item->floor = height;\n    item->pos.y = height;\n    Item_UpdateRoom(item_num, room_num);\n\n    if (flip && item->current_anim_state == M_STATE_STOP) {\n        item->rot.y += DEG_180;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_SPINNING_BLADE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/springboard.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\ntypedef enum {\n    // clang-format off\n    SPRINGBOARD_STATE_OFF = 0,\n    SPRINGBOARD_STATE_ON = 1,\n    // clang-format on\n} SPRINGBOARD_STATE;\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    ITEM *const lara_item = Lara_GetItem();\n\n    if (item->current_anim_state == SPRINGBOARD_STATE_OFF\n        && lara_item->pos.y == item->pos.y\n        && ROUND_TO_SECTOR(lara_item->pos.x) == ROUND_TO_SECTOR(item->pos.x)\n        && ROUND_TO_SECTOR(lara_item->pos.z) == ROUND_TO_SECTOR(item->pos.z)) {\n        if (lara_item->hit_points <= 0) {\n            return;\n        }\n\n        ITEM *const vehicle = Lara_Vehicle_GetItem();\n        if (vehicle != nullptr) {\n            if (vehicle->object_id != O_SKIDOO_FAST\n                && vehicle->object_id != O_SKIDOO_ARMED) {\n                return;\n            }\n\n            vehicle->fall_speed = -200;\n            vehicle->pos.y -= STEP_L;\n        } else {\n            if (lara_item->current_anim_state == LS(LS_WALK_BACK)\n                || lara_item->current_anim_state == LS(LS_FAST_BACK)) {\n                lara_item->speed = -lara_item->speed;\n            }\n\n            lara_item->fall_speed = -240;\n            lara_item->gravity = true;\n\n            Item_SwitchToAnim(lara_item, LA(LA_FALL_START), 0);\n            lara_item->current_anim_state = LS(LS_JUMP_FORWARD);\n            lara_item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        }\n        item->goal_anim_state = SPRINGBOARD_STATE_ON;\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_SPRINGBOARD, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/teeth_trap.c",
    "content": "#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/spawn.h>\n\n#define M_DAMAGE 400\n#define M_NUM_TEETH 6\n\ntypedef enum {\n    TEETH_TRAP_STATE_NICE = 0,\n    TEETH_TRAP_STATE_NASTY = 1,\n} TEETH_TRAP_STATE;\n\nstatic const BITE m_Teeth[M_NUM_TEETH] = {\n    // clang-format off\n    { .pos = { .x = -23, .y = 0,   .z = -1718 }, .mesh_num = 0 },\n    { .pos = { .x = 71,  .y = 0,   .z = -1718 }, .mesh_num = 1 },\n    { .pos = { .x = -23, .y = 10,  .z = -1718 }, .mesh_num = 0 },\n    { .pos = { .x = 71,  .y = 10,  .z = -1718 }, .mesh_num = 1 },\n    { .pos = { .x = -23, .y = -10, .z = -1718 }, .mesh_num = 0 },\n    { .pos = { .x = 71,  .y = -10, .z = -1718 }, .mesh_num = 1 },\n    // clang-format on\n};\n\nstatic void M_Bite(ITEM *const item, const BITE *const bite)\n{\n    XYZ_32 pos = bite->pos;\n    Collide_GetJointAbsPosition(item, &pos, bite->mesh_num);\n    Spawn_Blood(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n\n    if (Item_IsTriggerActive(item)) {\n        item->goal_anim_state = TEETH_TRAP_STATE_NASTY;\n        if (item->touch_bits != 0\n            && item->current_anim_state == TEETH_TRAP_STATE_NASTY) {\n            Lara_TakeDamage(M_DAMAGE, true);\n            for (int32_t i = 0; i < M_NUM_TEETH; i++) {\n                M_Bite(item, &m_Teeth[i]);\n            }\n        }\n    } else {\n        item->goal_anim_state = TEETH_TRAP_STATE_NICE;\n    }\n\n    Item_Animate(item);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->control_func = M_Control;\n    obj->collision_func = Object_Collision_Trap;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_TEETH_TRAP, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/thors_hammer.c",
    "content": "#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n\ntypedef enum {\n    THOR_HAMMER_STATE_SET = 0,\n    THOR_HAMMER_STATE_TEASE = 1,\n    THOR_HAMMER_STATE_ACTIVE = 2,\n    THOR_HAMMER_STATE_DONE = 3,\n} THOR_HAMMER_STATE;\n\ntypedef struct {\n    int16_t head_item_num;\n} M_PRIV;\n\nstatic void M_InitialiseHandle(const int16_t item_num)\n{\n    ITEM *const hand_item = Item_Get(item_num);\n    M_PRIV *const p = hand_item->priv;\n    const int16_t head_item_num = Item_CreateLevelItem();\n    ASSERT(head_item_num != NO_ITEM);\n    ITEM *const head_item = Item_Get(head_item_num);\n    head_item->object_id = O_THORS_HEAD;\n    head_item->room_num = hand_item->room_num;\n    head_item->pos = hand_item->pos;\n    head_item->rot = hand_item->rot;\n    head_item->shade.value_1 = hand_item->shade.value_1;\n    Item_Initialise(head_item_num);\n    p->head_item_num = head_item_num;\n}\n\nstatic void M_ControlHandle(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    ITEM *const lara_item = Lara_GetItem();\n\n    switch (item->current_anim_state) {\n    case THOR_HAMMER_STATE_SET:\n        if (Item_IsTriggerActive(item)) {\n            item->goal_anim_state = THOR_HAMMER_STATE_TEASE;\n        } else {\n            Item_RemoveActive(item_num);\n            item->status = IS_INACTIVE;\n        }\n        break;\n\n    case THOR_HAMMER_STATE_TEASE:\n        if (Item_IsTriggerActive(item)) {\n            item->goal_anim_state = THOR_HAMMER_STATE_ACTIVE;\n        } else {\n            item->goal_anim_state = THOR_HAMMER_STATE_SET;\n        }\n        break;\n\n    case THOR_HAMMER_STATE_ACTIVE: {\n        const int32_t frame_num = Item_GetRelativeFrame(item);\n        if (frame_num > 30) {\n            int32_t x = item->pos.x;\n            int32_t z = item->pos.z;\n\n            switch (item->rot.y) {\n            case 0:\n                z += WALL_L * 3;\n                break;\n            case DEG_90:\n                x += WALL_L * 3;\n                break;\n            case -DEG_90:\n                x -= WALL_L * 3;\n                break;\n            case -DEG_180:\n                z -= WALL_L * 3;\n                break;\n            }\n\n            if (lara_item->hit_points >= 0\n                && !g_Config.debug.enable_invulnerability\n                && lara_item->pos.x > x - 520 && lara_item->pos.x < x + 520\n                && lara_item->pos.z > z - 520 && lara_item->pos.z < z + 520) {\n                lara_item->hit_points = -1;\n                lara_item->pos.y = item->pos.y;\n                lara_item->gravity = false;\n                lara_item->current_anim_state = LS(LS_SPECIAL);\n                lara_item->goal_anim_state = LS(LS_SPECIAL);\n                Item_SwitchToAnim(lara_item, LA(LA_BOULDER_DEATH), 0);\n            }\n        }\n        break;\n    }\n\n    case THOR_HAMMER_STATE_DONE: {\n        int32_t x = item->pos.x;\n        int32_t z = item->pos.z;\n        int32_t old_x = x;\n        int32_t old_z = z;\n\n        Room_TestTriggers(item);\n\n        switch (item->rot.y) {\n        case 0:\n            z += WALL_L * 3;\n            break;\n        case DEG_90:\n            x += WALL_L * 3;\n            break;\n        case -DEG_90:\n            x -= WALL_L * 3;\n            break;\n        case -DEG_180:\n            z -= WALL_L * 3;\n            break;\n        }\n\n        item->pos.x = x;\n        item->pos.z = z;\n        if (lara_item->hit_points >= 0) {\n            Room_AlterFloorHeight(item, -WALL_L * 2);\n        }\n        item->pos.x = old_x;\n        item->pos.z = old_z;\n\n        Item_RemoveActive(item_num);\n        item->status = IS_DEACTIVATED;\n        break;\n    }\n    }\n    Item_Animate(item);\n\n    M_PRIV *const p = item->priv;\n    ITEM *const head_item = Item_Get(p->head_item_num);\n    const int16_t relative_anim = Item_GetRelativeAnim(item);\n    const int16_t relative_frame = Item_GetRelativeFrame(item);\n    Item_SwitchToAnim(head_item, relative_anim, relative_frame);\n    head_item->current_anim_state = item->current_anim_state;\n}\n\nstatic void M_CollisionHandle(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (coll->enable_baddie_push) {\n        Lara_Col_ItemPush(item, coll, false, true);\n    }\n}\n\nstatic void M_CollisionHead(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Lara_TestBoundsCollide(item, coll->radius)) {\n        return;\n    }\n    if (coll->enable_baddie_push\n        && item->current_anim_state != THOR_HAMMER_STATE_ACTIVE) {\n        Lara_Col_ItemPush(item, coll, false, true);\n    }\n}\n\nstatic void M_SetupHandle(OBJECT *const obj)\n{\n    obj->initialise_func = M_InitialiseHandle;\n    obj->control_func = M_ControlHandle;\n    obj->draw_func = Object_DrawUnclippedItem;\n    obj->collision_func = M_CollisionHandle;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nstatic void M_SetupHead(OBJECT *const obj)\n{\n    obj->collision_func = M_CollisionHead;\n    obj->draw_func = Object_DrawUnclippedItem;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_THORS_HANDLE, M_SetupHandle)\nREGISTER_OBJECT(O_THORS_HEAD, M_SetupHead)\n"
  },
  {
    "path": "src/trx/game/objects/traps/train.c",
    "content": "#include <trx/game/camera.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_DEFAULT_SPEED 260\n#define M_HIT_SPEED     160\n#define M_BRAKE_SPEED   48\n#define M_FRONT_DIST    (WALL_L * 5) // = 5120\n#define M_LIGHT_DIST    (WALL_L * 3) // = 3072\n#define M_CAM_DIST      (WALL_L * 8) // = 8192\n// clang-format on\n\ntypedef struct {\n    int32_t max_speed;\n} M_PRIV;\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    const ANIM *const anim = Item_GetAnim(item);\n    p->max_speed = anim->velocity != 0 ? anim->velocity : M_DEFAULT_SPEED;\n    item->speed = p->max_speed;\n}\n\nstatic int32_t M_GetHeight(\n    const ITEM *const item, const int32_t x, const int32_t z,\n    int16_t *const room_num)\n{\n    XYZ_32 pos = item->pos;\n    const int32_t sy = Math_Sin(item->rot.y);\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sx = Math_Sin(item->rot.x);\n    const int32_t sz = Math_Sin(item->rot.z);\n\n    pos.x += (z * sy + x * cy) >> W2V_SHIFT;\n    pos.z += (z * cy - x * sy) >> W2V_SHIFT;\n    pos.y = ((x * sz) >> W2V_SHIFT) + (item->pos.y - ((z * sx) >> W2V_SHIFT));\n\n    *room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, room_num);\n    return Room_GetHeight(sector, pos);\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const train_item = Item_Get(item_num);\n    if (train_item->status != IS_ACTIVE) {\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    if (!Item_TestBoundsCollide(train_item, lara_item, coll->radius)) {\n        return;\n    }\n\n    if (!Collide_TestCollision(train_item, lara_item)) {\n        return;\n    }\n\n    Sound_Effect(SFX_LARA_GENERAL_DEATH, &lara_item->pos, SPM_ALWAYS);\n    Sound_Effect(SFX_LARA_FALL_DEATH, &lara_item->pos, SPM_ALWAYS);\n    Sound_StopEffect(SFX_TRAIN_LOOP);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara_item->hit_points = 0;\n    lara_item->rot.y = train_item->rot.y;\n    lara->move_angle = lara_item->rot.y;\n    lara_item->gravity = false;\n    lara_item->fall_speed = 0;\n    lara_item->speed = 0;\n    lara->gun_type = LGT_UNARMED;\n    Lara_SwitchToExtraState(LS_EXTRA_TRAIN_KILL);\n\n    if (train_item->speed != 0) {\n        train_item->speed = M_HIT_SPEED;\n    }\n    XYZ_32 pos = XYZ_32_OffsetYaw(lara_item->pos, lara_item->rot.y, STEP_L);\n    pos.y -= STEP_L * 2;\n    Spawn_BloodBath(\n        pos.x, pos.y, pos.z, WALL_L, lara_item->rot.y, lara_item->room_num, 15);\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_IsTriggerActive(item)) {\n        return;\n    }\n\n    item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed);\n\n    int16_t room_num = NO_ROOM;\n    const int32_t front_height = M_GetHeight(item, 0, M_FRONT_DIST, &room_num);\n    const int32_t mid_height = M_GetHeight(item, 0, 0, &room_num);\n    item->pos.y = mid_height;\n\n    if (item->pos.y == NO_HEIGHT) {\n        Item_Kill(item_num);\n        return;\n    }\n\n    item->pos.y -= 32;\n    room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(item_num, room_num);\n\n    item->rot.x = (mid_height - front_height) << 1;\n\n    const XYZ_32 light_pos =\n        XYZ_32_OffsetYaw(item->pos, item->rot.y, M_LIGHT_DIST);\n    Output_AddDynamicLightRGB(light_pos, 14, (RGB_888) { 0xFF, 0xFF, 0xFF });\n\n    const M_PRIV *const p = item->priv;\n    if (item->speed == p->max_speed) {\n        Sound_Effect(SFX_TRAIN_LOOP, &item->pos, SPM_ALWAYS);\n        return;\n    }\n\n    if (item->speed == M_HIT_SPEED) {\n        XYZ_32 cam_pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, M_CAM_DIST);\n        cam_pos.y -= STEP_L * 2;\n        const SECTOR *const sector = Room_GetSector(cam_pos, &room_num);\n        cam_pos.y = Room_GetHeight(sector, cam_pos);\n        Camera_UpdateDynamicFixedObject(cam_pos, item->room_num);\n    }\n\n    item->speed -= M_BRAKE_SPEED;\n    CLAMPL(item->speed, 0);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = M_Collision;\n    obj->control_func = M_Control;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_TRAIN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/traps/wasp_emitter.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n\n// clang-format off\n#define M_MAX_SLOTS  3\n#define M_MAX_ACTIVE 2\n#define M_MAX_DIST   SQUARE(WALL_L * 12) // = 150994944\n#define M_COOLDOWN   255\n// clang-format on\n\ntypedef struct {\n    int32_t cooldown;\n    int32_t spawn_count;\n    int32_t spawn_total;\n    int16_t slots[M_MAX_SLOTS];\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_OPTIONAL(JSON_READ(io, \"cooldown\", &p->cooldown));\n    JSON_OPTIONAL(JSON_READ(io, \"spawn_count\", &p->spawn_count));\n    JSON_OPTIONAL(JSON_READ(io, \"spawn_total\", &p->spawn_total));\n    if (JSON_SHOULD(JSON_PUSH(io, \"slots\"))) {\n        for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n            JSON_SHOULD(JSON_READ_A(io, i, &p->slots[i]));\n        }\n        JSON_POP(io);\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"cooldown\", p->cooldown);\n    JSONW_WRITE(io, \"spawn_count\", p->spawn_count);\n    JSONW_WRITE(io, \"spawn_total\", p->spawn_total);\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n        JSONW_PUSH_VALUE(io, p->slots[i]);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"slots\");\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n        p->slots[i] = NO_ITEM;\n    }\n}\n\nstatic void M_PopulateSlots(M_PRIV *const p)\n{\n    int32_t count = 0;\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (item->object_id != O_WASP_MUTANT\n            || (item->ai_bits & AI_MODIFY) == 0) {\n            continue;\n        }\n\n        p->slots[count++] = i;\n        if (count >= M_MAX_SLOTS) {\n            break;\n        }\n    }\n}\n\nstatic int32_t M_GetEmptySlot(const M_PRIV *const p)\n{\n    for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n        const ITEM *const item = Item_Get(p->slots[i]);\n        if (item->creature_data == nullptr) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nstatic int32_t M_GetActiveCount(const M_PRIV *const p)\n{\n    int32_t count = 0;\n    for (int32_t i = 0; i < M_MAX_SLOTS; i++) {\n        const ITEM *const item = Item_Get(p->slots[i]);\n        if (item->active) {\n            count++;\n        }\n    }\n    return count;\n}\n\nstatic void M_SpawnWasp(const ITEM *const spawner_item, const int32_t slot_idx)\n{\n    M_PRIV *const p = spawner_item->priv;\n\n    ITEM *const wasp_item = Item_Get(p->slots[slot_idx]);\n    wasp_item->pos = spawner_item->pos;\n    wasp_item->rot = spawner_item->rot;\n    Item_SwitchToAnim(wasp_item, 0, 0);\n    wasp_item->current_anim_state = Item_GetAnim(wasp_item)->current_anim_state;\n    wasp_item->goal_anim_state = wasp_item->current_anim_state;\n    wasp_item->required_anim_state = 0;\n    wasp_item->flags &= ~(IF_INVISIBLE | IF_KILLED | 3);\n    wasp_item->creature_data = nullptr;\n    wasp_item->hit_points = Object_Get(wasp_item->object_id)->hit_points;\n    wasp_item->mesh_bits = -1;\n    wasp_item->status = IS_ACTIVE;\n    wasp_item->collidable = true;\n    wasp_item->ai_bits = AI_MODIFY;\n\n    if (wasp_item->active) {\n        Item_RemoveActive(p->slots[slot_idx]);\n    }\n\n    Item_AddActive(p->slots[slot_idx]);\n    Item_UpdateRoom(p->slots[slot_idx], NO_ITEM);\n    Item_UpdateRoom(p->slots[slot_idx], spawner_item->room_num);\n    LOT_EnableBaddieAI(p->slots[slot_idx], true);\n}\n\nstatic bool M_Trigger(ITEM *const item, const TRIGGER *const trigger)\n{\n    if (trigger == nullptr || trigger->type == TT_ANTITRIGGER\n        || trigger->type == TT_ANTIPAD) {\n        return true;\n    }\n\n    item->timer = 0;\n    item->flags |= IF_ONE_SHOT;\n\n    M_PRIV *const p = item->priv;\n    p->spawn_total = trigger->timer;\n    p->spawn_count = 0;\n    return true;\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    if (!item->active || p->spawn_count >= p->spawn_total) {\n        return;\n    }\n\n    if (p->slots[0] == NO_ITEM) {\n        M_PopulateSlots(p);\n        return;\n    }\n\n    const int16_t m_EmptySlot = M_GetEmptySlot(p);\n    if (m_EmptySlot == -1) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n    if (ABS(dx) > 32000 || ABS(dz) > 32000 || dist > M_MAX_DIST) {\n        return;\n    }\n\n    if (p->cooldown > 0) {\n        p->cooldown--;\n        return;\n    }\n\n    p->cooldown = M_COOLDOWN;\n\n    const int32_t active_count = M_GetActiveCount(item->priv);\n    if (active_count >= M_MAX_ACTIVE) {\n        return;\n    }\n\n    p->spawn_count++;\n    M_SpawnWasp(item, m_EmptySlot);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->trigger_func = M_Trigger;\n    obj->control_func = M_Control;\n    obj->draw_func = nullptr;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->save_flags = true;\n}\n\nREGISTER_OBJECT(O_WASP_MUTANT_EMITTER, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/types.h",
    "content": "#pragma once\n\n#include <trx/game/anims/types.h>\n#include <trx/game/collision.h>\n#include <trx/game/effects/types.h>\n#include <trx/game/items/types.h>\n#include <trx/game/pathing/types.h>\n#include <trx/game/rooms/types.h>\n#include <trx/game/savegame/enum.h>\n#include <trx/game/types.h>\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef struct {\n    const OBJECT_ID key_id;\n    const OBJECT_ID value_id;\n} GAME_OBJECT_PAIR;\n\ntypedef struct {\n    void *priv;\n    XYZ_16 center;\n    int32_t radius;\n    int16_t num_lights;\n    int16_t num_vertices;\n    union {\n        XYZ_16 *normals;\n        int16_t *lights;\n    } lighting;\n    XYZ_16 *vertices;\n\n    struct {\n        int16_t count;\n        FACE *data;\n    } all_faces, tex_faces, tex_face4s, tex_face3s, flat_faces, flat_face4s,\n        flat_face3s;\n\n    float depth_adjustment;\n    bool enable_reflections;\n    bool enable_caustics;\n} OBJECT_MESH;\n\ntypedef struct {\n    struct {\n        XYZ_16 min;\n        XYZ_16 max;\n    } shift, rot;\n    bool ignore_rot;\n} OBJECT_BOUNDS;\n\ntypedef struct JSON_READ_IO JSON_READ_IO;\ntypedef struct JSON_WRITE_IO JSON_WRITE_IO;\n\ntypedef enum {\n    OBJECT_EVENT_ALERT,\n    OBJECT_EVENT_BURNT,\n} OBJECT_EVENT;\n\ntypedef struct OBJECT {\n    int16_t mesh_count;\n    int16_t mesh_idx;\n    int32_t bone_idx;\n    uint32_t frame_ofs;\n    ANIM_FRAME *frame_base;\n\n    void (*setup_func)(struct OBJECT *obj);\n    void (*initialise_func)(int16_t item_num);\n\n    void (*control_func)(int16_t item_num);\n    bool (*draw_func)(const ITEM *item);\n    // NOTE: not to be union'd with draw_func, due to default draw_func impl\n    // being Object_DrawAnimatingItem which takes an ITEM*\n    bool (*effect_draw_func)(const EFFECT *item);\n\n    void (*collision_func)(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\n    int16_t (*floor_height_func)(\n        const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height);\n    int16_t (*ceiling_height_func)(\n        const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height);\n    void (*activate_func)(ITEM *item);\n    void (*event_func)(ITEM *item, OBJECT_EVENT event, const void *data);\n    bool (*trigger_func)(ITEM *item, const TRIGGER *trigger);\n    bool (*gun_hit_func)(\n        ITEM *item, const GAME_VECTOR *start, const GAME_VECTOR *hit_pos,\n        int32_t *damage);\n    void (*handle_flip_func)(ITEM *item, ROOM_FLIP_STATUS flip_status);\n    void (*handle_save_func)(ITEM *item, SAVEGAME_STAGE stage);\n    void (*priv_load_func)(ITEM *item, JSON_READ_IO *io);\n    void (*priv_save_func)(const ITEM *item, JSON_WRITE_IO *io);\n    const OBJECT_BOUNDS *(*bounds_func)(void);\n    bool (*is_usable_func)(int16_t item_num);\n    void (*add_walkable_func)(int16_t item_num);\n    int16_t (*carrier_item_num_func)(const ITEM *item);\n    bool (*can_drop_items_func)(const ITEM *item);\n    bool (*can_interpolate_func)(\n        const ITEM *item, int32_t frame_a, int32_t frame_b);\n    bool (*should_spawn_blood_func)(const ITEM *item);\n    bool (*is_alive_func)(const ITEM *item);\n    bool (*is_targetable_func)(const ITEM *item);\n    bool (*can_take_damage_func)(const ITEM *item);\n    bool (*can_be_projectile_target_func)(const ITEM *item);\n    bool (*can_be_exploded_func)(const ITEM *item);\n    int32_t (*get_mesh_index_func)(const ITEM *item, int32_t mesh_idx);\n\n    int16_t anim_idx;\n    int16_t anim_count;\n    int16_t hit_points;\n    int16_t pivot_length;\n    int16_t radius;\n    int16_t shadow_size;\n    int16_t smartness;\n    XYZ_BOOL base_rot;\n    LOT_SETUP lot_setup;\n\n    size_t priv_size;\n\n    bool enable_interpolation;\n    bool loaded;\n    bool intelligent;\n    bool save_position;\n    bool save_hitpoints;\n    bool save_flags;\n    bool save_anim;\n    bool load_floor;\n    bool semi_transparent;\n} OBJECT;\n\ntypedef struct {\n    bool loaded;\n    int16_t mesh_idx;\n    bool collidable;\n    bool visible;\n    BOUNDS_16 draw_bounds;\n    BOUNDS_16 collision_bounds;\n} STATIC_OBJECT_3D;\n\ntypedef struct {\n    bool loaded;\n    int16_t frame_count;\n    int16_t texture_idx;\n} STATIC_OBJECT_2D;\n\ntypedef enum {\n    TRAP_SET = 0,\n    TRAP_ACTIVATE = 1,\n    TRAP_WORKING = 2,\n    TRAP_FINISHED = 3,\n} TRAP_ANIM;\n"
  },
  {
    "path": "src/trx/game/objects/vars.c",
    "content": "#include <trx/game/objects/vars.h>\n\nconst GAME_OBJECT_PAIR g_KeyItemToReceptacleMap[] = {\n    // clang-format off\n    { O_KEY_OPTION_1, O_KEY_HOLE_1 },\n    { O_KEY_OPTION_2, O_KEY_HOLE_2 },\n    { O_KEY_OPTION_3, O_KEY_HOLE_3 },\n    { O_KEY_OPTION_4, O_KEY_HOLE_4 },\n    { O_PUZZLE_OPTION_1, O_PUZZLE_HOLE_1 },\n    { O_PUZZLE_OPTION_2, O_PUZZLE_HOLE_2 },\n    { O_PUZZLE_OPTION_3, O_PUZZLE_HOLE_3 },\n    { O_PUZZLE_OPTION_4, O_PUZZLE_HOLE_4 },\n    { O_LEADBAR_OPTION, O_MIDAS_TOUCH },\n    { O_KEY_OPTION_2, O_GONG },\n    { O_KEY_OPTION_2, O_DETONATOR_BOX },\n    { NO_OBJECT, NO_OBJECT },\n    // clang-format on\n};\n\nconst GAME_OBJECT_PAIR g_ReceptacleToReceptacleDoneMap[] = {\n    // clang-format off\n    { O_PUZZLE_HOLE_1, O_PUZZLE_DONE_1 },\n    { O_PUZZLE_HOLE_2, O_PUZZLE_DONE_2 },\n    { O_PUZZLE_HOLE_3, O_PUZZLE_DONE_3 },\n    { O_PUZZLE_HOLE_4, O_PUZZLE_DONE_4 },\n    { NO_OBJECT, NO_OBJECT },\n    // clang-format on\n};\n\nconst OBJECT_ID g_ReceptacleObjects[] = {\n    // clang-format off\n    O_KEY_HOLE_1,\n    O_KEY_HOLE_2,\n    O_KEY_HOLE_3,\n    O_KEY_HOLE_4,\n    O_PUZZLE_HOLE_1,\n    O_PUZZLE_HOLE_2,\n    O_PUZZLE_HOLE_3,\n    O_PUZZLE_HOLE_4,\n    O_PUZZLE_DONE_1,\n    O_PUZZLE_DONE_2,\n    O_PUZZLE_DONE_3,\n    O_PUZZLE_DONE_4,\n    O_MIDAS_TOUCH,\n    O_GONG,\n    O_DETONATOR_BOX,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_CreatureObjects[] = {\n    // clang-format off\n    O_ALLIGATOR,\n    O_APE,\n    O_ATLANTEAN_GROUND,\n    O_ATLANTEAN_SHOOTER,\n    O_ATLANTEAN_WINGED,\n    O_BALDY,\n    O_BANDIT_1,\n    O_BANDIT_2,\n    O_BANDIT_2B,\n    O_BARRACUDA,\n    O_BAT,\n    O_BEAR,\n    O_BIG_EEL,\n    O_BIG_SPIDER,\n    O_BIRD_GUARDIAN,\n    O_CENTAUR,\n    O_CENTAUR_STATUE,\n    O_CIVILIAN,\n    O_CLAW_MUTANT,\n    O_COBRA,\n    O_COMPY,\n    O_COWBOY,\n    O_CRAWLER_MUTANT,\n    O_CROCODILE,\n    O_CROW,\n    O_CULT_1,\n    O_CULT_1A,\n    O_CULT_1B,\n    O_CULT_2,\n    O_CULT_3,\n    O_DINO_WARRIOR,\n    O_DIVER,\n    O_DOG,\n    O_DRAGON_FRONT,\n    O_EAGLE,\n    O_EEL,\n    O_FISH,\n    O_HUSKIE,\n    O_HYBRID_MUTANT,\n    O_JELLY,\n    O_LARSON,\n    O_LION,\n    O_LIONESS,\n    O_LIZARD,\n    O_MONKEY,\n    O_MONK_1,\n    O_MONK_2,\n    O_MONK_3,\n    O_MOUSE,\n    O_MP_1,\n    O_MP_2,\n    O_MUMMY,\n    O_NATLA,\n    O_ORCA,\n    O_PATROL_DOG,\n    O_PIERRE,\n    O_PRISONER,\n    O_PUMA,\n    O_PUNK_1,\n    O_PUNK_2,\n    O_RAPTOR,\n    O_RAT,\n    O_RX_WORKER_1,\n    O_RX_WORKER_2,\n    O_RX_WORKER_3,\n    O_SECURITY_GUARD,\n    O_SENTRY_GUN,\n    O_SHARK,\n    O_SHIVA,\n    O_SKATEKID,\n    O_SKIDOO_DRIVER,\n    O_SOPHIA,\n    O_SPIDER,\n    O_STHPAC_MERCENARY,\n    O_SWAT_1,\n    O_SWAT_2,\n    O_SWAT_3,\n    O_TIGER,\n    O_TONY,\n    O_TORSO,\n    O_TREX,\n    O_TREX_ALPHA,\n    O_TRIBE_AXEMAN,\n    O_TRIBE_BOSS,\n    O_TRIBE_PIPEMAN,\n    O_VOLE,\n    O_VULTURE,\n    O_WASP_MUTANT,\n    O_WILLARD,\n    O_WOLF,\n    O_WORKER_1,\n    O_WORKER_2,\n    O_WORKER_3,\n    O_WORKER_4,\n    O_WORKER_5,\n    O_XIAN_KNIGHT,\n    O_XIAN_KNIGHT_STATUE,\n    O_XIAN_SPEARMAN,\n    O_XIAN_SPEARMAN_STATUE,\n    O_YETI,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_ProjectileObjects[] = {\n    // clang-format off\n    O_HARPOON_BOLT,\n    O_GRENADE,\n    O_ROCKET,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_WaterObjects[] = {\n    // clang-format off\n    O_ALLIGATOR,\n    O_BARRACUDA,\n    O_BIG_EEL,\n    O_DIVER,\n    O_EEL,\n    O_FISH,\n    O_GENERAL,\n    O_JELLY,\n    O_PROPELLER_2,\n    O_SHARK,\n    O_VOLE,\n    O_ORCA,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_LoyalObjects[] = {\n    // clang-format off\n    O_LARA,\n    O_WINSTON,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_BossObjects[] = {\n    // clang-format off\n    O_TREX,\n    O_TREX_ALPHA,\n    O_LARSON,\n    O_PIERRE,\n    O_SKATEKID,\n    O_COWBOY,\n    O_BALDY,\n    O_NATLA,\n    O_TORSO,\n    O_CULT_3,\n    O_DRAGON_FRONT,\n    O_BARTOLI,\n    O_BIRD_GUARDIAN,\n    O_SKIDOO_DRIVER,\n    O_SKIDOO_ARMED,\n    O_TONY,\n    O_TRIBE_BOSS,\n    O_SOPHIA,\n    O_WILLARD,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_MovableBlockObjects[] = {\n    // clang-format off\n    O_MOVABLE_BLOCK_1,\n    O_MOVABLE_BLOCK_2,\n    O_MOVABLE_BLOCK_3,\n    O_MOVABLE_BLOCK_4,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_SecretObjects[] = {\n    // clang-format off\n    O_SECRET_1,\n    O_SECRET_2,\n    O_SECRET_3,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_PickupObjects[] = {\n    // clang-format off\n    O_PISTOL_ITEM,\n    O_PISTOL_AMMO_ITEM,\n    O_SHOTGUN_ITEM,\n    O_SHOTGUN_AMMO_ITEM,\n    O_MAGNUM_ITEM,\n    O_MAGNUM_AMMO_ITEM,\n    O_AUTOS_ITEM,\n    O_AUTOS_AMMO_ITEM,\n    O_DESERT_EAGLE_ITEM,\n    O_DESERT_EAGLE_AMMO_ITEM,\n    O_UZI_AMMO_ITEM,\n    O_UZI_ITEM,\n    O_HARPOON_ITEM,\n    O_HARPOON_AMMO_ITEM,\n    O_M16_ITEM,\n    O_M16_AMMO_ITEM,\n    O_MP5_ITEM,\n    O_MP5_AMMO_ITEM,\n    O_GRENADE_GUN_ITEM,\n    O_GRENADE_AMMO_ITEM,\n    O_ROCKET_GUN_ITEM,\n    O_ROCKET_AMMO_ITEM,\n    O_EXPLOSIVE_ITEM,\n    O_SMALL_MEDIPACK_ITEM,\n    O_LARGE_MEDIPACK_ITEM,\n    O_FLAREBOX_ITEM,\n    O_FLARE_ITEM,\n    O_KEY_ITEM_1,\n    O_KEY_ITEM_2,\n    O_KEY_ITEM_3,\n    O_KEY_ITEM_4,\n    O_PICKUP_ITEM_1,\n    O_PICKUP_ITEM_2,\n    O_QUEST_ITEM_1,\n    O_QUEST_ITEM_2,\n    O_QUEST_ITEM_3,\n    O_QUEST_ITEM_4,\n    O_PUZZLE_ITEM_1,\n    O_PUZZLE_ITEM_2,\n    O_PUZZLE_ITEM_3,\n    O_PUZZLE_ITEM_4,\n    O_LEADBAR_ITEM,\n    O_SCION_ITEM_1,\n    O_SCION_ITEM_2,\n    O_SECRET_1,\n    O_SECRET_2,\n    O_SECRET_3,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_ElevatedPickupObjects[] = {\n    // clang-format off\n    O_SCION_ITEM_1,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_SwitchObjects[] = {\n    // clang-format off\n    O_SWITCH_TYPE_AIRLOCK,\n    O_SWITCH_TYPE_BUTTON,\n    O_SWITCH_TYPE_NORMAL,\n    O_SWITCH_TYPE_SMALL,\n    O_SWITCH_TYPE_UW,\n    O_SWITCH_TYPE_WHEEL,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_GunObjects[] = {\n    // clang-format off\n    O_PISTOL_ITEM,\n    O_SHOTGUN_ITEM,\n    O_MAGNUM_ITEM,\n    O_AUTOS_ITEM,\n    O_DESERT_EAGLE_ITEM,\n    O_UZI_ITEM,\n    O_HARPOON_ITEM,\n    O_M16_ITEM,\n    O_MP5_ITEM,\n    O_GRENADE_GUN_ITEM,\n    O_ROCKET_GUN_ITEM,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_GunAmmoObjects[] = {\n    // clang-format off\n    O_PISTOL_AMMO_ITEM,\n    O_SHOTGUN_AMMO_ITEM,\n    O_MAGNUM_AMMO_ITEM,\n    O_AUTOS_AMMO_ITEM,\n    O_DESERT_EAGLE_AMMO_ITEM,\n    O_UZI_AMMO_ITEM,\n    O_HARPOON_AMMO_ITEM,\n    O_M16_AMMO_ITEM,\n    O_MP5_AMMO_ITEM,\n    O_GRENADE_AMMO_ITEM,\n    O_ROCKET_AMMO_ITEM,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_DoorObjects[] = {\n    // clang-format off\n    O_DOOR_TYPE_1,\n    O_DOOR_TYPE_2,\n    O_DOOR_TYPE_3,\n    O_DOOR_TYPE_4,\n    O_DOOR_TYPE_5,\n    O_DOOR_TYPE_6,\n    O_DOOR_TYPE_7,\n    O_DOOR_TYPE_8,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_TrapdoorObjects[] = {\n    // clang-format off\n    O_TRAPDOOR_TYPE_1,\n    O_TRAPDOOR_TYPE_2,\n    O_TRAPDOOR_TYPE_3,\n    O_DRAWBRIDGE,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_AnimObjects[] = {\n    // clang-format off\n    O_LARA_PISTOLS,\n    O_LARA_SHOTGUN,\n    O_LARA_MAGNUMS,\n    O_LARA_DESERT_EAGLE,\n    O_LARA_UZIS,\n    O_LARA_HARPOON_GUN,\n    O_LARA_M16,\n    O_LARA_MP5,\n    O_LARA_GRENADE_GUN,\n    O_LARA_FLARE,\n    O_LARA_HAIR,\n    O_LARA_EXTRA,\n    O_LARA_SKIDOO,\n    O_LARA_BOAT,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_NullObjects[] = {\n    // clang-format off\n    O_ALPHABET,\n    O_ALPHABET_SMALL,\n    O_ASSAULT_DIGITS,\n    O_BLOOD,\n    O_BLOOD_PINK,\n    O_BODY_PART,\n    O_BUBBLE_1,\n    O_BUBBLE_2,\n    O_BUBBLE_EMITTER,\n    O_CAMERA_TARGET,\n    O_COMBAT_END,\n    O_CUT_SHOTGUN,\n    O_DART_EFFECT,\n    O_DRAGON_BONES_2,\n    O_DRAGON_BONES_3,\n    O_DUST,\n    O_EARTHQUAKE,\n    O_EXPLOSION_1,\n    O_EXPLOSION_2,\n    O_FLARE_FIRE,\n    O_FLARE_ITEM,\n    O_FX_RESERVED,\n    O_GLOW,\n    O_GLOW_RESERVED,\n    O_GONG_BONGER,\n    O_GRENADE,\n    O_GUN_FLASH,\n    O_GUN_SHELL,\n    O_HARPOON_BOLT,\n    O_HOT_LIQUID,\n    O_INV_BACKGROUND,\n    O_M16_FLASH,\n    O_NATLA_GUN,\n    O_MISSILE_ATLANTEAN_SHARD,\n    O_MISSILE_ATLANTEAN_BOMB,\n    O_MISSILE_FLAME,\n    O_MISSILE_HARPOON,\n    O_MISSILE_KNIFE,\n    O_PICKUP_AID,\n    O_ROCKET,\n    O_RICOCHET,\n    O_SHOTGUN_SHELL,\n    O_SKYBOX,\n    O_SNOW_SPRITE,\n    O_SPHERE_OF_DOOM_1,\n    O_SPHERE_OF_DOOM_2,\n    O_SPHERE_OF_DOOM_3,\n    O_SPLASH_1,\n    O_SPLASH_2,\n    O_TEXT_BOX,\n    O_TWINKLE,\n    O_WATER_SPRITE,\n    O_DUMMY,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_InvObjects[] = {\n    // clang-format off\n    O_PISTOL_OPTION,\n    O_PISTOL_AMMO_OPTION,\n    O_MAGNUM_OPTION,\n    O_MAGNUM_AMMO_OPTION,\n    O_AUTOS_OPTION,\n    O_AUTOS_AMMO_OPTION,\n    O_DESERT_EAGLE_OPTION,\n    O_DESERT_EAGLE_AMMO_OPTION,\n    O_SHOTGUN_OPTION,\n    O_SHOTGUN_AMMO_OPTION,\n    O_UZI_OPTION,\n    O_UZI_AMMO_OPTION,\n    O_HARPOON_OPTION,\n    O_HARPOON_AMMO_OPTION,\n    O_M16_OPTION,\n    O_M16_AMMO_OPTION,\n    O_MP5_OPTION,\n    O_MP5_AMMO_OPTION,\n    O_GRENADE_GUN_OPTION,\n    O_GRENADE_AMMO_OPTION,\n    O_ROCKET_GUN_OPTION,\n    O_ROCKET_AMMO_OPTION,\n    O_EXPLOSIVE_OPTION,\n    O_SMALL_MEDIPACK_OPTION,\n    O_LARGE_MEDIPACK_OPTION,\n    O_FLAREBOX_OPTION,\n    O_PUZZLE_OPTION_1,\n    O_PUZZLE_OPTION_2,\n    O_PUZZLE_OPTION_3,\n    O_PUZZLE_OPTION_4,\n    O_KEY_OPTION_1,\n    O_KEY_OPTION_2,\n    O_KEY_OPTION_3,\n    O_KEY_OPTION_4,\n    O_QUEST_OPTION_1,\n    O_QUEST_OPTION_2,\n    O_QUEST_OPTION_3,\n    O_QUEST_OPTION_4,\n    O_PICKUP_OPTION_1,\n    O_PICKUP_OPTION_2,\n    O_COMPASS_OPTION,\n    O_STOPWATCH_OPTION,\n    O_CONTROL_OPTION,\n    O_DETAIL_OPTION,\n    O_GAMMA_OPTION,\n    O_GLOBE_SELECT_OPTION,\n    O_LEADBAR_OPTION,\n    O_PASSPORT_OPTION,\n    O_PHOTO_OPTION,\n    O_SCION_OPTION,\n    O_SOUND_OPTION,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_WaterSpriteObjects[] = {\n    // clang-format off\n    O_WATERFALL,\n    O_SPLASH_1,\n    O_SPLASH_2,\n    O_BUBBLE_1,\n    O_BUBBLE_2,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_GameSpriteObjects[] = {\n    // clang-format off\n    O_PISTOL_ITEM,\n    O_SHOTGUN_ITEM,\n    O_MAGNUM_ITEM,\n    O_AUTOS_ITEM,\n    O_DESERT_EAGLE_ITEM,\n    O_UZI_ITEM,\n    O_HARPOON_ITEM,\n    O_M16_ITEM,\n    O_MP5_ITEM,\n    O_GRENADE_GUN_ITEM,\n    O_ROCKET_GUN_ITEM,\n    O_PISTOL_AMMO_ITEM,\n    O_SHOTGUN_AMMO_ITEM,\n    O_MAGNUM_AMMO_ITEM,\n    O_AUTOS_AMMO_ITEM,\n    O_DESERT_EAGLE_AMMO_ITEM,\n    O_UZI_AMMO_ITEM,\n    O_HARPOON_AMMO_ITEM,\n    O_M16_AMMO_ITEM,\n    O_MP5_AMMO_ITEM,\n    O_GRENADE_AMMO_ITEM,\n    O_ROCKET_AMMO_ITEM,\n    O_EXPLOSIVE_ITEM,\n\n    O_SMALL_MEDIPACK_ITEM,\n    O_LARGE_MEDIPACK_ITEM,\n    O_FLAREBOX_ITEM,\n\n    O_PUZZLE_ITEM_1,\n    O_PUZZLE_ITEM_2,\n    O_PUZZLE_ITEM_3,\n    O_PUZZLE_ITEM_4,\n    O_KEY_ITEM_1,\n    O_KEY_ITEM_2,\n    O_KEY_ITEM_3,\n    O_KEY_ITEM_4,\n    O_PICKUP_ITEM_1,\n    O_PICKUP_ITEM_2,\n    O_QUEST_ITEM_1,\n    O_QUEST_ITEM_2,\n    O_QUEST_ITEM_3,\n    O_QUEST_ITEM_4,\n    O_LEADBAR_ITEM,\n    O_SCION_ITEM_1,\n    O_SCION_ITEM_2,\n\n    O_SECRET_1,\n    O_SECRET_2,\n    O_SECRET_3,\n\n    O_EXPLOSION_1,\n    O_EXPLOSION_2,\n    O_MISSILE_FLAME,\n    O_SPLASH_1,\n    O_SPLASH_2,\n    O_BUBBLE_1,\n    O_BUBBLE_2,\n    O_BLOOD,\n    O_BLOOD_PINK,\n    O_DART_EFFECT,\n    O_RICOCHET,\n    O_TWINKLE,\n    O_DUST,\n    O_EMBER,\n    O_FLAME,\n    O_PICKUP_AID,\n    O_GLOW,\n    O_WATER_SPRITE,\n    O_SNOW_SPRITE,\n    O_HOT_LIQUID,\n    O_SHADOW,\n\n    O_GLOW_RESERVED,\n    O_FX_RESERVED,\n\n    O_ALPHABET,\n    O_ALPHABET_SMALL,\n    O_TEXT_BOX,\n    O_ASSAULT_DIGITS,\n\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst GAME_OBJECT_PAIR g_GunAmmoObjectMap[] = {\n    // clang-format off\n    { O_PISTOL_ITEM, O_PISTOL_AMMO_ITEM },\n    { O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM },\n    { O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM },\n    { O_AUTOS_ITEM, O_AUTOS_AMMO_ITEM },\n    { O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_AMMO_ITEM },\n    { O_UZI_ITEM, O_UZI_AMMO_ITEM },\n    { O_HARPOON_ITEM, O_HARPOON_AMMO_ITEM },\n    { O_M16_ITEM, O_M16_AMMO_ITEM },\n    { O_MP5_ITEM, O_MP5_AMMO_ITEM },\n    { O_GRENADE_GUN_ITEM, O_GRENADE_AMMO_ITEM },\n    { O_ROCKET_GUN_ITEM, O_ROCKET_AMMO_ITEM },\n    { NO_OBJECT, NO_OBJECT },\n    // clang-format on\n};\n\nconst GAME_OBJECT_PAIR g_ItemToInvObjectMap[] = {\n    // clang-format off\n    { O_PISTOL_ITEM, O_PISTOL_OPTION },\n    { O_SHOTGUN_ITEM, O_SHOTGUN_OPTION },\n    { O_MAGNUM_ITEM, O_MAGNUM_OPTION },\n    { O_AUTOS_ITEM, O_AUTOS_OPTION },\n    { O_DESERT_EAGLE_ITEM, O_DESERT_EAGLE_OPTION },\n    { O_UZI_ITEM, O_UZI_OPTION },\n    { O_HARPOON_ITEM, O_HARPOON_OPTION },\n    { O_M16_ITEM, O_M16_OPTION },\n    { O_MP5_ITEM, O_MP5_OPTION },\n    { O_GRENADE_GUN_ITEM, O_GRENADE_GUN_OPTION },\n    { O_ROCKET_GUN_ITEM, O_ROCKET_GUN_OPTION },\n    { O_PISTOL_AMMO_ITEM, O_PISTOL_AMMO_OPTION },\n    { O_SHOTGUN_AMMO_ITEM, O_SHOTGUN_AMMO_OPTION },\n    { O_MAGNUM_AMMO_ITEM, O_MAGNUM_AMMO_OPTION },\n    { O_AUTOS_AMMO_ITEM, O_AUTOS_AMMO_OPTION },\n    { O_DESERT_EAGLE_AMMO_ITEM, O_DESERT_EAGLE_AMMO_OPTION },\n    { O_UZI_AMMO_ITEM, O_UZI_AMMO_OPTION },\n    { O_HARPOON_AMMO_ITEM,O_HARPOON_AMMO_OPTION },\n    { O_M16_AMMO_ITEM, O_M16_AMMO_OPTION },\n    { O_MP5_AMMO_ITEM, O_MP5_AMMO_OPTION },\n    { O_GRENADE_AMMO_ITEM, O_GRENADE_AMMO_OPTION },\n    { O_ROCKET_AMMO_ITEM, O_ROCKET_AMMO_OPTION },\n    { O_EXPLOSIVE_ITEM, O_EXPLOSIVE_OPTION },\n    { O_SMALL_MEDIPACK_ITEM, O_SMALL_MEDIPACK_OPTION },\n    { O_LARGE_MEDIPACK_ITEM, O_LARGE_MEDIPACK_OPTION },\n    { O_FLARE_ITEM, O_FLAREBOX_OPTION },\n    { O_FLAREBOX_ITEM, O_FLAREBOX_OPTION },\n    { O_PUZZLE_ITEM_1, O_PUZZLE_OPTION_1 },\n    { O_PUZZLE_ITEM_2, O_PUZZLE_OPTION_2 },\n    { O_PUZZLE_ITEM_3, O_PUZZLE_OPTION_3 },\n    { O_PUZZLE_ITEM_4, O_PUZZLE_OPTION_4 },\n    { O_KEY_ITEM_1, O_KEY_OPTION_1 },\n    { O_KEY_ITEM_2, O_KEY_OPTION_2 },\n    { O_KEY_ITEM_3, O_KEY_OPTION_3 },\n    { O_KEY_ITEM_4, O_KEY_OPTION_4 },\n    { O_PICKUP_ITEM_1, O_PICKUP_OPTION_1 },\n    { O_PICKUP_ITEM_2, O_PICKUP_OPTION_2 },\n    { O_QUEST_ITEM_1, O_QUEST_OPTION_1 },\n    { O_QUEST_ITEM_2, O_QUEST_OPTION_2 },\n    { O_QUEST_ITEM_3, O_QUEST_OPTION_3 },\n    { O_QUEST_ITEM_4, O_QUEST_OPTION_4 },\n    { O_LEADBAR_ITEM, O_LEADBAR_OPTION },\n    { O_SCION_ITEM_1, O_SCION_OPTION },\n    { O_SCION_ITEM_2, O_SCION_OPTION },\n    { O_SECRET_1, O_SECRET_1_OPTION },\n    { O_SECRET_2, O_SECRET_2_OPTION },\n    { O_SECRET_3, O_SECRET_3_OPTION },\n    { NO_OBJECT, NO_OBJECT },\n    // clang-format on\n};\n\nconst OBJECT_ID g_ShatterableObjects[] = {\n    // clang-format off\n    O_SMASH_OBJECT_1,\n    O_SMASH_OBJECT_4,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_HeavyShatterableObjects[] = {\n    // clang-format off\n    O_SMASH_OBJECT_2,\n    O_SMASH_OBJECT_3,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_HeavyMissileObjects[] = {\n    // clang-format off\n    O_HEAVY_ROCKET,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_SmashableObjects[] = {\n    // clang-format off\n    O_BELL,\n    O_SCION_ITEM_3,\n    O_CARCASS,\n    O_FUSE_BOX,\n    NO_OBJECT,\n    // clang-format on\n};\n\nconst OBJECT_ID g_ShoalObjects[] = {\n    // clang-format off\n    O_TROPICAL_FISH,\n    O_PIRAHNAS,\n    NO_OBJECT,\n    // clang-format on\n};\n"
  },
  {
    "path": "src/trx/game/objects/vars.h",
    "content": "#pragma once\n\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/ids.h>\n\nextern const OBJECT_ID g_CreatureObjects[];\nextern const OBJECT_ID g_ProjectileObjects[];\nextern const OBJECT_ID g_WaterObjects[];\nextern const OBJECT_ID g_LoyalObjects[];\nextern const OBJECT_ID g_PickupObjects[];\nextern const OBJECT_ID g_ElevatedPickupObjects[];\nextern const OBJECT_ID g_SwitchObjects[];\nextern const OBJECT_ID g_ReceptacleObjects[];\nextern const OBJECT_ID g_GunObjects[];\nextern const OBJECT_ID g_GunAmmoObjects[];\nextern const OBJECT_ID g_DoorObjects[];\nextern const OBJECT_ID g_TrapdoorObjects[];\nextern const OBJECT_ID g_AnimObjects[];\nextern const OBJECT_ID g_NullObjects[];\nextern const OBJECT_ID g_InvObjects[];\nextern const OBJECT_ID g_WaterSpriteObjects[];\nextern const OBJECT_ID g_BossObjects[];\nextern const OBJECT_ID g_SecretObjects[];\nextern const OBJECT_ID g_MovableBlockObjects[];\nextern const OBJECT_ID g_GameSpriteObjects[];\n\nextern const GAME_OBJECT_PAIR g_GunAmmoObjectMap[];\nextern const GAME_OBJECT_PAIR g_ItemToInvObjectMap[];\nextern const GAME_OBJECT_PAIR g_KeyItemToReceptacleMap[];\nextern const GAME_OBJECT_PAIR g_ReceptacleToReceptacleDoneMap[];\n\nextern const OBJECT_ID g_ShatterableObjects[];\nextern const OBJECT_ID g_HeavyShatterableObjects[];\nextern const OBJECT_ID g_HeavyMissileObjects[];\nextern const OBJECT_ID g_SmashableObjects[];\nextern const OBJECT_ID g_ShoalObjects[];\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/boat.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/effects.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/traps/gondola.h>\n#include <trx/game/objects/vehicles/common.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define BOAT_FALL_ANIM 15\n#define BOAT_DEATH_ANIM 18\n#define BOAT_GET_ON_LW_ANIM 0\n#define BOAT_GET_ON_RW_ANIM 8\n#define BOAT_GET_ON_J_ANIM 6\n#define BOAT_GET_ON_START 1\n\n#define LF_BOAT_EXIT_END 24\n\n#define BOAT_RADIUS 500\n#define BOAT_SIDE 300\n#define BOAT_FRONT 750\n#define BOAT_TIP (BOAT_FRONT + 250)\n#define BOAT_MIN_SPEED 20\n#define BOAT_MAX_SPEED 90\n#define BOAT_SLOW_SPEED (BOAT_MAX_SPEED / 3) // = 30\n#define BOAT_FAST_SPEED (BOAT_MAX_SPEED + 50) // = 140\n#define BOAT_MAX_BACK (-20)\n#define BOAT_ACCELERATION 5\n#define BOAT_BRAKE 5\n#define BOAT_REVERSE (-5)\n#define BOAT_SLOWDOWN 1\n#define BOAT_WAKE 700\n#define BOAT_UNDO_TURN (DEG_1 / 4) // = 45\n#define BOAT_TURN (DEG_1 / 8) // = 22\n#define BOAT_MAX_TURN (DEG_1 * 4) // = 728\n#define BOAT_SOUND_CEILING (WALL_L * 5) // = 5120\n#define BOAT_SHIFT_Y (-5)\n\ntypedef enum {\n    BOAT_STATE_GET_ON = 0,\n    BOAT_STATE_STILL = 1,\n    BOAT_STATE_MOVING = 2,\n    BOAT_STATE_JUMP_R = 3,\n    BOAT_STATE_JUMP_L = 4,\n    BOAT_STATE_HIT = 5,\n    BOAT_STATE_FALL = 6,\n    BOAT_STATE_DEATH = 8,\n} BOAT_STATE;\n\ntypedef struct {\n    int32_t boat_turn;\n    int32_t left_fallspeed;\n    int32_t right_fallspeed;\n    int16_t tilt_angle;\n    int16_t extra_rotation;\n    int32_t water;\n    int32_t pitch;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"boat_turn\", &p->boat_turn));\n    JSON_SHOULD(JSON_READ(io, \"left_fallspeed\", &p->left_fallspeed));\n    JSON_SHOULD(JSON_READ(io, \"right_fallspeed\", &p->right_fallspeed));\n    JSON_SHOULD(JSON_READ(io, \"tilt_angle\", &p->tilt_angle));\n    JSON_SHOULD(JSON_READ(io, \"extra_rotation\", &p->extra_rotation));\n    JSON_SHOULD(JSON_READ(io, \"water\", &p->water));\n    JSON_SHOULD(JSON_READ(io, \"pitch\", &p->pitch));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"boat_turn\", p->boat_turn);\n    JSONW_WRITE(io, \"left_fallspeed\", p->left_fallspeed);\n    JSONW_WRITE(io, \"right_fallspeed\", p->right_fallspeed);\n    JSONW_WRITE(io, \"tilt_angle\", p->tilt_angle);\n    JSONW_WRITE(io, \"extra_rotation\", p->extra_rotation);\n    JSONW_WRITE(io, \"water\", p->water);\n    JSONW_WRITE(io, \"pitch\", p->pitch);\n}\n\nstatic int32_t M_CheckGetOn(const int16_t item_num, const COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_status != LGS_ARMLESS) {\n        return 0;\n    }\n\n    ITEM *const boat_item = Item_Get(item_num);\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dist =\n        ((lara_item->pos.z - boat_item->pos.z) * Math_Cos(-boat_item->rot.y)\n         - (lara_item->pos.x - boat_item->pos.x) * Math_Sin(-boat_item->rot.y))\n        >> W2V_SHIFT;\n\n    if (dist > 200) {\n        return 0;\n    }\n\n    int32_t get_on = 0;\n    const int16_t rot = boat_item->rot.y - lara_item->rot.y;\n\n    if (lara->water_status == LWS_SURFACE || lara->water_status == LWS_WADE) {\n        if (!g_Input.action || lara_item->gravity || boat_item->speed) {\n            return 0;\n        }\n\n        if (rot > DEG_45 && rot < DEG_135) {\n            get_on = 1;\n        } else if (rot > -DEG_135 && rot < -DEG_45) {\n            get_on = 2;\n        }\n    } else if (lara->water_status == LWS_ABOVE_WATER) {\n        int16_t fall_speed = lara_item->fall_speed;\n        if (fall_speed > 0) {\n            if (rot > -DEG_135 && rot < DEG_135\n                && lara_item->pos.y > boat_item->pos.y) {\n                get_on = 3;\n            }\n        } else if (!fall_speed && rot > -DEG_135 && rot < DEG_135) {\n            if (lara_item->pos.x == boat_item->pos.x\n                && lara_item->pos.y == boat_item->pos.y\n                && lara_item->pos.z == boat_item->pos.z) {\n                get_on = 4;\n            } else {\n                get_on = 3;\n            }\n        }\n    }\n\n    if (!get_on) {\n        return 0;\n    }\n\n    if (!Item_TestBoundsCollide(boat_item, lara_item, coll->radius)) {\n        return 0;\n    }\n\n    if (!Collide_TestCollision(boat_item, lara_item)) {\n        return 0;\n    }\n\n    return get_on;\n}\n\nstatic int32_t M_TestWaterHeight(\n    const ITEM *const item, const int32_t z_off, const int32_t x_off,\n    XYZ_32 *const pos)\n{\n    // clang-format off\n    pos->y = item->pos.y\n        + ((x_off * Math_Sin(item->rot.z)) >> W2V_SHIFT)\n        - ((z_off * Math_Sin(item->rot.x)) >> W2V_SHIFT);\n    // clang-format on\n\n    const int32_t c = Math_Cos(item->rot.y);\n    const int32_t s = Math_Sin(item->rot.y);\n    pos->x = item->pos.x + ((x_off * c + z_off * s) >> W2V_SHIFT);\n    pos->z = item->pos.z + ((z_off * c - x_off * s) >> W2V_SHIFT);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(*pos, &room_num);\n    int32_t height = Room_GetWaterHeight(*pos, room_num);\n    if (height == NO_HEIGHT) {\n        const SECTOR *const sector = Room_GetSector(*pos, &room_num);\n        height = Room_GetHeight(sector, *pos);\n        if (height != NO_HEIGHT) {\n            return height;\n        }\n    }\n\n    return height + BOAT_SHIFT_Y;\n}\n\nstatic void M_DoWakeEffect(const ITEM *const boat_item)\n{\n    g_MatrixPtr->_23 = 0;\n    g_WMatrixPtr->_23 = 0;\n    Output_CalculateLight(boat_item->pos, boat_item->room_num);\n\n    const int16_t frame =\n        (Random_GetDraw() * Object_Get(O_WATER_SPRITE)->mesh_count) >> 15;\n\n    for (int32_t i = 0; i < 3; i++) {\n        const int16_t effect_num = Effect_Create(boat_item->room_num);\n        if (effect_num == NO_EFFECT) {\n            continue;\n        }\n\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_WATER_SPRITE;\n        effect->room_num = boat_item->room_num;\n        effect->frame_num = frame;\n\n        const int32_t c = Math_Cos(boat_item->rot.y);\n        const int32_t s = Math_Sin(boat_item->rot.y);\n        const int32_t w = (1 - i) * BOAT_SIDE;\n        const int32_t h = BOAT_WAKE;\n        effect->pos.x = boat_item->pos.x + ((-c * w - s * h) >> W2V_SHIFT);\n        effect->pos.y = boat_item->pos.y;\n        effect->pos.z = boat_item->pos.z + ((-c * h + s * w) >> W2V_SHIFT);\n        effect->rot.y = boat_item->rot.y + (i << W2V_SHIFT) - DEG_90;\n\n        effect->counter = 20;\n        effect->speed = boat_item->speed >> 2;\n        if (boat_item->speed < 64) {\n            effect->fall_speed =\n                (Random_GetDraw() * (ABS(boat_item->speed) - 64)) >> 15;\n        } else {\n            effect->fall_speed = 0;\n        }\n\n        effect->shade = Output_GetLightAdder() - 768;\n        CLAMPL(effect->shade, 0);\n    }\n}\n\nstatic void M_DoShift(const int32_t boat_num)\n{\n    ITEM *const boat_item = Item_Get(boat_num);\n    int16_t item_num = Room_Get(boat_item->room_num)->item_num;\n\n    while (item_num != NO_ITEM) {\n        ITEM *const item = Item_Get(item_num);\n\n        if (item->object_id == O_BOAT && item_num != boat_num\n            && Lara_Vehicle_GetIndex() != item_num) {\n            const int32_t dx = item->pos.x - boat_item->pos.x;\n            const int32_t dz = item->pos.z - boat_item->pos.z;\n            const int32_t dist = SQUARE(dx) + SQUARE(dz);\n\n            if (dist < SQUARE(BOAT_RADIUS * 2)) {\n                boat_item->pos.x =\n                    item->pos.x - SQUARE(BOAT_RADIUS * 2) * dx / dist;\n                boat_item->pos.z =\n                    item->pos.z - SQUARE(BOAT_RADIUS * 2) * dz / dist;\n            }\n            break;\n        }\n\n        if (item->object_id == O_GONDOLA\n            && item->current_anim_state == GONDOLA_STATE_FLOATING) {\n            const int32_t c = Math_Cos(item->rot.y);\n            const int32_t s = Math_Sin(item->rot.y);\n            const int32_t ix = item->pos.x - ((s * STEP_L * 2) >> W2V_SHIFT);\n            const int32_t iz = item->pos.z - ((c * STEP_L * 2) >> W2V_SHIFT);\n            const int32_t dx = ix - boat_item->pos.x;\n            const int32_t dz = iz - boat_item->pos.z;\n            const int32_t dist = SQUARE(dx) + SQUARE(dz);\n\n            if (dist < SQUARE(BOAT_RADIUS * 2)) {\n                if (boat_item->speed < BOAT_MAX_SPEED - 10) {\n                    boat_item->pos.x = ix - SQUARE(BOAT_RADIUS * 2) * dx / dist;\n                    boat_item->pos.z = iz - SQUARE(BOAT_RADIUS * 2) * dz / dist;\n                } else if (item->pos.y - boat_item->pos.y < WALL_L * 2) {\n                    Sound_Effect(SFX_BOAT_INTO_WATER, &item->pos, SPM_NORMAL);\n                    item->goal_anim_state = GONDOLA_STATE_CRASH;\n                }\n            }\n        }\n\n        item_num = item->next_item;\n    }\n}\n\nstatic int32_t M_DoDynamics(\n    const int32_t height, int32_t fall_speed, int32_t *const y)\n{\n    if (height > *y) {\n        *y = fall_speed + *y;\n        if (*y > height) {\n            *y = height;\n            fall_speed = 0;\n        } else {\n            fall_speed += GRAVITY;\n        }\n    } else {\n        fall_speed += ((height - fall_speed - *y) >> 3);\n        CLAMPL(fall_speed, -20);\n        CLAMPG(*y, height);\n    }\n\n    return fall_speed;\n}\n\nstatic int32_t M_Dynamics(const int16_t boat_num)\n{\n    ITEM *const boat_item = Item_Get(boat_num);\n    M_PRIV *const p = boat_item->priv;\n    boat_item->rot.z -= p->tilt_angle;\n\n    XYZ_32 fl_old;\n    XYZ_32 bl_old;\n    XYZ_32 fr_old;\n    XYZ_32 br_old;\n    XYZ_32 f_old;\n    const int32_t hfl_old =\n        M_TestWaterHeight(boat_item, BOAT_FRONT, -BOAT_SIDE, &fl_old);\n    const int32_t hfr_old =\n        M_TestWaterHeight(boat_item, BOAT_FRONT, BOAT_SIDE, &fr_old);\n    const int32_t hbl_old =\n        M_TestWaterHeight(boat_item, -BOAT_FRONT, -BOAT_SIDE, &bl_old);\n    const int32_t hbr_old =\n        M_TestWaterHeight(boat_item, -BOAT_FRONT, BOAT_SIDE, &br_old);\n    const int32_t hf_old = M_TestWaterHeight(boat_item, BOAT_TIP, 0, &f_old);\n    XYZ_32 old = boat_item->pos;\n    CLAMPG(bl_old.y, hbl_old);\n    CLAMPG(br_old.y, hbr_old);\n    CLAMPG(fl_old.y, hfl_old);\n    CLAMPG(fr_old.y, hfr_old);\n    CLAMPG(f_old.y, hf_old);\n\n    boat_item->rot.y += p->extra_rotation + p->boat_turn;\n    p->tilt_angle = p->boat_turn * 6;\n\n    boat_item->pos.z +=\n        (boat_item->speed * Math_Cos(boat_item->rot.y)) >> W2V_SHIFT;\n    boat_item->pos.x +=\n        (boat_item->speed * Math_Sin(boat_item->rot.y)) >> W2V_SHIFT;\n\n    int32_t slip = (Math_Sin(boat_item->rot.z) * 30) >> W2V_SHIFT;\n    if (!slip && boat_item->rot.z) {\n        slip = boat_item->rot.z > 0 ? 1 : -1;\n    }\n    boat_item->pos.z -= (slip * Math_Sin(boat_item->rot.y)) >> W2V_SHIFT;\n    boat_item->pos.x += (slip * Math_Cos(boat_item->rot.y)) >> W2V_SHIFT;\n\n    slip = (Math_Sin(boat_item->rot.x) * 10) >> W2V_SHIFT;\n    if (!slip && boat_item->rot.x) {\n        slip = boat_item->rot.x > 0 ? 1 : -1;\n    }\n\n    boat_item->pos.z -= (slip * Math_Cos(boat_item->rot.y)) >> W2V_SHIFT;\n    boat_item->pos.x =\n        boat_item->pos.x - ((slip * Math_Sin(boat_item->rot.y)) >> W2V_SHIFT);\n\n    XYZ_32 moved = {\n        .x = boat_item->pos.x,\n        .y = 0,\n        .z = boat_item->pos.z,\n    };\n    M_DoShift(boat_num);\n\n    int32_t rot = 0;\n\n    XYZ_32 bl;\n    const int32_t hbl =\n        M_TestWaterHeight(boat_item, -BOAT_FRONT, -BOAT_SIDE, &bl);\n    if (hbl < bl_old.y - STEP_L / 2) {\n        rot = Vehicle_DoShift(boat_item, &bl, &bl_old);\n    }\n\n    XYZ_32 br;\n    const int32_t hbr =\n        M_TestWaterHeight(boat_item, -BOAT_FRONT, BOAT_SIDE, &br);\n    if (hbr < br_old.y - STEP_L / 2) {\n        rot += Vehicle_DoShift(boat_item, &br, &br_old);\n    }\n\n    XYZ_32 fl;\n    const int32_t hfl =\n        M_TestWaterHeight(boat_item, BOAT_FRONT, -BOAT_SIDE, &fl);\n    if (hfl < fl_old.y - STEP_L / 2) {\n        rot += Vehicle_DoShift(boat_item, &fl, &fl_old);\n    }\n\n    XYZ_32 fr;\n    const int32_t hfr =\n        M_TestWaterHeight(boat_item, BOAT_FRONT, BOAT_SIDE, &fr);\n    if (hfr < fr_old.y - STEP_L / 2) {\n        rot += Vehicle_DoShift(boat_item, &fr, &fr_old);\n    }\n\n    if (!slip) {\n        XYZ_32 f;\n        const int32_t hf = M_TestWaterHeight(boat_item, BOAT_TIP, 0, &f);\n        if (hf < f_old.y - STEP_L / 2) {\n            Vehicle_DoShift(boat_item, &f, &f_old);\n        }\n    }\n\n    int16_t room_num = boat_item->room_num;\n    const SECTOR *const sector = Room_GetSector(boat_item->pos, &room_num);\n    int32_t height = Room_GetWaterHeight(boat_item->pos, room_num);\n    if (height == NO_HEIGHT) {\n        height = Room_GetHeight(sector, boat_item->pos);\n    }\n    if (height < boat_item->pos.y - STEP_L / 2) {\n        Vehicle_DoShift(boat_item, &boat_item->pos, &old);\n    }\n\n    p->extra_rotation = rot;\n\n    const int32_t collide = Vehicle_GetCollisionAnim(boat_item, &moved);\n    if (slip || collide) {\n        // clang-format off\n        const int32_t new_speed = (\n            (boat_item->pos.z - old.z) * Math_Cos(boat_item->rot.y) +\n            (boat_item->pos.x - old.x) * Math_Sin(boat_item->rot.y)\n        ) >> W2V_SHIFT;\n        // clang-format on\n\n        if (Lara_Vehicle_GetIndex() == boat_num) {\n            if (boat_item->speed > BOAT_MAX_SPEED + BOAT_ACCELERATION\n                && new_speed < boat_item->speed - 10) {\n                Lara_TakeDamage((boat_item->speed - new_speed) / 2, true);\n                Sound_Effect(SFX_LARA_INJURY, &Lara_GetItem()->pos, SPM_NORMAL);\n            }\n        }\n\n        if (slip) {\n            if (boat_item->speed <= BOAT_MAX_SPEED + 10) {\n                boat_item->speed = new_speed;\n            }\n        } else {\n            if (boat_item->speed > 0 && new_speed < boat_item->speed) {\n                boat_item->speed = new_speed;\n            } else if (boat_item->speed < 0 && new_speed > boat_item->speed) {\n                boat_item->speed = new_speed;\n            }\n        }\n\n        CLAMPL(boat_item->speed, BOAT_MAX_BACK);\n    }\n\n    return collide;\n}\n\nstatic int32_t M_UserControl(ITEM *const boat_item)\n{\n    int32_t no_turn = 1;\n\n    M_PRIV *const p = boat_item->priv;\n    if (boat_item->pos.y < p->water - STEP_L / 2 || p->water == NO_HEIGHT) {\n        return no_turn;\n    }\n\n    if (g_Input.look && boat_item->speed == 0) {\n        Lara_Look_UpDown();\n        return no_turn;\n    }\n\n    if (g_Input.jump) {\n        return no_turn;\n    }\n\n    const bool look =\n        g_Input.look && g_Config.gameplay.look_mode != LOOK_MODE_RESTRICTED;\n    const bool left_input = g_Input.left && !look;\n    const bool right_input = g_Input.right && !look;\n\n    if ((left_input && !g_Input.back) || (right_input && g_Input.back)) {\n        if (p->boat_turn > 0) {\n            p->boat_turn -= BOAT_UNDO_TURN;\n        } else {\n            p->boat_turn -= BOAT_TURN;\n            CLAMPL(p->boat_turn, -BOAT_MAX_TURN);\n        }\n        no_turn = 0;\n    } else if ((right_input && !g_Input.back) || (left_input && g_Input.back)) {\n        if (p->boat_turn < 0) {\n            p->boat_turn += BOAT_UNDO_TURN;\n        } else {\n            p->boat_turn += BOAT_TURN;\n            CLAMPG(p->boat_turn, BOAT_MAX_TURN);\n        }\n        no_turn = 0;\n    }\n\n    if (g_Input.back) {\n        if (boat_item->speed > 0) {\n            boat_item->speed -= BOAT_BRAKE;\n        } else if (boat_item->speed > BOAT_MAX_BACK) {\n            boat_item->speed += BOAT_REVERSE;\n        }\n    } else if (g_Input.forward) {\n        int32_t max_speed;\n        if (g_Input.action) {\n            max_speed = BOAT_FAST_SPEED;\n        } else {\n            max_speed = g_Input.slow ? BOAT_SLOW_SPEED : BOAT_MAX_SPEED;\n        }\n\n        if (boat_item->speed < max_speed) {\n            boat_item->speed += BOAT_ACCELERATION / 2\n                + BOAT_ACCELERATION * boat_item->speed / (2 * max_speed);\n        } else if (boat_item->speed > max_speed + BOAT_SLOWDOWN) {\n            boat_item->speed -= BOAT_SLOWDOWN;\n        }\n    } else if (\n        boat_item->speed >= 0 && boat_item->speed < BOAT_MIN_SPEED\n        && (left_input || right_input)) {\n        boat_item->speed = BOAT_MIN_SPEED;\n    } else if (boat_item->speed > BOAT_SLOWDOWN) {\n        boat_item->speed -= BOAT_SLOWDOWN;\n    } else {\n        boat_item->speed = 0;\n    }\n\n    return no_turn;\n}\n\nstatic void M_Animation(const ITEM *const boat_item, const int32_t collide)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    const M_PRIV *const p = boat_item->priv;\n\n    if (lara_item->hit_points <= 0) {\n        if (lara_item->current_anim_state == BOAT_STATE_DEATH) {\n            return;\n        }\n        Item_SwitchToObjAnim(lara_item, BOAT_DEATH_ANIM, 0, O_LARA_BOAT);\n        lara_item->goal_anim_state = BOAT_STATE_DEATH;\n        lara_item->current_anim_state = BOAT_STATE_DEATH;\n        return;\n    }\n\n    if (boat_item->pos.y < p->water - STEP_L / 2 && boat_item->fall_speed > 0) {\n        if (lara_item->current_anim_state == BOAT_STATE_FALL) {\n            return;\n        }\n        Item_SwitchToObjAnim(lara_item, BOAT_FALL_ANIM, 0, O_LARA_BOAT);\n        lara_item->goal_anim_state = BOAT_STATE_FALL;\n        lara_item->current_anim_state = BOAT_STATE_FALL;\n        return;\n    }\n\n    if (collide) {\n        if (lara_item->current_anim_state == BOAT_STATE_HIT) {\n            return;\n        }\n        Item_SwitchToObjAnim(lara_item, collide, 0, O_LARA_BOAT);\n        lara_item->goal_anim_state = BOAT_STATE_HIT;\n        lara_item->current_anim_state = BOAT_STATE_HIT;\n        return;\n    }\n\n    switch (lara_item->current_anim_state) {\n    case BOAT_STATE_STILL:\n        if (g_Input.jump) {\n            if (g_Input.right) {\n                lara_item->goal_anim_state = BOAT_STATE_JUMP_R;\n            } else if (g_Input.left) {\n                lara_item->goal_anim_state = BOAT_STATE_JUMP_L;\n            }\n        }\n\n        if (boat_item->speed > 0) {\n            lara_item->goal_anim_state = BOAT_STATE_MOVING;\n        }\n        break;\n\n    case BOAT_STATE_MOVING:\n        if (g_Input.jump) {\n            if (g_Input.right) {\n                lara_item->goal_anim_state = BOAT_STATE_JUMP_R;\n            } else if (g_Input.left) {\n                lara_item->goal_anim_state = BOAT_STATE_JUMP_L;\n            }\n        } else if (boat_item->speed <= 0) {\n            lara_item->goal_anim_state = BOAT_STATE_STILL;\n        }\n        break;\n\n    case BOAT_STATE_FALL:\n        lara_item->goal_anim_state = BOAT_STATE_MOVING;\n        break;\n    }\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->boat_turn = 0;\n    p->left_fallspeed = 0;\n    p->right_fallspeed = 0;\n    p->tilt_angle = 0;\n    p->extra_rotation = 0;\n    p->water = 0;\n    p->pitch = 0;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) {\n        return;\n    }\n\n    const int32_t get_on = M_CheckGetOn(item_num, coll);\n    if (!get_on) {\n        coll->enable_baddie_push = 1;\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    Lara_Vehicle_SetIndex(item_num);\n\n    int16_t boat_anim_idx;\n    switch (get_on) {\n    case 1:\n        boat_anim_idx = BOAT_GET_ON_RW_ANIM;\n        break;\n    case 2:\n        boat_anim_idx = BOAT_GET_ON_LW_ANIM;\n        break;\n    case 3:\n        boat_anim_idx = BOAT_GET_ON_J_ANIM;\n        break;\n    default:\n        boat_anim_idx = BOAT_GET_ON_START;\n        break;\n    }\n\n    Item_SwitchToObjAnim(lara_item, boat_anim_idx, 0, O_LARA_BOAT);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->water_status = LWS_ABOVE_WATER;\n    lara->hit_direction = DIR_UNKNOWN;\n\n    ITEM *const boat_item = Item_Get(item_num);\n\n    lara_item->pos.x = boat_item->pos.x;\n    lara_item->pos.y = boat_item->pos.y + BOAT_SHIFT_Y;\n    lara_item->pos.z = boat_item->pos.z;\n    lara_item->gravity = false;\n    lara_item->rot.x = 0;\n    lara_item->rot.y = boat_item->rot.y;\n    lara_item->rot.z = 0;\n    lara_item->speed = 0;\n    lara_item->fall_speed = 0;\n    lara_item->goal_anim_state = 0;\n    lara_item->current_anim_state = 0;\n\n    Item_UpdateRoom(lara->item_num, boat_item->room_num);\n\n    Item_Animate(lara_item);\n    if (boat_item->status != IS_ACTIVE) {\n        Item_AddActive(item_num);\n        boat_item->status = IS_ACTIVE;\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    ITEM *const boat_item = Item_Get(item_num);\n    M_PRIV *const p = boat_item->priv;\n\n    bool drive = false;\n    int32_t no_turn = 1;\n    int32_t collide = M_Dynamics(item_num);\n\n    XYZ_32 fl;\n    XYZ_32 fr;\n    const int32_t hfl =\n        M_TestWaterHeight(boat_item, BOAT_FRONT, -BOAT_SIDE, &fl);\n    const int32_t hfr =\n        M_TestWaterHeight(boat_item, BOAT_FRONT, BOAT_SIDE, &fr);\n\n    int16_t room_num = boat_item->room_num;\n    const SECTOR *sector = Room_GetSector(\n        (XYZ_32) {\n            boat_item->pos.x,\n            boat_item->pos.y + BOAT_SHIFT_Y,\n            boat_item->pos.z,\n        },\n        &room_num);\n    int32_t height = Room_GetHeight(sector, boat_item->pos);\n    const int32_t ceiling = Room_GetCeiling(sector, boat_item->pos);\n\n    const int32_t water_height = Room_GetWaterHeight(boat_item->pos, room_num);\n    p->water = water_height;\n\n    if (Lara_Vehicle_GetIndex() == item_num && lara_item->hit_points > 0) {\n        switch (lara_item->current_anim_state) {\n        case BOAT_STATE_GET_ON:\n        case BOAT_STATE_JUMP_R:\n        case BOAT_STATE_JUMP_L:\n            break;\n\n        default:\n            drive = true;\n            no_turn = M_UserControl(boat_item);\n            break;\n        }\n    } else if (boat_item->speed > BOAT_SLOWDOWN) {\n        boat_item->speed -= BOAT_SLOWDOWN;\n    } else {\n        boat_item->speed = 0;\n    }\n\n    if (no_turn) {\n        if (p->boat_turn < -BOAT_UNDO_TURN) {\n            p->boat_turn += BOAT_UNDO_TURN;\n        } else if (p->boat_turn > BOAT_UNDO_TURN) {\n            p->boat_turn -= BOAT_UNDO_TURN;\n        } else {\n            p->boat_turn = 0;\n        }\n    }\n\n    boat_item->floor = height + BOAT_SHIFT_Y;\n    if (p->water == NO_HEIGHT) {\n        p->water = height;\n    } else {\n        p->water -= 5;\n    }\n\n    p->left_fallspeed = M_DoDynamics(hfl, p->left_fallspeed, &fl.y);\n    p->right_fallspeed = M_DoDynamics(hfr, p->right_fallspeed, &fr.y);\n    boat_item->fall_speed =\n        M_DoDynamics(p->water, boat_item->fall_speed, &boat_item->pos.y);\n\n    height = (fr.y + fl.y) / 2;\n\n    const int16_t x_rot = Math_Atan(BOAT_FRONT, boat_item->pos.y - height);\n    const int16_t z_rot = Math_Atan(BOAT_SIDE, height - fl.y);\n    boat_item->rot.x += (x_rot - boat_item->rot.x) / 2;\n    boat_item->rot.z += (z_rot - boat_item->rot.z) / 2;\n\n    if (x_rot == 0 && ABS(boat_item->rot.x) < 4) {\n        boat_item->rot.x = 0;\n    }\n    if (z_rot == 0 && ABS(boat_item->rot.z) < 4) {\n        boat_item->rot.z = 0;\n    }\n\n    if (Lara_Vehicle_GetIndex() == item_num) {\n        M_Animation(boat_item, collide);\n\n        Item_UpdateRoom(item_num, room_num);\n\n        boat_item->rot.z += p->tilt_angle;\n        lara_item->pos.x = boat_item->pos.x;\n        lara_item->pos.y = boat_item->pos.y;\n        lara_item->pos.z = boat_item->pos.z;\n        lara_item->rot.x = boat_item->rot.x;\n        lara_item->rot.y = boat_item->rot.y;\n        lara_item->rot.z = boat_item->rot.z;\n        Room_TestTriggers(lara_item);\n        Room_TestTriggers(boat_item);\n\n        sector = Room_GetSector(\n            (XYZ_32) {\n                lara_item->pos.x,\n                lara_item->pos.y + BOAT_SHIFT_Y,\n                lara_item->pos.z,\n            },\n            &room_num);\n        Item_UpdateRoom(lara->item_num, room_num);\n\n        Item_Animate(lara_item);\n\n        if (lara_item->hit_points > 0) {\n            const int16_t lara_anim_num =\n                Item_GetRelativeObjAnim(lara_item, O_LARA_BOAT);\n            const int16_t lara_frame_num = Item_GetRelativeFrame(lara_item);\n            Item_SwitchToAnim(boat_item, lara_anim_num, lara_frame_num);\n        }\n\n        g_Camera.target_elevation = -20 * DEG_1;\n        g_Camera.target_distance = 2 * WALL_L;\n    } else {\n        Item_UpdateRoom(item_num, room_num);\n        boat_item->rot.z += p->tilt_angle;\n    }\n\n    const int32_t pitch = water_height - ceiling < BOAT_SOUND_CEILING\n        ? boat_item->speed * (water_height - ceiling) / BOAT_SOUND_CEILING\n        : boat_item->speed;\n\n    p->pitch += ((pitch - p->pitch) >> 2);\n    if (boat_item->speed != 0\n        && water_height + BOAT_SHIFT_Y != boat_item->pos.y) {\n        Sound_Effect(SFX_BOAT_ENGINE, &boat_item->pos, SPM_NORMAL);\n    } else if (boat_item->speed > 20) {\n        Sound_Effect(\n            SFX_BOAT_MOVING, &boat_item->pos,\n            SPM_PITCH | ((0x10000 - (BOAT_MAX_SPEED - p->pitch) * 100) << 8));\n\n    } else if (drive) {\n        Sound_Effect(\n            SFX_BOAT_IDLE, &boat_item->pos,\n            SPM_PITCH | ((0x10000 - (BOAT_MAX_SPEED - p->pitch) * 100) << 8));\n    }\n\n    if (boat_item->speed && water_height + BOAT_SHIFT_Y == boat_item->pos.y) {\n        M_DoWakeEffect(boat_item);\n    }\n\n    if (Lara_Vehicle_GetIndex() != item_num) {\n        return;\n    }\n\n    if ((lara_item->current_anim_state == BOAT_STATE_JUMP_R\n         || lara_item->current_anim_state == BOAT_STATE_JUMP_L)\n        && Item_TestFrameEqual(lara_item, LF_BOAT_EXIT_END)) {\n        if (lara_item->current_anim_state == BOAT_STATE_JUMP_L) {\n            lara_item->rot.y -= DEG_90;\n        } else {\n            lara_item->rot.y += DEG_90;\n        }\n\n        Item_SwitchToAnim(lara_item, LA(LA_JUMP_FORWARD), 0);\n        lara_item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        lara_item->current_anim_state = LS(LS_JUMP_FORWARD);\n        lara_item->gravity = true;\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n        lara_item->speed = 20;\n        lara_item->fall_speed = -40;\n        Lara_Vehicle_SetIndex(NO_ITEM);\n\n        const XYZ_32 pos = {\n            .x = lara_item->pos.x\n                + ((360 * Math_Sin(lara_item->rot.y)) >> W2V_SHIFT),\n            .y = lara_item->pos.y - 90,\n            .z = lara_item->pos.z\n                + ((360 * Math_Cos(lara_item->rot.y)) >> W2V_SHIFT),\n        };\n\n        room_num = lara_item->room_num;\n        sector = Room_GetSector(pos, &room_num);\n        if (Room_GetHeight(sector, pos) >= pos.y - STEP_L) {\n            lara_item->pos.x = pos.x;\n            lara_item->pos.z = pos.z;\n            Item_UpdateRoom(lara->item_num, room_num);\n        }\n\n        lara_item->pos.y = pos.y;\n        Item_SwitchToAnim(boat_item, 0, 0);\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_BOAT, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/common.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/cutscene.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/table.h>\n#include <trx/game/lara.h>\n#include <trx/game/level.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/output.h>\n#include <trx/game/viewport.h>\n\ntypedef enum {\n    LA_VEHICLE_HIT_LEFT = 11,\n    LA_VEHICLE_HIT_RIGHT = 12,\n    LA_VEHICLE_HIT_FRONT = 13,\n    LA_VEHICLE_HIT_BACK = 14,\n} LARA_ANIM_VEHICLE;\n\nint32_t Vehicle_DoShift(\n    ITEM *const vehicle, const XYZ_32 *const pos, const XYZ_32 *const old)\n{\n    int32_t x = pos->x >> WALL_SHIFT;\n    int32_t z = pos->z >> WALL_SHIFT;\n    const int32_t old_x = old->x >> WALL_SHIFT;\n    const int32_t old_z = old->z >> WALL_SHIFT;\n    const int32_t shift_x = pos->x & (WALL_L - 1);\n    const int32_t shift_z = pos->z & (WALL_L - 1);\n\n    if (x == old_x) {\n        if (z == old_z) {\n            vehicle->pos.x += old->x - pos->x;\n            vehicle->pos.z += old->z - pos->z;\n        } else if (z > old_z) {\n            vehicle->pos.z -= shift_z + 1;\n            return pos->x - vehicle->pos.x;\n        } else {\n            vehicle->pos.z += WALL_L - shift_z;\n            return vehicle->pos.x - pos->x;\n        }\n    } else if (z == old_z) {\n        if (x > old_x) {\n            vehicle->pos.x -= shift_x + 1;\n            return vehicle->pos.z - pos->z;\n        } else {\n            vehicle->pos.x += WALL_L - shift_x;\n            return pos->z - vehicle->pos.z;\n        }\n    } else {\n        int16_t room_num;\n        const SECTOR *sector;\n        int32_t height;\n\n        x = 0;\n        z = 0;\n\n        XYZ_32 test_pos = (XYZ_32) { old->x, pos->y, pos->z };\n        room_num = vehicle->room_num;\n        sector = Room_GetSector(test_pos, &room_num);\n        height = Room_GetHeight(sector, test_pos);\n        if (height < old->y - STEP_L) {\n            if (pos->z > old->z) {\n                z = -shift_z - 1;\n            } else {\n                z = WALL_L - shift_z;\n            }\n        }\n\n        test_pos = (XYZ_32) { pos->x, pos->y, old->z };\n        room_num = vehicle->room_num;\n        sector = Room_GetSector(test_pos, &room_num);\n        height = Room_GetHeight(sector, test_pos);\n        if (height < old->y - STEP_L) {\n            if (pos->x > old->x) {\n                x = -shift_x - 1;\n            } else {\n                x = WALL_L - shift_x;\n            }\n        }\n\n        if (x != 0 && z != 0) {\n            vehicle->pos.x += x;\n            vehicle->pos.z += z;\n        } else if (z != 0) {\n            vehicle->pos.z += z;\n            if (z > 0) {\n                return vehicle->pos.x - pos->x;\n            } else {\n                return pos->x - vehicle->pos.x;\n            }\n        } else if (x != 0) {\n            vehicle->pos.x += x;\n            if (x > 0) {\n                return pos->z - vehicle->pos.z;\n            } else {\n                return vehicle->pos.z - pos->z;\n            }\n        } else {\n            vehicle->pos.x += old->x - pos->x;\n            vehicle->pos.z += old->z - pos->z;\n        }\n    }\n\n    return 0;\n}\n\nint32_t Vehicle_GetCollisionAnim(const ITEM *const vehicle, XYZ_32 *const moved)\n{\n    moved->x = vehicle->pos.x - moved->x;\n    moved->z = vehicle->pos.z - moved->z;\n\n    if (moved->x != 0 || moved->z != 0) {\n        const int32_t c = Math_Cos(vehicle->rot.y);\n        const int32_t s = Math_Sin(vehicle->rot.y);\n        const int32_t front = (moved->x * s + moved->z * c) >> W2V_SHIFT;\n        const int32_t side = (moved->x * c - moved->z * s) >> W2V_SHIFT;\n        if (ABS(front) > ABS(side)) {\n            if (front > 0) {\n                return LA_VEHICLE_HIT_BACK;\n            } else {\n                return LA_VEHICLE_HIT_FRONT;\n            }\n        } else {\n            if (side > 0) {\n                return LA_VEHICLE_HIT_LEFT;\n            } else {\n                return LA_VEHICLE_HIT_RIGHT;\n            }\n        }\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/common.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nint32_t Vehicle_DoShift(ITEM *vehicle, const XYZ_32 *pos, const XYZ_32 *old);\nint32_t Vehicle_GetCollisionAnim(const ITEM *vehicle, XYZ_32 *moved);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/kayak.c",
    "content": "#include <trx/game/objects/vehicles/kayak.h>\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/fx/wake.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/input.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/skin/common.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\ntypedef enum {\n    M_STATE_BACK = 0,\n    M_STATE_POSE = 1,\n    M_STATE_LEFT = 2,\n    M_STATE_RIGHT = 3,\n    M_STATE_CLIMB_IN = 4,\n    M_STATE_DEATH_IN = 5,\n    M_STATE_FORWARD = 6,\n    M_STATE_ROLL = 7,\n    M_STATE_DROWN_IN = 8,\n    M_STATE_JUMP_OUT = 9,\n    M_STATE_TURN_L = 10,\n    M_STATE_TURN_R = 11,\n    M_STATE_CLIMB_IN_R = 12,\n    M_STATE_CLIMB_OUT_L = 13,\n    M_STATE_CLIMB_OUT_R = 14,\n} M_STATE;\n\ntypedef struct {\n    struct {\n        bool frame2_latched;\n        uint8_t stroke_count;\n        bool equipped;\n    } paddle;\n    int32_t vel;\n    int32_t rot;\n    int32_t fall_speed_f;\n    int32_t fall_speed_l;\n    int32_t fall_speed_r;\n    int32_t water;\n    XYZ_32 old_pos;\n    bool turn;\n    bool forward;\n    bool true_water;\n    uint8_t counter;\n} M_PRIV;\n\n// clang-format off\n// Hidden while mounting/in kayak to prevent clipping through the hull.\nstatic const uint32_t m_KayakHiddenBodyMeshes =\n    (1u << LM_HIPS)\n    | (1u << LM_THIGH_L)\n    | (1u << LM_CALF_L)\n    | (1u << LM_FOOT_L)\n    | (1u << LM_THIGH_R)\n    | (1u << LM_CALF_R)\n    | (1u << LM_FOOT_R);\n\nstatic const XZ_16 m_MistPos[10] = {\n    { .x = 32, .z = 900 },\n    { .x = 96, .z = 750 },\n    { .x = 170, .z = 600 },\n    { .x = 220, .z = 450 },\n    { .x = 300, .z = 300 },\n    { .x = 400, .z = 150 },\n    { .x = 400, .z = 0 },\n    { .x = 300, .z = -150 },\n    { .x = 200, .z = -300 },\n    { .x = 64, .z = -450 },\n};\n// clang-format on\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"vel\", &p->vel));\n    JSON_SHOULD(JSON_READ(io, \"rot\", &p->rot));\n    JSON_SHOULD(JSON_READ(io, \"fall_speed_f\", &p->fall_speed_f));\n    JSON_SHOULD(JSON_READ(io, \"fall_speed_l\", &p->fall_speed_l));\n    JSON_SHOULD(JSON_READ(io, \"fall_speed_r\", &p->fall_speed_r));\n    JSON_SHOULD(JSON_READ(io, \"water\", &p->water));\n    JSON_SHOULD(JSON_READ(io, \"old_pos\", &p->old_pos));\n    JSON_SHOULD(JSON_READ(io, \"turn\", &p->turn));\n    JSON_SHOULD(JSON_READ(io, \"forward\", &p->forward));\n    JSON_SHOULD(JSON_READ(io, \"true_water\", &p->true_water));\n    JSON_SHOULD(JSON_READ(io, \"counter\", &p->counter));\n    JSON_SHOULD(\n        JSON_READ(io, \"paddle_frame2_latched\", &p->paddle.frame2_latched));\n    JSON_SHOULD(JSON_READ(io, \"paddle_stroke_count\", &p->paddle.stroke_count));\n    JSON_SHOULD(JSON_READ(io, \"paddle_equipped\", &p->paddle.equipped));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"vel\", p->vel);\n    JSONW_WRITE(io, \"rot\", p->rot);\n    JSONW_WRITE(io, \"fall_speed_f\", p->fall_speed_f);\n    JSONW_WRITE(io, \"fall_speed_l\", p->fall_speed_l);\n    JSONW_WRITE(io, \"fall_speed_r\", p->fall_speed_r);\n    JSONW_WRITE(io, \"water\", p->water);\n    JSONW_WRITE(io, \"old_pos\", p->old_pos);\n    JSONW_WRITE(io, \"turn\", p->turn);\n    JSONW_WRITE(io, \"forward\", p->forward);\n    JSONW_WRITE(io, \"true_water\", p->true_water);\n    JSONW_WRITE(io, \"counter\", p->counter);\n    JSONW_WRITE(io, \"paddle_frame2_latched\", p->paddle.frame2_latched);\n    JSONW_WRITE(io, \"paddle_stroke_count\", p->paddle.stroke_count);\n    JSONW_WRITE(io, \"paddle_equipped\", p->paddle.equipped);\n}\n\nstatic void M_Initialise(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->rot = 0;\n    p->vel = 0;\n    p->fall_speed_r = 0;\n    p->fall_speed_l = 0;\n    p->fall_speed_f = 0;\n    p->old_pos = item->pos;\n    p->paddle.equipped = false;\n    FX_Wake_Reset();\n}\n\nstatic int32_t M_GetInKayak(const int16_t item_num, const COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (!g_Input.action || lara_info->gun_status != LGS_ARMLESS\n        || lara_item->gravity) {\n        return 0;\n    }\n\n    const ITEM *const item = Item_Get(item_num);\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t dist = SQUARE(dx) + SQUARE(dz);\n    if (dist > 130000) {\n        return 0;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const floor = Room_GetSector(item->pos, &room_num);\n    const int32_t h = Room_GetHeight(floor, item->pos);\n    if (h <= -32000) {\n        return 0;\n    }\n\n    const int16_t ang =\n        Math_Atan(\n            item->pos.z - lara_item->pos.z, item->pos.x - lara_item->pos.x)\n        - item->rot.y;\n    const uint16_t temp_ang = lara_item->rot.y - item->rot.y;\n    if (ang > -45 * DEG_1 && ang < 135 * DEG_1) {\n        if (temp_ang > 45 * DEG_1 && temp_ang < 135 * DEG_1) {\n            return -1;\n        }\n    } else {\n        if (temp_ang > 225 * DEG_1 && temp_ang < 315 * DEG_1) {\n            return 1;\n        }\n    }\n\n    return 0;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const l, COLL_INFO *const coll)\n{\n    if (l->hit_points < 0 || Lara_Vehicle_GetIndex() != NO_ITEM) {\n        return;\n    }\n\n    const int32_t lr = M_GetInKayak(item_num, coll);\n    if (lr == 0) {\n        coll->enable_baddie_push = 1;\n        Object_Collision(item_num, l, coll);\n        return;\n    }\n\n    Lara_Vehicle_SetIndex(item_num);\n    ITEM *const item = Item_Get(item_num);\n\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara_info->gun_type == LGT_FLARE) {\n        Lara_Flare_Dispose(false);\n        lara_info->flare.control = false;\n        lara_info->gun_type = LGT_UNARMED;\n        lara_info->request_gun_type = LGT_UNARMED;\n    }\n\n    if (lr > 0) {\n        Item_SwitchToObjAnim(l, 3, 0, O_LARA_VEHICLE_ANIM);\n    } else {\n        Item_SwitchToObjAnim(l, 28, 0, O_LARA_VEHICLE_ANIM);\n    }\n    l->current_anim_state = M_STATE_CLIMB_IN;\n    l->goal_anim_state = M_STATE_CLIMB_IN;\n    lara_info->water_status = LWS_ABOVE_WATER;\n    l->pos = item->pos;\n    l->rot.x = 0;\n    l->rot.y = item->rot.y;\n    l->rot.z = 0;\n    l->gravity = false;\n    l->speed = 0;\n    l->fall_speed = 0;\n\n    if (l->room_num != item->room_num) {\n        Item_UpdateRoom(lara_info->item_num, item->room_num);\n    }\n\n    M_PRIV *p = item->priv;\n    p->water = item->pos.y;\n    p->paddle.equipped = false;\n}\n\nstatic void M_DoRipple(\n    const ITEM *const item, const int16_t x_offset, const int16_t z_offset)\n{\n    XYZ_32 pos = item->pos;\n    pos = XYZ_32_OffsetYaw(pos, item->rot.y, z_offset);\n    pos = XYZ_32_OffsetYaw(pos, item->rot.y + DEG_90, x_offset);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n    const int32_t wh = Room_GetWaterHeight(pos, room_num);\n    if (wh == NO_HEIGHT) {\n        return;\n    }\n\n    FX_WATER_RIPPLE *const ripple = FX_Water_SetupRipple(\n        pos.x, pos.y, pos.z, -2 - (Random_GetControl() & 1), 0);\n    if (ripple != nullptr) {\n        ripple->init = 0;\n    }\n}\n\nstatic int32_t M_TestHeight(\n    const ITEM *const item, const int32_t x, const int32_t z, XYZ_32 *const pos)\n{\n    const int32_t zs = Math_Sin(item->rot.z);\n    const int32_t xs = Math_Sin(item->rot.x);\n\n    *pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, z);\n    *pos = XYZ_32_OffsetYaw(*pos, item->rot.y + DEG_90, x);\n    pos->y = (item->pos.y + ((x * zs) >> W2V_SHIFT)) - ((z * xs) >> W2V_SHIFT);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(*pos, &room_num);\n    int32_t h = Room_GetWaterHeight(*pos, room_num);\n\n    if (h == NO_HEIGHT) {\n        room_num = item->room_num;\n        SECTOR *const floor = Room_GetSector(*pos, &room_num);\n        h = Room_GetHeight(floor, *pos);\n        if (h == NO_HEIGHT) {\n            return h;\n        }\n    }\n\n    return h - 5;\n}\n\nstatic bool M_CanGetOut(const ITEM *const item, const int32_t lr)\n{\n    XYZ_32 pos;\n    const int32_t h = M_TestHeight(item, lr >= 0 ? 768 : -768, 0, &pos);\n    return item->pos.y - h <= 0;\n}\n\nstatic void M_KayakUserInput(ITEM *const item, ITEM *const l, M_PRIV *const p)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n\n    if (l->hit_points <= 0 && l->current_anim_state != M_STATE_DEATH_IN) {\n        Item_SwitchToObjAnim(l, 5, 0, O_LARA_VEHICLE_ANIM);\n        l->current_anim_state = M_STATE_DEATH_IN;\n        l->goal_anim_state = M_STATE_DEATH_IN;\n    }\n\n    const int16_t frame = Item_GetRelativeFrame(l);\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    switch (l->current_anim_state) {\n    case M_STATE_BACK:\n        if (!(g_Input.back)) {\n            l->goal_anim_state = M_STATE_POSE;\n        }\n\n        if (l->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx == 2) {\n            if (frame == 8) {\n                p->rot += 0x800000;\n                p->vel -= 0x180000;\n            }\n\n            if (frame == 31) {\n                p->rot -= 0x800000;\n                p->vel -= 0x180000;\n            }\n\n            if (frame < 15 && (frame & 1) != 0) {\n                M_DoRipple(item, 384, -128);\n            } else if (frame >= 20 && frame <= 34 && frame & 1) {\n                M_DoRipple(item, -384, -128);\n            }\n        }\n\n        break;\n\n    case M_STATE_POSE:\n        if (g_Input.roll && lara_info->current.active == 0\n            && lara_info->current.vel.x == 0 && lara_info->current.vel.z == 0) {\n            if (g_Input.left && M_CanGetOut(item, -1)) {\n                l->goal_anim_state = M_STATE_JUMP_OUT;\n                l->required_anim_state = M_STATE_CLIMB_OUT_L;\n            } else if (g_Input.right && M_CanGetOut(item, 1)) {\n                l->goal_anim_state = M_STATE_JUMP_OUT;\n                l->required_anim_state = M_STATE_CLIMB_OUT_R;\n            }\n        } else if (g_Input.forward) {\n            l->goal_anim_state = M_STATE_RIGHT;\n            p->turn = false;\n            p->forward = true;\n        } else if (g_Input.back) {\n            l->goal_anim_state = M_STATE_BACK;\n        } else if (g_Input.left) {\n            l->goal_anim_state = M_STATE_LEFT;\n\n            if (p->vel) {\n                p->turn = false;\n            } else {\n                p->turn = true;\n            }\n\n            p->forward = false;\n        } else if (g_Input.right) {\n            l->goal_anim_state = M_STATE_RIGHT;\n\n            if (p->vel) {\n                p->turn = false;\n            } else {\n                p->turn = true;\n            }\n\n            p->forward = false;\n        } else if (\n            g_Input.step_left\n            && (p->vel || lara_info->current.vel.x\n                || lara_info->current.vel.z)) {\n            l->goal_anim_state = M_STATE_TURN_L;\n        } else if (\n            g_Input.step_right\n            && (p->vel || lara_info->current.vel.x\n                || lara_info->current.vel.z)) {\n            l->goal_anim_state = M_STATE_TURN_R;\n        }\n\n        break;\n\n    case M_STATE_LEFT:\n        if (!p->forward) {\n            if (!g_Input.left) {\n                l->goal_anim_state = M_STATE_POSE;\n            }\n        } else {\n            if (frame == 0) {\n                p->paddle.frame2_latched = false;\n                p->paddle.stroke_count = 0;\n            }\n\n            if (frame == 2 && !p->paddle.frame2_latched) {\n                p->paddle.frame2_latched = true;\n                p->paddle.stroke_count++;\n            } else if (frame > 2) {\n                p->paddle.frame2_latched = false;\n            }\n\n            if (!g_Input.forward) {\n                l->goal_anim_state = M_STATE_POSE;\n            } else if (!g_Input.left) {\n                l->goal_anim_state = M_STATE_RIGHT;\n            } else if (p->paddle.stroke_count >= 2) {\n                l->goal_anim_state = M_STATE_RIGHT;\n            }\n        }\n\n        if (frame == 7) {\n            if (p->forward) {\n                p->rot -= 0x800000;\n                CLAMPL(p->rot, -0x1000000);\n                p->vel += 0x180000;\n            } else if (p->turn) {\n                p->rot -= 0x1000000;\n                CLAMPL(p->rot, -0x1000000);\n            } else {\n                p->rot -= 0xC00000;\n                CLAMPL(p->rot, -0xC00000);\n                p->vel += 0x100000;\n            }\n        }\n\n        if (frame > 6 && frame < 24 && frame & 1) {\n            M_DoRipple(item, -384, -64);\n        }\n        break;\n\n    case M_STATE_RIGHT:\n        if (!p->forward) {\n            if (!g_Input.right) {\n                l->goal_anim_state = M_STATE_POSE;\n            }\n        } else {\n            if (frame == 0) {\n                p->paddle.frame2_latched = false;\n                p->paddle.stroke_count = 0;\n            }\n\n            if (frame == 2 && !p->paddle.frame2_latched) {\n                p->paddle.frame2_latched = true;\n                p->paddle.stroke_count++;\n            } else if (frame > 2) {\n                p->paddle.frame2_latched = false;\n            }\n\n            if (!g_Input.forward) {\n                l->goal_anim_state = M_STATE_POSE;\n            } else if (!g_Input.right) {\n                l->goal_anim_state = M_STATE_LEFT;\n            } else if (p->paddle.stroke_count >= 2) {\n                l->goal_anim_state = M_STATE_LEFT;\n            }\n        }\n\n        if (frame == 7) {\n            if (p->forward) {\n                p->rot += 0x800000;\n                CLAMPG(p->rot, 0x1000000);\n                p->vel += 0x180000;\n            } else if (p->turn) {\n                p->rot += 0x1000000;\n                CLAMPG(p->rot, 0x1000000);\n            } else {\n                p->rot += 0xC00000;\n                CLAMPG(p->rot, 0xC00000);\n                p->vel += 0x100000;\n            }\n        }\n\n        if (frame > 6 && frame < 24 && frame & 1) {\n            M_DoRipple(item, 384, -64);\n        }\n        break;\n\n    case M_STATE_CLIMB_IN:\n        if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 4\n            && frame == 24 && !p->paddle.equipped) {\n            Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_OAR);\n            Item_SetMeshVisibleMask(l, m_KayakHiddenBodyMeshes, false);\n            p->paddle.equipped = true;\n        }\n        break;\n\n    case M_STATE_JUMP_OUT:\n        if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 14\n            && frame == 27 && p->paddle.equipped) {\n            Lara_Skin_ClearEquipment(LM_HAND_R);\n            Item_SetMeshVisibleMask(l, m_KayakHiddenBodyMeshes, true);\n            p->paddle.equipped = false;\n        }\n        l->goal_anim_state = l->required_anim_state;\n        break;\n\n    case M_STATE_TURN_L:\n        if (!g_Input.step_left\n            || (!p->vel && !lara_info->current.vel.x\n                && !lara_info->current.vel.z)) {\n            l->goal_anim_state = M_STATE_POSE;\n        } else if (\n            l->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx == 26) {\n            if (p->vel >= 0) {\n                p->rot -= 0x200000;\n                CLAMPL(p->rot, -0x1000000);\n                p->vel -= 0x8000;\n                CLAMPL(p->vel, 0);\n            }\n\n            if (p->vel < 0) {\n                p->vel += 0x8000;\n                p->rot += 0x200000;\n                CLAMPG(p->vel, 0);\n            }\n\n            if (!(time4 & 3)) {\n                M_DoRipple(item, -256, -256);\n            }\n        }\n        break;\n\n    case M_STATE_TURN_R:\n        if (!g_Input.step_right\n            || (!p->vel && !lara_info->current.vel.x\n                && !lara_info->current.vel.z)) {\n            l->goal_anim_state = M_STATE_POSE;\n        } else if (\n            l->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx == 27) {\n            if (p->vel >= 0) {\n                p->rot += 0x200000;\n                CLAMPG(p->rot, 0x1000000);\n                p->vel -= 0x8000;\n                CLAMPL(p->vel, 0);\n            }\n\n            if (p->vel < 0) {\n                p->vel += 0x8000;\n                p->rot -= 0x200000;\n                CLAMPG(p->vel, 0);\n            }\n\n            if (!(time4 & 3)) {\n                M_DoRipple(item, 256, -256);\n            }\n        }\n        break;\n\n    case M_STATE_CLIMB_OUT_L:\n        if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 24\n            && frame == 83) {\n            XYZ_32 pos = { .x = 0, .y = 350, .z = 500 };\n            Lara_GetMeshPos(LM_HIPS, &pos);\n            l->pos = pos;\n            l->rot.x = 0;\n            l->rot.y = item->rot.y - 0x4000;\n            l->rot.z = 0;\n            Item_SwitchToAnim(l, LA(LA_FREEFALL), 0);\n            l->current_anim_state = LS_FAST_FALL;\n            l->goal_anim_state = LS_FAST_FALL;\n            l->gravity = true;\n            l->fall_speed = 0;\n            lara_info->gun_status = LGS_ARMLESS;\n            Lara_Vehicle_SetIndex(NO_ITEM);\n        }\n        break;\n\n    case M_STATE_CLIMB_OUT_R:\n        if (l->anim_num == Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx + 32\n            && frame == 83) {\n            XYZ_32 pos = { .x = 0, .y = 350, .z = 500 };\n            Lara_GetMeshPos(LM_HIPS, &pos);\n            l->pos = pos;\n            l->rot.x = 0;\n            l->rot.y = item->rot.y + 0x4000;\n            l->rot.z = 0;\n            Item_SwitchToAnim(l, LA(LA_FREEFALL), 0);\n            l->current_anim_state = LS_FAST_FALL;\n            l->goal_anim_state = LS_FAST_FALL;\n            l->gravity = true;\n            l->fall_speed = 0;\n            lara_info->gun_status = LGS_ARMLESS;\n            Lara_Vehicle_SetIndex(NO_ITEM);\n        }\n        break;\n    }\n\n    if (p->vel > 0) {\n        p->vel -= 0x8000;\n        CLAMPL(p->vel, 0);\n    } else if (p->vel < 0) {\n        p->vel += 0x8000;\n        CLAMPG(p->vel, 0);\n    }\n\n    CLAMP(p->vel, -0x380000, 0x380000);\n    item->speed = p->vel >> 16;\n\n    if (p->rot < 0) {\n        p->rot += 0x50000;\n        CLAMPG(p->rot, 0);\n    } else {\n        p->rot -= 0x50000;\n        CLAMPL(p->rot, 0);\n    }\n}\n\nstatic void M_DoCurrent(ITEM *const item)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (lara_info->current.active != 0) {\n        const OBJECT_VECTOR *const sink =\n            Camera_GetFixedObject(lara_info->current.active - 1);\n        const int32_t speed = sink->data;\n        const int32_t angle =\n            -Math_Atan(lara_item->pos.x - sink->x, lara_item->pos.z - sink->z)\n            - DEG_90;\n        const int32_t scaled_speed = speed << (W2V_SHIFT - 4);\n        const XYZ_32 target_vel =\n            XYZ_32_OffsetYaw((XYZ_32) {}, angle, scaled_speed);\n        lara_info->current.vel.x +=\n            (target_vel.x - lara_info->current.vel.x) >> 4;\n        lara_info->current.vel.z +=\n            (target_vel.z - lara_info->current.vel.z) >> 4;\n    } else {\n        int32_t shifter;\n        int32_t abs_vel;\n\n        abs_vel = ABS(lara_info->current.vel.x);\n        if (abs_vel > 16) {\n            shifter = 4;\n        } else if (abs_vel > 8) {\n            shifter = 3;\n        } else {\n            shifter = 2;\n        }\n\n        lara_info->current.vel.x -= lara_info->current.vel.x >> shifter;\n        if (ABS(lara_info->current.vel.x) < 4) {\n            lara_info->current.vel.x = 0;\n        }\n\n        abs_vel = ABS(lara_info->current.vel.z);\n        if (abs_vel > 16) {\n            shifter = 4;\n        } else if (abs_vel > 8) {\n            shifter = 3;\n        } else {\n            shifter = 2;\n        }\n\n        lara_info->current.vel.z -= lara_info->current.vel.z >> shifter;\n        if (ABS(lara_info->current.vel.z) < 4) {\n            lara_info->current.vel.z = 0;\n        }\n\n        if (lara_info->current.vel.x == 0 && lara_info->current.vel.z == 0) {\n            return;\n        }\n    }\n\n    item->pos.x += lara_info->current.vel.x >> 8;\n    item->pos.z += lara_info->current.vel.z >> 8;\n    lara_info->current.active = 0;\n}\n\nstatic int32_t M_DoDynamics(\n    const int32_t h, int32_t fall_speed, int32_t *const y)\n{\n    if (h <= *y) {\n        int32_t kick = (h - *y) << 2;\n        CLAMPL(kick, -80);\n        fall_speed += (kick - fall_speed) >> 3;\n        CLAMPG(*y, h);\n    } else {\n        *y += fall_speed;\n\n        if (*y <= h) {\n            fall_speed += 6;\n        } else {\n            *y = h;\n            fall_speed = 0;\n        }\n    }\n\n    return fall_speed;\n}\n\nstatic int32_t M_DoShift(\n    ITEM *const item, const XYZ_32 *const new_pos, XYZ_32 *const old_pos)\n{\n    const int32_t new_sector_x = new_pos->x >> WALL_SHIFT;\n    const int32_t new_sector_z = new_pos->z >> WALL_SHIFT;\n    const int32_t old_sector_x = old_pos->x >> WALL_SHIFT;\n    const int32_t old_sector_z = old_pos->z >> WALL_SHIFT;\n    const int32_t sector_offset_x = new_pos->x - ROUND_TO_SECTOR(new_pos->x);\n    const int32_t sector_offset_z = new_pos->z - ROUND_TO_SECTOR(new_pos->z);\n    int32_t shift_x = 0;\n    int32_t shift_z = 0;\n\n    if (new_sector_x == old_sector_x) {\n        if (new_sector_z == old_sector_z) {\n            item->pos.z += old_pos->z - new_pos->z;\n            item->pos.x += old_pos->x - new_pos->x;\n            return 0;\n        } else if (new_sector_z <= old_sector_z) {\n            item->pos.z += WALL_L - sector_offset_z;\n            return item->pos.x - new_pos->x;\n        } else {\n            item->pos.z -= 1 + sector_offset_z;\n            return new_pos->x - item->pos.x;\n        }\n    }\n\n    if (new_sector_z == old_sector_z) {\n        if (new_sector_x <= old_sector_x) {\n            item->pos.x += WALL_L - sector_offset_x;\n            return new_pos->z - item->pos.z;\n        } else {\n            item->pos.x -= 1 + sector_offset_x;\n            return item->pos.z - new_pos->z;\n        }\n    }\n\n    int16_t room_num = item->room_num;\n    XYZ_32 sample_pos = { old_pos->x, new_pos->y, new_pos->z };\n    const SECTOR *floor = Room_GetSector(sample_pos, &room_num);\n    int32_t height = Room_GetHeight(floor, sample_pos);\n    if (height < old_pos->y - 256) {\n        if (new_pos->z > old_pos->z) {\n            shift_z = -1 - sector_offset_z;\n        } else {\n            shift_z = WALL_L - sector_offset_z;\n        }\n    }\n\n    room_num = item->room_num;\n    sample_pos = (XYZ_32) { new_pos->x, new_pos->y, old_pos->z };\n    floor = Room_GetSector(sample_pos, &room_num);\n    height = Room_GetHeight(floor, sample_pos);\n    if (height < old_pos->y - 256) {\n        if (new_pos->x > old_pos->x) {\n            shift_x = -1 - sector_offset_x;\n        } else {\n            shift_x = WALL_L - sector_offset_x;\n        }\n    }\n\n    if (shift_x != 0 && shift_z != 0) {\n        item->pos.x += shift_x;\n        item->pos.z += shift_z;\n        return 0;\n    }\n\n    if (shift_z != 0) {\n        item->pos.z += shift_z;\n        if (shift_z > 0) {\n            return item->pos.x - new_pos->x;\n        } else {\n            return new_pos->x - item->pos.x;\n        }\n    }\n\n    if (shift_x != 0) {\n        item->pos.x += shift_x;\n        if (shift_x > 0) {\n            return new_pos->z - item->pos.z;\n        } else {\n            return item->pos.z - new_pos->z;\n        }\n    }\n\n    item->pos.x += old_pos->x - new_pos->x;\n    item->pos.z += old_pos->z - new_pos->z;\n    return 0;\n}\n\nstatic int32_t M_GetCollisionAnim(const ITEM *const item, int32_t x, int32_t z)\n{\n    x = item->pos.x - x;\n    z = item->pos.z - z;\n    if (x == 0 && z == 0) {\n        return 0;\n    }\n\n    const int32_t s = Math_Sin(item->rot.y);\n    const int32_t c = Math_Cos(item->rot.y);\n    const int32_t front = (x * s + z * c) >> W2V_SHIFT;\n    const int32_t side = (x * c - z * s) >> W2V_SHIFT;\n    if (ABS(front) <= ABS(side)) {\n        if (side > 0) {\n            return 3;\n        } else {\n            return 4;\n        }\n    } else {\n        if (front > 0) {\n            return 1;\n        } else {\n            return 2;\n        }\n    }\n}\n\nstatic void M_KayakToBackground(ITEM *const item, M_PRIV *const p)\n{\n    int32_t heights[8];\n    XYZ_32 old_pos[9];\n    p->old_pos = item->pos;\n    heights[0] = M_TestHeight(item, 0, 1024, old_pos);\n    heights[1] = M_TestHeight(item, -96, 512, &old_pos[1]);\n    heights[2] = M_TestHeight(item, 96, 512, &old_pos[2]);\n    heights[3] = M_TestHeight(item, -128, 128, &old_pos[3]);\n    heights[4] = M_TestHeight(item, 128, 128, &old_pos[4]);\n    heights[5] = M_TestHeight(item, -128, -320, &old_pos[5]);\n    heights[6] = M_TestHeight(item, 128, -320, &old_pos[6]);\n    heights[7] = M_TestHeight(item, 0, -640, &old_pos[7]);\n\n    for (int32_t i = 0; i < 8; i++) {\n        CLAMPG(old_pos[i].y, heights[i]);\n    }\n\n    old_pos[8] = item->pos;\n\n    XYZ_32 front_pos;\n    XYZ_32 left_pos;\n    XYZ_32 right_pos;\n    const int32_t front = M_TestHeight(item, 0, 1024, &front_pos);\n    const int32_t left = M_TestHeight(item, -128, 128, &left_pos);\n    const int32_t right = M_TestHeight(item, 128, 128, &right_pos);\n\n    item->rot.y += p->rot >> 16;\n    const XYZ_32 moved_pos =\n        XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed);\n    item->pos.x = moved_pos.x;\n    item->pos.z = moved_pos.z;\n\n    M_DoCurrent(item);\n    p->fall_speed_l = M_DoDynamics(left, p->fall_speed_l, &left_pos.y);\n    p->fall_speed_r = M_DoDynamics(right, p->fall_speed_r, &right_pos.y);\n    p->fall_speed_f = M_DoDynamics(front, p->fall_speed_f, &front_pos.y);\n    item->fall_speed = M_DoDynamics(p->water, item->fall_speed, &item->pos.y);\n\n    const int32_t pitch_diff = (right_pos.y + left_pos.y) >> 1;\n    const int16_t x_rot = Math_Atan(1024, item->pos.y - front_pos.y);\n    const int16_t z_rot = Math_Atan(128, pitch_diff - left_pos.y);\n    item->rot.x = x_rot;\n    item->rot.z = z_rot;\n    const int32_t old_x = item->pos.x;\n    const int32_t old_z = item->pos.z;\n\n    XYZ_32 pos;\n    int32_t rot = 0;\n\n    int32_t h = M_TestHeight(item, 0, -640, &pos);\n    if (h < old_pos[7].y - 64) {\n        rot = M_DoShift(item, &pos, &old_pos[7]);\n    }\n\n    h = M_TestHeight(item, 128, -320, &pos);\n    if (h < old_pos[6].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[6]);\n    }\n\n    h = M_TestHeight(item, -128, -320, &pos);\n    if (h < old_pos[5].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[5]);\n    }\n\n    h = M_TestHeight(item, 128, 128, &pos);\n    if (h < old_pos[4].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[4]);\n    }\n\n    h = M_TestHeight(item, -128, 128, &pos);\n    if (h < old_pos[3].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[3]);\n    }\n\n    h = M_TestHeight(item, 96, 512, &pos);\n    if (h < old_pos[2].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[2]);\n    }\n\n    h = M_TestHeight(item, -96, 512, &pos);\n    if (h < old_pos[1].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[1]);\n    }\n\n    h = M_TestHeight(item, 0, 1024, &pos);\n    if (h < old_pos[0].y - 64) {\n        rot += M_DoShift(item, &pos, &old_pos[0]);\n    }\n\n    item->rot.y += rot;\n\n    int16_t room_num = item->room_num;\n    const SECTOR *floor = Room_GetSector(item->pos, &room_num);\n    h = Room_GetWaterHeight(item->pos, room_num);\n    if (h == NO_HEIGHT) {\n        h = Room_GetHeight(floor, item->pos);\n    }\n    if (h < item->pos.y - 64) {\n        h = M_DoShift(item, (XYZ_32 *)&item->pos, &old_pos[8]);\n    }\n\n    room_num = item->room_num;\n    floor = Room_GetSector(item->pos, &room_num);\n    h = Room_GetWaterHeight(item->pos, room_num);\n    if (h == NO_HEIGHT) {\n        h = Room_GetHeight(floor, item->pos);\n        if (h == NO_HEIGHT) {\n            GAME_VECTOR reset_pos = {\n                .pos = p->old_pos,\n                .room_num = item->room_num,\n            };\n            Camera_Collide(&reset_pos, 256, 0);\n            item->pos = reset_pos.pos;\n            item->room_num = reset_pos.room_num;\n        }\n    }\n\n    if (M_GetCollisionAnim(item, old_x, old_z)) {\n        const int32_t sin_y = Math_Sin(item->rot.y);\n        const int32_t cos_y = Math_Cos(item->rot.y);\n        const int32_t dx = item->pos.x - old_pos[8].x;\n        const int32_t dz = item->pos.z - old_pos[8].z;\n        int32_t speed = (dx * sin_y + dz * cos_y) >> W2V_SHIFT;\n        speed <<= 8;\n\n        if ((p->vel > 0 && speed < p->vel) || (p->vel < 0 && speed > p->vel)) {\n            p->vel = speed;\n        }\n\n        CLAMPL(p->vel, -0x380000);\n    }\n}\n\nstatic void M_KayakSplash(\n    const ITEM *const item, const int32_t fall_speed, const int32_t water)\n{\n    if (water == NO_HEIGHT) {\n        return;\n    }\n    FX_WATER_SPLASH_SETUP splash_setup = {\n        .x = item->pos.x,\n        .y = item->pos.y,\n        .z = item->pos.z,\n        .inner_xz_off = 128,\n        .inner_xz_size = 48,\n        .inner_y_size = -384,\n        .inner_xz_vel = 160,\n        .inner_y_vel = (-fall_speed << 5),\n        .inner_gravity = 128,\n        .inner_friction = 7,\n        .middle_xz_off = 192,\n        .middle_xz_size = 96,\n        .middle_y_size = -256,\n        .middle_xz_vel = 224,\n        .middle_y_vel = (-fall_speed << 4),\n        .middle_gravity = 72,\n        .middle_friction = 8,\n        .outer_xz_off = 256,\n        .outer_xz_size = 128,\n        .outer_xz_vel = 272,\n        .outer_friction = -9,\n    };\n    FX_Water_SetupSplash(&splash_setup);\n}\n\nstatic void M_DoWake(\n    const ITEM *const item, const int32_t xoff, const int32_t zoff,\n    const int16_t rotate)\n{\n    int16_t angle1;\n    int16_t angle2;\n    XYZ_32 pos;\n\n    const int32_t start_idx = FX_Wake_GetStartIndex();\n    if (FX_Wake_GetPoint(start_idx, rotate)->life != 0) {\n        return;\n    }\n\n    pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, zoff);\n    pos = XYZ_32_OffsetYaw(pos, item->rot.y + DEG_90, xoff);\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n    const int32_t wh = Room_GetWaterHeight(pos, room_num);\n\n    if (wh == NO_HEIGHT) {\n        return;\n    }\n\n    if (item->speed >= 0) {\n        if (rotate) {\n            angle1 = item->rot.y + 30940;\n            angle2 = item->rot.y + 27300;\n        } else {\n            angle1 = item->rot.y - 30940;\n            angle2 = item->rot.y - 27300;\n        }\n    } else {\n        if (rotate) {\n            angle1 = item->rot.y + 1820;\n            angle2 = item->rot.y + 5460;\n        } else {\n            angle1 = item->rot.y - 1820;\n            angle2 = item->rot.y - 5460;\n        }\n    }\n\n    XYZ_32 vel[2] = {\n        XYZ_32_OffsetYaw((XYZ_32) {}, angle1, 4),\n        XYZ_32_OffsetYaw((XYZ_32) {}, angle2, 6),\n    };\n    FX_WAKE_POINT *const pt = FX_Wake_GetPoint(start_idx, rotate);\n    pt->life = 64;\n\n    for (int32_t i = 0; i < 2; i++) {\n        pt->pos[i].x = pos.x;\n        pt->pos[i].y = item->pos.y + 32;\n        pt->pos[i].z = pos.z;\n        pt->prev_pos[i] = pt->pos[i];\n        pt->vel[i].x = vel[i].x;\n        pt->vel[i].z = vel[i].z;\n    }\n\n    if (rotate == 1) {\n        FX_Wake_AdvanceStartIndex();\n    }\n}\n\nstatic void M_TriggerRapidsMist(const XYZ_32 pos)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 128;\n    spark->src_color.g = 128;\n    spark->src_color.b = 128;\n    spark->dst_color.r = 192;\n    spark->dst_color.g = 192;\n    spark->dst_color.b = 192;\n\n    spark->col_fade_speed = 2;\n    spark->fade_to_black = 4;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 6;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 3;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 4;\n    spark->sprite_idx = (uint8_t)Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->gravity = 0;\n    spark->max_y_vel = 0;\n    spark->dst_size.width = (Random_GetControl() & 7) + 16;\n    spark->src_size.width = spark->dst_size.width >> 1;\n    spark->size.width = spark->src_size.width;\n    spark->src_size.height = spark->src_size.width;\n    spark->size.height = spark->src_size.width;\n    spark->dst_size.height = spark->dst_size.width;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_KayakToBaddieCollision(const ITEM *const p)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    int16_t nearby_rooms[20] = { p->room_num };\n    int16_t nearby_room_count = 1;\n\n    const PORTALS *const portals = Room_Get(p->room_num)->portals;\n    if (portals != nullptr) {\n        for (int32_t i = 0; i < portals->count && nearby_room_count < 20; i++) {\n            nearby_rooms[nearby_room_count] = portals->portal[i].room_num;\n            nearby_room_count++;\n        }\n    }\n\n    for (int32_t i = 0; i < nearby_room_count; i++) {\n        const ITEM *item;\n        for (int16_t item_num = Room_Get(nearby_rooms[i])->item_num;\n             item_num != NO_ITEM; item_num = item->next_item) {\n            item = Item_Get(item_num);\n\n            if (!item->collidable || item->status == IS_INVISIBLE) {\n                continue;\n            }\n\n            const OBJECT_ID obj_num = item->object_id;\n            const OBJECT *const obj = Object_Get(obj_num);\n            if (obj->collision_func == nullptr) {\n                continue;\n            }\n\n            const bool is_hazard = obj_num == O_SPIKES || obj_num == O_DART\n                || obj_num == O_TEETH_TRAP\n                || (obj_num == O_BLADE && item->current_anim_state != 1)\n                || (obj_num == O_ICICLE && item->current_anim_state != 3);\n            if (!is_hazard) {\n                continue;\n            }\n\n            const int32_t dx = p->pos.x - item->pos.x;\n            const int32_t dy = p->pos.y - item->pos.y;\n            const int32_t dz = p->pos.z - item->pos.z;\n            if (dx <= -2048 || dx >= 2048 || dz <= -2048 || dz >= 2048\n                || dy <= -2048 || dy >= 2048) {\n                continue;\n            }\n\n            if (!Item_TestBoundsCollide(item, p, 256)) {\n                continue;\n            }\n\n            Spawn_BloodBath(\n                lara_item->pos.x, lara_item->pos.y - 256, lara_item->pos.z,\n                p->speed, p->rot.y, lara_item->room_num, 3);\n            Lara_TakeDamage(5, false);\n        }\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    ((ITEM *)item)->pos.y += 32;\n    Object_DrawAnimatingItem(item);\n    ((ITEM *)item)->pos.y -= 32;\n    FX_Wake_Draw(item);\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->collision_func = M_Collision;\n    obj->draw_func = M_Draw;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nbool Kayak_Control(void)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n    ITEM *const item = Lara_Vehicle_GetItem();\n    M_PRIV *const p = item->priv;\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n\n    const LARA_SKIN_EQUIPMENT *const hand_r_equipment =\n        Lara_Skin_GetEquipment(LM_HAND_R);\n    if (p->paddle.equipped) {\n        Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_OAR);\n    } else if (\n        hand_r_equipment->type == EQUIPMENT_TYPE_EXTRA\n        && hand_r_equipment->data == EXTRA_MESH_OAR) {\n        Lara_Skin_ClearEquipment(LM_HAND_R);\n    }\n\n    if (g_Input.look) {\n        Lara_Look_UpDown();\n    }\n\n    const int32_t old_fall_speed = item->fall_speed;\n    M_KayakUserInput(item, lara_item, p);\n    M_KayakToBackground(item, p);\n    int16_t room_num = item->room_num;\n    const SECTOR *floor = Room_GetSector(item->pos, &room_num);\n    int32_t h = Room_GetHeight(floor, item->pos);\n    int32_t wh = Room_GetWaterHeight(item->pos, room_num);\n    p->water = wh;\n\n    if (wh == NO_HEIGHT) {\n        wh = h;\n        p->water = h;\n        p->true_water = false;\n    } else {\n        p->true_water = true;\n        p->water = wh - 5;\n    }\n\n    const int32_t damage = old_fall_speed - item->fall_speed;\n\n    if (damage > 128 && !item->fall_speed && wh != NO_HEIGHT) {\n        if (damage > 160) {\n            Lara_TakeDamage((damage - 160) << 3, false);\n        }\n\n        M_KayakSplash(item, old_fall_speed - item->fall_speed, wh);\n    }\n\n    if (Lara_Vehicle_GetIndex() != NO_ITEM) {\n        lara_item->pos.x = item->pos.x;\n        lara_item->pos.y = item->pos.y + 32;\n        lara_item->pos.z = item->pos.z;\n        lara_item->rot.x = item->rot.x;\n        lara_item->rot.y = item->rot.y;\n        lara_item->rot.z = item->rot.z >> 1;\n        Item_Animate(lara_item);\n        item->anim_num = lara_item->anim_num + Object_Get(O_KAYAK)->anim_idx\n            - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx;\n        item->frame_num = lara_item->frame_num + Item_GetAnim(item)->frame_base\n            - Item_GetAnim(lara_item)->frame_base;\n        Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num);\n        Item_UpdateRoom(lara_info->item_num, room_num);\n        Room_TestTriggers(lara_item);\n        Room_TestTriggers(item);\n        g_Camera.target_elevation = -5460;\n        g_Camera.target_distance = 2048;\n    }\n\n    if (!(time4 & 0xF) && p->true_water) {\n        M_DoWake(item, -128, 0, 0);\n        M_DoWake(item, 128, 0, 1);\n    }\n\n    if (time4 & 7 && !p->true_water && item->fall_speed < 20) {\n        p->counter ^= 1;\n\n        for (int32_t i = p->counter; i < 10; i += 2) {\n            int32_t x;\n            if (Random_GetControl() & 1) {\n                x = m_MistPos[i].x >> 1;\n            } else {\n                x = -(m_MistPos[i].x >> 1);\n            }\n\n            const int32_t y = 50;\n            const int32_t z = m_MistPos[i].z;\n\n            Matrix_PushUnit();\n            Matrix_Rot16(item->rot);\n\n            // NOTE: not part of the OG PC\n            const XYZ_32 pos =\n                Matrix_MulVec32_M(g_MatrixPtr, (XYZ_32) { x, y, z });\n            M_TriggerRapidsMist((XYZ_32) {\n                .x = item->pos.x + pos.x,\n                .y = item->pos.y + pos.y,\n                .z = item->pos.z + pos.z,\n            });\n\n            Matrix_Pop();\n        }\n    }\n\n    uint8_t wake_shade = FX_Wake_GetShade();\n    if (item->speed != 0 || lara_info->current.vel.x != 0\n        || lara_info->current.vel.z != 0) {\n        if (wake_shade < 16) {\n            wake_shade++;\n        }\n    } else if (wake_shade) {\n        wake_shade--;\n    }\n    FX_Wake_SetShade(wake_shade);\n\n    M_KayakToBaddieCollision(item);\n\n    return Lara_Vehicle_GetIndex() != NO_ITEM;\n}\n\nREGISTER_OBJECT(O_KAYAK, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/kayak.h",
    "content": "#pragma once\n\nbool Kayak_Control(void);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/mine_cart.c",
    "content": "#include <trx/game/objects/vehicles/mine_cart.h>\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/camera.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n\n// clang-format off\n#define M_MOUNT_DIST          200000\n#define M_DISMOUNT_DIST       330\n#define M_MAX_COLL_ROOMS      12\n#define M_MIN_BEAM_DAMAGE     20\n#define M_MESH_FRAME          20\n#define M_RADIUS              STEP_L\n#define M_ANGLE_CLAMP         (DEG_90 - 1)     // = 16383\n#define M_MIN_VELOCITY        (STEP_L / 8)     // = 32\n#define M_MAX_VELOCITY        (STEP_L * 63)    // = 16128\n#define M_MIN_SPEED           (STEP_L * 10)    // = 2560\n#define M_BRAKE_SPEED         (STEP_L * 6)     // = 1536\n#define M_STOP_SPEED          (WALL_L * 60)    // = 61440\n#define M_GRAVITY             (WALL_L + 1)     // = 1025\n#define M_TARGET_DIST         (WALL_L * 2)     // = 2048\n#define M_MAX_GRADIENT        (STEP_L / 2)     // = 128\n#define M_MAX_ROLL            (DEG_90 / 4)     // = 4096\n#define M_TURN_SHIFT          (DEG_90 / 4)     // = 4096\n#define M_TURN_DIST           (STEP_L * 14)    // = 3584\n#define M_JUMP_DIST           (STEP_L * 9 / 4) // = 576\n#define M_JUMP_VELOCITY       (-WALL_L)        // = -1024\n#define M_CAM_RIDE_ELEVATION  (-DEG_45)        // = -8190\n#define M_CAM_RIDE_DISTANCE   (WALL_L * 2)     // = 2048\n#define M_CAM_CRASH_ELEVATION (-DEG_1 * 25)    // = -4550\n#define M_CAM_CRASH_DISTANCE  (WALL_L * 4)     // = 4096\n// clang-format on\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_MOUNT_LEFT       = 0,\n    M_ANIM_PREPARE_RIDE     = 5,\n    M_ANIM_SWIPE            = 6,\n    M_ANIM_PREPARE_DISMOUNT = 7,\n    M_ANIM_CRASH            = 23,\n    M_ANIM_TOPPLED          = 30,\n    M_ANIM_TOPPLE_START     = 31,\n    M_ANIM_HIT_BEAM         = 34,\n    M_ANIM_MOUNT_RIGHT      = 46,\n    // clang-format on\n} M_ANIM;\n\ntypedef enum {\n    M_STATE_MOUNT,\n    M_STATE_STOP,\n    M_STATE_DISMOUNT_LEFT,\n    M_STATE_DISMOUNT_RIGHT,\n    M_STATE_IDLE,\n    M_STATE_DUCK,\n    M_STATE_RIDE,\n    M_STATE_RIGHT,\n    M_STATE_HUG_RIGHT,\n    M_STATE_LEFT,\n    M_STATE_HUG_LEFT,\n    M_STATE_BRAKE,\n    M_STATE_TILT_FORWARD,\n    M_STATE_TILT_BACK,\n    M_STATE_DEATH,\n    M_STATE_CRASH_START,\n    M_STATE_CRASH,\n    M_STATE_HIT_BEAM,\n    M_STATE_SWIPE,\n    M_STATE_BRAKING,\n} M_STATE;\n\ntypedef enum {\n    M_SIDE_NONE,\n    M_SIDE_LEFT,\n    M_SIDE_RIGHT,\n} M_SIDE;\n\ntypedef struct {\n    int32_t speed;\n    int32_t mid_pos;\n    int32_t front_pos;\n    int16_t y_velocity;\n    int16_t gradient;\n    uint8_t stop_delay;\n    M_SIDE dismount_side;\n    struct {\n        XZ_32 pos;\n        int16_t angle;\n        int16_t length;\n        M_SIDE side;\n    } turn;\n    struct {\n        bool control;\n        bool dead;\n        bool stopped;\n        bool suppress_anim;\n    } flags;\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"speed\", &p->speed));\n    JSON_SHOULD(JSON_READ(io, \"mid_pos\", &p->mid_pos));\n    JSON_SHOULD(JSON_READ(io, \"front_pos\", &p->front_pos));\n    JSON_SHOULD(JSON_READ(io, \"y_velocity\", &p->y_velocity));\n    JSON_SHOULD(JSON_READ(io, \"gradient\", &p->gradient));\n    JSON_SHOULD(JSON_READ(io, \"stop_delay\", &p->stop_delay));\n    if (JSON_SHOULD(JSON_PUSH(io, \"turn\"))) {\n        JSON_SHOULD(JSON_READ(io, \"pos.x\", &p->turn.pos.x));\n        JSON_SHOULD(JSON_READ(io, \"pos.z\", &p->turn.pos.z));\n        JSON_SHOULD(JSON_READ(io, \"angle\", &p->turn.angle));\n        JSON_SHOULD(JSON_READ(io, \"length\", &p->turn.length));\n        JSON_SHOULD(JSON_READ(io, \"side\", &p->turn.side));\n        JSON_POP(io);\n    }\n    if (JSON_SHOULD(JSON_PUSH(io, \"flags\"))) {\n        JSON_SHOULD(JSON_READ(io, \"control\", &p->flags.control));\n        JSON_SHOULD(JSON_READ(io, \"dead\", &p->flags.dead));\n        JSON_SHOULD(JSON_READ(io, \"stopped\", &p->flags.stopped));\n        JSON_SHOULD(JSON_READ(io, \"suppress_anim\", &p->flags.suppress_anim));\n        JSON_POP(io);\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"speed\", p->speed);\n    JSONW_WRITE(io, \"mid_pos\", p->mid_pos);\n    JSONW_WRITE(io, \"front_pos\", p->front_pos);\n    JSONW_WRITE(io, \"y_velocity\", p->y_velocity);\n    JSONW_WRITE(io, \"gradient\", p->gradient);\n    JSONW_WRITE(io, \"stop_delay\", p->stop_delay);\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"pos.x\", p->turn.pos.x);\n    JSONW_WRITE(io, \"pos.z\", p->turn.pos.z);\n    JSONW_WRITE(io, \"angle\", p->turn.angle);\n    JSONW_WRITE(io, \"length\", p->turn.length);\n    JSONW_WRITE(io, \"side\", p->turn.side);\n    JSONW_POP_AND_SET(io, \"turn\");\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"control\", p->flags.control);\n    JSONW_WRITE(io, \"dead\", p->flags.dead);\n    JSONW_WRITE(io, \"stopped\", p->flags.stopped);\n    JSONW_WRITE(io, \"suppress_anim\", p->flags.suppress_anim);\n    JSONW_POP_AND_SET(io, \"flags\");\n}\n\nstatic M_SIDE M_CheckMount(ITEM *const item, COLL_INFO *const coll)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || lara_item->gravity) {\n        return M_SIDE_NONE;\n    }\n\n    if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) {\n        return M_SIDE_NONE;\n    }\n\n    if (!Collide_TestCollision(item, lara_item)) {\n        return M_SIDE_NONE;\n    }\n\n    const XYZ_32 delta = {\n        .x = lara_item->pos.x - item->pos.x,\n        .z = lara_item->pos.z - item->pos.z,\n        .y = 0,\n    };\n    if (XYZ_32_GetLength2(delta) > M_MOUNT_DIST) {\n        return M_SIDE_NONE;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, item->pos);\n    if (height < -MAX_HEIGHT) {\n        return M_SIDE_NONE;\n    }\n\n    const int16_t angle =\n        (int16_t)Math_Atan(\n            lara_item->pos.z - item->pos.z, lara_item->pos.x - item->pos.x)\n        - item->rot.y;\n    return angle > -0x1FFE && angle < 0x5FFA ? M_SIDE_RIGHT : M_SIDE_LEFT;\n}\n\nstatic bool M_CheckDismount(const M_SIDE side)\n{\n    const ITEM *const item = Lara_Vehicle_GetItem();\n\n    const int16_t rot = item->rot.y + (side == M_SIDE_LEFT ? DEG_90 : -DEG_90);\n    const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, rot, M_DISMOUNT_DIST);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    const HEIGHT_TYPE height_type = Room_GetHeightType();\n\n    if (height_type == HT_BIG_SLOPE || height_type == HT_DIAGONAL\n        || height == NO_HEIGHT || ABS(height) <= WALL_L / 2) {\n        return false;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (ceiling - item->pos.y > -LARA_HEIGHT) {\n        return false;\n    }\n    if (height - ceiling < LARA_HEIGHT) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) {\n        return;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    const M_SIDE mount_side = M_CheckMount(item, coll);\n    if (mount_side == M_SIDE_NONE) {\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    Lara_Vehicle_SetIndex(item_num);\n    if (lara->gun_type == LGT_FLARE) {\n        Lara_Flare_Dispose(false);\n        lara->gun_type = LGT_UNARMED;\n        lara->request_gun_type = LGT_UNARMED;\n    }\n\n    const M_ANIM anim_idx =\n        mount_side == M_SIDE_LEFT ? M_ANIM_MOUNT_LEFT : M_ANIM_MOUNT_RIGHT;\n    Item_SwitchToObjAnim(lara_item, anim_idx, 0, O_LARA_VEHICLE_ANIM);\n    lara_item->current_anim_state = M_STATE_MOUNT;\n    lara_item->goal_anim_state = M_STATE_MOUNT;\n    lara_item->pos = item->pos;\n    lara_item->rot = item->rot;\n    lara->gun_status = LGS_HANDS_BUSY;\n    lara->hit_direction = DIR_UNKNOWN;\n\n    M_PRIV *const p = item->priv;\n    p->speed = 0;\n    p->y_velocity = 0;\n    p->gradient = 0;\n    p->turn.side = M_SIDE_NONE;\n    p->dismount_side = M_SIDE_NONE;\n    p->flags.control = false;\n    p->flags.dead = false;\n    p->flags.stopped = false;\n    p->flags.suppress_anim = false;\n\n    Music_Play(MX_MINE_CART_THEME, MPM_ONCE);\n}\n\nstatic void M_CheckStrikeSwitch(ITEM *const item)\n{\n    if (item->status == IS_ACTIVE) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item->current_anim_state != M_STATE_SWIPE\n        || !Item_TestObjAnimEqual(lara_item, M_ANIM_SWIPE, O_LARA_VEHICLE_ANIM)\n        || !Item_TestFrameRange(lara_item, 12, 22)) {\n        return;\n    }\n\n    Sound_Effect(SFX_SPANNER_CLUNK, &item->pos, SPM_ALWAYS);\n    Room_TestTriggers(item);\n    Item_AddActive(Item_GetIndex(item));\n    item->flags = IF_CODE_BITS;\n    item->status = IS_ACTIVE;\n}\n\nstatic void M_CheckObjectCollision(ITEM *const item, ITEM *const cart)\n{\n    if (!item->collidable || item->status == IS_INVISIBLE\n        || item == Lara_GetItem() || item == cart) {\n        return;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const bool is_flip_switch = item->object_id == O_ANIMATING_2;\n    if (obj->collision_func == nullptr\n        || (!obj->intelligent && !is_flip_switch)) {\n        return;\n    }\n\n    if (!Item_IsNearby(item, cart, M_TARGET_DIST)) {\n        return;\n    }\n\n    if (!Item_TestBoundsCollide(item, cart, M_RADIUS)) {\n        return;\n    }\n\n    if (is_flip_switch) {\n        M_CheckStrikeSwitch(item);\n        return;\n    }\n\n    if (Item_ShouldSpawnBlood(item)) {\n        Spawn_BloodBath(\n            item->pos.x, cart->pos.y - STEP_L, item->pos.z, cart->speed,\n            cart->rot.y, item->room_num, 3);\n    }\n    if (item->hit_points > 0) {\n        item->hit_points = 0;\n        if (item->include_in_kill_stats) {\n            Stats_AddKill();\n        }\n    }\n}\n\nstatic void M_ObjectCollision(ITEM *const cart)\n{\n    int16_t roomies[M_MAX_COLL_ROOMS];\n    const int32_t roomies_count =\n        Room_GetAdjoiningRooms(cart->room_num, roomies, M_MAX_COLL_ROOMS);\n\n    for (int32_t i = 0; i < roomies_count; i++) {\n        const ROOM *const room = Room_Get(roomies[i]);\n        int16_t item_num = room->item_num;\n        while (item_num != NO_ITEM) {\n            ITEM *const item = Item_Get(item_num);\n            const int16_t next_item_num = item->next_item;\n            M_CheckObjectCollision(item, cart);\n            item_num = next_item_num;\n        }\n    }\n}\n\nstatic int32_t M_GetCollision(\n    ITEM *const item, const int16_t angle, const int32_t distance,\n    int32_t *const ceiling)\n{\n    XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, distance);\n    pos.y -= LARA_HEIGHT;\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    *ceiling = Room_GetCeiling(sector, pos);\n\n    return height == NO_HEIGHT ? NO_HEIGHT : (height - item->pos.y);\n}\n\nstatic int32_t M_GetHeight(ITEM *const item, const int32_t x, const int32_t z)\n{\n    const int32_t s = Math_Sin(item->rot.y);\n    const int32_t c = Math_Cos(item->rot.y);\n    const XYZ_32 pos = {\n        .x = item->pos.x + ((x * c + z * s) >> W2V_SHIFT),\n        .y = (item->pos.y + ((x * Math_Sin(item->rot.z)) >> W2V_SHIFT))\n            - ((z * Math_Sin(item->rot.x)) >> W2V_SHIFT),\n        .z = item->pos.z + ((z * c - x * s) >> W2V_SHIFT),\n    };\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    return Room_GetHeight(sector, pos);\n}\n\nstatic void M_UserControl(ITEM *const item)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    M_PRIV *const p = item->priv;\n\n    switch (lara_item->current_anim_state) {\n    case M_STATE_MOUNT:\n        if (Item_TestObjAnimEqual(\n                lara_item, M_ANIM_PREPARE_RIDE, O_LARA_VEHICLE_ANIM)\n            && Item_TestFrameEqual(lara_item, M_MESH_FRAME)) {\n            Lara_Skin_SetExtraEquipment(LM_HAND_R, EXTRA_MESH_SPANNER);\n        }\n        break;\n\n    case M_STATE_STOP:\n        if (Item_TestObjAnimEqual(\n                lara_item, M_ANIM_PREPARE_DISMOUNT, O_LARA_VEHICLE_ANIM)) {\n            if (Item_TestFrameEqual(lara_item, M_MESH_FRAME)) {\n                Lara_Skin_ClearEquipment(LM_HAND_R);\n            }\n\n            if (p->dismount_side == M_SIDE_RIGHT) {\n                lara_item->goal_anim_state = M_STATE_DISMOUNT_RIGHT;\n            } else {\n                lara_item->goal_anim_state = M_STATE_DISMOUNT_LEFT;\n            }\n        }\n        break;\n\n    case M_STATE_DISMOUNT_LEFT:\n    case M_STATE_DISMOUNT_RIGHT:\n        if (Item_TestFrameEqual(lara_item, -1)) {\n            XYZ_32 pos = {\n                .x = 0,\n                .y = 640,\n                .z = 0,\n            };\n            Lara_GetMeshPos(LM_HIPS, &pos);\n            lara_item->pos = pos;\n            lara_item->rot.y = item->rot.y;\n            if (lara_item->current_anim_state == M_STATE_DISMOUNT_LEFT) {\n                lara_item->rot.y += DEG_90;\n            } else {\n                lara_item->rot.y -= DEG_90;\n            }\n\n            Lara_Vehicle_Dismount();\n            lara->gun_status = LGS_ARMLESS;\n        }\n        break;\n\n    case M_STATE_IDLE:\n        if (!p->flags.control) {\n            Sound_Effect(SFX_MINE_CART_CLUNK_START, &item->pos, SPM_ALWAYS);\n            p->stop_delay = 64;\n            p->flags.control = true;\n        }\n\n        if (g_Input.roll && p->flags.stopped) {\n            if (g_Input.left && M_CheckDismount(M_SIDE_LEFT)) {\n                lara_item->goal_anim_state = M_STATE_STOP;\n                p->dismount_side = M_SIDE_LEFT;\n            } else if (g_Input.right && M_CheckDismount(M_SIDE_RIGHT)) {\n                lara_item->goal_anim_state = M_STATE_STOP;\n                p->dismount_side = M_SIDE_RIGHT;\n            }\n        }\n        if (g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_DUCK;\n        } else if (p->speed > M_MIN_VELOCITY) {\n            lara_item->goal_anim_state = M_STATE_RIDE;\n        }\n\n        break;\n\n    case M_STATE_DUCK:\n        if (g_Input.action) {\n            lara_item->goal_anim_state = M_STATE_SWIPE;\n        } else if (g_Input.jump) {\n            lara_item->goal_anim_state = M_STATE_BRAKE;\n        } else if (!g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_IDLE;\n        }\n        break;\n\n    case M_STATE_RIDE:\n        if (g_Input.action) {\n            lara_item->goal_anim_state = M_STATE_SWIPE;\n        } else if (g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_DUCK;\n        } else if (g_Input.jump) {\n            lara_item->goal_anim_state = M_STATE_BRAKE;\n        } else if (p->speed == M_MIN_VELOCITY || p->flags.stopped) {\n            lara_item->goal_anim_state = M_STATE_IDLE;\n        } else if (p->gradient < -M_MAX_GRADIENT) {\n            lara_item->goal_anim_state = M_STATE_TILT_FORWARD;\n        } else if (p->gradient > M_MAX_GRADIENT) {\n            lara_item->goal_anim_state = M_STATE_TILT_BACK;\n        } else if (g_Input.left) {\n            lara_item->goal_anim_state = M_STATE_LEFT;\n        } else if (g_Input.right) {\n            lara_item->goal_anim_state = M_STATE_RIGHT;\n        }\n        break;\n\n    case M_STATE_RIGHT:\n        if (!g_Input.right) {\n            lara_item->goal_anim_state = M_STATE_RIDE;\n        } else if (g_Input.action) {\n            lara_item->goal_anim_state = M_STATE_SWIPE;\n        } else if (g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_DUCK;\n        } else if (g_Input.jump) {\n            lara_item->goal_anim_state = M_STATE_BRAKE;\n        }\n        break;\n\n    case M_STATE_LEFT:\n        if (!g_Input.left) {\n            lara_item->goal_anim_state = M_STATE_RIDE;\n        } else if (g_Input.action) {\n            lara_item->goal_anim_state = M_STATE_SWIPE;\n        } else if (g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_DUCK;\n        } else if (g_Input.jump) {\n            lara_item->goal_anim_state = M_STATE_BRAKE;\n        }\n        break;\n\n    case M_STATE_BRAKE:\n        lara_item->goal_anim_state = M_STATE_BRAKING;\n        break;\n\n    case M_STATE_TILT_FORWARD:\n    case M_STATE_TILT_BACK:\n        if (g_Input.action) {\n            lara_item->goal_anim_state = M_STATE_SWIPE;\n        } else if (g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_DUCK;\n        } else if (g_Input.jump) {\n            lara_item->goal_anim_state = M_STATE_BRAKE;\n        } else {\n            const bool forward =\n                lara_item->current_anim_state == M_STATE_TILT_FORWARD;\n            if ((forward && p->gradient > -M_MAX_GRADIENT)\n                || (!forward && p->gradient < M_MAX_GRADIENT)) {\n                lara_item->goal_anim_state = M_STATE_RIDE;\n            }\n        }\n        break;\n\n    case M_STATE_DEATH: {\n        g_Camera.target_elevation = M_CAM_RIDE_ELEVATION;\n        g_Camera.target_distance = M_CAM_RIDE_DISTANCE;\n        int32_t ceiling = 0;\n        const int32_t height =\n            M_GetCollision(item, item->rot.y, STEP_L * 2, &ceiling);\n\n        if (height > -STEP_L && height < STEP_L) {\n            const int32_t time4 = Output_GetTimeInGame() * 4;\n            if ((time4 & 7) == 0) {\n                Sound_Effect(SFX_QUAD_FRONT_IMPACT, &item->pos, SPM_ALWAYS);\n            }\n            item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, STEP_L / 2);\n        } else if (\n            Item_TestObjAnimEqual(\n                lara_item, M_ANIM_TOPPLED, O_LARA_VEHICLE_ANIM)) {\n            p->flags.suppress_anim = true;\n            lara_item->hit_points = -1;\n        }\n        break;\n    }\n\n    case M_STATE_CRASH:\n        g_Camera.target_elevation = M_CAM_CRASH_ELEVATION;\n        g_Camera.target_distance = M_CAM_CRASH_DISTANCE;\n        break;\n\n    case M_STATE_HIT_BEAM:\n        if (lara_item->hit_points <= 0 && Item_TestFrameEqual(lara_item, 28)) {\n            p->flags.control = false;\n            p->flags.suppress_anim = true;\n            p->speed = 0;\n            item->speed = 0;\n        }\n        break;\n\n    case M_STATE_SWIPE:\n        lara_item->goal_anim_state = M_STATE_RIDE;\n        break;\n\n    case M_STATE_BRAKING:\n        if (g_Input.crouch) {\n            lara_item->goal_anim_state = M_STATE_DUCK;\n            Sound_StopEffect(SFX_MINE_CART_SREECH_BRAKE);\n        } else if (!g_Input.jump || p->flags.stopped) {\n            lara_item->goal_anim_state = M_STATE_RIDE;\n            Sound_StopEffect(SFX_MINE_CART_SREECH_BRAKE);\n        } else {\n            p->speed -= M_BRAKE_SPEED;\n            Sound_Effect(\n                SFX_MINE_CART_SREECH_BRAKE, &lara_item->pos, SPM_ALWAYS);\n        }\n        break;\n\n    default:\n        break;\n    }\n\n    if (Lara_Vehicle_IsMounted() && !p->flags.suppress_anim) {\n        Item_Animate(lara_item);\n        const int16_t lara_anim_num =\n            Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM);\n        const int16_t lara_frame_num = Item_GetRelativeFrame(lara_item);\n        Item_SwitchToAnim(item, lara_anim_num, lara_frame_num);\n    }\n\n    if (lara_item->current_anim_state == M_STATE_DEATH\n        || lara_item->current_anim_state == M_STATE_CRASH\n        || lara_item->hit_points <= 0) {\n        return;\n    }\n\n    if (item->rot.z > M_MAX_ROLL || item->rot.z < -M_MAX_ROLL) {\n        Item_SwitchToObjAnim(\n            lara_item, M_ANIM_TOPPLE_START, 0, O_LARA_VEHICLE_ANIM);\n        lara_item->current_anim_state = M_STATE_DEATH;\n        lara_item->goal_anim_state = M_STATE_DEATH;\n        p->flags.control = false;\n        p->flags.stopped = true;\n        p->flags.dead = true;\n        p->speed = 0;\n        item->speed = 0;\n        return;\n    }\n\n    int32_t ceiling = 0;\n    const int32_t height =\n        M_GetCollision(item, item->rot.y, STEP_L * 2, &ceiling);\n\n    if (height < -STEP_L * 2) {\n        Item_SwitchToObjAnim(lara_item, M_ANIM_CRASH, 0, O_LARA_VEHICLE_ANIM);\n        lara_item->current_anim_state = M_STATE_CRASH;\n        lara_item->goal_anim_state = M_STATE_CRASH;\n        p->flags.control = false;\n        p->flags.stopped = true;\n        p->flags.dead = true;\n        p->speed = 0;\n        item->speed = 0;\n        lara_item->hit_points = -1;\n        return;\n    }\n\n    if (lara_item->current_anim_state != M_STATE_DUCK\n        && lara_item->current_anim_state != M_STATE_HIT_BEAM) {\n        COLL_INFO coll = {\n            .radius = 100,\n            .quadrant = Math_GetDirection(item->rot.y),\n        };\n        if (Collide_CollideStaticObjects(\n                &coll, item->pos.x, item->pos.y, item->pos.z, item->room_num,\n                STEP_L * 3)) {\n            Item_SwitchToObjAnim(\n                lara_item, M_ANIM_HIT_BEAM, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_HIT_BEAM;\n            lara_item->goal_anim_state = M_STATE_HIT_BEAM;\n            Spawn_BloodBath(\n                lara_item->pos.x, lara_item->pos.y - STEP_L * 3,\n                lara_item->pos.z, item->speed, item->rot.y, lara_item->room_num,\n                3);\n\n            int16_t damage = 25 * ((uint16_t)p->speed >> 11);\n            CLAMPL(damage, M_MIN_BEAM_DAMAGE);\n            Lara_TakeDamage(damage, false);\n        }\n    }\n\n    if (height > M_JUMP_DIST && p->y_velocity == 0) {\n        p->y_velocity = M_JUMP_VELOCITY;\n    }\n\n    M_ObjectCollision(item);\n}\n\nstatic MINE_CART_TYPE M_GetFloorType(const ITEM *const item)\n{\n    const XYZ_32 pos = {\n        .x = item->pos.x,\n        .y = MAX_HEIGHT,\n        .z = item->pos.z,\n    };\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    return sector->mine_cart_type;\n}\n\nstatic void M_Move(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    if (p->stop_delay != 0) {\n        p->stop_delay--;\n    }\n\n#define L_STOP_POS(p) ((p & (STEP_L * 7 / 2)) == STEP_L * 2)\n\n    const MINE_CART_TYPE floor_type = M_GetFloorType(item);\n    if (floor_type == MINE_CART_STOP && p->stop_delay == 0\n        && (L_STOP_POS(item->pos.x) || L_STOP_POS(item->pos.z))) {\n        if (p->speed < M_STOP_SPEED) {\n            p->flags.control = true;\n            p->flags.stopped = true;\n            p->speed = 0;\n            item->speed = 0;\n            return;\n        }\n\n        p->stop_delay = 16;\n    }\n\n#undef L_STOP_POS\n\n    if ((floor_type == MINE_CART_LEFT || floor_type == MINE_CART_RIGHT)\n        && p->stop_delay == 0 && p->turn.side == M_SIDE_NONE) {\n        uint16_t rot = ((uint16_t)item->rot.y) >> W2V_SHIFT;\n        if (floor_type == MINE_CART_LEFT) {\n            rot |= 4;\n        }\n\n        switch (rot) {\n        case 0:\n            p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x + M_TURN_SHIFT);\n            p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z);\n            break;\n        case 1:\n            p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x);\n            p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z - M_TURN_SHIFT);\n            break;\n        case 2:\n            p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x - M_TURN_SHIFT);\n            p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z);\n            break;\n        case 3:\n            p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x);\n            p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z + M_TURN_SHIFT);\n            break;\n        case 4:\n            p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x - M_TURN_SHIFT);\n            p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z);\n            break;\n        case 5:\n            p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x);\n            p->turn.pos.z = ROUND_TO_SECTOR(item->pos.z + M_TURN_SHIFT);\n            break;\n        case 6:\n            p->turn.pos.x = ROUND_TO_SECTOR(item->pos.x + M_TURN_SHIFT);\n            p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z);\n            break;\n        case 7:\n            p->turn.pos.x = ROUND_TO_SECTOR_END(item->pos.x);\n            p->turn.pos.z = ROUND_TO_SECTOR_END(item->pos.z - M_TURN_SHIFT);\n            break;\n        default:\n            break;\n        }\n\n        int16_t angle =\n            Math_Atan(item->pos.z - p->turn.pos.z, item->pos.x - p->turn.pos.x)\n            & M_ANGLE_CLAMP;\n        if (rot >= 4 && angle != 0) {\n            angle = DEG_90 - angle;\n        }\n\n        p->turn.angle = item->rot.y;\n        p->turn.length = angle;\n        p->turn.side =\n            floor_type == MINE_CART_LEFT ? M_SIDE_LEFT : M_SIDE_RIGHT;\n    }\n\n    CLAMPL(p->speed, M_MIN_SPEED);\n    p->speed += -4 * p->gradient;\n    item->speed = (int16_t)(p->speed >> 8);\n\n    if (item->speed < M_MIN_VELOCITY) {\n        item->speed = M_MIN_VELOCITY;\n        Sound_StopEffect(SFX_MINE_CART_TRACK_LOOP);\n        if (p->y_velocity != 0) {\n            Sound_StopEffect(SFX_MINE_CART_PULLY_LOOP);\n        } else {\n            Sound_Effect(SFX_MINE_CART_PULLY_LOOP, &item->pos, SPM_ALWAYS);\n        }\n    } else {\n        Sound_StopEffect(SFX_MINE_CART_PULLY_LOOP);\n        if (p->y_velocity != 0) {\n            Sound_StopEffect(SFX_MINE_CART_TRACK_LOOP);\n        } else {\n            Sound_Effect(\n                SFX_MINE_CART_TRACK_LOOP, &item->pos,\n                ((item->speed << 15) + 0x1000000) | SPM_PITCH | SPM_ALWAYS);\n        }\n    }\n\n    if (p->turn.side != M_SIDE_NONE) {\n        p->turn.length += 3 * item->speed;\n        if (p->turn.length > (DEG_1 * 90)) {\n            if (p->turn.side == M_SIDE_LEFT) {\n                item->rot.y = p->turn.angle - DEG_90;\n            } else {\n                item->rot.y = p->turn.angle + DEG_90;\n            }\n            p->turn.side = M_SIDE_NONE;\n        } else if (p->turn.side == M_SIDE_LEFT) {\n            item->rot.y = p->turn.angle - p->turn.length;\n        } else {\n            item->rot.y = p->turn.angle + p->turn.length;\n        }\n\n        if (p->turn.side != M_SIDE_NONE) {\n            const uint16_t quadrant = (uint16_t)item->rot.y >> W2V_SHIFT;\n            const int16_t angle = item->rot.y & M_ANGLE_CLAMP;\n\n            XZ_32 shift = {};\n            switch (quadrant) {\n            case DIR_NORTH:\n                shift.x = -Math_Cos(angle);\n                shift.z = Math_Sin(angle);\n                break;\n            case DIR_EAST:\n                shift.x = Math_Sin(angle);\n                shift.z = Math_Cos(angle);\n                break;\n            case DIR_SOUTH:\n                shift.x = Math_Cos(angle);\n                shift.z = -Math_Sin(angle);\n                break;\n            default:\n                shift.x = -Math_Sin(angle);\n                shift.z = -Math_Cos(angle);\n                break;\n            }\n\n            if (p->turn.side == M_SIDE_LEFT) {\n                shift.x = -shift.x;\n                shift.z = -shift.z;\n            }\n\n            item->pos.x =\n                p->turn.pos.x + ((M_TURN_DIST * shift.x) >> W2V_SHIFT);\n            item->pos.z =\n                p->turn.pos.z + ((M_TURN_DIST * shift.z) >> W2V_SHIFT);\n        }\n    } else {\n        item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed);\n    }\n\n    p->mid_pos = M_GetHeight(item, 0, 0);\n\n    if (p->y_velocity == 0) {\n        p->front_pos = M_GetHeight(item, 0, STEP_L);\n        p->gradient = (int16_t)(p->mid_pos - p->front_pos);\n        item->pos.y = p->mid_pos;\n    } else if (item->pos.y > p->mid_pos) {\n        if (p->y_velocity > 0) {\n            Sound_Effect(SFX_QUAD_FRONT_IMPACT, &item->pos, SPM_ALWAYS);\n        }\n\n        item->pos.y = p->mid_pos;\n        p->y_velocity = 0;\n    } else {\n        p->y_velocity += M_GRAVITY;\n        CLAMPG(p->y_velocity, M_MAX_VELOCITY);\n        item->pos.y += p->y_velocity >> 8;\n    }\n\n    item->rot.x = p->gradient << 5;\n\n    if (p->turn.side != M_SIDE_NONE) {\n        const int16_t angle = item->rot.y & M_ANGLE_CLAMP;\n        if (p->turn.side == M_SIDE_RIGHT) {\n            item->rot.z = -(item->speed * angle) >> 9;\n        } else {\n            item->rot.z = (item->speed * (DEG_90 - angle)) >> 9;\n        }\n    } else {\n        item->rot.z -= item->rot.z >> 3;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = M_Collision;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->save_anim = true;\n    obj->save_flags = true;\n    obj->save_position = true;\n}\n\nbool MineCart_Control(void)\n{\n    ITEM *const item = Lara_Vehicle_GetItem();\n    M_PRIV *const p = item->priv;\n    M_UserControl(item);\n\n    if (p->flags.control) {\n        M_Move(item);\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n    const bool mounted = Lara_Vehicle_IsMounted();\n    if (mounted) {\n        lara_item->pos = item->pos;\n        lara_item->rot = item->rot;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    Item_UpdateRoom(lara->item_num, room_num);\n    if (mounted) {\n        Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num);\n    }\n\n    Room_TestTriggers(lara_item);\n\n    if (!p->flags.dead) {\n        g_Camera.target_elevation = M_CAM_RIDE_ELEVATION;\n        g_Camera.target_distance = M_CAM_RIDE_DISTANCE;\n    }\n\n    return mounted;\n}\n\nREGISTER_OBJECT(O_MINE_CART, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/mine_cart.h",
    "content": "#pragma once\n\nbool MineCart_Control(void);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/mounted_gun.c",
    "content": "#include <trx/game/objects/vehicles/mounted_gun.h>\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/core/math.h>\n#include <trx/game/camera.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n// clang-format off\n#define M_MOUNT_RADIUS_SQ 30000\n#define M_NEUTRAL_TILT    30\n#define M_MAX_TILT        (2 * M_NEUTRAL_TILT - 1) // = 59\n#define M_MAX_ROT         544\n#define M_MIN_ROT         (-M_MAX_ROT) // = -544\n#define M_ROT_SPEED       8\n#define M_MAX_ROT_SPEED   64\n#define M_MIN_ROT_SPEED   (-M_MAX_ROT_SPEED) // = -64\n#define M_ROT_SCALE       45\n#define M_FIRE_COOLDOWN   26\n#define M_CAM_ELEVATION   (DEG_1 * -15) // = -2730\n// clang-format on\n\ntypedef enum {\n    M_GUN_STATE_IDLE,\n    M_GUN_STATE_CONTROL,\n    M_GUN_STATE_DISMOUNT,\n    M_GUN_STATE_WAIT_END,\n} M_GUN_STATE;\n\ntypedef struct {\n    M_GUN_STATE state;\n    int32_t fire_count;\n    int16_t tilt;\n    int16_t yaw;\n    int16_t yaw_offset;\n    int32_t yaw_speed;\n} M_PRIV;\n\ntypedef enum {\n    M_STATE_MOUNT,\n    M_STATE_DISMOUNT,\n    M_STATE_TILT,\n} M_STATE;\n\ntypedef enum {\n    M_ANIM_MOUNT,\n    M_ANIM_DISMOUNT,\n    M_ANIM_TILT,\n} M_ANIM;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"state\", &p->state));\n    JSON_SHOULD(JSON_READ(io, \"fire_count\", &p->fire_count));\n    JSON_SHOULD(JSON_READ(io, \"tilt\", &p->tilt));\n    JSON_SHOULD(JSON_READ(io, \"yaw\", &p->yaw));\n    JSON_SHOULD(JSON_READ(io, \"yaw_offset\", &p->yaw_offset));\n    JSON_SHOULD(JSON_READ(io, \"yaw_speed\", &p->yaw_speed));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"state\", p->state);\n    JSONW_WRITE(io, \"fire_count\", p->fire_count);\n    JSONW_WRITE(io, \"tilt\", p->tilt);\n    JSONW_WRITE(io, \"yaw\", p->yaw);\n    JSONW_WRITE(io, \"yaw_offset\", p->yaw_offset);\n    JSONW_WRITE(io, \"yaw_speed\", p->yaw_speed);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->tilt = M_NEUTRAL_TILT;\n    p->yaw_offset = item->rot.y;\n}\n\nstatic bool M_Mount(const int16_t item_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || lara_item->gravity) {\n        return false;\n    }\n\n    const ITEM *const item = Item_Get(item_num);\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t dist = SQUARE(dx) + SQUARE(dz);\n    if (dist > M_MOUNT_RADIUS_SQ) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (!M_Mount(item_num)) {\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    Lara_Vehicle_SetIndex(item_num);\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_type == LGT_FLARE) {\n        Lara_Flare_Dispose(false);\n        lara->gun_type = LGT_UNARMED;\n        lara->request_gun_type = LGT_UNARMED;\n    }\n\n    const ITEM *const item = Item_Get(item_num);\n    lara->gun_status = LGS_HANDS_BUSY;\n    lara_item->pos = item->pos;\n    lara_item->rot = item->rot;\n    Item_SwitchToObjAnim(lara_item, M_ANIM_MOUNT, 0, O_LARA_VEHICLE_ANIM);\n    lara_item->current_anim_state = M_STATE_MOUNT;\n    lara_item->goal_anim_state = M_STATE_MOUNT;\n\n    M_PRIV *const p = item->priv;\n    p->state = M_GUN_STATE_IDLE;\n    p->tilt = M_NEUTRAL_TILT;\n}\n\nstatic void M_Fire(ITEM *const gun_item)\n{\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    ITEM *const projectile_item = Item_Get(item_num);\n    projectile_item->object_id = O_HEAVY_ROCKET;\n    projectile_item->room_num = lara_item->room_num;\n\n    XYZ_32 offset = {\n        .x = 0,\n        .y = 0,\n        .z = STEP_L,\n    };\n    Collide_GetJointAbsPosition(gun_item, &offset, 2);\n    projectile_item->pos = offset;\n    projectile_item->interp.prev.pos = projectile_item->pos;\n    Item_Initialise(item_num);\n\n    M_PRIV *const p = gun_item->priv;\n    projectile_item->rot.x = DEG_1 * (32 - p->tilt);\n    projectile_item->rot.y = gun_item->rot.y;\n    projectile_item->rot.z = 0;\n\n    projectile_item->speed = 16;\n    Item_AddActive(item_num);\n    projectile_item->status = IS_ACTIVE;\n\n    Sound_Effect(SFX_ROCKET_FIRE, &projectile_item->pos, SPM_NORMAL);\n    if (g_TRVersion >= 3) {\n        Sound_Effect(\n            SFX_EXPLOSION_1, &projectile_item->pos, 0x2000000 | SPM_PITCH);\n    }\n\n    const GAME_VECTOR pos = {\n        .pos = projectile_item->pos,\n        .room_num = projectile_item->room_num,\n    };\n    const int32_t smoke_count = g_Weapons[LGT_ROCKET].smoke_count;\n    Sparks_TriggerGunSmoke(pos, true, LGT_ROCKET, smoke_count);\n\n    projectile_item->shade.value_1 = -1;\n    projectile_item->shade.value_2 = -1;\n\n    const XYZ_32 back_128 = XYZ_32_FromYawPitch(\n        projectile_item->rot.y, projectile_item->rot.x, -128);\n    for (int32_t i = 0; i < 8; i++) {\n        const int32_t dist = -(Random_GetControl() & 0x7FF);\n        const XYZ_32 back_vel = XYZ_32_FromYawPitch(\n            projectile_item->rot.y, projectile_item->rot.x, dist);\n        Sparks_TriggerRocketFlame(\n            back_128,\n            (XYZ_32) {\n                .x = back_vel.x - back_128.x,\n                .y = back_vel.y - back_128.y,\n                .z = back_vel.z - back_128.z,\n            },\n            item_num, projectile_item->room_num);\n    }\n}\n\nstatic void M_UserControl(ITEM *const gun_item)\n{\n    M_PRIV *const p = gun_item->priv;\n    if (p->state != M_GUN_STATE_CONTROL) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const int16_t frame_num = Item_GetRelativeFrame(gun_item);\n    if (lara_item->hit_points <= 0 || g_Input.roll) {\n        p->state = M_GUN_STATE_DISMOUNT;\n        return;\n    }\n\n    if (g_Input.action && p->fire_count == 0) {\n        M_Fire(gun_item);\n        p->fire_count = M_FIRE_COOLDOWN;\n        return;\n    }\n\n    if (g_Input.left) {\n        if (p->yaw_speed > 0) {\n            p->yaw_speed >>= 1;\n        }\n\n        p->yaw_speed -= M_ROT_SPEED;\n        CLAMPL(p->yaw_speed, M_MIN_ROT_SPEED);\n\n        if ((frame_num & 7) == 0 && ABS(p->yaw) < M_MAX_ROT) {\n            Sound_Effect(SFX_LARA_UZI_STOP, &gun_item->pos, SPM_NORMAL);\n        }\n    } else if (g_Input.right) {\n        if (p->yaw_speed < 0) {\n            p->yaw_speed >>= 1;\n        }\n\n        p->yaw_speed += M_ROT_SPEED;\n        CLAMPG(p->yaw_speed, M_MAX_ROT_SPEED);\n\n        if ((frame_num & 7) == 0 && ABS(p->yaw) < M_MAX_ROT) {\n            Sound_Effect(SFX_LARA_UZI_STOP, &gun_item->pos, SPM_NORMAL);\n        }\n    } else {\n        p->yaw_speed -= p->yaw_speed >> 2;\n        if (ABS(p->yaw_speed) < M_ROT_SPEED) {\n            p->yaw_speed = 0;\n        }\n    }\n\n    p->yaw += (int16_t)(p->yaw_speed >> 2);\n    if (p->yaw < M_MIN_ROT) {\n        p->yaw = M_MIN_ROT;\n        p->yaw_speed = 0;\n    } else if (p->yaw > M_MAX_ROT) {\n        p->yaw = M_MAX_ROT;\n        p->yaw_speed = 0;\n    }\n\n    if (g_Input.forward && p->tilt < M_MAX_TILT) {\n        p->tilt++;\n    } else if (g_Input.back && p->tilt != 0) {\n        p->tilt--;\n    }\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = M_Collision;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nbool MountedGun_Control(void)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n    ITEM *const gun_item = Lara_Vehicle_GetItem();\n    M_PRIV *const p = gun_item->priv;\n\n    M_UserControl(gun_item);\n\n    if (p->state == M_GUN_STATE_DISMOUNT) {\n        if (p->tilt < M_NEUTRAL_TILT) {\n            p->tilt++;\n        } else if (p->tilt > M_NEUTRAL_TILT) {\n            p->tilt--;\n        } else {\n            Item_SwitchToObjAnim(\n                lara_item, M_ANIM_DISMOUNT, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_DISMOUNT;\n            lara_item->goal_anim_state = M_STATE_DISMOUNT;\n            p->state = M_GUN_STATE_WAIT_END;\n        }\n    }\n\n    switch (lara_item->current_anim_state) {\n    case M_STATE_MOUNT:\n    case M_STATE_DISMOUNT:\n        Item_Animate(lara_item);\n        const ANIM *const anim = Item_GetAnim(lara_item);\n        const int16_t anim_num =\n            lara_item->anim_num - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx;\n        const int16_t frame_num = lara_item->frame_num - anim->frame_base;\n        Item_SwitchToAnim(gun_item, anim_num, frame_num);\n\n        if (p->state == M_GUN_STATE_WAIT_END\n            && Item_TestFrameEqual(gun_item, -1)) {\n            Lara_Vehicle_Dismount();\n            lara->gun_status = LGS_ARMLESS;\n        }\n        break;\n\n    case M_STATE_TILT:\n        Item_SwitchToObjAnim(\n            lara_item, M_ANIM_TILT, p->tilt, O_LARA_VEHICLE_ANIM);\n        Item_SwitchToAnim(gun_item, M_ANIM_TILT, p->tilt);\n\n        if (p->fire_count != 0) {\n            p->fire_count--;\n        }\n\n        p->state = M_GUN_STATE_CONTROL;\n        break;\n\n    default:\n        break;\n    }\n\n    gun_item->rot.y = p->yaw_offset + M_ROT_SCALE * p->yaw;\n    lara_item->rot.y = gun_item->rot.y;\n    g_Camera.target_elevation = M_CAM_ELEVATION;\n\n    return true;\n}\n\nREGISTER_OBJECT(O_MOUNTED_GUN, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/mounted_gun.h",
    "content": "#pragma once\n\nbool MountedGun_Control(void);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/quad_bike.c",
    "content": "#include <trx/game/objects/vehicles/quad_bike.h>\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/anims.h>\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/gym.h>\n#include <trx/game/input.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\nstatic const BITE m_QuadBites[6] = {\n    { .pos = { .x = -56, .y = -32, .z = -380 }, .mesh_num = 0 },\n    { .pos = { .x = 56, .y = -32, .z = -380 }, .mesh_num = 0 },\n    { .pos = { .x = -8, .y = 180, .z = -48 }, .mesh_num = 3 },\n    { .pos = { .x = 8, .y = 180, .z = -48 }, .mesh_num = 4 },\n    { .pos = { .x = 90, .y = 180, .z = -32 }, .mesh_num = 6 },\n    { .pos = { .x = -90, .y = 180, .z = -32 }, .mesh_num = 7 },\n};\n\ntypedef enum {\n    M_STATE_EMPTY,\n    M_STATE_DRIVE,\n    M_STATE_TURN_L,\n    M_STATE_3,\n    M_STATE_4,\n    M_STATE_SLOW,\n    M_STATE_BRAKE,\n    M_STATE_BIKE_DEATH,\n    M_STATE_FALL,\n    M_STATE_GET_ON_R,\n    M_STATE_GET_OFF_R,\n    M_STATE_HIT_BACK,\n    M_STATE_HIT_FRONT,\n    M_STATE_HIT_LEFT,\n    M_STATE_HIT_RIGHT,\n    M_STATE_STOP,\n    M_STATE_16,\n    M_STATE_LAND,\n    M_STATE_STOP_SLOWLY,\n    M_STATE_FALL_DEATH,\n    M_STATE_FALL_OFF,\n    M_STATE_WHEELIE,\n    M_STATE_TURN_R,\n    M_STATE_GET_ON_L,\n    M_STATE_GET_OFF_L,\n} M_STATE;\n\nstatic bool m_DontExitQuad;\nstatic bool m_HandbrakeStarting;\nstatic bool m_CanHandbrakeStart;\nstatic uint8_t m_ExhaustSmokeVel;\n\ntypedef struct {\n    int32_t velocity;\n    int16_t front_rot;\n    int16_t rear_rot;\n    int32_t revs;\n    int32_t engine_revs;\n    int16_t track_mesh;\n    int32_t skidoo_turn;\n    int32_t left_fall_speed;\n    int32_t right_fall_speed;\n    int16_t momentum_angle;\n    int16_t extra_rotation;\n    int32_t pitch;\n    uint8_t flags;\n} M_QUAD_BIKE_INFO;\n\ntypedef struct {\n    M_QUAD_BIKE_INFO quad;\n    int16_t *extra_rotation;\n    int32_t extra_rotation_count;\n    int32_t rear_rot_x_idx[2];\n    int32_t front_rot_x_idx[2];\n} M_PRIV;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"velocity\", &p->quad.velocity));\n    JSON_SHOULD(JSON_READ(io, \"front_rot\", &p->quad.front_rot));\n    JSON_SHOULD(JSON_READ(io, \"rear_rot\", &p->quad.rear_rot));\n    JSON_SHOULD(JSON_READ(io, \"revs\", &p->quad.revs));\n    JSON_SHOULD(JSON_READ(io, \"engine_revs\", &p->quad.engine_revs));\n    JSON_SHOULD(JSON_READ(io, \"track_mesh\", &p->quad.track_mesh));\n    JSON_SHOULD(JSON_READ(io, \"skidoo_turn\", &p->quad.skidoo_turn));\n    JSON_SHOULD(JSON_READ(io, \"left_fall_speed\", &p->quad.left_fall_speed));\n    JSON_SHOULD(JSON_READ(io, \"right_fall_speed\", &p->quad.right_fall_speed));\n    JSON_SHOULD(JSON_READ(io, \"momentum_angle\", &p->quad.momentum_angle));\n    JSON_SHOULD(JSON_READ(io, \"extra_rotation\", &p->quad.extra_rotation));\n    JSON_SHOULD(JSON_READ(io, \"pitch\", &p->quad.pitch));\n    JSON_SHOULD(JSON_READ(io, \"flags\", &p->quad.flags));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"velocity\", p->quad.velocity);\n    JSONW_WRITE(io, \"front_rot\", p->quad.front_rot);\n    JSONW_WRITE(io, \"rear_rot\", p->quad.rear_rot);\n    JSONW_WRITE(io, \"revs\", p->quad.revs);\n    JSONW_WRITE(io, \"engine_revs\", p->quad.engine_revs);\n    JSONW_WRITE(io, \"track_mesh\", p->quad.track_mesh);\n    JSONW_WRITE(io, \"skidoo_turn\", p->quad.skidoo_turn);\n    JSONW_WRITE(io, \"left_fall_speed\", p->quad.left_fall_speed);\n    JSONW_WRITE(io, \"right_fall_speed\", p->quad.right_fall_speed);\n    JSONW_WRITE(io, \"momentum_angle\", p->quad.momentum_angle);\n    JSONW_WRITE(io, \"extra_rotation\", p->quad.extra_rotation);\n    JSONW_WRITE(io, \"pitch\", p->quad.pitch);\n    JSONW_WRITE(io, \"flags\", p->quad.flags);\n}\n\nstatic int32_t M_CountExtraRotationValues(const XYZ_BOOL flags)\n{\n    return (flags.y ? 1 : 0) + (flags.x ? 1 : 0) + (flags.z ? 1 : 0);\n}\n\nstatic void M_EnableWheelExtraRotations(OBJECT *const obj)\n{\n    // TR3 rotates wheels around X at meshes 3/4 (rear) and 6/7 (front).\n    if (obj->mesh_count > 3) {\n        Object_GetBone(obj, 2)->rot.x = true;\n    }\n    if (obj->mesh_count > 4) {\n        Object_GetBone(obj, 3)->rot.x = true;\n    }\n    if (obj->mesh_count > 6) {\n        Object_GetBone(obj, 5)->rot.x = true;\n    }\n    if (obj->mesh_count > 7) {\n        Object_GetBone(obj, 6)->rot.x = true;\n    }\n}\n\nstatic void M_CalcExtraRotationLayout(const OBJECT *const obj, M_PRIV *const p)\n{\n    p->extra_rotation_count = 0;\n    p->rear_rot_x_idx[0] = -1;\n    p->rear_rot_x_idx[1] = -1;\n    p->front_rot_x_idx[0] = -1;\n    p->front_rot_x_idx[1] = -1;\n\n    if (obj == nullptr || !obj->loaded) {\n        return;\n    }\n\n    int32_t cursor = 0;\n    cursor += M_CountExtraRotationValues(obj->base_rot);\n\n    for (int32_t mesh_idx = 1; mesh_idx < obj->mesh_count; mesh_idx++) {\n        const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n        const XYZ_BOOL flags = bone->rot;\n\n        if (flags.x) {\n            const int32_t x_idx = cursor + (flags.y ? 1 : 0);\n            switch (mesh_idx) {\n            case 3:\n                p->rear_rot_x_idx[0] = x_idx;\n                break;\n            case 4:\n                p->rear_rot_x_idx[1] = x_idx;\n                break;\n            case 6:\n                p->front_rot_x_idx[0] = x_idx;\n                break;\n            case 7:\n                p->front_rot_x_idx[1] = x_idx;\n                break;\n            default:\n                break;\n            }\n        }\n\n        cursor += M_CountExtraRotationValues(flags);\n    }\n\n    p->extra_rotation_count = cursor;\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    p->quad.momentum_angle = item->rot.y;\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    M_CalcExtraRotationLayout(obj, p);\n    if (p->extra_rotation_count > 0) {\n        p->extra_rotation = GameBuf_Alloc(\n            sizeof(int16_t) * p->extra_rotation_count, GBUF_ITEM_DATA);\n    } else {\n        p->extra_rotation = nullptr;\n    }\n\n    item->extra_rotations = p->extra_rotation;\n}\n\nstatic int32_t M_GetOnQuadBike(\n    const int16_t item_num, const COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if (!g_Input.action || (item->flags & IF_INVISIBLE) != 0\n        || lara->gun_status != LGS_ARMLESS || lara_item->gravity) {\n        return 0;\n    }\n\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dy = ABS(item->pos.y - lara_item->pos.y);\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t dist = SQUARE(dx) + SQUARE(dz);\n\n    if (dy > 256 || dist > 170000) {\n        return 0;\n    }\n\n    int16_t room_num = item->room_num;\n    SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n\n    const int32_t h = Room_GetHeight(sector, item->pos);\n    if (h < -32000) {\n        return 0;\n    }\n\n    const int16_t ang =\n        Math_Atan(\n            item->pos.z - lara_item->pos.z, item->pos.x - lara_item->pos.x)\n        - item->rot.y;\n    uint16_t uang = lara_item->rot.y - item->rot.y;\n\n    if (ang > -0x1FFE && ang < 0x5FFA) {\n        if (uang <= 0x1FFE || uang >= 0x5FFA) {\n            return 0;\n        }\n    } else {\n        if (uang <= 0x9FF6 || uang >= 0xDFF2) {\n            return 0;\n        }\n    }\n\n    return 1;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara_item->hit_points < 0 || Lara_Vehicle_GetItem() != nullptr) {\n        return;\n    }\n\n    if (!M_GetOnQuadBike(item_num, coll)) {\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    Lara_Vehicle_SetIndex(item_num);\n\n    if (lara->gun_type == LGT_FLARE) {\n        Lara_Flare_Dispose(false);\n        lara->gun_type = LGT_UNARMED;\n        lara->request_gun_type = LGT_UNARMED;\n    }\n\n    lara->gun_status = LGS_HANDS_BUSY;\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    M_QUAD_BIKE_INFO *const quad = &p->quad;\n\n    const int16_t angle =\n        (int16_t)Math_Atan(\n            item->pos.z - lara_item->pos.z, item->pos.x - lara_item->pos.x)\n        - item->rot.y;\n\n    if (angle > -0x1FFE && angle < 0x5FFA) {\n        Item_SwitchToObjAnim(lara_item, 23, 0, O_LARA_VEHICLE_ANIM);\n        lara_item->current_anim_state = M_STATE_GET_ON_L;\n        lara_item->goal_anim_state = M_STATE_GET_ON_L;\n    } else {\n        Item_SwitchToObjAnim(lara_item, 9, 0, O_LARA_VEHICLE_ANIM);\n        lara_item->current_anim_state = M_STATE_GET_ON_R;\n        lara_item->goal_anim_state = M_STATE_GET_ON_R;\n    }\n\n    lara_item->pos.x = item->pos.x;\n    lara_item->pos.y = item->pos.y;\n    lara_item->pos.z = item->pos.z;\n    lara_item->rot.y = item->rot.y;\n    lara->head_rot.y = 0;\n    lara->head_rot.x = 0;\n    lara->torso_rot.y = 0;\n    lara->torso_rot.x = 0;\n    lara->hit_direction = DIR_UNKNOWN;\n    Item_Animate(lara_item);\n\n    // TODO: do not hardcode this\n    if (g_TRVersion == 3 && GF_GetCurrentLevel()->num == 3) {\n        const bool is_ambient =\n            Music_GetCurrentPlayingTrack() == Music_GetCurrentLoopedTrack();\n        const MUSIC_ID tunes[4] = { 9, 12, 4, 12 };\n        if (is_ambient) {\n            Music_Play_Direct(\n                tunes[Random_GetControl() % ARRAY_SIZE(tunes)], MPM_ONCE);\n        }\n    }\n\n    quad->revs = 0;\n}\n\nstatic void M_Explode(ITEM *const item)\n{\n    if (Room_Get(item->room_num)->flags.underwater) {\n        Sparks_TriggerUnderwaterExplosion(item);\n    } else {\n        Sparks_TriggerExplosionSparks(item->pos, 3, -2, 0, item->room_num);\n\n        for (int32_t i = 0; i < 3; i++) {\n            Sparks_TriggerExplosionSparks(item->pos, 3, -1, 0, item->room_num);\n        }\n    }\n\n    const int16_t vehicle_item_num = Lara_Vehicle_GetIndex();\n    Item_Explode(vehicle_item_num, -2, 0);\n    Item_Kill(vehicle_item_num);\n    item->status = IS_DEACTIVATED;\n    Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL);\n    Sound_Effect(SFX_EXPLOSION_2, nullptr, SPM_NORMAL);\n    Lara_Vehicle_SetIndex(NO_ITEM);\n}\n\nstatic bool M_CheckGetOff(void)\n{\n    ITEM *const item = Lara_Vehicle_GetItem();\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if ((lara_item->current_anim_state == M_STATE_GET_OFF_R\n         || lara_item->current_anim_state == M_STATE_GET_OFF_L)\n        && lara_item->frame_num == Item_GetAnim(lara_item)->frame_end) {\n        if (lara_item->current_anim_state == M_STATE_GET_OFF_L) {\n            lara_item->rot.y += DEG_90;\n        } else {\n            lara_item->rot.y -= DEG_90;\n        }\n\n        Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n        lara_item->current_anim_state = LS_STOP;\n        lara_item->goal_anim_state = LS_STOP;\n        lara_item->pos.x -= (512 * Math_Sin(lara_item->rot.y)) >> W2V_SHIFT;\n        lara_item->pos.z -= (512 * Math_Cos(lara_item->rot.y)) >> W2V_SHIFT;\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n        Lara_Vehicle_SetIndex(NO_ITEM);\n        lara->gun_status = LGS_ARMLESS;\n    } else if (lara_item->frame_num == Item_GetAnim(lara_item)->frame_end) {\n        M_PRIV *const p = item->priv;\n        M_QUAD_BIKE_INFO *const quad = &p->quad;\n\n        if (lara_item->current_anim_state == M_STATE_FALL_OFF) {\n            Item_SwitchToAnim(lara_item, LA(LA_FREEFALL), 0);\n            lara_item->current_anim_state = LS_FAST_FALL;\n\n            XYZ_32 pos = {};\n            Lara_GetMeshPos(LM_HIPS, &pos);\n\n            lara_item->pos.x = pos.x;\n            lara_item->pos.y = pos.y;\n            lara_item->pos.z = pos.z;\n            lara_item->gravity = true;\n            lara_item->fall_speed = item->fall_speed;\n            lara_item->rot.x = 0;\n            lara_item->rot.z = 0;\n            lara_item->hit_points = 0;\n            lara->gun_status = LGS_ARMLESS;\n            item->flags |= IF_INVISIBLE;\n            return false;\n        }\n\n        if (lara_item->current_anim_state == M_STATE_FALL_DEATH) {\n            lara_item->goal_anim_state = M_STATE_FALL;\n            lara_item->fall_speed = 154;\n            lara_item->speed = 0;\n            quad->flags |= 0x80;\n            return false;\n        }\n    }\n\n    return true;\n}\n\nstatic int32_t M_TestHeight(\n    const ITEM *const item, const int32_t x, const int32_t z, XYZ_32 *const pos)\n{\n    const int32_t s = Math_Sin(item->rot.y);\n    const int32_t c = Math_Cos(item->rot.y);\n    pos->x = item->pos.x + ((z * c + x * s) >> W2V_SHIFT);\n    pos->y = item->pos.y + ((z * Math_Sin(item->rot.z)) >> W2V_SHIFT)\n        - ((x * Math_Sin(item->rot.x)) >> W2V_SHIFT);\n    pos->z = item->pos.z + ((x * c - z * s) >> W2V_SHIFT);\n\n    int16_t room_num = item->room_num;\n    SECTOR *const sector = Room_GetSector(*pos, &room_num);\n    const int32_t ceiling = Room_GetCeiling(sector, *pos);\n\n    if (pos->y < ceiling || ceiling == NO_HEIGHT) {\n        return NO_HEIGHT;\n    }\n\n    return Room_GetHeight(sector, *pos);\n}\n\nstatic void M_TriggerExhaustSmoke(\n    XYZ_32 pos, int16_t angle, int32_t speed, const bool moving)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n\n    if (moving) {\n#if 0\n        // OG\n        spark->dst_color.r = MINMAX((96 * speed) >> 5, 0, 255);\n        spark->dst_color.g = MINMAX((96 * speed) >> 5, 0, 255);\n        spark->dst_color.b = MINMAX((128 * speed) >> 5, 0, 255);\n#else\n        spark->dst_color.r = 96 >> 1;\n        spark->dst_color.g = 96 >> 1;\n        spark->dst_color.b = 128 >> 1;\n#endif\n    } else {\n        spark->dst_color.r = 96;\n        spark->dst_color.g = 96;\n        spark->dst_color.b = 128;\n    }\n\n    spark->col_fade_speed = 4;\n    spark->fade_to_black = 4;\n    spark->life = (Random_GetControl() & 3) - (speed >> 12) + 20;\n    CLAMPL(spark->life, 9);\n    spark->s_life = spark->life;\n\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos = (XYZ_32) {\n        .x = pos.x + (Random_GetControl() & 0xF) - 8,\n        .y = pos.y + (Random_GetControl() & 0xF) - 8,\n        .z = pos.z + (Random_GetControl() & 0xF) - 8,\n    };\n    spark->vel = (XYZ_32) {\n        .x = (Random_GetControl() & 0xFF) + ((speed * Math_Sin(angle)) >> 16)\n            - 128,\n        .y = -8 - (Random_GetControl() & 7),\n        .z = (Random_GetControl() & 0xFF) + ((speed * Math_Cos(angle)) >> 16)\n            - 128,\n    };\n    spark->friction = 4;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -24 - (Random_GetControl() & 7);\n        } else {\n            spark->rot_add = (Random_GetControl() & 7) + 24;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 2;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->gravity = -4 - (Random_GetControl() & 3);\n    spark->max_y_vel = -8 - (Random_GetControl() & 7);\n    spark->dst_size.width = (Random_GetControl() & 7) + (speed >> 7) + 32;\n    spark->src_size.width = spark->dst_size.width >> 1;\n    spark->size.width = spark->dst_size.width >> 1;\n    spark->dst_size.height = spark->dst_size.width;\n    spark->src_size.height = spark->dst_size.height >> 1;\n    spark->size.height = spark->dst_size.height >> 1;\n    Sparks_FinishSetup(spark);\n}\n\nstatic bool M_SkidooCanGetOff(const int32_t lr)\n{\n    ITEM *const item = Lara_Vehicle_GetItem();\n\n    const int16_t angle = item->rot.y + DEG_90 * lr;\n    const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, 512);\n\n    int16_t room_num = item->room_num;\n    SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t h = Room_GetHeight(sector, pos);\n    const int32_t c = Room_GetCeiling(sector, pos);\n\n    const HEIGHT_TYPE height_type = Room_GetHeightType();\n    if (height_type != HT_BIG_SLOPE && height_type != HT_DIAGONAL\n        && h != NO_HEIGHT && ABS(h - item->pos.y) <= 512\n        && c - item->pos.y <= -LARA_HEIGHT && h - c >= LARA_HEIGHT) {\n        return true;\n    }\n\n    return false;\n}\n\nstatic int32_t M_GetCollisionAnim(ITEM *const item, XYZ_32 *const pos)\n{\n    pos->x = item->pos.x - pos->x;\n    pos->z = item->pos.z - pos->z;\n\n    if (pos->x == 0 && pos->z == 0) {\n        return 0;\n    }\n\n    const int32_t s = Math_Sin(item->rot.y);\n    const int32_t c = Math_Cos(item->rot.y);\n    const int32_t fb = (pos->x * s + pos->z * c) >> W2V_SHIFT;\n    const int32_t lr = (pos->x * c - pos->z * s) >> W2V_SHIFT;\n\n    if (ABS(fb) > ABS(lr)) {\n        return fb > 0 ? 14 : 13;\n    } else {\n        return lr > 0 ? 11 : 12;\n    }\n}\n\nstatic int32_t M_DoDynamics(\n    const int32_t height, int32_t fall_speed, int32_t *const y_pos)\n{\n    if (height <= *y_pos) {\n        int32_t bounce = (height - *y_pos) << 2;\n        CLAMPL(bounce, -80);\n        fall_speed += (bounce - fall_speed) >> 3;\n        CLAMPG(*y_pos, height);\n    } else {\n        *y_pos += fall_speed;\n\n        if (*y_pos <= height - 80) {\n            fall_speed += 6;\n        } else {\n            *y_pos = height;\n            fall_speed = 0;\n        }\n    }\n\n    return fall_speed;\n}\n\nstatic int32_t M_DoShift(\n    ITEM *const item, const XYZ_32 *const new_pos, const XYZ_32 *const old_pos)\n{\n    const int32_t new_x = new_pos->x >> WALL_SHIFT;\n    const int32_t new_z = new_pos->z >> WALL_SHIFT;\n    const int32_t old_x = old_pos->x >> WALL_SHIFT;\n    const int32_t old_z = old_pos->z >> WALL_SHIFT;\n    const int32_t shift_x = new_pos->x & (WALL_L - 1);\n    const int32_t shift_z = new_pos->z & (WALL_L - 1);\n\n    if (new_x == old_x) {\n        if (new_z == old_z) {\n            item->pos.z += (old_pos->z - new_pos->z);\n            item->pos.x += (old_pos->x - new_pos->x);\n            return 0;\n        } else if (new_z <= old_z) {\n            item->pos.z += WALL_L - shift_z;\n            return item->pos.x - new_pos->x;\n        } else {\n            item->pos.z -= 1 + shift_z;\n            return new_pos->x - item->pos.x;\n        }\n    }\n\n    if (new_z == old_z) {\n        if (new_x <= old_x) {\n            item->pos.x += WALL_L - shift_x;\n            return new_pos->z - item->pos.z;\n        } else {\n            item->pos.x -= 1 + shift_x;\n            return item->pos.z - new_pos->z;\n        }\n    }\n\n    int32_t x = 0;\n    int32_t z = 0;\n    XYZ_32 test_pos = { old_pos->x, new_pos->y, new_pos->z };\n    int16_t room_num = item->room_num;\n    SECTOR *sector = Room_GetSector(test_pos, &room_num);\n    const int32_t h = Room_GetHeight(sector, test_pos);\n\n    if (h < old_pos->y - 256) {\n        if (new_pos->z > old_pos->z) {\n            z = -1 - shift_z;\n        } else {\n            z = WALL_L - shift_z;\n        }\n    }\n\n    test_pos = (XYZ_32) { new_pos->x, new_pos->y, old_pos->z };\n    room_num = item->room_num;\n    sector = Room_GetSector(test_pos, &room_num);\n    const int32_t h2 = Room_GetHeight(sector, test_pos);\n\n    if (h2 < old_pos->y - 256) {\n        if (new_pos->x > old_pos->x) {\n            x = -1 - shift_x;\n        } else {\n            x = WALL_L - shift_x;\n        }\n    }\n\n    if (x != 0 && z != 0) {\n        item->pos.x += x;\n        item->pos.z += z;\n        return 0;\n    }\n\n    if (z != 0) {\n        item->pos.z += z;\n\n        if (z > 0) {\n            return item->pos.x - new_pos->x;\n        } else {\n            return new_pos->x - item->pos.x;\n        }\n    }\n\n    if (x != 0) {\n        item->pos.x += x;\n\n        if (x > 0) {\n            return new_pos->z - item->pos.z;\n        } else {\n            return item->pos.z - new_pos->z;\n        }\n    }\n\n    item->pos.x += old_pos->x - new_pos->x;\n    item->pos.z += old_pos->z - new_pos->z;\n    return 0;\n}\n\nstatic void M_SkidooBaddieCollision(ITEM *const quad)\n{\n    ITEM *const lara_item = Lara_GetItem();\n\n    int16_t nearby_rooms[16] = { quad->room_num };\n    int16_t nearby_room_count = 1;\n\n    const PORTALS *const portals = Room_Get(quad->room_num)->portals;\n    if (portals != nullptr) {\n        for (int32_t i = 0; i < portals->count && i < 16; i++) {\n            nearby_rooms[nearby_room_count] = portals->portal[i].room_num;\n            nearby_room_count++;\n        }\n    }\n\n    for (int32_t i = 0; i < nearby_room_count; i++) {\n        int16_t item_num = Room_Get(nearby_rooms[i])->item_num;\n        while (item_num != NO_ITEM) {\n            ITEM *const item = Item_Get(item_num);\n\n            if (!item->collidable || item->status == IS_INVISIBLE\n                || item == lara_item || item == quad) {\n                goto loop_end;\n            }\n\n            const OBJECT *const obj = Object_Get(item->object_id);\n            if (obj->collision_func == nullptr\n                || (!obj->intelligent && item->object_id != O_ROLLING_BALL_2)) {\n                goto loop_end;\n            }\n\n            int32_t dx = quad->pos.x - item->pos.x;\n            int32_t dy = quad->pos.y - item->pos.y;\n            int32_t dz = quad->pos.z - item->pos.z;\n\n            if (dx <= -2048 || dx >= 2048 || dz <= -2048 || dz >= 2048\n                || dy <= -2048 || dy >= 2048\n                || !Item_TestBoundsCollide(item, quad, 500)) {\n                goto loop_end;\n            }\n\n            if (item->object_id == O_ROLLING_BALL_2) {\n                if (item->current_anim_state == 1) {\n                    Lara_TakeDamage(100, true);\n                }\n            } else {\n                if (Item_ShouldSpawnBlood(item)) {\n                    Spawn_BloodBath(\n                        item->pos.x, quad->pos.y - 256, item->pos.z,\n                        quad->speed, quad->rot.y, item->room_num, 3);\n                }\n                if (item->hit_points > 0) {\n                    item->hit_points = 0;\n                    if (item->include_in_kill_stats) {\n                        Stats_AddKill();\n                    }\n                }\n            }\n\n        loop_end:\n            item_num = item->next_item;\n        }\n    }\n}\n\nstatic int32_t M_SkidooDynamics(ITEM *const item)\n{\n    m_DontExitQuad = false;\n    M_PRIV *const p = item->priv;\n    M_QUAD_BIKE_INFO *const quad = &p->quad;\n\n    XYZ_32 old_pos;\n    old_pos.x = item->pos.x;\n    old_pos.y = item->pos.y;\n    old_pos.z = item->pos.z;\n\n    XYZ_32 new_pos = {};\n\n    XYZ_32 front_left_pos = {};\n    XYZ_32 front_right_pos = {};\n    XYZ_32 back_left_pos = {};\n    XYZ_32 back_right_pos = {};\n    XYZ_32 mid_left_pos = {};\n    XYZ_32 mid_right_pos = {};\n    XYZ_32 bm_left_pos = {};\n    XYZ_32 bm_right_pos = {};\n    XYZ_32 fm_left_pos = {};\n    XYZ_32 fm_right_pos = {};\n\n    // clang-format off\n    const int32_t front_left_height  = M_TestHeight(item, 550,  -260, &front_left_pos);\n    const int32_t front_right_height = M_TestHeight(item, 550,  260,  &front_right_pos);\n    const int32_t back_left_height   = M_TestHeight(item, -550, -260, &back_left_pos);\n    const int32_t back_right_height  = M_TestHeight(item, -550, 260,  &back_right_pos);\n    const int32_t mid_left_height    = M_TestHeight(item, 0,    -260, &mid_left_pos);\n    const int32_t mid_right_height   = M_TestHeight(item, 0,    260,  &mid_right_pos);\n    const int32_t bm_left_height     = M_TestHeight(item, 275,  -260, &bm_left_pos);\n    const int32_t bm_right_height    = M_TestHeight(item, 275,  260,  &bm_right_pos);\n    const int32_t fm_left_height     = M_TestHeight(item, -275, -260, &fm_left_pos);\n    const int32_t fm_right_height    = M_TestHeight(item, -275, 260,  &fm_right_pos);\n    // clang-format on\n\n    CLAMPG(back_left_pos.y, back_left_height);\n    CLAMPG(back_right_pos.y, back_right_height);\n    CLAMPG(front_left_pos.y, front_left_height);\n    CLAMPG(front_right_pos.y, front_right_height);\n    CLAMPG(fm_left_pos.y, fm_left_height);\n    CLAMPG(fm_right_pos.y, fm_right_height);\n    CLAMPG(bm_left_pos.y, bm_left_height);\n    CLAMPG(bm_right_pos.y, bm_right_height);\n    CLAMPG(mid_left_pos.y, mid_left_height);\n    CLAMPG(mid_right_pos.y, mid_right_height);\n\n    if (item->pos.y <= item->floor - 256) {\n        item->rot.y += quad->extra_rotation + quad->skidoo_turn;\n    } else {\n        if (quad->skidoo_turn < -364) {\n            quad->skidoo_turn += 364;\n        } else if (quad->skidoo_turn > 364) {\n            quad->skidoo_turn -= 364;\n        } else {\n            quad->skidoo_turn = 0;\n        }\n\n        item->rot.y += quad->extra_rotation + quad->skidoo_turn;\n\n        int16_t vel = 546 - (quad->velocity >> 8);\n        const int16_t ang = item->rot.y - quad->momentum_angle;\n        if (!g_Input.action && quad->velocity > 0) {\n            vel += vel >> 2;\n        }\n\n        if (ang < -273) {\n            if (ang >= -27300) {\n                quad->momentum_angle -= vel;\n            } else {\n                quad->momentum_angle = item->rot.y + 27300;\n            }\n        } else if (ang > 273) {\n            if (ang <= 27300) {\n                quad->momentum_angle += vel;\n            } else {\n                quad->momentum_angle = item->rot.y - 27300;\n            }\n        } else {\n            quad->momentum_angle = item->rot.y;\n        }\n    }\n\n    int16_t room_num = item->room_num;\n    SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, item->pos);\n\n    int32_t speed = item->pos.y < height\n        ? item->speed\n        : (item->speed * Math_Cos(item->rot.x)) >> W2V_SHIFT;\n\n    item->pos.x += (speed * Math_Sin(quad->momentum_angle)) >> W2V_SHIFT;\n    item->pos.z += (speed * Math_Cos(quad->momentum_angle)) >> W2V_SHIFT;\n\n    int32_t slip = (100 * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n    if (ABS(slip) > 50) {\n        m_DontExitQuad = true;\n\n        if (slip > 0) {\n            slip -= 10;\n        } else {\n            slip += 10;\n        }\n\n        item->pos.x -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n        item->pos.z -= (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    }\n\n    slip = (50 * Math_Sin(item->rot.z)) >> W2V_SHIFT;\n\n    if (ABS(slip) > 25) {\n        m_DontExitQuad = true;\n        item->pos.x += (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n        item->pos.z -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    }\n\n    new_pos.x = item->pos.x;\n    new_pos.z = item->pos.z;\n\n    if ((item->flags & IF_INVISIBLE) == 0) {\n        M_SkidooBaddieCollision(item);\n    }\n\n    int16_t shift = 0;\n    int16_t shift2 = 0;\n\n    XYZ_32 front_left_pos2 = {};\n    XYZ_32 bm_left_pos2 = {};\n    XYZ_32 mid_left_pos2 = {};\n    XYZ_32 fm_left_pos2 = {};\n    XYZ_32 back_left_pos2 = {};\n    XYZ_32 front_right_pos2 = {};\n    XYZ_32 bm_right_pos2 = {};\n    XYZ_32 mid_right_pos2 = {};\n    XYZ_32 fm_right_pos2 = {};\n    XYZ_32 back_right_pos2 = {};\n\n    const int32_t front_left_height2 =\n        M_TestHeight(item, 550, -260, &front_left_pos2);\n    if (front_left_height2 < front_left_pos.y - 256) {\n        shift = (int16_t)M_DoShift(item, &front_left_pos2, &front_left_pos);\n    }\n\n    const int32_t bm_left_height2 =\n        M_TestHeight(item, 275, -260, &bm_left_pos2);\n    if (bm_left_height2 < bm_left_pos.y - 256) {\n        M_DoShift(item, &bm_left_pos2, &bm_left_pos);\n    }\n\n    const int32_t mid_left_height2 =\n        M_TestHeight(item, 0, -260, &mid_left_pos2);\n    if (mid_left_height2 < mid_left_pos.y - 256) {\n        M_DoShift(item, &mid_left_pos2, &mid_left_pos);\n    }\n\n    const int32_t fm_left_height2 =\n        M_TestHeight(item, -275, -260, &fm_left_pos2);\n    if (fm_left_height2 < fm_left_pos.y - 256) {\n        M_DoShift(item, &fm_left_pos2, &fm_left_pos);\n    }\n\n    const int32_t back_left_height2 =\n        M_TestHeight(item, -550, -260, &back_left_pos2);\n    if (back_left_height2 < back_left_pos.y - 256) {\n        shift2 = M_DoShift(item, &back_left_pos2, &back_left_pos);\n        if ((shift2 > 0 && shift >= 0) || (shift2 < 0 && shift <= 0)) {\n            shift += shift2;\n        }\n    }\n\n    const int32_t front_right_height2 =\n        M_TestHeight(item, 550, 260, &front_right_pos2);\n    if (front_right_height2 < front_right_pos.y - 256) {\n        shift2 = M_DoShift(item, &front_right_pos2, &front_right_pos);\n        if ((shift2 > 0 && shift >= 0) || (shift2 < 0 && shift <= 0)) {\n            shift += shift2;\n        }\n    }\n\n    const int32_t bm_right_height2 =\n        M_TestHeight(item, 275, 260, &bm_right_pos2);\n    if (bm_right_height2 < bm_right_pos.y - 256) {\n        M_DoShift(item, &bm_right_pos2, &bm_right_pos);\n    }\n\n    const int32_t mid_right_height2 =\n        M_TestHeight(item, 0, 260, &mid_right_pos2);\n    if (mid_right_height2 < mid_right_pos.y - 256) {\n        M_DoShift(item, &mid_right_pos2, &mid_right_pos);\n    }\n\n    const int32_t fm_right_height2 =\n        M_TestHeight(item, -275, 260, &fm_right_pos2);\n    if (fm_right_height2 < fm_right_pos.y - 256) {\n        M_DoShift(item, &fm_right_pos2, &fm_right_pos);\n    }\n\n    const int32_t back_right_height2 =\n        M_TestHeight(item, -550, 260, &back_right_pos2);\n    if (back_right_height2 < back_right_pos.y - 256) {\n        shift2 = M_DoShift(item, &back_right_pos2, &back_right_pos);\n        if ((shift2 > 0 && shift >= 0) || (shift2 < 0 && shift <= 0)) {\n            shift += shift2;\n        }\n    }\n\n    room_num = item->room_num;\n    SECTOR *const sector2 = Room_GetSector(item->pos, &room_num);\n    const int32_t height2 = Room_GetHeight(sector2, item->pos);\n\n    if (height2 < item->pos.y - 256) {\n        M_DoShift(item, &item->pos, &old_pos);\n    }\n\n    quad->extra_rotation = shift;\n    const int32_t anim = M_GetCollisionAnim(item, &new_pos);\n\n    if (anim != 0) {\n        const int32_t dx = item->pos.x - old_pos.x;\n        const int32_t dz = item->pos.z - old_pos.z;\n        int32_t speed2 = (dx * Math_Sin(quad->momentum_angle)\n                          + dz * Math_Cos(quad->momentum_angle))\n            >> W2V_SHIFT;\n        speed2 <<= 8;\n\n        if (Lara_Vehicle_GetItem() == item && quad->velocity == 0xA000\n            && speed2 < 0x9FF6) {\n            ITEM *const lara_item = Lara_GetItem();\n            lara_item->hit_points -= (0xA000 - speed2) >> 7;\n            lara_item->hit_status = 1;\n        }\n\n        if (quad->velocity > 0 && speed2 < quad->velocity) {\n            quad->velocity = speed2 < 0 ? 0 : speed2;\n        } else if (quad->velocity < 0 && speed2 > quad->velocity) {\n            quad->velocity = speed2 > 0 ? 0 : speed2;\n        }\n\n        if (quad->velocity < -0x3000) {\n            quad->velocity = -0x3000;\n        }\n    }\n\n    return anim;\n}\n\nstatic void M_AnimateQuadBike(\n    ITEM *const item, const int32_t hit_wall, const bool killed)\n{\n    int16_t state;\n\n    ITEM *const lara_item = Lara_GetItem();\n    M_PRIV *const p = item->priv;\n    M_QUAD_BIKE_INFO *const quad = &p->quad;\n    state = lara_item->current_anim_state;\n\n    if (item->pos.y != item->floor && state != M_STATE_FALL\n        && state != M_STATE_LAND && state != M_STATE_FALL_OFF && !killed) {\n        if (quad->velocity < 0) {\n            Item_SwitchToObjAnim(lara_item, 6, 0, O_LARA_VEHICLE_ANIM);\n        } else {\n            Item_SwitchToObjAnim(lara_item, 25, 0, O_LARA_VEHICLE_ANIM);\n        }\n\n        lara_item->current_anim_state = M_STATE_FALL;\n        lara_item->goal_anim_state = M_STATE_FALL;\n    } else if (\n        hit_wall != 0 && state != M_STATE_HIT_FRONT && state != M_STATE_HIT_BACK\n        && state != M_STATE_HIT_LEFT && state != M_STATE_HIT_RIGHT\n        && state != M_STATE_FALL_OFF && quad->velocity > 0x3555 && !killed) {\n        switch (hit_wall) {\n        case 13:\n            Item_SwitchToObjAnim(lara_item, 12, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_HIT_FRONT;\n            lara_item->goal_anim_state = M_STATE_HIT_FRONT;\n            break;\n\n        case 14:\n            Item_SwitchToObjAnim(lara_item, 11, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_HIT_BACK;\n            lara_item->goal_anim_state = M_STATE_HIT_BACK;\n            break;\n\n        case 11:\n            Item_SwitchToObjAnim(lara_item, 14, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_HIT_LEFT;\n            lara_item->goal_anim_state = M_STATE_HIT_LEFT;\n            break;\n\n        default:\n            Item_SwitchToObjAnim(lara_item, 13, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_HIT_RIGHT;\n            lara_item->goal_anim_state = M_STATE_HIT_RIGHT;\n            break;\n        }\n\n        Sound_Effect(SFX_QUAD_FRONT_IMPACT, &item->pos, SPM_NORMAL);\n    } else {\n        switch (lara_item->current_anim_state) {\n        case M_STATE_DRIVE:\n            if (killed) {\n                if (quad->velocity <= 0x5000) {\n                    lara_item->goal_anim_state = M_STATE_BIKE_DEATH;\n                } else {\n                    lara_item->goal_anim_state = M_STATE_FALL_DEATH;\n                }\n            } else if (\n                !(quad->velocity & 0xFFFFFF00) && !g_Input.jump\n                && !g_Input.action) {\n                lara_item->goal_anim_state = M_STATE_STOP;\n            } else if (g_Input.left && !m_HandbrakeStarting) {\n                lara_item->goal_anim_state = M_STATE_TURN_L;\n            } else if (g_Input.right && !m_HandbrakeStarting) {\n                lara_item->goal_anim_state = M_STATE_TURN_R;\n            } else if (g_Input.jump) {\n                if (quad->velocity <= 0x6AAA) {\n                    lara_item->goal_anim_state = M_STATE_SLOW;\n                } else {\n                    lara_item->goal_anim_state = M_STATE_BRAKE;\n                }\n            }\n            break;\n\n        case 2:\n            if (!(quad->velocity & 0xFFFFFF00)) {\n                lara_item->goal_anim_state = M_STATE_STOP;\n            } else if (g_Input.right) {\n                Item_SwitchToObjAnim(lara_item, 20, 0, O_LARA_VEHICLE_ANIM);\n                lara_item->current_anim_state = M_STATE_TURN_R;\n                lara_item->goal_anim_state = M_STATE_TURN_R;\n            } else if (!g_Input.left) {\n                lara_item->goal_anim_state = M_STATE_DRIVE;\n            }\n            break;\n\n        case 5:\n        case 6:\n        case 18:\n            if (!(quad->velocity & 0xFFFFFF00)) {\n                lara_item->goal_anim_state = M_STATE_STOP;\n            } else if (g_Input.left) {\n                lara_item->goal_anim_state = M_STATE_TURN_L;\n            } else if (g_Input.right) {\n                lara_item->goal_anim_state = M_STATE_TURN_R;\n            }\n            break;\n\n        case 8:\n            if (item->pos.y == item->floor) {\n                lara_item->goal_anim_state = M_STATE_LAND;\n            } else if (item->fall_speed > 240 && !Game_IsInGym()) {\n                quad->flags |= 0x40;\n            }\n            break;\n\n        case 11:\n        case 12:\n        case 13:\n        case 14:\n            if (g_Input.jump || g_Input.action) {\n                lara_item->goal_anim_state = M_STATE_DRIVE;\n            }\n            break;\n\n        case 15:\n            if (killed) {\n                lara_item->goal_anim_state = M_STATE_BIKE_DEATH;\n                break;\n            }\n            if (g_Input.roll && !quad->velocity && !m_DontExitQuad) {\n                if (g_Input.right && M_SkidooCanGetOff(1)) {\n                    lara_item->goal_anim_state = M_STATE_GET_OFF_R;\n                } else if (g_Input.left && M_SkidooCanGetOff(-1)) {\n                    lara_item->goal_anim_state = M_STATE_GET_OFF_L;\n                }\n            } else if (g_Input.jump || g_Input.action) {\n                lara_item->goal_anim_state = M_STATE_DRIVE;\n            }\n            break;\n\n        case 22:\n            if (!(quad->velocity & 0xFFFFFF00)) {\n                lara_item->goal_anim_state = M_STATE_STOP;\n            } else if (g_Input.left) {\n                Item_SwitchToObjAnim(lara_item, 3, 0, O_LARA_VEHICLE_ANIM);\n                lara_item->current_anim_state = M_STATE_TURN_L;\n                lara_item->goal_anim_state = M_STATE_TURN_L;\n            } else if (!g_Input.right) {\n                lara_item->goal_anim_state = M_STATE_DRIVE;\n            }\n            break;\n        }\n    }\n\n    if (Room_Get(item->room_num)->flags.underwater\n        || Room_Get(item->room_num)->flags.swamp) {\n        lara_item->goal_anim_state = M_STATE_FALL_OFF;\n        lara_item->hit_points = 0;\n        M_Explode(item);\n    }\n}\n\nstatic bool M_UserControl(ITEM *item, int32_t height, int32_t *pitch)\n{\n    M_QUAD_BIKE_INFO *quad;\n\n    M_PRIV *const p = item->priv;\n    quad = &p->quad;\n\n    if (!quad->velocity && !g_Input.sprint && !m_CanHandbrakeStart) {\n        m_CanHandbrakeStart = true;\n    } else if (quad->velocity) {\n        m_CanHandbrakeStart = false;\n    }\n\n    if (!g_Input.sprint) {\n        m_HandbrakeStarting = 0;\n    }\n\n    if (!m_HandbrakeStarting) {\n        if (quad->revs > 16) {\n            quad->velocity += quad->revs >> 4;\n            quad->revs -= quad->revs >> 3;\n        } else {\n            quad->revs = 0;\n        }\n    }\n\n    if (item->pos.y < height - 256) {\n        if (quad->engine_revs < 0xA000) {\n            quad->engine_revs += (0xA000 - quad->engine_revs) >> 3;\n        }\n    } else {\n        if (!quad->velocity && g_Input.look) {\n            Lara_Look_UpDown();\n        }\n\n        if (quad->velocity > 0) {\n            if (g_Input.sprint && !m_HandbrakeStarting\n                && quad->velocity > 0x3000) {\n                if (g_Input.left) {\n                    quad->skidoo_turn -= 500;\n\n                    if (quad->skidoo_turn < -0x5B0) {\n                        quad->skidoo_turn = -0x5B0;\n                    }\n                } else if (g_Input.right) {\n                    quad->skidoo_turn += 500;\n\n                    if (quad->skidoo_turn > 0x5B0) {\n                        quad->skidoo_turn = 0x5B0;\n                    }\n                }\n            } else {\n                if (g_Input.left) {\n                    quad->skidoo_turn -= 455;\n\n                    if (quad->skidoo_turn < -910) {\n                        quad->skidoo_turn = -910;\n                    }\n                } else if (g_Input.right) {\n                    quad->skidoo_turn += 455;\n\n                    if (quad->skidoo_turn > 910) {\n                        quad->skidoo_turn = 910;\n                    }\n                }\n            }\n        } else if (quad->velocity < 0) {\n            if (g_Input.sprint && !m_HandbrakeStarting\n                && quad->velocity < -0x2800) {\n                if (g_Input.right) {\n                    quad->skidoo_turn -= 500;\n\n                    if (quad->skidoo_turn < -0x5B0) {\n                        quad->skidoo_turn = -0x5B0;\n                    }\n                } else if (g_Input.left) {\n                    quad->skidoo_turn += 500;\n\n                    if (quad->skidoo_turn > 0x5B0) {\n                        quad->skidoo_turn = 0x5B0;\n                    }\n                }\n            } else {\n                if (g_Input.right) {\n                    quad->skidoo_turn -= 455;\n\n                    if (quad->skidoo_turn < -910) {\n                        quad->skidoo_turn = -910;\n                    }\n                } else if (g_Input.left) {\n                    quad->skidoo_turn += 455;\n\n                    if (quad->skidoo_turn > 910) {\n                        quad->skidoo_turn = 910;\n                    }\n                }\n            }\n        }\n\n        if (g_Input.jump) {\n            if (g_Input.sprint\n                && (m_CanHandbrakeStart || m_HandbrakeStarting)) {\n                m_HandbrakeStarting = 1;\n                quad->revs -= 512;\n\n                if (quad->revs < -0x3000) {\n                    quad->revs = -0x3000;\n                }\n            } else if (quad->velocity > 0) {\n                quad->velocity -= 640;\n            } else if (quad->velocity > -0x3000) {\n                quad->velocity -= 768;\n            }\n        } else if (g_Input.action) {\n            if (g_Input.sprint\n                && (m_CanHandbrakeStart || m_HandbrakeStarting)) {\n                m_HandbrakeStarting = 1;\n                quad->revs += 512;\n\n                if (quad->revs >= 0xA000) {\n                    quad->revs = 0xA000;\n                }\n            } else if (quad->velocity < 0xA000) {\n                if (quad->velocity < 0x4000) {\n                    quad->velocity += ((0x4800 - quad->velocity) >> 3) + 8;\n                } else if (quad->velocity < 0x7000) {\n                    quad->velocity += ((0x7800 - quad->velocity) >> 4) + 4;\n                } else {\n                    quad->velocity += ((0xA000 - quad->velocity) >> 3) + 2;\n                }\n            } else {\n                quad->velocity = 0xA000;\n            }\n        } else if (quad->velocity > 256) {\n            quad->velocity -= 256;\n        } else if (quad->velocity < -256) {\n            quad->velocity += 256;\n        } else {\n            quad->velocity = 0;\n        }\n\n        if (m_HandbrakeStarting && quad->revs && !g_Input.jump\n            && !g_Input.action) {\n            if (quad->revs > 8) {\n                quad->revs -= quad->revs >> 3;\n            } else {\n                quad->revs = 0;\n            }\n        }\n\n        item->speed = quad->velocity >> 8;\n\n        if (quad->engine_revs > 0x7000) {\n            quad->engine_revs = -0x2000;\n        }\n\n        int32_t revs = 0;\n        if (quad->velocity < 0) {\n            revs = ABS(quad->revs) + ABS(quad->velocity >> 1);\n        } else if (quad->velocity < 0x7000) {\n            revs = ABS(quad->revs) + 0x8800 * quad->velocity / 0x7000 - 0x2000;\n        } else if (quad->velocity <= 0xA000) {\n            revs = ABS(quad->revs) + 0x9800 * (quad->velocity - 0x7000) / 0x3000\n                - 0x2800;\n        } else {\n            revs += ABS(quad->revs);\n        }\n\n        quad->engine_revs += (revs - quad->engine_revs) >> 3;\n    }\n\n    *pitch = quad->engine_revs;\n    return false;\n}\n\nbool QuadBike_Control(void)\n{\n    ITEM *const item = Lara_Vehicle_GetItem();\n    ITEM *const lara_item = Lara_GetItem();\n    M_PRIV *const p = item->priv;\n    M_QUAD_BIKE_INFO *const quad = &p->quad;\n\n    int32_t hit_wall = M_SkidooDynamics(item);\n    bool killed = false;\n    int32_t pitch = 0;\n\n    XYZ_32 front_left_pos = { 0 };\n    XYZ_32 front_right_pos = { 0 };\n    const int32_t front_left_height =\n        M_TestHeight(item, 550, -260, &front_left_pos);\n    const int32_t front_right_height =\n        M_TestHeight(item, 550, 260, &front_right_pos);\n\n    int16_t room_num = item->room_num;\n    SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, item->pos);\n    Room_TestTriggers(lara_item);\n\n    if (lara_item->hit_points <= 0) {\n        killed = true;\n        g_Input.forward = 0;\n        g_Input.back = 0;\n        g_Input.left = 0;\n        g_Input.right = 0;\n    }\n\n    int32_t driving = 0;\n    if (quad->flags != 0) {\n        driving = front_right_height; // what\n        hit_wall = 0;\n    } else {\n        switch (lara_item->current_anim_state) {\n        case M_STATE_GET_ON_R:\n        case M_STATE_GET_OFF_R:\n        case M_STATE_GET_ON_L:\n        case M_STATE_GET_OFF_L:\n            driving = -1;\n            hit_wall = 0;\n            break;\n\n        default:\n            driving = M_UserControl(item, height, &pitch);\n            break;\n        }\n    }\n\n    if (quad->velocity != 0 || quad->revs != 0) {\n        quad->pitch = pitch;\n\n        if (quad->pitch < -0x8000) {\n            quad->pitch = -0x8000;\n        } else if (quad->pitch > 0xA000) {\n            quad->pitch = 0xA000;\n        }\n\n        Sound_Effect(\n            SFX_QUAD_MOVE, &item->pos,\n            SPM_PITCH | ((SOUND_DEFAULT_PITCH + quad->pitch) << 8));\n    } else {\n        if (driving != -1) {\n            Sound_Effect(SFX_QUAD_IDLE, &item->pos, SPM_NORMAL);\n        }\n\n        quad->pitch = 0;\n    }\n\n    item->floor = height;\n    // Cap per-frame wheel rotation delta to avoid large int16 angle jumps.\n    // Large jumps can make interpolation pick the \"short way\" and appear to\n    // spin backwards at high speed.\n    int32_t wheel_delta = quad->velocity >> 2;\n    CLAMP(wheel_delta, -0x1000, 0x1000);\n    quad->front_rot = quad->front_rot - wheel_delta;\n\n    int32_t rear_delta = wheel_delta + (quad->revs >> 3);\n    CLAMP(rear_delta, -0x1000, 0x1000);\n    quad->rear_rot = quad->rear_rot - rear_delta;\n\n    if (p->extra_rotation != nullptr) {\n        if (p->rear_rot_x_idx[0] >= 0) {\n            p->extra_rotation[p->rear_rot_x_idx[0]] = quad->rear_rot;\n        }\n        if (p->rear_rot_x_idx[1] >= 0) {\n            p->extra_rotation[p->rear_rot_x_idx[1]] = quad->rear_rot;\n        }\n        if (p->front_rot_x_idx[0] >= 0) {\n            p->extra_rotation[p->front_rot_x_idx[0]] = quad->front_rot;\n        }\n        if (p->front_rot_x_idx[1] >= 0) {\n            p->extra_rotation[p->front_rot_x_idx[1]] = quad->front_rot;\n        }\n    }\n\n    quad->left_fall_speed = M_DoDynamics(\n        front_left_height, quad->left_fall_speed, &front_left_pos.y);\n    quad->right_fall_speed = M_DoDynamics(\n        front_right_height, quad->right_fall_speed, &front_right_pos.y);\n    item->fall_speed =\n        (int16_t)M_DoDynamics(height, item->fall_speed, &item->pos.y);\n\n    const int32_t avg_front_height =\n        (front_left_pos.y + front_right_pos.y) >> 1;\n    const int16_t x_rot =\n        (int16_t)Math_Atan(550, item->pos.y - avg_front_height);\n    const int16_t z_rot =\n        (int16_t)Math_Atan(260, avg_front_height - front_left_pos.y);\n    item->rot.x += (x_rot - item->rot.x) >> 1;\n    item->rot.z += (z_rot - item->rot.z) >> 1;\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!(quad->flags & 0x80)) {\n        if (room_num != item->room_num) {\n            Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num);\n            Item_UpdateRoom(lara->item_num, room_num);\n        }\n\n        lara_item->pos.x = item->pos.x;\n        lara_item->pos.y = item->pos.y;\n        lara_item->pos.z = item->pos.z;\n        lara_item->rot.x = item->rot.x;\n        lara_item->rot.y = item->rot.y;\n        lara_item->rot.z = item->rot.z;\n        M_AnimateQuadBike(item, hit_wall, killed);\n        Item_Animate(lara_item);\n        item->anim_num = lara_item->anim_num + Object_Get(O_QUAD_BIKE)->anim_idx\n            - Object_Get(O_LARA_VEHICLE_ANIM)->anim_idx;\n        item->frame_num = lara_item->frame_num + Item_GetAnim(item)->frame_base\n            - Item_GetAnim(lara_item)->frame_base;\n        g_Camera.target_elevation = -5460;\n\n        if (quad->flags & 0x40 && item->pos.y == item->floor) {\n            Item_Explode(lara->item_num, -1, 0);\n            lara_item->hit_points = 0;\n            lara_item->flags |= IF_INVISIBLE;\n            M_Explode(item);\n            return false;\n        }\n    }\n\n    const int16_t state = lara_item->current_anim_state;\n    if (state != M_STATE_GET_ON_R && state != M_STATE_GET_ON_L\n        && state != M_STATE_GET_OFF_R && state != M_STATE_GET_OFF_L) {\n        XYZ_32 pos = { 0 };\n        for (int32_t i = 0; i < 2; i++) {\n            pos.x = m_QuadBites[i].pos.x;\n            pos.y = m_QuadBites[i].pos.y;\n            pos.z = m_QuadBites[i].pos.z;\n            Collide_GetJointAbsPosition(item, &pos, m_QuadBites[i].mesh_num);\n            const int16_t smoke_rot = item->rot.y + (i == 0 ? 0x9000 : 0x7000);\n\n            if (item->speed > 32) {\n                const int32_t smoke_vel = MINMAX(96 - item->speed, 8, 64);\n                M_TriggerExhaustSmoke(pos, smoke_rot, smoke_vel, true);\n            } else {\n                int32_t smoke_vel = 0;\n                if (m_ExhaustSmokeVel < 16) {\n                    smoke_vel =\n                        ((Random_GetControl() & 7)\n                         + (Random_GetControl() & 0x10) + 2 * m_ExhaustSmokeVel)\n                        << 7;\n                    m_ExhaustSmokeVel++;\n                } else if (m_HandbrakeStarting) {\n                    smoke_vel = (ABS(quad->revs) >> 2)\n                        + ((Random_GetControl() & 7) << 7);\n                } else if (Random_GetControl() & 3) {\n                    smoke_vel = 0;\n                } else {\n                    smoke_vel = ((Random_GetControl() & 0xF)\n                                 + (Random_GetControl() & 0x10))\n                        << 7;\n                }\n\n                M_TriggerExhaustSmoke(pos, smoke_rot, smoke_vel, false);\n            }\n        }\n    } else {\n        if (Game_IsInGym()) {\n            Gym_TrackManager_Reset(GYM_TRACK_ASSAULT);\n        }\n\n        m_ExhaustSmokeVel = 0;\n    }\n\n    return M_CheckGetOff();\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->collision_func = M_Collision;\n    obj->draw_func = Object_DrawAnimatingItem;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    M_EnableWheelExtraRotations(obj);\n}\n\nREGISTER_OBJECT(O_QUAD_BIKE, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/quad_bike.h",
    "content": "#pragma once\n\nbool QuadBike_Control(void);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/rib.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/camera.h>\n#include <trx/game/fx.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/vehicles/common.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n\n// clang-format off\n#define M_RADIUS        500\n#define M_COLL_DIST     SQUARE(M_RADIUS * 2) // = 1000000\n#define M_FRONT         750\n#define M_SIDE          300\n#define M_TIP           (M_FRONT + 250)      // = 1000\n#define M_TURN          (DEG_1 / 8)          // = 22\n#define M_UNDO_TURN     (DEG_1 / 4)          // = 45\n#define M_MAX_TURN      (DEG_1 * 4)          // = 728\n#define M_MIN_SPEED     20\n#define M_MAX_SPEED     110\n#define M_FAST_SPEED    185\n#define M_SLOW_SPEED    36\n#define M_ACCELERATION  5\n#define M_REVERSE_SPEED 2\n#define M_SHIFT_Y       (-5)\n#define M_CAM_ELEVATION (DEG_1 * -20)        // = -3640\n#define M_CAM_DISTANCE  (WALL_L * 2)         // = 2048\n// clang-format on\n\ntypedef enum {\n    M_STATE_MOUNT,\n    M_STATE_STILL,\n    M_STATE_MOVING,\n    M_STATE_JUMP_R,\n    M_STATE_JUMP_L,\n    M_STATE_HIT,\n    M_STATE_FALL,\n    M_STATE_TURN_R,\n    M_STATE_DEATH,\n    M_STATE_TURN_L,\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_MOUNT_LEFT  = 0,\n    M_ANIM_MOUNT_START = 1,\n    M_ANIM_MOUNT_JUMP  = 6,\n    M_ANIM_MOUNT_RIGHT = 8,\n    M_ANIM_FALL        = 15,\n    M_ANIM_DEATH       = 18,\n    // clang-format on\n} M_ANIM;\n\ntypedef enum {\n    M_MOUNT_NONE,\n    M_MOUNT_LEFT,\n    M_MOUNT_RIGHT,\n    M_MOUNT_JUMP,\n    M_MOUNT_START,\n} M_MOUNT_TYPE;\n\ntypedef struct {\n    int32_t boat_turn;\n    int32_t left_fallspeed;\n    int32_t right_fallspeed;\n    int16_t tilt_angle;\n    int16_t extra_rotation;\n    int32_t water;\n    int32_t pitch;\n    int16_t propeller_roll;\n} M_PRIV;\n\nstatic const BITE m_Propeller = {\n    .pos = { .x = 0, .y = 0, .z = -80 },\n    .mesh_num = 2,\n};\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"boat_turn\", &p->boat_turn));\n    JSON_SHOULD(JSON_READ(io, \"left_fallspeed\", &p->left_fallspeed));\n    JSON_SHOULD(JSON_READ(io, \"right_fallspeed\", &p->right_fallspeed));\n    JSON_SHOULD(JSON_READ(io, \"tilt_angle\", &p->tilt_angle));\n    JSON_SHOULD(JSON_READ(io, \"extra_rotation\", &p->extra_rotation));\n    JSON_SHOULD(JSON_READ(io, \"water\", &p->water));\n    JSON_SHOULD(JSON_READ(io, \"pitch\", &p->pitch));\n    JSON_SHOULD(JSON_READ(io, \"propeller_roll\", &p->propeller_roll));\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"boat_turn\", p->boat_turn);\n    JSONW_WRITE(io, \"left_fallspeed\", p->left_fallspeed);\n    JSONW_WRITE(io, \"right_fallspeed\", p->right_fallspeed);\n    JSONW_WRITE(io, \"tilt_angle\", p->tilt_angle);\n    JSONW_WRITE(io, \"extra_rotation\", p->extra_rotation);\n    JSONW_WRITE(io, \"water\", p->water);\n    JSONW_WRITE(io, \"pitch\", p->pitch);\n    JSONW_WRITE(io, \"propeller_roll\", p->propeller_roll);\n}\n\nstatic void M_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    item->extra_rotations = &p->propeller_roll;\n    FX_Wake_Reset();\n}\n\nstatic M_MOUNT_TYPE M_CheckMount(\n    const int16_t item_num, const COLL_INFO *const coll)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (lara->gun_status != LGS_ARMLESS) {\n        return M_MOUNT_NONE;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    if ((dz * Math_Cos(-item->rot.y) - dx * Math_Sin(-item->rot.y)) >> W2V_SHIFT\n        > WALL_L / 2) {\n        return M_MOUNT_NONE;\n    }\n\n    M_MOUNT_TYPE result = M_MOUNT_NONE;\n    const int16_t angle = item->rot.y - lara_item->rot.y;\n    if (lara->water_status == LWS_SURFACE || lara->water_status == LWS_WADE) {\n        if (!g_Input.action || lara_item->gravity || item->speed) {\n            return M_MOUNT_NONE;\n        }\n\n        if (angle > DEG_45 && angle < DEG_135) {\n            result = M_MOUNT_RIGHT;\n        } else if (angle > -DEG_135 && angle < -DEG_45) {\n            result = M_MOUNT_LEFT;\n        }\n    } else if (lara->water_status == LWS_ABOVE_WATER) {\n        if (lara_item->fall_speed > 0) {\n            if (lara_item->pos.y + 512 > item->pos.y) {\n                result = M_MOUNT_JUMP;\n            }\n        } else if (lara_item->fall_speed == 0) {\n            if (angle > -DEG_135 && angle < DEG_135) {\n                result = XYZ_32_AreEquivalent(lara_item->pos, item->pos)\n                    ? M_MOUNT_START\n                    : M_MOUNT_JUMP;\n            }\n        }\n    }\n\n    if (result != M_MOUNT_NONE) {\n        if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) {\n            return M_MOUNT_NONE;\n        }\n\n        if (!Collide_TestCollision(item, lara_item)) {\n            return M_MOUNT_NONE;\n        }\n    }\n\n    return result;\n}\n\nstatic bool M_CheckDismount(const M_MOUNT_TYPE type)\n{\n    const ITEM *const item = Lara_Vehicle_GetItem();\n    int16_t angle = item->rot.y;\n    if (type == M_MOUNT_RIGHT) {\n        angle += DEG_90;\n    } else {\n        angle -= DEG_90;\n    }\n\n    const XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, angle, WALL_L);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    const HEIGHT_TYPE height_type = Room_GetHeight(sector, pos);\n\n    if (height_type == HT_BIG_SLOPE || height_type == HT_DIAGONAL\n        || height - item->pos.y < -WALL_L / 2) {\n        return false;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (ceiling - item->pos.y > -LARA_HEIGHT) {\n        return false;\n    }\n    if (height - ceiling < LARA_HEIGHT) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) {\n        return;\n    }\n\n    const M_MOUNT_TYPE mount_type = M_CheckMount(item_num, coll);\n    if (mount_type == M_MOUNT_NONE) {\n        coll->enable_baddie_push = true;\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    Lara_Vehicle_SetIndex(item_num);\n    M_ANIM boat_anim_idx;\n    switch (mount_type) {\n    case M_MOUNT_RIGHT:\n        boat_anim_idx = M_ANIM_MOUNT_RIGHT;\n        break;\n    case M_MOUNT_LEFT:\n        boat_anim_idx = M_ANIM_MOUNT_LEFT;\n        break;\n    case M_MOUNT_JUMP:\n        boat_anim_idx = M_ANIM_MOUNT_JUMP;\n        break;\n    default:\n        boat_anim_idx = M_ANIM_MOUNT_START;\n        break;\n    }\n\n    Item_SwitchToObjAnim(lara_item, boat_anim_idx, 0, O_LARA_VEHICLE_ANIM);\n    lara_item->current_anim_state = M_STATE_MOUNT;\n    lara_item->goal_anim_state = M_STATE_MOUNT;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    lara->water_status = LWS_ABOVE_WATER;\n\n    ITEM *const item = Item_Get(item_num);\n    lara_item->pos.x = item->pos.x;\n    lara_item->pos.y = item->pos.y + M_SHIFT_Y;\n    lara_item->pos.z = item->pos.z;\n    lara_item->rot.x = 0;\n    lara_item->rot.y = item->rot.y;\n    lara_item->rot.z = 0;\n    lara_item->gravity = 0;\n    lara_item->speed = 0;\n    lara_item->fall_speed = 0;\n\n    if (lara_item->room_num != item->room_num) {\n        Item_UpdateRoom(lara->item_num, item->room_num);\n    }\n\n    Item_Animate(lara_item);\n\n    if (item->status != IS_ACTIVE) {\n        Item_AddActive(item_num);\n        item->status = IS_ACTIVE;\n    }\n\n    Music_Play(MX_RIB_THEME, MPM_ONCE);\n}\n\nstatic bool M_UserControl(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    bool no_turn = true;\n\n    if (item->pos.y < p->water - STEP_L / 2 || p->water == NO_HEIGHT) {\n        return no_turn;\n    }\n\n    const bool input_left = g_Input.left || g_Input.step_left;\n    const bool input_right = g_Input.right || g_Input.step_right;\n\n    if ((g_Input.roll || g_Input.look) && item->speed == 0) {\n        if (!input_left && !input_right) {\n            item->speed = 0;\n        } else if (!g_Input.roll) {\n            item->speed = M_MIN_SPEED;\n        }\n\n        if (g_Input.look && item->speed == 0) {\n            Lara_Look_UpDown();\n        }\n\n        return no_turn;\n    }\n\n    if ((input_left && !g_Input.jump) || (input_right && g_Input.jump)) {\n        if (p->boat_turn > 0) {\n            p->boat_turn -= M_UNDO_TURN;\n        } else {\n            p->boat_turn -= M_TURN;\n            CLAMPL(p->boat_turn, -M_MAX_TURN);\n        }\n        no_turn = false;\n    } else if ((input_right && !g_Input.jump) || (input_left && g_Input.jump)) {\n        if (p->boat_turn < 0) {\n            p->boat_turn += M_UNDO_TURN;\n        } else {\n            p->boat_turn += M_TURN;\n            CLAMPG(p->boat_turn, M_MAX_TURN);\n        }\n        no_turn = false;\n    }\n\n    if (g_Input.jump) {\n        if (item->speed > 0) {\n            item->speed -= M_ACCELERATION;\n        } else if (item->speed > -M_MIN_SPEED) {\n            item->speed -= M_REVERSE_SPEED;\n        }\n    } else if (g_Input.action) {\n        int16_t max_speed;\n        if (g_Input.sprint) {\n            max_speed = M_FAST_SPEED;\n        } else {\n            max_speed = g_Input.slow ? M_SLOW_SPEED : M_MAX_SPEED;\n        }\n\n        if (item->speed < max_speed) {\n            item->speed = M_ACCELERATION * item->speed / (2 * max_speed)\n                + item->speed + 2;\n        } else if (item->speed > max_speed + 1) {\n            item->speed--;\n        }\n    } else if (\n        item->speed >= 0 && item->speed < M_MIN_SPEED\n        && (input_left || input_right)) {\n        if (item->speed == 0 && !g_Input.roll) {\n            item->speed = M_MIN_SPEED;\n        }\n    } else if (item->speed > 1) {\n        item->speed--;\n    } else {\n        item->speed = 0;\n    }\n\n    return no_turn;\n}\n\nstatic void M_Animate(const ITEM *const item, const M_ANIM collide_anim)\n{\n    const M_PRIV *const p = item->priv;\n    ITEM *const lara_item = Lara_GetItem();\n\n    if (lara_item->hit_points <= 0) {\n        if (lara_item->current_anim_state != M_STATE_DEATH) {\n            Item_SwitchToObjAnim(\n                lara_item, M_ANIM_DEATH, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_DEATH;\n            lara_item->goal_anim_state = M_STATE_DEATH;\n        }\n        return;\n    }\n\n    if (item->pos.y < p->water - STEP_L / 2 && item->fall_speed > 0) {\n        if (lara_item->current_anim_state != M_STATE_FALL) {\n            Item_SwitchToObjAnim(\n                lara_item, M_ANIM_FALL, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_FALL;\n            lara_item->goal_anim_state = M_STATE_FALL;\n        }\n        return;\n    }\n\n    if (collide_anim != 0) {\n        if (lara_item->current_anim_state != M_STATE_HIT) {\n            Item_SwitchToObjAnim(\n                lara_item, collide_anim, 0, O_LARA_VEHICLE_ANIM);\n            lara_item->current_anim_state = M_STATE_HIT;\n            lara_item->goal_anim_state = M_STATE_HIT;\n        }\n        return;\n    }\n\n    const bool input_left = g_Input.left || g_Input.step_left;\n    const bool input_right = g_Input.right || g_Input.step_right;\n\n    switch (lara_item->current_anim_state) {\n    case M_STATE_STILL:\n        if (g_Input.roll && item->speed == 0) {\n            if (input_right && M_CheckDismount(M_MOUNT_RIGHT)) {\n                lara_item->goal_anim_state = M_STATE_JUMP_R;\n            } else if (input_left && M_CheckDismount(M_MOUNT_LEFT)) {\n                lara_item->goal_anim_state = M_STATE_JUMP_L;\n            }\n        }\n\n        if (item->speed > 0) {\n            lara_item->goal_anim_state = M_STATE_MOVING;\n        }\n        break;\n\n    case M_STATE_MOVING:\n        if (item->speed <= 0) {\n            lara_item->goal_anim_state = M_STATE_STILL;\n        } else if (input_right) {\n            lara_item->goal_anim_state = M_STATE_TURN_R;\n        } else if (input_left) {\n            lara_item->goal_anim_state = M_STATE_TURN_L;\n        }\n        break;\n\n    case M_STATE_FALL:\n        lara_item->goal_anim_state = M_STATE_MOVING;\n        break;\n\n    case M_STATE_TURN_R:\n        if (item->speed <= 0) {\n            lara_item->goal_anim_state = M_STATE_STILL;\n        } else if (!input_right) {\n            lara_item->goal_anim_state = M_STATE_MOVING;\n        }\n        break;\n\n    case M_STATE_TURN_L:\n        if (item->speed <= 0) {\n            lara_item->goal_anim_state = M_STATE_STILL;\n        } else if (!input_left) {\n            lara_item->goal_anim_state = M_STATE_MOVING;\n        }\n        break;\n    }\n}\n\nstatic int32_t M_TestWaterHeight(\n    const ITEM *const item, const int32_t z_off, const int32_t x_off,\n    XYZ_32 *const pos)\n{\n    const int32_t sx = Math_Sin(item->rot.x);\n    const int32_t sz = Math_Sin(item->rot.z);\n    pos->y =\n        item->pos.y + ((x_off * sz) >> W2V_SHIFT) - ((z_off * sx) >> W2V_SHIFT);\n\n    const int32_t sy = Math_Sin(item->rot.y);\n    const int32_t cy = Math_Cos(item->rot.y);\n    pos->x = item->pos.x + ((x_off * cy + z_off * sy) >> W2V_SHIFT);\n    pos->z = item->pos.z + ((z_off * cy - x_off * sy) >> W2V_SHIFT);\n\n    int16_t room_num = item->room_num;\n    Room_GetSector(*pos, &room_num);\n    int32_t height = Room_GetWaterHeight(*pos, room_num);\n\n    if (height == NO_HEIGHT) {\n        const SECTOR *const sector = Room_GetSector(*pos, &room_num);\n        height = Room_GetHeight(sector, *pos);\n        if (height == NO_HEIGHT) {\n            return height;\n        }\n    }\n\n    return height + M_SHIFT_Y;\n}\n\nstatic void M_DoShift(const int32_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    int16_t next_item_num = Room_Get(item->room_num)->item_num;\n\n    while (next_item_num != NO_ITEM) {\n        const ITEM *const next_item = Item_Get(next_item_num);\n        if ((next_item->object_id == O_RIB || next_item->object_id == O_BOAT)\n            && next_item_num != Lara_Vehicle_GetIndex()\n            && next_item_num != item_num) {\n            const int32_t dx = next_item->pos.x - item->pos.x;\n            const int32_t dz = next_item->pos.z - item->pos.z;\n            const int32_t dist = XYZ_32_GetLength2((XYZ_32) { dx, 0, dz });\n            if (dist < M_COLL_DIST) {\n                item->pos.x = next_item->pos.x - M_COLL_DIST * dx / dist;\n                item->pos.z = next_item->pos.z - M_COLL_DIST * dz / dist;\n            }\n            break;\n        }\n\n        next_item_num = next_item->next_item;\n    }\n}\n\nstatic int32_t M_DoDynamics(\n    const int32_t height, int32_t fall_speed, int32_t *const y_pos)\n{\n    if (height > *y_pos) {\n        *y_pos += fall_speed;\n        if (*y_pos <= height) {\n            fall_speed += GRAVITY;\n        } else {\n            *y_pos = height;\n            fall_speed = 0;\n        }\n    } else {\n        fall_speed += (height - fall_speed - *y_pos) >> 3;\n        CLAMPG(*y_pos, height);\n    }\n\n    return fall_speed;\n}\n\nstatic inline int32_t M_DoCornerShift(\n    ITEM *const item, const int32_t x_off, const int32_t z_off,\n    const XYZ_32 *const old_pos)\n{\n    XYZ_32 pos;\n    const int32_t h = M_TestWaterHeight(item, x_off, z_off, &pos);\n    if (h < old_pos->y - STEP_L / 2) {\n        return Vehicle_DoShift(item, &pos, old_pos);\n    }\n    return 0;\n}\n\nstatic int32_t M_Dynamics(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    item->rot.z -= p->tilt_angle;\n\n    XYZ_32 fl_old;\n    XYZ_32 fr_old;\n    XYZ_32 bl_old;\n    XYZ_32 br_old;\n    XYZ_32 f_old;\n    const int32_t hfl_old = M_TestWaterHeight(item, M_FRONT, -M_SIDE, &fl_old);\n    const int32_t hfr_old = M_TestWaterHeight(item, M_FRONT, M_SIDE, &fr_old);\n    const int32_t hbl_old = M_TestWaterHeight(item, -M_FRONT, -M_SIDE, &bl_old);\n    const int32_t hbr_old = M_TestWaterHeight(item, -M_FRONT, M_SIDE, &br_old);\n    const int32_t hf_old = M_TestWaterHeight(item, M_TIP, 0, &f_old);\n    CLAMPG(bl_old.y, hbl_old);\n    CLAMPG(br_old.y, hbr_old);\n    CLAMPG(fl_old.y, hfl_old);\n    CLAMPG(fr_old.y, hfr_old);\n    CLAMPG(f_old.y, hf_old);\n\n    XYZ_32 old_pos = item->pos;\n\n    item->rot.y += p->extra_rotation + p->boat_turn;\n    p->tilt_angle = p->boat_turn * 6;\n    item->pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, item->speed);\n\n    if (item->speed < 0) {\n        p->propeller_roll += DEG_1 * 33;\n    } else {\n        p->propeller_roll += DEG_1 * (3 * item->speed + 2);\n    }\n\n    int32_t slip = (Math_Sin(item->rot.z) * 30) >> W2V_SHIFT;\n    if (slip == 0 && item->rot.z != 0) {\n        slip = item->rot.z > 0 ? 1 : -1;\n    }\n    item->pos.x += (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n\n    slip = (Math_Sin(item->rot.x) * 10) >> W2V_SHIFT;\n    if (slip == 0 && item->rot.x != 0) {\n        slip = item->rot.x > 0 ? 1 : -1;\n    }\n    item->pos.x -= (slip * Math_Sin(item->rot.y)) >> W2V_SHIFT;\n    item->pos.z -= (slip * Math_Cos(item->rot.y)) >> W2V_SHIFT;\n\n    XYZ_32 moved = {\n        .x = item->pos.x,\n        .y = 0,\n        .z = item->pos.z,\n    };\n    M_DoShift(item_num);\n\n    int32_t rot = 0;\n    rot += M_DoCornerShift(item, -M_FRONT, -M_SIDE, &bl_old);\n    rot += M_DoCornerShift(item, -M_FRONT, +M_SIDE, &br_old);\n    rot += M_DoCornerShift(item, +M_FRONT, -M_SIDE, &fl_old);\n    rot += M_DoCornerShift(item, +M_FRONT, +M_SIDE, &fr_old);\n    if (slip == 0) {\n        M_DoCornerShift(item, M_TIP, 0, &f_old);\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    int32_t height = Room_GetWaterHeight(item->pos, room_num);\n    if (height == NO_HEIGHT) {\n        height = Room_GetHeight(sector, item->pos);\n    }\n\n    if (height < item->pos.y - STEP_L / 2) {\n        Vehicle_DoShift(item, &item->pos, &old_pos);\n    }\n\n    p->extra_rotation = rot;\n    const int32_t coll_anim = Vehicle_GetCollisionAnim(item, &moved);\n\n    if (slip != 0 || coll_anim != 0) {\n        const int32_t sx = (item->pos.x - old_pos.x) * Math_Sin(item->rot.y);\n        const int32_t sz = (item->pos.z - old_pos.z) * Math_Cos(item->rot.y);\n        int32_t new_speed = (sx + sz) >> W2V_SHIFT;\n\n        if (Lara_Vehicle_GetIndex() == item_num\n            && item->speed > (M_MAX_SPEED + M_ACCELERATION)\n            && new_speed < item->speed - 10) {\n            Lara_TakeDamage(item->speed, true);\n            Sound_Effect(SFX_LARA_INJURY, &Lara_GetItem()->pos, SPM_NORMAL);\n            new_speed >>= 1;\n            item->speed >>= 1;\n        }\n\n        if (slip != 0) {\n            if (item->speed <= M_MAX_SPEED + 10) {\n                item->speed = new_speed;\n            }\n        } else if (item->speed > 0 && new_speed < item->speed) {\n            item->speed = new_speed;\n        } else if (item->speed < 0 && new_speed > item->speed) {\n            item->speed = new_speed;\n        }\n        CLAMPL(item->speed, -M_MIN_SPEED);\n    }\n\n    return coll_anim;\n}\n\nstatic void M_Splash(\n    const ITEM *const item, const int32_t fall_speed,\n    const int32_t water_height)\n{\n    FX_WATER_SPLASH_SETUP splash_setup = {\n        .x = item->pos.x,\n        .y = water_height,\n        .z = item->pos.z,\n        .inner_xz_off = 64,\n        .inner_xz_size = 48,\n        .inner_y_size = -384,\n        .inner_xz_vel = 160,\n        .inner_y_vel = -128 * fall_speed,\n        .inner_gravity = 128,\n        .inner_friction = 7,\n        .middle_xz_off = 96,\n        .middle_xz_size = 96,\n        .middle_y_size = -256,\n        .middle_xz_vel = 224,\n        .middle_y_vel = (-64 * fall_speed),\n        .middle_gravity = 72,\n        .middle_friction = 8,\n        .outer_xz_off = 128,\n        .outer_xz_size = 128,\n        .outer_xz_vel = 272,\n        .outer_friction = 9,\n    };\n    FX_Water_SetupSplash(&splash_setup);\n}\n\nstatic void M_TriggerMist(\n    const XYZ_32 pos, const int32_t speed, const int16_t angle,\n    const int32_t snow)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n\n    if (snow != 0) {\n        spark->dst_color.r = 255;\n        spark->dst_color.g = 255;\n        spark->dst_color.b = 255;\n    } else {\n        spark->dst_color.r = 64;\n        spark->dst_color.g = 64;\n        spark->dst_color.b = 64;\n    }\n\n    spark->col_fade_speed = (Random_GetControl() & 3) + 4;\n    spark->fade_to_black = 12 - (snow << 3);\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 20;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0x7F)\n        + ((speed * Math_Sin(angle)) >> (W2V_SHIFT + 2)) - 64;\n    spark->vel.y = 12 * speed;\n    spark->vel.z = (Random_GetControl() & 0x7F)\n        + ((speed * Math_Cos(angle)) >> (W2V_SHIFT + 2)) - 64;\n    spark->friction = 3;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = +16 + (Random_GetControl() & 0xF);\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    if (snow != 0) {\n        spark->friction = 0;\n        spark->scalar = 3;\n        spark->vel.y = -spark->vel.y >> 5;\n        spark->max_y_vel = 0;\n        spark->gravity = (Random_GetControl() & 0x1F) + 32;\n        const uint8_t size = (Random_GetControl() & 7) + 16;\n        spark->size.width = size;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = spark->size.width;\n        spark->size.height = size;\n        spark->src_size.height = spark->size.height;\n        spark->dst_size.height = spark->size.height;\n    } else {\n        spark->scalar = 4;\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n        const uint8_t size = (Random_GetControl() & 7) + (speed >> 1) + 16;\n        spark->dst_size.width = size;\n        spark->size.width = spark->dst_size.width >> 2;\n        spark->src_size.width = spark->dst_size.width >> 2;\n        spark->dst_size.height = size;\n        spark->src_size.height = spark->dst_size.height >> 2;\n        spark->size.height = spark->dst_size.height >> 2;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_DoWake(\n    const ITEM *const item, const int32_t x_off, const int32_t z_off,\n    const int16_t rotate)\n{\n    const int32_t start_idx = FX_Wake_GetStartIndex();\n    if (FX_Wake_GetPoint(start_idx, rotate)->life != 0) {\n        return;\n    }\n\n    XYZ_32 pos = XYZ_32_OffsetYaw(item->pos, item->rot.y, z_off);\n    pos = XYZ_32_OffsetYaw(pos, item->rot.y + DEG_90, x_off);\n    pos.y += STEP_L / 2;\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n    const int32_t water_height = Room_GetWaterHeight(pos, room_num);\n    if (water_height == NO_HEIGHT) {\n        return;\n    }\n\n    int16_t angle1;\n    int16_t angle2;\n    if (item->speed >= 0) {\n        if (rotate) {\n            angle1 = item->rot.y + (DEG_1 * 160);\n            angle2 = item->rot.y + (DEG_1 * 140);\n        } else {\n            angle1 = item->rot.y - (DEG_1 * 160);\n            angle2 = item->rot.y - (DEG_1 * 140);\n        }\n    } else {\n        if (rotate) {\n            angle1 = item->rot.y + (DEG_1 * 20);\n            angle2 = item->rot.y + (DEG_1 * 40);\n        } else {\n            angle1 = item->rot.y - (DEG_1 * 20);\n            angle2 = item->rot.y - (DEG_1 * 40);\n        }\n    }\n\n    XYZ_32 vel[2] = {\n        XYZ_32_OffsetYaw((XYZ_32) {}, angle1, 4),\n        XYZ_32_OffsetYaw((XYZ_32) {}, angle2, 10),\n    };\n    FX_WAKE_POINT *const pt = FX_Wake_GetPoint(start_idx, rotate);\n    pt->life = 64;\n\n    for (int32_t i = 0; i < 2; i++) {\n        pt->pos[i].x = pos.x;\n        pt->pos[i].y = item->pos.y + 32;\n        pt->pos[i].z = pos.z;\n        pt->prev_pos[i] = pt->pos[i];\n        pt->vel[i].x = vel[i].x;\n        pt->vel[i].z = vel[i].z;\n    }\n\n    if (rotate == 1) {\n        FX_Wake_AdvanceStartIndex();\n    }\n}\n\nstatic void M_ControlWake(const ITEM *const item)\n{\n    const XYZ_32 pos = {\n        .x = item->pos.x,\n        .y = item->pos.y + STEP_L / 2,\n        .z = item->pos.z,\n    };\n    int16_t room_num = item->room_num;\n    Room_GetSector(pos, &room_num);\n    const int32_t water_height = Room_GetWaterHeight(pos, room_num);\n    const bool valid_height =\n        water_height <= item->pos.y + 32 && water_height != NO_HEIGHT;\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const bool leaving = lara_item->current_anim_state == M_STATE_JUMP_R\n        || lara_item->current_anim_state == M_STATE_JUMP_L;\n\n    const int32_t time4 = Output_GetTimeInGame() * 4;\n    if ((time4 & 0xF) == 0 && valid_height && !leaving) {\n        M_DoWake(item, -384, 0, 0);\n        M_DoWake(item, 384, 0, 1);\n    }\n\n    uint8_t wake_shade = FX_Wake_GetShade();\n    if (item->speed == 0 || !valid_height || leaving) {\n        if (wake_shade != 0) {\n            wake_shade--;\n        }\n    } else if (wake_shade < 16) {\n        wake_shade++;\n    }\n    FX_Wake_SetShade(wake_shade);\n}\n\nstatic void M_ControlEffects(const ITEM *const item)\n{\n    XYZ_32 pos = m_Propeller.pos;\n    Collide_GetJointAbsPosition(item, &pos, m_Propeller.mesh_num);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t water_height = Room_GetWaterHeight(pos, room_num);\n\n    if (item->speed != 0 && water_height < pos.y && water_height != NO_HEIGHT) {\n        M_TriggerMist(pos, ABS(item->speed), item->rot.y + 0x8000, 0);\n        if ((Random_GetControl() & 1) == 0) {\n            XYZ_32 bubble_pos = {\n                .x = pos.x + (Random_GetControl() & 0x3F) - 32,\n                .y = pos.y + (Random_GetControl() & 0xF),\n                .z = pos.z + (Random_GetControl() & 0x3F) - 32,\n            };\n            room_num = item->room_num;\n            Room_GetSector(bubble_pos, &room_num);\n            Spawn_BubbleEx(&bubble_pos, room_num, 16, 8);\n        }\n        return;\n    }\n\n    const int32_t height = Room_GetHeight(sector, pos);\n    if (pos.y > height && !(Room_Get(room_num)->flags.underwater)) {\n        for (int32_t i = (Random_GetControl() & 3) + 3; i > 0; i--) {\n            const int16_t angle = item->rot.y + Random_GetControl() + DEG_90;\n            M_TriggerMist(\n                pos, ((Random_GetControl() & 0xF) + 96) << 4, angle, 1);\n        }\n    }\n}\n\nstatic void M_Control(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    bool drive = false;\n    bool no_turn = true;\n    const int32_t coll_anim = M_Dynamics(item_num);\n\n    XYZ_32 fl;\n    XYZ_32 fr;\n    const int32_t hfl = M_TestWaterHeight(item, M_FRONT, -M_SIDE, &fl);\n    const int32_t hfr = M_TestWaterHeight(item, M_FRONT, M_SIDE, &fr);\n\n    int16_t room_num = item->room_num;\n    const SECTOR *sector = Room_GetSector(item->pos, &room_num);\n    int32_t height = Room_GetHeight(sector, item->pos);\n    Room_GetCeiling(sector, item->pos);\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (Lara_Vehicle_GetIndex() == item_num) {\n        Room_TestTriggers(lara_item);\n        Room_TestTriggers(item);\n    }\n\n    int32_t water_height = Room_GetWaterHeight(item->pos, room_num);\n    p->water = water_height;\n\n    if (Lara_Vehicle_GetIndex() == item_num && lara_item->hit_points > 0) {\n        switch (lara_item->current_anim_state) {\n        case M_STATE_MOUNT:\n        case M_STATE_JUMP_R:\n        case M_STATE_JUMP_L:\n            break;\n        default:\n            drive = true;\n            no_turn = M_UserControl(item);\n            break;\n        }\n    } else if (item->speed > 1) {\n        item->speed--;\n    } else {\n        item->speed = 0;\n    }\n\n    if (no_turn) {\n        if (p->boat_turn < -M_UNDO_TURN) {\n            p->boat_turn += M_UNDO_TURN;\n        } else if (p->boat_turn > M_UNDO_TURN) {\n            p->boat_turn -= M_UNDO_TURN;\n        } else {\n            p->boat_turn = 0;\n        }\n    }\n\n    item->floor = height + M_SHIFT_Y;\n    if (p->water == NO_HEIGHT) {\n        p->water = height;\n    } else {\n        p->water += M_SHIFT_Y;\n    }\n\n    p->left_fallspeed = M_DoDynamics(hfl, p->left_fallspeed, &fl.y);\n    p->right_fallspeed = M_DoDynamics(hfr, p->right_fallspeed, &fr.y);\n\n    const int16_t fall_speed = item->fall_speed;\n    item->fall_speed = M_DoDynamics(p->water, item->fall_speed, &item->pos.y);\n\n    if (fall_speed - item->fall_speed > 32 && !item->fall_speed\n        && water_height != NO_HEIGHT) {\n        M_Splash(item, fall_speed - item->fall_speed, water_height);\n    }\n\n    height = fr.y + fl.y;\n    if (height >= 0) {\n        height >>= 1;\n    } else {\n        height = -ABS(height) >> 1;\n    }\n\n    const int16_t x_rot = Math_Atan(M_FRONT, item->pos.y - height);\n    const int16_t z_rot = Math_Atan(M_SIDE, height - fl.y);\n    item->rot.x += (x_rot - item->rot.x) / 2;\n    item->rot.z += (z_rot - item->rot.z) / 2;\n\n    if (x_rot == 0 && ABS(item->rot.x) < 4) {\n        item->rot.x = 0;\n    }\n    if (z_rot == 0 && ABS(item->rot.z) < 4) {\n        item->rot.z = 0;\n    }\n\n    if (Lara_Vehicle_GetIndex() == item_num) {\n        M_Animate(item, coll_anim);\n\n        if (room_num != item->room_num) {\n            Item_UpdateRoom(item_num, room_num);\n            Item_UpdateRoom(Item_GetIndex(lara_item), room_num);\n        }\n\n        item->rot.z += p->tilt_angle;\n        lara_item->pos = item->pos;\n        lara_item->rot = item->rot;\n        Item_Animate(lara_item);\n\n        if (lara_item->hit_points > 0) {\n            const int16_t anim_idx =\n                Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM);\n            const int16_t frame_idx = Item_GetRelativeFrame(lara_item);\n            Item_SwitchToAnim(item, anim_idx, frame_idx);\n        }\n\n        g_Camera.target_elevation = M_CAM_ELEVATION;\n        g_Camera.target_distance = M_CAM_DISTANCE;\n    } else {\n        if (room_num != item->room_num) {\n            Item_UpdateRoom(item_num, room_num);\n        }\n        item->rot.z += p->tilt_angle;\n    }\n\n    p->pitch += (item->speed - p->pitch) >> 2;\n    if (item->speed > 8) {\n        Sound_Effect(\n            SFX_RIB_MOVING, &item->pos,\n            SPM_PITCH | ((0x10000 - (M_MAX_SPEED - p->pitch) * 100) << 8));\n    } else if (drive) {\n        Sound_Effect(\n            SFX_RIB_IDLE, &item->pos,\n            SPM_PITCH | ((0x10000 - (M_MAX_SPEED - p->pitch) * 100) << 8));\n    }\n\n    if (Lara_Vehicle_GetIndex() != item_num) {\n        return;\n    }\n\n    if ((lara_item->current_anim_state == M_STATE_JUMP_R\n         || lara_item->current_anim_state == M_STATE_JUMP_L)\n        && Item_TestFrameEqual(lara_item, -1)) {\n        if (lara_item->current_anim_state == M_STATE_JUMP_L) {\n            lara_item->rot.y -= DEG_90;\n        } else {\n            lara_item->rot.y += DEG_90;\n        }\n\n        Lara_Vehicle_Dismount();\n        Item_SwitchToAnim(lara_item, LA(LA_JUMP_FORWARD), 0);\n        lara_item->current_anim_state = LS(LS_JUMP_FORWARD);\n        lara_item->goal_anim_state = LS(LS_JUMP_FORWARD);\n        lara_item->gravity = true;\n        lara_item->fall_speed = -40;\n        lara_item->speed = 20;\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n\n        room_num = lara_item->room_num;\n        XYZ_32 pos = XYZ_32_OffsetYaw(lara_item->pos, lara_item->rot.y, 360);\n        pos.y -= 90;\n        sector = Room_GetSector(pos, &room_num);\n\n        if (Room_GetHeight(sector, pos) >= pos.y - STEP_L) {\n            lara_item->pos.x = pos.x;\n            lara_item->pos.z = pos.z;\n            Item_UpdateRoom(Item_GetIndex(lara_item), room_num);\n        }\n\n        lara_item->pos.y = pos.y;\n        Item_SwitchToAnim(item, 0, 0);\n    }\n\n    M_ControlWake(item);\n    M_ControlEffects(item);\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    Object_DrawAnimatingItem(item);\n    FX_Wake_Draw(item);\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->initialise_func = M_Initialise;\n    obj->collision_func = M_Collision;\n    obj->control_func = M_Control;\n    obj->draw_func = M_Draw;\n\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n\n    Object_GetBone(obj, 1)->rot.z = true;\n}\n\nREGISTER_OBJECT(O_RIB, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/skidoo_armed.c",
    "content": "#include <trx/core/math.h>\n#include <trx/game/collision.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/creatures/skidoo_driver.h>\n#include <trx/game/pathing.h>\n\n#define M_ARMED_RADIUS (WALL_L / 3) // = 341\n\nstatic void M_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) {\n        return;\n    }\n\n    if (!Collide_TestCollision(item, lara_item)) {\n        return;\n    }\n\n    if (coll->enable_baddie_push) {\n        Lara_Col_ItemPush(\n            item, coll, item->speed > 0 ? coll->enable_hit : false, false);\n    }\n\n    if (!Lara_Vehicle_IsMounted() && item->speed > 0) {\n        Lara_TakeDamage(100, true);\n    }\n}\n\nvoid SkidooArmed_Push(\n    const ITEM *const item, ITEM *const lara_item, const int32_t radius)\n{\n    const int32_t dx = lara_item->pos.x - item->pos.x;\n    const int32_t dz = lara_item->pos.z - item->pos.z;\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sy = Math_Sin(item->rot.y);\n\n    int32_t rx = (cy * dx - sy * dz) >> W2V_SHIFT;\n    int32_t rz = (sy * dx + cy * dz) >> W2V_SHIFT;\n\n    const ANIM_FRAME *const best_frame = Item_GetBestFrame(item);\n    BOUNDS_16 bounds = {\n        .min.x = best_frame->bounds.min.x - radius,\n        .max.x = best_frame->bounds.max.x + radius,\n        .min.z = best_frame->bounds.min.z - radius,\n        .max.z = best_frame->bounds.max.z + radius,\n    };\n\n    if (rx < bounds.min.x || rx > bounds.max.x || rz < bounds.min.z\n        || rz > bounds.max.z) {\n        return;\n    }\n\n    const int32_t r = bounds.max.x - rx;\n    const int32_t l = rx - bounds.min.x;\n    const int32_t t = bounds.max.z - rz;\n    const int32_t b = rz - bounds.min.z;\n    if (l <= r && l <= t && l <= b) {\n        rx -= l;\n    } else if (r <= l && r <= t && r <= b) {\n        rx += r;\n    } else if (t <= l && t <= r && t <= b) {\n        rz += t;\n    } else {\n        rz -= b;\n    }\n\n    lara_item->pos.x = item->pos.x + ((rz * sy + rx * cy) >> W2V_SHIFT);\n    lara_item->pos.z = item->pos.z + ((rz * cy - rx * sy) >> W2V_SHIFT);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    if (!obj->loaded) {\n        return;\n    }\n\n    obj->collision_func = M_Collision;\n\n    obj->hit_points = SKIDOO_DRIVER_HITPOINTS;\n    obj->radius = M_ARMED_RADIUS;\n    obj->shadow_size = UNIT_SHADOW / 2;\n    obj->pivot_length = 0;\n    obj->lot_setup = LOT_Setup(LOT_SETUP_JUMPER);\n\n    obj->intelligent = true;\n    obj->save_position = true;\n    obj->save_hitpoints = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_SKIDOO_ARMED, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/skidoo_armed.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n\nvoid SkidooArmed_Push(const ITEM *item, ITEM *lara_item, int32_t radius);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/skidoo_common.c",
    "content": "#include <trx/game/objects/vehicles/skidoo_common.h>\n\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/game/collision.h>\n#include <trx/game/creature.h>\n#include <trx/game/effects.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/vehicles/common.h>\n#include <trx/game/objects/vehicles/skidoo_armed.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/spawn.h>\n\n#define M_RADIUS 500\n#define M_SIDE 260\n#define M_FRONT 550\n#define M_SNOW 500\n#define M_GET_OFF_DIST 330\n#define M_TARGET_DIST (WALL_L * 2) // = 2048\n\n#define M_ACCELERATION 10\n#define M_SLOWDOWN 2\n\n#define M_SLIP 100\n#define M_SLIP_SIDE 50\n\n#define M_MAX_BACK -30\n#define M_BRAKE 5\n#define M_REVERSE (-5)\n#define M_UNDO_TURN (DEG_1 * 2) // = 364\n#define M_TURN (DEG_1 / 2 + M_UNDO_TURN) // = 455\n#define M_MOMENTUM_TURN (DEG_1 * 3) // = 546\n#define M_MAX_MOMENTUM_TURN (DEG_1 * 150) // = 27300\n#define M_MIN_BOUNCE 50\n#define M_MAX_KICK -80\n\n#define LF_SKIDOO_EXIT_END 59\n#define LF_SKIDOO_LET_GO_END 17\n\ntypedef enum {\n    M_GET_ON_NONE = 0,\n    SKIDOO_GET_ON_LEFT = 1,\n    M_GET_ON_RIGHT = 2,\n} M_GET_ON_SIDE;\n\ntypedef enum {\n    // clang-format off\n    LARA_STATE_SKIDOO_SIT       = 0,\n    LARA_STATE_SKIDOO_GET_ON    = 1,\n    LARA_STATE_SKIDOO_LEFT      = 2,\n    LARA_STATE_SKIDOO_RIGHT     = 3,\n    LARA_STATE_SKIDOO_FALL      = 4,\n    LARA_STATE_SKIDOO_HIT       = 5,\n    LARA_STATE_SKIDOO_GET_ON_L  = 6,\n    LARA_STATE_SKIDOO_GET_OFF_L = 7,\n    LARA_STATE_SKIDOO_STILL     = 8,\n    LARA_STATE_SKIDOO_GET_OFF_R = 9,\n    LARA_STATE_SKIDOO_LET_GO    = 10,\n    LARA_STATE_SKIDOO_DEATH     = 11,\n    LARA_STATE_SKIDOO_FALLOFF   = 12,\n    // clang-format on\n} LARA_SKIDOO_STATE;\n\ntypedef enum {\n    // clang-format off\n    LA_SKIDOO_GET_ON_L = 1,\n    LA_SKIDOO_FALL = 8,\n    LA_SKIDOO_HIT_LEFT = 11,\n    LA_SKIDOO_HIT_RIGHT = 12,\n    LA_SKIDOO_HIT_FRONT = 13,\n    LA_SKIDOO_HIT_BACK = 14,\n    LA_SKIDOO_DEAD = 15,\n    LA_SKIDOO_GET_ON_R = 18,\n    // clang-format on\n} LARA_ANIM_SKIDOO;\n\nBITE g_Skidoo_LeftGun = {\n    .pos = { .x = 219, .y = -71, .z = M_FRONT },\n    .mesh_num = 0,\n};\n\nBITE g_Skidoo_RightGun = {\n    .pos = { .x = -235, .y = -71, .z = M_FRONT },\n    .mesh_num = 0,\n};\n\nstatic int32_t M_DoDynamics(\n    const int32_t height, const int32_t fall_speed, int32_t *const out_y)\n{\n    if (height > *out_y) {\n        *out_y += fall_speed;\n        if (*out_y > height - M_MIN_BOUNCE) {\n            *out_y = height;\n            return 0;\n        }\n        return fall_speed + GRAVITY;\n    }\n\n    int32_t kick = 4 * (height - *out_y);\n    CLAMPL(kick, M_MAX_KICK);\n    CLAMPG(*out_y, height);\n    return fall_speed + ((kick - fall_speed) >> 3);\n}\n\nstatic bool M_IsArmed(const SKIDOO_INFO *const skidoo_data)\n{\n    return skidoo_data->track_mesh & SKIDOO_GUN_MESH;\n}\n\nstatic bool M_CheckBaddieCollision(ITEM *const item, ITEM *const skidoo)\n{\n    if (!item->collidable || item->status == IS_INVISIBLE\n        || item == Lara_GetItem() || item == skidoo) {\n        return false;\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    const bool is_availanche = item->object_id == O_ROLLING_BALL_2;\n    if (obj->collision_func == nullptr\n        || (!obj->intelligent && !is_availanche)) {\n        return false;\n    }\n\n    if (!Item_IsNearby(item, skidoo, M_TARGET_DIST)) {\n        return false;\n    }\n\n    if (!Item_TestBoundsCollide(item, skidoo, M_RADIUS)) {\n        return false;\n    }\n\n    if (item->object_id == O_SKIDOO_ARMED) {\n        SkidooArmed_Push(item, skidoo, M_RADIUS);\n    } else if (is_availanche) {\n        if (item->current_anim_state == TRAP_ACTIVATE) {\n            Lara_TakeDamage(100, true);\n        }\n    } else if (\n        obj->intelligent && item->status == IS_ACTIVE\n        && (Item_IsTargetable(item) || item->hit_points == 0)) {\n        if (Item_ShouldSpawnBlood(item)) {\n            Spawn_BloodBath(\n                item->pos.x, skidoo->pos.y - STEP_L, item->pos.z, skidoo->speed,\n                skidoo->rot.y, item->room_num, 3);\n        }\n        Gun_HitTarget(item, nullptr, nullptr, item->hit_points);\n    }\n    return true;\n}\n\nvoid Skidoo_Initialise(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (item->priv == nullptr) {\n        item->priv = GameBuf_Alloc(sizeof(SKIDOO_INFO), GBUF_ITEM_DATA);\n    }\n\n    SKIDOO_INFO *const skidoo_data = item->priv;\n    skidoo_data->skidoo_turn = 0;\n    skidoo_data->right_fallspeed = 0;\n    skidoo_data->left_fallspeed = 0;\n    skidoo_data->extra_rotation = 0;\n    skidoo_data->momentum_angle = item->rot.y;\n    skidoo_data->track_mesh = 0;\n    skidoo_data->pitch = 0;\n}\n\nint32_t Skidoo_CheckGetOn(const int16_t item_num, COLL_INFO *const coll)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS || lara_item->gravity\n        || (lara->water_status != LWS_ABOVE_WATER\n            && lara->water_status != LWS_WADE)) {\n        return M_GET_ON_NONE;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n    const int16_t angle = item->rot.y - lara_item->rot.y;\n\n    M_GET_ON_SIDE get_on = M_GET_ON_NONE;\n    if (angle > DEG_45 && angle < DEG_135) {\n        get_on = SKIDOO_GET_ON_LEFT;\n    } else if (angle > -DEG_135 && angle < -DEG_45) {\n        get_on = M_GET_ON_RIGHT;\n    }\n\n    if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) {\n        return M_GET_ON_NONE;\n    }\n\n    if (!Collide_TestCollision(item, lara_item)) {\n        return M_GET_ON_NONE;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, item->pos);\n    if (height < -32000) {\n        return M_GET_ON_NONE;\n    }\n\n    return get_on;\n}\n\nvoid Skidoo_Collision(\n    const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll)\n{\n    if (lara_item->hit_points < 0 || Lara_Vehicle_IsMounted()) {\n        return;\n    }\n\n    const M_GET_ON_SIDE get_on = Skidoo_CheckGetOn(item_num, coll);\n    if (get_on == M_GET_ON_NONE) {\n        Object_Collision(item_num, lara_item, coll);\n        return;\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    Lara_Vehicle_SetIndex(item_num);\n    if (lara->gun_type == LGT_FLARE) {\n        Lara_Flare_Dispose(false);\n        lara->gun_type = LGT_UNARMED;\n        lara->request_gun_type = LGT_UNARMED;\n    }\n\n    const LARA_ANIM_SKIDOO anim_idx =\n        get_on == SKIDOO_GET_ON_LEFT ? LA_SKIDOO_GET_ON_L : LA_SKIDOO_GET_ON_R;\n    Item_SwitchToObjAnim(lara_item, anim_idx, 0, O_LARA_SKIDOO);\n    lara_item->current_anim_state = LARA_STATE_SKIDOO_GET_ON;\n    lara->gun_status = LGS_ARMLESS;\n    lara->hit_direction = DIR_UNKNOWN;\n\n    ITEM *const item = Item_Get(item_num);\n    lara_item->pos.x = item->pos.x;\n    lara_item->pos.y = item->pos.y;\n    lara_item->pos.z = item->pos.z;\n    lara_item->rot.y = item->rot.y;\n    item->hit_points = 1;\n}\n\nvoid Skidoo_BaddieCollision(ITEM *const skidoo)\n{\n    int16_t roomies[12];\n    const int32_t roomies_count =\n        Room_GetAdjoiningRooms(skidoo->room_num, roomies, 12);\n\n    for (int32_t i = 0; i < roomies_count; i++) {\n        const ROOM *const room = Room_Get(roomies[i]);\n        int16_t item_num = room->item_num;\n        while (item_num != NO_ITEM) {\n            ITEM *item = Item_Get(item_num);\n            M_CheckBaddieCollision(item, skidoo);\n            item_num = item->next_item;\n        }\n    }\n}\n\nint32_t Skidoo_TestHeight(\n    const ITEM *const item, const int32_t z_off, const int32_t x_off,\n    XYZ_32 *const out_pos)\n{\n    const int32_t sx = Math_Sin(item->rot.x);\n    const int32_t sz = Math_Sin(item->rot.z);\n    const int32_t cy = Math_Cos(item->rot.y);\n    const int32_t sy = Math_Sin(item->rot.y);\n    out_pos->x = item->pos.x + ((x_off * cy + z_off * sy) >> W2V_SHIFT);\n    out_pos->y = item->pos.y + ((x_off * sz - z_off * sx) >> W2V_SHIFT);\n    out_pos->z = item->pos.z + ((z_off * cy - x_off * sy) >> W2V_SHIFT);\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(*out_pos, &room_num);\n    return Room_GetHeight(sector, *out_pos);\n}\n\nvoid Skidoo_DoSnowEffect(const ITEM *const skidoo)\n{\n    if (!Object_Get(O_SNOW_SPRITE)->loaded) {\n        return;\n    }\n\n    const int16_t effect_num = Effect_Create(skidoo->room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    const int32_t sx = Math_Sin(skidoo->rot.x);\n    const int32_t sy = Math_Sin(skidoo->rot.y);\n    const int32_t cy = Math_Cos(skidoo->rot.y);\n    const int32_t x = (M_SIDE * (Random_GetDraw() - 0x4000)) >> 14;\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos.x = skidoo->pos.x - ((sy * M_SNOW + cy * x) >> W2V_SHIFT);\n    effect->pos.y = skidoo->pos.y + ((sx * M_SNOW) >> W2V_SHIFT);\n    effect->pos.z = skidoo->pos.z - ((cy * M_SNOW - sy * x) >> W2V_SHIFT);\n    effect->room_num = skidoo->room_num;\n    effect->object_id = O_SNOW_SPRITE;\n    effect->frame_num = 0;\n    effect->speed = 0;\n    if (skidoo->speed < 64) {\n        effect->fall_speed =\n            (Random_GetDraw() * (ABS(skidoo->speed) - 64)) >> 15;\n    } else {\n        effect->fall_speed = 0;\n    }\n\n    g_MatrixPtr->_23 = 0;\n    g_WMatrixPtr->_23 = 0;\n\n    Output_CalculateLight(effect->pos, effect->room_num);\n    effect->shade = Output_GetLightAdder() - 512;\n    CLAMPL(effect->shade, 0);\n}\n\nint32_t Skidoo_Dynamics(ITEM *const skidoo)\n{\n    SKIDOO_INFO *const skidoo_data = skidoo->priv;\n\n    XYZ_32 fl_old;\n    XYZ_32 bl_old;\n    XYZ_32 br_old;\n    XYZ_32 fr_old;\n    int32_t hfl_old = Skidoo_TestHeight(skidoo, M_FRONT, -M_SIDE, &fl_old);\n    int32_t hfr_old = Skidoo_TestHeight(skidoo, M_FRONT, M_SIDE, &fr_old);\n    int32_t hbl_old = Skidoo_TestHeight(skidoo, -M_FRONT, -M_SIDE, &bl_old);\n    int32_t hbr_old = Skidoo_TestHeight(skidoo, -M_FRONT, M_SIDE, &br_old);\n\n    XYZ_32 old = {\n        .z = skidoo->pos.z,\n        .x = skidoo->pos.x,\n        .y = skidoo->pos.y,\n    };\n\n    CLAMPG(bl_old.y, hbl_old);\n    CLAMPG(br_old.y, hbr_old);\n    CLAMPG(fl_old.y, hfl_old);\n    CLAMPG(fr_old.y, hfr_old);\n\n    if (skidoo->pos.y <= skidoo->floor - STEP_L) {\n        skidoo->rot.y += skidoo_data->extra_rotation + skidoo_data->skidoo_turn;\n    } else {\n        if (skidoo_data->skidoo_turn < -M_UNDO_TURN) {\n            skidoo_data->skidoo_turn += M_UNDO_TURN;\n        } else if (skidoo_data->skidoo_turn > M_UNDO_TURN) {\n            skidoo_data->skidoo_turn -= M_UNDO_TURN;\n        } else {\n            skidoo_data->skidoo_turn = 0;\n        }\n        skidoo->rot.y += skidoo_data->skidoo_turn + skidoo_data->extra_rotation;\n\n        int16_t rot = skidoo->rot.y - skidoo_data->momentum_angle;\n        if (rot < -M_MOMENTUM_TURN) {\n            if (rot < -M_MAX_MOMENTUM_TURN) {\n                rot = -M_MAX_MOMENTUM_TURN;\n                skidoo_data->momentum_angle = skidoo->rot.y - rot;\n            } else {\n                skidoo_data->momentum_angle -= M_MOMENTUM_TURN;\n            }\n        } else if (rot > M_MOMENTUM_TURN) {\n            if (rot > M_MAX_MOMENTUM_TURN) {\n                rot = M_MAX_MOMENTUM_TURN;\n                skidoo_data->momentum_angle = skidoo->rot.y - rot;\n            } else {\n                skidoo_data->momentum_angle += M_MOMENTUM_TURN;\n            }\n        } else {\n            skidoo_data->momentum_angle = skidoo->rot.y;\n        }\n    }\n\n    skidoo->pos.z +=\n        (skidoo->speed * Math_Cos(skidoo_data->momentum_angle)) >> W2V_SHIFT;\n    skidoo->pos.x +=\n        (skidoo->speed * Math_Sin(skidoo_data->momentum_angle)) >> W2V_SHIFT;\n\n    int32_t slip;\n    slip = (M_SLIP * Math_Sin(skidoo->rot.x)) >> W2V_SHIFT;\n    if (ABS(slip) > M_SLIP / 2) {\n        skidoo->pos.z -= (slip * Math_Cos(skidoo->rot.y)) >> W2V_SHIFT;\n        skidoo->pos.x -= (slip * Math_Sin(skidoo->rot.y)) >> W2V_SHIFT;\n    }\n\n    slip = (M_SLIP_SIDE * Math_Sin(skidoo->rot.z)) >> W2V_SHIFT;\n    if (ABS(slip) > M_SLIP_SIDE / 2) {\n        skidoo->pos.z -= (slip * Math_Sin(skidoo->rot.y)) >> W2V_SHIFT;\n        skidoo->pos.x += (slip * Math_Cos(skidoo->rot.y)) >> W2V_SHIFT;\n    }\n\n    XYZ_32 moved = {\n        .x = skidoo->pos.x,\n        .z = skidoo->pos.z,\n    };\n    if (!(skidoo->flags & IF_ONE_SHOT)) {\n        Skidoo_BaddieCollision(skidoo);\n    }\n\n    int32_t rot = 0;\n\n    XYZ_32 br;\n    XYZ_32 fl;\n    XYZ_32 bl;\n    XYZ_32 fr;\n    const int32_t hbl = Skidoo_TestHeight(skidoo, -M_FRONT, -M_SIDE, &bl);\n    if (hbl < bl_old.y - STEP_L) {\n        rot = Vehicle_DoShift(skidoo, &bl, &bl_old);\n    }\n    const int32_t hbr = Skidoo_TestHeight(skidoo, -M_FRONT, M_SIDE, &br);\n    if (hbr < br_old.y - STEP_L) {\n        rot += Vehicle_DoShift(skidoo, &br, &br_old);\n    }\n    const int32_t hfl = Skidoo_TestHeight(skidoo, M_FRONT, -M_SIDE, &fl);\n    if (hfl < fl_old.y - STEP_L) {\n        rot += Vehicle_DoShift(skidoo, &fl, &fl_old);\n    }\n    const int32_t hfr = Skidoo_TestHeight(skidoo, M_FRONT, M_SIDE, &fr);\n    if (hfr < fr_old.y - STEP_L) {\n        rot += Vehicle_DoShift(skidoo, &fr, &fr_old);\n    }\n\n    int16_t room_num = skidoo->room_num;\n    const SECTOR *const sector = Room_GetSector(skidoo->pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, skidoo->pos);\n    if (height < skidoo->pos.y - STEP_L) {\n        Vehicle_DoShift(skidoo, &skidoo->pos, &old);\n    }\n\n    skidoo_data->extra_rotation = rot;\n\n    int32_t collide = Vehicle_GetCollisionAnim(skidoo, &moved);\n    if (collide != 0) {\n        const int32_t c = Math_Cos(skidoo_data->momentum_angle);\n        const int32_t s = Math_Sin(skidoo_data->momentum_angle);\n        const int32_t dx = skidoo->pos.x - old.x;\n        const int32_t dz = skidoo->pos.z - old.z;\n        const int32_t new_speed = (s * dx + c * dz) >> W2V_SHIFT;\n\n        if (skidoo == Lara_Vehicle_GetItem()\n            && skidoo->speed > SKIDOO_MAX_SPEED + M_ACCELERATION\n            && new_speed < skidoo->speed - M_ACCELERATION) {\n            Lara_TakeDamage((skidoo->speed - new_speed) / 2, true);\n        }\n\n        if (skidoo->speed > 0 && new_speed < skidoo->speed) {\n            skidoo->speed = new_speed < 0 ? 0 : new_speed;\n        } else if (skidoo->speed < 0 && new_speed > skidoo->speed) {\n            skidoo->speed = new_speed > 0 ? 0 : new_speed;\n        }\n\n        if (skidoo->speed < M_MAX_BACK) {\n            skidoo->speed = M_MAX_BACK;\n        }\n    }\n\n    return collide;\n}\n\nint32_t Skidoo_UserControl(\n    ITEM *const skidoo, const int32_t height, int32_t *const out_pitch)\n{\n    SKIDOO_INFO *const skidoo_data = skidoo->priv;\n\n    bool drive = false;\n\n    if (skidoo->pos.y >= height - STEP_L) {\n        *out_pitch = skidoo->speed + (height - skidoo->pos.y);\n\n        if (skidoo->speed == 0 && g_Input.look) {\n            Lara_Look_UpDown();\n        }\n\n        if ((g_Input.left && !g_Input.back)\n            || (g_Input.right && g_Input.back)) {\n            skidoo_data->skidoo_turn -= M_TURN;\n            CLAMPL(skidoo_data->skidoo_turn, -SKIDOO_MAX_TURN);\n        }\n\n        if ((g_Input.right && !g_Input.back)\n            || (g_Input.left && g_Input.back)) {\n            skidoo_data->skidoo_turn += M_TURN;\n            CLAMPG(skidoo_data->skidoo_turn, SKIDOO_MAX_TURN);\n        }\n\n        if (g_Input.back) {\n            if (skidoo->speed > 0) {\n                skidoo->speed -= M_BRAKE;\n            } else {\n                if (skidoo->speed > M_MAX_BACK) {\n                    skidoo->speed += M_REVERSE;\n                }\n                drive = true;\n            }\n        } else if (g_Input.forward) {\n            int32_t max_speed;\n            if (g_Input.action && !M_IsArmed(skidoo_data)) {\n                max_speed = SKIDOO_FAST_SPEED;\n            } else if (g_Input.slow) {\n                max_speed = SKIDOO_SLOW_SPEED;\n            } else {\n                max_speed = SKIDOO_MAX_SPEED;\n            }\n\n            if (skidoo->speed < max_speed) {\n                skidoo->speed +=\n                    M_ACCELERATION * skidoo->speed / (2 * max_speed)\n                    + M_ACCELERATION / 2;\n            } else if (skidoo->speed > max_speed + M_SLOWDOWN) {\n                skidoo->speed -= M_SLOWDOWN;\n            }\n\n            drive = true;\n        } else if (\n            skidoo->speed >= 0 && skidoo->speed < SKIDOO_MIN_SPEED\n            && (g_Input.left || g_Input.right)) {\n            skidoo->speed = SKIDOO_MIN_SPEED;\n            drive = true;\n        } else if (skidoo->speed > M_SLOWDOWN) {\n            skidoo->speed -= M_SLOWDOWN;\n            if ((Random_GetDraw() & 0x7F) < skidoo->speed) {\n                drive = true;\n            }\n        } else {\n            skidoo->speed = 0;\n        }\n    } else if (g_Input.forward || g_Input.back) {\n        drive = true;\n        *out_pitch = skidoo_data->pitch + 50;\n    }\n\n    return drive;\n}\n\nint32_t Skidoo_CheckGetOffOK(int32_t direction)\n{\n    ITEM *const skidoo = Lara_Vehicle_GetItem();\n\n    int16_t rot;\n    if (direction == LARA_STATE_SKIDOO_GET_OFF_L) {\n        rot = skidoo->rot.y + DEG_90;\n    } else {\n        rot = skidoo->rot.y - DEG_90;\n    }\n\n    const XYZ_32 pos = XYZ_32_OffsetYaw(skidoo->pos, rot, -M_GET_OFF_DIST);\n\n    int16_t room_num = skidoo->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n    const int32_t height = Room_GetHeight(sector, pos);\n    const HEIGHT_TYPE height_type = Room_GetHeightType();\n\n    if (height_type == HT_BIG_SLOPE || height_type == HT_DIAGONAL\n        || height == NO_HEIGHT) {\n        return false;\n    }\n\n    if (ABS(height - skidoo->pos.y) > WALL_L / 2) {\n        return false;\n    }\n\n    const int32_t ceiling = Room_GetCeiling(sector, pos);\n    if (ceiling - skidoo->pos.y > -LARA_HEIGHT) {\n        return false;\n    }\n    if (height - ceiling < LARA_HEIGHT) {\n        return false;\n    }\n\n    return true;\n}\n\nvoid Skidoo_Animation(\n    ITEM *const skidoo, const int32_t collide, const int32_t dead)\n{\n    const SKIDOO_INFO *const skidoo_data = skidoo->priv;\n    ITEM *const lara_item = Lara_GetItem();\n\n    if (skidoo->pos.y != skidoo->floor && skidoo->fall_speed > 0\n        && lara_item->current_anim_state != LARA_STATE_SKIDOO_FALL && !dead) {\n        Item_SwitchToObjAnim(lara_item, LA_SKIDOO_FALL, 0, O_LARA_SKIDOO);\n        lara_item->goal_anim_state = LARA_STATE_SKIDOO_FALL;\n        lara_item->current_anim_state = LARA_STATE_SKIDOO_FALL;\n        return;\n    }\n\n    if (collide != 0 && !dead\n        && lara_item->current_anim_state != LARA_STATE_SKIDOO_FALL) {\n        if (lara_item->current_anim_state != LARA_STATE_SKIDOO_HIT) {\n            if (collide == LA_SKIDOO_HIT_FRONT) {\n                Sound_Effect(SFX_CLATTER_2, &skidoo->pos, SPM_NORMAL);\n            } else {\n                Sound_Effect(SFX_CLATTER_1, &skidoo->pos, SPM_NORMAL);\n            }\n            Item_SwitchToObjAnim(lara_item, collide, 0, O_LARA_SKIDOO);\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_HIT;\n            lara_item->current_anim_state = LARA_STATE_SKIDOO_HIT;\n        }\n        return;\n    }\n\n    switch (lara_item->current_anim_state) {\n    case LARA_STATE_SKIDOO_SIT:\n        if (skidoo->speed == 0) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_STILL;\n        }\n        if (dead) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_FALLOFF;\n        } else if (g_Input.left) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_LEFT;\n        } else if (g_Input.right) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_RIGHT;\n        }\n        break;\n\n    case LARA_STATE_SKIDOO_LEFT:\n        if (!g_Input.left) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT;\n        }\n        break;\n\n    case LARA_STATE_SKIDOO_RIGHT:\n        if (!g_Input.right) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT;\n        }\n        break;\n\n    case LARA_STATE_SKIDOO_FALL:\n        if (skidoo->fall_speed <= 0 || skidoo_data->left_fallspeed <= 0\n            || skidoo_data->right_fallspeed <= 0) {\n            Sound_Effect(SFX_CLATTER_3, &skidoo->pos, SPM_NORMAL);\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT;\n        } else if (skidoo->fall_speed > DAMAGE_START + DAMAGE_LENGTH) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_LET_GO;\n        }\n        break;\n\n    case LARA_STATE_SKIDOO_STILL: {\n        const int32_t music_track = Music_ToGameID(\n            M_IsArmed(skidoo_data) ? MX_BATTLE_THEME : MX_SKIDOO_THEME);\n        const uint16_t music_flags = Music_GetTrackFlags(music_track);\n        if (!(music_flags & IF_ONE_SHOT)) {\n            Music_Play_Direct(music_track, MPM_ONCE);\n            Music_SetTrackFlags(music_track, music_flags | IF_ONE_SHOT);\n        }\n\n        if (dead) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_DEATH;\n            return;\n        }\n\n        lara_item->goal_anim_state = LARA_STATE_SKIDOO_STILL;\n\n        if (g_Input.jump) {\n            if (g_Input.right\n                && Skidoo_CheckGetOffOK(LARA_STATE_SKIDOO_GET_OFF_R)) {\n                lara_item->goal_anim_state = LARA_STATE_SKIDOO_GET_OFF_R;\n                skidoo->speed = 0;\n            } else if (\n                g_Input.left\n                && Skidoo_CheckGetOffOK(LARA_STATE_SKIDOO_GET_OFF_L)) {\n                lara_item->goal_anim_state = LARA_STATE_SKIDOO_GET_OFF_L;\n                skidoo->speed = 0;\n            }\n        } else if (g_Input.left) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_LEFT;\n        } else if (g_Input.right) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_RIGHT;\n        } else if (g_Input.back || g_Input.forward) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_SIT;\n        }\n        break;\n    }\n\n    default:\n        break;\n    }\n}\n\nvoid Skidoo_Explode(const ITEM *const skidoo)\n{\n    const int16_t effect_num = Effect_Create(skidoo->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->pos.x = skidoo->pos.x;\n        effect->pos.y = skidoo->pos.y;\n        effect->pos.z = skidoo->pos.z;\n        effect->speed = 0;\n        effect->frame_num = 0;\n        effect->counter = 0;\n        effect->object_id = O_EXPLOSION_1;\n    }\n\n    Item_Explode(Item_GetIndex(skidoo), ~(SKIDOO_GUN_MESH - 1), 0);\n    Sound_Effect(SFX_EXPLOSION_1, nullptr, SPM_NORMAL);\n    Lara_Vehicle_SetIndex(NO_ITEM);\n}\n\nbool Skidoo_CheckGetOff(void)\n{\n    ITEM *const skidoo = Lara_Vehicle_GetItem();\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    if ((lara_item->current_anim_state == LARA_STATE_SKIDOO_GET_OFF_R\n         || lara_item->current_anim_state == LARA_STATE_SKIDOO_GET_OFF_L)\n        && Item_TestFrameEqual(lara_item, LF_SKIDOO_EXIT_END)) {\n        if (lara_item->current_anim_state == LARA_STATE_SKIDOO_GET_OFF_L) {\n            lara_item->rot.y += DEG_90;\n        } else {\n            lara_item->rot.y -= DEG_90;\n        }\n        Item_SwitchToAnim(lara_item, LA(LA_STAND_STILL), 0);\n        lara_item->goal_anim_state = LS(LS_STOP);\n        lara_item->current_anim_state = LS(LS_STOP);\n        lara_item->pos.x -=\n            (M_GET_OFF_DIST * Math_Sin(lara_item->rot.y)) >> W2V_SHIFT;\n        lara_item->pos.z -=\n            (M_GET_OFF_DIST * Math_Cos(lara_item->rot.y)) >> W2V_SHIFT;\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n        Lara_Vehicle_SetIndex(NO_ITEM);\n        lara->gun_status = LGS_ARMLESS;\n        return true;\n    }\n\n    if (lara_item->current_anim_state == LARA_STATE_SKIDOO_LET_GO\n        && (skidoo->pos.y == skidoo->floor\n            || Item_TestFrameEqual(lara_item, LF_SKIDOO_LET_GO_END))) {\n        Item_SwitchToAnim(lara_item, LA(LA_FREEFALL), 0);\n        lara_item->current_anim_state = LARA_STATE_SKIDOO_GET_OFF_R;\n        if (skidoo->pos.y == skidoo->floor) {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_STILL;\n            lara_item->fall_speed = DAMAGE_START + DAMAGE_LENGTH;\n            lara_item->speed = 0;\n            Skidoo_Explode(skidoo);\n        } else {\n            lara_item->goal_anim_state = LARA_STATE_SKIDOO_GET_OFF_R;\n            lara_item->pos.y -= 200;\n            lara_item->fall_speed = skidoo->fall_speed;\n            lara_item->speed = skidoo->speed;\n            Sound_Effect(SFX_LARA_FALL, &lara_item->pos, SPM_NORMAL);\n        }\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n        lara_item->gravity = true;\n        lara->gun_status = LGS_ARMLESS;\n        lara->move_angle = skidoo->rot.y;\n        skidoo->flags |= IF_ONE_SHOT;\n        skidoo->collidable = 0;\n        return false;\n    }\n\n    return true;\n}\n\nvoid Skidoo_Guns(void)\n{\n    WEAPON_INFO *const weapon = &g_Weapons[LGT_SKIDOO];\n    const ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    Gun_GetNewTarget(weapon);\n    Gun_AimWeapon(weapon, &lara->right_arm);\n\n    if (!g_Input.action) {\n        return;\n    }\n\n    int16_t angles[2];\n    angles[0] = lara->right_arm.rot.y + lara_item->rot.y;\n    angles[1] = lara->right_arm.rot.x;\n\n    if (!Gun_FireWeapon(LGT_SKIDOO, lara->target, lara_item, angles)) {\n        return;\n    }\n\n    lara->right_arm.flash_gun = weapon->flash_time;\n    Sound_Effect(weapon->sample_num, &lara_item->pos, SPM_NORMAL);\n    Gun_AddDynamicLight();\n\n    ITEM *const skidoo = Lara_Vehicle_GetItem();\n    Creature_Effect(skidoo, &g_Skidoo_LeftGun, Spawn_GunShot);\n    Creature_Effect(skidoo, &g_Skidoo_RightGun, Spawn_GunShot);\n}\n\nbool Skidoo_Control(void)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    ITEM *const skidoo = Lara_Vehicle_GetItem();\n    SKIDOO_INFO *const skidoo_data = skidoo->priv;\n    int32_t collide = Skidoo_Dynamics(skidoo);\n\n    XYZ_32 fl;\n    XYZ_32 fr;\n    const int32_t hfl = Skidoo_TestHeight(skidoo, M_FRONT, -M_SIDE, &fl);\n    const int32_t hfr = Skidoo_TestHeight(skidoo, M_FRONT, M_SIDE, &fr);\n\n    int16_t room_num = skidoo->room_num;\n    const SECTOR *const sector = Room_GetSector(skidoo->pos, &room_num);\n    int32_t height = Room_GetHeight(sector, skidoo->pos);\n\n    bool dead = false;\n    if (lara_item->hit_points <= 0) {\n        dead = true;\n        g_Input.back = 0;\n        g_Input.forward = 0;\n        g_Input.left = 0;\n        g_Input.right = 0;\n    } else if (lara_item->current_anim_state == LARA_STATE_SKIDOO_LET_GO) {\n        dead = true;\n        collide = 0;\n    }\n\n    int32_t drive;\n    int32_t pitch;\n    if (skidoo->flags & IF_ONE_SHOT) {\n        drive = 0;\n        collide = 0;\n    } else {\n        switch (lara_item->current_anim_state) {\n        case LARA_STATE_SKIDOO_GET_ON:\n        case LARA_STATE_SKIDOO_GET_OFF_L:\n        case LARA_STATE_SKIDOO_GET_OFF_R:\n        case LARA_STATE_SKIDOO_LET_GO:\n            drive = -1;\n            collide = 0;\n            break;\n\n        default:\n            drive = Skidoo_UserControl(skidoo, height, &pitch);\n            break;\n        }\n    }\n\n    const int32_t old_track_mesh = skidoo_data->track_mesh;\n    if (drive > 0) {\n        skidoo_data->track_mesh = (skidoo_data->track_mesh & 3) == 1 ? 2 : 1;\n        skidoo_data->pitch += (pitch - skidoo_data->pitch) >> 2;\n\n        const int32_t pitch_delta =\n            (SKIDOO_MAX_SPEED - skidoo_data->pitch) * 100;\n\n        Sound_Effect(\n            SFX_SKIDOO_MOVING, &skidoo->pos,\n            SPM_PITCH | ((SOUND_DEFAULT_PITCH - pitch_delta) << 8));\n    } else {\n        skidoo_data->track_mesh = 0;\n        if (!drive) {\n            Sound_Effect(SFX_SKIDOO_IDLE, &skidoo->pos, SPM_NORMAL);\n        }\n        skidoo_data->pitch = 0;\n    }\n    skidoo_data->track_mesh |= old_track_mesh & SKIDOO_GUN_MESH;\n\n    skidoo->floor = height;\n\n    skidoo_data->left_fallspeed =\n        M_DoDynamics(hfl, skidoo_data->left_fallspeed, &fl.y);\n    skidoo_data->right_fallspeed =\n        M_DoDynamics(hfr, skidoo_data->right_fallspeed, &fr.y);\n    skidoo->fall_speed =\n        M_DoDynamics(height, skidoo->fall_speed, &skidoo->pos.y);\n\n    height = (fr.y + fl.y) / 2;\n    const int16_t x_rot = Math_Atan(M_FRONT, skidoo->pos.y - height);\n    const int16_t z_rot = Math_Atan(M_SIDE, height - fl.y);\n    skidoo->rot.x += (x_rot - skidoo->rot.x) >> 1;\n    skidoo->rot.z += (z_rot - skidoo->rot.z) >> 1;\n\n    if (skidoo->flags & IF_ONE_SHOT) {\n        Room_TestTriggers(lara_item);\n        Item_UpdateRoom(Item_GetIndex(skidoo), room_num);\n        if (skidoo->pos.y == skidoo->floor) {\n            Skidoo_Explode(skidoo);\n        }\n        return false;\n    }\n\n    Skidoo_Animation(skidoo, collide, dead);\n    Item_UpdateRoom(Item_GetIndex(skidoo), room_num);\n    Item_UpdateRoom(Item_GetIndex(lara_item), room_num);\n\n    if (lara_item->current_anim_state == LARA_STATE_SKIDOO_FALLOFF) {\n        lara_item->rot.x = 0;\n        lara_item->rot.z = 0;\n    } else {\n        lara_item->pos.x = skidoo->pos.x;\n        lara_item->pos.y = skidoo->pos.y;\n        lara_item->pos.z = skidoo->pos.z;\n        lara_item->rot.y = skidoo->rot.y;\n        if (drive >= 0) {\n            lara_item->rot.x = skidoo->rot.x;\n            lara_item->rot.z = skidoo->rot.z;\n        } else {\n            lara_item->rot.x = 0;\n            lara_item->rot.z = 0;\n        }\n    }\n    Room_TestTriggers(lara_item);\n\n    Item_Animate(lara_item);\n    if (!dead && drive >= 0 && M_IsArmed(skidoo_data)) {\n        Skidoo_Guns();\n    }\n\n    if (dead) {\n        Item_SwitchToObjAnim(skidoo, LA_SKIDOO_DEAD, 0, O_SKIDOO_FAST);\n    } else {\n        const int16_t lara_anim_num =\n            Item_GetRelativeObjAnim(lara_item, O_LARA_SKIDOO);\n        const int16_t lara_frame_num = Item_GetRelativeFrame(lara_item);\n        Item_SwitchToObjAnim(\n            skidoo, lara_anim_num, lara_frame_num, O_SKIDOO_FAST);\n    }\n\n    if (skidoo->speed != 0 && skidoo->floor == skidoo->pos.y) {\n        Skidoo_DoSnowEffect(skidoo);\n        if (skidoo->speed < SKIDOO_SLOW_SPEED) {\n            Skidoo_DoSnowEffect(skidoo);\n        }\n    }\n\n    return Skidoo_CheckGetOff();\n}\n\nbool Skidoo_Draw(const ITEM *const item)\n{\n    int32_t track_mesh_status = 0;\n    const SKIDOO_INFO *const skidoo_data = item->priv;\n    if (skidoo_data != nullptr) {\n        track_mesh_status = skidoo_data->track_mesh;\n    }\n\n    const OBJECT *obj = Object_Get(item->object_id);\n    if ((track_mesh_status & SKIDOO_GUN_MESH) != 0) {\n        obj = Object_Get(O_SKIDOO_ARMED);\n    }\n\n    const OBJECT *const track_obj = Object_Get(O_SKIDOO_TRACK);\n    const OBJECT_MESH *track_mesh = nullptr;\n    if ((track_mesh_status & 3) == 1) {\n        track_mesh = Object_GetMesh(track_obj->mesh_idx + 1);\n    } else if ((track_mesh_status & 3) == 2) {\n        track_mesh = Object_GetMesh(track_obj->mesh_idx + 7);\n    }\n\n    // TODO: merge common code parts down below with Object_DrawAnimatingItem.\n\n    ANIM_FRAME *frames[2];\n    int32_t rate;\n    const int32_t frac = Item_GetFrames(item, frames, &rate);\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(item->interp.result.pos);\n    Matrix_Rot16(item->interp.result.rot);\n\n    const CLIP clip = Output_CheckBoundsClip(&frames[0]->bounds);\n    if (clip == CLIP_NOT_VISIBLE) {\n        Matrix_Pop();\n        return false;\n    }\n\n    Output_CalculateObjectLighting(item, &frames[0]->bounds);\n\n    if (frac) {\n        Matrix_InitInterpolate(frac, rate);\n        Matrix_TranslateRel16_ID(frames[0]->offset, frames[1]->offset);\n        Matrix_Rot16_ID(frames[0]->mesh_rots[0], frames[1]->mesh_rots[0]);\n\n        Object_DrawMesh(obj->mesh_idx, clip, true);\n        for (int32_t mesh_idx = 1; mesh_idx < obj->mesh_count; mesh_idx++) {\n            const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n            if (bone->matrix_pop) {\n                Matrix_Pop_I();\n            }\n            if (bone->matrix_push) {\n                Matrix_Push_I();\n            }\n\n            Matrix_TranslateRel32_I(bone->pos);\n            Matrix_Rot16_ID(\n                frames[0]->mesh_rots[mesh_idx], frames[1]->mesh_rots[mesh_idx]);\n\n            if (mesh_idx == 1 && track_mesh != nullptr) {\n                Output_DrawObjectMesh_I(track_mesh, clip);\n            } else {\n                Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, true);\n            }\n        }\n    } else {\n        Matrix_TranslateRel16(frames[0]->offset);\n        Matrix_Rot16(frames[0]->mesh_rots[0]);\n\n        Object_DrawMesh(obj->mesh_idx, clip, false);\n        for (int32_t mesh_idx = 1; mesh_idx < obj->mesh_count; mesh_idx++) {\n            const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1);\n            if (bone->matrix_pop) {\n                Matrix_Pop();\n            }\n            if (bone->matrix_push) {\n                Matrix_Push();\n            }\n\n            Matrix_TranslateRel32(bone->pos);\n            Matrix_Rot16(frames[0]->mesh_rots[mesh_idx]);\n\n            if (mesh_idx == 1 && track_mesh != nullptr) {\n                Output_DrawObjectMesh(track_mesh, clip);\n            } else {\n                Object_DrawMesh(obj->mesh_idx + mesh_idx, clip, false);\n            }\n        }\n    }\n\n    Matrix_Pop();\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/skidoo_common.h",
    "content": "#pragma once\n\n#include <trx/game/collision.h>\n#include <trx/game/creature/types.h>\n#include <trx/game/items/types.h>\n#include <trx/game/objects/types.h>\n\n#define SKIDOO_MIN_SPEED 15\n#define SKIDOO_MAX_SPEED 100\n#define SKIDOO_SLOW_SPEED 50\n#define SKIDOO_FAST_SPEED 150\n\n#define SKIDOO_MAX_TURN (DEG_1 * 6) // = 1092\n#define SKIDOO_GUN_MESH 4\n\ntypedef struct {\n    int16_t track_mesh;\n    int32_t skidoo_turn;\n    int32_t left_fallspeed;\n    int32_t right_fallspeed;\n    int16_t momentum_angle;\n    int16_t extra_rotation;\n    int32_t pitch;\n} SKIDOO_INFO;\n\nextern BITE g_Skidoo_LeftGun;\nextern BITE g_Skidoo_RightGun;\n\nvoid Skidoo_Initialise(int16_t item_num);\nint32_t Skidoo_CheckGetOn(int16_t item_num, COLL_INFO *coll);\nvoid Skidoo_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll);\nvoid Skidoo_BaddieCollision(ITEM *skidoo);\nint32_t Skidoo_TestHeight(\n    const ITEM *item, int32_t z_off, int32_t x_off, XYZ_32 *out_pos);\nvoid Skidoo_DoSnowEffect(const ITEM *skidoo);\nint32_t Skidoo_Dynamics(ITEM *skidoo);\nint32_t Skidoo_UserControl(ITEM *skidoo, int32_t height, int32_t *out_pitch);\nint32_t Skidoo_CheckGetOffOK(int32_t direction);\nvoid Skidoo_Animation(ITEM *skidoo, int32_t collide, int32_t dead);\nvoid Skidoo_Explode(const ITEM *skidoo);\nbool Skidoo_CheckGetOff(void);\nvoid Skidoo_Guns(void);\nbool Skidoo_Control(void);\nbool Skidoo_Draw(const ITEM *item);\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/skidoo_fast.c",
    "content": "#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/vehicles/skidoo_common.h>\n\nstatic void M_PrivLoad(ITEM *const item, JSON_READ_IO *const io)\n{\n    SKIDOO_INFO *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"track_mesh\", &p->track_mesh));\n    JSON_SHOULD(JSON_READ(io, \"skidoo_turn\", &p->skidoo_turn));\n    JSON_SHOULD(JSON_READ(io, \"left_fallspeed\", &p->left_fallspeed));\n    JSON_SHOULD(JSON_READ(io, \"right_fallspeed\", &p->right_fallspeed));\n    JSON_SHOULD(JSON_READ(io, \"momentum_angle\", &p->momentum_angle));\n    JSON_SHOULD(JSON_READ(io, \"extra_rotation\", &p->extra_rotation));\n    JSON_SHOULD(JSON_READ(io, \"pitch\", &p->pitch));\n}\n\nstatic void M_PrivSave(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const SKIDOO_INFO *const p = item->priv;\n    JSONW_WRITE(io, \"track_mesh\", p->track_mesh);\n    JSONW_WRITE(io, \"skidoo_turn\", p->skidoo_turn);\n    JSONW_WRITE(io, \"left_fallspeed\", p->left_fallspeed);\n    JSONW_WRITE(io, \"right_fallspeed\", p->right_fallspeed);\n    JSONW_WRITE(io, \"momentum_angle\", p->momentum_angle);\n    JSONW_WRITE(io, \"extra_rotation\", p->extra_rotation);\n    JSONW_WRITE(io, \"pitch\", p->pitch);\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->initialise_func = Skidoo_Initialise;\n    obj->draw_func = Skidoo_Draw;\n    obj->collision_func = Skidoo_Collision;\n    obj->priv_size = sizeof(SKIDOO_INFO);\n    obj->priv_load_func = M_PrivLoad;\n    obj->priv_save_func = M_PrivSave;\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_SKIDOO_FAST, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/upv.c",
    "content": "#include <trx/game/objects/vehicles/upv.h>\n\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/los.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/spawn.h>\n#include <trx/game/stats.h>\n\n// clang-format off\n#define M_ACCELERATION     0x40000\n#define M_FRICTION         0x18000\n#define M_MAX_SPEED        0x400000\n#define M_ROT_ACCELERATION 0x400000\n#define M_ROT_SLOWACCEL    0x200000\n#define M_ROT_FRICTION     0x100000\n#define M_MAX_ROTATION     0x1C00000\n#define M_UPDOWN_ACCEL     0x16C0000\n#define M_UPDOWN_FRICTION  0xB60000\n#define M_MAX_UPDOWN       0x16C0000\n#define M_CAM_ELEVATION    (DEG_1 * -60) // = -10920\n// clang-format on\n\nstatic const BITE m_UPVBites[6] = {\n    { .pos = { .x = 0, .y = 0, .z = 0 }, .mesh_num = 3 },\n    { .pos = { .x = 0, .y = 96, .z = 256 }, .mesh_num = 0 },\n    { .pos = { .x = -128, .y = 0, .z = -64 }, .mesh_num = 1 },\n    { .pos = { .x = 0, .y = 0, .z = -64 }, .mesh_num = 1 },\n    { .pos = { .x = 128, .y = 0, .z = -64 }, .mesh_num = 2 },\n    { .pos = { .x = 0, .y = 0, .z = -64 }, .mesh_num = 2 },\n};\n\ntypedef struct {\n    int32_t vel;\n    int32_t rot;\n    int32_t rot_x;\n    int16_t fan_rot;\n    struct {\n        bool control;\n        bool surface;\n        bool dive;\n        bool dead;\n    } flags;\n    int8_t weapon_timer;\n    bool current_weapon; // left|right\n} M_PRIV;\n\ntypedef enum {\n    // clang-format off\n    M_STATE_DEATH           = 0,\n    M_STATE_GET_OFF_SURFACE = 2,\n    M_STATE_MOVE            = 4,\n    M_STATE_POSE            = 5,\n    M_STATE_GET_ON          = 8,\n    M_STATE_GET_OFF         = 9,\n    // clang-format on\n} M_STATE;\n\ntypedef enum {\n    // clang-format off\n    M_ANIM_DEATH            = 0,\n    M_ANIM_IDLE             = 5,\n    M_ANIM_GET_OFF_SURFACE  = 9,\n    M_ANIM_GET_ON_SURFACE   = 10,\n    M_ANIM_GET_ON_SURFACE_1 = 11,\n    M_ANIM_GET_OFF          = 12,\n    M_ANIM_GET_ON           = 13,\n    // clang-format on\n} M_ANIM;\n\nstatic void M_LoadPriv(ITEM *const item, JSON_READ_IO *const io)\n{\n    M_PRIV *const p = item->priv;\n    JSON_SHOULD(JSON_READ(io, \"vel\", &p->vel));\n    JSON_SHOULD(JSON_READ(io, \"rot\", &p->rot));\n    JSON_SHOULD(JSON_READ(io, \"rot_x\", &p->rot_x));\n    JSON_SHOULD(JSON_READ(io, \"fan_rot\", &p->fan_rot));\n    JSON_SHOULD(JSON_READ(io, \"weapon_timer\", &p->weapon_timer));\n    JSON_SHOULD(JSON_READ(io, \"current_weapon\", &p->current_weapon));\n    if (JSON_SHOULD(JSON_PUSH(io, \"flags\"))) {\n        JSON_SHOULD(JSON_READ(io, \"control\", &p->flags.control));\n        JSON_SHOULD(JSON_READ(io, \"surface\", &p->flags.surface));\n        JSON_SHOULD(JSON_READ(io, \"dive\", &p->flags.dive));\n        JSON_SHOULD(JSON_READ(io, \"dead\", &p->flags.dead));\n        JSON_POP(io);\n    }\n}\n\nstatic void M_SavePriv(const ITEM *const item, JSON_WRITE_IO *const io)\n{\n    const M_PRIV *const p = item->priv;\n    JSONW_WRITE(io, \"vel\", p->vel);\n    JSONW_WRITE(io, \"rot\", p->rot);\n    JSONW_WRITE(io, \"rot_x\", p->rot_x);\n    JSONW_WRITE(io, \"fan_rot\", p->fan_rot);\n    JSONW_WRITE(io, \"weapon_timer\", p->weapon_timer);\n    JSONW_WRITE(io, \"current_weapon\", p->current_weapon);\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"control\", p->flags.control);\n    JSONW_WRITE(io, \"surface\", p->flags.surface);\n    JSONW_WRITE(io, \"dive\", p->flags.dive);\n    JSONW_WRITE(io, \"dead\", p->flags.dead);\n    JSONW_POP_AND_SET(io, \"flags\");\n}\n\nstatic void M_Initialise(int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n    p->flags.surface = true;\n}\n\nstatic bool M_CanGetOn(const ITEM *const item)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n    if (!g_Input.action || lara->gun_status != LGS_ARMLESS\n        || lara_item->gravity) {\n        return false;\n    }\n\n    if (ABS(lara_item->pos.y - item->pos.y + 128) > 256) {\n        return false;\n    }\n\n    const int32_t dist = XYZ_32_GetLength2((XYZ_32) {\n        .x = lara_item->pos.x - item->pos.x,\n        .y = 0,\n        .z = lara_item->pos.z - item->pos.z,\n    });\n    if (dist > SQUARE(WALL_L / 2)) {\n        return false;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    const int32_t h = Room_GetHeight(sector, item->pos);\n    if (h < -32000) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool M_CanGetOff(const ITEM *const item)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    M_PRIV *const p = item->priv;\n    if (lara->current.vel.x || lara->current.vel.z || p->vel) {\n        return false;\n    }\n\n    const int32_t rad = WALL_L * Math_Cos(item->rot.x) >> W2V_SHIFT;\n    XYZ_32 pos = {\n        .x = item->pos.x\n            + ((rad * Math_Sin(item->rot.y + DEG_180)) >> W2V_SHIFT),\n        .y = item->pos.y - ((WALL_L * Math_Sin(item->rot.x)) >> W2V_SHIFT),\n        .z = item->pos.z\n            + ((rad * Math_Cos(item->rot.y + DEG_180)) >> W2V_SHIFT),\n    };\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(pos, &room_num);\n\n    const int32_t h = Room_GetHeight(sector, pos);\n    if (h == NO_HEIGHT || pos.y > h) {\n        return false;\n    }\n\n    const int32_t c = Room_GetCeiling(sector, pos);\n    if (h - c < STEP_L || pos.y < c || c == NO_HEIGHT) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic void M_GetOn(ITEM *const item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ITEM *const lara_item = Lara_GetItem();\n\n    Lara_Vehicle_SetIndex(Item_GetIndex(item));\n    lara->water_status = LWS_ABOVE_WATER;\n\n    if (lara->gun_type == LGT_FLARE) {\n        Lara_Flare_Dispose(false);\n        lara->flare.control = false;\n        lara->gun_type = LGT_UNARMED;\n        lara->request_gun_type = LGT_UNARMED;\n    }\n\n    lara->gun_status = LGS_HANDS_BUSY;\n    // item->hit_points = 1; // TODO: why is it set?\n    lara_item->pos = item->pos;\n    lara_item->rot.y = item->rot.y;\n\n    if (lara_item->current_anim_state == LS(LS_SURF_TREAD)\n        || lara_item->current_anim_state == LS(LS_SURF_SWIM)) {\n        Item_SwitchToObjAnim(\n            lara_item, M_ANIM_GET_ON_SURFACE, 0, O_LARA_VEHICLE_ANIM);\n    } else {\n        Item_SwitchToObjAnim(lara_item, M_ANIM_GET_ON, 0, O_LARA_VEHICLE_ANIM);\n    }\n\n    lara_item->goal_anim_state = M_STATE_GET_ON;\n    lara_item->current_anim_state = M_STATE_GET_ON;\n    Item_Animate(lara_item);\n\n    if (item->status != IS_ACTIVE) {\n        item->status = IS_ACTIVE;\n        Item_AddActive(Item_GetIndex(item));\n    }\n}\n\nstatic void M_Collision(\n    int16_t item_num, ITEM *const lara_item, COLL_INFO *coll)\n{\n    ITEM *const item = Item_Get(item_num);\n    if (lara_item->hit_points < 0 || Lara_Vehicle_GetItem() != nullptr) {\n        return;\n    }\n\n    if (M_CanGetOn(item)) {\n        M_GetOn(item);\n    } else {\n        item->pos.y += 128;\n        if (Lara_TestBoundsCollide(item, coll->radius)\n            && Collide_TestCollision(item, lara_item)) {\n            Lara_Col_ItemPush(item, coll, false, false);\n        }\n        item->pos.y -= 128;\n    }\n}\n\nstatic bool M_Draw(const ITEM *const item)\n{\n    int32_t rate;\n    ANIM_FRAME *frames[2];\n    const int32_t frac = Item_GetFrames(item, frames, &rate);\n\n    OBJECT *const obj = Object_Get(item->object_id);\n    Output_DrawShadow(256, &frames[0]->bounds, item);\n\n    Matrix_Push();\n    Matrix_TranslateAbs32((XYZ_32) {\n        item->interp.result.pos.x,\n        item->interp.result.pos.y + 128,\n        item->interp.result.pos.z,\n    });\n    Matrix_Rot16(item->interp.result.rot);\n\n    bool result = false;\n    const CLIP clip = Output_CheckBoundsClip(&frames[0]->bounds);\n    if (clip == CLIP_NOT_VISIBLE) {\n        goto finish;\n    }\n\n    M_PRIV *const p = item->priv;\n    Output_CalculateObjectLighting(item, &frames[0]->bounds);\n    const ANIM_BONE *const bone = Anim_GetBone(obj->bone_idx);\n\n    if (frac != 0) {\n        Matrix_InitInterpolate(frac, rate);\n        Matrix_TranslateRel16_ID(frames[0]->offset, frames[1]->offset);\n        Matrix_Rot16_ID(frames[0]->mesh_rots[0], frames[1]->mesh_rots[0]);\n        Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx), clip);\n\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[0].pos);\n        Matrix_Rot16_ID(frames[0]->mesh_rots[1], frames[1]->mesh_rots[1]);\n        Matrix_RotX_I((item->rot.z + (p->rot_x >> 13)));\n        Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx + 1), clip);\n        Matrix_Pop_I();\n\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[1].pos);\n        Matrix_Rot16_ID(frames[0]->mesh_rots[2], frames[1]->mesh_rots[2]);\n        Matrix_RotX_I(((p->rot_x >> 13) - item->rot.z));\n        Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx + 2), clip);\n        Matrix_Pop_I();\n\n        Matrix_Push_I();\n        Matrix_TranslateRel32_I(bone[2].pos);\n        Matrix_Rot16_ID(frames[0]->mesh_rots[3], frames[1]->mesh_rots[3]);\n        Matrix_RotZ_I(p->fan_rot);\n        Output_DrawObjectMesh_I(Object_GetMesh(obj->mesh_idx + 3), clip);\n        Matrix_Pop_I();\n    } else {\n        Matrix_TranslateRel16(frames[0]->offset);\n        Matrix_Rot16(frames[0]->mesh_rots[0]);\n        Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx), clip);\n\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[0].pos);\n        Matrix_Rot16(frames[0]->mesh_rots[1]);\n        Matrix_RotX((item->rot.z + (p->rot_x >> 13)));\n        Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx + 1), clip);\n        Matrix_Pop();\n\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[1].pos);\n        Matrix_Rot16(frames[0]->mesh_rots[2]);\n        Matrix_RotX(((p->rot_x >> 13) - item->rot.z));\n        Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx + 2), clip);\n        Matrix_Pop();\n\n        Matrix_Push();\n        Matrix_TranslateRel32(bone[2].pos);\n        Matrix_Rot16(frames[0]->mesh_rots[3]);\n        Matrix_RotZ(p->fan_rot);\n        Output_DrawObjectMesh(Object_GetMesh(obj->mesh_idx + 3), clip);\n        Matrix_Pop();\n    }\n    result = true;\n\nfinish:\n    Matrix_Pop();\n    return result;\n}\n\nstatic void M_UserInput(\n    ITEM *const item, ITEM *const lara_item, M_PRIV *const p)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    XYZ_32 pos;\n    GAME_VECTOR start_pos;\n    GAME_VECTOR target_pos;\n    int32_t water_height;\n    int16_t anim, frame;\n\n    M_CanGetOff(item);\n    anim = Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM);\n    frame = Item_GetRelativeFrame(lara_item);\n\n    switch (lara_item->current_anim_state) {\n    case M_STATE_DEATH:\n        if (anim == M_ANIM_DEATH && (frame == 16 || frame == 17)) {\n            pos.x = 0;\n            pos.y = 0;\n            pos.z = 0;\n            Lara_GetMeshPos(LM_HIPS, &pos);\n\n            lara_item->pos = pos;\n            Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_DEATH), 0);\n            lara_item->current_anim_state = LS_UW_DEATH;\n            lara_item->goal_anim_state = LS_UW_DEATH;\n            lara_item->fall_speed = 0;\n            lara_item->gravity = false;\n            lara_item->rot.x = 0;\n            lara_item->rot.z = 0;\n            p->flags.dead = true;\n        }\n\n        item->speed = 0;\n        break;\n\n    case M_STATE_GET_OFF_SURFACE:\n        if (anim == M_ANIM_GET_OFF_SURFACE && frame == 51) {\n            pos.x = 0;\n            pos.y = 0;\n            pos.z = 0;\n            water_height =\n                Room_GetWaterHeight(lara_item->pos, lara_item->room_num);\n\n            int32_t water_surface_dist;\n            if (water_height == NO_HEIGHT) {\n                water_surface_dist = NO_HEIGHT;\n            } else {\n                water_surface_dist = lara_item->pos.y - water_height;\n            }\n\n            Lara_GetMeshPos(LM_HIPS, &pos);\n            lara_item->pos = pos;\n            Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_TO_ONWATER), 0);\n            lara_item->current_anim_state = LS_SURF_TREAD;\n            lara_item->goal_anim_state = LS_SURF_TREAD;\n            lara_item->fall_speed = 0;\n            lara_item->gravity = false;\n            lara_item->rot.x = 0;\n            lara_item->rot.z = 0;\n            Lara_UpdateRoomToHeight(-381);\n            lara->water_status = LWS_SURFACE;\n            lara->water_surface_dist = -water_surface_dist;\n            lara->dive_timer = 11;\n            lara->torso_rot.x = 0;\n            lara->torso_rot.y = 0;\n            lara->head_rot.x = 0;\n            lara->head_rot.y = 0;\n            lara->gun_status = LGS_ARMLESS;\n            Lara_Vehicle_SetIndex(NO_ITEM);\n            item->hit_points = 0;\n        }\n        break;\n\n    case M_STATE_MOVE:\n        if (lara_item->hit_points <= 0) {\n            lara_item->goal_anim_state = 0;\n            break;\n        }\n\n        if (g_Input.left) {\n            p->rot -= M_ROT_ACCELERATION;\n        } else if (g_Input.right) {\n            p->rot += M_ROT_ACCELERATION;\n        }\n\n        if (p->flags.surface) {\n            if (item->rot.x > 9100) {\n                item->rot.x -= 182;\n            } else if (item->rot.x < 9100) {\n                item->rot.x += 182;\n            }\n        } else if (g_Input.forward) {\n            p->rot_x -= M_UPDOWN_ACCEL;\n        } else if (g_Input.back) {\n            p->rot_x += M_UPDOWN_ACCEL;\n        }\n\n        if (g_Input.jump) {\n            if (p->flags.surface && g_Input.forward && item->rot.x > -2730) {\n                p->flags.dive = true;\n            }\n\n            p->vel += M_ACCELERATION;\n        } else {\n            lara_item->goal_anim_state = M_STATE_POSE;\n        }\n        break;\n\n    case M_STATE_POSE:\n        if (lara_item->hit_points <= 0) {\n            lara_item->goal_anim_state = 0;\n            break;\n        }\n\n        if (g_Input.left) {\n            p->rot -= M_ROT_SLOWACCEL;\n        } else if (g_Input.right) {\n            p->rot += M_ROT_SLOWACCEL;\n        }\n\n        if (p->flags.surface) {\n            if (item->rot.x > 9100) {\n                item->rot.x -= 182;\n            } else if (item->rot.x < 9100) {\n                item->rot.x += 182;\n            }\n        } else if (g_Input.forward) {\n            p->rot_x -= M_UPDOWN_ACCEL;\n        } else if (g_Input.back) {\n            p->rot_x += M_UPDOWN_ACCEL;\n        }\n\n        if (g_Input.roll && M_CanGetOff(item)) {\n            if (p->flags.surface) {\n                lara_item->goal_anim_state = M_STATE_GET_OFF_SURFACE;\n            } else {\n                lara_item->goal_anim_state = M_STATE_GET_OFF;\n            }\n\n            p->flags.control = false;\n            Sound_StopEffect(SFX_UPV_LOOP);\n            Sound_Effect(SFX_UPV_STOP, &item->pos, SPM_ALWAYS);\n        } else if (g_Input.jump) {\n            if (p->flags.surface && g_Input.forward && item->rot.x > -2730) {\n                p->flags.dive = true;\n            }\n\n            lara_item->goal_anim_state = M_STATE_MOVE;\n        }\n        break;\n\n    case M_STATE_GET_ON:\n        if (anim == M_ANIM_GET_ON_SURFACE_1) {\n            item->rot.x += 182;\n            item->pos.y += 4;\n\n            if (frame == 30) {\n                Sound_Effect(SFX_UPV_START, &item->pos, SPM_ALWAYS);\n            }\n\n            if (frame == 50) {\n                p->flags.control = true;\n            }\n        } else if (anim == M_ANIM_GET_ON) {\n            if (frame == 30) {\n                Sound_Effect(SFX_UPV_START, &item->pos, SPM_ALWAYS);\n            }\n\n            if (frame == 42) {\n                p->flags.control = true;\n            }\n        }\n        break;\n\n    case M_STATE_GET_OFF:\n        if (anim == M_ANIM_GET_OFF && frame == 42) {\n            pos.x = 0;\n            pos.y = 0;\n            pos.z = 0;\n            Lara_GetMeshPos(LM_HIPS, &pos);\n\n            start_pos.pos = item->pos;\n            start_pos.room_num = item->room_num;\n            target_pos.pos = pos;\n            target_pos.room_num = item->room_num;\n            Camera_LOSCheck(&start_pos, &target_pos, 0);\n\n            lara_item->pos = target_pos.pos;\n            Item_SwitchToAnim(lara_item, LA(LA_UNDERWATER_IDLE), 0);\n            lara_item->current_anim_state = LS_TREAD;\n            lara_item->fall_speed = 0;\n            lara_item->gravity = false;\n            lara_item->rot.x = 0;\n            lara_item->rot.z = 0;\n            Lara_UpdateRoomToHeight(0);\n            lara->water_status = LWS_UNDERWATER;\n            lara->gun_status = LGS_ARMLESS;\n            Lara_Vehicle_SetIndex(NO_ITEM);\n            item->hit_points = 0;\n        }\n        break;\n    }\n\n    if (p->flags.dive) {\n        if (item->rot.x > -2730) {\n            item->rot.x -= 910;\n        } else {\n            p->flags.dive = false;\n        }\n    }\n\n    if (p->vel > 0) {\n        p->vel -= M_FRICTION;\n        CLAMPL(p->vel, 0);\n\n    } else if (p->vel < 0) {\n        p->vel += M_FRICTION;\n        CLAMPG(p->vel, 0);\n    }\n\n    CLAMP(p->vel, -M_MAX_SPEED, M_MAX_SPEED);\n\n    if (p->rot > 0) {\n        p->rot -= M_ROT_FRICTION;\n        CLAMPL(p->rot, 0);\n    } else if (p->rot < 0) {\n        p->rot += M_ROT_FRICTION;\n        CLAMPG(p->rot, 0);\n    }\n\n    if (p->rot_x > 0) {\n        p->rot_x -= M_UPDOWN_FRICTION;\n        CLAMPL(p->rot_x, 0);\n    } else if (p->rot_x < 0) {\n        p->rot_x += M_UPDOWN_FRICTION;\n        CLAMPG(p->rot_x, 0);\n    }\n\n    CLAMP(p->rot, -M_MAX_ROTATION, M_MAX_ROTATION);\n    CLAMP(p->rot_x, -M_MAX_UPDOWN, M_MAX_UPDOWN);\n}\n\nstatic void M_DoCurrent(ITEM *const item)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (lara->current.active != 0) {\n        const int32_t sink_val = lara->current.active - 1;\n        const OBJECT_VECTOR *const camera_obj = Camera_GetFixedObject(sink_val);\n        const int32_t angle = -Math_Atan(\n                                  lara_item->pos.x - camera_obj->pos.x,\n                                  lara_item->pos.z - camera_obj->pos.z)\n            - DEG_90;\n        const int32_t speed = camera_obj->data;\n        const int32_t xvel = (speed * Math_Sin(angle)) >> 4;\n        const int32_t zvel = (speed * Math_Cos(angle)) >> 4;\n        lara->current.vel.x += ((xvel - lara->current.vel.x) >> 4);\n        lara->current.vel.z += ((zvel - lara->current.vel.z) >> 4);\n    } else {\n        int32_t shifter;\n\n        int32_t abs_vel = ABS(lara->current.vel.x);\n        if (abs_vel > 16) {\n            shifter = 4;\n        } else if (abs_vel > 8) {\n            shifter = 3;\n        } else {\n            shifter = 2;\n        }\n        lara->current.vel.x -= lara->current.vel.x >> shifter;\n        if (ABS(lara->current.vel.x) < 4) {\n            lara->current.vel.x = 0;\n        }\n\n        abs_vel = ABS(lara->current.vel.z);\n        if (abs_vel > 16) {\n            shifter = 4;\n        } else if (abs_vel > 8) {\n            shifter = 3;\n        } else {\n            shifter = 2;\n        }\n        lara->current.vel.z -= lara->current.vel.z >> shifter;\n        if (ABS(lara->current.vel.z) < 4) {\n            lara->current.vel.z = 0;\n        }\n\n        if (lara->current.vel.x == 0 && lara->current.vel.z == 0) {\n            return;\n        }\n    }\n\n    item->pos.x += lara->current.vel.x >> 8;\n    item->pos.z += lara->current.vel.z >> 8;\n    lara->current.active = false;\n}\n\nstatic void M_FireHarpoon(ITEM *const item)\n{\n    M_PRIV *const p = item->priv;\n    AMMO_INFO *const ammo = Gun_GetAmmoInfo(LGT_HARPOON);\n    if (ammo->ammo <= 0) {\n        return;\n    }\n\n    const int16_t item_num = Item_Create();\n    if (item_num == NO_ITEM) {\n        return;\n    }\n\n    ITEM *const bolt = Item_Get(item_num);\n    bolt->object_id = O_HARPOON_BOLT;\n    bolt->shade.value_1 = -0x3DF0;\n    bolt->room_num = item->room_num;\n    XYZ_32 pos = {\n        .x = p->current_weapon != 0 ? 22 : -22,\n        .y = 24,\n        .z = 230,\n    };\n    Collide_GetJointAbsPosition(item, &pos, 3);\n    bolt->pos = pos;\n    Item_Initialise(item_num);\n    bolt->rot.x = item->rot.x;\n    bolt->rot.y = item->rot.y;\n    bolt->rot.z = 0;\n    bolt->fall_speed = (-256 * Math_Sin(bolt->rot.x)) >> W2V_SHIFT;\n    bolt->speed = (256 * Math_Cos(bolt->rot.x)) >> W2V_SHIFT;\n    bolt->hit_points = 256;\n    // bolt->item_flags[0] = 1; // TODO: what\n    Item_AddActive(item_num);\n    Sound_Effect(SFX_UPV_HARPOON, &Lara_GetItem()->pos, SPM_ALWAYS);\n\n    if (!Game_IsBonusFlagSet(GBF_NGPLUS)) {\n        ammo->ammo--;\n    }\n\n    Stats_AddAmmoUsed();\n    p->current_weapon ^= 1;\n}\n\nstatic void M_BackgroundCollision(\n    ITEM *const item, const ITEM *const lara_item, M_PRIV *const p)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    COLL_INFO coll = {\n        .bad_pos = -NO_HEIGHT,\n        .bad_neg = -400,\n        .bad_ceiling = 400,\n        .old = item->pos,\n        .radius = 300,\n        .slopes_are_walls = false,\n        .slopes_are_pits = false,\n        .lava_is_pit = false,\n        .enable_hit = false,\n        .enable_baddie_push = true,\n    };\n\n    if (item->rot.x < -DEG_90 || item->rot.x > DEG_90) {\n        lara->move_angle = item->rot.y + DEG_180;\n    } else {\n        lara->move_angle = item->rot.y;\n    }\n\n    coll.facing = lara->move_angle;\n    int32_t h = (WALL_L * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n    h = ABS(h);\n    CLAMPL(h, 200);\n\n    coll.bad_neg = -h;\n    Collide_GetCollisionInfo(\n        &coll, item->pos.x, item->pos.y + h / 2, item->pos.z, item->room_num,\n        h);\n    Collide_ShiftItem(item, &coll);\n\n    switch (coll.coll_type) {\n    case COLL_FRONT:\n        if (p->rot_x > 0x1FFE0000) {\n            p->rot_x += 0x16C0000;\n        } else if (p->rot_x < -0x1FFE0000) {\n            p->rot_x -= 0x16C0000;\n        } else {\n            p->vel = 0;\n        }\n        break;\n\n    case COLL_TOP:\n        if (p->rot_x >= -0x1FFE0000) {\n            p->rot_x -= 0x16C0000;\n        }\n        break;\n\n    case COLL_TOP_FRONT:\n        p->vel = 0;\n        break;\n\n    case COLL_LEFT:\n        item->rot.y += 910;\n        break;\n\n    case COLL_RIGHT:\n        item->rot.y -= 910;\n        break;\n\n    case COLL_CLAMP:\n        item->pos = coll.old;\n        p->vel = 0;\n        return;\n    }\n\n    if (coll.side_mid.floor < 0) {\n        item->pos.y += coll.side_mid.floor;\n        p->rot_x += 0x16C0000;\n    }\n}\n\nstatic void M_TriggerMist(\n    const XYZ_32 pos, const int32_t speed, const int16_t angle)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 64;\n    spark->dst_color.g = 64;\n    spark->dst_color.b = 64;\n\n    spark->fade_to_black = 12;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 4;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 20;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n\n    spark->vel.x = (Random_GetControl() & 0x7F)\n        + ((speed * Math_Sin(angle)) >> (W2V_SHIFT + 2)) - 64;\n    spark->vel.y = 0;\n    spark->vel.z = (Random_GetControl() & 0x7F)\n        + ((speed * Math_Cos(angle)) >> (W2V_SHIFT + 2)) - 64;\n\n    spark->friction = 3;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    spark->dst_size.width = ((Random_GetControl() & 7) + (speed >> 1) + 16);\n    spark->dst_size.height = spark->dst_size.width;\n    spark->src_size.width = spark->dst_size.width >> 2;\n    spark->src_size.height = spark->src_size.width;\n    spark->size.width = spark->src_size.width;\n    spark->size.height = spark->src_size.height;\n    Sparks_FinishSetup(spark);\n}\n\nstatic void M_Control(int16_t item_num)\n{\n    XYZ_32 pos;\n    GAME_VECTOR start_pos;\n    GAME_VECTOR target_pos;\n    int32_t c;\n\n    ITEM *const item = Item_Get(item_num);\n    M_PRIV *const p = item->priv;\n\n    if (Lara_Vehicle_GetItem() == item) {\n        if (p->vel) {\n            p->fan_rot += (p->vel >> 12);\n            pos = m_UPVBites[0].pos;\n            Collide_GetJointAbsPosition(item, &pos, m_UPVBites[0].mesh_num);\n            M_TriggerMist(\n                (XYZ_32) { pos.x, pos.y + 128, pos.z }, ABS(p->vel) >> 16,\n                item->rot.y + DEG_180);\n\n            if (!(Random_GetControl() & 1)) {\n                XYZ_32 bubble_pos = {\n                    .x = pos.x + (Random_GetControl() & 0x3F) - 32,\n                    .y = pos.y + 128,\n                    .z = pos.z + (Random_GetControl() & 0x3F) - 32,\n                };\n                int16_t room_num = item->room_num;\n                Room_GetSector(bubble_pos, &room_num);\n                Spawn_BubbleEx(&bubble_pos, room_num, 4, 8);\n            }\n        } else {\n            p->fan_rot += 364;\n        }\n    }\n\n    for (int32_t i = 0; i < 2; i++) {\n        pos = (XYZ_32) {\n            .x = m_UPVBites[1].pos.x,\n            .y = m_UPVBites[1].pos.y,\n            .z = m_UPVBites[1].pos.z << (6 * i),\n        };\n        Collide_GetJointAbsPosition(item, &pos, m_UPVBites[1].mesh_num);\n        c = 255 - (Random_GetControl() & 0x1F);\n\n        if (i == 1) {\n            target_pos.pos = pos;\n            target_pos.room_num = item->room_num;\n            LOS_Check(&start_pos, &target_pos, true);\n            pos = target_pos.pos;\n        } else {\n            start_pos.pos = pos;\n            start_pos.room_num = item->room_num;\n        }\n\n        Output_AddDynamicLightRGB(pos, 8 * i + 16, (RGB_888) { c, c, c });\n    }\n\n    if (p->weapon_timer > 0) {\n        p->weapon_timer--;\n    }\n}\n\nbool UPV_Control(void)\n{\n    ITEM *const item = Lara_Vehicle_GetItem();\n    ITEM *const lara_item = Lara_GetItem();\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    M_PRIV *const p = item->priv;\n\n    if (!p->flags.dead) {\n        M_UserInput(item, lara_item, p);\n        item->speed = p->vel >> 16;\n        item->rot.x += p->rot_x >> 16;\n        item->rot.y += p->rot >> 16;\n        item->rot.z = (int16_t)(p->rot >> 12);\n\n        if (item->rot.x > 14560) {\n            item->rot.x = 14560;\n        } else if (item->rot.x < -14560) {\n            item->rot.x = -14560;\n        }\n\n        item->pos.x += (Math_Cos(item->rot.x)\n                        * ((item->speed * Math_Sin(item->rot.y)) >> W2V_SHIFT))\n            >> W2V_SHIFT;\n        item->pos.y -= (item->speed * Math_Sin(item->rot.x)) >> W2V_SHIFT;\n        item->pos.z += (Math_Cos(item->rot.x)\n                        * ((item->speed * Math_Cos(item->rot.y)) >> W2V_SHIFT))\n            >> W2V_SHIFT;\n    }\n\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n    item->floor = Room_GetHeight(sector, item->pos);\n\n    if (p->flags.control && !p->flags.dead) {\n        const int32_t water_height =\n            Room_GetWaterHeightEx(item->pos, room_num, false);\n\n        if (water_height != NO_HEIGHT\n            && !Room_Get(item->room_num)->flags.underwater) {\n            if (water_height - item->pos.y >= -210) {\n                item->pos.y = water_height + 210;\n            }\n\n            if (!p->flags.surface) {\n                Sound_Effect(SFX_LARA_BREATH, &lara_item->pos, SPM_ALWAYS);\n                p->flags.dive = false;\n            }\n\n            p->flags.surface = true;\n        } else if (\n            water_height != NO_HEIGHT && water_height - item->pos.y >= -210) {\n            item->pos.y = water_height + 210;\n\n            if (!p->flags.surface) {\n                Sound_Effect(SFX_LARA_BREATH, &lara_item->pos, SPM_ALWAYS);\n                p->flags.dive = false;\n            }\n\n            p->flags.surface = true;\n        } else {\n            p->flags.surface = false;\n        }\n\n        if (p->flags.surface) {\n            if (lara_item->hit_points >= 0) {\n                lara->air += 10;\n                CLAMPG(lara->air, LARA_MAX_AIR);\n            }\n        } else if (lara_item->hit_points >= 0) {\n            lara->air--;\n            if (lara->air < 0) {\n                lara->air = -1;\n                lara_item->hit_points -= 5;\n            }\n        }\n    }\n\n    Room_TestTriggers(lara_item);\n    Room_TestTriggers(item);\n\n    if (Lara_Vehicle_GetItem() == nullptr) {\n        if (!p->flags.dead) {\n            return false;\n        }\n    } else if (!p->flags.dead) {\n        M_DoCurrent(item);\n\n        if (g_Input.action && p->flags.control && p->weapon_timer == 0) {\n            M_FireHarpoon(item);\n            p->weapon_timer = 15;\n        }\n\n        if (room_num != item->room_num) {\n            Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num);\n            Item_UpdateRoom(lara->item_num, room_num);\n        }\n\n        lara_item->pos.x = item->pos.x;\n        lara_item->pos.y = item->pos.y + 128;\n        lara_item->pos.z = item->pos.z;\n        lara_item->rot = item->rot;\n        Item_Animate(lara_item);\n        M_BackgroundCollision(item, lara_item, p);\n\n        if (p->flags.control) {\n            Sound_Effect(\n                SFX_UPV_LOOP, &item->pos,\n                (item->speed << 16) | 0x1000000 | SPM_PITCH | SPM_ALWAYS);\n        }\n\n        const int16_t anim_idx =\n            Item_GetRelativeObjAnim(lara_item, O_LARA_VEHICLE_ANIM);\n        const int16_t frame_idx = Item_GetRelativeFrame(lara_item);\n        Item_SwitchToAnim(item, anim_idx, frame_idx);\n        g_Camera.target_elevation = p->flags.surface ? M_CAM_ELEVATION : 0;\n        return true;\n    }\n\n    Item_Animate(lara_item);\n\n    if (room_num != item->room_num) {\n        Item_UpdateRoom(Lara_Vehicle_GetIndex(), room_num);\n    }\n\n    M_BackgroundCollision(item, lara_item, p);\n    p->rot_x = 0;\n    Item_SwitchToAnim(item, M_ANIM_IDLE, 0);\n    item->current_anim_state = M_STATE_POSE;\n    item->goal_anim_state = M_STATE_POSE;\n    item->fall_speed = 0;\n    item->gravity = true;\n    item->speed = 0;\n    Item_Animate(item);\n    return true;\n}\n\nstatic void M_Setup(OBJECT *const obj)\n{\n    obj->priv_size = sizeof(M_PRIV);\n    obj->priv_load_func = M_LoadPriv;\n    obj->priv_save_func = M_SavePriv;\n    obj->initialise_func = M_Initialise;\n    obj->control_func = M_Control;\n    obj->collision_func = M_Collision;\n    obj->draw_func = M_Draw;\n\n    obj->save_position = true;\n    obj->save_flags = true;\n    obj->save_anim = true;\n}\n\nREGISTER_OBJECT(O_UPV, M_Setup)\n"
  },
  {
    "path": "src/trx/game/objects/vehicles/upv.h",
    "content": "#pragma once\n\nbool UPV_Control(void);\n"
  },
  {
    "path": "src/trx/game/objects.h",
    "content": "#pragma once\n\n#include <trx/game/objects/col.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/draw.h>\n#include <trx/game/objects/ids.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/objects/setup.h>\n#include <trx/game/objects/types.h>\n#include <trx/game/objects/vars.h>\n"
  },
  {
    "path": "src/trx/game/option/common.c",
    "content": "#include <trx/game/option/common.h>\n\n#include <trx/game/input.h>\n#include <trx/game/option/controls.h>\n#include <trx/game/option/examine.h>\n#include <trx/game/option/gameplay.h>\n#include <trx/game/option/globe_select.h>\n#include <trx/game/option/graphics.h>\n#include <trx/game/option/passport.h>\n#include <trx/game/option/sound.h>\n#include <trx/game/option/stats.h>\n#include <trx/version.h>\n\nvoid Option_Reset(void)\n{\n    Option_Shutdown();\n}\n\nvoid Option_Shutdown(void)\n{\n    Option_Gameplay_Shutdown();\n    Option_Graphics_Shutdown();\n    Option_Sound_Shutdown();\n    Option_Controls_Shutdown();\n    Option_GlobeSelect_Shutdown();\n}\n\nvoid Option_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    if (inv_item->action == ACTION_EXAMINE) {\n        Option_Examine_Control(inv_item, is_busy);\n        return;\n    }\n\n    switch (inv_item->object_id) {\n    case O_PASSPORT_OPTION:\n        Option_Passport_Control(inv_item, is_busy);\n        break;\n    case O_COMPASS_OPTION:\n    case O_STOPWATCH_OPTION:\n        Option_Stats_Control(inv_item, is_busy);\n        break;\n    case O_PDA_OPTION:\n        Option_Gameplay_Control(inv_item, is_busy);\n        break;\n    case O_DETAIL_OPTION:\n        Option_Graphics_Control(inv_item, is_busy);\n        break;\n    case O_SOUND_OPTION:\n        Option_Sound_Control(inv_item, is_busy);\n        break;\n    case O_CONTROL_OPTION:\n        Option_Controls_Control(inv_item, is_busy);\n        break;\n    case O_GLOBE_SELECT_OPTION:\n        Option_GlobeSelect_Control(inv_item, is_busy);\n        break;\n\n    case O_PISTOL_OPTION:\n    case O_SHOTGUN_OPTION:\n    case O_MAGNUM_OPTION:\n    case O_AUTOS_OPTION:\n    case O_DESERT_EAGLE_OPTION:\n    case O_UZI_OPTION:\n    case O_HARPOON_OPTION:\n    case O_M16_OPTION:\n    case O_MP5_OPTION:\n    case O_GRENADE_GUN_OPTION:\n    case O_ROCKET_GUN_OPTION:\n    case O_EXPLOSIVE_OPTION:\n    case O_SMALL_MEDIPACK_OPTION:\n    case O_LARGE_MEDIPACK_OPTION:\n        if (!is_busy) {\n            g_InputDB.menu_confirm = 1;\n        }\n        break;\n\n    case O_PISTOL_AMMO_OPTION:\n    case O_SHOTGUN_AMMO_OPTION:\n    case O_MAGNUM_AMMO_OPTION:\n    case O_AUTOS_AMMO_OPTION:\n    case O_DESERT_EAGLE_AMMO_OPTION:\n    case O_UZI_AMMO_OPTION:\n    case O_HARPOON_AMMO_OPTION:\n    case O_M16_AMMO_OPTION:\n    case O_MP5_AMMO_OPTION:\n    case O_GRENADE_AMMO_OPTION:\n    case O_ROCKET_AMMO_OPTION:\n        break;\n\n    case O_PUZZLE_OPTION_1:\n    case O_PUZZLE_OPTION_2:\n    case O_PUZZLE_OPTION_3:\n    case O_PUZZLE_OPTION_4:\n    case O_KEY_OPTION_1:\n    case O_KEY_OPTION_2:\n    case O_KEY_OPTION_3:\n    case O_KEY_OPTION_4:\n    case O_QUEST_OPTION_1:\n    case O_QUEST_OPTION_2:\n    case O_QUEST_OPTION_3:\n    case O_QUEST_OPTION_4:\n    case O_PICKUP_OPTION_1:\n    case O_PICKUP_OPTION_2:\n    case O_SCION_OPTION:\n    case O_LEADBAR_OPTION:\n        if (!is_busy) {\n            g_InputDB.menu_confirm = 1;\n        }\n        break;\n\n    default:\n        if (!is_busy && (g_InputDB.menu_confirm || g_InputDB.menu_back)) {\n            inv_item->goal_frame = 0;\n            inv_item->anim_direction = -1;\n        }\n        break;\n    }\n}\n\nvoid Option_Draw(INVENTORY_ITEM *const inv_item)\n{\n    if (inv_item->action == ACTION_EXAMINE) {\n        Option_Examine_Draw();\n        return;\n    }\n\n    switch (inv_item->object_id) {\n    case O_PASSPORT_OPTION:\n        Option_Passport_Draw(inv_item);\n        break;\n    case O_COMPASS_OPTION:\n    case O_STOPWATCH_OPTION:\n        Option_Stats_Draw();\n        break;\n    case O_PDA_OPTION:\n        Option_Gameplay_Draw(inv_item);\n        break;\n    case O_DETAIL_OPTION:\n        Option_Graphics_Draw(inv_item);\n        break;\n    case O_SOUND_OPTION:\n        Option_Sound_Draw(inv_item);\n        break;\n    case O_CONTROL_OPTION:\n        Option_Controls_Draw(inv_item);\n        break;\n    case O_GLOBE_SELECT_OPTION:\n        Option_GlobeSelect_Draw(inv_item);\n        break;\n    default:\n        break;\n    }\n}\n\nvoid Option_Close(const INVENTORY_ITEM *const inv_item)\n{\n    switch (inv_item->object_id) {\n    case O_PASSPORT_OPTION:\n        Option_Passport_Close();\n        break;\n    case O_COMPASS_OPTION:\n    case O_STOPWATCH_OPTION:\n        Option_Stats_Close();\n        break;\n    case O_PDA_OPTION:\n        Option_Gameplay_Close();\n        break;\n    case O_DETAIL_OPTION:\n        Option_Graphics_Close();\n        break;\n    case O_SOUND_OPTION:\n        Option_Sound_Close();\n        break;\n    case O_CONTROL_OPTION:\n        Option_Controls_Close();\n        break;\n    case O_GLOBE_SELECT_OPTION:\n        Option_GlobeSelect_Close();\n        break;\n    default:\n        Option_Examine_Close();\n        break;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/option/common.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_Close(const INVENTORY_ITEM *inv_item);\n\n// Reset internal positioning of option UIs.\nvoid Option_Reset(void);\n\n// Free up resources associated with option UIs.\nvoid Option_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/option/controls.c",
    "content": "#include <trx/game/option/controls.h>\n\n#include <trx/config.h>\n#include <trx/game/ui.h>\n\ntypedef struct {\n    int32_t listeners[2];\n    struct {\n        bool is_ready;\n        UI_CONTROLS_STATE state;\n    } ui;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_HandleKeyChange(const EVENT *event, void *user_data)\n{\n    g_Config.dirty = true;\n    Config_Update();\n}\n\nstatic void M_HandleLayoutChange(const EVENT *event, void *user_data)\n{\n    const M_PRIV *const p = user_data;\n    g_Config.input.layout[p->ui.state.backend] =\n        p->ui.state.editor_state[p->ui.state.backend].active_layout;\n    Config_Update();\n}\n\nstatic void M_Init(M_PRIV *const p)\n{\n    UI_Controls_Init(&p->ui.state);\n    p->ui.is_ready = true;\n    p->listeners[0] = EventManager_Subscribe(\n        p->ui.state.events, \"layout_change\", nullptr, M_HandleLayoutChange, p);\n    p->listeners[1] = EventManager_Subscribe(\n        p->ui.state.events, \"key_change\", nullptr, M_HandleKeyChange, p);\n}\n\nstatic void M_Shutdown(M_PRIV *const p)\n{\n    if (p->ui.is_ready) {\n        EventManager_Unsubscribe(p->ui.state.events, p->listeners[0]);\n        EventManager_Unsubscribe(p->ui.state.events, p->listeners[1]);\n        UI_Controls_Free(&p->ui.state);\n        p->ui.is_ready = false;\n    }\n}\n\nvoid Option_Controls_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    M_PRIV *const p = &m_Priv;\n    if (is_busy) {\n        return;\n    }\n\n    if (!p->ui.is_ready) {\n        M_Init(p);\n    }\n\n    if (!UI_Controls_Control(&p->ui.state)) {\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    }\n}\n\nvoid Option_Controls_Draw(INVENTORY_ITEM *const inv_item)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->ui.is_ready) {\n        UI_Controls(&p->ui.state);\n    }\n}\n\nvoid Option_Controls_Close(void)\n{\n}\n\nvoid Option_Controls_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_Shutdown(p);\n}\n"
  },
  {
    "path": "src/trx/game/option/controls.h",
    "content": "#pragma once\n\n#include <trx/game/input.h>\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_Controls_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Controls_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_Controls_Close(void);\nvoid Option_Controls_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/option/examine.c",
    "content": "#include <trx/game/option/examine.h>\n\n#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/const.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory_ring/control.h>\n#include <trx/game/matrix.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/viewport.h>\n\n#define M_EXAMINE_ROTATION_SPEED (DEG_1 * 3)\n\ntypedef struct {\n    OBJECT_ID object_id;\n    bool is_dialog_hidden;\n    struct {\n        bool is_ready;\n        UI_TEXT_DIALOG_STATE *state;\n    } ui;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_DrawHideDialogFooter(void *const user_data)\n{\n    UI_BeginAnchor(0.5f, 0.5f);\n    UI_LabelFmt(\n        \"\\\\{input look} %s\",\n        user_data != nullptr ? (const char *)user_data : \"\");\n    UI_EndAnchor();\n}\n\nstatic void M_DrawRotateHint(void *const user_data)\n{\n    UI_ButtonLabelEx(\n        g_Config.input.backend == INPUT_BACKEND_KEYBOARD\n            ? GS(\"general/misc/direction_keys_keyboard\")\n            : GS(\"general/misc/direction_keys_controller\"),\n        GS(\"general/actions/rotate\"));\n}\n\nstatic bool M_ShouldShowDialog(const OBJECT_ID obj_id)\n{\n    const char *const description = Object_GetDescription(obj_id);\n    return !String_IsEmpty(description);\n}\n\nstatic int32_t M_GetMaxRows(void)\n{\n    const int32_t res_h = UI_Scaler_CalcInverse(\n        Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT);\n    if (res_h <= 240) {\n        return 5;\n    } else if (res_h <= 384) {\n        return 7;\n    } else {\n        return 12;\n    }\n}\n\nstatic void M_Init(M_PRIV *const p, const OBJECT_ID obj_id)\n{\n    p->object_id = obj_id;\n    p->is_dialog_hidden = false;\n    p->ui.is_ready = true;\n    p->ui.state = UI_TextDialog_Init(\n        UI_GetCanvasWidth() * 2.0 / 3.0f, M_GetMaxRows(), false);\n}\n\nstatic void M_Close(M_PRIV *const p)\n{\n    InvRing_ClearButtonHint();\n    if (p->ui.is_ready) {\n        UI_TextDialog_Free(p->ui.state);\n        p->ui.state = nullptr;\n        p->ui.is_ready = false;\n    }\n}\n\nstatic void M_ApplyExamineRotation(INVENTORY_ITEM *const inv_item)\n{\n    const int32_t yaw_input =\n        (g_Input.menu_left ? 1 : 0) - (g_Input.menu_right ? 1 : 0);\n    const int32_t pitch_input =\n        (g_Input.menu_down ? 1 : 0) - (g_Input.menu_up ? 1 : 0);\n    if (yaw_input == 0 && pitch_input == 0) {\n        return;\n    }\n    inv_item->has_manual_rot = true;\n    MATRIX delta = g_IDMatrix;\n    if (yaw_input != 0) {\n        Matrix_RotY_M(&delta, yaw_input * M_EXAMINE_ROTATION_SPEED);\n    }\n    if (pitch_input != 0) {\n        Matrix_RotX_M(&delta, pitch_input * M_EXAMINE_ROTATION_SPEED);\n    }\n    Matrix_Mul3x3_M(&inv_item->manual_rot, &delta, &inv_item->manual_rot);\n}\n\nbool Option_Examine_CanExamine(const OBJECT_ID obj_id)\n{\n    return Object_GetDescription(obj_id) != nullptr;\n}\n\nvoid Option_Examine_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    M_PRIV *const p = &m_Priv;\n    if (is_busy) {\n        return;\n    }\n\n    const OBJECT_ID obj_id = inv_item->object_id;\n    if (!p->ui.is_ready) {\n        M_Init(p, obj_id);\n    }\n\n    const bool has_dialog = M_ShouldShowDialog(obj_id);\n    const bool show_dialog = has_dialog && !p->is_dialog_hidden;\n    if (show_dialog) {\n        InvRing_ClearButtonHint();\n    } else {\n        InvRing_SetButtonHintDrawer(M_DrawRotateHint, nullptr);\n    }\n    if (show_dialog) {\n        UI_TextDialog_Control(p->ui.state);\n    }\n\n    if (g_InputDB.look) {\n        if (show_dialog) {\n            p->is_dialog_hidden = true;\n            return;\n        } else {\n            g_InputDB.menu_back = true;\n            g_InputDB.menu_confirm = false;\n            inv_item->has_manual_rot = false;\n            p->is_dialog_hidden = false;\n            M_Close(p);\n            return;\n        }\n    }\n\n    if (g_InputDB.menu_back || g_InputDB.menu_confirm) {\n        g_InputDB.menu_back = true;\n        g_InputDB.menu_confirm = false;\n        inv_item->has_manual_rot = false;\n        p->is_dialog_hidden = false;\n        M_Close(p);\n        return;\n    }\n\n    if (!show_dialog) {\n        M_ApplyExamineRotation(inv_item);\n    }\n}\n\nvoid Option_Examine_Draw(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!p->ui.is_ready) {\n        return;\n    }\n\n    if (M_ShouldShowDialog(p->object_id) && !p->is_dialog_hidden) {\n        const char *const footer_label = GS(\"general/actions/hide_dialog\");\n        UI_TextDialogEx(\n            p->ui.state,\n            (UI_TEXT_DIALOG_SETTINGS) {\n                .title_raw = Object_GetName(p->object_id),\n                .text_raw = Object_GetDescription(p->object_id),\n                .footer_func = M_DrawHideDialogFooter,\n                .footer_user_data = (void *)footer_label,\n            });\n    }\n}\n\nvoid Option_Examine_Close(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_Close(p);\n}\n"
  },
  {
    "path": "src/trx/game/option/examine.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nbool Option_Examine_CanExamine(OBJECT_ID obj_id);\nvoid Option_Examine_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Examine_Draw(void);\nvoid Option_Examine_Close(void);\n"
  },
  {
    "path": "src/trx/game/option/gameplay.c",
    "content": "#include <trx/game/option/gameplay.h>\n\n#include <trx/game/input.h>\n#include <trx/game/ui/dialogs/gameplay_settings.h>\n\ntypedef struct {\n    bool is_ready;\n    UI_SETTINGS_DIALOG_STATE *ui_state;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_Init(M_PRIV *const p)\n{\n    p->is_ready = true;\n    if (p->ui_state == nullptr) {\n        p->ui_state = UI_GameplaySettings_Init();\n    }\n}\n\nstatic void M_Shutdown(M_PRIV *const p)\n{\n    if (p->ui_state != nullptr) {\n        UI_GameplaySettings_Free(p->ui_state);\n        p->ui_state = nullptr;\n    }\n    p->is_ready = false;\n}\n\nvoid Option_Gameplay_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    M_PRIV *const p = &m_Priv;\n    if (is_busy) {\n        return;\n    }\n    if (!p->is_ready) {\n        M_Init(p);\n    }\n    if (UI_GameplaySettings_Control(p->ui_state)) {\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n            inv_item->anim_direction = 1;\n            inv_item->goal_frame = inv_item->frames_total - 1;\n        }\n    } else {\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    }\n}\n\nvoid Option_Gameplay_Draw(INVENTORY_ITEM *const inv_item)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->is_ready && p->ui_state != nullptr) {\n        UI_GameplaySettings(p->ui_state);\n    }\n}\n\nvoid Option_Gameplay_Close(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->is_ready = false;\n}\n\nvoid Option_Gameplay_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_Shutdown(p);\n}\n"
  },
  {
    "path": "src/trx/game/option/gameplay.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_Gameplay_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Gameplay_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_Gameplay_Close(void);\nvoid Option_Gameplay_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/option/globe_select.c",
    "content": "#include <trx/game/option/globe_select.h>\n\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n\ntypedef struct {\n    GAME_STRING_ID gs_area_id;\n} M_AREA_STRING_ENTRY;\n\nstatic const M_AREA_STRING_ENTRY m_AreaStrings[] = {\n    { .gs_area_id = GS_ID(\"general/globe_select/area_1\") },\n    { .gs_area_id = GS_ID(\"general/globe_select/area_2\") },\n    { .gs_area_id = GS_ID(\"general/globe_select/area_3\") },\n    { .gs_area_id = GS_ID(\"general/globe_select/area_4\") }, // Unused Peru\n    { .gs_area_id = GS_ID(\"general/globe_select/area_5\") },\n    { .gs_area_id = GS_ID(\"general/globe_select/area_6\") },\n};\n\nstatic int32_t M_GetEntryCount(void)\n{\n    return MIN(g_GameFlow.globe.count, (int32_t)ARRAY_SIZE(m_AreaStrings));\n}\n\nstatic const GF_GLOBE_ENTRY *M_GetEntry(const int32_t idx)\n{\n    const int32_t entry_count = M_GetEntryCount();\n    if (idx < 0 || idx >= entry_count) {\n        return nullptr;\n    }\n    return &g_GameFlow.globe.entries[idx];\n}\n\nstatic bool M_IsLevelCompleted(const int32_t level_ordinal)\n{\n    if (level_ordinal < 0) {\n        return false;\n    }\n    const GF_LEVEL *const level =\n        GF_GetLevelByOrdinalNumber(GFLT_MAIN, level_ordinal);\n    if (level == nullptr) {\n        return false;\n    }\n    const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    return resume != nullptr && resume->level_completed;\n}\n\nstatic int32_t M_GetNextSelectableIndex(\n    const INV_RING *const ring, const int32_t direction)\n{\n    if (direction == 0) {\n        return ring->globe_select.selection;\n    }\n\n    const int32_t entry_count = M_GetEntryCount();\n    if (entry_count <= 0) {\n        return ring->globe_select.selection;\n    }\n\n    for (int32_t step = 0; step < entry_count; step++) {\n        int32_t idx = ring->globe_select.selection + direction * (step + 1);\n        while (idx < 0 && entry_count != 0) {\n            idx += entry_count;\n        }\n        idx %= entry_count;\n        if (ring->globe_select.selectable[idx]) {\n            return idx;\n        }\n    }\n    return ring->globe_select.selection;\n}\n\nstatic bool M_UpdateRotAxis(int16_t *const cur, const int16_t target)\n{\n    int16_t ang = target - *cur;\n    if (ang >= 128 || ang <= -128) {\n        *cur += ang >> 3;\n        return false;\n    }\n    *cur = target;\n    return true;\n}\n\nstatic bool M_IsAligned(INV_RING *const ring)\n{\n    const GF_GLOBE_ENTRY *const entry =\n        M_GetEntry(ring->globe_select.selection);\n    if (entry == nullptr) {\n        return true;\n    }\n    int32_t axes = 0;\n    axes += M_UpdateRotAxis(&ring->globe_select.rot.x, entry->rot.x) ? 1 : 0;\n    axes += M_UpdateRotAxis(&ring->globe_select.rot.y, entry->rot.y) ? 1 : 0;\n    axes += M_UpdateRotAxis(&ring->globe_select.rot.z, entry->rot.z) ? 1 : 0;\n    return axes == 3;\n}\n\nint32_t Option_GlobeSelect_AreaFromMeshIdx(const int32_t mesh_idx)\n{\n    const int32_t entry_count = M_GetEntryCount();\n    for (int32_t i = 0; i < entry_count; i++) {\n        if (g_GameFlow.globe.entries[i].mesh_idx == mesh_idx) {\n            return (int32_t)i;\n        }\n    }\n    return -1;\n}\n\nvoid Option_GlobeSelect_UpdateSelectable(INV_RING *const ring)\n{\n    ring->globe_select.selection = -1;\n    ring->globe_select.rot.x = 0;\n    ring->globe_select.rot.y = 0;\n    ring->globe_select.rot.z = 0;\n    ring->globe_select.meshes_drawn = 0x0FFFu;\n    ring->globe_select.confirmed = false;\n    for (int32_t i = 0; i < MAX_GLOBE_ZONES; i++) {\n        ring->globe_select.selectable[i] = false;\n        ring->globe_select.start_level_num[i] = -1;\n    }\n\n    uint32_t completed_mask = 0u;\n    const int32_t entry_count = M_GetEntryCount();\n    for (int32_t i = 0; i < entry_count; i++) {\n        if (M_IsLevelCompleted(\n                g_GameFlow.globe.entries[i].completion_level_ordinal)) {\n            completed_mask |= 1u << i;\n        }\n    }\n\n    int32_t selectable_count = 0;\n    for (int32_t i = 0; i < entry_count; i++) {\n        ring->globe_select.selectable[i] = false;\n        ring->globe_select.start_level_num[i] = -1;\n\n        const GF_GLOBE_ENTRY *const entry = &g_GameFlow.globe.entries[i];\n\n        const GF_LEVEL *const start_level =\n            GF_GetLevelByOrdinalNumber(GFLT_MAIN, entry->start_level_ordinal);\n        if (start_level != nullptr) {\n            ring->globe_select.start_level_num[i] = start_level->num;\n        }\n\n        if ((completed_mask & (1u << i)) != 0u) {\n            continue;\n        }\n        if ((completed_mask & entry->prereq_mask) != entry->prereq_mask) {\n            continue;\n        }\n\n        if (start_level == nullptr) {\n            continue;\n        }\n\n        ring->globe_select.selectable[i] = true;\n        selectable_count++;\n    }\n\n    ring->globe_select.meshes_drawn = 0x0FFFu;\n    for (int32_t i = 0; i < entry_count; i++) {\n        if (ring->globe_select.start_level_num[i] < 0) {\n            ring->globe_select.meshes_drawn &=\n                ~(1 << g_GameFlow.globe.entries[i].mesh_idx);\n        }\n    }\n\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, selectable_count > 1);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, selectable_count > 1);\n\n    if (ring->globe_select.selection < 0\n        || ring->globe_select.selection >= entry_count\n        || !ring->globe_select.selectable[ring->globe_select.selection]) {\n        ring->globe_select.selection = -1;\n        for (int32_t i = 0; i < entry_count; i++) {\n            if (ring->globe_select.selectable[i]) {\n                ring->globe_select.selection = i;\n                break;\n            }\n        }\n    }\n}\n\nvoid Option_GlobeSelect_Control(\n    INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    INV_RING *const ring = InvRing_GetActiveRing();\n    if (ring == nullptr || ring->mode != INV_GLOBE_SELECT_MODE) {\n        return;\n    }\n\n    Overlay_SetTopText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_GS_KEY,\n        .fmt_gs_key = GS_ID(\"general/inventory_ring/heading_fmt\"),\n        .literal = GS_ID(\"general/inventory_ring/heading_adventure\"),\n    });\n\n    if (ring->globe_select.selection < 0) {\n        Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n        ring->globe_select.confirmed = false;\n        return;\n    }\n\n    const bool aligned = M_IsAligned(ring);\n\n    if (aligned && !is_busy) {\n        if (g_Input.menu_left) {\n            ring->globe_select.selection = M_GetNextSelectableIndex(ring, -1);\n        } else if (g_Input.menu_right) {\n            ring->globe_select.selection = M_GetNextSelectableIndex(ring, 1);\n        }\n    }\n\n    const int32_t entry_count = M_GetEntryCount();\n    if (ring->globe_select.selection >= 0\n        && ring->globe_select.selection < entry_count\n        && ring->globe_select.selection < (int32_t)ARRAY_SIZE(m_AreaStrings)) {\n        Overlay_SetBottomText((OVERLAY_TEXT) {\n            .kind = UI_OVERLAY_TEXT_GS_KEY,\n            .fmt_gs_key = GS_ID(\"general/inventory_ring/object_name_fmt\"),\n            .literal = m_AreaStrings[ring->globe_select.selection].gs_area_id,\n        });\n    } else {\n        Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n    }\n\n    if (g_InputDB.menu_confirm && !is_busy) {\n        if (!aligned) {\n            g_InputDB.menu_confirm = false;\n            return;\n        }\n        ring->globe_select.confirmed = true;\n    }\n}\n\nvoid Option_GlobeSelect_Draw(INVENTORY_ITEM *const inv_item)\n{\n}\n\nvoid Option_GlobeSelect_Close(void)\n{\n}\n\nvoid Option_GlobeSelect_Shutdown(void)\n{\n}\n"
  },
  {
    "path": "src/trx/game/option/globe_select.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_GlobeSelect_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_GlobeSelect_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_GlobeSelect_Close(void);\nvoid Option_GlobeSelect_Shutdown(void);\n\nvoid Option_GlobeSelect_UpdateSelectable(INV_RING *ring);\nint32_t Option_GlobeSelect_AreaFromMeshIdx(int32_t mesh_idx);\n"
  },
  {
    "path": "src/trx/game/option/graphics.c",
    "content": "#include <trx/game/option/graphics.h>\n\n#include <trx/game/input.h>\n#include <trx/game/ui/dialogs/graphic_settings.h>\n\ntypedef struct {\n    UI_SETTINGS_DIALOG_STATE *ui_state;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_Init(M_PRIV *const p)\n{\n    if (p->ui_state == nullptr) {\n        p->ui_state = UI_GraphicSettings_Init();\n    }\n}\n\nstatic void M_Shutdown(M_PRIV *const p)\n{\n    if (p->ui_state != nullptr) {\n        UI_GraphicSettings_Free(p->ui_state);\n        p->ui_state = nullptr;\n    }\n}\n\nvoid Option_Graphics_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    M_PRIV *const p = &m_Priv;\n    if (is_busy) {\n        return;\n    }\n    if (p->ui_state == nullptr) {\n        M_Init(p);\n    }\n    if (!UI_GraphicSettings_Control(p->ui_state)) {\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    }\n}\n\nvoid Option_Graphics_Draw(INVENTORY_ITEM *const inv_item)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->ui_state != nullptr) {\n        UI_GraphicSettings(p->ui_state);\n    }\n}\n\nvoid Option_Graphics_Close(void)\n{\n}\n\nvoid Option_Graphics_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_Shutdown(p);\n}\n"
  },
  {
    "path": "src/trx/game/option/graphics.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_Graphics_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Graphics_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_Graphics_Close(void);\nvoid Option_Graphics_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/option/passport.c",
    "content": "#include <trx/game/option/passport.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell/common.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n#include <trx/version.h>\n\n#define M_IMMEDIATE (g_TRVersion >= 2)\n\ntypedef enum {\n    M_ROLE_PLAY_ANY_LEVEL_SELECT_LEVEL,\n    M_ROLE_PLAY_ANY_LEVEL_SELECT_MODE,\n    M_ROLE_PLAY_PREV_LEVEL_SELECT_SLOT,\n    M_ROLE_PLAY_PREV_LEVEL_SELECT_LEVEL,\n    M_ROLE_SWITCH_MOD,\n    M_ROLE_STORY_SO_FAR,\n    M_ROLE_STORY_SO_FAR_CONFIRM,\n    M_ROLE_NEW_GAME,\n    M_ROLE_LOAD_GAME,\n    M_ROLE_SAVE_GAME,\n    M_ROLE_RESTART_LEVEL,\n    M_ROLE_EXIT_TO_TITLE,\n    M_ROLE_EXIT_GAME,\n} M_PAGE_ROLE;\n\ntypedef struct {\n    GAME_STRING_ID title;\n    bool (*func)(INVENTORY_ITEM *inv_item);\n    bool flat;\n} M_PAGE_HANDLER;\n\ntypedef enum {\n    PAGE_UNDETERMINED = -1,\n    PAGE_1 = 0,\n    PAGE_2 = 1,\n    PAGE_3 = 2,\n    PAGE_COUNT = 3,\n} M_PAGE_NUMBER;\n\ntypedef enum {\n    M_MODE_BROWSE,\n    M_MODE_PICK_OPTION,\n} M_PAGE_MODE;\n\ntypedef struct {\n    M_PAGE_ROLE role;\n    int32_t selection;\n\n    struct {\n        UI_NEW_GAME_STATE *new_game;\n        UI_SELECT_LEVEL_DIALOG_STATE *select_level;\n        UI_PLAY_ANY_LEVEL_DIALOG_STATE *play_any_level;\n        UI_SAVE_SLOT_DIALOG_STATE *save_slot;\n        UI_SWITCH_MOD_DIALOG_STATE *switch_mod;\n    } ui;\n} M_NAV_FRAME;\n\ntypedef struct {\n    bool available;\n    M_PAGE_ROLE role;\n\n    struct { // Hierarchical navigation\n        int32_t depth;\n        M_NAV_FRAME stack[4];\n        M_NAV_FRAME *current;\n    } nav;\n} M_PAGE;\n\nPASSPORT g_Passport = {\n    .select_level = -1,\n};\n\nstatic struct {\n    M_PAGE_MODE mode;\n    M_PAGE pages[PAGE_COUNT];\n    M_PAGE_NUMBER current_page;\n    M_PAGE_NUMBER active_page;\n    GAME_STRING_ID error_msg;\n} m_Priv = {\n    .active_page = PAGE_UNDETERMINED,\n};\n\nstatic void M_ResetNavStack(M_PAGE *const page)\n{\n    ASSERT(page != nullptr);\n    page->nav.depth = 0;\n    page->nav.current = &page->nav.stack[page->nav.depth];\n    page->nav.current->selection = -1;\n    page->nav.current->role = page->role;\n}\n\nstatic void M_InitText(void)\n{\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, false);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, false);\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n}\n\nstatic void M_FreeDialogs(M_NAV_FRAME *const frame)\n{\n    if (frame->ui.select_level != nullptr) {\n        UI_SelectLevelDialog_Free(frame->ui.select_level);\n        frame->ui.select_level = nullptr;\n    }\n    if (frame->ui.play_any_level != nullptr) {\n        UI_PlayAnyLevelDialog_Free(frame->ui.play_any_level);\n        frame->ui.play_any_level = nullptr;\n    }\n    if (frame->ui.save_slot != nullptr) {\n        UI_SaveSlotDialog_Free(frame->ui.save_slot);\n        frame->ui.save_slot = nullptr;\n    }\n    if (frame->ui.switch_mod != nullptr) {\n        UI_SwitchModDialog_Free(frame->ui.switch_mod);\n        frame->ui.switch_mod = nullptr;\n    }\n    if (frame->ui.new_game != nullptr) {\n        UI_NewGame_Free(frame->ui.new_game);\n        frame->ui.new_game = nullptr;\n    }\n}\n\nstatic void M_FreeAllDialogs(void)\n{\n    for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n        for (int32_t j = 0; j <= m_Priv.pages[i].nav.depth; j++) {\n            M_FreeDialogs(&m_Priv.pages[i].nav.stack[j]);\n        }\n    }\n}\n\nstatic void M_RemoveAllText(void)\n{\n    m_Priv.error_msg = nullptr;\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, false);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, false);\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n}\n\nstatic M_PAGE *M_TryGetActivePage(void)\n{\n    if (m_Priv.active_page < 0 || m_Priv.active_page >= PAGE_COUNT) {\n        return nullptr;\n    }\n    return &m_Priv.pages[m_Priv.active_page];\n}\n\nstatic M_PAGE *M_GetActivePage(void)\n{\n    M_PAGE *const page = M_TryGetActivePage();\n    ASSERT(page != nullptr);\n    return page;\n}\n\nstatic bool M_IsArrowVisible(int32_t direction)\n{\n    if (m_Priv.mode == M_MODE_PICK_OPTION && !M_IMMEDIATE) {\n        return false;\n    }\n    const M_PAGE *const page = M_TryGetActivePage();\n    if (page == nullptr || page->nav.depth > 0) {\n        return false;\n    }\n    for (M_PAGE_NUMBER i = m_Priv.active_page + direction;\n         i >= PAGE_1 && i < PAGE_COUNT; i += direction) {\n        if (m_Priv.pages[i].available) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic void M_SyncArrowsVisibility(void)\n{\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, M_IsArrowVisible(-1));\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, M_IsArrowVisible(1));\n}\n\nstatic void M_ChangePageTextContent(const char *const content)\n{\n    InvRing_RemoveAllText();\n    Overlay_SetBottomText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_LITERAL,\n        .literal = content,\n        .fmt_gs_key = GS_ID(\"general/inventory_ring/object_name_fmt\"),\n    });\n}\n\nstatic M_PAGE_NUMBER M_GetCurrentPage(const INVENTORY_ITEM *const inv_item)\n{\n    const int32_t frame = inv_item->goal_frame - inv_item->open_frame;\n    return frame % 5 == 0 ? frame / 5 : PAGE_UNDETERMINED;\n}\n\nstatic bool M_IsFlipping(const INVENTORY_ITEM *const inv_item)\n{\n    return M_GetCurrentPage(inv_item) == PAGE_UNDETERMINED;\n}\n\nstatic void M_FlipLeft(INVENTORY_ITEM *const inv_item)\n{\n    M_RemoveAllText();\n    inv_item->anim_direction = -1;\n    inv_item->goal_frame = inv_item->open_frame + 5 * m_Priv.active_page;\n    Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS);\n}\n\nstatic void M_FlipRight(INVENTORY_ITEM *const inv_item)\n{\n    M_RemoveAllText();\n    inv_item->anim_direction = 1;\n    inv_item->goal_frame = inv_item->open_frame + 5 * m_Priv.active_page;\n    Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS);\n}\n\nstatic void M_Close(INVENTORY_ITEM *const inv_item)\n{\n    m_Priv.active_page = PAGE_UNDETERMINED;\n    M_RemoveAllText();\n    M_FreeAllDialogs();\n    if (m_Priv.current_page == PAGE_3) {\n        inv_item->anim_direction = 1;\n        inv_item->goal_frame = inv_item->frames_total - 1;\n    } else {\n        inv_item->anim_direction = -1;\n        inv_item->goal_frame = 0;\n    }\n}\n\nstatic void M_SoftClose(INVENTORY_ITEM *const inv_item)\n{\n    if (g_Inv_Mode == INV_DEATH_MODE) {\n        if (!M_IMMEDIATE && m_Priv.mode != M_MODE_BROWSE) {\n            m_Priv.mode = M_MODE_BROWSE;\n        }\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n        return;\n    }\n    if (m_Priv.mode == M_MODE_BROWSE || M_IMMEDIATE\n        || (g_Inv_Mode != INV_GAME_MODE && g_Inv_Mode != INV_TITLE_MODE)) {\n        M_Close(inv_item);\n    } else {\n        m_Priv.mode = M_MODE_BROWSE;\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    }\n}\n\nstatic void M_NavigateInto(const M_PAGE_ROLE role, const int32_t selection)\n{\n    M_PAGE *const page = M_GetActivePage();\n    if (page->nav.depth + 1\n        < (int32_t)(sizeof page->nav.stack / sizeof page->nav.stack[0])) {\n        page->nav.current->selection = selection;\n        page->nav.depth++;\n        page->nav.current = &page->nav.stack[page->nav.depth];\n        page->nav.current->role = role;\n        page->nav.current->selection = -1;\n    }\n    g_Input = (INPUT_STATE) {};\n    g_InputDB = (INPUT_STATE) {};\n}\n\nstatic void M_NavigateOut(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_TryGetActivePage();\n    if (page == nullptr) {\n        return;\n    }\n    m_Priv.error_msg = nullptr;\n    M_FreeDialogs(page->nav.current);\n    if (page->nav.depth > 0) {\n        page->nav.depth--;\n        page->nav.current = &page->nav.stack[page->nav.depth];\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    } else {\n        M_SoftClose(inv_item);\n    }\n}\n\nstatic void M_Confirm(const PASSPORT_ACTION role, const int32_t argument)\n{\n    g_Passport.select_action = role;\n    g_Passport.select_level = argument;\n}\n\nstatic void M_ConfirmSaveSlot(\n    const PASSPORT_ACTION role, const SAVEGAME_SLOT_REF slot)\n{\n    g_Passport.select_action = role;\n    g_Passport.select_save_slot = slot;\n}\n\nstatic void M_SetPage(\n    const M_PAGE_NUMBER page, const M_PAGE_ROLE role, const bool available)\n{\n    m_Priv.pages[page].role = role;\n    m_Priv.pages[page].available = available;\n}\n\nstatic void M_DeterminePages(void)\n{\n    const bool can_restart = Savegame_RestartAvailable(Savegame_GetBoundSlot());\n    const bool saving_enabled =\n        Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL) > 0\n        && !g_Config.flow.load_save_disabled;\n    const bool has_saves = Savegame_GetTotalCount() > 0 && saving_enabled;\n\n    for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n        m_Priv.pages[i].available = false;\n    }\n\n    switch (g_Inv_Mode) {\n    case INV_TITLE_MODE:\n        m_Priv.mode = M_IMMEDIATE ? M_MODE_PICK_OPTION : M_MODE_BROWSE;\n        M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, has_saves);\n        M_SetPage(PAGE_2, M_ROLE_NEW_GAME, true);\n        M_SetPage(PAGE_3, M_ROLE_EXIT_GAME, true);\n        break;\n\n    case INV_GAME_MODE:\n        m_Priv.mode = M_IMMEDIATE ? M_MODE_PICK_OPTION : M_MODE_BROWSE;\n        if (!saving_enabled) {\n            M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart);\n        } else {\n            M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, has_saves);\n            M_SetPage(PAGE_2, M_ROLE_SAVE_GAME, true);\n        }\n        M_SetPage(PAGE_3, M_ROLE_EXIT_TO_TITLE, true);\n        break;\n\n    case INV_LOAD_MODE:\n        m_Priv.mode = M_MODE_PICK_OPTION;\n        if (!saving_enabled) {\n            M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart);\n        } else if (has_saves) {\n            M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, true);\n        } else {\n            M_SetPage(PAGE_2, M_ROLE_SAVE_GAME, true);\n        }\n        break;\n\n    case INV_SAVE_MODE:\n    case INV_SAVE_CRYSTAL_MODE:\n        m_Priv.mode = M_MODE_PICK_OPTION;\n        if (!saving_enabled) {\n            M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart);\n        } else {\n            M_SetPage(PAGE_2, M_ROLE_SAVE_GAME, true);\n        }\n        break;\n\n    case INV_DEATH_MODE:\n        m_Priv.mode = M_IMMEDIATE ? M_MODE_PICK_OPTION : M_MODE_BROWSE;\n        M_SetPage(PAGE_1, M_ROLE_LOAD_GAME, has_saves);\n        M_SetPage(PAGE_2, M_ROLE_RESTART_LEVEL, can_restart);\n        M_SetPage(PAGE_3, M_ROLE_EXIT_TO_TITLE, true);\n        break;\n\n    case INV_KEYS_MODE:\n    case INV_GLOBE_SELECT_MODE:\n        ASSERT_FAIL();\n    }\n\n    // Disable saves in gym and save crystals mode.\n    // Offer New Game or Restart instead.\n    for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n        if (m_Priv.pages[i].role != M_ROLE_SAVE_GAME) {\n            continue;\n        }\n        if (Game_IsInGym()) {\n            m_Priv.pages[i].role = M_ROLE_NEW_GAME;\n        } else if (\n            g_Config.gameplay.enable_save_crystals\n            && g_Inv_Mode != INV_SAVE_CRYSTAL_MODE) {\n            if (can_restart) {\n                m_Priv.pages[i].role = M_ROLE_RESTART_LEVEL;\n            } else {\n                m_Priv.pages[i].available = false;\n            }\n        }\n    }\n\n    // If play any level is enabled, replace New Game with Play Any Level.\n    if (g_Config.flow.play_any_level) {\n        for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n            if (m_Priv.pages[i].role == M_ROLE_NEW_GAME) {\n                m_Priv.pages[i].role = M_ROLE_PLAY_ANY_LEVEL_SELECT_LEVEL;\n            }\n        }\n    }\n\n    // Select first available page\n    m_Priv.active_page = PAGE_UNDETERMINED;\n    for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n        if (m_Priv.pages[i].available) {\n            m_Priv.active_page = i;\n            break;\n        }\n    }\n\n    // Guard: if no pages are available, force-add exit game or exit to title\n    if (m_Priv.active_page == PAGE_UNDETERMINED) {\n        M_SetPage(\n            PAGE_3,\n            g_Inv_Mode == INV_TITLE_MODE ? M_ROLE_EXIT_GAME\n                                         : M_ROLE_EXIT_TO_TITLE,\n            true);\n        m_Priv.active_page = PAGE_3;\n    }\n\n    // reset hierarchical nav stack now that top-level pages are set\n    ASSERT(m_Priv.active_page != PAGE_UNDETERMINED);\n    for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n        M_ResetNavStack(&m_Priv.pages[i]);\n    }\n\n    for (M_PAGE_NUMBER i = PAGE_1; i < PAGE_COUNT; i++) {\n        LOG_DEBUG(\n            \"page %d: role=%d available=%d\", i, m_Priv.pages[i].role,\n            m_Priv.pages[i].available);\n    }\n}\n\nstatic bool M_ChooseSaveSlot(\n    INVENTORY_ITEM *const inv_item, const UI_SAVE_SLOT_DIALOG_TYPE dialog_type,\n    SAVEGAME_SLOT_REF *const selected_slot)\n{\n    *selected_slot = Savegame_InvalidSlot();\n    M_PAGE *const page = M_GetActivePage();\n    M_NAV_FRAME *const frame = page->nav.current;\n    if (frame->ui.save_slot == nullptr) {\n        const int32_t selection = page->nav.stack[page->nav.depth].selection;\n        SAVEGAME_SLOT_REF initial_slot = selection != -1\n            ? Savegame_SlotFromParam(selection)\n            : Savegame_InvalidSlot();\n        if (!Savegame_IsValidSlotRef(initial_slot)) {\n            initial_slot = Savegame_GetMostRecentlyUsedSlot();\n        }\n        if (!Savegame_IsValidSlotRef(initial_slot)) {\n            initial_slot = Savegame_GetMostRecentlyCreatedSlot();\n        }\n        if (!Savegame_IsValidSlotRef(initial_slot)) {\n            initial_slot = Savegame_NormalSlot(0);\n        }\n        page->nav.current->ui.save_slot =\n            UI_SaveSlotDialog_Init(dialog_type, initial_slot);\n    }\n    const UI_SAVE_SLOT_DIALOG_CHOICE choice =\n        UI_SaveSlotDialog_Control(frame->ui.save_slot);\n    switch (choice.action) {\n    case UI_SAVE_SLOT_DIALOG_NO_CHOICE:\n        if (M_IMMEDIATE) {\n            // Make sure it's not possible to confirm empty slots\n            g_Input.menu_confirm = false;\n            g_InputDB.menu_confirm = false;\n        } else {\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n        return false;\n    case UI_SAVE_SLOT_DIALOG_CANCEL:\n        M_NavigateOut(inv_item);\n        return true;\n    case UI_SAVE_SLOT_DIALOG_CONFIRM:\n        *selected_slot = choice.slot;\n        return true;\n    case UI_SAVE_SLOT_DIALOG_DELETE_FAILED:\n        m_Priv.error_msg = GS_ID(\"general/passport/delete_save_failed\");\n        return false;\n    }\n    return false;\n}\n\nstatic bool M_CheckConfirm(const PASSPORT_ACTION action)\n{\n    if (g_InputDB.menu_confirm) {\n        M_Confirm(action, -1);\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_HandleLoadGame(INVENTORY_ITEM *const inv_item)\n{\n    SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot();\n    const bool result = M_ChooseSaveSlot(\n        inv_item, UI_SAVE_SLOT_DIALOG_LOAD_GAME, &selected_slot);\n    if (Savegame_IsValidSlotRef(selected_slot)) {\n        M_ConfirmSaveSlot(PASSPORT_ACTION_LOAD_GAME, selected_slot);\n    }\n    return result;\n}\n\nstatic bool M_HandleSaveGame(INVENTORY_ITEM *const inv_item)\n{\n    SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot();\n    const bool result = M_ChooseSaveSlot(\n        inv_item, UI_SAVE_SLOT_DIALOG_SAVE_GAME, &selected_slot);\n    if (Savegame_IsValidSlotRef(selected_slot)) {\n        M_ConfirmSaveSlot(PASSPORT_ACTION_SAVE_GAME, selected_slot);\n    }\n    return result;\n}\n\nstatic bool M_HandleNewGame(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_GetActivePage();\n    M_NAV_FRAME *const frame = page->nav.current;\n\n    // If no options – start the game already\n    if (!g_Config.gameplay.enable_game_modes\n        && !g_Config.profile.new_game_plus_unlock\n        && !g_Config.gameplay.enable_play_previous_levels\n        && !UI_NewGame_HasModChoices()) {\n        // But only if in title mode\n        if (g_InputDB.menu_confirm\n            || (!M_IMMEDIATE && g_Inv_Mode == INV_TITLE_MODE)) {\n            M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num);\n            g_InputDB.menu_confirm = true;\n            M_Close(inv_item);\n        }\n        return false;\n    }\n\n    if (frame->ui.new_game == nullptr) {\n        frame->ui.new_game = UI_NewGame_Init(true);\n    }\n    const int32_t choice = UI_NewGame_Control(frame->ui.new_game);\n    if (choice == UI_REQUESTER_NO_CHOICE) {\n        if (!M_IMMEDIATE) {\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n        return false;\n    } else {\n        switch (choice) {\n        case UI_REQUESTER_CANCEL:\n            M_NavigateOut(inv_item);\n            return true;\n\n        case UI_NEW_GAME_CHOICE_NG:\n            // Handle the scenario where enable_game_modes is off, and\n            // enable_play_previous_levels is on. In this scenario the dialog\n            // adds a \"New Game\" row just to let the player start the game. It\n            // shouldn't touch the NG+ flag.\n            if (g_Config.gameplay.enable_game_modes) {\n                Game_SetBonusFlag(GBF_NONE);\n            }\n            M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num);\n            return true;\n        case UI_NEW_GAME_CHOICE_NGPLUS:\n            Game_SetBonusFlag(GBF_NGPLUS);\n            M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num);\n            return true;\n        case UI_NEW_GAME_CHOICE_JP_NG:\n            Game_SetBonusFlag(GBF_JAPANESE);\n            M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num);\n            return true;\n        case UI_NEW_GAME_CHOICE_JP_NGPLUS:\n            Game_SetBonusFlag(GBF_JAPANESE | GBF_NGPLUS);\n            M_Confirm(PASSPORT_ACTION_NEW_GAME, GF_GetFirstLevel()->num);\n            return true;\n        case UI_NEW_GAME_CHOICE_SWITCH_MOD:\n            M_NavigateInto(M_ROLE_SWITCH_MOD, -1);\n            return true;\n        case UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS:\n            M_NavigateInto(M_ROLE_PLAY_PREV_LEVEL_SELECT_SLOT, -1);\n            return true;\n        case UI_NEW_GAME_CHOICE_STORY_SO_FAR:\n            M_NavigateInto(M_ROLE_STORY_SO_FAR, -1);\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_HandlePlayAnyLevel(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_GetActivePage();\n    M_NAV_FRAME *const frame = page->nav.current;\n    if (frame->ui.play_any_level == nullptr) {\n        frame->ui.play_any_level = UI_PlayAnyLevelDialog_Init();\n    }\n    const int32_t choice =\n        UI_PlayAnyLevelDialog_Control(frame->ui.play_any_level);\n    if (choice == UI_REQUESTER_NO_CHOICE) {\n        if (!M_IMMEDIATE) {\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n        return false;\n    } else if (choice == UI_REQUESTER_CANCEL) {\n        M_NavigateOut(inv_item);\n        return true;\n    } else if (\n        g_Config.gameplay.enable_game_modes\n        || g_Config.profile.new_game_plus_unlock) {\n        M_NavigateInto(M_ROLE_PLAY_ANY_LEVEL_SELECT_MODE, choice);\n        return true;\n    } else {\n        Savegame_UnbindSlot();\n        M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, choice);\n        return true;\n    }\n}\n\nstatic bool M_HandlePlayAnyLevelSelectMode(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_GetActivePage();\n    M_NAV_FRAME *const frame = page->nav.current;\n    ASSERT(m_Priv.mode == M_MODE_PICK_OPTION);\n    if (frame->ui.new_game == nullptr) {\n        frame->ui.new_game = UI_NewGame_Init(false);\n    }\n    const int32_t choice = UI_NewGame_Control(frame->ui.new_game);\n    if (choice == UI_REQUESTER_NO_CHOICE) {\n        return false;\n    } else {\n        const int32_t level_num =\n            page->nav.stack[page->nav.depth - 1].selection;\n        switch (choice) {\n        case UI_REQUESTER_CANCEL:\n            M_NavigateOut(inv_item);\n            return true;\n        case UI_NEW_GAME_CHOICE_NG:\n            Game_SetBonusFlag(GBF_NONE);\n            Savegame_UnbindSlot();\n            M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num);\n            return true;\n        case UI_NEW_GAME_CHOICE_NGPLUS:\n            Game_SetBonusFlag(GBF_NGPLUS);\n            Savegame_UnbindSlot();\n            M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num);\n            return true;\n        case UI_NEW_GAME_CHOICE_JP_NG:\n            Game_SetBonusFlag(GBF_JAPANESE);\n            Savegame_UnbindSlot();\n            M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num);\n            return true;\n        case UI_NEW_GAME_CHOICE_JP_NGPLUS:\n            Game_SetBonusFlag(GBF_JAPANESE | GBF_NGPLUS);\n            Savegame_UnbindSlot();\n            M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, level_num);\n            return true;\n        default:\n            ASSERT_FAIL();\n        }\n    }\n    return false;\n}\n\nstatic bool M_HandleSwitchMod(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_GetActivePage();\n    M_NAV_FRAME *const frame = page->nav.current;\n    if (frame->ui.switch_mod == nullptr) {\n        frame->ui.switch_mod = UI_SwitchModDialog_Init();\n    }\n\n    const int32_t choice = UI_SwitchModDialog_Control(frame->ui.switch_mod);\n    if (choice == UI_REQUESTER_NO_CHOICE) {\n        return false;\n    }\n    if (choice == UI_REQUESTER_CANCEL) {\n        M_NavigateOut(inv_item);\n        return true;\n    }\n\n    const char *const mod_name =\n        UI_SwitchModDialog_GetSelectedMod(frame->ui.switch_mod, choice);\n    Shell_RequestModSwitch(mod_name);\n    M_Confirm(PASSPORT_ACTION_SWITCH_MOD, -1);\n    g_InputDB.menu_confirm = true;\n    M_Close(inv_item);\n    return true;\n}\n\nstatic bool M_HandlePlayPrevLevelSelectSlot(INVENTORY_ITEM *const inv_item)\n{\n    SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot();\n    const bool result =\n        M_ChooseSaveSlot(inv_item, UI_SAVE_SLOT_DIALOG_GENERIC, &selected_slot);\n    if (Savegame_IsValidSlotRef(selected_slot)) {\n        M_NavigateInto(\n            M_ROLE_PLAY_PREV_LEVEL_SELECT_LEVEL,\n            Savegame_SlotToParam(selected_slot));\n    }\n    return result;\n}\n\nstatic bool M_HandlePlayPrevLevelSelectLevel(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_GetActivePage();\n    const SAVEGAME_SLOT_REF slot =\n        Savegame_SlotFromParam(page->nav.stack[page->nav.depth - 1].selection);\n    if (!Savegame_IsValidSlotRef(slot)) {\n        M_NavigateOut(inv_item);\n        return true;\n    }\n    const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot);\n    if (info == nullptr) {\n        M_NavigateOut(inv_item);\n        return true;\n    }\n    if (!info->features.select_level) {\n        m_Priv.error_msg = GS_ID(\"general/passport/save_slot_unsupported\");\n        if (g_InputDB.menu_back || g_InputDB.menu_confirm) {\n            M_NavigateOut(inv_item);\n            return true;\n        }\n        return false;\n    }\n    M_NAV_FRAME *const frame = page->nav.current;\n    if (frame->ui.select_level == nullptr) {\n        frame->ui.select_level = UI_SelectLevelDialog_Init(slot);\n    }\n    const int32_t choice = UI_SelectLevelDialog_Control(frame->ui.select_level);\n    if (choice == UI_REQUESTER_NO_CHOICE) {\n        if (!M_IMMEDIATE) {\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n        return false;\n    } else if (choice == UI_REQUESTER_CANCEL) {\n        M_NavigateOut(inv_item);\n        return true;\n    } else {\n        Savegame_BindSlot(slot);\n        M_Confirm(PASSPORT_ACTION_SELECT_LEVEL, choice);\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_HandleStorySoFar(INVENTORY_ITEM *const inv_item)\n{\n    SAVEGAME_SLOT_REF selected_slot = Savegame_InvalidSlot();\n    const bool result =\n        M_ChooseSaveSlot(inv_item, UI_SAVE_SLOT_DIALOG_GENERIC, &selected_slot);\n    if (Savegame_IsValidSlotRef(selected_slot)) {\n        M_NavigateInto(\n            M_ROLE_STORY_SO_FAR_CONFIRM, Savegame_SlotToParam(selected_slot));\n    }\n    return result;\n}\n\nstatic bool M_HandleStorySoFarConfirm(INVENTORY_ITEM *const inv_item)\n{\n    M_PAGE *const page = M_GetActivePage();\n    const SAVEGAME_SLOT_REF slot =\n        Savegame_SlotFromParam(page->nav.stack[page->nav.depth - 1].selection);\n    if (GF_HasAvailableStory(slot)) {\n        M_ConfirmSaveSlot(PASSPORT_ACTION_STORY_SO_FAR, slot);\n        g_InputDB.menu_confirm = true;\n        M_Close(inv_item);\n        return true;\n    } else if (g_InputDB.menu_back || g_InputDB.menu_confirm) {\n        M_NavigateOut(inv_item);\n        return true;\n    } else {\n        m_Priv.error_msg = GS_ID(\"general/passport/save_slot_unsupported\");\n        return false;\n    }\n    return false;\n}\n\nstatic bool M_HandleRestartLevel(INVENTORY_ITEM *const inv_item)\n{\n    return M_CheckConfirm(PASSPORT_ACTION_RESTART);\n}\n\nstatic bool M_HandleExitGame(INVENTORY_ITEM *const inv_item)\n{\n    return M_CheckConfirm(PASSPORT_ACTION_EXIT_GAME);\n}\n\nstatic bool M_HandleExitToTitle(INVENTORY_ITEM *const inv_item)\n{\n    return M_CheckConfirm(PASSPORT_ACTION_EXIT_TO_TITLE);\n}\n\nstatic bool M_ShowPage(INVENTORY_ITEM *const inv_item)\n{\n    static M_PAGE_HANDLER m_PageHandlers[] = {\n        [M_ROLE_LOAD_GAME] = {\n            .title = GS_ID(\"general/passport/load_game\"),\n            .func = M_HandleLoadGame,\n            .flat = false,\n        },\n        [M_ROLE_SAVE_GAME] = {\n            .title = GS_ID(\"general/passport/save_game\"),\n            .func = M_HandleSaveGame,\n            .flat = false,\n        },\n        [M_ROLE_NEW_GAME] = {\n            .title = GS_ID(\"general/passport/new_game\"),\n            .func = M_HandleNewGame,\n            .flat = false,\n        },\n        [M_ROLE_PLAY_ANY_LEVEL_SELECT_LEVEL] = {\n            .title = GS_ID(\"general/passport/select_level\"),\n            .func = M_HandlePlayAnyLevel,\n            .flat = false,\n        },\n        [M_ROLE_PLAY_ANY_LEVEL_SELECT_MODE] = {\n            .title = GS_ID(\"general/passport/select_level\"),\n            .func = M_HandlePlayAnyLevelSelectMode,\n            .flat = false,\n        },\n        [M_ROLE_PLAY_PREV_LEVEL_SELECT_SLOT] = {\n            .title = GS_ID(\"general/passport/play_previous_levels\"),\n            .func = M_HandlePlayPrevLevelSelectSlot,\n            .flat = false,\n        },\n        [M_ROLE_PLAY_PREV_LEVEL_SELECT_LEVEL] = {\n            .title = GS_ID(\"general/passport/play_previous_levels\"),\n            .func = M_HandlePlayPrevLevelSelectLevel,\n            .flat = false,\n        },\n        [M_ROLE_SWITCH_MOD] = {\n            .title = GS_ID(\"general/passport/switch_mod\"),\n            .func = M_HandleSwitchMod,\n            .flat = false,\n        },\n        [M_ROLE_RESTART_LEVEL] = {\n            .title = GS_ID(\"general/passport/restart_level\"),\n            .func = M_HandleRestartLevel,\n            .flat = true,\n        },\n        [M_ROLE_EXIT_GAME] = {\n            .title = GS_ID(\"general/passport/exit_game\"),\n            .func = M_HandleExitGame,\n            .flat = true,\n        },\n        [M_ROLE_EXIT_TO_TITLE] = {\n            .title = GS_ID(\"general/passport/exit_to_title\"),\n            .func = M_HandleExitToTitle,\n            .flat = true,\n        },\n        [M_ROLE_STORY_SO_FAR] = {\n            .title = GS_ID(\"general/passport/story_so_far\"),\n            .func = M_HandleStorySoFar,\n            .flat = false,\n        },\n        [M_ROLE_STORY_SO_FAR_CONFIRM] = {\n            .title = GS_ID(\"general/passport/story_so_far\"),\n            .func = M_HandleStorySoFarConfirm,\n            .flat = false,\n        },\n    };\n\n    M_PAGE *const page = M_TryGetActivePage();\n    if (page == nullptr) {\n        return false;\n    }\n    const M_PAGE_HANDLER *const handler =\n        &m_PageHandlers[page->nav.current->role];\n    M_ChangePageTextContent(GameString_Get(handler->title));\n    if (m_Priv.mode == M_MODE_BROWSE && !handler->flat) {\n        if (g_InputDB.menu_confirm) {\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n            m_Priv.mode = M_MODE_PICK_OPTION;\n            return true;\n        }\n        return false;\n    }\n    return handler->func(inv_item);\n}\n\nstatic void M_HandleFlipInputs(void)\n{\n    bool flipped = false;\n    if (g_InputDB.menu_left && M_IsArrowVisible(-1)) {\n        for (M_PAGE_NUMBER page = m_Priv.active_page - 1; page >= PAGE_1;\n             page--) {\n            if (m_Priv.pages[page].available) {\n                m_Priv.active_page = page;\n                flipped = true;\n                break;\n            }\n        }\n    } else if (g_InputDB.menu_right && M_IsArrowVisible(1)) {\n        for (M_PAGE_NUMBER page = m_Priv.active_page + 1; page < PAGE_COUNT;\n             page++) {\n            if (m_Priv.pages[page].available) {\n                m_Priv.active_page = page;\n                flipped = true;\n                break;\n            }\n        }\n    }\n    if (flipped) {\n        M_ResetNavStack(M_GetActivePage());\n    }\n}\n\nvoid Option_Passport_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    if (m_Priv.active_page == PAGE_UNDETERMINED) {\n        M_DeterminePages();\n    }\n\n    if (is_busy) {\n        if (g_Config.input.enable_responsive_passport) {\n            M_HandleFlipInputs();\n        }\n        return;\n    }\n\n    InvRing_RemoveAllText();\n\n    if (M_IsFlipping(inv_item)) {\n        return;\n    }\n\n    m_Priv.current_page = M_GetCurrentPage(inv_item);\n    if (m_Priv.current_page < m_Priv.active_page) {\n        M_FlipRight(inv_item);\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    } else if (m_Priv.current_page > m_Priv.active_page) {\n        M_FlipLeft(inv_item);\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    } else {\n        if (M_ShowPage(inv_item)) {\n            // In case of state-changes, apply changes immediately\n            M_ShowPage(inv_item);\n        }\n        M_SyncArrowsVisibility();\n        if (g_InputDB.menu_confirm) {\n            M_Close(inv_item);\n        } else if (g_InputDB.menu_back) {\n            if (g_Inv_Mode == INV_DEATH_MODE) {\n                g_Input = (INPUT_STATE) {};\n                g_InputDB = (INPUT_STATE) {};\n            } else {\n                M_NavigateOut(inv_item);\n            }\n        } else {\n            M_HandleFlipInputs();\n        }\n    }\n}\n\nvoid Option_Passport_Draw(INVENTORY_ITEM *const inv_item)\n{\n    if (m_Priv.mode == M_MODE_BROWSE || M_IsFlipping(inv_item)\n        || m_Priv.active_page != m_Priv.current_page) {\n        return;\n    }\n\n    M_PAGE *const page = M_TryGetActivePage();\n    if (page == nullptr) {\n        return;\n    }\n    M_NAV_FRAME *const frame = page->nav.current;\n    if (frame->ui.new_game != nullptr) {\n        UI_NewGame(frame->ui.new_game);\n    }\n    if (frame->ui.play_any_level != nullptr) {\n        UI_PlayAnyLevelDialog(frame->ui.play_any_level);\n    }\n    if (frame->ui.select_level != nullptr) {\n        UI_SelectLevelDialog(frame->ui.select_level);\n    }\n    if (frame->ui.switch_mod != nullptr) {\n        UI_SwitchModDialog(frame->ui.switch_mod);\n    }\n    if (frame->ui.save_slot != nullptr) {\n        UI_SaveSlotDialog(frame->ui.save_slot);\n    }\n\n    if (m_Priv.error_msg != nullptr) {\n        UI_BeginModal(0.5f, 0.67f);\n        UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND);\n        UI_BeginPad(8.0f, 8.0f);\n        UI_Label(GameString_Get(m_Priv.error_msg));\n        UI_EndPad();\n        UI_EndFrame();\n        UI_EndModal();\n    }\n}\n\nvoid Option_Passport_Close(void)\n{\n    M_RemoveAllText();\n    M_FreeAllDialogs();\n    m_Priv.active_page = PAGE_UNDETERMINED;\n}\n"
  },
  {
    "path": "src/trx/game/option/passport.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/savegame/types.h>\n\ntypedef enum {\n    PASSPORT_ACTION_LOAD_GAME,\n    PASSPORT_ACTION_SELECT_LEVEL,\n    PASSPORT_ACTION_GLOBE_SELECT,\n    PASSPORT_ACTION_STORY_SO_FAR,\n    PASSPORT_ACTION_SAVE_GAME,\n    PASSPORT_ACTION_NEW_GAME,\n    PASSPORT_ACTION_SWITCH_MOD,\n    PASSPORT_ACTION_RESTART,\n    PASSPORT_ACTION_EXIT_TO_TITLE,\n    PASSPORT_ACTION_EXIT_GAME,\n} PASSPORT_ACTION;\n\ntypedef struct {\n    PASSPORT_ACTION select_action;\n    union {\n        int32_t select_level;\n        SAVEGAME_SLOT_REF select_save_slot;\n    };\n    bool ask_for_save;\n} PASSPORT;\n\nextern PASSPORT g_Passport; // TODO: meh\n\nvoid Option_Passport_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Passport_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_Passport_Close(void);\n"
  },
  {
    "path": "src/trx/game/option/sound.c",
    "content": "#include <trx/game/option/sound.h>\n\n#include <trx/config.h>\n#include <trx/game/input.h>\n#include <trx/game/ui.h>\n\ntypedef struct {\n    UI_SETTINGS_DIALOG_STATE *ui_state;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_Init(M_PRIV *const p)\n{\n    if (p->ui_state == nullptr) {\n        p->ui_state = UI_SoundSettings_Init();\n    }\n}\n\nstatic void M_Shutdown(M_PRIV *const p)\n{\n    if (p->ui_state != nullptr) {\n        UI_SoundSettings_Free(p->ui_state);\n        p->ui_state = nullptr;\n    }\n}\n\nvoid Option_Sound_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    M_PRIV *const p = &m_Priv;\n    if (is_busy) {\n        return;\n    }\n    if (p->ui_state == nullptr) {\n        M_Init(p);\n    }\n    if (!UI_SoundSettings_Control(p->ui_state)) {\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    }\n}\n\nvoid Option_Sound_Draw(INVENTORY_ITEM *const inv_item)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->ui_state != nullptr) {\n        UI_SoundSettings(p->ui_state);\n    }\n}\n\nvoid Option_Sound_Close(void)\n{\n}\n\nvoid Option_Sound_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_Shutdown(p);\n}\n"
  },
  {
    "path": "src/trx/game/option/sound.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_Sound_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Sound_Draw(INVENTORY_ITEM *inv_item);\nvoid Option_Sound_Close(void);\nvoid Option_Sound_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/option/stats.c",
    "content": "#include <trx/game/option/stats.h>\n\n#include <trx/config.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/gym.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n\ntypedef struct {\n    bool is_ready;\n    UI_STATS_DIALOG_STATE *ui_state;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\nstatic int16_t m_CompassNeedle = 0;\nstatic int16_t m_CompassSpeed = 0;\n\nstatic void M_Init(M_PRIV *const p, INVENTORY_ITEM *const inv_item)\n{\n    if (inv_item->object_id == O_COMPASS_OPTION\n        && !g_Config.gameplay.enable_compass_stats) {\n        return;\n    }\n\n    p->is_ready = true;\n    p->ui_state = UI_StatsDialog_Init((UI_STATS_DIALOG_ARGS) {\n        .mode = Game_IsInGym() && Gym_TrackManager_HasStats(GYM_TRACK_ASSAULT)\n            ? UI_STATS_DIALOG_MODE_ASSAULT_COURSE\n            : UI_STATS_DIALOG_MODE_LEVEL,\n        .style = g_Config.ui.stats.style == STATS_STYLE_BARE\n            ? UI_STATS_DIALOG_STYLE_BARE\n            : UI_STATS_DIALOG_STYLE_BORDERED,\n        .level_num = Game_GetCurrentLevel()->num,\n    });\n}\n\nstatic void M_Close(M_PRIV *const p)\n{\n    if (p->is_ready) {\n        p->is_ready = false;\n        UI_StatsDialog_Free(p->ui_state);\n        p->ui_state = nullptr;\n    }\n}\n\nvoid Option_Stats_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)\n{\n    M_PRIV *const p = &m_Priv;\n    if (is_busy) {\n        return;\n    }\n\n    if (!p->is_ready) {\n        M_Init(p, inv_item);\n    }\n    if (p->is_ready) {\n        UI_StatsDialog_Control(p->ui_state);\n    }\n\n    if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n        M_Close(p);\n        inv_item->anim_direction = 1;\n        inv_item->goal_frame = inv_item->frames_total - 1;\n        if (inv_item->object_id == O_STOPWATCH_OPTION) {\n            Sound_StopEffect(SFX_MENU_STOPWATCH);\n        }\n    } else if (inv_item->object_id == O_STOPWATCH_OPTION) {\n        Sound_Effect(SFX_MENU_STOPWATCH, 0, SPM_ALWAYS);\n    }\n}\n\nvoid Option_Stats_Draw(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->is_ready) {\n        UI_StatsDialog(p->ui_state);\n    }\n}\n\nvoid Option_Stats_Close(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_Close(p);\n}\n\nvoid Option_Stats_UpdateCompassNeedle(const INVENTORY_ITEM *const inv_item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return;\n    }\n    int16_t delta = -inv_item->y_rot - lara_item->rot.y - m_CompassNeedle;\n    m_CompassSpeed = m_CompassSpeed * 19 / 20 + delta / 50;\n    m_CompassNeedle += m_CompassSpeed;\n}\n\nint16_t Option_Stats_GetCompassNeedleAngle(void)\n{\n    return m_CompassNeedle;\n}\n"
  },
  {
    "path": "src/trx/game/option/stats.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n\nvoid Option_Stats_Control(INVENTORY_ITEM *inv_item, bool is_busy);\nvoid Option_Stats_Draw(void);\nvoid Option_Stats_Close(void);\n\nvoid Option_Stats_UpdateCompassNeedle(const INVENTORY_ITEM *inv_item);\nint16_t Option_Stats_GetCompassNeedleAngle(void);\n"
  },
  {
    "path": "src/trx/game/option.h",
    "content": "#pragma once\n\n#include <trx/game/option/common.h>\n"
  },
  {
    "path": "src/trx/game/output/bind.c",
    "content": "#include <trx/game/output/bind.h>\n\n#include <trx/game/items.h>\n#include <trx/game/rooms/common.h>\n#include <trx/game/rooms/const.h>\n\n#include <string.h>\n\nstatic OUTPUT_ITEM_BIND m_ItemBindings[MAX_ITEMS] = {};\nstatic OUTPUT_ROOM_BIND m_RoomBindings[MAX_ROOMS] = {};\n\nvoid Output_Bind_ResetItems(void)\n{\n    memset(m_ItemBindings, 0, sizeof(m_ItemBindings));\n}\n\nOUTPUT_ITEM_BIND *Output_Bind_GetItem(const ITEM *const item)\n{\n    return &m_ItemBindings[Item_GetIndex(item)];\n}\n\nvoid Output_Bind_ResetRooms(void)\n{\n    for (int32_t i = 0; i < MAX_ROOMS; i++) {\n        m_RoomBindings[i].active = false;\n        m_RoomBindings[i].drawn = false;\n    }\n}\n\nOUTPUT_ROOM_BIND *Output_Bind_GetRoom(const ROOM *const room)\n{\n    return &m_RoomBindings[Room_GetNumber(room)];\n}\n"
  },
  {
    "path": "src/trx/game/output/bind.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n#include <trx/game/rooms/types.h>\n\ntypedef struct {\n    bool drawn;\n    bool shadow_drawn;\n} OUTPUT_ITEM_BIND;\n\ntypedef struct {\n    bool active;\n    bool drawn;\n    int16_t bound_left;\n    int16_t bound_right;\n    int16_t bound_top;\n    int16_t bound_bottom;\n    int16_t test_left;\n    int16_t test_right;\n    int16_t test_top;\n    int16_t test_bottom;\n} OUTPUT_ROOM_BIND;\n\nvoid Output_Bind_ResetItems(void);\nOUTPUT_ITEM_BIND *Output_Bind_GetItem(const ITEM *item);\n\nvoid Output_Bind_ResetRooms(void);\nOUTPUT_ROOM_BIND *Output_Bind_GetRoom(const ROOM *room);\n"
  },
  {
    "path": "src/trx/game/output/common.c",
    "content": "#include <trx/game/output/common.h>\n\n#include <trx/config.h>\n#include <trx/game/level.h>\n#include <trx/game/output/func.h>\n#include <trx/game/output/lights.h>\n#include <trx/game/output/mesh_batcher/batcher.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/sources/lightnings.h>\n#include <trx/game/output/sources/misc.h>\n#include <trx/game/output/sources/objects.h>\n#include <trx/game/output/sources/overlay.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/sources/rooms.h>\n#include <trx/game/output/sources/rooms_debug.h>\n#include <trx/game/output/sources/shadows.h>\n#include <trx/game/output/sources/sprites.h>\n#include <trx/game/output/sources/ui.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/shell.h>\n#include <trx/gl/context.h>\n#include <trx/gl/track.h>\n\nstatic MESH_BATCHER *m_Batcher = nullptr;\nstatic OUTPUT_UNIFORMS *m_Uniforms = nullptr;\nstatic OUTPUT_MESH_SHADER *m_ShaderWorld = nullptr;\nstatic OUTPUT_UI_SHADER *m_ShaderUI = nullptr;\n\nvoid Output_Init(void)\n{\n    SceneCompositor_Init();\n    Output_Textures_Init();\n\n    m_Uniforms = Output_Uniforms_Create();\n    m_ShaderWorld = Output_MeshShader_Create();\n    m_ShaderUI = Output_UIShader_Create();\n    m_Batcher = MeshBatcher_Create();\n    SceneCompositor_AddSource(MeshBatcher_AsSource(m_Batcher));\n    OutputSource_Rooms_Init(m_Batcher);\n    OutputSource_RoomsDebug_Init();\n    OutputSource_Objects_Init(m_Batcher);\n    OutputSource_Sprites_Init(m_Batcher);\n    OutputSource_Lightnings_Init();\n    OutputSource_PolyFX_Init();\n    OutputSource_Shadows_Init(m_Batcher);\n    OutputSource_Misc_Init();\n    OutputSource_Overlay_Init();\n\n    Output_Lights_Init();\n    OutputSource_UI_Init();\n\n    Output_ApplyRenderSettings();\n}\n\nvoid Output_Shutdown(void)\n{\n    SceneCompositor_Shutdown();\n    OutputSource_Rooms_Shutdown();\n    OutputSource_RoomsDebug_Shutdown();\n    OutputSource_Objects_Shutdown();\n    OutputSource_Sprites_Shutdown();\n    OutputSource_Lightnings_Shutdown();\n    OutputSource_PolyFX_Shutdown();\n    OutputSource_Shadows_Shutdown();\n    OutputSource_Misc_Shutdown();\n    OutputSource_Overlay_Shutdown();\n    OutputSource_UI_Shutdown();\n\n    if (m_ShaderWorld != nullptr) {\n        Output_MeshShader_Free(m_ShaderWorld);\n        m_ShaderWorld = nullptr;\n    }\n    if (m_ShaderUI != nullptr) {\n        Output_UIShader_Free(m_ShaderUI);\n        m_ShaderUI = nullptr;\n    }\n    if (m_Uniforms != nullptr) {\n        Output_Uniforms_Free(m_Uniforms);\n        m_Uniforms = nullptr;\n    }\n    if (m_Batcher != nullptr) {\n        MeshBatcher_Destroy(m_Batcher);\n        m_Batcher = nullptr;\n    }\n\n    Output_Textures_Shutdown();\n    Output_Lights_Shutdown();\n}\n\nbool Output_IsHeadless(void)\n{\n    return Shell_GetArgs()->headless;\n}\n\nconst OUTPUT_UNIFORMS *Output_GetUniforms(void)\n{\n    return m_Uniforms;\n}\n\nOUTPUT_MESH_SHADER *Output_GetMeshShader(void)\n{\n    return m_ShaderWorld;\n}\n\nOUTPUT_UI_SHADER *Output_GetUIShader(void)\n{\n    return m_ShaderUI;\n}\n\nvoid Output_BeginScene(void)\n{\n    Output_ApplyFOV();\n    TRX_GL_Context_Clear();\n    TRX_GL_Track_Reset();\n    TRX_GL_Context_SetWireframeMode(g_Config.rendering.enable_wireframe);\n    SceneCompositor_BeginScene();\n}\n\nvoid Output_EndScene(void)\n{\n    SceneCompositor_EndScene();\n}\n\nvoid Output_Flush(void)\n{\n    SceneCompositor_Flush();\n}\n\nvoid Output_FlipScreen(void)\n{\n    TRX_GL_Context_SwapBuffers();\n}\n\nvoid Output_SwitchViewport(const VIEWPORT_SPACE space)\n{\n    if (space == VIEWPORT_GAME) {\n        TRX_GL_Renderer_BindGeometryFbo();\n    } else if (space == VIEWPORT_UI) {\n        TRX_GL_Renderer_BindUiFbo();\n    }\n    TRX_GL_Context_SwitchToViewport(space);\n    TRX_GL_Context_Clear();\n    glClear(GL_DEPTH_BUFFER_BIT);\n}\n\nvoid Output_ApplyRenderSettings(void)\n{\n    Output_Textures_ApplyRenderSettings();\n    Output_ApplyLevelSettings();\n\n    if (m_ShaderWorld == nullptr) {\n        return;\n    }\n\n    TRX_GL_Context_SetVSync(g_Config.rendering.enable_vsync);\n    TRX_GL_Context_SetDisplayFilter(g_Config.rendering.upscaling_filter);\n    TRX_GL_Context_SetWireframeMode(g_Config.rendering.enable_wireframe);\n    TRX_GL_Context_SetLineWidth(g_Config.rendering.wireframe_width);\n}\n\nvoid Output_ApplyLevelSettings(void)\n{\n    Output_SetWaterColor(Level_GetWaterColor());\n    Output_SetFogColor(Level_GetFogColor());\n    Output_SetFogStart(Level_GetFogStart() * WALL_L);\n    Output_SetFogEnd(Level_GetFogEnd() * WALL_L);\n}\n\nvoid Output_DispatchLevelLoad(void)\n{\n    Output_Textures_ObserveLevelLoad();\n    Output_Lights_ObserveLevelLoad();\n\n    OutputSource_Objects_ObserveLevelLoad();\n    OutputSource_Rooms_ObserveLevelLoad();\n    OutputSource_RoomsDebug_ObserveLevelLoad();\n    OutputSource_Sprites_ObserveLevelLoad();\n\n    MeshBatcher_Seal(m_Batcher);\n\n    Output_ApplyLevelSettings();\n}\n\nvoid Output_DispatchLevelUnload(void)\n{\n    OutputSource_Objects_ObserveLevelUnload();\n    OutputSource_Rooms_ObserveLevelUnload();\n    OutputSource_RoomsDebug_ObserveLevelUnload();\n    OutputSource_Sprites_ObserveLevelUnload();\n}\n\nvoid Output_DispatchRoomFlip(const ROOM *room)\n{\n    OutputSource_Rooms_ObserveRoomFlip(room);\n    OutputSource_RoomsDebug_ObserveRoomFlip(room);\n}\n\nvoid Output_DispatchObjectMeshSwap(\n    const int32_t mesh_idx_1, const int32_t mesh_idx_2)\n{\n    OutputSource_Objects_ObserveObjectMeshSwap(mesh_idx_1, mesh_idx_2);\n}\n\nvoid Output_DispatchObjectMeshUpdate(const int32_t mesh_idx)\n{\n    OutputSource_Objects_ObserveObjectMeshUpdate(mesh_idx);\n}\n"
  },
  {
    "path": "src/trx/game/output/common.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/shaders/ui.h>\n#include <trx/game/output/uniforms.h>\n#include <trx/game/rooms.h>\n#include <trx/game/viewport.h>\n\nvoid Output_Init(void);\nvoid Output_Shutdown(void);\nbool Output_IsHeadless(void);\n\nconst OUTPUT_UNIFORMS *Output_GetUniforms(void);\nOUTPUT_MESH_SHADER *Output_GetMeshShader(void);\nOUTPUT_UI_SHADER *Output_GetUIShader(void);\n\nvoid Output_BeginScene(void);\nvoid Output_EndScene(void);\nvoid Output_Flush(void);\nvoid Output_FlipScreen(void);\n\nvoid Output_SwitchViewport(VIEWPORT_SPACE space);\n\nvoid Output_ApplyRenderSettings(void);\nvoid Output_ApplyLevelSettings(void);\n\nvoid Output_DispatchLevelLoad(void);\nvoid Output_DispatchLevelUnload(void);\nvoid Output_DispatchRoomFlip(const ROOM *room);\nvoid Output_DispatchObjectMeshUpdate(int32_t mesh_idx);\nvoid Output_DispatchObjectMeshSwap(int32_t mesh_idx_0, int32_t mesh_idx_1);\n"
  },
  {
    "path": "src/trx/game/output/const.h",
    "content": "#pragma once\n\n#define MAX_TEXTURE_PAGES 128\n\n#define TEXTURE_PAGE_WIDTH 256\n#define TEXTURE_PAGE_HEIGHT 256\n#define TEXTURE_PAGE_SIZE (TEXTURE_PAGE_WIDTH * TEXTURE_PAGE_HEIGHT)\n\n// clang-format off\n#define SHADE_CAUSTICS 0x300\n#define SHADE_MAX      0x1FFF\n#define SHADE_LOW      0x1400\n#define SHADE_HIGH     0x800\n#define SHADE_NEUTRAL  0x1000\n#define SHADE_SUNSET   0x400\n// clang-format on\n\n#define WIBBLE_SIZE 32\n\n#define LIGHT_MAP_SIZE 32\n#define LIGHT_MAP_NEUTRAL 16\n\n// TODO: get rid of this\n#define PHD_ONE 0x10000\n"
  },
  {
    "path": "src/trx/game/output/draw.c",
    "content": "#include <trx/game/output/draw.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/creature/const.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/bind.h>\n#include <trx/game/output/sources/lightnings.h>\n#include <trx/game/output/sources/misc.h>\n#include <trx/game/output/sources/objects.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/output/sources/rooms.h>\n#include <trx/game/output/sources/rooms_debug.h>\n#include <trx/game/output/sources/shadows.h>\n#include <trx/game/output/sources/sprites.h>\n#include <trx/game/output/sources/ui.h>\n#include <trx/game/output/state.h>\n#include <trx/game/rooms.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\n#define M_SHADOW_LINE_POINTS 4\n#define M_SHADOW_GRID_POINTS (M_SHADOW_LINE_POINTS * M_SHADOW_LINE_POINTS)\n\nstatic bool M_DrawShadow_Sprite(\n    const int32_t size, const BOUNDS_16 *const bounds, const ITEM *const item)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    const OBJECT *const shadow_obj = Object_Get(O_SHADOW);\n    if (!shadow_obj->loaded) {\n        return false;\n    }\n\n    // OG: shadow intensity is based on Lara's height above the floor, even for\n    // non-Lara items.\n    int32_t c = ((4096 - ABS(item->floor - item->pos.y)) >> 4) - 1;\n    CLAMP(c, 32, 255);\n\n    const RGBA_8888 shadow_color = { c, c, c, 255 };\n    const RGBA_8888 quad_color[4] = {\n        shadow_color,\n        shadow_color,\n        shadow_color,\n        shadow_color,\n    };\n\n    const int32_t x_size = size * (bounds->max.x - bounds->min.x) / 128;\n    const int32_t z_size = size * (bounds->max.z - bounds->min.z) / 128;\n    const int32_t x_dist = x_size / M_SHADOW_LINE_POINTS;\n    const int32_t z_dist = z_size / M_SHADOW_LINE_POINTS;\n\n    int32_t grid_local_x[M_SHADOW_GRID_POINTS];\n    int32_t grid_local_z[M_SHADOW_GRID_POINTS];\n    int32_t x = -x_dist - (x_dist >> 1);\n    int32_t z = z_dist + (z_dist >> 1);\n    int32_t grid_idx = 0;\n    for (int32_t row = 0; row < M_SHADOW_LINE_POINTS; row++) {\n        for (int32_t col = 0; col < M_SHADOW_LINE_POINTS; col++) {\n            grid_local_x[grid_idx] = x;\n            grid_local_z[grid_idx] = z;\n            grid_idx++;\n            x += x_dist;\n        }\n        x = -x_dist - (x_dist >> 1);\n        z -= z_dist;\n    }\n\n    // Determine the shadow anchor position.\n    XYZ_32 anchor_pos = item->interp.result.pos;\n    int32_t anchor_floor = item->interp.result.floor;\n    const int16_t anim_state = item->current_anim_state;\n    if (item == lara_item && anim_state != LS(LS_CRAWL_IDLE)\n        && anim_state != LS(LS_CRAWL_FORWARD) && anim_state != LS(LS_CRAWL_BACK)\n        && anim_state != LS(LS_CRAWL_TURN_LEFT)\n        && anim_state != LS(LS_CRAWL_TURN_RIGHT)) {\n        ANIM_FRAME *frames[2] = { nullptr, nullptr };\n        int32_t rate = 0;\n        const int32_t frac = Item_GetFrames(item, frames, &rate);\n        if (frames[0] != nullptr) {\n            XYZ_32 offset_a = XYZ_32_From16(frames[0]->offset);\n            XYZ_32 offset = offset_a;\n            if (frames[1] != nullptr && rate != 0 && frac != 0) {\n                const XYZ_32 offset_b = XYZ_32_From16(frames[1]->offset);\n                offset.x += ((offset_b.x - offset_a.x) * frac) / rate;\n                offset.y += ((offset_b.y - offset_a.y) * frac) / rate;\n                offset.z += ((offset_b.z - offset_a.z) * frac) / rate;\n            }\n\n            const int32_t sy = Math_Sin(item->interp.result.rot.y);\n            const int32_t cy = Math_Cos(item->interp.result.rot.y);\n            anchor_pos.x += (offset.x * cy + offset.z * sy) >> W2V_SHIFT;\n            anchor_pos.y += offset.y;\n            anchor_pos.z += (offset.z * cy - offset.x * sy) >> W2V_SHIFT;\n\n            int16_t room_num = item->room_num;\n            const SECTOR *const sector = Room_GetSector(anchor_pos, &room_num);\n            const int32_t height = Room_GetHeight(sector, anchor_pos);\n            if (height != NO_HEIGHT) {\n                anchor_floor = height;\n            }\n        }\n    } else {\n        // TR3 cutscene actors are driven by cutscene data, so their item origin\n        // can diverge from the visual mesh center.\n        const int32_t x_mid = (bounds->min.x + bounds->max.x) / 2;\n        const int32_t z_mid = (bounds->min.z + bounds->max.z) / 2;\n        Matrix_Push();\n        *g_MatrixPtr = g_ViewMatrix;\n        *g_WMatrixPtr = g_IDMatrix;\n        Matrix_TranslateAbs(\n            item->interp.result.pos.x, item->interp.result.floor,\n            item->interp.result.pos.z);\n        Matrix_RotY(item->interp.result.rot.y);\n        Matrix_TranslateRel(x_mid, 0, z_mid);\n        anchor_pos = Matrix_GetOffset_M(g_WMatrixPtr);\n        Matrix_Pop();\n    }\n\n    const int32_t base_y = anchor_floor - 16;\n    const int32_t sy = Math_Sin(item->interp.result.rot.y);\n    const int32_t cy = Math_Cos(item->interp.result.rot.y);\n\n    // Compute the world-space grid points with floor-conforming Y offsets.\n    XYZ_32 grid_world[M_SHADOW_GRID_POINTS];\n    for (int32_t i = 0; i < M_SHADOW_GRID_POINTS; i++) {\n        const int32_t lx = grid_local_x[i];\n        const int32_t lz = grid_local_z[i];\n        const int32_t rx = (lx * cy + lz * sy) >> W2V_SHIFT;\n        const int32_t rz = (lz * cy - lx * sy) >> W2V_SHIFT;\n\n        const int32_t wx = anchor_pos.x + rx;\n        const int32_t wz = anchor_pos.z + rz;\n\n        int16_t room_num = item->room_num;\n        XYZ_32 test_pos = { wx, anchor_floor, wz };\n        const SECTOR *const sector = Room_GetSector(test_pos, &room_num);\n        int32_t height = Room_GetHeight(sector, test_pos);\n        if (height == NO_HEIGHT) {\n            height = anchor_floor;\n        }\n        if (ABS(height - anchor_floor) > 196) {\n            height = anchor_floor;\n        }\n\n        grid_world[i] = (XYZ_32) {\n            .x = wx,\n            .y = base_y + (height - anchor_floor),\n            .z = wz,\n        };\n    }\n\n    const int32_t sprite_idx = shadow_obj->mesh_idx;\n    const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex(sprite_idx, 0);\n    const OUTPUT_TEXTURE_SIZE atlas_size =\n        Output_Textures_GetAtlasSize(uvw_idx / 4);\n    const OUTPUT_TEXTURE_SIZE quad_atlas_size[4] = {\n        atlas_size,\n        atlas_size,\n        atlas_size,\n        atlas_size,\n    };\n\n    OUTPUT_UVW sprite_uvw[4];\n    for (int32_t i = 0; i < 4; i++) {\n        const int32_t corner_uvw_idx =\n            Output_Textures_GetSpriteUVWIndex(sprite_idx, i);\n        sprite_uvw[i] = Output_Textures_GetUVW(corner_uvw_idx);\n    }\n\n    const float u_min =\n        MIN(MIN(sprite_uvw[0].u, sprite_uvw[1].u),\n            MIN(sprite_uvw[2].u, sprite_uvw[3].u));\n    const float u_max =\n        MAX(MAX(sprite_uvw[0].u, sprite_uvw[1].u),\n            MAX(sprite_uvw[2].u, sprite_uvw[3].u));\n    const float v_min =\n        MIN(MIN(sprite_uvw[0].v, sprite_uvw[1].v),\n            MIN(sprite_uvw[2].v, sprite_uvw[3].v));\n    const float v_max =\n        MAX(MAX(sprite_uvw[0].v, sprite_uvw[1].v),\n            MAX(sprite_uvw[2].v, sprite_uvw[3].v));\n    const float w = sprite_uvw[0].w;\n\n    const float u_span = u_max - u_min;\n    const float v_span = v_max - v_min;\n    const float denom = (float)(M_SHADOW_LINE_POINTS - 1);\n\n    for (int32_t row = 0; row < M_SHADOW_LINE_POINTS - 1; row++) {\n        const float v0 = v_min + v_span * ((float)row / denom);\n        const float v1 = v_min + v_span * ((float)(row + 1) / denom);\n        for (int32_t col = 0; col < M_SHADOW_LINE_POINTS - 1; col++) {\n            const float u0 = u_min + u_span * ((float)col / denom);\n            const float u1 = u_min + u_span * ((float)(col + 1) / denom);\n\n            const int32_t i0 = (row * M_SHADOW_LINE_POINTS) + col;\n            const int32_t i1 = i0 + 1;\n            const int32_t i2 = i0 + (M_SHADOW_LINE_POINTS + 1);\n            const int32_t i3 = i0 + M_SHADOW_LINE_POINTS;\n\n            const XYZ_32 quad_pos[4] = {\n                grid_world[i0],\n                grid_world[i1],\n                grid_world[i2],\n                grid_world[i3],\n            };\n            const OUTPUT_UVW quad_uvw[4] = {\n                { u0, v0, w },\n                { u1, v0, w },\n                { u1, v1, w },\n                { u0, v1, w },\n            };\n\n            OutputSource_PolyFX_StageQuadExtUV(\n                quad_pos, quad_uvw, quad_atlas_size, nullptr, quad_color,\n                VERT_NO_LIGHTING | VERT_NO_WIBBLE, DRAW_BLEND_SUB);\n        }\n    }\n    return true;\n}\n\nstatic void M_DrawScreenQuad(\n    const float x0, const float y0, const float x1, const float y1,\n    const float z, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl,\n    const RGBA_8888 br)\n{\n    OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) {\n        .x0 = x0,\n        .y0 = y0,\n        .x1 = x1,\n        .y1 = y1,\n        .tl = tl,\n        .tr = tr,\n        .bl = bl,\n        .br = br,\n        .z = Output_GetNearZ_UI() + z,\n    });\n}\n\nvoid Output_DrawBlackRectangle(const int32_t opacity)\n{\n    const int32_t sx = 0;\n    const int32_t sy = 0;\n    const int32_t sw = Viewport_GetWidth(VIEWPORT_UI);\n    const int32_t sh = Viewport_GetHeight(VIEWPORT_UI);\n    const RGBA_8888 background = { 0, 0, 0, opacity };\n    Output_DrawScreenFlatQuad(sx, sy, 0, sw, sh, background);\n}\n\nvoid Output_DrawRoom(const ROOM *const room, const bool is_outside)\n{\n    OutputSource_Rooms_StageRoom(room);\n    OutputSource_RoomsDebug_StageRoom(room);\n}\n\nvoid Output_DrawSprite(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t sprite_idx,\n    const int16_t shade, const RGB_F tint, const DRAW_TYPE draw_type)\n{\n    Matrix_Push();\n    Matrix_TranslateAbs(x, y, z);\n    OutputSource_Sprites_Stage(sprite_idx, shade, tint, draw_type);\n    Matrix_Pop();\n}\n\nvoid Output_DrawObjectMesh(const OBJECT_MESH *const mesh, const CLIP clip)\n{\n    OutputSource_Objects_StageObjectMesh(mesh);\n    if (g_Config.debug.enable_debug_spheres) {\n        Output_DrawSphere(mesh->center, mesh->radius);\n    }\n}\n\nvoid Output_DrawObjectMesh_I(const OBJECT_MESH *const mesh, const CLIP clip)\n{\n    Matrix_Push();\n    Matrix_Interpolate();\n    Output_DrawObjectMesh(mesh, clip);\n    Matrix_Pop();\n}\n\nvoid Output_DrawSkybox(const OBJECT_MESH *const mesh)\n{\n    float sunset_progress = Output_GetTimeInGame() / Output_GetSunsetDuration();\n    CLAMP(sunset_progress, 0.0f, 1.0f);\n    OutputSource_Objects_StageSkyboxMesh(\n        mesh, SHADE_NEUTRAL + SHADE_SUNSET * sunset_progress);\n    SceneCompositor_Flush();\n}\n\nvoid Output_DrawShadow(\n    const int16_t size, const BOUNDS_16 *const bounds, const ITEM *const item)\n{\n    if (!item->enable_shadow) {\n        return;\n    }\n\n    OUTPUT_ITEM_BIND *const bind = Output_Bind_GetItem(item);\n    if (bind->shadow_drawn) {\n        return;\n    }\n    bind->shadow_drawn = true;\n\n    if (g_Config.visuals.shadow_type == SHADOW_TYPE_SPRITE) {\n        if (M_DrawShadow_Sprite(size, bounds, item)) {\n            return;\n        }\n    }\n\n    const int32_t x_mid = (bounds->min.x + bounds->max.x) / 2;\n    const int32_t z_mid = (bounds->min.z + bounds->max.z) / 2;\n    const int32_t x_size = (bounds->max.x - bounds->min.x) * size / 1024;\n    const int32_t z_size = (bounds->max.z - bounds->min.z) * size / 1024;\n\n    Matrix_Push();\n    *g_MatrixPtr = g_ViewMatrix;\n    *g_WMatrixPtr = g_IDMatrix;\n    Matrix_TranslateAbs(\n        item->interp.result.pos.x, item->interp.result.floor,\n        item->interp.result.pos.z);\n    Matrix_RotY(item->interp.result.rot.y);\n    Matrix_TranslateRel(x_mid, 0, z_mid);\n    Matrix_ScaleX((1 << W2V_SHIFT) * x_size / UNIT_SHADOW);\n    Matrix_ScaleZ((1 << W2V_SHIFT) * z_size / UNIT_SHADOW);\n    OutputSource_Shadows_StageShadow();\n    Matrix_Pop();\n}\n\nvoid Output_DrawLightningSegment(const LIGHTNING_SEGMENT segment)\n{\n    OutputSource_Lightnings_StageSegment(&segment);\n}\n\nvoid Output_DrawScreenSprite(\n    const int32_t sx, const int32_t sy, const int32_t z, const int32_t scale_h,\n    const int32_t scale_v, const int32_t sprite_idx, const RGBA_F colors[4])\n{\n    const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_idx);\n    const int32_t x0 = sx + (scale_h * sprite->x0 / PHD_ONE);\n    const int32_t x1 = sx + (scale_h * sprite->x1 / PHD_ONE);\n    const int32_t y0 = sy + (scale_v * sprite->y0 / PHD_ONE);\n    const int32_t y1 = sy + (scale_v * sprite->y1 / PHD_ONE);\n    OutputSource_UI_StageSprite((OUTPUT_UI_SPRITE) {\n        .sprite_idx = sprite_idx,\n        .x0 = x0,\n        .y0 = y0,\n        .x1 = x1,\n        .y1 = y1,\n        .z = Output_GetNearZ_UI() + z,\n        .color = {\n            colors[0],\n            colors[1],\n            colors[2],\n            colors[3],\n        },\n    });\n}\n\nvoid Output_DrawScreenFlatQuad(\n    const int32_t sx, const int32_t sy, const int32_t z, const int32_t w,\n    const int32_t h, const RGBA_8888 color)\n{\n    M_DrawScreenQuad(sx, sy, sx + w, sy + h, z, color, color, color, color);\n}\n\nvoid Output_DrawScreenGradientQuad(\n    const int32_t sx, const int32_t sy, const int32_t z, const int32_t w,\n    const int32_t h, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl,\n    const RGBA_8888 br)\n{\n    M_DrawScreenQuad(sx, sy, sx + w, sy + h, z, tl, tr, bl, br);\n}\n\nvoid Output_DrawScreenFrame(\n    const int32_t sx, const int32_t sy, const int32_t w, const int32_t h,\n    const RGBA_8888 col_dark, const RGBA_8888 col_light, const float thickness)\n{\n    const float e = thickness;\n    const float x0 = sx;\n    const float y0 = sy;\n    const float x1 = sx + w;\n    const float y1 = sy + h;\n    const RGBA_8888 cd = col_dark;\n    const RGBA_8888 cl = col_light;\n\n    // clang-format off\n    M_DrawScreenQuad(x0,     y0,     x1 - e, y0 + e, 0, cd, cd, cd, cd);\n    M_DrawScreenQuad(x0 - e, y0 - e, x1,     y0,     0, cl, cl, cl, cl);\n    M_DrawScreenQuad(x1,     y0 - e, x1 + e, y1 + e, 0, cd, cd, cd, cd);\n    M_DrawScreenQuad(x1 - e, y0,     x1,     y1,     0, cl, cl, cl, cl);\n    M_DrawScreenQuad(x0,     y0,     x0 + e, y1 - e, 0, cd, cd, cd, cd);\n    M_DrawScreenQuad(x0 - e, y0 - e, x0,     y1,     0, cl, cl, cl, cl);\n    M_DrawScreenQuad(x0 - e, y1,     x1 + e, y1 + e, 0, cd, cd, cd, cd);\n    M_DrawScreenQuad(x0 - e, y1 - e, x1,     y1,     0, cl, cl, cl, cl);\n    // clang-format on\n}\n\nvoid Output_DrawPhotoModeFrame(const int32_t thickness)\n{\n    const VIEWPORT_RECT rect = Viewport_GetRect(VIEWPORT_UI);\n    const RGBA_8888 color = { 255, 0, 0, 96 };\n    OutputSource_UI_StagePhotoModeFrame(rect, color, thickness);\n}\n\nvoid Output_DrawSphere(const XYZ_16 center, const int32_t radius)\n{\n    const bool wireframe_state = g_Config.rendering.enable_wireframe;\n    const RGBA_8888 color_black = { 0, 0, 0, 128 };\n    const RGBA_8888 color_white = { 255, 255, 255, 128 };\n    const RGBA_8888 color = wireframe_state ? color_black : color_white;\n    Output_DrawSphereEx(center, radius, color);\n}\n\nvoid Output_DrawSphereEx(\n    const XYZ_16 center, const int32_t radius, const RGBA_8888 color)\n{\n    Matrix_Push();\n    Matrix_TranslateRel16(center);\n    Matrix_Scale(radius << W2V_SHIFT);\n    OutputSource_Misc_StageSphere(color);\n    Matrix_Pop();\n}\n\nvoid Output_DrawCuboid(const BOUNDS_16 *const bounds)\n{\n    Output_DrawCuboidEx(bounds, (RGBA_8888) { 255, 0, 0, 255 });\n}\n\nvoid Output_DrawCuboidEx(const BOUNDS_16 *const bounds, const RGBA_8888 color)\n{\n    const int32_t x0 = bounds->min.x;\n    const int32_t x1 = bounds->max.x;\n    const int32_t y0 = bounds->min.y;\n    const int32_t y1 = bounds->max.y;\n    const int32_t z0 = bounds->min.z;\n    const int32_t z1 = bounds->max.z;\n    const int32_t x_mid = (x0 + x1) / 2;\n    const int32_t y_mid = (y0 + y1) / 2;\n    const int32_t z_mid = (z0 + z1) / 2;\n    const int32_t x_size = (x1 - x0) / 2;\n    const int32_t y_size = (y1 - y0) / 2;\n    const int32_t z_size = (z1 - z0) / 2;\n    Matrix_Push();\n    Matrix_TranslateRel32((XYZ_32) { x_mid, y_mid, z_mid });\n    Matrix_ScaleX(x_size << W2V_SHIFT);\n    Matrix_ScaleY(y_size << W2V_SHIFT);\n    Matrix_ScaleZ(z_size << W2V_SHIFT);\n    OutputSource_Misc_StageCuboid(color);\n    Matrix_Pop();\n}\n"
  },
  {
    "path": "src/trx/game/output/draw.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n#include <trx/game/objects/types.h>\n#include <trx/game/output/types.h>\n#include <trx/game/rooms/types.h>\n\nvoid Output_DrawSkybox(const OBJECT_MESH *mesh);\nvoid Output_DrawObjectMesh(const OBJECT_MESH *mesh, CLIP clip);\nvoid Output_DrawObjectMesh_I(const OBJECT_MESH *mesh, CLIP clip);\nvoid Output_DrawRoom(const ROOM *room, bool is_outside);\n\nvoid Output_DrawSprite(\n    int32_t x, int32_t y, int32_t z, int16_t sprnum, int16_t shade, RGB_F tint,\n    DRAW_TYPE draw_type);\nvoid Output_DrawShadow(int16_t size, const BOUNDS_16 *bounds, const ITEM *item);\nvoid Output_DrawLightningSegment(const LIGHTNING_SEGMENT segment);\n\n// Fades\nvoid Output_DrawBlackRectangle(int32_t opacity);\n\n// UI\nvoid Output_DrawScreenSprite(\n    int32_t sx, int32_t sy, int32_t sz, int32_t scale_h, int32_t scale_v,\n    int32_t sprite_idx, const RGBA_F colors[4]);\nvoid Output_DrawScreenFlatQuad(\n    int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 color);\nvoid Output_DrawScreenGradientQuad(\n    int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 tl,\n    RGBA_8888 tr, RGBA_8888 bl, RGBA_8888 br);\nvoid Output_DrawScreenFrame(\n    int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 col_dark,\n    RGBA_8888 col_light, float thickness);\nvoid Output_DrawPhotoModeFrame(int32_t thickness);\n\nvoid Output_DrawSphere(XYZ_16 center, int32_t radius);\nvoid Output_DrawCuboid(const BOUNDS_16 *bounds);\nvoid Output_DrawSphereEx(XYZ_16 center, int32_t radius, RGBA_8888 color);\nvoid Output_DrawCuboidEx(const BOUNDS_16 *bounds, RGBA_8888 color);\n"
  },
  {
    "path": "src/trx/game/output/func.c",
    "content": "#include <trx/game/output/func.h>\n\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/math.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output/const.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/vars.h>\n#include <trx/gl/context.h>\n\n#include <math.h>\n\nCLIP Output_CheckBoundsClip(const BOUNDS_16 *const bounds)\n{\n    if (g_MatrixPtr->_23 >= Output_GetFarZ()) {\n        return CLIP_NOT_VISIBLE;\n    }\n\n    const XYZ_32 vtx[8] = {\n        { .x = bounds->min.x, .y = bounds->min.y, .z = bounds->min.z },\n        { .x = bounds->max.x, .y = bounds->min.y, .z = bounds->min.z },\n        { .x = bounds->max.x, .y = bounds->max.y, .z = bounds->min.z },\n        { .x = bounds->min.x, .y = bounds->max.y, .z = bounds->min.z },\n        { .x = bounds->min.x, .y = bounds->min.y, .z = bounds->max.z },\n        { .x = bounds->max.x, .y = bounds->min.y, .z = bounds->max.z },\n        { .x = bounds->max.x, .y = bounds->max.y, .z = bounds->max.z },\n        { .x = bounds->min.x, .y = bounds->max.y, .z = bounds->max.z },\n    };\n\n    int32_t num_z = 0;\n    int32_t x_min = INT32_MAX;\n    int32_t y_min = INT32_MAX;\n    int32_t x_max = INT32_MIN;\n    int32_t y_max = INT32_MIN;\n\n    for (int32_t i = 0; i < 8; i++) {\n        // clang-format off\n        const int32_t zv = (\n            g_MatrixPtr->_20 * vtx[i].x +\n            g_MatrixPtr->_21 * vtx[i].y +\n            g_MatrixPtr->_22 * vtx[i].z +\n            g_MatrixPtr->_23);\n        // clang-format on\n\n        if (zv <= Output_GetNearZ() || zv >= Output_GetFarZ()) {\n            continue;\n        }\n\n        num_z++;\n        const int32_t zp = zv / g_PhdPersp;\n        // clang-format off\n        const int32_t xv = (\n            g_MatrixPtr->_00 * vtx[i].x +\n            g_MatrixPtr->_01 * vtx[i].y +\n            g_MatrixPtr->_02 * vtx[i].z +\n            g_MatrixPtr->_03) / zp;\n        const int32_t yv = (\n            g_MatrixPtr->_10 * vtx[i].x +\n            g_MatrixPtr->_11 * vtx[i].y +\n            g_MatrixPtr->_12 * vtx[i].z +\n            g_MatrixPtr->_13) / zp;\n        // clang-format on\n\n        CLAMPG(x_min, xv);\n        CLAMPL(x_max, xv);\n        CLAMPG(y_min, yv);\n        CLAMPL(y_max, yv);\n    }\n\n    if (num_z == 0) {\n        return CLIP_NOT_VISIBLE; // out of screen\n    }\n\n    const VIEWPORT_RECT vp = Viewport_GetRect(VIEWPORT_GAME);\n    x_min += vp.w / 2;\n    x_max += vp.w / 2;\n    y_min += vp.h / 2;\n    y_max += vp.h / 2;\n\n    // clang-format off\n    if (x_min > g_PhdRight\n        || y_min > g_PhdBottom\n        || x_max < g_PhdLeft\n        || y_max < g_PhdTop) {\n        return CLIP_NOT_VISIBLE; // out of screen\n    }\n    // clang-format on\n\n    // clang-format off\n    if (num_z < 8 ||\n        x_min < 0 ||\n        y_min < 0 ||\n        x_max >= vp.w ||\n        y_max >= vp.h) {\n        return CLIP_PARTIALLY_VISIBLE;\n    }\n    // clang-format on\n\n    return CLIP_FULLY_VISIBLE;\n}\n\nvoid Output_MakeScreenshot(const char *const path)\n{\n    LOG_INFO(\"Taking screenshot\");\n    TRX_GL_Context_ScheduleScreenshot(path);\n}\n\nvoid Output_ApplyFOV(void)\n{\n    int32_t fov = Viewport_GetEffectiveFOV();\n    const int32_t sw = Viewport_GetWidth(VIEWPORT_GAME);\n    const int32_t sh = Viewport_GetHeight(VIEWPORT_GAME);\n    const float aspect = sw / (float)sh;\n\n    int32_t fov_width;\n    switch (Viewport_GetFOVMode()) {\n    case FOV_MODE_VERTICAL: {\n        const float fov_rad_h = fov * M_PI / (180 * DEG_1);\n        const float fov_rad_v = 2 * atan(aspect * tan(fov_rad_h / 2));\n        fov = round((fov_rad_v / M_PI) * (180 * DEG_1));\n        fov_width = sw;\n        break;\n    }\n    case FOV_MODE_HORIZONTAL:\n        fov_width = sw;\n        break;\n    case FOV_MODE_PC:\n        fov_width = sw * ((4.0f / 3.0f) / aspect);\n        break;\n    case FOV_MODE_PS1:\n        fov_width = sw * ((4.0f / 3.0f) / aspect) * 240 / 200;\n        break;\n    default:\n        ASSERT_FAIL();\n    }\n\n    const int16_t c = Math_Cos(fov / 2);\n    const int16_t s = Math_Sin(fov / 2);\n    g_PhdPersp = fov_width / 2;\n    if (s != 0) {\n        g_PhdPersp *= c;\n        g_PhdPersp /= s;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/output/func.h",
    "content": "#pragma once\n\n#include <trx/game/output/types.h>\n\nCLIP Output_CheckBoundsClip(const BOUNDS_16 *bounds);\n\nvoid Output_MakeScreenshot(const char *path);\n\nvoid Output_ApplyFOV(void);\n"
  },
  {
    "path": "src/trx/game/output/lights.c",
    "content": "#include <trx/game/output/lights.h>\n\n#include <trx/core/colors.h>\n#include <trx/core/math/geom.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/const.h>\n#include <trx/game/items/manager.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\n#include <string.h>\n\n#define M_LIGHT_CYCLE 32\n#define M_MAX_ROOM_LIGHT_UNIT (0x2000 / (M_LIGHT_CYCLE / 2))\n#define M_TR3_A_SHIFT 5\n#define M_TR3_DYNAMIC_FALLOFF_SHIFT 8\n\ntypedef struct {\n    XYZ_32 pos;\n    int32_t shade;\n} M_COMMON_LIGHT;\n\ntypedef struct {\n    int32_t table[M_LIGHT_CYCLE];\n} M_ROOM_LIGHT_TABLE;\n\nstatic bool m_IsSunsetEnabled = false;\nstatic int32_t m_RoomLightShades[RLM_NUMBER_OF] = {};\nstatic M_ROOM_LIGHT_TABLE m_RoomLightTables[M_LIGHT_CYCLE] = {};\nstatic VECTOR *m_DynamicLights = nullptr;\n\ntypedef struct {\n    XYZ_32 sun_dir_world;\n    XYZ_32 bulb_dir_world;\n    XYZ_32 dynamic_dir_world;\n    RGB_888 sun_color;\n    RGB_888 bulb_color;\n    RGB_888 dynamic_color;\n    uint8_t ambient;\n    struct {\n        bool has_sun : 1;\n        bool has_bulb : 1;\n        bool has_dynamic : 1;\n        bool has_ambient : 1;\n    } flags;\n} M_TR3_ITEM_LIGHT;\n\nstatic M_TR3_ITEM_LIGHT m_TR3ItemLights[MAX_ITEMS] = {};\n\nstatic RGB_F M_TR3_RGB15ToRGBF(const int16_t rgb15)\n{\n    const int32_t r8 = (rgb15 & 0x1F) << 3;\n    const int32_t g8 = (rgb15 & 0x3E0) >> 2;\n    const int32_t b8 = (rgb15 & 0x7C00) >> 7;\n    return (RGB_F) {\n        .r = r8 / 255.0f,\n        .g = g8 / 255.0f,\n        .b = b8 / 255.0f,\n    };\n}\n\nstatic int16_t M_TR3_ShadeFromMul(const float mul)\n{\n    float shade_f = (2.0f - mul) * (float)SHADE_NEUTRAL;\n    CLAMP(shade_f, 0.0f, SHADE_MAX);\n    return (int16_t)shade_f;\n}\n\nstatic uint8_t M_TR3_LerpU8Shift(\n    const uint8_t current, const uint8_t target, const int32_t shift)\n{\n    const int32_t cur = (int32_t)current;\n    const int32_t dst = (int32_t)target;\n    int32_t next = cur + ((dst - cur) >> shift);\n    CLAMP(next, 0, 255);\n    return (uint8_t)next;\n}\n\nstatic RGB_888 M_TR3_LerpRGBShift(\n    const RGB_888 current, const RGB_888 target, const int32_t shift)\n{\n    return (RGB_888) {\n        .r = M_TR3_LerpU8Shift(current.r, target.r, shift),\n        .g = M_TR3_LerpU8Shift(current.g, target.g, shift),\n        .b = M_TR3_LerpU8Shift(current.b, target.b, shift),\n    };\n}\n\nstatic XYZ_32 M_TR3_LerpXYZShift(\n    const XYZ_32 current, const XYZ_32 target, const int32_t shift)\n{\n    return (XYZ_32) {\n        .x = current.x + ((target.x - current.x) >> shift),\n        .y = current.y + ((target.y - current.y) >> shift),\n        .z = current.z + ((target.z - current.z) >> shift),\n    };\n}\n\nstatic XYZ_32 M_TR3_NormalizeDeltaWorld(const XYZ_32 delta)\n{\n    const int32_t dx = delta.x >> 2;\n    const int32_t dy = delta.y >> 2;\n    const int32_t dz = delta.z >> 2;\n    const uint32_t len =\n        Math_Sqrt((uint32_t)(SQUARE(dx) + SQUARE(dy) + SQUARE(dz)));\n    if (len == 0u) {\n        return (XYZ_32) { 0, 0, 0 };\n    }\n\n    return (XYZ_32) {\n        .x = (dx * (1 << W2V_SHIFT)) / (int32_t)len,\n        .y = (dy * (1 << W2V_SHIFT)) / (int32_t)len,\n        .z = (dz * (1 << W2V_SHIFT)) / (int32_t)len,\n    };\n}\n\nstatic XYZ_32 M_TR3_VectorViewFromWorld(const XYZ_32 v_world)\n{\n    const MATRIX *const m = &g_ViewMatrix;\n    return (XYZ_32) {\n        .x = (m->_00 * v_world.x + m->_01 * v_world.y + m->_02 * v_world.z)\n            >> W2V_SHIFT,\n        .y = (m->_10 * v_world.x + m->_11 * v_world.y + m->_12 * v_world.z)\n            >> W2V_SHIFT,\n        .z = (m->_20 * v_world.x + m->_21 * v_world.y + m->_22 * v_world.z)\n            >> W2V_SHIFT,\n    };\n}\n\nstatic void M_TR3_SetConstantLight(const RGB_F ambient)\n{\n    const RGB_F colors[3] = {};\n    const XYZ_32 dirs_view[3] = {};\n    Output_SetTR3Light(ambient, colors, dirs_view);\n}\n\nstatic void M_CalculateBrightestLight(\n    const XYZ_32 pos, const ROOM *const room,\n    M_COMMON_LIGHT *const brightest_light)\n{\n    if (room->light_mode != RLM_NORMAL) {\n        const int32_t light_shade = Output_GetRoomLightShade(room->light_mode);\n        for (int32_t i = 0; i < room->num_lights; i++) {\n            const LIGHT *const light = &room->lights[i];\n            const int32_t dx = pos.x - light->pos.x;\n            const int32_t dy = pos.y - light->pos.y;\n            const int32_t dz = pos.z - light->pos.z;\n\n            const int32_t falloff_1 = SQUARE(light->falloff.value_1) >> 12;\n            const int32_t falloff_2 = SQUARE(light->falloff.value_2) >> 12;\n            const int32_t dist = (SQUARE(dx) + SQUARE(dy) + SQUARE(dz)) >> 12;\n\n            const int32_t shade_1 =\n                falloff_1 * light->shade.value_1 / MAX(1, falloff_1 + dist);\n            const int32_t shade_2 =\n                falloff_2 * light->shade.value_2 / MAX(1, falloff_2 + dist);\n            const int32_t shade = shade_1\n                + (shade_2 - shade_1) * light_shade / (M_LIGHT_CYCLE - 1);\n\n            if (shade > brightest_light->shade) {\n                brightest_light->shade = shade;\n                brightest_light->pos = light->pos;\n            }\n        }\n        return;\n    }\n\n    const int32_t ambient = g_TRVersion == 1 ? (SHADE_MAX - room->ambient) : 0;\n    for (int32_t i = 0; i < room->num_lights; i++) {\n        const LIGHT *const light = &room->lights[i];\n        const int32_t dx = pos.x - light->pos.x;\n        const int32_t dy = pos.y - light->pos.y;\n        const int32_t dz = pos.z - light->pos.z;\n        const int32_t falloff = SQUARE(light->falloff.value_1) >> 12;\n        const int32_t dist = (SQUARE(dx) + SQUARE(dy) + SQUARE(dz)) >> 12;\n        const int32_t shade =\n            ambient + (falloff * light->shade.value_1 / (falloff + dist));\n        if (shade > brightest_light->shade) {\n            brightest_light->shade = shade;\n            brightest_light->pos = light->pos;\n        }\n    }\n}\n\nstatic int32_t M_CalculateDynamicLight(\n    const XYZ_32 pos, M_COMMON_LIGHT *const brightest_light)\n{\n    int32_t adder = 0;\n    for (int32_t i = 0; i < m_DynamicLights->count; i++) {\n        const LIGHT *const light = Vector_Get(m_DynamicLights, i);\n        const int32_t dx = pos.x - light->pos.x;\n        const int32_t dy = pos.y - light->pos.y;\n        const int32_t dz = pos.z - light->pos.z;\n        const int32_t radius = 1 << light->falloff.value_1;\n        if (dx < -radius || dx > radius || dy < -radius || dy > radius\n            || dz < -radius || dz > radius) {\n            continue;\n        }\n\n        const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (dist > SQUARE(radius)) {\n            continue;\n        }\n\n        const int32_t shade = (1 << light->shade.value_1)\n            - (dist >> (2 * light->falloff.value_1 - light->shade.value_1));\n        if (shade > brightest_light->shade) {\n            brightest_light->shade = shade;\n            brightest_light->pos = light->pos;\n        }\n        adder += shade;\n    }\n\n    return adder;\n}\n\nstatic void M_TR3_CalculateLightSmoothed(\n    const ITEM *const item, const XYZ_32 pos, const ROOM *const room)\n{\n    const LIGHT *sun_light = nullptr;\n    bool has_sun = false;\n\n    const LIGHT *brightest_light = nullptr;\n    int32_t brightest = -1;\n    XYZ_32 bulb_delta = {};\n\n    int32_t ambience = ((SHADE_MAX - room->ambient) >> M_TR3_A_SHIFT) + 1;\n\n    for (int32_t i = 0; i < room->num_lights; i++) {\n        const LIGHT *const light = &room->lights[i];\n\n        if (light->type != 0u) {\n            has_sun = true;\n            sun_light = light;\n            continue;\n        }\n\n        const int32_t falloff = light->falloff.value_1;\n        if (falloff <= 0) {\n            continue;\n        }\n\n        const int32_t dx = (light->pos.x - pos.x) >> 2;\n        const int32_t dy = (light->pos.y - pos.y) >> 2;\n        const int32_t dz = (light->pos.z - pos.z) >> 2;\n        const uint32_t distance =\n            Math_Sqrt((uint32_t)(SQUARE(dx) + SQUARE(dy) + SQUARE(dz)));\n\n        if ((int32_t)distance > falloff) {\n            continue;\n        }\n\n        const int32_t intensity = light->shade.value_1;\n        int32_t shade = intensity - (intensity * (int32_t)distance) / falloff;\n        CLAMPL(shade, 0);\n        ambience += shade >> 7;\n\n        if (shade > brightest) {\n            brightest = shade;\n            brightest_light = light;\n            bulb_delta = (XYZ_32) {\n                .x = light->pos.x - pos.x,\n                .y = light->pos.y - pos.y,\n                .z = light->pos.z - pos.z,\n            };\n        }\n    }\n\n    const LIGHT *brightest_dynamic = nullptr;\n    int32_t brightest_dyn_shade = -1;\n    XYZ_32 dyn_delta = {};\n\n    for (int32_t i = 0; i < m_DynamicLights->count; i++) {\n        const LIGHT *const light = Vector_Get(m_DynamicLights, i);\n        const int32_t falloff_half = light->falloff.value_1 >> 1;\n        if (falloff_half <= 0) {\n            continue;\n        }\n\n        const int32_t dx = light->pos.x - pos.x;\n        const int32_t dy = light->pos.y - pos.y;\n        const int32_t dz = light->pos.z - pos.z;\n        const int32_t max_dist = WALL_L * 8;\n        if (ABS(dx) > max_dist || ABS(dy) > max_dist || ABS(dz) > max_dist) {\n            continue;\n        }\n\n        const uint32_t distance =\n            Math_Sqrt((uint32_t)(SQUARE(dx) + SQUARE(dy) + SQUARE(dz)));\n\n        if ((int32_t)distance > falloff_half) {\n            continue;\n        }\n\n        int32_t shade =\n            SHADE_MAX - (SHADE_MAX * (int32_t)distance) / falloff_half;\n        CLAMPL(shade, 0);\n        ambience += shade >> 8;\n\n        if (shade > brightest_dyn_shade) {\n            brightest_dyn_shade = shade;\n            brightest_dynamic = light;\n            dyn_delta = (XYZ_32) {\n                .x = light->pos.x - pos.x,\n                .y = light->pos.y - pos.y,\n                .z = light->pos.z - pos.z,\n            };\n        }\n    }\n\n    CLAMP(ambience, 0, 255);\n\n    const uint8_t ambient_target = (uint8_t)ambience;\n\n    M_TR3_ITEM_LIGHT dummy = {};\n    M_TR3_ITEM_LIGHT *il = &dummy;\n    bool enable_smoothing = false;\n    if (item != nullptr) {\n        const int16_t item_num = Item_GetIndex(item);\n        if (item_num >= 0 && item_num < MAX_ITEMS) {\n            il = &m_TR3ItemLights[item_num];\n            enable_smoothing = true;\n        }\n    }\n\n    // Ambient (smoothed)\n    if (enable_smoothing && il->flags.has_ambient) {\n        il->ambient = M_TR3_LerpU8Shift(il->ambient, ambient_target, 3);\n    } else {\n        il->ambient = ambient_target;\n        il->flags.has_ambient = true;\n    }\n\n    // Sun (smoothed)\n    bool want_sun = false;\n    XYZ_32 sun_dir_world_target = {};\n    RGB_888 sun_target = {};\n\n    int32_t ambient_base = (SHADE_MAX - room->ambient) >> M_TR3_A_SHIFT;\n    CLAMP(ambient_base, 0, 255);\n\n    if (has_sun && sun_light != nullptr) {\n        want_sun = true;\n        sun_dir_world_target = (XYZ_32) {\n            .x = sun_light->dir.x,\n            .y = sun_light->dir.y,\n            .z = sun_light->dir.z,\n        };\n        sun_target = sun_light->color;\n    } else if (enable_smoothing && il->flags.has_sun) {\n        want_sun = true;\n        sun_dir_world_target = il->sun_dir_world;\n        sun_target = (RGB_888) { ambient_base, ambient_base, ambient_base };\n    }\n\n    if (want_sun) {\n        if (enable_smoothing && il->flags.has_sun) {\n            il->sun_dir_world =\n                M_TR3_LerpXYZShift(il->sun_dir_world, sun_dir_world_target, 3);\n            il->sun_color = M_TR3_LerpRGBShift(il->sun_color, sun_target, 3);\n        } else {\n            il->sun_dir_world = sun_dir_world_target;\n            il->sun_color = sun_target;\n            il->flags.has_sun = true;\n        }\n    }\n\n    // Bulb (smoothed)\n    bool want_bulb = false;\n    XYZ_32 bulb_dir_world_target = {};\n    RGB_888 bulb_target = {};\n\n    if (brightest_light != nullptr && brightest > 0) {\n        want_bulb = true;\n        bulb_dir_world_target = M_TR3_NormalizeDeltaWorld(bulb_delta);\n\n        int32_t r8 = (brightest * (int32_t)brightest_light->color.r) >> 13;\n        int32_t g8 = (brightest * (int32_t)brightest_light->color.g) >> 13;\n        int32_t b8 = (brightest * (int32_t)brightest_light->color.b) >> 13;\n        CLAMP(r8, 0, 255);\n        CLAMP(g8, 0, 255);\n        CLAMP(b8, 0, 255);\n        bulb_target = (RGB_888) { r8, g8, b8 };\n    } else if (enable_smoothing && il->flags.has_bulb) {\n        want_bulb = true;\n        bulb_dir_world_target = il->bulb_dir_world;\n        bulb_target = (RGB_888) { ambient_base, ambient_base, ambient_base };\n    }\n\n    if (want_bulb) {\n        if (enable_smoothing && il->flags.has_bulb) {\n            il->bulb_dir_world = M_TR3_LerpXYZShift(\n                il->bulb_dir_world, bulb_dir_world_target, 3);\n            il->bulb_color = M_TR3_LerpRGBShift(il->bulb_color, bulb_target, 3);\n        } else {\n            il->bulb_dir_world = bulb_dir_world_target;\n            il->bulb_color = bulb_target;\n            il->flags.has_bulb = true;\n        }\n    }\n\n    // Dynamic (smoothed while active, drops instantly when not present)\n    bool want_dynamic = false;\n    XYZ_32 dynamic_dir_world_target = {};\n    RGB_888 dynamic_target = {};\n\n    if (brightest_dynamic != nullptr && brightest_dyn_shade > 0) {\n        want_dynamic = true;\n        dynamic_dir_world_target = M_TR3_NormalizeDeltaWorld(dyn_delta);\n\n        int32_t r8 =\n            (brightest_dyn_shade * (int32_t)brightest_dynamic->color.r) >> 13;\n        int32_t g8 =\n            (brightest_dyn_shade * (int32_t)brightest_dynamic->color.g) >> 13;\n        int32_t b8 =\n            (brightest_dyn_shade * (int32_t)brightest_dynamic->color.b) >> 13;\n        CLAMP(r8, 0, 255);\n        CLAMP(g8, 0, 255);\n        CLAMP(b8, 0, 255);\n        dynamic_target = (RGB_888) { r8, g8, b8 };\n    }\n\n    if (want_dynamic) {\n        if (enable_smoothing && il->flags.has_dynamic) {\n            il->dynamic_dir_world = M_TR3_LerpXYZShift(\n                il->dynamic_dir_world, dynamic_dir_world_target, 1);\n            il->dynamic_color =\n                M_TR3_LerpRGBShift(il->dynamic_color, dynamic_target, 1);\n        } else {\n            il->dynamic_dir_world = dynamic_dir_world_target;\n            il->dynamic_color = dynamic_target;\n            il->flags.has_dynamic = true;\n        }\n    }\n\n    const RGB_F ambient = {\n        .r = il->ambient / 255.0f,\n        .g = il->ambient / 255.0f,\n        .b = il->ambient / 255.0f,\n    };\n\n    RGB_F colors[3] = {};\n    XYZ_32 dirs_view[3] = {};\n\n    if (want_sun && il->flags.has_sun) {\n        dirs_view[0] = M_TR3_VectorViewFromWorld(il->sun_dir_world);\n        colors[0] = (RGB_F) {\n            .r = il->sun_color.r / 255.0f,\n            .g = il->sun_color.g / 255.0f,\n            .b = il->sun_color.b / 255.0f,\n        };\n    }\n\n    if (want_bulb && il->flags.has_bulb) {\n        dirs_view[1] = M_TR3_VectorViewFromWorld(il->bulb_dir_world);\n        colors[1] = (RGB_F) {\n            .r = il->bulb_color.r / 255.0f,\n            .g = il->bulb_color.g / 255.0f,\n            .b = il->bulb_color.b / 255.0f,\n        };\n    }\n\n    if (want_dynamic && il->flags.has_dynamic) {\n        dirs_view[2] = M_TR3_VectorViewFromWorld(il->dynamic_dir_world);\n        colors[2] = (RGB_F) {\n            .r = il->dynamic_color.r / 255.0f,\n            .g = il->dynamic_color.g / 255.0f,\n            .b = il->dynamic_color.b / 255.0f,\n        };\n    }\n\n    Output_SetTR3Light(ambient, colors, dirs_view);\n\n    // Keep legacy scalar shade meaningful for sprite/effect code paths.\n    Output_SetLightDivider(0);\n    Output_SetLightAdder(\n        M_TR3_ShadeFromMul((ambient.r + ambient.g + ambient.b) / 3.0f));\n}\n\nvoid Output_CalculateLight(const XYZ_32 pos, const int16_t room_num)\n{\n    const ROOM *const room = Room_Get(room_num);\n\n    if (g_TRVersion >= 3) {\n        M_TR3_CalculateLightSmoothed(nullptr, pos, room);\n        return;\n    }\n\n    M_COMMON_LIGHT brightest_light = {};\n\n    M_CalculateBrightestLight(pos, room, &brightest_light);\n    int32_t adder = brightest_light.shade;\n    int32_t dynamic_adder = M_CalculateDynamicLight(pos, &brightest_light);\n\n    adder = (adder + dynamic_adder) / 2;\n    if (g_TRVersion == 1 && (room->num_lights > 0 || dynamic_adder > 0)) {\n        adder += (SHADE_MAX - room->ambient) / 2;\n    }\n\n    // TODO: use m_LsAdder and m_LsDivider once ported\n    int32_t global_adder;\n    int32_t global_divider;\n    if (adder == 0) {\n        global_adder = room->ambient;\n        global_divider = 0;\n    } else {\n        if (g_TRVersion == 1) {\n            global_adder = SHADE_MAX - adder;\n            const int32_t divider = brightest_light.shade == adder\n                ? adder\n                : brightest_light.shade - adder;\n            global_divider = (1 << W2V_SHIFT) * SHADE_NEUTRAL / divider;\n        } else {\n            global_adder = room->ambient - adder;\n            global_divider = (1 << W2V_SHIFT) * SHADE_NEUTRAL / adder;\n        }\n        int16_t angles[2];\n        Math_GetVectorAngles(\n            pos.x - brightest_light.pos.x, pos.y - brightest_light.pos.y,\n            pos.z - brightest_light.pos.z, angles);\n        Output_RotateLight(angles[1], angles[0]);\n    }\n\n    CLAMPG(global_adder, SHADE_MAX);\n\n    Output_SetLightAdder(global_adder);\n    Output_SetLightDivider(global_divider);\n}\n\nvoid Output_CalculateStaticLight(const int16_t adder)\n{\n    // TODO: use m_LsAdder\n    int32_t global_adder = adder - SHADE_NEUTRAL;\n    CLAMPG(global_adder, SHADE_MAX);\n    Output_SetLightAdder(global_adder);\n\n    if (g_TRVersion >= 3) {\n        float mul = 2.0f - (adder / (float)SHADE_NEUTRAL);\n        CLAMP(mul, 0.0f, 1.0f);\n        M_TR3_SetConstantLight((RGB_F) { mul, mul, mul });\n    }\n}\n\nvoid Output_CalculateStaticLightRGB15(const int16_t rgb15)\n{\n    if (g_TRVersion < 3) {\n        return;\n    }\n    M_TR3_SetConstantLight(M_TR3_RGB15ToRGBF(rgb15));\n}\n\nvoid Output_CalculateStaticLightRGB_F(const RGB_F rgb)\n{\n    if (g_TRVersion < 3) {\n        return;\n    }\n    M_TR3_SetConstantLight(rgb);\n}\n\nvoid Output_CalculateStaticMeshLight(\n    const XYZ_32 pos, const SHADE shade, const ROOM *const room)\n{\n    if (g_TRVersion >= 3) {\n        const RGB_F base = M_TR3_RGB15ToRGBF(shade.value_1 & 0x7FFF);\n        int32_t r = (int32_t)(base.r * 255.0f);\n        int32_t g = (int32_t)(base.g * 255.0f);\n        int32_t b = (int32_t)(base.b * 255.0f);\n\n        for (int32_t i = 0; i < m_DynamicLights->count; i++) {\n            const LIGHT *const light = Vector_Get(m_DynamicLights, i);\n            const int32_t falloff_half = light->falloff.value_1 >> 1;\n            if (falloff_half <= 0) {\n                continue;\n            }\n            const XYZ_32 delta = {\n                .x = pos.x - light->pos.x,\n                .y = pos.y - light->pos.y,\n                .z = pos.z - light->pos.z,\n            };\n            const uint32_t distance = XYZ_32_GetLength(delta);\n            if ((int32_t)distance > falloff_half) {\n                continue;\n            }\n\n            int32_t fall =\n                SHADE_MAX - (SHADE_MAX * (int32_t)distance) / falloff_half;\n            CLAMPL(fall, 0);\n            r += (fall * (int32_t)light->color.r) >> 13;\n            g += (fall * (int32_t)light->color.g) >> 13;\n            b += (fall * (int32_t)light->color.b) >> 13;\n        }\n\n        CLAMP(r, 0, 255);\n        CLAMP(g, 0, 255);\n        CLAMP(b, 0, 255);\n        const RGB_F ambient = { r / 255.0f, g / 255.0f, b / 255.0f };\n        M_TR3_SetConstantLight(ambient);\n\n        Output_SetLightDivider(0);\n        Output_SetLightAdder(\n            M_TR3_ShadeFromMul((ambient.r + ambient.g + ambient.b) / 3.0f));\n        return;\n    }\n\n    int32_t adder = shade.value_1;\n    if (room->light_mode != RLM_NORMAL) {\n        const int32_t room_shade = Output_GetRoomLightShade(room->light_mode);\n        adder +=\n            (shade.value_2 - shade.value_1) * room_shade / (M_LIGHT_CYCLE - 1);\n    }\n\n    for (int32_t i = 0; i < m_DynamicLights->count; i++) {\n        const LIGHT *const light = Vector_Get(m_DynamicLights, i);\n        const int32_t dx = pos.x - light->pos.x;\n        const int32_t dy = pos.y - light->pos.y;\n        const int32_t dz = pos.z - light->pos.z;\n        const int32_t radius = 1 << light->falloff.value_1;\n        if (dx < -radius || dx > radius || dy < -radius || dy > radius\n            || dz < -radius || dz > radius) {\n            continue;\n        }\n\n        const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (dist > SQUARE(radius)) {\n            continue;\n        }\n\n        adder -= (1 << light->shade.value_1)\n            - (dist >> (2 * light->falloff.value_1 - light->shade.value_1));\n        if (adder < 0) {\n            break;\n        }\n    }\n\n    Output_CalculateStaticLight(adder);\n}\n\nvoid Output_CalculateObjectLighting(\n    const ITEM *const item, const BOUNDS_16 *const bounds)\n{\n    if (item->shade.value_1 >= 0) {\n        Output_CalculateStaticMeshLight(\n            item->pos, item->shade, Room_Get(item->room_num));\n        return;\n    }\n\n    Matrix_PushUnit();\n    Matrix_TranslateSet32((XYZ_32) {});\n    Matrix_Rot16(item->rot);\n    Matrix_TranslateRel32((XYZ_32) {\n        .x = (bounds->min.x + bounds->max.x) / 2,\n        .y = (bounds->max.y + bounds->min.y) / 2,\n        .z = (bounds->max.z + bounds->min.z) / 2,\n    });\n    const GAME_VECTOR sample_pos = {\n        .room_num = item->room_num,\n        .pos = {\n            .x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT),\n            .y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT),\n            .z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT),\n        },\n    };\n    Matrix_Pop();\n\n    Output_CalculateObjectLightingAt(item, sample_pos);\n}\n\nvoid Output_CalculateObjectLightingAt(\n    const ITEM *const item, const GAME_VECTOR sample_pos)\n{\n    int16_t room_num = sample_pos.room_num;\n    if (g_TRVersion >= 3) {\n        Room_GetSector(sample_pos.pos, &room_num);\n        M_TR3_CalculateLightSmoothed(item, sample_pos.pos, Room_Get(room_num));\n    } else {\n        Output_CalculateLight(sample_pos.pos, room_num);\n    }\n}\n\nvoid Output_Lights_Init(void)\n{\n    if (m_DynamicLights == nullptr) {\n        m_DynamicLights = Vector_Create(sizeof(LIGHT));\n    }\n\n    memset(m_TR3ItemLights, 0, sizeof(m_TR3ItemLights));\n\n    for (int32_t i = 0; i < M_LIGHT_CYCLE; i++) {\n        for (int32_t j = 0; j < M_LIGHT_CYCLE; j++) {\n            m_RoomLightTables[i].table[j] = (j - (M_LIGHT_CYCLE / 2)) * i\n                * M_MAX_ROOM_LIGHT_UNIT / (M_LIGHT_CYCLE - 1);\n        }\n    }\n}\n\nvoid Output_Lights_Shutdown(void)\n{\n    if (m_DynamicLights != nullptr) {\n        Vector_Free(m_DynamicLights);\n        m_DynamicLights = nullptr;\n    }\n}\n\nvoid Output_Lights_ObserveLevelLoad(void)\n{\n    memset(m_TR3ItemLights, 0, sizeof(m_TR3ItemLights));\n}\n\nvoid Output_ResetDynamicLights(void)\n{\n    Vector_Clear(m_DynamicLights);\n}\n\nVECTOR *Output_GetDynamicLights(void)\n{\n    return m_DynamicLights;\n}\n\nvoid Output_AddDynamicLight(\n    const XYZ_32 pos, const int32_t intensity, const int32_t falloff)\n{\n    if (g_TRVersion >= 3) {\n        int32_t safe_intensity = intensity;\n        int32_t safe_falloff = falloff;\n        CLAMP(safe_intensity, 0, 30);\n        CLAMP(safe_falloff, 0, 30);\n\n        int32_t max_shade = 1 << safe_intensity;\n        int32_t c = max_shade >> 4;\n        CLAMPG(c, 255);\n\n        int32_t radius = 1 << safe_falloff;\n        int32_t falloff_param = radius >> 7;\n        CLAMP(falloff_param, 1, 255);\n\n        const LIGHT light = {\n            .pos = pos,\n            .shade = {},\n            .falloff.value_1 = falloff_param << M_TR3_DYNAMIC_FALLOFF_SHIFT,\n            .color = (RGB_888) { c, c, c },\n            .type = 0,\n            .dir = {},\n        };\n        Vector_Add(m_DynamicLights, &light);\n    } else {\n        const LIGHT light = {\n            .pos = pos,\n            .shade.value_1 = intensity,\n            .falloff.value_1 = falloff,\n            .color = COLOR_RGB_888_WHITE,\n            .type = 0,\n            .dir = {},\n        };\n        Vector_Add(m_DynamicLights, &light);\n    }\n}\n\nvoid Output_AddDynamicLightRGB(\n    const XYZ_32 pos, const int32_t falloff, const RGB_888 color)\n{\n    int32_t safe_falloff = falloff;\n    CLAMP(safe_falloff, 0, 255);\n\n    const LIGHT light = {\n        .pos = pos,\n        .shade = {},\n        .falloff.value_1 = safe_falloff << M_TR3_DYNAMIC_FALLOFF_SHIFT,\n        .color = color,\n        .type = g_TRVersion < 3 ? 1 : 0,\n        .dir = {},\n    };\n    Vector_Add(m_DynamicLights, &light);\n}\n\nint32_t Output_GetRoomLightShade(const ROOM_LIGHT_MODE mode)\n{\n    return m_RoomLightShades[mode];\n}\n\nint32_t Output_GetSunsetDuration(void)\n{\n    return 20 * 60 * LOGIC_FPS; // = 20 minutes / 36000 frames\n}\n\nvoid Output_SetSunsetEnabled(const bool enabled)\n{\n    m_IsSunsetEnabled = enabled;\n}\n\nint16_t Output_GetSkyShade(void)\n{\n    if (!m_IsSunsetEnabled) {\n        return SHADE_NEUTRAL;\n    }\n    float sunset_progress =\n        Output_GetTimeInGame() / (float)Output_GetSunsetDuration();\n    CLAMP(sunset_progress, 0.0f, 1.0f);\n    return SHADE_NEUTRAL + SHADE_SUNSET * sunset_progress;\n}\n\nvoid Output_AnimateLights(const int32_t num_frames)\n{\n    const int32_t time = ((int32_t)Output_GetTimeInGame()) % M_LIGHT_CYCLE;\n    if (g_TRVersion >= 2) {\n        m_RoomLightShades[RLM_FLICKER] = Random_GetDraw() % M_LIGHT_CYCLE;\n        m_RoomLightShades[RLM_GLOW] = (M_LIGHT_CYCLE - 1)\n                * (Math_Sin((time * DEG_360) / M_LIGHT_CYCLE) + 0x4000)\n            >> 15;\n\n        if (m_IsSunsetEnabled) {\n            int32_t sunset_timer = Output_GetTimeInGame();\n            CLAMPG(sunset_timer, Output_GetSunsetDuration());\n            m_RoomLightShades[RLM_SUNSET] =\n                sunset_timer * (M_LIGHT_CYCLE - 1) / Output_GetSunsetDuration();\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/output/lights.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/vector.h>\n#include <trx/game/objects/types.h>\n#include <trx/game/rooms.h>\n#include <trx/game/types.h>\n#include <trx/game/viewport.h>\n\nvoid Output_Lights_Init(void);\nvoid Output_Lights_Shutdown(void);\nvoid Output_Lights_ObserveLevelLoad(void);\n\nvoid Output_CalculateLight(XYZ_32 pos, int16_t room_num);\nvoid Output_CalculateStaticLight(int16_t adder);\nvoid Output_CalculateStaticLightRGB15(int16_t rgb15);\nvoid Output_CalculateStaticLightRGB_F(RGB_F rgb);\nvoid Output_CalculateStaticMeshLight(XYZ_32 pos, SHADE shade, const ROOM *room);\nvoid Output_CalculateObjectLighting(const ITEM *item, const BOUNDS_16 *bounds);\nvoid Output_CalculateObjectLightingAt(\n    const ITEM *item, const GAME_VECTOR sample_pos);\nint32_t Output_GetRoomLightShade(ROOM_LIGHT_MODE mode);\n\nint32_t Output_GetSunsetDuration(void);\nvoid Output_SetSunsetEnabled(bool enabled);\nint16_t Output_GetSkyShade(void);\n\nvoid Output_ResetDynamicLights(void);\nvoid Output_AddDynamicLight(XYZ_32 pos, int32_t intensity, int32_t falloff);\nvoid Output_AddDynamicLightRGB(XYZ_32 pos, int32_t falloff, RGB_888 color);\nVECTOR *Output_GetDynamicLights(void);\n\nvoid Output_AnimateLights(int32_t num_frames);\n"
  },
  {
    "path": "src/trx/game/output/mesh_batcher/batcher.c",
    "content": "#include <trx/game/output/mesh_batcher/batcher.h>\n\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/output.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/uniforms.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/output/vertex_range.h>\n#include <trx/gl/utils.h>\n\n#include <uthash.h>\n\ntypedef float M_MESH_SHADE;\n\ntypedef struct {\n    XYZW_F pos;\n    XYZW_F normal;\n    OUTPUT_USHORT flags;\n    RGBA_8888 color;\n} M_MESH_GEOM;\n\ntypedef struct {\n    OUTPUT_UVW uvw;\n    OUTPUT_TEXTURE_SIZE texture_size;\n    float trapezoid_ratio[2];\n} M_MESH_TEXTURE;\n\ntypedef struct M_MESH_BUF_BINDING {\n    OUTPUT_MESH *mesh;\n    M_MESH_GEOM *geom_data;\n    M_MESH_TEXTURE *tex_data;\n    M_MESH_SHADE *shade_data;\n    bool needs_room_lights;\n    bool needs_cpu_light;\n    bool needs_object_light;\n    bool needs_own_light;\n    int32_t vertex_start;\n    int32_t vertex_count;\n\n    int32_t opaque_index_start;\n    int32_t opaque_index_count;\n    int32_t blend_add_index_start;\n    int32_t blend_add_index_count;\n    int32_t transparent_index_start;\n    int32_t transparent_index_count;\n    int32_t transparent_face_count;\n    int32_t *transparent_face_index_starts;\n    int32_t *transparent_face_index_counts;\n\n    UT_hash_handle hh;\n} M_MESH_BUF_BINDING;\n\ntypedef struct {\n    int32_t sort_key;\n    const MESH_INSTANCE *inst;\n    const OUTPUT_MESH_FACE *face;\n    int32_t index_start;\n    int32_t index_count;\n} M_FACE_SORT;\n\ntypedef struct MESH_BATCHER {\n    SCENE_SOURCE source;\n\n    int32_t vertex_count;\n\n    VECTOR *bindings;\n    M_MESH_BUF_BINDING *binding_map;\n    VECTOR *staged[SCENE_PASS_COUNT];\n\n    OUTPUT_MESH_SHADER *shader;\n    GLuint vao;\n    struct {\n        GLuint geom;\n        GLuint tex;\n        GLuint shade;\n    } vbo;\n\n    VECTOR *transparent_sort; // M_FACE_SORT\n    struct {\n        GLuint opaque;\n        GLuint transparent;\n        GLuint blend_add;\n    } ebo;\n\n    int32_t opaque_total_indices;\n    int32_t blend_add_total_indices;\n    int32_t transparent_total_indices;\n    bool layout_dirty;\n} MESH_BATCHER;\n\nstatic M_MESH_BUF_BINDING *M_GetBinding(\n    const MESH_BATCHER *const batcher, const OUTPUT_MESH *const mesh)\n{\n    M_MESH_BUF_BINDING *bind = nullptr;\n    HASH_FIND_PTR(batcher->binding_map, &mesh, bind);\n    return bind;\n}\n\nstatic void M_FillGeometry(\n    M_MESH_GEOM *const geom, const OUTPUT_MESH_VERTEX *const vertex)\n{\n    geom->pos.x = vertex->pos.x;\n    geom->pos.y = vertex->pos.y;\n    geom->pos.z = vertex->pos.z;\n    geom->pos.w = vertex->pos.w;\n    geom->normal.x = vertex->normal.x;\n    geom->normal.y = vertex->normal.y;\n    geom->normal.z = vertex->normal.z;\n    geom->normal.w = vertex->light_table_idx;\n    geom->color = vertex->color;\n    geom->flags = vertex->flags;\n}\n\nstatic void M_FillTexture(\n    M_MESH_TEXTURE *const tex, const OUTPUT_MESH_VERTEX *const vertex)\n{\n    if (vertex->uvw_idx < 0) {\n        return;\n    }\n    tex->uvw = Output_Textures_GetUVW(vertex->uvw_idx);\n    tex->texture_size = Output_Textures_GetAtlasSize(vertex->uvw_idx / 4);\n    tex->trapezoid_ratio[0] = vertex->trapezoid_ratio[0];\n    tex->trapezoid_ratio[1] = vertex->trapezoid_ratio[1];\n}\n\nstatic void M_FillShade(\n    M_MESH_SHADE *const shade, const OUTPUT_MESH_VERTEX *const vertex)\n{\n    *shade = vertex->shade;\n}\n\nstatic void M_SyncRoom(\n    const MESH_BATCHER *const batcher, const M_MESH_BUF_BINDING *const bind,\n    const ROOM *const room)\n{\n    if (!bind->needs_room_lights) {\n        return;\n    }\n    Output_Uniforms_UploadRoomLights(Output_GetUniforms(), room);\n}\n\nstatic void M_AnimateBinding(\n    const MESH_BATCHER *const batcher, const M_MESH_BUF_BINDING *const bind)\n{\n    ASSERT(bind != nullptr);\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex);\n    const OUTPUT_MESH_VERTEX *const vertices =\n        Vector_GetData(bind->mesh->vertices);\n    for (int32_t i = 0; i < bind->mesh->animated_vertices->count; i++) {\n        const OUTPUT_VERTEX_RANGE *const range =\n            Vector_Get(bind->mesh->animated_vertices, i);\n        for (int32_t j = range->vertex_start;\n             j < range->vertex_start + range->vertex_count; j++) {\n            M_FillTexture(&bind->tex_data[j], &vertices[j]);\n        }\n        TRX_GL_TRACK_DATA(\n            glBufferSubData, GL_ARRAY_BUFFER,\n            (bind->vertex_start + range->vertex_start) * sizeof(M_MESH_TEXTURE),\n            range->vertex_count * sizeof(M_MESH_TEXTURE),\n            &bind->tex_data[range->vertex_start]);\n    }\n}\n\nstatic void M_UpdateMeshGeometry(\n    const MESH_BATCHER *const batcher, const OUTPUT_MESH *const mesh)\n{\n    M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, mesh);\n    const OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(mesh->vertices);\n    for (int32_t i = 0; i < bind->vertex_count; i++) {\n        M_FillGeometry(&bind->geom_data[i], &vertices[i]);\n    }\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_ARRAY_BUFFER,\n        bind->vertex_start * sizeof(M_MESH_GEOM),\n        bind->vertex_count * sizeof(M_MESH_GEOM), bind->geom_data);\n}\n\n// Compare two faces by camera-space depth.\nstatic int M_CompareFaceDepth(const void *const a, const void *const b)\n{\n    const M_FACE_SORT *const face_a = a;\n    const M_FACE_SORT *const face_b = b;\n    if (face_b->sort_key == face_a->sort_key) {\n        return face_b->inst - face_a->inst;\n    }\n    return face_b->sort_key - face_a->sort_key;\n}\n\n// Compute per-face view depth and sort the mesh's transparent ranges\n// back-to-front.\nstatic void M_SortTransparentFaces(const MESH_BATCHER *const batcher)\n{\n    const int n = batcher->transparent_sort->count;\n    if (n == 0) {\n        return;\n    }\n    M_FACE_SORT *const buf = Vector_GetData(batcher->transparent_sort);\n    M_FACE_SORT *bptr = buf;\n    for (int32_t i = 0; i < n; i++) {\n        // clang-format off\n        bptr->sort_key = (\n            bptr->inst->cwmatrix._20 * (int32_t)bptr->face->mesh_centroid.x +\n            bptr->inst->cwmatrix._21 * (int32_t)bptr->face->mesh_centroid.y +\n            bptr->inst->cwmatrix._22 * (int32_t)bptr->face->mesh_centroid.z +\n            bptr->inst->cwmatrix._23);\n        // clang-format on\n        bptr++;\n    }\n    qsort(buf, n, sizeof(*buf), M_CompareFaceDepth);\n}\n\nstatic void M_DrawOpaqueVertices(\n    const MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst)\n{\n    M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh);\n    const void *indices_offset =\n        (void *)(intptr_t)(bind->opaque_index_start * sizeof(uint32_t));\n    glDrawElementsBaseVertex(\n        GL_TRIANGLES, bind->opaque_index_count, GL_UNSIGNED_INT,\n        indices_offset, // Offset in EBO\n        bind->vertex_start // Offset in VBO (baseVertex)\n    );\n    TRX_GL_CheckError();\n    g_TRX_GL_Metrics.opaque_vert_count += bind->opaque_index_count;\n}\n\nstatic void M_DrawBlendAddVertices(\n    const MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst)\n{\n    M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh);\n    const void *indices_offset =\n        (void *)(intptr_t)(bind->blend_add_index_start * sizeof(uint32_t));\n    glDrawElementsBaseVertex(\n        GL_TRIANGLES, bind->blend_add_index_count, GL_UNSIGNED_INT,\n        indices_offset, // Offset in EBO\n        bind->vertex_start // Offset in VBO (baseVertex)\n    );\n    TRX_GL_CheckError();\n    g_TRX_GL_Metrics.blend_add_vert_count += bind->blend_add_index_count;\n}\n\nstatic void M_DrawOpaqueInstance(\n    MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst)\n{\n    M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh);\n    ASSERT(bind != nullptr);\n\n    M_SyncRoom(batcher, bind, inst->room);\n    if (bind->needs_object_light) {\n        Output_Uniforms_UploadCPULight(Output_GetUniforms(), &inst->light_info);\n    } else if (bind->needs_own_light) {\n        Output_Uniforms_UploadOwnLight(Output_GetUniforms(), &inst->light_info);\n    }\n    Output_MeshShader_UploadModelMatrix(batcher->shader, &inst->wmatrix);\n    Output_MeshShader_UploadTint(batcher->shader, inst->tint);\n\n    if (inst->enable_scissor) {\n        Output_EnableScissor(\n            inst->scissor.x, inst->scissor.y, inst->scissor.width,\n            inst->scissor.height);\n    }\n\n    Output_MeshShader_UploadWaterEffect(batcher->shader, inst->water_effect);\n    if (inst->wibble) {\n        Output_MeshShader_UploadWibbleEffect(batcher->shader, false);\n        glDepthMask(GL_FALSE);\n        M_DrawOpaqueVertices(batcher, inst);\n        glDepthMask(GL_TRUE);\n        Output_MeshShader_UploadWibbleEffect(batcher->shader, true);\n        M_DrawOpaqueVertices(batcher, inst);\n    } else {\n        Output_MeshShader_UploadWibbleEffect(batcher->shader, false);\n        M_DrawOpaqueVertices(batcher, inst);\n    }\n\n    if (inst->enable_scissor) {\n        Output_DisableScissor();\n    }\n}\n\nstatic void M_DrawBlendAddInstance(\n    MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst)\n{\n    M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, inst->mesh);\n    ASSERT(bind != nullptr);\n\n    M_SyncRoom(batcher, bind, inst->room);\n    if (bind->needs_object_light) {\n        Output_Uniforms_UploadCPULight(Output_GetUniforms(), &inst->light_info);\n    } else if (bind->needs_own_light) {\n        Output_Uniforms_UploadOwnLight(Output_GetUniforms(), &inst->light_info);\n    }\n    Output_MeshShader_UploadModelMatrix(batcher->shader, &inst->wmatrix);\n    Output_MeshShader_UploadTint(batcher->shader, inst->tint);\n    Output_MeshShader_UploadWaterEffect(batcher->shader, inst->water_effect);\n    Output_MeshShader_UploadWibbleEffect(batcher->shader, false);\n\n    if (inst->enable_scissor) {\n        Output_EnableScissor(\n            inst->scissor.x, inst->scissor.y, inst->scissor.width,\n            inst->scissor.height);\n    }\n    M_DrawBlendAddVertices(batcher, inst);\n    if (inst->enable_scissor) {\n        Output_DisableScissor();\n    }\n}\n\nstatic void M_OpaquePass(MESH_BATCHER *const batcher)\n{\n    VECTOR *const staged = batcher->staged[SCENE_PASS_OPAQUE];\n\n    glBindVertexArray(batcher->vao);\n    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.opaque);\n    for (int32_t i = 0; i < staged->count; i++) {\n        MESH_INSTANCE *const inst = Vector_Get(staged, i);\n        const M_MESH_BUF_BINDING *const bind =\n            M_GetBinding(batcher, inst->mesh);\n\n        if (inst->mesh->opaque_vertex_indices->count != 0) {\n            Output_AdjustDepth(0.0f, inst->depth_adjust * 2.0f / 0.005f);\n            M_DrawOpaqueInstance(batcher, inst);\n        }\n\n        // Accumulate transparent polygons and faces.\n        for (int32_t j = 0; j < inst->mesh->transparent_faces->count; j++) {\n            Vector_Add(\n                batcher->transparent_sort,\n                &(M_FACE_SORT) {\n                    .inst = inst,\n                    .face = Vector_Get(inst->mesh->transparent_faces, j),\n                    .index_start = bind->transparent_index_start\n                        + bind->transparent_face_index_starts[j],\n                    .index_count = bind->transparent_face_index_counts[j],\n                });\n        }\n    }\n\n    Output_AdjustDepth(0.0f, 0.0f);\n}\n\nstatic void M_BlendPass(MESH_BATCHER *const batcher, const SCENE_PASS pass)\n{\n    VECTOR *const staged = batcher->staged[pass];\n\n    glBindVertexArray(batcher->vao);\n    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.blend_add);\n    for (int32_t i = 0; i < staged->count; i++) {\n        const MESH_INSTANCE *const inst = Vector_Get(staged, i);\n        const M_MESH_BUF_BINDING *const bind =\n            M_GetBinding(batcher, inst->mesh);\n\n        if (inst->mesh->blend_add_vertex_indices->count != 0) {\n            Output_AdjustDepth(0.0f, inst->depth_adjust * 2.0f / 0.005f);\n            M_DrawBlendAddInstance(batcher, inst);\n        }\n    }\n\n    Output_AdjustDepth(0.0f, 0.0f);\n}\n\nstatic void M_TransparentPass(MESH_BATCHER *const batcher)\n{\n    if (batcher->transparent_sort->count == 0) {\n        return;\n    }\n\n    glBindVertexArray(batcher->vao);\n    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.transparent);\n\n    const MESH_INSTANCE *inst = nullptr;\n\n    for (int32_t i = 0; i < batcher->transparent_sort->count; i++) {\n        const M_FACE_SORT *const sort_ptr =\n            Vector_Get(batcher->transparent_sort, i);\n\n        if (sort_ptr->index_count == 0) {\n            continue;\n        }\n\n        if (sort_ptr->inst != inst) {\n            inst = sort_ptr->inst;\n            const M_MESH_BUF_BINDING *const bind =\n                M_GetBinding(batcher, inst->mesh);\n            ASSERT(bind != nullptr);\n            if (bind->needs_object_light) {\n                Output_Uniforms_UploadCPULight(\n                    Output_GetUniforms(), &inst->light_info);\n            } else if (bind->needs_own_light) {\n                Output_Uniforms_UploadOwnLight(\n                    Output_GetUniforms(), &inst->light_info);\n            }\n            Output_MeshShader_UploadModelMatrix(\n                batcher->shader, &inst->wmatrix);\n            Output_MeshShader_UploadTint(batcher->shader, inst->tint);\n            Output_MeshShader_UploadWaterEffect(\n                batcher->shader, inst->water_effect);\n            Output_MeshShader_UploadWibbleEffect(batcher->shader, inst->wibble);\n            Output_AdjustDepth(0.0f, inst->depth_adjust * 2.0f / 0.005f);\n            M_SyncRoom(batcher, bind, inst->room);\n        }\n\n        // indices live in the EBO starting at index_start\n        const void *index_offset =\n            (void *)(intptr_t)(sort_ptr->index_start * sizeof(uint32_t));\n\n        glDrawElements(\n            GL_TRIANGLES, sort_ptr->index_count, GL_UNSIGNED_INT, index_offset);\n\n        g_TRX_GL_Metrics.trans_vert_count += sort_ptr->index_count;\n    }\n\n    Output_AdjustDepth(0.0f, 0.0f);\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    MESH_BATCHER *const batcher = source->priv;\n    for (int32_t pass = 0; pass < SCENE_PASS_COUNT; pass++) {\n        Vector_Clear(batcher->staged[pass]);\n    }\n    Vector_Clear(batcher->transparent_sort);\n}\n\nstatic void M_RenderPass(\n    const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    MESH_BATCHER *const batcher = source->priv;\n\n    if (pass == SCENE_PASS_OPAQUE) {\n        M_OpaquePass(batcher);\n    } else if (pass == SCENE_PASS_TRANSPARENT) {\n        M_SortTransparentFaces(batcher);\n        M_TransparentPass(batcher);\n    } else if (pass == SCENE_PASS_BLEND_SUB) {\n        M_BlendPass(batcher, pass);\n    } else if (pass == SCENE_PASS_BLEND_ADD) {\n        M_BlendPass(batcher, pass);\n    }\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    const MESH_BATCHER *const batcher = source->priv;\n    return batcher->staged[pass]->count > 0\n        || (batcher->transparent_sort->count > 0\n            && pass == SCENE_PASS_TRANSPARENT);\n}\n\nstatic void M_RecalculateLayout(MESH_BATCHER *const batcher)\n{\n    batcher->vertex_count = 0;\n    batcher->opaque_total_indices = 0;\n    batcher->blend_add_total_indices = 0;\n    batcher->transparent_total_indices = 0;\n\n    for (int32_t i = 0; i < batcher->bindings->count; i++) {\n        M_MESH_BUF_BINDING *const bind =\n            *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i);\n\n        bind->vertex_start = batcher->vertex_count;\n        batcher->vertex_count += bind->vertex_count;\n\n        bind->opaque_index_start = batcher->opaque_total_indices;\n        batcher->opaque_total_indices += bind->opaque_index_count;\n\n        bind->blend_add_index_start = batcher->blend_add_total_indices;\n        batcher->blend_add_total_indices += bind->blend_add_index_count;\n\n        bind->transparent_index_start = batcher->transparent_total_indices;\n        batcher->transparent_total_indices += bind->transparent_index_count;\n    }\n    batcher->layout_dirty = false;\n}\n\nstatic void M_AnimateTextures(const SCENE_SOURCE *const source)\n{\n    MESH_BATCHER *const batcher = source->priv;\n    for (int32_t i = 0; i < batcher->bindings->count; i++) {\n        M_MESH_BUF_BINDING *const bind =\n            *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i);\n        M_AnimateBinding(batcher, bind);\n    }\n}\n\nMESH_BATCHER *MeshBatcher_Create(void)\n{\n    MESH_BATCHER *const batcher = Memory_Alloc(sizeof(MESH_BATCHER));\n    batcher->shader = Output_GetMeshShader();\n    batcher->bindings = Vector_Create(sizeof(OUTPUT_MESH *));\n    batcher->binding_map = nullptr;\n    for (int32_t pass = 0; pass < SCENE_PASS_COUNT; pass++) {\n        batcher->staged[pass] = Vector_Create(sizeof(MESH_INSTANCE));\n    }\n    batcher->source.render_begin = M_RenderBegin;\n    batcher->source.render_pass = M_RenderPass;\n    batcher->source.is_dirty = M_IsDirty;\n    batcher->source.animate_textures = M_AnimateTextures;\n    batcher->source.priv = batcher;\n\n    batcher->transparent_sort = Vector_Create(sizeof(M_FACE_SORT));\n    batcher->layout_dirty = true;\n\n    glGenVertexArrays(1, &batcher->vao);\n    glGenBuffers(3, &batcher->vbo.geom);\n\n    glBindVertexArray(batcher->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom);\n\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR);\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_MESH_GEOM),\n        (void *)(intptr_t)offsetof(M_MESH_GEOM, pos));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(M_MESH_GEOM),\n        (void *)(intptr_t)offsetof(M_MESH_GEOM, normal));\n    glVertexAttribIPointer(\n        OUTPUT_MESH_ATTR_FLAGS, 1, OUTPUT_USHORT_GL, sizeof(M_MESH_GEOM),\n        (void *)(intptr_t)offsetof(M_MESH_GEOM, flags));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE,\n        sizeof(M_MESH_GEOM), (void *)(intptr_t)offsetof(M_MESH_GEOM, color));\n\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_UVW);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO);\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_UVW, 3, GL_FLOAT, GL_FALSE, sizeof(M_MESH_TEXTURE),\n        (void *)(intptr_t)offsetof(M_MESH_TEXTURE, uvw));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_TEXTURE_SIZE, 4, GL_FLOAT, GL_FALSE,\n        sizeof(M_MESH_TEXTURE),\n        (void *)(intptr_t)offsetof(M_MESH_TEXTURE, texture_size));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 2, GL_FLOAT, GL_FALSE,\n        sizeof(M_MESH_TEXTURE),\n        (void *)(intptr_t)offsetof(M_MESH_TEXTURE, trapezoid_ratio));\n\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.shade);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE);\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_SHADE, 1, GL_FLOAT, GL_FALSE, sizeof(M_MESH_SHADE), 0);\n\n    glGenBuffers(3, &batcher->ebo.opaque);\n\n    return batcher;\n}\n\nvoid MeshBatcher_Destroy(MESH_BATCHER *const batcher)\n{\n    glBindVertexArray(0);\n    glBindBuffer(GL_ARRAY_BUFFER, 0);\n    if (batcher->vao != 0) {\n        glDeleteVertexArrays(1, &batcher->vao);\n        batcher->vao = 0;\n    }\n    if (batcher->vbo.geom != 0) {\n        glDeleteBuffers(3, &batcher->vbo.geom);\n        batcher->vbo.geom = 0;\n        batcher->vbo.tex = 0;\n        batcher->vbo.shade = 0;\n    }\n    if (batcher->ebo.opaque != 0) {\n        glDeleteBuffers(3, &batcher->ebo.opaque);\n        batcher->ebo.opaque = 0;\n        batcher->ebo.transparent = 0;\n        batcher->ebo.blend_add = 0;\n    }\n    ASSERT(batcher->bindings->count == 0);\n    if (batcher->bindings != nullptr) {\n        Vector_Free(batcher->bindings);\n        batcher->bindings = nullptr;\n    }\n    if (batcher->transparent_sort != nullptr) {\n        Vector_Free(batcher->transparent_sort);\n        batcher->transparent_sort = nullptr;\n    }\n    for (int32_t pass = 0; pass < SCENE_PASS_COUNT; pass++) {\n        if (batcher->staged[pass] != nullptr) {\n            Vector_Free(batcher->staged[pass]);\n            batcher->staged[pass] = nullptr;\n        }\n    }\n    Memory_Free(batcher);\n}\n\nvoid MeshBatcher_RemoveMesh(\n    MESH_BATCHER *const batcher, OUTPUT_MESH *const mesh)\n{\n    M_MESH_BUF_BINDING *const bind = M_GetBinding(batcher, mesh);\n    if (bind == nullptr) {\n        return;\n    }\n    Memory_Free(bind->geom_data);\n    Memory_Free(bind->tex_data);\n    Memory_Free(bind->shade_data);\n    Memory_Free(bind->transparent_face_index_starts);\n    Memory_Free(bind->transparent_face_index_counts);\n    Vector_Remove(batcher->bindings, &bind);\n    HASH_DEL(batcher->binding_map, bind);\n    batcher->layout_dirty = true;\n    Memory_Free(bind);\n}\n\nvoid MeshBatcher_AddMesh(MESH_BATCHER *const batcher, OUTPUT_MESH *const mesh)\n{\n    ASSERT(mesh->sealed == 1);\n\n    M_MESH_BUF_BINDING *const bind = Memory_Alloc(sizeof(M_MESH_BUF_BINDING));\n    bind->mesh = mesh;\n    bind->vertex_count = mesh->vertices->count;\n\n    // 1. Copy Vertex Data\n    const OUTPUT_MESH_VERTEX *const vertices = Vector_GetData(mesh->vertices);\n    bind->geom_data = Memory_Alloc(sizeof(M_MESH_GEOM) * bind->vertex_count);\n    bind->tex_data = Memory_Alloc(sizeof(M_MESH_TEXTURE) * bind->vertex_count);\n    bind->shade_data = Memory_Alloc(sizeof(M_MESH_SHADE) * bind->vertex_count);\n    for (int32_t i = 0; i < bind->vertex_count; i++) {\n        M_FillGeometry(&bind->geom_data[i], &vertices[i]);\n        M_FillTexture(&bind->tex_data[i], &vertices[i]);\n        M_FillShade(&bind->shade_data[i], &vertices[i]);\n        if ((vertices[i].flags & VERT_USE_DYNAMIC_LIGHT) != 0) {\n            bind->needs_room_lights = true;\n        }\n        if ((vertices[i].flags & VERT_USE_OBJECT_LIGHT) != 0) {\n            bind->needs_object_light = true;\n            bind->needs_cpu_light = true;\n        }\n        if ((vertices[i].flags & VERT_USE_OWN_LIGHT) != 0) {\n            bind->needs_own_light = true;\n            bind->needs_cpu_light = true;\n        }\n    }\n\n    // 2. Prepare index counts\n    // Opaque\n    bind->opaque_index_count = mesh->opaque_vertex_indices->count;\n    // Blend/Add\n    bind->blend_add_index_count = mesh->blend_add_vertex_indices->count;\n\n    // Transparent\n    bind->transparent_face_count = mesh->transparent_faces->count;\n    bind->transparent_face_index_starts = nullptr;\n    bind->transparent_face_index_counts = nullptr;\n    bind->transparent_index_count = 0;\n    if (bind->transparent_face_count > 0) {\n        bind->transparent_face_index_starts =\n            Memory_Alloc(sizeof(int32_t) * bind->transparent_face_count);\n        bind->transparent_face_index_counts =\n            Memory_Alloc(sizeof(int32_t) * bind->transparent_face_count);\n        for (int32_t i = 0; i < bind->transparent_face_count; i++) {\n            const OUTPUT_MESH_FACE *const face =\n                Vector_Get(mesh->transparent_faces, i);\n            bind->transparent_face_index_starts[i] =\n                bind->transparent_index_count;\n            bind->transparent_face_index_counts[i] = face->vertex_count;\n            bind->transparent_index_count += face->vertex_count;\n        }\n    }\n\n    // Prevent double add\n    mesh->sealed = 2;\n\n    Vector_Add(batcher->bindings, &bind);\n    HASH_ADD_PTR(batcher->binding_map, mesh, bind);\n    batcher->layout_dirty = true;\n}\n\nvoid MeshBatcher_Seal(MESH_BATCHER *const batcher)\n{\n    if (batcher->layout_dirty) {\n        M_RecalculateLayout(batcher);\n    }\n\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER,\n        batcher->vertex_count * sizeof(M_MESH_GEOM), nullptr,\n        GL_DYNAMIC_DRAW); // allow updating mesh flags\n\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER,\n        batcher->vertex_count * sizeof(M_MESH_TEXTURE), nullptr,\n        GL_DYNAMIC_DRAW); // allow animating textures\n\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.shade);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER,\n        batcher->vertex_count * sizeof(M_MESH_SHADE), nullptr, GL_DYNAMIC_DRAW);\n\n    // Upload vertex data\n    for (int32_t i = 0; i < batcher->bindings->count; i++) {\n        M_MESH_BUF_BINDING *const bind =\n            *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i);\n        glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom);\n        TRX_GL_TRACK_SUBDATA(\n            glBufferSubData, GL_ARRAY_BUFFER,\n            bind->vertex_start * sizeof(M_MESH_GEOM),\n            bind->vertex_count * sizeof(M_MESH_GEOM), bind->geom_data);\n        glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.tex);\n        TRX_GL_TRACK_SUBDATA(\n            glBufferSubData, GL_ARRAY_BUFFER,\n            bind->vertex_start * sizeof(M_MESH_TEXTURE),\n            bind->vertex_count * sizeof(M_MESH_TEXTURE), bind->tex_data);\n        glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.shade);\n        TRX_GL_TRACK_SUBDATA(\n            glBufferSubData, GL_ARRAY_BUFFER,\n            bind->vertex_start * sizeof(M_MESH_SHADE),\n            bind->vertex_count * sizeof(M_MESH_SHADE), bind->shade_data);\n    }\n\n    // Allocate CPU scratch memory for the combined indices\n    uint32_t *opaque_indices =\n        Memory_Alloc(batcher->opaque_total_indices * sizeof(uint32_t));\n    uint32_t *blend_indices =\n        Memory_Alloc(batcher->blend_add_total_indices * sizeof(uint32_t));\n    uint32_t *transparent_indices =\n        Memory_Alloc(batcher->transparent_total_indices * sizeof(uint32_t));\n\n    // Flatten the data\n    for (int32_t i = 0; i < batcher->bindings->count; i++) {\n        M_MESH_BUF_BINDING *const bind =\n            *(M_MESH_BUF_BINDING **)Vector_Get(batcher->bindings, i);\n\n        // Copy Opaque Indices\n        if (bind->opaque_index_count > 0) {\n            memcpy(\n                &opaque_indices[bind->opaque_index_start],\n                Vector_GetData(bind->mesh->opaque_vertex_indices),\n                bind->opaque_index_count * sizeof(uint32_t));\n        }\n\n        // Copy Blend Indices\n        if (bind->blend_add_index_count > 0) {\n            memcpy(\n                &blend_indices[bind->blend_add_index_start],\n                Vector_GetData(bind->mesh->blend_add_vertex_indices),\n                bind->blend_add_index_count * sizeof(uint32_t));\n        }\n\n        // Copy Transparent Indices\n        if (bind->transparent_index_count > 0) {\n            for (int32_t j = 0; j < bind->transparent_face_count; j++) {\n                const OUTPUT_MESH_FACE *const face =\n                    Vector_Get(bind->mesh->transparent_faces, j);\n                const int32_t dst_start = bind->transparent_index_start\n                    + bind->transparent_face_index_starts[j];\n                for (int32_t k = 0; k < face->vertex_count; k++) {\n                    transparent_indices[dst_start + k] =\n                        bind->vertex_start + face->vertex_indices[k];\n                }\n            }\n        }\n    }\n\n    // Upload to GPU\n    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.opaque);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ELEMENT_ARRAY_BUFFER,\n        batcher->opaque_total_indices * sizeof(uint32_t), opaque_indices,\n        GL_STATIC_DRAW);\n\n    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.blend_add);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ELEMENT_ARRAY_BUFFER,\n        batcher->blend_add_total_indices * sizeof(uint32_t), blend_indices,\n        GL_STATIC_DRAW);\n\n    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, batcher->ebo.transparent);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ELEMENT_ARRAY_BUFFER,\n        batcher->transparent_total_indices * sizeof(uint32_t),\n        transparent_indices, GL_STATIC_DRAW);\n\n    Memory_FreePointer(&opaque_indices);\n    Memory_FreePointer(&blend_indices);\n    Memory_FreePointer(&transparent_indices);\n}\n\nvoid MeshBatcher_UpdateMeshGeometry(\n    const MESH_BATCHER *const batcher, const OUTPUT_MESH *const mesh)\n{\n    if (mesh == nullptr) {\n        return;\n    }\n    glBindBuffer(GL_ARRAY_BUFFER, batcher->vbo.geom);\n    M_UpdateMeshGeometry(batcher, mesh);\n}\n\nvoid MeshBatcher_Stage(\n    MESH_BATCHER *const batcher, const MESH_INSTANCE *const inst,\n    const SCENE_PASS pass)\n{\n    if (inst->mesh == nullptr) {\n        return;\n    }\n    Vector_Add(batcher->staged[pass], inst);\n}\n\nconst SCENE_SOURCE *MeshBatcher_AsSource(const MESH_BATCHER *const batcher)\n{\n    return &batcher->source;\n}\n"
  },
  {
    "path": "src/trx/game/output/mesh_batcher/batcher.h",
    "content": "#pragma once\n\n#include <trx/game/output/mesh_batcher/mesh.h>\n#include <trx/game/output/scene_source.h>\n#include <trx/game/output/uniforms.h>\n#include <trx/game/rooms/types.h>\n#include <trx/game/viewport.h>\n\n#include <stdint.h>\n\ntypedef struct MESH_INSTANCE {\n    OUTPUT_MESH *mesh;\n\n    // TODO: use gl_InstanceID some day for this\n    // and glMultiDrawArraysIndirect\n    MATRIX cwmatrix;\n    MATRIX wmatrix;\n    const ROOM *room;\n    RGB_F tint;\n    bool wibble;\n    int32_t water_effect;\n\n    OUTPUT_LIGHT_INFO light_info;\n\n    bool enable_scissor;\n    bool disable_z_writes;\n    float depth_adjust;\n    VIEWPORT_RECT scissor;\n} MESH_INSTANCE;\n\ntypedef struct MESH_BATCHER MESH_BATCHER;\n\nMESH_BATCHER *MeshBatcher_Create(void);\nvoid MeshBatcher_Destroy(struct MESH_BATCHER *batcher);\nvoid MeshBatcher_AddMesh(struct MESH_BATCHER *batcher, OUTPUT_MESH *mesh);\nvoid MeshBatcher_RemoveMesh(struct MESH_BATCHER *batcher, OUTPUT_MESH *mesh);\nvoid MeshBatcher_Seal(MESH_BATCHER *batcher);\n\nconst SCENE_SOURCE *MeshBatcher_AsSource(const struct MESH_BATCHER *batcher);\n\nvoid MeshBatcher_Stage(\n    struct MESH_BATCHER *batcher, const MESH_INSTANCE *inst, SCENE_PASS pass);\nvoid MeshBatcher_UpdateMeshGeometry(\n    const struct MESH_BATCHER *batcher, const OUTPUT_MESH *mesh);\n"
  },
  {
    "path": "src/trx/game/output/mesh_batcher/mesh.c",
    "content": "#include <trx/game/output/mesh_batcher/mesh.h>\n\n#include <trx/core/memory.h>\n#include <trx/game/output/vertex_range.h>\n\nOUTPUT_MESH *Output_Mesh_Create(void)\n{\n    OUTPUT_MESH *const mesh = Memory_Alloc(sizeof(OUTPUT_MESH));\n    Memory_ArenaReset(&mesh->allocator);\n    mesh->vertices = Vector_Create(sizeof(OUTPUT_MESH_VERTEX));\n    mesh->animated_vertices = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE));\n    mesh->transparent_faces = Vector_Create(sizeof(OUTPUT_MESH_FACE));\n    mesh->opaque_vertex_indices = Vector_Create(sizeof(uint32_t));\n    mesh->blend_add_vertex_indices = Vector_Create(sizeof(uint32_t));\n    mesh->sealed = false;\n    return mesh;\n}\n\nvoid Output_Mesh_Destroy(OUTPUT_MESH *const mesh)\n{\n    if (mesh->animated_vertices != nullptr) {\n        Vector_Free(mesh->animated_vertices);\n    }\n    Vector_Free(mesh->vertices);\n    if (mesh->transparent_faces != nullptr) {\n        Vector_Free(mesh->transparent_faces);\n    }\n    if (mesh->opaque_vertex_indices != nullptr) {\n        Vector_Free(mesh->opaque_vertex_indices);\n    }\n    if (mesh->blend_add_vertex_indices != nullptr) {\n        Vector_Free(mesh->blend_add_vertex_indices);\n    }\n    Memory_ArenaFree(&mesh->allocator);\n    Memory_Free(mesh);\n}\n"
  },
  {
    "path": "src/trx/game/output/mesh_batcher/mesh.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output/textures.h>\n\ntypedef struct {\n    // attribute 2\n    OUTPUT_UVW uvw;\n    // attribute 3\n    OUTPUT_TEXTURE_SIZE texture_size;\n    // attribute 4\n    float trapezoid_ratio[2];\n} OUTPUT_MESH_TEXTURE;\n\ntypedef struct {\n    XYZW_F pos;\n    XYZ_F normal;\n    uint16_t flags;\n    int32_t uvw_idx;\n    float trapezoid_ratio[2];\n    int16_t shade;\n    RGBA_8888 color;\n    uint8_t light_table_idx;\n} OUTPUT_MESH_VERTEX;\n\n// Describes a contiguous block of vertices belonging to one face,\n// with sort keys.\ntypedef struct {\n    int32_t vertex_count;\n    int32_t *vertex_indices;\n    XYZ_F mesh_centroid;\n} OUTPUT_MESH_FACE;\n\ntypedef struct {\n    VECTOR *vertices;\n\n    MEMORY_ARENA_ALLOCATOR allocator;\n    int32_t sealed;\n    VECTOR *animated_vertices; // OUTPUT_VERTEX_RANGE\n    VECTOR *transparent_faces; // OUTPUT_MESH_FACE\n    VECTOR *opaque_vertex_indices; // uint32_t\n    VECTOR *blend_add_vertex_indices; // uint32_t\n} OUTPUT_MESH;\n\nOUTPUT_MESH *Output_Mesh_Create(void);\nvoid Output_Mesh_Destroy(OUTPUT_MESH *mesh);\n"
  },
  {
    "path": "src/trx/game/output/mesh_batcher/mesh_builder.c",
    "content": "#include <trx/game/output/mesh_batcher/mesh_builder.h>\n\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/output/mesh_batcher/mesh.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/vertex_range.h>\n\n#include <string.h>\n\nstruct MESH_BUILDER {\n    size_t pending_vertex_count;\n    OUTPUT_MESH *mesh;\n    VECTOR *indices;\n};\n\nstatic void M_EnsureMesh(MESH_BUILDER *const builder)\n{\n    if (builder->mesh == nullptr) {\n        builder->mesh = Output_Mesh_Create();\n        builder->pending_vertex_count = 0;\n    }\n}\n\nstatic void M_AddAnimatedVertexRanges(\n    OUTPUT_MESH *const mesh, const OUTPUT_MESH_VERTEX *const vertices,\n    const size_t vertex_count, const size_t vertex_start)\n{\n    size_t range_start = 0;\n    size_t range_count = 0;\n\n    for (size_t i = 0; i < vertex_count; i++) {\n        const bool animated = !(vertices[i].flags & VERT_FLAT_SHADED)\n            && Output_Textures_IsObjectTextureAnimated(vertices[i].uvw_idx / 4);\n        if (!animated) {\n            if (range_count > 0) {\n                Vector_Add(\n                    mesh->animated_vertices,\n                    &(OUTPUT_VERTEX_RANGE) {\n                        .vertex_start = vertex_start + range_start,\n                        .vertex_count = range_count,\n                    });\n                range_count = 0;\n            }\n            continue;\n        }\n\n        if (range_count == 0) {\n            range_start = i;\n        }\n        range_count++;\n    }\n\n    if (range_count > 0) {\n        Vector_Add(\n            mesh->animated_vertices,\n            &(OUTPUT_VERTEX_RANGE) {\n                .vertex_start = vertex_start + range_start,\n                .vertex_count = range_count,\n            });\n    }\n}\n\nstatic void M_FillFanIndices(\n    VECTOR *const indices, const size_t vertex_count, const bool double_sided)\n{\n    ASSERT(vertex_count >= 3);\n    const size_t tri_count = vertex_count - 2;\n    const size_t index_count = tri_count * 3 * (double_sided ? 2 : 1);\n    int32_t *const out = Vector_Expand(indices, index_count);\n    for (size_t i = 0, j = 0; i < tri_count; i++) {\n        out[j++] = 0;\n        out[j++] = i + 2;\n        out[j++] = i + 1;\n\n        if (double_sided) {\n            out[j++] = i + 1;\n            out[j++] = i + 2;\n            out[j++] = 0;\n        }\n    }\n}\n\nMESH_BUILDER *MeshBuilder_Create(void)\n{\n    MESH_BUILDER *const builder = Memory_Alloc(sizeof(*builder));\n    builder->indices = Vector_Create(sizeof(int32_t));\n    return builder;\n}\n\nvoid MeshBuilder_Destroy(MESH_BUILDER *const builder)\n{\n    ASSERT(builder != nullptr);\n    if (builder->mesh != nullptr) {\n        Output_Mesh_Destroy(builder->mesh);\n        builder->mesh = nullptr;\n    }\n    if (builder->indices != nullptr) {\n        Vector_Free(builder->indices);\n        builder->indices = nullptr;\n    }\n\n    Memory_Free(builder);\n}\n\nvoid MeshBuilder_AddVertex(\n    MESH_BUILDER *const builder, const OUTPUT_MESH_VERTEX *const vertex)\n{\n    MeshBuilder_AddVertices(builder, vertex, 1);\n}\n\nvoid MeshBuilder_AddVertices(\n    MESH_BUILDER *const builder, const OUTPUT_MESH_VERTEX *const vertices,\n    const size_t vertex_count)\n{\n    ASSERT(builder != nullptr);\n    ASSERT(vertex_count > 0);\n    M_EnsureMesh(builder);\n    ASSERT(builder->mesh != nullptr);\n    ASSERT(!builder->mesh->sealed);\n\n    const size_t vertex_start = builder->mesh->vertices->count;\n    memcpy(\n        Vector_Expand(builder->mesh->vertices, vertex_count), vertices,\n        sizeof(OUTPUT_MESH_VERTEX) * vertex_count);\n    M_AddAnimatedVertexRanges(\n        builder->mesh, vertices, vertex_count, vertex_start);\n    builder->pending_vertex_count += vertex_count;\n}\n\nvoid MeshBuilder_AddFace(\n    MESH_BUILDER *const builder, const SCENE_PASS pass, const int32_t *indices,\n    const size_t idx_count)\n{\n    ASSERT(builder != nullptr);\n    ASSERT(\n        (pass == SCENE_PASS_TRANSPARENT) || (pass == SCENE_PASS_OPAQUE)\n        || (pass == SCENE_PASS_BLEND_SUB) || (pass == SCENE_PASS_BLEND_ADD));\n    M_EnsureMesh(builder);\n    ASSERT(builder->mesh != nullptr);\n    ASSERT(!builder->mesh->sealed);\n\n    const size_t vtx_count = builder->pending_vertex_count;\n    const size_t start = builder->mesh->vertices->count - vtx_count;\n    const OUTPUT_MESH_VERTEX *const vbuf =\n        Vector_GetData(builder->mesh->vertices);\n    XYZ_F centroid = { 0.0f, 0.0f, 0.0f };\n    for (size_t i = 0; i < vtx_count; i++) {\n        centroid.x += vbuf[start + i].pos.x;\n        centroid.y += vbuf[start + i].pos.y;\n        centroid.z += vbuf[start + i].pos.z;\n    }\n    centroid.x /= (float)vtx_count;\n    centroid.y /= (float)vtx_count;\n    centroid.z /= (float)vtx_count;\n    if (pass == SCENE_PASS_TRANSPARENT) {\n        OUTPUT_MESH_FACE face = {\n            .vertex_count = idx_count,\n            .mesh_centroid = centroid,\n        };\n        face.vertex_indices = Memory_ArenaAlloc(\n            &builder->mesh->allocator, sizeof(int32_t) * idx_count);\n        for (size_t i = 0; i < idx_count; i++) {\n            face.vertex_indices[i] = start + indices[i];\n        }\n        Vector_Add(builder->mesh->transparent_faces, &face);\n    }\n    VECTOR *const target = pass == SCENE_PASS_BLEND_ADD\n        ? builder->mesh->blend_add_vertex_indices\n        : builder->mesh->opaque_vertex_indices;\n    uint32_t *const out = Vector_Expand(target, idx_count);\n    for (size_t i = 0; i < idx_count; i++) {\n        out[i] = start + indices[i];\n    }\n    builder->pending_vertex_count = 0;\n}\n\nvoid MeshBuilder_AddFan(\n    MESH_BUILDER *const builder, const SCENE_PASS pass, const bool double_sided)\n{\n    ASSERT(builder != nullptr);\n    M_EnsureMesh(builder);\n    const size_t vtx_count = builder->pending_vertex_count;\n    ASSERT(vtx_count >= 3);\n    M_FillFanIndices(builder->indices, vtx_count, double_sided);\n    MeshBuilder_AddFace(\n        builder, pass, Vector_GetData(builder->indices),\n        builder->indices->count);\n    Vector_Clear(builder->indices);\n}\n\nvoid MeshBuilder_AddRoomSprite(\n    MESH_BUILDER *const builder, const ROOM_SPRITE *const room_sprite,\n    const ROOM *const room, const float depth_adjust,\n    const uint16_t extra_flags)\n{\n    const int16_t texture_idx = room_sprite->texture;\n    const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(texture_idx);\n    const ROOM_VERTEX *const room_vert =\n        &room->mesh.vertices[room_sprite->vertex];\n    const XYZ_16 *pos = &room_vert->pos;\n    const struct {\n        float x, y;\n    } normal[4] = {\n        { .x = sprite->x0, .y = sprite->y0 },\n        { .x = sprite->x1, .y = sprite->y0 },\n        { .x = sprite->x1, .y = sprite->y1 },\n        { .x = sprite->x0, .y = sprite->y1 },\n    };\n    for (int32_t j = 0; j < 4; j++) {\n        const OUTPUT_MESH_VERTEX vertex = {\n            .pos = { .x = pos->x, .y = pos->y, .z = pos->z, .w = depth_adjust },\n            .normal = { .x = normal[j].x, .y = normal[j].y, .z = 0.0f },\n            .flags = Output_Textures_GetSpriteTextureFlags(texture_idx)\n                | extra_flags,\n            .color = { 255, 255, 255, 255 },\n            .uvw_idx = Output_Textures_GetSpriteUVWIndex(texture_idx, j),\n            .shade = room_vert->light_base,\n            .trapezoid_ratio = { 1.0f, 1.0f },\n        };\n        MeshBuilder_AddVertex(builder, &vertex);\n    }\n    MeshBuilder_AddFan(builder, SCENE_PASS_TRANSPARENT, false);\n}\n\nvoid MeshBuilder_AdjustDepth(MESH_BUILDER *const builder, const float depth)\n{\n    if (builder->mesh == nullptr || builder->mesh->vertices == nullptr) {\n        return;\n    }\n    OUTPUT_MESH_VERTEX *const vbuf = Vector_GetData(builder->mesh->vertices);\n    for (int32_t i = 0; i < builder->mesh->vertices->count; i++) {\n        vbuf[i].pos.w = depth;\n    }\n}\n\nOUTPUT_MESH *MeshBuilder_Seal(MESH_BUILDER *const builder)\n{\n    ASSERT(builder != nullptr);\n    if (builder->mesh == nullptr) {\n        return nullptr;\n    }\n    OUTPUT_MESH *const mesh = builder->mesh;\n    Output_GlueVertexRanges(mesh->animated_vertices);\n    mesh->sealed = 1;\n    builder->mesh = nullptr;\n    return mesh;\n}\n"
  },
  {
    "path": "src/trx/game/output/mesh_batcher/mesh_builder.h",
    "content": "#pragma once\n\n#include <trx/game/output/mesh_batcher/mesh.h>\n#include <trx/game/output/scene_source.h>\n#include <trx/game/rooms/types.h>\n\n// Opaque builder for incrementally constructing an OUTPUT_MESH.\ntypedef struct MESH_BUILDER MESH_BUILDER;\n\n// Create a new mesh builder. Call MeshBuilder_Seal() when done, then\n// MeshBuilder_Destroy().\nMESH_BUILDER *MeshBuilder_Create(void);\n\n// Destroy the builder state. Does NOT free the emitted OUTPUT_MESHes.\nvoid MeshBuilder_Destroy(MESH_BUILDER *builder);\n\n// Append one vertex to the mesh under construction.\n// Must be called before adding faces for those vertices.\nvoid MeshBuilder_AddVertex(\n    MESH_BUILDER *builder, const OUTPUT_MESH_VERTEX *vertex);\n\nvoid MeshBuilder_AddVertices(\n    MESH_BUILDER *builder, const OUTPUT_MESH_VERTEX *vertices,\n    size_t vertex_count);\n\n// Add a face using the recently added vertices.\nvoid MeshBuilder_AddFace(\n    MESH_BUILDER *builder, SCENE_PASS pass, const int32_t *indices,\n    size_t idx_count);\n\n// Add a triangle fan face using the last vertices added: a center followed by\n// ring vertices.If double_sided is true, generates mirrored winding\n// for backfaces as well.\nvoid MeshBuilder_AddFan(\n    MESH_BUILDER *builder, SCENE_PASS pass, bool double_sided);\n\n// Applies invisible z offset to all vertices that helps with the z-fighting.\nvoid MeshBuilder_AdjustDepth(MESH_BUILDER *builder, float depth);\n\n// Finalize all pending vertices and faces into the OUTPUT_MESH and seal it.\n// Returns the sealed mesh; builder must still be destroyed via\n// MeshBuilder_Destroy().\nOUTPUT_MESH *MeshBuilder_Seal(MESH_BUILDER *builder);\n\n// Utility method to add a quad representing a room sprite.\nvoid MeshBuilder_AddRoomSprite(\n    MESH_BUILDER *builder, const ROOM_SPRITE *room_sprite, const ROOM *room,\n    float depth_adjust, uint16_t extra_flags);\n"
  },
  {
    "path": "src/trx/game/output/overlay.h",
    "content": "#pragma once\n\nvoid Output_Overlay_DrawGame(void);\nvoid Output_Overlay_DrawGameMono(float desaturation);\nvoid Output_Overlay_DrawGameMonoCool(float desaturation);\nvoid Output_Overlay_DrawGameMonoWarm(float desaturation);\nvoid Output_Overlay_DrawPattern(bool wave);\nvoid Output_Overlay_DrawPatternOpacity(bool wave, float opacity);\nvoid Output_Overlay_DrawBlackRectangle(float opacity, bool post_ui);\nbool Output_Overlay_LoadImage(const char *file_name);\nvoid Output_Overlay_DrawImage(const char *file_name);\nvoid Output_Overlay_DrawImageBilinear(const char *file_name);\nvoid Output_Overlay_DrawImageMono(const char *file_name, float intensity);\nvoid Output_Overlay_CaptureSnapshot(void);\nvoid Output_Overlay_DrawSnapshot(float opacity);\nvoid Output_Overlay_BeginTransitionFadeOut(float duration, float start);\n"
  },
  {
    "path": "src/trx/game/output/quad.c",
    "content": "#include <trx/game/output/quad.h>\n\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/output/shaders/generic.h>\n#include <trx/gl/utils.h>\n\n#include <GL/glew.h>\n#include <stddef.h>\n#include <string.h>\n\ntypedef enum {\n    M_UNIFORM_TEXTURE_MAIN,\n    M_UNIFORM_TEXTURE_SIZE,\n    M_UNIFORM_EFFECT,\n    M_UNIFORM_OPACITY,\n    M_UNIFORM_BRIGHTNESS_SCALE,\n    M_UNIFORM_FIT_MODE,\n    M_UNIFORM_SRC_ASPECT,\n    M_UNIFORM_NUMBER_OF,\n} M_UNIFORM;\n\ntypedef struct {\n    struct {\n        GLfloat x;\n        GLfloat y;\n    } pos;\n    struct {\n        GLfloat u;\n        GLfloat v;\n    } uv;\n} M_VERTEX;\n\nstruct OUTPUT_QUAD {\n    GLuint vao;\n    GLuint vbo;\n    GLuint texture;\n    OUTPUT_SHADER *shader;\n\n    M_VERTEX *vertices;\n    int32_t vertex_count;\n\n    bool ready;\n    OUTPUT_QUAD_SURFACE_DESC desc;\n    struct {\n        int32_t x;\n        int32_t y;\n    } repeat;\n\n    OUTPUT_QUAD_EFFECT effect;\n\n    float opacity;\n    float brightness_scale;\n    TEXTURE_FILTER filter_mode;\n\n    OUTPUT_QUAD_FIT_MODE fit_mode;\n    float src_aspect;\n\n    bool use_external_texture;\n    GLuint external_texture_id;\n\n    GLint loc[M_UNIFORM_NUMBER_OF];\n};\n\nstatic const M_VERTEX m_Vertices[] = {\n    { .pos = { .x = 0.0, .y = 0.0 }, .uv = { .u = 0.0, .v = 0.0 } },\n    { .pos = { .x = 1.0, .y = 0.0 }, .uv = { .u = 1.0, .v = 0.0 } },\n    { .pos = { .x = 0.0, .y = 1.0 }, .uv = { .u = 0.0, .v = 1.0 } },\n    { .pos = { .x = 0.0, .y = 1.0 }, .uv = { .u = 0.0, .v = 1.0 } },\n    { .pos = { .x = 1.0, .y = 0.0 }, .uv = { .u = 1.0, .v = 0.0 } },\n    { .pos = { .x = 1.0, .y = 1.0 }, .uv = { .u = 1.0, .v = 1.0 } },\n};\n\nstatic const OUTPUT_QUAD_SURFACE_UV m_DefaultUV[] = {\n    { .u = 0.0f, .v = 0.0f },\n    { .u = 1.0f, .v = 0.0f },\n    { .u = 1.0f, .v = 1.0f },\n    { .u = 0.0f, .v = 1.0f },\n};\n\nstatic bool M_AllUVsZero(const OUTPUT_QUAD_SURFACE_DESC *const desc)\n{\n    for (int32_t i = 0; i < 4; i++) {\n        if (desc->uv[i].u != 0.0f || desc->uv[i].v != 0.0f) {\n            return false;\n        }\n    }\n    return true;\n}\n\nstatic OUTPUT_QUAD_SURFACE_DESC M_NormalizeDesc(\n    const OUTPUT_QUAD_SURFACE_DESC *const desc)\n{\n    OUTPUT_QUAD_SURFACE_DESC out = *desc;\n    if (M_AllUVsZero(desc)) {\n        memcpy(out.uv, m_DefaultUV, sizeof(m_DefaultUV));\n    }\n    return out;\n}\n\nstatic void M_BindProgram(const OUTPUT_QUAD *const r)\n{\n    Output_Shader_Bind(r->shader);\n}\n\nstatic void M_UploadVertices(OUTPUT_QUAD *const r)\n{\n    if (!r->ready) {\n        return;\n    }\n\n    const int32_t mapping[] = { 0, 1, 3, 3, 1, 2 };\n    r->vertex_count = r->repeat.x * r->repeat.y * 6;\n    r->vertices = Memory_Realloc(\n        r->vertices, r->repeat.x * r->repeat.y * 6 * sizeof(M_VERTEX));\n\n    M_VERTEX *ptr = r->vertices;\n    for (int32_t y = 0; y < r->repeat.y; y++) {\n        for (int32_t x = 0; x < r->repeat.x; x++) {\n            for (int32_t i = 0; i < 6; i++) {\n                const float x_factor = (float)x / (float)r->repeat.x;\n                const float y_factor = (float)y / (float)r->repeat.y;\n                const float x_offset = 1.0f / (float)r->repeat.x;\n                const float y_offset = 1.0f / (float)r->repeat.y;\n\n                ptr->pos.x = m_Vertices[i].pos.x * x_offset + x_factor;\n                ptr->pos.y = m_Vertices[i].pos.y * y_offset + y_factor;\n                ptr->uv.u = r->desc.uv[mapping[i]].u;\n                ptr->uv.v = r->desc.uv[mapping[i]].v;\n\n                ptr++;\n            }\n        }\n    }\n\n    glBindBuffer(GL_ARRAY_BUFFER, r->vbo);\n    glBufferData(\n        GL_ARRAY_BUFFER, sizeof(M_VERTEX) * 6 * r->repeat.x * r->repeat.y,\n        r->vertices, GL_STATIC_DRAW);\n    TRX_GL_CheckError();\n}\n\nOUTPUT_QUAD *Output_Quad_Create(void)\n{\n    OUTPUT_QUAD *const r = Memory_Alloc(sizeof(OUTPUT_QUAD));\n\n    r->effect = OUTPUT_QUAD_EFFECT_NONE;\n    r->opacity = 1.0f;\n    r->brightness_scale = 1.0f;\n    r->filter_mode = TEXTURE_FILTER_POINT;\n    r->repeat.x = 1;\n    r->repeat.y = 1;\n\n    r->fit_mode = OUTPUT_QUAD_FIT_STRETCH;\n    r->src_aspect = 1.0f;\n\n    r->vertices = nullptr;\n    r->vertex_count = 6;\n    r->use_external_texture = false;\n    r->external_texture_id = 0;\n\n    glGenBuffers(1, &r->vbo);\n    glBindBuffer(GL_ARRAY_BUFFER, r->vbo);\n    glBufferData(\n        GL_ARRAY_BUFFER, sizeof(m_Vertices), m_Vertices, GL_STATIC_DRAW);\n\n    glGenVertexArrays(1, &r->vao);\n    glBindVertexArray(r->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, r->vbo);\n    glEnableVertexAttribArray(0);\n    glVertexAttribPointer(\n        0, 2, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)offsetof(M_VERTEX, pos));\n    glEnableVertexAttribArray(1);\n    glVertexAttribPointer(\n        1, 2, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)offsetof(M_VERTEX, uv));\n    TRX_GL_CheckError();\n\n    glGenTextures(1, &r->texture);\n    TRX_GL_CheckError();\n\n    r->shader = Output_Shader_Create(\"2d.glsl\");\n\n    r->loc[M_UNIFORM_TEXTURE_MAIN] =\n        Output_Shader_LookupUniform(r->shader, \"uTexMain\");\n    r->loc[M_UNIFORM_TEXTURE_SIZE] =\n        Output_Shader_LookupUniform(r->shader, \"uTexSize\");\n    r->loc[M_UNIFORM_EFFECT] =\n        Output_Shader_LookupUniform(r->shader, \"uEffect\");\n    r->loc[M_UNIFORM_OPACITY] =\n        Output_Shader_LookupUniform(r->shader, \"uOpacity\");\n    r->loc[M_UNIFORM_BRIGHTNESS_SCALE] =\n        Output_Shader_LookupUniform(r->shader, \"uBrightnessScale\");\n    r->loc[M_UNIFORM_FIT_MODE] =\n        Output_Shader_LookupUniform(r->shader, \"uFitMode\");\n    r->loc[M_UNIFORM_SRC_ASPECT] =\n        Output_Shader_LookupUniform(r->shader, \"uSrcAspect\");\n\n    M_BindProgram(r);\n    glUniform1i(r->loc[M_UNIFORM_TEXTURE_MAIN], 0);\n    glUniform4f(r->loc[M_UNIFORM_TEXTURE_SIZE], 0.0f, 0.0f, 1.0f, 1.0f);\n    glUniform1i(r->loc[M_UNIFORM_EFFECT], r->effect);\n    glUniform1f(r->loc[M_UNIFORM_OPACITY], r->opacity);\n    glUniform1f(r->loc[M_UNIFORM_BRIGHTNESS_SCALE], r->brightness_scale);\n    glUniform1i(r->loc[M_UNIFORM_FIT_MODE], (int32_t)r->fit_mode);\n    glUniform1f(r->loc[M_UNIFORM_SRC_ASPECT], r->src_aspect);\n    TRX_GL_CheckError();\n\n    return r;\n}\n\nvoid Output_Quad_Destroy(OUTPUT_QUAD *const r)\n{\n    ASSERT(r != nullptr);\n\n    if (r->vao != 0) {\n        glDeleteVertexArrays(1, &r->vao);\n    }\n    if (r->vbo != 0) {\n        glDeleteBuffers(1, &r->vbo);\n    }\n    if (r->texture != 0) {\n        glDeleteTextures(1, &r->texture);\n    }\n    TRX_GL_CheckError();\n\n    if (r->shader != nullptr) {\n        Output_Shader_Free(r->shader);\n    }\n    Memory_FreePointer(&r->vertices);\n    Memory_Free(r);\n}\n\nvoid Output_Quad_Upload(\n    OUTPUT_QUAD *const r, const OUTPUT_QUAD_SURFACE_DESC *const desc,\n    const uint8_t *const data)\n{\n    ASSERT(r != nullptr);\n\n    const OUTPUT_QUAD_SURFACE_DESC normalized_desc = M_NormalizeDesc(desc);\n\n    bool reupload_vert = false;\n    if (memcmp(r->desc.uv, normalized_desc.uv, sizeof(normalized_desc.uv))\n        != 0) {\n        reupload_vert = true;\n    }\n    if (!r->ready) {\n        reupload_vert = true;\n    }\n\n    glActiveTexture(GL_TEXTURE0);\n    glBindTexture(GL_TEXTURE_2D, r->texture);\n\n    if (r->desc.width != normalized_desc.width\n        || r->desc.height != normalized_desc.height\n        || r->desc.tex_format != normalized_desc.tex_format\n        || r->desc.tex_type != normalized_desc.tex_type) {\n        glPixelStorei(GL_PACK_ALIGNMENT, 1);\n        TRX_GL_CheckError();\n        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n        TRX_GL_CheckError();\n        glTexImage2D(\n            GL_TEXTURE_2D, 0, GL_RGBA, normalized_desc.width,\n            normalized_desc.height, 0, normalized_desc.tex_format,\n            normalized_desc.tex_type, data);\n        TRX_GL_CheckError();\n    } else {\n        glPixelStorei(GL_PACK_ALIGNMENT, 1);\n        TRX_GL_CheckError();\n        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n        TRX_GL_CheckError();\n        glTexSubImage2D(\n            GL_TEXTURE_2D, 0, 0, 0, normalized_desc.width,\n            normalized_desc.height, normalized_desc.tex_format,\n            normalized_desc.tex_type, data);\n        TRX_GL_CheckError();\n    }\n\n    r->ready = true;\n    r->desc = normalized_desc;\n    r->use_external_texture = false;\n    r->external_texture_id = 0;\n    if (reupload_vert) {\n        M_UploadVertices(r);\n    }\n}\n\nvoid Output_Quad_SetExternalTexture(\n    OUTPUT_QUAD *const r, const GLuint texture_id, const int32_t width,\n    const int32_t height, const bool flip_y)\n{\n    ASSERT(r != nullptr);\n    r->use_external_texture = true;\n    r->external_texture_id = texture_id;\n\n    const float v0 = flip_y ? 1.0f : 0.0f;\n    const float v1 = flip_y ? 0.0f : 1.0f;\n    const OUTPUT_QUAD_SURFACE_DESC desc = {\n        .width = width,\n        .height = height,\n        .bit_count = 32,\n        .tex_format = GL_RGBA,\n        .tex_type = GL_UNSIGNED_INT_8_8_8_8_REV,\n        .uv = {\n            { .u = 0.0f, .v = v0 },\n            { .u = 1.0f, .v = v0 },\n            { .u = 1.0f, .v = v1 },\n            { .u = 0.0f, .v = v1 },\n        },\n        .pitch = width * 4,\n    };\n\n    const bool reupload_vert =\n        memcmp(r->desc.uv, desc.uv, sizeof(desc.uv)) != 0 || !r->ready;\n    r->ready = true;\n    r->desc = desc;\n    if (reupload_vert) {\n        M_UploadVertices(r);\n    }\n}\n\nvoid Output_Quad_ClearExternalTexture(OUTPUT_QUAD *const r)\n{\n    ASSERT(r != nullptr);\n    r->use_external_texture = false;\n    r->external_texture_id = 0;\n}\n\nvoid Output_Quad_SetTextureSize(\n    OUTPUT_QUAD *const r, const OUTPUT_QUAD_TEXTURE_SIZE *const size)\n{\n    ASSERT(r != nullptr);\n    M_BindProgram(r);\n    if (size == nullptr) {\n        glUniform4f(r->loc[M_UNIFORM_TEXTURE_SIZE], 0.0f, 0.0f, 1.0f, 1.0f);\n    } else {\n        glUniform4f(\n            r->loc[M_UNIFORM_TEXTURE_SIZE], size->x0, size->y0, size->x1,\n            size->y1);\n    }\n    TRX_GL_CheckError();\n}\n\nvoid Output_Quad_SetRepeat(\n    OUTPUT_QUAD *const r, const int32_t x, const int32_t y)\n{\n    ASSERT(r != nullptr);\n    if (r->repeat.x == x && r->repeat.y == y) {\n        return;\n    }\n    r->repeat.x = x;\n    r->repeat.y = y;\n    M_UploadVertices(r);\n}\n\nvoid Output_Quad_SetEffect(OUTPUT_QUAD *const r, const uint32_t effect)\n{\n    ASSERT(r != nullptr);\n\n    if (r->effect != effect) {\n        M_BindProgram(r);\n        glUniform1i(r->loc[M_UNIFORM_EFFECT], effect);\n        TRX_GL_CheckError();\n        r->effect = effect;\n    }\n}\n\nvoid Output_Quad_SetOpacity(OUTPUT_QUAD *const r, const float opacity)\n{\n    ASSERT(r != nullptr);\n\n    if (r->opacity != opacity) {\n        M_BindProgram(r);\n        glUniform1f(r->loc[M_UNIFORM_OPACITY], opacity);\n        TRX_GL_CheckError();\n        r->opacity = opacity;\n    }\n}\n\nvoid Output_Quad_SetBrightnessScale(\n    OUTPUT_QUAD *const r, const float brightness_scale)\n{\n    ASSERT(r != nullptr);\n\n    if (r->brightness_scale != brightness_scale) {\n        M_BindProgram(r);\n        glUniform1f(r->loc[M_UNIFORM_BRIGHTNESS_SCALE], brightness_scale);\n        TRX_GL_CheckError();\n        r->brightness_scale = brightness_scale;\n    }\n}\n\nvoid Output_Quad_SetFilter(\n    OUTPUT_QUAD *const r, const TEXTURE_FILTER filter_mode)\n{\n    ASSERT(r != nullptr);\n    r->filter_mode = filter_mode;\n}\n\nvoid Output_Quad_SetFit(\n    OUTPUT_QUAD *const r, const OUTPUT_QUAD_FIT_MODE fit_mode,\n    const float src_w, const float src_h)\n{\n    ASSERT(r != nullptr);\n\n    if (src_w <= 0.0f || src_h <= 0.0f) {\n        Output_Quad_ClearFit(r);\n        return;\n    }\n    const float src_aspect = src_w / src_h;\n    if (r->fit_mode == fit_mode && r->src_aspect == src_aspect) {\n        return;\n    }\n\n    r->fit_mode = fit_mode;\n    r->src_aspect = src_aspect;\n\n    M_BindProgram(r);\n    glUniform1i(r->loc[M_UNIFORM_FIT_MODE], (int32_t)fit_mode);\n    glUniform1f(r->loc[M_UNIFORM_SRC_ASPECT], src_aspect);\n    TRX_GL_CheckError();\n}\n\nvoid Output_Quad_ClearFit(OUTPUT_QUAD *const r)\n{\n    ASSERT(r != nullptr);\n    if (r->fit_mode == OUTPUT_QUAD_FIT_STRETCH && r->src_aspect == 1.0f) {\n        return;\n    }\n\n    r->fit_mode = OUTPUT_QUAD_FIT_STRETCH;\n    r->src_aspect = 1.0f;\n    M_BindProgram(r);\n    glUniform1i(r->loc[M_UNIFORM_FIT_MODE], (int32_t)r->fit_mode);\n    glUniform1f(r->loc[M_UNIFORM_SRC_ASPECT], r->src_aspect);\n    TRX_GL_CheckError();\n}\n\nvoid Output_Quad_Render(OUTPUT_QUAD *const r)\n{\n    ASSERT(r != nullptr);\n\n    M_BindProgram(r);\n    glUniform1i(r->loc[M_UNIFORM_EFFECT], r->effect);\n    glBindVertexArray(r->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, r->vbo);\n\n    glActiveTexture(GL_TEXTURE0);\n    if (r->use_external_texture) {\n        glBindTexture(GL_TEXTURE_2D, r->external_texture_id);\n    } else {\n        glBindTexture(GL_TEXTURE_2D, r->texture);\n    }\n    const GLint gl_filter =\n        r->filter_mode == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST;\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter);\n    GLint prev_sampler = 0;\n    glGetIntegeri_v(GL_SAMPLER_BINDING, 0, &prev_sampler);\n    glBindSampler(0, 0);\n\n    const GLboolean was_blend_enabled = glIsEnabled(GL_BLEND);\n    if (was_blend_enabled) {\n        glDisable(GL_BLEND);\n    }\n\n    GLint bound_polygon_mode[2];\n    glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]);\n    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);\n\n    const GLboolean was_depth_test_enabled = glIsEnabled(GL_DEPTH_TEST);\n    if (was_depth_test_enabled) {\n        glDisable(GL_DEPTH_TEST);\n    }\n\n    glDrawArrays(GL_TRIANGLES, 0, r->vertex_count);\n\n    glBindSampler(0, (GLuint)prev_sampler);\n    glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]);\n    if (was_depth_test_enabled) {\n        glEnable(GL_DEPTH_TEST);\n    }\n    if (was_blend_enabled) {\n        glEnable(GL_BLEND);\n    }\n    TRX_GL_CheckError();\n}\n\nvoid Output_Quad_RenderWithBlend(OUTPUT_QUAD *const r)\n{\n    ASSERT(r != nullptr);\n\n    M_BindProgram(r);\n    glUniform1i(r->loc[M_UNIFORM_EFFECT], r->effect);\n    glBindVertexArray(r->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, r->vbo);\n\n    glActiveTexture(GL_TEXTURE0);\n    if (r->use_external_texture) {\n        glBindTexture(GL_TEXTURE_2D, r->external_texture_id);\n    } else {\n        glBindTexture(GL_TEXTURE_2D, r->texture);\n    }\n    const GLint gl_filter =\n        r->filter_mode == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST;\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter);\n    GLint prev_sampler = 0;\n    glGetIntegeri_v(GL_SAMPLER_BINDING, 0, &prev_sampler);\n    glBindSampler(0, 0);\n\n    const GLboolean was_blend_enabled = glIsEnabled(GL_BLEND);\n    GLint prev_blend_src = 0;\n    GLint prev_blend_dst = 0;\n    glGetIntegerv(GL_BLEND_SRC_RGB, &prev_blend_src);\n    glGetIntegerv(GL_BLEND_DST_RGB, &prev_blend_dst);\n    glEnable(GL_BLEND);\n    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n\n    GLint bound_polygon_mode[2];\n    glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]);\n    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);\n\n    const GLboolean was_depth_test_enabled = glIsEnabled(GL_DEPTH_TEST);\n    if (was_depth_test_enabled) {\n        glDisable(GL_DEPTH_TEST);\n    }\n\n    glDrawArrays(GL_TRIANGLES, 0, r->vertex_count);\n\n    glBindSampler(0, (GLuint)prev_sampler);\n    glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]);\n    if (was_depth_test_enabled) {\n        glEnable(GL_DEPTH_TEST);\n    }\n    glBlendFunc(prev_blend_src, prev_blend_dst);\n    if (!was_blend_enabled) {\n        glDisable(GL_BLEND);\n    }\n    TRX_GL_CheckError();\n}\n"
  },
  {
    "path": "src/trx/game/output/quad.h",
    "content": "#pragma once\n\n#include <trx/gl/enum.h>\n\n#include <GL/glew.h>\n#include <stdint.h>\n\n// Textured screen-space quad renderer used by output code paths such as FMV\n// presentation and overlay composition. It owns GL state/resources needed to\n// upload 2D image data or bind external textures, then draw them with optional\n// fit/repeat/effect controls.\n\ntypedef struct {\n    float u;\n    float v;\n} OUTPUT_QUAD_SURFACE_UV;\n\ntypedef struct {\n    int32_t width;\n    int32_t height;\n    int32_t bit_count;\n    GLenum tex_format;\n    GLenum tex_type;\n    OUTPUT_QUAD_SURFACE_UV uv[4];\n    int32_t pitch;\n} OUTPUT_QUAD_SURFACE_DESC;\n\ntypedef struct {\n    float x0;\n    float y0;\n    float x1;\n    float y1;\n} OUTPUT_QUAD_TEXTURE_SIZE;\n\ntypedef enum {\n    OUTPUT_QUAD_EFFECT_NONE = 0,\n    OUTPUT_QUAD_EFFECT_VIGNETTE = 1 << 0,\n    OUTPUT_QUAD_EFFECT_WAVE = 1 << 1,\n} OUTPUT_QUAD_EFFECT;\n\ntypedef struct OUTPUT_QUAD OUTPUT_QUAD;\n\ntypedef enum {\n    OUTPUT_QUAD_FIT_STRETCH,\n    OUTPUT_QUAD_FIT_LETTERBOX,\n    OUTPUT_QUAD_FIT_CROP,\n    OUTPUT_QUAD_FIT_SMART,\n} OUTPUT_QUAD_FIT_MODE;\n\n// Create a quad renderer instance and initialize GL resources.\nOUTPUT_QUAD *Output_Quad_Create(void);\n// Destroy a quad renderer instance and release its GL resources.\nvoid Output_Quad_Destroy(OUTPUT_QUAD *renderer);\n\n// Upload pixel data into the renderer-owned texture.\nvoid Output_Quad_Upload(\n    OUTPUT_QUAD *renderer, const OUTPUT_QUAD_SURFACE_DESC *desc,\n    const uint8_t *data);\n\n// Bind an external texture as the source image for rendering.\nvoid Output_Quad_SetExternalTexture(\n    OUTPUT_QUAD *renderer, GLuint texture_id, int32_t width, int32_t height,\n    bool flip_y);\n// Switch back to the renderer-owned texture source.\nvoid Output_Quad_ClearExternalTexture(OUTPUT_QUAD *renderer);\n\n// Set how many times the quad should repeat in X and Y.\nvoid Output_Quad_SetRepeat(OUTPUT_QUAD *renderer, int32_t x, int32_t y);\n// Set the source UV rectangle used for texture sampling.\nvoid Output_Quad_SetTextureSize(\n    OUTPUT_QUAD *renderer, const OUTPUT_QUAD_TEXTURE_SIZE *size);\n// Set visual effect flags applied by the quad shader.\nvoid Output_Quad_SetEffect(OUTPUT_QUAD *renderer, uint32_t effect);\n\n// Set output opacity multiplier.\nvoid Output_Quad_SetOpacity(OUTPUT_QUAD *renderer, float opacity);\n// Set brightness scaling multiplier applied in shader.\nvoid Output_Quad_SetBrightnessScale(\n    OUTPUT_QUAD *renderer, float brightness_scale);\nvoid Output_Quad_SetFilter(OUTPUT_QUAD *renderer, TEXTURE_FILTER filter_mode);\n\n// Configure fitting mode and source aspect ratio handling.\nvoid Output_Quad_SetFit(\n    OUTPUT_QUAD *renderer, OUTPUT_QUAD_FIT_MODE fit_mode, float src_w,\n    float src_h);\n// Reset fit behavior to stretch with default aspect.\nvoid Output_Quad_ClearFit(OUTPUT_QUAD *renderer);\n\n// Render the quad without forcing blend state changes.\nvoid Output_Quad_Render(OUTPUT_QUAD *renderer);\n// Render the quad with alpha blending enabled.\nvoid Output_Quad_RenderWithBlend(OUTPUT_QUAD *renderer);\n"
  },
  {
    "path": "src/trx/game/output/scene_compositor.c",
    "content": "#include <trx/game/output/scene_compositor.h>\n\n#include <trx/config.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/output.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/shaders/ui.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/uniforms.h>\n#include <trx/game/shell.h>\n#include <trx/gl/context.h>\n#include <trx/gl/utils.h>\n\n#define M_PROCESS_SOURCES(p, func, ...)                                        \\\n    do {                                                                       \\\n        for (int32_t i = 0; i < p->sources->count; i++) {                      \\\n            const SCENE_SOURCE *const source =                                 \\\n                *(SCENE_SOURCE **)Vector_Get(p->sources, i);                   \\\n            if (source->func != nullptr) {                                     \\\n                source->func(source, ##__VA_ARGS__);                           \\\n            }                                                                  \\\n        }                                                                      \\\n    } while (0)\n\ntypedef struct {\n    VECTOR *sources;\n    GLuint sampler_id;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_SetSamplerFilter(\n    const GLuint sampler, const TEXTURE_FILTER filter)\n{\n    const GLenum gl_filter =\n        filter == TEXTURE_FILTER_BILINEAR ? GL_LINEAR : GL_NEAREST;\n    glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, gl_filter);\n    glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, gl_filter);\n}\n\nstatic void M_BindTextures(const M_PRIV *const p)\n{\n    glActiveTexture(GL_TEXTURE0);\n    glBindTexture(GL_TEXTURE_2D_ARRAY, Output_Textures_GetAtlasTexture());\n    glActiveTexture(GL_TEXTURE1);\n    glBindTexture(GL_TEXTURE_2D, Output_Textures_GetEnvMapTexture());\n}\n\nstatic void M_SetupScene(const M_PRIV *const p)\n{\n    Output_MeshShader_Bind(Output_GetMeshShader());\n    Output_Uniforms_UploadViewMatrix(Output_GetUniforms(), &g_ViewMatrix);\n    glEnable(GL_BLEND);\n    glBlendFunc(\n        GL_ONE,\n        g_Config.rendering.enable_wireframe ? GL_ZERO : GL_ONE_MINUS_SRC_ALPHA);\n    M_SetSamplerFilter(p->sampler_id, g_Config.rendering.texture_filter);\n}\n\nstatic void M_SetupUI(const M_PRIV *const p)\n{\n    Output_UIShader_Bind(Output_GetUIShader());\n    Output_Uniforms_UploadOrthoMatrix(Output_GetUniforms());\n    glEnable(GL_BLEND);\n    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n    M_SetSamplerFilter(p->sampler_id, g_Config.rendering.ui_filter);\n    glClear(GL_DEPTH_BUFFER_BIT);\n}\n\nstatic void M_RenderSourcePass(const M_PRIV *const p, const SCENE_PASS pass)\n{\n    for (int32_t i = 0; i < p->sources->count; i++) {\n        const SCENE_SOURCE *const source =\n            *(SCENE_SOURCE **)Vector_Get(p->sources, i);\n        if (source->is_dirty != nullptr && source->is_dirty(source, pass)) {\n            ASSERT(source->render_pass != nullptr);\n            source->render_pass(source, pass);\n        }\n    }\n}\n\nstatic bool M_IsSourceDirty(const M_PRIV *const p, const SCENE_PASS pass)\n{\n    for (int32_t i = 0; i < p->sources->count; i++) {\n        const SCENE_SOURCE *const source =\n            *(SCENE_SOURCE **)Vector_Get(p->sources, i);\n        if (source->is_dirty != nullptr && source->is_dirty(source, pass)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_IsAnySourceDirty(const M_PRIV *const p)\n{\n    for (SCENE_PASS pass = 0; pass < SCENE_PASS_COUNT; pass++) {\n        if (M_IsSourceDirty(p, pass)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic void M_PrepareScene(const M_PRIV *const p)\n{\n#ifndef __APPLE__\n    glLineWidth(g_Config.rendering.wireframe_width);\n    TRX_GL_CheckError();\n#endif\n\n    glBindSampler(0, p->sampler_id);\n    glSamplerParameterf(\n        p->sampler_id, GL_TEXTURE_MAX_ANISOTROPY_EXT,\n        g_Config.rendering.anisotropy_filter);\n\n    Output_Uniforms_UploadGeneral(Output_GetUniforms());\n    Output_Uniforms_UploadRoomLights(Output_GetUniforms(), nullptr);\n    Output_SetCurrentRoom(nullptr);\n}\n\nstatic void M_RenderScenePasses(const M_PRIV *const p)\n{\n    if (!M_IsAnySourceDirty(p)) {\n        return;\n    }\n\n    M_BindTextures(p);\n    M_SetupScene(p);\n\n    glDisable(GL_DEPTH_TEST);\n    if (M_IsSourceDirty(p, SCENE_PASS_BACKGROUND)) {\n        M_RenderSourcePass(p, SCENE_PASS_BACKGROUND);\n    }\n\n    OUTPUT_MESH_SHADER *const shader = Output_GetMeshShader();\n    Output_MeshShader_Bind(shader);\n\n    glPolygonMode(\n        GL_FRONT_AND_BACK,\n        g_Config.rendering.enable_wireframe ? GL_LINE : GL_FILL);\n\n    glEnable(GL_DEPTH_TEST);\n    glEnable(GL_POLYGON_OFFSET_FILL);\n\n    if (M_IsSourceDirty(p, SCENE_PASS_OPAQUE)\n        || M_IsSourceDirty(p, SCENE_PASS_TRANSPARENT)\n        || M_IsSourceDirty(p, SCENE_PASS_BLEND_SUB)\n        || M_IsSourceDirty(p, SCENE_PASS_BLEND_ADD)) {\n        glEnable(GL_CULL_FACE);\n        Output_MeshShader_UploadAlphaDiscard(shader, true);\n        M_RenderSourcePass(p, SCENE_PASS_OPAQUE);\n        Output_MeshShader_UploadAlphaDiscard(shader, false);\n        glDepthMask(GL_FALSE);\n        glEnable(GL_BLEND);\n        M_RenderSourcePass(p, SCENE_PASS_TRANSPARENT);\n        glBlendEquation(GL_FUNC_ADD);\n        glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);\n        M_RenderSourcePass(p, SCENE_PASS_BLEND_SUB);\n        glBlendEquation(GL_FUNC_ADD);\n        glBlendFunc(GL_SRC_ALPHA, GL_ONE);\n        M_RenderSourcePass(p, SCENE_PASS_BLEND_ADD);\n        glDepthMask(GL_TRUE);\n        glDisable(GL_CULL_FACE);\n    }\n\n    if (M_IsSourceDirty(p, SCENE_PASS_OVERLAY_PRE_UI)) {\n        M_RenderSourcePass(p, SCENE_PASS_OVERLAY_PRE_UI);\n    }\n\n    if (M_IsSourceDirty(p, SCENE_PASS_UI)) {\n        M_SetupUI(p);\n        M_RenderSourcePass(p, SCENE_PASS_UI);\n    }\n\n    if (M_IsSourceDirty(p, SCENE_PASS_OVERLAY_POST_UI)) {\n        M_RenderSourcePass(p, SCENE_PASS_OVERLAY_POST_UI);\n    }\n}\n\nvoid SceneCompositor_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->sources = Vector_Create(sizeof(SCENE_SOURCE *));\n    glGenSamplers(1, &p->sampler_id);\n    glSamplerParameteri(p->sampler_id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glSamplerParameteri(p->sampler_id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glSamplerParameteri(p->sampler_id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n    glSamplerParameteri(p->sampler_id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n    TRX_GL_CheckError();\n}\n\nvoid SceneCompositor_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->sources != nullptr) {\n        Vector_Free(p->sources);\n        p->sources = nullptr;\n    }\n    if (p->sampler_id != 0) {\n        glDeleteSamplers(1, &p->sampler_id);\n        p->sampler_id = 0;\n    }\n}\n\nbool M_IsActive(void)\n{\n    return !Output_IsHeadless() || Shell_GetArgs()->debug_render_performance\n        || TRX_GL_Context_GetScheduledScreenshotPath() != nullptr;\n}\n\nvoid SceneCompositor_BeginScene(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!M_IsActive()) {\n        return;\n    }\n    M_PrepareScene(p);\n    M_PROCESS_SOURCES(p, render_begin);\n}\n\nvoid SceneCompositor_Flush(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!M_IsActive()) {\n        return;\n    }\n    M_RenderScenePasses(p);\n    M_PROCESS_SOURCES(p, render_end);\n    M_PROCESS_SOURCES(p, render_begin);\n    glClear(GL_DEPTH_BUFFER_BIT);\n}\n\nvoid SceneCompositor_EndScene(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!M_IsActive()) {\n        return;\n    }\n    M_RenderScenePasses(p);\n    M_PROCESS_SOURCES(p, render_end);\n}\n\nvoid SceneCompositor_AnimateTextures(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_PROCESS_SOURCES(p, animate_textures);\n}\n\nvoid SceneCompositor_AddSource(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Add(p->sources, &source);\n}\n\nvoid SceneCompositor_SetSamplerFilter(const TEXTURE_FILTER filter)\n{\n    M_PRIV *const p = &m_Priv;\n    M_SetSamplerFilter(p->sampler_id, filter);\n}\n"
  },
  {
    "path": "src/trx/game/output/scene_compositor.h",
    "content": "#pragma once\n\n#include <trx/game/output/scene_source.h>\n#include <trx/gl/enum.h>\n\nvoid SceneCompositor_Init(void);\nvoid SceneCompositor_Shutdown(void);\nvoid SceneCompositor_AddSource(const SCENE_SOURCE *source);\nvoid SceneCompositor_BeginScene(void);\nvoid SceneCompositor_EndScene(void);\nvoid SceneCompositor_Flush(void);\nvoid SceneCompositor_AnimateTextures(void);\nvoid SceneCompositor_SetSamplerFilter(TEXTURE_FILTER filter);\n"
  },
  {
    "path": "src/trx/game/output/scene_source.h",
    "content": "#pragma once\n\ntypedef enum {\n    SCENE_PASS_BACKGROUND,\n    SCENE_PASS_OPAQUE,\n    SCENE_PASS_TRANSPARENT,\n    SCENE_PASS_BLEND_SUB,\n    SCENE_PASS_BLEND_ADD,\n    SCENE_PASS_OVERLAY_PRE_UI,\n    SCENE_PASS_UI,\n    SCENE_PASS_OVERLAY_POST_UI,\n    SCENE_PASS_COUNT,\n} SCENE_PASS;\n\ntypedef struct SCENE_SOURCE {\n    void (*render_begin)(const struct SCENE_SOURCE *);\n    void (*render_pass)(const struct SCENE_SOURCE *, SCENE_PASS pass);\n    void (*render_end)(const struct SCENE_SOURCE *);\n    bool (*is_dirty)(const struct SCENE_SOURCE *, SCENE_PASS pass);\n    void (*animate_textures)(const struct SCENE_SOURCE *);\n    void *priv;\n} SCENE_SOURCE;\n"
  },
  {
    "path": "src/trx/game/output/shaders/generic.c",
    "content": "#include <trx/game/output/shaders/generic.h>\n\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/output.h>\n#include <trx/game/viewport.h>\n#include <trx/gl/program.h>\n#include <trx/gl/utils.h>\n\n#include <uthash.h>\n\ntypedef struct {\n    GLint location;\n    GLenum type;\n    GLsizei size;\n    char name[64];\n    UT_hash_handle hh;\n} M_UNIFORM;\n\nstruct OUTPUT_SHADER {\n    TRX_GL_PROGRAM program;\n\n    int32_t count;\n    M_UNIFORM *uniforms;\n    M_UNIFORM *uniform_hash;\n};\n\nstatic const char *const m_UniformBlocks[] = {\n    \"Globals\", \"Matrices\", \"Lights\", \"LightSource\", nullptr,\n};\n\nstatic void M_DebugUBO(const GLuint program_id, const GLuint block_idx)\n{\n    // Prints memory layout of the specific UBO in the GPU\n\n    // Get the block name\n    GLint name_len = 0;\n    glGetActiveUniformBlockiv(\n        program_id, block_idx, GL_UNIFORM_BLOCK_NAME_LENGTH, &name_len);\n    char *const block_name = Memory_Alloc(name_len);\n    glGetActiveUniformBlockName(\n        program_id, block_idx, name_len, nullptr, block_name);\n\n    // Get all uniforms within that block\n    GLint uniform_count = 0;\n    glGetActiveUniformBlockiv(\n        program_id, block_idx, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS,\n        &uniform_count);\n    GLuint *const uniform_indices =\n        Memory_Alloc(sizeof(GLuint) * uniform_count);\n    glGetActiveUniformBlockiv(\n        program_id, block_idx, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES,\n        (GLint *)uniform_indices);\n\n    // Query offsets\n    GLint *const offsets = Memory_Alloc(sizeof(GLint) * uniform_count);\n    glGetActiveUniformsiv(\n        program_id, uniform_count, uniform_indices, GL_UNIFORM_OFFSET, offsets);\n\n    // Print block name and all members\n    LOG_DEBUG(\"Uniform Block %u: %s\", block_idx, block_name);\n    for (GLint i = 0; i < uniform_count; ++i) {\n        char name[256];\n        GLsizei length;\n        glGetActiveUniformName(\n            program_id, uniform_indices[i], sizeof(name), &length, name);\n        LOG_DEBUG(\"  %s → offset %d\", name, offsets[i]);\n    }\n\n    // Cleanup\n    Memory_Free(offsets);\n    Memory_Free(uniform_indices);\n    Memory_Free(block_name);\n}\n\nOUTPUT_SHADER *Output_Shader_Create(const char *const path)\n{\n    OUTPUT_SHADER *const shader = Memory_Alloc(sizeof(OUTPUT_SHADER));\n\n    TRX_GL_Program_Init(&shader->program);\n    TRX_GL_Program_AttachShader(&shader->program, GL_VERTEX_SHADER, path);\n    TRX_GL_Program_AttachShader(&shader->program, GL_FRAGMENT_SHADER, path);\n    TRX_GL_Program_FragmentData(&shader->program, \"outColor\");\n    TRX_GL_Program_Link(&shader->program);\n\n#if 0\n    M_DebugUBO(shader->program.id, 0);\n#endif\n\n    // Bind uniform blocks to UBO binding points\n    const GLuint program_id = shader->program.id;\n    for (int32_t i = 0; m_UniformBlocks[i] != nullptr; i++) {\n        GLuint block_index =\n            glGetUniformBlockIndex(program_id, m_UniformBlocks[i]);\n        if (block_index != GL_INVALID_INDEX) {\n            glUniformBlockBinding(program_id, block_index, i);\n        }\n    }\n\n    GLint count;\n    glGetProgramiv(shader->program.id, GL_ACTIVE_UNIFORMS, &count);\n    shader->count = count;\n    shader->uniforms = Memory_Alloc(sizeof(M_UNIFORM) * count);\n    shader->uniform_hash = nullptr;\n\n    for (GLint i = 0; i < count; i++) {\n        M_UNIFORM *const uniform = &shader->uniforms[i];\n\n        GLsizei len;\n        GLchar name[64];\n        glGetActiveUniform(\n            shader->program.id, i, sizeof(name), &len, &uniform->size,\n            &uniform->type, name);\n\n        uniform->location = glGetUniformLocation(shader->program.id, name);\n        strncpy(uniform->name, name, sizeof(uniform->name));\n        HASH_ADD_STR(shader->uniform_hash, name, uniform);\n    }\n\n    TRX_GL_Program_Bind(&shader->program);\n    return shader;\n}\n\nvoid Output_Shader_Free(OUTPUT_SHADER *const shader)\n{\n    TRX_GL_Program_Close(&shader->program);\n    M_UNIFORM *cur, *tmp;\n    HASH_ITER(hh, shader->uniform_hash, cur, tmp)\n    {\n        HASH_DEL(shader->uniform_hash, cur);\n    }\n    Memory_Free(shader->uniforms);\n    Memory_Free(shader);\n}\n\nvoid Output_Shader_Bind(const OUTPUT_SHADER *const shader)\n{\n    ASSERT(shader != nullptr);\n    TRX_GL_Program_Bind(&shader->program);\n    const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms();\n    glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniforms->general);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 1, uniforms->matrices);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniforms->lights);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 3, uniforms->ls);\n    TRX_GL_CheckError();\n}\n\nGLint Output_Shader_LookupUniform(\n    const OUTPUT_SHADER *const shader, const char *const name)\n{\n    M_UNIFORM *uniform = nullptr;\n    HASH_FIND_STR(shader->uniform_hash, name, uniform);\n    if (uniform == nullptr) {\n        LOG_ERROR(\"Uniform %s not found\", name);\n        return -1;\n    }\n    return uniform->location;\n}\n\nbool Output_Shader_TryLookupUniform(\n    const OUTPUT_SHADER *const shader, const char *const name,\n    GLint *const out_location)\n{\n    M_UNIFORM *uniform = nullptr;\n    HASH_FIND_STR(shader->uniform_hash, name, uniform);\n    if (uniform == nullptr) {\n        if (out_location != nullptr) {\n            *out_location = -1;\n        }\n        return false;\n    }\n    if (out_location != nullptr) {\n        *out_location = uniform->location;\n    }\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/output/shaders/generic.h",
    "content": "#pragma once\n\n#include <GL/glew.h>\n\ntypedef struct OUTPUT_SHADER OUTPUT_SHADER;\n\nOUTPUT_SHADER *Output_Shader_Create(const char *path);\nvoid Output_Shader_Free(OUTPUT_SHADER *shader);\nvoid Output_Shader_Bind(const OUTPUT_SHADER *shader);\n\nGLint Output_Shader_LookupUniform(\n    const OUTPUT_SHADER *shader, const char *name);\n\nbool Output_Shader_TryLookupUniform(\n    const OUTPUT_SHADER *shader, const char *name, GLint *out_location);\n"
  },
  {
    "path": "src/trx/game/output/shaders/mesh.c",
    "content": "#include <trx/game/output/shaders/mesh.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/utils.h>\n#include <trx/gl/utils.h>\n#include <trx/version.h>\n\n#include <string.h>\n\nstruct OUTPUT_MESH_SHADER {\n    OUTPUT_SHADER *base_tr12;\n    OUTPUT_SHADER *base_tr3;\n\n    MATRIX model_matrix[2];\n    bool has_model_matrix[2];\n    int32_t water_effect[2];\n    float water_effect_params[2][3];\n    bool is_wibble_effect[2];\n    bool is_alpha_discard_enabled[2];\n    RGB_F tint[2];\n};\n\nstatic int32_t M_GetVariantIndex(void)\n{\n    return g_TRVersion >= 3 ? 1 : 0;\n}\n\nstatic OUTPUT_SHADER *M_GetVariantBase(\n    const OUTPUT_MESH_SHADER *const shader, const int32_t variant_idx)\n{\n    return variant_idx != 0 ? shader->base_tr3 : shader->base_tr12;\n}\n\nOUTPUT_MESH_SHADER *Output_MeshShader_Create(void)\n{\n    OUTPUT_MESH_SHADER *const shader = Memory_Alloc(sizeof(*shader));\n    shader->has_model_matrix[0] = false;\n    shader->has_model_matrix[1] = false;\n    shader->water_effect[0] = -1;\n    shader->water_effect[1] = -1;\n    shader->water_effect_params[0][0] = 0.0f;\n    shader->water_effect_params[0][1] = 0.0f;\n    shader->water_effect_params[0][2] = 0.0f;\n    shader->water_effect_params[1][0] = 0.0f;\n    shader->water_effect_params[1][1] = 0.0f;\n    shader->water_effect_params[1][2] = 0.0f;\n    shader->is_wibble_effect[0] = false;\n    shader->is_wibble_effect[1] = false;\n    shader->is_alpha_discard_enabled[0] = false;\n    shader->is_alpha_discard_enabled[1] = false;\n    shader->tint[0] = (RGB_F) { 0.0f, 0.0f, 0.0f };\n    shader->tint[1] = (RGB_F) { 0.0f, 0.0f, 0.0f };\n\n    shader->base_tr12 = Output_Shader_Create(\"meshes_tr12.glsl\");\n    shader->base_tr3 = Output_Shader_Create(\"meshes_tr3.glsl\");\n\n    Output_Shader_Bind(shader->base_tr12);\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i,\n        Output_Shader_LookupUniform(shader->base_tr12, \"uTexAtlas\"), 0);\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i,\n        Output_Shader_LookupUniform(shader->base_tr12, \"uTexEnvMap\"), 1);\n\n    Output_Shader_Bind(shader->base_tr3);\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i, Output_Shader_LookupUniform(shader->base_tr3, \"uTexAtlas\"),\n        0);\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i,\n        Output_Shader_LookupUniform(shader->base_tr3, \"uTexEnvMap\"), 1);\n    return shader;\n}\n\nvoid Output_MeshShader_Bind(const OUTPUT_MESH_SHADER *const shader)\n{\n    const int32_t variant_idx = M_GetVariantIndex();\n    Output_Shader_Bind(M_GetVariantBase(shader, variant_idx));\n}\n\nvoid Output_MeshShader_Free(OUTPUT_MESH_SHADER *const shader)\n{\n    Output_Shader_Free(shader->base_tr12);\n    Output_Shader_Free(shader->base_tr3);\n    Memory_Free(shader);\n}\n\nvoid Output_MeshShader_UploadModelMatrix(\n    OUTPUT_MESH_SHADER *const shader, const MATRIX *const source)\n{\n    const int32_t variant_idx = M_GetVariantIndex();\n    OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx);\n    if (shader->has_model_matrix[variant_idx]\n        && memcmp(&shader->model_matrix[variant_idx], source, sizeof(*source))\n            == 0) {\n        return;\n    }\n    memcpy(&shader->model_matrix[variant_idx], source, sizeof(*source));\n    shader->has_model_matrix[variant_idx] = true;\n\n    GLfloat m[4][4];\n    Output_FillMatrix(m, source);\n\n    TRX_GL_TRACK_UNIFORM(\n        glUniformMatrix4fv, Output_Shader_LookupUniform(base, \"uMatModel\"), 1,\n        GL_FALSE, &m[0][0]);\n}\n\nvoid Output_MeshShader_UploadAlphaDiscard(\n    OUTPUT_MESH_SHADER *const shader, const bool is_enabled)\n{\n    const int32_t variant_idx = M_GetVariantIndex();\n    OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx);\n    if (is_enabled == shader->is_alpha_discard_enabled[variant_idx]) {\n        return;\n    }\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i, Output_Shader_LookupUniform(base, \"uDiscardAlpha\"),\n        is_enabled);\n    shader->is_alpha_discard_enabled[variant_idx] = is_enabled;\n}\n\nvoid Output_MeshShader_UploadWaterEffect(\n    OUTPUT_MESH_SHADER *const shader, const int32_t water_effect)\n{\n    const int32_t variant_idx = M_GetVariantIndex();\n    OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx);\n    if (water_effect == shader->water_effect[variant_idx]) {\n        return;\n    }\n\n    static const float m_ChoppyAmp[22] = {\n        16.0f, 0.0f,   0.0f,   0.0f,   0.0f,   16.0f, 16.0f, 16.0f,\n        16.0f, 53.0f,  53.0f,  53.0f,  53.0f,  90.0f, 90.0f, 90.0f,\n        90.0f, 127.0f, 127.0f, 127.0f, 127.0f, 0.0f,\n    };\n    static const float m_ShimmerAmp[22] = {\n        7.875f,   4.0f,     8.0f,     12.0f,    15.875f,  -3.875f,\n        -7.875f,  -11.875f, -15.875f, -3.875f,  -7.875f,  -11.875f,\n        -15.875f, -3.875f,  -7.875f,  -11.875f, -15.875f, -3.875f,\n        -7.875f,  -11.875f, -15.875f, 0.0f,\n    };\n    static const float m_AbsIntensity[22] = {\n        0.0f,  253.0f, 0.0f, 4.0f,  8.0f,  4.0f, 8.0f, 12.0f,\n        16.0f, 4.0f,   8.0f, 12.0f, 16.0f, 4.0f, 8.0f, 12.0f,\n        16.0f, 4.0f,   8.0f, 12.0f, 16.0f, 0.0f,\n    };\n\n    int32_t scheme = water_effect - 2;\n    CLAMP(scheme, 0, 21);\n    const float p0 = m_ChoppyAmp[scheme];\n    const float p1 = m_ShimmerAmp[scheme];\n    const float p2 = m_AbsIntensity[scheme];\n\n    GLint loc = -1;\n    if (Output_Shader_TryLookupUniform(base, \"uWaterEffect\", &loc)) {\n        TRX_GL_TRACK_UNIFORM(glUniform1i, loc, water_effect);\n    }\n    if (Output_Shader_TryLookupUniform(base, \"uWaterEffectParams\", &loc)) {\n        TRX_GL_TRACK_UNIFORM(glUniform3f, loc, p0, p1, p2);\n    }\n    shader->water_effect[variant_idx] = water_effect;\n    shader->water_effect_params[variant_idx][0] = p0;\n    shader->water_effect_params[variant_idx][1] = p1;\n    shader->water_effect_params[variant_idx][2] = p2;\n}\n\nvoid Output_MeshShader_UploadWibbleEffect(\n    OUTPUT_MESH_SHADER *const shader, const bool is_enabled)\n{\n    const int32_t variant_idx = M_GetVariantIndex();\n    OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx);\n    if (is_enabled == shader->is_wibble_effect[variant_idx]) {\n        return;\n    }\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i, Output_Shader_LookupUniform(base, \"uWibbleEffect\"),\n        is_enabled);\n    shader->is_wibble_effect[variant_idx] = is_enabled;\n}\n\nvoid Output_MeshShader_UploadTint(OUTPUT_MESH_SHADER *const shader, RGB_F tint)\n{\n    const int32_t variant_idx = M_GetVariantIndex();\n    OUTPUT_SHADER *const base = M_GetVariantBase(shader, variant_idx);\n    if (tint.r == shader->tint[variant_idx].r\n        && tint.g == shader->tint[variant_idx].g\n        && tint.b == shader->tint[variant_idx].b) {\n        return;\n    }\n    TRX_GL_TRACK_UNIFORM(\n        glUniform3f, Output_Shader_LookupUniform(base, \"uTint\"), tint.r, tint.g,\n        tint.b);\n    shader->tint[variant_idx] = tint;\n}\n"
  },
  {
    "path": "src/trx/game/output/shaders/mesh.h",
    "content": "#pragma once\n\n#include <trx/game/matrix.h>\n#include <trx/game/output/shaders/generic.h>\n#include <trx/game/output/types.h>\n\n// clang-format off\n#define VERT_NO_WIBBLE         0b0000'0000'0001 // = 0x0001\n#define VERT_FLAT_SHADED       0b0000'0000'0010 // = 0x0002\n#define VERT_REFLECTIVE        0b0000'0000'0100 // = 0x0004\n#define VERT_NO_LIGHTING       0b0000'0000'1000 // = 0x0008\n#define VERT_BILLBOARD         0b0000'0001'0000 // = 0x0010\n#define VERT_ABS_SPRITE        0b0000'0010'0000 // = 0x0020\n#define VERT_NO_ALPHA_DISCARD  0b0000'0100'0000 // = 0x0040\n#define VERT_USE_DYNAMIC_LIGHT 0b0000'1000'0000 // = 0x0080\n#define VERT_USE_OBJECT_LIGHT  0b0001'0000'0000 // = 0x0100\n#define VERT_USE_OWN_LIGHT     0b0010'0000'0000 // = 0x0200\n#define VERT_MOVE              0b0100'0000'0000 // = 0x0400\n#define VERT_GLOW              0b1000'0000'0000 // = 0x0800\n// clang-format on\n\n// GL attribute mapping in the shader\ntypedef enum {\n    // clang-format off\n    OUTPUT_MESH_ATTR_POS             = 0,\n    OUTPUT_MESH_ATTR_NORMAL          = 1,\n    OUTPUT_MESH_ATTR_UVW             = 2,\n    OUTPUT_MESH_ATTR_TEXTURE_SIZE    = 3,\n    OUTPUT_MESH_ATTR_TRAPEZOID_RATIO = 4,\n    OUTPUT_MESH_ATTR_FLAGS           = 5,\n    OUTPUT_MESH_ATTR_COLOR           = 6,\n    OUTPUT_MESH_ATTR_SHADE           = 7,\n    // clang-format on\n} OUTPUT_MESH_ATTRIBUTE;\n\ntypedef struct OUTPUT_MESH_SHADER OUTPUT_MESH_SHADER;\n\nOUTPUT_MESH_SHADER *Output_MeshShader_Create(void);\nvoid Output_MeshShader_Free(OUTPUT_MESH_SHADER *shader);\nvoid Output_MeshShader_Bind(const OUTPUT_MESH_SHADER *shader);\n\n// TODO: these could could use UBOs\nvoid Output_MeshShader_UploadModelMatrix(\n    OUTPUT_MESH_SHADER *shader, const MATRIX *source);\nvoid Output_MeshShader_UploadWaterEffect(\n    OUTPUT_MESH_SHADER *shader, int32_t water_effect);\nvoid Output_MeshShader_UploadWibbleEffect(\n    OUTPUT_MESH_SHADER *shader, bool is_enabled);\nvoid Output_MeshShader_UploadTint(OUTPUT_MESH_SHADER *shader, RGB_F tint);\nvoid Output_MeshShader_UploadAlphaDiscard(\n    OUTPUT_MESH_SHADER *shader, bool is_enabled);\n"
  },
  {
    "path": "src/trx/game/output/shaders/ui.c",
    "content": "#include <trx/game/output/shaders/ui.h>\n\n#include <trx/gl/utils.h>\n\nOUTPUT_UI_SHADER *Output_UIShader_Create(void)\n{\n    OUTPUT_SHADER *const shader = Output_Shader_Create(\"ui.glsl\");\n    TRX_GL_TRACK_UNIFORM(\n        glUniform1i, Output_Shader_LookupUniform(shader, \"uTexAtlas\"), 0);\n    return shader;\n}\n\nvoid Output_UIShader_Bind(const OUTPUT_UI_SHADER *const shader)\n{\n    Output_Shader_Bind(shader);\n}\n\nvoid Output_UIShader_Free(OUTPUT_UI_SHADER *const shader)\n{\n    Output_Shader_Free(shader);\n}\n"
  },
  {
    "path": "src/trx/game/output/shaders/ui.h",
    "content": "#pragma once\n\n#include <trx/game/output/shaders/generic.h>\n\ntypedef OUTPUT_SHADER OUTPUT_UI_SHADER;\n\nOUTPUT_UI_SHADER *Output_UIShader_Create(void);\nvoid Output_UIShader_Free(OUTPUT_UI_SHADER *shader);\nvoid Output_UIShader_Bind(const OUTPUT_UI_SHADER *shader);\n"
  },
  {
    "path": "src/trx/game/output/sources/lightnings.c",
    "content": "#include <trx/game/output/sources/lightnings.h>\n\n#include <trx/game/output.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/output/vertex_range.h>\n#include <trx/gl/utils.h>\n\ntypedef struct {\n    XYZW_F pos;\n    XYZW_F normal;\n    RGBA_8888 color;\n} M_VERTEX;\n\ntypedef struct {\n    SCENE_SOURCE source;\n    OUTPUT_MESH_SHADER *shader;\n    VECTOR *vertices;\n    VECTOR *scheduled;\n    GLuint vao;\n    GLuint vbo;\n} M_PRIV;\n\nstatic M_PRIV m_Priv;\n\nstatic void M_GenerateLightningSegment(\n    M_PRIV *const p, const LIGHTNING_SEGMENT *const segment)\n{\n    const RGBA_8888 blue = { 0, 0, 255, 128 };\n    const RGBA_8888 white = { 255, 255, 255, 128 };\n    const int32_t w = segment->thickness / 2;\n\n    const XYZW_F pos_0 = {\n        .x = segment->from.x,\n        .y = segment->from.y,\n        .z = segment->from.z,\n        .w = 0.0f,\n    };\n    const XYZW_F pos_1 = {\n        .x = segment->to.x,\n        .y = segment->to.y,\n        .z = segment->to.z,\n        .w = 0.0f,\n    };\n\n    // 2 quads side-to-side (blue-white) (white-blue);\n    // double-sided so that visible from both sides\n    const M_VERTEX vertices[4][4] = {\n        // clang-format off\n        {\n            { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white },\n            { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white },\n            { .pos = pos_1, .normal = { w, 0, 0, 0 }, .color = blue },\n            { .pos = pos_0, .normal = { w, 0, 0, 0 }, .color = blue },\n        },\n        {\n            { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white },\n            { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white },\n            { .pos = pos_1, .normal = { -w, 0, 0, 0 }, .color = blue },\n            { .pos = pos_0, .normal = { -w, 0, 0, 0 }, .color = blue },\n        },\n        {\n            { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white },\n            { .pos = pos_0, .normal = { w, 0, 0, 0 }, .color = blue },\n            { .pos = pos_1, .normal = { w, 0, 0, 0 }, .color = blue },\n            { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white },\n        },\n        {\n            { .pos = pos_0, .normal = { 0, 0, 0, 0 }, .color = white },\n            { .pos = pos_0, .normal = { -w, 0, 0, 0 }, .color = blue },\n            { .pos = pos_1, .normal = { -w, 0, 0, 0 }, .color = blue },\n            { .pos = pos_1, .normal = { 0, 0, 0, 0 }, .color = white },\n        },\n        // clang-format on\n    };\n\n    for (int32_t quad = 0; quad < 4; quad++) {\n        for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) {\n            const int32_t j = OUTPUT_QUAD_TO_FAN(i);\n            Vector_Add(p->vertices, &vertices[quad][j]);\n        }\n    }\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->scheduled);\n    Vector_Clear(p->vertices);\n}\n\nstatic void M_RenderPass(\n    const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass != SCENE_PASS_TRANSPARENT) {\n        return;\n    }\n\n    glBindVertexArray(p->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n    glVertexAttrib4f(OUTPUT_MESH_ATTR_NORMAL, 0.0f, 0.0f, 0.0f, 0.0f);\n    glVertexAttrib3f(OUTPUT_MESH_ATTR_UVW, 0.0f, 0.0f, 0.0f);\n    glVertexAttrib4f(OUTPUT_MESH_ATTR_TEXTURE_SIZE, 0.0f, 0.0f, 1.0f, 1.0f);\n    glVertexAttrib2f(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 1.0f, 1.0f);\n    glVertexAttribI1ui(\n        OUTPUT_MESH_ATTR_FLAGS,\n        VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD\n            | VERT_ABS_SPRITE);\n    glVertexAttrib1f(OUTPUT_MESH_ATTR_SHADE, SHADE_NEUTRAL);\n\n    for (int32_t i = 0; i < p->scheduled->count; i++) {\n        const LIGHTNING_SEGMENT *const segment = Vector_Get(p->scheduled, i);\n        M_GenerateLightningSegment(p, segment);\n    }\n\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX),\n        Vector_GetData(p->vertices), GL_STATIC_DRAW);\n\n    Output_MeshShader_UploadModelMatrix(p->shader, &g_IDMatrix);\n    glDrawArrays(GL_TRIANGLES, 0, p->vertices->count);\n    TRX_GL_CheckError();\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    const M_PRIV *const p = &m_Priv;\n    return pass == SCENE_PASS_TRANSPARENT && p->scheduled->count > 0;\n}\n\nvoid OutputSource_Lightnings_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->shader = Output_GetMeshShader();\n    p->vertices = Vector_Create(sizeof(M_VERTEX));\n    p->scheduled = Vector_Create(sizeof(LIGHTNING_SEGMENT));\n    p->source.render_begin = M_RenderBegin;\n    p->source.render_pass = M_RenderPass;\n    p->source.is_dirty = M_IsDirty;\n    SceneCompositor_AddSource(&p->source);\n\n    glGenVertexArrays(1, &p->vao);\n    glBindVertexArray(p->vao);\n\n    glGenBuffers(1, &p->vbo);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_UVW);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR);\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, pos));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, normal));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, color));\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE);\n}\n\nvoid OutputSource_Lightnings_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->vertices != nullptr) {\n        Vector_Free(p->vertices);\n        p->vertices = nullptr;\n    }\n    if (p->scheduled != nullptr) {\n        Vector_Free(p->scheduled);\n        p->scheduled = nullptr;\n    }\n    if (p->vao != 0) {\n        glDeleteVertexArrays(1, &p->vao);\n        p->vao = 0;\n    }\n    if (p->vbo != 0) {\n        glDeleteBuffers(1, &p->vbo);\n        p->vbo = 0;\n    }\n}\n\nvoid OutputSource_Lightnings_StageSegment(\n    const LIGHTNING_SEGMENT *const segment)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Add(p->scheduled, segment);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/lightnings.h",
    "content": "#pragma once\n\n#include <trx/game/output/types.h>\n#include <trx/game/types.h>\n\nvoid OutputSource_Lightnings_Init(void);\nvoid OutputSource_Lightnings_Shutdown(void);\n\nvoid OutputSource_Lightnings_StageSegment(const LIGHTNING_SEGMENT *segment);\n"
  },
  {
    "path": "src/trx/game/output/sources/misc.c",
    "content": "#include <trx/game/output/sources/misc.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/output.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/output/vertex_range.h>\n#include <trx/gl/utils.h>\n\n#include <math.h>\n\ntypedef struct {\n    XYZW_F pos;\n} M_VERTEX;\n\ntypedef enum {\n    M_PRIMITIVE_SPHERE,\n    M_PRIMITIVE_CUBOID,\n    M_PRIMITIVE_NUMBER_OF,\n} M_PRIMITIVE_TYPE;\n\ntypedef struct {\n    MATRIX matrix;\n    M_PRIMITIVE_TYPE prim_type;\n    RGBA_8888 color;\n} M_INSTANCE;\n\ntypedef struct {\n    SCENE_SOURCE source;\n    OUTPUT_MESH_SHADER *shader;\n    OUTPUT_VERTEX_RANGE primitive_ranges[M_PRIMITIVE_NUMBER_OF];\n    VECTOR *vertices;\n    VECTOR *scheduled_spheres;\n    VECTOR *scheduled_cuboids;\n    GLuint vao;\n    GLuint vbo;\n    int32_t vertex_count;\n} M_PRIV;\n\nstatic M_PRIV m_Priv;\n\nstatic void M_SealPrimitive(\n    M_PRIV *const p, OUTPUT_VERTEX_RANGE *const target_range)\n{\n    target_range->vertex_start = p->vertex_count;\n    target_range->vertex_count =\n        p->vertices->count - target_range->vertex_start;\n    p->vertex_count += target_range->vertex_count;\n}\n\nstatic void M_GenerateSphere(\n    M_PRIV *const p, OUTPUT_VERTEX_RANGE *const target_range,\n    const int32_t subdivisions)\n{\n    // More subdivisions means smoother spheres.\n    const int32_t position_count = SQUARE(subdivisions + 1);\n    XYZW_F positions[position_count];\n    int32_t index = 0;\n\n    for (int32_t i = 0; i <= subdivisions; i++) {\n        const float theta = (M_PI * i) / subdivisions; // Latitude angle\n        const float sin_theta = sinf(theta);\n        const float cos_theta = cosf(theta);\n\n        for (int32_t j = 0; j <= subdivisions; j++) {\n            const float phi = (2 * M_PI * j) / subdivisions; // Longitude angle\n            const float sin_phi = sinf(phi);\n            const float cos_phi = cosf(phi);\n\n            // Convert spherical coordinates to 3D points.\n            positions[index] = (XYZW_F) {\n                .x = cos_phi * sin_theta,\n                .y = cos_theta,\n                .z = sin_phi * sin_theta,\n                .w = 0.0f,\n            };\n            index++;\n        }\n    }\n\n    const int32_t vertex_count =\n        subdivisions * subdivisions * OUTPUT_QUAD_VERTICES;\n    for (int32_t i = 0; i < subdivisions; i++) {\n        for (int32_t j = 0; j < subdivisions; j++) {\n            const int32_t indices[4] = {\n                i * (subdivisions + 1) + j,\n                (i + 1) * (subdivisions + 1) + j,\n                (i + 1) * (subdivisions + 1) + (j + 1),\n                i * (subdivisions + 1) + (j + 1),\n            };\n            for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) {\n                const int32_t l = OUTPUT_QUAD_TO_FAN(k);\n                Vector_Add(\n                    p->vertices,\n                    &(M_VERTEX) {\n                        .pos = positions[indices[l]],\n                    });\n            }\n            for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) {\n                const int32_t l = OUTPUT_QUAD_TO_FAN_CW(k);\n                Vector_Add(\n                    p->vertices,\n                    &(M_VERTEX) {\n                        .pos = positions[indices[l]],\n                    });\n            }\n        }\n    }\n    M_SealPrimitive(p, target_range);\n}\n\nstatic void M_GenerateCuboid(\n    M_PRIV *const p, OUTPUT_VERTEX_RANGE *const target_range)\n{\n    const XYZW_F vertices[8] = {\n        { -1, -1, 1, 0 },  { 1, -1, 1, 0 },\n        { 1, 1, 1, 0 },    { -1, 1, 1, 0 }, // front\n        { -1, -1, -1, 0 }, { 1, -1, -1, 0 },\n        { 1, 1, -1, 0 },   { -1, 1, -1, 0 } // back\n    };\n    const uint8_t order[6][6] = {\n        { 0, 1, 2, 0, 2, 3 }, { 5, 4, 7, 5, 7, 6 }, { 4, 0, 3, 4, 3, 7 },\n        { 1, 5, 6, 1, 6, 2 }, { 3, 2, 6, 3, 6, 7 }, { 4, 5, 1, 4, 1, 0 },\n    };\n    for (int32_t i = 0; i < 6; i++) {\n        for (int32_t j = 0; j < 6; j++) {\n            Vector_Add(\n                p->vertices,\n                &(M_VERTEX) {\n                    .pos = vertices[order[i][j]],\n                });\n        }\n    }\n    M_SealPrimitive(p, target_range);\n}\n\nstatic void M_DrawScheduled(M_PRIV *const p, VECTOR *const scheduled)\n{\n    for (int32_t i = 0; i < scheduled->count; i++) {\n        const M_INSTANCE *const instance = Vector_Get(scheduled, i);\n        Output_MeshShader_UploadModelMatrix(p->shader, &instance->matrix);\n        const RGBA_8888 c = instance->color;\n        glVertexAttrib4f(\n            OUTPUT_MESH_ATTR_COLOR, c.r / 255.0f, c.g / 255.0f, c.b / 255.0f,\n            c.a / 255.0f);\n\n        const OUTPUT_VERTEX_RANGE *const range =\n            &p->primitive_ranges[instance->prim_type];\n        glDrawArrays(GL_TRIANGLES, range->vertex_start, range->vertex_count);\n    }\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->scheduled_spheres);\n    Vector_Clear(p->scheduled_cuboids);\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    const M_PRIV *const p = &m_Priv;\n    return pass == SCENE_PASS_TRANSPARENT\n        && (p->scheduled_spheres->count > 0 || p->scheduled_cuboids->count > 0);\n}\n\nstatic void M_RenderPass(\n    const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass != SCENE_PASS_TRANSPARENT) {\n        return;\n    }\n    glBindVertexArray(p->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n    glVertexAttrib4f(OUTPUT_MESH_ATTR_NORMAL, 0.0f, 0.0f, 0.0f, 0.0f);\n    glVertexAttrib3f(OUTPUT_MESH_ATTR_UVW, 0.0f, 0.0f, 0.0f);\n    glVertexAttrib4f(OUTPUT_MESH_ATTR_TEXTURE_SIZE, 0.0f, 0.0f, 1.0f, 1.0f);\n    glVertexAttrib2f(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 1.0f, 1.0f);\n    glVertexAttribI1ui(\n        OUTPUT_MESH_ATTR_FLAGS,\n        VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE);\n    glVertexAttrib1f(OUTPUT_MESH_ATTR_SHADE, SHADE_NEUTRAL);\n\n    GLint bound_polygon_mode[2];\n    glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]);\n    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);\n    if (p->scheduled_spheres->count > 0) {\n        M_DrawScheduled(p, p->scheduled_spheres);\n    }\n    if (p->scheduled_cuboids->count > 0) {\n        M_DrawScheduled(p, p->scheduled_cuboids);\n    }\n    glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]);\n}\n\nvoid OutputSource_Misc_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->shader = Output_GetMeshShader();\n    p->vertices = Vector_Create(sizeof(M_VERTEX));\n    p->scheduled_spheres = Vector_Create(sizeof(M_INSTANCE));\n    p->scheduled_cuboids = Vector_Create(sizeof(M_INSTANCE));\n    p->source.render_begin = M_RenderBegin;\n    p->source.render_pass = M_RenderPass;\n    p->source.is_dirty = M_IsDirty;\n    SceneCompositor_AddSource(&p->source);\n\n    glGenVertexArrays(1, &p->vao);\n    glBindVertexArray(p->vao);\n\n    glGenBuffers(1, &p->vbo);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_UVW);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE);\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, pos));\n\n    M_GenerateSphere(p, &p->primitive_ranges[M_PRIMITIVE_SPHERE], 12);\n    M_GenerateCuboid(p, &p->primitive_ranges[M_PRIMITIVE_CUBOID]);\n\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER, p->vertex_count * sizeof(M_VERTEX),\n        Vector_GetData(p->vertices), GL_STATIC_DRAW);\n}\n\nvoid OutputSource_Misc_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->vertices != nullptr) {\n        Vector_Free(p->vertices);\n        p->vertices = nullptr;\n    }\n    if (p->scheduled_spheres != nullptr) {\n        Vector_Free(p->scheduled_spheres);\n        p->scheduled_spheres = nullptr;\n    }\n    if (p->scheduled_cuboids != nullptr) {\n        Vector_Free(p->scheduled_cuboids);\n        p->scheduled_cuboids = nullptr;\n    }\n    if (p->vao != 0) {\n        glDeleteVertexArrays(1, &p->vao);\n        p->vao = 0;\n    }\n    if (p->vbo != 0) {\n        glDeleteBuffers(1, &p->vbo);\n        p->vbo = 0;\n    }\n}\n\nvoid OutputSource_Misc_StageSphere(const RGBA_8888 color)\n{\n    M_PRIV *const p = &m_Priv;\n    M_INSTANCE inst = {\n        .matrix = *g_WMatrixPtr,\n        .prim_type = M_PRIMITIVE_SPHERE,\n        .color = color,\n    };\n    Vector_Add(p->scheduled_spheres, &inst);\n}\n\nvoid OutputSource_Misc_StageCuboid(const RGBA_8888 color)\n{\n    M_PRIV *const p = &m_Priv;\n    M_INSTANCE inst = {\n        .matrix = *g_WMatrixPtr,\n        .prim_type = M_PRIMITIVE_CUBOID,\n        .color = color,\n    };\n    Vector_Add(p->scheduled_cuboids, &inst);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/misc.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n\nvoid OutputSource_Misc_Init(void);\nvoid OutputSource_Misc_Shutdown(void);\n\nvoid OutputSource_Misc_StageSphere(RGBA_8888 color);\nvoid OutputSource_Misc_StageCuboid(RGBA_8888 color);\n"
  },
  {
    "path": "src/trx/game/output/sources/objects.c",
    "content": "#include <trx/game/output/sources/objects.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/output/mesh_batcher/mesh_builder.h>\n#include <trx/game/output/state.h>\n#include <trx/version.h>\n\ntypedef struct {\n    OUTPUT_MESH *mesh_batch;\n} M_MESH;\n\ntypedef struct {\n    MEMORY_ARENA_ALLOCATOR alloc;\n    MESH_BATCHER *batcher;\n    int16_t skybox_shade;\n    size_t mesh_count;\n    M_MESH *meshes;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic bool M_IsMeshSkybox(const int32_t mesh_idx)\n{\n    const OBJECT *const object = Object_Get(O_SKYBOX);\n    if (!object->loaded) {\n        return false;\n    }\n    return mesh_idx >= object->mesh_idx\n        && mesh_idx < object->mesh_idx + object->mesh_count;\n}\n\nstatic SCENE_PASS M_GetScenePass(const FACE *const face, const uint16_t flags)\n{\n    if ((flags & VERT_FLAT_SHADED) != 0) {\n        return SCENE_PASS_OPAQUE;\n    }\n    return Output_Textures_GetObjectTextureScenePass(face->texture_idx);\n}\n\nstatic void M_AddObjectFace(\n    MESH_BUILDER *const builder, const OBJECT_MESH *const obj_mesh,\n    const FACE *const face, uint16_t flags)\n{\n    RGBA_8888 color = COLOR_RGBA_8888_WHITE;\n    OUTPUT_MESH_VERTEX vertices[4];\n    int32_t uvw_idx = -1;\n\n    ASSERT(face->vertex_count <= 4);\n\n    if (flags & VERT_FLAT_SHADED) {\n        if (g_TRVersion == 1) {\n            color = Output_RGB2RGBA(Output_GetPaletteColor8(face->palette_idx));\n        } else {\n            color = Output_RGB2RGBA(\n                Output_GetPaletteColor16(face->palette_idx >> 8));\n        }\n    } else if (\n        Output_Textures_GetObjectTextureScenePass(face->texture_idx)\n        == SCENE_PASS_OPAQUE) {\n        flags |= VERT_NO_ALPHA_DISCARD;\n    }\n\n    if (obj_mesh->num_lights <= 0) {\n        flags |= VERT_USE_OWN_LIGHT;\n    } else {\n        flags |= VERT_USE_OBJECT_LIGHT;\n    }\n\n    for (int32_t i = 0; i < face->vertex_count; i++) {\n        const int32_t shade = obj_mesh->num_lights <= 0\n                && face->vertices[i] < -obj_mesh->num_lights\n            ? obj_mesh->lighting.lights[face->vertices[i]]\n            : SHADE_NEUTRAL;\n        const XYZ_16 normal = face->vertices[i] < obj_mesh->num_lights\n            ? obj_mesh->lighting.normals[face->vertices[i]]\n            : (XYZ_16) { 1, 0, 0 };\n        const XYZ_16 *const pos = &obj_mesh->vertices[face->vertices[i]];\n\n        if ((flags & VERT_FLAT_SHADED) == 0) {\n            uvw_idx = Output_Textures_GetObjectUVWIndex(face->texture_idx, i);\n        }\n        vertices[i] = (OUTPUT_MESH_VERTEX) {\n            .pos = { .x = pos->x, .y = pos->y, .z = pos->z },\n            .normal = { .x = normal.x, .y = normal.y, .z = normal.z },\n            .flags = flags,\n            .uvw_idx = uvw_idx,\n            .shade = shade,\n            .color = color,\n            .trapezoid_ratio = {\n                [0] = face->texture_zw[i].z,\n                [1] = face->texture_zw[i].w,\n            },\n        };\n    }\n\n    MeshBuilder_AddVertices(builder, vertices, face->vertex_count);\n    MeshBuilder_AddFan(\n        builder, M_GetScenePass(face, flags), face->double_sided);\n}\n\nstatic void M_PrepareMeshes(M_PRIV *const p)\n{\n    p->mesh_count = Object_GetMeshCount();\n    p->meshes = Memory_ArenaAlloc(&p->alloc, sizeof(M_MESH) * p->mesh_count);\n\n    MESH_BUILDER *const builder = MeshBuilder_Create();\n    for (int32_t i = 0; i < Object_GetMeshCount(); i++) {\n        const OBJECT_MESH *const obj_mesh = Object_GetMesh(i);\n        M_MESH *const new_batch = &p->meshes[i];\n\n        uint16_t flags = 0;\n        if (obj_mesh->enable_reflections) {\n            flags |= VERT_REFLECTIVE;\n        }\n\n        if (M_IsMeshSkybox(i)) {\n            flags |= VERT_USE_OWN_LIGHT;\n        }\n        for (int32_t j = 0; j < obj_mesh->tex_faces.count; j++) {\n            M_AddObjectFace(\n                builder, obj_mesh, &obj_mesh->tex_faces.data[j], flags);\n        }\n        for (int32_t j = 0; j < obj_mesh->flat_faces.count; j++) {\n            M_AddObjectFace(\n                builder, obj_mesh, &obj_mesh->flat_faces.data[j],\n                flags | VERT_FLAT_SHADED);\n        }\n\n        MeshBuilder_AdjustDepth(builder, obj_mesh->depth_adjustment);\n        OUTPUT_MESH *const mesh = MeshBuilder_Seal(builder);\n        if (mesh != nullptr) {\n            MeshBatcher_AddMesh(p->batcher, mesh);\n            new_batch->mesh_batch = mesh;\n        }\n    }\n    MeshBuilder_Destroy(builder);\n}\n\nstatic void M_FreeMeshes(M_PRIV *const p)\n{\n    if (p->meshes != nullptr) {\n        for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) {\n            MeshBatcher_RemoveMesh(p->batcher, p->meshes[i].mesh_batch);\n            if (p->meshes[i].mesh_batch != nullptr) {\n                Output_Mesh_Destroy(p->meshes[i].mesh_batch);\n            }\n        }\n        p->meshes = nullptr;\n    }\n    Memory_ArenaReset(&p->alloc);\n}\n\nstatic void M_UpdateShadesSkybox(\n    MESH_INSTANCE *const inst, void *const user_data)\n{\n    const OBJECT_MESH *const mesh = user_data;\n    const M_PRIV *const p = &m_Priv;\n\n    M_MESH *const batch = &p->meshes[Object_GetMeshIndex(mesh)];\n    if (batch->mesh_batch == nullptr) {\n        return;\n    }\n    OUTPUT_MESH_VERTEX *const vertices =\n        Vector_GetData(batch->mesh_batch->vertices);\n\n    const int16_t shade =\n        g_Config.rendering.enable_lighting ? p->skybox_shade : SHADE_NEUTRAL;\n    for (int32_t i = 0; i < batch->mesh_batch->vertices->count; i++) {\n        vertices[i].shade = shade;\n    }\n}\n\nstatic void M_UpdateFlags(const OBJECT_MESH *const mesh, M_MESH *const batch)\n{\n    uint16_t mask = VERT_REFLECTIVE | VERT_NO_LIGHTING;\n    uint16_t flags = 0;\n    if (mesh->enable_reflections) {\n        flags |= VERT_REFLECTIVE;\n    }\n    OUTPUT_MESH_VERTEX *const vertices =\n        Vector_GetData(batch->mesh_batch->vertices);\n    for (int32_t i = 0; i < batch->mesh_batch->vertices->count; i++) {\n        vertices[i].flags &= ~mask;\n        vertices[i].flags |= flags;\n    }\n}\n\nstatic void M_Stage(const OBJECT_MESH *const mesh)\n{\n    M_PRIV *const p = &m_Priv;\n    M_MESH *const batch = &p->meshes[Object_GetMeshIndex(mesh)];\n    if (batch->mesh_batch == nullptr) {\n        return;\n    }\n\n    OUTPUT_LIGHT_INFO light_info = Output_GetLightInfo();\n    if (g_TRVersion >= 3 && M_IsMeshSkybox(Object_GetMeshIndex(mesh))) {\n        light_info.tr3_ambient = COLOR_RGB_F_WHITE;\n        for (int32_t i = 0; i < 3; i++) {\n            light_info.tr3_light_color[i] = (RGB_F) { 0.0f, 0.0f, 0.0f };\n            light_info.tr3_light_dir_view[i] = (XYZ_32) { 0, 0, 0 };\n        }\n    }\n\n    const MESH_INSTANCE inst = {\n        .mesh = batch->mesh_batch,\n        .cwmatrix = *g_MatrixPtr,\n        .wmatrix = *g_WMatrixPtr,\n        .tint = Output_GetTint(),\n        .wibble = false,\n        .water_effect =\n            (mesh->enable_caustics && Output_GetWaterEffect()) ? 1 : 0,\n        .light_info = light_info,\n        .room = Output_GetCurrentRoom(),\n    };\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE);\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_TRANSPARENT);\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_BLEND_ADD);\n}\n\nvoid OutputSource_Objects_Init(MESH_BATCHER *const batcher)\n{\n    M_PRIV *const p = &m_Priv;\n    p->batcher = batcher;\n}\n\nvoid OutputSource_Objects_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeMeshes(p);\n    Memory_ArenaFree(&p->alloc);\n}\n\nvoid OutputSource_Objects_ObserveLevelLoad(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeMeshes(p);\n    M_PrepareMeshes(p);\n}\n\nvoid OutputSource_Objects_ObserveLevelUnload(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeMeshes(p);\n}\n\nvoid OutputSource_Objects_ObserveObjectMeshSwap(\n    const int32_t mesh_idx_1, const int32_t mesh_idx_2)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->meshes == nullptr) {\n        return;\n    }\n\n    SWAP(p->meshes[mesh_idx_1], p->meshes[mesh_idx_2]);\n    OutputSource_Objects_ObserveObjectMeshUpdate(mesh_idx_1);\n    OutputSource_Objects_ObserveObjectMeshUpdate(mesh_idx_2);\n}\n\nvoid OutputSource_Objects_ObserveObjectMeshUpdate(const int32_t mesh_idx)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->meshes == nullptr) {\n        return;\n    }\n    M_MESH *const batch = &p->meshes[mesh_idx];\n    if (batch->mesh_batch == nullptr) {\n        return;\n    }\n    M_UpdateFlags(Object_GetMesh(mesh_idx), batch);\n    MeshBatcher_UpdateMeshGeometry(p->batcher, batch->mesh_batch);\n}\n\nvoid OutputSource_Objects_StageSkyboxMesh(\n    const OBJECT_MESH *const mesh, const int16_t shade)\n{\n    M_PRIV *const p = &m_Priv;\n    p->skybox_shade = shade;\n    M_Stage(mesh);\n}\n\nvoid OutputSource_Objects_StageObjectMesh(const OBJECT_MESH *const mesh)\n{\n    M_Stage(mesh);\n}\n\nconst SCENE_SOURCE *OutputSource_Objects_GetSource(void)\n{\n    M_PRIV *const p = &m_Priv;\n    return MeshBatcher_AsSource(p->batcher);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/objects.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n#include <trx/game/output/mesh_batcher/batcher.h>\n#include <trx/game/output/scene_source.h>\n\nvoid OutputSource_Objects_Init(MESH_BATCHER *batcher);\nvoid OutputSource_Objects_Shutdown(void);\nvoid OutputSource_Objects_ObserveLevelLoad(void);\nvoid OutputSource_Objects_ObserveLevelUnload(void);\nvoid OutputSource_Objects_ObserveObjectMeshSwap(\n    int32_t mesh_idx_1, int32_t mesh_idx_2);\nvoid OutputSource_Objects_ObserveObjectMeshUpdate(int32_t mesh_idx);\n\nvoid OutputSource_Objects_StageSkyboxMesh(\n    const OBJECT_MESH *mesh, int16_t shade);\nvoid OutputSource_Objects_StageObjectMesh(const OBJECT_MESH *mesh);\n\nconst SCENE_SOURCE *OutputSource_Objects_GetSource(void);\n"
  },
  {
    "path": "src/trx/game/output/sources/overlay.c",
    "content": "#include <trx/game/output/sources/overlay.h>\n\n#include <trx/av/image.h>\n#include <trx/config.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/fader.h>\n#include <trx/game/game.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/const.h>\n#include <trx/game/output/overlay.h>\n#include <trx/game/output/quad.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/scene_source.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/viewport.h>\n#include <trx/gl/texture.h>\n#include <trx/gl/utils.h>\n\n#include <float.h>\n#include <math.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n\n#define M_SCHEDULE_OP(queue_id, draw_func, inst)                               \\\n    M_ScheduleOpHelper(                                                        \\\n        &m_Priv, queue_id, (M_DRAW_OP_FUNC)draw_func, sizeof(inst),            \\\n        (const M_DRAW_OP *)&inst);\n\n#define M_IMAGE_CACHE_CAPACITY 5\n#define M_RELATIVE_ERROR(a, b) ABS((a) - (b)) / (b)\n\nstruct M_DRAW_OP;\ntypedef void (*M_DRAW_OP_FUNC)(const struct M_DRAW_OP *);\n\ntypedef struct M_DRAW_OP {\n    M_DRAW_OP_FUNC draw;\n} M_DRAW_OP;\n\ntypedef struct {\n    M_DRAW_OP base;\n    float opacity;\n} M_DRAW_OP_BLACK_RECTANGLE;\n\ntypedef struct {\n    M_DRAW_OP base;\n    GLuint texture_id;\n    int32_t width;\n    int32_t height;\n    float opacity;\n    float desaturation;\n    bool flip_y;\n    bool use_fit;\n    TEXTURE_FILTER texture_filter;\n} M_DRAW_OP_IMAGE;\n\ntypedef struct {\n    M_DRAW_OP base;\n    GLuint texture_id;\n    int32_t width;\n    int32_t height;\n    float opacity;\n} M_DRAW_OP_SNAPSHOT;\n\ntypedef struct {\n    M_DRAW_OP base;\n    bool wave;\n    float opacity;\n} M_DRAW_OP_PATTERN;\n\ntypedef struct {\n    char *path;\n    int32_t width;\n    int32_t height;\n} M_IMAGE_CANDIDATE;\n\ntypedef struct {\n    bool in_use;\n    uint64_t last_used_token;\n    char *file_name;\n\n    char *scan_path;\n    VECTOR *candidates;\n\n    char *loaded_path;\n    float loaded_for_screen_ratio;\n    TRX_GL_TEXTURE texture;\n    int32_t texture_width;\n    int32_t texture_height;\n} M_IMAGE_CACHE_ENTRY;\n\ntypedef struct {\n    bool has_content;\n    float captured_brightness;\n    TRX_GL_TEXTURE texture;\n    int32_t width;\n    int32_t height;\n} M_SNAPSHOT_STATE;\n\ntypedef struct {\n    SCENE_SOURCE source;\n    MEMORY_ARENA_ALLOCATOR alloc;\n    VECTOR *ops[2];\n    OUTPUT_QUAD *renderer;\n    TRX_GL_TEXTURE solid_black_texture;\n    struct {\n        OUTPUT_QUAD *renderer;\n        uint64_t next_use_token;\n        M_IMAGE_CACHE_ENTRY entries[M_IMAGE_CACHE_CAPACITY];\n    } image;\n    struct {\n        OUTPUT_QUAD *renderer;\n        M_SNAPSHOT_STATE state;\n        bool transition_active;\n        FADER transition_fader;\n    } snapshot;\n    struct {\n        OUTPUT_QUAD *renderer;\n        bool uploaded;\n        int32_t texture_idx;\n        int32_t tex_page;\n        OUTPUT_QUAD_SURFACE_DESC desc;\n        OUTPUT_TEXTURE_SIZE atlas_size;\n    } pattern;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {\n    .alloc = {\n        .default_chunk_size = 1024 * 4,\n    },\n};\n\nstatic bool M_CreateTextureRGB8(\n    TRX_GL_TEXTURE *const texture, const int32_t width, const int32_t height,\n    const void *const data)\n{\n    if (texture == nullptr || width <= 0 || height <= 0) {\n        return false;\n    }\n    if (!texture->initialized) {\n        TRX_GL_Texture_Init(texture, GL_TEXTURE_2D);\n    }\n    TRX_GL_Texture_Bind(texture);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n\n    // RGB8 rows are tightly packed (3 bytes per pixel). With the default\n    // unpack alignment of 4, OpenGL will assume padding at the end of each row\n    // when width * 3 is not a multiple of 4, which looks like an incorrect\n    // source stride.\n    GLint prev_unpack_alignment = 0;\n    glGetIntegerv(GL_UNPACK_ALIGNMENT, &prev_unpack_alignment);\n    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n    TRX_GL_CheckError();\n    glTexImage2D(\n        GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE,\n        data);\n    glPixelStorei(GL_UNPACK_ALIGNMENT, prev_unpack_alignment);\n    TRX_GL_CheckError();\n    return true;\n}\n\nstatic void M_CloseTexture(TRX_GL_TEXTURE *const texture)\n{\n    if (texture != nullptr && texture->initialized) {\n        TRX_GL_Texture_Close(texture);\n        texture->initialized = false;\n    }\n}\n\nstatic float M_GetScreenAspectRatio(void)\n{\n    const int32_t w = Viewport_GetWidth(VIEWPORT_GAME);\n    const int32_t h = Viewport_GetHeight(VIEWPORT_GAME);\n    if (w <= 0 || h <= 0) {\n        return 1.0f;\n    }\n    return w / (float)h;\n}\n\nstatic bool M_PrepareViewportCopy(\n    const VIEWPORT_SPACE viewport, const int32_t desired_width,\n    const int32_t desired_height, VIEWPORT_RECT *const rect,\n    int32_t *const copy_width, int32_t *const copy_height)\n{\n    if (rect == nullptr || copy_width == nullptr || copy_height == nullptr) {\n        return false;\n    }\n\n    const VIEWPORT_RECT viewport_rect = Viewport_GetRect(viewport);\n    if (viewport_rect.width <= 0 || viewport_rect.height <= 0) {\n        return false;\n    }\n\n    *rect = viewport_rect;\n    *copy_width = desired_width;\n    *copy_height = desired_height;\n    CLAMPG(*copy_width, viewport_rect.width);\n    CLAMPG(*copy_height, viewport_rect.height);\n    return *copy_width > 0 && *copy_height > 0;\n}\n\nstatic void M_CopyPresentedFrameToTexture(\n    TRX_GL_TEXTURE *const texture, const int32_t width, const int32_t height)\n{\n    if (texture == nullptr || !texture->initialized || width <= 0\n        || height <= 0) {\n        return;\n    }\n\n    VIEWPORT_RECT rect;\n    int32_t copy_width = 0;\n    int32_t copy_height = 0;\n    if (!M_PrepareViewportCopy(\n            VIEWPORT_TARGET, width, height, &rect, &copy_width, &copy_height)) {\n        return;\n    }\n\n    GLint prev_read_fbo = 0;\n    glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read_fbo);\n    GLint prev_read_buffer = 0;\n    glGetIntegerv(GL_READ_BUFFER, &prev_read_buffer);\n\n    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);\n    glReadBuffer(GL_FRONT);\n\n    TRX_GL_Texture_Bind(texture);\n    glCopyTexSubImage2D(\n        GL_TEXTURE_2D, 0, 0, 0, rect.x, rect.y, copy_width, copy_height);\n\n    glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prev_read_fbo);\n    glReadBuffer(prev_read_buffer);\n    TRX_GL_CheckError();\n}\n\nstatic void M_EnsureSolidBlackTexture(void)\n{\n    if (m_Priv.solid_black_texture.initialized) {\n        return;\n    }\n    const uint8_t pixel[3] = { 0, 0, 0 };\n    M_CreateTextureRGB8(&m_Priv.solid_black_texture, 1, 1, &pixel[0]);\n}\n\nstatic void M_ImageCandidates_Free(M_IMAGE_CACHE_ENTRY *const e)\n{\n    if (e->candidates == nullptr) {\n        Memory_FreePointer(&e->scan_path);\n        return;\n    }\n\n    for (int32_t i = 0; i < e->candidates->count; i++) {\n        M_IMAGE_CANDIDATE *const candidate = Vector_Get(e->candidates, i);\n        Memory_FreePointer(&candidate->path);\n    }\n    Vector_Free(e->candidates);\n    e->candidates = nullptr;\n    Memory_FreePointer(&e->scan_path);\n}\n\nstatic void M_ImageCandidates_Scan(\n    M_IMAGE_CACHE_ENTRY *const e, const char *const base_image_path)\n{\n    ASSERT(e != nullptr);\n    ASSERT(base_image_path != nullptr);\n\n    M_ImageCandidates_Free(e);\n\n    LOG_INFO(\"Searching for overlay images\");\n    VECTOR *candidates = nullptr;\n\n    const char *last_slash = strrchr(base_image_path, '/');\n    const char *last_backslash = strrchr(base_image_path, '\\\\');\n    const char *last_sep =\n        last_slash > last_backslash ? last_slash : last_backslash;\n\n    size_t dir_len = 0;\n    char *dir_path = nullptr;\n    if (last_sep != nullptr) {\n        dir_len = (size_t)(last_sep - base_image_path);\n        dir_path = String_Format(\"%.*s\", (int)dir_len, base_image_path);\n    } else {\n        dir_path = Memory_DupStr(\".\");\n    }\n\n    const char *const file_name =\n        last_sep != nullptr ? last_sep + 1 : base_image_path;\n\n    void *const dir_handle = File_OpenDirectory(dir_path);\n    if (dir_handle == nullptr) {\n        e->scan_path = dir_path;\n        return;\n    }\n\n    candidates = Vector_Create(sizeof(M_IMAGE_CANDIDATE));\n    const char *entry;\n    while ((entry = File_ReadDirectory(dir_handle)) != nullptr) {\n        // Match the file itself, and assume it's of 16:9 aspect ratio.\n        if (String_Equivalent(entry, file_name)) {\n            Vector_Add(\n                candidates,\n                &(M_IMAGE_CANDIDATE) {\n                    .path = String_Format(\"%s/%s\", dir_path, file_name),\n                    .width = 16,\n                    .height = 9,\n                });\n        }\n\n        // Match directories with pattern: <width>x<height>\n        int32_t w = 0;\n        int32_t h = 0;\n        if (sscanf(entry, \"%dx%d\", &w, &h) == 2) {\n            const char *const candidate_path =\n                String_FormatStatic(\"%s/%s/%s\", dir_path, entry, file_name);\n            if (File_Exists(candidate_path)) {\n                Vector_Add(\n                    candidates,\n                    &(M_IMAGE_CANDIDATE) {\n                        .path = Memory_DupStr(candidate_path),\n                        .width = w,\n                        .height = h,\n                    });\n            }\n        }\n    }\n    File_CloseDirectory(dir_handle);\n\n    for (int32_t i = 0; i < candidates->count; i++) {\n        const M_IMAGE_CANDIDATE *const candidate = Vector_Get(candidates, i);\n        LOG_INFO(\n            \"%d. %s (%d:%d)\", i + 1, candidate->path, candidate->width,\n            candidate->height);\n    }\n\n    e->scan_path = dir_path;\n    e->candidates = candidates;\n}\n\nstatic const M_IMAGE_CANDIDATE *M_ImageCandidates_PickBest(\n    const M_IMAGE_CACHE_ENTRY *const e, const float screen_ratio)\n{\n    if (e->candidates == nullptr) {\n        return nullptr;\n    }\n    int32_t best_idx = -1;\n    float best_err = FLT_MAX;\n    const M_IMAGE_CANDIDATE *const raw = Vector_GetData(e->candidates);\n    for (int32_t i = 0; i < e->candidates->count; i++) {\n        const float candidate_ratio = raw[i].width / (float)raw[i].height;\n        const float err = M_RELATIVE_ERROR(candidate_ratio, screen_ratio);\n        if (err < best_err) {\n            best_err = err;\n            best_idx = i;\n        }\n    }\n    return best_idx >= 0 ? &raw[best_idx] : nullptr;\n}\n\nstatic bool M_Image_LoadIntoTexture(\n    M_IMAGE_CACHE_ENTRY *const e, const char *const path)\n{\n    ASSERT(e != nullptr);\n    ASSERT(path != nullptr);\n\n    IMAGE *const img = Image_CreateFromFile(path);\n    if (img == nullptr) {\n        return false;\n    }\n\n    const int32_t width = img->width;\n    const int32_t height = img->height;\n    if (width <= 0 || height <= 0 || img->data == nullptr) {\n        Image_Free(img);\n        return false;\n    }\n\n    const bool ok = M_CreateTextureRGB8(&e->texture, width, height, img->data);\n    Image_Free(img);\n    if (!ok) {\n        return false;\n    }\n\n    e->texture_width = width;\n    e->texture_height = height;\n    return true;\n}\n\nstatic void M_ImageCacheEntry_Reset(M_IMAGE_CACHE_ENTRY *const e)\n{\n    ASSERT(e != nullptr);\n\n    M_CloseTexture(&e->texture);\n    Memory_FreePointer(&e->file_name);\n    Memory_FreePointer(&e->loaded_path);\n    M_ImageCandidates_Free(e);\n    *e = (M_IMAGE_CACHE_ENTRY) { 0 };\n}\n\nstatic M_IMAGE_CACHE_ENTRY *M_ImageCache_GetEntry(\n    M_PRIV *const p, const char *const file_name)\n{\n    ASSERT(p != nullptr);\n    ASSERT(file_name != nullptr);\n\n    for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) {\n        M_IMAGE_CACHE_ENTRY *const e = &p->image.entries[i];\n        if (e->in_use && e->file_name != nullptr\n            && String_Equivalent(e->file_name, file_name)) {\n            return e;\n        }\n    }\n\n    for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) {\n        M_IMAGE_CACHE_ENTRY *const e = &p->image.entries[i];\n        if (!e->in_use) {\n            e->in_use = true;\n            e->file_name = Memory_DupStr(file_name);\n            return e;\n        }\n    }\n\n    int32_t evict_idx = 0;\n    uint64_t best_token = p->image.entries[0].last_used_token;\n    for (int32_t i = 1; i < M_IMAGE_CACHE_CAPACITY; i++) {\n        if (p->image.entries[i].last_used_token < best_token) {\n            best_token = p->image.entries[i].last_used_token;\n            evict_idx = i;\n        }\n    }\n\n    M_ImageCacheEntry_Reset(&p->image.entries[evict_idx]);\n    p->image.entries[evict_idx].in_use = true;\n    p->image.entries[evict_idx].file_name = Memory_DupStr(file_name);\n    return &p->image.entries[evict_idx];\n}\n\nstatic bool M_ImageCache_Load(M_IMAGE_CACHE_ENTRY *const e)\n{\n    ASSERT(e != nullptr);\n    ASSERT(e->file_name != nullptr);\n\n    if (e->candidates == nullptr && e->scan_path == nullptr) {\n        M_ImageCandidates_Scan(e, e->file_name);\n    }\n\n    const float screen_ratio = M_GetScreenAspectRatio();\n    const bool should_reselect =\n        e->loaded_path == nullptr || e->loaded_for_screen_ratio != screen_ratio;\n\n    if (e->texture.initialized && !should_reselect) {\n        return true;\n    }\n\n    const M_IMAGE_CANDIDATE *const best =\n        M_ImageCandidates_PickBest(e, screen_ratio);\n    if (best != nullptr) {\n        const bool already_loaded = e->loaded_path != nullptr\n            && String_Equivalent(best->path, e->loaded_path);\n        if (!already_loaded || !e->texture.initialized) {\n            if (M_Image_LoadIntoTexture(e, best->path)) {\n                char *prev = e->loaded_path;\n                e->loaded_path = Memory_DupStr(best->path);\n                Memory_FreePointer(&prev);\n                e->loaded_for_screen_ratio = screen_ratio;\n                return true;\n            }\n        } else {\n            e->loaded_for_screen_ratio = screen_ratio;\n            return true;\n        }\n    }\n\n    const bool already_loaded = e->loaded_path != nullptr\n        && String_Equivalent(e->file_name, e->loaded_path);\n    if (!already_loaded || !e->texture.initialized) {\n        if (M_Image_LoadIntoTexture(e, e->file_name)) {\n            char *prev = e->loaded_path;\n            e->loaded_path = Memory_DupStr(e->file_name);\n            Memory_FreePointer(&prev);\n            e->loaded_for_screen_ratio = screen_ratio;\n            return true;\n        }\n    }\n\n    M_CloseTexture(&e->texture);\n    e->texture_width = 0;\n    e->texture_height = 0;\n    Memory_FreePointer(&e->loaded_path);\n    e->loaded_for_screen_ratio = 0.0f;\n    return false;\n}\n\nstatic inline void *M_ArenaAlloc(M_PRIV *const p, const size_t sz)\n{\n    void *const mem = Memory_ArenaAlloc(&p->alloc, sz);\n    memset(mem, 0, sz);\n    return mem;\n}\n\nstatic inline void M_ScheduleOp(\n    M_PRIV *const p, const int32_t queue_id, M_DRAW_OP *const op)\n{\n    Vector_Add(p->ops[queue_id], &op);\n}\n\nstatic void M_ScheduleOpHelper(\n    M_PRIV *const p, const int32_t queue_id, const M_DRAW_OP_FUNC draw_func,\n    const size_t size, const M_DRAW_OP *const op_src)\n{\n    M_DRAW_OP *const op = M_ArenaAlloc(&m_Priv, size);\n    memcpy(op, op_src, size);\n    op->draw = draw_func;\n    M_ScheduleOp(p, queue_id, op);\n}\n\nstatic void M_DrawOp_BlackRectangle(const M_DRAW_OP_BLACK_RECTANGLE *const op)\n{\n    const M_PRIV *const p = &m_Priv;\n    if (op->opacity <= 0.0f) {\n        return;\n    }\n    M_EnsureSolidBlackTexture();\n    Output_Quad_SetExternalTexture(\n        p->renderer, p->solid_black_texture.id, 1, 1, false);\n    Output_Quad_SetEffect(p->renderer, OUTPUT_QUAD_EFFECT_NONE);\n    Output_Quad_SetRepeat(p->renderer, 1, 1);\n    Output_Quad_SetTextureSize(p->renderer, nullptr);\n    Output_Quad_SetOpacity(p->renderer, op->opacity);\n    Output_Quad_RenderWithBlend(p->renderer);\n}\n\nstatic void M_DrawOp_Image(const M_DRAW_OP_IMAGE *const op)\n{\n    const M_PRIV *const p = &m_Priv;\n    if (op->texture_id == 0 || op->width <= 0 || op->height <= 0) {\n        return;\n    }\n\n    Output_Quad_SetExternalTexture(\n        p->image.renderer, op->texture_id, op->width, op->height, op->flip_y);\n    Output_Quad_SetEffect(p->image.renderer, OUTPUT_QUAD_EFFECT_NONE);\n    Output_Quad_SetRepeat(p->image.renderer, 1, 1);\n    Output_Quad_SetTextureSize(p->image.renderer, nullptr);\n    Output_Quad_SetFilter(p->image.renderer, op->texture_filter);\n    Output_SetDesaturation(op->desaturation);\n    if (op->use_fit) {\n        Output_Quad_SetFit(\n            p->image.renderer, OUTPUT_QUAD_FIT_SMART, (float)op->width,\n            (float)op->height);\n    } else {\n        Output_Quad_ClearFit(p->image.renderer);\n    }\n    Output_Quad_SetOpacity(p->image.renderer, op->opacity);\n    if (op->opacity >= 1.0f) {\n        Output_Quad_Render(p->image.renderer);\n    } else {\n        Output_Quad_RenderWithBlend(p->image.renderer);\n    }\n    Output_SetDesaturation(0.0f);\n}\n\nstatic void M_DrawImageImpl(\n    const char *const file_name, const float intensity,\n    const TEXTURE_FILTER texture_filter)\n{\n    if (!Output_Overlay_LoadImage(file_name)) {\n        return;\n    }\n\n    const M_PRIV *const p = &m_Priv;\n    for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) {\n        const M_IMAGE_CACHE_ENTRY *const e = &p->image.entries[i];\n        if (e->in_use && e->file_name != nullptr\n            && String_Equivalent(e->file_name, file_name)\n            && e->texture.initialized) {\n            M_SCHEDULE_OP(\n                false, M_DrawOp_Image,\n                ((M_DRAW_OP_IMAGE) {\n                    .texture_id = e->texture.id,\n                    .width = e->texture_width,\n                    .height = e->texture_height,\n                    .opacity = 1.0f,\n                    .desaturation = intensity,\n                    .flip_y = false,\n                    .use_fit = true,\n                    .texture_filter = texture_filter,\n                }));\n            return;\n        }\n    }\n}\n\nstatic void M_DrawOp_Snapshot(const M_DRAW_OP_SNAPSHOT *const op)\n{\n    const M_PRIV *const p = &m_Priv;\n    if (op->opacity <= 0.0f) {\n        return;\n    }\n    if (op->texture_id == 0 || op->width <= 0 || op->height <= 0) {\n        return;\n    }\n\n    Output_Quad_SetExternalTexture(\n        p->snapshot.renderer, op->texture_id, op->width, op->height, true);\n    Output_Quad_SetEffect(p->snapshot.renderer, OUTPUT_QUAD_EFFECT_NONE);\n    Output_Quad_SetRepeat(p->snapshot.renderer, 1, 1);\n    Output_Quad_SetTextureSize(p->snapshot.renderer, nullptr);\n    Output_Quad_ClearFit(p->snapshot.renderer);\n    Output_Quad_SetOpacity(p->snapshot.renderer, op->opacity);\n    Output_Quad_RenderWithBlend(p->snapshot.renderer);\n}\n\nstatic bool M_EnsurePatternUploaded(M_PRIV *const p)\n{\n    if (p->pattern.renderer == nullptr) {\n        return false;\n    }\n\n    const OBJECT *const obj = Object_Get(O_INV_BACKGROUND);\n    if (obj == nullptr || !obj->loaded) {\n        return false;\n    }\n\n    const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx);\n    if (mesh == nullptr || mesh->tex_face4s.count < 1) {\n        return false;\n    }\n\n    const int32_t texture_idx = mesh->tex_face4s.data[0].texture_idx;\n    const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(texture_idx);\n    if (texture == nullptr) {\n        return false;\n    }\n\n    const RGBA_8888 *const page = Output_GetTexturePage32(texture->tex_page);\n    if (page == nullptr) {\n        return false;\n    }\n\n    const OUTPUT_QUAD_SURFACE_DESC desc = {\n        .width = TEXTURE_PAGE_WIDTH,\n        .height = TEXTURE_PAGE_HEIGHT,\n        .bit_count = 32,\n        .tex_format = GL_RGBA,\n        .tex_type = GL_UNSIGNED_INT_8_8_8_8_REV,\n        .uv = {\n            {\n                texture->uv[0].u / 256.0f / TEXTURE_PAGE_WIDTH,\n                texture->uv[0].v / 256.0f / TEXTURE_PAGE_HEIGHT\n            },\n            {\n                texture->uv[1].u / 256.0f / TEXTURE_PAGE_WIDTH,\n                texture->uv[1].v / 256.0f / TEXTURE_PAGE_HEIGHT\n            },\n            {\n                texture->uv[2].u / 256.0f / TEXTURE_PAGE_WIDTH,\n                texture->uv[2].v / 256.0f / TEXTURE_PAGE_HEIGHT\n            },\n            {\n                texture->uv[3].u / 256.0f / TEXTURE_PAGE_WIDTH,\n                texture->uv[3].v / 256.0f / TEXTURE_PAGE_HEIGHT\n            },\n        },\n        .pitch = TEXTURE_PAGE_WIDTH * 2,\n    };\n\n    if (!p->pattern.uploaded || p->pattern.texture_idx != texture_idx\n        || p->pattern.tex_page != texture->tex_page\n        || memcmp(&p->pattern.desc, &desc, sizeof(desc)) != 0) {\n        OUTPUT_QUAD_SURFACE_DESC tmp_desc = desc;\n        Output_Quad_Upload(p->pattern.renderer, &tmp_desc, (uint8_t *)page);\n        p->pattern.uploaded = true;\n        p->pattern.texture_idx = texture_idx;\n        p->pattern.tex_page = texture->tex_page;\n        p->pattern.desc = desc;\n        p->pattern.atlas_size = Output_Textures_GetAtlasSize(texture_idx);\n    }\n\n    return true;\n}\n\nstatic void M_DrawOp_Pattern(const M_DRAW_OP_PATTERN *const op)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!M_EnsurePatternUploaded(p)) {\n        return;\n    }\n    if (op->opacity <= 0.0f) {\n        return;\n    }\n\n    const int32_t repeat_y = 6;\n    const float screen_ratio = M_GetScreenAspectRatio();\n    const int32_t repeat_x = (int32_t)roundf(repeat_y * screen_ratio);\n\n    Output_Quad_SetRepeat(p->pattern.renderer, repeat_x, repeat_y);\n    Output_Quad_SetTextureSize(\n        p->pattern.renderer,\n        &(OUTPUT_QUAD_TEXTURE_SIZE) {\n            .x0 = p->pattern.atlas_size.x0,\n            .y0 = p->pattern.atlas_size.y0,\n            .x1 = p->pattern.atlas_size.x1,\n            .y1 = p->pattern.atlas_size.y1,\n        });\n    Output_Quad_SetEffect(\n        p->pattern.renderer,\n        op->wave ? OUTPUT_QUAD_EFFECT_WAVE : OUTPUT_QUAD_EFFECT_VIGNETTE);\n    Output_Quad_SetFilter(\n        p->pattern.renderer, g_Config.rendering.texture_filter);\n    Output_Quad_ClearFit(p->pattern.renderer);\n    Output_Quad_SetOpacity(p->pattern.renderer, op->opacity);\n    if (op->opacity >= 1.0f) {\n        Output_Quad_Render(p->pattern.renderer);\n    } else {\n        Output_Quad_RenderWithBlend(p->pattern.renderer);\n    }\n}\n\nstatic void M_EnsureSnapshotTexture(M_PRIV *const p)\n{\n    const int32_t w = Viewport_GetWidth(VIEWPORT_TARGET);\n    const int32_t h = Viewport_GetHeight(VIEWPORT_TARGET);\n    if (w <= 0 || h <= 0) {\n        return;\n    }\n\n    M_SNAPSHOT_STATE *const s = &p->snapshot.state;\n    if (s->texture.initialized && s->width == w && s->height == h) {\n        return;\n    }\n\n    s->width = w;\n    s->height = h;\n    M_CreateTextureRGB8(&s->texture, s->width, s->height, nullptr);\n    Output_Quad_ClearFit(p->snapshot.renderer);\n}\n\nstatic void M_RunQueue(const VECTOR *const queue)\n{\n    for (int32_t i = 0; i < queue->count; i++) {\n        M_DRAW_OP *const op = *(M_DRAW_OP **)Vector_Get(queue, i);\n        op->draw(op);\n    }\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->ops[0]);\n    Vector_Clear(p->ops[1]);\n    Memory_ArenaReset(&p->alloc);\n\n    if (p->snapshot.transition_active) {\n        const float opacity =\n            Fader_GetCurrentValue(&p->snapshot.transition_fader);\n\n        if (opacity <= 0.0f || !p->snapshot.state.has_content\n            || !p->snapshot.state.texture.initialized) {\n            p->snapshot.transition_active = false;\n            p->snapshot.state.has_content = false;\n            return;\n        }\n\n        M_SCHEDULE_OP(\n            false, M_DrawOp_Snapshot,\n            ((M_DRAW_OP_SNAPSHOT) {\n                .texture_id = p->snapshot.state.texture.id,\n                .width = p->snapshot.state.width,\n                .height = p->snapshot.state.height,\n                .opacity = opacity,\n            }));\n\n        if (!Fader_IsActive(&p->snapshot.transition_fader)) {\n            p->snapshot.transition_active = false;\n            p->snapshot.state.has_content = false;\n        }\n    }\n}\n\nstatic void M_RenderPass(const SCENE_SOURCE *const src, const SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass == SCENE_PASS_OVERLAY_PRE_UI) {\n        M_RunQueue(p->ops[0]);\n    } else if (pass == SCENE_PASS_OVERLAY_POST_UI) {\n        M_RunQueue(p->ops[1]);\n    }\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const src, const SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass == SCENE_PASS_OVERLAY_PRE_UI) {\n        return p->ops[0]->count > 0 || p->snapshot.transition_active;\n    } else if (pass == SCENE_PASS_OVERLAY_POST_UI) {\n        return p->ops[1]->count > 0;\n    }\n    return false;\n}\n\nbool Output_Overlay_LoadImage(const char *const file_name)\n{\n    if (file_name == nullptr || file_name[0] == '\\0') {\n        return false;\n    }\n\n    M_PRIV *const p = &m_Priv;\n    M_IMAGE_CACHE_ENTRY *const e = M_ImageCache_GetEntry(p, file_name);\n    if (p->image.next_use_token == 0) {\n        p->image.next_use_token = 1;\n    }\n    e->last_used_token = p->image.next_use_token++;\n    return M_ImageCache_Load(e);\n}\n\nvoid Output_Overlay_DrawImage(const char *const file_name)\n{\n    M_DrawImageImpl(file_name, 0.0f, TEXTURE_FILTER_POINT);\n}\n\nvoid Output_Overlay_DrawImageBilinear(const char *const file_name)\n{\n    M_DrawImageImpl(file_name, 0.0f, TEXTURE_FILTER_BILINEAR);\n}\n\nvoid Output_Overlay_DrawImageMono(\n    const char *const file_name, const float intensity)\n{\n    M_DrawImageImpl(file_name, intensity, TEXTURE_FILTER_POINT);\n}\n\nvoid Output_Overlay_CaptureSnapshot(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->snapshot.transition_active = false;\n    p->snapshot.state.has_content = false;\n\n    M_EnsureSnapshotTexture(p);\n    if (!p->snapshot.state.texture.initialized) {\n        return;\n    }\n\n    M_CopyPresentedFrameToTexture(\n        &p->snapshot.state.texture, p->snapshot.state.width,\n        p->snapshot.state.height);\n    p->snapshot.state.has_content = true;\n\n    // Remove the captured brightness so we can reapply the current multiplier.\n    p->snapshot.state.captured_brightness = g_Config.visuals.ui_brightness;\n    CLAMPL(p->snapshot.state.captured_brightness, 0.001f);\n    Output_Quad_SetBrightnessScale(\n        p->snapshot.renderer, 1.0f / p->snapshot.state.captured_brightness);\n}\n\nvoid Output_Overlay_DrawSnapshot(const float opacity)\n{\n    const M_PRIV *const p = &m_Priv;\n    if (!p->snapshot.state.has_content\n        || !p->snapshot.state.texture.initialized) {\n        return;\n    }\n    if (opacity <= 0.0f) {\n        return;\n    }\n\n    M_SCHEDULE_OP(\n        false, M_DrawOp_Snapshot,\n        ((M_DRAW_OP_SNAPSHOT) {\n            .texture_id = p->snapshot.state.texture.id,\n            .width = p->snapshot.state.width,\n            .height = p->snapshot.state.height,\n            .opacity = opacity,\n        }));\n}\n\nvoid Output_Overlay_DrawPattern(const bool wave)\n{\n    Output_Overlay_DrawPatternOpacity(wave, 1.0f);\n}\n\nvoid Output_Overlay_DrawPatternOpacity(const bool wave, const float opacity)\n{\n    M_SCHEDULE_OP(\n        false, M_DrawOp_Pattern,\n        ((M_DRAW_OP_PATTERN) { .wave = wave, .opacity = opacity }));\n}\n\nvoid Output_Overlay_BeginTransitionFadeOut(\n    const float duration, const float start)\n{\n    M_PRIV *const p = &m_Priv;\n    Output_Overlay_CaptureSnapshot();\n    if (!p->snapshot.state.has_content) {\n        return;\n    }\n\n    p->snapshot.transition_active = true;\n    Fader_InitTo(&p->snapshot.transition_fader, start, 0.0f, duration);\n}\n\nvoid Output_Overlay_DrawGame(void)\n{\n    Interpolation_Disable();\n    Game_Draw(false);\n    Interpolation_Enable();\n}\n\nvoid Output_Overlay_DrawGameMonoCool(const float desaturation)\n{\n    Output_SetDesaturation(desaturation);\n    Output_SetGlobalTint(Color_Mix(\n        COLOR_RGB_F_WHITE, ((RGB_F) { 0.666f, 0.666f, 1.0f }), desaturation));\n    Output_Overlay_DrawGame();\n    Output_SetGlobalTint(COLOR_RGB_F_WHITE);\n    Output_SetDesaturation(0.0f);\n}\n\nvoid Output_Overlay_DrawGameMonoWarm(const float desaturation)\n{\n    Output_SetDesaturation(desaturation);\n    Output_SetGlobalTint(Color_Mix(\n        COLOR_RGB_F_WHITE, ((RGB_F) { 1.0f, 0.666f, 0.666f }), desaturation));\n    Output_Overlay_DrawGame();\n    Output_SetGlobalTint(COLOR_RGB_F_WHITE);\n    Output_SetDesaturation(0.0f);\n}\n\nvoid Output_Overlay_DrawGameMono(const float desaturation)\n{\n    Output_SetDesaturation(desaturation);\n    Output_Overlay_DrawGame();\n    Output_SetDesaturation(0.0f);\n}\n\nvoid Output_Overlay_DrawBlackRectangle(const float opacity, const bool post_ui)\n{\n    if (opacity > 0.0f) {\n        M_SCHEDULE_OP(\n            post_ui, M_DrawOp_BlackRectangle,\n            ((M_DRAW_OP_BLACK_RECTANGLE) { .opacity = opacity }));\n    }\n}\n\nvoid OutputSource_Overlay_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->renderer = Output_Quad_Create();\n    p->image.renderer = Output_Quad_Create();\n    p->snapshot.renderer = Output_Quad_Create();\n    p->pattern.renderer = Output_Quad_Create();\n    p->pattern.uploaded = false;\n    M_EnsureSolidBlackTexture();\n\n    p->source.render_begin = M_RenderBegin;\n    p->source.render_pass = M_RenderPass;\n    p->source.is_dirty = M_IsDirty;\n    p->ops[0] = Vector_Create(sizeof(M_DRAW_OP *));\n    p->ops[1] = Vector_Create(sizeof(M_DRAW_OP *));\n    SceneCompositor_AddSource(&p->source);\n}\n\nvoid OutputSource_Overlay_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->renderer != nullptr) {\n        Output_Quad_Destroy(p->renderer);\n        p->renderer = nullptr;\n    }\n    if (p->image.renderer != nullptr) {\n        Output_Quad_Destroy(p->image.renderer);\n        p->image.renderer = nullptr;\n    }\n    if (p->snapshot.renderer != nullptr) {\n        Output_Quad_Destroy(p->snapshot.renderer);\n        p->snapshot.renderer = nullptr;\n    }\n    if (p->pattern.renderer != nullptr) {\n        Output_Quad_Destroy(p->pattern.renderer);\n        p->pattern.renderer = nullptr;\n    }\n    for (int32_t i = 0; i < M_IMAGE_CACHE_CAPACITY; i++) {\n        if (p->image.entries[i].in_use) {\n            M_ImageCacheEntry_Reset(&p->image.entries[i]);\n        }\n    }\n    p->pattern.uploaded = false;\n    M_CloseTexture(&p->snapshot.state.texture);\n    p->snapshot.state.has_content = false;\n    M_CloseTexture(&p->solid_black_texture);\n\n    if (p->ops[0] != nullptr) {\n        Vector_Free(p->ops[0]);\n        p->ops[0] = nullptr;\n    }\n    if (p->ops[1] != nullptr) {\n        Vector_Free(p->ops[1]);\n        p->ops[1] = nullptr;\n    }\n    Memory_ArenaFree(&p->alloc);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/overlay.h",
    "content": "#pragma once\n\nvoid OutputSource_Overlay_Init(void);\nvoid OutputSource_Overlay_Shutdown(void);\n"
  },
  {
    "path": "src/trx/game/output/sources/poly_fx.c",
    "content": "#include <trx/game/output/sources/poly_fx.h>\n\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/items.h>\n#include <trx/game/output.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/sparks.h>\n#include <trx/gl/utils.h>\n\n#include <math.h>\n#include <stddef.h>\n#include <stdlib.h>\n#include <string.h>\n\ntypedef struct {\n    XYZW_F pos;\n    XYZW_F normal;\n    OUTPUT_UVW uvw;\n    OUTPUT_TEXTURE_SIZE texture_size;\n    float trapezoid_ratio[2];\n    OUTPUT_USHORT flags;\n    RGBA_8888 color;\n    float shade;\n} M_VERTEX;\n\ntypedef struct {\n    int32_t sprite_idx;\n    bool use_custom_uv;\n    uint8_t corner_count;\n    float z_depth_adjust;\n    XYZ_32 world_pos[4];\n    OUTPUT_UVW uvw[4];\n    OUTPUT_TEXTURE_SIZE texture_size[4];\n    float disp[4][2];\n    RGBA_8888 color[4];\n    uint16_t flags;\n} M_PRIM;\n\ntypedef struct {\n    int32_t sort_key;\n    const M_PRIM *prim;\n} M_PRIM_SORT;\n\ntypedef struct {\n    SCENE_SOURCE source;\n    OUTPUT_MESH_SHADER *shader;\n    VECTOR *scheduled_transparent; // M_PRIM\n    VECTOR *scheduled_blend_add; // M_PRIM\n    VECTOR *scheduled_blend_sub; // M_PRIM\n    VECTOR *sorted; // M_PRIM_SORT\n    VECTOR *vertices; // M_VERTEX\n    GLuint vao;\n    GLuint vbo;\n} M_PRIV;\n\nstatic M_PRIV m_Priv;\n\nstatic int M_ComparePrimDepth(const void *const a, const void *const b)\n{\n    const M_PRIM_SORT *const prim_a = a;\n    const M_PRIM_SORT *const prim_b = b;\n    if (prim_b->sort_key == prim_a->sort_key) {\n        return (intptr_t)prim_b->prim - (intptr_t)prim_a->prim;\n    }\n    return prim_b->sort_key - prim_a->sort_key;\n}\n\nstatic int32_t M_GetViewDepth(const XYZ_32 pos)\n{\n    // clang-format off\n    return\n        g_ViewMatrix._20 * pos.x +\n        g_ViewMatrix._21 * pos.y +\n        g_ViewMatrix._22 * pos.z +\n        g_ViewMatrix._23;\n    // clang-format on\n}\n\nstatic XYZ_32 M_GetSparkRenderPos(const SPARK *const spark, const float ratio)\n{\n    const bool use_current_state =\n        (int32_t)spark->s_life - (int32_t)spark->life <= 1;\n    if (use_current_state) {\n        return Sparks_GetWorldPos(spark);\n    }\n\n    if ((spark->flags & SPARK_F_ATTACHED_NODE) != 0U) {\n        const XYZ_32 current_pos = Sparks_GetWorldPos(spark);\n        return (XYZ_32) {\n            .x = (int32_t)LERP(spark->prev_world_pos.x, current_pos.x, ratio),\n            .y = (int32_t)LERP(spark->prev_world_pos.y, current_pos.y, ratio),\n            .z = (int32_t)LERP(spark->prev_world_pos.z, current_pos.z, ratio),\n        };\n    }\n\n    const XYZ_32 local_pos = {\n        .x = (int32_t)LERP(spark->prev_pos.x, spark->pos.x, ratio),\n        .y = (int32_t)LERP(spark->prev_pos.y, spark->pos.y, ratio),\n        .z = (int32_t)LERP(spark->prev_pos.z, spark->pos.z, ratio),\n    };\n\n    if ((spark->flags & SPARK_F_FX) != 0U) {\n        const EFFECT *const effect = Effect_Get(spark->effect_num);\n        if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) {\n            return effect->interp.result.pos;\n        }\n        return (XYZ_32) {\n            .x = effect->interp.result.pos.x + local_pos.x,\n            .y = effect->interp.result.pos.y + local_pos.y,\n            .z = effect->interp.result.pos.z + local_pos.z,\n        };\n    }\n\n    if ((spark->flags & SPARK_F_ITEM) != 0U) {\n        const ITEM *const item = Item_Get(spark->item_num);\n        if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) {\n            return item->interp.result.pos;\n        }\n        return (XYZ_32) {\n            .x = item->interp.result.pos.x + local_pos.x,\n            .y = item->interp.result.pos.y + local_pos.y,\n            .z = item->interp.result.pos.z + local_pos.z,\n        };\n    }\n\n    return local_pos;\n}\n\nstatic void M_ApplyTintToColors(RGBA_8888 color[4], const uint8_t corner_count)\n{\n    const RGB_F tint = Output_GetTint();\n    for (uint8_t i = 0; i < corner_count; i++) {\n        int32_t r = (int32_t)((float)color[i].r * tint.r);\n        int32_t g = (int32_t)((float)color[i].g * tint.g);\n        int32_t b = (int32_t)((float)color[i].b * tint.b);\n        CLAMP(r, 0, 255);\n        CLAMP(g, 0, 255);\n        CLAMP(b, 0, 255);\n        color[i].r = (uint8_t)r;\n        color[i].g = (uint8_t)g;\n        color[i].b = (uint8_t)b;\n    }\n}\n\nstatic XYZ_32 M_GetPrimCentroid(const M_PRIM *const prim)\n{\n    XYZ_32 centroid = { 0, 0, 0 };\n    for (uint8_t i = 0; i < prim->corner_count; i++) {\n        centroid.x += prim->world_pos[i].x;\n        centroid.y += prim->world_pos[i].y;\n        centroid.z += prim->world_pos[i].z;\n    }\n    centroid.x /= (int32_t)prim->corner_count;\n    centroid.y /= (int32_t)prim->corner_count;\n    centroid.z /= (int32_t)prim->corner_count;\n    return centroid;\n}\n\nstatic VECTOR *M_GetScheduledVectorForPass(\n    M_PRIV *const p, const SCENE_PASS pass)\n{\n    if (pass == SCENE_PASS_BLEND_ADD) {\n        return p->scheduled_blend_add;\n    }\n    if (pass == SCENE_PASS_BLEND_SUB) {\n        return p->scheduled_blend_sub;\n    }\n    return p->scheduled_transparent;\n}\n\nstatic void M_SortPrims(M_PRIV *const p, const SCENE_PASS pass)\n{\n    Vector_Clear(p->sorted);\n\n    const VECTOR *const prims = M_GetScheduledVectorForPass(p, pass);\n    for (int32_t i = 0; i < prims->count; i++) {\n        const M_PRIM *const prim = Vector_Get(prims, i);\n        const XYZ_32 centroid = M_GetPrimCentroid(prim);\n        const M_PRIM_SORT sort = {\n            .sort_key = M_GetViewDepth(centroid),\n            .prim = prim,\n        };\n        Vector_Add(p->sorted, &sort);\n    }\n\n    if (p->sorted->count > 1) {\n        qsort(\n            Vector_GetData(p->sorted), p->sorted->count, sizeof(M_PRIM_SORT),\n            M_ComparePrimDepth);\n    }\n}\n\nstatic void M_EmitPrimVertices(M_PRIV *const p, const M_PRIM *const prim)\n{\n    const uint16_t flags = prim->flags;\n    const uint8_t corner_count = prim->corner_count;\n\n    const int32_t sprite_corner_map_quad[4] = { 0, 1, 2, 3 };\n    const int32_t sprite_corner_map_tri[3] = { 0, 1, 3 };\n    const int32_t *sprite_corner_map = sprite_corner_map_quad;\n    if (corner_count == 3U) {\n        sprite_corner_map = sprite_corner_map_tri;\n    }\n\n    OUTPUT_UVW uvw[4];\n    OUTPUT_TEXTURE_SIZE texture_size[4];\n    for (uint8_t i = 0; i < corner_count; i++) {\n        if (prim->use_custom_uv) {\n            uvw[i] = prim->uvw[i];\n            texture_size[i] = prim->texture_size[i];\n        } else if (prim->sprite_idx >= 0) {\n            const int32_t sprite_corner = sprite_corner_map[i];\n            const int32_t uvw_idx = Output_Textures_GetSpriteUVWIndex(\n                prim->sprite_idx, (int16_t)sprite_corner);\n            uvw[i] = Output_Textures_GetUVW(uvw_idx);\n            texture_size[i] = Output_Textures_GetAtlasSize(uvw_idx / 4);\n        } else {\n            uvw[i] = (OUTPUT_UVW) { 0.0f, 0.0f, 0.0f };\n            texture_size[i] = (OUTPUT_TEXTURE_SIZE) { 0.0f, 0.0f, 0.0f, 0.0f };\n        }\n    }\n\n    const int32_t idx_quad[2][OUTPUT_QUAD_VERTICES] = {\n        { 0, 1, 2, 0, 2, 3 }, // front\n        { 0, 2, 1, 0, 3, 2 }, // back\n    };\n    const int32_t idx_tri[2][3] = {\n        { 0, 1, 2 }, // front\n        { 0, 2, 1 }, // back\n    };\n\n    const int32_t *idx = (const int32_t *)idx_quad;\n    int32_t vertex_count = OUTPUT_QUAD_VERTICES;\n    if (corner_count == 3U) {\n        idx = (const int32_t *)idx_tri;\n        vertex_count = 3;\n    }\n\n    for (int32_t side = 0; side < 2; side++) {\n        for (int32_t i = 0; i < vertex_count; i++) {\n            const int32_t corner = idx[side * vertex_count + i];\n            const int32_t uv_idx = corner;\n            const M_VERTEX v = {\n                .pos = {\n                    .x = (float)prim->world_pos[corner].x,\n                    .y = (float)prim->world_pos[corner].y,\n                    .z = (float)prim->world_pos[corner].z,\n                    .w = prim->z_depth_adjust,\n                },\n                .normal = {\n                    .x = prim->disp[corner][0],\n                    .y = prim->disp[corner][1],\n                    .z = 0.0f,\n                    .w = 0.0f,\n                },\n                .uvw = uvw[uv_idx],\n                .texture_size = texture_size[uv_idx],\n                .trapezoid_ratio = { 1.0f, 1.0f },\n                .flags = flags,\n                .color = prim->color[corner],\n                .shade = (float)SHADE_NEUTRAL,\n            };\n            Vector_Add(p->vertices, &v);\n        }\n    }\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->scheduled_transparent);\n    Vector_Clear(p->scheduled_blend_add);\n    Vector_Clear(p->scheduled_blend_sub);\n    Vector_Clear(p->vertices);\n    Vector_Clear(p->sorted);\n}\n\nstatic void M_RenderPass(const SCENE_SOURCE *const source, SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass != SCENE_PASS_OPAQUE && pass != SCENE_PASS_TRANSPARENT\n        && pass != SCENE_PASS_BLEND_SUB && pass != SCENE_PASS_BLEND_ADD) {\n        return;\n    }\n\n    if (pass == SCENE_PASS_OPAQUE) {\n        pass = SCENE_PASS_TRANSPARENT;\n    } else {\n        Vector_Clear(p->vertices);\n        Vector_Clear(p->sorted);\n    }\n\n    M_SortPrims(p, pass);\n    for (int32_t i = 0; i < p->sorted->count; i++) {\n        const M_PRIM_SORT *const sort = Vector_Get(p->sorted, i);\n        M_EmitPrimVertices(p, sort->prim);\n    }\n\n    glBindVertexArray(p->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX),\n        Vector_GetData(p->vertices), GL_DYNAMIC_DRAW);\n\n    // PolyFX primitives are staged with Output_GetTint() already applied at\n    // stage time, because different rooms can have different water state.\n    Output_MeshShader_UploadTint(p->shader, COLOR_RGB_F_WHITE);\n    Output_MeshShader_UploadWaterEffect(p->shader, 0);\n    Output_MeshShader_UploadWibbleEffect(p->shader, false);\n    Output_MeshShader_UploadModelMatrix(p->shader, &g_IDMatrix);\n    glDrawArrays(GL_TRIANGLES, 0, p->vertices->count);\n    TRX_GL_CheckError();\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    const M_PRIV *const p = &m_Priv;\n    if (pass == SCENE_PASS_TRANSPARENT || pass == SCENE_PASS_OPAQUE) {\n        return p->scheduled_transparent->count > 0;\n    }\n    if (pass == SCENE_PASS_BLEND_SUB) {\n        return p->scheduled_blend_sub->count > 0;\n    }\n    if (pass == SCENE_PASS_BLEND_ADD) {\n        return p->scheduled_blend_add->count > 0;\n    }\n    return false;\n}\n\nvoid OutputSource_PolyFX_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->shader = Output_GetMeshShader();\n    p->scheduled_transparent = Vector_Create(sizeof(M_PRIM));\n    p->scheduled_blend_add = Vector_Create(sizeof(M_PRIM));\n    p->scheduled_blend_sub = Vector_Create(sizeof(M_PRIM));\n    p->sorted = Vector_Create(sizeof(M_PRIM_SORT));\n    p->vertices = Vector_Create(sizeof(M_VERTEX));\n    p->source.render_begin = M_RenderBegin;\n    p->source.render_pass = M_RenderPass;\n    p->source.is_dirty = M_IsDirty;\n    SceneCompositor_AddSource(&p->source);\n\n    glGenVertexArrays(1, &p->vao);\n    glBindVertexArray(p->vao);\n\n    glGenBuffers(1, &p->vbo);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_UVW);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE);\n\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, pos));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, normal));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_UVW, 3, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, uvw));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_TEXTURE_SIZE, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, texture_size));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 2, GL_FLOAT, GL_FALSE,\n        sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, trapezoid_ratio));\n    glVertexAttribIPointer(\n        OUTPUT_MESH_ATTR_FLAGS, 1, OUTPUT_USHORT_GL, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, flags));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, color));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_SHADE, 1, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, shade));\n}\n\nvoid OutputSource_PolyFX_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->scheduled_transparent != nullptr) {\n        Vector_Free(p->scheduled_transparent);\n        p->scheduled_transparent = nullptr;\n    }\n    if (p->scheduled_blend_add != nullptr) {\n        Vector_Free(p->scheduled_blend_add);\n        p->scheduled_blend_add = nullptr;\n    }\n    if (p->scheduled_blend_sub != nullptr) {\n        Vector_Free(p->scheduled_blend_sub);\n        p->scheduled_blend_sub = nullptr;\n    }\n    if (p->sorted != nullptr) {\n        Vector_Free(p->sorted);\n        p->sorted = nullptr;\n    }\n    if (p->vertices != nullptr) {\n        Vector_Free(p->vertices);\n        p->vertices = nullptr;\n    }\n    if (p->vao != 0) {\n        glDeleteVertexArrays(1, &p->vao);\n        p->vao = 0;\n    }\n    if (p->vbo != 0) {\n        glDeleteBuffers(1, &p->vbo);\n        p->vbo = 0;\n    }\n}\n\nstatic void M_StagePrim(\n    const int32_t sprite_idx, const uint8_t corner_count,\n    const XYZ_32 *const world_pos, const float (*disp)[2],\n    const RGBA_8888 *const color, const uint16_t flags,\n    const float z_depth_adjust, VECTOR *const target)\n{\n    M_PRIM prim;\n    prim.sprite_idx = sprite_idx;\n    prim.use_custom_uv = false;\n    prim.corner_count = corner_count;\n    prim.z_depth_adjust = z_depth_adjust;\n    memset(prim.world_pos, 0, sizeof(prim.world_pos));\n    memcpy(prim.world_pos, world_pos, sizeof(prim.world_pos[0]) * corner_count);\n    memset(prim.uvw, 0, sizeof(prim.uvw));\n    memset(prim.texture_size, 0, sizeof(prim.texture_size));\n    if (disp != nullptr) {\n        memset(prim.disp, 0, sizeof(prim.disp));\n        memcpy(prim.disp, disp, sizeof(prim.disp[0]) * corner_count);\n    } else {\n        memset(prim.disp, 0, sizeof(prim.disp));\n    }\n    memset(prim.color, 0, sizeof(prim.color));\n    memcpy(prim.color, color, sizeof(prim.color[0]) * corner_count);\n    M_ApplyTintToColors(prim.color, corner_count);\n    prim.flags = flags;\n    Vector_Add(target, &prim);\n}\n\nstatic VECTOR *M_GetScheduledVectorForDrawType(\n    M_PRIV *const p, const DRAW_TYPE draw_type)\n{\n    if (draw_type == DRAW_BLEND_ADD || draw_type == DRAW_REFLECTIVE_BLEND_ADD) {\n        return p->scheduled_blend_add;\n    }\n    if (draw_type == DRAW_BLEND_SUB) {\n        return p->scheduled_blend_sub;\n    }\n    return p->scheduled_transparent;\n}\n\nvoid OutputSource_PolyFX_StageSpriteQuadWorld(\n    const int32_t sprite_idx, const XYZ_32 world_pos[4],\n    const RGBA_8888 color[4], const DRAW_TYPE draw_type)\n{\n    OutputSource_PolyFX_StageSpriteQuadWorldDepth(\n        sprite_idx, world_pos, color, 0.0f, draw_type);\n}\n\nvoid OutputSource_PolyFX_StageSpriteQuadWorldDepth(\n    const int32_t sprite_idx, const XYZ_32 world_pos[4],\n    const RGBA_8888 color[4], const float z_depth_adjust,\n    const DRAW_TYPE draw_type)\n{\n    M_PRIV *const p = &m_Priv;\n    VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type);\n    M_StagePrim(\n        sprite_idx, 4, &world_pos[0], nullptr, &color[0],\n        VERT_NO_LIGHTING | VERT_NO_WIBBLE, z_depth_adjust, target);\n}\n\nvoid OutputSource_PolyFX_StageSpriteTriWorld(\n    const int32_t sprite_idx, const XYZ_32 world_pos[3],\n    const RGBA_8888 color[3], const DRAW_TYPE draw_type)\n{\n    OutputSource_PolyFX_StageSpriteTriWorldDepth(\n        sprite_idx, world_pos, color, 0.0f, draw_type);\n}\n\nvoid OutputSource_PolyFX_StageSpriteTriWorldDepth(\n    const int32_t sprite_idx, const XYZ_32 world_pos[3],\n    const RGBA_8888 color[3], const float z_depth_adjust,\n    const DRAW_TYPE draw_type)\n{\n    M_PRIV *const p = &m_Priv;\n    VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type);\n    M_StagePrim(\n        sprite_idx, 3, &world_pos[0], nullptr, &color[0],\n        VERT_NO_LIGHTING | VERT_NO_WIBBLE, z_depth_adjust, target);\n}\n\nvoid OutputSource_PolyFX_StageQuadExt(\n    const int32_t sprite_idx, const XYZ_32 world_pos[4], const float disp[4][2],\n    const RGBA_8888 color[4], const uint16_t flags, const DRAW_TYPE draw_type)\n{\n    M_PRIV *const p = &m_Priv;\n    VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type);\n    M_StagePrim(\n        sprite_idx, 4, &world_pos[0], disp, &color[0], flags, 0.0f, target);\n}\n\nvoid OutputSource_PolyFX_StageQuadExtUV(\n    const XYZ_32 world_pos[4], const OUTPUT_UVW uvw[4],\n    const OUTPUT_TEXTURE_SIZE texture_size[4], const float disp[4][2],\n    const RGBA_8888 color[4], const uint16_t flags, const DRAW_TYPE draw_type)\n{\n    M_PRIV *const p = &m_Priv;\n    VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type);\n\n    M_PRIM prim;\n    prim.sprite_idx = -1;\n    prim.use_custom_uv = true;\n    prim.corner_count = 4;\n    prim.z_depth_adjust = 0.0f;\n    memcpy(prim.world_pos, world_pos, sizeof(prim.world_pos));\n    memcpy(prim.uvw, uvw, sizeof(prim.uvw));\n    memcpy(prim.texture_size, texture_size, sizeof(prim.texture_size));\n    if (disp != nullptr) {\n        memcpy(prim.disp, disp, sizeof(prim.disp));\n    } else {\n        memset(prim.disp, 0, sizeof(prim.disp));\n    }\n    memcpy(prim.color, color, sizeof(prim.color));\n    M_ApplyTintToColors(prim.color, 4);\n    prim.flags = flags;\n    Vector_Add(target, &prim);\n}\n\nvoid OutputSource_PolyFX_StageTriExtUV(\n    const XYZ_32 world_pos[3], const OUTPUT_UVW uvw[3],\n    const OUTPUT_TEXTURE_SIZE texture_size[3], const float disp[3][2],\n    const RGBA_8888 color[3], const uint16_t flags, const DRAW_TYPE draw_type)\n{\n    M_PRIV *const p = &m_Priv;\n    VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type);\n\n    M_PRIM prim;\n    prim.sprite_idx = -1;\n    prim.use_custom_uv = true;\n    prim.corner_count = 3;\n    prim.z_depth_adjust = 0.0f;\n    memset(prim.world_pos, 0, sizeof(prim.world_pos));\n    memcpy(prim.world_pos, world_pos, sizeof(world_pos[0]) * 3);\n    memset(prim.uvw, 0, sizeof(prim.uvw));\n    memcpy(prim.uvw, uvw, sizeof(uvw[0]) * 3);\n    memset(prim.texture_size, 0, sizeof(prim.texture_size));\n    memcpy(prim.texture_size, texture_size, sizeof(texture_size[0]) * 3);\n    if (disp != nullptr) {\n        memset(prim.disp, 0, sizeof(prim.disp));\n        memcpy(prim.disp, disp, sizeof(disp[0]) * 3);\n    } else {\n        memset(prim.disp, 0, sizeof(prim.disp));\n    }\n    memset(prim.color, 0, sizeof(prim.color));\n    memcpy(prim.color, color, sizeof(color[0]) * 3);\n    M_ApplyTintToColors(prim.color, 3);\n    prim.flags = flags;\n    Vector_Add(target, &prim);\n}\n\nvoid OutputSource_PolyFX_StageLineSegment(\n    const XYZ_32 from, const RGBA_8888 from_color, const XYZ_32 to,\n    const RGBA_8888 to_color, const float half_width, const DRAW_TYPE draw_type)\n{\n    const int64_t zv_mid = (M_GetViewDepth(from) + M_GetViewDepth(to)) / 2;\n    const int64_t near_z = Output_GetNearZ();\n    const int64_t far_z = Output_GetFarZ();\n    if (zv_mid <= near_z || zv_mid >= far_z) {\n        return;\n    }\n\n    const float delta_x = (float)(to.x - from.x);\n    const float delta_y = (float)(to.y - from.y);\n    const float delta_z = (float)(to.z - from.z);\n    float dir_len =\n        sqrtf(delta_x * delta_x + delta_y * delta_y + delta_z * delta_z);\n    if (dir_len <= 0.00001f) {\n        return;\n    }\n    dir_len = 1.0f / dir_len;\n\n    const float dir_x = delta_x * dir_len;\n    const float dir_y = delta_y * dir_len;\n    const float dir_z = delta_z * dir_len;\n\n    const XYZ_32 mid = {\n        (from.x + to.x) / 2,\n        (from.y + to.y) / 2,\n        (from.z + to.z) / 2,\n    };\n    const float vx = (float)(mid.x - g_Camera.pos.x);\n    const float vy = (float)(mid.y - g_Camera.pos.y);\n    const float vz = (float)(mid.z - g_Camera.pos.z);\n    float v_len = sqrtf(vx * vx + vy * vy + vz * vz);\n    if (v_len <= 0.00001f) {\n        return;\n    }\n    v_len = 1.0f / v_len;\n    const float view_x = vx * v_len;\n    const float view_y = vy * v_len;\n    const float view_z = vz * v_len;\n\n    float side_x = dir_y * view_z - dir_z * view_y;\n    float side_y = dir_z * view_x - dir_x * view_z;\n    float side_z = dir_x * view_y - dir_y * view_x;\n    float side_len = sqrtf(side_x * side_x + side_y * side_y + side_z * side_z);\n    if (side_len <= 0.00001f) {\n        side_x = dir_y * 0.0f - dir_z * 1.0f;\n        side_y = dir_z * 0.0f - dir_x * 0.0f;\n        side_z = dir_x * 1.0f - dir_y * 0.0f;\n        side_len = sqrtf(side_x * side_x + side_y * side_y + side_z * side_z);\n        if (side_len <= 0.00001f) {\n            return;\n        }\n    }\n    side_len = 1.0f / side_len;\n    side_x *= side_len;\n    side_y *= side_len;\n    side_z *= side_len;\n\n    const XYZ_32 world_pos[4] = {\n        {\n            from.x - (int32_t)lrintf(side_x * half_width),\n            from.y - (int32_t)lrintf(side_y * half_width),\n            from.z - (int32_t)lrintf(side_z * half_width),\n        },\n        {\n            from.x + (int32_t)lrintf(side_x * half_width),\n            from.y + (int32_t)lrintf(side_y * half_width),\n            from.z + (int32_t)lrintf(side_z * half_width),\n        },\n        {\n            to.x + (int32_t)lrintf(side_x * half_width),\n            to.y + (int32_t)lrintf(side_y * half_width),\n            to.z + (int32_t)lrintf(side_z * half_width),\n        },\n        {\n            to.x - (int32_t)lrintf(side_x * half_width),\n            to.y - (int32_t)lrintf(side_y * half_width),\n            to.z - (int32_t)lrintf(side_z * half_width),\n        },\n    };\n    const RGBA_8888 color[4] = { from_color, from_color, to_color, to_color };\n    const float disp[4][2] = {};\n\n    OutputSource_PolyFX_StageQuadExt(\n        -1, world_pos, disp, color,\n        VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE, draw_type);\n}\n\nvoid OutputSource_PolyFX_StageSpark(const SPARK *const spark)\n{\n    if (spark == nullptr || !spark->on) {\n        return;\n    }\n\n    DRAW_TYPE draw_type = spark->draw_type;\n    const float ratio = Interpolation_GetWorldRate();\n    const bool use_current_state =\n        (int32_t)spark->s_life - (int32_t)spark->life <= 1;\n    const XYZ_32 pos = M_GetSparkRenderPos(spark, ratio);\n    const XYZ_32 world_pos[4] = { pos, pos, pos, pos };\n\n    const int64_t zv = M_GetViewDepth(pos);\n    const int64_t near_z = Output_GetNearZ();\n    const int64_t far_z = Output_GetFarZ();\n    if (zv <= near_z || zv >= far_z) {\n        return;\n    }\n\n    int32_t vpos_z = (int32_t)(zv >> W2V_SHIFT);\n    if (vpos_z == 0) {\n        vpos_z = 1;\n    }\n\n    const RGB_888 render_color = use_current_state\n        ? spark->color\n        : (RGB_888) {\n              .r = (uint8_t)LERP(\n                  (int32_t)spark->prev_color.r, (int32_t)spark->color.r, ratio),\n              .g = (uint8_t)LERP(\n                  (int32_t)spark->prev_color.g, (int32_t)spark->color.g, ratio),\n              .b = (uint8_t)LERP(\n                  (int32_t)spark->prev_color.b, (int32_t)spark->color.b, ratio),\n          };\n\n    const int32_t render_width = use_current_state\n        ? (int32_t)spark->size.width\n        : (int32_t)LERP(\n              (int32_t)spark->prev_size.width, (int32_t)spark->size.width,\n              ratio);\n    const int32_t render_height = use_current_state\n        ? (int32_t)spark->size.height\n        : (int32_t)LERP(\n              (int32_t)spark->prev_size.height, (int32_t)spark->size.height,\n              ratio);\n    int32_t sw = render_width;\n    int32_t sh = render_height;\n\n    const bool use_sprite = (spark->flags & SPARK_F_SPRITE) != 0U;\n    if ((spark->flags & SPARK_F_SCALE) != 0U) {\n        const int32_t scalar = spark->scalar;\n        sw = (int32_t)(((((int64_t)sw * g_PhdPersp) << scalar) / vpos_z));\n        sh = (int32_t)(((((int64_t)sh * g_PhdPersp) << scalar) / vpos_z));\n\n        if (use_sprite) {\n            const int32_t max_w = render_width << scalar;\n            const int32_t max_h = render_height << scalar;\n            int32_t min_wh = 4;\n            if ((spark->flags & SPARK_F_ATTACHED_NODE) != 0U\n                && spark->node_num == 0U) {\n                min_wh = 2;\n            }\n            CLAMP(sw, min_wh, max_w);\n            CLAMP(sh, min_wh, max_h);\n        } else {\n            const int32_t max_w = render_width << 2;\n            const int32_t max_h = render_height << 2;\n            CLAMP(sw, 1, max_w);\n            CLAMP(sh, 1, max_h);\n        }\n    }\n\n    const float w = ((sw / 2.0f) * (float)vpos_z) / (float)g_PhdPersp;\n    const float h = ((sh / 2.0f) * (float)vpos_z) / (float)g_PhdPersp;\n    float disp[4][2] = {\n        { -w, -h },\n        { -w, h },\n        { w, h },\n        { w, -h },\n    };\n\n    RGBA_8888 color = { render_color.r, render_color.g, render_color.b, 255 };\n\n    if ((spark->flags & SPARK_F_ROTATE) != 0U) {\n        const int32_t rot_angle = use_current_state\n            ? (int32_t)spark->rot_angle\n            : Math_AngleMean(\n                  (int32_t)spark->prev_rot_angle, (int32_t)spark->rot_angle,\n                  ratio);\n        const int32_t angle = rot_angle * DEG_360 / 0xFFF.p0;\n        const float s = Math_Sin(angle) / (float)(1 << W2V_SHIFT);\n        const float c = Math_Cos(angle) / (float)(1 << W2V_SHIFT);\n        for (int32_t i = 0; i < 4; i++) {\n            const float x = disp[i][0];\n            const float y = disp[i][1];\n            disp[i][0] = x * c - y * s;\n            disp[i][1] = x * s + y * c;\n        }\n    }\n\n    uint16_t flags =\n        VERT_NO_LIGHTING | VERT_NO_WIBBLE | VERT_BILLBOARD | VERT_ABS_SPRITE;\n    int32_t sprite_idx = spark->sprite_idx;\n    if ((spark->flags & SPARK_F_SPRITE) == 0U) {\n        flags |= VERT_FLAT_SHADED;\n        sprite_idx = -1;\n        if (draw_type == DRAW_BLEND_ADD\n            || draw_type == DRAW_REFLECTIVE_BLEND_ADD) {\n            color.a = 128;\n        }\n        draw_type = DRAW_BLEND;\n    }\n    M_PRIV *const p = &m_Priv;\n    VECTOR *const target = M_GetScheduledVectorForDrawType(p, draw_type);\n\n    const RGBA_8888 world_color[4] = { color, color, color, color };\n    M_StagePrim(\n        sprite_idx, 4, &world_pos[0], disp, &world_color[0], flags, 0.0f,\n        target);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/poly_fx.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/types.h>\n#include <trx/game/sparks.h>\n#include <trx/game/types.h>\n\n#include <stdint.h>\n\nvoid OutputSource_PolyFX_Init(void);\nvoid OutputSource_PolyFX_Shutdown(void);\n\nvoid OutputSource_PolyFX_StageSpriteQuadWorld(\n    int32_t sprite_idx, const XYZ_32 world_pos[4], const RGBA_8888 color[4],\n    DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageSpriteQuadWorldDepth(\n    int32_t sprite_idx, const XYZ_32 world_pos[4], const RGBA_8888 color[4],\n    float z_depth_adjust, DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageSpriteTriWorld(\n    int32_t sprite_idx, const XYZ_32 world_pos[3], const RGBA_8888 color[3],\n    DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageSpriteTriWorldDepth(\n    int32_t sprite_idx, const XYZ_32 world_pos[3], const RGBA_8888 color[3],\n    float z_depth_adjust, DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageQuadExt(\n    int32_t sprite_idx, const XYZ_32 world_pos[4], const float disp[4][2],\n    const RGBA_8888 color[4], uint16_t flags, DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageQuadExtUV(\n    const XYZ_32 world_pos[4], const OUTPUT_UVW uvw[4],\n    const OUTPUT_TEXTURE_SIZE texture_size[4], const float disp[4][2],\n    const RGBA_8888 color[4], uint16_t flags, DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageTriExtUV(\n    const XYZ_32 world_pos[3], const OUTPUT_UVW uvw[3],\n    const OUTPUT_TEXTURE_SIZE texture_size[3], const float disp[3][2],\n    const RGBA_8888 color[3], uint16_t flags, DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageLineSegment(\n    XYZ_32 from, RGBA_8888 from_color, XYZ_32 to, RGBA_8888 to_color,\n    float half_width, DRAW_TYPE draw_type);\n\nvoid OutputSource_PolyFX_StageSpark(const SPARK *spark);\n"
  },
  {
    "path": "src/trx/game/output/sources/rooms.c",
    "content": "#include <trx/game/output/sources/rooms.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/output.h>\n#include <trx/game/output/bind.h>\n#include <trx/game/output/mesh_batcher/mesh_builder.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\ntypedef struct {\n    MESH_BATCHER *batcher;\n    size_t mesh_count;\n    OUTPUT_MESH **meshes;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic SCENE_PASS M_GetScenePass(const FACE *const face)\n{\n    return Output_Textures_GetObjectTextureScenePass(face->texture_idx);\n}\n\nstatic void M_AddRoomFace(\n    MESH_BUILDER *const builder, const FACE *const face, const ROOM *const room)\n{\n    OUTPUT_MESH_VERTEX vertices[4];\n\n    ASSERT(face->vertex_count <= 4);\n\n    for (int32_t i = 0; i < face->vertex_count; i++) {\n        const ROOM_VERTEX *const room_vert =\n            &room->mesh.vertices[face->vertices[i]];\n\n        uint16_t flags = 0;\n        if (room_vert->flags.disable_wibble) {\n            flags |= VERT_NO_WIBBLE;\n        }\n        if (room_vert->flags.move) {\n            flags |= VERT_MOVE;\n        }\n        if (room_vert->flags.glow) {\n            flags |= VERT_GLOW;\n        }\n        if (Output_Textures_GetObjectTextureScenePass(face->texture_idx)\n            == SCENE_PASS_OPAQUE) {\n            flags |= VERT_NO_ALPHA_DISCARD;\n        }\n        flags |= VERT_USE_DYNAMIC_LIGHT;\n\n        const XYZ_16 *const pos = &room_vert->pos;\n        vertices[i] = (OUTPUT_MESH_VERTEX) {\n            .pos = { .x = pos->x, .y = pos->y, .z = pos->z },\n            .flags = flags,\n            .uvw_idx = Output_Textures_GetObjectUVWIndex(face->texture_idx, i),\n            .shade = room_vert->light_base,\n            .light_table_idx = room_vert->light_table_value,\n            .color = room_vert->color,\n            .trapezoid_ratio = {\n                [0] = face->texture_zw[i].z,\n                [1] = face->texture_zw[i].w,\n            },\n        };\n    }\n    MeshBuilder_AddVertices(builder, vertices, face->vertex_count);\n    MeshBuilder_AddFan(builder, M_GetScenePass(face), face->double_sided);\n}\n\nstatic int32_t M_GetWaterEffect(const ROOM *const room)\n{\n    if (g_TRVersion >= 3) {\n        return 2 + (int32_t)room->water_scheme;\n    }\n    return Output_GetWaterEffect() ? 1 : 0;\n}\n\nstatic void M_PrepareMeshes(M_PRIV *const p)\n{\n    p->mesh_count = Room_GetCount();\n    p->meshes = Memory_Alloc(sizeof(OUTPUT_MESH *) * p->mesh_count);\n\n    MESH_BUILDER *const builder = MeshBuilder_Create();\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        for (int32_t j = 0; j < room->mesh.all_faces.count; j++) {\n            M_AddRoomFace(builder, &room->mesh.all_faces.data[j], room);\n        }\n\n        int32_t stack = 0;\n        XYZ_16 prev_pos = { -1, -1, -1 };\n        for (int32_t j = 0; j < room->mesh.sprites.count; j++) {\n            const ROOM_SPRITE *const sprite = &room->mesh.sprites.data[j];\n            const ROOM_VERTEX *const vert =\n                &room->mesh.vertices[sprite->vertex];\n            if (vert->pos.x == prev_pos.x && vert->pos.z == prev_pos.z) {\n                stack++;\n            } else {\n                stack = 0;\n            }\n            MeshBuilder_AddRoomSprite(\n                builder, sprite, room, stack * -0.005f, VERT_USE_DYNAMIC_LIGHT);\n            prev_pos = vert->pos;\n        }\n\n        OUTPUT_MESH *const mesh = MeshBuilder_Seal(builder);\n        if (mesh != nullptr) {\n            MeshBatcher_AddMesh(p->batcher, mesh);\n        }\n\n        p->meshes[i] = mesh;\n    }\n    MeshBuilder_Destroy(builder);\n}\n\nstatic void M_FreeMeshes(M_PRIV *const p)\n{\n    if (p->meshes != nullptr) {\n        for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) {\n            MeshBatcher_RemoveMesh(p->batcher, p->meshes[i]);\n            if (p->meshes[i] != nullptr) {\n                Output_Mesh_Destroy(p->meshes[i]);\n            }\n        }\n        Memory_FreePointer(&p->meshes);\n    }\n}\n\nvoid OutputSource_Rooms_Init(MESH_BATCHER *const batcher)\n{\n    M_PRIV *const p = &m_Priv;\n    p->batcher = batcher;\n}\n\nvoid OutputSource_Rooms_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeMeshes(p);\n}\n\nvoid OutputSource_Rooms_ObserveLevelLoad(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeMeshes(p);\n    M_PrepareMeshes(p);\n}\n\nvoid OutputSource_Rooms_ObserveLevelUnload(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeMeshes(p);\n}\n\nvoid OutputSource_Rooms_ObserveRoomFlip(const ROOM *const room)\n{\n    if (room->flip_status == RFS_UNFLIPPED && room->flipped_room != NO_ROOM) {\n        const int16_t room_1 = Room_GetNumber(room);\n        const int16_t room_2 = room->flipped_room;\n        SWAP(m_Priv.meshes[room_1], m_Priv.meshes[room_2]);\n    }\n}\n\nvoid OutputSource_Rooms_StageRoom(const ROOM *const room)\n{\n    M_PRIV *const p = &m_Priv;\n    OUTPUT_MESH *const mesh = p->meshes[Room_GetNumber(room)];\n    const OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room);\n    const MESH_INSTANCE inst = {\n        .mesh = mesh,\n        .cwmatrix = *g_MatrixPtr,\n        .wmatrix = *g_WMatrixPtr,\n        .tint = Output_GetTint(),\n        .wibble = Output_GetWibbleEffect(),\n        .water_effect = M_GetWaterEffect(room),\n        .enable_scissor = true,\n        .scissor = {\n            .x = bind->bound_left,\n            .y = bind->bound_bottom,\n            .width = bind->bound_right - bind->bound_left,\n            .height = bind->bound_bottom - bind->bound_top,\n        },\n        .room = Output_GetCurrentRoom(),\n    };\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE);\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_TRANSPARENT);\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_BLEND_ADD);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/rooms.h",
    "content": "#pragma once\n\n#include <trx/game/output/mesh_batcher/batcher.h>\n#include <trx/game/rooms/types.h>\n\nvoid OutputSource_Rooms_Init(MESH_BATCHER *batcher);\nvoid OutputSource_Rooms_Shutdown(void);\nvoid OutputSource_Rooms_ObserveLevelLoad(void);\nvoid OutputSource_Rooms_ObserveLevelUnload(void);\nvoid OutputSource_Rooms_ObserveRoomFlip(const ROOM *room);\n\nvoid OutputSource_Rooms_StageRoom(const ROOM *room);\n"
  },
  {
    "path": "src/trx/game/output/sources/rooms_debug.c",
    "content": "#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/output.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/sources/rooms.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/output/vertex_range.h>\n#include <trx/game/random.h>\n#include <trx/gl/utils.h>\n\ntypedef struct {\n    XYZW_F pos;\n    RGBA_8888 color;\n} M_VERTEX;\n\ntypedef struct {\n    const ROOM *room;\n    MATRIX matrix;\n} M_INSTANCE;\n\ntypedef struct {\n    OUTPUT_VERTEX_RANGE triggers;\n    OUTPUT_VERTEX_RANGE portals;\n} M_ROOM_MESH;\n\ntypedef struct {\n    SCENE_SOURCE source;\n    OUTPUT_MESH_SHADER *shader;\n    VECTOR *vertices;\n    size_t mesh_count;\n    M_ROOM_MESH *meshes;\n    VECTOR *scheduled;\n    GLuint vao;\n    GLuint vbo;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic int32_t M_GetTriggerTriangleIndices(\n    const SECTOR *const sector, int32_t indices[6])\n{\n    int32_t skip = -1;\n\n    SPLIT_TYPE split = sector->floor.split.type;\n    if (!sector->floor.is_split) {\n        split = SPLIT_NONE;\n    }\n\n    switch (split) {\n    case SPLIT_NONE:\n    case SPLIT_NESW_SOLID:\n        for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) {\n            indices[i] = OUTPUT_QUAD_TO_FAN(i);\n        }\n        return OUTPUT_QUAD_VERTICES;\n    case SPLIT_NWSE_SOLID:\n        for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) {\n            indices[i] = OUTPUT_QUAD_TO_FAN_BACK(i);\n        }\n        return OUTPUT_QUAD_VERTICES;\n    case SPLIT_NWSE_PORTAL_SW:\n        skip = 0;\n        break;\n    case SPLIT_NWSE_PORTAL_NE:\n        skip = 2;\n        break;\n    case SPLIT_NESW_PORTAL_SE:\n        skip = 1;\n        break;\n    case SPLIT_NESW_PORTAL_NW:\n        skip = 3;\n        break;\n    default:\n        return 0;\n    }\n\n    const int32_t clockwise[4] = { 0, 3, 2, 1 };\n    int32_t count = 0;\n    for (int32_t i = 0; i < 4; i++) {\n        if (clockwise[i] != skip) {\n            indices[count++] = clockwise[i];\n        }\n    }\n    return count;\n}\n\nstatic void M_PrepareRoomTriggers(\n    const M_PRIV *const p, M_ROOM_MESH *const mesh, const ROOM *const room)\n{\n    mesh->triggers.vertex_start = p->vertices->count;\n    const RGBA_8888 color = { .r = 255, .g = 0, .b = 255, .a = 128 };\n    const XZ_16 offsets[4] = { { 0, 0 }, { 0, 1 }, { 1, 1 }, { 1, 0 } };\n\n    int32_t output_indices[OUTPUT_QUAD_VERTICES];\n    for (int32_t z = 0; z < room->size.z; z++) {\n        for (int32_t x = 0; x < room->size.x; x++) {\n            const SECTOR *sector = Room_GetUnitSector(room, x, z);\n            if (sector->trigger == nullptr) {\n                continue;\n            }\n\n            const int32_t vertex_count =\n                M_GetTriggerTriangleIndices(sector, output_indices);\n\n            for (int32_t i = 0; i < vertex_count; i++) {\n                int32_t j = output_indices[i];\n                XYZ_32 vertex_pos = {\n                    .x = (x + offsets[j].x) * WALL_L,\n                    .z = (z + offsets[j].z) * WALL_L,\n                };\n                XYZ_32 world_pos = {\n                    .x = room->pos.x + x * WALL_L + offsets[j].x * (WALL_L - 1),\n                    .z = room->pos.z + z * WALL_L + offsets[j].z * (WALL_L - 1),\n                    .y = room->pos.y,\n                };\n\n                const int32_t height = Room_GetFloorHeightForSector(\n                    sector, world_pos.x, world_pos.z, true);\n                vertex_pos.y = height + (Output_GetWaterEffect() ? -16 : -2);\n\n                M_VERTEX vertex = {\n                    .pos = {\n                        .x = vertex_pos.x,\n                        .y = vertex_pos.y,\n                        .z = vertex_pos.z,\n                        .w = 0.0f,\n                    },\n                    .color = color,\n                };\n                Vector_Add(p->vertices, &vertex);\n            }\n        }\n    }\n\n    mesh->triggers.vertex_count =\n        p->vertices->count - mesh->triggers.vertex_start;\n}\n\nstatic void M_PrepareRoomPortals(\n    const M_PRIV *const p, M_ROOM_MESH *const mesh, const ROOM *const room)\n{\n    mesh->portals.vertex_start = p->vertices->count;\n    const RGBA_8888 color = { 0, 0, 255, 255 };\n    if (room->portals == nullptr) {\n        mesh->portals.vertex_count = 0;\n        return;\n    }\n    for (int32_t i = 0; i < room->portals->count; i++) {\n        const XYZ_16 *const portal = room->portals->portal[i].vertex;\n        const XYZW_F positions[4] = {\n            { portal[0].x, portal[0].y, portal[0].z, 0.0f },\n            { portal[1].x, portal[1].y, portal[1].z, 0.0f },\n            { portal[2].x, portal[2].y, portal[2].z, 0.0f },\n            { portal[3].x, portal[3].y, portal[3].z, 0.0f },\n        };\n        const int32_t indices[8] = { 0, 1, 1, 2, 2, 3, 3, 0 };\n        for (int32_t j = 0; j < 8; j++) {\n            const M_VERTEX vertex = {\n                .pos = positions[indices[j]],\n                .color = color,\n            };\n            Vector_Add(p->vertices, &vertex);\n        }\n    }\n    mesh->portals.vertex_count =\n        p->vertices->count - mesh->portals.vertex_start;\n}\n\nstatic void M_FreeRoom(M_ROOM_MESH *const mesh)\n{\n}\n\nstatic void M_PrepareBuffers(M_PRIV *const p)\n{\n    p->mesh_count = Room_GetCount();\n    p->meshes = Memory_Alloc(sizeof(M_ROOM_MESH) * p->mesh_count);\n    p->vertices = Vector_Create(sizeof(M_VERTEX));\n\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        M_PrepareRoomTriggers(p, &p->meshes[i], Room_Get(i));\n        M_PrepareRoomPortals(p, &p->meshes[i], Room_Get(i));\n    }\n\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX),\n        Vector_GetData(p->vertices), GL_STATIC_DRAW);\n}\n\nstatic void M_FreeBuffers(M_PRIV *const p)\n{\n    if (p->meshes != nullptr) {\n        for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) {\n            M_FreeRoom(&p->meshes[i]);\n        }\n        Memory_FreePointer(&p->meshes);\n    }\n    if (p->vertices != nullptr) {\n        Vector_Free(p->vertices);\n        p->vertices = nullptr;\n    }\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->scheduled);\n}\n\nstatic void M_RenderPass(\n    const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass != SCENE_PASS_TRANSPARENT) {\n        return;\n    }\n\n    Output_MeshShader_UploadTint(p->shader, COLOR_RGB_F_WHITE);\n\n    glBindVertexArray(p->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n    glVertexAttrib4f(OUTPUT_MESH_ATTR_NORMAL, 0.0f, 0.0f, 0.0f, 0.0f);\n    glVertexAttrib3f(OUTPUT_MESH_ATTR_UVW, 0.0f, 0.0f, 0.0f);\n    glVertexAttrib4f(OUTPUT_MESH_ATTR_TEXTURE_SIZE, 0.0f, 0.0f, 1.0f, 1.0f);\n    glVertexAttrib2f(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO, 1.0f, 1.0f);\n    glVertexAttribI1ui(\n        OUTPUT_MESH_ATTR_FLAGS,\n        VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE);\n    glVertexAttrib1f(OUTPUT_MESH_ATTR_SHADE, SHADE_NEUTRAL);\n\n    for (int32_t i = 0; i < p->scheduled->count; i++) {\n        const M_INSTANCE *const instance = Vector_Get(p->scheduled, i);\n        const M_ROOM_MESH *const mesh =\n            &p->meshes[Room_GetNumber(instance->room)];\n        Output_MeshShader_UploadModelMatrix(p->shader, &instance->matrix);\n        if (g_Config.debug.enable_debug_triggers) {\n            glDrawArrays(\n                GL_TRIANGLES, mesh->triggers.vertex_start,\n                mesh->triggers.vertex_count);\n        }\n\n        if (g_Config.debug.enable_debug_portals) {\n            GLint bound_polygon_mode[2];\n            glDisable(GL_DEPTH_TEST);\n            glGetIntegerv(GL_POLYGON_MODE, &bound_polygon_mode[0]);\n            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);\n            glDrawArrays(\n                GL_LINES, mesh->portals.vertex_start,\n                mesh->portals.vertex_count);\n            glPolygonMode(GL_FRONT_AND_BACK, bound_polygon_mode[0]);\n            glEnable(GL_DEPTH_TEST);\n        }\n    }\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    const M_PRIV *const p = &m_Priv;\n    return pass == SCENE_PASS_TRANSPARENT && p->scheduled->count > 0;\n}\n\nvoid OutputSource_RoomsDebug_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->shader = Output_GetMeshShader();\n    p->scheduled = Vector_Create(sizeof(M_INSTANCE));\n    p->source.render_begin = M_RenderBegin;\n    p->source.render_pass = M_RenderPass;\n    p->source.is_dirty = M_IsDirty;\n    SceneCompositor_AddSource(&p->source);\n\n    glGenVertexArrays(1, &p->vao);\n    glBindVertexArray(p->vao);\n\n    glGenBuffers(1, &p->vbo);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_POS);\n    glEnableVertexAttribArray(OUTPUT_MESH_ATTR_COLOR);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_NORMAL);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_UVW);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TEXTURE_SIZE);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_TRAPEZOID_RATIO);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_FLAGS);\n    glDisableVertexAttribArray(OUTPUT_MESH_ATTR_SHADE);\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, pos));\n    glVertexAttribPointer(\n        OUTPUT_MESH_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, color));\n}\n\nvoid OutputSource_RoomsDebug_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->scheduled != nullptr) {\n        Vector_Free(p->scheduled);\n        p->scheduled = nullptr;\n    }\n    M_FreeBuffers(p);\n    if (p->vao != 0) {\n        glDeleteVertexArrays(1, &p->vao);\n        p->vao = 0;\n    }\n    if (p->vbo != 0) {\n        glDeleteBuffers(1, &p->vbo);\n        p->vbo = 0;\n    }\n}\n\nvoid OutputSource_RoomsDebug_ObserveLevelLoad(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_PrepareBuffers(p);\n}\n\nvoid OutputSource_RoomsDebug_ObserveLevelUnload(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_FreeBuffers(p);\n}\n\nvoid OutputSource_RoomsDebug_ObserveRoomFlip(const ROOM *const room)\n{\n    M_PRIV *const p = &m_Priv;\n    if (room->flip_status == RFS_UNFLIPPED && room->flipped_room != NO_ROOM) {\n        const int16_t room_1 = Room_GetNumber(room);\n        const int16_t room_2 = room->flipped_room;\n        SWAP(p->meshes[room_1], p->meshes[room_2]);\n    }\n}\n\nvoid OutputSource_RoomsDebug_StageRoom(const ROOM *const room)\n{\n    if (!g_Config.debug.enable_debug_triggers\n        && !g_Config.debug.enable_debug_portals) {\n        return;\n    }\n\n    M_PRIV *const p = &m_Priv;\n    Vector_Add(\n        p->scheduled, &(M_INSTANCE) { .room = room, .matrix = *g_WMatrixPtr });\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/rooms_debug.h",
    "content": "#pragma once\n\n#include <trx/game/rooms/types.h>\n\nvoid OutputSource_RoomsDebug_Init(void);\nvoid OutputSource_RoomsDebug_Shutdown(void);\nvoid OutputSource_RoomsDebug_ObserveLevelLoad(void);\nvoid OutputSource_RoomsDebug_ObserveLevelUnload(void);\nvoid OutputSource_RoomsDebug_ObserveRoomFlip(const ROOM *room);\n\nvoid OutputSource_RoomsDebug_StageRoom(const ROOM *room);\n"
  },
  {
    "path": "src/trx/game/output/sources/shadows.c",
    "content": "#include <trx/game/output/sources/shadows.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/game/output.h>\n#include <trx/game/output/mesh_batcher/mesh_builder.h>\n\ntypedef struct {\n    MESH_BATCHER *batcher;\n    OUTPUT_MESH *mesh_low;\n    OUTPUT_MESH *mesh_high;\n} M_PRIV;\n\nstatic M_PRIV m_Priv;\n\nstatic OUTPUT_MESH *M_GenerateShadow(\n    MESH_BUILDER *const builder, const int32_t fidelity)\n{\n    const int32_t y = -5;\n    const RGBA_8888 color = { 0, 0, 0, 128 };\n    const OUTPUT_MESH_VERTEX center = {\n        .pos = { 0.0f, (float)y, 0.0f, 0.0f },\n        .normal = { 0.0f, 0.0f, 0.0f },\n        .flags = VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_WIBBLE,\n        .uvw_idx = -1,\n        .trapezoid_ratio = { 1.0f, 1.0f },\n        .shade = SHADE_NEUTRAL,\n        .color = color,\n    };\n\n    MeshBuilder_AddVertex(builder, &center);\n    for (int32_t i = 0; i <= fidelity; i++) {\n        const int16_t angle = ((i * DEG_360) + DEG_180) / fidelity;\n        const int32_t size = WALL_L / 2;\n        const int32_t x = (Math_Sin(angle) * size) >> W2V_SHIFT;\n        const int32_t z = (Math_Cos(angle) * size) >> W2V_SHIFT;\n        OUTPUT_MESH_VERTEX edge = center;\n        edge.pos.x = x;\n        edge.pos.z = z;\n        MeshBuilder_AddVertex(builder, &edge);\n    }\n    MeshBuilder_AddFan(builder, SCENE_PASS_TRANSPARENT, false);\n    return MeshBuilder_Seal(builder);\n}\n\nvoid OutputSource_Shadows_Init(MESH_BATCHER *const batcher)\n{\n    M_PRIV *const p = &m_Priv;\n    p->batcher = batcher;\n\n    // Build low- and high-fidelity circular shadow meshes.\n    MESH_BUILDER *const builder = MeshBuilder_Create();\n    p->mesh_low = M_GenerateShadow(builder, 8);\n    p->mesh_high = M_GenerateShadow(builder, 32);\n    MeshBuilder_Destroy(builder);\n\n    MeshBatcher_AddMesh(p->batcher, p->mesh_low);\n    MeshBatcher_AddMesh(p->batcher, p->mesh_high);\n}\n\nvoid OutputSource_Shadows_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->mesh_low != nullptr) {\n        if (p->batcher != nullptr) {\n            MeshBatcher_RemoveMesh(p->batcher, p->mesh_low);\n        }\n        Output_Mesh_Destroy(p->mesh_low);\n        p->mesh_low = nullptr;\n    }\n    if (p->mesh_high != nullptr) {\n        if (p->batcher != nullptr) {\n            MeshBatcher_RemoveMesh(p->batcher, p->mesh_high);\n        }\n        Output_Mesh_Destroy(p->mesh_high);\n        p->mesh_high = nullptr;\n    }\n    p->batcher = nullptr;\n}\n\nvoid OutputSource_Shadows_StageShadow(void)\n{\n    M_PRIV *const p = &m_Priv;\n    OUTPUT_MESH *const mesh = g_Config.visuals.shadow_type == SHADOW_TYPE_CIRCLE\n        ? p->mesh_high\n        : p->mesh_low;\n    const MESH_INSTANCE inst = {\n        .mesh = mesh,\n        .cwmatrix = *g_MatrixPtr,\n        .wmatrix = *g_WMatrixPtr,\n        .tint = { 1.0f, 1.0f, 1.0f },\n        .room = Output_GetCurrentRoom(),\n    };\n    // XXX: Mesh batcher currently collects the transparent faces for the\n    // transparent pass in the opaque pass, so the shadow, even though\n    // transparent, needs to be staged in the opaque pass to work.\n    MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE);\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/shadows.h",
    "content": "#pragma once\n\n#include <trx/game/output/mesh_batcher/batcher.h>\n\nvoid OutputSource_Shadows_Init(MESH_BATCHER *batcher);\nvoid OutputSource_Shadows_Shutdown(void);\n\nvoid OutputSource_Shadows_StageShadow(void);\n"
  },
  {
    "path": "src/trx/game/output/sources/sprites.c",
    "content": "#include <trx/game/output/sources/sprites.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/output/const.h>\n#include <trx/game/output/mesh_batcher/mesh_builder.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/shaders/mesh.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/textures.h>\n\n#include <string.h>\n\ntypedef struct {\n    SCENE_SOURCE source;\n    MESH_BATCHER *batcher;\n    OUTPUT_MESH **meshes;\n    OUTPUT_MESH **meshes_blend_add;\n    size_t mesh_count;\n\n    MATRIX last_matrix;\n    int32_t stack;\n} M_PRIV;\n\nstatic M_PRIV m_Priv;\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const src)\n{\n    M_PRIV *const p = &m_Priv;\n    memset(&p->last_matrix, 0, sizeof(MATRIX));\n    p->stack = 0;\n}\n\nstatic void M_AddSpriteMesh(\n    MESH_BUILDER *const builder, const int32_t texture_idx,\n    const SCENE_PASS pass, const uint16_t extra_flags)\n{\n    const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(texture_idx);\n    const struct {\n        float x, y;\n    } normal[4] = {\n        { .x = sprite->x0, .y = sprite->y0 },\n        { .x = sprite->x1, .y = sprite->y0 },\n        { .x = sprite->x1, .y = sprite->y1 },\n        { .x = sprite->x0, .y = sprite->y1 },\n    };\n    for (int32_t j = 0; j < 4; j++) {\n        const OUTPUT_MESH_VERTEX vertex = {\n            .pos = { .x = 0.0f, .y = 0.0f, .z = 0.0f, .w = 0.0f },\n            .normal = { .x = normal[j].x, .y = normal[j].y, .z = 0.0f },\n            .flags = Output_Textures_GetSpriteTextureFlags(texture_idx)\n                | extra_flags,\n            .color = { 255, 255, 255, 255 },\n            .uvw_idx = Output_Textures_GetSpriteUVWIndex(texture_idx, j),\n            .shade = 0,\n            .trapezoid_ratio = { 1.0f, 1.0f },\n        };\n        MeshBuilder_AddVertex(builder, &vertex);\n    }\n    MeshBuilder_AddFan(builder, pass, false);\n}\n\nstatic void M_PrepareMeshes(M_PRIV *const p)\n{\n    p->mesh_count = Output_GetSpriteTextureCount();\n    p->meshes = Memory_Alloc(sizeof(*p->meshes) * p->mesh_count);\n    p->meshes_blend_add =\n        Memory_Alloc(sizeof(*p->meshes_blend_add) * p->mesh_count);\n    MESH_BUILDER *const builder = MeshBuilder_Create();\n    for (int32_t i = 0; i < (int32_t)p->mesh_count; i++) {\n        M_AddSpriteMesh(builder, i, SCENE_PASS_TRANSPARENT, VERT_USE_OWN_LIGHT);\n        OUTPUT_MESH *const mesh_transparent = MeshBuilder_Seal(builder);\n        MeshBatcher_AddMesh(p->batcher, mesh_transparent);\n        p->meshes[i] = mesh_transparent;\n\n        M_AddSpriteMesh(builder, i, SCENE_PASS_BLEND_ADD, VERT_USE_OWN_LIGHT);\n        OUTPUT_MESH *const mesh_blend_add = MeshBuilder_Seal(builder);\n        MeshBatcher_AddMesh(p->batcher, mesh_blend_add);\n        p->meshes_blend_add[i] = mesh_blend_add;\n    }\n    MeshBuilder_Destroy(builder);\n}\n\nstatic void M_FreeMeshes(M_PRIV *const p)\n{\n    if (p->meshes != nullptr) {\n        for (size_t i = 0; i < p->mesh_count; i++) {\n            MeshBatcher_RemoveMesh(p->batcher, p->meshes[i]);\n            Output_Mesh_Destroy(p->meshes[i]);\n            MeshBatcher_RemoveMesh(p->batcher, p->meshes_blend_add[i]);\n            Output_Mesh_Destroy(p->meshes_blend_add[i]);\n        }\n        Memory_FreePointer(&p->meshes);\n        Memory_FreePointer(&p->meshes_blend_add);\n    }\n}\n\nvoid OutputSource_Sprites_Init(MESH_BATCHER *batcher)\n{\n    m_Priv.batcher = batcher;\n    m_Priv.source.render_begin = M_RenderBegin;\n    SceneCompositor_AddSource(&m_Priv.source);\n}\n\nvoid OutputSource_Sprites_Shutdown(void)\n{\n    M_FreeMeshes(&m_Priv);\n}\n\nvoid OutputSource_Sprites_ObserveLevelLoad(void)\n{\n    M_FreeMeshes(&m_Priv);\n    M_PrepareMeshes(&m_Priv);\n}\n\nvoid OutputSource_Sprites_ObserveLevelUnload(void)\n{\n    M_FreeMeshes(&m_Priv);\n}\n\nvoid OutputSource_Sprites_Stage(\n    const int32_t sprite_idx, const int16_t shade, const RGB_F tint,\n    const DRAW_TYPE draw_type)\n{\n    M_PRIV *const p = &m_Priv;\n    OUTPUT_MESH *mesh = p->meshes[sprite_idx];\n\n    if (memcmp(&p->last_matrix, g_WMatrixPtr, sizeof(MATRIX)) == 0) {\n        p->stack++;\n    } else {\n        p->stack = 0;\n    }\n    p->last_matrix = *g_WMatrixPtr;\n\n    float tr3_mul = 2.0f - (shade / (float)SHADE_NEUTRAL);\n    CLAMP(tr3_mul, 0.0f, 1.0f);\n    const OUTPUT_LIGHT_INFO light_info = {\n        .ls_adder = shade,\n        .ls_divider = 0,\n        .ls_vector_view = {},\n        .tr3_ambient = { tr3_mul, tr3_mul, tr3_mul },\n        .tr3_light_color = {},\n        .tr3_light_dir_view = {},\n    };\n\n    MESH_INSTANCE inst = {\n        .mesh = mesh,\n        .cwmatrix = *g_MatrixPtr,\n        .wmatrix = *g_WMatrixPtr,\n        .depth_adjust = p->stack * -0.005f,\n        .tint = tint,\n        .wibble = false,\n        .water_effect = 0,\n        .room = Output_GetCurrentRoom(),\n        .light_info = light_info,\n    };\n\n    if (draw_type == DRAW_BLEND_ADD || draw_type == DRAW_REFLECTIVE_BLEND_ADD) {\n        inst.mesh = p->meshes_blend_add[sprite_idx];\n        MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_BLEND_ADD);\n    } else {\n        MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_OPAQUE);\n        MeshBatcher_Stage(p->batcher, &inst, SCENE_PASS_TRANSPARENT);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/sprites.h",
    "content": "#pragma once\n\n#include <trx/game/output/mesh_batcher/batcher.h>\n#include <trx/game/output/scene_source.h>\n#include <trx/game/rooms/types.h>\n\nvoid OutputSource_Sprites_Init(MESH_BATCHER *batcher);\nvoid OutputSource_Sprites_Shutdown(void);\nvoid OutputSource_Sprites_ObserveLevelLoad(void);\nvoid OutputSource_Sprites_ObserveLevelUnload(void);\n\nvoid OutputSource_Sprites_Stage(\n    int32_t sprite_idx, int16_t shade, RGB_F tint, DRAW_TYPE draw_type);\n"
  },
  {
    "path": "src/trx/game/output/sources/ui.c",
    "content": "#include <trx/game/output/sources/ui.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/sources/objects.h>\n#include <trx/game/viewport.h>\n#include <trx/gl/utils.h>\n#include <trx/version.h>\n\n// GL attribute mapping in the shader\ntypedef enum {\n    // clang-format off\n    M_ATTR_POS          = 0,\n    M_ATTR_UVW          = 1,\n    M_ATTR_TEXTURE_SIZE = 2,\n    M_ATTR_FLAGS        = 3,\n    M_ATTR_COLOR        = 4,\n    // clang-format on\n} M_VERTEX_ATTR;\n\ntypedef struct {\n    XYZW_F pos;\n    OUTPUT_UVW uvw;\n    OUTPUT_TEXTURE_SIZE texture_size;\n    OUTPUT_USHORT flags;\n    RGBA_F color;\n} M_VERTEX;\n\ntypedef struct {\n    SCENE_SOURCE source;\n    const SCENE_SOURCE *objects_source;\n    VECTOR *scheduled_pickups;\n    VECTOR *vertices;\n    GLuint vao;\n    GLuint vbo;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic RGBA_F M_ToRGBA_F(const RGBA_8888 color)\n{\n    return (RGBA_F) {\n        .r = color.r / 255.0f,\n        .g = color.g / 255.0f,\n        .b = color.b / 255.0f,\n        .a = color.a / 255.0f,\n    };\n}\n\nVIEWPORT_RECT OutputSource_UI_GetPickupRect(\n    const OUTPUT_UI_PICKUP *const pickup)\n{\n    const VIEWPORT_RECT viewport = Viewport_GetRect(VIEWPORT_UI);\n\n    const float pickup_h = viewport.h * g_Config.ui.pickup_scale / 6;\n    const float pickup_w = pickup_h * 5 / 4;\n    const float window_padding_y = viewport.h / 16;\n    const float window_padding_x = window_padding_y * 4 / 3;\n    const float grid_padding_x = pickup_w / 8;\n    const float grid_padding_y = pickup_h / 8;\n\n    const float src_x = viewport.w + window_padding_x + pickup_w;\n    const float src_y = viewport.h - window_padding_y - pickup_h / 2;\n\n    const float dst_x = viewport.w - window_padding_x - pickup_w / 2\n        - (pickup_w + grid_padding_x) * pickup->grid_x;\n    const float dst_y = viewport.h - window_padding_y - pickup_h / 2\n        - (pickup_h + grid_padding_y) * pickup->grid_y;\n\n    const float x = src_x + (dst_x - src_x) * pickup->ease;\n    const float y = src_y + (dst_y - src_y) * pickup->ease;\n    return (VIEWPORT_RECT) {\n        .x = x - pickup_w / 2,\n        .y = y - pickup_h / 2,\n        .w = pickup_w,\n        .h = pickup_h,\n    };\n}\n\nstatic float M_Get3DPickupScale(\n    const VIEWPORT_RECT pickup_rect, const ANIM_FRAME *const frame)\n{\n    const XYZ_F obj_size = {\n        .x = MAX(1, frame->bounds.max.x - frame->bounds.min.x),\n        .y = MAX(1, frame->bounds.max.y - frame->bounds.min.y),\n        .z = MAX(1, frame->bounds.max.z - frame->bounds.min.z),\n    };\n\n    // Reference scale that seems to works OK based on the following data:\n    // pickup_rect: 480×360 (changes with window resizes)\n    // key:         81  182 11\n    // scion:       184 190 54\n    // pistols:     215 57  146\n    // shotgun:     365 123 147\n    const float ref_scale = pickup_rect.w / 200.0f;\n\n    // A scale factor to fit the mesh within pickup_rect,\n    // ensuring it touches either side and is entirely contained.\n    // clang-format off\n    const float perfect_fit_scale = MIN3(\n        pickup_rect.w / obj_size.x,\n        pickup_rect.h / obj_size.y,\n        pickup_rect.w / obj_size.z);\n    // clang-format on\n\n    // Some items are too big or too small – try to find a middle ground.\n    return (ref_scale + perfect_fit_scale) / 2.0f;\n}\n\nstatic XYZ_32 M_VectorViewFromWorld(\n    const MATRIX *const view_matrix, const XYZ_32 vec_world)\n{\n    return (XYZ_32) {\n        .x = (view_matrix->_00 * vec_world.x + view_matrix->_01 * vec_world.y\n              + view_matrix->_02 * vec_world.z)\n            >> W2V_SHIFT,\n        .y = (view_matrix->_10 * vec_world.x + view_matrix->_11 * vec_world.y\n              + view_matrix->_12 * vec_world.z)\n            >> W2V_SHIFT,\n        .z = (view_matrix->_20 * vec_world.x + view_matrix->_21 * vec_world.y\n              + view_matrix->_22 * vec_world.z)\n            >> W2V_SHIFT,\n    };\n}\n\nstatic void M_Draw3DPickups(const M_PRIV *const p)\n{\n    SceneCompositor_SetSamplerFilter(g_Config.rendering.texture_filter);\n    Output_MeshShader_Bind(Output_GetMeshShader());\n\n    for (int32_t i = 0; i < p->scheduled_pickups->count; i++) {\n        if (p->objects_source->render_begin != nullptr) {\n            p->objects_source->render_begin(p->objects_source);\n        }\n\n        const OUTPUT_UI_PICKUP *const pickup =\n            Vector_Get(p->scheduled_pickups, i);\n        const ANIM_FRAME *const frame =\n            Object_GetAnim(pickup->object, 0)->frame_ptr;\n\n        const VIEWPORT_RECT pickup_rect = OutputSource_UI_GetPickupRect(pickup);\n        const XYZ_32 origin = {\n            .x = pickup_rect.x + pickup_rect.w / 2,\n            .y = pickup_rect.y + pickup_rect.h / 2,\n            .z = (Output_GetNearZ_UI() + Output_GetFarZ_UI()) / 2,\n        };\n\n        const float scale = M_Get3DPickupScale(pickup_rect, frame);\n\n        // Lighting routines needs a W2V matrix to work; set up something for\n        // it.\n        MATRIX pickup_view_matrix = {};\n        XYZ_32 camera = g_TRVersion >= 3 ?\n             (XYZ_32) {\n                .x = origin.x,\n                .y = origin.y - WALL_L,\n                .z = origin.z,\n            } :\n             (XYZ_32) {\n                .x = origin.x,\n                .y = origin.y,\n                .z = origin.z - WALL_L,\n            };\n        Matrix_LookAt(\n            camera.x, camera.y, camera.z, origin.x, origin.y, origin.z, 0);\n        pickup_view_matrix = g_ViewMatrix;\n\n        Matrix_PushUnit();\n        Matrix_TranslateSet32(origin);\n        Matrix_RotX(DEG_1 * 15);\n        Matrix_RotY(-DEG_180);\n        Matrix_RotY(pickup->rot_y);\n        Matrix_Scale((1 << W2V_SHIFT) * scale);\n\n        // Set up lighting for the pickup mesh.\n        if (g_TRVersion >= 3) {\n            // Port of OG TR3's SetPickupLight().\n            // ambient = (64, 64, 64)\n            // sun     = (3072, 1680, 640)\n            // spot    = (1024, 1024, 1024)\n            // dynamic = (640, 2432, 4080)\n            const float ambient_u8 = 64.0f / 255.0f;\n            const RGB_F ambient = { ambient_u8, ambient_u8, ambient_u8 };\n            const RGB_F colors[3] = {\n                {\n                    .r = 3072.0f / 4096.0f,\n                    .g = 1680.0f / 4096.0f,\n                    .b = 640.0f / 4096.0f,\n                },\n                {\n                    .r = 1024.0f / 4096.0f,\n                    .g = 1024.0f / 4096.0f,\n                    .b = 1024.0f / 4096.0f,\n                },\n                {\n                    .r = 640.0f / 4096.0f,\n                    .g = 2432.0f / 4096.0f,\n                    .b = 4080.0f / 4096.0f,\n                },\n            };\n\n            const XYZ_32 dirs_view[3] = {\n                M_VectorViewFromWorld(\n                    &pickup_view_matrix,\n                    (XYZ_32) { .x = 0x2000, .y = -0x2000, .z = 0x1800 }),\n                M_VectorViewFromWorld(\n                    &pickup_view_matrix,\n                    (XYZ_32) { .x = -0x2000, .y = -0x4000, .z = 0x3000 }),\n                M_VectorViewFromWorld(\n                    &pickup_view_matrix,\n                    (XYZ_32) { .x = 0, .y = 0x2000, .z = 0x3000 }),\n            };\n\n            Output_SetTR3Light(ambient, colors, dirs_view);\n        } else {\n            Output_SetLightDivider((1 << W2V_SHIFT) * 2);\n            Output_SetLightAdder(SHADE_LOW);\n            Output_RotateLight(DEG_1 * -30, DEG_1 * 45);\n        }\n\n        Matrix_TranslateRel16(frame->offset);\n        Matrix_TranslateRel32((XYZ_32) {\n            .x = -(frame->bounds.min.x + frame->bounds.max.x) / 2,\n            .y = -(frame->bounds.min.y + frame->bounds.max.y) / 2,\n            .z = -(frame->bounds.min.z + frame->bounds.max.z) / 2,\n        });\n        Matrix_Rot16(frame->mesh_rots[0]);\n        Object_DrawStaticObject(pickup->object, frame);\n        Matrix_Pop();\n\n        // Immediately flush scheduled object, so that it gets rendered\n        // in the target viewport\n        p->objects_source->render_pass(p->objects_source, SCENE_PASS_OPAQUE);\n        p->objects_source->render_pass(\n            p->objects_source, SCENE_PASS_TRANSPARENT);\n        glBlendFunc(GL_ONE, GL_ONE);\n        p->objects_source->render_pass(p->objects_source, SCENE_PASS_BLEND_ADD);\n        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n        if (p->objects_source->render_end != nullptr) {\n            p->objects_source->render_end(p->objects_source);\n        }\n    }\n\n    SceneCompositor_SetSamplerFilter(g_Config.rendering.ui_filter);\n}\n\nstatic void M_DrawVertices(const M_PRIV *const p)\n{\n    glBindVertexArray(p->vao);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n    TRX_GL_TRACK_DATA(\n        glBufferData, GL_ARRAY_BUFFER, p->vertices->count * sizeof(M_VERTEX),\n        Vector_GetData(p->vertices), GL_STATIC_DRAW);\n    glDrawArrays(GL_TRIANGLES, 0, p->vertices->count);\n}\n\nstatic void M_RenderBegin(const SCENE_SOURCE *const source)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->scheduled_pickups);\n    Vector_Clear(p->vertices);\n}\n\nstatic void M_RenderPass(\n    const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    M_PRIV *const p = &m_Priv;\n    if (pass != SCENE_PASS_UI) {\n        return;\n    }\n\n    if (p->scheduled_pickups->count == 0 && p->vertices->count == 0) {\n        return;\n    }\n\n    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);\n    if (p->vertices->count > 0) {\n        M_DrawVertices(p);\n    }\n\n    if (p->scheduled_pickups->count > 0) {\n        glEnable(GL_CULL_FACE);\n        M_Draw3DPickups(p);\n        glDisable(GL_CULL_FACE);\n        Output_UIShader_Bind(Output_GetUIShader());\n    }\n}\n\nstatic bool M_IsDirty(const SCENE_SOURCE *const source, const SCENE_PASS pass)\n{\n    const M_PRIV *const p = &m_Priv;\n    return pass == SCENE_PASS_UI\n        && (p->scheduled_pickups->count > 0 || p->vertices->count > 0);\n}\n\nvoid OutputSource_UI_Init(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->scheduled_pickups = Vector_Create(sizeof(OUTPUT_UI_PICKUP));\n    p->vertices = Vector_CreateAtCapacity(sizeof(M_VERTEX), 500);\n    p->source.render_begin = M_RenderBegin;\n    p->source.render_pass = M_RenderPass;\n    p->source.is_dirty = M_IsDirty;\n    p->objects_source = OutputSource_Objects_GetSource();\n    SceneCompositor_AddSource(&p->source);\n\n    glGenVertexArrays(1, &p->vao);\n    glBindVertexArray(p->vao);\n\n    glGenBuffers(1, &p->vbo);\n    glBindBuffer(GL_ARRAY_BUFFER, p->vbo);\n\n    glEnableVertexAttribArray(M_ATTR_POS);\n    glEnableVertexAttribArray(M_ATTR_UVW);\n    glEnableVertexAttribArray(M_ATTR_COLOR);\n    glEnableVertexAttribArray(M_ATTR_TEXTURE_SIZE);\n    glEnableVertexAttribArray(M_ATTR_FLAGS);\n    glVertexAttribPointer(\n        M_ATTR_POS, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, pos));\n    glVertexAttribPointer(\n        M_ATTR_UVW, 3, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, uvw));\n    glVertexAttribPointer(\n        M_ATTR_COLOR, 4, GL_FLOAT, GL_TRUE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, color));\n    glVertexAttribPointer(\n        M_ATTR_TEXTURE_SIZE, 4, GL_FLOAT, GL_FALSE, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, texture_size));\n    glVertexAttribIPointer(\n        M_ATTR_FLAGS, 1, OUTPUT_USHORT_GL, sizeof(M_VERTEX),\n        (void *)(intptr_t)offsetof(M_VERTEX, flags));\n}\n\nvoid OutputSource_UI_Shutdown(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->scheduled_pickups != nullptr) {\n        Vector_Free(p->scheduled_pickups);\n        p->scheduled_pickups = nullptr;\n    }\n    if (p->vertices != nullptr) {\n        Vector_Free(p->vertices);\n        p->vertices = nullptr;\n    }\n    if (p->vao != 0) {\n        glDeleteVertexArrays(1, &p->vao);\n        p->vao = 0;\n    }\n    if (p->vbo != 0) {\n        glDeleteBuffers(1, &p->vbo);\n        p->vbo = 0;\n    }\n}\n\nvoid OutputSource_UI_StagePickup(const OUTPUT_UI_PICKUP pickup)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Add(p->scheduled_pickups, &pickup);\n}\n\nvoid OutputSource_UI_StageSprite(const OUTPUT_UI_SPRITE sprite)\n{\n    M_PRIV *const p = &m_Priv;\n\n    const SPRITE_TEXTURE *const sprite_tex =\n        Output_GetSpriteTexture(sprite.sprite_idx);\n\n    const float u0 = (sprite_tex->offset & 0xFF) / 256.0f;\n    const float v0 = (sprite_tex->offset >> 8) / 256.0f;\n    const float u1 = u0 + sprite_tex->width / 65536.0f;\n    const float v1 = v0 + sprite_tex->height / 65536.0f;\n\n    M_VERTEX vertices[4];\n    for (int32_t i = 0; i < 4; i++) {\n        vertices[i].pos.z = sprite.z;\n        vertices[i].pos.w = 0.0f;\n        vertices[i].color = sprite.color[i];\n        vertices[i].uvw.w = sprite_tex->tex_page;\n        vertices[i].texture_size.x0 = u0;\n        vertices[i].texture_size.y0 = v0;\n        vertices[i].texture_size.x1 = u1;\n        vertices[i].texture_size.y1 = v1;\n        vertices[i].flags = 0;\n    }\n\n#define L_SET(vtx_idx, x_, y_, u_, v_)                                         \\\n    vertices[vtx_idx].pos.x = x_;                                              \\\n    vertices[vtx_idx].pos.y = y_;                                              \\\n    vertices[vtx_idx].uvw.u = u_;                                              \\\n    vertices[vtx_idx].uvw.v = v_;\n    L_SET(0, sprite.x0, sprite.y0, u0, v0);\n    L_SET(1, sprite.x1, sprite.y0, u1, v0);\n    L_SET(2, sprite.x1, sprite.y1, u1, v1);\n    L_SET(3, sprite.x0, sprite.y1, u0, v1);\n#undef L_SET\n\n    for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) {\n        const int32_t j = OUTPUT_QUAD_TO_FAN(i);\n        Vector_Add(p->vertices, &vertices[j]);\n    }\n}\n\nvoid OutputSource_UI_StageQuad(const OUTPUT_UI_QUAD quad)\n{\n    M_PRIV *const p = &m_Priv;\n\n    M_VERTEX vertices[4];\n    for (int32_t i = 0; i < 4; i++) {\n        vertices[i].pos.z = quad.z;\n        vertices[i].pos.w = 0.0f;\n        vertices[i].flags = VERT_FLAT_SHADED;\n    }\n\n#define L_SET(vtx_idx, x_, y_, color_)                                         \\\n    vertices[vtx_idx].pos.x = x_;                                              \\\n    vertices[vtx_idx].pos.y = y_;                                              \\\n    vertices[vtx_idx].color = M_ToRGBA_F(color_);\n    L_SET(0, quad.x0, quad.y0, quad.tl);\n    L_SET(1, quad.x1, quad.y0, quad.tr);\n    L_SET(2, quad.x1, quad.y1, quad.br);\n    L_SET(3, quad.x0, quad.y1, quad.bl);\n#undef L_SET\n\n    for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) {\n        const int32_t j = OUTPUT_QUAD_TO_FAN(i);\n        Vector_Add(p->vertices, &vertices[j]);\n    }\n}\n\nvoid OutputSource_UI_StagePhotoModeFrame(\n    const VIEWPORT_RECT rect, const RGBA_8888 color, const int32_t thickness)\n{\n    const int32_t t = thickness;\n    if (t <= 0) {\n        return;\n    }\n\n    const int32_t x0 = rect.x;\n    const int32_t y0 = rect.y;\n    const int32_t x1 = rect.x + rect.w;\n    const int32_t y1 = rect.y + rect.h;\n    const int32_t z = Output_GetNearZ_UI();\n\n    OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) {\n        .x0 = x0,\n        .y0 = y0,\n        .x1 = x1,\n        .y1 = y0 + t,\n        .z = z,\n        .tl = color,\n        .tr = color,\n        .bl = color,\n        .br = color,\n    });\n\n    OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) {\n        .x0 = x0,\n        .y0 = y1 - t,\n        .x1 = x1,\n        .y1 = y1,\n        .z = z,\n        .tl = color,\n        .tr = color,\n        .bl = color,\n        .br = color,\n    });\n\n    OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) {\n        .x0 = x0,\n        .y0 = y0 + t,\n        .x1 = x0 + t,\n        .y1 = y1 - t,\n        .z = z,\n        .tl = color,\n        .tr = color,\n        .bl = color,\n        .br = color,\n    });\n\n    OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) {\n        .x0 = x1 - t,\n        .y0 = y0 + t,\n        .x1 = x1,\n        .y1 = y1 - t,\n        .z = z,\n        .tl = color,\n        .tr = color,\n        .bl = color,\n        .br = color,\n    });\n}\n"
  },
  {
    "path": "src/trx/game/output/sources/ui.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/viewport.h>\n\n#define OUTPUT_UI_MAX_PICKUP_ROWS 3\n#define OUTPUT_UI_MAX_PICKUP_COLUMNS 4\n#define OUTPUT_UI_MAX_PICKUPS                                                  \\\n    (OUTPUT_UI_MAX_PICKUP_COLUMNS * OUTPUT_UI_MAX_PICKUP_ROWS)\n\ntypedef struct {\n    const OBJECT *object;\n    int32_t grid_x;\n    int32_t grid_y;\n    int32_t rot_y;\n    float ease;\n} OUTPUT_UI_PICKUP;\n\ntypedef struct {\n    int32_t sprite_idx;\n    int32_t x0, y0;\n    int32_t x1, y1;\n    int32_t z;\n    int16_t shade;\n    RGBA_F color[4];\n} OUTPUT_UI_SPRITE;\n\ntypedef struct {\n    int32_t x0, y0;\n    int32_t x1, y1;\n    int32_t z;\n    RGBA_8888 tl, tr, bl, br;\n} OUTPUT_UI_QUAD;\n\nvoid OutputSource_UI_Init(void);\nvoid OutputSource_UI_Shutdown(void);\n\nvoid OutputSource_UI_StagePickup(OUTPUT_UI_PICKUP pickup);\nvoid OutputSource_UI_StageSprite(OUTPUT_UI_SPRITE sprite);\nvoid OutputSource_UI_StageQuad(OUTPUT_UI_QUAD quad);\nvoid OutputSource_UI_StagePhotoModeFrame(\n    VIEWPORT_RECT rect, RGBA_8888 color, int32_t thickness);\n\nVIEWPORT_RECT OutputSource_UI_GetPickupRect(const OUTPUT_UI_PICKUP *pickup);\n"
  },
  {
    "path": "src/trx/game/output/state.c",
    "content": "#include <trx/game/output/state.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/output/common.h>\n#include <trx/game/output/lights.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/sources/objects.h>\n#include <trx/game/output/sources/rooms.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/uniforms.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\n#include <math.h>\n\nstatic float m_Time = 0.0f;\nstatic float m_TimeInGame = 0.0f;\nstatic bool m_ControlFrame = false;\nstatic int32_t m_AnimatedTexturesOffset = 0;\n\nstatic int32_t m_FogStart = 0;\nstatic int32_t m_FogEnd = 0;\nstatic RGBA_F m_FogColor = {};\nstatic RGB_F m_WaterColor = {};\n\nstatic float m_DepthFactor = 0.0f;\nstatic float m_DepthUnits = 0.0f;\n\nstatic const ROOM *m_CurrentRoom = nullptr;\nstatic int32_t m_LsAdder = 0;\nstatic int32_t m_LsDivider = 0;\nstatic XYZ_32 m_LsVectorView = {};\nstatic RGB_F m_TR3Ambient = { 1.0f, 1.0f, 1.0f };\nstatic RGB_F m_TR3LightColor[3] = {};\nstatic XYZ_32 m_TR3LightDirView[3] = {};\n\nstatic bool m_IsWibbleEffect = false;\nstatic bool m_IsWaterEffect = false;\nstatic bool m_IsShadeEffect = false;\nstatic bool m_IsSkyboxEnabled = false;\nstatic float m_Desaturation = 0.0f;\n\nstatic int32_t m_TintOverrideDepth = 0;\nstatic RGB_F m_TintOverrideStack[8] = {};\n\nstatic RGB_F m_GlobalTint = { 1.0f, 1.0f, 1.0f };\n\nfloat Output_GetTime(void)\n{\n    return m_Time;\n}\n\nfloat Output_GetTimeInGame(void)\n{\n    return m_TimeInGame;\n}\n\nvoid Output_SetTimeInGame(const float time)\n{\n    m_TimeInGame = time;\n}\n\nint32_t Output_GetNearZ(void)\n{\n    return 20 << W2V_SHIFT;\n}\n\nint32_t Output_GetFarZ(void)\n{\n    return Output_GetFogEnd() << W2V_SHIFT;\n}\n\nint32_t Output_GetNearZ_UI(void)\n{\n    return 20;\n}\n\nint32_t Output_GetFarZ_UI(void)\n{\n    return 10000;\n}\n\nvoid Output_SetSkyboxEnabled(const bool enabled)\n{\n    m_IsSkyboxEnabled = enabled;\n}\n\nbool Output_IsSkyboxEnabled(void)\n{\n    return m_IsSkyboxEnabled && g_Config.visuals.enable_skybox;\n}\n\nRGBA_F Output_GetFogColor(void)\n{\n    return m_FogColor;\n}\n\nint32_t Output_GetFogStart(void)\n{\n    return MIN(m_FogStart, Output_GetFogEnd());\n}\n\nint32_t Output_GetFogEnd(void)\n{\n    return m_FogEnd;\n}\n\nvoid Output_SetFogStart(const int32_t dist)\n{\n    m_FogStart = dist;\n    const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms();\n    if (uniforms != nullptr) {\n        Output_Uniforms_UploadFogDistance(\n            uniforms, Output_GetFogStart(), Output_GetFogEnd());\n    }\n}\n\nvoid Output_SetFogEnd(const int32_t dist)\n{\n    m_FogEnd = dist;\n    const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms();\n    if (uniforms != nullptr) {\n        Output_Uniforms_UploadFogDistance(\n            uniforms, Output_GetFogStart(), Output_GetFogEnd());\n    }\n}\n\nvoid Output_SetupBelowWater(const bool underwater)\n{\n    m_IsWaterEffect = true;\n    m_IsWibbleEffect = !underwater;\n    m_IsShadeEffect = true;\n}\n\nvoid Output_SetupAboveWater(const bool underwater)\n{\n    m_IsWaterEffect = false;\n    m_IsWibbleEffect = underwater;\n    m_IsShadeEffect = underwater;\n}\n\nbool Output_GetWaterEffect(void)\n{\n    return m_IsWaterEffect;\n}\n\nbool Output_GetWibbleEffect(void)\n{\n    return m_IsWibbleEffect;\n}\n\nfloat Output_GetDesaturation(void)\n{\n    return m_Desaturation;\n}\n\nvoid Output_SetDesaturation(const float desaturation)\n{\n    if (m_Desaturation == desaturation) {\n        return;\n    }\n    m_Desaturation = desaturation;\n\n    const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms();\n    if (uniforms != nullptr) {\n        Output_Uniforms_UploadDesaturation(uniforms, m_Desaturation);\n    }\n}\n\nvoid Output_SetFogColor(const RGBA_8888 color)\n{\n    m_FogColor.r = color.r / 255.0f;\n    m_FogColor.g = color.g / 255.0f;\n    m_FogColor.b = color.b / 255.0f;\n    m_FogColor.a = color.a / 255.0f;\n}\n\nRGB_F Output_GetWaterColor(void)\n{\n    return m_WaterColor;\n}\n\nvoid Output_SetWaterColor(const RGB_888 color)\n{\n    m_WaterColor.r = color.r / 255.0f;\n    m_WaterColor.g = color.g / 255.0f;\n    m_WaterColor.b = color.b / 255.0f;\n}\n\nRGB_F Output_GetGlobalTint(void)\n{\n    return m_GlobalTint;\n}\n\nvoid Output_SetGlobalTint(const RGB_F tint)\n{\n    m_GlobalTint = tint;\n    const OUTPUT_UNIFORMS *const uniforms = Output_GetUniforms();\n    if (uniforms != nullptr) {\n        Output_Uniforms_UploadGlobalTint(uniforms, m_GlobalTint);\n    }\n}\n\nRGB_F Output_GetTint(void)\n{\n    if (m_TintOverrideDepth != 0) {\n        return m_TintOverrideStack[m_TintOverrideDepth - 1];\n    }\n    if (m_IsShadeEffect) {\n        return m_WaterColor;\n    }\n    return COLOR_RGB_F_WHITE;\n}\n\nvoid Output_PushTintOverride(const RGB_F tint)\n{\n    ASSERT(m_TintOverrideDepth < (int32_t)ARRAY_SIZE(m_TintOverrideStack));\n    m_TintOverrideStack[m_TintOverrideDepth++] = tint;\n}\n\nvoid Output_PopTintOverride(void)\n{\n    ASSERT(m_TintOverrideDepth > 0);\n    m_TintOverrideDepth--;\n}\n\nvoid Output_GetPerspProjectionMatrix(GLfloat output[][4])\n{\n    const float left = Viewport_GetMinX(VIEWPORT_GAME);\n    const float top = Viewport_GetMinY(VIEWPORT_GAME);\n    const float right = Viewport_GetWidth(VIEWPORT_GAME);\n    const float bottom = Viewport_GetHeight(VIEWPORT_GAME);\n    const float near = Output_GetNearZ() / (float)(1 << W2V_SHIFT);\n    const float far = Output_GetFarZ() / (float)(1 << W2V_SHIFT);\n    const float aspect = (float)(right - left) / (float)(bottom - top);\n    const float fov = Viewport_GetEffectiveFOV() * M_PI / (float)DEG_180;\n\n    float f_x, f_y;\n    switch (Viewport_GetFOVMode()) {\n    case FOV_MODE_HORIZONTAL:\n        f_x = 1.0f / tanf(fov * 0.5f);\n        f_y = f_x * aspect;\n        break;\n    case FOV_MODE_VERTICAL:\n        f_y = 1.0f / tanf(fov * 0.5f);\n        f_x = f_y / aspect;\n        break;\n    case FOV_MODE_PC: {\n        const float persp = ((4.0f / 3.0f) / aspect);\n        f_x = persp / tanf(fov * 0.5f);\n        f_y = f_x * aspect;\n        break;\n    }\n    case FOV_MODE_PS1: {\n        const float persp = ((4.0f / 3.0f) / aspect) * (240.0f / 200.0f);\n        f_x = persp / tanf(fov * 0.5f);\n        f_y = f_x * aspect;\n        break;\n    }\n    default:\n        ASSERT_FAIL();\n    }\n\n    const float near_z = Output_GetNearZ();\n    const float far_z = Output_GetFarZ();\n    const float res_z = 0.99 * near_z * far_z / (far_z - near_z);\n\n    output[0][0] = f_x;\n    output[0][1] = 0.0f;\n    output[0][2] = 0.0f;\n    output[0][3] = 0.0f;\n\n    output[1][0] = 0.0f;\n    output[1][1] = -f_y;\n    output[1][2] = 0.0f;\n    output[1][3] = 0.0f;\n\n    output[2][0] = 0.0f;\n    output[2][1] = 0.0f;\n    output[2][2] = 0.005 + res_z / near_z;\n    output[2][3] = 1.0f;\n\n    output[3][0] = 0.0f;\n    output[3][1] = 0.0f;\n    output[3][2] = -res_z / (float)(1 << W2V_SHIFT);\n    output[3][3] = 0.0f;\n}\n\nvoid Output_GetOrthoProjectionMatrix(GLfloat output[][4])\n{\n    const float left = 0.0f;\n    const float top = 0.0f;\n    const float right = Viewport_GetWidth(VIEWPORT_UI);\n    const float bottom = Viewport_GetHeight(VIEWPORT_UI);\n    const float near = Output_GetNearZ_UI();\n    const float far = Output_GetFarZ_UI();\n\n    output[0][0] = 2.0f / (right - left);\n    output[0][1] = 0.0f;\n    output[0][2] = 0.0f;\n    output[0][3] = 0.0f;\n\n    output[1][0] = 0.0f;\n    output[1][1] = 2.0f / (top - bottom);\n    output[1][2] = 0.0f;\n    output[1][3] = 0.0f;\n\n    output[2][0] = 0.0f;\n    output[2][1] = 0.0f;\n    output[2][2] = 2.0f / (far - near);\n    output[2][3] = 0.0f;\n\n    output[3][0] = -(right + left) / (right - left);\n    output[3][1] = -(top + bottom) / (top - bottom);\n    output[3][2] = -(far + near) / (far - near);\n    output[3][3] = 1.0f;\n}\n\nvoid Output_SetCurrentRoom(const ROOM *const room)\n{\n    m_CurrentRoom = room;\n}\n\nconst ROOM *Output_GetCurrentRoom(void)\n{\n    return m_CurrentRoom;\n}\n\nint32_t Output_GetLightAdder(void)\n{\n    return m_LsAdder;\n}\n\nvoid Output_SetLightAdder(const int32_t adder)\n{\n    m_LsAdder = adder;\n}\n\nint32_t Output_GetLightDivider(void)\n{\n    return m_LsDivider;\n}\n\nvoid Output_SetLightDivider(const int32_t divider)\n{\n    m_LsDivider = divider;\n}\n\nXYZ_32 Output_GetLightVectorView(void)\n{\n    return m_LsVectorView;\n}\n\nOUTPUT_LIGHT_INFO Output_GetLightInfo(void)\n{\n    OUTPUT_LIGHT_INFO info = {\n        .ls_adder = m_LsAdder,\n        .ls_divider = m_LsDivider,\n        .ls_vector_view = m_LsVectorView,\n        .tr3_ambient = m_TR3Ambient,\n    };\n    for (int32_t i = 0; i < 3; i++) {\n        info.tr3_light_color[i] = m_TR3LightColor[i];\n        info.tr3_light_dir_view[i] = m_TR3LightDirView[i];\n    }\n    return info;\n}\n\nvoid Output_RotateLight(const int16_t pitch, const int16_t yaw)\n{\n    const int32_t cp = Math_Cos(pitch);\n    const int32_t sp = Math_Sin(pitch);\n    const int32_t cy = Math_Cos(yaw);\n    const int32_t sy = Math_Sin(yaw);\n    const int32_t x = TRIGMULT2(cp, sy);\n    const int32_t y = -sp;\n    const int32_t z = TRIGMULT2(cp, cy);\n    const MATRIX *const m = &g_ViewMatrix;\n    m_LsVectorView.x = (m->_00 * x + m->_01 * y + m->_02 * z) >> W2V_SHIFT;\n    m_LsVectorView.y = (m->_10 * x + m->_11 * y + m->_12 * z) >> W2V_SHIFT;\n    m_LsVectorView.z = (m->_20 * x + m->_21 * y + m->_22 * z) >> W2V_SHIFT;\n}\n\nvoid Output_SetTR3Light(\n    const RGB_F ambient, const RGB_F colors[3], const XYZ_32 dirs_view[3])\n{\n    m_TR3Ambient = ambient;\n    for (int32_t i = 0; i < 3; i++) {\n        m_TR3LightColor[i] = colors[i];\n        m_TR3LightDirView[i] = dirs_view[i];\n    }\n}\n\nvoid Output_SetTime(const float time)\n{\n    m_Time = time;\n}\n\nvoid Output_AnimateTextures(int32_t num_frames)\n{\n    const int32_t anim_delta = g_TRVersion == 3 ? 2 : 1;\n\n    m_TimeInGame += num_frames;\n    m_AnimatedTexturesOffset += num_frames * anim_delta;\n    bool update = false;\n    while (m_AnimatedTexturesOffset > 5) {\n        Output_CycleAnimatedTextures();\n        update = true;\n        m_AnimatedTexturesOffset -= 5;\n    }\n    if (update) {\n        Output_Textures_CycleAnimations();\n        SceneCompositor_AnimateTextures();\n    }\n\n    Output_AnimateLights(num_frames);\n}\n\nvoid Output_EnableScissor(\n    const float x, const float y, const float w, const float h)\n{\n    // Causes the rendering pipeline to discard every pixel outside of the\n    // specified window. The window is in game framebuffer viewport's\n    // coordinates; to make it work properly, we need to translate it to the\n    // SDL window coordinates first.\n\n    // To deal with precision issues coming from using integer matrix ops\n    const int32_t border = 4;\n\n    const VIEWPORT_RECT game = Viewport_GetRect(VIEWPORT_GAME);\n    const VIEWPORT_RECT window = Viewport_GetRect(VIEWPORT_GAME);\n    const float scale_x = window.w / (float)game.w;\n    const float scale_y = window.h / (float)game.h;\n    VIEWPORT_RECT scissor = {\n        .x = window.x + (x * scale_x) - border,\n        .y = window.y + (game.h - y) * scale_y - border,\n        .w = w * scale_x + border * 2,\n        .h = h * scale_y + border * 2,\n    };\n\n    glEnable(GL_SCISSOR_TEST);\n    glScissor(scissor.x, scissor.y, scissor.w, scissor.h);\n}\n\nvoid Output_DisableScissor(void)\n{\n    glDisable(GL_SCISSOR_TEST);\n}\n\nvoid Output_AdjustDepth(const float factor, const float units)\n{\n    if (factor != m_DepthFactor || units != m_DepthUnits) {\n        glPolygonOffset(factor, units);\n        m_DepthFactor = factor;\n        m_DepthUnits = units;\n    }\n}\n\nbool Output_IsControlFrame(void)\n{\n    return m_ControlFrame;\n}\n\nvoid Output_SetControlFrame(const bool is_control_frame)\n{\n    m_ControlFrame = is_control_frame;\n}\n"
  },
  {
    "path": "src/trx/game/output/state.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/math/types.h>\n#include <trx/game/output/uniforms.h>\n#include <trx/game/rooms.h>\n\n#include <GL/glew.h>\n\nvoid Output_SetSkyboxEnabled(bool enabled);\nbool Output_IsSkyboxEnabled(void);\n\nvoid Output_GetPerspProjectionMatrix(GLfloat output[][4]);\nvoid Output_GetOrthoProjectionMatrix(GLfloat output[][4]);\n\nvoid Output_SetTime(float time);\nfloat Output_GetTime(void);\nfloat Output_GetTimeInGame(void);\nvoid Output_SetTimeInGame(float time);\nbool Output_IsControlFrame(void);\nvoid Output_SetControlFrame(bool is_control_frame);\n\nvoid Output_SetupBelowWater(bool is_underwater);\nvoid Output_SetupAboveWater(bool is_underwater);\nRGB_F Output_GetWaterColor(void);\nvoid Output_SetWaterColor(RGB_888 color);\n\nRGB_F Output_GetTint(void);\nvoid Output_PushTintOverride(RGB_F tint);\nvoid Output_PopTintOverride(void);\nbool Output_GetWaterEffect(void);\nbool Output_GetWibbleEffect(void);\nfloat Output_GetDesaturation(void);\nvoid Output_SetDesaturation(float desaturation);\nRGB_F Output_GetGlobalTint(void);\nvoid Output_SetGlobalTint(RGB_F tint);\n\nRGBA_F Output_GetFogColor(void);\nint32_t Output_GetFogStart(void);\nint32_t Output_GetFogEnd(void);\n\nvoid Output_SetFogColor(RGBA_8888 color);\nvoid Output_SetFogStart(int32_t dist);\nvoid Output_SetFogEnd(int32_t dist);\n\nint32_t Output_GetNearZ(void);\nint32_t Output_GetFarZ(void);\nint32_t Output_GetNearZ_UI(void);\nint32_t Output_GetFarZ_UI(void);\n\nvoid Output_SetCurrentRoom(const ROOM *room_num);\nconst ROOM *Output_GetCurrentRoom(void);\n\nint32_t Output_GetLightAdder(void);\nint32_t Output_GetLightDivider(void);\nXYZ_32 Output_GetLightVectorView(void);\nOUTPUT_LIGHT_INFO Output_GetLightInfo(void);\nvoid Output_SetLightAdder(int32_t adder);\nvoid Output_SetLightDivider(int32_t divider);\nvoid Output_RotateLight(int16_t pitch, int16_t yaw);\nvoid Output_SetTR3Light(\n    RGB_F ambient, const RGB_F colors[3], const XYZ_32 dirs_view[3]);\n\nvoid Output_EnableScissor(float x, float y, float w, float h);\nvoid Output_DisableScissor(void);\n\nvoid Output_AdjustDepth(float factor, float units);\n\nvoid Output_SetupBelowWater(bool underwater);\nvoid Output_SetupAboveWater(bool underwater);\n\nvoid Output_AnimateTextures(int32_t num_frames);\n"
  },
  {
    "path": "src/trx/game/output/textures.c",
    "content": "#include <trx/game/output/textures.h>\n\n#include <trx/core/hash.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/level/cache.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/output.h>\n#include <trx/game/output/vertex_range.h>\n#include <trx/gl/utils.h>\n#include <trx/version.h>\n\n#include <SDL2/SDL_mutex.h>\n#include <string.h>\n\ntypedef struct {\n    OUTPUT_UVW corners[4];\n} M_UVW_PACK;\n\nstatic struct {\n    VECTOR *objects;\n    VECTOR *sprites;\n} m_AnimationRanges;\n\nstatic struct {\n    GLuint tex_atlas;\n    GLuint tex_env_map;\n\n    struct {\n        int32_t count;\n        int32_t count_objects;\n        int32_t count_sprites;\n        M_UVW_PACK *data;\n        M_UVW_PACK *data_objects;\n        M_UVW_PACK *data_sprites;\n\n        bool *animated;\n        bool *animated_objects;\n        bool *animated_sprites;\n        bool *has_transparency_objects;\n\n        uint16_t *flags;\n        uint16_t *flags_objects;\n        uint16_t *flags_sprites;\n    } uvws;\n\n    struct {\n        OUTPUT_TEXTURE_SIZE *data;\n        OUTPUT_TEXTURE_SIZE *data_objects;\n        OUTPUT_TEXTURE_SIZE *data_sprites;\n    } atlas_sizes;\n} m_Priv = {};\n\nstatic int32_t m_TexturePageCount = 0;\nstatic uint8_t *m_TexturePages8 = nullptr;\nstatic RGBA_8888 *m_TexturePages32 = nullptr;\nstatic SDL_mutex **m_TexturePageLocks = nullptr;\n\nstatic int32_t m_PaletteSize = 0;\nstatic RGB_888 *m_Palette8 = nullptr;\nstatic RGB_888 *m_Palette16 = nullptr;\n\nstatic LIGHT_MAP m_LightMap[32];\nstatic SHADE_MAP m_ShadeMap[256];\n\nstatic int32_t m_ObjectTextureCount = 0;\nstatic int32_t m_SpriteTextureCount = 0;\nstatic OBJECT_TEXTURE *m_ObjectTextures = nullptr;\nstatic SPRITE_TEXTURE *m_SpriteTextures = nullptr;\nstatic ANIMATED_TEXTURE_RANGE *m_AnimTextureRanges = nullptr;\n\n#define M_TRANSPARENCY_CACHE_VERSION 1\nstatic uint64_t M_ComputeTransparencyChecksum(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level == nullptr) {\n        return 0;\n    }\n\n    uint64_t hash = LevelCache_InitChecksum(\n        \"object_transparency_cache\", M_TRANSPARENCY_CACHE_VERSION);\n    hash = LevelCache_UpdateLevelChecksum(hash, level);\n    hash = Hash_FNV1a64_UpdateU32(hash, Output_GetObjectTextureCount());\n    return hash;\n}\n\nstatic const char *M_GetTransparencyCacheFilename(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    const char *const level_key = LevelCache_GetLevelKey(level);\n    if (level_key == nullptr) {\n        return nullptr;\n    }\n\n    return String_FormatStatic(\"object_transparency_%s.cache.dat\", level_key);\n}\n\nstatic bool M_TryLoadTransparencyCache(void)\n{\n    const int32_t texture_count = Output_GetObjectTextureCount();\n    const uint64_t expected_checksum = M_ComputeTransparencyChecksum();\n    const char *const cache_filename = M_GetTransparencyCacheFilename();\n    if (cache_filename == nullptr || expected_checksum == 0\n        || texture_count <= 0) {\n        return false;\n    }\n\n    MYFILE *const file =\n        LevelCache_OpenBinaryRead(cache_filename, expected_checksum);\n    if (file == nullptr) {\n        return false;\n    }\n\n    const int32_t version = File_ReadS32(file);\n    const int32_t cached_texture_count = File_ReadS32(file);\n    if (version != M_TRANSPARENCY_CACHE_VERSION\n        || cached_texture_count != texture_count\n        || !File_ReadData(\n            file, m_Priv.uvws.has_transparency_objects,\n            sizeof(bool) * (size_t)texture_count)) {\n        File_Close(file);\n        return false;\n    }\n\n    File_Close(file);\n    return true;\n}\n\nstatic void M_WriteTransparencyCache(void)\n{\n    const int32_t texture_count = Output_GetObjectTextureCount();\n    const uint64_t checksum = M_ComputeTransparencyChecksum();\n    const char *const cache_filename = M_GetTransparencyCacheFilename();\n    if (cache_filename == nullptr || checksum == 0 || texture_count <= 0) {\n        return;\n    }\n\n    MYFILE *const file = LevelCache_OpenBinaryWrite(cache_filename, checksum);\n    if (file == nullptr) {\n        return;\n    }\n\n    File_WriteS32(file, M_TRANSPARENCY_CACHE_VERSION);\n    File_WriteS32(file, texture_count);\n    File_WriteData(\n        file, m_Priv.uvws.has_transparency_objects,\n        sizeof(bool) * (size_t)texture_count);\n    File_Close(file);\n}\n\nstatic float M_NormalizeObjectUV(const uint16_t uv)\n{\n    return uv / 65535.0f;\n}\n\nstatic void M_PrepareObjectAnimationRanges(void)\n{\n    size_t required_size = 0;\n    for (const ANIMATED_TEXTURE_RANGE *src_range =\n             Output_GetAnimatedTextureRange(0);\n         src_range != nullptr; src_range = src_range->next_range) {\n        required_size += src_range->num_textures;\n    }\n\n    Vector_Clear(m_AnimationRanges.objects);\n    Vector_EnsureCapacity(m_AnimationRanges.objects, required_size);\n\n    for (const ANIMATED_TEXTURE_RANGE *src_range =\n             Output_GetAnimatedTextureRange(0);\n         src_range != nullptr; src_range = src_range->next_range) {\n        for (int32_t i = 0; i < src_range->num_textures; i++) {\n            Vector_Add(\n                m_AnimationRanges.objects,\n                &(OUTPUT_VERTEX_RANGE) {\n                    .vertex_start = src_range->textures[i],\n                    .vertex_count = 1,\n                });\n        }\n    }\n    Output_GlueVertexRanges(m_AnimationRanges.objects);\n}\n\nstatic void M_PrepareSpriteAnimationRanges(void)\n{\n    size_t required_size = 0;\n    const int32_t static_2d_count = Object_GetStaticObjects2DCount();\n    for (int32_t i = 0; i < static_2d_count; i++) {\n        const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i);\n        if (obj == nullptr || !obj->loaded || obj->frame_count == 1) {\n            continue;\n        }\n        required_size++;\n    }\n\n    Vector_Clear(m_AnimationRanges.sprites);\n    Vector_EnsureCapacity(m_AnimationRanges.sprites, required_size);\n\n    for (int32_t i = 0; i < static_2d_count; i++) {\n        const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i);\n        if (obj == nullptr || !obj->loaded || obj->frame_count == 1) {\n            continue;\n        }\n        Vector_Add(\n            m_AnimationRanges.sprites,\n            &(OUTPUT_VERTEX_RANGE) {\n                .vertex_start = obj->texture_idx,\n                .vertex_count = obj->frame_count,\n            });\n    }\n    Output_GlueVertexRanges(m_AnimationRanges.sprites);\n}\n\nstatic void M_PrepareAnimationRanges(void)\n{\n    M_PrepareObjectAnimationRanges();\n    M_PrepareSpriteAnimationRanges();\n\n    for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) {\n        m_Priv.uvws.animated_objects[i] = false;\n        for (int32_t j = 0; j < m_AnimationRanges.objects->count; j++) {\n            const OUTPUT_VERTEX_RANGE *const dst_range =\n                Vector_Get(m_AnimationRanges.objects, j);\n            const int32_t range_start = dst_range->vertex_start;\n            const int32_t range_end = range_start + dst_range->vertex_count;\n            if (i >= range_start && i < range_end) {\n                m_Priv.uvws.animated_objects[i] = true;\n                break;\n            }\n        }\n    }\n\n    for (int32_t i = 0; i < Output_GetSpriteTextureCount(); i++) {\n        m_Priv.uvws.animated_sprites[i] = false;\n        for (int32_t j = 0; j < m_AnimationRanges.sprites->count; j++) {\n            const OUTPUT_VERTEX_RANGE *const dst_range =\n                Vector_Get(m_AnimationRanges.sprites, j);\n            const int32_t range_start = dst_range->vertex_start;\n            const int32_t range_end = range_start + dst_range->vertex_count;\n            if (i >= range_start && i < range_end) {\n                m_Priv.uvws.animated_sprites[i] = true;\n                break;\n            }\n        }\n    }\n}\n\nstatic void M_FillAtlasObjectSize(const int32_t i)\n{\n    OUTPUT_TEXTURE_SIZE *const size = &m_Priv.atlas_sizes.data_objects[i];\n    const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(i);\n    size->x0 = texture->uv[0].u;\n    size->y0 = texture->uv[0].v;\n    size->x1 = texture->uv[0].u;\n    size->y1 = texture->uv[0].v;\n    for (int32_t j = 1; j < texture->uv_count; j++) {\n        size->x0 = MIN(size->x0, texture->uv[j].u);\n        size->y0 = MIN(size->y0, texture->uv[j].v);\n        size->x1 = MAX(size->x1, texture->uv[j].u);\n        size->y1 = MAX(size->y1, texture->uv[j].v);\n    }\n    size->x0 = M_NormalizeObjectUV(size->x0);\n    size->y0 = M_NormalizeObjectUV(size->y0);\n    size->x1 = M_NormalizeObjectUV(size->x1);\n    size->y1 = M_NormalizeObjectUV(size->y1);\n}\n\nstatic void M_FillAtlasSpriteSize(const int32_t i)\n{\n    OUTPUT_TEXTURE_SIZE *const size = &m_Priv.atlas_sizes.data_sprites[i];\n    const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(i);\n    const float adj = 0.1 / 256.0f;\n    const float u0 = (sprite->offset & 0xFF) / 256.0f + adj;\n    const float v0 = (sprite->offset >> 8) / 256.0f + adj;\n    const float u1 = u0 + sprite->width / 65536.0f - 2 * adj;\n    const float v1 = v0 + sprite->height / 65536.0f - 2 * adj;\n    size->x0 = u0;\n    size->y0 = v0;\n    size->x1 = u1;\n    size->y1 = v1;\n}\n\nstatic void M_FillObjectUVW(const int32_t i)\n{\n    const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(i);\n    OUTPUT_UVW *const corners = m_Priv.uvws.data_objects[i].corners;\n    for (int32_t j = 0; j < 4; j++) {\n        corners[j].u = M_NormalizeObjectUV(texture->uv[j].u);\n        corners[j].v = M_NormalizeObjectUV(texture->uv[j].v);\n        corners[j].w = texture->tex_page;\n    }\n}\n\nstatic void M_FillSpriteUVW(const int32_t i)\n{\n    const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(i);\n    const float adj = 0.1 / 256.0f;\n    const float u0 = (sprite->offset & 0xFF) / 256.0f + adj;\n    const float v0 = (sprite->offset >> 8) / 256.0f + adj;\n    const float u1 = u0 + sprite->width / 65536.0f - 2 * adj;\n    const float v1 = v0 + sprite->height / 65536.0f - 2 * adj;\n    OUTPUT_UVW *const corners = m_Priv.uvws.data_sprites[i].corners;\n    // clang-format off\n    corners[0].u = u0; corners[0].v = v0; corners[0].w = sprite->tex_page;\n    corners[1].u = u1; corners[1].v = v0; corners[1].w = sprite->tex_page;\n    corners[2].u = u1; corners[2].v = v1; corners[2].w = sprite->tex_page;\n    corners[3].u = u0; corners[3].v = v1; corners[3].w = sprite->tex_page;\n    m_Priv.uvws.flags_sprites[i] = sprite->flags;\n    // clang-format on\n}\n\nstatic void M_FillObjectUVWs(void)\n{\n    for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) {\n        M_FillObjectUVW(i);\n    }\n}\n\nstatic void M_FillSpriteUVWs(void)\n{\n    for (int32_t i = 0; i < Output_GetSpriteTextureCount(); i++) {\n        M_FillSpriteUVW(i);\n    }\n}\n\nstatic void M_UpdateObjectAnimatedUVWs(VECTOR *const source)\n{\n    for (int32_t i = 0; i < source->count; i++) {\n        const OUTPUT_VERTEX_RANGE *const range = Vector_Get(source, i);\n        for (int32_t j = 0; j < range->vertex_count; j++) {\n            M_FillObjectUVW(range->vertex_start + j);\n            M_FillAtlasObjectSize(range->vertex_start + j);\n        }\n    }\n}\n\nstatic void M_UpdateSpriteAnimatedUVWs(VECTOR *const source)\n{\n    for (int32_t i = 0; i < source->count; i++) {\n        const OUTPUT_VERTEX_RANGE *const range = Vector_Get(source, i);\n        for (int32_t j = 0; j < range->vertex_count; j++) {\n            M_FillSpriteUVW(range->vertex_start + j);\n            M_FillAtlasSpriteSize(range->vertex_start + j);\n        }\n    }\n}\n\nstatic void M_PrepareUVWs(void)\n{\n    m_Priv.uvws.count_objects = Output_GetObjectTextureCount();\n    m_Priv.uvws.count_sprites = Output_GetSpriteTextureCount();\n    m_Priv.uvws.count = m_Priv.uvws.count_objects + m_Priv.uvws.count_sprites;\n    m_Priv.uvws.data = Memory_Alloc(m_Priv.uvws.count * sizeof(M_UVW_PACK));\n    m_Priv.uvws.data_objects = m_Priv.uvws.data;\n    m_Priv.uvws.data_sprites = m_Priv.uvws.data + m_Priv.uvws.count_objects;\n    m_Priv.uvws.animated = Memory_Alloc(m_Priv.uvws.count * sizeof(bool));\n    m_Priv.uvws.animated_objects = m_Priv.uvws.animated;\n    m_Priv.uvws.animated_sprites =\n        m_Priv.uvws.animated + m_Priv.uvws.count_objects;\n    m_Priv.uvws.flags = Memory_Alloc(m_Priv.uvws.count * sizeof(uint16_t));\n    m_Priv.uvws.flags_objects = m_Priv.uvws.flags;\n    m_Priv.uvws.flags_sprites = m_Priv.uvws.flags + m_Priv.uvws.count_objects;\n    m_Priv.uvws.has_transparency_objects =\n        Memory_Alloc(m_Priv.uvws.count_objects * sizeof(bool));\n    M_FillObjectUVWs();\n    M_FillSpriteUVWs();\n}\n\nstatic bool M_ObjectTextureHasTransparency(const int32_t texture_idx)\n{\n    if (texture_idx < 0 || texture_idx >= Output_GetObjectTextureCount()\n        || m_TexturePages32 == nullptr) {\n        return true;\n    }\n\n    const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(texture_idx);\n    if (texture == nullptr || texture->uv_count <= 0) {\n        return true;\n    }\n    if (texture->tex_page >= m_TexturePageCount) {\n        return true;\n    }\n\n    int32_t min_u = INT32_MAX;\n    int32_t min_v = INT32_MAX;\n    int32_t max_u = INT32_MIN;\n    int32_t max_v = INT32_MIN;\n    for (int32_t i = 0; i < texture->uv_count; i++) {\n        CLAMPG(min_u, texture->uv[i].u);\n        CLAMPG(min_v, texture->uv[i].v);\n        CLAMPL(max_u, texture->uv[i].u);\n        CLAMPL(max_v, texture->uv[i].v);\n    }\n\n    const int32_t x0 = (min_u * (TEXTURE_PAGE_WIDTH - 1)) / 65535;\n    const int32_t y0 = (min_v * (TEXTURE_PAGE_HEIGHT - 1)) / 65535;\n    const int32_t x1 =\n        (max_u * (TEXTURE_PAGE_WIDTH - 1) + 65534) / 65535; // ceil\n    const int32_t y1 =\n        (max_v * (TEXTURE_PAGE_HEIGHT - 1) + 65534) / 65535; // ceil\n\n    int32_t px0 = x0;\n    int32_t py0 = y0;\n    int32_t px1 = x1;\n    int32_t py1 = y1;\n    CLAMP(px0, 0, TEXTURE_PAGE_WIDTH - 1);\n    CLAMP(py0, 0, TEXTURE_PAGE_HEIGHT - 1);\n    CLAMP(px1, 0, TEXTURE_PAGE_WIDTH - 1);\n    CLAMP(py1, 0, TEXTURE_PAGE_HEIGHT - 1);\n\n    const RGBA_8888 *const page = Output_GetTexturePage32(texture->tex_page);\n    if (page == nullptr) {\n        return true;\n    }\n\n    for (int32_t y = py0; y <= py1; y++) {\n        const int32_t row = y * TEXTURE_PAGE_WIDTH;\n        for (int32_t x = px0; x <= px1; x++) {\n            if (page[row + x].a < 255) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nstatic void M_PrepareObjectTransparencyFlags(void)\n{\n    for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) {\n        m_Priv.uvws.has_transparency_objects[i] =\n            M_ObjectTextureHasTransparency(i);\n    }\n}\n\nstatic void M_PrepareEnvMap(void)\n{\n    glGenTextures(1, &m_Priv.tex_env_map);\n    glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n    TRX_GL_CheckError();\n\n    if (Output_IsHeadless()) {\n        const int32_t pattern_size = 256;\n        RGB_888 *test_pattern =\n            Memory_Alloc(pattern_size * pattern_size * sizeof(RGB_888));\n        RGB_888 *pixel = test_pattern;\n        for (int32_t i = 0; i < pattern_size; i++) {\n            for (int32_t j = 0; j < pattern_size; j++) {\n                pixel->r = i % 256;\n                pixel->g = j % 256;\n                pixel->b = ((i / 32) % 2 == (j / 32) % 2) ? 255 : 0;\n                pixel++;\n            }\n        }\n\n        glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map);\n        glTexImage2D(\n            GL_TEXTURE_2D, 0, GL_RGB, pattern_size, pattern_size, 0, GL_RGB,\n            GL_UNSIGNED_BYTE, test_pattern);\n        TRX_GL_CheckError();\n        Memory_FreePointer(&test_pattern);\n    }\n}\n\nstatic void M_PrepareAtlasSizes(void)\n{\n    const int32_t count_objects = Output_GetObjectTextureCount();\n    const int32_t count_sprites = Output_GetSpriteTextureCount();\n    const int32_t count = count_objects + count_sprites;\n    m_Priv.atlas_sizes.data = Memory_Realloc(\n        m_Priv.atlas_sizes.data, count * sizeof(OUTPUT_TEXTURE_SIZE));\n    m_Priv.atlas_sizes.data_objects = m_Priv.atlas_sizes.data;\n    m_Priv.atlas_sizes.data_sprites = m_Priv.atlas_sizes.data + count_objects;\n    for (int32_t i = 0; i < count_objects; i++) {\n        M_FillAtlasObjectSize(i);\n    }\n    for (int32_t i = 0; i < count_sprites; i++) {\n        M_FillAtlasSpriteSize(i);\n    }\n}\n\nstatic void M_UploadAtlas(void)\n{\n    glGenTextures(1, &m_Priv.tex_atlas);\n    glBindTexture(GL_TEXTURE_2D_ARRAY, m_Priv.tex_atlas);\n    glTexStorage3D(\n        GL_TEXTURE_2D_ARRAY,\n        1, // number of mipmaps\n        GL_RGBA8, TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_HEIGHT,\n        Output_GetTexturePageCount());\n    TRX_GL_CheckError();\n\n    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n    TRX_GL_CheckError();\n\n    for (int32_t i = 0; i < Output_GetTexturePageCount(); i++) {\n        const RGBA_8888 *const input_ptr = Output_GetTexturePage32(i);\n\n        glTexSubImage3D(\n            GL_TEXTURE_2D_ARRAY,\n            0, // mipmap level\n            0, // x offset\n            0, // y offset\n            i, // z offset\n            TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_HEIGHT,\n            1, // depth\n            GL_RGBA, GL_UNSIGNED_BYTE, input_ptr);\n    }\n    TRX_GL_CheckError();\n\n    M_PrepareAtlasSizes();\n\n    TRX_GL_CheckError();\n}\n\nstatic void M_FreeLevelData(void)\n{\n    // destroy per-page locks\n    if (m_TexturePageLocks != nullptr) {\n        for (int32_t i = 0; i < m_TexturePageCount; i++) {\n            SDL_DestroyMutex(m_TexturePageLocks[i]);\n        }\n        m_TexturePageLocks = nullptr;\n    }\n\n    if (m_Priv.tex_atlas != 0) {\n        glDeleteTextures(1, &m_Priv.tex_atlas);\n        m_Priv.tex_atlas = 0;\n    }\n    Memory_FreePointer(&m_Priv.uvws.data);\n    Memory_FreePointer(&m_Priv.uvws.animated);\n    Memory_FreePointer(&m_Priv.uvws.flags);\n    Memory_FreePointer(&m_Priv.uvws.has_transparency_objects);\n    Memory_FreePointer(&m_Priv.atlas_sizes.data);\n\n    memset(&m_Priv.uvws, 0, sizeof(m_Priv.uvws));\n    memset(&m_Priv.atlas_sizes, 0, sizeof(m_Priv.atlas_sizes));\n}\n\nvoid Output_Textures_Init(void)\n{\n    M_PrepareEnvMap();\n    m_AnimationRanges.objects = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE));\n    m_AnimationRanges.sprites = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE));\n}\n\nvoid Output_Textures_Shutdown(void)\n{\n    if (m_AnimationRanges.objects != nullptr) {\n        Vector_Free(m_AnimationRanges.objects);\n        m_AnimationRanges.objects = nullptr;\n    }\n    if (m_AnimationRanges.sprites != nullptr) {\n        Vector_Free(m_AnimationRanges.sprites);\n        m_AnimationRanges.sprites = nullptr;\n    }\n    M_FreeLevelData();\n\n    if (m_Priv.tex_env_map != 0) {\n        glDeleteTextures(1, &m_Priv.tex_env_map);\n        m_Priv.tex_env_map = 0;\n    }\n\n    // These are GameBuf-backed and become invalid once GameBuf_Shutdown runs.\n    m_TexturePageCount = 0;\n    m_TexturePages8 = nullptr;\n    m_TexturePages32 = nullptr;\n    m_TexturePageLocks = nullptr;\n    m_PaletteSize = 0;\n    m_Palette8 = nullptr;\n    m_Palette16 = nullptr;\n    m_ObjectTextureCount = 0;\n    m_SpriteTextureCount = 0;\n    m_ObjectTextures = nullptr;\n    m_SpriteTextures = nullptr;\n    m_AnimTextureRanges = nullptr;\n}\n\nvoid Output_Textures_ObserveLevelLoad(void)\n{\n    M_FreeLevelData();\n    M_PrepareUVWs();\n    if (!M_TryLoadTransparencyCache()) {\n        M_PrepareObjectTransparencyFlags();\n        M_WriteTransparencyCache();\n    }\n    M_PrepareAnimationRanges();\n    M_UploadAtlas();\n}\n\nvoid Output_Textures_UpdateEnvironmentMap(void)\n{\n    if (Output_IsHeadless()) {\n        return;\n    }\n\n    GLint viewport[4];\n    glGetIntegerv(GL_VIEWPORT, viewport);\n    TRX_GL_CheckError();\n\n    const GLint vp_x = viewport[0];\n    const GLint vp_y = viewport[1];\n    const GLint vp_w = viewport[2];\n    const GLint vp_h = viewport[3];\n\n    const int32_t side = MIN(vp_w, vp_h);\n    const int32_t x = vp_x + (vp_w - side) / 2;\n    const int32_t y = vp_y + (vp_h - side) / 2;\n    const int32_t w = side;\n    const int32_t h = side;\n\n    glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map);\n    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, w, h, 0);\n    TRX_GL_CheckError();\n}\n\nvoid Output_Textures_CycleAnimations(void)\n{\n    if (m_Priv.uvws.count != 0) {\n        M_UpdateSpriteAnimatedUVWs(m_AnimationRanges.sprites);\n        M_UpdateObjectAnimatedUVWs(m_AnimationRanges.objects);\n    }\n}\n\nGLuint Output_Textures_GetAtlasTexture(void)\n{\n    return m_Priv.tex_atlas;\n}\n\nGLuint Output_Textures_GetEnvMapTexture(void)\n{\n    return m_Priv.tex_env_map;\n}\n\nint32_t Output_Textures_GetObjectUVWIndex(int32_t texture_idx, int32_t corner)\n{\n    return texture_idx * 4 + corner;\n}\n\nint32_t Output_Textures_GetSpriteUVWIndex(int32_t texture_idx, int32_t corner)\n{\n    return (m_Priv.uvws.count_objects + texture_idx) * 4 + corner;\n}\n\nOUTPUT_UVW Output_Textures_GetUVW(const int32_t uvw_idx)\n{\n    ASSERT(uvw_idx >= 0 && uvw_idx / 4 < m_Priv.uvws.count);\n    return m_Priv.uvws.data[uvw_idx / 4].corners[uvw_idx % 4];\n}\n\nOUTPUT_TEXTURE_SIZE Output_Textures_GetAtlasSize(const int32_t uvw_idx)\n{\n    ASSERT(uvw_idx >= 0 && uvw_idx < m_Priv.uvws.count);\n    return m_Priv.atlas_sizes.data[uvw_idx];\n}\n\nbool Output_Textures_IsObjectTextureAnimated(const int32_t texture_idx)\n{\n    return m_Priv.uvws.animated_objects[texture_idx];\n}\n\nbool Output_Textures_IsSpriteTextureAnimated(const int32_t texture_idx)\n{\n    return m_Priv.uvws.animated_sprites[texture_idx];\n}\n\nvoid Output_Textures_SetSpriteTextureFlags(\n    const int32_t texture_idx, const uint16_t flags)\n{\n    m_Priv.uvws.flags_sprites[texture_idx] = flags;\n}\n\nuint16_t Output_Textures_GetSpriteTextureFlags(const int32_t texture_idx)\n{\n    return VERT_BILLBOARD | m_Priv.uvws.flags_sprites[texture_idx];\n}\n\nSCENE_PASS Output_Textures_GetObjectTextureScenePass(const int32_t texture_idx)\n{\n    switch (Output_GetObjectTexture(texture_idx)->draw_type) {\n    case DRAW_OPAQUE:\n    case DRAW_REFLECTIVE_OPAQUE:\n        return SCENE_PASS_OPAQUE;\n    case DRAW_BLEND:\n        if (!m_Priv.uvws.animated_objects[texture_idx]\n            && !m_Priv.uvws.has_transparency_objects[texture_idx]) {\n            return SCENE_PASS_OPAQUE;\n        }\n        return SCENE_PASS_TRANSPARENT;\n    case DRAW_BLEND_ADD:\n    case DRAW_REFLECTIVE_BLEND_ADD:\n        return SCENE_PASS_BLEND_ADD;\n    case DRAW_BLEND_SUB:\n        return SCENE_PASS_BLEND_SUB;\n    }\n    return SCENE_PASS_OPAQUE;\n}\n\nvoid Output_Textures_ApplyRenderSettings(void)\n{\n    // re-adjust UVs when the bilinear filter is toggled.\n    if (m_Priv.uvws.count != 0) {\n        M_FillObjectUVWs();\n    }\n}\nvoid Output_InitialiseTexturePages(const int32_t num_pages, const bool use_8bit)\n{\n    m_TexturePageCount = num_pages;\n    if (num_pages == 0) {\n        m_TexturePages32 = nullptr;\n        m_TexturePages8 = nullptr;\n        return;\n    }\n\n    const int32_t page_size = num_pages * TEXTURE_PAGE_SIZE;\n    m_TexturePages32 =\n        GameBuf_Alloc(sizeof(RGBA_8888) * page_size, GBUF_TEXTURE_PAGES);\n    m_TexturePages8 = use_8bit\n        ? GameBuf_Alloc(sizeof(uint8_t) * page_size, GBUF_TEXTURE_PAGES)\n        : nullptr;\n\n    m_TexturePageLocks =\n        GameBuf_Alloc(sizeof(SDL_mutex *) * num_pages, GBUF_TEXTURE_PAGES);\n    for (int32_t i = 0; i < num_pages; i++) {\n        m_TexturePageLocks[i] = SDL_CreateMutex();\n        ASSERT(m_TexturePageLocks[i] != nullptr);\n    }\n}\n\nvoid Output_InitialisePalettes(\n    const int32_t palette_size, const RGB_888 *const palette_8,\n    const RGB_888 *const palette_16)\n{\n    ASSERT(palette_size != 0);\n    ASSERT(palette_8 != nullptr);\n    m_PaletteSize = palette_size;\n\n    m_Palette8 = GameBuf_Alloc(sizeof(RGB_888) * palette_size, GBUF_PALETTES);\n    memcpy(m_Palette8, palette_8, sizeof(RGB_888) * palette_size);\n\n    if (palette_16 != nullptr) {\n        m_Palette16 =\n            GameBuf_Alloc(sizeof(RGB_888) * palette_size, GBUF_PALETTES);\n        memcpy(m_Palette16, palette_16, sizeof(RGB_888) * palette_size);\n    } else {\n        m_Palette16 = nullptr;\n    }\n}\n\nvoid Output_InitialiseObjectTextures(const int32_t num_textures)\n{\n    m_ObjectTextureCount = num_textures;\n    m_ObjectTextures = num_textures == 0\n        ? nullptr\n        : GameBuf_Alloc(\n              sizeof(OBJECT_TEXTURE) * num_textures, GBUF_OBJECT_TEXTURES);\n}\n\nvoid Output_InitialiseSpriteTextures(const int32_t num_textures)\n{\n    m_SpriteTextureCount = num_textures;\n    m_SpriteTextures = num_textures == 0\n        ? nullptr\n        : GameBuf_Alloc(\n              sizeof(SPRITE_TEXTURE) * num_textures, GBUF_SPRITE_TEXTURES);\n}\n\nvoid Output_InitialiseAnimatedTextures(const int32_t num_ranges)\n{\n    m_AnimTextureRanges = num_ranges == 0\n        ? nullptr\n        : GameBuf_Alloc(\n              sizeof(ANIMATED_TEXTURE_RANGE) * num_ranges,\n              GBUF_ANIMATED_TEXTURE_RANGES);\n}\n\nint32_t Output_GetTexturePageCount(void)\n{\n    return m_TexturePageCount;\n}\n\nuint8_t *Output_GetTexturePage8(const int32_t page_idx)\n{\n    if (m_TexturePages8 == nullptr) {\n        return nullptr;\n    }\n    return &m_TexturePages8[page_idx * TEXTURE_PAGE_SIZE];\n}\n\nRGBA_8888 *Output_GetTexturePage32(const int32_t page_idx)\n{\n    if (m_TexturePages32 == nullptr) {\n        return nullptr;\n    }\n    return &m_TexturePages32[page_idx * TEXTURE_PAGE_SIZE];\n}\n\nvoid Output_LockTexturePage32(const int32_t page_idx)\n{\n    ASSERT(page_idx >= 0 && page_idx < m_TexturePageCount);\n    SDL_LockMutex(m_TexturePageLocks[page_idx]);\n}\n\nvoid Output_UnlockTexturePage32(const int32_t page_idx)\n{\n    ASSERT(page_idx >= 0 && page_idx < m_TexturePageCount);\n    SDL_UnlockMutex(m_TexturePageLocks[page_idx]);\n}\n\nint32_t Output_GetPaletteSize(void)\n{\n    return m_PaletteSize;\n}\n\nRGB_888 Output_GetPaletteColor8(const uint16_t idx)\n{\n    if (m_Palette8 == nullptr) {\n        return COLOR_RGB_888_BLACK;\n    }\n    return m_Palette8[idx];\n}\n\nRGB_888 Output_GetPaletteColor16(const uint16_t idx)\n{\n    if (m_Palette16 == nullptr) {\n        return COLOR_RGB_888_BLACK;\n    }\n    return m_Palette16[idx];\n}\n\nLIGHT_MAP *Output_GetLightMap(const uint8_t idx)\n{\n    return &m_LightMap[idx];\n}\n\nSHADE_MAP *Output_GetShadeMap(const uint8_t idx)\n{\n    return &m_ShadeMap[idx];\n}\n\nint32_t Output_GetObjectTextureCount(void)\n{\n    return m_ObjectTextureCount;\n}\n\nint32_t Output_GetSpriteTextureCount(void)\n{\n    return m_SpriteTextureCount;\n}\n\nOBJECT_TEXTURE *Output_GetObjectTexture(const int32_t texture_idx)\n{\n    if (m_ObjectTextures == nullptr) {\n        return nullptr;\n    }\n    return &m_ObjectTextures[texture_idx];\n}\n\nSPRITE_TEXTURE *Output_GetSpriteTexture(const int32_t texture_idx)\n{\n    if (m_SpriteTextures == nullptr) {\n        return nullptr;\n    }\n    return &m_SpriteTextures[texture_idx];\n}\n\nANIMATED_TEXTURE_RANGE *Output_GetAnimatedTextureRange(const int32_t range_idx)\n{\n    if (m_AnimTextureRanges == nullptr) {\n        return nullptr;\n    }\n    return &m_AnimTextureRanges[range_idx];\n}\n\nRGBA_8888 Output_RGB2RGBA(const RGB_888 color)\n{\n    return (RGBA_8888) { .r = color.r, .g = color.g, .b = color.b, .a = 255 };\n}\n\nRGBA_F Output_RGB2RGBA_F(const RGB_F color)\n{\n    return (RGBA_F) { .r = color.r, .g = color.g, .b = color.b, .a = 1.0f };\n}\n\nint16_t Output_FindColor8(const RGB_888 color)\n{\n    if (m_Palette8 == nullptr) {\n        return -1;\n    }\n\n    int32_t best_idx = 0;\n    int32_t best_diff = INT32_MAX;\n    for (int32_t i = 0; i < m_PaletteSize; i++) {\n        const int32_t dr = color.r - m_Palette8[i].r;\n        const int32_t dg = color.g - m_Palette8[i].g;\n        const int32_t db = color.b - m_Palette8[i].b;\n        const int32_t diff = SQUARE(dr) + SQUARE(dg) + SQUARE(db);\n        if (diff < best_diff) {\n            best_diff = diff;\n            best_idx = i;\n        }\n    }\n\n    return best_idx;\n}\n\nvoid Output_CycleAnimatedTextures(void)\n{\n    const ANIMATED_TEXTURE_RANGE *range = m_AnimTextureRanges;\n    for (; range != nullptr; range = range->next_range) {\n        int32_t i = 0;\n        const OBJECT_TEXTURE temp = m_ObjectTextures[range->textures[i]];\n        for (; i < range->num_textures - 1; i++) {\n            m_ObjectTextures[range->textures[i]] =\n                m_ObjectTextures[range->textures[i + 1]];\n        }\n        m_ObjectTextures[range->textures[i]] = temp;\n    }\n\n    const int32_t static_2d_count = Object_GetStaticObjects2DCount();\n    for (int32_t i = 0; i < static_2d_count; i++) {\n        const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i);\n        if (obj == nullptr || !obj->loaded || obj->frame_count == 1) {\n            continue;\n        }\n\n        const int16_t frame_count = obj->frame_count;\n        const SPRITE_TEXTURE temp = m_SpriteTextures[obj->texture_idx];\n        for (int32_t j = 0; j < frame_count - 1; j++) {\n            m_SpriteTextures[obj->texture_idx + j] =\n                m_SpriteTextures[obj->texture_idx + j + 1];\n        }\n        m_SpriteTextures[obj->texture_idx + frame_count - 1] = temp;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/output/textures.h",
    "content": "#pragma once\n\n#include <trx/game/output/scene_source.h>\n#include <trx/game/output/types.h>\n\n#include <GL/glew.h>\n\n#pragma pack(push, 1)\ntypedef struct {\n    float x0;\n    float y0;\n    float x1;\n    float y1;\n} OUTPUT_TEXTURE_SIZE;\n\ntypedef struct {\n    float u;\n    float v;\n    float w;\n} OUTPUT_UVW;\n#pragma pack(pop)\n\nvoid Output_Textures_Init(void);\nvoid Output_Textures_Shutdown(void);\nvoid Output_Textures_ObserveLevelLoad(void);\nvoid Output_Textures_UpdateEnvironmentMap(void);\nvoid Output_Textures_CycleAnimations(void);\nvoid Output_Textures_ApplyRenderSettings(void);\nGLuint Output_Textures_GetAtlasTexture(void);\nGLuint Output_Textures_GetEnvMapTexture(void);\n\nint32_t Output_Textures_GetObjectUVWIndex(\n    int32_t texture_idx, int32_t face_idx);\nint32_t Output_Textures_GetSpriteUVWIndex(\n    int32_t texture_idx, int32_t face_idx);\n\nOUTPUT_UVW Output_Textures_GetUVW(int32_t uvw_idx);\nOUTPUT_TEXTURE_SIZE Output_Textures_GetAtlasSize(int32_t uvw_idx);\nbool Output_Textures_IsObjectTextureAnimated(int32_t texture_idx);\nSCENE_PASS Output_Textures_GetObjectTextureScenePass(int32_t texture_idx);\nbool Output_Textures_IsSpriteTextureAnimated(int32_t sprite_idx);\nuint16_t Output_Textures_GetSpriteTextureFlags(int32_t sprite_idx);\nvoid Output_Textures_SetSpriteTextureFlags(int32_t sprite_idx, uint16_t flags);\n\n// Public utility methods =====================================================\n\nvoid Output_InitialiseTexturePages(int32_t num_pages, bool use_8bit);\nvoid Output_InitialisePalettes(\n    int32_t palette_size, const RGB_888 *palette_8, const RGB_888 *palette_16);\nvoid Output_InitialiseObjectTextures(int32_t num_textures);\nvoid Output_InitialiseSpriteTextures(int32_t num_textures);\nvoid Output_InitialiseAnimatedTextures(int32_t num_ranges);\n\nint32_t Output_GetTexturePageCount(void);\nuint8_t *Output_GetTexturePage8(int32_t page_idx);\nRGBA_8888 *Output_GetTexturePage32(int32_t page_idx);\n\nvoid Output_LockTexturePage32(int32_t page_idx);\nvoid Output_UnlockTexturePage32(int32_t page_idx);\n\nint32_t Output_GetPaletteSize(void);\nRGB_888 Output_GetPaletteColor8(uint16_t idx);\nRGB_888 Output_GetPaletteColor16(uint16_t idx);\nLIGHT_MAP *Output_GetLightMap(uint8_t idx);\nSHADE_MAP *Output_GetShadeMap(uint8_t idx);\nint32_t Output_GetObjectTextureCount(void);\nint32_t Output_GetSpriteTextureCount(void);\nOBJECT_TEXTURE *Output_GetObjectTexture(int32_t texture_idx);\nSPRITE_TEXTURE *Output_GetSpriteTexture(int32_t texture_idx);\nANIMATED_TEXTURE_RANGE *Output_GetAnimatedTextureRange(int32_t range_idx);\n\nRGBA_8888 Output_RGB2RGBA(RGB_888 color);\nRGBA_F Output_RGB2RGBA_F(RGB_F color);\nint16_t Output_FindColor8(RGB_888 color);\nvoid Output_CycleAnimatedTextures(void);\n"
  },
  {
    "path": "src/trx/game/output/types.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/math/types.h>\n#include <trx/game/output/const.h>\n\n#include <stdint.h>\n\ntypedef enum {\n    CLIP_NOT_VISIBLE = 0,\n    CLIP_PARTIALLY_VISIBLE = -1,\n    CLIP_FULLY_VISIBLE = 1,\n} CLIP;\n\ntypedef enum {\n    TS_HEADING,\n    TS_BACKGROUND,\n    TS_BACKGROUND_HEAVY,\n    TS_REQUESTED,\n} TEXT_STYLE;\n\ntypedef enum {\n    DRAW_OPAQUE = 0,\n    DRAW_BLEND = 1,\n    DRAW_BLEND_ADD = 2,\n    DRAW_BLEND_SUB = 3,\n    DRAW_REFLECTIVE_OPAQUE = 8,\n    DRAW_REFLECTIVE_BLEND_ADD = 9,\n} DRAW_TYPE;\n\ntypedef struct {\n    int16_t value_1;\n    int16_t value_2;\n} SHADE;\n\ntypedef struct {\n    int32_t value_1;\n    int32_t value_2;\n} FALLOFF;\n\ntypedef struct {\n    uint16_t u;\n    uint16_t v;\n} TEXTURE_UV;\n\ntypedef struct {\n    union {\n        struct {\n            float z;\n            float w;\n        };\n        float zw[2];\n    };\n} TEXTURE_ZW_F;\n\ntypedef struct {\n    uint16_t draw_type;\n    uint16_t tex_page;\n    int32_t uv_count;\n    TEXTURE_UV uv[4];\n} OBJECT_TEXTURE;\n\ntypedef struct {\n    uint16_t tex_page;\n    uint16_t offset;\n    uint16_t width;\n    uint16_t height;\n    int16_t x0;\n    int16_t y0;\n    int16_t x1;\n    int16_t y1;\n    uint16_t flags;\n} SPRITE_TEXTURE;\n\ntypedef struct ANIMATED_TEXTURE_RANGE {\n    int16_t num_textures;\n    int16_t *textures;\n    struct ANIMATED_TEXTURE_RANGE *next_range;\n} ANIMATED_TEXTURE_RANGE;\n\ntypedef struct {\n    uint8_t index[256];\n} LIGHT_MAP;\n\ntypedef struct {\n    uint8_t index[LIGHT_MAP_SIZE];\n} SHADE_MAP;\n\ntypedef struct {\n    XYZ_32 from;\n    XYZ_32 to;\n    int32_t thickness;\n} LIGHTNING_SEGMENT;\n"
  },
  {
    "path": "src/trx/game/output/uniforms.c",
    "content": "#define M_MAX_LIGHTS 32\n\n#include <trx/game/output/uniforms.h>\n\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/output.h>\n#include <trx/game/output/utils.h>\n#include <trx/game/rooms.h>\n#include <trx/gl/utils.h>\n#include <trx/version.h>\n\n#include <math.h>\n#include <string.h>\n\n#define M_GLOBAL_MEMBERS                                                       \\\n    X_DECLARE_MEMBER(float, global_tint, [4])                                  \\\n    X_DECLARE_MEMBER(float, fog_color, [4])                                    \\\n    X_DECLARE_MEMBER(float, fog_distance, [2])                                 \\\n    X_DECLARE_MEMBER(float, viewport_size, [2])                                \\\n    X_DECLARE_MEMBER(float, time)                                              \\\n    X_DECLARE_MEMBER(float, time_in_game)                                      \\\n    X_DECLARE_MEMBER(float, brightness_multiplier)                             \\\n    X_DECLARE_MEMBER(float, ui_brightness_multiplier)                          \\\n    X_DECLARE_MEMBER(float, gamma)                                             \\\n    X_DECLARE_MEMBER(float, desaturation)                                      \\\n    X_DECLARE_MEMBER(float, sunset_duration)                                   \\\n    X_DECLARE_MEMBER(float, min_shade)                                         \\\n    X_DECLARE_MEMBER(int, billboard_lock_mode)                                 \\\n    X_DECLARE_MEMBER(int, lighting_enabled)                                    \\\n    X_DECLARE_MEMBER(int, trapezoid_filter_enabled)                            \\\n    X_DECLARE_MEMBER(int, reflections_enabled)                                 \\\n    X_DECLARE_MEMBER(int, textures_enabled)                                    \\\n    X_DECLARE_MEMBER(int, tr_version)\n\n#pragma pack(push, 4)\ntypedef struct {\n#define X_DECLARE_MEMBER(a, b, ...) a b __VA_ARGS__;\n    M_GLOBAL_MEMBERS\n#undef X_DECLARE_MEMBER\n} M_UNIFORM_GENERAL;\n\ntypedef struct {\n    float mat_proj[4][4];\n    float mat_view[4][4];\n} M_UNIFORM_MATRICES;\n\ntypedef struct {\n    float pos[4];\n    float color[4];\n    float shade;\n    float falloff;\n    float kind;\n    float _pad;\n} M_UNIFORM_LIGHT;\n\ntypedef struct {\n    int num_lights;\n    int room_light_mode;\n    int _pad[2];\n    M_UNIFORM_LIGHT lights[M_MAX_LIGHTS];\n} M_UNIFORM_LIGHTS;\n\ntypedef struct {\n    float adder;\n    float divider;\n    float _pad0[2];\n    float vector_view[4];\n    float tr3_ambient[4];\n    float tr3_light_dir_view[3][4];\n    float tr3_light_color[3][4];\n} M_UNIFORM_LS;\n#pragma pack(pop)\n\ntypedef enum {\n    M_LS_MODE_NONE = 0,\n    M_LS_MODE_FULL = 1,\n    M_LS_MODE_OWN = 2,\n} M_LS_MODE;\n\ntypedef struct {\n    M_UNIFORM_LIGHTS last_lights;\n    OUTPUT_LIGHT_INFO last_light_info;\n    M_LS_MODE last_ls_mode;\n    int32_t last_own_light_adder;\n    RGB_F last_own_light_tr3_ambient;\n} M_PRIV;\n\nstatic void M_FillLight(\n    M_UNIFORM_LIGHT *const dst_light, const LIGHT *const src_light)\n{\n    dst_light->pos[0] = src_light->pos.x;\n    dst_light->pos[1] = src_light->pos.y;\n    dst_light->pos[2] = src_light->pos.z;\n    dst_light->pos[3] = 0.0f;\n    dst_light->color[0] = src_light->color.r / 255.0f;\n    dst_light->color[1] = src_light->color.g / 255.0f;\n    dst_light->color[2] = src_light->color.b / 255.0f;\n    dst_light->color[3] = 0.0f;\n    dst_light->shade = src_light->shade.value_1;\n    dst_light->falloff = src_light->falloff.value_1;\n    dst_light->kind = src_light->type;\n}\n\nstatic int16_t M_GetMinShade(void)\n{\n    switch (g_Config.rendering.lighting_contrast) {\n    case LIGHTING_CONTRAST_LOW:\n        return SHADE_NEUTRAL;\n    case LIGHTING_CONTRAST_MEDIUM:\n        return SHADE_HIGH;\n    case LIGHTING_CONTRAST_HIGH:\n        return 0;\n    default:\n        return SHADE_NEUTRAL;\n    }\n}\n\nvoid Output_Uniforms_UploadOrthoMatrix(const OUTPUT_UNIFORMS *const uniforms)\n{\n    M_UNIFORM_MATRICES matrices = {};\n    Output_GetOrthoProjectionMatrix(matrices.mat_proj);\n    Output_FillMatrix(matrices.mat_view, &g_IDMatrix);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->matrices);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(matrices), &matrices);\n}\n\nvoid Output_Uniforms_UploadViewMatrix(\n    const OUTPUT_UNIFORMS *const uniforms, const MATRIX *const matrix)\n{\n    M_UNIFORM_MATRICES matrices = {};\n    Output_GetPerspProjectionMatrix(matrices.mat_proj);\n    Output_FillMatrix(matrices.mat_view, matrix);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->matrices);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(matrices), &matrices);\n}\n\nvoid Output_Uniforms_UploadGeneral(const OUTPUT_UNIFORMS *const uniforms)\n{\n    M_UNIFORM_GENERAL general = {\n        .time = Output_GetTime(),\n        .time_in_game = Output_GetTimeInGame(),\n        .brightness_multiplier = g_Config.visuals.game_brightness,\n        .ui_brightness_multiplier = g_Config.visuals.ui_brightness,\n        .gamma = g_Config.visuals.gamma,\n        .desaturation = Output_GetDesaturation(),\n        .sunset_duration = Output_GetSunsetDuration(),\n        .tr_version = g_TRVersion,\n        .viewport_size = {\n            (float)Viewport_GetWidth(VIEWPORT_GAME),\n            (float)Viewport_GetHeight(VIEWPORT_GAME),\n        },\n        .min_shade = M_GetMinShade(),\n        .billboard_lock_mode = g_Config.rendering.sprite_lock_mode,\n        .lighting_enabled = g_Config.rendering.enable_lighting,\n        .textures_enabled = g_Config.rendering.enable_textures,\n        .trapezoid_filter_enabled = g_Config.rendering.enable_trapezoid_filter,\n        .reflections_enabled = g_Config.visuals.enable_reflections,\n        .fog_distance = {Output_GetFogStart(), Output_GetFogEnd()},\n        .fog_color = {\n            Output_GetFogColor().r,\n            Output_GetFogColor().g,\n            Output_GetFogColor().b,\n            Output_GetFogColor().a,\n        },\n        .global_tint = {\n            Output_GetGlobalTint().r,\n            Output_GetGlobalTint().g,\n            Output_GetGlobalTint().b,\n            1.0f,\n        },\n    };\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(general), &general);\n}\n\nvoid Output_Uniforms_UploadFogDistance(\n    const OUTPUT_UNIFORMS *const uniforms, const float start, const float end)\n{\n    ASSERT(uniforms != nullptr);\n    const float fog_distance[2] = { start, end };\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER,\n        offsetof(M_UNIFORM_GENERAL, fog_distance), sizeof(fog_distance),\n        &fog_distance);\n}\n\nvoid Output_Uniforms_UploadDesaturation(\n    const OUTPUT_UNIFORMS *const uniforms, const float desaturation)\n{\n    ASSERT(uniforms != nullptr);\n    float clamped = desaturation;\n    CLAMP(clamped, 0.0f, 1.0f);\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER,\n        offsetof(M_UNIFORM_GENERAL, desaturation), sizeof(clamped), &clamped);\n}\n\nvoid Output_Uniforms_UploadGlobalTint(\n    const OUTPUT_UNIFORMS *const uniforms, const RGB_F tint)\n{\n    ASSERT(uniforms != nullptr);\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER,\n        offsetof(M_UNIFORM_GENERAL, global_tint), sizeof(tint), &tint);\n}\n\nvoid Output_Uniforms_UploadGameBrightnessMultiplier(\n    const OUTPUT_UNIFORMS *const uniforms,\n    const float game_brightness_multiplier)\n{\n    ASSERT(uniforms != nullptr);\n\n    float clamped = game_brightness_multiplier;\n    CLAMP(clamped, CONFIG_MIN_BRIGHTNESS, CONFIG_MAX_BRIGHTNESS);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER,\n        offsetof(M_UNIFORM_GENERAL, brightness_multiplier), sizeof(clamped),\n        &clamped);\n}\n\nvoid Output_Uniforms_UploadUIBrightnessMultiplier(\n    const OUTPUT_UNIFORMS *const uniforms, const float brightness_multiplier)\n{\n    ASSERT(uniforms != nullptr);\n\n    float clamped = brightness_multiplier;\n    CLAMP(clamped, CONFIG_MIN_BRIGHTNESS, CONFIG_MAX_BRIGHTNESS);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER,\n        offsetof(M_UNIFORM_GENERAL, ui_brightness_multiplier), sizeof(clamped),\n        &clamped);\n}\n\nvoid Output_Uniforms_UploadRoomLights(\n    const OUTPUT_UNIFORMS *const uniforms, const ROOM *const room)\n{\n    M_UNIFORM_LIGHTS lights = {};\n\n    // Only dynamic lights for now.\n    M_UNIFORM_LIGHT *dst_light = lights.lights;\n    if (room == nullptr) {\n        lights.room_light_mode = RLM_SUNSET;\n    } else {\n        lights.room_light_mode = room->light_mode;\n    }\n    VECTOR *const dynamic_lights = Output_GetDynamicLights();\n    for (int32_t i = 0; i < dynamic_lights->count; i++) {\n        M_FillLight(dst_light, Vector_Get(dynamic_lights, i));\n        dst_light++;\n        if (dst_light - lights.lights >= M_MAX_LIGHTS) {\n            break;\n        }\n    }\n\n    lights.num_lights = dst_light - lights.lights;\n    const size_t size = offsetof(M_UNIFORM_LIGHTS, lights)\n        + lights.num_lights * sizeof(M_UNIFORM_LIGHT);\n\n    M_PRIV *const p = uniforms->priv;\n    if (memcmp(&p->last_lights, &lights, sizeof(lights)) == 0) {\n        return;\n    }\n    memcpy(&p->last_lights, &lights, sizeof(lights));\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->lights);\n    TRX_GL_TRACK_SUBDATA(glBufferSubData, GL_UNIFORM_BUFFER, 0, size, &lights);\n}\n\nvoid Output_Uniforms_UploadCPULight(\n    const OUTPUT_UNIFORMS *const uniforms, const OUTPUT_LIGHT_INFO *const info)\n{\n    M_PRIV *const p = uniforms->priv;\n    if (p->last_ls_mode == M_LS_MODE_FULL\n        && memcmp(&p->last_light_info, info, sizeof(*info)) == 0) {\n        return;\n    }\n    memcpy(&p->last_light_info, info, sizeof(*info));\n    p->last_own_light_adder = info->ls_adder;\n    p->last_own_light_tr3_ambient = info->tr3_ambient;\n    p->last_ls_mode = M_LS_MODE_FULL;\n\n    M_UNIFORM_LS ls = {};\n    ls.adder = info->ls_adder;\n    ls.divider = info->ls_divider / (float)(1 << (W2V_SHIFT));\n    ls.vector_view[0] = info->ls_vector_view.x;\n    ls.vector_view[1] = info->ls_vector_view.y;\n    ls.vector_view[2] = info->ls_vector_view.z;\n    ls.vector_view[3] = 0;\n    ls.tr3_ambient[0] = info->tr3_ambient.r;\n    ls.tr3_ambient[1] = info->tr3_ambient.g;\n    ls.tr3_ambient[2] = info->tr3_ambient.b;\n    ls.tr3_ambient[3] = 0.0f;\n    for (int32_t i = 0; i < 3; i++) {\n        float x = (float)info->tr3_light_dir_view[i].x;\n        float y = (float)info->tr3_light_dir_view[i].y;\n        float z = (float)info->tr3_light_dir_view[i].z;\n        const float len2 = x * x + y * y + z * z;\n        if (len2 > 0.0f) {\n            const float inv_len = 1.0f / sqrtf(len2);\n            x *= inv_len;\n            y *= inv_len;\n            z *= inv_len;\n        }\n        ls.tr3_light_dir_view[i][0] = x;\n        ls.tr3_light_dir_view[i][1] = y;\n        ls.tr3_light_dir_view[i][2] = z;\n        ls.tr3_light_dir_view[i][3] = 0.0f;\n        ls.tr3_light_color[i][0] = info->tr3_light_color[i].r;\n        ls.tr3_light_color[i][1] = info->tr3_light_color[i].g;\n        ls.tr3_light_color[i][2] = info->tr3_light_color[i].b;\n        ls.tr3_light_color[i][3] = 0.0f;\n    }\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls);\n    TRX_GL_TRACK_SUBDATA(\n        glBufferSubData, GL_UNIFORM_BUFFER, 0, sizeof(ls), &ls);\n    TRX_GL_CheckError();\n}\n\nvoid Output_Uniforms_UploadOwnLight(\n    const OUTPUT_UNIFORMS *const uniforms, const OUTPUT_LIGHT_INFO *const info)\n{\n    M_PRIV *const priv = uniforms->priv;\n\n    if (g_TRVersion >= 3) {\n        if (priv->last_ls_mode == M_LS_MODE_OWN\n            && priv->last_own_light_tr3_ambient.r == info->tr3_ambient.r\n            && priv->last_own_light_tr3_ambient.g == info->tr3_ambient.g\n            && priv->last_own_light_tr3_ambient.b == info->tr3_ambient.b) {\n            return;\n        }\n\n        const float ambient[4] = {\n            info->tr3_ambient.r,\n            info->tr3_ambient.g,\n            info->tr3_ambient.b,\n            0.0f,\n        };\n        glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls);\n        TRX_GL_TRACK_SUBDATA(\n            glBufferSubData, GL_UNIFORM_BUFFER,\n            offsetof(M_UNIFORM_LS, tr3_ambient), sizeof(ambient), ambient);\n        priv->last_own_light_tr3_ambient = info->tr3_ambient;\n    } else {\n        if (priv->last_ls_mode == M_LS_MODE_OWN\n            && priv->last_own_light_adder == info->ls_adder) {\n            return;\n        }\n\n        const float light_adder = info->ls_adder;\n        glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls);\n        TRX_GL_TRACK_SUBDATA(\n            glBufferSubData, GL_UNIFORM_BUFFER, offsetof(M_UNIFORM_LS, adder),\n            sizeof(light_adder), &light_adder);\n        priv->last_own_light_adder = info->ls_adder;\n    }\n    priv->last_ls_mode = M_LS_MODE_OWN;\n}\n\nOUTPUT_UNIFORMS *Output_Uniforms_Create(void)\n{\n    OUTPUT_UNIFORMS *const uniforms =\n        Memory_Alloc(sizeof(OUTPUT_UNIFORMS) + sizeof(M_PRIV));\n    glGenBuffers(4, &uniforms->general);\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->general);\n    glBufferData(\n        GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_GENERAL), nullptr, GL_DYNAMIC_DRAW);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniforms->general);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->matrices);\n    glBufferData(\n        GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_MATRICES), nullptr,\n        GL_DYNAMIC_DRAW);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 1, uniforms->matrices);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->lights);\n    glBufferData(\n        GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_LIGHTS), nullptr, GL_DYNAMIC_DRAW);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniforms->lights);\n\n    glBindBuffer(GL_UNIFORM_BUFFER, uniforms->ls);\n    glBufferData(\n        GL_UNIFORM_BUFFER, sizeof(M_UNIFORM_LS), nullptr, GL_DYNAMIC_DRAW);\n    glBindBufferBase(GL_UNIFORM_BUFFER, 3, uniforms->ls);\n    TRX_GL_CheckError();\n\n    uniforms->priv = (char *)uniforms + sizeof(OUTPUT_UNIFORMS);\n    return uniforms;\n}\n\nvoid Output_Uniforms_Free(OUTPUT_UNIFORMS *const uniforms)\n{\n    if (uniforms == nullptr) {\n        return;\n    }\n    if (uniforms->general != 0) {\n        glDeleteBuffers(4, &uniforms->general);\n        uniforms->general = 0;\n        uniforms->matrices = 0;\n        uniforms->lights = 0;\n        uniforms->ls = 0;\n    }\n    Memory_Free(uniforms);\n}\n"
  },
  {
    "path": "src/trx/game/output/uniforms.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/game/matrix.h>\n#include <trx/game/rooms/types.h>\n\n#include <GL/glew.h>\n\ntypedef struct {\n    int32_t ls_adder;\n    int32_t ls_divider;\n    XYZ_32 ls_vector_view;\n    RGB_F tr3_ambient;\n    RGB_F tr3_light_color[3];\n    XYZ_32 tr3_light_dir_view[3];\n} OUTPUT_LIGHT_INFO;\n\ntypedef struct {\n    GLuint general;\n    GLuint matrices;\n    GLuint lights;\n    GLuint ls;\n    void *priv;\n} OUTPUT_UNIFORMS;\n\nOUTPUT_UNIFORMS *Output_Uniforms_Create(void);\nvoid Output_Uniforms_Free(OUTPUT_UNIFORMS *uniforms);\n\nvoid Output_Uniforms_UploadGeneral(const OUTPUT_UNIFORMS *uniforms);\nvoid Output_Uniforms_UploadOrthoMatrix(const OUTPUT_UNIFORMS *uniforms);\nvoid Output_Uniforms_UploadViewMatrix(\n    const OUTPUT_UNIFORMS *uniforms, const MATRIX *matrix);\nvoid Output_Uniforms_UploadRoomLights(\n    const OUTPUT_UNIFORMS *uniforms, const ROOM *room);\nvoid Output_Uniforms_UploadCPULight(\n    const OUTPUT_UNIFORMS *uniforms, const OUTPUT_LIGHT_INFO *info);\nvoid Output_Uniforms_UploadOwnLight(\n    const OUTPUT_UNIFORMS *uniforms, const OUTPUT_LIGHT_INFO *info);\n\nvoid Output_Uniforms_UploadFogDistance(\n    const OUTPUT_UNIFORMS *uniforms, float start, float end);\nvoid Output_Uniforms_UploadDesaturation(\n    const OUTPUT_UNIFORMS *uniforms, float desaturation);\nvoid Output_Uniforms_UploadGlobalTint(\n    const OUTPUT_UNIFORMS *uniforms, RGB_F tint);\n"
  },
  {
    "path": "src/trx/game/output/utils.c",
    "content": "#include <trx/game/output/utils.h>\n\n#include <trx/game/const.h>\n\nvoid Output_FillMatrix(GLfloat m[4][4], const MATRIX *const source)\n{\n    m[0][0] = source->_00 / (float)(1 << W2V_SHIFT);\n    m[0][1] = source->_10 / (float)(1 << W2V_SHIFT);\n    m[0][2] = source->_20 / (float)(1 << W2V_SHIFT);\n    m[0][3] = 0.0;\n\n    m[1][0] = source->_01 / (float)(1 << W2V_SHIFT);\n    m[1][1] = source->_11 / (float)(1 << W2V_SHIFT);\n    m[1][2] = source->_21 / (float)(1 << W2V_SHIFT);\n    m[1][3] = 0.0;\n\n    m[2][0] = source->_02 / (float)(1 << W2V_SHIFT);\n    m[2][1] = source->_12 / (float)(1 << W2V_SHIFT);\n    m[2][2] = source->_22 / (float)(1 << W2V_SHIFT);\n    m[2][3] = 0.0;\n\n    m[3][0] = source->_03 / (float)(1 << W2V_SHIFT);\n    m[3][1] = source->_13 / (float)(1 << W2V_SHIFT);\n    m[3][2] = source->_23 / (float)(1 << W2V_SHIFT);\n    m[3][3] = 1.0;\n}\n"
  },
  {
    "path": "src/trx/game/output/utils.h",
    "content": "#pragma once\n\n#include <trx/game/matrix.h>\n\n#include <GL/glew.h>\n\n#define OUTPUT_QUAD_VERTICES 6\n#define OUTPUT_TRI_VERTICES 3\n\n#define OUTPUT_TRI_TO_FAN(i) ((int32_t[]) { 0, 2, 1 }[i])\n// |＼|\n#define OUTPUT_QUAD_TO_FAN(i) ((int32_t[]) { 0, 2, 1, 0, 3, 2 }[i])\n// |＼| Opposite winding\n#define OUTPUT_QUAD_TO_FAN_CW(i) ((int32_t[]) { 0, 1, 2, 0, 2, 3 }[i])\n// |／|\n#define OUTPUT_QUAD_TO_FAN_BACK(i) ((int32_t[]) { 0, 3, 1, 1, 3, 2 }[i])\n\n#define L_ATI_FIX 1\n\n#if L_ATI_FIX\n    // Evergreen‐generation GPUs (HD 5000/6000 – such as HD 5570 – have a\n    // long-standing driver/firm-ware quirk: an integer vertex attribute whose\n    // storage size is smaller than 32 bit is fetched as if it were 32-bit,\n    // even when you declare it as 8- or 16-bit in glVertexAttribIPointer. The\n    // call succeeds, no error is raised, but the hardware fetch unit still\n    // steps four bytes, not one or two. If the stride you give is tighter than\n    // that, the fetch unit starts reading the next vertex in the middle of the\n    // current one and every attribute that follows is garbage – positions turn\n    // into huge values, so the whole mesh explodes. NVIDIA, Intel and all\n    // post-GCN AMD GPUs fixed the bug.\n    //\n    // To deal with this, we simply pack our data to increments of 4.\n    #define OUTPUT_USHORT uint32_t\n    #define OUTPUT_USHORT_GL GL_UNSIGNED_INT\n    #define OUTPUT_SHORT int32_t\n    #define OUTPUT_SHORT_GL GL_INT\n#else\n    #define OUTPUT_SHORT int16_t\n    #define OUTPUT_SHORT_GL GL_SHORT\n    #define OUTPUT_USHORT uint16_t\n    #define OUTPUT_USHORT_GL GL_UNSIGNED_SHORT\n#endif\n\n#undef L_ATI_FIX\n\nvoid Output_FillMatrix(GLfloat m[4][4], const MATRIX *source);\n"
  },
  {
    "path": "src/trx/game/output/vars.c",
    "content": "#include <trx/game/output/vars.h>\n\nint32_t g_PhdPersp = 0;\nint32_t g_PhdLeft = 0;\nint32_t g_PhdBottom = 0;\nint32_t g_PhdRight = 0;\nint32_t g_PhdTop = 0;\n"
  },
  {
    "path": "src/trx/game/output/vars.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nextern int32_t g_PhdPersp;\nextern int32_t g_PhdLeft;\nextern int32_t g_PhdBottom;\nextern int32_t g_PhdRight;\nextern int32_t g_PhdTop;\n"
  },
  {
    "path": "src/trx/game/output/vertex_range.c",
    "content": "#include <trx/game/output/vertex_range.h>\n\n#include <trx/debug.h>\n\n#include <stdlib.h>\n\nstatic int M_CompareRanges(const void *const a, const void *const b)\n{\n    const OUTPUT_VERTEX_RANGE *const range_a = (OUTPUT_VERTEX_RANGE *)a;\n    const OUTPUT_VERTEX_RANGE *const range_b = (OUTPUT_VERTEX_RANGE *)b;\n    return range_a->vertex_start - range_b->vertex_start;\n}\n\nvoid Output_GlueVertexRanges(VECTOR *const target)\n{\n    ASSERT(target != nullptr);\n    if (target->count == 0) {\n        return;\n    }\n\n    OUTPUT_VERTEX_RANGE *const ranges =\n        (OUTPUT_VERTEX_RANGE *)Vector_Get(target, 0);\n\n    qsort(ranges, target->count, sizeof(OUTPUT_VERTEX_RANGE), M_CompareRanges);\n\n    // Initialize a new index to store the merged ranges\n    int32_t new_range_count = 0;\n\n    // Iterate over sorted ranges and merge them\n    for (int32_t i = 0; i < target->count; i++) {\n        if (new_range_count == 0) {\n            // First range - just copy it\n            ranges[new_range_count] = ranges[i];\n            new_range_count++;\n        } else {\n            // Check if the previous range can be merged with the current one\n            OUTPUT_VERTEX_RANGE *const last_range =\n                &ranges[new_range_count - 1];\n            const int32_t last_start = last_range->vertex_start;\n            const int32_t last_end =\n                last_range->vertex_start + last_range->vertex_count;\n            const int32_t current_start = ranges[i].vertex_start;\n            const int32_t current_end =\n                ranges[i].vertex_start + ranges[i].vertex_count;\n\n            if (current_start >= last_start && current_start <= last_end) {\n                last_range->vertex_count =\n                    current_end - last_range->vertex_start;\n            } else if (current_end >= last_start && current_end <= last_end) {\n                last_range->vertex_start = ranges[i].vertex_start;\n            } else {\n                ranges[new_range_count++] = ranges[i];\n            }\n        }\n    }\n\n    // Update the range vertex_count with the new number of merged ranges\n    target->count = new_range_count;\n}\n"
  },
  {
    "path": "src/trx/game/output/vertex_range.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n\ntypedef struct {\n    int32_t vertex_start;\n    int32_t vertex_count;\n} OUTPUT_VERTEX_RANGE;\n\nvoid Output_GlueVertexRanges(VECTOR *vertex_range);\n"
  },
  {
    "path": "src/trx/game/output.h",
    "content": "#pragma once\n\n#include <trx/game/output/common.h>\n#include <trx/game/output/const.h>\n#include <trx/game/output/draw.h>\n#include <trx/game/output/func.h>\n#include <trx/game/output/lights.h>\n#include <trx/game/output/overlay.h>\n#include <trx/game/output/scene_compositor.h>\n#include <trx/game/output/state.h>\n#include <trx/game/output/textures.h>\n#include <trx/game/output/types.h>\n#include <trx/game/output/vars.h>\n"
  },
  {
    "path": "src/trx/game/overlay.c",
    "content": "#include <trx/game/overlay.h>\n\n#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/const.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/gym.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/inventory.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/output/sources/ui.h>\n#include <trx/game/savegame.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/version.h>\n\n#define M_MAX_PICKUP_DURATION_DISPLAY (LOGIC_FPS * 2)\n#define M_MAX_PICKUP_DURATION_EASE_IN (LOGIC_FPS / 2)\n#define M_MAX_PICKUP_DURATION_EASE_OUT LOGIC_FPS\n\ntypedef enum {\n    DPP_EASE_IN,\n    DPP_DISPLAY,\n    DPP_EASE_OUT,\n    DPP_DEAD,\n} DISPLAY_PICKUP_PHASE;\n\ntypedef struct {\n    DISPLAY_PICKUP_PHASE phase;\n    OBJECT_ID object_id;\n    OUTPUT_UI_PICKUP display;\n    int16_t start_rot;\n    int32_t elapsed;\n    int32_t total_elapsed;\n} DISPLAY_PICKUP;\n\nstatic UI_OVERLAY_STATE *m_UI = nullptr;\nstatic DISPLAY_PICKUP m_Pickups[OUTPUT_UI_MAX_PICKUPS] = {};\nstatic bool m_PickupsActive;\n\nstatic const RGBA_F m_WhiteTextColor[4] = {\n    { 1.0f, 1.0f, 1.0f, 1.0f },\n    { 1.0f, 1.0f, 1.0f, 1.0f },\n    { 1.0f, 1.0f, 1.0f, 1.0f },\n    { 1.0f, 1.0f, 1.0f, 1.0f },\n};\n\nstatic const RGBA_F m_NeutralTextColor[4] = {\n    { 1.0f, 1.0f, 1.0f, 1.0f },\n    { 1.0f, 1.0f, 1.0f, 1.0f },\n    { 0.25f, 0.25f, 0.25f, 1.0f },\n    { 0.25f, 0.25f, 0.25f, 1.0f },\n};\n\nstatic const RGBA_F m_GreyTextColor[4] = {\n    { 0.5f, 0.5f, 0.5f, 1.0f },\n    { 0.5f, 0.5f, 0.5f, 1.0f },\n    { 0.1f, 0.1f, 0.1f, 1.0f },\n    { 0.1f, 0.1f, 0.1f, 1.0f },\n};\n\nstatic const RGBA_F m_GreenTextColor[4] = {\n    { 0.35f, 0.75f, 0.2f, 1.0f },\n    { 0.35f, 0.75f, 0.2f, 1.0f },\n    { 0.1f, 0.25f, 0.0f, 1.0f },\n    { 0.1f, 0.25f, 0.0f, 1.0f },\n};\n\nstatic const RGBA_F m_RedTextColor[4] = {\n    { 0.9f, 0.2f, 0.0f, 1.0f },\n    { 0.9f, 0.2f, 0.0f, 1.0f },\n    { 0.3f, 0.0f, 0.0f, 1.0f },\n    { 0.3f, 0.0f, 0.0f, 1.0f },\n};\n\nstatic const RGBA_F m_PinkTextColor[4] = {\n    { 1.0f, 0.0f, 1.0f, 1.0f },\n    { 1.0f, 0.0f, 1.0f, 1.0f },\n    { 0.25f, 0.0f, 0.25f, 1.0f },\n    { 0.25f, 0.0f, 0.25f, 1.0f },\n};\n\nstatic const char *M_FormatAssaultTimeText(\n    const int32_t frames, const bool placeholder)\n{\n    if (placeholder && frames <= 0) {\n        return \"--:--.-\";\n    }\n    const int32_t total_sec = frames / LOGIC_FPS;\n    const int32_t frame = frames % LOGIC_FPS;\n    return String_FormatStatic(\n        \"%d:%02d.%d\", total_sec / 60, total_sec % 60, frame * 10 / LOGIC_FPS);\n}\n\nstatic int32_t M_DrawAssaultTimerText(\n    const OBJECT *const digits_obj, const char *const text, int32_t x,\n    const int32_t y, const RGBA_F color[4])\n{\n    const int32_t scale_h =\n        UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t scale_v =\n        UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d0 = UI_Scaler_Calc(-6, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d1 = UI_Scaler_Calc(14, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d2 = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS);\n\n    for (const char *c = text; *c != '\\0'; c++) {\n        if (*c == '-') {\n            x += d2;\n            continue;\n        }\n\n        int32_t mesh_num = 0;\n        int32_t offset = 0;\n        int32_t width = 0;\n        if (*c == ':') {\n            mesh_num = 10;\n            offset = d0;\n            width = d1;\n        } else if (*c == '.') {\n            mesh_num = 11;\n            offset = d0;\n            width = d1;\n        } else {\n            mesh_num = *c - '0';\n            offset = 0;\n            width = d2;\n        }\n\n        x += offset;\n        UI_ScheduleDrawScreenSprite(\n            x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + mesh_num, color);\n        x += width;\n    }\n\n    return x;\n}\n\nstatic int32_t M_MeasureAssaultTimerText(const char *const text)\n{\n    const int32_t d0 = UI_Scaler_Calc(-6, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d1 = UI_Scaler_Calc(14, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d2 = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS);\n\n    int32_t w = 0;\n    for (const char *c = text; *c != '\\0'; c++) {\n        if (*c == ':' || *c == '.') {\n            w += d0 + d1;\n        } else if (*c == '-') {\n            w += d2;\n        } else {\n            w += d2;\n        }\n    }\n    return w;\n}\n\nstatic bool M_IsSprite(const DISPLAY_PICKUP *const pickup)\n{\n    return !g_Config.visuals.enable_3d_pickups\n        || pickup->display.object == nullptr;\n}\n\nstatic float M_Ease(float current, const float start, const float goal)\n{\n    if (start == goal) {\n        return start;\n    } else if (start > goal) {\n        return 1.0f - M_Ease(current, goal, start);\n    } else {\n        CLAMP(current, start, goal);\n        const float ratio = (current - start) / (goal - start);\n        if (ratio < 0.5f) {\n            return 2.0f * SQUARE(ratio);\n        }\n        const float new_ratio = ratio - 1.0f;\n        return 1.0f - 2.0f * SQUARE(new_ratio);\n    }\n}\n\nstatic void M_DrawTrackTimer(const GYM_TRACK_TYPE track_type)\n{\n    if (!Gym_TrackManager_IsTimerDisplay(track_type)) {\n        return;\n    }\n\n    const OBJECT *const digits_obj = Object_Get(O_ASSAULT_DIGITS);\n    if (!digits_obj->loaded) {\n        return;\n    }\n\n    const RESUME_INFO *const resume =\n        Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n    const char *const buffer =\n        M_FormatAssaultTimeText(resume->stats.timer, false);\n\n    const int32_t y = UI_Scaler_Calc(36, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    int32_t x = Viewport_GetCenterX(VIEWPORT_UI)\n        - UI_Scaler_Calc(50, UI_SCALER_TARGET_ASSAULT_DIGITS);\n\n    M_DrawAssaultTimerText(\n        digits_obj, buffer, x, y,\n        g_TRVersion < 3 ? m_WhiteTextColor : m_NeutralTextColor);\n}\n\nstatic void M_DrawAssaultPenalties(\n    const GYM_TRACK_TYPE track_type, const bool is_target_penalty)\n{\n    if (!Gym_TrackManager_IsTimerDisplay(track_type)\n        || Gym_TrackManager_GetPenaltyDisplayTimer(track_type) <= 0) {\n        return;\n    }\n\n    const OBJECT *const digits_obj = Object_Get(O_ASSAULT_DIGITS);\n    if (!digits_obj->loaded) {\n        return;\n    }\n\n    const int32_t timer = is_target_penalty\n        ? Gym_TrackManager_GetTargetPenaltyFrames(track_type)\n        : Gym_TrackManager_GetPenaltyFrames(track_type);\n    if (timer <= 0) {\n        return;\n    }\n\n    const int32_t total_sec = timer / LOGIC_FPS;\n    const char *const fmt = is_target_penalty ? \"T %d:%02d s\" : \"%d:%02d s\";\n    const char *const buffer =\n        String_FormatStatic(fmt, total_sec / 60, total_sec % 60);\n\n    const int32_t scale_h =\n        UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t scale_v =\n        UI_Scaler_Calc(PHD_ONE, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t p = UI_Scaler_Calc(1, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d0 = UI_Scaler_Calc(-6, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d1 = UI_Scaler_Calc(14, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d2 = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    const int32_t d3 = UI_Scaler_Calc(8, UI_SCALER_TARGET_ASSAULT_DIGITS);\n\n    int32_t x =\n        Viewport_GetCenterX(VIEWPORT_UI)\n        - UI_Scaler_Calc(\n            is_target_penalty ? 193 : 175, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    int32_t y = UI_Scaler_Calc(36, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    if (is_target_penalty\n        && Gym_TrackManager_GetPenaltyFrames(track_type) != 0) {\n        y = UI_Scaler_Calc(64, UI_SCALER_TARGET_ASSAULT_DIGITS);\n    }\n\n    for (const char *c = buffer; *c != '\\0'; c++) {\n        if (*c == ' ') {\n            x += d3;\n        } else if (*c == 'T') {\n            x += d0;\n            UI_ScheduleDrawScreenSprite(\n                x, y + p, 0, scale_h, scale_v, digits_obj->mesh_idx + 12,\n                g_TRVersion < 3 ? m_WhiteTextColor : m_NeutralTextColor);\n            x += d1 + (p * 2);\n        } else if (*c == 's') {\n            x += d0;\n            UI_ScheduleDrawScreenSprite(\n                x - (p * 4), y, 0, scale_h, scale_v, digits_obj->mesh_idx + 13,\n                m_PinkTextColor);\n            x += d1;\n        } else if (*c == ':') {\n            x += d0;\n            UI_ScheduleDrawScreenSprite(\n                x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + 10,\n                m_PinkTextColor);\n            x += d1;\n        } else if (*c == '.') {\n            x += d0;\n            UI_ScheduleDrawScreenSprite(\n                x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + 11,\n                m_PinkTextColor);\n            x += d1;\n        } else {\n            UI_ScheduleDrawScreenSprite(\n                x, y, 0, scale_h, scale_v, digits_obj->mesh_idx + (*c - '0'),\n                m_PinkTextColor);\n            x += d2;\n        }\n    }\n}\n\nstatic int32_t M_GetBestTrackTime(const GYM_TRACK_TYPE track_type)\n{\n    const GYM_TRACK_STATS *const stats = Gym_TrackManager_GetStats(track_type);\n    return stats->total_attempts > 0 ? (int32_t)stats->entries[0].time : 0;\n}\n\nstatic void M_DrawRacetrackLapTimes(const GYM_TRACK_TYPE track_type)\n{\n    const int32_t last_lap_frames = Gym_TrackManager_GetLapTime(track_type);\n    if (last_lap_frames <= 0) {\n        return;\n    }\n\n    const OBJECT *const digits_obj = Object_Get(O_ASSAULT_DIGITS);\n    if (!digits_obj->loaded) {\n        return;\n    }\n\n    const int32_t best_lap_frames = M_GetBestTrackTime(track_type);\n    const bool is_best_lap =\n        best_lap_frames > 0 && last_lap_frames == best_lap_frames;\n\n    const char *const last_text =\n        M_FormatAssaultTimeText(last_lap_frames, true);\n    const char *const best_text = best_lap_frames > 0\n        ? M_FormatAssaultTimeText(best_lap_frames, true)\n        : \"\";\n\n    const int32_t w_last = M_MeasureAssaultTimerText(last_text);\n    const int32_t w_best = M_MeasureAssaultTimerText(best_text);\n    const int32_t gap = UI_Scaler_Calc(20, UI_SCALER_TARGET_ASSAULT_DIGITS);\n\n    const int32_t cx = Viewport_GetCenterX(VIEWPORT_UI);\n    const int32_t y = UI_Scaler_Calc(36, UI_SCALER_TARGET_ASSAULT_DIGITS);\n\n    int32_t x = cx - w_last / 2;\n    x = M_DrawAssaultTimerText(\n        digits_obj, last_text, x, y,\n        is_best_lap               ? m_GreenTextColor\n            : best_lap_frames > 0 ? m_RedTextColor\n                                  : m_NeutralTextColor);\n    x += gap;\n    M_DrawAssaultTimerText(\n        digits_obj, best_text, x, y,\n        is_best_lap ? m_GreenTextColor : m_GreyTextColor);\n}\n\nstatic void M_DrawPickup2D(const DISPLAY_PICKUP *const pickup)\n{\n    const VIEWPORT_RECT pickup_rect =\n        OutputSource_UI_GetPickupRect(&pickup->display);\n    const int16_t sprite_num = Object_Get(pickup->object_id)->mesh_idx;\n    const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_num);\n    const float sprite_w = ABS(sprite->x1 - sprite->x0);\n    const float sprite_h = ABS(sprite->y1 - sprite->y0);\n    const float scale = MIN(pickup_rect.h / sprite_h, pickup_rect.w / sprite_w);\n    const float scaled_sprite_w = sprite_w * scale;\n    const float scaled_sprite_h = sprite_h * scale;\n    const float x = pickup_rect.x + (pickup_rect.w - scaled_sprite_w) / 2;\n    const float y = pickup_rect.y + (pickup_rect.h - scaled_sprite_h) / 2;\n    OutputSource_UI_StageSprite((OUTPUT_UI_SPRITE) {\n        .sprite_idx = sprite_num,\n        .x0 = x,\n        .y0 = y,\n        .x1 = x + (sprite->x1 - sprite->x0) * scale,\n        .y1 = y + (sprite->y1 - sprite->y0) * scale,\n        .z = Output_GetNearZ_UI(),\n        .shade = SHADE_NEUTRAL,\n        .color = {\n            m_WhiteTextColor[0],\n            m_WhiteTextColor[1],\n            m_WhiteTextColor[2],\n            m_WhiteTextColor[3],\n        },\n    });\n}\n\nstatic void M_DrawPickup3D(const DISPLAY_PICKUP *const pickup)\n{\n    OutputSource_UI_StagePickup(pickup->display);\n}\n\nstatic void M_DrawPickups(void)\n{\n    for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) {\n        DISPLAY_PICKUP *const pickup = &m_Pickups[i];\n\n        int32_t duration = 0;\n        float slide_start = 0.0f;\n        float slide_goal = 0.0f;\n        switch (pickup->phase) {\n        case DPP_DEAD:\n            continue;\n        case DPP_EASE_IN:\n            duration = M_MAX_PICKUP_DURATION_EASE_IN;\n            slide_start = 0.0f;\n            slide_goal = 1.0f;\n            break;\n        case DPP_DISPLAY:\n            duration = M_MAX_PICKUP_DURATION_DISPLAY;\n            slide_start = 1.0f;\n            slide_goal = 1.0f;\n            break;\n        case DPP_EASE_OUT:\n            duration = M_MAX_PICKUP_DURATION_EASE_OUT;\n            slide_start = 1.0f;\n            slide_goal = 0.0f;\n            break;\n        }\n\n        if (M_IsSprite(pickup)) {\n            pickup->display.ease = 1.0f;\n        } else {\n            const float rate = Interpolation_GetRate();\n            pickup->display.rot_y = pickup->start_rot\n                + (4 * DEG_1 * (pickup->total_elapsed + rate));\n            pickup->display.ease = M_Ease(\n                (pickup->elapsed + rate) / (float)duration, slide_start,\n                slide_goal);\n        }\n\n        if (M_IsSprite(pickup)) {\n            M_DrawPickup2D(pickup);\n        } else {\n            M_DrawPickup3D(pickup);\n        }\n    }\n}\n\nstatic void M_AnimatePickups(const int32_t frames)\n{\n    m_PickupsActive = false;\n    for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) {\n        DISPLAY_PICKUP *const pickup = &m_Pickups[i];\n        pickup->elapsed += frames;\n        pickup->total_elapsed += frames;\n        switch (pickup->phase) {\n        case DPP_EASE_IN:\n            if (pickup->elapsed >= M_MAX_PICKUP_DURATION_EASE_IN) {\n                pickup->elapsed = 0;\n                pickup->phase = DPP_DISPLAY;\n            }\n            m_PickupsActive = true;\n            break;\n        case DPP_DISPLAY:\n            if (pickup->elapsed >= M_MAX_PICKUP_DURATION_DISPLAY) {\n                pickup->elapsed = 0;\n                pickup->phase = DPP_EASE_OUT;\n            }\n            m_PickupsActive = true;\n            break;\n        case DPP_EASE_OUT:\n            if (pickup->elapsed >= M_MAX_PICKUP_DURATION_EASE_OUT) {\n                pickup->elapsed = 0;\n                pickup->phase = DPP_DEAD;\n            } else {\n                m_PickupsActive = true;\n            }\n            break;\n        case DPP_DEAD:\n            continue;\n        }\n    }\n}\n\nvoid Overlay_Init(void)\n{\n    if (m_UI == nullptr) {\n        m_UI = UI_Overlay_Init();\n    }\n}\n\nvoid Overlay_Shutdown(void)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay_Free(m_UI);\n        m_UI = nullptr;\n    }\n}\n\nvoid Overlay_Reset(void)\n{\n    for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) {\n        m_Pickups[i].phase = DPP_DEAD;\n    }\n}\n\nvoid Overlay_Control(void)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay_Control(m_UI);\n    }\n}\n\nvoid Overlay_Animate(int32_t frames)\n{\n    if (Game_IsPlaying()) {\n        M_AnimatePickups(frames);\n    }\n}\n\nvoid Overlay_Draw(void)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay(m_UI);\n    }\n}\n\nvoid Overlay_DrawGameInfo(void)\n{\n    if (!Game_IsPlaying()) {\n        return;\n    }\n\n    if (g_Config.ui.show_pickups_overlay && m_PickupsActive) {\n        SceneCompositor_Flush();\n        const int32_t old_fog_start = Output_GetFogStart();\n        const int32_t old_fog_end = Output_GetFogEnd();\n        Output_SetFogStart(20 * WALL_L);\n        Output_SetFogEnd(100 * WALL_L);\n        M_DrawPickups();\n        SceneCompositor_Flush();\n        Output_SetFogStart(old_fog_start);\n        Output_SetFogEnd(old_fog_end);\n    }\n\n    if (Gym_TrackManager_GetLapTimeDisplayTimer(GYM_TRACK_QUAD) > 0) {\n        M_DrawRacetrackLapTimes(GYM_TRACK_QUAD);\n    } else {\n        const GYM_TRACK_TYPE track_type = Gym_TrackManager_GetActiveTrackType();\n        if (track_type == GYM_TRACK_NONE) {\n            return;\n        }\n        M_DrawTrackTimer(track_type);\n        M_DrawAssaultPenalties(track_type, false);\n        M_DrawAssaultPenalties(track_type, true);\n    }\n}\n\nvoid Overlay_ForceHealthBar(const bool show)\n{\n    UI_Overlay_ForceHealthBar(m_UI, show);\n}\n\nvoid Overlay_SetHealthBarTimer(const int16_t timer)\n{\n    UI_LaraHealthBar_SetTimer(timer);\n}\n\nvoid Overlay_ShowArrow(const UI_OVERLAY_ARROW arrow, const bool show)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay_ShowArrow(m_UI, arrow, show);\n    }\n}\n\nvoid Overlay_ShowVersion(const bool show)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay_ShowVersion(m_UI, show);\n    }\n}\n\nvoid Overlay_SetTopText(const OVERLAY_TEXT text)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay_SetTopText(m_UI, text);\n    }\n}\n\nvoid Overlay_SetBottomText(const OVERLAY_TEXT text)\n{\n    if (m_UI != nullptr) {\n        UI_Overlay_SetBottomText(m_UI, text);\n    }\n}\n\nvoid Overlay_AddDisplayPickup(const OBJECT_ID obj_id)\n{\n    if (Object_IsType(obj_id, g_SecretObjects)) {\n        const MUSIC_PLAY_MODE mode =\n            g_Config.audio.fix_secrets_killing_music ? MPM_OVERLAY : MPM_ONCE;\n        Music_Play(MX_SECRET, mode);\n    }\n\n    int32_t grid_x = -1;\n    int32_t grid_y = -1;\n    for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) {\n        const int32_t x = i % OUTPUT_UI_MAX_PICKUP_COLUMNS;\n        const int32_t y = i / OUTPUT_UI_MAX_PICKUP_COLUMNS;\n        bool is_occupied = false;\n        for (int32_t j = 0; j < OUTPUT_UI_MAX_PICKUPS; j++) {\n            DISPLAY_PICKUP *const pickup = &m_Pickups[j];\n            const bool is_dead_or_dying = pickup->phase == DPP_DEAD\n                || (!M_IsSprite(pickup) && pickup->phase == DPP_EASE_OUT);\n            if (pickup->display.grid_x == x && pickup->display.grid_y == y\n                && !is_dead_or_dying) {\n                is_occupied = true;\n                break;\n            }\n        }\n        if (!is_occupied) {\n            grid_x = x;\n            grid_y = y;\n            break;\n        }\n    }\n\n    for (int32_t i = 0; i < OUTPUT_UI_MAX_PICKUPS; i++) {\n        DISPLAY_PICKUP *const pickup = &m_Pickups[i];\n        if (pickup->phase != DPP_DEAD) {\n            continue;\n        }\n        const OBJECT_ID inv_object_id = Inv_GetItemOption(obj_id);\n        const INVENTORY_ITEM *const inv_item = InvRing_GetInvItem(obj_id);\n        pickup->phase = DPP_EASE_IN;\n        pickup->object_id = obj_id;\n        pickup->display.object =\n            inv_object_id != NO_OBJECT ? Object_Get(inv_object_id) : nullptr;\n        pickup->display.grid_x = grid_x;\n        pickup->display.grid_y = grid_y;\n        pickup->start_rot = inv_item != nullptr ? inv_item->y_rot_sel : 0;\n        pickup->elapsed = 0;\n        pickup->total_elapsed = 0;\n        return;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/overlay.h",
    "content": "#pragma once\n\n#include <trx/game/objects/types.h>\n#include <trx/game/ui/hud/overlay.h>\n\ntypedef UI_OVERLAY_TEXT OVERLAY_TEXT;\n\nvoid Overlay_Init(void);\nvoid Overlay_Shutdown(void);\n\nvoid Overlay_Reset(void);\nvoid Overlay_Control(void);\nvoid Overlay_Animate(int32_t num_frames);\nvoid Overlay_Draw(void);\n\nvoid Overlay_DrawGameInfo(void);\nvoid Overlay_AddDisplayPickup(OBJECT_ID obj_id);\n\nvoid Overlay_ForceHealthBar(bool show);\nvoid Overlay_SetHealthBarTimer(int16_t health_bar_timer);\nvoid Overlay_ShowArrow(UI_OVERLAY_ARROW arrow, bool show);\nvoid Overlay_ShowVersion(bool show);\n\nvoid Overlay_SetTopText(OVERLAY_TEXT text);\nvoid Overlay_SetBottomText(OVERLAY_TEXT text);\n"
  },
  {
    "path": "src/trx/game/pathing/box.c",
    "content": "#include <trx/core/utils.h>\n#include <trx/game/creature.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/version.h>\n\n#define BOX_OVERLAP_BITS 0x3FFF\n#define BOX_SEARCH_NUMBER 0x7FFF\n#define BOX_END_BIT 0x8000\n#define BOX_NUMBER_BITS 0x7FFF // = ~BOX_END_BIT\n\n#define BOX_MAX_EXPANSION 5\n#define BOX_BIFF (WALL_L / 2) // = 0x200 = 512\n#define BOX_CLIP_LEFT 1\n#define BOX_CLIP_RIGHT 2\n#define BOX_CLIP_TOP 4\n#define BOX_CLIP_BOTTOM 8\n#define BOX_CLIP_ALL                                                           \\\n    (BOX_CLIP_LEFT | BOX_CLIP_RIGHT | BOX_CLIP_TOP | BOX_CLIP_BOTTOM) // = 15\n#define BOX_CLIP_SECONDARY 16\n\nstatic int32_t m_BoxCount = 0;\nstatic BOX_INFO *m_Boxes = nullptr;\nstatic int16_t *m_Overlaps = nullptr;\nstatic int16_t *m_FlyZone[2] = {};\nstatic int16_t *m_GroundZone[MAX_ZONES][2] = {};\n\nvoid Box_InitialiseBoxes(const int32_t num_boxes)\n{\n    m_BoxCount = num_boxes;\n    m_Boxes = num_boxes == 0\n        ? nullptr\n        : GameBuf_Alloc(sizeof(BOX_INFO) * num_boxes, GBUF_BOXES);\n\n    if (num_boxes == 0) {\n        return;\n    }\n\n    for (int32_t i = 0; i < 2; i++) {\n        for (int32_t j = 0; j < MAX_ZONES; j++) {\n            m_GroundZone[j][i] =\n                GameBuf_Alloc(sizeof(int16_t) * num_boxes, GBUF_GROUND_ZONE);\n        }\n        m_FlyZone[i] =\n            GameBuf_Alloc(sizeof(int16_t) * num_boxes, GBUF_FLY_ZONE);\n    }\n}\n\nint16_t *Box_InitialiseOverlaps(const int32_t num_overlaps)\n{\n    m_Overlaps = num_overlaps == 0\n        ? nullptr\n        : GameBuf_Alloc(sizeof(int16_t) * num_overlaps, GBUF_OVERLAPS);\n    return m_Overlaps;\n}\n\nint32_t Box_GetCount(void)\n{\n    return m_BoxCount;\n}\n\nBOX_INFO *Box_GetBox(const int32_t box_idx)\n{\n    // TODO: in many cases, NO_BOX is blindly passed here and goes unchecked.\n    // Update each instance to handle NO_BOX safely.\n    return m_Boxes == nullptr ? nullptr : &m_Boxes[box_idx];\n}\n\nint16_t Box_GetOverlap(const int32_t overlap_idx)\n{\n    return m_Overlaps == nullptr ? -1 : m_Overlaps[overlap_idx];\n}\n\nint16_t *Box_GetFlyZone(const bool flip_status)\n{\n    return m_FlyZone[flip_status];\n}\n\nint16_t *Box_GetGroundZone(const bool flip_status, const int32_t zone_idx)\n{\n    return m_GroundZone[zone_idx][flip_status];\n}\n\nint16_t *Box_GetLotZone(const LOT_INFO *const lot)\n{\n    const bool flip_status = Room_GetFlipStatus();\n    return lot->setup.fly != 0\n        ? Box_GetFlyZone(flip_status)\n        : Box_GetGroundZone(flip_status, BOX_ZONE(lot->setup.step));\n}\n\nbool Box_SearchLOT(LOT_INFO *const lot, const int32_t expansion)\n{\n    const int16_t *const zone = Box_GetLotZone(lot);\n    const bool use_fixed_fly_zone = g_TRVersion == 3 && lot->setup.fly != 0;\n    const int16_t search_zone =\n        use_fixed_fly_zone ? BOX_FIXED_FLY_ZONE : zone[lot->head];\n\n    for (int32_t i = 0; i < expansion; i++) {\n        if (lot->head == NO_BOX) {\n            if (g_TRVersion >= 2) {\n                lot->tail = NO_BOX;\n            }\n            return false;\n        }\n\n        BOX_NODE *const node = &lot->node[lot->head];\n        const BOX_INFO *const head_box = Box_GetBox(lot->head);\n\n        bool done = false;\n        int32_t index = head_box->overlap_index & BOX_OVERLAP_BITS;\n        while (!done) {\n            int16_t box_num = Box_GetOverlap(index++);\n            if ((box_num & BOX_END_BIT) != 0) {\n                done = true;\n                box_num &= BOX_NUMBER_BITS;\n            }\n\n            if (!use_fixed_fly_zone && search_zone != zone[box_num]) {\n                continue;\n            }\n\n            const BOX_INFO *const box = Box_GetBox(box_num);\n            const int32_t change = box->height - head_box->height;\n            if (change > lot->setup.step || change < lot->setup.drop) {\n                continue;\n            }\n\n            BOX_NODE *const expand = &lot->node[box_num];\n            const int16_t node_search_num =\n                node->search_num & BOX_SEARCH_NUMBER;\n            const int16_t expand_search_num =\n                expand->search_num & BOX_SEARCH_NUMBER;\n            const bool node_search_blocked =\n                (node->search_num & BOX_BLOCKED_SEARCH) != 0;\n            const bool expand_search_blocked =\n                (expand->search_num & BOX_BLOCKED_SEARCH) != 0;\n            if (node_search_num < expand_search_num) {\n                continue;\n            }\n\n            if (node_search_blocked) {\n                if (expand_search_num == node_search_num) {\n                    continue;\n                }\n                expand->search_num = node->search_num;\n            } else {\n                if (expand_search_num == node_search_num\n                    && !expand_search_blocked) {\n                    continue;\n                }\n\n                if ((box->overlap_index & lot->setup.block_mask) != 0) {\n                    expand->search_num = node->search_num | BOX_BLOCKED_SEARCH;\n                } else {\n                    expand->search_num = node->search_num;\n                    expand->exit_box = lot->head;\n                }\n            }\n\n            if (expand->next_expansion == NO_BOX && box_num != lot->tail) {\n                lot->node[lot->tail].next_expansion = box_num;\n                lot->tail = box_num;\n            }\n        }\n\n        lot->head = node->next_expansion;\n        node->next_expansion = NO_BOX;\n    }\n\n    return true;\n}\n\nbool Box_UpdateLOT(LOT_INFO *const lot, const int32_t expansion)\n{\n    if (lot->required_box == NO_BOX || lot->required_box == lot->target_box) {\n        goto end;\n    }\n\n    lot->target_box = lot->required_box;\n    BOX_NODE *const expand = &lot->node[lot->target_box];\n    if (expand->next_expansion == NO_BOX && lot->tail != lot->target_box) {\n        expand->next_expansion = lot->head;\n        if (lot->head == NO_BOX) {\n            lot->tail = lot->target_box;\n        }\n        lot->head = lot->target_box;\n    }\n    lot->search_num++;\n    expand->search_num = lot->search_num;\n    expand->exit_box = NO_BOX;\n\nend:\n    return Box_SearchLOT(lot, expansion);\n}\n\nvoid Box_TargetBox(LOT_INFO *const lot, int16_t box_num)\n{\n    box_num &= BOX_NUMBER_BITS;\n    const BOX_INFO *const box = Box_GetBox(box_num);\n\n    // TODO: determine if the shift is essential\n    const int32_t shift = g_TRVersion >= 2 ? 1 : 0;\n    lot->target.z = box->left + WALL_L / 2\n        + (Random_GetControl() * (box->right + shift - box->left - WALL_L)\n           >> 15);\n    lot->target.x = box->top + WALL_L / 2\n        + (Random_GetControl() * (box->bottom + shift - box->top - WALL_L)\n           >> 15);\n    lot->required_box = box_num;\n    if (lot->setup.fly != 0) {\n        lot->target.y = box->height - STEP_L * 3 / 2;\n    } else {\n        lot->target.y = box->height;\n    }\n}\n\nbool Box_StalkBox(\n    const ITEM *const item, const ITEM *const enemy, const int16_t box_num)\n{\n    if (enemy == nullptr) {\n        return false;\n    }\n\n    const BOX_INFO *const box = Box_GetBox(box_num);\n\n    // TODO: determine if the shift is essential\n    const int32_t shift = g_TRVersion >= 2 ? 1 : 0;\n    const int32_t z = ((box->left + box->right + shift) >> 1) - enemy->pos.z;\n    const int32_t x = ((box->top + box->bottom + shift) >> 1) - enemy->pos.x;\n    const int32_t x_range = g_TRVersion >= 2\n        ? box->bottom + shift - box->top + CREATURE_STALK_DIST\n        : CREATURE_STALK_DIST;\n    const int32_t z_range = g_TRVersion >= 2\n        ? box->right + shift - box->left + CREATURE_STALK_DIST\n        : CREATURE_STALK_DIST;\n    if (x > x_range || x < -x_range || z > z_range || z < -z_range) {\n        return false;\n    }\n\n    const int32_t enemy_quad = (enemy->rot.y >> 14) + 2;\n    const int32_t box_quad = (z > 0) ? ((x > 0) ? DIR_SOUTH : DIR_EAST)\n                                     : ((x > 0) ? DIR_WEST : DIR_NORTH);\n    if (enemy_quad == box_quad) {\n        return false;\n    }\n\n    const int32_t baddie_quad = item->pos.z > enemy->pos.z\n        ? (item->pos.x > enemy->pos.x ? DIR_SOUTH : DIR_EAST)\n        : (item->pos.x > enemy->pos.x ? DIR_WEST : DIR_NORTH);\n\n    return enemy_quad != baddie_quad || ABS(enemy_quad - box_quad) != 2;\n}\n\nbool Box_EscapeBox(\n    const ITEM *item, const ITEM *const enemy, const int16_t box_num)\n{\n    const BOX_INFO *const box = Box_GetBox(box_num);\n\n    // TODO: determine if the shift is essential\n    const int32_t shift = g_TRVersion >= 2 ? 1 : 0;\n    const int32_t x = ((box->top + box->bottom + shift) >> 1) - enemy->pos.x;\n    const int32_t z = ((box->left + box->right + shift) >> 1) - enemy->pos.z;\n    if (x > -CREATURE_ESCAPE_DIST && x < CREATURE_ESCAPE_DIST\n        && z > -CREATURE_ESCAPE_DIST && z < CREATURE_ESCAPE_DIST) {\n        return false;\n    }\n\n    return ((z > 0) == (item->pos.z > enemy->pos.z))\n        || ((x > 0) == (item->pos.x > enemy->pos.x));\n}\n\nbool Box_ValidBox(\n    const ITEM *item, const int16_t zone_num, const int16_t box_num)\n{\n    const CREATURE *const creature = item->creature_data;\n    const int16_t *const zone = Box_GetLotZone(&creature->lot);\n    const bool use_fixed_fly_zone =\n        g_TRVersion == 3 && creature->lot.setup.fly != 0;\n    if (!use_fixed_fly_zone && zone[box_num] != zone_num) {\n        return false;\n    }\n\n    const BOX_INFO *const box = Box_GetBox(box_num);\n    if ((box->overlap_index & creature->lot.setup.block_mask) != 0) {\n        return false;\n    }\n\n    // TODO: determine if the shift is essential\n    const int32_t shift = g_TRVersion >= 2 ? 1 : 0;\n    return !(\n        item->pos.z > box->left && item->pos.z < box->right + shift\n        && item->pos.x > box->top && item->pos.x < box->bottom + shift);\n}\n\nTARGET_TYPE Box_CalculateTarget(\n    XYZ_32 *const target, const ITEM *const item, LOT_INFO *const lot)\n{\n    Box_UpdateLOT(lot, BOX_MAX_EXPANSION);\n\n    *target = item->pos;\n\n    int32_t box_num = item->box_num;\n    if (box_num == NO_BOX) {\n        return TARGET_NONE;\n    }\n\n    int32_t bottom = 0;\n    int32_t top = 0;\n    int32_t right = 0;\n    int32_t left = 0;\n\n    const BOX_INFO *box = nullptr;\n    int32_t prime_free = BOX_CLIP_ALL;\n    do {\n        box = Box_GetBox(box_num);\n        if (lot->setup.fly != 0) {\n            CLAMPG(target->y, box->height - WALL_L);\n        } else {\n            CLAMPG(target->y, box->height);\n        }\n\n        if (item->pos.z >= box->left && item->pos.z <= box->right\n            && item->pos.x >= box->top && item->pos.x <= box->bottom) {\n            left = box->left;\n            right = box->right;\n            top = box->top;\n            bottom = box->bottom;\n        } else {\n            if (item->pos.z < box->left) {\n                if ((prime_free & BOX_CLIP_LEFT) != 0 && item->pos.x >= box->top\n                    && item->pos.x <= box->bottom) {\n                    CLAMPL(target->z, box->left + BOX_BIFF);\n                    if ((prime_free & BOX_CLIP_SECONDARY) != 0) {\n                        return TARGET_SECONDARY;\n                    }\n                    CLAMPL(top, box->top);\n                    CLAMPG(bottom, box->bottom);\n                    prime_free = BOX_CLIP_LEFT;\n                } else if (prime_free != BOX_CLIP_LEFT) {\n                    target->z = right - BOX_BIFF;\n                    if (prime_free != BOX_CLIP_ALL) {\n                        return TARGET_SECONDARY;\n                    }\n                    prime_free |= BOX_CLIP_SECONDARY;\n                }\n            } else if (item->pos.z > box->right) {\n                if ((prime_free & BOX_CLIP_RIGHT) != 0\n                    && item->pos.x >= box->top && item->pos.x <= box->bottom) {\n                    CLAMPG(target->z, box->right - BOX_BIFF);\n                    if ((prime_free & BOX_CLIP_SECONDARY) != 0) {\n                        return TARGET_SECONDARY;\n                    }\n                    CLAMPL(top, box->top);\n                    CLAMPG(bottom, box->bottom);\n                    prime_free = BOX_CLIP_RIGHT;\n                } else if (prime_free != BOX_CLIP_RIGHT) {\n                    target->z = left + BOX_BIFF;\n                    if (prime_free != BOX_CLIP_ALL) {\n                        return TARGET_SECONDARY;\n                    }\n                    prime_free |= BOX_CLIP_SECONDARY;\n                }\n            }\n\n            if (item->pos.x < box->top) {\n                if ((prime_free & BOX_CLIP_TOP) != 0 && item->pos.z >= box->left\n                    && item->pos.z <= box->right) {\n                    CLAMPL(target->x, box->top + BOX_BIFF);\n                    if ((prime_free & BOX_CLIP_SECONDARY) != 0) {\n                        return TARGET_SECONDARY;\n                    }\n                    CLAMPL(left, box->left);\n                    CLAMPG(right, box->right);\n                    prime_free = BOX_CLIP_TOP;\n                } else if (prime_free != BOX_CLIP_TOP) {\n                    target->x = bottom - BOX_BIFF;\n                    if (prime_free != BOX_CLIP_ALL) {\n                        return TARGET_SECONDARY;\n                    }\n                    prime_free |= BOX_CLIP_SECONDARY;\n                }\n            } else if (item->pos.x > box->bottom) {\n                if ((prime_free & BOX_CLIP_BOTTOM) != 0\n                    && item->pos.z >= box->left && item->pos.z <= box->right) {\n                    CLAMPG(target->x, box->bottom - BOX_BIFF);\n                    if ((prime_free & BOX_CLIP_SECONDARY) != 0) {\n                        return TARGET_SECONDARY;\n                    }\n                    CLAMPL(left, box->left);\n                    CLAMPG(right, box->right);\n                    prime_free = BOX_CLIP_BOTTOM;\n                } else if (prime_free != BOX_CLIP_BOTTOM) {\n                    target->x = top + BOX_BIFF;\n                    if (prime_free != BOX_CLIP_ALL) {\n                        return TARGET_SECONDARY;\n                    }\n                    prime_free |= BOX_CLIP_SECONDARY;\n                }\n            }\n        }\n\n        if (box_num == lot->target_box) {\n            if ((prime_free & (BOX_CLIP_LEFT | BOX_CLIP_RIGHT)) != 0) {\n                target->z = lot->target.z;\n            } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) {\n                CLAMP(target->z, box->left + BOX_BIFF, box->right - BOX_BIFF);\n            }\n\n            if ((prime_free & (BOX_CLIP_TOP | BOX_CLIP_BOTTOM)) != 0) {\n                target->x = lot->target.x;\n            } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) {\n                CLAMP(target->x, box->top + BOX_BIFF, box->bottom - BOX_BIFF);\n            }\n\n            target->y = lot->target.y;\n            return TARGET_PRIMARY;\n        }\n\n        box_num = lot->node[box_num].exit_box;\n        if (box_num != NO_BOX\n            && (Box_GetBox(box_num)->overlap_index & lot->setup.block_mask)\n                != 0) {\n            break;\n        }\n    } while (box_num != NO_BOX);\n\n    if ((prime_free & (BOX_CLIP_LEFT | BOX_CLIP_RIGHT)) != 0) {\n        target->z = box->left + WALL_L / 2\n            + (((box->right - box->left - WALL_L) * Random_GetControl()) >> 15);\n    } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) {\n        CLAMP(target->z, box->left + BOX_BIFF, box->right - BOX_BIFF);\n    }\n\n    if ((prime_free & (BOX_CLIP_TOP | BOX_CLIP_BOTTOM)) != 0) {\n        target->x = box->top + WALL_L / 2\n            + (((box->bottom - box->top - WALL_L) * Random_GetControl()) >> 15);\n    } else if ((prime_free & BOX_CLIP_SECONDARY) == 0) {\n        CLAMP(target->x, box->top + BOX_BIFF, box->bottom - BOX_BIFF);\n    }\n\n    if (lot->setup.fly != 0) {\n        target->y = box->height - STEP_L * 3 / 2;\n    } else {\n        target->y = box->height;\n    }\n\n    return TARGET_NONE;\n}\n\nbool Box_BadFloor(\n    const int32_t x, const int32_t y, const int32_t z, const int32_t box_height,\n    const int32_t next_height, int16_t room_num, const LOT_INFO *const lot)\n{\n    const SECTOR *const sector =\n        Room_GetSector((XYZ_32) { x, y, z }, &room_num);\n    if (sector->box == NO_BOX) {\n        return true;\n    }\n\n    const BOX_INFO *const box = Box_GetBox(sector->box);\n    if ((box->overlap_index & lot->setup.block_mask) != 0) {\n        return true;\n    }\n\n    const int32_t height = box->height;\n    if (box_height - height > lot->setup.step\n        || box_height - height < lot->setup.drop) {\n        return true;\n    }\n    if (box_height - height < -lot->setup.step && height > next_height) {\n        return true;\n    }\n    if (lot->setup.fly != 0 && y > height + lot->setup.fly) {\n        return true;\n    }\n    return false;\n}\n\nint32_t Box_GetZoneCount(void)\n{\n    return g_TRVersion == 1 ? 2 : 4;\n}\n"
  },
  {
    "path": "src/trx/game/pathing/box.h",
    "content": "#pragma once\n\n#include <trx/game/creature/types.h>\n#include <trx/game/items/types.h>\n#include <trx/game/pathing/types.h>\n\nvoid Box_InitialiseBoxes(int32_t num_boxes);\nint16_t *Box_InitialiseOverlaps(int32_t num_overlaps);\nint32_t Box_GetCount(void);\nBOX_INFO *Box_GetBox(int32_t box_idx);\nint16_t Box_GetOverlap(int32_t overlap_idx);\nint16_t *Box_GetFlyZone(bool flip_status);\nint16_t *Box_GetGroundZone(bool flip_status, int32_t zone_idx);\nint16_t *Box_GetLotZone(const LOT_INFO *lot);\nint16_t AIGuard(CREATURE *creature);\nvoid GetAITarget(CREATURE *creature);\nbool Box_SearchLOT(LOT_INFO *lot, int32_t expansion);\nbool Box_UpdateLOT(LOT_INFO *lot, int32_t expansion);\nvoid Box_TargetBox(LOT_INFO *lot, int16_t box_num);\nbool Box_StalkBox(const ITEM *item, const ITEM *enemy, int16_t box_num);\nbool Box_EscapeBox(const ITEM *item, const ITEM *enemy, int16_t box_num);\nbool Box_ValidBox(const ITEM *item, int16_t zone_num, int16_t box_num);\nTARGET_TYPE Box_CalculateTarget(\n    XYZ_32 *target, const ITEM *item, LOT_INFO *lot);\nbool Box_BadFloor(\n    int32_t x, int32_t y, int32_t z, int32_t box_height, int32_t next_height,\n    int16_t room_num, const LOT_INFO *lot);\nint32_t Box_GetZoneCount(void);\n"
  },
  {
    "path": "src/trx/game/pathing/const.h",
    "content": "#pragma once\n\n#include <trx/game/const.h>\n#include <trx/version.h>\n\n#define NO_BOX (-1)\n#define BOX_ZONE(num) (((num) / STEP_L) - 1)\n#define LOT_SLOT_COUNT 32\n#define MAX_ZONES 4\n\n#define BOX_BLOCKED 0x4000\n#define BOX_BLOCKED_SEARCH 0x8000\n#define BOX_BLOCKABLE 0x8000\n#define BOX_FIXED_FLY_ZONE 0x2000\n"
  },
  {
    "path": "src/trx/game/pathing/lot.c",
    "content": "#include <trx/game/pathing/lot.h>\n\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\nstatic int32_t m_SlotsUsed = 0;\nstatic CREATURE *m_BaddieSlots = nullptr;\n\nLOT_SETUP LOT_Setup(const LOT_SETUP_TYPE type)\n{\n    switch (type) {\n    case LOT_SETUP_DEFAULT:\n        return (LOT_SETUP) {\n            .step = STEP_L,\n            .drop = g_TRVersion == 1 ? -STEP_L : -STEP_L * 2,\n            .fly = 0,\n            .block_mask = BOX_BLOCKED,\n        };\n\n    case LOT_SETUP_BEAST:\n        return (LOT_SETUP) {\n            .step = STEP_L,\n            .drop = g_TRVersion == 1 ? -STEP_L : -STEP_L * 2,\n            .fly = 0,\n            .block_mask = BOX_BLOCKABLE,\n        };\n\n    case LOT_SETUP_QUADRUPED:\n        return (LOT_SETUP) {\n            .step = STEP_L,\n            .drop = -WALL_L,\n            .fly = 0,\n            .block_mask = BOX_BLOCKED,\n        };\n\n    case LOT_SETUP_JUMPER:\n        return (LOT_SETUP) {\n            .step = WALL_L / 2,\n            .drop = -WALL_L,\n            .fly = 0,\n            .block_mask = BOX_BLOCKED,\n        };\n\n    case LOT_SETUP_CLIMBER:\n        return (LOT_SETUP) {\n            .step = WALL_L,\n            .drop = -WALL_L,\n            .fly = 0,\n            .block_mask = BOX_BLOCKED,\n        };\n\n    case LOT_SETUP_FLYER:\n        return (LOT_SETUP) {\n            .step = WALL_L * 20,\n            .drop = -WALL_L * 20,\n            .fly = STEP_L / 16,\n            .block_mask = BOX_BLOCKED,\n        };\n    }\n\n    ASSERT_FAIL();\n    return (LOT_SETUP) {};\n}\n\nvoid LOT_InitialiseArray(void)\n{\n    m_BaddieSlots =\n        GameBuf_Alloc(LOT_SLOT_COUNT * sizeof(CREATURE), GBUF_CREATURE_DATA);\n\n    for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) {\n        CREATURE *const creature = &m_BaddieSlots[i];\n        creature->item_num = NO_ITEM;\n        creature->lot.node =\n            GameBuf_Alloc(Box_GetCount() * sizeof(BOX_NODE), GBUF_CREATURE_LOT);\n    }\n\n    m_SlotsUsed = 0;\n}\n\nCREATURE *LOT_GetBaddieSlot(const int32_t i)\n{\n    return &m_BaddieSlots[i];\n}\n\nvoid LOT_DisableBaddieAI(const int16_t item_num)\n{\n    ITEM *const item = Item_Get(item_num);\n    CREATURE *const creature = item->creature_data;\n    item->creature_data = nullptr;\n    item->extra_rotations = nullptr;\n\n    if (creature != nullptr) {\n        creature->item_num = NO_ITEM;\n        m_SlotsUsed--;\n    }\n}\n\nbool LOT_EnableBaddieAI(const int16_t item_num, const bool always)\n{\n    if (Item_Get(item_num)->creature_data != nullptr) {\n        return true;\n    }\n\n    if (m_SlotsUsed < LOT_SLOT_COUNT) {\n        for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) {\n            if (m_BaddieSlots[slot].item_num == NO_ITEM) {\n                LOT_InitialiseSlot(item_num, slot);\n                return true;\n            }\n        }\n        ASSERT_FAIL();\n    }\n\n    int32_t worst_dist = 0;\n    if (!always) {\n        const ITEM *const item = Item_Get(item_num);\n        const int32_t dx = (item->pos.x - g_Camera.pos.pos.x) >> 8;\n        const int32_t dy = (item->pos.y - g_Camera.pos.pos.y) >> 8;\n        const int32_t dz = (item->pos.z - g_Camera.pos.pos.z) >> 8;\n        worst_dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n    }\n\n    int32_t worst_slot = -1;\n    for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) {\n        const ITEM *const item = Item_Get(m_BaddieSlots[slot].item_num);\n        const int32_t dx = (item->pos.x - g_Camera.pos.pos.x) >> 8;\n        const int32_t dy = (item->pos.y - g_Camera.pos.pos.y) >> 8;\n        const int32_t dz = (item->pos.z - g_Camera.pos.pos.z) >> 8;\n        const int32_t dist = SQUARE(dx) + SQUARE(dy) + SQUARE(dz);\n        if (dist > worst_dist) {\n            worst_dist = dist;\n            worst_slot = slot;\n        }\n    }\n\n    if (worst_slot < 0) {\n        return false;\n    }\n\n    const CREATURE *const creature = &m_BaddieSlots[worst_slot];\n    Item_Get(creature->item_num)->status = IS_INVISIBLE;\n    LOT_DisableBaddieAI(creature->item_num);\n    LOT_InitialiseSlot(item_num, worst_slot);\n    return true;\n}\n\nvoid LOT_InitialiseSlot(const int16_t item_num, const int32_t slot)\n{\n    CREATURE *const creature = &m_BaddieSlots[slot];\n    ITEM *const item = Item_Get(item_num);\n    item->creature_data = creature;\n    item->extra_rotations = creature->joint_rotation;\n\n    creature->item_num = item_num;\n    creature->mood = MOOD_BORED;\n    creature->neck_rotation = 0;\n    creature->head_rotation = 0;\n    creature->joint_rotation[0] = 0;\n    creature->joint_rotation[1] = 0;\n    creature->joint_rotation[2] = 0;\n    creature->joint_rotation[3] = 0;\n    creature->maximum_turn = DEG_1;\n    creature->flags = 0;\n    creature->enemy = nullptr;\n    creature->head_left = false;\n    creature->head_right = false;\n    creature->reached_goal = false;\n    creature->hurt_by_lara = false;\n    creature->patrol_2 = false;\n    creature->alerted = false;\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    creature->lot.setup = obj->lot_setup;\n\n    LOT_ClearLOT(&creature->lot);\n    LOT_CreateZone(item);\n\n    m_SlotsUsed++;\n}\n\nvoid LOT_CreateZone(ITEM *const item)\n{\n    CREATURE *const creature = item->creature_data;\n\n    const int16_t *zone;\n    const int16_t *flip;\n    if (creature->lot.setup.fly) {\n        zone = Box_GetFlyZone(false);\n        flip = Box_GetFlyZone(true);\n    } else {\n        zone = Box_GetGroundZone(false, BOX_ZONE(creature->lot.setup.step));\n        flip = Box_GetGroundZone(true, BOX_ZONE(creature->lot.setup.step));\n    }\n\n    const ROOM *const room = Room_Get(item->room_num);\n    item->box_num = Room_GetWorldSector(room, item->pos.x, item->pos.z)->box;\n\n    int16_t zone_num = zone[item->box_num];\n    int16_t flip_num = flip[item->box_num];\n\n    const bool use_fixed_fly_zone =\n        g_TRVersion == 3 && creature->lot.setup.fly != 0;\n\n    creature->lot.zone_count = 0;\n    BOX_NODE *node = creature->lot.node;\n    for (int32_t i = 0; i < Box_GetCount(); i++) {\n        if (use_fixed_fly_zone || zone[i] == zone_num || flip[i] == flip_num) {\n            node->box_num = i;\n            node++;\n            creature->lot.zone_count++;\n        }\n    }\n}\n\nvoid LOT_InitialiseLOT(LOT_INFO *const lot)\n{\n    lot->node =\n        GameBuf_Alloc(sizeof(BOX_NODE) * Box_GetCount(), GBUF_CREATURE_LOT);\n    LOT_ClearLOT(lot);\n}\n\nvoid LOT_ClearLOT(LOT_INFO *const lot)\n{\n    lot->search_num = 0;\n    lot->head = NO_BOX;\n    lot->tail = NO_BOX;\n    lot->target_box = NO_BOX;\n    lot->required_box = NO_BOX;\n\n    for (int32_t i = 0; i < Box_GetCount(); i++) {\n        BOX_NODE *const node = &lot->node[i];\n        node->next_expansion = NO_BOX;\n        node->exit_box = NO_BOX;\n        node->search_num = 0;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/pathing/lot.h",
    "content": "#pragma once\n\n#include <trx/game/creature/types.h>\n\nLOT_SETUP LOT_Setup(LOT_SETUP_TYPE type);\nvoid LOT_InitialiseArray(void);\nvoid LOT_InitialiseSlot(int16_t item_num, int32_t slot);\nvoid LOT_CreateZone(ITEM *item);\nvoid LOT_InitialiseLOT(LOT_INFO *LOT);\nbool LOT_EnableBaddieAI(int16_t item_num, bool always);\nvoid LOT_DisableBaddieAI(int16_t item_num);\nCREATURE *LOT_GetBaddieSlot(int32_t i);\nvoid LOT_ClearLOT(LOT_INFO *LOT);\n"
  },
  {
    "path": "src/trx/game/pathing/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n\ntypedef struct {\n    int32_t left;\n    int32_t right;\n    int32_t top;\n    int32_t bottom;\n    int16_t height;\n    int16_t overlap_index;\n} BOX_INFO;\n\ntypedef struct {\n    int16_t exit_box;\n    uint16_t search_num;\n    int16_t next_expansion;\n    int16_t box_num;\n} BOX_NODE;\n\ntypedef enum {\n    LOT_SETUP_DEFAULT,\n    LOT_SETUP_BEAST,\n    LOT_SETUP_QUADRUPED,\n    LOT_SETUP_JUMPER,\n    LOT_SETUP_CLIMBER,\n    LOT_SETUP_FLYER,\n} LOT_SETUP_TYPE;\n\ntypedef struct {\n    int16_t step;\n    int16_t drop;\n    int16_t fly;\n    uint16_t block_mask;\n} LOT_SETUP;\n\ntypedef struct {\n    LOT_SETUP setup;\n    BOX_NODE *node;\n    int16_t head;\n    int16_t tail;\n    uint16_t search_num;\n    int16_t zone_count;\n    int16_t target_box;\n    int16_t required_box;\n    XYZ_32 target;\n} LOT_INFO;\n\ntypedef enum {\n    TARGET_NONE = 0,\n    TARGET_PRIMARY = 1,\n    TARGET_SECONDARY = 2,\n} TARGET_TYPE;\n"
  },
  {
    "path": "src/trx/game/pathing.h",
    "content": "#pragma once\n\n#include <trx/game/pathing/box.h>\n#include <trx/game/pathing/const.h>\n#include <trx/game/pathing/lot.h>\n#include <trx/game/pathing/types.h>\n"
  },
  {
    "path": "src/trx/game/phase/control.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n\ntypedef enum {\n    PHASE_ACTION_CONTINUE,\n    PHASE_ACTION_NO_WAIT,\n    PHASE_ACTION_END,\n    PHASE_ACTION_END_FAST,\n} PHASE_ACTION;\n\n// Status returned upon every logical frame by the control routine.\n//\n// 1. To carry on executing current phase, .action member should be set to\n//    either PHASE_ACTION_CONTINUE, which will let the cycle continue, draw the\n//    phase and wait one frame before repeating the cycle, or\n//    PHASE_ACTION_NO_WAIT which will immediately repeat the control routine\n//    without drawing or waiting the current cycle. The latter is useful for\n//    easier state switches.\n//    The gf_cmd member is unused in this scenario.\n//\n// 2. To end the current phase and carry on continuing current game sequence,\n//    .action member should be set to PHASE_ACTION_END, and .gf_cmd.action\n//    member should be set to GF_NOOP.\n//\n// 3. To end the current phase and switch to another game sequence, .action\n//    member should be set to PHASE_ACTION_END, and .gf_cmd.action member\n//    should be set to the phase to switch to.\ntypedef struct {\n    PHASE_ACTION action;\n    GF_COMMAND gf_cmd;\n} PHASE_CONTROL;\n"
  },
  {
    "path": "src/trx/game/phase/executor.c",
    "content": "#include <trx/game/phase/executor.h>\n\n#include <trx/config.h>\n#include <trx/core/benchmark.h>\n#include <trx/game/clock.h>\n#include <trx/game/console/common.h>\n#include <trx/game/fader.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/output/overlay.h>\n#include <trx/game/overlay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/ui.h>\n#include <trx/gl/context.h>\n#include <trx/gl/track.h>\n\n#include <stdio.h>\n\n#define M_MAX_PHASES 10\n\nstatic int32_t m_CurrentFrame = 0;\nstatic bool m_Exiting;\nstatic FADER m_ExitFader;\nstatic int32_t m_PhaseStackSize = 0;\nstatic PHASE *m_PhaseStack[M_MAX_PHASES] = {};\nstatic bool m_PendingFadeToBlack = false;\nstatic FADER_ARGS m_PendingFadeToBlackArgs;\n\nstatic bool M_ShouldSuspendForFocusLoss(void)\n{\n    return g_Config.gameplay.pause_on_focus_lost && !Shell_IsFocused();\n}\n\nstatic GF_COMMAND M_HandleOverride(void)\n{\n    const GF_COMMAND gf_override_cmd = GF_GetOverrideCommand();\n    if (gf_override_cmd.action != GF_NOOP) {\n        const GF_COMMAND gf_cmd = gf_override_cmd;\n        GF_OverrideCommand((GF_COMMAND) { .action = GF_NOOP });\n\n        // A change in the game flow is not natural. Force features like death\n        // counter to break from the currently active savegame file.\n        Savegame_UnbindSlot();\n        // This flag needs to be cleared as well.\n        Game_SetIsPlaying(false);\n        // Usually, sequences permit music to flow through - for instance, the\n        // end of level screen in The Great Wall transitioning to Venice.\n        // We must stop it manually here when derailing the sequence (#3469).\n        Music_Stop();\n\n        return gf_cmd;\n    }\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nstatic void M_DrawFadeToBlackTransition(const float opacity)\n{\n    Output_BeginScene();\n    Output_SwitchViewport(VIEWPORT_GAME);\n    UI_BeginScene();\n\n    Output_Overlay_DrawSnapshot(1.0f);\n    Output_Overlay_DrawBlackRectangle(opacity, false);\n\n    Overlay_Draw();\n    Console_Draw();\n    UI_EndScene();\n\n    Output_SwitchViewport(VIEWPORT_UI);\n    UI_Draw();\n\n    Output_Flush();\n    Output_Overlay_DrawBlackRectangle(\n        Fader_GetCurrentValue(&m_ExitFader), true);\n    Output_EndScene();\n\n    if (!Output_IsHeadless()\n        || TRX_GL_Context_GetScheduledScreenshotPath() != nullptr) {\n        Output_FlipScreen();\n    } else {\n        TRX_GL_Track_Reset();\n    }\n}\n\nstatic GF_COMMAND M_RunFadeToBlackTransition(const FADER_ARGS args)\n{\n    Output_Overlay_CaptureSnapshot();\n\n    FADER fader = {};\n    Fader_InitToHold(&fader, 0.0f, 1.0f, args.duration, args.debuff);\n    while (Fader_IsActive(&fader)) {\n        Clock_WaitTick();\n        m_CurrentFrame++;\n\n        Shell_ProcessEvents();\n        Console_Control();\n        Overlay_Control();\n\n        const GF_COMMAND gf_cmd = M_HandleOverride();\n        if (gf_cmd.action != GF_NOOP) {\n            return gf_cmd;\n        }\n\n        if (Shell_IsExiting() && !m_Exiting) {\n            m_Exiting = true;\n            if (g_Config.visuals.enable_exit_fade_effects) {\n                Fader_InitFromCurrentHold(&m_ExitFader, 1.0f, 0.333f, 0.1f);\n            }\n        } else if (m_Exiting && !Fader_IsActive(&m_ExitFader)) {\n            return (GF_COMMAND) { .action = GF_EXIT_GAME };\n        }\n\n        Interpolation_SetRate(1.0f);\n        Output_SetTime(m_CurrentFrame);\n        M_DrawFadeToBlackTransition(Fader_GetCurrentValue(&fader));\n    }\n\n    return (GF_COMMAND) { .action = GF_NOOP };\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    m_CurrentFrame++;\n    Shell_ProcessEvents();\n    Console_Control();\n    Overlay_Control();\n\n    const GF_COMMAND gf_cmd = M_HandleOverride();\n    if (gf_cmd.action != GF_NOOP) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END_FAST,\n            .gf_cmd = gf_cmd,\n        };\n    }\n\n    if (Shell_IsExiting() && !m_Exiting) {\n        m_Exiting = true;\n        if (g_Config.visuals.enable_exit_fade_effects) {\n            Fader_InitFromCurrentHold(&m_ExitFader, 1.0f, 0.333f, 0.1f);\n        }\n    } else if (m_Exiting && !Fader_IsActive(&m_ExitFader)) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_EXIT_GAME },\n        };\n    }\n\n    if (m_Exiting) {\n        return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n    }\n\n    if (M_ShouldSuspendForFocusLoss()) {\n        return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n    }\n\n    if (phase != nullptr && phase->control != nullptr) {\n        return phase->control(phase);\n    }\n    return (PHASE_CONTROL) {\n        .action = PHASE_ACTION_END,\n        .gf_cmd = { .action = GF_NOOP },\n    };\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n\n    Output_BeginScene();\n    Output_SwitchViewport(VIEWPORT_GAME);\n    UI_BeginScene();\n    if (phase != nullptr && phase->draw != nullptr) {\n        phase->draw(phase);\n    }\n\n    Overlay_Draw();\n    Console_Draw();\n    UI_EndScene();\n\n    Output_SwitchViewport(VIEWPORT_UI);\n    UI_Draw();\n\n    Output_Flush();\n    Output_Overlay_DrawBlackRectangle(\n        Fader_GetCurrentValue(&m_ExitFader), true);\n    Output_EndScene();\n\n    if (Shell_GetArgs()->debug_render_performance) {\n        char buffer[80];\n        const TRX_GL_METRICS metrics = TRX_GL_Track_GetMetrics();\n        sprintf(\n            buffer, \"%.03f KB T:%d U:%d Vo:%d Vt:%d Vb:%d\",\n            metrics.buffer_total_bytes / 1024.0f, metrics.buffer_transfer_count,\n            metrics.uniform_changes, metrics.opaque_vert_count,\n            metrics.trans_vert_count, metrics.blend_add_vert_count);\n        Benchmark_End(&benchmark, buffer);\n    }\n\n    if (!Output_IsHeadless()\n        || TRX_GL_Context_GetScheduledScreenshotPath() != nullptr) {\n        Output_FlipScreen();\n    } else {\n        TRX_GL_Track_Reset();\n    }\n}\n\nGF_COMMAND PhaseExecutor_Run(PHASE *const phase)\n{\n    GF_COMMAND gf_cmd = { .action = GF_NOOP };\n    bool skip_fade_out = false;\n\n    gf_cmd = M_HandleOverride();\n    if (gf_cmd.action != GF_NOOP) {\n        return gf_cmd;\n    }\n\n    PHASE *const prev_phase =\n        m_PhaseStackSize > 0 ? m_PhaseStack[m_PhaseStackSize - 1] : nullptr;\n    if (prev_phase != nullptr && prev_phase->suspend != nullptr) {\n        prev_phase->suspend(phase);\n    }\n    m_PhaseStack[m_PhaseStackSize++] = phase;\n\n    if (m_PendingFadeToBlack) {\n        const bool uses_cross_fade_in = phase != nullptr\n            && phase->uses_cross_fade_in != nullptr\n            && phase->uses_cross_fade_in(phase);\n        if (!uses_cross_fade_in) {\n            gf_cmd = M_RunFadeToBlackTransition(m_PendingFadeToBlackArgs);\n            if (gf_cmd.action != GF_NOOP) {\n                goto finish;\n            }\n        }\n        m_PendingFadeToBlack = false;\n    }\n\n    if (phase->start != nullptr) {\n        Clock_SyncTick();\n        g_OldInputDB = g_Input;\n        const PHASE_CONTROL control = phase->start(phase);\n        if (Shell_IsExiting()) {\n            gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME };\n            goto finish;\n        } else if (control.action == PHASE_ACTION_END) {\n            gf_cmd = control.gf_cmd;\n            goto finish;\n        } else if (control.action == PHASE_ACTION_END_FAST) {\n            gf_cmd = control.gf_cmd;\n            skip_fade_out = true;\n            goto finish;\n        }\n    }\n\n    while (true) {\n        int32_t nframes = Clock_WaitTick();\n        int32_t frame = 0;\n        while (true) {\n            const PHASE_CONTROL control = M_Control(phase);\n            if (control.action == PHASE_ACTION_END) {\n                if (Shell_IsExiting()) {\n                    gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME };\n                } else {\n                    gf_cmd = control.gf_cmd;\n                }\n                goto finish;\n            } else if (control.action == PHASE_ACTION_END_FAST) {\n                if (Shell_IsExiting()) {\n                    gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME };\n                } else {\n                    skip_fade_out = true;\n                    gf_cmd = control.gf_cmd;\n                }\n                goto finish;\n            } else if (control.action == PHASE_ACTION_NO_WAIT) {\n                continue;\n            }\n\n            frame++;\n            if (frame >= nframes) {\n                break;\n            }\n        }\n\n        if (!M_ShouldSuspendForFocusLoss() && Interpolation_IsActive()) {\n            Interpolation_SetRate(0.5);\n            Output_SetTime(m_CurrentFrame - 0.5f);\n            M_Draw(phase);\n            Clock_WaitTick();\n        }\n\n        Interpolation_SetRate(1.0);\n        Output_SetTime(m_CurrentFrame);\n        Output_SetControlFrame(true);\n        M_Draw(phase);\n        Output_SetControlFrame(false);\n    }\n\nfinish:\n    if (phase->end != nullptr) {\n        phase->end(phase);\n    }\n\n    if (!skip_fade_out && phase->request_fade_to_black != nullptr) {\n        m_PendingFadeToBlack =\n            phase->request_fade_to_black(phase, &m_PendingFadeToBlackArgs);\n    } else {\n        m_PendingFadeToBlack = false;\n    }\n\n    if (prev_phase != nullptr && prev_phase->resume != nullptr) {\n        Clock_SyncTick();\n        prev_phase->resume(phase);\n    }\n    m_PhaseStackSize--;\n\n    return gf_cmd;\n}\n\nPHASE *PhaseExecutor_GetOuterPhase(void)\n{\n    if (m_PhaseStackSize < 2) {\n        return nullptr;\n    }\n    return m_PhaseStack[m_PhaseStackSize - 2];\n}\n"
  },
  {
    "path": "src/trx/game/phase/executor.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\nGF_COMMAND PhaseExecutor_Run(PHASE *phase);\n\nPHASE *PhaseExecutor_GetOuterPhase(void);\n"
  },
  {
    "path": "src/trx/game/phase/phase_cutscene.c",
    "content": "#include <trx/game/phase/phase_cutscene.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/game/cutscene.h>\n#include <trx/game/fader.h>\n#include <trx/game/game.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lua/events.h>\n#include <trx/game/output.h>\n#include <trx/game/output/overlay.h>\n#include <trx/version.h>\n\n#define M_CROSS_FADE_DURATION 0.5f\n\ntypedef struct {\n    PHASE_CUTSCENE_ARGS args;\n    FADER cross_fader;\n} M_PRIV;\n\nstatic bool M_UsesCrossFadeIn(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    return p->args.cross_fade_in && g_Config.visuals.enable_fade_effects\n        && g_TRVersion == 3;\n}\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (phase->uses_cross_fade_in != nullptr\n        && phase->uses_cross_fade_in(phase)) {\n        Output_Overlay_CaptureSnapshot();\n        Fader_InitTo(&p->cross_fader, 1.0f, 0.0f, M_CROSS_FADE_DURATION);\n    }\n\n    if (!Cutscene_Start(p->args.level_num)) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_NOOP },\n        };\n    }\n    Game_SetIsPlaying(true);\n    Lara_SetControllable(false);\n    return (PHASE_CONTROL) {};\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Game_SetIsPlaying(false);\n    Cutscene_End();\n}\n\nstatic void M_Suspend(PHASE *const phase)\n{\n    Game_SetIsPlaying(false);\n}\n\nstatic void M_Resume(PHASE *const phase)\n{\n    Game_SetIsPlaying(true);\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    Lua_FireEventInt32(LUA_EVENT_BEFORE_CONTROL, 0);\n    M_PRIV *const p = phase->priv;\n    const GF_COMMAND gf_cmd = Cutscene_Control();\n    Lua_FireEventInt32(LUA_EVENT_AFTER_CONTROL, 0);\n    if (gf_cmd.action != GF_NOOP) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = gf_cmd,\n        };\n    }\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Cutscene_Draw();\n    if (phase->uses_cross_fade_in != nullptr\n        && phase->uses_cross_fade_in(phase)) {\n        Output_Overlay_DrawSnapshot(Fader_GetCurrentValue(&p->cross_fader));\n    }\n}\n\nPHASE *Phase_Cutscene_Create(const PHASE_CUTSCENE_ARGS args)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV));\n    p->args = args;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->suspend = M_Suspend;\n    phase->resume = M_Resume;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    phase->uses_cross_fade_in = M_UsesCrossFadeIn;\n    return phase;\n}\n\nvoid Phase_Cutscene_Destroy(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Memory_Free(p);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_cutscene.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\ntypedef struct {\n    int32_t level_num;\n    bool cross_fade_in;\n} PHASE_CUTSCENE_ARGS;\n\nPHASE *Phase_Cutscene_Create(PHASE_CUTSCENE_ARGS args);\nvoid Phase_Cutscene_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_demo.c",
    "content": "#include <trx/game/phase/phase_demo.h>\n\n#include <trx/core/memory.h>\n#include <trx/game/demo.h>\n#include <trx/game/fader.h>\n#include <trx/game/game.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/output/overlay.h>\n#include <trx/game/shell.h>\n\ntypedef enum {\n    STATE_RUN,\n    STATE_FADE_OUT,\n    STATE_FINISH,\n} STATE;\n\ntypedef struct {\n    STATE state;\n    int32_t level_num;\n    FADER fader;\n    GF_COMMAND exit_gf_cmd;\n} M_PRIV;\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (p->level_num == -1) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_EXIT_TO_TITLE },\n        };\n    }\n\n    if (!Demo_Start(p->level_num)) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_EXIT_TO_TITLE },\n        };\n    }\n\n    p->state = STATE_RUN;\n    Game_SetIsPlaying(true);\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    Demo_End();\n}\n\nstatic void M_Suspend(PHASE *const phase)\n{\n    Game_SetIsPlaying(false);\n    Demo_Pause();\n}\n\nstatic void M_Resume(PHASE *const phase)\n{\n    Game_SetIsPlaying(true);\n    Demo_Unpause();\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    switch (p->state) {\n    case STATE_RUN:\n        const GF_COMMAND gf_cmd = Demo_Control();\n        if (gf_cmd.action != GF_NOOP) {\n            p->state = STATE_FADE_OUT;\n            p->exit_gf_cmd = gf_cmd;\n            Fader_InitToHold(&p->fader, 0.0f, 1.0f, 0.5f, 0.1f);\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        }\n        break;\n\n    case STATE_FADE_OUT:\n        Game_SetIsPlaying(false);\n        Demo_StopFlashing();\n        if (!Fader_IsActive(&p->fader)) {\n            p->state = STATE_FINISH;\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        }\n        break;\n\n    case STATE_FINISH:\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = Shell_IsExiting()\n                ? (GF_COMMAND) { .action = GF_EXIT_GAME }\n                : p->exit_gf_cmd,\n        };\n    }\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (p->state == STATE_FADE_OUT) {\n        Interpolation_Disable();\n    }\n    Game_Draw(true);\n    if (p->state == STATE_FADE_OUT) {\n        Interpolation_Enable();\n    }\n\n    Output_Overlay_DrawBlackRectangle(Fader_GetCurrentValue(&p->fader), true);\n}\n\nPHASE *Phase_Demo_Create(const int32_t level_num)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV));\n    p->level_num = level_num;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->suspend = M_Suspend;\n    phase->resume = M_Resume;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    return phase;\n}\n\nvoid Phase_Demo_Destroy(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Memory_Free(p);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_demo.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\nPHASE *Phase_Demo_Create(int32_t level_num);\nvoid Phase_Demo_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_game.c",
    "content": "#include <trx/game/phase/phase_game.h>\n\n#include <trx/core/memory.h>\n#include <trx/game/game.h>\n#include <trx/game/lua/events.h>\n#include <trx/game/output.h>\n#include <trx/game/sound.h>\n\ntypedef struct {\n    const GF_LEVEL *level;\n    GF_SEQUENCE_CONTEXT seq_ctx;\n    struct {\n        uint8_t reverb_type;\n    } stashed_state;\n} M_PRIV;\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (!Game_Start(p->level, p->seq_ctx)) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_EXIT_TO_TITLE },\n        };\n    }\n    Game_SetIsPlaying(true);\n    return (PHASE_CONTROL) {\n        .action = PHASE_ACTION_CONTINUE,\n    };\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    Game_End();\n    Game_SetIsPlaying(false);\n    Sound_SetReverbType(0);\n}\n\nstatic void M_Suspend(PHASE *const phase)\n{\n    Game_SetIsPlaying(false);\n    M_PRIV *const p = phase->priv;\n    p->stashed_state.reverb_type = Sound_GetReverbType();\n    Sound_SetReverbType(0);\n}\n\nstatic void M_Resume(PHASE *const phase)\n{\n    Game_SetIsPlaying(true);\n    M_PRIV *const p = phase->priv;\n    Sound_SetReverbType(p->stashed_state.reverb_type);\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    Lua_FireEventInt32(LUA_EVENT_BEFORE_CONTROL, 0);\n    const GF_COMMAND gf_cmd = Game_Control(false);\n    Lua_FireEventInt32(LUA_EVENT_AFTER_CONTROL, 0);\n    if (gf_cmd.action != GF_NOOP) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = gf_cmd,\n        };\n    }\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    Game_Draw(true);\n}\n\nPHASE *Phase_Game_Create(\n    const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV));\n    p->level = level;\n    p->seq_ctx = seq_ctx;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->suspend = M_Suspend;\n    phase->resume = M_Resume;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    return phase;\n}\n\nvoid Phase_Game_Destroy(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Memory_Free(p);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_game.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/phase/types.h>\n\nPHASE *Phase_Game_Create(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx);\nvoid Phase_Game_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_globe_select.c",
    "content": "#include <trx/game/phase/phase_globe_select.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/fader.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/shell.h>\n\ntypedef enum {\n    STATE_FADE_IN,\n    STATE_DISPLAY,\n    STATE_FADE_OUT,\n    STATE_FINISH,\n} STATE;\n\ntypedef struct {\n    PHASE_GLOBE_SELECT_ARGS args;\n    STATE state;\n    FADER fader;\n    INV_RING *ring;\n    GF_COMMAND result;\n} M_PRIV;\n\nstatic bool M_IsFading(const M_PRIV *const p)\n{\n    return Fader_IsActive(&p->fader);\n}\n\nstatic void M_FadeIn(M_PRIV *const p)\n{\n    if (p->args.background_path != nullptr) {\n        Fader_InitTo(&p->fader, 1.0f, 0.0f, 1.0);\n    } else {\n        Fader_InitTo(&p->fader, 0.0f, 1.0f, 0.5);\n    }\n    p->state = STATE_FADE_IN;\n}\n\nstatic void M_FadeOut(M_PRIV *const p)\n{\n    Output_Overlay_CaptureSnapshot();\n    Fader_InitFromCurrentHold(&p->fader, 1.0f, 0.5f, 0.1f);\n    p->state = STATE_FADE_OUT;\n}\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    p->ring = InvRing_Open(INV_GLOBE_SELECT_MODE);\n    if (p->ring == nullptr) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_EXIT_TO_TITLE },\n        };\n    }\n    M_FadeIn(p);\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (p->ring != nullptr) {\n        InvRing_Close(p->ring);\n        p->ring = nullptr;\n    }\n\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCL, false);\n    Overlay_ShowArrow(UI_OVERLAY_ARROW_BCR, false);\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    switch (p->state) {\n    case STATE_FADE_IN:\n        if (!M_IsFading(p)) {\n            p->state = STATE_DISPLAY;\n        }\n        break;\n\n    case STATE_DISPLAY:\n        break;\n\n    case STATE_FADE_OUT:\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back || !M_IsFading(p)) {\n            p->state = STATE_FINISH;\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        }\n        break;\n\n    case STATE_FINISH:\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = p->result,\n        };\n    }\n\n    ASSERT(p->ring != nullptr);\n    const GF_COMMAND gf_cmd = InvRing_Control(p->ring);\n    if (gf_cmd.action != GF_NOOP) {\n        p->result = gf_cmd;\n        M_FadeOut(p);\n    }\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    const float opacity = Fader_GetCurrentValue(&p->fader);\n\n    if (p->args.background_path != nullptr) {\n        Output_Overlay_DrawImageMono(p->args.background_path, 1.0f);\n        Output_Overlay_DrawBlackRectangle(0.5f, false);\n    } else {\n        Output_Overlay_DrawBlackRectangle(1.0f, false);\n    }\n    Output_Flush();\n\n    ASSERT(p->ring != nullptr);\n    InvRing_Draw(p->ring);\n\n    if (opacity > 0.0f) {\n        Output_Overlay_DrawBlackRectangle(opacity, false);\n    }\n}\n\nPHASE *Phase_GlobeSelect_Create(const PHASE_GLOBE_SELECT_ARGS args)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(*phase));\n    M_PRIV *const p = Memory_Alloc(sizeof(*p));\n    p->result = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n    p->args = args;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    return phase;\n}\n\nvoid Phase_GlobeSelect_Destroy(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Memory_Free(p);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_globe_select.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\ntypedef struct {\n    const char *background_path;\n} PHASE_GLOBE_SELECT_ARGS;\n\nPHASE *Phase_GlobeSelect_Create(PHASE_GLOBE_SELECT_ARGS args);\nvoid Phase_GlobeSelect_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_inventory.c",
    "content": "#include <trx/game/phase/phase_inventory.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/fader.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n\ntypedef struct {\n    INVENTORY_MODE mode;\n    INV_RING *ring;\n    bool fade_to_black;\n} M_PRIV;\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    const GF_LEVEL *const level = GF_GetTitleLevel();\n    if (p->mode == INV_TITLE_MODE && g_Config.audio.enable_music_in_menu\n        && level->music_track >= 0) {\n        Music_Stop();\n        Music_Play_Direct(level->music_track, MPM_LOOP);\n    }\n\n    p->ring = InvRing_Open(p->mode);\n    if (p->ring == nullptr) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_NOOP },\n        };\n    }\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    ASSERT(p->ring != nullptr);\n    const GF_COMMAND gf_cmd = InvRing_Control(p->ring);\n    if (p->mode == INV_TITLE_MODE && p->ring->status == RNG_DONE) {\n        p->fade_to_black = true;\n    }\n    return (PHASE_CONTROL) {\n        .action = (p->mode == INV_GLOBE_SELECT_MODE && gf_cmd.action != GF_NOOP)\n                || p->ring->status == RNG_DONE\n            ? PHASE_ACTION_END\n            : PHASE_ACTION_CONTINUE,\n        .gf_cmd = gf_cmd,\n    };\n}\n\nstatic bool M_RequestFadeToBlack(PHASE *const phase, FADER_ARGS *const out_args)\n{\n    const M_PRIV *const p = phase->priv;\n    if (p->mode != INV_TITLE_MODE || !p->fade_to_black) {\n        return false;\n    }\n\n    if (out_args != nullptr) {\n        *out_args = (FADER_ARGS) {\n            .from_current = false,\n            .initial = 0.0f,\n            .target = 1.0f,\n            .duration = 0.25f,\n            .debuff = 0.1f,\n        };\n    }\n    return true;\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (p->mode == INV_TITLE_MODE) {\n        Music_Stop();\n    }\n    if (p->ring != nullptr) {\n        InvRing_Close(p->ring);\n        p->ring = nullptr;\n    }\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    ASSERT(p->ring != nullptr);\n    InvRing_Draw(p->ring);\n}\n\nPHASE *Phase_Inventory_Create(const INVENTORY_MODE mode)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV));\n    p->mode = mode;\n    p->fade_to_black = false;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    phase->request_fade_to_black =\n        mode == INV_TITLE_MODE ? M_RequestFadeToBlack : nullptr;\n    return phase;\n}\n\nvoid Phase_Inventory_Destroy(PHASE *const phase)\n{\n    Memory_Free(phase->priv);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_inventory.h",
    "content": "#pragma once\n\n#include <trx/game/inventory_ring/types.h>\n#include <trx/game/phase/types.h>\n\nPHASE *Phase_Inventory_Create(INVENTORY_MODE mode);\nvoid Phase_Inventory_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_pause.c",
    "content": "#include <trx/game/phase/phase_pause.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/game/const.h>\n#include <trx/game/fader.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n\n#include <stdint.h>\n\n#define M_FADE_TIME 0.4\n\ntypedef enum {\n    STATE_FADE_IN,\n    STATE_WAIT,\n    STATE_ASK,\n    STATE_FADE_OUT,\n} STATE;\n\ntypedef struct {\n    STATE state;\n    struct {\n        bool is_ready;\n        UI_PAUSE_STATE state;\n    } ui;\n    GF_ACTION action;\n    FADER fader;\n} M_PRIV;\n\nstatic void M_RemoveText(M_PRIV *const p)\n{\n    Overlay_SetBottomText((OVERLAY_TEXT) { 0 });\n}\n\nstatic void M_FadeIn(M_PRIV *const p)\n{\n    p->state = STATE_FADE_IN;\n    Fader_InitTo(&p->fader, 0.0f, 1.0f, M_FADE_TIME);\n}\n\nstatic void M_FadeOut(M_PRIV *const p)\n{\n    M_RemoveText(p);\n    p->ui.is_ready = false;\n    if (p->action == GF_NOOP) {\n        Fader_InitFromCurrent(&p->fader, 0.0f, M_FADE_TIME);\n    } else {\n        Fader_InitFromCurrentHold(\n            &p->fader, 1.0f, M_FADE_TIME, 3.0 / (double)LOGIC_FPS);\n    }\n    p->state = STATE_FADE_OUT;\n}\n\nstatic void M_PauseGame(M_PRIV *const p)\n{\n    p->action = GF_NOOP;\n    Music_Pause();\n    Sound_PauseAll();\n    M_FadeIn(p);\n}\n\nstatic void M_ReturnToGame(M_PRIV *const p)\n{\n    Music_Unpause();\n    Sound_UnpauseAll();\n    M_FadeOut(p);\n}\n\nstatic void M_ExitToTitle(M_PRIV *const p)\n{\n    p->action = GF_EXIT_TO_TITLE;\n    M_FadeOut(p);\n}\n\nstatic void M_CreateText(M_PRIV *const p)\n{\n    Overlay_SetBottomText((OVERLAY_TEXT) {\n        .kind = UI_OVERLAY_TEXT_GS_KEY,\n        .gs_key = GS_ID(\"general/pause/paused\"),\n    });\n}\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    p->ui.is_ready = false;\n    UI_Pause_Init(&p->ui.state);\n    M_PauseGame(p);\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    M_RemoveText(p);\n    UI_Pause_Free(&p->ui.state);\n}\n\nstatic bool M_IsFadeActive(M_PRIV *const p)\n{\n    return Fader_IsActive(&p->fader) && g_Config.ui.pause_fade_effects\n        && g_Config.ui.pause_background_style != BK_NONE;\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Input_Update();\n    Shell_ProcessInput();\n\n    if (p->ui.is_ready) {\n        UI_Pause_Control(&p->ui.state);\n    }\n\n    switch (p->state) {\n    case STATE_FADE_IN:\n        if (g_InputDB.pause) {\n            M_ReturnToGame(p);\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        } else if (!M_IsFadeActive(p)) {\n            p->state = STATE_WAIT;\n            M_CreateText(p);\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        }\n        break;\n\n    case STATE_WAIT:\n        if (g_InputDB.pause) {\n            M_ReturnToGame(p);\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        } else if (g_InputDB.option) {\n            p->state = STATE_ASK;\n        }\n        break;\n\n    case STATE_ASK: {\n        const UI_PAUSE_EXIT_CHOICE choice = UI_Pause_Control(&p->ui.state);\n        switch (choice) {\n        case UI_PAUSE_RESUME_PAUSE:\n            p->state = STATE_WAIT;\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        case UI_PAUSE_EXIT_TO_GAME:\n            M_ReturnToGame(p);\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        case UI_PAUSE_EXIT_TO_TITLE:\n            M_ExitToTitle(p);\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        default:\n            break;\n        }\n        break;\n    }\n\n    case STATE_FADE_OUT:\n        if (!M_IsFadeActive(p)) {\n            return (PHASE_CONTROL) {\n                .action = PHASE_ACTION_END,\n                .gf_cmd = { .action = p->action },\n            };\n        }\n        break;\n    }\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    const float progress = g_Config.ui.pause_fade_effects\n        ? Fader_GetCurrentValue(&p->fader)\n        : p->fader.args.target;\n    switch (g_Config.ui.pause_background_style) {\n    case BK_NONE:\n        Output_Overlay_DrawGame();\n        break;\n\n    case BK_TRANSPARENT_MEDIUM:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress * 0.5f, false);\n        break;\n\n    case BK_TRANSPARENT_DARK:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress * 0.8f, false);\n        break;\n\n    case BK_BLACK:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress, false);\n        break;\n\n    case BK_MONOCHROME:\n        Output_Overlay_DrawGameMono(progress);\n        break;\n\n    case BK_MONOCHROME_COOL:\n        Output_Overlay_DrawGameMonoCool(progress);\n        break;\n\n    case BK_MONOCHROME_WARM:\n        Output_Overlay_DrawGameMonoWarm(progress);\n        break;\n\n    case BK_PATTERN_STATIC:\n    case BK_PATTERN_WAVE:\n        if (progress < 1.0f) {\n            Output_Overlay_DrawGame();\n        }\n        Output_Overlay_DrawPatternOpacity(\n            g_Config.ui.pause_background_style == BK_PATTERN_WAVE, progress);\n        break;\n\n    case BK_IMAGE:\n    default:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress * 0.8f, false);\n        break;\n    }\n\n    if (p->state == STATE_ASK) {\n        UI_Pause(&p->ui.state);\n    }\n}\n\nPHASE *Phase_Pause_Create(void)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    phase->priv = Memory_Alloc(sizeof(M_PRIV));\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    return phase;\n}\n\nvoid Phase_Pause_Destroy(PHASE *phase)\n{\n    Memory_Free(phase->priv);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_pause.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\nPHASE *Phase_Pause_Create(void);\nvoid Phase_Pause_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_photo_mode.c",
    "content": "#include <trx/game/phase/phase_photo_mode.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/game/cutscene.h>\n#include <trx/game/game.h>\n#include <trx/game/phase/executor.h>\n#include <trx/game/photo_mode.h>\n#include <trx/game/screenshot.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n\ntypedef struct {\n    bool in_cutscene;\n    bool taking_screenshot;\n} M_PRIV;\n\nstatic PHASE_CONTROL M_Start(PHASE *phase)\n{\n    M_PRIV *const p = phase->priv;\n    p->in_cutscene = GF_GetCurrentLevel()->type == GFL_CUTSCENE;\n    PhotoMode_Start();\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    PhotoMode_End();\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Input_Update();\n    Shell_ProcessInput();\n\n    // XXX: normally we'd be using menu_back alone to let the player go back\n    // and exit the photo mode UI, BUT for controller players, the default\n    // menu_back button conflicts with the roll input, as both are bound to the\n    // B button. This causes neither to work as expected, when the player\n    // presses B. This is a hacky solution since technically the player might\n    // remap the roll input to some other button, making the roll check below\n    // redundant, but this is the most straightforward approach.\n    if (g_InputDB.toggle_photo_mode\n        || (g_InputDB.menu_back && !g_InputDB.roll)) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_NOOP },\n        };\n    } else if (g_InputDB.action) {\n        p->taking_screenshot = true;\n        Screenshot_Make(g_Config.rendering.screenshot_format);\n        Sound_Effect(SFX_MENU_LARA_HOME, nullptr, SPM_ALWAYS);\n    }\n    return PhotoMode_Control();\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (p->in_cutscene) {\n        Cutscene_Draw();\n    } else {\n        Game_Draw(false);\n    }\n\n    if (p->taking_screenshot) {\n        p->taking_screenshot = false;\n    } else {\n        UI_PhotoMode(PhotoMode_GetCurrentMode());\n    }\n}\n\nPHASE *Phase_PhotoMode_Create(void)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    phase->priv = Memory_Alloc(sizeof(M_PRIV));\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    return phase;\n}\n\nvoid Phase_PhotoMode_Destroy(PHASE *phase)\n{\n    Memory_Free(phase->priv);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_photo_mode.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\nPHASE *Phase_PhotoMode_Create(void);\nvoid Phase_PhotoMode_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_picture.c",
    "content": "#include <trx/game/phase/phase_picture.h>\n\n#include <trx/core/memory.h>\n#include <trx/game/fader.h>\n#include <trx/game/input.h>\n#include <trx/game/output.h>\n#include <trx/game/shell.h>\n\ntypedef enum {\n    STATE_FADE_IN,\n    STATE_DISPLAY,\n    STATE_FADE_OUT,\n} M_STATE;\n\ntypedef struct {\n    M_STATE state;\n    FADER fader;\n    CLOCK_TIMER timer;\n    PHASE_PICTURE_ARGS args;\n    bool has_drawn;\n} M_PRIV;\n\nstatic bool M_UsesCrossFadeIn(PHASE *const phase)\n{\n    const M_PRIV *const p = phase->priv;\n    return p->args.loading_pic && !p->args.block_cross_fade_in;\n}\n\nstatic void M_FadeOut(M_PRIV *const p)\n{\n    p->state = STATE_FADE_OUT;\n    Fader_InitFromCurrentHold(&p->fader, 1.0f, p->args.fade_out_time, 0.1f);\n}\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (!Output_Overlay_LoadImage(p->args.file_name)) {\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_NOOP },\n        };\n    }\n\n    if (p->args.loading_pic && !p->args.block_cross_fade_in) {\n        Output_Overlay_CaptureSnapshot();\n    }\n    Fader_InitTo(&p->fader, 1.0f, 0.0f, p->args.fade_in_time);\n    ClockTimer_Sync(&p->timer);\n    return (PHASE_CONTROL) {};\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Input_Update();\n    Shell_ProcessInput();\n\n    switch (p->state) {\n    case STATE_FADE_IN:\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back\n            || g_InputDB.menu_skip) {\n            M_FadeOut(p);\n        } else if (!Fader_IsActive(&p->fader)) {\n            p->state = STATE_DISPLAY;\n            ClockTimer_Sync(&p->timer);\n        }\n        break;\n\n    case STATE_DISPLAY:\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back || g_InputDB.menu_skip\n            || ClockTimer_CheckElapsed(\n                &p->timer,\n                p->args.display_time\n                    - (p->args.display_time_includes_fades\n                           ? p->args.fade_in_time + p->args.fade_out_time\n                           : 0.0))) {\n            M_FadeOut(p);\n        }\n        break;\n\n    case STATE_FADE_OUT:\n        if (p->args.loading_pic && p->has_drawn) {\n            Output_Overlay_BeginTransitionFadeOut(\n                p->args.fade_out_time, 1.0f - Fader_GetCurrentValue(&p->fader));\n            return (PHASE_CONTROL) {\n                .action = PHASE_ACTION_END,\n                .gf_cmd = { .action = GF_NOOP },\n            };\n        }\n\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back || g_InputDB.menu_skip\n            || !Fader_IsActive(&p->fader)) {\n            return (PHASE_CONTROL) {\n                .action = PHASE_ACTION_END,\n                .gf_cmd = { .action = GF_NOOP },\n            };\n        }\n        break;\n    }\n\n    return (PHASE_CONTROL) {};\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    const float progress = Fader_GetCurrentValue(&p->fader);\n    if (p->args.loading_pic\n        && (p->state != STATE_FADE_IN || !p->args.block_cross_fade_in)) {\n        Output_Overlay_DrawImage(p->args.file_name);\n        if (p->state == STATE_FADE_IN) {\n            Output_Overlay_DrawSnapshot(progress);\n        }\n    } else {\n        Output_Overlay_DrawImage(p->args.file_name);\n        Output_Overlay_DrawBlackRectangle(progress, false);\n    }\n    p->has_drawn = true;\n}\n\nPHASE *Phase_Picture_Create(const PHASE_PICTURE_ARGS args)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV));\n    p->args = args;\n    p->state = STATE_FADE_IN;\n    p->has_drawn = false;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    phase->request_fade_to_black = nullptr;\n    phase->uses_cross_fade_in = M_UsesCrossFadeIn;\n    return phase;\n}\n\nvoid Phase_Picture_Destroy(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Memory_Free(p);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_picture.h",
    "content": "#pragma once\n\n#include <trx/game/phase/types.h>\n\ntypedef struct {\n    const char *file_name;\n    double display_time;\n    double fade_in_time;\n    double fade_out_time;\n    bool display_time_includes_fades;\n    bool loading_pic;\n    bool block_cross_fade_in;\n} PHASE_PICTURE_ARGS;\n\nPHASE *Phase_Picture_Create(PHASE_PICTURE_ARGS args);\nvoid Phase_Picture_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/phase_stats.c",
    "content": "#include <trx/game/phase/phase_stats.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/fader.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/input.h>\n#include <trx/game/output.h>\n#include <trx/game/shell.h>\n#include <trx/game/ui.h>\n\ntypedef enum {\n    STATE_FADE_IN,\n    STATE_DISPLAY,\n    STATE_FADE_OUT,\n    STATE_FINISH,\n} STATE;\n\ntypedef struct {\n    PHASE_STATS_ARGS args;\n    STATE state;\n    FADER back_fader;\n    FADER top_fader;\n    bool ui_active;\n    UI_STATS_DIALOG_STATE *ui_state;\n} M_PRIV;\n\nstatic bool M_EnableFade(const M_PRIV *const p)\n{\n    return g_Config.ui.stats_fade_effects || p->args.show_final_stats;\n}\n\nstatic bool M_IsFading(const M_PRIV *const p)\n{\n    return M_EnableFade(p)\n        && (Fader_IsActive(&p->top_fader) || Fader_IsActive(&p->back_fader));\n}\n\nstatic void M_FadeIn(M_PRIV *const p)\n{\n    if (p->args.background_path != nullptr) {\n        Fader_InitTo(&p->back_fader, 1.0f, 0.0f, 1.0);\n    } else {\n        Fader_InitTo(&p->back_fader, 0.0f, 1.0f, 0.5);\n    }\n}\n\nstatic void M_FadeOut(M_PRIV *const p)\n{\n    p->state = STATE_FINISH;\n}\n\nstatic PHASE_CONTROL M_Start(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    if (!Game_IsInGym()) {\n        p->ui_state = UI_StatsDialog_Init((UI_STATS_DIALOG_ARGS) {\n            .mode = p->args.show_final_stats ? UI_STATS_DIALOG_MODE_FINAL\n                                             : UI_STATS_DIALOG_MODE_LEVEL,\n            .style = p->args.use_bare_style ? UI_STATS_DIALOG_STYLE_BARE\n                                            : UI_STATS_DIALOG_STYLE_BORDERED,\n            .level_num = p->args.level_num != -1 ? p->args.level_num\n                                                 : Game_GetCurrentLevel()->num,\n        });\n        if (p->args.show_final_stats\n            && !UI_StatsDialog_HasVisibleRows(p->ui_state)) {\n            UI_StatsDialog_Free(p->ui_state);\n            p->ui_state = nullptr;\n            p->state = STATE_FINISH;\n            return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n        }\n    }\n\n    switch (p->args.background_type) {\n    case BK_IMAGE:\n        if (p->args.background_path == nullptr) {\n            LOG_WARNING(\"Trying to load empty background image\");\n        } else if (!Output_Overlay_LoadImage(p->args.background_path)) {\n            LOG_WARNING(\n                \"Failed to load background image: %s\", p->args.background_path);\n        }\n        break;\n\n    case BK_NONE:\n    case BK_PATTERN_STATIC:\n    case BK_PATTERN_WAVE:\n    case BK_BLACK:\n    case BK_MONOCHROME:\n    case BK_MONOCHROME_COOL:\n    case BK_MONOCHROME_WARM:\n    case BK_TRANSPARENT_MEDIUM:\n    case BK_TRANSPARENT_DARK:\n        break;\n    }\n\n    if (Game_IsInGym()) {\n        M_FadeOut(p);\n    } else {\n        if (p->args.background_type == BK_PATTERN_STATIC\n            || p->args.background_type == BK_PATTERN_WAVE) {\n            p->state = STATE_DISPLAY;\n        } else {\n            p->state = STATE_FADE_IN;\n            M_FadeIn(p);\n        }\n\n        p->ui_active = true;\n    }\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic void M_End(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    if (p->ui_active) {\n        p->ui_active = false;\n        UI_StatsDialog_Free(p->ui_state);\n        p->ui_state = nullptr;\n    }\n}\n\nstatic PHASE_CONTROL M_Control(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Input_Update();\n    Shell_ProcessInput();\n\n    switch (p->state) {\n    case STATE_FADE_IN:\n        if (!M_IsFading(p)) {\n            p->state = STATE_DISPLAY;\n        } else if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n            M_FadeOut(p);\n        }\n        break;\n\n    case STATE_DISPLAY:\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n            M_FadeOut(p);\n        }\n        break;\n\n    case STATE_FADE_OUT:\n        p->state = STATE_FINISH;\n        return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT };\n        break;\n\n    case STATE_FINISH:\n        return (PHASE_CONTROL) {\n            .action = PHASE_ACTION_END,\n            .gf_cmd = { .action = GF_NOOP },\n        };\n    }\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nstatic bool M_RequestFadeToBlack(PHASE *const phase, FADER_ARGS *const out_args)\n{\n    M_PRIV *const p = phase->priv;\n    if (!M_EnableFade(p)) {\n        return false;\n    }\n\n    if (out_args != nullptr) {\n        *out_args = (FADER_ARGS) {\n            .from_current = false,\n            .initial = 0.0f,\n            .target = 1.0f,\n            .duration = 0.5f,\n            .debuff = 0.1f,\n        };\n    }\n    return true;\n}\n\nstatic void M_Draw(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n\n    const float top_opacity = M_EnableFade(p)\n        ? Fader_GetCurrentValue(&p->top_fader)\n        : p->top_fader.args.target;\n    if (top_opacity > 0.0f) {\n        Output_Overlay_DrawSnapshot(1.0f);\n        Output_Overlay_DrawBlackRectangle(top_opacity, false);\n        return;\n    }\n\n    const float progress = M_EnableFade(p)\n        ? Fader_GetCurrentValue(&p->back_fader)\n        : p->back_fader.args.target;\n    switch (p->args.background_type) {\n    case BK_NONE:\n        Output_Overlay_DrawGame();\n        break;\n\n    case BK_TRANSPARENT_MEDIUM:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress * 0.5f, false);\n        break;\n\n    case BK_TRANSPARENT_DARK:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress * 0.8f, false);\n        break;\n\n    case BK_BLACK:\n        Output_Overlay_DrawGame();\n        Output_Overlay_DrawBlackRectangle(progress, false);\n        break;\n\n    case BK_MONOCHROME:\n        Output_Overlay_DrawGameMono(progress);\n        break;\n\n    case BK_MONOCHROME_COOL:\n        Output_Overlay_DrawGameMonoCool(progress);\n        break;\n\n    case BK_MONOCHROME_WARM:\n        Output_Overlay_DrawGameMonoWarm(progress);\n        break;\n\n    case BK_IMAGE:\n        if (p->args.background_path != nullptr) {\n            Output_Overlay_DrawImageBilinear(p->args.background_path);\n        }\n        Output_Overlay_DrawBlackRectangle(progress, false);\n        break;\n\n    case BK_PATTERN_STATIC:\n    case BK_PATTERN_WAVE:\n        Output_Overlay_DrawPattern(p->args.background_type == BK_PATTERN_WAVE);\n        Output_Overlay_DrawBlackRectangle(progress, false);\n        break;\n\n    default:\n        break;\n    }\n\n    if (p->ui_active) {\n        UI_StatsDialog(p->ui_state);\n    }\n}\n\nPHASE *Phase_Stats_Create(const PHASE_STATS_ARGS args)\n{\n    PHASE *const phase = Memory_Alloc(sizeof(PHASE));\n    M_PRIV *const p = Memory_Alloc(sizeof(M_PRIV));\n    p->args = args;\n    p->state = STATE_FADE_IN;\n    phase->priv = p;\n    phase->start = M_Start;\n    phase->end = M_End;\n    phase->control = M_Control;\n    phase->draw = M_Draw;\n    phase->request_fade_to_black = M_RequestFadeToBlack;\n    phase->uses_cross_fade_in = nullptr;\n    return phase;\n}\n\nvoid Phase_Stats_Destroy(PHASE *const phase)\n{\n    M_PRIV *const p = phase->priv;\n    Memory_Free(p);\n    Memory_Free(phase);\n}\n"
  },
  {
    "path": "src/trx/game/phase/phase_stats.h",
    "content": "#pragma once\n\n#include <trx/config/enum.h>\n#include <trx/game/phase/types.h>\n\ntypedef struct {\n    BACKGROUND_TYPE background_type;\n    const char *background_path;\n    bool show_final_stats;\n    bool use_bare_style;\n    int32_t level_num;\n} PHASE_STATS_ARGS;\n\nPHASE *Phase_Stats_Create(PHASE_STATS_ARGS args);\nvoid Phase_Stats_Destroy(PHASE *phase);\n"
  },
  {
    "path": "src/trx/game/phase/types.h",
    "content": "#pragma once\n\n#include <trx/game/fader.h>\n#include <trx/game/phase/control.h>\n\ntypedef struct PHASE PHASE;\n\ntypedef PHASE_CONTROL (*PHASE_START_FUNC)(PHASE *phase);\ntypedef void (*PHASE_END_FUNC)(PHASE *phase);\ntypedef void (*PHASE_SUSPEND_FUNC)(PHASE *phase);\ntypedef void (*PHASE_RESUME_FUNC)(PHASE *phase);\ntypedef PHASE_CONTROL (*PHASE_CONTROL_FUNC)(PHASE *phase);\ntypedef void (*PHASE_DRAW_FUNC)(PHASE *phase);\ntypedef bool (*PHASE_REQUEST_FADE_TO_BLACK_FUNC)(\n    PHASE *phase, FADER_ARGS *out_args);\ntypedef bool (*PHASE_USES_CROSS_FADE_IN_FUNC)(PHASE *phase);\n\ntypedef struct PHASE {\n    PHASE_START_FUNC start;\n    PHASE_END_FUNC end;\n    PHASE_SUSPEND_FUNC suspend;\n    PHASE_RESUME_FUNC resume;\n    PHASE_CONTROL_FUNC control;\n    PHASE_DRAW_FUNC draw;\n    PHASE_REQUEST_FADE_TO_BLACK_FUNC request_fade_to_black;\n    PHASE_USES_CROSS_FADE_IN_FUNC uses_cross_fade_in;\n    void *priv;\n} PHASE;\n"
  },
  {
    "path": "src/trx/game/phase.h",
    "content": "#pragma once\n\n#include <trx/game/phase/control.h>\n#include <trx/game/phase/executor.h>\n#include <trx/game/phase/phase_cutscene.h>\n#include <trx/game/phase/phase_demo.h>\n#include <trx/game/phase/phase_game.h>\n#include <trx/game/phase/phase_globe_select.h>\n#include <trx/game/phase/phase_inventory.h>\n#include <trx/game/phase/phase_pause.h>\n#include <trx/game/phase/phase_photo_mode.h>\n#include <trx/game/phase/phase_picture.h>\n#include <trx/game/phase/phase_stats.h>\n#include <trx/game/phase/types.h>\n"
  },
  {
    "path": "src/trx/game/photo_mode.c",
    "content": "#include <trx/game/photo_mode.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/core/memory.h>\n#include <trx/game/camera.h>\n#include <trx/game/collision.h>\n#include <trx/game/console/common.h>\n#include <trx/game/const.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/interpolation.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/hair.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/matrix.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/phase/executor.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui.h>\n\n#define M_INTERPOLATION_STEP 0.25\n\ntypedef struct {\n    PHOTO_MODE current_mode;\n    bool show_fps_counter;\n    double rate;\n    bool lara_pos_touched;\n    XYZ_32 orig_lara_pos;\n    XYZ_16 orig_lara_rot;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\nstatic void M_ApplyInterpolation(void)\n{\n    Interpolation_CommitLara();\n    Lara_Hair_Control(true);\n    Interpolation_CommitBraid();\n}\n\nstatic void M_RememberLaraPos(M_PRIV *const p)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item != nullptr) {\n        p->orig_lara_pos = lara_item->pos;\n        p->orig_lara_rot = lara_item->rot;\n    }\n}\n\nstatic void M_RestoreLaraPos(M_PRIV *const p)\n{\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item != nullptr) {\n        lara_item->pos = p->orig_lara_pos;\n        lara_item->rot = p->orig_lara_rot;\n    }\n}\n\nstatic PHASE_CONTROL M_AdvanceFrame(M_PRIV *const p)\n{\n    PHASE_CONTROL result = (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n    PHASE *const phase = PhaseExecutor_GetOuterPhase();\n    if (phase == nullptr || phase->control == nullptr\n        || phase->draw == nullptr) {\n        return result;\n    }\n\n    XYZ_32 prev_lara_pos;\n    XYZ_16 prev_lara_rot;\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item != nullptr) {\n        prev_lara_pos = lara_item->pos;\n        prev_lara_rot = lara_item->rot;\n    }\n    M_RestoreLaraPos(p);\n\n    Camera_PhotoMode_Pause();\n    const bool is_enabled = Interpolation_IsEnabled();\n    const bool slow = g_Input.slow;\n    Interpolation_Enable();\n\n    if (phase->resume != nullptr) {\n        phase->resume(phase);\n    }\n    if (p->rate >= 1.0) {\n        InputState_Clear(&g_Input);\n        InputState_Clear(&g_InputDB);\n        result = phase->control(phase);\n        InputState_Clear(&g_Input);\n        InputState_Clear(&g_InputDB);\n        p->rate = 0.0;\n    }\n    p->rate += slow ? M_INTERPOLATION_STEP : 1.0f;\n    Interpolation_SetRate(p->rate);\n    phase->draw(phase);\n    if (phase->suspend != nullptr) {\n        phase->suspend(phase);\n    }\n    if (!is_enabled) {\n        Interpolation_Disable();\n    }\n\n    Camera_PhotoMode_Resume();\n\n    M_RememberLaraPos(p);\n    if (lara_item != nullptr && p->lara_pos_touched) {\n        lara_item->pos = prev_lara_pos;\n        lara_item->rot = prev_lara_rot;\n        Lara_Hair_Initialise();\n        M_ApplyInterpolation();\n    }\n    return result;\n}\n\nstatic bool M_HandleItemPositionInputs(ITEM *const item)\n{\n    const int32_t trans_speed = STEP_L / 10;\n\n    XYZ_32 delta = {};\n    if (g_Input.camera_left) {\n        delta.x -= trans_speed;\n    } else if (g_Input.camera_right) {\n        delta.x += trans_speed;\n    }\n    if (g_Input.camera_forward) {\n        delta.z += trans_speed;\n    } else if (g_Input.camera_back) {\n        delta.z -= trans_speed;\n    }\n    if (!g_Input.slow && g_Input.camera_up) {\n        delta.y -= trans_speed;\n    } else if (!g_Input.slow && g_Input.camera_down) {\n        delta.y += trans_speed;\n    }\n\n    if (delta.x == 0 && delta.y == 0 && delta.z == 0) {\n        return false;\n    }\n\n    const int32_t pitch = item->rot.x;\n    const int32_t cos_p = Math_Cos(pitch);\n    const int32_t sin_p = Math_Sin(pitch);\n    const int32_t local_y = delta.y - TRIGMULT2(sin_p, delta.z);\n    const int32_t local_z = TRIGMULT2(cos_p, delta.z);\n    const int32_t local_x = delta.x;\n\n    const int32_t yaw = item->rot.y;\n    const int32_t cos_y = Math_Cos(yaw);\n    const int32_t sin_y = Math_Sin(yaw);\n    const int32_t world_x =\n        TRIGMULT2(cos_y, local_x) + TRIGMULT2(sin_y, local_z);\n    const int32_t world_z =\n        -TRIGMULT2(sin_y, local_x) + TRIGMULT2(cos_y, local_z);\n\n    item->pos.x += world_x;\n    item->pos.y += local_y;\n    item->pos.z += world_z;\n    return true;\n}\n\nstatic bool M_HandleItemRotationInputs(ITEM *const item)\n{\n    const int32_t rot_speed = DEG_1 * 2;\n    XYZ_32 delta = {};\n    if (g_Input.left) {\n        delta.y = -rot_speed;\n    } else if (g_Input.right) {\n        delta.y = +rot_speed;\n    }\n    if (g_Input.forward) {\n        delta.x = -rot_speed;\n    } else if (g_Input.back) {\n        delta.x = +rot_speed;\n    }\n    if (g_Input.slow && g_Input.camera_up) {\n        delta.z = -rot_speed;\n    } else if (g_Input.slow && g_Input.camera_down) {\n        delta.z = +rot_speed;\n    }\n    if (g_InputDB.roll) {\n        delta.y = DEG_90;\n    }\n    if (delta.x == 0 && delta.y == 0 && delta.z == 0) {\n        return false;\n    }\n\n    // Keep the item's root joint anchored while rotating, so off-center\n    // animation origins (especially in cutscenes) do not cause huge offsets.\n    // Use live item transforms; interpolation may still contain previous-frame\n    // values and would make both samples identical.\n    const bool was_item_interp_enabled = item->enable_interpolation;\n    item->enable_interpolation = false;\n\n    XYZ_32 old_root_pos = {};\n    Collide_GetJointAbsPosition(item, &old_root_pos, 0);\n\n    item->rot.x += delta.x;\n    item->rot.y += delta.y;\n    item->rot.z += delta.z;\n\n    XYZ_32 new_root_pos = {};\n    Collide_GetJointAbsPosition(item, &new_root_pos, 0);\n    item->enable_interpolation = was_item_interp_enabled;\n    item->pos.x += old_root_pos.x - new_root_pos.x;\n    item->pos.y += old_root_pos.y - new_root_pos.y;\n    item->pos.z += old_root_pos.z - new_root_pos.z;\n    return true;\n}\n\nstatic void M_HandleEditLaraMode(M_PRIV *const p)\n{\n    Camera_PhotoMode_UpdateFOV();\n\n    ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return;\n    }\n\n    bool changed = false;\n    changed |= M_HandleItemPositionInputs(lara_item);\n    changed |= M_HandleItemRotationInputs(lara_item);\n\n    if (changed) {\n        p->lara_pos_touched = true;\n    }\n\n    if (g_InputDB.look) {\n        M_RestoreLaraPos(p);\n        changed = true;\n        p->lara_pos_touched = false;\n    }\n\n    if (changed) {\n        M_ApplyInterpolation();\n    }\n}\n\nstatic void M_HandleCameraMode(M_PRIV *const p)\n{\n    Camera_PhotoMode_Update();\n}\n\nvoid PhotoMode_Start(void)\n{\n    M_PRIV *const p = &m_Priv;\n    p->show_fps_counter = g_Config.ui.enable_fps_counter;\n    p->rate = 1.0;\n    p->current_mode = PHOTO_MODE_CAMERA;\n    p->lara_pos_touched = false;\n    g_Config.ui.enable_fps_counter = false;\n\n    M_RememberLaraPos(p);\n    Camera_PhotoMode_Enter();\n    Music_Pause();\n    Sound_PauseAll();\n}\n\nvoid PhotoMode_End(void)\n{\n    M_PRIV *const p = &m_Priv;\n    Camera_PhotoMode_Exit();\n    M_RestoreLaraPos(p);\n\n    g_Config.ui.enable_fps_counter = p->show_fps_counter;\n    Music_Unpause();\n    Sound_UnpauseAll();\n}\n\nPHASE_CONTROL PhotoMode_Control(void)\n{\n    M_PRIV *const p = &m_Priv;\n    Interpolation_Remember();\n\n    if (g_InputDB.pause) {\n        return M_AdvanceFrame(p);\n    } else if (g_InputDB.toggle_ui) {\n        UI_ToggleState(&g_Config.ui.enable_photo_mode_ui);\n    } else if (g_InputDB.step_left) {\n        if (p->current_mode == 0) {\n            p->current_mode = PHOTO_MODE_LAST;\n        } else {\n            p->current_mode--;\n        }\n    } else if (g_InputDB.step_right) {\n        if (p->current_mode == PHOTO_MODE_LAST) {\n            p->current_mode = 0;\n        } else {\n            p->current_mode++;\n        }\n    } else if (g_InputDB.fly_cheat) {\n        Lara_Pose_Cycle(g_Input.slow ? -1 : 1);\n    } else {\n        switch (p->current_mode) {\n        case PHOTO_MODE_LARA_POS:\n            M_HandleEditLaraMode(p);\n            break;\n        case PHOTO_MODE_CAMERA:\n            M_HandleCameraMode(p);\n            break;\n        }\n    }\n\n    return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };\n}\n\nPHOTO_MODE PhotoMode_GetCurrentMode(void)\n{\n    M_PRIV *const p = &m_Priv;\n    return p->current_mode;\n}\n"
  },
  {
    "path": "src/trx/game/photo_mode.h",
    "content": "#pragma once\n\n#include <trx/game/phase/executor.h>\n\ntypedef enum {\n    PHOTO_MODE_CAMERA,\n    PHOTO_MODE_LARA_POS,\n    PHOTO_MODE_LAST = PHOTO_MODE_LARA_POS,\n} PHOTO_MODE;\n\nvoid PhotoMode_Start(void);\nvoid PhotoMode_End(void);\n\nPHOTO_MODE PhotoMode_GetCurrentMode(void);\nPHASE_CONTROL PhotoMode_Control(void);\n"
  },
  {
    "path": "src/trx/game/random.c",
    "content": "#include <trx/game/random.h>\n\n#include <trx/core/log.h>\n\n#include <time.h>\n\nstatic uint32_t m_RandControl = 0xD371F947U;\nstatic uint32_t m_RandDraw = 0xD371F947U;\nstatic bool m_IsDrawFrozen = false;\n\nvoid Random_Seed(void)\n{\n    time_t lt = time(0);\n    struct tm *tptr = localtime(&lt);\n    Random_SeedControl(tptr->tm_sec + 57 * tptr->tm_min + 3543 * tptr->tm_hour);\n    Random_SeedDraw(tptr->tm_sec + 43 * tptr->tm_min + 3477 * tptr->tm_hour);\n}\n\nvoid Random_SeedControl(int32_t seed)\n{\n    LOG_DEBUG(\"%d\", seed);\n    m_RandControl = (uint32_t)seed;\n}\n\nint32_t Random_GetControl(void)\n{\n    m_RandControl = 0x41C64E6DU * m_RandControl + 0x3039U;\n    return (int32_t)((m_RandControl >> 10) & 0x7FFFU);\n}\n\nvoid Random_SeedDraw(int32_t seed)\n{\n    LOG_DEBUG(\"%d\", seed);\n    m_RandDraw = (uint32_t)seed;\n}\n\nint32_t Random_GetDraw(void)\n{\n    // Allow draw RNG to advance only during initial game setup (for such things\n    // as caustic initialisation) and normal game play. RNG should remain static\n    // when the game output is paused e.g. inventory, pause screen etc.\n    if (!m_IsDrawFrozen) {\n        m_RandDraw = 0x41C64E6DU * m_RandDraw + 0x3039U;\n    }\n    return (int32_t)((m_RandDraw >> 10) & 0x7FFFU);\n}\n\nint32_t Random_GetControlSeed(void)\n{\n    return (int32_t)m_RandControl;\n}\n\nint32_t Random_GetDrawSeed(void)\n{\n    return (int32_t)m_RandDraw;\n}\n\nvoid Random_FreezeDraw(bool is_frozen)\n{\n    m_IsDrawFrozen = is_frozen;\n}\n"
  },
  {
    "path": "src/trx/game/random.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid Random_Seed(void);\nvoid Random_SeedControl(int32_t seed);\nvoid Random_SeedDraw(int32_t seed);\n\nint32_t Random_GetControl(void);\nint32_t Random_GetDraw(void);\nint32_t Random_GetControlSeed(void);\nint32_t Random_GetDrawSeed(void);\n\nvoid Random_FreezeDraw(bool is_frozen);\n"
  },
  {
    "path": "src/trx/game/replay/test_recorder.c",
    "content": "#include <trx/game/replay/test_recorder.h>\n\n#include <trx/config.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/console/common.h>\n#include <trx/game/events.h>\n#include <trx/game/input/backends/controller.h>\n#include <trx/game/input/backends/keyboard.h>\n#include <trx/game/input/common.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n\n#include <stdlib.h>\n#include <string.h>\n\n#define M_DEBUG 0\n#define M_MAX_EVENTS 64 // Maximum SDL or custom events per frame\n\n// Internal event codes for recorder swimlane\ntypedef enum {\n    M_CUSTOM_EVENT_SCREENSHOT,\n    M_CUSTOM_EVENT_COMMAND,\n} M_CUSTOM_EVENT;\n\ntypedef struct {\n    MYFILE *file;\n    int32_t prev_frame_idx;\n    int32_t frame_idx;\n    SDL_Event queue[M_MAX_EVENTS];\n    int32_t queue_size;\n    int32_t listeners[2];\n} M_PRIV;\n\nstatic const struct {\n    const char *arg;\n    bool takes_value;\n} m_SkipArgs[] = {\n    { \"--debug-render-performance\", false },\n    { \"--test-record\", true },\n    { \"--test-replay\", true },\n    { \"--test-play\", true },\n    { \"--headless-fps\", true },\n    { nullptr, false },\n};\n\nstatic M_PRIV m_Priv = {};\n\nstatic int M_CompareConfigOption(const void *a, const void *b)\n{\n    const CONFIG_OPTION *const *opt_a = a;\n    const CONFIG_OPTION *const *opt_b = b;\n    return strcmp((*opt_a)->name, (*opt_b)->name);\n}\n\nstatic const char *M_DumpEvent(const SDL_Event *const event)\n{\n    switch (event->type) {\n    case SDL_USEREVENT:\n        const char *result = nullptr;\n        if (event->user.code == M_CUSTOM_EVENT_SCREENSHOT) {\n            char *path = event->user.data1;\n            result = String_FormatStatic(\"noop  # cmd { screenshot %s }\", path);\n            Memory_FreePointer(&path);\n        } else if (event->user.code == M_CUSTOM_EVENT_COMMAND) {\n            char *cmd = event->user.data1;\n            result = String_FormatStatic(\"noop  # cmd { %s }\", cmd);\n            Memory_FreePointer(&cmd);\n        }\n        return result;\n\n    case SDL_KEYDOWN:\n        // NOTE: we do not serialize the modifiers to avoid noise, as currently\n        // they are unused by the engine. In the future, once we add support\n        // for compound keybindings, it may become necessary to either\n        // serialize them, or simulate them in the replay module.\n        return String_FormatStatic(\n            \"● \\\"%s\\\"\", Input_KeyDescFromSDL(event->key.keysym.scancode, 0));\n\n    case SDL_KEYUP:\n        return String_FormatStatic(\n            \"○ \\\"%s\\\"\", Input_KeyDescFromSDL(event->key.keysym.scancode, 0));\n\n    case SDL_TEXTINPUT:\n        return String_FormatStatic(\"text-input \\\"%s\\\"\", event->text.text);\n\n    case SDL_QUIT:\n        return String_FormatStatic(\"quit\");\n    }\n\n    return nullptr;\n}\n\nstatic void M_DumpQueue(M_PRIV *const p)\n{\n#if !M_DEBUG\n    if (p->queue_size == 0) {\n        return;\n    }\n#endif\n    const size_t indent = 8;\n    File_WriteString(\n        p->file, \"%-*s\", indent,\n        String_FormatStatic(\"@+%d:\", p->frame_idx - p->prev_frame_idx));\n    for (int32_t i = 0; i < p->queue_size; i++) {\n        const SDL_Event *const event = &p->queue[i];\n        const char *const event_str = M_DumpEvent(event);\n        if (event_str == nullptr) {\n            continue;\n        }\n        File_WriteString(p->file, event_str);\n        if (i < p->queue_size - 1) {\n            File_WriteString(p->file, \"\\n%*s\", indent, \"\");\n        }\n    }\n#if M_DEBUG\n    if (p->queue_size == 0) {\n        File_WriteString(p->file, \"noop\");\n    }\n    const ITEM *const lara_item = Lara_GetItem();\n    const OBJECT_ID obj_id = Lara_GetAnimationObject();\n    const ITEM *const vehicle_item = Lara_Vehicle_GetItem();\n    if (lara_item != nullptr) {\n        File_WriteString(p->file, \"\\n%*s\", indent, \"\");\n        File_WriteString(\n            p->file, \"assert lara.pos=%d,%d,%d\", lara_item->pos.x,\n            lara_item->pos.y, lara_item->pos.z);\n        File_WriteString(p->file, \"\\n%*s\", indent, \"\");\n        File_WriteString(\n            p->file, \"assert lara.rot=%d,%d,%d\", lara_item->rot.x,\n            lara_item->rot.y, lara_item->rot.z);\n        File_WriteString(p->file, \"\\n%*s\", indent, \"\");\n        File_WriteString(\n            p->file, \"assert lara.anim=%d,%d,%d\", obj_id,\n            Item_GetRelativeObjAnim(lara_item, obj_id),\n            Item_GetRelativeFrame(lara_item));\n        File_WriteString(p->file, \"\\n%*s\", indent, \"\");\n        File_WriteString(\n            p->file, \"assert lara.speed=%d,%d\",\n            (vehicle_item != nullptr ? vehicle_item : lara_item)->speed,\n            (vehicle_item != nullptr ? vehicle_item : lara_item)->fall_speed);\n    }\n#endif\n    File_WriteString(p->file, \"\\n\");\n    p->prev_frame_idx = p->frame_idx;\n}\n\nstatic void M_DumpHeader(MYFILE *const fp)\n{\n    File_WriteString(fp, \"seed_control %d\\n\", Random_GetControlSeed());\n    File_WriteString(fp, \"seed_draw %d\\n\", Random_GetDrawSeed());\n}\n\nstatic void M_DumpArguments(MYFILE *const fp, VECTOR *const original_args)\n{\n    // Record original arguments passed to the game\n    if (original_args->count <= 0) {\n        return;\n    }\n\n    // Skip tracking irrelevant arguments.\n    VECTOR *const filtered_args = Vector_Create(sizeof(char *));\n    for (int32_t i = 0; i < original_args->count; i++) {\n        const char *const arg = *(char **)Vector_Get(original_args, i);\n        int32_t skip = 0;\n        for (size_t j = 0; m_SkipArgs[j].arg != nullptr; j++) {\n            if (strcmp(arg, m_SkipArgs[j].arg) == 0) {\n                skip = 1 + m_SkipArgs[j].takes_value;\n                break;\n            }\n        }\n        if (skip) {\n            i += skip - 1;\n        } else {\n            Vector_Add(filtered_args, &arg);\n        }\n    }\n\n    if (filtered_args->count > 0) {\n        File_WriteString(fp, \"args\");\n        for (int32_t i = 0; i < filtered_args->count; i++) {\n            const char *const arg = *(char **)Vector_Get(filtered_args, i);\n            File_WriteString(fp, \" \\\"%s\\\"\", arg);\n        }\n        File_WriteString(fp, \"\\n\");\n    }\n    Vector_Free(filtered_args);\n}\n\nstatic void M_DumpConfig(MYFILE *const fp)\n{\n    // Record any non-default config options for later replay\n    const CONFIG_OPTION *const map = Config_GetOptionMap();\n    VECTOR *opts = Vector_Create(sizeof(CONFIG_OPTION *));\n\n    for (const CONFIG_OPTION *opt = map; opt->name != nullptr; opt++) {\n        if (Config_IsOptionAtDefault(opt->target)) {\n            continue;\n        }\n        Vector_Add(opts, &opt);\n    }\n\n    CONFIG_OPTION **raw_opts = Vector_GetData(opts);\n    qsort(\n        raw_opts, opts->count, sizeof(CONFIG_OPTION *), M_CompareConfigOption);\n    for (int32_t i = 0; i < opts->count; i++) {\n        const CONFIG_OPTION *opt = raw_opts[i];\n        const char *const fmt = opt->type == COT_ENUM || opt->type == COT_STRING\n                || opt->type == COT_DYNAMIC_ENUM\n            ? \"config %s \\\"%s\\\"\\n\"\n            : \"config %s %s\\n\";\n        File_WriteString(\n            fp, fmt, opt->name, Config_GetOptionValueAsString(opt, false));\n    }\n    Vector_Free(opts);\n}\n\nstatic void M_DumpBindings(MYFILE *const fp)\n{\n    // Record any non-default key/controller bindings for later replay.\n    // Keyboard binds\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        JSON_OBJECT *bind = JSON_ObjectNew();\n        if (g_Input_Keyboard.assign_to_json_object(\n                g_Config.input.keyboard_layout, role, 0, bind)) {\n            const SDL_Scancode sc =\n                JSON_ObjectGetInt(bind, \"scancode\", SDL_SCANCODE_UNKNOWN);\n            const char *const key_desc =\n                sc == SDL_SCANCODE_UNKNOWN ? \"\" : Input_KeyDescFromSDL(sc, 0);\n            File_WriteString(\n                fp, \"bind keyboard %s \\\"%s\\\"\\n\",\n                ENUM_MAP_TO_STRING(INPUT_ROLE, role), key_desc);\n        }\n        JSON_ObjectFree(bind);\n    }\n    // Controller binds\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        JSON_OBJECT *bind = JSON_ObjectNew();\n        if (g_Input_Controller.assign_to_json_object(\n                g_Config.input.controller_layout, role, 0, bind)) {\n            const int32_t bt = JSON_ObjectGetInt(bind, \"button_type\", 0);\n            const int32_t b = JSON_ObjectGetInt(bind, \"bind\", 0);\n            const int32_t ad = JSON_ObjectGetInt(bind, \"axis_dir\", 0);\n            File_WriteString(\n                fp, \"bind controller %s %d %d %d\\n\",\n                ENUM_MAP_TO_STRING(INPUT_ROLE, role), bt, b, ad);\n        }\n        JSON_ObjectFree(bind);\n    }\n    File_WriteString(fp, \"\\n\");\n}\n\n// Callback for game events: inject synthetic SDL_USEREVENT into queue\nstatic void M_HandleGameEvent(const EVENT *const event, void *const user_data)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->file == nullptr || p->queue_size >= M_MAX_EVENTS) {\n        return;\n    }\n    SDL_Event ev = { .type = SDL_USEREVENT };\n    ev.user.code = (strcmp(event->name, GAME_EVENT_SCREENSHOT) == 0)\n        ? M_CUSTOM_EVENT_SCREENSHOT\n        : M_CUSTOM_EVENT_COMMAND;\n    ev.user.data1 = Memory_DupStr(event->data);\n    ev.user.data2 = nullptr;\n    p->queue[p->queue_size++] = ev;\n}\n\nvoid TestRecorder_Open(const char *path, VECTOR *const original_args)\n{\n    M_PRIV *const p = &m_Priv;\n    p->file = File_Open(path, FILE_OPEN_WRITE);\n    if (p->file == nullptr) {\n        LOG_ERROR(\"Cannot open record file '%s'\", path);\n        return;\n    }\n\n    M_DumpHeader(p->file);\n    M_DumpArguments(p->file, original_args);\n    M_DumpConfig(p->file);\n    M_DumpBindings(p->file);\n\n    p->listeners[0] = GameEvent_Subscribe(\n        GAME_EVENT_SCREENSHOT, nullptr, M_HandleGameEvent, nullptr);\n    p->listeners[1] = GameEvent_Subscribe(\n        GAME_EVENT_COMMAND, nullptr, M_HandleGameEvent, nullptr);\n\n    LOG_INFO(\"Starting recording\");\n}\n\nbool TestRecorder_IsOpened(void)\n{\n    M_PRIV *const p = &m_Priv;\n    return p->file != nullptr;\n}\n\nvoid TestRecorder_Close(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->file != nullptr) {\n        File_Close(p->file);\n        p->file = nullptr;\n    }\n\n    GameEvent_Unsubscribe(p->listeners[0]);\n    GameEvent_Unsubscribe(p->listeners[1]);\n}\n\nvoid TestRecorder_BeginFrame(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->file != nullptr) {\n        p->queue_size = 0;\n    }\n}\n\nvoid TestRecorder_EndFrame(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->file != nullptr) {\n        M_DumpQueue(p);\n    }\n    p->frame_idx++;\n}\n\nvoid TestRecorder_RecordEvent(const SDL_Event *const event)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->file == nullptr) {\n        return;\n    }\n\n    // Only record eligible events\n    if (event->type != SDL_KEYDOWN && event->type != SDL_KEYUP\n        && event->type != SDL_QUIT && event->type != SDL_TEXTINPUT\n        && event->type != SDL_USEREVENT) {\n        return;\n    }\n    if (event->type == SDL_KEYDOWN && event->key.repeat) {\n        return;\n    }\n    if (event->type == SDL_TEXTINPUT && !Console_IsOpened()) {\n        return;\n    }\n\n    if (p->queue_size < M_MAX_EVENTS) {\n        p->queue[p->queue_size++] = *event;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/replay/test_recorder.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n#include <trx/game/shell/args.h>\n\n#include <SDL2/SDL_events.h>\n\n// Test replay: a module to record game playthroughs.\n// ============================================================================\n\n// Initialize test recorder for recording mode.\n// @param path  Path to the recording to write to.\nvoid TestRecorder_Open(const char *path, VECTOR *original_args);\n\n// Close the recorder.\nvoid TestRecorder_Close(void);\n\n// Return whether the recording mode is currently active.\nbool TestRecorder_IsOpened(void);\n\n// Should be called at the start of each frame to handle skip logic.\nvoid TestRecorder_BeginFrame(void);\n\n// Record a single SDL_Event. Called for each event polled. Only essential\n// events are recorded.\n// @param event     Event to record\nvoid TestRecorder_RecordEvent(const SDL_Event *event);\n\n// Should be called after processing events each frame to update skip counters.\nvoid TestRecorder_EndFrame(void);\n"
  },
  {
    "path": "src/trx/game/replay/test_replay.c",
    "content": "#include <trx/game/replay/test_replay.h>\n\n#include <trx/config.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/console/common.h>\n#include <trx/game/input/backends/controller.h>\n#include <trx/game/input/backends/keyboard.h>\n#include <trx/game/input/common.h>\n#include <trx/game/lara.h>\n#include <trx/game/lua.h>\n#include <trx/game/random.h>\n#include <trx/game/shell.h>\n#include <trx/game/shell/events.h>\n\n#include <ctype.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <string.h>\n\n#define M_DEBUG 0\n\ntypedef struct {\n    SHELL_ARGS *args;\n} M_PARSE_CTX;\n\ntypedef struct {\n    bool collecting;\n    int32_t brace_depth;\n    bool in_quote;\n    bool escaped;\n} M_BLOCK_EVENT_CTX;\n\n// Parsed frame events\ntypedef struct {\n    int32_t frame_idx;\n    VECTOR *events; // vector of char*\n} M_FRAME;\n\n// Replay private state\ntypedef struct {\n    char *data; // Replay file data buffer\n    size_t size; // Size of data buffer\n    VECTOR *headers; // Vector of char* header lines\n    VECTOR *frames; // Vector of M_FRAME frames to play\n    int32_t frame_idx; // Current playback frame index\n    int32_t next_frame_idx; // Next frame to process\n    bool replay_quiet;\n    struct {\n        bool seen;\n        bool quiet_applied;\n        LOG_LEVEL log_level_before_quiet;\n        bool summary_printed;\n        bool case_active;\n        char *case_name;\n        int32_t case_checks;\n        int32_t case_fails;\n        int32_t cases_passed;\n        int32_t cases_failed;\n        int32_t checks_passed;\n        int32_t checks_failed;\n        int32_t exit_code_override;\n        bool use_ansi_colors;\n    } test_mode;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {};\n\ntypedef bool (*M_EVENT_HANDLER)(const char *token);\ntypedef bool (*M_HEADER_HANDLER)(const char *line, M_PARSE_CTX *ctx);\n\n// Event parsers\nstatic bool M_ParseQuitEvent(const char *event_str);\nstatic bool M_ParseKeyDownEvent(const char *event_str);\nstatic bool M_ParseKeyUpEvent(const char *event_str);\nstatic bool M_ParseTextInputEvent(const char *event_str);\nstatic bool M_ParseCommandEvent(const char *event_str);\nstatic bool M_ParseNoopEvent(const char *event_str);\nstatic bool M_ParseLuaEvent(const char *event_str);\nstatic bool M_ParseTestCaseEvent(const char *event_str);\nstatic bool M_ParseExpectEvent(const char *event_str);\n\n// Header parsers\nstatic bool M_ParseSeedControl(const char *line, M_PARSE_CTX *ctx);\nstatic bool M_ParseSeedDraw(const char *line, M_PARSE_CTX *ctx);\nstatic bool M_ParseBindKeyboard(const char *line, M_PARSE_CTX *ctx);\nstatic bool M_ParseBindController(const char *line, M_PARSE_CTX *ctx);\nstatic bool M_ParseArgs(const char *line, M_PARSE_CTX *ctx);\nstatic bool M_ParseConfig(const char *line, M_PARSE_CTX *ctx);\nstatic bool M_ParseTestCaseHeader(const char *line, M_PARSE_CTX *ctx);\n\nstatic const M_HEADER_HANDLER m_HeaderHandlers[] = {\n    M_ParseSeedControl,    M_ParseSeedDraw, M_ParseBindKeyboard,\n    M_ParseBindController, M_ParseArgs,     M_ParseConfig,\n    M_ParseTestCaseHeader, nullptr,\n};\n\nstatic const M_EVENT_HANDLER m_EventHandlers[] = {\n    M_ParseQuitEvent,   M_ParseTestCaseEvent,\n    M_ParseExpectEvent, M_ParseKeyDownEvent,\n    M_ParseKeyUpEvent,  M_ParseTextInputEvent,\n    M_ParseNoopEvent,   M_ParseCommandEvent,\n    M_ParseLuaEvent,    nullptr,\n};\n\nstatic void M_TestPrint(const char *const fmt, ...)\n{\n    va_list va;\n    va_start(va, fmt);\n    vprintf(fmt, va);\n    printf(\"\\n\");\n    fflush(stdout);\n    va_end(va);\n}\n\nstatic const char *M_TestColor(const char *const color)\n{\n    return m_Priv.test_mode.use_ansi_colors ? color : \"\";\n}\n\nstatic const char *M_TestColorReset(void)\n{\n    return m_Priv.test_mode.use_ansi_colors ? LOG_ANSI_COLOR_RESET : \"\";\n}\n\nstatic void M_EndTestCase(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!p->test_mode.case_active) {\n        return;\n    }\n\n    if (p->test_mode.case_fails == 0) {\n        p->test_mode.cases_passed++;\n        M_TestPrint(\n            \"%sPASS%s | %s\", M_TestColor(LOG_ANSI_COLOR_GREEN),\n            M_TestColorReset(), p->test_mode.case_name);\n    } else {\n        p->test_mode.cases_failed++;\n        M_TestPrint(\n            \"%sFAIL%s | %s (%d/%d checks failed)\",\n            M_TestColor(LOG_ANSI_COLOR_RED), M_TestColorReset(),\n            p->test_mode.case_name, p->test_mode.case_fails,\n            p->test_mode.case_checks);\n    }\n\n    p->test_mode.case_active = false;\n    Memory_FreePointer(&p->test_mode.case_name);\n    p->test_mode.case_checks = 0;\n    p->test_mode.case_fails = 0;\n}\n\nstatic void M_TestReportSummary(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!p->test_mode.seen || p->test_mode.summary_printed) {\n        return;\n    }\n\n    if (p->test_mode.case_active) {\n        M_EndTestCase();\n    }\n\n    M_TestPrint(\"\\n=== TEST_SUMMARY ===\");\n    const int32_t total_cases =\n        p->test_mode.cases_passed + p->test_mode.cases_failed;\n    if (p->test_mode.cases_passed > 0) {\n        M_TestPrint(\n            \"%sPASSED: %d of %d%s\", M_TestColor(LOG_ANSI_COLOR_GREEN),\n            p->test_mode.cases_passed, total_cases, M_TestColorReset());\n    }\n    if (p->test_mode.cases_failed > 0) {\n        M_TestPrint(\n            \"%sFAILED: %d of %d%s\", M_TestColor(LOG_ANSI_COLOR_RED),\n            p->test_mode.cases_failed, total_cases, M_TestColorReset());\n    }\n    p->test_mode.summary_printed = true;\n}\n\nstatic void M_ApplyQuietInTestMode(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!p->replay_quiet || !p->test_mode.seen || p->test_mode.quiet_applied) {\n        return;\n    }\n    p->test_mode.log_level_before_quiet = Log_GetMinLevel();\n    Log_SetMinLevel((LOG_LEVEL)100);\n    p->test_mode.quiet_applied = true;\n}\n\nstatic void M_TerminateFromTestResult(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->test_mode.cases_failed > 0 || p->test_mode.checks_failed > 0) {\n        p->test_mode.exit_code_override = 1;\n    } else {\n        p->test_mode.exit_code_override = 0;\n    }\n\n    SDL_Event event = { .type = SDL_QUIT };\n    Shell_ProcessEvent(&event);\n}\n\nstatic bool M_ParseQuotedPayload(\n    const char *const event_str, const char *const prefix,\n    const char **const out_start, size_t *const out_len)\n{\n    if (strncmp(event_str, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n\n    const char *const start = strchr(event_str + strlen(prefix), '\"');\n    const char *const end = start ? strrchr(start + 1, '\"') : nullptr;\n    if (start == nullptr || end == nullptr || end <= start + 1) {\n        return false;\n    }\n\n    *out_start = start + 1;\n    *out_len = (size_t)(end - (start + 1));\n    return true;\n}\n\nstatic const char *M_SkipWhitespaceConst(const char *const s)\n{\n    const char *p = s;\n    while (*p == ' ' || *p == '\\t') {\n        p++;\n    }\n    return p;\n}\n\nstatic char *M_TrimWhitespaceInPlace(char *const s)\n{\n    char *start = s;\n    while (*start == ' ' || *start == '\\t' || *start == '\\n'\n           || *start == '\\r') {\n        start++;\n    }\n    char *end = start + strlen(start);\n    while (end > start\n           && (end[-1] == ' ' || end[-1] == '\\t' || end[-1] == '\\n'\n               || end[-1] == '\\r')) {\n        end--;\n    }\n    *end = '\\0';\n    if (start != s) {\n        memmove(s, start, strlen(start) + 1);\n    }\n    return s;\n}\n\nstatic void M_ScanBraceState(\n    const char *const s, int32_t *const io_brace_depth, bool *const io_in_quote,\n    bool *const io_escaped)\n{\n    for (const char *p = s; *p != '\\0'; p++) {\n        if (*io_in_quote) {\n            if (*io_escaped) {\n                *io_escaped = false;\n                continue;\n            }\n            if (*p == '\\\\') {\n                *io_escaped = true;\n                continue;\n            }\n            if (*p == '\"') {\n                *io_in_quote = false;\n            }\n            continue;\n        }\n\n        if (*p == '\"') {\n            *io_in_quote = true;\n            continue;\n        }\n        if (*p == '{') {\n            (*io_brace_depth)++;\n            continue;\n        }\n        if (*p == '}') {\n            (*io_brace_depth)--;\n            continue;\n        }\n    }\n}\n\nstatic const char *M_GetBlockPayloadStartIfAny(const char *const evt)\n{\n    if (strncmp(evt, \"expect \", strlen(\"expect \")) == 0) {\n        return M_SkipWhitespaceConst(evt + strlen(\"expect \"));\n    }\n    if (strncmp(evt, \"lua \", strlen(\"lua \")) == 0) {\n        return M_SkipWhitespaceConst(evt + strlen(\"lua \"));\n    }\n    if (strncmp(evt, \"cmd \", strlen(\"cmd \")) == 0) {\n        return M_SkipWhitespaceConst(evt + strlen(\"cmd \"));\n    }\n    return nullptr;\n}\n\nstatic bool M_TryStartBlockEvent(\n    M_BLOCK_EVENT_CTX *const ctx, const char *const evt)\n{\n    const char *const payload_start = M_GetBlockPayloadStartIfAny(evt);\n    if (payload_start == nullptr || *payload_start != '{') {\n        return false;\n    }\n\n    ctx->collecting = true;\n    ctx->brace_depth = 0;\n    ctx->in_quote = false;\n    ctx->escaped = false;\n    M_ScanBraceState(\n        payload_start, &ctx->brace_depth, &ctx->in_quote, &ctx->escaped);\n    if (ctx->brace_depth == 0) {\n        ctx->collecting = false;\n    }\n    return true;\n}\n\nstatic bool M_GetBracedPayload(\n    const char *const event_str, const char *const prefix,\n    const char **const out_start, size_t *const out_len)\n{\n    if (strncmp(event_str, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n\n    const char *p = M_SkipWhitespaceConst(event_str + strlen(prefix));\n    if (*p != '{') {\n        return false;\n    }\n\n    const char *payload_start = p + 1;\n    int32_t depth = 1;\n    bool in_quote = false;\n    bool escaped = false;\n    p++;\n    for (; *p != '\\0'; p++) {\n        if (in_quote) {\n            if (escaped) {\n                escaped = false;\n                continue;\n            }\n            if (*p == '\\\\') {\n                escaped = true;\n                continue;\n            }\n            if (*p == '\"') {\n                in_quote = false;\n            }\n            continue;\n        }\n\n        if (*p == '\"') {\n            in_quote = true;\n            continue;\n        }\n        if (*p == '{') {\n            depth++;\n            continue;\n        }\n        if (*p == '}') {\n            depth--;\n            if (depth == 0) {\n                const char *trail = M_SkipWhitespaceConst(p + 1);\n                if (*trail != '\\0') {\n                    return false;\n                }\n                *out_start = payload_start;\n                *out_len = (size_t)(p - payload_start);\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nstatic bool M_ParseQuitEvent(const char *const event_str)\n{\n    if (strcmp(event_str, \"quit\") != 0) {\n        return false;\n    }\n\n    if (m_Priv.test_mode.seen) {\n        M_TestReportSummary();\n        M_TerminateFromTestResult();\n    }\n\n    SDL_Event event = { .type = SDL_QUIT };\n    Shell_ProcessEvent(&event);\n    return true;\n}\n\n// Consolidate keydown/keyup parsing into a single helper\nstatic bool M_ParseKeyEvent(\n    const char *event_str, SDL_EventType type, const char *prefix)\n{\n    if (strncmp(event_str, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n    SDL_Event event = { .type = type };\n    const char *p = event_str + strlen(prefix);\n    const char *start = strchr(p, '\"');\n    const char *end = start ? strrchr(start + 1, '\"') : nullptr;\n    if (!start || !end || end <= start + 1) {\n        LOG_WARNING(\"Malformed %s instruction: %s\", prefix, event_str);\n        return false;\n    }\n    const size_t slen = end - (start + 1);\n    const char *desc = String_FormatStatic(\"%.*s\", slen, start + 1);\n    SDL_Keymod mod;\n    if (!Input_ParseKeyDesc(desc, &event.key.keysym.scancode, &mod)) {\n        return false;\n    }\n    event.key.keysym.mod = mod;\n    event.key.keysym.sym = SDL_GetKeyFromScancode(event.key.keysym.scancode);\n    Shell_ProcessEvent(&event);\n    return true;\n}\n\nstatic bool M_ParseKeyDownEvent(const char *event_str)\n{\n    return M_ParseKeyEvent(event_str, SDL_KEYDOWN, \"●\");\n}\n\nstatic bool M_ParseKeyUpEvent(const char *event_str)\n{\n    return M_ParseKeyEvent(event_str, SDL_KEYUP, \"○\");\n}\n\nstatic bool M_ParseTextInputEvent(const char *const event_str)\n{\n    SDL_Event event = { .type = SDL_TEXTINPUT };\n    const char *const fmt = String_FormatStatic(\n        \"text-input \\\"%%%d[^\\\"]\\\"\", SDL_TEXTEDITINGEVENT_TEXT_SIZE - 1);\n    if (sscanf(event_str, fmt, &event.text.text) != 1) {\n        return false;\n    }\n    Shell_ProcessEvent(&event);\n    return true;\n}\n\nstatic bool M_ParseNoopEvent(const char *const event_str)\n{\n    // No-op event for inline comments and empty frame markers\n    if (strncmp(event_str, \"noop\", 4) != 0) {\n        return false;\n    }\n    return true;\n}\n\nstatic bool M_ParseTestCaseEvent(const char *const event_str)\n{\n    M_PRIV *const p = &m_Priv;\n    const char *name_start = nullptr;\n    size_t name_len = 0;\n    if (!M_ParseQuotedPayload(event_str, \"testcase \", &name_start, &name_len)) {\n        return false;\n    }\n\n    p->test_mode.seen = true;\n    M_ApplyQuietInTestMode();\n\n    if (p->test_mode.case_active) {\n        M_EndTestCase();\n    }\n\n    Memory_FreePointer(&p->test_mode.case_name);\n    p->test_mode.case_name = String_Format(\"%.*s\", (int)name_len, name_start);\n    p->test_mode.case_checks = 0;\n    p->test_mode.case_fails = 0;\n    p->test_mode.case_active = true;\n    return true;\n}\n\nstatic bool M_ParseTestCaseHeader(const char *const line, M_PARSE_CTX *const)\n{\n    return M_ParseTestCaseEvent(line);\n}\n\nstatic bool M_ParseExpectEvent(const char *const event_str)\n{\n    M_PRIV *const p = &m_Priv;\n    const char *const prefix = \"expect \";\n    if (strncmp(event_str, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n    const char *expr_start = nullptr;\n    size_t expr_len = 0;\n    if (!M_GetBracedPayload(event_str, prefix, &expr_start, &expr_len)) {\n        expr_start = M_SkipWhitespaceConst(event_str + strlen(prefix));\n        if (*expr_start == '\\0') {\n            return false;\n        }\n        expr_len = strlen(expr_start);\n    }\n\n    p->test_mode.seen = true;\n    M_ApplyQuietInTestMode();\n\n    if (!p->test_mode.case_active) {\n        M_TestPrint(\n            \"%sFAIL%s | expect outside test case\",\n            M_TestColor(LOG_ANSI_COLOR_RED), M_TestColorReset());\n        p->test_mode.checks_failed++;\n        p->test_mode.cases_failed++;\n        return true;\n    }\n\n    char *const expr = String_Format(\"%.*s\", (int)expr_len, expr_start);\n    char *const script = String_Format(\n        \"if not ((function() return %s\\n end)()) then error('expect failed') \"\n        \"end\",\n        expr);\n    LUA_RESULT eval_result = Lua_Eval(script);\n\n    p->test_mode.case_checks++;\n    if (eval_result.code == LUA_OK) {\n        p->test_mode.checks_passed++;\n    } else {\n        p->test_mode.checks_failed++;\n        p->test_mode.case_fails++;\n        M_TestPrint(\n            \"%sFAIL%s | %s | expect \\\"%s\\\" | %s\",\n            M_TestColor(LOG_ANSI_COLOR_RED), M_TestColorReset(),\n            p->test_mode.case_name, expr,\n            eval_result.message != nullptr ? eval_result.message : \"lua error\");\n    }\n\n    Lua_FreeResult(&eval_result);\n    Memory_Free(script);\n    Memory_Free(expr);\n    return true;\n}\n\nstatic bool M_ParseCommandEvent(const char *const event_str)\n{\n    const char *const prefix = \"cmd \";\n    if (strncmp(event_str, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n\n    const char *payload_start = nullptr;\n    size_t payload_len = 0;\n\n    if (M_GetBracedPayload(event_str, prefix, &payload_start, &payload_len)) {\n        char *const cmd_str =\n            String_Format(\"%.*s\", (int)payload_len, payload_start);\n        M_TrimWhitespaceInPlace(cmd_str);\n        Console_Eval(cmd_str);\n        Memory_Free(cmd_str);\n        return true;\n    }\n\n    if (M_ParseQuotedPayload(event_str, prefix, &payload_start, &payload_len)) {\n        char *const cmd_str =\n            String_Format(\"%.*s\", (int)payload_len, payload_start);\n        Console_Eval(cmd_str);\n        Memory_Free(cmd_str);\n        return true;\n    }\n\n    payload_start = M_SkipWhitespaceConst(event_str + strlen(prefix));\n    payload_len = strlen(payload_start);\n    if (payload_len == 0) {\n        LOG_WARNING(\"Malformed cmd instruction: %s\", event_str);\n        return false;\n    }\n\n    char *const cmd_str =\n        String_Format(\"%.*s\", (int)payload_len, payload_start);\n    M_TrimWhitespaceInPlace(cmd_str);\n    if (cmd_str[0] == '\\0') {\n        LOG_WARNING(\"Malformed cmd instruction: %s\", event_str);\n        Memory_Free(cmd_str);\n        return false;\n    }\n    Console_Eval(cmd_str);\n    Memory_Free(cmd_str);\n    return true;\n}\n\nstatic bool M_ParseLuaEvent(const char *const event_str)\n{\n    M_PRIV *const p = &m_Priv;\n    if (strncmp(event_str, \"lua \", 4) != 0) {\n        return false;\n    }\n\n    const char *chunk_start = nullptr;\n    size_t chunk_len = 0;\n    LUA_RESULT eval_result = {};\n    if (M_GetBracedPayload(event_str, \"lua \", &chunk_start, &chunk_len)) {\n        char *const chunk = String_Format(\"%.*s\", (int)chunk_len, chunk_start);\n        eval_result = Lua_Eval(chunk);\n        Memory_Free(chunk);\n    } else {\n        eval_result = Lua_Eval(event_str + 4);\n    }\n    if (eval_result.code == LUA_ERRSYNTAX) {\n        LOG_ERROR(\n            \"LUA syntax error on frame %d: %s\", p->frame_idx,\n            eval_result.message);\n        Shell_Terminate(1);\n    } else if (eval_result.code != LUA_OK) {\n        LOG_ERROR(\n            \"LUA error on frame %d: %s\", p->frame_idx, eval_result.message);\n        Shell_Terminate(1);\n    }\n    Lua_FreeResult(&eval_result);\n    return true;\n}\n\nstatic bool M_ParseEvent(const char *const event_str)\n{\n    for (int32_t i = 0; m_EventHandlers[i] != nullptr; i++) {\n        if (m_EventHandlers[i](event_str)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_ParseSeedControl(const char *const line, M_PARSE_CTX *const ctx)\n{\n    int32_t val;\n    if (sscanf(line, \"seed_control %d\", &val) == 1) {\n        Random_SeedControl(val);\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_ParseSeedDraw(const char *const line, M_PARSE_CTX *const ctx)\n{\n    int32_t val;\n    if (sscanf(line, \"seed_draw %d\", &val) == 1) {\n        Random_SeedDraw(val);\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_ParseBindKeyboard(const char *const line, M_PARSE_CTX *const ctx)\n{\n    const char *prefix = \"bind keyboard \";\n    if (strncmp(line, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n    const char *p = line + strlen(prefix);\n    const char *q = strchr(p, ' ');\n    if (q == nullptr) {\n        return false;\n    }\n    const char *role_str = String_FormatStatic(\"%.*s\", (int)(q - p), p);\n    const INPUT_ROLE role = ENUM_MAP_GET(INPUT_ROLE, role_str, -1);\n    if (role == (INPUT_ROLE)-1) {\n        return false;\n    }\n    const char *const start = strchr(p, '\"');\n    const char *const end = start ? strrchr(start + 1, '\"') : nullptr;\n    if (start == nullptr || end == nullptr || end < start + 1) {\n        LOG_WARNING(\"Malformed bind keyboard instruction: %s\", line);\n        return false;\n    }\n    const size_t slen = end - (start + 1);\n    const char *desc = String_FormatStatic(\"%.*s\", slen, start + 1);\n    SDL_Scancode sc = SDL_SCANCODE_UNKNOWN;\n    SDL_Keymod mod = KMOD_NONE;\n    if (desc[0] != '\\0') {\n        if (!Input_ParseKeyDesc(desc, &sc, &mod)) {\n            return false;\n        }\n    }\n    JSON_OBJECT *const bind = JSON_ObjectNew();\n    JSON_ObjectAppendInt(bind, \"scancode\", sc);\n    JSON_ObjectAppendInt(bind, \"mod\", mod);\n    g_Input_Keyboard.assign_from_json_object(\n        g_Config.input.keyboard_layout, role, 0, bind);\n    JSON_ObjectFree(bind);\n    return true;\n}\n\nstatic bool M_ParseBindController(\n    const char *const line, M_PARSE_CTX *const ctx)\n{\n    const char *prefix = \"bind controller \";\n    if (strncmp(line, prefix, strlen(prefix)) != 0) {\n        return false;\n    }\n    const char *p = line + strlen(prefix);\n    const char *q = strchr(p, ' ');\n    if (q == nullptr) {\n        return false;\n    }\n    const char *role_str = String_FormatStatic(\"%.*s\", (int)(q - p), p);\n    const INPUT_ROLE role =\n        (INPUT_ROLE)ENUM_MAP_GET(INPUT_ROLE, role_str, (int32_t)(INPUT_ROLE)-1);\n    if (role == (INPUT_ROLE)-1) {\n        return false;\n    }\n    int32_t bt, b, ad;\n    if (sscanf(q + 1, \"%d %d %d\", &bt, &b, &ad) == 3) {\n        JSON_OBJECT *bind = JSON_ObjectNew();\n        JSON_ObjectAppendInt(bind, \"button_type\", bt);\n        JSON_ObjectAppendInt(bind, \"bind\", b);\n        JSON_ObjectAppendInt(bind, \"axis_dir\", ad);\n        g_Input_Controller.assign_from_json_object(\n            g_Config.input.controller_layout, role, 0, bind);\n        JSON_ObjectFree(bind);\n        return true;\n    }\n    return false;\n}\n\nstatic bool M_ParseArgs(const char *const line, M_PARSE_CTX *const ctx)\n{\n    if (strncmp(line, \"args\", 4) != 0) {\n        return false;\n    }\n\n    if (ctx->args != nullptr) {\n        Shell_FreeArgs(ctx->args);\n        ctx->args = nullptr;\n    }\n    // Build an owned argv vector for Shell_ParseArgs adoption.\n    VECTOR *raw_args = Vector_Create(sizeof(const char *));\n    const char *p = line + 4;\n    while (*p != '\\0') {\n        while (isspace((unsigned char)*p)) {\n            p++;\n        }\n        if (*p != '\"') {\n            break;\n        }\n\n        p++;\n        const char *start = p;\n        while (*p != '\\0' && *p != '\"') {\n            p++;\n        }\n        const ptrdiff_t len = p - start;\n        char tmp[len + 1];\n        memcpy(tmp, start, len);\n        tmp[len] = '\\0';\n        char *arg = Memory_DupStr(tmp);\n        Vector_Add(raw_args, &arg);\n        if (*p == '\"') {\n            p++;\n        }\n    }\n    ctx->args = Shell_ParseArgs(raw_args);\n    return true;\n}\n\nstatic bool M_ParseConfig(const char *const line, M_PARSE_CTX *const ctx)\n{\n    char keybuf[64];\n    char valbuf[128];\n    if (sscanf(line, \"config %63s %127s\", keybuf, valbuf) == 2) {\n        // Strip surrounding quotes from the value, if present\n        size_t vlen = strlen(valbuf);\n        if (vlen >= 2 && valbuf[0] == '\"' && valbuf[vlen - 1] == '\"') {\n            valbuf[vlen - 1] = '\\0';\n            memmove(valbuf, valbuf + 1, vlen - 1);\n        }\n        const CONFIG_OPTION *opt = Config_GetOptionByPath(keybuf);\n        if (opt) {\n            Config_SetOptionValueFromString(opt, valbuf);\n        } else {\n            LOG_WARNING(\"Unknown option: %s\", keybuf);\n        }\n        return true;\n    }\n    return false;\n}\n\nstatic void M_StripInlineComment(char *const line)\n{\n    bool in_quote = false;\n    char *p;\n    for (p = line; *p != '\\0'; p++) {\n        if (*p == '\"') {\n            in_quote = !in_quote;\n        } else if (*p == '#' && !in_quote) {\n            *p = '\\0';\n            break;\n        }\n    }\n    // Trim trailing whitespace\n    {\n        char *end = line + strlen(line);\n        while (end > line && (end[-1] == ' ' || end[-1] == '\\t')) {\n            end[-1] = '\\0';\n            end--;\n        }\n    }\n}\n\nstatic char *M_SkipWhitespace(char *const line)\n{\n    char *start = line;\n    while (*start == ' ' || *start == '\\t') {\n        start++;\n    }\n    return start;\n}\n\nstatic bool M_IsFrameMarkerLine(const char *const line)\n{\n    int32_t delta = 0;\n    return sscanf(line, \"@+%d:\", &delta) == 1;\n}\n\nSHELL_ARGS *TestReplay_Open(const char *path)\n{\n    M_PRIV *const p = &m_Priv;\n\n    Memory_FreePointer(&p->test_mode.case_name);\n    memset(p, 0, sizeof(m_Priv));\n    p->replay_quiet = Log_GetMinLevel() >= LOG_LEVEL_WARNING;\n    p->test_mode.log_level_before_quiet = Log_GetMinLevel();\n    p->test_mode.use_ansi_colors = Log_ShouldUseAnsiColors();\n    p->test_mode.exit_code_override = -1;\n\n    char *data = nullptr;\n    size_t size = 0;\n    if (!File_Load(path, &data, &size)) {\n        Shell_ExitSystemFmt(\"Cannot open replay file '%s'\", path);\n        return nullptr;\n    }\n    p->data = data;\n    p->size = size;\n\n    // Split file into lines by replacing '\\n' with '\\0'\n    char *end = data + size;\n    for (char *ch = data; ch < end; ch++) {\n        if (*ch == '\\n') {\n            *ch = '\\0';\n        }\n    }\n\n    // Collect non-empty, comment-stripped lines\n    VECTOR *lines = Vector_Create(sizeof(char *));\n    for (char *line = data; line < end;) {\n        char *const next_line = line + strlen(line) + 1;\n        M_StripInlineComment(line);\n        char *start = M_SkipWhitespace(line);\n        if (*start != '\\0') {\n            Vector_Add(lines, &start);\n        }\n        line = next_line;\n    }\n\n    // Parse and execute headers\n    p->headers = Vector_Create(sizeof(char *));\n    int32_t idx = 0;\n    while (idx < lines->count) {\n        char *const ln = *(char **)Vector_Get(lines, idx);\n        int32_t delta = 0;\n        if (sscanf(ln, \"@+%d:\", &delta) == 1) {\n            break;\n        }\n        Vector_Add(p->headers, &ln);\n        idx++;\n    }\n\n    // Parse frames and their events\n    p->frames = Vector_Create(sizeof(M_FRAME));\n    p->next_frame_idx = 0;\n    p->frame_idx = 0;\n    int32_t last_frame = 0;\n    while (idx < lines->count) {\n        char *ln = *(char **)Vector_Get(lines, idx);\n        int32_t delta = 0;\n        if (sscanf(ln, \"@+%d:\", &delta) == 1) {\n            M_FRAME frame = {\n                .frame_idx = last_frame + delta,\n                .events = Vector_Create(sizeof(char *)),\n            };\n            M_BLOCK_EVENT_CTX block_ctx = {};\n\n            // Primary event on same line\n            char *const colon = strchr(ln, ':');\n            if (colon != nullptr) {\n                char *const evt = M_SkipWhitespace(colon + 1);\n                if (*evt != '\\0') {\n                    Vector_Add(frame.events, &evt);\n                    M_TryStartBlockEvent(&block_ctx, evt);\n                }\n            }\n\n            // Continued events\n            idx++;\n            while (idx < lines->count) {\n                char *const cont = *(char **)Vector_Get(lines, idx);\n                if (M_IsFrameMarkerLine(cont)) {\n                    // Reached next frame - stop\n                    break;\n                }\n\n                char *const evt = M_SkipWhitespace(cont);\n                if (*evt == '\\0') {\n                    idx++;\n                    continue;\n                }\n\n                if (block_ctx.collecting) {\n                    if (evt > p->data) {\n                        evt[-1] = '\\n';\n                    }\n                    M_ScanBraceState(\n                        evt, &block_ctx.brace_depth, &block_ctx.in_quote,\n                        &block_ctx.escaped);\n                    if (block_ctx.brace_depth == 0) {\n                        block_ctx.collecting = false;\n                    }\n                    idx++;\n                    continue;\n                }\n\n                Vector_Add(frame.events, &evt);\n                M_TryStartBlockEvent(&block_ctx, evt);\n                idx++;\n            }\n            Vector_Add(p->frames, &frame);\n            last_frame = frame.frame_idx;\n            continue;\n        }\n        idx++;\n    }\n\n    M_PARSE_CTX ctx = {};\n    for (int32_t i = 0; i < p->headers->count; i++) {\n        const char *const ln = *(const char **)Vector_Get(p->headers, i);\n        M_ParseArgs(ln, &ctx);\n    }\n    Vector_Free(lines);\n    LOG_INFO(\"Loaded %zu frames for playback\", p->frames->count);\n    if (ctx.args == nullptr) {\n        ctx.args = Shell_ParseArgs(nullptr);\n    }\n    if (ctx.args != nullptr && ctx.args->quiet) {\n        p->replay_quiet = true;\n    }\n    return ctx.args;\n}\n\nvoid TestReplay_Start(void)\n{\n    M_PARSE_CTX ctx = {};\n    M_PRIV *const p = &m_Priv;\n    for (int32_t i = 0; i < p->headers->count; i++) {\n        const char *const ln = *(const char **)Vector_Get(p->headers, i);\n        bool handled = false;\n        for (int32_t j = 0; m_HeaderHandlers[j]; j++) {\n            if (m_HeaderHandlers[j](ln, &ctx)) {\n                handled = true;\n                break;\n            }\n        }\n        if (!handled) {\n            LOG_WARNING(\"Unknown line: %s\", ln);\n        }\n    }\n    g_SavedConfig = g_Config;\n    Shell_FreeArgs(ctx.args);\n}\n\nvoid TestReplay_Close(void)\n{\n    M_PRIV *const p = &m_Priv;\n    M_TestReportSummary();\n\n    if (p->test_mode.quiet_applied) {\n        Log_SetMinLevel(p->test_mode.log_level_before_quiet);\n        p->test_mode.quiet_applied = false;\n    }\n\n    Memory_FreePointer(&p->test_mode.case_name);\n    if (p->headers) {\n        Vector_Free(p->headers);\n        p->headers = nullptr;\n    }\n    if (p->frames) {\n        for (int32_t i = 0; i < p->frames->count; i++) {\n            M_FRAME *const f = Vector_Get(p->frames, i);\n            Vector_Free(f->events);\n        }\n        Vector_Free(p->frames);\n        p->frames = nullptr;\n    }\n    if (p->data) {\n        Memory_Free(p->data);\n        p->data = nullptr;\n    }\n}\n\nbool TestReplay_IsOpened(void)\n{\n    M_PRIV *const p = &m_Priv;\n    return p->frames != nullptr;\n}\n\nvoid TestReplay_RunFrame(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (!TestReplay_IsOpened()) {\n        return;\n    }\n    while (p->next_frame_idx < p->frames->count) {\n        M_FRAME *const f = Vector_Get(p->frames, p->next_frame_idx);\n        if (f->frame_idx != p->frame_idx) {\n            break;\n        }\n        for (int32_t j = 0; j < f->events->count; j++) {\n            const char *const evt = *(char **)Vector_Get(f->events, j);\n            if (!M_ParseEvent(evt)) {\n                LOG_WARNING(\n                    \"Unknown replay event on frame %d: %s\", p->frame_idx, evt);\n            }\n        }\n        p->next_frame_idx++;\n    }\n    p->frame_idx++;\n\n    if (p->test_mode.seen && p->next_frame_idx >= p->frames->count) {\n        M_TestReportSummary();\n        M_TerminateFromTestResult();\n    }\n}\n\nint32_t TestReplay_GetExitCodeOverride(void)\n{\n    return m_Priv.test_mode.exit_code_override;\n}\n"
  },
  {
    "path": "src/trx/game/replay/test_replay.h",
    "content": "#pragma once\n\n#include <trx/game/shell/args.h>\n\n// Test replay: a module to simulate game playthroughs.\n// ============================================================================\n\n// Initialize test replay for playback mode.\n// @param path  Path to the recording to play from.\n// @return      Parsed shell arguments from the replay file, or nullptr on\n//              error.\nSHELL_ARGS *TestReplay_Open(const char *path);\n\n// Executes the initial configuration headers after the system is done\n// initializing.\nvoid TestReplay_Start(void);\n\n// Shutdown test replay.\nvoid TestReplay_Close(void);\n\n// Return whether the replay mode is currently active.\nbool TestReplay_IsOpened(void);\n\n// Run all events associated with the given frame.\nvoid TestReplay_RunFrame(void);\n\n// Returns -1 when replay does not override process exit code.\n// Otherwise 0/1 for replay-driven pass/fail status.\nint32_t TestReplay_GetExitCodeOverride(void);\n"
  },
  {
    "path": "src/trx/game/rooms/common.c",
    "content": "#include <trx/game/rooms/common.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/level.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/output.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound/common.h>\n#include <trx/version.h>\n\n#include <string.h>\n\nstatic int32_t m_RoomCount = 0;\nstatic ROOM *m_Rooms = nullptr;\nstatic bool m_FlipStatus = false;\nstatic int32_t m_FlipEffect = -1;\nstatic int32_t m_FlipTimer = 0;\nstatic int32_t m_FlipSlotFlags[MAX_FLIP_MAPS] = {};\n\n#define M_OUTSIDE_TABLE_STEP_SHIFT 2\n#define M_OUTSIDE_TABLE_STEP (1 << M_OUTSIDE_TABLE_STEP_SHIFT)\n#define M_OUTSIDE_TABLE_BLOCK_SHIFT (WALL_SHIFT + M_OUTSIDE_TABLE_STEP_SHIFT)\n#define M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL 64\n#define M_OUTSIDE_TABLE_SENTINEL NO_ROOM\n#define M_OUTSIDE_OFFSET_EMPTY 0xFFFF\n\nstatic int16_t *m_OutsideRoomTable = nullptr;\nstatic uint16_t *m_OutsideRoomOffsets = nullptr;\nstatic int32_t m_OutsideGridX = 0;\nstatic int32_t m_OutsideGridZ = 0;\nstatic int32_t m_OutsideOriginCellX = 0;\nstatic int32_t m_OutsideOriginCellZ = 0;\n\nstatic void M_AddFlipItems(const ROOM *const room)\n{\n    int16_t item_num = room->item_num;\n    while (item_num != NO_ITEM) {\n        ITEM *const item = Item_Get(item_num);\n        const OBJECT *const obj = Object_Get(item->object_id);\n\n        if (obj->handle_flip_func != nullptr) {\n            obj->handle_flip_func(item, RFS_FLIPPED);\n        }\n\n        item_num = item->next_item;\n    }\n}\n\nstatic void M_RemoveFlipItems(const ROOM *const room)\n{\n    int16_t item_num = room->item_num;\n    while (item_num != NO_ITEM) {\n        ITEM *const item = Item_Get(item_num);\n        const OBJECT *const obj = Object_Get(item->object_id);\n\n        if (obj->handle_flip_func != nullptr) {\n            obj->handle_flip_func(item, RFS_UNFLIPPED);\n        }\n\n        // TR2 does not have land/water objects like crocodile/alligator in TR1,\n        // so avoid instances of floating water creatures in drained rooms.\n        if (g_TRVersion >= 2 && (item->flags & IF_ONE_SHOT) && obj->intelligent\n            && item->hit_points <= 0) {\n            Item_RemoveDrawn(item_num);\n            item->flags |= IF_KILLED;\n        }\n\n        item_num = item->next_item;\n    }\n}\n\nstatic void M_GetNewRoom(\n    const int32_t x, const int32_t y, const int32_t z, int16_t room_num)\n{\n    Room_GetSector((XYZ_32) { x, y, z }, &room_num);\n    Room_MarkToBeDrawn(room_num);\n}\n\nvoid Room_InitialiseRooms(const int32_t num_rooms)\n{\n    m_RoomCount = num_rooms;\n    m_Rooms = num_rooms == 0\n        ? nullptr\n        : GameBuf_Alloc(sizeof(ROOM) * num_rooms, GBUF_ROOMS);\n\n    m_OutsideRoomTable = nullptr;\n    m_OutsideRoomOffsets = nullptr;\n    m_OutsideGridX = 0;\n    m_OutsideGridZ = 0;\n    m_OutsideOriginCellX = 0;\n    m_OutsideOriginCellZ = 0;\n}\n\nvoid Room_Shutdown(void)\n{\n    m_RoomCount = 0;\n    m_Rooms = nullptr;\n    m_FlipStatus = false;\n    m_FlipEffect = -1;\n    m_FlipTimer = 0;\n    memset(m_FlipSlotFlags, 0, sizeof(m_FlipSlotFlags));\n\n    m_OutsideRoomTable = nullptr;\n    m_OutsideRoomOffsets = nullptr;\n    m_OutsideGridX = 0;\n    m_OutsideGridZ = 0;\n    m_OutsideOriginCellX = 0;\n    m_OutsideOriginCellZ = 0;\n}\n\nint32_t Room_GetCount(void)\n{\n    return m_RoomCount;\n}\n\nROOM *Room_Get(const int32_t room_num)\n{\n    if (m_Rooms == nullptr) {\n        return nullptr;\n    }\n    if (room_num < 0 || room_num >= Room_GetCount()) {\n        return nullptr;\n    }\n    return &m_Rooms[room_num];\n}\n\nvoid Room_BuildOutsideTable(void)\n{\n    m_OutsideRoomTable = nullptr;\n    m_OutsideRoomOffsets = nullptr;\n    m_OutsideGridX = 0;\n    m_OutsideGridZ = 0;\n    m_OutsideOriginCellX = 0;\n    m_OutsideOriginCellZ = 0;\n\n    const int32_t num_rooms = Room_GetCount();\n    if (num_rooms <= 0) {\n        return;\n    }\n\n    {\n        int32_t min_x = INT32_MAX;\n        int32_t min_z = INT32_MAX;\n        int32_t max_x = INT32_MIN;\n        int32_t max_z = INT32_MIN;\n        for (int32_t i = 0; i < num_rooms; i++) {\n            const ROOM *const room = Room_Get(i);\n            if (room == nullptr) {\n                continue;\n            }\n            min_x = MIN(min_x, room->pos.x);\n            min_z = MIN(min_z, room->pos.z);\n            max_x = MAX(max_x, room->pos.x + (room->size.x << WALL_SHIFT));\n            max_z = MAX(max_z, room->pos.z + (room->size.z << WALL_SHIFT));\n        }\n\n        m_OutsideOriginCellX = (min_x >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - 1;\n        m_OutsideOriginCellZ = (min_z >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - 1;\n        const int32_t max_cell_x = (max_x >> M_OUTSIDE_TABLE_BLOCK_SHIFT) + 1;\n        const int32_t max_cell_z = (max_z >> M_OUTSIDE_TABLE_BLOCK_SHIFT) + 1;\n\n        m_OutsideGridX = max_cell_x - m_OutsideOriginCellX + 1;\n        m_OutsideGridZ = max_cell_z - m_OutsideOriginCellZ + 1;\n        if (m_OutsideGridX < 1) {\n            m_OutsideGridX = 1;\n        }\n        if (m_OutsideGridZ < 1) {\n            m_OutsideGridZ = 1;\n        }\n    }\n\n    const int32_t full_table_size =\n        m_OutsideGridX * m_OutsideGridZ * M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL;\n    int16_t *full_table = Memory_Alloc(full_table_size * sizeof(int16_t));\n    for (int32_t i = 0; i < full_table_size; i++) {\n        full_table[i] = M_OUTSIDE_TABLE_SENTINEL;\n    }\n\n    const int32_t blocks_x = m_OutsideGridX * M_OUTSIDE_TABLE_STEP;\n    const int32_t blocks_z = m_OutsideGridZ * M_OUTSIDE_TABLE_STEP;\n    for (int32_t y = 0; y < blocks_x; y += M_OUTSIDE_TABLE_STEP) {\n        for (int32_t x = 0; x < blocks_z; x += M_OUTSIDE_TABLE_STEP) {\n            for (int32_t i = 0; i < num_rooms; i++) {\n                const ROOM *const room = Room_Get(i);\n\n                const int32_t room_x = (room->pos.z >> WALL_SHIFT)\n                    - (m_OutsideOriginCellZ * M_OUTSIDE_TABLE_STEP);\n                const int32_t room_y = (room->pos.x >> WALL_SHIFT)\n                    - (m_OutsideOriginCellX * M_OUTSIDE_TABLE_STEP);\n\n                bool cont = false;\n                for (int32_t ry = 0; ry < M_OUTSIDE_TABLE_STEP && !cont; ry++) {\n                    for (int32_t rx = 0; rx < M_OUTSIDE_TABLE_STEP; rx++) {\n                        if (x + rx >= room_x && x + rx < room_x + room->size.z\n                            && y + ry >= room_y\n                            && y + ry < room_y + room->size.x) {\n                            cont = true;\n                            break;\n                        }\n                    }\n                }\n\n                if (!cont) {\n                    continue;\n                }\n\n                int16_t *const cell =\n                    &full_table\n                        [M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL\n                         * ((x >> M_OUTSIDE_TABLE_STEP_SHIFT)\n                            + m_OutsideGridZ\n                                * (y >> M_OUTSIDE_TABLE_STEP_SHIFT))];\n                for (int32_t lp = 0; lp < M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL;\n                     lp++) {\n                    if (cell[lp] == M_OUTSIDE_TABLE_SENTINEL) {\n                        cell[lp] = i;\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    const int32_t offset_count = m_OutsideGridX * m_OutsideGridZ;\n    m_OutsideRoomOffsets = GameBuf_Alloc(\n        sizeof(uint16_t) * (size_t)offset_count, GBUF_OUTSIDE_ROOM_TABLE);\n    for (int32_t i = 0; i < offset_count; i++) {\n        m_OutsideRoomOffsets[i] = M_OUTSIDE_OFFSET_EMPTY;\n    }\n\n    m_OutsideRoomTable = GameBuf_Alloc(\n        full_table_size * sizeof(int16_t), GBUF_OUTSIDE_ROOM_TABLE);\n    int16_t *const out_base = m_OutsideRoomTable;\n    int16_t *out_ptr = out_base;\n\n    for (int32_t y = 0; y < m_OutsideGridX; y++) {\n        for (int32_t x = 0; x < m_OutsideGridZ; x++) {\n            const int32_t cell_idx = x + y * m_OutsideGridZ;\n            const int16_t *const cell =\n                &full_table[M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL * cell_idx];\n\n            int32_t count = 0;\n            while (count < M_OUTSIDE_TABLE_MAX_ROOMS_PER_CELL\n                   && cell[count] != M_OUTSIDE_TABLE_SENTINEL) {\n                count++;\n            }\n\n            if (count == 0) {\n                continue;\n            }\n\n            if (count == 1) {\n                m_OutsideRoomOffsets[cell_idx] = 0x8000U | (uint16_t)cell[0];\n                continue;\n            }\n\n            int16_t *scan = out_base;\n            while (scan < out_ptr) {\n                if (memcmp(scan, cell, count * sizeof(int16_t)) == 0) {\n                    m_OutsideRoomOffsets[cell_idx] =\n                        (uint16_t)(scan - out_base);\n                    break;\n                }\n\n                int32_t scan_len = 0;\n                while (scan[scan_len] != M_OUTSIDE_TABLE_SENTINEL) {\n                    scan_len++;\n                }\n                scan += scan_len + 1;\n            }\n\n            if (scan < out_ptr) {\n                continue;\n            }\n\n            const int32_t new_off = (int32_t)(out_ptr - out_base);\n            ASSERT(new_off >= 0);\n            ASSERT(new_off < 0x8000);\n            m_OutsideRoomOffsets[cell_idx] = new_off;\n\n            ASSERT(new_off + count + 1 <= full_table_size);\n            memcpy(out_ptr, cell, count * sizeof(int16_t));\n            out_ptr += count;\n            *out_ptr++ = M_OUTSIDE_TABLE_SENTINEL;\n        }\n    }\n\n    Memory_FreePointer(&full_table);\n}\n\nint32_t Room_GetOutsideStatus(const XYZ_32 pos, int16_t *const out_room_num)\n{\n    if (out_room_num != nullptr) {\n        *out_room_num = NO_ROOM;\n    }\n\n    if (m_OutsideRoomTable == nullptr || m_OutsideRoomOffsets == nullptr) {\n        return -2;\n    }\n\n    const int32_t cell_x =\n        (pos.x >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - m_OutsideOriginCellX;\n    const int32_t cell_z =\n        (pos.z >> M_OUTSIDE_TABLE_BLOCK_SHIFT) - m_OutsideOriginCellZ;\n    if (cell_x < 0 || cell_x >= m_OutsideGridX || cell_z < 0\n        || cell_z >= m_OutsideGridZ) {\n        return -2;\n    }\n\n    const uint16_t entry =\n        m_OutsideRoomOffsets[m_OutsideGridZ * cell_x + cell_z];\n    if (entry == M_OUTSIDE_OFFSET_EMPTY) {\n        return -2;\n    }\n\n    const int16_t *p = nullptr;\n    int16_t single_room = M_OUTSIDE_TABLE_SENTINEL;\n    if ((entry & 0x8000U) != 0U) {\n        single_room = (int16_t)(entry & ~0x8000U);\n    } else {\n        p = &m_OutsideRoomTable[entry];\n    }\n\n    while (true) {\n        int16_t candidate_room_num;\n        if (p != nullptr) {\n            if (*p == M_OUTSIDE_TABLE_SENTINEL) {\n                break;\n            }\n            candidate_room_num = (int16_t)(*p);\n            p++;\n        } else {\n            if (single_room == M_OUTSIDE_TABLE_SENTINEL) {\n                break;\n            }\n            candidate_room_num = single_room;\n            single_room = M_OUTSIDE_TABLE_SENTINEL;\n        }\n\n        const ROOM *const room = Room_Get(candidate_room_num);\n        if (room == nullptr) {\n            continue;\n        }\n\n        if (pos.y <= room->max_ceiling || pos.y >= room->min_floor) {\n            continue;\n        }\n\n        if (pos.z <= room->pos.z + WALL_L\n            || pos.z >= room->pos.z + (room->size.z << WALL_SHIFT) - WALL_L) {\n            continue;\n        }\n\n        if (pos.x <= room->pos.x + WALL_L\n            || pos.x >= room->pos.x + (room->size.x << WALL_SHIFT) - WALL_L) {\n            continue;\n        }\n\n        int16_t rn = candidate_room_num;\n        const SECTOR *const sector = Room_GetSector(pos, &rn);\n        const int32_t floor = Room_GetHeight(sector, pos);\n        if (floor == NO_HEIGHT || pos.y > floor) {\n            return -2;\n        }\n\n        const int32_t ceiling = Room_GetCeiling(sector, pos);\n        if (pos.y < ceiling) {\n            return -2;\n        }\n\n        if (!room->flags.underwater && !room->flags.wind) {\n            return -3;\n        }\n\n        if (out_room_num != nullptr) {\n            *out_room_num = candidate_room_num;\n        }\n        return 1;\n    }\n\n    return -2;\n}\n\nint32_t Room_GetNumber(const ROOM *const room)\n{\n    if (room == nullptr) {\n        return NO_ROOM;\n    }\n    return room - m_Rooms;\n}\n\nvoid Room_InitialiseFlipStatus(void)\n{\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        ROOM *const room = Room_Get(i);\n        // Some level data links only one side of a flip pair. In that case, a\n        // previous iteration may already have marked this room as flipped.\n        if (room->flip_status == RFS_FLIPPED) {\n            continue;\n        }\n        if (room->flipped_room == NO_ROOM) {\n            room->flip_status = RFS_NONE;\n        } else {\n            ROOM *const flipped_room = Room_Get(room->flipped_room);\n            room->flip_status = RFS_UNFLIPPED;\n            flipped_room->flip_status = RFS_FLIPPED;\n        }\n    }\n\n    m_FlipStatus = false;\n    m_FlipEffect = -1;\n    m_FlipTimer = 0;\n    for (int32_t i = 0; i < MAX_FLIP_MAPS; i++) {\n        m_FlipSlotFlags[i] = 0;\n    }\n}\n\nvoid Room_FlipMap(void)\n{\n    Walkable_Reset();\n\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        ROOM *const room = Room_Get(i);\n        if (room->flipped_room < 0) {\n            continue;\n        }\n\n        M_RemoveFlipItems(room);\n\n        ROOM *const flipped = Room_Get(room->flipped_room);\n        const ROOM temp = *room;\n        *room = *flipped;\n        *flipped = temp;\n\n        room->flipped_room = flipped->flipped_room;\n        room->flip_status = RFS_UNFLIPPED;\n        flipped->flipped_room = NO_ROOM;\n        flipped->flip_status = RFS_FLIPPED;\n\n        room->item_num = flipped->item_num;\n        room->effect_num = flipped->effect_num;\n        memcpy(\n            &room->drawn_items, &flipped->drawn_items,\n            sizeof(room->drawn_items));\n\n        M_AddFlipItems(room);\n    }\n\n    m_FlipStatus = !m_FlipStatus;\n\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        if (room->flip_status != RFS_NONE) {\n            Output_DispatchRoomFlip(room);\n        }\n    }\n\n    Level_Finalize_LoadWalkables(Level_Context_Get());\n}\n\nbool Room_GetFlipStatus(void)\n{\n    return m_FlipStatus;\n}\n\nint32_t Room_GetFlipEffect(void)\n{\n    return m_FlipEffect;\n}\n\nvoid Room_SetFlipEffect(const int32_t flip_effect)\n{\n    m_FlipEffect = flip_effect;\n}\n\nint32_t Room_GetFlipTimer(void)\n{\n    return m_FlipTimer;\n}\n\nvoid Room_SetFlipTimer(const int32_t flip_timer)\n{\n    m_FlipTimer = flip_timer;\n}\n\nvoid Room_IncrementFlipTimer(const int32_t num_frames)\n{\n    m_FlipTimer += num_frames;\n}\n\nint32_t Room_GetFlipSlotFlags(const int32_t slot_idx)\n{\n    return m_FlipSlotFlags[slot_idx];\n}\n\nvoid Room_SetFlipSlotFlags(const int32_t slot_idx, const int32_t flags)\n{\n    m_FlipSlotFlags[slot_idx] = flags;\n}\n\nint32_t Room_GetAdjoiningRooms(\n    int16_t init_room_num, int16_t out_room_nums[],\n    const int32_t max_room_num_count)\n{\n    int32_t count = 0;\n    if (max_room_num_count >= 1) {\n        out_room_nums[count++] = init_room_num;\n    }\n\n    const PORTALS *const portals = Room_Get(init_room_num)->portals;\n    if (portals != nullptr) {\n        for (int32_t i = 0; i < portals->count; i++) {\n            if (count >= max_room_num_count) {\n                break;\n            }\n            const int16_t room_num = portals->portal[i].room_num;\n            out_room_nums[count++] = room_num;\n        }\n    }\n\n    return count;\n}\n\nbool Room_PointInside(const ROOM *const room, const XYZ_32 point)\n{\n    if (room == nullptr) {\n        return false;\n    }\n    const BOUNDS_32 bounds = Room_GetRoomBounds(room);\n    const int32_t x1 = bounds.min.x;\n    const int32_t y1 = bounds.min.y;\n    const int32_t z1 = bounds.min.z;\n    const int32_t x2 = bounds.max.x;\n    const int32_t y2 = bounds.max.y;\n    const int32_t z2 = bounds.max.z;\n    if (point.x >= x1 && point.x < x2 && point.y >= y1 && point.y <= y2\n        && point.z >= z1 && point.z < z2) {\n        const SECTOR *sector = Room_GetWorldSector(room, point.x, point.z);\n        const int32_t height = Room_GetHeight(sector, point);\n        if (height != NO_HEIGHT) {\n            return true;\n        }\n    }\n    return false;\n}\n\nint16_t Room_GetIndexFromPos(const XYZ_32 point)\n{\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        if (room->flip_status == RFS_FLIPPED) {\n            continue;\n        }\n        if (Room_PointInside(room, point)) {\n            return i;\n        }\n    }\n\n    return NO_ROOM;\n}\n\nint32_t Room_GetFlippedBaseRoom(const int32_t room_num)\n{\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        if (room->flipped_room == room_num) {\n            return i;\n        }\n    }\n    return NO_ROOM;\n}\n\nBOUNDS_32 Room_GetWorldBounds(void)\n{\n    BOUNDS_32 world_bounds = {\n        .min.x = INT32_MAX,\n        .min.z = INT32_MAX,\n        .max.x = 0,\n        .max.z = 0,\n        .min.y = MAX_HEIGHT,\n        .max.y = -MAX_HEIGHT,\n    };\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const BOUNDS_32 room_bounds = Room_GetRoomBounds(Room_Get(i));\n        world_bounds.min.x = MIN(world_bounds.min.x, room_bounds.min.x);\n        world_bounds.max.x = MAX(world_bounds.max.x, room_bounds.max.x);\n        world_bounds.min.z = MIN(world_bounds.min.z, room_bounds.min.z);\n        world_bounds.max.z = MAX(world_bounds.max.z, room_bounds.max.z);\n        world_bounds.min.y = MIN(world_bounds.min.y, room_bounds.min.y);\n        world_bounds.max.y = MAX(world_bounds.max.y, room_bounds.max.y);\n    }\n    return world_bounds;\n}\n\nvoid Room_GetNearbyRooms(\n    const XYZ_32 pos, const int32_t r, const int32_t h, const int16_t room_num)\n{\n    Room_DrawReset();\n    Room_MarkToBeDrawn(room_num);\n\n    M_GetNewRoom(pos.x + r, pos.y, pos.z + r, room_num);\n    M_GetNewRoom(pos.x - r, pos.y, pos.z + r, room_num);\n    M_GetNewRoom(pos.x + r, pos.y, pos.z - r, room_num);\n    M_GetNewRoom(pos.x - r, pos.y, pos.z - r, room_num);\n    M_GetNewRoom(pos.x + r, pos.y - h, pos.z + r, room_num);\n    M_GetNewRoom(pos.x - r, pos.y - h, pos.z + r, room_num);\n    M_GetNewRoom(pos.x + r, pos.y - h, pos.z - r, room_num);\n    M_GetNewRoom(pos.x - r, pos.y - h, pos.z - r, room_num);\n}\n\nbool Room_CheckOverlap(const int16_t room_num_0, const int16_t room_num_1)\n{\n    const BOUNDS_32 room_0_bounds = Room_GetRoomBounds(Room_Get(room_num_0));\n    const BOUNDS_32 room_1_bounds = Room_GetRoomBounds(Room_Get(room_num_1));\n\n    // clang-format off\n    return (\n        room_0_bounds.min.x <= room_1_bounds.max.x &&\n        room_0_bounds.max.x >= room_1_bounds.min.x &&\n        room_0_bounds.min.y <= room_1_bounds.max.y &&\n        room_0_bounds.max.y >= room_1_bounds.min.y &&\n        room_0_bounds.min.z <= room_1_bounds.max.z &&\n        room_0_bounds.max.z >= room_1_bounds.min.z);\n    // clang-format on\n}\n\nbool Room_FindValidPos(XYZ_32 *const out_pos, int16_t *const out_room_num)\n{\n    ASSERT(out_pos != nullptr);\n    ASSERT(out_room_num != nullptr);\n    XYZ_32 initial_pos = *out_pos;\n    int16_t room_num = *out_room_num;\n    if (room_num == NO_ROOM) {\n        room_num = Room_GetIndexFromPos(*out_pos);\n    }\n    if (room_num == NO_ROOM) {\n        return false;\n    }\n\n    const ROOM *const room = Room_Get(room_num);\n    if (room->flip_status == RFS_FLIPPED && Room_GetFlipStatus()) {\n        room_num = Room_GetFlippedBaseRoom(room_num);\n        if (room_num == NO_ROOM) {\n            return false;\n        }\n    }\n\n    const SECTOR *sector = Room_GetSector(*out_pos, &room_num);\n    int32_t height = Room_GetHeight(sector, *out_pos);\n\n    int32_t x = out_pos->x;\n    int32_t y = out_pos->y;\n    int32_t z = out_pos->z;\n    if (height == NO_HEIGHT) {\n        // Sample a sphere of points around target x, y, z\n        // and teleport to the first available location.\n        VECTOR *const points = Vector_Create(sizeof(XYZ_32));\n\n        const int32_t radius = 10;\n        const int32_t unit = STEP_L;\n        for (int32_t dx = -radius; dx <= radius; dx++) {\n            for (int32_t dz = -radius; dz <= radius; dz++) {\n                if (SQUARE(dx) + SQUARE(dz) > SQUARE(radius)) {\n                    continue;\n                }\n\n                const XYZ_32 point = {\n                    .x = ROUND_TO_SECTOR(x + dx * unit) + WALL_L / 2,\n                    .y = y,\n                    .z = ROUND_TO_SECTOR(z + dz * unit) + WALL_L / 2,\n                };\n                sector = Room_GetSector(point, &room_num);\n                height = Room_GetHeightEx(sector, point, true, NO_ITEM);\n                if (height == NO_HEIGHT) {\n                    continue;\n                }\n                Vector_Add(points, (void *)&point);\n            }\n        }\n\n        int32_t best_distance = INT32_MAX;\n        for (int32_t i = 0; i < points->count; i++) {\n            const XYZ_32 *const point = (const XYZ_32 *)Vector_Get(points, i);\n            const int32_t distance = XYZ_32_GetDistance(*point, initial_pos);\n            if (distance < best_distance) {\n                best_distance = distance;\n                x = point->x;\n                y = point->y;\n                z = point->z;\n            }\n        }\n\n        Vector_Free(points);\n        if (best_distance == INT32_MAX) {\n            return false;\n        }\n    }\n\n    out_pos->x = x;\n    out_pos->y = y;\n    out_pos->z = z;\n    *out_room_num = room_num;\n\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/rooms/common.h",
    "content": "#pragma once\n\n#include <trx/core/math/types.h>\n#include <trx/game/rooms/types.h>\n\nvoid Room_InitialiseRooms(int32_t num_rooms);\nvoid Room_Shutdown(void);\nint32_t Room_GetCount(void);\nROOM *Room_Get(int32_t room_num);\nint32_t Room_GetNumber(const ROOM *room);\n\nvoid Room_InitialiseFlipStatus(void);\nvoid Room_FlipMap(void);\nbool Room_GetFlipStatus(void);\nint32_t Room_GetFlipEffect(void);\nvoid Room_SetFlipEffect(int32_t flip_effect);\nint32_t Room_GetFlipTimer(void);\nvoid Room_SetFlipTimer(int32_t flip_timer);\nvoid Room_IncrementFlipTimer(int32_t num_frames);\nint32_t Room_GetFlipSlotFlags(int32_t slot_idx);\nvoid Room_SetFlipSlotFlags(int32_t slot_idx, int32_t flags);\n\nint32_t Room_GetAdjoiningRooms(\n    int16_t init_room_num, int16_t out_room_nums[], int32_t max_room_num_count);\n\nint16_t Room_GetIndexFromPos(XYZ_32 pos);\nint32_t Room_GetFlippedBaseRoom(int32_t room_num);\nBOUNDS_32 Room_GetWorldBounds(void);\n\nvoid Room_BuildOutsideTable(void);\nint32_t Room_GetOutsideStatus(XYZ_32 pos, int16_t *out_room_num);\n\nbool Room_PointInside(const ROOM *room, XYZ_32 point);\nbool Room_CheckOverlap(int16_t room_num_0, int16_t room_num_1);\nvoid Room_GetNearbyRooms(XYZ_32 pos, int32_t r, int32_t h, int16_t room_num);\n\nbool Room_FindValidPos(XYZ_32 *out_pos, int16_t *out_room_num);\n"
  },
  {
    "path": "src/trx/game/rooms/const.h",
    "content": "#pragma once\n\n#define MAX_ROOMS 1024\n#define MAX_FLIP_MAPS 10\n\n#define NO_ROOM (-1)\n\n#define MAX_SLOPE 2\n#define NO_HEIGHT (-32512)\n#define MAX_HEIGHT 32000\n"
  },
  {
    "path": "src/trx/game/rooms/draw.c",
    "content": "#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx.h>\n#include <trx/game/lara.h>\n#include <trx/game/matrix.h>\n#include <trx/game/output.h>\n#include <trx/game/output/bind.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sparks.h>\n#include <trx/version.h>\n\n#include <string.h>\n\n#define M_MAX_BOUND_ROOMS 128\n\ntypedef struct {\n    int32_t xv;\n    int32_t yv;\n    int32_t zv;\n} M_PORTAL_VBUF;\n\nstatic inline void M_DrawSet_Init(ROOM_DRAWSET *const s)\n{\n    s->count = 0;\n    memset(s->bits, 0, sizeof(s->bits));\n}\n\nstatic inline bool M_DrawSet_Has(\n    const ROOM_DRAWSET *const s, const int16_t item_num)\n{\n    const uint32_t w = item_num >> 6;\n    const uint32_t b = item_num & 63;\n    return (s->bits[w] >> b) & 1ULL;\n}\n\nstatic inline bool M_DrawSet_Add(ROOM_DRAWSET *const s, const int16_t item_num)\n{\n    const uint32_t w = item_num >> 6;\n    const uint32_t b = item_num & 63;\n    const uint64_t mask = 1ULL << b;\n    if (s->bits[w] & mask) {\n        return false;\n    }\n    s->bits[w] |= mask;\n    s->count++;\n    return true;\n}\n\nstatic inline bool M_DrawSet_Remove(\n    ROOM_DRAWSET *const s, const int16_t item_num)\n{\n    const uint32_t w = item_num >> 6;\n    const uint32_t b = item_num & 63;\n    const uint64_t mask = 1ULL << b;\n    if (!(s->bits[w] & mask)) {\n        return false;\n    }\n    s->bits[w] &= ~mask;\n    s->count--;\n    return true;\n}\n\nstatic inline void M_DrawSet_ForEach(\n    const ROOM_DRAWSET *const s, void (*const fn)(int16_t item, void *ud),\n    void *ud)\n{\n    for (uint32_t w = 0; w < ROOM_DRAWSET_WORDS; w++) {\n        uint64_t x = s->bits[w];\n        while (x != 0ULL) {\n            const uint32_t b = __builtin_ctzll(x);\n            fn((int16_t)((w << 6) + b), ud);\n            x &= x - 1; // clear lowest set bit\n        }\n    }\n}\n\nstatic VECTOR *m_RoomsToDraw = nullptr;\nstatic ROOM_DRAWSET m_DrawnStatics = {};\n\nstatic int32_t m_Outside;\nstatic int32_t m_OutsideRight;\nstatic int32_t m_OutsideLeft;\nstatic int32_t m_OutsideTop;\nstatic int32_t m_OutsideBottom;\n\nstatic int32_t m_BoundStart;\nstatic int32_t m_BoundEnd;\nstatic int32_t m_BoundRooms[M_MAX_BOUND_ROOMS] = {};\n\nstatic void M_EnsureRoomsToDraw(void)\n{\n    if (m_RoomsToDraw != nullptr) {\n        return;\n    }\n    m_RoomsToDraw = Vector_CreateAtCapacity(sizeof(int16_t), 100);\n}\n\nstatic inline void M_SetupWaterStatus(const ROOM *const room)\n{\n    if (room->flags.underwater) {\n        Output_SetupBelowWater(g_Camera.underwater);\n    } else {\n        Output_SetupAboveWater(g_Camera.underwater);\n    }\n}\n\nstatic void M_SetBounds(\n    const PORTAL *const portal, int32_t room_num, const ROOM *parent);\n\nstatic inline bool M_PortalFacesCamera(\n    const ROOM *const room, const PORTAL *const portal)\n{\n    // clang-format off\n    const XYZ_32 offset = {\n        .x = portal->normal.x * (room->pos.x + portal->vertex[0].x - g_ViewPos.x),\n        .y = portal->normal.y * (room->pos.y + portal->vertex[0].y - g_ViewPos.y),\n        .z = portal->normal.z * (room->pos.z + portal->vertex[0].z - g_ViewPos.z),\n    };\n    // clang-format on\n    return offset.x + offset.y + offset.z < 0;\n}\n\nstatic void M_GetBounds(void)\n{\n    while (m_BoundStart != m_BoundEnd) {\n        const int16_t room_num = m_BoundRooms[m_BoundStart % M_MAX_BOUND_ROOMS];\n        m_BoundStart++;\n        const ROOM *const room = Room_Get(room_num);\n        OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room);\n        bind->active = false;\n\n        CLAMPG(bind->bound_left, bind->test_left);\n        CLAMPG(bind->bound_top, bind->test_top);\n        CLAMPL(bind->bound_right, bind->test_right);\n        CLAMPL(bind->bound_bottom, bind->test_bottom);\n\n        if (!bind->drawn) {\n            Room_MarkToBeDrawn(room_num);\n            bind->drawn = true;\n            if (room->flags.outside) {\n                m_Outside = 1;\n            }\n        }\n\n        if (!room->flags.inside || room->flags.outside) {\n            CLAMPG(m_OutsideLeft, bind->bound_left);\n            CLAMPG(m_OutsideTop, bind->bound_top);\n            CLAMPL(m_OutsideRight, bind->bound_right);\n            CLAMPL(m_OutsideBottom, bind->bound_bottom);\n        }\n\n        if (room->portals == nullptr) {\n            continue;\n        }\n\n        Matrix_Push();\n        Matrix_TranslateAbs32(room->pos);\n        for (int32_t i = 0; i < room->portals->count; i++) {\n            PORTAL *const portal = &room->portals->portal[i];\n            if (M_PortalFacesCamera(room, portal)) {\n                M_SetBounds(portal, portal->room_num, room);\n            }\n        }\n        Matrix_Pop();\n    }\n}\n\nstatic void M_SetBounds(\n    const PORTAL *const portal, const int32_t room_num,\n    const ROOM *const parent)\n{\n    const ROOM *const room = Room_Get(room_num);\n    const OUTPUT_ROOM_BIND *const parent_bind = Output_Bind_GetRoom(parent);\n    OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room);\n\n    if (bind->bound_left <= parent_bind->test_left\n        && bind->bound_top <= parent_bind->test_top\n        && bind->bound_right >= parent_bind->test_right\n        && bind->bound_bottom >= parent_bind->test_bottom) {\n        return;\n    }\n\n    const MATRIX *const m = g_MatrixPtr;\n    int32_t left = parent_bind->test_right;\n    int32_t right = parent_bind->test_left;\n    int32_t bottom = parent_bind->test_top;\n    int32_t top = parent_bind->test_bottom;\n\n    M_PORTAL_VBUF portal_vbuf[4];\n    int32_t too_near = 0;\n\n    for (int32_t i = 0; i < 4; i++) {\n        M_PORTAL_VBUF *const dvbuf = &portal_vbuf[i];\n        const XYZ_16 *const dvtx = &portal->vertex[i];\n        const int32_t xv =\n            dvtx->x * m->_00 + dvtx->y * m->_01 + dvtx->z * m->_02 + m->_03;\n        const int32_t yv =\n            dvtx->x * m->_10 + dvtx->y * m->_11 + dvtx->z * m->_12 + m->_13;\n        const int32_t zv =\n            dvtx->x * m->_20 + dvtx->y * m->_21 + dvtx->z * m->_22 + m->_23;\n        dvbuf->xv = xv;\n        dvbuf->yv = yv;\n        dvbuf->zv = zv;\n\n        if (zv <= 0) {\n            too_near++;\n            continue;\n        }\n\n        int32_t xs;\n        int32_t ys;\n        const int32_t zp = zv / g_PhdPersp;\n        if (zp != 0) {\n            xs = Viewport_GetCenterX(VIEWPORT_GAME) + xv / zp;\n            ys = Viewport_GetCenterY(VIEWPORT_GAME) + yv / zp;\n        } else {\n            xs = xv < 0 ? g_PhdLeft : g_PhdRight;\n            ys = yv < 0 ? g_PhdTop : g_PhdBottom;\n        }\n\n        if (xs - 1 < left) {\n            left = xs - 1;\n        }\n        if (xs + 1 > right) {\n            right = xs + 1;\n        }\n        if (ys - 1 < top) {\n            top = ys - 1;\n        }\n        if (ys + 1 > bottom) {\n            bottom = ys + 1;\n        }\n    }\n\n    if (too_near == 4) {\n        return;\n    }\n\n    if (too_near > 0) {\n        const M_PORTAL_VBUF *dest = &portal_vbuf[0];\n        const M_PORTAL_VBUF *last = &portal_vbuf[3];\n\n        for (int32_t i = 0; i < 4; i++, last = dest, dest++) {\n            if ((dest->zv <= 0) == (last->zv <= 0)) {\n                continue;\n            }\n\n            if (dest->xv < 0 && last->xv < 0) {\n                left = Viewport_GetMinX(VIEWPORT_GAME);\n            } else if (dest->xv > 0 && last->xv > 0) {\n                right = Viewport_GetMaxX(VIEWPORT_GAME);\n            } else {\n                left = Viewport_GetMinX(VIEWPORT_GAME);\n                right = Viewport_GetMaxX(VIEWPORT_GAME);\n            }\n\n            if (dest->yv < 0 && last->yv < 0) {\n                top = Viewport_GetMinY(VIEWPORT_GAME);\n            } else if (dest->yv > 0 && last->yv > 0) {\n                bottom = Viewport_GetMaxY(VIEWPORT_GAME);\n            } else {\n                top = Viewport_GetMinY(VIEWPORT_GAME);\n                bottom = Viewport_GetMaxY(VIEWPORT_GAME);\n            }\n        }\n    }\n\n    if (left < parent_bind->test_left) {\n        left = parent_bind->test_left;\n    }\n    if (right > parent_bind->test_right) {\n        right = parent_bind->test_right;\n    }\n    if (top < parent_bind->test_top) {\n        top = parent_bind->test_top;\n    }\n    if (bottom > parent_bind->test_bottom) {\n        bottom = parent_bind->test_bottom;\n    }\n\n    if (left >= right || top >= bottom) {\n        return;\n    }\n\n    if (bind->active) {\n        CLAMPG(bind->test_left, left);\n        CLAMPG(bind->test_top, top);\n        CLAMPL(bind->test_right, right);\n        CLAMPL(bind->test_bottom, bottom);\n    } else {\n        m_BoundRooms[m_BoundEnd % M_MAX_BOUND_ROOMS] = room_num;\n        m_BoundEnd++;\n        bind->active = true;\n        bind->test_left = left;\n        bind->test_top = top;\n        bind->test_right = right;\n        bind->test_bottom = bottom;\n    }\n}\n\nstatic void M_DrawSkybox(void)\n{\n    if (!Output_IsSkyboxEnabled()) {\n        return;\n    }\n\n    g_PhdLeft = m_OutsideLeft;\n    g_PhdTop = m_OutsideTop;\n    g_PhdRight = m_OutsideRight;\n    g_PhdBottom = m_OutsideBottom;\n\n    const OBJECT *const skybox = Object_Get(O_SKYBOX);\n    if (skybox->loaded) {\n        Output_SetupAboveWater(g_Camera.underwater);\n        Matrix_PushUnit();\n        Matrix_TranslateAbs32(g_ViewPos);\n        Matrix_Rot16(skybox->frame_base->mesh_rots[0]);\n        Output_CalculateStaticLight(Output_GetSkyShade());\n        Output_DrawSkybox(Object_GetMesh(skybox->mesh_idx));\n        Matrix_Pop();\n    } else {\n        m_Outside = -1;\n    }\n}\n\nstatic void M_DrawRoomItem(const int16_t item_num, void *const ud)\n{\n    ITEM *const item = Item_Get(item_num);\n    const OBJECT *const obj = Object_Get(item->object_id);\n    OUTPUT_ITEM_BIND *const bind = Output_Bind_GetItem(item);\n    if (bind->drawn || item->status == IS_INVISIBLE\n        || obj->draw_func == nullptr) {\n        return;\n    }\n\n    M_SetupWaterStatus(Room_Get(item->room_num));\n    bind->drawn |= obj->draw_func(item);\n\n    if (Output_IsControlFrame()) {\n        Item_ControlDraw(item);\n    }\n}\n\nstatic void M_DrawSingleRoom(const ROOM *const room)\n{\n    Output_SetCurrentRoom(room);\n    M_SetupWaterStatus(room);\n\n    OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room);\n    g_PhdLeft = bind->bound_left;\n    g_PhdTop = bind->bound_top;\n    g_PhdRight = bind->bound_right;\n    g_PhdBottom = bind->bound_bottom;\n\n    if (g_Config.debug.enable_debug_room_clip) {\n        Output_DrawScreenFrame(\n            g_PhdLeft, g_PhdTop, g_PhdRight - g_PhdLeft, g_PhdBottom - g_PhdTop,\n            (RGBA_8888) { 0, 255, 0, 128 }, (RGBA_8888) { 0, 255, 0, 128 }, 1);\n    }\n\n    Matrix_TranslateAbs32(room->pos);\n    Output_DrawRoom(room, false);\n\n    M_SetupWaterStatus(room);\n\n    Matrix_Push();\n    Matrix_TranslateAbs32(room->pos);\n\n    g_PhdLeft = bind->bound_left;\n    g_PhdTop = bind->bound_top;\n    g_PhdRight = bind->bound_right;\n    g_PhdBottom = bind->bound_bottom;\n\n    for (int32_t i = 0; i < room->num_static_meshes; i++) {\n        const STATIC_MESH *const mesh = &room->static_meshes[i];\n        if (M_DrawSet_Has(&m_DrawnStatics, mesh->draw_num)) {\n            continue;\n        }\n        const STATIC_OBJECT_3D *const obj =\n            Object_Get3DStatic(mesh->static_num);\n        if (!obj->visible) {\n            continue;\n        }\n\n        Matrix_Push();\n        Matrix_TranslateAbs32(mesh->pos);\n        Matrix_RotY(mesh->rot.y);\n        const CLIP clip = Output_CheckBoundsClip(&obj->draw_bounds);\n        if (clip != CLIP_NOT_VISIBLE) {\n            M_DrawSet_Add(&m_DrawnStatics, mesh->draw_num);\n            Output_CalculateStaticMeshLight(mesh->pos, mesh->shade, room);\n            Object_DrawMesh(obj->mesh_idx, clip, false);\n            if (g_Config.debug.enable_debug_bounding_boxes) {\n                Output_DrawCuboid(&obj->draw_bounds);\n            }\n        }\n        Matrix_Pop();\n    }\n\n    M_DrawSet_ForEach(&room->drawn_items, M_DrawRoomItem, nullptr);\n    M_SetupWaterStatus(room);\n\n    g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME);\n    g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME);\n    g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME);\n    g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME);\n\n    int16_t effect_num = room->effect_num;\n    while (effect_num != NO_EFFECT) {\n        const EFFECT *const effect = Effect_Get(effect_num);\n        Effect_Draw(effect_num);\n        effect_num = effect->next_free;\n    }\n\n    Matrix_Pop();\n\n    bind->bound_left = Viewport_GetMaxX(VIEWPORT_GAME);\n    bind->bound_top = Viewport_GetMaxY(VIEWPORT_GAME);\n    bind->bound_right = Viewport_GetMinX(VIEWPORT_GAME);\n    bind->bound_bottom = Viewport_GetMinY(VIEWPORT_GAME);\n}\n\nvoid Room_DrawReset(void)\n{\n    M_EnsureRoomsToDraw();\n    M_DrawSet_Init(&m_DrawnStatics);\n    Vector_Clear(m_RoomsToDraw);\n}\n\nvoid Room_MarkToBeDrawn(const int16_t room_num)\n{\n    if (Vector_Contains(m_RoomsToDraw, &room_num)) {\n        return;\n    }\n    Vector_Add(m_RoomsToDraw, &room_num);\n}\n\nint32_t Room_DrawGetCount(void)\n{\n    return m_RoomsToDraw->count;\n}\n\nint16_t Room_DrawGetRoom(const int16_t idx)\n{\n    return *(int16_t *)Vector_Get(m_RoomsToDraw, idx);\n}\n\nvoid Room_DrawAllRooms(const int16_t current_room, const int16_t target_room)\n{\n    const ROOM *const room = Room_Get(current_room);\n    Output_Bind_ResetRooms();\n    OUTPUT_ROOM_BIND *const bind = Output_Bind_GetRoom(room);\n    bind->test_left = Viewport_GetMinX(VIEWPORT_GAME);\n    bind->test_top = Viewport_GetMinY(VIEWPORT_GAME);\n    bind->test_right = Viewport_GetMaxX(VIEWPORT_GAME);\n    bind->test_bottom = Viewport_GetMaxY(VIEWPORT_GAME);\n    bind->active = true;\n    bind->drawn = false;\n\n    g_PhdLeft = bind->test_left;\n    g_PhdTop = bind->test_top;\n    g_PhdRight = bind->test_right;\n    g_PhdBottom = bind->test_bottom;\n\n    m_BoundRooms[0] = current_room;\n    m_BoundStart = 0;\n    m_BoundEnd = 1;\n\n    Room_DrawReset();\n    m_Outside = room->flags.outside;\n\n    if (m_Outside) {\n        m_OutsideLeft = Viewport_GetMinX(VIEWPORT_GAME);\n        m_OutsideTop = Viewport_GetMinY(VIEWPORT_GAME);\n        m_OutsideRight = Viewport_GetMaxX(VIEWPORT_GAME);\n        m_OutsideBottom = Viewport_GetMaxY(VIEWPORT_GAME);\n    } else {\n        m_OutsideLeft = Viewport_GetMaxX(VIEWPORT_GAME);\n        m_OutsideTop = Viewport_GetMaxY(VIEWPORT_GAME);\n        m_OutsideBottom = Viewport_GetMinY(VIEWPORT_GAME);\n        m_OutsideRight = Viewport_GetMinX(VIEWPORT_GAME);\n    }\n\n    M_GetBounds();\n\n    if (m_Outside) {\n        M_DrawSkybox();\n    }\n\n    Output_Bind_ResetItems();\n\n    for (int32_t i = 0; i < Room_DrawGetCount(); i++) {\n        const int16_t draw_room_num = Room_DrawGetRoom(i);\n        const ROOM *const draw_room = Room_Get(draw_room_num);\n        M_DrawSingleRoom(draw_room);\n        OUTPUT_ROOM_BIND *const draw_bind = Output_Bind_GetRoom(draw_room);\n        draw_bind->active = false;\n        draw_bind->drawn = false;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    if (Object_Get(O_LARA)->loaded) {\n        const ROOM *const lara_room = Room_Get(lara_item->room_num);\n        M_SetupWaterStatus(lara_room);\n        Output_SetCurrentRoom(lara_room);\n        Lara_Draw(lara_item);\n    }\n\n    Output_SetupAboveWater(false);\n    FX_Draw();\n    Sparks_Draw();\n}\n\nvoid Room_AddDrawnItem(const int16_t room_num, const int16_t item_num)\n{\n    if (room_num != NO_ROOM) {\n        ROOM *const room = Room_Get(room_num);\n        M_DrawSet_Add(&room->drawn_items, item_num);\n    }\n}\n\nvoid Room_RemoveDrawnItem(const int16_t room_num, const int16_t item_num)\n{\n    if (room_num != NO_ROOM) {\n        ROOM *const room = Room_Get(room_num);\n        M_DrawSet_Remove(&room->drawn_items, item_num);\n    }\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    Vector_Free(m_RoomsToDraw);\n    m_RoomsToDraw = nullptr;\n}\n"
  },
  {
    "path": "src/trx/game/rooms/draw.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid Room_DrawReset(void);\nvoid Room_MarkToBeDrawn(int16_t room_num);\nint32_t Room_DrawGetCount(void);\nint16_t Room_DrawGetRoom(int16_t idx);\n\nvoid Room_DrawAllRooms(int16_t base_room, int16_t target_room);\n\n// Manage per-room draw queue of items\nvoid Room_AddDrawnItem(int16_t room_num, int16_t item_num);\nvoid Room_RemoveDrawnItem(int16_t room_num, int16_t item_num);\n"
  },
  {
    "path": "src/trx/game/rooms/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    HT_WALL = 0,\n    HT_SMALL_SLOPE = 1,\n    HT_BIG_SLOPE = 2,\n    HT_DIAGONAL = 3,\n    HT_SPLIT_TRI = 4,\n} HEIGHT_TYPE;\n\ntypedef enum {\n    RLM_NORMAL = 0,\n    RLM_FLICKER = 1,\n    RLM_GLOW = 2,\n    RLM_SUNSET = 3,\n    RLM_NUMBER_OF = 4,\n} ROOM_LIGHT_MODE;\n\ntypedef enum {\n    RFS_NONE = 0,\n    RFS_UNFLIPPED = 1,\n    RFS_FLIPPED = 2,\n} ROOM_FLIP_STATUS;\n\ntypedef enum {\n    FT_FLOOR = 0,\n    FT_DOOR = 1,\n    FT_TILT = 2,\n    FT_ROOF = 3,\n    FT_TRIGGER = 4,\n    FT_LAVA = 5,\n    FT_CLIMB = 6,\n    FT_FLOOR_NWSE_SOLID = 7,\n    FT_FLOOR_NESW_SOLID = 8,\n    FT_ROOF_NWSE_SOLID = 9,\n    FT_ROOF_NESW_SOLID = 10,\n    FT_FLOOR_NWSE_PORTAL_SW = 11,\n    FT_FLOOR_NWSE_PORTAL_NE = 12,\n    FT_FLOOR_NESW_PORTAL_SE = 13,\n    FT_FLOOR_NESW_PORTAL_NW = 14,\n    FT_ROOF_NWSE_PORTAL_SW = 15,\n    FT_ROOF_NWSE_PORTAL_NE = 16,\n    FT_ROOF_NESW_PORTAL_NW = 17,\n    FT_ROOF_NESW_PORTAL_SE = 18,\n    FT_MONKEY = 19,\n    FT_MINE_CART_LEFT = 20,\n    FT_MINE_CART_RIGHT = 21,\n} FLOOR_TYPE;\n\ntypedef enum {\n    SPLIT_NONE,\n    SPLIT_NWSE_SOLID,\n    SPLIT_NESW_SOLID,\n    SPLIT_NWSE_PORTAL_SW,\n    SPLIT_NWSE_PORTAL_NE,\n    SPLIT_NESW_PORTAL_SE,\n    SPLIT_NESW_PORTAL_NW,\n} SPLIT_TYPE;\n\ntypedef enum {\n    SURFACE_FLOOR,\n    SURFACE_CEILING,\n} SURFACE_TYPE;\n\ntypedef enum {\n    TO_OBJECT = 0,\n    TO_CAMERA = 1,\n    TO_SINK = 2,\n    TO_FLIPMAP = 3,\n    TO_FLIPON = 4,\n    TO_FLIPOFF = 5,\n    TO_TARGET = 6,\n    TO_FINISH = 7,\n    TO_CD = 8,\n    TO_FLIPEFFECT = 9,\n    TO_SECRET = 10,\n    TO_BODY_BAG = 11,\n} TRIGGER_OBJECT;\n\ntypedef enum {\n    TT_TRIGGER = 0,\n    TT_PAD = 1,\n    TT_SWITCH = 2,\n    TT_KEY = 3,\n    TT_PICKUP = 4,\n    TT_HEAVY = 5,\n    TT_ANTIPAD = 6,\n    TT_COMBAT = 7,\n    TT_DUMMY = 8,\n    TT_ANTITRIGGER = 9,\n} TRIGGER_TYPE;\n\ntypedef enum {\n    LADDER_NONE = 0,\n    LADDER_NORTH = 1 << 0,\n    LADDER_EAST = 1 << 1,\n    LADDER_SOUTH = 1 << 2,\n    LADDER_WEST = 1 << 3,\n    LADDER_CEILING = 1 << 4,\n} LADDER_DIRECTION;\n\ntypedef enum {\n    MINE_CART_NONE,\n    MINE_CART_LEFT,\n    MINE_CART_RIGHT,\n    MINE_CART_STOP,\n} MINE_CART_TYPE;\n"
  },
  {
    "path": "src/trx/game/rooms/floor_data.c",
    "content": "#include <trx/game/rooms/floor_data.h>\n\n#include <trx/config.h>\n#include <trx/game/camera.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/gym.h>\n#include <trx/game/items.h>\n#include <trx/game/lara.h>\n#include <trx/game/level/settings.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/general/keyhole.h>\n#include <trx/game/objects/general/pickup.h>\n#include <trx/game/objects/general/shoal.h>\n#include <trx/game/objects/general/switch.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_NULL_INDEX 0\n#define M_IS_DONE(t) ((t & 0x8000) == 0x8000)\n\n#define M_ENTRY_TYPE(t) (t & 0x1F)\n#define M_TRIG_TYPE(t) ((t & 0x7F00) >> 8)\n#define M_TRIG_TIMER(t) (t & 0xFF)\n#define M_TRIG_ONE_SHOT(t) ((t & 0x100) == 0x100)\n#define M_TRIG_MASK(t) (t & 0x3E00)\n#define M_TRIG_CMD_TYPE(t) ((t & 0x7C00) >> 10)\n#define M_TRIG_CMD_ARG(t) (t & 0x3FF)\n#define M_TRIG_CAM_GLIDE(t) ((t & 0x3E00) >> 6)\n#define M_LADDER_TYPE(t) ((t & 0x7F00) >> 8)\n\nstatic bool M_IsSpeechTrack(const MUSIC_ID track_id)\n{\n    switch (Music_FromGameID(track_id)) {\n    case MX_BALDY_SPEECH:\n    case MX_COWBOY_SPEECH:\n    case MX_LARSON_SPEECH:\n    case MX_NATLA_SPEECH:\n    case MX_PIERRE_SPEECH:\n    case MX_SKATEKID_SPEECH:\n        return true;\n    default:\n        return false;\n    }\n}\n\nstatic const int16_t *M_ReadTrigger(\n    const int16_t *data, const int16_t fd_entry, SECTOR *const sector)\n{\n    TRIGGER *const trigger = GameBuf_Alloc(sizeof(TRIGGER), GBUF_FLOOR_DATA);\n\n    const int16_t trig_setup = *data++;\n    trigger->enabled = true;\n    trigger->type = M_TRIG_TYPE(fd_entry);\n    trigger->timer = M_TRIG_TIMER(trig_setup);\n    trigger->one_shot = M_TRIG_ONE_SHOT(trig_setup);\n    trigger->mask = M_TRIG_MASK(trig_setup);\n    trigger->item_index = NO_ITEM;\n\n    if (trigger->type == TT_SWITCH || trigger->type == TT_KEY\n        || trigger->type == TT_PICKUP) {\n        const int16_t item_data = *data++;\n        trigger->item_index = M_TRIG_CMD_ARG(item_data);\n        if (M_IS_DONE(item_data)) {\n            return data;\n        }\n    }\n\n    TRIGGER_CMD *cmd;\n    if (sector->trigger == nullptr) {\n        sector->trigger = trigger;\n        sector->trigger->command =\n            GameBuf_Alloc(sizeof(TRIGGER_CMD), GBUF_FLOOR_DATA);\n        cmd = sector->trigger->command;\n    } else {\n        // Some old TRLEs have incorrectly formatted floor data, with multiple\n        // trigger entries defined where regular triggers overlap dummies. In\n        // this case we link the new commands onto the old.\n        cmd = sector->trigger->command;\n        while (cmd->next_cmd != nullptr) {\n            cmd = cmd->next_cmd;\n        }\n        cmd->next_cmd = GameBuf_Alloc(sizeof(TRIGGER_CMD), GBUF_FLOOR_DATA);\n        cmd = cmd->next_cmd;\n    }\n\n    while (true) {\n        int16_t command = *data++;\n        cmd->type = M_TRIG_CMD_TYPE(command);\n\n        if (cmd->type == TO_CAMERA) {\n            TRIGGER_CAMERA_DATA *const cam_data =\n                GameBuf_Alloc(sizeof(TRIGGER_CAMERA_DATA), GBUF_FLOOR_DATA);\n            cmd->parameter = (void *)cam_data;\n            cam_data->camera_num = M_TRIG_CMD_ARG(command);\n\n            command = *data++;\n            cam_data->timer = M_TRIG_TIMER(command);\n            cam_data->glide = M_TRIG_CAM_GLIDE(command);\n            cam_data->one_shot = M_TRIG_ONE_SHOT(command);\n        } else {\n            cmd->parameter = (void *)(intptr_t)M_TRIG_CMD_ARG(command);\n        }\n\n        if (M_IS_DONE(command)) {\n            cmd->next_cmd = nullptr;\n            break;\n        }\n\n        cmd->next_cmd = GameBuf_Alloc(sizeof(TRIGGER_CMD), GBUF_FLOOR_DATA);\n        cmd = cmd->next_cmd;\n    }\n\n    return data;\n}\n\nstatic bool M_TestLava(const ITEM *const item)\n{\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (item->hit_points < 0 || lara_info->water_status == LWS_CHEAT\n        || (lara_info->water_status == LWS_ABOVE_WATER\n            && item->pos.y != item->floor)) {\n        return false;\n    }\n\n    // OG fix: check if floor index has lava\n    int16_t room_num = item->room_num;\n    const SECTOR *const sector = Room_GetSector(\n        (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n    return sector->is_death_sector;\n}\n\nstatic void M_TriggerMusicTrack(MUSIC_ID track_id, const TRIGGER *const trigger)\n{\n    if (track_id == (MUSIC_ID)0\n        && (trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER)) {\n        Music_Stop();\n        return;\n    }\n\n    if (track_id <= Music_ToGameID(MX_UNUSED_1) || track_id >= MAX_MUSIC_TRACKS\n        || (Game_IsInGym() && !Gym_CanPlayMusicTrack(&track_id))) {\n        return;\n    }\n\n    uint16_t flags = Music_GetTrackFlags(track_id);\n    MUSIC_PLAY_MODE play_mode = MPM_NO_REPEAT;\n    if (g_Config.audio.fix_speeches_killing_music\n        && M_IsSpeechTrack(track_id)) {\n        play_mode = MPM_OVERLAY;\n    }\n\n    // TODO: consolidate\n    if (g_TRVersion == 1) {\n        if ((flags & IF_ONE_SHOT) != 0) {\n            return;\n        }\n\n        if (trigger->type == TT_SWITCH) {\n            flags ^= trigger->mask;\n        } else if (\n            trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER) {\n            flags &= -1 - trigger->mask;\n        } else if (trigger->mask) {\n            flags |= trigger->mask;\n        }\n\n        if ((flags & IF_CODE_BITS) == IF_CODE_BITS) {\n            if (trigger->one_shot) {\n                flags |= IF_ONE_SHOT;\n            }\n            Music_Play_Direct(track_id, play_mode);\n        } else {\n            Music_StopTrack_Direct(track_id);\n        }\n    } else {\n        if (trigger->type != TT_SWITCH) {\n            const int32_t code = trigger->mask;\n            if ((flags & code) != 0) {\n                return;\n            }\n            if (trigger->one_shot) {\n                flags |= code;\n            }\n        }\n\n        if (trigger->timer == 0 || g_TRVersion != 2) {\n            Music_Play_Direct(track_id, play_mode);\n            goto finish;\n        }\n\n        if (track_id != Music_GetDelayedTrack()) {\n            Music_Play_Direct(track_id, MPM_DELAY);\n            flags = (flags & 0xFF00) | ((LOGIC_FPS * trigger->timer) & 0xFF);\n            goto finish;\n        }\n\n        int32_t timer = flags & 0xFF;\n        if (timer == 0) {\n            goto finish;\n        }\n\n        timer--;\n        if (timer == 0) {\n            Music_Play_Direct(track_id, play_mode);\n        }\n        flags = (flags & 0xFF00) | (timer & 0xFF);\n    }\n\nfinish:\n    Music_SetTrackFlags(track_id, flags);\n}\n\nvoid Room_ParseFloorData(const int16_t *floor_data)\n{\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        for (int32_t j = 0; j < room->size.x * room->size.z; j++) {\n            SECTOR *const sector = &room->sectors[j];\n            Room_PopulateSectorData(\n                sector, floor_data, sector->idx, M_NULL_INDEX);\n        }\n    }\n}\n\nvoid Room_PopulateSectorData(\n    SECTOR *const sector, const int16_t *floor_data, const uint16_t start_index,\n    const uint16_t null_index)\n{\n    sector->floor.type = SURFACE_FLOOR;\n    sector->ceiling.type = SURFACE_CEILING;\n    sector->floor.tilt = (XZ_16) {};\n    sector->ceiling.tilt = (XZ_16) {};\n    sector->floor.split.type = SPLIT_NONE;\n    sector->ceiling.split.type = SPLIT_NONE;\n    sector->floor.is_split = false;\n    sector->ceiling.is_split = false;\n    sector->portal_room.wall = NO_ROOM;\n    sector->is_death_sector = false;\n    sector->trigger = nullptr;\n    sector->ladder = LADDER_NONE;\n    sector->mine_cart_type = MINE_CART_NONE;\n\n    if (start_index == null_index || floor_data == nullptr) {\n        return;\n    }\n\n#define L_TILT(tilt)                                                           \\\n    do {                                                                       \\\n        const int16_t tilt_value = *data++;                                    \\\n        tilt.x = (int8_t)tilt_value;                                           \\\n        tilt.z = tilt_value >> 8;                                              \\\n    } while (false)\n\n    const int16_t *data = &floor_data[start_index];\n    int16_t fd_entry;\n    do {\n        fd_entry = *data++;\n\n        switch (M_ENTRY_TYPE(fd_entry)) {\n        case FT_TILT:\n            L_TILT(sector->floor.tilt);\n            break;\n\n        case FT_ROOF:\n            L_TILT(sector->ceiling.tilt);\n            break;\n\n        case FT_DOOR:\n            const int16_t portal_room = *data++;\n            if (sector->portal_room.wall == NO_ROOM) {\n                sector->portal_room.wall = portal_room;\n            }\n            break;\n\n        case FT_LAVA:\n            sector->is_death_sector = true;\n            break;\n\n        case FT_TRIGGER:\n            data = M_ReadTrigger(data, fd_entry, sector);\n            break;\n\n        case FT_CLIMB:\n            sector->ladder |= (LADDER_DIRECTION)M_LADDER_TYPE(fd_entry);\n            break;\n\n        case FT_MONKEY:\n            sector->ladder |= LADDER_CEILING;\n            break;\n\n        case FT_FLOOR_NWSE_SOLID:\n        case FT_FLOOR_NESW_SOLID:\n        case FT_FLOOR_NWSE_PORTAL_SW:\n        case FT_FLOOR_NWSE_PORTAL_NE:\n        case FT_FLOOR_NESW_PORTAL_SE:\n        case FT_FLOOR_NESW_PORTAL_NW:\n            Room_ReadTriangulation(&sector->floor, fd_entry, *data++);\n            break;\n\n        case FT_ROOF_NWSE_SOLID:\n        case FT_ROOF_NESW_SOLID:\n        case FT_ROOF_NWSE_PORTAL_SW:\n        case FT_ROOF_NWSE_PORTAL_NE:\n        case FT_ROOF_NESW_PORTAL_NW:\n        case FT_ROOF_NESW_PORTAL_SE:\n            Room_ReadTriangulation(&sector->ceiling, fd_entry, *data++);\n            break;\n\n        case FT_MINE_CART_LEFT:\n            sector->mine_cart_type = MINE_CART_LEFT;\n            break;\n\n        case FT_MINE_CART_RIGHT:\n            sector->mine_cart_type = sector->mine_cart_type == MINE_CART_LEFT\n                ? MINE_CART_STOP\n                : MINE_CART_RIGHT;\n            break;\n\n        default:\n            break;\n        }\n    } while (!M_IS_DONE(fd_entry));\n\n#undef L_TILT\n}\n\nvoid Room_ReadTriangulation(\n    SURFACE *const surface, const int16_t func_data, const int16_t tilt_data)\n{\n    switch (M_ENTRY_TYPE(func_data)) {\n    case FT_FLOOR_NWSE_SOLID:\n    case FT_ROOF_NWSE_SOLID:\n        surface->split.type = SPLIT_NWSE_SOLID;\n        break;\n    case FT_FLOOR_NESW_SOLID:\n    case FT_ROOF_NESW_SOLID:\n        surface->split.type = SPLIT_NESW_SOLID;\n        break;\n    case FT_FLOOR_NWSE_PORTAL_SW:\n    case FT_ROOF_NWSE_PORTAL_SW:\n        surface->split.type = SPLIT_NWSE_PORTAL_SW;\n        break;\n    case FT_FLOOR_NWSE_PORTAL_NE:\n    case FT_ROOF_NWSE_PORTAL_NE:\n        surface->split.type = SPLIT_NWSE_PORTAL_NE;\n        break;\n    case FT_FLOOR_NESW_PORTAL_SE:\n    case FT_ROOF_NESW_PORTAL_SE:\n        surface->split.type = SPLIT_NESW_PORTAL_SE;\n        break;\n    case FT_FLOOR_NESW_PORTAL_NW:\n    case FT_ROOF_NESW_PORTAL_NW:\n        surface->split.type = SPLIT_NESW_PORTAL_NW;\n        break;\n    default:\n        return;\n    }\n\n    surface->is_split = true;\n    surface->split.h1 = (func_data & 0x03E0) >> 5;\n    surface->split.h2 = (func_data & 0x7C00) >> 10;\n    if ((surface->split.h1 & 0x10) != 0) {\n        surface->split.h1 |= 0xFFF0;\n    }\n    if ((surface->split.h2 & 0x10) != 0) {\n        surface->split.h2 |= 0xFFF0;\n    }\n    surface->split.h1 <<= 8;\n    surface->split.h2 <<= 8;\n\n    for (int32_t i = 0; i < 4; i++) {\n        surface->split.tilts[i] = (tilt_data >> (i * 4)) & 0xF;\n        if (surface->type == SURFACE_CEILING) {\n            surface->split.tilts[i] *= -1;\n        }\n    }\n}\n\nbool Room_TestTriggers(const ITEM *const item)\n{\n    int16_t room_num = item->room_num;\n    const SECTOR *sector = Room_GetSector(\n        (XYZ_32) { item->pos.x, MAX_HEIGHT, item->pos.z }, &room_num);\n\n    bool result = Room_TestSectorTrigger(item, sector);\n    if (item->object_id != O_TORSO) {\n        return result;\n    }\n\n    for (int32_t dx = -1; dx < 2; dx++) {\n        for (int32_t dz = -1; dz < 2; dz++) {\n            if (dx == 0 && dz == 0) {\n                continue;\n            }\n\n            room_num = item->room_num;\n            sector = Room_GetSector(\n                (XYZ_32) {\n                    item->pos.x + dx * WALL_L,\n                    MAX_HEIGHT,\n                    item->pos.z + dz * WALL_L,\n                },\n                &room_num);\n            result |= Room_TestSectorTrigger(item, sector);\n        }\n    }\n\n    return result;\n}\n\nbool Room_TestSectorTrigger(const ITEM *const item, const SECTOR *const sector)\n{\n    LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    const bool is_heavy = item->object_id != O_LARA;\n    if (!is_heavy) {\n        if (sector->is_death_sector && M_TestLava(item)) {\n            Lara_TouchDeathSector(Level_GetDeathTile());\n        }\n\n        const LADDER_DIRECTION direction = 1 << Math_GetDirection(item->rot.y);\n        lara_info->climb_status = (sector->ladder & direction) == direction;\n    }\n\n    const TRIGGER *const trigger = sector->trigger;\n    if (trigger == nullptr || !trigger->enabled) {\n        return false;\n    }\n\n    if (g_Camera.type != CAM_HEAVY) {\n        Camera_RefreshFromTrigger(trigger);\n    }\n\n    ITEM *camera_item = nullptr;\n    bool switch_off = false;\n    bool flip_map = false;\n    bool flip_available = false;\n    int32_t new_effect = -1;\n    const bool flip_status = Room_GetFlipStatus();\n\n    if (is_heavy) {\n        if (trigger->type != TT_HEAVY) {\n            return false;\n        }\n    } else {\n        switch (trigger->type) {\n        case TT_PAD:\n        case TT_ANTIPAD:\n            if (!Gym_TrackManager_OnPadContact(GYM_TRACK_ASSAULT, false)) {\n                return false;\n            }\n            if (item->pos.y != item->floor) {\n                return false;\n            }\n            if (item->object_id == O_LARA\n                && !Gym_TrackManager_OnPadContact(GYM_TRACK_ASSAULT, true)) {\n                return false;\n            }\n            break;\n\n        case TT_SWITCH: {\n            const bool switch_result =\n                Switch_Trigger(trigger->item_index, trigger->timer);\n            ITEM *const switch_item = Item_Get(trigger->item_index);\n            if (g_TRVersion >= 3 && trigger->one_shot) {\n                switch_item->flags |= IF_ONE_SHOT_SWITCH;\n            }\n            if (!switch_result) {\n                return false;\n            }\n            switch_off = switch_item->current_anim_state == SWITCH_STATE_OFF;\n            break;\n        }\n\n        case TT_KEY: {\n            if (!Keyhole_Trigger(trigger->item_index)) {\n                return false;\n            }\n            break;\n        }\n\n        case TT_PICKUP: {\n            if (!Pickup_Trigger(trigger->item_index)) {\n                return false;\n            }\n            break;\n        }\n\n        case TT_HEAVY:\n        case TT_DUMMY:\n            return false;\n\n        case TT_COMBAT:\n            if (lara_info->gun_status != LGS_READY) {\n                return false;\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    const TRIGGER_CMD *cmd = trigger->command;\n    for (; cmd != nullptr; cmd = cmd->next_cmd) {\n        switch (cmd->type) {\n        case TO_OBJECT: {\n            const int16_t item_num = (int16_t)(intptr_t)cmd->parameter;\n            ITEM *const trig_item = Item_Get(item_num);\n\n            bool one_shot = false;\n            if (g_TRVersion == 3) {\n                switch (trigger->type) {\n                case TT_SWITCH:\n                    one_shot = trig_item->flags & IF_ONE_SHOT_SWITCH;\n                    break;\n                case TT_ANTIPAD:\n                case TT_ANTITRIGGER:\n                    one_shot = trig_item->flags & IF_ONE_SHOT_ANTITRIGGER;\n                    break;\n                default:\n                    one_shot = trig_item->flags & IF_ONE_SHOT;\n                    break;\n                }\n            } else {\n                one_shot = trig_item->flags & IF_ONE_SHOT;\n            }\n            if (one_shot) {\n                break;\n            }\n\n            const OBJECT *const obj = Object_Get(trig_item->object_id);\n\n            const bool is_shoal_object =\n                Object_IsType(trig_item->object_id, g_ShoalObjects);\n\n            if (is_shoal_object && trigger->type != TT_ANTIPAD\n                && trigger->type != TT_ANTITRIGGER) {\n                Shoal_TriggerActivate(trig_item, trigger->timer);\n            } else {\n                trig_item->timer = trigger->timer;\n                if (trig_item->timer != 1) {\n                    trig_item->timer *= LOGIC_FPS;\n                }\n\n                if (obj->trigger_func != nullptr) {\n                    const bool use_default_handling =\n                        obj->trigger_func(trig_item, trigger);\n                    if (!use_default_handling) {\n                        break;\n                    }\n                }\n            }\n\n            if (trigger->type == TT_SWITCH) {\n                trig_item->flags ^= trigger->mask;\n                if (trigger->one_shot && g_TRVersion == 3) {\n                    trig_item->flags |= IF_ONE_SHOT_SWITCH;\n                }\n            } else if (\n                trigger->type == TT_ANTIPAD\n                || trigger->type == TT_ANTITRIGGER) {\n                if (is_shoal_object) {\n                    Shoal_TriggerDeactivate(trig_item);\n                }\n\n                // TODO investigate unifying as ~(trigger->mask | IF_REVERSE)\n                if (g_TRVersion >= 3) {\n                    trig_item->flags &= ~(IF_CODE_BITS | IF_REVERSE);\n                } else {\n                    trig_item->flags &= ~trigger->mask;\n                }\n                if (trigger->one_shot) {\n                    if (g_TRVersion == 3) {\n                        trig_item->flags |= IF_ONE_SHOT_ANTITRIGGER;\n                    } else {\n                        trig_item->flags |= IF_ONE_SHOT;\n                    }\n                }\n            } else {\n                trig_item->flags |= trigger->mask;\n            }\n\n            if ((trig_item->flags & IF_CODE_BITS) != IF_CODE_BITS) {\n                break;\n            }\n\n            if (trigger->one_shot) {\n                trig_item->flags |= IF_ONE_SHOT;\n            }\n\n            if (trig_item->active) {\n                break;\n            }\n\n            if (obj->activate_func != nullptr) {\n                obj->activate_func(trig_item);\n            } else if (obj->intelligent) {\n                if (trig_item->status == IS_INACTIVE) {\n                    trig_item->touch_bits = 0;\n                    trig_item->status = IS_ACTIVE;\n                    Item_AddActive(item_num);\n                    LOT_EnableBaddieAI(item_num, true);\n                } else if (trig_item->status == IS_INVISIBLE) {\n                    trig_item->touch_bits = 0;\n                    if (LOT_EnableBaddieAI(item_num, false)) {\n                        trig_item->status = IS_ACTIVE;\n                    } else {\n                        trig_item->status = IS_INVISIBLE;\n                    }\n                    Item_AddActive(item_num);\n                }\n            } else {\n                trig_item->touch_bits = 0;\n                trig_item->status = IS_ACTIVE;\n                Item_AddActive(item_num);\n            }\n\n            break;\n        }\n\n        case TO_CAMERA: {\n            const TRIGGER_CAMERA_DATA *const cam_data =\n                (TRIGGER_CAMERA_DATA *)cmd->parameter;\n            OBJECT_VECTOR *const camera =\n                Camera_GetFixedObject(cam_data->camera_num);\n            if ((camera->flags & IF_ONE_SHOT) != 0) {\n                break;\n            }\n\n            g_Camera.num = cam_data->camera_num;\n\n            if ((g_Camera.type == CAM_LOOK || g_Camera.type == CAM_COMBAT)\n                && !Camera_IsLocked(g_Camera.num)) {\n                break;\n            }\n\n            if (trigger->type == TT_COMBAT) {\n                break;\n            }\n\n            if (trigger->type == TT_SWITCH && trigger->timer != 0\n                && switch_off) {\n                break;\n            }\n\n            if (g_Camera.num == g_Camera.last && trigger->type != TT_SWITCH) {\n                break;\n            }\n\n            g_Camera.timer = LOGIC_FPS * cam_data->timer;\n\n            if (cam_data->one_shot) {\n                camera->flags |= IF_ONE_SHOT;\n            }\n\n            g_Camera.speed = 1;\n            if (g_Config.visuals.enable_glide_cameras) {\n                g_Camera.speed += cam_data->glide;\n            }\n            g_Camera.type = is_heavy ? CAM_HEAVY : CAM_FIXED;\n            break;\n        }\n\n        case TO_SINK: {\n            if (g_TRVersion == 3) {\n                lara_info->current.active =\n                    1 + (int16_t)(intptr_t)cmd->parameter;\n            } else {\n                const OBJECT_VECTOR *const sink =\n                    Camera_GetFixedObject((int16_t)(intptr_t)cmd->parameter);\n\n                if (g_TRVersion == 2\n                    || lara_info->lot.required_box != sink->flags) {\n                    lara_info->lot.target = sink->pos;\n                    lara_info->lot.required_box = sink->flags;\n                }\n                lara_info->current.active = sink->data * 6;\n            }\n            break;\n        }\n\n        case TO_FLIPMAP: {\n            const int16_t flip_slot = (int16_t)(intptr_t)cmd->parameter;\n            int32_t slot_flags = Room_GetFlipSlotFlags(flip_slot);\n            flip_available = true;\n\n            if (slot_flags & IF_ONE_SHOT) {\n                break;\n            }\n\n            if (trigger->type == TT_SWITCH) {\n                slot_flags ^= trigger->mask;\n            } else {\n                slot_flags |= trigger->mask;\n            }\n\n            if ((slot_flags & IF_CODE_BITS) == IF_CODE_BITS) {\n                if (trigger->one_shot) {\n                    slot_flags |= IF_ONE_SHOT;\n                }\n\n                if (!flip_status) {\n                    flip_map = true;\n                }\n            } else if (flip_status) {\n                flip_map = true;\n            }\n\n            Room_SetFlipSlotFlags(flip_slot, slot_flags);\n            break;\n        }\n\n        case TO_FLIPON: {\n            const int16_t flip_slot = (int16_t)(intptr_t)cmd->parameter;\n            const int32_t slot_flags = Room_GetFlipSlotFlags(flip_slot);\n            flip_available = true;\n\n            if ((slot_flags & IF_CODE_BITS) == IF_CODE_BITS && !flip_status) {\n                flip_map = true;\n            }\n            break;\n        }\n\n        case TO_FLIPOFF: {\n            const int16_t flip_slot = (int16_t)(intptr_t)cmd->parameter;\n            const int32_t slot_flags = Room_GetFlipSlotFlags(flip_slot);\n            flip_available = true;\n\n            if ((slot_flags & IF_CODE_BITS) == IF_CODE_BITS && flip_status) {\n                flip_map = true;\n            }\n            break;\n        }\n\n        case TO_TARGET: {\n            const int16_t target_num = (int16_t)(intptr_t)cmd->parameter;\n            camera_item = Item_Get(target_num);\n            break;\n        }\n\n        case TO_FINISH:\n            Game_SetIsLevelComplete(true);\n            break;\n\n        case TO_FLIPEFFECT:\n            new_effect = (int16_t)(intptr_t)cmd->parameter;\n            break;\n\n        case TO_CD:\n            M_TriggerMusicTrack((MUSIC_ID)(intptr_t)cmd->parameter, trigger);\n            break;\n\n        case TO_SECRET: {\n            const int16_t secret_num = (int16_t)(intptr_t)cmd->parameter;\n            if (Stats_AddSecret(secret_num)) {\n                const MUSIC_PLAY_MODE mode =\n                    g_Config.audio.fix_secrets_killing_music ? MPM_OVERLAY\n                                                             : MPM_ONCE;\n                Music_Play(MX_SECRET, mode);\n            }\n            break;\n        }\n\n        case TO_BODY_BAG:\n            if (g_Config.gameplay.enable_body_bags) {\n                Item_ClearKilled();\n            }\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    if (camera_item != nullptr\n        && (g_Camera.type == CAM_FIXED || g_Camera.type == CAM_HEAVY)) {\n        g_Camera.item = camera_item;\n    }\n\n    if (flip_map) {\n        Room_FlipMap();\n    }\n\n    if (new_effect != -1 && (flip_map || !flip_available)) {\n        Room_SetFlipEffect(new_effect);\n        Room_SetFlipTimer(0);\n    }\n\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/rooms/floor_data.h",
    "content": "#pragma once\n\n#include <trx/game/rooms/types.h>\n\nvoid Room_ParseFloorData(const int16_t *floor_data);\nvoid Room_PopulateSectorData(\n    SECTOR *sector, const int16_t *floor_data, uint16_t start_index,\n    uint16_t null_index);\nvoid Room_ReadTriangulation(\n    SURFACE *surface, int16_t func_data, int16_t tilt_data);\n\nbool Room_TestTriggers(const ITEM *item);\nbool Room_TestSectorTrigger(const ITEM *item, const SECTOR *sector);\n"
  },
  {
    "path": "src/trx/game/rooms/geometry.c",
    "content": "#include <trx/game/rooms/geometry.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/objects.h>\n#include <trx/game/pathing.h>\n#include <trx/game/rooms.h>\n\n#define M_WALL_MASK (WALL_L - 1)\n#define M_NEG_TILT(T, H) ((T * (H & M_WALL_MASK)) >> 2)\n#define M_POS_TILT(T, H) ((T * ((M_WALL_MASK - H) & M_WALL_MASK)) >> 2)\n\nstatic int16_t m_AbyssMinHeight = 0;\nstatic int32_t m_AbyssMaxHeight = 0;\nstatic HEIGHT_TYPE m_HeightType = HT_WALL;\n\nstatic inline int32_t M_GetTiltShift(\n    const XZ_16 tilt, const int32_t x, const int32_t z, const bool is_ceiling)\n{\n    int32_t shift = 0;\n    if (is_ceiling) {\n        if (tilt.z < 0) {\n            shift += M_NEG_TILT(tilt.z, z);\n        } else {\n            shift -= M_POS_TILT(tilt.z, z);\n        }\n\n        if (tilt.x < 0) {\n            shift += M_POS_TILT(tilt.x, x);\n        } else {\n            shift -= M_NEG_TILT(tilt.x, x);\n        }\n    } else {\n        if (tilt.z < 0) {\n            shift -= M_NEG_TILT(tilt.z, z);\n        } else {\n            shift += M_POS_TILT(tilt.z, z);\n        }\n\n        if (tilt.x < 0) {\n            shift -= M_NEG_TILT(tilt.x, x);\n        } else {\n            shift += M_POS_TILT(tilt.x, x);\n        }\n    }\n\n    return shift;\n}\n\nstatic int32_t M_GetUnsplitSurfaceHeight(\n    const SURFACE surface, const int32_t x, const int32_t z)\n{\n    int32_t height = surface.height;\n    if (surface.tilt.x == 0 && surface.tilt.z == 0) {\n        return height;\n    }\n\n    const HEIGHT_TYPE slope_type =\n        (ABS(surface.tilt.z) > MAX_SLOPE || ABS(surface.tilt.x) > MAX_SLOPE)\n        ? HT_BIG_SLOPE\n        : HT_SMALL_SLOPE;\n    if (Camera_IsChunky() && slope_type == HT_BIG_SLOPE) {\n        return height;\n    }\n\n    const bool is_ceiling = surface.type == SURFACE_CEILING;\n    height += M_GetTiltShift(surface.tilt, x, z, is_ceiling);\n\n    if (!is_ceiling) {\n        m_HeightType = slope_type;\n    }\n\n    return height;\n}\n\nstatic inline XZ_16 M_GetSplitTilt(\n    const SURFACE *const surface, const int32_t x, const int32_t z,\n    int32_t *const shift)\n{\n    const bool is_ceiling = surface->type == SURFACE_CEILING;\n    const SPLIT split = surface->split;\n    const int32_t dx = x & M_WALL_MASK;\n    const int32_t dz = z & M_WALL_MASK;\n    const int16_t t0 = split.tilts[0];\n    const int16_t t1 = split.tilts[1];\n    const int16_t t2 = split.tilts[2];\n    const int16_t t3 = split.tilts[3];\n    XZ_16 tilt = {};\n\n    if (split.type == SPLIT_NWSE_SOLID || split.type == SPLIT_NWSE_PORTAL_SW\n        || split.type == SPLIT_NWSE_PORTAL_NE) {\n        if (dx > WALL_L - dz) {\n            tilt.x = is_ceiling ? (t0 - t1) : (t3 - t2);\n            tilt.z = t3 - t0;\n            *shift = split.h1;\n        } else {\n            tilt.x = is_ceiling ? (t3 - t2) : (t0 - t1);\n            tilt.z = t2 - t1;\n            *shift = split.h2;\n        }\n    } else if (dx > dz) {\n        tilt.x = is_ceiling ? (t3 - t2) : (t0 - t1);\n        tilt.z = t3 - t0;\n        *shift = split.h1;\n    } else {\n        tilt.x = is_ceiling ? (t0 - t1) : (t3 - t2);\n        tilt.z = t2 - t1;\n        *shift = split.h2;\n    }\n\n    return tilt;\n}\n\nstatic int16_t M_GetSplitSurfaceHeight(\n    const SURFACE surface, const int32_t x, const int32_t z)\n{\n    const bool is_ceiling = surface.type == SURFACE_CEILING;\n    if (Camera_IsChunky()) {\n        const int16_t ch1 = surface.height + surface.split.h2;\n        const int16_t ch2 = surface.height + surface.split.h1;\n        return is_ceiling ? MAX(ch1, ch2) : MIN(ch1, ch2);\n    }\n\n    int16_t height = surface.height;\n    if (!is_ceiling) {\n        m_HeightType = HT_SPLIT_TRI;\n    }\n\n    int32_t shift = 0;\n    const XZ_16 tilt = M_GetSplitTilt(&surface, x, z, &shift);\n    shift += M_GetTiltShift(tilt, x, z, is_ceiling);\n    height += shift;\n\n    if (!is_ceiling) {\n        if (ABS(tilt.x) > MAX_SLOPE || ABS(tilt.z) > MAX_SLOPE) {\n            m_HeightType = HT_DIAGONAL;\n        } else if (m_HeightType != HT_SPLIT_TRI) {\n            m_HeightType = HT_SMALL_SLOPE;\n        }\n    }\n\n    return height;\n}\n\nstatic int32_t M_GetSurfaceHeight(\n    const SURFACE surface, const int32_t x, const int32_t z,\n    const bool fix_tilts)\n{\n    if (surface.height == NO_HEIGHT && (surface.is_split || fix_tilts)) {\n        return NO_HEIGHT;\n    }\n\n    return surface.is_split ? M_GetSplitSurfaceHeight(surface, x, z)\n                            : M_GetUnsplitSurfaceHeight(surface, x, z);\n}\n\nstatic bool M_IsPortalSolid(\n    const SURFACE surface, const int32_t x, const int32_t z)\n{\n    if (!surface.is_split) {\n        return false;\n    }\n\n    const int32_t dx = x & M_WALL_MASK;\n    const int32_t dz = z & M_WALL_MASK;\n    const bool is_ceiling = surface.type == SURFACE_CEILING;\n\n    switch (surface.split.type) {\n    case SPLIT_NWSE_PORTAL_SW:\n        return dx > WALL_L - dz;\n    case SPLIT_NWSE_PORTAL_NE:\n        return dx <= WALL_L - dz;\n    case SPLIT_NESW_PORTAL_SE:\n        return is_ceiling ? (dx <= dz) : (dx > dz);\n    case SPLIT_NESW_PORTAL_NW:\n        return is_ceiling ? (dx > dz) : (dx <= dz);\n    default:\n        return false;\n    }\n}\n\nBOUNDS_32 Room_GetRoomBounds(const ROOM *const room)\n{\n    ASSERT(room != nullptr);\n    return (BOUNDS_32) {\n        .min = {\n            .x = room->pos.x + WALL_L,\n            .y = room->max_ceiling,\n            .z = room->pos.z + WALL_L,\n        },\n        .max = {\n            .x = room->pos.x + room->size.x * WALL_L - WALL_L,\n            .y = room->min_floor,\n            .z = room->pos.z + room->size.z * WALL_L - WALL_L,\n        },\n    };\n}\n\nSECTOR *Room_GetSector(const XYZ_32 pos, int16_t *const room_num)\n{\n    SECTOR *sector = nullptr;\n\n    while (true) {\n        const ROOM *room = Room_Get(*room_num);\n        int32_t z_sector = (pos.z - room->pos.z) >> WALL_SHIFT;\n        int32_t x_sector = (pos.x - room->pos.x) >> WALL_SHIFT;\n\n        if (z_sector <= 0) {\n            z_sector = 0;\n            if (x_sector < 1) {\n                x_sector = 1;\n            } else if (x_sector > room->size.x - 2) {\n                x_sector = room->size.x - 2;\n            }\n        } else if (z_sector >= room->size.z - 1) {\n            z_sector = room->size.z - 1;\n            if (x_sector < 1) {\n                x_sector = 1;\n            } else if (x_sector > room->size.x - 2) {\n                x_sector = room->size.x - 2;\n            }\n        } else if (x_sector < 0) {\n            x_sector = 0;\n        } else if (x_sector >= room->size.x) {\n            x_sector = room->size.x - 1;\n        }\n\n        sector = Room_GetUnitSector(room, x_sector, z_sector);\n        if (sector->portal_room.wall == NO_ROOM) {\n            break;\n        }\n        *room_num = sector->portal_room.wall;\n    }\n\n    ASSERT(sector != nullptr);\n\n    if (pos.y >= M_GetSurfaceHeight(sector->floor, pos.x, pos.z, true)) {\n        do {\n            if (sector->portal_room.pit == NO_ROOM\n                || M_IsPortalSolid(sector->floor, pos.x, pos.z)) {\n                break;\n            }\n            *room_num = sector->portal_room.pit;\n            const ROOM *const room = Room_Get(*room_num);\n            sector = Room_GetWorldSector(room, pos.x, pos.z);\n        } while (pos.y\n                 >= M_GetSurfaceHeight(sector->floor, pos.x, pos.z, true));\n    } else if (\n        pos.y < M_GetSurfaceHeight(sector->ceiling, pos.x, pos.z, true)) {\n        do {\n            if (sector->portal_room.sky == NO_ROOM\n                || M_IsPortalSolid(sector->ceiling, pos.x, pos.z)) {\n                break;\n            }\n            *room_num = sector->portal_room.sky;\n            const ROOM *const room = Room_Get(sector->portal_room.sky);\n            sector = Room_GetWorldSector(room, pos.x, pos.z);\n        } while (pos.y\n                 < M_GetSurfaceHeight(sector->ceiling, pos.x, pos.z, true));\n    }\n\n    return sector;\n}\n\nSECTOR *Room_GetSectorOnWalkable(const XYZ_32 pos, int16_t *const room_num)\n{\n    // Resolve wall portals.\n    const ROOM *room = Room_Get(*room_num);\n    SECTOR *sector = Room_GetWorldSector(room, pos.x, pos.z);\n    while (sector->portal_room.wall != NO_ROOM) {\n        *room_num = sector->portal_room.wall;\n        room = Room_Get(*room_num);\n        sector = Room_GetWorldSector(room, pos.x, pos.z);\n    }\n\n    // Check if on a walkable.\n    const int32_t room_height = Room_GetHeight(sector, pos);\n    const bool skip_pit = Room_IsOnWalkable(\n        sector,\n        (XYZ_32) {\n            pos.x,\n            ROUND_TO_HALF_CLICK(pos.y),\n            pos.z,\n        },\n        ROUND_TO_HALF_CLICK(pos.y), NO_ITEM);\n\n    // Traverse pit sector unless on a walkable.\n    if (!skip_pit && pos.y >= sector->floor.height) {\n        while (sector->portal_room.pit != NO_ROOM) {\n            *room_num = sector->portal_room.pit;\n            room = Room_Get(*room_num);\n            sector = Room_GetWorldSector(room, pos.x, pos.z);\n            if (pos.y < sector->floor.height) {\n                break;\n            }\n        }\n    } else if (pos.y < sector->ceiling.height) {\n        while (sector->portal_room.sky != NO_ROOM) {\n            *room_num = sector->portal_room.sky;\n            room = Room_Get(*room_num);\n            sector = Room_GetWorldSector(room, pos.x, pos.z);\n            if (pos.y >= sector->ceiling.height) {\n                break;\n            }\n        }\n    }\n\n    return sector;\n}\n\nSECTOR *Room_GetWorldSector(\n    const ROOM *const room, const int32_t x_pos, const int32_t z_pos)\n{\n    int32_t x_sector = (x_pos - room->pos.x) >> WALL_SHIFT;\n    int32_t z_sector = (z_pos - room->pos.z) >> WALL_SHIFT;\n    CLAMP(x_sector, 0, room->size.x - 1);\n    CLAMP(z_sector, 0, room->size.z - 1);\n    return Room_GetUnitSector(room, x_sector, z_sector);\n}\n\nSECTOR *Room_GetUnitSector(\n    const ROOM *const room, const int32_t x_sector, const int32_t z_sector)\n{\n    return &room->sectors[z_sector + x_sector * room->size.z];\n}\n\nSECTOR *Room_GetPitSector(\n    const SECTOR *sector, const int32_t x, const int32_t z)\n{\n    while (sector->portal_room.pit != NO_ROOM\n           && !M_IsPortalSolid(sector->floor, x, z)) {\n        const ROOM *const room = Room_Get(sector->portal_room.pit);\n        sector = Room_GetWorldSector(room, x, z);\n    }\n\n    return (SECTOR *)sector;\n}\n\nSECTOR *Room_GetSkySector(\n    const SECTOR *sector, const int32_t x, const int32_t z)\n{\n    while (sector->portal_room.sky != NO_ROOM\n           && !M_IsPortalSolid(sector->ceiling, x, z)) {\n        const ROOM *const room = Room_Get(sector->portal_room.sky);\n        sector = Room_GetWorldSector(room, x, z);\n    }\n\n    return (SECTOR *)sector;\n}\n\nvoid Room_SetAbyssHeight(const int16_t height)\n{\n    // Once Lara reaches the min abyss height, she will be killed; she will\n    // continue to fall however, so the max height is needed until the inventory\n    // is shown, otherwise Lara will hit the floor.\n    m_AbyssMinHeight = height;\n    m_AbyssMaxHeight = height == 0 ? 0 : m_AbyssMinHeight + 26 * STEP_L;\n    CLAMPG(m_AbyssMaxHeight, MAX_HEIGHT - STEP_L);\n}\n\nbool Room_IsAbyssHeight(const int32_t height)\n{\n    return m_AbyssMinHeight != 0 && height >= m_AbyssMinHeight;\n}\n\nHEIGHT_TYPE Room_GetHeightType(void)\n{\n    return m_HeightType;\n}\n\nXZ_16 Room_GetTiltType(const SECTOR *sector, const XYZ_32 pos)\n{\n    sector = Room_GetPitSector(sector, pos.x, pos.z);\n\n    if ((pos.y + STEP_L * 2) < sector->floor.height) {\n        return (XZ_16) {};\n    }\n\n    if (!sector->floor.is_split) {\n        return sector->floor.tilt;\n    }\n\n    int32_t shift = 0;\n    return M_GetSplitTilt(&sector->floor, pos.x, pos.z, &shift);\n}\n\nint32_t Room_GetHeight(const SECTOR *const sector, const XYZ_32 pos)\n{\n    return Room_GetHeightEx(\n        sector, pos, g_Config.gameplay.fix_wall_geometry, NO_ITEM);\n}\n\nint32_t Room_GetFloorHeightForSector(\n    const SECTOR *const sector, const int32_t x, const int32_t z,\n    const bool fix_tilts)\n{\n    m_HeightType = HT_WALL;\n    if (Room_IsAbyssHeight(sector->floor.height)) {\n        return m_AbyssMaxHeight;\n    }\n    return M_GetSurfaceHeight(sector->floor, x, z, fix_tilts);\n}\n\nint32_t Room_GetHeightEx(\n    const SECTOR *const sector, const XYZ_32 pos, const bool fix_tilts,\n    const int16_t ignore_item_num)\n{\n    m_HeightType = HT_WALL;\n\n    const SECTOR *const pit_sector = Room_GetPitSector(sector, pos.x, pos.z);\n    int32_t height = pit_sector->floor.height;\n\n    if (Room_IsAbyssHeight(height)) {\n        height = m_AbyssMaxHeight;\n    } else {\n        height = M_GetSurfaceHeight(pit_sector->floor, pos.x, pos.z, fix_tilts);\n    }\n\n    // Climb the stack of walkables. In each iteration the test Y pos is moved\n    // up to match the current height, so preventing testing below previous\n    // walkables.\n    int32_t base_height = height;\n    int32_t test_y = pos.y;\n    for (const WALKABLE *w = pit_sector->walkable; w != nullptr; w = w->next) {\n        if (w->item_num == ignore_item_num) {\n            continue;\n        }\n        const ITEM *const item = Item_Get(w->item_num);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->floor_height_func == nullptr) {\n            continue;\n        }\n        height = obj->floor_height_func(item, pos.x, test_y, pos.z, height);\n        test_y = MIN(pos.y, height);\n    }\n\n    if (base_height != height) {\n        // A walkable is present, which always override slopes below.\n        m_HeightType = HT_WALL;\n    }\n\n    return height;\n}\n\nint32_t Room_GetCeiling(const SECTOR *const sector, const XYZ_32 pos)\n{\n    return Room_GetCeilingEx(sector, pos, g_Config.gameplay.fix_wall_geometry);\n}\n\nint32_t Room_GetCeilingEx(\n    const SECTOR *const sector, const XYZ_32 pos, const bool fix_tilts)\n{\n    const SECTOR *const sky_sector = Room_GetSkySector(sector, pos.x, pos.z);\n    int32_t height =\n        M_GetSurfaceHeight(sky_sector->ceiling, pos.x, pos.z, fix_tilts);\n\n    const SECTOR *const pit_sector = Room_GetPitSector(sector, pos.x, pos.z);\n\n    for (const WALKABLE *w = pit_sector->walkable; w != nullptr; w = w->next) {\n        const ITEM *const item = Item_Get(w->item_num);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->ceiling_height_func != nullptr) {\n            height =\n                obj->ceiling_height_func(item, pos.x, pos.y, pos.z, height);\n        }\n    }\n\n    return height;\n}\n\nint32_t Room_GetWaterHeight(const XYZ_32 pos, const int16_t room_num)\n{\n    return Room_GetWaterHeightEx(\n        pos, room_num, g_Config.gameplay.fix_wall_geometry);\n}\n\nint32_t Room_GetWaterHeightEx(\n    const XYZ_32 pos, int16_t room_num, const bool fix_tilts)\n{\n    const int32_t x = pos.x;\n    const int32_t y = pos.y;\n    const int32_t z = pos.z;\n    const SECTOR *sector = nullptr;\n    const ROOM *room = nullptr;\n\n    do {\n        room = Room_Get(room_num);\n        int32_t z_sector = (z - room->pos.z) >> WALL_SHIFT;\n        int32_t x_sector = (x - room->pos.x) >> WALL_SHIFT;\n\n        if (z_sector <= 0) {\n            z_sector = 0;\n            if (x_sector < 1) {\n                x_sector = 1;\n            } else if (x_sector > room->size.x - 2) {\n                x_sector = room->size.x - 2;\n            }\n        } else if (z_sector >= room->size.z - 1) {\n            z_sector = room->size.z - 1;\n            if (x_sector < 1) {\n                x_sector = 1;\n            } else if (x_sector > room->size.x - 2) {\n                x_sector = room->size.x - 2;\n            }\n        } else if (x_sector < 0) {\n            x_sector = 0;\n        } else if (x_sector >= room->size.x) {\n            x_sector = room->size.x - 1;\n        }\n\n        sector = Room_GetUnitSector(room, x_sector, z_sector);\n        room_num = sector->portal_room.wall;\n    } while (room_num != NO_ROOM);\n\n    if (room->flags.underwater || room->flags.swamp) {\n        while (sector->portal_room.sky != NO_ROOM\n               && !M_IsPortalSolid(sector->ceiling, x, z)) {\n            room = Room_Get(sector->portal_room.sky);\n            if (!room->flags.underwater && !room->flags.swamp) {\n                if (fix_tilts) {\n                    break;\n                } else {\n                    return room->min_floor;\n                }\n            }\n            sector = Room_GetWorldSector(room, x, z);\n        }\n        return fix_tilts ? M_GetSurfaceHeight(sector->ceiling, x, z, true)\n                         : room->max_ceiling;\n    } else {\n        while (sector->portal_room.pit != NO_ROOM\n               && !M_IsPortalSolid(sector->floor, x, z)) {\n            room = Room_Get(sector->portal_room.pit);\n            if (room->flags.underwater || room->flags.swamp) {\n                return fix_tilts ? M_GetSurfaceHeight(sector->floor, x, z, true)\n                                 : room->max_ceiling;\n            }\n            sector = Room_GetWorldSector(room, x, z);\n        }\n        return NO_HEIGHT;\n    }\n}\n\nvoid Room_AlterFloorHeight(const ITEM *const item, const int32_t height)\n{\n    if (height == 0) {\n        return;\n    }\n\n    int16_t portal_room;\n    SECTOR *sector;\n    const ROOM *room = Room_Get(item->room_num);\n\n    do {\n        int32_t z_sector = (item->pos.z - room->pos.z) >> WALL_SHIFT;\n        int32_t x_sector = (item->pos.x - room->pos.x) >> WALL_SHIFT;\n\n        if (z_sector <= 0) {\n            z_sector = 0;\n            CLAMP(x_sector, 1, room->size.x - 2);\n        } else if (z_sector >= room->size.z - 1) {\n            z_sector = room->size.z - 1;\n            CLAMP(x_sector, 1, room->size.x - 2);\n        } else {\n            CLAMP(x_sector, 0, room->size.x - 1);\n        }\n\n        sector = Room_GetUnitSector(room, x_sector, z_sector);\n        portal_room = sector->portal_room.wall;\n        if (portal_room != NO_ROOM) {\n            room = Room_Get(portal_room);\n        }\n    } while (portal_room != NO_ROOM);\n\n    const SECTOR *const sky_sector =\n        Room_GetSkySector(sector, item->pos.x, item->pos.z);\n    sector = Room_GetPitSector(sector, item->pos.x, item->pos.z);\n\n    if (sector->floor.height != NO_HEIGHT) {\n        sector->floor.height += ROUND_TO_CLICK(height);\n        if (sector->floor.height == sky_sector->ceiling.height) {\n            sector->floor.height = NO_HEIGHT;\n        }\n    } else {\n        sector->floor.height =\n            sky_sector->ceiling.height + ROUND_TO_CLICK(height);\n    }\n\n    BOX_INFO *const box = Box_GetBox(sector->box);\n    if (box->overlap_index & BOX_BLOCKABLE) {\n        if (height < 0) {\n            box->overlap_index |= BOX_BLOCKED;\n        } else {\n            box->overlap_index &= ~BOX_BLOCKED;\n        }\n    }\n}\n\nint32_t Room_FindGridShift(int32_t src, const int32_t dst)\n{\n    const int32_t src_w = src >> WALL_SHIFT;\n    const int32_t dst_w = dst >> WALL_SHIFT;\n    if (src_w == dst_w) {\n        return 0;\n    }\n\n    src &= WALL_L - 1;\n    if (dst_w > src_w) {\n        return WALL_L - (src - 1);\n    } else {\n        return -(src + 1);\n    }\n}\n\nbool Room_IsOnWalkable(\n    const SECTOR *sector, const XYZ_32 pos, const int32_t room_height,\n    const int16_t ignore_item_num)\n{\n    sector = Room_GetPitSector(sector, pos.x, pos.z);\n\n    int32_t height = sector->floor.height;\n    bool object_found = false;\n    for (WALKABLE *w = sector->walkable; w != nullptr; w = w->next) {\n        // Optionally ignore a walkable.\n        if (w->item_num == ignore_item_num) {\n            continue;\n        }\n        const ITEM *const item = Item_Get(w->item_num);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->floor_height_func != nullptr) {\n            const int32_t test_height =\n                obj->floor_height_func(item, pos.x, pos.y, pos.z, height);\n            // If the floor height changed, try to climb the walkable stack.\n            if (test_height != height) {\n                // Check if height changed aka actually on a walkable.\n                height = test_height;\n                object_found = true;\n            }\n        }\n    }\n\n    return object_found && room_height == height;\n}\n"
  },
  {
    "path": "src/trx/game/rooms/geometry.h",
    "content": "#pragma once\n\n#include <trx/game/rooms/types.h>\n\nBOUNDS_32 Room_GetRoomBounds(const ROOM *room);\nSECTOR *Room_GetSector(XYZ_32 pos, int16_t *room_num);\nSECTOR *Room_GetSectorOnWalkable(XYZ_32 pos, int16_t *room_num);\nSECTOR *Room_GetWorldSector(const ROOM *room, int32_t x_pos, int32_t z_pos);\nSECTOR *Room_GetUnitSector(\n    const ROOM *room, int32_t x_sector, int32_t z_sector);\nSECTOR *Room_GetPitSector(const SECTOR *sector, int32_t x, int32_t z);\nSECTOR *Room_GetSkySector(const SECTOR *sector, int32_t x, int32_t z);\n\nvoid Room_SetAbyssHeight(int16_t height);\nbool Room_IsAbyssHeight(int32_t height);\n\nHEIGHT_TYPE Room_GetHeightType(void);\nXZ_16 Room_GetTiltType(const SECTOR *sector, XYZ_32 pos);\n\nint32_t Room_GetHeight(const SECTOR *sector, XYZ_32 pos);\nint32_t Room_GetHeightEx(\n    const SECTOR *sector, XYZ_32 pos, bool fix_tilts, int16_t ignore_item_num);\nint32_t Room_GetCeiling(const SECTOR *sector, XYZ_32 pos);\nint32_t Room_GetCeilingEx(const SECTOR *sector, XYZ_32 pos, bool fix_tilts);\nint32_t Room_GetFloorHeightForSector(\n    const SECTOR *sector, int32_t x, int32_t z, bool fix_tilts);\n\nint32_t Room_GetWaterHeight(XYZ_32 pos, int16_t room_num);\nint32_t Room_GetWaterHeightEx(XYZ_32 pos, int16_t room_num, bool fix_tilts);\nvoid Room_AlterFloorHeight(const ITEM *item, int32_t height);\n\nint32_t Room_FindGridShift(int32_t src, int32_t dst);\n\nbool Room_IsOnWalkable(\n    const SECTOR *sector, XYZ_32 pos, int32_t room_height,\n    int16_t ignore_item_num);\n"
  },
  {
    "path": "src/trx/game/rooms/types.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/math.h>\n#include <trx/game/items/const.h>\n#include <trx/game/rooms/enum.h>\n#include <trx/game/types.h>\n\n#define ROOM_DRAWSET_WORDS (MAX_ITEMS / 64)\n\ntypedef struct TRIGGER_CMD {\n    TRIGGER_OBJECT type;\n    void *parameter;\n    struct TRIGGER_CMD *next_cmd;\n} TRIGGER_CMD;\n\ntypedef struct {\n    int16_t camera_num;\n    uint8_t timer;\n    uint8_t glide;\n    bool one_shot;\n} TRIGGER_CAMERA_DATA;\n\ntypedef struct {\n    bool enabled;\n    TRIGGER_TYPE type;\n    int8_t timer;\n    int16_t mask;\n    bool one_shot;\n    int16_t item_index;\n    TRIGGER_CMD *command;\n} TRIGGER;\n\ntypedef struct {\n    int16_t room_num;\n    XYZ_16 normal;\n    XYZ_16 vertex[4];\n    BOUNDS_32 bounds;\n} PORTAL;\n\ntypedef struct {\n    uint16_t count;\n    PORTAL portal[];\n} PORTALS;\n\ntypedef struct WALKABLE {\n    int16_t item_num;\n    XYZ_32 pos;\n    struct WALKABLE *next;\n} WALKABLE;\n\ntypedef struct {\n    SPLIT_TYPE type;\n    int16_t tilts[4];\n    int32_t h1;\n    int32_t h2;\n} SPLIT;\n\ntypedef struct {\n    SURFACE_TYPE type;\n    int32_t height;\n    bool is_split;\n    union {\n        XZ_16 tilt;\n        SPLIT split;\n    };\n} SURFACE;\n\ntypedef struct {\n    uint16_t idx;\n    int16_t box;\n    bool is_death_sector;\n    LADDER_DIRECTION ladder;\n    MINE_CART_TYPE mine_cart_type;\n    TRIGGER *trigger;\n    WALKABLE *walkable;\n    struct {\n        int16_t pit;\n        int16_t sky;\n        int16_t wall;\n    } portal_room;\n    SURFACE floor;\n    SURFACE ceiling;\n    uint8_t fx;\n    bool stopper;\n} SECTOR;\n\ntypedef struct {\n    XYZ_32 pos;\n    SHADE shade;\n    FALLOFF falloff;\n    RGB_888 color;\n    uint8_t type; // TR3: 0 = point, != 0 = sun\n    XYZ_16 dir; // TR3: sun direction (type != 0)\n} LIGHT;\n\ntypedef struct {\n    XYZ_16 pos;\n    RGBA_8888 color;\n    int16_t light_base;\n    uint8_t light_table_value;\n    struct {\n        bool disable_wibble;\n        bool move;\n        bool glow;\n    } flags;\n} ROOM_VERTEX;\n\ntypedef struct {\n    uint16_t texture;\n    uint16_t vertex;\n} ROOM_SPRITE;\n\ntypedef struct {\n    int16_t num_vertices;\n    struct {\n        int16_t count;\n        FACE *data;\n    } all_faces, face4s, face3s;\n    struct {\n        int16_t count;\n        ROOM_SPRITE *data;\n    } sprites;\n    ROOM_VERTEX *vertices;\n} ROOM_MESH;\n\ntypedef struct {\n    XYZ_32 pos;\n    struct {\n        int16_t y;\n    } rot;\n    RGBA_8888 color;\n    SHADE shade;\n    int16_t static_num;\n    int16_t draw_num;\n} STATIC_MESH;\n\ntypedef struct {\n    uint64_t bits[ROOM_DRAWSET_WORDS];\n    uint16_t count;\n} ROOM_DRAWSET;\n\ntypedef struct {\n    ROOM_MESH mesh;\n    PORTALS *portals;\n    SECTOR *sectors;\n    LIGHT *lights;\n    STATIC_MESH *static_meshes;\n    XYZ_32 pos;\n    int32_t min_floor;\n    int32_t max_ceiling;\n    struct {\n        int16_t z;\n        int16_t x;\n    } size;\n    int16_t ambient;\n    ROOM_LIGHT_MODE light_mode;\n    int16_t num_lights;\n    int16_t num_static_meshes;\n    int16_t item_num;\n    int16_t effect_num;\n    int16_t flipped_room;\n    ROOM_FLIP_STATUS flip_status;\n    struct {\n        bool underwater;\n        bool outside;\n        bool wind;\n        bool inside;\n        bool dynamic_lit;\n        bool swamp;\n    } flags;\n\n    ROOM_DRAWSET drawn_items;\n    uint8_t water_scheme;\n    uint8_t reverb_info;\n} ROOM;\n"
  },
  {
    "path": "src/trx/game/rooms/utils.h",
    "content": "#pragma once\n\n#include <trx/game/const.h>\n\n#define ROUND_TO_CLICK(V) ((V) & ~(STEP_L - 1))\n#define ROUND_TO_SECTOR(V) ((V) & ~(WALL_L - 1))\n#define ROUND_TO_SECTOR_END(V) ((V) | (WALL_L - 1))\n#define ROUND_TO_CLICK_UP(V) (((V) + STEP_L / 2 - 1) & ~(STEP_L / 2 - 1))\n#define ROUND_TO_HALF_CLICK(V) ((V) & ~(STEP_L / 2 - 1))\n#define ROUND_TO_CLICK_SIGNED(V)                                               \\\n    (((V) >= 0) ? (((V) + STEP_L - 1) & ~(STEP_L - 1)) : ((V) & ~(STEP_L - 1)))\n"
  },
  {
    "path": "src/trx/game/rooms.h",
    "content": "#pragma once\n\n#include <trx/game/rooms/common.h>\n#include <trx/game/rooms/const.h>\n#include <trx/game/rooms/draw.h>\n#include <trx/game/rooms/enum.h>\n#include <trx/game/rooms/floor_data.h>\n#include <trx/game/rooms/geometry.h>\n#include <trx/game/rooms/utils.h>\n"
  },
  {
    "path": "src/trx/game/savegame/common.c",
    "content": "#include <trx/config.h>\n#include <trx/core/benchmark.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/traps/movable_block.h>\n#include <trx/game/pathing/lot.h>\n#include <trx/game/savegame.h>\n#include <trx/game/savegame/file.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\n#include <stdio.h>\n#include <string.h>\n\nstatic SAVEGAME_VERSION m_InitialVersion = SG_VERSION_LEGACY;\nstatic SAVEGAME_INFO *m_NormalSavegameInfo = nullptr;\nstatic SAVEGAME_INFO *m_QuickSavegameInfo = nullptr;\nstatic RESUME_INFO *m_ResumeInfo = nullptr;\nstatic int32_t m_SaveSlots = 0;\nstatic int32_t m_QuickSaveSlots = 0;\nstatic int32_t m_SavedGames = 0;\nstatic int32_t m_SaveCounter = 0;\nstatic int32_t m_NextQuickSlot = 0;\nstatic SAVEGAME_SLOT_REF m_MostRecentlyUsedSlot = { .index = -1 };\nstatic SAVEGAME_SLOT_REF m_MostRecentlyCreatedSlot = { .index = -1 };\nstatic SAVEGAME_SLOT_REF m_BoundSlot = { .index = -1 };\n\nstatic const char *M_GetSaveWriteDir(void)\n{\n    const char *const saves_dir = TRXPath_Get(TRX_PATH_SAVES_DIR);\n    const SHELL_ARGS *const args = Shell_GetArgs();\n    if (args != nullptr && args->mod != nullptr && args->mod->name != nullptr) {\n        return String_FormatStatic(\"%s/%s\", saves_dir, args->mod->name);\n    }\n    return saves_dir;\n}\n\nstatic char *M_GetSaveWritePath(const char *const file_name)\n{\n    ASSERT(file_name != nullptr);\n    return String_Format(\"%s/%s\", M_GetSaveWriteDir(), file_name);\n}\n\nstatic SAVEGAME_INFO *M_GetSavegameInfoSlot(const SAVEGAME_SLOT_REF slot)\n{\n    switch (slot.pool) {\n    case SAVEGAME_SLOT_POOL_NORMAL:\n        if (slot.index >= 0 && slot.index < m_SaveSlots) {\n            return &m_NormalSavegameInfo[slot.index];\n        }\n        break;\n    case SAVEGAME_SLOT_POOL_QUICK:\n        if (slot.index >= 0 && slot.index < m_QuickSaveSlots) {\n            return &m_QuickSavegameInfo[slot.index];\n        }\n        break;\n    case SAVEGAME_SLOT_POOL_NUMBER_OF:\n        break;\n    }\n    return nullptr;\n}\n\nstatic const char *M_GetSaveFilePatternForPool(const SAVEGAME_SLOT_POOL pool)\n{\n    switch (pool) {\n    case SAVEGAME_SLOT_POOL_NORMAL:\n        return SG_File_GetSaveFilePattern();\n    case SAVEGAME_SLOT_POOL_QUICK:\n        return SG_File_GetQuickSaveFilePattern();\n    case SAVEGAME_SLOT_POOL_NUMBER_OF:\n        break;\n    }\n    return nullptr;\n}\n\nstatic void M_CopyResumeInfo(\n    RESUME_INFO *const target, const RESUME_INFO *const source)\n{\n    memcpy(target, source, sizeof(RESUME_INFO));\n}\n\nstatic void M_ClearSlot(SAVEGAME_INFO *const savegame_info)\n{\n    savegame_info->counter = -1;\n    savegame_info->level_num = -1;\n    savegame_info->is_quick = false;\n    Memory_FreePointer(&savegame_info->full_path);\n    Memory_FreePointer(&savegame_info->level_title);\n}\n\nstatic void M_ClearSlots(void)\n{\n    if (m_NormalSavegameInfo != nullptr) {\n        for (int32_t i = 0; i < m_SaveSlots; i++) {\n            M_ClearSlot(&m_NormalSavegameInfo[i]);\n        }\n    }\n    if (m_QuickSavegameInfo != nullptr) {\n        for (int32_t i = 0; i < m_QuickSaveSlots; i++) {\n            M_ClearSlot(&m_QuickSavegameInfo[i]);\n        }\n    }\n}\n\nstatic bool M_FillSlot(const SAVEGAME_SLOT_REF slot, const char *const path)\n{\n    SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(slot);\n    if (savegame_info == nullptr) {\n        return false;\n    }\n    bool result = false;\n    MYFILE *const fp = File_Open(path, FILE_OPEN_READ);\n    if (fp != nullptr) {\n        SAVEGAME_INFO tmp_savegame_info;\n        if (SG_File_FillInfo(fp, &tmp_savegame_info)) {\n            M_ClearSlot(savegame_info);\n            *savegame_info = tmp_savegame_info;\n            savegame_info->is_quick = slot.pool == SAVEGAME_SLOT_POOL_QUICK;\n            savegame_info->full_path = Memory_DupStr(path);\n            result = true;\n        }\n        File_Close(fp);\n    }\n    return result;\n}\n\nstatic void M_ScanSavedGamesDir(const char *const dir_path)\n{\n    void *const dir_handle = File_OpenDirectory(dir_path);\n    if (dir_handle == nullptr) {\n        return;\n    }\n\n    while (true) {\n        const char *const file_name = File_ReadDirectory(dir_handle);\n        if (file_name == nullptr) {\n            break;\n        }\n        if (strcmp(file_name, \".\") == 0 || strcmp(file_name, \"..\") == 0) {\n            continue;\n        }\n\n        char *file_name_ci = String_ToUpper(file_name);\n        for (SAVEGAME_SLOT_POOL pool = 0; pool < SAVEGAME_SLOT_POOL_NUMBER_OF;\n             pool++) {\n            const char *const pattern = M_GetSaveFilePatternForPool(pool);\n            char *pattern_ci = String_ToUpperPattern(pattern);\n            int32_t slot_idx = -1;\n            const int32_t parsed = sscanf(file_name_ci, pattern_ci, &slot_idx);\n            Memory_FreePointer(&pattern_ci);\n\n            if (parsed != 1 || slot_idx < 0\n                || slot_idx >= Savegame_GetSlotCount(pool)) {\n                continue;\n            }\n\n            char *file_path = String_Format(\"%s/%s\", dir_path, file_name);\n            M_FillSlot(\n                (SAVEGAME_SLOT_REF) { .pool = pool, .index = slot_idx },\n                file_path);\n            Memory_FreePointer(&file_path);\n            break;\n        }\n        Memory_FreePointer(&file_name_ci);\n    }\n\n    File_CloseDirectory(dir_handle);\n}\n\nstatic void M_LoadPreprocess(void)\n{\n    Savegame_InitCurrentInfo();\n}\n\nstatic void M_LoadPostprocess(void)\n{\n    // TODO: tidy this; skidoo drivers currently require handle_save_func to be\n    // called immediately on load within the strategies.\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        const OBJECT *const obj = Object_Get(item->object_id);\n\n        if (obj->save_position && (obj->shadow_size != 0 || obj->load_floor)) {\n            int16_t room_num = item->room_num;\n            const SECTOR *const sector = Room_GetSector(item->pos, &room_num);\n            item->floor = Room_GetHeight(sector, item->pos);\n        }\n\n        // TODO: make this engine-agnostic\n        if (g_TRVersion == 1 && obj->handle_save_func != nullptr) {\n            obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD);\n        }\n    }\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    if (Game_GetBonusFlag() != GBF_NONE) {\n        g_Config.profile.new_game_plus_unlock = true;\n        Config_Update();\n    }\n    if (lara->burn && !g_Config.gameplay.enable_enhanced_saves) {\n        lara->burn = false;\n        Lara_CatchFire();\n    }\n}\n\nstatic void M_DetermineLegacyGunTypes(RESUME_INFO *const resume)\n{\n    // Fallback logic to figure out holster and back gun items for saves from\n    // TR1X 4.2 and earlier (including TombATI) and TR2X 1.2 and earlier, where\n    // these values are missing. Make educated guesses based on the type of gun\n    // equipped.\n    if (resume->holsters_gun_type == LGT_UNKNOWN) {\n        switch (resume->equipped_gun_type) {\n        case LGT_PISTOLS:\n        case LGT_MAGNUMS:\n        case LGT_AUTOS:\n        case LGT_DESERT_EAGLE:\n        case LGT_UZIS:\n            resume->holsters_gun_type = resume->equipped_gun_type;\n            break;\n        case LGT_SHOTGUN:\n        case LGT_M16:\n        case LGT_MP5:\n        case LGT_GRENADE:\n        case LGT_ROCKET:\n        case LGT_HARPOON:\n            if (resume->flags.has_pistols) {\n                resume->holsters_gun_type = LGT_PISTOLS;\n            } else if (resume->flags.has_magnums) {\n                resume->holsters_gun_type = LGT_MAGNUMS;\n            } else if (resume->flags.has_autos) {\n                resume->holsters_gun_type = LGT_AUTOS;\n            } else if (resume->flags.has_desert_eagle) {\n                resume->holsters_gun_type = LGT_DESERT_EAGLE;\n            } else if (resume->flags.has_uzis) {\n                resume->holsters_gun_type = LGT_UZIS;\n            } else {\n                resume->holsters_gun_type = LGT_UNARMED;\n            }\n            break;\n        default:\n            resume->holsters_gun_type = LGT_UNARMED;\n            break;\n        }\n    }\n    if (resume->back_gun_type == LGT_UNKNOWN) {\n        resume->back_gun_type = LGT_UNARMED;\n        if (resume->flags.has_shotgun) {\n            resume->back_gun_type = LGT_SHOTGUN;\n        } else if (resume->flags.has_m16) {\n            resume->back_gun_type = LGT_M16;\n        } else if (resume->flags.has_mp5) {\n            resume->back_gun_type = LGT_MP5;\n        } else if (resume->flags.has_grenade) {\n            resume->back_gun_type = LGT_GRENADE;\n        } else if (resume->flags.has_rocket) {\n            resume->back_gun_type = LGT_ROCKET;\n        } else if (resume->flags.has_harpoon) {\n            resume->back_gun_type = LGT_HARPOON;\n        }\n    }\n}\n\nSAVEGAME_VERSION Savegame_GetInitialVersion(void)\n{\n    return m_InitialVersion;\n}\n\nvoid Savegame_SetInitialVersion(const SAVEGAME_VERSION version)\n{\n    m_InitialVersion = version;\n}\n\nvoid Savegame_BindSlot(const SAVEGAME_SLOT_REF slot)\n{\n    if (!Savegame_IsValidSlotRef(slot)) {\n        m_BoundSlot = Savegame_InvalidSlot();\n        return;\n    }\n    m_BoundSlot = slot;\n    m_MostRecentlyUsedSlot = slot;\n    LOG_DEBUG(\"Binding save slot %d:%d\", slot.pool, slot.index);\n}\n\nSAVEGAME_SLOT_REF Savegame_GetMostRecentlyUsedSlot(void)\n{\n    return m_MostRecentlyUsedSlot;\n}\n\nvoid Savegame_UnbindSlot(void)\n{\n    LOG_DEBUG(\"Resetting the save slot\");\n    m_BoundSlot = Savegame_InvalidSlot();\n}\n\nSAVEGAME_SLOT_REF Savegame_GetBoundSlot(void)\n{\n    return m_BoundSlot;\n}\n\nint32_t Savegame_GetLevelNumber(const SAVEGAME_SLOT_REF slot)\n{\n    const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot);\n    return info != nullptr ? info->level_num : -1;\n}\n\nbool Savegame_IsSlotFree(const SAVEGAME_SLOT_REF slot)\n{\n    const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot);\n    return info == nullptr || info->level_num == -1;\n}\n\nint32_t Savegame_GetCounter(void)\n{\n    return m_SaveCounter;\n}\n\nint32_t Savegame_GetTotalCount(void)\n{\n    return m_SavedGames;\n}\n\nSAVEGAME_SLOT_REF Savegame_GetMostRecentlyCreatedSlot(void)\n{\n    return m_MostRecentlyCreatedSlot;\n}\n\nSAVEGAME_SLOT_REF Savegame_NormalSlot(const int32_t index)\n{\n    return (SAVEGAME_SLOT_REF) {\n        .pool = SAVEGAME_SLOT_POOL_NORMAL,\n        .index = index,\n    };\n}\n\nSAVEGAME_SLOT_REF Savegame_QuickSlot(const int32_t index)\n{\n    return (SAVEGAME_SLOT_REF) {\n        .pool = SAVEGAME_SLOT_POOL_QUICK,\n        .index = index,\n    };\n}\n\nSAVEGAME_SLOT_REF Savegame_InvalidSlot(void)\n{\n    return (SAVEGAME_SLOT_REF) {\n        .pool = SAVEGAME_SLOT_POOL_NORMAL,\n        .index = -1,\n    };\n}\n\nbool Savegame_IsValidSlotRef(const SAVEGAME_SLOT_REF slot)\n{\n    return slot.pool >= SAVEGAME_SLOT_POOL_NORMAL\n        && slot.pool < SAVEGAME_SLOT_POOL_NUMBER_OF && slot.index >= 0\n        && slot.index < Savegame_GetSlotCount(slot.pool);\n}\n\nint32_t Savegame_SlotToParam(const SAVEGAME_SLOT_REF slot)\n{\n    if (!Savegame_IsValidSlotRef(slot)) {\n        return -1;\n    }\n    const uint32_t packed = ((uint32_t)slot.pool << 31) | (uint32_t)slot.index;\n    return (int32_t)packed;\n}\n\nSAVEGAME_SLOT_REF Savegame_SlotFromParam(const int32_t param)\n{\n    if (param == -1) {\n        return Savegame_InvalidSlot();\n    }\n\n    const uint32_t packed = (uint32_t)param;\n    const SAVEGAME_SLOT_POOL pool = (packed >> 31) & 1;\n    const int32_t index = (int32_t)(packed & 0x7FFFFFFF);\n    return (SAVEGAME_SLOT_REF) {\n        .pool = pool,\n        .index = index,\n    };\n}\n\nvoid Savegame_Init(void)\n{\n    m_ResumeInfo = Memory_Alloc(\n        sizeof(RESUME_INFO)\n        * (GF_GetLevelTable(GFLT_MAIN)->count\n           + GF_GetLevelTable(GFLT_DEMOS)->count));\n\n    m_SaveSlots = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL);\n    m_QuickSaveSlots = Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK);\n    m_NormalSavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots);\n    m_QuickSavegameInfo = m_QuickSaveSlots > 0\n        ? Memory_Alloc(sizeof(SAVEGAME_INFO) * m_QuickSaveSlots)\n        : nullptr;\n\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        RESUME_INFO *const resume_info =\n            Savegame_GetCurrentInfo(&level_table->levels[i]);\n        resume_info->lara_hitpoints = LARA_MAX_HITPOINTS;\n        resume_info->flags.available = true;\n        resume_info->flags.has_pistols = true;\n        resume_info->pistol_ammo = 1000;\n        resume_info->gun_status = LGS_ARMLESS;\n        resume_info->equipped_gun_type = LGT_PISTOLS;\n        resume_info->holsters_gun_type = LGT_PISTOLS;\n        resume_info->back_gun_type = LGT_UNARMED;\n        resume_info->prev_level = -1;\n    }\n}\n\nbool Savegame_IsInitialised(void)\n{\n    return m_NormalSavegameInfo != nullptr;\n}\n\nvoid Savegame_Shutdown(void)\n{\n    M_ClearSlots();\n    Memory_FreePointer(&m_ResumeInfo);\n    Memory_FreePointer(&m_NormalSavegameInfo);\n    Memory_FreePointer(&m_QuickSavegameInfo);\n}\n\nint32_t Savegame_GetSlotCount(const SAVEGAME_SLOT_POOL pool)\n{\n    switch (pool) {\n    case SAVEGAME_SLOT_POOL_NORMAL:\n        return g_Config.gameplay.maximum_save_slots;\n    case SAVEGAME_SLOT_POOL_QUICK:\n        return g_Config.gameplay.maximum_quick_save_slots;\n    case SAVEGAME_SLOT_POOL_NUMBER_OF:\n        break;\n    }\n    return 0;\n}\n\nSAVEGAME_SLOT_REF Savegame_GetNextQuickSlot(void)\n{\n    if (m_QuickSaveSlots <= 0) {\n        return Savegame_InvalidSlot();\n    }\n    if (m_NextQuickSlot < 0 || m_NextQuickSlot >= m_QuickSaveSlots) {\n        m_NextQuickSlot = 0;\n    }\n    return Savegame_QuickSlot(m_NextQuickSlot);\n}\n\nstatic bool M_IsQuickSlotSortedBefore(\n    const SAVEGAME_SLOT_REF left, const SAVEGAME_SLOT_REF right)\n{\n    const SAVEGAME_INFO *const left_info = Savegame_GetSavegameInfo(left);\n    const SAVEGAME_INFO *const right_info = Savegame_GetSavegameInfo(right);\n    if (left_info == nullptr || right_info == nullptr) {\n        return false;\n    }\n    if (left_info->counter != right_info->counter) {\n        return left_info->counter > right_info->counter;\n    }\n    return left.index < right.index;\n}\n\nint32_t Savegame_GetQuickVisualCount(void)\n{\n    int32_t count = 0;\n    const int32_t quick_slot_count =\n        Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK);\n    for (int32_t i = 0; i < quick_slot_count; i++) {\n        if (!Savegame_IsSlotFree(Savegame_QuickSlot(i))) {\n            count++;\n        }\n    }\n    return count;\n}\n\nSAVEGAME_SLOT_REF Savegame_QuickFromVisualIndex(const int32_t visual_index)\n{\n    if (visual_index < 0) {\n        return Savegame_InvalidSlot();\n    }\n\n    const int32_t quick_slot_count =\n        Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK);\n    for (int32_t i = 0; i < quick_slot_count; i++) {\n        const SAVEGAME_SLOT_REF candidate = Savegame_QuickSlot(i);\n        if (Savegame_IsSlotFree(candidate)) {\n            continue;\n        }\n\n        int32_t better_count = 0;\n        for (int32_t j = 0; j < quick_slot_count; j++) {\n            const SAVEGAME_SLOT_REF other = Savegame_QuickSlot(j);\n            if (Savegame_IsSlotFree(other)) {\n                continue;\n            }\n            if (M_IsQuickSlotSortedBefore(other, candidate)) {\n                better_count++;\n            }\n        }\n\n        if (better_count == visual_index) {\n            return candidate;\n        }\n    }\n\n    return Savegame_InvalidSlot();\n}\n\nint32_t Savegame_QuickToVisualIndex(const SAVEGAME_SLOT_REF slot)\n{\n    if (!Savegame_IsValidSlotRef(slot) || slot.pool != SAVEGAME_SLOT_POOL_QUICK\n        || Savegame_IsSlotFree(slot)) {\n        return -1;\n    }\n\n    const int32_t quick_slot_count =\n        Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK);\n    int32_t better_count = 0;\n    for (int32_t i = 0; i < quick_slot_count; i++) {\n        const SAVEGAME_SLOT_REF other = Savegame_QuickSlot(i);\n        if (Savegame_IsSlotFree(other)) {\n            continue;\n        }\n        if (M_IsQuickSlotSortedBefore(other, slot)) {\n            better_count++;\n        }\n    }\n    return better_count;\n}\n\nRESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level)\n{\n    ASSERT(m_ResumeInfo != nullptr);\n    if (level == nullptr) {\n        return nullptr;\n    }\n    if (GF_GetLevelTableType(level->type) == GFLT_MAIN) {\n        return &m_ResumeInfo[level->num];\n    } else if (level->type == GFL_DEMO) {\n        return &m_ResumeInfo[GF_GetLevelTable(GFLT_MAIN)->count];\n    } else if (level->type == GFL_CUTSCENE || level->type == GFL_TITLE) {\n        return nullptr;\n    }\n    LOG_WARNING(\n        \"Warning: unable to get resume info for level %d (type=%s)\", level->num,\n        ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type));\n    return nullptr;\n}\n\nvoid Savegame_SetCurrentInfo(const int32_t current_slot, const int32_t src_slot)\n{\n    m_ResumeInfo[current_slot] = m_ResumeInfo[src_slot];\n}\n\nconst SAVEGAME_INFO *Savegame_GetSavegameInfo(const SAVEGAME_SLOT_REF slot)\n{\n    return M_GetSavegameInfoSlot(slot);\n}\n\nvoid Savegame_InitCurrentInfo(void)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        Savegame_ResetCurrentInfo(level);\n        Savegame_ApplyLogicToCurrentInfo(level);\n        RESUME_INFO *const current = Savegame_GetCurrentInfo(level);\n        current->level_completed = false;\n        current->flags.available = false;\n    }\n\n    if (GF_GetGymLevel() != nullptr) {\n        Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = true;\n    }\n    if (GF_GetFirstLevel() != nullptr) {\n        Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = true;\n    }\n}\n\nvoid Savegame_ResetCurrentInfo(const GF_LEVEL *const level)\n{\n    LOG_INFO(\"Resetting resume info for level #%d\", level->num);\n    RESUME_INFO *const current = Savegame_GetCurrentInfo(level);\n    *current = (RESUME_INFO) { .prev_level = -1, .level_completed = false };\n}\n\nvoid Savegame_CarryCurrentInfoToNextLevel(\n    const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level)\n{\n    LOG_INFO(\n        \"Copying resume info from level #%d to level #%d\", src_level->num,\n        dst_level->num);\n    RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level);\n    RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level);\n    if (src_resume != nullptr && dst_resume != nullptr) {\n        const bool dst_level_completed = dst_resume->level_completed;\n        M_CopyResumeInfo(dst_resume, src_resume);\n        dst_resume->level_completed = dst_level_completed;\n        dst_resume->prev_level = src_level->num;\n    }\n}\n\nvoid Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (resume == nullptr) {\n        return;\n    }\n\n    resume->flags.available = true;\n\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    const ITEM *const lara_item = Lara_GetItem();\n\n    if (lara_item != nullptr) {\n        resume->lara_hitpoints = lara_item->hit_points;\n    }\n    resume->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM);\n    resume->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM);\n\n    resume->pistol_ammo = 1000;\n    if (Inv_RequestItem(O_PISTOL_ITEM)) {\n        resume->flags.has_pistols = true;\n    } else {\n        resume->flags.has_pistols = false;\n    }\n\n    if (Inv_RequestItem(O_SHOTGUN_ITEM)) {\n        resume->flags.has_shotgun = true;\n        resume->shotgun_ammo = lara->shotgun_ammo.ammo;\n    } else {\n        resume->flags.has_shotgun = false;\n        resume->shotgun_ammo = Inv_RequestItem(O_SHOTGUN_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_SHOTGUN);\n    }\n\n    if (Inv_RequestItem(O_MAGNUM_ITEM)) {\n        resume->flags.has_magnums = true;\n        resume->magnum_ammo = lara->magnum_ammo.ammo;\n    } else {\n        resume->flags.has_magnums = false;\n        resume->magnum_ammo = Inv_RequestItem(O_MAGNUM_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_MAGNUMS);\n    }\n\n    if (Inv_RequestItem(O_AUTOS_ITEM)) {\n        resume->flags.has_autos = true;\n        resume->autos_ammo = lara->autos_ammo.ammo;\n    } else {\n        resume->flags.has_autos = false;\n        resume->autos_ammo = Inv_RequestItem(O_AUTOS_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_AUTOS);\n    }\n\n    if (Inv_RequestItem(O_DESERT_EAGLE_ITEM)) {\n        resume->flags.has_desert_eagle = true;\n        resume->desert_eagle_ammo = lara->desert_eagle_ammo.ammo;\n    } else {\n        resume->flags.has_desert_eagle = false;\n        resume->desert_eagle_ammo = Inv_RequestItem(O_DESERT_EAGLE_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_DESERT_EAGLE);\n    }\n\n    if (Inv_RequestItem(O_UZI_ITEM)) {\n        resume->flags.has_uzis = true;\n        resume->uzi_ammo = lara->uzi_ammo.ammo;\n    } else {\n        resume->flags.has_uzis = false;\n        resume->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_UZIS);\n    }\n\n    resume->flares = Inv_RequestItem(O_FLARE_ITEM);\n    resume->num_scions = Inv_RequestItem(O_SCION_ITEM_1);\n    resume->num_quest_item_1 = Inv_RequestItem(O_QUEST_ITEM_1);\n    resume->num_quest_item_2 = Inv_RequestItem(O_QUEST_ITEM_2);\n    resume->num_quest_item_3 = Inv_RequestItem(O_QUEST_ITEM_3);\n    resume->num_quest_item_4 = Inv_RequestItem(O_QUEST_ITEM_4);\n\n    if (Inv_RequestItem(O_M16_ITEM)) {\n        resume->flags.has_m16 = true;\n        resume->m16_ammo = lara->m16_ammo.ammo;\n    } else {\n        resume->flags.has_m16 = false;\n        resume->m16_ammo = Inv_RequestItem(O_M16_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_M16);\n    }\n\n    if (Inv_RequestItem(O_MP5_ITEM)) {\n        resume->flags.has_mp5 = true;\n        resume->mp5_ammo = lara->mp5_ammo.ammo;\n    } else {\n        resume->flags.has_mp5 = false;\n        resume->mp5_ammo = Inv_RequestItem(O_MP5_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_MP5);\n    }\n\n    if (Inv_RequestItem(O_HARPOON_ITEM)) {\n        resume->flags.has_harpoon = true;\n        resume->harpoon_ammo = lara->harpoon_ammo.ammo;\n    } else {\n        resume->flags.has_harpoon = false;\n        resume->harpoon_ammo = Inv_RequestItem(O_HARPOON_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_HARPOON);\n    }\n\n    if (Inv_RequestItem(O_GRENADE_GUN_ITEM)) {\n        resume->flags.has_grenade = true;\n        resume->grenade_ammo = lara->grenade_ammo.ammo;\n    } else {\n        resume->flags.has_grenade = false;\n        resume->grenade_ammo = Inv_RequestItem(O_GRENADE_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_GRENADE);\n    }\n\n    if (Inv_RequestItem(O_ROCKET_GUN_ITEM)) {\n        resume->flags.has_rocket = true;\n        resume->rocket_ammo = lara->rocket_ammo.ammo;\n    } else {\n        resume->flags.has_rocket = false;\n        resume->rocket_ammo = Inv_RequestItem(O_ROCKET_AMMO_ITEM)\n            * Gun_GetAmmoPickupQuantity(LGT_ROCKET);\n    }\n\n    resume->equipped_gun_type = lara->last_gun_type;\n    resume->holsters_gun_type = lara->holsters_gun_type;\n    resume->back_gun_type = lara->back_gun_type;\n    if (resume->back_gun_type == LGT_UNARMED\n        && Gun_IsRifleType(resume->equipped_gun_type)\n        && Inv_RequestItem(Gun_GetGunObject(resume->equipped_gun_type)) != 0) {\n        // If a rifle is currently drawn, Lara's back mesh is temporarily\n        // unarmed. Preserve the preferred rifle for next-level mesh restore.\n        resume->back_gun_type = resume->equipped_gun_type;\n    }\n    if (lara->gun_status == LGS_READY) {\n        resume->gun_status = LGS_READY;\n    } else {\n        resume->gun_status = LGS_ARMLESS;\n    }\n}\n\nvoid Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (resume == nullptr) {\n        return;\n    }\n\n    LOG_INFO(\"Applying game logic to level #%d\", level->num);\n\n    if (!g_Config.gameplay.disable_healing_between_levels\n        || level == GF_GetGymLevel() || level == GF_GetFirstLevel()) {\n        resume->lara_hitpoints = g_Config.gameplay.start_lara_hitpoints;\n    }\n\n    if (level == GF_GetGymLevel()) {\n        resume->flags.available = true;\n        resume->flags.costume = g_TRVersion == 1;\n\n        resume->flags.has_pistols = false;\n        resume->flags.has_shotgun = false;\n        resume->flags.has_magnums = false;\n        resume->flags.has_autos = false;\n        resume->flags.has_desert_eagle = false;\n        resume->flags.has_uzis = false;\n        resume->flags.has_harpoon = false;\n        resume->flags.has_m16 = false;\n        resume->flags.has_mp5 = false;\n        resume->flags.has_grenade = false;\n        resume->flags.has_rocket = false;\n\n        resume->pistol_ammo = 0;\n        resume->shotgun_ammo = 0;\n        resume->magnum_ammo = 0;\n        resume->autos_ammo = 0;\n        resume->desert_eagle_ammo = 0;\n        resume->uzi_ammo = 0;\n        resume->harpoon_ammo = 0;\n        resume->m16_ammo = 0;\n        resume->mp5_ammo = 0;\n        resume->grenade_ammo = 0;\n        resume->rocket_ammo = 0;\n\n        resume->small_medipacks = 0;\n        resume->large_medipacks = 0;\n        resume->num_scions = 0;\n        resume->num_quest_item_1 = 0;\n        resume->num_quest_item_2 = 0;\n        resume->num_quest_item_3 = 0;\n        resume->num_quest_item_4 = 0;\n        resume->flares = 0;\n\n        resume->equipped_gun_type = LGT_UNARMED;\n        resume->holsters_gun_type = LGT_UNARMED;\n        resume->back_gun_type = LGT_UNARMED;\n        resume->gun_status = LGS_ARMLESS;\n    }\n\n    if (level == GF_GetFirstLevel()) {\n        resume->flags.available = true;\n        resume->flags.costume = false;\n\n        resume->flags.has_pistols = true;\n        resume->flags.has_shotgun = false;\n        resume->flags.has_magnums = false;\n        resume->flags.has_autos = false;\n        resume->flags.has_desert_eagle = false;\n        resume->flags.has_uzis = false;\n\n        resume->small_medipacks = 0;\n        resume->large_medipacks = 0;\n        resume->flares = 0;\n        resume->pistol_ammo = 1000;\n        resume->shotgun_ammo = 0;\n        resume->magnum_ammo = 0;\n        resume->autos_ammo = 0;\n        resume->desert_eagle_ammo = 0;\n        resume->uzi_ammo = 0;\n        resume->num_scions = 0;\n        resume->num_quest_item_1 = 0;\n        resume->num_quest_item_2 = 0;\n        resume->num_quest_item_3 = 0;\n        resume->num_quest_item_4 = 0;\n        resume->flags.has_harpoon = false;\n        resume->flags.has_m16 = false;\n        resume->flags.has_mp5 = false;\n        resume->flags.has_grenade = false;\n        resume->flags.has_rocket = false;\n        resume->harpoon_ammo = 0;\n        resume->m16_ammo = 0;\n        resume->mp5_ammo = 0;\n        resume->grenade_ammo = 0;\n        resume->rocket_ammo = 0;\n        resume->equipped_gun_type = LGT_PISTOLS;\n        resume->holsters_gun_type = LGT_PISTOLS;\n        resume->back_gun_type = LGT_UNARMED;\n        resume->gun_status = LGS_ARMLESS;\n    }\n\n    if (Game_IsBonusFlagSet(GBF_NGPLUS) && level != GF_GetGymLevel()) {\n        resume->flags.has_pistols = true;\n        resume->flags.has_shotgun = true;\n        resume->flags.has_magnums = g_Weapons[LGT_MAGNUMS].is_available;\n        resume->flags.has_autos = g_Weapons[LGT_AUTOS].is_available;\n        resume->flags.has_desert_eagle =\n            g_Weapons[LGT_DESERT_EAGLE].is_available;\n        resume->flags.has_uzis = true;\n        resume->flags.has_m16 = g_Weapons[LGT_M16].is_available;\n        resume->flags.has_mp5 = g_Weapons[LGT_MP5].is_available;\n        resume->flags.has_grenade = g_Weapons[LGT_GRENADE].is_available;\n        resume->flags.has_rocket = g_Weapons[LGT_ROCKET].is_available;\n        resume->flags.has_harpoon = g_Weapons[LGT_HARPOON].is_available;\n\n        resume->shotgun_ammo = 10000;\n        resume->magnum_ammo = resume->flags.has_magnums ? 10000 : 0;\n        resume->autos_ammo = resume->flags.has_autos ? 10000 : 0;\n        resume->desert_eagle_ammo = resume->flags.has_desert_eagle ? 10000 : 0;\n        resume->uzi_ammo = 10000;\n        resume->flares = g_TRVersion == 1 ? 0 : -1;\n\n        resume->m16_ammo = resume->flags.has_m16 ? 10000 : 0;\n        resume->mp5_ammo = resume->flags.has_mp5 ? 10000 : 0;\n        resume->grenade_ammo = resume->flags.has_grenade ? 10000 : 0;\n        resume->rocket_ammo = resume->flags.has_rocket ? 10000 : 0;\n        resume->harpoon_ammo = resume->flags.has_harpoon ? 10000 : 0;\n\n        const bool should_force_ngplus_gun_setup =\n            !g_Config.gameplay.remember_gun_status || resume->prev_level == -1;\n        if (should_force_ngplus_gun_setup) {\n            switch (g_TRVersion) {\n            case 1:\n                resume->equipped_gun_type = LGT_UZIS;\n                resume->back_gun_type = LGT_SHOTGUN;\n                resume->holsters_gun_type = LGT_UZIS;\n                break;\n            case 2:\n                resume->equipped_gun_type = LGT_GRENADE;\n                resume->back_gun_type = LGT_GRENADE;\n                resume->holsters_gun_type = LGT_PISTOLS;\n                break;\n            case 3:\n            default:\n                resume->equipped_gun_type = LGT_ROCKET;\n                resume->back_gun_type = LGT_ROCKET;\n                resume->holsters_gun_type = LGT_PISTOLS;\n                break;\n            }\n        }\n    }\n\n    resume->stats.secret_flags = 0;\n\n    M_DetermineLegacyGunTypes(resume);\n}\n\nvoid Savegame_ProcessItemsBeforeSave(void)\n{\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->handle_save_func != nullptr) {\n            obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE);\n        }\n    }\n}\n\nvoid Savegame_ProcessItemsBeforeLoad(void)\n{\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        const OBJECT *const obj = Object_Get(item->object_id);\n        if (obj->handle_save_func != nullptr) {\n            obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD);\n        }\n    }\n}\n\nvoid Savegame_ScanSavedGames(void)\n{\n    BENCHMARK benchmark = Benchmark_Start();\n    M_ClearSlots();\n\n    m_SaveCounter = 0;\n    m_SavedGames = 0;\n    m_MostRecentlyCreatedSlot = Savegame_InvalidSlot();\n    m_NextQuickSlot = 0;\n    int32_t newest_quick_counter = -1;\n    int32_t newest_quick_slot = -1;\n\n    // Scan low-priority locations first; the write directory is authoritative.\n    M_ScanSavedGamesDir(\".\");\n    M_ScanSavedGamesDir(TRXPath_Get(TRX_PATH_LEGACY_SAVES_DIR));\n    M_ScanSavedGamesDir(TRXPath_Get(TRX_PATH_SAVES_DIR));\n\n    {\n        // M_GetSaveWriteDir may use static formatting storage, so copy it\n        // before scanning because nested formatting calls during scan can\n        // overwrite it.\n        AUTO_FREE char *write_dir = Memory_DupStr(M_GetSaveWriteDir());\n        M_ScanSavedGamesDir(write_dir);\n    }\n\n    for (SAVEGAME_SLOT_POOL pool = 0; pool < SAVEGAME_SLOT_POOL_NUMBER_OF;\n         pool++) {\n        for (int32_t i = 0; i < Savegame_GetSlotCount(pool); i++) {\n            SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(\n                (SAVEGAME_SLOT_REF) { .pool = pool, .index = i });\n            if (savegame_info->level_title == nullptr) {\n                continue;\n            }\n            if (savegame_info->counter > m_SaveCounter) {\n                m_SaveCounter = savegame_info->counter;\n                m_MostRecentlyCreatedSlot =\n                    (SAVEGAME_SLOT_REF) { .pool = pool, .index = i };\n            }\n            m_SavedGames++;\n\n            if (pool == SAVEGAME_SLOT_POOL_QUICK\n                && savegame_info->counter > newest_quick_counter) {\n                newest_quick_counter = savegame_info->counter;\n                newest_quick_slot = i;\n            }\n        }\n    }\n\n    if (m_QuickSaveSlots > 0 && newest_quick_slot >= 0) {\n        m_NextQuickSlot = (newest_quick_slot + 1) % m_QuickSaveSlots;\n    }\n\n    Benchmark_End(&benchmark, nullptr);\n}\n\nbool Savegame_Save(const SAVEGAME_SLOT_REF slot)\n{\n    if (!Savegame_IsValidSlotRef(slot)) {\n        return false;\n    }\n\n    bool result = false;\n    Savegame_BindSlot(slot);\n\n    const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n\n    Savegame_PersistGameToCurrentInfo(current_level);\n\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (level->type == GFL_CURRENT) {\n            Savegame_SetCurrentInfo(i, current_level->num);\n        }\n    }\n\n    SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(slot);\n    const bool was_slot_empty = savegame_info->full_path == nullptr;\n\n    m_SaveCounter++;\n    const char *const save_pattern = M_GetSaveFilePatternForPool(slot.pool);\n    char *file_name = String_Format(save_pattern, slot.index);\n    char *full_path = M_GetSaveWritePath(file_name);\n    File_EnsureParentDirectories(full_path);\n    MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE);\n    if (fp != nullptr) {\n        savegame_info->is_quick = slot.pool == SAVEGAME_SLOT_POOL_QUICK;\n        SG_File_SaveToFile(fp, savegame_info);\n        File_Close(fp);\n        result = true;\n    }\n    if (result) {\n        M_FillSlot(slot, full_path);\n    }\n\n    Memory_FreePointer(&file_name);\n    Memory_FreePointer(&full_path);\n\n    if (result) {\n        m_MostRecentlyCreatedSlot = slot;\n        if (was_slot_empty) {\n            m_SavedGames++;\n        }\n\n        if (slot.pool == SAVEGAME_SLOT_POOL_QUICK && m_QuickSaveSlots > 0) {\n            m_NextQuickSlot = (slot.index + 1) % m_QuickSaveSlots;\n        }\n    } else {\n        m_SaveCounter--;\n    }\n\n    return result;\n}\n\nbool Savegame_Delete(const SAVEGAME_SLOT_REF slot)\n{\n    if (!Savegame_IsValidSlotRef(slot) || Savegame_IsSlotFree(slot)) {\n        return false;\n    }\n\n    SAVEGAME_INFO *const savegame_info = M_GetSavegameInfoSlot(slot);\n    if (savegame_info == nullptr || savegame_info->full_path == nullptr) {\n        return false;\n    }\n\n    const bool result = remove(savegame_info->full_path) == 0;\n    if (!result) {\n        return false;\n    }\n\n    M_ClearSlot(savegame_info);\n    if (m_SavedGames > 0) {\n        m_SavedGames--;\n    }\n    if (m_BoundSlot.pool == slot.pool && m_BoundSlot.index == slot.index) {\n        m_BoundSlot = Savegame_InvalidSlot();\n    }\n    if (m_MostRecentlyUsedSlot.pool == slot.pool\n        && m_MostRecentlyUsedSlot.index == slot.index) {\n        m_MostRecentlyUsedSlot = Savegame_InvalidSlot();\n    }\n    if (m_MostRecentlyCreatedSlot.pool == slot.pool\n        && m_MostRecentlyCreatedSlot.index == slot.index) {\n        m_MostRecentlyCreatedSlot = Savegame_InvalidSlot();\n    }\n    return true;\n}\n\nbool Savegame_Load(const SAVEGAME_SLOT_REF slot)\n{\n    const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot);\n    if (savegame_info == nullptr) {\n        return false;\n    }\n    ASSERT(savegame_info->full_path != nullptr);\n\n    M_LoadPreprocess();\n\n    bool result = false;\n    MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ);\n    if (fp != nullptr) {\n        result = SG_File_LoadFromFile(fp);\n        File_Close(fp);\n    }\n\n    M_LoadPostprocess();\n    m_InitialVersion = savegame_info->initial_version;\n    return result;\n}\n\nbool Savegame_UpdateDeathCounters(\n    const SAVEGAME_SLOT_REF slot, const int32_t death_count)\n{\n    const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot);\n    if (savegame_info == nullptr) {\n        return false;\n    }\n    ASSERT(savegame_info->full_path != nullptr);\n\n    bool ret = false;\n    MYFILE *const fp =\n        File_Open(savegame_info->full_path, FILE_OPEN_READ_WRITE);\n    if (fp != nullptr) {\n        ret = SG_File_UpdateDeathCounters(\n            fp, savegame_info->level_num, death_count, savegame_info->is_quick);\n        File_Close(fp);\n    }\n    return ret;\n}\n\nbool Savegame_LoadOnlyResumeInfo(const SAVEGAME_SLOT_REF slot)\n{\n    const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot);\n    if (savegame_info == nullptr) {\n        return false;\n    }\n    ASSERT(savegame_info->full_path != nullptr);\n\n    bool ret = false;\n    MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ);\n    if (fp != nullptr) {\n        ret = SG_File_LoadOnlyResumeInfo(fp);\n        File_Close(fp);\n    }\n\n    Savegame_SetInitialVersion(savegame_info->initial_version);\n    return ret;\n}\n\nbool Savegame_RestartAvailable(const SAVEGAME_SLOT_REF slot)\n{\n    if (!Savegame_IsValidSlotRef(slot)) {\n        return true;\n    }\n    const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(slot);\n    return savegame_info->features.restart;\n}\n"
  },
  {
    "path": "src/trx/game/savegame/common.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/savegame/types.h>\n\n// Loading a saved game is divided into two phases. First, the game reads the\n// savegame file contents to look for the level number. The rest of the save\n// data is stored in a special buffer. Then the engine continues to execute the\n// normal game flow and loads the specified level. Second phase occurs after\n// everything finishes loading, e.g. items, creatures, triggers etc., and is\n// what actually sets Lara's health, creatures status, triggers, inventory etc.\n\nvoid Savegame_Init(void);\nvoid Savegame_Shutdown(void);\nbool Savegame_IsInitialised(void);\nvoid Savegame_ScanSavedGames(void);\n\nSAVEGAME_VERSION Savegame_GetInitialVersion(void);\nvoid Savegame_SetInitialVersion(SAVEGAME_VERSION version);\nint32_t Savegame_GetCounter(void);\nint32_t Savegame_GetTotalCount(void);\nint32_t Savegame_GetLevelNumber(SAVEGAME_SLOT_REF slot);\nbool Savegame_IsSlotFree(SAVEGAME_SLOT_REF slot);\nbool Savegame_RestartAvailable(SAVEGAME_SLOT_REF slot);\n\n// Remembers the slot used when the player starts a loaded game.\n// Persists across level reloads.\nvoid Savegame_BindSlot(SAVEGAME_SLOT_REF slot);\n\n// Removes the binding of the current slot. Used when the player exits to\n// title, issues a command like `/play` etc.\nvoid Savegame_UnbindSlot(void);\n\n// Returns the currently bound slot. If there is none, returns an invalid slot.\nSAVEGAME_SLOT_REF Savegame_GetBoundSlot(void);\n\n// Returns the most recently created save slot number. If there is none,\n// returns an invalid slot.\nSAVEGAME_SLOT_REF Savegame_GetMostRecentlyCreatedSlot(void);\n\n// Returns the most recently used slot save number. If there is none, returns\n// an invalid slot.\nSAVEGAME_SLOT_REF Savegame_GetMostRecentlyUsedSlot(void);\n\nvoid Savegame_ProcessItemsBeforeLoad(void);\nvoid Savegame_ProcessItemsBeforeSave(void);\nbool Savegame_Load(SAVEGAME_SLOT_REF slot);\nbool Savegame_Save(SAVEGAME_SLOT_REF slot);\nbool Savegame_Delete(SAVEGAME_SLOT_REF slot);\nbool Savegame_UpdateDeathCounters(SAVEGAME_SLOT_REF slot, int32_t death_count);\nbool Savegame_LoadOnlyResumeInfo(SAVEGAME_SLOT_REF slot);\n\nvoid Savegame_InitCurrentInfo(void);\nvoid Savegame_SetCurrentInfo(int32_t current_slot, int32_t src_slot);\nRESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level);\nconst SAVEGAME_INFO *Savegame_GetSavegameInfo(SAVEGAME_SLOT_REF slot);\nvoid Savegame_ResetCurrentInfo(const GF_LEVEL *level);\nvoid Savegame_CarryCurrentInfoToNextLevel(\n    const GF_LEVEL *src_level, const GF_LEVEL *dst_level);\nvoid Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level);\nvoid Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level);\n\nint32_t Savegame_GetSlotCount(SAVEGAME_SLOT_POOL pool);\nSAVEGAME_SLOT_REF Savegame_GetNextQuickSlot(void);\nint32_t Savegame_GetQuickVisualCount(void);\nSAVEGAME_SLOT_REF Savegame_QuickFromVisualIndex(int32_t visual_index);\nint32_t Savegame_QuickToVisualIndex(SAVEGAME_SLOT_REF slot);\nbool Savegame_IsValidSlotRef(SAVEGAME_SLOT_REF slot);\nSAVEGAME_SLOT_REF Savegame_NormalSlot(int32_t index);\nSAVEGAME_SLOT_REF Savegame_QuickSlot(int32_t index);\nSAVEGAME_SLOT_REF Savegame_InvalidSlot(void);\nint32_t Savegame_SlotToParam(SAVEGAME_SLOT_REF slot);\nSAVEGAME_SLOT_REF Savegame_SlotFromParam(int32_t param);\n"
  },
  {
    "path": "src/trx/game/savegame/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    SAVEGAME_STAGE_BEFORE_LOAD,\n    SAVEGAME_STAGE_AFTER_LOAD,\n    SAVEGAME_STAGE_BEFORE_SAVE,\n} SAVEGAME_STAGE;\n\ntypedef enum {\n    SG_VERSION_LEGACY = -1,\n    SG_VERSION_1 = 1,\n\n    // Before TRX 1.0\n    SG_VERSION_13 = 13,\n\n    // Separated Magnums and Automatic Pistols.\n    SG_VERSION_14 = 14,\n\n    // Replaced Lara mesh pointers with outfits\n    SG_VERSION_15 = 15,\n\n    // Music save format switched to stream list with play modes.\n    SG_VERSION_16 = 16,\n\n    // Carried-item drops are persisted with truthful statuses/positions.\n    SG_VERSION_17 = 17,\n\n    // Crystal statistics are persisted in savegames.\n    SG_VERSION_18 = 18,\n\n    SG_MIN_SUPPORTED_VERSION = SG_VERSION_13,\n    SG_CURRENT_VERSION = SG_VERSION_18,\n} SAVEGAME_VERSION;\n"
  },
  {
    "path": "src/trx/game/savegame/file.c",
    "content": "#include <trx/game/savegame/file.h>\n\n#include <trx/core/bson.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\n#include <string.h>\n#include <zconf.h>\n#include <zlib.h>\n\n#define M_MAGIC_TR1X MKTAG('T', '1', 'M', 'B') // TOOD: remove me after TRX 1.5\n#define M_MAGIC_TR2X MKTAG('T', '2', 'X', 'B') // TOOD: remove me after TRX 1.5\n#define M_MAGIC_TRX MKTAG('T', 'R', 'X', 'S')\n\n#define M_MUST(x)                                                              \\\n    if (!(x)) {                                                                \\\n        goto fail;                                                             \\\n    }\n\nstatic JSON_VALUE *M_ReadRaw(MYFILE *fp, int32_t *version_out);\n\nconst char *SG_File_GetSaveFilePattern(void)\n{\n    return g_GameFlow.savegame_file_fmt;\n}\n\nconst char *SG_File_GetQuickSaveFilePattern(void)\n{\n    const char *const pattern = SG_File_GetSaveFilePattern();\n    const char *const placeholder = strchr(pattern, '%');\n    if (placeholder == nullptr) {\n        return String_FormatStatic(\"%s_q\", pattern);\n    }\n    const int32_t prefix_size = placeholder - pattern;\n    return String_FormatStatic(\"%.*sq%s\", prefix_size, pattern, placeholder);\n}\n\nstatic JSON_VALUE *M_ParseFromBuffer(\n    const char *const buffer, int32_t *const version_out)\n{\n    const SAVEGAME_BSON_HEADER *const header = (SAVEGAME_BSON_HEADER *)buffer;\n    if (header->magic != M_MAGIC_TR1X && header->magic != M_MAGIC_TR2X\n        && header->magic != M_MAGIC_TRX) {\n        LOG_ERROR(\"Invalid savegame magic\");\n        return nullptr;\n    }\n\n    if (version_out != nullptr) {\n        *version_out = header->version;\n    }\n\n    const char *const compressed = buffer + sizeof(SAVEGAME_BSON_HEADER);\n    char *uncompressed = Memory_Alloc(header->uncompressed_size);\n\n    uLongf uncompressed_size = header->uncompressed_size;\n    const int32_t error_code = uncompress(\n        (Bytef *)uncompressed, &uncompressed_size, (const Bytef *)compressed,\n        (uLongf)header->compressed_size);\n    if (error_code != Z_OK) {\n        LOG_ERROR(\"Failed to decompress the data (error %d)\", error_code);\n        Memory_FreePointer(&uncompressed);\n        return nullptr;\n    }\n\n    JSON_VALUE *const root = BSON_Parse(uncompressed, uncompressed_size);\n    Memory_FreePointer(&uncompressed);\n    return root;\n}\n\nstatic JSON_VALUE *M_ReadRaw(MYFILE *const fp, int32_t *const version_out)\n{\n    const size_t buffer_size = File_Size(fp);\n    char *buffer = Memory_Alloc(buffer_size);\n    File_Seek(fp, 0, FILE_SEEK_SET);\n    File_ReadData(fp, buffer, buffer_size);\n\n    JSON_VALUE *const result = M_ParseFromBuffer(buffer, version_out);\n    Memory_FreePointer(&buffer);\n    return result;\n}\n\nstatic void M_SaveRaw(\n    MYFILE *const fp, const JSON_VALUE *const root, const int32_t level_num,\n    const bool is_quick)\n{\n    size_t uncompressed_size;\n    char *uncompressed = BSON_Write(root, &uncompressed_size);\n\n    uLongf compressed_size = compressBound(uncompressed_size);\n    char *compressed = Memory_Alloc(compressed_size);\n    const int32_t result = compress(\n        (Bytef *)compressed, &compressed_size, (const Bytef *)uncompressed,\n        (uLongf)uncompressed_size);\n    if (result != Z_OK) {\n        Shell_ExitSystem(\"Failed to compress savegame data\");\n    }\n\n    const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num);\n    const JSON_OBJECT *const root_obj = JSON_ValueAsObject(root);\n    const SAVEGAME_BSON_HEADER header = {\n        .magic = M_MAGIC_TRX,\n        .initial_version = Savegame_GetInitialVersion(),\n        .version = SG_CURRENT_VERSION,\n        .compressed_size = compressed_size,\n        .uncompressed_size = uncompressed_size,\n    };\n    const SAVEGAME_BSON_EXTENDED_HEADER extra_header = {\n        .flags = Game_GetBonusFlag() | (is_quick ? SAVEGAME_EXT_FLAG_QUICK : 0),\n        .counter =\n            JSON_ObjectGetInt(root_obj, \"save_counter\", Savegame_GetCounter()),\n        .level_num = level->num,\n        .title_size = level->title != nullptr ? strlen(level->title) : 0,\n    };\n\n    File_WriteData(fp, &header, sizeof(header));\n    File_WriteData(fp, compressed, compressed_size);\n    File_WriteData(fp, &extra_header, sizeof(extra_header));\n    File_WriteData(\n        fp, level->title, level->title != nullptr ? strlen(level->title) : 0);\n\n    Memory_FreePointer(&uncompressed);\n    Memory_FreePointer(&compressed);\n}\n\nbool SG_File_LoadFromFile(MYFILE *const fp)\n{\n    bool result = false;\n\n    int32_t sg_version = -1;\n    JSON_VALUE *const root = M_ReadRaw(fp, &sg_version);\n    JSON_READ_IO *const io = JSON_ReadIO_Create(root, sg_version, nullptr);\n\n    M_MUST(SG_File_LoadResumeInfoList(io));\n    M_MUST(SG_File_LoadMisc(io));\n    M_MUST(SG_File_LoadInventory(io));\n    M_MUST(SG_File_LoadFlipmaps(io));\n    M_MUST(SG_File_LoadCameras(io));\n    M_MUST(SG_File_LoadItems(io));\n    M_MUST(SG_File_LoadEffects(io));\n    M_MUST(SG_File_LoadFX(io));\n    M_MUST(SG_File_LoadFlares(io));\n    M_MUST(SG_File_LoadMusic(io));\n    M_MUST(SG_File_LoadLara(io));\n\n    result = true;\n\nfail:\n    JSON_ReadIO_Destroy(io);\n    JSON_ValueFree(root);\n    return result;\n}\n\nvoid SG_File_SaveToFile(MYFILE *const fp, SAVEGAME_INFO *const info)\n{\n    const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n    JSON_WRITE_IO *const io = JSON_WriteIO_Create();\n\n    SG_File_DumpResumeInfoList(io);\n    SG_File_DumpInventory(io);\n    SG_File_DumpFlipmaps(io);\n    SG_File_DumpCameras(io);\n    SG_File_DumpItems(io);\n    SG_File_DumpEffects(io);\n    SG_File_DumpFX(io);\n    SG_File_DumpLara(io);\n    SG_File_DumpMusic(io);\n    SG_File_DumpFlares(io);\n    SG_File_DumpMisc(io);\n\n    M_SaveRaw(\n        fp, JSON_WriteIO_GetRoot(io), current_level->num,\n        info != nullptr && info->is_quick);\n    JSON_WriteIO_Destroy(io);\n}\n\nbool SG_File_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info)\n{\n    *info = (SAVEGAME_INFO) {};\n\n    SAVEGAME_BSON_HEADER header = {};\n    File_Seek(fp, 0, FILE_SEEK_SET);\n    if (!File_ReadData(fp, &header, sizeof(SAVEGAME_BSON_HEADER))) {\n        return false;\n    }\n    if (header.magic != M_MAGIC_TR1X && header.magic != M_MAGIC_TR2X\n        && header.magic != M_MAGIC_TRX) {\n        return false;\n    }\n\n    if (header.version < SG_MIN_SUPPORTED_VERSION) {\n        LOG_WARNING(\n            \"Too old SG version: %d (min supported: %d)\", header.version,\n            SG_MIN_SUPPORTED_VERSION);\n        return false;\n    }\n    info->initial_version = header.initial_version;\n    info->features.restart = header.initial_version >= SG_VERSION_LEGACY;\n    info->features.select_level = header.initial_version >= SG_VERSION_1;\n\n    // recover the slot information from the end of the file\n    File_Skip(fp, header.compressed_size);\n    SAVEGAME_BSON_EXTENDED_HEADER extra_header;\n    if (File_ReadData(fp, &extra_header, sizeof(extra_header))) {\n        info->counter = extra_header.counter;\n        info->level_num = extra_header.level_num;\n        info->is_quick = (extra_header.flags & SAVEGAME_EXT_FLAG_QUICK) != 0;\n        info->level_title = Memory_Alloc(extra_header.title_size + 1);\n        File_ReadData(fp, info->level_title, extra_header.title_size);\n        return true;\n    }\n\n    // recover the slot information from the savegame structures\n    bool result = false;\n    File_Seek(fp, 0, FILE_SEEK_SET);\n    JSON_VALUE *root = M_ReadRaw(fp, nullptr);\n    JSON_OBJECT *root_obj = JSON_ValueAsObject(root);\n    if (root_obj != nullptr) {\n        info->counter = JSON_ObjectGetInt(root_obj, \"save_counter\", -1);\n        info->level_num = JSON_ObjectGetInt(root_obj, \"level_num\", -1);\n        const char *level_title =\n            JSON_ObjectGetString(root_obj, \"level_title\", nullptr);\n        if (level_title != nullptr) {\n            info->level_title = Memory_DupStr(level_title);\n        }\n        result = info->level_num != -1;\n    }\n    JSON_ValueFree(root);\n\n    return result;\n}\n\nbool SG_File_LoadOnlyResumeInfo(MYFILE *const fp)\n{\n    int32_t sg_version = -1;\n    JSON_VALUE *const root = M_ReadRaw(fp, &sg_version);\n    JSON_READ_IO *const io = JSON_ReadIO_Create(root, sg_version, nullptr);\n    const bool result = SG_File_LoadResumeInfoList(io);\n    JSON_ReadIO_Destroy(io);\n    JSON_ValueFree(root);\n    return result;\n}\n\nbool SG_File_UpdateDeathCounters(\n    MYFILE *const fp, int32_t level_num, const int32_t death_count,\n    const bool is_quick)\n{\n    bool result = false;\n    JSON_VALUE *const root = M_ReadRaw(fp, nullptr);\n    JSON_OBJECT *const root_obj = JSON_ValueAsObject(root);\n    if (root_obj == nullptr) {\n        LOG_ERROR(\"Cannot find the root object\");\n        goto cleanup;\n    }\n\n    JSON_OBJECT *const misc_obj = JSON_ObjectGetObject(root_obj, \"misc\");\n    if (misc_obj == nullptr) {\n        LOG_ERROR(\"Cannot find the misc object\");\n        goto cleanup;\n    }\n    JSON_ObjectEvictKey(misc_obj, \"death_count\");\n    JSON_ObjectAppendInt(misc_obj, \"death_count\", death_count);\n\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    int32_t resume_idx = -1;\n    for (int32_t i = 0; i < level_table->count; i++) {\n        if (level_table->levels[i].num == level_num) {\n            resume_idx = i;\n            break;\n        }\n    }\n\n    JSON_ARRAY *const resume_arr = JSON_ObjectGetArray(root_obj, \"resume_info\");\n    if (resume_arr != nullptr && resume_idx != -1) {\n        JSON_OBJECT *const resume_obj =\n            JSON_ArrayGetObject(resume_arr, resume_idx);\n        if (resume_obj != nullptr) {\n            JSON_ObjectEvictKey(resume_obj, \"death_count\");\n            JSON_ObjectAppendInt(resume_obj, \"death_count\", death_count);\n        }\n    }\n\n    File_Seek(fp, 0, FILE_SEEK_SET);\n    M_SaveRaw(fp, root, level_num, is_quick);\n    result = true;\n\ncleanup:\n    JSON_ValueFree(root);\n    return result;\n}\n"
  },
  {
    "path": "src/trx/game/savegame/file.h",
    "content": "#pragma once\n\n#include <trx/core/filesystem.h>\n#include <trx/core/json.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/game/savegame/types.h>\n\n#include <stdint.h>\n\nconst char *SG_File_GetSaveFilePattern(void);\nconst char *SG_File_GetQuickSaveFilePattern(void);\nbool SG_File_FillInfo(MYFILE *fp, SAVEGAME_INFO *info);\nbool SG_File_LoadFromFile(MYFILE *fp);\nbool SG_File_LoadOnlyResumeInfo(MYFILE *fp);\nvoid SG_File_SaveToFile(MYFILE *fp, SAVEGAME_INFO *info);\nbool SG_File_UpdateDeathCounters(\n    MYFILE *fp, int32_t level_num, int32_t death_count, bool is_quick);\n\n// Start of reader functions ===================================================\nbool SG_File_LoadLara(JSON_READ_IO *io);\nbool SG_File_LoadInventory(JSON_READ_IO *io);\nbool SG_File_LoadFlipmaps(JSON_READ_IO *io);\nbool SG_File_LoadCameras(JSON_READ_IO *io);\nbool SG_File_LoadItems(JSON_READ_IO *io);\nbool SG_File_LoadEffects(JSON_READ_IO *io);\nbool SG_File_LoadFX(JSON_READ_IO *io);\nbool SG_File_LoadFlares(JSON_READ_IO *io);\nbool SG_File_LoadMusic(JSON_READ_IO *io);\nbool SG_File_LoadResumeInfoList(JSON_READ_IO *io);\nbool SG_File_LoadMisc(JSON_READ_IO *io);\n// End of reader functions =====================================================\n\n// Start of writer functions ===================================================\nvoid SG_File_DumpFlares(JSON_WRITE_IO *io);\nvoid SG_File_DumpEffects(JSON_WRITE_IO *io);\nvoid SG_File_DumpInventory(JSON_WRITE_IO *io);\nvoid SG_File_DumpFlipmaps(JSON_WRITE_IO *io);\nvoid SG_File_DumpCameras(JSON_WRITE_IO *io);\nvoid SG_File_DumpMusic(JSON_WRITE_IO *io);\nvoid SG_File_DumpItems(JSON_WRITE_IO *io);\nvoid SG_File_DumpFX(JSON_WRITE_IO *io);\nvoid SG_File_DumpLara(JSON_WRITE_IO *io);\nvoid SG_File_DumpResumeInfoList(JSON_WRITE_IO *io);\nvoid SG_File_DumpMisc(JSON_WRITE_IO *io);\n// End of writer functions =====================================================\n\n#pragma pack(push, 1)\ntypedef struct {\n    uint32_t magic;\n    int16_t initial_version;\n    uint16_t version;\n    int32_t compressed_size;\n    int32_t uncompressed_size;\n} SAVEGAME_BSON_HEADER;\n\ntypedef struct {\n    uint32_t flags;\n    int32_t counter;\n    int32_t level_num;\n    int32_t title_size;\n} SAVEGAME_BSON_EXTENDED_HEADER;\n#pragma pack(pop)\n\n#define SAVEGAME_EXT_FLAG_QUICK (1U << 31)\n"
  },
  {
    "path": "src/trx/game/savegame/file_read.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/fx/weather.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/gun.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/flare_item.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/output.h>\n#include <trx/game/pathing.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#define M_SHOULD JSON_SHOULD\n#define M_OPTIONAL JSON_OPTIONAL\n#define M_MUST JSON_MUST\n#define M_FAIL JSON_FAIL\n#define M_FINISH JSON_FINISH\n\nstatic bool M_ReadObjectID(\n    JSON_READ_IO *const io, const char *const key, OBJECT_ID *const target)\n{\n    int32_t game_id = 0;\n    M_MUST(JSON_READ(io, key, &game_id));\n    *target = Object_FromGameID(game_id);\n    if (*target == NO_OBJECT) {\n        JSON_ReadIO_SetError(io, \"unsupported object #%d\", game_id);\n        M_FAIL();\n    }\n    M_FINISH();\n}\n\nstatic bool M_ReadArm(\n    JSON_READ_IO *const io, const char *const key, LARA_ARM *const arm)\n{\n    ASSERT(arm != nullptr);\n    M_MUST(JSON_PUSH(io, key));\n    M_MUST(JSON_READ(io, \"anim_num\", &arm->anim_num));\n    M_MUST(JSON_READ(io, \"frame_num\", &arm->frame_num));\n    M_MUST(JSON_READ(io, \"lock\", &arm->lock));\n    M_MUST(JSON_READ(io, \"flash_gun\", &arm->flash_gun));\n    M_MUST(JSON_READ(io, \"rot\", &arm->rot));\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nstatic bool M_ReadAmmo(\n    JSON_READ_IO *const io, const char *const key, AMMO_INFO *const ammo)\n{\n    ASSERT(ammo != nullptr);\n    M_MUST(JSON_PUSH(io, key));\n    M_MUST(JSON_READ(io, \"ammo\", &ammo->ammo));\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nstatic bool M_ReadLara(JSON_READ_IO *const io)\n{\n    LARA_INFO *const lara = Lara_GetLaraInfo();\n    ASSERT(lara != nullptr);\n\n    if (!M_OPTIONAL(JSON_READ(io, \"item_number\", &lara->item_num))) {\n        // Introduced in TRX 1.2\n        M_MUST(JSON_READ(io, \"item_num\", &lara->item_num));\n    }\n    M_MUST(JSON_READ(io, \"gun_status\", &lara->gun_status));\n    M_MUST(JSON_READ(io, \"gun_type\", &lara->gun_type));\n    M_MUST(JSON_READ(io, \"request_gun_type\", &lara->request_gun_type));\n\n    // TRX <1.1\n    if (g_TRVersion == 2 && JSON_ReadIO_GetVersion(io) < SG_VERSION_14) {\n        if (lara->gun_type == LGT_MAGNUMS) {\n            lara->gun_type = LGT_AUTOS;\n        }\n        if (lara->request_gun_type == LGT_MAGNUMS) {\n            lara->request_gun_type = LGT_AUTOS;\n        }\n    }\n\n    M_MUST(JSON_READ(io, \"last_gun_type\", &lara->last_gun_type));\n    M_MUST(JSON_READ(io, \"calc_fall_speed\", &lara->calc_fall_speed));\n    M_MUST(JSON_READ(io, \"water_status\", &lara->water_status));\n    M_MUST(JSON_READ(io, \"climb_status\", &lara->climb_status));\n    M_SHOULD(JSON_READ(io, \"is_crouched\", &lara->is_crouched));\n    M_SHOULD(JSON_READ(io, \"keep_crouched\", &lara->keep_crouched));\n    M_SHOULD(JSON_READ(io, \"sprinting\", &lara->sprinting));\n    M_MUST(JSON_READ(io, \"pose_count\", &lara->pose_count));\n    M_MUST(JSON_READ(io, \"hit_frame\", &lara->hit_frame));\n    M_MUST(JSON_READ(io, \"hit_direction\", &lara->hit_direction));\n    M_MUST(JSON_READ(io, \"air\", &lara->air));\n    M_MUST(JSON_READ(io, \"sprint_timer\", &lara->sprint_timer));\n    M_MUST(JSON_READ(io, \"exposure_timer\", &lara->exposure_timer));\n    M_SHOULD(JSON_READ(io, \"poison_timer\", &lara->poison_timer));\n    M_MUST(JSON_READ(io, \"dive_count\", &lara->dive_timer));\n    M_MUST(JSON_READ(io, \"death_count\", &lara->death_timer));\n    M_MUST(JSON_READ(io, \"current_active\", &lara->current.active));\n    M_SHOULD(JSON_READ(io, \"current_vel_x\", &lara->current.vel.x));\n    M_SHOULD(JSON_READ(io, \"current_vel_z\", &lara->current.vel.z));\n    M_MUST(JSON_READ(io, \"burn\", &lara->burn));\n    // Introduced in TRX 1.2\n    M_SHOULD(JSON_READ(io, \"electric\", &lara->electric));\n\n    M_MUST(JSON_READ(io, \"mesh_effects\", &lara->mesh_effects));\n    M_MUST(JSON_READ(io, \"extra_anim\", &lara->extra_anim));\n    M_MUST(JSON_READ(io, \"water_surface_dist\", &lara->water_surface_dist));\n\n    M_MUST(JSON_READ(io, \"hit_effect_count\", &lara->hit_effect_count));\n    int16_t hit_effect = NO_EFFECT;\n    M_MUST(JSON_READ(io, \"hit_effect\", &hit_effect));\n    lara->hit_effect =\n        hit_effect != NO_EFFECT && g_Config.gameplay.enable_enhanced_saves\n        ? Effect_Get(hit_effect)\n        : nullptr;\n\n    int16_t vehicle_idx = Lara_Vehicle_GetIndex();\n    if (!M_OPTIONAL(JSON_READ(io, \"vehicle_item_number\", &vehicle_idx))) {\n        // Introduced in TRX 1.2\n        M_MUST(JSON_READ(io, \"vehicle_item_num\", &vehicle_idx));\n    }\n    Lara_Vehicle_SetIndex(vehicle_idx);\n\n    M_MUST(JSON_READ(io, \"flare_age\", &lara->flare.age));\n    M_MUST(JSON_READ(io, \"flare_frame\", &lara->flare.frame_num));\n    M_MUST(JSON_READ(io, \"flare_control_left\", &lara->flare.control));\n\n    // < TRX 1.2\n    if (JSON_ReadIO_GetVersion(io) < SG_VERSION_15) {\n        // TODO: remove in TRX 1.5.\n        M_MUST(JSON_PUSH(io, \"meshes\"));\n        const int32_t mesh_count = JSON_ARRAY_LEN(io);\n        if (mesh_count != LM_NUMBER_OF) {\n            JSON_ReadIO_SetError(\n                io, \"expected %d Lara meshes, got %d\", LM_NUMBER_OF,\n                mesh_count);\n            M_FAIL();\n        }\n        const OBJECT_MESH *meshes[LM_NUMBER_OF] = {};\n        for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n            int32_t idx = 0;\n            M_MUST(JSON_READ_A(io, i, &idx));\n            meshes[i] = Object_FindMesh(idx);\n        }\n        M_MUST(JSON_POP(io));\n\n        Lara_Skin_ExtractLegacyEquipment(meshes);\n    } else {\n        M_MUST(JSON_PUSH(io, \"skin\"));\n        LARA_SKIN_TYPE skin_type = LARA_SKIN_TYPE_DEFAULT;\n        bool skin_is_default = false;\n        M_MUST(JSON_READ(io, \"skin_type\", &skin_type));\n        M_MUST(JSON_READ(io, \"skin_is_default\", &skin_is_default));\n        if (!skin_is_default) {\n            Lara_Skin_SetType(skin_type);\n        }\n\n        bool holsters_visible = true;\n        M_MUST(JSON_READ(io, \"holsters_visible\", &holsters_visible));\n        Lara_Skin_SetHolstersVisible(holsters_visible);\n\n        M_MUST(JSON_PUSH(io, \"equipment\"));\n        const int32_t mesh_count = JSON_ARRAY_LEN(io);\n        if (mesh_count != LM_NUMBER_OF) {\n            JSON_ReadIO_SetError(\n                io, \"expected %d equipment meshes, got %d\", LM_NUMBER_OF,\n                mesh_count);\n            M_FAIL();\n        }\n        for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n            LARA_SKIN_EQUIPMENT_TYPE type = EQUIPMENT_TYPE_NONE;\n            int32_t data = -1;\n            M_MUST(JSON_PUSH_INDEX(io, i));\n            M_MUST(JSON_READ(io, \"type\", &type));\n            M_MUST(JSON_READ(io, \"data\", &data));\n            M_MUST(JSON_POP(io));\n\n            if (type == EQUIPMENT_TYPE_WEAPON) {\n                Lara_Skin_SetGunEquipment(i, data);\n            } else if (type == EQUIPMENT_TYPE_EXTRA) {\n                Lara_Skin_SetExtraEquipment(i, data);\n            } else {\n                Lara_Skin_ClearEquipment(i);\n            }\n        }\n        M_MUST(JSON_POP(io));\n        M_MUST(JSON_POP(io));\n        Lara_Skin_ApplyOutfit();\n    }\n\n    lara->target = nullptr;\n    M_MUST(JSON_READ(io, \"target_angle1\", &lara->target_angles[0]));\n    M_MUST(JSON_READ(io, \"target_angle2\", &lara->target_angles[1]));\n    M_MUST(JSON_READ(io, \"turn_rate\", &lara->turn_rate));\n    M_MUST(JSON_READ(io, \"move_angle\", &lara->move_angle));\n    M_MUST(JSON_READ(io, \"head_rot\", &lara->head_rot));\n    M_MUST(JSON_READ(io, \"torso_rot\", &lara->torso_rot));\n    M_MUST(JSON_READ(io, \"last_pos\", &lara->last_pos));\n\n    M_MUST(M_ReadArm(io, \"left_arm\", &lara->left_arm));\n    M_MUST(M_ReadArm(io, \"right_arm\", &lara->right_arm));\n    M_MUST(M_ReadAmmo(io, \"pistols\", &lara->pistol_ammo));\n    M_MUST(M_ReadAmmo(io, \"magnums\", &lara->magnum_ammo));\n    M_MUST(M_ReadAmmo(io, \"uzis\", &lara->uzi_ammo));\n    M_MUST(M_ReadAmmo(io, \"shotgun\", &lara->shotgun_ammo));\n    M_MUST(M_ReadAmmo(io, \"harpoon\", &lara->harpoon_ammo));\n    M_MUST(M_ReadAmmo(io, \"grenade\", &lara->grenade_ammo));\n    M_MUST(M_ReadAmmo(io, \"m16\", &lara->m16_ammo));\n    M_SHOULD(M_ReadAmmo(io, \"autos\", &lara->autos_ammo));\n    M_SHOULD(M_ReadAmmo(io, \"desert_eagle\", &lara->desert_eagle_ammo));\n    M_SHOULD(M_ReadAmmo(io, \"mp5\", &lara->mp5_ammo));\n    M_SHOULD(M_ReadAmmo(io, \"rocket\", &lara->rocket_ammo));\n\n    if (M_OPTIONAL(JSON_PUSH(io, \"weapon\"))) {\n        lara->gun_item_num = Item_Create();\n        ITEM *const weapon_item = Item_Get(lara->gun_item_num);\n        weapon_item->status = IS_ACTIVE;\n        weapon_item->room_num = NO_ROOM;\n        // Introduced in TRX 1.2\n        if (!M_SHOULD(\n                M_ReadObjectID(io, \"object_id\", &weapon_item->object_id))) {\n            M_MUST(M_ReadObjectID(io, \"obj_id\", &weapon_item->object_id));\n        }\n        M_MUST(JSON_READ(io, \"anim_num\", &weapon_item->anim_num));\n        M_MUST(JSON_READ(io, \"frame_num\", &weapon_item->frame_num));\n        M_MUST(JSON_READ(\n            io, \"current_anim_state\", &weapon_item->current_anim_state));\n        M_MUST(JSON_READ(io, \"goal_anim_state\", &weapon_item->goal_anim_state));\n        M_MUST(JSON_POP(io));\n    }\n\n    M_MUST(JSON_PUSH(io, \"interact_target\"));\n    M_MUST(JSON_READ(io, \"item_num\", &lara->interact_target.item_num));\n    M_MUST(JSON_READ(io, \"move_count\", &lara->interact_target.move_count));\n    M_MUST(JSON_READ(io, \"is_moving\", &lara->interact_target.is_moving));\n    M_MUST(JSON_POP(io));\n\n    M_FINISH();\n}\n\nstatic bool M_IsValidItemObject(\n    const OBJECT_ID saved_obj_id, const OBJECT_ID initial_obj_id)\n{\n    if (saved_obj_id == initial_obj_id) {\n        return true;\n    }\n    if (Object_IsType(initial_obj_id, g_GunObjects)\n        && Object_IsType(saved_obj_id, g_GunObjects)) {\n        return true;\n    }\n\n    // clang-format off\n    switch (saved_obj_id) {\n        // used keyholes\n        case O_PUZZLE_DONE_1: return initial_obj_id == O_PUZZLE_HOLE_1;\n        case O_PUZZLE_DONE_2: return initial_obj_id == O_PUZZLE_HOLE_2;\n        case O_PUZZLE_DONE_3: return initial_obj_id == O_PUZZLE_HOLE_3;\n        case O_PUZZLE_DONE_4: return initial_obj_id == O_PUZZLE_HOLE_4;\n        // pickups\n        case O_PISTOL_AMMO_ITEM: return initial_obj_id == O_PISTOL_ITEM;\n        case O_SHOTGUN_AMMO_ITEM: return initial_obj_id == O_SHOTGUN_ITEM;\n        case O_MAGNUM_AMMO_ITEM: return initial_obj_id == O_MAGNUM_ITEM;\n        case O_AUTOS_AMMO_ITEM: return initial_obj_id == O_AUTOS_ITEM;\n        case O_DESERT_EAGLE_AMMO_ITEM: return initial_obj_id == O_DESERT_EAGLE_ITEM;\n        case O_UZI_AMMO_ITEM: return initial_obj_id == O_UZI_ITEM;\n        case O_HARPOON_AMMO_ITEM: return initial_obj_id == O_HARPOON_ITEM;\n        case O_M16_AMMO_ITEM: return initial_obj_id == O_M16_ITEM;\n        case O_MP5_AMMO_ITEM: return initial_obj_id == O_MP5_ITEM;\n        case O_GRENADE_AMMO_ITEM: return initial_obj_id == O_GRENADE_GUN_ITEM;\n        case O_ROCKET_AMMO_ITEM: return initial_obj_id == O_ROCKET_GUN_ITEM;\n        // dual-state animals\n        case O_ALLIGATOR: return initial_obj_id == O_CROCODILE;\n        case O_CROCODILE: return initial_obj_id == O_ALLIGATOR;\n        case O_RAT: return initial_obj_id == O_VOLE;\n        case O_VOLE: return initial_obj_id == O_RAT;\n        // skidoo swaps\n        case O_SKIDOO_FAST: return initial_obj_id == O_SKIDOO_ARMED;\n        // default\n        default: return false;\n    }\n    // clang-format on\n}\n\nstatic int16_t M_ResolveItem(JSON_READ_IO *const io, const int16_t read_index)\n{\n    const char *item_name = nullptr;\n    if (M_OPTIONAL(JSON_READ(io, \"name\", &item_name))) {\n        const ITEM *const item = Item_GetByName(item_name);\n        if (item == nullptr) {\n            LOG_WARNING(\n                \"invalid item name '%s' (read index %d)\", item_name,\n                read_index);\n            return NO_ITEM;\n        }\n\n        return Item_GetIndex(item);\n    }\n\n    int16_t item_num;\n    if (!M_SHOULD(JSON_READ(io, \"index\", &item_num))) {\n        item_num = read_index; // TODO: remove after TRX 2.0\n    }\n\n    if (item_num < 0 || item_num >= Item_GetLevelCount()) {\n        LOG_WARNING(\n            \"invalid item index %d (read index %d)\", item_num, read_index);\n        return NO_ITEM;\n    }\n\n    return item_num;\n}\n\nstatic bool M_ReadItem(JSON_READ_IO *const io, const int16_t read_index)\n{\n    const int16_t item_num = M_ResolveItem(io, read_index);\n    if (item_num == NO_ITEM) {\n        // soft exit for unresolvable items\n        return true;\n    }\n\n    ITEM *const item = Item_Get(item_num);\n\n    OBJECT_ID object_id = NO_OBJECT;\n    // Not all TR3 objects are implemented as of >= TRX 1.1\n    if (!M_SHOULD(M_ReadObjectID(io, \"object_id\", &object_id))) {\n        item->object_id = O_DUMMY;\n        return true;\n    }\n\n    const OBJECT *const obj = Object_Get(object_id);\n    item->object_id = object_id;\n    if (!M_IsValidItemObject(object_id, item->object_id)) {\n        JSON_ReadIO_SetError(\n            io, \"level has %d (%s), save has %d (%s)\", item->object_id,\n            Object_GetName(item->object_id), object_id,\n            Object_GetName(object_id));\n        M_FAIL();\n    }\n\n    // Not sure why some items do not have their their position saved,\n    // despite OBJECT telling them to.\n    if (obj->save_position && JSON_ReadIO_HasKey(io, \"room_num\")) {\n        M_MUST(JSON_READ(io, \"pos\", &item->pos));\n        M_MUST(JSON_READ(io, \"rot\", &item->rot));\n        M_MUST(JSON_READ(io, \"speed\", &item->speed));\n        M_MUST(JSON_READ(io, \"fall_speed\", &item->fall_speed));\n        int16_t room_num = NO_ROOM;\n        M_MUST(JSON_READ(io, \"room_num\", &room_num));\n        if (room_num != NO_ROOM) {\n            Item_UpdateRoom(item_num, room_num);\n        }\n    }\n\n    if (obj->save_anim) {\n        // TRX >= 1.1 animated puzzle holes became animated\n        M_SHOULD(JSON_READ(io, \"current_anim\", &item->current_anim_state));\n        M_SHOULD(JSON_READ(io, \"goal_anim\", &item->goal_anim_state));\n        M_SHOULD(JSON_READ(io, \"required_anim\", &item->required_anim_state));\n        M_SHOULD(JSON_READ(io, \"anim_num\", &item->anim_num));\n        M_SHOULD(JSON_READ(io, \"frame_num\", &item->frame_num));\n        M_SHOULD(JSON_READ(io, \"prev_frame_num\", &item->prev_frame_num));\n\n        // Prevent issues with pre-injection saves and Lara's enhanced\n        // animation set.\n        if (item->object_id == O_LARA\n            && item->anim_num < LARA_ORIGINAL_ANIM_COUNT) {\n            item->anim_num += obj->anim_idx;\n        }\n    }\n\n    if (obj->save_hitpoints) {\n        M_MUST(JSON_READ(io, \"hitpoints\", &item->hit_points));\n        M_MUST(JSON_READ(io, \"max_hitpoints\", &item->max_hit_points));\n    }\n\n    if (obj->save_flags) {\n        if (!JSON_ReadIO_HasKey(io, \"flags\")) {\n            // TRX 1.1 save-crystal entries were serialized as bare items\n            // without save-state fields. Treat them as default-state crystals\n            // so those legacy saves remain loadable.\n            if (object_id == O_SAVE_CRYSTAL_ITEM) {\n                goto skip_flags;\n            }\n        }\n        M_MUST(JSON_READ(io, \"flags\", &item->flags));\n        M_MUST(JSON_READ(io, \"timer\", &item->timer));\n        ITEM_STATUS saved_status = item->status;\n        M_MUST(JSON_READ(io, \"status\", &saved_status));\n\n        if ((item->flags & IF_KILLED) != 0) {\n            Item_Kill(item_num);\n            item->status = saved_status;\n        } else {\n            bool is_active;\n            M_MUST(JSON_READ(io, \"active\", &is_active));\n            if (is_active && !item->active) {\n                Item_AddActive(item_num);\n            }\n            item->status = saved_status;\n            M_MUST(JSON_READ(io, \"gravity\", &item->gravity));\n            // Introduced in TRX 1.2\n            M_OPTIONAL(JSON_READ(io, \"collidable\", &item->collidable));\n        }\n        // Introduced in TRX 1.2, not written if zero\n        M_OPTIONAL(JSON_READ(io, \"ai_bits\", &item->ai_bits));\n        M_OPTIONAL(JSON_READ(io, \"ai_tag\", &item->ai_tag));\n\n        bool intelligent = obj->intelligent;\n        // Introduced in TRX 1.2\n        M_SHOULD(JSON_READ(io, \"intelligent\", &intelligent));\n        if (intelligent) {\n            LOT_EnableBaddieAI(item_num, true);\n            CREATURE *const creature = item->creature_data;\n            if (creature != nullptr) {\n                M_MUST(JSON_READ(io, \"head_rot\", &creature->head_rotation));\n                M_MUST(JSON_READ(io, \"neck_rot\", &creature->neck_rotation));\n                M_MUST(JSON_READ(io, \"max_turn\", &creature->maximum_turn));\n                M_MUST(JSON_READ(io, \"creature_flags\", &creature->flags));\n                M_MUST(JSON_READ(io, \"creature_mood\", &creature->mood));\n                if (M_SHOULD(JSON_PUSH(io, \"creature\"))) {\n                    // Introduced in TRX 1.2\n                    M_MUST(JSON_READ(io, \"alerted\", &creature->alerted));\n                    M_MUST(JSON_READ(io, \"head_left\", &creature->head_left));\n                    M_MUST(JSON_READ(io, \"head_right\", &creature->head_right));\n                    M_MUST(\n                        JSON_READ(io, \"reached_goal\", &creature->reached_goal));\n                    M_MUST(JSON_READ(io, \"patrol_2\", &creature->patrol_2));\n                    M_MUST(\n                        JSON_READ(io, \"hurt_by_lara\", &creature->hurt_by_lara));\n                    M_MUST(JSON_READ(\n                        io, \"damage_from_lara\", &creature->damage_from_lara));\n                    M_MUST(JSON_PUSH(io, \"joint_rotations\"));\n                    for (int32_t i = 0; i < 4; i++) {\n                        // Introduced in TRX 1.2\n                        M_SHOULD(\n                            JSON_READ_A(io, i, &creature->joint_rotation[i]));\n                    }\n                    M_MUST(JSON_POP(io));\n                    M_MUST(JSON_POP(io));\n                }\n            }\n        } else if (obj->intelligent) {\n            item->creature_data = nullptr;\n            item->extra_rotations = nullptr;\n        }\n    }\nskip_flags:\n\n    if (M_SHOULD(JSON_PUSH(io, \"carried_items\"))) {\n        CARRIED_ITEM *carried_item = item->carried_item;\n        CARRIED_ITEM *prev_item = nullptr;\n        for (int32_t j = 0;; j++) {\n            if (!JSON_PUSH_INDEX(io, j)) {\n                break;\n            }\n            if (carried_item == nullptr) {\n                carried_item = GameBuf_Alloc(sizeof(CARRIED_ITEM), GBUF_ITEMS);\n                carried_item->next_item = nullptr;\n                carried_item->spawn_num = NO_ITEM;\n                if (prev_item != nullptr) {\n                    prev_item->next_item = carried_item;\n                } else {\n                    item->carried_item = carried_item;\n                }\n            }\n            // Introduced in TRX 1.2. Must be read for both newly allocated and\n            // pre-existing carried entries (e.g. gameflow-defined drops).\n            M_SHOULD(JSON_READ(io, \"spawn_num\", &carried_item->spawn_num));\n\n            M_MUST(M_ReadObjectID(io, \"object_id\", &carried_item->object_id));\n            M_MUST(JSON_READ(io, \"pos\", &carried_item->pos));\n            M_MUST(JSON_READ(io, \"y_rot\", &carried_item->rot.y));\n            M_MUST(JSON_READ(io, \"room_num\", &carried_item->room_num));\n            M_MUST(JSON_READ(io, \"fall_speed\", &carried_item->fall_speed));\n            M_MUST(JSON_READ(io, \"status\", &carried_item->status));\n\n            Carrier_SyncItem(item_num, carried_item);\n\n            prev_item = carried_item;\n            carried_item = carried_item->next_item;\n            M_MUST(JSON_POP(io));\n        }\n        // TODO: remove legacy branch in TRX 1.5.\n        if (JSON_ReadIO_GetVersion(io) < SG_VERSION_17) {\n            Carrier_TestItemDrops(item_num);\n        }\n        M_MUST(JSON_POP(io));\n    }\n\n    if (obj->priv_size > 0 && obj->priv_load_func != nullptr) {\n        // \"priv\" introduced in TRX 1.2\n        if (M_SHOULD(JSON_PUSH(io, \"priv\"))\n            || M_SHOULD(JSON_PUSH(io, \"data\"))) {\n            obj->priv_load_func(item, io);\n            M_MUST(JSON_POP(io));\n        }\n    }\n\n    if (g_TRVersion >= 2) {\n        // TODO: make this call in both engines consistently\n        if (obj->handle_save_func != nullptr) {\n            obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD);\n        }\n    }\n\n    M_FINISH();\n}\n\nstatic bool M_ReadEffect(JSON_READ_IO *const io)\n{\n    int32_t room_num = NO_ROOM;\n    if (!M_OPTIONAL(JSON_READ(io, \"room_number\", &room_num))) {\n        // Introduced in TRX 1.2\n        M_MUST(JSON_READ(io, \"room_num\", &room_num));\n    }\n\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_EFFECT) {\n        return true;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    M_MUST(JSON_READ(io, \"pos\", &effect->pos));\n    M_MUST(JSON_READ(io, \"rot\", &effect->rot));\n    if (!M_OPTIONAL(M_ReadObjectID(io, \"object_number\", &effect->object_id))) {\n        // Introduced in TRX 1.2\n        M_MUST(M_ReadObjectID(io, \"object_id\", &effect->object_id));\n    }\n    M_MUST(JSON_READ(io, \"speed\", &effect->speed));\n    M_MUST(JSON_READ(io, \"fall_speed\", &effect->fall_speed));\n    if (!M_OPTIONAL(JSON_READ(io, \"frame_number\", &effect->frame_num))) {\n        // Introduced in TRX 1.2\n        M_MUST(JSON_READ(io, \"frame_num\", &effect->frame_num));\n    }\n    M_MUST(JSON_READ(io, \"counter\", &effect->counter));\n    M_MUST(JSON_READ(io, \"shade\", &effect->shade));\n    JSON_SHOULD(JSON_READ(io, \"flag1\", &effect->flag1));\n    JSON_SHOULD(JSON_READ(io, \"flag2\", &effect->flag2));\n    M_FINISH();\n}\n\nstatic bool M_ReadFlare(JSON_READ_IO *const io)\n{\n    const int16_t item_num = Item_Create();\n    ITEM *const item = Item_Get(item_num);\n    item->object_id = O_FLARE_ITEM;\n    M_MUST(JSON_READ(io, \"pos\", &item->pos));\n    M_MUST(JSON_READ(io, \"rot\", &item->rot));\n    M_MUST(JSON_READ(io, \"room_num\", &item->room_num));\n    Item_Initialise(item_num);\n    M_MUST(JSON_READ(io, \"speed\", &item->speed));\n    M_MUST(JSON_READ(io, \"fall_speed\", &item->fall_speed));\n    int32_t flare_age;\n    M_MUST(JSON_READ(io, \"age\", &flare_age));\n    FlareItem_SetAge(item, flare_age & 0x7FFF, (flare_age & 0x8000) != 0);\n    Item_AddActive(item_num);\n    M_FINISH();\n}\n\nstatic bool M_ReadFXRing(JSON_READ_IO *const io, FX_RING *const ring)\n{\n    ASSERT(ring != nullptr);\n\n    M_MUST(JSON_READ(io, \"on\", &ring->on));\n    M_MUST(JSON_READ(io, \"life\", &ring->life));\n    M_MUST(JSON_READ(io, \"speed\", &ring->speed));\n    M_MUST(JSON_READ(io, \"radius\", &ring->radius));\n    M_MUST(JSON_READ(io, \"prev_radius\", &ring->prev_radius));\n\n    XYZ_16 rot = {};\n    M_MUST(JSON_READ(io, \"rot\", &rot));\n    ring->rot = (XZ_16) { rot.x, rot.z };\n\n    XYZ_16 prev_rot = {};\n    M_MUST(JSON_READ(io, \"prev_rot\", &prev_rot));\n    ring->prev_rot = (XZ_16) { prev_rot.x, prev_rot.z };\n\n    M_MUST(JSON_READ(io, \"pos\", &ring->pos));\n    M_MUST(JSON_READ(io, \"prev_pos\", &ring->prev_pos));\n    M_FINISH();\n}\n\nstatic bool M_ReadFXRings(\n    JSON_READ_IO *const io, const FX_RING_TYPE type, const char *const key)\n{\n    if (!M_OPTIONAL(JSON_PUSH(io, key))) {\n        return true;\n    }\n\n    const int32_t ring_count = JSON_ARRAY_LEN(io);\n    for (int32_t i = 0; i < ring_count; i++) {\n        M_MUST(JSON_PUSH_INDEX(io, i));\n        FX_RING *const ring = FX_Ring_GetRing(type, i);\n        if (ring != nullptr) {\n            M_MUST(M_ReadFXRing(io, ring));\n        } else {\n            LOG_WARNING(\n                \"Malformed save: too many %s rings. Extra rings will be \"\n                \"ignored.\",\n                key);\n        }\n        M_MUST(JSON_POP(io));\n    }\n\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nstatic bool M_ShouldLoadMusicTimestamp(\n    const MUSIC_ID track_id, const MUSIC_PLAY_MODE mode,\n    const MUSIC_ID ambient_track)\n{\n    const bool is_ambient = mode == MPM_LOOP && track_id == ambient_track;\n    return !is_ambient\n        || g_Config.audio.music_load_condition == MUSIC_LOAD_CONDITION_ALWAYS;\n}\n\nstatic bool M_ReadMusicTracks(JSON_READ_IO *const io)\n{\n    MUSIC_ID ambient_track = MX_INACTIVE;\n    M_MUST(JSON_READ(io, \"current_ambient\", &ambient_track));\n\n    Music_Stop();\n    if (ambient_track != MX_INACTIVE) {\n        // Always restart the ambient as it may have changed based on the\n        // current position in the level.\n        Music_Play_Direct(ambient_track, MPM_LOOP);\n    }\n\n    if (g_Config.audio.music_load_condition == MUSIC_LOAD_CONDITION_NEVER) {\n        return true;\n    }\n\n    if (M_SHOULD(JSON_PUSH(io, \"streams\"))) {\n        // TRX 1.2\n        const int32_t stream_count = JSON_ARRAY_LEN(io);\n        for (int32_t i = 0; i < stream_count; i++) {\n            MUSIC_ID track_id = MX_INACTIVE;\n            MUSIC_PLAY_MODE mode = MPM_ONCE;\n            double timestamp = -1.0;\n            M_MUST(JSON_PUSH_INDEX(io, i));\n            M_MUST(JSON_READ(io, \"track\", &track_id));\n            M_MUST(JSON_READ(io, \"mode\", &mode));\n            M_MUST(JSON_READ(io, \"timestamp\", &timestamp));\n            M_MUST(JSON_POP(io));\n\n            if (track_id == MX_INACTIVE) {\n                continue;\n            }\n            if (!Music_Play_Direct(track_id, mode)) {\n                LOG_WARNING(\"Could not load stream track %d\", track_id);\n                continue;\n            }\n\n            if (M_ShouldLoadMusicTimestamp(track_id, mode, ambient_track)\n                && !Music_SeekTrackTimestamp(track_id, mode, timestamp)) {\n                LOG_WARNING(\n                    \"Could not load stream track %d at timestamp %lf.\",\n                    track_id, timestamp);\n            }\n        }\n        M_MUST(JSON_POP(io));\n    } else {\n        MUSIC_ID current_track = MX_INACTIVE;\n        double timestamp = -1.0;\n        M_MUST(JSON_READ(io, \"current_track\", &current_track));\n        M_MUST(JSON_READ(io, \"timestamp\", &timestamp));\n\n        const bool is_ambient =\n            current_track != MX_INACTIVE && current_track == ambient_track;\n        if (!is_ambient && current_track != MX_INACTIVE\n            && !Music_Play_Direct(current_track, MPM_ONCE)) {\n            LOG_WARNING(\"Could not load current track %d.\", current_track);\n        }\n\n        const MUSIC_ID track_to_seek =\n            is_ambient ? ambient_track : current_track;\n        const MUSIC_PLAY_MODE mode_to_seek = is_ambient ? MPM_LOOP : MPM_ONCE;\n        if (M_ShouldLoadMusicTimestamp(\n                track_to_seek, mode_to_seek, ambient_track)\n            && !Music_SeekTrackTimestamp(\n                track_to_seek, mode_to_seek, timestamp)) {\n            LOG_WARNING(\n                \"Could not load current track %d at timestamp %lf.\",\n                current_track, timestamp);\n        }\n    }\n\n    M_FINISH();\n}\n\nstatic bool M_ReadMusicTrackFlags(JSON_READ_IO *const io)\n{\n    if (!g_Config.audio.load_music_triggers) {\n        return true;\n    }\n\n    const int32_t count = JSON_ARRAY_LEN(io);\n    if (count > MAX_MUSIC_TRACKS) {\n        JSON_ReadIO_SetError(\n            io, \"expected at most %d music track flags, got %d\",\n            MAX_MUSIC_TRACKS, count);\n        M_FAIL();\n    }\n\n    for (int32_t i = 0; i < count; i++) {\n        uint32_t flags;\n        M_MUST(JSON_READ_A(io, i, &flags));\n        Music_SetTrackFlags(i, flags);\n    }\n\n    M_FINISH();\n}\n\nstatic bool M_ReadResumeInfo(JSON_READ_IO *const io, RESUME_INFO *const resume)\n{\n    resume->lara_hitpoints = g_Config.gameplay.start_lara_hitpoints;\n    M_MUST(JSON_READ(io, \"lara_hitpoints\", &resume->lara_hitpoints));\n    M_MUST(JSON_READ(io, \"gun_status\", &resume->gun_status)); // LGS_ARMLESS\n    M_MUST(\n        JSON_READ(io, \"gun_type\", &resume->equipped_gun_type)); // LGT_UNARMED\n    M_MUST(JSON_READ(\n        io, \"holsters_gun_type\",\n        &resume->holsters_gun_type)); // LGT_UNKNOWN\n\n    // TRX <1.1\n    if (g_TRVersion == 2 && JSON_ReadIO_GetVersion(io) < SG_VERSION_14) {\n        if (resume->equipped_gun_type == LGT_MAGNUMS) {\n            resume->equipped_gun_type = LGT_AUTOS;\n        }\n        if (resume->holsters_gun_type == LGT_MAGNUMS) {\n            resume->holsters_gun_type = LGT_AUTOS;\n        }\n    }\n\n    M_MUST(\n        JSON_READ(io, \"back_gun_type\", &resume->back_gun_type)); // LGT_UNKNOWN\n    M_MUST(JSON_READ(io, \"costume\", &resume->flags.costume));\n\n    M_MUST(JSON_READ(io, \"pistol_ammo\", &resume->pistol_ammo));\n    M_MUST(JSON_READ(io, \"uzi_ammo\", &resume->uzi_ammo));\n    M_MUST(JSON_READ(io, \"shotgun_ammo\", &resume->shotgun_ammo));\n    M_MUST(JSON_READ(io, \"magnum_ammo\", &resume->magnum_ammo));\n    // Introduced in TRX 1.1\n    M_SHOULD(JSON_READ(io, \"autos_ammo\", &resume->autos_ammo));\n    M_SHOULD(JSON_READ(io, \"desert_eagle_ammo\", &resume->desert_eagle_ammo));\n\n    M_MUST(JSON_READ(io, \"m16_ammo\", &resume->m16_ammo));\n    M_MUST(JSON_READ(io, \"grenade_ammo\", &resume->grenade_ammo));\n    M_MUST(JSON_READ(io, \"harpoon_ammo\", &resume->harpoon_ammo));\n    M_MUST(JSON_READ(io, \"num_medis\", &resume->small_medipacks));\n    M_MUST(JSON_READ(io, \"num_big_medis\", &resume->large_medipacks));\n    M_MUST(JSON_READ(io, \"num_flares\", &resume->flares));\n    M_MUST(JSON_READ(io, \"num_scions\", &resume->num_scions));\n\n    // Introduced in TRX 1.2\n    M_SHOULD(JSON_READ(io, \"num_quest_item_1\", &resume->num_quest_item_1));\n    M_SHOULD(JSON_READ(io, \"num_quest_item_2\", &resume->num_quest_item_2));\n    M_SHOULD(JSON_READ(io, \"num_quest_item_3\", &resume->num_quest_item_3));\n    M_SHOULD(JSON_READ(io, \"num_quest_item_4\", &resume->num_quest_item_4));\n\n    M_MUST(JSON_READ(io, \"available\", &resume->flags.available));\n\n    // Introduced in TRX 1.2\n    resume->level_completed = false;\n    resume->prev_level = -1;\n    resume->hurt_allies = false;\n    M_SHOULD(JSON_READ(io, \"level_completed\", &resume->level_completed));\n    M_SHOULD(JSON_READ(io, \"prev_level\", &resume->prev_level));\n    M_SHOULD(JSON_READ(io, \"hurt_allies\", &resume->hurt_allies));\n\n    M_MUST(JSON_READ(io, \"has_pistols\", &resume->flags.has_pistols));\n    M_MUST(JSON_READ(io, \"has_shotgun\", &resume->flags.has_shotgun));\n    M_MUST(JSON_READ(io, \"has_uzis\", &resume->flags.has_uzis));\n    M_MUST(JSON_READ(io, \"has_m16\", &resume->flags.has_m16));\n    M_MUST(JSON_READ(io, \"has_grenade\", &resume->flags.has_grenade));\n    M_MUST(JSON_READ(io, \"has_harpoon\", &resume->flags.has_harpoon));\n\n    // Introduced in TRX 1.1\n    M_MUST(JSON_READ(io, \"has_magnums\", &resume->flags.has_magnums));\n    M_SHOULD(JSON_READ(io, \"has_autos\", &resume->flags.has_autos));\n    M_SHOULD(\n        JSON_READ(io, \"has_desert_eagle\", &resume->flags.has_desert_eagle));\n    M_SHOULD(JSON_READ(io, \"has_mp5\", &resume->flags.has_mp5));\n    M_SHOULD(JSON_READ(io, \"mp5_ammo\", &resume->mp5_ammo));\n    M_SHOULD(JSON_READ(io, \"has_rocket\", &resume->flags.has_rocket));\n    M_SHOULD(JSON_READ(io, \"rocket_ammo\", &resume->rocket_ammo));\n\n    M_MUST(JSON_READ(io, \"timer\", &resume->stats.timer));\n    M_MUST(JSON_READ(io, \"ammo_hits\", &resume->stats.ammo_hits));\n    M_MUST(JSON_READ(io, \"ammo_used\", &resume->stats.ammo_used));\n    M_MUST(JSON_READ(io, \"medipacks_used\", &resume->stats.medipacks_used));\n    M_MUST(\n        JSON_READ(io, \"distance_travelled\", &resume->stats.distance_travelled));\n    M_MUST(JSON_READ(io, \"kills\", &resume->stats.kill_count));\n    M_SHOULD(JSON_READ(io, \"crystals\", &resume->stats.crystal_count));\n    M_MUST(JSON_READ(io, \"pickups\", &resume->stats.pickup_count));\n    M_MUST(JSON_READ(io, \"secrets\", &resume->stats.secret_flags));\n    M_SHOULD(JSON_READ(io, \"death_count\", &resume->stats.death_count));\n    Stats_UpdateSecrets(&resume->stats);\n    M_FINISH();\n}\n\nbool SG_File_LoadInventory(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"inventory\"));\n    const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n\n    struct {\n        OBJECT_ID object_id;\n        const char *const key;\n    } objects[] = {\n        { O_PICKUP_ITEM_1, \"pickup1\" }, { O_PICKUP_ITEM_2, \"pickup2\" },\n        { O_QUEST_ITEM_1, \"quest1\" },   { O_QUEST_ITEM_2, \"quest2\" },\n        { O_QUEST_ITEM_3, \"quest3\" },   { O_QUEST_ITEM_4, \"quest4\" },\n        { O_PUZZLE_ITEM_1, \"puzzle1\" }, { O_PUZZLE_ITEM_2, \"puzzle2\" },\n        { O_PUZZLE_ITEM_3, \"puzzle3\" }, { O_PUZZLE_ITEM_4, \"puzzle4\" },\n        { O_KEY_ITEM_1, \"key1\" },       { O_KEY_ITEM_2, \"key2\" },\n        { O_KEY_ITEM_3, \"key3\" },       { O_KEY_ITEM_4, \"key4\" },\n        { O_LEADBAR_ITEM, \"leadbar\" },  { NO_OBJECT, nullptr },\n    };\n\n    Lara_InitialiseInventory(current_level);\n    for (int32_t i = 0; objects[i].key != nullptr; i++) {\n        int16_t qty;\n        if (JSON_READ(io, objects[i].key, &qty)) {\n            while (Inv_RequestItem(objects[i].object_id) != 0) {\n                Inv_RemoveItem(objects[i].object_id);\n            }\n            Inv_AddItemNTimes(objects[i].object_id, qty);\n        }\n    }\n\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadFlipmaps(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"flipmap\"));\n\n    bool status;\n    M_MUST(JSON_READ(io, \"status\", &status));\n    if (status) {\n        Room_FlipMap();\n    }\n\n    int32_t flip_effect;\n    int32_t flip_timer;\n    M_MUST(JSON_READ(io, \"effect\", &flip_effect));\n    M_MUST(JSON_READ(io, \"timer\", &flip_timer));\n    Room_SetFlipEffect(flip_effect);\n    Room_SetFlipTimer(flip_timer);\n\n    M_MUST(JSON_PUSH(io, \"table\"));\n    const size_t count = JSON_ARRAY_LEN(io);\n    if (count != MAX_FLIP_MAPS) {\n        JSON_ReadIO_SetError(\n            io, \"expected %d flipmap elements, got %d\", MAX_FLIP_MAPS, count);\n        M_FAIL();\n    }\n    for (size_t i = 0; i < count; i++) {\n        uint32_t flags;\n        M_MUST(JSON_READ_A(io, i, &flags));\n        Room_SetFlipSlotFlags(i, flags << 8);\n    }\n    M_MUST(JSON_POP(io));\n\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadCameras(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"cameras\"));\n    const size_t count = JSON_ARRAY_LEN(io);\n    if (count != (size_t)Camera_GetFixedObjectCount()) {\n        JSON_ReadIO_SetError(\n            io, \"expected %d cameras, got %d\", Camera_GetFixedObjectCount(),\n            count);\n        M_FAIL();\n    }\n    for (size_t i = 0; i < count; i++) {\n        OBJECT_VECTOR *const object = Camera_GetFixedObject(i);\n        M_MUST(JSON_READ_A(io, i, &object->flags));\n    }\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadLara(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"lara\"));\n    M_MUST(M_ReadLara(io));\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadItems(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"items\"));\n    const int32_t count = JSON_ARRAY_LEN(io);\n\n    Savegame_ProcessItemsBeforeLoad();\n\n    for (int32_t i = 0; i < count; i++) {\n        M_MUST(JSON_PUSH_INDEX(io, i));\n        M_MUST(M_ReadItem(io, i));\n        M_MUST(JSON_POP(io));\n    }\n\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadEffects(JSON_READ_IO *const io)\n{\n    if (!g_Config.gameplay.enable_enhanced_saves) {\n        return true;\n    }\n\n    // Introduced in TRX 1.4\n    if (!M_SHOULD(JSON_PUSH(io, \"effects\"))) {\n        M_MUST(JSON_PUSH(io, \"fx\"));\n    }\n    for (int32_t i = 0;; i++) {\n        if (!JSON_PUSH_INDEX(io, i)) {\n            break;\n        }\n        if (i < MAX_EFFECTS) {\n            M_ReadEffect(io);\n        } else {\n            LOG_WARNING(\n                \"Malformed save: expected a max of %d effect, got at least \"\n                \"%d. Extra effects will be ignored.\",\n                MAX_EFFECTS - 1, i);\n        }\n        M_MUST(JSON_POP(io));\n    }\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadFX(JSON_READ_IO *const io)\n{\n    FX_Ring_Reset();\n\n    if (!M_OPTIONAL(JSON_PUSH(io, \"vfx\"))) {\n        return true;\n    }\n    if (!M_OPTIONAL(JSON_PUSH(io, \"rings\"))) {\n        M_MUST(JSON_POP(io));\n        return true;\n    }\n\n    M_MUST(M_ReadFXRings(io, FX_RING_TYPE_BLAST, \"blast\"));\n    M_MUST(M_ReadFXRings(io, FX_RING_TYPE_KNOCKBACK, \"knockback\"));\n    M_MUST(M_ReadFXRings(io, FX_RING_TYPE_SUMMON, \"summon\"));\n\n    M_MUST(JSON_POP(io));\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadFlares(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"flares\"));\n    for (int32_t i = 0;; i++) {\n        if (!JSON_PUSH_INDEX(io, i)) {\n            break;\n        }\n        M_MUST(M_ReadFlare(io));\n        M_MUST(JSON_POP(io));\n    }\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadMusic(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"music\"));\n    M_MUST(JSON_PUSH(io, \"current\"));\n    M_MUST(M_ReadMusicTracks(io));\n    M_MUST(JSON_POP(io));\n    M_MUST(JSON_PUSH(io, \"flags\"));\n    M_MUST(M_ReadMusicTrackFlags(io));\n    M_MUST(JSON_POP(io));\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadResumeInfoList(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"resume_info\"));\n    const int32_t length = JSON_ARRAY_LEN(io);\n    const int32_t expected_length = GF_GetLevelTable(GFLT_MAIN)->count;\n    if (length != expected_length) {\n        JSON_ReadIO_SetError(\n            io, \"expected %d resume info elements, got %d\", expected_length,\n            length);\n        M_FAIL();\n    }\n    for (int32_t i = 0; i < length; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        M_MUST(JSON_PUSH_INDEX(io, i));\n        const bool has_prev_level = JSON_ReadIO_HasKey(io, \"prev_level\");\n        M_MUST(M_ReadResumeInfo(io, resume));\n        M_MUST(JSON_POP(io));\n\n        // TRX 1.0/1.1 did not store prev_level for resume entries. Infer the\n        // canonical predecessor so \"Play previous levels\" can carry loadout.\n        if (!has_prev_level && resume->prev_level == -1) {\n            const GF_LEVEL *const prev_level = GF_GetLevelBefore(level);\n            if (prev_level != nullptr) {\n                resume->prev_level = prev_level->num;\n            }\n        }\n    }\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n\nbool SG_File_LoadMisc(JSON_READ_IO *const io)\n{\n    M_MUST(JSON_PUSH(io, \"misc\"));\n\n    {\n        int32_t bonus_flag = false;\n        M_MUST(JSON_READ(io, \"bonus_flag\", &bonus_flag));\n        Game_SetBonusFlag(bonus_flag);\n    }\n\n    {\n        bool allies_hostile = false;\n        M_MUST(JSON_READ(io, \"are_monks_angry\", &allies_hostile));\n        Creature_SetAlliesHostile(allies_hostile);\n    }\n\n    {\n        int32_t sunset_timer;\n        M_MUST(JSON_READ(io, \"sunset_timer\", &sunset_timer));\n        Output_SetTimeInGame(sunset_timer);\n    }\n\n    {\n        // Introduced in TRX 1.4\n        int32_t rng_control_seed = 0;\n        if (M_OPTIONAL(JSON_READ(io, \"rng_control_seed\", &rng_control_seed))) {\n            Random_SeedControl(rng_control_seed);\n        }\n    }\n\n    {\n        // Introduced in TRX 1.4\n        int32_t rng_draw_seed = 0;\n        if (M_OPTIONAL(JSON_READ(io, \"rng_draw_seed\", &rng_draw_seed))) {\n            Random_SeedDraw(rng_draw_seed);\n        }\n    }\n\n    {\n        const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level);\n        resume->stats.death_count = -1;\n        M_MUST(JSON_READ(io, \"death_count\", &resume->stats.death_count));\n    }\n\n    {\n        int32_t weather_type = (int32_t)WEATHER_NONE;\n        if (M_OPTIONAL(JSON_READ(io, \"weather_type\", &weather_type))) {\n            if (weather_type >= (int32_t)WEATHER_NONE\n                && weather_type <= (int32_t)WEATHER_SNOW) {\n                FX_Weather_SetWeather((WEATHER_TYPE)weather_type);\n            } else {\n                FX_Weather_SetWeather(WEATHER_NONE);\n            }\n        }\n    }\n\n    M_MUST(JSON_POP(io));\n    M_FINISH();\n}\n"
  },
  {
    "path": "src/trx/game/savegame/file_write.c",
    "content": "#include <trx/config.h>\n#include <trx/core/json/util/write_io.h>\n#include <trx/debug.h>\n#include <trx/game/camera.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/explosion_ring.h>\n#include <trx/game/fx/weather.h>\n#include <trx/game/game.h>\n#include <trx/game/inventory.h>\n#include <trx/game/items.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/general/flare_item.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/game/savegame/file.h>\n#include <trx/version.h>\n\ntypedef struct {\n    int16_t count;\n    int16_t id_map[MAX_EFFECTS];\n} M_FX_ORDER;\n\nstatic void M_WriteXYZ32(\n    JSON_WRITE_IO *const io, const char *const key, const XYZ_32 source)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"x\", source.x);\n    JSONW_WRITE(io, \"y\", source.y);\n    JSONW_WRITE(io, \"z\", source.z);\n    JSONW_POP_AND_SET(io, key);\n}\n\nstatic void M_WriteXYZ16(\n    JSON_WRITE_IO *const io, const char *const key, const XYZ_16 source)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"x\", source.x);\n    JSONW_WRITE(io, \"y\", source.y);\n    JSONW_WRITE(io, \"z\", source.z);\n    JSONW_POP_AND_SET(io, key);\n}\n\nstatic void M_GetFXOrder(M_FX_ORDER *const order)\n{\n    order->count = 0;\n    for (int32_t i = 0; i < MAX_EFFECTS; i++) {\n        order->id_map[i] = -1;\n    }\n\n    for (int16_t link_num = Effect_GetActiveNum(); link_num != NO_ITEM;\n         link_num = Effect_Get(link_num)->next_active) {\n        order->id_map[link_num] = order->count;\n        order->count++;\n    }\n}\n\nstatic void M_WriteItem(\n    JSON_WRITE_IO *const io, const ITEM *const item,\n    const M_FX_ORDER *const fx_order)\n{\n    JSONW_WRITE(io, \"index\", Item_GetIndex(item));\n    if (item->name != nullptr) {\n        JSONW_WRITE(io, \"name\", item->name);\n    }\n\n    const OBJECT *const obj = Object_Get(item->object_id);\n    JSONW_WRITE(io, \"object_id\", Object_ToGameID(item->object_id));\n\n    if (obj->save_position) {\n        M_WriteXYZ32(io, \"pos\", item->pos);\n        M_WriteXYZ16(io, \"rot\", item->rot);\n        JSONW_WRITE(io, \"room_num\", item->room_num);\n        JSONW_WRITE(io, \"speed\", item->speed);\n        JSONW_WRITE(io, \"fall_speed\", item->fall_speed);\n    }\n\n    if (obj->save_anim) {\n        JSONW_WRITE(io, \"current_anim\", item->current_anim_state);\n        JSONW_WRITE(io, \"goal_anim\", item->goal_anim_state);\n        JSONW_WRITE(io, \"required_anim\", item->required_anim_state);\n        JSONW_WRITE(io, \"anim_num\", item->anim_num);\n        JSONW_WRITE(io, \"frame_num\", item->frame_num);\n        JSONW_WRITE(io, \"prev_frame_num\", item->prev_frame_num);\n    }\n\n    if (obj->save_hitpoints) {\n        JSONW_WRITE(io, \"hitpoints\", item->hit_points);\n        JSONW_WRITE(io, \"max_hitpoints\", item->max_hit_points);\n    }\n\n    if (obj->save_flags) {\n        JSONW_WRITE(io, \"flags\", item->flags);\n        JSONW_WRITE(io, \"status\", item->status);\n        JSONW_WRITE(io, \"active\", item->active);\n        JSONW_WRITE(io, \"gravity\", item->gravity);\n        JSONW_WRITE(io, \"collidable\", item->collidable);\n        const bool intelligent =\n            obj->intelligent && item->creature_data != nullptr;\n        JSONW_WRITE(io, \"intelligent\", intelligent);\n        JSONW_WRITE(io, \"timer\", item->timer);\n        JSONW_WRITE_NZ(io, \"ai_bits\", item->ai_bits);\n        JSONW_WRITE_NZ(io, \"ai_tag\", item->ai_tag);\n        if (intelligent) {\n            const CREATURE *const creature = item->creature_data;\n            JSONW_WRITE(io, \"head_rot\", creature->head_rotation);\n            JSONW_WRITE(io, \"neck_rot\", creature->neck_rotation);\n            JSONW_WRITE(io, \"max_turn\", creature->maximum_turn);\n            JSONW_WRITE(io, \"creature_flags\", creature->flags);\n            JSONW_WRITE(io, \"creature_mood\", creature->mood);\n            JSONW_PUSH_OBJECT(io);\n            JSONW_WRITE(io, \"alerted\", creature->alerted);\n            JSONW_WRITE(io, \"head_left\", creature->head_left);\n            JSONW_WRITE(io, \"head_right\", creature->head_right);\n            JSONW_WRITE(io, \"reached_goal\", creature->reached_goal);\n            JSONW_WRITE(io, \"patrol_2\", creature->patrol_2);\n            JSONW_WRITE(io, \"hurt_by_lara\", creature->hurt_by_lara);\n            JSONW_WRITE(io, \"damage_from_lara\", creature->damage_from_lara);\n            JSONW_PUSH_ARRAY(io);\n            for (int32_t i = 0; i < 4; i++) {\n                JSONW_PUSH_VALUE(io, creature->joint_rotation[i]);\n                JSONW_POP_AND_APPEND(io);\n            }\n            JSONW_POP_AND_SET(io, \"joint_rotations\");\n            JSONW_POP_AND_SET(io, \"creature\");\n        }\n    }\n\n    JSONW_PUSH_ARRAY(io);\n    const CARRIED_ITEM *drop_item = item->carried_item;\n    while (drop_item != nullptr) {\n        XYZ_32 drop_pos = drop_item->pos;\n        int16_t drop_rot_y = drop_item->rot.y;\n        int16_t drop_room_num = drop_item->room_num;\n        int16_t drop_fall_speed = drop_item->fall_speed;\n        const DROP_STATUS save_status = Carrier_GetSaveStatus(drop_item);\n\n        if ((save_status == DS_FALLING || save_status == DS_DROPPED)\n            && drop_item->spawn_num != NO_ITEM) {\n            const ITEM *const pickup = Item_Get(drop_item->spawn_num);\n            if (pickup != nullptr) {\n                drop_pos = pickup->pos;\n                drop_rot_y = pickup->rot.y;\n                drop_room_num = pickup->room_num;\n                drop_fall_speed = pickup->fall_speed;\n            }\n        }\n\n        JSONW_PUSH_OBJECT(io);\n        JSONW_WRITE(io, \"object_id\", Object_ToGameID(drop_item->object_id));\n        M_WriteXYZ32(io, \"pos\", drop_pos);\n        JSONW_WRITE(io, \"y_rot\", drop_rot_y);\n        JSONW_WRITE(io, \"room_num\", drop_room_num);\n        JSONW_WRITE(io, \"fall_speed\", drop_fall_speed);\n        JSONW_WRITE(io, \"spawn_num\", drop_item->spawn_num);\n        JSONW_WRITE(io, \"status\", (int32_t)save_status);\n        JSONW_POP_AND_APPEND(io);\n        drop_item = drop_item->next_item;\n    }\n    JSONW_POP_AND_SET(io, \"carried_items\");\n\n    if (obj->priv_size > 0 && obj->priv_save_func != nullptr) {\n        JSONW_PUSH_OBJECT(io);\n        obj->priv_save_func(item, io);\n        JSONW_POP_AND_SET(io, \"priv\");\n    }\n}\n\nstatic void M_WriteArm(\n    JSON_WRITE_IO *const io, const char *const key, const LARA_ARM *const arm)\n{\n    ASSERT(arm != nullptr);\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"anim_num\", arm->anim_num);\n    JSONW_WRITE(io, \"frame_num\", arm->frame_num);\n    JSONW_WRITE(io, \"lock\", arm->lock);\n    JSONW_WRITE(io, \"flash_gun\", arm->flash_gun);\n    M_WriteXYZ16(io, \"rot\", arm->rot);\n    JSONW_POP_AND_SET(io, key);\n}\n\nstatic void M_WriteAmmo(\n    JSON_WRITE_IO *const io, const char *const key, const AMMO_INFO *const ammo)\n{\n    ASSERT(ammo != nullptr);\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"ammo\", ammo->ammo);\n    JSONW_POP_AND_SET(io, key);\n}\n\nstatic void M_WriteLOT(JSON_WRITE_IO *const io, const LOT_INFO *const lot)\n{\n    ASSERT(lot != nullptr);\n    JSONW_WRITE(io, \"head\", lot->head);\n    JSONW_WRITE(io, \"tail\", lot->tail);\n    JSONW_WRITE(io, \"search_num\", lot->search_num);\n    JSONW_WRITE(io, \"block_mask\", lot->setup.block_mask);\n    JSONW_WRITE(io, \"step\", lot->setup.step);\n    JSONW_WRITE(io, \"drop\", lot->setup.drop);\n    JSONW_WRITE(io, \"fly\", lot->setup.fly);\n    JSONW_WRITE(io, \"zone_count\", lot->zone_count);\n    JSONW_WRITE(io, \"target_box\", lot->target_box);\n    JSONW_WRITE(io, \"required_box\", lot->required_box);\n    JSONW_WRITE(io, \"x\", lot->target.x);\n    JSONW_WRITE(io, \"y\", lot->target.y);\n    JSONW_WRITE(io, \"z\", lot->target.z);\n}\n\nstatic void M_WriteResumeInfo(\n    JSON_WRITE_IO *const io, const RESUME_INFO *const resume)\n{\n    JSONW_WRITE(io, \"available\", resume->flags.available);\n    JSONW_WRITE(io, \"level_completed\", resume->level_completed);\n    JSONW_WRITE(io, \"prev_level\", resume->prev_level);\n\n    JSONW_WRITE(io, \"hurt_allies\", resume->hurt_allies);\n\n    JSONW_WRITE(io, \"lara_hitpoints\", resume->lara_hitpoints);\n    JSONW_WRITE(io, \"pistol_ammo\", resume->pistol_ammo);\n    JSONW_WRITE(io, \"shotgun_ammo\", resume->shotgun_ammo);\n    JSONW_WRITE(io, \"magnum_ammo\", resume->magnum_ammo);\n    JSONW_WRITE(io, \"autos_ammo\", resume->autos_ammo);\n    JSONW_WRITE(io, \"desert_eagle_ammo\", resume->desert_eagle_ammo);\n    JSONW_WRITE(io, \"uzi_ammo\", resume->uzi_ammo);\n    JSONW_WRITE(io, \"m16_ammo\", resume->m16_ammo);\n    JSONW_WRITE(io, \"mp5_ammo\", resume->mp5_ammo);\n    JSONW_WRITE(io, \"grenade_ammo\", resume->grenade_ammo);\n    JSONW_WRITE(io, \"rocket_ammo\", resume->rocket_ammo);\n    JSONW_WRITE(io, \"harpoon_ammo\", resume->harpoon_ammo);\n    JSONW_WRITE(io, \"num_medis\", resume->small_medipacks);\n    JSONW_WRITE(io, \"num_big_medis\", resume->large_medipacks);\n    JSONW_WRITE(io, \"num_flares\", resume->flares);\n    JSONW_WRITE(io, \"num_scions\", resume->num_scions);\n    JSONW_WRITE(io, \"num_quest_item_1\", resume->num_quest_item_1);\n    JSONW_WRITE(io, \"num_quest_item_2\", resume->num_quest_item_2);\n    JSONW_WRITE(io, \"num_quest_item_3\", resume->num_quest_item_3);\n    JSONW_WRITE(io, \"num_quest_item_4\", resume->num_quest_item_4);\n    JSONW_WRITE(io, \"gun_status\", resume->gun_status);\n    JSONW_WRITE(io, \"gun_type\", resume->equipped_gun_type);\n    JSONW_WRITE(io, \"holsters_gun_type\", resume->holsters_gun_type);\n    JSONW_WRITE(io, \"back_gun_type\", resume->back_gun_type);\n\n    JSONW_WRITE(io, \"has_pistols\", resume->flags.has_pistols);\n    JSONW_WRITE(io, \"has_shotgun\", resume->flags.has_shotgun);\n    JSONW_WRITE(io, \"has_magnums\", resume->flags.has_magnums);\n    JSONW_WRITE(io, \"has_autos\", resume->flags.has_autos);\n    JSONW_WRITE(io, \"has_desert_eagle\", resume->flags.has_desert_eagle);\n    JSONW_WRITE(io, \"has_uzis\", resume->flags.has_uzis);\n    JSONW_WRITE(io, \"has_m16\", resume->flags.has_m16);\n    JSONW_WRITE(io, \"has_mp5\", resume->flags.has_mp5);\n    JSONW_WRITE(io, \"has_grenade\", resume->flags.has_grenade);\n    JSONW_WRITE(io, \"has_rocket\", resume->flags.has_rocket);\n    JSONW_WRITE(io, \"has_harpoon\", resume->flags.has_harpoon);\n\n    JSONW_WRITE(io, \"costume\", resume->flags.costume);\n    JSONW_WRITE(io, \"timer\", resume->stats.timer);\n    JSONW_WRITE(io, \"kills\", resume->stats.kill_count);\n    JSONW_WRITE(io, \"secrets\", resume->stats.secret_flags);\n    JSONW_WRITE(io, \"crystals\", resume->stats.crystal_count);\n    JSONW_WRITE(io, \"pickups\", resume->stats.pickup_count);\n    JSONW_WRITE(io, \"ammo_hits\", resume->stats.ammo_hits);\n    JSONW_WRITE(io, \"ammo_used\", resume->stats.ammo_used);\n    JSONW_WRITE(io, \"distance_travelled\", resume->stats.distance_travelled);\n    JSONW_WRITE(io, \"medipacks_used\", resume->stats.medipacks_used);\n    JSONW_WRITE(io, \"death_count\", resume->stats.death_count);\n}\n\nstatic int32_t M_GetMusicTrackFlagsCount(void)\n{\n    int32_t last_index = -1;\n    for (int32_t i = 0; i < MAX_MUSIC_TRACKS; i++) {\n        const uint16_t flags = Music_GetTrackFlags(i);\n        if (flags != 0) {\n            last_index = i;\n        }\n    }\n    return last_index + 1;\n}\n\nstatic void M_WriteFXRings(\n    JSON_WRITE_IO *const io, const FX_RING_TYPE type, const char *const key)\n{\n    if (!FX_Ring_IsRingActive(type)) {\n        return;\n    }\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0;; i++) {\n        const FX_RING *const ring = FX_Ring_PeekRing(type, i);\n        if (ring == nullptr) {\n            break;\n        }\n\n        JSONW_PUSH_OBJECT(io);\n        JSONW_WRITE(io, \"on\", ring->on);\n        JSONW_WRITE(io, \"life\", ring->life);\n        JSONW_WRITE(io, \"speed\", ring->speed);\n        JSONW_WRITE(io, \"radius\", ring->radius);\n        JSONW_WRITE(io, \"prev_radius\", ring->prev_radius);\n        M_WriteXYZ16(io, \"rot\", (XYZ_16) { ring->rot.x, 0, ring->rot.z });\n        M_WriteXYZ16(\n            io, \"prev_rot\", (XYZ_16) { ring->prev_rot.x, 0, ring->prev_rot.z });\n        M_WriteXYZ32(io, \"pos\", ring->pos);\n        M_WriteXYZ32(io, \"prev_pos\", ring->prev_pos);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET_NZ(io, key);\n}\n\nvoid SG_File_DumpFlares(JSON_WRITE_IO *const io)\n{\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        const ITEM *const item = Item_Get(i);\n        if (!item->active || item->object_id != O_FLARE_ITEM) {\n            continue;\n        }\n        JSONW_PUSH_OBJECT(io);\n        M_WriteXYZ32(io, \"pos\", item->pos);\n        M_WriteXYZ16(io, \"rot\", item->rot);\n        JSONW_WRITE(io, \"room_num\", item->room_num);\n        JSONW_WRITE(io, \"speed\", item->speed);\n        JSONW_WRITE(io, \"fall_speed\", item->fall_speed);\n        const int32_t flare_age = FlareItem_GetAge(item);\n        const int32_t active = FlareItem_IsActive(item) ? 0x8000 : 0;\n        JSONW_WRITE(io, \"age\", flare_age | active);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"flares\");\n}\n\nvoid SG_File_DumpEffects(JSON_WRITE_IO *const io)\n{\n    M_FX_ORDER fx_order;\n    M_GetFXOrder(&fx_order);\n\n    JSONW_PUSH_ARRAY(io);\n    for (int16_t link_num = Effect_GetActiveNum(); link_num != NO_ITEM;\n         link_num = Effect_Get(link_num)->next_active) {\n        EFFECT *const effect = Effect_Get(link_num);\n        if (Object_ToGameID(effect->object_id) == -1) {\n            continue;\n        }\n        JSONW_PUSH_OBJECT(io);\n        M_WriteXYZ32(io, \"pos\", effect->pos);\n        M_WriteXYZ16(io, \"rot\", effect->rot);\n        JSONW_WRITE(io, \"room_num\", effect->room_num);\n        JSONW_WRITE(io, \"object_id\", Object_ToGameID(effect->object_id));\n        JSONW_WRITE(io, \"speed\", effect->speed);\n        JSONW_WRITE(io, \"fall_speed\", effect->fall_speed);\n        // Introduced in TRX 1.2\n        JSONW_WRITE(io, \"frame_num\", effect->frame_num);\n        JSONW_WRITE(io, \"frame_number\", effect->frame_num);\n        JSONW_WRITE(io, \"counter\", effect->counter);\n        JSONW_WRITE(io, \"shade\", effect->shade);\n        JSONW_WRITE(io, \"flag1\", effect->flag1);\n        JSONW_WRITE(io, \"flag2\", effect->flag2);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"effects\");\n}\n\nvoid SG_File_DumpFX(JSON_WRITE_IO *const io)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_PUSH_OBJECT(io);\n    M_WriteFXRings(io, FX_RING_TYPE_BLAST, \"blast\");\n    M_WriteFXRings(io, FX_RING_TYPE_KNOCKBACK, \"knockback\");\n    M_WriteFXRings(io, FX_RING_TYPE_SUMMON, \"summon\");\n    JSONW_POP_AND_SET_NZ(io, \"rings\");\n    JSONW_POP_AND_SET_NZ(io, \"vfx\");\n}\n\nvoid SG_File_DumpInventory(JSON_WRITE_IO *const io)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"pickup1\", Inv_RequestItem(O_PICKUP_ITEM_1));\n    JSONW_WRITE(io, \"pickup2\", Inv_RequestItem(O_PICKUP_ITEM_2));\n    JSONW_WRITE(io, \"quest1\", Inv_RequestItem(O_QUEST_ITEM_1));\n    JSONW_WRITE(io, \"quest2\", Inv_RequestItem(O_QUEST_ITEM_2));\n    JSONW_WRITE(io, \"quest3\", Inv_RequestItem(O_QUEST_ITEM_3));\n    JSONW_WRITE(io, \"quest4\", Inv_RequestItem(O_QUEST_ITEM_4));\n    JSONW_WRITE(io, \"puzzle1\", Inv_RequestItem(O_PUZZLE_ITEM_1));\n    JSONW_WRITE(io, \"puzzle2\", Inv_RequestItem(O_PUZZLE_ITEM_2));\n    JSONW_WRITE(io, \"puzzle3\", Inv_RequestItem(O_PUZZLE_ITEM_3));\n    JSONW_WRITE(io, \"puzzle4\", Inv_RequestItem(O_PUZZLE_ITEM_4));\n    JSONW_WRITE(io, \"key1\", Inv_RequestItem(O_KEY_ITEM_1));\n    JSONW_WRITE(io, \"key2\", Inv_RequestItem(O_KEY_ITEM_2));\n    JSONW_WRITE(io, \"key3\", Inv_RequestItem(O_KEY_ITEM_3));\n    JSONW_WRITE(io, \"key4\", Inv_RequestItem(O_KEY_ITEM_4));\n    JSONW_WRITE(io, \"leadbar\", Inv_RequestItem(O_LEADBAR_ITEM));\n    JSONW_POP_AND_SET(io, \"inventory\");\n}\n\nvoid SG_File_DumpFlipmaps(JSON_WRITE_IO *const io)\n{\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"status\", Room_GetFlipStatus());\n    JSONW_WRITE(io, \"effect\", Room_GetFlipEffect());\n    JSONW_WRITE(io, \"timer\", Room_GetFlipTimer());\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < MAX_FLIP_MAPS; i++) {\n        JSONW_PUSH_VALUE(io, Room_GetFlipSlotFlags(i) >> 8);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"table\");\n    JSONW_POP_AND_SET(io, \"flipmap\");\n}\n\nvoid SG_File_DumpCameras(JSON_WRITE_IO *const io)\n{\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < Camera_GetFixedObjectCount(); i++) {\n        const OBJECT_VECTOR *const object = Camera_GetFixedObject(i);\n        JSONW_PUSH_VALUE(io, object->flags);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"cameras\");\n}\n\nvoid SG_File_DumpMusic(JSON_WRITE_IO *const io)\n{\n    const int32_t track_flag_count = M_GetMusicTrackFlagsCount();\n    JSONW_PUSH_OBJECT(io);\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < track_flag_count; i++) {\n        JSONW_PUSH_VALUE(io, Music_GetTrackFlags(i));\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"flags\");\n\n    const MUSIC_ID current_ambient = Music_GetCurrentLoopedTrack();\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"current_ambient\", current_ambient);\n    JSONW_PUSH_ARRAY(io);\n    const int32_t stream_count = Music_GetStreamCount();\n    for (int32_t i = 0; i < stream_count; i++) {\n        MUSIC_STREAM_STATE state = {};\n        if (!Music_GetStreamState(i, &state)) {\n            continue;\n        }\n\n        JSONW_PUSH_OBJECT(io);\n        JSONW_WRITE(io, \"track\", state.track_id);\n        JSONW_WRITE(io, \"mode\", state.mode);\n        JSONW_WRITE(io, \"timestamp\", state.timestamp);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"streams\");\n    JSONW_POP_AND_SET(io, \"current\");\n    JSONW_POP_AND_SET(io, \"music\");\n}\n\nvoid SG_File_DumpItems(JSON_WRITE_IO *const io)\n{\n    Savegame_ProcessItemsBeforeSave();\n    M_FX_ORDER fx_order;\n    M_GetFXOrder(&fx_order);\n\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < Item_GetLevelCount(); i++) {\n        JSONW_PUSH_OBJECT(io);\n        M_WriteItem(io, Item_Get(i), &fx_order);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"items\");\n}\n\nvoid SG_File_DumpLara(JSON_WRITE_IO *const io)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    ASSERT(lara != nullptr);\n\n    JSONW_PUSH_OBJECT(io);\n\n    // Introduced in TRX 1.2\n    JSONW_WRITE(io, \"item_num\", lara->item_num);\n    JSONW_WRITE(io, \"item_number\", lara->item_num);\n    JSONW_WRITE(io, \"gun_status\", lara->gun_status);\n    JSONW_WRITE(io, \"gun_type\", lara->gun_type);\n    JSONW_WRITE(io, \"request_gun_type\", lara->request_gun_type);\n    JSONW_WRITE(io, \"last_gun_type\", lara->last_gun_type);\n\n    JSONW_WRITE(io, \"calc_fall_speed\", lara->calc_fall_speed);\n    JSONW_WRITE(io, \"water_status\", lara->water_status);\n    JSONW_WRITE(io, \"climb_status\", lara->climb_status);\n    JSONW_WRITE(io, \"is_crouched\", lara->is_crouched);\n    JSONW_WRITE(io, \"keep_crouched\", lara->keep_crouched);\n    JSONW_WRITE(io, \"sprinting\", lara->sprinting);\n\n    JSONW_WRITE(io, \"pose_count\", lara->pose_count);\n    JSONW_WRITE(io, \"hit_frame\", lara->hit_frame);\n    JSONW_WRITE(io, \"hit_direction\", lara->hit_direction);\n    JSONW_WRITE(io, \"hit_effect_count\", lara->hit_effect_count);\n    JSONW_WRITE(\n        io, \"hit_effect\",\n        lara->hit_effect ? Effect_GetIndex(lara->hit_effect) : 0);\n\n    JSONW_WRITE(io, \"air\", lara->air);\n    JSONW_WRITE(io, \"sprint_timer\", lara->sprint_timer);\n    JSONW_WRITE(io, \"exposure_timer\", lara->exposure_timer);\n    JSONW_WRITE(io, \"poison_timer\", lara->poison_timer);\n    JSONW_WRITE(io, \"dive_count\", lara->dive_timer);\n    JSONW_WRITE(io, \"death_count\", lara->death_timer);\n\n    JSONW_WRITE(io, \"current_active\", lara->current.active);\n    JSONW_WRITE(io, \"current_vel_x\", lara->current.vel.x);\n    JSONW_WRITE(io, \"current_vel_z\", lara->current.vel.z);\n    JSONW_WRITE(io, \"burn\", lara->burn);\n    JSONW_WRITE(io, \"electric\", lara->electric);\n    JSONW_WRITE(io, \"water_surface_dist\", lara->water_surface_dist);\n\n    JSONW_WRITE(io, \"flare_age\", lara->flare.age);\n    JSONW_WRITE(io, \"flare_frame\", lara->flare.frame_num);\n    JSONW_WRITE(io, \"flare_control_left\", lara->flare.control);\n    JSONW_WRITE(io, \"extra_anim\", lara->extra_anim);\n    // Introduced in TRX 1.2\n    JSONW_WRITE(io, \"vehicle_item_num\", Lara_Vehicle_GetIndex());\n    JSONW_WRITE(io, \"vehicle_item_number\", Lara_Vehicle_GetIndex());\n\n    JSONW_WRITE(io, \"mesh_effects\", lara->mesh_effects);\n\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"skin_type\", Lara_Skin_GetType());\n    JSONW_WRITE(io, \"skin_is_default\", Lara_Skin_IsDefaultType());\n    JSONW_WRITE(io, \"holsters_visible\", Lara_Skin_AreHolstersVisible());\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < LM_NUMBER_OF; i++) {\n        const LARA_SKIN_EQUIPMENT *const equipment = Lara_Skin_GetEquipment(i);\n        JSONW_PUSH_OBJECT(io);\n        JSONW_WRITE(io, \"type\", equipment->type);\n        JSONW_WRITE(io, \"data\", equipment->data);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"equipment\");\n    JSONW_POP_AND_SET(io, \"skin\");\n\n    JSONW_WRITE(io, \"target_angle1\", lara->target_angles[0]);\n    JSONW_WRITE(io, \"target_angle2\", lara->target_angles[1]);\n    JSONW_WRITE(io, \"turn_rate\", lara->turn_rate);\n    JSONW_WRITE(io, \"move_angle\", lara->move_angle);\n    M_WriteXYZ16(io, \"head_rot\", lara->head_rot);\n    M_WriteXYZ16(io, \"torso_rot\", lara->torso_rot);\n    M_WriteXYZ32(io, \"last_pos\", lara->last_pos);\n    M_WriteArm(io, \"left_arm\", &lara->left_arm);\n    M_WriteArm(io, \"right_arm\", &lara->right_arm);\n    M_WriteAmmo(io, \"pistols\", &lara->pistol_ammo);\n    M_WriteAmmo(io, \"shotgun\", &lara->shotgun_ammo);\n    M_WriteAmmo(io, \"magnums\", &lara->magnum_ammo);\n    M_WriteAmmo(io, \"autos\", &lara->autos_ammo);\n    M_WriteAmmo(io, \"desert_eagle\", &lara->desert_eagle_ammo);\n    M_WriteAmmo(io, \"uzis\", &lara->uzi_ammo);\n    M_WriteAmmo(io, \"harpoon\", &lara->harpoon_ammo);\n    M_WriteAmmo(io, \"grenade\", &lara->grenade_ammo);\n    M_WriteAmmo(io, \"rocket\", &lara->rocket_ammo);\n    M_WriteAmmo(io, \"m16\", &lara->m16_ammo);\n    M_WriteAmmo(io, \"mp5\", &lara->mp5_ammo);\n\n    if (lara->gun_item_num != NO_ITEM) {\n        JSONW_PUSH_OBJECT(io);\n        const ITEM *const weapon_item = Item_Get(lara->gun_item_num);\n        JSONW_WRITE(io, \"object_id\", Object_ToGameID(weapon_item->object_id));\n        JSONW_WRITE(io, \"anim_num\", weapon_item->anim_num);\n        JSONW_WRITE(io, \"frame_num\", weapon_item->frame_num);\n        JSONW_WRITE(io, \"current_anim_state\", weapon_item->current_anim_state);\n        JSONW_WRITE(io, \"goal_anim_state\", weapon_item->goal_anim_state);\n        JSONW_POP_AND_SET(io, \"weapon\");\n    }\n\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"item_num\", lara->interact_target.item_num);\n    JSONW_WRITE(io, \"move_count\", lara->interact_target.move_count);\n    JSONW_WRITE(io, \"is_moving\", lara->interact_target.is_moving);\n    JSONW_POP_AND_SET(io, \"interact_target\");\n\n    JSONW_POP_AND_SET(io, \"lara\");\n}\n\nvoid SG_File_DumpResumeInfoList(JSON_WRITE_IO *const io)\n{\n    const int32_t count = GF_GetLevelTable(GFLT_MAIN)->count;\n    JSONW_PUSH_ARRAY(io);\n    for (int32_t i = 0; i < count; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        JSONW_PUSH_OBJECT(io);\n        M_WriteResumeInfo(io, resume);\n        JSONW_POP_AND_APPEND(io);\n    }\n    JSONW_POP_AND_SET(io, \"resume_info\");\n}\n\nvoid SG_File_DumpMisc(JSON_WRITE_IO *const io)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n\n    JSONW_PUSH_OBJECT(io);\n    JSONW_WRITE(io, \"game_version\", g_TRXVersion);\n    JSONW_WRITE(io, \"bonus_flag\", Game_GetBonusFlag());\n    JSONW_WRITE(io, \"death_count\", resume->stats.death_count);\n    JSONW_WRITE(io, \"are_monks_angry\", Creature_AreAlliesHostile());\n    JSONW_WRITE(io, \"sunset_timer\", Output_GetTimeInGame());\n    JSONW_WRITE(io, \"rng_control_seed\", Random_GetControlSeed());\n    JSONW_WRITE(io, \"rng_draw_seed\", Random_GetDrawSeed());\n    JSONW_WRITE(io, \"weather_type\", FX_Weather_GetWeather());\n    JSONW_POP_AND_SET(io, \"misc\");\n\n    JSONW_WRITE(io, \"level_title\", level->title != nullptr ? level->title : \"\");\n    JSONW_WRITE(io, \"save_counter\", Savegame_GetCounter());\n    JSONW_WRITE(io, \"level_num\", level->num);\n}\n"
  },
  {
    "path": "src/trx/game/savegame/types.h",
    "content": "#pragma once\n\n#include <trx/core/filesystem.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/savegame/enum.h>\n#include <trx/game/stats/types.h>\n\ntypedef struct {\n    uint8_t small_medipacks;\n    uint8_t large_medipacks;\n    uint16_t pistol_ammo;\n    uint16_t magnum_ammo;\n    uint16_t autos_ammo;\n    uint16_t desert_eagle_ammo;\n    uint16_t uzi_ammo;\n    uint16_t shotgun_ammo;\n    int32_t lara_hitpoints;\n    LARA_GUN_STATE gun_status;\n    LARA_GUN_TYPE equipped_gun_type;\n    LARA_GUN_TYPE holsters_gun_type;\n    LARA_GUN_TYPE back_gun_type;\n    uint8_t num_scions;\n    uint8_t num_quest_item_1;\n    uint8_t num_quest_item_2;\n    uint8_t num_quest_item_3;\n    uint8_t num_quest_item_4;\n    uint16_t m16_ammo;\n    uint16_t mp5_ammo;\n    uint16_t grenade_ammo;\n    uint16_t rocket_ammo;\n    uint16_t harpoon_ammo;\n    uint16_t flares;\n\n    struct {\n        bool available;\n        bool costume;\n        bool has_pistols;\n        bool has_magnums;\n        bool has_autos;\n        bool has_desert_eagle;\n        bool has_uzis;\n        bool has_shotgun;\n        bool has_m16;\n        bool has_mp5;\n        bool has_grenade;\n        bool has_rocket;\n        bool has_harpoon;\n    } flags;\n\n    bool level_completed;\n    int32_t prev_level;\n    bool hurt_allies;\n\n    LEVEL_STATS stats;\n} RESUME_INFO;\n\ntypedef enum {\n    SAVEGAME_SLOT_POOL_NORMAL = 0,\n    SAVEGAME_SLOT_POOL_QUICK = 1,\n    SAVEGAME_SLOT_POOL_NUMBER_OF,\n} SAVEGAME_SLOT_POOL;\n\ntypedef struct {\n    SAVEGAME_SLOT_POOL pool;\n    int32_t index;\n} SAVEGAME_SLOT_REF;\n\ntypedef struct {\n    char *full_path;\n    int32_t counter;\n    int32_t level_num;\n    char *level_title;\n    int16_t initial_version;\n    bool is_quick;\n    struct {\n        bool restart;\n        bool select_level;\n    } features;\n} SAVEGAME_INFO;\n"
  },
  {
    "path": "src/trx/game/savegame.h",
    "content": "#pragma once\n\n#include <trx/game/savegame/common.h>\n#include <trx/game/savegame/enum.h>\n#include <trx/game/savegame/types.h>\n"
  },
  {
    "path": "src/trx/game/screenshot.c",
    "content": "#include <trx/game/screenshot.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/clock.h>\n#include <trx/game/events.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/output.h>\n#include <trx/game/shell.h>\n\n#include <stdio.h>\n#include <string.h>\n\nstatic char *M_CleanScreenshotTitle(const char *const source)\n{\n    // Sanitize screenshot title.\n    // - Remove filesystem-sensitive characters\n    // - Replace spaces with underscores\n    // - Merge consecutive underscores together\n    // - Remove leading underscores\n    // - Remove trailing underscores\n    char *result = Memory_Alloc(strlen(source) + 1);\n    const char *const sensitive_characters = \"/\\\\:*?\\\"<>|\";\n\n    bool last_was_underscore = false;\n    char *out = result;\n    for (size_t i = 0; i < strlen(source); i++) {\n        if (source[i] == ' ' || source[i] == '_') {\n            if (!last_was_underscore && out > result) {\n                *out++ = '_';\n                last_was_underscore = true;\n            }\n            continue;\n        }\n\n        const size_t char_size = String_GetCharByteSize(out);\n        if (char_size != 1\n            || strchr(sensitive_characters, source[i]) == nullptr) {\n            memcpy(out, source + i, char_size);\n            out += char_size;\n            i += char_size - 1;\n            last_was_underscore = false;\n        }\n    }\n    *out++ = '\\0';\n\n    // Strip trailing underscores\n    while (out > result && out[-1] == '_') {\n        out--;\n    }\n    *out = '\\0';\n\n    return result;\n}\n\nstatic char *M_GetScreenshotTitle(void)\n{\n    const GF_LEVEL *const level = GF_GetCurrentLevel();\n    if (level == nullptr) {\n        return Memory_DupStr(\"Intro\");\n    }\n\n    if (level->title != nullptr && strlen(level->title) > 0) {\n        char *clean_level_title = M_CleanScreenshotTitle(level->title);\n        if (clean_level_title != nullptr && strlen(clean_level_title) > 0) {\n            return clean_level_title;\n        }\n        Memory_FreePointer(&clean_level_title);\n    }\n\n    // If title totally invalid, name it based on level number\n    const char *const fmt = \"Level_%d\";\n    const size_t result_size = snprintf(nullptr, 0, fmt, level->num) + 1;\n    char *result = Memory_Alloc(result_size);\n    snprintf(result, result_size, fmt, level->num);\n    return result;\n}\n\nstatic char *M_GetScreenshotBaseName(void)\n{\n    char *screenshot_title = M_GetScreenshotTitle();\n\n    // Get timestamp\n    char date_time[30];\n    Clock_GetDateTime(date_time, 30);\n\n    // Full screenshot name\n    char *const result = String_Format(\"%s_%s\", date_time, screenshot_title);\n    Memory_FreePointer(&screenshot_title);\n    return result;\n}\n\nstatic const char *M_GetScreenshotFileExt(const SCREENSHOT_FORMAT format)\n{\n    switch (format) {\n    case SCREENSHOT_FORMAT_JPEG:\n        return \"jpg\";\n    case SCREENSHOT_FORMAT_PNG:\n        return \"png\";\n    default:\n        return \"jpg\";\n    }\n}\n\nstatic char *M_GetScreenshotPath(const SCREENSHOT_FORMAT format)\n{\n    char *base_name = M_GetScreenshotBaseName();\n    const char *const ext = M_GetScreenshotFileExt(format);\n    char *rel_path = String_Format(\"%s.%s\", base_name, ext);\n\n    char *full_path = Memory_DupStr(\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE, rel_path));\n    Memory_FreePointer(&rel_path);\n    File_EnsureParentDirectories(full_path);\n    if (File_Exists(full_path)) {\n        for (int i = 2; i < 100; i++) {\n            Memory_FreePointer(&full_path);\n            rel_path = String_Format(\"%s_%d.%s\", base_name, i, ext);\n            full_path = Memory_DupStr(TRXPath_Resolve(\n                TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE, rel_path));\n            Memory_FreePointer(&rel_path);\n            if (!File_Exists(full_path)) {\n                break;\n            }\n        }\n    }\n\n    Memory_FreePointer(&base_name);\n    return full_path;\n}\n\nvoid Screenshot_Make(const SCREENSHOT_FORMAT format)\n{\n    char *full_path = M_GetScreenshotPath(format);\n    Output_MakeScreenshot(full_path);\n    GameEvent_Fire((EVENT) {\n        .name = GAME_EVENT_SCREENSHOT,\n        .data = full_path,\n    });\n    Memory_FreePointer(&full_path);\n}\n\nvoid Screenshot_MakeToPath(const char *const path)\n{\n    Output_MakeScreenshot(path);\n}\n"
  },
  {
    "path": "src/trx/game/screenshot.h",
    "content": "#pragma once\n\n#include <trx/config/enum.h>\n\nvoid Screenshot_Make(SCREENSHOT_FORMAT format);\nvoid Screenshot_MakeToPath(const char *path);\n"
  },
  {
    "path": "src/trx/game/shell/args.c",
    "content": "#include <trx/game/shell/args.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/core/virtual_file.h>\n#include <trx/debug.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/shell/common.h>\n#include <trx/game/shell/paths.h>\n#include <trx/version.h>\n\n#include <stdio.h>\n#include <string.h>\n\nstatic void M_FreeArgVector(VECTOR *const args)\n{\n    if (args == nullptr) {\n        return;\n    }\n\n    for (int32_t i = 0; i < args->count; i++) {\n        const char *const arg = *(char **)Vector_Get(args, i);\n        Memory_Free((char *)arg);\n    }\n    Vector_Free(args);\n}\n\nstatic void M_ShowHelp(void)\n{\n    puts(\"Available options:\");\n    puts(\"\");\n    puts(\"-h/--help: show this help.\");\n    puts(\"   --mod <MOD_ID>: launch a specific game or mod directly.\");\n    puts(\"-e/--engine <1|2|3>: pick a game engine explicitly.\");\n    puts(\"-l/--level <PATH|NUM>: launch a level file or level number.\");\n    puts(\"-s/--save <NUM>: launch from a specific save slot (starts at 1).\");\n    puts(\"   --test-record <PATH>: record gameplay events to file.\");\n    puts(\n        \"   --test-replay/--test-play <PATH>: replay gameplay events from \"\n        \"file.\");\n    puts(\"   --headless: replay gameplay without showing a game window.\");\n    puts(\n        \"   --headless-fps <NUM>: control replay frame rate in headless mode.\");\n    puts(\"-q/--quiet: silence logs and only show errors.\");\n    puts(\n        \"   --debug-render-performance: output diagnostic information after \"\n        \"each \"\n        \"frame.\");\n    puts(\"\");\n\n    puts(\"Available mods:\");\n    for (int32_t i = 0; i < Shell_GetModCount(); i++) {\n        const SHELL_MOD *const mod = Shell_GetMod(i);\n        if (mod == nullptr || !mod->is_available) {\n            continue;\n        }\n        if (mod->mod_type == MOD_DIRECT_LEVEL) {\n            continue;\n        }\n        if (mod->title != nullptr && strcmp(mod->title, mod->name) != 0) {\n            printf(\"  %s (%s)\\n\", mod->name, mod->title);\n        } else {\n            printf(\"  %s\\n\", mod->name);\n        }\n    }\n    puts(\"\");\n\n    puts(\"Legacy options:\");\n    puts(\"-g/--gold/-gold: launch the matching Gold expansion pack.\");\n    if (Shell_GetModByName(\"tr1-demo-pc\") != nullptr) {\n        puts(\"   --demo-pc/-demo_pc: launch the TR1 PC demo.\");\n    }\n    puts(\"These options are deprecated; please use --mod instead.\");\n}\n\nstatic int32_t M_GuessEngineVersionFromLevelPath(const char *const path)\n{\n    if (path == nullptr || !File_Exists(path)) {\n        return 0;\n    }\n\n    VFILE *const file = VFile_CreateFromPath(path);\n    if (file == nullptr) {\n        return 0;\n    }\n\n    const LEVEL_FORMAT_LOADER *const loader = Level_Format_GuessLoader(file);\n    const int32_t game_version = loader != nullptr ? loader->game_version : 0;\n    VFile_Close(file);\n    return game_version;\n}\n\nSHELL_ARGS *Shell_ParseArgs(VECTOR *const args)\n{\n    SHELL_ARGS *const result = Memory_Alloc(sizeof(SHELL_ARGS));\n    bool wants_gold = false;\n    bool explicit_engine_version = false;\n    result->save_to_load = -1;\n    result->level_to_select = -1;\n    result->original_args = args;\n    result->engine_version = 0;\n\n    // First pass: set the engine version.\n    for (int32_t i = 0; args != nullptr && i < args->count; i++) {\n        const char *const arg = *(char **)Vector_Get(args, i);\n        const char *const next_arg =\n            i + 1 < args->count ? *(char **)Vector_Get(args, i + 1) : nullptr;\n        if (!strcmp(arg, \"-e\") || !strcmp(arg, \"--engine\")) {\n            String_ParseInteger(next_arg, &result->engine_version);\n            CLAMP(result->engine_version, 1, 3);\n            explicit_engine_version = true;\n            i++;\n        }\n    }\n    if (result->engine_version <= 0 && g_TRVersion > 0) {\n        // Hydrate recordings using old-style directory tree to use\n        // runtime engine version if they miss it.\n        result->engine_version = g_TRVersion;\n    }\n\n    // Second pass: remaining options.\n    for (int32_t i = 0; args != nullptr && i < args->count; i++) {\n        const char *const arg = *(char **)Vector_Get(args, i);\n        const char *const next_arg =\n            i + 1 < args->count ? *(char **)Vector_Get(args, i + 1) : nullptr;\n\n        if (!strcmp(arg, \"-e\") || !strcmp(arg, \"--engine\")) {\n            i++;\n        }\n\n        if (!strcmp(arg, \"-h\") || !strcmp(arg, \"--help\")) {\n            M_ShowHelp();\n            Shell_FreeArgs(result);\n            return nullptr;\n        }\n        if (!strcmp(arg, \"-g\") || !strcmp(arg, \"--gold\")\n            || !strcmp(arg, \"-gold\")) {\n            wants_gold = true;\n        }\n\n        if (!strcmp(arg, \"--demo-pc\") || !strcmp(arg, \"-demo_pc\")) {\n            result->mod = Shell_GetModByName(\"tr1-demo-pc\");\n        }\n        if (!strcmp(arg, \"--mod\") && next_arg != nullptr) {\n            const SHELL_MOD *const mod = Shell_GetModByName(next_arg);\n            if (mod != nullptr) {\n                result->mod = mod;\n            }\n            i++;\n        }\n\n        if ((!strcmp(arg, \"-l\") || !strcmp(arg, \"--level\"))\n            && next_arg != nullptr) {\n            int32_t lvnum = -1;\n            if (String_ParseInteger(next_arg, &lvnum)) {\n                result->level_to_select = lvnum;\n                if (result->mod == nullptr && result->engine_version > 0) {\n                    result->mod = Shell_GetModByType(\n                        MOD_BASE_GAME, result->engine_version);\n                }\n            } else {\n                char **const level_arg = Vector_Get(args, i + 1);\n                ASSERT(level_arg != nullptr);\n\n                const char *const resolved_level_path =\n                    TRXPath_PeekResolveUserPath(\n                        TRX_DYNAMIC_PATH_LEVEL_FILE, next_arg);\n                result->level_to_play = resolved_level_path != nullptr\n                    ? Memory_DupStr(resolved_level_path)\n                    : nullptr;\n                if (result->level_to_play == nullptr) {\n                    Shell_ExitSystemFmt(\n                        \"Cannot find level file '%s'. Relative paths are \"\n                        \"resolved from the current working directory, then \"\n                        \"from the game directory.\",\n                        next_arg);\n                }\n                Memory_Free(*level_arg);\n                *level_arg = (char *)result->level_to_play;\n\n                if (result->engine_version == 0) {\n                    result->engine_version = M_GuessEngineVersionFromLevelPath(\n                        result->level_to_play);\n                }\n                if (result->engine_version == 0) {\n                    Shell_ExitSystem(\n                        \"Cannot determine engine version for --level. \"\n                        \"Please provide --engine.\");\n                }\n                result->mod = Shell_GetModByType(\n                    MOD_DIRECT_LEVEL, result->engine_version);\n                if (result->mod == nullptr) {\n                    Shell_ExitSystemFmt(\n                        \"Engine %d does not support --level with a file path \"\n                        \"because no direct-level mod is available for that \"\n                        \"engine.\",\n                        result->engine_version);\n                }\n            }\n            i++;\n        }\n        if ((!strcmp(arg, \"-s\") || !strcmp(arg, \"--save\"))\n            && next_arg != nullptr) {\n            if (String_ParseInteger(next_arg, &result->save_to_load)) {\n                result->save_to_load--;\n            }\n            i++;\n        }\n        if (!strcmp(arg, \"--test-record\") && next_arg != nullptr) {\n            result->test_record_path = next_arg;\n            i++;\n        }\n        if ((!strcmp(arg, \"--test-play\") || !strcmp(arg, \"--test-replay\"))\n            && next_arg != nullptr) {\n            result->test_replay_path = next_arg;\n            i++;\n        }\n        if (!strcmp(arg, \"--headless\")) {\n            result->headless = true;\n        }\n        if (!strcmp(arg, \"--headless-fps\") && next_arg != nullptr) {\n            int32_t fps = 0;\n            if (String_ParseInteger(next_arg, &fps) && fps > 0) {\n                result->headless_fps = fps;\n            }\n            i++;\n        }\n        if (!strcmp(arg, \"--debug-render-performance\")) {\n            result->debug_render_performance = true;\n        }\n        if (!strcmp(arg, \"-q\") || !strcmp(arg, \"--quiet\")) {\n            result->quiet = true;\n        }\n    }\n\n    if (result->mod == nullptr) {\n        result->mod = Shell_SelectStartupMod(result->engine_version);\n    }\n    if (!explicit_engine_version && result->mod != nullptr) {\n        result->engine_version = result->mod->engine_version;\n    }\n    if (wants_gold) {\n        const int32_t engine_version = result->engine_version != 0\n            ? result->engine_version\n            : (result->mod != nullptr ? result->mod->engine_version : 0);\n        const SHELL_MOD *const gold_mod =\n            Shell_GetModByType(MOD_EXPANSION_PACK, engine_version);\n        if (gold_mod != nullptr) {\n            result->mod = gold_mod;\n            result->engine_version = gold_mod->engine_version;\n        }\n    }\n\n    return result;\n}\n\nvoid Shell_FreeArgs(SHELL_ARGS *const args)\n{\n    if (args == nullptr) {\n        return;\n    }\n\n    M_FreeArgVector(args->original_args);\n    args->original_args = nullptr;\n    Memory_Free(args);\n}\n"
  },
  {
    "path": "src/trx/game/shell/args.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n#include <trx/game/shell/mod.h>\n\ntypedef struct {\n    // Owned argv snapshot used as backing storage for pointer-valued fields\n    // (e.g. level/replay/test paths). Freed by Shell_FreeArgs.\n    VECTOR *original_args;\n\n    int32_t engine_version;\n    const SHELL_MOD *mod;\n    int32_t level_to_select;\n    const char *level_to_play;\n    int32_t save_to_load;\n    const char *test_record_path;\n    const char *test_replay_path;\n    bool headless;\n    bool debug_render_performance;\n    int32_t headless_fps; // in headless mode, force fixed fps (0 = unlocked)\n    bool quiet;\n} SHELL_ARGS;\n\n// Adopts `raw_args`.\n// Requirements:\n// - `raw_args` items must be `char *` allocated on heap (or nullptr).\n// - ownership of vector + strings transfers to returned SHELL_ARGS.\n// - caller must not free or mutate `raw_args` after this call.\nSHELL_ARGS *Shell_ParseArgs(VECTOR *raw_args);\n// Frees SHELL_ARGS and its adopted `original_args` vector + strings.\nvoid Shell_FreeArgs(SHELL_ARGS *args);\n"
  },
  {
    "path": "src/trx/game/shell/common.c",
    "content": "#include <trx/av/audio.h>\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/shell.h>\n\n#ifdef _WIN32\n    #include <objbase.h>\n    #include <windows.h>\n#endif\n\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_messagebox.h>\n#include <libavcodec/version.h>\n#include <libavutil/log.h>\n#include <stdio.h>\n\nstatic bool m_IsExiting = false;\nstatic bool m_IsFocused = true;\n\nstatic void M_ShowFatalError(\n    const char *const log_message, const char *const dialog_message)\n{\n    LOG_ERROR(\"%s\", log_message);\n    SDL_Window *const window = Shell_GetWindow();\n    SDL_ShowSimpleMessageBox(\n        SDL_MESSAGEBOX_ERROR, \"Tomb Raider Error\", dialog_message, window);\n    Shell_Terminate(1);\n}\n\nconst char *Shell_GetConfigDir(void)\n{\n    return TRXPath_Get(TRX_PATH_CONFIG_DIR);\n}\n\nconst char *Shell_GetCacheDir(void)\n{\n    return TRXPath_Get(TRX_PATH_CACHE_DIR);\n}\n\nvoid Shell_Terminate(int32_t exit_code)\n{\n    Shell_Shutdown();\n\n    SDL_Window *const window = Shell_GetWindow();\n    if (window != nullptr) {\n        SDL_DestroyWindow(window);\n    }\n    if (Audio_ShouldSkipSDLQuitAudio()) {\n        const Uint32 inited = SDL_WasInit(0);\n        const Uint32 quit_flags = inited & ~SDL_INIT_AUDIO;\n        if (quit_flags != 0) {\n            SDL_QuitSubSystem(quit_flags);\n        }\n    } else {\n        SDL_Quit();\n    }\n    exit(exit_code);\n}\n\nvoid Shell_ExitSystem(const char *message)\n{\n    M_ShowFatalError(message, message);\n    Shell_Shutdown();\n}\n\nvoid Shell_ExitSystemEx(\n    const char *const log_message, const char *const dialog_message)\n{\n    M_ShowFatalError(log_message, dialog_message);\n    Shell_Shutdown();\n}\n\nvoid Shell_ExitSystemFmt(const char *fmt, ...)\n{\n    va_list va;\n    va_start(va, fmt);\n    int32_t size = vsnprintf(nullptr, 0, fmt, va) + 1;\n    char *message = Memory_Alloc(size);\n    va_end(va);\n\n    va_start(va, fmt);\n    vsnprintf(message, size, fmt, va);\n    va_end(va);\n\n    Shell_ExitSystem(message);\n\n    Memory_FreePointer(&message);\n}\n\nbool Shell_IsFullscreen(void)\n{\n    SDL_Window *const window = Shell_GetWindow();\n    ASSERT(window != nullptr);\n    const Uint32 flags = SDL_GetWindowFlags(window);\n    return (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0;\n}\n\nSHELL_SIZE Shell_GetCurrentSize(void)\n{\n    return Shell_IsFullscreen() ? Shell_GetCurrentDisplaySize()\n                                : Shell_GetWindowSize();\n}\n\nSHELL_SIZE Shell_GetDefaultSize(void)\n{\n    return (SHELL_SIZE) { SHELL_HEADLESS_WIDTH, SHELL_HEADLESS_HEIGHT };\n}\n\nSHELL_SIZE Shell_GetWindowSize(void)\n{\n    if (Shell_GetArgs()->headless) {\n        return Shell_GetDefaultSize();\n    }\n    SDL_Window *const window = Shell_GetWindow();\n    SHELL_SIZE result = { .w = -1, .h = -1 };\n    if (window != nullptr) {\n        SDL_GetWindowSize(window, &result.w, &result.h);\n    }\n    return result;\n}\n\nSHELL_SIZE Shell_GetCurrentDisplaySize(void)\n{\n    if (Shell_GetArgs()->headless) {\n        return Shell_GetDefaultSize();\n    }\n    int32_t display_idx = 0;\n    SDL_Window *const window = Shell_GetWindow();\n    if (window != nullptr) {\n        display_idx = SDL_GetWindowDisplayIndex(window);\n    }\n    SDL_DisplayMode dm;\n    if (SDL_GetCurrentDisplayMode(display_idx, &dm) == 0) {\n        return (SHELL_SIZE) { .w = dm.w, .h = dm.h };\n    }\n    return (SHELL_SIZE) { .w = -1, .h = -1 };\n}\n\nvoid Shell_ScheduleExit(void)\n{\n    m_IsExiting = true;\n}\n\nbool Shell_IsExiting(void)\n{\n    return m_IsExiting;\n}\n\nvoid Shell_SetIsFocused(const bool is_focused)\n{\n    m_IsFocused = is_focused;\n}\n\nbool Shell_IsFocused(void)\n{\n    return m_IsFocused;\n}\n"
  },
  {
    "path": "src/trx/game/shell/common.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n#include <trx/core/event_manager.h>\n#include <trx/core/shell.h>\n#include <trx/game/shell/args.h>\n\n#include <SDL2/SDL_events.h>\n#include <stdint.h>\n\ntypedef struct {\n    int32_t w;\n    int32_t h;\n} SHELL_SIZE;\n\nvoid Shell_Shutdown(void);\n\nSDL_Window *Shell_GetWindow(void);\nconst char *Shell_GetConfigDir(void);\nconst char *Shell_GetCacheDir(void);\n\nint32_t Shell_Main(const SHELL_ARGS *args);\nvoid Shell_Terminate(int32_t exit_code);\n\nvoid Shell_ScheduleExit(void);\nbool Shell_IsExiting(void);\nvoid Shell_SetIsFocused(bool is_focused);\nbool Shell_IsFocused(void);\n\nvoid Shell_RequestModSwitch(const char *mod_name);\nconst char *Shell_GetPendingMod(void);\nvoid Shell_ClearPendingMod(void);\nbool Shell_GetPrevHeadless(void);\nbool Shell_GetPrevQuiet(void);\nconst SHELL_ARGS *Shell_GetArgs(void);\n\nbool Shell_IsFullscreen(void);\nSHELL_SIZE Shell_GetDefaultSize(void);\nSHELL_SIZE Shell_GetWindowSize(void);\nSHELL_SIZE Shell_GetCurrentSize(void);\nSHELL_SIZE Shell_GetCurrentDisplaySize(void);\n"
  },
  {
    "path": "src/trx/game/shell/config.c",
    "content": "#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/game/game_strings/manager.h>\n#include <trx/game/lara.h>\n#include <trx/game/music.h>\n#include <trx/game/output.h>\n#include <trx/game/replay/test_replay.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/viewport.h>\n\n#include <SDL2/SDL_timer.h>\n\nstatic Uint64 m_UpdateDebounce = 0;\nstatic bool m_IgnoreConfigChanges = false;\nstatic SHELL_SIZE m_ViewportSize = { .w = -1, .h = -1 };\n\nstatic bool M_MustUpdateRendererViewport(void)\n{\n    const SHELL_SIZE size = Shell_GetCurrentSize();\n    return m_ViewportSize.w != size.w || m_ViewportSize.h != size.h;\n}\n\nvoid Shell_RefreshRendererViewport(void)\n{\n    Viewport_Reset();\n    m_ViewportSize = Shell_GetCurrentSize();\n}\n\nvoid Shell_SyncToWindow(void)\n{\n    m_UpdateDebounce = SDL_GetTicks();\n\n    LOG_DEBUG(\n        \"is_fullscreen=%d is_maximized=%d x=%d y=%d width=%d height=%d\",\n        g_Config.window.is_fullscreen, g_Config.window.is_maximized,\n        g_Config.window.x, g_Config.window.y, g_Config.window.width,\n        g_Config.window.height);\n\n    SDL_Window *const window = Shell_GetWindow();\n    if (g_Config.window.is_fullscreen) {\n        SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);\n        SDL_ShowCursor(SDL_DISABLE);\n    } else if (g_Config.window.is_maximized) {\n        SDL_SetWindowFullscreen(window, 0);\n        SDL_MaximizeWindow(window);\n        SDL_ShowCursor(SDL_ENABLE);\n    } else {\n        int32_t x = g_Config.window.x;\n        int32_t y = g_Config.window.y;\n        int32_t width = g_Config.window.width;\n        int32_t height = g_Config.window.height;\n        if (width <= 0 || height <= 0) {\n            width = 1280;\n            height = 720;\n        }\n\n        // Handle default position\n        if (x == -1 && y == -1) {\n            SDL_DisplayMode display_mode;\n            SDL_GetCurrentDisplayMode(0, &display_mode);\n            x = (display_mode.w - width) / 2;\n            y = (display_mode.h - height) / 2;\n        } else {\n            // Adjust window position if completely offscreen\n            bool on_screen = false;\n            const int32_t num_displays = SDL_GetNumVideoDisplays();\n            for (int32_t i = 0; i < num_displays; i++) {\n                SDL_Rect bounds;\n                SDL_GetDisplayBounds(i, &bounds);\n                if (x + width > bounds.x && x < bounds.x + bounds.w\n                    && y + height > bounds.y && y < bounds.y + bounds.h) {\n                    on_screen = true;\n                    break;\n                }\n            }\n            if (!on_screen) {\n                x = 0;\n                y = 0;\n                // Find the first display to reposition the window\n                SDL_Rect bounds;\n                SDL_GetDisplayBounds(0, &bounds);\n                x = bounds.x + (bounds.w - width) / 2;\n                y = bounds.y + (bounds.h - height) / 2;\n            }\n        }\n\n        SDL_SetWindowFullscreen(window, 0);\n        SDL_SetWindowPosition(window, x, y);\n        SDL_SetWindowSize(window, width, height);\n        SDL_ShowCursor(SDL_ENABLE);\n    }\n}\n\nvoid Shell_SyncFromWindow(const bool update_viewport)\n{\n    // Determine if this call should sync config, i.e., skip immediate\n    // programmatic events\n    const Uint32 now = SDL_GetTicks();\n    const bool skip_config = (now - m_UpdateDebounce) < 500;\n\n    // Always pull current window state for logging and viewport reset\n    SDL_Window *const window = Shell_GetWindow();\n    const Uint32 window_flags = SDL_GetWindowFlags(window);\n    const bool is_maximized = window_flags & SDL_WINDOW_MAXIMIZED;\n    int32_t x, y;\n    int32_t width, height;\n    SDL_GetWindowSize(window, &width, &height);\n    SDL_GetWindowPosition(window, &x, &y);\n    LOG_TRACE(\"%dx%d+%d,%d (maximized: %d)\", width, height, x, y, is_maximized);\n\n    // Update config only when not in debounce window\n    if (!skip_config) {\n        g_Config.window.is_maximized = is_maximized;\n        if (!is_maximized && !g_Config.window.is_fullscreen) {\n            g_Config.window.x = x;\n            g_Config.window.y = y;\n            g_Config.window.width = width;\n            g_Config.window.height = height;\n        } else {\n            g_Config.window.fs_width = width;\n            g_Config.window.fs_height = height;\n        }\n        if (g_Config.loaded) {\n            m_IgnoreConfigChanges = true;\n            Config_Update();\n            m_IgnoreConfigChanges = false;\n        }\n    }\n\n    if (update_viewport || M_MustUpdateRendererViewport()) {\n        // Refresh viewport to reflect the actual window size\n        Shell_RefreshRendererViewport();\n    }\n}\n\nvoid Shell_HandleConfigChange(const CONFIG *const old, const CONFIG *const new)\n{\n    if (!TestReplay_IsOpened()) {\n        Config_Write();\n    }\n\n#define L_CHANGED(subject) (old->subject != new->subject)\n\n    if (L_CHANGED(audio.sound_volume)) {\n        Sound_SetMasterVolume(g_Config.audio.sound_volume);\n    }\n    if (L_CHANGED(audio.master_volume) || L_CHANGED(audio.music_volume)\n        || L_CHANGED(audio.ambient_volume)) {\n        Music_SetVolume(g_Config.audio.music_volume);\n    }\n\n    if (L_CHANGED(language)) {\n        GameStringManager_ReloadLanguage(g_Config.language);\n    }\n\n    if (L_CHANGED(window.is_fullscreen) || L_CHANGED(window.is_maximized)\n        || L_CHANGED(window.width) || L_CHANGED(window.height)\n        || L_CHANGED(window.fs_width) || L_CHANGED(window.fs_height)\n        || L_CHANGED(rendering.upscaling_factor) || L_CHANGED(rendering.borders)\n        || L_CHANGED(rendering.aspect_mode)) {\n        if (!m_IgnoreConfigChanges) {\n            Shell_SyncToWindow();\n        }\n        Shell_RefreshRendererViewport();\n    }\n\n    if (L_CHANGED(visuals.fog_start) || L_CHANGED(visuals.fog_end)\n        || L_CHANGED(visuals.fog_color.g) || L_CHANGED(visuals.fog_color.b)\n        || L_CHANGED(visuals.fog_color.r) || L_CHANGED(visuals.fog_transparency)\n        || L_CHANGED(visuals.water_color.g) || L_CHANGED(visuals.water_color.b)\n        || L_CHANGED(visuals.water_color.r)) {\n        Output_ApplyLevelSettings();\n    }\n\n    if (L_CHANGED(visuals.enable_braid) || L_CHANGED(visuals.sunglasses_mode)) {\n        Lara_Skin_ApplyOutfit();\n    }\n    if (L_CHANGED(visuals.lara_outfit)) {\n        Lara_Skin_ApplyOutfitFromConfig();\n    }\n\n    if (L_CHANGED(rendering.upscaling_filter)\n        || L_CHANGED(rendering.enable_wireframe)\n        || L_CHANGED(rendering.wireframe_width)\n        || L_CHANGED(rendering.enable_vsync)\n        || L_CHANGED(rendering.anisotropy_filter)) {\n        Output_ApplyRenderSettings();\n    }\n\n    if (L_CHANGED(visuals.fov)) {\n        if (Viewport_GetSystemFOV() == -1) {\n            Viewport_AlterFOV(-1, FOV_MODE_GAME);\n        }\n    }\n\n    if ((L_CHANGED(gameplay.maximum_save_slots)\n         || L_CHANGED(gameplay.maximum_quick_save_slots))\n        && Savegame_IsInitialised()) {\n        Savegame_Shutdown();\n        Savegame_Init();\n        Savegame_ScanSavedGames();\n    }\n#undef L_CHANGED\n}\n"
  },
  {
    "path": "src/trx/game/shell/config.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n\nvoid Shell_SyncToWindow(void);\nvoid Shell_SyncFromWindow(bool update_viewport);\n\nvoid Shell_RefreshRendererViewport(void);\nvoid Shell_HandleConfigChange(const CONFIG *old, const CONFIG *new);\n"
  },
  {
    "path": "src/trx/game/shell/const.h",
    "content": "#pragma once\n\n#define SHELL_HEADLESS_WIDTH 1280\n#define SHELL_HEADLESS_HEIGHT 720\n"
  },
  {
    "path": "src/trx/game/shell/events.c",
    "content": "#include <trx/av/audio.h>\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/console/common.h>\n#include <trx/game/fmv.h>\n#include <trx/game/input/common.h>\n#include <trx/game/replay/test_recorder.h>\n#include <trx/game/replay/test_replay.h>\n#include <trx/game/screenshot.h>\n#include <trx/game/shell.h>\n#include <trx/game/ui.h>\n\n// If true, next SDL_TEXT* event should be zeroed out.\nstatic bool m_ConsoleJustOpened = false;\n\nstatic void M_HandleQuit(void)\n{\n    Shell_ScheduleExit();\n}\n\nstatic void M_HandleKeyDown(const SDL_Event *const event)\n{\n    // NOTE: Opening the console normally would get handled by Input_Update,\n    // but by the time Input_Update gets ran, we may already have lost some\n    // keypresses if the player types really fast, so we need to react sooner.\n    if (g_Config.gameplay.enable_console && !Console_IsOpened()\n        && !Input_IsInListenMode()\n        && Input_IsPressedEx(\n            INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout,\n            INPUT_ROLE_ENTER_CONSOLE)) {\n        Console_Open();\n        // Zero out the next text event so the console-open glyph never\n        // shows up.\n        m_ConsoleJustOpened = true;\n    } else {\n        UI_HandleKeyDown(event->key.keysym.sym);\n    }\n}\n\nstatic void M_HandleKeyUp(const SDL_Event *const event)\n{\n    // NOTE: needs special handling on Windows -\n    // SDL_SCANCODE_PRINTSCREEN is not sufficient to react to this.\n    if (event->key.keysym.sym == SDLK_PRINTSCREEN) {\n        Screenshot_Make(g_Config.rendering.screenshot_format);\n    }\n}\n\nstatic void M_HandleFocusGained(void)\n{\n    Shell_SetIsFocused(true);\n    if (g_Config.audio.mute_out_of_focus) {\n        Audio_Unmute();\n    }\n}\n\nstatic void M_HandleFocusLost(void)\n{\n    Shell_SetIsFocused(false);\n    if (g_Config.audio.mute_out_of_focus) {\n        Audio_Mute();\n    }\n}\n\nstatic void M_HandleWindowShown(void)\n{\n    LOG_DEBUG(\"\");\n}\n\nstatic void M_HandleWindowRestored(void)\n{\n    Shell_SyncFromWindow(true);\n}\n\nstatic void M_HandleWindowMinimized(void)\n{\n    LOG_DEBUG(\"\");\n}\n\nstatic void M_HandleWindowMaximized(void)\n{\n    Shell_SyncFromWindow(true);\n}\n\nstatic void M_HandleWindowMoved(const int32_t x, const int32_t y)\n{\n    Shell_SyncFromWindow(false);\n}\n\nstatic void M_HandleWindowResized(int32_t width, int32_t height)\n{\n    Shell_SyncFromWindow(true);\n}\n\nstatic bool M_ProcessReplayEvent(const SDL_Event *const event)\n{\n    switch (event->type) {\n    case SDL_QUIT:\n        M_HandleQuit();\n        return true;\n    }\n    return false;\n}\n\nbool Shell_ProcessEvent(const SDL_Event *const event)\n{\n    Input_ProcessEvent(event);\n\n    switch (event->type) {\n    case SDL_QUIT:\n        M_HandleQuit();\n        return true;\n\n    case SDL_KEYDOWN:\n        M_HandleKeyDown(event);\n        return true;\n\n    case SDL_KEYUP:\n        M_HandleKeyUp(event);\n        return true;\n\n    case SDL_TEXTINPUT:\n        if (m_ConsoleJustOpened) {\n            m_ConsoleJustOpened = false;\n        } else {\n            UI_HandleTextEdit(event->text.text);\n        }\n        return true;\n\n    case SDL_CONTROLLERDEVICEADDED:\n    case SDL_JOYDEVICEADDED:\n    case SDL_CONTROLLERDEVICEREMOVED:\n    case SDL_JOYDEVICEREMOVED:\n        Input_Discover();\n        return true;\n\n    case SDL_WINDOWEVENT:\n        switch (event->window.event) {\n        case SDL_WINDOWEVENT_SHOWN:\n            M_HandleWindowShown();\n            break;\n\n        case SDL_WINDOWEVENT_FOCUS_GAINED:\n            M_HandleFocusGained();\n            break;\n\n        case SDL_WINDOWEVENT_FOCUS_LOST:\n            M_HandleFocusLost();\n            break;\n\n        case SDL_WINDOWEVENT_RESTORED:\n            M_HandleWindowRestored();\n            break;\n\n        case SDL_WINDOWEVENT_MINIMIZED:\n            M_HandleWindowMinimized();\n            break;\n\n        case SDL_WINDOWEVENT_MAXIMIZED:\n            M_HandleWindowMaximized();\n            break;\n\n        case SDL_WINDOWEVENT_MOVED:\n            M_HandleWindowMoved(event->window.data1, event->window.data2);\n            break;\n\n        case SDL_WINDOWEVENT_RESIZED:\n            M_HandleWindowResized(event->window.data1, event->window.data2);\n            break;\n        }\n        break;\n    }\n\n    return false;\n}\n\nvoid Shell_ProcessEvents(void)\n{\n    SDL_Event event;\n    if (TestReplay_IsOpened()) {\n        TestReplay_RunFrame();\n        while (SDL_PollEvent(&event) != 0) {\n            M_ProcessReplayEvent(&event);\n        }\n        return;\n    }\n\n    if (TestRecorder_IsOpened()) {\n        TestRecorder_BeginFrame();\n    }\n    while (SDL_PollEvent(&event) != 0) {\n        TestRecorder_RecordEvent(&event);\n        Shell_ProcessEvent(&event);\n    }\n    if (TestRecorder_IsOpened()) {\n        TestRecorder_EndFrame();\n    }\n}\n"
  },
  {
    "path": "src/trx/game/shell/events.h",
    "content": "#pragma once\n\n#include <SDL2/SDL_events.h>\n\nbool Shell_ProcessEvent(const SDL_Event *event);\nvoid Shell_ProcessEvents(void);\n"
  },
  {
    "path": "src/trx/game/shell/flow.c",
    "content": "#include <trx/config.h>\n#include <trx/config/presets.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/catalog/manager.h>\n#include <trx/game/clock.h>\n#include <trx/game/console.h>\n#include <trx/game/events.h>\n#include <trx/game/fmv.h>\n#include <trx/game/game.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/game_strings/manager.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory_ring.h>\n#include <trx/game/items/walkable.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/lara/skin.h>\n#include <trx/game/lua.h>\n#include <trx/game/music.h>\n#include <trx/game/option.h>\n#include <trx/game/output.h>\n#include <trx/game/overlay.h>\n#include <trx/game/random.h>\n#include <trx/game/replay/test_recorder.h>\n#include <trx/game/replay/test_replay.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/shell/platform.h>\n#include <trx/game/shell/session.h>\n#include <trx/game/shell/state.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/game/ui/settings.h>\n#include <trx/gl/context.h>\n#include <trx/version.h>\n\n#include <SDL2/SDL.h>\n#include <stdio.h>\n\nstatic SHELL_SESSION *m_Session = nullptr;\nstatic SDL_Window *m_Window = nullptr;\nstatic char *m_PendingMod = nullptr;\n\n// Flags preserved across mod switches (needed to rebuild args in main()).\nstatic bool m_PrevHeadless = false;\nstatic bool m_PrevQuiet = false;\n\nstatic void M_CreateGameWindow(void)\n{\n    if (m_Window != nullptr) {\n        return; // Window persists across mod switches\n    }\n    m_Window = SDL_CreateWindow(\n        \"TRX\", g_Config.window.x, g_Config.window.y, g_Config.window.width,\n        g_Config.window.height,\n        SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);\n\n    if (m_Window == nullptr) {\n        Shell_ExitSystemFmt(\"Failed to create SDL window: %s\", SDL_GetError());\n    }\n    Shell_EnableThemeSupport(m_Window);\n}\n\nstatic void M_CreateGLContext(void)\n{\n    if (TRX_GL_Context_GetWindowHandle() != nullptr) {\n        return; // GL context persists across mod switches\n    }\n    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);\n    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);\n    SDL_GL_SetAttribute(\n        SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);\n    if (!TRX_GL_Context_Attach(m_Window)) {\n        Shell_ExitSystem(\"System Error: cannot attach opengl context\");\n    }\n}\n\nstatic void M_ShowWindow(void)\n{\n    Shell_SyncToWindow();\n    SDL_ShowWindow(m_Window);\n    SDL_RaiseWindow(m_Window);\n    Shell_RefreshRendererViewport();\n}\n\nstatic void M_HandleConfigChange(const EVENT *const event, void *const data)\n{\n    const CONFIG *const old = &g_SavedConfig;\n    const CONFIG *const new = &g_Config;\n    Shell_HandleConfigChange(old, new);\n}\n\nstatic void M_SetupSDL(void)\n{\n    SDL_version compiled;\n    SDL_VERSION(&compiled);\n    LOG_INFO(\n        \"SDL version: %d.%d.%d\", compiled.major, compiled.minor,\n        compiled.patch);\n    if (SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO) < 0) {\n        Shell_ExitSystemFmt(\"Cannot initialize SDL: %s\", SDL_GetError());\n    }\n}\n\nstatic void M_SetupGL(void)\n{\n    // Setup minimum properties of GL context\n    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);\n    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);\n    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);\n    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);\n    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);\n    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);\n}\n\nstatic void M_LoadCatalog(\n    const CATALOG_CONTEXT context, const char *const filename,\n    const bool allow_duplicates)\n{\n    const char *const path =\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_CATALOG, filename);\n    if (!Catalog_Load(context, path, allow_duplicates)) {\n        Shell_ExitSystemFmt(\"Failed to load catalogs from %s\", path);\n    }\n}\n\nvoid Shell_RequestModSwitch(const char *const mod_name)\n{\n    Memory_FreePointer(&m_PendingMod);\n    m_PendingMod = Memory_DupStr(mod_name);\n}\n\nconst char *Shell_GetPendingMod(void)\n{\n    return m_PendingMod;\n}\n\nvoid Shell_ClearPendingMod(void)\n{\n    Memory_FreePointer(&m_PendingMod);\n}\n\nbool Shell_GetPrevHeadless(void)\n{\n    return m_PrevHeadless;\n}\n\nbool Shell_GetPrevQuiet(void)\n{\n    return m_PrevQuiet;\n}\n\nconst SHELL_ARGS *Shell_GetArgs(void)\n{\n    ASSERT(m_Session != nullptr);\n    return m_Session->args;\n}\n\nstatic void M_InitModules(void)\n{\n    Shell_SetupHiDPI();\n    Shell_SetupLibAV();\n    M_SetupSDL();\n    M_SetupGL();\n\n    GameString_Init();\n    GameStringManager_Init();\n    UI_Init();\n    Overlay_Init();\n    GameEvent_Init();\n\n    GameBuf_Init();\n    Random_Seed();\n\n    Clock_Init();\n    LUA_Init();\n}\n\nstatic void M_ShutdownModules(void)\n{\n    if (TestReplay_IsOpened()) {\n        TestReplay_Close();\n    }\n    if (TestRecorder_IsOpened()) {\n        TestRecorder_Close();\n    }\n\n    Lara_Pose_Shutdown();\n    Lara_Skin_Shutdown();\n\n    Console_Shutdown();\n    Savegame_Shutdown();\n\n    GF_Shutdown();\n    LUA_Shutdown();\n    Overlay_Shutdown();\n    Option_Shutdown();\n    Output_Shutdown();\n\n    Input_Shutdown();\n    Music_Shutdown();\n    Sound_Shutdown();\n    UI_Shutdown();\n    GameEvent_Shutdown();\n\n    GameStringManager_Shutdown();\n    GameString_Shutdown();\n    Walkable_Shutdown();\n    Room_Shutdown();\n    GameBuf_Shutdown();\n    Catalog_Shutdown();\n}\n\nstatic void M_PrepareSystem(void)\n{\n    SHELL_SESSION *const s = m_Session;\n    ASSERT(s != nullptr);\n    const char *const test_replay_path = s->args->test_replay_path;\n\n    if (s->args->test_record_path != nullptr\n        && s->args->test_replay_path != nullptr) {\n        Shell_ExitSystem(\"Cannot use both --test-record and --test-replay\");\n    }\n\n    if (test_replay_path != nullptr) {\n        // Allow inferring engine version from outer args for replays lacking\n        // embedded info (created with the old directory layout).\n        g_TRVersion = s->args->engine_version;\n        SHELL_ARGS *const tmp_args = TestReplay_Open(test_replay_path);\n        if (tmp_args != nullptr) {\n            tmp_args->headless = s->args->headless;\n            tmp_args->debug_render_performance =\n                s->args->debug_render_performance;\n            ShellSession_UseArgs(s, tmp_args);\n        }\n    } else if (s->args->headless) {\n        Shell_ExitSystem(\"--headless can only be used with --test-replay\");\n    }\n\n    g_TRVersion = s->args->engine_version;\n    LOG_INFO(\"Engine version: %d\", g_TRVersion);\n    LOG_INFO(\"Mod: %s\", s->args->mod != nullptr ? s->args->mod->name : nullptr);\n    if (s->args->engine_version <= 0 || s->args->mod == nullptr) {\n        Shell_ExitSystem(\"No playable mods available.\");\n    }\n    if (s->args->mod->mod_type != MOD_DIRECT_LEVEL) {\n        ShellState_RememberLastPlayedMod(s->args->mod->name);\n    }\n\n    Config_ApplyDefaultSettings();\n\n    TRXPath_Init(s->args);\n\n    Input_Init();\n    Console_Init();\n    M_LoadCatalog(CATALOG_OBJECTS, \"catalog_objects.csv\", false);\n    M_LoadCatalog(CATALOG_MUSIC, \"catalog_music.csv\", false);\n    M_LoadCatalog(CATALOG_SAMPLES, \"catalog_samples.csv\", true);\n    M_LoadCatalog(CATALOG_LARA_STATES, \"catalog_lara_states.csv\", false);\n    M_LoadCatalog(CATALOG_LARA_ANIMS, \"catalog_lara_anims.csv\", false);\n    M_LoadCatalog(CATALOG_ITEM_ACTIONS, \"catalog_item_actions.csv\", false);\n    Lara_Pose_Init();\n    InvRing_LoadVars(\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, \"inv_ring.json5\"));\n    Gun_LoadVars(\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, \"weapons.json5\"));\n    UI_Settings_LoadFromFile(\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, \"ui.json5\"));\n    Lara_Skin_LoadFromFile(\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_COMMON_CONFIG, \"outfits.json5\"));\n    Config_Presets_ScanFiles();\n\n    if (test_replay_path != nullptr) {\n        TestReplay_Start();\n    } else {\n        char *engine_config_path =\n            TRXPath_ExpandVars(\"%config_dir%/TR%tr_version%X.json5\");\n        if (engine_config_path == nullptr) {\n            Shell_ExitSystem(\"Failed to resolve engine config path\");\n        }\n        Config_Read(engine_config_path, Shell_GetGameFlowPath(s->args->mod));\n        Memory_FreePointer(&engine_config_path);\n\n        if (s->args->test_record_path != nullptr) {\n            TestRecorder_Open(\n                s->args->test_record_path, s->args->original_args);\n        }\n    }\n    Config_SubscribeChanges(M_HandleConfigChange, nullptr);\n\n    Clock_SetSimSpeed(Clock_GetSpeedMultiplier());\n    if (!s->args->headless) {\n        Sound_Init();\n        Music_Init();\n        Sound_SetMasterVolume(g_Config.audio.sound_volume);\n        Music_SetVolume(g_Config.audio.music_volume);\n    } else {\n        Clock_DisableWait();\n        const int32_t fps = s->args->headless_fps > 0 ? s->args->headless_fps\n                                                      : Clock_GetCurrentFPS();\n        Clock_EnableHeadlessFixedFPS(fps);\n    }\n}\n\nSDL_Window *Shell_GetWindow(void)\n{\n    return m_Window;\n}\n\nint32_t Shell_Main(const SHELL_ARGS *const args)\n{\n    ASSERT(m_Session == nullptr);\n    m_Session = ShellSession_Create();\n\n    SHELL_SESSION *const s = m_Session;\n    ShellSession_UseArgs(s, args);\n\n    LOG_INFO(\"Game directory: %s\", TRXPath_Get(TRX_PATH_TRX_DIR));\n\n    M_InitModules();\n    M_PrepareSystem();\n    if (s->args->mod == nullptr) {\n        Shell_ExitSystem(\"No --mod specified.\");\n        return 1;\n    }\n    TRXPath_Init(s->args);\n    M_CreateGameWindow();\n    M_CreateGLContext();\n    Output_Init();\n    if (!s->args->headless) {\n        M_ShowWindow();\n    }\n\n    GF_Init();\n    GF_LoadFromFile(Shell_GetGameFlowPath(s->args->mod));\n\n    GameStringManager_ClearSourceFiles();\n    const char *const common_strings_path = Shell_GetCommonStringsPath();\n    if (common_strings_path == nullptr) {\n        Shell_ExitSystem(\"Missing common strings file\");\n    }\n    GameStringManager_AddSourceFile(common_strings_path, false);\n    if (s->args->mod->base_mod != nullptr) {\n        const char *const base_strings_path =\n            Shell_GetBaseGameStringsPath(s->args->mod);\n        if (base_strings_path == nullptr) {\n            Shell_ExitSystemFmt(\n                \"Missing base mod strings file for '%s'\", s->args->mod->name);\n        }\n        GameStringManager_AddSourceFile(base_strings_path, false);\n    }\n    const char *const mod_strings_path = Shell_GetGameStringsPath(s->args->mod);\n    if (mod_strings_path == nullptr) {\n        Shell_ExitSystemFmt(\n            \"Missing strings file for selected mod '%s'\", s->args->mod->name);\n    }\n    GameStringManager_AddSourceFile(mod_strings_path, true);\n    GameStringManager_DiscoverLanguages();\n    GameStringManager_ReloadLanguage(g_Config.language);\n\n    Savegame_Init();\n    Savegame_ScanSavedGames();\n\n    // Execute global Lua script if provided\n    if (g_GameFlow.main_script_path != nullptr) {\n        LUA_RESULT res = Lua_EvalFile(g_GameFlow.main_script_path);\n        if (res.code != LUA_OK) {\n            LOG_ERROR(\"Lua main script error: %s\", res.message);\n        }\n        Lua_FreeResult(&res);\n    }\n\n    Stats_CalculateMaxStats();\n    GF_COMMAND gf_cmd = GF_DoFrontendSequence();\n\n    bool loop_continue = !Shell_IsExiting();\n    while (loop_continue) {\n        LOG_INFO(\n            \"action=%s param=%d\", ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action),\n            gf_cmd.param);\n\n        switch (gf_cmd.action) {\n        case GF_START_GAME:\n        case GF_SELECT_GAME: {\n            const int32_t level_num = gf_cmd.param;\n            const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num);\n            const GF_SEQUENCE_CONTEXT seq_ctx =\n                gf_cmd.action == GF_SELECT_GAME ? GFSC_SELECT : GFSC_NORMAL;\n            if (level != nullptr) {\n                gf_cmd = GF_DoLevelSequence(level, seq_ctx);\n            }\n            break;\n        }\n\n        case GF_GLOBE_SELECT:\n            gf_cmd = GF_RunGlobeSelect(nullptr);\n            break;\n\n        case GF_START_SAVED_GAME: {\n            const SAVEGAME_SLOT_REF slot = Savegame_SlotFromParam(gf_cmd.param);\n            const int32_t level_num = Savegame_GetLevelNumber(slot);\n            if (level_num < 0) {\n                LOG_ERROR(\"Corrupt save file!\");\n                gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n            } else {\n                Savegame_BindSlot(slot);\n                const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num);\n                gf_cmd = GF_DoLevelSequence(level, GFSC_SAVED);\n            }\n            break;\n        }\n\n        case GF_RESTART_GAME: {\n            const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, gf_cmd.param);\n            gf_cmd = GF_InterpretSequence(level, GFSC_RESTART, nullptr);\n            break;\n        }\n\n        case GF_STORY_SO_FAR:\n            gf_cmd =\n                GF_PlayAvailableStory(Savegame_SlotFromParam(gf_cmd.param));\n            break;\n\n        case GF_START_CINE:\n            gf_cmd = GF_DoCutsceneSequence(gf_cmd.param, false);\n            break;\n\n        case GF_START_DEMO:\n            gf_cmd = GF_DoDemoSequence(gf_cmd.param);\n            break;\n\n        case GF_NOOP:\n        case GF_LEVEL_COMPLETE:\n            gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };\n            break;\n\n        case GF_EXIT_TO_TITLE:\n            if (s->args->level_to_play != nullptr) {\n                gf_cmd = (GF_COMMAND) { .action = GF_EXIT_GAME };\n            } else if (g_GameFlow.title_level == nullptr) {\n                Shell_ExitSystem(\"Missing title level\");\n            } else {\n                gf_cmd = GF_RunTitle();\n            }\n            break;\n\n        case GF_EXIT_GAME:\n        case GF_SWITCH_MOD:\n            loop_continue = false;\n            break;\n\n        default:\n            ASSERT_FAIL_FMT(\n                \"invalid action (action=%s, param=%d)\",\n                ENUM_MAP_TO_STRING(GF_ACTION, gf_cmd.action), gf_cmd.param);\n        }\n    }\n\n    Game_SetCurrentLevel(nullptr);\n\n    if (m_PendingMod != nullptr) {\n        if (TestReplay_IsOpened()) {\n            TestReplay_Close();\n        }\n        if (TestRecorder_IsOpened()) {\n            TestRecorder_Close();\n        }\n        // Save flags needed to rebuild args in main() before freeing the\n        // session (which owns and will free the args struct).\n        m_PrevHeadless = s->args->headless;\n        m_PrevQuiet = s->args->quiet;\n        M_ShutdownModules();\n        ShellSession_Free(m_Session);\n        m_Session = nullptr;\n        return 0;\n    }\n\n    const int32_t replay_exit_code = TestReplay_GetExitCodeOverride();\n    return replay_exit_code >= 0 ? replay_exit_code : 0;\n}\n\nvoid Shell_Shutdown(void)\n{\n    M_ShutdownModules();\n    TRX_GL_Context_Detach();\n    Log_Shutdown();\n    if (m_Session != nullptr) {\n        ShellSession_Free(m_Session);\n        m_Session = nullptr;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/shell/flow.h",
    "content": "#pragma once\n\n#include <trx/game/shell/args.h>\n\nvoid Shell_InitCommonModules(void);\nvoid Shell_ShutdownCommonModules(void);\n\nconst SHELL_ARGS *Shell_CommonInit(const SHELL_ARGS *args);\n"
  },
  {
    "path": "src/trx/game/shell/input.c",
    "content": "#include <trx/game/shell/input.h>\n\n#include <trx/config.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/utils.h>\n#include <trx/game/clock.h>\n#include <trx/game/console.h>\n#include <trx/game/console/common.h>\n#include <trx/game/input.h>\n#include <trx/game/lara.h>\n#include <trx/game/screenshot.h>\n#include <trx/game/viewport.h>\n#include <trx/gl/context.h>\n\nstatic void M_ToggleFullscreen(void)\n{\n    TOGGLE(g_Config.window.is_fullscreen);\n    Config_Update();\n}\n\nstatic void M_ToggleFPSCounter(void)\n{\n    TOGGLE(g_Config.ui.enable_fps_counter);\n    Config_Update();\n    Console_Log(\n        \"%s\",\n        g_Config.ui.enable_fps_counter ? GS(\"general/osd/fps_counter_on\")\n                                       : GS(\"general/osd/fps_counter_off\"));\n}\n\nstatic void M_ToggleBilinearFilter(void)\n{\n    CYCLE(g_Config.rendering.texture_filter, 1, TEXTURE_FILTER_NUMBER_OF);\n    Config_Update();\n    Console_Log(\n        \"%s\",\n        g_Config.rendering.texture_filter == TEXTURE_FILTER_BILINEAR\n            ? GS(\"general/osd/bilinear_filter_on\")\n            : GS(\"general/osd/bilinear_filter_off\"));\n}\n\nstatic void M_ToggleTrapezoidFilter(void)\n{\n    TOGGLE(g_Config.rendering.enable_trapezoid_filter);\n    Config_Update();\n    Console_Log(\n        \"%s\",\n        g_Config.rendering.enable_trapezoid_filter\n            ? GS(\"general/osd/trapezoid_filter_on\")\n            : GS(\"general/osd/trapezoid_filter_off\"));\n}\n\nstatic void M_ToggleWireframe(void)\n{\n    TOGGLE(g_Config.rendering.enable_wireframe);\n    Config_Update();\n    Console_Log(\n        \"%s\",\n        g_Config.rendering.enable_wireframe\n            ? GS(\"general/osd/wireframe_mode_on\")\n            : GS(\"general/osd/wireframe_mode_off\"));\n}\n\nstatic void M_ToggleTextures(void)\n{\n    TOGGLE(g_Config.rendering.enable_textures);\n    Config_Update();\n    Console_Log(\n        \"%s\",\n        g_Config.rendering.enable_textures ? GS(\"general/osd/textures_on\")\n                                           : GS(\"general/osd/textures_off\"));\n}\n\nstatic void M_CycleLightingContrast(void)\n{\n    CYCLE(\n        g_Config.rendering.lighting_contrast, g_Input.slow ? -1 : 1,\n        LIGHTING_CONTRAST_NUMBER_OF);\n    Config_Update();\n    Console_Log(\n        GS(\"general/osd/lighting_contrast_fmt\"),\n        ENUM_MAP_TO_STRING(\n            LIGHTING_CONTRAST, g_Config.rendering.lighting_contrast));\n}\n\nstatic void M_CycleUpscalingFactor(void)\n{\n    g_Config.rendering.upscaling_factor += g_Input.slow ? -1 : 1;\n    Config_Update();\n    Console_Log(\n        GS(\"general/osd/upscaling_factor\"),\n        g_Config.rendering.upscaling_factor);\n}\n\nstatic void M_CycleBorders(void)\n{\n    if (g_Input.slow) {\n        if (g_Config.rendering.borders > 0.0) {\n            g_Config.rendering.borders -= 0.05;\n            CLAMPL(g_Config.rendering.borders, 0.0);\n            Viewport_Reset();\n        }\n    } else {\n        if (g_Config.rendering.borders < 0.45) {\n            g_Config.rendering.borders += 0.05;\n            CLAMPG(g_Config.rendering.borders, 0.45);\n            Viewport_Reset();\n        }\n    }\n}\n\nvoid Shell_ProcessInput(void)\n{\n    if (g_InputDB.screenshot) {\n        Screenshot_Make(g_Config.rendering.screenshot_format);\n    }\n\n    if (g_InputDB.toggle_fullscreen) {\n        M_ToggleFullscreen();\n    }\n    if (g_InputDB.toggle_fps_counter) {\n        M_ToggleFPSCounter();\n    }\n    if (g_InputDB.toggle_bilinear_filter) {\n        M_ToggleBilinearFilter();\n    }\n    if (g_InputDB.toggle_trapezoid_filter) {\n        M_ToggleTrapezoidFilter();\n    }\n    if (g_InputDB.toggle_wireframe) {\n        M_ToggleWireframe();\n    }\n    if (g_InputDB.toggle_textures) {\n        M_ToggleTextures();\n    }\n    if (g_InputDB.cycle_lighting_contrast) {\n        M_CycleLightingContrast();\n    }\n    if (g_InputDB.switch_upscaling) {\n        M_CycleUpscalingFactor();\n    }\n    if (g_InputDB.switch_borders) {\n        M_CycleBorders();\n    }\n\n    if (g_InputDB.turbo_cheat && g_Config.gameplay.enable_cheats) {\n        Clock_CycleTurboSpeed(!g_Input.slow);\n    }\n\n    if (g_InputDB.change_outfit) {\n        Lara_Skin_CycleOutfit(g_Input.slow ? -1 : 1);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/shell/input.h",
    "content": "#pragma once\n\nvoid Shell_ProcessCommonInput(void);\nvoid Shell_ProcessInput(void);\n"
  },
  {
    "path": "src/trx/game/shell/main.c",
    "content": "#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/shell.h>\n#include <trx/game/shell/common.h>\n#include <trx/game/shell/mod.h>\n#include <trx/version.h>\n\n#include <string.h>\n\nint main(int argc, char *argv[])\n{\n    VECTOR *raw_args = Vector_Create(sizeof(const char *));\n    for (int32_t i = 1; i < argc; i++) {\n        char *const copied_arg = Memory_DupStr(argv[i]);\n        Vector_Add(raw_args, &copied_arg);\n    }\n\n    TRXPath_Init(nullptr);\n    Shell_ScanAvailableMods();\n    SHELL_ARGS *args = Shell_ParseArgs(raw_args);\n    if (args == nullptr) {\n        return 0;\n    }\n\n    TRXPath_Init(args);\n\n    char *log_path = String_Format(\"%s/TRX.log\", TRXPath_Get(TRX_PATH_TRX_DIR));\n    Log_Init(log_path, args->quiet ? LOG_LEVEL_WARNING : LOG_LEVEL_MAX);\n    Memory_FreePointer(&log_path);\n\n    LOG_INFO(\"Starting %s\", g_TRXVersion);\n    Shell_ValidateMods();\n    if (args->mod == nullptr || !args->mod->is_valid) {\n        args->mod = Shell_SelectStartupMod(args->engine_version);\n        if (args->mod != nullptr && args->engine_version == 0) {\n            args->engine_version = args->mod->engine_version;\n        }\n    }\n\n    int32_t exit_code;\n    bool restart;\n    do {\n        TRXPath_Init(args);\n        restart = false;\n        exit_code = Shell_Main(args);\n        // Note: on a mod switch, Shell_Main has already freed args (via the\n        // session) and reset m_Session to nullptr. Do not touch args after\n        // this point in the restart branch.\n\n        const char *const pending_mod = Shell_GetPendingMod();\n        if (pending_mod != nullptr) {\n            const SHELL_MOD *const mod = Shell_GetModByName(pending_mod);\n            Shell_ClearPendingMod();\n            if (mod != nullptr && mod->is_available) {\n                LOG_INFO(\"Switching mod to: %s\", mod->name);\n                SHELL_ARGS *const next_args = Memory_Alloc(sizeof(SHELL_ARGS));\n                *next_args = (SHELL_ARGS) {\n                    .engine_version = mod->engine_version,\n                    .mod = mod,\n                    .level_to_select = -1,\n                    .save_to_load = -1,\n                    .headless = Shell_GetPrevHeadless(),\n                    .quiet = Shell_GetPrevQuiet(),\n                };\n                args = next_args;\n                TRXPath_Init(args);\n                restart = true;\n            }\n        }\n    } while (restart);\n\n    Shell_Terminate(exit_code);\n    return exit_code;\n}\n"
  },
  {
    "path": "src/trx/game/shell/mod.c",
    "content": "#include <trx/game/shell/mod.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow/reader.h>\n#include <trx/game/shell/common.h>\n#include <trx/game/shell/paths.h>\n#include <trx/game/shell/state.h>\n#include <trx/version.h>\n\n#include <string.h>\n\ntypedef struct {\n    GF_MOD_META meta;\n    SHELL_MOD_TYPE mod_type;\n} M_KNOWN_MOD;\n\nstatic const M_KNOWN_MOD m_KnownModSeeds[] = {\n    { .meta = { .name = \"tr1\", .engine = 1 }, .mod_type = MOD_BASE_GAME },\n    { .meta = { .name = \"tr1-ub\", .engine = 1, .extends = \"tr1\" },\n      .mod_type = MOD_EXPANSION_PACK },\n    { .meta = { .name = \"tr1-demo-pc\", .engine = 1, .extends = \"tr1\" },\n      .mod_type = MOD_MISC },\n    { .meta = { .name = \"tr1-level\", .engine = 1, .extends = \"tr1\" },\n      .mod_type = MOD_DIRECT_LEVEL },\n    { .meta = { .name = \"tr2\", .engine = 2 }, .mod_type = MOD_BASE_GAME },\n    { .meta = { .name = \"tr2-gm\", .engine = 2, .extends = \"tr2\" },\n      .mod_type = MOD_EXPANSION_PACK },\n    { .meta = { .name = \"tr2-level\", .engine = 2, .extends = \"tr2\" },\n      .mod_type = MOD_DIRECT_LEVEL },\n    { .meta = { .name = \"tr3\", .engine = 3 }, .mod_type = MOD_BASE_GAME },\n    { .meta = { .name = \"tr3-la\", .engine = 3, .extends = \"tr3\" },\n      .mod_type = MOD_EXPANSION_PACK },\n    { .meta = { .name = \"tr3-level\", .engine = 3, .extends = \"tr3\" },\n      .mod_type = MOD_DIRECT_LEVEL },\n};\n\nstatic VECTOR *m_Mods = nullptr;\n\nstatic SHELL_MOD *M_FindMod(const char *const name)\n{\n    if (m_Mods == nullptr || name == nullptr) {\n        return nullptr;\n    }\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (strcmp(mod->name, name) == 0) {\n            return mod;\n        }\n    }\n    return nullptr;\n}\n\nstatic void M_AddMod(\n    const char *const name, const char *const title,\n    const SHELL_MOD_TYPE mod_type, const int32_t engine_version,\n    const char *const base_mod)\n{\n    const SHELL_MOD mod = {\n        .name = Memory_DupStr(name),\n        .title = title != nullptr ? Memory_DupStr(title) : nullptr,\n        .mod_type = mod_type,\n        .engine_version = engine_version,\n        .base_mod = base_mod != nullptr ? Memory_DupStr(base_mod) : nullptr,\n        .is_available = false,\n        .is_valid = false,\n    };\n    Vector_Add(m_Mods, &mod);\n}\n\nstatic void M_SeedKnownMods(void)\n{\n    for (size_t i = 0; i < ARRAY_SIZE(m_KnownModSeeds); i++) {\n        const M_KNOWN_MOD *const seed = &m_KnownModSeeds[i];\n        M_AddMod(\n            seed->meta.name, nullptr, seed->mod_type, seed->meta.engine,\n            seed->meta.extends);\n    }\n}\n\nstatic void M_ScanForCustomMods(void)\n{\n    const char *const games_dir = TRXPath_Get(TRX_PATH_GAMES_DIR);\n    if (games_dir == nullptr) {\n        return;\n    }\n\n    void *const dir = File_OpenDirectory(games_dir);\n    if (dir == nullptr) {\n        return;\n    }\n\n    const char *entry;\n    while ((entry = File_ReadDirectory(dir)) != nullptr) {\n        if (strcmp(entry, \".\") == 0 || strcmp(entry, \"..\") == 0) {\n            continue;\n        }\n\n        if (M_FindMod(entry) != nullptr) {\n            continue;\n        }\n\n        const char *const gameflow_path =\n            String_FormatStatic(\"%s/%s/gameflow.json5\", games_dir, entry);\n\n        GF_MOD_META meta = {};\n        if (!GF_ReadModMeta(gameflow_path, &meta)) {\n            LOG_WARNING(\"Failed to read mod metadata from '%s'\", gameflow_path);\n            continue;\n        }\n\n        if (meta.engine <= 0) {\n            LOG_WARNING(\n                \"Custom mod '%s' has no 'engine' field in gameflow; skipping\",\n                entry);\n            Memory_FreePointer(&meta.name);\n            Memory_FreePointer(&meta.extends);\n            continue;\n        }\n\n        M_AddMod(entry, meta.name, MOD_CUSTOM, meta.engine, meta.extends);\n        Memory_FreePointer(&meta.name);\n        Memory_FreePointer(&meta.extends);\n    }\n\n    File_CloseDirectory(dir);\n}\n\nstatic void M_ReadModMetaForKnownMods(void)\n{\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (!mod->is_available || mod->mod_type == MOD_CUSTOM) {\n            continue;\n        }\n\n        const char *const gameflow_path =\n            TRXPath_Resolve(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name);\n        if (gameflow_path == nullptr) {\n            continue;\n        }\n\n        GF_MOD_META meta = {};\n        if (!GF_ReadModMeta(gameflow_path, &meta)) {\n            continue;\n        }\n\n        if (meta.name != nullptr) {\n            Memory_FreePointer(&mod->title);\n            mod->title = meta.name;\n            meta.name = nullptr;\n        }\n\n        if (meta.engine > 0) {\n            mod->engine_version = meta.engine;\n        }\n\n        if (meta.extends != nullptr) {\n            Memory_FreePointer(&mod->base_mod);\n            mod->base_mod = meta.extends;\n            meta.extends = nullptr;\n        }\n\n        Memory_FreePointer(&meta.name);\n        Memory_FreePointer(&meta.extends);\n    }\n}\n\nstatic void M_ValidateEngineVersions(void)\n{\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (mod->engine_version <= 0 && mod->is_available) {\n            LOG_WARNING(\n                \"Mod '%s' has no valid engine version; disabling\", mod->name);\n            mod->is_available = false;\n            mod->is_valid = false;\n        }\n    }\n}\n\nstatic void M_ValidateNoMixedModLayouts(void)\n{\n    const char *const games_dir = TRXPath_Get(TRX_PATH_GAMES_DIR);\n    const char *const config_dir = TRXPath_Get(TRX_PATH_CONFIG_DIR);\n    if (games_dir == nullptr || config_dir == nullptr\n        || strcmp(games_dir, config_dir) == 0) {\n        return;\n    }\n\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        const SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (mod->mod_type == MOD_CUSTOM) {\n            continue;\n        }\n        const char *const legacy_gameflow =\n            String_FormatStatic(\"%s/%s/gameflow.json5\", config_dir, mod->name);\n        if (File_Exists(legacy_gameflow)) {\n            Shell_ExitSystemFmt(\n                \"Mixed mod layout detected: found legacy mod data at '%s' \"\n                \"while '%s' is used for mods. Move '%s' to '%s/%s/'.\",\n                legacy_gameflow, games_dir, mod->name, games_dir, mod->name);\n        }\n    }\n}\n\nstatic const char *M_GetModStringsPath(const char *const mod_id)\n{\n    ASSERT(mod_id != nullptr);\n    return TRXPath_Join(\n        TRX_PATH_GAMES_DIR, String_FormatStatic(\"%s/strings.json5\", mod_id));\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    if (m_Mods == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        Memory_FreePointer(&mod->name);\n        Memory_FreePointer(&mod->title);\n        Memory_FreePointer(&mod->base_mod);\n    }\n    Vector_Free(m_Mods);\n    m_Mods = nullptr;\n}\n\nvoid Shell_ScanAvailableMods(void)\n{\n    if (m_Mods != nullptr) {\n        M_Shutdown();\n    }\n    m_Mods = Vector_Create(sizeof(SHELL_MOD));\n\n    M_SeedKnownMods();\n\n    // Mark availability for all seeded mods.\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        mod->is_available =\n            TRXPath_Exists(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name);\n        mod->is_valid = mod->is_available;\n    }\n\n    M_ValidateNoMixedModLayouts();\n    M_ScanForCustomMods();\n\n    // Mark availability for newly added custom mods.\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (mod->mod_type == MOD_CUSTOM) {\n            mod->is_available =\n                TRXPath_Exists(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name);\n            mod->is_valid = mod->is_available;\n        }\n    }\n\n    M_ReadModMetaForKnownMods();\n    M_ValidateEngineVersions();\n}\n\nvoid Shell_ValidateMods(void)\n{\n    const int32_t original_tr_version = g_TRVersion;\n\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (!mod->is_available) {\n            mod->is_valid = false;\n            continue;\n        }\n\n        const SHELL_ARGS args = {\n            .engine_version = mod->engine_version,\n            .mod = mod,\n            .level_to_select = -1,\n            .save_to_load = -1,\n        };\n        g_TRVersion = mod->engine_version;\n        TRXPath_Init(&args);\n\n        mod->is_valid = GF_ValidateMod(mod->name, Shell_GetGameFlowPath(mod));\n    }\n\n    g_TRVersion = original_tr_version;\n}\n\nint32_t Shell_GetModCount(void)\n{\n    return m_Mods != nullptr ? m_Mods->count : 0;\n}\n\nconst SHELL_MOD *Shell_GetMod(const int32_t index)\n{\n    if (index < 0 || index >= Shell_GetModCount()) {\n        return nullptr;\n    }\n    return Vector_Get(m_Mods, index);\n}\n\nconst SHELL_MOD *Shell_GetModByName(const char *const name)\n{\n    if (m_Mods == nullptr || name == nullptr) {\n        return nullptr;\n    }\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        const SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (mod->is_available && strcmp(mod->name, name) == 0) {\n            return mod;\n        }\n    }\n    return nullptr;\n}\n\nstatic bool M_MatchesEngineVersion(\n    const SHELL_MOD *const mod, const int32_t engine_version)\n{\n    return engine_version == 0 || mod->engine_version == engine_version;\n}\n\nstatic const SHELL_MOD *M_GetFirstAvailableMod(const int32_t engine_version)\n{\n    for (int32_t i = 0; i < m_Mods->count; i++) {\n        const SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (!Shell_CanSwitchToMod(mod)\n            || !M_MatchesEngineVersion(mod, engine_version)) {\n            continue;\n        }\n        return mod;\n    }\n    return nullptr;\n}\n\nconst SHELL_MOD *Shell_SelectStartupMod(const int32_t engine_version)\n{\n    const char *const last_played_mod = ShellState_GetLastPlayedMod();\n    if (last_played_mod != nullptr) {\n        const SHELL_MOD *const mod = Shell_GetModByName(last_played_mod);\n        if (mod != nullptr && Shell_CanSwitchToMod(mod)\n            && M_MatchesEngineVersion(mod, engine_version)) {\n            return mod;\n        }\n    }\n\n    return M_GetFirstAvailableMod(engine_version);\n}\n\nconst SHELL_MOD *Shell_GetModByType(\n    const SHELL_MOD_TYPE mod_type, const int32_t engine_version)\n{\n    const SHELL_MOD *found = nullptr;\n\n    for (int32_t i = 0; i < Shell_GetModCount(); i++) {\n        const SHELL_MOD *const mod = Vector_Get(m_Mods, i);\n        if (!mod->is_available || mod->mod_type != mod_type\n            || (engine_version > 0 && mod->engine_version != engine_version)) {\n            continue;\n        }\n\n        // match\n        if (engine_version == 0) {\n            if (found) {\n                // more than one mod matches this engine version, abort\n                return nullptr;\n            }\n            found = mod;\n        } else {\n            // exact version match\n            return mod;\n        }\n    }\n\n    return found;\n}\n\nbool Shell_CanSwitchToMod(const SHELL_MOD *const mod)\n{\n    return mod != nullptr && mod->is_available && mod->is_valid\n        && mod->mod_type != MOD_DIRECT_LEVEL;\n}\n\nbool Shell_IsCurrentMod(const char *const name)\n{\n    if (name == nullptr) {\n        return false;\n    }\n\n    const SHELL_ARGS *const args = Shell_GetArgs();\n    if (args == nullptr || args->mod == nullptr || args->mod->name == nullptr) {\n        return false;\n    }\n\n    return strcmp(args->mod->name, name) == 0;\n}\n\nconst char *Shell_GetCommonStringsPath(void)\n{\n    return TRXPath_TryResolve(\n        TRX_DYNAMIC_PATH_COMMON_CONFIG, \"base_strings.json5\");\n}\n\nconst char *Shell_GetBaseGameStringsPath(const SHELL_MOD *const mod)\n{\n    const char *const base_mod =\n        mod->base_mod != nullptr ? mod->base_mod : mod->name;\n    return M_GetModStringsPath(base_mod);\n}\n\nconst char *Shell_GetGameStringsPath(const SHELL_MOD *const mod)\n{\n    return M_GetModStringsPath(mod->name);\n}\n\nconst char *Shell_GetGameFlowPath(const SHELL_MOD *const mod)\n{\n    return TRXPath_Resolve(TRX_DYNAMIC_PATH_GAMEFLOW_FILE, mod->name);\n}\n"
  },
  {
    "path": "src/trx/game/shell/mod.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef enum {\n    MOD_BASE_GAME,\n    MOD_EXPANSION_PACK,\n    MOD_MISC,\n    MOD_DIRECT_LEVEL,\n    MOD_CUSTOM,\n} SHELL_MOD_TYPE;\n\ntypedef struct {\n    char *name;\n    char *title;\n    SHELL_MOD_TYPE mod_type;\n    int32_t engine_version;\n    char *base_mod;\n    bool is_available;\n    bool is_valid;\n} SHELL_MOD;\n\nvoid Shell_ScanAvailableMods(void);\nvoid Shell_ValidateMods(void);\nint32_t Shell_GetModCount(void);\nconst SHELL_MOD *Shell_GetMod(int32_t index);\nconst SHELL_MOD *Shell_GetModByName(const char *name);\nconst SHELL_MOD *Shell_SelectStartupMod(int32_t engine_version);\nconst SHELL_MOD *Shell_GetModByType(\n    SHELL_MOD_TYPE mod_type, int32_t engine_version);\nbool Shell_CanSwitchToMod(const SHELL_MOD *mod);\nbool Shell_IsCurrentMod(const char *name);\n\nconst char *Shell_GetCommonStringsPath(void);\nconst char *Shell_GetBaseGameStringsPath(const SHELL_MOD *mod);\nconst char *Shell_GetGameStringsPath(const SHELL_MOD *mod);\nconst char *Shell_GetGameFlowPath(const SHELL_MOD *mod);\n"
  },
  {
    "path": "src/trx/game/shell/paths.c",
    "content": "#include <trx/game/shell/paths.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/shell/common.h>\n#include <trx/game/shell/mod.h>\n#include <trx/version.h>\n\n#include <SDL2/SDL_filesystem.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n\n#if defined(_WIN32)\n    #include <direct.h>\n#else\n    #include <unistd.h>\n#endif\n\n#define M_MAX_MOD_CHAIN 8\n\ntypedef struct {\n    char *trx_dir;\n    char *config_dir;\n    char *cache_dir;\n    char *games_dir;\n    char *screenshots_dir;\n    char *saves_dir;\n    char *legacy_saves_dir;\n    char *music_dir;\n    const SHELL_ARGS *args;\n    const char *mod_chain[M_MAX_MOD_CHAIN];\n    int32_t mod_chain_count;\n    bool inited;\n} M_CONTEXT;\n\ntypedef struct {\n    const char *key;\n    const char *value;\n} M_PATH_TOKEN;\n\ntypedef struct {\n    TRX_DYNAMIC_PATH id;\n    const char *patterns[8];\n    const char **extensions;\n    bool check_exists;\n    bool is_dir;\n} M_DYNAMIC_PATH_POLICY;\n\ntypedef struct {\n    char *name;\n} M_DIR_ENTRY;\n\ntypedef struct {\n    char *dir;\n    bool exists;\n    VECTOR *entries; // M_DIR_ENTRY\n} M_DIR_CACHE_ENTRY;\n\ntypedef struct {\n    uint32_t generation;\n    TRX_DYNAMIC_PATH path;\n    char *rel;\n    char *resolved;\n    bool found;\n} M_RESOLVE_CACHE_ENTRY;\n\ntypedef bool (*M_RESOLVE_ATTEMPT_CALLBACK)(\n    const char *attempt_path, void *user_data);\n\nstatic const char *m_FMVExtensions[] = {\n    \".mp4\", \".mkv\", \".mpeg\", \".avi\", \".webm\", \".ogv\", \".rpl\", \".fmv\", nullptr,\n};\n\nstatic const M_DYNAMIC_PATH_POLICY m_PathPolicies[TRX_DYNAMIC_PATH_NUMBER_OF] = {\n    [TRX_DYNAMIC_PATH_COMMON_CONFIG] = {\n        .patterns = {\n            \"%mod_dir%/%rel%\",\n            \"%base_mod_dir%/%rel%\",\n            \"%config_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_CATALOG] = {\n        .patterns = {\n            \"%mod_dir%/%rel%\",\n            \"%base_mod_dir%/%rel%\",\n            \"%config_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_GAMEFLOW_FILE] = {\n        .patterns = {\n            \"%games_dir%/%rel%/gameflow.json5\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_LEVEL_FILE] = {\n        .patterns = {\n            \"%mod_dir%/levels/%rel%\",\n            \"%trx_dir%/data/%rel%\",\n            \"%trx_dir%/%rel%\",\n            // TR3 legacy cutscenes\n            \"%trx_dir%/cuts/%rel%\",\n            \"%mod_dir%/cuts/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE] = {\n        .patterns = {\n            \"%mod_dir%/levels/%rel%\",\n            \"%base_mod_dir%/levels/%rel%\",\n            \"%trx_dir%/data/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_IMAGE_FILE] = {\n        .patterns = {\n            \"%mod_dir%/images/%rel%\",\n            \"%base_mod_dir%/images/%rel%\",\n            \"%trx_dir%/data/images/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_INJECTION_FILE] = {\n        .patterns = {\n            \"%mod_dir%/injections/%rel%\",\n            \"%base_mod_dir%/injections/%rel%\",\n            \"%trx_dir%/data/injections/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_SCRIPT_FILE] = {\n        .patterns = {\n            \"%mod_dir%/scripts/%rel%\",\n            \"%base_mod_dir%/scripts/%rel%\",\n            \"%trx_dir%/data/scripts/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_FMV_FILE] = {\n        .patterns = {\n            \"%mod_dir%/fmv/%rel%\",\n            \"%base_mod_dir%/fmv/%rel%\",\n            \"%trx_dir%/fmv/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .extensions = m_FMVExtensions,\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_SFX_FILE] = {\n        .patterns = {\n            \"%mod_dir%/%rel%\",\n            \"%base_mod_dir%/%rel%\",\n            \"%trx_dir%/data/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_CDAUDIO_FILE] = {\n        .patterns = {\n            \"%mod_dir%/music/%rel%\",\n            \"%mod_dir%/audio/%rel%\",\n            \"%base_mod_dir%/music/%rel%\",\n            \"%base_mod_dir%/audio/%rel%\",\n            \"%trx_dir%/audio/%rel%\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_MUSIC_DIR] = {\n        .patterns = {\n            \"%mod_dir%/music\",\n            \"%base_mod_dir%/music\",\n            \"%trx_dir%/music\",\n            \"%trx_dir%/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n        .is_dir = true,\n    },\n    [TRX_DYNAMIC_PATH_SHADER_FILE] = {\n        .patterns = {\n            \"%mod_dir%/shaders/%rel%\",\n            \"%base_mod_dir%/shaders/%rel%\",\n            \"%trx_dir%/shaders/%rel%\",\n            \"%trx_dir%/cfg/shaders/%rel%\",\n            nullptr,\n        },\n        .check_exists = true,\n    },\n    [TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE] = {\n        .patterns = {\n            \"%trx_dir%/screenshots/%mod%/%rel%\",\n            \"%trx_dir%/screenshots/%rel%\",\n            nullptr,\n        },\n        .check_exists = false,\n    },\n};\n\nstatic M_CONTEXT m_Context = {};\nstatic VECTOR *m_DirCache = nullptr; // M_DIR_CACHE_ENTRY\nstatic VECTOR *m_ResolveCache = nullptr; // M_RESOLVE_CACHE_ENTRY\nstatic uint32_t m_ResolveCacheGeneration = 0;\n\nstatic void M_ClearResolveCache(void)\n{\n    if (m_ResolveCache == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < m_ResolveCache->count; i++) {\n        M_RESOLVE_CACHE_ENTRY *const e = Vector_Get(m_ResolveCache, i);\n        Memory_FreePointer(&e->rel);\n        Memory_FreePointer(&e->resolved);\n    }\n    Vector_Free(m_ResolveCache);\n    m_ResolveCache = nullptr;\n}\n\n// Returns a non-owning pointer that may reference static formatting storage.\n// Do not free it.\nstatic const char *M_JoinPathStatic(const char *const a, const char *const b)\n{\n    ASSERT(b != nullptr);\n    if (a == nullptr) {\n        return b;\n    }\n    if (String_IsEmpty(b)) {\n        return a;\n    }\n    const bool a_has_sep = String_EndsWith(a, \"/\") || String_EndsWith(a, \"\\\\\");\n    const bool b_has_sep = b[0] == '/' || b[0] == '\\\\';\n    const char *const b_join = a_has_sep && b_has_sep ? b + 1 : b;\n    const char *const sep = (!a_has_sep && !b_has_sep) ? \"/\" : \"\";\n    return String_FormatStatic(\"%s%s%s\", a, sep, b_join);\n}\n\nstatic char *M_JoinPathAlloc(const char *const a, const char *const b)\n{\n    ASSERT(a != nullptr);\n    ASSERT(b != nullptr);\n    return Memory_DupStr(M_JoinPathStatic(a, b));\n}\n\nstatic M_DIR_CACHE_ENTRY *M_FindDirCache(const char *const dir)\n{\n    if (m_DirCache == nullptr || dir == nullptr) {\n        return nullptr;\n    }\n    for (int32_t i = 0; i < m_DirCache->count; i++) {\n        M_DIR_CACHE_ENTRY *const entry = Vector_Get(m_DirCache, i);\n        if (strcmp(entry->dir, dir) == 0) {\n            return entry;\n        }\n    }\n    return nullptr;\n}\n\nstatic M_DIR_CACHE_ENTRY *M_LoadDirCache(const char *const dir)\n{\n    if (dir == nullptr) {\n        return nullptr;\n    }\n    if (m_DirCache == nullptr) {\n        m_DirCache = Vector_Create(sizeof(M_DIR_CACHE_ENTRY));\n    }\n    M_DIR_CACHE_ENTRY *existing = M_FindDirCache(dir);\n    if (existing != nullptr) {\n        return existing;\n    }\n\n    M_DIR_CACHE_ENTRY entry = {\n        .dir = Memory_DupStr(dir),\n        .exists = false,\n        .entries = Vector_Create(sizeof(M_DIR_ENTRY)),\n    };\n    void *const d = File_OpenDirectory(dir);\n    if (d != nullptr) {\n        entry.exists = true;\n        const char *name = nullptr;\n        while ((name = File_ReadDirectory(d)) != nullptr) {\n            M_DIR_ENTRY de = { .name = Memory_DupStr(name) };\n            Vector_Add(entry.entries, &de);\n        }\n        File_CloseDirectory(d);\n    }\n    Vector_Add(m_DirCache, &entry);\n    return Vector_Get(m_DirCache, m_DirCache->count - 1);\n}\n\nstatic const char *M_FindDirEntryCaseAware(\n    const M_DIR_CACHE_ENTRY *const dir_cache, const char *const segment)\n{\n    ASSERT(dir_cache != nullptr);\n    ASSERT(segment != nullptr);\n    for (int32_t i = 0; i < dir_cache->entries->count; i++) {\n        const M_DIR_ENTRY *const e = Vector_Get(dir_cache->entries, i);\n        if (strcmp(e->name, segment) == 0) {\n            return e->name;\n        }\n    }\n    for (int32_t i = 0; i < dir_cache->entries->count; i++) {\n        const M_DIR_ENTRY *const e = Vector_Get(dir_cache->entries, i);\n        if (String_Equivalent(e->name, segment)) {\n            return e->name;\n        }\n    }\n    return nullptr;\n}\n\nstatic char *M_ResolveCasePathCached(const char *const path)\n{\n    if (path == nullptr || String_IsEmpty(path)) {\n        return nullptr;\n    }\n    char *path_copy = Memory_DupStr(path);\n    char *path_piece = path_copy;\n    char *current_path = Memory_Alloc(strlen(path) + 2);\n\n    if (path_copy[0] == '/') {\n        strcpy(current_path, \"/\");\n        path_piece++;\n    } else if (strstr(path_copy, \":\\\\\") || strstr(path_copy, \":/\")) {\n        strcpy(current_path, path_copy);\n        char *drive_sep = strstr(current_path, \":\\\\\");\n        if (drive_sep == nullptr) {\n            drive_sep = strstr(current_path, \":/\");\n        }\n        ASSERT(drive_sep != nullptr);\n        drive_sep[2] = '\\0';\n        path_piece += 3;\n    } else {\n        strcpy(current_path, \".\");\n    }\n\n    while (path_piece != nullptr) {\n        char *delim = strpbrk(path_piece, \"/\\\\\");\n        if (delim != nullptr) {\n            *delim = '\\0';\n        }\n\n        if (path_piece[0] == '\\0') {\n            if (delim != nullptr) {\n                path_piece = delim + 1;\n                continue;\n            }\n            break;\n        }\n\n        M_DIR_CACHE_ENTRY *const dir_cache = M_LoadDirCache(current_path);\n        if (dir_cache == nullptr || !dir_cache->exists) {\n            Memory_FreePointer(&path_copy);\n            Memory_FreePointer(&current_path);\n            return nullptr;\n        }\n\n        const char *const resolved_piece =\n            M_FindDirEntryCaseAware(dir_cache, path_piece);\n        if (resolved_piece == nullptr) {\n            Memory_FreePointer(&path_copy);\n            Memory_FreePointer(&current_path);\n            return nullptr;\n        }\n        char *next = M_JoinPathAlloc(current_path, resolved_piece);\n        Memory_FreePointer(&current_path);\n        current_path = next;\n\n        if (delim != nullptr) {\n            path_piece = delim + 1;\n        } else {\n            break;\n        }\n    }\n\n    Memory_FreePointer(&path_copy);\n    return current_path;\n}\n\nstatic char *M_GetCurrentDirectory(void)\n{\n#if defined(_WIN32)\n    char *const cwd = _getcwd(nullptr, 0);\n#else\n    char *const cwd = getcwd(nullptr, 0);\n#endif\n    if (cwd == nullptr) {\n        return nullptr;\n    }\n    char *const result = Memory_DupStr(cwd);\n    free(cwd);\n    return result;\n}\n\nstatic char *M_GuessExtensionCached(\n    const char *const path, const char **const extensions)\n{\n    if (path == nullptr || extensions == nullptr) {\n        return nullptr;\n    }\n\n    char *parent_dir = File_GetParentDirectory(path);\n    if (parent_dir == nullptr) {\n        return nullptr;\n    }\n    char *resolved_parent = M_ResolveCasePathCached(parent_dir);\n    Memory_FreePointer(&parent_dir);\n    if (resolved_parent == nullptr) {\n        return nullptr;\n    }\n    Memory_FreePointer(&resolved_parent);\n\n    char *resolved = M_ResolveCasePathCached(path);\n    if (resolved != nullptr) {\n        return resolved;\n    }\n\n    const char *const dot = strrchr(path, '.');\n    if (dot == nullptr) {\n        return nullptr;\n    }\n\n    for (const char **ext = &extensions[0]; *ext != nullptr; ext++) {\n        const size_t out_size = (size_t)(dot - path) + strlen(*ext) + 1;\n        char *out = Memory_Alloc(out_size);\n        strncpy(out, path, (size_t)(dot - path));\n        out[dot - path] = '\\0';\n        strcat(out, *ext);\n\n        resolved = M_ResolveCasePathCached(out);\n        Memory_FreePointer(&out);\n        if (resolved != nullptr) {\n            return resolved;\n        }\n    }\n\n    return nullptr;\n}\n\nstatic M_RESOLVE_CACHE_ENTRY *M_FindResolveCache(\n    const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    if (m_ResolveCache == nullptr || rel == nullptr) {\n        return nullptr;\n    }\n    for (int32_t i = 0; i < m_ResolveCache->count; i++) {\n        M_RESOLVE_CACHE_ENTRY *const e = Vector_Get(m_ResolveCache, i);\n        if (e->generation == m_ResolveCacheGeneration && e->path == path\n            && strcmp(e->rel, rel) == 0) {\n            return e;\n        }\n    }\n    return nullptr;\n}\n\nstatic void M_SetResolveCache(\n    const TRX_DYNAMIC_PATH path, const char *const rel,\n    const char *const resolved)\n{\n    if (rel == nullptr) {\n        return;\n    }\n    if (m_ResolveCache == nullptr) {\n        m_ResolveCache = Vector_Create(sizeof(M_RESOLVE_CACHE_ENTRY));\n    }\n    M_RESOLVE_CACHE_ENTRY *const existing = M_FindResolveCache(path, rel);\n    if (existing != nullptr) {\n        Memory_FreePointer(&existing->resolved);\n        existing->resolved =\n            resolved != nullptr ? Memory_DupStr(resolved) : nullptr;\n        existing->found = resolved != nullptr;\n        return;\n    }\n    M_RESOLVE_CACHE_ENTRY entry = {\n        .generation = m_ResolveCacheGeneration,\n        .path = path,\n        .rel = Memory_DupStr(rel),\n        .resolved = resolved != nullptr ? Memory_DupStr(resolved) : nullptr,\n        .found = resolved != nullptr,\n    };\n    Vector_Add(m_ResolveCache, &entry);\n}\n\nstatic const char *M_GetCurrentModID(void)\n{\n    if (m_Context.mod_chain_count > 0) {\n        return m_Context.mod_chain[0];\n    }\n    if (m_Context.args != nullptr && m_Context.args->mod != nullptr) {\n        return m_Context.args->mod->name;\n    }\n    return nullptr;\n}\n\nstatic const char *M_GetModDir(const char *const mod_id)\n{\n    if (mod_id == nullptr || String_IsEmpty(mod_id)\n        || m_Context.games_dir == nullptr) {\n        return nullptr;\n    }\n    return M_JoinPathStatic(m_Context.games_dir, mod_id);\n}\n\nstatic const char *M_GetCurrentModDir(void)\n{\n    return M_GetModDir(M_GetCurrentModID());\n}\n\nstatic const char *M_GetBaseModID(void)\n{\n    if (m_Context.mod_chain_count > 1) {\n        return m_Context.mod_chain[1];\n    }\n    if (m_Context.args != nullptr && m_Context.args->mod != nullptr) {\n        return m_Context.args->mod->base_mod;\n    }\n    return nullptr;\n}\n\nstatic const char *M_GetDirectLevelArg(void)\n{\n    return m_Context.args != nullptr && m_Context.args->level_to_play != nullptr\n        ? m_Context.args->level_to_play\n        : \"\";\n}\n\nstatic const char *M_GetBaseModDir(void)\n{\n    return M_GetModDir(M_GetBaseModID());\n}\n\nstatic const char *M_GetLegacyDataDir(void)\n{\n    return M_JoinPathStatic(m_Context.trx_dir, \"data\");\n}\n\nstatic const char *M_GetBaseDirForDynamicPath(const TRX_DYNAMIC_PATH path)\n{\n    switch (path) {\n    case TRX_DYNAMIC_PATH_COMMON_CONFIG:\n    case TRX_DYNAMIC_PATH_CATALOG:\n        return m_Context.config_dir;\n    case TRX_DYNAMIC_PATH_LEVEL_FILE:\n    case TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE:\n    case TRX_DYNAMIC_PATH_SFX_FILE:\n        return M_GetLegacyDataDir();\n    case TRX_DYNAMIC_PATH_IMAGE_FILE:\n        return M_JoinPathStatic(M_GetLegacyDataDir(), \"images\");\n    case TRX_DYNAMIC_PATH_INJECTION_FILE:\n        return M_JoinPathStatic(M_GetLegacyDataDir(), \"injections\");\n    case TRX_DYNAMIC_PATH_SCRIPT_FILE:\n        return M_JoinPathStatic(M_GetLegacyDataDir(), \"scripts\");\n    case TRX_DYNAMIC_PATH_SHADER_FILE:\n        return M_JoinPathStatic(m_Context.trx_dir, \"shaders\");\n    case TRX_DYNAMIC_PATH_FMV_FILE:\n        return M_JoinPathStatic(m_Context.trx_dir, \"fmv\");\n    case TRX_DYNAMIC_PATH_CDAUDIO_FILE:\n        return M_JoinPathStatic(M_GetLegacyDataDir(), \"audio\");\n    case TRX_DYNAMIC_PATH_MUSIC_DIR:\n        return M_JoinPathStatic(M_GetLegacyDataDir(), \"music\");\n    default:\n        return m_Context.trx_dir;\n    }\n}\n\nstatic const char *M_StripOptionalPrefix(\n    const char *const value, const char *const prefix)\n{\n    if (value != nullptr && prefix != nullptr\n        && String_CaseSubstring(value, prefix) == value) {\n        return value + strlen(prefix);\n    }\n    return value;\n}\n\nstatic void M_TrimTrailingSeparators(char *const path)\n{\n    if (path == nullptr) {\n        return;\n    }\n\n    size_t len = strlen(path);\n    while (len > 1 && (path[len - 1] == '/' || path[len - 1] == '\\\\')) {\n        // Keep Windows drive roots like C:\\ intact.\n        if (len == 3 && path[1] == ':') {\n            break;\n        }\n        path[len - 1] = '\\0';\n        len--;\n    }\n}\n\nstatic bool M_SetDirFromEnv(\n    char **const target, const char *const env_value,\n    const char *const default_suffix, const bool check_existence)\n{\n    ASSERT(target != nullptr);\n    ASSERT(default_suffix != nullptr);\n\n    Memory_FreePointer(target);\n    if (env_value != nullptr && !String_IsEmpty(env_value)) {\n        *target = Memory_DupStr(env_value);\n    } else {\n        *target = String_Format(\"%s/%s\", m_Context.trx_dir, default_suffix);\n    }\n    M_TrimTrailingSeparators(*target);\n    if (check_existence && !File_DirExists(*target)) {\n        Memory_FreePointer(target);\n        return false;\n    }\n    return true;\n}\n\nstatic char *M_ReplacePathTokens(\n    char *result, const M_PATH_TOKEN *const tokens, const size_t token_count)\n{\n    ASSERT(result != nullptr);\n    ASSERT(tokens != nullptr);\n\n    for (size_t i = 0; i < token_count; i++) {\n        const char *const tok = tokens[i].key;\n        const char *const val = tokens[i].value;\n        if (val == nullptr || strstr(result, tok) == nullptr) {\n            continue;\n        }\n\n        const size_t tok_len = strlen(tok);\n        const size_t val_len = strlen(val);\n        const size_t src_len = strlen(result);\n        int32_t tok_count = 0;\n        const char *scan = result;\n        while ((scan = strstr(scan, tok)) != nullptr) {\n            tok_count++;\n            scan += tok_len;\n        }\n        const int64_t delta = (int64_t)val_len - (int64_t)tok_len;\n        const int64_t out_len_signed = (int64_t)src_len + delta * tok_count;\n        ASSERT(out_len_signed >= 0);\n        const size_t out_len = (size_t)out_len_signed;\n        char *out = Memory_Alloc(out_len + 1);\n        char *dst = out;\n        scan = result;\n        const char *hit = nullptr;\n        while ((hit = strstr(scan, tok)) != nullptr) {\n            const size_t prefix_len = (size_t)(hit - scan);\n            memcpy(dst, scan, prefix_len);\n            dst += prefix_len;\n            memcpy(dst, val, val_len);\n            dst += val_len;\n            scan = hit + tok_len;\n        }\n        strcpy(dst, scan);\n        Memory_FreePointer(&result);\n        result = out;\n    }\n\n    return result;\n}\n\nchar *TRXPath_ExpandVars(const char *const in)\n{\n    if (in == nullptr) {\n        return nullptr;\n    }\n    if (!m_Context.inited) {\n        TRXPath_Init(m_Context.args);\n    }\n\n    char *result = Memory_DupStr(in);\n    const char *const mod_id = M_GetCurrentModID();\n    const char *const base_mod_id = M_GetBaseModID();\n\n    const M_PATH_TOKEN tokens[] = {\n#define M_PATH_TOKEN_ITEM(name, field, token) { token, m_Context.field },\n        TRX_PATH_DIR_LIST(M_PATH_TOKEN_ITEM)\n#undef M_PATH_TOKEN_ITEM\n            { \"%mod%\", mod_id != nullptr ? mod_id : \"\" },\n        { \"%mod_dir%\", M_GetCurrentModDir() },\n        { \"%levels_dir%\",\n          M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_LEVEL_FILE) },\n        { \"%images_dir%\",\n          M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_IMAGE_FILE) },\n        { \"%injections_dir%\",\n          M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_INJECTION_FILE) },\n        { \"%scripts_dir%\",\n          M_GetBaseDirForDynamicPath(TRX_DYNAMIC_PATH_SCRIPT_FILE) },\n        { \"%base_mod%\", base_mod_id != nullptr ? base_mod_id : \"\" },\n        { \"%base_mod_dir%\", M_GetBaseModDir() },\n        { \"%direct_level%\", M_GetDirectLevelArg() },\n        { \"%tr_version%\", String_FormatStatic(\"%d\", g_TRVersion) },\n    };\n\n    return M_ReplacePathTokens(result, tokens, ARRAY_SIZE(tokens));\n}\n\nstatic void M_BuildModChain(const SHELL_ARGS *const args)\n{\n    m_Context.mod_chain_count = 0;\n    if (args == nullptr || args->mod == nullptr) {\n        return;\n    }\n\n    const SHELL_MOD *mod = args->mod;\n    while (mod != nullptr && m_Context.mod_chain_count < M_MAX_MOD_CHAIN) {\n        for (int32_t i = 0; i < m_Context.mod_chain_count; i++) {\n            if (strcmp(m_Context.mod_chain[i], mod->name) == 0) {\n                LOG_WARNING(\n                    \"Mod chain cycle detected at '%s'; stopping traversal\",\n                    mod->name);\n                return;\n            }\n        }\n        m_Context.mod_chain[m_Context.mod_chain_count++] = mod->name;\n        if (mod->base_mod == nullptr) {\n            break;\n        }\n        mod = Shell_GetModByName(mod->base_mod);\n        if (mod == nullptr) {\n            break;\n        }\n    }\n    if (mod != nullptr && m_Context.mod_chain_count == M_MAX_MOD_CHAIN) {\n        LOG_WARNING(\n            \"Mod chain truncated at %d entries; remaining base_mod links will \"\n            \"be ignored\",\n            M_MAX_MOD_CHAIN);\n    }\n}\n\nstatic void M_SeedResolverCaches(void)\n{\n    const char *const dirs[] = {\n        m_Context.trx_dir,\n        m_Context.config_dir,\n        m_Context.cache_dir,\n        m_Context.games_dir,\n        m_Context.screenshots_dir,\n        m_Context.saves_dir,\n        m_Context.legacy_saves_dir,\n        M_GetCurrentModDir(),\n        M_GetBaseModDir(),\n        M_GetLegacyDataDir(),\n        M_JoinPathStatic(M_GetLegacyDataDir(), \"levels\"),\n        M_JoinPathStatic(M_GetLegacyDataDir(), \"images\"),\n        M_JoinPathStatic(M_GetLegacyDataDir(), \"injections\"),\n        M_JoinPathStatic(M_GetLegacyDataDir(), \"scripts\"),\n        M_JoinPathStatic(m_Context.trx_dir, \"cuts\"),\n        M_JoinPathStatic(m_Context.trx_dir, \"fmv\"),\n        M_JoinPathStatic(m_Context.trx_dir, \"audio\"),\n        M_JoinPathStatic(m_Context.trx_dir, \"music\"),\n        M_JoinPathStatic(m_Context.trx_dir, \"shaders\"),\n        M_JoinPathStatic(m_Context.trx_dir, \"cfg\"),\n        nullptr,\n    };\n    for (int32_t i = 0; dirs[i] != nullptr; i++) {\n        M_LoadDirCache(dirs[i]);\n    }\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    Memory_FreePointer(&m_Context.trx_dir);\n    Memory_FreePointer(&m_Context.config_dir);\n    Memory_FreePointer(&m_Context.cache_dir);\n    Memory_FreePointer(&m_Context.games_dir);\n    Memory_FreePointer(&m_Context.screenshots_dir);\n    Memory_FreePointer(&m_Context.saves_dir);\n    Memory_FreePointer(&m_Context.legacy_saves_dir);\n    if (m_DirCache != nullptr) {\n        for (int32_t i = 0; i < m_DirCache->count; i++) {\n            M_DIR_CACHE_ENTRY *const e = Vector_Get(m_DirCache, i);\n            for (int32_t j = 0; j < e->entries->count; j++) {\n                M_DIR_ENTRY *const de = Vector_Get(e->entries, j);\n                Memory_FreePointer(&de->name);\n            }\n            Vector_Free(e->entries);\n            Memory_FreePointer(&e->dir);\n        }\n        Vector_Free(m_DirCache);\n        m_DirCache = nullptr;\n    }\n    M_ClearResolveCache();\n    m_Context.args = nullptr;\n    m_Context.mod_chain_count = 0;\n    m_Context.inited = false;\n}\n\nvoid TRXPath_Init(const SHELL_ARGS *const args)\n{\n    M_Shutdown();\n\n    m_Context.args = args;\n    m_ResolveCacheGeneration++;\n\n    if (m_Context.trx_dir == nullptr) {\n        const char *const base = SDL_GetBasePath();\n        if (base != nullptr) {\n            m_Context.trx_dir = Memory_DupStr(base);\n            SDL_free((void *)base);\n        } else {\n            m_Context.trx_dir = Memory_DupStr(\".\");\n        }\n        M_TrimTrailingSeparators(m_Context.trx_dir);\n    }\n\n    M_SetDirFromEnv(\n        &m_Context.config_dir, getenv(\"TRX_CONFIG_DIR\"), \"cfg\", false);\n    M_SetDirFromEnv(\n        &m_Context.cache_dir, getenv(\"TRX_CACHE_DIR\"), \"cache\", false);\n\n    if (!M_SetDirFromEnv(\n            &m_Context.games_dir, getenv(\"TRX_GAMES_DIR\"), \"games\", true)) {\n        M_SetDirFromEnv(&m_Context.games_dir, nullptr, \"cfg\", false);\n    }\n\n    M_SetDirFromEnv(\n        &m_Context.screenshots_dir, getenv(\"TRX_SCREENSHOTS_DIR\"),\n        \"screenshots\", false);\n\n    M_SetDirFromEnv(\n        &m_Context.saves_dir, getenv(\"TRX_SAVES_DIR\"), \"saves\", false);\n\n    Memory_FreePointer(&m_Context.legacy_saves_dir);\n    m_Context.legacy_saves_dir = String_Format(\"%s/saves\", m_Context.trx_dir);\n\n    M_BuildModChain(args);\n    M_SeedResolverCaches();\n    m_Context.inited = true;\n}\n\nconst char *TRXPath_Get(const TRX_PATH path)\n{\n    if (!m_Context.inited) {\n        TRXPath_Init(m_Context.args);\n    }\n\n    switch (path) {\n#define M_GET_CASE(name, field, token)                                         \\\n    case TRX_PATH_##name:                                                      \\\n        return m_Context.field;\n        TRX_PATH_DIR_LIST(M_GET_CASE)\n#undef M_GET_CASE\n    default:\n        ASSERT_FAIL_FMT(\"Unknown TRX_PATH %d\", path);\n        return nullptr;\n    }\n}\n\nconst char *TRXPath_Join(const TRX_PATH path, const char *const rel)\n{\n    const char *const root = TRXPath_Get(path);\n    if (root == nullptr || rel == nullptr || String_IsEmpty(rel)) {\n        return root;\n    }\n    return M_JoinPathStatic(root, rel);\n}\n\nstatic const char *M_ExpandDynamicPattern(\n    const TRX_DYNAMIC_PATH path, const char *const pattern,\n    const char *const rel, const char *const mod_dir_override)\n{\n    ASSERT(pattern != nullptr);\n\n    const char *rel_value = rel;\n    const char *const mod_id = M_GetCurrentModID();\n    const char *const base_mod_id = M_GetBaseModID();\n\n    const M_PATH_TOKEN tokens[] = {\n#define M_DYNAMIC_PATH_TOKEN_ITEM(name, field, token)                          \\\n    { token, m_Context.field },\n        TRX_PATH_DIR_LIST(M_DYNAMIC_PATH_TOKEN_ITEM)\n#undef M_DYNAMIC_PATH_TOKEN_ITEM\n            { \"%rel%\", rel_value },\n        { \"%mod%\", mod_id != nullptr ? mod_id : \"\" },\n        { \"%base_mod%\", base_mod_id != nullptr ? base_mod_id : \"\" },\n        { \"%mod_dir%\",\n          mod_dir_override != nullptr ? mod_dir_override\n                                      : M_GetCurrentModDir() },\n        { \"%base_mod_dir%\", M_GetBaseModDir() },\n        { \"%base_dir%\", M_GetBaseDirForDynamicPath(path) },\n        { \"%tr_version%\", String_FormatStatic(\"%d\", g_TRVersion) },\n    };\n\n    char *expanded = Memory_DupStr(pattern);\n    expanded = M_ReplacePathTokens(expanded, tokens, ARRAY_SIZE(tokens));\n\n    const char *const resolved = String_FormatStatic(\"%s\", expanded);\n    Memory_FreePointer(&expanded);\n    return resolved;\n}\n\nstatic bool M_ForEachResolveAttempt(\n    const TRX_DYNAMIC_PATH path, const char *const rel,\n    const M_RESOLVE_ATTEMPT_CALLBACK callback, void *const user_data,\n    const bool stop_after_first_pattern)\n{\n    ASSERT(path >= 0 && path < TRX_DYNAMIC_PATH_NUMBER_OF);\n    ASSERT(callback != nullptr);\n\n    char *expanded_rel = TRXPath_ExpandVars(rel);\n    const char *const effective_rel =\n        expanded_rel != nullptr ? expanded_rel : rel;\n\n    if (effective_rel != nullptr && File_IsAbsolute(effective_rel)) {\n        const bool result = callback(effective_rel, user_data);\n        Memory_FreePointer(&expanded_rel);\n        return result;\n    }\n\n    const M_DYNAMIC_PATH_POLICY *const policy = &m_PathPolicies[path];\n    ASSERT(policy != nullptr);\n\n    for (size_t i = 0; i < ARRAY_SIZE(policy->patterns); i++) {\n        const char *const pattern = policy->patterns[i];\n        if (pattern == nullptr) {\n            break;\n        }\n\n        char *candidate = Memory_DupStr(\n            M_ExpandDynamicPattern(path, pattern, effective_rel, nullptr));\n        if (strchr(candidate, '%') != nullptr) {\n            Memory_FreePointer(&candidate);\n            continue;\n        }\n        if (!callback(candidate, user_data)) {\n            Memory_FreePointer(&candidate);\n            Memory_FreePointer(&expanded_rel);\n            return false;\n        }\n\n        if (policy->extensions == nullptr || policy->is_dir) {\n            Memory_FreePointer(&candidate);\n            continue;\n        }\n\n        const char *const dot = strrchr(candidate, '.');\n        if (dot == nullptr) {\n            Memory_FreePointer(&candidate);\n            continue;\n        }\n\n        for (const char **ext = &policy->extensions[0]; *ext != nullptr;\n             ext++) {\n            const size_t out_size =\n                (size_t)(dot - candidate) + strlen(*ext) + 1;\n            char *out = Memory_Alloc(out_size);\n            strncpy(out, candidate, (size_t)(dot - candidate));\n            out[dot - candidate] = '\\0';\n            strcat(out, *ext);\n            const bool keep_going = callback(out, user_data);\n            Memory_FreePointer(&out);\n            if (!keep_going) {\n                Memory_FreePointer(&candidate);\n                Memory_FreePointer(&expanded_rel);\n                return false;\n            }\n        }\n\n        if (stop_after_first_pattern) {\n            Memory_FreePointer(&candidate);\n            break;\n        }\n        Memory_FreePointer(&candidate);\n    }\n\n    Memory_FreePointer(&expanded_rel);\n    return true;\n}\n\ntypedef struct {\n    const M_DYNAMIC_PATH_POLICY *policy;\n    const char *resolved;\n} M_RESOLVE_VISITOR_CONTEXT;\n\nstatic bool M_ResolveAttemptVisitor(\n    const char *const attempt_path, void *const user_data)\n{\n    ASSERT(attempt_path != nullptr);\n    ASSERT(user_data != nullptr);\n\n    M_RESOLVE_VISITOR_CONTEXT *const ctx = user_data;\n    ASSERT(ctx->policy != nullptr);\n\n    if (ctx->policy->is_dir) {\n        if (!ctx->policy->check_exists) {\n            ctx->resolved = String_FormatStatic(\"%s\", attempt_path);\n            return false;\n        }\n\n        char *dir_path = M_ResolveCasePathCached(attempt_path);\n        if (dir_path == nullptr) {\n            return true;\n        }\n        M_DIR_CACHE_ENTRY *const dir_cache = M_LoadDirCache(dir_path);\n        if (dir_cache != nullptr && dir_cache->exists) {\n            ctx->resolved = String_FormatStatic(\"%s\", dir_path);\n            Memory_FreePointer(&dir_path);\n            return false;\n        }\n        Memory_FreePointer(&dir_path);\n        return true;\n    }\n\n    if (!ctx->policy->check_exists) {\n        ctx->resolved = String_FormatStatic(\"%s\", attempt_path);\n        return false;\n    }\n\n    char *full_path = M_ResolveCasePathCached(attempt_path);\n    if (full_path == nullptr) {\n        return true;\n    }\n    ctx->resolved = String_FormatStatic(\"%s\", full_path);\n    Memory_FreePointer(&full_path);\n    return false;\n}\n\nstatic char *M_AppendResolveAttempt(\n    char *attempts, const char *const attempt_path)\n{\n    if (attempt_path == nullptr || String_IsEmpty(attempt_path)) {\n        return attempts;\n    }\n    if (attempts == nullptr) {\n        return String_Format(\"%s\", attempt_path);\n    }\n    char *const joined = String_Format(\"%s\\n  - %s\", attempts, attempt_path);\n    Memory_FreePointer(&attempts);\n    return joined;\n}\n\nstatic bool M_CollectResolveAttemptVisitor(\n    const char *const attempt_path, void *const user_data)\n{\n    ASSERT(user_data != nullptr);\n    char **const attempts = user_data;\n    *attempts = M_AppendResolveAttempt(*attempts, attempt_path);\n    return true;\n}\n\nstatic char *M_GetResolveAttempts(\n    const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    char *attempts = nullptr;\n    M_ForEachResolveAttempt(\n        path, rel, M_CollectResolveAttemptVisitor, &attempts, false);\n    return attempts;\n}\n\nstatic const char *M_GetResolveError(\n    const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    char *attempts = M_GetResolveAttempts(path, rel);\n    const char *const out = String_FormatStatic(\n        \"Failed to resolve path \\\"%s\\\". Searched paths:\\n\"\n        \"  - %s)\",\n        rel != nullptr ? rel : \"(null)\",\n        attempts != nullptr ? attempts : \"(none)\");\n    Memory_FreePointer(&attempts);\n    return out;\n}\n\nstatic const char *M_PeekResolvedUserPathCandidate(\n    const M_DYNAMIC_PATH_POLICY *const policy, const char *const candidate)\n{\n    ASSERT(policy != nullptr);\n\n    if (candidate == nullptr) {\n        return nullptr;\n    }\n\n    if (!policy->check_exists) {\n        return String_FormatStatic(\"%s\", candidate);\n    }\n\n    if (policy->is_dir) {\n        char *dir_path = M_ResolveCasePathCached(candidate);\n        if (dir_path == nullptr) {\n            return nullptr;\n        }\n        M_DIR_CACHE_ENTRY *const dir_cache = M_LoadDirCache(dir_path);\n        if (dir_cache == nullptr || !dir_cache->exists) {\n            Memory_FreePointer(&dir_path);\n            return nullptr;\n        }\n        const char *const resolved = String_FormatStatic(\"%s\", dir_path);\n        Memory_FreePointer(&dir_path);\n        return resolved;\n    }\n\n    char *full_path = M_ResolveCasePathCached(candidate);\n    if (full_path == nullptr) {\n        return nullptr;\n    }\n    const char *const resolved = String_FormatStatic(\"%s\", full_path);\n    Memory_FreePointer(&full_path);\n    return resolved;\n}\n\nconst char *TRXPath_PeekResolve(\n    const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    if (!m_Context.inited) {\n        TRXPath_Init(m_Context.args);\n    }\n    ASSERT(path >= 0 && path < TRX_DYNAMIC_PATH_NUMBER_OF);\n    const M_DYNAMIC_PATH_POLICY *const policy = &m_PathPolicies[path];\n    ASSERT(policy != nullptr);\n\n    if (rel != nullptr && File_IsAbsolute(rel)) {\n        return rel;\n    }\n    if (rel != nullptr) {\n        const M_RESOLVE_CACHE_ENTRY *const cached =\n            M_FindResolveCache(path, rel);\n        if (cached != nullptr) {\n            return cached->found ? cached->resolved : nullptr;\n        }\n    }\n\n    M_RESOLVE_VISITOR_CONTEXT ctx = {\n        .policy = policy,\n        .resolved = nullptr,\n    };\n    M_ForEachResolveAttempt(\n        path, rel, M_ResolveAttemptVisitor, &ctx, !policy->check_exists);\n    if (ctx.resolved != nullptr) {\n        M_SetResolveCache(path, rel, ctx.resolved);\n        if (rel != nullptr) {\n            const M_RESOLVE_CACHE_ENTRY *const cached =\n                M_FindResolveCache(path, rel);\n            if (cached != nullptr && cached->found) {\n                return cached->resolved;\n            }\n        }\n        return ctx.resolved;\n    }\n\n    M_SetResolveCache(path, rel, nullptr);\n    return nullptr;\n}\n\nconst char *TRXPath_TryResolve(\n    const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    const char *const resolved = TRXPath_PeekResolve(path, rel);\n    if (resolved == nullptr) {\n        LOG_ERROR(\"%s\", M_GetResolveError(path, rel));\n    }\n    return resolved;\n}\n\nconst char *TRXPath_Resolve(const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    const char *const resolved = TRXPath_PeekResolve(path, rel);\n    if (resolved == nullptr) {\n        Shell_ExitSystem(M_GetResolveError(path, rel));\n    }\n    return resolved;\n}\n\nMYFILE *TRXPath_OpenFile(\n    const TRX_DYNAMIC_PATH path, const char *const rel,\n    const FILE_OPEN_MODE mode)\n{\n    const char *const resolved = TRXPath_TryResolve(path, rel);\n    if (resolved == nullptr) {\n        return nullptr;\n    }\n    return File_Open(resolved, mode);\n}\n\nbool TRXPath_LoadFile(\n    const TRX_DYNAMIC_PATH path, const char *const rel, char **const out_data,\n    size_t *const out_size)\n{\n    const char *const resolved = TRXPath_TryResolve(path, rel);\n    if (resolved == nullptr) {\n        if (out_data != nullptr) {\n            *out_data = nullptr;\n        }\n        if (out_size != nullptr) {\n            *out_size = 0;\n        }\n        return false;\n    }\n    return File_Load(resolved, out_data, out_size);\n}\n\nbool TRXPath_Exists(const TRX_DYNAMIC_PATH path, const char *const rel)\n{\n    const char *const resolved = TRXPath_PeekResolve(path, rel);\n    if (resolved == nullptr) {\n        return false;\n    }\n    return File_Exists(resolved);\n}\n\nchar *TRXPath_GuessExtension(const char *const path, const char **extensions)\n{\n    if (!m_Context.inited) {\n        TRXPath_Init(m_Context.args);\n    }\n    return M_GuessExtensionCached(path, extensions);\n}\n\nconst char *TRXPath_PeekResolveUserPath(\n    const TRX_DYNAMIC_PATH path, const char *const input_path)\n{\n    if (!m_Context.inited) {\n        TRXPath_Init(m_Context.args);\n    }\n    ASSERT(path >= 0 && path < TRX_DYNAMIC_PATH_NUMBER_OF);\n    const M_DYNAMIC_PATH_POLICY *const policy = &m_PathPolicies[path];\n    ASSERT(policy != nullptr);\n\n    if (input_path == nullptr) {\n        return nullptr;\n    }\n\n    if (File_IsAbsolute(input_path)) {\n        return M_PeekResolvedUserPathCandidate(policy, input_path);\n    }\n\n    char *cwd = M_GetCurrentDirectory();\n    if (cwd != nullptr) {\n        char *cwd_path = String_Format(\"%s/%s\", cwd, input_path);\n        Memory_FreePointer(&cwd);\n        const char *const resolved =\n            M_PeekResolvedUserPathCandidate(policy, cwd_path);\n        Memory_FreePointer(&cwd_path);\n        if (resolved != nullptr) {\n            return resolved;\n        }\n    }\n\n    return TRXPath_PeekResolve(path, input_path);\n}\n"
  },
  {
    "path": "src/trx/game/shell/paths.h",
    "content": "#pragma once\n\n#include <trx/core/filesystem.h>\n#include <trx/game/shell/args.h>\n\n// Shell path module.\n// This layer owns high-level path policy: token expansion (%trx_dir%),\n// mod/base fallback order, case-aware canonicalization, and extension\n// guessing/caching.\n//\n// Use these APIs for game asset/config resolution instead of ad-hoc file\n// probes.\n\n// clang-format off\n#define TRX_PATH_DIR_LIST(X)                                                   \\\n    X(TRX_DIR,          trx_dir,          \"%trx_dir%\")                         \\\n    X(CONFIG_DIR,       config_dir,       \"%config_dir%\")                      \\\n    X(CACHE_DIR,        cache_dir,        \"%cache_dir%\")                      \\\n    X(GAMES_DIR,        games_dir,        \"%games_dir%\")                       \\\n    X(SCREENSHOTS_DIR,  screenshots_dir,  \"%screenshots_dir%\")                 \\\n    X(SAVES_DIR,        saves_dir,        \"%saves_dir%\")                       \\\n    X(LEGACY_SAVES_DIR, legacy_saves_dir, \"%legacy_saves_dir%\")                                                            \\\n    // clang-format on\n\ntypedef enum {\n#define M_DIR_ENUM(name, field, token) TRX_PATH_##name,\n    TRX_PATH_DIR_LIST(M_DIR_ENUM)\n#undef M_DIR_ENUM\n} TRX_PATH;\n\ntypedef enum {\n    TRX_DYNAMIC_PATH_COMMON_CONFIG,\n    TRX_DYNAMIC_PATH_CATALOG,\n    TRX_DYNAMIC_PATH_GAMEFLOW_FILE,\n    TRX_DYNAMIC_PATH_SHADER_FILE,\n    TRX_DYNAMIC_PATH_FMV_FILE,\n    TRX_DYNAMIC_PATH_LEVEL_FILE,\n    TRX_DYNAMIC_PATH_SHARED_LEVEL_FILE,\n    TRX_DYNAMIC_PATH_IMAGE_FILE,\n    TRX_DYNAMIC_PATH_INJECTION_FILE,\n    TRX_DYNAMIC_PATH_SCRIPT_FILE,\n    TRX_DYNAMIC_PATH_SFX_FILE,\n    TRX_DYNAMIC_PATH_CDAUDIO_FILE,\n    TRX_DYNAMIC_PATH_MUSIC_DIR,\n    TRX_DYNAMIC_PATH_SCREENSHOT_WRITE_FILE,\n    TRX_DYNAMIC_PATH_NUMBER_OF,\n} TRX_DYNAMIC_PATH;\n\n// Initialize resolver state from shell args and environment variables.\n// Safe to call multiple times; later calls refresh context-derived values.\nvoid TRXPath_Init(const SHELL_ARGS *args);\n\n// Expand `%token%` variables in an arbitrary string.\n// Returns an owning string; caller must free.\nchar *TRXPath_ExpandVars(const char *in);\n\n// Return configured root directory for a static TRX_PATH id.\n// Returned pointer is owned by resolver context; do not free.\nconst char *TRXPath_Get(TRX_PATH path);\n\n// Join static root and relative path.\n// Returned pointer may use static formatting storage; copy if you need to keep\n// it.\nconst char *TRXPath_Join(TRX_PATH path, const char *rel);\n\n// Resolve with policy fallback and existence checks.\n// Returns nullptr on miss; does not log.\nconst char *TRXPath_PeekResolve(TRX_DYNAMIC_PATH path, const char *rel);\n// Same as TRXPath_PeekResolve, but logs an error on miss.\nconst char *TRXPath_TryResolve(TRX_DYNAMIC_PATH path, const char *rel);\n// Same as TRXPath_PeekResolve, but terminates the game on miss.\nconst char *TRXPath_Resolve(TRX_DYNAMIC_PATH path, const char *rel);\n\n// Resolve and open a file in one call.\n// Returns nullptr if resolution/open fails.\nMYFILE *TRXPath_OpenFile(\n    TRX_DYNAMIC_PATH path, const char *rel, FILE_OPEN_MODE mode);\n\n// Resolve and load file contents into memory.\n// On failure, sets `out_data` to nullptr and `out_size` to 0 when provided.\nbool TRXPath_LoadFile(\n    TRX_DYNAMIC_PATH path, const char *rel, char **out_data, size_t *out_size);\n\n// Resolve and check whether the file exists.\nbool TRXPath_Exists(TRX_DYNAMIC_PATH path, const char *rel);\n\n// Guess file extension (e.g. \".mp4\" vs \".rpl\") with case-aware resolver cache.\n// Returns an owning canonical path or nullptr if no candidate exists; caller\n// must free.\nchar *TRXPath_GuessExtension(const char *path, const char **extensions);\n\n// Resolve a user-supplied path by trying:\n// 1. absolute path as-is\n// 2. current working directory for relative paths\n// 3. normal TRX path policy fallback for the given dynamic path\n// Returns nullptr on miss; does not log.\nconst char *TRXPath_PeekResolveUserPath(\n    TRX_DYNAMIC_PATH path, const char *input_path);\n"
  },
  {
    "path": "src/trx/game/shell/platform.c",
    "content": "#include <trx/game/shell/platform.h>\n\n#ifdef _WIN32\n    #include <objbase.h>\n    #include <windows.h>\n    #include <SDL2/SDL_syswm.h>\n    #include <winreg.h>\n\n#endif\n\n#include <SDL2/SDL.h>\n#include <libavcodec/version.h>\n#include <libavutil/log.h>\n\nvoid Shell_SetupHiDPI(void)\n{\n#ifdef _WIN32\n    SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, \"permonitorv2\");\n    SDL_SetHint(SDL_HINT_WINDOWS_DPI_SCALING, \"0\");\n#endif\n}\n\nvoid Shell_SetupLibAV(void)\n{\n#ifdef _WIN32\n    // necessary for SDL_OpenAudioDevice to work with WASAPI\n    // https://www.mail-archive.com/ffmpeg-trac@avcodec.org/msg43300.html\n    CoInitializeEx(nullptr, COINIT_MULTITHREADED);\n#endif\n\n#if LIBAVCODEC_VERSION_MAJOR <= 57\n    av_register_all();\n#endif\n\n    av_log_set_level(AV_LOG_ERROR);\n}\n\n#ifdef _WIN32\n// NOTE – taken from SDL3:\n// From 8994878767cfb9403f525d12c0770c1e149a4d08 Mon Sep 17 00:00:00 2001\n// From: Sam Lantinga <slouken@libsdl.org>\n// Date: Tue, 7 Mar 2023 00:01:34 -0800\n// Subject: [PATCH] Added SDL_GetSystemTheme() to return whether the system is\n//  using a dark or light color theme, and SDL_EVENT_SYSTEM_THEME_CHANGED is\n//  sent when this changes\n\n    #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n        #define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n    #endif\n\n// Previous window procedure pointer.\nLRESULT(CALLBACK *m_OldWndProc)(HWND, UINT, WPARAM, LPARAM) = nullptr;\n\nstatic bool M_GetWindowsDarkMode(void)\n{\n    DWORD type = 0;\n    DWORD value = 1;\n    DWORD size = sizeof(value);\n    const char *const key =\n        \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\n    const LSTATUS status = RegGetValue(\n        HKEY_CURRENT_USER, TEXT(key), TEXT(\"AppsUseLightTheme\"),\n        RRF_RT_REG_DWORD, &type, &value, &size);\n    return (status == ERROR_SUCCESS && value == 0);\n}\n\nstatic void M_ApplyDarkMode(HWND hwnd)\n{\n    void *dwm = SDL_LoadObject(\"dwmapi.dll\");\n    if (dwm == nullptr) {\n        return;\n    }\n    typedef HRESULT(WINAPI * DwmSetWindowAttribute_t)(\n        HWND, DWORD, LPCVOID, DWORD);\n    #pragma GCC diagnostic push\n    #pragma GCC diagnostic ignored \"-Wpedantic\"\n    DwmSetWindowAttribute_t fn =\n        (DwmSetWindowAttribute_t)SDL_LoadFunction(dwm, \"DwmSetWindowAttribute\");\n    #pragma GCC diagnostic pop\n    if (fn != nullptr) {\n        BOOL dark = M_GetWindowsDarkMode() ? TRUE : FALSE;\n        fn(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark));\n    }\n    SDL_UnloadObject(dwm);\n}\n\n// Custom window procedure to listen for theme changes.\nstatic LRESULT CALLBACK\nM_DarkModeWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)\n{\n    if (msg == WM_SETTINGCHANGE && wParam == 0 && lParam != 0\n        && lstrcmpi((LPCTSTR)lParam, TEXT(\"ImmersiveColorSet\")) == 0) {\n        M_ApplyDarkMode(hwnd);\n    }\n    return CallWindowProc(m_OldWndProc, hwnd, msg, wParam, lParam);\n}\n\nvoid Shell_EnableThemeSupport(SDL_Window *const window)\n{\n    SDL_SysWMinfo info;\n    SDL_VERSION(&info.version);\n    if (!SDL_GetWindowWMInfo(window, &info)) {\n        return;\n    }\n    HWND hwnd = info.info.win.window;\n    m_OldWndProc = (WNDPROC)SetWindowLongPtr(\n        hwnd, GWLP_WNDPROC, (LONG_PTR)M_DarkModeWndProc);\n    M_ApplyDarkMode(hwnd);\n}\n\n#else\nvoid Shell_EnableThemeSupport(SDL_Window *const window)\n{\n}\n#endif\n"
  },
  {
    "path": "src/trx/game/shell/platform.h",
    "content": "#pragma once\n\n// Isolated platform-sensitive initialization code\n#include <SDL2/SDL_video.h>\n\nvoid Shell_SetupHiDPI(void);\nvoid Shell_SetupLibAV(void);\n\nvoid Shell_EnableThemeSupport(SDL_Window *window);\n"
  },
  {
    "path": "src/trx/game/shell/session.c",
    "content": "#include <trx/game/shell/session.h>\n\n#include <trx/core/memory.h>\n\nSHELL_SESSION *ShellSession_Create(void)\n{\n    return Memory_Alloc(sizeof(SHELL_SESSION));\n}\n\nvoid ShellSession_Free(SHELL_SESSION *const session)\n{\n    if (session != nullptr) {\n        if (session->args != nullptr) {\n            Shell_FreeArgs((SHELL_ARGS *)session->args);\n        }\n        Memory_Free(session);\n    }\n}\n\nvoid ShellSession_UseArgs(\n    SHELL_SESSION *const session, const SHELL_ARGS *const args)\n{\n    if (session->args != nullptr) {\n        Shell_FreeArgs((SHELL_ARGS *)session->args);\n    }\n    session->args = args;\n}\n"
  },
  {
    "path": "src/trx/game/shell/session.h",
    "content": "#pragma once\n\n#include <trx/game/shell/args.h>\n\ntypedef struct {\n    const SHELL_ARGS *args;\n} SHELL_SESSION;\n\nSHELL_SESSION *ShellSession_Create(void);\n// Frees session and currently attached args (if any).\nvoid ShellSession_Free(SHELL_SESSION *session);\n\n// Replaces session args ownership.\n// The session takes ownership of `args` and frees previously attached args.\nvoid ShellSession_UseArgs(SHELL_SESSION *session, const SHELL_ARGS *args);\n"
  },
  {
    "path": "src/trx/game/shell/state.c",
    "content": "#include <trx/game/shell/state.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/json.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/shell.h>\n\n#include <string.h>\n\nstatic char *m_LastPlayedMod = nullptr;\n\n#define M_LAST_PLAYED_MOD_KEY \"last_played_mod\"\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    Memory_FreePointer(&m_LastPlayedMod);\n}\n\nstatic const char *M_GetStatePath(void)\n{\n    return String_FormatStatic(\"%s/shell.json5\", Shell_GetConfigDir());\n}\n\nstatic void M_LoadState(void)\n{\n    if (m_LastPlayedMod != nullptr) {\n        return;\n    }\n\n    if (!File_Exists(M_GetStatePath())) {\n        return;\n    }\n\n    JSON_VALUE *const root = JSONFile_Read(M_GetStatePath());\n    if (root == nullptr) {\n        return;\n    }\n\n    JSON_OBJECT *const root_obj = JSON_ValueAsObject(root);\n    const char *const mod_name =\n        JSON_ObjectGetString(root_obj, M_LAST_PLAYED_MOD_KEY, nullptr);\n    if (mod_name != nullptr) {\n        m_LastPlayedMod = Memory_DupStr(mod_name);\n    }\n\n    JSON_ValueFree(root);\n}\n\nconst char *ShellState_GetLastPlayedMod(void)\n{\n    M_LoadState();\n    return m_LastPlayedMod;\n}\n\nvoid ShellState_RememberLastPlayedMod(const char *const mod_name)\n{\n    if (mod_name == nullptr) {\n        return;\n    }\n\n    M_LoadState();\n    if (m_LastPlayedMod != nullptr && strcmp(m_LastPlayedMod, mod_name) == 0) {\n        return;\n    }\n\n    Memory_FreePointer(&m_LastPlayedMod);\n    m_LastPlayedMod = Memory_DupStr(mod_name);\n\n    JSON_OBJECT *const root_obj = JSON_ObjectNew();\n    JSON_ObjectAppendString(root_obj, M_LAST_PLAYED_MOD_KEY, m_LastPlayedMod);\n\n    JSON_VALUE *const root = JSON_ValueFromObject(root_obj);\n    const char *const state_path = M_GetStatePath();\n    File_EnsureParentDirectories(state_path);\n    if (File_Exists(state_path)) {\n        JSONFile_Write(state_path, root);\n    } else {\n        size_t out_len = 0;\n        char *out_data = JSON_WritePretty(root, \"  \", \"\\n\", &out_len);\n        MYFILE *const fp = File_Open(state_path, FILE_OPEN_WRITE);\n        if (fp != nullptr) {\n            File_WriteData(fp, out_data, out_len - 1); // w/o \\0\n            File_Close(fp);\n        }\n        Memory_FreePointer(&out_data);\n    }\n    JSON_ValueFree(root);\n}\n"
  },
  {
    "path": "src/trx/game/shell/state.h",
    "content": "#pragma once\n\nconst char *ShellState_GetLastPlayedMod(void);\nvoid ShellState_RememberLastPlayedMod(const char *mod_name);\n"
  },
  {
    "path": "src/trx/game/shell.h",
    "content": "#pragma once\n\n#include <trx/game/shell/common.h>\n#include <trx/game/shell/config.h>\n#include <trx/game/shell/const.h>\n#include <trx/game/shell/events.h>\n#include <trx/game/shell/flow.h>\n#include <trx/game/shell/input.h>\n#include <trx/game/shell/paths.h>\n"
  },
  {
    "path": "src/trx/game/sound/common.c",
    "content": "#include <trx/game/sound/common.h>\n\n#include <trx/av/audio.h>\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/core/math/geom.h>\n#include <trx/core/memory.h>\n#include <trx/game/camera.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/lara.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/version.h>\n\n#include <math.h>\n#include <uthash.h>\n\ntypedef enum {\n    SF_FLIP = 0x40,\n    SF_UNFLIP = 0x80,\n} SOUND_SOURCE_FLAG;\n\n#define M_DECIBEL_LUT_SIZE 512\n#define M_SOUND_CLOSE_RANGE (1 * WALL_L)\n\n#define M_MAX_ACTIVE_SOUNDS AUDIO_MAX_ACTIVE_SAMPLES\n#define M_SOUND_RANGE_MULT_CONSTANT 4\n#define M_SOUND_MAX_VOLUME 0x8000\n#define M_SOUND_MAX_PITCH_CHANGE 6000\n#define M_SOUND_MAX_VOLUME_CHANGE (g_TRVersion >= 3 ? 0x1000 : 0x2000)\n\ntypedef struct {\n    SAMPLE_ID sample_id;\n    const SAMPLE_INFO *sample;\n    int32_t handle;\n    int32_t volume;\n    int32_t pitch;\n    int32_t pan;\n\n    XYZ_32 initial_pos;\n    const XYZ_32 *pos_ptr;\n} M_ACTIVE_SOUND;\n\ntypedef struct {\n    int number;\n    int size;\n    UT_hash_handle hh;\n} M_SAMPLE_DATA_ENTRY;\n\ntypedef struct M_SAMPLE_ENTRY {\n    SAMPLE_ID sample_id;\n    SAMPLE_INFO sample;\n    UT_hash_handle hh;\n} M_SAMPLE_ENTRY;\n\nstatic M_ACTIVE_SOUND m_ActiveSounds[M_MAX_ACTIVE_SOUNDS] = {};\nstatic bool m_Initialised = false;\nstatic float m_MasterVolume = 0.0f;\nstatic M_SAMPLE_DATA_ENTRY *m_SampleDataMap = nullptr;\nstatic M_SAMPLE_ENTRY *m_SampleMap = nullptr;\nstatic int32_t m_DecibelLUT[M_DECIBEL_LUT_SIZE] = {};\nstatic int32_t m_SourceCount = 0;\nstatic OBJECT_VECTOR *m_Sources = nullptr;\n\nstatic int M_SampleDataEntry_Cmp(\n    const M_SAMPLE_DATA_ENTRY *const a, const M_SAMPLE_DATA_ENTRY *const b)\n{\n    return a->number - b->number;\n}\n\nstatic int32_t M_ConvertVolumeToDecibel(const int32_t volume)\n{\n    int32_t idx = volume * g_Config.audio.master_volume * m_MasterVolume\n        * M_DECIBEL_LUT_SIZE / M_SOUND_MAX_VOLUME;\n    CLAMP(idx, 0, M_DECIBEL_LUT_SIZE - 1);\n    return m_DecibelLUT[idx];\n}\n\nstatic int32_t M_ConvertPanToDecibel(const uint16_t pan)\n{\n    const int32_t result =\n        sin((pan / 32767.0) * M_PI) * (M_DECIBEL_LUT_SIZE / 2);\n    if (result > 0) {\n        return -m_DecibelLUT[M_DECIBEL_LUT_SIZE - result];\n    } else if (result < 0) {\n        return m_DecibelLUT[M_DECIBEL_LUT_SIZE + result];\n    } else {\n        return 0;\n    }\n}\n\nstatic float M_ConvertPitch(const int32_t pitch)\n{\n    return pitch / 0x10000.p0;\n}\n\nstatic int32_t M_GetDistance(\n    const SAMPLE_INFO *const sample, const XYZ_32 *const pos)\n{\n    if (pos == nullptr) {\n        return 0;\n    }\n    const XYZ_32 delta = {\n        .x = pos->x - g_Camera.mic_pos.x,\n        .y = pos->y - g_Camera.mic_pos.y,\n        .z = pos->z - g_Camera.mic_pos.z,\n    };\n    const int32_t distance = XYZ_32_GetLength(delta);\n    if (distance > sample->range) {\n        return INT32_MAX;\n    } else if (distance < M_SOUND_CLOSE_RANGE) {\n        return 0;\n    } else {\n        return distance - M_SOUND_CLOSE_RANGE;\n    }\n}\n\nstatic int32_t M_GetVolume(\n    const SAMPLE_INFO *const sample, const int32_t distance, const bool random)\n{\n    int32_t volume = sample->volume;\n    if (random && sample->flags.randomize_volume) {\n        volume -= Random_GetDraw() * M_SOUND_MAX_VOLUME_CHANGE / 0x8000;\n    }\n\n    if (g_TRVersion == 1) {\n        return volume - distance * 3.5f;\n    }\n\n    const int32_t attenuation =\n        SQUARE(distance) / (SQUARE(sample->range) / 0x10000);\n    return (volume * (0x10000 - attenuation)) / 0x10000;\n}\n\nstatic int32_t M_GetPitch(const SAMPLE_INFO *const sample, const uint32_t flags)\n{\n    int32_t pitch = (flags & SPM_PITCH) != 0 ? (flags >> 8) & 0xFFFFFF\n                                             : SOUND_DEFAULT_PITCH;\n    pitch += sample->pitch * (1 << 9);\n    if (!g_Config.audio.enable_pitched_sounds) {\n        return pitch;\n    }\n    if (sample->flags.randomize_pitch) {\n        pitch += ((Random_GetDraw() * M_SOUND_MAX_PITCH_CHANGE) / 0x4000)\n            - M_SOUND_MAX_PITCH_CHANGE;\n    }\n    return pitch;\n}\n\nstatic int32_t M_GetPan(\n    const SAMPLE_INFO *const sample, const XYZ_32 *const pos)\n{\n    if (pos == nullptr) {\n        return 0;\n    }\n    const int32_t distance = M_GetDistance(sample, pos);\n    if (distance > 0 && !sample->flags.no_pan) {\n        const int32_t dx = pos->x - g_Camera.mic_pos.x;\n        const int32_t dz = pos->z - g_Camera.mic_pos.z;\n        return (int16_t)Math_Atan(dz, dx) - g_Camera.actual_angle;\n    }\n    return 0;\n}\n\nstatic M_ACTIVE_SOUND *M_SelectUnusedSound(void)\n{\n    // Try to get an unused slot\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i];\n        if (sound->sample == nullptr) {\n            return sound;\n        }\n    }\n\n    // No sound found - try to find the most quiet track, and use this one\n    M_ACTIVE_SOUND *best_sound = nullptr;\n    int32_t min_volume = INT32_MAX;\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i];\n        if (sound->sample != nullptr && sound->volume < min_volume) {\n            min_volume = sound->volume;\n            best_sound = sound;\n        }\n    }\n\n    return best_sound;\n}\n\nstatic M_ACTIVE_SOUND *M_SelectUsedSound(const SAMPLE_ID sample_id)\n{\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const result = &m_ActiveSounds[i];\n        if (result->sample_id == sample_id) {\n            return result;\n        }\n    }\n    return nullptr;\n}\n\nstatic M_ACTIVE_SOUND *M_SelectUsedSoundWithPos(\n    const SAMPLE_ID sample_id, const XYZ_32 *const pos)\n{\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const result = &m_ActiveSounds[i];\n        if (result->sample_id == sample_id && result->pos_ptr == pos) {\n            return result;\n        }\n    }\n    return nullptr;\n}\n\nstatic void M_ClearActiveSound(M_ACTIVE_SOUND *const sound)\n{\n    sound->sample = nullptr;\n    sound->sample_id = SFX_INVALID;\n    sound->handle = AUDIO_NO_SOUND;\n}\n\nstatic void M_ClearAllActiveSounds(void)\n{\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i];\n        M_ClearActiveSound(sound);\n    }\n}\n\nstatic void M_CloseActiveSound(M_ACTIVE_SOUND *const sound)\n{\n    Audio_Sample_Close(sound->handle);\n    M_ClearActiveSound(sound);\n}\n\nstatic void M_ClearActiveSoundHandles(const M_ACTIVE_SOUND *const sound)\n{\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const rsound = &m_ActiveSounds[i];\n        if (rsound != sound && rsound->handle == sound->handle) {\n            rsound->handle = AUDIO_NO_SOUND;\n        }\n    }\n}\n\nstatic void M_ClearSampleMaps(void)\n{\n    M_SAMPLE_DATA_ENTRY *sentry, *stmp;\n    HASH_ITER(hh, m_SampleDataMap, sentry, stmp)\n    {\n        HASH_DEL(m_SampleDataMap, sentry);\n        Memory_Free(sentry);\n    }\n    M_SAMPLE_ENTRY *entry, *tmp;\n    HASH_ITER(hh, m_SampleMap, entry, tmp)\n    {\n        HASH_DEL(m_SampleMap, entry);\n        Memory_Free(entry);\n    }\n}\n\nstatic void M_SyncActiveSoundHandle(M_ACTIVE_SOUND *const sound)\n{\n    Audio_Sample_SetPan(sound->handle, M_ConvertPanToDecibel(sound->pan));\n    Audio_Sample_SetPitch(sound->handle, M_ConvertPitch(sound->pitch));\n    Audio_Sample_SetVolume(\n        sound->handle, M_ConvertVolumeToDecibel(sound->volume));\n}\n\nstatic void M_UpdateActiveSoundParams(M_ACTIVE_SOUND *const sound)\n{\n    const int32_t distance = M_GetDistance(sound->sample, sound->pos_ptr);\n    if (distance == INT32_MAX) {\n        sound->volume = 0;\n        return;\n    }\n    int32_t volume = M_GetVolume(sound->sample, distance, false);\n    if (volume < 0) {\n        sound->volume = 0;\n        return;\n    }\n\n    sound->volume = volume;\n    sound->pan = M_GetPan(sound->sample, sound->pos_ptr);\n}\n\nbool Sound_Init(void)\n{\n    m_MasterVolume = g_Config.audio.sound_volume;\n    m_DecibelLUT[0] = -10000;\n\n    for (int32_t i = 1; i < M_DECIBEL_LUT_SIZE; i++) {\n        if (g_TRVersion < 3) {\n            // Legacy scale\n            m_DecibelLUT[i] =\n                (log2(1.0 / M_DECIBEL_LUT_SIZE) - log2(1.0 / i)) * 1000;\n        } else {\n            // Hundredths of a dB in the range [-10000..0].\n            // Later we apply a linear gain of `10^(centi_dB/2000)`.\n            const double gain = (double)i / (double)M_DECIBEL_LUT_SIZE;\n            int32_t centi_db = (int32_t)lrint(2000.0 * log10(gain));\n            CLAMP(centi_db, -10000, 0);\n            m_DecibelLUT[i] = centi_db;\n        }\n    }\n\n    if (!Audio_Init()) {\n        LOG_ERROR(\"Failed to initialize libtrx sound system\");\n        return false;\n    }\n\n    m_Initialised = true;\n    M_ClearAllActiveSounds();\n    return true;\n}\n\nvoid Sound_Shutdown(void)\n{\n    m_Initialised = false;\n    Audio_Shutdown();\n    M_ClearSampleMaps();\n}\n\nbool Sound_IsInitialised(void)\n{\n    return m_Initialised;\n}\n\nvoid Sound_SetMasterVolume(const float volume)\n{\n    m_MasterVolume = volume;\n}\n\nuint8_t Sound_GetReverbType(void)\n{\n    return Audio_GetReverbType();\n}\n\nvoid Sound_SetReverbType(uint8_t reverb_type)\n{\n    Audio_SetReverbType(reverb_type);\n}\n\nvoid Sound_ResetSamples(void)\n{\n    if (!Sound_IsInitialised()) {\n        return;\n    }\n    Audio_Sample_CloseAll();\n    Audio_Sample_UnloadAll();\n    M_ClearAllActiveSounds();\n    M_ClearSampleMaps();\n}\n\nbool Sound_LoadSampleData(\n    const int32_t sample_data_id, const char *const sample_data,\n    const size_t size)\n{\n    if (!Sound_IsInitialised()) {\n        return false;\n    }\n    return Audio_Sample_Load(sample_data_id, sample_data, size);\n}\n\nint32_t Sound_ReserveSampleData(int32_t index, const int32_t how_many)\n{\n    M_SAMPLE_DATA_ENTRY *entry;\n\n    if (index != -1) {\n        HASH_FIND_INT(m_SampleDataMap, &index, entry);\n        if (entry != nullptr) {\n            return index;\n        }\n    } else {\n        index = 0;\n\n        // Ensure entries are ordered by starting slot\n        HASH_SORT(m_SampleDataMap, M_SampleDataEntry_Cmp);\n        // Find first gap large enough for how_many slots\n        M_SAMPLE_DATA_ENTRY *e;\n        M_SAMPLE_DATA_ENTRY *prev = nullptr;\n        for (e = m_SampleDataMap; e != nullptr; prev = e, e = e->hh.next) {\n            if (prev == nullptr && e->number >= how_many) {\n                index = 0;\n                break;\n            }\n            if (prev != nullptr\n                && e->number - (prev->number + prev->size) >= how_many) {\n                index = prev->number + prev->size;\n                break;\n            }\n        }\n        if (e == nullptr && prev != nullptr) {\n            index = prev->number + prev->size;\n        }\n    }\n\n    entry = Memory_Alloc(sizeof(*entry));\n    entry->number = index;\n    entry->size = how_many;\n    HASH_ADD_INT(m_SampleDataMap, number, entry);\n    return index;\n}\n\nSAMPLE_INFO *Sound_GetSample(const SAMPLE_ID sample_id)\n{\n    M_SAMPLE_ENTRY *entry = nullptr;\n    if (sample_id == SFX_INVALID) {\n        return nullptr;\n    }\n    HASH_FIND_INT(m_SampleMap, &sample_id, entry);\n    return entry != nullptr ? &entry->sample : nullptr;\n}\n\nSAMPLE_INFO *Sound_GetOrCreateSample(const SAMPLE_ID sample_id)\n{\n    SAMPLE_INFO *const sample = Sound_GetSample(sample_id);\n    if (sample != nullptr) {\n        return sample;\n    }\n    M_SAMPLE_ENTRY *const entry = Memory_Alloc(sizeof(*entry));\n    entry->sample_id = sample_id;\n    HASH_ADD_INT(m_SampleMap, sample_id, entry);\n    return &entry->sample;\n}\n\nbool Sound_IsAvailable_Direct(const SAMPLE_ID sample_id)\n{\n    return Sound_GetSample(sample_id) != nullptr;\n}\n\nbool Sound_IsAvailable(const SAMPLE_TRX_ID sample_id)\n{\n    return Sound_IsAvailable_Direct(Sound_ToGameID(sample_id));\n}\n\n// Get the maximum direct SAMPLE_ID loaded for playback.\n// Returns SFX_INVALID if no samples are available.\nSAMPLE_ID Sound_GetMaxDirectSampleID(void)\n{\n    M_SAMPLE_ENTRY *entry, *tmp;\n    SAMPLE_ID max_id = SFX_INVALID;\n    HASH_ITER(hh, m_SampleMap, entry, tmp)\n    {\n        if (entry->sample_id > max_id) {\n            max_id = entry->sample_id;\n        }\n    }\n    return max_id;\n}\n\nvoid Sound_InitialiseSources(const int32_t num_sources)\n{\n    m_SourceCount = num_sources;\n    m_Sources = num_sources == 0\n        ? nullptr\n        : GameBuf_Alloc(\n              num_sources * sizeof(OBJECT_VECTOR), GBUF_SOUND_SOURCES);\n}\n\nint32_t Sound_GetSourceCount(void)\n{\n    return m_SourceCount;\n}\n\nOBJECT_VECTOR *Sound_GetSource(const int32_t source_idx)\n{\n    if (m_Sources == nullptr) {\n        return nullptr;\n    }\n    return &m_Sources[source_idx];\n}\n\nvoid Sound_ResetSources(void)\n{\n    const bool flip_status = Room_GetFlipStatus();\n    for (int32_t i = 0; i < m_SourceCount; i++) {\n        OBJECT_VECTOR *const source = &m_Sources[i];\n        if ((flip_status && (source->flags & SF_FLIP))\n            || (!flip_status && (source->flags & SF_UNFLIP))) {\n            Sound_Effect_Direct(source->data, &source->pos, SPM_NORMAL);\n        }\n    }\n}\n\nbool Sound_Effect_Direct(\n    const SAMPLE_ID sample_id, const XYZ_32 *const pos, const uint32_t flags)\n{\n    if (!Sound_IsInitialised()) {\n        return false;\n    }\n\n    if ((flags & SPM_ALWAYS) == 0) {\n        const bool play_underwater = (flags & SPM_UNDERWATER) != 0;\n        const ROOM *const room = Room_Get(g_Camera.pos.room_num);\n        const bool room_submerged = room != nullptr && room->flags.underwater;\n        if (play_underwater != room_submerged) {\n            return false;\n        }\n    }\n\n    const SAMPLE_INFO *const sample = Sound_GetSample(sample_id);\n    if (sample == nullptr || sample->number < 0) {\n        return false;\n    }\n\n    if (sample->randomness) {\n        int32_t r = Random_GetDraw();\n        if (g_TRVersion >= 3) {\n            r &= 0xFF;\n        }\n        if (r > sample->randomness) {\n            return false;\n        }\n    }\n\n    const int32_t distance = M_GetDistance(sample, pos);\n    if (distance == INT32_MAX) {\n        return false;\n    }\n\n    const int32_t pan = M_GetPan(sample, pos);\n    const int32_t volume = M_GetVolume(sample, distance, true);\n    if (volume <= 0) {\n        return false;\n    }\n\n    const int32_t pitch = M_GetPitch(sample, flags);\n    const int32_t num_samples = sample->flags.num_samples;\n    const int32_t track_id = num_samples == 1\n        ? sample->number\n        : sample->number + ((num_samples * Random_GetDraw()) / 0x8000);\n\n    M_ACTIVE_SOUND *sound = nullptr;\n    switch (sample->mode) {\n    case SAMPLE_MODE_NORMAL:\n        sound = M_SelectUnusedSound();\n        break;\n\n    case SAMPLE_MODE_WAIT:\n        sound = g_TRVersion == 1 ? M_SelectUsedSoundWithPos(sample_id, pos)\n                                 : M_SelectUsedSound(sample_id);\n        if (sound != nullptr && Audio_Sample_IsPlaying(sound->handle)) {\n            return true;\n        }\n        if (sound == nullptr) {\n            sound = M_SelectUnusedSound();\n        }\n        break;\n\n    case SAMPLE_MODE_RESTART:\n        sound = M_SelectUsedSound(sample_id);\n        if (sound == nullptr) {\n            sound = M_SelectUnusedSound();\n        }\n        break;\n\n    case SAMPLE_MODE_LOOPED:\n        sound = M_SelectUsedSound(sample_id);\n        if (sound != nullptr) {\n            if (volume > sound->volume) {\n                sound->volume = volume;\n                sound->pan = pan;\n                sound->pitch = pitch;\n            }\n            return true;\n        }\n        sound = M_SelectUnusedSound();\n        break;\n    }\n\n    if (sound == nullptr) {\n        return false;\n    }\n\n    M_CloseActiveSound(sound);\n    const int32_t handle = Audio_Sample_Play(\n        track_id, M_ConvertVolumeToDecibel(volume), M_ConvertPitch(pitch),\n        M_ConvertPanToDecibel(pan), sample->mode == SAMPLE_MODE_LOOPED);\n    if (handle == AUDIO_NO_SOUND) {\n        return false;\n    }\n    sound->sample = sample;\n    sound->sample_id = sample_id;\n    sound->handle = handle;\n    sound->volume = volume;\n    sound->pitch = pitch;\n    sound->pan = pan;\n    if (pos != nullptr) {\n        sound->initial_pos = *pos;\n        if (flags & SPM_STATIC_POS) {\n            sound->pos_ptr = &sound->initial_pos;\n        } else {\n            sound->pos_ptr = pos;\n        }\n    } else {\n        sound->pos_ptr = nullptr;\n    }\n    M_ClearActiveSoundHandles(sound);\n    return true;\n}\n\nbool Sound_Effect(\n    const SAMPLE_TRX_ID sample_id, const XYZ_32 *const pos,\n    const uint32_t flags)\n{\n    return Sound_Effect_Direct(Sound_ToGameID(sample_id), pos, flags);\n}\n\nvoid Sound_StopEffect_Direct(const SAMPLE_ID sample_id)\n{\n    if (!Sound_IsInitialised()) {\n        return;\n    }\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i];\n        if (sound->sample_id == sample_id) {\n            M_CloseActiveSound(sound);\n        }\n    }\n}\n\nvoid Sound_StopEffect(const SAMPLE_TRX_ID sample_id)\n{\n    Sound_StopEffect_Direct(Sound_ToGameID(sample_id));\n}\n\nvoid Sound_ResetAmbient(void)\n{\n    if (!Sound_IsInitialised()) {\n        return;\n    }\n    Sound_ResetSources();\n}\n\nvoid Sound_UpdateEffects(void)\n{\n    if (!Sound_IsInitialised()) {\n        return;\n    }\n    for (int32_t i = 0; i < M_MAX_ACTIVE_SOUNDS; i++) {\n        M_ACTIVE_SOUND *const sound = &m_ActiveSounds[i];\n        if (sound->sample == nullptr) {\n            continue;\n        }\n\n        if (sound->sample->mode == SAMPLE_MODE_LOOPED) {\n            if (sound->volume <= 0) {\n                M_CloseActiveSound(sound);\n            } else {\n                M_SyncActiveSoundHandle(sound);\n                sound->volume = 0;\n            }\n        } else if (!Audio_Sample_IsPlaying(sound->handle)) {\n            M_ClearActiveSound(sound);\n        } else if (g_TRVersion == 1 && sound->pos_ptr != nullptr) {\n            M_UpdateActiveSoundParams(sound);\n            if (sound->volume <= 0) {\n                M_CloseActiveSound(sound);\n            } else {\n                M_SyncActiveSoundHandle(sound);\n            }\n        }\n    }\n}\n\nvoid Sound_PauseAll(void)\n{\n    Audio_Sample_PauseAll();\n}\n\nvoid Sound_UnpauseAll(void)\n{\n    Audio_Sample_UnpauseAll();\n}\n\nvoid Sound_StopAll(void)\n{\n    if (!Sound_IsInitialised()) {\n        return;\n    }\n    Audio_Sample_CloseAll();\n    M_ClearAllActiveSounds();\n}\n"
  },
  {
    "path": "src/trx/game/sound/common.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/sound/enum.h>\n#include <trx/game/sound/ids.h>\n#include <trx/game/sound/types.h>\n#include <trx/game/types.h>\n\n#include <stddef.h>\n\n#define SOUND_DEFAULT_PITCH 0x10000\n\nbool Sound_Init(void);\nvoid Sound_Shutdown(void);\nbool Sound_IsInitialised(void);\n\nvoid Sound_SetMasterVolume(float volume);\nuint8_t Sound_GetReverbType(void);\nvoid Sound_SetReverbType(uint8_t reverb_type);\n\nvoid Sound_ResetSamples(void);\n\nbool Sound_LoadSampleData(\n    int32_t sample_data_id, const char *sample_data, size_t size);\n\nvoid Sound_InitialiseSources(int32_t num_sources);\nint32_t Sound_GetSourceCount(void);\nOBJECT_VECTOR *Sound_GetSource(int32_t source_idx);\nvoid Sound_ResetSources(void);\n\n// Reserve a contiguous block of sample data IDs for loading audio samples.\n// Returns the starting sample_data_id for the reserved block of size how_many.\nint32_t Sound_ReserveSampleData(int32_t index, int32_t how_many);\n\n// Look up an existing SAMPLE_INFO by SAMPLE_ID. Returns nullptr if not found.\nSAMPLE_INFO *Sound_GetSample(SAMPLE_ID sample_id);\n\n// Get or create a SAMPLE_INFO for the given SAMPLE_ID.\n// If no sample is found, a new sample slot is created.\nSAMPLE_INFO *Sound_GetOrCreateSample(SAMPLE_ID sample_id);\n\n// Returns true if a SAMPLE_INFO exists for the given SAMPLE_ID.\nbool Sound_IsAvailable_Direct(SAMPLE_ID sample_id);\nbool Sound_IsAvailable(SAMPLE_TRX_ID sample_id);\n\n// Get the maximum direct SAMPLE_ID loaded for playback.\n// Returns SFX_INVALID if no samples are available.\nSAMPLE_ID Sound_GetMaxDirectSampleID(void);\n\n// Play a sample with the given number.\n// pos is an optional argument that takes the world position to play the sound\n// at and can be nullptr.\nbool Sound_Effect_Direct(SAMPLE_ID sfx_num, const XYZ_32 *pos, uint32_t flags);\nbool Sound_Effect(SAMPLE_TRX_ID sfx_num, const XYZ_32 *pos, uint32_t flags);\n\nvoid Sound_StopEffect_Direct(SAMPLE_ID sfx_num);\nvoid Sound_StopEffect(SAMPLE_TRX_ID sfx_num);\n\nvoid Sound_ResetAmbient(void);\nvoid Sound_UpdateEffects(void);\n\nvoid Sound_PauseAll(void);\nvoid Sound_UnpauseAll(void);\nvoid Sound_StopAll(void);\n"
  },
  {
    "path": "src/trx/game/sound/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    SAMPLE_MODE_NORMAL = 0,\n    SAMPLE_MODE_WAIT = 1,\n    SAMPLE_MODE_RESTART = 2,\n    SAMPLE_MODE_LOOPED = 3,\n} SAMPLE_MODE;\n\n// clang-format off\ntypedef enum {\n    SPM_NORMAL     = 0,\n    SPM_UNDERWATER = 1,\n    SPM_ALWAYS     = 2,\n    SPM_PITCH      = 4,\n    SPM_STATIC_POS = 8,\n} SOUND_PLAY_MODE;\n// clang-format on\n"
  },
  {
    "path": "src/trx/game/sound/ids.c",
    "content": "#include <trx/game/catalog/manager.h>\n#include <trx/game/sound.h>\n\nSAMPLE_ID Sound_ToGameID(const SAMPLE_TRX_ID trx_id)\n{\n    int32_t out;\n    if (Catalog_EnumToGameID(CATALOG_SAMPLES, trx_id, &out)) {\n        return out;\n    }\n    return SFX_INVALID;\n}\n\nSAMPLE_TRX_ID Sound_FromGameID(const SAMPLE_ID sample_id)\n{\n    CATALOG_ID out;\n    if (Catalog_GameIDToEnum(CATALOG_SAMPLES, sample_id, &out)) {\n        return out;\n    }\n    return SFX_TRX_INVALID;\n}\n"
  },
  {
    "path": "src/trx/game/sound/ids.h",
    "content": "#pragma once\n\ntypedef enum {\n    SFX_INVALID = -1,\n} SAMPLE_ID;\n\ntypedef enum {\n    SFX_TRX_INVALID = -1,\n#define X_CATALOG_ID(enum_value) enum_value,\n#include <trx/game/catalog/samples.def>\n#undef X_CATALOG_ID\n} SAMPLE_TRX_ID;\n\nSAMPLE_ID Sound_ToGameID(SAMPLE_TRX_ID sample_id);\nSAMPLE_TRX_ID Sound_FromGameID(SAMPLE_ID sample_id);\n"
  },
  {
    "path": "src/trx/game/sound/types.h",
    "content": "#pragma once\n\n#include <trx/game/sound/enum.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    int16_t number;\n    int16_t volume;\n    int32_t range;\n    int32_t randomness;\n    int8_t pitch;\n    union {\n        struct {\n            uint16_t mode_bits : 2;\n            uint16_t num_samples : 4;\n            uint16_t reserved : 6;\n            uint16_t no_pan : 1;\n            uint16_t randomize_pitch : 1;\n            uint16_t randomize_volume : 1;\n            uint16_t : 1;\n        };\n        uint16_t all;\n    } flags;\n    SAMPLE_MODE mode;\n} SAMPLE_INFO;\n"
  },
  {
    "path": "src/trx/game/sound.h",
    "content": "#pragma once\n\n#include <trx/game/sound/common.h>\n#include <trx/game/sound/ids.h>\n#include <trx/game/sound/types.h>\n"
  },
  {
    "path": "src/trx/game/sparks/enum.h",
    "content": "#pragma once\n\nenum {\n    // clang-format off\n    SPARK_F_NONE          = 0x0,\n    SPARK_F_SCALE         = 0x2,\n    SPARK_F_BLOOD         = 0x4,\n    SPARK_F_SPRITE        = 0x8,\n    SPARK_F_ROTATE        = 0x10,\n    SPARK_F_FX            = 0x40,\n    SPARK_F_ITEM          = 0x80,\n    SPARK_F_OUTSIDE       = 0x100,\n    SPARK_F_ALT_SPRITE    = 0x200,\n    SPARK_F_ATTACHED_POS  = 0x400,\n    SPARK_F_UNDERWATER    = 0x800,\n    SPARK_F_ATTACHED_NODE = 0x1000,\n    SPARK_F_GREEN         = 0x2000,\n    // clang-format on\n};\n"
  },
  {
    "path": "src/trx/game/sparks/manager.c",
    "content": "#include <trx/game/sparks/manager.h>\n\n#include <trx/config.h>\n#include <trx/debug.h>\n#include <trx/game/effects.h>\n#include <trx/game/lara.h>\n#include <trx/game/output/lights.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n#include <trx/version.h>\n\n#define M_MAX_SPARKS 400\n#define M_MAX_SPARK_DYNAMICS 32\n\ntypedef struct {\n    bool on;\n    uint8_t falloff;\n    RGB_888 color;\n    uint8_t flags;\n} M_SPARK_DYNAMIC;\n\nstatic SPARK m_Sparks[M_MAX_SPARKS];\nstatic M_SPARK_DYNAMIC m_Dynamics[M_MAX_SPARK_DYNAMICS];\nstatic int32_t m_NextSpark = 0;\nstatic XZ_32 m_SmokeWind = {};\nstatic int32_t m_HairWindZ = 0;\nstatic int32_t m_TR3Wind = 0;\nstatic int32_t m_TR3WindAngle = DEG_180;\nstatic int32_t m_TR3DWindAngle = DEG_180;\n\nstatic const BITE m_NodeOffsets[16] = {\n    { .pos = { 0, 340, 64 }, .mesh_num = 7 },\n    { .pos = { 0, 0, -96 }, .mesh_num = 10 },\n    { .pos = { 16, 48, 320 }, .mesh_num = 13 },\n    { .pos = { 0, -256, 0 }, .mesh_num = 5 },\n    { .pos = { 0, 64, 0 }, .mesh_num = 10 },\n    { .pos = { 0, 64, 0 }, .mesh_num = 13 },\n    { .pos = { -32, -16, -192 }, .mesh_num = 13 },\n    { .pos = { -64, 410, 0 }, .mesh_num = 20 },\n    { .pos = { 64, 410, 0 }, .mesh_num = 23 },\n    { .pos = { -160, -8, 16 }, .mesh_num = 5 },\n    { .pos = { -160, -8, 16 }, .mesh_num = 9 },\n    { .pos = { -160, -8, 16 }, .mesh_num = 13 },\n    { .pos = { 0, 0, 0 }, .mesh_num = 0 },\n    { .pos = { 0, 0, 0 }, .mesh_num = 0 },\n    { .pos = { 0, 0, 0 }, .mesh_num = 0 },\n    { .pos = { 0, 0, 0 }, .mesh_num = 0 },\n};\n\nXYZ_32 Sparks_GetWorldPos(const SPARK *const spark)\n{\n    if (spark == nullptr) {\n        return (XYZ_32) { 0, 0, 0 };\n    }\n\n    if ((spark->flags & SPARK_F_FX) != 0U) {\n        const EFFECT *const effect = Effect_Get(spark->effect_num);\n        if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) {\n            return effect->pos;\n        }\n        return (XYZ_32) {\n            .x = effect->pos.x + spark->pos.x,\n            .y = effect->pos.y + spark->pos.y,\n            .z = effect->pos.z + spark->pos.z,\n        };\n    }\n\n    if ((spark->flags & SPARK_F_ITEM) != 0U) {\n        const ITEM *const item = Item_Get(spark->item_num);\n        if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) {\n            return item->pos;\n        }\n\n        if ((spark->flags & SPARK_F_ATTACHED_NODE) != 0U) {\n            XYZ_32 joint_pos = m_NodeOffsets[spark->node_num & 0xF].pos;\n            Collide_GetJointAbsPosition(\n                item, &joint_pos,\n                m_NodeOffsets[spark->node_num & 0xF].mesh_num);\n            return (XYZ_32) {\n                .x = joint_pos.x + spark->pos.x,\n                .y = joint_pos.y + spark->pos.y,\n                .z = joint_pos.z + spark->pos.z,\n            };\n        }\n\n        return (XYZ_32) {\n            .x = item->pos.x + spark->pos.x,\n            .y = item->pos.y + spark->pos.y,\n            .z = item->pos.z + spark->pos.z,\n        };\n    }\n\n    return spark->pos;\n}\n\nstatic int32_t M_GetFreeSpark(void)\n{\n    int32_t idx = m_NextSpark;\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        if (!m_Sparks[idx].on) {\n            m_NextSpark = (idx + 1) & 0xBF;\n            return idx;\n        }\n        idx = idx == (M_MAX_SPARKS - 1) ? 0 : idx + 1;\n    }\n\n    int32_t free = 0;\n    int32_t min_life = INT32_MAX;\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        const SPARK *const spark = &m_Sparks[i];\n        if ((int32_t)spark->life < min_life && spark->dynamic == -1\n            && ((spark->flags & SPARK_F_BLOOD) == 0U || (i & 1) != 0)) {\n            free = i;\n            min_life = (int32_t)spark->life;\n        }\n    }\n\n    m_NextSpark = (free + 1) & 0xBF;\n    return free;\n}\n\nSPARK *Sparks_GetFreeSpark(void)\n{\n    const int32_t idx = M_GetFreeSpark();\n    return &m_Sparks[idx];\n}\n\nSPARK *Sparks_GetSpark(const int32_t idx)\n{\n    ASSERT(idx >= 0 && idx < M_MAX_SPARKS);\n    return &m_Sparks[idx];\n}\n\nvoid Sparks_Sync(SPARK *const spark)\n{\n    if (spark == nullptr) {\n        return;\n    }\n\n    const XYZ_32 world_pos = Sparks_GetWorldPos(spark);\n    spark->prev_pos = spark->pos;\n    spark->prev_world_pos = world_pos;\n    spark->prev_color = spark->color;\n    spark->prev_size = spark->size;\n    spark->prev_rot_angle = spark->rot_angle;\n}\n\nvoid Sparks_FinishSetup(SPARK *const spark)\n{\n    if (spark == nullptr) {\n        return;\n    }\n\n    spark->color = spark->src_color;\n    Sparks_Sync(spark);\n}\n\nint8_t Sparks_AllocDynamic(const uint8_t flags)\n{\n    for (int32_t i = 0; i < M_MAX_SPARK_DYNAMICS; i++) {\n        if (!m_Dynamics[i].on) {\n            m_Dynamics[i].on = true;\n            m_Dynamics[i].falloff = 4;\n            m_Dynamics[i].flags = flags;\n            m_Dynamics[i].color = COLOR_RGB_888_BLACK;\n            return (int8_t)i;\n        }\n    }\n    return -1;\n}\n\nvoid Sparks_FreeDynamic(const int8_t idx)\n{\n    if (idx < 0 || idx >= M_MAX_SPARK_DYNAMICS) {\n        return;\n    }\n    m_Dynamics[idx].on = false;\n}\n\nvoid Sparks_Reset(void)\n{\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        m_Sparks[i].on = false;\n        m_Sparks[i].dynamic = -1;\n    }\n    for (int32_t i = 0; i < M_MAX_SPARK_DYNAMICS; i++) {\n        m_Dynamics[i].on = false;\n    }\n    m_NextSpark = 0;\n    m_SmokeWind = (XZ_32) {};\n    m_HairWindZ = 0;\n    m_TR3Wind = 0;\n    m_TR3WindAngle = DEG_180;\n    m_TR3DWindAngle = DEG_180;\n}\n\nXZ_32 Sparks_GetSmokeWind(void)\n{\n    return m_SmokeWind;\n}\n\nvoid Sparks_SetSmokeWind(const XZ_32 wind)\n{\n    m_SmokeWind = wind;\n}\n\nint32_t Sparks_GetHairWindZ(void)\n{\n    return m_HairWindZ;\n}\n\nstatic void M_UpdateWind(void)\n{\n    if (!g_Config.visuals.enable_breeze) {\n        m_SmokeWind = (XZ_32) {};\n        m_HairWindZ = 0;\n        return;\n    }\n\n    if (g_TRVersion != 3) {\n        const ITEM *const lara_item = Lara_GetItem();\n        if (lara_item == nullptr) {\n            m_HairWindZ = 0;\n            return;\n        }\n\n        const ROOM *const room = Room_Get(lara_item->room_num);\n        if (room == nullptr || !room->flags.wind) {\n            m_HairWindZ = 0;\n            return;\n        }\n\n        const int32_t random = Random_GetDraw() & 7;\n        if (random != 0) {\n            m_HairWindZ += random - 4;\n            if (m_HairWindZ < 0) {\n                m_HairWindZ = 0;\n            } else if (m_HairWindZ >= 8) {\n                m_HairWindZ--;\n            }\n        }\n        m_SmokeWind = (XZ_32) {};\n        return;\n    }\n\n    // TR3 wind logic: a small random wind magnitude with a slowly-changing\n    // direction, biased to the [90°, 270°] range.\n    m_TR3Wind += (Random_GetControl() & 7) - 3;\n    if (m_TR3Wind <= -2) {\n        m_TR3Wind++;\n    } else if (m_TR3Wind >= 9) {\n        m_TR3Wind--;\n    }\n\n    // Original TR3 uses a 0..4095 angle space; keep the calculations faithful.\n    m_TR3DWindAngle =\n        (m_TR3DWindAngle + (((Random_GetControl() & 0x3F) - 32) * 2)) & 0x1FFE;\n\n    if (m_TR3DWindAngle < 1024) { // DEG_90\n        m_TR3DWindAngle += (1024 - m_TR3DWindAngle) << 1;\n    } else if (m_TR3DWindAngle > 3072) { // DEG_270\n        m_TR3DWindAngle -= (m_TR3DWindAngle - 3072) << 1;\n    }\n    m_TR3DWindAngle &= 0x1FFE;\n    m_TR3WindAngle =\n        (m_TR3WindAngle + ((m_TR3DWindAngle - m_TR3WindAngle) >> 3)) & 0x1FFE;\n\n    // Promote to DEG_360 for Math_Sin/Cos just at the end.\n    m_SmokeWind = (XZ_32) {\n        .x = (m_TR3Wind * Math_Sin(m_TR3WindAngle << 3)) >> W2V_SHIFT,\n        .z = (m_TR3Wind * Math_Cos(m_TR3WindAngle << 3)) >> W2V_SHIFT,\n    };\n\n    m_HairWindZ = 0;\n}\n\nvoid Sparks_Control(void)\n{\n    M_UpdateWind();\n\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        SPARK *const spark = &m_Sparks[i];\n        if (!spark->on) {\n            continue;\n        }\n\n        if ((spark->flags & SPARK_F_ATTACHED_POS) == 0U || spark->life > 16) {\n            if (spark->life > 0) {\n                spark->life--;\n            }\n        }\n\n        if (spark->life == 0) {\n            if (spark->dynamic != -1) {\n                m_Dynamics[(uint8_t)spark->dynamic].on = false;\n                spark->dynamic = -1;\n            }\n\n            spark->on = false;\n            continue;\n        }\n\n        const int32_t lived = (int32_t)spark->s_life - (int32_t)spark->life;\n\n        spark->prev_pos = spark->pos;\n        spark->prev_world_pos = Sparks_GetWorldPos(spark);\n        spark->prev_color = spark->color;\n        spark->prev_size = spark->size;\n        spark->prev_rot_angle = spark->rot_angle;\n\n        // Color fade: src -> dst, then fade-to-black.\n        if (lived < (int32_t)spark->col_fade_speed\n            && spark->col_fade_speed != 0U) {\n            const float fade = lived / (float)spark->col_fade_speed;\n            spark->color.r = LERP(\n                (int32_t)spark->src_color.r, (int32_t)spark->dst_color.r, fade);\n            spark->color.g = LERP(\n                (int32_t)spark->src_color.g, (int32_t)spark->dst_color.g, fade);\n            spark->color.b = LERP(\n                (int32_t)spark->src_color.b, (int32_t)spark->dst_color.b, fade);\n        } else if (\n            spark->life < spark->fade_to_black && spark->fade_to_black != 0U) {\n            const float fade = spark->life / (float)spark->fade_to_black;\n            spark->color.r = spark->dst_color.r * fade;\n            spark->color.g = spark->dst_color.g * fade;\n            spark->color.b = spark->dst_color.b * fade;\n        } else {\n            spark->color = spark->dst_color;\n        }\n\n        if (spark->life == spark->fade_to_black\n            && (spark->flags & SPARK_F_UNDERWATER) != 0U) {\n            spark->dst_size.width >>= 2;\n            spark->dst_size.height >>= 2;\n        }\n\n        if ((spark->flags & SPARK_F_ROTATE) != 0U) {\n            spark->rot_angle = (spark->rot_angle + spark->rot_add) & 0xFFF;\n        }\n\n        if ((spark->flags & SPARK_F_ALT_SPRITE) != 0U) {\n            const OBJECT *const explosion = Object_Get(O_EXPLOSION_1);\n            if (explosion->loaded) {\n                const int32_t base = explosion->mesh_idx;\n                if (spark->color.r < 16 && spark->color.g < 16\n                    && spark->color.b < 16) {\n                    spark->sprite_idx = base + 3;\n                } else if (\n                    spark->color.r < 64 && spark->color.g < 64\n                    && spark->color.b < 64) {\n                    spark->sprite_idx = base + 2;\n                } else if (\n                    spark->color.r < 96 && spark->color.g < 96\n                    && spark->color.b < 96) {\n                    spark->sprite_idx = base + 1;\n                } else {\n                    spark->sprite_idx = base;\n                }\n            }\n        }\n\n        if (lived == (int32_t)(spark->extras >> 3)\n            && (spark->extras & 7U) != 0U) {\n            int32_t uw = 0;\n            if ((spark->flags & SPARK_F_UNDERWATER) != 0U) {\n                uw = 1;\n            } else if ((spark->flags & SPARK_F_GREEN) != 0U) {\n                uw = 2;\n            }\n\n            const XYZ_32 spark_pos = Sparks_GetWorldPos(spark);\n\n            for (int32_t j = 0; j < (int32_t)(spark->extras & 7U); j++) {\n                Sparks_TriggerExplosionSparks(\n                    spark_pos, (int32_t)(spark->extras & 7U) - 1,\n                    spark->dynamic, uw, spark->room_num);\n                spark->dynamic = -1;\n            }\n\n            if ((spark->flags & SPARK_F_UNDERWATER) != 0U) {\n                Sparks_TriggerExplosionBubble(spark_pos, spark->room_num);\n            }\n\n            spark->extras = 0;\n        }\n\n        // Physics\n        spark->vel.y += spark->gravity;\n        if (spark->max_y_vel != 0) {\n            const int32_t limit = (int32_t)spark->max_y_vel * (1 << 5);\n            if ((spark->vel.y < 0 && spark->vel.y < limit)\n                || (spark->vel.y > 0 && spark->vel.y > limit)) {\n                spark->vel.y = limit;\n            }\n        }\n\n        if ((spark->friction & 0x0FU) != 0U) {\n            spark->vel.x -= spark->vel.x >> (spark->friction & 0x0FU);\n            spark->vel.z -= spark->vel.z >> (spark->friction & 0x0FU);\n        }\n\n        if ((spark->friction & 0xF0U) != 0U) {\n            spark->vel.y -= spark->vel.y >> (spark->friction >> 4);\n        }\n\n        spark->pos.x += spark->vel.x >> 5;\n        spark->pos.y += spark->vel.y >> 5;\n        spark->pos.z += spark->vel.z >> 5;\n\n        if ((spark->flags & SPARK_F_OUTSIDE) != 0U) {\n            spark->pos.x += m_SmokeWind.x >> 1;\n            spark->pos.z += m_SmokeWind.z >> 1;\n        }\n\n        // Size lerp across lifetime.\n        if (spark->s_life != 0U) {\n            const float fade = lived / (float)spark->s_life;\n            spark->size.width = LERP(\n                (int32_t)spark->src_size.width, (int32_t)spark->dst_size.width,\n                fade);\n            spark->size.height = LERP(\n                (int32_t)spark->src_size.height,\n                (int32_t)spark->dst_size.height, fade);\n        } else {\n            spark->size = spark->src_size;\n        }\n\n        // If attached to a node, detach after a short random delay for some\n        // node types.\n        if ((spark->flags & (SPARK_F_ITEM | SPARK_F_ATTACHED_NODE))\n                == (SPARK_F_ITEM | SPARK_F_ATTACHED_NODE)\n            && (spark->node_num == 2 || spark->node_num == 3)) {\n            const int32_t b = spark->node_num == 3 ? (Random_GetDraw() & 3) + 12\n                                                   : (Random_GetDraw() & 3) + 8;\n            if (lived > b) {\n                spark->pos = Sparks_GetWorldPos(spark);\n                spark->flags &= ~(SPARK_F_ATTACHED_NODE | SPARK_F_ITEM);\n                Sparks_Sync(spark);\n            }\n        }\n    }\n\n    // Dynamic light pass.\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        const SPARK *const spark = &m_Sparks[i];\n        if (!spark->on || spark->dynamic == -1) {\n            continue;\n        }\n\n        M_SPARK_DYNAMIC *const dl = &m_Dynamics[(uint8_t)spark->dynamic];\n        if (!dl->on) {\n            continue;\n        }\n\n        const XYZ_32 world_pos = Sparks_GetWorldPos(spark);\n        const int32_t rnd = Random_GetControl();\n        XYZ_32 pos = {\n            .x = world_pos.x + ((rnd & 0xF) << 4),\n            .y = world_pos.y + (rnd & 0xF0),\n            .z = world_pos.z + ((rnd >> 4) & 0xF0),\n        };\n\n        int32_t falloff = (int32_t)spark->s_life - (int32_t)spark->life - 1;\n        int32_t r = 0;\n        int32_t g = 0;\n        int32_t b = 0;\n\n        if (falloff < 2) {\n            if (dl->falloff < 28) {\n                dl->falloff = (uint8_t)MIN((int32_t)dl->falloff + 6, 255);\n            }\n            r = 255 - (rnd & 0x1F) - (falloff << 3);\n            g = 255 - (rnd & 0x1F) - (falloff << 4);\n            b = 255 - (rnd & 0x1F) - (falloff << 6);\n        } else if (falloff < 4) {\n            if (dl->falloff < 28) {\n                dl->falloff = (uint8_t)MIN((int32_t)dl->falloff + 6, 255);\n            }\n            r = 255 - (rnd & 0x1F) - (falloff << 3);\n            g = 128 - (falloff << 3);\n            b = (4 - falloff) << 2;\n            if (b < 0) {\n                b = 0;\n            } else {\n                b <<= 3;\n            }\n        } else {\n            if (dl->falloff != 0U) {\n                dl->falloff--;\n            }\n            r = (rnd & 0x1F) + 224;\n            g = ((rnd >> 4) & 0x1F) + 128;\n            b = (rnd >> 8) & 0x3F;\n        }\n\n        falloff = (int32_t)dl->falloff;\n        if (falloff > 31) {\n            falloff = 31;\n        }\n\n        if ((spark->flags & SPARK_F_GREEN) != 0U) {\n            Output_AddDynamicLightRGB(pos, falloff, (RGB_888) { b, r, g });\n        } else {\n            Output_AddDynamicLightRGB(pos, falloff, (RGB_888) { r, g, b });\n        }\n    }\n}\n\nvoid Sparks_Draw(void)\n{\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        SPARK *const spark = &m_Sparks[i];\n        if (!spark->on) {\n            continue;\n        }\n\n        OutputSource_PolyFX_StageSpark(spark);\n    }\n}\n\nvoid Sparks_DetachEffect(const int16_t effect_num)\n{\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        SPARK *const spark = &m_Sparks[i];\n        if (!spark->on) {\n            continue;\n        }\n\n        if ((spark->flags & SPARK_F_FX) != 0U\n            && spark->effect_num == effect_num) {\n            if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) {\n                spark->on = false;\n                continue;\n            }\n\n            const EFFECT *const effect = Effect_Get(effect_num);\n            spark->pos.x += effect->pos.x;\n            spark->pos.y += effect->pos.y;\n            spark->pos.z += effect->pos.z;\n            spark->flags &= ~SPARK_F_FX;\n        }\n    }\n}\n\nvoid Sparks_DetachItem(const int16_t item_num)\n{\n    for (int32_t i = 0; i < M_MAX_SPARKS; i++) {\n        SPARK *const spark = &m_Sparks[i];\n        if (!spark->on) {\n            continue;\n        }\n\n        if ((spark->flags & SPARK_F_ITEM) != 0U\n            && spark->item_num == item_num) {\n            if ((spark->flags & SPARK_F_ATTACHED_POS) != 0U) {\n                spark->on = false;\n                continue;\n            }\n\n            const ITEM *const item = Item_Get(item_num);\n            spark->pos.x += item->pos.x;\n            spark->pos.y += item->pos.y;\n            spark->pos.z += item->pos.z;\n            spark->flags &= ~SPARK_F_ITEM;\n            spark->flags &= ~SPARK_F_ATTACHED_NODE;\n        }\n    }\n}\n"
  },
  {
    "path": "src/trx/game/sparks/manager.h",
    "content": "#pragma once\n\n#include <trx/game/sparks/types.h>\n\nvoid Sparks_Reset(void);\nvoid Sparks_Control(void);\nvoid Sparks_Draw(void);\n\nvoid Sparks_DetachEffect(int16_t effect_num);\nvoid Sparks_DetachItem(int16_t item_num);\n\nXYZ_32 Sparks_GetWorldPos(const SPARK *spark);\n\nSPARK *Sparks_GetFreeSpark(void);\nSPARK *Sparks_GetSpark(int32_t idx);\nvoid Sparks_Sync(SPARK *spark);\nvoid Sparks_FinishSetup(SPARK *spark);\n\nint8_t Sparks_AllocDynamic(uint8_t flags);\nvoid Sparks_FreeDynamic(int8_t idx);\n\nXZ_32 Sparks_GetSmokeWind(void);\nvoid Sparks_SetSmokeWind(XZ_32 wind);\nint32_t Sparks_GetHairWindZ(void);\n"
  },
  {
    "path": "src/trx/game/sparks/spawners.c",
    "content": "#include <trx/game/sparks/spawners.h>\n\n#include <trx/config.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/lara.h>\n#include <trx/game/output/sources/poly_fx.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/spawn.h>\n\nstatic bool M_GetBloodSparkColors(RGB_888 *const src, RGB_888 *const dst)\n{\n    switch (g_Config.visuals.blood_effects) {\n    case BLOOD_EFFECTS_DISABLED:\n        return false;\n    case BLOOD_EFFECTS_PINK:\n        *src = (RGB_888) { 112, 0, 224 };\n        *dst = (RGB_888) { 96, 0, 192 };\n        return true;\n    case BLOOD_EFFECTS_RED:\n        *src = (RGB_888) { 224, 0, 32 };\n        *dst = (RGB_888) { 192, 0, 24 };\n        return true;\n    case BLOOD_EFFECTS_NUMBER_OF:\n        break;\n    }\n    return false;\n}\n\nvoid Sparks_TriggerBubble(\n    const int32_t x, const int32_t y, const int32_t z, const int32_t size,\n    const int32_t size_range, const int16_t effect_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - x;\n    const int32_t dz = lara_item->pos.z - z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    const OBJECT *const bubble_obj = Object_Get(O_BUBBLE_1);\n    if (!bubble_obj->loaded) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    *spark = (SPARK) {\n        .on = true,\n        .src_color = { 0, 0, 0 },\n        .dst_color = { 144, 144, 144 },\n        .fade_to_black = 2,\n        .draw_type = DRAW_BLEND_ADD,\n        .col_fade_speed = 4,\n        .life = 128,\n        .s_life = 128,\n        .flags =\n            SPARK_F_ATTACHED_POS | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE,\n        .effect_num = effect_num,\n        .sprite_idx = bubble_obj->mesh_idx,\n        .pos = { .x = 0, .y = 0, .z = 0 },\n        .vel = { .x = 0, .y = 0, .z = 0 },\n        .gravity = 0,\n        .max_y_vel = 0,\n        .friction = 0,\n        .scalar = 0,\n        .dynamic = -1,\n    };\n\n    const int32_t safe_range = size_range > 0 ? size_range : 1;\n    int32_t full_size = (Random_GetControl() % safe_range) + size;\n    CLAMP(full_size, 0, 255);\n\n    const uint8_t base = (uint8_t)full_size;\n    const uint8_t dst = (uint8_t)(base << 3);\n    spark->src_size.width = base;\n    spark->src_size.height = base;\n    spark->dst_size.width = dst;\n    spark->dst_size.height = dst;\n    spark->size = spark->src_size;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerWaterfallMist(\n    const int32_t x, const int32_t y, const int32_t z, const int32_t angle)\n{\n    const OBJECT *const explosion = Object_Get(O_EXPLOSION_1);\n    if (explosion == nullptr || !explosion->loaded) {\n        return;\n    }\n\n    static const int32_t offsets[] = { 576, 203, -203, -576 };\n\n    for (int32_t i = 0; i < (int32_t)ARRAY_SIZE(offsets); i++) {\n        SPARK *const spark = Sparks_GetFreeSpark();\n\n        const int32_t offset = (Random_GetControl() & 0x1F) + offsets[i] - 16;\n        const int32_t c = Math_Cos(angle) >> W2V_SHIFT;\n        const int32_t s = Math_Sin(angle) >> W2V_SHIFT;\n\n        *spark = (SPARK) {\n            .on = true,\n            .src_color = { 128, 128, 128 },\n            .dst_color = { 192, 192, 192 },\n            .col_fade_speed = 2,\n            .fade_to_black = 4,\n            .draw_type = DRAW_BLEND_ADD,\n            .extras = 0,\n            .life = (uint8_t)((Random_GetControl() & 3) + 6),\n            .dynamic = -1,\n            .sprite_idx = explosion->mesh_idx,\n            .pos = {\n                .x = x + (Random_GetControl() % 16) - 8 + c * offset,\n                .y = y + (Random_GetControl() % 16) - 8,\n                .z = z + (Random_GetControl() % 16) - 8 + s * offset,\n            },\n            .vel = {\n                .x = s,\n                .y = 0,\n                .z = c,\n            },\n            .gravity = 0,\n            .max_y_vel = 0,\n            .friction = 3,\n            .flags = SPARK_F_SPRITE | SPARK_F_ALT_SPRITE | SPARK_F_SCALE,\n            .scalar = 6,\n        };\n        spark->s_life = spark->life;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->flags |= SPARK_F_ROTATE;\n            spark->rot_angle = (uint16_t)(Random_GetControl() & 0xFFF);\n            if ((Random_GetControl() & 1) != 0) {\n                spark->rot_add = -16 - (Random_GetControl() % 16);\n            } else {\n                spark->rot_add = 16 + (Random_GetControl() % 16);\n            }\n        }\n\n        const uint8_t dst_size = (uint8_t)((Random_GetControl() & 7) + 12);\n        const uint8_t src_size = (uint8_t)(dst_size >> 1);\n        spark->src_size.width = src_size;\n        spark->src_size.height = src_size;\n        spark->dst_size.width = dst_size;\n        spark->dst_size.height = dst_size;\n        spark->size = spark->src_size;\n        Sparks_FinishSetup(spark);\n    }\n}\n\nvoid Sparks_TriggerBreath(\n    const XYZ_32 pos, const XYZ_32 vel, const int16_t room_num)\n{\n    const OBJECT *const object = Object_Get(O_EXPLOSION_1);\n    if (object == nullptr || !object->loaded) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    const int32_t jitter_x = (Random_GetControl() & 0xF) - 8;\n    const int32_t jitter_y = (Random_GetControl() & 0xF) - 8;\n    const int32_t jitter_z = (Random_GetControl() & 0xF) - 8;\n\n    *spark = (SPARK) {\n        .on = true,\n        .src_color = { 0, 0, 0 },\n        .dst_color = { 32, 32, 32 },\n        .col_fade_speed = 4,\n        .fade_to_black = 32,\n        .draw_type = DRAW_BLEND_ADD,\n        .extras = 0,\n        .life = (uint8_t)((Random_GetControl() & 3) + 37),\n        .dynamic = -1,\n        .sprite_idx = object->mesh_idx,\n        .pos = {\n            .x = pos.x + jitter_x,\n            .y = pos.y + jitter_y,\n            .z = pos.z + jitter_z,\n        },\n        .vel = vel,\n        .gravity = 0,\n        .max_y_vel = 0,\n        .friction = 0,\n        .flags = SPARK_F_SPRITE | SPARK_F_ALT_SPRITE | SPARK_F_SCALE,\n        .scalar = 3,\n        .room_num = room_num,\n    };\n    spark->s_life = spark->life;\n\n    const ROOM *const room = Room_Get(room_num);\n    if (room != nullptr && room->flags.wind) {\n        spark->flags |= SPARK_F_OUTSIDE;\n    }\n\n    const uint8_t dst_size = ((Random_GetControl() & 7) + 32);\n    const uint8_t src_size = (dst_size >> 3);\n    spark->src_size.width = src_size;\n    spark->src_size.height = src_size;\n    spark->dst_size.width = dst_size;\n    spark->dst_size.height = dst_size;\n    spark->size = spark->src_size;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerFireFlame(\n    const XYZ_32 pos, const int32_t body_part, const int32_t type)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 20 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    if (type == 2) {\n        spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n        spark->src_color.g = spark->src_color.r;\n        spark->src_color.b = (Random_GetControl() & 0x3F) - 64;\n    } else if (type == 254) {\n        spark->src_color.r = 48;\n        spark->src_color.g = 255;\n        spark->src_color.b = (Random_GetControl() & 0x1F) + 48;\n        spark->dst_color.r = 32;\n        spark->dst_color.g = (Random_GetControl() & 0x3F) - 64;\n        spark->dst_color.b = (Random_GetControl() & 0x3F) + 128;\n    } else {\n        spark->src_color.r = 255;\n        spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n        spark->src_color.b = 48;\n    }\n\n    if (type != 254) {\n        spark->dst_color.r = (Random_GetControl() & 0x3F) - 64;\n        spark->dst_color.b = 32;\n        spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    }\n\n    if (body_part == -1) {\n        if (type == 2 || type == 255 || type == 254) {\n            spark->fade_to_black = 6;\n            spark->col_fade_speed = (Random_GetControl() & 3) + 5;\n            spark->life = (type < 254 ? 0 : 8) + (Random_GetControl() & 3) + 16;\n            spark->s_life = spark->life;\n        } else {\n            spark->fade_to_black = 8;\n            spark->col_fade_speed = (Random_GetControl() & 3) + 20;\n            spark->life = (Random_GetControl() & 7) + 40;\n            spark->s_life = spark->life;\n        }\n    } else {\n        spark->fade_to_black = 16;\n        spark->col_fade_speed = (Random_GetControl() & 3) + 8;\n        spark->life = (Random_GetControl() & 3) + 28;\n        spark->s_life = spark->life;\n    }\n\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n\n    if (body_part != -1) {\n        spark->pos.x = (Random_GetControl() & 0x1F) - 16;\n        spark->pos.y = 0;\n        spark->pos.z = (Random_GetControl() & 0x1F) - 16;\n    } else {\n        spark->pos = pos;\n        if (type == 0 || type == 1) {\n            spark->pos.x += (Random_GetControl() & 0x1F) - 16;\n            spark->pos.z += (Random_GetControl() & 0x1F) - 16;\n        } else if (type >= 254) {\n            spark->pos.x += (Random_GetControl() & 0x3F) - 32;\n            spark->pos.z += (Random_GetControl() & 0x3F) - 32;\n        } else {\n            spark->pos.x += (Random_GetControl() & 0xF) - 8;\n            spark->pos.z += (Random_GetControl() & 0xF) - 8;\n        }\n    }\n\n    if (type == 2) {\n        spark->vel.x = (Random_GetControl() & 0x1F) - 16;\n        spark->vel.y = -1024 - (Random_GetControl() & 0x1FF);\n        spark->vel.z = (Random_GetControl() & 0x1F) - 16;\n        spark->friction = 68;\n    } else {\n        spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n        spark->vel.y = -16 - (Random_GetControl() & 0xF);\n        spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n\n        if (type == 1) {\n            spark->friction = 51;\n        } else {\n            spark->friction = 5;\n        }\n    }\n\n    if (Random_GetControl() & 1) {\n        if (body_part == -1) {\n            spark->gravity = -16 - (Random_GetControl() & 0x1F);\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n                | SPARK_F_SCALE;\n            spark->max_y_vel = -16 - (Random_GetControl() & 7);\n        } else {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_ROTATE\n                | SPARK_F_SPRITE | SPARK_F_SCALE;\n            spark->item_num = body_part;\n            spark->gravity = -32 - (Random_GetControl() & 0x3F);\n            spark->max_y_vel = -24 - (Random_GetControl() & 7);\n        }\n\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else if (body_part == -1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->gravity = -16 - (Random_GetControl() & 0x1F);\n        spark->max_y_vel = -16 - (Random_GetControl() & 7);\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_FX | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->item_num = body_part;\n        spark->gravity = -32 - (Random_GetControl() & 0x3F);\n        spark->max_y_vel = -24 - (Random_GetControl() & 7);\n    }\n\n    spark->scalar = 2;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n\n    uint8_t size;\n    if (type == 0) {\n        size = (Random_GetControl() & 0x1F) + 128;\n    } else if (type == 1) {\n        size = (Random_GetControl() & 0x1F) + 64;\n    } else if (type < 254) {\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n        size = (Random_GetControl() & 0x1F) + 32;\n    } else {\n        size = (Random_GetControl() & 0xF) + 48;\n    }\n\n    spark->src_size.width = size;\n    spark->src_size.height = size;\n    spark->size.width = size;\n    spark->size.height = size;\n\n    if (type == 2) {\n        spark->dst_size.width = size >> 2;\n        spark->dst_size.height = size >> 2;\n    } else {\n        spark->dst_size.width = size >> 4;\n        spark->dst_size.height = size >> 4;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerFireSmoke(\n    const XYZ_32 pos, const int32_t body_part, const int32_t type)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 20 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 32;\n    spark->dst_color.g = 32;\n    spark->dst_color.b = 32;\n\n    if (body_part == -1) {\n        if (type == 255) {\n            spark->fade_to_black = 8;\n            spark->col_fade_speed = (Random_GetControl() & 3) + 16;\n            spark->life = (Random_GetControl() & 7) + 28;\n            spark->s_life = spark->life;\n        } else {\n            spark->fade_to_black = 16;\n            spark->col_fade_speed = (Random_GetControl() & 7) + 32;\n            spark->life = (Random_GetControl() & 0xF) + 57;\n            spark->s_life = spark->life;\n        }\n    } else {\n        spark->fade_to_black = 12;\n        spark->col_fade_speed = (Random_GetControl() & 3) + 4;\n        spark->life = (Random_GetControl() & 3) + 20;\n        spark->s_life = spark->life;\n    }\n\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y - (Random_GetControl() & 0x7F) - 256;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 4;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->gravity = -16 - (Random_GetControl() & 0xF);\n    spark->max_y_vel = -8 - (Random_GetControl() & 7);\n    spark->dst_size.width = (Random_GetControl() & 0x3F) + 64;\n    spark->src_size.width = spark->dst_size.width >> 2;\n    spark->size.width = spark->src_size.width;\n    spark->src_size.height = spark->src_size.width;\n    spark->size.height = spark->src_size.width;\n    spark->dst_size.height = spark->dst_size.width;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerStaticFlame(const XYZ_32 pos, const int32_t size)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 20 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x3F) - 64;\n    spark->src_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->src_color.b = 64;\n    spark->dst_color.r = spark->src_color.r;\n    spark->dst_color.g = spark->src_color.g;\n    spark->dst_color.b = 64;\n    spark->col_fade_speed = 1;\n    spark->fade_to_black = 0;\n    spark->life = 2;\n    spark->s_life = 2;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 7) - 4;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 7) - 4;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    spark->friction = 0;\n    spark->vel.z = 0;\n    spark->vel.y = 0;\n    spark->vel.x = 0;\n    spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 2;\n    spark->dst_size.width = size;\n    spark->dst_size.height = size;\n    spark->src_size.height = size;\n    spark->src_size.width = size;\n    spark->size.height = size;\n    spark->size.width = size;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerSideFlame(\n    const XYZ_32 pos, const int32_t angle, const int32_t speed,\n    const bool pilot)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 20 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.g = spark->src_color.r;\n    spark->src_color.b = (Random_GetControl() & 0x3F) - 64;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) - 64;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 0x80;\n    spark->dst_color.b = 32;\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 28;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n\n    int32_t dist;\n    if (pilot) {\n        dist = (speed << 7) + (Random_GetControl() & 0x1F);\n    } else {\n        dist = (speed << 8) + (Random_GetControl() & 0x1FF);\n    }\n    dist <<= 1;\n\n    const int32_t s = (dist * Math_Sin(angle)) >> W2V_SHIFT;\n    const int32_t c = (dist * Math_Cos(angle)) >> W2V_SHIFT;\n    spark->vel.x = (int16_t)((Random_GetControl() & 0x7F) + s - 64);\n    spark->vel.y = -6 - (Random_GetControl() & 7);\n    spark->vel.z = (int16_t)((Random_GetControl() & 0x7F) + c - 64);\n    spark->friction = 4;\n    spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->gravity = -8 - (Random_GetControl() & 0xF);\n    spark->max_y_vel = -8 - (Random_GetControl() & 7);\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n\n    int32_t size = (Random_GetControl() & 0x1F) + 128;\n    if (pilot) {\n        size >>= 2;\n    }\n\n    spark->dst_size.width = size;\n    spark->dst_size.height = size;\n    spark->src_size.width = size >> 1;\n    spark->src_size.height = size >> 1;\n    spark->size.width = size >> 1;\n    spark->size.height = size >> 1;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerBlood(\n    const XYZ_32 pos, int32_t angle_12, const int32_t count)\n{\n    RGB_888 src_color;\n    RGB_888 dst_color;\n    if (!M_GetBloodSparkColors(&src_color, &dst_color)) {\n        return;\n    }\n\n    for (int32_t i = 0; i < count; i++) {\n        SPARK *const spark = Sparks_GetFreeSpark();\n        spark->on = true;\n        spark->src_color = src_color;\n        spark->dst_color = dst_color;\n\n        spark->col_fade_speed = 8;\n        spark->fade_to_black = 8;\n        spark->life = 24;\n        spark->s_life = 24;\n        spark->draw_type = DRAW_BLEND;\n        spark->extras = 0;\n        spark->dynamic = -1;\n        spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n        spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n        spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n        const int16_t dist = Random_GetControl() & 0xF;\n        const int32_t ang =\n            ((Random_GetControl() & 0x1F) + angle_12 - 16) & 0xFFF;\n        spark->vel.x = -(dist * Math_Sin(ang << 4)) >> 7;\n        spark->vel.y = -128 - (Random_GetControl() & 0xFF);\n        spark->vel.z = dist * Math_Cos(ang << 4) >> 7;\n        spark->friction = 4;\n        spark->flags = SPARK_F_BLOOD | SPARK_F_SCALE;\n        spark->scalar = 3;\n        spark->max_y_vel = 0;\n        spark->gravity = (Random_GetControl() & 0x1F) + 31;\n        spark->size.width = 2;\n        spark->src_size.width = 2;\n        spark->size.height = 2;\n        spark->src_size.height = 2;\n        spark->dst_size.width = 2 - (Random_GetControl() & 1);\n        spark->dst_size.height = 2 - (Random_GetControl() & 1);\n        Sparks_FinishSetup(spark);\n    }\n}\n\nvoid Sparks_TriggerBloodD(\n    const XYZ_32 pos, int32_t angle_12, const int32_t count)\n{\n    RGB_888 src_color;\n    RGB_888 dst_color;\n    if (!M_GetBloodSparkColors(&src_color, &dst_color)) {\n        return;\n    }\n\n    for (int32_t i = 0; i < count; i++) {\n        SPARK *const spark = Sparks_GetFreeSpark();\n        spark->on = true;\n        spark->src_color = src_color;\n        spark->dst_color = dst_color;\n\n        spark->col_fade_speed = 8;\n        spark->fade_to_black = 8;\n        spark->life = 24;\n        spark->s_life = 24;\n        spark->draw_type = DRAW_BLEND;\n        spark->extras = 0;\n        spark->dynamic = -1;\n        spark->pos.x = pos.x + (Random_GetDraw() & 0x1F) - 16;\n        spark->pos.y = pos.y + (Random_GetDraw() & 0x1F) - 16;\n        spark->pos.z = pos.z + (Random_GetDraw() & 0x1F) - 16;\n        const int16_t dist = Random_GetDraw() & 0xF;\n        const int32_t ang = ((Random_GetDraw() & 0x1F) + angle_12 - 16) & 0xFFF;\n        spark->vel.x = -(dist * Math_Sin(ang << 4)) >> 7;\n        spark->vel.y = -128 - (Random_GetDraw() & 0xFF);\n        spark->vel.z = dist * Math_Cos(ang << 4) >> 7;\n        spark->friction = 4;\n        spark->flags = SPARK_F_SCALE;\n        spark->scalar = 3;\n        spark->max_y_vel = 0;\n        spark->gravity = (Random_GetDraw() & 0x1F) + 31;\n        spark->size.width = 2;\n        spark->src_size.width = 2;\n        spark->size.height = 2;\n        spark->src_size.height = 2;\n        spark->dst_size.width = 2 - (Random_GetDraw() & 1);\n        spark->dst_size.height = 2 - (Random_GetDraw() & 1);\n        Sparks_FinishSetup(spark);\n    }\n}\n\nvoid Sparks_TriggerUnderwaterExplosion(const ITEM *item)\n{\n    if (item == nullptr) {\n        return;\n    }\n\n    Sparks_TriggerExplosionBubble(item->pos, item->room_num);\n    Sparks_TriggerExplosionSparks(item->pos, 2, -2, 1, item->room_num);\n\n    for (int32_t i = 0; i < 3; i++) {\n        Sparks_TriggerExplosionSparks(item->pos, 2, -1, 1, item->room_num);\n    }\n\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    if (water_height == NO_HEIGHT) {\n        return;\n    }\n\n    int32_t y = item->pos.y - water_height;\n    if (y >= 2048) {\n        return;\n    }\n\n    const int32_t wh = 2048 - y;\n    y = wh >> 6;\n\n    const ROOM *const room = Room_Get(item->room_num);\n    FX_Water_SetupSplash(&(FX_WATER_SPLASH_SETUP) {\n        .x = item->pos.x,\n        .y = room->max_ceiling,\n        .z = item->pos.z,\n        .inner_y_size = -96,\n        .inner_xz_vel = 160,\n        .inner_gravity = 96,\n        .inner_xz_off = y + 16,\n        .inner_xz_size = y + 12,\n        .inner_friction = 7,\n        .inner_y_vel = (-512 - wh) << 3,\n        .middle_xz_off = y + 24,\n        .middle_xz_size = y + 24,\n        .middle_y_size = -64,\n        .middle_xz_vel = 224,\n        .middle_y_vel = (-768 - wh) << 2,\n        .middle_gravity = 56,\n        .middle_friction = 8,\n        .outer_xz_off = y + 32,\n        .outer_xz_size = y + 32,\n        .outer_xz_vel = 272,\n        .outer_friction = 9,\n    });\n}\n\nvoid Sparks_TriggerExplosionSparks(\n    XYZ_32 pos, int32_t extras, int32_t dynamic, int32_t uw, int16_t room_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 30 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    const OBJECT *const explosion = Object_Get(O_EXPLOSION_1);\n    if (explosion == nullptr || !explosion->loaded) {\n        return;\n    }\n\n    int32_t safe_extras = extras;\n    CLAMP(safe_extras, 0, 3);\n    static const uint8_t extras_table[4] = { 0, 4, 7, 10 };\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    *spark = (SPARK) {\n        .on = true,\n        .src_color = { 255, 0, 0 },\n        .dst_color = { 0, 0, 0 },\n        .draw_type = DRAW_BLEND_ADD,\n        .extras = (uint8_t)(\n            safe_extras\n            | ((extras_table[safe_extras] + (Random_GetControl() & 7) - 4)\n               << 3)),\n        .life = 0,\n        .dynamic = (int8_t)dynamic,\n        .sprite_idx = explosion->mesh_idx,\n        .pos = pos,\n        .vel = {\n            .x = (Random_GetControl() & 0xFFF) - 2048,\n            .y = (Random_GetControl() & 0xFFF) - 2048,\n            .z = (Random_GetControl() & 0xFFF) - 2048,\n        },\n        .gravity = 0,\n        .max_y_vel = 0,\n        .friction = 0,\n        .flags = SPARK_F_SPRITE | SPARK_F_SCALE,\n        .scalar = 3,\n        .room_num = (uint8_t)room_num,\n    };\n\n    if (uw == 1) {\n        spark->src_color.g = (uint8_t)((Random_GetControl() & 0x3F) + 128);\n        spark->src_color.b = 32;\n        spark->dst_color.r = 192;\n        spark->dst_color.g = (uint8_t)((Random_GetControl() & 0x1F) + 64);\n        spark->dst_color.b = 0;\n        spark->col_fade_speed = 7;\n        spark->fade_to_black = 8;\n        spark->life = (uint8_t)((Random_GetControl() & 7) + 16);\n        spark->flags |= SPARK_F_UNDERWATER;\n    } else {\n        spark->src_color.g = (uint8_t)((Random_GetControl() & 0xF) + 32);\n        spark->src_color.b = 0;\n        spark->dst_color.r = (uint8_t)((Random_GetControl() & 0x3F) + 192);\n        spark->dst_color.g = (uint8_t)((Random_GetControl() & 0x3F) + 128);\n        spark->dst_color.b = 32;\n        spark->col_fade_speed = 8;\n        spark->fade_to_black = 16;\n        spark->life = (uint8_t)((Random_GetControl() & 7) + 24);\n    }\n    spark->s_life = spark->life;\n\n    if (dynamic == -2) {\n        spark->dynamic = Sparks_AllocDynamic(uw == 1 ? 2 : 1);\n    }\n\n    if (dynamic != -2 || uw == 1) {\n        spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n        spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n        spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    } else {\n        spark->pos.x = pos.x + (Random_GetControl() & 0x1FF) - 256;\n        spark->pos.y = pos.y + (Random_GetControl() & 0x1FF) - 256;\n        spark->pos.z = pos.z + (Random_GetControl() & 0x1FF) - 256;\n    }\n\n    spark->friction = (uint8_t)(uw == 1 ? 0x11 : 0x33);\n\n    spark->flags |= SPARK_F_ALT_SPRITE;\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags |= SPARK_F_ROTATE;\n        spark->rot_angle = (uint16_t)(Random_GetControl() & 0xFFF);\n        const int32_t rot_add = (Random_GetControl() & 0x7F) + 32;\n        spark->rot_add = (int8_t)MIN(rot_add, 127);\n    }\n\n    spark->src_size.width = (uint8_t)((Random_GetControl() & 0xF) + 40);\n    spark->src_size.height =\n        (uint8_t)(spark->src_size.width + (Random_GetControl() & 7) + 8);\n    spark->dst_size.width = (uint8_t)(spark->src_size.width << 1);\n    spark->dst_size.height = (uint8_t)(spark->src_size.height << 1);\n    spark->size = spark->src_size;\n\n    if (uw == 2) {\n        const RGB_888 src = spark->src_color;\n        const RGB_888 dst = spark->dst_color;\n        spark->src_color = (RGB_888) { src.b, src.r, src.g };\n        spark->dst_color = (RGB_888) { dst.b, dst.r, dst.g };\n        spark->color = spark->src_color;\n        spark->flags |= SPARK_F_GREEN;\n    } else if (extras != 0) {\n        Sparks_TriggerExplosionSmoke(pos, uw, room_num);\n    } else {\n        Sparks_TriggerExplosionSmokeEnd(pos, uw, room_num);\n    }\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerExplosionBubble(const XYZ_32 pos, const int16_t room_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 30 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    const OBJECT *const explosion = Object_Get(O_EXPLOSION_1);\n    if (explosion == nullptr || !explosion->loaded) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    *spark = (SPARK) {\n        .on = true,\n        .src_color = { 128, 64, 0 },\n        .dst_color = { 128, 128, 128 },\n        .col_fade_speed = 8,\n        .fade_to_black = 12,\n        .life = 24,\n        .s_life = 24,\n        .draw_type = DRAW_BLEND_ADD,\n        .extras = 0,\n        .dynamic = -1,\n        .sprite_idx = explosion->mesh_idx,\n        .pos = pos,\n        .vel = { .x = 0, .y = 0, .z = 0 },\n        .gravity = 0,\n        .max_y_vel = 0,\n        .friction = 0,\n        .flags = SPARK_F_UNDERWATER | SPARK_F_SPRITE | SPARK_F_SCALE,\n        .scalar = 3,\n        .room_num = (uint8_t)room_num,\n    };\n\n    const uint8_t size = (uint8_t)((Random_GetControl() & 7) + 63);\n    spark->src_size.width = (uint8_t)(size >> 1);\n    spark->src_size.height = spark->src_size.width;\n    spark->dst_size.width = (uint8_t)(size << 1);\n    spark->dst_size.height = spark->dst_size.width;\n    spark->size = spark->src_size;\n    Sparks_FinishSetup(spark);\n\n    for (int32_t i = 0; i < 7; i++) {\n        const XYZ_32 bubble_pos = {\n            .x = pos.x + (Random_GetControl() & 0x1FF) - 256,\n            .y = pos.y + (Random_GetControl() & 0x7F) - 64,\n            .z = pos.z + (Random_GetControl() & 0x1FF) - 256,\n        };\n        Spawn_BubbleEx(&bubble_pos, room_num, 6, 15);\n    }\n}\n\nvoid Sparks_TriggerExplosionSmoke(\n    const XYZ_32 pos, const bool uw, const int16_t room_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 30 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 144;\n    spark->src_color.g = 144;\n    spark->src_color.b = 144;\n    spark->dst_color.r = 64;\n    spark->dst_color.g = 64;\n    spark->dst_color.b = 64;\n    spark->col_fade_speed = 2;\n    spark->fade_to_black = 8;\n    spark->draw_type = DRAW_BLEND_SUB;\n    spark->extras = 0;\n    spark->life = (uint8_t)((Random_GetControl() & 3) + 10);\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1FF) - 256;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1FF) - 256;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1FF) - 256;\n    spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n    spark->vel.y = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n\n    if (uw) {\n        spark->friction = 2;\n    } else {\n        spark->friction = 6;\n    }\n\n    spark->flags =\n        SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->rot_angle = Random_GetControl() & 0xFFF;\n    spark->rot_add = (Random_GetControl() & 0xF) + 16;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    spark->gravity = -3 - (Random_GetControl() & 3);\n    spark->max_y_vel = -4 - (Random_GetControl() & 3);\n    spark->dst_size.width = (Random_GetControl() & 0x1F) + 128;\n    spark->size.width = spark->dst_size.width >> 2;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.height =\n        spark->dst_size.width + (Random_GetControl() & 0x1F) + 32;\n    spark->size.height = spark->dst_size.height >> 3;\n    spark->src_size.height = spark->size.height;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerExplosionSmokeEnd(\n    const XYZ_32 pos, const bool uw, const int16_t room_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 30 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    if (uw) {\n        spark->src_color.r = 0;\n        spark->src_color.g = 0;\n        spark->src_color.b = 0;\n        spark->dst_color.r = 192;\n        spark->dst_color.g = 192;\n        spark->dst_color.b = 208;\n    } else {\n        spark->src_color.r = 144;\n        spark->src_color.g = 144;\n        spark->src_color.b = 144;\n        spark->dst_color.r = 64;\n        spark->dst_color.g = 64;\n        spark->dst_color.b = 64;\n    }\n\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 64;\n    spark->life = (uint8_t)((Random_GetControl() & 0x1F) + 96);\n    spark->s_life = spark->life;\n\n    spark->draw_type = uw ? DRAW_BLEND_ADD : DRAW_BLEND_SUB;\n\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n    spark->vel.y = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n\n    if (uw) {\n        spark->friction = 20;\n        spark->vel.y = (int16_t)(spark->vel.y >> 4);\n        spark->pos.y += 32;\n    } else {\n        spark->friction = 6;\n    }\n\n    spark->flags =\n        SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->rot_angle = (uint16_t)(Random_GetControl() & 0xFFF);\n    if ((Random_GetControl() & 1) != 0) {\n        spark->rot_add = (int8_t)(-16 - (Random_GetControl() & 0xF));\n    } else {\n        spark->rot_add = (int8_t)((Random_GetControl() & 0xF) + 16);\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n\n    if (uw) {\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n    } else {\n        spark->gravity = (int16_t)(-3 - (Random_GetControl() & 3));\n        spark->max_y_vel = (int8_t)(-4 - (Random_GetControl() & 3));\n    }\n\n    spark->dst_size.width = (uint8_t)((Random_GetControl() & 0x1F) + 128);\n    spark->size.width = (uint8_t)(spark->dst_size.width >> 2);\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.height =\n        (uint8_t)(spark->dst_size.width + (Random_GetControl() & 0x1F) + 32);\n    spark->size.height = (uint8_t)(spark->dst_size.height >> 3);\n    spark->src_size.height = spark->size.height;\n    spark->room_num = (uint8_t)room_num;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerDartSmoke(const XYZ_32 pos, const XZ_32 vel, const bool hit)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 16;\n    spark->src_color.g = 8;\n    spark->src_color.b = 4;\n    spark->dst_color.r = 64;\n    spark->dst_color.g = 48;\n    spark->dst_color.b = 32;\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 4;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 32;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n\n    if (hit) {\n        spark->vel.x = (Random_GetControl() & 0xFF) - vel.x - 128;\n        spark->vel.y = -4 - (Random_GetControl() & 3);\n        spark->vel.z = (Random_GetControl() & 0xFF) - vel.z - 128;\n    } else {\n        if (vel.x != 0) {\n            spark->vel.x = -vel.x;\n        } else {\n            spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n        }\n\n        spark->vel.y = -4 - (Random_GetControl() & 3);\n\n        if (vel.z != 0) {\n            spark->vel.z = -vel.z;\n        } else {\n            spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n        }\n    }\n\n    spark->friction = 3;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 1;\n    int32_t rnd = (Random_GetControl() & 0x3F) + 72;\n    if (hit) {\n        rnd >>= 1;\n        spark->dst_size.width = (uint8_t)rnd;\n        spark->size.width = spark->dst_size.width >> 2;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.height = (uint8_t)rnd;\n        spark->size.height = spark->dst_size.height >> 2;\n        spark->src_size.height = spark->size.height;\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n    } else {\n        spark->dst_size.width = (uint8_t)rnd;\n        spark->size.width = spark->dst_size.width >> 4;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.height = (uint8_t)rnd;\n        spark->size.height = spark->dst_size.height >> 4;\n        spark->src_size.height = spark->size.height;\n        spark->gravity = -4 - (Random_GetControl() & 3);\n        spark->max_y_vel = -4 - (Random_GetControl() & 3);\n    }\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerPickupAid(const XYZ_32 pos, const XZ_32 vel)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 48;\n    spark->src_color.g = 40;\n    spark->src_color.b = 36;\n    spark->dst_color.r = (Random_GetDraw() & 0x20) + 96;\n    spark->dst_color.g = spark->dst_color.r;\n    spark->dst_color.b = 96;\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 2;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 7;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos = pos;\n    spark->pos.y -= Random_GetControl() & 16;\n\n    if (vel.x != 0) {\n        spark->vel.x = -vel.x;\n    } else {\n        spark->vel.x = (Random_GetControl() & 0x20) - 16;\n    }\n\n    spark->vel.y = -4 - (Random_GetControl() & 3);\n\n    if (vel.z != 0) {\n        spark->vel.z = -vel.z;\n    } else {\n        spark->vel.z = (Random_GetControl() & 0x20) - 16;\n    }\n\n    spark->friction = 3;\n\n    if ((Random_GetControl() & 1) != 0) {\n        spark->flags = SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if ((Random_GetControl() & 1) != 0) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    const OBJECT *const obj = Object_Get(O_PICKUP_AID);\n    const int32_t mesh_count = ABS(obj->mesh_count) - 1;\n    spark->sprite_idx = obj->mesh_idx + (Random_GetControl() & mesh_count);\n\n    spark->scalar = 1;\n    int32_t rnd = (Random_GetControl() & 0x3F) + 36;\n    spark->dst_size.width = (uint8_t)rnd;\n    spark->size.width = spark->dst_size.width >> 4;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.height = (uint8_t)rnd;\n    spark->size.height = spark->dst_size.height >> 4;\n    spark->src_size.height = spark->size.height;\n    spark->gravity = -4 - (Random_GetControl() & 3);\n    spark->max_y_vel = -4 - (Random_GetControl() & 3);\n\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerFlareSparks(\n    const XYZ_32 pos, const XYZ_32 vel, const bool smoke)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = 255;\n    spark->src_color.b = 255;\n    spark->dst_color.r = 255;\n    spark->dst_color.g = (Random_GetDraw() & 0x7F) + 64;\n    spark->dst_color.b = 192 - spark->dst_color.g;\n    spark->col_fade_speed = 3;\n    spark->fade_to_black = 5;\n    spark->life = 10;\n    spark->s_life = 10;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetDraw() & 7) - 3;\n    spark->pos.y = pos.y + (Random_GetDraw() & 7) - 3;\n    spark->pos.z = pos.z + (Random_GetDraw() & 7) - 3;\n    spark->vel.x = (int16_t)(vel.x + (Random_GetDraw() & 0xFF) - 128);\n    spark->vel.y = (int16_t)(vel.y + (Random_GetDraw() & 0xFF) - 128);\n    spark->vel.z = (int16_t)(vel.z + (Random_GetDraw() & 0xFF) - 128);\n    spark->friction = 34;\n    spark->scalar = 1;\n    spark->size.width = (Random_GetDraw() & 3) + 4;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = (Random_GetDraw() & 1) + 1;\n    spark->size.height = (Random_GetDraw() & 3) + 4;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = (Random_GetDraw() & 1) + 1;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    spark->flags = SPARK_F_SCALE;\n    Sparks_FinishSetup(spark);\n\n    if (!smoke) {\n        return;\n    }\n\n    SPARK *const smoke_spark = Sparks_GetFreeSpark();\n    smoke_spark->on = true;\n    smoke_spark->src_color.r = spark->dst_color.r >> 1;\n    smoke_spark->src_color.g = spark->dst_color.g >> 1;\n    smoke_spark->src_color.b = spark->dst_color.b >> 1;\n    smoke_spark->dst_color.r = 32;\n    smoke_spark->dst_color.g = 32;\n    smoke_spark->dst_color.b = 32;\n    smoke_spark->col_fade_speed = (Random_GetDraw() & 3) + 8;\n    smoke_spark->fade_to_black = 4;\n    smoke_spark->draw_type = DRAW_BLEND_ADD;\n    smoke_spark->life = (Random_GetDraw() & 7) + 13;\n    smoke_spark->s_life = smoke_spark->life;\n    smoke_spark->pos.x = pos.x + (vel.x >> 5);\n    smoke_spark->pos.y = pos.y + (vel.y >> 5);\n    smoke_spark->pos.z = pos.z + (vel.z >> 5);\n    smoke_spark->extras = 0;\n    smoke_spark->dynamic = -1;\n    smoke_spark->vel.x = (int16_t)(vel.x + (Random_GetDraw() & 0x3F) - 32);\n    smoke_spark->vel.y = (int16_t)vel.y;\n    smoke_spark->vel.z = (int16_t)(vel.z + (Random_GetDraw() & 0x3F) - 32);\n    smoke_spark->friction = 4;\n\n    if (Random_GetDraw() & 1) {\n        smoke_spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        smoke_spark->rot_angle = Random_GetDraw() & 0xFFF;\n\n        if (Random_GetDraw() & 1) {\n            smoke_spark->rot_add = -16 - (Random_GetDraw() & 0xF);\n        } else {\n            smoke_spark->rot_add = (Random_GetDraw() & 0xF) + 16;\n        }\n    } else {\n        smoke_spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    smoke_spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    smoke_spark->scalar = 2;\n    smoke_spark->gravity = -8 - (Random_GetDraw() & 3);\n    smoke_spark->max_y_vel = -4 - (Random_GetDraw() & 3);\n    smoke_spark->dst_size.width = (Random_GetDraw() & 0xF) + 24;\n    smoke_spark->src_size.width = smoke_spark->dst_size.width >> 3;\n    smoke_spark->size.width = smoke_spark->dst_size.width >> 3;\n    smoke_spark->dst_size.height = smoke_spark->dst_size.width;\n    smoke_spark->src_size.height = smoke_spark->dst_size.height >> 3;\n    smoke_spark->size.height = smoke_spark->dst_size.height >> 3;\n    Sparks_FinishSetup(smoke_spark);\n}\n\nvoid Sparks_TriggerRicochet(\n    const GAME_VECTOR pos, const int32_t angle, const int32_t size)\n{\n    SPARK *spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 32;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 96;\n    spark->dst_color.b = 0;\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 8;\n    spark->life = 24;\n    spark->s_life = 24;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z;\n    int32_t ang = ((Random_GetControl() & 0x7FF) + angle - 1024) & 0xFFF;\n    spark->vel.x = -Math_Sin(ang << 4) >> 3;\n    spark->vel.y = 2 * (Random_GetControl() & 0x1FF) - 768;\n    spark->vel.z = Math_Cos(ang << 4) >> 3;\n    spark->friction = 1;\n    spark->flags = SPARK_F_SCALE;\n    spark->scalar = 3;\n    spark->gravity =\n        (int16_t)(ABS(spark->vel.y >> 6) + (Random_GetControl() & 0x1F));\n    spark->size.width = (Random_GetControl() & 3) + 4;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = (Random_GetControl() & 1) + 1;\n    spark->size.height = (Random_GetControl() & 3) + 4;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = (Random_GetControl() & 1) + 1;\n    spark->max_y_vel = 0;\n    Sparks_FinishSetup(spark);\n\n    spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    uint8_t c = (uint8_t)((Random_GetControl() & 0x3F) + 128);\n    spark->src_color.r = c;\n    spark->src_color.g = c;\n    spark->src_color.b = c;\n    c >>= 1;\n    spark->dst_color.r = c;\n    spark->dst_color.g = c;\n    spark->dst_color.b = c;\n    spark->draw_type = DRAW_BLEND_SUB;\n    spark->extras = 0;\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 16;\n    spark->life = 28;\n    spark->s_life = 28;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z;\n    ang = ((Random_GetControl() & 0x7FF) + angle - 1023) & 0xFFF;\n    spark->vel.x = -Math_Sin(ang << 4) >> 3;\n    spark->vel.y = (Random_GetControl() & 0x1FF) - 384;\n    spark->vel.z = Math_Cos(ang << 4) >> 3;\n    spark->friction = 33;\n    spark->flags = SPARK_F_SCALE;\n    spark->scalar = 3;\n    spark->gravity = (Random_GetControl() & 7) + 4;\n    spark->size.width = (Random_GetControl() & 3) + 4;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = (Random_GetControl() & 1) + 1;\n    spark->size.height = (Random_GetControl() & 3) + 4;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = (Random_GetControl() & 1) + 1;\n    spark->max_y_vel = 0;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerGunSmoke(\n    const GAME_VECTOR pos, const bool initial, const LARA_GUN_TYPE weapon,\n    const int32_t shade)\n{\n    Sparks_TriggerGunSmokeDirected(pos, (XYZ_32) {}, initial, weapon, shade);\n}\n\nvoid Sparks_TriggerGunSmokeDirected(\n    const GAME_VECTOR pos, const XYZ_32 vel, const bool initial,\n    const LARA_GUN_TYPE weapon, const int32_t shade)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n    spark->dst_color.r = shade << 2;\n    spark->dst_color.g = shade << 2;\n    spark->dst_color.b = shade << 2;\n    spark->col_fade_speed = 4;\n    spark->fade_to_black = 32 - (initial << 4);\n    spark->life = (Random_GetControl() & 3) + 40;\n    spark->s_life = spark->life;\n\n    if ((weapon == LGT_PISTOLS || weapon == LGT_MAGNUMS || weapon == LGT_UZIS)\n        && spark->dst_color.r > 64) {\n        spark->dst_color.r = 64;\n        spark->dst_color.g = 64;\n        spark->dst_color.b = 64;\n    }\n\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n\n    if (initial) {\n        spark->vel.x = vel.x + (Random_GetControl() & 0x3FF) - 512;\n        spark->vel.y = vel.y + (Random_GetControl() & 0x3FF) - 512;\n        spark->vel.z = vel.z + (Random_GetControl() & 0x3FF) - 512;\n    } else {\n        spark->vel.x = ((Random_GetControl() & 0x1FF) - 256) >> 1;\n        spark->vel.y = ((Random_GetControl() & 0x1FF) - 256) >> 1;\n        spark->vel.z = ((Random_GetControl() & 0x1FF) - 256) >> 1;\n    }\n\n    spark->friction = 4;\n\n    if (Random_GetControl() & 1) {\n        if (Room_Get(Lara_GetItem()->room_num)->flags.wind) {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_ROTATE\n                | SPARK_F_SPRITE | SPARK_F_SCALE;\n        } else {\n            spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n                | SPARK_F_SCALE;\n        }\n\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else if (Room_Get(Lara_GetItem()->room_num)->flags.wind) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_OUTSIDE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n    spark->gravity = -2 - (Random_GetControl() & 1);\n    spark->max_y_vel = -2 - (Random_GetControl() & 1);\n\n    uint8_t size = (Random_GetControl() & 7)\n        - ((weapon == LGT_ROCKET || weapon == LGT_GRENADE) ? 0 : 12) + 24;\n\n    if (initial) {\n        spark->size.width = size >> 1;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = (size + 4) << 1;\n    } else {\n        spark->size.width = size >> 2;\n        spark->src_size.width = spark->size.width;\n        spark->dst_size.width = size;\n    }\n\n    if (initial) {\n        spark->size.height = size >> 1;\n        spark->src_size.height = spark->size.width;\n        spark->dst_size.height = (size + 4) << 1;\n    } else {\n        spark->size.height = size >> 2;\n        spark->src_size.height = spark->size.width;\n        spark->dst_size.height = size;\n    }\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerShotgunSparks(const XYZ_32 pos, const XYZ_32 vel)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = 255;\n    spark->src_color.b = 0;\n    spark->dst_color.r = 255;\n    spark->dst_color.g = (Random_GetControl() & 0x7F) + 64;\n    spark->dst_color.b = 0;\n    spark->col_fade_speed = 3;\n    spark->fade_to_black = 5;\n    spark->life = 10;\n    spark->s_life = 10;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 7) - 3;\n    spark->pos.y = pos.y + (Random_GetControl() & 7) - 3;\n    spark->pos.z = pos.z + (Random_GetControl() & 7) - 3;\n    spark->vel.x = vel.x + (Random_GetControl() & 0x1FF) - 256;\n    spark->vel.y = vel.y + (Random_GetControl() & 0x1FF) - 256;\n    spark->vel.z = vel.z + (Random_GetControl() & 0x1FF) - 256;\n    spark->friction = 0;\n    spark->flags = SPARK_F_SCALE;\n    spark->scalar = 2;\n    spark->max_y_vel = 0;\n    spark->gravity = 0;\n    spark->size.width = (Random_GetControl() & 3) + 4;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = 1;\n    spark->size.height = (Random_GetControl() & 3) + 4;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = 1;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerRocketSmoke(\n    const XYZ_32 pos, const int32_t c, const int16_t room_num)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    spark->src_color.r = 0;\n    spark->src_color.g = 0;\n    spark->src_color.b = 0;\n    spark->dst_color.r = c + 64;\n    spark->dst_color.g = c + 64;\n    spark->dst_color.b = c + 64;\n    spark->fade_to_black = 12;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 4;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 3) + 20;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0xF) - 8;\n    spark->pos.y = pos.y + (Random_GetControl() & 0xF) - 8;\n    spark->pos.z = pos.z + (Random_GetControl() & 0xF) - 8;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -4 - (Random_GetControl() & 3);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 4;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->scalar = 3;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->gravity = -4 - (Random_GetControl() & 3);\n    spark->max_y_vel = -4 - (Random_GetControl() & 3);\n    const uint8_t size = (Random_GetControl() & 7) + 32;\n    spark->dst_size.width = size;\n    spark->src_size.width = size >> 2;\n    spark->size.width = size >> 2;\n    spark->src_size.height = size >> 2;\n    spark->size.height = size >> 2;\n    spark->dst_size.height = size;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerRocketFlame(\n    const XYZ_32 pos, const XYZ_32 vel, const int16_t item_num,\n    const int16_t room_num)\n{\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.g = spark->src_color.r;\n    spark->src_color.b = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n    spark->fade_to_black = 12;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 12;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->life = (Random_GetControl() & 3) + 28;\n    spark->s_life = spark->life;\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = vel.x;\n    spark->vel.y = vel.y;\n    spark->vel.z = vel.z;\n    spark->friction = 51;\n    spark->item_num = item_num;\n\n    if (Random_GetControl() & 1) {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_ROTATE\n            | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags =\n            SPARK_F_ALT_SPRITE | SPARK_F_ITEM | SPARK_F_SPRITE | SPARK_F_SCALE;\n    }\n\n    spark->gravity = 0;\n    spark->max_y_vel = 0;\n    spark->scalar = 2;\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->size.width = (Random_GetControl() & 7) + 32;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = 2;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = 2;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerFlamethrowerHitFlame(const XYZ_32 pos)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n    spark->src_color.r = 255;\n    spark->src_color.g = (Random_GetControl() & 0x1F) + 48;\n    spark->src_color.b = 48;\n    spark->dst_color.r = (Random_GetControl() & 0x3F) + 192;\n    spark->dst_color.g = (Random_GetControl() & 0x3F) + 128;\n    spark->dst_color.b = 32;\n\n    spark->fade_to_black = 8;\n    spark->col_fade_speed = (Random_GetControl() & 3) + 8;\n    spark->draw_type = DRAW_BLEND_ADD;\n    spark->extras = 0;\n    spark->life = (Random_GetControl() & 7) + 20;\n    spark->s_life = spark->life;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.y = -16 - (Random_GetControl() & 0xF);\n    spark->vel.z = (Random_GetControl() & 0xFF) - 128;\n    spark->friction = 5;\n\n    if (Random_GetControl() & 1) {\n        spark->gravity = -16 - (Random_GetControl() & 0x1F);\n        spark->max_y_vel = -16 - (Random_GetControl() & 7);\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE\n            | SPARK_F_SCALE;\n        spark->rot_angle = Random_GetControl() & 0xFFF;\n\n        if (Random_GetControl() & 1) {\n            spark->rot_add = -16 - (Random_GetControl() & 0xF);\n        } else {\n            spark->rot_add = (Random_GetControl() & 0xF) + 16;\n        }\n    } else {\n        spark->flags = SPARK_F_ALT_SPRITE | SPARK_F_SPRITE | SPARK_F_SCALE;\n        spark->gravity = -16 - (Random_GetControl() & 0x1F);\n        spark->max_y_vel = -16 - (Random_GetControl() & 7);\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 2;\n    spark->size.width = (Random_GetControl() & 0x1F) + 128;\n    spark->src_size.width = spark->size.width;\n    spark->dst_size.width = spark->size.width >> 4;\n    spark->size.height = spark->size.width;\n    spark->src_size.height = spark->size.height;\n    spark->dst_size.height = spark->size.height >> 4;\n    Sparks_FinishSetup(spark);\n}\n\nvoid Sparks_TriggerFlamethrowerSmoke(const XYZ_32 pos, const bool uw)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - pos.x;\n    const int32_t dz = lara_item->pos.z - pos.z;\n    const int32_t max_dist = 16 * WALL_L;\n    if (dx < -max_dist || dx > max_dist || dz < -max_dist || dz > max_dist) {\n        return;\n    }\n\n    SPARK *const spark = Sparks_GetFreeSpark();\n    spark->on = true;\n\n    if (uw) {\n        spark->src_color.r = 0;\n        spark->src_color.g = 0;\n        spark->src_color.b = 0;\n        spark->dst_color.r = 192;\n        spark->dst_color.g = 192;\n        spark->dst_color.b = 208;\n    } else {\n        spark->src_color.r = 144;\n        spark->src_color.g = 144;\n        spark->src_color.b = 144;\n        spark->dst_color.r = 64;\n        spark->dst_color.g = 64;\n        spark->dst_color.b = 64;\n    }\n\n    spark->col_fade_speed = 8;\n    spark->fade_to_black = 23;\n    spark->life = (Random_GetControl() & 0xF) + 32;\n    spark->s_life = spark->life;\n\n    if (uw) {\n        spark->draw_type = DRAW_BLEND_ADD;\n    } else {\n        spark->draw_type = DRAW_BLEND_SUB;\n    }\n\n    spark->extras = 0;\n    spark->dynamic = -1;\n    spark->pos.x = pos.x + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.y = pos.y + (Random_GetControl() & 0x1F) - 16;\n    spark->pos.z = pos.z + (Random_GetControl() & 0x1F) - 16;\n    spark->vel.x = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n    spark->vel.y = (Random_GetControl() & 0xFF) - 128;\n    spark->vel.z = ((Random_GetControl() & 0xFFF) - 2048) >> 2;\n\n    if (uw) {\n        spark->friction = 20;\n        spark->vel.y >>= 4;\n        spark->pos.y += 32;\n    } else {\n        spark->friction = 6;\n    }\n\n    spark->flags =\n        SPARK_F_ALT_SPRITE | SPARK_F_ROTATE | SPARK_F_SPRITE | SPARK_F_SCALE;\n    spark->rot_angle = Random_GetControl() & 0xFFF;\n\n    if (Random_GetControl() & 1) {\n        spark->rot_add = -16 - (Random_GetControl() & 0xF);\n    } else {\n        spark->rot_add = (Random_GetControl() & 0xF) + 16;\n    }\n\n    spark->sprite_idx = Object_Get(O_EXPLOSION_1)->mesh_idx;\n    spark->scalar = 3;\n\n    if (uw) {\n        spark->max_y_vel = 0;\n        spark->gravity = 0;\n    } else {\n        spark->gravity = -3 - (Random_GetControl() & 3);\n        spark->max_y_vel = -4 - (Random_GetControl() & 3);\n    }\n\n    spark->dst_size.width = (Random_GetControl() & 0x1F) + 128;\n    spark->size.width = spark->dst_size.width >> 2;\n    spark->src_size.width = spark->size.width;\n\n    spark->dst_size.height =\n        spark->dst_size.width + (Random_GetControl() & 0x1F) + 32;\n    spark->size.height = spark->dst_size.height >> 3;\n    spark->src_size.height = spark->size.height;\n    Sparks_FinishSetup(spark);\n}\n"
  },
  {
    "path": "src/trx/game/sparks/spawners.h",
    "content": "#pragma once\n\n#include <trx/game/items/types.h>\n#include <trx/game/lara/enum.h>\n#include <trx/game/types.h>\n\nvoid Sparks_TriggerBubble(\n    int32_t x, int32_t y, int32_t z, int32_t size, int32_t size_range,\n    int16_t effect_num);\n\nvoid Sparks_TriggerWaterfallMist(int32_t x, int32_t y, int32_t z, int32_t ang);\n\nvoid Sparks_TriggerBreath(XYZ_32 pos, XYZ_32 vel, int16_t room_num);\n\nvoid Sparks_TriggerUnderwaterExplosion(const ITEM *item);\nvoid Sparks_TriggerExplosionBubble(XYZ_32 pos, int16_t room_num);\nvoid Sparks_TriggerExplosionSparks(\n    XYZ_32 pos, int32_t extras, int32_t dynamic, int32_t uw, int16_t room_num);\nvoid Sparks_TriggerExplosionSmoke(XYZ_32 pos, bool uw, int16_t room_num);\nvoid Sparks_TriggerExplosionSmokeEnd(XYZ_32 pos, bool uw, int16_t room_num);\n\nvoid Sparks_TriggerFireFlame(XYZ_32 pos, int32_t body_part, int32_t type);\nvoid Sparks_TriggerStaticFlame(XYZ_32 pos, int32_t size);\nvoid Sparks_TriggerFireSmoke(XYZ_32 pos, int32_t body_part, int32_t type);\nvoid Sparks_TriggerSideFlame(\n    XYZ_32 pos, int32_t angle, int32_t speed, bool pilot);\n\nvoid Sparks_TriggerDartSmoke(XYZ_32 pos, XZ_32 vel, bool hit);\n\nvoid Sparks_TriggerPickupAid(XYZ_32 pos, XZ_32 vel);\n\nvoid Sparks_TriggerFlareSparks(XYZ_32 pos, XYZ_32 vel, bool smoke);\n\nvoid Sparks_TriggerRicochet(GAME_VECTOR pos, int32_t angle, int32_t size);\n\nvoid Sparks_TriggerGunSmoke(\n    GAME_VECTOR pos, bool initial, LARA_GUN_TYPE weapon, int32_t shade);\nvoid Sparks_TriggerGunSmokeDirected(\n    GAME_VECTOR pos, XYZ_32 vel, bool initial, LARA_GUN_TYPE weapon,\n    int32_t shade);\n\nvoid Sparks_TriggerShotgunSparks(XYZ_32 pos, XYZ_32 vel);\n\nvoid Sparks_TriggerRocketSmoke(XYZ_32 pos, int32_t c, int16_t room_num);\nvoid Sparks_TriggerRocketFlame(\n    XYZ_32 pos, XYZ_32 vel, int16_t item_num, int16_t room_num);\n\nvoid Sparks_TriggerBlood(XYZ_32 pos, int32_t angle_12, int32_t count);\nvoid Sparks_TriggerBloodD(XYZ_32 pos, int32_t angle_12, int32_t count);\n\nvoid Sparks_TriggerFlamethrowerHitFlame(XYZ_32 pos);\nvoid Sparks_TriggerFlamethrowerSmoke(XYZ_32 pos, bool uw);\n"
  },
  {
    "path": "src/trx/game/sparks/types.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n#include <trx/core/math/types.h>\n#include <trx/game/output/types.h>\n#include <trx/game/sparks/enum.h>\n\n#include <stdint.h>\n\ntypedef struct SPARK {\n    bool on;\n\n    uint8_t s_life;\n    uint8_t life;\n\n    // NOTE: `pos` is either absolute world position, or a relative offset when\n    // attached to an FX/ITEM and not using `SPARK_F_ATTACHED_POS`.\n    XYZ_32 pos;\n    XYZ_32 prev_pos;\n    XYZ_32 prev_world_pos;\n    XYZ_32 vel;\n    struct {\n        uint8_t width;\n        uint8_t height;\n    } src_size, dst_size, size, prev_size;\n\n    RGB_888 src_color;\n    RGB_888 dst_color;\n    RGB_888 color;\n    RGB_888 prev_color;\n\n    uint8_t scalar;\n    uint8_t col_fade_speed;\n    uint8_t fade_to_black;\n    int16_t gravity;\n    int8_t max_y_vel;\n    uint8_t friction;\n\n    uint16_t flags;\n    union {\n        // effect/item index depending on flags (SF_FX/SF_ITEM)\n        int16_t effect_num;\n        int16_t item_num;\n    };\n    uint8_t room_num;\n    uint8_t node_num;\n\n    uint8_t extras;\n    int8_t dynamic;\n\n    uint16_t rot_angle; // 0..0xFFF\n    uint16_t prev_rot_angle; // 0..0xFFF\n    int8_t rot_add;\n\n    int32_t sprite_idx;\n    DRAW_TYPE draw_type;\n} SPARK;\n"
  },
  {
    "path": "src/trx/game/sparks.h",
    "content": "#pragma once\n\n#include <trx/game/sparks/manager.h>\n#include <trx/game/sparks/spawners.h>\n"
  },
  {
    "path": "src/trx/game/spawn.c",
    "content": "#include <trx/game/spawn.h>\n\n#include <trx/config.h>\n#include <trx/core/math.h>\n#include <trx/game/collision.h>\n#include <trx/game/collision/los.h>\n#include <trx/game/effects.h>\n#include <trx/game/fx/water.h>\n#include <trx/game/gun/vars.h>\n#include <trx/game/lara.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/output.h>\n#include <trx/game/random.h>\n#include <trx/game/rooms.h>\n#include <trx/game/sound.h>\n#include <trx/game/sparks.h>\n#include <trx/game/sparks/spawners.h>\n#include <trx/version.h>\n\nstatic void M_ShootAtLara(EFFECT *const effect)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const int32_t dx = lara_item->pos.x - effect->pos.x;\n    const int32_t dy = lara_item->pos.y - effect->pos.y;\n    const int32_t dz = lara_item->pos.z - effect->pos.z;\n\n    const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(lara_item);\n    const int32_t dist_vert =\n        dy + bounds->max.y + 3 * (bounds->min.y - bounds->max.y) / 4;\n    const int32_t dist_horz = Math_Sqrt(SQUARE(dz) + SQUARE(dx));\n    effect->rot.x = -Math_Atan(dist_horz, dist_vert);\n    effect->rot.y = Math_Atan(dz, dx);\n    effect->rot.x += (Random_GetControl() - 0x4000) / 64;\n    effect->rot.y += (Random_GetControl() - 0x4000) / 64;\n}\n\nXYZ_32 Spawn_GetRayPos(\n    const GAME_VECTOR start, GAME_VECTOR hit_pos, const int32_t dist)\n{\n    // Get the position at wall\n    LOS_Check(&start, &hit_pos, true);\n\n    // Retract a bit\n    const int16_t angle = XYZ_32_GetYaw((XYZ_32) {\n        .x = hit_pos.x - start.x,\n        .y = hit_pos.y - start.y,\n        .z = hit_pos.z - start.z,\n    });\n    hit_pos.pos.x -= (dist * Math_Sin(angle)) >> W2V_SHIFT;\n    hit_pos.pos.z -= (dist * Math_Cos(angle)) >> W2V_SHIFT;\n\n    return hit_pos.pos;\n}\n\nvoid Spawn_Splash(const ITEM *const item)\n{\n    if (g_TRVersion == 3) {\n        FX_Water_Splash(item);\n        return;\n    }\n\n    const int32_t water_height = Room_GetWaterHeight(item->pos, item->room_num);\n    int16_t room_num = item->room_num;\n    Room_GetSector(item->pos, &room_num);\n\n    for (int32_t i = 0; i < 10; i++) {\n        const int16_t effect_num = Effect_Create(room_num);\n        if (effect_num == NO_EFFECT) {\n            continue;\n        }\n\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_SPLASH_1;\n        effect->pos.x = item->pos.x;\n        effect->pos.y = water_height;\n        effect->pos.z = item->pos.z;\n        effect->rot.y = 2 * Random_GetDraw() + DEG_180;\n        effect->speed = Random_GetDraw() / 256;\n        effect->frame_num = 0;\n    }\n}\n\nvoid Spawn_Ricochet(const GAME_VECTOR pos)\n{\n    if (g_TRVersion == 3) {\n        const ITEM *const lara_item = Lara_GetItem();\n        const int32_t angle16 = Math_Atan(\n            lara_item->pos.z - pos.pos.z, lara_item->pos.x - pos.pos.x);\n        Sparks_TriggerRicochet(pos, ((uint16_t)angle16 >> 4) & 0x0FFF, 16);\n        Sound_Effect(SFX_LARA_RICOCHET, &pos.pos, SPM_NORMAL);\n        return;\n    }\n\n    const int16_t effect_num = Effect_Create(pos.room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_RICOCHET;\n        effect->pos = pos.pos;\n        effect->counter = 4;\n        effect->frame_num = -3 * Random_GetDraw() / 0x8000;\n        Sound_Effect(SFX_LARA_RICOCHET, &effect->pos, SPM_NORMAL);\n    }\n}\n\nvoid Spawn_RicochetRay(const GAME_VECTOR start, GAME_VECTOR hit_pos)\n{\n    hit_pos.pos = Spawn_GetRayPos(start, hit_pos, STEP_L / 12);\n    Spawn_Ricochet(hit_pos);\n}\n\nvoid Spawn_Bubble(const XYZ_32 *const pos, const int16_t room_num)\n{\n    if (g_TRVersion == 3) {\n        Spawn_BubbleEx(pos, room_num, 8, 8);\n        return;\n    }\n\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = *pos;\n    effect->object_id = O_BUBBLE_1;\n    effect->frame_num = -((Random_GetDraw() * 3) / 0x8000);\n    effect->speed = 10 + ((Random_GetDraw() * 6) / 0x8000);\n}\n\nvoid Spawn_BubbleEx(\n    const XYZ_32 *const pos, const int16_t room_num, const int32_t size,\n    const int32_t size_range)\n{\n    if (g_TRVersion != 3) {\n        Spawn_Bubble(pos, room_num);\n        return;\n    }\n\n    int16_t water_room = room_num;\n    Room_GetSector(*pos, &water_room);\n    if (!Room_Get(water_room)->flags.underwater) {\n        return;\n    }\n\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = *pos;\n    effect->object_id = O_BUBBLE_1;\n    effect->frame_num = 0;\n\n    effect->speed = (Random_GetControl() & 0xFF) + 64;\n    effect->fall_speed = (Random_GetControl() & 0x1F) + 32;\n\n    Sparks_TriggerBubble(pos->x, pos->y, pos->z, size, size_range, effect_num);\n}\n\nint16_t Spawn_Blood(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    if (g_TRVersion == 3) {\n        if (Room_Get(room_num)->flags.underwater) {\n            FX_Water_TriggerUnderwaterBlood(\n                (XYZ_32) { x, y, z }, Random_GetControl() & 7);\n        } else {\n            Sparks_TriggerBlood(\n                (XYZ_32) { x, y, z }, y_rot >> 4,\n                (Random_GetControl() & 7) + 6);\n        }\n        return NO_EFFECT;\n    }\n\n    OBJECT_ID object_id = NO_OBJECT;\n    switch (g_Config.visuals.blood_effects) {\n    case BLOOD_EFFECTS_DISABLED:\n        return NO_EFFECT;\n    case BLOOD_EFFECTS_PINK:\n        object_id = O_BLOOD_PINK;\n        break;\n    case BLOOD_EFFECTS_RED:\n        object_id = O_BLOOD;\n        break;\n    case BLOOD_EFFECTS_NUMBER_OF:\n        return NO_EFFECT;\n    }\n    if (object_id == NO_OBJECT) {\n        return NO_EFFECT;\n    }\n\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->pos.x = x;\n        effect->pos.y = y;\n        effect->pos.z = z;\n        effect->rot.y = y_rot;\n        effect->speed = speed;\n        effect->frame_num = 0;\n        effect->object_id = object_id;\n        effect->counter = 0;\n    }\n    return effect_num;\n}\n\nint16_t Spawn_BloodD(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    if (g_TRVersion == 3) {\n        if (Room_Get(room_num)->flags.underwater) {\n            FX_Water_TriggerUnderwaterBloodD(\n                (XYZ_32) { x, y + 64, z }, Random_GetDraw() & 7);\n        } else {\n            Sparks_TriggerBloodD(\n                (XYZ_32) { x, y, z }, y_rot >> 4, (Random_GetDraw() & 7) + 6);\n        }\n        return NO_EFFECT;\n    }\n    return Spawn_Blood(x, y, z, speed, y_rot, room_num);\n}\n\nvoid Spawn_BloodBath(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num, const int32_t count)\n{\n    for (int32_t i = 0; i < count; i++) {\n        if (g_TRVersion == 3) {\n            Spawn_Blood(\n                x - (Random_GetControl() << 9) / 0x8000 + 256,\n                y - (Random_GetControl() << 9) / 0x8000 + 256,\n                z - (Random_GetControl() << 9) / 0x8000 + 256, speed, y_rot,\n                room_num);\n        } else {\n            Spawn_Blood(\n                x - (Random_GetDraw() << 9) / 0x8000 + 256,\n                y - (Random_GetDraw() << 9) / 0x8000 + 256,\n                z - (Random_GetDraw() << 9) / 0x8000 + 256, speed, y_rot,\n                room_num);\n        }\n    }\n}\n\nvoid Spawn_BloodBathD(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num, const int32_t count)\n{\n    for (int32_t i = 0; i < count; i++) {\n        Spawn_BloodD(\n            x - (Random_GetDraw() << 9) / 0x8000 + 256, y,\n            z - (Random_GetDraw() << 9) / 0x8000 + 256, speed, y_rot, room_num);\n    }\n}\n\nint16_t Spawn_GunShot(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_EFFECT) {\n        return effect_num;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos.x = x;\n    effect->pos.y = y;\n    effect->pos.z = z;\n    effect->room_num = room_num;\n    effect->rot.z = 0;\n    effect->rot.x = 0;\n    effect->rot.y = y_rot;\n    effect->counter = 3;\n    effect->frame_num = 0;\n    effect->object_id = O_GUN_FLASH;\n    effect->shade = SHADE_NEUTRAL;\n    return effect_num;\n}\n\nint16_t Spawn_GunHit(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    XYZ_32 vec = {\n        .x = -((Random_GetDraw() - 0x4000) << 7) / 0x7FFF,\n        .y = -((Random_GetDraw() - 0x4000) << 7) / 0x7FFF,\n        .z = -((Random_GetDraw() - 0x4000) << 7) / 0x7FFF,\n    };\n    Collide_GetJointAbsPosition(\n        lara_item, &vec, Random_GetControl() * LM_NUMBER_OF / 0x7FFF);\n    Spawn_Blood(\n        vec.x, vec.y, vec.z, lara_item->speed, lara_item->rot.y,\n        lara_item->room_num);\n    Sound_Effect(SFX_LARA_BULLETHIT, &lara_item->pos, SPM_NORMAL);\n    return Spawn_GunShot(x, y, z, speed, y_rot, room_num);\n}\n\nint16_t Spawn_GunMiss(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    const GAME_VECTOR pos = {\n        .x = lara_item->pos.x + ((Random_GetDraw() - 0x4000) << 9) / 0x7FFF,\n        .y = lara_item->floor,\n        .z = lara_item->pos.z + ((Random_GetDraw() - 0x4000) << 9) / 0x7FFF,\n        .room_num = lara_item->room_num,\n    };\n    Spawn_Ricochet(pos);\n    return Spawn_GunShot(x, y, z, speed, y_rot, room_num);\n}\n\nvoid Spawn_GunShell(const LARA_GUN_TYPE weapon_type, const bool right)\n{\n    if (g_TRVersion < 3) {\n        return;\n    }\n\n    const ITEM *const lara_item = Lara_GetItem();\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    XYZ_32 offset = right ? g_Weapons[weapon_type].shell_pos\n                          : g_Weapons[weapon_type].shell_pos_alt;\n    if (offset.x == 0 && offset.y == 0 && offset.z == 0) {\n        return;\n    }\n\n    Lara_GetMeshPos(right ? LM_HAND_R : LM_HAND_L, &offset);\n\n    const int16_t effect_num = Effect_Create(lara_item->room_num);\n    if (effect_num == NO_EFFECT) {\n        return;\n    }\n\n    const bool shotgun = weapon_type == LGT_SHOTGUN;\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos = offset;\n    effect->room_num = lara_item->room_num;\n    effect->rot.x = 0;\n    effect->rot.y = 0;\n    effect->rot.z = (int16_t)Random_GetControl();\n    effect->speed = (int16_t)((Random_GetControl() & 0x1F) + 16);\n    effect->object_id = shotgun ? O_SHOTGUN_SHELL : O_GUN_SHELL;\n    effect->frame_num = Object_Get(effect->object_id)->mesh_idx;\n    effect->fall_speed = (int16_t)(-48 - (Random_GetControl() & 7));\n    effect->shade = 0x4210;\n    effect->counter = (int16_t)((Random_GetControl() & 1) + 1);\n\n    if (shotgun || weapon_type == LGT_M16 || weapon_type == LGT_MP5) {\n        const int32_t spread = (Random_GetControl() & 0xFFF);\n        effect->flag1 = lara->left_arm.rot.y + lara_item->rot.y - spread\n            + lara->torso_rot.y + (shotgun ? 0x2800 : 0x4800);\n        if (!shotgun && effect->speed < 24) {\n            effect->speed += 24;\n        }\n    } else if (right) {\n        effect->flag1 = lara_item->rot.y - (Random_GetControl() & 0xFFF)\n            + lara->left_arm.rot.y + 0x4800;\n    } else {\n        effect->flag1 = lara_item->rot.y + (Random_GetControl() & 0xFFF)\n            + lara->left_arm.rot.y - 0x4800;\n    }\n}\n\nint16_t Spawn_AtlanteanShard(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num)\n{\n    int16_t effect_num = Effect_Create(room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *effect = Effect_Get(effect_num);\n        effect->room_num = room_num;\n        effect->pos.x = x;\n        effect->pos.y = y;\n        effect->pos.z = z;\n        effect->rot.x = 0;\n        effect->rot.y = y_rot;\n        effect->rot.z = 0;\n        effect->object_id = O_MISSILE_ATLANTEAN_SHARD;\n        effect->frame_num = 0;\n        effect->speed = 250;\n        effect->shade = 3584;\n        M_ShootAtLara(effect);\n    }\n    return effect_num;\n}\n\nint16_t Spawn_AtlanteanBomb(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num)\n{\n    int16_t effect_num = Effect_Create(room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *effect = Effect_Get(effect_num);\n        effect->room_num = room_num;\n        effect->pos.x = x;\n        effect->pos.y = y;\n        effect->pos.z = z;\n        effect->rot.x = 0;\n        effect->rot.y = y_rot;\n        effect->rot.z = 0;\n        effect->object_id = O_MISSILE_ATLANTEAN_BOMB;\n        effect->frame_num = 0;\n        effect->speed = 220;\n        effect->shade = SHADE_NEUTRAL;\n        M_ShootAtLara(effect);\n    }\n    return effect_num;\n}\n\nint16_t Spawn_FireStream(\n    const int32_t x, const int32_t y, const int32_t z, int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_EFFECT) {\n        return effect_num;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos.x = x;\n    effect->pos.y = y;\n    effect->pos.z = z;\n    effect->rot.x = 0;\n    effect->rot.y = y_rot;\n    effect->rot.z = 0;\n    effect->room_num = room_num;\n    effect->speed = 200;\n    effect->frame_num =\n        ((Object_Get(O_MISSILE_FLAME)->mesh_count + 1) * Random_GetDraw())\n        >> 15;\n    effect->object_id = O_MISSILE_FLAME;\n    effect->shade = 14 * 256;\n\n    M_ShootAtLara(effect);\n\n    if (Object_Get(O_DRAGON_FRONT)->loaded) {\n        effect->counter = 0x4000;\n    } else {\n        effect->counter = 20;\n    }\n\n    return effect_num;\n}\n\nvoid Spawn_MysticLight(const int16_t item_num)\n{\n    const ITEM *const item = Item_Get(item_num);\n\n    const int16_t effect_num = Effect_Create(item->room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->object_id = O_TWINKLE;\n\n        effect->rot.y = 2 * Random_GetDraw();\n        effect->pos = XYZ_32_OffsetYaw(item->pos, effect->rot.y, 5 * WALL_L);\n        effect->pos.y += (Random_GetDraw() >> 2) - WALL_L;\n        effect->room_num = item->room_num;\n        effect->counter = item_num;\n        effect->frame_num = 0;\n    }\n\n    // clang-format off\n    Output_AddDynamicLight(\n        item->pos,\n        ((4 * Random_GetDraw()) >> 15) + 12,\n        ((4 * Random_GetDraw()) >> 15) + 10);\n    // clang-format on\n}\n\nint16_t Spawn_Knife(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num == NO_EFFECT) {\n        return effect_num;\n    }\n\n    EFFECT *const effect = Effect_Get(effect_num);\n    effect->pos.x = x;\n    effect->pos.y = y;\n    effect->pos.z = z;\n    effect->room_num = room_num;\n    effect->rot.x = 0;\n    effect->rot.y = y_rot;\n    effect->rot.z = 0;\n    effect->speed = 150;\n    effect->frame_num = 0;\n    effect->object_id = O_MISSILE_KNIFE;\n    effect->shade = 3584;\n    M_ShootAtLara(effect);\n    return effect_num;\n}\n\nint16_t Spawn_Harpoon(\n    const int32_t x, const int32_t y, const int32_t z, const int16_t speed,\n    const int16_t y_rot, const int16_t room_num)\n{\n    const int16_t effect_num = Effect_Create(room_num);\n    if (effect_num != NO_EFFECT) {\n        EFFECT *const effect = Effect_Get(effect_num);\n        effect->pos.x = x;\n        effect->pos.y = y;\n        effect->pos.z = z;\n        effect->room_num = room_num;\n        effect->rot.x = 0;\n        effect->rot.y = y_rot;\n        effect->rot.z = 0;\n        effect->speed = 150;\n        effect->fall_speed = 0;\n        effect->frame_num = 0;\n        effect->object_id = O_MISSILE_HARPOON;\n        effect->shade = 3584;\n        M_ShootAtLara(effect);\n    }\n    return effect_num;\n}\n"
  },
  {
    "path": "src/trx/game/spawn.h",
    "content": "#pragma once\n\n#include <trx/game/gun/types.h>\n#include <trx/game/items/types.h>\n#include <trx/game/lara/types.h>\n#include <trx/game/types.h>\n\nXYZ_32 Spawn_GetRayPos(GAME_VECTOR start, GAME_VECTOR hit_pos, int32_t dist);\n\nvoid Spawn_Splash(const ITEM *item);\nvoid Spawn_Ricochet(GAME_VECTOR pos);\nvoid Spawn_RicochetRay(GAME_VECTOR start, GAME_VECTOR hit_pos);\n\nvoid Spawn_Bubble(const XYZ_32 *pos, int16_t room_num);\nvoid Spawn_BubbleEx(\n    const XYZ_32 *pos, int16_t room_num, int32_t size, int32_t size_range);\n\nint16_t Spawn_Blood(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\nint16_t Spawn_BloodD(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\nvoid Spawn_BloodBath(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num, int32_t count);\nvoid Spawn_BloodBathD(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num, int32_t count);\nint16_t Spawn_GunShot(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\nint16_t Spawn_GunHit(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\nint16_t Spawn_GunMiss(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\n\nvoid Spawn_GunShell(LARA_GUN_TYPE weapon_type, bool right);\nvoid Spawn_ShotgunShell(void);\n\nint16_t Spawn_AtlanteanShard(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\nint16_t Spawn_AtlanteanBomb(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\n\nint16_t Spawn_FireStream(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\n\nvoid Spawn_MysticLight(int16_t item_num);\n\nint16_t Spawn_Knife(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\nint16_t Spawn_Harpoon(\n    int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot,\n    int16_t room_num);\n"
  },
  {
    "path": "src/trx/game/stats/common.c",
    "content": "#include <trx/core/log.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/objects/general/pickup.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n\nbool Stats_IsSecretValid(const int16_t secret_idx)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) {\n        return false;\n    }\n    const uint32_t secret_mask = 1 << secret_idx;\n    return (secret_mask & max_stats->all_secrets_mask) != 0;\n}\n\nbool Stats_HasSecret(const int16_t secret_idx)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) {\n        return false;\n    }\n    const uint32_t secret_mask = 1 << secret_idx;\n    if ((secret_mask & max_stats->all_secrets_mask) == 0) {\n        return false;\n    }\n    return (resume->stats.secret_flags & secret_mask) != 0;\n}\n\nbool Stats_RemoveSecret(const int16_t secret_idx)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) {\n        return false;\n    }\n    const uint32_t secret_mask = 1 << secret_idx;\n    if ((secret_mask & max_stats->all_secrets_mask) == 0) {\n        return false;\n    }\n    if (!(resume->stats.secret_flags & secret_mask)) {\n        return false;\n    }\n    LOG_INFO(\"Removing secret %d\", secret_idx);\n    resume->stats.secret_flags &= ~secret_mask;\n    resume->stats.secret_count--;\n    return true;\n}\n\nbool Stats_AddSecret(const int16_t secret_idx)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    if (secret_idx < 0 || secret_idx >= STATS_MAX_SECRETS) {\n        return false;\n    }\n    const uint32_t secret_mask = 1 << secret_idx;\n    if ((secret_mask & max_stats->all_secrets_mask) == 0) {\n        return false;\n    }\n    if (resume->stats.secret_flags & secret_mask) {\n        return false;\n    }\n    LOG_INFO(\"Adding secret %d\", secret_idx);\n    resume->stats.secret_flags |= secret_mask;\n    resume->stats.secret_count++;\n    return true;\n}\n\nvoid Stats_UpdateSecrets(LEVEL_STATS *const stats)\n{\n    stats->secret_count = 0;\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        stats->secret_count += (stats->secret_flags & (1 << i)) ? 1 : 0;\n    }\n}\n\nvoid Stats_MarkSecretCollected(const ITEM *const item)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n    resume->stats.secret_flags |= Pickup_GetSecretMask(item);\n    Stats_UpdateSecrets(&resume->stats);\n}\n\nbool Stats_CheckAllLevelSecretsPickedUp(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    int32_t flags = resume->stats.secret_flags;\n    size_t count = 0;\n    while (flags != 0) {\n        count += flags & 1;\n        flags >>= 1;\n    }\n    return count >= max_stats->max_pickup_secret_count;\n}\n\nbool Stats_CheckAllSecretsCollected(void)\n{\n    const FINAL_STATS final_stats = Stats_ComputeFinalStats(false);\n    return final_stats.stats.secret_count\n        >= final_stats.max_stats.max_secret_count;\n}\n\nvoid Stats_AddMedipacksUsed(const double medipack_value)\n{\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel());\n    resume->stats.medipacks_used += medipack_value;\n}\n\nvoid Stats_AddDeath(void)\n{\n    const GF_LEVEL *const current_level = Game_GetCurrentLevel();\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level);\n    resume->stats.death_count++;\n    const SAVEGAME_SLOT_REF save_slot = Savegame_GetBoundSlot();\n    if (Savegame_IsValidSlotRef(save_slot)) {\n        Savegame_UpdateDeathCounters(save_slot, resume->stats.death_count);\n    }\n}\n\nvoid Stats_UpdateTimer(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.timer++;\n    }\n}\n\nvoid Stats_AddKill(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.kill_count++;\n    }\n}\n\nvoid Stats_AddCrystal(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.crystal_count++;\n    }\n}\n\nvoid Stats_AddPickup(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.pickup_count++;\n    }\n}\n\nvoid Stats_AddAmmoHits(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.ammo_hits++;\n    }\n}\n\nvoid Stats_AddAmmoUsed(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.ammo_used++;\n    }\n}\n\nvoid Stats_AddDistanceTravelled(const XYZ_32 pos, const XYZ_32 last_pos)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level != nullptr) {\n        RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        resume->stats.distance_travelled += XYZ_32_GetDistance(pos, last_pos);\n    }\n}\n\nFINAL_STATS Stats_ComputeFinalStats(const bool include_bonus_levels)\n{\n    FINAL_STATS result = {};\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (!(level->type == GFL_NORMAL\n              || (level->type == GFL_BONUS && include_bonus_levels))) {\n            continue;\n        }\n\n        const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        if (resume != nullptr) {\n#define L_ADD(prop) result.stats.prop += resume->stats.prop;\n            L_ADD(kill_count);\n            L_ADD(crystal_count);\n            L_ADD(pickup_count);\n            L_ADD(secret_count);\n            L_ADD(timer);\n            L_ADD(ammo_hits);\n            L_ADD(ammo_used);\n            L_ADD(medipacks_used);\n            L_ADD(distance_travelled);\n            L_ADD(death_count);\n#undef L_ADD\n        }\n\n        const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n        if (max_stats != nullptr) {\n#define L_ADD(prop) result.max_stats.prop += max_stats->prop;\n            L_ADD(max_kill_count);\n            L_ADD(max_kill_ally_count);\n            L_ADD(max_kill_non_ally_count);\n            L_ADD(max_crystal_count);\n            L_ADD(max_pickup_count);\n            L_ADD(max_secret_count);\n            L_ADD(max_pickup_secret_count);\n#undef L_ADD\n        }\n    }\n\n    return result;\n}\n\nOBJECT_ID Stats_GetSecretObject(const int32_t secret_idx)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    if (level == nullptr) {\n        return NO_OBJECT;\n    }\n    const LEVEL_MAX_STATS *const stats = Stats_GetLevelMaxStats(level);\n    ASSERT(stats != nullptr);\n    ASSERT(secret_idx >= 0 && secret_idx < STATS_MAX_SECRETS);\n    return stats->secret_objects[secret_idx].assigned_object_id;\n}\n\nuint32_t Stats_GetSecretMaskForItem(\n    const GF_LEVEL *const level, const int16_t item_num)\n{\n    if (level == nullptr) {\n        return 0;\n    }\n\n    const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    if (max_stats == nullptr) {\n        return 0;\n    }\n\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        if (max_stats->secret_item_masks[i].item_num == item_num) {\n            return max_stats->secret_item_masks[i].secret_mask;\n        }\n    }\n\n    return 0;\n}\n\nvoid Stats_MarkAlliesHostile(void)\n{\n    const GF_LEVEL *const level = Game_GetCurrentLevel();\n    RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n    resume->hurt_allies = true;\n}\n"
  },
  {
    "path": "src/trx/game/stats/common.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/game_flow/types.h>\n#include <trx/game/stats/types.h>\n\nbool Stats_HasSecret(int16_t secret_idx);\nbool Stats_RemoveSecret(int16_t secret_idx);\nbool Stats_AddSecret(int16_t secret_idx);\nbool Stats_IsSecretValid(int16_t secret_idx);\nOBJECT_ID Stats_GetSecretObject(int32_t secret_idx);\nuint32_t Stats_GetSecretMaskForItem(const GF_LEVEL *level, int16_t item_num);\n\nvoid Stats_UpdateSecrets(LEVEL_STATS *stats);\nvoid Stats_MarkSecretCollected(const ITEM *item);\nbool Stats_CheckAllSecretsCollected(void);\nbool Stats_CheckAllLevelSecretsPickedUp(void);\n\nvoid Stats_UpdateTimer(void);\nvoid Stats_AddKill(void);\nvoid Stats_AddCrystal(void);\nvoid Stats_AddPickup(void);\nvoid Stats_AddAmmoHits(void);\nvoid Stats_AddAmmoUsed(void);\nvoid Stats_AddDeath(void);\nvoid Stats_AddMedipacksUsed(double medipack_value);\nvoid Stats_AddDistanceTravelled(XYZ_32 pos, XYZ_32 last_pos);\nvoid Stats_MarkAlliesHostile(void);\n\nFINAL_STATS Stats_ComputeFinalStats(bool include_bonus_levels);\n"
  },
  {
    "path": "src/trx/game/stats/const.h",
    "content": "#pragma once\n\n#define STATS_MAX_SECRETS 16\n"
  },
  {
    "path": "src/trx/game/stats/init.c",
    "content": "#include <trx/game/stats/init.h>\n\n#include <trx/config.h>\n#include <trx/core/benchmark.h>\n#include <trx/core/hash.h>\n#include <trx/core/json.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/virtual_file.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/inject.h>\n#include <trx/game/items.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/level.h>\n#include <trx/game/level/cache.h>\n#include <trx/game/level/format/format.h>\n#include <trx/game/lua.h>\n#include <trx/game/objects.h>\n#include <trx/game/rooms.h>\n#include <trx/game/stats.h>\n\n#include <string.h>\n\n#define M_CACHE_VERSION 5\n#define M_CACHE_FILENAME \"max_stats.cache.json\"\n\nstatic LEVEL_MAX_STATS *m_Stats = nullptr;\nstatic int32_t m_StatsCapacity = 0;\nstatic bool m_GameHasCrystals = false;\n\nstatic const OBJECT_ID m_FullInitObjectIDs[] = {\n    O_BARTOLI, O_CENTAUR_STATUE, O_PODS, O_BIG_POD, NO_OBJECT,\n};\n\nstatic bool M_ShouldUseFullInitialisation(const OBJECT_ID object_id)\n{\n    for (int32_t i = 0; m_FullInitObjectIDs[i] != NO_OBJECT; i++) {\n        if (m_FullInitObjectIDs[i] == object_id) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic void M_SetupStatsFullInitObjects(void)\n{\n    for (int32_t i = 0; m_FullInitObjectIDs[i] != NO_OBJECT; i++) {\n        OBJECT *const obj = Object_Get(m_FullInitObjectIDs[i]);\n        if (!obj->loaded || obj->setup_func == nullptr) {\n            continue;\n        }\n        obj->setup_func(obj);\n    }\n}\n\nstatic void M_EnsureStatsStorage(const int32_t level_count)\n{\n    ASSERT(level_count >= 0);\n\n    if (m_StatsCapacity != level_count) {\n        m_Stats = Memory_Realloc(\n            m_Stats, sizeof(LEVEL_MAX_STATS) * (size_t)level_count);\n        m_StatsCapacity = level_count;\n    }\n}\n\nstatic uint64_t M_ComputeInputsChecksum(const GF_LEVEL_TABLE *const level_table)\n{\n    uint64_t hash = LevelCache_InitChecksum(\"max_stats_cache\", M_CACHE_VERSION);\n    hash = Hash_FNV1a64_UpdateU32(hash, (uint32_t)level_table->count);\n    hash = Hash_FNV1a64_UpdateU32(\n        hash, (uint32_t)g_Config.gameplay.restore_ps1_enemies);\n\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        hash = LevelCache_UpdateLevelChecksum(hash, level);\n        hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.pickups);\n        hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.kills);\n        hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.ally_kills);\n        hash = Hash_FNV1a64_UpdateU32(hash, level->unobtainable.secrets);\n    }\n\n    return hash;\n}\n\nstatic JSON_OBJECT *M_SerializeLevelMaxStats(const LEVEL_MAX_STATS *const stats)\n{\n    JSON_OBJECT *const out = JSON_ObjectNew();\n\n    JSON_ObjectAppendInt64(\n        out, \"max_pickup_secret_count\",\n        (int64_t)stats->max_pickup_secret_count);\n    JSON_ObjectAppendInt64(\n        out, \"max_kill_count\", (int64_t)stats->max_kill_count);\n    JSON_ObjectAppendInt64(\n        out, \"max_kill_ally_count\", (int64_t)stats->max_kill_ally_count);\n    JSON_ObjectAppendInt64(\n        out, \"max_kill_non_ally_count\",\n        (int64_t)stats->max_kill_non_ally_count);\n    JSON_ObjectAppendInt64(\n        out, \"max_crystal_count\", (int64_t)stats->max_crystal_count);\n    JSON_ObjectAppendInt64(\n        out, \"max_pickup_count\", (int64_t)stats->max_pickup_count);\n    JSON_ObjectAppendInt64(\n        out, \"max_secret_count\", (int64_t)stats->max_secret_count);\n    JSON_ObjectAppendInt64(out, \"all_secrets_mask\", stats->all_secrets_mask);\n\n    JSON_ARRAY *const secret_item_masks = JSON_ArrayNew();\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        JSON_OBJECT *const entry = JSON_ObjectNew();\n        JSON_ObjectAppendInt(\n            entry, \"item_num\", stats->secret_item_masks[i].item_num);\n        JSON_ObjectAppendInt64(\n            entry, \"secret_mask\",\n            (int64_t)stats->secret_item_masks[i].secret_mask);\n        JSON_ArrayAppendObject(secret_item_masks, entry);\n    }\n    JSON_ObjectAppendArray(out, \"secret_item_masks\", secret_item_masks);\n\n    JSON_ARRAY *const secret_objects = JSON_ArrayNew();\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        JSON_OBJECT *const entry = JSON_ObjectNew();\n        JSON_ObjectAppendBool(entry, \"taken\", stats->secret_objects[i].taken);\n        JSON_ObjectAppendInt(\n            entry, \"assigned_object_id\",\n            (int32_t)stats->secret_objects[i].assigned_object_id);\n        JSON_ObjectAppendInt(\n            entry, \"item_num\", stats->secret_objects[i].item_num);\n        JSON_ArrayAppendObject(secret_objects, entry);\n    }\n    JSON_ObjectAppendArray(out, \"secret_objects\", secret_objects);\n\n    return out;\n}\n\nstatic bool M_DeserializeLevelMaxStats(\n    LEVEL_MAX_STATS *const out, JSON_OBJECT *const obj)\n{\n    if (out == nullptr || obj == nullptr) {\n        return false;\n    }\n\n    out->max_pickup_secret_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_pickup_secret_count\", (int64_t)out->max_pickup_secret_count);\n    out->max_kill_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_kill_count\", (int64_t)out->max_kill_count);\n    out->max_kill_ally_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_kill_ally_count\", (int64_t)out->max_kill_ally_count);\n    out->max_kill_non_ally_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_kill_non_ally_count\", (int64_t)out->max_kill_non_ally_count);\n    out->max_crystal_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_crystal_count\", (int64_t)out->max_crystal_count);\n    out->max_pickup_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_pickup_count\", (int64_t)out->max_pickup_count);\n    out->max_secret_count = (size_t)JSON_ObjectGetInt64(\n        obj, \"max_secret_count\", (int64_t)out->max_secret_count);\n    out->all_secrets_mask = (uint32_t)JSON_ObjectGetInt64(\n        obj, \"all_secrets_mask\", out->all_secrets_mask);\n\n    JSON_ARRAY *const secret_item_masks =\n        JSON_ObjectGetArray(obj, \"secret_item_masks\");\n    if (secret_item_masks != nullptr\n        && secret_item_masks->length == (size_t)STATS_MAX_SECRETS) {\n        for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n            JSON_OBJECT *const entry =\n                JSON_ArrayGetObject(secret_item_masks, i);\n            if (entry == nullptr) {\n                continue;\n            }\n            out->secret_item_masks[i].item_num = JSON_ObjectGetInt(\n                entry, \"item_num\", out->secret_item_masks[i].item_num);\n            out->secret_item_masks[i].secret_mask =\n                (uint32_t)JSON_ObjectGetInt64(\n                    entry, \"secret_mask\",\n                    out->secret_item_masks[i].secret_mask);\n        }\n    }\n\n    JSON_ARRAY *const secret_objects =\n        JSON_ObjectGetArray(obj, \"secret_objects\");\n    if (secret_objects != nullptr\n        && secret_objects->length == (size_t)STATS_MAX_SECRETS) {\n        for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n            JSON_OBJECT *const entry = JSON_ArrayGetObject(secret_objects, i);\n            if (entry == nullptr) {\n                continue;\n            }\n            out->secret_objects[i].taken = JSON_ObjectGetBool(\n                entry, \"taken\", out->secret_objects[i].taken);\n            out->secret_objects[i].assigned_object_id =\n                (OBJECT_ID)JSON_ObjectGetInt(\n                    entry, \"assigned_object_id\",\n                    (int32_t)out->secret_objects[i].assigned_object_id);\n            out->secret_objects[i].item_num = JSON_ObjectGetInt(\n                entry, \"item_num\", out->secret_objects[i].item_num);\n        }\n    }\n\n    return true;\n}\n\nstatic bool M_TryLoadCache(\n    const uint64_t expected_checksum, const GF_LEVEL_TABLE *const level_table)\n{\n    JSON_VALUE *const root_value =\n        LevelCache_ReadJSON(M_CACHE_FILENAME, expected_checksum);\n    if (root_value == nullptr) {\n        return false;\n    }\n\n    JSON_OBJECT *const root = JSON_ValueAsObject(root_value);\n    if (root == nullptr) {\n        JSON_ValueFree(root_value);\n        return false;\n    }\n\n    const int32_t version = JSON_ObjectGetInt(root, \"version\", -1);\n    if (version != M_CACHE_VERSION) {\n        JSON_ValueFree(root_value);\n        return false;\n    }\n\n    const int32_t cached_level_count =\n        JSON_ObjectGetInt(root, \"level_count\", -1);\n    if (cached_level_count != level_table->count) {\n        JSON_ValueFree(root_value);\n        return false;\n    }\n\n    JSON_ARRAY *const levels = JSON_ObjectGetArray(root, \"levels\");\n    if (levels == nullptr) {\n        JSON_ValueFree(root_value);\n        return false;\n    }\n\n    // Clear any existing stats; cache may not cover every entry.\n    memset(m_Stats, 0, sizeof(LEVEL_MAX_STATS) * (size_t)m_StatsCapacity);\n\n    for (size_t i = 0; i < levels->length; i++) {\n        JSON_OBJECT *const entry = JSON_ArrayGetObject(levels, i);\n        if (entry == nullptr) {\n            continue;\n        }\n        const int32_t level_num = JSON_ObjectGetInt(entry, \"num\", -1);\n        if (level_num < 0 || level_num >= m_StatsCapacity) {\n            continue;\n        }\n        JSON_OBJECT *const stats_obj = JSON_ObjectGetObject(entry, \"stats\");\n        if (stats_obj == nullptr) {\n            continue;\n        }\n        M_DeserializeLevelMaxStats(&m_Stats[level_num], stats_obj);\n    }\n\n    JSON_ValueFree(root_value);\n    return true;\n}\n\nstatic void M_WriteCache(\n    const uint64_t checksum, const GF_LEVEL_TABLE *const level_table)\n{\n    JSON_OBJECT *const root = JSON_ObjectNew();\n    JSON_ObjectAppendInt(root, \"version\", M_CACHE_VERSION);\n    JSON_ObjectAppendInt(root, \"level_count\", level_table->count);\n\n    JSON_ARRAY *const levels = JSON_ArrayNew();\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        JSON_OBJECT *const entry = JSON_ObjectNew();\n        JSON_ObjectAppendInt(entry, \"num\", level->num);\n        JSON_ObjectAppendObject(\n            entry, \"stats\", M_SerializeLevelMaxStats(&m_Stats[level->num]));\n        JSON_ArrayAppendObject(levels, entry);\n    }\n    JSON_ObjectAppendArray(root, \"levels\", levels);\n\n    JSON_VALUE *const root_value = JSON_ValueFromObject(root);\n    LevelCache_WriteJSON(M_CACHE_FILENAME, checksum, root_value);\n    JSON_ValueFree(root_value);\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    if (m_Stats != nullptr) {\n        Memory_Free(m_Stats);\n        m_Stats = nullptr;\n    }\n    m_StatsCapacity = 0;\n}\n\nLEVEL_MAX_STATS *Stats_GetLevelMaxStats(const GF_LEVEL *const level)\n{\n    ASSERT(m_Stats != nullptr);\n    ASSERT(level != nullptr);\n    ASSERT(level->num >= 0 && level->num < m_StatsCapacity);\n    return &m_Stats[level->num];\n}\n\nbool Stats_GameHasCrystals(void)\n{\n    return m_GameHasCrystals;\n}\n\nvoid Stats_CalculateMaxStats(void)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    M_EnsureStatsStorage(level_table->count);\n    memset(m_Stats, 0, sizeof(LEVEL_MAX_STATS) * (size_t)m_StatsCapacity);\n    m_GameHasCrystals = false;\n\n    BENCHMARK benchmark = Benchmark_Start();\n    const uint64_t expected_checksum = M_ComputeInputsChecksum(level_table);\n    if (M_TryLoadCache(expected_checksum, level_table)) {\n        goto finish;\n    }\n\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        if (level->type != GFL_NORMAL && level->type != GFL_BONUS) {\n            continue;\n        }\n\n        VFILE *const file = VFile_CreateFromPath(level->path);\n        if (file == nullptr) {\n            continue;\n        }\n\n        const LEVEL_FORMAT_LOADER *const loader =\n            Level_Format_GuessLoader(file);\n        if (loader != nullptr) {\n            Level_Unload();\n            Creature_Reset();\n\n            Lua_ClearLevelListeners();\n            Lua_SetScriptContext(LUA_CONTEXT_LEVEL);\n            if (level->script_path != nullptr) {\n                LUA_RESULT res = Lua_EvalFile(level->script_path);\n                if (res.code != LUA_OK) {\n                    LOG_ERROR(\"Lua level script error: %s\", res.message);\n                }\n                Lua_FreeResult(&res);\n            }\n            Lua_SetScriptContext(LUA_CONTEXT_GLOBAL);\n            Lua_FireEventInt32(LUA_EVENT_BEFORE_LEVEL_FILE, level->num);\n\n            Inject_InitLevel(level, INJECTION_MODE_STATS);\n            if (loader->probe(loader, file, LEVEL_FORMAT_PROBE_STATS)) {\n                Inject_AllInjections();\n                M_SetupStatsFullInitObjects();\n\n                const int32_t item_count = Item_GetLevelCount();\n                for (int32_t item_num = 0; item_num < item_count; item_num++) {\n                    ITEM *const item = Item_Get(item_num);\n                    if (M_ShouldUseFullInitialisation(item->object_id)) {\n                        Item_Initialise(item_num);\n                    } else {\n                        ROOM *const room = Room_Get(item->room_num);\n                        item->next_item = room->item_num;\n                        room->item_num = item_num;\n                    }\n                }\n                Carrier_InitialiseLevel(level);\n\n                Stats_ScanLevel(level);\n            }\n            Inject_Cleanup();\n        }\n\n        GameBuf_Reset();\n        VFile_Close(file);\n\n#if 0\n        const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n        LOG_INFO(\n            \"Level %d (%s)\", GF_GetLevelOrdinalNumber(GFLT_MAIN, level),\n            level->title);\n        LOG_INFO(\"    pickups:   %d\", max_stats->max_pickup_count);\n        LOG_INFO(\"    kills:     %d\", max_stats->max_kill_count);\n        LOG_INFO(\"      allies:  %d\", max_stats->max_kill_ally_count);\n        LOG_INFO(\"      enemies: %d\", max_stats->max_kill_non_ally_count);\n        LOG_INFO(\"    crystals:  %d\", max_stats->max_crystal_count);\n        LOG_INFO(\"    secrets:   %d\", max_stats->max_secret_count);\n#endif\n    }\n\n    M_WriteCache(expected_checksum, level_table);\n\nfinish:\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i);\n        const LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n        if (max_stats->max_crystal_count != 0) {\n            m_GameHasCrystals = true;\n            break;\n        }\n    }\n\n    const FINAL_STATS final_stats = Stats_ComputeFinalStats(true);\n    LOG_INFO(\"Max pickups: %d\", final_stats.max_stats.max_pickup_count);\n    LOG_INFO(\"Max kills:   %d\", final_stats.max_stats.max_kill_count);\n    LOG_INFO(\"  allies:    %d\", final_stats.max_stats.max_kill_ally_count);\n    LOG_INFO(\"  enemies:   %d\", final_stats.max_stats.max_kill_non_ally_count);\n    LOG_INFO(\"Max crystals: %d\", final_stats.max_stats.max_crystal_count);\n    LOG_INFO(\"Max secrets: %d\", final_stats.max_stats.max_secret_count);\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/stats/init.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/stats/types.h>\n\nLEVEL_MAX_STATS *Stats_GetLevelMaxStats(const GF_LEVEL *level);\nbool Stats_GameHasCrystals(void);\nvoid Stats_CalculateMaxStats(void);\n"
  },
  {
    "path": "src/trx/game/stats/scan.c",
    "content": "#include <trx/core/benchmark.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/creature.h>\n#include <trx/game/game_buf.h>\n#include <trx/game/inject.h>\n#include <trx/game/items.h>\n#include <trx/game/items/carrier.h>\n#include <trx/game/level.h>\n#include <trx/game/objects.h>\n#include <trx/game/objects/creatures/pod.h>\n#include <trx/game/rooms.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/version.h>\n\n#include <string.h>\n\nstatic bool m_KillableItems[MAX_ITEMS] = {};\n\nstatic void M_IncludeKillableItem(\n    LEVEL_MAX_STATS *const stats, int16_t item_num)\n{\n    m_KillableItems[item_num] = true;\n    const ITEM *const item = Item_Get(item_num);\n    const bool is_ally = Creature_IsAlly(item);\n    if (is_ally) {\n        stats->max_kill_ally_count++;\n    } else {\n        stats->max_kill_non_ally_count++;\n    }\n    LOG_TRACE(\n        \"Killable item %d: object = %s\", item_num,\n        Object_GetName(item->object_id));\n    if (Carrier_GetItemCount(item_num) > 0) {\n        LOG_TRACE(\n            \"+%d pickups from carrier %d\", Carrier_GetItemCount(item_num),\n            item_num);\n        stats->max_pickup_count += Carrier_GetItemCount(item_num);\n    }\n}\n\nstatic uint32_t M_ReserveSecretConcreteBit(\n    LEVEL_MAX_STATS *const stats, const OBJECT_ID object_id,\n    const int32_t position)\n{\n    LOG_TRACE(\"Reserving bit %d for secret %d\", position, object_id);\n    if (position < 0 || position >= STATS_MAX_SECRETS) {\n        LOG_ERROR(\n            \"Invalid secret bit %d (max: %d)\", position, STATS_MAX_SECRETS);\n        return 0;\n    }\n    const uint32_t secret_bit = 1 << position;\n    if (!(stats->all_secrets_mask & secret_bit)) {\n        stats->all_secrets_mask |= secret_bit;\n        stats->max_secret_count++;\n        if (object_id != NO_OBJECT) {\n            stats->max_pickup_secret_count++;\n        }\n    }\n    stats->secret_objects[position].assigned_object_id = object_id;\n    stats->secret_objects[position].item_num = NO_ITEM;\n    stats->secret_objects[position].taken = true;\n    return secret_bit;\n}\n\nstatic uint32_t M_ReserveSecretUnusedBit(\n    LEVEL_MAX_STATS *const stats, const OBJECT_ID object_id)\n{\n    // Find unused bit\n    int32_t position = 0;\n    uint32_t n = stats->all_secrets_mask;\n    while ((n & 1) == 1) {\n        n >>= 1;\n        position++;\n    }\n    return M_ReserveSecretConcreteBit(stats, object_id, position);\n}\n\nstatic void M_CheckTriggers(\n    LEVEL_MAX_STATS *const stats, const ROOM *const room,\n    const int32_t room_num, const int32_t z_sector, const int32_t x_sector)\n{\n    if (z_sector == 0 || z_sector == room->size.z - 1) {\n        if (x_sector == 0 || x_sector == room->size.x - 1) {\n            return;\n        }\n    }\n    const SECTOR *const sector = Room_GetUnitSector(room, x_sector, z_sector);\n\n    if (sector->trigger == nullptr) {\n        return;\n    }\n\n    const TRIGGER_CMD *cmd = sector->trigger->command;\n    for (; cmd != nullptr; cmd = cmd->next_cmd) {\n        if (cmd->type == TO_SECRET) {\n            const uint16_t secret_num = (uint16_t)(intptr_t)cmd->parameter;\n            M_ReserveSecretConcreteBit(stats, NO_OBJECT, secret_num);\n        } else if (cmd->type == TO_OBJECT) {\n            const int16_t item_num = (int16_t)(intptr_t)cmd->parameter;\n            if (m_KillableItems[item_num]) {\n                continue;\n            }\n\n            const ITEM *const item = Item_Get(item_num);\n            switch (item->object_id) {\n            case O_RAPTOR_EMITTER:\n            case O_WASP_MUTANT_EMITTER:\n                for (int32_t i = 0; i < sector->trigger->timer; i++) {\n                    M_IncludeKillableItem(stats, item_num);\n                }\n                break;\n\n            case O_PIERRE:\n                // Add Pierre pickup and kills if oneshot\n                if (sector->trigger->one_shot) {\n                    M_IncludeKillableItem(stats, item_num);\n                }\n                break;\n\n            case O_PODS:\n            case O_BIG_POD:\n                // Check for only valid pods\n                const OBJECT_ID object_id = Pod_GetBugObjectID(item);\n                if (Object_Get(object_id)->loaded) {\n                    M_IncludeKillableItem(stats, item_num);\n                }\n                break;\n\n            case O_BARTOLI:\n            case O_DRAGON_BACK:\n            case O_DRAGON_FRONT:\n                if (Object_Get(O_DRAGON_BACK)->loaded\n                    && Object_Get(O_DRAGON_FRONT)->loaded) {\n                    M_IncludeKillableItem(stats, item_num);\n                    if (Object_Get(O_PUZZLE_OPTION_2)->loaded\n                        || Object_Get(O_PUZZLE_ITEM_2)->loaded) {\n                        LOG_TRACE(\"+1 pickup from dragon\");\n                        stats->max_pickup_count++;\n                    }\n                }\n                break;\n\n            case O_EEL:\n            case O_BIG_EEL:\n            case O_ORCA:\n                break;\n\n            case O_SCION_ITEM_3:\n                M_IncludeKillableItem(stats, item_num);\n                break;\n\n            default:\n                // Add killable if object triggered\n                if (Creature_IsHostile(item) || Creature_IsAlly(item)\n                    || Creature_IsAllyTargetingEnemy(item)) {\n                    M_IncludeKillableItem(stats, item_num);\n                }\n                break;\n            }\n        }\n    }\n}\n\nstatic void M_TraverseFloor(LEVEL_MAX_STATS *const stats)\n{\n    for (int32_t i = 0; i < Room_GetCount(); i++) {\n        const ROOM *const room = Room_Get(i);\n        for (int32_t z_sector = 0; z_sector < room->size.z; z_sector++) {\n            for (int32_t x_sector = 0; x_sector < room->size.x; x_sector++) {\n                M_CheckTriggers(stats, room, i, z_sector, x_sector);\n            }\n        }\n    }\n}\n\nstatic void M_CalculateStats(LEVEL_MAX_STATS *const stats)\n{\n    memset(stats, 0, sizeof(*stats));\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        stats->secret_item_masks[i].item_num = NO_ITEM;\n        stats->secret_item_masks[i].secret_mask = 0;\n\n        stats->secret_objects[i].assigned_object_id = NO_OBJECT;\n        stats->secret_objects[i].item_num = NO_ITEM;\n        stats->secret_objects[i].taken = false;\n    }\n\n    memset(&m_KillableItems, 0, sizeof(m_KillableItems));\n\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        const ITEM *const item = Item_Get(i);\n        if (Object_IsType(item->object_id, g_PickupObjects)\n            && !Carrier_IsItemCarried(i)) {\n            LOG_TRACE(\n                \"+1 pickup from pickup item %d in room %d\", i, item->room_num);\n            stats->max_pickup_count++;\n        } else if (item->object_id == O_SAVE_CRYSTAL_ITEM) {\n            LOG_TRACE(\n                \"+1 crystal from save crystal item %d in room %d\", i,\n                item->room_num);\n            stats->max_crystal_count++;\n        }\n    }\n\n    // Check triggers for special pickups / killables\n    M_TraverseFloor(stats);\n\n    for (int32_t i = 0; i < Item_GetTotalCount(); i++) {\n        ITEM *const item = Item_Get(i);\n        if (item->object_id < O_FIRST || item->object_id >= O_NUMBER_OF) {\n            LOG_ERROR(\"Bad Object number (%d) on Item %d\", item->object_id, i);\n            continue;\n        }\n\n        if (item->object_id == O_COMBAT_END) {\n            M_IncludeKillableItem(stats, i);\n        }\n\n        if (Object_IsType(item->object_id, g_SecretObjects)) {\n            int32_t position = -1;\n            for (int32_t j = 0; j < STATS_MAX_SECRETS; j++) {\n                if (!stats->secret_objects[j].taken) {\n                    position = j;\n                    break;\n                }\n            }\n            if (position == -1) {\n                LOG_ERROR(\"Too many secrets, max %d\", STATS_MAX_SECRETS);\n                break;\n            }\n            stats->secret_objects[position].assigned_object_id =\n                item->object_id;\n            stats->secret_objects[position].item_num = i;\n            stats->secret_objects[position].taken = true;\n            position++;\n        }\n    }\n\n    // Sorts secret objects by their type so that the dragons line up nicely\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        if (stats->secret_objects[i].assigned_object_id == NO_OBJECT) {\n            continue;\n        }\n        for (int32_t j = i + 1; j < STATS_MAX_SECRETS; j++) {\n            if (stats->secret_objects[j].assigned_object_id == NO_OBJECT) {\n                continue;\n            }\n            if (stats->secret_objects[i].assigned_object_id\n                > stats->secret_objects[j].assigned_object_id) {\n                SWAP(stats->secret_objects[i], stats->secret_objects[j]);\n            }\n        }\n    }\n\n    // Assign secret items their bits so they know which secret to set on\n    // pickup. NOTE: Do not persist runtime pickup state here. This scan runs\n    // at game launch to compute max stats; gameplay level loads restore secret\n    // masks using cached info in LEVEL_MAX_STATS.\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        const int32_t item_num = stats->secret_objects[i].item_num;\n        if (item_num == NO_ITEM) {\n            continue;\n        }\n\n        const uint32_t secret_mask = M_ReserveSecretUnusedBit(\n            stats, stats->secret_objects[i].assigned_object_id);\n\n        for (int32_t j = 0; j < STATS_MAX_SECRETS; j++) {\n            if (stats->secret_item_masks[j].item_num == NO_ITEM) {\n                stats->secret_item_masks[j].item_num = item_num;\n                stats->secret_item_masks[j].secret_mask = secret_mask;\n                break;\n            }\n        }\n    }\n}\n\nvoid Stats_ScanLevel(const GF_LEVEL *const level)\n{\n    ASSERT(level != nullptr);\n    BENCHMARK benchmark = Benchmark_Start();\n    LEVEL_MAX_STATS *const max_stats = Stats_GetLevelMaxStats(level);\n    M_CalculateStats(max_stats);\n    max_stats->max_pickup_count += GF_GetSecretRewardCount(level);\n    max_stats->max_pickup_count -= level->unobtainable.pickups;\n    max_stats->max_secret_count -= level->unobtainable.secrets;\n    max_stats->max_kill_ally_count -= level->unobtainable.ally_kills;\n    max_stats->max_kill_non_ally_count -= level->unobtainable.kills;\n    max_stats->max_kill_count =\n        max_stats->max_kill_non_ally_count + max_stats->max_kill_ally_count;\n    Benchmark_End(&benchmark, nullptr);\n}\n"
  },
  {
    "path": "src/trx/game/stats/scan.h",
    "content": "#pragma once\n\n#include <trx/game/game_flow/types.h>\n#include <trx/game/stats/types.h>\n\nvoid Stats_ScanLevel(const GF_LEVEL *level);\n"
  },
  {
    "path": "src/trx/game/stats/types.h",
    "content": "#pragma once\n\n#include <trx/game/objects/ids.h>\n#include <trx/game/stats/const.h>\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef struct {\n    size_t max_pickup_secret_count;\n    size_t max_kill_count;\n    size_t max_kill_ally_count;\n    size_t max_kill_non_ally_count;\n    size_t max_crystal_count;\n    size_t max_pickup_count;\n    size_t max_secret_count;\n    uint32_t all_secrets_mask;\n\n    struct {\n        int32_t item_num;\n        uint32_t secret_mask;\n    } secret_item_masks[STATS_MAX_SECRETS];\n\n    struct {\n        bool taken;\n        OBJECT_ID assigned_object_id;\n        int32_t item_num;\n    } secret_objects[STATS_MAX_SECRETS];\n} LEVEL_MAX_STATS;\n\ntypedef struct STATS_COMMON {\n    uint32_t timer;\n    uint32_t kill_count;\n    uint32_t ammo_used;\n    uint32_t ammo_hits;\n    uint32_t distance_travelled;\n    double medipacks_used;\n    uint16_t crystal_count;\n    uint16_t pickup_count;\n    int32_t death_count;\n    uint16_t secrets_mask;\n    uint16_t secret_count;\n} STATS_COMMON;\n\ntypedef struct {\n    struct STATS_COMMON;\n    uint16_t secret_flags;\n} LEVEL_STATS;\n\ntypedef struct {\n    STATS_COMMON stats;\n    LEVEL_MAX_STATS max_stats;\n} FINAL_STATS;\n"
  },
  {
    "path": "src/trx/game/stats.h",
    "content": "#pragma once\n\n#include <trx/game/stats/common.h>\n#include <trx/game/stats/const.h>\n#include <trx/game/stats/init.h>\n#include <trx/game/stats/scan.h>\n#include <trx/game/stats/types.h>\n"
  },
  {
    "path": "src/trx/game/types.h",
    "content": "#pragma once\n\n#include <trx/core/math.h>\n#include <trx/game/items/types.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    union {\n        struct {\n            int32_t x;\n            int32_t y;\n            int32_t z;\n        };\n        XYZ_32 pos;\n    };\n    int16_t room_num;\n} GAME_VECTOR;\n\ntypedef struct {\n    union {\n        struct {\n            int32_t x;\n            int32_t y;\n            int32_t z;\n        };\n        XYZ_32 pos;\n    };\n    int16_t data;\n    int16_t flags;\n} OBJECT_VECTOR;\n\ntypedef struct {\n    int32_t vertex_count;\n\n    union {\n        uint16_t texture_idx;\n        uint16_t palette_idx;\n    };\n    uint16_t vertices[4];\n\n    // trapezoid ratios for textured quads\n    // that cannot be really shared between vertices\n    TEXTURE_ZW_F texture_zw[4];\n\n    bool double_sided;\n    bool enable_reflections;\n} FACE;\n"
  },
  {
    "path": "src/trx/game/ui/common.c",
    "content": "#include <trx/game/ui/common.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/game/console/common.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/events.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/ui/text.h>\n#include <trx/game/viewport.h>\n\n#include <SDL2/SDL.h>\n#include <string.h>\n\nstatic struct {\n    MEMORY_ARENA_ALLOCATOR alloc;\n    UI_NODE *root; // The top-level container\n    UI_NODE *current; // The current container into which we attach nodes\n} m_Priv = {\n    .alloc = {\n        .default_chunk_size = 1024 * 4,\n    },\n};\n\nextern void UI_ClearDraw(void);\n\nstatic UI_INPUT M_TranslateInput(const uint32_t system_keycode)\n{\n    // clang-format off\n    switch (system_keycode) {\n    case SDLK_UP:        return UI_KEY_UP;\n    case SDLK_DOWN:      return UI_KEY_DOWN;\n    case SDLK_LEFT:      return UI_KEY_LEFT;\n    case SDLK_RIGHT:     return UI_KEY_RIGHT;\n    case SDLK_HOME:      return UI_KEY_HOME;\n    case SDLK_END:       return UI_KEY_END;\n    case SDLK_BACKSPACE: return UI_KEY_BACK;\n    case SDLK_RETURN:    return UI_KEY_RETURN;\n    case SDLK_ESCAPE:    return UI_KEY_ESCAPE;\n    }\n    // clang-format on\n    return -1;\n}\n\n// Depth-first measure pass\nstatic void M_MeasureNode(UI_NODE *const node)\n{\n    if (node == nullptr || node->ops.measure == nullptr) {\n        return;\n    }\n\n    // Recurse to children\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        M_MeasureNode(child);\n        child = child->next_sibling;\n    }\n\n    node->ops.measure(node);\n}\n\n// Depth-first layout pass\nstatic void M_LayoutNode(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    if (node == nullptr || node->ops.layout == nullptr) {\n        return;\n    }\n\n    node->ops.layout(node, x, y, w, h);\n    // Recursing to children is a responsibility of the layout function.\n}\n\n// Depth-first draw pass\nstatic void M_DrawNode(const UI_NODE *const node)\n{\n    if (node == nullptr || node->ops.draw == nullptr) {\n        return;\n    }\n\n    node->ops.draw(node);\n    // Recursing to children is a responsibility of the draw function.\n}\n\n// Allocate a new node\nUI_NODE *UI_AllocNode(\n    const UI_WIDGET_OPS *const ops, const size_t additional_size)\n{\n    const size_t size =\n        Memory_Align(sizeof(UI_NODE)) + Memory_Align(additional_size);\n    UI_NODE *const node = Memory_ArenaAlloc(&m_Priv.alloc, size);\n    memset(node, 0, size);\n    node->ops = *ops;\n    node->data = (char *)node + Memory_Align(sizeof(UI_NODE));\n    return node;\n}\n\n// Attach child to parent's child list\nvoid UI_AddChild(UI_NODE *const child)\n{\n    // Special case - the root widget\n    if (m_Priv.root == nullptr) {\n        m_Priv.root = child;\n        return;\n    }\n\n    UI_NODE *const parent = m_Priv.current;\n    if (parent == nullptr || child == nullptr) {\n        return;\n    }\n    child->parent = parent;\n    if (parent->first_child == nullptr) {\n        parent->first_child = child;\n    } else {\n        parent->last_child->next_sibling = child;\n    }\n    parent->last_child = child;\n}\n\nvoid UI_PushCurrent(UI_NODE *const child)\n{\n    m_Priv.current = child;\n}\n\nvoid UI_PopCurrent(void)\n{\n    ASSERT(m_Priv.current != nullptr);\n    m_Priv.current = m_Priv.current->parent;\n    if (m_Priv.current == nullptr) {\n        m_Priv.root = nullptr;\n    }\n}\n\nconst UI_NODE *UI_GetCurrent(void)\n{\n    return m_Priv.current;\n}\n\n// Scene management\nvoid UI_BeginScene(void)\n{\n    UI_ClearDraw();\n    Memory_ArenaReset(&m_Priv.alloc);\n    UI_BeginAnchor(0.5f, 0.5f); // Make a root node.\n}\n\nvoid UI_EndScene(void)\n{\n    M_MeasureNode(m_Priv.root);\n    M_LayoutNode(m_Priv.root, 0, 0, UI_GetCanvasWidth(), UI_GetCanvasHeight());\n    M_DrawNode(m_Priv.root);\n    UI_EndAnchor();\n    ASSERT(m_Priv.root == nullptr);\n}\n\nvoid UI_Init(void)\n{\n    UI_InitEvents();\n    UI_InitText();\n    UI_InitDraw();\n}\n\nvoid UI_Shutdown(void)\n{\n    UI_ShutdownDraw();\n    UI_ShutdownText();\n    Memory_ArenaFree(&m_Priv.alloc);\n    UI_ShutdownEvents();\n}\n\nvoid UI_ToggleState(bool *const config_setting)\n{\n    *config_setting ^= true;\n    Config_Update();\n    Console_Log(\n        *config_setting ? GS(\"general/osd/ui_on\") : GS(\"general/osd/ui_off\"));\n}\n\nvoid UI_HandleKeyDown(const uint32_t key)\n{\n    UI_FireEvent((EVENT) {\n        .name = \"key_down\",\n        .sender = nullptr,\n        .data = (void *)M_TranslateInput(key),\n    });\n}\n\nvoid UI_HandleKeyUp(const uint32_t key)\n{\n    UI_FireEvent((EVENT) {\n        .name = \"key_up\",\n        .sender = nullptr,\n        .data = (void *)M_TranslateInput(key),\n    });\n}\n\nvoid UI_HandleTextEdit(const char *const text)\n{\n    UI_FireEvent((EVENT) {\n        .name = \"text_edit\", .sender = nullptr, .data = (void *)text });\n}\n\nint32_t UI_GetCanvasWidth(void)\n{\n    return UI_Scaler_CalcInverse(\n        Viewport_GetWidth(VIEWPORT_UI), UI_SCALER_TARGET_GENERIC);\n}\n\nint32_t UI_GetCanvasHeight(void)\n{\n    return UI_Scaler_CalcInverse(\n        Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_GENERIC);\n}\n\nfloat UI_ScaleX(const float x)\n{\n    return UI_Scaler_Calc(x * 0x10000, UI_SCALER_TARGET_GENERIC) / 0x10000.p0;\n}\n\nfloat UI_ScaleY(const float y)\n{\n    return UI_Scaler_Calc(y * 0x10000, UI_SCALER_TARGET_GENERIC) / 0x10000.p0;\n}\n"
  },
  {
    "path": "src/trx/game/ui/common.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\ntypedef enum {\n    UI_KEY_UP,\n    UI_KEY_DOWN,\n    UI_KEY_LEFT,\n    UI_KEY_RIGHT,\n    UI_KEY_HOME,\n    UI_KEY_END,\n    UI_KEY_BACK,\n    UI_KEY_RETURN,\n    UI_KEY_ESCAPE,\n} UI_INPUT;\n\n// Forward declaration of the node and its vtable.\nstruct UI_NODE;\ntypedef struct {\n    void (*measure)(struct UI_NODE *node);\n    void (*layout)(struct UI_NODE *node, float x, float y, float w, float h);\n    void (*draw)(const struct UI_NODE *node);\n} UI_WIDGET_OPS;\n\n// Node structure that forms the UI tree\ntypedef struct UI_NODE {\n    // Common operations on a widget\n    UI_WIDGET_OPS ops;\n\n    // Final layout rectangle\n    float x;\n    float y;\n    float w;\n    float h;\n\n    // Needed size from measure pass\n    float measure_w;\n    float measure_h;\n\n    // Link to parent and siblings to form a tree\n    struct UI_NODE *parent;\n    struct UI_NODE *first_child;\n    struct UI_NODE *last_child;\n    struct UI_NODE *next_sibling;\n\n    // Widget-specific data\n    void *data;\n} UI_NODE;\n\n// Dimensions in virtual pixels of the screen area\n// (640x480 for any 4:3 resolution on 1.00 text scaling)\nint32_t UI_GetCanvasWidth(void);\nint32_t UI_GetCanvasHeight(void);\nfloat UI_ScaleX(float x);\nfloat UI_ScaleY(float y);\n\n// Public API for scene management\nvoid UI_BeginScene(void);\nvoid UI_EndScene(void);\n\n// Helpers to add children, etc.\nUI_NODE *UI_AllocNode(const UI_WIDGET_OPS *ops, size_t additional_size);\nvoid UI_AddChild(UI_NODE *child);\nvoid UI_PushCurrent(UI_NODE *child);\nvoid UI_PopCurrent(void);\nconst UI_NODE *UI_GetCurrent(void);\n\nvoid UI_Init(void);\nvoid UI_Shutdown(void);\nvoid UI_ToggleState(bool *config_setting);\n\nvoid UI_HandleKeyDown(uint32_t key);\nvoid UI_HandleKeyUp(uint32_t key);\nvoid UI_HandleTextEdit(const char *text);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/base_passport.c",
    "content": "#include <trx/game/ui/dialogs/base_passport.h>\n\n#include <trx/game/inventory.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/resize.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\nstatic int32_t M_GetVisibleRows(void)\n{\n    if (g_TRVersion >= 2) {\n        return 10;\n    } else {\n        const int32_t res_h = UI_Scaler_CalcInverse(\n            Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT);\n        if (res_h <= 240) {\n            return 5;\n        } else if (res_h <= 384) {\n            return 7;\n        } else if (res_h <= 480) {\n            return 10;\n        } else {\n            return 12;\n        }\n    }\n}\n\nvoid UI_BasePassportDialog_Init(\n    UI_REQUESTER_STATE *const req, const size_t max_rows)\n{\n    UI_Requester_Init(req, M_GetVisibleRows(), max_rows, true);\n    req->row_pad = 4.0f;\n    req->row_spacing = g_TRVersion == 1 ? 2.0f : 3.0f;\n    req->show_arrows = g_TRVersion == 1;\n    req->reserve_space = true;\n}\n\nvoid UI_BasePassportDialog_Control(UI_REQUESTER_STATE *const req)\n{\n    UI_Requester_SetVisibleRows(req, M_GetVisibleRows());\n}\n\nvoid UI_BeginBasePassportDialog(void)\n{\n    const float modal_y = g_Inv_Mode == INV_TITLE_MODE ? 0.81f : 0.62f;\n    UI_BeginModal(0.5f, modal_y);\n    UI_BeginResize(300.0f, -1.0f);\n}\n\nvoid UI_EndBasePassportDialog(void)\n{\n    UI_EndResize();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/base_passport.h",
    "content": "// Base passport dialog functions.\n// Does not implement a function on its own, and is used mostly for placement\n// and sizing of the larger dialogs such as load/save game.\n\n#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/requester.h>\n\n// state functions\nvoid UI_BasePassportDialog_Init(UI_REQUESTER_STATE *req, size_t max_rows);\nvoid UI_BasePassportDialog_Control(UI_REQUESTER_STATE *req);\n\n// draw functions\nvoid UI_BeginBasePassportDialog(void);\nvoid UI_EndBasePassportDialog(void);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/color_editor.c",
    "content": "#include <trx/game/ui/dialogs/color_editor.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/input.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/elements/color_swatch.h>\n#include <trx/game/ui/elements/gradient_slider.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/version.h>\n\n#define M_COLOR_EDITOR_PADDING 8.0f\n#define M_COLOR_EDITOR_TITLE_MARGIN 5.0f\n#define M_MAX_STOPS 7\n#define M_OKLCH_MAX_CHROMA 0.4f\n\ntypedef enum {\n    M_COLOR_ROW_HUE,\n    M_COLOR_ROW_CHROMA,\n    M_COLOR_ROW_LIGHTNESS,\n    M_COLOR_ROW_COUNT,\n} M_COLOR_ROW;\n\ntypedef struct {\n    float h;\n    float c;\n    float l;\n    bool use_state_h;\n    bool use_state_c;\n    bool use_state_l;\n} M_STOP_DEF;\n\ntypedef struct {\n    GAME_STRING_ID label_id;\n    int32_t stop_count;\n    M_STOP_DEF stops[M_MAX_STOPS];\n} M_ROW_DEF;\n\nstruct UI_COLOR_EDITOR_DIALOG_STATE {\n    bool show;\n    const UI_SETTINGS_OPTION *option;\n    M_COLOR_ROW component_idx;\n    float h;\n    float c;\n    float l;\n    RGB_888 color;\n    RGB_888 cached_stops[M_COLOR_ROW_COUNT][M_MAX_STOPS];\n};\n\nstatic M_ROW_DEF m_RowDefs[M_COLOR_ROW_COUNT];\n\n__attribute__((constructor)) static void M_Init(void)\n{\n    m_RowDefs[M_COLOR_ROW_HUE] = (M_ROW_DEF) {\n        .label_id = GS_ID(\"general/settings/common/hue\"),\n        .stop_count = 7,\n        .stops = {\n            { .h = 0.0f, .use_state_c = true, .use_state_l = true },\n            { .h = 60.0f, .use_state_c = true, .use_state_l = true },\n            { .h = 120.0f, .use_state_c = true, .use_state_l = true },\n            { .h = 180.0f, .use_state_c = true, .use_state_l = true },\n            { .h = 240.0f, .use_state_c = true, .use_state_l = true },\n            { .h = 300.0f, .use_state_c = true, .use_state_l = true },\n            { .h = 360.0f, .use_state_c = true, .use_state_l = true },\n        },\n    };\n\n    m_RowDefs[M_COLOR_ROW_CHROMA] = (M_ROW_DEF) {\n        .label_id = GS_ID(\"general/settings/common/chroma\"),\n        .stop_count = 2,\n        .stops = {\n            { .c = 0.0f, .use_state_h = true, .use_state_l = true },\n            { .c = M_OKLCH_MAX_CHROMA, .use_state_h = true, .use_state_l = true },\n        },\n    };\n\n    m_RowDefs[M_COLOR_ROW_LIGHTNESS] = (M_ROW_DEF) {\n        .label_id = GS_ID(\"general/settings/common/lightness\"),\n        .stop_count = 3,\n        .stops = {\n            { .l = 0.0f, .use_state_h = true, .use_state_c = true },\n            { .l = 0.5f, .use_state_h = true, .use_state_c = true },\n            { .l = 1.0f, .use_state_h = true, .use_state_c = true },\n        },\n    };\n}\n\nstatic float M_GetSliderValue(\n    const UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_COLOR_ROW row)\n{\n    switch (row) {\n    case M_COLOR_ROW_HUE:\n        return s->h / 360.0f;\n    case M_COLOR_ROW_CHROMA:\n        return s->c / M_OKLCH_MAX_CHROMA;\n    case M_COLOR_ROW_LIGHTNESS:\n        return s->l;\n    case M_COLOR_ROW_COUNT:\n        break;\n    }\n\n    return 0.0f;\n}\n\nstatic RGB_888 M_GetStopColor(\n    const UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_STOP_DEF *const stop)\n{\n    const float h = stop->use_state_h ? s->h : stop->h;\n    const float c = stop->use_state_c ? s->c : stop->c;\n    const float l = stop->use_state_l ? s->l : stop->l;\n    return Color_OKLCHToRGB(l, c, h);\n}\n\nstatic void M_GetGradientStops(\n    const UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_COLOR_ROW row,\n    RGB_888 out_stops[M_MAX_STOPS])\n{\n    const M_ROW_DEF *const row_def = &m_RowDefs[row];\n    for (int32_t i = 0; i < row_def->stop_count; i++) {\n        out_stops[i] = M_GetStopColor(s, &row_def->stops[i]);\n    }\n}\n\nstatic void M_RebuildCache(UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    s->color = Color_OKLCHToRGB(s->l, s->c, s->h);\n    for (M_COLOR_ROW row = M_COLOR_ROW_HUE; row < M_COLOR_ROW_COUNT; row++) {\n        M_GetGradientStops(s, row, s->cached_stops[row]);\n    }\n}\n\nstatic void M_SetLocalColorFromRGB(\n    UI_COLOR_EDITOR_DIALOG_STATE *const s, const RGB_888 rgb)\n{\n    Color_RGBToOKLCH(rgb, &s->l, &s->c, &s->h);\n    CLAMP(s->c, 0.0f, M_OKLCH_MAX_CHROMA);\n    M_RebuildCache(s);\n}\n\nstatic void M_EmitLocalColorAsRGB(UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    M_RebuildCache(s);\n    *(RGB_888 *)s->option->target = s->color;\n    Config_Update();\n}\n\nstatic void M_ColorEditorRow(\n    UI_COLOR_EDITOR_DIALOG_STATE *const s, const M_COLOR_ROW row)\n{\n    const bool is_selected = s->component_idx == row;\n    const M_ROW_DEF *const row_def = &m_RowDefs[row];\n\n    if (is_selected) {\n        UI_BeginFrame(UI_FRAME_SELECTED_OPTION);\n    }\n\n    UI_BeginPad(g_TRVersion == 1 ? 1.0f : 0.0f, g_TRVersion == 1 ? 1.0f : 0.0f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = 1.0f },\n    });\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n    });\n    UI_Label(GameString_Get(row_def->label_id));\n    UI_BeginRowArrows(is_selected, is_selected, UI_ROW_ARROWS_MEDIUM);\n    UI_GradientSlider((UI_GRADIENT_SLIDER_SETTINGS) {\n        .width = 100.0f,\n        .value = M_GetSliderValue(s, row),\n        .stop_count = row_def->stop_count,\n        .stops = s->cached_stops[row],\n    });\n    UI_EndRowArrows();\n    UI_EndStack();\n\n    UI_EndStack();\n    UI_EndPad();\n\n    if (is_selected) {\n        UI_EndFrame();\n    }\n}\n\nUI_COLOR_EDITOR_DIALOG_STATE *UI_ColorEditorDialog_Init(void)\n{\n    UI_COLOR_EDITOR_DIALOG_STATE *const s = Memory_Alloc(sizeof(*s));\n    return s;\n}\n\nvoid UI_ColorEditorDialog_Free(UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    Memory_Free(s);\n}\n\nvoid UI_ColorEditorDialog_Open(\n    UI_COLOR_EDITOR_DIALOG_STATE *const s,\n    const UI_SETTINGS_OPTION *const option)\n{\n    ASSERT(s != nullptr);\n    ASSERT(option != nullptr);\n    ASSERT(Config_GetOption(option->target)->type == COT_RGB888);\n    const RGB_888 *const color = option->target;\n    s->show = true;\n    s->option = option;\n    s->component_idx = 0;\n    M_SetLocalColorFromRGB(s, *color);\n}\n\nvoid UI_ColorEditorDialog_Close(UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    if (s == nullptr) {\n        return;\n    }\n    s->show = false;\n    s->option = nullptr;\n    s->component_idx = 0;\n    s->h = 0.0f;\n    s->c = 0.0f;\n    s->l = 0.0f;\n}\n\nbool UI_ColorEditorDialog_IsOpen(const UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    return s != nullptr && s->show;\n}\n\nvoid UI_ColorEditorDialog_Control(UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    if (s == nullptr || !s->show) {\n        return;\n    }\n    const UI_SETTINGS_OPTION *const option = s->option;\n    if (option == nullptr) {\n        UI_ColorEditorDialog_Close(s);\n        return;\n    }\n    if (g_InputDB.menu_back || g_InputDB.look) {\n        UI_ColorEditorDialog_Close(s);\n        return;\n    }\n    if (g_InputDB.menu_up) {\n        int32_t next_idx = (int32_t)s->component_idx - 1;\n        if (next_idx < 0) {\n            next_idx = M_COLOR_ROW_COUNT - 1;\n        }\n        s->component_idx = (M_COLOR_ROW)next_idx;\n    } else if (g_InputDB.menu_down) {\n        int32_t next_idx = (int32_t)s->component_idx + 1;\n        if (next_idx >= M_COLOR_ROW_COUNT) {\n            next_idx = 0;\n        }\n        s->component_idx = (M_COLOR_ROW)next_idx;\n    } else if (g_InputDB.menu_left || g_InputDB.menu_right) {\n        int32_t delta = g_Input.slow ? option->delta_slow : option->delta_fast;\n        if (delta == 0) {\n            delta = 1;\n        }\n        if (g_InputDB.menu_left) {\n            delta = -delta;\n        }\n        if (s->component_idx == M_COLOR_ROW_HUE) {\n            s->h += delta;\n            while (s->h < 0.0f) {\n                s->h += 360.0f;\n            }\n            while (s->h > 360.0f) {\n                s->h -= 360.0f;\n            }\n        } else if (s->component_idx == M_COLOR_ROW_CHROMA) {\n            s->c += (delta / 100.0f) * M_OKLCH_MAX_CHROMA;\n            CLAMP(s->c, 0.0f, M_OKLCH_MAX_CHROMA);\n        } else {\n            s->l += delta / 100.0f;\n            CLAMP(s->l, 0.0f, 1.0f);\n        }\n        M_EmitLocalColorAsRGB(s);\n    } else if (g_InputDB.unbind_key) {\n        Config_RestoreOptionDefault(option->target);\n        M_SetLocalColorFromRGB(s, *(RGB_888 *)option->target);\n        Config_Update();\n    }\n}\n\nvoid UI_ColorEditorDialog(UI_COLOR_EDITOR_DIALOG_STATE *const s)\n{\n    if (s == nullptr || !s->show || s->option == nullptr) {\n        return;\n    }\n\n    UI_BeginModal(0.5f, 0.5f);\n    UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND_HEAVY);\n    UI_BeginPad(M_COLOR_EDITOR_PADDING, M_COLOR_EDITOR_PADDING);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .spacing = { .v = 5.0f },\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n    });\n    UI_BeginAnchor(0.5f, 0.5f);\n    const char *const title =\n        Config_GetOptionTitle(Config_GetOption(s->option->target));\n    UI_Label(title != nullptr ? title : \"\");\n    UI_EndAnchor();\n    UI_Spacer(M_COLOR_EDITOR_TITLE_MARGIN, M_COLOR_EDITOR_TITLE_MARGIN);\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = 4.0f },\n    });\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        .spacing = { .h = 10.0f },\n    });\n    UI_LabelFmt(\"#%02X%02X%02X\", s->color.r, s->color.g, s->color.b);\n    UI_BeginRowArrows(false, false, UI_ROW_ARROWS_MEDIUM);\n    UI_ColorSwatch((UI_COLOR_SWATCH_SETTINGS) {\n        .color = s->color,\n        .w = 48.0f,\n        .h = 12.0f,\n    });\n    UI_EndRowArrows();\n    UI_EndStack();\n\n    M_ColorEditorRow(s, M_COLOR_ROW_HUE);\n    M_ColorEditorRow(s, M_COLOR_ROW_CHROMA);\n    M_ColorEditorRow(s, M_COLOR_ROW_LIGHTNESS);\n\n    UI_EndStack();\n\n    UI_EndStack();\n    UI_EndPad();\n    UI_EndFrame();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/color_editor.h",
    "content": "#pragma once\n\n#include <trx/game/ui/dialogs/settings.h>\n\ntypedef struct UI_COLOR_EDITOR_DIALOG_STATE UI_COLOR_EDITOR_DIALOG_STATE;\n\nUI_COLOR_EDITOR_DIALOG_STATE *UI_ColorEditorDialog_Init(void);\nvoid UI_ColorEditorDialog_Free(UI_COLOR_EDITOR_DIALOG_STATE *s);\n\nvoid UI_ColorEditorDialog_Open(\n    UI_COLOR_EDITOR_DIALOG_STATE *s, const UI_SETTINGS_OPTION *option);\nvoid UI_ColorEditorDialog_Close(UI_COLOR_EDITOR_DIALOG_STATE *s);\nbool UI_ColorEditorDialog_IsOpen(const UI_COLOR_EDITOR_DIALOG_STATE *s);\n\nvoid UI_ColorEditorDialog_Control(UI_COLOR_EDITOR_DIALOG_STATE *s);\nvoid UI_ColorEditorDialog(UI_COLOR_EDITOR_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/config_presets.c",
    "content": "#include <trx/game/ui/dialogs/config_presets.h>\n\n#include <trx/config.h>\n#include <trx/config/common.h>\n#include <trx/config/presets.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/resize.h>\n#include <trx/game/ui/elements/scrollable_stack.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/elements/window.h>\n#include <trx/game/ui/scrollable.h>\n\n#include <string.h>\n\n#define M_VISIBLE_ROWS 8\n#define M_CONFIRM_VISIBLE_ROWS 10\n#define M_CONFIRM_DIALOG_W 72.0f\n#define M_LIST_ROW_SPACING 5.0f\n\ntypedef enum {\n    M_PHASE_BROWSE,\n    M_PHASE_CONFIRM,\n    M_PHASE_NO_CHANGES,\n    M_PHASE_APPLIED,\n} M_PHASE;\n\nstruct UI_CONFIG_PRESETS_STATE {\n    M_PHASE phase;\n    UI_REQUESTER_STATE req;\n    UI_SCROLLABLE confirm_scroll;\n    int32_t selected_idx;\n};\n\nstatic const char *M_GetPresetKeyLabel(const char *const key)\n{\n    const CONFIG_OPTION *const opt = Config_GetOptionByPath(key);\n    if (opt != nullptr) {\n        const char *const label = Config_GetOptionTitle(opt);\n        if (label != nullptr) {\n            return label;\n        }\n    }\n    return key;\n}\n\nstatic int32_t M_GetChangedSettingCount(const int32_t preset_idx)\n{\n    const CONFIG_PRESET *const preset = Config_Presets_Get(preset_idx);\n    if (preset == nullptr || preset->setting_count == 0) {\n        return 0;\n    }\n\n    int32_t changed_count = 0;\n    for (int32_t i = 0; i < preset->setting_count; i++) {\n        const CONFIG_OPTION *const opt =\n            Config_GetOptionByPath(preset->keys[i]);\n        if (opt == nullptr) {\n            continue;\n        }\n        const char *const current_value =\n            Config_GetOptionValueAsString(opt, false);\n        if (strcmp(current_value, preset->values[i]) != 0) {\n            changed_count++;\n        }\n    }\n    return changed_count;\n}\n\nstatic int32_t M_GetConfirmRowCount(const int32_t preset_idx)\n{\n    return M_GetChangedSettingCount(preset_idx) * 2;\n}\n\nstatic void M_DrawConfirmRows(UI_CONFIG_PRESETS_STATE *const s)\n{\n    const CONFIG_PRESET *const preset = Config_Presets_Get(s->selected_idx);\n    if (preset == nullptr) {\n        return;\n    }\n\n    for (int32_t i = 0; i < preset->setting_count; i++) {\n        const CONFIG_OPTION *const opt =\n            Config_GetOptionByPath(preset->keys[i]);\n        if (opt == nullptr) {\n            continue;\n        }\n        const char *const current_value_raw =\n            Config_GetOptionValueAsString(opt, false);\n        if (strcmp(current_value_raw, preset->values[i]) == 0) {\n            continue;\n        }\n        const char *const current_value =\n            Config_GetOptionValueAsString(opt, true);\n        char *const target_value =\n            Config_NormalizeOptionValueString(opt, preset->values[i], true);\n        UI_LabelFmt(\"%s\", M_GetPresetKeyLabel(preset->keys[i]));\n        UI_LabelFmt(\"  %s \\\\{button right} %s\", current_value, target_value);\n        Memory_Free(target_value);\n    }\n}\n\nstatic void M_Header(void *const user_data)\n{\n    UI_CONFIG_PRESETS_STATE *const s = user_data;\n    if (s->phase == M_PHASE_CONFIRM) {\n        UI_Label(GS(\"general/config_presets/confirm_description\"));\n        UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n    }\n}\n\nstatic void M_Footer(void *const user_data)\n{\n    UI_CONFIG_PRESETS_STATE *const s = user_data;\n    if (s->phase == M_PHASE_CONFIRM) {\n        UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n        UI_Label(GS(\"general/config_presets/confirm_restart_note\"));\n    }\n}\n\nUI_CONFIG_PRESETS_STATE *UI_ConfigPresets_Init(void)\n{\n    UI_CONFIG_PRESETS_STATE *const s =\n        Memory_Alloc(sizeof(UI_CONFIG_PRESETS_STATE));\n    s->phase = M_PHASE_BROWSE;\n    s->selected_idx = -1;\n    s->confirm_scroll = (UI_SCROLLABLE) {\n        .first_item = 0,\n        .sel_item = -1,\n        .vis_items = M_CONFIRM_VISIBLE_ROWS,\n        .max_items = 0,\n    };\n\n    const int32_t count = Config_Presets_GetCount();\n    UI_Requester_Init(&s->req, M_VISIBLE_ROWS, count, true);\n    UI_Requester_SelectRow(&s->req, -1);\n    return s;\n}\n\nvoid UI_ConfigPresets_Free(UI_CONFIG_PRESETS_STATE *const s)\n{\n    UI_Requester_Free(&s->req);\n    Memory_Free(s);\n}\n\nint32_t UI_ConfigPresets_GetItemCount(UI_CONFIG_PRESETS_STATE *const s)\n{\n    return Config_Presets_GetCount();\n}\n\nvoid UI_ConfigPresets_RecomputeSizes(\n    UI_CONFIG_PRESETS_STATE *const s, const int32_t visible_rows)\n{\n    int32_t clamped_rows = visible_rows;\n    if (clamped_rows < 0) {\n        clamped_rows = 0;\n    } else if (clamped_rows > M_VISIBLE_ROWS) {\n        clamped_rows = M_VISIBLE_ROWS;\n    }\n    UI_Requester_SetVisibleRows(&s->req, clamped_rows);\n}\n\nfloat UI_ConfigPresets_GetContentWidth(UI_CONFIG_PRESETS_STATE *const s)\n{\n    return -1.0f;\n}\n\nfloat UI_ConfigPresets_GetContentHeight(UI_CONFIG_PRESETS_STATE *const s)\n{\n    if (s == nullptr) {\n        return -1.0f;\n    }\n    int32_t rows = s->req.scroll.vis_items;\n    if (rows <= 0) {\n        return -1.0f;\n    }\n    return rows * UI_TEXT_HEIGHT + (rows - 1) * M_LIST_ROW_SPACING;\n}\n\nUI_SCROLLABLE *UI_ConfigPresets_GetScrollable(UI_CONFIG_PRESETS_STATE *const s)\n{\n    return &s->req.scroll;\n}\n\nbool UI_ConfigPresets_Control(UI_CONFIG_PRESETS_STATE *const s)\n{\n    if (s->phase == M_PHASE_APPLIED || s->phase == M_PHASE_NO_CHANGES) {\n        if (g_InputDB.menu_confirm || g_InputDB.menu_back) {\n            s->phase = M_PHASE_BROWSE;\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n        return false;\n    }\n\n    if (s->phase == M_PHASE_CONFIRM) {\n        UI_ScrollableStack_Control(&s->confirm_scroll, UI_STACK_VERTICAL);\n        if (g_InputDB.menu_confirm) {\n            Config_Presets_Apply(s->selected_idx);\n            s->phase = M_PHASE_APPLIED;\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        } else if (g_InputDB.menu_back) {\n            s->phase = M_PHASE_BROWSE;\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n        }\n        return false;\n    }\n\n    const int32_t count = Config_Presets_GetCount();\n    UI_Requester_SetMaxRows(&s->req, count);\n    if (count <= 0) {\n        return g_InputDB.menu_back || g_InputDB.menu_up\n            || (g_InputDB.menu_down && g_Config.ui.enable_wraparound);\n    }\n\n    if (g_InputDB.menu_up && UI_Requester_GetCurrentRow(&s->req) <= 0) {\n        return true;\n    }\n    if (g_InputDB.menu_down && g_Config.ui.enable_wraparound\n        && UI_Requester_GetCurrentRow(&s->req) >= count - 1) {\n        return true;\n    }\n\n    const int32_t choice = UI_Requester_Control(&s->req);\n    if (choice == UI_REQUESTER_CANCEL || g_InputDB.menu_back) {\n        return true;\n    }\n    if (choice >= 0 && choice < count) {\n        s->selected_idx = choice;\n        const int32_t row_count = M_GetConfirmRowCount(choice);\n        if (row_count > 0) {\n            s->confirm_scroll.first_item = 0;\n            s->confirm_scroll.sel_item = -1;\n            s->confirm_scroll.max_items = row_count;\n            s->phase = M_PHASE_CONFIRM;\n        } else {\n            s->phase = M_PHASE_NO_CHANGES;\n        }\n        g_Input = (INPUT_STATE) {};\n        g_InputDB = (INPUT_STATE) {};\n    }\n    return false;\n}\n\nvoid UI_ConfigPresets(UI_CONFIG_PRESETS_STATE *const s)\n{\n    const int32_t count = Config_Presets_GetCount();\n\n    UI_BeginResize(-1.0f, -1.0f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = M_LIST_ROW_SPACING },\n    });\n\n    const int32_t first_row = UI_Requester_GetFirstRow(&s->req);\n    for (int32_t i = 0; i < s->req.scroll.vis_items; i++) {\n        const int32_t row = first_row + i;\n        if (row < count) {\n            const CONFIG_PRESET *const preset = Config_Presets_Get(row);\n            UI_BeginRequesterRow(&s->req, row);\n            UI_BeginAnchor(0.5f, 0.5f);\n            UI_Label(preset != nullptr ? GameString_Get(preset->name_gs) : \"\");\n            UI_EndAnchor();\n            UI_EndRequesterRow(&s->req, row);\n        } else {\n            UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n        }\n    }\n\n    if (count <= 0) {\n        UI_BeginAnchor(0.5f, 0.5f);\n        UI_Label(GS(\"general/config_presets/empty\"));\n        UI_EndAnchor();\n    }\n\n    UI_EndStack();\n    UI_EndResize();\n}\n\nvoid UI_ConfigPresetsApplyModal(UI_CONFIG_PRESETS_STATE *const s)\n{\n    if (s->phase == M_PHASE_BROWSE) {\n        return;\n    }\n\n    const CONFIG_PRESET *const preset = Config_Presets_Get(s->selected_idx);\n    const char *const preset_name =\n        preset != nullptr ? GameString_Get(preset->name_gs) : \"\";\n    const char *const title = String_FormatStatic(\n        GS(\"general/config_presets/title_fmt\"), preset_name);\n\n    UI_BeginModal(0.5f, 0.5f);\n    UI_BeginPad(6.0f, 6.0f);\n    UI_BeginWindow((UI_WINDOW_SETTINGS) {\n        .title = title,\n        .scrollable =\n            s->phase == M_PHASE_CONFIRM ? &s->confirm_scroll : nullptr,\n        .title_spacing = -1.0f,\n        .header_func = M_Header,\n        .footer_func = M_Footer,\n        .user_data = s,\n        .heavy = true,\n        .reserve_scroll_space = true,\n    });\n\n    if (s->phase == M_PHASE_APPLIED) {\n        UI_BeginPad(6.0f, 6.0f);\n        UI_LabelFmt(\"%s\", GS(\"general/config_presets/applied\"));\n        UI_EndPad();\n    } else if (s->phase == M_PHASE_NO_CHANGES) {\n        UI_BeginPad(6.0f, 6.0f);\n        UI_LabelFmt(\"%s\", GS(\"general/config_presets/no_changes\"));\n        UI_EndPad();\n    } else if (s->phase == M_PHASE_CONFIRM) {\n        UI_BeginResize(M_CONFIRM_DIALOG_W, -1.0f);\n\n        UI_BeginScrollableStack(\n            &s->confirm_scroll,\n            (UI_SCROLLABLE_STACK_SETTINGS) {\n                .orientation = UI_STACK_VERTICAL,\n                .spacing = 2.0f,\n            });\n        M_DrawConfirmRows(s);\n        UI_EndScrollableStack();\n        UI_EndResize();\n    }\n\n    UI_EndWindow();\n    UI_EndPad();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/config_presets.h",
    "content": "#pragma once\n\n// A UI dialog for browsing and applying config presets.\n// Shows a scrollable list of presets. Selecting one prompts the user\n// with a list of settings that will change, then applies them on confirm.\n\n#include <trx/game/ui/scrollable.h>\n\ntypedef struct UI_CONFIG_PRESETS_STATE UI_CONFIG_PRESETS_STATE;\n\n// State functions\nUI_CONFIG_PRESETS_STATE *UI_ConfigPresets_Init(void);\nvoid UI_ConfigPresets_Free(UI_CONFIG_PRESETS_STATE *s);\nint32_t UI_ConfigPresets_GetItemCount(UI_CONFIG_PRESETS_STATE *s);\nvoid UI_ConfigPresets_RecomputeSizes(\n    UI_CONFIG_PRESETS_STATE *s, int32_t visible_rows);\nfloat UI_ConfigPresets_GetContentWidth(UI_CONFIG_PRESETS_STATE *s);\nfloat UI_ConfigPresets_GetContentHeight(UI_CONFIG_PRESETS_STATE *s);\n\n// Handle input. Returns true when the user wants to exit the dialog.\nbool UI_ConfigPresets_Control(UI_CONFIG_PRESETS_STATE *s);\nUI_SCROLLABLE *UI_ConfigPresets_GetScrollable(UI_CONFIG_PRESETS_STATE *s);\n\n// Draw functions\nvoid UI_ConfigPresets(UI_CONFIG_PRESETS_STATE *s);\nvoid UI_ConfigPresetsApplyModal(UI_CONFIG_PRESETS_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/controls.c",
    "content": "#include <trx/game/ui/dialogs/controls.h>\n\n#include <trx/config.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/dialogs/controls_backend.h>\n#include <trx/game/ui/dialogs/controls_editor.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef enum {\n    M_PHASE_BACKEND,\n    M_PHASE_EDITOR,\n} M_PHASE;\n\nvoid UI_Controls_Init(UI_CONTROLS_STATE *const s)\n{\n    s->events = EventManager_Create();\n    s->phase = M_PHASE_BACKEND;\n    s->backend = INPUT_BACKEND_KEYBOARD;\n    s->active_layout = g_Config.input.layout[s->backend];\n    UI_ControlsBackend_Init(&s->backend_state);\n    for (INPUT_BACKEND backend = 0; backend < INPUT_BACKEND_NUMBER_OF;\n         backend++) {\n        UI_ControlsEditor_Init(\n            &s->editor_state[backend], backend, g_Config.input.layout[backend],\n            s->events);\n    }\n}\n\nvoid UI_Controls_Free(UI_CONTROLS_STATE *const s)\n{\n    for (INPUT_BACKEND backend = 0; backend < INPUT_BACKEND_NUMBER_OF;\n         backend++) {\n        UI_ControlsEditor_Free(&s->editor_state[backend]);\n    }\n    UI_ControlsBackend_Free(&s->backend_state);\n    EventManager_Free(s->events);\n    s->events = nullptr;\n}\n\nbool UI_Controls_Control(UI_CONTROLS_STATE *const s)\n{\n    switch (s->phase) {\n    case M_PHASE_BACKEND: {\n        const int32_t choice = UI_ControlsBackend_Control(&s->backend_state);\n        switch (choice) {\n        case UI_REQUESTER_NO_CHOICE:\n            return false;\n        case UI_REQUESTER_CANCEL:\n            return true;\n        case INPUT_BACKEND_KEYBOARD:\n        case INPUT_BACKEND_CONTROLLER:\n            s->backend = choice;\n            s->phase = M_PHASE_EDITOR;\n            g_Config.input.backend = s->backend;\n            Config_Update();\n            break;\n        }\n        break;\n    }\n\n    case M_PHASE_EDITOR: {\n        const UI_CONTROLS_CHOICE choice =\n            UI_ControlsEditor_Control(&s->editor_state[s->backend]);\n        switch (choice) {\n        case UI_CONTROLS_CHOICE_NOOP:\n            break;\n        case UI_CONTROLS_CHOICE_GO_BACK:\n            s->phase = M_PHASE_BACKEND;\n            break;\n        case UI_CONTROLS_CHOICE_EXIT:\n            return true;\n        }\n        break;\n    }\n    }\n\n    return false;\n}\n\nvoid UI_Controls(UI_CONTROLS_STATE *const s)\n{\n    switch (s->phase) {\n    case M_PHASE_BACKEND:\n        UI_ControlsBackend(&s->backend_state);\n        break;\n    case M_PHASE_EDITOR:\n        UI_ControlsEditor(&s->editor_state[s->backend]);\n        break;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/controls.h",
    "content": "#pragma once\n\n// A controls editor dialog.\n\n#include <trx/core/event_manager.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/controls_backend.h>\n#include <trx/game/ui/dialogs/controls_editor.h>\n\ntypedef struct {\n    int32_t phase;\n    INPUT_BACKEND backend;\n    int32_t active_layout;\n\n    EVENT_MANAGER *events;\n    UI_CONTROLS_BACKEND_STATE backend_state;\n    UI_CONTROLS_EDITOR_STATE editor_state[INPUT_BACKEND_NUMBER_OF];\n} UI_CONTROLS_STATE;\n\n// state functions\nvoid UI_Controls_Init(UI_CONTROLS_STATE *s);\nvoid UI_Controls_Free(UI_CONTROLS_STATE *s);\nbool UI_Controls_Control(UI_CONTROLS_STATE *s);\n\n// draw functions\nvoid UI_Controls(UI_CONTROLS_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/controls_backend.c",
    "content": "#include <trx/game/ui/dialogs/controls_backend.h>\n\n#include <trx/config.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef struct {\n    GAME_STRING_ID gs_id;\n    INPUT_BACKEND backend;\n} M_OPTION;\n\nstatic const M_OPTION m_Options[] = {\n    { .gs_id = GS_ID(\"general/settings/controls/backend/keyboard\"),\n      .backend = INPUT_BACKEND_KEYBOARD },\n    { .gs_id = GS_ID(\"general/settings/controls/backend/controller\"),\n      .backend = INPUT_BACKEND_CONTROLLER },\n    { .gs_id = nullptr },\n};\n\nvoid UI_ControlsBackend_Init(UI_CONTROLS_BACKEND_STATE *const s)\n{\n    int32_t count = 0;\n    int32_t sel_row = -1;\n    for (int32_t i = 0; m_Options[i].gs_id != nullptr; i++) {\n        if (m_Options[i].backend == g_Config.input.backend) {\n            sel_row = i;\n        }\n        count++;\n    }\n    UI_Requester_Init(&s->req, count, count, true);\n    if (sel_row != -1) {\n        UI_Requester_SelectRow(&s->req, sel_row);\n    }\n}\n\nvoid UI_ControlsBackend_Free(UI_CONTROLS_BACKEND_STATE *const s)\n{\n    UI_Requester_Free(&s->req);\n}\n\nint32_t UI_ControlsBackend_Control(UI_CONTROLS_BACKEND_STATE *const s)\n{\n    const int32_t choice = UI_Requester_Control(&s->req);\n    if (choice >= 0) {\n        return m_Options[choice].backend;\n    }\n    return choice;\n}\n\nvoid UI_ControlsBackend(UI_CONTROLS_BACKEND_STATE *const s)\n{\n    UI_BeginModal(0.5f, 2.0f / 3.0f);\n    UI_BeginRequester(&s->req, GS(\"general/settings/controls/customize\"));\n\n    for (int32_t i = UI_Requester_GetFirstRow(&s->req);\n         i < UI_Requester_GetLastRow(&s->req); i++) {\n        UI_BeginRequesterRow(&s->req, i);\n        UI_BeginAnchor(0.5f, 0.5f);\n        UI_Label(GameString_Get(m_Options[i].gs_id));\n        UI_EndAnchor();\n        UI_EndRequesterRow(&s->req, i);\n    }\n\n    UI_EndRequester(&s->req);\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/controls_backend.h",
    "content": "#pragma once\n\n// A control backend (keyboard/controller) choice dialog.\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef struct {\n    UI_REQUESTER_STATE req;\n} UI_CONTROLS_BACKEND_STATE;\n\n// state functions\nvoid UI_ControlsBackend_Init(UI_CONTROLS_BACKEND_STATE *s);\nvoid UI_ControlsBackend_Free(UI_CONTROLS_BACKEND_STATE *s);\nint32_t UI_ControlsBackend_Control(UI_CONTROLS_BACKEND_STATE *s);\n\n// draw functions\nvoid UI_ControlsBackend(UI_CONTROLS_BACKEND_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/controls_editor.c",
    "content": "#include <trx/game/ui/dialogs/controls_editor.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gun.h>\n#include <trx/game/input.h>\n#include <trx/game/shell.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/frame.h>\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/resize.h>\n#include <trx/game/ui/elements/row_arrows.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/span.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/elements/window.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\ntypedef enum {\n    M_PHASE_NAVIGATE_LAYOUT,\n    M_PHASE_NAVIGATE_GROUP,\n    M_PHASE_NAVIGATE_INPUTS,\n    M_PHASE_NAVIGATE_INPUTS_DEBOUNCE,\n    M_PHASE_LISTEN,\n    M_PHASE_LISTEN_DEBOUNCE,\n    M_PHASE_EXIT,\n} M_PHASE;\n\nstatic const UI_CONTROLS_EDITOR_GROUP m_Groups[] = {\n    {\n        .header_gs = GS_ID(\"general/settings/controls/tabs/basics\"),\n        .rows =\n            (UI_CONTROLS_EDITOR_ROW[]) {\n                { .role = INPUT_ROLE_UP },\n                { .role = INPUT_ROLE_DOWN },\n                { .role = INPUT_ROLE_LEFT },\n                { .role = INPUT_ROLE_RIGHT },\n                { .role = INPUT_ROLE_JUMP },\n                { .role = INPUT_ROLE_STEP_LEFT },\n                { .role = INPUT_ROLE_STEP_RIGHT },\n                { .role = INPUT_ROLE_ROLL },\n                { .role = INPUT_ROLE_SLOW },\n                { .role = INPUT_ROLE_SPRINT },\n                { .role = INPUT_ROLE_CROUCH },\n                { .role = INPUT_ROLE_ACTION },\n                { .role = INPUT_ROLE_DRAW_WEAPON },\n                { .role = INPUT_ROLE_LOOK },\n                { .role = (INPUT_ROLE)-1 },\n            },\n    },\n\n    {\n        .header_gs = GS_ID(\"general/settings/controls/tabs/items\"),\n        .rows =\n            (UI_CONTROLS_EDITOR_ROW[]) {\n                { .role = INPUT_ROLE_USE_FLARE },\n                { .role = INPUT_ROLE_USE_SMALL_MEDI },\n                { .role = INPUT_ROLE_USE_BIG_MEDI },\n                { .role = INPUT_ROLE_EQUIP_PISTOLS },\n                { .role = INPUT_ROLE_EQUIP_SHOTGUN },\n                { .role = INPUT_ROLE_EQUIP_MAGNUMS },\n                { .role = INPUT_ROLE_EQUIP_AUTOS },\n                { .role = INPUT_ROLE_EQUIP_DESERT_EAGLE },\n                { .role = INPUT_ROLE_EQUIP_UZIS },\n                { .role = INPUT_ROLE_EQUIP_HARPOON },\n                { .role = INPUT_ROLE_EQUIP_M16 },\n                { .role = INPUT_ROLE_EQUIP_MP5 },\n                { .role = INPUT_ROLE_EQUIP_ROCKET_LAUNCHER },\n                { .role = INPUT_ROLE_EQUIP_GRENADE_LAUNCHER },\n                { .role = (INPUT_ROLE)-1 },\n            },\n    },\n\n    {\n        .header_gs = GS_ID(\"general/settings/controls/tabs/misc\"),\n        .rows =\n            (UI_CONTROLS_EDITOR_ROW[]) {\n                { .role = INPUT_ROLE_CHANGE_TARGET },\n                { .role = INPUT_ROLE_CAMERA_UP },\n                { .role = INPUT_ROLE_CAMERA_DOWN },\n                { .role = INPUT_ROLE_CAMERA_LEFT },\n                { .role = INPUT_ROLE_CAMERA_RIGHT },\n                { .role = INPUT_ROLE_CAMERA_FORWARD },\n                { .role = INPUT_ROLE_CAMERA_BACK },\n                { .role = INPUT_ROLE_CHANGE_OUTFIT },\n                { .role = INPUT_ROLE_FLY_CHEAT },\n                { .role = INPUT_ROLE_ITEM_CHEAT },\n                { .role = INPUT_ROLE_LEVEL_SKIP_CHEAT },\n                { .role = INPUT_ROLE_TURBO_CHEAT },\n                { .role = (INPUT_ROLE)-1 },\n            },\n    },\n\n    {\n        .header_gs = GS_ID(\"general/settings/controls/tabs/system\"),\n        .rows =\n            (UI_CONTROLS_EDITOR_ROW[]) {\n                { .role = INPUT_ROLE_INVENTORY },\n                { .role = INPUT_ROLE_SAVE },\n                { .role = INPUT_ROLE_LOAD },\n                { .role = INPUT_ROLE_QUICK_SAVE },\n                { .role = INPUT_ROLE_QUICK_LOAD },\n                { .role = INPUT_ROLE_PAUSE },\n                // { .role = INPUT_ROLE_SCREENSHOT }, // handled specially\n                { .role = INPUT_ROLE_FPS },\n                // { .role = INPUT_ROLE_TOGGLE_FULLSCREEN }, // handled\n                // specially\n                { .role = INPUT_ROLE_ENTER_CONSOLE },\n                { .role = INPUT_ROLE_TOGGLE_PHOTO_MODE },\n                { .role = INPUT_ROLE_TOGGLE_UI },\n                { .role = INPUT_ROLE_TOGGLE_BILINEAR_FILTER },\n                { .role = INPUT_ROLE_TOGGLE_TRAPEZOID_FILTER },\n                { .role = INPUT_ROLE_SWITCH_UPSCALING },\n                { .role = INPUT_ROLE_SWITCH_BORDERS },\n                { .role = INPUT_ROLE_TOGGLE_WIREFRAME },\n                { .role = INPUT_ROLE_TOGGLE_TEXTURES },\n                { .role = INPUT_ROLE_CYCLE_LIGHTING_CONTRAST },\n                { .role = (INPUT_ROLE)-1 },\n            },\n    },\n\n    {\n        .header_gs = nullptr,\n        .rows = nullptr,\n    },\n};\n\nstatic int32_t M_GetVisibleRows(void)\n{\n    const int32_t res_h = UI_Scaler_CalcInverse(\n        Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT);\n    if (res_h <= 240) {\n        return 5;\n    } else if (res_h <= 252) {\n        return 6;\n    } else if (res_h <= 266) {\n        return 7;\n    } else if (res_h <= 282) {\n        return 8;\n    } else if (res_h <= 300) {\n        return 9;\n    } else if (res_h <= 320) {\n        return 10;\n    } else if (res_h <= 342) {\n        return 11;\n    } else if (res_h <= 370) {\n        return 12;\n    } else if (res_h <= 420) {\n        return 13;\n    } else if (res_h <= 480) {\n        return 15;\n    } else {\n        return 16;\n    }\n}\n\nstatic bool M_IsRoleUsable(const INPUT_ROLE role)\n{\n    switch (role) {\n    case INPUT_ROLE_USE_FLARE:\n        return g_Weapons[LGT_FLARE].is_available;\n    case INPUT_ROLE_EQUIP_MAGNUMS:\n        return g_Weapons[LGT_MAGNUMS].is_available;\n    case INPUT_ROLE_EQUIP_AUTOS:\n        return g_Weapons[LGT_AUTOS].is_available;\n    case INPUT_ROLE_EQUIP_DESERT_EAGLE:\n        return g_Weapons[LGT_DESERT_EAGLE].is_available;\n    case INPUT_ROLE_EQUIP_HARPOON:\n        return g_Weapons[LGT_HARPOON].is_available;\n    case INPUT_ROLE_EQUIP_M16:\n        return g_Weapons[LGT_M16].is_available;\n    case INPUT_ROLE_EQUIP_MP5:\n        return g_Weapons[LGT_MP5].is_available;\n    case INPUT_ROLE_EQUIP_GRENADE_LAUNCHER:\n        return g_Weapons[LGT_GRENADE].is_available;\n    case INPUT_ROLE_EQUIP_ROCKET_LAUNCHER:\n        return g_Weapons[LGT_ROCKET].is_available;\n    case INPUT_ROLE_FLY_CHEAT:\n    case INPUT_ROLE_ITEM_CHEAT:\n    case INPUT_ROLE_LEVEL_SKIP_CHEAT:\n    case INPUT_ROLE_TURBO_CHEAT:\n        return g_Config.gameplay.enable_cheats;\n    default:\n        break;\n    }\n    return true;\n}\n\nstatic int32_t M_GetInputRoleCount(const UI_CONTROLS_EDITOR_GROUP *const group)\n{\n    int32_t count = 0;\n    for (int32_t i = 0; group->rows[i].role != (INPUT_ROLE)-1; i++) {\n        count++;\n    }\n    return count;\n}\n\nstatic void M_ResetLayout(void *const arg)\n{\n    const UI_CONTROLS_EDITOR_STATE *const s = arg;\n    Sound_Effect(\n        g_TRVersion == 1 ? SFX_MENU_GAMEBOY : SFX_MENU_SPINOUT, nullptr,\n        SPM_NORMAL);\n    Input_ResetLayout(s->backend, s->active_layout);\n    g_Config.dirty = true;\n    Config_Update();\n}\n\nstatic void M_UnbindKey(void *const arg)\n{\n    const UI_CONTROLS_EDITOR_STATE *const s = arg;\n    Sound_Effect(\n        g_TRVersion == 1 ? SFX_MENU_GAMEBOY : SFX_MENU_SPINOUT, nullptr,\n        SPM_NORMAL);\n    Input_UnassignRole(\n        s->backend, s->active_layout, s->active_role, s->active_slot);\n    g_Config.dirty = true;\n    Config_Update();\n}\n\nstatic bool M_CanResetLayout(const UI_CONTROLS_EDITOR_STATE *const s)\n{\n    return !Input_IsInListenMode()\n        && s->phase != M_PHASE_NAVIGATE_INPUTS_DEBOUNCE\n        && s->active_layout != INPUT_LAYOUT_DEFAULT;\n}\n\nstatic bool M_CanUnbindKey(const UI_CONTROLS_EDITOR_STATE *const s)\n{\n    return !Input_IsInListenMode()\n        && (s->phase == M_PHASE_NAVIGATE_INPUTS\n            || s->phase == M_PHASE_LISTEN_DEBOUNCE)\n        && s->active_layout != INPUT_LAYOUT_DEFAULT\n        && s->active_role != (INPUT_ROLE)-1\n        && Input_IsRoleUnbindable(s->active_role);\n}\n\nstatic void M_CheckResetKeys(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    if (M_CanResetLayout(s)) {\n        UI_ProgressButton_Control(s->reset_bindings_button);\n    }\n    if (M_CanUnbindKey(s)) {\n        UI_ProgressButton_Control(s->unbind_key_button);\n    }\n}\n\nstatic UI_CONTROLS_CHOICE M_NavigateLayout(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    M_CheckResetKeys(s);\n    if (g_InputDB.menu_confirm) {\n        return UI_CONTROLS_CHOICE_EXIT;\n    } else if (g_InputDB.menu_back) {\n        return UI_CONTROLS_CHOICE_GO_BACK;\n    } else if (\n        UI_TabSwitch_Control(s->layout_tab_switch, UI_TAB_SWITCH_NORMAL)) {\n        s->active_layout = s->layout_tab_switch->active_tab_idx;\n        const EVENT event = {\n            .name = \"layout_change\",\n            .sender = nullptr,\n            .data = nullptr,\n        };\n        EventManager_Fire(s->events, &event);\n    } else if (g_InputDB.menu_down) {\n        s->phase = M_PHASE_NAVIGATE_GROUP;\n    } else if (\n        g_InputDB.menu_up && s->active_layout != 0\n        && g_Config.ui.enable_wraparound) {\n        s->phase = M_PHASE_NAVIGATE_INPUTS;\n        UI_Scrollable_SelectLastItem(&s->scroll);\n        s->active_role = s->active_group->rows[s->scroll.sel_item].role;\n    } else {\n        return UI_CONTROLS_CHOICE_NOOP;\n    }\n    s->active_role = s->active_group->rows[s->scroll.sel_item].role;\n    return UI_CONTROLS_CHOICE_NOOP;\n}\n\nstatic UI_CONTROLS_CHOICE M_NavigateGroup(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    M_CheckResetKeys(s);\n    if (g_InputDB.menu_confirm) {\n        return UI_CONTROLS_CHOICE_EXIT;\n    } else if (g_InputDB.menu_back) {\n        return UI_CONTROLS_CHOICE_GO_BACK;\n    } else if (\n        UI_TabSwitch_Control(s->controls_tab_switch, UI_TAB_SWITCH_NORMAL)) {\n        s->active_group = &m_Groups[s->controls_tab_switch->active_tab_idx];\n        UI_Scrollable_SetMaxItems(\n            &s->scroll, M_GetInputRoleCount(s->active_group));\n    } else if (g_InputDB.menu_down && s->active_layout != 0) {\n        s->phase = M_PHASE_NAVIGATE_INPUTS;\n        UI_Scrollable_SelectFirstItem(&s->scroll);\n        s->active_role = s->active_group->rows[s->scroll.sel_item].role;\n    } else if (g_InputDB.menu_up) {\n        s->phase = M_PHASE_NAVIGATE_LAYOUT;\n    }\n    s->active_role = s->active_group->rows[s->scroll.sel_item].role;\n    return UI_CONTROLS_CHOICE_NOOP;\n}\n\nstatic UI_CONTROLS_CHOICE M_NavigateInputs(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    M_CheckResetKeys(s);\n    if (g_InputDB.menu_confirm) {\n        s->phase = M_PHASE_NAVIGATE_INPUTS_DEBOUNCE;\n    } else if (g_InputDB.menu_back) {\n        return UI_CONTROLS_CHOICE_GO_BACK;\n    } else if (\n        UI_TabSwitch_Control(s->controls_tab_switch, UI_TAB_SWITCH_NO_ARROWS)) {\n        s->active_group = &m_Groups[s->controls_tab_switch->active_tab_idx];\n        UI_Scrollable_SetMaxItems(\n            &s->scroll, M_GetInputRoleCount(s->active_group));\n    } else if (g_InputDB.menu_left) {\n        s->active_slot =\n            (s->active_slot - 1 + INPUT_BINDING_SLOTS) % INPUT_BINDING_SLOTS;\n    } else if (g_InputDB.menu_right) {\n        s->active_slot = (s->active_slot + 1) % INPUT_BINDING_SLOTS;\n    } else if (g_InputDB.menu_up) {\n        if (!UI_Scrollable_SelectPrev(&s->scroll, false)) {\n            s->phase = M_PHASE_NAVIGATE_GROUP;\n        }\n    } else if (g_InputDB.menu_down) {\n        if (!UI_Scrollable_SelectNext(&s->scroll, false)\n            && g_Config.ui.enable_wraparound) {\n            s->phase = M_PHASE_NAVIGATE_LAYOUT;\n        }\n    } else {\n        return UI_CONTROLS_CHOICE_NOOP;\n    }\n    s->active_role = s->active_group->rows[s->scroll.sel_item].role;\n    return UI_CONTROLS_CHOICE_NOOP;\n}\n\nstatic UI_CONTROLS_CHOICE M_NavigateInputsDebounce(\n    UI_CONTROLS_EDITOR_STATE *const s)\n{\n    Input_Update();\n    if (InputState_IsAnyPressed(g_Input)) {\n        return UI_CONTROLS_CHOICE_NOOP;\n    }\n    Input_EnterListenMode();\n    s->phase = M_PHASE_LISTEN;\n    return UI_CONTROLS_CHOICE_NOOP;\n}\n\nstatic UI_CONTROLS_CHOICE M_Listen(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    if (!Input_ReadAndAssignRole(\n            s->backend, s->active_layout, s->active_role, s->active_slot)) {\n        return UI_CONTROLS_CHOICE_NOOP;\n    }\n\n    Input_ExitListenMode();\n\n    const EVENT event = {\n        .name = \"key_change\",\n        .sender = nullptr,\n        .data = nullptr,\n    };\n    EventManager_Fire(s->events, &event);\n\n    s->phase = M_PHASE_LISTEN_DEBOUNCE;\n    return UI_CONTROLS_CHOICE_NOOP;\n}\n\nstatic UI_CONTROLS_CHOICE M_ListenDebounce(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    if (!InputState_IsAnyPressed(g_Input)) {\n        s->phase = M_PHASE_NAVIGATE_INPUTS;\n    }\n    return UI_CONTROLS_CHOICE_NOOP;\n}\n\nstatic void M_CurrentLayout(const UI_CONTROLS_EDITOR_STATE *const s)\n{\n    UI_TabSwitchSingle(\n        s->layout_tab_switch, s->phase == M_PHASE_NAVIGATE_LAYOUT);\n}\n\nstatic void M_GroupsHeader(const UI_CONTROLS_EDITOR_STATE *const s)\n{\n    UI_TabSwitch(s->controls_tab_switch, s->phase == M_PHASE_NAVIGATE_GROUP);\n}\n\nstatic void M_InputLabel(\n    const UI_CONTROLS_EDITOR_STATE *const s,\n    const UI_CONTROLS_EDITOR_ROW *const row)\n{\n    const bool is_selected = s->active_role == row->role\n        && (s->phase == M_PHASE_NAVIGATE_INPUTS\n            || s->phase == M_PHASE_LISTEN_DEBOUNCE);\n    if (is_selected) {\n        UI_BeginFrame(UI_FRAME_SELECTED_OPTION);\n    }\n    const char *const role_name = Input_GetRoleName(row->role);\n    if (!M_IsRoleUsable(row->role)) {\n        UI_LabelFmt(\"\\\\{dim}%s\\\\{/dim}\", role_name);\n    } else {\n        UI_Label(role_name);\n    }\n    if (is_selected) {\n        UI_EndFrame();\n    }\n}\n\nstatic void M_InputSlot(\n    UI_CONTROLS_EDITOR_STATE *const s, const UI_CONTROLS_EDITOR_ROW *const row,\n    const int32_t slot)\n{\n    const bool is_flashing =\n        Input_IsKeyConflicted(s->backend, s->active_layout, row->role);\n    const bool is_active_row = s->active_role == row->role;\n    const bool is_listening = is_active_row && s->active_slot == slot\n        && (s->phase == M_PHASE_LISTEN\n            || s->phase == M_PHASE_NAVIGATE_INPUTS_DEBOUNCE);\n    const bool is_slot_selected = is_active_row && s->active_slot == slot\n        && (s->phase == M_PHASE_NAVIGATE_INPUTS\n            || s->phase == M_PHASE_LISTEN_DEBOUNCE);\n\n    if (is_flashing) {\n        UI_BeginFlash(&s->flash);\n    }\n    if (is_listening || is_slot_selected) {\n        UI_BeginFrame(UI_FRAME_SELECTED_OPTION);\n    }\n    const char *key_name =\n        Input_GetKeyName(s->backend, s->active_layout, row->role, slot);\n    if (key_name == nullptr) {\n        if (s->active_layout != INPUT_LAYOUT_DEFAULT) {\n            UI_Label(\"—\");\n        } else {\n            UI_Label(\"\");\n        }\n    } else if (!M_IsRoleUsable(row->role)) {\n        UI_LabelFmt(\"\\\\{dim}%s\\\\{/dim}\", key_name);\n    } else {\n        UI_Label(key_name);\n    }\n    if (is_listening || is_slot_selected) {\n        UI_EndFrame();\n    }\n    if (is_flashing) {\n        UI_EndFlash();\n    }\n}\n\nstatic void M_Group(\n    UI_CONTROLS_EDITOR_STATE *const s,\n    const UI_CONTROLS_EDITOR_GROUP *const group)\n{\n    UI_BeginStack(UI_STACK_VERTICAL);\n    for (int32_t i = 0; i < s->scroll.vis_items; i++) {\n        const int32_t row_idx = s->scroll.first_item + i;\n        if (row_idx >= s->scroll.max_items) {\n            UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n            continue;\n        }\n\n        const UI_CONTROLS_EDITOR_ROW *const row = &group->rows[row_idx];\n        UI_BeginStack(UI_STACK_HORIZONTAL);\n        for (int32_t slot = 0; slot < INPUT_BINDING_SLOTS; slot++) {\n            UI_BeginResize(s->input_size, -1.0f);\n            UI_BeginAnchor(0.0f, 0.5f);\n            M_InputSlot(s, row, slot);\n            UI_EndAnchor();\n            UI_EndResize();\n        }\n        UI_BeginResize(s->label_size, -1.0f);\n        UI_BeginAnchor(0.0f, 0.5f);\n        M_InputLabel(s, row);\n        UI_EndAnchor();\n        UI_EndResize();\n        UI_EndStack();\n    }\n    UI_EndStack();\n}\n\nstatic void M_Footer(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        .spacing = { .h = 40.0f },\n    });\n    UI_BeginHide(!M_CanResetLayout(s));\n    UI_ProgressButton(s->reset_bindings_button);\n    UI_EndHide();\n\n    UI_BeginHide(!M_CanUnbindKey(s));\n    UI_ProgressButton(s->unbind_key_button);\n    UI_EndHide();\n    UI_EndStack();\n}\n\nvoid UI_ControlsEditor_Init(\n    UI_CONTROLS_EDITOR_STATE *const s, const INPUT_BACKEND backend,\n    const int32_t layout, EVENT_MANAGER *const events)\n{\n    s->backend = backend;\n    s->active_layout = layout;\n    s->active_slot = 0;\n    s->phase = M_PHASE_NAVIGATE_LAYOUT;\n\n    s->events = events;\n    UI_Flash_Init(&s->flash, LOGIC_FPS * 2 / 3);\n\n    s->reset_bindings_button = UI_ProgressButton_Init(\n        s->backend, INPUT_ROLE_RESET_BINDINGS,\n        GS_ID(\"general/actions/reset_defaults\"), M_ResetLayout, s);\n    s->unbind_key_button = UI_ProgressButton_Init(\n        s->backend, INPUT_ROLE_UNBIND_KEY, GS_ID(\"general/actions/unbind\"),\n        M_UnbindKey, s);\n\n    {\n        UI_TAB_SWITCH_TAB layout_tabs[INPUT_LAYOUT_NUMBER_OF];\n        for (INPUT_LAYOUT i = 0; i < INPUT_LAYOUT_NUMBER_OF; i++) {\n            layout_tabs[i].header.one_off = nullptr;\n            layout_tabs[i].header.live_ptr = Input_GetLayoutNamePtr(i);\n        }\n        s->layout_tab_switch =\n            UI_TabSwitch_Init(INPUT_LAYOUT_NUMBER_OF, layout_tabs);\n        s->layout_tab_switch->active_tab_idx = s->active_layout;\n    }\n\n    {\n        int32_t tab_count = 0;\n        for (int32_t i = 0; m_Groups[i].rows != nullptr; i++) {\n            tab_count++;\n        }\n        UI_TAB_SWITCH_TAB controls_tabs[tab_count];\n        for (int32_t i = 0; m_Groups[i].rows != nullptr; i++) {\n            controls_tabs[i].header.one_off = nullptr;\n            controls_tabs[i].header.live_ptr =\n                GameString_GetPtr(m_Groups[i].header_gs);\n        }\n        s->controls_tab_switch = UI_TabSwitch_Init(tab_count, controls_tabs);\n    }\n\n    s->max_group_items = 0;\n    for (const UI_CONTROLS_EDITOR_GROUP *group = m_Groups;\n         group->rows != nullptr; group++) {\n        s->max_group_items =\n            MAX(s->max_group_items, M_GetInputRoleCount(group));\n    }\n\n    s->active_group = &m_Groups[0];\n    s->scroll.first_item = 0;\n    s->scroll.sel_item = 0;\n    s->scroll.vis_items = MIN(s->max_group_items, M_GetVisibleRows());\n    s->scroll.max_items = M_GetInputRoleCount(s->active_group);\n    s->active_role = s->active_group->rows[s->scroll.sel_item].role;\n\n    s->label_size = 0.0f;\n    for (int32_t i = 0; i < INPUT_ROLE_NUMBER_OF; i++) {\n        float w;\n        UI_Label_Measure(Input_GetRoleName(i), &w, nullptr);\n        s->label_size = MAX(s->label_size, w / g_Config.ui.text_scale);\n    }\n    s->input_size = 80;\n}\n\nvoid UI_ControlsEditor_Free(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    UI_Flash_Free(&s->flash);\n    UI_TabSwitch_Free(s->layout_tab_switch);\n    s->layout_tab_switch = nullptr;\n    UI_TabSwitch_Free(s->controls_tab_switch);\n    s->controls_tab_switch = nullptr;\n    UI_ProgressButton_Free(s->reset_bindings_button);\n    s->reset_bindings_button = nullptr;\n    UI_ProgressButton_Free(s->unbind_key_button);\n    s->unbind_key_button = nullptr;\n}\n\nUI_CONTROLS_CHOICE UI_ControlsEditor_Control(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    UI_Flash_Control(&s->flash);\n    switch (s->phase) {\n    case M_PHASE_NAVIGATE_LAYOUT:\n        return M_NavigateLayout(s);\n    case M_PHASE_NAVIGATE_GROUP:\n        return M_NavigateGroup(s);\n    case M_PHASE_NAVIGATE_INPUTS:\n        return M_NavigateInputs(s);\n    case M_PHASE_NAVIGATE_INPUTS_DEBOUNCE:\n        return M_NavigateInputsDebounce(s);\n    case M_PHASE_LISTEN:\n        return M_Listen(s);\n    case M_PHASE_LISTEN_DEBOUNCE:\n        return M_ListenDebounce(s);\n    default:\n        return UI_CONTROLS_CHOICE_NOOP;\n    }\n}\n\nstatic void M_Header(void *const user_data)\n{\n    UI_CONTROLS_EDITOR_STATE *const s = user_data;\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = 4.0f },\n    });\n    M_CurrentLayout(s);\n    M_GroupsHeader(s);\n    UI_EndStack();\n    UI_Spacer(0.0f, 5.0f);\n}\n\nvoid UI_ControlsEditor(UI_CONTROLS_EDITOR_STATE *const s)\n{\n    UI_BeginModal(0.5f, 0.55f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n    });\n\n    UI_BeginWindow((UI_WINDOW_SETTINGS) {\n        .title = GS(\"general/settings/controls/customize\"),\n        .scrollable = nullptr,\n        .title_spacing = -1.0f,\n        .header_func = M_Header,\n        .user_data = s,\n    });\n\n    UI_BeginStack(UI_STACK_HORIZONTAL);\n    M_Group(s, s->active_group);\n    UI_EndStack();\n\n    UI_EndWindow();\n\n    UI_Spacer(0.0f, 5.0f);\n    M_Footer(s);\n    UI_EndStack();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/controls_editor.h",
    "content": "#pragma once\n\n// A controls remapper dialog.\n\n#include <trx/core/event_manager.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/flash.h>\n#include <trx/game/ui/elements/progress_button.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/tab_switch.h>\n#include <trx/game/ui/scrollable.h>\n\ntypedef struct {\n    INPUT_ROLE role;\n} UI_CONTROLS_EDITOR_ROW;\n\ntypedef struct {\n    GAME_STRING_ID header_gs;\n    UI_CONTROLS_EDITOR_ROW *rows;\n} UI_CONTROLS_EDITOR_GROUP;\n\ntypedef struct {\n    int32_t phase;\n    INPUT_BACKEND backend;\n    INPUT_LAYOUT active_layout;\n    INPUT_ROLE active_role;\n    int32_t active_slot;\n    const UI_CONTROLS_EDITOR_GROUP *active_group;\n    UI_FLASH_STATE flash;\n    EVENT_MANAGER *events;\n\n    UI_SCROLLABLE scroll;\n\n    int32_t max_group_items;\n    int32_t input_size;\n    int32_t label_size;\n    UI_TAB_SWITCH_STATE *layout_tab_switch;\n    UI_TAB_SWITCH_STATE *controls_tab_switch;\n    UI_PROGRESS_BUTTON_STATE *reset_bindings_button;\n    UI_PROGRESS_BUTTON_STATE *unbind_key_button;\n} UI_CONTROLS_EDITOR_STATE;\n\ntypedef enum {\n    UI_CONTROLS_CHOICE_EXIT,\n    UI_CONTROLS_CHOICE_GO_BACK,\n    UI_CONTROLS_CHOICE_NOOP,\n} UI_CONTROLS_CHOICE;\n\n// state functions\nvoid UI_ControlsEditor_Init(\n    UI_CONTROLS_EDITOR_STATE *s, INPUT_BACKEND backend, int32_t layout,\n    EVENT_MANAGER *events);\nvoid UI_ControlsEditor_Free(UI_CONTROLS_EDITOR_STATE *s);\nUI_CONTROLS_CHOICE UI_ControlsEditor_Control(UI_CONTROLS_EDITOR_STATE *s);\n\n// draw functions\nvoid UI_ControlsEditor(UI_CONTROLS_EDITOR_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/gameplay_settings.c",
    "content": "#include <trx/game/ui/dialogs/gameplay_settings.h>\n\n#include <trx/config.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/ui/dialogs/setting_helpers/enums.h>\n#include <trx/game/ui/dialogs/setting_helpers/handlers.h>\n#include <trx/game/ui/dialogs/settings_tabs.h>\n\nstatic const UI_SETTINGS_OPTION m_GeneralOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/gameplay_general.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_ControlOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/gameplay_controls.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_GameplayModOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/gameplay_mods.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_GameplayFixOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/gameplay_fixes.def>\n    { .target = nullptr },\n};\n\nUI_SETTINGS_DIALOG_STATE *UI_GameplaySettings_Init(void)\n{\n    const UI_SETTINGS_TAB tabs[] = {\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/gameplay/tabs/general\"), m_GeneralOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/gameplay/tabs/controls\"), m_ControlOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/gameplay/tabs/mods\"), m_GameplayModOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/gameplay/tabs/fixes\"),\n            m_GameplayFixOptions),\n        UI_SettingsTab_MakePresets(\n            GS_ID(\"general/settings/gameplay/tabs/presets\")),\n    };\n\n    return UI_SettingsDialog_Init(\n        GS_ID(\"general/settings/gameplay/title\"), ARRAY_SIZE(tabs), tabs);\n}\n\nvoid UI_GameplaySettings_Free(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SettingsDialog_Free(s);\n}\n\nbool UI_GameplaySettings_Control(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    return UI_SettingsDialog_Control(s);\n}\n\nvoid UI_GameplaySettings(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SettingsDialog(s);\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/gameplay_settings.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/settings.h>\n\nUI_SETTINGS_DIALOG_STATE *UI_GameplaySettings_Init(void);\nvoid UI_GameplaySettings_Free(UI_SETTINGS_DIALOG_STATE *s);\nbool UI_GameplaySettings_Control(UI_SETTINGS_DIALOG_STATE *s);\n\nvoid UI_GameplaySettings(UI_SETTINGS_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/graphic_settings.c",
    "content": "#include <trx/game/ui/dialogs/graphic_settings.h>\n\n#include <trx/config.h>\n#include <trx/game/ui/dialogs/setting_helpers/enums.h>\n#include <trx/game/ui/dialogs/setting_helpers/handlers.h>\n#include <trx/game/ui/dialogs/settings_tabs.h>\n\nstatic const UI_SETTINGS_OPTION m_VisualsOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/graphic_visuals.def>\n    { .target = nullptr },\n};\n\nstatic UI_SETTINGS_OPTION m_UIOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/graphic_ui.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_UIStatsOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/graphic_ui_stats.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_UIBarsOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/graphic_ui_bars.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_RenderOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/graphic_rendering.def>\n    { .target = nullptr },\n};\n\nUI_SETTINGS_DIALOG_STATE *UI_GraphicSettings_Init(void)\n{\n    const UI_SETTINGS_TAB tabs[] = {\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/graphic_settings/tabs/visuals\"),\n            m_VisualsOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/graphic_settings/tabs/ui\"), m_UIOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/graphic_settings/tabs/stats\"),\n            m_UIStatsOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/graphic_settings/tabs/bars\"),\n            m_UIBarsOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/graphic_settings/tabs/rendering\"),\n            m_RenderOptions),\n    };\n\n    return UI_SettingsDialog_Init(\n        GS_ID(\"general/settings/graphic_settings/title\"), ARRAY_SIZE(tabs),\n        tabs);\n}\n\nvoid UI_GraphicSettings_Free(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SettingsDialog_Free(s);\n}\n\nbool UI_GraphicSettings_Control(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    return UI_SettingsDialog_Control(s);\n}\n\nvoid UI_GraphicSettings(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SettingsDialog(s);\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/graphic_settings.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/settings.h>\n\nUI_SETTINGS_DIALOG_STATE *UI_GraphicSettings_Init(void);\nvoid UI_GraphicSettings_Free(UI_SETTINGS_DIALOG_STATE *s);\nbool UI_GraphicSettings_Control(UI_SETTINGS_DIALOG_STATE *s);\n\nvoid UI_GraphicSettings(UI_SETTINGS_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/new_game.c",
    "content": "#include <trx/game/ui/dialogs/new_game.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/savegame.h>\n#include <trx/game/shell/mod.h>\n#include <trx/game/ui.h>\n#include <trx/version.h>\n\ntypedef struct {\n    bool play_prev_levels;\n    bool story_so_far;\n    bool switch_mod;\n} M_FEATURES;\n\ntypedef struct {\n    GAME_STRING_ID label_id;\n    UI_NEW_GAME_CHOICE choice;\n} M_OPTION;\n\ntypedef struct UI_NEW_GAME_STATE {\n    VECTOR *options;\n    UI_REQUESTER_STATE req;\n} UI_NEW_GAME_STATE;\n\nstatic const M_OPTION m_Options[] = {\n    {\n        .label_id = GS_ID(\"general/passport/mode_new_game\"),\n        .choice = UI_NEW_GAME_CHOICE_NG,\n    },\n    {\n        .label_id = GS_ID(\"general/passport/mode_new_game_plus\"),\n        .choice = UI_NEW_GAME_CHOICE_NGPLUS,\n    },\n    {\n        .label_id = GS_ID(\"general/passport/mode_new_game_jp\"),\n        .choice = UI_NEW_GAME_CHOICE_JP_NG,\n    },\n    {\n        .label_id = GS_ID(\"general/passport/mode_new_game_jp_plus\"),\n        .choice = UI_NEW_GAME_CHOICE_JP_NGPLUS,\n    },\n    {\n        .label_id = GS_ID(\"general/passport/play_previous_levels\"),\n        .choice = UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS,\n    },\n    {\n        .label_id = GS_ID(\"general/passport/story_so_far\"),\n        .choice = UI_NEW_GAME_CHOICE_STORY_SO_FAR,\n    },\n    {\n        .label_id = GS_ID(\"general/passport/switch_mod\"),\n        .choice = UI_NEW_GAME_CHOICE_SWITCH_MOD,\n    },\n    { .label_id = nullptr, .choice = (UI_NEW_GAME_CHOICE)-1 },\n};\n\nstatic bool M_HasSwitchModChoice(void)\n{\n    int32_t count = 0;\n    for (int32_t i = 0; i < Shell_GetModCount(); i++) {\n        const SHELL_MOD *const mod = Shell_GetMod(i);\n        if (Shell_CanSwitchToMod(mod)) {\n            count++;\n        }\n    }\n    return count > 1;\n}\n\nstatic M_FEATURES M_CheckFeatures(const bool check_save_features)\n{\n    M_FEATURES features = {\n        .switch_mod = M_HasSwitchModChoice(),\n    };\n\n    if (g_Config.flow.load_save_disabled || !check_save_features) {\n        return features;\n    }\n    for (SAVEGAME_SLOT_POOL pool = 0; pool < SAVEGAME_SLOT_POOL_NUMBER_OF;\n         pool++) {\n        for (int32_t slot_num = 0; slot_num < Savegame_GetSlotCount(pool);\n             slot_num++) {\n            const SAVEGAME_SLOT_REF slot = { .pool = pool, .index = slot_num };\n            if (Savegame_IsSlotFree(slot)) {\n                continue;\n            }\n            if (!features.play_prev_levels) {\n                const SAVEGAME_INFO *const info =\n                    Savegame_GetSavegameInfo(slot);\n                if (info->features.select_level) {\n                    features.play_prev_levels = true;\n                }\n            }\n            if (!features.story_so_far && GF_HasAvailableStory(slot)) {\n                features.story_so_far = true;\n            }\n        }\n    }\n    return features;\n}\n\nbool UI_NewGame_HasModChoices(void)\n{\n    return M_HasSwitchModChoice();\n}\n\nstatic bool M_OptionVisible(\n    const M_FEATURES *const features, const M_OPTION *const option)\n{\n    if (option->choice == UI_NEW_GAME_CHOICE_STORY_SO_FAR) {\n        return features->story_so_far;\n    }\n    if (option->choice == UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS) {\n        return features->play_prev_levels;\n    }\n    if (option->choice == UI_NEW_GAME_CHOICE_SWITCH_MOD) {\n        return features->switch_mod;\n    }\n    return g_Config.gameplay.enable_game_modes\n        || option->choice == UI_NEW_GAME_CHOICE_NG;\n}\n\nUI_NEW_GAME_STATE *UI_NewGame_Init(const bool show_play_prev_levels)\n{\n    UI_NEW_GAME_STATE *const s = Memory_Alloc(sizeof(UI_NEW_GAME_STATE));\n    s->options = Vector_Create(sizeof(M_OPTION));\n\n    const M_FEATURES features = show_play_prev_levels\n        ? M_CheckFeatures(g_Config.gameplay.enable_play_previous_levels)\n        : (M_FEATURES) {};\n    for (int32_t i = 0; m_Options[i].label_id != nullptr; i++) {\n        if (M_OptionVisible(&features, &m_Options[i])) {\n            Vector_Add(s->options, &m_Options[i]);\n        }\n    }\n\n    UI_Requester_Init(&s->req, s->options->count, s->options->count, true);\n    return s;\n}\n\nvoid UI_NewGame_Free(UI_NEW_GAME_STATE *const s)\n{\n    Vector_Free(s->options);\n    UI_Requester_Free(&s->req);\n    Memory_Free(s);\n}\n\nint32_t UI_NewGame_Control(UI_NEW_GAME_STATE *const s)\n{\n    const int32_t choice = UI_Requester_Control(&s->req);\n    if (choice == UI_REQUESTER_CANCEL || choice == UI_REQUESTER_NO_CHOICE) {\n        return choice;\n    }\n    const M_OPTION *const opt = Vector_Get(s->options, choice);\n    return opt->choice;\n}\n\nvoid UI_NewGame(UI_NEW_GAME_STATE *const s)\n{\n    UI_BeginModal(0.5f, 2.0f / 3.0f);\n    UI_BeginRequester(&s->req, GS(\"general/passport/select_mode\"));\n\n    bool line_drawn = false;\n    for (int32_t i = 0; i < s->options->count; i++) {\n        const M_OPTION *const opt = Vector_Get(s->options, i);\n        UI_BeginStackEx((UI_STACK_SETTINGS) {\n            .orientation = UI_STACK_VERTICAL,\n            { .h = UI_STACK_H_ALIGN_SPAN },\n        });\n        if (i > 0 && !line_drawn\n            && (opt->choice == UI_NEW_GAME_CHOICE_SWITCH_MOD\n                || opt->choice == UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS\n                || opt->choice == UI_NEW_GAME_CHOICE_STORY_SO_FAR)) {\n            // TODO: do not hardcode the numbers (they come from\n            // UI_BeginWindowBody)\n            UI_BeginPad(g_TRVersion >= 2 ? -7.0f : -10.0f, 4.0f);\n            UI_HorizontalLine();\n            UI_EndPad();\n            line_drawn = true;\n        }\n        UI_BeginRequesterRow(&s->req, i);\n        UI_BeginAnchor(0.5f, 0.5f);\n        UI_Label(GameString_Get(opt->label_id));\n        UI_EndAnchor();\n        UI_EndRequesterRow(&s->req, i);\n        UI_EndStack();\n    }\n\n    UI_EndRequester(&s->req);\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/new_game.h",
    "content": "#pragma once\n\n// A new game mode selector dialog.\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef enum {\n    UI_NEW_GAME_CHOICE_NG,\n    UI_NEW_GAME_CHOICE_NGPLUS,\n    UI_NEW_GAME_CHOICE_JP_NG,\n    UI_NEW_GAME_CHOICE_JP_NGPLUS,\n    UI_NEW_GAME_CHOICE_SWITCH_MOD,\n    UI_NEW_GAME_CHOICE_PLAY_PREV_LEVELS,\n    UI_NEW_GAME_CHOICE_STORY_SO_FAR,\n} UI_NEW_GAME_CHOICE;\n\ntypedef struct UI_NEW_GAME_STATE UI_NEW_GAME_STATE;\n\n// state functions\nbool UI_NewGame_HasModChoices(void);\nUI_NEW_GAME_STATE *UI_NewGame_Init(bool show_play_prev_levels);\nint32_t UI_NewGame_Control(UI_NEW_GAME_STATE *s);\nvoid UI_NewGame_Free(UI_NEW_GAME_STATE *s);\n\n// draw functions\nvoid UI_NewGame(UI_NEW_GAME_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/pause.c",
    "content": "#include <trx/game/ui/dialogs/pause.h>\n\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/frame.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/requester.h>\n\nstatic const GAME_STRING_ID m_Options[2][2] = {\n    { GS_ID(\"general/pause/continue\"), GS_ID(\"general/pause/quit\") },\n    { GS_ID(\"general/pause/yes\"), GS_ID(\"general/pause/no\") },\n};\n\nvoid UI_Pause_Init(UI_PAUSE_STATE *const s)\n{\n    s->phase = 0;\n    UI_Requester_Init(&s->req, 2, 2, true);\n}\n\nvoid UI_Pause_Free(UI_PAUSE_STATE *const s)\n{\n    UI_Requester_Free(&s->req);\n}\n\nUI_PAUSE_EXIT_CHOICE UI_Pause_Control(UI_PAUSE_STATE *const s)\n{\n    const int32_t choice = UI_Requester_Control(&s->req);\n    if (s->phase == 0) {\n        if (choice == UI_REQUESTER_CANCEL) {\n            return UI_PAUSE_RESUME_PAUSE;\n        } else if (choice == 0) {\n            return UI_PAUSE_EXIT_TO_GAME;\n        } else if (choice == 1) {\n            s->phase = 1;\n            UI_Requester_Free(&s->req);\n            UI_Requester_Init(&s->req, 2, 2, true);\n        }\n    } else {\n        if (choice == UI_REQUESTER_CANCEL) {\n            s->phase = 0;\n        } else if (choice == 0) {\n            return UI_PAUSE_EXIT_TO_TITLE;\n        } else if (choice == 1) {\n            return UI_PAUSE_EXIT_TO_GAME;\n        }\n    }\n    return UI_PAUSE_NOOP;\n}\n\nvoid UI_Pause(UI_PAUSE_STATE *const s)\n{\n    UI_BeginModal(0.5f, 1.0f);\n    UI_BeginPad(50.0f, 50.0f);\n    UI_BeginRequester(\n        &s->req,\n        s->phase == 0 ? GS(\"general/pause/exit_to_title\")\n                      : GS(\"general/pause/are_you_sure\"));\n\n    for (int32_t i = UI_Requester_GetFirstRow(&s->req);\n         i < UI_Requester_GetLastRow(&s->req); i++) {\n        UI_BeginRequesterRow(&s->req, i);\n        UI_BeginAnchor(0.5f, 0.5f);\n        UI_Label(GameString_Get(m_Options[s->phase][i]));\n        UI_EndAnchor();\n        UI_EndRequesterRow(&s->req, i);\n    }\n\n    UI_EndRequester(&s->req);\n    UI_EndPad();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/pause.h",
    "content": "#pragma once\n\n// A pause exit confirmation dialog.\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef struct {\n    int32_t phase;\n    UI_REQUESTER_STATE req;\n} UI_PAUSE_STATE;\n\ntypedef enum {\n    UI_PAUSE_NOOP,\n    UI_PAUSE_RESUME_PAUSE,\n    UI_PAUSE_EXIT_TO_GAME,\n    UI_PAUSE_EXIT_TO_TITLE,\n} UI_PAUSE_EXIT_CHOICE;\n\n// state functions\nvoid UI_Pause_Init(UI_PAUSE_STATE *s);\nUI_PAUSE_EXIT_CHOICE UI_Pause_Control(UI_PAUSE_STATE *s);\nvoid UI_Pause_Free(UI_PAUSE_STATE *s);\n\n// draw functions\nvoid UI_Pause(UI_PAUSE_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/photo_mode.c",
    "content": "#include <trx/game/ui/dialogs/photo_mode.h>\n\n#include <trx/config.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/lara/pose.h>\n#include <trx/game/output/draw.h>\n#include <trx/game/ui/elements/frame.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/scaler.h>\n\n#include <stdio.h>\n\nstatic bool M_HasIcon(const INPUT_ROLE role)\n{\n    return Input_GetKeyName(\n               g_Config.input.backend,\n               g_Config.input.layout[g_Config.input.backend], role, 0)\n        != nullptr;\n}\n\nstatic void M_Title(const PHOTO_MODE current_mode)\n{\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        .spacing = { .v = 8.0f },\n    });\n    switch (current_mode) {\n    case PHOTO_MODE_CAMERA:\n        UI_Label(GS(\"general/photo_mode/title_camera_pos\"));\n        break;\n    case PHOTO_MODE_LARA_POS:\n        UI_Label(GS(\"general/photo_mode/title_lara_pos\"));\n        break;\n    }\n    UI_Label(\"\\\\{input step_left}\\\\{input step_right}\");\n    UI_EndStack();\n}\n\nstatic void M_Inputs(const PHOTO_MODE current_mode)\n{\n    UI_Label(\n        \"\\\\{input camera_up}\\\\{input camera_down}\"\n        \"\\\\{input camera_forward}\\\\{input camera_back}\"\n        \"\\\\{input camera_left}\\\\{input camera_right}\");\n    UI_Label(\n        \"\\\\{input left}\\\\{input forward}\"\n        \"\\\\{input back}\\\\{input right}\");\n    UI_Label(\"\\\\{input slow}+\\\\{input camera_up}/\\\\{input camera_down}\");\n    UI_Label(\"\\\\{input roll}\");\n    UI_Label(\"\\\\{input look}\");\n\n    UI_Label(\"[\\\\{input slow}+]\\\\{input draw}\");\n    if (Lara_Pose_IsAvailable()) {\n        UI_Label(\"[\\\\{input slow}+]\\\\{input fly_cheat}\");\n    }\n    UI_Label(\"[\\\\{input slow}+]\\\\{input pause}\");\n    UI_Label(\"\\\\{input toggle_ui}\");\n    UI_Label(\"\\\\{input action}\");\n\n    if (M_HasIcon(INPUT_ROLE_TOGGLE_PHOTO_MODE)\n        && M_HasIcon(INPUT_ROLE_INVENTORY)) {\n        UI_Label(\"\\\\{input toggle_photo_mode}/\\\\{input option}\");\n    } else if (M_HasIcon(INPUT_ROLE_TOGGLE_PHOTO_MODE)) {\n        UI_Label(\"\\\\{input toggle_photo_mode}\");\n    } else if (M_HasIcon(INPUT_ROLE_INVENTORY)) {\n        UI_Label(\"\\\\{input option}\");\n    }\n}\n\nstatic void M_Actions(const PHOTO_MODE current_mode)\n{\n    switch (current_mode) {\n    case PHOTO_MODE_CAMERA:\n        UI_Label(GS(\"general/photo_mode/camera_move_prompt\"));\n        UI_Label(GS(\"general/photo_mode/camera_rotate_prompt\"));\n        UI_Label(GS(\"general/photo_mode/camera_roll_prompt\"));\n        UI_Label(GS(\"general/photo_mode/camera_rotate_90_prompt\"));\n        UI_Label(GS(\"general/photo_mode/camera_reset_prompt\"));\n        break;\n    case PHOTO_MODE_LARA_POS:\n        UI_Label(GS(\"general/photo_mode/lara_move_prompt\"));\n        UI_Label(GS(\"general/photo_mode/lara_rotate_prompt\"));\n        UI_Label(GS(\"general/photo_mode/lara_roll_prompt\"));\n        UI_Label(GS(\"general/photo_mode/lara_rotate_90_prompt\"));\n        UI_Label(GS(\"general/photo_mode/lara_reset_prompt\"));\n        break;\n    }\n    UI_Label(GS(\"general/photo_mode/fov_prompt\"));\n    if (Lara_Pose_IsAvailable()) {\n        UI_Label(GS(\"general/photo_mode/change_lara_pose\"));\n    }\n    UI_Label(GS(\"general/photo_mode/advance_frame\"));\n    UI_Label(GS(\"general/photo_mode/toggle_help\"));\n    UI_Label(GS(\"general/photo_mode/snap_prompt\"));\n    UI_Label(GS(\"general/misc/exit\"));\n}\n\nvoid UI_PhotoMode(const PHOTO_MODE current_mode)\n{\n    const int32_t frame_thickness =\n        (int32_t)(UI_Scaler_Calc(4.0f, UI_SCALER_TARGET_TEXT) + 0.5f);\n    Output_DrawPhotoModeFrame(frame_thickness);\n\n    if (!g_Config.ui.enable_photo_mode_ui) {\n        return;\n    }\n\n    UI_BeginModal(0.0f, 0.0f);\n    UI_BeginPad(8.0f, 8.0f);\n    UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND);\n    UI_BeginPad(8.0, 6.0);\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = 8.0f },\n    });\n\n    M_Title(current_mode);\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .spacing = { .h = 8.0f },\n    });\n\n    // Inputs column\n    UI_BeginStack(UI_STACK_VERTICAL);\n    M_Inputs(current_mode);\n    UI_EndStack();\n    UI_BeginStack(UI_STACK_VERTICAL);\n    M_Actions(current_mode);\n    UI_EndStack();\n\n    UI_EndStack();\n\n    UI_EndStack();\n    UI_EndPad();\n    UI_EndFrame();\n    UI_EndPad();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/photo_mode.h",
    "content": "#pragma once\n\n#include <trx/game/photo_mode.h>\n#include <trx/game/ui/common.h>\n\n// A photo mode tutorial dialog.\n\nvoid UI_PhotoMode(PHOTO_MODE current_mode);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/play_any_level.c",
    "content": "#include <trx/game/ui/dialogs/play_any_level.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/base_passport.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/offset.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n\ntypedef struct {\n    int32_t level_num;\n    const char *text;\n} M_ROW;\n\ntypedef struct UI_PLAY_ANY_LEVEL_DIALOG_STATE {\n    VECTOR *rows;\n    UI_REQUESTER_STATE req;\n} UI_PLAY_ANY_LEVEL_DIALOG_STATE;\n\nUI_PLAY_ANY_LEVEL_DIALOG_STATE *UI_PlayAnyLevelDialog_Init(void)\n{\n    UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s =\n        Memory_Alloc(sizeof(UI_PLAY_ANY_LEVEL_DIALOG_STATE));\n    s->rows = Vector_Create(sizeof(M_ROW));\n\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (level->type != GFL_GYM && level->type != GFL_DUMMY\n            && level->type != GFL_CURRENT) {\n            const M_ROW row = { .level_num = level->num, .text = level->title };\n            Vector_Add(s->rows, &row);\n        }\n    }\n\n    UI_BasePassportDialog_Init(&s->req, s->rows->count);\n    return s;\n}\n\nvoid UI_PlayAnyLevelDialog_Free(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s)\n{\n    Vector_Free(s->rows);\n    UI_Requester_Free(&s->req);\n    Memory_Free(s);\n}\n\nint32_t UI_PlayAnyLevelDialog_Control(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s)\n{\n    UI_BasePassportDialog_Control(&s->req);\n    const int32_t choice = UI_Requester_Control(&s->req);\n    switch (choice) {\n    case UI_REQUESTER_NO_CHOICE:\n        return UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE;\n    case UI_REQUESTER_CANCEL:\n        return UI_PLAY_ANY_LEVEL_CHOICE_CANCEL;\n    default:\n        return ((M_ROW *)Vector_Get(s->rows, choice))->level_num;\n    }\n}\n\nvoid UI_PlayAnyLevelDialog(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s)\n{\n    UI_BeginBasePassportDialog();\n    UI_BeginRequester(&s->req, GS(\"general/passport/select_level\"));\n\n    for (int32_t i = 0; i < s->rows->count; i++) {\n        if (UI_Requester_IsRowVisible(&s->req, i)) {\n            const M_ROW *const row = Vector_Get(s->rows, i);\n            UI_BeginRequesterRow(&s->req, i);\n            UI_BeginAnchor(0.5f, 0.5f);\n            UI_Label(row->text);\n            UI_EndAnchor();\n            UI_EndRequesterRow(&s->req, i);\n        }\n    }\n\n    UI_EndRequester(&s->req);\n    UI_EndBasePassportDialog();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/play_any_level.h",
    "content": "// UI dialog for selecting a level within a save slot\n\n#pragma once\n\n#include <trx/game/ui/common.h>\n\n#include <stdint.h>\n\n#define UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE UI_REQUESTER_NO_CHOICE\n#define UI_PLAY_ANY_LEVEL_CHOICE_CANCEL UI_REQUESTER_CANCEL\n\ntypedef struct UI_PLAY_ANY_LEVEL_DIALOG_STATE UI_PLAY_ANY_LEVEL_DIALOG_STATE;\n\nstruct UI_PLAY_ANY_LEVEL_DIALOG_STATE *UI_PlayAnyLevelDialog_Init(void);\n\nvoid UI_PlayAnyLevelDialog_Free(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s);\n\nint32_t UI_PlayAnyLevelDialog_Control(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s);\n\nvoid UI_PlayAnyLevelDialog(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/save_slot.c",
    "content": "#include <trx/game/ui/dialogs/save_slot.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/inventory.h>\n#include <trx/game/savegame.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/base_passport.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/offset.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/progress_button.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/elements/window.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/viewport.h>\n#include <trx/version.h>\n\n#define M_IMMEDIATE (g_TRVersion >= 2)\n\ntypedef enum {\n    M_PHASE_BROWSE,\n    M_PHASE_CONFIRM_DELETE,\n} M_PHASE;\n\ntypedef struct UI_SAVE_SLOT_DIALOG_STATE {\n    UI_SAVE_SLOT_DIALOG_TYPE type;\n    SAVEGAME_SLOT_REF *rows;\n    int32_t row_count;\n    UI_REQUESTER_STATE req;\n    UI_REQUESTER_STATE confirm_req;\n    UI_PROGRESS_BUTTON_STATE *delete_button;\n    M_PHASE phase;\n    int32_t pending_delete_row;\n    int32_t last_selected_row;\n} UI_SAVE_SLOT_DIALOG_STATE;\n\nstatic const GAME_STRING_ID m_DeleteConfirmOptions[2] = {\n    GS_ID(\"general/passport/delete_save_yes\"),\n    GS_ID(\"general/passport/delete_save_no\"),\n};\n\nstatic void M_NonEmptySlot(\n    const UI_SAVE_SLOT_DIALOG_STATE *const s, const SAVEGAME_SLOT_REF slot,\n    const SAVEGAME_INFO *const info)\n{\n    if (g_TRVersion == 1) {\n        UI_BeginAnchor(0.5f, 0.5f);\n        UI_BeginStack(UI_STACK_HORIZONTAL);\n    } else {\n        UI_BeginStackEx((UI_STACK_SETTINGS) {\n            .orientation = UI_STACK_HORIZONTAL,\n            .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        });\n    }\n\n    // Level title with the save counter\n    UI_Label(info->level_title);\n    if (info->counter > 0) {\n        UI_Spacer(8.0f, 0.0f);\n        if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) {\n            UI_BeginStackEx((UI_STACK_SETTINGS) {\n                .orientation = UI_STACK_HORIZONTAL,\n                .align = { .h = UI_STACK_H_ALIGN_RIGHT },\n            });\n            if (g_TRVersion != 1) {\n                UI_Label(\"QS \");\n            }\n        }\n        UI_LabelFmt(\"%d\", info->counter);\n        if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) {\n            if (g_TRVersion == 1) {\n                UI_Label(\" (QS)\");\n            }\n            UI_EndStack();\n        }\n    }\n\n    UI_EndStack();\n    if (g_TRVersion == 1) {\n        UI_EndAnchor();\n    }\n}\n\nstatic void M_EmptySlot(\n    const UI_SAVE_SLOT_DIALOG_STATE *const s, const SAVEGAME_SLOT_REF slot)\n{\n    UI_BeginAnchor(0.5f, 0.5f);\n    if (slot.pool == SAVEGAME_SLOT_POOL_QUICK) {\n        UI_LabelFmt(\n            \"[Q%d] %s\", slot.index + 1, GS(\"general/misc/empty_slot_fmt\"));\n    } else {\n        UI_LabelFmt(GS(\"general/misc/empty_slot_fmt\"), slot.index + 1);\n    }\n    UI_EndAnchor();\n}\n\nstatic void M_ConfirmDeleteDialog(const UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    UI_BeginModal(0.5f, g_Inv_Mode == INV_TITLE_MODE ? 0.69f : 0.55f);\n    UI_BeginPad(50.0f, 50.0f);\n    UI_BeginWindow((UI_WINDOW_SETTINGS) {\n        .title = GS(\"general/passport/delete_save_confirm\"),\n        .scrollable = nullptr,\n        .title_spacing = -1.0f,\n        .heavy = true,\n    });\n    for (int32_t i = UI_Requester_GetFirstRow(&s->confirm_req);\n         i < UI_Requester_GetLastRow(&s->confirm_req); i++) {\n        UI_BeginRequesterRow(&s->confirm_req, i);\n        UI_BeginAnchor(0.5f, 0.5f);\n        UI_Label(GameString_Get(m_DeleteConfirmOptions[i]));\n        UI_EndAnchor();\n        UI_EndRequesterRow(&s->confirm_req, i);\n    }\n    UI_EndWindow();\n    UI_EndPad();\n    UI_EndModal();\n}\n\nstatic int32_t M_GetTotalSlots(void)\n{\n    return Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_QUICK)\n        + Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL);\n}\n\nstatic SAVEGAME_SLOT_REF M_MapRowToSlot(\n    const UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t row)\n{\n    ASSERT(s != nullptr);\n    if (row < 0 || row >= s->row_count || s->rows == nullptr) {\n        return Savegame_InvalidSlot();\n    }\n    return s->rows[row];\n}\n\nstatic void M_BuildRows(UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    const int32_t max_row_count = MAX(M_GetTotalSlots(), 1);\n    s->rows = Memory_Alloc(sizeof(SAVEGAME_SLOT_REF) * max_row_count);\n    s->row_count = 0;\n\n    const int32_t quick_visual_count = Savegame_GetQuickVisualCount();\n    for (int32_t i = 0; i < quick_visual_count; i++) {\n        const SAVEGAME_SLOT_REF slot = Savegame_QuickFromVisualIndex(i);\n        if (Savegame_IsValidSlotRef(slot)) {\n            s->rows[s->row_count++] = slot;\n        }\n    }\n\n    const int32_t normal_slot_count =\n        Savegame_GetSlotCount(SAVEGAME_SLOT_POOL_NORMAL);\n    for (int32_t i = 0; i < normal_slot_count; i++) {\n        s->rows[s->row_count++] = Savegame_NormalSlot(i);\n    }\n\n    if (s->row_count == 0) {\n        s->rows[s->row_count++] = Savegame_InvalidSlot();\n    }\n}\n\nstatic void M_RebuildRows(\n    UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t focused_row)\n{\n    int32_t selected_row = focused_row;\n    UI_Requester_Free(&s->req);\n    Memory_FreePointer(&s->rows);\n    M_BuildRows(s);\n    UI_BasePassportDialog_Init(&s->req, s->row_count);\n    CLAMP(selected_row, 0, s->row_count - 1);\n    UI_Requester_SelectRow(&s->req, selected_row);\n}\n\nstatic bool M_IsSlotDeletable(const SAVEGAME_SLOT_REF slot)\n{\n    return Savegame_IsValidSlotRef(slot) && !Savegame_IsSlotFree(slot);\n}\n\nstatic void M_BeginDeleteConfirmButton(void *const arg)\n{\n    UI_SAVE_SLOT_DIALOG_STATE *const s = arg;\n    const SAVEGAME_SLOT_REF slot =\n        M_MapRowToSlot(s, UI_Requester_GetCurrentRow(&s->req));\n    if (!M_IsSlotDeletable(slot)) {\n        return;\n    }\n    s->phase = M_PHASE_CONFIRM_DELETE;\n    s->pending_delete_row = UI_Requester_GetCurrentRow(&s->req);\n    UI_Requester_Init(&s->confirm_req, 2, 2, true);\n}\n\nstatic void M_ResetDeleteState(UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    s->phase = M_PHASE_BROWSE;\n    s->pending_delete_row = -1;\n}\n\nstatic void M_ResetDeleteButton(UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    if (s->delete_button != nullptr) {\n        UI_ProgressButton_Free(s->delete_button);\n    }\n    s->delete_button = UI_ProgressButton_Init(\n        g_Config.input.backend, INPUT_ROLE_UNBIND_KEY,\n        GS_ID(\"general/passport/delete_save\"), M_BeginDeleteConfirmButton, s);\n}\n\nUI_SAVE_SLOT_DIALOG_STATE *UI_SaveSlotDialog_Init(\n    const UI_SAVE_SLOT_DIALOG_TYPE type, const SAVEGAME_SLOT_REF initial_slot)\n{\n    UI_SAVE_SLOT_DIALOG_STATE *const s =\n        Memory_Alloc(sizeof(UI_SAVE_SLOT_DIALOG_STATE));\n    s->type = type;\n    M_BuildRows(s);\n    M_ResetDeleteState(s);\n    s->delete_button = nullptr;\n    s->last_selected_row = -1;\n\n    int32_t initial_row = 0;\n    if (Savegame_IsValidSlotRef(initial_slot)) {\n        for (int32_t i = 0; i < s->row_count; i++) {\n            if (s->rows[i].pool == initial_slot.pool\n                && s->rows[i].index == initial_slot.index) {\n                initial_row = i;\n                break;\n            }\n        }\n    }\n    UI_BasePassportDialog_Init(&s->req, s->row_count);\n    UI_Requester_SelectRow(&s->req, initial_row);\n    s->last_selected_row = initial_row;\n    M_ResetDeleteButton(s);\n    return s;\n}\n\nvoid UI_SaveSlotDialog_Free(UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    UI_Requester_Free(&s->req);\n    UI_Requester_Free(&s->confirm_req);\n    if (s->delete_button != nullptr) {\n        UI_ProgressButton_Free(s->delete_button);\n        s->delete_button = nullptr;\n    }\n    Memory_FreePointer(&s->rows);\n    Memory_Free(s);\n}\n\nUI_SAVE_SLOT_DIALOG_CHOICE UI_SaveSlotDialog_Control(\n    UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    if (s->phase == M_PHASE_CONFIRM_DELETE) {\n        const int32_t choice = UI_Requester_Control(&s->confirm_req);\n        if (choice == UI_REQUESTER_CANCEL || choice == 1) {\n            M_ResetDeleteState(s);\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n            return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n                .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE,\n            };\n        }\n        if (choice == 0) {\n            const SAVEGAME_SLOT_REF slot =\n                M_MapRowToSlot(s, s->pending_delete_row);\n            const int32_t focused_row = s->pending_delete_row;\n            M_ResetDeleteState(s);\n            g_Input = (INPUT_STATE) {};\n            g_InputDB = (INPUT_STATE) {};\n            if (!Savegame_Delete(slot)) {\n                return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n                    .action = UI_SAVE_SLOT_DIALOG_DELETE_FAILED,\n                };\n            }\n            Savegame_ScanSavedGames();\n            M_RebuildRows(s, focused_row);\n        }\n        return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n            .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE,\n        };\n    }\n\n    UI_BasePassportDialog_Control(&s->req);\n    const int32_t sel_row = UI_Requester_GetCurrentRow(&s->req);\n    if (sel_row != s->last_selected_row) {\n        s->last_selected_row = sel_row;\n        M_ResetDeleteButton(s);\n    }\n    if (M_IsSlotDeletable(M_MapRowToSlot(s, sel_row))) {\n        UI_ProgressButton_Control(s->delete_button);\n    }\n    const int32_t choice = UI_Requester_Control(&s->req);\n    if (choice == UI_REQUESTER_CANCEL) {\n        return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n            .action = UI_SAVE_SLOT_DIALOG_CANCEL,\n        };\n    }\n    if (choice != UI_REQUESTER_NO_CHOICE) {\n        const SAVEGAME_SLOT_REF slot = M_MapRowToSlot(s, sel_row);\n        if (!Savegame_IsValidSlotRef(slot)) {\n            return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n                .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE,\n            };\n        }\n        const bool is_valid_save_target =\n            s->type == UI_SAVE_SLOT_DIALOG_SAVE_GAME\n            && slot.pool == SAVEGAME_SLOT_POOL_NORMAL;\n        const bool is_valid_load_target =\n            s->type == UI_SAVE_SLOT_DIALOG_LOAD_GAME\n            && !Savegame_IsSlotFree(slot);\n        const bool is_valid_generic_target =\n            s->type == UI_SAVE_SLOT_DIALOG_GENERIC\n            && !Savegame_IsSlotFree(slot);\n\n        if (is_valid_save_target || is_valid_load_target\n            || is_valid_generic_target) {\n            return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n                .action = UI_SAVE_SLOT_DIALOG_CONFIRM,\n                .slot = slot,\n            };\n        }\n    }\n    return (UI_SAVE_SLOT_DIALOG_CHOICE) {\n        .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE,\n    };\n}\n\nvoid UI_SaveSlotDialog(const UI_SAVE_SLOT_DIALOG_STATE *const s)\n{\n    const SAVEGAME_SLOT_REF selected_slot =\n        M_MapRowToSlot(s, UI_Requester_GetCurrentRow(&s->req));\n    const bool can_delete = M_IsSlotDeletable(selected_slot);\n\n    UI_BeginBasePassportDialog();\n    const char *title = nullptr;\n    switch (s->type) {\n    case UI_SAVE_SLOT_DIALOG_SAVE_GAME:\n        title = GS(\"general/passport/save_game\");\n        break;\n    case UI_SAVE_SLOT_DIALOG_LOAD_GAME:\n        title = GS(\"general/passport/load_game\");\n        break;\n    case UI_SAVE_SLOT_DIALOG_GENERIC:\n        title = GS(\"general/passport/select_save\");\n        break;\n    }\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = 3.0f },\n    });\n    UI_BeginRequester(&s->req, title);\n\n    const int32_t first = UI_Requester_GetFirstRow(&s->req);\n    const int32_t last = UI_Requester_GetLastRow(&s->req);\n    for (int32_t i = first; i < last; ++i) {\n        UI_BeginRequesterRow(&s->req, i);\n        const SAVEGAME_SLOT_REF slot = M_MapRowToSlot(s, i);\n        const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(slot);\n        if (Savegame_IsValidSlotRef(slot) && info != nullptr\n            && info->level_title != nullptr) {\n            M_NonEmptySlot(s, slot, info);\n        } else {\n            M_EmptySlot(s, slot);\n        }\n        UI_EndRequesterRow(&s->req, i);\n    }\n\n    UI_EndRequester(&s->req);\n\n    UI_BeginHide(s->phase != M_PHASE_BROWSE || !can_delete);\n    UI_BeginAnchor(1.0f, 0.5f);\n    UI_ProgressButton(s->delete_button);\n    UI_EndAnchor();\n    UI_EndHide();\n    UI_EndStack();\n\n    UI_EndBasePassportDialog();\n\n    if (s->phase == M_PHASE_CONFIRM_DELETE) {\n        M_ConfirmDeleteDialog(s);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/save_slot.h",
    "content": "// UI dialog for selecting a save slot (load or save game)\n\n#pragma once\n\n#include <trx/game/savegame/types.h>\n#include <trx/game/ui/common.h>\n\ntypedef enum {\n    UI_SAVE_SLOT_DIALOG_LOAD_GAME,\n    UI_SAVE_SLOT_DIALOG_SAVE_GAME,\n    UI_SAVE_SLOT_DIALOG_GENERIC,\n} UI_SAVE_SLOT_DIALOG_TYPE;\n\ntypedef enum {\n    UI_SAVE_SLOT_DIALOG_NO_CHOICE,\n    UI_SAVE_SLOT_DIALOG_CANCEL,\n    UI_SAVE_SLOT_DIALOG_CONFIRM,\n    UI_SAVE_SLOT_DIALOG_DELETE_FAILED,\n} UI_SAVE_SLOT_DIALOG_ACTION;\n\ntypedef struct {\n    UI_SAVE_SLOT_DIALOG_ACTION action;\n    SAVEGAME_SLOT_REF slot;\n} UI_SAVE_SLOT_DIALOG_CHOICE;\n\ntypedef struct UI_SAVE_SLOT_DIALOG_STATE UI_SAVE_SLOT_DIALOG_STATE;\n\n// state functions\nstruct UI_SAVE_SLOT_DIALOG_STATE *UI_SaveSlotDialog_Init(\n    UI_SAVE_SLOT_DIALOG_TYPE type, SAVEGAME_SLOT_REF initial_slot);\nvoid UI_SaveSlotDialog_Free(struct UI_SAVE_SLOT_DIALOG_STATE *s);\nUI_SAVE_SLOT_DIALOG_CHOICE UI_SaveSlotDialog_Control(\n    struct UI_SAVE_SLOT_DIALOG_STATE *s);\n\n// draw functions\nvoid UI_SaveSlotDialog(const struct UI_SAVE_SLOT_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/select_level.c",
    "content": "#include <trx/game/ui/dialogs/select_level.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/inventory.h>\n#include <trx/game/savegame.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/base_passport.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/offset.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n\ntypedef struct {\n    const char *const text;\n    const GF_LEVEL *const level;\n} M_ROW;\n\ntypedef struct UI_SELECT_LEVEL_DIALOG_STATE {\n    SAVEGAME_SLOT_REF save_slot;\n    VECTOR *rows;\n    UI_REQUESTER_STATE req;\n} UI_SELECT_LEVEL_DIALOG_STATE;\n\nUI_SELECT_LEVEL_DIALOG_STATE *UI_SelectLevelDialog_Init(\n    const SAVEGAME_SLOT_REF save_slot)\n{\n    UI_SELECT_LEVEL_DIALOG_STATE *const s =\n        Memory_Alloc(sizeof(UI_SELECT_LEVEL_DIALOG_STATE));\n    s->save_slot = save_slot;\n    s->rows = Vector_Create(sizeof(M_ROW));\n\n    const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(save_slot);\n    ASSERT(info != nullptr);\n    ASSERT(info->features.select_level);\n\n    Savegame_LoadOnlyResumeInfo(save_slot);\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    for (int32_t i = 0; i <= info->level_num && i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        if (resume != nullptr && resume->flags.available\n            && level->type != GFL_GYM) {\n            Vector_Add(\n                s->rows,\n                &(M_ROW) {\n                    .text = level_table->levels[i].title,\n                    .level = level,\n                });\n        }\n    }\n\n    UI_BasePassportDialog_Init(&s->req, s->rows->count);\n    return s;\n}\n\nvoid UI_SelectLevelDialog_Free(UI_SELECT_LEVEL_DIALOG_STATE *const s)\n{\n    Vector_Free(s->rows);\n    UI_Requester_Free(&s->req);\n    Memory_Free(s);\n}\n\nint32_t UI_SelectLevelDialog_Control(UI_SELECT_LEVEL_DIALOG_STATE *const s)\n{\n    UI_BasePassportDialog_Control(&s->req);\n    const int32_t choice = UI_Requester_Control(&s->req);\n    if (choice == UI_REQUESTER_NO_CHOICE || choice == UI_REQUESTER_CANCEL) {\n        return choice;\n    }\n    const M_ROW *const row = Vector_Get(s->rows, choice);\n    return row->level->num;\n}\n\nvoid UI_SelectLevelDialog(UI_SELECT_LEVEL_DIALOG_STATE *const s)\n{\n    UI_BeginBasePassportDialog();\n    UI_BeginRequester(&s->req, GS(\"general/passport/select_level\"));\n\n    const SAVEGAME_INFO *info = Savegame_GetSavegameInfo(s->save_slot);\n    for (int32_t i = 0; i < s->rows->count; i++) {\n        if (UI_Requester_IsRowVisible(&s->req, i)) {\n            const M_ROW *const row = Vector_Get(s->rows, i);\n            UI_BeginRequesterRow(&s->req, i);\n            UI_BeginAnchor(0.5f, 0.5f);\n            UI_Label(row->text);\n            UI_EndAnchor();\n            UI_EndRequesterRow(&s->req, i);\n        }\n    }\n\n    UI_EndRequester(&s->req);\n    UI_EndBasePassportDialog();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/select_level.h",
    "content": "// UI dialog for selecting a level within a save slot\n\n#pragma once\n\n#include <trx/game/savegame/types.h>\n#include <trx/game/ui/common.h>\n\n#include <stdint.h>\n\n#define UI_SELECT_LEVEL_CHOICE_NOOP -1\n#define UI_SELECT_LEVEL_CHOICE_CANCEL -2\n#define UI_SELECT_LEVEL_CHOICE_PLAY_STORY_SO_FAR -3\n\ntypedef struct UI_SELECT_LEVEL_DIALOG_STATE UI_SELECT_LEVEL_DIALOG_STATE;\n\nstruct UI_SELECT_LEVEL_DIALOG_STATE *UI_SelectLevelDialog_Init(\n    SAVEGAME_SLOT_REF save_slot);\n\nvoid UI_SelectLevelDialog_Free(struct UI_SELECT_LEVEL_DIALOG_STATE *s);\n\nint32_t UI_SelectLevelDialog_Control(struct UI_SELECT_LEVEL_DIALOG_STATE *s);\n\nvoid UI_SelectLevelDialog(struct UI_SELECT_LEVEL_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_helpers/enums.c",
    "content": "#include <trx/game/ui/dialogs/setting_helpers/enums.h>\n\n#include <trx/config.h>\n#include <trx/game/lara/const.h>\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_StatsStyleEnumEntries[] = {\n    { STATS_STYLE_BARE },\n    { STATS_STYLE_BORDERED },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_TargetModeEnumEntries[] = {\n    { TARGET_LOCK_MODE_FULL },\n    { TARGET_LOCK_MODE_SEMI },\n    { TARGET_LOCK_MODE_NONE },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_LookModeEnumEntries[] = {\n    { LOOK_MODE_RESTRICTED },\n    { LOOK_MODE_ENHANCED },\n    { LOOK_MODE_UNRESTRICTED },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_QuickGunsModeEnumEntries[] = {\n    { QUICK_GUNS_MODE_DRAW_ONLY },\n    { QUICK_GUNS_MODE_DRAW_AND_HOLSTER },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_JumpLockModeEnumEntries[] = {\n    { JUMP_LOCK_LEGACY },\n    { JUMP_LOCK_TUNED },\n    { JUMP_LOCK_DISABLED },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_WallGlitchEnumEntries[] = {\n    { WALL_GLITCH_FIXED },\n    { WALL_GLITCH_TR1 },\n    { WALL_GLITCH_TR2 },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_EnemyHealthBarShowModeEnumEntries[] = {\n    { BAR_SHOW_MODE_NEVER },\n    { BAR_SHOW_MODE_BOSS_ONLY },\n    { BAR_SHOW_MODE_ALWAYS },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_UIElementLocationEnumEntries[] = {\n    { UI_ELEMENT_LOCATION_TOP_LEFT },\n    { UI_ELEMENT_LOCATION_TOP_CENTER },\n    { UI_ELEMENT_LOCATION_TOP_RIGHT },\n    { UI_ELEMENT_LOCATION_BOTTOM_LEFT },\n    { UI_ELEMENT_LOCATION_BOTTOM_CENTER },\n    { UI_ELEMENT_LOCATION_BOTTOM_RIGHT },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_BackgroundStyleEnumEntries[] = {\n    { BK_NONE },\n    { BK_TRANSPARENT_MEDIUM },\n    { BK_TRANSPARENT_DARK },\n    { BK_BLACK },\n    { BK_PATTERN_STATIC },\n    { BK_PATTERN_WAVE },\n    { BK_MONOCHROME },\n    { BK_MONOCHROME_COOL },\n    { BK_MONOCHROME_WARM },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_CameraModeEnumEntries[] = {\n    { CAMERA_MODE_TR1 },\n    { CAMERA_MODE_TR2 },\n    { CAMERA_MODE_TR3 },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_TextureFilterEnumEntries[] = {\n    { TEXTURE_FILTER_POINT },\n    { TEXTURE_FILTER_BILINEAR },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_MenuStyleEnumEntries[] = {\n    { UI_STYLE_PS1 },\n    { UI_STYLE_PC },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_LightingContrastEnumEntries[] = {\n    { LIGHTING_CONTRAST_LOW },\n    { LIGHTING_CONTRAST_MEDIUM },\n    { LIGHTING_CONTRAST_HIGH },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_SpriteLockModeEnumEntries[] = {\n    { BILLBOARD_LOCK_NONE },\n    { BILLBOARD_LOCK_ROLL },\n    { BILLBOARD_LOCK_ROLL_PITCH },\n    { BILLBOARD_LOCK_PERSPECTIVE },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_AspectModeEnumEntries[] = {\n    { ASPECT_MODE_4_3 },\n    { ASPECT_MODE_16_9 },\n    { ASPECT_MODE_16_10 },\n    { ASPECT_MODE_ANY },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_ScreenshotFormatEnumEntries[] = {\n    { SCREENSHOT_FORMAT_JPEG },\n    { SCREENSHOT_FORMAT_PNG },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_AllyHostilityPolicyEnumEntries[] = {\n    { ALLY_HOSTILITY_POLICY_INDIVIDUAL },\n    { ALLY_HOSTILITY_POLICY_SHARED },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_CreatureDrownPolicyEnumEntries[] = {\n    { CREATURE_DROWN_POLICY_NEVER },\n    { CREATURE_DROWN_POLICY_DEFAULT },\n    { CREATURE_DROWN_POLICY_SUBMERGED },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_ProjectileAreaDamageEnumEntries[] = {\n    { PROJECTILE_AREA_DAMAGE_SINGLE_SWEEP },\n    { PROJECTILE_AREA_DAMAGE_MULTI_SWEEP },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_LoadingScreensModeEnumEntries[] = {\n    { LOADING_SCREENS_DISABLED },\n    { LOADING_SCREENS_ALWAYS },\n    { LOADING_SCREENS_NEW_GAMES },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_MusicLoadConditionEnumEntries[] = {\n    { MUSIC_LOAD_CONDITION_NEVER },\n    { MUSIC_LOAD_CONDITION_NON_AMBIENT },\n    { MUSIC_LOAD_CONDITION_ALWAYS },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_ShadowTypeEnumEntries[] = {\n    { SHADOW_TYPE_OCTAGON },\n    { SHADOW_TYPE_CIRCLE },\n    { SHADOW_TYPE_SPRITE },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_BloodEffectsEnumEntries[] = {\n    { BLOOD_EFFECTS_DISABLED },\n    { BLOOD_EFFECTS_PINK },\n    { BLOOD_EFFECTS_RED },\n    { -1 },\n};\n\nconst UI_SETTINGS_ENUM_ENTRY UI_Settings_SunglassesModeEnumEntries[] = {\n    { SUNGLASSES_MODE_OFF },\n    { SUNGLASSES_MODE_OPAQUE },\n    { SUNGLASSES_MODE_TRANSPARENT },\n    { -1 },\n};\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_helpers/enums.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct {\n    int32_t value;\n} UI_SETTINGS_ENUM_ENTRY;\n\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_TargetModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_LookModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_QuickGunsModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_JumpLockModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_WallGlitchEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_StatsStyleEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY\n    UI_Settings_EnemyHealthBarShowModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_UIElementLocationEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_BackgroundStyleEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_CameraModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_FOVModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_TextureFilterEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_MenuStyleEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_LightingContrastEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_SpriteLockModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_AspectModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_ScreenshotFormatEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY\n    UI_Settings_AllyHostilityPolicyEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY\n    UI_Settings_CreatureDrownPolicyEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_LoadingScreensModeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_MusicLoadConditionEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_ShadowTypeEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_BloodEffectsEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY\n    UI_Settings_ProjectileAreaDamageEnumEntries[];\nextern const UI_SETTINGS_ENUM_ENTRY UI_Settings_SunglassesModeEnumEntries[];\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_helpers/handlers.c",
    "content": "#include <trx/game/ui/dialogs/setting_helpers/handlers.h>\n\n#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/game_flow/common.h>\n#include <trx/game/gun.h>\n#include <trx/game/music.h>\n#include <trx/game/objects/common.h>\n#include <trx/game/sound.h>\n#include <trx/game/stats.h>\n#include <trx/game/ui/dialogs/settings_editor.h>\n#include <trx/game/ui/settings.h>\n#include <trx/version.h>\n\nbool UI_Settings_EnablePS1Crystals_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.enable_save_crystals;\n}\n\nbool UI_Settings_ShowCrystals_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    if (!Stats_GameHasCrystals()) {\n        return false;\n    }\n    if (g_TRVersion <= 2 && !g_Config.gameplay.enable_save_crystals) {\n        return false;\n    }\n    return true;\n}\n\nbool UI_Settings_EnableFadeEffects_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.visuals.enable_fade_effects;\n}\n\nbool UI_Settings_FogColor_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return !g_Config.visuals.fog_transparency;\n}\n\nbool UI_Settings_EnableBreeze_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.visuals.enable_braid || g_TRVersion == 3;\n}\n\nbool UI_Settings_ResponsiveJumping_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.enable_tr2_jumping;\n}\n\nbool UI_Settings_Crawl_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.enable_crawling;\n}\n\nbool UI_Settings_Sprint_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.enable_sprint;\n}\n\nbool UI_Settings_Bar_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.ui.show_bars;\n}\n\nbool UI_Settings_Healthbar_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Bar_IsAvailable(option);\n}\n\nbool UI_Settings_Airbar_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Bar_IsAvailable(option);\n}\n\nbool UI_Settings_Sprintbar_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Sprint_IsAvailable(option)\n        && UI_Settings_Bar_IsAvailable(option);\n}\n\nbool UI_Settings_Exposurebar_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Bar_IsAvailable(option);\n}\n\nbool UI_Settings_EnemyHealthbar_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Bar_IsAvailable(option)\n        && g_Config.ui.enemy_health_bar.show_mode != BAR_SHOW_MODE_NEVER;\n}\n\nbool UI_Settings_AllyHealthbar_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Bar_IsAvailable(option)\n        && g_Config.ui.enemy_health_bar.show_mode == BAR_SHOW_MODE_ALWAYS\n        && g_Config.gameplay.enable_ally_targeting;\n}\n\nbool UI_Settings_HealthbarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Healthbar_IsAvailable(option);\n}\n\nbool UI_Settings_AirbarColor_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Airbar_IsAvailable(option);\n}\n\nbool UI_Settings_SprintbarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Sprintbar_IsAvailable(option);\n}\n\nbool UI_Settings_ExposurebarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_Exposurebar_IsAvailable(option);\n}\n\nbool UI_Settings_EnemyHealthbarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_EnemyHealthbar_IsAvailable(option);\n}\n\nbool UI_Settings_AllyHealthbarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_AllyHealthbar_IsAvailable(option);\n}\n\nbool UI_Settings_BarColorPC_IsVisible(const UI_SETTINGS_OPTION *const option)\n{\n    return !UI_Settings_IsCurrentBarLookPS1();\n}\n\nbool UI_Settings_BarColorPS1_IsVisible(const UI_SETTINGS_OPTION *const option)\n{\n    return UI_Settings_IsCurrentBarLookPS1();\n}\n\nbool UI_Settings_IdlePose_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.idle_pose_timeout > 0;\n}\n\nconst char *UI_Settings_ColorEditor_FormatValue(\n    const UI_SETTINGS_OPTION *const option)\n{\n    const RGB_888 *const color = option->target;\n    return String_FormatStatic(\"#%02X%02X%02X\", color->r, color->g, color->b);\n}\n\nbool UI_Settings_ColorEditor_CanChangeValue(\n    const UI_SETTINGS_OPTION *const option, const int32_t dir)\n{\n    return false;\n}\n\nbool UI_Settings_FixItemRots_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.visuals.enable_3d_pickups;\n}\n\nbool UI_Settings_FixStepGlitch_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.enable_smooth_wall_deflect;\n}\n\nbool UI_Settings_FixWadeWallHit_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.gameplay.enable_wading;\n}\n\nbool UI_Settings_PauseMusicInInventory_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Config.audio.enable_music_in_inventory;\n}\n\nbool UI_Settings_BackgroundStyle_IsEnumValueAvailable(\n    const UI_SETTINGS_OPTION *const option, const int32_t value)\n{\n    if (value == BK_PATTERN_STATIC || value == BK_PATTERN_WAVE) {\n        return Object_Get(O_INV_BACKGROUND)->loaded;\n    }\n    return true;\n}\n\nbool UI_Settings_ShadowType_IsEnumValueAvailable(\n    const UI_SETTINGS_OPTION *const option, const int32_t value)\n{\n    if (value == SHADOW_TYPE_SPRITE) {\n        return Object_Get(O_SHADOW)->loaded;\n    }\n    return true;\n}\n\nbool UI_Settings_Volume_RequestChange(\n    const UI_SETTINGS_OPTION *const option, const int32_t dir)\n{\n    UI_SettingsEditor_RequestChange(option, dir);\n    if (option->target == &g_Config.audio.music_volume) {\n        Music_SetVolume(g_Config.audio.music_volume);\n    } else if (option->target == &g_Config.audio.sound_volume) {\n        Sound_SetMasterVolume(g_Config.audio.sound_volume);\n    }\n    Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS);\n    return true;\n}\n\nbool UI_Settings_Flare_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Weapons[LGT_FLARE].is_available;\n}\n\nbool UI_Settings_Grenade_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Weapons[LGT_GRENADE].is_available;\n}\n\nbool UI_Settings_Harpoon_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Weapons[LGT_HARPOON].is_available;\n}\n\nbool UI_Settings_M16_IsAvailable(const UI_SETTINGS_OPTION *const option)\n{\n    return g_Weapons[LGT_M16].is_available || g_Weapons[LGT_MP5].is_available;\n}\n\nbool UI_Settings_ProjectileAreaDamage_IsAvailable(\n    const UI_SETTINGS_OPTION *const option)\n{\n    return g_Weapons[LGT_ROCKET].is_available\n        || g_Weapons[LGT_GRENADE].is_available;\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_helpers/handlers.h",
    "content": "#pragma once\n\n#include <trx/game/ui/dialogs/settings.h>\n\nbool UI_Settings_EnablePS1Crystals_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\nbool UI_Settings_ShowCrystals_IsAvailable(const UI_SETTINGS_OPTION *option);\n\nbool UI_Settings_FixItemRots_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_FixStepGlitch_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_FixWadeWallHit_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_EnableFadeEffects_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\nbool UI_Settings_PauseMusicInInventory_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\n\nbool UI_Settings_BackgroundStyle_IsEnumValueAvailable(\n    const UI_SETTINGS_OPTION *option, int32_t value);\n\nbool UI_Settings_ShadowType_IsEnumValueAvailable(\n    const UI_SETTINGS_OPTION *option, int32_t value);\n\nbool UI_Settings_FogColor_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_EnableBreeze_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_ResponsiveJumping_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Bar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Crawl_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Sprint_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Healthbar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Airbar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Sprintbar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Exposurebar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_EnemyHealthbar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_AllyHealthbar_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_HealthbarColor_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_AirbarColor_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_SprintbarColor_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_ExposurebarColor_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_EnemyHealthbarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\nbool UI_Settings_AllyHealthbarColor_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\nbool UI_Settings_BarColorPC_IsVisible(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_BarColorPS1_IsVisible(const UI_SETTINGS_OPTION *option);\n\nbool UI_Settings_IdlePose_IsAvailable(const UI_SETTINGS_OPTION *option);\n\nconst char *UI_Settings_ColorEditor_FormatValue(\n    const UI_SETTINGS_OPTION *option);\nbool UI_Settings_ColorEditor_CanChangeValue(\n    const UI_SETTINGS_OPTION *option, int32_t dir);\n\nconst char *UI_Settings_Language_FormatValue(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Language_CanChangeValue(\n    const UI_SETTINGS_OPTION *option, int32_t dir);\nbool UI_Settings_Language_RequestChangeValue(\n    const UI_SETTINGS_OPTION *option, int32_t dir);\n\nbool UI_Settings_Volume_RequestChange(\n    const UI_SETTINGS_OPTION *option, int32_t dir);\n\nbool UI_Settings_Flare_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Grenade_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_Harpoon_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_M16_IsAvailable(const UI_SETTINGS_OPTION *option);\nbool UI_Settings_ProjectileAreaDamage_IsAvailable(\n    const UI_SETTINGS_OPTION *option);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_helpers/handlers_language.c",
    "content": "#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/game_strings/manager.h>\n#include <trx/game/ui/dialogs/setting_helpers/handlers.h>\n\n#include <stdlib.h>\n\nstatic VECTOR *m_Languages = nullptr;\n\nstatic void M_Language_Cleanup(void)\n{\n    // Free the languages vector and its strings.\n    if (m_Languages != nullptr) {\n        for (int32_t i = 0; i < m_Languages->count; i++) {\n            char *lang = *(char **)Vector_Get(m_Languages, i);\n            Memory_Free(lang);\n        }\n        Vector_Free(m_Languages);\n        m_Languages = nullptr;\n    }\n}\n\nstatic const VECTOR *M_Language_GetLanguages(void)\n{\n    if (m_Languages == nullptr) {\n        // Initialize available languages for the language option.\n        m_Languages = GameStringManager_GetAvailableLanguages();\n        atexit(M_Language_Cleanup);\n    }\n    return m_Languages;\n}\n\nstatic int32_t M_Language_FindIndex(const UI_SETTINGS_OPTION *const option)\n{\n    const VECTOR *const langs = M_Language_GetLanguages();\n    const char *const cur = *(char **)option->target;\n    for (int32_t i = 0; i < langs->count; i++) {\n        const char *const lang = *(char **)Vector_Get(langs, i);\n        if (String_Equivalent(lang, cur)) {\n            return i;\n        }\n    }\n    return -1;\n}\n\nconst char *UI_Settings_Language_FormatValue(\n    const UI_SETTINGS_OPTION *const option)\n{\n    const char *const code = *(const char **)option->target;\n    const char *const name = GameStringManager_GetLanguageName(code);\n    return name != nullptr ? name : code;\n}\n\nbool UI_Settings_Language_CanChangeValue(\n    const UI_SETTINGS_OPTION *const option, const int32_t dir)\n{\n    const VECTOR *const langs = M_Language_GetLanguages();\n    const int32_t idx = M_Language_FindIndex(option);\n    if (idx < 0) {\n        // If the language from the user config somehow is no longer on the list\n        // (the file was deleted), let the player return to the default language\n        return true;\n    }\n    if (langs->count < 2) {\n        return false;\n    }\n    return idx + dir >= 0 && idx + dir < langs->count;\n}\n\nbool UI_Settings_Language_RequestChangeValue(\n    const UI_SETTINGS_OPTION *const option, const int32_t dir)\n{\n    const VECTOR *const langs = M_Language_GetLanguages();\n    if (!UI_Settings_Language_CanChangeValue(option, dir)) {\n        return false;\n    }\n    const char *new_lang;\n    const int32_t idx = M_Language_FindIndex(option);\n    if (idx != -1) {\n        new_lang = *(char **)Vector_Get(langs, idx + dir);\n    } else {\n        // If the language from the user config somehow is no longer on the list\n        // (the file was deleted), default to the first entry, which is English\n        new_lang = *(char **)Vector_Get(langs, 0);\n    }\n    Config_SetOptionValueFromString(Config_GetOption(option->target), new_lang);\n    GameStringManager_ReloadLanguage(new_lang);\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/gameplay_controls.def",
    "content": "X_UI_CFG(input.enable_responsive_passport)\nX_UI_CFG(gameplay.enable_walk_to_items)\nX_UI_CFG(input.enable_tr3_sidesteps)\nX_UI_CFG_ENUM(input.quick_guns_mode,        .misc = UI_Settings_QuickGunsModeEnumEntries)\nX_UI_CFG_ENUM(gameplay.look_mode,           .misc = UI_Settings_LookModeEnumEntries)\nX_UI_CFG(gameplay.enable_manual_camera)\nX_UI_CFG(gameplay.idle_pose_timeout,        .min_value = 0, .max_value = 1200)\nX_UI_CFG(gameplay.enable_idle_pose_camera,  .custom_handler = { .is_available = UI_Settings_IdlePose_IsAvailable })\n\nX_UI_CFG(gameplay.enable_tr2_jumping)\nX_UI_CFG_ENUM(gameplay.jump_lock_mode,      .misc = UI_Settings_JumpLockModeEnumEntries, .custom_handler = { .is_available = UI_Settings_ResponsiveJumping_IsAvailable })\nX_UI_CFG(gameplay.enable_jump_twists)\nX_UI_CFG(gameplay.enable_crawling)\nX_UI_CFG(gameplay.enable_responsive_crawl,  .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable })\nX_UI_CFG(gameplay.enable_crawl_jump,        .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable })\nX_UI_CFG(gameplay.enable_crawl_tilt,        .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable })\nX_UI_CFG(gameplay.enable_crouch_roll,       .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable })\nX_UI_CFG(gameplay.enable_toggle_crouch,     .custom_handler = { .is_available = UI_Settings_Crawl_IsAvailable })\nX_UI_CFG(gameplay.enable_sprint)\nX_UI_CFG(gameplay.enable_responsive_sprint, .custom_handler = { .is_available = UI_Settings_Sprint_IsAvailable })\nX_UI_CFG(gameplay.enable_toggle_sprint,     .custom_handler = { .is_available = UI_Settings_Sprint_IsAvailable })\nX_UI_CFG(gameplay.enable_neutral_twists)\nX_UI_CFG(gameplay.enable_slide_to_run)\nX_UI_CFG(gameplay.enable_back_slope_stumble)\nX_UI_CFG(gameplay.enable_lean_jumping)\nX_UI_CFG(gameplay.enable_slow_ledge_swing)\nX_UI_CFG(gameplay.enable_swing_cancel)\nX_UI_CFG(gameplay.enable_controlled_drops)\nX_UI_CFG(gameplay.enable_ledge_jumps)\nX_UI_CFG(gameplay.enable_smooth_wall_deflect)\nX_UI_CFG(gameplay.enable_soft_statics)\nX_UI_CFG(gameplay.enable_step_roll_boost)\nX_UI_CFG(gameplay.enable_uw_roll)\nX_UI_CFG(gameplay.enable_tr2_swimming)\nX_UI_CFG(gameplay.enable_tr2_swim_cancel)\nX_UI_CFG(gameplay.enable_wading)\n\nX_UI_CFG_ENUM(gameplay.target_mode,         .misc = UI_Settings_TargetModeEnumEntries)\nX_UI_CFG(gameplay.enable_target_change)\n\nX_UI_CFG(gameplay.enable_inverted_look)\nX_UI_CFG(gameplay.camera_speed,             .min_value = 1, .max_value = 10)\nX_UI_CFG(input.enable_buffering_func_keys)\nX_UI_CFG(input.enable_buffering_inventory)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/gameplay_fixes.def",
    "content": "X_UI_CFG(gameplay.fix_floor_data_issues)\nX_UI_CFG(visuals.fix_texture_issues)\nX_UI_CFG(visuals.fix_item_rots,             .custom_handler = { .is_available = UI_Settings_FixItemRots_IsAvailable })\n\nX_UI_CFG(audio.load_music_triggers)\nX_UI_CFG(visuals.fix_animated_sprites)\nX_UI_CFG(gameplay.fix_bridge_collision)\n\nX_UI_CFG(gameplay.fix_walk_run_jump)\nX_UI_CFG(gameplay.fix_flare_throw_priority, .custom_handler = { .is_available = UI_Settings_Flare_IsAvailable })\n\nX_UI_CFG(gameplay.fix_m16_accuracy,         .custom_handler = { .is_available = UI_Settings_M16_IsAvailable })\n\nX_UI_CFG(gameplay.fix_descending_glitch)\n\nX_UI_CFG(gameplay.fix_wall_geometry)\nX_UI_CFG_ENUM(gameplay.wall_glitch_mode,    .misc = UI_Settings_WallGlitchEnumEntries)\n\nX_UI_CFG(gameplay.fix_wade_wall_hit,        .custom_handler = { .is_available = UI_Settings_FixWadeWallHit_IsAvailable })\nX_UI_CFG(gameplay.fix_water_exit)\nX_UI_CFG(gameplay.fix_qwop_glitch)\nX_UI_CFG(gameplay.fix_step_glitch,          .custom_handler = { .is_available = UI_Settings_FixStepGlitch_IsAvailable })\nX_UI_CFG(gameplay.fix_item_duplication_glitch)\nX_UI_CFG(gameplay.fix_lara_pickup_embed)\nX_UI_CFG(gameplay.fix_free_flare_glitch,    .custom_handler = { .is_available = UI_Settings_Flare_IsAvailable })\n\nX_UI_CFG(gameplay.fix_alligator_ai)\nX_UI_CFG(gameplay.fix_bear_ai)\nX_UI_CFG(gameplay.fix_monkey_pickup_priority)\nX_UI_CFG(gameplay.fix_pipeman_aim)\nX_UI_CFG(audio.fix_chainblock_secret_sound)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/gameplay_general.def",
    "content": "X_UI_CFG(gameplay.enable_legal)\nX_UI_CFG(gameplay.enable_credits)\nX_UI_CFG(gameplay.enable_fmv)\nX_UI_CFG(gameplay.enable_demo)\nX_UI_CFG(gameplay.enable_cinematics)\nX_UI_CFG(gameplay.enable_cutscenes)\nX_UI_CFG_ENUM(gameplay.loading_screens,       .misc = UI_Settings_LoadingScreensModeEnumEntries)\n\nX_UI_CFG(gameplay.enable_game_modes)\nX_UI_CFG(gameplay.enable_play_previous_levels)\nX_UI_CFG(gameplay.enable_save_crystals)\n\nX_UI_CFG(gameplay.enable_auto_item_selection)\nX_UI_CFG(gameplay.enable_item_examining)\n\nX_UI_CFG(gameplay.enable_compass_stats)\nX_UI_CFG(gameplay.enable_total_stats)\n\nX_UI_CFG(gameplay.enable_timer_in_inventory)\nX_UI_CFG(gameplay.pause_on_focus_lost)\n\nX_UI_CFG(gameplay.maximum_save_slots,         .min_value = 1, .max_value = 1000, .delta_fast = 10, .delta_slow = 1)\nX_UI_CFG(gameplay.maximum_quick_save_slots,   .min_value = 0, .max_value = 1000, .delta_fast = 10, .delta_slow = 1)\nX_UI_CFG(gameplay.enable_enhanced_saves)\nX_UI_CFG(gameplay.enable_bouncy_grenades,     .custom_handler = { .is_available = UI_Settings_Grenade_IsAvailable })\nX_UI_CFG(gameplay.remember_gun_status)\n\nX_UI_CFG(gameplay.restore_ps1_enemies)\nX_UI_CFG(gameplay.change_pierre_spawn)\nX_UI_CFG(gameplay.disable_trex_collision)\nX_UI_CFG(gameplay.enable_enemy_rotation)\nX_UI_CFG(gameplay.enable_body_bags)\nX_UI_CFG(gameplay.enable_killer_pushblocks)\nX_UI_CFG(gameplay.enable_boulder_shake)\nX_UI_CFG(gameplay.enable_ally_targeting)\nX_UI_CFG_ENUM(gameplay.ally_hostility_policy, .misc = UI_Settings_AllyHostilityPolicyEnumEntries)\nX_UI_CFG_ENUM(gameplay.creature_drown_policy, .misc = UI_Settings_CreatureDrownPolicyEnumEntries)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/gameplay_mods.def",
    "content": "X_UI_CFG(gameplay.enable_cheats)\nX_UI_CFG(gameplay.enable_console)\n\nX_UI_CFG(gameplay.harpoon_recoil,              .min_value = 0, .max_value = 1000, .delta_slow = 1, .delta_fast = 1, .custom_handler = { .is_available = UI_Settings_Harpoon_IsAvailable })\n\nX_UI_CFG(gameplay.start_lara_hitpoints,        .min_value = 1, .max_value = LARA_MAX_HITPOINTS, .delta_slow = 10, .delta_fast = 100)\nX_UI_CFG(gameplay.disable_healing_between_levels)\nX_UI_CFG(gameplay.disable_medpacks)\nX_UI_CFG(gameplay.disable_extra_guns)\nX_UI_CFG(debug.enable_endless_sprint)\nX_UI_CFG(debug.enable_endless_flare_time,      .custom_handler = { .is_available = UI_Settings_Flare_IsAvailable })\nX_UI_CFG_ENUM(gameplay.projectile_area_damage, .misc = UI_Settings_ProjectileAreaDamageEnumEntries, .custom_handler = { .is_available = UI_Settings_ProjectileAreaDamage_IsAvailable })\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/graphic_rendering.def",
    "content": "X_UI_CFG(rendering.fps,                    .min_value = 30, .max_value = 60, .delta_slow = 30, .delta_fast = 30)\nX_UI_CFG(rendering.enable_trapezoid_filter)\nX_UI_CFG_ENUM(rendering.texture_filter,    .misc = UI_Settings_TextureFilterEnumEntries)\nX_UI_CFG_ENUM(rendering.ui_filter,         .misc = UI_Settings_TextureFilterEnumEntries)\nX_UI_CFG(rendering.anisotropy_filter,      .min_value = 1 * 100, .max_value = 32 * 100, .delta_slow = 10, .delta_fast = 100)\nX_UI_CFG(rendering.enable_vsync)\nX_UI_CFG(visuals.game_brightness,          .min_value = CONFIG_MIN_BRIGHTNESS * 100, .max_value = CONFIG_MAX_BRIGHTNESS * 100, .delta_slow = 1, .delta_fast = 5)\nX_UI_CFG(visuals.ui_brightness,            .min_value = CONFIG_MIN_BRIGHTNESS * 100, .max_value = CONFIG_MAX_BRIGHTNESS * 100, .delta_slow = 1, .delta_fast = 5)\nX_UI_CFG(visuals.gamma,                    .min_value = CONFIG_MIN_GAMMA * 100, .max_value = CONFIG_MAX_GAMMA * 100, .delta_slow = 10, .delta_fast = 50)\n\nX_UI_CFG_ENUM(rendering.lighting_contrast, .misc = UI_Settings_LightingContrastEnumEntries)\nX_UI_CFG_ENUM(rendering.sprite_lock_mode,  .misc = UI_Settings_SpriteLockModeEnumEntries)\n\nX_UI_CFG_ENUM(rendering.aspect_mode,       .misc = UI_Settings_AspectModeEnumEntries)\nX_UI_CFG_ENUM(rendering.upscaling_filter,  .misc = UI_Settings_TextureFilterEnumEntries)\nX_UI_CFG(rendering.upscaling_factor,       .min_value = 1, .max_value = 8, .delta_slow = 1, .delta_fast = 1)\nX_UI_CFG(rendering.borders,                .min_value = 0, .max_value = 45, .delta_slow = 1, .delta_fast = 5)\n\nX_UI_CFG_ENUM(rendering.screenshot_format, .misc = UI_Settings_ScreenshotFormatEnumEntries)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/graphic_ui.def",
    "content": "X_UI_CFG(language,                           .custom_handler = { .format_value = UI_Settings_Language_FormatValue, .can_change_value = UI_Settings_Language_CanChangeValue, .request_change_value = UI_Settings_Language_RequestChangeValue })\n\nX_UI_CFG(ui.text_scale,                      .min_value = 50, .max_value = 200, .delta_slow = 1, .delta_fast = 5)\nX_UI_CFG(ui.pickup_scale,                    .min_value = 50, .max_value = 200, .delta_slow = 1, .delta_fast = 5)\nX_UI_CFG(ui.show_pickups_overlay)\nX_UI_CFG(ui.show_title_version)\nX_UI_CFG(ui.enable_wraparound)\n\nX_UI_CFG_ENUM(ui.menu_style,                 .misc = UI_Settings_MenuStyleEnumEntries)\n\nX_UI_CFG_ENUM(ui.inventory_background_style, .misc = UI_Settings_BackgroundStyleEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_BackgroundStyle_IsEnumValueAvailable })\nX_UI_CFG(ui.inventory_fade_effects,          .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable })\nX_UI_CFG_ENUM(ui.pause_background_style,     .misc = UI_Settings_BackgroundStyleEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_BackgroundStyle_IsEnumValueAvailable })\nX_UI_CFG(ui.pause_fade_effects,              .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable })\nX_UI_CFG_ENUM(ui.stats_background_style,     .misc = UI_Settings_BackgroundStyleEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_BackgroundStyle_IsEnumValueAvailable })\nX_UI_CFG(ui.stats_fade_effects,              .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable })\n\nX_UI_CFG(visuals.enable_fade_effects)\nX_UI_CFG(visuals.enable_exit_fade_effects,   .custom_handler = { .is_available = UI_Settings_EnableFadeEffects_IsAvailable })\n\nX_UI_CFG_ENUM(ui.ammo_counter.location,      .misc = UI_Settings_UIElementLocationEnumEntries)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/graphic_ui_bars.def",
    "content": "X_UI_CFG(ui.show_bars)\nX_UI_CFG(ui.bar_scale,                                  .min_value = 50, .max_value = 200, .delta_slow = 1, .delta_fast = 5, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable })\nX_UI_CFG_DYN_ENUM(ui.bar_look,                          .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable })\nX_UI_CFG(ui.enable_smooth_bars,                         .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable })\nX_UI_CFG(ui.enable_bar_flashing,                        .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable })\n\nX_UI_CFG_DYN_ENUM(ui.lara_health_bar.color,             .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable,      .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_health_bar.color_ps1,         .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable,      .is_visible = UI_Settings_BarColorPS1_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_health_bar.poison_color,      .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable,      .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_health_bar.poison_color_ps1,  .custom_handler = { .is_available = UI_Settings_HealthbarColor_IsAvailable,      .is_visible = UI_Settings_BarColorPS1_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_air_bar.color,                .custom_handler = { .is_available = UI_Settings_AirbarColor_IsAvailable,         .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_air_bar.color_ps1,            .custom_handler = { .is_available = UI_Settings_AirbarColor_IsAvailable,         .is_visible = UI_Settings_BarColorPS1_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_sprint_bar.color,             .custom_handler = { .is_available = UI_Settings_SprintbarColor_IsAvailable,      .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_sprint_bar.color_ps1,         .custom_handler = { .is_available = UI_Settings_SprintbarColor_IsAvailable,      .is_visible = UI_Settings_BarColorPS1_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_exposure_bar.color,           .custom_handler = { .is_available = UI_Settings_ExposurebarColor_IsAvailable,    .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.lara_exposure_bar.color_ps1,       .custom_handler = { .is_available = UI_Settings_ExposurebarColor_IsAvailable,    .is_visible = UI_Settings_BarColorPS1_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color,            .custom_handler = { .is_available = UI_Settings_EnemyHealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color_ps1,        .custom_handler = { .is_available = UI_Settings_EnemyHealthbarColor_IsAvailable, .is_visible = UI_Settings_BarColorPS1_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color_allies,     .custom_handler = { .is_available = UI_Settings_AllyHealthbarColor_IsAvailable,  .is_visible = UI_Settings_BarColorPC_IsVisible })\nX_UI_CFG_DYN_ENUM(ui.enemy_health_bar.color_allies_ps1, .custom_handler = { .is_available = UI_Settings_AllyHealthbarColor_IsAvailable,  .is_visible = UI_Settings_BarColorPS1_IsVisible })\n\nX_UI_CFG_ENUM(ui.enemy_health_bar.show_mode,            .misc = UI_Settings_EnemyHealthBarShowModeEnumEntries, .custom_handler = { .is_available = UI_Settings_Bar_IsAvailable })\nX_UI_CFG_ENUM(ui.lara_health_bar.location,              .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Healthbar_IsAvailable })\nX_UI_CFG_ENUM(ui.lara_air_bar.location,                 .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Airbar_IsAvailable })\nX_UI_CFG_ENUM(ui.lara_sprint_bar.location,              .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Sprintbar_IsAvailable })\nX_UI_CFG_ENUM(ui.lara_exposure_bar.location,            .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_Exposurebar_IsAvailable })\nX_UI_CFG_ENUM(ui.enemy_health_bar.location,             .misc = UI_Settings_UIElementLocationEnumEntries, .custom_handler = { .is_available = UI_Settings_EnemyHealthbar_IsAvailable })\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/graphic_ui_stats.def",
    "content": "X_UI_CFG_ENUM(ui.stats.style, .misc = UI_Settings_StatsStyleEnumEntries)\nX_UI_CFG(ui.stats.show_totals)\nX_UI_CFG(ui.stats.show_level_header)\nX_UI_CFG(ui.stats.show_time_taken)\nX_UI_CFG(ui.stats.show_secrets)\nX_UI_CFG(ui.stats.show_crystals, .custom_handler = { .is_available = UI_Settings_ShowCrystals_IsAvailable })\nX_UI_CFG(ui.stats.show_pickups)\nX_UI_CFG(ui.stats.show_kills)\nX_UI_CFG(ui.stats.show_ammo)\nX_UI_CFG(ui.stats.show_medipacks_used)\nX_UI_CFG(ui.stats.show_distance_travelled)\nX_UI_CFG(ui.stats.show_deaths)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/graphic_visuals.def",
    "content": "X_UI_CFG(visuals.fog_start,            .min_value = 1, .max_value = 100, .delta_slow = 1, .delta_fast = 10)\nX_UI_CFG(visuals.fog_end,              .min_value = 1, .max_value = 100, .delta_slow = 1, .delta_fast = 10)\nX_UI_CFG(visuals.fog_transparency)\nX_UI_CFG_RGB888(visuals.fog_color,     .custom_handler = { .format_value = UI_Settings_ColorEditor_FormatValue, .can_change_value = UI_Settings_ColorEditor_CanChangeValue, .is_available = UI_Settings_FogColor_IsAvailable })\n\nX_UI_CFG_RGB888(visuals.water_color,   .custom_handler = { .format_value = UI_Settings_ColorEditor_FormatValue, .can_change_value = UI_Settings_ColorEditor_CanChangeValue })\n\nX_UI_CFG_ENUM(visuals.camera_mode,     .misc = UI_Settings_CameraModeEnumEntries)\nX_UI_CFG(visuals.enable_glide_cameras)\n\nX_UI_CFG(visuals.fov,                  .min_value = 30, .max_value = 150, .delta_slow = 1, .delta_fast = 5)\n\nX_UI_CFG(visuals.enable_reflections)\nX_UI_CFG(visuals.enable_skybox)\nX_UI_CFG(visuals.enable_weather)\nX_UI_CFG(visuals.enable_footprints)\nX_UI_CFG(visuals.enable_responsive_mesh_tint)\nX_UI_CFG_ENUM(visuals.blood_effects,   .misc = UI_Settings_BloodEffectsEnumEntries)\nX_UI_CFG_DYN_ENUM(visuals.lara_outfit)\nX_UI_CFG(visuals.enable_braid)\nX_UI_CFG_ENUM(visuals.sunglasses_mode, .misc = UI_Settings_SunglassesModeEnumEntries)\nX_UI_CFG(visuals.enable_breeze,        .custom_handler = { .is_available = UI_Settings_EnableBreeze_IsAvailable })\nX_UI_CFG(visuals.enable_3d_pickups)\nX_UI_CFG(gameplay.enable_pickup_aids)\n\nX_UI_CFG(visuals.enable_ps1_crystals,  .custom_handler = { .is_available = UI_Settings_EnablePS1Crystals_IsAvailable })\n\nX_UI_CFG_ENUM(visuals.shadow_type,     .misc = UI_Settings_ShadowTypeEnumEntries, .custom_handler = { .is_enum_value_available = UI_Settings_ShadowType_IsEnumValueAvailable })\nX_UI_CFG(visuals.enable_gun_lighting)\nX_UI_CFG(visuals.enable_fire_lighting)\n\nX_UI_CFG(visuals.enable_shotgun_flash)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/sound_misc.def",
    "content": "X_UI_CFG(audio.enable_music_in_menu)\nX_UI_CFG(audio.fix_secrets_killing_music)\nX_UI_CFG(audio.fix_speeches_killing_music)\n\nX_UI_CFG(audio.enable_music_in_inventory)\nX_UI_CFG_ENUM(audio.music_load_condition, .misc = UI_Settings_MusicLoadConditionEnumEntries)\nX_UI_CFG(audio.enable_underwater_anim_sfx)\nX_UI_CFG(audio.mute_out_of_focus)\n\nX_UI_CFG(audio.enable_pitched_sounds)\nX_UI_CFG(audio.enable_ps1_sfx)\nX_UI_CFG(audio.enable_lara_mic)\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/setting_tabs/sound_volume.def",
    "content": "X_UI_CFG(audio.master_volume,             .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.sound_volume,              .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.music_volume,              .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.inventory_music_volume,    .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange, .is_available = UI_Settings_PauseMusicInInventory_IsAvailable })\nX_UI_CFG(audio.underwater_music_volume,   .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.ambient_volume,            .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.inventory_ambient_volume,  .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange, .is_available = UI_Settings_PauseMusicInInventory_IsAvailable})\nX_UI_CFG(audio.underwater_ambient_volume, .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.cutscene_volume,           .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\nX_UI_CFG(audio.fmv_volume,                .min_value = 0, .max_value = 100, .delta_slow = 1, .delta_fast = 10, .custom_handler = { .request_change_value = UI_Settings_Volume_RequestChange })\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/settings.c",
    "content": "#include <trx/game/ui/dialogs/settings.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/game_strings/manager.h>\n#include <trx/game/input.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/viewport.h>\n\ntypedef struct UI_SETTINGS_DIALOG_STATE {\n    UI_SETTINGS_PHASE phase;\n    int32_t visible_rows;\n\n    float max_content_width;\n    float max_content_height;\n\n    int32_t tab_count;\n    UI_SETTINGS_TAB *tabs;\n    UI_TAB_SWITCH_STATE *tab_switch;\n    GAME_STRING_ID title;\n\n    int32_t listener_id;\n} UI_SETTINGS_DIALOG_STATE;\n\nstatic int32_t M_GetVisibleRows(void)\n{\n    const int32_t res_h = UI_Scaler_CalcInverse(\n        Viewport_GetHeight(VIEWPORT_UI), UI_SCALER_TARGET_TEXT);\n    static struct {\n        int32_t threshold;\n        int32_t rows;\n    } thresholds[] = {\n        { 240, 5 },  { 252, 6 },  { 266, 7 },  { 282, 8 },\n        { 300, 9 },  { 320, 10 }, { 342, 11 }, { 370, 12 },\n        { 420, 13 }, { 480, 15 }, { -1, 16 },\n    };\n    for (int32_t i = 0;; i++) {\n        if (res_h <= thresholds[i].threshold || thresholds[i].threshold == -1) {\n            return thresholds[i].rows;\n        }\n    }\n}\n\nstatic UI_SETTINGS_TAB *M_GetActiveTab(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    if (s->tab_switch == nullptr || s->tabs == nullptr || s->tab_count <= 0) {\n        return nullptr;\n    }\n    const int32_t idx = s->tab_switch->active_tab_idx;\n    if (idx < 0 || idx >= s->tab_count) {\n        return nullptr;\n    }\n    return &s->tabs[idx];\n}\n\nstatic const UI_SETTINGS_TAB *M_GetActiveTabConst(\n    const UI_SETTINGS_DIALOG_STATE *const s)\n{\n    if (s->tab_switch == nullptr || s->tabs == nullptr || s->tab_count <= 0) {\n        return nullptr;\n    }\n    const int32_t idx = s->tab_switch->active_tab_idx;\n    if (idx < 0 || idx >= s->tab_count) {\n        return nullptr;\n    }\n    return &s->tabs[idx];\n}\n\nstatic UI_SCROLLABLE *M_GetTabScrollable(UI_SETTINGS_TAB *const tab)\n{\n    if (tab == nullptr || tab->ops == nullptr\n        || tab->ops->get_scrollable == nullptr) {\n        return nullptr;\n    }\n    return tab->ops->get_scrollable(tab->user_data);\n}\n\nstatic void M_RecomputeSizes(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    int32_t max_item_count = 0;\n    for (int32_t i = 0; i < s->tab_count; i++) {\n        UI_SETTINGS_TAB *const tab = &s->tabs[i];\n        if (tab->ops != nullptr && tab->ops->get_item_count != nullptr) {\n            max_item_count =\n                MAX(max_item_count, tab->ops->get_item_count(tab->user_data));\n        }\n    }\n\n    const int32_t visible_rows = MIN(max_item_count, M_GetVisibleRows());\n    float max_content_width = 0.0f;\n    float max_content_height = -1.0f;\n\n    for (int32_t i = 0; i < s->tab_count; i++) {\n        UI_SETTINGS_TAB *const tab = &s->tabs[i];\n        if (tab->ops != nullptr && tab->ops->recompute != nullptr) {\n            tab->ops->recompute(tab->user_data, visible_rows);\n        }\n        if (tab->ops != nullptr && tab->ops->get_content_width != nullptr) {\n            max_content_width = MAX(\n                max_content_width, tab->ops->get_content_width(tab->user_data));\n        }\n        if (tab->ops != nullptr) {\n            const UI_SCROLLABLE *const tab_scroll = M_GetTabScrollable(tab);\n            const int32_t tab_visible_rows =\n                tab_scroll != nullptr ? tab_scroll->vis_items : visible_rows;\n            float tab_content_height = -1.0f;\n            if (tab->ops->get_content_height != nullptr) {\n                tab_content_height =\n                    tab->ops->get_content_height(tab->user_data);\n            } else if (tab_visible_rows > 0) {\n                tab_content_height = tab_visible_rows * UI_TEXT_HEIGHT;\n            }\n            max_content_height = MAX(max_content_height, tab_content_height);\n        }\n    }\n\n    s->visible_rows = visible_rows;\n    s->max_content_width = max_content_width / g_Config.ui.text_scale;\n    s->max_content_height = max_content_height;\n}\n\nstatic void M_WindowHeader(void *const user_data)\n{\n    UI_SETTINGS_DIALOG_STATE *const s = user_data;\n    if (s->tab_switch != nullptr && s->tab_count > 0) {\n        UI_TabSwitch(\n            s->tab_switch, s->phase == UI_SETTINGS_PHASE_NAVIGATE_TABS);\n        UI_Spacer(0.0f, 8.0f);\n    }\n}\n\nstatic void M_HandleLanguageReload(const EVENT *const, void *const data)\n{\n    UI_SETTINGS_DIALOG_STATE *const s = data;\n    M_RecomputeSizes(s);\n}\n\nstatic UI_SETTINGS_DIALOG_STATE *M_InitCommon(const GAME_STRING_ID title)\n{\n    UI_SETTINGS_DIALOG_STATE *const s = Memory_Alloc(sizeof(*s));\n    s->title = title;\n    s->listener_id =\n        GameStringManager_SubscribeReload(M_HandleLanguageReload, s);\n    return s;\n}\n\nstatic void M_SetActiveTab(UI_SETTINGS_DIALOG_STATE *const s, const int32_t idx)\n{\n    s->tab_switch->active_tab_idx = idx;\n    M_RecomputeSizes(s);\n}\n\nstatic void M_EnterEditMode(\n    UI_SETTINGS_DIALOG_STATE *const s, const bool focus_last)\n{\n    s->phase = UI_SETTINGS_PHASE_EDIT_SETTINGS;\n    UI_SETTINGS_TAB *const tab = M_GetActiveTab(s);\n    UI_SCROLLABLE *const active_scroll = M_GetTabScrollable(tab);\n    if (active_scroll != nullptr) {\n        if (focus_last) {\n            UI_Scrollable_SelectLastItem(active_scroll);\n        } else {\n            UI_Scrollable_SelectFirstItem(active_scroll);\n        }\n    }\n}\n\nstatic void M_ClearActiveCustomSelection(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SETTINGS_TAB *const tab = M_GetActiveTab(s);\n    if (tab == nullptr || tab->ops == nullptr\n        || tab->ops->get_content_height == nullptr) {\n        return;\n    }\n    UI_SCROLLABLE *const active_scroll = M_GetTabScrollable(tab);\n    if (active_scroll != nullptr) {\n        UI_Scrollable_SelectItem(active_scroll, -1);\n    }\n}\n\nUI_SETTINGS_DIALOG_STATE *UI_SettingsDialog_Init(\n    const GAME_STRING_ID title, const int32_t tab_count,\n    const UI_SETTINGS_TAB *const tabs)\n{\n    ASSERT(tabs != nullptr);\n    UI_SETTINGS_DIALOG_STATE *const s = M_InitCommon(title);\n\n    int32_t visible_tab_count = 0;\n    for (int32_t i = 0; i < tab_count; i++) {\n        const UI_SETTINGS_TAB *const tab = &tabs[i];\n        int32_t item_count = 0;\n        if (tab->ops != nullptr && tab->ops->get_item_count != nullptr) {\n            item_count = tab->ops->get_item_count(tab->user_data);\n        }\n\n        const bool is_list_tab =\n            tab->ops != nullptr && tab->ops->get_content_height == nullptr;\n        if (is_list_tab && item_count <= 0) {\n            continue;\n        }\n        visible_tab_count++;\n    }\n\n    UI_TAB_SWITCH_TAB tab_switch_tabs[tab_count];\n    UI_SETTINGS_TAB *visible_tabs = nullptr;\n    if (visible_tab_count > 0) {\n        visible_tabs =\n            Memory_Alloc(sizeof(UI_SETTINGS_TAB) * visible_tab_count);\n    }\n\n    int32_t vt = 0;\n    for (int32_t i = 0; i < tab_count; i++) {\n        const UI_SETTINGS_TAB *const tab = &tabs[i];\n        int32_t item_count = 0;\n        if (tab->ops != nullptr && tab->ops->get_item_count != nullptr) {\n            item_count = tab->ops->get_item_count(tab->user_data);\n        }\n        const bool is_list_tab =\n            tab->ops != nullptr && tab->ops->get_content_height == nullptr;\n        if (is_list_tab && item_count <= 0) {\n            continue;\n        }\n        visible_tabs[vt] = tabs[i];\n        tab_switch_tabs[vt].header.one_off = nullptr;\n        tab_switch_tabs[vt].header.live_ptr =\n            GameString_GetPtr(tabs[i].header_gs);\n        vt++;\n    }\n\n    s->tabs = visible_tabs;\n    s->tab_count = visible_tab_count;\n    s->tab_switch = UI_TabSwitch_Init(s->tab_count, tab_switch_tabs);\n\n    s->phase = UI_SETTINGS_PHASE_NAVIGATE_TABS;\n    if (s->tab_count > 0) {\n        M_SetActiveTab(s, 0);\n    } else {\n        M_RecomputeSizes(s);\n    }\n\n    return s;\n}\n\nvoid UI_SettingsDialog_Free(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    if (s->listener_id >= 0) {\n        GameStringManager_UnsubscribeReload(s->listener_id);\n        s->listener_id = -1;\n    }\n    if (s->tab_switch != nullptr) {\n        UI_TabSwitch_Free(s->tab_switch);\n        s->tab_switch = nullptr;\n    }\n    if (s->tabs != nullptr) {\n        for (int32_t i = 0; i < s->tab_count; i++) {\n            if (s->tabs[i].ops != nullptr && s->tabs[i].ops->free != nullptr\n                && s->tabs[i].user_data != nullptr) {\n                s->tabs[i].ops->free(s->tabs[i].user_data);\n            }\n        }\n        Memory_FreePointer(&s->tabs);\n    }\n    Memory_Free(s);\n}\n\nbool UI_SettingsDialog_Control(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    M_RecomputeSizes(s);\n\n    if (s->phase == UI_SETTINGS_PHASE_NAVIGATE_TABS) {\n        if (UI_TabSwitch_Control(s->tab_switch, UI_TAB_SWITCH_NORMAL)) {\n            M_SetActiveTab(s, s->tab_switch->active_tab_idx);\n            return false;\n        }\n        if (g_InputDB.menu_down || g_InputDB.menu_confirm) {\n            if (!g_InputDB.menu_confirm) {\n                M_EnterEditMode(s, false);\n            }\n        } else if (g_InputDB.menu_up && g_Config.ui.enable_wraparound) {\n            M_EnterEditMode(s, true);\n        } else if (g_InputDB.menu_back) {\n            return true;\n        }\n        return false;\n    }\n\n    UI_SETTINGS_TAB *const tab = M_GetActiveTab(s);\n    if (tab == nullptr || tab->ops == nullptr) {\n        return g_InputDB.menu_back;\n    }\n\n    const bool consumed = tab->ops->control(tab->user_data, &s->phase);\n\n    if (s->phase == UI_SETTINGS_PHASE_NAVIGATE_TABS) {\n        M_ClearActiveCustomSelection(s);\n        return false;\n    }\n\n    if (consumed) {\n        return false;\n    }\n\n    if (s->tab_switch != nullptr && !g_Input.menu_left && !g_Input.menu_right\n        && UI_TabSwitch_Control(s->tab_switch, UI_TAB_SWITCH_NO_ARROWS)) {\n        M_SetActiveTab(s, s->tab_switch->active_tab_idx);\n        return false;\n    }\n\n    if (g_InputDB.menu_back) {\n        return true;\n    }\n\n    return false;\n}\n\nvoid UI_SettingsDialog(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    const UI_SETTINGS_TAB *const tab = M_GetActiveTabConst(s);\n    UI_SCROLLABLE *const active_scroll = M_GetTabScrollable(M_GetActiveTab(s));\n\n    UI_BeginModal(0.5f, 0.6f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .spacing = { .v = 5.0f },\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n    });\n\n    UI_BeginWindow((UI_WINDOW_SETTINGS) {\n        .title = GameString_Get(s->title),\n        .scrollable = active_scroll,\n        .title_spacing = -1.0f,\n        .header_func = M_WindowHeader,\n        .footer_func = nullptr,\n        .user_data = s,\n        .reserve_scroll_space = true,\n    });\n\n    if (tab == nullptr || tab->ops == nullptr || tab->ops->draw == nullptr) {\n        UI_BeginResize(-1.0f, -1.0f);\n        UI_BeginPad(\n            g_TRVersion == 1 ? -1.0f : 0.0f, g_TRVersion == 1 ? -1.0f : 0.0f);\n        UI_BeginStackEx((UI_STACK_SETTINGS) {\n            .orientation = UI_STACK_VERTICAL,\n            .align = { .h = UI_STACK_H_ALIGN_CENTER },\n        });\n        UI_Label(GS(\"general/settings/common/all_hidden_disclaimer\"));\n        UI_EndStack();\n        UI_EndPad();\n        UI_EndResize();\n    } else {\n        float content_width =\n            s->max_content_width > 0.0f ? s->max_content_width : -1.0f;\n        float content_height = s->max_content_height;\n        UI_BeginResize(content_width, content_height);\n        tab->ops->draw(tab->user_data, s->phase, s->max_content_width);\n        UI_EndResize();\n    }\n\n    UI_EndWindow();\n    if (tab != nullptr && tab->ops != nullptr\n        && tab->ops->draw_footer != nullptr) {\n        tab->ops->draw_footer(tab->user_data, s->phase);\n    } else {\n        UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n    }\n\n    UI_EndStack();\n    UI_EndModal();\n\n    if (tab != nullptr && tab->ops != nullptr\n        && tab->ops->draw_overlay != nullptr) {\n        tab->ops->draw_overlay(tab->user_data);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/settings.h",
    "content": "#pragma once\n\n#include <trx/config/option.h>\n#include <trx/config/types.h>\n#include <trx/core/utils.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/settings_tabs.h>\n#include <trx/game/ui/dialogs/text.h>\n#include <trx/game/ui/elements/tab_switch.h>\n#include <trx/game/ui/scrollable.h>\n\ntypedef struct {\n    const char *(*format_value)(const struct UI_SETTINGS_OPTION *option);\n    bool (*can_change_value)(\n        const struct UI_SETTINGS_OPTION *option, int32_t dir);\n    bool (*request_change_value)(\n        const struct UI_SETTINGS_OPTION *option, int32_t dir);\n    bool (*is_available)(const struct UI_SETTINGS_OPTION *option);\n    bool (*is_visible)(const struct UI_SETTINGS_OPTION *option);\n    bool (*is_enum_value_available)(\n        const struct UI_SETTINGS_OPTION *option, int32_t value);\n} UI_SETTINGS_CUSTOM_OPITON_HANDLER;\n\ntypedef struct UI_SETTINGS_OPTION {\n    // A custom handler that must have all the function pointers filled,\n    UI_SETTINGS_CUSTOM_OPITON_HANDLER custom_handler;\n\n    // ...or a convenience default handler options\n    struct {\n        void *target;\n        int32_t min_value;\n        int32_t max_value;\n        int32_t delta_slow;\n        int32_t delta_fast;\n        const void *misc;\n    };\n} UI_SETTINGS_OPTION;\n\n#define X_UI_CFG(TARGET_, ...) { .target = &g_Config.TARGET_, ##__VA_ARGS__ },\n\n#define X_UI_CFG_DYN_ENUM(TARGET_, ...)                                        \\\n    X_UI_CFG(TARGET_, .delta_slow = 1, .delta_fast = 1, ##__VA_ARGS__)\n\n#define X_UI_CFG_ENUM(TARGET_, ...)                                            \\\n    X_UI_CFG(TARGET_, .delta_slow = 1, .delta_fast = 1, ##__VA_ARGS__)\n\n#define X_UI_CFG_RGB888(TARGET_, ...)                                          \\\n    X_UI_CFG(TARGET_, .min_value = 0, .max_value = 255, ##__VA_ARGS__)\n\ntypedef struct UI_SETTINGS_DIALOG_STATE UI_SETTINGS_DIALOG_STATE;\n\nUI_SETTINGS_DIALOG_STATE *UI_SettingsDialog_Init(\n    GAME_STRING_ID title, int32_t tab_count, const UI_SETTINGS_TAB *tabs);\nvoid UI_SettingsDialog_Free(UI_SETTINGS_DIALOG_STATE *s);\nbool UI_SettingsDialog_Control(UI_SETTINGS_DIALOG_STATE *s);\n\nvoid UI_SettingsDialog(UI_SETTINGS_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/settings_editor.c",
    "content": "#include <trx/game/ui/dialogs/settings_editor.h>\n\n#include <trx/config.h>\n#include <trx/config/dynamic_enum.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/input.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/dialogs/color_editor.h>\n#include <trx/game/ui/dialogs/setting_helpers/enums.h>\n#include <trx/game/ui/dialogs/text.h>\n#include <trx/version.h>\n\n#include <math.h>\n\n#define M_BAR_WIDTH 60\n#define M_BAR_HEIGHT 12\n\ntypedef struct {\n    int32_t position;\n    int32_t count;\n} M_ENUM_LOOKUP;\n\ntypedef struct UI_SETTINGS_EDITOR_STATE {\n    const UI_SETTINGS_OPTION *options;\n    int32_t visible_rows;\n    UI_SCROLLABLE scroll;\n    struct {\n        bool show;\n        UI_TEXT_DIALOG_STATE *state;\n    } description;\n    UI_COLOR_EDITOR_DIALOG_STATE *color_editor;\n} UI_SETTINGS_EDITOR_STATE;\n\nstatic const CONFIG_OPTION *M_GetConfigOption(\n    const UI_SETTINGS_OPTION *const option)\n{\n    ASSERT(option != nullptr);\n    ASSERT(option->target != nullptr);\n    const CONFIG_OPTION *const result = Config_GetOption(option->target);\n    ASSERT(result != nullptr);\n    return result;\n}\n\nstatic const char *M_GetOptionDescription(\n    const UI_SETTINGS_OPTION *const option)\n{\n    if (option == nullptr || option->target == nullptr) {\n        return nullptr;\n    }\n    return Config_GetOptionDescription(M_GetConfigOption(option));\n}\n\nstatic const char *M_GetOptionTitle(const UI_SETTINGS_OPTION *const option)\n{\n    if (option == nullptr || option->target == nullptr) {\n        return \"\";\n    }\n    const char *const result = Config_GetOptionTitle(M_GetConfigOption(option));\n    return result != nullptr ? result : \"\";\n}\n\nstatic bool M_IsEnumEntryAvailable(\n    const UI_SETTINGS_OPTION *const option,\n    const UI_SETTINGS_ENUM_ENTRY *const entry)\n{\n    if (entry == nullptr || entry->value == -1) {\n        return false;\n    }\n    if (option->custom_handler.is_enum_value_available == nullptr) {\n        return true;\n    }\n    return option->custom_handler.is_enum_value_available(option, entry->value);\n}\n\nstatic UI_BAR_TYPE M_GetBarType(const UI_SETTINGS_OPTION *const option)\n{\n    if (option->target == &g_Config.ui.lara_health_bar.color\n        || option->target == &g_Config.ui.lara_health_bar.color_ps1) {\n        return UI_BAR_LARA_HP;\n    } else if (\n        option->target == &g_Config.ui.lara_health_bar.poison_color\n        || option->target == &g_Config.ui.lara_health_bar.poison_color_ps1) {\n        return UI_BAR_LARA_HP_POISON;\n    } else if (\n        option->target == &g_Config.ui.lara_air_bar.color\n        || option->target == &g_Config.ui.lara_air_bar.color_ps1) {\n        return UI_BAR_LARA_AIR;\n    } else if (\n        option->target == &g_Config.ui.lara_sprint_bar.color\n        || option->target == &g_Config.ui.lara_sprint_bar.color_ps1) {\n        return UI_BAR_LARA_STAMINA;\n    } else if (\n        option->target == &g_Config.ui.lara_exposure_bar.color\n        || option->target == &g_Config.ui.lara_exposure_bar.color_ps1) {\n        return UI_BAR_LARA_EXPOSURE;\n    } else if (\n        option->target == &g_Config.ui.enemy_health_bar.color\n        || option->target == &g_Config.ui.enemy_health_bar.color_ps1) {\n        return UI_BAR_ENEMY_HP;\n    } else if (\n        option->target == &g_Config.ui.enemy_health_bar.color_allies\n        || option->target == &g_Config.ui.enemy_health_bar.color_allies_ps1) {\n        return UI_BAR_ALLY_HP;\n    } else {\n        return (UI_BAR_TYPE)-1;\n    }\n}\n\nstatic bool M_IsBarColorEnum(const UI_SETTINGS_OPTION *const option)\n{\n    return M_GetBarType(option) != (UI_BAR_TYPE)-1;\n}\n\nstatic bool M_IsColorEditorOption(const UI_SETTINGS_OPTION *const option)\n{\n    return option != nullptr && M_GetConfigOption(option)->type == COT_RGB888;\n}\n\nstatic bool M_HasAvailableEnumValue(const UI_SETTINGS_OPTION *const option)\n{\n    const UI_SETTINGS_ENUM_ENTRY *entry =\n        (UI_SETTINGS_ENUM_ENTRY *)option->misc;\n    if (entry == nullptr) {\n        return false;\n    }\n    while (entry->value != -1) {\n        if (M_IsEnumEntryAvailable(option, entry)) {\n            return true;\n        }\n        entry++;\n    }\n    return false;\n}\n\nstatic bool M_IsOptionHidden(const UI_SETTINGS_OPTION *const option)\n{\n    if (option->custom_handler.is_visible != nullptr\n        && !option->custom_handler.is_visible(option)) {\n        return true;\n    }\n    if (Config_IsOptionHidden(option->target)) {\n        return true;\n    }\n    if (M_GetConfigOption(option)->type == COT_ENUM && option->misc != nullptr\n        && !M_HasAvailableEnumValue(option)) {\n        return true;\n    }\n    return false;\n}\n\nstatic const UI_SETTINGS_OPTION *M_GetOptionByRow(\n    const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx)\n{\n    if (s->options == nullptr) {\n        return nullptr;\n    }\n    int32_t count = 0;\n    for (int32_t i = 0; s->options[i].target != nullptr; i++) {\n        const UI_SETTINGS_OPTION *const opt = &s->options[i];\n        if (M_IsOptionHidden(opt)) {\n            continue;\n        }\n        if (count == row_idx) {\n            return opt;\n        }\n        count++;\n    }\n    return nullptr;\n}\n\nstatic int32_t M_GetRowCount(const UI_SETTINGS_EDITOR_STATE *const s)\n{\n    if (s->options == nullptr) {\n        return 0;\n    }\n    int32_t count = 0;\n    for (int32_t i = 0; s->options[i].target != nullptr; i++) {\n        if (!M_IsOptionHidden(&s->options[i])) {\n            count++;\n        }\n    }\n    return count;\n}\n\nstatic M_ENUM_LOOKUP M_GetEnumEntry(const UI_SETTINGS_OPTION *const option)\n{\n    M_ENUM_LOOKUP result = {\n        .position = -1,\n        .count = 0,\n    };\n    int32_t current_pos = 0;\n    const UI_SETTINGS_ENUM_ENTRY *entry =\n        &((UI_SETTINGS_ENUM_ENTRY *)option->misc)[0];\n    while (entry->value != -1) {\n        if (entry->value == *(int32_t *)option->target) {\n            result.position = current_pos;\n        }\n        entry++;\n        current_pos++;\n        result.count++;\n    }\n    return result;\n}\n\nstatic int32_t M_FindNextAvailableEnumPosition(\n    const UI_SETTINGS_OPTION *const option,\n    const M_ENUM_LOOKUP *const enum_lookup, const int32_t dir)\n{\n    if (enum_lookup->position < 0 || enum_lookup->count <= 0 || dir == 0) {\n        return -1;\n    }\n    const UI_SETTINGS_ENUM_ENTRY *const entries = option->misc;\n    const int32_t step = dir < 0 ? -1 : 1;\n\n    for (int32_t pos = enum_lookup->position + step;\n         pos >= 0 && pos < enum_lookup->count; pos += step) {\n        if (M_IsEnumEntryAvailable(option, &entries[pos])) {\n            return pos;\n        }\n    }\n\n    return -1;\n}\n\nstatic const char *M_FormatRowValue(\n    const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx)\n{\n    const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx);\n    if (option == nullptr) {\n        return nullptr;\n    }\n    if (option->custom_handler.format_value != nullptr) {\n        return option->custom_handler.format_value(option);\n    }\n\n    const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option);\n    return Config_GetOptionValueAsString(cfg_opt, true);\n}\n\nstatic float M_MeasureMaxValueWidth(const UI_SETTINGS_OPTION *const option)\n{\n    if (option->custom_handler.format_value != nullptr) {\n        const char *const value = option->custom_handler.format_value(option);\n        const float result = UI_Label_MeasureW(value);\n        return result;\n    }\n\n    if (M_IsBarColorEnum(option)) {\n        return M_BAR_WIDTH * g_Config.ui.text_scale;\n    }\n\n    switch (M_GetConfigOption(option)->type) {\n    case COT_BOOL: {\n        const float min_value_w = UI_Label_MeasureW(GS(\"general/misc/off\"));\n        const float max_value_w = UI_Label_MeasureW(GS(\"general/misc/on\"));\n        return MAX(min_value_w, max_value_w);\n    }\n    case COT_INT32: {\n        const char *const min_value_s =\n            String_FormatStatic(\"%d\", option->min_value);\n        const float min_value_w = UI_Label_MeasureW(min_value_s);\n        const char *const max_value_s =\n            String_FormatStatic(\"%d\", option->max_value);\n        const float max_value_w = UI_Label_MeasureW(max_value_s);\n        return MAX(min_value_w, max_value_w);\n    }\n    case COT_DOUBLE:\n    case COT_FLOAT: {\n        const char *const min_value_s =\n            String_FormatStatic(\"%.2f\", (double)option->min_value / 100.0);\n        const float min_value_w = UI_Label_MeasureW(min_value_s);\n        const char *const max_value_s =\n            String_FormatStatic(\"%.2f\", (double)option->max_value / 100.0);\n        const float max_value_w = UI_Label_MeasureW(max_value_s);\n        return MAX(min_value_w, max_value_w);\n    }\n    case COT_FLOAT_PERCENT: {\n        const char *const min_value_s =\n            String_FormatStatic(\"%.00f%%\", (double)option->min_value);\n        const float min_value_w = UI_Label_MeasureW(min_value_s);\n        const char *const max_value_s =\n            String_FormatStatic(\"%.00f%%\", (double)option->max_value);\n        const float max_value_w = UI_Label_MeasureW(max_value_s);\n        return MAX(min_value_w, max_value_w);\n    }\n    case COT_RGB888:\n        return UI_Label_MeasureW(\"#FFFFFF\") + 8.0f * g_Config.ui.text_scale\n            + 32.0f * g_Config.ui.text_scale;\n    case COT_STRING:\n        return UI_Label_MeasureW(*(char **)option->target);\n    case COT_DYNAMIC_ENUM: {\n        const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option);\n        float result = 0.0f;\n        const int32_t count = Config_DynamicEnum_GetValueCount(cfg_opt);\n        for (int32_t i = 0; i < count; i++) {\n            const char *const label = Config_DynamicEnum_GetLabelAt(cfg_opt, i);\n            result = MAX(result, UI_Label_MeasureW(label));\n        }\n        return result;\n    }\n    case COT_ENUM: {\n        const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option);\n        float result = 0.0f;\n        const UI_SETTINGS_ENUM_ENTRY *entry = option->misc;\n        const int32_t current_value = *(int32_t *)option->target;\n        while (entry->value != -1) {\n            const bool is_current = entry->value == current_value;\n            if (!is_current && !M_IsEnumEntryAvailable(option, entry)) {\n                entry++;\n                continue;\n            }\n            const char *const value =\n                EnumMap_GetLabel(cfg_opt->param, entry->value);\n            ASSERT(value != nullptr);\n            const float value_w = UI_Label_MeasureW(value);\n            result = MAX(result, value_w);\n            entry++;\n        }\n        return result;\n    }\n    default:\n        break;\n    }\n    return 0.0f;\n}\n\nstatic bool M_CanChangeValue(\n    const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx,\n    const int32_t dir)\n{\n    const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx);\n    if (option == nullptr || Config_IsOptionEnforced(option->target)) {\n        return false;\n    }\n    if (option->custom_handler.can_change_value != nullptr) {\n        return option->custom_handler.can_change_value(option, dir);\n    }\n\n    switch (M_GetConfigOption(option)->type) {\n    case COT_BOOL:\n        return true;\n\n    case COT_INT32:\n        if (dir < 0) {\n            return *(int32_t *)option->target > option->min_value;\n        } else if (dir > 0) {\n            return *(int32_t *)option->target < option->max_value;\n        }\n        break;\n\n    case COT_DOUBLE: {\n        const double target_value =\n            (round(*(double *)option->target * 100) + dir) / 100.0;\n        return target_value >= (double)option->min_value / 100.0\n            && target_value <= (double)option->max_value / 100.0;\n    }\n\n    case COT_FLOAT:\n    case COT_FLOAT_PERCENT: {\n        const float target_value =\n            (round(*(float *)option->target * 100) + dir) / 100.0f;\n        return target_value >= (float)option->min_value / 100.0f\n            && target_value <= (float)option->max_value / 100.0f;\n    }\n\n    case COT_RGB888:\n        return false;\n\n    case COT_STRING:\n        return false;\n\n    case COT_DYNAMIC_ENUM: {\n        const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option);\n        return Config_DynamicEnum_CanCycle(\n            cfg_opt, *(char **)option->target, dir);\n    }\n\n    case COT_ENUM: {\n        const M_ENUM_LOOKUP enum_lookup = M_GetEnumEntry(option);\n        ASSERT(enum_lookup.position >= 0);\n        return M_FindNextAvailableEnumPosition(option, &enum_lookup, dir) >= 0;\n    }\n\n    default:\n        break;\n    }\n    return false;\n}\n\nstatic bool M_RequestChangeValue(\n    const UI_SETTINGS_EDITOR_STATE *const s, const int32_t row_idx,\n    const int32_t dir)\n{\n    if (!M_CanChangeValue(s, row_idx, dir)) {\n        return false;\n    }\n\n    const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx);\n    if (option->custom_handler.request_change_value != nullptr) {\n        if (option->custom_handler.request_change_value(option, dir)) {\n            goto changed;\n        }\n        return false;\n    }\n\n    UI_SettingsEditor_RequestChange(option, dir);\nchanged:\n    Config_Update();\n    return true;\n}\n\nUI_SETTINGS_EDITOR_STATE *UI_SettingsEditor_Init(\n    const UI_SETTINGS_OPTION *const options)\n{\n    UI_SETTINGS_EDITOR_STATE *const s = Memory_Alloc(sizeof(*s));\n    s->options = options;\n    s->scroll = (UI_SCROLLABLE) {\n        .first_item = 0,\n        .sel_item = -1,\n        .max_items = 0,\n        .vis_items = 0,\n    };\n    s->color_editor = UI_ColorEditorDialog_Init();\n    return s;\n}\n\nvoid UI_SettingsEditor_Free(UI_SETTINGS_EDITOR_STATE *const s)\n{\n    if (s->description.show) {\n        UI_TextDialog_Free(s->description.state);\n        s->description.state = nullptr;\n        s->description.show = false;\n    }\n    UI_ColorEditorDialog_Free(s->color_editor);\n    s->color_editor = nullptr;\n    Memory_Free(s);\n}\n\nstatic float M_GetMaxLabelWidth(const UI_SETTINGS_EDITOR_STATE *const s)\n{\n    float result = -1.0f;\n    if (s->options != nullptr) {\n        for (int32_t i = 0; s->options[i].target != nullptr; i++) {\n            const UI_SETTINGS_OPTION *const option = &s->options[i];\n            const float label_w = UI_Label_MeasureW(M_GetOptionTitle(option));\n            result = MAX(label_w, result);\n        }\n    }\n    return result;\n}\n\nstatic float M_GetMaxValueWidth(const UI_SETTINGS_EDITOR_STATE *const s)\n{\n    float result = -1.0f;\n    if (s->options != nullptr) {\n        for (int32_t i = 0; s->options[i].target != nullptr; i++) {\n            const UI_SETTINGS_OPTION *const option = &s->options[i];\n            const float value_w = M_MeasureMaxValueWidth(option);\n            result = MAX(value_w, result);\n        }\n    }\n\n    result += UI_Label_MeasureW(\"\\\\{button left}\");\n    result += UI_Label_MeasureW(\"\\\\{button right}\");\n    result += UI_ROW_ARROWS_TIGHT * 2;\n    return result;\n}\n\nfloat UI_SettingsEditor_GetContentWidth(const UI_SETTINGS_EDITOR_STATE *const s)\n{\n    return M_GetMaxLabelWidth(s) + 20.0f * g_Config.ui.text_scale\n        + M_GetMaxValueWidth(s);\n}\n\nint32_t UI_SettingsEditor_GetItemCount(const UI_SETTINGS_EDITOR_STATE *const s)\n{\n    return M_GetRowCount(s);\n}\n\nvoid UI_SettingsEditor_RequestChange(\n    const UI_SETTINGS_OPTION *const option, const int32_t dir)\n{\n    int32_t delta = g_Input.slow ? option->delta_slow : option->delta_fast;\n    if (delta == 0) {\n        delta = 1;\n    }\n    delta *= dir;\n\n    switch (M_GetConfigOption(option)->type) {\n    case COT_BOOL:\n        *(bool *)option->target = !*(bool *)option->target;\n        break;\n    case COT_INT32:\n        *(int32_t *)option->target += delta;\n        break;\n    case COT_DOUBLE:\n        *(double *)option->target =\n            (round(*(double *)option->target * 100) + delta) / 100.0f;\n        if (*(double *)option->target == -0.0) {\n            *(double *)option->target = 0.0;\n        }\n        break;\n    case COT_FLOAT:\n    case COT_FLOAT_PERCENT:\n        *(float *)option->target =\n            (round(*(float *)option->target * 100) + delta) / 100.0f;\n        if (*(float *)option->target == -0.0f) {\n            *(float *)option->target = 0.0f;\n        }\n        break;\n    case COT_RGB888:\n        break;\n    case COT_ENUM: {\n        const UI_SETTINGS_ENUM_ENTRY *const entries = option->misc;\n        int32_t position = -1;\n        int32_t count = 0;\n        for (; entries[count].value != -1; count++) {\n            if (entries[count].value == *(int32_t *)option->target) {\n                position = count;\n            }\n        }\n        if (position < 0 || count <= 0 || delta == 0) {\n            break;\n        }\n        const int32_t step = delta < 0 ? -1 : 1;\n        for (int32_t pos = position + step; pos >= 0 && pos < count;\n             pos += step) {\n            const bool can_use =\n                option->custom_handler.is_enum_value_available == nullptr\n                || option->custom_handler.is_enum_value_available(\n                    option, entries[pos].value);\n            if (can_use) {\n                *(int32_t *)option->target = entries[pos].value;\n                break;\n            }\n        }\n        break;\n    }\n    case COT_DYNAMIC_ENUM: {\n        const CONFIG_OPTION *const cfg_opt = M_GetConfigOption(option);\n        const char *const next = Config_DynamicEnum_GetNext(\n            cfg_opt, *(char **)option->target, delta);\n        if (next != nullptr\n            || Config_DynamicEnum_IsValidValue(cfg_opt, nullptr)) {\n            Config_SetOptionValueFromString(cfg_opt, next);\n        }\n        break;\n    }\n    case COT_STRING:\n        break;\n    }\n}\n\nvoid UI_SettingsEditor_RecomputeSizes(\n    UI_SETTINGS_EDITOR_STATE *const s, const int32_t visible_rows)\n{\n    s->visible_rows = visible_rows;\n    UI_Scrollable_SetMaxItems(&s->scroll, M_GetRowCount(s));\n    UI_Scrollable_SetVisibleItems(&s->scroll, visible_rows);\n}\n\nUI_SCROLLABLE *UI_SettingsEditor_GetScrollable(\n    UI_SETTINGS_EDITOR_STATE *const s)\n{\n    return &s->scroll;\n}\n\nbool UI_SettingsEditor_Control(\n    UI_SETTINGS_EDITOR_STATE *const s, UI_SETTINGS_PHASE *const dialog_phase)\n{\n    if (UI_ColorEditorDialog_IsOpen(s->color_editor)) {\n        UI_ColorEditorDialog_Control(s->color_editor);\n        return true;\n    }\n    if (s->description.show) {\n        UI_TextDialog_Control(s->description.state);\n        if (g_InputDB.menu_back || g_InputDB.look) {\n            UI_TextDialog_Free(s->description.state);\n            s->description.state = nullptr;\n            s->description.show = false;\n        }\n        return true;\n    }\n\n    const int32_t sel_row = UI_Scrollable_GetSelectedItem(&s->scroll);\n\n    if (g_InputDB.menu_left && sel_row >= 0) {\n        M_RequestChangeValue(s, sel_row, -1);\n        return true;\n    }\n    if (g_InputDB.menu_right && sel_row >= 0) {\n        M_RequestChangeValue(s, sel_row, +1);\n        return true;\n    }\n\n    if (g_InputDB.menu_up) {\n        if (!UI_Scrollable_SelectPrev(&s->scroll, false)) {\n            *dialog_phase = UI_SETTINGS_PHASE_NAVIGATE_TABS;\n        }\n        return true;\n    }\n    if (g_InputDB.menu_down) {\n        if (!UI_Scrollable_SelectNext(&s->scroll, false)\n            && g_Config.ui.enable_wraparound) {\n            *dialog_phase = UI_SETTINGS_PHASE_NAVIGATE_TABS;\n        }\n        return true;\n    }\n    if (g_InputDB.menu_confirm && sel_row >= 0) {\n        const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, sel_row);\n        if (M_IsColorEditorOption(option)) {\n            UI_ColorEditorDialog_Open(s->color_editor, option);\n            return true;\n        }\n    }\n    if (g_InputDB.look && sel_row >= 0) {\n        const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, sel_row);\n        const char *const title = M_GetOptionTitle(option);\n        const char *const text = M_GetOptionDescription(option);\n        if (title != nullptr && text != nullptr) {\n            s->description.show = true;\n            s->description.state = UI_TextDialog_Init(\n                UI_GetCanvasWidth() * 2.0f / 3.0f, (size_t)s->visible_rows,\n                true);\n            return true;\n        }\n    }\n    if (g_InputDB.unbind_key && sel_row >= 0) {\n        const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, sel_row);\n        if (option != nullptr && option->target != nullptr\n            && !Config_IsOptionEnforced(option->target)\n            && !Config_IsOptionAtDefault(option->target)) {\n            Config_RestoreOptionDefault(option->target);\n            Config_Update();\n            return true;\n        }\n    }\n\n    return false;\n}\n\nvoid UI_SettingsEditor_DrawOverlay(UI_SETTINGS_EDITOR_STATE *const s)\n{\n    UI_ColorEditorDialog(s->color_editor);\n\n    if (s->description.show) {\n        const int32_t row = UI_Scrollable_GetSelectedItem(&s->scroll);\n        const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row);\n        if (option != nullptr) {\n            const char *title = M_GetOptionTitle(option);\n            const char *text = M_GetOptionDescription(option);\n            if (title != nullptr && text != nullptr) {\n                if (Config_IsOptionEnforced(option->target)) {\n                    title = String_FormatStatic(\"%s*\", title);\n                    text = String_FormatStatic(\n                        \"* %s\\n\\n%s\",\n                        *GS_PTR(\n                            \"general/settings/common/frozen_option_disclaimer\"),\n                        text);\n                }\n                UI_TextDialog(s->description.state, title, text);\n            }\n        }\n    }\n}\n\nstatic void M_OptionLabel(\n    const UI_SETTINGS_OPTION *const option, const char *const text,\n    const bool star_if_enforced)\n{\n    const bool is_available = option == nullptr\n        || option->custom_handler.is_available == nullptr\n        || option->custom_handler.is_available(option);\n    const bool is_enforced = star_if_enforced && option != nullptr\n        && Config_IsOptionEnforced(option->target);\n    const char *const suffix = is_enforced ? \"*\" : \"\";\n\n    if (!is_available) {\n        UI_LabelFmt(\"\\\\{dim}%s%s\\\\{/dim}\", text, suffix);\n    } else if (is_enforced) {\n        UI_LabelFmt(\"%s%s\", text, suffix);\n    } else {\n        UI_Label(text);\n    }\n}\n\nvoid UI_SettingsEditor_Draw(\n    UI_SETTINGS_EDITOR_STATE *const s, const UI_SCROLLABLE *const dialog_scroll,\n    const UI_SETTINGS_PHASE dialog_phase, const float row_width)\n{\n    const float max_label_w = M_GetMaxLabelWidth(s) / g_Config.ui.text_scale;\n    const float max_value_w = M_GetMaxValueWidth(s) / g_Config.ui.text_scale;\n    float label_w = max_label_w;\n    const float total_w = max_label_w + 20.0f + max_value_w;\n    if (row_width > total_w) {\n        label_w += row_width - total_w;\n    }\n\n    const int32_t sel_row = UI_Scrollable_GetSelectedItem(dialog_scroll);\n\n    if (dialog_scroll->vis_items == 0) {\n        return;\n    }\n\n    UI_BeginStack(UI_STACK_VERTICAL);\n    for (int32_t i = 0; i < dialog_scroll->vis_items; i++) {\n        const int32_t row = dialog_scroll->first_item + i;\n        if (row >= dialog_scroll->max_items) {\n            UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n            continue;\n        }\n\n        const bool is_row_focused =\n            dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS && row == sel_row;\n        if (!UI_Scrollable_IsItemVisible(dialog_scroll, row)) {\n            UI_BeginResize(-1.0f, 0.0f);\n        } else {\n            UI_BeginResize(-1.0f, -1.0f);\n        }\n\n        UI_BeginPad(\n            g_TRVersion == 1 ? -1.0f : 0.0f, g_TRVersion == 1 ? -1.0f : 0.0f);\n        if (is_row_focused) {\n            UI_BeginFrame(UI_FRAME_SELECTED_OPTION);\n        }\n        UI_BeginPad(\n            (g_TRVersion == 1 ? 1.0f : 0.0f), g_TRVersion == 1 ? 1.0f : 0.0f);\n        UI_BeginStackEx((UI_STACK_SETTINGS) {\n            .orientation = UI_STACK_HORIZONTAL,\n            .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        });\n        UI_BeginResize(label_w, -1.0f);\n        {\n            const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row);\n            const char *const name =\n                option != nullptr ? M_GetOptionTitle(option) : \"\";\n            M_OptionLabel(option, name, true);\n        }\n        UI_EndResize();\n        UI_Spacer(20.0f, 0.0f);\n\n        UI_BeginResize(max_value_w, -1.0f);\n        UI_BeginAnchor(1.0f, 0.5f);\n\n        UI_BeginRowArrows(\n            is_row_focused && M_CanChangeValue(s, row, -1),\n            is_row_focused && M_CanChangeValue(s, row, +1),\n            UI_ROW_ARROWS_MEDIUM);\n        {\n            const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row);\n            if (M_IsBarColorEnum(option)) {\n                UI_Bar((UI_BAR_SETTINGS) {\n                    .w = M_BAR_WIDTH,\n                    .h = M_BAR_HEIGHT,\n                    .value = 100,\n                    .max_value = 100,\n                    .type = M_GetBarType(option),\n                    .preview = true,\n                });\n            } else if (M_IsColorEditorOption(option)) {\n                const char *const value = M_FormatRowValue(s, row);\n                const RGB_888 *const color = option->target;\n                UI_BeginStackEx((UI_STACK_SETTINGS) {\n                    .orientation = UI_STACK_HORIZONTAL,\n                    .align = { .v = UI_STACK_V_ALIGN_CENTER },\n                    .spacing = { .h = 4.0f },\n                });\n                M_OptionLabel(option, value, false);\n                UI_ColorSwatch((UI_COLOR_SWATCH_SETTINGS) {\n                    .color = *color,\n                    .w = UI_TEXT_HEIGHT - 2.0f,\n                    .h = UI_TEXT_HEIGHT - 2.0f,\n                });\n                UI_EndStack();\n            } else {\n                const char *const value = M_FormatRowValue(s, row);\n                M_OptionLabel(option, value, false);\n            }\n        }\n        UI_EndRowArrows();\n\n        UI_EndAnchor();\n        UI_EndResize();\n\n        UI_EndStack();\n\n        UI_EndPad();\n        if (is_row_focused) {\n            UI_EndFrame();\n        }\n        UI_EndPad();\n\n        UI_EndResize();\n    }\n    UI_EndStack();\n}\n\nvoid UI_SettingsEditor_DrawFooter(\n    UI_SETTINGS_EDITOR_STATE *const s, const UI_SETTINGS_PHASE dialog_phase)\n{\n    const int32_t row_idx = UI_Scrollable_GetSelectedItem(&s->scroll);\n    const UI_SETTINGS_OPTION *const option = M_GetOptionByRow(s, row_idx);\n\n    const bool can_edit_value = dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS\n        && row_idx >= 0 && option != nullptr\n        && M_GetConfigOption(option)->type == COT_RGB888;\n    const bool can_examine = dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS\n        && row_idx >= 0 && option != nullptr\n        && M_GetOptionDescription(option) != nullptr\n        && M_GetOptionTitle(option) != nullptr;\n    const bool can_restore_default =\n        dialog_phase == UI_SETTINGS_PHASE_EDIT_SETTINGS && row_idx >= 0\n        && option != nullptr && option->target != nullptr\n        && !Config_IsOptionEnforced(option->target)\n        && !Config_IsOptionAtDefault(option->target);\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        .spacing = { .h = 20 },\n    });\n    UI_BeginHide(!can_examine && !can_edit_value);\n    if (can_edit_value) {\n        UI_LabelFmt(\n            \"\\\\{input action} %s\", GS(\"general/settings/common/edit_value\"));\n    } else {\n        UI_LabelFmt(\n            \"\\\\{input look} %s\", GS(\"general/settings/common/toggle_help\"));\n    }\n    UI_EndHide();\n    UI_BeginHide(!can_restore_default);\n    UI_LabelFmt(\n        \"\\\\{input unbind_key} %s\",\n        GS(\"general/settings/common/restore_default\"));\n    UI_EndHide();\n    UI_EndStack();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/settings_editor.h",
    "content": "#pragma once\n\n#include <trx/game/ui/dialogs/settings.h>\n\ntypedef struct UI_SETTINGS_EDITOR_STATE UI_SETTINGS_EDITOR_STATE;\n\nUI_SETTINGS_EDITOR_STATE *UI_SettingsEditor_Init(\n    const UI_SETTINGS_OPTION *options);\nvoid UI_SettingsEditor_Free(UI_SETTINGS_EDITOR_STATE *s);\n\nvoid UI_SettingsEditor_RecomputeSizes(\n    UI_SETTINGS_EDITOR_STATE *s, int32_t visible_rows);\nUI_SCROLLABLE *UI_SettingsEditor_GetScrollable(UI_SETTINGS_EDITOR_STATE *s);\n\nbool UI_SettingsEditor_Control(\n    UI_SETTINGS_EDITOR_STATE *s, UI_SETTINGS_PHASE *dialog_phase);\n\nvoid UI_SettingsEditor_Draw(\n    UI_SETTINGS_EDITOR_STATE *s, const UI_SCROLLABLE *dialog_scroll,\n    UI_SETTINGS_PHASE dialog_phase, float row_width);\nvoid UI_SettingsEditor_DrawOverlay(UI_SETTINGS_EDITOR_STATE *s);\nvoid UI_SettingsEditor_DrawFooter(\n    UI_SETTINGS_EDITOR_STATE *s, UI_SETTINGS_PHASE dialog_phase);\n\nfloat UI_SettingsEditor_GetContentWidth(const UI_SETTINGS_EDITOR_STATE *s);\nint32_t UI_SettingsEditor_GetItemCount(const UI_SETTINGS_EDITOR_STATE *s);\nvoid UI_SettingsEditor_RequestChange(\n    const UI_SETTINGS_OPTION *option, int32_t dir);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/settings_tabs.c",
    "content": "#include <trx/game/ui/dialogs/settings_tabs.h>\n\n#include <trx/game/ui/dialogs/config_presets.h>\n#include <trx/game/ui/dialogs/settings_editor.h>\n\nstatic bool M_EditorControl(\n    void *const user_data, UI_SETTINGS_PHASE *const phase)\n{\n    return UI_SettingsEditor_Control(user_data, phase);\n}\n\nstatic void M_EditorDraw(\n    void *const user_data, const UI_SETTINGS_PHASE phase, const float row_width)\n{\n    UI_SETTINGS_EDITOR_STATE *const editor = user_data;\n    UI_SettingsEditor_Draw(\n        editor, UI_SettingsEditor_GetScrollable(editor), phase, row_width);\n}\n\nstatic void M_EditorDrawFooter(\n    void *const user_data, const UI_SETTINGS_PHASE phase)\n{\n    UI_SettingsEditor_DrawFooter(user_data, phase);\n}\n\nstatic void M_EditorDrawOverlay(void *const user_data)\n{\n    UI_SettingsEditor_DrawOverlay(user_data);\n}\n\nstatic void M_EditorFree(void *const user_data)\n{\n    UI_SettingsEditor_Free(user_data);\n}\n\nstatic UI_SCROLLABLE *M_EditorGetScrollable(void *const user_data)\n{\n    return UI_SettingsEditor_GetScrollable(user_data);\n}\n\nstatic void M_EditorRecompute(void *const user_data, const int32_t visible_rows)\n{\n    UI_SettingsEditor_RecomputeSizes(user_data, visible_rows);\n}\n\nstatic float M_EditorGetContentWidth(void *const user_data)\n{\n    return UI_SettingsEditor_GetContentWidth(user_data);\n}\n\nstatic int32_t M_EditorGetItemCount(void *const user_data)\n{\n    return UI_SettingsEditor_GetItemCount(user_data);\n}\n\nstatic const UI_SETTINGS_TAB_OPS m_EditorOps = {\n    .control = M_EditorControl,\n    .draw = M_EditorDraw,\n    .draw_footer = M_EditorDrawFooter,\n    .draw_overlay = M_EditorDrawOverlay,\n    .free = M_EditorFree,\n    .get_scrollable = M_EditorGetScrollable,\n    .recompute = M_EditorRecompute,\n    .get_content_width = M_EditorGetContentWidth,\n    .get_content_height = nullptr,\n    .get_item_count = M_EditorGetItemCount,\n};\n\nUI_SETTINGS_TAB UI_SettingsTab_MakeEditor(\n    const GAME_STRING_ID header_gs, const UI_SETTINGS_OPTION *const options)\n{\n    return (UI_SETTINGS_TAB) {\n        .header_gs = header_gs,\n        .ops = &m_EditorOps,\n        .user_data = UI_SettingsEditor_Init(options),\n    };\n}\n\nstatic bool M_PresetsControl(\n    void *const user_data, UI_SETTINGS_PHASE *const phase)\n{\n    if (UI_ConfigPresets_Control(user_data)) {\n        *phase = UI_SETTINGS_PHASE_NAVIGATE_TABS;\n    }\n    return false;\n}\n\nstatic void M_PresetsDraw(\n    void *const user_data, const UI_SETTINGS_PHASE, const float)\n{\n    UI_ConfigPresets(user_data);\n}\n\nstatic void M_PresetsDrawOverlay(void *const user_data)\n{\n    UI_ConfigPresetsApplyModal(user_data);\n}\n\nstatic void M_PresetsFree(void *const user_data)\n{\n    UI_ConfigPresets_Free(user_data);\n}\n\nstatic UI_SCROLLABLE *M_PresetsGetScrollable(void *const user_data)\n{\n    return UI_ConfigPresets_GetScrollable(user_data);\n}\n\nstatic void M_PresetsRecompute(\n    void *const user_data, const int32_t visible_rows)\n{\n    UI_ConfigPresets_RecomputeSizes(user_data, visible_rows);\n}\n\nstatic float M_PresetsGetContentWidth(void *const user_data)\n{\n    return UI_ConfigPresets_GetContentWidth(user_data);\n}\n\nstatic float M_PresetsGetContentHeight(void *const user_data)\n{\n    return UI_ConfigPresets_GetContentHeight(user_data);\n}\n\nstatic int32_t M_PresetsGetItemCount(void *const user_data)\n{\n    return UI_ConfigPresets_GetItemCount(user_data);\n}\n\nstatic const UI_SETTINGS_TAB_OPS m_PresetsOps = {\n    .control = M_PresetsControl,\n    .draw = M_PresetsDraw,\n    .draw_overlay = M_PresetsDrawOverlay,\n    .free = M_PresetsFree,\n    .get_scrollable = M_PresetsGetScrollable,\n    .recompute = M_PresetsRecompute,\n    .get_content_width = M_PresetsGetContentWidth,\n    .get_content_height = M_PresetsGetContentHeight,\n    .get_item_count = M_PresetsGetItemCount,\n};\n\nUI_SETTINGS_TAB UI_SettingsTab_MakePresets(const GAME_STRING_ID header_gs)\n{\n    return (UI_SETTINGS_TAB) {\n        .header_gs = header_gs,\n        .ops = &m_PresetsOps,\n        .user_data = UI_ConfigPresets_Init(),\n    };\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/settings_tabs.h",
    "content": "#pragma once\n\n// settings tab contracts and tab factory helpers\n\n#include <trx/core/utils.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/ui/scrollable.h>\n\ntypedef struct UI_SETTINGS_OPTION UI_SETTINGS_OPTION;\n\ntypedef enum {\n    UI_SETTINGS_PHASE_NAVIGATE_TABS,\n    UI_SETTINGS_PHASE_EDIT_SETTINGS,\n} UI_SETTINGS_PHASE;\n\ntypedef bool (*UI_SETTINGS_TAB_CONTROL_FUNC)(\n    void *user_data, UI_SETTINGS_PHASE *phase);\ntypedef void (*UI_SETTINGS_TAB_DRAW_FUNC)(\n    void *user_data, UI_SETTINGS_PHASE phase, float row_width);\ntypedef void (*UI_SETTINGS_TAB_DRAW_FOOTER_FUNC)(\n    void *user_data, UI_SETTINGS_PHASE phase);\ntypedef void (*UI_SETTINGS_TAB_FREE_FUNC)(void *user_data);\ntypedef void (*UI_SETTINGS_TAB_DRAW_OVERLAY_FUNC)(void *user_data);\ntypedef UI_SCROLLABLE *(*UI_SETTINGS_TAB_GET_SCROLLABLE_FUNC)(void *user_data);\ntypedef void (*UI_SETTINGS_TAB_RECOMPUTE_FUNC)(\n    void *user_data, int32_t visible_rows);\ntypedef float (*UI_SETTINGS_TAB_GET_CONTENT_WIDTH_FUNC)(void *user_data);\ntypedef float (*UI_SETTINGS_TAB_GET_CONTENT_HEIGHT_FUNC)(void *user_data);\ntypedef int32_t (*UI_SETTINGS_TAB_GET_ITEM_COUNT_FUNC)(void *user_data);\n\ntypedef struct UI_SETTINGS_TAB_OPS {\n    UI_SETTINGS_TAB_CONTROL_FUNC control;\n    UI_SETTINGS_TAB_DRAW_FUNC draw;\n    UI_SETTINGS_TAB_DRAW_FOOTER_FUNC draw_footer;\n    UI_SETTINGS_TAB_DRAW_OVERLAY_FUNC draw_overlay;\n    UI_SETTINGS_TAB_FREE_FUNC free;\n    UI_SETTINGS_TAB_GET_SCROLLABLE_FUNC get_scrollable;\n    UI_SETTINGS_TAB_RECOMPUTE_FUNC recompute;\n    UI_SETTINGS_TAB_GET_CONTENT_WIDTH_FUNC get_content_width;\n    UI_SETTINGS_TAB_GET_CONTENT_HEIGHT_FUNC get_content_height;\n    UI_SETTINGS_TAB_GET_ITEM_COUNT_FUNC get_item_count;\n} UI_SETTINGS_TAB_OPS;\n\ntypedef struct UI_SETTINGS_TAB {\n    GAME_STRING_ID header_gs;\n    const UI_SETTINGS_TAB_OPS *ops;\n    void *user_data;\n} UI_SETTINGS_TAB;\n\nUI_SETTINGS_TAB UI_SettingsTab_MakeEditor(\n    GAME_STRING_ID header_gs, const UI_SETTINGS_OPTION *options);\nUI_SETTINGS_TAB UI_SettingsTab_MakePresets(GAME_STRING_ID header_gs);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/sound_settings.c",
    "content": "#include <trx/game/ui/dialogs/sound_settings.h>\n\n#include <trx/config.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/ui/dialogs/setting_helpers/enums.h>\n#include <trx/game/ui/dialogs/setting_helpers/handlers.h>\n#include <trx/game/ui/dialogs/settings_tabs.h>\n\nstatic const UI_SETTINGS_OPTION m_SoundVolumeOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/sound_volume.def>\n    { .target = nullptr },\n};\n\nstatic const UI_SETTINGS_OPTION m_SoundMiscOptions[] = {\n#include <trx/game/ui/dialogs/setting_tabs/sound_misc.def>\n    { .target = nullptr },\n};\n\nUI_SETTINGS_DIALOG_STATE *UI_SoundSettings_Init(void)\n{\n    const UI_SETTINGS_TAB tabs[] = {\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/sound/tabs/volume\"), m_SoundVolumeOptions),\n        UI_SettingsTab_MakeEditor(\n            GS_ID(\"general/settings/sound/tabs/misc\"), m_SoundMiscOptions),\n    };\n\n    return UI_SettingsDialog_Init(\n        GS_ID(\"general/settings/sound/title\"), ARRAY_SIZE(tabs), tabs);\n}\n\nvoid UI_SoundSettings_Free(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SettingsDialog_Free(s);\n}\n\nbool UI_SoundSettings_Control(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    return UI_SettingsDialog_Control(s);\n}\n\nvoid UI_SoundSettings(UI_SETTINGS_DIALOG_STATE *const s)\n{\n    UI_SettingsDialog(s);\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/sound_settings.h",
    "content": "// UI dialog for adjusting music and sound volumes\n#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/settings.h>\n\n// Initialize the sound settings dialog state.\nUI_SETTINGS_DIALOG_STATE *UI_SoundSettings_Init(void);\n\n// Free resources used by the sound settings dialog.\nvoid UI_SoundSettings_Free(UI_SETTINGS_DIALOG_STATE *s);\n\n// Handle input/control for the sound settings dialog.\n// Returns true if the dialog should be closed.\nbool UI_SoundSettings_Control(UI_SETTINGS_DIALOG_STATE *s);\n\n// Render the sound settings dialog.\nvoid UI_SoundSettings(UI_SETTINGS_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/stats.c",
    "content": "#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/const.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/game_flow.h>\n#include <trx/game/gym.h>\n#include <trx/game/objects.h>\n#include <trx/game/savegame.h>\n#include <trx/game/stats.h>\n#include <trx/game/ui.h>\n#include <trx/version.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_MIN_ASSAULT_COURSE_ROWS 7\n\ntypedef enum {\n    M_ROW_GENERIC,\n    M_ROW_LEVEL_COUNTER,\n    M_ROW_TIMER,\n    M_ROW_AUTO_SECRETS,\n    M_ROW_ICON_SECRETS,\n    M_ROW_NUM_SECRETS,\n    M_ROW_CRYSTALS,\n    M_ROW_PICKUPS,\n    M_ROW_DEATHS,\n    M_ROW_KILLS,\n    M_ROW_AMMO,\n    M_ROW_AMMO_USED,\n    M_ROW_AMMO_HITS,\n    M_ROW_MEDIPACKS_USED,\n    M_ROW_DISTANCE_TRAVELLED,\n    M_ROW_ASSAULT_COURSE_TITLE,\n    M_ROW_ASSAULT_COURSE_ROW,\n    M_ROW_ASSAULT_NO_TIMES_SET,\n    M_ROW_RACETRACK_TITLE,\n    M_ROW_RACETRACK_ROW,\n    M_ROW_SPACER,\n} M_ROW_ROLE;\n\ntypedef struct {\n    float window_margin;\n    float window_y;\n    float title_spacing;\n    float min_width;\n    float row_spacing;\n    bool use_full_hours;\n} M_LOOK;\n\ntypedef struct UI_STATS_DIALOG_STATE {\n    UI_STATS_DIALOG_ARGS args;\n    UI_SCROLLABLE scrollable;\n\n    union {\n        struct {\n            const STATS_COMMON *stats;\n            const LEVEL_MAX_STATS *max_stats;\n            FINAL_STATS final_stats;\n            LEVEL_MAX_STATS adjusted_max_stats;\n        };\n        const GYM_TRACK_STATS *assault_stats[GYM_TRACK_NUMBER_OF];\n    };\n\n    const M_LOOK *look;\n    bool has_floordata_secrets;\n    bool has_visible_rows;\n} UI_STATS_DIALOG_STATE;\n\nstatic const M_LOOK m_Looks[TR_VERSION_COUNT] = {\n    [0] = {\n        .window_margin = 0.0f,\n        .window_y = 0.5f,\n        .title_spacing = 4.0f,\n        .min_width = 0.0f,\n        .row_spacing = 30.0f,\n        .use_full_hours = false,\n    },\n    [1] = {\n        .window_margin = 40.0f,\n        .window_y = 1.0f,\n        .title_spacing = 3.0f,\n        .min_width = 290.0f,\n        .row_spacing = 25.0f,\n        .use_full_hours = true,\n    },\n    [2] = {\n        .window_margin = 40.0f,\n        .window_y = 1.0f,\n        .title_spacing = 3.0f,\n        .min_width = 290.0f,\n        .row_spacing = 25.0f,\n        .use_full_hours = true,\n    },\n};\n\nstatic const char *M_FormatRecordTime(const int32_t total_frames)\n{\n    const int32_t total_seconds = total_frames / LOGIC_FPS;\n    const int32_t minutes = (total_seconds / 60) % 60;\n    const int32_t seconds = total_seconds % 60;\n    const int32_t centiseconds = total_frames % LOGIC_FPS / (LOGIC_FPS / 10);\n    return String_FormatStatic(\n        \"%02d:%02d.%-2d\", minutes, seconds, centiseconds);\n}\n\nstatic const char *M_FormatTime(\n    const UI_STATS_DIALOG_STATE *const s, const int32_t total_frames)\n{\n    const int32_t total_seconds = total_frames / LOGIC_FPS;\n    const int32_t hours = total_seconds / 3600;\n    const int32_t minutes = (total_seconds / 60) % 60;\n    const int32_t seconds = total_seconds % 60;\n    if (s->look->use_full_hours) {\n        return String_FormatStatic(\"%02d:%02d:%02d\", hours, minutes, seconds);\n    } else if (hours != 0) {\n        return String_FormatStatic(\"%d:%02d:%02d\", hours, minutes, seconds);\n    } else {\n        return String_FormatStatic(\"%d:%02d\", minutes, seconds);\n    }\n}\n\nstatic const char *M_FormatDistance(int32_t distance)\n{\n    distance /= 445;\n    if (distance < 1000) {\n        return String_FormatStatic(\"%dm\", distance);\n    } else {\n        return String_FormatStatic(\n            \"%d.%02dkm\", distance / 1000, (distance % 1000) / 10);\n    }\n}\n\nstatic void M_AdjustMaxKills(\n    UI_STATS_DIALOG_STATE *const s, const bool include_allies)\n{\n    if (s->max_stats == nullptr) {\n        return;\n    }\n    s->adjusted_max_stats = *s->max_stats;\n    s->adjusted_max_stats.max_kill_count =\n        s->adjusted_max_stats.max_kill_non_ally_count;\n    if (include_allies) {\n        s->adjusted_max_stats.max_kill_count +=\n            s->adjusted_max_stats.max_kill_ally_count;\n    }\n    s->max_stats = &s->adjusted_max_stats;\n}\n\nstatic bool M_HasHurtAlliesSoFar(const int32_t level_num)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    if (level_table == nullptr || level_num <= 0) {\n        return false;\n    }\n    for (int32_t i = 0; i <= level_num && i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        if (resume != nullptr && resume->flags.available\n            && resume->hurt_allies) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic bool M_HasHurtAlliesEver(const bool include_bonus_levels)\n{\n    const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);\n    if (level_table == nullptr) {\n        return false;\n    }\n    for (int32_t i = 0; i < level_table->count; i++) {\n        const GF_LEVEL *const level = &level_table->levels[i];\n        if (!(level->type == GFL_NORMAL\n              || (level->type == GFL_BONUS && include_bonus_levels))) {\n            continue;\n        }\n        const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);\n        if (resume != nullptr && resume->hurt_allies) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic void M_FormatIconSecrets(\n    char *const out, const LEVEL_STATS *const level_stats)\n{\n    char *ptr = out;\n    int32_t num_secrets = 0;\n    for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n        if (!Stats_IsSecretValid(i)) {\n            continue;\n        }\n\n        const bool has_secret = Stats_HasSecret(i);\n        if (!has_secret && out == ptr) {\n            // Do not reserve space pointlessly.\n            // Good: [secret][ ][ ]\n            // Bad:  [ ][ ][secret] – should be just [secret]\n            continue;\n        }\n        const OBJECT_ID obj_id = Stats_GetSecretObject(i);\n        if (obj_id != NO_OBJECT) {\n            ptr += sprintf(\n                ptr, has_secret ? \"\\\\{secret %d}\" : \"\\\\{i}\\\\{secret %d}\\\\{/i}\",\n                obj_id + 1 - O_SECRET_1);\n        }\n        if (has_secret) {\n            num_secrets++;\n        }\n    }\n    *ptr++ = '\\0';\n\n    if (num_secrets == 0) {\n        strcpy(out, GS(\"general/stats/none\"));\n    }\n}\n\nstatic void M_RowCentered(\n    const UI_STATS_DIALOG_STATE *const s, const char *const text)\n{\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_CENTER },\n    });\n    if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) {\n        char *text_upper = String_ToUpper(text);\n        UI_Label(text_upper);\n        Memory_FreePointer(&text_upper);\n    } else {\n        UI_Label(text);\n    }\n    UI_EndStack();\n}\n\nstatic void M_Row(\n    const UI_STATS_DIALOG_STATE *const s, const char *const key,\n    const char *const value)\n{\n    if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) {\n        char *key_upper = String_ToUpper(key);\n        UI_BeginStack(UI_STACK_HORIZONTAL);\n        UI_Label(key_upper);\n        UI_Label(\" \");\n        UI_Label(value);\n        UI_EndStack();\n        Memory_FreePointer(&key_upper);\n    } else {\n        UI_BeginStackEx((UI_STACK_SETTINGS) {\n            .orientation = UI_STACK_HORIZONTAL,\n            .spacing = { .h = s->look->row_spacing },\n            .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE },\n        });\n        UI_Label(key);\n        UI_Label(value);\n        UI_EndStack();\n    }\n}\n\nstatic void M_RowFromRole(\n    const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role,\n    const int32_t param)\n{\n    const char *const num_fmt = g_Config.ui.stats.show_totals\n        ? GS(\"general/stats/detail_fmt\")\n        : GS(\"general/stats/basic_fmt\");\n\n    switch (role) {\n    case M_ROW_LEVEL_COUNTER:\n        M_Row(\n            s, GS(\"general/stats/level\"),\n            String_FormatStatic(\n                GS(\"general/stats/detail_fmt\"),\n                GF_GetLevelOrdinalNumber(\n                    GFLT_MAIN, GF_GetLevel(GFLT_MAIN, s->args.level_num)),\n                GF_GetLevelCount(GFLT_MAIN)));\n        break;\n\n    case M_ROW_TIMER:\n        M_Row(\n            s, GS(\"general/stats/time_taken\"),\n            M_FormatTime(s, s->stats->timer));\n        break;\n\n    case M_ROW_AUTO_SECRETS:\n        if (s->args.mode == UI_STATS_DIALOG_MODE_FINAL\n            || s->has_floordata_secrets) {\n            M_RowFromRole(s, M_ROW_NUM_SECRETS, 0);\n        } else {\n            M_RowFromRole(s, M_ROW_ICON_SECRETS, 0);\n        }\n        break;\n\n    case M_ROW_ICON_SECRETS: {\n        char buf[256];\n        M_FormatIconSecrets(buf, (LEVEL_STATS *)s->stats);\n        M_Row(s, GS(\"general/stats/secrets\"), buf);\n        break;\n    }\n\n    case M_ROW_NUM_SECRETS:\n        M_Row(\n            s, GS(\"general/stats/secrets\"),\n            String_FormatStatic(\n                GS(\"general/stats/detail_fmt\"), s->stats->secret_count,\n                s->max_stats->max_secret_count));\n        break;\n\n    case M_ROW_CRYSTALS:\n        M_Row(\n            s, GS(\"general/stats/crystals\"),\n            String_FormatStatic(\n                num_fmt, s->stats->crystal_count,\n                s->max_stats->max_crystal_count));\n        break;\n\n    case M_ROW_PICKUPS:\n        M_Row(\n            s, GS(\"general/stats/pickups\"),\n            String_FormatStatic(\n                num_fmt, s->stats->pickup_count,\n                s->max_stats->max_pickup_count));\n        break;\n\n    case M_ROW_KILLS:\n        M_Row(\n            s, GS(\"general/stats/kills\"),\n            String_FormatStatic(\n                num_fmt, s->stats->kill_count, s->max_stats->max_kill_count));\n        break;\n\n    case M_ROW_DEATHS:\n        M_Row(\n            s, GS(\"general/stats/deaths\"),\n            String_FormatStatic(\n                GS(\"general/stats/basic_fmt\"), s->stats->death_count));\n        break;\n\n    case M_ROW_AMMO:\n        M_Row(\n            s, GS(\"general/stats/ammo\"),\n            String_FormatStatic(\n                GS(\"general/misc/pagination_nav\"), s->stats->ammo_hits,\n                s->stats->ammo_used));\n        break;\n\n    case M_ROW_AMMO_USED:\n        M_Row(\n            s, GS(\"general/stats/ammo_used\"),\n            String_FormatStatic(\"%d\", s->stats->ammo_used));\n        break;\n\n    case M_ROW_AMMO_HITS:\n        M_Row(\n            s, GS(\"general/stats/ammo_hits\"),\n            String_FormatStatic(\"%d\", s->stats->ammo_hits));\n        break;\n\n    case M_ROW_MEDIPACKS_USED:\n        M_Row(\n            s, GS(\"general/stats/medipacks_used\"),\n            String_FormatStatic(\"%.1f\", s->stats->medipacks_used));\n        break;\n\n    case M_ROW_DISTANCE_TRAVELLED:\n        M_Row(\n            s, GS(\"general/stats/distance_travelled\"),\n            M_FormatDistance(s->stats->distance_travelled));\n        break;\n\n    case M_ROW_ASSAULT_COURSE_TITLE:\n        M_RowCentered(s, GS(\"general/stats/gym_assault_course\"));\n        break;\n\n    case M_ROW_ASSAULT_COURSE_ROW:\n    case M_ROW_RACETRACK_ROW: {\n        const GYM_TRACK_TYPE track_type = role == M_ROW_ASSAULT_COURSE_ROW\n            ? GYM_TRACK_ASSAULT\n            : GYM_TRACK_QUAD;\n        const GYM_TRACK_ENTRY *const entry =\n            &s->assault_stats[track_type]->entries[param];\n        const char *const attempt_str = String_FormatStatic(\n            \"%2d: %s %d\", param + 1, GS(\"general/stats/assault_finish\"),\n            entry->attempt_num);\n        const char *const time_str = String_FormatStatic(\n            param == 0 ? GS(\"general/stats/assault_best_time_fmt\")\n                       : GS(\"general/stats/assault_other_times_fmt\"),\n            M_FormatRecordTime(entry->time));\n        if (g_TRVersion == 3) {\n            M_RowCentered(s, time_str);\n        } else {\n            M_Row(s, attempt_str, time_str);\n        }\n        break;\n    }\n\n    case M_ROW_ASSAULT_NO_TIMES_SET:\n        M_RowCentered(s, GS(\"general/stats/assault_no_times_set\"));\n        break;\n\n    case M_ROW_RACETRACK_TITLE:\n        M_RowCentered(s, GS(\"general/stats/gym_racetrack_course\"));\n        break;\n\n    case M_ROW_SPACER:\n        M_RowCentered(s, \" \");\n        break;\n\n    default:\n        break;\n    }\n}\n\nstatic bool M_EmitRow(\n    const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role,\n    const int32_t param)\n{\n    M_RowFromRole(s, role, param);\n    return true;\n}\n\nstatic bool M_EmitDummyRow(\n    const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role,\n    const int32_t param)\n{\n    return true;\n}\n\nstatic bool M_EmitConfiguredStatsRows(\n    const UI_STATS_DIALOG_STATE *const s, const bool dry_run)\n{\n    bool has_rows = false;\n    bool (*emit_row_func)(const UI_STATS_DIALOG_STATE *, M_ROW_ROLE, int32_t) =\n        dry_run ? M_EmitDummyRow : M_EmitRow;\n\n    if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) {\n        if (g_Config.ui.stats.show_kills) {\n            has_rows |= emit_row_func(s, M_ROW_KILLS, 0);\n        }\n        if (g_Config.ui.stats.show_pickups) {\n            has_rows |= emit_row_func(s, M_ROW_PICKUPS, 0);\n        }\n        if (g_Config.ui.stats.show_crystals\n            && s->max_stats->max_crystal_count != 0) {\n            has_rows |= emit_row_func(s, M_ROW_CRYSTALS, 0);\n        }\n        if (g_Config.ui.stats.show_secrets\n            && s->max_stats->max_secret_count != 0) {\n            has_rows |= emit_row_func(s, M_ROW_AUTO_SECRETS, 0);\n        }\n        if (g_Config.ui.stats.show_time_taken) {\n            has_rows |= emit_row_func(s, M_ROW_TIMER, 0);\n        }\n    } else {\n        if (g_Config.ui.stats.show_time_taken) {\n            has_rows |= emit_row_func(s, M_ROW_TIMER, 0);\n        }\n        if (g_Config.ui.stats.show_secrets\n            && s->max_stats->max_secret_count != 0) {\n            has_rows |= emit_row_func(s, M_ROW_AUTO_SECRETS, 0);\n        }\n        if (g_Config.ui.stats.show_crystals\n            && s->max_stats->max_crystal_count != 0) {\n            has_rows |= emit_row_func(s, M_ROW_CRYSTALS, 0);\n        }\n        if (g_Config.ui.stats.show_pickups) {\n            has_rows |= emit_row_func(s, M_ROW_PICKUPS, 0);\n        }\n        if (g_Config.ui.stats.show_kills) {\n            has_rows |= emit_row_func(s, M_ROW_KILLS, 0);\n        }\n    }\n\n    if (g_Config.ui.stats.show_ammo) {\n        if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) {\n            M_RowFromRole(s, M_ROW_AMMO_USED, 0);\n            M_RowFromRole(s, M_ROW_AMMO_HITS, 0);\n        } else {\n            M_RowFromRole(s, M_ROW_AMMO, 0);\n        }\n        has_rows = true;\n    }\n    if (g_Config.ui.stats.show_medipacks_used) {\n        has_rows |= emit_row_func(s, M_ROW_MEDIPACKS_USED, 0);\n    }\n    if (g_Config.ui.stats.show_distance_travelled) {\n        has_rows |= emit_row_func(s, M_ROW_DISTANCE_TRAVELLED, 0);\n    }\n    if (g_Config.ui.stats.show_deaths && s->stats->death_count >= 0) {\n        // Always use the sum of all levels for deaths.\n        // Deaths get stored in the resume info for the level they happen on,\n        // so if the player dies in Vilcabamba and reloads Caves, they should\n        // still see an incremented death counter.\n        has_rows |= emit_row_func(s, M_ROW_DEATHS, 0);\n    }\n\n    return has_rows;\n}\n\nstatic bool M_HasVisibleRows(const UI_STATS_DIALOG_STATE *const s)\n{\n    if (s->args.mode == UI_STATS_DIALOG_MODE_LEVEL\n        && g_Config.ui.stats.show_level_header) {\n        return true;\n    }\n\n    return M_EmitConfiguredStatsRows(s, true);\n}\n\nstatic void M_LevelStatsRows(const UI_STATS_DIALOG_STATE *const s)\n{\n    if (g_Config.ui.stats.show_level_header) {\n        M_EmitRow(s, M_ROW_LEVEL_COUNTER, 0);\n    }\n    if (!s->has_visible_rows) {\n        M_RowCentered(s, GS(\"general/osd/complete_level\"));\n        return;\n    }\n    M_EmitConfiguredStatsRows(s, false);\n}\n\nstatic void M_FinalStatsRows(const UI_STATS_DIALOG_STATE *const s)\n{\n    M_EmitConfiguredStatsRows(s, false);\n}\n\nstatic const char *M_GetDialogTitle(const UI_STATS_DIALOG_STATE *const s)\n{\n    switch (s->args.mode) {\n    case UI_STATS_DIALOG_MODE_LEVEL:\n        return GF_GetLevel(GFLT_MAIN, s->args.level_num)->title;\n    case UI_STATS_DIALOG_MODE_FINAL: {\n        const GF_LEVEL_TYPE level_type =\n            GF_GetLevel(GFLT_MAIN, s->args.level_num)->type;\n        const char *const title = level_type == GFL_BONUS\n            ? GS(\"general/stats/bonus_statistics\")\n            : GS(\"general/stats/final_statistics\");\n        return title;\n    }\n    case UI_STATS_DIALOG_MODE_ASSAULT_COURSE:\n        return GS(\"general/stats/assault_title\");\n    }\n    return nullptr;\n}\n\nstatic void M_AssaultCourseStatsRows(UI_STATS_DIALOG_STATE *const s)\n{\n    const int32_t record_limit = g_TRVersion >= 3 ? 3 : MAX_ASSAULT_TIMES;\n    const bool has_race_track = Gym_TrackManager_HasStats(GYM_TRACK_QUAD);\n    int32_t count = 0;\n\n#define L_EMIT_ROW(...)                                                        \\\n    M_RowFromRole(__VA_ARGS__);                                                \\\n    count++;\n\n    if (has_race_track) {\n        L_EMIT_ROW(s, M_ROW_ASSAULT_COURSE_TITLE, 0);\n    }\n\n    if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[0].time == 0) {\n        L_EMIT_ROW(s, M_ROW_ASSAULT_NO_TIMES_SET, 0);\n    } else {\n        for (int32_t i = 0; i < record_limit; i++) {\n            if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[i].time == 0) {\n                break;\n            }\n            L_EMIT_ROW(s, M_ROW_ASSAULT_COURSE_ROW, i);\n        }\n    }\n\n    if (has_race_track) {\n        L_EMIT_ROW(s, M_ROW_SPACER, 0);\n        L_EMIT_ROW(s, M_ROW_RACETRACK_TITLE, 0);\n        if (s->assault_stats[GYM_TRACK_QUAD]->entries[0].time == 0) {\n            L_EMIT_ROW(s, M_ROW_ASSAULT_NO_TIMES_SET, 0);\n        } else {\n            for (int32_t i = 0; i < record_limit; i++) {\n                if (s->assault_stats[GYM_TRACK_QUAD]->entries[i].time == 0) {\n                    break;\n                }\n                L_EMIT_ROW(s, M_ROW_RACETRACK_ROW, i);\n            }\n        }\n    }\n\n#undef L_EMIT_ROW\n\n    while (count < M_MIN_ASSAULT_COURSE_ROWS) {\n        M_RowFromRole(s, M_ROW_SPACER, 0);\n        count++;\n    }\n}\n\nstatic int32_t M_GetAssaultCourseRowCount(const UI_STATS_DIALOG_STATE *const s)\n{\n    const int32_t record_limit = g_TRVersion >= 3 ? 3 : MAX_ASSAULT_TIMES;\n    const bool has_race_track = Gym_TrackManager_HasStats(GYM_TRACK_QUAD);\n    int32_t count = 0;\n\n    if (has_race_track) {\n        count++;\n    }\n\n    if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[0].time == 0) {\n        count++;\n    } else {\n        for (int32_t i = 0; i < record_limit; i++) {\n            if (s->assault_stats[GYM_TRACK_ASSAULT]->entries[i].time == 0) {\n                break;\n            }\n            count++;\n        }\n    }\n\n    if (has_race_track) {\n        count += 2;\n        if (s->assault_stats[GYM_TRACK_QUAD]->entries[0].time == 0) {\n            count++;\n        } else {\n            for (int32_t i = 0; i < record_limit; i++) {\n                if (s->assault_stats[GYM_TRACK_QUAD]->entries[i].time == 0) {\n                    break;\n                }\n                count++;\n            }\n        }\n    }\n\n    return MAX(count, M_MIN_ASSAULT_COURSE_ROWS);\n}\n\nstatic UI_WINDOW_SETTINGS M_GetWindowSettings(\n    const UI_STATS_DIALOG_STATE *const s)\n{\n    const bool is_assault_mode =\n        s->args.mode == UI_STATS_DIALOG_MODE_ASSAULT_COURSE;\n    return (UI_WINDOW_SETTINGS) {\n        .title = M_GetDialogTitle(s),\n        .scrollable = is_assault_mode ? &s->scrollable : nullptr,\n        .title_spacing = s->look->title_spacing,\n    };\n}\n\nstatic void M_BeginDialog(const UI_STATS_DIALOG_STATE *const s)\n{\n    if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) {\n        UI_BeginStackEx((UI_STACK_SETTINGS) {\n            .orientation = UI_STACK_VERTICAL,\n            .spacing = { .v = 11.0f },\n            .align = { .h = UI_STACK_H_ALIGN_CENTER },\n        });\n        const char *const title = M_GetDialogTitle(s);\n        if (title != nullptr) {\n            UI_Label(title);\n        }\n    } else {\n        UI_BeginWindow(M_GetWindowSettings(s));\n    }\n    // ensure minimum dialog width\n    UI_Spacer(s->look->min_width, 0.0f);\n}\n\nstatic void M_EndDialog(const UI_STATS_DIALOG_STATE *const s)\n{\n    if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) {\n        UI_EndStack();\n    } else {\n        UI_EndWindow();\n    }\n}\n\nUI_STATS_DIALOG_STATE *UI_StatsDialog_Init(const UI_STATS_DIALOG_ARGS args)\n{\n    UI_STATS_DIALOG_STATE *const s = Memory_Alloc(sizeof(*s));\n\n    s->has_floordata_secrets = false;\n    s->scrollable.vis_items = M_MIN_ASSAULT_COURSE_ROWS;\n    s->args = args;\n    s->look = &m_Looks[g_TRVersion - 1];\n\n    switch (args.mode) {\n    case UI_STATS_DIALOG_MODE_LEVEL:\n        const GF_LEVEL *const current_level =\n            GF_GetLevel(GFLT_MAIN, s->args.level_num);\n        const RESUME_INFO *const current_info =\n            Savegame_GetCurrentInfo(current_level);\n        s->stats = (const STATS_COMMON *)&current_info->stats;\n        s->max_stats = Stats_GetLevelMaxStats(current_level);\n        const bool include_allies = M_HasHurtAlliesSoFar(s->args.level_num);\n        M_AdjustMaxKills(s, include_allies);\n\n        const GF_LEVEL *const level = Game_GetCurrentLevel();\n        for (int32_t i = 0; i < STATS_MAX_SECRETS; i++) {\n            if (s->max_stats->secret_objects[i].taken\n                && s->max_stats->secret_objects[i].assigned_object_id\n                    == NO_OBJECT) {\n                s->has_floordata_secrets = true;\n            }\n        }\n        s->has_visible_rows = M_HasVisibleRows(s);\n        break;\n\n    case UI_STATS_DIALOG_MODE_FINAL:\n        const GF_LEVEL_TYPE level_type =\n            GF_GetLevel(GFLT_MAIN, s->args.level_num)->type;\n        const bool include_bonus_levels = level_type == GFL_BONUS;\n        s->final_stats = Stats_ComputeFinalStats(include_bonus_levels);\n        s->stats = &s->final_stats.stats;\n        s->max_stats = &s->final_stats.max_stats;\n        M_AdjustMaxKills(s, M_HasHurtAlliesEver(include_bonus_levels));\n        s->has_visible_rows = M_HasVisibleRows(s);\n        break;\n\n    case UI_STATS_DIALOG_MODE_ASSAULT_COURSE:\n        for (int32_t track_type = 0; track_type < GYM_TRACK_NUMBER_OF;\n             track_type++) {\n            s->assault_stats[track_type] =\n                Gym_TrackManager_GetStats(track_type);\n        }\n        s->scrollable.max_items = M_GetAssaultCourseRowCount(s);\n        s->has_visible_rows = true;\n        break;\n    }\n\n    return s;\n}\n\nvoid UI_StatsDialog_Free(UI_STATS_DIALOG_STATE *const s)\n{\n    Memory_Free(s);\n}\n\nbool UI_StatsDialog_HasVisibleRows(const UI_STATS_DIALOG_STATE *const s)\n{\n    return s->has_visible_rows;\n}\n\nint32_t UI_StatsDialog_Control(UI_STATS_DIALOG_STATE *const s)\n{\n    return UI_ScrollableStack_Control(&s->scrollable, UI_STACK_VERTICAL);\n}\n\nvoid UI_StatsDialog(UI_STATS_DIALOG_STATE *const s)\n{\n    UI_BeginModal(0.5f, s->look->window_y);\n    UI_BeginPad(s->look->window_margin, s->look->window_margin);\n\n    M_BeginDialog(s);\n    switch (s->args.mode) {\n    case UI_STATS_DIALOG_MODE_LEVEL:\n        M_LevelStatsRows(s);\n        break;\n\n    case UI_STATS_DIALOG_MODE_FINAL:\n        M_FinalStatsRows(s);\n        break;\n\n    case UI_STATS_DIALOG_MODE_ASSAULT_COURSE:\n        // Ensure minimum size even if there are no items\n        UI_Spacer(290.0f, 0.0f);\n        UI_BeginScrollableStack(\n            &s->scrollable,\n            (UI_SCROLLABLE_STACK_SETTINGS) {\n                .orientation = UI_STACK_VERTICAL,\n                .spacing = 3.0f,\n            });\n        M_AssaultCourseStatsRows(s);\n        UI_EndScrollableStack();\n        break;\n    }\n\n    M_EndDialog(s);\n\n    UI_EndPad();\n    UI_EndModal();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/stats.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef enum {\n    UI_STATS_DIALOG_MODE_LEVEL,\n    UI_STATS_DIALOG_MODE_FINAL,\n    UI_STATS_DIALOG_MODE_ASSAULT_COURSE,\n} UI_STATS_DIALOG_MODE;\n\ntypedef enum {\n    UI_STATS_DIALOG_STYLE_BARE,\n    UI_STATS_DIALOG_STYLE_BORDERED,\n} UI_STATS_DIALOG_STYLE;\n\ntypedef struct {\n    UI_STATS_DIALOG_MODE mode;\n    UI_STATS_DIALOG_STYLE style;\n    int32_t level_num;\n} UI_STATS_DIALOG_ARGS;\n\ntypedef struct UI_STATS_DIALOG_STATE UI_STATS_DIALOG_STATE;\n\nUI_STATS_DIALOG_STATE *UI_StatsDialog_Init(UI_STATS_DIALOG_ARGS args);\nvoid UI_StatsDialog_Free(UI_STATS_DIALOG_STATE *s);\nbool UI_StatsDialog_HasVisibleRows(const UI_STATS_DIALOG_STATE *s);\nint32_t UI_StatsDialog_Control(UI_STATS_DIALOG_STATE *s);\nvoid UI_StatsDialog(UI_STATS_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/switch_mod.c",
    "content": "#include <trx/game/ui/dialogs/switch_mod.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/vector.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/shell/mod.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs/base_passport.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/requester.h>\n\ntypedef struct {\n    const char *name;\n    const char *title;\n} M_ROW;\n\nstruct UI_SWITCH_MOD_DIALOG_STATE {\n    VECTOR *rows;\n    UI_REQUESTER_STATE req;\n};\n\nUI_SWITCH_MOD_DIALOG_STATE *UI_SwitchModDialog_Init(void)\n{\n    UI_SWITCH_MOD_DIALOG_STATE *const s =\n        Memory_Alloc(sizeof(UI_SWITCH_MOD_DIALOG_STATE));\n    s->rows = Vector_Create(sizeof(M_ROW));\n\n    int32_t current_row = 0;\n    for (int32_t i = 0; i < Shell_GetModCount(); i++) {\n        const SHELL_MOD *const mod = Shell_GetMod(i);\n        if (!Shell_CanSwitchToMod(mod)) {\n            continue;\n        }\n        if (Shell_IsCurrentMod(mod->name)) {\n            current_row = s->rows->count;\n        }\n        Vector_Add(\n            s->rows, &(M_ROW) { .name = mod->name, .title = mod->title });\n    }\n\n    UI_BasePassportDialog_Init(&s->req, s->rows->count);\n    UI_Requester_SelectRow(&s->req, current_row);\n    return s;\n}\n\nvoid UI_SwitchModDialog_Free(UI_SWITCH_MOD_DIALOG_STATE *const s)\n{\n    Vector_Free(s->rows);\n    UI_Requester_Free(&s->req);\n    Memory_Free(s);\n}\n\nint32_t UI_SwitchModDialog_Control(UI_SWITCH_MOD_DIALOG_STATE *const s)\n{\n    UI_BasePassportDialog_Control(&s->req);\n    return UI_Requester_Control(&s->req);\n}\n\nconst char *UI_SwitchModDialog_GetSelectedMod(\n    const UI_SWITCH_MOD_DIALOG_STATE *const s, const int32_t choice)\n{\n    if (choice < 0 || choice >= s->rows->count) {\n        return nullptr;\n    }\n    const M_ROW *const row = Vector_Get(s->rows, choice);\n    return row->name;\n}\n\nvoid UI_SwitchModDialog(UI_SWITCH_MOD_DIALOG_STATE *const s)\n{\n    UI_BeginBasePassportDialog();\n    UI_BeginRequester(&s->req, GS(\"general/passport/select_mod\"));\n\n    for (int32_t i = 0; i < s->rows->count; i++) {\n        if (!UI_Requester_IsRowVisible(&s->req, i)) {\n            continue;\n        }\n\n        const M_ROW *const row = Vector_Get(s->rows, i);\n        UI_BeginRequesterRow(&s->req, i);\n        UI_BeginAnchor(0.5f, 0.5f);\n        const char *const gs_key =\n            String_FormatStatic(\"dynamic/mods/%s/title\", row->name);\n        const char *const gs_title = GS(gs_key);\n        const char *const display = gs_title != nullptr ? gs_title\n            : row->title != nullptr                     ? row->title\n                                                        : row->name;\n        UI_Label(display);\n        UI_EndAnchor();\n        UI_EndRequesterRow(&s->req, i);\n    }\n\n    UI_EndRequester(&s->req);\n    UI_EndBasePassportDialog();\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/switch_mod.h",
    "content": "#pragma once\n\n// A mod selector dialog used by the passport.\n\n#include <trx/game/ui/common.h>\n\ntypedef struct UI_SWITCH_MOD_DIALOG_STATE UI_SWITCH_MOD_DIALOG_STATE;\n\n// state functions\nUI_SWITCH_MOD_DIALOG_STATE *UI_SwitchModDialog_Init(void);\nvoid UI_SwitchModDialog_Free(UI_SWITCH_MOD_DIALOG_STATE *s);\nint32_t UI_SwitchModDialog_Control(UI_SWITCH_MOD_DIALOG_STATE *s);\nconst char *UI_SwitchModDialog_GetSelectedMod(\n    const UI_SWITCH_MOD_DIALOG_STATE *s, int32_t choice);\n\n// draw functions\nvoid UI_SwitchModDialog(UI_SWITCH_MOD_DIALOG_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/text.c",
    "content": "#include <trx/game/ui/dialogs/text.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/sound.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/frame.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/resize.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n\n#include <stdio.h>\n#include <string.h>\n\n#define M_TITLE_MARGIN 5.0f\n#define M_DIALOG_PADDING 8.0f\n\ntypedef struct UI_TEXT_DIALOG_STATE {\n    char *title; // uppercase working title string\n    char *last_raw_title; // last raw title (duplicate for strcmp)\n    char *last_raw_text; // last raw text (duplicate for strcmp)\n\n    size_t wrap_width;\n    size_t wrap_max_lines;\n    bool is_heavy;\n\n    size_t max_vis_lines; // maximum visible lines in pagination\n    VECTOR *page_content; // paginated wrapped pages\n    int32_t current_page;\n} UI_TEXT_DIALOG_STATE;\n\nstatic void M_UpdateTitle(\n    UI_TEXT_DIALOG_STATE *const s, const char *const title_raw)\n{\n    // Title update on change (strcmp to handle static ring buffers)\n    if (title_raw != nullptr\n        && (s->last_raw_title == nullptr\n            || strcmp(s->last_raw_title, title_raw) != 0)) {\n        Memory_FreePointer(&s->title);\n        s->title = String_ToUpper(title_raw);\n        Memory_FreePointer(&s->last_raw_title);\n        s->last_raw_title = Memory_DupStr(title_raw);\n    }\n}\n\nstatic void M_UpdateText(\n    UI_TEXT_DIALOG_STATE *const s, const char *const text_raw)\n{\n    // Text update on change (strcmp to in case the pointers does not change)\n    if (!s->last_raw_text || strcmp(s->last_raw_text, text_raw) != 0) {\n        if (s->page_content != nullptr) {\n            for (int32_t i = 0; i < s->page_content->count; i++) {\n                char *page = *(char **)Vector_Get(s->page_content, i);\n                Memory_Free(page);\n            }\n            Vector_Free(s->page_content);\n        }\n\n        const char *wrapped = UI_Text_WordWrap(text_raw, 1.0f, s->wrap_width);\n        s->page_content = String_Paginate(wrapped, s->wrap_max_lines);\n        Memory_FreePointer(&wrapped);\n\n        s->max_vis_lines = 0;\n        for (int32_t i = 0; i < s->page_content->count; ++i) {\n            size_t page_lines = 1;\n            const char *c = *(char **)Vector_Get(s->page_content, i);\n            while (*c != '\\0') {\n                page_lines += *c++ == '\\n';\n            }\n            CLAMPL(s->max_vis_lines, page_lines);\n        }\n        Memory_FreePointer(&s->last_raw_text);\n        s->last_raw_text = Memory_DupStr(text_raw);\n        s->current_page = 0;\n    }\n}\nstatic bool M_SelectPage(UI_TEXT_DIALOG_STATE *const s, const int32_t new_page)\n{\n    if (s->page_content == nullptr) {\n        return false;\n    }\n    if (new_page == s->current_page || new_page < 0\n        || new_page >= s->page_content->count) {\n        return false;\n    }\n    s->current_page = new_page;\n    return true;\n}\n\nUI_TEXT_DIALOG_STATE *UI_TextDialog_Init(\n    size_t wrap_width, size_t wrap_max_lines, bool is_heavy)\n{\n    UI_TEXT_DIALOG_STATE *s = Memory_Alloc(sizeof(*s));\n    s->wrap_width = wrap_width;\n    s->wrap_max_lines = wrap_max_lines;\n    s->is_heavy = is_heavy;\n    return s;\n}\n\nvoid UI_TextDialog_Free(UI_TEXT_DIALOG_STATE *const s)\n{\n    ASSERT(s != nullptr);\n    Memory_FreePointer(&s->title);\n    Memory_FreePointer(&s->last_raw_title);\n    Memory_FreePointer(&s->last_raw_text);\n    if (s->page_content != nullptr) {\n        for (int32_t i = s->page_content->count - 1; i >= 0; --i) {\n            Memory_Free(*(char **)Vector_Get(s->page_content, i));\n        }\n        Vector_Free(s->page_content);\n        s->page_content = nullptr;\n    }\n    Memory_Free(s);\n}\n\nvoid UI_TextDialog_Control(UI_TEXT_DIALOG_STATE *const s)\n{\n    ASSERT(s != nullptr);\n    const int32_t page_shift =\n        g_InputDB.menu_left ? -1 : (g_InputDB.menu_right ? 1 : 0);\n    if (M_SelectPage(s, s->current_page + page_shift)) {\n        Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS);\n    }\n}\n\nvoid UI_TextDialogEx(\n    UI_TEXT_DIALOG_STATE *const s, const UI_TEXT_DIALOG_SETTINGS settings)\n{\n    ASSERT(s != nullptr);\n\n    const char *const title_raw = settings.title_raw;\n    const char *const text_raw = settings.text_raw;\n\n    if (text_raw == nullptr || String_IsEmpty(text_raw)) {\n        return;\n    }\n\n    M_UpdateTitle(s, title_raw);\n    M_UpdateText(s, text_raw);\n\n    UI_BeginModal(0.5f, 0.5f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .spacing = { .v = 5.0f },\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n    });\n\n    UI_BeginFrame(\n        s->is_heavy ? UI_FRAME_DIALOG_BACKGROUND_HEAVY\n                    : UI_FRAME_DIALOG_BACKGROUND);\n    UI_BeginPad(M_DIALOG_PADDING, M_DIALOG_PADDING);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        { .h = UI_STACK_H_ALIGN_SPAN },\n    });\n\n    UI_BeginAnchor(0.5f, 0.5f);\n    UI_Label(s->title != nullptr ? s->title : \"\");\n    UI_EndAnchor();\n    UI_Spacer(M_TITLE_MARGIN, M_TITLE_MARGIN);\n\n    for (int32_t i = 0; i < s->page_content->count; ++i) {\n        if (i != s->current_page) {\n            UI_BeginResize(-1.0f, 0.0f);\n        } else if (s->page_content->count == 1) {\n            UI_BeginResize(-1.0f, -1.0f);\n        } else {\n            UI_BeginResize(-1.0f, UI_TEXT_HEIGHT * s->max_vis_lines);\n        }\n        UI_Label(*(char **)Vector_Get(s->page_content, i));\n        UI_EndResize();\n    }\n\n    if (s->page_content->count > 1) {\n        UI_Spacer(M_TITLE_MARGIN, M_TITLE_MARGIN * 3);\n\n        UI_BeginAnchor(1.0f, 0.5f);\n        UI_BeginStack(UI_STACK_HORIZONTAL);\n        if (s->current_page > 0) {\n            UI_Label(\"\\\\{button left} \");\n        }\n        char page_indicator[100];\n        sprintf(\n            page_indicator, *GS_PTR(\"general/misc/pagination_nav\"),\n            s->current_page + 1, s->page_content->count);\n        UI_Label(page_indicator);\n        if (s->current_page < s->page_content->count - 1) {\n            UI_Label(\" \\\\{button right}\");\n        }\n        UI_EndStack();\n        UI_EndAnchor();\n    }\n\n    UI_EndStack();\n    UI_EndPad();\n    UI_EndFrame();\n\n    if (settings.footer_func != nullptr) {\n        settings.footer_func(settings.footer_user_data);\n    }\n    UI_EndStack();\n\n    UI_EndModal();\n}\n\nvoid UI_TextDialog(\n    UI_TEXT_DIALOG_STATE *const s, const char *const title_raw,\n    const char *const text_raw)\n{\n    UI_TextDialogEx(\n        s,\n        (UI_TEXT_DIALOG_SETTINGS) {\n            .title_raw = title_raw,\n            .text_raw = text_raw,\n        });\n}\n"
  },
  {
    "path": "src/trx/game/ui/dialogs/text.h",
    "content": "#pragma once\n\n#include <trx/core/vector.h>\n#include <trx/game/ui/common.h>\n\n// A widget to cycle through several pages of a text content.\n\ntypedef struct UI_TEXT_DIALOG_STATE UI_TEXT_DIALOG_STATE;\ntypedef void (*UI_TEXT_DIALOG_FOOTER_FUNC)(void *user_data);\n\ntypedef struct {\n    const char *title_raw;\n    const char *text_raw;\n    UI_TEXT_DIALOG_FOOTER_FUNC footer_func;\n    void *footer_user_data;\n} UI_TEXT_DIALOG_SETTINGS;\n\n// state functions\nUI_TEXT_DIALOG_STATE *UI_TextDialog_Init(\n    size_t wrap_width, size_t wrap_max_lines, bool is_heavy);\n\n// Handle page-left/right input.  Call before UI_TextDialog().\nvoid UI_TextDialog_Control(UI_TEXT_DIALOG_STATE *state);\n\n// Free any allocated buffers.  Call when dialog is dismissed.\nvoid UI_TextDialog_Free(UI_TEXT_DIALOG_STATE *state);\n\n// draw functions\n\n// Draw and manage a text dialog in one call.  Rewraps/recapitalizes only\n// when title_raw/text_raw differ (compared via strcmp).  Call every frame.\nvoid UI_TextDialog(\n    UI_TEXT_DIALOG_STATE *state, const char *title_raw, const char *text_raw);\n\n// Same as UI_TextDialog(), with optional settings.\nvoid UI_TextDialogEx(\n    UI_TEXT_DIALOG_STATE *state, UI_TEXT_DIALOG_SETTINGS settings);\n"
  },
  {
    "path": "src/trx/game/ui/dialogs.h",
    "content": "#pragma once\n\n#include <trx/game/ui/dialogs/color_editor.h>\n#include <trx/game/ui/dialogs/controls.h>\n#include <trx/game/ui/dialogs/controls_backend.h>\n#include <trx/game/ui/dialogs/gameplay_settings.h>\n#include <trx/game/ui/dialogs/graphic_settings.h>\n#include <trx/game/ui/dialogs/new_game.h>\n#include <trx/game/ui/dialogs/pause.h>\n#include <trx/game/ui/dialogs/photo_mode.h>\n#include <trx/game/ui/dialogs/play_any_level.h>\n#include <trx/game/ui/dialogs/save_slot.h>\n#include <trx/game/ui/dialogs/select_level.h>\n#include <trx/game/ui/dialogs/sound_settings.h>\n#include <trx/game/ui/dialogs/stats.h>\n#include <trx/game/ui/dialogs/switch_mod.h>\n#include <trx/game/ui/dialogs/text.h>\n"
  },
  {
    "path": "src/trx/game/ui/draw.c",
    "content": "#include <trx/game/ui/draw.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/game/objects.h>\n#include <trx/game/output/common.h>\n#include <trx/game/output/sources/ui.h>\n#include <trx/game/output/state.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/ui/settings.h>\n#include <trx/version.h>\n\n#include <string.h>\n\n#define M_WHITE ((RGBA_F) { 1.0f, 1.0f, 1.0f, 1.0f })\n#define M_OUTLINE_THICKNESS 0.75f\n#define M_SCHEDULE_OP(draw_func, inst)                                         \\\n    M_ScheduleOpHelper(                                                        \\\n        (M_DRAW_OP_FUNC)draw_func, sizeof(inst), (const M_DRAW_OP *)&inst);\n\nstruct M_DRAW_OP;\ntypedef void (*M_DRAW_OP_FUNC)(const struct M_DRAW_OP *);\n\ntypedef struct M_DRAW_OP {\n    M_DRAW_OP_FUNC draw;\n} M_DRAW_OP;\n\ntypedef struct {\n    M_DRAW_OP base;\n    UI_STYLE ui_style;\n    int32_t x0, x1, y, z;\n} M_DRAW_OP_HORZ_LINE;\n\ntypedef struct {\n    M_DRAW_OP base;\n    UI_STYLE ui_style;\n    int32_t x0, y0, x1, y1, z;\n    TEXT_STYLE text_style;\n} M_DRAW_OP_TEXT_RECT;\n\ntypedef struct {\n    M_DRAW_OP base;\n    int32_t sx, sy, z, scale_h, scale_v, sprite_idx;\n    const RGBA_F colors[4];\n} M_DRAW_OP_SPRITE;\n\ntypedef struct {\n    M_DRAW_OP base;\n    int32_t x0, y0, x1, y1, z;\n    RGBA_8888 tl, tr, bl, br;\n} M_DRAW_OP_QUAD;\n\ntypedef struct {\n    MEMORY_ARENA_ALLOCATOR alloc;\n    VECTOR *ops;\n} M_PRIV;\n\nstatic M_PRIV m_Priv = {\n    .alloc = {\n        .default_chunk_size = 1024 * 4,\n    },\n};\n\nstatic void M_DrawScreenQuad(\n    const int32_t x0, const int32_t y0, const int32_t x1, const int32_t y1,\n    const int32_t z, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl,\n    const RGBA_8888 br)\n{\n    OutputSource_UI_StageQuad((OUTPUT_UI_QUAD) {\n        .x0 = x0,\n        .y0 = y0,\n        .x1 = x1,\n        .y1 = y1,\n        .tl = tl,\n        .tr = tr,\n        .bl = bl,\n        .br = br,\n        .z = Output_GetNearZ_UI() + z,\n    });\n}\n\nstatic void M_DrawScreenSprite(\n    const int32_t sx, const int32_t sy, const int32_t sz, const int32_t scale_h,\n    const int32_t scale_v, const int32_t sprite_idx, const RGBA_F colors[4])\n{\n    Output_DrawScreenSprite(sx, sy, sz, scale_h, scale_v, sprite_idx, colors);\n}\n\nstatic void M_DrawScreenGradientBox(\n    const int32_t x0, const int32_t y0, const int32_t x1, const int32_t y1,\n    const int32_t z, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl,\n    const RGBA_8888 br, const float thickness)\n{\n    const float e = UI_Scaler_Calc(thickness, UI_SCALER_TARGET_TEXT);\n    M_DrawScreenQuad(x0 - e, y0 - e, x1 + e, y0 + e, z, tl, tr, tl, tr);\n    M_DrawScreenQuad(x0 - e, y1 - e, x1 + e, y1 + e, z, bl, br, bl, br);\n    M_DrawScreenQuad(x0 - e, y0 - e, x0 + e, y1 + e, z, tl, tl, bl, bl);\n    M_DrawScreenQuad(x1 - e, y0 - e, x1 + e, y1 + e, z, tr, tr, br, br);\n}\n\nstatic void M_DrawScreenCentreGradientBox(\n    const int32_t x0, const int32_t y0, const int32_t x1, const int32_t y1,\n    const int32_t z, const RGBA_8888 edge, const RGBA_8888 center_h,\n    const RGBA_8888 center_v, const float thickness)\n{\n    const float e = UI_Scaler_Calc(thickness, UI_SCALER_TARGET_TEXT);\n    const int32_t xm = (x0 + x1) / 2;\n    const int32_t ym = (y0 + y1) / 2;\n    const RGBA_8888 ch = center_h;\n    const RGBA_8888 cv = center_v;\n    const RGBA_8888 ce = edge;\n\n    // clang-format off\n    M_DrawScreenQuad(x0 - e, y0 - e, xm,     y0 + e, z, ce, ch, ce, ch);\n    M_DrawScreenQuad(xm,     y0 - e, x1 + e, y0 + e, z, ch, ce, ch, ce);\n    M_DrawScreenQuad(x0 - e, y1 - e, xm,     y1 + e, z, ce, ch, ce, ch);\n    M_DrawScreenQuad(xm,     y1 - e, x1 + e, y1 + e, z, ch, ce, ch, ce);\n    M_DrawScreenQuad(x0 - e, y0,     x0 + e, ym,     z, ce, ce, cv, cv);\n    M_DrawScreenQuad(x0 - e, ym,     x0 + e, y1,     z, cv, cv, ce, ce);\n    M_DrawScreenQuad(x1 - e, y0,     x1 + e, ym,     z, ce, ce, cv, cv);\n    M_DrawScreenQuad(x1 - e, ym,     x1 + e, y1,     z, cv, cv, ce, ce);\n    // clang-format on\n}\n\nstatic void M_DrawOp_HorizontalLine(const M_DRAW_OP_HORZ_LINE *const op)\n{\n    if (g_TRVersion == 1 && op->ui_style == UI_STYLE_PC) {\n        const float e =\n            UI_Scaler_Calc(M_OUTLINE_THICKNESS, UI_SCALER_TARGET_TEXT);\n        const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC();\n        M_DrawScreenQuad(\n            op->x0, op->y - e, op->x1, op->y, op->z, c->outline_light,\n            c->outline_light, c->outline_light, c->outline_light);\n        M_DrawScreenQuad(\n            op->x0, op->y, op->x1, op->y + e, op->z, c->outline_dark,\n            c->outline_dark, c->outline_dark, c->outline_dark);\n    } else if (g_TRVersion == 2 && op->ui_style == UI_STYLE_PC) {\n        const int32_t mesh_idx = Object_Get(O_TEXT_BOX)->mesh_idx;\n        M_DrawScreenSprite(\n            op->x0, op->y, op->z, (op->x1 - op->x0) * PHD_ONE / 8, PHD_ONE,\n            mesh_idx + 4, (RGBA_F[4]) { M_WHITE, M_WHITE, M_WHITE, M_WHITE });\n    } else if (g_TRVersion == 3 && op->ui_style == UI_STYLE_PC) {\n        const float e1 = UI_Scaler_Calc(1.0f, UI_SCALER_TARGET_TEXT);\n        const float e2 = e1 / 3.0f;\n        const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC();\n        M_DrawScreenQuad(\n            op->x0, op->y - e1, op->x1, op->y + e1, op->z, c->outline_dark,\n            c->outline_dark, c->outline_dark, c->outline_dark);\n        M_DrawScreenQuad(\n            op->x0, op->y - e2, op->x1, op->y + e2, op->z, c->outline_light,\n            c->outline_light, c->outline_light, c->outline_light);\n    } else {\n        const float e =\n            UI_Scaler_Calc(M_OUTLINE_THICKNESS, UI_SCALER_TARGET_TEXT);\n        const UI_MENU_COLORS_PS1 *const c = UI_Settings_GetMenuColorsPS1();\n        M_DrawScreenQuad(\n            op->x0, op->y - e, op->x1, op->y + e, op->z, c->outline_bl,\n            c->outline_br, c->outline_bl, c->outline_br);\n    }\n}\n\nstatic void M_DrawOp_TextBackground(const M_DRAW_OP_TEXT_RECT *const op)\n{\n    switch (op->ui_style) {\n    case UI_STYLE_PC: {\n        const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC();\n        const RGBA_8888 *const ramp = op->text_style == TS_BACKGROUND_HEAVY\n            ? c->background_heavy\n            : c->background;\n        M_DrawScreenQuad(\n            op->x0, op->y0, op->x1, op->y1, op->z, ramp[0], ramp[0], ramp[1],\n            ramp[1]);\n        break;\n    }\n\n    case UI_STYLE_PS1: {\n        const UI_MENU_COLORS_PS1 *const c = UI_Settings_GetMenuColorsPS1();\n        const int32_t xm = (op->x0 + op->x1) / 2;\n        const int32_t ym = (op->y0 + op->y1) / 2;\n        RGBA_8888 edge, center;\n        switch (op->text_style) {\n        case TS_BACKGROUND_HEAVY:\n            edge = c->background_heavy_edge;\n            center = c->background_heavy_center;\n            break;\n        case TS_HEADING:\n            edge = c->heading_edge;\n            center = c->heading_center;\n            break;\n        case TS_REQUESTED:\n            edge = c->requested_edge;\n            center = c->requested_center;\n            break;\n        default:\n            edge = c->background_edge;\n            center = c->background_center;\n            break;\n        }\n\n        // clang-format off\n#define L_DRAW(x0, y0, x1, y1, tl, tr, bl, br) \\\n    M_DrawScreenQuad(x0, y0, x1, y1, op->z, tl, tr, bl, br);\n        L_DRAW(xm,     op->y0, op->x0, ym,     edge,   edge,   center, edge  );\n        L_DRAW(op->x1, op->y0, xm,     ym,     edge,   edge,   edge,   center);\n        L_DRAW(xm,     ym,     op->x0, op->y1, center, edge,   edge,   edge  );\n        L_DRAW(op->x1, ym,     xm,     op->y1, edge,   center, edge,   edge  );\n#undef L_DRAW\n        // clang-format on\n        break;\n    }\n    }\n}\n\nstatic void M_DrawOp_TextOutline(const M_DRAW_OP_TEXT_RECT *const op)\n{\n    int32_t x0 = op->x0;\n    int32_t x1 = op->x1;\n    int32_t y0 = op->y0;\n    int32_t y1 = op->y1;\n\n    switch (op->ui_style) {\n    case UI_STYLE_PC:\n        if (g_TRVersion == 1) {\n            const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC();\n            const float thickness =\n                UI_Scaler_Calc(M_OUTLINE_THICKNESS, UI_SCALER_TARGET_TEXT);\n            Output_DrawScreenFrame(\n                x0, y0, x1 - x0, y1 - y0, c->outline_dark, c->outline_light,\n                thickness);\n        } else if (g_TRVersion == 2) {\n            const int32_t mesh_idx = Object_Get(O_TEXT_BOX)->mesh_idx;\n\n            const int32_t offset = 4;\n            x0 += offset;\n            y0 += offset;\n            x1 -= offset;\n            y1 -= offset;\n            const int32_t scale_h = PHD_ONE;\n            const int32_t scale_v = PHD_ONE;\n            const int32_t w = (x1 - x0) * PHD_ONE / 8;\n            const int32_t h = (y1 - y0) * PHD_ONE / 8;\n\n            const RGBA_F neutral[4] = { M_WHITE, M_WHITE, M_WHITE, M_WHITE };\n\n            // Corners\n            M_DrawScreenSprite(\n                x0, y0, op->z, scale_h, scale_v, mesh_idx + 0, neutral);\n            M_DrawScreenSprite(\n                x1, y0, op->z, scale_h, scale_v, mesh_idx + 1, neutral);\n            M_DrawScreenSprite(\n                x1, y1, op->z, scale_h, scale_v, mesh_idx + 2, neutral);\n            M_DrawScreenSprite(\n                x0, y1, op->z, scale_h, scale_v, mesh_idx + 3, neutral);\n\n            // Lines\n            M_DrawScreenSprite(\n                x0, y0, op->z, w, scale_v, mesh_idx + 4, neutral);\n            M_DrawScreenSprite(\n                x1, y0, op->z, scale_h, h, mesh_idx + 5, neutral);\n            M_DrawScreenSprite(\n                x0, y1, op->z, w, scale_v, mesh_idx + 6, neutral);\n            M_DrawScreenSprite(\n                x0, y0, op->z, scale_h, h, mesh_idx + 7, neutral);\n        } else if (g_TRVersion == 3) {\n            const UI_MENU_COLORS_PC *const c = UI_Settings_GetMenuColorsPC();\n            const float thickness = UI_Scaler_Calc(1.0f, UI_SCALER_TARGET_TEXT);\n            Output_DrawScreenFrame(\n                x0, y0, x1 - x0, y1 - y0, c->outline_dark, c->outline_dark,\n                thickness);\n            Output_DrawScreenFrame(\n                x0, y0, x1 - x0, y1 - y0, c->outline_light, c->outline_light,\n                thickness / 3.0f);\n        }\n\n        break;\n\n    case UI_STYLE_PS1: {\n        const UI_MENU_COLORS_PS1 *const c = UI_Settings_GetMenuColorsPS1();\n        switch (op->text_style) {\n        case TS_BACKGROUND:\n        case TS_BACKGROUND_HEAVY:\n            M_DrawScreenGradientBox(\n                x0, y0, x1, y1, op->z, c->outline_tl, c->outline_tr,\n                c->outline_bl, c->outline_br, M_OUTLINE_THICKNESS);\n            break;\n        case TS_HEADING:\n            M_DrawScreenGradientBox(\n                x0, y0, x1, y1, op->z, c->heading_outline, c->heading_outline,\n                c->heading_outline, c->heading_outline, M_OUTLINE_THICKNESS);\n            break;\n        case TS_REQUESTED:\n            M_DrawScreenCentreGradientBox(\n                x0, y0, x1, y1, op->z, c->requested_outline_edge,\n                c->requested_outline_ch, c->requested_outline_cv,\n                M_OUTLINE_THICKNESS);\n            break;\n        }\n        break;\n    }\n    }\n}\n\nstatic void M_DrawOp_Sprite(const M_DRAW_OP_SPRITE *const op)\n{\n    Output_DrawScreenSprite(\n        op->sx, op->sy, op->z, op->scale_h, op->scale_v, op->sprite_idx,\n        op->colors);\n}\n\nstatic void M_DrawOp_Quad(const M_DRAW_OP_QUAD *const op)\n{\n    M_DrawScreenQuad(\n        op->x0, op->y0, op->x1, op->y1, op->z, op->tl, op->tr, op->bl, op->br);\n}\n\n// Allocate a new deferred draw operation in the arena.\nstatic inline void *M_ArenaAlloc(const size_t sz)\n{\n    void *p = Memory_ArenaAlloc(&m_Priv.alloc, sz);\n    memset(p, 0, sz);\n    return p;\n}\n\nstatic inline void M_ScheduleOp(M_DRAW_OP *const op)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Add(p->ops, &op);\n}\n\nstatic void M_ScheduleOpHelper(\n    const M_DRAW_OP_FUNC draw_func, const size_t size,\n    const M_DRAW_OP *const op_src)\n{\n    M_DRAW_OP *const op = M_ArenaAlloc(size);\n    memcpy(op, op_src, size);\n    op->draw = draw_func;\n    M_ScheduleOp(op);\n}\n\nvoid UI_ScheduleDrawTextBackground(\n    const UI_STYLE ui_style, const int32_t sx, const int32_t sy,\n    const int32_t z, const int32_t w, const int32_t h,\n    const TEXT_STYLE text_style)\n{\n    M_SCHEDULE_OP(\n        M_DrawOp_TextBackground,\n        ((M_DRAW_OP_TEXT_RECT) {\n            .ui_style = ui_style,\n            .x0 = sx,\n            .y0 = sy,\n            .x1 = sx + w,\n            .y1 = sy + h,\n            .z = z,\n            .text_style = text_style,\n        }));\n}\n\nvoid UI_ScheduleDrawTextOutline(\n    const UI_STYLE ui_style, const int32_t sx, const int32_t sy,\n    const int32_t z, const int32_t w, const int32_t h,\n    const TEXT_STYLE text_style)\n{\n    M_SCHEDULE_OP(\n        M_DrawOp_TextOutline,\n        ((M_DRAW_OP_TEXT_RECT) {\n            .ui_style = ui_style,\n            .x0 = sx,\n            .y0 = sy,\n            .x1 = sx + w,\n            .y1 = sy + h,\n            .z = z,\n            .text_style = text_style,\n        }));\n}\n\nvoid UI_ScheduleDrawScreenSprite(\n    const int32_t sx, const int32_t sy, const int32_t z, const int32_t scale_h,\n    const int32_t scale_v, const int32_t sprite_idx, const RGBA_F colors_[4])\n{\n    M_SCHEDULE_OP(\n        M_DrawOp_Sprite,\n        ((M_DRAW_OP_SPRITE) {\n            .sx = sx,\n            .sy = sy,\n            .z = z,\n            .scale_h = scale_h,\n            .scale_v = scale_v,\n            .sprite_idx = sprite_idx,\n            .colors = {\n                [0] = colors_[0],\n                [1] = colors_[1],\n                [2] = colors_[2],\n                [3] = colors_[3],\n            },\n        }));\n}\n\nvoid UI_ScheduleDrawScreenFlatQuad(\n    const int32_t sx, const int32_t sy, const int32_t z, const int32_t w,\n    const int32_t h, const RGBA_8888 color)\n{\n    M_SCHEDULE_OP(\n        M_DrawOp_Quad,\n        ((M_DRAW_OP_QUAD) {\n            .x0 = sx,\n            .y0 = sy,\n            .x1 = sx + w,\n            .y1 = sy + h,\n            .z = z,\n            .tl = color,\n            .tr = color,\n            .bl = color,\n            .br = color,\n        }));\n}\n\nvoid UI_ScheduleDrawScreenGradientQuad(\n    const int32_t sx, const int32_t sy, const int32_t z, const int32_t w,\n    const int32_t h, const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl,\n    const RGBA_8888 br)\n{\n    M_SCHEDULE_OP(\n        M_DrawOp_Quad,\n        ((M_DRAW_OP_QUAD) {\n            .x0 = sx,\n            .y0 = sy,\n            .x1 = sx + w,\n            .y1 = sy + h,\n            .z = z,\n            .tl = tl,\n            .tr = tr,\n            .bl = bl,\n            .br = br,\n        }));\n}\n\nvoid UI_ScheduleDrawHorizontalLine(\n    const UI_STYLE ui_style, const int32_t x0, const int32_t x1,\n    const int32_t y, const int32_t z)\n{\n    M_SCHEDULE_OP(\n        M_DrawOp_HorizontalLine,\n        ((M_DRAW_OP_HORZ_LINE) {\n            .ui_style = ui_style,\n            .x0 = x0,\n            .x1 = x1,\n            .y = y,\n            .z = z,\n        }));\n}\n\nvoid UI_InitDraw(void)\n{\n    M_PRIV *const p = &m_Priv;\n    if (p->ops == nullptr) {\n        p->ops = Vector_Create(sizeof(M_DRAW_OP *));\n    }\n}\n\nvoid UI_ShutdownDraw(void)\n{\n    M_PRIV *const p = &m_Priv;\n    Memory_ArenaFree(&p->alloc);\n    if (p->ops != nullptr) {\n        Vector_Free(p->ops);\n        p->ops = nullptr;\n    }\n}\n\nvoid UI_ClearDraw(void)\n{\n    M_PRIV *const p = &m_Priv;\n    Vector_Clear(p->ops);\n    Memory_ArenaReset(&p->alloc);\n}\n\nvoid UI_Draw(void)\n{\n    M_PRIV *const p = &m_Priv;\n    for (int32_t i = 0; i < m_Priv.ops->count; i++) {\n        const M_DRAW_OP *const op = *(M_DRAW_OP **)Vector_Get(m_Priv.ops, i);\n        op->draw(op);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/draw.h",
    "content": "#pragma once\n\n#include <trx/game/output/draw.h>\n\n// Schedule deferred UI draw operations to be executed by UI_Draw().\n// These record drawing commands during UI_EndScene instead of issuing them\n// immediately.\nvoid UI_ScheduleDrawTextBackground(\n    UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h,\n    TEXT_STYLE text_style);\nvoid UI_ScheduleDrawTextOutline(\n    UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h,\n    TEXT_STYLE text_style);\nvoid UI_ScheduleDrawScreenSprite(\n    int32_t sx, int32_t sy, int32_t z, int32_t scale_h, int32_t scale_v,\n    int32_t sprite_idx, const RGBA_F colors[4]);\nvoid UI_ScheduleDrawScreenFlatQuad(\n    int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 color);\nvoid UI_ScheduleDrawScreenGradientQuad(\n    int32_t sx, int32_t sy, int32_t z, int32_t w, int32_t h, RGBA_8888 tl,\n    RGBA_8888 tr, RGBA_8888 bl, RGBA_8888 br);\nvoid UI_ScheduleDrawHorizontalLine(\n    UI_STYLE ui_style, int32_t x0, int32_t x1, int32_t y, int32_t z);\n\nvoid UI_InitDraw(void);\nvoid UI_ShutdownDraw(void);\n\n// Execute all scheduled UI draw operations in order.\nvoid UI_Draw(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/ammo_label.c",
    "content": "#include <trx/game/ui/elements/ammo_label.h>\n\n#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/gun.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/vehicle.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/version.h>\n\n#include <stdio.h>\n\nbool UI_AmmoLabel(void)\n{\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n\n    int32_t ammo = 0;\n    const bool use_icon = g_TRVersion == 1;\n    const char *icon_str = nullptr;\n\n    const ITEM *const vehicle_item = Lara_Vehicle_GetItem();\n    if (vehicle_item != nullptr && vehicle_item->object_id == O_UPV) {\n        ammo = lara->harpoon_ammo.ammo;\n    } else {\n        if (lara->gun_status != LGS_READY || Game_IsBonusFlagSet(GBF_NGPLUS)) {\n            return false;\n        }\n\n        switch (lara->gun_type) {\n        case LGT_PISTOLS:\n            return false;\n        case LGT_SHOTGUN:\n            ammo = lara->shotgun_ammo.ammo / Gun_GetAmmoClipCount(LGT_SHOTGUN);\n            if (use_icon) {\n                icon_str = \"\\\\{ammo shotgun}\";\n            }\n            break;\n        case LGT_UZIS:\n            ammo = lara->uzi_ammo.ammo;\n            if (use_icon) {\n                icon_str = \"\\\\{ammo uzis}\";\n            }\n            break;\n        case LGT_MAGNUMS:\n            ammo = lara->magnum_ammo.ammo;\n            if (use_icon) {\n                icon_str = \"\\\\{ammo magnums}\";\n            }\n            break;\n        case LGT_AUTOS:\n            ammo = lara->autos_ammo.ammo;\n            break;\n        case LGT_DESERT_EAGLE:\n            ammo = lara->desert_eagle_ammo.ammo;\n            break;\n        case LGT_M16:\n            ammo = lara->m16_ammo.ammo;\n            break;\n        case LGT_MP5:\n            ammo = lara->mp5_ammo.ammo;\n            break;\n        case LGT_GRENADE:\n            ammo = lara->grenade_ammo.ammo;\n            break;\n        case LGT_ROCKET:\n            ammo = lara->rocket_ammo.ammo;\n            break;\n        case LGT_HARPOON:\n            ammo = lara->harpoon_ammo.ammo;\n            break;\n        default:\n            return false;\n        }\n    }\n\n    const char *inner_text = nullptr;\n    if (icon_str != nullptr) {\n        inner_text = String_FormatStatic(\"%6d %s\", ammo, icon_str);\n    } else {\n        inner_text = String_FormatStatic(\"%6d\", ammo);\n    }\n    const char *const outer_text = String_FormatStatic(\n        g_Config.ui.menu_style == UI_STYLE_PS1\n            ? GS(\"general/overlay/item_count_fmt_ps1\")\n            : GS(\"general/overlay/item_count_fmt_pc\"),\n        inner_text);\n\n    UI_LabelEx(outer_text, (UI_LABEL_SETTINGS) { .scale = 1.5f });\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/ammo_label.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\nbool UI_AmmoLabel(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/anchor.c",
    "content": "#include <trx/game/ui/elements/anchor.h>\n\n#include <trx/game/ui/helpers.h>\n#include <trx/game/ui/text.h>\n\ntypedef struct {\n    float x;\n    float y;\n} M_DATA;\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    node->measure_w = UI_GetCanvasWidth();\n    node->measure_h = UI_GetCanvasHeight() - UI_TEXT_HEIGHT;\n}\n\nstatic void M_Layout(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n    const M_DATA *const data = node->data;\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        const float cw = child->measure_w;\n        const float ch = child->measure_h;\n        const float cx = x + (w - cw) * data->x;\n        const float cy = y + (h - ch) * data->y;\n        child->ops.layout(child, cx, cy, cw, ch);\n        child = child->next_sibling;\n    }\n}\n\nvoid UI_BeginAnchor(const float x, const float y)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = M_Layout,\n            .draw = UI_DrawWrapper,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    data->x = x;\n    data->y = y;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndAnchor(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/anchor.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// Used to align a top-level widget to the screen center or to the screen edges.\n// Uses ratio inputs.\n\nvoid UI_BeginAnchor(const float x, const float y);\nvoid UI_EndAnchor(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar.c",
    "content": "#include <trx/game/ui/elements/bar.h>\n\n#include <trx/config.h>\n#include <trx/core/json.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/output.h>\n#include <trx/game/shell.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/version.h>\n\n#include <math.h>\n\ntypedef struct {\n    int32_t x, y, w, h;\n} M_RECT_32;\n\ntypedef struct {\n    UI_BAR_SETTINGS settings;\n    const UI_BAR_THEME *theme;\n    float scale;\n} M_DATA;\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    M_DATA *const data = node->data;\n    const float scale = UI_Scaler_GetScale(\n                            data->settings.preview ? UI_SCALER_TARGET_TEXT\n                                                   : UI_SCALER_TARGET_BAR)\n        * data->scale;\n    node->measure_w = data->settings.w * scale;\n    node->measure_h = data->settings.h * scale;\n}\n\nstatic void M_DrawBackground(\n    const UI_BAR_THEME *const theme, const M_RECT_32 rect)\n{\n    UI_ScheduleDrawScreenFlatQuad(\n        rect.x, rect.y, 0, rect.w, rect.h, (RGBA_8888) { 0, 0, 0, 255 });\n}\n\nstatic void M_DrawBorderPC(\n    const UI_BAR_THEME *const theme, const M_RECT_32 rect, const float border)\n{\n    UI_ScheduleDrawScreenFlatQuad(\n        rect.x, rect.y, 0, rect.w, rect.h, theme->border_light);\n    UI_ScheduleDrawScreenFlatQuad(\n        rect.x + border, rect.y + border, 0, rect.w - border, rect.h - border,\n        theme->border_dark);\n}\n\nstatic void M_DrawBorderPS1(\n    const UI_BAR_THEME *const theme, const M_RECT_32 rect, const float border)\n{\n#if 0\n    Output_DrawScreenGradientQuad(\n        rect.x - border, rect.y + border, 0, rect.w + border * 2, rect.h - border * 2, theme->border_bl, theme->border_br, theme->border_br, theme->border_bl);\n#endif\n    Output_DrawScreenGradientQuad(\n        rect.x, rect.y, 0, rect.w, rect.h, theme->border_tl, theme->border_tr,\n        theme->border_br, theme->border_bl);\n}\n\nstatic void M_DrawFillPC(\n    const UI_BAR_THEME *const theme, const UI_BAR_SETTINGS *const settings,\n    const M_RECT_32 rect, const float percent)\n{\n    if (g_Config.ui.enable_smooth_bars) {\n        for (int32_t i = 0; i < UI_BAR_COLOR_STEPS - 1; i++) {\n            const RGBA_8888 c1 = theme->ramp[i];\n            const RGBA_8888 c2 = theme->ramp[i + 1];\n            const int32_t lsy = rect.y + i * rect.h / (UI_BAR_COLOR_STEPS - 1);\n            const int32_t lsh =\n                rect.y + (i + 1) * rect.h / (UI_BAR_COLOR_STEPS - 1) - lsy;\n            UI_ScheduleDrawScreenGradientQuad(\n                rect.x, lsy, 0, rect.w, lsh, c1, c1, c2, c2);\n        }\n    } else {\n        for (int32_t i = 0; i < UI_BAR_COLOR_STEPS; i++) {\n            const RGBA_8888 c = theme->ramp[i];\n            const int32_t lsy = rect.y + i * rect.h / UI_BAR_COLOR_STEPS;\n            const int32_t lsh =\n                rect.y + (i + 1) * rect.h / UI_BAR_COLOR_STEPS - lsy;\n            UI_ScheduleDrawScreenFlatQuad(rect.x, lsy, 0, rect.w, lsh, c);\n        }\n    }\n}\n\nstatic void M_DrawFillPS1(\n    const UI_BAR_THEME *const theme, const UI_BAR_SETTINGS *const settings,\n    const M_RECT_32 rect, const float percent)\n{\n    const UI_BAR_TYPE type = settings->type;\n    if (g_Config.ui.enable_smooth_bars) {\n        for (int32_t i = 0; i < UI_BAR_COLOR_STEPS - 1; i++) {\n            const RGBA_8888 ctl = theme->ramp_left[i];\n            const RGBA_8888 ctr = theme->ramp_right[i];\n            const RGBA_8888 cbl = theme->ramp_left[i + 1];\n            const RGBA_8888 cbr = theme->ramp_right[i + 1];\n            const RGBA_8888 ctrm = Color_Mix(ctl, ctr, percent);\n            const RGBA_8888 cbrm = Color_Mix(cbl, cbr, percent);\n            const int32_t lsy = rect.y + i * rect.h / (UI_BAR_COLOR_STEPS - 1);\n            const int32_t lsh =\n                rect.y + (i + 1) * rect.h / (UI_BAR_COLOR_STEPS - 1) - lsy;\n            UI_ScheduleDrawScreenGradientQuad(\n                rect.x, lsy, 0, rect.w, lsh, ctl, ctrm, cbl, cbrm);\n        }\n    } else {\n        for (int32_t i = 0; i < UI_BAR_COLOR_STEPS; i++) {\n            const RGBA_8888 cl = theme->ramp_left[i];\n            const RGBA_8888 cr = theme->ramp_right[i];\n            const RGBA_8888 crm = Color_Mix(cl, cr, percent);\n            const int32_t lsy = rect.y + i * rect.h / UI_BAR_COLOR_STEPS;\n            const int32_t lsh =\n                rect.y + (i + 1) * rect.h / UI_BAR_COLOR_STEPS - lsy;\n            UI_ScheduleDrawScreenGradientQuad(\n                rect.x, lsy, 0, rect.w, lsh, cl, crm, cl, crm);\n        }\n    }\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    M_DATA *const data = node->data;\n    const UI_BAR_SETTINGS *const settings = &data->settings;\n\n    float percent = settings->value / (float)MAX(1, settings->max_value);\n    CLAMP(percent, 0.0f, 1.0f);\n    percent = (int32_t)(percent * 100) / 100.0f;\n\n    // Convert everything to screen coordinates\n    const int32_t x = UI_ScaleX(node->x);\n    const int32_t y = UI_ScaleY(node->y);\n    const int32_t w = UI_ScaleX(node->w);\n    const int32_t h = UI_ScaleY(node->h);\n    const int32_t border = h / (float)(UI_BAR_COLOR_STEPS + 4);\n    const int32_t padding = h / (float)(UI_BAR_COLOR_STEPS + 4);\n    const M_RECT_32 outer_rect = {\n        .x = x,\n        .y = y,\n        .w = w,\n        .h = h,\n    }, inner_rect = {\n        .x = outer_rect.x + border,\n        .y = outer_rect.y + border,\n        .w = outer_rect.w - border * 2,\n        .h = outer_rect.h - border * 2,\n    }, bar_rect = {\n        .x = inner_rect.x + padding,\n        .y = inner_rect.y + padding,\n        .w = (inner_rect.w - padding * 2) * percent,\n        .h = inner_rect.h - padding * 2,\n    };\n\n    switch (data->theme->kind) {\n    case UI_BAR_THEME_PC_KIND:\n        M_DrawBorderPC(data->theme, outer_rect, border);\n        M_DrawBackground(data->theme, inner_rect);\n        if (percent > 0.0f) {\n            M_DrawFillPC(data->theme, settings, bar_rect, percent);\n        }\n        break;\n    case UI_BAR_THEME_PS1_KIND:\n        M_DrawBorderPS1(data->theme, outer_rect, border);\n        M_DrawBackground(data->theme, inner_rect);\n        if (percent > 0.0f) {\n            M_DrawFillPS1(data->theme, settings, bar_rect, percent);\n        }\n        break;\n    }\n}\n\nvoid UI_Bar(const UI_BAR_SETTINGS settings)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    data->settings = settings;\n    data->theme = UI_Settings_GetBarTheme(settings.type);\n    data->scale = data->settings.preview ? 1.0f : data->theme->basic_scale;\n    UI_AddChild(node);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/settings.h>\n#include <trx/version.h>\n\n// shared properties of common ingame bars\n#define UI_BAR_WIDTH 208.0f\n#define UI_BAR_HEIGHT 18.0f\n#define UI_BAR_BORDER 2.0f\n#define UI_BAR_PADDING 2.0f\n#define UI_BAR_BLINK_THRESHOLD (g_TRVersion == 1 ? 0.2f : 0.25f)\n\ntypedef struct {\n    UI_BAR_TYPE type;\n    int32_t w;\n    int32_t h;\n    int32_t value;\n    int32_t max_value;\n    bool preview;\n} UI_BAR_SETTINGS;\n\n// draw functions\nvoid UI_Bar(UI_BAR_SETTINGS settings);\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_enemy_hp.c",
    "content": "#include <trx/game/ui/elements/bar_enemy_hp.h>\n\n#include <trx/config.h>\n#include <trx/game/creature.h>\n#include <trx/game/game.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/objects/vars.h>\n#include <trx/game/ui/elements/bar.h>\n\nbool UI_EnemyHealthBar(void)\n{\n    const ITEM *const target = Lara_GetLaraInfo()->target;\n    if (target == nullptr) {\n        return false;\n    }\n    const bool is_ally = Creature_IsAlly(target);\n\n    bool show = g_Config.ui.show_bars;\n    switch (g_Config.ui.enemy_health_bar.show_mode) {\n    case BAR_SHOW_MODE_NEVER:\n        show &= false;\n        break;\n    case BAR_SHOW_MODE_ALWAYS:\n        show &= true;\n        break;\n    case BAR_SHOW_MODE_BOSS_ONLY:\n        show &= Object_IsType(target->object_id, g_BossObjects);\n        break;\n    }\n    if (!show) {\n        return false;\n    }\n\n    UI_Bar((UI_BAR_SETTINGS) {\n        .type = is_ally ? UI_BAR_ALLY_HP : UI_BAR_ENEMY_HP,\n        .w = UI_BAR_WIDTH,\n        .h = UI_BAR_HEIGHT,\n        .value = target->hit_points,\n        .max_value =\n            target->max_hit_points * (Game_IsBonusFlagSet(GBF_NGPLUS) ? 2 : 1),\n    });\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_enemy_hp.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// draw functions\nbool UI_EnemyHealthBar(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_air.c",
    "content": "#include <trx/game/ui/elements/bar_lara_air.h>\n\n#include <trx/config.h>\n#include <trx/game/lara.h>\n#include <trx/game/rooms.h>\n#include <trx/game/ui/elements/bar.h>\n\nbool UI_LaraAirBar(const bool blink_state)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool is_blinking = g_Config.ui.enable_bar_flashing\n        && lara->air <= LARA_MAX_AIR * UI_BAR_BLINK_THRESHOLD;\n    const ROOM *const room = Room_Get(lara_item->room_num);\n    const bool show = g_Config.ui.show_bars\n        && (lara->water_status == LWS_UNDERWATER\n            || lara->water_status == LWS_SURFACE\n            || (room->flags.swamp && lara->air < LARA_MAX_AIR)\n            || (lara->water_status == LWS_ABOVE_WATER\n                && Lara_Vehicle_IsOnType(O_UPV)));\n    if (!show) {\n        return false;\n    }\n\n    UI_Bar((UI_BAR_SETTINGS) {\n        .type = UI_BAR_LARA_AIR,\n        .w = UI_BAR_WIDTH,\n        .h = UI_BAR_HEIGHT,\n        .value = is_blinking && blink_state ? 0 : lara->air,\n        .max_value = LARA_MAX_AIR,\n    });\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_air.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// draw functions\nbool UI_LaraAirBar(bool blink_state);\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_exposure.c",
    "content": "#include <trx/game/ui/elements/bar_lara_exposure.h>\n\n#include <trx/config.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/ui/elements/bar.h>\n\nbool UI_LaraExposureBar(const bool blink_state)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool is_blinking = g_Config.ui.enable_bar_flashing\n        && lara->exposure_timer <= LARA_MAX_EXPOSURE * UI_BAR_BLINK_THRESHOLD;\n\n    const bool show =\n        g_Config.ui.show_bars && lara->exposure_timer < LARA_MAX_EXPOSURE;\n    if (!show) {\n        return false;\n    }\n\n    int32_t value = is_blinking && blink_state ? 0 : lara->exposure_timer;\n    CLAMPL(value, 0);\n    UI_Bar((UI_BAR_SETTINGS) {\n        .type = UI_BAR_LARA_EXPOSURE,\n        .w = UI_BAR_WIDTH,\n        .h = UI_BAR_HEIGHT,\n        .value = value,\n        .max_value = LARA_MAX_EXPOSURE,\n    });\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_exposure.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// draw functions\nbool UI_LaraExposureBar(bool blink_state);\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_hp.c",
    "content": "#include <trx/game/ui/elements/bar_lara_hp.h>\n\n#include <trx/config.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/ui/elements/bar.h>\n\nstatic int32_t m_OldHealth = 0;\nstatic int32_t m_HitTimer = 0;\n\nvoid UI_LaraHealthBar_Control(void)\n{\n    m_HitTimer--;\n    CLAMPL(m_HitTimer, 0);\n}\n\nvoid UI_LaraHealthBar_SetTimer(const int16_t timer)\n{\n    m_HitTimer = timer;\n}\n\nbool UI_LaraHealthBar(const bool blink_state, const bool force)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    int32_t health = lara_item->hit_points;\n    CLAMP(health, 0, LARA_MAX_HITPOINTS);\n\n    if (m_OldHealth != health) {\n        m_OldHealth = health;\n        m_HitTimer = 40;\n    }\n\n    const bool is_blinking = g_Config.ui.enable_bar_flashing\n        && (health <= LARA_MAX_HITPOINTS * UI_BAR_BLINK_THRESHOLD\n            || lara->poison_timer != 0);\n    const bool is_recently_hurt = m_HitTimer > 0;\n    const bool show = force\n        || (g_Config.ui.show_bars\n            && (is_recently_hurt || health <= 0 || lara->gun_status == LGS_READY\n                || lara->poison_timer != 0 || is_blinking));\n    if (!show) {\n        return false;\n    }\n\n    UI_Bar((UI_BAR_SETTINGS) {\n        .type =\n            lara->poison_timer != 0 ? UI_BAR_LARA_HP_POISON : UI_BAR_LARA_HP,\n        .w = UI_BAR_WIDTH,\n        .h = UI_BAR_HEIGHT,\n        .value = is_blinking && blink_state ? 0 : health,\n        .max_value = LARA_MAX_HITPOINTS,\n    });\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_hp.h",
    "content": "#pragma once\n\n#include <trx/config/types.h>\n#include <trx/game/ui/common.h>\n\n// state functions\nvoid UI_LaraHealthBar_Control(void);\nvoid UI_LaraHealthBar_SetTimer(int16_t timer);\n\n// draw functions\nbool UI_LaraHealthBar(bool blink_state, bool force);\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_sprint.c",
    "content": "#include <trx/game/ui/elements/bar_lara_sprint.h>\n\n#include <trx/config.h>\n#include <trx/game/lara/common.h>\n#include <trx/game/lara/const.h>\n#include <trx/game/ui/elements/bar.h>\n\nbool UI_LaraSprintBar(void)\n{\n    const ITEM *const lara_item = Lara_GetItem();\n    if (lara_item == nullptr) {\n        return false;\n    }\n    const LARA_INFO *const lara = Lara_GetLaraInfo();\n    const bool show =\n        g_Config.ui.show_bars && lara->sprint_timer < LARA_MAX_SPRINT;\n    if (!show) {\n        return false;\n    }\n\n    UI_Bar((UI_BAR_SETTINGS) {\n        .type = UI_BAR_LARA_STAMINA,\n        .w = UI_BAR_WIDTH,\n        .h = UI_BAR_HEIGHT,\n        .value = lara->sprint_timer,\n        .max_value = LARA_MAX_SPRINT,\n    });\n    return true;\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/bar_lara_sprint.h",
    "content": "#pragma once\n\nbool UI_LaraSprintBar(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/button_label.c",
    "content": "#include <trx/game/ui/elements/button_label.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/ui/elements/label.h>\n\nvoid UI_ButtonLabel(INPUT_ROLE input_role, const char *const label)\n{\n    UI_ButtonLabelEx(Input_GetRoleName(input_role), label);\n}\n\nvoid UI_ButtonLabelEx(const char *const input_label, const char *const label)\n{\n    UI_LabelFmt(\"\\\\{button empty} %s: %s\", input_label, label);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/button_label.h",
    "content": "#pragma once\n\n#include <trx/game/input.h>\n#include <trx/game/ui/common.h>\n\nvoid UI_ButtonLabel(INPUT_ROLE input_role, const char *label);\nvoid UI_ButtonLabelEx(const char *input_label, const char *label);\n"
  },
  {
    "path": "src/trx/game/ui/elements/color_swatch.c",
    "content": "#include <trx/game/ui/elements/color_swatch.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/helpers.h>\n\ntypedef struct {\n    float w;\n    float h;\n    RGBA_8888 color;\n} M_DATA;\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    const M_DATA *const data = node->data;\n    node->measure_w = data->w * g_Config.ui.text_scale;\n    node->measure_h = data->h * g_Config.ui.text_scale;\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    const M_DATA *const data = node->data;\n    const int32_t x = UI_ScaleX(node->x);\n    const int32_t y = UI_ScaleY(node->y);\n    const int32_t w = UI_ScaleX(node->w);\n    const int32_t h = UI_ScaleY(node->h);\n    const int32_t border = MAX(1, UI_ScaleX(1.0f));\n    UI_ScheduleDrawScreenFlatQuad(x, y, 0, w, h, COLOR_RGBA_8888_BLACK);\n    UI_ScheduleDrawScreenFlatQuad(\n        x + border, y + border, 0, w - border * 2, h - border * 2, data->color);\n    UI_ScheduleDrawTextOutline(\n        g_Config.ui.menu_style, x + border, y + border, 0, w - border * 2,\n        h - border * 2, TS_HEADING);\n}\n\nvoid UI_ColorSwatch(const UI_COLOR_SWATCH_SETTINGS settings)\n{\n    ASSERT(settings.w > 0.0f);\n    ASSERT(settings.h > 0.0f);\n\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    data->w = settings.w;\n    data->h = settings.h;\n    data->color = Color_RGBToRGBA(settings.color);\n    UI_AddChild(node);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/color_swatch.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n\ntypedef struct {\n    RGB_888 color;\n    float w;\n    float h;\n} UI_COLOR_SWATCH_SETTINGS;\n\nvoid UI_ColorSwatch(UI_COLOR_SWATCH_SETTINGS settings);\n"
  },
  {
    "path": "src/trx/game/ui/elements/flash.c",
    "content": "#include <trx/game/ui/elements/flash.h>\n\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/helpers.h>\n\nvoid UI_Flash_Init(UI_FLASH_STATE *const s, const int32_t rate)\n{\n    s->count = 0;\n    s->rate = rate;\n}\n\nvoid UI_Flash_Free(UI_FLASH_STATE *const s)\n{\n}\n\nvoid UI_Flash_Control(UI_FLASH_STATE *const s)\n{\n    s->count -= 2;\n    if (s->count < -s->rate) {\n        s->count = s->rate;\n    }\n}\n\nvoid UI_BeginFlash(const UI_FLASH_STATE *const s)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = UI_LayoutWrapper,\n            .draw = UI_DrawWrapper,\n        },\n        0);\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n    UI_BeginHide(s->count >= 0);\n}\n\nvoid UI_EndFlash(void)\n{\n    UI_EndHide();\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/flash.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n#include <stdint.h>\n\n// Make the child widget invisible in the specified interval.\n\ntypedef struct {\n    int32_t rate;\n    int32_t count;\n} UI_FLASH_STATE;\n\n// state functions\nvoid UI_Flash_Init(UI_FLASH_STATE *s, int32_t rate);\nvoid UI_Flash_Free(UI_FLASH_STATE *s);\nvoid UI_Flash_Control(UI_FLASH_STATE *s);\n\n// draw functions\nvoid UI_BeginFlash(const UI_FLASH_STATE *s);\nvoid UI_EndFlash(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/fps_counter.c",
    "content": "#include <trx/game/ui/elements/fps_counter.h>\n\n#include <trx/core/memory.h>\n#include <trx/game/clock.h>\n#include <trx/game/ui/elements/label.h>\n\n#include <stdio.h>\n\ntypedef struct UI_FPS_COUNTER_STATE {\n    int32_t drawn_frames;\n    int32_t fps_counter;\n    CLOCK_TIMER timer;\n} UI_FPS_COUNTER_STATE;\n\nUI_FPS_COUNTER_STATE *UI_FPSCounter_Init(void)\n{\n    UI_FPS_COUNTER_STATE *const s = Memory_Alloc(sizeof(UI_FPS_COUNTER_STATE));\n    s->timer.type = CLOCK_TIMER_REAL;\n    return s;\n}\n\nvoid UI_FPSCounter_Free(UI_FPS_COUNTER_STATE *const s)\n{\n    if (s != nullptr) {\n        Memory_Free(s);\n    }\n}\n\nvoid UI_FPSCounter(UI_FPS_COUNTER_STATE *const s)\n{\n    UI_LabelFmt(\"%d FPS\", s->fps_counter);\n    s->drawn_frames++;\n    if (ClockTimer_CheckElapsedAndTake(&s->timer, 1.0)) {\n        s->fps_counter = s->drawn_frames;\n        s->drawn_frames = 0;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/fps_counter.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\ntypedef struct UI_FPS_COUNTER_STATE UI_FPS_COUNTER_STATE;\n\n// state functions\nUI_FPS_COUNTER_STATE *UI_FPSCounter_Init(void);\nvoid UI_FPSCounter_Free(UI_FPS_COUNTER_STATE *s);\n\n// draw functions\nvoid UI_FPSCounter(UI_FPS_COUNTER_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/elements/frame.c",
    "content": "#include <trx/game/ui/elements/frame.h>\n\n#include <trx/config.h>\n#include <trx/game/output.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/helpers.h>\n\ntypedef struct {\n    UI_STYLE ui_style;\n    TEXT_STYLE text_style;\n    int32_t outline_z;\n    int32_t background_z;\n} M_DATA;\n\nstatic void M_Draw(const UI_NODE *node)\n{\n    const M_DATA *const data = node->data;\n    if (data->background_z >= 0) {\n        UI_ScheduleDrawTextBackground(\n            data->ui_style, UI_ScaleX(node->x), UI_ScaleY(node->y),\n            data->background_z, UI_ScaleX(node->w), UI_ScaleY(node->h),\n            data->text_style);\n    }\n    if (data->outline_z >= 0) {\n        UI_ScheduleDrawTextOutline(\n            data->ui_style, UI_ScaleX(node->x), UI_ScaleY(node->y),\n            data->outline_z, UI_ScaleX(node->w), UI_ScaleY(node->h),\n            data->text_style);\n    }\n    UI_DrawWrapper(node);\n}\n\nvoid UI_BeginFrame(UI_FRAME_STYLE style)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = UI_LayoutWrapper,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n\n    data->ui_style = g_Config.ui.menu_style;\n\n    switch (style) {\n    case UI_FRAME_DIALOG_BACKGROUND:\n        data->outline_z = 160;\n        data->background_z = 160;\n        data->text_style = TS_BACKGROUND;\n        break;\n    case UI_FRAME_DIALOG_BACKGROUND_HEAVY:\n        data->outline_z = 160;\n        data->background_z = 160;\n        data->text_style = TS_BACKGROUND_HEAVY;\n        break;\n    case UI_FRAME_DIALOG_HEADING:\n        data->outline_z = 80;\n        data->background_z = 80;\n        data->text_style = TS_HEADING;\n        break;\n    case UI_FRAME_SELECTED_OPTION:\n        data->outline_z = 80;\n        data->background_z = 80;\n        data->text_style = TS_REQUESTED;\n        break;\n    case UI_FRAME_OUTLINE_ONLY:\n        data->outline_z = 80;\n        data->background_z = -1;\n        data->text_style = TS_REQUESTED;\n        break;\n    }\n\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndFrame(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/frame.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// A frame around the child widget.\n\ntypedef enum {\n    UI_FRAME_DIALOG_BACKGROUND,\n    UI_FRAME_DIALOG_BACKGROUND_HEAVY,\n    UI_FRAME_DIALOG_HEADING,\n    UI_FRAME_SELECTED_OPTION,\n    UI_FRAME_OUTLINE_ONLY,\n} UI_FRAME_STYLE;\n\nvoid UI_BeginFrame(UI_FRAME_STYLE style);\nvoid UI_EndFrame(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/gradient_slider.c",
    "content": "#include <trx/game/ui/elements/gradient_slider.h>\n\n#include <trx/config.h>\n#include <trx/core/colors.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/game/ui/text.h>\n\n#include <math.h>\n#include <string.h>\n\n#define M_MARKER_BORDER 1.0f\n#define M_MARKER_WIDTH 4.0f\n\ntypedef struct {\n    float width;\n    float value;\n    int32_t stop_count;\n} M_DATA;\n\nstatic void M_DrawMarker(const float x, const float y, const float h)\n{\n    const float inner_w_s = UI_ScaleX(M_MARKER_WIDTH);\n    const float inner_h_s = UI_ScaleY(h);\n    const int32_t border_s = UI_ScaleX(M_MARKER_BORDER);\n\n    // Compute pixel‑snapped corners so both sides are balanced\n    const int32_t inner_x1 = floorf(x - inner_w_s * 0.5f + 0.5f);\n    const int32_t inner_y1 = floorf(y - inner_h_s * 0.5f + 0.5f);\n    const int32_t inner_x2 = floorf(x + inner_w_s * 0.5f + 0.5f);\n    const int32_t inner_y2 = floorf(y + inner_h_s * 0.5f + 0.5f);\n\n    const int32_t outer_x1 = inner_x1 - border_s;\n    const int32_t outer_y1 = inner_y1 - border_s;\n    const int32_t outer_x2 = inner_x2 + border_s;\n    const int32_t outer_y2 = inner_y2 + border_s;\n\n    // Derive final snapped widths/heights\n    const int32_t inner_w = inner_x2 - inner_x1;\n    const int32_t inner_h = inner_y2 - inner_y1;\n    const int32_t outer_w = outer_x2 - outer_x1;\n    const int32_t outer_h = outer_y2 - outer_y1;\n\n    UI_ScheduleDrawScreenFlatQuad(\n        outer_x1, outer_y1, 0, outer_w, outer_h, COLOR_RGBA_8888_BLACK);\n    UI_ScheduleDrawScreenFlatQuad(\n        inner_x1, inner_y1, 0, inner_w, inner_h, COLOR_RGBA_8888_WHITE);\n\n    UI_ScheduleDrawTextOutline(\n        g_Config.ui.menu_style, outer_x1, outer_y1, 0, outer_w - 1, outer_h - 1,\n        TS_REQUESTED);\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    const M_DATA *const data = node->data;\n    const RGB_888 *const stops = (const RGB_888 *)(data + 1);\n\n    const float x = UI_ScaleX(node->x);\n    const float y = UI_ScaleY(node->y);\n    const float w = UI_ScaleX(node->w);\n    const float h = UI_ScaleY(node->h);\n    const float border = UI_ScaleX(1.0f);\n    const float inner_x = x + border;\n    const float inner_y = y + border;\n    const float inner_w = w - border * 2;\n    const float inner_h = h - border * 2;\n\n    UI_ScheduleDrawScreenFlatQuad(x, y, 0, w, h, COLOR_RGBA_8888_BLACK);\n\n    if (data->stop_count == 1) {\n        UI_ScheduleDrawScreenFlatQuad(\n            inner_x, inner_y, 0, inner_w, inner_h, Color_RGBToRGBA(stops[0]));\n    } else {\n        for (int32_t i = 0; i < data->stop_count - 1; i++) {\n            const int32_t sx = inner_x + i * inner_w / (data->stop_count - 1);\n            const int32_t sw =\n                inner_x + (i + 1) * inner_w / (data->stop_count - 1) - sx;\n            UI_ScheduleDrawScreenGradientQuad(\n                sx, inner_y, 0, MAX(1, sw), inner_h, Color_RGBToRGBA(stops[i]),\n                Color_RGBToRGBA(stops[i + 1]), Color_RGBToRGBA(stops[i]),\n                Color_RGBToRGBA(stops[i + 1]));\n        }\n    }\n\n    float marker_x = inner_x + data->value * inner_w;\n    float marker_y = inner_y + inner_h * 0.5f;\n    M_DrawMarker(marker_x, marker_y, node->h);\n}\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    const M_DATA *const data = node->data;\n    node->measure_w = data->width;\n    node->measure_h = UI_TEXT_HEIGHT * 0.5f * g_Config.ui.text_scale;\n}\n\nvoid UI_GradientSlider(const UI_GRADIENT_SLIDER_SETTINGS settings)\n{\n    ASSERT(settings.stop_count > 0);\n    ASSERT(settings.stops != nullptr);\n\n    const size_t extra_size =\n        sizeof(M_DATA) + sizeof(RGB_888) * settings.stop_count;\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = M_Draw,\n        },\n        extra_size);\n    M_DATA *const data = node->data;\n    data->width = settings.width;\n    data->value = settings.value;\n    CLAMP(data->value, 0.0f, 1.0f);\n    data->stop_count = settings.stop_count;\n    RGB_888 *const stops = (RGB_888 *)(data + 1);\n    memcpy(stops, settings.stops, sizeof(RGB_888) * settings.stop_count);\n    UI_AddChild(node);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/gradient_slider.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    float width;\n    float value;\n    int32_t stop_count;\n    const RGB_888 *stops;\n} UI_GRADIENT_SLIDER_SETTINGS;\n\nvoid UI_GradientSlider(UI_GRADIENT_SLIDER_SETTINGS settings);\n"
  },
  {
    "path": "src/trx/game/ui/elements/hide.c",
    "content": "#include <trx/game/ui/elements/hide.h>\n\n#include <trx/game/ui/helpers.h>\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    const bool draw_children = *(bool *)node->data;\n    if (draw_children) {\n        UI_DrawWrapper(node);\n    }\n}\n\nvoid UI_BeginHide(const bool hide_children)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = UI_LayoutWrapper,\n            .draw = M_Draw,\n        },\n        sizeof(bool));\n    *(bool *)node->data = !hide_children;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndHide(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/hide.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// Make the child widget invisible if the given flag is true.\n// The children still take up their size.\n\nvoid UI_BeginHide(bool hide_children);\nvoid UI_EndHide(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/horizontal_line.c",
    "content": "#include <trx/game/ui/elements/horizontal_line.h>\n\n#include <trx/config.h>\n#include <trx/game/output.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/helpers.h>\n\nstatic void M_Draw(const UI_NODE *node)\n{\n    // UI_DrawWrapper(node);\n    UI_ScheduleDrawHorizontalLine(\n        g_Config.ui.menu_style, UI_ScaleX(node->x),\n        UI_ScaleX(node->x + node->w), UI_ScaleY(node->y + node->h / 2.0f), 0);\n}\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    UI_MeasureWrapper(node);\n    node->measure_h = 2 * g_Config.ui.text_scale;\n}\n\nvoid UI_HorizontalLine(void)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = M_Draw,\n        },\n        0);\n    UI_AddChild(node);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/horizontal_line.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\nvoid UI_HorizontalLine(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/label.c",
    "content": "#include <trx/game/ui/elements/label.h>\n\n#include <trx/config.h>\n#include <trx/core/strings.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/game/ui/text.h>\n\n#include <stdarg.h>\n#include <string.h>\n\ntypedef struct {\n    UI_LABEL_SETTINGS settings;\n    char *text;\n} M_DATA;\n\nstatic UI_LABEL_SETTINGS m_DefaultSettings = { .scale = 1.0f };\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    M_DATA *const data = node->data;\n    float w = 0.0f, h = 0.0f;\n    UI_Text_Measure(data->text, &w, &h, data->settings);\n    node->measure_w = w;\n    node->measure_h = h;\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    M_DATA *const data = node->data;\n    UI_Text_Draw(data->text, node->x, node->y, data->settings);\n    UI_DrawWrapper(node);\n}\n\nvoid UI_Label(const char *const text)\n{\n    UI_LabelEx(text, m_DefaultSettings);\n}\n\nvoid UI_LabelFmt(const char *fmt, ...)\n{\n    va_list args;\n    va_start(args, fmt);\n    const char *const text = String_FormatStaticV(fmt, args);\n    va_end(args);\n    UI_Label(text);\n}\n\nvoid UI_LabelEx(const char *text, const UI_LABEL_SETTINGS settings)\n{\n    if (text == nullptr) {\n        text = \"(null)\"; // quality of life for UI development\n    }\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA) + (text != nullptr ? strlen(text) + 1 : 1));\n    M_DATA *const data = node->data;\n    data->settings = settings;\n    data->text = (char *)node->data + sizeof(M_DATA);\n    strcpy(data->text, text != nullptr ? text : \"\");\n    UI_AddChild(node);\n}\n\nvoid UI_Label_Measure(\n    const char *const text, float *const out_w, float *const out_h)\n{\n    UI_Label_MeasureEx(text, out_w, out_h, m_DefaultSettings);\n}\n\nfloat UI_Label_MeasureW(const char *const text)\n{\n    float result;\n    UI_Label_Measure(text, &result, nullptr);\n    return result;\n}\n\nvoid UI_Label_MeasureEx(\n    const char *const text, float *const out_w, float *const out_h,\n    const UI_LABEL_SETTINGS settings)\n{\n    float w = 0.0f, h = 0.0f;\n    UI_Text_Measure(text, &w, &h, settings);\n    if (out_w != nullptr) {\n        *out_w = w;\n    }\n    if (out_h != nullptr) {\n        *out_h = h;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/label.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/text.h>\n\n// Basic text widget.\n\ntypedef UI_TEXT_SETTINGS UI_LABEL_SETTINGS;\n\nvoid UI_Label(const char *text);\nvoid UI_LabelFmt(const char *fmt, ...);\nvoid UI_LabelEx(const char *text, UI_LABEL_SETTINGS settings);\n\nvoid UI_Label_Measure(const char *text, float *out_w, float *out_h);\nfloat UI_Label_MeasureW(const char *text);\nvoid UI_Label_MeasureEx(\n    const char *text, float *out_w, float *out_h, UI_LABEL_SETTINGS settings);\n"
  },
  {
    "path": "src/trx/game/ui/elements/modal.c",
    "content": "#include <trx/game/ui/elements/modal.h>\n\n#include <trx/game/output.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/helpers.h>\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    node->measure_w = UI_GetCanvasWidth();\n    node->measure_h = UI_GetCanvasHeight();\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    UI_DrawWrapper(node);\n}\n\nvoid UI_BeginModal(const float x, const float y)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutWrapper,\n            .draw = M_Draw,\n        },\n        0);\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n    UI_BeginAnchor(x, y);\n}\n\nvoid UI_EndModal(void)\n{\n    UI_EndAnchor();\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/modal.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// A widget that resizes the children to the canvas size\n// and places it at a specific proportional spot.\n\nvoid UI_BeginModal(float x, float y);\nvoid UI_EndModal(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/offset.c",
    "content": "#include <trx/game/ui/elements/offset.h>\n\n#include <trx/game/ui/elements/pad.h>\n\nvoid UI_BeginOffset(const float x, const float y)\n{\n    UI_BeginPadEx(x, -x, y, -y);\n}\n\nvoid UI_EndOffset(void)\n{\n    UI_EndPad();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/offset.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// A transformer to move the child element in a certain direction.\n// Does not affect the occupied space.\n\nvoid UI_BeginOffset(float x, float y);\nvoid UI_EndOffset(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/pad.c",
    "content": "#include <trx/game/ui/elements/pad.h>\n\n#include <trx/config.h>\n#include <trx/game/ui/helpers.h>\n\ntypedef struct {\n    float t;\n    float r;\n    float d;\n    float l;\n} M_DATA;\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    UI_MeasureWrapper(node);\n    const M_DATA *const data = node->data;\n    node->measure_w += data->l + data->r;\n    node->measure_h += data->t + data->d;\n}\n\nstatic void M_Layout(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n    const M_DATA *const data = node->data;\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        child->ops.layout(\n            child, x + data->l, y + data->t, w - data->l - data->r,\n            h - data->t - data->d);\n        child = child->next_sibling;\n    }\n}\n\nvoid UI_BeginPad(const float x, const float y)\n{\n    UI_BeginPadEx(x, x, y, y);\n}\n\nvoid UI_BeginPadEx(const float l, const float r, const float t, const float d)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = M_Layout,\n            .draw = UI_DrawWrapper,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    data->t = t * g_Config.ui.text_scale;\n    data->r = r * g_Config.ui.text_scale;\n    data->d = d * g_Config.ui.text_scale;\n    data->l = l * g_Config.ui.text_scale;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndPad(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/pad.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// An invisible border in pixel units around the child widget.\n\nvoid UI_BeginPad(float x, float y);\nvoid UI_BeginPadEx(float l, float r, float t, float d);\nvoid UI_EndPad(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/progress_button.c",
    "content": "#include <trx/game/ui/elements/progress_button.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/const.h>\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/sleek_bar.h>\n#include <trx/game/ui/elements/stack.h>\n\n#define M_HOLD_TIMER_DEBUFF (LOGIC_FPS / 3)\n#define M_HOLD_TIMER_MAX LOGIC_FPS\n\nstruct UI_PROGRESS_BUTTON_STATE {\n    GAME_STRING_ID text;\n    INPUT_BACKEND backend;\n    INPUT_ROLE role;\n    UI_PROGRESS_BUTTON_CALLBACK func;\n    void *func_arg;\n    int32_t hold_timer;\n};\n\nUI_PROGRESS_BUTTON_STATE *UI_ProgressButton_Init(\n    INPUT_BACKEND backend, INPUT_ROLE role, GAME_STRING_ID text,\n    UI_PROGRESS_BUTTON_CALLBACK func, void *func_arg)\n{\n    UI_PROGRESS_BUTTON_STATE *const s =\n        Memory_Alloc(sizeof(UI_PROGRESS_BUTTON_STATE));\n    s->backend = backend;\n    s->role = role;\n    s->text = text;\n    s->func = func;\n    s->func_arg = func_arg;\n    s->hold_timer = 0;\n    return s;\n}\n\nvoid UI_ProgressButton_Control(UI_PROGRESS_BUTTON_STATE *const s)\n{\n    if (!Input_IsPressedEx(s->backend, INPUT_LAYOUT_DEFAULT, s->role)) {\n        s->hold_timer = 0;\n        return;\n    }\n    if (s->hold_timer != -1) {\n        s->hold_timer++;\n        if (s->hold_timer - M_HOLD_TIMER_DEBUFF > M_HOLD_TIMER_MAX) {\n            s->func(s->func_arg);\n            s->hold_timer = -1; // Debounce the key\n        }\n    }\n}\n\nvoid UI_ProgressButton_Free(UI_PROGRESS_BUTTON_STATE *s)\n{\n    Memory_Free(s);\n}\n\nvoid UI_ProgressButton(UI_PROGRESS_BUTTON_STATE *const s)\n{\n    const float scale = 0.85f;\n    const char *const key_name =\n        Input_GetKeyName(s->backend, INPUT_LAYOUT_DEFAULT, s->role, 0);\n    if (key_name == nullptr) {\n        return;\n    }\n    const char *const value_label =\n        String_FormatStatic(GS(\"general/misc/hold_fmt\"), key_name);\n    const char *const text =\n        String_FormatStatic(\"%s: %s\", GameString_Get(s->text), value_label);\n\n    const float pad[2] = { 6.0f, 3.0f };\n    const float spacing = 2.0f;\n    const float progress =\n        (s->hold_timer - M_HOLD_TIMER_DEBUFF) / (float)M_HOLD_TIMER_MAX;\n\n    UI_BeginPad(pad[0], pad[1]);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = {\n            .h = UI_STACK_H_ALIGN_SPAN,\n            .v = UI_STACK_V_ALIGN_TOP,\n        },\n        .spacing = {\n            .h = 0.0f,\n            .v = spacing,\n        },\n    });\n    UI_LabelEx(text, (UI_LABEL_SETTINGS) { .scale = scale });\n    UI_BeginHide(progress < 0.0f);\n    UI_SleekBar(progress);\n    UI_EndHide();\n    UI_EndStack();\n    UI_EndPad();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/progress_button.h",
    "content": "#pragma once\n\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/common.h>\n\ntypedef void (*UI_PROGRESS_BUTTON_CALLBACK)(void *);\n\ntypedef struct UI_PROGRESS_BUTTON_STATE UI_PROGRESS_BUTTON_STATE;\n\nUI_PROGRESS_BUTTON_STATE *UI_ProgressButton_Init(\n    INPUT_BACKEND backend, INPUT_ROLE role, GAME_STRING_ID text,\n    UI_PROGRESS_BUTTON_CALLBACK func, void *func_arg);\nvoid UI_ProgressButton_Control(UI_PROGRESS_BUTTON_STATE *s);\nvoid UI_ProgressButton_Free(UI_PROGRESS_BUTTON_STATE *s);\n\nvoid UI_ProgressButton(UI_PROGRESS_BUTTON_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/elements/prompt.c",
    "content": "#include <trx/game/ui/elements/prompt.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/game/const.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/flash.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/events.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/game/ui/text.h>\n\n#include <string.h>\n\ntypedef struct {\n    UI_PROMPT_STATE *state;\n} M_DATA;\n\nstatic void M_Layout(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n    const M_DATA *const data = node->data;\n    const UI_PROMPT_STATE *const s = data->state;\n    UI_NODE *const prompt = node->first_child;\n    UI_NODE *const caret = prompt->next_sibling;\n    prompt->ops.layout(prompt, x, y, w, h);\n\n    const char old = s->current_text[s->caret_pos];\n    s->current_text[s->caret_pos] = '\\0';\n    float caret_pos;\n    UI_Label_Measure(s->current_text, &caret_pos, nullptr);\n    s->current_text[s->caret_pos] = old;\n\n    caret->ops.layout(caret, x + caret_pos, y, w, h);\n}\n\nstatic int32_t M_GetPrevCaretPos(\n    const char *const text, const int32_t caret_pos)\n{\n    if (caret_pos <= 0) {\n        return 0;\n    }\n\n    const char *const caret_ptr = text + caret_pos;\n    const char *p = text;\n    const char *prev = text;\n\n    while (p < caret_ptr) {\n        prev = p;\n        p += String_GetCharByteSize(p);\n    }\n\n    return (int32_t)(prev - text);\n}\n\nstatic int32_t M_GetNextCaretPos(\n    const char *const text, const int32_t caret_pos)\n{\n    const size_t text_len = strlen(text);\n    if ((size_t)caret_pos >= text_len) {\n        return (int32_t)text_len;\n    }\n\n    int32_t next_pos =\n        caret_pos + (int32_t)String_GetCharByteSize(text + caret_pos);\n    if ((size_t)next_pos > text_len) {\n        next_pos = (int32_t)text_len;\n    }\n    return next_pos;\n}\n\nstatic void M_MoveCaretLeft(UI_PROMPT_STATE *const s)\n{\n    s->caret_pos = M_GetPrevCaretPos(s->current_text, s->caret_pos);\n}\n\nstatic void M_MoveCaretRight(UI_PROMPT_STATE *const s)\n{\n    s->caret_pos = M_GetNextCaretPos(s->current_text, s->caret_pos);\n}\n\nstatic void M_MoveCaretStart(UI_PROMPT_STATE *const s)\n{\n    s->caret_pos = 0;\n}\n\nstatic void M_MoveCaretEnd(UI_PROMPT_STATE *const s)\n{\n    s->caret_pos = strlen(s->current_text);\n}\n\nstatic void M_DeleteCharBack(UI_PROMPT_STATE *const s)\n{\n    if (s->caret_pos <= 0) {\n        return;\n    }\n\n    const int32_t delete_start =\n        M_GetPrevCaretPos(s->current_text, s->caret_pos);\n    if (delete_start >= s->caret_pos || delete_start < 0) {\n        return;\n    }\n\n    memmove(\n        s->current_text + delete_start, s->current_text + s->caret_pos,\n        strlen(s->current_text) + 1 - (size_t)s->caret_pos);\n    s->caret_pos = delete_start;\n}\n\nstatic void M_Clear(UI_PROMPT_STATE *const s)\n{\n    strcpy(s->current_text, \"\");\n    s->caret_pos = 0;\n}\n\nstatic void M_Cancel(UI_PROMPT_STATE *const s)\n{\n    UI_FireEvent((EVENT) {\n        .name = \"cancel\",\n        .sender = s,\n        .data = s->current_text,\n    });\n    M_Clear(s);\n}\n\nstatic void M_Confirm(UI_PROMPT_STATE *const s)\n{\n    if (String_IsEmpty(s->current_text)) {\n        M_Cancel(s);\n        return;\n    }\n    UI_FireEvent((EVENT) {\n        .name = \"confirm\",\n        .sender = s,\n        .data = s->current_text,\n    });\n    M_Clear(s);\n}\n\nstatic void M_HandleKeyDown(const EVENT *const event, void *const user_data)\n{\n    const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data;\n    UI_PROMPT_STATE *const s = user_data;\n\n    if (!s->is_focused) {\n        return;\n    }\n\n    // clang-format off\n    switch (key) {\n    case UI_KEY_LEFT:   M_MoveCaretLeft(s); break;\n    case UI_KEY_RIGHT:  M_MoveCaretRight(s); break;\n    case UI_KEY_HOME:   M_MoveCaretStart(s); break;\n    case UI_KEY_END:    M_MoveCaretEnd(s); break;\n    case UI_KEY_BACK:   M_DeleteCharBack(s); break;\n    case UI_KEY_RETURN: M_Confirm(s); break;\n    case UI_KEY_ESCAPE: M_Cancel(s); break;\n    default:            break;\n    }\n    // clang-format on\n}\n\nstatic void M_HandleTextEdit(const EVENT *const event, void *const user_data)\n{\n    UI_PROMPT_STATE *const s = user_data;\n    if (!s->is_focused) {\n        return;\n    }\n\n    char *filtered = UI_Text_FilterGlyphs(event->data);\n    if (filtered == nullptr || filtered[0] == '\\0') {\n        Memory_FreePointer(&filtered);\n        return;\n    }\n    const char *insert_string = filtered;\n    const size_t insert_length = strlen(insert_string);\n\n    const size_t available_space =\n        s->current_text_capacity - strlen(s->current_text);\n    if (insert_length >= available_space) {\n        s->current_text_capacity *= 2;\n        s->current_text =\n            Memory_Realloc(s->current_text, s->current_text_capacity);\n    }\n\n    memmove(\n        s->current_text + s->caret_pos + insert_length,\n        s->current_text + s->caret_pos,\n        strlen(s->current_text) + 1 - s->caret_pos);\n    memcpy(s->current_text + s->caret_pos, insert_string, insert_length);\n\n    s->caret_pos += insert_length;\n    Memory_FreePointer(&filtered);\n}\n\nvoid UI_Prompt_Init(UI_PROMPT_STATE *const s)\n{\n    s->is_focused = false;\n    s->current_text_capacity = 30;\n    s->current_text = Memory_Alloc(s->current_text_capacity);\n    s->listener1 = UI_Subscribe(\"key_down\", nullptr, M_HandleKeyDown, s);\n    s->listener2 = UI_Subscribe(\"text_edit\", nullptr, M_HandleTextEdit, s);\n    UI_Flash_Init(&s->flash, LOGIC_FPS * 2 / 3);\n}\n\nvoid UI_Prompt_Free(UI_PROMPT_STATE *const s)\n{\n    UI_Unsubscribe(s->listener1);\n    UI_Unsubscribe(s->listener2);\n    UI_Flash_Free(&s->flash);\n    Memory_FreePointer(&s->current_text);\n}\n\nvoid UI_Prompt_Control(UI_PROMPT_STATE *const s)\n{\n    UI_Flash_Control(&s->flash);\n}\n\nvoid UI_Prompt(UI_PROMPT_STATE *const s)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = M_Layout,\n            .draw = UI_DrawWrapper,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    data->state = s;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n    UI_LabelEx(\n        s->current_text != nullptr ? s->current_text : \"\",\n        (UI_LABEL_SETTINGS) { .scale = 1.0f, .z = 16 });\n    if (s->is_focused) {\n        UI_BeginFlash(&s->flash);\n    }\n    UI_LabelEx(\n        \"\\\\{button left}\", (UI_LABEL_SETTINGS) { .scale = 1.0f, .z = 8 });\n    if (s->is_focused) {\n        UI_EndFlash();\n    }\n    UI_PopCurrent();\n}\n\nvoid UI_Prompt_SetFocus(UI_PROMPT_STATE *const s, const bool is_focused)\n{\n    if (s->is_focused == is_focused) {\n        return;\n    }\n    s->is_focused = is_focused;\n    s->flash.count = 0;\n    if (is_focused) {\n        Input_EnterListenMode();\n    } else {\n        Input_ExitListenMode();\n    }\n}\n\nvoid UI_Prompt_Clear(UI_PROMPT_STATE *const s)\n{\n    M_Clear(s);\n}\n\nvoid UI_Prompt_ChangeText(UI_PROMPT_STATE *const s, const char *const new_text)\n{\n    Memory_FreePointer(&s->current_text);\n    s->current_text = Memory_DupStr(new_text);\n    s->current_text_capacity = strlen(new_text) + 1;\n    s->caret_pos = strlen(new_text);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/prompt.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/elements/flash.h>\n\n#include <stdint.h>\n\n// A text edit widget that collects text input from the player.\n// Needs to be in focus to work, otherwise is inactive.\n\ntypedef struct {\n    bool is_focused;\n    int32_t caret_pos;\n    int32_t current_text_capacity;\n    char *current_text;\n\n    int32_t listener1;\n    int32_t listener2;\n\n    UI_FLASH_STATE flash;\n} UI_PROMPT_STATE;\n\n// state functions\nvoid UI_Prompt_Init(UI_PROMPT_STATE *s);\nvoid UI_Prompt_Free(UI_PROMPT_STATE *s);\nvoid UI_Prompt_Control(UI_PROMPT_STATE *s);\nvoid UI_Prompt_Clear(UI_PROMPT_STATE *s);\nvoid UI_Prompt_SetFocus(UI_PROMPT_STATE *s, bool is_focused);\nvoid UI_Prompt_ChangeText(UI_PROMPT_STATE *s, const char *new_text);\n\n// draw functions\nvoid UI_Prompt(UI_PROMPT_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/elements/requester.c",
    "content": "#include <trx/game/ui/elements/requester.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/input.h>\n#include <trx/game/ui.h>\n#include <trx/version.h>\n\nvoid UI_Requester_Init(\n    UI_REQUESTER_STATE *const s, const int32_t vis_rows, const int32_t max_rows,\n    const bool is_selectable)\n{\n    s->scroll = (UI_SCROLLABLE) {\n        .first_item = 0,\n        .sel_item = 0,\n        .vis_items = vis_rows,\n        .max_items = max_rows,\n    };\n    s->is_selectable = is_selectable;\n    s->row_pad = 20.0f;\n    s->row_spacing = 3.0f;\n    s->show_arrows = false;\n    s->reserve_space = false;\n}\n\nvoid UI_Requester_Free(UI_REQUESTER_STATE *const s)\n{\n}\n\nint32_t UI_Requester_Control(UI_REQUESTER_STATE *const s)\n{\n    if (s->is_selectable) {\n        if (g_InputDB.menu_down) {\n            UI_Scrollable_SelectNext(&s->scroll, g_Config.ui.enable_wraparound);\n        } else if (g_InputDB.menu_up) {\n            UI_Scrollable_SelectPrev(&s->scroll, g_Config.ui.enable_wraparound);\n        }\n    } else {\n        if (g_InputDB.menu_down) {\n            UI_Scrollable_ScrollDown(&s->scroll, g_Config.ui.enable_wraparound);\n        } else if (g_InputDB.menu_up) {\n            UI_Scrollable_ScrollUp(&s->scroll, g_Config.ui.enable_wraparound);\n        }\n    }\n\n    if (s->is_selectable) {\n        if (g_InputDB.menu_back) {\n            return UI_REQUESTER_CANCEL;\n        }\n        if (g_InputDB.menu_confirm) {\n            return s->scroll.sel_item;\n        }\n    }\n    return UI_REQUESTER_NO_CHOICE;\n}\n\nvoid UI_Requester_SetMaxRows(UI_REQUESTER_STATE *const s, const size_t max_rows)\n{\n    UI_Scrollable_SetMaxItems(&s->scroll, max_rows);\n}\n\nvoid UI_Requester_SetVisibleRows(\n    UI_REQUESTER_STATE *const s, const size_t visible_rows)\n{\n    UI_Scrollable_SetVisibleItems(&s->scroll, visible_rows);\n}\n\nint32_t UI_Requester_GetFirstRow(const UI_REQUESTER_STATE *const s)\n{\n    return UI_Scrollable_GetFirstVisibleItem(&s->scroll);\n}\n\nint32_t UI_Requester_GetLastRow(const UI_REQUESTER_STATE *const s)\n{\n    return UI_Scrollable_GetLastVisibleItem(&s->scroll) + 1;\n}\n\nint32_t UI_Requester_GetCurrentRow(const UI_REQUESTER_STATE *s)\n{\n    return UI_Scrollable_GetSelectedItem(&s->scroll);\n}\n\nbool UI_Requester_IsRowVisible(\n    const UI_REQUESTER_STATE *const s, const int32_t i)\n{\n    return UI_Scrollable_IsItemVisible(&s->scroll, i);\n}\n\nbool UI_Requester_IsRowSelected(\n    const UI_REQUESTER_STATE *const s, const int32_t i)\n{\n    return UI_Scrollable_IsItemSelected(&s->scroll, i);\n}\n\nvoid UI_Requester_SelectRow(UI_REQUESTER_STATE *const s, const int32_t i)\n{\n    UI_Scrollable_SelectItem(&s->scroll, i);\n}\n\nvoid UI_BeginRequester(\n    const UI_REQUESTER_STATE *const s, const char *const title)\n{\n    const bool show_scroll_hints =\n        s->show_arrows && s->scroll.vis_items < s->scroll.max_items;\n    UI_BeginWindow((UI_WINDOW_SETTINGS) {\n        .title = title,\n        .scrollable = show_scroll_hints ? &s->scroll : nullptr,\n        .title_spacing = -1.0f,\n    });\n    if (s->reserve_space) {\n        UI_BeginResize(\n            -1.0f,\n            s->scroll.vis_items * UI_TEXT_HEIGHT\n                + (s->scroll.vis_items - 1) * s->row_spacing);\n    }\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = s->row_spacing },\n    });\n}\n\nvoid UI_EndRequester(const UI_REQUESTER_STATE *const s)\n{\n    UI_EndStack();\n\n    if (s->reserve_space) {\n        UI_EndResize();\n    }\n    UI_EndWindow();\n}\n\nvoid UI_BeginRequesterRow(const UI_REQUESTER_STATE *const s, const int32_t i)\n{\n    UI_BeginPad(0.0f, g_TRVersion == 1 ? -1.0f : 0.0f);\n    if (UI_Requester_IsRowSelected(s, i)) {\n        UI_BeginFrame(UI_FRAME_SELECTED_OPTION);\n    }\n    UI_BeginPad(s->row_pad, g_TRVersion == 1 ? 1.0f : 0.0f);\n}\n\nvoid UI_EndRequesterRow(const UI_REQUESTER_STATE *const s, const int32_t i)\n{\n    UI_EndPad();\n    if (UI_Requester_IsRowSelected(s, i)) {\n        UI_EndFrame();\n    }\n    UI_EndPad();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/requester.h",
    "content": "#pragma once\n\n// A window to select a single option from a list of predefined choices.\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/scrollable.h>\n\n#include <stdint.h>\n\n#define UI_REQUESTER_CANCEL -2\n#define UI_REQUESTER_NO_CHOICE -1\n\ntypedef struct {\n    bool is_selectable;\n    UI_SCROLLABLE scroll;\n    float row_pad;\n    float row_spacing;\n    bool show_arrows;\n    bool reserve_space;\n} UI_REQUESTER_STATE;\n\n// state functions\nvoid UI_Requester_Init(\n    UI_REQUESTER_STATE *s, int32_t vis_rows, int32_t max_rows,\n    bool is_selectable);\nvoid UI_Requester_Free(UI_REQUESTER_STATE *s);\nint32_t UI_Requester_Control(UI_REQUESTER_STATE *s);\nvoid UI_Requester_SetMaxRows(UI_REQUESTER_STATE *s, size_t max_rows);\nvoid UI_Requester_SetVisibleRows(UI_REQUESTER_STATE *s, size_t visible_rows);\nvoid UI_Requester_SelectRow(UI_REQUESTER_STATE *s, int32_t i);\nint32_t UI_Requester_GetFirstRow(const UI_REQUESTER_STATE *s);\nint32_t UI_Requester_GetLastRow(const UI_REQUESTER_STATE *s);\nint32_t UI_Requester_GetCurrentRow(const UI_REQUESTER_STATE *s);\nbool UI_Requester_IsRowVisible(const UI_REQUESTER_STATE *s, int32_t i);\nbool UI_Requester_IsRowSelected(const UI_REQUESTER_STATE *s, int32_t i);\n\n// draw functions\nvoid UI_BeginRequester(const UI_REQUESTER_STATE *s, const char *title);\nvoid UI_EndRequester(const UI_REQUESTER_STATE *s);\n\nvoid UI_BeginRequesterRow(const UI_REQUESTER_STATE *s, int32_t i);\nvoid UI_EndRequesterRow(const UI_REQUESTER_STATE *s, int32_t i);\n"
  },
  {
    "path": "src/trx/game/ui/elements/resize.c",
    "content": "#include <trx/game/ui/elements/resize.h>\n\n#include <trx/config.h>\n#include <trx/game/ui/helpers.h>\n\ntypedef struct {\n    float x;\n    float y;\n} M_DATA;\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    UI_MeasureWrapper(node);\n    const M_DATA *const data = node->data;\n    if (data->x >= 0.0f) {\n        node->measure_w = data->x;\n    }\n    if (data->y >= 0.0f) {\n        node->measure_h = data->y;\n    }\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    if (node->measure_w <= 0.0f || node->measure_h <= 0.0f) {\n        return;\n    }\n    UI_DrawWrapper(node);\n}\n\nvoid UI_BeginResize(const float x, const float y)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutWrapper,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    data->x = x * g_Config.ui.text_scale;\n    data->y = y * g_Config.ui.text_scale;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndResize(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/resize.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// Resize child widget to a specified size in pixels.\n// A negative size means to use the child's size.\n// A zero value means to hide the child, but participate in the layout pass.\n\nvoid UI_BeginResize(float x, float y);\nvoid UI_EndResize(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/row_arrows.c",
    "content": "#include <trx/game/ui/elements/row_arrows.h>\n\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/helpers.h>\n\nvoid UI_BeginRowArrows(\n    const bool left_arrow, const bool right_arrow, const int32_t spacing)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = UI_LayoutWrapper,\n            .draw = UI_DrawWrapper,\n        },\n        sizeof(bool));\n    *(bool *)node->data = right_arrow;\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = {\n            .h = UI_STACK_H_ALIGN_DISTRIBUTE,\n            .v = UI_STACK_V_ALIGN_CENTER,\n        },\n        .spacing = { .h = spacing },\n    });\n    UI_BeginHide(!left_arrow);\n    UI_Label(\"\\\\{button left}\");\n    UI_EndHide();\n\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndRowArrows(void)\n{\n    const UI_NODE *const node = UI_GetCurrent();\n    const bool right_arrow = *(bool *)(intptr_t)node->data;\n    UI_PopCurrent();\n    UI_BeginHide(!right_arrow);\n    UI_Label(\"\\\\{button right}\");\n    UI_EndHide();\n    UI_EndStack();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/row_arrows.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n#define UI_ROW_ARROWS_TIGHT 2.0f\n#define UI_ROW_ARROWS_MEDIUM 5.0f\n#define UI_ROW_ARROWS_WIDE 15.0f\n\n// Conditionally display a left arrow and a right arrow around the child widget\n// with the given spacing.\n\nvoid UI_BeginRowArrows(bool left_arrow, bool right_arrow, int32_t spacing);\nvoid UI_EndRowArrows(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/scrollable_stack.c",
    "content": "#include <trx/game/ui/elements/scrollable_stack.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/input.h>\n#include <trx/game/ui/helpers.h>\n\ntypedef struct {\n    UI_SCROLLABLE *scroll;\n    UI_SCROLLABLE_STACK_SETTINGS settings;\n} M_DATA;\n\nstatic int32_t M_CountChildren(const UI_NODE *const node)\n{\n    int32_t count = 0;\n    const UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        count++;\n        child = child->next_sibling;\n    }\n    return count;\n}\n\nstatic void M_ClampScroll(UI_SCROLLABLE *const s)\n{\n    CLAMPG(s->first_item, s->max_items - s->vis_items);\n    CLAMPL(s->first_item, 0);\n}\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    M_DATA *const data = node->data;\n    UI_SCROLLABLE *const s = data->scroll;\n    s->max_items = M_CountChildren(node);\n    CLAMPL(s->vis_items, 0);\n    M_ClampScroll(s);\n\n    node->measure_w = 0.0f;\n    node->measure_h = 0.0f;\n\n    const int32_t first = s->first_item;\n    const int32_t last = MIN(first + s->vis_items, s->max_items);\n    const float scale = g_Config.ui.text_scale;\n\n    int32_t visible_count = 0;\n    int32_t i = 0;\n    const UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        if (i >= first && i < last) {\n            if (data->settings.orientation == UI_STACK_VERTICAL) {\n                node->measure_w = MAX(node->measure_w, child->measure_w);\n                node->measure_h += child->measure_h;\n            } else {\n                node->measure_h = MAX(node->measure_h, child->measure_h);\n                node->measure_w += child->measure_w;\n            }\n            visible_count++;\n        }\n        i++;\n        child = child->next_sibling;\n    }\n\n    const int32_t gaps = (visible_count > 1) ? (visible_count - 1) : 0;\n    if (data->settings.orientation == UI_STACK_VERTICAL) {\n        node->measure_h += (float)gaps * data->settings.spacing * scale;\n    } else {\n        node->measure_w += (float)gaps * data->settings.spacing * scale;\n    }\n}\n\nstatic void M_Layout(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n\n    M_DATA *const data = node->data;\n    const UI_SCROLLABLE *const s = data->scroll;\n    const int32_t first = s->first_item;\n    const int32_t last = MIN(first + s->vis_items, s->max_items);\n    const float scale = g_Config.ui.text_scale;\n\n    float cx = x;\n    float cy = y;\n\n    int32_t i = 0;\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        if (i >= first && i < last) {\n            if (data->settings.orientation == UI_STACK_VERTICAL) {\n                child->ops.layout(child, x, cy, w, child->measure_h);\n                cy += child->measure_h;\n                if (i + 1 < last) {\n                    cy += data->settings.spacing * scale;\n                }\n            } else {\n                child->ops.layout(child, cx, y, child->measure_w, h);\n                cx += child->measure_w;\n                if (i + 1 < last) {\n                    cx += data->settings.spacing * scale;\n                }\n            }\n        }\n        i++;\n        child = child->next_sibling;\n    }\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    const M_DATA *const data = node->data;\n    const UI_SCROLLABLE *const s = data->scroll;\n    const int32_t first = s->first_item;\n    const int32_t last = MIN(first + s->vis_items, s->max_items);\n\n    int32_t i = 0;\n    const UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        if (i >= first && i < last && child->ops.draw != nullptr) {\n            child->ops.draw(child);\n        }\n        i++;\n        child = child->next_sibling;\n    }\n}\n\nbool UI_ScrollableStack_Control(\n    UI_SCROLLABLE *const s, const UI_STACK_ORIENTATION orientation)\n{\n    if (orientation == UI_STACK_VERTICAL) {\n        if (g_InputDB.menu_down) {\n            return UI_Scrollable_ScrollDown(s, g_Config.ui.enable_wraparound);\n        } else if (g_InputDB.menu_up) {\n            return UI_Scrollable_ScrollUp(s, g_Config.ui.enable_wraparound);\n        }\n    } else {\n        if (g_InputDB.menu_right || g_InputDB.menu_tab_right) {\n            return UI_Scrollable_ScrollDown(s, g_Config.ui.enable_wraparound);\n        } else if (g_InputDB.menu_left || g_InputDB.menu_tab_left) {\n            return UI_Scrollable_ScrollUp(s, g_Config.ui.enable_wraparound);\n        }\n    }\n    return false;\n}\n\nvoid UI_BeginScrollableStack(\n    UI_SCROLLABLE *const s, const UI_SCROLLABLE_STACK_SETTINGS settings)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = M_Layout,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA));\n    if (node == nullptr) {\n        return;\n    }\n    M_DATA *const data = node->data;\n    data->scroll = s;\n    data->settings = settings;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndScrollableStack(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/scrollable_stack.h",
    "content": "#pragma once\n\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/scrollable.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    UI_STACK_ORIENTATION orientation;\n    float spacing;\n} UI_SCROLLABLE_STACK_SETTINGS;\n\nbool UI_ScrollableStack_Control(\n    UI_SCROLLABLE *s, UI_STACK_ORIENTATION orientation);\n\nvoid UI_BeginScrollableStack(\n    UI_SCROLLABLE *s, UI_SCROLLABLE_STACK_SETTINGS settings);\nvoid UI_EndScrollableStack(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/sleek_bar.c",
    "content": "#include <trx/game/ui/elements/sleek_bar.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/version.h>\n\ntypedef struct {\n    float progress;\n} M_DATA;\n\ntypedef struct {\n    float x, y, w, h;\n    RGBA_8888 color;\n} M_RECT;\n\nstatic RGBA_8888 m_BackgroundColor = { 0x06, 0x06, 0x06, 0xFF };\nstatic RGBA_8888 m_FillColors[TR_VERSION_COUNT] = {\n    { 0xA1, 0x83, 0x3C, 0xFF },\n    { 0x5A, 0xB5, 0x5A, 0xFF },\n    { 0x1C, 0x6A, 0xC4, 0xFF },\n};\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    node->measure_w = 0.0f;\n    node->measure_h = 4.0f * g_Config.ui.text_scale;\n}\n\nstatic void M_Draw(const UI_NODE *const node)\n{\n    const M_DATA *const data = node->data;\n    const float border = 1.0f;\n\n    const M_RECT out = {\n        .x = UI_ScaleX(node->x),\n        .y = UI_ScaleY(node->y),\n        .w = UI_ScaleX(node->w),\n        .h = UI_ScaleY(node->h),\n        .color = m_BackgroundColor,\n    };\n    const M_RECT in = {\n        .x = UI_ScaleX(node->x + border),\n        .y = UI_ScaleY(node->y + border),\n        .w = UI_ScaleX(node->w - border * 2.0f) * data->progress,\n        .h = UI_ScaleY(node->h - border * 2.0f),\n        .color = m_FillColors[g_TRVersion - 1],\n    };\n\n    UI_ScheduleDrawScreenFlatQuad(out.x, out.y, 0, out.w, out.h, out.color);\n    UI_ScheduleDrawScreenFlatQuad(in.x, in.y, 0, in.w, in.h, in.color);\n}\n\nvoid UI_SleekBar(float progress)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = M_Draw,\n        },\n        sizeof(M_DATA));\n    M_DATA *const data = node->data;\n    CLAMP(progress, 0.0f, 1.0f);\n    data->progress = progress;\n    UI_AddChild(node);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/sleek_bar.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\nvoid UI_SleekBar(float progress);\n"
  },
  {
    "path": "src/trx/game/ui/elements/spacer.c",
    "content": "#include <trx/game/ui/elements/spacer.h>\n\n#include <trx/config.h>\n#include <trx/game/ui/helpers.h>\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    // already done in the constructor\n}\n\nvoid UI_Spacer(const float w, const float h)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = UI_LayoutBasic,\n            .draw = UI_DrawWrapper,\n        },\n        0);\n    node->measure_w = w * g_Config.ui.text_scale;\n    node->measure_h = h * g_Config.ui.text_scale;\n    UI_AddChild(node);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/spacer.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// An invisible widget that occupies certain space in pixels.\n\nvoid UI_Spacer(float w, float h);\n"
  },
  {
    "path": "src/trx/game/ui/elements/span.c",
    "content": "#include <trx/game/ui/elements/span.h>\n\n#include <trx/core/utils.h>\n#include <trx/game/ui/helpers.h>\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    node->measure_w = 0.0f;\n    node->measure_h = 0.0f;\n    const UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        node->measure_w = MAX(node->measure_w, child->measure_w);\n        node->measure_h = MAX(node->measure_h, child->measure_h);\n        child = child->next_sibling;\n    }\n}\n\nstatic void M_Layout(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        child->ops.layout(child, x, y, node->measure_w, node->measure_h);\n        child = child->next_sibling;\n    }\n}\n\nvoid UI_BeginSpan(void)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = M_Layout,\n            .draw = UI_DrawWrapper,\n        },\n        0);\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n}\n\nvoid UI_EndSpan(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/span.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// Expands all children to match the biggest child size.\n// Renders the children one on top of each other.\n\nvoid UI_BeginSpan(void);\nvoid UI_EndSpan(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/stack.c",
    "content": "#include <trx/game/ui/elements/stack.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/ui/helpers.h>\n\n#include <math.h>\n#include <stdint.h>\n\ntypedef struct {\n    UI_STACK_SETTINGS settings;\n} M_DATA;\n\nstatic float M_CalcChildW(const UI_NODE *const node, const UI_NODE *const child)\n{\n    M_DATA *const data = node->data;\n    if (data->settings.align.h == UI_STACK_H_ALIGN_SPAN) {\n        return MAX(child->measure_w, node->w);\n    }\n    return child->measure_w;\n}\n\nstatic float M_CalcChildH(const UI_NODE *const node, const UI_NODE *const child)\n{\n    M_DATA *const data = node->data;\n    if (data->settings.align.v == UI_STACK_V_ALIGN_SPAN) {\n        return MAX(child->measure_h, node->h);\n    }\n    return child->measure_h;\n}\n\nstatic float M_CalcStartX(const UI_NODE *const node, const UI_NODE *const child)\n{\n    M_DATA *const data = node->data;\n    switch (data->settings.align.h) {\n    case UI_STACK_H_ALIGN_SPAN:\n    case UI_STACK_H_ALIGN_LEFT:\n        return node->x;\n    case UI_STACK_H_ALIGN_CENTER:\n        return node->x + (node->w - child->measure_w) * 0.5f;\n    case UI_STACK_H_ALIGN_RIGHT:\n        return node->x + node->w - child->measure_w;\n    case UI_STACK_H_ALIGN_DISTRIBUTE:\n        ASSERT_FAIL();\n    }\n    return 0.0f;\n}\n\nstatic float M_CalcStartY(const UI_NODE *const node, const UI_NODE *const child)\n{\n    M_DATA *const data = node->data;\n    switch (data->settings.align.v) {\n    case UI_STACK_V_ALIGN_SPAN:\n    case UI_STACK_V_ALIGN_TOP:\n        return node->y;\n    case UI_STACK_V_ALIGN_CENTER:\n        return node->y + (node->h - child->measure_h) * 0.5f;\n    case UI_STACK_V_ALIGN_BOTTOM:\n        return node->y + node->h - child->measure_h;\n    case UI_STACK_V_ALIGN_DISTRIBUTE:\n        ASSERT_FAIL();\n    }\n    return 0.0f;\n}\n\nstatic void M_Measure(UI_NODE *const node)\n{\n    node->measure_w = 0.0f;\n    node->measure_h = 0.0f;\n    UI_NODE *child = node->first_child;\n    M_DATA *const data = node->data;\n    const float scale = g_Config.ui.text_scale;\n    while (child != nullptr) {\n        if (data->settings.orientation == UI_STACK_VERTICAL) {\n            node->measure_w = MAX(node->measure_w, child->measure_w);\n            node->measure_h += child->measure_h;\n            if (child->next_sibling != nullptr) {\n                node->measure_h += data->settings.spacing.v * scale;\n            }\n        } else {\n            node->measure_h = MAX(node->measure_h, child->measure_h);\n            node->measure_w += child->measure_w;\n            if (child->next_sibling != nullptr) {\n                node->measure_w += data->settings.spacing.h * scale;\n            }\n        }\n        child = child->next_sibling;\n    }\n}\n\nstatic void M_Layout(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n    M_DATA *const data = node->data;\n\n    // Count children and compute the total size they occupy on the main axis\n    // including the base spacing from the settings.\n    int32_t child_count = 0;\n    float total_child_main_size = 0.0f;\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        switch (data->settings.orientation) {\n        case UI_STACK_HORIZONTAL:\n            total_child_main_size += child->measure_w;\n            break;\n        case UI_STACK_VERTICAL:\n            total_child_main_size += child->measure_h;\n            break;\n        }\n        child_count++;\n        child = child->next_sibling;\n    }\n\n    // If there is at least one gap between children, compute the normal\n    // (configured) total spacing on the main axis. If only 1 child or 0\n    // children, there's no gap to distribute leftover space into.\n    const int32_t gaps = (child_count > 1) ? (child_count - 1) : 0;\n    const float scale = g_Config.ui.text_scale;\n    float base_spacing = 0.0f;\n    switch (data->settings.orientation) {\n    case UI_STACK_HORIZONTAL:\n        base_spacing = data->settings.spacing.h * gaps * scale;\n        break;\n    case UI_STACK_VERTICAL:\n        base_spacing = data->settings.spacing.v * gaps * scale;\n        break;\n    }\n\n    // The space that the children + base spacing absolutely need\n    const float needed_size = total_child_main_size + base_spacing;\n\n    // The leftover that we can distribute among the (child_count - 1) internal\n    // gaps.\n    float leftover = 0.0f;\n    float extra_per_gap = 0.0f;\n    switch (data->settings.orientation) {\n    case UI_STACK_HORIZONTAL:\n        leftover = w - needed_size;\n        break;\n    case UI_STACK_VERTICAL:\n        leftover = h - needed_size;\n        break;\n    }\n\n    if ((data->settings.orientation == UI_STACK_HORIZONTAL\n         && data->settings.align.h == UI_STACK_H_ALIGN_DISTRIBUTE)\n        || (data->settings.orientation == UI_STACK_VERTICAL\n            && data->settings.align.v == UI_STACK_V_ALIGN_DISTRIBUTE)) {\n        if (gaps > 0 && leftover > 0.0f) {\n            extra_per_gap = leftover / (float)gaps;\n        }\n    }\n\n    // Now we actually lay out the children\n    float cx = x;\n    float cy = y;\n    child = node->first_child;\n    while (child != nullptr) {\n        const float cw = M_CalcChildW(node, child);\n        const float ch = M_CalcChildH(node, child);\n\n        switch (data->settings.orientation) {\n        case UI_STACK_HORIZONTAL:\n            // For horizontal: vertical alignment is determined by M_CalcStartY\n            cy = M_CalcStartY(node, child);\n\n            // Lay out the child\n            child->ops.layout(child, cx, cy, cw, ch);\n\n            // Advance cx for the next child\n            cx += cw;\n            // Add normal spacing + any extra leftover that we are distributing\n            if (child->next_sibling != nullptr) {\n                cx += data->settings.spacing.h * scale + extra_per_gap;\n            }\n            break;\n\n        case UI_STACK_VERTICAL:\n            cx = M_CalcStartX(node, child);\n\n            child->ops.layout(child, cx, cy, cw, ch);\n\n            cy += ch;\n            if (child->next_sibling != nullptr) {\n                cy += data->settings.spacing.v * scale + extra_per_gap;\n            }\n            break;\n        }\n\n        child = child->next_sibling;\n    }\n}\n\nUI_NODE *UI_CreateStack(const UI_STACK_SETTINGS settings)\n{\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = M_Measure,\n            .layout = M_Layout,\n            .draw = UI_DrawWrapper,\n        },\n        sizeof(M_DATA));\n    if (node == nullptr) {\n        return nullptr;\n    }\n    M_DATA *const data = node->data;\n    data->settings = settings;\n    return node;\n}\n\nvoid UI_BeginStack(const UI_STACK_ORIENTATION orientation)\n{\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = orientation,\n        .align = {\n            .h = UI_STACK_H_ALIGN_LEFT,\n            .v = UI_STACK_V_ALIGN_TOP,\n        },\n        .spacing = {\n            .h = 0.0f,\n            .v = 0.0f,\n        },\n    });\n}\n\nvoid UI_BeginStackEx(const UI_STACK_SETTINGS settings)\n{\n    UI_NODE *const child = UI_CreateStack(settings);\n    UI_AddChild(child);\n    UI_PushCurrent(child);\n}\n\nvoid UI_EndStack(void)\n{\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/stack.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n#include <stdint.h>\n\n// Stack several widgets vertically or horizontally.\n\ntypedef enum {\n    UI_STACK_VERTICAL,\n    UI_STACK_HORIZONTAL,\n} UI_STACK_ORIENTATION;\n\ntypedef enum {\n    UI_STACK_H_ALIGN_LEFT,\n    UI_STACK_H_ALIGN_CENTER,\n    UI_STACK_H_ALIGN_RIGHT,\n    UI_STACK_H_ALIGN_SPAN,\n    UI_STACK_H_ALIGN_DISTRIBUTE,\n} UI_STACK_H_ALIGN;\n\ntypedef enum {\n    UI_STACK_V_ALIGN_TOP,\n    UI_STACK_V_ALIGN_CENTER,\n    UI_STACK_V_ALIGN_BOTTOM,\n    UI_STACK_V_ALIGN_SPAN,\n    UI_STACK_V_ALIGN_DISTRIBUTE,\n} UI_STACK_V_ALIGN;\n\ntypedef struct {\n    UI_STACK_ORIENTATION orientation;\n    struct {\n        UI_STACK_H_ALIGN h;\n        UI_STACK_V_ALIGN v;\n    } align;\n    struct {\n        float h;\n        float v;\n    } spacing;\n} UI_STACK_SETTINGS;\n\nvoid UI_BeginStack(UI_STACK_ORIENTATION orientation);\nvoid UI_BeginStackEx(UI_STACK_SETTINGS settings);\nvoid UI_EndStack(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements/tab_switch.c",
    "content": "#include <trx/game/ui/elements/tab_switch.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/game/input/common.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/frame.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/row_arrows.h>\n#include <trx/game/ui/elements/stack.h>\n\nstatic void M_Draw(\n    const UI_TAB_SWITCH_STATE *const s, const bool is_focused,\n    const bool single)\n{\n    UI_BeginAnchor(0.5f, 0.5f);\n    UI_BeginRowArrows(\n        is_focused && s->tab_count > 0\n            && (g_Config.ui.enable_wraparound || s->active_tab_idx > 0),\n        is_focused && s->tab_count > 0\n            && (g_Config.ui.enable_wraparound\n                || s->active_tab_idx + 1 < s->tab_count),\n        UI_ROW_ARROWS_MEDIUM);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_HORIZONTAL,\n        .align = { .h = UI_STACK_H_ALIGN_CENTER },\n        .spacing = { .h = 10.0f },\n    });\n    for (int32_t i = 0; i < s->tab_count; i++) {\n        if (single && i != s->active_tab_idx) {\n            continue;\n        }\n        const UI_TAB_SWITCH_TAB *const tab = &s->tabs[i];\n        UI_BeginAnchor(0.5f, 0.5f);\n        if (i == s->active_tab_idx) {\n            UI_BeginFrame(\n                is_focused ? UI_FRAME_SELECTED_OPTION : UI_FRAME_OUTLINE_ONLY);\n        }\n        UI_BeginPad(2.0f, 1.0f);\n        UI_Label(\n            tab->header.live_ptr != nullptr ? *tab->header.live_ptr\n                                            : tab->header.one_off);\n        UI_EndPad();\n        if (i == s->active_tab_idx) {\n            UI_EndFrame();\n        }\n        UI_EndAnchor();\n    }\n    UI_EndStack();\n    UI_EndRowArrows();\n    UI_EndAnchor();\n}\n\nUI_TAB_SWITCH_STATE *UI_TabSwitch_Init(\n    const int32_t tab_count, const UI_TAB_SWITCH_TAB *const tabs)\n{\n    UI_TAB_SWITCH_STATE *const s = Memory_Alloc(sizeof(UI_TAB_SWITCH_STATE));\n    s->tab_count = tab_count;\n    s->tabs = Memory_Dup(tabs, sizeof(UI_TAB_SWITCH_TAB) * tab_count);\n    s->active_tab_idx = 0;\n    return s;\n}\n\nvoid UI_TabSwitch_Free(UI_TAB_SWITCH_STATE *const s)\n{\n    Memory_Free(s->tabs);\n    Memory_Free(s);\n}\n\nbool UI_TabSwitch_Cycle(UI_TAB_SWITCH_STATE *const s, const int32_t dir)\n{\n    if (s->tab_count == 0) {\n        return false;\n    } else if (s->active_tab_idx + dir < 0) {\n        if (g_Config.ui.enable_wraparound) {\n            s->active_tab_idx = s->tab_count - 1;\n            return true;\n        }\n    } else if (s->active_tab_idx + dir >= s->tab_count) {\n        if (g_Config.ui.enable_wraparound) {\n            s->active_tab_idx = 0;\n            return true;\n        }\n    } else {\n        s->active_tab_idx += dir;\n        return true;\n    }\n    return false;\n}\n\nbool UI_TabSwitch_Control(\n    UI_TAB_SWITCH_STATE *const s, const UI_TAB_SWITCH_FLAGS flags)\n{\n    if ((!(flags & UI_TAB_SWITCH_NO_ARROWS) && g_InputDB.menu_left)\n        || g_InputDB.menu_tab_left) {\n        return UI_TabSwitch_Cycle(s, -1);\n    } else if (\n        (!(flags & UI_TAB_SWITCH_NO_ARROWS) && g_InputDB.menu_right)\n        || g_InputDB.menu_tab_right) {\n        return UI_TabSwitch_Cycle(s, 1);\n    }\n    return false;\n}\n\nvoid UI_TabSwitch(const UI_TAB_SWITCH_STATE *const s, const bool is_focused)\n{\n    M_Draw(s, is_focused, false);\n}\n\nvoid UI_TabSwitchSingle(\n    const UI_TAB_SWITCH_STATE *const s, const bool is_focused)\n{\n    M_Draw(s, is_focused, true);\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/tab_switch.h",
    "content": "#pragma once\n\n// A tab switch UI element for navigating between multiple tabs via left/right\n// input.\n\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/scrollable.h>\n\n// Represents a single tab page for use with UI_TabSwitch_Control.\ntypedef struct {\n    struct {\n        const char *one_off;\n        const char *const *live_ptr;\n    } header;\n} UI_TAB_SWITCH_TAB;\n\ntypedef struct {\n    UI_TAB_SWITCH_TAB *tabs;\n    int32_t tab_count;\n    int32_t active_tab_idx;\n} UI_TAB_SWITCH_STATE;\n\ntypedef enum {\n    UI_TAB_SWITCH_NORMAL,\n    UI_TAB_SWITCH_NO_ARROWS,\n} UI_TAB_SWITCH_FLAGS;\n\n// state functions\nUI_TAB_SWITCH_STATE *UI_TabSwitch_Init(\n    int32_t tab_count, const UI_TAB_SWITCH_TAB *tabs);\nvoid UI_TabSwitch_Free(UI_TAB_SWITCH_STATE *s);\n\n// Handles left/right input for switching tabs. Returns true if the active tab\n// changed.\nbool UI_TabSwitch_Control(\n    UI_TAB_SWITCH_STATE *state, UI_TAB_SWITCH_FLAGS flags);\n\n// Advances the active tab by dir (-1 for previous, +1 for next), wrapping\n// around.\nbool UI_TabSwitch_Cycle(UI_TAB_SWITCH_STATE *state, int32_t dir);\n\n// draw functions\nvoid UI_TabSwitch(const UI_TAB_SWITCH_STATE *state, bool is_focused);\nvoid UI_TabSwitchSingle(const UI_TAB_SWITCH_STATE *state, bool is_focused);\n"
  },
  {
    "path": "src/trx/game/ui/elements/window.c",
    "content": "#include <trx/game/ui/elements/window.h>\n\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/ui.h>\n#include <trx/version.h>\n\ntypedef struct {\n    UI_WINDOW_SETTINGS settings;\n    bool show_scroll_hints;\n} M_CONTEXT;\n\nstatic M_CONTEXT m_ContextStack[16];\nstatic size_t m_ContextStackSize = 0;\n\nstatic bool M_ShouldShowScrollHints(const UI_WINDOW_SETTINGS *const settings)\n{\n    if (settings->scrollable == nullptr) {\n        return false;\n    }\n    return settings->reserve_scroll_space\n        || settings->scrollable->vis_items < settings->scrollable->max_items;\n}\n\nstatic float M_GetOuterPad(void)\n{\n    return g_TRVersion >= 2 ? 3.0f : 2.0f;\n}\n\nstatic float M_GetBodyPadX(void)\n{\n    return g_TRVersion >= 2 ? 4.0f : 8.0f;\n}\n\nstatic float M_GetBodyPadY(void)\n{\n    return 4.0f;\n}\n\nstatic float M_GetTitleSpacing(const UI_WINDOW_SETTINGS *const settings)\n{\n    if (settings->title_spacing >= 0.0f) {\n        return settings->title_spacing;\n    }\n    return 3.0f;\n}\n\nstatic void M_ScrollHintRow(\n    const UI_WINDOW_SETTINGS *const settings, const bool show_arrow,\n    const bool up)\n{\n    UI_BeginResize(-1.0f, 7.0f);\n    UI_BeginAnchor(0.5f, up ? 1.5f : -1.0f);\n    if (show_arrow) {\n        UI_LabelEx(\n            up ? \"\\\\{arrow up}\" : \"\\\\{arrow down}\",\n            (UI_LABEL_SETTINGS) { .scale = 0.7f });\n    }\n    UI_EndAnchor();\n    UI_EndResize();\n}\n\nstatic void M_Title(const char *const title)\n{\n    UI_BeginFrame(UI_FRAME_DIALOG_HEADING);\n    UI_BeginPad(10.0f, g_TRVersion >= 2 ? 1.0f : 2.0f);\n    UI_BeginAnchor(0.5f, 0.5f);\n    UI_Label(title);\n    UI_EndAnchor();\n    UI_EndPad();\n    UI_EndFrame();\n}\n\nvoid UI_BeginWindow(UI_WINDOW_SETTINGS settings)\n{\n    const bool show_scroll_hints = M_ShouldShowScrollHints(&settings);\n    ASSERT(m_ContextStackSize < ARRAY_SIZE(m_ContextStack));\n    m_ContextStack[m_ContextStackSize++] = (M_CONTEXT) {\n        .settings = settings,\n        .show_scroll_hints = show_scroll_hints,\n    };\n\n    UI_BeginFrame(\n        settings.heavy ? UI_FRAME_DIALOG_BACKGROUND_HEAVY\n                       : UI_FRAME_DIALOG_BACKGROUND);\n    UI_BeginPad(M_GetOuterPad(), M_GetOuterPad());\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n        .spacing = { .v = M_GetTitleSpacing(&settings) },\n    });\n\n    if (settings.title != nullptr) {\n        M_Title(settings.title);\n    }\n\n    UI_BeginPadEx(\n        M_GetBodyPadX(), M_GetBodyPadX(), M_GetBodyPadY(), M_GetBodyPadY());\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_SPAN },\n    });\n\n    if (settings.header_func != nullptr) {\n        settings.header_func(settings.user_data);\n    }\n\n    if (show_scroll_hints) {\n        M_ScrollHintRow(&settings, settings.scrollable->first_item != 0, true);\n    }\n}\n\nvoid UI_EndWindow(void)\n{\n    ASSERT(m_ContextStackSize > 0);\n    m_ContextStackSize--;\n    const M_CONTEXT *const ctx = &m_ContextStack[m_ContextStackSize];\n    const UI_WINDOW_SETTINGS *const settings = &ctx->settings;\n\n    if (ctx->show_scroll_hints) {\n        M_ScrollHintRow(\n            settings,\n            settings->scrollable->first_item + settings->scrollable->vis_items\n                < settings->scrollable->max_items,\n            false);\n    }\n\n    if (settings->footer_func != nullptr) {\n        settings->footer_func(settings->user_data);\n    }\n\n    UI_EndStack();\n    UI_EndPad();\n    UI_EndStack();\n    UI_EndPad();\n    UI_EndFrame();\n}\n"
  },
  {
    "path": "src/trx/game/ui/elements/window.h",
    "content": "#pragma once\n\n#include <trx/game/ui/scrollable.h>\n\ntypedef void UI_WINDOW_CALLBACK(void *user_data);\n\ntypedef struct {\n    const char *title;\n    const UI_SCROLLABLE *scrollable;\n    float title_spacing;\n    bool heavy;\n    UI_WINDOW_CALLBACK *header_func;\n    UI_WINDOW_CALLBACK *footer_func;\n    void *user_data;\n    bool reserve_scroll_space;\n} UI_WINDOW_SETTINGS;\n\nvoid UI_BeginWindow(UI_WINDOW_SETTINGS settings);\nvoid UI_EndWindow(void);\n"
  },
  {
    "path": "src/trx/game/ui/elements.h",
    "content": "#pragma once\n\n#include <trx/game/ui/elements/ammo_label.h>\n#include <trx/game/ui/elements/anchor.h>\n#include <trx/game/ui/elements/bar.h>\n#include <trx/game/ui/elements/bar_enemy_hp.h>\n#include <trx/game/ui/elements/bar_lara_air.h>\n#include <trx/game/ui/elements/bar_lara_exposure.h>\n#include <trx/game/ui/elements/bar_lara_hp.h>\n#include <trx/game/ui/elements/bar_lara_sprint.h>\n#include <trx/game/ui/elements/button_label.h>\n#include <trx/game/ui/elements/color_swatch.h>\n#include <trx/game/ui/elements/flash.h>\n#include <trx/game/ui/elements/fps_counter.h>\n#include <trx/game/ui/elements/frame.h>\n#include <trx/game/ui/elements/gradient_slider.h>\n#include <trx/game/ui/elements/hide.h>\n#include <trx/game/ui/elements/horizontal_line.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/offset.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/prompt.h>\n#include <trx/game/ui/elements/requester.h>\n#include <trx/game/ui/elements/resize.h>\n#include <trx/game/ui/elements/row_arrows.h>\n#include <trx/game/ui/elements/scrollable_stack.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/span.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/elements/window.h>\n"
  },
  {
    "path": "src/trx/game/ui/events.c",
    "content": "#include <trx/game/ui/events.h>\n\n#include <trx/config/common.h>\n#include <trx/debug.h>\n#include <trx/game/ui/common.h>\n\nstatic EVENT_MANAGER *m_EventManager = nullptr;\n\nvoid UI_InitEvents(void)\n{\n    m_EventManager = EventManager_Create();\n}\n\nvoid UI_ShutdownEvents(void)\n{\n    EventManager_Free(m_EventManager);\n    m_EventManager = nullptr;\n}\n\nint32_t UI_Subscribe(\n    const char *const event_name, const void *const sender,\n    const EVENT_LISTENER listener, void *const user_data)\n{\n    ASSERT(m_EventManager != nullptr);\n    return EventManager_Subscribe(\n        m_EventManager, event_name, sender, listener, user_data);\n}\n\nvoid UI_Unsubscribe(const int32_t listener_id)\n{\n    if (m_EventManager != nullptr) {\n        EventManager_Unsubscribe(m_EventManager, listener_id);\n    }\n}\n\nvoid UI_FireEvent(const EVENT event)\n{\n    if (m_EventManager != nullptr) {\n        EventManager_Fire(m_EventManager, &event);\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/events.h",
    "content": "#pragma once\n\n#include <trx/core/event_manager.h>\n\ntypedef void (*EVENT_LISTENER)(const EVENT *, void *user_data);\n\nvoid UI_InitEvents(void);\nvoid UI_ShutdownEvents(void);\n\nint32_t UI_Subscribe(\n    const char *event_name, const void *sender, EVENT_LISTENER listener,\n    void *user_data);\n\nvoid UI_Unsubscribe(int32_t listener_id);\n\nvoid UI_FireEvent(EVENT event);\n"
  },
  {
    "path": "src/trx/game/ui/helpers.c",
    "content": "#include <trx/game/ui/helpers.h>\n\n#include <trx/core/utils.h>\n\nvoid UI_MeasureWrapper(UI_NODE *const node)\n{\n    node->measure_w = 0.0f;\n    node->measure_h = 0.0f;\n    const UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        node->measure_w = MAX(node->measure_w, child->measure_w);\n        node->measure_h = MAX(node->measure_h, child->measure_h);\n        child = child->next_sibling;\n    }\n}\n\nvoid UI_LayoutBasic(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    node->x = x;\n    node->y = y;\n    node->w = w;\n    node->h = h;\n}\n\nvoid UI_LayoutWrapper(\n    UI_NODE *const node, const float x, const float y, const float w,\n    const float h)\n{\n    UI_LayoutBasic(node, x, y, w, h);\n    UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        if (child->ops.layout != nullptr) {\n            child->ops.layout(child, x, y, w, h);\n        }\n        child = child->next_sibling;\n    }\n}\n\nvoid UI_DrawWrapper(const UI_NODE *const node)\n{\n    const UI_NODE *child = node->first_child;\n    while (child != nullptr) {\n        if (child->ops.draw != nullptr) {\n            child->ops.draw(child);\n        }\n        child = child->next_sibling;\n    }\n}\n"
  },
  {
    "path": "src/trx/game/ui/helpers.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n\n// Repetitive widget ops strategies\nvoid UI_MeasureWrapper(UI_NODE *node);\nvoid UI_LayoutBasic(UI_NODE *node, float x, float y, float w, float h);\nvoid UI_LayoutWrapper(UI_NODE *node, float x, float y, float w, float h);\nvoid UI_DrawWrapper(const UI_NODE *node);\n"
  },
  {
    "path": "src/trx/game/ui/hud/console.c",
    "content": "#include <trx/game/ui/hud/console.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/game/console.h>\n#include <trx/game/events.h>\n#include <trx/game/output.h>\n#include <trx/game/ui/elements/modal.h>\n#include <trx/game/ui/elements/pad.h>\n#include <trx/game/ui/elements/prompt.h>\n#include <trx/game/ui/elements/spacer.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/events.h>\n#include <trx/game/ui/helpers.h>\n#include <trx/game/ui/hud/console_logs.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/game/ui/text.h>\n\nstatic void M_MoveHistoryUp(UI_CONSOLE_STATE *const s)\n{\n    s->history_idx--;\n    CLAMP(s->history_idx, 0, Console_History_GetLength());\n    const char *const new_prompt = Console_History_Get(s->history_idx);\n    UI_Prompt_ChangeText(&s->prompt, new_prompt == nullptr ? \"\" : new_prompt);\n}\n\nstatic void M_MoveHistoryDown(UI_CONSOLE_STATE *const s)\n{\n    s->history_idx++;\n    CLAMP(s->history_idx, 0, Console_History_GetLength());\n    const char *const new_prompt = Console_History_Get(s->history_idx);\n    UI_Prompt_ChangeText(&s->prompt, new_prompt == nullptr ? \"\" : new_prompt);\n}\n\nstatic void M_HandleKeyDown(const EVENT *const event, void *const user_data)\n{\n    if (!Console_IsOpened()) {\n        return;\n    }\n\n    UI_CONSOLE_STATE *const s = user_data;\n    const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data;\n\n    // clang-format off\n    switch (key) {\n    case UI_KEY_UP:   M_MoveHistoryUp(s); break;\n    case UI_KEY_DOWN: M_MoveHistoryDown(s); break;\n    default:          break;\n    }\n    // clang-format on\n}\n\nstatic void M_HandleOpen(const EVENT *event, void *user_data)\n{\n    UI_CONSOLE_STATE *const s = user_data;\n    UI_Prompt_SetFocus(&s->prompt, true);\n    s->history_idx = Console_History_GetLength();\n}\n\nstatic void M_HandleClose(const EVENT *event, void *user_data)\n{\n    UI_CONSOLE_STATE *const s = user_data;\n    UI_Prompt_SetFocus(&s->prompt, false);\n    UI_Prompt_Clear(&s->prompt);\n}\n\nstatic void M_HandleCancel(const EVENT *const event, void *const data)\n{\n    Console_Close();\n}\n\nstatic void M_HandleConfirm(const EVENT *event, void *user_data)\n{\n    UI_CONSOLE_STATE *const s = user_data;\n    const char *text = event->data;\n    Console_History_Append(text);\n    Console_Eval(text);\n    GameEvent_Fire((EVENT) {\n        .name = GAME_EVENT_COMMAND,\n        .data = text,\n    });\n    Console_Close();\n    s->history_idx = Console_History_GetLength();\n}\n\nstatic void M_DrawBackdrop(void)\n{\n    const int32_t sx = 0;\n    const int32_t sw = Viewport_GetWidth(VIEWPORT_UI);\n    const int32_t sh = UI_Scaler_Calc(\n        // not entirely accurate, but good enough\n        UI_TEXT_HEIGHT * 1.0 + 7 * UI_TEXT_HEIGHT * 0.8, UI_SCALER_TARGET_TEXT);\n    const int32_t sy = Viewport_GetHeight(VIEWPORT_UI) - sh;\n    const RGBA_8888 top = { 0, 0, 0, 0 };\n    const RGBA_8888 bottom = { 0, 0, 0, 196 };\n    Output_DrawScreenGradientQuad(sx, sy, 0, sw, sh, top, top, bottom, bottom);\n}\n\nstatic void M_Draw(const UI_NODE *node)\n{\n    UI_CONSOLE_STATE *const s = *(UI_CONSOLE_STATE **)node->data;\n    if (Console_IsOpened() || s->logs.vis_lines > 0) {\n        M_DrawBackdrop();\n    }\n    UI_DrawWrapper(node);\n}\n\nvoid UI_Console_Init(UI_CONSOLE_STATE *const s)\n{\n    UI_Prompt_Init(&s->prompt);\n    UI_ConsoleLogs_Init(&s->logs);\n\n    struct {\n        const char *event_name;\n        const void *sender;\n        EVENT_LISTENER handler;\n    } listeners[] = {\n        { \"console_open\", nullptr, M_HandleOpen },\n        { \"console_close\", nullptr, M_HandleClose },\n        { \"cancel\", &s->prompt, M_HandleCancel },\n        { \"confirm\", &s->prompt, M_HandleConfirm },\n        { \"key_down\", nullptr, M_HandleKeyDown },\n        { 0 },\n    };\n    for (int32_t i = 0; listeners[i].event_name != nullptr; i++) {\n        s->listeners[i] = UI_Subscribe(\n            listeners[i].event_name, listeners[i].sender, listeners[i].handler,\n            s);\n    }\n\n    s->history_idx = -1;\n}\n\nvoid UI_Console_Free(UI_CONSOLE_STATE *const s)\n{\n    UI_ConsoleLogs_Free(&s->logs);\n    UI_Prompt_Free(&s->prompt);\n    for (int32_t i = 0; i < 5; i++) {\n        UI_Unsubscribe(s->listeners[i]);\n    }\n}\n\nvoid UI_Console_Control(UI_CONSOLE_STATE *const s)\n{\n    UI_Prompt_Control(&s->prompt);\n}\n\nvoid UI_Console(UI_CONSOLE_STATE *const s)\n{\n    UI_Prompt_SetFocus(&s->prompt, Console_IsOpened());\n\n    UI_NODE *const node = UI_AllocNode(\n        &(UI_WIDGET_OPS) {\n            .measure = UI_MeasureWrapper,\n            .layout = UI_LayoutWrapper,\n            .draw = M_Draw,\n        },\n        sizeof(UI_CONSOLE_STATE *));\n    *(UI_CONSOLE_STATE **)node->data = s;\n    UI_AddChild(node);\n    UI_PushCurrent(node);\n\n    UI_BeginModal(0.0f, 1.0f);\n    UI_BeginPad(5.0f, 5.0f);\n    UI_BeginStack(UI_STACK_VERTICAL);\n\n    UI_ConsoleLogs(&s->logs);\n    UI_Spacer(0.0f, 8.0f);\n    if (Console_IsOpened()) {\n        UI_Prompt(&s->prompt);\n    } else {\n        UI_Spacer(0.0f, UI_TEXT_HEIGHT);\n    }\n\n    UI_EndStack();\n    UI_EndModal();\n    UI_EndPad();\n\n    UI_PopCurrent();\n}\n"
  },
  {
    "path": "src/trx/game/ui/hud/console.h",
    "content": "#pragma once\n\n#include <trx/game/ui/elements/prompt.h>\n#include <trx/game/ui/hud/console_logs.h>\n\n// Dev console display widget.\n\ntypedef struct {\n    UI_CONSOLE_LOGS logs;\n    UI_PROMPT_STATE prompt;\n\n    int32_t listeners[5];\n    int32_t history_idx;\n} UI_CONSOLE_STATE;\n\n// state functions\nvoid UI_Console_Init(UI_CONSOLE_STATE *s);\nvoid UI_Console_Free(UI_CONSOLE_STATE *s);\nvoid UI_Console_Control(UI_CONSOLE_STATE *s);\n\n// draw functions\nvoid UI_Console(UI_CONSOLE_STATE *s);\n"
  },
  {
    "path": "src/trx/game/ui/hud/console_logs.c",
    "content": "#include <trx/game/ui/hud/console_logs.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/debug.h>\n#include <trx/game/clock.h>\n#include <trx/game/ui/elements/label.h>\n#include <trx/game/ui/elements/stack.h>\n#include <trx/game/ui/events.h>\n\n#include <string.h>\n\n#define M_LOG_SCALE 0.8f\n#define M_MAX_LOG_LINES 20\n#define M_DELAY_PER_CHAR 0.2\n\nstatic void M_ScrollLogs(UI_CONSOLE_LOGS *s);\nstatic void M_UpdateLogCount(UI_CONSOLE_LOGS *s);\nstatic void M_HandleLog(const EVENT *event, void *user_data);\nstatic void M_HandleClear(const EVENT *event, void *user_data);\n\nstatic void M_ScrollLogs(UI_CONSOLE_LOGS *const s)\n{\n    int32_t i = s->max_lines - 1;\n    while (i >= 0 && s->logs[i].text == nullptr) {\n        i--;\n    }\n\n    bool need_layout = false;\n    while (i >= 0 && s->logs[i].text != nullptr\n           && Clock_GetRealTime() >= s->logs[i].expire_at) {\n        s->logs[i].expire_at = 0.0;\n        Memory_FreePointer(&s->logs[i].text);\n        need_layout = true;\n        i--;\n    }\n\n    if (need_layout) {\n        M_UpdateLogCount(s);\n    }\n}\n\nstatic void M_UpdateLogCount(UI_CONSOLE_LOGS *const s)\n{\n    s->vis_lines = 0;\n    for (int32_t i = s->max_lines - 1; i >= 0; i--) {\n        if (s->logs[i].expire_at != 0.0) {\n            s->vis_lines = i + 1;\n            break;\n        }\n    }\n}\n\nstatic void M_HandleLog(const EVENT *const event, void *const user_data)\n{\n    const char *text = event->data;\n    UI_CONSOLE_LOGS *const s = user_data;\n    Memory_FreePointer(&s->logs[s->max_lines - 1].text);\n    for (int32_t i = s->max_lines - 1; i > 0; i--) {\n        s->logs[i] = s->logs[i - 1];\n    }\n\n    s->logs[0].expire_at =\n        Clock_GetRealTime() + strlen(text) * M_DELAY_PER_CHAR;\n    s->logs[0].text = UI_Text_WordWrap(text, M_LOG_SCALE, UI_GetCanvasWidth());\n    M_UpdateLogCount(s);\n}\n\nstatic void M_HandleClear(const EVENT *const event, void *const user_data)\n{\n    UI_CONSOLE_LOGS *const s = user_data;\n    for (size_t i = 0; i < s->max_lines; i++) {\n        s->logs[i].expire_at = 0.0;\n    }\n    M_ScrollLogs(s);\n}\n\nvoid UI_ConsoleLogs_Init(UI_CONSOLE_LOGS *const s)\n{\n    if (s->max_lines <= 0) {\n        s->max_lines = M_MAX_LOG_LINES;\n    }\n    s->logs = Memory_Alloc(s->max_lines * sizeof(UI_CONSOLE_LOG_LINE));\n    s->vis_lines = 0;\n    s->listeners[0] = UI_Subscribe(\"console_log\", nullptr, M_HandleLog, s);\n    s->listeners[1] = UI_Subscribe(\"console_clear\", nullptr, M_HandleClear, s);\n}\n\nvoid UI_ConsoleLogs_Free(UI_CONSOLE_LOGS *const s)\n{\n    if (s->logs != nullptr) {\n        for (int32_t i = 0; i < M_MAX_LOG_LINES; i++) {\n            Memory_FreePointer(&s->logs[i].text);\n        }\n        Memory_FreePointer(&s->logs);\n    }\n    UI_Unsubscribe(s->listeners[0]);\n    UI_Unsubscribe(s->listeners[1]);\n}\n\nvoid UI_ConsoleLogs(UI_CONSOLE_LOGS *const s)\n{\n    ASSERT(s != nullptr);\n    M_ScrollLogs(s);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = {\n            .h = UI_STACK_H_ALIGN_LEFT,\n            .v = UI_STACK_V_ALIGN_CENTER,\n        },\n    });\n    for (int32_t i = s->vis_lines - 1; i >= 0; i--) {\n        UI_LabelEx(\n            s->logs[i].text, (UI_LABEL_SETTINGS) { .scale = M_LOG_SCALE });\n    }\n    UI_EndStack();\n}\n"
  },
  {
    "path": "src/trx/game/ui/hud/console_logs.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\n// Scrollback for the dev console.\n\ntypedef struct {\n    char *text;\n    double expire_at;\n} UI_CONSOLE_LOG_LINE;\n\ntypedef struct {\n    size_t max_lines;\n    size_t vis_lines;\n    UI_CONSOLE_LOG_LINE *logs;\n    int32_t listeners[2];\n} UI_CONSOLE_LOGS;\n\n// state functions\nvoid UI_ConsoleLogs_Init(UI_CONSOLE_LOGS *s);\nvoid UI_ConsoleLogs_Free(UI_CONSOLE_LOGS *s);\nvoid UI_ConsoleLogs(UI_CONSOLE_LOGS *s);\n"
  },
  {
    "path": "src/trx/game/ui/hud/overlay.c",
    "content": "#include <trx/game/ui/hud/overlay.h>\n\n#include <trx/config.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/camera.h>\n#include <trx/game/const.h>\n#include <trx/game/game.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/lara.h>\n#include <trx/game/objects/names.h>\n#include <trx/game/ui.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/version.h>\n\ntypedef struct UI_OVERLAY_STATE {\n    struct {\n        bool state;\n        int32_t frame;\n    } blink;\n    UI_FPS_COUNTER_STATE *fps;\n    bool force_show_healthbar;\n    bool show_arrows[6];\n    bool show_version;\n    UI_OVERLAY_TEXT top_text;\n    UI_OVERLAY_TEXT bottom_text;\n    UI_FLASH_STATE flash_state;\n} UI_OVERLAY_STATE;\n\nstatic struct {\n    bool resize;\n    const char *label;\n} m_ArrowInfo[] = {\n    [UI_OVERLAY_ARROW_TR] = { true, \"\\\\{arrow up}\" },\n    [UI_OVERLAY_ARROW_TL] = { true, \"\\\\{arrow up}\" },\n    [UI_OVERLAY_ARROW_BL] = { true, \"\\\\{arrow down}\" },\n    [UI_OVERLAY_ARROW_BR] = { true, \"\\\\{arrow down}\" },\n    [UI_OVERLAY_ARROW_BCL] = { false, \"\\\\{button left}\" },\n    [UI_OVERLAY_ARROW_BCR] = { false, \"\\\\{button right}\" },\n};\n\nstatic bool M_LaraHealthBar(\n    const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location)\n{\n    if (location != g_Config.ui.lara_health_bar.location) {\n        return false;\n    }\n    if (!Lara_IsControllable()\n        || (!Game_IsPlaying() && !s->force_show_healthbar)) {\n        return false;\n    }\n    if (!g_Config.ui.enable_game_ui) {\n        return false;\n    }\n    return UI_LaraHealthBar(s->blink.state, s->force_show_healthbar);\n}\n\nstatic bool M_LaraAirBar(\n    const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location)\n{\n    if (location != g_Config.ui.lara_air_bar.location) {\n        return false;\n    }\n    if (!Lara_IsControllable() || !Game_IsPlaying()) {\n        return false;\n    }\n    if (!g_Config.ui.enable_game_ui) {\n        return false;\n    }\n    return UI_LaraAirBar(s->blink.state);\n}\n\nstatic bool M_LaraSprintBar(\n    const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location)\n{\n    if (location != g_Config.ui.lara_sprint_bar.location) {\n        return false;\n    }\n    if (!Lara_IsControllable() || !Game_IsPlaying()) {\n        return false;\n    }\n    if (!g_Config.ui.enable_game_ui) {\n        return false;\n    }\n    return UI_LaraSprintBar();\n}\n\nstatic bool M_LaraExposureBar(\n    const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location)\n{\n    if (location != g_Config.ui.lara_exposure_bar.location) {\n        return false;\n    }\n    if (!Lara_IsControllable() || !Game_IsPlaying()) {\n        return false;\n    }\n    if (!g_Config.ui.enable_game_ui) {\n        return false;\n    }\n    return UI_LaraExposureBar(s->blink.state);\n}\n\nstatic const char *M_ResolveOverlayTextRaw(const UI_OVERLAY_TEXT *const t)\n{\n    if (t == nullptr) {\n        return nullptr;\n    }\n\n    switch (t->kind) {\n    case UI_OVERLAY_TEXT_NONE:\n        return nullptr;\n    case UI_OVERLAY_TEXT_LITERAL:\n        return t->literal;\n    case UI_OVERLAY_TEXT_GS_KEY:\n        return GameString_Get(t->gs_key);\n    case UI_OVERLAY_TEXT_OBJECT_NAME:\n        return Object_GetName(t->object_id);\n    }\n    return nullptr;\n}\n\nstatic const char *M_ResolveOverlayText(const UI_OVERLAY_TEXT *const t)\n{\n    const char *const raw = M_ResolveOverlayTextRaw(t);\n    if (raw == nullptr) {\n        return nullptr;\n    }\n\n    if (t->fmt_gs_key == nullptr) {\n        return raw;\n    }\n\n    return String_FormatStatic(GameString_Get(t->fmt_gs_key), raw);\n}\n\nstatic bool M_EnemyHealthBar(const UI_ELEMENT_LOCATION location)\n{\n    if (location != g_Config.ui.enemy_health_bar.location) {\n        return false;\n    }\n    if (!Game_IsPlaying()) {\n        return false;\n    }\n    if (!g_Config.ui.enable_game_ui) {\n        return false;\n    }\n    return UI_EnemyHealthBar();\n}\n\nstatic bool M_AmmoLabel(const UI_ELEMENT_LOCATION location)\n{\n    if (location != g_Config.ui.ammo_counter.location) {\n        return false;\n    }\n    if (!Game_IsPlaying()) {\n        return false;\n    }\n    if (!g_Config.ui.enable_game_ui) {\n        return false;\n    }\n    return UI_AmmoLabel();\n}\n\nstatic void M_Arrow(\n    const UI_OVERLAY_STATE *const s, const UI_OVERLAY_ARROW arrow)\n{\n    if (s->show_arrows[arrow]) {\n        // make sure the arrow has exactly the same size as the bar\n        if (m_ArrowInfo[arrow].resize) {\n            UI_BeginResize(\n                -1.0,\n                UI_BAR_HEIGHT * UI_Scaler_GetScale(UI_SCALER_TARGET_BAR)\n                    / UI_Scaler_GetScale(UI_SCALER_TARGET_TEXT));\n        }\n        UI_Label(m_ArrowInfo[arrow].label);\n        if (m_ArrowInfo[arrow].resize) {\n            UI_EndResize();\n        }\n    }\n}\n\nstatic void M_DebugPosTopLeft(void)\n{\n    const ITEM *const lara = Lara_GetItem();\n    const LARA_INFO *const lara_info = Lara_GetLaraInfo();\n    if (lara == nullptr) {\n        return;\n    }\n\n    const OBJECT_ID obj_id = Lara_GetAnimationObject();\n    const ITEM *const vehicle = Lara_Vehicle_GetItem();\n    UI_BeginStack(UI_STACK_HORIZONTAL);\n    UI_BeginStack(UI_STACK_VERTICAL);\n    if (g_Config.debug.enable_debug_pos) {\n        UI_Label(GS(\"general/overlay/debug_position\"));\n        UI_Label(GS(\"general/overlay/debug_rotation\"));\n        UI_Label(GS(\"general/overlay/debug_speed\"));\n    }\n    if (g_Config.debug.enable_debug_anim) {\n        UI_Label(GS(\"general/overlay/debug_animation\"));\n        UI_Label(GS(\"general/overlay/debug_animation_state\"));\n    }\n    if (g_Config.debug.enable_debug_camera) {\n        UI_Label(GS(\"general/overlay/debug_camera_pos\"));\n        UI_Label(GS(\"general/overlay/debug_camera_target\"));\n    }\n    UI_EndStack();\n    UI_BeginStack(UI_STACK_VERTICAL);\n    if (g_Config.debug.enable_debug_pos) {\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d / %d\", lara->pos.x / WALL_L,\n            lara->pos.y / WALL_L, lara->pos.z / WALL_L, lara->room_num));\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d°, %d°, %d°\", (int32_t)lara->rot.x * 360 / DEG_360,\n            (int32_t)lara->rot.y * 360 / DEG_360,\n            (int32_t)lara->rot.z * 360 / DEG_360));\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d\",\n            vehicle != nullptr ? vehicle->speed : lara->speed,\n            vehicle != nullptr ? vehicle->fall_speed : lara->fall_speed));\n    }\n    if (g_Config.debug.enable_debug_anim) {\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d\", Item_GetRelativeObjAnim(lara, obj_id),\n            Item_GetRelativeFrame(lara)));\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d (%d)\", lara->current_anim_state,\n            lara->goal_anim_state, Object_ToGameID(obj_id)));\n    }\n    if (g_Config.debug.enable_debug_camera) {\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d / %d\", g_Camera.pos.x / WALL_L,\n            g_Camera.pos.y / WALL_L, g_Camera.pos.z / WALL_L,\n            g_Camera.pos.room_num));\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d / %d\", g_Camera.target.x / WALL_L,\n            g_Camera.target.y / WALL_L, g_Camera.target.z / WALL_L,\n            g_Camera.target.room_num));\n    }\n    UI_EndStack();\n    UI_EndStack();\n}\n\nstatic void M_DebugPosTopRight(void)\n{\n    const ITEM *const lara = Lara_GetItem();\n    if (lara == nullptr) {\n        return;\n    }\n\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_RIGHT },\n    });\n    if (g_Config.debug.enable_debug_pos) {\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d\", lara->pos.x, lara->pos.y, lara->pos.z));\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d\", lara->rot.x, lara->rot.y, lara->rot.z));\n    }\n    if (g_Config.debug.enable_debug_camera) {\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d\", g_Camera.pos.x, g_Camera.pos.y,\n            g_Camera.pos.z));\n        UI_Label(String_FormatStatic(\n            \"\\\\{small}%d, %d, %d\", g_Camera.target.x, g_Camera.target.y,\n            g_Camera.target.z));\n    }\n    if (g_Config.debug.enable_debug_status\n        && g_Config.debug.enable_invulnerability) {\n        UI_LabelEx(\n            GS(\"general/overlay/debug_immune\"),\n            (UI_LABEL_SETTINGS) { .scale = 0.8 });\n    }\n    UI_EndStack();\n}\n\nstatic bool M_CommonRegion(\n    const UI_OVERLAY_STATE *const s, const UI_ELEMENT_LOCATION location)\n{\n    bool shown = false;\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = UI_STACK_H_ALIGN_CENTER },\n        .spacing = { .v = 10 },\n    });\n    shown |= M_LaraHealthBar(s, location);\n    shown |= M_LaraAirBar(s, location);\n    shown |= M_LaraSprintBar(s, location);\n    shown |= M_LaraExposureBar(s, location);\n    shown |= M_EnemyHealthBar(location);\n    UI_EndStack();\n    shown |= M_AmmoLabel(location);\n    return shown;\n}\n\nstatic void M_TopLeftRegion(const UI_OVERLAY_STATE *const s)\n{\n    UI_BeginOverlayRegion(0.0f, 0.0f);\n    if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_TOP_LEFT)) {\n        M_Arrow(s, UI_OVERLAY_ARROW_TL);\n    }\n    if (g_Config.ui.enable_game_ui) {\n        M_DebugPosTopLeft();\n        if (g_Config.ui.enable_fps_counter) {\n            UI_FPSCounter(s->fps);\n        }\n    }\n    UI_EndOverlayRegion();\n}\n\nstatic void M_TopCenterRegion(const UI_OVERLAY_STATE *const s)\n{\n    UI_BeginOverlayRegion(0.5f, 0.0f);\n    M_CommonRegion(s, UI_ELEMENT_LOCATION_TOP_CENTER);\n    {\n        const char *const txt = M_ResolveOverlayText(&s->top_text);\n        if (txt != nullptr) {\n            if (s->top_text.flash_enabled) {\n                UI_BeginFlash(&s->flash_state);\n            }\n            UI_Label(txt);\n            if (s->top_text.flash_enabled) {\n                UI_EndFlash();\n            }\n        }\n    }\n    UI_EndOverlayRegion();\n}\n\nstatic void M_TopRightRegion(const UI_OVERLAY_STATE *const s)\n{\n    UI_BeginOverlayRegion(1.0f, 0.0f);\n    if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_TOP_RIGHT)) {\n        M_Arrow(s, UI_OVERLAY_ARROW_TR);\n    }\n    if (g_Config.ui.enable_game_ui) {\n        M_DebugPosTopRight();\n    }\n    UI_EndOverlayRegion();\n}\n\nstatic void M_BottomLeftRegion(const UI_OVERLAY_STATE *const s)\n{\n    UI_BeginOverlayRegion(0.0f, 1.0f);\n    if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_BOTTOM_LEFT)) {\n        M_Arrow(s, UI_OVERLAY_ARROW_BL);\n    }\n    UI_EndOverlayRegion();\n}\n\nstatic void M_BottomCenterRegion(const UI_OVERLAY_STATE *const s)\n{\n    UI_BeginOverlayRegion(0.5f, 1.0f);\n    {\n        const char *const txt = M_ResolveOverlayText(&s->bottom_text);\n        if (txt != nullptr) {\n            if (s->bottom_text.flash_enabled) {\n                UI_BeginFlash(&s->flash_state);\n            }\n            UI_BeginRowArrows(\n                s->show_arrows[UI_OVERLAY_ARROW_BCL],\n                s->show_arrows[UI_OVERLAY_ARROW_BCR], UI_ROW_ARROWS_WIDE);\n            UI_Label(txt);\n            UI_EndRowArrows();\n            if (s->bottom_text.flash_enabled) {\n                UI_EndFlash();\n            }\n        }\n    }\n    M_CommonRegion(s, UI_ELEMENT_LOCATION_BOTTOM_CENTER);\n    UI_EndOverlayRegion();\n}\n\nstatic void M_BottomRightRegion(const UI_OVERLAY_STATE *const s)\n{\n    UI_BeginOverlayRegion(1.0f, 1.0f);\n    if (!M_CommonRegion(s, UI_ELEMENT_LOCATION_BOTTOM_RIGHT)) {\n        M_Arrow(s, UI_OVERLAY_ARROW_BR);\n    }\n    if (s->show_version && g_Config.ui.show_title_version) {\n        UI_LabelEx(g_TRXVersion, (UI_LABEL_SETTINGS) { .scale = 0.5f });\n    }\n    UI_EndOverlayRegion();\n}\n\nUI_OVERLAY_STATE *UI_Overlay_Init(void)\n{\n    UI_OVERLAY_STATE *const s = Memory_Alloc(sizeof(UI_OVERLAY_STATE));\n    s->fps = UI_FPSCounter_Init();\n    UI_Flash_Init(&s->flash_state, 20);\n    return s;\n}\n\nvoid UI_Overlay_Free(UI_OVERLAY_STATE *const s)\n{\n    if (s == nullptr) {\n        return;\n    }\n    if (s->fps != nullptr) {\n        UI_FPSCounter_Free(s->fps);\n        s->fps = nullptr;\n    }\n    UI_Flash_Free(&s->flash_state);\n    Memory_Free(s);\n}\n\nvoid UI_Overlay_Control(UI_OVERLAY_STATE *const s)\n{\n    s->force_show_healthbar = false;\n    UI_LaraHealthBar_Control();\n    s->blink.frame++;\n    if (s->blink.frame >= 10) {\n        s->blink.state = !s->blink.state;\n        s->blink.frame = 0;\n    }\n    UI_Flash_Control(&s->flash_state);\n}\n\nvoid UI_Overlay_ForceHealthBar(UI_OVERLAY_STATE *const s, const bool show)\n{\n    s->force_show_healthbar = show;\n}\n\nvoid UI_Overlay(UI_OVERLAY_STATE *const s)\n{\n    M_TopLeftRegion(s);\n    M_TopCenterRegion(s);\n    M_TopRightRegion(s);\n\n    M_BottomLeftRegion(s);\n    M_BottomCenterRegion(s);\n    M_BottomRightRegion(s);\n}\n\nvoid UI_BeginOverlayRegion(const float x, const float y)\n{\n    // clang-format off\n    const UI_STACK_H_ALIGN h_align =\n        x > 0.55f ? UI_STACK_H_ALIGN_RIGHT :\n        x < 0.45f ? UI_STACK_H_ALIGN_LEFT :\n        UI_STACK_H_ALIGN_CENTER;\n    // clang-format on\n    UI_BeginModal(x, y);\n    UI_BeginPad(20.0f, 14.0f);\n    UI_BeginStackEx((UI_STACK_SETTINGS) {\n        .orientation = UI_STACK_VERTICAL,\n        .align = { .h = h_align },\n        .spacing = { .v = 3 },\n    });\n}\n\nvoid UI_EndOverlayRegion(void)\n{\n    UI_EndStack();\n    UI_EndPad();\n    UI_EndModal();\n}\n\nvoid UI_Overlay_ShowArrow(\n    UI_OVERLAY_STATE *const s, const UI_OVERLAY_ARROW arrow, const bool show)\n{\n    s->show_arrows[arrow] = show;\n}\n\nvoid UI_Overlay_ShowVersion(UI_OVERLAY_STATE *const s, const bool show)\n{\n    s->show_version = show;\n}\n\nvoid UI_Overlay_SetTopText(\n    UI_OVERLAY_STATE *const s, const UI_OVERLAY_TEXT text)\n{\n    s->top_text = text;\n}\n\nvoid UI_Overlay_SetBottomText(\n    UI_OVERLAY_STATE *const s, const UI_OVERLAY_TEXT text)\n{\n    s->bottom_text = text;\n}\n"
  },
  {
    "path": "src/trx/game/ui/hud/overlay.h",
    "content": "#pragma once\n\n// Ingame user interface display widget.\n\n#include <trx/game/objects/types.h>\n#include <trx/game/ui/common.h>\n\ntypedef enum {\n    UI_OVERLAY_TEXT_NONE = 0,\n    UI_OVERLAY_TEXT_LITERAL,\n    UI_OVERLAY_TEXT_GS_KEY,\n    UI_OVERLAY_TEXT_OBJECT_NAME,\n} UI_OVERLAY_TEXT_KIND;\n\ntypedef struct {\n    UI_OVERLAY_TEXT_KIND kind;\n    // Optional GS key of a %s format wrapper applied at draw time.\n    const char *fmt_gs_key;\n    bool flash_enabled;\n    union {\n        const char *literal;\n        const char *gs_key;\n        OBJECT_ID object_id;\n    };\n} UI_OVERLAY_TEXT;\n\ntypedef enum {\n    UI_OVERLAY_ARROW_TL, // top-left screen corner\n    UI_OVERLAY_ARROW_TR, // top-right screen corner\n    UI_OVERLAY_ARROW_BL, // bottom-left screen corner\n    UI_OVERLAY_ARROW_BR, // bottom-right screen corner\n    UI_OVERLAY_ARROW_BCL, // low text left side\n    UI_OVERLAY_ARROW_BCR, // low text right side\n} UI_OVERLAY_ARROW;\n\ntypedef struct UI_OVERLAY_STATE UI_OVERLAY_STATE;\n\n// state functions\nUI_OVERLAY_STATE *UI_Overlay_Init(void);\nvoid UI_Overlay_Free(UI_OVERLAY_STATE *s);\nvoid UI_Overlay_Control(UI_OVERLAY_STATE *s);\n\n// draw functions\nvoid UI_Overlay(UI_OVERLAY_STATE *s);\nvoid UI_BeginOverlayRegion(float x, float y);\nvoid UI_EndOverlayRegion(void);\n\nvoid UI_Overlay_ForceHealthBar(UI_OVERLAY_STATE *s, bool show);\nvoid UI_Overlay_ShowArrow(\n    UI_OVERLAY_STATE *s, UI_OVERLAY_ARROW arrow, bool show);\nvoid UI_Overlay_ShowVersion(UI_OVERLAY_STATE *s, bool show);\nvoid UI_Overlay_SetTopText(UI_OVERLAY_STATE *s, UI_OVERLAY_TEXT text);\nvoid UI_Overlay_SetBottomText(UI_OVERLAY_STATE *s, UI_OVERLAY_TEXT text);\n"
  },
  {
    "path": "src/trx/game/ui/hud.h",
    "content": "#pragma once\n\n#include <trx/game/ui/hud/console.h>\n#include <trx/game/ui/hud/console_logs.h>\n#include <trx/game/ui/hud/overlay.h>\n"
  },
  {
    "path": "src/trx/game/ui/scaler.c",
    "content": "#include <trx/game/ui/scaler.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n#include <trx/game/viewport.h>\n\nstatic float M_DoCalc(\n    const float unit, const float base_width, const float base_height,\n    const double factor)\n{\n    const float vp_width = Viewport_GetWidth(VIEWPORT_UI);\n    const float vp_height = Viewport_GetHeight(VIEWPORT_UI);\n    const float sign = unit < 0 ? -1 : 1;\n    const float sx =\n        ((double)vp_width * ABS(unit) * factor) / MAX(1, base_width);\n    const float sy =\n        ((double)vp_height * ABS(unit) * factor) / MAX(1, base_height);\n    return MIN(sx, sy) * sign;\n}\n\ndouble UI_Scaler_GetScale(const UI_SCALER_TARGET target)\n{\n    switch (target) {\n    case UI_SCALER_TARGET_BAR:\n        return g_Config.ui.bar_scale;\n    case UI_SCALER_TARGET_TEXT:\n        return g_Config.ui.text_scale;\n    case UI_SCALER_TARGET_ASSAULT_DIGITS:\n        return g_Config.ui.text_scale;\n    default:\n        return 1.0;\n    }\n}\n\nfloat UI_Scaler_Calc(const float unit, const UI_SCALER_TARGET target)\n{\n    return M_DoCalc(unit, 640, 480, UI_Scaler_GetScale(target));\n}\n\nfloat UI_Scaler_CalcInverse(const float unit, const UI_SCALER_TARGET target)\n{\n    return unit * 0x10000 / MAX(1, UI_Scaler_Calc(0x10000, target));\n}\n"
  },
  {
    "path": "src/trx/game/ui/scaler.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef enum {\n    UI_SCALER_TARGET_GENERIC,\n    UI_SCALER_TARGET_BAR,\n    UI_SCALER_TARGET_TEXT,\n    UI_SCALER_TARGET_ASSAULT_DIGITS,\n} UI_SCALER_TARGET;\n\ndouble UI_Scaler_GetScale(const UI_SCALER_TARGET target);\nfloat UI_Scaler_Calc(float unit, UI_SCALER_TARGET target);\nfloat UI_Scaler_CalcInverse(float unit, UI_SCALER_TARGET target);\n"
  },
  {
    "path": "src/trx/game/ui/scrollable.c",
    "content": "#include <trx/game/ui/scrollable.h>\n\n#include <trx/config.h>\n#include <trx/core/utils.h>\n\nstatic void M_Clamp(UI_SCROLLABLE *const s, const bool include_selected_item)\n{\n    if (include_selected_item && s->sel_item != -1) {\n        CLAMP(s->first_item, s->sel_item - s->vis_items + 1, s->sel_item);\n    }\n    CLAMPG(s->first_item, s->max_items - s->vis_items);\n    CLAMPL(s->first_item, 0);\n}\n\nbool UI_Scrollable_SelectNext(\n    UI_SCROLLABLE *const s, const bool enable_wraparound)\n{\n    if (s->sel_item + 1 < s->max_items) {\n        s->sel_item++;\n    } else if (enable_wraparound) {\n        s->sel_item = 0;\n    } else {\n        return false;\n    }\n    M_Clamp(s, true);\n    return true;\n}\n\nbool UI_Scrollable_SelectPrev(\n    UI_SCROLLABLE *const s, const bool enable_wraparound)\n{\n    if (s->sel_item > 0) {\n        s->sel_item--;\n    } else if (enable_wraparound) {\n        s->sel_item = s->max_items - 1;\n    } else {\n        return false;\n    }\n    M_Clamp(s, true);\n    return true;\n}\n\nbool UI_Scrollable_ScrollDown(\n    UI_SCROLLABLE *const s, const bool enable_wraparound)\n{\n    if (s->first_item + 1 <= s->max_items - s->vis_items) {\n        s->first_item++;\n    } else if (enable_wraparound) {\n        s->first_item = 0;\n    } else {\n        return false;\n    }\n    M_Clamp(s, false);\n    return true;\n}\n\nbool UI_Scrollable_ScrollUp(\n    UI_SCROLLABLE *const s, const bool enable_wraparound)\n{\n    if (s->first_item > 0) {\n        s->first_item--;\n    } else if (enable_wraparound) {\n        s->first_item = s->max_items - 1;\n    } else {\n        return false;\n    }\n    M_Clamp(s, false);\n    return true;\n}\n\nvoid UI_Scrollable_SetVisibleItems(\n    UI_SCROLLABLE *const s, const int32_t visible_items)\n{\n    s->vis_items = visible_items;\n    CLAMPL(s->vis_items, 0);\n    M_Clamp(s, true);\n}\n\nvoid UI_Scrollable_SetMaxItems(UI_SCROLLABLE *const s, const int32_t max_items)\n{\n    s->max_items = max_items;\n    CLAMP(s->sel_item, 0, s->max_items - 1);\n}\n\nvoid UI_Scrollable_SelectItem(UI_SCROLLABLE *const s, const int32_t row)\n{\n    s->sel_item = row;\n    if (s->sel_item != -1) {\n        CLAMP(s->sel_item, 0, s->max_items - 1);\n        CLAMP(s->first_item, s->sel_item - s->vis_items + 1, s->sel_item);\n    }\n}\n\nvoid UI_Scrollable_SelectFirstItem(UI_SCROLLABLE *const s)\n{\n    UI_Scrollable_SelectItem(s, 0);\n}\n\nvoid UI_Scrollable_SelectLastItem(UI_SCROLLABLE *const s)\n{\n    UI_Scrollable_SelectItem(s, s->max_items - 1);\n}\n\nint32_t UI_Scrollable_GetFirstVisibleItem(const UI_SCROLLABLE *const s)\n{\n    return s->first_item;\n}\n\nint32_t UI_Scrollable_GetSelectedItem(const UI_SCROLLABLE *const s)\n{\n    return s->sel_item;\n}\n\nint32_t UI_Scrollable_GetLastVisibleItem(const UI_SCROLLABLE *const s)\n{\n    return MIN(s->first_item + s->vis_items - 1, s->max_items - 1);\n}\n\nbool UI_Scrollable_IsItemVisible(\n    const UI_SCROLLABLE *const s, const int32_t item)\n{\n    return item >= UI_Scrollable_GetFirstVisibleItem(s)\n        && item <= UI_Scrollable_GetLastVisibleItem(s);\n}\n\nbool UI_Scrollable_IsItemSelected(\n    const UI_SCROLLABLE *const s, const int32_t item)\n{\n    return item == UI_Scrollable_GetSelectedItem(s);\n}\n"
  },
  {
    "path": "src/trx/game/ui/scrollable.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct {\n    int32_t first_item;\n    int32_t sel_item;\n    int32_t max_items;\n    int32_t vis_items;\n} UI_SCROLLABLE;\n\nbool UI_Scrollable_SelectNext(UI_SCROLLABLE *s, bool enable_wraparound);\nbool UI_Scrollable_SelectPrev(UI_SCROLLABLE *s, bool enable_wraparound);\nbool UI_Scrollable_ScrollDown(UI_SCROLLABLE *s, bool enable_wraparound);\nbool UI_Scrollable_ScrollUp(UI_SCROLLABLE *s, bool enable_wraparound);\n\nvoid UI_Scrollable_SetVisibleItems(UI_SCROLLABLE *s, int32_t visible_items);\nvoid UI_Scrollable_SetMaxItems(UI_SCROLLABLE *s, int32_t max_items);\nvoid UI_Scrollable_SelectItem(UI_SCROLLABLE *s, int32_t item);\nvoid UI_Scrollable_SelectFirstItem(UI_SCROLLABLE *s);\nvoid UI_Scrollable_SelectLastItem(UI_SCROLLABLE *s);\n\nint32_t UI_Scrollable_GetFirstVisibleItem(const UI_SCROLLABLE *s);\nint32_t UI_Scrollable_GetLastVisibleItem(const UI_SCROLLABLE *s);\nint32_t UI_Scrollable_GetSelectedItem(const UI_SCROLLABLE *s);\nbool UI_Scrollable_IsItemVisible(const UI_SCROLLABLE *s, int32_t item);\nbool UI_Scrollable_IsItemSelected(const UI_SCROLLABLE *s, int32_t item);\n"
  },
  {
    "path": "src/trx/game/ui/settings.c",
    "content": "#include <trx/game/ui/settings.h>\n\n#include <trx/config.h>\n#include <trx/core/json/util/file.h>\n#include <trx/core/json/util/read_io.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/game/game_strings/entries.h>\n#include <trx/game/shell.h>\n#include <trx/version.h>\n\n#include <uthash.h>\n\ntypedef struct {\n    char *name;\n    UI_BAR_THEME theme;\n} M_THEME_ENTRY;\n\ntypedef struct M_THEME_LOOKUP {\n    char *name;\n    int32_t index;\n    UT_hash_handle hh;\n} M_THEME_LOOKUP;\n\ntypedef struct {\n    int32_t color_count;\n    M_THEME_ENTRY *colors;\n    struct M_THEME_LOOKUP *lookup;\n} M_THEME_GROUP;\n\ntypedef struct {\n    char *name;\n    char *name_gs;\n    UI_BAR_THEME_KIND kind;\n    M_THEME_GROUP group;\n} M_BAR_THEME_ENTRY;\n\ntypedef struct M_BAR_THEME_LOOKUP {\n    char *name;\n    int32_t index;\n    UT_hash_handle hh;\n} M_BAR_THEME_LOOKUP;\n\ntypedef struct {\n    int32_t bar_theme_count;\n    M_BAR_THEME_ENTRY *bar_themes;\n    struct M_BAR_THEME_LOOKUP *bar_lookup;\n} M_SETTINGS;\n\ntypedef struct {\n    char *const *const pc_color;\n    char *const *const ps1_color;\n} M_BAR_COLOR_SELECT;\n\nstatic const M_BAR_COLOR_SELECT m_BarColorSelect[UI_BAR_NUMBER_OF] = {\n    [UI_BAR_LARA_HP] = {\n        .pc_color = &g_Config.ui.lara_health_bar.color,\n        .ps1_color = &g_Config.ui.lara_health_bar.color_ps1,\n    },\n    [UI_BAR_LARA_HP_POISON] = {\n        .pc_color = &g_Config.ui.lara_health_bar.poison_color,\n        .ps1_color = &g_Config.ui.lara_health_bar.poison_color_ps1,\n    },\n    [UI_BAR_LARA_AIR] = {\n        .pc_color = &g_Config.ui.lara_air_bar.color,\n        .ps1_color = &g_Config.ui.lara_air_bar.color_ps1,\n    },\n    [UI_BAR_LARA_STAMINA] = {\n        .pc_color = &g_Config.ui.lara_sprint_bar.color,\n        .ps1_color = &g_Config.ui.lara_sprint_bar.color_ps1,\n    },\n    [UI_BAR_LARA_EXPOSURE] = {\n        .pc_color = &g_Config.ui.lara_exposure_bar.color,\n        .ps1_color = &g_Config.ui.lara_exposure_bar.color_ps1,\n    },\n    [UI_BAR_ENEMY_HP] = {\n        .pc_color = &g_Config.ui.enemy_health_bar.color,\n        .ps1_color = &g_Config.ui.enemy_health_bar.color_ps1,\n    },\n    [UI_BAR_ALLY_HP] = {\n        .pc_color = &g_Config.ui.enemy_health_bar.color_allies,\n        .ps1_color = &g_Config.ui.enemy_health_bar.color_allies_ps1,\n    },\n};\n\nstatic M_SETTINGS m_Settings;\n\nstatic UI_MENU_COLORS_PC m_MenuColorsPC[3]; // indexed [g_TRVersion - 1]\nstatic UI_MENU_COLORS_PS1 m_MenuColorsPS1[3]; // indexed [g_TRVersion - 1]\n\nstatic void M_ExitWithJSONError(\n    const char *const source_path, const JSON_READ_IO *const io)\n{\n    JSONFile_ExitWithReadIOError(\n        io, String_FormatStatic(\"%s: ui settings parse error\", source_path));\n}\n\nstatic void M_FreeThemeGroup(M_THEME_GROUP *const group)\n{\n    M_THEME_LOOKUP *entry = nullptr;\n    M_THEME_LOOKUP *tmp = nullptr;\n    HASH_ITER(hh, group->lookup, entry, tmp)\n    {\n        HASH_DEL(group->lookup, entry);\n        Memory_FreePointer(&entry);\n    }\n    if (group->colors == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < group->color_count; i++) {\n        Memory_FreePointer(&group->colors[i].name);\n    }\n    Memory_FreePointer(&group->colors);\n    group->color_count = 0;\n    group->lookup = nullptr;\n}\n\nstatic void M_ResetDynamicEnumValues(void)\n{\n    const CONFIG_OPTION *const bar_look_option =\n        Config_GetOption(&g_Config.ui.bar_look);\n    if (bar_look_option != nullptr) {\n        Config_DynamicEnum_ResetValues(bar_look_option);\n    }\n\n    for (int32_t i = 0; i < UI_BAR_NUMBER_OF; i++) {\n        const M_BAR_COLOR_SELECT *const select = &m_BarColorSelect[i];\n        const CONFIG_OPTION *const pc_option =\n            Config_GetOption(select->pc_color);\n        if (pc_option != nullptr) {\n            Config_DynamicEnum_ResetValues(pc_option);\n        }\n        const CONFIG_OPTION *const ps1_option =\n            Config_GetOption(select->ps1_color);\n        if (ps1_option != nullptr) {\n            Config_DynamicEnum_ResetValues(ps1_option);\n        }\n    }\n}\n\nstatic bool M_IsBarColorNameEncountered(\n    const UI_BAR_THEME_KIND kind, const char *const name, const int32_t stop_i,\n    const int32_t stop_j)\n{\n    for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) {\n        M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i];\n        if (theme->kind != kind) {\n            continue;\n        }\n        for (int32_t j = 0; j < theme->group.color_count; j++) {\n            if (i == stop_i && j == stop_j) {\n                return false;\n            }\n            if (String_Equivalent(theme->group.colors[j].name, name)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nstatic void M_SeedDynamicEnumBarColors(\n    const CONFIG_OPTION *const option, const UI_BAR_THEME_KIND kind)\n{\n    Config_DynamicEnum_ResetValues(option);\n    for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) {\n        const M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i];\n        if (theme->kind != kind) {\n            continue;\n        }\n        for (int32_t j = 0; j < theme->group.color_count; j++) {\n            const char *const name = theme->group.colors[j].name;\n            if (M_IsBarColorNameEncountered(kind, name, i, j)) {\n                continue;\n            }\n            Config_DynamicEnum_AddValue(option, name, nullptr);\n        }\n    }\n}\n\nstatic void M_SeedDynamicEnumValues(void)\n{\n    const CONFIG_OPTION *const bar_look_option =\n        Config_GetOption(&g_Config.ui.bar_look);\n    if (bar_look_option != nullptr) {\n        Config_DynamicEnum_ResetValues(bar_look_option);\n        for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) {\n            const M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i];\n            Config_DynamicEnum_AddValue(\n                bar_look_option, theme->name, theme->name_gs);\n        }\n    }\n\n    for (int32_t i = 0; i < UI_BAR_NUMBER_OF; i++) {\n        const M_BAR_COLOR_SELECT *const select = &m_BarColorSelect[i];\n        M_SeedDynamicEnumBarColors(\n            Config_GetOption(select->pc_color), UI_BAR_THEME_PC_KIND);\n        M_SeedDynamicEnumBarColors(\n            Config_GetOption(select->ps1_color), UI_BAR_THEME_PS1_KIND);\n    }\n}\n\nstatic void M_FreeBarThemes(void)\n{\n    M_ResetDynamicEnumValues();\n\n    M_BAR_THEME_LOOKUP *entry = nullptr;\n    M_BAR_THEME_LOOKUP *tmp = nullptr;\n    HASH_ITER(hh, m_Settings.bar_lookup, entry, tmp)\n    {\n        HASH_DEL(m_Settings.bar_lookup, entry);\n        Memory_FreePointer(&entry);\n    }\n\n    if (m_Settings.bar_themes == nullptr) {\n        return;\n    }\n\n    for (int32_t i = 0; i < m_Settings.bar_theme_count; i++) {\n        M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[i];\n        Memory_FreePointer(&theme->name);\n        Memory_FreePointer(&theme->name_gs);\n        M_FreeThemeGroup(&theme->group);\n    }\n\n    Memory_FreePointer(&m_Settings.bar_themes);\n    m_Settings.bar_theme_count = 0;\n    m_Settings.bar_lookup = nullptr;\n}\n\nstatic bool M_ReadColorArray(\n    JSON_READ_IO *const io, RGBA_8888 colors[UI_BAR_COLOR_STEPS])\n{\n    const int32_t count = JSON_ARRAY_LEN(io);\n    if (count != UI_BAR_COLOR_STEPS) {\n        JSON_ReadIO_SetError(\n            io, \"invalid color array (expected %d entries)\",\n            UI_BAR_COLOR_STEPS);\n        JSON_FAIL();\n    }\n\n    for (int32_t i = 0; i < UI_BAR_COLOR_STEPS; i++) {\n        RGB_888 rgb = {};\n        JSON_MUST(JSON_READ_A(io, i, &rgb));\n        colors[i] = Color_RGBToRGBA(rgb);\n    }\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadThemesPC(JSON_READ_IO *const io, M_THEME_GROUP *const group)\n{\n    float basic_scale = 1.0f;\n    RGBA_8888 border_light = {};\n    RGBA_8888 border_dark = {};\n\n    JSON_READ_D(io, \"scale\", &basic_scale, 1.0f);\n\n    RGB_888 border_light_rgb = {};\n    JSON_MUST(JSON_READ(io, \"border_light\", &border_light_rgb));\n    border_light = Color_RGBToRGBA(border_light_rgb);\n\n    RGB_888 border_dark_rgb = {};\n    JSON_MUST(JSON_READ(io, \"border_dark\", &border_dark_rgb));\n    border_dark = Color_RGBToRGBA(border_dark_rgb);\n\n    JSON_MUST(JSON_PUSH(io, \"colors\"));\n    JSON_OBJECT *const colors_obj = JSON_ReadIO_GetCurrentObject(io);\n    if (colors_obj == nullptr) {\n        JSON_ReadIO_SetError(io, \"'colors' must be an object\");\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    size_t count = 0;\n    for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr;\n         elem = elem->next) {\n        count++;\n    }\n    if (count == 0) {\n        JSON_ReadIO_SetError(io, \"'colors' cannot be empty\");\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    M_FreeThemeGroup(group);\n    group->colors = Memory_Alloc(sizeof(*group->colors) * count);\n    group->color_count = (int32_t)count;\n    group->lookup = nullptr;\n\n    size_t idx = 0;\n    for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr;\n         elem = elem->next) {\n        const char *const name = elem->name->string;\n        JSON_MUST(JSON_PUSH(io, name));\n\n        group->colors[idx].name = Memory_DupStr(name);\n        M_THEME_LOOKUP *existing = nullptr;\n        HASH_FIND_STR(group->lookup, group->colors[idx].name, existing);\n        if (existing != nullptr) {\n            JSON_ReadIO_SetError(io, \"duplicate color '%s'\", name);\n            JSON_MUST(JSON_POP(io));\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n\n        M_THEME_LOOKUP *const entry = Memory_Alloc(sizeof(*entry));\n        entry->name = group->colors[idx].name;\n        entry->index = (int32_t)idx;\n        HASH_ADD_KEYPTR(\n            hh, group->lookup, entry->name, strlen(entry->name), entry);\n\n        UI_BAR_THEME *const theme = &group->colors[idx].theme;\n        *theme = (UI_BAR_THEME) {\n            .kind = UI_BAR_THEME_PC_KIND,\n            .basic_scale = basic_scale,\n            .border_light = border_light,\n            .border_dark = border_dark,\n        };\n        JSON_MUST(M_ReadColorArray(io, theme->ramp));\n        JSON_MUST(JSON_POP(io));\n        idx++;\n    }\n\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadThemesPS1(JSON_READ_IO *const io, M_THEME_GROUP *const group)\n{\n    float basic_scale = 1.0f;\n\n    JSON_READ_D(io, \"scale\", &basic_scale, 1.0f);\n\n    RGB_888 border_tl_rgb = {};\n    RGB_888 border_tr_rgb = {};\n    RGB_888 border_bl_rgb = {};\n    RGB_888 border_br_rgb = {};\n    JSON_MUST(JSON_READ(io, \"border_tl\", &border_tl_rgb));\n    JSON_MUST(JSON_READ(io, \"border_tr\", &border_tr_rgb));\n    JSON_MUST(JSON_READ(io, \"border_bl\", &border_bl_rgb));\n    JSON_MUST(JSON_READ(io, \"border_br\", &border_br_rgb));\n    const RGBA_8888 border_tl = Color_RGBToRGBA(border_tl_rgb);\n    const RGBA_8888 border_tr = Color_RGBToRGBA(border_tr_rgb);\n    const RGBA_8888 border_bl = Color_RGBToRGBA(border_bl_rgb);\n    const RGBA_8888 border_br = Color_RGBToRGBA(border_br_rgb);\n\n    JSON_MUST(JSON_PUSH(io, \"colors\"));\n    JSON_OBJECT *const colors_obj = JSON_ReadIO_GetCurrentObject(io);\n    if (colors_obj == nullptr) {\n        JSON_ReadIO_SetError(io, \"'colors' must be an object\");\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    size_t count = 0;\n    for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr;\n         elem = elem->next) {\n        count++;\n    }\n    if (count == 0) {\n        JSON_ReadIO_SetError(io, \"'colors' cannot be empty\");\n        JSON_MUST(JSON_POP(io));\n        JSON_FAIL();\n    }\n\n    M_FreeThemeGroup(group);\n    group->colors = Memory_Alloc(sizeof(*group->colors) * count);\n    group->color_count = (int32_t)count;\n    group->lookup = nullptr;\n\n    size_t idx = 0;\n    for (JSON_OBJECT_ELEMENT *elem = colors_obj->start; elem != nullptr;\n         elem = elem->next) {\n        const char *const name = elem->name->string;\n        JSON_MUST(JSON_PUSH(io, name));\n\n        const int32_t ramps_count = JSON_ARRAY_LEN(io);\n        if (ramps_count != 2) {\n            JSON_ReadIO_SetError(\n                io, \"invalid '%s' color definition (expected 2 arrays)\", name);\n            JSON_MUST(JSON_POP(io));\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n\n        group->colors[idx].name = Memory_DupStr(name);\n        M_THEME_LOOKUP *existing = nullptr;\n        HASH_FIND_STR(group->lookup, group->colors[idx].name, existing);\n        if (existing != nullptr) {\n            JSON_ReadIO_SetError(io, \"duplicate color '%s'\", name);\n            JSON_MUST(JSON_POP(io));\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n        M_THEME_LOOKUP *const entry = Memory_Alloc(sizeof(*entry));\n        entry->name = group->colors[idx].name;\n        entry->index = (int32_t)idx;\n        HASH_ADD_KEYPTR(\n            hh, group->lookup, entry->name, strlen(entry->name), entry);\n\n        UI_BAR_THEME *const theme = &group->colors[idx].theme;\n        *theme = (UI_BAR_THEME) {\n            .kind = UI_BAR_THEME_PS1_KIND,\n            .basic_scale = basic_scale,\n            .border_tl = border_tl,\n            .border_tr = border_tr,\n            .border_bl = border_bl,\n            .border_br = border_br,\n        };\n\n        JSON_MUST(JSON_PUSH_INDEX(io, 0));\n        JSON_MUST(M_ReadColorArray(io, theme->ramp_left));\n        JSON_MUST(JSON_POP(io));\n\n        JSON_MUST(JSON_PUSH_INDEX(io, 1));\n        JSON_MUST(M_ReadColorArray(io, theme->ramp_right));\n        JSON_MUST(JSON_POP(io));\n\n        JSON_MUST(JSON_POP(io));\n        idx++;\n    }\n\n    JSON_MUST(JSON_POP(io));\n    JSON_FINISH();\n}\n\nstatic bool M_LoadTheme(JSON_READ_IO *const io, M_BAR_THEME_ENTRY *const theme)\n{\n    const char *name_gs = nullptr;\n    JSON_MUST(JSON_READ(io, \"name_gs\", &name_gs));\n    theme->name_gs = Memory_DupStr(name_gs);\n\n    const char *style = nullptr;\n    JSON_MUST(JSON_READ(io, \"style\", &style));\n    if (String_Equivalent(style, \"pc\")) {\n        theme->kind = UI_BAR_THEME_PC_KIND;\n        JSON_MUST(M_LoadThemesPC(io, &theme->group));\n    } else if (String_Equivalent(style, \"ps1\")) {\n        theme->kind = UI_BAR_THEME_PS1_KIND;\n        JSON_MUST(M_LoadThemesPS1(io, &theme->group));\n    } else {\n        JSON_ReadIO_SetError(io, \"invalid 'style' value '%s'\", style);\n        JSON_FAIL();\n    }\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadBarThemes(JSON_READ_IO *const io)\n{\n    JSON_OBJECT *const root_obj = JSON_ReadIO_GetCurrentObject(io);\n    if (root_obj == nullptr) {\n        JSON_ReadIO_SetError(\n            io, \"invalid ui settings file: root must be object\");\n        JSON_FAIL();\n    }\n\n    size_t theme_count = 0;\n    for (JSON_OBJECT_ELEMENT *elem = root_obj->start; elem != nullptr;\n         elem = elem->next) {\n        theme_count++;\n    }\n    if (theme_count == 0) {\n        JSON_ReadIO_SetError(io, \"ui settings file has no bar themes\");\n        JSON_FAIL();\n    }\n\n    m_Settings.bar_themes =\n        Memory_Alloc(sizeof(*m_Settings.bar_themes) * theme_count);\n    m_Settings.bar_theme_count = (int32_t)theme_count;\n    m_Settings.bar_lookup = nullptr;\n\n    size_t idx = 0;\n    for (JSON_OBJECT_ELEMENT *elem = root_obj->start; elem != nullptr;\n         elem = elem->next) {\n        const char *const theme_name = elem->name->string;\n        JSON_MUST(JSON_PUSH(io, theme_name));\n\n        M_BAR_THEME_ENTRY *const theme = &m_Settings.bar_themes[idx];\n        theme->name = Memory_DupStr(theme_name);\n        theme->name_gs = nullptr;\n        theme->kind = UI_BAR_THEME_PC_KIND;\n        theme->group = (M_THEME_GROUP) {};\n\n        M_BAR_THEME_LOOKUP *existing = nullptr;\n        HASH_FIND_STR(m_Settings.bar_lookup, theme->name, existing);\n        if (existing != nullptr) {\n            JSON_ReadIO_SetError(io, \"duplicate theme '%s'\", theme_name);\n            JSON_MUST(JSON_POP(io));\n            JSON_FAIL();\n        }\n\n        JSON_MUST(M_LoadTheme(io, theme));\n\n        M_BAR_THEME_LOOKUP *const entry = Memory_Alloc(sizeof(*entry));\n        entry->name = theme->name;\n        entry->index = (int32_t)idx;\n        HASH_ADD_KEYPTR(\n            hh, m_Settings.bar_lookup, entry->name, strlen(entry->name), entry);\n\n        JSON_MUST(JSON_POP(io));\n        idx++;\n    }\n\n    JSON_FINISH();\n}\n\nstatic M_BAR_THEME_ENTRY *M_FindBarThemeByName(const char *const name)\n{\n    if (name == nullptr) {\n        return nullptr;\n    }\n    M_BAR_THEME_LOOKUP *entry = nullptr;\n    HASH_FIND_STR(m_Settings.bar_lookup, name, entry);\n    if (entry == nullptr) {\n        return nullptr;\n    }\n    return &m_Settings.bar_themes[entry->index];\n}\n\nstatic M_BAR_THEME_ENTRY *M_GetCurrentBarTheme(void)\n{\n    M_BAR_THEME_ENTRY *theme = M_FindBarThemeByName(g_Config.ui.bar_look);\n    if (theme != nullptr) {\n        return theme;\n    }\n    if (m_Settings.bar_theme_count <= 0) {\n        return nullptr;\n    }\n    return &m_Settings.bar_themes[0];\n}\n\nstatic const M_THEME_GROUP *M_GetCurrentBarGroup(void)\n{\n    M_BAR_THEME_ENTRY *const theme = M_GetCurrentBarTheme();\n    if (theme == nullptr) {\n        return nullptr;\n    }\n    return &theme->group;\n}\n\nstatic bool M_LoadMenuColorsPC(\n    JSON_READ_IO *const io, UI_MENU_COLORS_PC *const c)\n{\n    JSON_MUST(JSON_PUSH(io, \"background\"));\n    JSON_MUST(JSON_READ_A(io, 0, &c->background[0]));\n    JSON_MUST(JSON_READ_A(io, 1, &c->background[1]));\n    JSON_MUST(JSON_POP(io));\n\n    JSON_MUST(JSON_PUSH(io, \"background_heavy\"));\n    JSON_MUST(JSON_READ_A(io, 0, &c->background_heavy[0]));\n    JSON_MUST(JSON_READ_A(io, 1, &c->background_heavy[1]));\n    JSON_MUST(JSON_POP(io));\n\n    JSON_MUST(JSON_READ(io, \"outline_light\", &c->outline_light));\n    JSON_MUST(JSON_READ(io, \"outline_dark\", &c->outline_dark));\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadMenuColorsPS1(\n    JSON_READ_IO *const io, UI_MENU_COLORS_PS1 *const c)\n{\n    JSON_MUST(JSON_READ(io, \"background_edge\", &c->background_edge));\n    JSON_MUST(JSON_READ(io, \"background_center\", &c->background_center));\n    JSON_MUST(\n        JSON_READ(io, \"background_heavy_edge\", &c->background_heavy_edge));\n    JSON_MUST(\n        JSON_READ(io, \"background_heavy_center\", &c->background_heavy_center));\n    JSON_MUST(JSON_READ(io, \"heading_edge\", &c->heading_edge));\n    JSON_MUST(JSON_READ(io, \"heading_center\", &c->heading_center));\n    JSON_MUST(JSON_READ(io, \"requested_edge\", &c->requested_edge));\n    JSON_MUST(JSON_READ(io, \"requested_center\", &c->requested_center));\n    JSON_MUST(JSON_READ(io, \"requested_outline_ch\", &c->requested_outline_ch));\n    JSON_MUST(JSON_READ(io, \"requested_outline_cv\", &c->requested_outline_cv));\n    JSON_MUST(\n        JSON_READ(io, \"requested_outline_edge\", &c->requested_outline_edge));\n    JSON_MUST(JSON_READ(io, \"outline_tl\", &c->outline_tl));\n    JSON_MUST(JSON_READ(io, \"outline_tr\", &c->outline_tr));\n    JSON_MUST(JSON_READ(io, \"outline_bl\", &c->outline_bl));\n    JSON_MUST(JSON_READ(io, \"outline_br\", &c->outline_br));\n    JSON_MUST(JSON_READ(io, \"heading_outline\", &c->heading_outline));\n\n    JSON_FINISH();\n}\n\nstatic bool M_LoadMenuColors(JSON_READ_IO *const io)\n{\n    static const char *const tr_keys[] = { \"tr1\", \"tr2\", \"tr3\" };\n\n    for (int32_t i = 0; i < 3; i++) {\n        JSON_MUST(JSON_PUSH(io, tr_keys[i]));\n\n        JSON_MUST(JSON_PUSH(io, \"pc\"));\n        JSON_MUST(M_LoadMenuColorsPC(io, &m_MenuColorsPC[i]));\n        JSON_MUST(JSON_POP(io));\n\n        JSON_MUST(JSON_PUSH(io, \"ps1\"));\n        JSON_MUST(M_LoadMenuColorsPS1(io, &m_MenuColorsPS1[i]));\n        JSON_MUST(JSON_POP(io));\n\n        JSON_MUST(JSON_POP(io));\n    }\n\n    JSON_FINISH();\n}\n\nvoid UI_Settings_LoadFromFile(const char *const path)\n{\n    JSON_VALUE *const root = JSONFile_ReadEx(path, true);\n    JSON_READ_IO *const io = JSON_ReadIO_Create(root, 0, path);\n\n    M_FreeBarThemes();\n    if (!JSON_PUSH(io, \"bars\") || !M_LoadBarThemes(io) || !JSON_POP(io)) {\n        M_ExitWithJSONError(path, io);\n    }\n\n    if (!JSON_PUSH(io, \"ui\") || !M_LoadMenuColors(io) || !JSON_POP(io)) {\n        M_ExitWithJSONError(path, io);\n    }\n\n    M_SeedDynamicEnumValues();\n\n    JSON_ReadIO_Destroy(io);\n    JSON_ValueFree(root);\n}\n\n__attribute__((destructor)) static void M_Shutdown(void)\n{\n    M_FreeBarThemes();\n}\n\nstatic const char *M_GetBarColorName(const UI_BAR_TYPE type)\n{\n    if (type < 0 || type >= UI_BAR_NUMBER_OF) {\n        return \"gold\";\n    }\n\n    const M_BAR_THEME_ENTRY *const theme = M_GetCurrentBarTheme();\n    const bool use_ps1 =\n        theme != nullptr && theme->kind == UI_BAR_THEME_PS1_KIND;\n    const M_BAR_COLOR_SELECT *const select = &m_BarColorSelect[type];\n    const char *value = nullptr;\n\n    if (use_ps1 && select->ps1_color != nullptr) {\n        value = *select->ps1_color;\n    } else if (!use_ps1 && select->pc_color != nullptr) {\n        value = *select->pc_color;\n    }\n\n    return value;\n}\n\nstatic const UI_BAR_THEME *M_FindThemeByName(\n    const M_THEME_GROUP *const group, const char *const name)\n{\n    if (group == nullptr || group->colors == nullptr || group->color_count <= 0\n        || name == nullptr) {\n        return nullptr;\n    }\n    M_THEME_LOOKUP *entry = nullptr;\n    HASH_FIND_STR(group->lookup, name, entry);\n    if (entry != nullptr) {\n        return &group->colors[entry->index].theme;\n    }\n    return nullptr;\n}\n\nbool UI_Settings_IsCurrentBarLookPS1(void)\n{\n    const M_BAR_THEME_ENTRY *const theme = M_GetCurrentBarTheme();\n    return theme != nullptr && theme->kind == UI_BAR_THEME_PS1_KIND;\n}\n\nconst UI_BAR_THEME *UI_Settings_GetBarTheme(const UI_BAR_TYPE type)\n{\n    if (type < 0 || type >= UI_BAR_NUMBER_OF) {\n        return nullptr;\n    }\n    const M_THEME_GROUP *const group = M_GetCurrentBarGroup();\n    if (group == nullptr || group->color_count <= 0) {\n        return nullptr;\n    }\n    const char *const name = M_GetBarColorName(type);\n    const UI_BAR_THEME *theme = M_FindThemeByName(group, name);\n    if (theme != nullptr) {\n        return theme;\n    }\n    return &group->colors[0].theme;\n}\n\nconst UI_MENU_COLORS_PC *UI_Settings_GetMenuColorsPC(void)\n{\n    return &m_MenuColorsPC[g_TRVersion - 1];\n}\n\nconst UI_MENU_COLORS_PS1 *UI_Settings_GetMenuColorsPS1(void)\n{\n    return &m_MenuColorsPS1[g_TRVersion - 1];\n}\n"
  },
  {
    "path": "src/trx/game/ui/settings.h",
    "content": "#pragma once\n\n#include <trx/core/colors.h>\n\n#include <stddef.h>\n#include <stdint.h>\n\n#define UI_BAR_COLOR_STEPS 5\n\ntypedef enum {\n    UI_BAR_LARA_HP,\n    UI_BAR_LARA_HP_POISON,\n    UI_BAR_LARA_AIR,\n    UI_BAR_LARA_STAMINA,\n    UI_BAR_LARA_EXPOSURE,\n    UI_BAR_ENEMY_HP,\n    UI_BAR_ALLY_HP,\n    UI_BAR_PROGRESS,\n    UI_BAR_NUMBER_OF,\n} UI_BAR_TYPE;\n\ntypedef enum {\n    UI_BAR_THEME_PC_KIND,\n    UI_BAR_THEME_PS1_KIND,\n} UI_BAR_THEME_KIND;\n\ntypedef struct {\n    UI_BAR_THEME_KIND kind;\n    float basic_scale;\n    RGBA_8888 border_light;\n    RGBA_8888 border_dark;\n    RGBA_8888 border_tl;\n    RGBA_8888 border_tr;\n    RGBA_8888 border_bl;\n    RGBA_8888 border_br;\n    RGBA_8888 ramp[UI_BAR_COLOR_STEPS];\n    RGBA_8888 ramp_left[UI_BAR_COLOR_STEPS];\n    RGBA_8888 ramp_right[UI_BAR_COLOR_STEPS];\n} UI_BAR_THEME;\n\ntypedef struct {\n    RGBA_8888 background[2]; // TS_BACKGROUND: [top, bottom]\n    RGBA_8888 background_heavy[2]; // TS_BACKGROUND_HEAVY: [top, bottom]\n    RGBA_8888 outline_light;\n    RGBA_8888 outline_dark;\n} UI_MENU_COLORS_PC;\n\ntypedef struct {\n    RGBA_8888 background_edge;\n    RGBA_8888 background_center;\n    RGBA_8888 background_heavy_edge;\n    RGBA_8888 background_heavy_center;\n    RGBA_8888 heading_edge;\n    RGBA_8888 heading_center;\n    RGBA_8888 requested_edge;\n    RGBA_8888 requested_center;\n    RGBA_8888 requested_outline_ch;\n    RGBA_8888 requested_outline_cv;\n    RGBA_8888 requested_outline_edge;\n    RGBA_8888 outline_tl;\n    RGBA_8888 outline_tr;\n    RGBA_8888 outline_bl;\n    RGBA_8888 outline_br;\n    RGBA_8888 heading_outline;\n} UI_MENU_COLORS_PS1;\n\nvoid UI_Settings_LoadFromFile(const char *path);\n\nconst UI_BAR_THEME *UI_Settings_GetBarTheme(UI_BAR_TYPE type);\nbool UI_Settings_IsCurrentBarLookPS1(void);\n\nconst UI_MENU_COLORS_PC *UI_Settings_GetMenuColorsPC(void);\nconst UI_MENU_COLORS_PS1 *UI_Settings_GetMenuColorsPS1(void);\n"
  },
  {
    "path": "src/trx/game/ui/text.c",
    "content": "#include <trx/game/ui/text.h>\n\n#include <trx/config.h>\n#include <trx/core/enum_map.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/strings.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/game/input/common.h>\n#include <trx/game/objects.h>\n#include <trx/game/output.h>\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/scaler.h>\n#include <trx/version.h>\n\n#include <ctype.h>\n#include <stdio.h>\n#include <string.h>\n#include <uthash.h>\n\n#define M_LETTER_SPACING 0.5f\n#define M_WORD_SPACING 6.0f\n#define M_DIM_COLOR 12\n#define M_MAX_COLOR 13\n\ntypedef enum {\n    M_FONT_DEFAULT = 0,\n    M_FONT_SMALL = 1,\n    M_FONT_COUNT,\n} M_FONT;\n\ntypedef enum {\n    // A text character.\n    GLYPH_TEXT,\n    // An icon.\n    GLYPH_ICON,\n    // Spacing between words.\n    GLYPH_SPACE,\n    // Line break.\n    GLYPH_NEW_LINE,\n    // Marker used in the examine item dialog and others to force a new page.\n    GLYPH_NEW_PAGE,\n    // Icon for collectible secrets, taking the sprite from O_SECRET\n    GLYPH_SECRET,\n    // Icon requesting translators to verify AI-translated text.\n    GLYPH_REVIEW_MARKER,\n    // Marker that toggles the visibility of the following text.\n    GLYPH_VISIBILITY_MARKER,\n    // Marker that toggles the dimming of the following text.\n    GLYPH_DIM_MARKER,\n    // Marker that changes the color of the following text.\n    GLYPH_COLOR_MARKER,\n    // Marker that changes the font of the following text.\n    // - mesh_idx = 0: default font (O_ALPHABET).\n    // - mesh_idx = 1: default font (O_ALPHABET_SMALL).\n    GLYPH_FONT_MARKER,\n    // Glyph that dynamically expands a key role to its current key icon.\n    GLYPH_INPUT,\n} M_GLYPH_ROLE;\n\ntypedef struct {\n    const char *text;\n    M_GLYPH_ROLE role;\n    int32_t width[M_FONT_COUNT];\n    union {\n        int32_t mesh_idx;\n        INPUT_ROLE input_role; // for role == GLYPH_INPUT\n    };\n} M_GLYPH_INFO;\n\ntypedef struct {\n    M_GLYPH_INFO *glyph;\n    UT_hash_handle hh;\n} M_GLYPH_MAP_ENTRY;\n\ntypedef struct {\n    char *text;\n    const M_GLYPH_INFO **glyphs;\n    size_t glyph_count;\n    UT_hash_handle hh;\n} M_TEXT_MAP_ENTRY;\n\nstatic M_GLYPH_INFO m_Glyphs[] = {\n#define X_GLYPH_DEFINE(text_, role_, mesh_idx_)                                \\\n    { .text = text_, .role = role_, .mesh_idx = mesh_idx_ },\n#include <trx/game/ui/text.def>\n    { .text = nullptr }, // guard\n};\n\nstatic M_GLYPH_MAP_ENTRY *m_GlyphMap = nullptr;\nstatic M_TEXT_MAP_ENTRY *m_TextMap = nullptr;\n\nOBJECT_ID m_FontObjects[M_FONT_COUNT] = {\n    [M_FONT_DEFAULT] = O_ALPHABET,\n    [M_FONT_SMALL] = O_ALPHABET_SMALL,\n};\n\nstatic RGB_888 m_ColorLight[M_MAX_COLOR] = {\n    // clang-format off\n    [0]  = { 0xFF, 0xFF, 0xFF },\n    [1]  = { 0xB0, 0xB0, 0x00 },\n    [2]  = { 0xA0, 0xA0, 0xA0 },\n    [3]  = { 0xFF, 0x60, 0x60 },\n    [4]  = { 0x80, 0x80, 0xFF },\n    [5]  = { 0xC0, 0x80, 0x40 },\n    [6]  = { 0xB6, 0xD1, 0x64 },\n    [7]  = { 0xC0, 0xFF, 0xC0 },\n    [8]  = { 0xFF, 0xFF, 0xFF },\n    [9]  = { 0xFF, 0x00, 0xFF },\n    [10] = { 0xFF, 0x00, 0xFF },\n    [11] = { 0xFF, 0x00, 0xFF },\n    [12] = { 0x80, 0x80, 0x80 },\n    // clang-format on\n};\n\nstatic RGB_888 m_ColorDark[M_MAX_COLOR] = {\n    // clang-format off\n    [0]  = { 0x80, 0x80, 0x80 },\n    [1]  = { 0x50, 0x50, 0x00 },\n    [2]  = { 0x18, 0x18, 0x18 },\n    [3]  = { 0x18, 0x00, 0x00 },\n    [4]  = { 0x00, 0x00, 0x18 },\n    [5]  = { 0x40, 0x10, 0x00 },\n    [6]  = { 0xB6, 0x20, 0x13 },\n    [7]  = { 0xC0, 0xFF, 0xC0 },\n    [8]  = { 0xFF, 0xFF, 0xFF },\n    [9]  = { 0x3F, 0x00, 0x3F },\n    [10] = { 0x3F, 0x00, 0x3F },\n    [11] = { 0x3F, 0x00, 0x3F },\n    [12] = { 0x80, 0x80, 0x80 },\n    // clang-format on\n};\n\nstatic RGBA_F m_TextColor[M_MAX_COLOR][4] = {};\n\nstatic float M_ScaleScreen(const float value)\n{\n    return UI_Scaler_Calc(value, UI_SCALER_TARGET_TEXT);\n}\n\nstatic float M_ScaleNeutral(const float value)\n{\n    return value * g_Config.ui.text_scale;\n}\n\nstatic RGBA_F M_ToRGBA_F(const RGB_888 color)\n{\n    return (RGBA_F) {\n        .r = color.r / 255.0f,\n        .g = color.g / 255.0f,\n        .b = color.b / 255.0f,\n        .a = 1.0f,\n    };\n}\n\nstatic int32_t M_HasGlyph(const M_FONT font, const M_GLYPH_INFO *const glyph)\n{\n    return glyph->width[font] > 0;\n}\n\nstatic int32_t M_GetGlyphWidth(\n    const M_FONT font, const M_GLYPH_INFO *const glyph)\n{\n    // Non-breaking space\n    if (strcmp(glyph->text, \" \") == 0) {\n        return M_WORD_SPACING;\n    }\n\n    if (glyph->role == GLYPH_SECRET) {\n        return 16;\n    }\n\n    if (glyph->mesh_idx != -1\n        && (glyph->role == GLYPH_TEXT || glyph->role == GLYPH_ICON\n            || glyph->role == GLYPH_REVIEW_MARKER)) {\n        const OBJECT *const object = Object_Get(m_FontObjects[font]);\n        if (!object->loaded) {\n            return -1;\n        }\n        if (glyph->mesh_idx >= ABS(object->mesh_count)) {\n            return -1;\n        }\n        const SPRITE_TEXTURE *const sprite =\n            Output_GetSpriteTexture(object->mesh_idx + glyph->mesh_idx);\n        if (sprite == nullptr) {\n            return -1;\n        }\n        if (sprite->x1 - sprite->x0 == 0 && sprite->width / 255 == 1) {\n            // Just a placeholder glyph necessary for indexing of other glyphs\n            return -1;\n        }\n        return sprite->width / 255;\n    }\n\n    return 0;\n}\n\nstatic const M_GLYPH_INFO **M_Decompose(\n    const char *const content, size_t *const out_glyph_count)\n{\n    // Count number of characters\n    size_t glyph_count = 0;\n    const char *content_ptr = content;\n    while (*content_ptr != '\\0') {\n        const size_t glyph_size = String_GetCharByteSize(content_ptr);\n        content_ptr += glyph_size;\n        glyph_count++;\n    }\n\n    // Assign glyphs using hash table\n    const M_GLYPH_INFO **glyphs =\n        Memory_Alloc((glyph_count + 1) * sizeof(M_GLYPH_INFO *));\n    content_ptr = content;\n    const M_GLYPH_INFO **glyph_ptr = glyphs;\n    while (*content_ptr != '\\0') {\n        const size_t glyph_size = String_GetCharByteSize(content_ptr);\n        const char *const key_buf =\n            String_FormatStatic(\"%.*s\", (int)glyph_size, content_ptr);\n        M_GLYPH_MAP_ENTRY *entry;\n        HASH_FIND_STR(m_GlyphMap, key_buf, entry);\n\n        if (entry != nullptr) {\n            *glyph_ptr++ = entry->glyph;\n        } else {\n            LOG_WARNING(\"Unknown glyph: %s\", key_buf);\n            glyph_count--;\n        }\n\n        content_ptr += glyph_size;\n    }\n\n    if (out_glyph_count != nullptr) {\n        *out_glyph_count = glyph_count;\n    }\n\n    // guard\n    *glyph_ptr++ = nullptr;\n    return glyphs;\n}\n\nstatic const M_GLYPH_INFO **M_DecomposeWithCache(\n    const char *const content, size_t *const out_glyph_count)\n{\n    M_TEXT_MAP_ENTRY *entry;\n    HASH_FIND_STR(m_TextMap, content, entry);\n    if (entry == nullptr) {\n        entry = Memory_Alloc(sizeof(M_TEXT_MAP_ENTRY));\n        entry->text = Memory_DupStr(content);\n        entry->glyphs = M_Decompose(content, &entry->glyph_count);\n        HASH_ADD_STR(m_TextMap, text, entry);\n    }\n    if (out_glyph_count != nullptr) {\n        *out_glyph_count = entry->glyph_count;\n    }\n    return entry->glyphs;\n}\n\n// Replace input placeholder glyph with the actual keyboard glyph for the\n// current binding\nstatic const M_GLYPH_INFO *M_GetResolvedGlyph(const M_GLYPH_INFO *glyph)\n{\n    if (glyph->role != GLYPH_INPUT) {\n        return glyph;\n    }\n    const char *const key_name = Input_GetKeyName(\n        g_Config.input.backend, g_Config.input.layout[g_Config.input.backend],\n        glyph->input_role, 0);\n    // NOTE: this aliasing approach assumes that Input_GetKeyName returns\n    // text that resolves to a single glyph.\n    M_GLYPH_MAP_ENTRY *entry = nullptr;\n    if (key_name != nullptr) {\n        HASH_FIND_STR(m_GlyphMap, key_name, entry);\n    }\n    if (entry == nullptr) {\n        HASH_FIND_STR(m_GlyphMap, \"?\", entry);\n    }\n    return entry != nullptr ? entry->glyph : nullptr;\n}\n\nstatic int32_t M_DetectBulletIndent(\n    const M_GLYPH_INFO **glyphs, const size_t glyph_count, const size_t idx)\n{\n    size_t scan = idx;\n    int32_t leading_spaces = 0;\n    while (scan < glyph_count && glyphs[scan]->role == GLYPH_SPACE) {\n        leading_spaces++;\n        scan++;\n    }\n    if (scan + 1 < glyph_count && glyphs[scan]->role == GLYPH_TEXT\n        && glyphs[scan]->text[0] == '-' && glyphs[scan]->text[1] == '\\0'\n        && glyphs[scan + 1]->role == GLYPH_SPACE) {\n        return leading_spaces + 2;\n    }\n    return 0;\n}\n\nstatic void M_EmitIndent(\n    char *const dst, size_t *const out_len, const int32_t indent,\n    const float space_width, float *const cur_width)\n{\n    for (int32_t s = 0; s < indent; s++) {\n        if (dst != nullptr) {\n            dst[*out_len] = ' ';\n        }\n        (*out_len)++;\n    }\n    *cur_width += indent * space_width;\n}\n\nstatic void M_EmitNewline(\n    char *const dst, size_t *const out_len, const int32_t indent,\n    const float space_width, float *const cur_width)\n{\n    if (dst != nullptr) {\n        dst[*out_len] = '\\n';\n    }\n    (*out_len)++;\n    *cur_width = 0.0f;\n    if (indent > 0) {\n        M_EmitIndent(dst, out_len, indent, space_width, cur_width);\n    }\n}\n\nstatic size_t M_WordWrap(\n    const M_GLYPH_INFO **glyphs, const size_t glyph_count, const float scale_f,\n    const float max_width, char *const dst)\n{\n    size_t out_len = 0;\n    float cur_width = 0.0f;\n    int32_t bullet_indent = 0;\n\n    const float space_width = M_WORD_SPACING * scale_f;\n\n#define L_CONCAT_CHAR(part)                                                    \\\n    if (dst != nullptr) {                                                      \\\n        dst[out_len] = part;                                                   \\\n    }                                                                          \\\n    out_len++;\n#define L_CONCAT_STR(part)                                                     \\\n    if (dst != nullptr) {                                                      \\\n        strcpy(dst + out_len, part);                                           \\\n    }                                                                          \\\n    out_len += strlen(part);\n\n    M_FONT current_font = M_FONT_DEFAULT;\n\n    // Iterate glyphs for wrapping\n    for (size_t i = 0; i < glyph_count; i++) {\n        const M_GLYPH_INFO *const glyph = M_GetResolvedGlyph(glyphs[i]);\n        if (glyph == nullptr) {\n            continue;\n        }\n\n        if (cur_width == 0.0f && bullet_indent == 0) {\n            bullet_indent = M_DetectBulletIndent(glyphs, glyph_count, i);\n        }\n\n        if (glyph->role == GLYPH_FONT_MARKER) {\n            current_font = glyph->mesh_idx;\n        } else if (glyph->role == GLYPH_NEW_LINE) {\n            L_CONCAT_CHAR('\\n')\n            cur_width = 0.0f;\n            bullet_indent = 0;\n        } else if (glyph->role == GLYPH_NEW_PAGE) {\n            L_CONCAT_CHAR('\\f')\n            cur_width = 0.0f;\n            bullet_indent = 0;\n        } else if (glyph->role == GLYPH_SPACE) {\n            const float w = M_WORD_SPACING * scale_f;\n            if (cur_width + w > max_width) {\n                M_EmitNewline(\n                    dst, &out_len, bullet_indent, space_width, &cur_width);\n            } else {\n                L_CONCAT_CHAR(' ')\n                cur_width += w;\n            }\n        } else if (\n            glyph->role == GLYPH_REVIEW_MARKER\n            && !g_Config.debug.enable_review_markers) {\n            continue;\n        } else {\n            // Gather next word glyphs\n            size_t word_len = 0;\n            for (size_t j = i; j < glyph_count; j++) {\n                if (glyphs[i + word_len]->role == GLYPH_SPACE\n                    || glyphs[i + word_len]->role == GLYPH_NEW_LINE\n                    || glyphs[i + word_len]->role == GLYPH_NEW_PAGE) {\n                    break;\n                }\n                word_len++;\n            }\n\n            // Compute width (sum widths + spacing)\n            float word_width = 0.0f;\n            for (size_t j = i; j < i + word_len; j++) {\n                word_width += M_LETTER_SPACING;\n                word_width += glyphs[j]->width[current_font];\n            }\n            if (word_width > 0) {\n                word_width -= M_LETTER_SPACING;\n            }\n            word_width *= scale_f;\n\n            // Wrap line if needed\n            if (cur_width + word_width > max_width) {\n                if (cur_width > 0.0f) {\n                    M_EmitNewline(\n                        dst, &out_len, bullet_indent, space_width, &cur_width);\n                }\n\n                // Break word if longer than line\n                if (word_width > max_width) {\n                    for (size_t j = i; j < i + word_len; j++) {\n                        const M_GLYPH_INFO *const next_glyph = glyphs[j];\n                        const float glyph_width =\n                            (next_glyph->width[current_font] + M_LETTER_SPACING)\n                            * scale_f;\n                        if (cur_width + glyph_width > max_width) {\n                            M_EmitNewline(\n                                dst, &out_len, bullet_indent, space_width,\n                                &cur_width);\n                        }\n                        L_CONCAT_STR(next_glyph->text)\n                        cur_width += glyph_width;\n                    }\n                } else {\n                    for (size_t j = i; j < i + word_len; j++) {\n                        const M_GLYPH_INFO *const next_glyph = glyphs[j];\n                        L_CONCAT_STR(next_glyph->text)\n                    }\n                    cur_width = word_width;\n                }\n            } else {\n                // Copy word as is\n                for (size_t j = i; j < i + word_len; j++) {\n                    const M_GLYPH_INFO *const next_glyph = glyphs[j];\n                    L_CONCAT_STR(next_glyph->text)\n                }\n                cur_width += word_width;\n            }\n\n            // Skip forward the characters, respecting the default loop\n            // accumulator\n            i += word_len - 1;\n        }\n    }\n\n    L_CONCAT_CHAR('\\0')\n\n#undef L_CONCAT_CHAR\n#undef L_CONCAT_STR\n    return out_len;\n}\n\nstatic void M_Process(\n    const char *const text, float *const out_w, float *const out_h,\n    const UI_TEXT_SETTINGS settings, const float base_x, const float base_y,\n    float (*const scale_func)(float),\n    void (*const draw_func)(\n        int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, const RGBA_F[4]))\n{\n    if (text == nullptr) {\n        return;\n    }\n\n    const M_GLYPH_INFO **glyphs = M_DecomposeWithCache(text, nullptr);\n    ASSERT(glyphs != nullptr);\n\n    const float scale = scale_func(UI_TEXT_BASE_SCALE * settings.scale);\n\n    float x = scale_func(base_x / g_Config.ui.text_scale);\n    float y = scale_func(\n        base_y / g_Config.ui.text_scale + settings.scale * UI_TEXT_HEIGHT);\n    int32_t z = settings.z;\n\n    float max_width = 0.0f;\n    const float start_x = x;\n\n    M_FONT current_font = M_FONT_DEFAULT;\n    int32_t color_idx = 0;\n    int32_t prev_color_idx = color_idx;\n    bool visible = true;\n\n    const M_GLYPH_INFO **glyph_ptr = glyphs;\n    while (*glyph_ptr != nullptr) {\n        const M_GLYPH_INFO *const glyph = M_GetResolvedGlyph(*glyph_ptr);\n        if (glyph == nullptr) {\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_REVIEW_MARKER\n            && !g_Config.debug.enable_review_markers) {\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_VISIBILITY_MARKER) {\n            visible = glyph->mesh_idx;\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_FONT_MARKER) {\n            current_font = glyph->mesh_idx;\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_DIM_MARKER) {\n            if (glyph->mesh_idx != 0) {\n                prev_color_idx = color_idx;\n                color_idx = M_DIM_COLOR;\n            } else {\n                color_idx = prev_color_idx;\n            }\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_COLOR_MARKER) {\n            if (glyph->mesh_idx != -1) {\n                prev_color_idx = color_idx;\n                color_idx = glyph->mesh_idx;\n            } else {\n                color_idx = prev_color_idx;\n            }\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_NEW_LINE || glyph->role == GLYPH_NEW_PAGE) {\n            y += UI_TEXT_HEIGHT * scale / UI_TEXT_BASE_SCALE;\n            x = start_x;\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_SPACE) {\n            if (glyph_ptr[1] == nullptr\n                || (glyph_ptr[1]->role != GLYPH_NEW_LINE\n                    && glyph_ptr[1]->role != GLYPH_NEW_PAGE)) {\n                x += M_WORD_SPACING * scale / UI_TEXT_BASE_SCALE;\n            }\n            goto loop_end;\n        }\n\n        if (glyph->role == GLYPH_SECRET) {\n            const int16_t sprite_idx =\n                Object_Get(O_SECRET_1 + glyph->mesh_idx)->mesh_idx;\n            const SPRITE_TEXTURE *const sprite =\n                Output_GetSpriteTexture(sprite_idx);\n            const float input_scale_h =\n                settings.scale / (sprite->x1 - sprite->x0);\n            const float input_scale_v =\n                settings.scale / (sprite->y1 - sprite->y0);\n            const float input_scale = MIN(input_scale_h, input_scale_v);\n            const float output_scale = scale_func(\n                UI_TEXT_BASE_SCALE * glyph->width[current_font] * input_scale);\n            if (visible && draw_func != nullptr) {\n                draw_func(\n                    x + scale_func(10), y, z, output_scale, output_scale,\n                    sprite_idx, m_TextColor[color_idx]);\n            }\n            x += glyph->width[current_font] * scale / UI_TEXT_BASE_SCALE;\n            goto loop_end;\n        }\n\n        M_FONT glyph_font = current_font;\n        if (glyph_font == M_FONT_SMALL && !M_HasGlyph(glyph_font, glyph)) {\n            glyph_font = M_FONT_DEFAULT;\n        }\n\n        float spacing = glyph->width[glyph_font];\n        if (glyph_ptr[1] != nullptr && glyph_ptr[1]->role != GLYPH_NEW_LINE\n            && glyph_ptr[1]->role != GLYPH_NEW_PAGE) {\n            spacing += M_LETTER_SPACING;\n        }\n\n        if (glyph->role == GLYPH_TEXT && glyph->mesh_idx < 0) {\n            // Non-breaking space or other non-rendered text glyphs.\n            x += spacing * scale / UI_TEXT_BASE_SCALE;\n            goto loop_end;\n        }\n\n        if (visible && draw_func != nullptr) {\n            const OBJECT *object = Object_Get(m_FontObjects[glyph_font]);\n            draw_func(\n                x, y, z, scale, scale, object->mesh_idx + glyph->mesh_idx,\n                m_TextColor[color_idx]);\n        }\n\n        x += spacing * scale / UI_TEXT_BASE_SCALE;\n\n    loop_end:\n        max_width = MAX(max_width, x);\n        glyph_ptr++;\n    }\n\n    if (out_w != nullptr) {\n        *out_w = max_width;\n    }\n\n    if (out_h != nullptr) {\n        *out_h = y;\n    }\n}\n\nvoid UI_InitText(void)\n{\n    // Convert the linear array coming from the .def macros to a hash lookup\n    // table for faster text-to-glyph resolution.\n    for (M_GLYPH_INFO *glyph_ptr = m_Glyphs; glyph_ptr->text != nullptr;\n         glyph_ptr++) {\n        // mark static glyphs as non-input\n        M_GLYPH_MAP_ENTRY *const hash_entry = Memory_Alloc(sizeof(*hash_entry));\n        hash_entry->glyph = glyph_ptr;\n        HASH_ADD_KEYPTR(\n            hh, m_GlyphMap, glyph_ptr->text, strlen(glyph_ptr->text),\n            hash_entry);\n    }\n\n    // Create dynamic glyphs for \"{key <role>}\" tokens; resolution happens when\n    // drawing/wrapping\n    for (INPUT_ROLE role = 0; role < INPUT_ROLE_NUMBER_OF; role++) {\n        const char *role_str =\n            EnumMap_ToString(ENUM_MAP_NAME(INPUT_ROLE), role);\n        if (role_str == nullptr || *role_str == '\\0') {\n            continue;\n        }\n        M_GLYPH_INFO *input_glyph = Memory_Alloc(sizeof(*input_glyph));\n        input_glyph->text = String_Format(\"\\\\{input %s}\", role_str);\n        input_glyph->role = GLYPH_INPUT;\n        input_glyph->input_role = role;\n        for (M_FONT font = 0; font < M_FONT_COUNT; font++) {\n            input_glyph->width[font] = 0;\n        }\n        M_GLYPH_MAP_ENTRY *entry = Memory_Alloc(sizeof(*entry));\n        entry->glyph = input_glyph;\n        HASH_ADD_KEYPTR(\n            hh, m_GlyphMap, input_glyph->text, strlen(input_glyph->text),\n            entry);\n    }\n}\n\nvoid UI_LoadText(void)\n{\n    for (int32_t i = 0; i < M_MAX_COLOR; i++) {\n        m_TextColor[i][0] = M_ToRGBA_F(m_ColorLight[i]);\n        m_TextColor[i][1] = M_ToRGBA_F(m_ColorLight[i]);\n        if (g_TRVersion == 3) {\n            m_TextColor[i][2] = M_ToRGBA_F(m_ColorDark[i]);\n            m_TextColor[i][3] = M_ToRGBA_F(m_ColorDark[i]);\n        } else {\n            m_TextColor[i][2] = M_ToRGBA_F(m_ColorLight[i]);\n            m_TextColor[i][3] = M_ToRGBA_F(m_ColorLight[i]);\n        }\n    }\n\n    for (M_FONT font = 0; font < M_FONT_COUNT; font++) {\n        for (M_GLYPH_INFO *glyph_ptr = m_Glyphs; glyph_ptr->text != nullptr;\n             glyph_ptr++) {\n            glyph_ptr->width[font] = M_GetGlyphWidth(font, glyph_ptr);\n        }\n    }\n}\n\nvoid UI_ShutdownText(void)\n{\n    {\n        M_GLYPH_MAP_ENTRY *current, *tmp;\n        HASH_ITER(hh, m_GlyphMap, current, tmp)\n        {\n            if (current->glyph->role == GLYPH_INPUT) {\n                Memory_FreePointer(&current->glyph->text);\n                Memory_FreePointer(&current->glyph);\n            }\n            HASH_DEL(m_GlyphMap, current);\n            Memory_Free(current);\n        }\n    }\n\n    {\n        M_TEXT_MAP_ENTRY *current, *tmp;\n        HASH_ITER(hh, m_TextMap, current, tmp)\n        {\n            Memory_FreePointer(&current->text);\n            Memory_FreePointer(&current->glyphs);\n            HASH_DEL(m_TextMap, current);\n            Memory_FreePointer(&current);\n        }\n    }\n}\n\nvoid UI_Text_Measure(\n    const char *const text, float *const out_w, float *const out_h,\n    const UI_TEXT_SETTINGS settings)\n{\n    M_Process(\n        text, out_w, out_h, settings, 0.0f, 0.0f, M_ScaleNeutral, nullptr);\n}\n\nvoid UI_Text_Draw(\n    const char *const text, const float base_x, const float base_y,\n    const UI_TEXT_SETTINGS settings)\n{\n    M_Process(\n        text, nullptr, nullptr, settings, base_x,\n        base_y - g_Config.ui.text_scale, M_ScaleScreen,\n        UI_ScheduleDrawScreenSprite);\n}\n\nchar *UI_Text_WordWrap(\n    const char *text, const float scale, const float max_width)\n{\n    if (text == nullptr || max_width <= 0) {\n        return nullptr;\n    }\n\n    size_t glyph_count = 0;\n    const M_GLYPH_INFO **glyphs = M_DecomposeWithCache(text, &glyph_count);\n\n    const float scale_f = scale * g_Config.ui.text_scale;\n    size_t len = M_WordWrap(glyphs, glyph_count, scale_f, max_width, nullptr);\n    char *const wrapped_text = Memory_Alloc(len);\n    M_WordWrap(glyphs, glyph_count, scale_f, max_width, wrapped_text);\n    return wrapped_text;\n}\n\nchar *UI_Text_FilterGlyphs(const char *const text)\n{\n    if (text == nullptr) {\n        return nullptr;\n    }\n    const size_t in_len = strlen(text);\n    char *out = Memory_Alloc(in_len + 1);\n    size_t out_len = 0;\n    const char *p = text;\n    while (*p != '\\0') {\n        const size_t sz = String_GetCharByteSize(p);\n        const char *const key_buf = String_FormatStatic(\"%.*s\", (int32_t)sz, p);\n        M_GLYPH_MAP_ENTRY *entry = nullptr;\n        HASH_FIND_STR(m_GlyphMap, key_buf, entry);\n        if (entry != nullptr) {\n            memcpy(out + out_len, p, sz);\n            out_len += sz;\n        }\n        p += sz;\n    }\n    out[out_len] = '\\0';\n    return out;\n}\n"
  },
  {
    "path": "src/trx/game/ui/text.def",
    "content": "X_GLYPH_DEFINE(\"\\n\", GLYPH_NEW_LINE, -1)\nX_GLYPH_DEFINE(\"\\f\", GLYPH_NEW_PAGE, -1)\nX_GLYPH_DEFINE(\" \", GLYPH_SPACE, -1)\nX_GLYPH_DEFINE(\" \", GLYPH_TEXT, -1)\nX_GLYPH_DEFINE(\"\\\\{i}\", GLYPH_VISIBILITY_MARKER, 0)\nX_GLYPH_DEFINE(\"\\\\{/i}\", GLYPH_VISIBILITY_MARKER, 1)\nX_GLYPH_DEFINE(\"\\\\{dim}\", GLYPH_DIM_MARKER, 1)\nX_GLYPH_DEFINE(\"\\\\{/dim}\", GLYPH_DIM_MARKER, 0)\nX_GLYPH_DEFINE(\"\\\\{color 0}\", GLYPH_COLOR_MARKER, 0)\nX_GLYPH_DEFINE(\"\\\\{color 1}\", GLYPH_COLOR_MARKER, 1)\nX_GLYPH_DEFINE(\"\\\\{color 2}\", GLYPH_COLOR_MARKER, 2)\nX_GLYPH_DEFINE(\"\\\\{color 3}\", GLYPH_COLOR_MARKER, 3)\nX_GLYPH_DEFINE(\"\\\\{color 4}\", GLYPH_COLOR_MARKER, 4)\nX_GLYPH_DEFINE(\"\\\\{color 5}\", GLYPH_COLOR_MARKER, 5)\nX_GLYPH_DEFINE(\"\\\\{color 6}\", GLYPH_COLOR_MARKER, 6)\nX_GLYPH_DEFINE(\"\\\\{color 7}\", GLYPH_COLOR_MARKER, 7)\nX_GLYPH_DEFINE(\"\\\\{color 8}\", GLYPH_COLOR_MARKER, 8)\nX_GLYPH_DEFINE(\"\\\\{color 9}\", GLYPH_COLOR_MARKER, 9)\nX_GLYPH_DEFINE(\"\\\\{color 10}\", GLYPH_COLOR_MARKER, 10)\nX_GLYPH_DEFINE(\"\\\\{color 11}\", GLYPH_COLOR_MARKER, 11)\nX_GLYPH_DEFINE(\"\\\\{/color}\", GLYPH_COLOR_MARKER, -1)\nX_GLYPH_DEFINE(\"\\\\{secret 1}\", GLYPH_SECRET, 0)\nX_GLYPH_DEFINE(\"\\\\{secret 2}\", GLYPH_SECRET, 1)\nX_GLYPH_DEFINE(\"\\\\{secret 3}\", GLYPH_SECRET, 2)\nX_GLYPH_DEFINE(\"\\\\{small}\", GLYPH_FONT_MARKER, 1)\nX_GLYPH_DEFINE(\"\\\\{/small}\", GLYPH_FONT_MARKER, 0)\n\n#include <trx/game/ui/text_autogen.def>\n"
  },
  {
    "path": "src/trx/game/ui/text.h",
    "content": "#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n\n#define UI_TEXT_HEIGHT 15\n#define UI_TEXT_BASE_SCALE 0x10000\n\ntypedef struct {\n    float scale;\n    int32_t z;\n} UI_TEXT_SETTINGS;\n\n// Initialize and shutdown UI text rendering cache.\nvoid UI_InitText(void);\nvoid UI_ShutdownText(void);\n\n// Observe level load to establish glyph widths.\nvoid UI_LoadText(void);\n\n// Draw the given text at screen coordinates (x, y) with specified settings.\nvoid UI_Text_Draw(\n    const char *text, float x, float y, UI_TEXT_SETTINGS settings);\n\n// Measure the width and height of the given text with specified settings.\nvoid UI_Text_Measure(\n    const char *text, float *out_w, float *out_h, UI_TEXT_SETTINGS settings);\n\n// Wrap a text into multiple lines to fit a specific width in pixels.\nchar *UI_Text_WordWrap(\n    const char *text, const float scale, const float max_width);\n\n// Filter out any characters not present in the glyph map.\n// Returns a newly-allocated string containing only known glyphs.\n// Caller must free the result with Memory_Free*().\nchar *UI_Text_FilterGlyphs(const char *text);\n"
  },
  {
    "path": "src/trx/game/ui/text_autogen.def",
    "content": "// This file is autogenerated. See tools/glyphs/generate_defs for details.\nX_GLYPH_DEFINE(\"a\", GLYPH_TEXT, 26)\nX_GLYPH_DEFINE(\"b\", GLYPH_TEXT, 27)\nX_GLYPH_DEFINE(\"c\", GLYPH_TEXT, 28)\nX_GLYPH_DEFINE(\"d\", GLYPH_TEXT, 29)\nX_GLYPH_DEFINE(\"e\", GLYPH_TEXT, 30)\nX_GLYPH_DEFINE(\"f\", GLYPH_TEXT, 31)\nX_GLYPH_DEFINE(\"g\", GLYPH_TEXT, 32)\nX_GLYPH_DEFINE(\"h\", GLYPH_TEXT, 33)\nX_GLYPH_DEFINE(\"i\", GLYPH_TEXT, 34)\nX_GLYPH_DEFINE(\"j\", GLYPH_TEXT, 35)\nX_GLYPH_DEFINE(\"k\", GLYPH_TEXT, 36)\nX_GLYPH_DEFINE(\"l\", GLYPH_TEXT, 37)\nX_GLYPH_DEFINE(\"m\", GLYPH_TEXT, 38)\nX_GLYPH_DEFINE(\"n\", GLYPH_TEXT, 39)\nX_GLYPH_DEFINE(\"o\", GLYPH_TEXT, 40)\nX_GLYPH_DEFINE(\"p\", GLYPH_TEXT, 41)\nX_GLYPH_DEFINE(\"q\", GLYPH_TEXT, 42)\nX_GLYPH_DEFINE(\"r\", GLYPH_TEXT, 43)\nX_GLYPH_DEFINE(\"s\", GLYPH_TEXT, 44)\nX_GLYPH_DEFINE(\"t\", GLYPH_TEXT, 45)\nX_GLYPH_DEFINE(\"u\", GLYPH_TEXT, 46)\nX_GLYPH_DEFINE(\"v\", GLYPH_TEXT, 47)\nX_GLYPH_DEFINE(\"w\", GLYPH_TEXT, 48)\nX_GLYPH_DEFINE(\"x\", GLYPH_TEXT, 49)\nX_GLYPH_DEFINE(\"y\", GLYPH_TEXT, 50)\nX_GLYPH_DEFINE(\"z\", GLYPH_TEXT, 51)\nX_GLYPH_DEFINE(\"A\", GLYPH_TEXT, 0)\nX_GLYPH_DEFINE(\"B\", GLYPH_TEXT, 1)\nX_GLYPH_DEFINE(\"C\", GLYPH_TEXT, 2)\nX_GLYPH_DEFINE(\"D\", GLYPH_TEXT, 3)\nX_GLYPH_DEFINE(\"E\", GLYPH_TEXT, 4)\nX_GLYPH_DEFINE(\"F\", GLYPH_TEXT, 5)\nX_GLYPH_DEFINE(\"G\", GLYPH_TEXT, 6)\nX_GLYPH_DEFINE(\"H\", GLYPH_TEXT, 7)\nX_GLYPH_DEFINE(\"I\", GLYPH_TEXT, 8)\nX_GLYPH_DEFINE(\"J\", GLYPH_TEXT, 9)\nX_GLYPH_DEFINE(\"K\", GLYPH_TEXT, 10)\nX_GLYPH_DEFINE(\"L\", GLYPH_TEXT, 11)\nX_GLYPH_DEFINE(\"M\", GLYPH_TEXT, 12)\nX_GLYPH_DEFINE(\"N\", GLYPH_TEXT, 13)\nX_GLYPH_DEFINE(\"O\", GLYPH_TEXT, 14)\nX_GLYPH_DEFINE(\"P\", GLYPH_TEXT, 15)\nX_GLYPH_DEFINE(\"Q\", GLYPH_TEXT, 16)\nX_GLYPH_DEFINE(\"R\", GLYPH_TEXT, 17)\nX_GLYPH_DEFINE(\"S\", GLYPH_TEXT, 18)\nX_GLYPH_DEFINE(\"T\", GLYPH_TEXT, 19)\nX_GLYPH_DEFINE(\"U\", GLYPH_TEXT, 20)\nX_GLYPH_DEFINE(\"V\", GLYPH_TEXT, 21)\nX_GLYPH_DEFINE(\"W\", GLYPH_TEXT, 22)\nX_GLYPH_DEFINE(\"X\", GLYPH_TEXT, 23)\nX_GLYPH_DEFINE(\"Y\", GLYPH_TEXT, 24)\nX_GLYPH_DEFINE(\"Z\", GLYPH_TEXT, 25)\nX_GLYPH_DEFINE(\"0\", GLYPH_TEXT, 52)\nX_GLYPH_DEFINE(\"1\", GLYPH_TEXT, 53)\nX_GLYPH_DEFINE(\"2\", GLYPH_TEXT, 54)\nX_GLYPH_DEFINE(\"3\", GLYPH_TEXT, 55)\nX_GLYPH_DEFINE(\"4\", GLYPH_TEXT, 56)\nX_GLYPH_DEFINE(\"5\", GLYPH_TEXT, 57)\nX_GLYPH_DEFINE(\"6\", GLYPH_TEXT, 58)\nX_GLYPH_DEFINE(\"7\", GLYPH_TEXT, 59)\nX_GLYPH_DEFINE(\"8\", GLYPH_TEXT, 60)\nX_GLYPH_DEFINE(\"9\", GLYPH_TEXT, 61)\nX_GLYPH_DEFINE(\"!\", GLYPH_TEXT, 64)\nX_GLYPH_DEFINE(\"\\\"\", GLYPH_TEXT, 82)\nX_GLYPH_DEFINE(\"#\", GLYPH_TEXT, 78)\nX_GLYPH_DEFINE(\"$\", GLYPH_TEXT, 83)\nX_GLYPH_DEFINE(\"%\", GLYPH_TEXT, 84)\nX_GLYPH_DEFINE(\"&\", GLYPH_TEXT, 85)\nX_GLYPH_DEFINE(\"'\", GLYPH_TEXT, 79)\nX_GLYPH_DEFINE(\"(\", GLYPH_TEXT, 86)\nX_GLYPH_DEFINE(\")\", GLYPH_TEXT, 87)\nX_GLYPH_DEFINE(\"*\", GLYPH_TEXT, 88)\nX_GLYPH_DEFINE(\"+\", GLYPH_TEXT, 72)\nX_GLYPH_DEFINE(\",\", GLYPH_TEXT, 63)\nX_GLYPH_DEFINE(\"-\", GLYPH_TEXT, 71)\nX_GLYPH_DEFINE(\".\", GLYPH_TEXT, 62)\nX_GLYPH_DEFINE(\"/\", GLYPH_TEXT, 68)\nX_GLYPH_DEFINE(\":\", GLYPH_TEXT, 73)\nX_GLYPH_DEFINE(\";\", GLYPH_TEXT, 89)\nX_GLYPH_DEFINE(\"<\", GLYPH_TEXT, 90)\nX_GLYPH_DEFINE(\"=\", GLYPH_TEXT, 91)\nX_GLYPH_DEFINE(\">\", GLYPH_TEXT, 110)\nX_GLYPH_DEFINE(\"?\", GLYPH_TEXT, 65)\nX_GLYPH_DEFINE(\"@\", GLYPH_TEXT, 111)\nX_GLYPH_DEFINE(\"[\", GLYPH_TEXT, 66)\nX_GLYPH_DEFINE(\"\\\\\", GLYPH_TEXT, 76)\nX_GLYPH_DEFINE(\"]\", GLYPH_TEXT, 75)\nX_GLYPH_DEFINE(\"^\", GLYPH_TEXT, 112)\nX_GLYPH_DEFINE(\"_\", GLYPH_TEXT, 113)\nX_GLYPH_DEFINE(\"`\", GLYPH_TEXT, 114)\nX_GLYPH_DEFINE(\"{\", GLYPH_TEXT, 115)\nX_GLYPH_DEFINE(\"|\", GLYPH_TEXT, 116)\nX_GLYPH_DEFINE(\"}\", GLYPH_TEXT, 117)\nX_GLYPH_DEFINE(\"~\", GLYPH_TEXT, 118)\nX_GLYPH_DEFINE(\"\\\\{button down}\", GLYPH_ICON, 106)\nX_GLYPH_DEFINE(\"\\\\{button up}\", GLYPH_ICON, 107)\nX_GLYPH_DEFINE(\"\\\\{button left}\", GLYPH_ICON, 108)\nX_GLYPH_DEFINE(\"\\\\{button right}\", GLYPH_ICON, 109)\nX_GLYPH_DEFINE(\"\\\\{button triangle}\", GLYPH_ICON, 93)\nX_GLYPH_DEFINE(\"\\\\{button circle}\", GLYPH_ICON, 94)\nX_GLYPH_DEFINE(\"\\\\{button x}\", GLYPH_ICON, 95)\nX_GLYPH_DEFINE(\"\\\\{button square}\", GLYPH_ICON, 96)\nX_GLYPH_DEFINE(\"\\\\{button empty}\", GLYPH_ICON, 92)\nX_GLYPH_DEFINE(\"\\\\{button l1}\", GLYPH_ICON, 97)\nX_GLYPH_DEFINE(\"\\\\{button r1}\", GLYPH_ICON, 98)\nX_GLYPH_DEFINE(\"\\\\{button l2}\", GLYPH_ICON, 99)\nX_GLYPH_DEFINE(\"\\\\{button r2}\", GLYPH_ICON, 100)\nX_GLYPH_DEFINE(\"\\\\{icon sound}\", GLYPH_ICON, 101)\nX_GLYPH_DEFINE(\"\\\\{icon music}\", GLYPH_ICON, 102)\nX_GLYPH_DEFINE(\"\\\\{ammo shotgun}\", GLYPH_ICON, 103)\nX_GLYPH_DEFINE(\"\\\\{ammo magnums}\", GLYPH_ICON, 104)\nX_GLYPH_DEFINE(\"\\\\{ammo uzis}\", GLYPH_ICON, 105)\nX_GLYPH_DEFINE(\"\\\\{arrow up}\", GLYPH_ICON, 80)\nX_GLYPH_DEFINE(\"\\\\{arrow down}\", GLYPH_ICON, 81)\nX_GLYPH_DEFINE(\"\\\\{review}\", GLYPH_REVIEW_MARKER, 119)\nX_GLYPH_DEFINE(\"\\\\{grave accent}\", GLYPH_TEXT, 77)\nX_GLYPH_DEFINE(\"\\\\{acute accent}\", GLYPH_TEXT, 70)\nX_GLYPH_DEFINE(\"\\\\{circumflex accent}\", GLYPH_TEXT, 69)\nX_GLYPH_DEFINE(\"\\\\{circumflex}\", GLYPH_TEXT, 120)\nX_GLYPH_DEFINE(\"\\\\{macron}\", GLYPH_TEXT, 121)\nX_GLYPH_DEFINE(\"\\\\{breve}\", GLYPH_TEXT, 122)\nX_GLYPH_DEFINE(\"\\\\{dot above}\", GLYPH_TEXT, 123)\nX_GLYPH_DEFINE(\"\\\\{umlaut}\", GLYPH_TEXT, 67)\nX_GLYPH_DEFINE(\"\\\\{caron}\", GLYPH_TEXT, 124)\nX_GLYPH_DEFINE(\"\\\\{ring above}\", GLYPH_TEXT, 125)\nX_GLYPH_DEFINE(\"\\\\{tilde}\", GLYPH_TEXT, 126)\nX_GLYPH_DEFINE(\"\\\\{double acute accent}\", GLYPH_TEXT, 127)\nX_GLYPH_DEFINE(\"\\\\{acute umlaut}\", GLYPH_TEXT, 128)\nX_GLYPH_DEFINE(\"¡\", GLYPH_TEXT, 129)\nX_GLYPH_DEFINE(\"¢\", GLYPH_TEXT, 130)\nX_GLYPH_DEFINE(\"£\", GLYPH_TEXT, 131)\nX_GLYPH_DEFINE(\"¤\", GLYPH_TEXT, 132)\nX_GLYPH_DEFINE(\"¥\", GLYPH_TEXT, 133)\nX_GLYPH_DEFINE(\"¦\", GLYPH_TEXT, 134)\nX_GLYPH_DEFINE(\"§\", GLYPH_TEXT, 135)\nX_GLYPH_DEFINE(\"©\", GLYPH_TEXT, 136)\nX_GLYPH_DEFINE(\"ª\", GLYPH_TEXT, 137)\nX_GLYPH_DEFINE(\"«\", GLYPH_TEXT, 138)\nX_GLYPH_DEFINE(\"¬\", GLYPH_TEXT, 139)\nX_GLYPH_DEFINE(\"®\", GLYPH_TEXT, 140)\nX_GLYPH_DEFINE(\"°\", GLYPH_TEXT, 141)\nX_GLYPH_DEFINE(\"±\", GLYPH_TEXT, 142)\nX_GLYPH_DEFINE(\"²\", GLYPH_TEXT, 143)\nX_GLYPH_DEFINE(\"³\", GLYPH_TEXT, 144)\nX_GLYPH_DEFINE(\"µ\", GLYPH_TEXT, 145)\nX_GLYPH_DEFINE(\"¶\", GLYPH_TEXT, 146)\nX_GLYPH_DEFINE(\"·\", GLYPH_TEXT, 147)\nX_GLYPH_DEFINE(\"¹\", GLYPH_TEXT, 148)\nX_GLYPH_DEFINE(\"º\", GLYPH_TEXT, 149)\nX_GLYPH_DEFINE(\"»\", GLYPH_TEXT, 150)\nX_GLYPH_DEFINE(\"¼\", GLYPH_TEXT, 151)\nX_GLYPH_DEFINE(\"½\", GLYPH_TEXT, 152)\nX_GLYPH_DEFINE(\"¾\", GLYPH_TEXT, 153)\nX_GLYPH_DEFINE(\"¿\", GLYPH_TEXT, 154)\nX_GLYPH_DEFINE(\"À\", GLYPH_TEXT, 155)\nX_GLYPH_DEFINE(\"Á\", GLYPH_TEXT, 156)\nX_GLYPH_DEFINE(\"Â\", GLYPH_TEXT, 157)\nX_GLYPH_DEFINE(\"Ã\", GLYPH_TEXT, 158)\nX_GLYPH_DEFINE(\"Ä\", GLYPH_TEXT, 159)\nX_GLYPH_DEFINE(\"Å\", GLYPH_TEXT, 160)\nX_GLYPH_DEFINE(\"Æ\", GLYPH_TEXT, 161)\nX_GLYPH_DEFINE(\"Ç\", GLYPH_TEXT, 162)\nX_GLYPH_DEFINE(\"È\", GLYPH_TEXT, 163)\nX_GLYPH_DEFINE(\"É\", GLYPH_TEXT, 164)\nX_GLYPH_DEFINE(\"Ê\", GLYPH_TEXT, 165)\nX_GLYPH_DEFINE(\"Ë\", GLYPH_TEXT, 166)\nX_GLYPH_DEFINE(\"Ì\", GLYPH_TEXT, 167)\nX_GLYPH_DEFINE(\"Í\", GLYPH_TEXT, 168)\nX_GLYPH_DEFINE(\"Î\", GLYPH_TEXT, 169)\nX_GLYPH_DEFINE(\"Ï\", GLYPH_TEXT, 170)\nX_GLYPH_DEFINE(\"Ð\", GLYPH_TEXT, 171)\nX_GLYPH_DEFINE(\"Ñ\", GLYPH_TEXT, 172)\nX_GLYPH_DEFINE(\"Ò\", GLYPH_TEXT, 173)\nX_GLYPH_DEFINE(\"Ó\", GLYPH_TEXT, 174)\nX_GLYPH_DEFINE(\"Ô\", GLYPH_TEXT, 175)\nX_GLYPH_DEFINE(\"Õ\", GLYPH_TEXT, 176)\nX_GLYPH_DEFINE(\"Ö\", GLYPH_TEXT, 177)\nX_GLYPH_DEFINE(\"×\", GLYPH_TEXT, 178)\nX_GLYPH_DEFINE(\"Ø\", GLYPH_TEXT, 179)\nX_GLYPH_DEFINE(\"Ù\", GLYPH_TEXT, 180)\nX_GLYPH_DEFINE(\"Ú\", GLYPH_TEXT, 181)\nX_GLYPH_DEFINE(\"Û\", GLYPH_TEXT, 182)\nX_GLYPH_DEFINE(\"Ü\", GLYPH_TEXT, 183)\nX_GLYPH_DEFINE(\"Ý\", GLYPH_TEXT, 184)\nX_GLYPH_DEFINE(\"Þ\", GLYPH_TEXT, 185)\nX_GLYPH_DEFINE(\"ß\", GLYPH_TEXT, 74)\nX_GLYPH_DEFINE(\"à\", GLYPH_TEXT, 186)\nX_GLYPH_DEFINE(\"á\", GLYPH_TEXT, 187)\nX_GLYPH_DEFINE(\"â\", GLYPH_TEXT, 188)\nX_GLYPH_DEFINE(\"ã\", GLYPH_TEXT, 189)\nX_GLYPH_DEFINE(\"ä\", GLYPH_TEXT, 190)\nX_GLYPH_DEFINE(\"å\", GLYPH_TEXT, 191)\nX_GLYPH_DEFINE(\"æ\", GLYPH_TEXT, 192)\nX_GLYPH_DEFINE(\"ç\", GLYPH_TEXT, 193)\nX_GLYPH_DEFINE(\"è\", GLYPH_TEXT, 194)\nX_GLYPH_DEFINE(\"é\", GLYPH_TEXT, 195)\nX_GLYPH_DEFINE(\"ê\", GLYPH_TEXT, 196)\nX_GLYPH_DEFINE(\"ë\", GLYPH_TEXT, 197)\nX_GLYPH_DEFINE(\"ì\", GLYPH_TEXT, 198)\nX_GLYPH_DEFINE(\"í\", GLYPH_TEXT, 199)\nX_GLYPH_DEFINE(\"î\", GLYPH_TEXT, 200)\nX_GLYPH_DEFINE(\"ï\", GLYPH_TEXT, 201)\nX_GLYPH_DEFINE(\"ð\", GLYPH_TEXT, 202)\nX_GLYPH_DEFINE(\"ñ\", GLYPH_TEXT, 203)\nX_GLYPH_DEFINE(\"ò\", GLYPH_TEXT, 204)\nX_GLYPH_DEFINE(\"ó\", GLYPH_TEXT, 205)\nX_GLYPH_DEFINE(\"ô\", GLYPH_TEXT, 206)\nX_GLYPH_DEFINE(\"õ\", GLYPH_TEXT, 207)\nX_GLYPH_DEFINE(\"ö\", GLYPH_TEXT, 208)\nX_GLYPH_DEFINE(\"÷\", GLYPH_TEXT, 209)\nX_GLYPH_DEFINE(\"ø\", GLYPH_TEXT, 210)\nX_GLYPH_DEFINE(\"ù\", GLYPH_TEXT, 211)\nX_GLYPH_DEFINE(\"ú\", GLYPH_TEXT, 212)\nX_GLYPH_DEFINE(\"û\", GLYPH_TEXT, 213)\nX_GLYPH_DEFINE(\"ü\", GLYPH_TEXT, 214)\nX_GLYPH_DEFINE(\"ý\", GLYPH_TEXT, 215)\nX_GLYPH_DEFINE(\"þ\", GLYPH_TEXT, 216)\nX_GLYPH_DEFINE(\"ÿ\", GLYPH_TEXT, 217)\nX_GLYPH_DEFINE(\"Ā\", GLYPH_TEXT, 218)\nX_GLYPH_DEFINE(\"ā\", GLYPH_TEXT, 219)\nX_GLYPH_DEFINE(\"Ă\", GLYPH_TEXT, 220)\nX_GLYPH_DEFINE(\"ă\", GLYPH_TEXT, 221)\nX_GLYPH_DEFINE(\"Ą\", GLYPH_TEXT, 222)\nX_GLYPH_DEFINE(\"ą\", GLYPH_TEXT, 223)\nX_GLYPH_DEFINE(\"Ć\", GLYPH_TEXT, 224)\nX_GLYPH_DEFINE(\"ć\", GLYPH_TEXT, 225)\nX_GLYPH_DEFINE(\"Ĉ\", GLYPH_TEXT, 226)\nX_GLYPH_DEFINE(\"ĉ\", GLYPH_TEXT, 227)\nX_GLYPH_DEFINE(\"Ċ\", GLYPH_TEXT, 228)\nX_GLYPH_DEFINE(\"ċ\", GLYPH_TEXT, 229)\nX_GLYPH_DEFINE(\"Č\", GLYPH_TEXT, 230)\nX_GLYPH_DEFINE(\"č\", GLYPH_TEXT, 231)\nX_GLYPH_DEFINE(\"Ď\", GLYPH_TEXT, 232)\nX_GLYPH_DEFINE(\"ď\", GLYPH_TEXT, 233)\nX_GLYPH_DEFINE(\"Đ\", GLYPH_TEXT, 234)\nX_GLYPH_DEFINE(\"đ\", GLYPH_TEXT, 235)\nX_GLYPH_DEFINE(\"Ē\", GLYPH_TEXT, 236)\nX_GLYPH_DEFINE(\"ē\", GLYPH_TEXT, 237)\nX_GLYPH_DEFINE(\"Ĕ\", GLYPH_TEXT, 238)\nX_GLYPH_DEFINE(\"ĕ\", GLYPH_TEXT, 239)\nX_GLYPH_DEFINE(\"Ė\", GLYPH_TEXT, 240)\nX_GLYPH_DEFINE(\"ė\", GLYPH_TEXT, 241)\nX_GLYPH_DEFINE(\"Ę\", GLYPH_TEXT, 242)\nX_GLYPH_DEFINE(\"ę\", GLYPH_TEXT, 243)\nX_GLYPH_DEFINE(\"Ě\", GLYPH_TEXT, 244)\nX_GLYPH_DEFINE(\"ě\", GLYPH_TEXT, 245)\nX_GLYPH_DEFINE(\"Ĝ\", GLYPH_TEXT, 246)\nX_GLYPH_DEFINE(\"ĝ\", GLYPH_TEXT, 247)\nX_GLYPH_DEFINE(\"Ğ\", GLYPH_TEXT, 248)\nX_GLYPH_DEFINE(\"ğ\", GLYPH_TEXT, 249)\nX_GLYPH_DEFINE(\"Ġ\", GLYPH_TEXT, 250)\nX_GLYPH_DEFINE(\"ġ\", GLYPH_TEXT, 251)\nX_GLYPH_DEFINE(\"Ģ\", GLYPH_TEXT, 252)\nX_GLYPH_DEFINE(\"ģ\", GLYPH_TEXT, 253)\nX_GLYPH_DEFINE(\"Ĥ\", GLYPH_TEXT, 254)\nX_GLYPH_DEFINE(\"ĥ\", GLYPH_TEXT, 255)\nX_GLYPH_DEFINE(\"Ħ\", GLYPH_TEXT, 256)\nX_GLYPH_DEFINE(\"ħ\", GLYPH_TEXT, 257)\nX_GLYPH_DEFINE(\"Ĩ\", GLYPH_TEXT, 258)\nX_GLYPH_DEFINE(\"ĩ\", GLYPH_TEXT, 259)\nX_GLYPH_DEFINE(\"Ī\", GLYPH_TEXT, 260)\nX_GLYPH_DEFINE(\"ī\", GLYPH_TEXT, 261)\nX_GLYPH_DEFINE(\"Ĭ\", GLYPH_TEXT, 262)\nX_GLYPH_DEFINE(\"ĭ\", GLYPH_TEXT, 263)\nX_GLYPH_DEFINE(\"Į\", GLYPH_TEXT, 264)\nX_GLYPH_DEFINE(\"į\", GLYPH_TEXT, 265)\nX_GLYPH_DEFINE(\"İ\", GLYPH_TEXT, 266)\nX_GLYPH_DEFINE(\"ı\", GLYPH_TEXT, 267)\nX_GLYPH_DEFINE(\"Ĵ\", GLYPH_TEXT, 268)\nX_GLYPH_DEFINE(\"ĵ\", GLYPH_TEXT, 269)\nX_GLYPH_DEFINE(\"Ķ\", GLYPH_TEXT, 270)\nX_GLYPH_DEFINE(\"ķ\", GLYPH_TEXT, 271)\nX_GLYPH_DEFINE(\"ĸ\", GLYPH_TEXT, 272)\nX_GLYPH_DEFINE(\"Ĺ\", GLYPH_TEXT, 273)\nX_GLYPH_DEFINE(\"ĺ\", GLYPH_TEXT, 274)\nX_GLYPH_DEFINE(\"Ļ\", GLYPH_TEXT, 275)\nX_GLYPH_DEFINE(\"ļ\", GLYPH_TEXT, 276)\nX_GLYPH_DEFINE(\"Ľ\", GLYPH_TEXT, 277)\nX_GLYPH_DEFINE(\"ľ\", GLYPH_TEXT, 278)\nX_GLYPH_DEFINE(\"Ŀ\", GLYPH_TEXT, 279)\nX_GLYPH_DEFINE(\"ŀ\", GLYPH_TEXT, 280)\nX_GLYPH_DEFINE(\"Ł\", GLYPH_TEXT, 281)\nX_GLYPH_DEFINE(\"ł\", GLYPH_TEXT, 282)\nX_GLYPH_DEFINE(\"Ń\", GLYPH_TEXT, 283)\nX_GLYPH_DEFINE(\"ń\", GLYPH_TEXT, 284)\nX_GLYPH_DEFINE(\"Ņ\", GLYPH_TEXT, 285)\nX_GLYPH_DEFINE(\"ņ\", GLYPH_TEXT, 286)\nX_GLYPH_DEFINE(\"Ň\", GLYPH_TEXT, 287)\nX_GLYPH_DEFINE(\"ň\", GLYPH_TEXT, 288)\nX_GLYPH_DEFINE(\"ŉ\", GLYPH_TEXT, 289)\nX_GLYPH_DEFINE(\"Ŋ\", GLYPH_TEXT, 290)\nX_GLYPH_DEFINE(\"ŋ\", GLYPH_TEXT, 291)\nX_GLYPH_DEFINE(\"Ō\", GLYPH_TEXT, 292)\nX_GLYPH_DEFINE(\"ō\", GLYPH_TEXT, 293)\nX_GLYPH_DEFINE(\"Ŏ\", GLYPH_TEXT, 294)\nX_GLYPH_DEFINE(\"ŏ\", GLYPH_TEXT, 295)\nX_GLYPH_DEFINE(\"Ő\", GLYPH_TEXT, 296)\nX_GLYPH_DEFINE(\"ő\", GLYPH_TEXT, 297)\nX_GLYPH_DEFINE(\"Œ\", GLYPH_TEXT, 298)\nX_GLYPH_DEFINE(\"œ\", GLYPH_TEXT, 299)\nX_GLYPH_DEFINE(\"Ŕ\", GLYPH_TEXT, 300)\nX_GLYPH_DEFINE(\"ŕ\", GLYPH_TEXT, 301)\nX_GLYPH_DEFINE(\"Ŗ\", GLYPH_TEXT, 302)\nX_GLYPH_DEFINE(\"ŗ\", GLYPH_TEXT, 303)\nX_GLYPH_DEFINE(\"Ř\", GLYPH_TEXT, 304)\nX_GLYPH_DEFINE(\"ř\", GLYPH_TEXT, 305)\nX_GLYPH_DEFINE(\"Ś\", GLYPH_TEXT, 306)\nX_GLYPH_DEFINE(\"ś\", GLYPH_TEXT, 307)\nX_GLYPH_DEFINE(\"Ŝ\", GLYPH_TEXT, 308)\nX_GLYPH_DEFINE(\"ŝ\", GLYPH_TEXT, 309)\nX_GLYPH_DEFINE(\"Ş\", GLYPH_TEXT, 310)\nX_GLYPH_DEFINE(\"ş\", GLYPH_TEXT, 311)\nX_GLYPH_DEFINE(\"Š\", GLYPH_TEXT, 312)\nX_GLYPH_DEFINE(\"š\", GLYPH_TEXT, 313)\nX_GLYPH_DEFINE(\"Ţ\", GLYPH_TEXT, 314)\nX_GLYPH_DEFINE(\"ţ\", GLYPH_TEXT, 315)\nX_GLYPH_DEFINE(\"Ť\", GLYPH_TEXT, 316)\nX_GLYPH_DEFINE(\"ť\", GLYPH_TEXT, 317)\nX_GLYPH_DEFINE(\"Ŧ\", GLYPH_TEXT, 318)\nX_GLYPH_DEFINE(\"ŧ\", GLYPH_TEXT, 319)\nX_GLYPH_DEFINE(\"Ũ\", GLYPH_TEXT, 320)\nX_GLYPH_DEFINE(\"ũ\", GLYPH_TEXT, 321)\nX_GLYPH_DEFINE(\"Ū\", GLYPH_TEXT, 322)\nX_GLYPH_DEFINE(\"ū\", GLYPH_TEXT, 323)\nX_GLYPH_DEFINE(\"Ŭ\", GLYPH_TEXT, 324)\nX_GLYPH_DEFINE(\"ŭ\", GLYPH_TEXT, 325)\nX_GLYPH_DEFINE(\"Ů\", GLYPH_TEXT, 326)\nX_GLYPH_DEFINE(\"ů\", GLYPH_TEXT, 327)\nX_GLYPH_DEFINE(\"Ű\", GLYPH_TEXT, 328)\nX_GLYPH_DEFINE(\"ű\", GLYPH_TEXT, 329)\nX_GLYPH_DEFINE(\"Ų\", GLYPH_TEXT, 330)\nX_GLYPH_DEFINE(\"ų\", GLYPH_TEXT, 331)\nX_GLYPH_DEFINE(\"Ŵ\", GLYPH_TEXT, 332)\nX_GLYPH_DEFINE(\"ŵ\", GLYPH_TEXT, 333)\nX_GLYPH_DEFINE(\"Ŷ\", GLYPH_TEXT, 334)\nX_GLYPH_DEFINE(\"ŷ\", GLYPH_TEXT, 335)\nX_GLYPH_DEFINE(\"Ÿ\", GLYPH_TEXT, 336)\nX_GLYPH_DEFINE(\"Ź\", GLYPH_TEXT, 337)\nX_GLYPH_DEFINE(\"ź\", GLYPH_TEXT, 338)\nX_GLYPH_DEFINE(\"Ż\", GLYPH_TEXT, 339)\nX_GLYPH_DEFINE(\"ż\", GLYPH_TEXT, 340)\nX_GLYPH_DEFINE(\"Ž\", GLYPH_TEXT, 341)\nX_GLYPH_DEFINE(\"ž\", GLYPH_TEXT, 342)\nX_GLYPH_DEFINE(\"ƒ\", GLYPH_TEXT, 343)\nX_GLYPH_DEFINE(\"Ǎ\", GLYPH_TEXT, 344)\nX_GLYPH_DEFINE(\"ǎ\", GLYPH_TEXT, 345)\nX_GLYPH_DEFINE(\"Ǐ\", GLYPH_TEXT, 346)\nX_GLYPH_DEFINE(\"ǐ\", GLYPH_TEXT, 347)\nX_GLYPH_DEFINE(\"Ǒ\", GLYPH_TEXT, 348)\nX_GLYPH_DEFINE(\"ǒ\", GLYPH_TEXT, 349)\nX_GLYPH_DEFINE(\"Ǔ\", GLYPH_TEXT, 350)\nX_GLYPH_DEFINE(\"ǔ\", GLYPH_TEXT, 351)\nX_GLYPH_DEFINE(\"Ǧ\", GLYPH_TEXT, 352)\nX_GLYPH_DEFINE(\"ǧ\", GLYPH_TEXT, 353)\nX_GLYPH_DEFINE(\"Ǩ\", GLYPH_TEXT, 354)\nX_GLYPH_DEFINE(\"ǩ\", GLYPH_TEXT, 355)\nX_GLYPH_DEFINE(\"ǰ\", GLYPH_TEXT, 356)\nX_GLYPH_DEFINE(\"Ǵ\", GLYPH_TEXT, 357)\nX_GLYPH_DEFINE(\"ǵ\", GLYPH_TEXT, 358)\nX_GLYPH_DEFINE(\"Ǹ\", GLYPH_TEXT, 359)\nX_GLYPH_DEFINE(\"ǹ\", GLYPH_TEXT, 360)\nX_GLYPH_DEFINE(\"Ȟ\", GLYPH_TEXT, 361)\nX_GLYPH_DEFINE(\"ȟ\", GLYPH_TEXT, 362)\nX_GLYPH_DEFINE(\"Ȧ\", GLYPH_TEXT, 363)\nX_GLYPH_DEFINE(\"ȧ\", GLYPH_TEXT, 364)\nX_GLYPH_DEFINE(\"Ȯ\", GLYPH_TEXT, 365)\nX_GLYPH_DEFINE(\"ȯ\", GLYPH_TEXT, 366)\nX_GLYPH_DEFINE(\"Ȳ\", GLYPH_TEXT, 367)\nX_GLYPH_DEFINE(\"ȳ\", GLYPH_TEXT, 368)\nX_GLYPH_DEFINE(\"Ș\", GLYPH_TEXT, 369)\nX_GLYPH_DEFINE(\"ș\", GLYPH_TEXT, 370)\nX_GLYPH_DEFINE(\"Ț\", GLYPH_TEXT, 371)\nX_GLYPH_DEFINE(\"ț\", GLYPH_TEXT, 372)\nX_GLYPH_DEFINE(\"Γ\", GLYPH_TEXT, 373)\nX_GLYPH_DEFINE(\"Δ\", GLYPH_TEXT, 374)\nX_GLYPH_DEFINE(\"Ε\", GLYPH_TEXT, 375)\nX_GLYPH_DEFINE(\"Ζ\", GLYPH_TEXT, 376)\nX_GLYPH_DEFINE(\"Η\", GLYPH_TEXT, 377)\nX_GLYPH_DEFINE(\"Θ\", GLYPH_TEXT, 378)\nX_GLYPH_DEFINE(\"Ι\", GLYPH_TEXT, 379)\nX_GLYPH_DEFINE(\"Κ\", GLYPH_TEXT, 380)\nX_GLYPH_DEFINE(\"Λ\", GLYPH_TEXT, 381)\nX_GLYPH_DEFINE(\"Μ\", GLYPH_TEXT, 382)\nX_GLYPH_DEFINE(\"Ν\", GLYPH_TEXT, 383)\nX_GLYPH_DEFINE(\"Ξ\", GLYPH_TEXT, 384)\nX_GLYPH_DEFINE(\"Ο\", GLYPH_TEXT, 385)\nX_GLYPH_DEFINE(\"Π\", GLYPH_TEXT, 386)\nX_GLYPH_DEFINE(\"Ρ\", GLYPH_TEXT, 387)\nX_GLYPH_DEFINE(\"Σ\", GLYPH_TEXT, 388)\nX_GLYPH_DEFINE(\"Τ\", GLYPH_TEXT, 389)\nX_GLYPH_DEFINE(\"Υ\", GLYPH_TEXT, 390)\nX_GLYPH_DEFINE(\"Φ\", GLYPH_TEXT, 391)\nX_GLYPH_DEFINE(\"Χ\", GLYPH_TEXT, 392)\nX_GLYPH_DEFINE(\"Ψ\", GLYPH_TEXT, 393)\nX_GLYPH_DEFINE(\"Ω\", GLYPH_TEXT, 394)\nX_GLYPH_DEFINE(\"α\", GLYPH_TEXT, 395)\nX_GLYPH_DEFINE(\"β\", GLYPH_TEXT, 396)\nX_GLYPH_DEFINE(\"γ\", GLYPH_TEXT, 397)\nX_GLYPH_DEFINE(\"δ\", GLYPH_TEXT, 398)\nX_GLYPH_DEFINE(\"ε\", GLYPH_TEXT, 399)\nX_GLYPH_DEFINE(\"ζ\", GLYPH_TEXT, 400)\nX_GLYPH_DEFINE(\"η\", GLYPH_TEXT, 401)\nX_GLYPH_DEFINE(\"θ\", GLYPH_TEXT, 402)\nX_GLYPH_DEFINE(\"ι\", GLYPH_TEXT, 403)\nX_GLYPH_DEFINE(\"κ\", GLYPH_TEXT, 404)\nX_GLYPH_DEFINE(\"λ\", GLYPH_TEXT, 405)\nX_GLYPH_DEFINE(\"μ\", GLYPH_TEXT, 406)\nX_GLYPH_DEFINE(\"ν\", GLYPH_TEXT, 407)\nX_GLYPH_DEFINE(\"ξ\", GLYPH_TEXT, 408)\nX_GLYPH_DEFINE(\"ο\", GLYPH_TEXT, 409)\nX_GLYPH_DEFINE(\"π\", GLYPH_TEXT, 410)\nX_GLYPH_DEFINE(\"ρ\", GLYPH_TEXT, 411)\nX_GLYPH_DEFINE(\"ς\", GLYPH_TEXT, 412)\nX_GLYPH_DEFINE(\"σ\", GLYPH_TEXT, 413)\nX_GLYPH_DEFINE(\"τ\", GLYPH_TEXT, 414)\nX_GLYPH_DEFINE(\"υ\", GLYPH_TEXT, 415)\nX_GLYPH_DEFINE(\"φ\", GLYPH_TEXT, 416)\nX_GLYPH_DEFINE(\"χ\", GLYPH_TEXT, 417)\nX_GLYPH_DEFINE(\"ψ\", GLYPH_TEXT, 418)\nX_GLYPH_DEFINE(\"ω\", GLYPH_TEXT, 419)\nX_GLYPH_DEFINE(\"Ά\", GLYPH_TEXT, 420)\nX_GLYPH_DEFINE(\"Έ\", GLYPH_TEXT, 421)\nX_GLYPH_DEFINE(\"Ή\", GLYPH_TEXT, 422)\nX_GLYPH_DEFINE(\"Ί\", GLYPH_TEXT, 423)\nX_GLYPH_DEFINE(\"Ό\", GLYPH_TEXT, 424)\nX_GLYPH_DEFINE(\"Ύ\", GLYPH_TEXT, 425)\nX_GLYPH_DEFINE(\"Ώ\", GLYPH_TEXT, 426)\nX_GLYPH_DEFINE(\"ΐ\", GLYPH_TEXT, 427)\nX_GLYPH_DEFINE(\"Α\", GLYPH_TEXT, 428)\nX_GLYPH_DEFINE(\"Β\", GLYPH_TEXT, 429)\nX_GLYPH_DEFINE(\"Ϊ\", GLYPH_TEXT, 430)\nX_GLYPH_DEFINE(\"Ϋ\", GLYPH_TEXT, 431)\nX_GLYPH_DEFINE(\"ά\", GLYPH_TEXT, 432)\nX_GLYPH_DEFINE(\"έ\", GLYPH_TEXT, 433)\nX_GLYPH_DEFINE(\"ή\", GLYPH_TEXT, 434)\nX_GLYPH_DEFINE(\"ί\", GLYPH_TEXT, 435)\nX_GLYPH_DEFINE(\"ΰ\", GLYPH_TEXT, 436)\nX_GLYPH_DEFINE(\"ϊ\", GLYPH_TEXT, 437)\nX_GLYPH_DEFINE(\"ϋ\", GLYPH_TEXT, 438)\nX_GLYPH_DEFINE(\"ό\", GLYPH_TEXT, 439)\nX_GLYPH_DEFINE(\"ύ\", GLYPH_TEXT, 440)\nX_GLYPH_DEFINE(\"ώ\", GLYPH_TEXT, 441)\nX_GLYPH_DEFINE(\"Ѐ\", GLYPH_TEXT, 442)\nX_GLYPH_DEFINE(\"Ё\", GLYPH_TEXT, 443)\nX_GLYPH_DEFINE(\"Ђ\", GLYPH_TEXT, 444)\nX_GLYPH_DEFINE(\"Ѓ\", GLYPH_TEXT, 445)\nX_GLYPH_DEFINE(\"Є\", GLYPH_TEXT, 446)\nX_GLYPH_DEFINE(\"Ѕ\", GLYPH_TEXT, 447)\nX_GLYPH_DEFINE(\"І\", GLYPH_TEXT, 448)\nX_GLYPH_DEFINE(\"Ї\", GLYPH_TEXT, 449)\nX_GLYPH_DEFINE(\"Ј\", GLYPH_TEXT, 450)\nX_GLYPH_DEFINE(\"Љ\", GLYPH_TEXT, 451)\nX_GLYPH_DEFINE(\"Њ\", GLYPH_TEXT, 452)\nX_GLYPH_DEFINE(\"Ћ\", GLYPH_TEXT, 453)\nX_GLYPH_DEFINE(\"Ќ\", GLYPH_TEXT, 454)\nX_GLYPH_DEFINE(\"Ѝ\", GLYPH_TEXT, 455)\nX_GLYPH_DEFINE(\"Ў\", GLYPH_TEXT, 456)\nX_GLYPH_DEFINE(\"Џ\", GLYPH_TEXT, 457)\nX_GLYPH_DEFINE(\"А\", GLYPH_TEXT, 458)\nX_GLYPH_DEFINE(\"Б\", GLYPH_TEXT, 459)\nX_GLYPH_DEFINE(\"В\", GLYPH_TEXT, 460)\nX_GLYPH_DEFINE(\"Г\", GLYPH_TEXT, 461)\nX_GLYPH_DEFINE(\"Д\", GLYPH_TEXT, 462)\nX_GLYPH_DEFINE(\"Е\", GLYPH_TEXT, 463)\nX_GLYPH_DEFINE(\"Ж\", GLYPH_TEXT, 464)\nX_GLYPH_DEFINE(\"З\", GLYPH_TEXT, 465)\nX_GLYPH_DEFINE(\"И\", GLYPH_TEXT, 466)\nX_GLYPH_DEFINE(\"Й\", GLYPH_TEXT, 467)\nX_GLYPH_DEFINE(\"К\", GLYPH_TEXT, 468)\nX_GLYPH_DEFINE(\"Л\", GLYPH_TEXT, 469)\nX_GLYPH_DEFINE(\"М\", GLYPH_TEXT, 470)\nX_GLYPH_DEFINE(\"Н\", GLYPH_TEXT, 471)\nX_GLYPH_DEFINE(\"О\", GLYPH_TEXT, 472)\nX_GLYPH_DEFINE(\"П\", GLYPH_TEXT, 473)\nX_GLYPH_DEFINE(\"Р\", GLYPH_TEXT, 474)\nX_GLYPH_DEFINE(\"С\", GLYPH_TEXT, 475)\nX_GLYPH_DEFINE(\"Т\", GLYPH_TEXT, 476)\nX_GLYPH_DEFINE(\"У\", GLYPH_TEXT, 477)\nX_GLYPH_DEFINE(\"Ф\", GLYPH_TEXT, 478)\nX_GLYPH_DEFINE(\"Х\", GLYPH_TEXT, 479)\nX_GLYPH_DEFINE(\"Ц\", GLYPH_TEXT, 480)\nX_GLYPH_DEFINE(\"Ч\", GLYPH_TEXT, 481)\nX_GLYPH_DEFINE(\"Ш\", GLYPH_TEXT, 482)\nX_GLYPH_DEFINE(\"Щ\", GLYPH_TEXT, 483)\nX_GLYPH_DEFINE(\"Ъ\", GLYPH_TEXT, 484)\nX_GLYPH_DEFINE(\"Ы\", GLYPH_TEXT, 485)\nX_GLYPH_DEFINE(\"Ь\", GLYPH_TEXT, 486)\nX_GLYPH_DEFINE(\"Э\", GLYPH_TEXT, 487)\nX_GLYPH_DEFINE(\"Ю\", GLYPH_TEXT, 488)\nX_GLYPH_DEFINE(\"Я\", GLYPH_TEXT, 489)\nX_GLYPH_DEFINE(\"а\", GLYPH_TEXT, 490)\nX_GLYPH_DEFINE(\"б\", GLYPH_TEXT, 491)\nX_GLYPH_DEFINE(\"в\", GLYPH_TEXT, 492)\nX_GLYPH_DEFINE(\"г\", GLYPH_TEXT, 493)\nX_GLYPH_DEFINE(\"д\", GLYPH_TEXT, 494)\nX_GLYPH_DEFINE(\"е\", GLYPH_TEXT, 495)\nX_GLYPH_DEFINE(\"ж\", GLYPH_TEXT, 496)\nX_GLYPH_DEFINE(\"з\", GLYPH_TEXT, 497)\nX_GLYPH_DEFINE(\"и\", GLYPH_TEXT, 498)\nX_GLYPH_DEFINE(\"й\", GLYPH_TEXT, 499)\nX_GLYPH_DEFINE(\"к\", GLYPH_TEXT, 500)\nX_GLYPH_DEFINE(\"л\", GLYPH_TEXT, 501)\nX_GLYPH_DEFINE(\"м\", GLYPH_TEXT, 502)\nX_GLYPH_DEFINE(\"н\", GLYPH_TEXT, 503)\nX_GLYPH_DEFINE(\"о\", GLYPH_TEXT, 504)\nX_GLYPH_DEFINE(\"п\", GLYPH_TEXT, 505)\nX_GLYPH_DEFINE(\"р\", GLYPH_TEXT, 506)\nX_GLYPH_DEFINE(\"с\", GLYPH_TEXT, 507)\nX_GLYPH_DEFINE(\"т\", GLYPH_TEXT, 508)\nX_GLYPH_DEFINE(\"у\", GLYPH_TEXT, 509)\nX_GLYPH_DEFINE(\"ф\", GLYPH_TEXT, 510)\nX_GLYPH_DEFINE(\"х\", GLYPH_TEXT, 511)\nX_GLYPH_DEFINE(\"ц\", GLYPH_TEXT, 512)\nX_GLYPH_DEFINE(\"ч\", GLYPH_TEXT, 513)\nX_GLYPH_DEFINE(\"ш\", GLYPH_TEXT, 514)\nX_GLYPH_DEFINE(\"щ\", GLYPH_TEXT, 515)\nX_GLYPH_DEFINE(\"ъ\", GLYPH_TEXT, 516)\nX_GLYPH_DEFINE(\"ы\", GLYPH_TEXT, 517)\nX_GLYPH_DEFINE(\"ь\", GLYPH_TEXT, 518)\nX_GLYPH_DEFINE(\"э\", GLYPH_TEXT, 519)\nX_GLYPH_DEFINE(\"ю\", GLYPH_TEXT, 520)\nX_GLYPH_DEFINE(\"я\", GLYPH_TEXT, 521)\nX_GLYPH_DEFINE(\"ѐ\", GLYPH_TEXT, 522)\nX_GLYPH_DEFINE(\"ё\", GLYPH_TEXT, 523)\nX_GLYPH_DEFINE(\"ђ\", GLYPH_TEXT, 524)\nX_GLYPH_DEFINE(\"ѓ\", GLYPH_TEXT, 525)\nX_GLYPH_DEFINE(\"є\", GLYPH_TEXT, 526)\nX_GLYPH_DEFINE(\"ѕ\", GLYPH_TEXT, 527)\nX_GLYPH_DEFINE(\"і\", GLYPH_TEXT, 528)\nX_GLYPH_DEFINE(\"ї\", GLYPH_TEXT, 529)\nX_GLYPH_DEFINE(\"ј\", GLYPH_TEXT, 530)\nX_GLYPH_DEFINE(\"љ\", GLYPH_TEXT, 531)\nX_GLYPH_DEFINE(\"њ\", GLYPH_TEXT, 532)\nX_GLYPH_DEFINE(\"ћ\", GLYPH_TEXT, 533)\nX_GLYPH_DEFINE(\"ќ\", GLYPH_TEXT, 534)\nX_GLYPH_DEFINE(\"ѝ\", GLYPH_TEXT, 535)\nX_GLYPH_DEFINE(\"ў\", GLYPH_TEXT, 536)\nX_GLYPH_DEFINE(\"џ\", GLYPH_TEXT, 537)\nX_GLYPH_DEFINE(\"Ґ\", GLYPH_TEXT, 538)\nX_GLYPH_DEFINE(\"ґ\", GLYPH_TEXT, 539)\nX_GLYPH_DEFINE(\"Ḃ\", GLYPH_TEXT, 540)\nX_GLYPH_DEFINE(\"ḃ\", GLYPH_TEXT, 541)\nX_GLYPH_DEFINE(\"Ḋ\", GLYPH_TEXT, 542)\nX_GLYPH_DEFINE(\"ḋ\", GLYPH_TEXT, 543)\nX_GLYPH_DEFINE(\"Ḟ\", GLYPH_TEXT, 544)\nX_GLYPH_DEFINE(\"ḟ\", GLYPH_TEXT, 545)\nX_GLYPH_DEFINE(\"Ḡ\", GLYPH_TEXT, 546)\nX_GLYPH_DEFINE(\"ḡ\", GLYPH_TEXT, 547)\nX_GLYPH_DEFINE(\"Ḣ\", GLYPH_TEXT, 548)\nX_GLYPH_DEFINE(\"ḣ\", GLYPH_TEXT, 549)\nX_GLYPH_DEFINE(\"Ḧ\", GLYPH_TEXT, 550)\nX_GLYPH_DEFINE(\"ḧ\", GLYPH_TEXT, 551)\nX_GLYPH_DEFINE(\"Ḱ\", GLYPH_TEXT, 552)\nX_GLYPH_DEFINE(\"ḱ\", GLYPH_TEXT, 553)\nX_GLYPH_DEFINE(\"Ḿ\", GLYPH_TEXT, 554)\nX_GLYPH_DEFINE(\"ḿ\", GLYPH_TEXT, 555)\nX_GLYPH_DEFINE(\"Ṁ\", GLYPH_TEXT, 556)\nX_GLYPH_DEFINE(\"ṁ\", GLYPH_TEXT, 557)\nX_GLYPH_DEFINE(\"Ṅ\", GLYPH_TEXT, 558)\nX_GLYPH_DEFINE(\"ṅ\", GLYPH_TEXT, 559)\nX_GLYPH_DEFINE(\"Ṕ\", GLYPH_TEXT, 560)\nX_GLYPH_DEFINE(\"ṕ\", GLYPH_TEXT, 561)\nX_GLYPH_DEFINE(\"Ṗ\", GLYPH_TEXT, 562)\nX_GLYPH_DEFINE(\"ṗ\", GLYPH_TEXT, 563)\nX_GLYPH_DEFINE(\"Ṙ\", GLYPH_TEXT, 564)\nX_GLYPH_DEFINE(\"ṙ\", GLYPH_TEXT, 565)\nX_GLYPH_DEFINE(\"Ṡ\", GLYPH_TEXT, 566)\nX_GLYPH_DEFINE(\"ṡ\", GLYPH_TEXT, 567)\nX_GLYPH_DEFINE(\"Ṫ\", GLYPH_TEXT, 568)\nX_GLYPH_DEFINE(\"ṫ\", GLYPH_TEXT, 569)\nX_GLYPH_DEFINE(\"Ṽ\", GLYPH_TEXT, 570)\nX_GLYPH_DEFINE(\"ṽ\", GLYPH_TEXT, 571)\nX_GLYPH_DEFINE(\"Ẁ\", GLYPH_TEXT, 572)\nX_GLYPH_DEFINE(\"ẁ\", GLYPH_TEXT, 573)\nX_GLYPH_DEFINE(\"Ẃ\", GLYPH_TEXT, 574)\nX_GLYPH_DEFINE(\"ẃ\", GLYPH_TEXT, 575)\nX_GLYPH_DEFINE(\"Ẅ\", GLYPH_TEXT, 576)\nX_GLYPH_DEFINE(\"ẅ\", GLYPH_TEXT, 577)\nX_GLYPH_DEFINE(\"Ẇ\", GLYPH_TEXT, 578)\nX_GLYPH_DEFINE(\"ẇ\", GLYPH_TEXT, 579)\nX_GLYPH_DEFINE(\"Ẋ\", GLYPH_TEXT, 580)\nX_GLYPH_DEFINE(\"ẋ\", GLYPH_TEXT, 581)\nX_GLYPH_DEFINE(\"Ẍ\", GLYPH_TEXT, 582)\nX_GLYPH_DEFINE(\"ẍ\", GLYPH_TEXT, 583)\nX_GLYPH_DEFINE(\"Ẏ\", GLYPH_TEXT, 584)\nX_GLYPH_DEFINE(\"ẏ\", GLYPH_TEXT, 585)\nX_GLYPH_DEFINE(\"Ẑ\", GLYPH_TEXT, 586)\nX_GLYPH_DEFINE(\"ẑ\", GLYPH_TEXT, 587)\nX_GLYPH_DEFINE(\"ẗ\", GLYPH_TEXT, 588)\nX_GLYPH_DEFINE(\"ẘ\", GLYPH_TEXT, 589)\nX_GLYPH_DEFINE(\"ẙ\", GLYPH_TEXT, 590)\nX_GLYPH_DEFINE(\"Ẽ\", GLYPH_TEXT, 591)\nX_GLYPH_DEFINE(\"ẽ\", GLYPH_TEXT, 592)\nX_GLYPH_DEFINE(\"Ỳ\", GLYPH_TEXT, 593)\nX_GLYPH_DEFINE(\"ỳ\", GLYPH_TEXT, 594)\nX_GLYPH_DEFINE(\"Ỹ\", GLYPH_TEXT, 595)\nX_GLYPH_DEFINE(\"ỹ\", GLYPH_TEXT, 596)\nX_GLYPH_DEFINE(\"–\", GLYPH_TEXT, 597)\nX_GLYPH_DEFINE(\"—\", GLYPH_TEXT, 598)\nX_GLYPH_DEFINE(\"‘\", GLYPH_TEXT, 599)\nX_GLYPH_DEFINE(\"’\", GLYPH_TEXT, 600)\nX_GLYPH_DEFINE(\"“\", GLYPH_TEXT, 601)\nX_GLYPH_DEFINE(\"”\", GLYPH_TEXT, 602)\nX_GLYPH_DEFINE(\"†\", GLYPH_TEXT, 603)\nX_GLYPH_DEFINE(\"‡\", GLYPH_TEXT, 604)\nX_GLYPH_DEFINE(\"•\", GLYPH_TEXT, 605)\nX_GLYPH_DEFINE(\"…\", GLYPH_TEXT, 606)\nX_GLYPH_DEFINE(\"‰\", GLYPH_TEXT, 607)\nX_GLYPH_DEFINE(\"‹\", GLYPH_TEXT, 608)\nX_GLYPH_DEFINE(\"›\", GLYPH_TEXT, 609)\nX_GLYPH_DEFINE(\"⁴\", GLYPH_TEXT, 610)\nX_GLYPH_DEFINE(\"€\", GLYPH_TEXT, 611)\nX_GLYPH_DEFINE(\"₯\", GLYPH_TEXT, 612)\nX_GLYPH_DEFINE(\"№\", GLYPH_TEXT, 613)\nX_GLYPH_DEFINE(\"™\", GLYPH_TEXT, 614)\nX_GLYPH_DEFINE(\"ﬁ\", GLYPH_TEXT, 615)\nX_GLYPH_DEFINE(\"ﬂ\", GLYPH_TEXT, 616)\nX_GLYPH_DEFINE(\"\\\\{keyboard backspace}\", GLYPH_ICON, 617)\nX_GLYPH_DEFINE(\"\\\\{keyboard scroll_lock}\", GLYPH_ICON, 618)\nX_GLYPH_DEFINE(\"\\\\{keyboard return}\", GLYPH_ICON, 619)\nX_GLYPH_DEFINE(\"\\\\{keyboard caps_lock}\", GLYPH_ICON, 620)\nX_GLYPH_DEFINE(\"\\\\{keyboard print_screen}\", GLYPH_ICON, 621)\nX_GLYPH_DEFINE(\"\\\\{keyboard insert}\", GLYPH_ICON, 622)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_lock}\", GLYPH_ICON, 623)\nX_GLYPH_DEFINE(\"\\\\{keyboard l_ctrl}\", GLYPH_ICON, 624)\nX_GLYPH_DEFINE(\"\\\\{keyboard r_ctrl}\", GLYPH_ICON, 625)\nX_GLYPH_DEFINE(\"\\\\{keyboard r_shift}\", GLYPH_ICON, 626)\nX_GLYPH_DEFINE(\"\\\\{keyboard l_shift}\", GLYPH_ICON, 627)\nX_GLYPH_DEFINE(\"\\\\{keyboard r_alt}\", GLYPH_ICON, 628)\nX_GLYPH_DEFINE(\"\\\\{keyboard l_alt}\", GLYPH_ICON, 629)\nX_GLYPH_DEFINE(\"\\\\{keyboard l_win}\", GLYPH_ICON, 630)\nX_GLYPH_DEFINE(\"\\\\{keyboard r_win}\", GLYPH_ICON, 631)\nX_GLYPH_DEFINE(\"\\\\{keyboard escape}\", GLYPH_ICON, 632)\nX_GLYPH_DEFINE(\"\\\\{keyboard tab}\", GLYPH_ICON, 633)\nX_GLYPH_DEFINE(\"\\\\{keyboard space}\", GLYPH_ICON, 634)\nX_GLYPH_DEFINE(\"\\\\{keyboard pause}\", GLYPH_ICON, 635)\nX_GLYPH_DEFINE(\"\\\\{keyboard home}\", GLYPH_ICON, 636)\nX_GLYPH_DEFINE(\"\\\\{keyboard page_up}\", GLYPH_ICON, 637)\nX_GLYPH_DEFINE(\"\\\\{keyboard delete}\", GLYPH_ICON, 638)\nX_GLYPH_DEFINE(\"\\\\{keyboard end}\", GLYPH_ICON, 639)\nX_GLYPH_DEFINE(\"\\\\{keyboard page_down}\", GLYPH_ICON, 640)\nX_GLYPH_DEFINE(\"\\\\{keyboard f10}\", GLYPH_ICON, 641)\nX_GLYPH_DEFINE(\"\\\\{keyboard f11}\", GLYPH_ICON, 642)\nX_GLYPH_DEFINE(\"\\\\{keyboard f12}\", GLYPH_ICON, 643)\nX_GLYPH_DEFINE(\"\\\\{keyboard f13}\", GLYPH_ICON, 644)\nX_GLYPH_DEFINE(\"\\\\{keyboard f14}\", GLYPH_ICON, 645)\nX_GLYPH_DEFINE(\"\\\\{keyboard f15}\", GLYPH_ICON, 646)\nX_GLYPH_DEFINE(\"\\\\{keyboard f16}\", GLYPH_ICON, 647)\nX_GLYPH_DEFINE(\"\\\\{keyboard f17}\", GLYPH_ICON, 648)\nX_GLYPH_DEFINE(\"\\\\{keyboard f18}\", GLYPH_ICON, 649)\nX_GLYPH_DEFINE(\"\\\\{keyboard f19}\", GLYPH_ICON, 650)\nX_GLYPH_DEFINE(\"\\\\{keyboard f20}\", GLYPH_ICON, 651)\nX_GLYPH_DEFINE(\"\\\\{keyboard f21}\", GLYPH_ICON, 652)\nX_GLYPH_DEFINE(\"\\\\{keyboard f22}\", GLYPH_ICON, 653)\nX_GLYPH_DEFINE(\"\\\\{keyboard f23}\", GLYPH_ICON, 654)\nX_GLYPH_DEFINE(\"\\\\{keyboard f24}\", GLYPH_ICON, 655)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_0}\", GLYPH_ICON, 656)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_1}\", GLYPH_ICON, 657)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_2}\", GLYPH_ICON, 658)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_3}\", GLYPH_ICON, 659)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_4}\", GLYPH_ICON, 660)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_5}\", GLYPH_ICON, 661)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_6}\", GLYPH_ICON, 662)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_7}\", GLYPH_ICON, 663)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_8}\", GLYPH_ICON, 664)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_9}\", GLYPH_ICON, 665)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_period}\", GLYPH_ICON, 666)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_divide}\", GLYPH_ICON, 667)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_multiply}\", GLYPH_ICON, 668)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_minus}\", GLYPH_ICON, 669)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_plus}\", GLYPH_ICON, 670)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_equals}\", GLYPH_ICON, 671)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_comma}\", GLYPH_ICON, 672)\nX_GLYPH_DEFINE(\"\\\\{keyboard num_enter}\", GLYPH_ICON, 673)\nX_GLYPH_DEFINE(\"\\\\{keyboard unknown}\", GLYPH_ICON, 674)\nX_GLYPH_DEFINE(\"\\\\{keyboard f1}\", GLYPH_ICON, 675)\nX_GLYPH_DEFINE(\"\\\\{keyboard f2}\", GLYPH_ICON, 676)\nX_GLYPH_DEFINE(\"\\\\{keyboard f3}\", GLYPH_ICON, 677)\nX_GLYPH_DEFINE(\"\\\\{keyboard f4}\", GLYPH_ICON, 678)\nX_GLYPH_DEFINE(\"\\\\{keyboard f5}\", GLYPH_ICON, 679)\nX_GLYPH_DEFINE(\"\\\\{keyboard f6}\", GLYPH_ICON, 680)\nX_GLYPH_DEFINE(\"\\\\{keyboard f7}\", GLYPH_ICON, 681)\nX_GLYPH_DEFINE(\"\\\\{keyboard f8}\", GLYPH_ICON, 682)\nX_GLYPH_DEFINE(\"\\\\{keyboard f9}\", GLYPH_ICON, 683)\nX_GLYPH_DEFINE(\"\\\\{keyboard left}\", GLYPH_ICON, 684)\nX_GLYPH_DEFINE(\"\\\\{keyboard up}\", GLYPH_ICON, 685)\nX_GLYPH_DEFINE(\"\\\\{keyboard right}\", GLYPH_ICON, 686)\nX_GLYPH_DEFINE(\"\\\\{keyboard down}\", GLYPH_ICON, 687)\nX_GLYPH_DEFINE(\"\\\\{keyboard a}\", GLYPH_ICON, 688)\nX_GLYPH_DEFINE(\"\\\\{keyboard b}\", GLYPH_ICON, 689)\nX_GLYPH_DEFINE(\"\\\\{keyboard c}\", GLYPH_ICON, 690)\nX_GLYPH_DEFINE(\"\\\\{keyboard d}\", GLYPH_ICON, 691)\nX_GLYPH_DEFINE(\"\\\\{keyboard e}\", GLYPH_ICON, 692)\nX_GLYPH_DEFINE(\"\\\\{keyboard f}\", GLYPH_ICON, 693)\nX_GLYPH_DEFINE(\"\\\\{keyboard g}\", GLYPH_ICON, 694)\nX_GLYPH_DEFINE(\"\\\\{keyboard h}\", GLYPH_ICON, 695)\nX_GLYPH_DEFINE(\"\\\\{keyboard i}\", GLYPH_ICON, 696)\nX_GLYPH_DEFINE(\"\\\\{keyboard j}\", GLYPH_ICON, 697)\nX_GLYPH_DEFINE(\"\\\\{keyboard k}\", GLYPH_ICON, 698)\nX_GLYPH_DEFINE(\"\\\\{keyboard l}\", GLYPH_ICON, 699)\nX_GLYPH_DEFINE(\"\\\\{keyboard m}\", GLYPH_ICON, 700)\nX_GLYPH_DEFINE(\"\\\\{keyboard n}\", GLYPH_ICON, 701)\nX_GLYPH_DEFINE(\"\\\\{keyboard o}\", GLYPH_ICON, 702)\nX_GLYPH_DEFINE(\"\\\\{keyboard p}\", GLYPH_ICON, 703)\nX_GLYPH_DEFINE(\"\\\\{keyboard q}\", GLYPH_ICON, 704)\nX_GLYPH_DEFINE(\"\\\\{keyboard r}\", GLYPH_ICON, 705)\nX_GLYPH_DEFINE(\"\\\\{keyboard s}\", GLYPH_ICON, 706)\nX_GLYPH_DEFINE(\"\\\\{keyboard t}\", GLYPH_ICON, 707)\nX_GLYPH_DEFINE(\"\\\\{keyboard u}\", GLYPH_ICON, 708)\nX_GLYPH_DEFINE(\"\\\\{keyboard v}\", GLYPH_ICON, 709)\nX_GLYPH_DEFINE(\"\\\\{keyboard w}\", GLYPH_ICON, 710)\nX_GLYPH_DEFINE(\"\\\\{keyboard x}\", GLYPH_ICON, 711)\nX_GLYPH_DEFINE(\"\\\\{keyboard y}\", GLYPH_ICON, 712)\nX_GLYPH_DEFINE(\"\\\\{keyboard z}\", GLYPH_ICON, 713)\nX_GLYPH_DEFINE(\"\\\\{keyboard 0}\", GLYPH_ICON, 714)\nX_GLYPH_DEFINE(\"\\\\{keyboard 1}\", GLYPH_ICON, 715)\nX_GLYPH_DEFINE(\"\\\\{keyboard 2}\", GLYPH_ICON, 716)\nX_GLYPH_DEFINE(\"\\\\{keyboard 3}\", GLYPH_ICON, 717)\nX_GLYPH_DEFINE(\"\\\\{keyboard 4}\", GLYPH_ICON, 718)\nX_GLYPH_DEFINE(\"\\\\{keyboard 5}\", GLYPH_ICON, 719)\nX_GLYPH_DEFINE(\"\\\\{keyboard 6}\", GLYPH_ICON, 720)\nX_GLYPH_DEFINE(\"\\\\{keyboard 7}\", GLYPH_ICON, 721)\nX_GLYPH_DEFINE(\"\\\\{keyboard 8}\", GLYPH_ICON, 722)\nX_GLYPH_DEFINE(\"\\\\{keyboard 9}\", GLYPH_ICON, 723)\nX_GLYPH_DEFINE(\"\\\\{keyboard minus}\", GLYPH_ICON, 724)\nX_GLYPH_DEFINE(\"\\\\{keyboard equals}\", GLYPH_ICON, 725)\nX_GLYPH_DEFINE(\"\\\\{keyboard left_square_bracket}\", GLYPH_ICON, 726)\nX_GLYPH_DEFINE(\"\\\\{keyboard right_square_bracket}\", GLYPH_ICON, 727)\nX_GLYPH_DEFINE(\"\\\\{keyboard backslash}\", GLYPH_ICON, 728)\nX_GLYPH_DEFINE(\"\\\\{keyboard hash}\", GLYPH_ICON, 729)\nX_GLYPH_DEFINE(\"\\\\{keyboard semicolon}\", GLYPH_ICON, 730)\nX_GLYPH_DEFINE(\"\\\\{keyboard apostrophe}\", GLYPH_ICON, 731)\nX_GLYPH_DEFINE(\"\\\\{keyboard backtick}\", GLYPH_ICON, 732)\nX_GLYPH_DEFINE(\"\\\\{keyboard comma}\", GLYPH_ICON, 733)\nX_GLYPH_DEFINE(\"\\\\{keyboard period}\", GLYPH_ICON, 734)\nX_GLYPH_DEFINE(\"\\\\{keyboard slash}\", GLYPH_ICON, 735)\nX_GLYPH_DEFINE(\"\\\\{controller rstick}\", GLYPH_ICON, 736)\nX_GLYPH_DEFINE(\"\\\\{controller rstick up}\", GLYPH_ICON, 737)\nX_GLYPH_DEFINE(\"\\\\{controller rstick right}\", GLYPH_ICON, 738)\nX_GLYPH_DEFINE(\"\\\\{controller rstick down}\", GLYPH_ICON, 739)\nX_GLYPH_DEFINE(\"\\\\{controller rstick left}\", GLYPH_ICON, 740)\nX_GLYPH_DEFINE(\"\\\\{controller lstick}\", GLYPH_ICON, 741)\nX_GLYPH_DEFINE(\"\\\\{controller lstick up}\", GLYPH_ICON, 742)\nX_GLYPH_DEFINE(\"\\\\{controller lstick right}\", GLYPH_ICON, 743)\nX_GLYPH_DEFINE(\"\\\\{controller lstick down}\", GLYPH_ICON, 744)\nX_GLYPH_DEFINE(\"\\\\{controller lstick left}\", GLYPH_ICON, 745)\nX_GLYPH_DEFINE(\"\\\\{controller dpad up}\", GLYPH_ICON, 746)\nX_GLYPH_DEFINE(\"\\\\{controller dpad right}\", GLYPH_ICON, 747)\nX_GLYPH_DEFINE(\"\\\\{controller dpad down}\", GLYPH_ICON, 748)\nX_GLYPH_DEFINE(\"\\\\{controller dpad left}\", GLYPH_ICON, 749)\nX_GLYPH_DEFINE(\"\\\\{controller button l1}\", GLYPH_ICON, 750)\nX_GLYPH_DEFINE(\"\\\\{controller button r1}\", GLYPH_ICON, 751)\nX_GLYPH_DEFINE(\"\\\\{controller button l2}\", GLYPH_ICON, 752)\nX_GLYPH_DEFINE(\"\\\\{controller button r2}\", GLYPH_ICON, 753)\nX_GLYPH_DEFINE(\"\\\\{controller bumper left}\", GLYPH_ICON, 754)\nX_GLYPH_DEFINE(\"\\\\{controller bumper right}\", GLYPH_ICON, 755)\nX_GLYPH_DEFINE(\"\\\\{controller button zl}\", GLYPH_ICON, 756)\nX_GLYPH_DEFINE(\"\\\\{controller button zr}\", GLYPH_ICON, 757)\nX_GLYPH_DEFINE(\"\\\\{controller trigger left}\", GLYPH_ICON, 758)\nX_GLYPH_DEFINE(\"\\\\{controller trigger right}\", GLYPH_ICON, 759)\nX_GLYPH_DEFINE(\"\\\\{controller button a}\", GLYPH_ICON, 760)\nX_GLYPH_DEFINE(\"\\\\{controller button b}\", GLYPH_ICON, 761)\nX_GLYPH_DEFINE(\"\\\\{controller button x}\", GLYPH_ICON, 762)\nX_GLYPH_DEFINE(\"\\\\{controller button y}\", GLYPH_ICON, 763)\nX_GLYPH_DEFINE(\"\\\\{controller button xbox}\", GLYPH_ICON, 764)\nX_GLYPH_DEFINE(\"\\\\{controller button triangle}\", GLYPH_ICON, 765)\nX_GLYPH_DEFINE(\"\\\\{controller button square}\", GLYPH_ICON, 766)\nX_GLYPH_DEFINE(\"\\\\{controller button cross}\", GLYPH_ICON, 767)\nX_GLYPH_DEFINE(\"\\\\{controller button circle}\", GLYPH_ICON, 768)\nX_GLYPH_DEFINE(\"\\\\{controller button ps}\", GLYPH_ICON, 769)\nX_GLYPH_DEFINE(\"\\\\{controller button capture}\", GLYPH_ICON, 770)\nX_GLYPH_DEFINE(\"\\\\{controller button touchpad}\", GLYPH_ICON, 771)\nX_GLYPH_DEFINE(\"\\\\{controller button paddle 1}\", GLYPH_ICON, 772)\nX_GLYPH_DEFINE(\"\\\\{controller button paddle 2}\", GLYPH_ICON, 773)\nX_GLYPH_DEFINE(\"\\\\{controller button paddle 3}\", GLYPH_ICON, 774)\nX_GLYPH_DEFINE(\"\\\\{controller button paddle 4}\", GLYPH_ICON, 775)\nX_GLYPH_DEFINE(\"\\\\{controller button share}\", GLYPH_ICON, 776)\nX_GLYPH_DEFINE(\"\\\\{controller button back}\", GLYPH_ICON, 777)\nX_GLYPH_DEFINE(\"\\\\{controller button start}\", GLYPH_ICON, 778)\nX_GLYPH_DEFINE(\"\\\\{controller button mic}\", GLYPH_ICON, 779)\nX_GLYPH_DEFINE(\"\\\\{controller button home}\", GLYPH_ICON, 780)\nX_GLYPH_DEFINE(\"\\\\{controller button options}\", GLYPH_ICON, 781)\n"
  },
  {
    "path": "src/trx/game/ui.h",
    "content": "#pragma once\n\n#include <trx/game/ui/common.h>\n#include <trx/game/ui/dialogs.h>\n#include <trx/game/ui/draw.h>\n#include <trx/game/ui/elements.h>\n#include <trx/game/ui/events.h>\n#include <trx/game/ui/hud.h>\n#include <trx/game/ui/text.h>\n"
  },
  {
    "path": "src/trx/game/viewport.c",
    "content": "#include <trx/game/viewport.h>\n\n#include <trx/config.h>\n#include <trx/core/log.h>\n#include <trx/game/const.h>\n#include <trx/game/output/vars.h>\n#include <trx/game/shell.h>\n\n#define L_DEFAULT_VIEWPORT                                                     \\\n    { .width = SHELL_HEADLESS_WIDTH, .height = SHELL_HEADLESS_HEIGHT }\nstatic VIEWPORT_RECT m_Rects[VIEWPORT_NUMBER_OF] = {\n    [VIEWPORT_WINDOW] = L_DEFAULT_VIEWPORT,\n    [VIEWPORT_TARGET] = L_DEFAULT_VIEWPORT,\n    [VIEWPORT_GAME] = L_DEFAULT_VIEWPORT,\n    [VIEWPORT_UI] = L_DEFAULT_VIEWPORT,\n};\n#undef L_DEFAULT_VIEWPORT\n\nstatic int16_t m_CurrentFOV = 65;\nstatic FOV_MODE m_CurrentFOVMode = FOV_MODE_GAME;\n\nvoid Viewport_Init(int32_t x, int32_t y, int32_t width, int32_t height)\n{\n    const VIEWPORT_RECT *const target = &m_Rects[VIEWPORT_TARGET];\n    VIEWPORT_RECT *const game = &m_Rects[VIEWPORT_GAME];\n    VIEWPORT_RECT *const ui = &m_Rects[VIEWPORT_UI];\n\n    if (x < 0 || y < 0 || width < 0 || height < 0) {\n        struct {\n            int32_t w, h;\n        } ar = { .w = 1, .h = 1 };\n        switch (g_Config.rendering.aspect_mode) {\n        case ASPECT_MODE_4_3:\n            ar.w = 4;\n            ar.h = 3;\n            break;\n        case ASPECT_MODE_16_9:\n            ar.w = 16;\n            ar.h = 9;\n            break;\n        case ASPECT_MODE_16_10:\n            ar.w = 16;\n            ar.h = 10;\n            break;\n        case ASPECT_MODE_ANY:\n            ar.w = target->width;\n            ar.h = target->height;\n            break;\n        }\n\n        x = 0;\n        y = 0;\n        width = target->width;\n        height = target->height;\n        if (g_Config.rendering.aspect_mode != ASPECT_MODE_ANY) {\n            width = height * ar.w / ar.h;\n        }\n    }\n\n    ui->x = x;\n    ui->y = y;\n    ui->width = width;\n    ui->height = height;\n\n    game->x = x;\n    game->y = y;\n    game->width = width / g_Config.rendering.upscaling_factor;\n    game->height = height / g_Config.rendering.upscaling_factor;\n\n    g_PhdLeft = Viewport_GetMinX(VIEWPORT_GAME);\n    g_PhdTop = Viewport_GetMinY(VIEWPORT_GAME);\n    g_PhdRight = Viewport_GetMaxX(VIEWPORT_GAME);\n    g_PhdBottom = Viewport_GetMaxY(VIEWPORT_GAME);\n}\n\nint16_t Viewport_GetSystemFOV(void)\n{\n    return m_CurrentFOV;\n}\n\nint16_t Viewport_GetUserFOV(void)\n{\n    return g_Config.visuals.fov * DEG_1;\n}\n\nint16_t Viewport_GetEffectiveFOV(void)\n{\n    return Viewport_GetSystemFOV() != -1 ? Viewport_GetSystemFOV()\n                                         : Viewport_GetUserFOV();\n}\n\nint32_t Viewport_GetMinX(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space].x;\n}\n\nint32_t Viewport_GetMinY(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space].y;\n}\n\nint32_t Viewport_GetMaxX(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space].x + m_Rects[space].width;\n}\n\nint32_t Viewport_GetMaxY(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space].y + m_Rects[space].height;\n}\n\nint32_t Viewport_GetCenterX(const VIEWPORT_SPACE space)\n{\n    return (m_Rects[space].x + m_Rects[space].width) / 2;\n}\n\nint32_t Viewport_GetCenterY(const VIEWPORT_SPACE space)\n{\n    return (m_Rects[space].y + m_Rects[space].height) / 2;\n}\n\nint32_t Viewport_GetWidth(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space].width;\n}\n\nint32_t Viewport_GetHeight(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space].height;\n}\n\nVIEWPORT_RECT Viewport_GetRect(const VIEWPORT_SPACE space)\n{\n    return m_Rects[space];\n}\n\nvoid Viewport_Reset(void)\n{\n    const SHELL_SIZE size = Shell_GetCurrentSize();\n    VIEWPORT_RECT *const window = &m_Rects[VIEWPORT_WINDOW];\n    VIEWPORT_RECT *const target = &m_Rects[VIEWPORT_TARGET];\n\n    window->x = 0;\n    window->y = 0;\n    window->width = size.w;\n    window->height = size.h;\n\n    int32_t border_x = window->width * g_Config.rendering.borders;\n    const int32_t border_y = window->height * g_Config.rendering.borders;\n    if (g_Config.rendering.aspect_mode == ASPECT_MODE_ANY) {\n        border_x = border_y;\n    }\n    const int32_t max_w = window->width - border_x;\n    const int32_t max_h = window->height - border_y;\n\n    double aspect_ratio = 0.0;\n    switch (g_Config.rendering.aspect_mode) {\n    case ASPECT_MODE_4_3:\n        aspect_ratio = 4.0 / 3.0;\n        break;\n    case ASPECT_MODE_16_9:\n        aspect_ratio = 16.0 / 9.0;\n        break;\n    case ASPECT_MODE_16_10:\n        aspect_ratio = 16.0 / 10.0;\n        break;\n    case ASPECT_MODE_ANY:\n    default:\n        aspect_ratio = (double)max_w / (double)max_h; // just match window\n        break;\n    }\n\n    // Fit the aspect ratio rectangle within max_w x max_h\n    target->width = max_w;\n    target->height = max_w / aspect_ratio;\n    if (target->height > max_h) {\n        // too tall, clamp\n        target->height = max_h;\n        target->width = max_h * aspect_ratio;\n    }\n    target->x = (window->width - target->width) / 2;\n    target->y = (window->height - target->height) / 2;\n    Viewport_Init(-1, -1, -1, -1);\n    Viewport_Debug();\n}\n\nFOV_MODE Viewport_GetFOVMode(void)\n{\n    return m_CurrentFOVMode;\n}\n\nvoid Viewport_AlterFOV(const int16_t fov, const FOV_MODE fov_mode)\n{\n    m_CurrentFOV = fov;\n    m_CurrentFOVMode = fov_mode;\n}\n\nvoid Viewport_Debug(void)\n{\n    const VIEWPORT_RECT *r;\n    r = &m_Rects[VIEWPORT_WINDOW];\n    LOG_TRACE(\"Window viewport: %dx%d+%d,%d\", r->width, r->height, r->x, r->y);\n    r = &m_Rects[VIEWPORT_TARGET];\n    LOG_TRACE(\"Target viewport: %dx%d+%d,%d\", r->width, r->height, r->x, r->y);\n    r = &m_Rects[VIEWPORT_GAME];\n    LOG_TRACE(\"Game viewport: %dx%d+%d,%d\", r->width, r->height, r->x, r->y);\n    r = &m_Rects[VIEWPORT_UI];\n    LOG_TRACE(\"UI viewport: %dx%d+%d,%d\", r->width, r->height, r->x, r->y);\n}\n"
  },
  {
    "path": "src/trx/game/viewport.h",
    "content": "#pragma once\n\n#include <trx/config/enum.h>\n\n#include <stdint.h>\n\ntypedef enum {\n    FOV_MODE_VERTICAL,\n    FOV_MODE_HORIZONTAL,\n    FOV_MODE_PC,\n    FOV_MODE_PS1,\n} FOV_MODE;\n\ntypedef enum {\n    VIEWPORT_WINDOW,\n    VIEWPORT_TARGET,\n    VIEWPORT_GAME,\n    VIEWPORT_UI,\n    VIEWPORT_NUMBER_OF,\n} VIEWPORT_SPACE;\n\ntypedef struct {\n    int32_t x;\n    int32_t y;\n    union {\n        int32_t width, w;\n    };\n    union {\n        int32_t height, h;\n    };\n} VIEWPORT_RECT;\n\nvoid Viewport_Init(int32_t x, int32_t y, int32_t width, int32_t height);\n\nint32_t Viewport_GetWidth(VIEWPORT_SPACE space);\nint32_t Viewport_GetHeight(VIEWPORT_SPACE space);\nint32_t Viewport_GetMinX(VIEWPORT_SPACE space);\nint32_t Viewport_GetMinY(VIEWPORT_SPACE space);\nint32_t Viewport_GetMaxX(VIEWPORT_SPACE space);\nint32_t Viewport_GetMaxY(VIEWPORT_SPACE space);\nint32_t Viewport_GetCenterX(VIEWPORT_SPACE space);\nint32_t Viewport_GetCenterY(VIEWPORT_SPACE space);\nVIEWPORT_RECT Viewport_GetRect(VIEWPORT_SPACE space);\n\n// Return the current FOV as overriden by the game mechanics, such as special\n// cameras or cutscenes. If the FOV is not overriden, returns -1.\nint16_t Viewport_GetSystemFOV(void);\n\n// Returns preferred player FOV.\nint16_t Viewport_GetUserFOV(void);\n\n// Returns the current effective FOV – eg system FOV if it's defined, otherwise\n// the player choice.\nint16_t Viewport_GetEffectiveFOV(void);\n\n// Returns the current FOV formula.\nFOV_MODE Viewport_GetFOVMode(void);\n\n// Sets the system FOV. Set to -1 to fallback to player FOV.\nvoid Viewport_AlterFOV(int16_t view_angle, FOV_MODE fov_mode);\n\n// TODO: decide what to do with this function\nvoid Viewport_Reset(void);\n\nvoid Viewport_Debug(void);\n"
  },
  {
    "path": "src/trx/gl/buffer.c",
    "content": "#include <trx/gl/buffer.h>\n\n#include <trx/debug.h>\n#include <trx/gl/track.h>\n#include <trx/gl/utils.h>\n\nvoid TRX_GL_Buffer_Init(TRX_GL_BUFFER *buf, GLenum target)\n{\n    ASSERT(buf != nullptr);\n    buf->target = target;\n    glGenBuffers(1, &buf->id);\n    TRX_GL_CheckError();\n    buf->initialized = true;\n}\n\nvoid TRX_GL_Buffer_Close(TRX_GL_BUFFER *buf)\n{\n    ASSERT(buf != nullptr);\n    if (buf->initialized) {\n        glDeleteBuffers(1, &buf->id);\n        TRX_GL_CheckError();\n    }\n    buf->initialized = false;\n}\n\nvoid TRX_GL_Buffer_Bind(TRX_GL_BUFFER *buf)\n{\n    ASSERT(buf != nullptr);\n    ASSERT(buf->initialized);\n    glBindBuffer(buf->target, buf->id);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Buffer_Data(\n    TRX_GL_BUFFER *buf, GLsizei size, const void *data, GLenum usage)\n{\n    ASSERT(buf != nullptr);\n    ASSERT(buf->initialized);\n    TRX_GL_TRACK_DATA(glBufferData, buf->target, size, data, usage);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Buffer_SubData(\n    TRX_GL_BUFFER *buf, GLsizei offset, GLsizei size, const void *data)\n{\n    ASSERT(buf != nullptr);\n    ASSERT(buf->initialized);\n    TRX_GL_TRACK_SUBDATA(glBufferSubData, buf->target, offset, size, data);\n    TRX_GL_CheckError();\n}\n\nvoid *TRX_GL_Buffer_Map(TRX_GL_BUFFER *buf, GLenum access)\n{\n    ASSERT(buf != nullptr);\n    ASSERT(buf->initialized);\n    void *ret = glMapBuffer(buf->target, access);\n    TRX_GL_CheckError();\n    return ret;\n}\n\nvoid TRX_GL_Buffer_Unmap(TRX_GL_BUFFER *buf)\n{\n    ASSERT(buf != nullptr);\n    ASSERT(buf->initialized);\n    glUnmapBuffer(buf->target);\n    TRX_GL_CheckError();\n}\n\nGLint TRX_GL_Buffer_Parameter(TRX_GL_BUFFER *buf, GLenum pname)\n{\n    ASSERT(buf != nullptr);\n    ASSERT(buf->initialized);\n    GLint params = 0;\n    glGetBufferParameteriv(buf->target, pname, &params);\n    TRX_GL_CheckError();\n    return params;\n}\n"
  },
  {
    "path": "src/trx/gl/buffer.h",
    "content": "#pragma once\n\n#include <GL/glew.h>\n\ntypedef struct {\n    bool initialized;\n    GLuint id;\n    GLenum target;\n} TRX_GL_BUFFER;\n\nvoid TRX_GL_Buffer_Init(TRX_GL_BUFFER *buf, GLenum target);\nvoid TRX_GL_Buffer_Close(TRX_GL_BUFFER *buf);\n\nvoid TRX_GL_Buffer_Bind(TRX_GL_BUFFER *buf);\nvoid TRX_GL_Buffer_Data(\n    TRX_GL_BUFFER *buf, GLsizei size, const void *data, GLenum usage);\nvoid TRX_GL_Buffer_SubData(\n    TRX_GL_BUFFER *buf, GLsizei offset, GLsizei size, const void *data);\nvoid *TRX_GL_Buffer_Map(TRX_GL_BUFFER *buf, GLenum access);\nvoid TRX_GL_Buffer_Unmap(TRX_GL_BUFFER *buf);\nGLint TRX_GL_Buffer_Parameter(TRX_GL_BUFFER *buf, GLenum pname);\n"
  },
  {
    "path": "src/trx/gl/config.h",
    "content": "#pragma once\n\n#include <trx/gl/enum.h>\n\n#include <stdint.h>\n\ntypedef struct {\n    TEXTURE_FILTER display_filter;\n    bool enable_wireframe;\n    int32_t line_width;\n} TRX_GL_CONFIG;\n"
  },
  {
    "path": "src/trx/gl/context.c",
    "content": "#include <trx/gl/context.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/game/shell.h>\n#include <trx/game/viewport.h>\n#include <trx/gl/renderer.h>\n#include <trx/gl/screenshot.h>\n#include <trx/gl/utils.h>\n\n#include <GL/glew.h>\n#include <SDL2/SDL_video.h>\n#include <string.h>\n\ntypedef struct {\n    SDL_GLContext context;\n    SDL_Window *window_handle;\n    VIEWPORT_SPACE space;\n\n    TRX_GL_CONFIG config;\n\n    // Size of the SDL window.\n    int32_t window_width;\n    int32_t window_height;\n\n    char *scheduled_screenshot_path;\n    TRX_GL_RENDERER *renderer;\n} TRX_GL_CONTEXT;\n\nextern RGBA_F Output_GetFogColor(void);\n\nstatic TRX_GL_CONTEXT m_Context = {};\n\nstatic bool M_IsExtensionSupported(const char *name)\n{\n    int number_of_extensions;\n\n    glGetIntegerv(GL_NUM_EXTENSIONS, &number_of_extensions);\n    TRX_GL_CheckError();\n\n    for (int i = 0; i < number_of_extensions; i++) {\n        const char *gl_ext = (const char *)glGetStringi(GL_EXTENSIONS, i);\n        TRX_GL_CheckError();\n\n        if (gl_ext && !strcmp(gl_ext, name)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic GLvoid GLAPIENTRY M_GLDebug(\n    const GLenum source, const GLenum type, const GLuint id,\n    const GLenum severity, const GLsizei length, const GLchar *const message,\n    const void *const user_param)\n{\n    if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) {\n        return;\n    }\n    size_t len = strlen(message);\n    if (len > 0 && message[len - 1] == '\\n') {\n        len--;\n    }\n    LOG_INFO(\"%d %*s\", source, len, message);\n}\n\nvoid TRX_GL_Context_SwitchToViewport(const VIEWPORT_SPACE space)\n{\n    const VIEWPORT_RECT rect = Viewport_GetRect(space);\n    m_Context.space = space;\n    glViewport(rect.x, rect.y, rect.width, rect.height);\n    TRX_GL_CheckError();\n}\n\nbool TRX_GL_Context_Attach(void *window_handle)\n{\n    const char *shading_ver;\n\n    if (m_Context.window_handle) {\n        LOG_ERROR(\"Context already attached\");\n        return false;\n    }\n\n    LOG_INFO(\"Attaching to window %p\", window_handle);\n    m_Context.context = SDL_GL_CreateContext(window_handle);\n    if (m_Context.context == nullptr) {\n        LOG_ERROR(\"Can't create OpenGL context: %s\", SDL_GetError());\n        return false;\n    }\n\n    m_Context.config.line_width = 1;\n    m_Context.config.enable_wireframe = false;\n    SDL_GetWindowSize(\n        window_handle, &m_Context.window_width, &m_Context.window_height);\n\n    m_Context.window_handle = window_handle;\n\n    if (SDL_GL_MakeCurrent(m_Context.window_handle, m_Context.context)) {\n        Shell_ExitSystemFmt(\n            \"Can't activate OpenGL context: %s\", SDL_GetError());\n    }\n\n    const GLenum err = glewInit();\n    if (err != GLEW_OK) {\n        if (err != 4) {\n            Shell_ExitSystemFmt(\n                \"Can't initialize GLEW for OpenGL extension loading: %d\", err);\n        }\n        // https://github.com/nigels-com/glew/issues/417\n        LOG_WARNING(\"GLEW failed to init: %d\", err);\n    }\n\n    LOG_INFO(\"OpenGL vendor string:   %s\", glGetString(GL_VENDOR));\n    LOG_INFO(\"OpenGL renderer string: %s\", glGetString(GL_RENDERER));\n    LOG_INFO(\"OpenGL version string:  %s\", glGetString(GL_VERSION));\n\n    shading_ver = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);\n    if (shading_ver != nullptr) {\n        LOG_INFO(\"Shading version string: %s\", shading_ver);\n    } else {\n        TRX_GL_CheckError();\n    }\n\n    glClearColor(0, 0, 0, 0);\n    glClearDepth(1);\n    TRX_GL_CheckError();\n\n    // VSync defaults to on unless user disabled it in runtime json\n    SDL_GL_SetSwapInterval(1);\n\n#if DEBUG\n    if (glDebugMessageCallback != nullptr) {\n        glDebugMessageCallback(M_GLDebug, nullptr);\n    }\n    glEnable(GL_DEBUG_OUTPUT);\n#endif\n\n    m_Context.renderer = &g_TRX_GL_Renderer;\n    if (m_Context.renderer->init != nullptr) {\n        m_Context.renderer->init(m_Context.renderer, &m_Context.config);\n    }\n\n    return true;\n}\n\nvoid TRX_GL_Context_Detach(void)\n{\n    if (!m_Context.window_handle) {\n        return;\n    }\n\n    if (m_Context.renderer != nullptr\n        && m_Context.renderer->shutdown != nullptr) {\n        m_Context.renderer->shutdown(m_Context.renderer);\n    }\n\n    SDL_GL_MakeCurrent(nullptr, nullptr);\n\n    if (m_Context.context != nullptr) {\n        SDL_GL_DeleteContext(m_Context.context);\n        m_Context.context = nullptr;\n    }\n    m_Context.window_handle = nullptr;\n}\n\nvoid TRX_GL_Context_SetDisplayFilter(const TEXTURE_FILTER filter)\n{\n    m_Context.config.display_filter = filter;\n}\n\nbool TRX_GL_Context_GetWireframeMode(void)\n{\n    return m_Context.config.enable_wireframe;\n}\n\nvoid TRX_GL_Context_SetWireframeMode(const bool enable)\n{\n    m_Context.config.enable_wireframe = enable;\n}\n\nvoid TRX_GL_Context_SetLineWidth(const int32_t line_width)\n{\n    m_Context.config.line_width = line_width;\n}\n\nvoid TRX_GL_Context_SetVSync(bool vsync)\n{\n    SDL_GL_SetSwapInterval(vsync);\n}\n\nvoid *TRX_GL_Context_GetWindowHandle(void)\n{\n    return m_Context.window_handle;\n}\n\nvoid TRX_GL_Context_Clear(void)\n{\n    const RGBA_F white = { 1.0f, 1.0f, 1.0f, 0.0f };\n    const RGBA_F fog = Output_GetFogColor();\n    const RGBA_F black = { 0.0f, 0.0f, 0.0f, 0.0f };\n    const RGBA_F color =\n        m_Context.space == VIEWPORT_GAME && m_Context.config.enable_wireframe\n        ? white\n        : m_Context.space == VIEWPORT_GAME ? fog\n                                           : black;\n    glClearBufferfv(GL_COLOR, 0, &color.r);\n}\n\nvoid TRX_GL_Context_SwapBuffers(void)\n{\n    glFinish();\n    TRX_GL_CheckError();\n\n    if (m_Context.renderer != nullptr\n        && m_Context.renderer->swap_buffers != nullptr) {\n        m_Context.renderer->swap_buffers(m_Context.renderer);\n    }\n}\n\nvoid TRX_GL_Context_ScheduleScreenshot(const char *path)\n{\n    Memory_FreePointer(&m_Context.scheduled_screenshot_path);\n    m_Context.scheduled_screenshot_path = Memory_DupStr(path);\n}\n\nconst char *TRX_GL_Context_GetScheduledScreenshotPath(void)\n{\n    return m_Context.scheduled_screenshot_path;\n}\n\nvoid TRX_GL_Context_ClearScheduledScreenshotPath(void)\n{\n    Memory_FreePointer(&m_Context.scheduled_screenshot_path);\n}\n\nTRX_GL_CONFIG *TRX_GL_Context_GetConfig(void)\n{\n    return &m_Context.config;\n}\n"
  },
  {
    "path": "src/trx/gl/context.h",
    "content": "#pragma once\n\n#include <trx/game/viewport.h>\n#include <trx/gl/enum.h>\n#include <trx/gl/renderer.h>\n\n#include <stdint.h>\n\nbool TRX_GL_Context_Attach(void *window_handle);\nvoid TRX_GL_Context_Detach(void);\n\nvoid TRX_GL_Context_SetDisplayFilter(TEXTURE_FILTER filter);\nbool TRX_GL_Context_GetWireframeMode(void);\nvoid TRX_GL_Context_SetWireframeMode(bool enable);\nvoid TRX_GL_Context_SetLineWidth(int32_t line_width);\nvoid TRX_GL_Context_SetVSync(bool vsync);\n\nvoid *TRX_GL_Context_GetWindowHandle(void);\n\nvoid TRX_GL_Context_Clear(void);\nvoid TRX_GL_Context_SwapBuffers(void);\nvoid TRX_GL_Context_SetRendered(void);\n\nvoid TRX_GL_Context_SwitchToViewport(VIEWPORT_SPACE space);\n\nvoid TRX_GL_Context_ScheduleScreenshot(const char *path);\nconst char *TRX_GL_Context_GetScheduledScreenshotPath(void);\nvoid TRX_GL_Context_ClearScheduledScreenshotPath(void);\n\nTRX_GL_CONFIG *TRX_GL_Context_GetConfig(void);\n"
  },
  {
    "path": "src/trx/gl/enum.c",
    "content": "#include <trx/gl/enum.h>\n\n#include <trx/core/enum_map.h>\n\nstatic __attribute__((constructor)) void M_Init(void)\n{\n    ENUM_MAP(TEXTURE_FILTER, TEXTURE_FILTER_BILINEAR, \"bilinear\");\n    ENUM_MAP(TEXTURE_FILTER, TEXTURE_FILTER_POINT, \"point\");\n}\n"
  },
  {
    "path": "src/trx/gl/enum.h",
    "content": "#pragma once\n\ntypedef enum {\n    TEXTURE_FILTER_POINT,\n    TEXTURE_FILTER_BILINEAR,\n    TEXTURE_FILTER_NUMBER_OF,\n} TEXTURE_FILTER;\n"
  },
  {
    "path": "src/trx/gl/fbo.c",
    "content": "#include <trx/gl/fbo.h>\n\n#include <trx/core/log.h>\n#include <trx/debug.h>\n#include <trx/game/viewport.h>\n#include <trx/gl/buffer.h>\n#include <trx/gl/context.h>\n#include <trx/gl/program.h>\n#include <trx/gl/texture.h>\n#include <trx/gl/utils.h>\n#include <trx/gl/vertex_array.h>\n\n#include <GL/glew.h>\n\nvoid TRX_GL_FBO_Init(\n    TRX_GL_FBO *const fbo, const int32_t width, const int32_t height,\n    const GLint internal_format, const GLenum format,\n    const bool with_depth_stencil)\n{\n    fbo->width = width;\n    fbo->height = height;\n    fbo->internal_format = internal_format;\n    fbo->format = format;\n    fbo->with_depth_stencil = with_depth_stencil;\n\n    ASSERT(width > 0);\n    ASSERT(height > 0);\n\n    // Allocate color texture (no mipmaps for FBO attachments).\n    TRX_GL_Texture_Init(&fbo->texture, GL_TEXTURE_2D);\n    glBindTexture(GL_TEXTURE_2D, fbo->texture.id);\n    TRX_GL_CheckError();\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexImage2D(\n        GL_TEXTURE_2D, 0, internal_format, width, height, 0, format,\n        GL_UNSIGNED_BYTE, nullptr);\n    glClearColor(0.0, 0.0, 0.0, 1.0);\n    TRX_GL_CheckError();\n\n    glGenFramebuffers(1, &fbo->fbo);\n    TRX_GL_CheckError();\n    glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo);\n    TRX_GL_CheckError();\n\n    glFramebufferTexture2D(\n        GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbo->texture.id,\n        0);\n    TRX_GL_CheckError();\n\n    // direct draw to color attachment 0.\n    glDrawBuffer(GL_COLOR_ATTACHMENT0);\n    TRX_GL_CheckError();\n\n    if (with_depth_stencil) {\n        glGenRenderbuffers(1, &fbo->rbo);\n        TRX_GL_CheckError();\n        glBindRenderbuffer(GL_RENDERBUFFER, fbo->rbo);\n        TRX_GL_CheckError();\n        glRenderbufferStorage(\n            GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);\n        TRX_GL_CheckError();\n        glBindRenderbuffer(GL_RENDERBUFFER, 0);\n        TRX_GL_CheckError();\n        glFramebufferRenderbuffer(\n            GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER,\n            fbo->rbo);\n        TRX_GL_CheckError();\n    } else {\n        fbo->rbo = 0;\n    }\n\n    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {\n        LOG_ERROR(\"framebuffer is not complete!\");\n    }\n\n    glBindFramebuffer(GL_FRAMEBUFFER, 0);\n}\n\nvoid TRX_GL_FBO_Close(TRX_GL_FBO *fbo)\n{\n    if (fbo->rbo) {\n        glDeleteRenderbuffers(1, &fbo->rbo);\n        fbo->rbo = 0;\n    }\n    if (fbo->fbo) {\n        glDeleteFramebuffers(1, &fbo->fbo);\n        fbo->fbo = 0;\n    }\n    TRX_GL_Texture_Close(&fbo->texture);\n}\n\nvoid TRX_GL_FBO_ResizeIfNeeded(\n    TRX_GL_FBO *const fbo, const int32_t width, const int32_t height)\n{\n    if (width == fbo->width && height == fbo->height) {\n        return;\n    }\n\n    const GLint internal_format = fbo->internal_format;\n    const GLenum format = fbo->format;\n    const bool with_depth_stencil = fbo->with_depth_stencil;\n\n    TRX_GL_FBO_Close(fbo);\n    TRX_GL_FBO_Init(\n        fbo, width, height, internal_format, format, with_depth_stencil);\n}\n\nvoid TRX_GL_FBO_Bind(const TRX_GL_FBO *const fbo)\n{\n    glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_FBO_Unbind(void)\n{\n    glBindFramebuffer(GL_FRAMEBUFFER, 0);\n    TRX_GL_CheckError();\n}\n"
  },
  {
    "path": "src/trx/gl/fbo.h",
    "content": "// Framebuffer object abstraction for off-screen rendering.\n\n#pragma once\n\n#include <trx/game/viewport.h>\n#include <trx/gl/context.h>\n#include <trx/gl/texture.h>\n\n#include <GL/glew.h>\n\n// Off-screen framebuffer with a single color attachment and optional\n// depth+stencil.\ntypedef struct {\n    int32_t width;\n    int32_t height;\n    GLuint fbo;\n    GLuint rbo;\n    GLint internal_format;\n    GLenum format;\n    bool with_depth_stencil;\n    TRX_GL_TEXTURE texture;\n} TRX_GL_FBO;\n\n// Initialize an off-screen FBO.\nvoid TRX_GL_FBO_Init(\n    TRX_GL_FBO *fbo, int32_t width, int32_t height, GLint internal_format,\n    GLenum format, bool with_depth_stencil);\n\n// Close and free the GL resources.\nvoid TRX_GL_FBO_Close(TRX_GL_FBO *fbo);\n\n// Resize the FBO attachments (reallocate textures and RBO).\nvoid TRX_GL_FBO_ResizeIfNeeded(TRX_GL_FBO *fbo, int32_t width, int32_t height);\n\n// Bind this FBO for rendering (GL_FRAMEBUFFER).\nvoid TRX_GL_FBO_Bind(const TRX_GL_FBO *fbo);\n\n// Bind the default framebuffer (0).\nvoid TRX_GL_FBO_Unbind(void);\n"
  },
  {
    "path": "src/trx/gl/program.c",
    "content": "#include <trx/gl/program.h>\n\n#include <trx/core/filesystem.h>\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/core/vector.h>\n#include <trx/debug.h>\n#include <trx/game/shell.h>\n#include <trx/gl/track.h>\n#include <trx/gl/utils.h>\n\n#include <stdio.h>\n#include <string.h>\n\ntypedef struct {\n    char *path;\n    char *content;\n} M_SHADER_FILE_CACHE_ENTRY;\n\nstatic VECTOR *m_ShaderFileCache = nullptr; // M_SHADER_FILE_CACHE_ENTRY\n\nstatic const char *M_LoadFileCached(const char *const path)\n{\n    ASSERT(path != nullptr);\n    if (m_ShaderFileCache == nullptr) {\n        m_ShaderFileCache = Vector_Create(sizeof(M_SHADER_FILE_CACHE_ENTRY));\n    }\n    for (int32_t i = 0; i < m_ShaderFileCache->count; i++) {\n        M_SHADER_FILE_CACHE_ENTRY *const entry =\n            Vector_Get(m_ShaderFileCache, i);\n        if (strcmp(entry->path, path) == 0) {\n            return entry->content;\n        }\n    }\n\n    char *content = nullptr;\n    if (!File_Load(path, &content, nullptr)) {\n        return nullptr;\n    }\n    M_SHADER_FILE_CACHE_ENTRY entry = {\n        .path = Memory_DupStr(path),\n        .content = content,\n    };\n    Vector_Add(m_ShaderFileCache, &entry);\n    return entry.content;\n}\n\n__attribute__((destructor)) static void M_ShutdownCache(void)\n{\n    if (m_ShaderFileCache == nullptr) {\n        return;\n    }\n    for (int32_t i = 0; i < m_ShaderFileCache->count; i++) {\n        M_SHADER_FILE_CACHE_ENTRY *const entry =\n            Vector_Get(m_ShaderFileCache, i);\n        Memory_FreePointer(&entry->path);\n        Memory_FreePointer(&entry->content);\n    }\n    Vector_Free(m_ShaderFileCache);\n    m_ShaderFileCache = nullptr;\n}\n\nstatic char *M_PreprocessIncludes(const char *src, const char *dir)\n{\n    ASSERT(src != nullptr);\n    ASSERT(dir != nullptr);\n\n    const char *p = src;\n    size_t result_cap = strlen(src) + 1;\n    char *result = Memory_Alloc(result_cap);\n    size_t used = 0;\n    result[0] = '\\0';\n\n    while (*p != '\\0') {\n        const char *include = strstr(p, \"#include\");\n        if (include == nullptr) {\n            size_t tail_len = strlen(p);\n            if (used + tail_len + 1 > result_cap) {\n                result_cap = (used + tail_len + 1) * 2;\n                result = Memory_Realloc(result, result_cap);\n            }\n            memcpy(result + used, p, tail_len);\n            used += tail_len;\n            result[used] = '\\0';\n            break;\n        }\n\n        // Copy text before #include\n        size_t prefix_len = include - p;\n        if (prefix_len > 0) {\n            if (used + prefix_len + 1 > result_cap) {\n                result_cap = (used + prefix_len + 1) * 2;\n                result = Memory_Realloc(result, result_cap);\n            }\n            memcpy(result + used, p, prefix_len);\n            used += prefix_len;\n            result[used] = '\\0';\n        }\n\n        // Parse filename between quotes\n        const char *start_quote = strchr(include, '\"');\n        const char *end_quote =\n            start_quote ? strchr(start_quote + 1, '\"') : nullptr;\n        if (!start_quote || !end_quote) {\n            Shell_ExitSystemFmt(\n                \"Malformed #include directive near: %.32s\", include);\n        }\n\n        char filename[512];\n        strncpy(filename, start_quote + 1, end_quote - start_quote - 1);\n        filename[end_quote - start_quote - 1] = '\\0';\n\n        // Build relative path\n        char full_path[1024];\n        snprintf(full_path, sizeof(full_path), \"%s/%s\", dir, filename);\n\n        const char *const include_src = M_LoadFileCached(full_path);\n        if (include_src == nullptr) {\n            Shell_ExitSystemFmt(\"Failed to include shader file: %s\", full_path);\n        }\n\n        // Handle nested includes\n        char *include_dir = File_GetParentDirectory(full_path);\n        char *processed_include =\n            M_PreprocessIncludes(include_src, include_dir ? include_dir : dir);\n        Memory_FreePointer(&include_dir);\n\n        // Append included content\n        size_t block_len = strlen(processed_include);\n        if (used + block_len + 1 > result_cap) {\n            result_cap = (used + block_len + 1) * 2;\n            result = Memory_Realloc(result, result_cap);\n        }\n        memcpy(result + used, processed_include, block_len);\n        used += block_len;\n        result[used] = '\\0';\n\n        Memory_FreePointer(&processed_include);\n\n        // Move past include line\n        const char *next_line = strchr(end_quote, '\\n');\n        p = next_line ? next_line + 1 : end_quote + 1;\n    }\n\n    result[used] = '\\0';\n    return result;\n}\n\nstatic char *M_Preprocess(const char *content, GLenum type)\n{\n    ASSERT(content != nullptr);\n\n    const char *version_ogl33c = \"#version 330 core\\n\";\n    const char *define_vertex = \"#define VERTEX\\n\";\n    const char *define_fragment = \"#define FRAGMENT\\n\";\n\n    size_t bufsize = strlen(content) + 1;\n\n    bufsize += strlen(version_ogl33c);\n\n    if (type == GL_VERTEX_SHADER) {\n        bufsize += strlen(define_vertex);\n    } else if (type == GL_FRAGMENT_SHADER) {\n        bufsize += strlen(define_fragment);\n    }\n\n    char *processed_content = Memory_Alloc(bufsize);\n    strcpy(processed_content, version_ogl33c);\n\n    if (type == GL_VERTEX_SHADER) {\n        strcat(processed_content, define_vertex);\n    } else if (type == GL_FRAGMENT_SHADER) {\n        strcat(processed_content, define_fragment);\n    }\n\n    strcat(processed_content, content);\n    return processed_content;\n}\n\nbool TRX_GL_Program_Init(TRX_GL_PROGRAM *const program)\n{\n    ASSERT(program != nullptr);\n    program->id = glCreateProgram();\n    TRX_GL_CheckError();\n    if (!program->id) {\n        LOG_ERROR(\"Can't create shader program\");\n        return false;\n    }\n    return true;\n}\n\nvoid TRX_GL_Program_Close(TRX_GL_PROGRAM *const program)\n{\n    ASSERT(program != nullptr);\n    Memory_FreePointer(&program->path);\n    if (program->id) {\n        glDeleteProgram(program->id);\n        TRX_GL_CheckError();\n        program->id = 0;\n    }\n}\n\nvoid TRX_GL_Program_Bind(const TRX_GL_PROGRAM *const program)\n{\n    ASSERT(program != nullptr);\n    glUseProgram(program->id);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Program_AttachShader(\n    TRX_GL_PROGRAM *program, GLenum type, const char *path)\n{\n    ASSERT(program != nullptr);\n    ASSERT(path != nullptr);\n\n    const char *const resolved_path =\n        TRXPath_Resolve(TRX_DYNAMIC_PATH_SHADER_FILE, path);\n    Memory_FreePointer(&program->path);\n    program->path = Memory_DupStr(resolved_path);\n\n    GLuint shader_id = glCreateShader(type);\n    TRX_GL_CheckError();\n    if (!shader_id) {\n        Shell_ExitSystem(\"Failed to create shader\");\n    }\n\n    const char *content = M_LoadFileCached(program->path);\n    char *processed_content = nullptr;\n    if (content == nullptr) {\n        Shell_ExitSystemFmt(\"Unable to find shader file: %s\", program->path);\n    }\n\n    char *shader_dir = File_GetParentDirectory(program->path);\n    processed_content = M_PreprocessIncludes(content, shader_dir);\n    ASSERT(processed_content != nullptr);\n    Memory_FreePointer(&shader_dir);\n\n    char *expanded_content = processed_content;\n    processed_content = M_Preprocess(expanded_content, type);\n    ASSERT(processed_content != nullptr);\n    Memory_FreePointer(&expanded_content);\n\n    glShaderSource(\n        shader_id, 1, (const char *const *)&processed_content, nullptr);\n\n    TRX_GL_CheckError();\n    glCompileShader(shader_id);\n    TRX_GL_CheckError();\n\n    int compile_status;\n    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compile_status);\n    TRX_GL_CheckError();\n\n    if (compile_status != GL_TRUE) {\n        GLsizei info_log_size = 4096;\n        char info_log[info_log_size];\n        glGetShaderInfoLog(shader_id, info_log_size, &info_log_size, info_log);\n        TRX_GL_CheckError();\n\n        if (info_log[0]) {\n            Shell_ExitSystemFmt(\n                \"%s: compilation failed\\n%s\", program->path, info_log);\n        } else {\n            Shell_ExitSystemFmt(\"%s: compilation failed.\", program->path);\n        }\n    }\n\n    Memory_FreePointer(&processed_content);\n\n    glAttachShader(program->id, shader_id);\n    TRX_GL_CheckError();\n\n    glDeleteShader(shader_id);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Program_Link(TRX_GL_PROGRAM *const program)\n{\n    ASSERT(program != nullptr);\n    glLinkProgram(program->id);\n    TRX_GL_CheckError();\n\n    GLint linkStatus;\n    glGetProgramiv(program->id, GL_LINK_STATUS, &linkStatus);\n    TRX_GL_CheckError();\n\n    if (!linkStatus) {\n        GLsizei info_log_size = 4096;\n        char info_log[info_log_size];\n        glGetProgramInfoLog(\n            program->id, info_log_size, &info_log_size, info_log);\n        TRX_GL_CheckError();\n        if (info_log[0]) {\n            Shell_ExitSystemFmt(\n                \"%s: shader linking failed\\n%s\", program->path, info_log);\n        } else {\n            Shell_ExitSystemFmt(\"%s: shader linking failed\", program->path);\n        }\n    }\n}\n\nvoid TRX_GL_Program_FragmentData(\n    TRX_GL_PROGRAM *const program, const char *const name)\n{\n    ASSERT(program != nullptr);\n    glBindFragDataLocation(program->id, 0, name);\n    TRX_GL_CheckError();\n}\n\nGLint TRX_GL_Program_UniformLocation(\n    TRX_GL_PROGRAM *const program, const char *const name)\n{\n    ASSERT(program != nullptr);\n    GLint location = glGetUniformLocation(program->id, name);\n    TRX_GL_CheckError();\n    if (location == -1) {\n        LOG_INFO(\"%s: uniform not found (%s)\", program->path, name);\n    }\n    return location;\n}\n\nvoid TRX_GL_Program_Uniform4f(\n    TRX_GL_PROGRAM *const program, const GLint loc, const GLfloat v0,\n    const GLfloat v1, const GLfloat v2, const GLfloat v3)\n{\n    ASSERT(program != nullptr);\n    TRX_GL_TRACK_UNIFORM(glUniform4f, loc, v0, v1, v2, v3);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Program_Uniform1i(\n    TRX_GL_PROGRAM *const program, const GLint loc, const GLint v0)\n{\n    ASSERT(program != nullptr);\n    TRX_GL_TRACK_UNIFORM(glUniform1i, loc, v0);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Program_Uniform1f(\n    TRX_GL_PROGRAM *const program, const GLint loc, const GLfloat v0)\n{\n    ASSERT(program != nullptr);\n    TRX_GL_TRACK_UNIFORM(glUniform1f, loc, v0);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Program_Uniform2f(\n    TRX_GL_PROGRAM *const program, const GLint loc, const GLfloat v0,\n    const GLfloat v1)\n{\n    ASSERT(program != nullptr);\n    TRX_GL_TRACK_UNIFORM(glUniform2f, loc, v0, v1);\n    TRX_GL_CheckError();\n}\n"
  },
  {
    "path": "src/trx/gl/program.h",
    "content": "#pragma once\n\n#include <trx/gl/enum.h>\n\n#include <GL/glew.h>\n\ntypedef struct {\n    char *path;\n    bool initialized;\n    GLuint id;\n} TRX_GL_PROGRAM;\n\nbool TRX_GL_Program_Init(TRX_GL_PROGRAM *program);\nvoid TRX_GL_Program_Close(TRX_GL_PROGRAM *program);\n\nvoid TRX_GL_Program_Bind(const TRX_GL_PROGRAM *program);\nvoid TRX_GL_Program_AttachShader(\n    TRX_GL_PROGRAM *program, GLenum type, const char *path);\nvoid TRX_GL_Program_Link(TRX_GL_PROGRAM *program);\nvoid TRX_GL_Program_FragmentData(TRX_GL_PROGRAM *program, const char *name);\nGLint TRX_GL_Program_UniformLocation(TRX_GL_PROGRAM *program, const char *name);\n\nvoid TRX_GL_Program_Uniform4f(\n    TRX_GL_PROGRAM *program, GLint loc, GLfloat v0, GLfloat v1, GLfloat v2,\n    GLfloat v3);\nvoid TRX_GL_Program_Uniform1i(TRX_GL_PROGRAM *program, GLint loc, GLint v0);\nvoid TRX_GL_Program_Uniform1f(TRX_GL_PROGRAM *program, GLint loc, GLfloat v0);\nvoid TRX_GL_Program_Uniform2f(\n    TRX_GL_PROGRAM *program, GLint loc, GLfloat v0, GLfloat v1);\n"
  },
  {
    "path": "src/trx/gl/renderer.c",
    "content": "#include <trx/gl/renderer.h>\n\n#include <trx/core/log.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/gl/buffer.h>\n#include <trx/gl/context.h>\n#include <trx/gl/enum.h>\n#include <trx/gl/fbo.h>\n#include <trx/gl/program.h>\n#include <trx/gl/sampler.h>\n#include <trx/gl/screenshot.h>\n#include <trx/gl/texture.h>\n#include <trx/gl/utils.h>\n#include <trx/gl/vertex_array.h>\n\n#include <GL/glew.h>\n#include <SDL2/SDL_video.h>\n#include <stdint.h>\n\ntypedef struct {\n    const TRX_GL_CONFIG *config;\n\n    TRX_GL_FBO geometry_fbo;\n    TRX_GL_FBO ui_fbo;\n\n    // Full-screen quad resources for blitting FBOs to default framebuffer.\n    TRX_GL_VERTEX_ARRAY vertex_array;\n    TRX_GL_BUFFER buffer;\n    TRX_GL_SAMPLER sampler;\n    TRX_GL_PROGRAM program;\n} M_CONTEXT;\n\nstatic void M_Blit(const M_CONTEXT *const p, const TRX_GL_FBO *const fbo)\n{\n    TRX_GL_Texture_Bind(&fbo->texture);\n    glDrawArrays(GL_TRIANGLES, 0, 6);\n    TRX_GL_CheckError();\n}\n\nstatic void M_UpdateFBOSizes(TRX_GL_RENDERER *renderer)\n{\n    M_CONTEXT *const p = renderer->priv;\n    const TRX_GL_CONFIG *const config = p->config;\n\n    VIEWPORT_RECT rect;\n    rect = Viewport_GetRect(VIEWPORT_GAME);\n    TRX_GL_FBO_ResizeIfNeeded(&p->geometry_fbo, rect.width, rect.height);\n    rect = Viewport_GetRect(VIEWPORT_UI);\n    TRX_GL_FBO_ResizeIfNeeded(&p->ui_fbo, rect.width, rect.height);\n}\n\nstatic void M_Render(TRX_GL_RENDERER *renderer)\n{\n    ASSERT(renderer != nullptr);\n    M_CONTEXT *const p = renderer->priv;\n    ASSERT(p != nullptr);\n\n    const GLuint filter = p->config->display_filter == TEXTURE_FILTER_BILINEAR\n        ? GL_LINEAR\n        : GL_NEAREST;\n\n    TRX_GL_FBO_Unbind();\n\n    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);\n    TRX_GL_CheckError();\n\n    TRX_GL_Program_Bind(&p->program);\n    TRX_GL_Buffer_Bind(&p->buffer);\n    TRX_GL_VertexArray_Bind(&p->vertex_array);\n    glActiveTexture(GL_TEXTURE0);\n    glDisable(GL_DEPTH_TEST);\n\n    TRX_GL_Sampler_Bind(&p->sampler, 0);\n    TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MAG_FILTER, filter);\n    TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MIN_FILTER, filter);\n\n    VIEWPORT_RECT rect = Viewport_GetRect(VIEWPORT_TARGET);\n    glViewport(rect.x, rect.y, rect.width, rect.height);\n    TRX_GL_CheckError();\n\n    // Composite geometry FBO (opaque)\n    glDisable(GL_BLEND);\n    M_Blit(p, &p->geometry_fbo);\n\n    // Composite UI FBO (with premultiplied alpha blending)\n    glEnable(GL_BLEND);\n    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n    M_Blit(p, &p->ui_fbo);\n    glDisable(GL_BLEND);\n\n    if (TRX_GL_Context_GetScheduledScreenshotPath() != nullptr) {\n        TRX_GL_Context_SwitchToViewport(VIEWPORT_TARGET);\n        TRX_GL_Screenshot_CaptureToFile(\n            TRX_GL_Context_GetScheduledScreenshotPath());\n        TRX_GL_Context_ClearScheduledScreenshotPath();\n    }\n}\n\nstatic void M_SwapBuffers(TRX_GL_RENDERER *const renderer)\n{\n    M_CONTEXT *const p = renderer->priv;\n\n    M_Render(renderer);\n    SDL_GL_SwapWindow(TRX_GL_Context_GetWindowHandle());\n    M_UpdateFBOSizes(renderer);\n\n    TRX_GL_Context_SwitchToViewport(VIEWPORT_WINDOW);\n    TRX_GL_Context_Clear();\n\n    // Rebind geometry FBO for the next frame\n    TRX_GL_Renderer_BindGeometryFbo();\n    TRX_GL_Context_SwitchToViewport(VIEWPORT_GAME);\n    TRX_GL_Context_Clear();\n}\n\nstatic void M_Init(\n    TRX_GL_RENDERER *const renderer, const TRX_GL_CONFIG *const config)\n{\n    ASSERT(renderer != nullptr);\n    renderer->priv = (M_CONTEXT *)Memory_Alloc(sizeof(M_CONTEXT));\n    M_CONTEXT *const p = renderer->priv;\n    ASSERT(p != nullptr);\n\n    p->config = config;\n\n    TRX_GL_Buffer_Init(&p->buffer, GL_ARRAY_BUFFER);\n    TRX_GL_Buffer_Bind(&p->buffer);\n    const GLfloat verts[] = {\n        0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,\n    };\n    TRX_GL_Buffer_Data(&p->buffer, sizeof(verts), verts, GL_STATIC_DRAW);\n\n    TRX_GL_VertexArray_Init(&p->vertex_array);\n    TRX_GL_VertexArray_Bind(&p->vertex_array);\n    TRX_GL_VertexArray_Attribute(\n        &p->vertex_array, 0, 2, GL_FLOAT, GL_FALSE, 0, 0);\n\n    TRX_GL_Sampler_Init(&p->sampler);\n    TRX_GL_Sampler_Bind(&p->sampler, 0);\n    TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n    TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n    TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    TRX_GL_Sampler_Parameteri(&p->sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n\n    TRX_GL_Program_Init(&p->program);\n    TRX_GL_Program_AttachShader(&p->program, GL_VERTEX_SHADER, \"fbo.glsl\");\n    TRX_GL_Program_AttachShader(&p->program, GL_FRAGMENT_SHADER, \"fbo.glsl\");\n    TRX_GL_Program_FragmentData(&p->program, \"outColor\");\n    TRX_GL_Program_Link(&p->program);\n    TRX_GL_Program_Bind(&p->program);\n    TRX_GL_Program_Uniform1i(\n        &p->program, TRX_GL_Program_UniformLocation(&p->program, \"uTex0\"), 0);\n\n    VIEWPORT_RECT rect;\n    rect = Viewport_GetRect(VIEWPORT_GAME);\n    TRX_GL_FBO_Init(\n        &p->geometry_fbo, rect.width, rect.height, GL_RGBA8, GL_RGBA, true);\n\n    rect = Viewport_GetRect(VIEWPORT_UI);\n    TRX_GL_FBO_Init(\n        &p->ui_fbo, rect.width, rect.height, GL_RGBA8, GL_RGBA, false);\n}\n\nstatic void M_Shutdown(TRX_GL_RENDERER *renderer)\n{\n    LOG_INFO(\"\");\n\n    ASSERT(renderer != nullptr);\n    M_CONTEXT *const p = renderer->priv;\n    ASSERT(p != nullptr);\n\n    TRX_GL_FBO_Close(&p->geometry_fbo);\n    TRX_GL_FBO_Close(&p->ui_fbo);\n    TRX_GL_Program_Close(&p->program);\n    TRX_GL_Sampler_Close(&p->sampler);\n    TRX_GL_Buffer_Close(&p->buffer);\n    TRX_GL_VertexArray_Close(&p->vertex_array);\n\n    Memory_FreePointer(&renderer->priv);\n}\n\nTRX_GL_RENDERER g_TRX_GL_Renderer = {\n    .swap_buffers = &M_SwapBuffers,\n    .init = &M_Init,\n    .shutdown = &M_Shutdown,\n};\n\nvoid TRX_GL_Renderer_BindGeometryFbo(void)\n{\n    M_CONTEXT *const p = (M_CONTEXT *)g_TRX_GL_Renderer.priv;\n    TRX_GL_FBO_Bind(&p->geometry_fbo);\n}\n\nvoid TRX_GL_Renderer_BindUiFbo(void)\n{\n    M_CONTEXT *const p = (M_CONTEXT *)g_TRX_GL_Renderer.priv;\n    TRX_GL_FBO_Bind(&p->ui_fbo);\n}\n"
  },
  {
    "path": "src/trx/gl/renderer.h",
    "content": "#pragma once\n\n#include <trx/gl/config.h>\n\ntypedef struct TRX_GL_Renderer {\n    void (*init)(struct TRX_GL_Renderer *renderer, const TRX_GL_CONFIG *config);\n    void (*shutdown)(struct TRX_GL_Renderer *renderer);\n    void (*swap_buffers)(struct TRX_GL_Renderer *renderer);\n    void *priv;\n} TRX_GL_RENDERER;\n\nextern TRX_GL_RENDERER g_TRX_GL_Renderer;\n\n// Bind the geometry framebuffer for rendering the 3D scene.\nvoid TRX_GL_Renderer_BindGeometryFbo(void);\n\n// Bind the UI framebuffer for rendering the UI overlay.\nvoid TRX_GL_Renderer_BindUiFbo(void);\n"
  },
  {
    "path": "src/trx/gl/sampler.c",
    "content": "#include <trx/gl/sampler.h>\n\n#include <trx/debug.h>\n#include <trx/gl/utils.h>\n\nvoid TRX_GL_Sampler_Init(TRX_GL_SAMPLER *sampler)\n{\n    ASSERT(sampler != nullptr);\n    glGenSamplers(1, &sampler->id);\n    TRX_GL_CheckError();\n    sampler->initialized = true;\n}\n\nvoid TRX_GL_Sampler_Close(TRX_GL_SAMPLER *sampler)\n{\n    ASSERT(sampler != nullptr);\n    if (sampler->initialized) {\n        glDeleteSamplers(1, &sampler->id);\n        TRX_GL_CheckError();\n    }\n    sampler->initialized = false;\n}\n\nvoid TRX_GL_Sampler_Bind(TRX_GL_SAMPLER *sampler, GLuint unit)\n{\n    ASSERT(sampler != nullptr);\n    ASSERT(sampler->initialized);\n    glBindSampler(unit, sampler->id);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Sampler_Parameteri(\n    TRX_GL_SAMPLER *sampler, GLenum pname, GLint param)\n{\n    ASSERT(sampler != nullptr);\n    ASSERT(sampler->initialized);\n    glSamplerParameteri(sampler->id, pname, param);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Sampler_Parameterf(\n    TRX_GL_SAMPLER *sampler, GLenum pname, GLfloat param)\n{\n    ASSERT(sampler != nullptr);\n    ASSERT(sampler->initialized);\n    glSamplerParameterf(sampler->id, pname, param);\n    TRX_GL_CheckError();\n}\n"
  },
  {
    "path": "src/trx/gl/sampler.h",
    "content": "#pragma once\n\n#include <GL/glew.h>\n\ntypedef struct {\n    bool initialized;\n    GLuint id;\n} TRX_GL_SAMPLER;\n\nvoid TRX_GL_Sampler_Init(TRX_GL_SAMPLER *sampler);\nvoid TRX_GL_Sampler_Close(TRX_GL_SAMPLER *sampler);\n\nvoid TRX_GL_Sampler_Bind(TRX_GL_SAMPLER *sampler, GLuint unit);\nvoid TRX_GL_Sampler_Parameteri(\n    TRX_GL_SAMPLER *sampler, GLenum pname, GLint param);\nvoid TRX_GL_Sampler_Parameterf(\n    TRX_GL_SAMPLER *sampler, GLenum pname, GLfloat param);\n"
  },
  {
    "path": "src/trx/gl/screenshot.c",
    "content": "#include <trx/gl/screenshot.h>\n\n#include <trx/av/image.h>\n#include <trx/core/memory.h>\n#include <trx/debug.h>\n#include <trx/gl/utils.h>\n\n#include <string.h>\n\nbool TRX_GL_Screenshot_CaptureToFile(const char *path)\n{\n    bool ret = false;\n\n    GLint width;\n    GLint height;\n    TRX_GL_Screenshot_CaptureToBuffer(\n        nullptr, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE, true);\n\n    IMAGE *image = Image_Create(width, height);\n    ASSERT(image != nullptr);\n\n    TRX_GL_Screenshot_CaptureToBuffer(\n        (uint8_t *)image->data, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE,\n        true);\n\n    ret = Image_SaveToFile(image, path);\n\n    if (image) {\n        Image_Free(image);\n    }\n    return ret;\n}\n\nvoid TRX_GL_Screenshot_CaptureToBuffer(\n    uint8_t *out_buffer, GLint *out_width, GLint *out_height, GLint depth,\n    GLenum format, GLenum type, bool vflip)\n{\n    ASSERT(out_width != nullptr);\n    ASSERT(out_height != nullptr);\n\n    GLint viewport[4];\n    glGetIntegerv(GL_VIEWPORT, viewport);\n    TRX_GL_CheckError();\n\n    GLint x = viewport[0];\n    GLint y = viewport[1];\n    *out_width = viewport[2];\n    *out_height = viewport[3];\n\n    if (!out_buffer) {\n        return;\n    }\n\n    GLint pitch = *out_width * depth;\n\n    glPixelStorei(GL_PACK_ALIGNMENT, 1);\n    TRX_GL_CheckError();\n    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n    TRX_GL_CheckError();\n\n    glReadBuffer(GL_BACK);\n    TRX_GL_CheckError();\n    glReadPixels(x, y, *out_width, *out_height, format, type, out_buffer);\n    TRX_GL_CheckError();\n\n    if (vflip) {\n        uint8_t *scanline = Memory_Alloc(pitch);\n        for (int y1 = 0, middle = *out_height / 2; y1 < middle; y1++) {\n            int y2 = *out_height - 1 - y1;\n            memcpy(scanline, &out_buffer[y1 * pitch], pitch);\n            memcpy(&out_buffer[y1 * pitch], &out_buffer[y2 * pitch], pitch);\n            memcpy(&out_buffer[y2 * pitch], scanline, pitch);\n        }\n        Memory_FreePointer(&scanline);\n    }\n}\n"
  },
  {
    "path": "src/trx/gl/screenshot.h",
    "content": "#pragma once\n\n#include <GL/glew.h>\n#include <stdint.h>\n\nbool TRX_GL_Screenshot_CaptureToFile(const char *path);\n\nvoid TRX_GL_Screenshot_CaptureToBuffer(\n    uint8_t *out_buffer, GLint *out_width, GLint *out_height, GLint depth,\n    GLenum format, GLenum type, bool vflip);\n"
  },
  {
    "path": "src/trx/gl/texture.c",
    "content": "#include <trx/gl/texture.h>\n\n#include <trx/core/memory.h>\n#include <trx/core/utils.h>\n#include <trx/debug.h>\n#include <trx/gl/utils.h>\n\nTRX_GL_TEXTURE *TRX_GL_Texture_Create(GLenum target)\n{\n    TRX_GL_TEXTURE *texture = Memory_Alloc(sizeof(TRX_GL_TEXTURE));\n    TRX_GL_Texture_Init(texture, target);\n    return texture;\n}\n\nvoid TRX_GL_Texture_Free(TRX_GL_TEXTURE *texture)\n{\n    if (texture != nullptr) {\n        TRX_GL_Texture_Close(texture);\n        Memory_FreePointer(&texture);\n    }\n}\n\nvoid TRX_GL_Texture_Init(TRX_GL_TEXTURE *texture, GLenum target)\n{\n    ASSERT(texture != nullptr);\n    texture->target = target;\n    glGenTextures(1, &texture->id);\n    TRX_GL_CheckError();\n    texture->initialized = true;\n}\n\nvoid TRX_GL_Texture_Close(TRX_GL_TEXTURE *texture)\n{\n    ASSERT(texture != nullptr);\n    if (texture->initialized) {\n        glDeleteTextures(1, &texture->id);\n        TRX_GL_CheckError();\n    }\n    texture->initialized = false;\n}\n\nvoid TRX_GL_Texture_Bind(const TRX_GL_TEXTURE *texture)\n{\n    ASSERT(texture != nullptr);\n    ASSERT(texture->initialized);\n    glBindTexture(texture->target, texture->id);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Texture_Load(\n    TRX_GL_TEXTURE *texture, const void *data, int width, int height,\n    GLint internal_format, GLint format)\n{\n    ASSERT(texture != nullptr);\n    ASSERT(texture->initialized);\n\n    TRX_GL_Texture_Bind(texture);\n\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexImage2D(\n        GL_TEXTURE_2D, 0, internal_format, width, height, 0, format,\n        GL_UNSIGNED_BYTE, data);\n    TRX_GL_CheckError();\n\n    glGenerateMipmap(GL_TEXTURE_2D);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_Texture_LoadFromBackBuffer(TRX_GL_TEXTURE *const texture)\n{\n    ASSERT(texture != nullptr);\n    ASSERT(texture->initialized);\n\n    TRX_GL_Texture_Bind(texture);\n\n    GLint viewport[4];\n    glGetIntegerv(GL_VIEWPORT, viewport);\n    TRX_GL_CheckError();\n\n    const GLint vp_x = viewport[0];\n    const GLint vp_y = viewport[1];\n    const GLint vp_w = viewport[2];\n    const GLint vp_h = viewport[3];\n\n    const int32_t side = MIN(vp_w, vp_h);\n    const int32_t x = vp_x + (vp_w - side) / 2;\n    const int32_t y = vp_y + (vp_h - side) / 2;\n    const int32_t w = side;\n    const int32_t h = side;\n\n    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, w, h, 0);\n    TRX_GL_CheckError();\n}\n"
  },
  {
    "path": "src/trx/gl/texture.h",
    "content": "#pragma once\n\n#include <GL/glew.h>\n\ntypedef struct {\n    bool initialized;\n    GLuint id;\n    GLenum target;\n} TRX_GL_TEXTURE;\n\nTRX_GL_TEXTURE *TRX_GL_Texture_Create(GLenum target);\nvoid TRX_GL_Texture_Free(TRX_GL_TEXTURE *texture);\n\nvoid TRX_GL_Texture_Init(TRX_GL_TEXTURE *texture, GLenum target);\nvoid TRX_GL_Texture_Close(TRX_GL_TEXTURE *texture);\nvoid TRX_GL_Texture_Bind(const TRX_GL_TEXTURE *texture);\nvoid TRX_GL_Texture_Load(\n    TRX_GL_TEXTURE *texture, const void *data, int width, int height,\n    GLint internal_format, GLint format);\nvoid TRX_GL_Texture_LoadFromBackBuffer(TRX_GL_TEXTURE *texture);\n"
  },
  {
    "path": "src/trx/gl/track.c",
    "content": "#include <trx/gl/track.h>\n\nTRX_GL_METRICS g_TRX_GL_Metrics;\n\nvoid TRX_GL_Track_Reset(void)\n{\n    g_TRX_GL_Metrics = (TRX_GL_METRICS) { 0 };\n}\n\nTRX_GL_METRICS TRX_GL_Track_GetMetrics(void)\n{\n    return g_TRX_GL_Metrics;\n}\n"
  },
  {
    "path": "src/trx/gl/track.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\ntypedef struct {\n    int32_t buffer_transfer_count;\n    int32_t buffer_total_bytes;\n    int32_t uniform_changes;\n    int32_t opaque_vert_count;\n    int32_t trans_vert_count;\n    int32_t blend_add_vert_count;\n} TRX_GL_METRICS;\n\nextern TRX_GL_METRICS g_TRX_GL_Metrics;\n\n#define TRX_GL_TRACK_UNIFORM(fn, ...)                                          \\\n    do {                                                                       \\\n        g_TRX_GL_Metrics.uniform_changes++;                                    \\\n        fn(__VA_ARGS__);                                                       \\\n    } while (0);\n\n#define TRX_GL_TRACK_DATA(fn, a, b, c, d)                                      \\\n    do {                                                                       \\\n        g_TRX_GL_Metrics.buffer_total_bytes += b;                              \\\n        g_TRX_GL_Metrics.buffer_transfer_count++;                              \\\n        fn(a, b, c, d);                                                        \\\n    } while (0);\n\n#define TRX_GL_TRACK_SUBDATA(fn, a, b, c, d)                                   \\\n    do {                                                                       \\\n        g_TRX_GL_Metrics.buffer_total_bytes += c;                              \\\n        g_TRX_GL_Metrics.buffer_transfer_count++;                              \\\n        fn(a, b, c, d);                                                        \\\n    } while (0);\n\nvoid TRX_GL_Track_Reset(void);\nTRX_GL_METRICS TRX_GL_Track_GetMetrics(void);\n"
  },
  {
    "path": "src/trx/gl/utils.c",
    "content": "#include <trx/gl/utils.h>\n\n#include <GL/glew.h>\n\nconst char *TRX_GL_GetErrorString(GLenum err)\n{\n    switch (err) {\n    case GL_NO_ERROR:\n        return \"GL_NO_ERROR\";\n    case GL_INVALID_ENUM:\n        return \"GL_INVALID_ENUM\";\n    case GL_INVALID_VALUE:\n        return \"GL_INVALID_VALUE\";\n    case GL_INVALID_OPERATION:\n        return \"GL_INVALID_OPERATION\";\n    case GL_INVALID_FRAMEBUFFER_OPERATION:\n        return \"GL_INVALID_FRAMEBUFFER_OPERATION\";\n    case GL_OUT_OF_MEMORY:\n        return \"GL_OUT_OF_MEMORY\";\n    case GL_STACK_UNDERFLOW:\n        return \"GL_STACK_UNDERFLOW\";\n    case GL_STACK_OVERFLOW:\n        return \"GL_STACK_OVERFLOW\";\n    default:\n        return \"UNKNOWN\";\n    }\n}\n\nvoid TRX_GL_CheckError(void)\n{\n    for (GLenum err; (err = glGetError()) != GL_NO_ERROR;) {\n        LOG_ERROR(\"glGetError: (%s)\", TRX_GL_GetErrorString(err));\n    }\n}\n"
  },
  {
    "path": "src/trx/gl/utils.h",
    "content": "#pragma once\n\n#include <trx/core/log.h>\n#include <trx/gl/track.h>\n\n#include <GL/glew.h>\n\nvoid TRX_GL_CheckError(void);\nconst char *TRX_GL_GetErrorString(GLenum err);\n"
  },
  {
    "path": "src/trx/gl/vertex_array.c",
    "content": "#include <trx/gl/vertex_array.h>\n\n#include <trx/debug.h>\n#include <trx/gl/utils.h>\n\n#include <stdint.h>\n\nvoid TRX_GL_VertexArray_Init(TRX_GL_VERTEX_ARRAY *array)\n{\n    ASSERT(array != nullptr);\n    glGenVertexArrays(1, &array->id);\n    TRX_GL_CheckError();\n    array->initialized = true;\n}\n\nvoid TRX_GL_VertexArray_Close(TRX_GL_VERTEX_ARRAY *array)\n{\n    ASSERT(array != nullptr);\n    if (array->initialized) {\n        glDeleteVertexArrays(1, &array->id);\n        TRX_GL_CheckError();\n    }\n    array->initialized = false;\n}\n\nvoid TRX_GL_VertexArray_Bind(TRX_GL_VERTEX_ARRAY *array)\n{\n    ASSERT(array != nullptr);\n    ASSERT(array->initialized);\n    glBindVertexArray(array->id);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_VertexArray_Attribute(\n    TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type,\n    GLboolean normalized, GLsizei stride, GLsizei offset)\n{\n    ASSERT(array != nullptr);\n    ASSERT(array->initialized);\n    glEnableVertexAttribArray(index);\n    TRX_GL_CheckError();\n\n    glVertexAttribPointer(\n        index, size, type, normalized, stride, (void *)(intptr_t)offset);\n    TRX_GL_CheckError();\n}\n\nvoid TRX_GL_VertexArray_IAttribute(\n    TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type,\n    GLsizei stride, GLsizei offset)\n{\n    ASSERT(array != nullptr);\n    ASSERT(array->initialized);\n    glEnableVertexAttribArray(index);\n    TRX_GL_CheckError();\n\n    glVertexAttribIPointer(index, size, type, stride, (void *)(intptr_t)offset);\n    TRX_GL_CheckError();\n}\n"
  },
  {
    "path": "src/trx/gl/vertex_array.h",
    "content": "#pragma once\n\n#include <GL/glew.h>\n\ntypedef struct {\n    bool initialized;\n    GLuint id;\n} TRX_GL_VERTEX_ARRAY;\n\nvoid TRX_GL_VertexArray_Init(TRX_GL_VERTEX_ARRAY *array);\nvoid TRX_GL_VertexArray_Close(TRX_GL_VERTEX_ARRAY *array);\nvoid TRX_GL_VertexArray_Bind(TRX_GL_VERTEX_ARRAY *array);\nvoid TRX_GL_VertexArray_Attribute(\n    TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type,\n    GLboolean normalized, GLsizei stride, GLsizei offset);\nvoid TRX_GL_VertexArray_IAttribute(\n    TRX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type,\n    GLsizei stride, GLsizei offset);\n"
  },
  {
    "path": "src/trx/version.c",
    "content": "#include <trx/version.h>\n\n#ifndef MESON_BUILD\nconst char *g_TRXVersion = \"TR1X (non-Docker build)\";\n#endif\n\nint32_t g_TRVersion = 0; // overriden at runtime when loading a level\n"
  },
  {
    "path": "src/trx/version.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\n#define TR_VERSION_COUNT 3\n\nextern const char *g_TRXVersion;\nextern int32_t g_TRVersion;\n"
  },
  {
    "path": "tools/additional_lint",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport sys\nfrom collections.abc import Iterable\nfrom fnmatch import fnmatch\nfrom pathlib import Path\n\nfrom shared.files import find_versioned_files, is_binary_file\nfrom shared.linting import LintContext, lint_repo, lint_bulk_files, lint_file\nfrom shared.paths import REPO_DIR\n\nIGNORED_PATTERNS = [\"*.patch\", \"*.bin\"]\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"path\", type=Path, nargs=\"*\")\n    parser.add_argument(\"-D\", \"--debug\", action=\"store_true\")\n    parser.add_argument(\"-a\", \"--all\", action=\"store_true\")\n    return parser.parse_args()\n\n\ndef filter_files(\n    files: Iterable[Path], ignored_patterns: list[str] | None, debug: bool\n) -> Iterable[Path]:\n    for path in files:\n        if not path.exists():\n            continue\n        if path.is_dir():\n            continue\n        if is_binary_file(path):\n            if debug:\n                print(f\"{path} is a binary file, ignoring\", file=sys.stderr)\n            continue\n        if ignored_patterns and any(\n            fnmatch(path.name, pattern) for pattern in ignored_patterns\n        ):\n            if debug:\n                print(\n                    f\"{path} has a prohibited extension, ignoring\",\n                    file=sys.stderr,\n                )\n            continue\n        yield path\n\n\ndef main(root_dir: Path) -> None:\n    args = parse_args()\n\n    context = LintContext(\n        root_dir=root_dir,\n        versioned_files=list(find_versioned_files(root_dir=REPO_DIR)),\n    )\n    if args.path:\n        files = args.path\n    else:\n        files = context.versioned_files\n    files = list(\n        filter_files(\n            files, ignored_patterns=IGNORED_PATTERNS, debug=args.debug\n        )\n    )\n\n    exit_code = 0\n    for file in files:\n        if args.debug:\n            print(f\"Checking {file}...\", file=sys.stderr)\n        for lint_warning in lint_file(context, file):\n            print(str(lint_warning), file=sys.stderr)\n            exit_code = 1\n\n    if args.debug:\n        print(f\"Checking files in bulk {file}...\", file=sys.stderr)\n    for lint_warning in lint_bulk_files(context, files):\n        print(str(lint_warning), file=sys.stderr)\n        exit_code = 1\n\n    if args.all:\n        if args.debug:\n            print(f\"Checking for repository-wide warnings...\", file=sys.stderr)\n        for lint_warning in lint_repo(context):\n            print(str(lint_warning), file=sys.stderr)\n            exit_code = 1\n\n    exit(exit_code)\n\n\nif __name__ == \"__main__\":\n    main(root_dir=REPO_DIR)\n"
  },
  {
    "path": "tools/download_assets",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport shutil\nimport ssl\nimport sys\nimport tempfile\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom zipfile import ZipFile\n\nfrom shared.paths import DATA_DIR, PROJECT_PATHS\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Downloads large binary assets for both the legacy per-game \"\n            \"ship layouts and the combined data/trx/ship hierarchy.\"\n        )\n    )\n    parser.add_argument(\n        \"game_version\",\n        choices=[\"1\", \"2\", \"3\", \"all\"],\n        default=\"all\",\n        nargs=\"?\",\n    )\n    parser.add_argument(\n        \"--combined\",\n        action=\"store_true\",\n        help=\"Also download assets for the combined data/trx/ship hierarchy.\",\n    )\n    return parser.parse_args()\n\n\ndef download_to_file(url: str, path: Path) -> None:\n    print(f\"Downloading {url}...\")\n    req = Request(url, headers={\"User-Agent\": \"download_assets\"})\n    context = ssl._create_unverified_context()\n    with urlopen(req, context=context) as response:\n        if getattr(response, \"status\", None) not in (None, 200):\n            sys.exit(\n                f\"Error: failed to download {url}. Status: {response.status}\"\n            )\n        with path.open(\"wb\") as f:\n            shutil.copyfileobj(response, f)\n\n\ndef extract_zip(zip_path: Path, dest_dir: Path) -> None:\n    print(f\"Extracting {zip_path} to {dest_dir}...\")\n    dest_dir.mkdir(parents=True, exist_ok=True)\n    with ZipFile(zip_path) as z:\n        z.extractall(dest_dir)\n\n\ndef download_assets(asset_urls: list[str], target_dir: Path) -> None:\n    with tempfile.TemporaryDirectory() as tmpdir_str:\n        tmpdir = Path(tmpdir_str)\n        for url in asset_urls:\n            filename = Path(url).name\n            local_zip = tmpdir / filename\n            download_to_file(url, local_zip)\n            extract_zip(local_zip, target_dir)\n    print(\"Asset download and extraction complete.\")\n\n\ndef main() -> None:\n    args = parse_args()\n    legacy_asset_urls_map: dict[int, list[str]] = {\n        1: [\n            \"https://lostartefacts.dev/aux/tr1x/main.zip\",\n            \"https://lostartefacts.dev/aux/tr1x/trub.zip\",\n        ],\n        2: [\n            \"https://lostartefacts.dev/aux/tr2x/main.zip\",\n            \"https://lostartefacts.dev/aux/tr2x/trgm.zip\",\n        ],\n        3: [\"https://lostartefacts.dev/aux/tr3x/main.zip\"],\n    }\n    combined_asset_urls_map: dict[int, list[str]] = {\n        1: [\n            \"https://lostartefacts.dev/aux/trx/tr1.zip\",\n            \"https://lostartefacts.dev/aux/trx/tr1-ub.zip\",\n            \"https://lostartefacts.dev/aux/trx/tr1-demo-pc.zip\",\n        ],\n        2: [\n            \"https://lostartefacts.dev/aux/trx/tr2.zip\",\n            \"https://lostartefacts.dev/aux/trx/tr2-gm.zip\",\n        ],\n        3: [\"https://lostartefacts.dev/aux/trx/tr3.zip\"],\n    }\n\n    versions = {\"1\": [1], \"2\": [2], \"3\": [3], \"all\": [1, 2, 3]}[\n        args.game_version\n    ]\n    for version in versions:\n        download_assets(\n            legacy_asset_urls_map[version],\n            target_dir=PROJECT_PATHS[version].shipped_data_dir,\n        )\n        if args.combined:\n            download_assets(\n                combined_asset_urls_map[version],\n                target_dir=DATA_DIR / \"trx\" / \"ship\",\n            )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/embed_trx_lua.py",
    "content": "#!/usr/bin/env python3\n\nfrom __future__ import annotations\n\nimport argparse\nfrom pathlib import Path\n\n\ndef _c_ident_from_path(path: Path) -> str:\n    raw = path.name\n    out: list[str] = []\n    for ch in raw:\n        if ch.isalnum():\n            out.append(ch)\n        else:\n            out.append(\"_\")\n    return \"m_TrxLua_\" + \"\".join(out)\n\n\ndef _bytes_to_c_array(data: bytes) -> str:\n    per_line = 16\n    chunks: list[str] = []\n    for i in range(0, len(data), per_line):\n        chunk = \", \".join(f\"0x{b:02x}\" for b in data[i : i + per_line])\n        chunks.append(\"    \" + chunk)\n    return \",\\n\".join(chunks)\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(description=\"Embed TRX Lua scripts into C\")\n    parser.add_argument(\"--output\", required=True, type=Path)\n    parser.add_argument(\"inputs\", nargs=\"+\", type=Path)\n    args = parser.parse_args()\n\n    output_path: Path = args.output\n    inputs: list[Path] = list(args.inputs)\n\n    scripts: list[tuple[str, str, bytes]] = []\n    for in_path in inputs:\n        data = in_path.read_bytes()\n        c_ident = _c_ident_from_path(in_path)\n        scripts.append((in_path.name, c_ident, data))\n\n    lines: list[str] = []\n    lines.append(\"// Auto-generated file; do not edit.\")\n    lines.append('#include <trx/game/lua/embedded_scripts.h>')\n    lines.append(\"\")\n    for name, c_ident, data in scripts:\n        lines.append(f\"static const uint8_t {c_ident}[] = {{\")\n        lines.append(_bytes_to_c_array(data) + \",\")\n        lines.append(\"};\")\n        lines.append(\"\")\n\n    lines.append(\"const LUA_EMBEDDED_SCRIPT g_LUA_EmbeddedScripts[] = {\")\n    for name, c_ident, data in scripts:\n        lines.append(\n            f'    {{ .path = \"{name}\", .data = {c_ident}, .size = {len(data)} }},'\n        )\n    lines.append(\"    { .path = nullptr, .data = nullptr, .size = 0 },\")\n    lines.append(\"};\")\n    lines.append(\"\")\n\n    output_path.write_text(\"\\n\".join(lines), encoding=\"utf-8\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "tools/ffmpeg_flags.txt",
    "content": "--enable-gpl\n--enable-decoder=pcx\n--enable-decoder=png\n--enable-decoder=gif\n--enable-decoder=mjpeg\n--enable-decoder=mpeg4\n--enable-decoder=mdec\n--enable-decoder=mp3\n--enable-decoder=wmav1\n--enable-decoder=wmav2\n--enable-decoder=h264\n--enable-decoder=h264_qsv\n--enable-decoder=libopenh264\n--enable-demuxer=mov\n--enable-demuxer=mp3\n--enable-demuxer=avi\n--enable-demuxer=h264\n--enable-demuxer=str\n--enable-demuxer=image2\n--enable-demuxer=asf\n--enable-parser=mpegaudio\n--enable-zlib\n--enable-small\n--disable-debug\n--disable-ffplay\n--disable-ffprobe\n--disable-doc\n--disable-network\n--disable-htmlpages\n--disable-manpages\n--disable-podpages\n--disable-txtpages\n--disable-asm\n"
  },
  {
    "path": "tools/generate_icon",
    "content": "#!/usr/bin/env python3\n# regenerate the .ICO file from .PSD.\nimport argparse\nfrom pathlib import Path\n\nfrom shared.icons import generate_icon\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"path\", type=Path)\n    parser.add_argument(\"-o\", \"--output\", type=Path, required=True)\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    if args.output.exists():\n        args.output.unlink()\n    generate_icon(args.path, args.output)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/generate_init",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport re\nfrom pathlib import Path\n\nfrom shared.versioning import generate_version\n\nTEMPLATE = \"\"\"\nconst char *g_TRXVersion = \"TRX {version}\";\n\"\"\".lstrip()\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-o\", \"--output\", type=Path)\n    return parser.parse_args()\n\n\ndef get_init_c() -> str:\n    return TEMPLATE.format(\n        version=generate_version()\n    )\n\n\ndef update_init_c(output_path: Path) -> None:\n    new_text = get_init_c()\n    if not output_path.exists() or output_path.read_text() != new_text:\n        output_path.write_text(new_text)\n\n\ndef main() -> None:\n    args = parse_args()\n    if args.output:\n        update_init_c(output_path=args.output)\n    else:\n        print(get_init_c(), end=\"\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/generate_rcfile",
    "content": "#!/usr/bin/env python3\nimport argparse\nfrom pathlib import Path\n\nfrom shared.paths import DATA_DIR\nfrom shared.versioning import generate_version\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-o\", \"--output\", type=Path, nargs=\"+\")\n    return parser.parse_args()\n\n\ndef write_rc_template(\n    input_path: Path, output_path: Path, version: str\n) -> None:\n    template = input_path.read_text()\n    template = template.replace(\"{version}\", version)\n    template = template.replace(\n        \"{icon_path}\", str(DATA_DIR / input_path.parent.name / 'icon.ico')\n    )\n    output_path.write_text(template)\n\n\ndef main() -> None:\n    args = parse_args()\n    version = generate_version()\n\n    for output_path in args.output or []:\n        write_rc_template(\n            input_path=DATA_DIR / output_path.name.replace('_', '/'),\n            output_path=output_path,\n            version=version,\n        )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/get_version",
    "content": "#!/usr/bin/env python3\nimport argparse\n\nfrom shared.versioning import generate_version\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    print(generate_version(), end=\"\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/glyphs/README.md",
    "content": "## Glyph Generation Tools\n\nThese tools work alongside the injection tool to expand the original Tomb\nRaider character set by adding new characters and accents.\n\n### Overview\n\nThe game displays text using bitmaps, which are organized as sprites within the\nalphabet object found in .phd and .tr2 level files. Additionally, the game\nexecutable has hardcoded information regarding the locations, sizes, and\nindices of these glyphs.\n\nExpanding the character set involves a complex process requiring the use of\nmultiple tools.\n\n**Summary:**\n\n`mapping.txt` + `glyphs.png` →\n`generate_defs` →\nintermediary files for the injection tool →\nTRXInjectionTool →\n`font.bin` injected into the game.\n\n### `mapping.txt` and `glyphs.png`\n\nThe master files are located at `data/tr*/glyphs/*.{png,txt}`. The PNG files\nare sprite sheets containing each character, while the text files provide\nmetadata for each glyph or icon's location, as well as optional transforms such\nas shifts or bounding box resizes, using a domain specific language.\n\nSome characters are composed using special combining sprites. For instance,\ninstead of creating an accented version of the character `a` for each\nvariation, we use a single `a` sprite and separate sprites for all possible\naccents. This method allows accents to be combined with various base characters\nwithout redundancy. Every individual variation still needs to be defined in\n`mapping.txt`.\n\nAll characters are encoded in UTF-8, meaning each character can consist of\nmultiple bytes. Icons and buttons follow the same approach and are represented\nby ASCII sequences like `\\{button x}`. The game processes these similarly to\nother glyphs.\n\n### `generate_keyboard_map`\n\nTakes blank keycap bitmaps and a bitmap font and outputs a sprite sheet and a\nmapping.txt suitable for the `generate_defs` script.\n\n### `generate_defs`\n\nThis tool processes `mapping.txt` and the associated source images to create\nintermediary files needed by the injection tool, as well as update internal\ndefinition files that end up embedded into the executable at the compilation\nphase. It requires an argument for the game version since different games have\nunique font styles and specifications. The result is a packed texture atlas\n(which the injection tool later repacks) and a JSON file detailing each sprite\ntexture's location. These files must be placed in their relevant directories\nwithin the injection tool resources. Afterward, the injection tool can be ran\nto generate the final output file, `font.bin`, which should be eventually\nplaced in the injections directory for the game's use.\n\n### `test_alignment.html`\n\nThis testing tool showcases how `mapping.txt` segments the sprite sheets,\nallowing the developers to verify and correct any alignment issues.\n\n### `generate_compositions`\n\nThis is a simple development tool that outputs all valid accented characters to\nthe standard output, and it is not a direct part of the main pipeline.\n\n### `test_language`\n\nAnother development tool, this tests language coverage for the languages\nintended to be supported. It operates based on `mapping.txt`, but is not\ndirectly involved in the main pipeline.\n"
  },
  {
    "path": "tools/glyphs/generate_case_map",
    "content": "#!/usr/bin/env python3\n\"\"\"\nGenerate a case mapping file of lowercase to uppercase characters\nbased on the supported UI glyph definitions in text_tr1.def and text_tr2.def.\n\"\"\"\nimport ast\nimport re\nimport sys\nfrom pathlib import Path\n\n# HACK: Ensure the shared module is visible for this script.\nsys.path.insert(0, str(Path(__file__).resolve().parents[1]))\n\nfrom shared.glyph_mapping import Glyph, get_glyph_map\nfrom shared.paths import PROJECT_PATHS, SHARED_SRC_DIR\n\n\ndef main() -> None:\n    ui_dir = SHARED_SRC_DIR / \"game/ui\"\n\n    glyphs: list[Glyph] = []\n    for project in PROJECT_PATHS.values():\n        glyphs += get_glyph_map(project.data_dir / \"glyphs\")\n\n    supported = set(g.text for g in glyphs)\n    lowers = [c for c in supported if len(c) == 1 and c.islower()]\n    mapping: list[tuple[str, str]] = []\n    for c in sorted(lowers, key=lambda x: ord(x)):\n        up = c.upper()\n        if len(up) == 1 and up in supported:\n            mapping.append((c, up))\n\n    out_path = SHARED_SRC_DIR / \"strings/case_map.def\"\n    lines: list[str] = [\n        \"// This file is autogenerated - do not edit.\",\n        \"// See tools/glyphs/generate_case_map for details.\",\n        \"\",\n    ]\n    for low, up in mapping:\n        lit_low = low.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"')\n        lit_up = up.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"')\n        lines.append(f'X_CASE_MAP(\"{lit_low}\", \"{lit_up}\")')\n\n    out_path.write_text(\"\\n\".join(lines) + \"\\n\", encoding=\"utf-8\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/glyphs/generate_compositions",
    "content": "#!/usr/bin/env python3\nimport json\nimport string\nimport unicodedata\n\n\ndef quote(letter):\n    return json.dumps(letter, ensure_ascii=False)\n\n\ndef show(accented, letter, accent):\n    print(\n        f\"U+{ord(accented):04X}:{accented} C combine({quote(letter)}, {quote(accent)})\"\n    )\n\n\ndef add_accent(letter, accent):\n    return unicodedata.normalize(\"NFC\", letter + accent)\n\n\nfor accent_name, accent_char in [\n    (\"\\\\{grave accent}\", \"\\u0300\"),\n    (\"\\\\{acute accent}\", \"\\u0301\"),\n    (\"\\\\{circumflex}\", \"\\u0302\"),\n    (\"\\\\{tilde}\", \"\\u0303\"),\n    (\"\\\\{macron}\", \"\\u0304\"),\n    (\"\\\\{overline}\", \"\\u0305\"),\n    (\"\\\\{breve}\", \"\\u0306\"),\n    (\"\\\\{dot above}\", \"\\u0307\"),\n    (\"\\\\{umlaut}\", \"\\u0308\"),\n    (\"\\\\{ring above}\", \"\\u030A\"),\n    (\"\\\\{double acute accent}\", \"\\u030B\"),\n    (\"\\\\{caron}\", \"\\u030C\"),\n]:\n    print(\"#\", accent_name)\n    for letter in string.ascii_uppercase + string.ascii_lowercase:\n        accented = add_accent(letter, accent_char)\n        if len(accented) == 1:\n            show(accented, letter, accent_name)\n    print()\n"
  },
  {
    "path": "tools/glyphs/generate_defs",
    "content": "#!/usr/bin/env -S uv run --script\n#\n# /// script\n# requires-python = \">=3.14\"\n# dependencies = [\"Pillow\", \"rectpack\", \"lark[interegular]\", \"numpy\"]\n# ///\n\nimport argparse\nimport ast\nimport json\nimport sys\nimport textwrap\nfrom dataclasses import dataclass\nfrom functools import lru_cache\nfrom pathlib import Path\n\n# pip install rectpack numpy lark Pillow\nimport numpy as np\nimport rectpack\nfrom lark import Discard, Lark, Transformer\nfrom PIL import Image\n\n# HACK: Ensure the shared module is visible for this script.\nsys.path.insert(0, str(Path(__file__).resolve().parents[1]))\n\nfrom shared.glyph_mapping import CombineSource, Glyph, get_glyph_map\nfrom shared.paths import PROJECT_PATHS, REPO_DIR, SHARED_SRC_DIR\n\n\ndef pack_sprites(\n    glyphs: list[Glyph],\n    output_dir: Path,\n    font_idx: int,\n    page_size: int = 256,\n    padding: int = 1,\n):\n    \"\"\"Create packed images with the glyph sprites, and a mapping JSON for the\n    injector that will later convert them into a .bin injection file.\n    \"\"\"\n    glyph_with_sources = {\n        glyph.source.index: (glyph, glyph.source)\n        for glyph in glyphs\n    }\n    if len(glyph_with_sources) == 0:\n        print(f\"No glyphs for font {font_idx}, skipping sprite packing.\")\n        return None\n\n    packer = rectpack.newPacker(rotation=False)\n    for sprite_index, (glyph, source) in glyph_with_sources.items():\n        pixels, _ = source.load(glyph)\n        height, width, _ = pixels.shape\n        packer.add_rect(width + padding * 2, height + padding * 2, sprite_index)\n    packer.add_bin(page_size, page_size, count=float(\"inf\"))\n    packer.pack()\n\n    injector_mappings: list[dict] = []\n    for dst_page_num, bin in enumerate(packer):\n        if font_idx == 0:\n            output_path = output_dir / f\"alpha_sprites_{dst_page_num:02d}.png\"\n        else:\n            output_path = output_dir / (\n                f\"alpha_sprites_font{font_idx}_{dst_page_num:02d}.png\"\n            )\n        dst_pixels = np.zeros((page_size, page_size, 4))\n\n        for rect in bin:\n            dst_x = rect.x + padding\n            dst_y = rect.y + padding\n            width = rect.width - padding * 2\n            height = rect.height - padding * 2\n            glyph, source = glyph_with_sources[rect.rid]\n\n            pixels, bbox = source.load(glyph)\n\n            dst_pixels[\n                dst_y : dst_y + height,\n                dst_x : dst_x + width,\n            ] = pixels\n\n            injector_mappings.append(\n                {\n                    \"mesh_num\": source.index,\n                    \"filename\": output_path.name,\n                    \"x\": dst_x,\n                    \"y\": dst_y,\n                    \"w\": width,\n                    \"h\": height,\n                    \"l\": bbox.x + glyph.extra_x,\n                    \"t\": bbox.y + glyph.extra_y,\n                    \"b\": bbox.y + glyph.extra_y + bbox.h,\n                    \"r\": bbox.x + glyph.extra_x + bbox.w,\n                }\n            )\n\n        image = Image.fromarray(dst_pixels.astype(\"uint8\"))\n        image.save(output_path)\n        print(\"Created a sprite sheet for injector in\", output_path)\n\n    if font_idx == 0:\n        info_path = output_dir / \"glyph_info.json\"\n    else:\n        info_path = output_dir / f\"glyph_info_font{font_idx}.json\"\n    info_path.write_text(json.dumps(injector_mappings))\n    print(\"Saved mappings for injector to\", info_path)\n    return info_path\n\n\ndef generate_def_file(output_path: Path, glyphs: list[Glyph]):\n    \"\"\"Generate a .def file for the game itself, with the C macros containing\n    information on how to render each glyph.\n    \"\"\"\n    header = textwrap.dedent(\n        r\"\"\"\n        // This file is autogenerated. See tools/glyphs/generate_defs for details.\n\n        \"\"\"\n    ).strip()\n\n    class_map = {\n        \"R\": \"GLYPH_REVIEW_MARKER\",\n        \"T\": \"GLYPH_TEXT\",\n        \"C\": \"GLYPH_TEXT\",\n        \"c\": \"GLYPH_TEXT\",\n        \"I\": \"GLYPH_ICON\",\n    }\n\n    with output_path.open(\"w\") as handle:\n        print(header, file=handle)\n        for glyph in glyphs:\n            args = [\n                json.dumps(glyph.text, ensure_ascii=False),\n                class_map[glyph.glyph_class],\n                str(glyph.source.index),\n            ]\n            print(\"X_GLYPH_DEFINE(\" + \", \".join(args) + \")\", file=handle)\n\n    print(\"Saved\", output_path)\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"game\", type=int)\n    parser.add_argument(\"--injector-output-dir\", type=Path, default=REPO_DIR)\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n\n    def_path = SHARED_SRC_DIR / f\"game/ui/text_autogen.def\"\n    input_dir = PROJECT_PATHS[args.game].data_dir / \"glyphs\"\n    injector_output_dir = args.injector_output_dir\n\n    glyphs = get_glyph_map(input_dir)\n\n    fonts = sorted({glyph.font_idx for glyph in glyphs})\n    for font_idx in fonts:\n        font_glyphs = [glyph for glyph in glyphs if glyph.font_idx == font_idx]\n        pack_sprites(font_glyphs, output_dir=injector_output_dir, font_idx=font_idx)\n\n    default_glyphs = [glyph for glyph in glyphs if glyph.font_idx == 0]\n    generate_def_file(def_path, default_glyphs)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/glyphs/generate_keyboard_map",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport sys\nimport warnings\nfrom pathlib import Path\n\n# pip install rectpack Pillow\nimport rectpack\nfrom PIL import Image, ImageDraw, ImageFont\n\n# HACK: Ensure the shared module is visible for this script.\nsys.path.insert(0, str(Path(__file__).resolve().parents[1]))\n\nfrom shared.glyph_mapping import CombineSource, Glyph, get_glyph_map\nfrom shared.paths import DATA_DIR, PROJECT_PATHS, REPO_DIR\n\nMAX_WIDTH = 500\nTEXT_MIN_PADDING = 3\nTEXT_SHIFT_Y = 1\nTEXT_COLOR = \"#060100\"\n\nKEYCAP_BORDER = 5\nKEYCAP_WIDTHS = [15, 21, 32, 36, 50, 60]\n\nKEY_TO_TEXT: dict[str, str] = {\n    r\"\\\\{keyboard l_ctrl}\": \"Ctrl\",\n    r\"\\\\{keyboard r_ctrl}\": \"Ctrl\",\n    r\"\\\\{keyboard r_shift}\": \"Shift\",\n    r\"\\\\{keyboard l_shift}\": \"Shift\",\n    r\"\\\\{keyboard r_alt}\": \"Alt\",\n    r\"\\\\{keyboard l_alt}\": \"Alt\",\n    r\"\\\\{keyboard l_win}\": \"Win\",\n    r\"\\\\{keyboard r_win}\": \"Win\",\n    r\"\\\\{keyboard left}\": \"←\",\n    r\"\\\\{keyboard up}\": \"↑\",\n    r\"\\\\{keyboard right}\": \"→\",\n    r\"\\\\{keyboard down}\": \"↓\",\n    r\"\\\\{keyboard return}\": \"Return ↵\",\n    r\"\\\\{keyboard escape}\": \"Esc\",\n    r\"\\\\{keyboard backspace}\": \"Backspace\",\n    r\"\\\\{keyboard tab}\": \"Tab\",\n    r\"\\\\{keyboard space}\": \"Space\",\n    r\"\\\\{keyboard caps_lock}\": \"CapsLock\",\n    r\"\\\\{keyboard print_screen}\": \"Screen\",\n    r\"\\\\{keyboard scroll_lock}\": \"ScrollLock\",\n    r\"\\\\{keyboard pause}\": \"Pause\",\n    r\"\\\\{keyboard insert}\": \"Insert\",\n    r\"\\\\{keyboard home}\": \"Home\",\n    r\"\\\\{keyboard page_up}\": \"PgUp\",\n    r\"\\\\{keyboard delete}\": \"Del\",\n    r\"\\\\{keyboard end}\": \"End\",\n    r\"\\\\{keyboard page_down}\": \"PgDn\",\n    r\"\\\\{keyboard a}\": \"A\",\n    r\"\\\\{keyboard b}\": \"B\",\n    r\"\\\\{keyboard c}\": \"C\",\n    r\"\\\\{keyboard d}\": \"D\",\n    r\"\\\\{keyboard e}\": \"E\",\n    r\"\\\\{keyboard f}\": \"F\",\n    r\"\\\\{keyboard g}\": \"G\",\n    r\"\\\\{keyboard h}\": \"H\",\n    r\"\\\\{keyboard i}\": \"I\",\n    r\"\\\\{keyboard j}\": \"J\",\n    r\"\\\\{keyboard k}\": \"K\",\n    r\"\\\\{keyboard l}\": \"L\",\n    r\"\\\\{keyboard m}\": \"M\",\n    r\"\\\\{keyboard n}\": \"N\",\n    r\"\\\\{keyboard o}\": \"O\",\n    r\"\\\\{keyboard p}\": \"P\",\n    r\"\\\\{keyboard q}\": \"Q\",\n    r\"\\\\{keyboard r}\": \"R\",\n    r\"\\\\{keyboard s}\": \"S\",\n    r\"\\\\{keyboard t}\": \"T\",\n    r\"\\\\{keyboard u}\": \"U\",\n    r\"\\\\{keyboard v}\": \"V\",\n    r\"\\\\{keyboard w}\": \"W\",\n    r\"\\\\{keyboard x}\": \"X\",\n    r\"\\\\{keyboard y}\": \"Y\",\n    r\"\\\\{keyboard z}\": \"Z\",\n    r\"\\\\{keyboard 0}\": \"0\",\n    r\"\\\\{keyboard 1}\": \"1\",\n    r\"\\\\{keyboard 2}\": \"2\",\n    r\"\\\\{keyboard 3}\": \"3\",\n    r\"\\\\{keyboard 4}\": \"4\",\n    r\"\\\\{keyboard 5}\": \"5\",\n    r\"\\\\{keyboard 6}\": \"6\",\n    r\"\\\\{keyboard 7}\": \"7\",\n    r\"\\\\{keyboard 8}\": \"8\",\n    r\"\\\\{keyboard 9}\": \"9\",\n    r\"\\\\{keyboard minus}\": \"-\",\n    r\"\\\\{keyboard equals}\": \"=\",\n    r\"\\\\{keyboard left_square_bracket}\": \"[\",\n    r\"\\\\{keyboard right_square_bracket}\": \"]\",\n    r\"\\\\{keyboard backslash}\": \"\\\\\",\n    r\"\\\\{keyboard hash}\": \"#\",\n    r\"\\\\{keyboard semicolon}\": \";\",\n    r\"\\\\{keyboard apostrophe}\": \"'\",\n    r\"\\\\{keyboard backtick}\": \"`\",\n    r\"\\\\{keyboard comma}\": \",\",\n    r\"\\\\{keyboard period}\": \".\",\n    r\"\\\\{keyboard slash}\": \"/\",\n    r\"\\\\{keyboard f1}\": \"F1\",\n    r\"\\\\{keyboard f2}\": \"F2\",\n    r\"\\\\{keyboard f3}\": \"F3\",\n    r\"\\\\{keyboard f4}\": \"F4\",\n    r\"\\\\{keyboard f5}\": \"F5\",\n    r\"\\\\{keyboard f6}\": \"F6\",\n    r\"\\\\{keyboard f7}\": \"F7\",\n    r\"\\\\{keyboard f8}\": \"F8\",\n    r\"\\\\{keyboard f9}\": \"F9\",\n    r\"\\\\{keyboard f10}\": \"F10\",\n    r\"\\\\{keyboard f11}\": \"F11\",\n    r\"\\\\{keyboard f12}\": \"F12\",\n    r\"\\\\{keyboard f13}\": \"F13\",\n    r\"\\\\{keyboard f14}\": \"F14\",\n    r\"\\\\{keyboard f15}\": \"F15\",\n    r\"\\\\{keyboard f16}\": \"F16\",\n    r\"\\\\{keyboard f17}\": \"F17\",\n    r\"\\\\{keyboard f18}\": \"F18\",\n    r\"\\\\{keyboard f19}\": \"F19\",\n    r\"\\\\{keyboard f20}\": \"F20\",\n    r\"\\\\{keyboard f21}\": \"F21\",\n    r\"\\\\{keyboard f22}\": \"F22\",\n    r\"\\\\{keyboard f23}\": \"F23\",\n    r\"\\\\{keyboard f24}\": \"F24\",\n    r\"\\\\{keyboard num_lock}\": \"NumLock\",\n    r\"\\\\{keyboard num_0}\": \"Num0\",\n    r\"\\\\{keyboard num_1}\": \"Num1\",\n    r\"\\\\{keyboard num_2}\": \"Num2\",\n    r\"\\\\{keyboard num_3}\": \"Num3\",\n    r\"\\\\{keyboard num_4}\": \"Num4\",\n    r\"\\\\{keyboard num_5}\": \"Num5\",\n    r\"\\\\{keyboard num_6}\": \"Num6\",\n    r\"\\\\{keyboard num_7}\": \"Num7\",\n    r\"\\\\{keyboard num_8}\": \"Num8\",\n    r\"\\\\{keyboard num_9}\": \"Num9\",\n    r\"\\\\{keyboard num_period}\": \"Num.\",\n    r\"\\\\{keyboard num_divide}\": \"Num/\",\n    r\"\\\\{keyboard num_multiply}\": \"Num*\",\n    r\"\\\\{keyboard num_minus}\": \"Num-\",\n    r\"\\\\{keyboard num_plus}\": \"Num+\",\n    r\"\\\\{keyboard num_equals}\": \"Num=\",\n    r\"\\\\{keyboard num_comma}\": \"Num,\",\n    r\"\\\\{keyboard num_enter}\": \"Num↵\",\n    r\"\\\\{keyboard unknown}\": \"????\",\n}\n\n\ndef align(value: int, align: int) -> int:\n    return ((value + align - 1) // align) * align\n\n\ndef generate_keycap_images(image_path: Path, widths: list[int]) -> list[Image.Image]:\n    base_keycap = Image.open(image_path)\n    keycap_images = []\n\n    original_width, original_height = base_keycap.size\n    inner_width = original_width - 2 * KEYCAP_BORDER\n\n    for width in widths:\n        # Create a new image with the desired width and original height\n        keycap_image = Image.new('RGBA', (width, original_height))\n\n        # Paste corners\n        keycap_image.paste(base_keycap.crop((0, 0, KEYCAP_BORDER, original_height)), (0, 0))\n        keycap_image.paste(base_keycap.crop((original_width - KEYCAP_BORDER, 0, original_width, original_height)),\n                           (width - KEYCAP_BORDER, 0))\n\n        # Stretch the middle part\n        middle = base_keycap.crop((KEYCAP_BORDER, 0, original_width - KEYCAP_BORDER, original_height))\n        stretched_middle = middle.resize((width - 2 * KEYCAP_BORDER, original_height), Image.LANCZOS)\n\n        # Paste the stretched middle part\n        keycap_image.paste(stretched_middle, (KEYCAP_BORDER, 0))\n\n        keycap_images.append(keycap_image)\n\n    return keycap_images\n\n\ndef find_best_keycap(\n    visible_text: str,\n    blank_keycap_images: list[Image.Image],\n    font: ImageFont.ImageFont,\n) -> Image.Image | None:\n    # find all keycap images that can fit the text, then choose the smallest one\n    candidates: list[Image.Image] = []\n    for keycap_img in blank_keycap_images:\n        text_bbox = ImageDraw.Draw(keycap_img).textbbox(\n            (0, 0), visible_text, font=font\n        )\n        text_width = text_bbox[2] - text_bbox[0]\n        if text_width + 2 * TEXT_MIN_PADDING <= keycap_img.width:\n            candidates.append(keycap_img)\n    if candidates:\n        # choose the keycap with the smallest width\n        return min(candidates, key=lambda img: img.width)\n    warnings.warn(f\"No suitable keycap image for text: {visible_text}\")\n    return None\n\n\ndef create_sprite_sheet(\n    font_path: Path,\n    blank_keycap_images: list[Image],\n    cell_size: int = 1\n) -> tuple[Image.Image, list[tuple[str, tuple[int, int, int, int]]]]:\n    for i in range(5, 21):\n        try:\n            font = ImageFont.truetype(str(font_path), size=i)\n        except Exception:\n            continue\n        else:\n            break\n\n    sprites = []\n    for key_name, visible_text in KEY_TO_TEXT.items():\n        keycap_img = find_best_keycap(visible_text, blank_keycap_images, font)\n        if not keycap_img:\n            continue\n        keycap_copy = keycap_img.copy()\n\n        # render text on a separate image and trim horizontal transparent pixels\n        text_img = Image.new(\"RGBA\", keycap_copy.size, (0, 0, 0, 0))\n        text_draw = ImageDraw.Draw(text_img)\n        text_draw.text((0, 0), visible_text, font=font, fill=TEXT_COLOR)\n        x0, y0, x1, y1 = text_draw.textbbox((0, 0), visible_text, font=font)\n\n        cropped = text_img.crop((x0, 0, x0 + x1, text_img.height))\n        text_x = (keycap_copy.width + 1 - cropped.width) // 2\n        text_y = (keycap_copy.height - cropped.height) // 2\n        text_y += TEXT_SHIFT_Y\n        keycap_copy.paste(cropped, (text_x, text_y), cropped)\n\n        sprites.append((key_name, keycap_copy))\n\n    packer = rectpack.newPacker(rotation=False)\n    for i, (_, keycap_copy) in enumerate(sprites):\n        packer.add_rect(\n            align(keycap_copy.width, cell_size),\n            align(keycap_copy.height, cell_size),\n            i,\n        )\n    packer.add_bin(MAX_WIDTH, float(\"inf\"))\n    packer.pack()\n    packed_areas = packer.rect_list()\n\n    total_width = max(x + w for _, x, _, w, _, _ in packed_areas)\n    total_height = max(y + h for _, _, y, _, h, _ in packed_areas)\n    sprite_sheet = Image.new(\"RGBA\", (total_width, total_height))\n\n    definitions = []\n    for _sheet, x, y, w, h, i in packed_areas:\n        key_name, keycap_copy = sprites[i]\n        sprite_sheet.paste(keycap_copy, (x, y))\n        definitions.append(\n            (key_name, (x, y, keycap_copy.width, keycap_copy.height))\n        )\n\n    return sprite_sheet, definitions\n\n\ndef output_definitions(\n    definitions: list[tuple[str, tuple[int, int, int, int]]],\n    output_image_name: str,\n    file,\n):\n    for key_name, (x, y, w, h) in definitions:\n        print(\n            f'\"{key_name}\" N manual_sprite(\"{output_image_name}\", {x}, {y}, {w}, {h}) translate(y=2)',\n            file=file,\n        )\n\n\n\ndef main():\n    font_path = DATA_DIR / \"tomb-11.bdf\"\n    image_name = \"keyboard.png\"\n    mapping_name = \"mapping_keyboard.txt\"\n\n    glyphs_dir = DATA_DIR / \"common/glyphs\"\n    keycap_images = generate_keycap_images(\n        glyphs_dir / \"blank.png\", KEYCAP_WIDTHS\n    )\n\n    image_path = glyphs_dir / image_name\n    mapping_path = glyphs_dir / mapping_name\n    sprite_sheet, definitions = create_sprite_sheet(font_path, keycap_images)\n    sprite_sheet.save(image_path)\n    print(f\"{image_path} updated\")\n\n    with mapping_path.open(\"w\") as file:\n        output_definitions(definitions, image_name, file=file)\n        print(f\"{mapping_path} updated\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/glyphs/test_alignment.html",
    "content": "<style>\n  body {\n    margin: 0;\n    min-height: 100vh;\n    font-family: sans-serif;\n    text-align: center;\n  }\n  #wrapper {\n    height: 100%;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 20px;\n    align-items: center;\n    justify-content: space-around;\n  }\n</style>\n\n<main id=\"wrapper\">\n  <p>\n    Run me with <code>python3 -m http.server 8000 -d .</code> in the repository\n    directory,<br />\n    then visiting\n    <a href=\"http://localhost:8000/tools/glyphs/test_alignment.html\"\n      ><code>http://localhost:8000/tools/glyphs/test_alignment.html</code></a\n    >\n    rather than opening this file directly.\n  </p>\n</main>\n\n<script>\n  const columns = 16;\n  const color1 = \"red\";\n  const color2 = \"blue\";\n  const imageBorderColor = \"lime\";\n  const cellSize = 20;\n  const scale = 4;\n  const adjustPos = true;\n\n  async function getDefs(filename) {\n    const response = await fetch(`/data/tr1/glyphs/${filename}`, {\n      cache: \"no-store\",\n    });\n    const content = await response.text();\n\n    const results = [];\n\n    for (const match of content.matchAll(/include \"(?<file>[^\\\"]+)\"/g)) {\n      results.push(...(await getDefs(match.groups.file)));\n    }\n\n    results.push(\n      ...content\n        .matchAll(\n          /^(?<text>.*?)\\s+\\w\\s+manual_sprite\\(\"(?<name>[^\"]+)\",\\s*(?<x>\\d+),\\s*(?<y>\\d+),\\s*(?<w>\\d+),\\s*(?<h>\\d+)(?:,\\s*offset_y=(?<offset_y>-?\\d+))?(?:,\\s*index=(?<index>\\d+))?\\)/gm,\n        )\n        .map((match) => ({\n          name: match.groups.name,\n          x: +match.groups.x,\n          y: +match.groups.y,\n          w: +match.groups.w,\n          h: +match.groups.h,\n          text: match.groups.text.trim(),\n        })),\n    );\n\n    results.push(\n      ...content\n        .matchAll(\n          /^(?<text>.*?)\\s+\\w\\s+grid_sprite\\(\"(?<name>[^\"]+)\",\\s*(?<x>\\d+),\\s*(?<y>\\d+)(?:[^\\)]*)?\\)/gm,\n        )\n        .map((match) => ({\n          name: match.groups.name,\n          x: +match.groups.x * 20,\n          y: +match.groups.y * 20,\n          w: 20,\n          h: 20,\n          text: match.groups.text.trim(),\n        })),\n    );\n\n    return results;\n  }\n\n  async function loadDataAndDraw() {\n    const wrapper = document.getElementById(\"wrapper\");\n    wrapper.innerHTML = \"\";\n\n    const defs = await getDefs(\"mapping.txt\");\n    const uniqueFilenames = [...new Set(defs.map((def) => def.name))];\n    const images = await downloadImage(uniqueFilenames);\n\n    for (const filename of uniqueFilenames) {\n      const image = images.find((img) => img.src.includes(filename));\n      const relevantDefs = defs.filter((def) => def.name === filename);\n\n      const outerContainer = document.createElement(\"div\");\n      const header = document.createElement(\"h1\");\n      header.textContent = `${filename}`;\n\n      const container = document.createElement(\"div\");\n      container.style.position = \"relative\";\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = image.width;\n      canvas.height = image.height;\n\n      container.appendChild(canvas);\n      outerContainer.appendChild(header);\n      outerContainer.appendChild(container);\n      wrapper.appendChild(outerContainer);\n\n      const ctx = canvas.getContext(\"2d\");\n      ctx.canvas.width = image.width;\n      ctx.canvas.height = image.height;\n\n      drawRect(ctx, 0, 0, image.width, image.height, imageBorderColor);\n      for (const [idx, def] of relevantDefs.entries()) {\n        drawDef(ctx, def, idx);\n      }\n\n      drawImage(ctx, image, 0, 0);\n\n      upscaleCanvas(canvas, scale);\n\n      for (const def of relevantDefs) {\n        const zone = document.createElement(\"div\");\n        zone.title = def.text || def.name;\n        zone.style.position = \"absolute\";\n        zone.style.left = `${def.x * scale}px`;\n        zone.style.top = `${def.y * scale}px`;\n        zone.style.width = `${def.w * scale}px`;\n        zone.style.height = `${def.h * scale}px`;\n        zone.style.cursor = \"help\";\n        zone.style.background = \"transparent\";\n        container.appendChild(zone);\n      }\n    }\n  }\n\n  async function downloadImage(urls) {\n    return Promise.all(\n      urls.map(\n        (url) =>\n          new Promise((resolve, reject) => {\n            const img = new Image();\n            img.src = `/data/tr1/glyphs/${url}`;\n            img.onload = () => resolve(img);\n            img.onerror = () => resolve(img);\n          }),\n      ),\n    );\n  }\n\n  function drawImage(ctx, img, x, y) {\n    ctx.drawImage(img, x, y, img.width, img.height);\n  }\n\n  function drawDef(ctx, def, idx) {\n    const strokeColor = idx % 2 === 0 ? color1 : color2;\n    drawRect(ctx, def.x, def.y, def.x + def.w, def.y + def.h, strokeColor);\n  }\n\n  function drawRect(ctx, x1, y1, x2, y2, color) {\n    ctx.lineWidth = 1;\n    ctx.strokeStyle = color;\n    ctx.strokeRect(x1 + 0.5, y1 + 0.5, x2 - x1 - 1, y2 - y1 - 1);\n  }\n\n  function upscaleCanvas(canvas, scale) {\n    const ctx = canvas.getContext(\"2d\");\n    const width = canvas.width;\n    const height = canvas.height;\n    const imageData = ctx.getImageData(0, 0, width, height);\n    const offscreenCanvas = new OffscreenCanvas(width * scale, height * scale);\n    const offscreenCtx = offscreenCanvas.getContext(\"2d\");\n    offscreenCtx.imageSmoothingEnabled = false;\n    offscreenCtx.putImageData(imageData, 0, 0);\n    canvas.width = width * scale;\n    canvas.height = height * scale;\n    ctx.imageSmoothingEnabled = false;\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    ctx.drawImage(\n      offscreenCanvas,\n      0,\n      0,\n      width,\n      height,\n      0,\n      0,\n      width * scale,\n      height * scale,\n    );\n  }\n\n  loadDataAndDraw();\n</script>\n"
  },
  {
    "path": "tools/glyphs/test_language",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport sys\nimport unicodedata\nfrom pathlib import Path\n\n# pip install pyicu\nimport icu\n\n# HACK: Ensure the shared module is visible for this script.\nsys.path.insert(0, str(Path(__file__).resolve().parents[1]))\n\nfrom shared.glyph_mapping import get_glyph_map\nfrom shared.paths import PROJECT_PATHS\n\nMISSING_PRINT_LIMIT = 10\n\nLOCALES_TO_TEST = {\n    \"ar_SA\": \"Arabic\",\n    \"az_AZ\": \"Azerbaijani\",\n    \"be_BY\": \"Belarusian\",\n    \"bg_BG\": \"Bulgarian\",\n    \"bn_IN\": \"Bengali\",\n    \"bs_BA\": \"Bosnian\",\n    \"ca_ES\": \"Catalan\",\n    \"cs_CZ\": \"Czech\",\n    \"da_DK\": \"Danish\",\n    \"de_DE\": \"German\",\n    \"el_GR\": \"Greek\",\n    \"en_US\": \"English\",\n    \"es_ES\": \"Spanish\",\n    \"et_EE\": \"Estonian\",\n    \"eu_ES\": \"Basque\",\n    \"fa_IR\": \"Persian\",\n    \"fi_FI\": \"Finnish\",\n    \"fo_FO\": \"Faroese\",\n    \"fr_FR\": \"French\",\n    \"ga_IE\": \"Irish\",\n    \"gl_ES\": \"Galician\",\n    \"gu_IN\": \"Gujarati\",\n    \"he_IL\": \"Hebrew\",\n    \"hi_IN\": \"Hindi\",\n    \"hr_HR\": \"Croatian\",\n    \"hu_HU\": \"Hungarian\",\n    \"hy_AM\": \"Armenian\",\n    \"id_ID\": \"Indonesian\",\n    \"is_IS\": \"Icelandic\",\n    \"it_IT\": \"Italian\",\n    \"ja_JP\": \"Japanese\",\n    \"ka_GE\": \"Georgian\",\n    \"kk_KZ\": \"Kazakh\",\n    \"kn_IN\": \"Kannada\",\n    \"ko_KR\": \"Korean\",\n    \"kok_IN\": \"Konkani\",\n    \"lt_LT\": \"Lithuanian\",\n    \"lv_LV\": \"Latvian\",\n    \"mk_MK\": \"Macedonian\",\n    \"ml_IN\": \"Malayalam\",\n    \"mn_MN\": \"Mongolian\",\n    \"mr_IN\": \"Marathi\",\n    \"ms_MY\": \"Malay\",\n    \"mt_MT\": \"Maltese\",\n    \"nb_NO\": \"Norwegian Bokmål\",\n    \"nl_NL\": \"Dutch\",\n    \"nn_NO\": \"Norwegian Nynorsk\",\n    \"no_NO\": \"Norwegian\",\n    \"pa_IN\": \"Punjabi\",\n    \"pl_PL\": \"Polish\",\n    \"pt_BR\": \"Portuguese\",\n    \"pt_PT\": \"Portuguese\",\n    \"ro_RO\": \"Romanian\",\n    \"ru_RU\": \"Russian\",\n    \"se_NO\": \"Northern Sami\",\n    \"sk_SK\": \"Slovak\",\n    \"sl_SI\": \"Slovenian\",\n    \"sr_BA\": \"Serbian\",\n    \"sv_SE\": \"Swedish\",\n    \"tr_TR\": \"Turkish\",\n    \"zh_CN\": \"Chinese\",\n    \"zh_TW\": \"Chinese\",\n}\n\n\ndef get_glyphs_for_locale(locale_code: str) -> list[str]:\n    locale = icu.LocaleData(locale_code)\n    results: set[str] = set()\n    for glyph in locale.getExemplarSet(0, 0):\n        results.add(unicodedata.normalize(\"NFC\", glyph.lower()))\n        results.add(unicodedata.normalize(\"NFC\", glyph.upper()))\n    return sorted(r for r in results if len(r) == 1)\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"game\", type=int)\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    input_dir = PROJECT_PATHS[args.game].data_dir / \"glyphs\"\n    glyph_map = {glyph.text: glyph for glyph in get_glyph_map(input_dir)}\n    present_glyphs = set(glyph_map.keys())\n\n    for locale_code, language_name in LOCALES_TO_TEST.items():\n        requested_glyphs = set(get_glyphs_for_locale(locale_code))\n        missing_glyphs = requested_glyphs - present_glyphs\n\n        print(f\"{locale_code:>10s} {language_name} \", end=\"\")\n        if missing_glyphs:\n            print(\"not supported: missing \", end=\"\")\n            for glyph in sorted(missing_glyphs)[:MISSING_PRINT_LIMIT]:\n                print(f\"U+{ord(glyph):04X}:{glyph}\", end=\" \")\n            if len(missing_glyphs) >= MISSING_PRINT_LIMIT:\n                print(f\"...\", end=\" \")\n            print(f\"({len(missing_glyphs)} total)\")\n        else:\n            print(\"supported!\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/inspect_save",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport sys\nimport json\nimport struct\nimport zlib\nfrom pathlib import Path\n\nimport bson\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"path\", type=Path)\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    with args.path.open(\"rb\") as handle:\n        magic = handle.read(4)\n        version, compressed_size, uncompressed_size = struct.unpack(\n            \"III\", handle.read(12)\n        )\n        data = bson.loads(zlib.decompress(handle.read(compressed_size)))\n        flags, counter, level_num, title_size = struct.unpack(\"I\" * 4, handle.read(16))\n        title = handle.read(title_size)\n        print(json.dumps(data, indent=4))\n        print(\"flags:\", flags, file=sys.stderr)\n        print(\"counter:\", counter, file=sys.stderr)\n        print(\"level_num:\", level_num, file=sys.stderr)\n        print(\"title:\", title, file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/installer/.gitignore",
    "content": "*.suo\n*.o\n*.obj\n*.pdb\n*.lib\n*.exp\n[Dd]ebug/\n[Rr]elease/\n[Oo]bj/\n*.user\n*.ipch\n.vs/\n*.vcxproj\n*.filters\n*.pubxml\n[Oo]ut/\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/App.xaml",
    "content": "﻿<Application\n    x:Class=\"TR1X_Installer.App\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"/>\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/App.xaml.cs",
    "content": "﻿using System.Windows;\nusing TR1X_Installer.Installers;\nusing TRX_InstallerLib.Controls;\nusing TRX_InstallerLib.Installers;\n\nnamespace TR1X_Installer;\n\npublic partial class App : Application\n{\n    public App()\n    {\n        Current.MainWindow = new TRXInstallWindow(new List<IInstallSource>\n        {\n            new SteamInstallSource(),\n            new GOGInstallSource(),\n            new TombATIInstallSource(),\n            new CDRomInstallSource(),\n            new TR1XInstallSource(),\n        });\n        Current.MainWindow.Show();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Installers/CDRomInstallSource.cs",
    "content": "﻿using System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TR1X_Installer.Installers;\n\npublic class CDRomInstallSource : BaseInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            DriveInfo[] allDrives = DriveInfo.GetDrives();\n            foreach (var drive in allDrives)\n            {\n                if (drive.DriveType == DriveType.CDRom && drive.IsReady)\n                {\n                    yield return drive.RootDirectory.FullName;\n                }\n            }\n        }\n    }\n\n    public override bool IsImportingSavesSupported => false;\n    public override string SourceName => \"CDRom\";\n\n    public override async Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    )\n    {\n        var filterRegex = new Regex(@\"(data|fmv|music)[\\\\/]\", RegexOptions.IgnoreCase);\n        await InstallUtils.CopyDirectoryTree(\n            sourceDirectory,\n            targetDirectory,\n            progress,\n            file => filterRegex.IsMatch(file),\n            path => ConvertTargetPath(path)\n        );\n    }\n\n    public override bool IsDownloadingMusicNeeded(string sourceDirectory)\n    {\n        return true;\n    }\n\n    public override bool IsDownloadingExpansionNeeded(string sourceDirectory)\n    {\n        return true;\n    }\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return Directory.Exists(Path.Combine(sourceDirectory, \"DATA\"))\n            && Directory.Exists(Path.Combine(sourceDirectory, \"FMV\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"dos4gw.exe\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"tomb.exe\"));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Installers/GOGInstallSource.cs",
    "content": "using DiscUtils.Iso9660;\nusing DiscUtils.Streams;\nusing Microsoft.Win32;\nusing System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Models;\nusing TRX_InstallerLib.Utils;\n\nnamespace TR1X_Installer.Installers;\n\npublic class GOGInstallSource : BaseInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            yield return @\"C:\\Program Files (x86)\\GOG Galaxy\\Games\\Tomb Raider 1\";\n\n            using var key = Registry.ClassesRoot.OpenSubKey(@\"goggalaxy\\shell\\open\\command\");\n            if (key is not null)\n            {\n                var value = key.GetValue(\"\")?.ToString();\n                if (value is not null && new Regex(@\"\"\"(?<path>[^\"\"]+)\"\"\").Match(value) is { Success: true } match)\n                {\n                    yield return Path.Combine(Path.GetDirectoryName(match.Groups[\"path\"].Value)!, @\"Games\\Tomb Raider 1\");\n                }\n            }\n        }\n    }\n\n    public override bool IsImportingSavesSupported => false;\n    public override string SourceName => \"GOG\";\n\n    public override Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    )\n    {\n        var cuePath = Path.Combine(sourceDirectory, \"game.dat\");\n        var isoPath = Path.Combine(sourceDirectory, \"game.iso\");\n        CueFile cueFile;\n        try\n        {\n            cueFile = new CueFile(cuePath);\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_cue_failure\"], cuePath, e.Message));\n        }\n\n        try\n        {\n            var firstTrack = cueFile.TrackList.First();\n            firstTrack.Write(isoPath, progress);\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_converting_bin_failure\"], e.Message));\n        }\n\n        try\n        {\n            using FileStream file = File.Open(isoPath, FileMode.Open, FileAccess.Read);\n            using CDReader reader = new(file, true);\n            int currentProgress = 0;\n\n            progress.Report(new InstallProgress\n            {\n                MaximumValue = 1,\n                CurrentValue = 0,\n                Description = Language.Instance.Controls![\"progress_scanning_source\"],\n            });\n            var filesToExtract = GetFilesToExtract(reader.Root);\n            progress.Report(new InstallProgress\n            {\n                MaximumValue = filesToExtract.Count(),\n                CurrentValue = 0,\n                Description = Language.Instance.Controls![\"progress_preparing_extract\"],\n            });\n            foreach (var path in filesToExtract)\n            {\n                var relPath = ConvertTargetPath(path);\n                var targetPath = Path.Combine(targetDirectory, relPath);\n                if (!File.Exists(targetPath))\n                {\n                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);\n\n                    using SparseStream sourceStream = reader.OpenFile(path, FileMode.Open, FileAccess.Read);\n                    var readAllByte = new byte[sourceStream.Length];\n                    sourceStream.Read(readAllByte, 0, readAllByte.Length);\n\n                    using FileStream targetStream = new(targetPath, FileMode.Create);\n                    targetStream.Position = 0;\n                    targetStream.Write(readAllByte, 0, readAllByte.Length);\n                }\n\n                progress.Report(new InstallProgress\n                {\n                    MaximumValue = filesToExtract.Count(),\n                    CurrentValue = ++currentProgress,\n                    Description = string.Format(Language.Instance.Controls![\"progress_extracting\"], relPath)\n                });\n            }\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_converting_iso_failure\"], e.Message));\n        }\n\n        File.Delete(isoPath);\n\n        return Task.CompletedTask;\n    }\n\n    public override bool IsDownloadingMusicNeeded(string sourceDirectory)\n    {\n        return true;\n    }\n\n    public override bool IsDownloadingExpansionNeeded(string sourceDirectory)\n    {\n        return true;\n    }\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return File.Exists(Path.Combine(sourceDirectory, \"GAME.GOG\"));\n    }\n\n    private static IEnumerable<string> GetFilesToExtract(DiscUtils.DiscDirectoryInfo root)\n    {\n        var regex = new Regex(@\"^(data|fmv)[\\\\/].*$\", RegexOptions.IgnoreCase);\n        foreach (var dir in root.GetDirectories())\n        {\n            foreach (var filePath in GetFilesToExtract(dir))\n            {\n                yield return filePath;\n            }\n        }\n        foreach (var file in root.GetFiles())\n        {\n            string filePath = file.FullName;\n            if (regex.IsMatch(filePath))\n            {\n                yield return filePath;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Installers/SteamInstallSource.cs",
    "content": "using Microsoft.Win32;\nusing System.IO;\n\nnamespace TR1X_Installer.Installers;\n\npublic class SteamInstallSource : GOGInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            yield return @\"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Tomb Raider (I)\";\n\n            using var key = Registry.CurrentUser.OpenSubKey(@\"Software\\Valve\\Steam\");\n            if (key is not null)\n            {\n                var value = key.GetValue(\"SteamPath\")?.ToString();\n                if (value is not null)\n                {\n                    yield return Path.Combine(value, @\"steamapps\\common\\Tomb Raider (I)\");\n                }\n            }\n        }\n    }\n\n    public override string SourceName => \"Steam\";\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Installers/TR1XInstallSource.cs",
    "content": "using System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TR1X_Installer.Installers;\n\npublic class TR1XInstallSource : BaseInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            var previousPath = InstallUtils.GetPreviousInstallationPath();\n            if (previousPath is not null)\n            {\n                yield return previousPath;\n            }\n\n            foreach (var path in InstallUtils.GetDesktopShortcutDirectories())\n            {\n                yield return path;\n            }\n        }\n    }\n\n    public override string SuggestedInstallationDirectory\n    {\n        get => InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory;\n    }\n\n    public override bool IsImportingSavesSupported => true;\n    public override string SourceName => \"TR1X\";\n\n    public override async Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    )\n    {\n        var filterRegex = new Regex(importSaves ? @\"(data|fmv|music|saves)[\\\\/]|save.*\\.\\d+\" : @\"(data|fmv|music)[\\\\/]\", RegexOptions.IgnoreCase);\n        await InstallUtils.CopyDirectoryTree(\n            sourceDirectory,\n            targetDirectory,\n            progress,\n            file => filterRegex.IsMatch(file)\n        );\n    }\n\n    public override bool IsDownloadingMusicNeeded(string sourceDirectory)\n    {\n        return !Directory.Exists(Path.Combine(sourceDirectory, \"music\"));\n    }\n\n    public override bool IsDownloadingExpansionNeeded(string sourceDirectory)\n    {\n        return !File.Exists(Path.Combine(sourceDirectory, \"data\", \"cat.phd\"));\n    }\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return File.Exists(Path.Combine(sourceDirectory, \"TRX.exe\"))\n            || File.Exists(Path.Combine(sourceDirectory, \"TR1X.exe\"))\n            || File.Exists(Path.Combine(sourceDirectory, \"Tomb1Main.exe\"));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Installers/TombATIInstallSource.cs",
    "content": "using System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TR1X_Installer.Installers;\n\npublic class TombATIInstallSource : BaseInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            yield return \"C:\\\\TOMBATI\";\n            foreach (var path in InstallUtils.GetDesktopShortcutDirectories())\n            {\n                yield return path;\n            }\n            foreach (var path in new SteamInstallSource().DirectoriesToTry)\n            {\n                yield return path;\n            }\n        }\n    }\n\n    public override bool IsImportingSavesSupported => true;\n    public override string SourceName => \"TombATI\";\n\n    public override async Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    )\n    {\n        var filterRegex = new Regex(importSaves ? @\"(data|fmv|music)[\\\\/]|save.*\\.\\d+\\b\" : @\"(data|fmv|music)[\\\\/]\", RegexOptions.IgnoreCase);\n        await InstallUtils.CopyDirectoryTree(\n            sourceDirectory,\n            targetDirectory,\n            progress,\n            file => filterRegex.IsMatch(file),\n            path => ConvertTargetPath(path)\n        );\n    }\n\n    public override bool IsDownloadingMusicNeeded(string sourceDirectory)\n    {\n        return !Directory.Exists(Path.Combine(sourceDirectory, \"music\"));\n    }\n\n    public override bool IsDownloadingExpansionNeeded(string sourceDirectory)\n    {\n        return !File.Exists(Path.Combine(sourceDirectory, \"data\", \"cat.phd\"));\n    }\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return Directory.Exists(Path.Combine(sourceDirectory, \"DATA\"))\n            && Directory.Exists(Path.Combine(sourceDirectory, \"FMV\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"TombATI.exe\"));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Resources/Lang/en.json",
    "content": "{\n  \"Controls\": {\n    \"window_title_main\": \"TR1X Installer\",\n    \"step_source_content\": \"TR1X requires original game files to run.\\nPlease choose the source location where to install the data files from.\\nIf you're upgrading an existing installation, please choose TR1X.\",\n    \"step_settings_music_content\": \"Neither the Steam nor GOG versions of the game ship with the full soundtrack found on the PlayStation or Saturn retail releases. This option lets you download the missing tracks automatically (164 MB). The legality of these files is disputable; the most legal way to import the music to PC is to rip the audio tracks yourself from a physical PlayStation or Saturn disc.\",\n    \"step_settings_expansion_heading\": \"Download Unfinished Business expansion pack\",\n    \"step_settings_expansion_content\": \"The Unfinished Business expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (6 MB).\",\n    \"step_settings_expansion_music\": \"Fan-patched edition (includes music triggers)\",\n    \"step_settings_expansion_vanilla\": \"Original edition (does not include music triggers)\",\n    \"step_settings_saves_content\": \"Imports existing savegame files. Only TombATI and TR1X savegame format is supported at this time.\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Resources/Lang/it.json",
    "content": "{\n  \"Controls\": {\n    \"window_title_main\": \"Programma di installazione di TR1X\",\n    \"step_source_content\": \"Per eseguire TR1X, sono richiesti i file di gioco di Tomb Raider.\\nSeleziona il percorso da cui copiare questi file.\\nSe stai aggiornando un'installazione già esistente, seleziona 'TR1X'.\",\n    \"step_settings_music_content\": \"Né la versione Steam né quella GOG del gioco includono la colonna sonora completa presente nelle versioni PlayStation e Saturn. Questa opzione ti consente di scaricare automaticamente le tracce audio mancanti (164 MB). La legalità di questi file è discutibile; il modo più legale per importare la musica su PC è estrarre le tracce audio da un disco fisico in tuo possesso della versione PlayStation o Saturn.\",\n    \"step_settings_expansion_heading\": \"Scarica l'espansione Conti in Sospeso\",\n    \"step_settings_expansion_content\": \"Il pacchetto di espansione Conti in Sospeso è stato reso gratuito. Tuttavia, le versioni Steam e GOG non lo includono. Questa opzione ti consente di scaricare automaticamente i file dell'espansione (6 MB).\",\n    \"step_settings_expansion_music\": \"Edizione amatoriale (aggiunge il supporto alle tracce musicali)\",\n    \"step_settings_expansion_vanilla\": \"Edizione originale (senza supporto alle tracce musicali)\",\n    \"step_settings_saves_content\": \"Importa i file di salvataggio esistenti. Al momento sono supportati solo i formati di salvataggio di TombATI e TR1X.\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/Resources/const.json",
    "content": "{\n  \"Game\": \"TR1X\",\n  \"GoldGame\": \"TR1X - UB\",\n  \"GoldFileIdentifier\": \"cat.phd\",\n  \"AllowExpansionTypeSelection\": true,\n  \"ShortcutTitle\": \"Tomb Raider I: Community Edition\",\n  \"GoldZips\": {\n    \"0\": \"trub-music.zip\",\n    \"1\": \"trub-vanilla.zip\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TR1X_Installer/TR1X_Installer.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net8.0-windows</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <UseWPF>true</UseWPF>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\n    <GenerateAssemblyInfo>true</GenerateAssemblyInfo>\n    <AssemblyName>TR1X_Installer</AssemblyName>\n    <ProduceReferenceAssembly>True</ProduceReferenceAssembly>\n    <EnableWindowsTargeting>true</EnableWindowsTargeting>\n    <PublishSingleFile>true</PublishSingleFile>\n    <PublishTrimmed>false</PublishTrimmed>\n    <PublishReadyToRun>true</PublishReadyToRun>\n    <EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>\n    <SelfContained>false</SelfContained>\n    <RuntimeIdentifier>win-x64</RuntimeIdentifier>\n    <ApplicationIcon>Resources\\icon.ico</ApplicationIcon>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"DiscUtils.Iso9660\" Version=\"0.16.13\" />\n    <ProjectReference Include=\"..\\TRX_InstallerLib\\TRX_InstallerLib.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Remove=\"Resources\\const.json\" />\n    <None Remove=\"Resources\\icon.ico\" />\n    <None Remove=\"Resources\\Lang\\en.json\" />\n    <None Remove=\"Resources\\release.zip\" />\n    <None Remove=\"Resources\\side1.jpg\" />\n    <None Remove=\"Resources\\side2.jpg\" />\n    <None Remove=\"Resources\\side3.jpg\" />\n    <None Remove=\"Resources\\side4.jpg\" />\n    <None Remove=\"Resources\\TombATI.png\" />\n    <None Remove=\"Resources\\TR1X.png\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Resource Include=\"Resources\\CDRom.png\" />\n    <Resource Include=\"Resources\\GOG.png\" />\n    <Resource Include=\"Resources\\icon.ico\" />\n    <Resource Include=\"Resources\\side1.jpg\" />\n    <Resource Include=\"Resources\\side2.jpg\" />\n    <Resource Include=\"Resources\\side3.jpg\" />\n    <Resource Include=\"Resources\\side4.jpg\" />\n    <Resource Include=\"Resources\\Steam.png\" />\n    <Resource Include=\"Resources\\TombATI.png\" />\n    <Resource Include=\"Resources\\TR1X.png\" />\n    <EmbeddedResource Include=\"Resources\\const.json\" />\n    <EmbeddedResource Include=\"Resources\\Lang\\en.json\" />\n    <EmbeddedResource Include=\"Resources\\release.zip\" Condition=\"Exists('Resources\\release.zip')\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/App.xaml",
    "content": "﻿<Application\n    x:Class=\"TR2X_Installer.App\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"/>\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/App.xaml.cs",
    "content": "﻿using System.Windows;\nusing TR2X_Installer.Installers;\nusing TRX_InstallerLib.Controls;\nusing TRX_InstallerLib.Installers;\n\nnamespace TR2X_Installer;\n\npublic partial class App : Application\n{\n    public App()\n    {\n        Current.MainWindow = new TRXInstallWindow(new List<IInstallSource>\n        {\n            new SteamInstallSource(),\n            new GOGInstallSource(),\n            new CDRomInstallSource(),\n            new TR2XInstallSource(),\n        });\n        Current.MainWindow.Show();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Installers/CDRomInstallSource.cs",
    "content": "﻿using System.IO;\n\nnamespace TR2X_Installer.Installers;\n\npublic class CDRomInstallSource : GenericInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            DriveInfo[] allDrives = DriveInfo.GetDrives();\n            foreach (var drive in allDrives)\n            {\n                if (drive.DriveType == DriveType.CDRom && drive.IsReady)\n                {\n                    yield return drive.RootDirectory.FullName;\n                }\n            }\n        }\n    }\n\n    public override bool IsImportingSavesSupported => false;\n    public override string SourceName => \"CDRom\";\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return File.Exists(Path.Combine(sourceDirectory, \"fmv\", \"ancient.rpl\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"data\", \"wall.tr2\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Installers/GOGInstallSource.cs",
    "content": "using Microsoft.Win32;\nusing System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace TR2X_Installer.Installers;\n\npublic class GOGInstallSource : GenericInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            yield return @\"C:\\Program Files (x86)\\GOG Galaxy\\Games\\Tomb Raider 2\";\n\n            using var key = Registry.ClassesRoot.OpenSubKey(@\"goggalaxy\\shell\\open\\command\");\n            if (key is not null)\n            {\n                var value = key.GetValue(\"\")?.ToString();\n                if (value is not null && new Regex(@\"\"\"(?<path>[^\"\"]+)\"\"\").Match(value) is { Success: true } match)\n                {\n                    yield return Path.Combine(Path.GetDirectoryName(match.Groups[\"path\"].Value)!, @\"Games\\Tomb Raider 2\");\n                }\n            }\n        }\n    }\n\n    public override bool IsImportingSavesSupported => true;\n    public override string SourceName => \"GOG\";\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return File.Exists(Path.Combine(sourceDirectory, \"tomb2.exe\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"data\", \"wall.tr2\"))\n            && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Installers/GenericInstallSource.cs",
    "content": "﻿using System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TR2X_Installer.Installers;\n\npublic abstract class GenericInstallSource : BaseInstallSource\n{\n    private static readonly Dictionary<string, List<string>> _targetFiles = new()\n    {\n        [\"data\"] = new() { \".tr2\", \".sfx\", \".pcx\" },\n        [\"fmv\"] = new() { \".*\" },\n        [\"music\"] = new() { \".flac\", \".ogg\", \".mp3\", \".wav\" },\n    };\n\n    public override bool IsDownloadingMusicNeeded(string sourceDirectory)\n        => true;\n\n    public override bool IsDownloadingExpansionNeeded(string sourceDirectory)\n        => true;\n\n    public override async Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    )\n    {\n        await InstallUtils.CopyDirectoryTree(\n            sourceDirectory,\n            targetDirectory,\n            progress,\n            file => IsMatch(sourceDirectory, file, importSaves),\n            path => ConvertTargetPath(path)\n        );\n\n        string musicDir = Path.Combine(targetDirectory, \"music\");\n        string audioDir = Path.Combine(sourceDirectory, \"audio\");\n        if ((Directory.Exists(musicDir) && Directory.EnumerateFiles(musicDir).Any()) || !Directory.Exists(audioDir))\n        {\n            return;\n        }\n\n        await InstallUtils.CopyDirectoryTree(\n            Path.Combine(sourceDirectory, \"audio\"),\n            Path.Combine(targetDirectory, \"audio\"),\n            progress,\n            null,\n            path => ConvertTargetPath(path)\n        );\n    }\n\n    private static bool IsMatch(string sourceDirectory, string path, bool importSaves)\n    {\n        string[] parts = Path.GetRelativePath(sourceDirectory, path).ToLower().Split('\\\\');\n        if (parts.Length == 1 && importSaves && Regex.IsMatch(parts[0], @\"savegame.\\d+\", RegexOptions.IgnoreCase))\n        {\n            return true;\n        }\n\n        return parts.Length > 0\n            && _targetFiles.ContainsKey(parts[0])\n            && (_targetFiles[parts[0]].Contains(\".*\") || _targetFiles[parts[0]].Contains(Path.GetExtension(path).ToLower()));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Installers/SteamInstallSource.cs",
    "content": "using Microsoft.Win32;\nusing System.IO;\n\nnamespace TR2X_Installer.Installers;\n\npublic class SteamInstallSource : GOGInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            yield return @\"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Tomb Raider (II)\";\n\n            using var key = Registry.CurrentUser.OpenSubKey(@\"Software\\Valve\\Steam\");\n            if (key is not null)\n            {\n                var value = key.GetValue(\"SteamPath\")?.ToString();\n                if (value is not null)\n                {\n                    yield return Path.Combine(value, @\"steamapps\\common\\Tomb Raider (II)\");\n                }\n            }\n        }\n    }\n\n    public override string SourceName => \"Steam\";\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Installers/TR2XInstallSource.cs",
    "content": "using System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TR2X_Installer.Installers;\n\npublic class TR2XInstallSource : GenericInstallSource\n{\n    public override IEnumerable<string> DirectoriesToTry\n    {\n        get\n        {\n            var previousPath = InstallUtils.GetPreviousInstallationPath();\n            if (previousPath is not null)\n            {\n                yield return previousPath;\n            }\n\n            foreach (var path in InstallUtils.GetDesktopShortcutDirectories())\n            {\n                yield return path;\n            }\n        }\n    }\n\n    public override string SuggestedInstallationDirectory\n    {\n        get\n        {\n            return InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory;\n        }\n    }\n\n    public override bool IsImportingSavesSupported => true;\n    public override string SourceName => \"TR2X\";\n\n    public override async Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    )\n    {\n        var filterRegex = new Regex(importSaves ? @\"(audio|data|fmv|music|saves)[\\\\/]|save.*\\.\\d+\" : @\"(audio|data|fmv|music)[\\\\/]\", RegexOptions.IgnoreCase);\n        await InstallUtils.CopyDirectoryTree(\n            sourceDirectory,\n            targetDirectory,\n            progress,\n            file => filterRegex.IsMatch(file)\n        );\n    }\n\n    public override bool IsDownloadingExpansionNeeded(string sourceDirectory)\n    {\n        return !File.Exists(Path.Combine(sourceDirectory, \"data\", \"title_gm.tr2\"));\n    }\n\n    public override bool IsGameFound(string sourceDirectory)\n    {\n        return File.Exists(Path.Combine(sourceDirectory, \"TRX.exe\"))\n            || File.Exists(Path.Combine(sourceDirectory, \"TR2X.exe\"));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Resources/Lang/en.json",
    "content": "{\n  \"Controls\": {\n    \"window_title_main\": \"TR2X Installer\",\n    \"step_source_content\": \"TR2X requires original game files to run.\\nPlease choose the source location where to install the data files from.\\nIf you're upgrading an existing installation, please choose TR2X.\",\n    \"step_settings_music_content\": \"This option lets you download compatible music files for the game automatically (60 MB). The legality of these files is disputable; the most legal way to import the music to PC is to obtain them from your own source - TR2 supports FLAC, OGG, MP3 and WAV files.\",\n    \"step_settings_expansion_heading\": \"Download The Golden Mask expansion pack\",\n    \"step_settings_expansion_content\": \"The Golden Mask expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (15 MB).\",\n    \"step_settings_saves_content\": \"Imports existing savegame files.\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Resources/Lang/it.json",
    "content": "{\n  \"Controls\": {\n    \"window_title_main\": \"Programma di installazione di TR2X\",\n    \"step_source_content\": \"Per eseguire TR2X, sono richiesti i file di gioco di Tomb Raider II.\\nSeleziona il percorso da cui copiare questi file.\\nSe stai aggiornando un'installazione già esistente, seleziona 'TR2X'.\",\n    \"step_settings_music_content\": \"Questa opzione ti consente di scaricare automaticamente i file musicali compatibili per il gioco (60 MB). La legalità di questi file è discutibile; il modo più legale per importare la musica su PC è estrarre le tracce audio da un disco fisico in tuo possesso - TR2X supporta i formati FLAC, OGG, MP3 e WAV.\",\n    \"step_settings_expansion_heading\": \"Scarica l'espansione La Maschera Dorata\",\n    \"step_settings_expansion_content\": \"Il pacchetto di espansione La Maschera Dorata è stato reso gratuito. Tuttavia, le versioni Steam e GOG non lo includono. Questa opzione ti consente di scaricare automaticamente i file dell'espansione (15 MB).\",\n    \"step_settings_saves_content\": \"Importa i file di salvataggio esistenti.\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/Resources/const.json",
    "content": "{\n  \"Game\": \"TR2X\",\n  \"GoldGame\": \"TR2X - GM\",\n  \"GoldFileIdentifier\": \"title_gm.tr2\",\n  \"AllowExpansionTypeSelection\": false,\n  \"ShortcutTitle\": \"Tomb Raider II: Community Edition\",\n  \"GoldZips\": {\n    \"0\": \"trgm.zip\",\n    \"1\": \"trgm.zip\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TR2X_Installer/TR2X_Installer.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net8.0-windows</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <UseWPF>true</UseWPF>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n\n    <GenerateAssemblyInfo>true</GenerateAssemblyInfo>\n    <AssemblyName>TR2X_Installer</AssemblyName>\n    <ProduceReferenceAssembly>True</ProduceReferenceAssembly>\n    <EnableWindowsTargeting>true</EnableWindowsTargeting>\n    <PublishSingleFile>true</PublishSingleFile>\n    <PublishTrimmed>false</PublishTrimmed>\n    <PublishReadyToRun>true</PublishReadyToRun>\n    <EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>\n    <SelfContained>false</SelfContained>\n    <RuntimeIdentifier>win-x64</RuntimeIdentifier>\n    <ApplicationIcon>Resources\\icon.ico</ApplicationIcon>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\TRX_InstallerLib\\TRX_InstallerLib.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Remove=\"Resources\\const.json\" />\n    <None Remove=\"Resources\\icon.ico\" />\n    <None Remove=\"Resources\\Lang\\en.json\" />\n    <None Remove=\"Resources\\release.zip\" />\n    <None Remove=\"Resources\\side1.jpg\" />\n    <None Remove=\"Resources\\side2.jpg\" />\n    <None Remove=\"Resources\\side3.jpg\" />\n    <None Remove=\"Resources\\side4.jpg\" />\n    <None Remove=\"Resources\\TR2X.png\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Resource Include=\"Resources\\CDRom.png\" />\n    <Resource Include=\"Resources\\GOG.png\" />\n    <Resource Include=\"Resources\\icon.ico\" />\n    <Resource Include=\"Resources\\side1.jpg\" />\n    <Resource Include=\"Resources\\side2.jpg\" />\n    <Resource Include=\"Resources\\side3.jpg\" />\n    <Resource Include=\"Resources\\side4.jpg\" />\n    <Resource Include=\"Resources\\Steam.png\" />\n    <Resource Include=\"Resources\\TR2X.png\" />\n    <EmbeddedResource Include=\"Resources\\const.json\" />\n    <EmbeddedResource Include=\"Resources\\Lang\\en.json\" />\n    <EmbeddedResource Include=\"Resources\\release.zip\" Condition=\"Exists('Resources\\release.zip')\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "tools/installer/TRX_Installer/App.xaml",
    "content": "﻿<Application x:Class=\"TRX_Installer.App\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n</Application>\n"
  },
  {
    "path": "tools/installer/TRX_Installer/App.xaml.cs",
    "content": "﻿using System.Windows;\n\nnamespace TRX_Installer;\n\npublic partial class App : System.Windows.Application\n{\n    public App()\n    {\n        Current.MainWindow = new MainWindow();\n        Current.MainWindow.Show();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/BoolToVisibilityConverter.cs",
    "content": "using System.Globalization;\nusing System.Windows;\nusing System.Windows.Data;\n\nnamespace TRX_Installer;\n\npublic class BoolToVisibilityConverter : IValueConverter\n{\n    public Visibility TrueValue { get; set; } = Visibility.Visible;\n    public Visibility FalseValue { get; set; } = Visibility.Collapsed;\n\n    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n        => value is true ? TrueValue : FalseValue;\n\n    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n        => throw new NotImplementedException();\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/CueFile.cs",
    "content": "using System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace TRX_Installer;\n\npublic class CueFile\n{\n    public readonly List<CueTrack> TrackList = new();\n\n    public CueFile(string cueFilePath)\n    {\n        _cueFilePath = cueFilePath;\n        string cueFileContent;\n        using (TextReader cueReader = new StreamReader(cueFilePath))\n        {\n            cueFileContent = cueReader.ReadToEnd();\n        }\n\n        MatchCollection fileMatches = FileGroupRegex.Matches(cueFileContent);\n        if (fileMatches.Count == 0)\n        {\n            throw new ApplicationException($\"Could not parse {cueFilePath}: no tracks were found\");\n        }\n\n        foreach (Match fileMatch in fileMatches.Cast<Match>())\n        {\n            string binFilePath = GetBinFilePath(fileMatch.Groups[\"name\"].Value.Trim('\"'));\n            MatchCollection matches = TrackRegex.Matches(fileMatch.Groups[\"content\"].Value);\n\n            if (matches.Count == 0)\n            {\n                throw new ApplicationException($\"Could not parse {cueFilePath}: no tracks were found\");\n            }\n\n            CueTrack? track = null;\n            CueTrack? prevTrack = null;\n            foreach (Match trackMatch in matches.Cast<Match>())\n            {\n                track = new CueTrack(\n                    binFilePath,\n                    int.Parse(trackMatch.Groups[\"track\"].Value),\n                    trackMatch.Groups[\"mode\"].Value,\n                    trackMatch.Groups[\"time\"].Value);\n\n                if (prevTrack is not null)\n                {\n                    prevTrack.Stop = track.StartPosition - 1;\n                    prevTrack.StopSector = track.StartSector;\n                }\n                TrackList.Add(track);\n                prevTrack = track;\n            }\n\n            if (track is null)\n            {\n                return;\n            }\n\n            track.Stop = GetBinFileLength(binFilePath);\n            track.StopSector = track.Stop / CueTrack.SectorLength;\n        }\n    }\n\n    private static readonly Regex FileGroupRegex = new(\n        @\"^file\\s+(?<name>\"\"[^\"\"]+\"\"|[^\"\"\\s]+)\\s+(?<mode>\\w+)\\s+(?<content>(.(?!^file))*)\",\n        RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline);\n\n    private static readonly Regex TrackRegex = new(\n        @\"track\\s+?(?<track>\\d+?)\\s+?(?<mode>\\S+?)[\\s$]+?index\\s+?\\d+?\\s+?(?<time>\\S*)\",\n        RegexOptions.IgnoreCase | RegexOptions.Multiline);\n\n    private readonly string _cueFilePath;\n\n    private static long GetBinFileLength(string binFilePath)\n    {\n        FileInfo fileInfo = new(binFilePath);\n        return fileInfo.Length;\n    }\n\n    private string GetBinFilePath(string name)\n    {\n        string cueDirectory = Path.GetDirectoryName(_cueFilePath)!;\n        string result = Path.Combine(cueDirectory, Path.GetFileName(name));\n        if (!File.Exists(result))\n        {\n            result = Path.Combine(cueDirectory, Path.GetFileNameWithoutExtension(_cueFilePath) + \".bin\");\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/CueTrack.cs",
    "content": "using System.IO;\n\nnamespace TRX_Installer;\n\npublic class CueTrack\n{\n    public const int SectorLength = 2352;\n\n    public CueTrack(string binFilePath, int trackNumber, string mode, string time)\n    {\n        BinFilePath = binFilePath;\n        TrackNumber = trackNumber;\n        SetMode(mode);\n        StartSector = ToFrames(time);\n    }\n\n    public bool Audio { get; private set; }\n    public string BinFilePath { get; }\n    public int BlockSize { get; private set; }\n    public int BlockStart { get; private set; }\n    public bool SwapAudioByteOrder { get; private set; }\n    public long StartPosition => StartSector * SectorLength;\n    public long StartSector { get; }\n    public long Stop { get; set; }\n    public long StopSector { get; set; }\n    public long TotalBytes => (StopSector - StartSector + 1) * BlockSize;\n    public int TrackNumber { get; }\n    public bool TruncatePsx { get; private set; }\n    public bool WavFormat { get; private set; }\n\n    public void Write(string targetPath, IProgress<(int current, int maximum)>? progress)\n    {\n        using FileStream fileStream = OpenBinFile();\n        using Stream stream = File.OpenWrite(targetPath);\n        long currentPosition = StartPosition;\n        long sector = StartSector;\n        long convertedBytes = 0;\n        byte[] buffer = new byte[SectorLength];\n\n        while (sector <= StopSector && fileStream.Read(buffer, 0, SectorLength) > 0)\n        {\n            if (Audio && SwapAudioByteOrder)\n            {\n                DoByteSwap(buffer);\n            }\n\n            stream.Write(buffer, BlockStart, BlockSize);\n            currentPosition += SectorLength;\n            convertedBytes += BlockSize;\n\n            if (progress is not null && currentPosition / SectorLength % 500 == 0)\n            {\n                progress.Report(((int)convertedBytes, (int)TotalBytes));\n            }\n\n            sector++;\n        }\n\n        progress?.Report(((int)TotalBytes, (int)TotalBytes));\n    }\n\n    private static long ToFrames(string time)\n    {\n        string[] segments = time.Split(':');\n        int mins = int.Parse(segments[0]);\n        int secs = int.Parse(segments[1]);\n        int frames = int.Parse(segments[2]);\n        return (mins * 60 + secs) * 75 + frames;\n    }\n\n    private void DoByteSwap(byte[] buffer)\n    {\n        int position = BlockStart;\n        while (position < BlockSize)\n        {\n            (buffer[position + 1], buffer[position]) = (buffer[position], buffer[position + 1]);\n            position += 2;\n        }\n    }\n\n    private FileStream OpenBinFile()\n    {\n        FileStream fileStream = File.OpenRead(BinFilePath);\n        fileStream.Seek(StartPosition, SeekOrigin.Begin);\n        return fileStream;\n    }\n\n    private void SetMode(string mode)\n    {\n        Audio = false;\n        BlockStart = 0;\n\n        switch (mode.ToUpperInvariant())\n        {\n            case \"AUDIO\":\n                BlockSize = 2352;\n                Audio = true;\n                break;\n            case \"MODE1/2352\":\n                BlockStart = 16;\n                BlockSize = 2048;\n                break;\n            case \"MODE2/2336\":\n                BlockStart = 16;\n                BlockSize = 2336;\n                break;\n            case \"MODE2/2352\":\n                if (TruncatePsx)\n                {\n                    BlockSize = 2336;\n                }\n                else\n                {\n                    BlockStart = 24;\n                    BlockSize = 2048;\n                }\n                break;\n            default:\n                BlockSize = 2352;\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/DiscImageInstallSource.cs",
    "content": "using DiscUtils.Iso9660;\nusing DiscUtils.Streams;\nusing System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace TRX_Installer;\n\ninternal class DiscImageInstallSource : IInstallSource\n{\n    public async Task InstallAsync(\n        string sourceDirectory,\n        string targetDirectory,\n        string gameId,\n        IInstallerProgress progress)\n    {\n        string cuePath = Path.Combine(sourceDirectory, \"game.dat\");\n        string isoPath = Path.Combine(sourceDirectory, \"game.iso\");\n        CueFile cueFile = new(cuePath);\n        CueTrack firstTrack = cueFile.TrackList.First();\n        firstTrack.Write(isoPath, null);\n\n        try\n        {\n            using FileStream file = File.Open(isoPath, FileMode.Open, FileAccess.Read);\n            using CDReader reader = new(file, true);\n            List<string> filesToExtract = GetFilesToExtract(reader.Root).ToList();\n            HashSet<string> availablePaths = filesToExtract\n                .Select(InstallMappings.Normalize)\n                .ToHashSet(StringComparer.OrdinalIgnoreCase);\n            progress.SetSubDescription(\"Extracting files from disc image...\");\n            progress.SetInnerProgress(0, Math.Max(1, filesToExtract.Count));\n\n            int done = 0;\n            foreach (string path in filesToExtract)\n            {\n                string? relPath = InstallMappings.MapOriginalFile(gameId, path, availablePaths);\n                if (relPath is null)\n                {\n                    done++;\n                    progress.SetInnerProgress(done, Math.Max(1, filesToExtract.Count));\n                    continue;\n                }\n\n                string targetPath = Path.Combine(targetDirectory, relPath);\n                Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);\n                using SparseStream sourceStream = reader.OpenFile(path, FileMode.Open, FileAccess.Read);\n                byte[] buffer = new byte[sourceStream.Length];\n                sourceStream.Read(buffer, 0, buffer.Length);\n                await File.WriteAllBytesAsync(targetPath, buffer);\n                done++;\n                progress.SetInnerProgress(done, Math.Max(1, filesToExtract.Count));\n                progress.AppendLog($\"Extracting {relPath}\");\n            }\n        }\n        finally\n        {\n            if (File.Exists(isoPath))\n            {\n                File.Delete(isoPath);\n            }\n        }\n    }\n\n    private static IEnumerable<string> GetFilesToExtract(DiscUtils.DiscDirectoryInfo root)\n    {\n        Regex regex = new(@\"^(data|fmv|music)[\\\\/].*$\", RegexOptions.IgnoreCase);\n        foreach (DiscUtils.DiscDirectoryInfo dir in root.GetDirectories())\n        {\n            foreach (string filePath in GetFilesToExtract(dir))\n            {\n                yield return filePath;\n            }\n        }\n        foreach (DiscUtils.DiscFileInfo file in root.GetFiles())\n        {\n            if (regex.IsMatch(file.FullName))\n            {\n                yield return file.FullName;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/DownloadOption.cs",
    "content": "namespace TRX_Installer;\n\npublic class DownloadOption\n{\n    public DownloadOption(string title, string url)\n    {\n        Title = title;\n        Url = url;\n    }\n\n    public string Title { get; }\n    public string Url { get; }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/ExistingTRXInstallSource.cs",
    "content": "namespace TRX_Installer;\n\ninternal class ExistingTRXInstallSource : IInstallSource\n{\n    public async Task InstallAsync(\n        string sourceDirectory,\n        string targetDirectory,\n        string gameId,\n        IInstallerProgress progress)\n    {\n        await InstallFileHelper.CopyMappedDirectoryAsync(\n            sourceDirectory,\n            targetDirectory,\n            (relPath, _) => InstallMappings.MapCombinedFile(gameId, relPath),\n            progress);\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/IInstallSource.cs",
    "content": "namespace TRX_Installer;\n\npublic interface IInstallSource\n{\n    Task InstallAsync(\n        string sourceDirectory,\n        string targetDirectory,\n        string gameId,\n        IInstallerProgress progress);\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/IInstallerProgress.cs",
    "content": "namespace TRX_Installer;\n\npublic interface IInstallerProgress\n{\n    void SetDescription(string description);\n    void SetSubDescription(string subDescription);\n    void SetOuterProgress(int current, int maximum);\n    void SetInnerProgress(int current, int maximum);\n    void AppendLog(string message);\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/InstallComponent.cs",
    "content": "using System.Collections.ObjectModel;\nusing System.ComponentModel;\nusing System.Runtime.CompilerServices;\nusing System.Windows.Media;\n\nnamespace TRX_Installer;\n\npublic class InstallComponent : INotifyPropertyChanged\n{\n    public InstallComponent(\n        string gameId,\n        string title,\n        string description,\n        IEnumerable<InstallSourceOption>? sourceOptions = null,\n        string? downloadUrl = null,\n        IEnumerable<DownloadOption>? downloadOptions = null,\n        IEnumerable<OptionalDownload>? optionalDownloads = null\n    )\n    {\n        GameId = gameId;\n        Title = title;\n        Description = description;\n        DownloadUrl = downloadUrl;\n        DownloadOptions = new ObservableCollection<DownloadOption>(\n            downloadOptions ?? Array.Empty<DownloadOption>());\n        OptionalDownloads = new ObservableCollection<OptionalDownload>(\n            optionalDownloads ?? Array.Empty<OptionalDownload>());\n        SourceOptions = new ObservableCollection<InstallSourceOption>(\n            sourceOptions ?? Array.Empty<InstallSourceOption>());\n        SelectedDownloadOption = DownloadOptions.FirstOrDefault();\n        AutoDetectSource();\n    }\n\n    public event PropertyChangedEventHandler? PropertyChanged;\n\n    public string Description { get; }\n    public InstallSourceOption? DetectedSource\n        => SourceDirectory is null\n            ? null\n            : SourceOptions.FirstOrDefault(option => option.IsGameFound(SourceDirectory));\n    public ObservableCollection<DownloadOption> DownloadOptions { get; }\n    public string? DownloadUrl { get; }\n    public string GameId { get; }\n    public bool HasDownload => DownloadUrl is not null || DownloadOptions.Count != 0;\n    public bool HasOptionalDownloads => OptionalDownloads.Count != 0;\n    public bool HasSource => SourceOptions.Count != 0;\n    public bool HasDownloadOptions => DownloadOptions.Count > 1;\n    public ObservableCollection<OptionalDownload> OptionalDownloads { get; }\n\n    public bool Install\n    {\n        get => _install;\n        set\n        {\n            if (value == _install)\n            {\n                return;\n            }\n            _install = value;\n            OnPropertyChanged();\n            OnPropertyChanged(nameof(IsReadyToInstall));\n        }\n    }\n\n    public bool IsReadyToInstall\n        => (HasSource && DetectedSource is not null) || (!HasSource && ResolvedDownloadUrl is not null);\n\n    public string? ResolvedDownloadUrl\n        => SelectedDownloadOption?.Url ?? DownloadUrl;\n\n    public DownloadOption? SelectedDownloadOption\n    {\n        get => _selectedDownloadOption;\n        set\n        {\n            if (value == _selectedDownloadOption)\n            {\n                return;\n            }\n            _selectedDownloadOption = value;\n            OnPropertyChanged();\n            OnPropertyChanged(nameof(ResolvedDownloadUrl));\n            OnPropertyChanged(nameof(IsReadyToInstall));\n        }\n    }\n\n    public string? SourceDirectory\n    {\n        get => _sourceDirectory;\n        set\n        {\n            if (value == _sourceDirectory)\n            {\n                return;\n            }\n            _sourceDirectory = value;\n            _isManualSourceSelected = false;\n            OnPropertyChanged();\n            OnPropertyChanged(nameof(DetectedSource));\n            OnPropertyChanged(nameof(SourceStatus));\n            OnPropertyChanged(nameof(SourceStatusBrush));\n            OnPropertyChanged(nameof(IsReadyToInstall));\n        }\n    }\n\n    public ObservableCollection<InstallSourceOption> SourceOptions { get; }\n\n    public string SourceStatus\n    {\n        get\n        {\n            if (HasDownload && !HasSource)\n            {\n                return \"(download)\";\n            }\n            if (_isManualSourceSelected && DetectedSource is not null)\n            {\n                return \"(manual source selected)\";\n            }\n            if (DetectedSource is not null)\n            {\n                return $\"(found via {DetectedSource.SourceName})\";\n            }\n            return \"(source not found)\";\n        }\n    }\n\n    public System.Windows.Media.Brush SourceStatusBrush\n        => DetectedSource is not null || (HasDownload && !HasSource)\n            ? System.Windows.Media.Brushes.ForestGreen\n            : System.Windows.Media.Brushes.Firebrick;\n\n    public string Title { get; }\n\n    private bool _install;\n    private bool _isManualSourceSelected;\n    private DownloadOption? _selectedDownloadOption;\n    private string? _sourceDirectory;\n\n    public void SetManualSourceDirectory(string sourceDirectory)\n    {\n        if (sourceDirectory == _sourceDirectory)\n        {\n            return;\n        }\n\n        _sourceDirectory = sourceDirectory;\n        _isManualSourceSelected = true;\n        OnPropertyChanged(nameof(SourceDirectory));\n        OnPropertyChanged(nameof(DetectedSource));\n        OnPropertyChanged(nameof(SourceStatus));\n        OnPropertyChanged(nameof(SourceStatusBrush));\n        OnPropertyChanged(nameof(IsReadyToInstall));\n    }\n\n    private void AutoDetectSource()\n    {\n        foreach (InstallSourceOption option in SourceOptions)\n        {\n            foreach (string directory in option.DirectoriesToTry)\n            {\n                if (!option.IsGameFound(directory))\n                {\n                    continue;\n                }\n                SourceDirectory = directory;\n                return;\n            }\n        }\n    }\n\n    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/InstallComponentFactory.cs",
    "content": "using Microsoft.Win32;\nusing System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace TRX_Installer;\n\ninternal static class InstallComponentFactory\n{\n    private const string RegistryPath = @\"Software\\TRX\";\n    private const string ResourceBaseUrl = \"https://lostartefacts.dev/aux/trx\";\n\n    private static readonly IInstallSource _originalDir = new OriginalDirectoryInstallSource();\n    private static readonly IInstallSource _discImage = new DiscImageInstallSource();\n    private static readonly IInstallSource _existingTRX = new ExistingTRXInstallSource();\n\n    public static IEnumerable<InstallComponent> CreateComponents()\n    {\n        yield return new InstallComponent(\n            \"tr1\",\n            \"TR1\",\n            \"Original Tomb Raider I files from Steam, GOG, disc, or an existing TRX installation.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr1.zip\",\n            sourceOptions:\n            [\n                new InstallSourceOption(\n                    \"Steam\",\n                    GetSteamDirectories(\"Tomb Raider (I)\"),\n                    _discImage,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"GAME.GOG\"))\n                        || File.Exists(Path.Combine(sourceDirectory, \"game.dat\"))),\n                new InstallSourceOption(\n                    \"GOG\",\n                    GetGOGDirectories(\"Tomb Raider 1\"),\n                    _discImage,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"GAME.GOG\"))\n                        || File.Exists(Path.Combine(sourceDirectory, \"game.dat\"))),\n                new InstallSourceOption(\n                    \"Disc\",\n                    DriveInfo.GetDrives()\n                        .Where(drive => drive.DriveType == DriveType.CDRom && drive.IsReady)\n                        .Select(drive => drive.RootDirectory.FullName),\n                    _originalDir,\n                    sourceDirectory =>\n                        Directory.Exists(Path.Combine(sourceDirectory, \"DATA\"))\n                        && Directory.Exists(Path.Combine(sourceDirectory, \"FMV\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"dos4gw.exe\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"tomb.exe\"))),\n                new InstallSourceOption(\n                    \"TombATI\",\n                    GetTombATIDirectories(),\n                    _originalDir,\n                    sourceDirectory =>\n                        Directory.Exists(Path.Combine(sourceDirectory, \"DATA\"))\n                        && Directory.Exists(Path.Combine(sourceDirectory, \"FMV\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"TombATI.exe\"))),\n                new InstallSourceOption(\n                    \"Legacy TR1X/TRX\",\n                    GetTRXDirectories(),\n                    _originalDir,\n                    sourceDirectory =>\n                        Directory.Exists(Path.Combine(sourceDirectory, \"cfg\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"level1.phd\"))),\n                new InstallSourceOption(\n                    \"TRX\",\n                    GetTRXDirectories(),\n                    _existingTRX,\n                    sourceDirectory => Directory.Exists(Path.Combine(sourceDirectory, \"games\", \"tr1\")))\n            ],\n            optionalDownloads:\n            [\n                new OptionalDownload(\"Download music\", $\"{ResourceBaseUrl}/tr1-music.zip\"),\n            ]);\n\n        yield return new InstallComponent(\n            \"tr1-ub\",\n            \"TR1:UB\",\n            \"Unfinished Business pack downloaded from Lost Artefacts.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr1-ub.zip\",\n            downloadOptions:\n            [\n                new DownloadOption(\"Fan-patched edition\", $\"{ResourceBaseUrl}/tr1-ub-music.zip\"),\n                new DownloadOption(\"Original edition\", $\"{ResourceBaseUrl}/tr1-ub-vanilla.zip\"),\n            ]);\n\n        yield return new InstallComponent(\n            \"tr1-demo-pc\",\n            \"TR1 PC Demo\",\n            \"PC demo pack downloaded from Lost Artefacts.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr1-demo-pc.zip\");\n\n        yield return new InstallComponent(\n            \"tr2\",\n            \"TR2\",\n            \"Original Tomb Raider II files from Steam, GOG, disc, or an existing TRX installation.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr2.zip\",\n            sourceOptions:\n            [\n                new InstallSourceOption(\n                    \"Steam\",\n                    GetSteamDirectories(\"Tomb Raider (II)\"),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"tomb2.exe\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"wall.tr2\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"))),\n                new InstallSourceOption(\n                    \"GOG\",\n                    GetGOGDirectories(\"Tomb Raider 2\"),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"tomb2.exe\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"wall.tr2\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"))),\n                new InstallSourceOption(\n                    \"Disc\",\n                    DriveInfo.GetDrives()\n                        .Where(drive => drive.DriveType == DriveType.CDRom && drive.IsReady)\n                        .Select(drive => drive.RootDirectory.FullName),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"fmv\", \"ancient.rpl\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"wall.tr2\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"))),\n                new InstallSourceOption(\n                    \"Legacy TR2X/TRX\",\n                    GetTRXDirectories(),\n                    _originalDir,\n                    sourceDirectory =>\n                        Directory.Exists(Path.Combine(sourceDirectory, \"cfg\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"wall.tr2\"))),\n                new InstallSourceOption(\n                    \"TRX\",\n                    GetTRXDirectories(),\n                    _existingTRX,\n                    sourceDirectory => Directory.Exists(Path.Combine(sourceDirectory, \"games\", \"tr2\")))\n            ],\n            optionalDownloads:\n            [\n                new OptionalDownload(\"Download music\", $\"{ResourceBaseUrl}/tr2-music.zip\"),\n            ]);\n\n        yield return new InstallComponent(\n            \"tr2-gm\",\n            \"TR2:GM\",\n            \"Golden Mask pack downloaded from Lost Artefacts.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr2-gm.zip\");\n\n        yield return new InstallComponent(\n            \"tr3\",\n            \"TR3\",\n            \"Original Tomb Raider III files from Steam, GOG, disc, or an existing TRX installation.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr3.zip\",\n            sourceOptions:\n            [\n                new InstallSourceOption(\n                    \"Steam\",\n                    GetSteamDirectories(\"Tomb Raider (III)\", \"TombRaider (III)\"),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"data\", \"jungle.tr2\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"audio\", \"cdaudio.wad\"))),\n                new InstallSourceOption(\n                    \"GOG\",\n                    GetGOGDirectories(\"Tomb Raider 3\"),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"data\", \"jungle.tr2\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"audio\", \"cdaudio.wad\"))),\n                new InstallSourceOption(\n                    \"Disc\",\n                    DriveInfo.GetDrives()\n                        .Where(drive => drive.DriveType == DriveType.CDRom && drive.IsReady)\n                        .Select(drive => drive.RootDirectory.FullName),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"data\", \"jungle.tr2\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"main.sfx\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"audio\", \"cdaudio.wad\"))),\n                new InstallSourceOption(\n                    \"Legacy TR3X/TRX\",\n                    GetTRXDirectories(),\n                    _originalDir,\n                    sourceDirectory =>\n                        Directory.Exists(Path.Combine(sourceDirectory, \"cfg\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"jungle.tr2\"))),\n                new InstallSourceOption(\n                    \"TRX\",\n                    GetTRXDirectories(),\n                    _existingTRX,\n                    sourceDirectory => Directory.Exists(Path.Combine(sourceDirectory, \"games\", \"tr3\")))\n            ]);\n\n        yield return new InstallComponent(\n            \"tr3-la\",\n            \"TR3:LA\",\n            \"The Lost Artifact files from disc or an existing TRX installation.\",\n            downloadUrl: $\"{ResourceBaseUrl}/tr3-la.zip\",\n            sourceOptions:\n            [\n                new InstallSourceOption(\n                    \"Steam\",\n                    GetSteamDirectories(\"Tomb Raider (III)\", \"TombRaider (III)\"),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"tr3gold.exe\"))\n                        || File.Exists(Path.Combine(sourceDirectory, \"data\", \"trtla.dat\"))),\n                new InstallSourceOption(\n                    \"GOG\",\n                    GetGOGDirectories(\"Tomb Raider 3\"),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"tr3gold.exe\"))\n                        || File.Exists(Path.Combine(sourceDirectory, \"data\", \"trtla.dat\"))),\n                new InstallSourceOption(\n                    \"Disc\",\n                    DriveInfo.GetDrives()\n                        .Where(drive => drive.DriveType == DriveType.CDRom && drive.IsReady)\n                        .Select(drive => drive.RootDirectory.FullName),\n                    _originalDir,\n                    sourceDirectory =>\n                        File.Exists(Path.Combine(sourceDirectory, \"tr3gold.exe\"))\n                        || File.Exists(Path.Combine(sourceDirectory, \"data\", \"trtla.dat\"))),\n                new InstallSourceOption(\n                    \"Legacy TR3X/TRX\",\n                    GetTRXDirectories(),\n                    _originalDir,\n                    sourceDirectory =>\n                        Directory.Exists(Path.Combine(sourceDirectory, \"cfg\"))\n                        && File.Exists(Path.Combine(sourceDirectory, \"data\", \"chunnel.tr2\"))),\n                new InstallSourceOption(\n                    \"TRX\",\n                    GetTRXDirectories(),\n                    _existingTRX,\n                    sourceDirectory => Directory.Exists(Path.Combine(sourceDirectory, \"games\", \"tr3-la\"))),\n            ]);\n    }\n\n    public static string? GetStoredInstallPath()\n    {\n        using RegistryKey? key = Registry.CurrentUser.OpenSubKey(RegistryPath);\n        return key?.GetValue(\"InstallPath\")?.ToString();\n    }\n\n    public static void StoreInstallPath(string installPath)\n    {\n        using RegistryKey? key = Registry.CurrentUser.CreateSubKey(RegistryPath);\n        key?.SetValue(\"InstallPath\", installPath);\n    }\n\n    private static IEnumerable<string> GetSteamDirectories(params string[] gameDirectoryNames)\n    {\n        foreach (string gameDirectoryName in gameDirectoryNames)\n        {\n            yield return Path.Combine(@\"C:\\Program Files (x86)\\Steam\\steamapps\\common\", gameDirectoryName);\n        }\n\n        using RegistryKey? key = Registry.CurrentUser.OpenSubKey(@\"Software\\Valve\\Steam\");\n        string? steamPath = key?.GetValue(\"SteamPath\")?.ToString();\n        if (steamPath is not null)\n        {\n            foreach (string gameDirectoryName in gameDirectoryNames)\n            {\n                yield return Path.Combine(steamPath, @\"steamapps\\common\", gameDirectoryName);\n            }\n        }\n    }\n\n    private static IEnumerable<string> GetGOGDirectories(string gameDirectoryName)\n    {\n        yield return Path.Combine(@\"C:\\Program Files (x86)\\GOG Galaxy\\Games\", gameDirectoryName);\n        using RegistryKey? key = Registry.ClassesRoot.OpenSubKey(@\"goggalaxy\\shell\\open\\command\");\n        string? value = key?.GetValue(\"\")?.ToString();\n        if (value is null)\n        {\n            yield break;\n        }\n        Match match = new Regex(@\"\"\"(?<path>[^\"\"]+)\"\"\").Match(value);\n        if (!match.Success)\n        {\n            yield break;\n        }\n        string? gogPath = Path.GetDirectoryName(match.Groups[\"path\"].Value);\n        if (gogPath is not null)\n        {\n            yield return Path.Combine(gogPath, @\"Games\", gameDirectoryName);\n        }\n    }\n\n    private static IEnumerable<string> GetTRXDirectories()\n    {\n        string? path = GetStoredInstallPath();\n        if (path is not null)\n        {\n            yield return path;\n        }\n    }\n\n    private static IEnumerable<string> GetTombATIDirectories()\n    {\n        yield return @\"C:\\TOMBATI\";\n\n        foreach (string path in GetDesktopShortcutDirectories())\n        {\n            yield return path;\n        }\n\n        foreach (string path in GetSteamDirectories(\"Tomb Raider (I)\"))\n        {\n            yield return path;\n        }\n    }\n\n    private static IEnumerable<string> GetDesktopShortcutDirectories()\n    {\n        string desktopDir = Environment.GetFolderPath(\n            Environment.SpecialFolder.DesktopDirectory);\n        if (!Directory.Exists(desktopDir))\n        {\n            yield break;\n        }\n\n        foreach (string shortcutPath in Directory.EnumerateFiles(desktopDir, \"*.lnk\"))\n        {\n            string? targetPath = null;\n            try\n            {\n                Type? shellType = Type.GetTypeFromProgID(\"WScript.Shell\");\n                if (shellType == null)\n                {\n                    continue;\n                }\n\n                dynamic shell = Activator.CreateInstance(shellType)!;\n                dynamic shortcut = shell.CreateShortcut(shortcutPath);\n                targetPath = shortcut.TargetPath as string;\n            }\n            catch\n            {\n                continue;\n            }\n\n            if (string.IsNullOrWhiteSpace(targetPath))\n            {\n                continue;\n            }\n\n            string? dirName = Path.GetDirectoryName(targetPath);\n            if (!string.IsNullOrWhiteSpace(dirName))\n            {\n                yield return dirName;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/InstallFileHelper.cs",
    "content": "using System.IO;\n\nnamespace TRX_Installer;\n\ninternal static class InstallFileHelper\n{\n    public static async Task CopyMappedDirectoryAsync(\n        string sourceDirectory,\n        string targetDirectory,\n        Func<string, ISet<string>, string?> mapPath,\n        IInstallerProgress progress)\n    {\n        string[] files = Directory.GetFiles(sourceDirectory, \"*\", SearchOption.AllDirectories);\n        HashSet<string> relativePaths = files\n            .Select(sourcePath => InstallMappings.Normalize(Path.GetRelativePath(sourceDirectory, sourcePath)))\n            .ToHashSet(StringComparer.OrdinalIgnoreCase);\n        int copied = 0;\n        progress.SetSubDescription($\"Copying files from {Path.GetFileName(sourceDirectory)}...\");\n        progress.SetInnerProgress(0, Math.Max(1, files.Length));\n\n        foreach (string sourcePath in files)\n        {\n            string relPath = Path.GetRelativePath(sourceDirectory, sourcePath);\n            string? mappedPath = mapPath(relPath, relativePaths);\n            if (mappedPath is null)\n            {\n                copied++;\n                progress.SetInnerProgress(copied, Math.Max(1, files.Length));\n                continue;\n            }\n\n            string targetPath = Path.Combine(targetDirectory, mappedPath);\n            string fullSourcePath = Path.GetFullPath(sourcePath);\n            string fullTargetPath = Path.GetFullPath(targetPath);\n            if (string.Equals(fullSourcePath, fullTargetPath, StringComparison.OrdinalIgnoreCase))\n            {\n                copied++;\n                progress.SetInnerProgress(copied, Math.Max(1, files.Length));\n                continue;\n            }\n\n            Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);\n            try\n            {\n                await Task.Run(() => File.Copy(sourcePath, targetPath, true));\n                progress.AppendLog($\"Copying {mappedPath}\");\n            }\n            catch (IOException ex)\n            {\n                progress.AppendLog($\"Skipping {mappedPath}: {ex.Message}\");\n            }\n            copied++;\n            progress.SetInnerProgress(copied, Math.Max(1, files.Length));\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/InstallMappings.cs",
    "content": "using System.IO;\n\nnamespace TRX_Installer;\n\npublic static class InstallMappings\n{\n    private sealed record MappingRule(string SourcePattern, string TargetPattern);\n\n    private static readonly Dictionary<string, List<MappingRule>> Rules = new()\n    {\n        [\"tr1\"] =\n        [\n            new(@\"data/*.phd\", @\"games\\tr1\\levels\\*.phd\"),\n            new(@\"data/*.pcx\", @\"games\\tr1\\images\\*.pcx\"),\n            new(@\"fmv/*\", @\"games\\tr1\\fmv\\*\"),\n            new(@\"music/*\", @\"games\\tr1\\music\\*\"),\n        ],\n        [\"tr1-ub\"] =\n        [\n            new(@\"data/*.phd\", @\"games\\tr1-ub\\levels\\*.phd\"),\n            new(@\"data/*.pcx\", @\"games\\tr1-ub\\images\\*.pcx\"),\n        ],\n        [\"tr1-demo-pc\"] =\n        [\n            new(@\"games/tr1-demo-pc/*\", @\"games\\tr1-demo-pc\\*\"),\n        ],\n        [\"tr2\"] =\n        [\n            new(@\"data/main.sfx\", @\"games\\tr2\\main.sfx\"),\n            new(@\"data/*.tr2\", @\"games\\tr2\\levels\\*.tr2\"),\n            new(@\"data/*.pcx\", @\"games\\tr2\\images\\*.pcx\"),\n            new(@\"fmv/*\", @\"games\\tr2\\fmv\\*\"),\n            new(@\"music/*\", @\"games\\tr2\\music\\*\"),\n        ],\n        [\"tr2-gm\"] =\n        [\n            new(@\"data/main_gm.sfx\", @\"games\\tr2-gm\\main_gm.sfx\"),\n            new(@\"data/*.tr2\", @\"games\\tr2-gm\\levels\\*.tr2\"),\n            new(@\"data/*.pcx\", @\"games\\tr2-gm\\images\\*.pcx\"),\n        ],\n        [\"tr3\"] =\n        [\n            new(@\"data/main.sfx\", @\"games\\tr3\\main.sfx\"),\n            new(@\"data/antarc.tr2\", @\"games\\tr3\\levels\\antarc.tr2\"),\n            new(@\"data/area51.tr2\", @\"games\\tr3\\levels\\area51.tr2\"),\n            new(@\"data/chamber.tr2\", @\"games\\tr3\\levels\\chamber.tr2\"),\n            new(@\"data/city.tr2\", @\"games\\tr3\\levels\\city.tr2\"),\n            new(@\"data/compound.tr2\", @\"games\\tr3\\levels\\compound.tr2\"),\n            new(@\"data/crash.tr2\", @\"games\\tr3\\levels\\crash.tr2\"),\n            new(@\"data/house.tr2\", @\"games\\tr3\\levels\\house.tr2\"),\n            new(@\"data/jungle.tr2\", @\"games\\tr3\\levels\\jungle.tr2\"),\n            new(@\"data/mines.tr2\", @\"games\\tr3\\levels\\mines.tr2\"),\n            new(@\"data/nevada.tr2\", @\"games\\tr3\\levels\\nevada.tr2\"),\n            new(@\"data/office.tr2\", @\"games\\tr3\\levels\\office.tr2\"),\n            new(@\"data/quadchas.tr2\", @\"games\\tr3\\levels\\quadchas.tr2\"),\n            new(@\"data/rapids.tr2\", @\"games\\tr3\\levels\\rapids.tr2\"),\n            new(@\"data/roofs.tr2\", @\"games\\tr3\\levels\\roofs.tr2\"),\n            new(@\"data/sewer.tr2\", @\"games\\tr3\\levels\\sewer.tr2\"),\n            new(@\"data/shore.tr2\", @\"games\\tr3\\levels\\shore.tr2\"),\n            new(@\"data/stpaul.tr2\", @\"games\\tr3\\levels\\stpaul.tr2\"),\n            new(@\"data/temple.tr2\", @\"games\\tr3\\levels\\temple.tr2\"),\n            new(@\"data/title.tr2\", @\"games\\tr3\\levels\\title.tr2\"),\n            new(@\"data/tonyboss.tr2\", @\"games\\tr3\\levels\\tonyboss.tr2\"),\n            new(@\"data/tower.tr2\", @\"games\\tr3\\levels\\tower.tr2\"),\n            new(@\"data/triboss.tr2\", @\"games\\tr3\\levels\\triboss.tr2\"),\n            new(@\"fmv/*\", @\"games\\tr3\\fmv\\*\"),\n            new(@\"audio/*\", @\"games\\tr3\\audio\\*\"),\n            new(@\"cuts/*\", @\"games\\tr3\\cuts\\*\"),\n            new(@\"pix/*\", @\"games\\tr3\\images\\*\"),\n        ],\n        [\"tr3-la\"] =\n        [\n            new(@\"data/chunnel.tr2\", @\"games\\tr3-la\\levels\\chunnel.tr2\"),\n            new(@\"data/scotland.tr2\", @\"games\\tr3-la\\levels\\scotland.tr2\"),\n            new(@\"data/slinc.tr2\", @\"games\\tr3-la\\levels\\slinc.tr2\"),\n            new(@\"data/undersea.tr2\", @\"games\\tr3-la\\levels\\undersea.tr2\"),\n            new(@\"data/willsden.tr2\", @\"games\\tr3-la\\levels\\willsden.tr2\"),\n            new(@\"data/zoo.tr2\", @\"games\\tr3-la\\levels\\zoo.tr2\"),\n            new(@\"data/title.tr2\", @\"games\\tr3-la\\levels\\title.tr2\"),\n            new(@\"data/title_la.tr2\", @\"games\\tr3-la\\levels\\title.tr2\"),\n            new(@\"data/main.sfx\", @\"games\\tr3-la\\main.sfx\"),\n            new(@\"data/main_la.sfx\", @\"games\\tr3-la\\main.sfx\"),\n            new(@\"pix/*\", @\"games\\tr3-la\\images\\*\"),\n        ],\n    };\n\n    public static string Normalize(string relativePath)\n    {\n        return relativePath.Replace('/', Path.DirectorySeparatorChar)\n            .Replace('\\\\', Path.DirectorySeparatorChar)\n            .ToLowerInvariant();\n    }\n\n    private static bool TryMatchPattern(\n        string path,\n        string pattern,\n        out string wildcardValue)\n    {\n        wildcardValue = string.Empty;\n\n        int wildcardPos = pattern.IndexOf('*');\n        if (wildcardPos == -1)\n        {\n            return string.Equals(path, pattern, StringComparison.OrdinalIgnoreCase);\n        }\n\n        string prefix = pattern[..wildcardPos];\n        string suffix = pattern[(wildcardPos + 1)..];\n        if (!path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)\n            || !path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)\n            || path.Length < prefix.Length + suffix.Length)\n        {\n            return false;\n        }\n\n        wildcardValue = path[prefix.Length..(path.Length - suffix.Length)];\n        if (wildcardValue.Contains(Path.DirectorySeparatorChar))\n        {\n            wildcardValue = string.Empty;\n            return false;\n        }\n\n        return true;\n    }\n\n    private static string ApplyPattern(string pattern, string wildcardValue)\n    {\n        int wildcardPos = pattern.IndexOf('*');\n        if (wildcardPos == -1)\n        {\n            return pattern;\n        }\n\n        return pattern[..wildcardPos] + wildcardValue + pattern[(wildcardPos + 1)..];\n    }\n\n    private static bool IsShadowedTR3LAAlias(\n        string normalizedPath,\n        ISet<string> availablePaths)\n    {\n        if (normalizedPath == Normalize(@\"data/title.tr2\")\n            && availablePaths.Contains(Normalize(@\"data/title_la.tr2\")))\n        {\n            return true;\n        }\n\n        if (normalizedPath == Normalize(@\"data/main.sfx\")\n            && availablePaths.Contains(Normalize(@\"data/main_la.sfx\")))\n        {\n            return true;\n        }\n\n        return false;\n    }\n\n    public static string? MapOriginalFile(\n        string gameId,\n        string relativePath,\n        ISet<string> availablePaths)\n    {\n        string normalized = Normalize(relativePath);\n        if (gameId == \"tr3-la\" && IsShadowedTR3LAAlias(normalized, availablePaths))\n        {\n            return null;\n        }\n\n        if (!Rules.TryGetValue(gameId, out List<MappingRule>? rules))\n        {\n            return null;\n        }\n\n        foreach (MappingRule rule in rules)\n        {\n            if (TryMatchPattern(normalized, Normalize(rule.SourcePattern), out string wildcardValue))\n            {\n                return ApplyPattern(Normalize(rule.TargetPattern), wildcardValue);\n            }\n        }\n\n        return null;\n    }\n\n    public static string? MapCombinedFile(string gameId, string relativePath)\n    {\n        string normalized = Normalize(relativePath);\n        if (!Rules.TryGetValue(gameId, out List<MappingRule>? rules))\n        {\n            return null;\n        }\n\n        foreach (MappingRule rule in rules)\n        {\n            if (TryMatchPattern(normalized, Normalize(rule.TargetPattern), out _))\n            {\n                return normalized;\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/InstallSourceOption.cs",
    "content": "namespace TRX_Installer;\n\npublic class InstallSourceOption\n{\n    public InstallSourceOption(\n        string sourceName,\n        IEnumerable<string> directoriesToTry,\n        IInstallSource source,\n        Func<string, bool> isGameFound\n    )\n    {\n        SourceName = sourceName;\n        DirectoriesToTry = directoriesToTry;\n        Source = source;\n        _isGameFound = isGameFound;\n    }\n\n    public IEnumerable<string> DirectoriesToTry { get; }\n    public string SourceName { get; }\n    public IInstallSource Source { get; }\n\n    private readonly Func<string, bool> _isGameFound;\n\n    public bool IsGameFound(string sourceDirectory)\n        => _isGameFound(sourceDirectory);\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/InstallerService.cs",
    "content": "using System.Collections.ObjectModel;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Net.Http;\nusing System.Text.RegularExpressions;\n\nnamespace TRX_Installer;\n\ninternal class InstallerService\n{\n    private static readonly HttpClient HttpClient = new()\n    {\n        // Music packs can take longer than the framework's default 100-second timeout.\n        Timeout = TimeSpan.FromMinutes(30),\n    };\n\n    public async Task RunInstallAsync(\n        ObservableCollection<InstallComponent> allComponents,\n        string targetDirectory,\n        IInstallerProgress progress)\n    {\n        List<InstallComponent> selectedComponents = allComponents.Where(c => c.Install).ToList();\n        progress.SetOuterProgress(0, 2 + selectedComponents.Count);\n\n        progress.SetDescription(\"Extracting TRX...\");\n        progress.SetSubDescription(\"Preparing bundled files...\");\n        await ExtractEmbeddedReleaseAsync(targetDirectory, progress);\n        progress.SetOuterProgress(1, 2 + selectedComponents.Count);\n\n        int outerDone = 1;\n        foreach (InstallComponent component in selectedComponents)\n        {\n            progress.SetDescription($\"Installing {component.Title}...\");\n            progress.SetSubDescription(string.Empty);\n            progress.AppendLog($\"Installing {component.Title}\");\n\n            if (component.HasDownload)\n            {\n                if (component.DownloadUrl is not null)\n                {\n                    await DownloadAndExtractZipAsync(component.DownloadUrl, targetDirectory, progress);\n                }\n                if (component.SelectedDownloadOption is not null)\n                {\n                    await DownloadAndExtractZipAsync(component.SelectedDownloadOption.Url, targetDirectory, progress);\n                }\n            }\n\n            if (component.HasSource)\n            {\n                if (component.SourceDirectory is null || component.DetectedSource is null)\n                {\n                    throw new ApplicationException($\"No valid source configured for {component.Title}.\");\n                }\n                await component.DetectedSource.Source.InstallAsync(\n                    component.SourceDirectory, targetDirectory, component.GameId, progress);\n\n                foreach (OptionalDownload optional in component.OptionalDownloads.Where(d => d.IsEnabled))\n                {\n                    await DownloadAndExtractZipAsync(optional.Url, targetDirectory, progress);\n                }\n            }\n\n            outerDone++;\n            progress.SetOuterProgress(outerDone, 2 + selectedComponents.Count);\n        }\n\n        string gamesDir = Path.Combine(targetDirectory, \"games\");\n        if (Directory.Exists(gamesDir))\n        {\n            progress.SetDescription(\"Refreshing installed game list...\");\n            progress.SetSubDescription(\"Removing unchecked packs...\");\n            progress.SetInnerProgress(0, 1);\n            var checkedIds = new HashSet<string>(\n                allComponents.Where(c => c.Install).Select(c => c.GameId),\n                StringComparer.OrdinalIgnoreCase);\n            foreach (string dir in Directory.GetDirectories(gamesDir))\n            {\n                string name = Path.GetFileName(dir);\n                if (!checkedIds.Contains(name)\n                    && !name.EndsWith(\"-level\", StringComparison.OrdinalIgnoreCase))\n                {\n                    progress.AppendLog($\"Removing {name}\");\n                    await Task.Run(() => Directory.Delete(dir, recursive: true));\n                }\n            }\n        }\n        outerDone++;\n        progress.SetOuterProgress(outerDone, 2 + selectedComponents.Count);\n\n        InstallComponentFactory.StoreInstallPath(targetDirectory);\n        progress.SetDescription(\"Installation complete.\");\n        progress.SetSubDescription(\"All steps finished.\");\n        progress.AppendLog(\"Finished.\");\n    }\n\n    private static async Task DownloadAndExtractZipAsync(\n        string url,\n        string targetDirectory,\n        IInstallerProgress progress)\n    {\n        string fileName = Path.GetFileName(url);\n        progress.SetDescription($\"Downloading {fileName}...\");\n        progress.SetSubDescription(\"Starting download...\");\n        progress.AppendLog($\"Downloading {url}\");\n        using HttpResponseMessage response = await HttpClient.GetAsync(\n            url, HttpCompletionOption.ResponseHeadersRead);\n        response.EnsureSuccessStatusCode();\n\n        long? totalBytes = response.Content.Headers.ContentLength;\n        using Stream responseStream = await response.Content.ReadAsStreamAsync();\n        using MemoryStream stream = new();\n        await CopyToMemoryStreamAsync(responseStream, stream, totalBytes, progress);\n        stream.Position = 0;\n        await ExtractZipAsync(stream, targetDirectory, progress);\n    }\n\n    private static async Task CopyToMemoryStreamAsync(\n        Stream source,\n        MemoryStream target,\n        long? totalBytes,\n        IInstallerProgress progress)\n    {\n        byte[] buffer = new byte[81920];\n        long downloadedBytes = 0;\n\n        if (totalBytes.HasValue)\n        {\n            progress.SetInnerProgress(0, 100);\n        }\n        else\n        {\n            progress.SetInnerProgress(0, 1);\n        }\n\n        while (true)\n        {\n            int bytesRead = await source.ReadAsync(buffer);\n            if (bytesRead == 0)\n            {\n                break;\n            }\n\n            await target.WriteAsync(buffer.AsMemory(0, bytesRead));\n            downloadedBytes += bytesRead;\n\n            if (totalBytes.HasValue && totalBytes.Value > 0)\n            {\n                int percent = (int)(downloadedBytes * 100 / totalBytes.Value);\n                progress.SetSubDescription(\n                    $\"Downloaded {FormatByteSize(downloadedBytes)} / {FormatByteSize(totalBytes.Value)} ({percent}%)\");\n                progress.SetInnerProgress(percent, 100);\n            }\n            else\n            {\n                progress.SetSubDescription($\"Downloaded {FormatByteSize(downloadedBytes)}\");\n                progress.SetInnerProgress(0, 1);\n            }\n        }\n    }\n\n    private static string FormatByteSize(long byteCount)\n    {\n        string[] units = [\"B\", \"KB\", \"MB\", \"GB\"];\n        double size = byteCount;\n        int unitIndex = 0;\n\n        while (size >= 1024 && unitIndex < units.Length - 1)\n        {\n            size /= 1024;\n            unitIndex++;\n        }\n\n        return $\"{size:0.#} {units[unitIndex]}\";\n    }\n\n    private static async Task ExtractEmbeddedReleaseAsync(string targetDirectory, IInstallerProgress progress)\n    {\n        progress.SetSubDescription(\"Extracting bundled files...\");\n        progress.AppendLog(\"Opening embedded ZIP\");\n        using Stream stream = typeof(InstallerService).Assembly\n            .GetManifestResourceStream(\"TRX_Installer.Resources.release.zip\")\n            ?? throw new ApplicationException(\"Could not open embedded ZIP.\");\n        await ExtractZipAsync(stream, targetDirectory, progress);\n    }\n\n    private static async Task ExtractZipAsync(\n        Stream stream,\n        string targetDirectory,\n        IInstallerProgress progress)\n    {\n        using ZipArchive zip = new(stream);\n        List<ZipArchiveEntry> entries = zip.Entries\n            .Where(entry => !Regex.IsMatch(entry.FullName, @\"[\\\\/]$\"))\n            .ToList();\n        progress.SetInnerProgress(0, Math.Max(1, entries.Count));\n\n        int done = 0;\n        foreach (ZipArchiveEntry entry in entries)\n        {\n            string relPath = entry.FullName\n                .Replace('/', Path.DirectorySeparatorChar)\n                .Replace('\\\\', Path.DirectorySeparatorChar);\n            string targetPath = Path.Combine(targetDirectory, relPath);\n            Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);\n            try\n            {\n                await Task.Run(() => entry.ExtractToFile(targetPath, true));\n                progress.AppendLog($\"Extracting {relPath}\");\n            }\n            catch (IOException ex)\n            {\n                progress.AppendLog($\"Skipping {relPath}: {ex.Message}\");\n            }\n            done++;\n            progress.SetInnerProgress(done, Math.Max(1, entries.Count));\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/MainWindow.xaml",
    "content": "<Window x:Class=\"TRX_Installer.MainWindow\"\n        xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n        xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n        xmlns:local=\"clr-namespace:TRX_Installer\"\n        Title=\"TRX Installer\"\n        MinWidth=\"480\" MinHeight=\"380\"\n        Width=\"{Binding WindowWidth, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged, FallbackValue=700}\"\n        Height=\"520\"\n        WindowStartupLocation=\"CenterScreen\"\n        ResizeMode=\"CanResizeWithGrip\">\n\n    <Window.Resources>\n        <local:BoolToVisibilityConverter x:Key=\"BoolToVisible\" />\n        <local:BoolToVisibilityConverter x:Key=\"BoolToHidden\" TrueValue=\"Collapsed\" FalseValue=\"Visible\" />\n        <Style x:Key=\"heading\" TargetType=\"TextBlock\">\n            <Setter Property=\"FontSize\" Value=\"20\" />\n            <Setter Property=\"TextWrapping\" Value=\"Wrap\" />\n            <Setter Property=\"VerticalAlignment\" Value=\"Center\" />\n            <Setter Property=\"Margin\" Value=\"0,0,0,12\" />\n        </Style>\n        <Style TargetType=\"Button\">\n            <Setter Property=\"Padding\" Value=\"10,5\" />\n            <Setter Property=\"MinWidth\" Value=\"70\" />\n            <Setter Property=\"Margin\" Value=\"8,0,0,0\" />\n        </Style>\n        <Style TargetType=\"Image\">\n            <Setter Property=\"RenderOptions.BitmapScalingMode\" Value=\"HighQuality\" />\n        </Style>\n    </Window.Resources>\n\n    <Grid>\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"Auto\" />\n            <ColumnDefinition Width=\"*\" />\n        </Grid.ColumnDefinitions>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"*\" />\n            <RowDefinition Height=\"Auto\" />\n        </Grid.RowDefinitions>\n\n        <!-- Sidebar image -->\n        <Rectangle Grid.Row=\"0\" Grid.Column=\"0\" Width=\"160\"\n                   Visibility=\"{Binding IsSidebarVisible, Mode=OneWay, Converter={StaticResource BoolToVisible}}\">\n            <Rectangle.Fill>\n                <ImageBrush ImageSource=\"{Binding SidebarImage, Mode=OneWay}\"\n                            Stretch=\"UniformToFill\"\n                            AlignmentX=\"Center\" AlignmentY=\"Center\" />\n            </Rectangle.Fill>\n        </Rectangle>\n\n        <!-- Step content -->\n        <Grid Grid.Row=\"0\" Grid.Column=\"1\" Margin=\"16,16,16,0\">\n\n            <!-- Step 1: Setup -->\n            <Grid Visibility=\"{Binding IsSetupStep, Mode=OneWay, Converter={StaticResource BoolToVisible}}\">\n                <Grid.RowDefinitions>\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"*\" />\n                    <RowDefinition Height=\"Auto\" />\n                </Grid.RowDefinitions>\n\n                <TextBlock Style=\"{StaticResource heading}\">Step 1: Select components</TextBlock>\n\n                <ScrollViewer Grid.Row=\"1\" VerticalScrollBarVisibility=\"Auto\"\n                              HorizontalScrollBarVisibility=\"Disabled\">\n                    <ItemsControl ItemsSource=\"{Binding Components}\">\n                        <ItemsControl.ItemTemplate>\n                            <DataTemplate>\n                                <Border Margin=\"0,0,0,8\" Padding=\"10\"\n                                        BorderThickness=\"1\" BorderBrush=\"#22000000\">\n                                    <StackPanel>\n                                        <!-- Title + source status -->\n                                        <CheckBox IsChecked=\"{Binding Install}\" Margin=\"0,0,0,4\">\n                                            <TextBlock TextWrapping=\"Wrap\">\n                                                <Run FontWeight=\"SemiBold\" Text=\"{Binding Title, Mode=OneWay}\" />\n                                                <Run Text=\"  \" />\n                                                <Run Foreground=\"{Binding SourceStatusBrush, Mode=OneWay}\" Text=\"{Binding SourceStatus, Mode=OneWay}\" />\n                                            </TextBlock>\n                                        </CheckBox>\n\n                                        <!-- Description -->\n                                        <TextBlock Margin=\"22,0,0,4\" TextWrapping=\"Wrap\"\n                                                   FontSize=\"11\" Foreground=\"#555555\"\n                                                   Text=\"{Binding Description, Mode=OneWay}\" />\n\n                                        <!-- Variant radio buttons (e.g. TR1:UB fan-made vs vanilla) -->\n                                        <ListBox Margin=\"22,4,0,0\"\n                                                 Visibility=\"{Binding HasDownloadOptions, Mode=OneWay, Converter={StaticResource BoolToVisible}}\"\n                                                 ItemsSource=\"{Binding DownloadOptions}\"\n                                                 SelectedItem=\"{Binding SelectedDownloadOption}\"\n                                                 Background=\"Transparent\" BorderThickness=\"0\">\n                                            <ListBox.ItemContainerStyle>\n                                                <Style TargetType=\"ListBoxItem\">\n                                                    <Setter Property=\"Template\">\n                                                        <Setter.Value>\n                                                            <ControlTemplate TargetType=\"ListBoxItem\">\n                                                                <RadioButton Margin=\"0,2,0,0\"\n                                                                             Content=\"{Binding Title, Mode=OneWay}\"\n                                                                             IsChecked=\"{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}}\" />\n                                                            </ControlTemplate>\n                                                        </Setter.Value>\n                                                    </Setter>\n                                                </Style>\n                                            </ListBox.ItemContainerStyle>\n                                        </ListBox>\n\n                                        <!-- Source directory (source-based components only) -->\n                                        <Grid Margin=\"22,4,0,0\"\n                                              Visibility=\"{Binding HasSource, Mode=OneWay, Converter={StaticResource BoolToVisible}}\">\n                                            <Grid.ColumnDefinitions>\n                                                <ColumnDefinition Width=\"*\" />\n                                                <ColumnDefinition Width=\"Auto\" />\n                                            </Grid.ColumnDefinitions>\n                                            <TextBlock Grid.Column=\"0\" VerticalAlignment=\"Center\" FontSize=\"11\"\n                                                       Text=\"{Binding SourceDirectory}\"\n                                                       TextTrimming=\"CharacterEllipsis\" />\n                                            <Button Grid.Column=\"1\" Padding=\"6,2\" FontSize=\"11\"\n                                                    Click=\"ChooseSource_Click\" Tag=\"{Binding}\">Change...</Button>\n                                        </Grid>\n\n                                        <!-- Optional downloads (music checkboxes) -->\n                                        <ItemsControl Margin=\"22,4,0,0\"\n                                                      Visibility=\"{Binding HasOptionalDownloads, Mode=OneWay, Converter={StaticResource BoolToVisible}}\"\n                                                      ItemsSource=\"{Binding OptionalDownloads}\">\n                                            <ItemsControl.ItemTemplate>\n                                                <DataTemplate>\n                                                    <CheckBox Margin=\"0,2,0,0\" FontSize=\"11\"\n                                                              IsChecked=\"{Binding IsEnabled}\"\n                                                              Content=\"{Binding Title, Mode=OneWay}\" />\n                                                </DataTemplate>\n                                            </ItemsControl.ItemTemplate>\n                                        </ItemsControl>\n                                    </StackPanel>\n                                </Border>\n                            </DataTemplate>\n                        </ItemsControl.ItemTemplate>\n                    </ItemsControl>\n                </ScrollViewer>\n\n                <!-- Destination folder -->\n                <Grid Grid.Row=\"2\" Margin=\"0,8,0,12\">\n                    <Grid.ColumnDefinitions>\n                        <ColumnDefinition Width=\"Auto\" />\n                        <ColumnDefinition Width=\"*\" />\n                        <ColumnDefinition Width=\"Auto\" />\n                    </Grid.ColumnDefinitions>\n                    <TextBlock Grid.Column=\"0\" VerticalAlignment=\"Center\" Margin=\"0,0,8,0\">Destination folder:</TextBlock>\n                    <TextBlock Grid.Column=\"1\" VerticalAlignment=\"Center\"\n                               Text=\"{Binding TargetDirectory}\" TextTrimming=\"CharacterEllipsis\" />\n                    <Button Grid.Column=\"2\" Padding=\"8,3\" Click=\"ChooseTarget_Click\">Change...</Button>\n                </Grid>\n            </Grid>\n\n            <!-- Step 2: Installing -->\n            <Grid Visibility=\"{Binding IsInstallingStep, Mode=OneWay, Converter={StaticResource BoolToVisible}}\">\n                <Grid.RowDefinitions>\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"*\" />\n                </Grid.RowDefinitions>\n\n                <TextBlock Style=\"{StaticResource heading}\">Step 2: Installing</TextBlock>\n                <TextBlock Grid.Row=\"1\" Margin=\"0,0,0,8\"\n                           Text=\"{Binding InstallDescription, Mode=OneWay}\" />\n                <ProgressBar Grid.Row=\"2\" Margin=\"0,0,0,12\" Height=\"18\"\n                             Minimum=\"0\"\n                             Maximum=\"{Binding OuterMaximumProgress}\"\n                             Value=\"{Binding OuterCurrentProgress}\" />\n                <TextBlock Grid.Row=\"3\" Margin=\"0,0,0,8\" FontSize=\"11\" Foreground=\"#555555\"\n                           Text=\"{Binding InstallSubDescription, Mode=OneWay}\" />\n                <ProgressBar Grid.Row=\"4\" Margin=\"0,0,0,12\" Height=\"14\"\n                             Minimum=\"0\"\n                             Maximum=\"{Binding InnerMaximumProgress}\"\n                             Value=\"{Binding InnerCurrentProgress}\" />\n                <TextBox x:Name=\"_logTextBox\" Grid.Row=\"5\" IsReadOnly=\"True\" TextWrapping=\"Wrap\" AcceptsReturn=\"True\"\n                         VerticalScrollBarVisibility=\"Auto\"\n                         Text=\"{Binding LogText, Mode=OneWay}\" />\n            </Grid>\n\n            <!-- Step 3: Finish -->\n            <StackPanel Visibility=\"{Binding IsFinishStep, Mode=OneWay, Converter={StaticResource BoolToVisible}}\">\n                <TextBlock Style=\"{StaticResource heading}\">Step 3: Done!</TextBlock>\n                <TextBlock TextWrapping=\"Wrap\" Margin=\"0,0,0,24\">Installation complete. To configure advanced features, edit the JSON files in the cfg/ directory with a text editor.\n\nHappy raiding :)</TextBlock>\n                <CheckBox IsChecked=\"{Binding OpenFolder}\" Margin=\"0,0,0,12\">Open installation folder after closing</CheckBox>\n                <CheckBox IsChecked=\"{Binding CreateShortcut}\" Margin=\"0,0,0,12\">Create desktop shortcut</CheckBox>\n            </StackPanel>\n        </Grid>\n\n        <!-- Navigation bar -->\n        <Grid Grid.Row=\"1\" Grid.Column=\"0\" Grid.ColumnSpan=\"2\"\n              Background=\"{DynamicResource {x:Static SystemColors.ControlBrushKey}}\">\n            <StackPanel Margin=\"12\" HorizontalAlignment=\"Right\" Orientation=\"Horizontal\"\n                        Grid.IsSharedSizeScope=\"True\">\n                <Button Click=\"Next_Click\"\n                        IsEnabled=\"{Binding CanGoNext, Mode=OneWay}\"\n                        Visibility=\"{Binding IsFinishStep, Mode=OneWay, Converter={StaticResource BoolToHidden}}\"\n                        Content=\"{Binding NextButtonText, Mode=OneWay}\" />\n                <Button Click=\"Cancel_Click\"\n                        Content=\"{Binding CancelButtonText, Mode=OneWay}\" />\n            </StackPanel>\n        </Grid>\n    </Grid>\n</Window>\n"
  },
  {
    "path": "tools/installer/TRX_Installer/MainWindow.xaml.cs",
    "content": "using Microsoft.Win32;\nusing System.Collections.ObjectModel;\nusing System.ComponentModel;\nusing System.IO;\nusing System.Runtime.CompilerServices;\nusing System.Windows;\nusing System.Windows.Forms;\n\nnamespace TRX_Installer;\n\npublic enum InstallerStep\n{\n    Setup,\n    Installing,\n    Finish,\n}\n\npublic partial class MainWindow : Window, INotifyPropertyChanged, IInstallerProgress\n{\n    private readonly InstallerService _installerService = new();\n\n    public MainWindow()\n    {\n        InitializeComponent();\n        DataContext = this;\n        Components = new ObservableCollection<InstallComponent>(InstallComponentFactory.CreateComponents());\n        foreach (InstallComponent component in Components)\n        {\n            component.PropertyChanged += (_, _) => OnPropertyChanged(nameof(CanGoNext));\n        }\n        TargetDirectory = InstallComponentFactory.GetStoredInstallPath()\n            ?? Path.Combine(\n                Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),\n                \"TRX\");\n        RefreshComponentInstallStates();\n    }\n\n    public event PropertyChangedEventHandler? PropertyChanged;\n\n    // ── Components ─────────────────────────────────────────────────────────────\n\n    public ObservableCollection<InstallComponent> Components { get; }\n\n    // ── Step navigation ─────────────────────────────────────────────────────────\n\n    public InstallerStep CurrentStep\n    {\n        get => _currentStep;\n        private set\n        {\n            if (value == _currentStep)\n            {\n                return;\n            }\n            _currentStep = value;\n            OnPropertyChanged();\n            OnPropertyChanged(nameof(IsSetupStep));\n            OnPropertyChanged(nameof(IsInstallingStep));\n            OnPropertyChanged(nameof(IsFinishStep));\n            OnPropertyChanged(nameof(SidebarImage));\n            OnPropertyChanged(nameof(CanGoNext));\n            OnPropertyChanged(nameof(NextButtonText));\n            OnPropertyChanged(nameof(CancelButtonText));\n        }\n    }\n\n    public bool IsSetupStep => CurrentStep == InstallerStep.Setup;\n    public bool IsInstallingStep => CurrentStep == InstallerStep.Installing;\n    public bool IsFinishStep => CurrentStep == InstallerStep.Finish;\n\n    public string SidebarImage => CurrentStep switch\n    {\n        InstallerStep.Installing => \"/TRX_Installer;component/Resources/side2.jpg\",\n        InstallerStep.Finish => \"/TRX_Installer;component/Resources/side3.jpg\",\n        _ => \"/TRX_Installer;component/Resources/side1.jpg\",\n    };\n\n    public bool IsSidebarVisible => WindowWidth >= 500;\n\n    public int WindowWidth\n    {\n        get => _windowWidth;\n        set\n        {\n            if (value == _windowWidth)\n            {\n                return;\n            }\n            _windowWidth = value;\n            OnPropertyChanged(nameof(IsSidebarVisible));\n        }\n    }\n\n    public bool CanGoNext => CurrentStep switch\n    {\n        InstallerStep.Setup =>\n            !string.IsNullOrWhiteSpace(TargetDirectory)\n            && Components.Any(c => c.Install)\n            && Components.Where(c => c.Install).All(c => c.IsReadyToInstall),\n        InstallerStep.Installing => _installFinished,\n        _ => false,\n    };\n\n    public string NextButtonText => CurrentStep switch\n    {\n        InstallerStep.Setup => \"_Install\",\n        _ => \"_Next >\",\n    };\n\n    public string CancelButtonText => IsFinishStep ? \"_Close\" : \"_Cancel\";\n\n    // ── Finish step ─────────────────────────────────────────────────────────────\n\n    public bool OpenFolder\n    {\n        get => _openFolder;\n        set\n        {\n            if (value == _openFolder)\n            {\n                return;\n            }\n            _openFolder = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public bool CreateShortcut\n    {\n        get => _createShortcut;\n        set\n        {\n            if (value == _createShortcut)\n            {\n                return;\n            }\n            _createShortcut = value;\n            OnPropertyChanged();\n        }\n    }\n\n    // ── Install progress ────────────────────────────────────────────────────────\n\n    public string InstallDescription\n    {\n        get => _installDescription;\n        set\n        {\n            if (value == _installDescription)\n            {\n                return;\n            }\n            _installDescription = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public string InstallSubDescription\n    {\n        get => _installSubDescription;\n        set\n        {\n            if (value == _installSubDescription)\n            {\n                return;\n            }\n            _installSubDescription = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public int OuterCurrentProgress\n    {\n        get => _outerCurrentProgress;\n        set\n        {\n            if (value == _outerCurrentProgress)\n            {\n                return;\n            }\n            _outerCurrentProgress = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public int OuterMaximumProgress\n    {\n        get => _outerMaximumProgress;\n        set\n        {\n            if (value == _outerMaximumProgress)\n            {\n                return;\n            }\n            _outerMaximumProgress = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public int InnerCurrentProgress\n    {\n        get => _innerCurrentProgress;\n        set\n        {\n            if (value == _innerCurrentProgress)\n            {\n                return;\n            }\n            _innerCurrentProgress = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public int InnerMaximumProgress\n    {\n        get => _innerMaximumProgress;\n        set\n        {\n            if (value == _innerMaximumProgress)\n            {\n                return;\n            }\n            _innerMaximumProgress = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public string LogText\n    {\n        get => _logText;\n        set\n        {\n            if (value == _logText)\n            {\n                return;\n            }\n            _logText = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public string? TargetDirectory\n    {\n        get => _targetDirectory;\n        set\n        {\n            if (value == _targetDirectory)\n            {\n                return;\n            }\n            _targetDirectory = value;\n            OnPropertyChanged();\n            OnPropertyChanged(nameof(CanGoNext));\n            RefreshComponentInstallStates();\n        }\n    }\n\n    // ── IInstallerProgress ──────────────────────────────────────────────────────\n\n    void IInstallerProgress.SetDescription(string description)\n    {\n        InstallDescription = description;\n    }\n\n    void IInstallerProgress.SetSubDescription(string subDescription)\n    {\n        InstallSubDescription = subDescription;\n    }\n\n    void IInstallerProgress.SetOuterProgress(int current, int maximum)\n    {\n        OuterMaximumProgress = maximum;\n        OuterCurrentProgress = current;\n    }\n\n    void IInstallerProgress.SetInnerProgress(int current, int maximum)\n    {\n        InnerMaximumProgress = maximum;\n        InnerCurrentProgress = current;\n    }\n\n    void IInstallerProgress.AppendLog(string message)\n    {\n        LogText = string.IsNullOrEmpty(LogText)\n            ? message\n            : $\"{LogText}{Environment.NewLine}{message}\";\n        _logTextBox.ScrollToEnd();\n    }\n\n    // ── Backing fields ──────────────────────────────────────────────────────────\n\n    private InstallerStep _currentStep = InstallerStep.Setup;\n    private bool _installFinished;\n    private bool _openFolder = true;\n    private bool _createShortcut;\n    private string _installDescription = string.Empty;\n    private string _installSubDescription = string.Empty;\n    private int _outerCurrentProgress;\n    private int _outerMaximumProgress = 1;\n    private int _innerCurrentProgress;\n    private int _innerMaximumProgress = 1;\n    private string _logText = string.Empty;\n    private string? _targetDirectory;\n    private int _windowWidth;\n\n    // ── Navigation button handlers ──────────────────────────────────────────────\n\n    private async void Next_Click(object sender, RoutedEventArgs e)\n    {\n        if (CurrentStep == InstallerStep.Setup)\n        {\n            await RunInstallAsync();\n        }\n        else if (CurrentStep == InstallerStep.Installing)\n        {\n            CurrentStep = InstallerStep.Finish;\n        }\n    }\n\n    private void Cancel_Click(object sender, RoutedEventArgs e)\n    {\n        if (CurrentStep == InstallerStep.Finish)\n        {\n            ExecuteFinishActions();\n        }\n        Close();\n    }\n\n    private void ExecuteFinishActions()\n    {\n        if (TargetDirectory is null)\n        {\n            return;\n        }\n\n        if (CreateShortcut)\n        {\n            TryCreateDesktopShortcut(Path.Combine(TargetDirectory, \"TRX.exe\"));\n        }\n\n        if (OpenFolder)\n        {\n            System.Diagnostics.Process.Start(\"explorer.exe\", TargetDirectory);\n        }\n    }\n\n    private static void TryCreateDesktopShortcut(string targetExePath)\n    {\n        try\n        {\n            string shortcutPath = Path.Combine(\n                Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),\n                \"TRX.lnk\");\n            Type? shellType = Type.GetTypeFromProgID(\"WScript.Shell\");\n            if (shellType is null)\n            {\n                return;\n            }\n            dynamic shell = Activator.CreateInstance(shellType)!;\n            dynamic shortcut = shell.CreateShortcut(shortcutPath);\n            shortcut.TargetPath = targetExePath;\n            shortcut.WorkingDirectory = Path.GetDirectoryName(targetExePath);\n            shortcut.Save();\n        }\n        catch\n        {\n            // Shortcut creation is best-effort.\n        }\n    }\n\n    // ── Source/target folder choosers ───────────────────────────────────────────\n\n    private void ChooseSource_Click(object sender, RoutedEventArgs e)\n    {\n        if (sender is not System.Windows.Controls.Button button\n            || button.Tag is not InstallComponent component)\n        {\n            return;\n        }\n        string? result = BrowseFolder(component.SourceDirectory);\n        if (result is not null)\n        {\n            component.SetManualSourceDirectory(result);\n        }\n    }\n\n    private void ChooseTarget_Click(object sender, RoutedEventArgs e)\n    {\n        string? result = BrowseFolder(TargetDirectory);\n        if (result is not null)\n        {\n            TargetDirectory = result;\n        }\n    }\n\n    private void RefreshComponentInstallStates()\n    {\n        if (TargetDirectory is null)\n        {\n            return;\n        }\n        foreach (InstallComponent component in Components)\n        {\n            string gameDir = Path.Combine(TargetDirectory, \"games\", component.GameId);\n            if (Directory.Exists(gameDir))\n            {\n                component.Install = true;\n            }\n        }\n    }\n\n    private static string? BrowseFolder(string? initialDirectory)\n    {\n        using FolderBrowserDialog dialog = new()\n        {\n            Description = \"Choose directory\",\n            SelectedPath = initialDirectory ?? string.Empty,\n            ShowNewFolderButton = true,\n        };\n        return dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK\n            ? dialog.SelectedPath\n            : null;\n    }\n\n    // ── Install orchestration ───────────────────────────────────────────────────\n\n    private async Task RunInstallAsync()\n    {\n        if (TargetDirectory is null)\n        {\n            return;\n        }\n\n        CurrentStep = InstallerStep.Installing;\n        _installFinished = false;\n        LogText = string.Empty;\n        InstallSubDescription = string.Empty;\n        OuterCurrentProgress = 0;\n        OuterMaximumProgress = 1;\n        InnerCurrentProgress = 0;\n        InnerMaximumProgress = 1;\n\n        try\n        {\n            await _installerService.RunInstallAsync(Components, TargetDirectory, this);\n        }\n        catch (Exception ex)\n        {\n            InstallDescription = \"Installation failed.\";\n            InstallSubDescription = \"See the log for details.\";\n            ((IInstallerProgress)this).AppendLog(ex.ToString());\n        }\n        finally\n        {\n            _installFinished = true;\n            OnPropertyChanged(nameof(CanGoNext));\n        }\n    }\n\n    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/OptionalDownload.cs",
    "content": "using System.ComponentModel;\nusing System.Runtime.CompilerServices;\n\nnamespace TRX_Installer;\n\npublic class OptionalDownload : INotifyPropertyChanged\n{\n    public OptionalDownload(string title, string url, bool isEnabled = true)\n    {\n        Title = title;\n        Url = url;\n        _isEnabled = isEnabled;\n    }\n\n    public event PropertyChangedEventHandler? PropertyChanged;\n\n    public string Title { get; }\n    public string Url { get; }\n\n    public bool IsEnabled\n    {\n        get => _isEnabled;\n        set\n        {\n            if (value == _isEnabled)\n            {\n                return;\n            }\n            _isEnabled = value;\n            OnPropertyChanged();\n        }\n    }\n\n    private bool _isEnabled;\n\n    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)\n        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/OriginalDirectoryInstallSource.cs",
    "content": "namespace TRX_Installer;\n\ninternal class OriginalDirectoryInstallSource : IInstallSource\n{\n    public async Task InstallAsync(\n        string sourceDirectory,\n        string targetDirectory,\n        string gameId,\n        IInstallerProgress progress)\n    {\n        await InstallFileHelper.CopyMappedDirectoryAsync(\n            sourceDirectory,\n            targetDirectory,\n            (relPath, availablePaths) => InstallMappings.MapOriginalFile(gameId, relPath, availablePaths),\n            progress);\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/Resources/Lang/en.json",
    "content": "{\n  \"Controls\": {\n    \"window_title_main\": \"TRX Installer\",\n    \"step_source_heading\": \"Step 1: Installation options\",\n    \"step_source_content\": \"\",\n    \"step_settings_heading\": \"Step 1: Installation options\",\n    \"step_install_heading\": \"Step 2: Installing\",\n    \"step_finish_heading\": \"Step 3: Done\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/Resources/Lang/it.json",
    "content": "{\n  \"Controls\": {\n    \"window_title_main\": \"Programma di installazione di TRX\",\n    \"step_source_heading\": \"Fase 1: Opzioni d'installazione\",\n    \"step_source_content\": \"\",\n    \"step_settings_heading\": \"Fase 1: Opzioni d'installazione\",\n    \"step_install_heading\": \"Fase 2: Installazione\",\n    \"step_finish_heading\": \"Fase 3: Completato\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/Resources/const.json",
    "content": "{\n  \"Game\": \"TRX\",\n  \"ShortcutTitle\": \"Tomb Raider: Community Edition\"\n}\n"
  },
  {
    "path": "tools/installer/TRX_Installer/TRX_Installer.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net8.0-windows</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <UseWPF>true</UseWPF>\n    <UseWindowsForms>true</UseWindowsForms>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <GenerateAssemblyInfo>true</GenerateAssemblyInfo>\n    <AssemblyName>TRX_Installer</AssemblyName>\n    <ProduceReferenceAssembly>True</ProduceReferenceAssembly>\n    <EnableWindowsTargeting>true</EnableWindowsTargeting>\n    <PublishSingleFile>true</PublishSingleFile>\n    <PublishTrimmed>false</PublishTrimmed>\n    <PublishReadyToRun>true</PublishReadyToRun>\n    <EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>\n    <SelfContained>false</SelfContained>\n    <RuntimeIdentifier>win-x64</RuntimeIdentifier>\n    <ApplicationIcon>Resources\\icon.ico</ApplicationIcon>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"DiscUtils.Iso9660\" Version=\"0.16.13\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <EmbeddedResource Include=\"Resources\\const.json\" />\n    <EmbeddedResource Include=\"Resources\\Lang\\en.json\" />\n    <EmbeddedResource Include=\"Resources\\Lang\\it.json\" />\n    <EmbeddedResource Include=\"Resources\\release.zip\" Condition=\"Exists('Resources\\release.zip')\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Resource Include=\"Resources\\icon.ico\" />\n    <Resource Include=\"Resources\\side1.jpg\" />\n    <Resource Include=\"Resources\\side2.jpg\" />\n    <Resource Include=\"Resources\\side3.jpg\" />\n    <Resource Include=\"Resources\\side4.jpg\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "tools/installer/TRX_Installer.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.11.35219.272\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"TRX_InstallerLib\", \"TRX_InstallerLib\\TRX_InstallerLib.csproj\", \"{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"TRX_Installer\", \"TRX_Installer\\TRX_Installer.csproj\", \"{145D34D9-4D6B-4AB8-B04C-57F2E68E1A01}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"TR1X_Installer\", \"TR1X_Installer\\TR1X_Installer.csproj\", \"{5B32640D-3997-472F-A1BA-FCE4128E0688}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"TR2X_Installer\", \"TR2X_Installer\\TR2X_Installer.csproj\", \"{DCCEAD2D-BC68-40D7-B1B9-981450416466}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{145D34D9-4D6B-4AB8-B04C-57F2E68E1A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{145D34D9-4D6B-4AB8-B04C-57F2E68E1A01}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{145D34D9-4D6B-4AB8-B04C-57F2E68E1A01}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{145D34D9-4D6B-4AB8-B04C-57F2E68E1A01}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{5B32640D-3997-472F-A1BA-FCE4128E0688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5B32640D-3997-472F-A1BA-FCE4128E0688}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5B32640D-3997-472F-A1BA-FCE4128E0688}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5B32640D-3997-472F-A1BA-FCE4128E0688}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {BA21B1D5-1CC7-4ED8-8C79-A1A5B0ACC840}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/FinishStepControl.xaml",
    "content": "﻿<UserControl\n    x:Class=\"TRX_InstallerLib.Controls.FinishStepControl\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:models=\"clr-namespace:TRX_InstallerLib.Models\"\n    d:DataContext=\"{d:DesignInstance Type=models:FinishStep}\"\n    mc:Ignorable=\"d\"\n    d:DesignHeight=\"450\" d:DesignWidth=\"800\">\n\n    <UserControl.Resources>\n        <ResourceDictionary Source=\"/TRX_InstallerLib;component/Resources/styles.xaml\" />\n    </UserControl.Resources>\n\n    <StackPanel Orientation=\"Vertical\">\n        <TextBlock\n            Style=\"{StaticResource heading}\"\n            Text=\"{Binding ViewText[step_finish_heading]}\"/>\n\n        <TextBlock\n            VerticalAlignment=\"Center\"\n            Margin=\"0,0,0,24\"\n            TextWrapping=\"Wrap\"\n            Text=\"{Binding ViewText[step_finish_content]}\"/>\n\n        <CheckBox IsChecked=\"{Binding FinishSettings.OpenGameDirectory}\" Content=\"{Binding ViewText[step_finish_open_directory]}\" Margin=\"0,0,0,12\" />\n        <CheckBox IsChecked=\"{Binding FinishSettings.LaunchGame}\" Content=\"{Binding ViewText[step_finish_open_game]}\" Margin=\"0,0,0,12\" />\n    </StackPanel>\n</UserControl>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/FinishStepControl.xaml.cs",
    "content": "﻿using WC = System.Windows.Controls;\n\nnamespace TRX_InstallerLib.Controls;\n\npublic partial class FinishStepControl : WC.UserControl\n{\n    public FinishStepControl()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/InstallSettingsStepControl.xaml",
    "content": "﻿<UserControl\n    x:Class=\"TRX_InstallerLib.Controls.InstallSettingsStepControl\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:models=\"clr-namespace:TRX_InstallerLib.Models\"\n    xmlns:utils=\"clr-namespace:TRX_InstallerLib.Utils\"\n    d:DataContext=\"{d:DesignInstance Type=models:InstallSettingsStep}\"\n    mc:Ignorable=\"d\"\n    d:DesignHeight=\"450\" d:DesignWidth=\"800\">\n\n    <UserControl.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"/TRX_InstallerLib;component/Resources/styles.xaml\" />\n                <ResourceDictionary>\n                    <utils:BoolToVisibilityConverter\n                        x:Key=\"BoolToVisibleConverter\"\n                        FalseValue=\"Collapsed\"\n                        TrueValue=\"Visible\" />\n                </ResourceDictionary>\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </UserControl.Resources>\n\n    <Grid>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n\n        <TextBlock\n            Style=\"{StaticResource heading}\"\n            Text=\"{Binding ViewText[step_settings_heading]}\"/>\n\n        <ScrollViewer Grid.Row=\"1\" ScrollViewer.VerticalScrollBarVisibility=\"Auto\">\n            <ScrollViewer.Template>\n                <ControlTemplate TargetType=\"{x:Type ScrollViewer}\">\n                    <Grid x:Name=\"Grid\" Background=\"{TemplateBinding Background}\">\n                        <Grid.ColumnDefinitions>\n                            <ColumnDefinition Width=\"*\" />\n                            <ColumnDefinition Width=\"Auto\" />\n                        </Grid.ColumnDefinitions>\n                        <Grid.RowDefinitions>\n                            <RowDefinition Height=\"*\" />\n                            <RowDefinition Height=\"Auto\" />\n                        </Grid.RowDefinitions>\n                        <ScrollContentPresenter\n                            Grid.Row=\"0\"\n                            Grid.Column=\"0\"\n                            x:Name=\"PART_ScrollContentPresenter\"\n                            CanContentScroll=\"{TemplateBinding CanContentScroll}\"\n                            CanHorizontallyScroll=\"False\"\n                            CanVerticallyScroll=\"False\"\n                            ContentTemplate=\"{TemplateBinding ContentTemplate}\"\n                            Content=\"{TemplateBinding Content}\"\n                            Margin=\"{TemplateBinding Padding}\" />\n                        <Rectangle\n                            Grid.Row=\"1\"\n                            Grid.Column=\"1\"\n                            x:Name=\"Corner\"\n                            Fill=\"{DynamicResource {x:Static SystemColors.ControlBrushKey}}\" />\n                        <ScrollBar\n                            Grid.Row=\"0\"\n                            Grid.Column=\"1\"\n                            x:Name=\"PART_VerticalScrollBar\"\n                            AutomationProperties.AutomationId=\"VerticalScrollBar\"\n                            Cursor=\"Arrow\"\n                            Minimum=\"0\"\n                            Maximum=\"{TemplateBinding ScrollableHeight}\"\n                            Value=\"{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}\"\n                            Visibility=\"{TemplateBinding ComputedVerticalScrollBarVisibility}\"\n                            ViewportSize=\"{TemplateBinding ViewportHeight}\"\n                            Margin=\"12,0,0,0\" />\n                        <ScrollBar\n                            Grid.Row=\"1\"\n                            Grid.Column=\"0\"\n                            x:Name=\"PART_HorizontalScrollBar\"\n                            AutomationProperties.AutomationId=\"HorizontalScrollBar\"\n                            Orientation=\"Horizontal\"\n                            Cursor=\"Arrow\"\n                            Minimum=\"0\"\n                            Maximum=\"{TemplateBinding ScrollableWidth}\"\n                            Value=\"{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}\"\n                            Visibility=\"{TemplateBinding ComputedHorizontalScrollBarVisibility}\"\n                            ViewportSize=\"{TemplateBinding ViewportWidth}\" />\n                    </Grid>\n                </ControlTemplate>\n            </ScrollViewer.Template>\n\n            <StackPanel Orientation=\"Vertical\">\n                <CheckBox VerticalAlignment=\"Center\" Margin=\"0,0,0,12\" IsChecked=\"{Binding InstallSettings.DownloadMusic}\" IsEnabled=\"{Binding InstallSettings.IsDownloadingMusicNeeded}\">\n                    <TextBlock TextWrapping=\"Wrap\">\n                        <Run Text=\"{Binding ViewText[step_settings_music_heading]}\"/>\n                        <Run Foreground=\"ForestGreen\" Text=\"{Binding InstallSettings.IsDownloadingMusicNeeded, Converter={utils:ConditionalViewTextConverter TrueValue='', FalseValue='label_already_found'}, Mode=OneWay}\" />\n                        <LineBreak />\n                        <Run Style=\"{StaticResource small}\" Text=\"{Binding ViewText[step_settings_music_content]}\"/>\n                    </TextBlock>\n                </CheckBox>\n\n                <CheckBox VerticalAlignment=\"Center\" Margin=\"0,0,0,6\" IsChecked=\"{Binding InstallSettings.DownloadExpansionPack}\" IsEnabled=\"{Binding InstallSettings.IsDownloadingExpansionNeeded}\">\n                    <StackPanel Orientation=\"Vertical\">\n                        <StackPanel.Resources>\n                            <utils:ComparisonConverter x:Key=\"ComparisonConverter\" />\n                        </StackPanel.Resources>\n                        <TextBlock TextWrapping=\"Wrap\" Margin=\"0,0,0,6\">\n                            <Run Text=\"{Binding ViewText[step_settings_expansion_heading]}\"/>\n                            <Run Foreground=\"ForestGreen\" Text=\"{Binding InstallSettings.IsDownloadingExpansionNeeded, Converter={utils:ConditionalViewTextConverter TrueValue='', FalseValue='label_already_found'}, Mode=OneWay}\" />\n                            <LineBreak />\n                            <Run Style=\"{StaticResource small}\" Text=\"{Binding ViewText[step_settings_expansion_content]}\"/>\n                        </TextBlock>\n                        <StackPanel Orientation=\"Vertical\" Visibility=\"{Binding InstallSettings.AllowExpansionTypeSelection, Converter={StaticResource BoolToVisibleConverter}}\">\n                            <RadioButton IsEnabled=\"{Binding InstallSettings.DownloadExpansionPack}\" Style=\"{StaticResource small}\" Margin=\"0,0,0,6\" Content=\"{Binding ViewText[step_settings_expansion_music]}\"\n                                         IsChecked=\"{Binding Path=InstallSettings.ExpansionPackType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static models:ExpansionPackType.Music}}\"/>\n                            <RadioButton IsEnabled=\"{Binding InstallSettings.DownloadExpansionPack}\" Style=\"{StaticResource small}\" Margin=\"0,0,0,6\" Content=\"{Binding ViewText[step_settings_expansion_vanilla]}\"\n                                         IsChecked=\"{Binding Path=InstallSettings.ExpansionPackType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static models:ExpansionPackType.Vanilla}}\"/>\n                        </StackPanel>\n                    </StackPanel>\n                </CheckBox>\n\n                <CheckBox VerticalAlignment=\"Center\" Margin=\"0,0,0,12\" IsChecked=\"{Binding InstallSettings.ImportSaves}\" IsEnabled=\"{Binding InstallSettings.InstallSource.IsImportingSavesSupported}\">\n                    <TextBlock TextWrapping=\"Wrap\">\n                        <Run Text=\"{Binding ViewText[step_settings_saves_header]}\"/>\n                        <LineBreak />\n                        <Run Style=\"{StaticResource small}\" Text=\"{Binding ViewText[step_settings_saves_content]}\"/>\n                    </TextBlock>\n                </CheckBox>\n\n                <CheckBox\n                    VerticalAlignment=\"Center\"\n                    Margin=\"0,0,0,12\"\n                    IsChecked=\"{Binding InstallSettings.CreateDesktopShortcut}\"\n                    Content=\"{Binding ViewText[step_settings_shortcut_heading]}\"/>\n\n                <Separator />\n\n                <Grid>\n                    <Grid.ColumnDefinitions>\n                        <ColumnDefinition Width=\"Auto\" />\n                        <ColumnDefinition Width=\"*\" />\n                        <ColumnDefinition Width=\"Auto\" />\n                    </Grid.ColumnDefinitions>\n                    <Label Grid.Column=\"0\" VerticalAlignment=\"Center\" Margin=\"0,0,12,0\" Padding=\"0\" Content=\"{Binding ViewText[label_destination_folder]}\" />\n                    <TextBlock Grid.Column=\"1\" VerticalAlignment=\"Center\" Text=\"{Binding InstallSettings.TargetDirectory}\" TextTrimming=\"CharacterEllipsis\" />\n                    <Button Grid.Column=\"2\" VerticalAlignment=\"Center\" Margin=\"12,0,0,0\" Command=\"{Binding ChooseLocationCommand}\" Content=\"{Binding ViewText[command_change]}\" />\n                </Grid>\n            </StackPanel>\n        </ScrollViewer>\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/InstallSettingsStepControl.xaml.cs",
    "content": "﻿using WC = System.Windows.Controls;\n\nnamespace TRX_InstallerLib.Controls;\n\npublic partial class InstallSettingsStepControl : WC.UserControl\n{\n    public InstallSettingsStepControl()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/InstallSourceControl.xaml",
    "content": "﻿<UserControl\n    x:Class=\"TRX_InstallerLib.Controls.InstallSourceControl\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:models=\"clr-namespace:TRX_InstallerLib.Models\"\n    xmlns:utils=\"clr-namespace:TRX_InstallerLib.Utils\"\n    d:DataContext=\"{d:DesignInstance Type=models:InstallSourceViewModel}\"\n    mc:Ignorable=\"d\"\n    d:DesignHeight=\"450\"\n    d:DesignWidth=\"800\">\n\n    <UserControl.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"/TRX_InstallerLib;component/Resources/styles.xaml\" />\n                <ResourceDictionary>\n                    <utils:BoolToVisibilityConverter\n                        x:Key=\"BoolToHiddenConverter\"\n                        FalseValue=\"Visible\"\n                        TrueValue=\"Collapsed\" />\n                    <utils:BoolToVisibilityConverter\n                        x:Key=\"BoolToVisibleConverter\"\n                        FalseValue=\"Collapsed\"\n                        TrueValue=\"Visible\" />\n                </ResourceDictionary>\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </UserControl.Resources>\n\n    <Grid>\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"48\" />\n            <ColumnDefinition Width=\"*\" />\n        </Grid.ColumnDefinitions>\n\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"Auto\" />\n        </Grid.RowDefinitions>\n\n        <Border Grid.Row=\"0\" Grid.Column=\"0\" Grid.RowSpan=\"2\" Margin=\"0,0,12,0\">\n            <Image Source=\"{Binding InstallSource.ImageSource}\" Height=\"{Binding RelativeSource={RelativeSource AncestorType=Border}, Path=ActualWidth}\" VerticalAlignment=\"Center\" />\n        </Border>\n\n        <Grid Grid.Row=\"0\" Grid.Column=\"1\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"*\" />\n                <ColumnDefinition Width=\"Auto\" />\n            </Grid.ColumnDefinitions>\n            <TextBlock Grid.Column=\"0\" Padding=\"0\" Margin=\"0,-3,12,3\" VerticalAlignment=\"Top\" Style=\"{StaticResource subHeading}\" Text=\"{Binding InstallSource.SourceName}\" Height=\"16\" />\n            <TextBlock Grid.Column=\"1\" Padding=\"0\" Margin=\"0,-3,0,3\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\" Style=\"{StaticResource subHeadingFound}\" Visibility=\"{Binding IsAvailable, Converter={StaticResource BoolToVisibleConverter}}\" Text=\"{Binding ViewText[label_found]}\" />\n            <TextBlock Grid.Column=\"1\" Padding=\"0\" Margin=\"0,-3,0,3\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\" Style=\"{StaticResource subHeadingNotFound}\" Visibility=\"{Binding IsAvailable, Converter={StaticResource BoolToHiddenConverter}}\" Text=\"{Binding ViewText[label_not_found]}\" />\n        </Grid>\n\n        <Grid Grid.Row=\"1\" Grid.Column=\"1\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"*\" />\n                <ColumnDefinition Width=\"Auto\" />\n            </Grid.ColumnDefinitions>\n            <TextBlock Grid.Column=\"0\" VerticalAlignment=\"Center\" Text=\"{Binding ViewText[label_folder_not_selected]}\" Visibility=\"{Binding IsSourceDirectoryDefined, Converter={StaticResource BoolToHiddenConverter}}\" />\n            <TextBlock Grid.Column=\"0\" VerticalAlignment=\"Center\" Text=\"{Binding SourceDirectory}\" TextTrimming=\"CharacterEllipsis\" />\n            <TextBlock Grid.Column=\"1\" VerticalAlignment=\"Center\" Margin=\"6,0,0,0\">\n                <Hyperlink Command=\"{Binding ChooseLocationCommand}\">\n                    <Run Text=\"{Binding ViewText[command_change_link]}\"/>\n                </Hyperlink>\n            </TextBlock>\n        </Grid>\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/InstallSourceControl.xaml.cs",
    "content": "﻿using WC = System.Windows.Controls;\n\nnamespace TRX_InstallerLib.Controls;\n\npublic partial class InstallSourceControl : WC.UserControl\n{\n    public InstallSourceControl()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/InstallStepControl.xaml",
    "content": "﻿<UserControl\n    x:Class=\"TRX_InstallerLib.Controls.InstallStepControl\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:models=\"clr-namespace:TRX_InstallerLib.Models\"\n    d:DataContext=\"{d:DesignInstance Type=models:InstallStep}\"\n    mc:Ignorable=\"d\"\n    d:DesignHeight=\"450\" d:DesignWidth=\"800\">\n\n    <UserControl.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"/TRX_InstallerLib;component/Resources/styles.xaml\" />\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </UserControl.Resources>\n\n    <Grid>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n\n        <StackPanel Grid.Row=\"0\" Orientation=\"Vertical\">\n            <TextBlock\n                Style=\"{StaticResource heading}\"\n                Text=\"{Binding ViewText[step_install_heading]}\"/>\n\n            <TextBlock VerticalAlignment=\"Center\" Margin=\"0,0,0,12\" Text=\"{Binding Description}\" />\n\n            <ProgressBar VerticalAlignment=\"Center\" Margin=\"0,0,0,12\" Value=\"{Binding CurrentProgress}\" Maximum=\"{Binding MaximumProgress}\" MinHeight=\"16\" />\n        </StackPanel>\n\n        <TextBox\n            x:Name=\"_logTextBox\"\n            Grid.Row=\"1\"\n            TextWrapping=\"Wrap\"\n            VerticalScrollBarVisibility=\"Auto\"\n            AcceptsReturn=\"True\"\n            IsReadOnly=\"True\"/>\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/InstallStepControl.xaml.cs",
    "content": "﻿using System.Windows;\nusing TRX_InstallerLib.Models;\nusing WC = System.Windows.Controls;\n\nnamespace TRX_InstallerLib.Controls;\n\npublic partial class InstallStepControl : WC.UserControl\n{\n    public InstallStepControl()\n    {\n        InitializeComponent();\n        DataContextChanged += (object sender, DependencyPropertyChangedEventArgs e) =>\n        {\n            var dataContext = DataContext as InstallStep;\n            if (dataContext is not null)\n            {\n                string? lastMessage = null;\n                dataContext.Logger.LogEvent += (object sender, LogEventArgs e) =>\n                {\n                    if (e.Message != lastMessage)\n                    {\n                        lastMessage = e.Message;\n                        AppendMessage(e.Message);\n                    }\n                };\n            }\n        };\n    }\n\n    private void AppendMessage(string message)\n    {\n        _logTextBox.Dispatcher.Invoke(() =>\n        {\n            _logTextBox.AppendText(message + Environment.NewLine);\n            _logTextBox.Focus();\n            _logTextBox.CaretIndex = _logTextBox.Text.Length;\n            _logTextBox.ScrollToEnd();\n        });\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/SourceStepControl.xaml",
    "content": "﻿<UserControl\n    x:Class=\"TRX_InstallerLib.Controls.SourceStepControl\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:controls=\"clr-namespace:TRX_InstallerLib.Controls\"\n    xmlns:models=\"clr-namespace:TRX_InstallerLib.Models\"\n    xmlns:utils=\"clr-namespace:TRX_InstallerLib.Utils\"\n    d:DataContext=\"{d:DesignInstance Type=models:SourceStep}\"\n    mc:Ignorable=\"d\"\n    d:DesignHeight=\"450\" d:DesignWidth=\"800\">\n\n    <UserControl.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"/TRX_InstallerLib;component/Resources/styles.xaml\" />\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </UserControl.Resources>\n\n    <Grid>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" MinHeight=\"30\" />\n            <RowDefinition Height=\"Auto\" MinHeight=\"30\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"*\" />\n        </Grid.ColumnDefinitions>\n\n        <TextBlock\n            Style=\"{StaticResource heading}\"\n            Text=\"{Binding ViewText[step_source_heading]}\"/>\n\n        <TextBlock\n            Grid.Row=\"1\"\n            TextWrapping=\"Wrap\"\n            VerticalAlignment=\"Center\"\n            Margin=\"0,0,0,10\"\n            Text=\"{Binding ViewText[step_source_content]}\"/>\n\n        <ListView\n            BorderThickness=\"0\"\n            Grid.Row=\"2\"\n            Grid.Column=\"0\"\n            ItemsSource=\"{Binding InstallationSources}\"\n            SelectedItem=\"{Binding SelectedInstallationSource, Mode=TwoWay}\"\n            VerticalContentAlignment=\"Top\"\n            HorizontalContentAlignment=\"Stretch\"\n            ScrollViewer.CanContentScroll=\"False\"\n            ScrollViewer.HorizontalScrollBarVisibility=\"Disabled\"\n            ScrollViewer.VerticalScrollBarVisibility=\"Auto\">\n            <ListView.ItemContainerStyle>\n                <Style TargetType=\"{x:Type ListViewItem}\">\n                    <Setter Property=\"Padding\" Value=\"6\" />\n                    <Setter Property=\"Margin\" Value=\"0,0,0,6\" />\n                </Style>\n            </ListView.ItemContainerStyle>\n            <ListView.ItemTemplate>\n                <DataTemplate>\n                    <controls:InstallSourceControl />\n                </DataTemplate>\n            </ListView.ItemTemplate>\n        </ListView>\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/SourceStepControl.xaml.cs",
    "content": "﻿using WC = System.Windows.Controls;\n\nnamespace TRX_InstallerLib.Controls;\n\npublic partial class SourceStepControl : WC.UserControl\n{\n    public SourceStepControl()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/TRXInstallWindow.xaml",
    "content": "﻿<Window\n    x:Class=\"TRX_InstallerLib.Controls.TRXInstallWindow\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:controls=\"clr-namespace:TRX_InstallerLib.Controls\"\n    xmlns:models=\"clr-namespace:TRX_InstallerLib.Models\"\n    xmlns:utils=\"clr-namespace:TRX_InstallerLib.Utils\"\n    d:DataContext=\"{d:DesignInstance Type=models:MainWindowViewModel}\"\n    mc:Ignorable=\"d\"\n    Title=\"{Binding ViewText[window_title_main]}\"\n    MinWidth=\"480\"\n    MinHeight=\"360\"\n    Width=\"{Binding WindowWidth, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged, FallbackValue=640}\"\n    Height=\"500\"\n    WindowStartupLocation=\"CenterScreen\"\n    ResizeMode=\"CanResizeWithGrip\">\n\n    <Window.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"/TRX_InstallerLib;component/Resources/styles.xaml\" />\n                <ResourceDictionary>\n                    <utils:BoolToVisibilityConverter\n                        x:Key=\"BoolToHiddenConverter\"\n                        FalseValue=\"Visible\"\n                        TrueValue=\"Collapsed\" />\n                    <utils:BoolToVisibilityConverter\n                        x:Key=\"BoolToVisibleConverter\"\n                        FalseValue=\"Collapsed\"\n                        TrueValue=\"Visible\" />\n                </ResourceDictionary>\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </Window.Resources>\n\n    <Grid>\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"Auto\" />\n            <ColumnDefinition Width=\"*\" />\n        </Grid.ColumnDefinitions>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"*\" />\n            <RowDefinition Height=\"Auto\" />\n        </Grid.RowDefinitions>\n\n        <Image\n            Grid.Row=\"0\"\n            Grid.Column=\"0\"\n            Source=\"{Binding CurrentStep.SidebarImage}\"\n            Visibility=\"{Binding IsSidebarVisible, Converter={StaticResource BoolToVisibleConverter}}\" />\n\n        <Border Grid.Row=\"0\" Grid.Column=\"1\">\n            <ContentControl Content=\"{Binding CurrentStep}\" Margin=\"12,0,12,12\">\n                <ContentControl.Resources>\n                    <DataTemplate DataType=\"{x:Type models:SourceStep}\">\n                        <controls:SourceStepControl />\n                    </DataTemplate>\n                    <DataTemplate DataType=\"{x:Type models:InstallSettingsStep}\">\n                        <controls:InstallSettingsStepControl />\n                    </DataTemplate>\n                    <DataTemplate DataType=\"{x:Type models:InstallStep}\">\n                        <controls:InstallStepControl />\n                    </DataTemplate>\n                    <DataTemplate DataType=\"{x:Type models:FinishStep}\">\n                        <controls:FinishStepControl />\n                    </DataTemplate>\n                </ContentControl.Resources>\n            </ContentControl>\n        </Border>\n\n        <Grid Grid.Row=\"1\" Grid.Column=\"0\" Grid.ColumnSpan=\"2\" Background=\"{DynamicResource {x:Static SystemColors.ControlBrushKey}}\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"*\" />\n                <ColumnDefinition Width=\"Auto\" />\n            </Grid.ColumnDefinitions>\n            <Grid.Resources>\n                <Style TargetType=\"{x:Type Button}\" BasedOn=\"{StaticResource ButtonStyle}\">\n                    <Setter Property=\"Margin\" Value=\"12,0,0,0\" />\n                </Style>\n            </Grid.Resources>\n\n            <Grid Margin=\"12\" Grid.Column=\"1\" Grid.IsSharedSizeScope=\"True\">\n                <Grid.ColumnDefinitions>\n                    <ColumnDefinition Width=\"Auto\" SharedSizeGroup=\"SSG\"/>\n                    <ColumnDefinition Width=\"Auto\" SharedSizeGroup=\"SSG\"/>\n                    <ColumnDefinition Width=\"Auto\" SharedSizeGroup=\"SSG\"/>\n                </Grid.ColumnDefinitions>\n                <Button\n                    Command=\"{Binding GoToPreviousStepCommand}\"\n                    Visibility=\"{Binding IsFinalStep, Converter={StaticResource BoolToHiddenConverter}}\"\n                    Content=\"{Binding ViewText[command_back]}\" />\n                <Button\n                    Grid.Column=\"1\"\n                    Command=\"{Binding GoToNextStepCommand}\"\n                    Visibility=\"{Binding IsFinalStep, Converter={StaticResource BoolToHiddenConverter}}\"\n                    Content=\"{Binding ViewText[command_next]}\" />\n                <Button\n                    Grid.Column=\"2\"\n                    Command=\"{Binding CloseWindowCommand}\"\n                    CommandParameter=\"{Binding RelativeSource={RelativeSource AncestorType=Window}}\"\n                    Content=\"{Binding IsFinalStep, Converter={utils:ConditionalViewTextConverter TrueValue='command_close', FalseValue='command_cancel'}}\" />\n            </Grid>\n        </Grid>\n    </Grid>\n</Window>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Controls/TRXInstallWindow.xaml.cs",
    "content": "﻿using System.Windows;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Models;\n\nnamespace TRX_InstallerLib.Controls;\n\npublic partial class TRXInstallWindow : Window\n{\n    public TRXInstallWindow(IEnumerable<IInstallSource> installSources)\n    {\n        InitializeComponent();\n        DataContext = new MainWindowViewModel(installSources);\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Installers/BaseInstallSource.cs",
    "content": "using System.IO;\nusing TRX_InstallerLib.Models;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Installers;\n\npublic abstract class BaseInstallSource : IInstallSource\n{\n    public abstract IEnumerable<string> DirectoriesToTry { get; }\n\n    public virtual string ImageSource\n    {\n        get => AssemblyUtils.GetEmbeddedResourcePath($\"{SourceName}.png\");\n    }\n\n    public abstract bool IsImportingSavesSupported { get; }\n    public abstract string SourceName { get; }\n\n    public virtual string SuggestedInstallationDirectory\n    {\n        get => Path.Combine(\n            Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),\n            TRXConstants.Instance.Game!);\n    }\n\n    public abstract Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    );\n\n    public abstract bool IsDownloadingMusicNeeded(string sourceDirectory);\n\n    public abstract bool IsDownloadingExpansionNeeded(string sourceDirectory);\n\n    public abstract bool IsGameFound(string sourceDirectory);\n\n    public static string ConvertTargetPath(string relPath)\n    {\n        string ext = Path.GetExtension(relPath).ToLower();\n        switch (ext)\n        {\n            case \".pcx\":\n                relPath = @$\"data\\images\\og\\{Path.GetFileName(relPath)}\";\n                break;\n            case \".json5\":\n            case \".exe\":\n                return relPath;\n            default:\n                break;\n        }\n\n        return relPath.ToLower();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Installers/IInstallSource.cs",
    "content": "using TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Installers;\n\npublic interface IInstallSource\n{\n    public IEnumerable<string> DirectoriesToTry { get; }\n\n    public string ImageSource { get; }\n\n    public string SourceName { get; }\n\n    public string SuggestedInstallationDirectory { get; }\n\n    public Task CopyOriginalGameFiles(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        bool importSaves\n    );\n\n    bool IsDownloadingMusicNeeded(string sourceDirectory);\n\n    bool IsDownloadingExpansionNeeded(string sourceDirectory);\n\n    public bool IsGameFound(string sourceDirectory);\n\n    bool IsImportingSavesSupported { get; }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Installers/InstallExecutor.cs",
    "content": "using System.IO;\nusing TRX_InstallerLib.Models;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Installers;\n\npublic class InstallExecutor\n{\n    private static readonly string _resourceBaseURL;\n\n    static InstallExecutor()\n    {\n        _resourceBaseURL = $\"https://lostartefacts.dev/aux/{TRXConstants.Instance.Game!.ToLower()}\";\n    }\n\n    private readonly InstallSettings _settings;\n\n    public InstallExecutor(InstallSettings settings)\n    {\n        _settings = settings;\n    }\n\n    public IInstallSource? InstallSource\n    {\n        get => _settings.InstallSource;\n    }\n\n    public async Task ExecuteInstall(IProgress<InstallProgress> progress)\n    {\n        if (_settings.SourceDirectory is null)\n        {\n            throw new NullReferenceException();\n        }\n        if (_settings.TargetDirectory is null)\n        {\n            throw new NullReferenceException();\n        }\n\n        await CopyOriginalGameFiles(_settings.SourceDirectory, _settings.TargetDirectory, progress);\n        await CopyTRXFiles(_settings.TargetDirectory, progress);\n        if (_settings.DownloadMusic)\n        {\n            await DownloadMusicFiles(_settings.TargetDirectory, progress);\n        }\n\n        if (_settings.DownloadExpansionPack)\n        {\n            await DownloadExpansionFiles(_settings.TargetDirectory, _settings.ExpansionPackType, progress);\n        }\n        if (_settings.CreateDesktopShortcut)\n        {\n            CreateDesktopShortcut(_settings.TargetDirectory);\n        }\n\n        progress.Report(new InstallProgress { Description = Language.Instance.Controls![\"progress_finished\"], Finished = true });\n    }\n\n    protected async Task CopyOriginalGameFiles(string sourceDirectory, string targetDirectory, IProgress<InstallProgress> progress)\n    {\n        if (_settings.InstallSource is null)\n        {\n            throw new NullReferenceException();\n        }\n        await _settings.InstallSource.CopyOriginalGameFiles(sourceDirectory, targetDirectory, progress, _settings.ImportSaves);\n    }\n\n    protected static async Task CopyTRXFiles(string targetDirectory, IProgress<InstallProgress> progress)\n    {\n        InstallUtils.StoreInstallationPath(targetDirectory);\n\n        progress.Report(new InstallProgress\n        {\n            CurrentValue = 0,\n            MaximumValue = 1,\n            Description = Language.Instance.Controls![\"progress_opening_zip\"],\n        });\n\n        using var stream = AssemblyUtils.GetResourceStream(\"Resources.release.zip\", false)\n            ?? throw new ApplicationException(Language.Instance.Controls![\"progress_zip_failure\"]);\n        await InstallUtils.ExtractZip(stream, targetDirectory, progress, overwrite: true);\n    }\n\n    protected static void CreateDesktopShortcut(string targetDirectory)\n    {\n        string targetExe = Path.Combine(targetDirectory, TRXConstants.Instance.Exe);\n        InstallUtils.CreateDesktopShortcut(TRXConstants.Instance.Game!, TRXConstants.Instance.ShortcutTitle!, targetExe);\n        if (File.Exists(Path.Combine(targetDirectory, \"data\", TRXConstants.Instance.GoldFileIdentifier!)))\n        {\n            InstallUtils.CreateDesktopShortcut(TRXConstants.Instance.GoldGame!, TRXConstants.Instance.ShortcutTitle!,\n                targetExe, new[] { TRXConstants.Instance.GoldArgs! });\n        }\n    }\n\n    protected static async Task DownloadMusicFiles(string targetDirectory, IProgress<InstallProgress> progress)\n    {\n        await InstallUtils.DownloadZip($\"{_resourceBaseURL}/music.zip\", targetDirectory, progress);\n    }\n\n    protected static async Task DownloadExpansionFiles(string targetDirectory, ExpansionPackType type, IProgress<InstallProgress> progress)\n    {\n        string? zipName = null;\n        TRXConstants.Instance.GoldZips?.TryGetValue(type, out zipName);\n        if (zipName == null)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_expansion_undefined\"], type));\n        }\n        await InstallUtils.DownloadZip($\"{_resourceBaseURL}/{zipName}\", targetDirectory, progress);\n    }\n\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Installers/InstallUtils.cs",
    "content": "using Microsoft.Win32;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Models;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Installers;\n\npublic static class InstallUtils\n{\n    private static readonly string _registryStorageKey;\n\n    static InstallUtils()\n    {\n        _registryStorageKey = $@\"Software\\{TRXConstants.Instance.Game}\";\n    }\n\n    public static async Task CopyDirectoryTree(\n        string sourceDirectory,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        Func<string, bool>? filterCallback = null,\n        Func<string, string>? targetCallback = null,\n        Func<string, bool>? overwriteCallback = null\n    )\n    {\n        try\n        {\n            progress.Report(new InstallProgress { Description = Language.Instance.Controls![\"progress_scanning\"] });\n            var files = Directory.GetFiles(sourceDirectory, \"*\", SearchOption.AllDirectories);\n            var currentProgress = 0;\n            var maximumProgress = files.Length;\n            foreach (var sourcePath in files)\n            {\n                if (filterCallback is not null && !filterCallback(sourcePath))\n                {\n                    continue;\n                }\n                var relPath = Path.GetRelativePath(sourceDirectory, sourcePath);\n                if (targetCallback is not null)\n                {\n                    relPath = targetCallback(relPath) ?? relPath;\n                }\n                var targetPath = Path.Combine(targetDirectory, relPath);\n                var isSamePath = string.Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(targetPath), StringComparison.OrdinalIgnoreCase);\n                if (!File.Exists(targetPath) || (overwriteCallback is not null && overwriteCallback(sourcePath) && !isSamePath))\n                {\n                    progress.Report(new InstallProgress\n                    {\n                        CurrentValue = currentProgress,\n                        MaximumValue = maximumProgress,\n                        Description = string.Format(Language.Instance.Controls![\"progress_copying\"], relPath),\n                    });\n                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);\n                    await Task.Run(() => File.Copy(sourcePath, targetPath, true));\n\n                    try\n                    {\n                        var file = new FileInfo(targetPath);\n                        if (file.Attributes.HasFlag(FileAttributes.ReadOnly))\n                        {\n                            file.IsReadOnly = false;\n                        }\n                    }\n                    catch { }\n                }\n                else\n                {\n                    progress.Report(new InstallProgress\n                    {\n                        CurrentValue = currentProgress,\n                        MaximumValue = maximumProgress,\n                        Description = string.Format(Language.Instance.Controls![\"progress_skipped\"], relPath),\n                    });\n                }\n\n                currentProgress++;\n            }\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(e.Message);\n        }\n    }\n\n    public static void CreateDesktopShortcut(string name, string title, string targetPath, string[]? args = null)\n    {\n        var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), $\"{name}.lnk\");\n        ShortcutUtils.CreateShortcut(shortcutPath, targetPath, title, args);\n    }\n\n    public static async Task<byte[]> DownloadFile(string url, IProgress<InstallProgress> progress)\n    {\n        HttpProgressClient wc = new();\n        progress.Report(new InstallProgress { Description = string.Format(Language.Instance.Controls![\"progress_init_download\"], url) });\n        wc.DownloadProgressChanged += (totalBytesToReceive, bytesReceived) =>\n        {\n            progress.Report(new InstallProgress\n            {\n                CurrentValue = (int)bytesReceived,\n                MaximumValue = (int)totalBytesToReceive,\n                Description = string.Format(Language.Instance.Controls![\"progress_downloading\"], url),\n            });\n        };\n        return await wc.DownloadDataTaskAsync(new Uri(url));\n    }\n\n    public static async Task DownloadZip(\n        string url,\n        string targetDirectory,\n        IProgress<InstallProgress> progress\n    )\n    {\n        var response = await DownloadFile(url, progress);\n        using var stream = new MemoryStream(response);\n        await ExtractZip(stream, targetDirectory, progress);\n    }\n\n    public static async Task ExtractZip(\n        Stream stream,\n        string targetDirectory,\n        IProgress<InstallProgress> progress,\n        Func<string, bool>? filterCallback = null,\n        bool overwrite = false\n    )\n    {\n        try\n        {\n            using var zip = new ZipArchive(stream);\n            progress.Report(new InstallProgress\n            {\n                Description = Language.Instance.Controls![\"progress_scanning_zip\"],\n            });\n            var currentProgress = 0;\n            var maximumProgress = zip.Entries.Count;\n            foreach (var entry in zip.Entries)\n            {\n                if (new Regex(@\"[\\\\/]$\").IsMatch(entry.FullName))\n                {\n                    continue;\n                }\n                if (filterCallback is not null && !filterCallback(entry.FullName))\n                {\n                    continue;\n                }\n                var relPath = BaseInstallSource.ConvertTargetPath(new Regex(@\"[\\\\/]\").Replace(entry.FullName, Path.DirectorySeparatorChar.ToString()));\n                var targetPath = Path.Combine(targetDirectory, relPath);\n\n                if (!File.Exists(targetPath) || overwrite)\n                {\n                    progress.Report(new InstallProgress\n                    {\n                        CurrentValue = currentProgress,\n                        MaximumValue = maximumProgress,\n                        Description = string.Format(Language.Instance.Controls![\"progress_extracting\"], relPath),\n                    });\n\n                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);\n                    await Task.Run(() => entry.ExtractToFile(targetPath, true));\n                }\n                else\n                {\n                    progress.Report(new InstallProgress\n                    {\n                        CurrentValue = currentProgress,\n                        MaximumValue = maximumProgress,\n                        Description = string.Format(Language.Instance.Controls![\"progress_extracting_skipped\"], relPath),\n                    });\n                }\n\n                currentProgress++;\n            }\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(e.Message);\n        }\n    }\n\n    public static IEnumerable<string> GetDesktopShortcutDirectories()\n    {\n        foreach (\n            var shortcutPath in Directory.EnumerateFiles(\n                Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), \"*.lnk\"\n            )\n        )\n        {\n            string? lnkPath;\n            try\n            {\n                lnkPath = ShortcutUtils.GetLnkTargetPath(shortcutPath);\n            }\n            catch (Exception)\n            {\n                continue;\n            }\n            if (lnkPath is not null)\n            {\n                var dirName = Path.GetDirectoryName(lnkPath);\n                if (dirName is not null)\n                {\n                    yield return dirName;\n                }\n            }\n        }\n    }\n\n    public static void StoreInstallationPath(string installPath)\n    {\n        using var key = Registry.CurrentUser.CreateSubKey(_registryStorageKey);\n        key?.SetValue(\"InstallPath\", installPath);\n    }\n\n    public static string? GetPreviousInstallationPath()\n    {\n        using var key = Registry.CurrentUser.OpenSubKey(_registryStorageKey);\n        return key?.GetValue(\"InstallPath\")?.ToString();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/BaseLanguageViewModel.cs",
    "content": "﻿using TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class BaseLanguageViewModel : BaseNotifyPropertyChanged\n{\n    public static Dictionary<string, string> ViewText\n    {\n        get => Language.Instance.Controls ?? new();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/ExpansionPackType.cs",
    "content": "﻿namespace TRX_InstallerLib.Models;\n\npublic enum ExpansionPackType\n{\n    Music,\n    Vanilla,\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/FinishSettings.cs",
    "content": "using TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class FinishSettings : BaseNotifyPropertyChanged\n{\n    public bool LaunchGame\n    {\n        get => _launchGame;\n        set\n        {\n            if (value != _launchGame)\n            {\n                _launchGame = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool OpenGameDirectory\n    {\n        get => _openGameDirectory;\n        set\n        {\n            if (value != _openGameDirectory)\n            {\n                _openGameDirectory = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    private bool _launchGame = false;\n    private bool _openGameDirectory = true;\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/FinishStep.cs",
    "content": "using TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class FinishStep : BaseLanguageViewModel, IStep\n{\n    public FinishStep(FinishSettings finishSettings)\n    {\n        FinishSettings = finishSettings;\n    }\n\n    public bool CanProceedToNextStep => false;\n    public bool CanProceedToPreviousStep => false;\n    public FinishSettings FinishSettings { get; }\n    public string SidebarImage => AssemblyUtils.GetEmbeddedResourcePath(\"side4.jpg\");\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/IStep.cs",
    "content": "using System.ComponentModel;\n\nnamespace TRX_InstallerLib.Models;\n\npublic interface IStep : INotifyPropertyChanged\n{\n    bool CanProceedToNextStep { get; }\n    bool CanProceedToPreviousStep { get; }\n    string SidebarImage { get; }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/InstallSettings.cs",
    "content": "using TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class InstallSettings : BaseNotifyPropertyChanged\n{\n\n    public bool CreateDesktopShortcut\n    {\n        get => _createDesktopShortcut;\n        set\n        {\n            if (value != _createDesktopShortcut)\n            {\n                _createDesktopShortcut = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool DownloadMusic\n    {\n        get => _downloadMusic;\n        set\n        {\n            if (value != _downloadMusic)\n            {\n                _downloadMusic = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool DownloadExpansionPack\n    {\n        get => _downloadExpansionPack;\n        set\n        {\n            if (value != _downloadExpansionPack)\n            {\n                _downloadExpansionPack = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool AllowExpansionTypeSelection\n    {\n        get => _allowExpansionPackSelection;\n        private set\n        {\n            if (value != _allowExpansionPackSelection)\n            {\n                _allowExpansionPackSelection = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public ExpansionPackType ExpansionPackType\n    {\n        get => _expansionPackType;\n        set\n        {\n            if (value != _expansionPackType)\n            {\n                _expansionPackType = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool ImportSaves\n    {\n        get => _importSaves;\n        set\n        {\n            if (value != _importSaves)\n            {\n                _importSaves = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public IInstallSource? InstallSource\n    {\n        get => _installSource;\n        set\n        {\n            if (value != _installSource)\n            {\n                _installSource = value;\n                DownloadMusic = SourceDirectory is not null && (_installSource?.IsDownloadingMusicNeeded(SourceDirectory) ?? false);\n                AllowExpansionTypeSelection = TRXConstants.Instance.AllowExpansionTypeSelection ?? false;\n                DownloadExpansionPack = SourceDirectory is not null && (_installSource?.IsDownloadingExpansionNeeded(SourceDirectory) ?? false);\n                ImportSaves = _installSource?.IsImportingSavesSupported ?? false;\n                TargetDirectory = _installSource?.SuggestedInstallationDirectory;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool IsDownloadingMusicNeeded\n    {\n        get\n        {\n            return SourceDirectory is not null && (InstallSource?.IsDownloadingMusicNeeded(SourceDirectory) ?? false);\n        }\n    }\n\n    public bool IsDownloadingExpansionNeeded\n    {\n        get\n        {\n            return SourceDirectory is not null && (InstallSource?.IsDownloadingExpansionNeeded(SourceDirectory) ?? false);\n        }\n    }\n\n    public string? SourceDirectory\n    {\n        get => _sourceDirectory;\n        set\n        {\n            if (value != _sourceDirectory)\n            {\n                _sourceDirectory = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public string? TargetDirectory\n    {\n        get => _targetDirectory;\n        set\n        {\n            if (value != _targetDirectory)\n            {\n                _targetDirectory = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    private bool _createDesktopShortcut = true;\n    private bool _downloadMusic;\n    private bool _downloadExpansionPack;\n    private bool _allowExpansionPackSelection;\n    private ExpansionPackType _expansionPackType;\n    private bool _importSaves;\n    private IInstallSource? _installSource;\n    private string? _sourceDirectory;\n    private string? _targetDirectory;\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/InstallSettingsStep.cs",
    "content": "using System.Windows.Input;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class InstallSettingsStep : BaseLanguageViewModel, IStep\n{\n    public InstallSettingsStep(InstallSettings installSettings)\n    {\n        InstallSettings = installSettings;\n        InstallSettings.PropertyChanged += (sender, e) =>\n        {\n            NotifyPropertyChanged(nameof(CanProceedToNextStep));\n        };\n    }\n\n    public bool CanProceedToNextStep => true;\n    public bool CanProceedToPreviousStep => true;\n    public InstallSettings InstallSettings { get; }\n    public string SidebarImage => AssemblyUtils.GetEmbeddedResourcePath(\"side2.jpg\");\n\n\n    private RelayCommand? _chooseLocationCommand;\n    public ICommand ChooseLocationCommand\n    {\n        get => _chooseLocationCommand ??= new RelayCommand(ChooseLocation);\n    }\n\n    private void ChooseLocation()\n    {\n        var result = FileBrowser.Browse(InstallSettings.TargetDirectory);\n        if (result is not null)\n        {\n            InstallSettings.TargetDirectory = result;\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/InstallSourceViewModel.cs",
    "content": "using System.Windows.Input;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class InstallSourceViewModel : BaseLanguageViewModel\n{\n    public InstallSourceViewModel(IInstallSource source)\n    {\n        InstallSource = source;\n\n        foreach (var directory in source.DirectoriesToTry)\n        {\n            if (InstallSource.IsGameFound(directory))\n            {\n                SourceDirectory = directory;\n                break;\n            }\n        }\n    }\n\n    public ICommand ChooseLocationCommand\n    {\n        get\n        {\n            return _chooseLocationCommand ??= new RelayCommand(ChooseLocation);\n        }\n    }\n\n    public IInstallSource InstallSource { get; private set; }\n\n    public bool IsAvailable\n    {\n        get\n        {\n            return SourceDirectory != null && InstallSource.IsGameFound(SourceDirectory);\n        }\n    }\n\n    public bool IsSourceDirectoryDefined\n    {\n        get => SourceDirectory != null;\n    }\n\n    public string? SourceDirectory\n    {\n        get => _sourceDirectory;\n        set\n        {\n            if (value != _sourceDirectory)\n            {\n                _sourceDirectory = value;\n                NotifyPropertyChanged();\n                NotifyPropertyChanged(nameof(IsAvailable));\n                NotifyPropertyChanged(nameof(IsSourceDirectoryDefined));\n            }\n        }\n    }\n\n    private RelayCommand? _chooseLocationCommand;\n    private string? _sourceDirectory;\n\n    private void ChooseLocation()\n    {\n        var result = FileBrowser.Browse(SourceDirectory);\n        if (result is not null)\n        {\n            SourceDirectory = result;\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/InstallStep.cs",
    "content": "using TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class InstallStep : BaseLanguageViewModel, IStep\n{\n    public InstallStep(InstallSettings installSettings)\n    {\n        Logger = new Logger();\n        InstallSettings = installSettings;\n    }\n\n    public bool CanProceedToNextStep\n    {\n        get => _canProceedToNextStep;\n        set\n        {\n            if (value != _canProceedToNextStep)\n            {\n                _canProceedToNextStep = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public bool CanProceedToPreviousStep => false;\n\n    public int CurrentProgress\n    {\n        get { return _currentProgress; }\n        set\n        {\n            if (value != _currentProgress)\n            {\n                _currentProgress = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public string? Description\n    {\n        get => _description;\n        set\n        {\n            if (value != _description)\n            {\n                _description = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n\n    public InstallSettings InstallSettings { get; }\n    public Logger Logger { get; }\n\n    public int MaximumProgress\n    {\n        get { return _maximumProgress; }\n        set\n        {\n            if (value != _maximumProgress)\n            {\n                _maximumProgress = value;\n                NotifyPropertyChanged();\n                NotifyPropertyChanged(nameof(CanProceedToNextStep));\n            }\n        }\n    }\n\n    public string SidebarImage => AssemblyUtils.GetEmbeddedResourcePath(\"side3.jpg\");\n\n    public void RunInstall()\n    {\n        var progress = new Progress<InstallProgress>();\n        progress.ProgressChanged += (sender, progress) =>\n        {\n            if (progress.CurrentValue is not null && progress.MaximumValue is not null)\n            {\n                CurrentProgress = progress.CurrentValue.Value;\n                MaximumProgress = progress.MaximumValue.Value;\n            }\n            else\n            {\n                CurrentProgress = progress.Finished ? 1 : 0;\n                MaximumProgress = 1;\n            }\n            Description = progress.Description;\n            if (progress.Description is not null)\n            {\n                Logger.RaiseLogEvent(progress.Description);\n            }\n            if (progress.Finished)\n            {\n                CanProceedToNextStep = true;\n            }\n        };\n\n        Task.Run(async () =>\n        {\n            try\n            {\n                var executor = new InstallExecutor(InstallSettings);\n                await executor.ExecuteInstall(progress);\n            }\n            catch (Exception ex)\n            {\n                Logger.RaiseLogEvent(ex.ToString());\n            }\n        });\n    }\n\n    private bool _canProceedToNextStep;\n    private int _currentProgress = 0;\n    private string? _description;\n    private int _maximumProgress = 1;\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/Language.cs",
    "content": "﻿using Newtonsoft.Json;\nusing Newtonsoft.Json.Linq;\nusing System.Globalization;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class Language\n{\n    private static readonly string _langPathFormat = \"Resources.Lang.{0}.json\";\n    private static readonly string _defaultCulture = \"en-US\";\n\n    public static Language Instance { get; private set; }\n\n    public Dictionary<string, string>? Controls { get; set; }\n\n    static Language()\n    {\n        CultureInfo defaultCulture = CultureInfo.GetCultureInfo(_defaultCulture);\n        JObject defaultData = ReadLanguage(defaultCulture.TwoLetterISOLanguageName);\n\n        if (CultureInfo.CurrentCulture != defaultCulture)\n        {\n            // Merge the main language first if it exists, and then the country specific if that exists.\n            // e.g. fr.json would load first, then fr-BE.json.\n            MergeLanguage(defaultData, CultureInfo.CurrentCulture.TwoLetterISOLanguageName);\n            MergeLanguage(defaultData, CultureInfo.CurrentCulture.Name);\n        }\n\n        Instance = JsonConvert.DeserializeObject<Language>(defaultData.ToString())!;\n    }\n\n    private static JObject ReadLanguage(string tag)\n    {\n        return JsonUtils.LoadEmbeddedResource(string.Format(_langPathFormat, tag)) ?? new();\n    }\n\n    private static void MergeLanguage(JObject data, string tag)\n    {\n        JObject cultureData = ReadLanguage(tag);\n        if (cultureData != null)\n        {\n            data.Merge(cultureData);\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/Logger.cs",
    "content": "namespace TRX_InstallerLib.Models;\n\npublic class LogEventArgs\n{\n    public LogEventArgs(string message)\n    {\n        Message = message;\n    }\n\n    public string Message { get; }\n}\n\npublic class Logger\n{\n    public delegate void LogEventHandler(object sender, LogEventArgs e);\n\n    public event LogEventHandler? LogEvent;\n\n    public void RaiseLogEvent(string message)\n    {\n        LogEvent?.Invoke(this, new LogEventArgs(message));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/MainWindowViewModel.cs",
    "content": "using System.IO;\nusing System.Windows;\nusing System.Windows.Input;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class MainWindowViewModel : BaseLanguageViewModel\n{\n    public MainWindowViewModel(IEnumerable<IInstallSource> installSources)\n    {\n        _sourceStep = new SourceStep(installSources);\n        _currentStep = _sourceStep;\n        _installSettings = new InstallSettings();\n    }\n\n    public ICommand CloseWindowCommand\n    {\n        get => _closeWindowCommand ??= new RelayCommand<Window>(CloseWindow);\n    }\n\n    public IStep CurrentStep\n    {\n        get => _currentStep;\n        set\n        {\n            _currentStep = value;\n            _goToPreviousStepCommand?.RaiseCanExecuteChanged();\n            _goToNextStepCommand?.RaiseCanExecuteChanged();\n            _currentStep.PropertyChanged += (sender, e) =>\n            {\n                _goToPreviousStepCommand?.RaiseCanExecuteChanged();\n                _goToNextStepCommand?.RaiseCanExecuteChanged();\n            };\n            NotifyPropertyChanged();\n            NotifyPropertyChanged(nameof(IsFinalStep));\n        }\n    }\n\n    public ICommand GoToNextStepCommand\n    {\n        get => _goToNextStepCommand ??= new RelayCommand(GoToNextStep, CanGoToNextStep);\n    }\n\n    public ICommand GoToPreviousStepCommand\n    {\n        get => _goToPreviousStepCommand ??= new RelayCommand(GoToPreviousStep, CanGoToPreviousStep);\n    }\n\n    public bool IsFinalStep\n    {\n        get => CurrentStep is FinishStep;\n    }\n\n    public bool IsSidebarVisible\n    {\n        get => WindowWidth >= 500;\n    }\n\n    public int WindowWidth\n    {\n        get => _windowWidth;\n        set\n        {\n            if (value != _windowWidth)\n            {\n                _windowWidth = value;\n                NotifyPropertyChanged(nameof(IsSidebarVisible));\n            }\n        }\n    }\n\n    private const bool _autoFinishInstallStep = false;\n\n    private RelayCommand<Window>? _closeWindowCommand;\n\n    private IStep _currentStep;\n    private FinishSettings? _finishSettings;\n    private RelayCommand? _goToNextStepCommand;\n    private RelayCommand? _goToPreviousStepCommand;\n    private readonly InstallSettings _installSettings;\n    private readonly IStep _sourceStep;\n    private int _windowWidth;\n\n    private bool CanGoToNextStep()\n    {\n        return CurrentStep.CanProceedToNextStep;\n    }\n\n    private bool CanGoToPreviousStep()\n    {\n        return CurrentStep.CanProceedToPreviousStep;\n    }\n\n    private void CloseWindow(Window? window)\n    {\n        if (_finishSettings is not null && _finishSettings.LaunchGame)\n        {\n            if (_installSettings.TargetDirectory is null)\n            {\n                throw new NullReferenceException();\n            }\n            ProcessUtils.Start(Path.Combine(_installSettings.TargetDirectory, TRXConstants.Instance.Exe));\n        }\n        if (_finishSettings is not null && _finishSettings.OpenGameDirectory)\n        {\n            if (_installSettings.TargetDirectory is null)\n            {\n                throw new NullReferenceException();\n            }\n            ProcessUtils.Start(_installSettings.TargetDirectory);\n        }\n        window?.Close();\n    }\n\n    private void GoToNextStep()\n    {\n        if (CurrentStep is SourceStep sourceStep)\n        {\n            var installSource = sourceStep.SelectedInstallationSource!.InstallSource;\n            _installSettings.InstallSource = installSource;\n            _installSettings.SourceDirectory = sourceStep.SelectedInstallationSource.SourceDirectory;\n            CurrentStep = new InstallSettingsStep(_installSettings);\n        }\n        else if (CurrentStep is InstallSettingsStep targetStep)\n        {\n            var installStep = new InstallStep(targetStep.InstallSettings);\n            installStep.RunInstall();\n            installStep.PropertyChanged += (sender, e) =>\n            {\n                if (_autoFinishInstallStep && installStep.CanProceedToNextStep)\n                {\n                    _finishSettings = new FinishSettings();\n                    CurrentStep = new FinishStep(_finishSettings);\n                }\n            };\n            CurrentStep = installStep;\n        }\n        else if (CurrentStep is InstallStep)\n        {\n            _finishSettings = new FinishSettings();\n            CurrentStep = new FinishStep(_finishSettings);\n        }\n    }\n\n    private void GoToPreviousStep()\n    {\n        CurrentStep = _sourceStep;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/SourceStep.cs",
    "content": "using System.Collections.ObjectModel;\nusing TRX_InstallerLib.Installers;\nusing TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class SourceStep : BaseLanguageViewModel, IStep\n{\n    public SourceStep(IEnumerable<IInstallSource> installSources)\n    {\n        // NOTE: the order also decides which installation source will be selected by default\n        InstallationSources = new ObservableCollection<InstallSourceViewModel>\n            (installSources.Select(i => new InstallSourceViewModel(i)));\n\n        foreach (var installationSource in InstallationSources)\n        {\n            installationSource.PropertyChanged += (sender, e) =>\n            {\n                NotifyPropertyChanged(nameof(InstallationSources));\n                if (installationSource == selectedInstallationSource)\n                {\n                    NotifyPropertyChanged(nameof(SelectedInstallationSource));\n                }\n            };\n        }\n\n        foreach (var source in InstallationSources)\n        {\n            if (source.IsAvailable)\n            {\n                SelectedInstallationSource = source;\n            }\n        }\n    }\n\n    public bool CanProceedToNextStep\n    {\n        get => SelectedInstallationSource != null && SelectedInstallationSource.IsAvailable;\n    }\n\n    public bool CanProceedToPreviousStep => false;\n    public IEnumerable<InstallSourceViewModel> InstallationSources { get; private set; }\n\n    public InstallSourceViewModel? SelectedInstallationSource\n    {\n        get => selectedInstallationSource;\n        set\n        {\n            if (value != selectedInstallationSource)\n            {\n                selectedInstallationSource = value;\n                NotifyPropertyChanged();\n                NotifyPropertyChanged(nameof(CanProceedToNextStep));\n            }\n        }\n    }\n\n    public string SidebarImage => AssemblyUtils.GetEmbeddedResourcePath(\"side1.jpg\");\n    private InstallSourceViewModel? selectedInstallationSource;\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Models/TRXConstants.cs",
    "content": "using TRX_InstallerLib.Utils;\n\nnamespace TRX_InstallerLib.Models;\n\npublic class TRXConstants\n{\n    private static readonly string _constConfigPath = \"Resources.const.json\";\n\n    public static TRXConstants Instance { get; private set; }\n\n    static TRXConstants()\n    {\n        Instance = JsonUtils.LoadEmbeddedResource(_constConfigPath)?.ToObject<TRXConstants>() ?? new();\n    }\n\n    public string? Game { get; set; }\n    public string? GoldGame { get; set; }\n    public string? GoldFileIdentifier { get; set; }\n    public string Exe => $\"TRX.exe\";\n    public bool? AllowExpansionTypeSelection { get; set; }\n    public string? GoldArgs { get; set; }\n    public string? ShortcutTitle { get; set; }\n    public Dictionary<ExpansionPackType, string>? GoldZips { get; set; }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Resources/Lang/en.json",
    "content": "{\n  \"Controls\": {\n    \"command_back\": \"_Back\",\n    \"command_next\": \"_Next\",\n    \"command_close\": \"_Close\",\n    \"command_cancel\": \"_Cancel\",\n    \"command_change\": \"C_hange...\",\n    \"command_change_link\": \"(change)\",\n    \"label_found\": \"Found\",\n    \"label_not_found\": \"Not found\",\n    \"label_already_found\": \"(already found)\",\n    \"label_folder_not_selected\": \"(no folder selected)\",\n    \"label_destination_folder\": \"Destination folder:\",\n    \"label_select_folder\": \"Choose directory\",\n    \"step_source_heading\": \"Step 1: Choose installation source\",\n    \"step_source_content\": \"Placeholder\",\n    \"step_settings_heading\": \"Step 2: Installation options\",\n    \"step_settings_music_heading\": \"Download music tracks\",\n    \"step_settings_music_content\": \"Placeholder\",\n    \"step_settings_expansion_heading\": \"Placeholder\",\n    \"step_settings_expansion_content\": \"Placeholder\",\n    \"step_settings_expansion_music\": \"Placeholder\",\n    \"step_settings_expansion_vanilla\": \"Placeholder\",\n    \"step_settings_saves_header\": \"Import saves\",\n    \"step_settings_saves_content\": \"Placeholder\",\n    \"step_settings_shortcut_heading\": \"Create desktop shortcut\",\n    \"step_install_heading\": \"Step 3: Installing\",\n    \"step_finish_heading\": \"Step 4: Done\",\n    \"step_finish_content\": \"Installation complete. To configure more advanced features, you can edit the JSON files in the cfg/ directory with a text editor.\\n\\nHappy raiding :)\",\n    \"step_finish_open_directory\": \"Open game directory after closing this window\",\n    \"step_finish_open_game\": \"Launch the game after closing this window\",\n    \"progress_scanning\": \"Scanning directory\",\n    \"progress_scanning_source\": \"Scanning the source directory\",\n    \"progress_preparing_extract\": \"Preparing to extract the ISO\",\n    \"progress_copying\": \"Copying {0}\",\n    \"progress_skipped\": \"Copying {0} - skipped\",\n    \"progress_init_download\": \"Initializing download of {0}\",\n    \"progress_expansion_undefined\": \"No target zip defined for expansion pack type {0}\",\n    \"progress_downloading\": \"Downloading {0}\",\n    \"progress_opening_zip\": \"Opening embedded ZIP\",\n    \"progress_zip_failure\": \"Could not open embedded ZIP.\",\n    \"progress_scanning_zip\": \"Scanning ZIP\",\n    \"progress_extracting\": \"Extracting {0}\",\n    \"progress_extracting_skipped\": \"Extracting {0} - skipped\",\n    \"progress_converting_bin\": \"Converting BIN to ISO\",\n    \"progress_converting_bin_failure\": \"Could not convert BIN to ISO: {0}\",\n    \"progress_converting_iso_failure\": \"Could not open converted ISO: {0}\",\n    \"progress_track_write_failure\": \"Could not write to track file {0}: {1}\",\n    \"progress_track_seek_failure\": \"Could not seek to track location: {0}\",\n    \"progress_bin_failure\": \"Could not open BIN {0}: {1}\",\n    \"progress_cue_failure\": \"Could not read CUE {0}: {1}\",\n    \"progress_cue_empty\": \"Could not parse {0}: no tracks were found\",\n    \"progress_finished\": \"Finished\",\n    \"shortcut_signature_failure\": \"Invalid LNK signature\",\n    \"shortcut_target_failure\": \"Unable to determine link target path\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Resources/Lang/it.json",
    "content": "{\n  \"Controls\": {\n    \"command_back\": \"_Indietro\",\n    \"command_next\": \"_Avanti\",\n    \"command_close\": \"_Chiudi\",\n    \"command_cancel\": \"_Annulla\",\n    \"command_change\": \"C_ambia...\",\n    \"command_change_link\": \"(cambia)\",\n    \"label_found\": \"Trovato\",\n    \"label_not_found\": \"Non trovato\",\n    \"label_already_found\": \"(già trovato)\",\n    \"label_folder_not_selected\": \"(nessuna cartella selezionata)\",\n    \"label_destination_folder\": \"Cartella di destinazione:\",\n    \"label_select_folder\": \"Scegli cartella\",\n    \"step_source_heading\": \"Fase 1: Scegli sorgente d'installazione\",\n    \"step_source_content\": \"Segnaposto\",\n    \"step_settings_heading\": \"Fase 2: Opzioni d'installazione\",\n    \"step_settings_music_heading\": \"Scarica tracce musicali\",\n    \"step_settings_music_content\": \"Segnaposto\",\n    \"step_settings_expansion_heading\": \"Segnaposto\",\n    \"step_settings_expansion_content\": \"Segnaposto\",\n    \"step_settings_expansion_music\": \"Segnaposto\",\n    \"step_settings_expansion_vanilla\": \"Segnaposto\",\n    \"step_settings_saves_header\": \"Importa salvataggi\",\n    \"step_settings_saves_content\": \"Segnaposto\",\n    \"step_settings_shortcut_heading\": \"Crea collegamento sul desktop\",\n    \"step_install_heading\": \"Fase 3: Installazione\",\n    \"step_finish_heading\": \"Fase 4: Fatto\",\n    \"step_finish_content\": \"Installazione completata. Per configurare le opzioni avanzate, puoi modifcare i file JSON nella cartella cfg/ con un editor di testo.\\n\\nÈ il momento di razziare qualche tomba :)\",\n    \"step_finish_open_directory\": \"Apri la cartella del gioco dopo aver chiuso questa finestra\",\n    \"step_finish_open_game\": \"Avvia il gioco dopo aver chiuso questa finestra\",\n    \"progress_scanning\": \"Analisi della cartella\",\n    \"progress_scanning_source\": \"Analisi della cartella sorgente\",\n    \"progress_preparing_extract\": \"Preparazione all'estrazione dell'ISO\",\n    \"progress_copying\": \"Copia di {0}\",\n    \"progress_skipped\": \"Copia di {0} - saltato\",\n    \"progress_init_download\": \"Inizializzazione scaricamento di {0}\",\n    \"progress_expansion_undefined\": \"Nessuno zip di destinazione definito per il pacchetto di espansione di tipo {0}\",\n    \"progress_downloading\": \"Scaricamento di {0}\",\n    \"progress_opening_zip\": \"Apertura ZIP incluso\",\n    \"progress_zip_failure\": \"Impossibile aprire ZIP incluso.\",\n    \"progress_scanning_zip\": \"Analisi ZIP\",\n    \"progress_extracting\": \"Estrazione di {0}\",\n    \"progress_extracting_skipped\": \"Estrazione di {0} - saltato\",\n    \"progress_converting_bin\": \"Conversione da BIN a ISO\",\n    \"progress_converting_bin_failure\": \"Impossibile convertire BIN in ISO: {0}\",\n    \"progress_converting_iso_failure\": \"Impossibile aprire ISO convertita: {0}\",\n    \"progress_track_write_failure\": \"Impossibile scrivere sul file di traccia {0}: {1}\",\n    \"progress_track_seek_failure\": \"Impossibile trovare la posizione della traccia: {0}\",\n    \"progress_bin_failure\": \"Impossibile aprire il file BIN {0}: {1}\",\n    \"progress_cue_failure\": \"Impossibile leggere il file CUE {0}: {1}\",\n    \"progress_cue_empty\": \"Impossibile analizzare {0}: nessuna traccia trovata\",\n    \"progress_finished\": \"Finito\",\n    \"shortcut_signature_failure\": \"Firma LNK non valida\",\n    \"shortcut_target_failure\": \"Impossibile determinare il percorso di destinazione del collegamento\"\n  }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Resources/const.json",
    "content": "{\n  \"Game\": \"TRX\",\n  \"AllowExpansionTypeSelection\": false,\n  \"GoldArgs\": \"-gold\"\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Resources/styles.xaml",
    "content": "<ResourceDictionary\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n   xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">\n    <Style TargetType=\"{x:Type Button}\" x:Key=\"ButtonStyle\">\n        <Setter Property=\"Padding\" Value=\"10,5\" />\n        <Setter Property=\"MinWidth\" Value=\"70\" />\n    </Style>\n    <Style TargetType=\"{x:Type Button}\" BasedOn=\"{StaticResource ButtonStyle}\" />\n    <Style x:Key=\"heading\" TargetType=\"TextBlock\">\n        <Setter Property=\"FontSize\" Value=\"20\" />\n        <Setter Property=\"TextWrapping\" Value=\"Wrap\" />\n        <Setter Property=\"VerticalAlignment\" Value=\"Center\" />\n        <Setter Property=\"Margin\" Value=\"0,0,0,12\" />\n    </Style>\n    <Style x:Key=\"subHeading\" TargetType=\"TextBlock\">\n        <Setter Property=\"FontSize\" Value=\"15\" />\n    </Style>\n    <Style x:Key=\"subHeadingFound\" TargetType=\"TextBlock\" BasedOn=\"{StaticResource subHeading}\">\n        <Setter Property=\"Foreground\" Value=\"ForestGreen\" />\n    </Style>\n    <Style x:Key=\"subHeadingNotFound\" TargetType=\"TextBlock\" BasedOn=\"{StaticResource subHeading}\">\n        <Setter Property=\"Foreground\" Value=\"Firebrick\" />\n    </Style>\n    <Style x:Key=\"normal\" TargetType=\"TextBlock\">\n        <Setter Property=\"FontSize\" Value=\"12\" />\n    </Style>\n    <Style x:Key=\"small\">\n        <Setter Property=\"Control.FontSize\" Value=\"10\" />\n        <Setter Property=\"Run.FontSize\" Value=\"10\" />\n    </Style>\n\n    <Style TargetType=\"Image\">\n        <Setter Property=\"RenderOptions.BitmapScalingMode\" Value=\"HighQuality\" />\n    </Style>\n</ResourceDictionary>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/TRX_InstallerLib.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0-windows</TargetFramework>\n    <Nullable>enable</Nullable>\n    <UseWPF>true</UseWPF>\n    <UseWindowsForms>true</UseWindowsForms>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>\n    <EnableWindowsTargeting>true</EnableWindowsTargeting>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.3\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Remove=\"Resources\\const.json\" />\n    <None Remove=\"Resources\\Lang\\en.json\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Resource Include=\"Resources\\styles.xaml\">\n      <Generator>MSBuild:Compile</Generator>\n    </Resource>\n    <EmbeddedResource Include=\"Resources\\const.json\" />\n    <EmbeddedResource Include=\"Resources\\Lang\\en.json\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/AssemblyUtils.cs",
    "content": "﻿using System.IO;\nusing System.Reflection;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic static class AssemblyUtils\n{\n    public static readonly string _resourcePathFormat = \"pack://application:,,,/{0};component/Resources/{1}\";\n\n    private static Assembly? GetReferencedAssembly(bool local)\n    {\n        return local ? Assembly.GetExecutingAssembly() : Assembly.GetEntryAssembly();\n    }\n\n    public static Stream GetResourceStream(string relativePath, bool local)\n    {\n        return GetReferencedAssembly(local)?.GetManifestResourceStream(GetAbsolutePath(relativePath, local))!;\n    }\n\n    public static bool ResourceExists(string relativePath, bool local)\n    {\n        return GetReferencedAssembly(local)?.GetManifestResourceNames()\n            .Contains(GetAbsolutePath(relativePath, local)) ?? false;\n    }\n\n    public static string GetAbsolutePath(string relativePath, bool local)\n    {\n        return $\"{GetReferencedAssembly(local)!.GetName().Name}.{relativePath}\";\n    }\n\n    public static string GetEmbeddedResourcePath(string resource)\n    {\n        return string.Format(_resourcePathFormat, Assembly.GetEntryAssembly()!.GetName().Name, resource);\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/BaseNotifyPropertyChanged.cs",
    "content": "using System.ComponentModel;\nusing System.Runtime.CompilerServices;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic abstract class BaseNotifyPropertyChanged : INotifyPropertyChanged\n{\n    public event PropertyChangedEventHandler? PropertyChanged;\n\n    public void NotifyPropertyChanged([CallerMemberName] string propertyName = \"\")\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/BinaryReaderExtensions.cs",
    "content": "using System.IO;\nusing System.Text;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic static class BinaryReaderExtensions\n{\n    public static string ReadNullTerminatedString(this BinaryReader stream)\n    {\n        string str = \"\";\n        char ch;\n        while ((int)(ch = stream.ReadChar()) != 0)\n        {\n            str += ch;\n        }\n        return str;\n    }\n\n    public static string ReadSystemCodepageString(this BinaryReader stream)\n    {\n        var length = stream.ReadUInt16();\n        return Encoding.Default.GetString(stream.ReadBytes(length));\n    }\n\n    public static string ReadUtf16String(this BinaryReader stream)\n    {\n        var length = stream.ReadUInt16();\n        return Encoding.Unicode.GetString(stream.ReadBytes(length * 2));\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/BoolToVisibilityConverter.cs",
    "content": "using System.Globalization;\nusing System.Windows;\nusing System.Windows.Data;\n\nnamespace TRX_InstallerLib.Utils;\n\n[ValueConversion(typeof(bool), typeof(Visibility))]\npublic class BoolToVisibilityConverter : IValueConverter\n{\n    public BoolToVisibilityConverter()\n    {\n        FalseValue = Visibility.Hidden;\n        TrueValue = Visibility.Visible;\n    }\n\n    public Visibility FalseValue { get; set; }\n    public Visibility TrueValue { get; set; }\n\n    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        return (bool)value ? TrueValue : FalseValue;\n    }\n\n    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/ComparisonConverter.cs",
    "content": "﻿using System.Globalization;\nusing WD = System.Windows.Data;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class ComparisonConverter : WD.IValueConverter\n{\n    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        return value.Equals(parameter);\n    }\n\n    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        return (bool)value ? parameter : WD.Binding.DoNothing;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/ConditionalMarkupConverter.cs",
    "content": "using System.Globalization;\nusing System.Windows.Data;\nusing System.Windows.Markup;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class ConditionalMarkupConverter : MarkupExtension, IValueConverter\n{\n    public object FalseValue { get; set; } = new();\n    public object TrueValue { get; set; } = new();\n\n    public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        return value is true ? TrueValue : FalseValue;\n    }\n\n    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        throw new NotSupportedException();\n    }\n\n    public override object ProvideValue(IServiceProvider serviceProvider)\n    {\n        return this;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/ConditionalViewTextConverter.cs",
    "content": "﻿using System.Globalization;\nusing TRX_InstallerLib.Models;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class ConditionalViewTextConverter : ConditionalMarkupConverter\n{\n    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        string result = base.Convert(value, targetType, parameter, culture).ToString()!;\n        return result.Length == 0 ? string.Empty : Language.Instance.Controls![result];\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/CueFile.cs",
    "content": "using System.IO;\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Models;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class CueFile\n{\n    public readonly List<CueTrack> TrackList = new();\n\n    public CueFile(string cueFilePath)\n    {\n        _cueFilePath = cueFilePath;\n        string cueFileContent;\n        using (TextReader cueReader = new StreamReader(cueFilePath))\n        {\n            cueFileContent = cueReader.ReadToEnd();\n        }\n\n        MatchCollection fileMatches = _fileGroupRegex.Matches(cueFileContent);\n        if (fileMatches.Count == 0)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_cue_empty\"], cueFilePath));\n        }\n\n        foreach (Match fileMatch in fileMatches.Cast<Match>())\n        {\n            var binFilePath = GetBinFilePath(fileMatch.Groups[\"name\"].Value.Trim('\"'));\n            var matches = _trackRegex.Matches(fileMatch.Groups[\"content\"].Value);\n\n            if (matches.Count == 0)\n            {\n                throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_cue_empty\"], cueFilePath));\n            }\n\n            CueTrack? track = null;\n            CueTrack? prevTrack = null;\n            foreach (Match trackMatch in matches.Cast<Match>())\n            {\n                track = new CueTrack(\n                    binFilePath,\n                    int.Parse(trackMatch.Groups[\"track\"].Value),\n                    trackMatch.Groups[\"mode\"].Value,\n                    trackMatch.Groups[\"time\"].Value);\n\n                if (prevTrack != null)\n                {\n                    prevTrack.Stop = track.StartPosition - 1;\n                    prevTrack.StopSector = track.StartSector;\n                }\n                TrackList.Add(track);\n                prevTrack = track;\n            }\n\n            if (track == null)\n            {\n                return;\n            }\n\n            track.Stop = GetBinFileLength(binFilePath);\n            track.StopSector = track.Stop / CueTrack.SectorLength;\n        }\n    }\n\n    private static readonly Regex _fileGroupRegex = new(\n        @\"^file\\s+(?<name>\"\"[^\"\"]+\"\"|[^\"\"\\s]+)\\s+(?<mode>\\w+)\\s+(?<content>(.(?!^file))*)\",\n        RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline);\n\n    private static readonly Regex _trackRegex = new(@\"track\\s+?(?<track>\\d+?)\\s+?(?<mode>\\S+?)[\\s$]+?index\\s+?\\d+?\\s+?(?<time>\\S*)\",\n             RegexOptions.IgnoreCase | RegexOptions.Multiline);\n\n    private readonly string _cueFilePath;\n\n    private static long GetBinFileLength(string binFilePath)\n    {\n        FileInfo fileInfo = new(binFilePath);\n        return fileInfo.Length;\n    }\n\n    private string GetBinFilePath(string name)\n    {\n        var cueDirectory = Path.GetDirectoryName(_cueFilePath)!;\n        string result = Path.Combine(cueDirectory, Path.GetFileName(name));\n        if (!File.Exists(result))\n        {\n            result = Path.Combine(cueDirectory, Path.GetFileNameWithoutExtension(_cueFilePath) + \".bin\");\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/CueTrack.cs",
    "content": "using System.IO;\nusing TRX_InstallerLib.Models;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class CueTrack\n{\n    public const int SectorLength = 2352;\n    public bool Audio;\n    public long StopSector;\n    public bool SwapAudioByteOrder;\n    public bool TruncatePsx;\n    public bool WavFormat;\n\n    public CueTrack(string binFilePath, int trackNumber, string mode, string time)\n    {\n        BinFilePath = binFilePath;\n        TrackNumber = trackNumber;\n        SetMode(mode);\n        StartSector = ToFrames(time);\n    }\n\n    public enum TrackExtension\n    {\n        ISO, CDR, WAV, UGH\n    }\n\n    public string BinFilePath { get; private set; }\n    public int BlockSize { get; private set; }\n    public int BlockStart { get; private set; }\n    public TrackExtension FileExtension { get; private set; }\n\n    public long StartPosition\n    {\n        get { return StartSector * SectorLength; }\n    }\n\n    public long StartSector { get; private set; }\n\n    public long Stop { get; set; }\n\n    public long TotalBytes\n    {\n        get { return (StopSector - StartSector + 1) * BlockSize; }\n    }\n\n    public int TrackNumber { get; private set; }\n\n    public void Write(string targetPath, IProgress<InstallProgress> progress)\n    {\n        using FileStream fileStream = OpenBinFile();\n        try\n        {\n            using Stream stream = File.OpenWrite(targetPath);\n            if (Audio && WavFormat)\n            {\n                byte[] header = MakeWavHeader(TotalBytes);\n                stream.Write(header, 0, header.Length);\n            }\n            long currentPosition = StartPosition;\n            long sector = StartSector;\n            long convertedBytes = 0;\n\n            byte[] buf = new byte[SectorLength];\n            while (sector <= StopSector && fileStream.Read(buf, 0, SectorLength) > 0)\n            {\n                if (Audio && SwapAudioByteOrder)\n                {\n                    DoByteSwap(buf);\n                }\n\n                stream.Write(buf, BlockStart, BlockSize);\n                currentPosition += SectorLength;\n                convertedBytes += BlockSize;\n\n                if (currentPosition / SectorLength % 500 == 0)\n                {\n                    progress.Report(new InstallProgress\n                    {\n                        MaximumValue = (int)TotalBytes,\n                        CurrentValue = (int)convertedBytes,\n                        Description = Language.Instance.Controls![\"progress_converting_bin\"],\n                    });\n                }\n\n                sector++;\n            }\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_track_write_failure\"], targetPath, e.Message));\n        }\n\n        progress.Report(new InstallProgress\n        {\n            MaximumValue = (int)TotalBytes,\n            CurrentValue = (int)TotalBytes,\n            Description = Language.Instance.Controls![\"progress_converting_bin\"],\n        });\n    }\n\n    private static byte[] MakeWavHeader(long length)\n    {\n        const int WAV_RIFF_HLEN = 12;\n        const int WAV_FORMAT_HLEN = 24;\n        const int WAV_DATA_HLEN = 8;\n        const int WAV_HEADER_LEN = WAV_RIFF_HLEN + WAV_FORMAT_HLEN + WAV_DATA_HLEN;\n\n        MemoryStream memoryStream = new(WAV_HEADER_LEN);\n        using (BinaryWriter writer = new(memoryStream))\n        {\n            // RIFF header\n            writer.Write(\"RIFF\".ToCharArray());\n            uint dwordValue = (uint)length + WAV_DATA_HLEN + WAV_FORMAT_HLEN + 4;\n            writer.Write(dwordValue);  // length of file, starting from WAVE\n            writer.Write(\"WAVE\".ToCharArray());\n            // FORMAT header\n            writer.Write(\"fmt \".ToCharArray());\n            dwordValue = 0x10;     // length of FORMAT header\n            writer.Write(dwordValue);\n            ushort wordValue = 0x01;     // constant\n            writer.Write(wordValue);\n            wordValue = 0x02;   // channels\n            writer.Write(wordValue);\n            dwordValue = 44100; // sample rate\n            writer.Write(dwordValue);\n            dwordValue = 44100 * 4; // bytes per second\n            writer.Write(dwordValue);\n            wordValue = 4;      // bytes per sample\n            writer.Write(wordValue);\n            wordValue = 2 * 8;  // bits per channel\n            writer.Write(wordValue);\n            // DATA header\n            writer.Write(\"data\".ToCharArray());\n            dwordValue = (uint)length;\n            writer.Write(dwordValue);\n        }\n        return memoryStream.ToArray();\n    }\n\n    private static long ToFrames(string time)\n    {\n        string[] segs = time.Split(':');\n\n        int mins = int.Parse(segs[0]);\n        int secs = int.Parse(segs[1]);\n        int frames = int.Parse(segs[2]);\n\n        return (mins * 60 + secs) * 75 + frames;\n    }\n\n    private void DoByteSwap(byte[] buf)\n    {\n        // swap low and high bytes\n        int p = BlockStart;\n        int ep = BlockSize;\n        while (p < ep)\n        {\n            (buf[p + 1], buf[p]) = (buf[p], buf[p + 1]);\n            p += 2;\n        }\n    }\n\n    private FileStream OpenBinFile()\n    {\n        FileStream fileStream;\n        try\n        {\n            fileStream = File.OpenRead(BinFilePath);\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_bin_failure\"], BinFilePath, e.Message));\n        }\n        try\n        {\n            fileStream.Seek(StartPosition, SeekOrigin.Begin);\n        }\n        catch (Exception e)\n        {\n            throw new ApplicationException(string.Format(Language.Instance.Controls![\"progress_track_seek_failure\"], e.Message));\n        }\n        return fileStream;\n    }\n\n    private void SetMode(string mode)\n    {\n        Audio = false;\n        BlockStart = 0;\n        FileExtension = TrackExtension.ISO;\n\n        switch (mode.ToUpper())\n        {\n            case \"AUDIO\":\n                BlockSize = 2352;\n                Audio = true;\n                FileExtension = WavFormat ? TrackExtension.WAV : TrackExtension.CDR;\n                break;\n\n            case \"MODE1/2352\":\n                BlockStart = 16;\n                BlockSize = 2048;\n                break;\n\n            case \"MODE2/2336\":\n                // WAS 2352 in V1.361B still work? What if MODE2/2336 single track bin, still 2352 sectors?\n                BlockStart = 16;\n                BlockSize = 2336;\n                break;\n\n            case \"MODE2/2352\":\n                if (TruncatePsx)\n                {\n                    // PSX: truncate from 2352 to 2336 byte tracks\n                    BlockSize = 2336;\n                }\n                else\n                {\n                    // Normal MODE2/2352\n                    BlockStart = 24;\n                    BlockSize = 2048;\n                }\n                break;\n\n            default:\n                BlockSize = 2352;\n                FileExtension = TrackExtension.UGH;\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/FileBrowser.cs",
    "content": "using TRX_InstallerLib.Models;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class FileBrowser\n{\n    public static string? Browse(string? initialDirectory)\n    {\n        using var dlg = new FolderBrowserDialog()\n        {\n            Description = Language.Instance.Controls![\"label_select_folder\"],\n            SelectedPath = initialDirectory ?? string.Empty,\n            ShowNewFolderButton = true,\n        };\n        if (dlg.ShowDialog() == DialogResult.OK)\n        {\n            return dlg.SelectedPath;\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/HttpProgressClient.cs",
    "content": "﻿using System.IO;\nusing System.Net.Http;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class HttpProgressClient\n{\n    public delegate void ProgressChangedHandler(long totalBytesToReceive, long bytesReceived);\n    public event ProgressChangedHandler? DownloadProgressChanged;\n\n    public async Task<byte[]> DownloadDataTaskAsync(Uri uri)\n    {\n        using HttpClient client = new();\n        client.DefaultRequestHeaders.CacheControl = new()\n        {\n            NoCache = true\n        };\n\n        HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);\n        response.EnsureSuccessStatusCode();\n\n        long totalBytes = response.Content.Headers.ContentLength ?? 0;\n\n        using Stream contentStream = await response.Content.ReadAsStreamAsync();\n        return await ProcessContentStream(totalBytes, contentStream);\n    }\n\n    private async Task<byte[]> ProcessContentStream(long totalBytes, Stream contentStream)\n    {\n        long totalBytesRead = 0;\n        byte[] buffer = new byte[8192];\n\n        using MemoryStream outputStream = new();\n        while (true)\n        {\n            int bytesRead = await contentStream.ReadAsync(buffer);\n            if (bytesRead == 0)\n            {\n                break;\n            }\n\n            await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead));\n            totalBytesRead += bytesRead;\n\n            DownloadProgressChanged?.Invoke(totalBytes, totalBytesRead);\n        }\n\n        return outputStream.ToArray();\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/InstallProgress.cs",
    "content": "namespace TRX_InstallerLib.Utils;\n\npublic class InstallProgress\n{\n    public int? CurrentValue { get; set; }\n    public string? Description { get; set; }\n    public bool Finished { get; set; }\n    public int? MaximumValue { get; set; }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/JsonUtils.cs",
    "content": "﻿using Newtonsoft.Json.Linq;\nusing System.IO;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic static class JsonUtils\n{\n    public static JObject? LoadEmbeddedResource(string path)\n    {\n        // Try to locate the data in this assembly first, then merge it\n        // with the same in the entry assembly if relevant.\n        JObject? data = null;\n\n        if (AssemblyUtils.ResourceExists(path, true))\n        {\n            using Stream stream = AssemblyUtils.GetResourceStream(path, true);\n            using StreamReader reader = new(stream);\n            data = JObject.Parse(reader.ReadToEnd());\n        }\n\n        if (AssemblyUtils.ResourceExists(path, false))\n        {\n            data ??= new();\n            using Stream stream = AssemblyUtils.GetResourceStream(path, false);\n            using StreamReader reader = new(stream);\n            data.Merge(JObject.Parse(reader.ReadToEnd()));\n        }\n\n        return data;\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/ProcessUtils.cs",
    "content": "﻿using System.Diagnostics;\nusing System.IO;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic static class ProcessUtils\n{\n    public static void Start(string fileName, string? arguments = null)\n    {\n        Process.Start(new ProcessStartInfo\n        {\n            FileName = fileName,\n            Arguments = arguments,\n            UseShellExecute = true,\n            WorkingDirectory = new Uri(fileName).IsFile\n                ? Path.GetDirectoryName(fileName)\n                : null\n        });\n    }\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/RelayCommand.cs",
    "content": "using System.Windows.Input;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic class RelayCommand : ICommand\n{\n    public RelayCommand(Action execute, Func<bool>? canExecute)\n    {\n        _execute = execute;\n        _canExecute = canExecute;\n    }\n\n    public RelayCommand(Action execute)\n    {\n        _execute = execute;\n        _canExecute = null;\n    }\n\n    public event EventHandler? CanExecuteChanged\n    {\n        add\n        {\n            CommandManager.RequerySuggested += value;\n            _canExecuteChanged += value;\n        }\n        remove\n        {\n            CommandManager.RequerySuggested -= value;\n            _canExecuteChanged -= value;\n        }\n    }\n\n    public bool CanExecute(object? parameter)\n    {\n        return _canExecute == null || _canExecute();\n    }\n\n    public void Execute(object? parameter)\n    {\n        _execute();\n    }\n\n    public void RaiseCanExecuteChanged()\n    {\n        _canExecuteChanged?.Invoke(this, EventArgs.Empty);\n    }\n\n    private readonly Func<bool>? _canExecute;\n    private readonly Action _execute;\n\n    private EventHandler? _canExecuteChanged;\n}\n\npublic class RelayCommand<T> : ICommand\n{\n    public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute)\n    {\n        _execute = execute;\n        _canExecute = canExecute;\n    }\n\n    public RelayCommand(Action<T?> execute)\n    {\n        _execute = execute;\n        _canExecute = null;\n    }\n\n    public event EventHandler? CanExecuteChanged\n    {\n        add\n        {\n            CommandManager.RequerySuggested += value;\n            _canExecuteChanged += value;\n        }\n        remove\n        {\n            CommandManager.RequerySuggested -= value;\n            _canExecuteChanged -= value;\n        }\n    }\n\n    public bool CanExecute(object? parameter)\n    {\n        return _canExecute == null || _canExecute((T?)parameter);\n    }\n\n    public void Execute(object? parameter)\n    {\n        _execute((T?)parameter);\n    }\n\n    public void RaiseCanExecuteChanged()\n    {\n        _canExecuteChanged?.Invoke(this, EventArgs.Empty);\n    }\n\n    private readonly Func<T?, bool>? _canExecute;\n    private readonly Action<T?> _execute;\n\n    private EventHandler? _canExecuteChanged;\n}\n"
  },
  {
    "path": "tools/installer/TRX_InstallerLib/Utils/ShortcutUtils.cs",
    "content": "using System.IO;\nusing System.Text;\n\nusing System.Text.RegularExpressions;\nusing TRX_InstallerLib.Models;\n\nnamespace TRX_InstallerLib.Utils;\n\npublic static class ShortcutUtils\n{\n    public static void CreateShortcut(string shortcutPath, string targetPath, string name, string[]? args = null)\n    {\n        var fileInfo = File.Exists(targetPath) ? new FileInfo(targetPath) : null;\n        using var stream = File.Open(Path.ChangeExtension(shortcutPath, \"lnk\"), FileMode.Create);\n        using var bw = new BinaryWriter(stream);\n\n        void writeShellLinkHeader()\n        {\n            // HeaderSize\n            bw.Write(0x4C);\n\n            // LinkCLSID\n            bw.Write(new byte[] { 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 });\n\n            // LinkFlags\n            bw.Write(\n                (1 << 0) // HasLinkTargetIDList\n                | (1 << 2) // HasName\n                | (1 << 3) // HasRelativePath\n                | (1 << 4) // HasWorkingDir\n                | (1 << 5) // HasArguments\n                | (1 << 7) // IsUnicode\n                | (1 << 8) // ForceNoLinkInfo\n            );\n\n            if (fileInfo is not null)\n            {\n                bw.Write((int)fileInfo.Attributes); // FileAttributes\n                bw.Write(fileInfo.CreationTimeUtc.ToFileTime()); // CreationTime\n                bw.Write(fileInfo.LastAccessTimeUtc.ToFileTime()); // AccessTime\n                bw.Write(fileInfo.LastWriteTimeUtc.ToFileTime()); // WriteTime\n                bw.Write((int)fileInfo.Length); // FileSize\n            }\n            else\n            {\n                bw.Write(0); // FileAttributes\n                bw.Write((long)0); // CreationTime\n                bw.Write((long)0); // AccessTime\n                bw.Write((long)0); // WriteTime\n                bw.Write(0); // FileSize\n            }\n            bw.Write(0); // IconIndex\n            bw.Write(1); // ShowCommand - SW_SHOWNORMAL\n            bw.Write((short)0); // HotKey\n            bw.Write((short)0); // Reserved1\n            bw.Write(0); // Reserved2\n            bw.Write(0); // Reserved3\n        }\n\n        void writeLinkTargetIDList()\n        {\n            var idListSizePos = (int)bw.BaseStream.Position;\n            bw.Write((ushort)0); // IDListSize\n\n            // CLSID for this computer\n            bw.Write((short)(0x12 + 2));\n            bw.Write(new byte[] { 0x1F, 0x50, 0xE0, 0x4F, 0xD0, 0x20, 0xEA, 0x3A, 0x69, 0x10, 0xA2, 0xD8, 0x08, 0x00, 0x2B, 0x30, 0x30, 0x9D });\n\n            // Root directory\n            var rootPrefix = \"/\";\n            var root = Path.GetPathRoot(targetPath)!;\n            var rootIdData = Encoding.Default.GetBytes(rootPrefix + root)\n                .Concat(Enumerable.Repeat((byte)0, 21).ToArray())\n                .Concat(new byte[] { 0x00 }).ToArray();\n            bw.Write((short)(rootIdData.Length + 2));\n            bw.Write(rootIdData);\n\n            var targetLeafPrefix = fileInfo is not null && (fileInfo.Attributes & FileAttributes.Directory) != 0\n                ? new byte[] { 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }\n                : new byte[] { 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n            var targetLeaf = Path.GetRelativePath(root, targetPath);\n            var targetLeafIdData = targetLeafPrefix.Concat(Encoding.Default.GetBytes(targetLeaf)).Concat(new byte[] { 0x00 }).ToArray();\n            bw.Write((short)(targetLeafIdData.Length + 2));\n            bw.Write(targetLeafIdData);\n\n            var idListSize = (int)bw.BaseStream.Position - idListSizePos;\n\n            bw.Write((short)0);\n\n            // fix offsets\n            // IDListSize\n            bw.Seek(idListSizePos, SeekOrigin.Begin);\n            bw.Write((short)idListSize);\n\n            // restore pos\n            bw.Seek(idListSizePos + idListSize + 2, SeekOrigin.Begin);\n        }\n\n        void writeStringData()\n        {\n            // NAME\n            bw.Write((short)name.Length);\n            bw.Write(Encoding.Unicode.GetBytes(name));\n\n            // RELATIVE_PATH\n            var relativePath = Path.GetFileName(targetPath);\n            bw.Write((short)relativePath.Length);\n            bw.Write(Encoding.Unicode.GetBytes(relativePath));\n\n            // WORKING_DIR\n            var targetDir = Path.GetDirectoryName(targetPath)!;\n            bw.Write((short)targetDir.Length);\n            bw.Write(Encoding.Unicode.GetBytes(targetDir));\n\n            // ARGUMENTS\n            var cmdline = args is null ? \"\" : string.Join(\n                \" \",\n                args.Select(\n                    arg =>\n                    {\n                        if (string.IsNullOrEmpty(arg))\n                            return arg;\n                        string value = Regex.Replace(arg, @\"(\\\\*)\" + \"\\\"\", @\"$1\\$0\");\n                        value = Regex.Replace(value, @\"^(.*\\s.*?)(\\\\*)$\", \"\\\"$1$2$2\\\"\");\n                        return value;\n                    }\n                ).ToArray()\n            );\n            bw.Write((short)cmdline.Length);\n            bw.Write(Encoding.Unicode.GetBytes(cmdline));\n        }\n\n        writeShellLinkHeader();\n        writeLinkTargetIDList();\n        writeStringData();\n    }\n\n    /// <summary>\n    /// .NET Core compatible .lnk reader.\n    /// MS Documentation:\n    /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/16cb4ca1-9339-4d0c-a68d-bf1d6cc0f943?redirectedfrom=MSDN\n    /// </summary>\n    public static string? GetLnkTargetPath(string filepath)\n    {\n        using var br = new BinaryReader(File.OpenRead(filepath));\n\n        var headerSize = br.ReadUInt32();\n        if (headerSize != 0x4C)\n        {\n            throw new ApplicationException(Language.Instance.Controls![\"shortcut_signature_failure\"]);\n        }\n\n        br.ReadBytes(0x10); // skip LinkCLSID\n\n        // LinkFlags\n        var linkFlags = br.ReadUInt32();\n\n        br.ReadBytes(4); // skip FileAttributes\n        br.ReadBytes(8); // skip CreationTime\n        br.ReadBytes(8); // skip AccessTime\n        br.ReadBytes(8); // skip WriteTime\n        br.ReadBytes(4); // skip FileSize\n        br.ReadBytes(4); // skip IconIndex\n        br.ReadBytes(4); // skip ShowCommand\n        br.ReadBytes(2); // skip Hotkey\n        br.ReadBytes(2); // skip Reserved\n        br.ReadBytes(4); // skip Reserved2\n        br.ReadBytes(4); // skip Reserved3\n\n        var hasLinkTargetIDList = (linkFlags & (1 << 0)) != 0;\n        var hasLinkInfo = (linkFlags & (1 << 1)) != 0;\n        var hasName = (linkFlags & (1 << 2)) != 0;\n        var hasRelativePath = (linkFlags & (1 << 3)) != 0;\n        var hasWorkingDir = (linkFlags & (1 << 4)) != 0;\n        var isUnicode = (linkFlags & (1 << 7)) != 0;\n\n        // if the HasLinkTargetIDList bit, skip LinkTargetIDList\n        if (hasLinkTargetIDList)\n        {\n            var skip = br.ReadUInt16();\n            br.ReadBytes(skip);\n        }\n\n        if (hasLinkInfo)\n        {\n            // get the number of bytes the path contains\n            var linkInfoSize = br.ReadUInt32();\n            br.ReadBytes(4); // skip LinkInfoHeaderSize\n            br.ReadBytes(4); // skip LinkInfoFlags\n            br.ReadBytes(4); // skip VolumeIDOffset\n\n            // Find the location of the LocalBasePath position\n            var localPathBaseOffset = br.ReadUInt32();\n            // Skip to the path position\n            // (subtract the length of the read (4 bytes), the length of the skip (12 bytes), and\n            // the length of the localPathBaseOffset read (4 bytes) from the localPathBaseOffset)\n            br.ReadBytes((int)localPathBaseOffset - 0x14);\n            var size = linkInfoSize - localPathBaseOffset - 0x02;\n            var bytePath = br.ReadBytes((int)size);\n            var path = Encoding.UTF8.GetString(bytePath, 0, bytePath.Length);\n            return path;\n        }\n\n        if (hasName)\n        {\n            var _ = isUnicode ? br.ReadSystemCodepageString() : br.ReadUtf16String(); // skip Name\n        }\n\n        string? relativePath = null;\n        if (hasRelativePath)\n        {\n            relativePath = isUnicode ? br.ReadSystemCodepageString() : br.ReadUtf16String();\n        }\n\n        string? workingDir = null;\n        if (hasWorkingDir)\n        {\n            workingDir = isUnicode ? br.ReadSystemCodepageString() : br.ReadUtf16String();\n        }\n\n        if (workingDir is not null && relativePath is not null)\n        {\n            return Path.Combine(workingDir, relativePath);\n        }\n        if (workingDir is not null)\n        {\n            return workingDir;\n        }\n        if (relativePath is not null)\n        {\n            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), relativePath);\n        }\n\n        throw new ApplicationException(Language.Instance.Controls![\"shortcut_target_failure\"]);\n    }\n}\n"
  },
  {
    "path": "tools/output_current_changelog",
    "content": "#!/usr/bin/env python3\nimport argparse\n\nfrom shared.changelog import Changelog, get_current_version_changelog\nfrom shared.git import Git\nfrom shared.paths import CommonPaths\nfrom shared.versioning import generate_version\n\n\ndef output_changelog(changelog: Changelog, commit_hash: str) -> None:\n    print(f\"**{changelog.release_name}** – \", end=\"\")\n    if changelog.release_date:\n        print(f\"{changelog.release_date:%Y-%m-%d}\", end=\" - \")\n    print(f\"**{commit_hash}**\", end=\" · \")\n    print(f\"[Diff]({changelog.diff_url})\", end=\" · \")\n    print(f\"[History]({changelog.commits_url})\")\n    print()\n\n    if changelog.video_id:\n        print(\n            f\"[![Video](https://i.ytimg.com/vi/{changelog.video_id}/maxresdefault.jpg)]\"\n            f\"(https://www.youtube.com/watch?v={changelog.video_id})\"\n        )\n        print()\n\n    if changelog.body:\n        print(changelog.body.strip())\n    else:\n        print(\"(no changes yet)\")\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--stable\", action=\"store_true\")\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n\n    commit_hash = Git().get_current_commit_hash()\n    changelog = get_current_version_changelog(\n        CommonPaths.changelog_path, skip_unstable=args.stable\n    )\n    output_changelog(changelog, commit_hash=commit_hash)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/output_package_name",
    "content": "#!/usr/bin/env python3\nimport argparse\n\nfrom shared.versioning import generate_package_name, generate_version\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-g\", \"--game\", type=int)\n    parser.add_argument(\"-p\", \"--platform\", required=True)\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    print(\n        generate_package_name(\n            engine_version=generate_version(),\n            platform=args.platform,\n            game_version=args.game,\n        )\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/output_release_name",
    "content": "#!/usr/bin/env python3\nimport argparse\n\nfrom shared.versioning import generate_version\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    return parser.parse_args()\n\n\ndef get_release_name() -> str:\n    return f\"TRX {generate_version()}\"\n\n\ndef main() -> None:\n    args = parse_args()\n    print(get_release_name())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/release",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport difflib\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nfrom shared.changelog import update_changelog_to_new_version\nfrom shared.git import Git\nfrom shared.paths import CommonPaths\n\n\n@dataclass\nclass Options:\n    stable_branch: str = \"stable\"\n    develop_branch: str = \"develop\"\n\n\nclass BaseCommand:\n    name: str = NotImplemented\n    help: str = NotImplemented\n\n    def __init__(self, git: Git) -> None:\n        self.git = git\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"version\")\n\n    def run(self, args: argparse.Namespace, options: Options) -> None:\n        raise NotImplementedError(\"not implemented\")\n\n\nclass CommitCommand(BaseCommand):\n    name = \"commit\"\n    help = \"Create and tag a commit with the release information\"\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        super().decorate_parser(parser)\n        parser.add_argument(\n            \"--video-url\",\n            \"--video\",\n            \"--yt\",\n            help=\"link to the release video URL\",\n        )\n        parser.add_argument(\n            \"-d\",\n            \"--dry-run\",\n            action=\"store_true\",\n            help=\"only output the changelog to stdout, do not commit anything\",\n        )\n\n    def run(self, args: argparse.Namespace, options: Options) -> None:\n        old_tag = self.git.get_branch_version(\n            pattern=f\"trx-*\",\n            branch=\"origin/stable\",\n            abbrev=0,\n        )\n        new_tag = f\"trx-{args.version}\"\n\n        changelog_path = CommonPaths.changelog_path\n        old_changelog = changelog_path.read_text()\n        new_changelog = update_changelog_to_new_version(\n            old_changelog,\n            old_tag=old_tag,\n            new_tag=new_tag,\n            new_version_name=args.version,\n            stable_branch=options.stable_branch,\n            develop_branch=options.develop_branch,\n            video_url=args.video_url,\n        )\n        if old_changelog == new_changelog:\n            return\n\n        if args.dry_run:\n            print(\n                \"\".join(\n                    difflib.unified_diff(\n                        old_changelog.splitlines(keepends=True),\n                        new_changelog.splitlines(keepends=True),\n                        fromfile=str(changelog_path),\n                        tofile=str(changelog_path),\n                    )\n                )\n            )\n            return\n\n        changelog_path.write_text(new_changelog)\n        self.git.add(changelog_path)\n        self.git.commit(f\"docs: release {args.version}\")\n\n\nclass TagCommand(BaseCommand):\n    name = \"tag\"\n    help = \"Tag the current commit with the target version\"\n\n    def run(self, args: argparse.Namespace, options: Options) -> None:\n        new_tag = f\"trx-{args.version}\"\n        self.git.delete_tag(new_tag)\n        self.git.create_tag(new_tag)\n\n\nclass PushCommand(BaseCommand):\n    name = \"push\"\n    help = (\n        \"Push the develop and stable branches, and the version tag to GitHub\"\n    )\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        super().decorate_parser(parser)\n        parser.add_argument(\n            \"-f\",\n            \"--force\",\n            action=\"store_true\",\n            help=\"force push all targets\",\n        )\n\n    def run(self, args: argparse.Namespace, options: Options) -> None:\n        new_tag = f\"trx-{args.version}\"\n        self.git.push(\n            \"origin\",\n            [\"develop\", \"stable\", new_tag],\n            force=args.force,\n            force_with_lease=not args.force,\n        )\n\n\ndef parse_args(commands: list[BaseCommand]) -> None:\n    parser = argparse.ArgumentParser(\n        description=\"Argument parser with subcommands\"\n    )\n    subparsers = parser.add_subparsers(title=\"subcommands\", dest=\"subcommand\")\n    for command in commands:\n        subparser = subparsers.add_parser(command.name, help=command.help)\n        command.decorate_parser(subparser)\n        subparser.set_defaults(command=command)\n\n    result = parser.parse_args()\n    if not hasattr(result, \"command\"):\n        parser.error(\"missing command\")\n    return result\n\n\ndef main() -> None:\n    git = Git()\n    commands = [\n        command_cls(git=git) for command_cls in BaseCommand.__subclasses__()\n    ]\n    options = Options()\n    args = parse_args(commands)\n    args.command.run(args, options)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/shared/__init__.py",
    "content": ""
  },
  {
    "path": "tools/shared/changelog.py",
    "content": "import re\nfrom dataclasses import dataclass\nfrom datetime import date\nfrom pathlib import Path\n\nfrom dateutil.parser import parse as parse_date\n\nBASE_URL = \"https://github.com/LostArtefacts/TRX\"\n\n\n@dataclass\nclass Changelog:\n    release_name: str\n    video_id: str | None\n    body: str\n    commit_tag: str\n    diff_url: str\n    release_date: date | None\n\n    @property\n    def commits_url(self):\n        return f\"{BASE_URL}/commits/{self.commit_tag}/\"\n\n\ndef get_youtube_id(url: str) -> str | None:\n    patterns = [\n        r\"https://(?:www\\.)?youtube\\.com/watch\\?v=([A-Za-z0-9_-]{11})\",\n        r\"https://(?:www\\.)?youtu\\.be/([A-Za-z0-9_-]{11})\",\n    ]\n    for pattern in patterns:\n        if match := re.search(pattern, url):\n            return match.group(1)\n    return None\n\n\ndef get_current_version_changelog(\n    changelog_path: Path, skip_unstable: bool\n) -> Changelog:\n    sections = [\n        section.strip()\n        for section in re.split(\n            \"^(?=##)\", changelog_path.read_text(), flags=re.M\n        )\n        if section\n    ]\n\n    if skip_unstable and sections and \"[unreleased]\" in sections[0].lower():\n        sections.pop(0)\n\n    if sections:\n        section_lines = sections[0].splitlines()\n        header = section_lines.pop(0)\n\n        video_id: str | None = None\n        for i, line in reversed(list(enumerate(section_lines))):\n            if video_id := get_youtube_id(line):\n                section_lines.pop(i)\n\n        return Changelog(\n            video_id=video_id,\n            release_name=re.search(r\"## \\[([^\\]]+?)\\]\", header).group(1),\n            commit_tag=re.search(r\"\\.\\.\\.([^\\)]+?)\\)\", header).group(1),\n            diff_url=re.search(r\"\\(([^\\)]+?)\\)\", header).group(1),\n            release_date=(\n                parse_date(match.group(1))\n                if (match := re.search(r\" - (\\d{4}-\\d{2}-\\d{2})\", header))\n                else None\n            ),\n            body=\"\\n\".join(\n                line for line in section_lines if not line.startswith(\"#\")\n            ),\n        )\n\n\ndef update_changelog_to_new_version(\n    changelog: str,\n    old_tag: str,\n    new_tag: str,\n    new_version_name: str,\n    stable_branch: str | None = \"stable\",\n    develop_branch: str = \"develop\",\n    video_url: str | None = None,\n) -> str:\n    if f\"[{new_version_name}]\" in changelog:\n        return changelog\n    today = date.today().strftime(\"%Y-%m-%d\")\n    repo_url = \"https://github.com/LostArtefacts/TRX\"\n\n    header = f\"## [Unreleased]({repo_url}/compare/{new_tag}...{develop_branch}) - ××××-××-××\\n\\n\"\n    header += f\"## [{new_version_name}]({repo_url}/compare/{old_tag}...{new_tag}) - {today}\\n\"\n    if video_url:\n        header += f\"Showcase: {video_url}\\n\"\n\n    changelog = re.sub(r\"^## \\[Unreleased\\].*\\n*\", \"\", changelog, flags=re.M)\n    changelog = header + changelog\n    return changelog\n"
  },
  {
    "path": "tools/shared/docker/__init__.py",
    "content": ""
  },
  {
    "path": "tools/shared/docker/game-linux/Dockerfile",
    "content": "# TRX building toolchain for Linux.\n#\n# This is a multi-stage Docker image. It is designed to keep the final image\n# size low. Each stage builds an external dependency. The final stage takes the\n# artifacts (binaries, includes etc.) from previous stages and installs all the\n# tools necessary to build TRX.\n\nFROM ubuntu:latest AS base\n\n# don't prompt during potential installation/update of tzinfo\nENV DEBIAN_FRONTEND=noninteractive\nENV TZ=Europe/Warsaw\n\nRUN apt-get update \\\n    && apt-get upgrade -y \\\n    && apt-get install -y \\\n        git \\\n        make\n\n\n\n# pcre2\nFROM base AS pcre2\nRUN apt-get install -y git gcc autoconf libtool\nRUN git clone https://github.com/PCRE2Project/pcre2\nRUN cd pcre2 \\\n    && autoreconf -fi \\\n    && ./configure \\\n        --prefix=/ext/ \\\n    && make -j 4 \\\n    && make install\n\n\n\n# libbacktrace\nFROM base AS backtrace\nRUN apt-get install -y git gcc autoconf libtool\nRUN git clone https://github.com/LostArtefacts/libbacktrace/\nRUN cd libbacktrace \\\n    && autoreconf -fi \\\n    && ./configure \\\n        --prefix=/ext/ \\\n    && make -j 4 \\\n    && make install\n\n\n\n# libav\nFROM base AS libav\nRUN apt-get install -y \\\n    nasm \\\n    gcc \\\n    zlib1g-dev\nRUN git clone \\\n    https://github.com/FFmpeg/FFmpeg\nRUN cd FFmpeg && git checkout 066432ebcf\nCOPY ./tools/ffmpeg_flags.txt /tmp/ffmpeg_flags.txt\nRUN cd FFmpeg \\\n    && ./configure \\\n        --arch=x86 \\\n        --prefix=/ext/ \\\n        --enable-static \\\n        --disable-shared \\\n        $(cat /tmp/ffmpeg_flags.txt) \\\n    && make -j 4 \\\n    && make install\n\n\n\n# SDL\nFROM base AS sdl\nRUN git clone https://github.com/libsdl-org/SDL -b SDL2\nRUN apt-get install -y \\\n    libgl1-mesa-dev \\\n    libglu1-mesa-dev \\\n    libpulse-dev \\\n    libasound2-dev \\\n    libxkbcommon-dev \\\n    libwayland-client0 \\\n    libwayland-cursor0 \\\n    libwayland-dev \\\n    libwayland-egl1 \\\n    automake \\\n    gcc \\\n    libxrandr-dev \\\n    libxext-dev\nRUN cd SDL \\\n    && aclocal -I acinclude \\\n    && autoconf \\\n    && mkdir sdl_build \\\n    && cd sdl_build \\\n    && ../configure \\\n        --prefix=/ext/ \\\n        --enable-video-x11-xrandr \\\n        --enable-shared \\\n        --enable-static \\\n    && make -j 4 \\\n    && make install\n\n\n\n# GLEW\nFROM base AS glew\nRUN git clone https://github.com/nigels-com/glew.git\nRUN apt-get install -y \\\n    build-essential \\\n    libxmu-dev \\\n    libxi-dev \\\n    libgl-dev \\\n    python3\nRUN cd glew/auto \\\n    && PYTHON=python3 make\nRUN mkdir -p /ext/lib \\\n    && export \\\n        GLEW_NO_GLU=-DGLEW_NO_GLU \\\n        GLEW_DEST=/ext \\\n    && cd glew \\\n    && make \\\n    && make install\nRUN sed -i \"s/Cflags: .*/\\\\0 -DGLEW_STATIC /\" /ext/lib/pkgconfig/glew.pc\n\n## Lua\nFROM base AS lua\nRUN apt-get update && apt-get install -y wget build-essential libreadline-dev\nRUN wget https://www.lua.org/ftp/lua-5.4.8.tar.gz \\\n    && tar xzf lua-5.4.8.tar.gz \\\n    && cd lua-5.4.8 \\\n    && make linux \\\n    && make INSTALL_TOP=/ext install\n\n# Provide pkg-config file for Lua\nRUN mkdir -p /ext/lib/pkgconfig\nCOPY tools/shared/docker/lua.pc /ext/lib/pkgconfig/lua.pc\n\n\n\n\n\n# TRX\nFROM base\n\n# set the build dir - actual files are mounted with a Docker volume\nRUN mkdir /app\nWORKDIR /app\n\n# package dependencies\nRUN apt-get install -y \\\n    zlib1g-dev \\\n    libgl1-mesa-dev\n\n# tooling dependencies\n# configure pkgconfig manually\n# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=967969\nENV PKG_CONFIG_LIBDIR=/ext/lib/\nENV PKG_CONFIG_PATH=/ext/lib/pkgconfig/\nRUN apt-get install -y \\\n        pkg-config \\\n        git \\\n        ccache \\\n        python3-pip \\\n    && python3 -m pip install --break-system-packages \\\n        pyjson5 \\\n        meson \\\n        ninja\n# Regular dependencies\nRUN apt-get install -y \\\n    upx \\\n    uthash-dev\n\n## manually built dependencies\nCOPY --from=lua /ext/ /ext/\nCOPY --from=libav /ext/ /ext/\nCOPY --from=sdl /ext/ /ext/\nCOPY --from=backtrace /ext/ /ext/\nCOPY --from=pcre2 /ext/ /ext/\nCOPY --from=glew /ext/ /ext/\n\nENV PYTHONPATH=/app/tools/\nENV CCACHE_BASEDIR=/app/\nENV CCACHE_COMPILERCHECK=content\nENV CCACHE_DIR=/app/.cache/ccache/linux\nENV CC=\"ccache gcc\"\nENV CXX=\"ccache g++\"\nENTRYPOINT [\"/app/tools/shared/docker/game-linux/entrypoint.sh\"]\n"
  },
  {
    "path": "tools/shared/docker/game-linux/entrypoint.sh",
    "content": "#!/usr/bin/env python3\nfrom shared.docker.game_entrypoint import main\n\nmain(platform=\"linux\")\n"
  },
  {
    "path": "tools/shared/docker/game-win/Dockerfile",
    "content": "# TRX building toolchain.\n#\n# This is a multi-stage Docker image. It is designed to keep the final image\n# size low. Each stage builds an external dependency. The final stage takes the\n# artifacts (binaries, includes etc.) from previous stages and installs all the\n# tools necessary to build TRX.\n\n# MinGW\nFROM ubuntu:latest AS mingw\n\n# don't prompt during potential installation/update of tzinfo\nENV DEBIAN_FRONTEND=noninteractive\nENV TZ=Europe/Warsaw\n\nRUN apt-get update \\\n    && apt-get upgrade -y \\\n    && apt-get install -y \\\n        gcc \\\n        gcc-mingw-w64-i686 \\\n        g++-mingw-w64-i686 \\\n        git \\\n        make\n\n\n\n# pcre\nFROM mingw AS pcre2\nRUN git clone https://github.com/PCRE2Project/pcre2\nRUN apt-get -y install libtool\nRUN cd pcre2 \\\n    && autoreconf -fi \\\n    && ./configure \\\n        --host=i686-w64-mingw32 \\\n        --prefix=/ext/ \\\n    && make -j 4 \\\n    && make install\n\n\n\n# zlib\nFROM mingw AS zlib\nRUN git clone https://github.com/madler/zlib --branch=v1.3.1\nRUN cd zlib \\\n    && make -f win32/Makefile.gcc \\\n        SHARED_MODE=1 \\\n        BINARY_PATH=/ext/bin \\\n        INCLUDE_PATH=/ext/include \\\n        LIBRARY_PATH=/ext/lib \\\n        PREFIX=i686-w64-mingw32- \\\n        -j 4 install\n\n\n\n# libav\nFROM mingw AS libav\nRUN apt-get install -y \\\n    nasm\nRUN git clone \\\n    https://github.com/FFmpeg/FFmpeg\nRUN cd FFmpeg && git checkout 066432ebcf\nCOPY --from=zlib /ext/ /usr/i686-w64-mingw32/\nCOPY ./tools/ffmpeg_flags.txt /tmp/ffmpeg_flags.txt\nRUN cd FFmpeg \\\n    && ./configure \\\n        --arch=x86 \\\n        --target-os=mingw32 \\\n        --cross-prefix=i686-w64-mingw32- \\\n        --prefix=/ext/ \\\n        --cc=i686-w64-mingw32-gcc \\\n        --cxx=i686-w64-mingw32-g++ \\\n        --host-cc=gcc \\\n        --strip=i686-w64-mingw32-strip \\\n        --pkg-config=i686-w64-mingw32-pkg-config \\\n        --enable-static \\\n        --disable-shared \\\n        $(cat /tmp/ffmpeg_flags.txt) \\\n    && make -j 4 \\\n    && make install\n\n\n\n# SDL\nFROM mingw AS sdl\nRUN git clone https://github.com/libsdl-org/SDL -b SDL2\nRUN apt-get install -y automake\nRUN cd SDL \\\n    && aclocal -I acinclude \\\n    && autoconf \\\n    && mkdir build \\\n    && cd build \\\n    && ../configure \\\n        --host=i686-w64-mingw32 \\\n        --build=i686-pc-mingw32 \\\n        --prefix=/ext/ \\\n        --enable-shared \\\n        --enable-static \\\n    && make -j 4 \\\n    && make install\n\n# SDL3 - Compiles fine, but meson cannot find it.\n# TODO - fix that once SDL reaches maturity and releases 3.x as a stable version\n# RUN git clone https://github.com/libsdl-org/SDL\n# RUN apt-get install -y cmake\n# RUN cd SDL \\\n#     && mkdir build \\\n#     && cd build \\\n#     && cmake .. \\\n#         --toolchain build-scripts/cmake-toolchain-mingw64-i686.cmake \\\n#         -DCMAKE_INSTALL_PREFIX=/ext/  \\\n#         -DCMAKE_BUILD_TYPE=Release \\\n#         -DSDL_VULKAN=ON \\\n#         -DSDL_TEST=ON \\\n#     && cmake --build . \\\n#     && make -j 4 \\\n#     && make install\n\n\n\n# uthash\nFROM mingw AS uthash\n\nRUN mkdir /ext/\nWORKDIR /tmp/\n\nRUN apt-get install -y wget xz-utils\nRUN wget https://github.com/troydhanson/uthash/archive/v2.3.0.tar.gz\nRUN tar -xvf v2.3.0.tar.gz\nRUN cp -rL uthash-2.3.0/* /ext/\n\n\n\n# GLEW\nFROM mingw AS glew\nRUN git clone https://github.com/nigels-com/glew.git\nRUN apt-get install -y \\\n    build-essential \\\n    libxmu-dev \\\n    libxi-dev \\\n    libgl-dev \\\n    python3\nRUN cd glew/auto \\\n    && PYTHON=python3 make\nRUN mkdir -p /ext/lib \\\n    && export \\\n        SYSTEM=linux-mingw32 \\\n        GLEW_NO_GLU=-DGLEW_NO_GLU \\\n        GLEW_DEST=/ext \\\n    && cd glew \\\n    && make \\\n    && make install\nRUN sed -i \"s/Cflags: .*/\\\\0 -DGLEW_STATIC/\" /ext/lib/pkgconfig/glew.pc\n\n## Lua\nFROM mingw AS lua\nRUN apt-get update && apt-get install -y wget build-essential\nRUN wget https://www.lua.org/ftp/lua-5.4.8.tar.gz \\\n    && tar xzf lua-5.4.8.tar.gz \\\n    && cd lua-5.4.8 \\\n    && make mingw CROSS=i686-w64-mingw32- CC=\"i686-w64-mingw32-gcc\" \\\n    && make INSTALL_TOP=/ext TO_BIN=\"lua.exe luac.exe\" install\n\n# Provide pkg-config file for Lua\nRUN mkdir -p /ext/lib/pkgconfig\nCOPY tools/shared/docker/lua.pc /ext/lib/pkgconfig/lua.pc\n\n\n\n\n# TRX\nFROM mingw\n\n# set the build dir - actual files are mounted with a Docker volume\nRUN mkdir /app\nWORKDIR /app\n\n# system dependencies\n# configure pkgconfig manually\n# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=967969\nRUN apt-get install -y \\\n        mingw-w64-tools \\\n        pkg-config \\\n        upx \\\n        ccache \\\n        python3-pip \\\n    && python3 -m pip install --break-system-packages \\\n        pyjson5 \\\n        meson \\\n        ninja\n\nCOPY --from=lua /ext/ /ext/\nCOPY --from=pcre2 /ext/ /ext/\nCOPY --from=zlib /ext/ /ext/\nCOPY --from=libav /ext/ /ext/\nCOPY --from=sdl /ext/ /ext/\nCOPY --from=uthash /ext/ /ext/\nCOPY --from=glew /ext/ /ext/\n\nENV PKG_CONFIG_LIBDIR=/ext/lib/\nENV PKG_CONFIG_PATH=/ext/lib/pkgconfig/\nENV C_INCLUDE_PATH=/ext/include/\nENV PYTHONPATH=/app/tools/\nENV CCACHE_BASEDIR=/app/\nENV CCACHE_COMPILERCHECK=content\nENV CCACHE_DIR=/app/.cache/ccache/win\nENTRYPOINT [\"/app/tools/shared/docker/game-win/entrypoint.sh\"]\n"
  },
  {
    "path": "tools/shared/docker/game-win/entrypoint.sh",
    "content": "#!/usr/bin/env python3\nfrom shared.docker.game_entrypoint import main\n\nmain(platform=\"win\")\n"
  },
  {
    "path": "tools/shared/docker/game-win/meson_linux_mingw32.txt",
    "content": "[binaries]\nc = ['ccache', '/usr/bin/i686-w64-mingw32-gcc']\ncpp = ['ccache', '/usr/bin/i686-w64-mingw32-g++']\nobjc = ['ccache', '/usr/bin/i686-w64-mingw32-gcc']\nar = '/usr/bin/i686-w64-mingw32-ar'\nstrip = '/usr/bin/i686-w64-mingw32-strip'\npkg-config = '/usr/bin/i686-w64-mingw32-pkg-config'\nwindres = '/usr/bin/i686-w64-mingw32-windres'\nexe_wrapper = 'wine'\nld = '/usr/bin/i686-w64-mingw32-ld'\n\n[properties]\nskip_sanity_check = true\n\n[host_machine]\nsystem = 'windows'\ncpu_family = 'x86'\ncpu = 'i686'\nendian = 'little'\n"
  },
  {
    "path": "tools/shared/docker/game_entrypoint.py",
    "content": "import argparse\nimport os\nimport shutil\nfrom dataclasses import dataclass, field, fields\nfrom pathlib import Path\nfrom subprocess import check_call, run\nfrom typing import Any, Self\n\nfrom shared.packaging import create_zip\nfrom shared.paths import TOOLS_DIR\nfrom shared.versioning import generate_package_name, generate_version\n\n\n@dataclass\nclass BaseOptions:\n    platform: str\n\n    @property\n    def build_root(self) -> Path:\n        return Path(f\"/app/build/trx/{self.platform}/\")\n\n    @property\n    def version(self) -> str:\n        return generate_version()\n\n    @classmethod\n    def from_args(cls, args: argparse.Namespace) -> Self:\n        cls_fields = [field.name for field in fields(cls)]\n        filtered_args = vars(args)\n        filtered_args = {\n            k: v for k, v in filtered_args.items() if k in cls_fields\n        }\n        return cls(**filtered_args)\n\n\n@dataclass\nclass PackageOptions(BaseOptions):\n    tr_version: int | None\n    platform: str\n\n    @property\n    def default_stem(self) -> Path:\n        return generate_package_name(\n            engine_version=self.version,\n            platform=self.platform,\n            game_version=self.tr_version,\n        )\n\n    @property\n    def ship_dirs(self) -> list[Path]:\n        if self.platform == \"win-installer\":\n            return []\n        elif self.tr_version is None:\n            return [Path(f\"/app/data/trx/ship/\")]\n        else:\n            return [\n                Path(f\"/app/data/common/ship/\"),\n                Path(f\"/app/data/tr{self.tr_version}/ship/\"),\n            ]\n\n    @property\n    def release_zip_files(self) -> list[tuple[Path, str]]:\n        if self.platform == \"linux\":\n            return [(self.build_root / f\"TRX\", f\"TRX\")]\n\n        elif self.platform == \"win\":\n            return [(self.build_root / f\"TRX.exe\", f\"TRX.exe\")]\n        elif self.platform == \"win-installer\":\n            if self.tr_version is None:\n                return [\n                    (\n                        TOOLS_DIR / \"installer/out/TRX_Installer.exe\",\n                        generate_package_name(\n                            engine_version=self.version,\n                            platform=self.platform,\n                            game_version=self.tr_version,\n                        )\n                        + \".exe\",\n                    )\n                ]\n            return [\n                (\n                    TOOLS_DIR\n                    / f\"installer/out/TR{self.tr_version}X_Installer.exe\",\n                    generate_package_name(\n                        engine_version=self.version,\n                        platform=self.platform,\n                        game_version=self.tr_version,\n                    )\n                    + \".exe\",\n                )\n            ]\n\n        return []\n\n\n@dataclass\nclass BuildOptions(BaseOptions):\n    target: str\n    meson_args: list[str] = field(default_factory=list)\n\n    strip_tool = \"strip\"\n    upx_tool = \"upx\"\n\n    @property\n    def build_args(self) -> list[str]:\n        if self.platform == \"win\":\n            return [\n                \"--cross\",\n                \"/app/tools/shared/docker/game-win/meson_linux_mingw32.txt\",\n            ]\n        return []\n\n    @property\n    def compressable_exes(self) -> list[Path]:\n        if self.platform == \"linux\":\n            return [self.build_root / f\"TRX\"]\n        elif self.platform == \"win\":\n            return [self.build_root / f\"TRX.exe\"]\n        return []\n\n    @property\n    def build_target(self) -> Path:\n        return Path(f\"src/\")\n\n\ndef compress_exe(options: BuildOptions, path: Path) -> None:\n    if run([options.upx_tool, \"-t\", str(path)]).returncode != 0:\n        check_call([options.strip_tool, str(path)])\n        check_call([options.upx_tool, str(path)])\n\n\nclass BaseCommand:\n    name: str = NotImplemented\n    help: str = NotImplemented\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        pass\n\n    def run(self, args: argparse.Namespace) -> None:\n        raise NotImplementedError(\"not implemented\")\n\n\nclass BuildCommand(BaseCommand):\n    name = \"build\"\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"--platform\")\n        parser.add_argument(\n            \"--target\",\n            choices=[\"debug\", \"release\", \"debugoptim\"],\n            required=True,\n        )\n        parser.add_argument(\n            \"--meson-arg\",\n            dest=\"meson_args\",\n            action=\"append\",\n            default=[],\n        )\n\n    def run(self, args: argparse.Namespace) -> None:\n        options = BuildOptions.from_args(args)\n        pkg_config_path = os.environ.get(\"PKG_CONFIG_PATH\")\n\n        if not (options.build_root / \"build.ninja\").exists():\n            command: list[str | Path] = [\n                \"meson\",\n                \"setup\",\n                \"--buildtype\",\n                options.target,\n                *options.build_args,\n                *options.meson_args,\n                options.build_root,\n                options.build_target,\n            ]\n            if pkg_config_path:\n                command.extend([\"--pkg-config-path\", pkg_config_path])\n            check_call(command)\n        check_call([\"meson\", \"compile\", f\"TRX\"], cwd=options.build_root)\n\n        if options.target == \"release\":\n            for exe_path in options.compressable_exes:\n                compress_exe(options, exe_path)\n\n\nclass PackageCommand(BaseCommand):\n    name = \"package\"\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"--platform\")\n        parser.add_argument(\"--tr-version\", type=int)\n        parser.add_argument(\"-o\", \"--output\", type=Path)\n        parser.add_argument(\"--no-zip\", action=\"store_true\")\n\n    def run(self, args: argparse.Namespace) -> None:\n        options = PackageOptions.from_args(args)\n        run_package(options=options, output=args.output, no_zip=args.no_zip)\n\n\nclass PackageAllCommand(BaseCommand):\n    name = \"package-all\"\n\n    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:\n        parser.add_argument(\"--platform\")\n        parser.add_argument(\"-o\", \"--output-root\", type=Path)\n        parser.add_argument(\"--no-zip\", action=\"store_true\")\n\n    def run(self, args: argparse.Namespace) -> None:\n        for tr_version in [1, 2, 3, None]:\n            options = PackageOptions(platform=args.platform, tr_version=tr_version)\n            output = None\n            if args.output_root:\n                if tr_version is not None:\n                    output = args.output_root / f\"tr{tr_version}\"\n                else:\n                    output = args.output_root / \"trx\"\n            run_package(options=options, output=output, no_zip=args.no_zip)\n\n\ndef run_package(\n    options: PackageOptions, output: Path | None, no_zip: bool\n) -> None:\n    if output:\n        zip_path = output\n        if zip_path.suffix.lower() != \".zip\" and not no_zip:\n            zip_path /= options.default_stem + \".zip\"\n    else:\n        zip_path = Path()\n        if no_zip:\n            zip_path /= options.default_stem\n        else:\n            zip_path /= options.default_stem + \".zip\"\n\n\n    source_files: list[tuple[Path, str] | Path] = []\n\n    for ship_dir in options.ship_dirs:\n        stack = [ship_dir]\n        while stack:\n            current = stack.pop()\n            for item in current.iterdir():\n                try:\n                    real = item.resolve(strict=True)\n                except FileNotFoundError:\n                    continue  # broken link, ignore\n                if real.is_file():\n                    source_files.append((real, str(item.relative_to(ship_dir))))\n                elif real.is_dir():\n                    stack.append(item)\n\n    for path in options.release_zip_files:\n        source_files.append(path)\n\n    if no_zip:\n        for src_path, dst_name in source_files:\n            dst_path = zip_path / dst_name\n            dst_path.parent.mkdir(parents=True, exist_ok=True)\n            shutil.copy(src_path, dst_path)\n    else:\n        zip_path.parent.mkdir(parents=True, exist_ok=True)\n        create_zip(zip_path, source_files)\n    print(f\"Created {zip_path}\")\n\n\ndef parse_args(\n    commands: dict[str, BaseCommand], **kwargs\n) -> argparse.Namespace:\n    parser = argparse.ArgumentParser(description=\"Docker entrypoint\")\n    subparsers = parser.add_subparsers(dest=\"action\", help=\"Subcommands\")\n    parser.set_defaults(action=\"build\", command=commands[\"build\"])\n    parser.set_defaults(**kwargs)\n\n    for command in commands.values():\n        subparser = subparsers.add_parser(command.name, help=command.help)\n        command.decorate_parser(subparser)\n        subparser.set_defaults(command=command)\n        subparser.set_defaults(**kwargs)\n    result = parser.parse_args()\n    return result\n\n\ndef main(**kwargs: Any) -> None:\n    commands = {\n        command_cls.name: command_cls()\n        for command_cls in BaseCommand.__subclasses__()\n    }\n    args = parse_args(commands, **kwargs)\n    args.command.run(args)\n"
  },
  {
    "path": "tools/shared/docker/installer/Dockerfile",
    "content": "FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env\n\nENV HOME /app\nWORKDIR /app\nENTRYPOINT [\"/app/tools/shared/docker/installer/entrypoint.sh\"]\n"
  },
  {
    "path": "tools/shared/docker/installer/entrypoint.sh",
    "content": "#!/bin/bash\nset -x\nset -e\n\nif [[ -z \"$1\" || ( \"$1\" != \"1\" && \"$1\" != \"2\" && \"$1\" != \"trx\" ) ]]; then\n    echo \"Error: You must supply '1', '2', or 'trx' as an argument.\"\n    exit 1\nfi\nTR_VERSION=$1\n\ncd /app/tools/installer/\n\nexport DOTNET_CLI_HOME=\"/tmp/DOTNET_CLI_HOME\"\n\nshopt -s globstar\nrm -rf **/bin **/obj **/out/*\nif [[ \"$TR_VERSION\" == \"trx\" ]]; then\n    dotnet publish TRX_Installer -c Release -o out\nelse\n    dotnet publish TR${TR_VERSION}X_Installer -c Release -o out\nfi\n"
  },
  {
    "path": "tools/shared/docker/lua.pc",
    "content": "prefix=/ext\nexec_prefix=${prefix}\nlibdir=${exec_prefix}/lib\nincludedir=${prefix}/include\n\nName: Lua\nDescription: The Lua interpreter and library\nVersion: 5.4.8\nLibs: -L${libdir} -llua\nCflags: -I${includedir}\n"
  },
  {
    "path": "tools/shared/files.py",
    "content": "import re\nfrom collections.abc import Iterable\nfrom pathlib import Path\nfrom subprocess import check_output\n\n\ndef find_files(\n    root_dir: Path,\n    extensions: list[str] | None = None,\n    exception_patterns: list[re.Pattern[str]] | None = None,\n) -> Iterable[Path]:\n    stack: list[Path] = [root_dir]\n\n    while stack:\n        parent_dir = stack.pop()\n        for path in parent_dir.iterdir():\n            rel_path = path.relative_to(root_dir)\n            if exception_patterns and any(\n                pattern.match(str(rel_path)) for pattern in exception_patterns\n            ):\n                continue\n\n            if path.is_dir():\n                stack.append(path)\n                continue\n\n            if not path.is_file():\n                continue\n\n            if extensions and path.suffix not in extensions:\n                continue\n\n            yield path\n\n\ndef find_versioned_files(root_dir: Path | None = None) -> Iterable[Path]:\n    for line in check_output(\n        [\"git\", \"ls-files\"], cwd=root_dir, text=True\n    ).splitlines():\n        path = Path(line)\n        if not path.is_dir():\n            if root_dir:\n                yield root_dir / path\n            else:\n                yield path\n\n\ndef is_binary_file(path: Path) -> bool:\n    try:\n        path.read_text(encoding=\"utf-8\")\n    except UnicodeDecodeError:\n        return True\n    return False\n"
  },
  {
    "path": "tools/shared/git.py",
    "content": "from pathlib import Path\nfrom subprocess import CompletedProcess, check_output, run\nfrom typing import Any\n\n\nclass Git:\n    def __init__(self, repo_dir: Path | None = None) -> None:\n        self.repo_dir = repo_dir\n\n    def checkout_branch(self, branch_name: str) -> None:\n        if self.check_output([\"git\", \"diff\", \"--cached\", \"--name-only\"]):\n            raise RuntimeError(\"Staged files\")\n        self.check_output([\"git\", \"checkout\", branch_name])\n\n    def reset(self, target: str, hard: bool = False) -> None:\n        self.check_output(\n            [\"git\", \"reset\", \"develop\", *([\"--hard\"] if hard else [])]\n        )\n\n    def merge_reset(self, target: str, text: str) -> None:\n        self.check_output(\n            [\"git\", \"merge\", target, \"--strategy=ours\", \"--no-commit\"]\n        )\n        self.check_output([\"git\", \"read-tree\", \"-u\", \"--reset\", target])\n        self.check_output([\"git\", \"commit\", \"-m\", text])\n\n    def delete_tag(self, tag_name: str) -> None:\n        self.grab_output([\"git\", \"tag\", \"-d\", tag_name])\n\n    def create_tag(self, tag_name: str) -> None:\n        self.check_output([\"git\", \"tag\", tag_name])\n\n    def add(self, target: str) -> None:\n        self.check_output([\"git\", \"add\", target])\n\n    def commit(self, message: str) -> None:\n        self.check_output([\"git\", \"commit\", \"-m\", message])\n\n    def push(\n        self,\n        upstream: str,\n        targets: list[str],\n        force_with_lease: bool = False,\n        force: bool = False,\n    ) -> None:\n        self.check_output(\n            [\n                \"git\",\n                \"push\",\n                upstream,\n                *targets,\n                *([\"--force-with-lease\"] if force else []),\n                *([\"--force\"] if force else []),\n            ]\n        )\n\n    def get_current_commit_hash(self) -> str:\n        return self.grab_output(\n            [\"git\", \"log\", \"-1\", \"--pretty=format:%H\"]\n        ).stdout.strip()\n\n    def get_branch_version(\n        self,\n        pattern: str | None = None,\n        branch: str | None = None,\n        abbrev: int = 7,\n    ) -> str:\n        return self.grab_output(\n            [\n                \"git\",\n                \"describe\",\n                *([branch] if branch else [\"--dirty\"]),\n                \"--always\",\n                f\"--abbrev={abbrev}\",\n                \"--tags\",\n                \"--exclude\",\n                \"latest\",\n                *([\"--match\", pattern] if pattern else []),\n            ]\n        ).stdout.strip()\n\n    def grab_output(self, *args: Any, **kwargs: Any) -> CompletedProcess:\n        return run(\n            *args, **kwargs, capture_output=True, text=True, cwd=self.repo_dir\n        )\n\n    def check_output(self, *args: Any, **kwargs: Any) -> None:\n        check_output(*args, **kwargs, cwd=self.repo_dir)\n"
  },
  {
    "path": "tools/shared/glyph_mapping.py",
    "content": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\nfrom collections import defaultdict\nimport argparse\nimport ast\nimport json\nimport sys\nimport textwrap\nfrom dataclasses import dataclass\nfrom functools import lru_cache\nfrom pathlib import Path\n\nfrom shared.paths import DATA_DIR\n\n# pip install rectpack numpy lark Pillow\nimport numpy as np\nimport rectpack\nfrom lark import Lark, Transformer\nfrom PIL import Image, ImageDraw, ImageFont\n\nGLYPH_GRAMMAR = Lark(\n    r\"\"\"\n    start: (command | include | _EMPTY_LINE | _COMMENT)*\n\n    include: \"include\" _WS quoted_string\n    command: glyph_text _WS font_idx _WS GLYPH_CLASS _WS source_func (_WS modifier_func)* _NL\n\n    glyph_text: unicode_definition | quoted_string\n    font_idx: number\n    GLYPH_CLASS: \"T\" | \"I\" | \"C\" | \"c\" | \"R\"\n    unicode_definition: \"U+\" /[0-9A-F]{4}/ \":\" /./\n    number: /-?\\d+/\n    quoted_string: QUOTED_STRING\n\n    source_func: grid_sprite_func | manual_sprite_func | image_func | combine_func | link_func | render_func\n    grid_sprite_func: \"grid_sprite\" func_params\n    manual_sprite_func: \"manual_sprite\" func_params\n    image_func: \"image\" func_params\n    combine_func: \"combine\" func_params\n    link_func: \"link\" func_params\n    render_func: \"render\" func_params\n\n    modifier_func: expand_func | translate_func\n    expand_func: \"expand\" func_params\n    translate_func: \"translate\" func_params\n\n    ?expr: quoted_string | number | unicode_definition\n    arglist: arg (\",\" _WS? arg)*\n    ?arg: kwarg | expr\n    kwarg: /[a-z][a-z0-9_]*/ _WS? \"=\" _WS? expr\n    func_params: \"(\" arglist \")\"\n\n    _STRING_INNER: /.*?/\n    _STRING_ESC_INNER: _STRING_INNER _STRING_INNER2\n    _STRING_INNER2: /(?<!\\\\)(\\\\\\\\)*?/\n    QUOTED_STRING: \"\\\"\" _STRING_ESC_INNER \"\\\"\"\n\n    _EMPTY_LINE: _WS? _NL\n    _COMMENT: \"#\" _COMMENT_VAL _NL\n    _COMMENT_VAL: /[^\\n]*/\n    _NL: \"\\n\"\n    _WS: \" \"+\n\"\"\",\n    parser=\"lalr\",\n    strict=True,\n)\n\n\n@lru_cache(maxsize=10)\ndef load_image(path: Path) -> np.ndarray:\n    return np.array(Image.open(path).convert(\"RGBA\"))\n\n\ndef trim_transparent_pixels(\n    image: np.ndarray,\n) -> tuple[np.ndarray, tuple[int, int, int, int]]:\n    if image.shape[2] != 4:\n        raise ValueError(\"Image must be RGBA\")\n\n    # Find non-transparent mask\n    non_transparent = image[:, :, 3] != 0\n\n    # If all pixels are transparent\n    if not non_transparent.any():\n        return image, (0, 0, 0, 0)\n\n    # Where the non-transparent pixels are located\n    rows = np.any(non_transparent, axis=1)\n    cols = np.any(non_transparent, axis=0)\n\n    # Calculate the number of rows and columns removed\n    top = int(np.argmax(rows))\n    bottom = int(np.argmax(rows[::-1]))\n    left = int(np.argmax(cols))\n    right = int(np.argmax(cols[::-1]))\n    height, width, _ = image.shape\n\n    # Trim the image\n    trimmed_image = image[top : height - bottom, left : width - right]\n\n    return trimmed_image, (top, bottom, left, right)\n\n\n@dataclass\nclass Rect:\n    x: int\n    y: int\n    w: int\n    h: int\n\n\n@dataclass\nclass ParserContext:\n    input_dir: Path\n\n\nclass BaseSource:\n    \"\"\"Contains information how to render the given glyph.\"\"\"\n\n    index: int | None = None\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        \"\"\"A method to load sprite pixels and the glyph bounding box.\"\"\"\n        raise NotImplementedError(\"not implemented\")\n\n\n@dataclass\nclass GridSpriteSource(BaseSource):\n    \"\"\"A grid-based sprite sheet source for the glyph.\"\"\"\n\n    ctx: ParserContext\n    filename: str\n    cell_x: int\n    cell_y: int\n    cell_size: int = 20\n    index: int | None = None\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        src_pixels = load_image(self.ctx.input_dir / self.filename)\n        x = self.cell_size * self.cell_x\n        y = self.cell_size * self.cell_y\n        cell_pixels = src_pixels[\n            y : y + self.cell_size, x : x + self.cell_size\n        ]\n        pixels, removed = trim_transparent_pixels(cell_pixels)\n        bbox = Rect(\n            x=0,\n            y=-16 + removed[0],\n            w=self.cell_size - removed[2] - removed[3],\n            h=self.cell_size - removed[1] - removed[0],\n        )\n        return pixels, bbox\n\n\n@dataclass\nclass ManualSpriteSource(BaseSource):\n    \"\"\"A grid-based sprite sheet source for the glyph.\"\"\"\n\n    ctx: ParserContext\n    filename: str\n    x: int\n    y: int\n    w: int\n    h: int\n    index: int | None = None\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        src_pixels = load_image(self.ctx.input_dir / self.filename)\n        cell_pixels = src_pixels[\n            self.y : self.y + self.h, self.x : self.x + self.w\n        ]\n        pixels, removed = trim_transparent_pixels(cell_pixels)\n        bbox = Rect(\n            x=0,\n            y=-15 + removed[0],\n            w=self.w - removed[2] - removed[3],\n            h=self.h - removed[1] - removed[0],\n        )\n        return pixels, bbox\n\n\n@dataclass\nclass ImageSource(BaseSource):\n    \"\"\"A full-image source for the glyph.\"\"\"\n\n    ctx: ParserContext\n    filename: str\n    x1: int\n    y1: int\n    x2: int\n    y2: int\n    index: int | None = None\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        pixels = load_image(self.ctx.input_dir / self.filename)\n        bbox = Rect(self.x1, self.y1, self.x2 - self.x1, self.y2 - self.y1)\n        assert bbox.w == pixels.shape[1]\n        return pixels, bbox\n\n\n@dataclass\nclass RenderSource(BaseSource):\n    \"\"\"A fresh rendering source for the glyph.\"\"\"\n\n    ctx: ParserContext\n    font: str\n    index: int | None = None\n    offset_x: int = 0\n    offset_y: int = 0\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        text = glyph.text\n\n        offset = -23\n        font_size = 18\n        pad = 5\n        font = ImageFont.truetype(DATA_DIR / self.font, font_size)\n\n        # Measure text\n        dummy = Image.new(\"RGBA\", (1, 1))\n        draw = ImageDraw.Draw(dummy)\n        x, y, w, h = draw.textbbox((0, 0), text, font=font)\n\n        img = Image.new(\"RGBA\", (w + pad * 2, h + pad * 2))\n\n        # Shadow\n        img_black = Image.new(\"RGBA\", img.size, (0, 0, 0, 0))\n        draw_b = ImageDraw.Draw(img_black)\n        draw_b.fontmode = \"L\"\n        draw_b.text((pad + 1, pad + 1), text, font=font, fill=(0, 0, 0, 255))\n\n        # Fill\n        img_white = Image.new(\"RGBA\", img.size, (0, 0, 0, 0))\n        draw_w = ImageDraw.Draw(img_white)\n        draw_w.fontmode = \"L\"\n        # Draw twice to establish a stronger weight\n        draw_w.text((pad, pad), text, font=font, fill=(255, 255, 255, 255))\n        draw_w.text((pad, pad), text, font=font, fill=(255, 255, 255, 255))\n\n        # Composite both layers in order\n        img.alpha_composite(img_black)\n        img.alpha_composite(img_white)\n\n        # Trim transparent margins\n        cell_pixels = np.array(img)\n        pixels, removed = trim_transparent_pixels(cell_pixels)\n        bbox = Rect(\n            x=self.offset_x + x,\n            y=self.offset_y + offset + removed[0],\n            w=img.width + 1 - removed[2] - removed[3],\n            h=img.height + 1 - removed[1] - removed[0])\n        return pixels, bbox\n\n\n@dataclass\nclass LinkSource(BaseSource):\n    \"\"\"A source pointing to another glyph's source.\"\"\"\n\n    ctx: ParserContext\n    link_to: str\n    linked_source: BaseSource | None = None\n    index: int | None = None\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        return self.linked_source.load(glyph)\n\n\nclass CombineSource(BaseSource):\n    \"\"\"A source merging two glyphs into one.\"\"\"\n\n    def __init__(\n        self,\n        ctx: ParserContext,\n        glyph1: str,\n        glyph2: str,\n        offset_x: int = 0,\n        offset_y: int = 0,\n        align: str | None = \"top\",\n    ) -> None:\n        self.glyph1 = glyph1\n        self.glyph2 = glyph2\n        self.offset_x = offset_x\n        self.offset_y = offset_y\n        self.align = align\n\n        self.index: int | None = None\n        self.glyph1_source: BaseSource | None = None\n        self.glyph2_source: BaseSource | None = None\n        self._cached_render: tuple[np.ndarray, Rect] | None = None\n\n    def load(self, glyph: Glyph) -> tuple[np.ndarray, Rect]:\n        assert self.glyph1_source is not None\n        assert self.glyph2_source is not None\n\n        if self._cached_render is not None:\n            return self._cached_render\n\n        main_pixels, main_bbox = self.glyph1_source.load(glyph)\n        combining_pixels, combining_bbox = self.glyph2_source.load(glyph)\n        offset_x, offset_y = self._compute_offset(main_bbox, combining_bbox)\n        composed_pixels, composed_bbox = self._compose_pixels(\n            main_pixels,\n            main_bbox,\n            combining_pixels,\n            combining_bbox,\n            offset_x,\n            offset_y,\n        )\n        self._cached_render = (composed_pixels, composed_bbox)\n        return self._cached_render\n\n    def _compute_offset(self, main_bbox: Rect, combining_bbox: Rect) -> tuple[int, int]:\n        offset_x = self.offset_x\n        offset_y = self.offset_y\n\n        offset_x += (main_bbox.w - combining_bbox.w) // 2\n        if self.align == \"top\":\n            offset_y += main_bbox.y - combining_bbox.y - combining_bbox.h\n        elif self.align == \"center\":\n            offset_y += (main_bbox.y - combining_bbox.y) + (\n                main_bbox.h - combining_bbox.h\n            ) // 2\n        elif self.align == \"bottom\":\n            offset_y += main_bbox.y + main_bbox.h - combining_bbox.y\n        return offset_x, offset_y\n\n    def _compose_pixels(\n        self,\n        main_pixels: np.ndarray,\n        main_bbox: Rect,\n        combining_pixels: np.ndarray,\n        combining_bbox: Rect,\n        offset_x: int,\n        offset_y: int,\n    ) -> tuple[np.ndarray, Rect]:\n        combining_left = combining_bbox.x + offset_x\n        combining_top = combining_bbox.y + offset_y\n\n        left = min(main_bbox.x, combining_left)\n        top = min(main_bbox.y, combining_top)\n        right = max(main_bbox.x + main_bbox.w, combining_left + combining_bbox.w)\n        bottom = max(main_bbox.y + main_bbox.h, combining_top + combining_bbox.h)\n\n        width = right - left\n        height = bottom - top\n        canvas = np.zeros((height, width, 4), dtype=np.uint8)\n\n        base_x = main_bbox.x - left\n        base_y = main_bbox.y - top\n        accent_x = combining_left - left\n        accent_y = combining_top - top\n\n        canvas[\n            base_y : base_y + main_bbox.h,\n            base_x : base_x + main_bbox.w,\n        ] = main_pixels\n        self._alpha_blit(canvas, combining_pixels, accent_x, accent_y)\n\n        return canvas, Rect(x=left, y=top, w=width, h=height)\n\n    @staticmethod\n    def _alpha_blit(\n        dst: np.ndarray,\n        src: np.ndarray,\n        dst_x: int,\n        dst_y: int,\n    ) -> None:\n        height, width, _ = src.shape\n        region = dst[dst_y : dst_y + height, dst_x : dst_x + width]\n\n        src_rgb = src[:, :, :3].astype(np.float32) / 255.0\n        src_alpha = src[:, :, 3:4].astype(np.float32) / 255.0\n        dst_rgb = region[:, :, :3].astype(np.float32) / 255.0\n        dst_alpha = region[:, :, 3:4].astype(np.float32) / 255.0\n\n        out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha)\n        safe_alpha = np.where(out_alpha == 0.0, 1.0, out_alpha)\n        out_rgb = (\n            src_rgb * src_alpha + dst_rgb * dst_alpha * (1.0 - src_alpha)\n        ) / safe_alpha\n\n        region[:, :, :3] = np.clip(out_rgb * 255.0, 0, 255).astype(np.uint8)\n        region[:, :, 3] = np.clip(out_alpha * 255.0, 0, 255).astype(np.uint8)[\n            :, :, 0\n        ]\n\n\nclass BaseModifier:\n    def modify_glyph(self, glyph: Glyph) -> None:\n        pass\n\n\n@dataclass\nclass ExpandModifier(BaseModifier):\n    w: int = 0\n\n    def modify_glyph(self, glyph: Glyph) -> None:\n        glyph.extra_width += self.w\n\n\n@dataclass\nclass TranslateModifier(BaseModifier):\n    x: int = 0\n    y: int = 0\n\n    def modify_glyph(self, glyph: Glyph) -> None:\n        glyph.extra_x += self.x\n        glyph.extra_y += self.y\n\n\n@dataclass\nclass Glyph:\n    text: str\n    glyph_class: str\n    font_idx: int\n    source: BaseSource\n    modifiers: list[BaseModifier]\n\n    extra_width: int = 0\n    extra_x: int = 0\n    extra_y: int = 0\n\n    def __str__(self) -> str:\n        if len(self.text) == 1:\n            return f\"U+{ord(self.text):04X}:{self.text}\"\n        return self.text\n\n\nclass GlyphParser(Transformer):\n    \"\"\"Parse the mapping.txt file into Python objects.\"\"\"\n\n    def __init__(self, ctx: ParserContext) -> None:\n        self.ctx = ctx\n        return super().__init__()\n\n    GLYPH_CLASS = lambda self, items: items[0]\n    font_idx = lambda self, items: items[0]\n\n    glyph_text = lambda self, items: items[0]\n    arglist = lambda self, items: items\n    kwarg = lambda self, items: (items[0].value, items[1])\n    func_kwargs = lambda self, items: dict(items)\n\n    def func_params(self, items):\n        args = []\n        kwargs = {}\n        for arg in items[0]:\n            if isinstance(arg, tuple):\n                kwargs[arg[0]] = arg[1]\n            else:\n                args.append(arg)\n        return (args, kwargs)\n\n    number = lambda self, items: int(items[0])\n    quoted_string = lambda self, items: ast.literal_eval(items[0].value)\n\n    def start(self, items) -> list[Glyph]:\n        return sum(items, [])\n\n    def unicode_definition(self, items):\n        codepoint, char = items\n        char = char.value\n        decoded_codepoint = chr(int(codepoint, 16))\n        assert decoded_codepoint == char, (\n            f\"Mismatching unicode codepoint for {char}: \"\n            f\"U+{ord(decoded_codepoint):04X} != U+{ord(char):04X}\"\n        )\n        return decoded_codepoint\n\n    def include(self, items):\n        return _read_mapping(self.ctx, filename=items[0])\n\n    def command(self, items):\n        text, font_idx, glyph_class, source, *modifiers = items\n        return [\n            Glyph(\n                text=text,\n                font_idx=font_idx,\n                glyph_class=glyph_class,\n                source=source,\n                modifiers=modifiers,\n            )\n        ]\n\n    def source_func(self, items):\n        func, args, kwargs = items[0]\n        return func(self.ctx, *args, **kwargs)\n\n    grid_sprite_func = lambda self, items: (GridSpriteSource, *items[0])\n    manual_sprite_func = lambda self, items: (ManualSpriteSource, *items[0])\n    image_func = lambda self, items: (ImageSource, *items[0])\n    combine_func = lambda self, items: (CombineSource, *items[0])\n    link_func = lambda self, items: (LinkSource, *items[0])\n    render_func = lambda self, items: (RenderSource, *items[0])\n\n    def modifier_func(self, items):\n        func, args, kwargs = items[0]\n        return func(*args, **kwargs)\n\n    expand_func = lambda self, items: (ExpandModifier, *items[0])\n    translate_func = lambda self, items: (TranslateModifier, *items[0])\n\n\ndef _read_mapping(ctx, filename: str = \"mapping.txt\") -> list[Glyph]:\n    \"\"\"Read and parse the .txt mapping file.\"\"\"\n    text = (ctx.input_dir / filename).read_text()\n    tree = GLYPH_GRAMMAR.parse(text)\n    results = GlyphParser(ctx).transform(tree)\n    return results\n\n\ndef reindex_sprites(glyphs: list[Glyph]) -> None:\n    \"\"\"Assign sprite indices; non-default fonts reuse base font indices.\"\"\"\n    font_glyphs_map: dict[int, list[Glyph]] = defaultdict(list)\n    for glyph in glyphs:\n        font_glyphs_map[glyph.font_idx].append(glyph)\n\n    base_glyphs = font_glyphs_map[0]\n    sources = [g.source for g in base_glyphs]\n\n    used = sorted({s.index for s in sources if s.index is not None})\n    next_index = 0\n\n    for source in sources:\n        if source.index is None:\n            while next_index in used:\n                next_index += 1\n            source.index = next_index\n            used.append(next_index)\n            used.sort()\n\n    base_index_map = {glyph.text: glyph.source.index for glyph in base_glyphs}\n    for glyph in glyphs:\n        if glyph.font_idx == 0:\n            continue\n        base_idx = base_index_map.get(glyph.text)\n        if base_idx is None:\n            raise ValueError(\n                f\"Glyph {glyph.text} (font {glyph.font_idx}) missing base font index\"\n            )\n        glyph.source.index = base_idx\n\n    for font_idx, font_glyphs in font_glyphs_map.items():\n        sources = [g.source for g in font_glyphs]\n        indices = [source.index for source in sources]\n        if indices:\n            assert len(set(indices)) == len(indices), \"Duplicate sprite indices found!\"\n            if font_idx == 0:\n                assert sorted(indices)[-1] + 1 == len(indices), \"Found gaps in sprite indices!\"\n\n\ndef resolve_links(glyphs: list[Glyph]) -> None:\n    \"\"\"Resolve glyph sources so that they know of their source sprites.\"\"\"\n\n    def _lookup(text: str, font_idx: int) -> Glyph:\n        glyph = glyph_map.get((text, font_idx))\n        if glyph is not None:\n            return glyph\n        glyph = glyph_map.get((text, 0))\n        if glyph is not None:\n            return glyph\n        raise KeyError(f\"Unable to resolve glyph {text} (font {font_idx})\")\n\n    glyph_map = {(glyph.text, glyph.font_idx): glyph for glyph in glyphs}\n    for glyph in glyphs:\n        if isinstance(glyph.source, LinkSource):\n            glyph.source.linked_source = _lookup(\n                glyph.source.link_to, glyph.font_idx\n            ).source\n    for glyph in glyphs:\n        if isinstance(glyph.source, CombineSource):\n            glyph.source.glyph1_source = _lookup(\n                glyph.source.glyph1, glyph.font_idx\n            ).source\n            glyph.source.glyph2_source = _lookup(\n                glyph.source.glyph2, glyph.font_idx\n            ).source\n\n\ndef apply_modifiers(glyphs: list[Glyph]) -> None:\n    for glyph in glyphs:\n        for modifier in glyph.modifiers:\n            modifier.modify_glyph(glyph)\n\n\ndef get_glyph_map(input_dir: Path) -> list[Glyph]:\n    glyphs = _read_mapping(ctx=ParserContext(input_dir=input_dir))\n    reindex_sprites(glyphs)\n    resolve_links(glyphs)\n    apply_modifiers(glyphs)\n    return glyphs\n"
  },
  {
    "path": "tools/shared/icons.py",
    "content": "import tempfile\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom subprocess import check_call\n\n\n@dataclass\nclass IconSpec:\n    size: int\n    type: str\n\n\nSPECS = [\n    IconSpec(size=32, type=\"bmp\"),\n    IconSpec(size=16, type=\"bmp\"),\n    IconSpec(size=256, type=\"png\"),\n    IconSpec(size=128, type=\"png\"),\n    IconSpec(size=64, type=\"png\"),\n    IconSpec(size=48, type=\"png\"),\n    IconSpec(size=32, type=\"png\"),\n    IconSpec(size=16, type=\"png\"),\n]\n\n\ndef resize_transformer(path: Path, spec: IconSpec) -> None:\n    check_call(\n        [\n            \"convert\",\n            f\"{path}[0]\",\n            \"-filter\",\n            \"lanczos\",\n            \"-resize\",\n            f\"{spec.size}x{spec.size}\",\n            f\"PNG:{path}\",\n        ]\n    )\n\n\ndef quantize_transformer(path: Path, spec: IconSpec) -> None:\n    quantized_path = path.with_stem(f\"{path.stem}-quantized\")\n    check_call([\"pngquant\", path, \"--output\", quantized_path])\n    path.write_bytes(quantized_path.read_bytes())\n    quantized_path.unlink()\n\n\ndef optimize_transformer(path: Path, spec: IconSpec) -> None:\n    check_call([\"zopflipng\", \"-y\", path, path])\n\n\ndef convert_transformer(path: Path, spec: IconSpec) -> None:\n    if spec.type != \"png\":\n        check_call([\"convert\", path, f\"{spec.type.upper()}:{path}\"])\n\n\nTRANSFORMERS = [\n    resize_transformer,\n    quantize_transformer,\n    optimize_transformer,\n    convert_transformer,\n]\n\n\ndef generate_icon(source_path: Path, target_path: Path) -> None:\n    aux_paths = []\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        tmp_path = Path(tmpdir)\n        for spec in SPECS:\n            aux_path = tmp_path / f\"{spec.size}-{spec.type}.tmp\"\n            aux_path.write_bytes(source_path.read_bytes())\n            for transform in TRANSFORMERS:\n                transform(aux_path, spec)\n\n            aux_paths.append(aux_path)\n\n        # NOTE: image order is important for certain software.\n        check_call([\"identify\", *aux_paths])\n        check_call([\"convert\", *aux_paths, target_path])\n"
  },
  {
    "path": "tools/shared/ida_progress.py",
    "content": "#!/usr/bin/env python3\nimport re\nfrom dataclasses import dataclass\nfrom enum import StrEnum, auto\nfrom pathlib import Path\n\nfrom parsimonious.exceptions import ParseError\nfrom parsimonious.grammar import Grammar\nfrom parsimonious.nodes import NodeVisitor\n\n# Define a simple grammar for C declarations\ngrammar = Grammar(\n    r\"\"\"\n    output = decl \";\" (_ ~\"//.*\")?\n    decl = function_decl / var_decl / function_pointer_decl\n    function_decl = type _ function_name _ parameters\n    var_decl = type _ var_name array_subscript (_ \"=\" _ ~\"[^;]*\")?\n    function_pointer_decl = type _ (\"(*\" _ qualifier _ function_name _ array_subscript _ \")\") _ parameters\n\n    var_name = identifier\n    function_name = identifier\n    parameters = (\"(\" _ \"void\" _ \")\") / (\"(\" _ (parameter_list?) _ \")\")\n    parameter_list = parameter _ (\",\" _ parameter _)*\n    parameter = decl / type\n    qualifier = ~\"const|__cdecl|__stdcall|__thiscall|__fastcall\"\n    array_subscript = (_ \"[\" number? \"]\")*\n\n    type = (qualifier _)? ~r\"[a-zA-Z_][a-zA-Z0-9_]*\" _ (qualifier _)? \"*\"* (qualifier _)?\n\n    number = ~\"0[0xX][0-9a-fA-F]+\" / ~\"[0-9]+\"\n    identifier = ~r\"[a-zA-Z_][a-zA-Z0-9_]*\"\n    _ = ~r\"\\s*\"\n    \"\"\"\n)\n\n\nclass DeclarationVisitor(NodeVisitor):\n    def visit_output(self, node, visited_children):\n        return visited_children[0]\n\n    def visit_decl(self, node, visited_children):\n        return visited_children[0]\n\n    def visit_function_decl(self, node, visited_children):\n        return visited_children[2]\n\n    def visit_function_pointer_decl(self, node, visited_children):\n        return visited_children[2][4]\n\n    def visit_identifier(self, node, visited_children):\n        return node.text\n\n    def visit_var_decl(self, node, visited_children):\n        return visited_children[2]\n\n    def visit_function_name(self, node, visited_children):\n        return node\n\n    def visit_var_name(self, node, visited_children):\n        return node\n\n    def generic_visit(self, node, visited_children):\n        return visited_children or node\n\n\ndef extract_symbol_name(c_declaration: str) -> str | None:\n    result: str | None\n    try:\n        visitor = DeclarationVisitor()\n        tree = grammar.parse(c_declaration)\n        result = visitor.visit(tree)\n    except ParseError:\n        result = None\n    return result\n\n\nclass ProgressFileSection(StrEnum):\n    TYPES = \"types\"\n    FUNCTIONS = \"functions\"\n    VARIABLES = \"variables\"\n\n\n@dataclass\nclass Symbol:\n    offset: int\n    signature: str\n    size: int | None = None\n\n    @property\n    def name(self) -> str:\n        return extract_symbol_name(self.signature)\n\n    @property\n    def offset_str(self) -> str:\n        return f\"0x{self.offset:08X}\"\n\n    @property\n    def is_known(self) -> bool:\n        return not re.search(r\"(\\s|^)sub_\", self.signature)\n\n\n@dataclass\nclass ProgressFile:\n    types: list[str]\n    functions: list[Symbol]\n    variables: list[Symbol]\n\n\ndef to_int(source: str) -> int | None:\n    source = source.strip()\n    if source.startswith(\"/*\"):\n        source = source[2:]\n    if source.endswith(\"*/\"):\n        source = source[:-2]\n    source = source.strip()\n    if not source.replace(\"-\", \"\"):\n        return None\n    if source.startswith((\"0x\", \"0X\")):\n        source = source[2:]\n    return int(source, 16)\n\n\ndef parse_progress_file(path: Path) -> ProgressFile:\n    result = ProgressFile(types=[], functions=[], variables=[])\n\n    type_contents = \"\"\n\n    section: ProgressFileSection | None = None\n    for line in path.read_text(encoding=\"utf-8\").splitlines():\n        if match := re.match(\"^# ([A-Z]+)$\", line.strip()):\n            section_name = match.group(1).lower()\n            if section_name in list(ProgressFileSection):\n                section = ProgressFileSection(section_name)\n\n        if line.strip().startswith(\"#\"):\n            continue\n\n        if section == ProgressFileSection.TYPES:\n            type_contents += line + \"\\n\"\n\n        line = line.strip()\n        if not line:\n            continue\n\n        if section == ProgressFileSection.FUNCTIONS:\n            offset, size, signature = re.split(r\"\\s+\", line, maxsplit=2)\n            result.functions.append(\n                Symbol(\n                    signature=signature,\n                    offset=to_int(offset),\n                    size=to_int(size),\n                )\n            )\n\n        if section == ProgressFileSection.VARIABLES:\n            offset, signature = re.split(r\"\\s+\", line, maxsplit=1)\n            result.variables.append(\n                Symbol(\n                    signature=signature,\n                    offset=to_int(offset),\n                )\n            )\n\n    result.types = [\n        definition\n        for definition in re.split(r\"\\n\\n(?!\\s)\", type_contents, flags=re.M)\n        if definition.strip()\n    ]\n    return result\n"
  },
  {
    "path": "tools/shared/import_sorter.py",
    "content": "import functools\nimport re\nfrom collections.abc import Iterable\nfrom pathlib import Path\nfrom shutil import which\nfrom subprocess import run\n\nfrom shared.files import find_versioned_files\n\n\ndef custom_sort(source: Iterable[str], forced_order: list[str]) -> list[str]:\n    def key_func(item: str) -> tuple[str, int]:\n        if item in forced_order:\n            return (forced_order[0], forced_order.index(item))\n        return (item, 0)\n\n    return sorted(source, key=key_func)\n\n\ndef sort_imports_single_file(\n    path: Path,\n    root_dir: Path,\n    own_include_map: dict[str, str],\n    fix_map: dict[str, str],\n    forced_order: list[str],\n) -> None:\n    source = path.read_text()\n    try:\n        rel_path = path.relative_to(root_dir)\n    except ValueError:\n        return\n\n    own_include = str(rel_path.with_suffix(\".h\"))\n    own_include = own_include_map.get(str(rel_path), own_include)\n\n    for key, value in fix_map.items():\n        source = re.sub(\n            r'(#include [\"<])' + re.escape(key) + '([\">])',\n            r\"\\1\" + value + r\"\\2\",\n            source,\n        )\n\n    def cb(match):\n        includes = re.findall(r'#include ([\"<][^\"<>]+[\">])', match.group(0))\n        groups = {\n            \"self\": set(),\n            \"local\": set(),\n            \"shared\": set(),\n            \"external\": set(),\n        }\n        for include in includes:\n            if include.strip('\"<>') == own_include:\n                groups[\"self\"].add(include)\n            elif include.startswith(\"<trx\"):\n                groups[\"shared\"].add(include)\n            elif include.startswith(\"<\"):\n                groups[\"external\"].add(include)\n            elif include.startswith('\"'):\n                groups[\"local\"].add(include)\n\n        groups = {key: value for key, value in groups.items() if value}\n\n        ret = \"\\n\\n\".join(\n            \"\\n\".join(\n                f\"#include {include}\"\n                for include in custom_sort(group, forced_order)\n            )\n            for group in groups.values()\n        ).strip()\n        return ret\n\n    source = re.sub(\n        \"^#include [^\\n]+(\\n*#include [^\\n]+)*\",\n        cb,\n        source,\n        flags=re.M,\n    )\n    if source != path.read_text():\n        path.write_text(source)\n\n\ndef sort_imports(\n    root_dir: Path,\n    system_include_dirs: list[Path],\n    paths: list[Path],\n    own_include_map: dict[str, str],\n    fix_map: dict[str, str],\n    forced_order: list[str],\n) -> None:\n    for path in paths:\n        sort_imports_single_file(\n            path,\n            root_dir=root_dir,\n            own_include_map=own_include_map,\n            fix_map=fix_map,\n            forced_order=forced_order,\n        )\n"
  },
  {
    "path": "tools/shared/json_utils.py",
    "content": "\"\"\"Utilities for working with JSON/JSON5 files.\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import Any\n\ntry:\n    import pyjson5\nexcept ImportError:\n    pyjson5 = None\n\n\ndef load_json5(path: Path):\n    \"\"\"Load and parse a JSON5 file as Python data structures. Requires `pyjson5`.\"\"\"\n    return load_json5_from_string(path.read_text(encoding=\"utf-8\"))\n\n\ndef load_json5_from_string(content: str):\n    if pyjson5 is None:\n        raise RuntimeError(\"pyjson5 is required to parse JSON5 files\")\n    return pyjson5.loads(content)\n\n\ndef write_json_to_string(data) -> str:\n    return json.dumps(data, ensure_ascii=False, indent=4)\n\n\ndef write_json(path: Path, data) -> None:\n    \"\"\"Serialize `data` as pretty-printed JSON to `path`, with UTF-8 encoding.\"\"\"\n    new_content = write_json_to_string(data) + \"\\n\"\n    if path.exists ( ) and new_content != path.read_text():\n        path.write_text(new_content, encoding=\"utf-8\")\n\n\nclass JSONPointers:\n    \"\"\"\n    Tiny helper around RFC-6901 JSON Pointers.\n\n        jp = JSONPointers(data)\n        leaves = list(jp)      # every reachable leaf pointer\n        value  = jp[\"/foo/0\"]  # read\n        jp[\"/foo/0\"] = \"BAR\"    # write (in-place)\n\n    Works with any mix of dict / list containers.\n    \"\"\"\n\n    def __init__(self, data: Any) -> None:\n        self._root = data\n        self._pointers: list[str] | None = None  # lazy-built\n\n    def __iter__(self) -> list[str]:\n        \"\"\"Return all JSON-Pointer strings that lead to a *leaf* value.\"\"\"\n        if self._pointers is None:\n            self._pointers = []\n            self._walk(self._root, [])\n        yield from self._pointers.copy()\n\n    def get(self, ptr: str, default: Any | None = None) -> Any:\n        try:\n            return self[ptr]\n        except (KeyError, IndexError):\n            return default\n\n    def __getitem__(self, ptr: str) -> Any:\n        \"\"\"Return the value at *ptr* (raises KeyError / IndexError if missing).\"\"\"\n        node = self._root\n        for token in self._split(ptr):\n            node = node[self._index_or_key(node, token)]\n        return node\n\n    def __setitem__(self, ptr: str, value: Any) -> None:\n        \"\"\"Replace the value at *ptr* with *value* (in-place), auto-creating\n        missing dict or list containers as needed.\"\"\"\n        tokens = self._split(ptr)\n        if not tokens:\n            raise ValueError(\n                \"Cannot replace the root object through set_path()\"\n            )\n\n        node = self._root\n        for idx, token in enumerate(tokens[:-1]):\n            next_index = tokens[idx + 1].lstrip(\"-\").isdigit()\n            key = self._index_or_key(node, token)\n            node = self._ensure_container(node, key, next_index)\n\n        last_key = self._index_or_key(node, tokens[-1])\n        if isinstance(node, list):\n            while last_key >= len(node):\n                node.append(None)\n        node[last_key] = value\n        # invalidate cached pointers: tree shape may have changed\n        self._pointers = None\n\n    def __delitem__(self, ptr: str) -> None:\n        \"\"\"Remove the value at *ptr* (in-place), deleting the key or list element.\"\"\"\n        tokens = self._split(ptr)\n        if not tokens:\n            raise ValueError(\"Cannot delete the root object\")\n\n        node = self._root\n        for token in tokens[:-1]:\n            node = node[self._index_or_key(node, token)]\n\n        last = tokens[-1]\n        key = self._index_or_key(node, last)\n        if isinstance(node, dict):\n            node.pop(key, None)\n        elif isinstance(node, list):\n            del node[key]\n        else:\n            raise TypeError(\n                f\"Cannot delete at non-container at token '{last}'\"\n            )\n        self._pointers = None\n\n    def _ensure_container(\n        self, node: Any, key: str | int, is_index: bool\n    ) -> Any:\n        \"\"\"Ensure node[key] exists as dict or list based on is_index, and return it.\"\"\"\n        if isinstance(node, dict):\n            if key not in node or not isinstance(node[key], (dict, list)):\n                node[key] = [] if is_index else {}\n            return node[key]\n        if isinstance(node, list):\n            while key >= len(node):\n                node.append([] if is_index else {})\n            if not isinstance(node[key], (dict, list)):\n                node[key] = [] if is_index else {}\n            return node[key]\n        raise TypeError(f\"Cannot traverse into non-container at token '{key}'\")\n\n    @property\n    def data(self) -> Any:\n        return self._root\n\n    def _walk(self, node: Any, path: list[str | int]) -> None:\n        \"\"\"DFS that records every *leaf* pointer.\"\"\"\n        if isinstance(node, dict):\n            for k, v in node.items():\n                self._walk(v, path + [k])\n        elif isinstance(node, list):\n            for i, v in enumerate(node):\n                self._walk(v, path + [i])\n        else:\n            self._pointers.append(self._encode(path))  # type: ignore[arg-type]\n\n    @staticmethod\n    def _split(ptr: str) -> list[str]:\n        if ptr == \"\":\n            return []\n        if not ptr.startswith(\"/\"):\n            raise ValueError(f\"Invalid JSON Pointer: {ptr!r}\")\n        return [\n            token.replace(\"~1\", \"/\").replace(\"~0\", \"~\")\n            for token in ptr.lstrip(\"/\").split(\"/\")\n        ]\n\n    @staticmethod\n    def _encode(path: list[str | int]) -> str:\n        esc = lambda s: str(s).replace(\"~\", \"~0\").replace(\"/\", \"~1\")\n        return \"/\" + \"/\".join(esc(p) for p in path)\n\n    @staticmethod\n    def _index_or_key(node: Any, token: str) -> str | int:\n        # if we're in a list and token looks like an int → use int index\n        if isinstance(node, list) and token.lstrip(\"-\").isdigit():\n            return int(token)\n        return token\n"
  },
  {
    "path": "tools/shared/linting.py",
    "content": "#!/usr/bin/env python3\nimport json\nimport re\nimport sys\nfrom collections.abc import Callable, Iterable\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nfrom shared.paths import PROJECT_PATHS, CommonPaths\n\n# enable importing translation_utils from this directory\nsys.path.insert(0, str(Path(__file__).resolve().parent))\n\nimport jsonschema\nfrom json_utils import load_json5\n\nSHARED_PROJECT = \"libtrx\"\nCHILD_PROJECTS = [\"tr1\", \"tr2\"]\nPROJECTS = [SHARED_PROJECT] + CHILD_PROJECTS\n\n\n@dataclass\nclass LintContext:\n    root_dir: Path\n    versioned_files: list[Path]\n\n\n@dataclass\nclass GameString:\n    project: str\n    path: Path\n    key: str\n    text: str\n\n\n@dataclass\nclass LintWarning:\n    path: Path\n    message: str\n    line: int | None = None\n\n    def __str__(self) -> str:\n        prefix = str(self.path)\n        if self.line is not None:\n            prefix += f\":{self.line}\"\n        return f\"{prefix}: {self.message}\"\n\n\ndef lint_json_validity(\n    context: LintContext, path: Path\n) -> Iterable[LintWarning]:\n    if path.suffix != \".json\":\n        return\n    try:\n        json.loads(path.read_text())\n    except json.JSONDecodeError as ex:\n        yield LintWarning(path, f\"malformed JSON: {ex!s}\")\n\n\ndef lint_newlines(context: LintContext, path: Path) -> Iterable[LintWarning]:\n    text = path.read_text(encoding=\"utf-8\")\n    if not text:\n        return\n    if not text.endswith(\"\\n\"):\n        yield LintWarning(path, \"missing newline character at end of file\")\n    if text.endswith(\"\\n\\n\"):\n        yield LintWarning(path, \"extra newline character at end of file\")\n\n\ndef lint_trailing_whitespace(\n    context: LintContext, path: Path\n) -> Iterable[LintWarning]:\n    if path.suffix == \".md\":\n        return\n    for i, line in enumerate(path.open(\"r\"), 1):\n        if line.rstrip(\"\\n\").endswith(\" \"):\n            yield LintWarning(path, \"trailing whitespace\", line=i)\n\n\ndef lint_const_primitives(\n    context: LintContext, path: Path\n) -> Iterable[LintWarning]:\n    if path.suffix != \".h\":\n        return\n    for i, line in enumerate(path.open(\"r\"), 1):\n        if re.search(r\"const (int[a-z0-9_]*|bool)\\b\\s*[a-z]\", line):\n            yield LintWarning(path, \"useless const\", line=i)\n        if re.search(r\"\\*\\s*const(?!\\s?\\*)\", line):\n            yield LintWarning(path, \"useless const\", line=i)\n\n\ndef lint_meson_build_sort_order(\n    context: LintContext, path: Path\n) -> Iterable[LintWarning]:\n    if path.name != \"meson.build\" or path.parent.name == \"dwarfstack\":\n        return\n    pattern = re.compile(r\"\\bcommon_sources\\s*=\\s*\\[(.*?)\\]\", re.S)\n    match = pattern.search(path.read_text())\n\n    if not match:\n        yield LintWarning(path, \"unable to find sources array\")\n    else:\n        block = match.group(1)\n        lines = [l.strip().strip(\",\") for l in block.splitlines() if l.strip()]\n        if not lines:\n            yield LintWarning(path, \"unable to parse sources array\")\n        else:\n            clean = [l.strip(\"'\") for l in lines]\n            init = [l for l in clean if l == \"init\"]\n            resources = [l for l in clean if l == \"resources\"]\n            middle = [l for l in clean if l not in (\"init\", \"resources\")]\n            is_sorted = clean == init + sorted(middle) + resources\n            if not is_sorted:\n                yield LintWarning(path, \"source list is not ordered\")\n\n\ndef lint_clang_format_markers(\n    context: LintContext, path: Path\n) -> Iterable[LintWarning]:\n    if path.suffix != \".c\":\n        return\n    clang_format_off_open = False\n    for i, line in enumerate(path.open(\"r\"), 1):\n        if \"// clang-format on\" in line:\n            clang_format_off_open = False\n        if \"// clang-format off\" in line:\n            if clang_format_off_open:\n                yield LintWarning(\n                    path,\n                    \"found `// clang-format off` before previous block was closed \"\n                    \"with `// clang-format on`\",\n                    line=i,\n                )\n            clang_format_off_open = True\n\n\ndef get_relevant_project(context: LintContext, path: Path) -> str:\n    for project, project_path in get_project_paths(context).items():\n        if path.absolute().is_relative_to(project_path.absolute()):\n            break\n    else:\n        raise RuntimeError(f\"{path}: Unable to get project path\")\n    return project\n\n\ndef get_project_paths(context: LintContext) -> dict[str, Path]:\n    return {project: context.src_dir for project in PROJECTS}\n\n\ndef lint_game_flow_schema(context: LintContext):\n    schema_path = CommonPaths.docs_dir / \"gameflow.schema.json\"\n    if not schema_path.exists():\n        return\n    schema = load_json5(schema_path)\n    validator_cls = jsonschema.validators.validator_for(schema)\n    validator = validator_cls(schema=schema)\n    for project, paths in PROJECT_PATHS.items():\n        game_flow_paths = paths.shipped_data_dir.rglob(\"**/gameflow.json5\")\n        for game_flow_path in game_flow_paths:\n            data = load_json5(game_flow_path)\n            for error in validator.iter_errors(instance=data):\n                yield LintWarning(game_flow_path, error)\n\n\ndef _get_known_config_keys(config_map_paths: list[Path]) -> set[str]:\n    known_keys: set[str] = set()\n    # Match the first argument of X_CFG_* macros:\n    # - bare key form: X_CFG_BOOL(audio.master_volume, ...)\n    # - explicit string key form: X_CFG_ENUM_EX(\"ui.airbar_location\", ...)\n    pattern = re.compile(\n        r\"\"\"^X_CFG_[A-Z0-9_]+(?:_EX)?\\(\\s*(?:\"([^\"]+)\"|([a-zA-Z0-9_.]+))\\s*,\"\"\"\n    )\n    for config_map_path in config_map_paths:\n        if not config_map_path.exists():\n            continue\n        for line in config_map_path.read_text(encoding=\"utf-8\").splitlines():\n            stripped = line.strip()\n            if not stripped or stripped.startswith(\"#\"):\n                continue\n            match = pattern.match(stripped)\n            if not match:\n                continue\n            key = match.group(1) or match.group(2)\n            if key:\n                known_keys.add(key)\n    return known_keys\n\n\ndef _get_preset_config_entries(preset_path: Path) -> dict[str, Any]:\n    try:\n        preset_data = load_json5(preset_path)\n    except Exception as ex:\n        raise ValueError(f\"failed to parse JSON5: {ex!s}\") from ex\n\n    if not isinstance(preset_data, dict):\n        raise ValueError(\"root must be an object\")\n\n    config_data = preset_data.get(\"config\")\n    if config_data is None:\n        raise ValueError(\"missing 'config' object\")\n    if not isinstance(config_data, dict):\n        raise ValueError(\"'config' must be an object\")\n    return config_data\n\n\ndef lint_preset_setting_keys(context: LintContext) -> Iterable[LintWarning]:\n    config_dir = CommonPaths.src_dir / \"config\"\n    config_map_paths = [\n        config_dir / \"map.def\",\n        config_dir / \"map_tr1.def\",\n        config_dir / \"map_tr2.def\",\n        config_dir / \"map_tr3.def\",\n    ]\n    known_keys = _get_known_config_keys(config_map_paths)\n    if not known_keys:\n        yield LintWarning(config_dir / \"map.def\", \"unable to parse config keys\")\n        return\n\n    presets_dir = CommonPaths.shipped_data_dir / \"cfg/presets\"\n    if not presets_dir.exists():\n        return\n\n    for preset_path in sorted(presets_dir.glob(\"*.json5\")):\n        try:\n            config_data = _get_preset_config_entries(preset_path)\n        except ValueError as ex:\n            yield LintWarning(preset_path, str(ex))\n            continue\n\n        for key in config_data:\n            if key not in known_keys:\n                yield LintWarning(\n                    preset_path, f\"unknown preset setting key: '{key}'\"\n                )\n\n\nALL_FILE_LINTERS: list[\n    Callable[[LintContext, Path], Iterable[LintWarning]]\n] = [\n    lint_json_validity,\n    lint_newlines,\n    lint_trailing_whitespace,\n    lint_const_primitives,\n    lint_meson_build_sort_order,\n    lint_clang_format_markers,\n]\n\nALL_BULK_LINTERS: list[\n    Callable[[LintContext, list[Path]], Iterable[LintWarning]]\n] = [\n]\n\nALL_REPO_LINTERS: list[\n    Callable[[LintContext, list[Path]], Iterable[LintWarning]]\n] = [\n    lint_game_flow_schema,\n    lint_preset_setting_keys,\n]\n\n\ndef lint_file(context: LintContext, file: Path) -> Iterable[LintWarning]:\n    for linter_func in ALL_FILE_LINTERS:\n        yield from linter_func(context, file)\n\n\ndef lint_bulk_files(\n    context: LintContext, files: list[Path]\n) -> Iterable[LintWarning]:\n    for linter_func in ALL_BULK_LINTERS:\n        yield from linter_func(context, files)\n\n\ndef lint_repo(context: LintContext) -> Iterable[LintWarning]:\n    for linter_func in ALL_REPO_LINTERS:\n        yield from linter_func(context)\n"
  },
  {
    "path": "tools/shared/mac/bundle_dylibs",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport re\nimport shutil\nfrom collections.abc import Iterable\nfrom pathlib import Path\nfrom subprocess import check_output, run\n\nIGNORE_LIB_PREFIXES = (\"/usr/lib/\", \"/System/\", \"@executable_path\")\nRPATH_PATTERN = re.compile(r\"@rpath/(.*)\")\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Copies shared libraries into the macOS app bundle.\"\n    )\n    parser.add_argument(\"-a\", \"--app-name\")\n    parser.add_argument(\n        \"--copy-only\",\n        action=\"store_true\",\n        help=\"Only copy libraries, do not update links.\",\n    )\n    parser.add_argument(\n        \"--links-only\",\n        action=\"store_true\",\n        help=\"Only update links, do not copy libraries.\",\n    )\n    return parser.parse_args()\n\n\ndef should_ignore_lib(lib_path: str) -> bool:\n    return any(lib_path.startswith(prefix) for prefix in IGNORE_LIB_PREFIXES)\n\n\ndef gather_libs(\n    binary_path: Path, visited_paths: set[Path] | None = None\n) -> Iterable[Path]:\n    if visited_paths is None:\n        visited_paths = set()\n    visited_paths.add(binary_path)\n\n    parent_path = binary_path.parent\n    output = check_output([\"otool\", \"-L\", str(binary_path)], text=True)\n    libs = [line.split()[0] for line in output.split(\"\\n\")[1:] if line]\n\n    for lib in libs:\n        match = RPATH_PATTERN.match(lib)\n        lib_path = parent_path / (match.group(1) if match else lib)\n\n        if should_ignore_lib(str(lib_path)) or lib_path == binary_path:\n            continue\n\n        yield lib_path\n        if lib_path not in visited_paths:\n            yield from gather_libs(lib_path, visited_paths=visited_paths)\n\n\ndef copy_libs(frameworks_path: Path, library_paths: set[Path]) -> None:\n    frameworks_path.mkdir(parents=True, exist_ok=True)\n    for lib_path in library_paths:\n        target_path = frameworks_path / lib_path.name\n        if not target_path.exists():\n            print(f\"Copying {lib_path} to {target_path}\")\n            shutil.copy2(lib_path, target_path)\n\n\ndef update_links(binary_path: Path) -> None:\n    output = check_output([\"otool\", \"-L\", str(binary_path)], text=True)\n    libs = [line.split()[0] for line in output.split(\"\\n\")[1:] if line]\n\n    for lib in libs:\n        if should_ignore_lib(lib):\n            continue\n\n        lib_name = Path(lib).name\n        target = f\"@executable_path/../Frameworks/{lib_name}\"\n        print(f\"Updating link for {lib_name} in {binary_path}\")\n        run([\"install_name_tool\", \"-change\", lib, target, str(binary_path)])\n\n        if lib_name == binary_path.name:\n            print(f\"Updating id for {lib_name} in {binary_path}\")\n            run([\"install_name_tool\", \"-id\", target, str(binary_path)])\n\n\ndef main() -> None:\n    args = parse_args()\n\n    app_bundle_path = Path(f\"/tmp/{args.app_name}.app\")\n    app_binary_path = app_bundle_path / f\"Contents/MacOS/TRX\"\n    frameworks_path = app_bundle_path / \"Contents/Frameworks\"\n\n    if args.copy_only or not args.links_only:\n        library_paths = set(gather_libs(app_binary_path))\n        copy_libs(frameworks_path, library_paths)\n\n    if args.links_only or not args.copy_only:\n        for lib_path in frameworks_path.glob(\"*\"):\n            update_links(lib_path)\n\n        update_links(app_binary_path)\n\n    print(f\"Libraries for {args.app_name} copied and updated.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/shared/mac/create_installer",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport os\nimport subprocess\nfrom pathlib import Path\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(description=f\"Create a DMG installer.\")\n    parser.add_argument(\"-a\", \"--app-name\")\n    parser.add_argument(\"-d\", \"--dmg-name\", type=Path)\n    parser.add_argument(\"-i\", \"--icon-path\", type=Path)\n    return parser.parse_args()\n\n\ndef create_dmg(app_name: str, dmg_name: str, icon_path: Path, app_bundle_path: Path) -> None:\n    subprocess.run(\n        (\n            \"create-dmg\",\n            \"--volname\",\n            f\"{app_name} Installer\",\n            \"--volicon\",\n            str(icon_path),\n            \"--window-pos\",\n            \"200\",\n            \"120\",\n            \"--window-size\",\n            \"800\",\n            \"400\",\n            \"--icon-size\",\n            \"100\",\n            \"--icon\",\n            f\"{app_name}.app\",\n            \"200\",\n            \"190\",\n            \"--hide-extension\",\n            f\"{app_name}.app\",\n            \"--app-drop-link\",\n            \"600\",\n            \"185\",\n            str(dmg_name),\n            str(app_bundle_path),\n        )\n    )\n\n\ndef main() -> None:\n    args = parse_args()\n\n    if args.dmg_name.is_file():\n        args.dmg_name.unlink()\n\n    app_bundle_path = Path(f\"/tmp/{args.app_name}.app\")\n    create_dmg(args.app_name, args.dmg_name, args.icon_path, app_bundle_path)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/shared/mac/install_tree",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport os\nimport shutil\nfrom pathlib import Path\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Copies a resource subtree into the macOS app bundle.\"\n    )\n    parser.add_argument(\"--source\", required=True)\n    parser.add_argument(\"--dest\", required=True)\n    return parser.parse_args()\n\n\ndef copy_tree(src: Path, dst: Path, active_dirs: set[Path]) -> None:\n    resolved_src = src.resolve()\n\n    if resolved_src.is_dir():\n        if resolved_src in active_dirs:\n            raise RuntimeError(f\"Refusing to recurse into symlink loop at {src}\")\n\n        active_dirs.add(resolved_src)\n        dst.mkdir(parents=True, exist_ok=True)\n        for child in sorted(resolved_src.iterdir()):\n            copy_tree(child, dst / child.name, active_dirs)\n        active_dirs.remove(resolved_src)\n        return\n\n    dst.parent.mkdir(parents=True, exist_ok=True)\n    shutil.copy2(resolved_src, dst)\n\n\ndef main() -> None:\n    args = parse_args()\n\n    destdir_prefix = os.environ[\"MESON_INSTALL_DESTDIR_PREFIX\"]\n    source = Path(args.source)\n    dest = Path(destdir_prefix) / args.dest\n\n    copy_tree(source, dest, set())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/shared/mac/x86-64_cross_file.txt",
    "content": "[binaries]\nc = ['clang', '--target=x86_64-apple-darwin']\ncpp = ['clang++', '--target=x86_64-apple-darwin']\nar = 'ar'\nstrip = 'strip'\npkg-config = '/opt/local/bin/pkg-config'\n\n[host_machine]\nsystem = 'darwin'\ncpu_family = 'x86_64'\ncpu = 'x86_64'\nendian = 'little'\n"
  },
  {
    "path": "tools/shared/packaging.py",
    "content": "import sys\nimport zipfile\nfrom collections.abc import Iterable\nfrom pathlib import Path\n\n\ndef create_zip(\n    output_path: Path, source_files: Iterable[tuple[Path, str]]\n) -> None:\n    with zipfile.ZipFile(output_path, \"w\") as handle:\n        for source_path, archive_name in source_files:\n            if not source_path.exists():\n                print(\n                    f\"WARNING: {source_path} does not exist\", file=sys.stderr\n                )\n                continue\n            handle.write(source_path, archive_name)\n"
  },
  {
    "path": "tools/shared/paths.py",
    "content": "from pathlib import Path\n\nREPO_DIR = Path(__file__).parent\nwhile REPO_DIR.parent != REPO_DIR and not (REPO_DIR / \".git\").exists():\n    REPO_DIR = REPO_DIR.parent\n\nTOOLS_DIR = REPO_DIR / \"tools\"\nSRC_DIR = REPO_DIR / \"src\"\nDATA_DIR = REPO_DIR / \"data\"\nDOCS_DIR = REPO_DIR / \"docs\"\n\nSHARED_SRC_DIR = SRC_DIR / \"trx\"\n\n\nclass BasePaths:\n    data_dir: Path\n    shipped_data_dir: Path\n    src_dir: Path\n    docs_dir: Path\n\n\nclass ProjectPaths(BasePaths):\n    def __init__(self, folder_name: str) -> None:\n        self.folder_name = folder_name\n\n        self.data_dir = DATA_DIR / folder_name\n        self.shipped_data_dir = self.data_dir / \"ship\"\n        self.tools_dir = TOOLS_DIR / folder_name\n        self.docs_dir = DOCS_DIR / folder_name\n        self.changelog_path = self.docs_dir / \"CHANGELOG.md\"\n\n\nTR1Paths = ProjectPaths(folder_name=\"tr1\")\nTR2Paths = ProjectPaths(folder_name=\"tr2\")\nTR3Paths = ProjectPaths(folder_name=\"tr3\")\n\nCommonPaths = BasePaths()\nCommonPaths.data_dir = DATA_DIR / \"common\"\nCommonPaths.shipped_data_dir = CommonPaths.data_dir / \"ship\"\nCommonPaths.src_dir = SHARED_SRC_DIR\nCommonPaths.changelog_path = DOCS_DIR / \"CHANGELOG.md\"\nCommonPaths.docs_dir = DOCS_DIR\n\nPROJECT_PATHS = {1: TR1Paths, 2: TR2Paths, 3: TR3Paths}\n"
  },
  {
    "path": "tools/shared/utils.py",
    "content": "from collections.abc import Iterable\n\n\ndef uniq[T](source: list[T]) -> list[T]:\n    return list(dict.fromkeys(source))\n\n\ndef chunks[T](generator: Iterable[T], n: int) -> Iterable[list[T]]:\n    \"\"\"Yield successive chunks from a generator.\"\"\"\n    chunk = []\n    for item in generator:\n        chunk.append(item)\n        if len(chunk) == n:\n            yield chunk\n            chunk = []\n    if chunk:\n        yield chunk\n"
  },
  {
    "path": "tools/shared/versioning.py",
    "content": "import re\nfrom pathlib import Path\n\nfrom shared.git import Git\n\n\ndef generate_version(repo_dir: Path | None = None) -> str:\n    git = Git(repo_dir=repo_dir)\n    return (\n        re.sub(\n            \"^trx-\",\n            \"\",\n            git.get_branch_version(pattern=\"trx-*\", branch=None),\n        )\n        or \"?\"\n    )\n\n\ndef generate_package_name(\n    engine_version: str, platform: str, game_version: int | None\n) -> str:\n    if platform == \"win\":\n        platform = \"windows\"\n    elif platform == \"win-installer\":\n        platform = \"windows_installer\"\n    platform = platform.title()\n    parts = [\"TRX\", engine_version, platform]\n    if game_version is not None:\n        parts.append(f\"tr{game_version}\")\n    return \"-\".join(parts)\n"
  },
  {
    "path": "tools/shared/vfs.py",
    "content": "import difflib\nfrom pathlib import Path\n\n\nclass VirtualFilesystem:\n    \"\"\"Lazy disk operations to avoid disk thrashing.\"\"\"\n\n    def __init__(self) -> None:\n        self.files = {}\n\n    def get(self, path: Path) -> None:\n        if content := self.files.get(path):\n            return content\n        content = path.read_text()\n        self.files[path] = content\n        return content\n\n    def put(self, path: Path, content: str) -> None:\n        self.files[path] = content\n\n    def show_diff(self) -> bool:\n        has_changes = False\n        for path, new_content in self.files.items():\n            if path.exists():\n                old_content = path.read_text()\n            else:\n                old_content = None\n            if old_content != new_content:\n                print(\n                    \"\".join(\n                        difflib.unified_diff(\n                            (\n                                old_content.splitlines(keepends=True)\n                                if old_content\n                                else []\n                            ),\n                            new_content.splitlines(keepends=True),\n                            fromfile=str(path),\n                            tofile=str(path),\n                        )\n                    )\n                )\n                has_changes = True\n        return has_changes\n\n    def commit(self) -> None:\n        for path, content in self.files.items():\n            if not path.exists() or path.read_text() != content:\n                path.parent.mkdir(parents=True, exist_ok=True)\n                path.write_text(content)\n"
  },
  {
    "path": "tools/sort_imports",
    "content": "#!/usr/bin/env python3\nimport argparse\nfrom pathlib import Path\n\nfrom shared.files import find_versioned_files, is_binary_file\nfrom shared.import_sorter import sort_imports\nfrom shared.paths import SHARED_SRC_DIR, SRC_DIR, TR1Paths, TR2Paths\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"path\", type=Path, nargs=\"*\")\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    if args.path:\n        paths = [path.absolute() for path in args.path]\n    else:\n        paths = [\n            path\n            for path in find_versioned_files(SRC_DIR)\n            if path.suffix in [\".c\", \".h\"]\n        ]\n\n    sort_imports(\n        paths=[path for path in paths if path.is_relative_to(SHARED_SRC_DIR)],\n        root_dir=SRC_DIR,\n        system_include_dirs=[],\n        own_include_map={\n            \"trx/core/json/bson_write.c\": \"trx/core/bson.h\",\n            \"trx/core/json/bson_parse.c\": \"trx/core/bson.h\",\n            \"trx/core/json/json_base.c\": \"trx/core/json.h\",\n            \"trx/core/json/json_write.c\": \"trx/core/json.h\",\n            \"trx/core/json/json_parse.c\": \"trx/core/json.h\",\n            \"trx/core/log_unknown.c\": \"trx/core/log.h\",\n            \"trx/core/log_linux.c\": \"trx/core/log.h\",\n            \"trx/core/log_windows.c\": \"trx/core/log.h\",\n            \"trx/av/audio.c\": \"trx/av/audio_internal.h\",\n            \"trx/av/audio_reverb.c\": \"trx/av/audio_internal.h\",\n            \"trx/av/audio_sample.c\": \"trx/av/audio_internal.h\",\n            \"trx/av/audio_stream.c\": \"trx/av/audio_internal.h\",\n        },\n        fix_map={},\n        forced_order=[\n            \"<dsound.h>\",\n            \"<windows.h>\",\n            \"<dbghelp.h>\",\n            \"<tlhelp32.h>\",\n        ],\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/tr2/__init__.py",
    "content": ""
  },
  {
    "path": "tools/tr2/generate_ida_importer",
    "content": "#!/usr/bin/env python3\n\"\"\"Converts symbols.txt to an IDC script usable with IDA Free, that propagates\nthe IDA database with typing information, function declarations and variable\ndeclarations.\n\"\"\"\nimport argparse\nimport json\nimport re\nimport tempfile\nfrom pathlib import Path\n\nimport regex\nfrom shared.ida_progress import Symbol, parse_progress_file\nfrom shared.paths import TR2Paths\n\n\ndef generate_types(types: list[str], file) -> None:\n    for definition in types:\n        # strip comments\n        definition = \" \".join(\n            re.sub(r\"//.*\", \"\", line.strip())\n            for line in definition.splitlines()\n        )\n\n        # merge consecutive whitespace\n        definition = re.sub(r\"\\s\\s+\", \" \", definition)\n\n        # convert: typedef struct { … } FOO;\n        # to:      typedef struct FOO { … } FOO;\n        # for readability purposes.\n        if match := re.search(\n            r\"(?P<prefix>typedef\\s+(?:struct|enum)(?:\\s+__\\S+)?\\s*)(?P<body>{.*})(?P<suffix>\\s+(?P<name>\\S+);)\",\n            definition,\n            flags=re.M | re.DOTALL,\n        ):\n            definition = (\n                match.group(\"prefix\")\n                + match.group(\"name\")\n                + \" \"\n                + match.group(\"body\")\n                + match.group(\"suffix\")\n            )\n\n        print(f\"parse_decls({json.dumps(definition)}, 0);\", file=file)\n\n\ndef import_symbol(symbol: Symbol, file) -> None:\n    known = not re.match(r\"(\\s+|^)(dword|sub)_\", symbol.signature)\n\n    if known:\n        signature = symbol.signature\n\n        print(\n            f\"apply_type(0x{symbol.offset:x}, parse_decl({json.dumps(symbol.signature)}, 0));\",\n            file=file,\n        )\n        if symbol.name:\n            print(\n                f\"set_name(0x{symbol.offset:x}, {json.dumps(symbol.name)});\",\n                file=file,\n            )\n\n\ndef generate_symbols(symbols: list[Symbol], file) -> None:\n    error_count = 0\n    for symbol in symbols:\n        import_symbol(symbol, file=file)\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-o\", \"--output\", type=Path, default=Path(\"Tomb2.idc\"))\n    return parser.parse_args()\n\n\ndef main():\n    args = parse_args()\n    progress_file = parse_progress_file(TR2Paths.progress_file)\n\n    output = Path(args.output)\n    with output.open(\"w\") as file:\n        print(\"#define CIC_FUNC 2\", file=file)\n        print(\"static main() {\", file=file)\n        generate_types(progress_file.types, file=file)\n        generate_symbols(progress_file.functions, file=file)\n        generate_symbols(progress_file.variables, file=file)\n        print(\"}\", file=file)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/tr2/read_tombpc_script",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport io\nimport json\nimport struct\nfrom dataclasses import dataclass\nfrom enum import IntEnum\nfrom pathlib import Path\n\n\ndef read_and_unpack(f, fmt):\n    size = struct.calcsize(fmt)\n    return struct.unpack(fmt, f.read(size))\n\n\ndef read_string_array(f, count, xor_byte):\n    offsets = read_and_unpack(f, f\"<{count}H\")\n    (datasz,) = read_and_unpack(f, \"<H\")\n    data = bytearray(f.read(datasz))\n    if xor_byte:\n        for i in range(len(data)):\n            data[i] ^= xor_byte\n    out = []\n    for off in offsets:\n        end = data.find(b\"\\x00\", off)\n        out.append(data[off:end].decode(\"ascii\", \"replace\"))\n    return out\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"file\", type=Path, help=\"Path to TOMBPC.DAT\")\n    return parser.parse_args()\n\n\nclass GameFlowEvent(IntEnum):\n    PICTURE = 0\n    LIST_START = 1\n    LIST_END = 2\n    PLAY_FMV = 3\n    START_LEVEL = 4\n    CUTSCENE = 5\n    LEVEL_COMPLETE = 6\n    DEMO_PLAY = 7\n    JUMP_TO_SEQ = 8\n    END_SEQ = 9\n    SET_TRACK = 10\n    SUNSET = 11\n    LOADING_PIC = 12\n    DEADLY_WATER = 13\n    REMOVE_WEAPONS = 14\n    GAME_COMPLETE = 15\n    CUT_ANGLE = 16\n    NO_FLOOR = 17\n    ADD_TO_INV = 18\n    START_ANIM = 19\n    NUM_SECRETS = 20\n    KILL_TO_COMPLETE = 21\n    REMOVE_AMMO = 22\n\n\n@dataclass\nclass GameScript:\n    version: int\n    description: str\n    cmd_init: int\n    cmd_title: int\n    cmd_death_in_demo: int\n    cmd_death_in_game: int\n    demo_time: int\n    cmd_demo_interrupt: int\n    cmd_demo_end: int\n\n    num_levels: int\n    num_chapter_screens: int\n    num_titles: int\n    num_fmvs: int\n    num_cutscenes: int\n    num_demos: int\n    title_sound: int\n    single_level: int\n    flags: int\n    secret_sound: int\n\n    level_names: list[str]\n    chapter_screen_names: list[str]\n    title_file_names: list[str]\n    fmv_file_names: list[str]\n    level_file_names: list[str]\n    cutscene_file_names: list[str]\n\n    script_offsets: list[int]\n    script_size: int\n    script_data: list[int]\n\n    demo_levels: list[int]\n\n    game_strings: list[str]\n    pc_strings: list[str]\n    puzzle_1_strings: list[str]\n    puzzle_2_strings: list[str]\n    puzzle_3_strings: list[str]\n    puzzle_4_strings: list[str]\n    pickup_1_strings: list[str]\n    pickup_2_strings: list[str]\n    key_1_strings: list[str]\n    key_2_strings: list[str]\n    key_3_strings: list[str]\n    key_4_strings: list[str]\n\n\ndef parse_game_script(path: Path) -> GameScript:\n    f = io.BytesIO(path.read_bytes())\n\n    (version, desc_b, header_size) = read_and_unpack(f, \"<I 256s H\")\n    description = desc_b.decode(\"ascii\", \"ignore\").rstrip(\"\\x00\")\n\n    (\n        cmd_init,\n        cmd_title,\n        cmd_death_in_demo,\n        cmd_death_in_game,\n        demo_time,\n        cmd_demo_interrupt,\n        cmd_demo_end,\n    ) = read_and_unpack(f, \"<7i\")\n\n    (\n        num_levels,\n        num_chapter_screens,\n        num_titles,\n        num_fmvs,\n        num_cutscenes,\n        num_demos,\n        title_sound,\n        single_level,\n        flags,\n        xor_byte,\n        secret_sound,\n    ) = read_and_unpack(f, \"<36x HHHHHHhh 32x H 6x B x H 4x\")\n\n    level_names = read_string_array(f, num_levels, xor_byte)\n    chapter_screen_names = read_string_array(f, num_chapter_screens, xor_byte)\n    title_file_names = read_string_array(f, num_titles, xor_byte)\n    fmv_file_names = read_string_array(f, num_fmvs, xor_byte)\n    level_file_names = read_string_array(f, num_levels, xor_byte)\n    cutscene_file_names = read_string_array(f, num_cutscenes, xor_byte)\n\n    script_offsets = read_and_unpack(f, f\"<{num_levels + 1}H\")\n    script_offsets = [offset // 2 for offset in script_offsets]\n    (script_size,) = read_and_unpack(f, \"<H\")\n    script_data = list(read_and_unpack(f, f\"<{script_size // 2}H\"))\n\n    demo_levels = read_and_unpack(f, f\"<{num_demos}H\")\n\n    (game_string_count,) = read_and_unpack(f, \"<H\")\n    game_strings = read_string_array(f, game_string_count, xor_byte)\n    pc_strings = read_string_array(f, 41, xor_byte)\n    puzzle_1_strings = read_string_array(f, num_levels, xor_byte)\n    puzzle_2_strings = read_string_array(f, num_levels, xor_byte)\n    puzzle_3_strings = read_string_array(f, num_levels, xor_byte)\n    puzzle_4_strings = read_string_array(f, num_levels, xor_byte)\n    pickup_1_strings = read_string_array(f, num_levels, xor_byte)\n    pickup_2_strings = read_string_array(f, num_levels, xor_byte)\n    key_1_strings = read_string_array(f, num_levels, xor_byte)\n    key_2_strings = read_string_array(f, num_levels, xor_byte)\n    key_3_strings = read_string_array(f, num_levels, xor_byte)\n    key_4_strings = read_string_array(f, num_levels, xor_byte)\n\n    return GameScript(\n        **{\n            k: v\n            for k, v in locals().items()\n            if k in GameScript.__dataclass_fields__\n        }\n    )\n\n\ndef create_trx_strings(game_script: GameScript):\n    return {\n        \"levels\": [\n            {\n                \"title\": game_script.level_names[i],\n                \"objects\": {\n                    k: v\n                    for k, v, placeholder in [\n                        (\"key_1\", game_script.key_1_strings[i], \"K1\"),\n                        (\"key_2\", game_script.key_2_strings[i], \"K2\"),\n                        (\"key_3\", game_script.key_3_strings[i], \"K3\"),\n                        (\"key_4\", game_script.key_4_strings[i], \"K4\"),\n                        (\"puzzle_1\", game_script.puzzle_1_strings[i], \"P1\"),\n                        (\"puzzle_2\", game_script.puzzle_2_strings[i], \"P2\"),\n                        (\"puzzle_3\", game_script.puzzle_3_strings[i], \"P3\"),\n                        (\"puzzle_4\", game_script.puzzle_4_strings[i], \"P4\"),\n                        (\"pickup_1\", game_script.pickup_1_strings[i], \"P1\"),\n                        (\"pickup_2\", game_script.pickup_2_strings[i], \"P2\"),\n                    ]\n                    if v != placeholder\n                },\n            }\n            for i in range(game_script.num_levels)\n        ],\n    }\n\n\ndef transform_script(script: list[int]):\n    while script:\n        opcode = script.pop(0)\n\n        match opcode:\n            case GameFlowEvent.PICTURE:\n                picture_num = script.pop(0)\n                yield {\n                    \"type\": \"display_picture\",\n                    \"path\": game_script.picture_strings[picture_num],\n                }\n\n            case GameFlowEvent.LIST_START | GameFlowEvent.LIST_END:\n                pass\n\n            case GameFlowEvent.PLAY_FMV:\n                fmv_id = script.pop(0)\n                yield {\"type\": \"play_fmv\", \"fmv_id\": fmv_id}\n\n            case GameFlowEvent.START_LEVEL:\n                level_id = script.pop(0)\n                yield {\"type\": \"play_level\", \"level_id\": level_id}\n\n            case GameFlowEvent.CUTSCENE:\n                cutscene_id = script.pop(0)\n                yield {\"type\": \"play_level\", \"cutscene_id\": cutscene_id}\n\n            case GameFlowEvent.LEVEL_COMPLETE:\n                yield {\"type\": \"play_music\", \"music_track\": 41}\n                yield {\"type\": \"level_stats\"}\n                yield {\"type\": \"level_complete\"}\n\n            case GameFlowEvent.DEMO_PLAY:\n                demo_num = script.pop(0)\n                yield {\"type\": \"play_level\", \"level_id\": demo_num}\n\n            case GameFlowEvent.JUMP_TO_SEQ:\n                seq_num = script.pop(0)\n                yield {\"type\": \"jump_to_seq\", \"seq_num\": seq_num}\n\n            case GameFlowEvent.SET_TRACK:\n                music_track = script.pop(0)\n                yield {\"type\": \"set_music_track\", \"music_track\": music_track}\n\n            case GameFlowEvent.SUNSET:\n                yield {\"type\": \"enable_sunset\"}\n\n            case GameFlowEvent.LOADING_PIC:\n                picture_num = script.pop(0)\n                yield {\n                    \"type\": \"set_loading_pic\",\n                    \"path\": game_script.picture_strings[picture_num],\n                }\n\n            case GameFlowEvent.DEADLY_WATER:\n                pass\n\n            case GameFlowEvent.REMOVE_WEAPONS:\n                yield {\"type\": \"remove_weapons\"}\n\n            case GameFlowEvent.GAME_COMPLETE:\n                yield {\"type\": \"game_complete\"}\n\n            case GameFlowEvent.CUT_ANGLE:\n                yield {\"type\": \"set_cutscene_angle\", \"angle\": script.pop(0)}\n\n            case GameFlowEvent.NO_FLOOR:\n                yield {\"type\": \"disable_floor\", \"height\": script.pop(0)}\n\n            case GameFlowEvent.ADD_TO_INV:\n                item = script.pop(0)\n                if item < 1000:\n                    yield {\"type\": \"add_secret_reward\", \"item\": item}\n                else:\n                    yield {\"type\": \"give_item\", \"item\": item - 1000}\n\n            case GameFlowEvent.START_ANIM:\n                yield {\"type\": \"set_lara_start_anim\", \"anim\": script.pop(0)}\n\n            case GameFlowEvent.NUM_SECRETS:\n                script.pop(0)\n                pass\n\n            case GameFlowEvent.KILL_TO_COMPLETE:\n                yield {\"type\": \"enable_kill_to_complete\"}\n\n            case GameFlowEvent.REMOVE_AMMO:\n                yield {\"type\": \"remove_ammo\"}\n\n            case GameFlowEvent.END_SEQ:\n                pass\n\n            case _:\n                pass\n\n\ndef transform_path(path: str) -> str:\n    return path.replace(\"\\\\\", \"/\").lower()\n\n\ndef transform_command(command: int):\n    if command == 0x500:\n        return {\"action\": \"exit_to_title\"}\n    elif command in [0, -1]:\n        return {\"action\": \"noop\"}\n    raise NotImplementedError(\"not implemented\")\n\n\ndef create_trx_game_flow(game_script: GameScript):\n    return {\n        \"cmd_init\": transform_command(game_script.cmd_init),\n        \"cmd_title\": transform_command(game_script.cmd_title),\n        \"cmd_death_in_demo\": transform_command(game_script.cmd_death_in_demo),\n        \"cmd_death_in_game\": transform_command(game_script.cmd_death_in_game),\n        \"cmd_demo_interrupt\": transform_command(\n            game_script.cmd_demo_interrupt\n        ),\n        \"cmd_demo_end\": transform_command(game_script.cmd_demo_end),\n        \"cheat_keys\": True if game_script.flags & 4 else False,\n        \"load_save_disabled\": True if game_script.flags & 16 else False,\n        \"play_any_level\": True if game_script.flags & 1024 else False,\n        \"lockout_option_ring\": True if game_script.flags & 64 else False,\n        \"demo_version\": True if game_script.flags & 1 else False,\n        \"single_level\": game_script.single_level,\n        \"demo_delay\": game_script.demo_time / 30,\n        \"title_track\": game_script.title_sound,\n        \"secret_track\": game_script.secret_sound,\n        **(\n            {\n                \"title\": {\n                    \"path\": transform_path(game_script.title_file_names[0]),\n                    \"sequence\": list(\n                        transform_script(\n                            game_script.script_data[\n                                game_script.script_offsets[\n                                    0\n                                ] : game_script.script_offsets[1]\n                            ]\n                        )\n                    ),\n                }\n            }\n            if game_script.flags & 2 == 0\n            else {}\n        ),\n        \"levels\": [\n            {\n                \"path\": transform_path(game_script.level_file_names[i]),\n                \"sequence\": list(\n                    transform_script(\n                        game_script.script_data[\n                            game_script.script_offsets[i + 1] : (\n                                game_script.script_offsets[i + 2]\n                                if i + 1 < game_script.num_levels\n                                else -1\n                            )\n                        ]\n                    )\n                ),\n            }\n            for i in range(game_script.num_levels)\n        ],\n        \"cutscenes\": [\n            {\"path\": transform_path(game_script.cutscene_file_names[i])}\n            for i in range(game_script.num_cutscenes)\n        ],\n        \"fmvs\": [\n            {\"path\": transform_path(game_script.fmv_file_names[i])}\n            for i in range(game_script.num_fmvs)\n        ],\n    }\n\n\nimport json\nfrom collections import OrderedDict\n\n\nclass CustomEncoder(json.JSONEncoder):\n    def __init__(self, *args, **kwargs):\n        # Set the base indentation level\n        self.indent_level = kwargs.pop(\"indent\", 4)\n        super().__init__(*args, **kwargs)\n\n    def encode(self, obj):\n        return self._encode(obj, 0)\n\n    def _encode(self, obj, current_indent):\n        if isinstance(obj, dict):\n            # Determine if this is an innermost dictionary\n            if not any(isinstance(v, (list, dict)) for v in obj.values()):\n                # Serialize innermost dictionary on a single line\n                items = [\n                    f\"{json.dumps(k)}: {self._encode(v, current_indent)}\"\n                    for k, v in obj.items()\n                ]\n                return \"{\" + \", \".join(items) + \"}\"\n            else:\n                # Serialize with indentation\n                items = []\n                indent_space = \" \" * (self.indent_level * (current_indent + 1))\n                for k, v in obj.items():\n                    encoded_key = json.dumps(k)\n                    encoded_val = self._encode(v, current_indent + 1)\n                    items.append(\n                        f\"\\n{indent_space}{encoded_key}: {encoded_val}\"\n                    )\n                closing_indent = \" \" * (self.indent_level * current_indent)\n                return \"{\" + \",\".join(items) + f\"\\n{closing_indent}\" + \"}\"\n        elif isinstance(obj, list):\n            # Handle lists with potential nested structures\n            if not any(isinstance(item, (dict, list)) for item in obj):\n                return json.dumps(obj)\n            else:\n                items = []\n                indent_space = \" \" * (self.indent_level * (current_indent + 1))\n                for item in obj:\n                    encoded_item = self._encode(item, current_indent + 1)\n                    items.append(f\"\\n{indent_space}{encoded_item}\")\n                closing_indent = \" \" * (self.indent_level * current_indent)\n                return \"[\" + \",\".join(items) + f\"\\n{closing_indent}\" + \"]\"\n        else:\n            return json.dumps(obj)\n\n\ndef main() -> None:\n    args = parse_args()\n\n    game_script = parse_game_script(args.file)\n    trx_game_strings = create_trx_strings(game_script)\n    trx_game_flow = create_trx_game_flow(game_script)\n\n    print(\"strings.json5:\")\n    print(json.dumps(trx_game_strings, indent=4))\n    print()\n    print(\"gameflow.json5:\")\n    print(json.dumps(trx_game_flow, cls=CustomEncoder, indent=4))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/tr3/objects_tracker/objects_dump.json",
    "content": "[\n    {\n        \"title\": \"Lara's Home\",\n        \"zone_num\": -1,\n        \"objects\": [0,1,2,5,10,11,16,24,30,97,118,127,128,129,131,132,133,134,135,136,137,138,139,145,146,158,159,160,178,179,181,182,183,185,203,204,224,228,232,234,235,285,300,304,315,319,330,331,332,336,339,349,350,351,352,353,355,357,358,360,361,366],\n        \"items\": [{\"object_id\":16,\"x\":44544,\"y\":2560,\"z\":49664,\"room_num\":0},{\"object_id\":350,\"x\":46592,\"y\":2560,\"z\":51712,\"room_num\":0},{\"object_id\":350,\"x\":46592,\"y\":2560,\"z\":47616,\"room_num\":0},{\"object_id\":361,\"x\":54784,\"y\":2560,\"z\":19968,\"room_num\":1},{\"object_id\":30,\"x\":47616,\"y\":2560,\"z\":16896,\"room_num\":1},{\"object_id\":30,\"x\":48640,\"y\":2560,\"z\":19968,\"room_num\":1},{\"object_id\":30,\"x\":50688,\"y\":2560,\"z\":13824,\"room_num\":1},{\"object_id\":30,\"x\":50688,\"y\":2560,\"z\":15872,\"room_num\":1},{\"object_id\":30,\"x\":52736,\"y\":2560,\"z\":18944,\"room_num\":1},{\"object_id\":30,\"x\":54784,\"y\":2560,\"z\":14848,\"room_num\":1},{\"object_id\":30,\"x\":54784,\"y\":2560,\"z\":19968,\"room_num\":1},{\"object_id\":353,\"x\":40448,\"y\":0,\"z\":42496,\"room_num\":8},{\"object_id\":133,\"x\":30208,\"y\":0,\"z\":34304,\"room_num\":9},{\"object_id\":128,\"x\":30208,\"y\":0,\"z\":35328,\"room_num\":9},{\"object_id\":235,\"x\":36352,\"y\":-2048,\"z\":27136,\"room_num\":9},{\"object_id\":235,\"x\":36352,\"y\":-2048,\"z\":32256,\"room_num\":9},{\"object_id\":330,\"x\":25088,\"y\":2176,\"z\":48640,\"room_num\":13},{\"object_id\":330,\"x\":23040,\"y\":2176,\"z\":48640,\"room_num\":13},{\"object_id\":128,\"x\":24064,\"y\":3328,\"z\":55808,\"room_num\":13},{\"object_id\":132,\"x\":24064,\"y\":2560,\"z\":17920,\"room_num\":19},{\"object_id\":139,\"x\":15872,\"y\":-1280,\"z\":15872,\"room_num\":28},{\"object_id\":139,\"x\":15872,\"y\":-1280,\"z\":16896,\"room_num\":28},{\"object_id\":139,\"x\":15872,\"y\":-1280,\"z\":17920,\"room_num\":28},{\"object_id\":139,\"x\":15872,\"y\":-1280,\"z\":18944,\"room_num\":28},{\"object_id\":139,\"x\":15872,\"y\":-1280,\"z\":19968,\"room_num\":28},{\"object_id\":139,\"x\":15872,\"y\":-1280,\"z\":20992,\"room_num\":28},{\"object_id\":127,\"x\":27136,\"y\":-256,\"z\":15872,\"room_num\":29},{\"object_id\":132,\"x\":42496,\"y\":2560,\"z\":29184,\"room_num\":50},{\"object_id\":128,\"x\":42496,\"y\":2560,\"z\":29184,\"room_num\":50},{\"object_id\":357,\"x\":42496,\"y\":2560,\"z\":29184,\"room_num\":50},{\"object_id\":135,\"x\":43520,\"y\":2560,\"z\":23040,\"room_num\":50},{\"object_id\":138,\"x\":54784,\"y\":2560,\"z\":31232,\"room_num\":50},{\"object_id\":138,\"x\":54784,\"y\":2560,\"z\":30208,\"room_num\":50},{\"object_id\":138,\"x\":54784,\"y\":2560,\"z\":29184,\"room_num\":50},{\"object_id\":138,\"x\":54784,\"y\":2560,\"z\":28160,\"room_num\":50},{\"object_id\":128,\"x\":54784,\"y\":2560,\"z\":31232,\"room_num\":50},{\"object_id\":97,\"x\":36352,\"y\":5376,\"z\":35328,\"room_num\":53},{\"object_id\":349,\"x\":67072,\"y\":2304,\"z\":17920,\"room_num\":54},{\"object_id\":349,\"x\":67072,\"y\":2304,\"z\":15872,\"room_num\":54},{\"object_id\":133,\"x\":26112,\"y\":256,\"z\":23040,\"room_num\":55},{\"object_id\":128,\"x\":27136,\"y\":256,\"z\":23040,\"room_num\":55},{\"object_id\":0,\"x\":26112,\"y\":256,\"z\":20992,\"room_num\":55},{\"object_id\":128,\"x\":28160,\"y\":-256,\"z\":34304,\"room_num\":56},{\"object_id\":97,\"x\":25088,\"y\":-2560,\"z\":29184,\"room_num\":57},{\"object_id\":319,\"x\":25088,\"y\":1152,\"z\":22016,\"room_num\":59},{\"object_id\":178,\"x\":26112,\"y\":256,\"z\":24064,\"room_num\":60},{\"object_id\":128,\"x\":41472,\"y\":2560,\"z\":28160,\"room_num\":63},{\"object_id\":136,\"x\":39424,\"y\":2560,\"z\":25088,\"room_num\":63},{\"object_id\":304,\"x\":39424,\"y\":1920,\"z\":25088,\"room_num\":63},{\"object_id\":128,\"x\":32256,\"y\":3072,\"z\":34304,\"room_num\":64},{\"object_id\":178,\"x\":32256,\"y\":3072,\"z\":34304,\"room_num\":64},{\"object_id\":304,\"x\":32256,\"y\":2176,\"z\":32256,\"room_num\":65},{\"object_id\":131,\"x\":32256,\"y\":2560,\"z\":32256,\"room_num\":65},{\"object_id\":360,\"x\":31232,\"y\":2560,\"z\":27136,\"room_num\":66},{\"object_id\":224,\"x\":42496,\"y\":5120,\"z\":31232,\"room_num\":69},{\"object_id\":339,\"x\":44544,\"y\":3712,\"z\":28160,\"room_num\":69},{\"object_id\":339,\"x\":40448,\"y\":4096,\"z\":29184,\"room_num\":69},{\"object_id\":339,\"x\":48640,\"y\":3840,\"z\":29184,\"room_num\":70},{\"object_id\":339,\"x\":40448,\"y\":4032,\"z\":40448,\"room_num\":72},{\"object_id\":339,\"x\":45568,\"y\":3712,\"z\":40448,\"room_num\":73},{\"object_id\":128,\"x\":20992,\"y\":2560,\"z\":27136,\"room_num\":79},{\"object_id\":232,\"x\":51712,\"y\":2560,\"z\":45568,\"room_num\":81},{\"object_id\":160,\"x\":40448,\"y\":3328,\"z\":15872,\"room_num\":84},{\"object_id\":135,\"x\":50688,\"y\":2560,\"z\":47616,\"room_num\":88},{\"object_id\":336,\"x\":33280,\"y\":2560,\"z\":24064,\"room_num\":94},{\"object_id\":336,\"x\":33280,\"y\":2560,\"z\":23040,\"room_num\":94},{\"object_id\":336,\"x\":34304,\"y\":2048,\"z\":24064,\"room_num\":94},{\"object_id\":336,\"x\":34304,\"y\":2048,\"z\":23040,\"room_num\":94},{\"object_id\":134,\"x\":33280,\"y\":2560,\"z\":20992,\"room_num\":95},{\"object_id\":128,\"x\":32256,\"y\":2560,\"z\":20992,\"room_num\":95},{\"object_id\":319,\"x\":27136,\"y\":1152,\"z\":22016,\"room_num\":96},{\"object_id\":319,\"x\":30208,\"y\":1152,\"z\":22016,\"room_num\":96},{\"object_id\":330,\"x\":28160,\"y\":-128,\"z\":18944,\"room_num\":99},{\"object_id\":330,\"x\":25088,\"y\":-1664,\"z\":37376,\"room_num\":102},{\"object_id\":118,\"x\":26112,\"y\":-1280,\"z\":41472,\"room_num\":105},{\"object_id\":330,\"x\":60928,\"y\":1280,\"z\":49664,\"room_num\":113},{\"object_id\":330,\"x\":54784,\"y\":6912,\"z\":85504,\"room_num\":114},{\"object_id\":339,\"x\":49664,\"y\":3968,\"z\":34304,\"room_num\":115},{\"object_id\":97,\"x\":26112,\"y\":-1536,\"z\":32256,\"room_num\":117},{\"object_id\":129,\"x\":22016,\"y\":-2560,\"z\":31232,\"room_num\":117},{\"object_id\":178,\"x\":26112,\"y\":-1536,\"z\":32256,\"room_num\":117},{\"object_id\":137,\"x\":39424,\"y\":2560,\"z\":36352,\"room_num\":120},{\"object_id\":351,\"x\":38400,\"y\":3072,\"z\":39424,\"room_num\":121},{\"object_id\":352,\"x\":38400,\"y\":3072,\"z\":41472,\"room_num\":121},{\"object_id\":128,\"x\":40448,\"y\":3072,\"z\":37376,\"room_num\":121},{\"object_id\":234,\"x\":38400,\"y\":1024,\"z\":39424,\"room_num\":121},{\"object_id\":234,\"x\":38400,\"y\":1024,\"z\":41472,\"room_num\":121},{\"object_id\":234,\"x\":42496,\"y\":1024,\"z\":41472,\"room_num\":121},{\"object_id\":234,\"x\":42496,\"y\":1024,\"z\":39424,\"room_num\":121},{\"object_id\":235,\"x\":38400,\"y\":1024,\"z\":39424,\"room_num\":121},{\"object_id\":235,\"x\":38400,\"y\":1024,\"z\":41472,\"room_num\":121},{\"object_id\":235,\"x\":42496,\"y\":1024,\"z\":41472,\"room_num\":121},{\"object_id\":235,\"x\":42496,\"y\":1024,\"z\":39424,\"room_num\":121},{\"object_id\":330,\"x\":35328,\"y\":2688,\"z\":40448,\"room_num\":123},{\"object_id\":331,\"x\":35328,\"y\":2944,\"z\":40448,\"room_num\":123},{\"object_id\":331,\"x\":35328,\"y\":2944,\"z\":40448,\"room_num\":123},{\"object_id\":129,\"x\":39424,\"y\":2560,\"z\":24064,\"room_num\":128},{\"object_id\":234,\"x\":34304,\"y\":3328,\"z\":43520,\"room_num\":129}]\n    },\n    {\n        \"title\": \"Jungle\",\n        \"zone_num\": 0,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,24,28,71,74,75,76,77,78,79,87,88,97,114,123,124,127,129,130,131,138,139,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,227,231,235,300,301,304,309,310,311,315,331,338,355,366,367],\n        \"items\": [{\"object_id\":131,\"x\":52736,\"y\":30208,\"z\":79360,\"room_num\":0},{\"object_id\":129,\"x\":34304,\"y\":27136,\"z\":77312,\"room_num\":1},{\"object_id\":75,\"x\":34304,\"y\":27136,\"z\":78336,\"room_num\":1},{\"object_id\":0,\"x\":28160,\"y\":-256,\"z\":28160,\"room_num\":4},{\"object_id\":171,\"x\":43520,\"y\":26112,\"z\":57856,\"room_num\":6},{\"object_id\":131,\"x\":53760,\"y\":24064,\"z\":80384,\"room_num\":7},{\"object_id\":180,\"x\":86528,\"y\":21248,\"z\":65024,\"room_num\":14},{\"object_id\":304,\"x\":93696,\"y\":20992,\"z\":62976,\"room_num\":14},{\"object_id\":71,\"x\":93696,\"y\":20992,\"z\":62976,\"room_num\":14},{\"object_id\":176,\"x\":92672,\"y\":22528,\"z\":60928,\"room_num\":14},{\"object_id\":87,\"x\":30208,\"y\":21504,\"z\":56832,\"room_num\":16},{\"object_id\":87,\"x\":29184,\"y\":21504,\"z\":56832,\"room_num\":16},{\"object_id\":87,\"x\":28160,\"y\":21504,\"z\":56832,\"room_num\":16},{\"object_id\":87,\"x\":27136,\"y\":21504,\"z\":56832,\"room_num\":16},{\"object_id\":88,\"x\":25088,\"y\":13440,\"z\":46592,\"room_num\":17},{\"object_id\":177,\"x\":25088,\"y\":17408,\"z\":53760,\"room_num\":17},{\"object_id\":169,\"x\":26112,\"y\":17664,\"z\":52736,\"room_num\":17},{\"object_id\":71,\"x\":30208,\"y\":5376,\"z\":40448,\"room_num\":20},{\"object_id\":171,\"x\":27136,\"y\":2816,\"z\":32256,\"room_num\":21},{\"object_id\":161,\"x\":27136,\"y\":2816,\"z\":32256,\"room_num\":21},{\"object_id\":87,\"x\":31232,\"y\":10752,\"z\":42496,\"room_num\":23},{\"object_id\":87,\"x\":30208,\"y\":11520,\"z\":43520,\"room_num\":23},{\"object_id\":87,\"x\":29184,\"y\":12288,\"z\":44544,\"room_num\":23},{\"object_id\":87,\"x\":28160,\"y\":12288,\"z\":44544,\"room_num\":23},{\"object_id\":87,\"x\":27136,\"y\":11520,\"z\":43520,\"room_num\":23},{\"object_id\":87,\"x\":26112,\"y\":10752,\"z\":42496,\"room_num\":23},{\"object_id\":169,\"x\":10752,\"y\":26240,\"z\":73216,\"room_num\":24},{\"object_id\":304,\"x\":29184,\"y\":24960,\"z\":61952,\"room_num\":27},{\"object_id\":28,\"x\":62976,\"y\":24832,\"z\":62976,\"room_num\":28},{\"object_id\":180,\"x\":31232,\"y\":16000,\"z\":60928,\"room_num\":29},{\"object_id\":169,\"x\":29184,\"y\":16896,\"z\":59904,\"room_num\":29},{\"object_id\":169,\"x\":74240,\"y\":24064,\"z\":59904,\"room_num\":39},{\"object_id\":129,\"x\":50688,\"y\":27904,\"z\":65024,\"room_num\":41},{\"object_id\":176,\"x\":58880,\"y\":26624,\"z\":56832,\"room_num\":42},{\"object_id\":28,\"x\":54784,\"y\":27264,\"z\":61952,\"room_num\":43},{\"object_id\":131,\"x\":60928,\"y\":25344,\"z\":66048,\"room_num\":45},{\"object_id\":71,\"x\":60928,\"y\":25600,\"z\":68096,\"room_num\":45},{\"object_id\":129,\"x\":60928,\"y\":25600,\"z\":70144,\"room_num\":45},{\"object_id\":28,\"x\":76288,\"y\":25344,\"z\":61952,\"room_num\":46},{\"object_id\":138,\"x\":61952,\"y\":25344,\"z\":65024,\"room_num\":47},{\"object_id\":88,\"x\":61952,\"y\":25344,\"z\":65024,\"room_num\":47},{\"object_id\":169,\"x\":77312,\"y\":24192,\"z\":49664,\"room_num\":48},{\"object_id\":171,\"x\":78336,\"y\":24320,\"z\":47616,\"room_num\":48},{\"object_id\":88,\"x\":80384,\"y\":24448,\"z\":60928,\"room_num\":50},{\"object_id\":88,\"x\":81408,\"y\":24448,\"z\":60928,\"room_num\":50},{\"object_id\":88,\"x\":79360,\"y\":24448,\"z\":60928,\"room_num\":50},{\"object_id\":75,\"x\":82432,\"y\":25600,\"z\":64000,\"room_num\":50},{\"object_id\":76,\"x\":91648,\"y\":25088,\"z\":48640,\"room_num\":51},{\"object_id\":79,\"x\":86528,\"y\":25088,\"z\":49664,\"room_num\":51},{\"object_id\":71,\"x\":83456,\"y\":25600,\"z\":49664,\"room_num\":54},{\"object_id\":76,\"x\":83456,\"y\":25600,\"z\":49664,\"room_num\":54},{\"object_id\":180,\"x\":89600,\"y\":23552,\"z\":41472,\"room_num\":61},{\"object_id\":138,\"x\":91648,\"y\":27648,\"z\":66048,\"room_num\":62},{\"object_id\":138,\"x\":90624,\"y\":27648,\"z\":66048,\"room_num\":62},{\"object_id\":138,\"x\":89600,\"y\":27648,\"z\":66048,\"room_num\":62},{\"object_id\":138,\"x\":88576,\"y\":27648,\"z\":66048,\"room_num\":62},{\"object_id\":71,\"x\":85504,\"y\":28416,\"z\":72192,\"room_num\":62},{\"object_id\":178,\"x\":87552,\"y\":27136,\"z\":69120,\"room_num\":62},{\"object_id\":173,\"x\":83456,\"y\":22528,\"z\":45568,\"room_num\":67},{\"object_id\":28,\"x\":89600,\"y\":22528,\"z\":54784,\"room_num\":68},{\"object_id\":28,\"x\":91648,\"y\":26368,\"z\":57856,\"room_num\":70},{\"object_id\":87,\"x\":89600,\"y\":26368,\"z\":56832,\"room_num\":70},{\"object_id\":170,\"x\":88576,\"y\":25344,\"z\":52736,\"room_num\":70},{\"object_id\":170,\"x\":91648,\"y\":25344,\"z\":51712,\"room_num\":70},{\"object_id\":178,\"x\":89600,\"y\":24064,\"z\":52736,\"room_num\":72},{\"object_id\":173,\"x\":89600,\"y\":24064,\"z\":51712,\"room_num\":72},{\"object_id\":71,\"x\":92672,\"y\":24064,\"z\":52736,\"room_num\":72},{\"object_id\":76,\"x\":92672,\"y\":24064,\"z\":52736,\"room_num\":72},{\"object_id\":79,\"x\":87552,\"y\":24064,\"z\":52736,\"room_num\":72},{\"object_id\":71,\"x\":95744,\"y\":18944,\"z\":70144,\"room_num\":77},{\"object_id\":88,\"x\":95744,\"y\":17280,\"z\":70144,\"room_num\":77},{\"object_id\":129,\"x\":95744,\"y\":18944,\"z\":70144,\"room_num\":77},{\"object_id\":176,\"x\":81408,\"y\":27904,\"z\":70144,\"room_num\":80},{\"object_id\":138,\"x\":9728,\"y\":27136,\"z\":76288,\"room_num\":82},{\"object_id\":235,\"x\":10752,\"y\":26880,\"z\":77312,\"room_num\":82},{\"object_id\":129,\"x\":59904,\"y\":24064,\"z\":79360,\"room_num\":86},{\"object_id\":87,\"x\":82432,\"y\":27392,\"z\":54784,\"room_num\":87},{\"object_id\":87,\"x\":83456,\"y\":27392,\"z\":54784,\"room_num\":88},{\"object_id\":87,\"x\":84480,\"y\":27392,\"z\":54784,\"room_num\":88},{\"object_id\":76,\"x\":87552,\"y\":23552,\"z\":64000,\"room_num\":90},{\"object_id\":180,\"x\":34304,\"y\":25472,\"z\":81408,\"room_num\":98},{\"object_id\":87,\"x\":82432,\"y\":28160,\"z\":62976,\"room_num\":99},{\"object_id\":178,\"x\":81408,\"y\":26880,\"z\":65024,\"room_num\":99},{\"object_id\":180,\"x\":83456,\"y\":27392,\"z\":65024,\"room_num\":99},{\"object_id\":129,\"x\":47616,\"y\":24064,\"z\":86528,\"room_num\":101},{\"object_id\":138,\"x\":52736,\"y\":30208,\"z\":87552,\"room_num\":102},{\"object_id\":138,\"x\":48640,\"y\":30208,\"z\":87552,\"room_num\":102},{\"object_id\":129,\"x\":59904,\"y\":27904,\"z\":90624,\"room_num\":103},{\"object_id\":131,\"x\":50688,\"y\":27904,\"z\":95744,\"room_num\":106},{\"object_id\":131,\"x\":55808,\"y\":27904,\"z\":90624,\"room_num\":106},{\"object_id\":139,\"x\":51712,\"y\":27904,\"z\":85504,\"room_num\":106},{\"object_id\":97,\"x\":52736,\"y\":27904,\"z\":85504,\"room_num\":106},{\"object_id\":178,\"x\":50688,\"y\":27904,\"z\":91648,\"room_num\":106},{\"object_id\":304,\"x\":48640,\"y\":27904,\"z\":88576,\"room_num\":106},{\"object_id\":304,\"x\":52736,\"y\":28160,\"z\":88576,\"room_num\":106},{\"object_id\":304,\"x\":55808,\"y\":27648,\"z\":90624,\"room_num\":106},{\"object_id\":331,\"x\":45568,\"y\":27008,\"z\":88576,\"room_num\":106},{\"object_id\":331,\"x\":45568,\"y\":27008,\"z\":90624,\"room_num\":106},{\"object_id\":304,\"x\":50688,\"y\":27392,\"z\":94720,\"room_num\":106},{\"object_id\":304,\"x\":51712,\"y\":28416,\"z\":85504,\"room_num\":107},{\"object_id\":129,\"x\":50688,\"y\":27904,\"z\":84480,\"room_num\":109},{\"object_id\":331,\"x\":49664,\"y\":27136,\"z\":83456,\"room_num\":109},{\"object_id\":331,\"x\":51712,\"y\":27136,\"z\":83456,\"room_num\":109},{\"object_id\":331,\"x\":51712,\"y\":27136,\"z\":81408,\"room_num\":109},{\"object_id\":331,\"x\":49664,\"y\":27136,\"z\":81408,\"room_num\":109},{\"object_id\":129,\"x\":50688,\"y\":27904,\"z\":80384,\"room_num\":109},{\"object_id\":28,\"x\":35328,\"y\":29952,\"z\":91648,\"room_num\":113},{\"object_id\":28,\"x\":38400,\"y\":28160,\"z\":81408,\"room_num\":113},{\"object_id\":138,\"x\":31232,\"y\":31232,\"z\":88576,\"room_num\":116},{\"object_id\":138,\"x\":31232,\"y\":31232,\"z\":87552,\"room_num\":116},{\"object_id\":129,\"x\":53760,\"y\":27904,\"z\":98816,\"room_num\":117},{\"object_id\":28,\"x\":20992,\"y\":25856,\"z\":72192,\"room_num\":122},{\"object_id\":169,\"x\":20992,\"y\":26368,\"z\":76288,\"room_num\":122},{\"object_id\":173,\"x\":20992,\"y\":26368,\"z\":69120,\"room_num\":122},{\"object_id\":28,\"x\":18944,\"y\":25472,\"z\":77312,\"room_num\":127},{\"object_id\":227,\"x\":24064,\"y\":23808,\"z\":71168,\"room_num\":132},{\"object_id\":71,\"x\":22016,\"y\":23808,\"z\":72192,\"room_num\":132},{\"object_id\":77,\"x\":22016,\"y\":23808,\"z\":72192,\"room_num\":132},{\"object_id\":180,\"x\":20992,\"y\":25856,\"z\":59904,\"room_num\":134},{\"object_id\":175,\"x\":25088,\"y\":25088,\"z\":54784,\"room_num\":141},{\"object_id\":178,\"x\":20992,\"y\":23296,\"z\":53760,\"room_num\":141},{\"object_id\":169,\"x\":16896,\"y\":23296,\"z\":53760,\"room_num\":141},{\"object_id\":174,\"x\":93696,\"y\":17152,\"z\":70144,\"room_num\":143},{\"object_id\":172,\"x\":94720,\"y\":17152,\"z\":70144,\"room_num\":143},{\"object_id\":71,\"x\":33280,\"y\":27136,\"z\":71168,\"room_num\":147},{\"object_id\":176,\"x\":30208,\"y\":27136,\"z\":70144,\"room_num\":147},{\"object_id\":114,\"x\":30208,\"y\":26368,\"z\":59904,\"room_num\":149},{\"object_id\":129,\"x\":27136,\"y\":26368,\"z\":59904,\"room_num\":149},{\"object_id\":131,\"x\":29184,\"y\":25344,\"z\":60928,\"room_num\":149},{\"object_id\":127,\"x\":31232,\"y\":24832,\"z\":59904,\"room_num\":149},{\"object_id\":171,\"x\":77312,\"y\":27136,\"z\":77312,\"room_num\":151},{\"object_id\":304,\"x\":39424,\"y\":27904,\"z\":88576,\"room_num\":153},{\"object_id\":176,\"x\":37376,\"y\":30592,\"z\":48640,\"room_num\":157},{\"object_id\":338,\"x\":38400,\"y\":28160,\"z\":65024,\"room_num\":158},{\"object_id\":87,\"x\":88576,\"y\":23808,\"z\":65024,\"room_num\":161},{\"object_id\":87,\"x\":89600,\"y\":23808,\"z\":65024,\"room_num\":162},{\"object_id\":87,\"x\":90624,\"y\":23808,\"z\":65024,\"room_num\":162},{\"object_id\":87,\"x\":91648,\"y\":23808,\"z\":65024,\"room_num\":162},{\"object_id\":127,\"x\":45568,\"y\":24064,\"z\":70144,\"room_num\":163},{\"object_id\":178,\"x\":43520,\"y\":26368,\"z\":61952,\"room_num\":164},{\"object_id\":71,\"x\":46592,\"y\":27392,\"z\":65024,\"room_num\":164},{\"object_id\":75,\"x\":43520,\"y\":26368,\"z\":61952,\"room_num\":164}]\n    },\n    {\n        \"title\": \"Temple Ruins\",\n        \"zone_num\": 0,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,13,24,69,70,71,87,88,90,91,97,107,111,114,116,122,123,129,130,131,132,133,134,139,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,209,210,213,214,217,218,224,228,232,300,301,304,305,309,310,311,315,330,331,332,333,338,349,350,355,366,367],\n        \"items\": [{\"object_id\":338,\"x\":84480,\"y\":1024,\"z\":53760,\"room_num\":8},{\"object_id\":71,\"x\":91648,\"y\":-256,\"z\":54784,\"room_num\":10},{\"object_id\":71,\"x\":93696,\"y\":-384,\"z\":49664,\"room_num\":10},{\"object_id\":176,\"x\":86528,\"y\":-768,\"z\":51712,\"room_num\":10},{\"object_id\":169,\"x\":94720,\"y\":-2560,\"z\":47616,\"room_num\":10},{\"object_id\":173,\"x\":94720,\"y\":-2048,\"z\":46592,\"room_num\":10},{\"object_id\":180,\"x\":82432,\"y\":-3840,\"z\":37376,\"room_num\":12},{\"object_id\":129,\"x\":55808,\"y\":-1536,\"z\":92672,\"room_num\":14},{\"object_id\":173,\"x\":56832,\"y\":-1536,\"z\":94720,\"room_num\":14},{\"object_id\":161,\"x\":54784,\"y\":-1536,\"z\":94720,\"room_num\":14},{\"object_id\":129,\"x\":94720,\"y\":-256,\"z\":36352,\"room_num\":16},{\"object_id\":69,\"x\":93696,\"y\":-256,\"z\":35328,\"room_num\":16},{\"object_id\":170,\"x\":89600,\"y\":-3456,\"z\":36352,\"room_num\":17},{\"object_id\":176,\"x\":92672,\"y\":-512,\"z\":29184,\"room_num\":18},{\"object_id\":69,\"x\":92672,\"y\":-256,\"z\":32256,\"room_num\":18},{\"object_id\":0,\"x\":94720,\"y\":-384,\"z\":26112,\"room_num\":18},{\"object_id\":178,\"x\":86528,\"y\":-3200,\"z\":27136,\"room_num\":19},{\"object_id\":305,\"x\":85504,\"y\":0,\"z\":25088,\"room_num\":20},{\"object_id\":305,\"x\":83456,\"y\":0,\"z\":25088,\"room_num\":20},{\"object_id\":338,\"x\":84480,\"y\":640,\"z\":30208,\"room_num\":22},{\"object_id\":305,\"x\":80384,\"y\":0,\"z\":25088,\"room_num\":24},{\"object_id\":305,\"x\":78336,\"y\":0,\"z\":25088,\"room_num\":24},{\"object_id\":176,\"x\":82432,\"y\":-3200,\"z\":26112,\"room_num\":25},{\"object_id\":71,\"x\":79360,\"y\":-3328,\"z\":34304,\"room_num\":27},{\"object_id\":69,\"x\":73216,\"y\":-2944,\"z\":31232,\"room_num\":30},{\"object_id\":338,\"x\":80384,\"y\":512,\"z\":27136,\"room_num\":31},{\"object_id\":71,\"x\":71168,\"y\":-256,\"z\":47616,\"room_num\":35},{\"object_id\":171,\"x\":75264,\"y\":-3328,\"z\":61952,\"room_num\":36},{\"object_id\":71,\"x\":75264,\"y\":-3584,\"z\":45568,\"room_num\":36},{\"object_id\":171,\"x\":80384,\"y\":-3072,\"z\":46592,\"room_num\":38},{\"object_id\":71,\"x\":78336,\"y\":-3328,\"z\":44544,\"room_num\":43},{\"object_id\":129,\"x\":81408,\"y\":-768,\"z\":60928,\"room_num\":46},{\"object_id\":71,\"x\":77312,\"y\":-640,\"z\":57856,\"room_num\":48},{\"object_id\":71,\"x\":82432,\"y\":-3328,\"z\":42496,\"room_num\":52},{\"object_id\":139,\"x\":92672,\"y\":-256,\"z\":37376,\"room_num\":55},{\"object_id\":176,\"x\":91648,\"y\":1280,\"z\":42496,\"room_num\":57},{\"object_id\":69,\"x\":96768,\"y\":1024,\"z\":45568,\"room_num\":61},{\"object_id\":178,\"x\":84480,\"y\":-256,\"z\":23040,\"room_num\":65},{\"object_id\":69,\"x\":82432,\"y\":-128,\"z\":23040,\"room_num\":65},{\"object_id\":169,\"x\":77312,\"y\":-3072,\"z\":24064,\"room_num\":70},{\"object_id\":88,\"x\":64000,\"y\":-1536,\"z\":40448,\"room_num\":74},{\"object_id\":69,\"x\":68096,\"y\":-768,\"z\":38400,\"room_num\":75},{\"object_id\":97,\"x\":61952,\"y\":7168,\"z\":52736,\"room_num\":77},{\"object_id\":24,\"x\":64000,\"y\":7168,\"z\":49664,\"room_num\":77},{\"object_id\":161,\"x\":61952,\"y\":6912,\"z\":49664,\"room_num\":77},{\"object_id\":169,\"x\":60928,\"y\":7168,\"z\":52736,\"room_num\":79},{\"object_id\":132,\"x\":79360,\"y\":2304,\"z\":52736,\"room_num\":80},{\"object_id\":70,\"x\":62976,\"y\":7168,\"z\":61952,\"room_num\":81},{\"object_id\":232,\"x\":56832,\"y\":7168,\"z\":59904,\"room_num\":82},{\"object_id\":232,\"x\":56832,\"y\":7168,\"z\":56832,\"room_num\":82},{\"object_id\":132,\"x\":57856,\"y\":6912,\"z\":53760,\"room_num\":82},{\"object_id\":129,\"x\":65024,\"y\":5120,\"z\":56832,\"room_num\":83},{\"object_id\":129,\"x\":65024,\"y\":5120,\"z\":59904,\"room_num\":83},{\"object_id\":180,\"x\":65024,\"y\":4864,\"z\":57856,\"room_num\":83},{\"object_id\":331,\"x\":59904,\"y\":4352,\"z\":54784,\"room_num\":84},{\"object_id\":331,\"x\":59904,\"y\":4608,\"z\":61696,\"room_num\":84},{\"object_id\":139,\"x\":57856,\"y\":7168,\"z\":61952,\"room_num\":85},{\"object_id\":97,\"x\":59904,\"y\":8448,\"z\":61952,\"room_num\":86},{\"object_id\":129,\"x\":58880,\"y\":8448,\"z\":61952,\"room_num\":86},{\"object_id\":91,\"x\":66048,\"y\":8064,\"z\":64000,\"room_num\":88},{\"object_id\":91,\"x\":61952,\"y\":7808,\"z\":66048,\"room_num\":88},{\"object_id\":88,\"x\":64000,\"y\":6656,\"z\":70144,\"room_num\":88},{\"object_id\":176,\"x\":64000,\"y\":8960,\"z\":62976,\"room_num\":88},{\"object_id\":178,\"x\":66048,\"y\":7936,\"z\":66048,\"room_num\":88},{\"object_id\":169,\"x\":61952,\"y\":7936,\"z\":69120,\"room_num\":88},{\"object_id\":176,\"x\":66048,\"y\":7936,\"z\":69120,\"room_num\":88},{\"object_id\":132,\"x\":57856,\"y\":6912,\"z\":64000,\"room_num\":90},{\"object_id\":129,\"x\":59904,\"y\":4864,\"z\":73216,\"room_num\":100},{\"object_id\":176,\"x\":55808,\"y\":4864,\"z\":72192,\"room_num\":100},{\"object_id\":71,\"x\":57856,\"y\":4864,\"z\":74240,\"room_num\":101},{\"object_id\":114,\"x\":58880,\"y\":4608,\"z\":76288,\"room_num\":105},{\"object_id\":114,\"x\":56832,\"y\":4608,\"z\":77312,\"room_num\":105},{\"object_id\":132,\"x\":57856,\"y\":4864,\"z\":76288,\"room_num\":105},{\"object_id\":111,\"x\":57856,\"y\":4736,\"z\":78336,\"room_num\":106},{\"object_id\":114,\"x\":55808,\"y\":4608,\"z\":80384,\"room_num\":107},{\"object_id\":114,\"x\":59904,\"y\":4608,\"z\":79360,\"room_num\":107},{\"object_id\":97,\"x\":62976,\"y\":3840,\"z\":94720,\"room_num\":110},{\"object_id\":71,\"x\":61952,\"y\":3840,\"z\":93696,\"room_num\":110},{\"object_id\":71,\"x\":54784,\"y\":3840,\"z\":94720,\"room_num\":111},{\"object_id\":304,\"x\":58880,\"y\":1408,\"z\":95744,\"room_num\":116},{\"object_id\":129,\"x\":58880,\"y\":3584,\"z\":94720,\"room_num\":119},{\"object_id\":132,\"x\":58880,\"y\":1536,\"z\":94720,\"room_num\":123},{\"object_id\":132,\"x\":57856,\"y\":7168,\"z\":92672,\"room_num\":124},{\"object_id\":130,\"x\":58880,\"y\":6144,\"z\":93696,\"room_num\":124},{\"object_id\":130,\"x\":44544,\"y\":6656,\"z\":90624,\"room_num\":129},{\"object_id\":130,\"x\":45568,\"y\":6656,\"z\":97792,\"room_num\":130},{\"object_id\":132,\"x\":41472,\"y\":7168,\"z\":96768,\"room_num\":130},{\"object_id\":130,\"x\":47616,\"y\":6656,\"z\":94720,\"room_num\":130},{\"object_id\":304,\"x\":42496,\"y\":6784,\"z\":96768,\"room_num\":130},{\"object_id\":333,\"x\":46592,\"y\":3584,\"z\":97792,\"room_num\":133},{\"object_id\":333,\"x\":43520,\"y\":3584,\"z\":97792,\"room_num\":133},{\"object_id\":107,\"x\":43520,\"y\":3328,\"z\":95744,\"room_num\":133},{\"object_id\":107,\"x\":46592,\"y\":3328,\"z\":95744,\"room_num\":133},{\"object_id\":129,\"x\":42496,\"y\":3840,\"z\":94720,\"room_num\":133},{\"object_id\":333,\"x\":46592,\"y\":3584,\"z\":90624,\"room_num\":134},{\"object_id\":333,\"x\":43520,\"y\":3584,\"z\":90624,\"room_num\":134},{\"object_id\":107,\"x\":44544,\"y\":3200,\"z\":92672,\"room_num\":134},{\"object_id\":134,\"x\":54784,\"y\":2560,\"z\":92672,\"room_num\":135},{\"object_id\":139,\"x\":52736,\"y\":5120,\"z\":93696,\"room_num\":135},{\"object_id\":134,\"x\":54784,\"y\":2560,\"z\":95744,\"room_num\":136},{\"object_id\":130,\"x\":49664,\"y\":6400,\"z\":90624,\"room_num\":137},{\"object_id\":130,\"x\":49664,\"y\":6528,\"z\":97792,\"room_num\":138},{\"object_id\":180,\"x\":38400,\"y\":4736,\"z\":94720,\"room_num\":140},{\"object_id\":177,\"x\":40448,\"y\":7168,\"z\":90624,\"room_num\":143},{\"object_id\":175,\"x\":39424,\"y\":7168,\"z\":91648,\"room_num\":143},{\"object_id\":178,\"x\":40448,\"y\":7168,\"z\":91648,\"room_num\":143},{\"object_id\":131,\"x\":35328,\"y\":4864,\"z\":90624,\"room_num\":144},{\"object_id\":133,\"x\":34304,\"y\":4864,\"z\":90624,\"room_num\":144},{\"object_id\":132,\"x\":35328,\"y\":4864,\"z\":91648,\"room_num\":144},{\"object_id\":114,\"x\":35328,\"y\":4864,\"z\":98816,\"room_num\":146},{\"object_id\":129,\"x\":34304,\"y\":4864,\"z\":96768,\"room_num\":146},{\"object_id\":176,\"x\":35328,\"y\":4864,\"z\":98816,\"room_num\":146},{\"object_id\":173,\"x\":34304,\"y\":4864,\"z\":98816,\"room_num\":146},{\"object_id\":224,\"x\":35328,\"y\":4864,\"z\":87552,\"room_num\":147},{\"object_id\":178,\"x\":55808,\"y\":1280,\"z\":82432,\"room_num\":162},{\"object_id\":88,\"x\":49664,\"y\":-640,\"z\":82432,\"room_num\":163},{\"object_id\":132,\"x\":49664,\"y\":256,\"z\":80384,\"room_num\":163},{\"object_id\":132,\"x\":49664,\"y\":256,\"z\":80384,\"room_num\":163},{\"object_id\":88,\"x\":50688,\"y\":-1024,\"z\":79360,\"room_num\":165},{\"object_id\":111,\"x\":50688,\"y\":256,\"z\":77312,\"room_num\":165},{\"object_id\":132,\"x\":48640,\"y\":256,\"z\":72192,\"room_num\":166},{\"object_id\":129,\"x\":47616,\"y\":256,\"z\":72192,\"room_num\":166},{\"object_id\":71,\"x\":47616,\"y\":256,\"z\":72192,\"room_num\":166},{\"object_id\":333,\"x\":47616,\"y\":6016,\"z\":32256,\"room_num\":170},{\"object_id\":333,\"x\":51712,\"y\":6016,\"z\":32256,\"room_num\":170},{\"object_id\":129,\"x\":49664,\"y\":6656,\"z\":32256,\"room_num\":170},{\"object_id\":333,\"x\":47616,\"y\":6016,\"z\":37376,\"room_num\":170},{\"object_id\":333,\"x\":51712,\"y\":6016,\"z\":37376,\"room_num\":170},{\"object_id\":132,\"x\":49664,\"y\":8960,\"z\":32256,\"room_num\":171},{\"object_id\":91,\"x\":49664,\"y\":8960,\"z\":17920,\"room_num\":173},{\"object_id\":176,\"x\":46592,\"y\":6656,\"z\":20992,\"room_num\":174},{\"object_id\":88,\"x\":42496,\"y\":-640,\"z\":19968,\"room_num\":176},{\"object_id\":91,\"x\":43520,\"y\":1024,\"z\":18944,\"room_num\":176},{\"object_id\":91,\"x\":43520,\"y\":1024,\"z\":22016,\"room_num\":176},{\"object_id\":111,\"x\":41472,\"y\":768,\"z\":19968,\"room_num\":176},{\"object_id\":111,\"x\":41472,\"y\":896,\"z\":20992,\"room_num\":176},{\"object_id\":176,\"x\":40448,\"y\":1024,\"z\":18944,\"room_num\":176},{\"object_id\":107,\"x\":49664,\"y\":640,\"z\":19968,\"room_num\":177},{\"object_id\":107,\"x\":50688,\"y\":640,\"z\":20992,\"room_num\":177},{\"object_id\":107,\"x\":48640,\"y\":768,\"z\":20992,\"room_num\":177},{\"object_id\":69,\"x\":49664,\"y\":1792,\"z\":17920,\"room_num\":177},{\"object_id\":87,\"x\":39424,\"y\":3712,\"z\":20992,\"room_num\":178},{\"object_id\":87,\"x\":39424,\"y\":3712,\"z\":19968,\"room_num\":178},{\"object_id\":87,\"x\":38400,\"y\":3712,\"z\":19968,\"room_num\":178},{\"object_id\":87,\"x\":38400,\"y\":3712,\"z\":20992,\"room_num\":178},{\"object_id\":180,\"x\":34304,\"y\":1024,\"z\":19968,\"room_num\":180},{\"object_id\":171,\"x\":50688,\"y\":3840,\"z\":13824,\"room_num\":184},{\"object_id\":170,\"x\":48640,\"y\":3840,\"z\":13824,\"room_num\":184},{\"object_id\":180,\"x\":49664,\"y\":3456,\"z\":13824,\"room_num\":184},{\"object_id\":69,\"x\":48640,\"y\":3840,\"z\":14848,\"room_num\":184},{\"object_id\":129,\"x\":31232,\"y\":1024,\"z\":24064,\"room_num\":185},{\"object_id\":97,\"x\":33280,\"y\":1024,\"z\":22016,\"room_num\":185},{\"object_id\":97,\"x\":33280,\"y\":1024,\"z\":23040,\"room_num\":185},{\"object_id\":97,\"x\":32256,\"y\":1024,\"z\":23040,\"room_num\":185},{\"object_id\":97,\"x\":32256,\"y\":1024,\"z\":24064,\"room_num\":185},{\"object_id\":97,\"x\":32256,\"y\":1024,\"z\":25088,\"room_num\":185},{\"object_id\":304,\"x\":50688,\"y\":7168,\"z\":45568,\"room_num\":186},{\"object_id\":71,\"x\":46592,\"y\":5888,\"z\":46592,\"room_num\":189},{\"object_id\":71,\"x\":53760,\"y\":5888,\"z\":45568,\"room_num\":190},{\"object_id\":130,\"x\":51712,\"y\":7424,\"z\":46592,\"room_num\":191},{\"object_id\":139,\"x\":49664,\"y\":7936,\"z\":45568,\"room_num\":191},{\"object_id\":139,\"x\":49664,\"y\":7936,\"z\":46592,\"room_num\":191},{\"object_id\":139,\"x\":50688,\"y\":7936,\"z\":46592,\"room_num\":191},{\"object_id\":139,\"x\":50688,\"y\":7936,\"z\":45568,\"room_num\":191},{\"object_id\":224,\"x\":44544,\"y\":8192,\"z\":46592,\"room_num\":192},{\"object_id\":132,\"x\":47616,\"y\":8448,\"z\":46592,\"room_num\":192},{\"object_id\":131,\"x\":54784,\"y\":7168,\"z\":58880,\"room_num\":193},{\"object_id\":133,\"x\":54784,\"y\":7168,\"z\":57856,\"room_num\":193},{\"object_id\":304,\"x\":53760,\"y\":5504,\"z\":58880,\"room_num\":193},{\"object_id\":116,\"x\":54784,\"y\":0,\"z\":59904,\"room_num\":194},{\"object_id\":116,\"x\":54784,\"y\":0,\"z\":57856,\"room_num\":194},{\"object_id\":116,\"x\":54784,\"y\":0,\"z\":55808,\"room_num\":194},{\"object_id\":97,\"x\":51712,\"y\":3328,\"z\":62976,\"room_num\":195},{\"object_id\":131,\"x\":46592,\"y\":3584,\"z\":57856,\"room_num\":195},{\"object_id\":133,\"x\":46592,\"y\":3584,\"z\":58880,\"room_num\":195},{\"object_id\":180,\"x\":50688,\"y\":3200,\"z\":54784,\"room_num\":195},{\"object_id\":71,\"x\":51712,\"y\":3328,\"z\":54784,\"room_num\":195},{\"object_id\":71,\"x\":51712,\"y\":3328,\"z\":60928,\"room_num\":195},{\"object_id\":129,\"x\":48640,\"y\":1280,\"z\":60928,\"room_num\":196},{\"object_id\":129,\"x\":48640,\"y\":1280,\"z\":55808,\"room_num\":196},{\"object_id\":180,\"x\":51712,\"y\":-768,\"z\":53760,\"room_num\":197},{\"object_id\":71,\"x\":49664,\"y\":-512,\"z\":53760,\"room_num\":197},{\"object_id\":71,\"x\":51712,\"y\":-512,\"z\":62976,\"room_num\":197},{\"object_id\":176,\"x\":54784,\"y\":-512,\"z\":58880,\"room_num\":198},{\"object_id\":88,\"x\":35328,\"y\":-384,\"z\":26112,\"room_num\":200},{\"object_id\":88,\"x\":35328,\"y\":-384,\"z\":27136,\"room_num\":200},{\"object_id\":87,\"x\":43520,\"y\":4224,\"z\":26112,\"room_num\":201},{\"object_id\":87,\"x\":44544,\"y\":3328,\"z\":26112,\"room_num\":201},{\"object_id\":87,\"x\":43520,\"y\":4224,\"z\":27136,\"room_num\":201},{\"object_id\":87,\"x\":49664,\"y\":6528,\"z\":27136,\"room_num\":203},{\"object_id\":206,\"x\":39424,\"y\":3584,\"z\":62976,\"room_num\":204},{\"object_id\":70,\"x\":39424,\"y\":3584,\"z\":62976,\"room_num\":204},{\"object_id\":213,\"x\":42496,\"y\":1536,\"z\":58880,\"room_num\":205},{\"object_id\":214,\"x\":42496,\"y\":1536,\"z\":57856,\"room_num\":205},{\"object_id\":205,\"x\":37376,\"y\":3584,\"z\":53760,\"room_num\":206},{\"object_id\":70,\"x\":37376,\"y\":3584,\"z\":53760,\"room_num\":206},{\"object_id\":169,\"x\":33280,\"y\":1536,\"z\":56832,\"room_num\":207},{\"object_id\":131,\"x\":32256,\"y\":4352,\"z\":58880,\"room_num\":208},{\"object_id\":133,\"x\":32256,\"y\":4352,\"z\":57856,\"room_num\":208},{\"object_id\":171,\"x\":31232,\"y\":5120,\"z\":57856,\"room_num\":208},{\"object_id\":224,\"x\":15872,\"y\":3328,\"z\":57856,\"room_num\":211},{\"object_id\":70,\"x\":22016,\"y\":4352,\"z\":57856,\"room_num\":211},{\"object_id\":349,\"x\":15872,\"y\":2560,\"z\":57856,\"room_num\":211},{\"object_id\":350,\"x\":9728,\"y\":2816,\"z\":57856,\"room_num\":211},{\"object_id\":178,\"x\":9728,\"y\":3328,\"z\":57856,\"room_num\":211},{\"object_id\":132,\"x\":10752,\"y\":2304,\"z\":69120,\"room_num\":212},{\"object_id\":232,\"x\":15872,\"y\":4096,\"z\":66048,\"room_num\":212},{\"object_id\":232,\"x\":13824,\"y\":4096,\"z\":66048,\"room_num\":212},{\"object_id\":232,\"x\":17920,\"y\":4096,\"z\":66048,\"room_num\":212},{\"object_id\":180,\"x\":22016,\"y\":3712,\"z\":67072,\"room_num\":212},{\"object_id\":24,\"x\":24064,\"y\":4864,\"z\":52736,\"room_num\":213},{\"object_id\":116,\"x\":14848,\"y\":1024,\"z\":47616,\"room_num\":215},{\"object_id\":116,\"x\":15872,\"y\":1024,\"z\":48640,\"room_num\":215},{\"object_id\":129,\"x\":15872,\"y\":4352,\"z\":46592,\"room_num\":215},{\"object_id\":129,\"x\":14848,\"y\":4352,\"z\":47616,\"room_num\":215},{\"object_id\":132,\"x\":15872,\"y\":4352,\"z\":49664,\"room_num\":215},{\"object_id\":139,\"x\":14848,\"y\":4352,\"z\":49664,\"room_num\":215},{\"object_id\":88,\"x\":45568,\"y\":1536,\"z\":58880,\"room_num\":216},{\"object_id\":88,\"x\":45568,\"y\":1536,\"z\":57856,\"room_num\":216},{\"object_id\":333,\"x\":45568,\"y\":2944,\"z\":54784,\"room_num\":217},{\"object_id\":333,\"x\":45568,\"y\":2944,\"z\":61952,\"room_num\":218},{\"object_id\":176,\"x\":42496,\"y\":4608,\"z\":58880,\"room_num\":219},{\"object_id\":114,\"x\":14848,\"y\":6912,\"z\":61952,\"room_num\":220},{\"object_id\":114,\"x\":17920,\"y\":6912,\"z\":61952,\"room_num\":220},{\"object_id\":130,\"x\":13824,\"y\":5888,\"z\":61952,\"room_num\":220},{\"object_id\":130,\"x\":19968,\"y\":5888,\"z\":61952,\"room_num\":220},{\"object_id\":224,\"x\":16896,\"y\":6912,\"z\":69120,\"room_num\":220},{\"object_id\":132,\"x\":14848,\"y\":5632,\"z\":51712,\"room_num\":221},{\"object_id\":224,\"x\":14848,\"y\":5632,\"z\":50688,\"room_num\":222}]\n    },\n    {\n        \"title\": \"The River Ganges\",\n        \"zone_num\": 0,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,16,24,29,69,71,75,87,88,90,91,123,128,131,132,133,139,142,143,144,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,224,228,232,300,301,304,305,309,310,311,315,330,331,332,333,338,339,347,349,355,366,367],\n        \"items\": [{\"object_id\":0,\"x\":84480,\"y\":-256,\"z\":58880,\"room_num\":1},{\"object_id\":305,\"x\":88576,\"y\":1280,\"z\":59904,\"room_num\":4},{\"object_id\":305,\"x\":87552,\"y\":1280,\"z\":59904,\"room_num\":4},{\"object_id\":305,\"x\":86528,\"y\":1280,\"z\":59904,\"room_num\":4},{\"object_id\":16,\"x\":87552,\"y\":-512,\"z\":62976,\"room_num\":7},{\"object_id\":338,\"x\":87552,\"y\":1792,\"z\":47616,\"room_num\":9},{\"object_id\":224,\"x\":76288,\"y\":0,\"z\":31232,\"room_num\":15},{\"object_id\":338,\"x\":86528,\"y\":1920,\"z\":34304,\"room_num\":20},{\"object_id\":177,\"x\":91648,\"y\":6912,\"z\":30208,\"room_num\":26},{\"object_id\":178,\"x\":88576,\"y\":6912,\"z\":30208,\"room_num\":26},{\"object_id\":169,\"x\":90624,\"y\":6912,\"z\":32256,\"room_num\":26},{\"object_id\":171,\"x\":55808,\"y\":768,\"z\":55808,\"room_num\":30},{\"object_id\":172,\"x\":55808,\"y\":768,\"z\":55808,\"room_num\":30},{\"object_id\":132,\"x\":87552,\"y\":-2560,\"z\":13824,\"room_num\":43},{\"object_id\":69,\"x\":97792,\"y\":-2816,\"z\":19968,\"room_num\":44},{\"object_id\":69,\"x\":86528,\"y\":-5376,\"z\":14848,\"room_num\":48},{\"object_id\":176,\"x\":85504,\"y\":-5376,\"z\":14848,\"room_num\":48},{\"object_id\":128,\"x\":85504,\"y\":-2560,\"z\":12800,\"room_num\":49},{\"object_id\":180,\"x\":80384,\"y\":-2048,\"z\":12800,\"room_num\":50},{\"object_id\":71,\"x\":79360,\"y\":-1792,\"z\":10752,\"room_num\":52},{\"object_id\":71,\"x\":80384,\"y\":-1792,\"z\":10752,\"room_num\":52},{\"object_id\":176,\"x\":78336,\"y\":640,\"z\":26112,\"room_num\":57},{\"object_id\":71,\"x\":72192,\"y\":384,\"z\":27136,\"room_num\":58},{\"object_id\":71,\"x\":69120,\"y\":-512,\"z\":33280,\"room_num\":59},{\"object_id\":71,\"x\":73216,\"y\":-768,\"z\":33280,\"room_num\":59},{\"object_id\":75,\"x\":70144,\"y\":512,\"z\":29184,\"room_num\":60},{\"object_id\":178,\"x\":70144,\"y\":512,\"z\":30208,\"room_num\":60},{\"object_id\":171,\"x\":70144,\"y\":512,\"z\":29184,\"room_num\":60},{\"object_id\":338,\"x\":70144,\"y\":1792,\"z\":20992,\"room_num\":62},{\"object_id\":69,\"x\":77312,\"y\":256,\"z\":34304,\"room_num\":72},{\"object_id\":71,\"x\":80384,\"y\":3072,\"z\":30208,\"room_num\":75},{\"object_id\":176,\"x\":71168,\"y\":256,\"z\":38400,\"room_num\":78},{\"object_id\":71,\"x\":72192,\"y\":256,\"z\":36352,\"room_num\":78},{\"object_id\":331,\"x\":74240,\"y\":640,\"z\":44544,\"room_num\":79},{\"object_id\":331,\"x\":74240,\"y\":640,\"z\":41472,\"room_num\":79},{\"object_id\":331,\"x\":73216,\"y\":640,\"z\":45568,\"room_num\":79},{\"object_id\":169,\"x\":71168,\"y\":1536,\"z\":41472,\"room_num\":79},{\"object_id\":232,\"x\":66048,\"y\":512,\"z\":42496,\"room_num\":80},{\"object_id\":232,\"x\":66048,\"y\":512,\"z\":45568,\"room_num\":80},{\"object_id\":331,\"x\":69120,\"y\":640,\"z\":45568,\"room_num\":80},{\"object_id\":71,\"x\":69120,\"y\":1536,\"z\":43520,\"room_num\":80},{\"object_id\":71,\"x\":67072,\"y\":1536,\"z\":44544,\"room_num\":80},{\"object_id\":71,\"x\":62976,\"y\":-256,\"z\":43520,\"room_num\":84},{\"object_id\":131,\"x\":64000,\"y\":512,\"z\":41472,\"room_num\":84},{\"object_id\":133,\"x\":64000,\"y\":512,\"z\":40448,\"room_num\":84},{\"object_id\":304,\"x\":58880,\"y\":-1536,\"z\":37376,\"room_num\":85},{\"object_id\":71,\"x\":64000,\"y\":-2560,\"z\":37376,\"room_num\":86},{\"object_id\":180,\"x\":64000,\"y\":-2944,\"z\":38400,\"room_num\":86},{\"object_id\":131,\"x\":57856,\"y\":-256,\"z\":37376,\"room_num\":88},{\"object_id\":133,\"x\":57856,\"y\":-256,\"z\":36352,\"room_num\":88},{\"object_id\":71,\"x\":56832,\"y\":-256,\"z\":42496,\"room_num\":89},{\"object_id\":71,\"x\":55808,\"y\":-256,\"z\":45568,\"room_num\":89},{\"object_id\":169,\"x\":54784,\"y\":-2688,\"z\":35328,\"room_num\":90},{\"object_id\":71,\"x\":51712,\"y\":-256,\"z\":40448,\"room_num\":94},{\"object_id\":71,\"x\":49664,\"y\":-2816,\"z\":43520,\"room_num\":95},{\"object_id\":71,\"x\":48640,\"y\":-256,\"z\":46592,\"room_num\":96},{\"object_id\":71,\"x\":47616,\"y\":-2560,\"z\":40448,\"room_num\":98},{\"object_id\":71,\"x\":45568,\"y\":-2560,\"z\":41472,\"room_num\":98},{\"object_id\":71,\"x\":52736,\"y\":-2560,\"z\":38400,\"room_num\":99},{\"object_id\":71,\"x\":50688,\"y\":-2304,\"z\":35328,\"room_num\":99},{\"object_id\":171,\"x\":53760,\"y\":-2816,\"z\":38400,\"room_num\":99},{\"object_id\":174,\"x\":53760,\"y\":-2816,\"z\":36352,\"room_num\":99},{\"object_id\":224,\"x\":53760,\"y\":-3072,\"z\":37376,\"room_num\":99},{\"object_id\":333,\"x\":45568,\"y\":-1664,\"z\":42496,\"room_num\":100},{\"object_id\":71,\"x\":47616,\"y\":-1024,\"z\":45568,\"room_num\":100},{\"object_id\":331,\"x\":45568,\"y\":-1408,\"z\":37376,\"room_num\":101},{\"object_id\":132,\"x\":51712,\"y\":-512,\"z\":39424,\"room_num\":102},{\"object_id\":128,\"x\":53760,\"y\":-1024,\"z\":37376,\"room_num\":102},{\"object_id\":71,\"x\":50688,\"y\":-512,\"z\":36352,\"room_num\":102},{\"object_id\":170,\"x\":55808,\"y\":-3840,\"z\":25088,\"room_num\":113},{\"object_id\":338,\"x\":56832,\"y\":1792,\"z\":20992,\"room_num\":116},{\"object_id\":169,\"x\":49664,\"y\":4608,\"z\":75264,\"room_num\":122},{\"object_id\":172,\"x\":49664,\"y\":4608,\"z\":75264,\"room_num\":122},{\"object_id\":180,\"x\":49664,\"y\":4352,\"z\":76288,\"room_num\":122},{\"object_id\":178,\"x\":91648,\"y\":-2048,\"z\":61952,\"room_num\":124},{\"object_id\":173,\"x\":90624,\"y\":-2048,\"z\":61952,\"room_num\":124},{\"object_id\":180,\"x\":54784,\"y\":-5120,\"z\":27136,\"room_num\":125},{\"object_id\":171,\"x\":54784,\"y\":-5120,\"z\":26112,\"room_num\":125},{\"object_id\":172,\"x\":54784,\"y\":-5120,\"z\":28160,\"room_num\":125},{\"object_id\":29,\"x\":35328,\"y\":-3584,\"z\":36352,\"room_num\":130},{\"object_id\":171,\"x\":43520,\"y\":-2560,\"z\":33280,\"room_num\":137},{\"object_id\":29,\"x\":42496,\"y\":-6656,\"z\":45568,\"room_num\":139},{\"object_id\":180,\"x\":42496,\"y\":-5504,\"z\":43520,\"room_num\":139},{\"object_id\":69,\"x\":43520,\"y\":-2560,\"z\":45568,\"room_num\":141},{\"object_id\":170,\"x\":42496,\"y\":-2560,\"z\":44544,\"room_num\":141},{\"object_id\":338,\"x\":48640,\"y\":1792,\"z\":27136,\"room_num\":142},{\"object_id\":338,\"x\":38400,\"y\":1792,\"z\":48640,\"room_num\":147},{\"object_id\":71,\"x\":47616,\"y\":0,\"z\":49664,\"room_num\":149},{\"object_id\":71,\"x\":48640,\"y\":128,\"z\":51712,\"room_num\":149},{\"object_id\":338,\"x\":46592,\"y\":1792,\"z\":55808,\"room_num\":153},{\"object_id\":29,\"x\":31232,\"y\":-7552,\"z\":44544,\"room_num\":154},{\"object_id\":29,\"x\":33280,\"y\":-7296,\"z\":44544,\"room_num\":154},{\"object_id\":29,\"x\":41472,\"y\":-1152,\"z\":67072,\"room_num\":156},{\"object_id\":338,\"x\":46592,\"y\":1920,\"z\":67072,\"room_num\":157},{\"object_id\":71,\"x\":48640,\"y\":512,\"z\":62976,\"room_num\":159},{\"object_id\":71,\"x\":44544,\"y\":512,\"z\":59904,\"room_num\":159},{\"object_id\":180,\"x\":46592,\"y\":0,\"z\":60928,\"room_num\":159},{\"object_id\":29,\"x\":45568,\"y\":-3840,\"z\":75264,\"room_num\":161},{\"object_id\":173,\"x\":42496,\"y\":4736,\"z\":79360,\"room_num\":162},{\"object_id\":172,\"x\":42496,\"y\":4736,\"z\":79360,\"room_num\":162},{\"object_id\":29,\"x\":42496,\"y\":4352,\"z\":79360,\"room_num\":162},{\"object_id\":305,\"x\":45568,\"y\":8448,\"z\":72192,\"room_num\":163},{\"object_id\":305,\"x\":44544,\"y\":8320,\"z\":72192,\"room_num\":163},{\"object_id\":305,\"x\":43520,\"y\":8448,\"z\":72192,\"room_num\":163},{\"object_id\":305,\"x\":42496,\"y\":8320,\"z\":72192,\"room_num\":163},{\"object_id\":176,\"x\":45568,\"y\":10112,\"z\":75264,\"room_num\":166},{\"object_id\":139,\"x\":34304,\"y\":-5120,\"z\":50688,\"room_num\":170},{\"object_id\":176,\"x\":39424,\"y\":-5120,\"z\":62976,\"room_num\":174},{\"object_id\":170,\"x\":40448,\"y\":-5120,\"z\":62976,\"room_num\":174},{\"object_id\":87,\"x\":82432,\"y\":640,\"z\":32256,\"room_num\":175},{\"object_id\":87,\"x\":81408,\"y\":640,\"z\":33280,\"room_num\":175},{\"object_id\":87,\"x\":82432,\"y\":640,\"z\":34304,\"room_num\":175},{\"object_id\":331,\"x\":85504,\"y\":-2176,\"z\":35328,\"room_num\":176},{\"object_id\":331,\"x\":85504,\"y\":-2176,\"z\":32256,\"room_num\":176},{\"object_id\":180,\"x\":85504,\"y\":-1536,\"z\":34304,\"room_num\":176},{\"object_id\":177,\"x\":85504,\"y\":-1280,\"z\":33280,\"room_num\":176},{\"object_id\":161,\"x\":81408,\"y\":-1792,\"z\":35328,\"room_num\":176}]\n    },\n    {\n        \"title\": \"Caves of Kaliya\",\n        \"zone_num\": 0,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,24,69,73,87,88,90,91,97,107,142,143,144,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,240,244,300,301,304,305,309,310,311,315,330,331,332,333,338,339,347,355,366,367],\n        \"items\": [{\"object_id\":69,\"x\":29184,\"y\":-6912,\"z\":57856,\"room_num\":4},{\"object_id\":69,\"x\":29184,\"y\":-6912,\"z\":55808,\"room_num\":4},{\"object_id\":69,\"x\":27136,\"y\":-6912,\"z\":57856,\"room_num\":4},{\"object_id\":69,\"x\":27136,\"y\":-6912,\"z\":55808,\"room_num\":4},{\"object_id\":69,\"x\":25088,\"y\":-6912,\"z\":57856,\"room_num\":4},{\"object_id\":69,\"x\":25088,\"y\":-6912,\"z\":55808,\"room_num\":4},{\"object_id\":69,\"x\":31232,\"y\":-6912,\"z\":57856,\"room_num\":4},{\"object_id\":69,\"x\":31232,\"y\":-6912,\"z\":55808,\"room_num\":4},{\"object_id\":69,\"x\":23040,\"y\":-6912,\"z\":57856,\"room_num\":4},{\"object_id\":69,\"x\":23040,\"y\":-6912,\"z\":55808,\"room_num\":4},{\"object_id\":180,\"x\":24064,\"y\":-11264,\"z\":56832,\"room_num\":5},{\"object_id\":178,\"x\":23040,\"y\":-11008,\"z\":56832,\"room_num\":5},{\"object_id\":88,\"x\":19968,\"y\":-8448,\"z\":56832,\"room_num\":7},{\"object_id\":69,\"x\":16896,\"y\":-15360,\"z\":49664,\"room_num\":9},{\"object_id\":171,\"x\":16896,\"y\":-15360,\"z\":53760,\"room_num\":9},{\"object_id\":178,\"x\":11776,\"y\":-12032,\"z\":59904,\"room_num\":10},{\"object_id\":69,\"x\":10752,\"y\":-12032,\"z\":60928,\"room_num\":10},{\"object_id\":88,\"x\":9728,\"y\":-17024,\"z\":44544,\"room_num\":14},{\"object_id\":88,\"x\":18944,\"y\":-14464,\"z\":51712,\"room_num\":15},{\"object_id\":69,\"x\":26112,\"y\":-14848,\"z\":62976,\"room_num\":16},{\"object_id\":176,\"x\":26112,\"y\":-14848,\"z\":61952,\"room_num\":16},{\"object_id\":97,\"x\":20992,\"y\":-14848,\"z\":59904,\"room_num\":17},{\"object_id\":169,\"x\":25088,\"y\":-14848,\"z\":55808,\"room_num\":17},{\"object_id\":69,\"x\":16896,\"y\":-14848,\"z\":55808,\"room_num\":17},{\"object_id\":0,\"x\":3584,\"y\":-15360,\"z\":47616,\"room_num\":20},{\"object_id\":73,\"x\":51712,\"y\":-256,\"z\":52736,\"room_num\":22},{\"object_id\":176,\"x\":51712,\"y\":-256,\"z\":56832,\"room_num\":22},{\"object_id\":240,\"x\":51712,\"y\":-256,\"z\":52736,\"room_num\":22},{\"object_id\":304,\"x\":51712,\"y\":-1024,\"z\":52736,\"room_num\":22},{\"object_id\":167,\"x\":55808,\"y\":-256,\"z\":52736,\"room_num\":22},{\"object_id\":175,\"x\":51712,\"y\":-256,\"z\":48640,\"room_num\":22},{\"object_id\":176,\"x\":40448,\"y\":-4864,\"z\":52736,\"room_num\":24},{\"object_id\":180,\"x\":41472,\"y\":-5248,\"z\":52736,\"room_num\":24}]\n    },\n    {\n        \"title\": \"Coastal Village\",\n        \"zone_num\": 1,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,13,20,21,24,32,74,75,77,87,90,91,94,111,117,128,131,132,133,134,139,142,143,144,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,209,213,217,224,228,232,244,245,246,247,300,301,304,305,309,310,311,315,330,331,332,333,338,339,347,349,350,355,366,367],\n        \"items\": [{\"object_id\":305,\"x\":28160,\"y\":-128,\"z\":40448,\"room_num\":1},{\"object_id\":305,\"x\":29184,\"y\":-256,\"z\":39424,\"room_num\":1},{\"object_id\":176,\"x\":13824,\"y\":-512,\"z\":38400,\"room_num\":2},{\"object_id\":173,\"x\":5632,\"y\":-512,\"z\":33280,\"room_num\":2},{\"object_id\":176,\"x\":14848,\"y\":-1280,\"z\":26112,\"room_num\":3},{\"object_id\":305,\"x\":33280,\"y\":128,\"z\":30208,\"room_num\":7},{\"object_id\":305,\"x\":33280,\"y\":320,\"z\":29184,\"room_num\":7},{\"object_id\":305,\"x\":34304,\"y\":128,\"z\":31232,\"room_num\":7},{\"object_id\":339,\"x\":20992,\"y\":448,\"z\":24064,\"room_num\":7},{\"object_id\":349,\"x\":19968,\"y\":2176,\"z\":13824,\"room_num\":7},{\"object_id\":349,\"x\":18944,\"y\":2688,\"z\":15872,\"room_num\":7},{\"object_id\":224,\"x\":27136,\"y\":3200,\"z\":17920,\"room_num\":7},{\"object_id\":339,\"x\":28160,\"y\":256,\"z\":18944,\"room_num\":7},{\"object_id\":0,\"x\":18944,\"y\":2304,\"z\":13824,\"room_num\":7},{\"object_id\":339,\"x\":25088,\"y\":512,\"z\":30208,\"room_num\":7},{\"object_id\":139,\"x\":8704,\"y\":-2816,\"z\":56832,\"room_num\":8},{\"object_id\":330,\"x\":8704,\"y\":-3072,\"z\":54784,\"room_num\":8},{\"object_id\":232,\"x\":7680,\"y\":-2816,\"z\":57856,\"room_num\":8},{\"object_id\":94,\"x\":4608,\"y\":1536,\"z\":56832,\"room_num\":12},{\"object_id\":132,\"x\":5632,\"y\":-896,\"z\":56832,\"room_num\":12},{\"object_id\":24,\"x\":6656,\"y\":1792,\"z\":56832,\"room_num\":12},{\"object_id\":180,\"x\":8704,\"y\":768,\"z\":56832,\"room_num\":12},{\"object_id\":143,\"x\":13824,\"y\":-5888,\"z\":79360,\"room_num\":14},{\"object_id\":143,\"x\":18944,\"y\":-5888,\"z\":79360,\"room_num\":14},{\"object_id\":143,\"x\":17920,\"y\":-5632,\"z\":79360,\"room_num\":14},{\"object_id\":143,\"x\":14848,\"y\":-5632,\"z\":79360,\"room_num\":14},{\"object_id\":142,\"x\":15872,\"y\":-5376,\"z\":79360,\"room_num\":14},{\"object_id\":142,\"x\":16896,\"y\":-5376,\"z\":79360,\"room_num\":14},{\"object_id\":20,\"x\":14848,\"y\":-384,\"z\":78336,\"room_num\":14},{\"object_id\":91,\"x\":13824,\"y\":-1280,\"z\":81408,\"room_num\":14},{\"object_id\":91,\"x\":17920,\"y\":-1024,\"z\":81408,\"room_num\":14},{\"object_id\":177,\"x\":18944,\"y\":-1792,\"z\":80384,\"room_num\":14},{\"object_id\":175,\"x\":13824,\"y\":-7680,\"z\":72192,\"room_num\":15},{\"object_id\":205,\"x\":32256,\"y\":2816,\"z\":91648,\"room_num\":17},{\"object_id\":20,\"x\":74240,\"y\":-2048,\"z\":77312,\"room_num\":19},{\"object_id\":350,\"x\":72192,\"y\":-3712,\"z\":74240,\"room_num\":19},{\"object_id\":24,\"x\":14848,\"y\":1152,\"z\":49664,\"room_num\":20},{\"object_id\":139,\"x\":60928,\"y\":1792,\"z\":70144,\"room_num\":27},{\"object_id\":172,\"x\":25088,\"y\":-4608,\"z\":52736,\"room_num\":30},{\"object_id\":32,\"x\":64000,\"y\":1920,\"z\":75264,\"room_num\":31},{\"object_id\":349,\"x\":32256,\"y\":256,\"z\":88576,\"room_num\":35},{\"object_id\":349,\"x\":32256,\"y\":256,\"z\":87552,\"room_num\":35},{\"object_id\":349,\"x\":33280,\"y\":128,\"z\":89600,\"room_num\":35},{\"object_id\":349,\"x\":32256,\"y\":256,\"z\":91648,\"room_num\":35},{\"object_id\":175,\"x\":43520,\"y\":-2048,\"z\":81408,\"room_num\":37},{\"object_id\":128,\"x\":25088,\"y\":-6912,\"z\":77312,\"room_num\":38},{\"object_id\":91,\"x\":38400,\"y\":-7040,\"z\":79360,\"room_num\":40},{\"object_id\":91,\"x\":35328,\"y\":-6912,\"z\":77312,\"room_num\":40},{\"object_id\":91,\"x\":36352,\"y\":-6912,\"z\":81408,\"room_num\":40},{\"object_id\":180,\"x\":36352,\"y\":-6912,\"z\":79360,\"room_num\":40},{\"object_id\":91,\"x\":29184,\"y\":-7424,\"z\":81408,\"room_num\":41},{\"object_id\":91,\"x\":29184,\"y\":-7040,\"z\":77312,\"room_num\":41},{\"object_id\":304,\"x\":29184,\"y\":-10880,\"z\":77312,\"room_num\":41},{\"object_id\":177,\"x\":30208,\"y\":-6912,\"z\":79360,\"room_num\":42},{\"object_id\":330,\"x\":52736,\"y\":-128,\"z\":78336,\"room_num\":48},{\"object_id\":176,\"x\":52736,\"y\":0,\"z\":80384,\"room_num\":48},{\"object_id\":177,\"x\":46592,\"y\":-512,\"z\":70144,\"room_num\":49},{\"object_id\":349,\"x\":31232,\"y\":128,\"z\":89600,\"room_num\":52},{\"object_id\":349,\"x\":31232,\"y\":256,\"z\":90624,\"room_num\":52},{\"object_id\":180,\"x\":25088,\"y\":2432,\"z\":93696,\"room_num\":53},{\"object_id\":174,\"x\":43520,\"y\":0,\"z\":85504,\"room_num\":54},{\"object_id\":20,\"x\":57856,\"y\":0,\"z\":85504,\"room_num\":56},{\"object_id\":24,\"x\":39424,\"y\":-3200,\"z\":79360,\"room_num\":59},{\"object_id\":180,\"x\":5632,\"y\":-6656,\"z\":76288,\"room_num\":62},{\"object_id\":21,\"x\":8704,\"y\":-5376,\"z\":71168,\"room_num\":62},{\"object_id\":173,\"x\":8704,\"y\":-6400,\"z\":69120,\"room_num\":62},{\"object_id\":21,\"x\":6656,\"y\":-6656,\"z\":76288,\"room_num\":62},{\"object_id\":177,\"x\":3584,\"y\":-5248,\"z\":82432,\"room_num\":62},{\"object_id\":176,\"x\":6656,\"y\":-6656,\"z\":76288,\"room_num\":62},{\"object_id\":32,\"x\":4608,\"y\":3584,\"z\":66048,\"room_num\":63},{\"object_id\":32,\"x\":5632,\"y\":3968,\"z\":81408,\"room_num\":63},{\"object_id\":164,\"x\":3584,\"y\":4352,\"z\":65024,\"room_num\":63},{\"object_id\":172,\"x\":4608,\"y\":4352,\"z\":65024,\"room_num\":63},{\"object_id\":172,\"x\":3584,\"y\":4352,\"z\":79360,\"room_num\":63},{\"object_id\":172,\"x\":8704,\"y\":4352,\"z\":72192,\"room_num\":63},{\"object_id\":20,\"x\":23040,\"y\":-6912,\"z\":77312,\"room_num\":65},{\"object_id\":173,\"x\":22016,\"y\":-768,\"z\":74240,\"room_num\":70},{\"object_id\":169,\"x\":22016,\"y\":-768,\"z\":75264,\"room_num\":70},{\"object_id\":305,\"x\":31232,\"y\":9984,\"z\":93696,\"room_num\":71},{\"object_id\":332,\"x\":38400,\"y\":-4352,\"z\":62976,\"room_num\":72},{\"object_id\":87,\"x\":38400,\"y\":384,\"z\":69120,\"room_num\":74},{\"object_id\":87,\"x\":38400,\"y\":384,\"z\":68096,\"room_num\":74},{\"object_id\":87,\"x\":37376,\"y\":384,\"z\":68096,\"room_num\":74},{\"object_id\":87,\"x\":37376,\"y\":384,\"z\":69120,\"room_num\":74},{\"object_id\":139,\"x\":37376,\"y\":-512,\"z\":69120,\"room_num\":74},{\"object_id\":139,\"x\":37376,\"y\":-512,\"z\":68096,\"room_num\":74},{\"object_id\":139,\"x\":38400,\"y\":-512,\"z\":68096,\"room_num\":74},{\"object_id\":139,\"x\":38400,\"y\":-512,\"z\":69120,\"room_num\":74},{\"object_id\":304,\"x\":37376,\"y\":-640,\"z\":69120,\"room_num\":74},{\"object_id\":169,\"x\":38400,\"y\":-2176,\"z\":68096,\"room_num\":75},{\"object_id\":128,\"x\":36352,\"y\":-3584,\"z\":73216,\"room_num\":76},{\"object_id\":87,\"x\":27136,\"y\":-3904,\"z\":64000,\"room_num\":76},{\"object_id\":20,\"x\":32256,\"y\":-3584,\"z\":73216,\"room_num\":76},{\"object_id\":111,\"x\":23040,\"y\":-7424,\"z\":81408,\"room_num\":77},{\"object_id\":180,\"x\":47616,\"y\":-512,\"z\":68096,\"room_num\":79},{\"object_id\":21,\"x\":62976,\"y\":0,\"z\":81408,\"room_num\":88},{\"object_id\":176,\"x\":56832,\"y\":-2048,\"z\":92672,\"room_num\":89},{\"object_id\":21,\"x\":57856,\"y\":-2048,\"z\":90624,\"room_num\":89},{\"object_id\":20,\"x\":54784,\"y\":0,\"z\":75264,\"room_num\":98},{\"object_id\":330,\"x\":50688,\"y\":-3456,\"z\":69120,\"room_num\":102},{\"object_id\":330,\"x\":54784,\"y\":-3456,\"z\":69120,\"room_num\":102},{\"object_id\":20,\"x\":53760,\"y\":-2048,\"z\":87552,\"room_num\":116},{\"object_id\":24,\"x\":43520,\"y\":-1408,\"z\":94720,\"room_num\":117},{\"object_id\":20,\"x\":22016,\"y\":-2048,\"z\":89600,\"room_num\":119},{\"object_id\":331,\"x\":28160,\"y\":-4992,\"z\":88576,\"room_num\":120},{\"object_id\":331,\"x\":36352,\"y\":-4864,\"z\":88576,\"room_num\":121},{\"object_id\":305,\"x\":32256,\"y\":-768,\"z\":86528,\"room_num\":121},{\"object_id\":205,\"x\":35328,\"y\":-4096,\"z\":88576,\"room_num\":121},{\"object_id\":169,\"x\":17920,\"y\":-6784,\"z\":92672,\"room_num\":123},{\"object_id\":205,\"x\":25088,\"y\":-6400,\"z\":88576,\"room_num\":124},{\"object_id\":305,\"x\":33280,\"y\":9984,\"z\":93696,\"room_num\":126},{\"object_id\":305,\"x\":32256,\"y\":9984,\"z\":93696,\"room_num\":126},{\"object_id\":87,\"x\":34304,\"y\":9728,\"z\":93696,\"room_num\":126},{\"object_id\":87,\"x\":34304,\"y\":9728,\"z\":94720,\"room_num\":126},{\"object_id\":87,\"x\":35328,\"y\":9600,\"z\":93696,\"room_num\":126},{\"object_id\":87,\"x\":35328,\"y\":9600,\"z\":94720,\"room_num\":126},{\"object_id\":87,\"x\":35328,\"y\":9728,\"z\":95744,\"room_num\":126},{\"object_id\":87,\"x\":36352,\"y\":9600,\"z\":93696,\"room_num\":126},{\"object_id\":87,\"x\":36352,\"y\":9600,\"z\":94720,\"room_num\":126},{\"object_id\":87,\"x\":36352,\"y\":9600,\"z\":95744,\"room_num\":126},{\"object_id\":87,\"x\":34304,\"y\":9856,\"z\":95744,\"room_num\":126},{\"object_id\":176,\"x\":36352,\"y\":-2304,\"z\":94720,\"room_num\":127},{\"object_id\":305,\"x\":32256,\"y\":-5888,\"z\":77312,\"room_num\":128},{\"object_id\":87,\"x\":33280,\"y\":-6272,\"z\":81408,\"room_num\":128},{\"object_id\":87,\"x\":33280,\"y\":-6272,\"z\":80384,\"room_num\":128},{\"object_id\":87,\"x\":33280,\"y\":-6272,\"z\":79360,\"room_num\":128},{\"object_id\":87,\"x\":33280,\"y\":-6272,\"z\":78336,\"room_num\":128},{\"object_id\":87,\"x\":33280,\"y\":-6272,\"z\":77312,\"room_num\":128},{\"object_id\":305,\"x\":32256,\"y\":2560,\"z\":93696,\"room_num\":130},{\"object_id\":131,\"x\":42496,\"y\":-2560,\"z\":92672,\"room_num\":133},{\"object_id\":131,\"x\":42496,\"y\":-2560,\"z\":93696,\"room_num\":133},{\"object_id\":213,\"x\":42496,\"y\":-2560,\"z\":92672,\"room_num\":133},{\"object_id\":213,\"x\":42496,\"y\":-2560,\"z\":93696,\"room_num\":133},{\"object_id\":131,\"x\":42496,\"y\":-2560,\"z\":91648,\"room_num\":134},{\"object_id\":213,\"x\":42496,\"y\":-2560,\"z\":91648,\"room_num\":134},{\"object_id\":20,\"x\":42496,\"y\":-2560,\"z\":91648,\"room_num\":134},{\"object_id\":128,\"x\":38400,\"y\":-4352,\"z\":67072,\"room_num\":136},{\"object_id\":128,\"x\":37376,\"y\":-4352,\"z\":66048,\"room_num\":136},{\"object_id\":131,\"x\":38400,\"y\":-4352,\"z\":66048,\"room_num\":136},{\"object_id\":131,\"x\":38400,\"y\":-4352,\"z\":66048,\"room_num\":136},{\"object_id\":332,\"x\":38400,\"y\":-4352,\"z\":64000,\"room_num\":136},{\"object_id\":332,\"x\":38400,\"y\":-4352,\"z\":65024,\"room_num\":136},{\"object_id\":139,\"x\":38400,\"y\":-5120,\"z\":64000,\"room_num\":136},{\"object_id\":304,\"x\":38400,\"y\":-4480,\"z\":66048,\"room_num\":136},{\"object_id\":332,\"x\":37376,\"y\":-4352,\"z\":66048,\"room_num\":136},{\"object_id\":20,\"x\":38400,\"y\":-4352,\"z\":55808,\"room_num\":137},{\"object_id\":331,\"x\":3584,\"y\":-3328,\"z\":72192,\"room_num\":138},{\"object_id\":87,\"x\":4608,\"y\":-640,\"z\":81408,\"room_num\":139},{\"object_id\":139,\"x\":33280,\"y\":-3584,\"z\":64000,\"room_num\":140},{\"object_id\":139,\"x\":36352,\"y\":-4608,\"z\":61952,\"room_num\":143},{\"object_id\":134,\"x\":34304,\"y\":-4096,\"z\":61952,\"room_num\":143},{\"object_id\":87,\"x\":27136,\"y\":-3840,\"z\":62976,\"room_num\":143},{\"object_id\":87,\"x\":28160,\"y\":-3840,\"z\":61952,\"room_num\":143},{\"object_id\":87,\"x\":28160,\"y\":-3584,\"z\":60928,\"room_num\":143},{\"object_id\":87,\"x\":27136,\"y\":-3584,\"z\":61952,\"room_num\":143},{\"object_id\":87,\"x\":28160,\"y\":-4224,\"z\":62976,\"room_num\":143},{\"object_id\":87,\"x\":29184,\"y\":-4096,\"z\":62976,\"room_num\":143},{\"object_id\":139,\"x\":36352,\"y\":-4608,\"z\":60928,\"room_num\":143},{\"object_id\":87,\"x\":26112,\"y\":-3712,\"z\":61952,\"room_num\":143},{\"object_id\":20,\"x\":24064,\"y\":-1024,\"z\":72192,\"room_num\":145},{\"object_id\":20,\"x\":26112,\"y\":-512,\"z\":65024,\"room_num\":145},{\"object_id\":178,\"x\":30208,\"y\":-512,\"z\":62976,\"room_num\":147},{\"object_id\":21,\"x\":24064,\"y\":-2048,\"z\":56832,\"room_num\":150},{\"object_id\":20,\"x\":27136,\"y\":-512,\"z\":70144,\"room_num\":156},{\"object_id\":117,\"x\":33280,\"y\":-2048,\"z\":58880,\"room_num\":159},{\"object_id\":20,\"x\":31232,\"y\":-2048,\"z\":59904,\"room_num\":159},{\"object_id\":20,\"x\":68096,\"y\":-1152,\"z\":80384,\"room_num\":162},{\"object_id\":20,\"x\":69120,\"y\":-1536,\"z\":79360,\"room_num\":162},{\"object_id\":142,\"x\":69120,\"y\":-4608,\"z\":79360,\"room_num\":163},{\"object_id\":142,\"x\":68096,\"y\":-4608,\"z\":79360,\"room_num\":163},{\"object_id\":170,\"x\":25088,\"y\":-7424,\"z\":84480,\"room_num\":178},{\"object_id\":117,\"x\":49664,\"y\":-3840,\"z\":66048,\"room_num\":182},{\"object_id\":134,\"x\":49664,\"y\":-4096,\"z\":66048,\"room_num\":182},{\"object_id\":20,\"x\":41472,\"y\":-3840,\"z\":66048,\"room_num\":186},{\"object_id\":111,\"x\":43520,\"y\":-3840,\"z\":66048,\"room_num\":186},{\"object_id\":111,\"x\":43520,\"y\":-4864,\"z\":66048,\"room_num\":186},{\"object_id\":117,\"x\":76288,\"y\":-2304,\"z\":78336,\"room_num\":188},{\"object_id\":21,\"x\":76288,\"y\":-2304,\"z\":78336,\"room_num\":188}]\n    },\n    {\n        \"title\": \"Crash Site\",\n        \"zone_num\": 1,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,13,18,24,34,37,38,48,74,75,76,77,78,79,80,81,82,87,90,91,102,107,111,118,128,129,130,131,132,139,140,142,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,224,225,228,229,232,233,236,238,244,245,246,247,272,273,287,288,300,301,304,305,309,310,311,315,318,330,331,332,333,338,339,347,351,352,354,355,365,366,367],\n        \"items\": [{\"object_id\":288,\"x\":71168,\"y\":-256,\"z\":43520,\"room_num\":1},{\"object_id\":75,\"x\":70144,\"y\":0,\"z\":46592,\"room_num\":1},{\"object_id\":18,\"x\":68096,\"y\":-512,\"z\":49664,\"room_num\":1},{\"object_id\":37,\"x\":72192,\"y\":-256,\"z\":48640,\"room_num\":1},{\"object_id\":48,\"x\":71168,\"y\":0,\"z\":58880,\"room_num\":1},{\"object_id\":48,\"x\":80384,\"y\":0,\"z\":44544,\"room_num\":1},{\"object_id\":173,\"x\":72192,\"y\":-256,\"z\":48640,\"room_num\":1},{\"object_id\":37,\"x\":70144,\"y\":0,\"z\":49664,\"room_num\":1},{\"object_id\":173,\"x\":70144,\"y\":0,\"z\":49664,\"room_num\":1},{\"object_id\":102,\"x\":76288,\"y\":-2048,\"z\":58880,\"room_num\":3},{\"object_id\":102,\"x\":80384,\"y\":-2048,\"z\":57856,\"room_num\":3},{\"object_id\":288,\"x\":58880,\"y\":-2048,\"z\":49664,\"room_num\":4},{\"object_id\":165,\"x\":56832,\"y\":-2048,\"z\":49664,\"room_num\":4},{\"object_id\":305,\"x\":77312,\"y\":2048,\"z\":43520,\"room_num\":8},{\"object_id\":305,\"x\":75264,\"y\":2048,\"z\":43520,\"room_num\":8},{\"object_id\":131,\"x\":54784,\"y\":0,\"z\":49664,\"room_num\":9},{\"object_id\":318,\"x\":57856,\"y\":-512,\"z\":49664,\"room_num\":9},{\"object_id\":288,\"x\":98816,\"y\":-4096,\"z\":1536,\"room_num\":15},{\"object_id\":288,\"x\":99840,\"y\":-4096,\"z\":1536,\"room_num\":15},{\"object_id\":288,\"x\":100864,\"y\":-4096,\"z\":1536,\"room_num\":15},{\"object_id\":77,\"x\":98816,\"y\":-4096,\"z\":1536,\"room_num\":15},{\"object_id\":77,\"x\":99840,\"y\":-4096,\"z\":1536,\"room_num\":15},{\"object_id\":77,\"x\":100864,\"y\":-4096,\"z\":1536,\"room_num\":15},{\"object_id\":232,\"x\":53760,\"y\":-2560,\"z\":50688,\"room_num\":16},{\"object_id\":233,\"x\":53760,\"y\":-2560,\"z\":48640,\"room_num\":16},{\"object_id\":365,\"x\":8704,\"y\":-2048,\"z\":77312,\"room_num\":19},{\"object_id\":140,\"x\":58880,\"y\":-4864,\"z\":62976,\"room_num\":24},{\"object_id\":140,\"x\":57856,\"y\":-4864,\"z\":62976,\"room_num\":24},{\"object_id\":129,\"x\":53760,\"y\":-1792,\"z\":60928,\"room_num\":24},{\"object_id\":140,\"x\":59904,\"y\":-3328,\"z\":66048,\"room_num\":24},{\"object_id\":140,\"x\":58880,\"y\":-3328,\"z\":66048,\"room_num\":24},{\"object_id\":129,\"x\":59904,\"y\":-2560,\"z\":64000,\"room_num\":24},{\"object_id\":140,\"x\":56832,\"y\":-4864,\"z\":64000,\"room_num\":24},{\"object_id\":132,\"x\":57856,\"y\":-1024,\"z\":67072,\"room_num\":24},{\"object_id\":304,\"x\":58880,\"y\":-2816,\"z\":66048,\"room_num\":24},{\"object_id\":304,\"x\":57856,\"y\":-5120,\"z\":64000,\"room_num\":24},{\"object_id\":180,\"x\":56832,\"y\":-768,\"z\":64000,\"room_num\":24},{\"object_id\":171,\"x\":61952,\"y\":-2048,\"z\":62976,\"room_num\":25},{\"object_id\":129,\"x\":59904,\"y\":2048,\"z\":66048,\"room_num\":26},{\"object_id\":132,\"x\":58880,\"y\":2048,\"z\":66048,\"room_num\":26},{\"object_id\":132,\"x\":59904,\"y\":2048,\"z\":66048,\"room_num\":26},{\"object_id\":87,\"x\":56832,\"y\":10112,\"z\":67072,\"room_num\":27},{\"object_id\":87,\"x\":55808,\"y\":10112,\"z\":66048,\"room_num\":27},{\"object_id\":87,\"x\":55808,\"y\":10112,\"z\":67072,\"room_num\":27},{\"object_id\":87,\"x\":56832,\"y\":10112,\"z\":66048,\"room_num\":27},{\"object_id\":34,\"x\":58880,\"y\":1024,\"z\":36352,\"room_num\":29},{\"object_id\":34,\"x\":58880,\"y\":1024,\"z\":33280,\"room_num\":29},{\"object_id\":34,\"x\":56832,\"y\":1024,\"z\":30208,\"room_num\":29},{\"object_id\":34,\"x\":54784,\"y\":1024,\"z\":30208,\"room_num\":29},{\"object_id\":34,\"x\":52736,\"y\":1024,\"z\":33280,\"room_num\":29},{\"object_id\":34,\"x\":52736,\"y\":1024,\"z\":36352,\"room_num\":29},{\"object_id\":34,\"x\":56832,\"y\":1024,\"z\":39424,\"room_num\":29},{\"object_id\":177,\"x\":56832,\"y\":1024,\"z\":32256,\"room_num\":29},{\"object_id\":352,\"x\":56832,\"y\":1024,\"z\":33280,\"room_num\":29},{\"object_id\":165,\"x\":55808,\"y\":1024,\"z\":33280,\"room_num\":29},{\"object_id\":129,\"x\":52736,\"y\":1024,\"z\":33280,\"room_num\":29},{\"object_id\":129,\"x\":58880,\"y\":1024,\"z\":36352,\"room_num\":29},{\"object_id\":354,\"x\":54784,\"y\":1024,\"z\":36352,\"room_num\":29},{\"object_id\":352,\"x\":59904,\"y\":-2048,\"z\":16896,\"room_num\":30},{\"object_id\":225,\"x\":59904,\"y\":-2048,\"z\":16896,\"room_num\":30},{\"object_id\":288,\"x\":62976,\"y\":-2048,\"z\":19968,\"room_num\":30},{\"object_id\":180,\"x\":60928,\"y\":-2048,\"z\":18944,\"room_num\":30},{\"object_id\":87,\"x\":76288,\"y\":9600,\"z\":57856,\"room_num\":31},{\"object_id\":87,\"x\":76288,\"y\":9600,\"z\":58880,\"room_num\":31},{\"object_id\":87,\"x\":77312,\"y\":9600,\"z\":57856,\"room_num\":31},{\"object_id\":87,\"x\":77312,\"y\":9600,\"z\":58880,\"room_num\":31},{\"object_id\":48,\"x\":72192,\"y\":-256,\"z\":37376,\"room_num\":32},{\"object_id\":129,\"x\":19968,\"y\":1792,\"z\":70144,\"room_num\":35},{\"object_id\":118,\"x\":52736,\"y\":-256,\"z\":50688,\"room_num\":38},{\"object_id\":177,\"x\":84480,\"y\":-2048,\"z\":55808,\"room_num\":42},{\"object_id\":180,\"x\":84480,\"y\":-2048,\"z\":56832,\"room_num\":42},{\"object_id\":132,\"x\":54784,\"y\":1024,\"z\":40448,\"room_num\":43},{\"object_id\":173,\"x\":57856,\"y\":1024,\"z\":40448,\"room_num\":43},{\"object_id\":175,\"x\":38400,\"y\":-2304,\"z\":81408,\"room_num\":44},{\"object_id\":287,\"x\":23040,\"y\":2048,\"z\":49664,\"room_num\":45},{\"object_id\":34,\"x\":20992,\"y\":2048,\"z\":47616,\"room_num\":45},{\"object_id\":34,\"x\":26112,\"y\":2048,\"z\":46592,\"room_num\":45},{\"object_id\":34,\"x\":19968,\"y\":2048,\"z\":51712,\"room_num\":45},{\"object_id\":34,\"x\":27136,\"y\":2048,\"z\":51712,\"room_num\":45},{\"object_id\":354,\"x\":24064,\"y\":2048,\"z\":52736,\"room_num\":45},{\"object_id\":178,\"x\":26112,\"y\":2048,\"z\":45568,\"room_num\":45},{\"object_id\":331,\"x\":27136,\"y\":512,\"z\":46592,\"room_num\":45},{\"object_id\":176,\"x\":18944,\"y\":1024,\"z\":50688,\"room_num\":45},{\"object_id\":224,\"x\":35328,\"y\":1536,\"z\":64000,\"room_num\":46},{\"object_id\":352,\"x\":35328,\"y\":1536,\"z\":64000,\"room_num\":46},{\"object_id\":178,\"x\":19968,\"y\":1792,\"z\":68096,\"room_num\":47},{\"object_id\":176,\"x\":29184,\"y\":768,\"z\":59904,\"room_num\":47},{\"object_id\":132,\"x\":19968,\"y\":1792,\"z\":69120,\"room_num\":47},{\"object_id\":331,\"x\":20992,\"y\":256,\"z\":69120,\"room_num\":47},{\"object_id\":288,\"x\":39424,\"y\":0,\"z\":54784,\"room_num\":48},{\"object_id\":288,\"x\":34304,\"y\":0,\"z\":47616,\"room_num\":48},{\"object_id\":37,\"x\":33280,\"y\":512,\"z\":53760,\"room_num\":48},{\"object_id\":173,\"x\":33280,\"y\":512,\"z\":53760,\"room_num\":48},{\"object_id\":165,\"x\":41472,\"y\":-512,\"z\":47616,\"room_num\":48},{\"object_id\":288,\"x\":45568,\"y\":1792,\"z\":67072,\"room_num\":50},{\"object_id\":178,\"x\":35328,\"y\":-1920,\"z\":53760,\"room_num\":53},{\"object_id\":177,\"x\":37376,\"y\":-1664,\"z\":53760,\"room_num\":53},{\"object_id\":139,\"x\":66048,\"y\":0,\"z\":28160,\"room_num\":57},{\"object_id\":139,\"x\":66048,\"y\":0,\"z\":29184,\"room_num\":57},{\"object_id\":139,\"x\":66048,\"y\":0,\"z\":27136,\"room_num\":57},{\"object_id\":139,\"x\":66048,\"y\":0,\"z\":26112,\"room_num\":57},{\"object_id\":139,\"x\":66048,\"y\":0,\"z\":25088,\"room_num\":57},{\"object_id\":37,\"x\":64000,\"y\":-128,\"z\":32256,\"room_num\":57},{\"object_id\":288,\"x\":68096,\"y\":0,\"z\":30208,\"room_num\":57},{\"object_id\":173,\"x\":64000,\"y\":-128,\"z\":32256,\"room_num\":57},{\"object_id\":87,\"x\":67072,\"y\":-4736,\"z\":23040,\"room_num\":59},{\"object_id\":87,\"x\":67072,\"y\":-4480,\"z\":24064,\"room_num\":59},{\"object_id\":87,\"x\":68096,\"y\":-4480,\"z\":24064,\"room_num\":59},{\"object_id\":87,\"x\":68096,\"y\":-4736,\"z\":23040,\"room_num\":59},{\"object_id\":288,\"x\":68096,\"y\":-4608,\"z\":31232,\"room_num\":59},{\"object_id\":38,\"x\":65024,\"y\":-6400,\"z\":27136,\"room_num\":60},{\"object_id\":288,\"x\":68096,\"y\":-6144,\"z\":27136,\"room_num\":60},{\"object_id\":176,\"x\":64000,\"y\":-6144,\"z\":23040,\"room_num\":60},{\"object_id\":180,\"x\":72192,\"y\":-6144,\"z\":30208,\"room_num\":60},{\"object_id\":338,\"x\":67072,\"y\":896,\"z\":27136,\"room_num\":61},{\"object_id\":130,\"x\":64000,\"y\":1792,\"z\":25088,\"room_num\":61},{\"object_id\":173,\"x\":70144,\"y\":-8576,\"z\":25088,\"room_num\":62},{\"object_id\":132,\"x\":67072,\"y\":0,\"z\":20992,\"room_num\":70},{\"object_id\":139,\"x\":61952,\"y\":-1792,\"z\":19968,\"room_num\":72},{\"object_id\":139,\"x\":60928,\"y\":-1792,\"z\":19968,\"room_num\":72},{\"object_id\":139,\"x\":59904,\"y\":-1792,\"z\":19968,\"room_num\":72},{\"object_id\":288,\"x\":61952,\"y\":256,\"z\":18944,\"room_num\":72},{\"object_id\":75,\"x\":61952,\"y\":256,\"z\":18944,\"room_num\":72},{\"object_id\":75,\"x\":59904,\"y\":256,\"z\":17920,\"room_num\":72},{\"object_id\":288,\"x\":64000,\"y\":0,\"z\":18944,\"room_num\":72},{\"object_id\":129,\"x\":58880,\"y\":256,\"z\":15872,\"room_num\":72},{\"object_id\":129,\"x\":62976,\"y\":256,\"z\":15872,\"room_num\":72},{\"object_id\":129,\"x\":58880,\"y\":256,\"z\":19968,\"room_num\":72},{\"object_id\":288,\"x\":58880,\"y\":0,\"z\":20992,\"room_num\":72},{\"object_id\":132,\"x\":64000,\"y\":0,\"z\":20992,\"room_num\":72},{\"object_id\":288,\"x\":61952,\"y\":256,\"z\":19968,\"room_num\":72},{\"object_id\":288,\"x\":64000,\"y\":0,\"z\":15872,\"room_num\":72},{\"object_id\":288,\"x\":59904,\"y\":-128,\"z\":55808,\"room_num\":76},{\"object_id\":129,\"x\":26112,\"y\":2048,\"z\":44544,\"room_num\":79},{\"object_id\":132,\"x\":26112,\"y\":2048,\"z\":44544,\"room_num\":79},{\"object_id\":132,\"x\":26112,\"y\":2048,\"z\":44544,\"room_num\":79},{\"object_id\":178,\"x\":27136,\"y\":2048,\"z\":44544,\"room_num\":80},{\"object_id\":176,\"x\":28160,\"y\":1792,\"z\":44544,\"room_num\":80},{\"object_id\":288,\"x\":30208,\"y\":1280,\"z\":44544,\"room_num\":80},{\"object_id\":107,\"x\":4608,\"y\":-4992,\"z\":83456,\"room_num\":81},{\"object_id\":142,\"x\":8704,\"y\":-4608,\"z\":80384,\"room_num\":84},{\"object_id\":142,\"x\":7680,\"y\":-4608,\"z\":80384,\"room_num\":84},{\"object_id\":107,\"x\":8704,\"y\":-5760,\"z\":81408,\"room_num\":84},{\"object_id\":107,\"x\":7680,\"y\":-6016,\"z\":82432,\"room_num\":84},{\"object_id\":351,\"x\":10752,\"y\":-5376,\"z\":73216,\"room_num\":90},{\"object_id\":0,\"x\":11776,\"y\":-5376,\"z\":74240,\"room_num\":90},{\"object_id\":173,\"x\":33280,\"y\":-2304,\"z\":83456,\"room_num\":93},{\"object_id\":173,\"x\":33280,\"y\":-2304,\"z\":82432,\"room_num\":93}]\n    },\n    {\n        \"title\": \"Madubu Gorge\",\n        \"zone_num\": 1,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,14,24,32,35,87,88,90,91,94,111,113,116,127,128,129,130,131,132,139,142,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,244,245,246,247,300,301,304,305,309,310,311,315,330,331,332,333,338,339,347,350,355,366,367],\n        \"items\": [{\"object_id\":305,\"x\":46592,\"y\":-9984,\"z\":13824,\"room_num\":3},{\"object_id\":305,\"x\":45568,\"y\":-9984,\"z\":13824,\"room_num\":3},{\"object_id\":111,\"x\":44544,\"y\":-9984,\"z\":18944,\"room_num\":3},{\"object_id\":111,\"x\":45568,\"y\":-9984,\"z\":18944,\"room_num\":3},{\"object_id\":24,\"x\":44544,\"y\":-9984,\"z\":20992,\"room_num\":4},{\"object_id\":305,\"x\":61952,\"y\":-17408,\"z\":14848,\"room_num\":6},{\"object_id\":305,\"x\":61952,\"y\":-17408,\"z\":16896,\"room_num\":6},{\"object_id\":305,\"x\":61952,\"y\":-17408,\"z\":18944,\"room_num\":6},{\"object_id\":139,\"x\":78336,\"y\":-21184,\"z\":19968,\"room_num\":6},{\"object_id\":180,\"x\":52736,\"y\":-17920,\"z\":16896,\"room_num\":7},{\"object_id\":305,\"x\":57856,\"y\":-13056,\"z\":17920,\"room_num\":9},{\"object_id\":113,\"x\":52736,\"y\":-15872,\"z\":15872,\"room_num\":9},{\"object_id\":113,\"x\":52736,\"y\":-15296,\"z\":14848,\"room_num\":9},{\"object_id\":173,\"x\":73216,\"y\":-17408,\"z\":37376,\"room_num\":11},{\"object_id\":35,\"x\":72192,\"y\":-18944,\"z\":36352,\"room_num\":11},{\"object_id\":338,\"x\":86528,\"y\":-17024,\"z\":29184,\"room_num\":12},{\"object_id\":304,\"x\":84480,\"y\":-18432,\"z\":29184,\"room_num\":13},{\"object_id\":139,\"x\":86528,\"y\":-18688,\"z\":28160,\"room_num\":13},{\"object_id\":139,\"x\":85504,\"y\":-18688,\"z\":28160,\"room_num\":13},{\"object_id\":180,\"x\":85504,\"y\":-18688,\"z\":28160,\"room_num\":13},{\"object_id\":304,\"x\":71168,\"y\":-23680,\"z\":33280,\"room_num\":14},{\"object_id\":305,\"x\":83456,\"y\":-17408,\"z\":30208,\"room_num\":18},{\"object_id\":305,\"x\":81408,\"y\":-21760,\"z\":24064,\"room_num\":18},{\"object_id\":305,\"x\":72192,\"y\":-17408,\"z\":26112,\"room_num\":18},{\"object_id\":305,\"x\":45568,\"y\":-4608,\"z\":30208,\"room_num\":22},{\"object_id\":305,\"x\":44544,\"y\":-4608,\"z\":30208,\"room_num\":22},{\"object_id\":305,\"x\":42496,\"y\":-4608,\"z\":33280,\"room_num\":22},{\"object_id\":174,\"x\":78336,\"y\":-20992,\"z\":12800,\"room_num\":25},{\"object_id\":177,\"x\":79360,\"y\":-20992,\"z\":12800,\"room_num\":25},{\"object_id\":14,\"x\":91648,\"y\":-24576,\"z\":22016,\"room_num\":28},{\"object_id\":32,\"x\":91648,\"y\":-19456,\"z\":23040,\"room_num\":30},{\"object_id\":14,\"x\":77312,\"y\":-23808,\"z\":51712,\"room_num\":31},{\"object_id\":35,\"x\":71168,\"y\":-20992,\"z\":23040,\"room_num\":32},{\"object_id\":128,\"x\":71168,\"y\":-20992,\"z\":23040,\"room_num\":32},{\"object_id\":131,\"x\":69120,\"y\":-23040,\"z\":33280,\"room_num\":33},{\"object_id\":180,\"x\":71168,\"y\":-17408,\"z\":26112,\"room_num\":34},{\"object_id\":35,\"x\":59904,\"y\":-19072,\"z\":28160,\"room_num\":39},{\"object_id\":142,\"x\":56832,\"y\":-18688,\"z\":28160,\"room_num\":39},{\"object_id\":142,\"x\":57856,\"y\":-18688,\"z\":28160,\"room_num\":39},{\"object_id\":142,\"x\":58880,\"y\":-18688,\"z\":28160,\"room_num\":39},{\"object_id\":180,\"x\":58880,\"y\":-19456,\"z\":35328,\"room_num\":39},{\"object_id\":139,\"x\":65024,\"y\":-22528,\"z\":43520,\"room_num\":45},{\"object_id\":35,\"x\":55808,\"y\":-18688,\"z\":26112,\"room_num\":46},{\"object_id\":35,\"x\":71168,\"y\":-24320,\"z\":47616,\"room_num\":48},{\"object_id\":128,\"x\":66048,\"y\":-23808,\"z\":57856,\"room_num\":48},{\"object_id\":128,\"x\":62976,\"y\":-20736,\"z\":41472,\"room_num\":49},{\"object_id\":180,\"x\":62976,\"y\":-20736,\"z\":42496,\"room_num\":49},{\"object_id\":347,\"x\":77312,\"y\":-26624,\"z\":51712,\"room_num\":50},{\"object_id\":0,\"x\":73216,\"y\":-25984,\"z\":55808,\"room_num\":50},{\"object_id\":35,\"x\":77312,\"y\":-28416,\"z\":45568,\"room_num\":53},{\"object_id\":87,\"x\":65024,\"y\":-19200,\"z\":31232,\"room_num\":58},{\"object_id\":338,\"x\":51712,\"y\":5504,\"z\":72192,\"room_num\":62},{\"object_id\":111,\"x\":39424,\"y\":2048,\"z\":42496,\"room_num\":71},{\"object_id\":111,\"x\":39424,\"y\":2048,\"z\":42496,\"room_num\":71},{\"object_id\":87,\"x\":40448,\"y\":2048,\"z\":41472,\"room_num\":71},{\"object_id\":87,\"x\":39424,\"y\":2048,\"z\":41472,\"room_num\":71},{\"object_id\":87,\"x\":38400,\"y\":2048,\"z\":41472,\"room_num\":71},{\"object_id\":87,\"x\":39424,\"y\":1920,\"z\":40448,\"room_num\":71},{\"object_id\":347,\"x\":55808,\"y\":-4608,\"z\":51712,\"room_num\":72},{\"object_id\":91,\"x\":50688,\"y\":-4736,\"z\":46592,\"room_num\":73},{\"object_id\":91,\"x\":88576,\"y\":-22016,\"z\":36352,\"room_num\":76},{\"object_id\":91,\"x\":88576,\"y\":-22016,\"z\":37376,\"room_num\":76},{\"object_id\":176,\"x\":88576,\"y\":-22016,\"z\":40448,\"room_num\":76},{\"object_id\":332,\"x\":82432,\"y\":-20480,\"z\":38400,\"room_num\":77},{\"object_id\":332,\"x\":79360,\"y\":-18432,\"z\":38400,\"room_num\":77},{\"object_id\":35,\"x\":85504,\"y\":-18432,\"z\":38400,\"room_num\":77},{\"object_id\":180,\"x\":82432,\"y\":-20480,\"z\":38400,\"room_num\":77},{\"object_id\":178,\"x\":44544,\"y\":2048,\"z\":44544,\"room_num\":79},{\"object_id\":169,\"x\":44544,\"y\":2048,\"z\":43520,\"room_num\":79},{\"object_id\":333,\"x\":38400,\"y\":1920,\"z\":52736,\"room_num\":79},{\"object_id\":305,\"x\":34304,\"y\":5120,\"z\":52736,\"room_num\":81},{\"object_id\":305,\"x\":34304,\"y\":5120,\"z\":54784,\"room_num\":81},{\"object_id\":173,\"x\":33280,\"y\":4352,\"z\":50688,\"room_num\":84},{\"object_id\":177,\"x\":33280,\"y\":4352,\"z\":54784,\"room_num\":84},{\"object_id\":180,\"x\":26112,\"y\":5120,\"z\":51712,\"room_num\":87},{\"object_id\":347,\"x\":46592,\"y\":4992,\"z\":52736,\"room_num\":88},{\"object_id\":127,\"x\":51712,\"y\":-4096,\"z\":57856,\"room_num\":90},{\"object_id\":180,\"x\":53760,\"y\":-2304,\"z\":64000,\"room_num\":90},{\"object_id\":347,\"x\":55808,\"y\":-3072,\"z\":67072,\"room_num\":90},{\"object_id\":87,\"x\":52736,\"y\":-1792,\"z\":64000,\"room_num\":90},{\"object_id\":87,\"x\":51712,\"y\":-1792,\"z\":64000,\"room_num\":90},{\"object_id\":87,\"x\":50688,\"y\":-1792,\"z\":64000,\"room_num\":90},{\"object_id\":87,\"x\":49664,\"y\":-1792,\"z\":64000,\"room_num\":90},{\"object_id\":87,\"x\":55808,\"y\":-2816,\"z\":59904,\"room_num\":90},{\"object_id\":87,\"x\":56832,\"y\":-2816,\"z\":59904,\"room_num\":90},{\"object_id\":87,\"x\":57856,\"y\":-2816,\"z\":59904,\"room_num\":90},{\"object_id\":87,\"x\":58880,\"y\":-2816,\"z\":59904,\"room_num\":90},{\"object_id\":87,\"x\":55808,\"y\":-512,\"z\":68096,\"room_num\":90},{\"object_id\":87,\"x\":56832,\"y\":-512,\"z\":68096,\"room_num\":90},{\"object_id\":87,\"x\":54784,\"y\":-512,\"z\":68096,\"room_num\":90},{\"object_id\":87,\"x\":57856,\"y\":-512,\"z\":68096,\"room_num\":90},{\"object_id\":333,\"x\":45568,\"y\":3712,\"z\":72192,\"room_num\":92},{\"object_id\":333,\"x\":47616,\"y\":3712,\"z\":68096,\"room_num\":92},{\"object_id\":331,\"x\":51712,\"y\":12416,\"z\":76288,\"room_num\":93},{\"object_id\":331,\"x\":51712,\"y\":12416,\"z\":68096,\"room_num\":94},{\"object_id\":131,\"x\":58880,\"y\":13056,\"z\":72192,\"room_num\":96},{\"object_id\":130,\"x\":58880,\"y\":13824,\"z\":72192,\"room_num\":97},{\"object_id\":32,\"x\":57856,\"y\":14336,\"z\":71168,\"room_num\":97},{\"object_id\":176,\"x\":77312,\"y\":-28160,\"z\":50688,\"room_num\":99},{\"object_id\":171,\"x\":79360,\"y\":-28160,\"z\":51712,\"room_num\":99},{\"object_id\":175,\"x\":79360,\"y\":-28160,\"z\":50688,\"room_num\":99},{\"object_id\":139,\"x\":79360,\"y\":-27392,\"z\":52736,\"room_num\":99},{\"object_id\":331,\"x\":50688,\"y\":1792,\"z\":81408,\"room_num\":101},{\"object_id\":139,\"x\":51712,\"y\":2816,\"z\":80384,\"room_num\":102},{\"object_id\":35,\"x\":51712,\"y\":3840,\"z\":80384,\"room_num\":102},{\"object_id\":32,\"x\":50688,\"y\":6400,\"z\":82432,\"room_num\":103},{\"object_id\":87,\"x\":58880,\"y\":768,\"z\":73216,\"room_num\":105},{\"object_id\":87,\"x\":58880,\"y\":1024,\"z\":74240,\"room_num\":105},{\"object_id\":87,\"x\":61952,\"y\":1792,\"z\":76288,\"room_num\":106},{\"object_id\":87,\"x\":61952,\"y\":1920,\"z\":75264,\"room_num\":106},{\"object_id\":87,\"x\":61952,\"y\":2432,\"z\":71168,\"room_num\":106},{\"object_id\":87,\"x\":62976,\"y\":2432,\"z\":71168,\"room_num\":106},{\"object_id\":180,\"x\":32256,\"y\":2688,\"z\":77312,\"room_num\":110},{\"object_id\":129,\"x\":49664,\"y\":-1792,\"z\":72192,\"room_num\":118},{\"object_id\":350,\"x\":51712,\"y\":-3328,\"z\":72192,\"room_num\":118},{\"object_id\":35,\"x\":52736,\"y\":-1792,\"z\":70144,\"room_num\":118},{\"object_id\":350,\"x\":98816,\"y\":-3456,\"z\":98816,\"room_num\":119},{\"object_id\":304,\"x\":100864,\"y\":-6400,\"z\":96768,\"room_num\":119},{\"object_id\":166,\"x\":43520,\"y\":4864,\"z\":59904,\"room_num\":125},{\"object_id\":88,\"x\":49664,\"y\":-1280,\"z\":31232,\"room_num\":128},{\"object_id\":88,\"x\":55808,\"y\":-1792,\"z\":31232,\"room_num\":129},{\"object_id\":88,\"x\":50688,\"y\":-2944,\"z\":31232,\"room_num\":129},{\"object_id\":332,\"x\":52736,\"y\":512,\"z\":30208,\"room_num\":129},{\"object_id\":332,\"x\":52736,\"y\":512,\"z\":32256,\"room_num\":129},{\"object_id\":88,\"x\":55808,\"y\":-4992,\"z\":40448,\"room_num\":130},{\"object_id\":332,\"x\":55808,\"y\":-1792,\"z\":34304,\"room_num\":130},{\"object_id\":331,\"x\":51712,\"y\":-2432,\"z\":80384,\"room_num\":134},{\"object_id\":139,\"x\":52736,\"y\":-1792,\"z\":79360,\"room_num\":136},{\"object_id\":35,\"x\":52736,\"y\":-768,\"z\":79360,\"room_num\":136},{\"object_id\":32,\"x\":49664,\"y\":14336,\"z\":75264,\"room_num\":137},{\"object_id\":32,\"x\":45568,\"y\":14336,\"z\":73216,\"room_num\":139},{\"object_id\":331,\"x\":87552,\"y\":-25216,\"z\":17920,\"room_num\":143},{\"object_id\":131,\"x\":87552,\"y\":-24576,\"z\":16896,\"room_num\":143},{\"object_id\":130,\"x\":89600,\"y\":-21248,\"z\":19968,\"room_num\":144},{\"object_id\":32,\"x\":90624,\"y\":-20480,\"z\":17920,\"room_num\":144}]\n    },\n    {\n        \"title\": \"Temple of Puna\",\n        \"zone_num\": 1,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,21,24,35,36,83,87,89,90,91,97,108,116,128,129,131,132,133,134,139,141,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,243,244,245,246,247,300,301,304,305,309,310,311,315,330,331,332,333,338,339,347,365,366,367],\n        \"items\": [{\"object_id\":0,\"x\":46592,\"y\":-9344,\"z\":60928,\"room_num\":0},{\"object_id\":131,\"x\":50688,\"y\":-9216,\"z\":57856,\"room_num\":1},{\"object_id\":21,\"x\":49664,\"y\":-8960,\"z\":57856,\"room_num\":1},{\"object_id\":21,\"x\":37376,\"y\":-11264,\"z\":58880,\"room_num\":5},{\"object_id\":21,\"x\":38400,\"y\":-11008,\"z\":57856,\"room_num\":5},{\"object_id\":21,\"x\":41472,\"y\":-10240,\"z\":57856,\"room_num\":5},{\"object_id\":21,\"x\":40448,\"y\":-10496,\"z\":58880,\"room_num\":5},{\"object_id\":176,\"x\":40448,\"y\":-10496,\"z\":58880,\"room_num\":5},{\"object_id\":21,\"x\":35328,\"y\":-13824,\"z\":58880,\"room_num\":7},{\"object_id\":134,\"x\":30208,\"y\":-18176,\"z\":58880,\"room_num\":9},{\"object_id\":21,\"x\":37376,\"y\":-17408,\"z\":57856,\"room_num\":10},{\"object_id\":162,\"x\":32256,\"y\":-18176,\"z\":64000,\"room_num\":13},{\"object_id\":170,\"x\":28160,\"y\":-18176,\"z\":64000,\"room_num\":13},{\"object_id\":170,\"x\":28160,\"y\":-18176,\"z\":61952,\"room_num\":13},{\"object_id\":177,\"x\":32256,\"y\":-18176,\"z\":61952,\"room_num\":13},{\"object_id\":178,\"x\":30208,\"y\":-18176,\"z\":62976,\"room_num\":13},{\"object_id\":331,\"x\":30208,\"y\":-18944,\"z\":64000,\"room_num\":13},{\"object_id\":108,\"x\":44544,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":108,\"x\":43520,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":108,\"x\":42496,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":108,\"x\":41472,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":108,\"x\":40448,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":108,\"x\":39424,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":133,\"x\":44544,\"y\":-16640,\"z\":52736,\"room_num\":15},{\"object_id\":132,\"x\":44544,\"y\":-16640,\"z\":51712,\"room_num\":15},{\"object_id\":128,\"x\":40448,\"y\":-17920,\"z\":56832,\"room_num\":15},{\"object_id\":128,\"x\":43520,\"y\":-17920,\"z\":56832,\"room_num\":15},{\"object_id\":128,\"x\":43520,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":128,\"x\":40448,\"y\":-17920,\"z\":47616,\"room_num\":15},{\"object_id\":180,\"x\":41472,\"y\":-16640,\"z\":52736,\"room_num\":15},{\"object_id\":365,\"x\":39424,\"y\":-16640,\"z\":51712,\"room_num\":15},{\"object_id\":176,\"x\":37376,\"y\":-17920,\"z\":51712,\"room_num\":16},{\"object_id\":116,\"x\":49664,\"y\":-18688,\"z\":52736,\"room_num\":18},{\"object_id\":129,\"x\":48640,\"y\":-14592,\"z\":52736,\"room_num\":20},{\"object_id\":129,\"x\":48640,\"y\":-14592,\"z\":51712,\"room_num\":20},{\"object_id\":129,\"x\":49664,\"y\":-14592,\"z\":51712,\"room_num\":20},{\"object_id\":131,\"x\":49664,\"y\":-14592,\"z\":51712,\"room_num\":20},{\"object_id\":97,\"x\":49664,\"y\":-14592,\"z\":53760,\"room_num\":20},{\"object_id\":89,\"x\":58880,\"y\":-12928,\"z\":48640,\"room_num\":22},{\"object_id\":176,\"x\":50688,\"y\":-10240,\"z\":48640,\"room_num\":22},{\"object_id\":131,\"x\":50688,\"y\":-11008,\"z\":49664,\"room_num\":22},{\"object_id\":129,\"x\":57856,\"y\":-10240,\"z\":48640,\"room_num\":22},{\"object_id\":131,\"x\":48640,\"y\":-10240,\"z\":48640,\"room_num\":22},{\"object_id\":83,\"x\":48640,\"y\":-9728,\"z\":48640,\"room_num\":22},{\"object_id\":330,\"x\":50688,\"y\":-10624,\"z\":49664,\"room_num\":22},{\"object_id\":89,\"x\":46592,\"y\":-12416,\"z\":42496,\"room_num\":23},{\"object_id\":331,\"x\":47616,\"y\":-10880,\"z\":52736,\"room_num\":23},{\"object_id\":331,\"x\":45568,\"y\":-10880,\"z\":52736,\"room_num\":23},{\"object_id\":83,\"x\":47616,\"y\":-9472,\"z\":48640,\"room_num\":23},{\"object_id\":83,\"x\":47616,\"y\":-9472,\"z\":49664,\"room_num\":23},{\"object_id\":83,\"x\":45568,\"y\":-9472,\"z\":48640,\"room_num\":23},{\"object_id\":83,\"x\":45568,\"y\":-9472,\"z\":49664,\"room_num\":23},{\"object_id\":83,\"x\":47616,\"y\":-9472,\"z\":50688,\"room_num\":23},{\"object_id\":83,\"x\":45568,\"y\":-9472,\"z\":50688,\"room_num\":23},{\"object_id\":83,\"x\":47616,\"y\":-9472,\"z\":51712,\"room_num\":23},{\"object_id\":83,\"x\":45568,\"y\":-9472,\"z\":51712,\"room_num\":23},{\"object_id\":83,\"x\":47616,\"y\":-9472,\"z\":47616,\"room_num\":23},{\"object_id\":83,\"x\":45568,\"y\":-9472,\"z\":47616,\"room_num\":23},{\"object_id\":180,\"x\":46592,\"y\":-9984,\"z\":50688,\"room_num\":23},{\"object_id\":24,\"x\":51712,\"y\":-8832,\"z\":57856,\"room_num\":28},{\"object_id\":36,\"x\":65024,\"y\":-6144,\"z\":57856,\"room_num\":29},{\"object_id\":35,\"x\":61952,\"y\":-5376,\"z\":62976,\"room_num\":29},{\"object_id\":243,\"x\":65024,\"y\":-6144,\"z\":57856,\"room_num\":29},{\"object_id\":176,\"x\":61952,\"y\":-5376,\"z\":53760,\"room_num\":29},{\"object_id\":176,\"x\":61952,\"y\":-5376,\"z\":61952,\"room_num\":29}]\n    },\n    {\n        \"title\": \"Thames Wharf\",\n        \"zone_num\": 4,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,23,24,27,51,56,58,74,75,76,77,78,79,80,81,82,83,87,97,98,118,119,127,128,130,131,139,140,142,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,224,225,228,229,232,244,245,247,300,301,304,309,310,311,315,318,319,330,333,335,349,351,354,355,366,367],\n        \"items\": [{\"object_id\":177,\"x\":59904,\"y\":-21504,\"z\":45568,\"room_num\":0},{\"object_id\":225,\"x\":55808,\"y\":-21504,\"z\":45568,\"room_num\":0},{\"object_id\":51,\"x\":54784,\"y\":-19200,\"z\":48640,\"room_num\":2},{\"object_id\":51,\"x\":57856,\"y\":-19200,\"z\":54784,\"room_num\":2},{\"object_id\":76,\"x\":57856,\"y\":-19200,\"z\":54784,\"room_num\":2},{\"object_id\":79,\"x\":51712,\"y\":-19200,\"z\":54784,\"room_num\":2},{\"object_id\":98,\"x\":57856,\"y\":-18944,\"z\":48640,\"room_num\":2},{\"object_id\":130,\"x\":19968,\"y\":-2176,\"z\":43520,\"room_num\":3},{\"object_id\":171,\"x\":23040,\"y\":-512,\"z\":44544,\"room_num\":3},{\"object_id\":176,\"x\":24064,\"y\":-512,\"z\":43520,\"room_num\":3},{\"object_id\":319,\"x\":19968,\"y\":-896,\"z\":33280,\"room_num\":4},{\"object_id\":319,\"x\":19968,\"y\":-896,\"z\":32256,\"room_num\":4},{\"object_id\":319,\"x\":24064,\"y\":-896,\"z\":29184,\"room_num\":4},{\"object_id\":319,\"x\":23040,\"y\":-896,\"z\":29184,\"room_num\":4},{\"object_id\":319,\"x\":27136,\"y\":-896,\"z\":33280,\"room_num\":4},{\"object_id\":319,\"x\":27136,\"y\":-896,\"z\":32256,\"room_num\":4},{\"object_id\":319,\"x\":23040,\"y\":-1024,\"z\":36352,\"room_num\":4},{\"object_id\":319,\"x\":24064,\"y\":-1024,\"z\":36352,\"room_num\":4},{\"object_id\":131,\"x\":27136,\"y\":-8448,\"z\":36352,\"room_num\":8},{\"object_id\":318,\"x\":24064,\"y\":-8960,\"z\":32256,\"room_num\":8},{\"object_id\":318,\"x\":23040,\"y\":-8960,\"z\":34304,\"room_num\":8},{\"object_id\":75,\"x\":27136,\"y\":-8192,\"z\":35328,\"room_num\":8},{\"object_id\":351,\"x\":37376,\"y\":-21888,\"z\":56832,\"room_num\":22},{\"object_id\":351,\"x\":39424,\"y\":-21888,\"z\":56832,\"room_num\":22},{\"object_id\":27,\"x\":43520,\"y\":-24832,\"z\":60928,\"room_num\":22},{\"object_id\":27,\"x\":43520,\"y\":-24320,\"z\":56832,\"room_num\":22},{\"object_id\":142,\"x\":34304,\"y\":-23296,\"z\":41472,\"room_num\":25},{\"object_id\":142,\"x\":35328,\"y\":-23296,\"z\":41472,\"room_num\":25},{\"object_id\":319,\"x\":27136,\"y\":-3712,\"z\":67072,\"room_num\":32},{\"object_id\":319,\"x\":23040,\"y\":-3712,\"z\":67072,\"room_num\":33},{\"object_id\":23,\"x\":44544,\"y\":-11520,\"z\":24064,\"room_num\":35},{\"object_id\":23,\"x\":45568,\"y\":-11520,\"z\":23040,\"room_num\":35},{\"object_id\":176,\"x\":44544,\"y\":-11520,\"z\":23040,\"room_num\":35},{\"object_id\":174,\"x\":45568,\"y\":-11520,\"z\":24064,\"room_num\":35},{\"object_id\":131,\"x\":22016,\"y\":-8448,\"z\":37376,\"room_num\":43},{\"object_id\":87,\"x\":37376,\"y\":-22784,\"z\":38400,\"room_num\":45},{\"object_id\":87,\"x\":37376,\"y\":-22784,\"z\":37376,\"room_num\":45},{\"object_id\":87,\"x\":37376,\"y\":-22784,\"z\":37376,\"room_num\":45},{\"object_id\":87,\"x\":37376,\"y\":-22784,\"z\":38400,\"room_num\":45},{\"object_id\":87,\"x\":37376,\"y\":-22784,\"z\":36352,\"room_num\":45},{\"object_id\":87,\"x\":37376,\"y\":-22784,\"z\":36352,\"room_num\":45},{\"object_id\":128,\"x\":39424,\"y\":-17664,\"z\":47616,\"room_num\":48},{\"object_id\":333,\"x\":37376,\"y\":-18432,\"z\":49664,\"room_num\":48},{\"object_id\":333,\"x\":37376,\"y\":-18432,\"z\":50688,\"room_num\":48},{\"object_id\":333,\"x\":39424,\"y\":-17920,\"z\":52736,\"room_num\":48},{\"object_id\":180,\"x\":38400,\"y\":-17664,\"z\":47616,\"room_num\":48},{\"object_id\":51,\"x\":46592,\"y\":-8704,\"z\":32256,\"room_num\":51},{\"object_id\":180,\"x\":47616,\"y\":-8704,\"z\":32256,\"room_num\":51},{\"object_id\":176,\"x\":46592,\"y\":-8704,\"z\":32256,\"room_num\":51},{\"object_id\":0,\"x\":41472,\"y\":-21504,\"z\":41472,\"room_num\":52},{\"object_id\":304,\"x\":26112,\"y\":-3840,\"z\":54784,\"room_num\":59},{\"object_id\":128,\"x\":33280,\"y\":-8448,\"z\":36352,\"room_num\":61},{\"object_id\":56,\"x\":29184,\"y\":-8448,\"z\":36352,\"room_num\":61},{\"object_id\":75,\"x\":29184,\"y\":-8448,\"z\":36352,\"room_num\":61},{\"object_id\":77,\"x\":29184,\"y\":-8448,\"z\":36352,\"room_num\":61},{\"object_id\":131,\"x\":38400,\"y\":-19200,\"z\":54784,\"room_num\":62},{\"object_id\":232,\"x\":38400,\"y\":-19200,\"z\":54784,\"room_num\":62},{\"object_id\":142,\"x\":43520,\"y\":-20480,\"z\":41472,\"room_num\":62},{\"object_id\":304,\"x\":43520,\"y\":-19200,\"z\":49664,\"room_num\":62},{\"object_id\":178,\"x\":38400,\"y\":-19200,\"z\":54784,\"room_num\":62},{\"object_id\":174,\"x\":44544,\"y\":-19968,\"z\":51712,\"room_num\":62},{\"object_id\":51,\"x\":42496,\"y\":-20736,\"z\":54784,\"room_num\":62},{\"object_id\":75,\"x\":42496,\"y\":-20736,\"z\":54784,\"room_num\":62},{\"object_id\":77,\"x\":42496,\"y\":-20736,\"z\":54784,\"room_num\":62},{\"object_id\":169,\"x\":44544,\"y\":-22784,\"z\":49664,\"room_num\":65},{\"object_id\":58,\"x\":27136,\"y\":-2048,\"z\":59904,\"room_num\":66},{\"object_id\":319,\"x\":27136,\"y\":-3712,\"z\":64000,\"room_num\":66},{\"object_id\":319,\"x\":27136,\"y\":-3712,\"z\":59904,\"room_num\":66},{\"object_id\":304,\"x\":27136,\"y\":-2688,\"z\":65024,\"room_num\":66},{\"object_id\":128,\"x\":24064,\"y\":-1792,\"z\":72192,\"room_num\":68},{\"object_id\":319,\"x\":24064,\"y\":-3456,\"z\":72192,\"room_num\":68},{\"object_id\":180,\"x\":23040,\"y\":-1920,\"z\":72192,\"room_num\":68},{\"object_id\":140,\"x\":31232,\"y\":-8192,\"z\":61952,\"room_num\":70},{\"object_id\":140,\"x\":30208,\"y\":-8192,\"z\":61952,\"room_num\":70},{\"object_id\":349,\"x\":33280,\"y\":-7936,\"z\":61952,\"room_num\":70},{\"object_id\":118,\"x\":31232,\"y\":-7936,\"z\":64000,\"room_num\":70},{\"object_id\":128,\"x\":33280,\"y\":-7936,\"z\":61952,\"room_num\":70},{\"object_id\":304,\"x\":31232,\"y\":-8704,\"z\":61952,\"room_num\":70},{\"object_id\":128,\"x\":29184,\"y\":-8448,\"z\":61952,\"room_num\":70},{\"object_id\":349,\"x\":29184,\"y\":-8448,\"z\":61952,\"room_num\":70},{\"object_id\":304,\"x\":29184,\"y\":-8960,\"z\":61952,\"room_num\":70},{\"object_id\":56,\"x\":31232,\"y\":-7936,\"z\":51712,\"room_num\":71},{\"object_id\":76,\"x\":31232,\"y\":-7936,\"z\":51712,\"room_num\":71},{\"object_id\":79,\"x\":31232,\"y\":-7936,\"z\":56832,\"room_num\":71},{\"object_id\":79,\"x\":31232,\"y\":-7936,\"z\":44544,\"room_num\":71},{\"object_id\":56,\"x\":31232,\"y\":-7936,\"z\":54784,\"room_num\":71},{\"object_id\":56,\"x\":32256,\"y\":-7936,\"z\":41472,\"room_num\":71},{\"object_id\":304,\"x\":25088,\"y\":-2560,\"z\":65024,\"room_num\":72},{\"object_id\":335,\"x\":25088,\"y\":-2432,\"z\":65024,\"room_num\":72},{\"object_id\":354,\"x\":25088,\"y\":-2048,\"z\":65024,\"room_num\":72},{\"object_id\":319,\"x\":23040,\"y\":-3712,\"z\":59904,\"room_num\":73},{\"object_id\":319,\"x\":23040,\"y\":-3712,\"z\":64000,\"room_num\":73},{\"object_id\":131,\"x\":18944,\"y\":-11264,\"z\":37376,\"room_num\":75},{\"object_id\":131,\"x\":18944,\"y\":-11264,\"z\":40448,\"room_num\":75},{\"object_id\":119,\"x\":22016,\"y\":0,\"z\":54784,\"room_num\":76},{\"object_id\":119,\"x\":19968,\"y\":0,\"z\":54784,\"room_num\":76},{\"object_id\":140,\"x\":26112,\"y\":-3328,\"z\":54784,\"room_num\":76},{\"object_id\":177,\"x\":19968,\"y\":-11264,\"z\":41472,\"room_num\":80},{\"object_id\":318,\"x\":28160,\"y\":-9216,\"z\":61952,\"room_num\":81},{\"object_id\":97,\"x\":25088,\"y\":-2048,\"z\":57856,\"room_num\":82},{\"object_id\":23,\"x\":27136,\"y\":-4352,\"z\":61952,\"room_num\":83},{\"object_id\":23,\"x\":25088,\"y\":-4352,\"z\":61952,\"room_num\":83},{\"object_id\":176,\"x\":24064,\"y\":-4352,\"z\":61952,\"room_num\":83},{\"object_id\":169,\"x\":52736,\"y\":-5632,\"z\":20992,\"room_num\":84},{\"object_id\":176,\"x\":52736,\"y\":-5632,\"z\":30208,\"room_num\":84},{\"object_id\":127,\"x\":38400,\"y\":-17920,\"z\":41472,\"room_num\":86},{\"object_id\":27,\"x\":37376,\"y\":-17664,\"z\":41472,\"room_num\":86},{\"object_id\":304,\"x\":36352,\"y\":-16512,\"z\":44544,\"room_num\":86},{\"object_id\":176,\"x\":39424,\"y\":-17792,\"z\":45568,\"room_num\":86},{\"object_id\":172,\"x\":23040,\"y\":1536,\"z\":24064,\"room_num\":87},{\"object_id\":140,\"x\":24064,\"y\":-512,\"z\":29184,\"room_num\":88},{\"object_id\":139,\"x\":39424,\"y\":-15104,\"z\":44544,\"room_num\":95},{\"object_id\":118,\"x\":37376,\"y\":-14848,\"z\":45568,\"room_num\":95},{\"object_id\":51,\"x\":38400,\"y\":-14848,\"z\":45568,\"room_num\":95},{\"object_id\":76,\"x\":38400,\"y\":-14848,\"z\":45568,\"room_num\":95},{\"object_id\":76,\"x\":39424,\"y\":-14848,\"z\":45568,\"room_num\":95},{\"object_id\":79,\"x\":37376,\"y\":-14848,\"z\":45568,\"room_num\":95},{\"object_id\":304,\"x\":39424,\"y\":-15104,\"z\":44544,\"room_num\":95},{\"object_id\":176,\"x\":38400,\"y\":-14848,\"z\":45568,\"room_num\":95},{\"object_id\":87,\"x\":55808,\"y\":-17536,\"z\":57856,\"room_num\":102},{\"object_id\":87,\"x\":55808,\"y\":-17536,\"z\":58880,\"room_num\":102},{\"object_id\":87,\"x\":55808,\"y\":-17536,\"z\":59904,\"room_num\":102},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":38400,\"room_num\":108},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":39424,\"room_num\":108},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":39424,\"room_num\":108},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":40448,\"room_num\":108},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":40448,\"room_num\":108},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":41472,\"room_num\":108},{\"object_id\":87,\"x\":62976,\"y\":-15104,\"z\":41472,\"room_num\":108},{\"object_id\":170,\"x\":62976,\"y\":-15104,\"z\":41472,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":41472,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":40448,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":39424,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":38400,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":37376,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":36352,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":41472,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":40448,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":39424,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":38400,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":37376,\"room_num\":108},{\"object_id\":87,\"x\":51712,\"y\":-14848,\"z\":36352,\"room_num\":108},{\"object_id\":349,\"x\":37376,\"y\":-20736,\"z\":59904,\"room_num\":122},{\"object_id\":128,\"x\":37376,\"y\":-20736,\"z\":59904,\"room_num\":122},{\"object_id\":304,\"x\":37376,\"y\":-21120,\"z\":59904,\"room_num\":122},{\"object_id\":75,\"x\":42496,\"y\":-20736,\"z\":55808,\"room_num\":122},{\"object_id\":177,\"x\":37376,\"y\":-20736,\"z\":58880,\"room_num\":122},{\"object_id\":23,\"x\":18944,\"y\":-11264,\"z\":34304,\"room_num\":123},{\"object_id\":23,\"x\":18944,\"y\":-11264,\"z\":35328,\"room_num\":123},{\"object_id\":176,\"x\":18944,\"y\":-11264,\"z\":29184,\"room_num\":123},{\"object_id\":180,\"x\":18944,\"y\":-11264,\"z\":36352,\"room_num\":123},{\"object_id\":180,\"x\":43520,\"y\":-19712,\"z\":32256,\"room_num\":127},{\"object_id\":169,\"x\":42496,\"y\":-19200,\"z\":32256,\"room_num\":127},{\"object_id\":27,\"x\":39424,\"y\":-19072,\"z\":45568,\"room_num\":131},{\"object_id\":118,\"x\":41472,\"y\":-18688,\"z\":41472,\"room_num\":131},{\"object_id\":177,\"x\":50688,\"y\":-13312,\"z\":55808,\"room_num\":140},{\"object_id\":87,\"x\":47616,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":46592,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":50688,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":50688,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":50688,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":49664,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":48640,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":47616,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":46592,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":142,\"x\":48640,\"y\":-13568,\"z\":37376,\"room_num\":148},{\"object_id\":87,\"x\":46592,\"y\":-11520,\"z\":29184,\"room_num\":148},{\"object_id\":87,\"x\":47616,\"y\":-11520,\"z\":29184,\"room_num\":148},{\"object_id\":87,\"x\":48640,\"y\":-11520,\"z\":29184,\"room_num\":148},{\"object_id\":87,\"x\":49664,\"y\":-11520,\"z\":29184,\"room_num\":148},{\"object_id\":87,\"x\":50688,\"y\":-11520,\"z\":29184,\"room_num\":148},{\"object_id\":87,\"x\":49664,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":87,\"x\":48640,\"y\":-12544,\"z\":35328,\"room_num\":148},{\"object_id\":128,\"x\":36352,\"y\":-15104,\"z\":56832,\"room_num\":153},{\"object_id\":161,\"x\":43520,\"y\":-13824,\"z\":46592,\"room_num\":156},{\"object_id\":169,\"x\":41472,\"y\":-13824,\"z\":46592,\"room_num\":156},{\"object_id\":23,\"x\":41472,\"y\":-15104,\"z\":53760,\"room_num\":158},{\"object_id\":23,\"x\":40448,\"y\":-15104,\"z\":53760,\"room_num\":158},{\"object_id\":140,\"x\":37376,\"y\":-15104,\"z\":53760,\"room_num\":158},{\"object_id\":83,\"x\":37376,\"y\":-14336,\"z\":50688,\"room_num\":158},{\"object_id\":304,\"x\":40448,\"y\":-14848,\"z\":47616,\"room_num\":158},{\"object_id\":304,\"x\":37376,\"y\":-15104,\"z\":53760,\"room_num\":158},{\"object_id\":330,\"x\":43520,\"y\":-18304,\"z\":49664,\"room_num\":159},{\"object_id\":304,\"x\":43520,\"y\":-18304,\"z\":49664,\"room_num\":159},{\"object_id\":87,\"x\":49664,\"y\":-7680,\"z\":34304,\"room_num\":160},{\"object_id\":87,\"x\":49664,\"y\":-7680,\"z\":34304,\"room_num\":160},{\"object_id\":51,\"x\":43520,\"y\":-6400,\"z\":44544,\"room_num\":166},{\"object_id\":178,\"x\":43520,\"y\":-6400,\"z\":41472,\"room_num\":166},{\"object_id\":171,\"x\":36352,\"y\":-6400,\"z\":41472,\"room_num\":166},{\"object_id\":180,\"x\":43520,\"y\":-6400,\"z\":41472,\"room_num\":166},{\"object_id\":224,\"x\":44544,\"y\":-14848,\"z\":53760,\"room_num\":172},{\"object_id\":51,\"x\":44544,\"y\":-14848,\"z\":53760,\"room_num\":172},{\"object_id\":74,\"x\":44544,\"y\":-14848,\"z\":53760,\"room_num\":172},{\"object_id\":142,\"x\":40448,\"y\":-13824,\"z\":52736,\"room_num\":173},{\"object_id\":142,\"x\":40448,\"y\":-13824,\"z\":53760,\"room_num\":173},{\"object_id\":128,\"x\":40448,\"y\":-13824,\"z\":53760,\"room_num\":173},{\"object_id\":139,\"x\":40448,\"y\":-14976,\"z\":47616,\"room_num\":173},{\"object_id\":172,\"x\":41472,\"y\":-12032,\"z\":50688,\"room_num\":173},{\"object_id\":128,\"x\":38400,\"y\":-8704,\"z\":53760,\"room_num\":174},{\"object_id\":176,\"x\":37376,\"y\":-8704,\"z\":50688,\"room_num\":174},{\"object_id\":180,\"x\":41472,\"y\":-8704,\"z\":47616,\"room_num\":174},{\"object_id\":178,\"x\":37376,\"y\":-8704,\"z\":47616,\"room_num\":174},{\"object_id\":87,\"x\":55808,\"y\":-17280,\"z\":56832,\"room_num\":178},{\"object_id\":87,\"x\":55808,\"y\":-17280,\"z\":55808,\"room_num\":178},{\"object_id\":87,\"x\":54784,\"y\":-16512,\"z\":56832,\"room_num\":178},{\"object_id\":87,\"x\":54784,\"y\":-16512,\"z\":55808,\"room_num\":178},{\"object_id\":180,\"x\":43520,\"y\":-13312,\"z\":61952,\"room_num\":181},{\"object_id\":23,\"x\":33280,\"y\":-10496,\"z\":61952,\"room_num\":182},{\"object_id\":23,\"x\":34304,\"y\":-10496,\"z\":61952,\"room_num\":182},{\"object_id\":177,\"x\":40448,\"y\":-12800,\"z\":61952,\"room_num\":182},{\"object_id\":319,\"x\":23040,\"y\":-3840,\"z\":70144,\"room_num\":186},{\"object_id\":319,\"x\":27136,\"y\":-3840,\"z\":70144,\"room_num\":187},{\"object_id\":56,\"x\":16896,\"y\":-3840,\"z\":61952,\"room_num\":189},{\"object_id\":76,\"x\":16896,\"y\":-3840,\"z\":61952,\"room_num\":189},{\"object_id\":79,\"x\":16896,\"y\":-3840,\"z\":65024,\"room_num\":189},{\"object_id\":79,\"x\":16896,\"y\":-3840,\"z\":57856,\"room_num\":189},{\"object_id\":169,\"x\":16896,\"y\":-3840,\"z\":65024,\"room_num\":189}]\n    },\n    {\n        \"title\": \"Aldwych\",\n        \"zone_num\": 4,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,13,22,23,24,53,74,75,76,77,78,79,83,87,97,101,110,116,128,131,132,133,134,135,136,137,139,140,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,224,225,226,228,229,230,232,233,234,244,245,247,300,301,304,309,310,311,315,318,319,321,330,331,333,347,349,350,351,352,354,366,367],\n        \"items\": [{\"object_id\":352,\"x\":51712,\"y\":-128,\"z\":25088,\"room_num\":1},{\"object_id\":352,\"x\":49664,\"y\":-128,\"z\":25088,\"room_num\":1},{\"object_id\":352,\"x\":47616,\"y\":-128,\"z\":19968,\"room_num\":1},{\"object_id\":233,\"x\":51712,\"y\":1024,\"z\":26112,\"room_num\":1},{\"object_id\":234,\"x\":49664,\"y\":1024,\"z\":26112,\"room_num\":1},{\"object_id\":83,\"x\":52736,\"y\":1536,\"z\":18944,\"room_num\":1},{\"object_id\":331,\"x\":51712,\"y\":128,\"z\":24064,\"room_num\":1},{\"object_id\":331,\"x\":47616,\"y\":0,\"z\":20992,\"room_num\":1},{\"object_id\":351,\"x\":43520,\"y\":1024,\"z\":19968,\"room_num\":1},{\"object_id\":354,\"x\":43520,\"y\":1024,\"z\":22016,\"room_num\":1},{\"object_id\":350,\"x\":50688,\"y\":896,\"z\":23040,\"room_num\":1},{\"object_id\":304,\"x\":43520,\"y\":512,\"z\":20992,\"room_num\":1},{\"object_id\":136,\"x\":43520,\"y\":768,\"z\":20992,\"room_num\":1},{\"object_id\":83,\"x\":48640,\"y\":1536,\"z\":18944,\"room_num\":1},{\"object_id\":352,\"x\":47616,\"y\":-128,\"z\":25088,\"room_num\":1},{\"object_id\":352,\"x\":49664,\"y\":-128,\"z\":19968,\"room_num\":1},{\"object_id\":352,\"x\":51712,\"y\":-128,\"z\":19968,\"room_num\":1},{\"object_id\":83,\"x\":45568,\"y\":1536,\"z\":26112,\"room_num\":1},{\"object_id\":136,\"x\":53760,\"y\":1024,\"z\":26112,\"room_num\":1},{\"object_id\":87,\"x\":52736,\"y\":1792,\"z\":18944,\"room_num\":1},{\"object_id\":87,\"x\":48640,\"y\":1792,\"z\":18944,\"room_num\":1},{\"object_id\":87,\"x\":45568,\"y\":1792,\"z\":26112,\"room_num\":1},{\"object_id\":22,\"x\":48640,\"y\":1024,\"z\":25088,\"room_num\":1},{\"object_id\":83,\"x\":44544,\"y\":1536,\"z\":18944,\"room_num\":1},{\"object_id\":180,\"x\":49664,\"y\":1024,\"z\":20992,\"room_num\":1},{\"object_id\":163,\"x\":49664,\"y\":1024,\"z\":24064,\"room_num\":1},{\"object_id\":165,\"x\":55808,\"y\":12800,\"z\":68096,\"room_num\":5},{\"object_id\":180,\"x\":56832,\"y\":12800,\"z\":68096,\"room_num\":5},{\"object_id\":180,\"x\":52736,\"y\":2560,\"z\":66048,\"room_num\":8},{\"object_id\":22,\"x\":52736,\"y\":2304,\"z\":70144,\"room_num\":8},{\"object_id\":226,\"x\":57856,\"y\":-1536,\"z\":70144,\"room_num\":9},{\"object_id\":134,\"x\":65024,\"y\":1024,\"z\":29184,\"room_num\":10},{\"object_id\":135,\"x\":65024,\"y\":1024,\"z\":27136,\"room_num\":10},{\"object_id\":135,\"x\":62976,\"y\":1024,\"z\":27136,\"room_num\":10},{\"object_id\":133,\"x\":60928,\"y\":1024,\"z\":29184,\"room_num\":10},{\"object_id\":133,\"x\":62976,\"y\":1024,\"z\":31232,\"room_num\":10},{\"object_id\":134,\"x\":62976,\"y\":1024,\"z\":28160,\"room_num\":10},{\"object_id\":134,\"x\":67072,\"y\":1024,\"z\":27136,\"room_num\":10},{\"object_id\":128,\"x\":67072,\"y\":1024,\"z\":27136,\"room_num\":10},{\"object_id\":128,\"x\":59904,\"y\":1024,\"z\":26112,\"room_num\":10},{\"object_id\":128,\"x\":62976,\"y\":1024,\"z\":24064,\"room_num\":10},{\"object_id\":128,\"x\":65024,\"y\":1024,\"z\":24064,\"room_num\":10},{\"object_id\":128,\"x\":66048,\"y\":1024,\"z\":29184,\"room_num\":10},{\"object_id\":135,\"x\":60928,\"y\":1024,\"z\":31232,\"room_num\":10},{\"object_id\":128,\"x\":59904,\"y\":1024,\"z\":23040,\"room_num\":10},{\"object_id\":176,\"x\":60928,\"y\":1024,\"z\":32256,\"room_num\":10},{\"object_id\":135,\"x\":66048,\"y\":1024,\"z\":25088,\"room_num\":10},{\"object_id\":135,\"x\":58880,\"y\":1024,\"z\":30208,\"room_num\":10},{\"object_id\":176,\"x\":68096,\"y\":1024,\"z\":25088,\"room_num\":10},{\"object_id\":173,\"x\":58880,\"y\":1024,\"z\":32256,\"room_num\":10},{\"object_id\":136,\"x\":72192,\"y\":1024,\"z\":27136,\"room_num\":12},{\"object_id\":132,\"x\":72192,\"y\":1024,\"z\":33280,\"room_num\":12},{\"object_id\":132,\"x\":72192,\"y\":1024,\"z\":29184,\"room_num\":12},{\"object_id\":110,\"x\":72192,\"y\":3200,\"z\":1536,\"room_num\":13},{\"object_id\":128,\"x\":48640,\"y\":-2048,\"z\":50688,\"room_num\":15},{\"object_id\":22,\"x\":41472,\"y\":-256,\"z\":50688,\"room_num\":18},{\"object_id\":131,\"x\":45568,\"y\":768,\"z\":57856,\"room_num\":20},{\"object_id\":176,\"x\":42496,\"y\":768,\"z\":57856,\"room_num\":20},{\"object_id\":53,\"x\":70144,\"y\":1024,\"z\":33280,\"room_num\":25},{\"object_id\":75,\"x\":70144,\"y\":1024,\"z\":33280,\"room_num\":25},{\"object_id\":77,\"x\":70144,\"y\":1024,\"z\":33280,\"room_num\":25},{\"object_id\":180,\"x\":71168,\"y\":1024,\"z\":33280,\"room_num\":25},{\"object_id\":110,\"x\":72192,\"y\":3200,\"z\":7680,\"room_num\":26},{\"object_id\":178,\"x\":54784,\"y\":1024,\"z\":27136,\"room_num\":27},{\"object_id\":176,\"x\":53760,\"y\":1024,\"z\":28160,\"room_num\":27},{\"object_id\":101,\"x\":68096,\"y\":-4864,\"z\":92672,\"room_num\":29},{\"object_id\":180,\"x\":69120,\"y\":-4096,\"z\":92672,\"room_num\":29},{\"object_id\":75,\"x\":70144,\"y\":1280,\"z\":29184,\"room_num\":31},{\"object_id\":128,\"x\":69120,\"y\":1280,\"z\":29184,\"room_num\":31},{\"object_id\":213,\"x\":65024,\"y\":-5888,\"z\":85504,\"room_num\":37},{\"object_id\":206,\"x\":65024,\"y\":-5888,\"z\":85504,\"room_num\":37},{\"object_id\":131,\"x\":60928,\"y\":-5888,\"z\":89600,\"room_num\":37},{\"object_id\":53,\"x\":65024,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":76,\"x\":65024,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":79,\"x\":61952,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":76,\"x\":68096,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":131,\"x\":69120,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":53,\"x\":61952,\"y\":-5888,\"z\":88576,\"room_num\":37},{\"object_id\":330,\"x\":61952,\"y\":-5888,\"z\":88576,\"room_num\":37},{\"object_id\":330,\"x\":65024,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":176,\"x\":65024,\"y\":-5888,\"z\":86528,\"room_num\":37},{\"object_id\":224,\"x\":71168,\"y\":-5888,\"z\":83456,\"room_num\":38},{\"object_id\":128,\"x\":71168,\"y\":-5888,\"z\":84480,\"room_num\":38},{\"object_id\":131,\"x\":71168,\"y\":-5888,\"z\":84480,\"room_num\":38},{\"object_id\":177,\"x\":70144,\"y\":-6400,\"z\":83456,\"room_num\":38},{\"object_id\":232,\"x\":46592,\"y\":768,\"z\":57856,\"room_num\":42},{\"object_id\":205,\"x\":62976,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":53,\"x\":54784,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":330,\"x\":54784,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":76,\"x\":54784,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":76,\"x\":56832,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":79,\"x\":49664,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":319,\"x\":62976,\"y\":768,\"z\":56832,\"room_num\":42},{\"object_id\":22,\"x\":54784,\"y\":768,\"z\":57856,\"room_num\":42},{\"object_id\":76,\"x\":54784,\"y\":768,\"z\":57856,\"room_num\":42},{\"object_id\":76,\"x\":49664,\"y\":768,\"z\":57856,\"room_num\":42},{\"object_id\":79,\"x\":56832,\"y\":768,\"z\":57856,\"room_num\":42},{\"object_id\":330,\"x\":61952,\"y\":640,\"z\":50688,\"room_num\":43},{\"object_id\":53,\"x\":60928,\"y\":768,\"z\":50688,\"room_num\":43},{\"object_id\":74,\"x\":60928,\"y\":768,\"z\":50688,\"room_num\":43},{\"object_id\":176,\"x\":60928,\"y\":768,\"z\":50688,\"room_num\":43},{\"object_id\":178,\"x\":46592,\"y\":-384,\"z\":47616,\"room_num\":43},{\"object_id\":53,\"x\":49664,\"y\":-768,\"z\":57856,\"room_num\":45},{\"object_id\":176,\"x\":49664,\"y\":-768,\"z\":57856,\"room_num\":45},{\"object_id\":110,\"x\":33280,\"y\":-3200,\"z\":61952,\"room_num\":47},{\"object_id\":110,\"x\":28160,\"y\":-3200,\"z\":61952,\"room_num\":47},{\"object_id\":110,\"x\":23040,\"y\":-3200,\"z\":61952,\"room_num\":47},{\"object_id\":347,\"x\":68096,\"y\":-128,\"z\":47616,\"room_num\":48},{\"object_id\":161,\"x\":69120,\"y\":-7168,\"z\":98816,\"room_num\":49},{\"object_id\":83,\"x\":55808,\"y\":7168,\"z\":70144,\"room_num\":50},{\"object_id\":333,\"x\":54784,\"y\":5120,\"z\":65024,\"room_num\":50},{\"object_id\":333,\"x\":55808,\"y\":5248,\"z\":66048,\"room_num\":50},{\"object_id\":180,\"x\":57856,\"y\":768,\"z\":51712,\"room_num\":53},{\"object_id\":131,\"x\":40448,\"y\":-3328,\"z\":69120,\"room_num\":55},{\"object_id\":131,\"x\":48640,\"y\":-3328,\"z\":68096,\"room_num\":55},{\"object_id\":83,\"x\":45568,\"y\":-2816,\"z\":71168,\"room_num\":55},{\"object_id\":304,\"x\":49664,\"y\":-3584,\"z\":68096,\"room_num\":55},{\"object_id\":53,\"x\":50688,\"y\":-3328,\"z\":70144,\"room_num\":55},{\"object_id\":166,\"x\":51712,\"y\":-3328,\"z\":70144,\"room_num\":55},{\"object_id\":173,\"x\":51712,\"y\":-3328,\"z\":68096,\"room_num\":55},{\"object_id\":22,\"x\":42496,\"y\":-3328,\"z\":65024,\"room_num\":55},{\"object_id\":74,\"x\":42496,\"y\":-3328,\"z\":65024,\"room_num\":55},{\"object_id\":77,\"x\":42496,\"y\":-3328,\"z\":65024,\"room_num\":55},{\"object_id\":53,\"x\":43520,\"y\":-3328,\"z\":65024,\"room_num\":55},{\"object_id\":74,\"x\":43520,\"y\":-3328,\"z\":65024,\"room_num\":55},{\"object_id\":173,\"x\":43520,\"y\":-3328,\"z\":73216,\"room_num\":55},{\"object_id\":169,\"x\":48640,\"y\":-3840,\"z\":64000,\"room_num\":55},{\"object_id\":177,\"x\":50688,\"y\":-3328,\"z\":70144,\"room_num\":55},{\"object_id\":53,\"x\":42496,\"y\":-3328,\"z\":70144,\"room_num\":55},{\"object_id\":170,\"x\":51712,\"y\":-3328,\"z\":69120,\"room_num\":55},{\"object_id\":116,\"x\":55808,\"y\":-2560,\"z\":70144,\"room_num\":58},{\"object_id\":333,\"x\":55808,\"y\":-1920,\"z\":66048,\"room_num\":58},{\"object_id\":171,\"x\":56832,\"y\":-1024,\"z\":66048,\"room_num\":58},{\"object_id\":132,\"x\":47616,\"y\":-4096,\"z\":82432,\"room_num\":60},{\"object_id\":132,\"x\":50688,\"y\":-4096,\"z\":82432,\"room_num\":60},{\"object_id\":132,\"x\":53760,\"y\":-4096,\"z\":82432,\"room_num\":60},{\"object_id\":304,\"x\":47616,\"y\":-4352,\"z\":82432,\"room_num\":60},{\"object_id\":304,\"x\":52736,\"y\":-4352,\"z\":82432,\"room_num\":60},{\"object_id\":216,\"x\":48640,\"y\":-4608,\"z\":38400,\"room_num\":62},{\"object_id\":131,\"x\":48640,\"y\":-4608,\"z\":38400,\"room_num\":62},{\"object_id\":330,\"x\":49664,\"y\":-5248,\"z\":40448,\"room_num\":62},{\"object_id\":53,\"x\":48640,\"y\":-4608,\"z\":41472,\"room_num\":62},{\"object_id\":131,\"x\":51712,\"y\":-2048,\"z\":51712,\"room_num\":63},{\"object_id\":215,\"x\":51712,\"y\":-2048,\"z\":51712,\"room_num\":63},{\"object_id\":180,\"x\":54784,\"y\":9728,\"z\":65024,\"room_num\":64},{\"object_id\":83,\"x\":56832,\"y\":7424,\"z\":70144,\"room_num\":64},{\"object_id\":177,\"x\":57856,\"y\":15360,\"z\":70144,\"room_num\":65},{\"object_id\":128,\"x\":41472,\"y\":-3328,\"z\":81408,\"room_num\":66},{\"object_id\":128,\"x\":40448,\"y\":-3328,\"z\":81408,\"room_num\":66},{\"object_id\":128,\"x\":42496,\"y\":768,\"z\":58880,\"room_num\":68},{\"object_id\":319,\"x\":42496,\"y\":128,\"z\":58880,\"room_num\":68},{\"object_id\":171,\"x\":44544,\"y\":768,\"z\":58880,\"room_num\":68},{\"object_id\":23,\"x\":62976,\"y\":-2304,\"z\":62976,\"room_num\":69},{\"object_id\":177,\"x\":62976,\"y\":-2304,\"z\":64000,\"room_num\":69},{\"object_id\":169,\"x\":62976,\"y\":-2304,\"z\":64000,\"room_num\":69},{\"object_id\":53,\"x\":51712,\"y\":4608,\"z\":68096,\"room_num\":70},{\"object_id\":53,\"x\":48640,\"y\":3584,\"z\":67072,\"room_num\":70},{\"object_id\":75,\"x\":51712,\"y\":4608,\"z\":68096,\"room_num\":70},{\"object_id\":75,\"x\":48640,\"y\":3584,\"z\":69120,\"room_num\":70},{\"object_id\":77,\"x\":51712,\"y\":4608,\"z\":68096,\"room_num\":70},{\"object_id\":128,\"x\":49664,\"y\":3584,\"z\":66048,\"room_num\":70},{\"object_id\":176,\"x\":48640,\"y\":3584,\"z\":68096,\"room_num\":70},{\"object_id\":128,\"x\":53760,\"y\":-4352,\"z\":85504,\"room_num\":71},{\"object_id\":136,\"x\":58880,\"y\":1536,\"z\":45568,\"room_num\":72},{\"object_id\":304,\"x\":64000,\"y\":512,\"z\":47616,\"room_num\":75},{\"object_id\":225,\"x\":44544,\"y\":-5376,\"z\":91648,\"room_num\":76},{\"object_id\":169,\"x\":44544,\"y\":-5376,\"z\":89600,\"room_num\":76},{\"object_id\":180,\"x\":44544,\"y\":-5376,\"z\":90624,\"room_num\":76},{\"object_id\":139,\"x\":49664,\"y\":-6656,\"z\":86528,\"room_num\":79},{\"object_id\":131,\"x\":61952,\"y\":-3328,\"z\":45568,\"room_num\":83},{\"object_id\":22,\"x\":66048,\"y\":-2816,\"z\":45568,\"room_num\":83},{\"object_id\":180,\"x\":61952,\"y\":-3328,\"z\":43520,\"room_num\":83},{\"object_id\":23,\"x\":72192,\"y\":-512,\"z\":48640,\"room_num\":84},{\"object_id\":23,\"x\":73216,\"y\":-512,\"z\":49664,\"room_num\":84},{\"object_id\":53,\"x\":71168,\"y\":-1024,\"z\":57856,\"room_num\":84},{\"object_id\":330,\"x\":71168,\"y\":-1024,\"z\":57856,\"room_num\":84},{\"object_id\":76,\"x\":71168,\"y\":-1024,\"z\":57856,\"room_num\":84},{\"object_id\":76,\"x\":71168,\"y\":-1024,\"z\":52736,\"room_num\":84},{\"object_id\":79,\"x\":71168,\"y\":-1024,\"z\":60928,\"room_num\":84},{\"object_id\":349,\"x\":55808,\"y\":-4096,\"z\":40448,\"room_num\":86},{\"object_id\":214,\"x\":56832,\"y\":-4096,\"z\":40448,\"room_num\":86},{\"object_id\":23,\"x\":56832,\"y\":-4096,\"z\":41472,\"room_num\":86},{\"object_id\":23,\"x\":56832,\"y\":-4096,\"z\":42496,\"room_num\":86},{\"object_id\":140,\"x\":45568,\"y\":-6912,\"z\":86528,\"room_num\":89},{\"object_id\":128,\"x\":47616,\"y\":-6912,\"z\":86528,\"room_num\":91},{\"object_id\":110,\"x\":77312,\"y\":4096,\"z\":54784,\"room_num\":98},{\"object_id\":110,\"x\":82432,\"y\":5376,\"z\":54784,\"room_num\":98},{\"object_id\":110,\"x\":73216,\"y\":896,\"z\":87552,\"room_num\":100},{\"object_id\":83,\"x\":55808,\"y\":3072,\"z\":69120,\"room_num\":102},{\"object_id\":176,\"x\":53760,\"y\":3584,\"z\":66048,\"room_num\":102},{\"object_id\":53,\"x\":54784,\"y\":2560,\"z\":38400,\"room_num\":104},{\"object_id\":75,\"x\":54784,\"y\":2560,\"z\":38400,\"room_num\":104},{\"object_id\":75,\"x\":54784,\"y\":2560,\"z\":34304,\"room_num\":104},{\"object_id\":131,\"x\":53760,\"y\":2560,\"z\":35328,\"room_num\":104},{\"object_id\":128,\"x\":55808,\"y\":2560,\"z\":36352,\"room_num\":104},{\"object_id\":128,\"x\":54784,\"y\":2560,\"z\":36352,\"room_num\":104},{\"object_id\":331,\"x\":52736,\"y\":1920,\"z\":39424,\"room_num\":104},{\"object_id\":77,\"x\":54784,\"y\":2560,\"z\":38400,\"room_num\":104},{\"object_id\":174,\"x\":53760,\"y\":2560,\"z\":36352,\"room_num\":104},{\"object_id\":53,\"x\":55808,\"y\":2560,\"z\":37376,\"room_num\":104},{\"object_id\":330,\"x\":55808,\"y\":2560,\"z\":37376,\"room_num\":104},{\"object_id\":176,\"x\":50688,\"y\":3840,\"z\":37376,\"room_num\":104},{\"object_id\":139,\"x\":48640,\"y\":-3072,\"z\":65024,\"room_num\":107},{\"object_id\":304,\"x\":48640,\"y\":-2176,\"z\":65024,\"room_num\":107},{\"object_id\":53,\"x\":52736,\"y\":-1536,\"z\":65024,\"room_num\":107},{\"object_id\":318,\"x\":49664,\"y\":-768,\"z\":67072,\"room_num\":107},{\"object_id\":304,\"x\":49664,\"y\":-1408,\"z\":67072,\"room_num\":107},{\"object_id\":176,\"x\":52736,\"y\":-1536,\"z\":65024,\"room_num\":107},{\"object_id\":174,\"x\":51712,\"y\":-1536,\"z\":69120,\"room_num\":107},{\"object_id\":304,\"x\":46592,\"y\":1664,\"z\":47616,\"room_num\":109},{\"object_id\":304,\"x\":46592,\"y\":1664,\"z\":47616,\"room_num\":109},{\"object_id\":304,\"x\":46592,\"y\":1664,\"z\":47616,\"room_num\":109},{\"object_id\":139,\"x\":46592,\"y\":1024,\"z\":47616,\"room_num\":109},{\"object_id\":97,\"x\":69120,\"y\":-8704,\"z\":84480,\"room_num\":110},{\"object_id\":178,\"x\":60928,\"y\":-8704,\"z\":90624,\"room_num\":110},{\"object_id\":171,\"x\":60928,\"y\":-8704,\"z\":82432,\"room_num\":110},{\"object_id\":53,\"x\":70144,\"y\":-8704,\"z\":82432,\"room_num\":110},{\"object_id\":161,\"x\":60928,\"y\":-8704,\"z\":86528,\"room_num\":110},{\"object_id\":163,\"x\":50688,\"y\":-256,\"z\":70144,\"room_num\":112},{\"object_id\":128,\"x\":56832,\"y\":512,\"z\":64000,\"room_num\":115},{\"object_id\":208,\"x\":44544,\"y\":1280,\"z\":17920,\"room_num\":117},{\"object_id\":173,\"x\":44544,\"y\":1280,\"z\":17920,\"room_num\":117},{\"object_id\":97,\"x\":43520,\"y\":-2048,\"z\":71168,\"room_num\":118},{\"object_id\":139,\"x\":43520,\"y\":-3200,\"z\":71168,\"room_num\":118},{\"object_id\":110,\"x\":90624,\"y\":6912,\"z\":61952,\"room_num\":121},{\"object_id\":110,\"x\":26112,\"y\":6272,\"z\":62976,\"room_num\":125},{\"object_id\":53,\"x\":52736,\"y\":-4608,\"z\":38400,\"room_num\":126},{\"object_id\":74,\"x\":52736,\"y\":-4608,\"z\":38400,\"room_num\":126},{\"object_id\":176,\"x\":52736,\"y\":-4608,\"z\":38400,\"room_num\":126},{\"object_id\":207,\"x\":41472,\"y\":768,\"z\":20992,\"room_num\":127},{\"object_id\":83,\"x\":42496,\"y\":1280,\"z\":20992,\"room_num\":127},{\"object_id\":87,\"x\":42496,\"y\":2048,\"z\":20992,\"room_num\":127},{\"object_id\":178,\"x\":48640,\"y\":-2048,\"z\":91648,\"room_num\":131},{\"object_id\":139,\"x\":44544,\"y\":768,\"z\":47616,\"room_num\":135},{\"object_id\":128,\"x\":44544,\"y\":768,\"z\":48640,\"room_num\":135},{\"object_id\":137,\"x\":45568,\"y\":768,\"z\":48640,\"room_num\":135},{\"object_id\":110,\"x\":13824,\"y\":6272,\"z\":62976,\"room_num\":139},{\"object_id\":110,\"x\":91648,\"y\":6528,\"z\":54784,\"room_num\":141},{\"object_id\":110,\"x\":13824,\"y\":8960,\"z\":55808,\"room_num\":142},{\"object_id\":331,\"x\":55808,\"y\":256,\"z\":27136,\"room_num\":143},{\"object_id\":131,\"x\":49664,\"y\":-768,\"z\":53760,\"room_num\":149},{\"object_id\":0,\"x\":71168,\"y\":-12160,\"z\":98816,\"room_num\":157},{\"object_id\":128,\"x\":53760,\"y\":-3328,\"z\":70144,\"room_num\":158},{\"object_id\":169,\"x\":66048,\"y\":0,\"z\":49664,\"room_num\":159},{\"object_id\":174,\"x\":48640,\"y\":-3840,\"z\":51712,\"room_num\":160},{\"object_id\":177,\"x\":69120,\"y\":1792,\"z\":47616,\"room_num\":161},{\"object_id\":169,\"x\":69120,\"y\":1792,\"z\":47616,\"room_num\":161},{\"object_id\":101,\"x\":67072,\"y\":-1792,\"z\":50688,\"room_num\":162}]\n    },\n    {\n        \"title\": \"Lud's Gate\",\n        \"zone_num\": 4,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,19,24,26,32,53,56,74,75,76,77,78,79,83,86,87,97,98,116,118,119,121,128,130,131,132,133,134,137,138,139,140,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,209,213,217,224,228,232,244,245,247,300,301,304,306,309,310,311,315,318,321,322,330,331,333,337,349,351,366,367],\n        \"items\": [{\"object_id\":161,\"x\":62976,\"y\":-22016,\"z\":31232,\"room_num\":0},{\"object_id\":180,\"x\":62976,\"y\":-22144,\"z\":31232,\"room_num\":0},{\"object_id\":128,\"x\":61952,\"y\":-12032,\"z\":31232,\"room_num\":3},{\"object_id\":139,\"x\":60928,\"y\":-15616,\"z\":32256,\"room_num\":4},{\"object_id\":87,\"x\":60928,\"y\":-15616,\"z\":31232,\"room_num\":4},{\"object_id\":87,\"x\":60928,\"y\":-15616,\"z\":31232,\"room_num\":4},{\"object_id\":116,\"x\":61952,\"y\":-16384,\"z\":26112,\"room_num\":8},{\"object_id\":32,\"x\":61952,\"y\":2048,\"z\":49664,\"room_num\":11},{\"object_id\":176,\"x\":56832,\"y\":4608,\"z\":49664,\"room_num\":11},{\"object_id\":32,\"x\":65024,\"y\":1408,\"z\":46592,\"room_num\":11},{\"object_id\":172,\"x\":54784,\"y\":5248,\"z\":46592,\"room_num\":11},{\"object_id\":172,\"x\":61952,\"y\":3328,\"z\":50688,\"room_num\":11},{\"object_id\":172,\"x\":61952,\"y\":3328,\"z\":50688,\"room_num\":11},{\"object_id\":176,\"x\":49664,\"y\":-23552,\"z\":28160,\"room_num\":15},{\"object_id\":166,\"x\":51712,\"y\":-24576,\"z\":25088,\"room_num\":15},{\"object_id\":304,\"x\":51712,\"y\":-23552,\"z\":24064,\"room_num\":15},{\"object_id\":166,\"x\":62976,\"y\":-20736,\"z\":31232,\"room_num\":17},{\"object_id\":56,\"x\":66048,\"y\":-14592,\"z\":25088,\"room_num\":20},{\"object_id\":76,\"x\":66048,\"y\":-14592,\"z\":25088,\"room_num\":20},{\"object_id\":79,\"x\":72192,\"y\":-15360,\"z\":25088,\"room_num\":20},{\"object_id\":79,\"x\":60928,\"y\":-13824,\"z\":25088,\"room_num\":20},{\"object_id\":176,\"x\":66048,\"y\":-14592,\"z\":25088,\"room_num\":20},{\"object_id\":56,\"x\":53760,\"y\":-6912,\"z\":22016,\"room_num\":24},{\"object_id\":318,\"x\":49664,\"y\":-7680,\"z\":20992,\"room_num\":24},{\"object_id\":180,\"x\":42496,\"y\":-6912,\"z\":20992,\"room_num\":24},{\"object_id\":178,\"x\":42496,\"y\":-6912,\"z\":22016,\"room_num\":24},{\"object_id\":26,\"x\":39424,\"y\":640,\"z\":22016,\"room_num\":34},{\"object_id\":331,\"x\":83456,\"y\":-18304,\"z\":35328,\"room_num\":36},{\"object_id\":331,\"x\":83456,\"y\":-18304,\"z\":42496,\"room_num\":36},{\"object_id\":331,\"x\":79360,\"y\":-18176,\"z\":41472,\"room_num\":36},{\"object_id\":331,\"x\":79360,\"y\":-18048,\"z\":36352,\"room_num\":36},{\"object_id\":213,\"x\":83456,\"y\":-17920,\"z\":37376,\"room_num\":36},{\"object_id\":53,\"x\":81408,\"y\":-17920,\"z\":36352,\"room_num\":36},{\"object_id\":330,\"x\":81408,\"y\":-17920,\"z\":36352,\"room_num\":36},{\"object_id\":78,\"x\":81408,\"y\":-17920,\"z\":36352,\"room_num\":36},{\"object_id\":132,\"x\":81408,\"y\":-17920,\"z\":42496,\"room_num\":36},{\"object_id\":0,\"x\":81408,\"y\":-17920,\"z\":39424,\"room_num\":36},{\"object_id\":177,\"x\":62976,\"y\":-15616,\"z\":31232,\"room_num\":38},{\"object_id\":32,\"x\":30208,\"y\":-768,\"z\":23040,\"room_num\":39},{\"object_id\":32,\"x\":29184,\"y\":-128,\"z\":20992,\"room_num\":39},{\"object_id\":177,\"x\":31232,\"y\":1280,\"z\":28160,\"room_num\":39},{\"object_id\":132,\"x\":67072,\"y\":-15872,\"z\":29184,\"room_num\":43},{\"object_id\":130,\"x\":51712,\"y\":-2176,\"z\":37376,\"room_num\":47},{\"object_id\":176,\"x\":53760,\"y\":-1536,\"z\":37376,\"room_num\":47},{\"object_id\":53,\"x\":76288,\"y\":-16640,\"z\":33280,\"room_num\":49},{\"object_id\":53,\"x\":78336,\"y\":-17664,\"z\":31232,\"room_num\":49},{\"object_id\":330,\"x\":76288,\"y\":-16640,\"z\":33280,\"room_num\":49},{\"object_id\":330,\"x\":78336,\"y\":-17664,\"z\":31232,\"room_num\":49},{\"object_id\":74,\"x\":78336,\"y\":-17664,\"z\":31232,\"room_num\":49},{\"object_id\":74,\"x\":76288,\"y\":-16640,\"z\":33280,\"room_num\":49},{\"object_id\":78,\"x\":70144,\"y\":-16128,\"z\":32256,\"room_num\":50},{\"object_id\":56,\"x\":32256,\"y\":-26368,\"z\":53760,\"room_num\":53},{\"object_id\":177,\"x\":69120,\"y\":-18432,\"z\":25088,\"room_num\":54},{\"object_id\":174,\"x\":70144,\"y\":-18432,\"z\":25088,\"room_num\":54},{\"object_id\":304,\"x\":54784,\"y\":-26880,\"z\":31232,\"room_num\":56},{\"object_id\":304,\"x\":48640,\"y\":-27136,\"z\":31232,\"room_num\":56},{\"object_id\":130,\"x\":38400,\"y\":-128,\"z\":24064,\"room_num\":64},{\"object_id\":174,\"x\":43520,\"y\":-22016,\"z\":19968,\"room_num\":68},{\"object_id\":174,\"x\":43520,\"y\":-22016,\"z\":18944,\"room_num\":68},{\"object_id\":180,\"x\":43520,\"y\":-22016,\"z\":17920,\"room_num\":68},{\"object_id\":56,\"x\":56832,\"y\":-4864,\"z\":30208,\"room_num\":69},{\"object_id\":74,\"x\":56832,\"y\":-4864,\"z\":30208,\"room_num\":69},{\"object_id\":75,\"x\":56832,\"y\":-4864,\"z\":30208,\"room_num\":69},{\"object_id\":119,\"x\":54784,\"y\":-6144,\"z\":28160,\"room_num\":69},{\"object_id\":304,\"x\":54784,\"y\":-6144,\"z\":28160,\"room_num\":69},{\"object_id\":75,\"x\":55808,\"y\":-4864,\"z\":30208,\"room_num\":69},{\"object_id\":224,\"x\":56832,\"y\":-4864,\"z\":30208,\"room_num\":69},{\"object_id\":318,\"x\":46592,\"y\":-6144,\"z\":28160,\"room_num\":69},{\"object_id\":177,\"x\":53760,\"y\":-5120,\"z\":37376,\"room_num\":69},{\"object_id\":164,\"x\":59904,\"y\":-4352,\"z\":33280,\"room_num\":69},{\"object_id\":172,\"x\":54784,\"y\":-4992,\"z\":27136,\"room_num\":69},{\"object_id\":176,\"x\":49664,\"y\":-5632,\"z\":24064,\"room_num\":69},{\"object_id\":304,\"x\":50688,\"y\":-4224,\"z\":28160,\"room_num\":69},{\"object_id\":172,\"x\":55808,\"y\":-6144,\"z\":28160,\"room_num\":69},{\"object_id\":172,\"x\":59904,\"y\":-4352,\"z\":33280,\"room_num\":69},{\"object_id\":172,\"x\":59904,\"y\":-4352,\"z\":33280,\"room_num\":69},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":34304,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":33280,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":32256,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":31232,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":30208,\"room_num\":72},{\"object_id\":87,\"x\":55808,\"y\":-8960,\"z\":30208,\"room_num\":72},{\"object_id\":87,\"x\":55808,\"y\":-8960,\"z\":33280,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":33280,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":32256,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":31232,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":30208,\"room_num\":72},{\"object_id\":87,\"x\":57856,\"y\":-8704,\"z\":29184,\"room_num\":72},{\"object_id\":178,\"x\":59904,\"y\":-8704,\"z\":26112,\"room_num\":72},{\"object_id\":177,\"x\":59904,\"y\":-8704,\"z\":27136,\"room_num\":72},{\"object_id\":172,\"x\":46592,\"y\":-8704,\"z\":31232,\"room_num\":72},{\"object_id\":87,\"x\":55808,\"y\":-8960,\"z\":33280,\"room_num\":72},{\"object_id\":87,\"x\":55808,\"y\":-8960,\"z\":30208,\"room_num\":72},{\"object_id\":56,\"x\":43520,\"y\":-13824,\"z\":31232,\"room_num\":73},{\"object_id\":169,\"x\":59904,\"y\":-13824,\"z\":31232,\"room_num\":77},{\"object_id\":205,\"x\":42496,\"y\":-31744,\"z\":38400,\"room_num\":78},{\"object_id\":337,\"x\":42496,\"y\":-31744,\"z\":40448,\"room_num\":78},{\"object_id\":337,\"x\":42496,\"y\":-31744,\"z\":39424,\"room_num\":78},{\"object_id\":337,\"x\":42496,\"y\":-31744,\"z\":37376,\"room_num\":78},{\"object_id\":337,\"x\":42496,\"y\":-31744,\"z\":36352,\"room_num\":78},{\"object_id\":83,\"x\":44544,\"y\":-29952,\"z\":40448,\"room_num\":78},{\"object_id\":83,\"x\":44544,\"y\":-29952,\"z\":38400,\"room_num\":78},{\"object_id\":180,\"x\":32256,\"y\":256,\"z\":16896,\"room_num\":86},{\"object_id\":174,\"x\":44544,\"y\":-24064,\"z\":37376,\"room_num\":87},{\"object_id\":322,\"x\":28160,\"y\":-24576,\"z\":50688,\"room_num\":88},{\"object_id\":322,\"x\":28160,\"y\":-22528,\"z\":50688,\"room_num\":89},{\"object_id\":304,\"x\":38400,\"y\":2816,\"z\":30208,\"room_num\":90},{\"object_id\":322,\"x\":28160,\"y\":-20480,\"z\":50688,\"room_num\":91},{\"object_id\":19,\"x\":80384,\"y\":-384,\"z\":45568,\"room_num\":98},{\"object_id\":128,\"x\":60928,\"y\":-4864,\"z\":29184,\"room_num\":100},{\"object_id\":26,\"x\":55808,\"y\":-2176,\"z\":25088,\"room_num\":102},{\"object_id\":180,\"x\":58880,\"y\":-1536,\"z\":23040,\"room_num\":103},{\"object_id\":177,\"x\":59904,\"y\":-1536,\"z\":23040,\"room_num\":103},{\"object_id\":172,\"x\":59904,\"y\":-1536,\"z\":24064,\"room_num\":103},{\"object_id\":172,\"x\":59904,\"y\":-1536,\"z\":25088,\"room_num\":103},{\"object_id\":172,\"x\":58880,\"y\":-1536,\"z\":25088,\"room_num\":103},{\"object_id\":172,\"x\":58880,\"y\":-1536,\"z\":24064,\"room_num\":103},{\"object_id\":19,\"x\":59904,\"y\":-2048,\"z\":24064,\"room_num\":103},{\"object_id\":169,\"x\":46592,\"y\":-26880,\"z\":33280,\"room_num\":105},{\"object_id\":56,\"x\":48640,\"y\":-26368,\"z\":34304,\"room_num\":107},{\"object_id\":76,\"x\":48640,\"y\":-26368,\"z\":34304,\"room_num\":107},{\"object_id\":76,\"x\":51712,\"y\":-26368,\"z\":34304,\"room_num\":107},{\"object_id\":79,\"x\":46592,\"y\":-26368,\"z\":34304,\"room_num\":107},{\"object_id\":176,\"x\":51712,\"y\":-26368,\"z\":42496,\"room_num\":108},{\"object_id\":171,\"x\":40448,\"y\":-31488,\"z\":37376,\"room_num\":109},{\"object_id\":118,\"x\":53760,\"y\":-28672,\"z\":37376,\"room_num\":113},{\"object_id\":140,\"x\":53760,\"y\":-29440,\"z\":39424,\"room_num\":113},{\"object_id\":304,\"x\":53760,\"y\":-27904,\"z\":39424,\"room_num\":113},{\"object_id\":180,\"x\":53760,\"y\":-28160,\"z\":42496,\"room_num\":113},{\"object_id\":176,\"x\":54784,\"y\":-28672,\"z\":33280,\"room_num\":120},{\"object_id\":98,\"x\":56832,\"y\":-26112,\"z\":36352,\"room_num\":124},{\"object_id\":131,\"x\":54784,\"y\":-26368,\"z\":34304,\"room_num\":124},{\"object_id\":128,\"x\":54784,\"y\":-26368,\"z\":35328,\"room_num\":124},{\"object_id\":56,\"x\":56832,\"y\":-26368,\"z\":35328,\"room_num\":124},{\"object_id\":74,\"x\":56832,\"y\":-26368,\"z\":35328,\"room_num\":124},{\"object_id\":98,\"x\":49664,\"y\":-29440,\"z\":45568,\"room_num\":128},{\"object_id\":133,\"x\":51712,\"y\":-29440,\"z\":42496,\"room_num\":129},{\"object_id\":118,\"x\":49664,\"y\":-29440,\"z\":42496,\"room_num\":129},{\"object_id\":304,\"x\":50688,\"y\":-29440,\"z\":39424,\"room_num\":129},{\"object_id\":304,\"x\":51712,\"y\":-29440,\"z\":42496,\"room_num\":129},{\"object_id\":304,\"x\":48640,\"y\":-30976,\"z\":37376,\"room_num\":130},{\"object_id\":178,\"x\":53760,\"y\":-30976,\"z\":41472,\"room_num\":130},{\"object_id\":169,\"x\":53760,\"y\":-30976,\"z\":42496,\"room_num\":130},{\"object_id\":133,\"x\":47616,\"y\":-30976,\"z\":37376,\"room_num\":132},{\"object_id\":170,\"x\":46592,\"y\":-1280,\"z\":40448,\"room_num\":134},{\"object_id\":137,\"x\":58880,\"y\":-1536,\"z\":26112,\"room_num\":135},{\"object_id\":138,\"x\":59904,\"y\":-1536,\"z\":26112,\"room_num\":135},{\"object_id\":171,\"x\":66048,\"y\":2304,\"z\":55808,\"room_num\":137},{\"object_id\":134,\"x\":37376,\"y\":2816,\"z\":30208,\"room_num\":140},{\"object_id\":130,\"x\":35328,\"y\":2304,\"z\":30208,\"room_num\":140},{\"object_id\":176,\"x\":36352,\"y\":2816,\"z\":30208,\"room_num\":140},{\"object_id\":130,\"x\":38400,\"y\":-2944,\"z\":18944,\"room_num\":145},{\"object_id\":180,\"x\":38400,\"y\":-2176,\"z\":16896,\"room_num\":145},{\"object_id\":134,\"x\":37376,\"y\":-2816,\"z\":16896,\"room_num\":146},{\"object_id\":304,\"x\":36352,\"y\":-3200,\"z\":16896,\"room_num\":146},{\"object_id\":26,\"x\":33280,\"y\":-2944,\"z\":16896,\"room_num\":146},{\"object_id\":56,\"x\":40448,\"y\":-30464,\"z\":34304,\"room_num\":147},{\"object_id\":76,\"x\":40448,\"y\":-30464,\"z\":34304,\"room_num\":147},{\"object_id\":79,\"x\":40448,\"y\":-30464,\"z\":32256,\"room_num\":147},{\"object_id\":79,\"x\":40448,\"y\":-30464,\"z\":35328,\"room_num\":147},{\"object_id\":97,\"x\":73216,\"y\":-17152,\"z\":28160,\"room_num\":148},{\"object_id\":131,\"x\":44544,\"y\":-30464,\"z\":30208,\"room_num\":150},{\"object_id\":174,\"x\":44544,\"y\":-30464,\"z\":29184,\"room_num\":152},{\"object_id\":86,\"x\":33280,\"y\":-26112,\"z\":38400,\"room_num\":155},{\"object_id\":86,\"x\":33280,\"y\":-26112,\"z\":41472,\"room_num\":155},{\"object_id\":330,\"x\":29184,\"y\":-26752,\"z\":39424,\"room_num\":156},{\"object_id\":330,\"x\":29184,\"y\":-26752,\"z\":42496,\"room_num\":156},{\"object_id\":177,\"x\":27136,\"y\":-27392,\"z\":42496,\"room_num\":156},{\"object_id\":171,\"x\":27136,\"y\":-27392,\"z\":43520,\"room_num\":156},{\"object_id\":121,\"x\":31232,\"y\":-26368,\"z\":43520,\"room_num\":157},{\"object_id\":121,\"x\":31232,\"y\":-26368,\"z\":41472,\"room_num\":157},{\"object_id\":121,\"x\":31232,\"y\":-26368,\"z\":39424,\"room_num\":157},{\"object_id\":177,\"x\":33280,\"y\":-26624,\"z\":32256,\"room_num\":158},{\"object_id\":170,\"x\":26112,\"y\":-26624,\"z\":32256,\"room_num\":159},{\"object_id\":178,\"x\":26112,\"y\":-26624,\"z\":33280,\"room_num\":159},{\"object_id\":172,\"x\":29184,\"y\":-21760,\"z\":44544,\"room_num\":160},{\"object_id\":130,\"x\":30208,\"y\":-23296,\"z\":38400,\"room_num\":160},{\"object_id\":131,\"x\":44544,\"y\":-30464,\"z\":28160,\"room_num\":164},{\"object_id\":128,\"x\":45568,\"y\":-30464,\"z\":28160,\"room_num\":164},{\"object_id\":56,\"x\":44544,\"y\":-30464,\"z\":28160,\"room_num\":164},{\"object_id\":74,\"x\":44544,\"y\":-30464,\"z\":28160,\"room_num\":164},{\"object_id\":232,\"x\":28160,\"y\":-26880,\"z\":30208,\"room_num\":167},{\"object_id\":180,\"x\":54784,\"y\":-28928,\"z\":27136,\"room_num\":173},{\"object_id\":97,\"x\":73216,\"y\":-17152,\"z\":26112,\"room_num\":175},{\"object_id\":139,\"x\":81408,\"y\":-17920,\"z\":44544,\"room_num\":177},{\"object_id\":349,\"x\":57856,\"y\":-26368,\"z\":35328,\"room_num\":178},{\"object_id\":26,\"x\":24064,\"y\":-2816,\"z\":30208,\"room_num\":179},{\"object_id\":139,\"x\":26112,\"y\":-1792,\"z\":34304,\"room_num\":181},{\"object_id\":304,\"x\":26112,\"y\":-1536,\"z\":34304,\"room_num\":181},{\"object_id\":172,\"x\":70144,\"y\":-512,\"z\":42496,\"room_num\":182},{\"object_id\":172,\"x\":70144,\"y\":-512,\"z\":43520,\"room_num\":182},{\"object_id\":180,\"x\":70144,\"y\":0,\"z\":40448,\"room_num\":182},{\"object_id\":177,\"x\":66048,\"y\":768,\"z\":40448,\"room_num\":183},{\"object_id\":172,\"x\":66048,\"y\":256,\"z\":42496,\"room_num\":183},{\"object_id\":172,\"x\":66048,\"y\":256,\"z\":43520,\"room_num\":183},{\"object_id\":134,\"x\":35328,\"y\":-2304,\"z\":34304,\"room_num\":184},{\"object_id\":304,\"x\":35328,\"y\":-2304,\"z\":34304,\"room_num\":184},{\"object_id\":139,\"x\":50688,\"y\":-3968,\"z\":28160,\"room_num\":189},{\"object_id\":180,\"x\":50688,\"y\":-3072,\"z\":29184,\"room_num\":189},{\"object_id\":19,\"x\":31232,\"y\":-2816,\"z\":30208,\"room_num\":190},{\"object_id\":351,\"x\":32256,\"y\":-3456,\"z\":30208,\"room_num\":190},{\"object_id\":128,\"x\":28160,\"y\":-26880,\"z\":27136,\"room_num\":191},{\"object_id\":134,\"x\":28160,\"y\":-26880,\"z\":28160,\"room_num\":191},{\"object_id\":180,\"x\":28160,\"y\":-26880,\"z\":27136,\"room_num\":191},{\"object_id\":172,\"x\":58880,\"y\":-2816,\"z\":37376,\"room_num\":195},{\"object_id\":172,\"x\":58880,\"y\":-2816,\"z\":38400,\"room_num\":195},{\"object_id\":304,\"x\":60928,\"y\":-1792,\"z\":32256,\"room_num\":196},{\"object_id\":304,\"x\":61952,\"y\":-2432,\"z\":29184,\"room_num\":196},{\"object_id\":139,\"x\":61952,\"y\":-3072,\"z\":29184,\"room_num\":196},{\"object_id\":130,\"x\":58880,\"y\":-2304,\"z\":29184,\"room_num\":197},{\"object_id\":134,\"x\":59904,\"y\":-1536,\"z\":32256,\"room_num\":198},{\"object_id\":134,\"x\":57856,\"y\":-1536,\"z\":32256,\"room_num\":198}]\n    },\n    {\n        \"title\": \"City\",\n        \"zone_num\": 4,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,24,57,75,97,128,131,139,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,242,244,245,246,247,292,300,301,304,309,310,311,315,319,322,335,349,350,351,352,353,355,366,367,373],\n        \"items\": [{\"object_id\":350,\"x\":50688,\"y\":-512,\"z\":47616,\"room_num\":2},{\"object_id\":352,\"x\":49664,\"y\":-512,\"z\":47616,\"room_num\":2},{\"object_id\":353,\"x\":50688,\"y\":-512,\"z\":47616,\"room_num\":2},{\"object_id\":350,\"x\":53760,\"y\":-640,\"z\":46592,\"room_num\":3},{\"object_id\":351,\"x\":54784,\"y\":-512,\"z\":47616,\"room_num\":3},{\"object_id\":352,\"x\":54784,\"y\":-512,\"z\":47616,\"room_num\":3},{\"object_id\":350,\"x\":53760,\"y\":-512,\"z\":47616,\"room_num\":3},{\"object_id\":0,\"x\":51712,\"y\":0,\"z\":50688,\"room_num\":4},{\"object_id\":131,\"x\":46592,\"y\":0,\"z\":53760,\"room_num\":7},{\"object_id\":57,\"x\":35328,\"y\":-1536,\"z\":59904,\"room_num\":8},{\"object_id\":75,\"x\":35328,\"y\":-1536,\"z\":59904,\"room_num\":8},{\"object_id\":75,\"x\":34304,\"y\":-4096,\"z\":59904,\"room_num\":8},{\"object_id\":242,\"x\":35328,\"y\":-1536,\"z\":59904,\"room_num\":8},{\"object_id\":177,\"x\":41472,\"y\":-1280,\"z\":51712,\"room_num\":8},{\"object_id\":174,\"x\":41472,\"y\":-2048,\"z\":52736,\"room_num\":8},{\"object_id\":322,\"x\":39424,\"y\":-2688,\"z\":54784,\"room_num\":8},{\"object_id\":128,\"x\":39424,\"y\":-4608,\"z\":59904,\"room_num\":10},{\"object_id\":322,\"x\":39424,\"y\":-5632,\"z\":59904,\"room_num\":10},{\"object_id\":176,\"x\":39424,\"y\":-4608,\"z\":56832,\"room_num\":10},{\"object_id\":139,\"x\":41472,\"y\":-6400,\"z\":53760,\"room_num\":11},{\"object_id\":322,\"x\":40448,\"y\":-7424,\"z\":53760,\"room_num\":11},{\"object_id\":75,\"x\":34304,\"y\":-7936,\"z\":56832,\"room_num\":12},{\"object_id\":177,\"x\":42496,\"y\":-7936,\"z\":52736,\"room_num\":12},{\"object_id\":75,\"x\":35328,\"y\":-8448,\"z\":58880,\"room_num\":12},{\"object_id\":176,\"x\":39424,\"y\":-8192,\"z\":55808,\"room_num\":12},{\"object_id\":75,\"x\":34304,\"y\":-11008,\"z\":56832,\"room_num\":13},{\"object_id\":322,\"x\":38400,\"y\":-11136,\"z\":59904,\"room_num\":13},{\"object_id\":176,\"x\":37376,\"y\":-10496,\"z\":59904,\"room_num\":13},{\"object_id\":75,\"x\":33280,\"y\":-11776,\"z\":54784,\"room_num\":14},{\"object_id\":128,\"x\":32256,\"y\":-12032,\"z\":53760,\"room_num\":14},{\"object_id\":304,\"x\":33280,\"y\":-12032,\"z\":54784,\"room_num\":14},{\"object_id\":322,\"x\":38400,\"y\":-12800,\"z\":54784,\"room_num\":14},{\"object_id\":322,\"x\":32256,\"y\":-12800,\"z\":55808,\"room_num\":14},{\"object_id\":373,\"x\":38400,\"y\":-12160,\"z\":52736,\"room_num\":14},{\"object_id\":292,\"x\":33280,\"y\":-11776,\"z\":54784,\"room_num\":14},{\"object_id\":292,\"x\":34304,\"y\":-11776,\"z\":54784,\"room_num\":14},{\"object_id\":292,\"x\":35328,\"y\":-11776,\"z\":54784,\"room_num\":14},{\"object_id\":292,\"x\":36352,\"y\":-11776,\"z\":54784,\"room_num\":14},{\"object_id\":292,\"x\":37376,\"y\":-11776,\"z\":54784,\"room_num\":14},{\"object_id\":292,\"x\":34304,\"y\":-11520,\"z\":55808,\"room_num\":14},{\"object_id\":292,\"x\":33280,\"y\":-11520,\"z\":55808,\"room_num\":14},{\"object_id\":335,\"x\":36352,\"y\":-11904,\"z\":53760,\"room_num\":14},{\"object_id\":335,\"x\":37376,\"y\":-13184,\"z\":53760,\"room_num\":14},{\"object_id\":349,\"x\":31232,\"y\":-13312,\"z\":62976,\"room_num\":15}]\n    },\n    {\n        \"title\": \"Nevada Desert\",\n        \"zone_num\": 2,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,13,16,24,29,65,69,76,79,87,88,97,118,130,131,132,133,134,139,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,224,225,228,229,232,244,245,246,247,292,295,300,301,304,305,309,310,311,315,349,350,351,355,366,367],\n        \"items\": [{\"object_id\":0,\"x\":23040,\"y\":-2176,\"z\":4608,\"room_num\":0},{\"object_id\":69,\"x\":24064,\"y\":0,\"z\":16896,\"room_num\":1},{\"object_id\":29,\"x\":19968,\"y\":-5120,\"z\":18944,\"room_num\":1},{\"object_id\":29,\"x\":27136,\"y\":-9088,\"z\":18944,\"room_num\":2},{\"object_id\":69,\"x\":24064,\"y\":-5376,\"z\":18944,\"room_num\":2},{\"object_id\":69,\"x\":24064,\"y\":-5248,\"z\":29184,\"room_num\":3},{\"object_id\":174,\"x\":32256,\"y\":0,\"z\":23040,\"room_num\":6},{\"object_id\":69,\"x\":31232,\"y\":0,\"z\":23040,\"room_num\":6},{\"object_id\":350,\"x\":31232,\"y\":-5120,\"z\":22016,\"room_num\":7},{\"object_id\":97,\"x\":29184,\"y\":-512,\"z\":33280,\"room_num\":17},{\"object_id\":169,\"x\":29184,\"y\":-512,\"z\":33280,\"room_num\":17},{\"object_id\":87,\"x\":34304,\"y\":256,\"z\":31232,\"room_num\":18},{\"object_id\":87,\"x\":33280,\"y\":-384,\"z\":31232,\"room_num\":18},{\"object_id\":87,\"x\":34304,\"y\":256,\"z\":31232,\"room_num\":18},{\"object_id\":87,\"x\":31232,\"y\":-256,\"z\":31232,\"room_num\":18},{\"object_id\":87,\"x\":31232,\"y\":-256,\"z\":31232,\"room_num\":18},{\"object_id\":178,\"x\":32256,\"y\":128,\"z\":34304,\"room_num\":18},{\"object_id\":176,\"x\":33280,\"y\":0,\"z\":33280,\"room_num\":18},{\"object_id\":69,\"x\":32256,\"y\":-128,\"z\":33280,\"room_num\":18},{\"object_id\":69,\"x\":34304,\"y\":256,\"z\":33280,\"room_num\":18},{\"object_id\":69,\"x\":30208,\"y\":-256,\"z\":31232,\"room_num\":18},{\"object_id\":180,\"x\":33280,\"y\":256,\"z\":34304,\"room_num\":18},{\"object_id\":177,\"x\":13824,\"y\":-2048,\"z\":18944,\"room_num\":19},{\"object_id\":169,\"x\":36352,\"y\":3840,\"z\":43520,\"room_num\":29},{\"object_id\":69,\"x\":41472,\"y\":-3840,\"z\":43520,\"room_num\":30},{\"object_id\":180,\"x\":36352,\"y\":-3328,\"z\":42496,\"room_num\":30},{\"object_id\":351,\"x\":39424,\"y\":-3712,\"z\":42496,\"room_num\":30},{\"object_id\":351,\"x\":32256,\"y\":-7552,\"z\":43520,\"room_num\":30},{\"object_id\":304,\"x\":36352,\"y\":-6144,\"z\":42496,\"room_num\":30},{\"object_id\":176,\"x\":30208,\"y\":-3968,\"z\":38400,\"room_num\":30},{\"object_id\":169,\"x\":35328,\"y\":10240,\"z\":39424,\"room_num\":31},{\"object_id\":171,\"x\":31232,\"y\":10240,\"z\":40448,\"room_num\":31},{\"object_id\":171,\"x\":43520,\"y\":7936,\"z\":43520,\"room_num\":31},{\"object_id\":173,\"x\":41472,\"y\":2304,\"z\":51712,\"room_num\":33},{\"object_id\":170,\"x\":41472,\"y\":2304,\"z\":50688,\"room_num\":33},{\"object_id\":29,\"x\":42496,\"y\":-4352,\"z\":49664,\"room_num\":35},{\"object_id\":171,\"x\":46592,\"y\":-4096,\"z\":44544,\"room_num\":35},{\"object_id\":295,\"x\":35328,\"y\":-3072,\"z\":46592,\"room_num\":36},{\"object_id\":176,\"x\":33280,\"y\":-3072,\"z\":47616,\"room_num\":36},{\"object_id\":178,\"x\":28160,\"y\":9472,\"z\":39424,\"room_num\":40},{\"object_id\":169,\"x\":28160,\"y\":8704,\"z\":42496,\"room_num\":40},{\"object_id\":65,\"x\":25088,\"y\":-3200,\"z\":61952,\"room_num\":43},{\"object_id\":305,\"x\":22016,\"y\":-3072,\"z\":62976,\"room_num\":43},{\"object_id\":69,\"x\":13824,\"y\":-5376,\"z\":48640,\"room_num\":46},{\"object_id\":163,\"x\":14848,\"y\":-5376,\"z\":47616,\"room_num\":46},{\"object_id\":305,\"x\":20992,\"y\":-3072,\"z\":57856,\"room_num\":47},{\"object_id\":305,\"x\":22016,\"y\":-3072,\"z\":57856,\"room_num\":47},{\"object_id\":69,\"x\":13824,\"y\":-3328,\"z\":53760,\"room_num\":48},{\"object_id\":177,\"x\":34304,\"y\":-8448,\"z\":60928,\"room_num\":63},{\"object_id\":176,\"x\":32256,\"y\":-9600,\"z\":56832,\"room_num\":64},{\"object_id\":139,\"x\":34304,\"y\":-12800,\"z\":85504,\"room_num\":65},{\"object_id\":139,\"x\":38400,\"y\":-12800,\"z\":85504,\"room_num\":65},{\"object_id\":29,\"x\":30208,\"y\":-13824,\"z\":70144,\"room_num\":65},{\"object_id\":69,\"x\":30208,\"y\":-9728,\"z\":79360,\"room_num\":65},{\"object_id\":69,\"x\":42496,\"y\":-9728,\"z\":74240,\"room_num\":65},{\"object_id\":175,\"x\":41472,\"y\":-9728,\"z\":74240,\"room_num\":65},{\"object_id\":29,\"x\":39424,\"y\":-15872,\"z\":74240,\"room_num\":66},{\"object_id\":225,\"x\":4608,\"y\":-3328,\"z\":60928,\"room_num\":69},{\"object_id\":180,\"x\":1536,\"y\":-3328,\"z\":59904,\"room_num\":73},{\"object_id\":65,\"x\":5632,\"y\":-3328,\"z\":61952,\"room_num\":78},{\"object_id\":304,\"x\":6656,\"y\":-4096,\"z\":59904,\"room_num\":82},{\"object_id\":225,\"x\":4608,\"y\":4864,\"z\":60928,\"room_num\":83},{\"object_id\":131,\"x\":55808,\"y\":-5120,\"z\":85504,\"room_num\":93},{\"object_id\":130,\"x\":58880,\"y\":-6016,\"z\":84480,\"room_num\":93},{\"object_id\":130,\"x\":31232,\"y\":-6144,\"z\":71168,\"room_num\":108},{\"object_id\":134,\"x\":34304,\"y\":-5632,\"z\":71168,\"room_num\":108},{\"object_id\":139,\"x\":36352,\"y\":-5376,\"z\":80384,\"room_num\":111},{\"object_id\":130,\"x\":40448,\"y\":-6144,\"z\":78336,\"room_num\":111},{\"object_id\":16,\"x\":47616,\"y\":-3072,\"z\":66048,\"room_num\":113},{\"object_id\":65,\"x\":42496,\"y\":-3072,\"z\":66048,\"room_num\":113},{\"object_id\":65,\"x\":45568,\"y\":-3072,\"z\":69120,\"room_num\":113},{\"object_id\":171,\"x\":47616,\"y\":-3072,\"z\":70144,\"room_num\":113},{\"object_id\":176,\"x\":42496,\"y\":-3072,\"z\":66048,\"room_num\":113},{\"object_id\":292,\"x\":49664,\"y\":-3072,\"z\":61952,\"room_num\":114},{\"object_id\":292,\"x\":47616,\"y\":-3072,\"z\":61952,\"room_num\":114},{\"object_id\":292,\"x\":46592,\"y\":-3072,\"z\":61952,\"room_num\":114},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":61952,\"room_num\":114},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":64000,\"room_num\":114},{\"object_id\":292,\"x\":51712,\"y\":-3072,\"z\":61952,\"room_num\":114},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":62976,\"room_num\":114},{\"object_id\":118,\"x\":52736,\"y\":-3072,\"z\":61952,\"room_num\":114},{\"object_id\":65,\"x\":49664,\"y\":-2816,\"z\":62976,\"room_num\":114},{\"object_id\":76,\"x\":49664,\"y\":-2816,\"z\":62976,\"room_num\":114},{\"object_id\":76,\"x\":52736,\"y\":-2816,\"z\":62976,\"room_num\":114},{\"object_id\":79,\"x\":45568,\"y\":-2816,\"z\":62976,\"room_num\":114},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":66048,\"room_num\":115},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":68096,\"room_num\":115},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":70144,\"room_num\":115},{\"object_id\":351,\"x\":51712,\"y\":-2816,\"z\":67072,\"room_num\":115},{\"object_id\":65,\"x\":51712,\"y\":-2816,\"z\":68096,\"room_num\":115},{\"object_id\":76,\"x\":51712,\"y\":-2816,\"z\":68096,\"room_num\":115},{\"object_id\":79,\"x\":51712,\"y\":-2816,\"z\":65024,\"room_num\":115},{\"object_id\":132,\"x\":50688,\"y\":-3072,\"z\":60928,\"room_num\":116},{\"object_id\":133,\"x\":51712,\"y\":-3072,\"z\":60928,\"room_num\":116},{\"object_id\":175,\"x\":54784,\"y\":-2816,\"z\":56832,\"room_num\":116},{\"object_id\":304,\"x\":55808,\"y\":-7296,\"z\":52736,\"room_num\":118},{\"object_id\":118,\"x\":32256,\"y\":-12800,\"z\":86528,\"room_num\":119},{\"object_id\":139,\"x\":34304,\"y\":-12800,\"z\":86528,\"room_num\":119},{\"object_id\":131,\"x\":39424,\"y\":-12800,\"z\":86528,\"room_num\":120},{\"object_id\":139,\"x\":38400,\"y\":-12800,\"z\":86528,\"room_num\":120},{\"object_id\":88,\"x\":35328,\"y\":-3456,\"z\":51712,\"room_num\":123},{\"object_id\":349,\"x\":20992,\"y\":-3072,\"z\":60928,\"room_num\":130},{\"object_id\":305,\"x\":20992,\"y\":-3072,\"z\":62976,\"room_num\":130},{\"object_id\":130,\"x\":36352,\"y\":-3584,\"z\":81408,\"room_num\":141},{\"object_id\":139,\"x\":40448,\"y\":-12544,\"z\":86528,\"room_num\":142},{\"object_id\":292,\"x\":60928,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":62976,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":65024,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":67072,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":69120,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":71168,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":73216,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":292,\"x\":75264,\"y\":768,\"z\":80384,\"room_num\":144},{\"object_id\":350,\"x\":69120,\"y\":-2816,\"z\":82432,\"room_num\":145},{\"object_id\":131,\"x\":69120,\"y\":0,\"z\":68096,\"room_num\":148},{\"object_id\":118,\"x\":71168,\"y\":0,\"z\":69120,\"room_num\":148},{\"object_id\":69,\"x\":66048,\"y\":0,\"z\":69120,\"room_num\":148},{\"object_id\":69,\"x\":68096,\"y\":0,\"z\":68096,\"room_num\":148},{\"object_id\":304,\"x\":69120,\"y\":-128,\"z\":68096,\"room_num\":148},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":72192,\"room_num\":149},{\"object_id\":292,\"x\":53760,\"y\":-3072,\"z\":73216,\"room_num\":149},{\"object_id\":139,\"x\":48640,\"y\":-5120,\"z\":73216,\"room_num\":149},{\"object_id\":76,\"x\":51712,\"y\":-2944,\"z\":71168,\"room_num\":149},{\"object_id\":292,\"x\":43520,\"y\":-3072,\"z\":81408,\"room_num\":151},{\"object_id\":292,\"x\":44544,\"y\":-3072,\"z\":81408,\"room_num\":151},{\"object_id\":292,\"x\":46592,\"y\":-3072,\"z\":81408,\"room_num\":151},{\"object_id\":292,\"x\":48640,\"y\":-3072,\"z\":81408,\"room_num\":151},{\"object_id\":292,\"x\":50688,\"y\":-3072,\"z\":81408,\"room_num\":151},{\"object_id\":292,\"x\":52736,\"y\":-3072,\"z\":81408,\"room_num\":151},{\"object_id\":87,\"x\":55808,\"y\":-8832,\"z\":76288,\"room_num\":154},{\"object_id\":87,\"x\":56832,\"y\":-8960,\"z\":76288,\"room_num\":154},{\"object_id\":87,\"x\":54784,\"y\":-8320,\"z\":76288,\"room_num\":154},{\"object_id\":87,\"x\":54784,\"y\":-7808,\"z\":75264,\"room_num\":154},{\"object_id\":87,\"x\":54784,\"y\":-7360,\"z\":74240,\"room_num\":154},{\"object_id\":87,\"x\":55808,\"y\":-7936,\"z\":75264,\"room_num\":154},{\"object_id\":87,\"x\":55808,\"y\":-6336,\"z\":74240,\"room_num\":154},{\"object_id\":224,\"x\":53760,\"y\":-6656,\"z\":76288,\"room_num\":154},{\"object_id\":87,\"x\":54784,\"y\":-6784,\"z\":74240,\"room_num\":154},{\"object_id\":87,\"x\":53760,\"y\":-6400,\"z\":75264,\"room_num\":154},{\"object_id\":65,\"x\":46592,\"y\":-2816,\"z\":84480,\"room_num\":155},{\"object_id\":65,\"x\":49664,\"y\":-2816,\"z\":87552,\"room_num\":155},{\"object_id\":65,\"x\":47616,\"y\":-3072,\"z\":77312,\"room_num\":156},{\"object_id\":131,\"x\":49664,\"y\":-6656,\"z\":84480,\"room_num\":159},{\"object_id\":134,\"x\":51712,\"y\":-3072,\"z\":75264,\"room_num\":162},{\"object_id\":232,\"x\":51712,\"y\":-3072,\"z\":74240,\"room_num\":162},{\"object_id\":65,\"x\":48640,\"y\":-3072,\"z\":75264,\"room_num\":163},{\"object_id\":118,\"x\":48640,\"y\":-3072,\"z\":74240,\"room_num\":163},{\"object_id\":130,\"x\":37376,\"y\":-2048,\"z\":86528,\"room_num\":166},{\"object_id\":130,\"x\":53760,\"y\":-5760,\"z\":85504,\"room_num\":168},{\"object_id\":131,\"x\":50688,\"y\":-5120,\"z\":85504,\"room_num\":169},{\"object_id\":304,\"x\":47616,\"y\":-4736,\"z\":85504,\"room_num\":169},{\"object_id\":178,\"x\":39424,\"y\":-128,\"z\":60928,\"room_num\":174},{\"object_id\":170,\"x\":35328,\"y\":-5632,\"z\":71168,\"room_num\":177},{\"object_id\":174,\"x\":30208,\"y\":-6656,\"z\":56832,\"room_num\":179}]\n    },\n    {\n        \"title\": \"High Security Compound\",\n        \"zone_num\": 2,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,13,22,24,60,61,62,64,66,67,68,74,75,76,77,78,79,87,97,106,118,120,128,130,131,132,133,134,135,136,139,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,209,210,213,214,217,218,224,225,228,229,232,233,244,245,246,247,300,301,304,309,310,311,315,319,325,330,331,332,349,350,351,352,353,354,355,366,367],\n        \"items\": [{\"object_id\":62,\"x\":13824,\"y\":0,\"z\":24064,\"room_num\":1},{\"object_id\":77,\"x\":13824,\"y\":0,\"z\":24064,\"room_num\":1},{\"object_id\":66,\"x\":16896,\"y\":-640,\"z\":22016,\"room_num\":2},{\"object_id\":0,\"x\":16896,\"y\":0,\"z\":24064,\"room_num\":2},{\"object_id\":62,\"x\":14848,\"y\":0,\"z\":36352,\"room_num\":5},{\"object_id\":62,\"x\":17920,\"y\":0,\"z\":36352,\"room_num\":6},{\"object_id\":132,\"x\":17920,\"y\":2048,\"z\":30208,\"room_num\":7},{\"object_id\":133,\"x\":17920,\"y\":2048,\"z\":31232,\"room_num\":7},{\"object_id\":87,\"x\":9728,\"y\":-1408,\"z\":37376,\"room_num\":9},{\"object_id\":87,\"x\":9728,\"y\":-1408,\"z\":37376,\"room_num\":9},{\"object_id\":87,\"x\":10752,\"y\":-640,\"z\":37376,\"room_num\":9},{\"object_id\":87,\"x\":10752,\"y\":-640,\"z\":37376,\"room_num\":9},{\"object_id\":132,\"x\":23040,\"y\":2048,\"z\":32256,\"room_num\":10},{\"object_id\":133,\"x\":23040,\"y\":2048,\"z\":33280,\"room_num\":10},{\"object_id\":76,\"x\":19968,\"y\":2048,\"z\":33280,\"room_num\":10},{\"object_id\":232,\"x\":23040,\"y\":2048,\"z\":31232,\"room_num\":10},{\"object_id\":224,\"x\":19968,\"y\":2048,\"z\":33280,\"room_num\":10},{\"object_id\":61,\"x\":19968,\"y\":2048,\"z\":33280,\"room_num\":10},{\"object_id\":139,\"x\":19968,\"y\":256,\"z\":34304,\"room_num\":10},{\"object_id\":169,\"x\":19968,\"y\":2048,\"z\":33280,\"room_num\":10},{\"object_id\":176,\"x\":19968,\"y\":2048,\"z\":33280,\"room_num\":10},{\"object_id\":118,\"x\":26112,\"y\":2816,\"z\":16896,\"room_num\":12},{\"object_id\":134,\"x\":27136,\"y\":2304,\"z\":20992,\"room_num\":13},{\"object_id\":128,\"x\":28160,\"y\":2304,\"z\":20992,\"room_num\":13},{\"object_id\":87,\"x\":29184,\"y\":1792,\"z\":27136,\"room_num\":15},{\"object_id\":87,\"x\":29184,\"y\":1792,\"z\":26112,\"room_num\":15},{\"object_id\":87,\"x\":29184,\"y\":1792,\"z\":26112,\"room_num\":15},{\"object_id\":87,\"x\":29184,\"y\":1792,\"z\":27136,\"room_num\":15},{\"object_id\":87,\"x\":29184,\"y\":1792,\"z\":30208,\"room_num\":15},{\"object_id\":87,\"x\":29184,\"y\":1792,\"z\":30208,\"room_num\":15},{\"object_id\":134,\"x\":31232,\"y\":2048,\"z\":36352,\"room_num\":16},{\"object_id\":128,\"x\":31232,\"y\":2048,\"z\":36352,\"room_num\":16},{\"object_id\":128,\"x\":27136,\"y\":2048,\"z\":39424,\"room_num\":20},{\"object_id\":131,\"x\":26112,\"y\":2048,\"z\":39424,\"room_num\":20},{\"object_id\":97,\"x\":30208,\"y\":2560,\"z\":43520,\"room_num\":21},{\"object_id\":128,\"x\":44544,\"y\":2048,\"z\":39424,\"room_num\":22},{\"object_id\":135,\"x\":39424,\"y\":256,\"z\":43520,\"room_num\":22},{\"object_id\":135,\"x\":37376,\"y\":256,\"z\":43520,\"room_num\":22},{\"object_id\":304,\"x\":38400,\"y\":256,\"z\":43520,\"room_num\":22},{\"object_id\":304,\"x\":35328,\"y\":1152,\"z\":42496,\"room_num\":22},{\"object_id\":332,\"x\":36352,\"y\":1536,\"z\":42496,\"room_num\":22},{\"object_id\":332,\"x\":35328,\"y\":1536,\"z\":41472,\"room_num\":22},{\"object_id\":332,\"x\":35328,\"y\":1536,\"z\":42496,\"room_num\":22},{\"object_id\":332,\"x\":36352,\"y\":1536,\"z\":41472,\"room_num\":22},{\"object_id\":176,\"x\":40448,\"y\":1536,\"z\":39424,\"room_num\":22},{\"object_id\":120,\"x\":37376,\"y\":256,\"z\":45568,\"room_num\":25},{\"object_id\":120,\"x\":38400,\"y\":256,\"z\":44544,\"room_num\":25},{\"object_id\":120,\"x\":39424,\"y\":256,\"z\":45568,\"room_num\":25},{\"object_id\":87,\"x\":39424,\"y\":1664,\"z\":46592,\"room_num\":25},{\"object_id\":180,\"x\":38400,\"y\":256,\"z\":44544,\"room_num\":25},{\"object_id\":118,\"x\":23040,\"y\":0,\"z\":46592,\"room_num\":27},{\"object_id\":79,\"x\":19968,\"y\":2048,\"z\":11776,\"room_num\":32},{\"object_id\":67,\"x\":73216,\"y\":-3840,\"z\":59904,\"room_num\":33},{\"object_id\":61,\"x\":76288,\"y\":-3584,\"z\":60928,\"room_num\":33},{\"object_id\":76,\"x\":76288,\"y\":-3584,\"z\":60928,\"room_num\":33},{\"object_id\":79,\"x\":74240,\"y\":-3584,\"z\":53760,\"room_num\":33},{\"object_id\":206,\"x\":76288,\"y\":-3584,\"z\":60928,\"room_num\":33},{\"object_id\":180,\"x\":77312,\"y\":-3840,\"z\":58880,\"room_num\":33},{\"object_id\":176,\"x\":73216,\"y\":-2048,\"z\":53760,\"room_num\":34},{\"object_id\":106,\"x\":70144,\"y\":-3328,\"z\":58880,\"room_num\":34},{\"object_id\":22,\"x\":75264,\"y\":128,\"z\":67072,\"room_num\":37},{\"object_id\":61,\"x\":79360,\"y\":-1920,\"z\":75264,\"room_num\":40},{\"object_id\":75,\"x\":83456,\"y\":-2944,\"z\":76288,\"room_num\":40},{\"object_id\":60,\"x\":83456,\"y\":-2944,\"z\":76288,\"room_num\":40},{\"object_id\":77,\"x\":83456,\"y\":-2944,\"z\":76288,\"room_num\":40},{\"object_id\":304,\"x\":75264,\"y\":-5504,\"z\":16896,\"room_num\":44},{\"object_id\":352,\"x\":71168,\"y\":-3840,\"z\":20992,\"room_num\":44},{\"object_id\":351,\"x\":71168,\"y\":-3840,\"z\":20992,\"room_num\":44},{\"object_id\":128,\"x\":67072,\"y\":-1280,\"z\":18944,\"room_num\":46},{\"object_id\":353,\"x\":66048,\"y\":-1024,\"z\":22016,\"room_num\":46},{\"object_id\":131,\"x\":65024,\"y\":-1280,\"z\":27136,\"room_num\":47},{\"object_id\":128,\"x\":65024,\"y\":-1280,\"z\":25088,\"room_num\":48},{\"object_id\":118,\"x\":23040,\"y\":2048,\"z\":26112,\"room_num\":50},{\"object_id\":353,\"x\":22016,\"y\":2048,\"z\":26112,\"room_num\":50},{\"object_id\":139,\"x\":22016,\"y\":256,\"z\":23040,\"room_num\":51},{\"object_id\":97,\"x\":1536,\"y\":-512,\"z\":37376,\"room_num\":54},{\"object_id\":176,\"x\":1536,\"y\":-512,\"z\":38400,\"room_num\":54},{\"object_id\":97,\"x\":3584,\"y\":-512,\"z\":37376,\"room_num\":57},{\"object_id\":64,\"x\":78336,\"y\":-1024,\"z\":55808,\"room_num\":59},{\"object_id\":139,\"x\":20992,\"y\":-1792,\"z\":37376,\"room_num\":63},{\"object_id\":139,\"x\":22016,\"y\":-1792,\"z\":37376,\"room_num\":63},{\"object_id\":87,\"x\":19968,\"y\":-1792,\"z\":37376,\"room_num\":63},{\"object_id\":87,\"x\":19968,\"y\":-1792,\"z\":37376,\"room_num\":63},{\"object_id\":87,\"x\":23040,\"y\":-1792,\"z\":37376,\"room_num\":63},{\"object_id\":118,\"x\":23040,\"y\":-1792,\"z\":37376,\"room_num\":63},{\"object_id\":87,\"x\":22016,\"y\":3328,\"z\":37376,\"room_num\":64},{\"object_id\":87,\"x\":20992,\"y\":3328,\"z\":37376,\"room_num\":64},{\"object_id\":87,\"x\":22016,\"y\":3328,\"z\":37376,\"room_num\":64},{\"object_id\":87,\"x\":20992,\"y\":3328,\"z\":37376,\"room_num\":64},{\"object_id\":62,\"x\":72192,\"y\":1280,\"z\":58880,\"room_num\":65},{\"object_id\":78,\"x\":72192,\"y\":1280,\"z\":58880,\"room_num\":65},{\"object_id\":133,\"x\":73216,\"y\":2048,\"z\":53760,\"room_num\":65},{\"object_id\":132,\"x\":72192,\"y\":2048,\"z\":53760,\"room_num\":65},{\"object_id\":214,\"x\":71168,\"y\":768,\"z\":53760,\"room_num\":65},{\"object_id\":78,\"x\":78336,\"y\":1024,\"z\":53760,\"room_num\":66},{\"object_id\":136,\"x\":79360,\"y\":1024,\"z\":53760,\"room_num\":66},{\"object_id\":66,\"x\":79360,\"y\":1024,\"z\":54784,\"room_num\":66},{\"object_id\":128,\"x\":79360,\"y\":1024,\"z\":53760,\"room_num\":66},{\"object_id\":128,\"x\":92672,\"y\":-6656,\"z\":71168,\"room_num\":71},{\"object_id\":128,\"x\":93696,\"y\":-6656,\"z\":71168,\"room_num\":71},{\"object_id\":60,\"x\":93696,\"y\":-6656,\"z\":71168,\"room_num\":71},{\"object_id\":74,\"x\":93696,\"y\":-6656,\"z\":71168,\"room_num\":71},{\"object_id\":132,\"x\":94720,\"y\":-4864,\"z\":83456,\"room_num\":74},{\"object_id\":133,\"x\":93696,\"y\":-4864,\"z\":83456,\"room_num\":74},{\"object_id\":205,\"x\":92672,\"y\":-4864,\"z\":79360,\"room_num\":74},{\"object_id\":61,\"x\":92672,\"y\":-4864,\"z\":79360,\"room_num\":74},{\"object_id\":304,\"x\":93696,\"y\":-5504,\"z\":83456,\"room_num\":74},{\"object_id\":75,\"x\":91648,\"y\":-4864,\"z\":82432,\"room_num\":74},{\"object_id\":175,\"x\":92672,\"y\":-4864,\"z\":79360,\"room_num\":74},{\"object_id\":304,\"x\":91648,\"y\":-4864,\"z\":81408,\"room_num\":74},{\"object_id\":75,\"x\":94720,\"y\":-4864,\"z\":75264,\"room_num\":74},{\"object_id\":131,\"x\":90624,\"y\":-6656,\"z\":72192,\"room_num\":75},{\"object_id\":213,\"x\":89600,\"y\":-6656,\"z\":73216,\"room_num\":75},{\"object_id\":61,\"x\":92672,\"y\":-4864,\"z\":85504,\"room_num\":78},{\"object_id\":22,\"x\":92672,\"y\":-4864,\"z\":86528,\"room_num\":78},{\"object_id\":75,\"x\":92672,\"y\":-4864,\"z\":86528,\"room_num\":78},{\"object_id\":75,\"x\":92672,\"y\":-4864,\"z\":85504,\"room_num\":78},{\"object_id\":77,\"x\":92672,\"y\":-4864,\"z\":86528,\"room_num\":78},{\"object_id\":77,\"x\":92672,\"y\":-4864,\"z\":85504,\"room_num\":78},{\"object_id\":175,\"x\":50688,\"y\":0,\"z\":71168,\"room_num\":81},{\"object_id\":171,\"x\":50688,\"y\":0,\"z\":71168,\"room_num\":81},{\"object_id\":131,\"x\":43520,\"y\":2048,\"z\":38400,\"room_num\":84},{\"object_id\":131,\"x\":42496,\"y\":2048,\"z\":36352,\"room_num\":84},{\"object_id\":128,\"x\":42496,\"y\":2048,\"z\":36352,\"room_num\":84},{\"object_id\":128,\"x\":43520,\"y\":2048,\"z\":37376,\"room_num\":84},{\"object_id\":131,\"x\":44544,\"y\":2048,\"z\":36352,\"room_num\":86},{\"object_id\":60,\"x\":45568,\"y\":2048,\"z\":36352,\"room_num\":86},{\"object_id\":128,\"x\":46592,\"y\":2048,\"z\":36352,\"room_num\":86},{\"object_id\":128,\"x\":44544,\"y\":2048,\"z\":36352,\"room_num\":86},{\"object_id\":176,\"x\":45568,\"y\":2048,\"z\":36352,\"room_num\":86},{\"object_id\":139,\"x\":42496,\"y\":-2304,\"z\":50688,\"room_num\":87},{\"object_id\":75,\"x\":38400,\"y\":-2432,\"z\":50688,\"room_num\":87},{\"object_id\":62,\"x\":41472,\"y\":-2304,\"z\":50688,\"room_num\":87},{\"object_id\":75,\"x\":41472,\"y\":-2304,\"z\":50688,\"room_num\":87},{\"object_id\":77,\"x\":41472,\"y\":-2304,\"z\":50688,\"room_num\":87},{\"object_id\":304,\"x\":39424,\"y\":-2304,\"z\":50688,\"room_num\":87},{\"object_id\":87,\"x\":38400,\"y\":4864,\"z\":47616,\"room_num\":88},{\"object_id\":87,\"x\":38400,\"y\":4864,\"z\":47616,\"room_num\":88},{\"object_id\":61,\"x\":56832,\"y\":0,\"z\":61952,\"room_num\":96},{\"object_id\":176,\"x\":49664,\"y\":-1024,\"z\":61952,\"room_num\":96},{\"object_id\":170,\"x\":49664,\"y\":-1024,\"z\":61952,\"room_num\":96},{\"object_id\":97,\"x\":50688,\"y\":0,\"z\":70144,\"room_num\":96},{\"object_id\":61,\"x\":58880,\"y\":512,\"z\":55808,\"room_num\":99},{\"object_id\":79,\"x\":64000,\"y\":-9472,\"z\":53760,\"room_num\":101},{\"object_id\":128,\"x\":73216,\"y\":-3584,\"z\":29184,\"room_num\":105},{\"object_id\":134,\"x\":73216,\"y\":-3584,\"z\":30208,\"room_num\":105},{\"object_id\":131,\"x\":73216,\"y\":-3584,\"z\":30208,\"room_num\":105},{\"object_id\":62,\"x\":71168,\"y\":-3584,\"z\":32256,\"room_num\":106},{\"object_id\":62,\"x\":69120,\"y\":-3584,\"z\":32256,\"room_num\":106},{\"object_id\":62,\"x\":77312,\"y\":-3584,\"z\":31232,\"room_num\":108},{\"object_id\":77,\"x\":77312,\"y\":-3584,\"z\":31232,\"room_num\":108},{\"object_id\":319,\"x\":47616,\"y\":-6912,\"z\":19968,\"room_num\":110},{\"object_id\":136,\"x\":50688,\"y\":-5120,\"z\":15872,\"room_num\":111},{\"object_id\":128,\"x\":71168,\"y\":-3584,\"z\":30208,\"room_num\":112},{\"object_id\":131,\"x\":70144,\"y\":-3584,\"z\":30208,\"room_num\":112},{\"object_id\":128,\"x\":69120,\"y\":-3584,\"z\":30208,\"room_num\":112},{\"object_id\":62,\"x\":8704,\"y\":0,\"z\":25088,\"room_num\":113},{\"object_id\":131,\"x\":10752,\"y\":0,\"z\":34304,\"room_num\":117},{\"object_id\":131,\"x\":7680,\"y\":0,\"z\":34304,\"room_num\":117},{\"object_id\":131,\"x\":7680,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":131,\"x\":10752,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":131,\"x\":13824,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":131,\"x\":16896,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":131,\"x\":16896,\"y\":0,\"z\":34304,\"room_num\":117},{\"object_id\":131,\"x\":13824,\"y\":0,\"z\":34304,\"room_num\":117},{\"object_id\":60,\"x\":14848,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":128,\"x\":14848,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":128,\"x\":9728,\"y\":0,\"z\":27136,\"room_num\":117},{\"object_id\":128,\"x\":9728,\"y\":0,\"z\":34304,\"room_num\":117},{\"object_id\":128,\"x\":14848,\"y\":0,\"z\":34304,\"room_num\":117},{\"object_id\":131,\"x\":72192,\"y\":8960,\"z\":29184,\"room_num\":123},{\"object_id\":128,\"x\":72192,\"y\":8960,\"z\":28160,\"room_num\":123},{\"object_id\":214,\"x\":70144,\"y\":8960,\"z\":23040,\"room_num\":124},{\"object_id\":61,\"x\":75264,\"y\":-256,\"z\":7680,\"room_num\":125},{\"object_id\":74,\"x\":75264,\"y\":-256,\"z\":7680,\"room_num\":125},{\"object_id\":175,\"x\":75264,\"y\":-256,\"z\":7680,\"room_num\":125},{\"object_id\":175,\"x\":75264,\"y\":-256,\"z\":7680,\"room_num\":125},{\"object_id\":225,\"x\":75264,\"y\":-256,\"z\":7680,\"room_num\":125},{\"object_id\":233,\"x\":81408,\"y\":-256,\"z\":7680,\"room_num\":126},{\"object_id\":206,\"x\":67072,\"y\":-1280,\"z\":10752,\"room_num\":129},{\"object_id\":60,\"x\":64000,\"y\":-9472,\"z\":45568,\"room_num\":130},{\"object_id\":76,\"x\":64000,\"y\":-9472,\"z\":45568,\"room_num\":130},{\"object_id\":233,\"x\":64000,\"y\":-9472,\"z\":42496,\"room_num\":130},{\"object_id\":225,\"x\":64000,\"y\":-9472,\"z\":45568,\"room_num\":130},{\"object_id\":176,\"x\":54784,\"y\":-6400,\"z\":48640,\"room_num\":133},{\"object_id\":64,\"x\":61952,\"y\":-11008,\"z\":38400,\"room_num\":134},{\"object_id\":66,\"x\":64000,\"y\":-9472,\"z\":39424,\"room_num\":134},{\"object_id\":77,\"x\":61952,\"y\":-11008,\"z\":38400,\"room_num\":134},{\"object_id\":61,\"x\":53760,\"y\":-2560,\"z\":13824,\"room_num\":135},{\"object_id\":76,\"x\":53760,\"y\":-2560,\"z\":13824,\"room_num\":135},{\"object_id\":79,\"x\":53760,\"y\":-2560,\"z\":17920,\"room_num\":135},{\"object_id\":68,\"x\":56832,\"y\":-2816,\"z\":16896,\"room_num\":135},{\"object_id\":118,\"x\":56832,\"y\":-2560,\"z\":15872,\"room_num\":135},{\"object_id\":304,\"x\":55808,\"y\":-3200,\"z\":14848,\"room_num\":135},{\"object_id\":64,\"x\":56832,\"y\":-12032,\"z\":35328,\"room_num\":138},{\"object_id\":77,\"x\":56832,\"y\":-12032,\"z\":35328,\"room_num\":138},{\"object_id\":136,\"x\":55808,\"y\":-2304,\"z\":18944,\"room_num\":140},{\"object_id\":118,\"x\":72192,\"y\":8960,\"z\":30208,\"room_num\":147},{\"object_id\":304,\"x\":71168,\"y\":16640,\"z\":22016,\"room_num\":148},{\"object_id\":180,\"x\":71168,\"y\":16640,\"z\":22016,\"room_num\":148},{\"object_id\":128,\"x\":61952,\"y\":-10496,\"z\":44544,\"room_num\":149},{\"object_id\":136,\"x\":62976,\"y\":-9472,\"z\":41472,\"room_num\":149},{\"object_id\":177,\"x\":62976,\"y\":-10496,\"z\":43520,\"room_num\":149},{\"object_id\":171,\"x\":62976,\"y\":-10496,\"z\":43520,\"room_num\":149},{\"object_id\":176,\"x\":79360,\"y\":9472,\"z\":44544,\"room_num\":152},{\"object_id\":67,\"x\":80384,\"y\":6400,\"z\":49664,\"room_num\":154},{\"object_id\":354,\"x\":55808,\"y\":-12288,\"z\":42496,\"room_num\":158},{\"object_id\":64,\"x\":58880,\"y\":-5888,\"z\":33280,\"room_num\":159},{\"object_id\":77,\"x\":58880,\"y\":-5888,\"z\":33280,\"room_num\":159},{\"object_id\":60,\"x\":53760,\"y\":-4864,\"z\":22016,\"room_num\":160},{\"object_id\":76,\"x\":53760,\"y\":-4864,\"z\":22016,\"room_num\":160},{\"object_id\":214,\"x\":51712,\"y\":-4864,\"z\":15872,\"room_num\":160},{\"object_id\":206,\"x\":53760,\"y\":-4864,\"z\":22016,\"room_num\":160},{\"object_id\":64,\"x\":58880,\"y\":-5888,\"z\":24064,\"room_num\":160},{\"object_id\":77,\"x\":58880,\"y\":-5888,\"z\":24064,\"room_num\":160},{\"object_id\":349,\"x\":54784,\"y\":-4864,\"z\":13824,\"room_num\":160},{\"object_id\":79,\"x\":53760,\"y\":-4864,\"z\":12800,\"room_num\":160},{\"object_id\":178,\"x\":53760,\"y\":-4864,\"z\":22016,\"room_num\":160},{\"object_id\":76,\"x\":77312,\"y\":8960,\"z\":28160,\"room_num\":162},{\"object_id\":61,\"x\":77312,\"y\":8960,\"z\":28160,\"room_num\":162},{\"object_id\":79,\"x\":77312,\"y\":8960,\"z\":19968,\"room_num\":162},{\"object_id\":160,\"x\":76288,\"y\":512,\"z\":51712,\"room_num\":167},{\"object_id\":177,\"x\":77312,\"y\":512,\"z\":51712,\"room_num\":167},{\"object_id\":162,\"x\":76288,\"y\":512,\"z\":49664,\"room_num\":168},{\"object_id\":170,\"x\":76288,\"y\":512,\"z\":49664,\"room_num\":168},{\"object_id\":170,\"x\":76288,\"y\":512,\"z\":49664,\"room_num\":168},{\"object_id\":61,\"x\":87552,\"y\":-4864,\"z\":79360,\"room_num\":169},{\"object_id\":61,\"x\":88576,\"y\":-4864,\"z\":79360,\"room_num\":169},{\"object_id\":136,\"x\":90624,\"y\":-4864,\"z\":81408,\"room_num\":169},{\"object_id\":177,\"x\":89600,\"y\":-4864,\"z\":80384,\"room_num\":169},{\"object_id\":176,\"x\":82432,\"y\":11264,\"z\":22016,\"room_num\":170},{\"object_id\":120,\"x\":71168,\"y\":16640,\"z\":19968,\"room_num\":172},{\"object_id\":136,\"x\":71168,\"y\":16640,\"z\":20992,\"room_num\":172},{\"object_id\":136,\"x\":79360,\"y\":12032,\"z\":39424,\"room_num\":175},{\"object_id\":130,\"x\":79360,\"y\":11520,\"z\":39424,\"room_num\":175},{\"object_id\":178,\"x\":79360,\"y\":12544,\"z\":36352,\"room_num\":175},{\"object_id\":350,\"x\":2560,\"y\":-1664,\"z\":94720,\"room_num\":176},{\"object_id\":350,\"x\":2560,\"y\":-896,\"z\":91648,\"room_num\":176},{\"object_id\":304,\"x\":2560,\"y\":-1152,\"z\":92672,\"room_num\":176},{\"object_id\":136,\"x\":82432,\"y\":-256,\"z\":7680,\"room_num\":177},{\"object_id\":167,\"x\":82432,\"y\":-256,\"z\":10752,\"room_num\":177}]\n    },\n    {\n        \"title\": \"Area 51\",\n        \"zone_num\": 2,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,22,24,25,60,62,63,64,66,67,68,74,75,76,77,78,79,80,81,82,87,101,106,118,128,131,132,133,134,135,136,137,138,139,140,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,224,227,228,231,232,235,241,244,245,246,247,291,300,301,304,309,310,311,312,313,314,315,318,319,321,331,332,333,336,349,351,355,366,367],\n        \"items\": [{\"object_id\":63,\"x\":43520,\"y\":-256,\"z\":62976,\"room_num\":0},{\"object_id\":169,\"x\":32256,\"y\":0,\"z\":58880,\"room_num\":0},{\"object_id\":79,\"x\":40448,\"y\":-256,\"z\":62976,\"room_num\":0},{\"object_id\":76,\"x\":43520,\"y\":-256,\"z\":62976,\"room_num\":0},{\"object_id\":75,\"x\":43520,\"y\":-256,\"z\":60928,\"room_num\":0},{\"object_id\":22,\"x\":44544,\"y\":256,\"z\":57856,\"room_num\":0},{\"object_id\":22,\"x\":43520,\"y\":256,\"z\":58880,\"room_num\":0},{\"object_id\":75,\"x\":43520,\"y\":-256,\"z\":62976,\"room_num\":0},{\"object_id\":74,\"x\":39424,\"y\":-3328,\"z\":71168,\"room_num\":1},{\"object_id\":60,\"x\":39424,\"y\":-3328,\"z\":71168,\"room_num\":1},{\"object_id\":134,\"x\":43520,\"y\":-3328,\"z\":73216,\"room_num\":1},{\"object_id\":75,\"x\":39424,\"y\":-3328,\"z\":71168,\"room_num\":1},{\"object_id\":75,\"x\":42496,\"y\":-3328,\"z\":72192,\"room_num\":1},{\"object_id\":118,\"x\":38400,\"y\":-3328,\"z\":57856,\"room_num\":3},{\"object_id\":118,\"x\":38400,\"y\":-3328,\"z\":58880,\"room_num\":3},{\"object_id\":118,\"x\":38400,\"y\":-3328,\"z\":59904,\"room_num\":3},{\"object_id\":118,\"x\":38400,\"y\":-3328,\"z\":60928,\"room_num\":3},{\"object_id\":118,\"x\":38400,\"y\":-3328,\"z\":61952,\"room_num\":3},{\"object_id\":63,\"x\":36352,\"y\":-3328,\"z\":59904,\"room_num\":3},{\"object_id\":139,\"x\":39424,\"y\":-3328,\"z\":66048,\"room_num\":4},{\"object_id\":139,\"x\":40448,\"y\":-3328,\"z\":66048,\"room_num\":4},{\"object_id\":118,\"x\":39424,\"y\":-3328,\"z\":66048,\"room_num\":4},{\"object_id\":79,\"x\":50688,\"y\":-1024,\"z\":64000,\"room_num\":9},{\"object_id\":318,\"x\":50688,\"y\":-1280,\"z\":57856,\"room_num\":9},{\"object_id\":75,\"x\":49664,\"y\":-1024,\"z\":66048,\"room_num\":9},{\"object_id\":68,\"x\":50688,\"y\":-1408,\"z\":60928,\"room_num\":9},{\"object_id\":68,\"x\":50688,\"y\":-1280,\"z\":62976,\"room_num\":9},{\"object_id\":68,\"x\":50688,\"y\":-1664,\"z\":62976,\"room_num\":9},{\"object_id\":66,\"x\":51712,\"y\":-2176,\"z\":70144,\"room_num\":10},{\"object_id\":64,\"x\":50688,\"y\":-2816,\"z\":70144,\"room_num\":10},{\"object_id\":176,\"x\":61952,\"y\":3072,\"z\":55808,\"room_num\":11},{\"object_id\":138,\"x\":57856,\"y\":3072,\"z\":61952,\"room_num\":12},{\"object_id\":128,\"x\":56832,\"y\":3072,\"z\":61952,\"room_num\":12},{\"object_id\":318,\"x\":64000,\"y\":2560,\"z\":62976,\"room_num\":12},{\"object_id\":318,\"x\":54784,\"y\":2560,\"z\":62976,\"room_num\":12},{\"object_id\":60,\"x\":59904,\"y\":3072,\"z\":61952,\"room_num\":12},{\"object_id\":75,\"x\":59904,\"y\":3072,\"z\":61952,\"room_num\":12},{\"object_id\":75,\"x\":54784,\"y\":3072,\"z\":62976,\"room_num\":12},{\"object_id\":62,\"x\":48640,\"y\":-1024,\"z\":51712,\"room_num\":13},{\"object_id\":68,\"x\":54784,\"y\":3072,\"z\":59904,\"room_num\":14},{\"object_id\":173,\"x\":53760,\"y\":3072,\"z\":58880,\"room_num\":14},{\"object_id\":118,\"x\":53760,\"y\":3072,\"z\":57856,\"room_num\":14},{\"object_id\":165,\"x\":54784,\"y\":3072,\"z\":57856,\"room_num\":14},{\"object_id\":173,\"x\":54784,\"y\":3072,\"z\":58880,\"room_num\":14},{\"object_id\":241,\"x\":44544,\"y\":-7424,\"z\":35328,\"room_num\":18},{\"object_id\":63,\"x\":44544,\"y\":-7168,\"z\":41472,\"room_num\":18},{\"object_id\":63,\"x\":44544,\"y\":-7168,\"z\":29184,\"room_num\":18},{\"object_id\":63,\"x\":49664,\"y\":-5248,\"z\":35328,\"room_num\":19},{\"object_id\":321,\"x\":44544,\"y\":-7168,\"z\":28160,\"room_num\":20},{\"object_id\":321,\"x\":44544,\"y\":-7168,\"z\":42496,\"room_num\":21},{\"object_id\":63,\"x\":13824,\"y\":-3840,\"z\":51712,\"room_num\":26},{\"object_id\":118,\"x\":12800,\"y\":-3840,\"z\":49664,\"room_num\":26},{\"object_id\":207,\"x\":13824,\"y\":-3840,\"z\":51712,\"room_num\":26},{\"object_id\":291,\"x\":27136,\"y\":-1920,\"z\":45568,\"room_num\":28},{\"object_id\":0,\"x\":64000,\"y\":3072,\"z\":52736,\"room_num\":31},{\"object_id\":135,\"x\":50688,\"y\":-1024,\"z\":54784,\"room_num\":34},{\"object_id\":134,\"x\":49664,\"y\":-1024,\"z\":54784,\"room_num\":34},{\"object_id\":118,\"x\":49664,\"y\":-1024,\"z\":55808,\"room_num\":34},{\"object_id\":76,\"x\":50688,\"y\":-1024,\"z\":54784,\"room_num\":34},{\"object_id\":60,\"x\":50688,\"y\":-1024,\"z\":54784,\"room_num\":34},{\"object_id\":75,\"x\":50688,\"y\":-1024,\"z\":54784,\"room_num\":34},{\"object_id\":176,\"x\":51712,\"y\":-1024,\"z\":53760,\"room_num\":35},{\"object_id\":170,\"x\":29184,\"y\":-1280,\"z\":48640,\"room_num\":36},{\"object_id\":178,\"x\":29184,\"y\":-1280,\"z\":47616,\"room_num\":36},{\"object_id\":167,\"x\":44544,\"y\":1024,\"z\":60928,\"room_num\":41},{\"object_id\":291,\"x\":41472,\"y\":-7168,\"z\":41472,\"room_num\":42},{\"object_id\":291,\"x\":41472,\"y\":-6144,\"z\":41472,\"room_num\":42},{\"object_id\":118,\"x\":40448,\"y\":2048,\"z\":61952,\"room_num\":44},{\"object_id\":175,\"x\":26112,\"y\":-2304,\"z\":57856,\"room_num\":45},{\"object_id\":291,\"x\":57856,\"y\":1536,\"z\":55808,\"room_num\":46},{\"object_id\":312,\"x\":24064,\"y\":3968,\"z\":65024,\"room_num\":49},{\"object_id\":313,\"x\":24064,\"y\":4096,\"z\":65024,\"room_num\":49},{\"object_id\":314,\"x\":24064,\"y\":2304,\"z\":65024,\"room_num\":49},{\"object_id\":140,\"x\":24064,\"y\":-2560,\"z\":66048,\"room_num\":50},{\"object_id\":137,\"x\":23040,\"y\":-2816,\"z\":66048,\"room_num\":50},{\"object_id\":118,\"x\":22016,\"y\":-2816,\"z\":66048,\"room_num\":50},{\"object_id\":60,\"x\":25088,\"y\":-2816,\"z\":62976,\"room_num\":50},{\"object_id\":314,\"x\":24064,\"y\":1152,\"z\":65024,\"room_num\":51},{\"object_id\":63,\"x\":23040,\"y\":1536,\"z\":66048,\"room_num\":51},{\"object_id\":140,\"x\":22016,\"y\":-512,\"z\":66048,\"room_num\":51},{\"object_id\":206,\"x\":23040,\"y\":1536,\"z\":66048,\"room_num\":51},{\"object_id\":304,\"x\":23040,\"y\":4480,\"z\":65024,\"room_num\":52},{\"object_id\":304,\"x\":22016,\"y\":-1408,\"z\":62976,\"room_num\":53},{\"object_id\":177,\"x\":25088,\"y\":-512,\"z\":62976,\"room_num\":53},{\"object_id\":177,\"x\":56832,\"y\":3072,\"z\":59904,\"room_num\":54},{\"object_id\":136,\"x\":24064,\"y\":5888,\"z\":67072,\"room_num\":59},{\"object_id\":118,\"x\":24064,\"y\":5888,\"z\":67072,\"room_num\":59},{\"object_id\":60,\"x\":37376,\"y\":7424,\"z\":51712,\"room_num\":60},{\"object_id\":76,\"x\":37376,\"y\":7424,\"z\":51712,\"room_num\":60},{\"object_id\":79,\"x\":30208,\"y\":7424,\"z\":51712,\"room_num\":60},{\"object_id\":140,\"x\":41472,\"y\":2048,\"z\":58880,\"room_num\":62},{\"object_id\":138,\"x\":41472,\"y\":3584,\"z\":61952,\"room_num\":62},{\"object_id\":138,\"x\":38400,\"y\":3584,\"z\":61952,\"room_num\":62},{\"object_id\":304,\"x\":38400,\"y\":3584,\"z\":61952,\"room_num\":62},{\"object_id\":60,\"x\":39424,\"y\":3584,\"z\":61952,\"room_num\":62},{\"object_id\":173,\"x\":34304,\"y\":3584,\"z\":62976,\"room_num\":63},{\"object_id\":170,\"x\":36352,\"y\":3584,\"z\":62976,\"room_num\":63},{\"object_id\":140,\"x\":34304,\"y\":3584,\"z\":60928,\"room_num\":64},{\"object_id\":60,\"x\":34304,\"y\":5632,\"z\":62976,\"room_num\":64},{\"object_id\":60,\"x\":34304,\"y\":5632,\"z\":58880,\"room_num\":64},{\"object_id\":176,\"x\":34304,\"y\":5632,\"z\":62976,\"room_num\":64},{\"object_id\":76,\"x\":34304,\"y\":5632,\"z\":62976,\"room_num\":64},{\"object_id\":79,\"x\":34304,\"y\":5632,\"z\":53760,\"room_num\":64},{\"object_id\":131,\"x\":29184,\"y\":5632,\"z\":68096,\"room_num\":66},{\"object_id\":118,\"x\":29184,\"y\":5632,\"z\":68096,\"room_num\":66},{\"object_id\":318,\"x\":32256,\"y\":5120,\"z\":68096,\"room_num\":66},{\"object_id\":62,\"x\":27136,\"y\":5632,\"z\":69120,\"room_num\":68},{\"object_id\":177,\"x\":28160,\"y\":5632,\"z\":70144,\"room_num\":68},{\"object_id\":66,\"x\":39424,\"y\":5248,\"z\":41472,\"room_num\":69},{\"object_id\":66,\"x\":36352,\"y\":5632,\"z\":41472,\"room_num\":69},{\"object_id\":67,\"x\":28160,\"y\":5632,\"z\":41472,\"room_num\":69},{\"object_id\":128,\"x\":40448,\"y\":7424,\"z\":52736,\"room_num\":70},{\"object_id\":64,\"x\":41472,\"y\":5120,\"z\":41472,\"room_num\":70},{\"object_id\":128,\"x\":25088,\"y\":7424,\"z\":52736,\"room_num\":71},{\"object_id\":64,\"x\":24064,\"y\":4864,\"z\":41472,\"room_num\":71},{\"object_id\":63,\"x\":25088,\"y\":7424,\"z\":51712,\"room_num\":71},{\"object_id\":74,\"x\":25088,\"y\":7424,\"z\":51712,\"room_num\":71},{\"object_id\":134,\"x\":33280,\"y\":7424,\"z\":55808,\"room_num\":72},{\"object_id\":135,\"x\":33280,\"y\":7424,\"z\":54784,\"room_num\":72},{\"object_id\":137,\"x\":34304,\"y\":7424,\"z\":55808,\"room_num\":72},{\"object_id\":135,\"x\":35328,\"y\":7424,\"z\":55808,\"room_num\":72},{\"object_id\":134,\"x\":35328,\"y\":7424,\"z\":54784,\"room_num\":72},{\"object_id\":137,\"x\":34304,\"y\":7424,\"z\":57856,\"room_num\":72},{\"object_id\":318,\"x\":25088,\"y\":4096,\"z\":58880,\"room_num\":73},{\"object_id\":66,\"x\":32256,\"y\":7168,\"z\":57856,\"room_num\":73},{\"object_id\":60,\"x\":25088,\"y\":3584,\"z\":55808,\"room_num\":74},{\"object_id\":214,\"x\":20992,\"y\":4864,\"z\":55808,\"room_num\":75},{\"object_id\":349,\"x\":16896,\"y\":4608,\"z\":52736,\"room_num\":75},{\"object_id\":349,\"x\":14848,\"y\":4608,\"z\":52736,\"room_num\":75},{\"object_id\":304,\"x\":15872,\"y\":3328,\"z\":52736,\"room_num\":75},{\"object_id\":66,\"x\":20992,\"y\":4864,\"z\":54784,\"room_num\":75},{\"object_id\":66,\"x\":19968,\"y\":4864,\"z\":50688,\"room_num\":75},{\"object_id\":176,\"x\":11776,\"y\":5504,\"z\":51712,\"room_num\":75},{\"object_id\":64,\"x\":20992,\"y\":2816,\"z\":56832,\"room_num\":76},{\"object_id\":169,\"x\":14848,\"y\":3584,\"z\":53760,\"room_num\":76},{\"object_id\":106,\"x\":15872,\"y\":1792,\"z\":54784,\"room_num\":76},{\"object_id\":101,\"x\":13824,\"y\":2816,\"z\":55808,\"room_num\":76},{\"object_id\":63,\"x\":20992,\"y\":1536,\"z\":56832,\"room_num\":78},{\"object_id\":208,\"x\":20992,\"y\":1536,\"z\":56832,\"room_num\":78},{\"object_id\":180,\"x\":15872,\"y\":6912,\"z\":49664,\"room_num\":80},{\"object_id\":60,\"x\":32256,\"y\":3840,\"z\":72192,\"room_num\":87},{\"object_id\":74,\"x\":32256,\"y\":3840,\"z\":72192,\"room_num\":87},{\"object_id\":128,\"x\":31232,\"y\":768,\"z\":80384,\"room_num\":88},{\"object_id\":131,\"x\":28160,\"y\":3584,\"z\":75264,\"room_num\":89},{\"object_id\":216,\"x\":28160,\"y\":3584,\"z\":75264,\"room_num\":89},{\"object_id\":169,\"x\":33280,\"y\":4608,\"z\":50688,\"room_num\":91},{\"object_id\":75,\"x\":31232,\"y\":3840,\"z\":50688,\"room_num\":92},{\"object_id\":60,\"x\":31232,\"y\":3840,\"z\":50688,\"room_num\":92},{\"object_id\":77,\"x\":31232,\"y\":3840,\"z\":50688,\"room_num\":92},{\"object_id\":140,\"x\":34304,\"y\":1536,\"z\":56832,\"room_num\":93},{\"object_id\":140,\"x\":34304,\"y\":1536,\"z\":57856,\"room_num\":93},{\"object_id\":140,\"x\":34304,\"y\":1536,\"z\":58880,\"room_num\":93},{\"object_id\":291,\"x\":34304,\"y\":640,\"z\":54784,\"room_num\":93},{\"object_id\":134,\"x\":40448,\"y\":-2304,\"z\":54784,\"room_num\":95},{\"object_id\":135,\"x\":39424,\"y\":-2304,\"z\":54784,\"room_num\":95},{\"object_id\":139,\"x\":39424,\"y\":-2304,\"z\":53760,\"room_num\":95},{\"object_id\":139,\"x\":40448,\"y\":-2304,\"z\":53760,\"room_num\":95},{\"object_id\":139,\"x\":40448,\"y\":-2304,\"z\":54784,\"room_num\":95},{\"object_id\":139,\"x\":39424,\"y\":-2304,\"z\":54784,\"room_num\":95},{\"object_id\":134,\"x\":40448,\"y\":-2304,\"z\":52736,\"room_num\":96},{\"object_id\":135,\"x\":39424,\"y\":-2304,\"z\":52736,\"room_num\":96},{\"object_id\":60,\"x\":37376,\"y\":-2048,\"z\":47616,\"room_num\":96},{\"object_id\":75,\"x\":37376,\"y\":-2048,\"z\":47616,\"room_num\":96},{\"object_id\":67,\"x\":37376,\"y\":-2304,\"z\":52736,\"room_num\":96},{\"object_id\":67,\"x\":42496,\"y\":-2304,\"z\":52736,\"room_num\":96},{\"object_id\":67,\"x\":43520,\"y\":-2304,\"z\":45568,\"room_num\":96},{\"object_id\":67,\"x\":43520,\"y\":-2304,\"z\":51712,\"room_num\":96},{\"object_id\":67,\"x\":43520,\"y\":-2944,\"z\":49664,\"room_num\":96},{\"object_id\":77,\"x\":37376,\"y\":-2048,\"z\":47616,\"room_num\":96},{\"object_id\":128,\"x\":37376,\"y\":-4352,\"z\":44544,\"room_num\":97},{\"object_id\":128,\"x\":42496,\"y\":-4352,\"z\":44544,\"room_num\":97},{\"object_id\":131,\"x\":37376,\"y\":-4096,\"z\":52736,\"room_num\":97},{\"object_id\":64,\"x\":43520,\"y\":-4864,\"z\":52736,\"room_num\":97},{\"object_id\":67,\"x\":43520,\"y\":-4352,\"z\":46592,\"room_num\":97},{\"object_id\":224,\"x\":43520,\"y\":-4352,\"z\":36352,\"room_num\":98},{\"object_id\":135,\"x\":48640,\"y\":-2048,\"z\":35328,\"room_num\":99},{\"object_id\":134,\"x\":48640,\"y\":-2048,\"z\":36352,\"room_num\":99},{\"object_id\":304,\"x\":46592,\"y\":-2432,\"z\":34304,\"room_num\":99},{\"object_id\":128,\"x\":40448,\"y\":-2048,\"z\":40448,\"room_num\":99},{\"object_id\":139,\"x\":43520,\"y\":-3328,\"z\":35328,\"room_num\":99},{\"object_id\":319,\"x\":44544,\"y\":-1792,\"z\":35328,\"room_num\":99},{\"object_id\":319,\"x\":43520,\"y\":-1792,\"z\":36352,\"room_num\":99},{\"object_id\":134,\"x\":46592,\"y\":-6656,\"z\":40448,\"room_num\":101},{\"object_id\":139,\"x\":41472,\"y\":-7936,\"z\":40448,\"room_num\":102},{\"object_id\":169,\"x\":41472,\"y\":-8192,\"z\":38400,\"room_num\":102},{\"object_id\":304,\"x\":41472,\"y\":-8832,\"z\":40448,\"room_num\":102},{\"object_id\":63,\"x\":40448,\"y\":-2048,\"z\":28160,\"room_num\":105},{\"object_id\":66,\"x\":42496,\"y\":-2304,\"z\":30208,\"room_num\":105},{\"object_id\":66,\"x\":45568,\"y\":-2304,\"z\":30208,\"room_num\":105},{\"object_id\":67,\"x\":48640,\"y\":-2432,\"z\":30208,\"room_num\":105},{\"object_id\":64,\"x\":50688,\"y\":-3584,\"z\":28160,\"room_num\":107},{\"object_id\":64,\"x\":50688,\"y\":-3584,\"z\":42496,\"room_num\":107},{\"object_id\":75,\"x\":37376,\"y\":-1280,\"z\":29184,\"room_num\":108},{\"object_id\":232,\"x\":20992,\"y\":5888,\"z\":64000,\"room_num\":109},{\"object_id\":351,\"x\":19968,\"y\":5888,\"z\":62976,\"room_num\":109},{\"object_id\":128,\"x\":19968,\"y\":5888,\"z\":62976,\"room_num\":109},{\"object_id\":131,\"x\":17920,\"y\":5888,\"z\":64000,\"room_num\":109},{\"object_id\":176,\"x\":34304,\"y\":-1024,\"z\":33280,\"room_num\":111},{\"object_id\":180,\"x\":31232,\"y\":-768,\"z\":28160,\"room_num\":111},{\"object_id\":319,\"x\":32256,\"y\":-2560,\"z\":30208,\"room_num\":112},{\"object_id\":319,\"x\":32256,\"y\":-2560,\"z\":32256,\"room_num\":112},{\"object_id\":319,\"x\":32256,\"y\":-1792,\"z\":30208,\"room_num\":112},{\"object_id\":319,\"x\":32256,\"y\":-1792,\"z\":32256,\"room_num\":112},{\"object_id\":215,\"x\":37376,\"y\":-1536,\"z\":32256,\"room_num\":113},{\"object_id\":131,\"x\":35328,\"y\":-1536,\"z\":28160,\"room_num\":113},{\"object_id\":135,\"x\":37376,\"y\":-2048,\"z\":36352,\"room_num\":114},{\"object_id\":131,\"x\":30208,\"y\":2816,\"z\":43520,\"room_num\":115},{\"object_id\":75,\"x\":51712,\"y\":-2304,\"z\":47616,\"room_num\":120},{\"object_id\":63,\"x\":49664,\"y\":-2304,\"z\":47616,\"room_num\":120},{\"object_id\":336,\"x\":44544,\"y\":-4864,\"z\":34304,\"room_num\":121},{\"object_id\":336,\"x\":43520,\"y\":-4864,\"z\":37376,\"room_num\":121},{\"object_id\":63,\"x\":29184,\"y\":-2304,\"z\":47616,\"room_num\":123},{\"object_id\":63,\"x\":33280,\"y\":-2304,\"z\":47616,\"room_num\":123},{\"object_id\":174,\"x\":44544,\"y\":-3328,\"z\":76288,\"room_num\":124},{\"object_id\":175,\"x\":43520,\"y\":-3328,\"z\":76288,\"room_num\":124},{\"object_id\":172,\"x\":43520,\"y\":-3328,\"z\":75264,\"room_num\":124},{\"object_id\":128,\"x\":44544,\"y\":-3328,\"z\":75264,\"room_num\":124},{\"object_id\":291,\"x\":26112,\"y\":-2304,\"z\":65024,\"room_num\":125},{\"object_id\":60,\"x\":22016,\"y\":-4864,\"z\":50688,\"room_num\":127},{\"object_id\":74,\"x\":22016,\"y\":-4864,\"z\":50688,\"room_num\":127},{\"object_id\":139,\"x\":20992,\"y\":-4864,\"z\":48640,\"room_num\":127},{\"object_id\":177,\"x\":61952,\"y\":1536,\"z\":55808,\"room_num\":128},{\"object_id\":178,\"x\":52736,\"y\":-1024,\"z\":64000,\"room_num\":131},{\"object_id\":177,\"x\":37376,\"y\":5632,\"z\":47616,\"room_num\":136},{\"object_id\":161,\"x\":38400,\"y\":6400,\"z\":47616,\"room_num\":136},{\"object_id\":25,\"x\":31232,\"y\":-2176,\"z\":44544,\"room_num\":138},{\"object_id\":25,\"x\":32256,\"y\":-2688,\"z\":47616,\"room_num\":138},{\"object_id\":180,\"x\":33280,\"y\":-768,\"z\":45568,\"room_num\":138},{\"object_id\":180,\"x\":43520,\"y\":3584,\"z\":61952,\"room_num\":140},{\"object_id\":161,\"x\":38400,\"y\":2816,\"z\":70144,\"room_num\":141},{\"object_id\":63,\"x\":38400,\"y\":7424,\"z\":55808,\"room_num\":144},{\"object_id\":304,\"x\":37376,\"y\":7424,\"z\":55808,\"room_num\":144},{\"object_id\":138,\"x\":37376,\"y\":7424,\"z\":55808,\"room_num\":144},{\"object_id\":74,\"x\":38400,\"y\":7424,\"z\":55808,\"room_num\":144},{\"object_id\":176,\"x\":38400,\"y\":7424,\"z\":55808,\"room_num\":144},{\"object_id\":304,\"x\":32256,\"y\":7424,\"z\":55808,\"room_num\":145},{\"object_id\":118,\"x\":30208,\"y\":7424,\"z\":55808,\"room_num\":145},{\"object_id\":74,\"x\":31232,\"y\":7424,\"z\":55808,\"room_num\":145},{\"object_id\":63,\"x\":31232,\"y\":7424,\"z\":55808,\"room_num\":145},{\"object_id\":63,\"x\":18944,\"y\":-7936,\"z\":47616,\"room_num\":150},{\"object_id\":74,\"x\":18944,\"y\":-7936,\"z\":47616,\"room_num\":150},{\"object_id\":173,\"x\":18944,\"y\":-7936,\"z\":47616,\"room_num\":150}]\n    },\n    {\n        \"title\": \"Antarctica\",\n        \"zone_num\": 5,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,13,15,24,31,39,41,74,75,76,77,78,79,94,117,128,129,130,131,132,133,134,135,136,139,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,209,210,213,214,217,218,224,228,232,244,245,246,247,300,301,304,309,310,311,315,319,330,335,349,350,355,366,367],\n        \"items\": [{\"object_id\":170,\"x\":29184,\"y\":128,\"z\":3584,\"room_num\":11},{\"object_id\":0,\"x\":32256,\"y\":-3712,\"z\":4608,\"room_num\":12},{\"object_id\":304,\"x\":26112,\"y\":-5120,\"z\":9728,\"room_num\":14},{\"object_id\":15,\"x\":26112,\"y\":-5888,\"z\":9728,\"room_num\":14},{\"object_id\":319,\"x\":61952,\"y\":-3456,\"z\":52736,\"room_num\":17},{\"object_id\":349,\"x\":41472,\"y\":-5888,\"z\":16896,\"room_num\":20},{\"object_id\":232,\"x\":39424,\"y\":-5120,\"z\":18944,\"room_num\":20},{\"object_id\":135,\"x\":40448,\"y\":-5120,\"z\":18944,\"room_num\":20},{\"object_id\":39,\"x\":39424,\"y\":-4608,\"z\":61952,\"room_num\":27},{\"object_id\":39,\"x\":39424,\"y\":-4608,\"z\":65024,\"room_num\":27},{\"object_id\":75,\"x\":39424,\"y\":-4608,\"z\":61952,\"room_num\":27},{\"object_id\":77,\"x\":39424,\"y\":-4608,\"z\":61952,\"room_num\":27},{\"object_id\":75,\"x\":42496,\"y\":-4608,\"z\":65024,\"room_num\":27},{\"object_id\":161,\"x\":39424,\"y\":-4608,\"z\":65024,\"room_num\":27},{\"object_id\":31,\"x\":60928,\"y\":-4608,\"z\":78336,\"room_num\":29},{\"object_id\":330,\"x\":57856,\"y\":-4864,\"z\":75264,\"room_num\":31},{\"object_id\":39,\"x\":43520,\"y\":-7168,\"z\":51712,\"room_num\":44},{\"object_id\":178,\"x\":57856,\"y\":-1280,\"z\":24064,\"room_num\":46},{\"object_id\":134,\"x\":69120,\"y\":-2816,\"z\":79360,\"room_num\":47},{\"object_id\":128,\"x\":69120,\"y\":-2816,\"z\":78336,\"room_num\":47},{\"object_id\":39,\"x\":26112,\"y\":-3584,\"z\":39424,\"room_num\":48},{\"object_id\":350,\"x\":27136,\"y\":-4352,\"z\":41472,\"room_num\":48},{\"object_id\":350,\"x\":27136,\"y\":-3840,\"z\":40448,\"room_num\":48},{\"object_id\":75,\"x\":26112,\"y\":-3584,\"z\":39424,\"room_num\":48},{\"object_id\":75,\"x\":25088,\"y\":-3584,\"z\":42496,\"room_num\":48},{\"object_id\":335,\"x\":27136,\"y\":-3968,\"z\":40448,\"room_num\":48},{\"object_id\":335,\"x\":27136,\"y\":-4736,\"z\":41472,\"room_num\":48},{\"object_id\":129,\"x\":28160,\"y\":-4352,\"z\":42496,\"room_num\":48},{\"object_id\":39,\"x\":27136,\"y\":-3072,\"z\":15872,\"room_num\":53},{\"object_id\":171,\"x\":27136,\"y\":-3072,\"z\":15872,\"room_num\":53},{\"object_id\":178,\"x\":44544,\"y\":-7424,\"z\":16896,\"room_num\":57},{\"object_id\":174,\"x\":43520,\"y\":-7424,\"z\":16896,\"room_num\":57},{\"object_id\":180,\"x\":42496,\"y\":-7296,\"z\":16896,\"room_num\":57},{\"object_id\":177,\"x\":44544,\"y\":-7296,\"z\":17920,\"room_num\":57},{\"object_id\":128,\"x\":26112,\"y\":-6144,\"z\":16896,\"room_num\":58},{\"object_id\":132,\"x\":26112,\"y\":-6144,\"z\":16896,\"room_num\":58},{\"object_id\":132,\"x\":25088,\"y\":-4864,\"z\":23040,\"room_num\":62},{\"object_id\":128,\"x\":27136,\"y\":-4864,\"z\":24064,\"room_num\":62},{\"object_id\":132,\"x\":26112,\"y\":-4864,\"z\":24064,\"room_num\":62},{\"object_id\":349,\"x\":70144,\"y\":-2816,\"z\":72192,\"room_num\":69},{\"object_id\":169,\"x\":47616,\"y\":-4224,\"z\":39424,\"room_num\":70},{\"object_id\":128,\"x\":73216,\"y\":-3072,\"z\":75264,\"room_num\":77},{\"object_id\":349,\"x\":72192,\"y\":-2816,\"z\":76288,\"room_num\":77},{\"object_id\":41,\"x\":73216,\"y\":-2816,\"z\":74240,\"room_num\":77},{\"object_id\":39,\"x\":71168,\"y\":-2944,\"z\":73216,\"room_num\":77},{\"object_id\":41,\"x\":70144,\"y\":-2816,\"z\":75264,\"room_num\":77},{\"object_id\":77,\"x\":70144,\"y\":-2816,\"z\":75264,\"room_num\":77},{\"object_id\":134,\"x\":71168,\"y\":-2816,\"z\":77312,\"room_num\":78},{\"object_id\":128,\"x\":70144,\"y\":-2816,\"z\":77312,\"room_num\":78},{\"object_id\":41,\"x\":71168,\"y\":-2688,\"z\":78336,\"room_num\":78},{\"object_id\":39,\"x\":42496,\"y\":-7168,\"z\":54784,\"room_num\":83},{\"object_id\":74,\"x\":42496,\"y\":-7168,\"z\":54784,\"room_num\":83},{\"object_id\":213,\"x\":68096,\"y\":-3840,\"z\":28160,\"room_num\":87},{\"object_id\":131,\"x\":68096,\"y\":-3840,\"z\":28160,\"room_num\":87},{\"object_id\":349,\"x\":67072,\"y\":-3328,\"z\":27136,\"room_num\":87},{\"object_id\":205,\"x\":68096,\"y\":-3840,\"z\":28160,\"room_num\":87},{\"object_id\":39,\"x\":66048,\"y\":-3584,\"z\":28160,\"room_num\":87},{\"object_id\":214,\"x\":68096,\"y\":-3840,\"z\":30208,\"room_num\":88},{\"object_id\":128,\"x\":67072,\"y\":-3840,\"z\":30208,\"room_num\":88},{\"object_id\":133,\"x\":64000,\"y\":-3072,\"z\":35328,\"room_num\":89},{\"object_id\":133,\"x\":64000,\"y\":-3072,\"z\":36352,\"room_num\":89},{\"object_id\":304,\"x\":64000,\"y\":-3712,\"z\":35328,\"room_num\":89},{\"object_id\":39,\"x\":68096,\"y\":-3584,\"z\":32256,\"room_num\":89},{\"object_id\":74,\"x\":68096,\"y\":-3584,\"z\":32256,\"room_num\":89},{\"object_id\":213,\"x\":54784,\"y\":-6144,\"z\":47616,\"room_num\":93},{\"object_id\":131,\"x\":54784,\"y\":-6144,\"z\":47616,\"room_num\":93},{\"object_id\":205,\"x\":54784,\"y\":-6144,\"z\":47616,\"room_num\":93},{\"object_id\":129,\"x\":53760,\"y\":-6144,\"z\":47616,\"room_num\":93},{\"object_id\":176,\"x\":60928,\"y\":-6400,\"z\":43520,\"room_num\":95},{\"object_id\":139,\"x\":54784,\"y\":-6144,\"z\":52736,\"room_num\":98},{\"object_id\":304,\"x\":54784,\"y\":-6144,\"z\":51712,\"room_num\":98},{\"object_id\":174,\"x\":69120,\"y\":-1152,\"z\":34304,\"room_num\":101},{\"object_id\":169,\"x\":52736,\"y\":-3072,\"z\":52736,\"room_num\":105},{\"object_id\":41,\"x\":54784,\"y\":-2816,\"z\":49664,\"room_num\":107},{\"object_id\":330,\"x\":56832,\"y\":-3200,\"z\":49664,\"room_num\":108},{\"object_id\":349,\"x\":58880,\"y\":-3072,\"z\":48640,\"room_num\":108},{\"object_id\":75,\"x\":58880,\"y\":-2816,\"z\":52736,\"room_num\":108},{\"object_id\":132,\"x\":26112,\"y\":-3584,\"z\":33280,\"room_num\":114},{\"object_id\":128,\"x\":27136,\"y\":-3584,\"z\":33280,\"room_num\":114},{\"object_id\":39,\"x\":28160,\"y\":-3584,\"z\":35328,\"room_num\":114},{\"object_id\":206,\"x\":78336,\"y\":-3584,\"z\":82432,\"room_num\":115},{\"object_id\":41,\"x\":76288,\"y\":-3072,\"z\":81408,\"room_num\":115},{\"object_id\":163,\"x\":78336,\"y\":-3072,\"z\":77312,\"room_num\":115},{\"object_id\":39,\"x\":51712,\"y\":-2816,\"z\":60928,\"room_num\":117},{\"object_id\":176,\"x\":51712,\"y\":-2816,\"z\":60928,\"room_num\":117},{\"object_id\":131,\"x\":55808,\"y\":-2816,\"z\":56832,\"room_num\":118},{\"object_id\":129,\"x\":55808,\"y\":-2816,\"z\":56832,\"room_num\":118},{\"object_id\":128,\"x\":56832,\"y\":-2816,\"z\":57856,\"room_num\":119},{\"object_id\":169,\"x\":58880,\"y\":-2816,\"z\":61952,\"room_num\":119},{\"object_id\":39,\"x\":24064,\"y\":-2304,\"z\":36352,\"room_num\":125},{\"object_id\":180,\"x\":20992,\"y\":-4096,\"z\":30208,\"room_num\":126},{\"object_id\":128,\"x\":24064,\"y\":-4864,\"z\":23040,\"room_num\":131},{\"object_id\":39,\"x\":74240,\"y\":-2688,\"z\":66048,\"room_num\":132},{\"object_id\":41,\"x\":45568,\"y\":-2816,\"z\":72192,\"room_num\":133},{\"object_id\":39,\"x\":44544,\"y\":-4608,\"z\":71168,\"room_num\":134},{\"object_id\":205,\"x\":43520,\"y\":-5120,\"z\":71168,\"room_num\":134},{\"object_id\":74,\"x\":44544,\"y\":-4608,\"z\":71168,\"room_num\":134},{\"object_id\":176,\"x\":44544,\"y\":-4608,\"z\":71168,\"room_num\":134},{\"object_id\":128,\"x\":26112,\"y\":-5120,\"z\":11776,\"room_num\":138},{\"object_id\":41,\"x\":69120,\"y\":768,\"z\":60928,\"room_num\":141},{\"object_id\":175,\"x\":68096,\"y\":768,\"z\":61952,\"room_num\":141},{\"object_id\":94,\"x\":42496,\"y\":-3072,\"z\":67072,\"room_num\":147},{\"object_id\":224,\"x\":52736,\"y\":-512,\"z\":40448,\"room_num\":151},{\"object_id\":174,\"x\":30208,\"y\":-7680,\"z\":1536,\"room_num\":153},{\"object_id\":139,\"x\":26112,\"y\":-3584,\"z\":42496,\"room_num\":154},{\"object_id\":39,\"x\":26112,\"y\":-4352,\"z\":17920,\"room_num\":159},{\"object_id\":170,\"x\":26112,\"y\":-4352,\"z\":17920,\"room_num\":159},{\"object_id\":39,\"x\":25088,\"y\":-4608,\"z\":25088,\"room_num\":160},{\"object_id\":176,\"x\":38400,\"y\":-4608,\"z\":66048,\"room_num\":161},{\"object_id\":165,\"x\":19968,\"y\":-4224,\"z\":29184,\"room_num\":163},{\"object_id\":175,\"x\":50688,\"y\":-6400,\"z\":25088,\"room_num\":164},{\"object_id\":177,\"x\":50688,\"y\":-6400,\"z\":25088,\"room_num\":164},{\"object_id\":176,\"x\":50688,\"y\":-6400,\"z\":25088,\"room_num\":164},{\"object_id\":39,\"x\":65024,\"y\":-3456,\"z\":42496,\"room_num\":166},{\"object_id\":75,\"x\":65024,\"y\":-3456,\"z\":42496,\"room_num\":166},{\"object_id\":39,\"x\":58880,\"y\":-2816,\"z\":53760,\"room_num\":170},{\"object_id\":41,\"x\":59904,\"y\":-2816,\"z\":53760,\"room_num\":170},{\"object_id\":170,\"x\":58880,\"y\":-2816,\"z\":53760,\"room_num\":170},{\"object_id\":350,\"x\":46592,\"y\":-4864,\"z\":76288,\"room_num\":172},{\"object_id\":350,\"x\":47616,\"y\":-4864,\"z\":74240,\"room_num\":172},{\"object_id\":335,\"x\":46592,\"y\":-5376,\"z\":76288,\"room_num\":172},{\"object_id\":335,\"x\":47616,\"y\":-5376,\"z\":74240,\"room_num\":172},{\"object_id\":131,\"x\":48640,\"y\":-4864,\"z\":74240,\"room_num\":172},{\"object_id\":319,\"x\":47616,\"y\":-5248,\"z\":77312,\"room_num\":172},{\"object_id\":129,\"x\":46592,\"y\":-4864,\"z\":75264,\"room_num\":172},{\"object_id\":177,\"x\":46592,\"y\":-4864,\"z\":77312,\"room_num\":172},{\"object_id\":117,\"x\":50688,\"y\":-2816,\"z\":66048,\"room_num\":175},{\"object_id\":117,\"x\":50688,\"y\":-2816,\"z\":68096,\"room_num\":175},{\"object_id\":136,\"x\":50688,\"y\":-2816,\"z\":70144,\"room_num\":175},{\"object_id\":136,\"x\":50688,\"y\":-2816,\"z\":74240,\"room_num\":175},{\"object_id\":136,\"x\":50688,\"y\":-2816,\"z\":68096,\"room_num\":175},{\"object_id\":136,\"x\":50688,\"y\":-2816,\"z\":66048,\"room_num\":175},{\"object_id\":117,\"x\":50688,\"y\":-2816,\"z\":70144,\"room_num\":175},{\"object_id\":117,\"x\":50688,\"y\":-2816,\"z\":74240,\"room_num\":175},{\"object_id\":180,\"x\":50688,\"y\":-2688,\"z\":71168,\"room_num\":175},{\"object_id\":131,\"x\":74240,\"y\":-3072,\"z\":74240,\"room_num\":178},{\"object_id\":349,\"x\":47616,\"y\":-4352,\"z\":38400,\"room_num\":179},{\"object_id\":171,\"x\":48640,\"y\":-3584,\"z\":37376,\"room_num\":179},{\"object_id\":349,\"x\":44544,\"y\":-2816,\"z\":69120,\"room_num\":187},{\"object_id\":41,\"x\":46592,\"y\":-2944,\"z\":69120,\"room_num\":187},{\"object_id\":39,\"x\":43520,\"y\":-2816,\"z\":62976,\"room_num\":187},{\"object_id\":76,\"x\":43520,\"y\":-2816,\"z\":62976,\"room_num\":187},{\"object_id\":79,\"x\":45568,\"y\":-2816,\"z\":69120,\"room_num\":187},{\"object_id\":133,\"x\":39424,\"y\":-4608,\"z\":66048,\"room_num\":191},{\"object_id\":128,\"x\":40448,\"y\":-4608,\"z\":69120,\"room_num\":191},{\"object_id\":94,\"x\":39424,\"y\":-3072,\"z\":67072,\"room_num\":195},{\"object_id\":180,\"x\":39424,\"y\":-5120,\"z\":20992,\"room_num\":201},{\"object_id\":178,\"x\":39424,\"y\":-5120,\"z\":20992,\"room_num\":201},{\"object_id\":171,\"x\":39424,\"y\":-5120,\"z\":20992,\"room_num\":201},{\"object_id\":171,\"x\":39424,\"y\":-5120,\"z\":20992,\"room_num\":201},{\"object_id\":41,\"x\":40448,\"y\":-5120,\"z\":20992,\"room_num\":201},{\"object_id\":39,\"x\":47616,\"y\":-3712,\"z\":35328,\"room_num\":203}]\n    },\n    {\n        \"title\": \"RX-Tech Mines\",\n        \"zone_num\": 5,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,11,17,24,42,46,50,74,75,76,77,78,79,80,81,82,86,121,128,131,132,133,142,143,144,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,209,210,211,213,214,215,217,218,219,300,301,304,309,310,311,315,318,319,321,322,324,327,328,334,335,336,349,350,366,367],\n        \"items\": [{\"object_id\":144,\"x\":13824,\"y\":-512,\"z\":17920,\"room_num\":1},{\"object_id\":144,\"x\":17920,\"y\":-512,\"z\":17920,\"room_num\":1},{\"object_id\":128,\"x\":11776,\"y\":0,\"z\":15872,\"room_num\":1},{\"object_id\":50,\"x\":19968,\"y\":-512,\"z\":25088,\"room_num\":1},{\"object_id\":180,\"x\":36352,\"y\":4864,\"z\":41472,\"room_num\":2},{\"object_id\":173,\"x\":36352,\"y\":4864,\"z\":42496,\"room_num\":2},{\"object_id\":170,\"x\":36352,\"y\":4864,\"z\":42496,\"room_num\":2},{\"object_id\":42,\"x\":34304,\"y\":6272,\"z\":46592,\"room_num\":2},{\"object_id\":133,\"x\":10752,\"y\":-256,\"z\":16896,\"room_num\":3},{\"object_id\":318,\"x\":7680,\"y\":-512,\"z\":16896,\"room_num\":3},{\"object_id\":42,\"x\":6656,\"y\":0,\"z\":13824,\"room_num\":4},{\"object_id\":42,\"x\":6656,\"y\":256,\"z\":24064,\"room_num\":4},{\"object_id\":143,\"x\":16896,\"y\":0,\"z\":17920,\"room_num\":5},{\"object_id\":142,\"x\":15872,\"y\":256,\"z\":17920,\"room_num\":5},{\"object_id\":175,\"x\":12800,\"y\":2816,\"z\":20992,\"room_num\":5},{\"object_id\":143,\"x\":14848,\"y\":0,\"z\":17920,\"room_num\":5},{\"object_id\":336,\"x\":9728,\"y\":-2688,\"z\":24064,\"room_num\":6},{\"object_id\":336,\"x\":8704,\"y\":-2688,\"z\":25088,\"room_num\":6},{\"object_id\":336,\"x\":14848,\"y\":-2176,\"z\":29184,\"room_num\":6},{\"object_id\":176,\"x\":6656,\"y\":-1792,\"z\":16896,\"room_num\":6},{\"object_id\":336,\"x\":30208,\"y\":-4352,\"z\":25088,\"room_num\":9},{\"object_id\":42,\"x\":29184,\"y\":-2688,\"z\":25088,\"room_num\":9},{\"object_id\":142,\"x\":30208,\"y\":7936,\"z\":55808,\"room_num\":11},{\"object_id\":180,\"x\":33280,\"y\":7936,\"z\":57856,\"room_num\":11},{\"object_id\":86,\"x\":53760,\"y\":7808,\"z\":67072,\"room_num\":14},{\"object_id\":86,\"x\":53760,\"y\":7424,\"z\":67072,\"room_num\":14},{\"object_id\":42,\"x\":53760,\"y\":7936,\"z\":68096,\"room_num\":14},{\"object_id\":177,\"x\":22016,\"y\":6528,\"z\":32256,\"room_num\":21},{\"object_id\":304,\"x\":27136,\"y\":-8064,\"z\":30208,\"room_num\":32},{\"object_id\":214,\"x\":20992,\"y\":-7936,\"z\":33280,\"room_num\":32},{\"object_id\":215,\"x\":22016,\"y\":-8704,\"z\":34304,\"room_num\":32},{\"object_id\":336,\"x\":25088,\"y\":-10368,\"z\":35328,\"room_num\":32},{\"object_id\":178,\"x\":17920,\"y\":-8960,\"z\":27136,\"room_num\":32},{\"object_id\":176,\"x\":26112,\"y\":3328,\"z\":25088,\"room_num\":34},{\"object_id\":133,\"x\":25088,\"y\":15360,\"z\":30208,\"room_num\":35},{\"object_id\":180,\"x\":26112,\"y\":5888,\"z\":26112,\"room_num\":37},{\"object_id\":0,\"x\":59904,\"y\":1024,\"z\":25088,\"room_num\":40},{\"object_id\":50,\"x\":54784,\"y\":7936,\"z\":70144,\"room_num\":45},{\"object_id\":131,\"x\":58880,\"y\":-512,\"z\":20992,\"room_num\":46},{\"object_id\":131,\"x\":62976,\"y\":-512,\"z\":27136,\"room_num\":50},{\"object_id\":131,\"x\":59904,\"y\":-512,\"z\":28160,\"room_num\":51},{\"object_id\":350,\"x\":51712,\"y\":4608,\"z\":65024,\"room_num\":60},{\"object_id\":175,\"x\":46592,\"y\":3840,\"z\":50688,\"room_num\":64},{\"object_id\":176,\"x\":46592,\"y\":3840,\"z\":50688,\"room_num\":64},{\"object_id\":304,\"x\":46592,\"y\":3712,\"z\":49664,\"room_num\":64},{\"object_id\":128,\"x\":59904,\"y\":6144,\"z\":33280,\"room_num\":65},{\"object_id\":173,\"x\":60928,\"y\":6144,\"z\":33280,\"room_num\":65},{\"object_id\":42,\"x\":59904,\"y\":6272,\"z\":35328,\"room_num\":67},{\"object_id\":42,\"x\":60928,\"y\":6272,\"z\":35328,\"room_num\":67},{\"object_id\":144,\"x\":58880,\"y\":6656,\"z\":44544,\"room_num\":69},{\"object_id\":144,\"x\":58880,\"y\":6144,\"z\":43520,\"room_num\":69},{\"object_id\":144,\"x\":58880,\"y\":7168,\"z\":46592,\"room_num\":69},{\"object_id\":144,\"x\":57856,\"y\":7680,\"z\":47616,\"room_num\":69},{\"object_id\":144,\"x\":56832,\"y\":8192,\"z\":47616,\"room_num\":69},{\"object_id\":17,\"x\":51712,\"y\":7936,\"z\":46592,\"room_num\":69},{\"object_id\":42,\"x\":50688,\"y\":9472,\"z\":46592,\"room_num\":69},{\"object_id\":144,\"x\":57856,\"y\":4096,\"z\":45568,\"room_num\":70},{\"object_id\":144,\"x\":57856,\"y\":4608,\"z\":44544,\"room_num\":70},{\"object_id\":144,\"x\":57856,\"y\":5120,\"z\":43520,\"room_num\":70},{\"object_id\":143,\"x\":57856,\"y\":5632,\"z\":42496,\"room_num\":70},{\"object_id\":213,\"x\":48640,\"y\":5888,\"z\":41472,\"room_num\":70},{\"object_id\":336,\"x\":49664,\"y\":4224,\"z\":43520,\"room_num\":70},{\"object_id\":17,\"x\":54784,\"y\":5888,\"z\":45568,\"room_num\":70},{\"object_id\":322,\"x\":54784,\"y\":5248,\"z\":44544,\"room_num\":70},{\"object_id\":322,\"x\":55808,\"y\":5504,\"z\":49664,\"room_num\":70},{\"object_id\":336,\"x\":55808,\"y\":3968,\"z\":46592,\"room_num\":70},{\"object_id\":322,\"x\":50688,\"y\":5376,\"z\":43520,\"room_num\":70},{\"object_id\":128,\"x\":52736,\"y\":3840,\"z\":49664,\"room_num\":72},{\"object_id\":319,\"x\":51712,\"y\":3328,\"z\":45568,\"room_num\":72},{\"object_id\":319,\"x\":51712,\"y\":3328,\"z\":50688,\"room_num\":72},{\"object_id\":304,\"x\":51712,\"y\":2944,\"z\":46592,\"room_num\":72},{\"object_id\":42,\"x\":52736,\"y\":3840,\"z\":51712,\"room_num\":72},{\"object_id\":17,\"x\":54784,\"y\":3584,\"z\":47616,\"room_num\":74},{\"object_id\":178,\"x\":54784,\"y\":7936,\"z\":58880,\"room_num\":77},{\"object_id\":50,\"x\":55808,\"y\":5760,\"z\":39424,\"room_num\":79},{\"object_id\":180,\"x\":52736,\"y\":7936,\"z\":70144,\"room_num\":80},{\"object_id\":350,\"x\":52736,\"y\":7936,\"z\":73216,\"room_num\":80},{\"object_id\":322,\"x\":5632,\"y\":13440,\"z\":57856,\"room_num\":87},{\"object_id\":322,\"x\":8704,\"y\":13312,\"z\":58880,\"room_num\":87},{\"object_id\":322,\"x\":14848,\"y\":13440,\"z\":58880,\"room_num\":87},{\"object_id\":322,\"x\":11776,\"y\":13440,\"z\":57856,\"room_num\":87},{\"object_id\":322,\"x\":17920,\"y\":13440,\"z\":57856,\"room_num\":87},{\"object_id\":336,\"x\":10752,\"y\":14848,\"z\":57856,\"room_num\":87},{\"object_id\":336,\"x\":7680,\"y\":14848,\"z\":58880,\"room_num\":87},{\"object_id\":336,\"x\":5632,\"y\":14848,\"z\":57856,\"room_num\":87},{\"object_id\":336,\"x\":13824,\"y\":14848,\"z\":58880,\"room_num\":87},{\"object_id\":336,\"x\":16896,\"y\":14848,\"z\":57856,\"room_num\":87},{\"object_id\":336,\"x\":18944,\"y\":14848,\"z\":58880,\"room_num\":87},{\"object_id\":46,\"x\":18944,\"y\":14848,\"z\":57856,\"room_num\":87},{\"object_id\":75,\"x\":18944,\"y\":14848,\"z\":57856,\"room_num\":87},{\"object_id\":77,\"x\":18944,\"y\":14848,\"z\":57856,\"room_num\":87},{\"object_id\":322,\"x\":20992,\"y\":13312,\"z\":58880,\"room_num\":89},{\"object_id\":322,\"x\":13824,\"y\":14720,\"z\":61952,\"room_num\":90},{\"object_id\":322,\"x\":17920,\"y\":14720,\"z\":61952,\"room_num\":90},{\"object_id\":75,\"x\":16896,\"y\":15616,\"z\":61952,\"room_num\":90},{\"object_id\":46,\"x\":7680,\"y\":14976,\"z\":60928,\"room_num\":91},{\"object_id\":322,\"x\":7680,\"y\":13952,\"z\":59904,\"room_num\":91},{\"object_id\":322,\"x\":22016,\"y\":13952,\"z\":61952,\"room_num\":92},{\"object_id\":322,\"x\":7680,\"y\":14720,\"z\":64000,\"room_num\":93},{\"object_id\":322,\"x\":10752,\"y\":14720,\"z\":64000,\"room_num\":93},{\"object_id\":336,\"x\":70144,\"y\":3072,\"z\":66048,\"room_num\":95},{\"object_id\":132,\"x\":47616,\"y\":5888,\"z\":41472,\"room_num\":106},{\"object_id\":206,\"x\":46592,\"y\":5888,\"z\":41472,\"room_num\":106},{\"object_id\":322,\"x\":43520,\"y\":9472,\"z\":43520,\"room_num\":110},{\"object_id\":322,\"x\":43520,\"y\":7936,\"z\":41472,\"room_num\":110},{\"object_id\":322,\"x\":43520,\"y\":6400,\"z\":39424,\"room_num\":110},{\"object_id\":349,\"x\":7680,\"y\":-2432,\"z\":94720,\"room_num\":113},{\"object_id\":304,\"x\":6656,\"y\":-1280,\"z\":95744,\"room_num\":113},{\"object_id\":133,\"x\":25088,\"y\":7424,\"z\":55808,\"room_num\":116},{\"object_id\":128,\"x\":26112,\"y\":7424,\"z\":55808,\"room_num\":116},{\"object_id\":133,\"x\":23040,\"y\":6912,\"z\":59904,\"room_num\":117},{\"object_id\":133,\"x\":23040,\"y\":7680,\"z\":52736,\"room_num\":117},{\"object_id\":128,\"x\":23040,\"y\":7680,\"z\":53760,\"room_num\":117},{\"object_id\":132,\"x\":23040,\"y\":7424,\"z\":55808,\"room_num\":117},{\"object_id\":128,\"x\":23040,\"y\":7424,\"z\":56832,\"room_num\":117},{\"object_id\":133,\"x\":18944,\"y\":6912,\"z\":60928,\"room_num\":118},{\"object_id\":128,\"x\":22016,\"y\":6912,\"z\":60928,\"room_num\":118},{\"object_id\":46,\"x\":20992,\"y\":6912,\"z\":60928,\"room_num\":118},{\"object_id\":133,\"x\":16896,\"y\":7424,\"z\":66048,\"room_num\":122},{\"object_id\":128,\"x\":17920,\"y\":7424,\"z\":66048,\"room_num\":122},{\"object_id\":128,\"x\":17920,\"y\":6912,\"z\":59904,\"room_num\":122},{\"object_id\":162,\"x\":15872,\"y\":7424,\"z\":66048,\"room_num\":122},{\"object_id\":336,\"x\":12800,\"y\":6016,\"z\":61952,\"room_num\":124},{\"object_id\":46,\"x\":12800,\"y\":7424,\"z\":62976,\"room_num\":124},{\"object_id\":75,\"x\":12800,\"y\":7424,\"z\":62976,\"room_num\":124},{\"object_id\":77,\"x\":12800,\"y\":7424,\"z\":62976,\"room_num\":124},{\"object_id\":176,\"x\":12800,\"y\":7424,\"z\":62976,\"room_num\":124},{\"object_id\":171,\"x\":12800,\"y\":7424,\"z\":62976,\"room_num\":124},{\"object_id\":75,\"x\":16896,\"y\":7424,\"z\":68096,\"room_num\":125},{\"object_id\":128,\"x\":16896,\"y\":7424,\"z\":67072,\"room_num\":125},{\"object_id\":207,\"x\":11776,\"y\":11776,\"z\":65024,\"room_num\":126},{\"object_id\":350,\"x\":43520,\"y\":5376,\"z\":35328,\"room_num\":127},{\"object_id\":143,\"x\":15872,\"y\":8192,\"z\":50688,\"room_num\":130},{\"object_id\":144,\"x\":16896,\"y\":7680,\"z\":50688,\"room_num\":130},{\"object_id\":144,\"x\":17920,\"y\":7168,\"z\":50688,\"room_num\":130},{\"object_id\":322,\"x\":37376,\"y\":-1024,\"z\":30208,\"room_num\":132},{\"object_id\":180,\"x\":47616,\"y\":5376,\"z\":27136,\"room_num\":133},{\"object_id\":322,\"x\":54784,\"y\":-1408,\"z\":58880,\"room_num\":135},{\"object_id\":133,\"x\":28160,\"y\":-7936,\"z\":30208,\"room_num\":136},{\"object_id\":86,\"x\":33280,\"y\":-3712,\"z\":30208,\"room_num\":136},{\"object_id\":86,\"x\":30208,\"y\":-6528,\"z\":30208,\"room_num\":136},{\"object_id\":322,\"x\":6656,\"y\":-8320,\"z\":43520,\"room_num\":139},{\"object_id\":322,\"x\":46592,\"y\":-4992,\"z\":78336,\"room_num\":142},{\"object_id\":350,\"x\":11776,\"y\":-6784,\"z\":54784,\"room_num\":143},{\"object_id\":336,\"x\":55808,\"y\":7936,\"z\":70144,\"room_num\":146},{\"object_id\":336,\"x\":35328,\"y\":-8448,\"z\":72192,\"room_num\":147},{\"object_id\":322,\"x\":55808,\"y\":16384,\"z\":74240,\"room_num\":151},{\"object_id\":46,\"x\":56832,\"y\":17152,\"z\":74240,\"room_num\":151},{\"object_id\":42,\"x\":53760,\"y\":24576,\"z\":62976,\"room_num\":153},{\"object_id\":169,\"x\":12800,\"y\":15360,\"z\":57856,\"room_num\":159},{\"object_id\":174,\"x\":10752,\"y\":15360,\"z\":58880,\"room_num\":159},{\"object_id\":42,\"x\":67072,\"y\":26496,\"z\":56832,\"room_num\":160},{\"object_id\":42,\"x\":70144,\"y\":26624,\"z\":67072,\"room_num\":160},{\"object_id\":42,\"x\":62976,\"y\":26624,\"z\":55808,\"room_num\":160},{\"object_id\":50,\"x\":64000,\"y\":26624,\"z\":66048,\"room_num\":160},{\"object_id\":336,\"x\":66048,\"y\":23808,\"z\":65024,\"room_num\":160},{\"object_id\":142,\"x\":65024,\"y\":25216,\"z\":64000,\"room_num\":160},{\"object_id\":142,\"x\":65024,\"y\":25216,\"z\":62976,\"room_num\":160},{\"object_id\":142,\"x\":65024,\"y\":25216,\"z\":61952,\"room_num\":160},{\"object_id\":205,\"x\":65024,\"y\":25216,\"z\":61952,\"room_num\":160},{\"object_id\":86,\"x\":75264,\"y\":20992,\"z\":64000,\"room_num\":163},{\"object_id\":86,\"x\":75264,\"y\":20480,\"z\":65024,\"room_num\":163},{\"object_id\":86,\"x\":75264,\"y\":19968,\"z\":66048,\"room_num\":163},{\"object_id\":86,\"x\":75264,\"y\":19456,\"z\":67072,\"room_num\":163},{\"object_id\":42,\"x\":57856,\"y\":26624,\"z\":66048,\"room_num\":164},{\"object_id\":121,\"x\":73216,\"y\":17920,\"z\":70144,\"room_num\":165},{\"object_id\":335,\"x\":73216,\"y\":16384,\"z\":70144,\"room_num\":165},{\"object_id\":121,\"x\":70144,\"y\":14848,\"z\":71168,\"room_num\":166},{\"object_id\":335,\"x\":70144,\"y\":13312,\"z\":71168,\"room_num\":166},{\"object_id\":121,\"x\":67072,\"y\":13056,\"z\":72192,\"room_num\":166},{\"object_id\":335,\"x\":67072,\"y\":11520,\"z\":72192,\"room_num\":166},{\"object_id\":42,\"x\":70144,\"y\":2560,\"z\":67072,\"room_num\":167},{\"object_id\":322,\"x\":66048,\"y\":6400,\"z\":70144,\"room_num\":168},{\"object_id\":42,\"x\":54784,\"y\":29696,\"z\":72192,\"room_num\":170},{\"object_id\":177,\"x\":15872,\"y\":4096,\"z\":31232,\"room_num\":173},{\"object_id\":175,\"x\":15872,\"y\":3840,\"z\":30208,\"room_num\":173},{\"object_id\":170,\"x\":15872,\"y\":3840,\"z\":30208,\"room_num\":173},{\"object_id\":133,\"x\":58880,\"y\":5376,\"z\":34304,\"room_num\":174},{\"object_id\":350,\"x\":35328,\"y\":-6912,\"z\":74240,\"room_num\":176},{\"object_id\":177,\"x\":23040,\"y\":15872,\"z\":27136,\"room_num\":181},{\"object_id\":350,\"x\":98816,\"y\":5120,\"z\":61952,\"room_num\":184},{\"object_id\":133,\"x\":45568,\"y\":3840,\"z\":49664,\"room_num\":186},{\"object_id\":170,\"x\":10752,\"y\":10624,\"z\":62976,\"room_num\":187},{\"object_id\":178,\"x\":10752,\"y\":10624,\"z\":62976,\"room_num\":187},{\"object_id\":174,\"x\":61952,\"y\":28672,\"z\":58880,\"room_num\":188},{\"object_id\":128,\"x\":67072,\"y\":24320,\"z\":50688,\"room_num\":192}]\n    },\n    {\n        \"title\": \"Lost City of Tinnos\",\n        \"zone_num\": 5,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,24,44,45,47,74,75,76,77,78,79,80,81,82,86,88,97,107,119,121,128,129,130,131,132,133,134,135,136,137,139,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,209,213,217,224,228,232,282,300,301,304,309,310,311,315,318,321,326,330,331,332,333,349,365,366,367,370],\n        \"items\": [{\"object_id\":137,\"x\":94720,\"y\":-256,\"z\":69120,\"room_num\":4},{\"object_id\":131,\"x\":88576,\"y\":0,\"z\":78336,\"room_num\":8},{\"object_id\":132,\"x\":87552,\"y\":0,\"z\":78336,\"room_num\":8},{\"object_id\":129,\"x\":91648,\"y\":-256,\"z\":79360,\"room_num\":8},{\"object_id\":318,\"x\":97792,\"y\":-768,\"z\":69120,\"room_num\":9},{\"object_id\":0,\"x\":98816,\"y\":0,\"z\":69120,\"room_num\":9},{\"object_id\":129,\"x\":86528,\"y\":-3840,\"z\":64000,\"room_num\":11},{\"object_id\":131,\"x\":85504,\"y\":-2560,\"z\":61952,\"room_num\":13},{\"object_id\":232,\"x\":87552,\"y\":0,\"z\":75264,\"room_num\":15},{\"object_id\":180,\"x\":94720,\"y\":-2816,\"z\":66048,\"room_num\":17},{\"object_id\":166,\"x\":94720,\"y\":-2816,\"z\":66048,\"room_num\":17},{\"object_id\":224,\"x\":81408,\"y\":-2560,\"z\":62976,\"room_num\":27},{\"object_id\":133,\"x\":86528,\"y\":-512,\"z\":76288,\"room_num\":28},{\"object_id\":133,\"x\":86528,\"y\":-512,\"z\":76288,\"room_num\":28},{\"object_id\":129,\"x\":85504,\"y\":-3840,\"z\":72192,\"room_num\":30},{\"object_id\":133,\"x\":85504,\"y\":-3840,\"z\":72192,\"room_num\":30},{\"object_id\":304,\"x\":92672,\"y\":-1280,\"z\":54784,\"room_num\":31},{\"object_id\":128,\"x\":90624,\"y\":-2304,\"z\":54784,\"room_num\":32},{\"object_id\":128,\"x\":90624,\"y\":-2304,\"z\":55808,\"room_num\":32},{\"object_id\":128,\"x\":90624,\"y\":-2304,\"z\":56832,\"room_num\":32},{\"object_id\":129,\"x\":87552,\"y\":-2816,\"z\":76288,\"room_num\":34},{\"object_id\":331,\"x\":10752,\"y\":-4224,\"z\":36352,\"room_num\":35},{\"object_id\":134,\"x\":88576,\"y\":0,\"z\":54784,\"room_num\":36},{\"object_id\":128,\"x\":90624,\"y\":-2304,\"z\":53760,\"room_num\":39},{\"object_id\":128,\"x\":90624,\"y\":-2304,\"z\":52736,\"room_num\":39},{\"object_id\":88,\"x\":8704,\"y\":-7552,\"z\":47616,\"room_num\":42},{\"object_id\":44,\"x\":79360,\"y\":-3968,\"z\":60928,\"room_num\":46},{\"object_id\":44,\"x\":79360,\"y\":-3968,\"z\":59904,\"room_num\":46},{\"object_id\":44,\"x\":81408,\"y\":-3968,\"z\":60928,\"room_num\":46},{\"object_id\":77,\"x\":79360,\"y\":-3968,\"z\":60928,\"room_num\":46},{\"object_id\":77,\"x\":79360,\"y\":-3968,\"z\":59904,\"room_num\":46},{\"object_id\":77,\"x\":81408,\"y\":-3968,\"z\":60928,\"room_num\":46},{\"object_id\":170,\"x\":81408,\"y\":-3968,\"z\":59904,\"room_num\":46},{\"object_id\":170,\"x\":81408,\"y\":-3968,\"z\":59904,\"room_num\":46},{\"object_id\":176,\"x\":80384,\"y\":-3968,\"z\":58880,\"room_num\":46},{\"object_id\":176,\"x\":80384,\"y\":-3968,\"z\":58880,\"room_num\":46},{\"object_id\":167,\"x\":81408,\"y\":-3968,\"z\":59904,\"room_num\":46},{\"object_id\":47,\"x\":80384,\"y\":-3840,\"z\":59904,\"room_num\":46},{\"object_id\":180,\"x\":76288,\"y\":-768,\"z\":43520,\"room_num\":47},{\"object_id\":176,\"x\":67072,\"y\":0,\"z\":47616,\"room_num\":51},{\"object_id\":304,\"x\":48640,\"y\":5888,\"z\":47616,\"room_num\":54},{\"object_id\":45,\"x\":47616,\"y\":-1280,\"z\":55808,\"room_num\":56},{\"object_id\":45,\"x\":47616,\"y\":-1280,\"z\":50688,\"room_num\":56},{\"object_id\":97,\"x\":36352,\"y\":-6400,\"z\":52736,\"room_num\":58},{\"object_id\":86,\"x\":32256,\"y\":-4096,\"z\":51712,\"room_num\":59},{\"object_id\":304,\"x\":37376,\"y\":-6400,\"z\":51712,\"room_num\":60},{\"object_id\":86,\"x\":42496,\"y\":-1280,\"z\":51712,\"room_num\":62},{\"object_id\":86,\"x\":44544,\"y\":-1280,\"z\":52736,\"room_num\":62},{\"object_id\":47,\"x\":27136,\"y\":1152,\"z\":47616,\"room_num\":64},{\"object_id\":135,\"x\":25088,\"y\":0,\"z\":65024,\"room_num\":67},{\"object_id\":135,\"x\":23040,\"y\":0,\"z\":67072,\"room_num\":67},{\"object_id\":135,\"x\":20992,\"y\":0,\"z\":65024,\"room_num\":67},{\"object_id\":135,\"x\":23040,\"y\":0,\"z\":62976,\"room_num\":67},{\"object_id\":213,\"x\":23040,\"y\":768,\"z\":64000,\"room_num\":67},{\"object_id\":213,\"x\":22016,\"y\":768,\"z\":65024,\"room_num\":67},{\"object_id\":213,\"x\":23040,\"y\":768,\"z\":66048,\"room_num\":67},{\"object_id\":213,\"x\":24064,\"y\":768,\"z\":65024,\"room_num\":67},{\"object_id\":370,\"x\":23040,\"y\":-640,\"z\":65024,\"room_num\":67},{\"object_id\":133,\"x\":17920,\"y\":0,\"z\":65024,\"room_num\":68},{\"object_id\":133,\"x\":33280,\"y\":-3328,\"z\":65024,\"room_num\":75},{\"object_id\":133,\"x\":23040,\"y\":-3328,\"z\":68096,\"room_num\":75},{\"object_id\":133,\"x\":17920,\"y\":-3328,\"z\":65024,\"room_num\":75},{\"object_id\":133,\"x\":23040,\"y\":-3328,\"z\":61952,\"room_num\":75},{\"object_id\":370,\"x\":23040,\"y\":-3584,\"z\":65024,\"room_num\":75},{\"object_id\":175,\"x\":17920,\"y\":-3328,\"z\":38400,\"room_num\":81},{\"object_id\":176,\"x\":10752,\"y\":-3328,\"z\":42496,\"room_num\":87},{\"object_id\":331,\"x\":28160,\"y\":-4224,\"z\":55808,\"room_num\":89},{\"object_id\":331,\"x\":20992,\"y\":-4224,\"z\":53760,\"room_num\":90},{\"object_id\":331,\"x\":22016,\"y\":-4224,\"z\":38400,\"room_num\":90},{\"object_id\":331,\"x\":14848,\"y\":-4224,\"z\":50688,\"room_num\":91},{\"object_id\":331,\"x\":17920,\"y\":-4224,\"z\":41472,\"room_num\":91},{\"object_id\":169,\"x\":19968,\"y\":-3328,\"z\":38400,\"room_num\":92},{\"object_id\":331,\"x\":14848,\"y\":-4352,\"z\":52736,\"room_num\":93},{\"object_id\":205,\"x\":35328,\"y\":-2560,\"z\":89600,\"room_num\":94},{\"object_id\":88,\"x\":7680,\"y\":-11264,\"z\":52736,\"room_num\":97},{\"object_id\":88,\"x\":9728,\"y\":-11264,\"z\":52736,\"room_num\":97},{\"object_id\":205,\"x\":8704,\"y\":-12032,\"z\":54784,\"room_num\":97},{\"object_id\":136,\"x\":9728,\"y\":-11520,\"z\":54784,\"room_num\":97},{\"object_id\":88,\"x\":9728,\"y\":-11520,\"z\":53760,\"room_num\":97},{\"object_id\":88,\"x\":7680,\"y\":-11520,\"z\":53760,\"room_num\":97},{\"object_id\":139,\"x\":41472,\"y\":-3072,\"z\":30208,\"room_num\":101},{\"object_id\":304,\"x\":8704,\"y\":-5248,\"z\":66048,\"room_num\":102},{\"object_id\":331,\"x\":7680,\"y\":-3712,\"z\":61952,\"room_num\":102},{\"object_id\":331,\"x\":1536,\"y\":-4096,\"z\":66048,\"room_num\":103},{\"object_id\":332,\"x\":4608,\"y\":-2816,\"z\":67072,\"room_num\":104},{\"object_id\":332,\"x\":5632,\"y\":-2816,\"z\":65024,\"room_num\":104},{\"object_id\":332,\"x\":3584,\"y\":-2304,\"z\":64000,\"room_num\":104},{\"object_id\":332,\"x\":2560,\"y\":-1792,\"z\":61952,\"room_num\":104},{\"object_id\":332,\"x\":2560,\"y\":-1792,\"z\":66048,\"room_num\":104},{\"object_id\":332,\"x\":1536,\"y\":-2816,\"z\":70144,\"room_num\":104},{\"object_id\":332,\"x\":5632,\"y\":-1792,\"z\":71168,\"room_num\":104},{\"object_id\":332,\"x\":3584,\"y\":-2560,\"z\":69120,\"room_num\":104},{\"object_id\":177,\"x\":3584,\"y\":-2560,\"z\":69120,\"room_num\":104},{\"object_id\":169,\"x\":3584,\"y\":-2560,\"z\":69120,\"room_num\":104},{\"object_id\":129,\"x\":37376,\"y\":-2048,\"z\":89600,\"room_num\":109},{\"object_id\":180,\"x\":37376,\"y\":-2304,\"z\":89600,\"room_num\":109},{\"object_id\":177,\"x\":3584,\"y\":-3072,\"z\":76288,\"room_num\":111},{\"object_id\":139,\"x\":3584,\"y\":-3072,\"z\":73216,\"room_num\":111},{\"object_id\":129,\"x\":10752,\"y\":1024,\"z\":77312,\"room_num\":115},{\"object_id\":282,\"x\":13824,\"y\":1280,\"z\":77312,\"room_num\":115},{\"object_id\":282,\"x\":8704,\"y\":1280,\"z\":77312,\"room_num\":115},{\"object_id\":282,\"x\":10752,\"y\":1280,\"z\":83456,\"room_num\":115},{\"object_id\":282,\"x\":6656,\"y\":1280,\"z\":83456,\"room_num\":115},{\"object_id\":88,\"x\":3584,\"y\":-6144,\"z\":79360,\"room_num\":116},{\"object_id\":133,\"x\":14848,\"y\":1024,\"z\":80384,\"room_num\":118},{\"object_id\":205,\"x\":16896,\"y\":512,\"z\":80384,\"room_num\":119},{\"object_id\":136,\"x\":16896,\"y\":1024,\"z\":79360,\"room_num\":119},{\"object_id\":86,\"x\":15872,\"y\":1024,\"z\":80384,\"room_num\":119},{\"object_id\":205,\"x\":44544,\"y\":-8192,\"z\":77312,\"room_num\":120},{\"object_id\":365,\"x\":43520,\"y\":-7424,\"z\":75264,\"room_num\":120},{\"object_id\":121,\"x\":35328,\"y\":-1792,\"z\":78336,\"room_num\":122},{\"object_id\":121,\"x\":35328,\"y\":-1792,\"z\":76288,\"room_num\":122},{\"object_id\":121,\"x\":35328,\"y\":-1792,\"z\":73216,\"room_num\":122},{\"object_id\":121,\"x\":35328,\"y\":-1792,\"z\":71168,\"room_num\":122},{\"object_id\":119,\"x\":23040,\"y\":-3328,\"z\":75264,\"room_num\":125},{\"object_id\":119,\"x\":23040,\"y\":-1024,\"z\":75264,\"room_num\":126},{\"object_id\":177,\"x\":23040,\"y\":1792,\"z\":82432,\"room_num\":128},{\"object_id\":121,\"x\":32256,\"y\":6400,\"z\":75264,\"room_num\":131},{\"object_id\":139,\"x\":35328,\"y\":5248,\"z\":74240,\"room_num\":134},{\"object_id\":130,\"x\":38400,\"y\":7040,\"z\":76288,\"room_num\":135},{\"object_id\":133,\"x\":38400,\"y\":7424,\"z\":74240,\"room_num\":139},{\"object_id\":171,\"x\":34304,\"y\":-2560,\"z\":77312,\"room_num\":141},{\"object_id\":170,\"x\":36352,\"y\":-2560,\"z\":72192,\"room_num\":141},{\"object_id\":130,\"x\":29184,\"y\":3584,\"z\":69120,\"room_num\":142},{\"object_id\":133,\"x\":28160,\"y\":6400,\"z\":73216,\"room_num\":143},{\"object_id\":173,\"x\":28160,\"y\":6400,\"z\":73216,\"room_num\":143},{\"object_id\":130,\"x\":36352,\"y\":-2688,\"z\":79360,\"room_num\":145},{\"object_id\":133,\"x\":35328,\"y\":-768,\"z\":79360,\"room_num\":146},{\"object_id\":331,\"x\":31232,\"y\":-8704,\"z\":66048,\"room_num\":147},{\"object_id\":107,\"x\":32256,\"y\":-9728,\"z\":66048,\"room_num\":147},{\"object_id\":44,\"x\":32256,\"y\":-7680,\"z\":65024,\"room_num\":147},{\"object_id\":331,\"x\":46592,\"y\":-8064,\"z\":73216,\"room_num\":150},{\"object_id\":331,\"x\":42496,\"y\":-8064,\"z\":73216,\"room_num\":150},{\"object_id\":331,\"x\":42496,\"y\":-8576,\"z\":66048,\"room_num\":150},{\"object_id\":107,\"x\":43520,\"y\":-9216,\"z\":68096,\"room_num\":150},{\"object_id\":107,\"x\":44544,\"y\":-9472,\"z\":70144,\"room_num\":150},{\"object_id\":107,\"x\":44544,\"y\":-9344,\"z\":69120,\"room_num\":150},{\"object_id\":107,\"x\":43520,\"y\":-9216,\"z\":62976,\"room_num\":150},{\"object_id\":107,\"x\":41472,\"y\":-8192,\"z\":71168,\"room_num\":152},{\"object_id\":107,\"x\":39424,\"y\":-8192,\"z\":65024,\"room_num\":152},{\"object_id\":136,\"x\":47616,\"y\":6144,\"z\":47616,\"room_num\":156},{\"object_id\":177,\"x\":46592,\"y\":6144,\"z\":47616,\"room_num\":156},{\"object_id\":180,\"x\":46592,\"y\":5888,\"z\":47616,\"room_num\":156},{\"object_id\":129,\"x\":47616,\"y\":6144,\"z\":47616,\"room_num\":156},{\"object_id\":162,\"x\":46592,\"y\":6144,\"z\":47616,\"room_num\":156},{\"object_id\":139,\"x\":15872,\"y\":0,\"z\":65024,\"room_num\":161},{\"object_id\":86,\"x\":22016,\"y\":3840,\"z\":65024,\"room_num\":169},{\"object_id\":86,\"x\":19968,\"y\":3840,\"z\":65024,\"room_num\":169},{\"object_id\":133,\"x\":37376,\"y\":-3328,\"z\":66048,\"room_num\":170},{\"object_id\":129,\"x\":26112,\"y\":-3328,\"z\":37376,\"room_num\":172},{\"object_id\":139,\"x\":15872,\"y\":-1024,\"z\":69120,\"room_num\":182},{\"object_id\":129,\"x\":15872,\"y\":-1024,\"z\":69120,\"room_num\":182},{\"object_id\":134,\"x\":37376,\"y\":-256,\"z\":40448,\"room_num\":183},{\"object_id\":129,\"x\":37376,\"y\":-256,\"z\":41472,\"room_num\":183},{\"object_id\":129,\"x\":22016,\"y\":-3328,\"z\":32256,\"room_num\":187},{\"object_id\":134,\"x\":31232,\"y\":-256,\"z\":34304,\"room_num\":188},{\"object_id\":45,\"x\":30208,\"y\":-256,\"z\":33280,\"room_num\":188},{\"object_id\":134,\"x\":18944,\"y\":0,\"z\":60928,\"room_num\":189},{\"object_id\":129,\"x\":18944,\"y\":0,\"z\":59904,\"room_num\":189},{\"object_id\":232,\"x\":18944,\"y\":0,\"z\":60928,\"room_num\":189},{\"object_id\":86,\"x\":28160,\"y\":-3328,\"z\":32256,\"room_num\":190},{\"object_id\":86,\"x\":27136,\"y\":-3072,\"z\":30208,\"room_num\":190},{\"object_id\":139,\"x\":26112,\"y\":-1280,\"z\":29184,\"room_num\":192},{\"object_id\":139,\"x\":24064,\"y\":512,\"z\":32256,\"room_num\":193},{\"object_id\":139,\"x\":26112,\"y\":512,\"z\":34304,\"room_num\":193},{\"object_id\":139,\"x\":29184,\"y\":512,\"z\":29184,\"room_num\":193},{\"object_id\":129,\"x\":26112,\"y\":512,\"z\":34304,\"room_num\":193},{\"object_id\":139,\"x\":24064,\"y\":512,\"z\":29184,\"room_num\":193},{\"object_id\":139,\"x\":30208,\"y\":512,\"z\":34304,\"room_num\":193},{\"object_id\":129,\"x\":27136,\"y\":4608,\"z\":34304,\"room_num\":194},{\"object_id\":129,\"x\":29184,\"y\":512,\"z\":36352,\"room_num\":195},{\"object_id\":134,\"x\":43520,\"y\":-256,\"z\":34304,\"room_num\":196},{\"object_id\":45,\"x\":44544,\"y\":-256,\"z\":35328,\"room_num\":196},{\"object_id\":129,\"x\":44544,\"y\":0,\"z\":33280,\"room_num\":196},{\"object_id\":134,\"x\":37376,\"y\":-256,\"z\":28160,\"room_num\":197},{\"object_id\":45,\"x\":36352,\"y\":-256,\"z\":27136,\"room_num\":197},{\"object_id\":47,\"x\":50688,\"y\":-5888,\"z\":71168,\"room_num\":199},{\"object_id\":47,\"x\":28160,\"y\":5120,\"z\":58880,\"room_num\":200},{\"object_id\":178,\"x\":27136,\"y\":-3328,\"z\":26112,\"room_num\":202},{\"object_id\":171,\"x\":27136,\"y\":-3328,\"z\":26112,\"room_num\":202},{\"object_id\":129,\"x\":29184,\"y\":-2048,\"z\":40448,\"room_num\":203},{\"object_id\":129,\"x\":44544,\"y\":-4608,\"z\":76288,\"room_num\":204},{\"object_id\":176,\"x\":44544,\"y\":-4608,\"z\":76288,\"room_num\":204},{\"object_id\":47,\"x\":31232,\"y\":192,\"z\":30208,\"room_num\":205},{\"object_id\":129,\"x\":18944,\"y\":3840,\"z\":65024,\"room_num\":207},{\"object_id\":132,\"x\":23040,\"y\":5120,\"z\":31232,\"room_num\":208},{\"object_id\":131,\"x\":23040,\"y\":5120,\"z\":32256,\"room_num\":208},{\"object_id\":45,\"x\":31232,\"y\":5120,\"z\":38400,\"room_num\":209},{\"object_id\":45,\"x\":32256,\"y\":5120,\"z\":48640,\"room_num\":209},{\"object_id\":224,\"x\":28160,\"y\":4608,\"z\":38400,\"room_num\":211},{\"object_id\":45,\"x\":18944,\"y\":5120,\"z\":38400,\"room_num\":211},{\"object_id\":170,\"x\":22016,\"y\":3840,\"z\":66048,\"room_num\":226},{\"object_id\":177,\"x\":29184,\"y\":-4096,\"z\":51712,\"room_num\":227},{\"object_id\":180,\"x\":29184,\"y\":-4096,\"z\":51712,\"room_num\":227},{\"object_id\":47,\"x\":38400,\"y\":-6400,\"z\":48640,\"room_num\":229},{\"object_id\":178,\"x\":29184,\"y\":-3328,\"z\":39424,\"room_num\":230},{\"object_id\":176,\"x\":29184,\"y\":-3328,\"z\":39424,\"room_num\":230},{\"object_id\":97,\"x\":28160,\"y\":-3328,\"z\":39424,\"room_num\":231},{\"object_id\":44,\"x\":28160,\"y\":-3328,\"z\":41472,\"room_num\":231},{\"object_id\":331,\"x\":28160,\"y\":-4224,\"z\":44544,\"room_num\":231},{\"object_id\":171,\"x\":27136,\"y\":-3328,\"z\":49664,\"room_num\":232},{\"object_id\":331,\"x\":27136,\"y\":-4224,\"z\":46592,\"room_num\":232}]\n    },\n    {\n        \"title\": \"Meteorite Cavern\",\n        \"zone_num\": 5,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,13,24,40,49,50,74,75,76,77,78,79,80,81,82,119,128,131,132,142,145,146,158,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,240,241,242,243,244,245,246,247,276,277,278,279,300,301,304,309,310,311,315,317,321,322,330,336,350,355,366,367],\n        \"items\": [{\"object_id\":80,\"x\":60928,\"y\":-512,\"z\":62976,\"room_num\":5},{\"object_id\":80,\"x\":60928,\"y\":-512,\"z\":64000,\"room_num\":5},{\"object_id\":80,\"x\":59904,\"y\":-512,\"z\":65024,\"room_num\":5},{\"object_id\":80,\"x\":58880,\"y\":-512,\"z\":66048,\"room_num\":5},{\"object_id\":80,\"x\":57856,\"y\":-512,\"z\":66048,\"room_num\":5},{\"object_id\":80,\"x\":56832,\"y\":-512,\"z\":66048,\"room_num\":5},{\"object_id\":80,\"x\":55808,\"y\":-512,\"z\":65024,\"room_num\":5},{\"object_id\":80,\"x\":54784,\"y\":-512,\"z\":64000,\"room_num\":5},{\"object_id\":80,\"x\":54784,\"y\":-512,\"z\":62976,\"room_num\":5},{\"object_id\":80,\"x\":54784,\"y\":-512,\"z\":61952,\"room_num\":5},{\"object_id\":80,\"x\":55808,\"y\":-512,\"z\":60928,\"room_num\":5},{\"object_id\":80,\"x\":56832,\"y\":-512,\"z\":59904,\"room_num\":5},{\"object_id\":80,\"x\":57856,\"y\":-512,\"z\":59904,\"room_num\":5},{\"object_id\":80,\"x\":58880,\"y\":-512,\"z\":59904,\"room_num\":5},{\"object_id\":80,\"x\":59904,\"y\":-512,\"z\":60928,\"room_num\":5},{\"object_id\":80,\"x\":60928,\"y\":-512,\"z\":61952,\"room_num\":5},{\"object_id\":81,\"x\":57856,\"y\":-512,\"z\":67072,\"room_num\":5},{\"object_id\":81,\"x\":61952,\"y\":-512,\"z\":62976,\"room_num\":5},{\"object_id\":81,\"x\":57856,\"y\":-512,\"z\":58880,\"room_num\":5},{\"object_id\":81,\"x\":53760,\"y\":-512,\"z\":62976,\"room_num\":5},{\"object_id\":0,\"x\":61952,\"y\":-512,\"z\":62976,\"room_num\":5},{\"object_id\":119,\"x\":57856,\"y\":-4736,\"z\":62976,\"room_num\":5},{\"object_id\":49,\"x\":54784,\"y\":-512,\"z\":62976,\"room_num\":5},{\"object_id\":240,\"x\":57856,\"y\":-512,\"z\":75264,\"room_num\":6},{\"object_id\":242,\"x\":57856,\"y\":-512,\"z\":50688,\"room_num\":8},{\"object_id\":241,\"x\":45568,\"y\":-512,\"z\":62976,\"room_num\":9},{\"object_id\":243,\"x\":70144,\"y\":-512,\"z\":62976,\"room_num\":15},{\"object_id\":336,\"x\":39424,\"y\":-1536,\"z\":40448,\"room_num\":21},{\"object_id\":40,\"x\":49664,\"y\":0,\"z\":36352,\"room_num\":23},{\"object_id\":177,\"x\":49664,\"y\":0,\"z\":36352,\"room_num\":23},{\"object_id\":40,\"x\":43520,\"y\":0,\"z\":37376,\"room_num\":24},{\"object_id\":170,\"x\":43520,\"y\":0,\"z\":37376,\"room_num\":24},{\"object_id\":170,\"x\":43520,\"y\":0,\"z\":36352,\"room_num\":24},{\"object_id\":50,\"x\":43520,\"y\":0,\"z\":36352,\"room_num\":24},{\"object_id\":40,\"x\":39424,\"y\":0,\"z\":35328,\"room_num\":25},{\"object_id\":75,\"x\":39424,\"y\":0,\"z\":35328,\"room_num\":25},{\"object_id\":77,\"x\":39424,\"y\":0,\"z\":35328,\"room_num\":25},{\"object_id\":131,\"x\":39424,\"y\":0,\"z\":37376,\"room_num\":25},{\"object_id\":128,\"x\":36352,\"y\":0,\"z\":38400,\"room_num\":27},{\"object_id\":75,\"x\":36352,\"y\":0,\"z\":38400,\"room_num\":27},{\"object_id\":131,\"x\":24064,\"y\":0,\"z\":27136,\"room_num\":28},{\"object_id\":304,\"x\":24064,\"y\":-1664,\"z\":28160,\"room_num\":28},{\"object_id\":350,\"x\":19968,\"y\":-768,\"z\":30208,\"room_num\":29},{\"object_id\":336,\"x\":17920,\"y\":-2560,\"z\":33280,\"room_num\":29},{\"object_id\":322,\"x\":18944,\"y\":-1792,\"z\":29184,\"room_num\":29},{\"object_id\":322,\"x\":20992,\"y\":-1792,\"z\":29184,\"room_num\":29},{\"object_id\":322,\"x\":20992,\"y\":-1792,\"z\":31232,\"room_num\":29},{\"object_id\":322,\"x\":18944,\"y\":-1792,\"z\":31232,\"room_num\":29},{\"object_id\":40,\"x\":34304,\"y\":0,\"z\":36352,\"room_num\":43},{\"object_id\":132,\"x\":35328,\"y\":0,\"z\":37376,\"room_num\":43},{\"object_id\":50,\"x\":36352,\"y\":0,\"z\":35328,\"room_num\":43}]\n    },\n    {\n        \"title\": \"All Hallows\",\n        \"zone_num\": -1,\n        \"objects\": [0,1,2,3,4,5,6,7,8,9,10,22,24,56,76,79,83,87,101,111,121,127,128,129,131,132,135,139,140,142,143,144,145,146,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,224,228,232,244,245,247,300,301,304,305,309,310,311,315,318,330,331,332,347,349,350,355,366,367],\n        \"items\": [{\"object_id\":347,\"x\":48640,\"y\":-13568,\"z\":65024,\"room_num\":0},{\"object_id\":176,\"x\":48640,\"y\":-13568,\"z\":65024,\"room_num\":0},{\"object_id\":180,\"x\":48640,\"y\":-12288,\"z\":67072,\"room_num\":0},{\"object_id\":140,\"x\":44544,\"y\":-7424,\"z\":53760,\"room_num\":2},{\"object_id\":350,\"x\":37376,\"y\":7936,\"z\":40448,\"room_num\":5},{\"object_id\":350,\"x\":42496,\"y\":8064,\"z\":44544,\"room_num\":5},{\"object_id\":350,\"x\":37376,\"y\":7936,\"z\":53760,\"room_num\":5},{\"object_id\":350,\"x\":37376,\"y\":8192,\"z\":52736,\"room_num\":5},{\"object_id\":180,\"x\":34304,\"y\":7808,\"z\":54784,\"room_num\":5},{\"object_id\":127,\"x\":40448,\"y\":-8704,\"z\":52736,\"room_num\":13},{\"object_id\":131,\"x\":48640,\"y\":2304,\"z\":66048,\"room_num\":14},{\"object_id\":131,\"x\":46592,\"y\":1280,\"z\":65024,\"room_num\":17},{\"object_id\":224,\"x\":42496,\"y\":-11008,\"z\":53760,\"room_num\":18},{\"object_id\":330,\"x\":37376,\"y\":5504,\"z\":38400,\"room_num\":19},{\"object_id\":56,\"x\":34304,\"y\":5632,\"z\":38400,\"room_num\":19},{\"object_id\":22,\"x\":36352,\"y\":5632,\"z\":37376,\"room_num\":19},{\"object_id\":139,\"x\":35328,\"y\":2048,\"z\":38400,\"room_num\":19},{\"object_id\":304,\"x\":35328,\"y\":2944,\"z\":38400,\"room_num\":19},{\"object_id\":87,\"x\":52736,\"y\":-9472,\"z\":59904,\"room_num\":20},{\"object_id\":87,\"x\":52736,\"y\":-9472,\"z\":58880,\"room_num\":20},{\"object_id\":87,\"x\":52736,\"y\":-9472,\"z\":57856,\"room_num\":20},{\"object_id\":178,\"x\":52736,\"y\":-10496,\"z\":56832,\"room_num\":20},{\"object_id\":180,\"x\":52736,\"y\":-10496,\"z\":56832,\"room_num\":20},{\"object_id\":166,\"x\":30208,\"y\":1536,\"z\":37376,\"room_num\":21},{\"object_id\":170,\"x\":31232,\"y\":1536,\"z\":37376,\"room_num\":21},{\"object_id\":171,\"x\":31232,\"y\":1536,\"z\":40448,\"room_num\":21},{\"object_id\":171,\"x\":31232,\"y\":1536,\"z\":40448,\"room_num\":21},{\"object_id\":174,\"x\":30208,\"y\":1536,\"z\":40448,\"room_num\":21},{\"object_id\":167,\"x\":32256,\"y\":1536,\"z\":38400,\"room_num\":21},{\"object_id\":165,\"x\":32256,\"y\":1536,\"z\":39424,\"room_num\":21},{\"object_id\":177,\"x\":31232,\"y\":1536,\"z\":38400,\"room_num\":21},{\"object_id\":162,\"x\":31232,\"y\":1536,\"z\":39424,\"room_num\":21},{\"object_id\":87,\"x\":40448,\"y\":768,\"z\":66048,\"room_num\":22},{\"object_id\":87,\"x\":41472,\"y\":768,\"z\":66048,\"room_num\":22},{\"object_id\":87,\"x\":42496,\"y\":512,\"z\":66048,\"room_num\":22},{\"object_id\":330,\"x\":40448,\"y\":-128,\"z\":66048,\"room_num\":22},{\"object_id\":330,\"x\":41472,\"y\":-128,\"z\":66048,\"room_num\":22},{\"object_id\":330,\"x\":42496,\"y\":-384,\"z\":66048,\"room_num\":22},{\"object_id\":121,\"x\":44544,\"y\":2048,\"z\":69120,\"room_num\":26},{\"object_id\":318,\"x\":46592,\"y\":1024,\"z\":67072,\"room_num\":28},{\"object_id\":128,\"x\":45568,\"y\":1024,\"z\":66048,\"room_num\":29},{\"object_id\":176,\"x\":46592,\"y\":-256,\"z\":66048,\"room_num\":37},{\"object_id\":0,\"x\":57856,\"y\":-17536,\"z\":55808,\"room_num\":40},{\"object_id\":101,\"x\":44544,\"y\":5632,\"z\":57856,\"room_num\":41},{\"object_id\":178,\"x\":42496,\"y\":5632,\"z\":57856,\"room_num\":41},{\"object_id\":87,\"x\":51712,\"y\":3584,\"z\":53760,\"room_num\":42},{\"object_id\":87,\"x\":52736,\"y\":2816,\"z\":53760,\"room_num\":42},{\"object_id\":87,\"x\":53760,\"y\":2816,\"z\":53760,\"room_num\":42},{\"object_id\":129,\"x\":55808,\"y\":1280,\"z\":53760,\"room_num\":42},{\"object_id\":131,\"x\":50688,\"y\":1280,\"z\":53760,\"room_num\":42},{\"object_id\":83,\"x\":51712,\"y\":1792,\"z\":53760,\"room_num\":42},{\"object_id\":83,\"x\":52736,\"y\":1792,\"z\":53760,\"room_num\":42},{\"object_id\":83,\"x\":53760,\"y\":1792,\"z\":53760,\"room_num\":42},{\"object_id\":129,\"x\":39424,\"y\":5632,\"z\":59904,\"room_num\":43},{\"object_id\":101,\"x\":44544,\"y\":5632,\"z\":59904,\"room_num\":43},{\"object_id\":318,\"x\":42496,\"y\":4864,\"z\":59904,\"room_num\":43},{\"object_id\":111,\"x\":41472,\"y\":5632,\"z\":59904,\"room_num\":43},{\"object_id\":111,\"x\":43520,\"y\":5632,\"z\":59904,\"room_num\":43},{\"object_id\":129,\"x\":40448,\"y\":-2816,\"z\":61952,\"room_num\":44},{\"object_id\":131,\"x\":47616,\"y\":-2816,\"z\":64000,\"room_num\":44},{\"object_id\":177,\"x\":40448,\"y\":-2816,\"z\":61952,\"room_num\":44},{\"object_id\":176,\"x\":41472,\"y\":-2048,\"z\":54784,\"room_num\":44},{\"object_id\":177,\"x\":44544,\"y\":-7424,\"z\":57856,\"room_num\":45},{\"object_id\":304,\"x\":40448,\"y\":-7808,\"z\":58880,\"room_num\":45},{\"object_id\":178,\"x\":46592,\"y\":-7168,\"z\":54784,\"room_num\":46},{\"object_id\":304,\"x\":49664,\"y\":-4864,\"z\":61952,\"room_num\":47},{\"object_id\":139,\"x\":49664,\"y\":-3968,\"z\":61952,\"room_num\":47},{\"object_id\":171,\"x\":49664,\"y\":-3840,\"z\":55808,\"room_num\":47},{\"object_id\":177,\"x\":49664,\"y\":-3840,\"z\":55808,\"room_num\":47},{\"object_id\":142,\"x\":48640,\"y\":-14336,\"z\":58880,\"room_num\":50},{\"object_id\":144,\"x\":46592,\"y\":-14080,\"z\":57856,\"room_num\":50},{\"object_id\":142,\"x\":46592,\"y\":-15104,\"z\":54784,\"room_num\":50},{\"object_id\":142,\"x\":48640,\"y\":-12672,\"z\":59904,\"room_num\":50},{\"object_id\":144,\"x\":48640,\"y\":-12672,\"z\":60928,\"room_num\":50},{\"object_id\":142,\"x\":49664,\"y\":-16128,\"z\":58880,\"room_num\":50},{\"object_id\":163,\"x\":49664,\"y\":-16128,\"z\":58880,\"room_num\":50},{\"object_id\":178,\"x\":46592,\"y\":-15104,\"z\":53760,\"room_num\":50},{\"object_id\":83,\"x\":44544,\"y\":-11264,\"z\":58880,\"room_num\":51},{\"object_id\":331,\"x\":41472,\"y\":-6784,\"z\":55808,\"room_num\":55},{\"object_id\":331,\"x\":42496,\"y\":-5760,\"z\":56832,\"room_num\":55},{\"object_id\":331,\"x\":42496,\"y\":-6784,\"z\":56832,\"room_num\":55},{\"object_id\":135,\"x\":42496,\"y\":-5120,\"z\":61952,\"room_num\":55},{\"object_id\":304,\"x\":44544,\"y\":-6784,\"z\":53760,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":54784,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":55808,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":56832,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":57856,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":58880,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":59904,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":60928,\"room_num\":55},{\"object_id\":87,\"x\":40448,\"y\":-4352,\"z\":61952,\"room_num\":55},{\"object_id\":142,\"x\":46592,\"y\":-3328,\"z\":59904,\"room_num\":57},{\"object_id\":142,\"x\":45568,\"y\":-512,\"z\":57856,\"room_num\":57},{\"object_id\":304,\"x\":49664,\"y\":1024,\"z\":53760,\"room_num\":57},{\"object_id\":131,\"x\":46592,\"y\":5632,\"z\":56832,\"room_num\":58},{\"object_id\":232,\"x\":47616,\"y\":5632,\"z\":57856,\"room_num\":58},{\"object_id\":177,\"x\":49664,\"y\":5632,\"z\":53760,\"room_num\":58},{\"object_id\":349,\"x\":52736,\"y\":7296,\"z\":61952,\"room_num\":60},{\"object_id\":330,\"x\":44544,\"y\":-256,\"z\":66048,\"room_num\":62},{\"object_id\":176,\"x\":39424,\"y\":128,\"z\":66048,\"room_num\":62},{\"object_id\":139,\"x\":45568,\"y\":-2944,\"z\":66048,\"room_num\":65},{\"object_id\":131,\"x\":45568,\"y\":-2816,\"z\":66048,\"room_num\":65},{\"object_id\":129,\"x\":46592,\"y\":-6144,\"z\":49664,\"room_num\":67}]\n    }\n]\n"
  },
  {
    "path": "tools/tr3/objects_tracker/objects_support.json",
    "content": "{\n  \"O_AI_AMBUSH\":              { \"status\": \"fully implemented\" },\n  \"O_AI_FOLLOW\":              { \"status\": \"fully implemented\" },\n  \"O_AI_GUARD\":               { \"status\": \"fully implemented\" },\n  \"O_AI_MODIFY\":              { \"status\": \"fully implemented\" },\n  \"O_AI_PATROL_1\":            { \"status\": \"fully implemented\" },\n  \"O_AI_PATROL_2\":            { \"status\": \"fully implemented\" },\n  \"O_AI_X1\":                  { \"status\": \"fully implemented\" },\n  \"O_AI_X2\":                  { \"status\": \"fully implemented\" },\n  \"O_AI_X3\":                  { \"status\": \"fully implemented\" },\n  \"O_AMBER_LIGHT\":            { \"status\": \"fully implemented\" },\n  \"O_ANIMATING_1\":            { \"status\": \"fully implemented\" },\n  \"O_ANIMATING_2\":            { \"status\": \"fully implemented\" },\n  \"O_ANIMATING_3\":            { \"status\": \"fully implemented\" },\n  \"O_ANIMATING_4\":            { \"status\": \"fully implemented\" },\n  \"O_ANIMATING_5\":            { \"status\": \"fully implemented\" },\n  \"O_ANIMATING_6\":            { \"status\": \"fully implemented\" },\n  \"O_ASSAULT_TARGET\":         { \"status\": \"fully implemented\" },\n  \"O_BEACON_LIGHT\":           { \"status\": \"fully implemented\" },\n  \"O_BLADE\":                  { \"status\": \"fully implemented\" },\n  \"O_BLUE_LIGHT\":             { \"status\": \"fully implemented\" },\n  \"O_BRIDGE_FLAT\":            { \"status\": \"fully implemented\" },\n  \"O_BRIDGE_TILT_1\":          { \"status\": \"fully implemented\" },\n  \"O_BRIDGE_TILT_2\":          { \"status\": \"fully implemented\" },\n  \"O_CAMERA_TARGET\":          { \"status\": \"fully implemented\" },\n  \"O_CARCASS\":                { \"status\": \"fully implemented\" },\n  \"O_CEILING_SPIKES\":         { \"status\": \"fully implemented\" },\n  \"O_CIVILIAN\":               { \"status\": \"fully implemented\" },\n  \"O_COBRA\":                  { \"status\": \"fully implemented\" },\n  \"O_ALLIGATOR\":              { \"status\": \"fully implemented\" },\n  \"O_CLAW_MUTANT\":            { \"status\": \"fully implemented\" },\n  \"O_CRAWLER_MUTANT\":         { \"status\": \"fully implemented\" },\n  \"O_CROW\":                   { \"status\": \"fully implemented\" },\n  \"O_DESERT_EAGLE_AMMO_ITEM\": { \"status\": \"fully implemented\" },\n  \"O_DESERT_EAGLE_ITEM\":      { \"status\": \"fully implemented\" },\n  \"O_DING_DONG\":              { \"status\": \"fully implemented\" },\n  \"O_DIVER\":                  { \"status\": \"fully implemented\" },\n  \"O_DYING_MUTANT\":           { \"status\": \"fully implemented\" },\n  \"O_FIRE_HEAD\":              { \"status\": \"fully implemented\" },\n  \"O_HYBRID_MUTANT\":          { \"status\": \"fully implemented\" },\n  \"O_ORCA\":                   { \"status\": \"fully implemented\" },\n  \"O_DOG\":                    { \"status\": \"fully implemented\" },\n  \"O_PATROL_DOG\":             { \"status\": \"fully implemented\" },\n  \"O_HUSKIE\":                 { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_1\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_2\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_3\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_4\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_5\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_6\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_7\":            { \"status\": \"fully implemented\" },\n  \"O_DOOR_TYPE_8\":            { \"status\": \"fully implemented\" },\n  \"O_EARTHQUAKE\":             { \"status\": \"fully implemented\" },\n  \"O_FUSE_BOX\":               { \"status\": \"fully implemented\" },\n  \"O_ELECTRICAL_LIGHT\":       { \"status\": \"fully implemented\" },\n  \"O_ELECTRIC_CLEANER\":       { \"status\": \"fully implemented\" },\n  \"O_ELECTRIC_FENCE\":         { \"status\": \"fully implemented\" },\n  \"O_FALLING_BLOCK_1\":        { \"status\": \"fully implemented\" },\n  \"O_FALLING_CEILING_1\":      { \"status\": \"fully implemented\" },\n  \"O_FLAME_EMITTER_BIG\":      { \"status\": \"fully implemented\" },\n  \"O_FLAME_EMITTER_JET\":      { \"status\": \"fully implemented\" },\n  \"O_FLAME_EMITTER_SIDE\":     { \"status\": \"fully implemented\" },\n  \"O_FLAME_EMITTER_SMALL\":    { \"status\": \"fully implemented\" },\n  \"O_FLAREBOX_ITEM\":          { \"status\": \"fully implemented\" },\n  \"O_GAS_EMITTER_GREEN\":      { \"status\": \"fully implemented\" },\n  \"O_GREEN_LIGHT\":            { \"status\": \"fully implemented\" },\n  \"O_GRENADE_AMMO_ITEM\":      { \"status\": \"fully implemented\" },\n  \"O_GRENADE_GUN_ITEM\":       { \"status\": \"fully implemented\" },\n  \"O_HARPOON_AMMO_ITEM\":      { \"status\": \"fully implemented\" },\n  \"O_HARPOON_ITEM\":           { \"status\": \"fully implemented\" },\n  \"O_HOOK\":                   { \"status\": \"fully implemented\" },\n  \"O_ICICLE\":                 { \"status\": \"fully implemented\" },\n  \"O_KEY_HOLE_1\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_HOLE_2\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_HOLE_3\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_HOLE_4\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_ITEM_1\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_ITEM_2\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_ITEM_3\":             { \"status\": \"fully implemented\" },\n  \"O_KEY_ITEM_4\":             { \"status\": \"fully implemented\" },\n  \"O_KILL_ALL_TRIGGERED\":     { \"status\": \"fully implemented\" },\n  \"O_LARA\":                   { \"status\": \"fully implemented\" },\n  \"O_LARGE_MEDIPACK_ITEM\":    { \"status\": \"fully implemented\" },\n  \"O_DISPOSABLE_ANIMATING_1\": { \"status\": \"fully implemented\" },\n  \"O_MINE_CART\":              { \"status\": \"fully implemented\" },\n  \"O_MONKEY\":                 { \"status\": \"fully implemented\" },\n  \"O_MOUNTED_GUN\":            { \"status\": \"fully implemented\" },\n  \"O_MOUSE\":                  { \"status\": \"fully implemented\" },\n  \"O_MOVABLE_BLOCK_1\":        { \"status\": \"fully implemented\" },\n  \"O_MOVABLE_BLOCK_2\":        { \"status\": \"fully implemented\" },\n  \"O_MP_1\":                   { \"status\": \"fully implemented\" },\n  \"O_MP_2\":                   { \"status\": \"fully implemented\" },\n  \"O_MP5_AMMO_ITEM\":          { \"status\": \"fully implemented\" },\n  \"O_MP5_ITEM\":               { \"status\": \"fully implemented\" },\n  \"O_ON_OFF_LIGHT\":           { \"status\": \"fully implemented\" },\n  \"O_PENDULUM_1\":             { \"status\": \"fully implemented\" },\n  \"O_PENDULUM_2\":             { \"status\": \"fully implemented\" },\n  \"O_PIRAHNAS\":               { \"status\": \"fully implemented\" },\n  \"O_PISTOL_ITEM\":            { \"status\": \"fully implemented\" },\n  \"O_POISON_DART_EMITTER\":    { \"status\": \"fully implemented\" },\n  \"O_PRISONER\":               { \"status\": \"fully implemented\" },\n  \"O_PROPELLER_2\":            { \"status\": \"fully implemented\" },\n  \"O_PROPELLER_3\":            { \"status\": \"fully implemented\" },\n  \"O_PULSE_LIGHT\":            { \"status\": \"fully implemented\" },\n  \"O_PUNK_1\":                 { \"status\": \"fully implemented\" },\n  \"O_PUNK_2\":                 { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_HOLE_1\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_HOLE_2\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_HOLE_3\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_HOLE_4\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_ITEM_1\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_ITEM_2\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_ITEM_3\":          { \"status\": \"fully implemented\" },\n  \"O_PUZZLE_ITEM_4\":          { \"status\": \"fully implemented\" },\n  \"O_QUAD_BIKE\":              { \"status\": \"fully implemented\" },\n  \"O_UPV\":                    { \"status\": \"fully implemented\" },\n  \"O_KAYAK\":                  { \"status\": \"fully implemented\" },\n  \"O_QUEST_ITEM_1\":           { \"status\": \"fully implemented\" },\n  \"O_QUEST_ITEM_2\":           { \"status\": \"fully implemented\" },\n  \"O_QUEST_ITEM_3\":           { \"status\": \"fully implemented\" },\n  \"O_QUEST_ITEM_4\":           { \"status\": \"fully implemented\" },\n  \"O_RAPTOR\":                 { \"status\": \"fully implemented\" },\n  \"O_RED_LIGHT\":              { \"status\": \"fully implemented\" },\n  \"O_RIB\":                    { \"status\": \"fully implemented\" },\n  \"O_ROCKET_AMMO_ITEM\":       { \"status\": \"fully implemented\" },\n  \"O_ROCKET_GUN_ITEM\":        { \"status\": \"fully implemented\" },\n  \"O_ROLLING_BALL_1\":         { \"status\": \"fully implemented\" },\n  \"O_ROLLING_BALL_4\":         { \"status\": \"fully implemented\" },\n  \"O_RX_WORKER_1\":            { \"status\": \"fully implemented\" },\n  \"O_RX_WORKER_2\":            { \"status\": \"fully implemented\" },\n  \"O_RX_WORKER_3\":            { \"status\": \"fully implemented\" },\n  \"O_SAVE_CRYSTAL_ITEM\":      { \"status\": \"fully implemented\" },\n  \"O_ROTATING_LASER\":         { \"status\": \"fully implemented\" },\n  \"O_SECURITY_LASER_ALARM\":   { \"status\": \"fully implemented\" },\n  \"O_SECURITY_LASER_DEADLY\":  { \"status\": \"fully implemented\" },\n  \"O_SECURITY_LASER_KILLER\":  { \"status\": \"fully implemented\" },\n  \"O_SECURITY_GUARD\":         { \"status\": \"fully implemented\" },\n  \"O_SHIVA\":                  { \"status\": \"fully implemented\" },\n  \"O_SHOTGUN_AMMO_ITEM\":      { \"status\": \"fully implemented\" },\n  \"O_SHOTGUN_ITEM\":           { \"status\": \"fully implemented\" },\n  \"O_SMALL_MEDIPACK_ITEM\":    { \"status\": \"fully implemented\" },\n  \"O_SMASH_OBJECT_3\":         { \"status\": \"fully implemented\" },\n  \"O_SMASH_OBJECT_4\":         { \"status\": \"fully implemented\" },\n  \"O_SMOKE_EMITTER_BLACK\":    { \"status\": \"fully implemented\" },\n  \"O_SMOKE_EMITTER_WHITE\":    { \"status\": \"fully implemented\" },\n  \"O_SOPHIA\":                 { \"status\": \"fully implemented\" },\n  \"O_SPIKES\":                 { \"status\": \"fully implemented\" },\n  \"O_SPIKE_WALL\":             { \"status\": \"fully implemented\" },\n  \"O_SPINNING_BLADE\":         { \"status\": \"fully implemented\" },\n  \"O_STEAM_EMITTER\":          { \"status\": \"fully implemented\" },\n  \"O_STHPAC_MERCENARY\":       { \"status\": \"fully implemented\" },\n  \"O_STROBE_LIGHT\":           { \"status\": \"fully implemented\" },\n  \"O_SWAT_1\":                 { \"status\": \"fully implemented\" },\n  \"O_SWAT_2\":                 { \"status\": \"fully implemented\" },\n  \"O_SWAT_3\":                 { \"status\": \"fully implemented\" },\n  \"O_SWITCH_TYPE_BUTTON\":     { \"status\": \"fully implemented\" },\n  \"O_SWITCH_TYPE_NORMAL\":     { \"status\": \"fully implemented\" },\n  \"O_SWITCH_TYPE_SMALL\":      { \"status\": \"fully implemented\" },\n  \"O_SWITCH_TYPE_UW\":         { \"status\": \"fully implemented\" },\n  \"O_SWITCH_TYPE_WHEEL\":      { \"status\": \"fully implemented\" },\n  \"O_TEETH_TRAP\":             { \"status\": \"fully implemented\" },\n  \"O_TIGER\":                  { \"status\": \"fully implemented\" },\n  \"O_TONY\":                   { \"status\": \"fully implemented\" },\n  \"O_TRAIN\":                  { \"status\": \"fully implemented\" },\n  \"O_TRAPDOOR_TYPE_1\":        { \"status\": \"fully implemented\" },\n  \"O_TRAPDOOR_TYPE_2\":        { \"status\": \"fully implemented\" },\n  \"O_TREX_ALPHA\":             { \"status\": \"fully implemented\" },\n  \"O_COMPY\":                  { \"status\": \"fully implemented\" },\n  \"O_TROPICAL_FISH\":          { \"status\": \"fully implemented\" },\n  \"O_UZI_AMMO_ITEM\":          { \"status\": \"fully implemented\" },\n  \"O_UZI_ITEM\":               { \"status\": \"fully implemented\" },\n  \"O_VULTURE\":                { \"status\": \"fully implemented\" },\n  \"O_WASP_MUTANT\":            { \"status\": \"fully implemented\" },\n  \"O_WASP_MUTANT_EMITTER\":    { \"status\": \"fully implemented\" },\n  \"O_WATERFALL\":              { \"status\": \"fully implemented\" },\n  \"O_WHITE_LIGHT\":            { \"status\": \"fully implemented\" },\n  \"O_WILLARD\":                { \"status\": \"fully implemented\" },\n  \"O_WINSTON\":                { \"status\": \"fully implemented\" },\n  \"O_WINSTON_ARMY\":           { \"status\": \"fully implemented\" },\n  \"O_ZIPLINE_HANDLE\":         { \"status\": \"fully implemented\" },\n  \"O_LIZARD\":                 { \"status\": \"fully implemented\" },\n  \"O_RAPTOR_EMITTER\":         { \"status\": \"fully implemented\" },\n  \"O_BAT_EMITTER\":            { \"status\": \"fully implemented\" },\n  \"O_TRIBE_BOSS\":             { \"status\": \"fully implemented\" },\n  \"O_TRIBE_AXEMAN\":           { \"status\": \"fully implemented\" },\n  \"O_TRIBE_PIPEMAN\":          { \"status\": \"fully implemented\" },\n  \"O_DETONATOR_BOX\":          { \"status\": \"fully implemented\" },\n  \"O_SENTRY_GUN\":             { \"status\": \"fully implemented\" },\n  \"O_AREA_51_ROCKET\":         { \"status\": \"fully implemented\" },\n  \"O_AREA_51_ROCKET_BLAST\":   { \"status\": \"fully implemented\" },\n  \"O_AREA_51_ROCKET_SUPPORT\": { \"status\": \"fully implemented\" }\n}\n"
  },
  {
    "path": "tools/tr3/objects_tracker/read_levels",
    "content": "#!/usr/bin/env -S uv run --script\n#\n# /// script\n# requires-python = \">=3.14\"\n# dependencies = [\"pyjson5\", \"numpy\"]\n# ///\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nimport struct\nimport sys\nfrom argparse import ArgumentParser\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Sequence\n\nimport numpy as np\nimport pyjson5\n\nREPO_ROOT = Path(__file__).parents[3]\n\nTEXTURE_PAGE_WIDTH: int = 256\nTEXTURE_PAGE_HEIGHT: int = 256\nTEXTURE_PAGE_SIZE: int = TEXTURE_PAGE_WIDTH * TEXTURE_PAGE_HEIGHT\n\nlogger = logging.getLogger(__name__)\n\ntype JsonValue = (\n    None\n    | bool\n    | int\n    | float\n    | str\n    | list[JsonValue]\n    | dict[str, JsonValue]\n)\n\n\nclass _ByteReader:\n    def __init__(self, data: bytes):\n        self._data = data\n        self._pos = 0\n\n    def seek(self, pos: int) -> None:\n        if pos < 0 or pos > len(self._data):\n            raise ValueError(\"seek out of range\")\n        self._pos = pos\n\n    def tell(self) -> int:\n        return self._pos\n\n    def read_u8(self) -> int:\n        (value,) = struct.unpack_from(\"<B\", self._data, self._pos)\n        self._pos += 1\n        return int(value)\n\n    def read_s16(self) -> int:\n        (value,) = struct.unpack_from(\"<h\", self._data, self._pos)\n        self._pos += 2\n        return int(value)\n\n    def read_u16(self) -> int:\n        (value,) = struct.unpack_from(\"<H\", self._data, self._pos)\n        self._pos += 2\n        return int(value)\n\n    def read_s32(self) -> int:\n        (value,) = struct.unpack_from(\"<i\", self._data, self._pos)\n        self._pos += 4\n        return int(value)\n\n    def read_u32(self) -> int:\n        (value,) = struct.unpack_from(\"<I\", self._data, self._pos)\n        self._pos += 4\n        return int(value)\n\n    def skip(self, count: int) -> None:\n        self.seek(self._pos + count)\n\n    def slice(self, count: int) -> bytes:\n        start = self._pos\n        end = start + count\n        self._pos = end\n        return self._data[start:end]\n\n\ndef _argb1555_to_rgba8888(packed: int) -> tuple[int, int, int, int]:\n    a1 = (packed >> 15) & 0x01\n    r5 = (packed >> 10) & 0x1F\n    g5 = (packed >> 5) & 0x1F\n    b5 = packed & 0x1F\n    a8 = 255 if a1 != 0 else 0\n    r8 = (r5 << 3) | (r5 >> 2)\n    g8 = (g5 << 3) | (g5 >> 2)\n    b8 = (b5 << 3) | (b5 >> 2)\n    return (r8, g8, b8, a8)\n\n\ndef _skip_tr3_rooms(r: _ByteReader) -> None:\n    room_count = r.read_u16()\n    for _ in range(room_count):\n        r.skip(16)\n        num = r.read_s32()\n        r.skip(num * 2)\n        num = r.read_u16()\n        r.skip(num * 32)\n\n        size_z = r.read_s16()\n        size_x = r.read_s16()\n        r.skip(int(size_z) * int(size_x) * 8)\n\n        r.skip(4)\n        num = r.read_u16()\n        r.skip(num * 24)\n        num = r.read_u16()\n        r.skip(num * 20)\n        r.skip(7)\n\n    num = r.read_s32()\n    r.skip(num * 2)\n\n\n@dataclass(frozen=True, slots=True)\nclass TR3LevelItem:\n    object_id: int\n    x: int\n    y: int\n    z: int\n    room_num: int\n\n\n@dataclass(frozen=True, slots=True)\nclass TR3LevelData:\n    palette_rgba: np.ndarray\n    pages_rgba: list[np.ndarray]\n    object_textures: list[dict[str, int | list[tuple[int, int]]]]\n    anims: list[dict[str, int]]\n    bones: list[dict[str, int | bool]]\n    frame_data: np.ndarray\n    mesh_blob: bytes\n    mesh_offsets: list[int]\n    objects: list[int]\n    items: list[TR3LevelItem]\n\n\ndef read_tr3_level(level_path: str) -> TR3LevelData:\n    with open(level_path, \"rb\") as f:\n        level = f.read()\n\n    r = _ByteReader(level)\n    version = r.read_u32()\n    if version != 0xFF080038 and version != 0xFF180038:\n        raise ValueError(f\"not a TR3 level file (version=0x{version:08X})\")\n    logger.debug(\"version ok @ %d\", r.tell())\n\n    pal_rgb = r.slice(256 * 3)\n    r.skip(\n        256 * 4\n    )  # unused in this script; kept for correct offset progression\n    palette_rgba = np.zeros((256, 4), dtype=np.uint8)\n    palette_rgba[:, 0:3] = np.frombuffer(pal_rgb, dtype=np.uint8).reshape(\n        256, 3\n    )\n    palette_rgba[:, 3] = 255\n    logger.debug(\"palettes read @ %d\", r.tell())\n\n    num_pages = r.read_s32()\n    if num_pages <= 0:\n        raise ValueError(\"no texture pages\")\n    logger.debug(\"texture pages=%d @ %d\", num_pages, r.tell())\n\n    r.skip(num_pages * TEXTURE_PAGE_SIZE)\n    pages_16 = r.slice(num_pages * TEXTURE_PAGE_SIZE * 2)\n    logger.debug(\"pages read (16-bit) @ %d\", r.tell())\n    pages_rgba: list[np.ndarray] = []\n    for page_idx in range(num_pages):\n        start = page_idx * TEXTURE_PAGE_SIZE * 2\n        pix = np.frombuffer(\n            pages_16[start : start + TEXTURE_PAGE_SIZE * 2], dtype=\"<u2\"\n        )\n        rgba = np.zeros(\n            (TEXTURE_PAGE_HEIGHT, TEXTURE_PAGE_WIDTH, 4), dtype=np.uint8\n        )\n        rgba_flat = rgba.reshape(-1, 4)\n        for i in range(TEXTURE_PAGE_SIZE):\n            rgba_flat[i] = _argb1555_to_rgba8888(int(pix[i]))\n        pages_rgba.append(rgba)\n\n    r.skip(4)\n    logger.debug(\"unused version skipped @ %d\", r.tell())\n    _skip_tr3_rooms(r)\n    logger.debug(\"rooms skipped @ %d\", r.tell())\n\n    mesh_data_words = r.read_s32()\n    logger.debug(\"mesh_data_words=%d @ %d\", mesh_data_words, r.tell())\n    mesh_blob = r.slice(mesh_data_words * 2)\n    mesh_ptr_count = r.read_s32()\n    logger.debug(\"mesh_ptr_count=%d @ %d\", mesh_ptr_count, r.tell())\n    mesh_offsets = [r.read_s32() for _ in range(mesh_ptr_count)]\n    logger.debug(\"mesh_offsets read @ %d\", r.tell())\n\n    anim_count = r.read_s32()\n    logger.debug(\"anim_count=%d @ %d\", anim_count, r.tell())\n    anims: list[dict[str, int]] = []\n    for _ in range(anim_count):\n        anims.append(\n            {\n                \"frame_ofs\": r.read_u32(),\n                \"interpolation\": r.read_u8(),\n                \"frame_size\": r.read_u8(),\n                \"current_anim_state\": r.read_s16(),\n                \"velocity\": r.read_s32(),\n                \"acceleration\": r.read_s32(),\n                \"frame_base\": r.read_s16(),\n                \"frame_end\": r.read_s16(),\n                \"jump_anim_num\": r.read_s16(),\n                \"jump_frame_num\": r.read_s16(),\n                \"num_changes\": r.read_s16(),\n                \"change_idx\": r.read_s16(),\n                \"num_commands\": r.read_s16(),\n                \"command_idx\": r.read_s16(),\n            }\n        )\n    logger.debug(\"anims read @ %d\", r.tell())\n\n    num = r.read_s32()\n    logger.debug(\"anim changes count(words?)=%d @ %d\", num, r.tell())\n    r.skip(num * 6)\n    num = r.read_s32()\n    logger.debug(\"anim ranges count(words?)=%d @ %d\", num, r.tell())\n    r.skip(num * 8)\n    num = r.read_s32()\n    logger.debug(\"anim commands count(words?)=%d @ %d\", num, r.tell())\n    r.skip(num * 2)\n\n    bone_total_int32 = r.read_s32()\n    if bone_total_int32 < 0 or (bone_total_int32 % 4) != 0:\n        raise ValueError(\"invalid anim bone block size\")\n    bone_count = bone_total_int32 // 4\n    logger.debug(\"bone_count=%d @ %d\", bone_count, r.tell())\n    bones: list[dict[str, int | bool]] = []\n    for _ in range(bone_count):\n        flags = r.read_s32()\n        bones.append(\n            {\n                \"matrix_pop\": (flags & 1) != 0,\n                \"matrix_push\": (flags & 2) != 0,\n                \"pos_x\": r.read_s32(),\n                \"pos_y\": r.read_s32(),\n                \"pos_z\": r.read_s32(),\n            }\n        )\n    logger.debug(\"bones read @ %d\", r.tell())\n\n    frame_data_words = r.read_s32()\n    logger.debug(\"frame_data_words=%d @ %d\", frame_data_words, r.tell())\n    frame_data = np.frombuffer(r.slice(frame_data_words * 2), dtype=\"<i2\")\n    logger.debug(\"frame_data read @ %d\", r.tell())\n\n    num_objects = r.read_s32()\n    logger.debug(\"objects=%d @ %d\", num_objects, r.tell())\n    objects_by_game_id: dict[int, dict[str, int]] = {}\n    for _ in range(num_objects):\n        gid = r.read_s32()\n        mesh_count = r.read_s16()\n        mesh_idx = r.read_s16()\n        bone_idx = r.read_s32() // 4\n        frame_ofs = r.read_u32()\n        anim_idx = r.read_s16()\n        objects_by_game_id[gid] = {\n            \"mesh_count\": int(mesh_count),\n            \"mesh_idx\": int(mesh_idx),\n            \"bone_idx\": int(bone_idx),\n            \"frame_ofs\": int(frame_ofs),\n            \"anim_idx\": int(anim_idx),\n        }\n    logger.debug(\"objects block read @ %d\", r.tell())\n\n    num_static = r.read_s32()\n    logger.debug(\"static objects=%d @ %d\", num_static, r.tell())\n    r.skip(num_static * 32)\n    logger.debug(\"static objects skipped @ %d\", r.tell())\n\n    num = r.read_s32()\n    logger.debug(\"sprite textures=%d @ %d\", num, r.tell())\n    r.skip(num * 16)\n    num = r.read_s32()\n    logger.debug(\"sprite sequences=%d @ %d\", num, r.tell())\n    r.skip(num * 8)\n    num = r.read_s32()\n    logger.debug(\"cameras/sinks=%d @ %d\", num, r.tell())\n    r.skip(num * 16)\n    num = r.read_s32()\n    logger.debug(\"sound sources=%d @ %d\", num, r.tell())\n    r.skip(num * 16)\n\n    box_count = r.read_s32()\n    logger.debug(\"boxes=%d @ %d\", box_count, r.tell())\n    r.skip(box_count * 8)\n    num = r.read_s32()\n    logger.debug(\"overlaps=%d @ %d\", num, r.tell())\n    r.skip(num * 2)\n    r.skip(box_count * 20)\n    logger.debug(\"zones skipped @ %d\", r.tell())\n\n    num = r.read_s32()\n    logger.debug(\"animated texture ranges=%d @ %d\", num, r.tell())\n    r.skip(num * 2)\n\n    object_texture_count = r.read_s32()\n    logger.debug(\"object textures=%d @ %d\", object_texture_count, r.tell())\n    object_textures: list[dict[str, int | list[tuple[int, int]]]] = []\n    for _ in range(object_texture_count):\n        draw_type = r.read_u16()\n        tex_page = r.read_u16()\n        uvs: list[tuple[int, int]] = []\n        for __ in range(4):\n            uvs.append((r.read_u16(), r.read_u16()))\n        object_textures.append(\n            {\n                \"draw_type\": int(draw_type),\n                \"tex_page\": int(tex_page),\n                \"uvs\": uvs,\n            }\n        )\n    logger.debug(\"object textures read @ %d\", r.tell())\n\n    num_items = r.read_s32()\n    logger.debug(\"items=%d @ %d\", num_items, r.tell())\n    items: list[TR3LevelItem] = []\n    for _ in range(num_items):\n        object_id = r.read_s16()\n        room_num = r.read_s16()\n        x = r.read_s32()\n        y = r.read_s32()\n        z = r.read_s32()\n        r.skip(2)  # y_rot\n        r.skip(4)  # shade (value_1, value_2)\n        r.skip(2)  # flags\n        items.append(\n            TR3LevelItem(\n                object_id=int(object_id),\n                x=int(x),\n                y=int(y),\n                z=int(z),\n                room_num=int(room_num),\n            )\n        )\n    logger.debug(\"items read @ %d\", r.tell())\n\n    return TR3LevelData(\n        palette_rgba=palette_rgba,\n        pages_rgba=pages_rgba,\n        object_textures=object_textures,\n        anims=anims,\n        bones=bones,\n        frame_data=frame_data,\n        mesh_blob=mesh_blob,\n        mesh_offsets=mesh_offsets,\n        objects=sorted(objects_by_game_id.keys()),\n        items=items,\n    )\n\n\n@dataclass(frozen=True, slots=True)\nclass Args:\n    out: Path | None\n\n\ndef parse_args(argv: Sequence[str] | None = None) -> Args:\n    parser = ArgumentParser()\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        dest=\"out\",\n        type=Path,\n        default=None,\n        help=\"Write JSON output to this path (prints to stdout if omitted).\",\n    )\n    ns = parser.parse_args(argv)\n    return Args(out=ns.out)\n\n\ndef _json_dumps_limited_indent(\n    value: JsonValue, *, indent: int, max_depth: int\n) -> str:\n    def encode(node: JsonValue, depth: int) -> str:\n        if depth >= max_depth or not isinstance(node, (list, dict)):\n            return json.dumps(\n                node,\n                separators=(\",\", \":\"),\n                ensure_ascii=False,\n            )\n\n        pad = \" \" * (indent * depth)\n        child_pad = \" \" * (indent * (depth + 1))\n\n        if isinstance(node, list):\n            if not node:\n                return \"[]\"\n            inner = \",\\n\".join(\n                f\"{child_pad}{encode(item, depth + 1)}\" for item in node\n            )\n            return f\"[\\n{inner}\\n{pad}]\"\n\n        if not node:\n            return \"{}\"\n        inner = \",\\n\".join(\n            f\"{child_pad}{json.dumps(str(k), ensure_ascii=False)}: {encode(v, depth + 1)}\"\n            for k, v in node.items()\n        )\n        return f\"{{\\n{inner}\\n{pad}}}\"\n\n    if max_depth < 0:\n        raise ValueError(\"max_depth must be >= 0\")\n    if indent <= 0:\n        raise ValueError(\"indent must be > 0\")\n    return encode(value, 0)\n\n\ndef main() -> None:\n    logging.basicConfig(\n        level=logging.INFO,\n        format=\"%(levelname)s: %(message)s\",\n        stream=sys.stderr,\n    )\n    args: Args = parse_args()\n\n    gf_path = REPO_ROOT / \"data/tr3/ship/cfg/tr3/gameflow.json5\"\n    strings_path = REPO_ROOT / \"data/tr3/ship/cfg/tr3/strings.json5\"\n    data = pyjson5.loads(gf_path.read_text())\n    strings = pyjson5.loads(strings_path.read_text())\n\n    levels: list[dict[str, object]] = []\n    for idx, level in enumerate(data[\"levels\"]):\n        if level.get(\"type\") == \"gym\":\n            zone_num = -1\n        else:\n            zone_num = -1\n            for zone_idx, entry in enumerate(data.get(\"globe_select_entries\", [])):\n                start = int(entry.get(\"start_level_ordinal\", -1))\n                end = int(entry.get(\"completion_level_ordinal\", -1))\n                if start < 0 or end < 0:\n                    continue\n                if start <= idx <= end:\n                    zone_num = zone_idx\n                    break\n\n        level_path = REPO_ROOT / \"test/trx/games/tr3/levels\" / level[\"path\"]\n        logger.info(\"Reading %s\", level_path)\n\n        parsed = read_tr3_level(str(level_path))\n        title = strings[\"levels\"][idx][\"title\"]\n        levels.append(\n            {\n                \"title\": title,\n                \"zone_num\": zone_num,\n                \"objects\": parsed.objects,\n                \"items\": [\n                    {\n                        \"object_id\": item.object_id,\n                        \"x\": item.x,\n                        \"y\": item.y,\n                        \"z\": item.z,\n                        \"room_num\": item.room_num,\n                    }\n                    for item in parsed.items\n                ],\n            }\n        )\n\n    out_json = _json_dumps_limited_indent(levels, indent=4, max_depth=2) + \"\\n\"\n    if args.out is None:\n        logger.info(\"No --out provided; writing JSON to stdout\")\n        print(out_json)\n    else:\n        args.out.parent.mkdir(parents=True, exist_ok=True)\n        args.out.write_text(out_json, encoding=\"utf-8\")\n        print(args.out)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/tr3/objects_tracker/render",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport csv\nimport json\nfrom dataclasses import dataclass\nfrom enum import StrEnum\nfrom pathlib import Path\nfrom typing import Any\n\n\nclass Status(StrEnum):\n    NOT_IMPLEMENTED = \"not implemented\"\n    PARTIALLY_IMPLEMENTED = \"partially implemented\"\n    IN_PROGRESS = \"in progress\"\n    FULLY_IMPLEMENTED = \"fully implemented\"\n\n\nREPO_DIR = Path(__file__).parents[3]\n\ntype Level = dict[str, Any]\ntype Catalog = dict[int, str]\ntype SupportStatus = dict[str, tuple[Status, str | None]]\n\n\n@dataclass(frozen=True, slots=True)\nclass SvgSquare:\n    title: str\n    # Absolute pixel position.\n    x: int\n    y: int\n    size: int = 10\n\n\ndef load_catalog(path: Path) -> Catalog:\n    with path.open(newline=\"\", encoding=\"utf-8\") as f:\n        reader = csv.reader(f)\n        return {int(row[0]): row[1].strip() for row in reader if row}\n\n\ndef load_support_status(path: Path) -> SupportStatus:\n    raw = json.loads(path.read_text(encoding=\"utf-8\"))\n    out: SupportStatus = {}\n    for name, payload in raw.items():\n        status = Status(payload[\"status\"])\n        note = payload.get(\"notes\") or payload.get(\"todo\")\n        out[str(name)] = (status, note)\n    return out\n\n\ndef get_object_name(catalog: Catalog, object_id: int) -> str:\n    return catalog.get(object_id, \"unknown object\")\n\ndef build_object_tooltip(\n    *,\n    level_id: int,\n    level: Level,\n    object_id: int,\n    object_name: str,\n    status: Status,\n    todo: str | None,\n    items: list[Level],\n) -> str:\n    extra = f\" – note: {todo}\" if todo else \"\"\n    header = (\n        f\"Level #{level_id} ({level['title']})\\n\"\n        f\"Object #{object_id} ({object_name})\\n\"\n        f\"Status: {status!s}{extra}\"\n    )\n\n    locs: list[str] = []\n    for item in items:\n        x1024 = item[\"x\"] / 1024.0\n        y1024 = item[\"y\"] / 1024.0\n        z1024 = item[\"z\"] / 1024.0\n        room_num = int(item[\"room_num\"])\n        locs.append(f\"({x1024:.1f}, {y1024:.1f}, {z1024:.1f}) room {room_num}\")\n    locs = sorted(set(locs))\n\n    if not locs:\n        return header\n\n    lines = [header, \"Locations:\"]\n    for loc in locs[:5]:\n        lines.append(f\"- {loc}\")\n    if len(locs) > 5:\n        lines.append(f\"- +{len(locs) - 5} more\")\n    return \"\\n\".join(lines)\n\n\ndef build_level_summary_tooltip(\n    *,\n    level_id: int,\n    level: Level,\n    stats: str,\n) -> str:\n    return f\"Level #{level_id} ({level['title']})\\nSummary: {stats}\"\n\n\ndef parse_objects_dump(\n    dump_path: Path,\n) -> tuple[list[Level], list[int], dict[int, dict[int, list[Level]]]]:\n    # `objects_dump.json` is a list of levels, each having \"items\".\n    dump = json.loads(dump_path.read_text(encoding=\"utf-8\"))\n\n    used_object_ids: set[int] = set()\n    data: dict[int, dict[int, list[Level]]] = {}\n    for lvl_idx, level in enumerate(dump):\n        items = level.get(\"items\") or []\n        if not items:\n            continue\n        for item in items:\n            obj_id = int(item[\"object_id\"])\n            used_object_ids.add(obj_id)\n            data.setdefault(obj_id, {}).setdefault(lvl_idx, []).append(item)\n\n    object_ids = sorted(used_object_ids)\n    return dump, object_ids, data\n\n\ndef make_todo_list(\n    catalog: Catalog,\n    support_status: SupportStatus,\n    levels: list[Level],\n) -> str:\n    lines: list[str] = []\n    prev_zone_num: int | None = None\n\n    for level in levels:\n        items = level.get(\"items\") or []\n        if not items:\n            continue\n\n        object_counts: dict[str, int] = {}\n        for item in items:\n            object_id = int(item[\"object_id\"])\n            object_name = get_object_name(catalog, object_id)\n            object_counts[object_name] = object_counts.get(object_name, 0) + 1\n\n        level_rows: list[str] = []\n        sorted_objects = sorted(\n            object_counts.items(),\n            key=lambda pair: (-pair[1], pair[0]),\n        )\n        for object_name, count in sorted_objects:\n            status, note = support_status.get(\n                object_name, (Status.NOT_IMPLEMENTED, None)\n            )\n            if status == Status.FULLY_IMPLEMENTED:\n                continue\n\n            label = \"instance\" if count == 1 else \"instances\"\n            suffix = f\", {note}\" if note else \"\"\n            level_rows.append(\n                f\"\\t🟨 `{object_name}` ({count} {label}{suffix})\"\n            )\n\n        if not level_rows:\n            continue\n\n        zone_num = int(level.get(\"zone_num\", -1))\n        if prev_zone_num is not None and zone_num != prev_zone_num:\n            lines.append(\"\")\n\n        title = str(level.get(\"title\", \"Unknown level\"))\n        lines.append(f\"🟨 {title}\\t\")\n        lines.extend(level_rows)\n        prev_zone_num = zone_num\n\n    return \"\\n\".join(lines)\n\n\ndef make_svg(\n    catalog: Catalog,\n    support_status: SupportStatus,\n    levels: list[Level],\n    object_ids: list[int],\n    data: dict[int, dict[int, list[Level]]],\n) -> str:\n    padding = 1\n    squares: list[tuple[SvgSquare, str]] = []\n\n    status_colors: dict[tuple[bool, Status], str] = {\n        (False, Status.NOT_IMPLEMENTED): \"#fee\",\n        (False, Status.PARTIALLY_IMPLEMENTED): \"#ffc\",\n        (False, Status.IN_PROGRESS): \"#cff\",\n        (False, Status.FULLY_IMPLEMENTED): \"#dfd\",\n        (True, Status.NOT_IMPLEMENTED): \"tomato\",\n        (True, Status.IN_PROGRESS): \"deepskyblue\",\n        (True, Status.PARTIALLY_IMPLEMENTED): \"gold\",\n        (True, Status.FULLY_IMPLEMENTED): \"limegreen\",\n    }\n\n    status_order = list(Status)\n    status_rank = {s: i for i, s in enumerate(status_order)}\n\n    y = 0\n    prev_lvl: Level | None = None\n    for lvl_id, lvl in enumerate(levels):\n        if prev_lvl and lvl[\"zone_num\"] != prev_lvl[\"zone_num\"]:\n            y += 4\n\n        row_squares: list[tuple[SvgSquare, str]] = []\n        x = 0\n        row_statuses: list[Status] = []\n        for obj_id in object_ids:\n            obj_name = get_object_name(catalog, obj_id)\n            items = data.get(obj_id, {}).get(lvl_id, [])\n            present = bool(items)\n            status, todo = support_status.get(\n                obj_name, (Status.NOT_IMPLEMENTED, None)\n            )\n            tooltip = \"\"\n            if present:\n                row_statuses.append(status)\n                tooltip = build_object_tooltip(\n                    level_id=lvl_id,\n                    level=lvl,\n                    object_id=obj_id,\n                    object_name=obj_name,\n                    status=status,\n                    todo=todo,\n                    items=items,\n                )\n            color = status_colors[(present, status)]\n            square = SvgSquare(title=tooltip, x=x, y=y)\n            row_squares.append((square, color))\n            x += square.size + padding\n\n        if row_statuses:\n            lowest = min(\n                row_statuses, key=lambda s: status_rank.get(s, float(\"inf\"))\n            )\n            counts = {s: row_statuses.count(s) for s in status_order}\n            stats = \", \".join(f\"{s!s}: {n}\" for s, n in counts.items() if n)\n            tooltip = build_level_summary_tooltip(\n                level_id=lvl_id,\n                level=lvl,\n                stats=stats,\n            )\n            summary_color = status_colors[(True, lowest)]\n        else:\n            tooltip = build_level_summary_tooltip(\n                level_id=lvl_id,\n                level=lvl,\n                stats=\"no items\",\n            )\n            summary_color = status_colors[(False, Status.NOT_IMPLEMENTED)]\n\n        # Summary square goes immediately after the last object square.\n        x += 4\n        summary_square = SvgSquare(title=tooltip, x=x, y=y)\n        row_squares.append((summary_square, summary_color))\n        x_end = summary_square.x + summary_square.size\n\n        squares.extend(row_squares)\n\n        row_height = max(square.size for square, _ in row_squares)\n        y += row_height + padding\n        prev_lvl = lvl\n\n    width = 0\n    height = 0\n    if squares:\n        width = max(square.x + square.size for square, _ in squares)\n        height = max(square.y + square.size for square, _ in squares)\n\n    svg_parts: list[str] = [\n        f\"<svg xmlns='http://www.w3.org/2000/svg' width='{width}' height='{height}' shape-rendering='crispEdges'>\"\n    ]\n    for square, color in squares:\n        svg_parts.append(\n            f\"<rect x='{square.x}' y='{square.y}' \"\n            f\"width='{square.size}' height='{square.size}' fill='{color}'>\"\n            f\"<title>{square.title}</title></rect>\"\n        )\n    svg_parts.append(\"</svg>\")\n    return \"\\n\".join(svg_parts)\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"Generate SVG grid of object IDs per level.\"\n    )\n    parser.add_argument(\n        \"--dump\",\n        type=Path,\n        default=Path(__file__).with_name(\"objects_dump.json\"),\n        help=\"Path to objects_dump.json from read_levels.\",\n    )\n    parser.add_argument(\n        \"--support\",\n        type=Path,\n        default=Path(__file__).with_name(\"objects_support.json\"),\n        help=\"Path to objects_support.json (support status mapping).\",\n    )\n    parser.add_argument(\n        \"--todo\",\n        action=\"store_true\",\n        help=\"Output todo list for non-fully-implemented objects per level.\",\n    )\n    args = parser.parse_args()\n\n    catalog = load_catalog(REPO_DIR / \"data/tr3/ship/cfg/catalog_objects.csv\")\n    support_status = load_support_status(args.support)\n    levels, object_ids, data = parse_objects_dump(args.dump)\n    if args.todo:\n        print(make_todo_list(catalog, support_status, levels))\n        return\n    svg = make_svg(catalog, support_status, levels, object_ids, data)\n    print(svg)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/update_game_strings",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nfrom collections.abc import Iterable\nfrom copy import deepcopy\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\nfrom pathlib import Path\nfrom typing import Any\n\nfrom shared.files import find_versioned_files\nfrom shared.json_utils import load_json5_from_string, write_json_to_string\nfrom shared.paths import PROJECT_PATHS, REPO_DIR, CommonPaths\nfrom shared.utils import chunks, uniq\nfrom shared.vfs import VirtualFilesystem\n\nREVIEW_MARKER = r\"\\{review}\"\n\nRE_GAME_STRING_USAGE = re.compile(\n    r'GS(?:_ID|_PTR)?\\(\\s*\"((?:\\\\.|[^\"\\\\])*)\"\\s*\\)'\n)\nRE_GAME_STRING_DEFINE = re.compile(\n    r'GS_DEFINE\\(\\s*([A-Za-z0-9_./-]+)\\s*,\\s*\"((?:\\\\.|[^\"\\\\])*)\"\\)'\n)\nRE_UI_SETTING_USAGE = re.compile(\n    r\"X_UI_CFG[A-Z0-9_]*\\(\\s*([a-z0-9_.]+)\\s*[,)]\",\n    flags=re.M | re.DOTALL,\n)\nRE_ENUM_MAP_USAGE = re.compile(\n    r\"ENUM_MAP\\(\\s*[A-Z0-9_]+\\s*,\\s*([A-Z0-9_]+)\\s*,\"\n)\nRE_ENUM_MAP_DEFINE = re.compile(\n    r\"ENUM_MAP\\(\\s*([A-Z0-9_]+)\\s*,\\s*([A-Z0-9_]+)\\s*,\"\n)\nRE_ENUM_MAP_SELF_DEFINE = re.compile(\n    r\"ENUM_MAP_SELF\\(\\s*([A-Z0-9_]+)\\s*,\\s*([A-Z0-9_]+)\\s*\\)\"\n)\nRE_INPUT_ROLE_USAGE = re.compile(\n    r\"X_INPUT_ROLE\\(\\s*(INPUT_ROLE_[A-Z0-9_]+)\\s*,\"\n)\nRE_OBJ_NAME_DEFINE = re.compile(\n    r'^X_OBJ_NAME_DEFINE\\(\\s*\\w+\\s*,\\s*\"([^\"]+)\"\\s*,\\s*X_OBJ_NAMES\\(\\s*(.+?)\\s*\\)\\s*\\)$'\n)\nROOT_SECTIONS = (\"general\", \"console\", \"enums\", \"dynamic\", \"settings\")\nTOP_LEVEL_SECTION_ORDER = (\n    \"extends\",\n    \"language_name\",\n    \"general\",\n    \"console\",\n    \"dynamic\",\n    \"enums\",\n    \"settings\",\n    \"objects\",\n    \"cutscenes\",\n    \"levels\",\n    \"demos\",\n)\n\n\ndef enum_label_key(enum_type: str, enum_value: str) -> str:\n    return f\"enums/{enum_type}/{enum_value}\"\n\n\ndef resolve_enum_label_key(\n    enum_value: str, enum_value_types: dict[str, str]\n) -> str:\n    enum_type = enum_value_types.get(enum_value)\n    if enum_type is None:\n        return f\"ENUM_{enum_value}\"\n    return enum_label_key(enum_type, enum_value)\n\n\ndef sort_mapping_rec(value: Any) -> Any:\n    if isinstance(value, dict):\n        if set(value.keys()).issubset({\"title\", \"description\"}):\n            ordered: dict[str, Any] = {}\n            if \"title\" in value:\n                ordered[\"title\"] = sort_mapping_rec(value[\"title\"])\n            if \"description\" in value:\n                ordered[\"description\"] = sort_mapping_rec(value[\"description\"])\n            for key in sorted(value.keys()):\n                if key not in ordered:\n                    ordered[key] = sort_mapping_rec(value[key])\n            return ordered\n        return {\n            key: sort_mapping_rec(value[key]) for key in sorted(value.keys())\n        }\n    if isinstance(value, list):\n        return [sort_mapping_rec(item) for item in value]\n    return value\n\n\ndef format_strings_file(source: Any) -> str:\n    source = deepcopy(source)\n    ordered_source: dict[str, Any] = {}\n    for section in TOP_LEVEL_SECTION_ORDER:\n        if section in source:\n            value = source[section]\n            ordered_source[section] = (\n                sort_mapping_rec(value) if isinstance(value, dict) else value\n            )\n    for section in sorted(source.keys()):\n        if section in ordered_source:\n            continue\n        value = source[section]\n        ordered_source[section] = (\n            sort_mapping_rec(value) if isinstance(value, dict) else value\n        )\n    content = write_json_to_string(ordered_source)\n    content = (\n        \"\"\"{\n    // For usage, refer to the documentation here:\n    // https://lostartefacts.dev/trx/docs/stable/game_strings\n    \"\"\"\n        + content[1:].strip()\n        + \"\\n\"\n    )\n    content = re.sub(r'\"\\n(\\s*[\\]}])', r'\",\\n\\1', content, flags=re.M)\n    return content\n\n\ndef step(func=None):\n    if func is None:\n        return step\n    step.registry.append(func)\n    return func\n\n\ndef clean(source: str | list[str] | None) -> str | list[str] | None:\n    if not source:\n        return source\n    if isinstance(source, list):\n        return [clean(item) for item in source]\n    if isinstance(source, dict):\n        return {key: clean(value) for key, value in source.items()}\n    if not isinstance(source, str):\n        return source\n    return source.replace(REVIEW_MARKER, \"\")\n\n\nstep.registry = []\n\n\n@dataclass\nclass GameStringFile:\n    path: Path\n    data: dict[str, Any]\n\n    @property\n    def extends(self) -> str | None:\n        return self.data.get(\"extends\")\n\n    def __init__(self, path: Path, data: dict[str, Any]) -> None:\n        self.path = path\n        self.data = data\n\n\n@dataclass\nclass GameStringSet:\n    main: GameStringFile\n    translations: dict[str, GameStringFile]\n\n    def __init__(self, vfs: VirtualFilesystem, location: Path) -> None:\n        main_path = list(location.glob(\"*strings.json5\"))[0]\n        main_data = load_json5_from_string(vfs.get(main_path))\n        self.main = GameStringFile(main_path, main_data)\n        self.translations = {\n            re.match(\".*-(.*)\", path.stem).group(1): GameStringFile(\n                path, load_json5_from_string(vfs.get(path))\n            )\n            for path in location.glob(\"*strings-*.json5\")\n        }\n\n\ndef get_strings_map(path: Path) -> dict[str, str]:\n    result: dict[str, str] = {}\n    for line in path.read_text().splitlines():\n        if match := RE_GAME_STRING_DEFINE.match(line):\n            result[match.group(1)] = (\n                match.group(2)\n                .replace(\"\\\\n\", \"\\n\")\n                .replace('\\\\\"', '\"')\n                .replace(\"\\\\\\\\\", \"\\\\\")\n            )\n    return result\n\n\ndef get_config_aliases(path: Path) -> dict[str, str]:\n    aliases: dict[str, str] = {}\n    if not path.exists():\n        return aliases\n    ex_re = re.compile(\n        r'^X_CFG_[A-Z0-9_]+_EX\\(\\s*\"([^\"]+)\"\\s*,\\s*([a-z0-9_.]+)\\s*,'\n    )\n    for line in path.read_text().splitlines():\n        if match := ex_re.match(line.strip()):\n            aliases[match.group(2)] = match.group(1)\n    return aliases\n\n\ndef gs_get(game_strings: dict[str, Any], key: str) -> Any:\n    if \"/\" not in key:\n        return game_strings.get(key)\n    parts = key.split(\"/\")\n    cur: Any = game_strings\n    for part in parts:\n        if not isinstance(cur, dict):\n            return None\n        cur = cur.get(part)\n        if cur is None:\n            return None\n    return cur\n\n\ndef gs_set(game_strings: dict[str, Any], key: str, value: Any) -> None:\n    if \"/\" not in key:\n        game_strings[key] = value\n        return\n    parts = key.split(\"/\")\n    cur: dict[str, Any] = game_strings\n    for part in parts[:-1]:\n        nxt = cur.get(part)\n        if not isinstance(nxt, dict):\n            nxt = {}\n            cur[part] = nxt\n        cur = nxt\n    cur[parts[-1]] = value\n\n\ndef gs_delete(game_strings: dict[str, Any], key: str) -> None:\n    if \"/\" not in key:\n        game_strings.pop(key, None)\n        return\n    parts = key.split(\"/\")\n    cur: Any = game_strings\n    for part in parts[:-1]:\n        if not isinstance(cur, dict):\n            return\n        cur = cur.get(part)\n        if cur is None:\n            return\n    if isinstance(cur, dict):\n        cur.pop(parts[-1], None)\n\n\ndef gs_flatten(game_strings: dict[str, Any], prefix: str = \"\") -> dict[str, Any]:\n    result: dict[str, Any] = {}\n    for key, value in game_strings.items():\n        full_key = f\"{prefix}/{key}\" if prefix else key\n        if isinstance(value, dict):\n            result.update(gs_flatten(value, full_key))\n        else:\n            result[full_key] = value\n    return result\n\n\ndef split_root_key(key: str) -> tuple[str, str]:\n    for prefix in ROOT_SECTIONS:\n        full_prefix = f\"{prefix}/\"\n        if key.startswith(full_prefix):\n            return prefix, key[len(full_prefix) :]\n    raise ValueError(f\"game string key must have a known root prefix: {key}\")\n\n\ndef get_root_dict(\n    file_data: dict[str, Any], section: str, create: bool = False\n) -> dict[str, Any] | None:\n    section_data = file_data.get(section)\n    if isinstance(section_data, dict):\n        return section_data\n    if not create:\n        return None\n    section_data = {}\n    file_data[section] = section_data\n    return section_data\n\n\ndef root_get(file_data: dict[str, Any], key: str) -> Any:\n    section, path = split_root_key(key)\n    section_data = get_root_dict(file_data, section)\n    if section_data is None:\n        return None\n    return gs_get(section_data, path)\n\n\ndef root_set(file_data: dict[str, Any], key: str, value: Any) -> None:\n    section, path = split_root_key(key)\n    section_data = get_root_dict(file_data, section, create=True)\n    gs_set(section_data, path, value)\n\n\ndef root_delete(file_data: dict[str, Any], key: str) -> None:\n    section, path = split_root_key(key)\n    section_data = get_root_dict(file_data, section)\n    if section_data is not None:\n        gs_delete(section_data, path)\n\n\ndef root_flatten(file_data: dict[str, Any]) -> dict[str, Any]:\n    result: dict[str, Any] = {}\n    for section in ROOT_SECTIONS:\n        section_data = get_root_dict(file_data, section)\n        if section_data is None:\n            continue\n        result.update(gs_flatten(section_data, section))\n    return result\n\n\ndef get_used_strings(\n    path: Path, enum_value_types: dict[str, str], aliases: dict[str, str] | None = None\n) -> Iterable[tuple[int, str]]:\n    source = re.sub(\"//.*\", \"\", path.read_text(), flags=re.M)\n    source = re.sub(r\"^\\s*#define\\s+.*$\", \"\", source, flags=re.M)\n    for match in re.finditer(RE_GAME_STRING_USAGE, source):\n        yield source.count(\"\\n\", 0, match.start()) + 1, (\n            match.group(1)\n            .replace(\"\\\\n\", \"\\n\")\n            .replace('\\\\\"', '\"')\n            .replace(\"\\\\\\\\\", \"\\\\\")\n        )\n    for match in re.finditer(RE_UI_SETTING_USAGE, source):\n        option_name = match.group(1)\n        if aliases is not None:\n            option_name = aliases.get(option_name, option_name)\n        yield source.count(\"\\n\", 0, match.start()) + 1, (\n            f\"settings/{option_name}/title\"\n        )\n        yield source.count(\"\\n\", 0, match.start()) + 1, (\n            f\"settings/{option_name}/description\"\n        )\n    for match in re.finditer(RE_ENUM_MAP_USAGE, source):\n        enum_value = match.group(1)\n        yield source.count(\"\\n\", 0, match.start()) + 1, resolve_enum_label_key(\n            enum_value, enum_value_types\n        )\n    for match in re.finditer(RE_INPUT_ROLE_USAGE, source):\n        enum_value = match.group(1)\n        yield source.count(\"\\n\", 0, match.start()) + 1, enum_label_key(\n            \"INPUT_ROLE\", enum_value\n        )\n\n\ndef get_enum_value_types(paths: list[Path]) -> dict[str, str]:\n    value_types: dict[str, str] = {}\n    for path in paths:\n        source = re.sub(\"//.*\", \"\", path.read_text(), flags=re.M)\n        source = re.sub(r\"/\\*.*?\\*/\", \"\", source, flags=re.S)\n        source = re.sub(r\"^\\s*#define\\s+.*$\", \"\", source, flags=re.M)\n        for enum_type, enum_value in RE_ENUM_MAP_DEFINE.findall(source):\n            value_types[enum_value] = enum_type\n        for enum_type, enum_value in RE_ENUM_MAP_SELF_DEFINE.findall(source):\n            value_types[enum_value] = enum_type\n    return value_types\n\n\ndef get_used_ui_theme_strings(\n    vfs: VirtualFilesystem, path: Path\n) -> Iterable[tuple[int, str]]:\n    if not path.exists():\n        return\n    source = load_json5_from_string(vfs.get(path))\n    if not isinstance(source, dict):\n        return\n    for _, theme_data in source.get(\"bars\", {}).items():\n        if not isinstance(theme_data, dict):\n            continue\n        name_gs = theme_data.get(\"name_gs\")\n        if isinstance(name_gs, str):\n            yield 0, name_gs\n\n\ndef get_used_outfit_strings(\n    vfs: VirtualFilesystem, path: Path\n) -> Iterable[tuple[int, str]]:\n    if not path.exists():\n        return\n    source = load_json5_from_string(vfs.get(path))\n    if not isinstance(source, dict):\n        return\n    outfits = source.get(\"outfits\")\n    if not isinstance(outfits, dict):\n        return\n    for _, outfit_data in outfits.items():\n        if not isinstance(outfit_data, dict):\n            continue\n        name_gs = outfit_data.get(\"name_gs\")\n        if isinstance(name_gs, str):\n            yield 0, name_gs\n\n\ndef get_used_mod_strings(\n    project_paths: dict,\n) -> Iterable[tuple[int, str]]:\n    for paths in project_paths.values():\n        for mod_path in paths.shipped_data_dir.glob(\"cfg/*\"):\n            if (mod_path / \"gameflow.json5\").exists():\n                yield 0, f\"dynamic/mods/{mod_path.name}/title\"\n\n\ndef get_used_preset_strings(\n    vfs: VirtualFilesystem, presets_dir: Path\n) -> Iterable[tuple[int, str]]:\n    if not presets_dir.exists():\n        return\n    for preset_path in presets_dir.glob(\"*.json5\"):\n        source = load_json5_from_string(vfs.get(preset_path))\n        if not isinstance(source, dict):\n            continue\n        name_gs = source.get(\"name_gs\")\n        if isinstance(name_gs, str):\n            yield 0, name_gs\n\n\ndef get_objects_map(paths: list[Path]) -> dict[str, list[str]]:\n    \"\"\"\n    Parse object-name definitions from names.def-like files.\n    Supports:\n      X_OBJ_NAME_DEFINE(..., \"key\", X_OBJ_NAMES(\"Name1\", \"Name2\", ...))\n    \"\"\"\n    result: dict[str, list[str]] = {}\n    for path in paths:\n        for line in path.read_text().splitlines():\n            text = line.strip()\n            if match := RE_OBJ_NAME_DEFINE.match(text):\n                key = match.group(1)\n                names_list = uniq(re.findall(r'\"([^\"]+)\"', match.group(2)))\n                result.setdefault(key, {})[\"name\"] = (\n                    names_list[0] if len(names_list) == 1 else names_list\n                )\n    return result\n\n\nclass RunContext:\n    def __init__(\n        self, vfs: VirtualFilesystem, args: argparse.Namespace\n    ) -> None:\n        self.vfs = vfs\n        self.args = args\n\n        self.base = GameStringSet(vfs, CommonPaths.shipped_data_dir / \"cfg\")\n        self.mods = {\n            f\"tr{game}-{mod_path.name}\": GameStringSet(vfs, mod_path)\n            for game, paths in PROJECT_PATHS.items()\n            for mod_path in paths.shipped_data_dir.glob(\"cfg/*\")\n            if mod_path.is_dir()\n        }\n\n        source_files = [\n            path\n            for path in find_versioned_files(REPO_DIR)\n            if path.suffix in [\".c\", \".h\", \".def\"]\n        ]\n        self.enum_value_types = get_enum_value_types(source_files)\n        self.config_aliases = get_config_aliases(CommonPaths.src_dir / \"config/map.def\")\n        self.game_strings_def_path = (\n            CommonPaths.src_dir / \"game/game_strings/entries.def\"\n        )\n        self.game_strings_dict = get_strings_map(self.game_strings_def_path)\n        self.used_game_strings = sum(\n            [\n                list(\n                    get_used_strings(\n                        path,\n                        enum_value_types=self.enum_value_types,\n                        aliases=self.config_aliases,\n                    )\n                )\n                for path in source_files\n            ],\n            [],\n        )\n        self.used_game_strings += list(\n            get_used_ui_theme_strings(\n                vfs, CommonPaths.shipped_data_dir / \"cfg/ui.json5\"\n            )\n        )\n        self.used_game_strings += list(\n            get_used_outfit_strings(\n                vfs, CommonPaths.shipped_data_dir / \"cfg/outfits.json5\"\n            )\n        )\n        self.used_game_strings += list(\n            get_used_preset_strings(\n                vfs, CommonPaths.shipped_data_dir / \"cfg/presets\"\n            )\n        )\n        self.used_game_strings += list(get_used_mod_strings(PROJECT_PATHS))\n\n        self.object_names_dict = get_objects_map(source_files)\n\n    @property\n    def all_files(self) -> Iterable[GameStringFile]:\n        for gs in (self.base, *self.mods.values()):\n            yield gs.main\n            yield from gs.translations.values()\n\n\nclass BaseResolver:\n    \"\"\"Base logic for filling missing game_strings translations.\"\"\"\n\n    def __init__(self, base_file: GameStringFile, trans_file: GameStringFile) -> None:\n        self.base_file = base_file\n        self.trans_file = trans_file\n\n    def fill(self) -> Any:\n        self.fill_base_strings()\n        self.fill_object_names()\n        self.fill_level_object_names()\n        return self.trans_file.data\n\n    def fill_base_strings(self) -> None:\n        base_gs = root_flatten(self.base_file.data)\n        trans_gs = root_flatten(self.trans_file.data)\n        source = trans_gs if self.trans_file.extends else base_gs\n        missing = {}\n        for key in source:\n            base_value = clean(base_gs.get(key))\n            trans_value = clean(trans_gs.get(key))\n            if isinstance(base_value, (dict, list)):\n                if not trans_value:\n                    root_set(self.trans_file.data, key, base_value)\n                continue\n            if not trans_value:\n                missing[key] = base_value\n        for chunk in chunks(list(missing.items()), n=50):\n            filled = dict(self.fill_chunk(chunk))\n            for key, value in filled.items():\n                root_set(self.trans_file.data, key, value)\n\n    def fill_object_names(self) -> None:\n        base_objs = self.base_file.data.get(\"objects\", {})\n        trans_objs = self.trans_file.data.setdefault(\"objects\", {})\n        missing_objs = {}\n        for key, obj in base_objs.items():\n            base_name = obj.get(\"name\")\n            if isinstance(base_name, list) and base_name:\n                base_name = base_name[0]  # Only take the first\n            if key in trans_objs and not clean(trans_objs[key].get(\"name\")):\n                missing_objs[key] = clean(base_name)\n        for chunk in chunks(list(missing_objs.items()), n=50):\n            filled = dict(self.fill_chunk(chunk))\n            for k, v in filled.items():\n                trans_objs.setdefault(k, {})[\"name\"] = v\n        if not self.trans_file.data[\"objects\"]:\n            del self.trans_file.data[\"objects\"]\n\n    def fill_level_object_names(self) -> None:\n        base_levels = self.base_file.data.get(\"levels\")\n        trans_levels = self.trans_file.data.get(\"levels\")\n        if self.trans_file.extends:\n            return\n        if not isinstance(base_levels, list) or not isinstance(trans_levels, list):\n            return\n\n        missing: list[tuple[str, str]] = []\n        for level_index, base_level in enumerate(base_levels):\n            if level_index >= len(trans_levels):\n                break\n            if not isinstance(base_level, dict):\n                continue\n            trans_level = trans_levels[level_index]\n            if not isinstance(trans_level, dict):\n                continue\n\n            base_objects = base_level.get(\"objects\")\n            if not isinstance(base_objects, dict) or not base_objects:\n                continue\n\n            trans_objects = trans_level.setdefault(\"objects\", {})\n            if not isinstance(trans_objects, dict):\n                trans_objects = {}\n                trans_level[\"objects\"] = trans_objects\n\n            for obj_key, obj in base_objects.items():\n                if not isinstance(obj, dict):\n                    continue\n                base_name = obj.get(\"name\")\n                if isinstance(base_name, list) and base_name:\n                    base_name = base_name[0]  # Only take the first\n                trans_obj = trans_objects.get(obj_key)\n                if not isinstance(trans_obj, dict) or not clean(\n                    trans_obj.get(\"name\")\n                ):\n                    missing.append((f\"{level_index}:{obj_key}\", clean(base_name)))\n\n        for chunk in chunks(missing, n=50):\n            filled = dict(self.fill_chunk(chunk))\n            for packed_key, v in filled.items():\n                level_index_str, obj_key = packed_key.split(\":\", 1)\n                level_index = int(level_index_str)\n                if level_index >= len(trans_levels):\n                    continue\n                trans_level = trans_levels[level_index]\n                if not isinstance(trans_level, dict):\n                    continue\n                trans_objects = trans_level.setdefault(\"objects\", {})\n                if not isinstance(trans_objects, dict):\n                    trans_objects = {}\n                    trans_level[\"objects\"] = trans_objects\n                trans_objects.setdefault(obj_key, {})[\"name\"] = v\n\n\nclass AIResolver(BaseResolver):\n    def __init__(\n        self, base_file: Any, trans_file: GameStringFile, target_lang: str, model: str\n    ) -> None:\n        super().__init__(base_file, trans_file)\n        self.target_lang = target_lang\n        self.model = model\n        try:\n            import openai\n        except ImportError:\n            sys.exit(\"Error: openai library is required for --fill\")\n        if not openai.api_key and not os.getenv(\"OPENAI_API_KEY\"):\n            sys.exit(\"Error: OPENAI_API_KEY env var must be set for --fill\")\n        openai.api_key = os.getenv(\"OPENAI_API_KEY\", openai.api_key)\n\n    @property\n    def system_prompt(self) -> str:\n        return f\"\"\"\n            Translate the user-provided JSON to language '{self.target_lang}'\n            for a Tomb Raider game. You are not allowed to compress identical\n            lines - output must exactly match input.\"\"\"\n\n    def fill_chunk(self, chunk: list[tuple[str, str]]) -> list[tuple[str, str]]:\n        import openai\n\n        keys = [row[0] for row in chunk]\n        values = [row[1] for row in chunk]\n        messages = [\n            {\"role\": \"system\", \"content\": self.system_prompt},\n            {\n                \"role\": \"user\",\n                \"content\": json.dumps(values, ensure_ascii=False),\n            },\n        ]\n        buf = \"\"\n        response = openai.chat.completions.create(\n            model=self.model, messages=messages, temperature=0, stream=True\n        )\n        for chunk_resp in response:\n            delta = chunk_resp.choices[0].delta.content\n            if delta:\n                buf += delta\n        try:\n            parsed = json.loads(buf)\n        except Exception as e:\n            sys.exit(f\"Error parsing AI response: {e}\\n{buf}\")\n        parsed = [REVIEW_MARKER + (text or \"\") for text in parsed]\n        return list(zip(keys, parsed, strict=True))\n\n\nclass BlankResolver(BaseResolver):\n    def fill_chunk(self, chunk: list[tuple[str, str]]) -> dict[str, str]:\n        return {key: REVIEW_MARKER for key, _ in chunk}\n\n\n@step\ndef remove_unused_defines(ctx: RunContext) -> None:\n    \"\"\"Remove any GS_DEFINE() macros from the game_strings/entries.def that\n    aren't used.\n    \"\"\"\n    used = {key for _, key in ctx.used_game_strings}\n    orig = ctx.game_strings_def_path.read_text().splitlines(keepends=True)\n    filtered: list[str] = []\n    for line in orig:\n        stripped = line.strip()\n        if m := RE_GAME_STRING_DEFINE.match(stripped):\n            if m.group(1) not in used:\n                continue\n        filtered.append(line)\n    ctx.vfs.put(ctx.game_strings_def_path, \"\".join(filtered))\n\n\n@step\ndef sync_base_strings(ctx: RunContext) -> None:\n    \"\"\"Sync base strings and clear changed keys in translations and mods.\"\"\"\n    old_flat = root_flatten(ctx.base.main.data)\n    used = {key for _, key in ctx.used_game_strings}\n    new_flat = dict(ctx.game_strings_dict)\n    missing_used = sorted(\n        key\n        for key in used\n        if key.startswith(\"settings/\") and key not in new_flat\n    )\n    if missing_used:\n        preview = \"\\n\".join(f\"  - {key}\" for key in missing_used[:20])\n        extra = \"\" if len(missing_used) <= 20 else f\"\\n  ... and {len(missing_used) - 20} more\"\n        sys.exit(\n            \"Error: used game string keys are missing from \"\n            \"src/trx/game/game_strings/entries.def:\\n\"\n            f\"{preview}{extra}\"\n        )\n    changed = [k for k, v in new_flat.items() if old_flat.get(k) != v]\n\n    for section in ROOT_SECTIONS:\n        ctx.base.main.data[section] = {}\n    for key, value in new_flat.items():\n        root_set(ctx.base.main.data, key, value)\n    for key in changed:\n        for file in ctx.base.translations.values():\n            if not file.extends:\n                root_set(file.data, key, REVIEW_MARKER)\n        for gs in ctx.mods.values():\n            if root_get(gs.main.data, key):\n                root_set(gs.main.data, key, REVIEW_MARKER)\n                for file in gs.translations.values():\n                    if not file.extends:\n                        root_set(file.data, key, REVIEW_MARKER)\n\n    old = ctx.base.main.data.get(\"objects\", {})\n    new = ctx.object_names_dict\n    changed = [\n        k for k, v in new.items() if json.dumps(old.get(k)) != json.dumps(v)\n    ]\n    ctx.base.main.data[\"objects\"] = new\n    for key in changed:\n        for file in ctx.base.translations.values():\n            if not file.extends:\n                file.data.setdefault(\"objects\", {})[key] = {\"name\": REVIEW_MARKER}\n        for gs in ctx.mods.values():\n            if gs.main.data.get(\"objects\", {}).get(key):\n                gs.main.data.setdefault(\"objects\", {})[key] = {\n                    \"name\": REVIEW_MARKER\n                }\n                for file in gs.translations.values():\n                    if not file.extends:\n                        file.data.setdefault(\"objects\", {})[key] = {\n                            \"name\": REVIEW_MARKER\n                        }\n\n\n@step\ndef resolve_missing_translations(ctx: RunContext) -> None:\n    \"\"\"Fill missing translations from base and mods.\"\"\"\n    for gs in (ctx.base, *ctx.mods.values()):\n        base_file = gs.main\n        for lang, trans_file in gs.translations.items():\n            if ctx.args.fill:\n                resolver = AIResolver(\n                    base_file,\n                    trans_file,\n                    target_lang=lang,\n                    model=ctx.args.model,\n                )\n            else:\n                resolver = BlankResolver(base_file, trans_file)\n            merged = resolver.fill()\n            trans_file.data = merged\n\n\n@step\ndef remove_duplicate_mod_strings(ctx: RunContext) -> None:\n    \"\"\"Removes mods base keys if they're identical to the main base strings.\n    Only applies to game strings.\n    \"\"\"\n    base_gs = root_flatten(ctx.base.main.data)\n    for gs in ctx.mods.values():\n        mod_gs = root_flatten(gs.main.data)\n        if not mod_gs:\n            continue\n        for key in list(mod_gs.keys()):\n            if key in base_gs and base_gs[key] == mod_gs[key]:\n                root_delete(gs.main.data, key)\n                for file in gs.translations.values():\n                    if root_get(file.data, key) is not None:\n                        root_delete(file.data, key)\n\n\n@step\ndef remove_extra_translation_strings(ctx: RunContext) -> None:\n    \"\"\"Remove translation keys that aren't present in the base files.\n    Applies both to game strings and object names.\n    \"\"\"\n    for gs in (ctx.base, *ctx.mods.values()):\n        base_data = gs.main.data\n        for file in gs.translations.values():\n            gs_trans = root_flatten(file.data)\n            if gs_trans:\n                allowed = root_flatten(base_data)\n                for key in list(gs_trans):\n                    if key not in allowed:\n                        root_delete(file.data, key)\n            obj_trans = file.data.get(\"objects\")\n            if obj_trans:\n                allowed_objs = base_data.get(\"objects\", {})\n                for key in list(obj_trans):\n                    if key not in allowed_objs:\n                        del obj_trans[key]\n\n\n@step\ndef sort_game_strings(ctx):\n    \"\"\"Sort game strings by key.\"\"\"\n    for file in ctx.all_files:\n        for section in ROOT_SECTIONS:\n            if section_data := file.data.get(section):\n                file.data[section] = sort_mapping_rec(section_data)\n            elif not file.data.get(section):\n                file.data.pop(section, None)\n\n\n@step\ndef flatten_object_names(ctx):\n    \"\"\"Flatten single-item object \"names\" arrays into \"name\" properties.\"\"\"\n    for file in ctx.all_files:\n        if object_names_map := file.data.get(\"objects\"):\n            for key, obj in object_names_map.items():\n                if obj.get(\"names\"):\n                    names = obj.pop(\"names\")\n                else:\n                    names = obj[\"name\"]\n                if isinstance(names, list):\n                    names = uniq(names)\n                if isinstance(names, list) and len(names) == 1:\n                    obj[\"name\"] = names[0]\n                else:\n                    obj[\"name\"] = names\n\n\nclass Mode(Enum):\n    REPORT = auto()\n    FIX = auto()\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Update game strings JSON files to reflect GS_DEFINE() macros.\"\n    )\n    parser.add_argument(\n        \"--fill\",\n        action=\"store_true\",\n        help=\"Fill missing translations (AI if model specified, else blank).\",\n    )\n    parser.add_argument(\n        \"--model\",\n        default=os.getenv(\"OPENAI_MODEL\", \"gpt-4.1-mini\"),\n        help=\"OpenAI model to use for translation (default: %(default)s).\",\n    )\n    parser.add_argument(\n        \"--fix\",\n        action=\"store_true\",\n        help=\"Apply fixes. Defaults to dry-run reporting warnings.\",\n    )\n    return parser.parse_args()\n\n\ndef main() -> None:\n    args = parse_args()\n    mode = Mode.FIX if args.fix else Mode.REPORT\n\n    vfs = VirtualFilesystem()\n    ctx = RunContext(vfs, args)\n    for f in step.registry:\n        f(ctx)\n\n    # write all modified data back to virtual filesystem\n    for file in ctx.all_files:\n        ctx.vfs.put(file.path, format_strings_file(file.data))\n\n    if mode == Mode.REPORT:\n        if vfs.show_diff():\n            sys.exit(1)\n    else:\n        if vfs.show_diff():\n            vfs.commit()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/update_install_trees",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport re\nfrom collections import defaultdict\nfrom collections.abc import Iterable\nfrom pathlib import Path\nfrom typing import Any\n\nfrom shared.paths import DATA_DIR, DOCS_DIR, PROJECT_PATHS, REPO_DIR\nfrom shared.vfs import VirtualFilesystem\n\nDATA_REPO_ROOT_DIR = REPO_DIR.parent / \"TRX-data\"\n\nIGNORED_FILES = \"\"\"\nmax_stats.cache.json\n\"\"\"\n\nTR1_OG_FILES = \"\"\"\ndata/cut1.phd\ndata/cut2.phd\ndata/cut3.phd\ndata/cut4.phd\ndata/gym.phd\ndata/level1.phd\ndata/level2.phd\ndata/level3a.phd\ndata/level3b.phd\ndata/level4.phd\ndata/level5.phd\ndata/level6.phd\ndata/level7a.phd\ndata/level7b.phd\ndata/level8a.phd\ndata/level8b.phd\ndata/level8c.phd\ndata/level10a.phd\ndata/level10b.phd\ndata/level10c.phd\ndata/title.phd\nfmv/cafe.rpl\nfmv/canyon.rpl\nfmv/core.avi\nfmv/end.rpl\nfmv/escape.rpl\nfmv/lift.rpl\nfmv/mansion.rpl\nfmv/prison.rpl\nfmv/pyramid.rpl\nfmv/snow.rpl\nfmv/vision.rpl\nmusic/track02.flac\nmusic/track03.flac\nmusic/track04.flac\nmusic/track05.flac\nmusic/track06.flac\nmusic/track07.flac\nmusic/track08.flac\nmusic/track09.flac\nmusic/track10.flac\nmusic/track11.flac\nmusic/track12.flac\nmusic/track13.flac\nmusic/track14.flac\nmusic/track15.flac\nmusic/track16.flac\nmusic/track17.flac\nmusic/track18.flac\nmusic/track19.flac\nmusic/track20.flac\nmusic/track21.flac\nmusic/track22.flac\nmusic/track23.flac\nmusic/track24.flac\nmusic/track25.flac\nmusic/track26.flac\nmusic/track27.flac\nmusic/track28.flac\nmusic/track29.flac\nmusic/track30.flac\nmusic/track31.flac\nmusic/track32.flac\nmusic/track33.flac\nmusic/track34.flac\nmusic/track35.flac\nmusic/track36.flac\nmusic/track37.flac\nmusic/track38.flac\nmusic/track39.flac\nmusic/track40.flac\nmusic/track41.flac\nmusic/track42.flac\nmusic/track43.flac\nmusic/track44.flac\nmusic/track45.flac\nmusic/track46.flac\nmusic/track47.flac\nmusic/track48.flac\nmusic/track49.flac\nmusic/track50.flac\nmusic/track51.flac\nmusic/track52.flac\nmusic/track53.flac\nmusic/track54.flac\nmusic/track55.flac\nmusic/track56.flac\nmusic/track57.flac\nmusic/track58.flac\nmusic/track59.flac\nmusic/track60.flac\n\"\"\"\n\nTR1_UB_OG_FILES = \"\"\"\ndata/cat.phd\ndata/egypt.phd\ndata/end.phd\ndata/end2.phd\n\"\"\"\n\nTR2_OG_FILES = \"\"\"\ndata/assault.tr2\ndata/boat.tr2\ndata/catacomb.tr2\ndata/cut1.tr2\ndata/cut2.tr2\ndata/cut3.tr2\ndata/cut4.tr2\ndata/deck.tr2\ndata/emprtomb.tr2\ndata/floating.tr2\ndata/house.tr2\ndata/icecave.tr2\ndata/keel.tr2\ndata/living.tr2\ndata/main.sfx\ndata/monastry.tr2\ndata/opera.tr2\ndata/platform.tr2\ndata/rig.tr2\ndata/skidoo.tr2\ndata/title.tr2\ndata/unwater.tr2\ndata/venice.tr2\ndata/wall.tr2\ndata/xian.tr2\nfmv/ancient.rpl\nfmv/crash.rpl\nfmv/end.rpl\nfmv/jeep.rpl\nfmv/landing.rpl\nfmv/logo.rpl\nfmv/modern.rpl\nfmv/ms.rpl\nmusic/2.mp3\nmusic/3.mp3\nmusic/4.mp3\nmusic/5.mp3\nmusic/6.mp3\nmusic/7.mp3\nmusic/8.mp3\nmusic/9.mp3\nmusic/10.mp3\nmusic/11.mp3\nmusic/12.mp3\nmusic/13.mp3\nmusic/14.mp3\nmusic/15.mp3\nmusic/16.mp3\nmusic/17.mp3\nmusic/18.mp3\nmusic/19.mp3\nmusic/20.mp3\nmusic/21.mp3\nmusic/22.mp3\nmusic/23.mp3\nmusic/24.mp3\nmusic/25.mp3\nmusic/26.mp3\nmusic/27.mp3\nmusic/28.mp3\nmusic/29.mp3\nmusic/30.mp3\nmusic/31.mp3\nmusic/32.mp3\nmusic/33.mp3\nmusic/34.mp3\nmusic/35.mp3\nmusic/36.mp3\nmusic/37.mp3\nmusic/38.mp3\nmusic/39.mp3\nmusic/40.mp3\nmusic/41.mp3\nmusic/42.mp3\nmusic/43.mp3\nmusic/44.mp3\nmusic/45.mp3\nmusic/46.mp3\nmusic/47.mp3\nmusic/48.mp3\nmusic/49.mp3\nmusic/50.mp3\nmusic/51.mp3\nmusic/52.mp3\nmusic/53.mp3\nmusic/54.mp3\nmusic/55.mp3\nmusic/56.mp3\nmusic/57.mp3\nmusic/58.mp3\nmusic/59.mp3\nmusic/60.mp3\nmusic/61.mp3\n\"\"\"\n\nTR2_GM_OG_FILES = \"\"\"\ndata/level1.tr2\ndata/level2.tr2\ndata/level3.tr2\ndata/level4.tr2\ndata/level5.tr2\ndata/main_gm.sfx\ndata/title_gm.tr2\n\"\"\"\n\nTR3_OG_FILES = \"\"\"\ndata/main.sfx\ndata/tombpc.dat\nfmv/crsh_eng.rpl\nfmv/endgame.rpl\nfmv/intr_eng.rpl\nfmv/sail_eng.rpl\nfmv/logo.rpl\naudio/cdaudio.wad\ncuts/cut1.tr2\ncuts/cut2.tr2\ndata/antarc.tr2\ndata/chamber.tr2\ndata/city.tr2\ndata/compound.tr2\ndata/crash.tr2\ndata/house.tr2\ndata/jungle.tr2\ndata/mines.tr2\ndata/nevada.tr2\ndata/office.tr2\ndata/quadchas.tr2\ndata/rapids.tr2\ndata/roofs.tr2\ndata/sewer.tr2\ndata/shore.tr2\ndata/stpaul.tr2\ndata/temple.tr2\ndata/title.tr2\ndata/tonyboss.tr2\ndata/tower.tr2\ndata/triboss.tr2\ncuts/cut3.tr2\ncuts/cut4.tr2\ncuts/cut5.tr2\ncuts/cut6.tr2\ncuts/cut7.tr2\ncuts/cut8.tr2\ncuts/cut9.tr2\ncuts/cut11.tr2\ncuts/cut12.tr2\ndata/area51.tr2\n\"\"\"\n\nTR3_LA_OG_FILES = \"\"\"\ndata/chunnel.tr2\ndata/scotland.tr2\ndata/slinc.tr2\ndata/undersea.tr2\ndata/willsden.tr2\ndata/zoo.tr2\ndata/main_la.sfx\ndata/title_la.tr2\n\"\"\"\n\nOG_FILE_GROUPS = {\n    \"tr1\": tuple(Path(line) for line in TR1_OG_FILES.splitlines() if line),\n    \"tr1-ub\": tuple(Path(line) for line in TR1_UB_OG_FILES.splitlines() if line),\n    \"tr2\": tuple(Path(line) for line in TR2_OG_FILES.splitlines() if line),\n    \"tr2-gm\": tuple(Path(line) for line in TR2_GM_OG_FILES.splitlines() if line),\n    \"tr3\": tuple(Path(line) for line in TR3_OG_FILES.splitlines() if line),\n    \"tr3-la\": tuple(Path(line) for line in TR3_LA_OG_FILES.splitlines() if line),\n}\n\nOG_PREFIX_TARGETS: dict[str, dict[str, Path]] = {\n    \"tr1\": {\n        \"data\": Path(\"games/tr1/levels\"),\n        \"fmv\": Path(\"games/tr1/fmv\"),\n        \"music\": Path(\"games/tr1/music\"),\n    },\n    \"tr1-ub\": {\n        \"data\": Path(\"games/tr1-ub/levels\"),\n    },\n    \"tr2\": {\n        \"data\": Path(\"games/tr2/levels\"),\n        \"fmv\": Path(\"games/tr2/fmv\"),\n        \"music\": Path(\"games/tr2/music\"),\n    },\n    \"tr2-gm\": {\n        \"data\": Path(\"games/tr2-gm/levels\"),\n    },\n    \"tr3\": {\n        \"data\": Path(\"games/tr3/levels\"),\n        \"fmv\": Path(\"games/tr3/fmv\"),\n        \"audio\": Path(\"games/tr3/audio\"),\n        \"cuts\": Path(\"games/tr3/cuts\"),\n    },\n    \"tr3-la\": {\n        \"data\": Path(\"games/tr3-la/levels\"),\n    },\n}\n\nOG_FILE_OVERRIDES: dict[str, dict[Path, Path]] = {\n    \"tr2\": {\n        Path(\"data/main.sfx\"): Path(\"games/tr2/main.sfx\"),\n    },\n    \"tr2-gm\": {\n        Path(\"data/main_gm.sfx\"): Path(\"games/tr2-gm/main.sfx\"),\n        Path(\"data/title_gm.tr2\"): Path(\"games/tr2-gm/levels/title.tr2\"),\n    },\n    \"tr3\": {\n        Path(\"data/main.sfx\"): Path(\"games/tr3/main.sfx\"),\n        Path(\"data/tombpc.dat\"): Path(\"games/tr3/tombpc.dat\"),\n    },\n    \"tr3-la\": {\n        Path(\"data/main_la.sfx\"): Path(\"games/tr3-la/main.sfx\"),\n        Path(\"data/title_la.tr2\"): Path(\"games/tr3-la/levels/title.tr2\"),\n    },\n}\n\nLEGACY_DOC_CONFIGS = [\n    {\n        \"doc\": PROJECT_PATHS[1].docs_dir / \"INSTALLING.md\",\n        \"config\": \"TR1X\",\n        \"ship_roots\": [\n            DATA_DIR / \"common/ship\",\n            PROJECT_PATHS[1].data_dir / \"ship\",\n            DATA_REPO_ROOT_DIR / \"tr1/ship\",\n            DATA_REPO_ROOT_DIR / \"tr1-ub/ship\",\n        ],\n        \"og_groups\": [\"tr1\", \"tr1-ub\"],\n    },\n    {\n        \"doc\": PROJECT_PATHS[2].docs_dir / \"INSTALLING.md\",\n        \"config\": \"TR2X\",\n        \"ship_roots\": [\n            DATA_DIR / \"common/ship\",\n            PROJECT_PATHS[2].data_dir / \"ship\",\n            DATA_REPO_ROOT_DIR / \"tr2/ship\",\n            DATA_REPO_ROOT_DIR / \"tr2-gm/ship\",\n        ],\n        \"og_groups\": [\"tr2\", \"tr2-gm\"],\n    },\n    {\n        \"doc\": PROJECT_PATHS[3].docs_dir / \"INSTALLING.md\",\n        \"config\": \"TR3X\",\n        \"ship_roots\": [\n            DATA_DIR / \"common/ship\",\n            PROJECT_PATHS[3].data_dir / \"ship\",\n            DATA_REPO_ROOT_DIR / \"tr3/ship\",\n        ],\n        \"og_groups\": [\"tr3\", \"tr3-la\"],\n    },\n]\n\nTRX_IMAGE_ROOTS = {\n    \"tr1\": DATA_REPO_ROOT_DIR / \"tr1/ship/data/images\",\n    \"tr1-ub\": DATA_REPO_ROOT_DIR / \"tr1-ub/ship/data/images\",\n    \"tr2\": DATA_REPO_ROOT_DIR / \"tr2/ship/data/images\",\n    \"tr2-gm\": DATA_REPO_ROOT_DIR / \"tr2-gm/ship/data/images\",\n    \"tr3\": DATA_REPO_ROOT_DIR / \"tr3/ship/data/images\",\n}\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Update game strings JSON files to reflect GS_DEFINE() macros.\"\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--dry-run\",\n        action=\"store_true\",\n        help=\"Perform a dry run: show unified diffs instead of writing files.\",\n    )\n    return parser.parse_args()\n\n\ndef collect(root_dir: Path) -> Iterable[Path]:\n    paths = [\n        path.relative_to(root_dir)\n        for path in root_dir.rglob(\"*\", recurse_symlinks=True)\n        if path.is_file() and path.name not in IGNORED_FILES\n    ]\n    return sorted(paths)\n\n\ndef remap_og_path(path: Path, game: str) -> Path | None:\n    override = OG_FILE_OVERRIDES.get(game, {}).get(path)\n    if override is not None:\n        return override\n    if path.parts[0] in OG_PREFIX_TARGETS[game]:\n        return OG_PREFIX_TARGETS[game][path.parts[0]] / path.relative_to(\n            path.parts[0]\n        )\n    return None\n\n\ndef remap_og_paths(paths: Iterable[Path], game: str) -> Iterable[Path]:\n    for path in paths:\n        mapped = remap_og_path(path, game)\n        if mapped is not None:\n            yield mapped\n\n\ndef collect_many(root_dirs: Iterable[Path]) -> Iterable[Path]:\n    for root_dir in root_dirs:\n        yield from collect(root_dir)\n\n\ndef iter_og_groups(group_names: Iterable[str]) -> Iterable[Path]:\n    for group_name in group_names:\n        yield from OG_FILE_GROUPS[group_name]\n\n\ndef iter_trx_image_paths() -> Iterable[Path]:\n    for game, root_dir in TRX_IMAGE_ROOTS.items():\n        for path in collect(root_dir):\n            yield Path(\"games\") / game / \"images\" / path\n\n\ndef build_legacy_win_paths(config: dict[str, Any]) -> Iterable[Path]:\n    yield Path(\"TRX.exe\")\n    yield Path(f\"cfg/{config['config']}.json5*\")\n    yield from iter_og_groups(config[\"og_groups\"])\n    yield from collect_many(config[\"ship_roots\"])\n\n\ndef build_legacy_mac_paths(config: dict[str, Any]) -> Iterable[Path]:\n    yield Path(\"Contents/_CodeSignature\")\n    yield Path(\"Contents/Frameworks\")\n    yield Path(\"Contents/info.plist\")\n    yield Path(\"Contents/MacOS\")\n    yield from (\n        Path(\"Contents/Resources\") / sub\n        for sub in collect_many(config[\"ship_roots\"])\n    )\n    yield from (\n        Path(\"Contents/Resources\") / sub\n        for sub in iter_og_groups(config[\"og_groups\"])\n    )\n    yield Path(\"Contents/Resources/icon.icns\")\n\n\ndef build_trx_win_paths() -> Iterable[Path]:\n    yield Path(\"TRX.exe\")\n    yield Path(\"cfg/shell.json5*\")\n    yield Path(\"cfg/TR1X.json5*\")\n    yield Path(\"cfg/TR2X.json5*\")\n    yield Path(\"cfg/TR3X.json5*\")\n    yield from collect(DATA_DIR / \"trx/ship\")\n    yield from iter_trx_image_paths()\n    for group_name in OG_FILE_GROUPS:\n        yield from remap_og_paths(OG_FILE_GROUPS[group_name], group_name)\n\n\ndef build_doc_trees() -> Iterable[tuple[Path, str, list[Path]]]:\n    for config in LEGACY_DOC_CONFIGS:\n        yield (\n            config[\"doc\"],\n            \"win\",\n            list(build_legacy_win_paths(config)),\n        )\n        yield (\n            config[\"doc\"],\n            \"mac\",\n            list(build_legacy_mac_paths(config)),\n        )\n    yield (DOCS_DIR / \"trx/INSTALLING.md\", \"win\", list(build_trx_win_paths()))\n\n\n\n\ndef make_tree_string(paths: list[Path]) -> str:\n    type Node = defaultdict[Any, Any]\n\n    # build nested dict\n    def tree() -> Node:\n        return defaultdict(tree)\n\n    root = tree()\n    for path in paths:\n        node = root\n        for part in str(path).split(\"/\"):\n            node = node[part]\n\n    def natural_sort_key(s: str) -> list[str | int]:\n        return [\n            int(text) if text.isdigit() else text.lower()\n            for text in re.split(\"([0-9]+)\", s)\n        ]\n\n    # recursive render\n    def render(node: Node, prefix: str = \"\") -> list[str]:\n        entries = sorted(\n            node.keys(),\n            key=lambda name: (0 if node[name] else 1, natural_sort_key(name)),\n        )\n        lines = []\n        for i, name in enumerate(entries):\n            last = i == len(entries) - 1\n            branch = \"└── \" if last else \"├── \"\n            subprefix = \"    \" if last else \"│   \"\n            lines.append(prefix + branch + name)\n            if node[name]:\n                lines.extend(render(node[name], prefix + subprefix))\n        return lines\n\n    return \".\\n\" + \"\\n\".join(render(root))\n\n\ndef process_install_doc(content: str, platform: str, paths: list[Path]) -> str:\n    result: list[str] = []\n    lines = content.splitlines()\n    while lines:\n        line = lines.pop(0)\n        if line == f'<details data-id=\"file-tree-{platform}\">':\n            result.append(line)\n            while lines:\n                line = lines.pop(0)\n                if not line:\n                    break\n            result.append(\n                \"<pre><code>\" + make_tree_string(paths) + \"</code></pre>\"\n            )\n            result.append(\"</details>\")\n            if lines:\n                result.append(line)\n        else:\n            result.append(line)\n    return \"\\n\".join(result) + \"\\n\"\n\n\ndef main() -> None:\n    args = parse_args()\n    vfs = VirtualFilesystem()\n\n    for path, platform, paths in build_doc_trees():\n        vfs.put(\n            path, process_install_doc(vfs.get(path), platform, paths)\n        )\n\n    if args.dry_run:\n        vfs.show_diff()\n    else:\n        vfs.commit()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tools/update_water_colors",
    "content": "#!/usr/bin/env -S uv run --script\n#\n# /// script\n# requires-python = \">=3.14\"\n# dependencies = [\"pyyaml\"]\n# ///\n\nfrom pathlib import Path\nfrom typing import Any\n\nimport yaml\n\nROOT = Path(__file__).resolve().parent.parent\nYAML_PATH = ROOT / \"docs\" / \"trx\" / \"water_colors.yml\"\nMD_PATH = ROOT / \"docs\" / \"trx\" / \"WATER_COLORS.md\"\n\n\ndef format_array(hex_color: str) -> str:\n    values = [int(hex_color[i : i + 2], 16) / 255.0 for i in (1, 3, 5)]\n    formatted = \", \".join(\n        f\"{value:.3f}\".rstrip(\"0\").rstrip(\".\") for value in values\n    )\n    return f\"[{formatted}]\"\n\n\ndef split_group_name(group_name: str) -> tuple[str, str]:\n    parts = group_name.rsplit(\" \", 1)\n    if len(parts) == 2 and parts[1] in {\"DOS\", \"PC\", \"PS1\"}:\n        return parts[0], parts[1]\n    return group_name, \"\"\n\n\ndef build_table(data: dict[str, Any]) -> list[str]:\n    group_rows: list[tuple[str, str, list[dict[str, Any]]]] = []\n\n    for group_name, entries in data.items():\n        if group_name in {\"name\", \"order\"}:\n            continue\n        assert isinstance(entries, list)\n        game_name, platform_name = split_group_name(group_name)\n        group_rows.append((game_name, platform_name, entries))\n\n    lines: list[str] = []\n    table_start_lines: list[str] = [\n        \"<table>\",\n        \"<thead>\",\n        \"    <tr>\",\n        \"        <th>Platform</th>\",\n        \"        <th>Color</th>\",\n        \"        <th>Color&nbsp;(array)</th>\",\n        \"        <th>Usage</th>\",\n        \"    </tr>\",\n        \"</thead>\",\n        \"<tbody>\",\n    ]\n    table_end_lines: list[str] = [\"</tbody>\", \"</table>\"]\n\n    current_game_name = None\n    for game_name, platform_name, entries in group_rows:\n        if game_name != current_game_name:\n            if current_game_name is not None:\n                lines.extend(table_end_lines)\n                lines.append(\"\")\n            lines.append(f'<h4 colspan=\"4\">{game_name}</h4>')\n            lines.extend(table_start_lines)\n            current_game_name = game_name\n\n        rowspan = len(entries)\n        for i, entry in enumerate(entries):\n            color = entry[\"color\"]\n            usage = entry[\"name\"]\n            platform_cell = (\n                f'        <td rowspan=\"{rowspan}\">{platform_name}</td>'\n                if i == 0\n                else None\n            )\n            lines.append(\"    <tr>\")\n            if platform_cell is not None:\n                lines.append(platform_cell)\n            lines.append(\n                f'        <td><img src=\"https://dummyimage.com/20x20/{color[1:].lower()}/{color[1:].lower()}.png\" width=\"20\" height=\"20\" alt=\"{color}\" valign=\"middle\"/> <code>{color}</code></td>'\n            )\n            lines.append(\n                f\"        <td><code>{format_array(color)}</code></td>\"\n            )\n            lines.append(f\"        <td>{usage}</td>\")\n            lines.append(\"    </tr>\")\n\n    lines.extend(table_end_lines)\n    return lines\n\n\ndef main() -> None:\n    data = yaml.safe_load(YAML_PATH.read_text())\n    lines = [\n        \"---\",\n        f\"title: {data['name']}\",\n        f\"order: {data['order']}\",\n        \"---\",\n        \"\",\n        f\"# {data['name']}\",\n        \"\",\n        *build_table(data),\n        \"\",\n    ]\n    MD_PATH.write_text(\"\\n\".join(lines))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  }
]